目录
1.部分页面效果
2.环境
3.本项目遇到的知识点
3.1.长度尺寸单位vw
3.2 token鉴权
3.3.个人中心
3.4.路由守卫
3.5.axios部分设置
3.6.css穿透scoped修改子组件样式
3.7.video标签使用
3.8.父组件中子组件标签上添加点击事件
3.9.组件递归嵌套
3.10.vantUI使用(非按需引入)
登录
首页
文章详情
视频详情
win7+vue.js+h5+vantUI
使用vw实现页面尺寸适配
1vw 相当于1%的屏幕宽度
1vh 相当于1%的屏幕高度
如果用vw就永远只用宽作为标准,那么就可以指定所有的相对尺寸,因为vw有一个固定的参照物(宽度)
元素像素 / 设计稿宽度 * 100 = 以vw 为单位的长度数据
可以利用vscode插件px-to-vw自动转换px为vw,输入像素按alt+z即可转换
token和用户id放入本地存储,发送请求是在请求头传入
// 本地储存
localStorage.setItem('token',res.data.data.token);
localStorage.setItem('userId',res.data.data.user.id);
//headers 请求头中带上 ( JWT标准 json web token)
this.$axios({
url,
method,
headers: {
Authorization: "Bearer " + localStorage.getItem('token')
}
}).then(res => {
console.log(res.data);
})
切换class
通过判断性别来切换不同样式class
{{obj.has_follow?'已关注':'关注'}}
.heightlight{
color:#fff !important;
background-color: #ffc0cb !important;
}
切换页面
$router.push()把路由添加到最后一位
@click="$router.push({name:'editInfo'})"
$router.replace()把路由替换到最后一位
this.$router.replace('/login');
路由守卫可以理解为页面跳转之前的拦截手段,可以在拦截时判断验证用户信息
全局的前置路由守卫
- to 目标路由
- from 来源路由
- next 放行函数
这里通过前置路由守卫来对页面的跳转进行拦截,当判断需要鉴权的页面没有token时,则跳转到登录页,有token则放行
// 路由守卫
// 全局前置路由守卫,会在所有路由发生变化之前进行拦截
// 调用 router 的 beforeEach 方法
// router.beforeEach() 拦截所有跳转,跳转之前都会执行一个函数,我们要作为参数传进去
router.beforeEach((to,from,next)=>{
// //写法一 直接判断 to.name
// if (to.name == 'personalPage' || to.name == 'editPage') {
// // 另外一种写法,是将需要校验的路由封装在一个数组中
// var pageNeedAuth = [
// 'editPage',
// 'personalPage'
// ]
// // 如果在这个数组中能够找到 to.name 就证明需要校验
// if(pageNeedAuth.indexOf(to.name) >= 0) {
// 第三种判断方式, 直接在路由配置时, 添加 meta 数据
// 就可以在这里进行判断
if(to.meta.auth){
const token = localStorage.getItem('token');
if(token){
next();
}else{
router.push('/login')
}
}else{
next();
}
});
meta 在routes中自定义数据
这里在路由中使用meta来设置需要鉴权的页面,辅助路由守卫鉴权
const routes = [
{path:'/login',component:Login},
{path:'/register',component:Register},
{path:'/',component:index},
{name:'myFollow',path:'/myFollow',component:MyFollow,meta:{auth:true}},
{name:'personalCenter',path:'/personalCenter',component:PersonalCenter,meta:{auth:true}},
{name:'editInfo',path:'/editInfo',component:EditInfo,meta:{auth:true}},
{name:'channel',path:'/channel',component:Channel},
{name:'search',path:'/search',component:Search},
{name:'myCollection',path:'/myCollection',component:myCollection},
{name:'newsDetail',path:'/newsDetail/:id',component:NewsDetail},
{name:'myPost',path:'/myPost',component:MyPost},
{name:'funnyComment',path:'/funnyComment/:id',component:FunnyComment}
]
3.5.1.axios默认基准路径
提取请求的统一的服务器域名的部分进行抽离设置
在main.js中设置axios.defaults.baseURL
axios.defaults.baseURL = 'http://127.0.0.1:3000';
3.5.2.axios请求拦截器
可拦截所有请求,进行需要的配置
这里是拦截请求,统一在请求头添加token
// 设置axios请求拦截器
axios.interceptors.request.use(config=>{
// 判断是否有Authorization和token,如果两者都有则做用户验证
if(!config.headers.Authorization && localStorage.getItem('token')){
config.headers.Authorization = 'Bearer ' + localStorage.getItem('token');
}
return config;
})
3.5.3.axios响应拦截器
axios.interceptors.response.use(), 可以接受一个回调函数做参数, 带有响应数据 res
拦截器拦截数据, 处理后必须放行 (return)
这里拦截所有请求的响应数据,将错误数据匹配出来弹出提示,之后每个请求可以不再单独做错误处理
// 设置响应拦截器 (统一处理响应回来的数据)
// axios.interceptors.response.use() 这个函数可以拦截到所有请求的响应,并执行逻辑
// 我们需要将逻辑函数作为参数传入
// 在入口文件,也就是组件外部,要使用 toast 需要单独引入
axios.interceptors.response.use(res=>{
const {statusCode,message} = res.data;
const {url} = res.config;//获取接口
let urlArr = ['/login'];//这里的接口不走拦截器
if(!urlArr.includes(url)){
// 对用户信息验证失败情况进行处理
if(statusCode && statusCode == 401 && message == '用户信息验证失败'){
// 处理错误,在入口文件如果想要使用 vant ui 弹出窗口、
// 这里没有 this 也没有 $toast
// 可以使用单独引入的方式,只使用 Toast
Toast.fail('用户信息验证失败,请重新登录');
localStorage.removeItem('token');
localStorage.removeItem('userId');
router.replace('/login');
}else if(statusCode && statusCode.toString().startsWith('4',0)){
Toast.fail(message);
}
}
return res;
},(err)=>{
// 暂不处理
Toast.fail('请联系相关人员处理');
console.log(err);
})
lang="less" 书写less样式
scoped 在当前作用域有效
父组件想修改子组件的样式,或者想修改v-html指令中的html代码(v-html不受父组件scoped css的影响)
在需要穿透子组件的样式前端添加 /deep/
注意:如果是sass,/deep/可能不生效,可以使用 >>> 进行代替
/deep/ img{
max-width: 100%;
}
- video标签样式适配,一般宽度100%,高度自适应
- 使用 controls 属性控制播放器的按钮显示
- 使用 poster 实现图片功能(一般视频未播放前会展示一张图片), 在播放前显示图像吸引用户
- video常用的js事件:video.play() 播放 video.pause()暂停
直接在子组件上加点击事件会不生效,这时在点击事件上带后缀.native即可生效
效果图
3.9.1.回复跟帖(直接回复第一层评论)
这里不涉及递归
涉及单文件组件:NewsDetail.vue(文章详情)
涉及子组件:Comment.vue(帖子),ReplyArea.vue(回复区域)
思路:帖子中包含的信息(要回复的帖子的id,帖子的用户昵称)通过点击回复传给文章详情页面,详情页面再父传子传给回复区域,回复区域调起输入框,并将用户昵称展示在输入框的placeholder中。总结:帖子(子)=(parentInfo)=》文章详情(父)==>回复区域(子)
帖子子组件中html代码:
回复
帖子子组件中js代码:
this.$emit('parentCallReply')触发文章详情中的parentCallReply事件
// 这是主评论的回复事件
// 主评论由于不参与递归,单独对待
//以下两个方法分别都去触发文章详情的parentCallReply的事件
callReply(){
this.$emit('parentCallReply',{
id:this.mainCommentItem.id,
nickname:this.mainCommentItem.user.nickname
})
},
文章详情(父组件)中的html代码:
文章详情(父组件)中的js代码:
parentInfo为帖子的信息,这里通过$refs.ReplyArea调起回复区域的输入框
callReply(parentInfo){
// 从这里传给输入框的子组件
this.parentInfo = parentInfo;
// 调起输入框子组件的输入框显示事件 事件带括号
this.$refs.ReplyArea.showTextarea();
}
回复区域(子组件)中的html代码:
回复区域(子组件)的js代码:
接收文章详情的传值
props:['parentInfo','hasStar','commemtCount'],
根据parentInfo变更输入框placeholder的值
// 计算属性
computed:{
// 通过判断是否有昵称来变更placeholder的值
placeholderTxt(){
if(this.parentInfo.nickname){
return '回复@' + this.parentInfo.nickname;
}else{
return '写跟贴';
}
}
},
发送评论逻辑处理
//自己发帖只需要传文章id和帖子内容
sendReply(){
this.$axios({
// 文章id直接通过地址栏获取
url:'/post_comment/'+this.$route.params.id,
method:'post',
data:{
content:this.content,
parent_id:this.parentInfo.id?this.parentInfo.id:''
}
}).then(res => {
this.$emit('loadDetail');
this.content = '';
this.$toast(res.data.message);
})
},
3.9.2.回复跟帖中的评论(组件递归嵌套)
页面展示效果(平行):
内部组件为递归
想象中的递归:
涉及单文件组件:NewsDetail.vue(文章详情)
涉及子组件:Comment.vue(帖子),ReplyArea.vue(回复区域),ParentComment.vue(递归组件,Comment.vue 的 子组件)
思路:点击ParentComment.vue(递归子组件)中的回复一层一层往外传值(parentInfo),直到传到文章详情,文章详情再传给回复区域,递归组件自己点击自己,自己给自己传值。
ParentComment.vue(递归组件)中的html代码:
点击ParentComment中的回复,调用parentCallReply方法
{{commentData.user.nickname}}
{{commentData.create_date ? commentData.create_date.split('T')[0]:''}}
回复
{{commentData.content}}
ParentComment.vue(递归组件)中的js代码:
parentCallReply方法触发上一层的parentCallReply方法并把本层的评论信息带上(上一层的parentCallReply依然是本层),上一层的parentCallReply方法再调起diguiCallReply方法,diguiCallReply方法再调起上上一层的parentCallReply,在点击回复的当前层上,依次向上将本层数据传出(这个过程不断自己调用自己),直到传到文章详情,后续步骤和3.9.1一致。
注意:本组件内使用自己的组件,通过声明本组件的name值作为标签来做本组件中使用
name:'ParentComment',//声明当前组件的 name 来作为标签来使用
传送门:https://youzan.github.io/vant/#/zh-CN/
3.10.1.vantUI引入
# 通过 npm 安装
npm i vant -S
在main.js中引入全部组件
// 1.导入组件库
import Vant from 'vant';
// 2.引入对应的css文件
import 'vant/lib/index.css';
// 3.注册组件
Vue.use(Vant);
3.10.2.Toast(轻提示)
//没有图标样式,纯提示
this.$toast(message);
//成功提示
this.$toast.success(message);
//失败提示
this.$toast.fail(message);
3.10.3.Dialog(弹出框)和field(输入框组件)
3.10.4.ActionSheet(动作面板)
actions:[
{ name: '男', gender:'1'},
{ name: '女',gender:'0' }
]
3.10.5.Uploader(文件上传)
v-bind 形式传入一个 :after-read
在 methods 当中定义这个函数, 就是处理图片上传逻辑的函数
这个函数自动接收一个参数 fileObj
使用 ajax + FormData 对象上传图片即可
uploadImg(fileObj){
// 会接受一个参数 fileObj 文件对象
// 其中文件就在 fileObj.file
// 先将图片转成二进制的参数格式
let formdata = new FormData();
formdata.append('file',fileObj.file);
this.$axios({
url:'/upload',
method:'post',
data:formdata
}).then(res=>{
if(res.data.message == '文件上传成功'){
this.editUserInfo({'head_img':res.data.data.url});
}
})
}
3.10.6.Tab(标签页)/ List(列表)
标签页 v-model 是当前激活的栏目索引
List列表实现无限加载效果(分页)
该方法获取分类,获取数据后将每一个分类的所有文章数据(空数组,尚未去获取)初始化好,并添加每一个分类的文章要使用的分页
getCategory(){
this.$axios({
url:'/category',
methods:'get'
}).then(res=>{
// 这里是为了在获取栏目造好数据结构,以便方便文章数据存储在里面,这样再次点击栏目时可以从
// 该数据结构中直接获取文章列表,而不需要再次发送请求获取
const newData = res.data.data.map(category => {
// 将原返回数据中对象与其他数据封装成一个对象返回
return{
...category,
postList:[],
pageIndex:1,
pageSize:5,
// 是否正在加载
loading:false,
// 是否已经全部加载完毕
finished:false
}
});
this.categoryList = newData;
if(this.categoryList.length > 0){
// 打开页面时获取热点文章数据
this.getPost();
}
})
},
获取文章
getPost(){
const currentCategoryList = this.categoryList[this.active];
this.$axios({
url:'/post',
method:'get',
params:{
category: currentCategoryList.id,
pageIndex: currentCategoryList.pageIndex,
pageSize: currentCategoryList.pageSize
}
}).then(res=>{
// 获取文章后, 不应该放入公共 data 中的 postList
// 而是放入当前激活栏目 的 postList
// 以前我们获取文章列表之后, 直接将之前的数组替换掉
// 其实我们需要的是, 下一页的内容跟上一页拼接起来
// 展开,合并
this.categoryList[this.active].postList = [...currentCategoryList.postList,...res.data.data];
// 这里加载完了文章列表数据, 然后需要手动将当前栏目的加载状态改回 false 也就是没有正在加载
// 这样子才能在下次拉到底的时候重新触发加载下一页
currentCategoryList.loading = false;
// 如果这一页数据的数量少于页容量,则加载结束,数据已全部加载完
if(res.data.data.length < currentCategoryList.pageSize){
currentCategoryList.finished = true;
}
})
},
loadMore(){
// 读取更多文章, 实际上
// 就是将当前栏目的 pageIndex 加一
// 发送文章获取请求即可
console.log('加载下一页');
this.categoryList[this.active].pageIndex +=1;
this.getPost();
},
监听分类的切换,并判断有没有获取过文章
watch:{
active(){
// 如果这个栏目之前没有获取过文章列表,那么此时就去获取一次
if(this.categoryList[this.active].postList.length == 0){
this.getPost();
}
}
},