前后端分离多用户社区项目实战--- 前端页面开发

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')
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容