5、页面开发
删除App.vue的默认样式,删除id为nav下的about和home
1、前端公告板功能实现
App.vue 加入container
router/index.js,删除about组件
views/home 添加公告板
- views/home
<template>
<div>
<!--添加公告栏-->
<!-- 此处box会显示一个小白快-->
<div class="box">
🔔 {{billborad.content}}
</div>
</div>
</template>
<script>
import {getBillboard} from "@/api/billboard";
export default {
name: 'Home',
data() {
return {
billborad: {
content: ''
}
}
},
//在页面开始加载的时候
created(){
//请求后台方法
this.fetchBillborad()
},
methods: {
//定义异步方法
async fetchBillborad(){
//接收到服务端返回来的value
getBillboard().then((value) => {
const { data } = value
this.billborad = data;
})
}
}
}
</script>
小问题:
Vue项目 @路径提示Module is not installed
- api.billboard.js
import request from '@/utils/request'
export function getBillboard() {
return request({
url: '/billboard/show',
method: 'get'
})
}
billboard提供数据
前后端联调
前端开始npm run serve,后端也开启
然后出现
这是由于前端是localhost:8080和后端localhost:8081,端口号不一致产生的跨域问题,去后端设置跨域,对应后端【8、跨域问题】
2、每日一句功能实现
接下我们继续完成每日一句的实现
在完成每日一句之前,我们利用bulma帮我们完成框架布局搭建3:1
链接:https://bulma.io/documentation/columns/basics/
代码:
<div class="columns">
<div class="column">
First column
</div>
<div class="column">
Second column
</div>
<div class="column">
Third column
</div>
<div class="column">
Fourth column
</div>
</div>
- 在views/Home.vue中,将下面部分分成3:1
<template>
<div>
<!--添加公告栏-->
<!-- 此处box会显示一个小白快-->
<div class="box">
🔔 {{billborad.content}}
</div>
<div class="columns is-three-quarters">
<div class="column">
</div>
<div class="column">
</div>
</div>
</div>
</template>
<script>
import {getBillboard} from "@/api/billboard";
export default {
name: 'Home',
data() {
return {
billborad: {
content: ''
}
}
},
//在页面开始加载的时候
created(){
//请求后台方法
this.fetchBillborad()
},
methods: {
//定义异步方法
async fetchBillborad(){
//接收到服务端返回来的value
getBillboard().then((value) => {
const { data } = value
this.billborad = data;
})
}
}
}
</script>
- 新建views/card侧边栏相关组件
CardBar.vue
<template>
<div>
CardBar
</div>
</template>
<script>
export default {
name: 'CardBar',
data() {
return {
}
},
//在页面开始加载的时候
created(){
},
methods: {
}
}
</script>
- 新建views/post帖子相关组件
TopicList.vue
<template>
<div>
帖子列表
</div>
</template>
<script>
export default {
name: 'TopicList',
data() {
return {
}
},
//在页面开始加载的时候
created(){
},
methods: {
}
}
</script>
在Home.vue中引入
<template>
<div>
<!--添加公告栏-->
<!-- 此处box会显示一个小白快-->
<div class="box">
🔔 {{billborad.content}}
</div>
<div class="columns">
<div class="column is-three-quarters">
<!--3、使用组件-->
<TopicList></TopicList>
</div>
<div class="column">
<CardBar></CardBar>
</div>
...
//1、侧边栏
import CardBar from "@/views/card/CardBar";
//帖子相关
import TopicList from "@/views/post/TopicList"
export default {
name: 'Home',
//2、
components: {
CardBar,TopicList
},
...
测试
利用elementui对我们的侧边栏进行美化:
https://element-plus.gitee.io/#/zh-CN/component/card
<template>
<div>
<!--是否登录-->
<LoginWelcome></LoginWelcome>
<!--今日赠言-->
<Tip></Tip>
<!--资源推送-->
<Promotion></Promotion>
</div>
</template>
<script>
import LoginWelcome from "@/views/card/LoginWelcome";
import Tip from "@/views/card/Tip";
import Promotion from "@/views/card/Promotion";
export default {
name: 'CardBar',
components:{
LoginWelcome,Tip,Promotion
},
data() {
return {
}
},
//在页面开始加载的时候
created(){
},
methods: {
}
}
</script>
- LoginWelcome.vue
<template>
<el-card class="box-card" shadow="never">
<div slot="header">
<span>❀ 发帖</span>
</div>
<div>
body
</div>
</el-card>
</template>
<script>
export default {
name: 'LoginWelcome',
data() {
return {
}
},
//在页面开始加载的时候
created(){
},
methods: {
}
}
</script>
- Promotion.vue
<template>
<el-card class="box-card" shadow="never">
<div slot="header">
<span>🍻 推广</span>
</div>
<div>
body
</div>
</el-card>
</template>
...
- Tip.vue
<template>
<el-card class="box-card" shadow="never">
<!--通过slot分发,向组件内部指定位置传递内容-->
<div slot="header">
<span>😊 每日一句</span>
</div>
<div>
<div class="has-text-left block">
十个指头按不住十个跳骚
</div>
<!--block帮我们实现了块之间的间隙,md-5(内容外边距加5px)不生效-->
<div class="has-text-right block">
---傣族
</div>
</div>
</el-card>
</template>
...
上述三个组件,script重复部分我这边就没列出来了
上述样式可能会存在重复使用的情况,这边就,建立一个公共css
- assets/app.css
/*margin和padding全部清0,初始化*/
* {
margin: 0;
padding: 0;
}
body,
html {
background-color: #f6f6f6;
color: black;
width: 100%;
font-size: 14px;
/*字体间隔*/
letter-spacing: 0.03em;
font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC,
Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei,
sans-serif, Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji,
Segoe UI Symbol, Android Emoji, EmojiSymbols;
}
/*每个el-card添加下外边距*/
.el-card {
margin-bottom: 16px;
}
在main.js中引用
//引入全局样式
import '@/assets/app.css'
连接后台api
1、修改tip.vue
<template>
<el-card class="box-card" shadow="never">
<!--通过slot分发,向组件内部指定位置传递内容-->
<div slot="header">
<span>😊 每日一句</span>
</div>
<div>
<div class="has-text-left block">
{{tip.content}}}
</div>
<!--block帮我们实现了块之间的间隙,md-5(内容外边距加5px)不生效-->
<div class="has-text-right block">
---{{tip.author}}
</div>
</div>
</el-card>
</template>
<script>
export default {
name: 'Tip',
data() {
return {
tip:{}
}
},
//在页面开始加载的时候
created(){
},
methods: {
}
}
</script>
2、创建api/tip.js,帮助实现request请求
import request from '@/utils/request'
export function getTodayTip() {
return request({
url: '/tip/today',
method: 'get'
})
}
3、定义异步请求
tip.vue
<template>
<el-card class="box-card" shadow="never">
<!--通过slot分发,向组件内部指定位置传递内容-->
<div slot="header">
<span>😊 每日一句</span>
</div>
<div>
<div class="has-text-left block">
{{tip.content}}
</div>
<!--block帮我们实现了块之间的间隙,md-5(内容外边距加5px)不生效-->
<div class="has-text-right block">
---{{tip.author}}
</div>
</div>
</el-card>
</template>
<script>
import {getTodayTip} from "@/api/tip";
export default {
name: 'Tip',
data() {
return {
tip:{}
}
},
//在页面开始加载的时候
created(){
//请求后台方法
this.fetchTodayTip()
},
methods: {
//定义异步方法
async fetchTodayTip(){
//接收到服务端返回来的value
getTodayTip().then((value) => {
const { data } = value
this.tip = data;
})
}
}
}
</script>
4、测试
3、广告推广实现
1、api/promotion.js,完成http请求
import request from '@/utils/request'
export function getlist() {
return request({
url: '/promotion/list',
method: 'get'
})
}
2、views/card/Promotion.vue
<template>
<el-card class="box-card" shadow="never">
<div slot="header">
<span>🍻 推广</span>
</div>
<div>
<!--v-for实现循环,绑定key,vue要求我们给每个组件加上key,方便定位,block每个元素会有一定间距-->
<p v-for="(item,index) in list" :key="index" class="block">
<!--_blank用新页面打开-->
<a :href="item.link" target="_blank">{{ item.title }}</a>
</p>
</div>
</el-card>
</template>
<script>
import {getlist} from "@/api/promotion";
export default {
name: 'Promotion',
data() {
return {
list:[]
}
},
//在页面开始加载的时候
created(){
//请求后台方法
this.fetchList()
},
methods: {
//定义异步方法
async fetchList(){
//接收到服务端返回来的value
getlist().then((value) => {
console.log(value)
const { data } = value
this.list = data;
})
}
}
}
</script>
4、404页面
1、定义页面
error/404.vue
<template>
<div class="columns mt-6">
<div class="columns mt-6">
<div class="mt-6">
<p class="content">UH OH! 页面丢失</p>
<p class="content subtitle mt-6">
您所寻找的页面不存在,{{ times }}秒后,将返回首页!
</p>
</div>
</div>
</div>
</template>
<script>
import {getlist} from "@/api/promotion";
export default {
name: "404",
data() {
return {
times: 10
}
},
//在页面开始加载的时候
created(){
//请求后台方法
this.goHome()
},
methods: {
//定时器
goHome: function () {
this.timer = setInterval(() =>{
this.times--
if (this.times === 0){
//清空定时器
clearInterval(this.timer)
//页面跳转
this.$router.push({path:'/'})
}
},1000)
}
}
}
</script>
<style scoped>
</style>
2、路由
- router/index.js
const routes = [
...
{
path: '/404',
name: '404',
//改成动态引入
component: () => import('@/views/error/404'),
//从meta元数据中读取title
meta:{title: '404-NotFound'}
},
// 如果用户输入的不是上述路由,则重定向
{
path: '*',
redirect: '/404',
//隐藏
hidden: true
}
]
...
5、用户登录、注册
1、注册的api请求
api/auth/auth.js
import request from '@/utils/request'
export function userRegister(userDTO) {
return request({
url: '/ums/user/register',
method: 'post',
data: userDTO
})
}
2、页面
views/auth/Register.vue
<template>
<div class="columns py-6">
<div class="column is-half is-offset-one-quarter">
<el-card shadow="never">
<div slot="header" class="has-text-centered has-text-weight-bold">
新用户入驻
</div>
<div>
<el-form v-loding="loading" :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="账户" prop="name">
<el-input type="text" v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="ruleForm.pass" placeholder="请选择输入密码" type="password"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="password">
<el-input v-model="ruleForm.checkPass" placeholder="请选择输入密码" type="password"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input type="email" v-model="ruleForm.email"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">立即注册</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</div>
</div>
</template>
<script>
<!--引入注册api请求-->
import {userRegister} from "@/api/auth/auth";
export default {
//用户注册中,防止用户重新注册,需等待后台回应后再进行下步操作
loading: false,
name: "Register",
data(){
var validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'));
} else {
if (this.ruleForm.checkPass !== '') {
this.$refs.ruleForm.validateField('checkPass');
}
callback();
}
};
var validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== this.ruleForm.pass) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
};
return{
ruleForm: {
name: '',
pass: '',
checkPass: '',
email: ''
} ,
rules:{
name: [
{ required: true, message: '请输入账户', trigger: 'blur' },
{ min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
],
pass: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' },
{ validator: validatePass, trigger: 'blur' }
],
checkPass: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{ validator: validatePass2, trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }
]
}
}
},
methods: {
submitForm(formName) {
//校验信息
this.$refs[formName].validate((valid) => {
if (valid) {
this.loading = true
//服务器校验
userRegister(ths.ruleForm)
.then((value) =>{
const {code,message} = value
if(code == 200){
this.$message({
message: '账号注册成功',
type: 'success'
})
//账号注册成功后,启动定时器
setTimeout(() =>{
this.loading = false
this.$router.push({
path: this.redirect || '/login'
})
},0.1*1000)
}else{
this.$message.error('注册失败,'+message)
}
})
//最后解开loading
.catch(()=>{
this.loading = false
})
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
<style scoped>
</style>
3、路由
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
//改成动态引入
component: () => import('@/views/Home')
},
{
path: '/register',
name: 'Register',
//改成动态引入
component: () => import('@/views/auth/Register'),
meta:{title: '注册'}
}
...
6、用户登录
引入vuex ,存放组件的信息,便于各个组件的读取,有点像全局数据,但是router时存放在内存中的,我们为了方便下次登录时候记住用户的一些信息:比如token或者暗黑模式
安装js-cookie
npm install js-cookie
配置jscookie
- utils/token.js
import Cookies from 'js-cookie'
const uToken = 'u_token'
const darkMode = 'dark_mode'
//获取Token
export function getToken() {
return Cookies.get(uToken)
}
//设置Token,1天,与后端同步
export function setToken(token) {
return Cookies.set(uToken,token,{expires:1})
}
//删除Token
export function removeToken() {
return Cookies.remove(uToken)
}
export function removeAll() {
return Cookies.removeAll()
}
//设置暗黑模式
export function setDarkMode(mode) {
return Cookies.set(darkMode,mode,{expires:365})
}
//获取暗黑模式
export function getDarkMode(mode) {
return Cookies.get(darkMode)
}
用户登录
- views/auth/Login.vue
<template>
<div class="columns py-6">
<div class="column is-half is-offset-one-quarter">
<el-card shadow="never">
<div slot="header" class="has-text-centered has-text-weight-bold">
用户登录
</div>
<div>
<el-form v-loading="loading" :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="账户" prop="name">
<el-input type="text" v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="密码" prop="pass">
<el-input v-model="ruleForm.pass" placeholder="请选择输入密码" type="password"></el-input>
</el-form-item>
<el-form-item label="记住" prop="delivery">
<el-switch v-model="ruleForm.rememberMe"></el-switch>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data(){
return{
//用户注册中,防止用户重新注册,需等待后台回应后再进行下步操作
loading: false,
redirect: undefined,
ruleForm: {
name: '',
pass: '',
rememberMe: true
} ,
rules:{
name: [
{ required: true, message: '请输入账户', trigger: 'blur' },
{ min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
],
pass: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' },
]
}
}
},
methods: {
submitForm(formName) {
//校验信息
this.$refs[formName].validate((valid) => {
if (valid) {
this.loading = true
//向vue的store发送请求
this.$store
//指定modules:user,login方法
.dispatch("user/login", this.ruleForm)
.then(() =>{
this.$message({
message: '恭喜你,登录成功',
type: 'success',
duration: 2000
})
//账号登录成功后,启动定时器
setTimeout(() =>{
this.loading = false
this.$router.push({
path: this.redirect || '/'
})
},0.1*1000)
})
//最后解开loading
.catch(()=>{
this.loading = false
})
} else {
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
}
</script>
<style scoped>
</style>
分析上述代码,用户发起请求
submitForm(formName) {
//校验信息
this.$refs[formName].validate((valid) => {
if (valid) {
this.loading = true
loading是为了在请求后台的时候,解决进入等待状态 而不是可以随意点
if (valid)这里时根据rules规则,在浏览器端校验,成功后向vue的store发送请求
//向vue的store发送请求
this.$store
//指定modules:user,login方法
.dispatch("user/login", this.ruleForm)
dispatch方法
第一个参数:user
会调用store下的index.js中modules中的元素user
- store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from "@/store/modules/user";
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
//将数据模块化分类
user
}
})
user/login会调用user中的login方法
- store/moudles/user.js
import { login } from '@/api/auth/auth'
import { getToken, setToken } from '@/utils/token'
//定义全局数据
const state = {
token: getToken(), // token
user: '', // 用户对象
}
//state必须通过mutations改变,类似java中的 set
const mutations = {
SET_TOKEN_STATE: (state, token) => {
state.token = token
}
}
//mutations不能接受异步请求,后台发过来的异步请求放在actions中处理
const actions = {
// 用户登录
login({ commit }, userInfo) {
console.log(userInfo)
const { name, pass, rememberMe } = userInfo
return new Promise((resolve, reject) => {
login({ username: name.trim(), password: pass, rememberMe: rememberMe }).then(response => {
const { data } = response
//放在vuex的store下
commit('SET_TOKEN_STATE', data.token)
//放在cookie下
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
},
}
export default {
namespaced: true,
state,
mutations,
actions
}
actions方法第一个login是方法定义,供外部调用.dispatch("user/login", this.ruleForm)
第二个login是
- api/auth/auth.js
import request from '@/utils/request'
//前台用户登录
export function login(userDTO) {
return request({
url: '/ums/user/login',
method: 'post',
data: userDTO
})
}
配置路由
- router/index.js
,
{
path: '/login',
name: 'Login',
//改成动态引入
component: () => import('@/views/auth/Login'),
meta:{title: '登录'}
},
7、 前边侧边栏:马上入驻
-
views/card/LoginWelcome
<template> <el-card class="box-card" shadow="never"> <div slot="header"> <span>❀ 发帖</span> </div> <div v-if="token != null && token !== ''" class="has-text-centered"> <b-button type="is-danger" tag="router-link" :to="{path:'/post/create'}" outlined> ✍表达想法 </b-button> </div> <div v-else class="has-text-centered"> <b-button type="is-primary" tag="router-link" :to="{path:'/register'}" outlined> 马上入驻 </b-button> <b-button type="is-danger" tag="router-link" :to="{path:'/login'}" outlined class="ml-2"> 社区登录 </b-button> </div> </el-card> </template> <script> import { mapGetters } from 'vuex' export default { name: 'LoginWelcome', computed: { //可以使用store下的token ...mapGetters([ 'token' ]) }, data() { return { } }, //在页面开始加载的时候 created(){ }, methods: { } } </script>
前面用户登录,通过js-cookie将用户信息token存放在cookie中,我们前端通过判断cookie的值是否存在,来变化前端侧边的信息
上述
import { mapGetters } from 'vuex'
export default {
name: 'LoginWelcome',
computed: {
//可以使用store下的token
...mapGetters([
'token'
])
},
获取store下的token代码比较固定,
接下来就是要将配置获取token的方法,这里通过
- store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from "@/store/modules/user";
import getters from "@/store/getters";
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
//将数据模块化分类
user
},
getters
})
export default store
加上getters就可以,
getters的定义
-
store/getters.js
const getters = { //state =>箭头函数 token: state => state.user.token, //token user: state => state.user.user, //用户对象 } export default getters
其中state调用modules的user,
页面不带token的情况
页面带token的情况
如果删除掉cookie里面的u_token,再次刷新页面,我们的从getters获取token,getters又获取user.js中的token,该token是调用了js-cookie中的getToken方法,会发现我们删除了cookie中的数据
//定义全局数据
const state = {
token: getToken(), // token
8、前端在axios请求拦截中在请求头加入jwt
1、在用户登录的时候 ,加入代码,让前端发送请求给后台,请求用户信息
- views/auth/Login
methods: {
submitForm(formName) {
//校验信息
this.$refs[formName].validate((valid) => {
if (valid) {
this.loading = true
//向vue的store发送请求
this.$store
//指定modules:user,login方法
.dispatch("user/login", this.ruleForm)
.then(response =>{
console.log(response)
this.$message({
message: response.message,
type: 'success',
duration: 3000
})
//登录成功后,获取用户信息,存在store
this.$store.dispatch("user/getInfo")
同样通过this.$store.dispatch("user/getInfo"),store发送,
定义getInfo方法
- store/modules/user
1、action中加方法
// 获取用户信息
getInfo({ commit }) {
return new Promise((resolve, reject) => {
getUserInfo()
.then(response => {
const { data } = response
console.log(data)
if (!data){
commit('SET_TOKEN_STATE', '')
commit('SET_USER_STATE', '')
removeToken()
resolve()
reject('Verification failed,please Login again')
}
//放在vuex的store下
commit('SET_USER_STATE', data)
resolve(data)
})
//指定发生错误时的回调函数。
.catch(error => {
reject(error)
})
})
}
2、mutations中加函数
SET_USER_STATE: (state, user) => {
state.user = user
}
改变后的user.js
import { login ,getUserInfo} from '@/api/auth/auth'
import { getToken, setToken ,removeToken} from '@/utils/token'
import da from "element-ui/src/locale/lang/da";
//定义全局数据
const state = {
token: getToken(), // token
user: '', // 用户对象
}
//state必须通过mutations改变,类似java中的 set
const mutations = {
SET_TOKEN_STATE: (state, token) => {
state.token = token
},
SET_USER_STATE: (state, user) => {
state.user = user
}
}
//mutations不能接受异步请求,后台发过来的异步请求放在actions中处理
const actions = {
// 用户登录
login({ commit }, userInfo) {
const { name, pass, rememberMe } = userInfo
return new Promise((resolve, reject) => {
login({ username: name.trim(), password: pass, rememberMe: rememberMe })
.then(response => {
const { data } = response
//放在vuex的store下
commit('SET_TOKEN_STATE', data.token)
//放在cookie下
setToken(data.token)
resolve(response)
})
//指定发生错误时的回调函数。
.catch(error => {
reject(error)
})
})
},
// 获取用户信息
getInfo({ commit }) {
return new Promise((resolve, reject) => {
getUserInfo()
.then(response => {
const { data } = response
console.log(data)
if (!data){
commit('SET_TOKEN_STATE', '')
commit('SET_USER_STATE', '')
removeToken()
resolve()
reject('Verification failed,please Login again')
}
//放在vuex的store下
commit('SET_USER_STATE', data)
resolve(data)
})
//指定发生错误时的回调函数。
.catch(error => {
reject(error)
})
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
此时我们向后台发送请求的时候,没有带Authorization信息,我们希望带上Authorization信息,后台就可以识别我们,并给我们进行授权等后续操作
- utils/request.js 加入如下代码
import { getToken } from '@/utils/token'
// 2.请求拦截器request interceptor
service.interceptors.request.use(
config => {
// 发请求前做的一些处理,数据转化,配置请求头,设置token,设置loading等,根据需求去添加
// 注意使用token的时候需要引入cookie方法或者用本地localStorage等方法,推荐js-cookie
if (store.getters.token) {
// config.params = {'token': token} // 如果要求携带在参数中
// config.headers.token = token; // 如果要求携带在请求头中
// bearer:w3c规范
config.headers['Authorization'] = 'Bearer ' + getToken()
}
return config
},
error => {
// do something with request error
// console.log(error) // for debug
return Promise.reject(error)
}
)
测试
我们希望前端登录成功之后,后台可以返回给我们用户信息
- views/auth/Login.vue
向后端请求用户信息
submitForm(formName) {
//校验信息
this.$refs[formName].validate((valid) => {
if (valid) {
this.loading = true
//向vue的store发送请求
this.$store
//指定modules:user,login方法
.dispatch("user/login", this.ruleForm)
.then(response =>{
console.log(response)
this.$message({
message: response.message,
type: 'success',
duration: 3000
})
//登录成功后,获取用户信息
this.$store.dispatch("user/getInfo")
...
用的是store,发送请求,因为这块用户信息可能会被其他模块用到
getInfo的定义
- store/modules/user.js
import { login ,getUserInfo} from '@/api/auth/auth'
import { getToken, setToken ,removeToken} from '@/utils/token'
import da from "element-ui/src/locale/lang/da";
//定义全局数据
const state = {
token: getToken(), // token
user: '', // 用户对象
}
//state必须通过mutations改变,类似java中的 set
const mutations = {
SET_TOKEN_STATE: (state, token) => {
state.token = token
},
SET_USER_STATE: (state, user) => {
state.user = user
}
}
//mutations不能接受异步请求,后台发过来的异步请求放在actions中处理
const actions = {
...
// 获取用户信息
getInfo({ commit }) {
return new Promise((resolve, reject) => {
getUserInfo()
.then(response => {
const { data } = response
console.log(data)
if (!data){
commit('SET_TOKEN_STATE', '')
commit('SET_USER_STATE', '')
removeToken()
resolve()
reject('Verification failed,please Login again')
}
//放在vuex的store下
commit('SET_USER_STATE', data)
resolve(data)
})
//指定发生错误时的回调函数。
.catch(error => {
reject(error)
})
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
login方法
- api/auth/auth.js
import request from '@/utils/request'
...
//登录后获取前台用户信息
export function getUserInfo() {
return request({
url: '/ums/user/info',
method: 'get',
})
}
上述引入了request,我们需要在request里面加入封装后的用户信息
- utils
import axios from 'axios'
import { Message, MessageBox } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/token'
// 1.创建axios实例,vue请求后台
const service = axios.create({
// 公共接口--这里注意后面会讲,url = base url + request url
baseURL: process.env.VUE_APP_SERVER_URL,
// baseURL: 'https://api.example.com',
// 超时时间 单位是ms,这里设置了5s的超时时间
timeout: 5 * 1000
})
// 2.请求拦截器request interceptor
service.interceptors.request.use(
config => {
// 发请求前做的一些处理,数据转化,配置请求头,设置token,设置loading等,根据需求去添加
// 注意使用token的时候需要引入cookie方法或者用本地localStorage等方法,推荐js-cookie
if (store.getters.token) {
// config.params = {'token': token} // 如果要求携带在参数中
// config.headers.token = token; // 如果要求携带在请求头中
// bearer:w3c规范
config.headers['Authorization'] = 'Bearer ' + getToken()
}
return config
},
error => {
// do something with request error
// console.log(error) // for debug
return Promise.reject(error)
}
)
// 设置cross跨域 并设置访问权限 允许跨域携带cookie信息,使用JWT可关闭
service.defaults.withCredentials = false
// 3.请求拦截器response interceptor
service.interceptors.response.use(
// 接收到响应数据并成功后的一些共有的处理,关闭loading等
response => {
const res = response.data
// 如果自定义代码不是200,则将其判断为错误。
if (res.code !== 200) {
// 50008: 非法Token; 50012: 异地登录; 50014: Token失效;
if (res.code === 401 || res.code === 50012 || res.code === 50014) {
// 重新登录
MessageBox.confirm('会话失效,您可以留在当前页面,或重新登录', '权限不足', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
center: true
}).then(() => {
window.location.href = '#/login'
})
} else { // 其他异常直接提示
Message({
showClose: true,
message: '⚠' + res.message || 'Error',
type: 'error',
duration: 3 * 1000
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
/** *** 接收到异常响应的处理开始 *****/
Message({
showClose: true,
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
config.headers['Authorization'] = 'Bearer ' + getToken()
我们将用户信息放在请求头的Authorization属性中,并加上W3C规范,注意加了空格,后台取到请求头之后也需要反过来解析
9、实现暗黑模式、页头
使用现成的库
安装darkereader
npm stall darkereader
在components下新建Loyout文件夹代表我们的布局
<template>
<header class="header has-background-white has-text-black">
<b-navbar class="container is-white" :fixed-top="true">
<template slot="brand">
<b-navbar-item tag="div">
<img :src="doubaoImg" alt="logo"/>
</b-navbar-item>
<!--is-hidden-desktop PC端隐藏,手机端显示-->
<b-navbar-item
class="is-hidden-desktop"
tag="router-link"
:to="{ path: '/' }"
>
主页
</b-navbar-item>
</template>
<template slot="start">
<b-navbar-item
tag="router-link"
:to="{ path: '/' }"
>
🌐 主页
</b-navbar-item>
</template>
<template slot="end">
<b-navbar-item tag="div">
<b-field position="is-centered">
<b-input
v-model="searchKey"
class="s_input"
width="80%"
placeholder="搜索帖子、标签和用户"
rounded
clearable
@keyup.enter.native="search()"
/>
<p class="control">
<b-button
class="is-info"
@click="search()"
>检索
</b-button>
</p>
</b-field>
</b-navbar-item>
<b-navbar-item tag="div">
<b-switch
v-model="darkMode"
passive-type="is-warning"
type="is-dark"
>
{{ darkMode ? "夜" : "日" }}
</b-switch>
</b-navbar-item>
<b-navbar-item
v-if="token == null || token === ''"
tag="div"
>
<div class="buttons">
<b-button
class="is-light"
tag="router-link"
:to="{ path: '/register' }"
>
注册
</b-button>
<b-button
class="is-light"
tag="router-link"
:to="{ path: '/login' }"
>
登录
</b-button>
</div>
</b-navbar-item>
<b-navbar-dropdown
v-else
:label="user.alias"
>
<b-navbar-item
tag="router-link"
:to="{ path: `/member/${user.username}/home` }"
>
🧘 个人中心
</b-navbar-item>
<hr class="dropdown-divider">
<b-navbar-item
tag="router-link"
:to="{ path: `/member/${user.username}/setting` }"
>
⚙ 设置中心
</b-navbar-item>
<hr class="dropdown-divider">
<b-navbar-item
tag="a"
@click="logout"
> 👋 退出登录
</b-navbar-item>
</b-navbar-dropdown>
</template>
</b-navbar>
</header>
</template>
<script>
import { disable as disableDarkMode, enable as enableDarkMode } from 'darkreader'
import { getDarkMode, setDarkMode } from '@/utils/token'
import { mapGetters } from 'vuex'
export default {
name: "Header",
data() {
return {
logoUrl: require('@/assets/logo.png'),
doubaoImg: require('@/assets/img/doubao.png'),
searchKey: '',
darkMode: false
}
},
computed: {
...mapGetters(['token', 'user'])
},
watch:{
// 监听Theme模式
darkMode(val) {
if (val) {
enableDarkMode({})
} else {
disableDarkMode()
}
setDarkMode(this.darkMode)
}
},
//组件刚开始加载的时候
created() {
// 获取cookie中的夜间还是白天模式
this.darkMode = getDarkMode()
if (this.darkMode) {
enableDarkMode({})
} else {
disableDarkMode()
}
},
methods: {
}
}
</script>
<style scoped>
input {
width: 80%;
height: 86%;
}
</style>
我们通过监听darkMode属性的改变值,取做到切换暗黑模式,暗黑模式是借用
darkreader工具实现的,
v-if="token == null || token === ''"
这里会判断token是否存在
token是从store中取得的,store中的token是从cookie中取得的
import { mapGetters } from 'vuex'
computed: {
...mapGetters(['token', 'user'])
}
我们需要在App.vue中引入头部
<template>
<div id="app">
//3
<div class="mb-5">
<Header></Header>
</div>
<!--引入buefy的container样式-->
<div class="container">
<router-view/>
</div>
</div>
</template>
<script>
//1
import Header from "@/components/Layout/Header";
export default {
name: "App",
//2
components:{
Header
}
}
</script>
<style>
</style>
我们登录页面
刷新页面
发现用户信息不见了,原因在于我们每一次刷新,
Header.vue都会取调用
...mapGetters(['token', 'user'])
通过getters.js
const getters = {
//state =>箭头函数
token: state => {
return state.user.token//token
},
user: state => state.user.user, //用户对象
}
export default getters
获得用户信息,但是在user.js中
//定义全局数据
const state = {
token: getToken(), // token
user: '', // 用户对象
}
token每次都可以重新请求获得,但是user被重置为空了,问题出在这,我们获取的时机不对
我们是在登录Login.vue通过
//登录成功后,获取用户信息
this.$store.dispatch("user/getInfo")
获取的,但是这个只执行了一次,我们应该在每次刷新的时候取执行请求用户信息
新建src.permission.js,解决用户名消失的问题
import router from './router'
import store from './store'
import getPageTitle from '@/utils/get-page-title'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'
import {getToken} from "@/utils/token"; // progress bar style
NProgress.configure({showSpinner: false}) // NProgress Configuration
router.beforeEach(async (to, from, next) => {
// start progress bar
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
// determine whether the user has logged in
const hasToken = getToken();
if (hasToken) {
if (to.path === '/login') {
// 登录,跳转首页
next({path: '/'})
NProgress.done()
} else {
// 获取用户信息
await store.dispatch('user/getInfo')
next()
}
} else {
next()
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
新建src/utils/get-page-title.js
const title = '小而美的智慧社区系统'
export default function getPageTitle(pageTitle) {
if (pageTitle) {
return `${pageTitle} - ${title}`
}
return `${title}`
}
安装nprogress,这是告诉用户刷新页面进度提示的小工具
最后在main.js中引入permission,我们再刷新,发现解决了
10、退出登录
components/layout/Header.vue添加logout方法
async logout() {
this.$store.dispatch("user/logout").then(() => {
this.$message.info("退出登录成功")
setTimeout(() => {
this.$router.push({path: this.redirect || '/'})
}, 500)
})
},
store/modules/user.js
// 用户退出
logout({ commit }) {
return new Promise((resolve, reject) => {
logout(state.token)
.then(response => {
console.log(response)
//放在vuex的store下
commit('SET_TOKEN_STATE', "")
commit('SET_USER_STATE', "")
removeToken()
resolve()
})
//指定发生错误时的回调函数。
.catch(error => {
reject(error)
})
})
},
api/auth/auth.js
//前提用户注销
export function logout() {
return request({
url: '/ums/user/logout',
method: 'get',
})
}
测试
出现这个问题 是因为我们当前就在首页,但是logout还是请求跳转到/,为了解决这个问题
我们在router/index.js中加入
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch((err) => err);
};
就解决了
11、页脚
- components/layout/Footer.vue
<template>
<footer class="footer has-text-grey-light has-background-grey-darker">
<div class="container">
<div class="">
<span>简洁、实用、美观</span>
<span style="float: right">
<router-link :to="{path:'/admin/login'}">
管理员登录
</router-link>
|
<a href="/?lang=zh_CN">中文</a> |
<a href="/?lang=en_US">English</a>
</span>
</div>
<div>
<span>{{ title }} ALL RIGHTS RESERVED</span>
<div style="float: right">
<template>
<b-taglist attached>
<b-tag type="is-dark" size="is-normal">Design</b-tag>
<b-tag type="is-info" size="is-normal">{{ author }}</b-tag>
</b-taglist>
</template>
</div>
</div>
</div>
<back-top></back-top>
</footer>
</template>
<script>
import BackTop from "@/components/BackTop/BackTop";
export default {
name: "Footer",
components: {
BackTop
},
data() {
return {
title: "© " + new Date().getFullYear() + ' Ergou',
author: 'Ergou',
};
},
};
</script>
<style scoped>
footer {
margin-top: 120px;
height: 150px;
}
footer a{
color: #bfbfbf;
}
</style>
- components/BackTop/BackTop.vue
<template>
<el-backtop :bottom="60" :right="60">
<div title="回到顶部"
style="{
height: 100%;
width: 100%;
background-color: #f2f5f6;
box-shadow: 0 1px 0 0;
border-radius: 12px;
text-align: center;
line-height: 40px;
color: #167df0;
}"
>
<i class="fa fa-arrow-up"></i>
</div>
</el-backtop>
</template>
<script>
export default {
name: "BackTop"
}
</script>
<style scoped>
</style>
- 在app.vue中引入
<template>
<div id="app">
<div class="mb-5">
<Header></Header>
</div>
<!--引入buefy的container样式-->
<div class="container">
<router-view/>
</div>
<div >
<Footer></Footer>
</div>
</div>
</template>
<script>
import Header from "@/components/Layout/Header";
import Footer from "@/components/Layout/Footer";
export default {
name: "App",
components:{
Header,
Footer
}
}
</script>
<style>
</style>
12、帖子列表
1、安装dayjs,帮助我们完成时间的格式化
npm install dayjs
2、添加列表的请求工具
- src/api/post.js
import request from '@/utils/request'
// 列表
export function getList(pageNo, size, tab) {
return request(({
url: '/post/list',
method: 'get',
params: { pageNo: pageNo, size: size, tab: tab }
}))
}
//分页参数,pageNo 页号 size 每页多少条 tab 主题:最新或者最热
3、mian.js加入dayjs
- src/main.js
//引入全局样式
import '@/assets/app.css'
import '@/permission'
import relativeTime from 'dayjs/plugin/relativeTime';
//国际化
import 'dayjs/locale/zh-cn'
const dayjs = require('dayjs')
//相对时间插件
dayjs.extend(relativeTime)
dayjs.locale('zh-cn') // use locale globally
dayjs().locale('zh-cn').format() // use locale in a specific instance
Vue.prototype.dayjs = dayjs;//可以全局使用dayjs
4、修改index.js文件
- src/views/post/TopicList.vue
<template>
<div>
<el-card shadow="never">
<div slot="header" >
<!--标签选项卡-->
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="最新主题" name="latest">
<article v-for="(item, index) in articleList" :key="index" class="media">
<!--用户头像-->
<div class="media-left">
<figure class="image is-48x48">
<img :src="`http://b123.photo.store.qq.com/psb?/V10SZx2L2Tr4Ta/8bLZrdWJZn0RRH2BovLuqGtBH6eXk54zin2iTxv3JD4!/b/dNv3UUkMLAAA&bo=vgC.AAAAAAABFzA!&rf=viewer_4`" style="border-radius: 5px;">
</figure>
</div>
<div class="media-content">
<div class="">
<p class="ellipsis is-ellipsis-1">
<el-tooltip class="item" effect="dark" :content="item.title" placement="top">
<router-link :to="{name:'post-detail',params:{id:item.id}}">
<span class="is-size-6">{{ item.title }}</span>
</router-link>
</el-tooltip>
</p>
<p class="ellipsis is-ellipsis-3">{{item.content}} </p>
</div>
<nav class="level has-text-grey is-mobile is-size-7 mt-2">
<div class="level-left">
<div class="level-left">
<router-link class="level-item" :to="{ path: `/member/${item.username}/home` }">
{{ item.alias }}
</router-link>
<span class="mr-1">
发布于:{{ dayjs(item.createTime).format("YYYY/MM/DD") }}
</span>
<!--标签名称、is-hidden-mobile再pc端可见-->
<span
v-for="(tag, index) in item.tags"
:key="index"
class="tag is-hidden-mobile is-success is-light mr-1"
>
<router-link :to="{ name: 'tag', params: { name: tag.name } }">
{{ "#" + tag.name }}
</router-link>
</span>
<span class="is-hidden-mobile">浏览:{{ item.view }}</span>
</div>
</div>
</nav>
</div>
<div class="media-right" />
</article>
</el-tab-pane>
<el-tab-pane label="热门主题" name="hot">
<article v-for="(item, index) in articleList" :key="index" class="media">
<!--用户头像-->
<div class="media-left">
<figure class="image is-48x48">
<img :src="`https://cn.gravatar.com/avatar/${item.userId}?s=164&d=monsterid`" style="border-radius: 5px;">
</figure>
</div>
<div class="media-content">
<div class="">
<p class="ellipsis is-ellipsis-1">
<el-tooltip class="item" effect="dark" :content="item.title" placement="top">
<router-link :to="{name:'post-detail',params:{id:item.id}}">
<span class="is-size-6">{{ item.title }}</span>
</router-link>
</el-tooltip>
</p>
<p class="ellipsis is-ellipsis-3">{{item.content}} </p>
</div>
<nav class="level has-text-grey is-mobile is-size-7 mt-2">
<div class="level-left">
<div class="level-left">
<router-link class="level-item" :to="{ path: `/member/${item.username}/home` }">
{{ item.alias }}
</router-link>
<span class="mr-1">
发布于:{{ dayjs(item.createTime).format("YYYY/MM/DD") }}
</span>
<!--标签名称、is-hidden-mobile再pc端可见-->
<span
v-for="(tag, index) in item.tags"
:key="index"
class="tag is-hidden-mobile is-success is-light mr-1"
>
<router-link :to="{ name: 'tag', params: { name: tag.name } }">
{{ "#" + tag.name }}
</router-link>
</span>
<span class="is-hidden-mobile">浏览:{{ item.view }}</span>
</div>
</div>
</nav>
</div>
<div class="media-right" />
</article>
</el-tab-pane>
</el-tabs>
</div>
<!--分页-->
<pagination
v-show="page.total > 0"
:total="page.total"
:page.sync="page.current"
:limit.sync="page.size"
@pagination="init"
/>
</el-card>
</div>
</template>
<script>
import {getList} from '@/api/post'
import Pagination from '@/components/Pagination'
export default {
name: 'TopicList',
components:{
Pagination
},
data() {
return {
//切换选项,最新帖子还是热帖
activeName: 'latest',
articleList: [],
page: {
current: 1,
size: 10,
total: 0,
tab: 'latest'
}
}
},
//在页面开始加载的时候
created(){
this.init(this.tab)
},
methods: {
//向后台请求数据
init(tab) {
getList(this.page.current, this.page.size, tab).then((response) => {
const { data } = response
console.log(this.page.total)
this.page.current = data.current
this.page.total = data.total
this.page.size = data.size
this.articleList = data.records
})
},
//切换tab的时候触发
handleClick(tab) {
this.init(tab.name)
}
}
}
</script>
4、添加底部分页条
- src/components/Pagination/index.vue
<template>
<div :class="{ hidden: hidden }" class="pagination-container">
<el-pagination
:background="background"
:current-page.sync="currentPage"
:page-size.sync="pageSize"
:layout="layout"
:page-sizes="pageSizes"
:total="total"
v-bind="$attrs"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script>
import {scrollTo} from "@/utils/scroll-to";
export default {
name: "Pagination",
props: {
total: {
required: true,
type: Number,
},
page: {
type: Number,
default: 1,
},
limit: {
type: Number,
default: 10,
},
pageSizes: {
type: Array,
default() {
return [5, 10, 20, 30, 50];
},
},
layout: {
type: String,
default: "total, sizes, prev, pager, next, jumper",
// default: 'sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true,
},
autoScroll: {
type: Boolean,
default: true,
},
hidden: {
type: Boolean,
default: false,
},
},
computed: {
currentPage: {
get() {
return this.page;
},
set(val) {
this.$emit("update:page", val);
},
},
pageSize: {
get() {
return this.limit;
},
set(val) {
this.$emit("update:limit", val);
},
},
},
methods: {
handleSizeChange(val) {
this.$emit("pagination", { page: this.currentPage, limit: val });
if (this.autoScroll) {
scrollTo(0, 800);
}
},
handleCurrentChange(val) {
this.$emit("pagination", { page: val, limit: this.pageSize });
if (this.autoScroll) {
scrollTo(0, 800);
}
},
},
};
</script>
<style scoped>
.pagination-container {
/* background: #fff; */
padding: 5px 0px;
}
.pagination-container.hidden {
display: none;
}
</style>
- src/utils/scroll-to.js
Math.easeInOutQuad = function(t, b, c, d) {
t /= d / 2
if (t < 1) {
return c / 2 * t * t + b
}
t--
return -c / 2 * (t * (t - 2) - 1) + b
}
// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
var requestAnimFrame = (function() {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
})()
/**
* Because it's so fucking difficult to detect the scrolling element, just move them all
* @param {number} amount
*/
function move(amount) {
document.documentElement.scrollTop = amount
document.body.parentNode.scrollTop = amount
document.body.scrollTop = amount
}
function position() {
return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
}
/**
* @param {number} to
* @param {number} duration
* @param {Function} callback
*/
export function scrollTo(to, duration, callback) {
const start = position()
const change = to - start
const increment = 20
let currentTime = 0
duration = (typeof (duration) === 'undefined') ? 500 : duration
var animateScroll = function() {
// increment the time
currentTime += increment
// find the value with the quadratic in-out easing function
var val = Math.easeInOutQuad(currentTime, start, change, duration)
// move the document.body
move(val)
// do the animation unless its over
if (currentTime < duration) {
requestAnimFrame(animateScroll)
} else {
if (callback && typeof (callback) === 'function') {
// the animation is done so lets callback
callback()
}
}
}
animateScroll()
}
- src/views/post/Index.vue
<script>
import {getList} from '@/api/post'
import Pagination from '@/components/Pagination'
export default {
name: 'TopicList',
components:{
Pagination
},
data() {
return {
//切换选项,最新帖子还是热帖
activeName: 'latest',
articleList: [],
page: {
current: 1,
size: 10,
total: 0,
tab: 'latest'
}
}
},
//在页面开始加载的时候
created(){
this.init(this.tab)
},
methods: {
//向后台请求数据
init(tab) {
getList(this.page.current, this.page.size, tab).then((response) => {
const { data } = response
console.log(this.page.total)
this.page.current = data.current
this.page.total = data.total
this.page.size = data.size
this.articleList = data.records
})
},
//切换tab的时候触发
handleClick(tab) {
this.init(tab.name)
}
}
}
</script>
修改变成从后台取出来刷新current、total、size的
13、发表帖子
1、安装vditor
2、添加axios
- src/api/post.js
// 发布
export function post(topic) {
return request({
url: '/post/create',
method: 'post',
data: topic
})
}
3、添加路由
- src/router/index.js
// 发布
{
name: 'post-create',
path: '/post/create',
component: () => import('@/views/post/Create'),
meta: { title: '信息发布', requireAuth: true }
},
4、在登录情况下,views/card/LoginWelcome.vue
<template>
<el-card class="box-card" shadow="never">
<div slot="header">
<span>❀ 发帖</span>
</div>
<div v-if="token != null && token !== ''" class="has-text-centered">
<b-button type="is-danger" tag="router-link" :to="{path:'/post/create'}" outlined>
✍表达想法
</b-button>
会跳转到我们的create中
- src/views/post/Create.vue
<template>
<div class="columns">
<div class="column is-full">
<el-card
class="box-card"
shadow="never"
>
<div
slot="header"
class="clearfix"
>
<span><i class="fa fa fa-book"> 主题 / 发布主题</i></span>
</div>
<div>
<el-form
ref="ruleForm"
:model="ruleForm"
:rules="rules"
class="demo-ruleForm"
>
<el-form-item prop="title">
<el-input
v-model="ruleForm.title"
placeholder="输入主题名称"
/>
</el-form-item>
<!--Markdown-->
<div id="vditor" />
<!--这个组件帮我们封装好了-->
<b-taginput
v-model="ruleForm.tags"
class="my-3"
maxlength="15"
maxtags="3"
ellipsis
placeholder="请输入主题标签,限制为 15 个字符和 3 个标签"
/>
<el-form-item>
<el-button
type="primary"
@click="submitForm('ruleForm')"
>立即创建
</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</div>
</div>
</template>
<script>
import { post } from '@/api/post'
import Vditor from 'vditor'
import 'vditor/dist/index.css'
export default {
name: 'TopicPost',
data() {
return {
contentEditor: {},
ruleForm: {
title: '', // 标题
tags: [], // 标签
content: '' // 内容
},
rules: {
title: [
{ required: true, message: '请输入话题名称', trigger: 'blur' },
{
min: 1,
max: 25,
message: '长度在 1 到 25 个字符',
trigger: 'blur'
}
]
}
}
},
//一般在初始化页面完成后,再对dom节点进行相关操作
mounted() {
this.contentEditor = new Vditor('vditor', {
height: 500,
placeholder: '此处为话题内容……',
theme: 'classic',
counter: {
enable: true,
type: 'markdown'
},
preview: {
delay: 0,
hljs: {
style: 'monokai',
lineNumber: true
}
},
tab: '\t',
typewriterMode: true,
toolbarConfig: {
pin: true
},
cache: {
enable: false
},
mode: 'sv'
})
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
if (
this.contentEditor.getValue().length === 1 ||
this.contentEditor.getValue() == null ||
this.contentEditor.getValue() === ''
) {
alert('话题内容不可为空')
return false
}
if (this.ruleForm.tags == null || this.ruleForm.tags.length === 0) {
alert('标签不可以为空')
return false
}
this.ruleForm.content = this.contentEditor.getValue()
post(this.ruleForm).then((response) => {
const { data } = response
//跳转到帖子详情
setTimeout(() => {
this.$router.push({
name: 'post-detail',
params: { id: data.id }
})
}, 800)
})
} else {
console.log('error submit!!')
return false
}
})
},
resetForm(formName) {
this.$refs[formName].resetFields()
this.contentEditor.setValue('')
this.ruleForm.tags = ''
}
}
}
</script>
<style>
</style>
- src/api/post.js
// 发布
export function post(topic) {
return request({
url: '/post/create',
method: 'post',
data: topic
})
}
发送请求会被request.js中的service.interceptors.request.use( 拦截,加上头部的用户信息
14、帖子详情
- community_front/src/api/post.js
})
}
// 浏览
export function getTopic(id) {
return request({
url: `/post`,
method: 'get',
params: {
id: id
}
})
}
- community_front/src/router/index.js
},
// 详情
{
name: "post-detail",
path: "/post/:id",
component: () => import("@/views/post/Detail"),
},
- community_front/src/views/post/Detail.vue
<template>
<div class="columns">
<!--文章详情-->
<div class="column is-three-quarters">
<!--主题-->
<el-card
class="box-card"
shadow="never"
>
<div
slot="header"
class="has-text-centered"
>
<p class="is-size-5 has-text-weight-bold">{{ topic.title }}</p>
<div class="has-text-grey is-size-7 mt-3">
<span>{{ dayjs(topic.createTime).format('YYYY/MM/DD HH:mm:ss') }}</span>
<el-divider direction="vertical" />
<span>发布者:{{ topicUser.alias }}</span>
<el-divider direction="vertical" />
<span>查看:{{ topic.view }}</span>
</div>
</div>
<!--Markdown-->
<div id="preview" />
<!--标签-->
<nav class="level has-text-grey is-size-7 mt-6">
<div class="level-left">
<p class="level-item">
<b-taglist>
<router-link
v-for="(tag, index) in tags"
:key="index"
:to="{ name: 'tag', params: { name: tag.name } }"
>
<b-tag type="is-info is-light mr-1">
{{ "#" + tag.name }}
</b-tag>
</router-link>
</b-taglist>
</p>
</div>
<div
v-if="token && user.id === topicUser.id"
class="level-right"
>
<router-link
class="level-item"
:to="{name:'topic-edit',params: {id:topic.id}}"
>
<span class="tag">编辑</span>
</router-link>
<a class="level-item">
<span
class="tag"
@click="handleDelete(topic.id)"
>删除</span>
</a>
</div>
</nav>
</el-card>
</div>
<div class="column">
作者信息
</div>
</div>
</template>
<script>
import { deleteTopic, getTopic } from '@/api/post'
import { mapGetters } from 'vuex'
import Vditor from 'vditor'
import 'vditor/dist/index.css'
export default {
name: 'TopicDetail',
computed: {
...mapGetters([
'token','user'
])
},
data() {
return {
flag: false,
topic: {
content: '',
id: this.$route.params.id
},
tags: [],
topicUser: {}
}
},
mounted() {
//获取帖子信息
this.fetchTopic()
},
methods: {
renderMarkdown(md) {
Vditor.preview(document.getElementById('preview'), md, {
hljs: { style: 'github' }
})
},
// 初始化
async fetchTopic() {
getTopic(this.$route.params.id).then(response => {
const { data } = response
document.title = data.topic.title
this.topic = data.topic
this.tags = data.tags
this.topicUser = data.user
// this.comments = data.comments
this.renderMarkdown(this.topic.content)
this.flag = true
})
},
handleDelete(id) {
deleteTopic(id).then(value => {
const { code, message } = value
alert(message)
if (code === 200) {
setTimeout(() => {
this.$router.push({ path: '/' })
}, 500)
}
})
}
}
}
</script>
<style>
#preview {
min-height: 300px;
}
</style>
15、帖子详情---右边侧边栏作者详情
- community_front/src/api/follow.js
import request from '@/utils/request'
// 关注
export function follow(id) {
return request(({
url: `/relationship/subscribe/${id}`,
method: 'get'
}))
}
// 关注
export function unFollow(id) {
return request(({
url: '/relationship/unsubscribe/${id}',
method: 'get'
}))
}
// 验证是否关注
export function hasFollow(topicUserId) {
return request(({
url: '/relationship/validate/${topicUserId}',
method: 'get'
}))
}
- community_front/src/views/post/Author.vue
<template>
<section id="author">
<el-card class="" shadow="never">
<div slot="header">
<span class="has-text-weight-bold">👨💻 关于作者</span>
</div>
<div class="has-text-centered">
<p class="is-size-5 mb-5">
<router-link :to="{ path: '/member/${user.username}/home' }">
{{ user.alias }} <span class="is-size-7 has-text-grey">{{ '@' + user.username }}</span>
</router-link>
</p>
<div class="columns is-mobile">
<div class="column is-half">
<code>{{ user.topicCount }}</code>
<p>文章</p>
</div>
<div class="column is-half">
<code>{{ user.followerCount }}</code>
<p>粉丝</p>
</div>
</div>
<div>
<button
v-if="hasFollow"
class="button is-success button-center is-fullwidth"
@click="handleUnFollow(user.id)"
>
已关注
</button>
<button v-else class="button is-link button-center is-fullwidth" @click="handleFollow(user.id)">
关注
</button>
</div>
</div>
</el-card>
</section>
</template>
<script>
import { follow, hasFollow, unFollow } from '@/api/follow'
import { mapGetters } from 'vuex'
export default {
name: 'Author',
props: {
user: {
type: Object,
default: null
}
},
data() {
return {
hasFollow: false
}
},
mounted() {
this.fetchInfo()
},
computed: {
...mapGetters([
'token'
])
},
methods: {
fetchInfo() {
if(this.token != null && this.token !== '')
{
hasFollow(this.user.id).then(value => {
const { data } = value
this.hasFollow = data.hasFollow
})
}
},
handleFollow: function(id) {
if(this.token != null && this.token !== '')
{
follow(id).then(response => {
const { message } = response
this.$message.success(message)
this.hasFollow = !this.hasFollow
this.user.followerCount = parseInt(this.user.followerCount) + 1
})
}
else{
this.$message.success('请先登录')
}
},
handleUnFollow: function(id) {
unFollow(id).then(response => {
const { message } = response
this.$message.success(message)
this.hasFollow = !this.hasFollow
this.user.followerCount = parseInt(this.user.followerCount) - 1
})
}
}
}
</script>
<style scoped>
</style>
16、留言
安装date-fns,有一个时间解析工具
- community_front/src/api/comment.js
添加前端aioxs请求
- community_front/src/components/Comment/Comments.vue
添加评论组件
- community_front/src/components/Comment/CommentsItem.vue
添加评论组件的项
- community_front/src/main.js
全局定义date-fns
- community_front/src/views/post/Detail.vue
引入评论组件
17、留言---添加留言
必须是在登录的情况下才显示,不等了只显示留言信息
- community_front/src/api/comment.js
- community_front/src/components/Comment/Comments.vue
- community_front/src/components/Comment/CommentsForm.vue
18、帖子删除与更新
19、根据标签信息查出相关帖子
20、用户中心
21、个人设置
22、留言等 需要认证后才能访问
// 编辑
{
name: 'topic-edit',
path: '/topic/edit/:id',
component: () => import('@/views/post/Edit'),
meta: {
title: '编辑', requireAuth: true
}
},
在需要的路由上添加meta requireAuth: true
然后再permission中添加
} else if (!to.meta.requireAuth){
next()
}else {
//未登录
next('/login')
}