需要实现的主要功能如下:
资讯列表、标签页切换,文章举报,频道管理、文章详情、阅读记忆,关注功能、点赞功能、评论功能、回复评论、搜索功能、登录功能、个人中心、编辑资料、小智同学 ...
今天要实现的功能主要是:权限处理,401页面的处理,个人中心,编辑资料
1. 路由守卫
在router/index.js中补充一个前置路由守卫。业务逻辑有两条是:
- 如果没有登陆就不能访问某些敏感页面
- 如果登陆了,就不能再访问login页面
import store from '@/store/index.js'
const vipList = ['/setting']
router.beforeEach((to, from, next) => {
const token = store.state.tokenInfo.token
console.log(token)
if (!token && vipList.includes(to.path)) {
console.log('没有权限,不能访问vip页面')
next('/login?backto=' + encodeURIComponent(to.fullPath))
} else if (token && to.path === '/login') {
console.log('已经登录了,就不能访问login了')
next('/')
} else {
next()
}
})
export default router
测试效果
在没有token的情况,直接在地址栏中访问setting将无法访问,而会跳回login页面
2 页面跳转-登陆成功之后回到初始页面
在src\utils\request.js中,补充响应拦截器。
import router from '../router/auth.js'
// 响应拦截器
request.interceptors.response.use(function (response) {
console.log('响应拦截器', response)
return response
}, async function (error) {
// 如果发生了错误,判断是否是401
console.dir(error)
if (error.response.status === 401) {
// 开始处理
console.log('响应拦截器-错误-401')
const refreshToken = store.state.tokenInfo.refresh_token
// if (有refresh_token) {
if (refreshToken) {
// 1. 请求新token
try {
const res = await axios({
url: 'http://localhost:8000/v1_0/authorizations',
method: 'PUT',
headers: {
Authorization: `Bearer ${refreshToken}`
}
})
console.log('请求新token', res.data.data.token)
// 2. 保存到vuex
store.commit('mSetToken', {
refresh_token: refreshToken,
token: res.data.data.token
})
// 3. 重发请求
// request是上面创建的axios的实例,它会自动从vuex取出token带上
return request(error.config)
} catch (error) {
// 清除token
store.commit('mSetToken', {})
// 去到登录页(如果有token值,就不能到login)
const backtoUrl = encodeURIComponent(router.currentRoute.fullPath)
router.push('/login?backto=' + backtoUrl)
return Promise.reject(error)
}
} else {
// 去到登录页
// 清除token
store.commit('mSetToken', {})
const backtoUrl = encodeURIComponent(router.currentRoute.fullPath)
router.push('/login?backto=' + backtoUrl)
return Promise.reject(error)
}
} else {
return Promise.reject(error)
}
})
3 个人中心
3.1 布局
在src/views/setting/setting.vue中
<template>
<div class="user">
<!-- 用户个人资料 -->
<div class="user-profile">
<div class="info">
<van-image round src="https://img.yzcdn.cn/vant/cat.jpeg" />
<h3 class="name">
用户名
<br />
<van-tag size="mini">申请认证</van-tag>
</h3>
</div>
<van-row>
<van-col span="8">
<p>0</p>
<p>动态</p>
</van-col>
<van-col span="8">
<p>0</p>
<p>关注</p>
</van-col>
<van-col span="8">
<p>0</p>
<p>粉丝</p>
</van-col>
</van-row>
</div>
<!-- 操作链接 -->
<van-row class="user-links">
<van-col span="8">
<van-icon name="newspaper-o" color="#7af" />我的作品
</van-col>
<van-col span="8">
<van-icon name="star-o" color="#f00" />我的收藏
</van-col>
<van-col span="8">
<van-icon name="tosend" color="#fa0" />阅读历史
</van-col>
</van-row>
<!-- 编辑入口
is-link: 产生一个向右的箭头
to: 用来做路由的跳转
-->
<van-cell-group class="user-group">
<van-cell icon="edit" title="编辑资料" to="/setting/profile" is-link />
<van-cell icon="chat-o" title="小智同学" to="/setting/chat" is-link />
<van-cell icon="setting-o" title="系统设置" is-link />
<van-cell icon="warning-o" title="退出登录" to="/login" is-link />
</van-cell-group>
</div>
</template>
<script>
export default {
name: 'setting',
data () {
return {
}
}
}
</script>
<style lang="less" scoped>
.user {
.user-profile {
width: 100%;
height: 200px;
display: block;
background: #3296fa;
color: #fff;
.info {
display: flex;
padding: 20px;
align-items: center;
.van-image{
width: 64px;
height: 64px;
}
.name {
font-size: 16px;
font-weight: normal;
margin-left: 10px;
}
.van-tag {
background: #fff;
color: #3296fa;
}
}
p{
margin: 0;
text-align: center;
}
}
.user-group {
margin-bottom: 15px;
}
.user-links {
padding: 15px 0;
font-size: 12px;
text-align: center;
background-color: #fff;
.van-icon {
display: block;
font-size: 24px;
padding-bottom: 5px;
}
}
}
</style>
3.2 隐藏顶部导航
修改layout.vue中的nav-bar的显示 : 如果在/setting页面中, 则隐藏顶部的搜索导航区域
<!-- 顶部logo搜索导航区域
如果当前是在"我的" 页面,则不要出现搜索导航区域
只有当前页面不是“我的”,它才显示出来
-->
<van-nav-bar
fixed
v-show="cIsVisiable"
>
computed: {
cIsVisiable () {
// 只有在个人中心页才不可见
return this.$route.path !== '/setting'
},
...mapGetters(['isLogin'])
}
测试效果
3.3 个人中心-数据渲染
在 src/api/user.js 中定义接口文档
/**
* 获取用户自已的信息
*/
export const getInfo = () => {
return request({
method: 'GET',
url: 'v1_0/user'
})
}
在src\views\setting\setting.vue中调用
import { getInfo } from '@/api/user.js'
data () {
return {
userInfo: {}
}
},
created () {
this.loadUserInfo()
},
methods: {
async loadUserInfo () {
const res = await getInfo()
console.log(res)
this.userInfo = res.data.data
}
}
<!-- 用户个人资料 -->
<div class="user-profile">
<div class="info">
<van-image round
:src="userInfo.photo" />
<h3 class="name">
{{userInfo.name}}
<br />
<van-tag size="mini">申请认证</van-tag>
</h3>
</div>
<van-row>
<van-col span="8">
<p>{{userInfo.art_count}}</p>
<p>动态</p>
</van-col>
<van-col span="8">
<p>{{userInfo.follow_count}}</p>
<p>关注</p>
</van-col>
<van-col span="8">
<p>{{userInfo.fans_count}}</p>
<p>粉丝</p>
</van-col>
</van-row>
</div>
查看效果
3.3 个人中心-退出登录
用户点击退出时,弹出确认框,确认退出之后:
- 回到登陆页
- 清空本地登陆信息 在组件
view/settging /setting.vue中, 绑定事件
<!-- 用户退出 -->
<van-cell icon="warning-o" title="退出登录"
+ @click="hQuit"
is-link />
// 用户退出
hQuit () {
// 1. 是否确认要退出
// 2. 清空token,userInfo
// 3. 回去login
this.$dialog.confirm({
title: '系统提示',
message: '再多玩一会嘛'
}).then(() => {
this.$store.commit('getToken', {})
localStorage.removeItem('token')
this.$router.push('/login')
}).catch(() => {
// on cancel
})
}
4 编辑个人资料-创建组件并配置路由
4.1 结构
创建src/views/setting/profile.vue
<template>
<div class="container">
<!-- 导航条 -->
<van-nav-bar
left-arrow
@click-left="$router.back()"
title="编辑资料">
</van-nav-bar>
<!-- 编辑区 -->
<van-cell-group>
<van-cell is-link title="头像" center>
<van-image
slot="default"
width="1.5rem"
height="1.5rem"
fit="cover"
round
:src="userInfo.photo"
/>
</van-cell>
<!-- value: 设置右侧显示的文字 -->
<van-cell is-link title="姓名" :value="userInfo.name" @click="isShowName=true"/>
<van-cell is-link title="性别" :value="userInfo.gender === 1?'男':'女'" @click="isShowGender=true"/>
<van-cell is-link title="生日" :value="userInfo.birthday" @click="isShowBirthday=true"/>
</van-cell-group>
</div>
</template>
<script>
export default {
name: 'userProfile',
data () {
return {
// 控制弹层
isShowName: false,
isShowGender: false,
isShowBirthday: false,
// 当前用户的信息
userInfo: { name: '张三' },
// 修改后的新名字
newName: '',
// 修改后新生日
newDate: new Date(),
minDate: new Date(1965, 0, 10), // dateTime-picker中最小时间
maxDate: new Date() // 当前时间
}
}
}
</script>
4.2 添加路由
{
path: '/setting/profile',
name: 'settingProfile',
component: () => import('../views/setting/profile.vue')
}
4.3 测试效果
4.4 编辑个人资料-数据渲染-从vuex中获取数据
import { mapState } from 'vuex'
export default {
name: 'userProfile',
data () {
return {
// 控制弹层
isShowName: false,
isShowGender: false,
isShowBirthday: false,
// 修改后的新名字
newName: '',
// 修改后新生日
newDate: new Date(),
minDate: new Date(1965, 0, 10), // dateTime-picker中最小时间
maxDate: new Date() // 当前时间
}
},
created () {
// 调用带命名空间的modules中的action
this.$store.dispatch('user/getProfile')
},
computed: {
// ...mapState(命名空间名, ['state名字'])
...mapState('user', ['userInfo'])
}
}
模板
<van-cell-group>
<van-cell is-link title="头像" center>
<van-image
slot="default"
width="1.5rem"
height="1.5rem"
fit="cover"
round
:src="userInfo.photo"
/>
</van-cell>
<van-cell is-link title="名称" :value="userInfo.name" @click="isShowName=true"/>
<van-cell is-link title="性别" :value="userInfo.gender === 1?'男':'女'" @click="isShowGender=true"/>
<van-cell is-link title="生日" :value="userInfo.birthday" @click="isShowBirthday=true"/>
</van-cell-group>
5 编辑个人资料-姓名
5.1 添加弹层结构
<!-- 修改名字 -->
<van-dialog
v-model="isShowName"
title="修改名字"
show-cancel-button
@confirm="hSaveName">
<van-field v-model.trim="newName" />
</van-dialog>
5.2 封装接口api
/**
* 编辑用户信息
* data: {
* name,
* gender,
* birthday
* }
*/
export const updateUserInfo = (data) => {
return request({
method: 'PATCH',
url: 'v1_0/user/profile',
data
})
}
5.3 准备muation
mUpdateUserName (state, newName) {
state.userInfo.name = newName
},
5.4 准备action
// 修改用户名
async updateUserName (context, newName) {
try {
await updateUserInfo({
name: newName
})
context.commit('mUpdateUserName', newName)
} catch (err) {
// console.log(err)
throw new Error(err)
}
},
5.5 调用
在view/setting/profile.vue中,调用上面的接口,实现功能
// 修改名字
async hSaveName () {
if (this.newName === '') {
return
}
try {
await this.$store.dispatch('user/updateUserName', this.newName)
this.$toast.success('修改成功')
} catch (err) {
this.$toast.fail('修改失败')
console.log(err)
}
},
6 编辑个人资料-性别
6.1 加入弹层结构
<!-- 修改性别 -->
<van-popup v-model="showGender" position="bottom">
<van-nav-bar title="修改性别" left-text="取消" @click-left="showGender=false">
</van-nav-bar>
<van-cell title="女" @click="hSaveGender(0)" is-link></van-cell>
<van-cell title="男" @click="hSaveGender(1)" is-link></van-cell>
</van-popup>
6.2 准备muation
mUpdateGender (state, newGender) {
state.userInfo.gender = newGender
}
6.3 调用并实现功能
// 修改性别
async hSaveGender (gender) {
try {
await updateUserInfo({ gender })
// 在本地更新vuex
this.$store.commit('user/mUpdateGender', gender)
// 关闭弹层
this.isShowGender = false
this.$toast.success('操作成功')
} catch (err) {
console.log(err)
this.$toast.fail('操作失败')
}
}
7 编辑个人资料-生日
7.1 加入弹层结构
<!-- 修改生日 -->
<van-popup v-model="isShowBirthday" position="bottom">
<van-nav-bar title="修改生日"> </van-nav-bar>
<van-datetime-picker
v-model="newDate"
type="date"
title="选择年月日"
@cancel="isShowBirthday = false"
@confirm="hSaveBirthday"
:min-date="minDate"
:max-date="maxDate"
/>
</van-popup>
7.2 准备muation
// 修改生日
mUpdateUserBirthday (state, newBirthday) {
state.userInfo.birthday = newBirthday
},
7.3 封装时间过滤器函数
在utils/formateDate.js中加入
export const formatDate = (dateTime) => {
const date = new Date(dateTime) // 转换成Data();
var y = date.getFullYear()
var m = date.getMonth() + 1
m = m < 10 ? '0' + m : m
var d = date.getDate()
d = d < 10 ? ('0' + d) : d
return y + '-' + m + '-' + d
}
7.4 引入并实现代码
// 修改生日
async hSaveBirthday () {
// 组件中提供的时间是标准时间,后端接口中需要是年-月-日的格式,
// 要进行转换
const birthday = formatDate(this.newDate)
try {
await updateUserInfo({ birthday })
// 1. 更新到vuex中
this.$store.commit('user/mUpdateUserBirthday', birthday)
// 2. 关闭弹层
this.isShowBirthday = false
this.$toast.success('修改成功')
} catch (err) {
console.log(err)
this.$toast.fail('修改失败')
}
},
8 编辑个人资料-头像
编辑个人头像,本质就是从本地上传一张新的图片到服务器,以替换之前头像文件。
8.1 在前面加入上传文件input
<van-cell-group>
+ <input type="file" @change="hFileChange" />
<van-cell is-link title="头像" center>
<van-image
slot="default"
width="1.5rem"
height="1.5rem"
fit="cover"
round
:src="userInfo.photo"
/>
</van-cell>
8.2 封装接口api
/**
* 修改用户头像
*
* obj: 以formData格式保存参数
*/
/**
* 更新用户头像
* @param {*} fd 头像
*/
export const updateUserPhoto = fd => {
return request({
method: 'PATCH',
url: 'v1_0/user/photo',
data: fd
})
}
8.3 补充mutation和action
import { getProfile, updateUserInfo, updateUserPhoto } from '@/api/user.js'
mutations: {
mUpdateUserPhoto (state, newPhoto) {
state.userInfo.photo = newPhoto
}
},
actions: {
// 修改头像
async updateUserPhoto (context, fd) {
try {
const res = await updateUserPhoto(fd)
console.log('updateUserPhoto', res)
context.commit('mUpdateUserPhoto', res.data.data.photo)
} catch (err) {
console.log(err)
throw new Error(err)
}
}
}
8.4在回调中去上传头像
// 文件域选中文件之后,自动会传递事件对象给change的回调
// e.target: input对
// e.target.files // 选中的文件的集合
// e.target.files[0] // 选中的第一个文件
async hFileChange (e) {
const file = e.target.files[0]
if (!file) {
// 用户没有选中
return
}
// 做文件上传
console.dir(file)
// ajax文件上传
// 1. 封装FormData对象
// 2. append参数
const fd = new FormData()
fd.append('photo', file)
try {
await this.$store.dispatch('user/updateUserPhoto', fd)
this.$toast.success('操作成功')
} catch (err) {
this.$toast.fail('操作失败')
}
}
8.5 优化:隐藏文件域
<!-- 当用户在文件域中进行选择时,会触发change事件
hidden: 隐藏。它用来隐藏input元素。元素将不可见,也不占用页面的空间
ref="file": 添加ref为了通过引用来访问这个dom元素 -->
<input type="file"
+ hidden
@change="hFileChange"
+ref="file"/>
<van-cell is-link title="头像" center @click="hClickImage">
// 用户在头像上点击,希望也弹出文件选择框
// 移花接木
// 用户点击了头像: 理解他要更新头像,此时 弹出input type="file"
hClickImage () {
// 找到这个引用,并直接调用click()
this.$refs.file.click()
}