5、頁面開發(fā)
刪除App.vue的默認(rèn)樣式茎杂,刪除id為nav下的about和home
1仑鸥、前端公告板功能實現(xiàn)
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(){
//接收到服務(wù)端返回來的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提供數(shù)據(jù)
前后端聯(lián)調(diào)
前端開始npm run serve,后端也開啟
然后出現(xiàn)
這是由于前端是localhost:8080和后端localhost:8081,端口號不一致產(chǎn)生的跨域問題沸柔,去后端設(shè)置跨域,對應(yīng)后端【8铲敛、跨域問題】
2褐澎、每日一句功能實現(xiàn)
接下我們繼續(xù)完成每日一句的實現(xiàn)
在完成每日一句之前,我們利用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(){
//接收到服務(wù)端返回來的value
getBillboard().then((value) => {
const { data } = value
this.billborad = data;
})
}
}
}
</script>
- 新建views/card側(cè)邊欄相關(guān)組件
CardBar.vue
<template>
<div>
CardBar
</div>
</template>
<script>
export default {
name: 'CardBar',
data() {
return {
}
},
//在頁面開始加載的時候
created(){
},
methods: {
}
}
</script>
- 新建views/post帖子相關(guān)組件
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、側(cè)邊欄
import CardBar from "@/views/card/CardBar";
//帖子相關(guān)
import TopicList from "@/views/post/TopicList"
export default {
name: 'Home',
//2咽弦、
components: {
CardBar,TopicList
},
...
測試
利用elementui對我們的側(cè)邊欄進(jìn)行美化:
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>? 發(fā)帖</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分發(fā)徒蟆,向組件內(nèi)部指定位置傳遞內(nèi)容-->
<div slot="header">
<span>?? 每日一句</span>
</div>
<div>
<div class="has-text-left block">
十個指頭按不住十個跳騷
</div>
<!--block幫我們實現(xiàn)了塊之間的間隙,md-5(內(nèi)容外邊距加5px)不生效-->
<div class="has-text-right block">
---傣族
</div>
</div>
</el-card>
</template>
...
上述三個組件型型,script重復(fù)部分我這邊就沒列出來了
上述樣式可能會存在重復(fù)使用的情況段审,這邊就,建立一個公共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分發(fā)寺枉,向組件內(nèi)部指定位置傳遞內(nèi)容-->
<div slot="header">
<span>?? 每日一句</span>
</div>
<div>
<div class="has-text-left block">
{{tip.content}}}
</div>
<!--block幫我們實現(xiàn)了塊之間的間隙,md-5(內(nèi)容外邊距加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绷落、創(chuàng)建api/tip.js姥闪,幫助實現(xiàn)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分發(fā)砌烁,向組件內(nèi)部指定位置傳遞內(nèi)容-->
<div slot="header">
<span>?? 每日一句</span>
</div>
<div>
<div class="has-text-left block">
{{tip.content}}
</div>
<!--block幫我們實現(xiàn)了塊之間的間隙筐喳,md-5(內(nèi)容外邊距加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(){
//接收到服務(wù)端返回來的value
getTodayTip().then((value) => {
const { data } = value
this.tip = data;
})
}
}
}
</script>
4、測試
3函喉、廣告推廣實現(xiàn)
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實現(xiàn)循環(huán)梳毙,綁定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(){
//接收到服務(wù)端返回來的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)
//頁面跳轉(zhuǎn)
this.$router.push({path:'/'})
}
},1000)
}
}
}
</script>
<style scoped>
</style>
2鸟缕、路由
- router/index.js
const routes = [
...
{
path: '/404',
name: '404',
//改成動態(tài)引入
component: () => import('@/views/error/404'),
//從meta元數(shù)據(jù)中讀取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="確認(rèn)密碼" 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 {
//用戶注冊中番甩,防止用戶重新注冊,需等待后臺回應(yīng)后再進(jìn)行下步操作
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
//服務(wù)器校驗
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',
//改成動態(tài)引入
component: () => import('@/views/Home')
},
{
path: '/register',
name: 'Register',
//改成動態(tài)引入
component: () => import('@/views/auth/Register'),
meta:{title: '注冊'}
}
...
6卡睦、用戶登錄
引入vuex 宴胧,存放組件的信息,便于各個組件的讀取表锻,有點像全局?jǐn)?shù)據(jù),但是router時存放在內(nèi)存中的恕齐,我們?yōu)榱朔奖阆麓蔚卿洉r候記住用戶的一些信息:比如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)
}
//設(shè)置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()
}
//設(shè)置暗黑模式
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{
//用戶注冊中瞬逊,防止用戶重新注冊显歧,需等待后臺回應(yīng)后再進(jìn)行下步操作
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發(fā)送請求
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>
分析上述代碼,用戶發(fā)起請求
submitForm(formName) {
//校驗信息
this.$refs[formName].validate((valid) => {
if (valid) {
this.loading = true
loading是為了在請求后臺的時候蕾域,解決進(jìn)入等待狀態(tài) 而不是可以隨意點
if (valid)這里時根據(jù)rules規(guī)則拷肌,在瀏覽器端校驗,成功后向vue的store發(fā)送請求
//向vue的store發(fā)送請求
this.$store
//指定modules:user束铭,login方法
.dispatch("user/login", this.ruleForm)
dispatch方法
第一個參數(shù):user
會調(diào)用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: {
//將數(shù)據(jù)模塊化分類
user
}
})
user/login會調(diào)用user中的login方法
- store/moudles/user.js
import { login } from '@/api/auth/auth'
import { getToken, setToken } from '@/utils/token'
//定義全局?jǐn)?shù)據(jù)
const state = {
token: getToken(), // token
user: '', // 用戶對象
}
//state必須通過mutations改變廓块,類似java中的 set
const mutations = {
SET_TOKEN_STATE: (state, token) => {
state.token = token
}
}
//mutations不能接受異步請求,后臺發(fā)過來的異步請求放在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是方法定義契沫,供外部調(diào)用.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',
//改成動態(tài)引入
component: () => import('@/views/auth/Login'),
meta:{title: '登錄'}
},
7带猴、 前邊側(cè)邊欄:馬上入駐
-
views/card/LoginWelcome
<template> <el-card class="box-card" shadow="never"> <div slot="header"> <span>? 發(fā)帖</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> ?表達(dá)想法 </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"> 社區(qū)登錄 </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的值是否存在拴清,來變化前端側(cè)邊的信息
上述
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: {
//將數(shù)據(jù)模塊化分類
user
},
getters
})
export default store
加上getters就可以,
getters的定義
-
store/getters.js
const getters = { //state =>箭頭函數(shù) token: state => state.user.token, //token user: state => state.user.user, //用戶對象 } export default getters
其中state調(diào)用modules的user口予,
頁面不帶token的情況
頁面帶token的情況
如果刪除掉cookie里面的u_token,再次刷新頁面涕侈,我們的從getters獲取token沪停,getters又獲取user.js中的token,該token是調(diào)用了js-cookie中的getToken方法,會發(fā)現(xiàn)我們刪除了cookie中的數(shù)據(jù)
//定義全局?jǐn)?shù)據(jù)
const state = {
token: getToken(), // token
8、前端在axios請求攔截中在請求頭加入jwt
1木张、在用戶登錄的時候 众辨,加入代碼,讓前端發(fā)送請求給后臺舷礼,請求用戶信息
- views/auth/Login
methods: {
submitForm(formName) {
//校驗信息
this.$refs[formName].validate((valid) => {
if (valid) {
this.loading = true
//向vue的store發(fā)送請求
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發(fā)送蛛株,
定義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)
})
//指定發(fā)生錯誤時的回調(diào)函數(shù)育拨。
.catch(error => {
reject(error)
})
})
}
2谨履、mutations中加函數(shù)
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";
//定義全局?jǐn)?shù)據(jù)
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不能接受異步請求熬丧,后臺發(fā)過來的異步請求放在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)
})
//指定發(fā)生錯誤時的回調(diào)函數(shù)屉符。
.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)
})
//指定發(fā)生錯誤時的回調(diào)函數(shù)。
.catch(error => {
reject(error)
})
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
此時我們向后臺發(fā)送請求的時候锹引,沒有帶Authorization信息矗钟,我們希望帶上Authorization信息,后臺就可以識別我們嫌变,并給我們進(jìn)行授權(quán)等后續(xù)操作
- utils/request.js 加入如下代碼
import { getToken } from '@/utils/token'
// 2.請求攔截器request interceptor
service.interceptors.request.use(
config => {
// 發(fā)請求前做的一些處理吨艇,數(shù)據(jù)轉(zhuǎn)化,配置請求頭腾啥,設(shè)置token,設(shè)置loading等东涡,根據(jù)需求去添加
// 注意使用token的時候需要引入cookie方法或者用本地localStorage等方法,推薦js-cookie
if (store.getters.token) {
// config.params = {'token': token} // 如果要求攜帶在參數(shù)中
// config.headers.token = token; // 如果要求攜帶在請求頭中
// bearer:w3c規(guī)范
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發(fā)送請求
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凸舵,發(fā)送請求祖娘,因為這塊用戶信息可能會被其他模塊用到
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";
//定義全局?jǐn)?shù)據(jù)
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不能接受異步請求啊奄,后臺發(fā)過來的異步請求放在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)
})
//指定發(fā)生錯誤時的回調(diào)函數(shù)渐苏。
.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.創(chuàng)建axios實例,vue請求后臺
const service = axios.create({
// 公共接口--這里注意后面會講,url = base url + request url
baseURL: process.env.VUE_APP_SERVER_URL,
// baseURL: 'https://api.example.com',
// 超時時間 單位是ms菇夸,這里設(shè)置了5s的超時時間
timeout: 5 * 1000
})
// 2.請求攔截器request interceptor
service.interceptors.request.use(
config => {
// 發(fā)請求前做的一些處理琼富,數(shù)據(jù)轉(zhuǎn)化套耕,配置請求頭汛骂,設(shè)置token,設(shè)置loading等蜕劝,根據(jù)需求去添加
// 注意使用token的時候需要引入cookie方法或者用本地localStorage等方法守呜,推薦js-cookie
if (store.getters.token) {
// config.params = {'token': token} // 如果要求攜帶在參數(shù)中
// config.headers.token = token; // 如果要求攜帶在請求頭中
// bearer:w3c規(guī)范
config.headers['Authorization'] = 'Bearer ' + getToken()
}
return config
},
error => {
// do something with request error
// console.log(error) // for debug
return Promise.reject(error)
}
)
// 設(shè)置cross跨域 并設(shè)置訪問權(quán)限 允許跨域攜帶cookie信息,使用JWT可關(guān)閉
service.defaults.withCredentials = false
// 3.請求攔截器response interceptor
service.interceptors.response.use(
// 接收到響應(yīng)數(shù)據(jù)并成功后的一些共有的處理择膝,關(guān)閉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('會話失效耻蛇,您可以留在當(dāng)前頁面,或重新登錄', '權(quán)限不足', {
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 => {
/** *** 接收到異常響應(yīng)的處理開始 *****/
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規(guī)范,注意加了空格涩金,后臺取到請求頭之后也需要反過來解析
9谱醇、實現(xiàn)暗黑模式、頁頭
使用現(xiàn)成的庫
安裝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="搜索帖子副渴、標(biāo)簽和用戶"
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` }"
>
? 設(shè)置中心
</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:{
// 監(jiān)聽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>
我們通過監(jiān)聽darkMode屬性的改變值,取做到切換暗黑模式全度,暗黑模式是借用
darkreader工具實現(xiàn)的煮剧,
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>
我們登錄頁面
刷新頁面
發(fā)現(xiàn)用戶信息不見了将鸵,原因在于我們每一次刷新勉盅,
Header.vue都會取調(diào)用
...mapGetters(['token', 'user'])
通過getters.js
const getters = {
//state =>箭頭函數(shù)
token: state => {
return state.user.token//token
},
user: state => state.user.user, //用戶對象
}
export default getters
獲得用戶信息,但是在user.js中
//定義全局?jǐn)?shù)據(jù)
const state = {
token: getToken(), // token
user: '', // 用戶對象
}
token每次都可以重新請求獲得顶掉,但是user被重置為空了草娜,問題出在這,我們獲取的時機不對
我們是在登錄Login.vue通過
//登錄成功后痒筒,獲取用戶信息
this.$store.dispatch("user/getInfo")
獲取的宰闰,但是這個只執(zhí)行了一次,我們應(yīng)該在每次刷新的時候取執(zhí)行請求用戶信息
新建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') {
// 登錄移袍,跳轉(zhuǎn)首頁
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 = '小而美的智慧社區(qū)系統(tǒng)'
export default function getPageTitle(pageTitle) {
if (pageTitle) {
return `${pageTitle} - ${title}`
}
return `${title}`
}
安裝nprogress,這是告訴用戶刷新頁面進(jìn)度提示的小工具
最后在main.js中引入permission老充,我們再刷新葡盗,發(fā)現(xiàn)解決了
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()
})
//指定發(fā)生錯誤時的回調(diào)函數(shù)啡浊。
.catch(error => {
reject(error)
})
})
},
api/auth/auth.js
//前提用戶注銷
export function logout() {
return request({
url: '/ums/user/logout',
method: 'get',
})
}
測試
出現(xiàn)這個問題 是因為我們當(dāng)前就在首頁戳粒,但是logout還是請求跳轉(zhuǎn)到/,為了解決這個問題
我們在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 }
}))
}
//分頁參數(shù)树枫,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" >
<!--標(biāo)簽選項卡-->
<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">
發(fā)布于:{{ dayjs(item.createTime).format("YYYY/MM/DD") }}
</span>
<!--標(biāo)簽名稱砂轻、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">
發(fā)布于:{{ dayjs(item.createTime).format("YYYY/MM/DD") }}
</span>
<!--標(biāo)簽名稱奔誓、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: {
//向后臺請求數(shù)據(jù)
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的時候觸發(fā)
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: {
//向后臺請求數(shù)據(jù)
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的時候觸發(fā)
handleClick(tab) {
this.init(tab.name)
}
}
}
</script>
修改變成從后臺取出來刷新current、total庄呈、size的
13蜕煌、發(fā)表帖子
1、安裝vditor
2诬留、添加axios
- src/api/post.js
// 發(fā)布
export function post(topic) {
return request({
url: '/post/create',
method: 'post',
data: topic
})
}
3斜纪、添加路由
- src/router/index.js
// 發(fā)布
{
name: 'post-create',
path: '/post/create',
component: () => import('@/views/post/Create'),
meta: { title: '信息發(fā)布', requireAuth: true }
},
4、在登錄情況下,views/card/LoginWelcome.vue
<template>
<el-card class="box-card" shadow="never">
<div slot="header">
<span>? 發(fā)帖</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>
?表達(dá)想法
</b-button>
會跳轉(zhuǎn)到我們的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"> 主題 / 發(fā)布主題</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="請輸入主題標(biāo)簽文兑,限制為 15 個字符和 3 個標(biāo)簽"
/>
<el-form-item>
<el-button
type="primary"
@click="submitForm('ruleForm')"
>立即創(chuàng)建
</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: '', // 標(biāo)題
tags: [], // 標(biāo)簽
content: '' // 內(nèi)容
},
rules: {
title: [
{ required: true, message: '請輸入話題名稱', trigger: 'blur' },
{
min: 1,
max: 25,
message: '長度在 1 到 25 個字符',
trigger: 'blur'
}
]
}
}
},
//一般在初始化頁面完成后盒刚,再對dom節(jié)點進(jìn)行相關(guān)操作
mounted() {
this.contentEditor = new Vditor('vditor', {
height: 500,
placeholder: '此處為話題內(nèi)容……',
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('話題內(nèi)容不可為空')
return false
}
if (this.ruleForm.tags == null || this.ruleForm.tags.length === 0) {
alert('標(biāo)簽不可以為空')
return false
}
this.ruleForm.content = this.contentEditor.getValue()
post(this.ruleForm).then((response) => {
const { data } = response
//跳轉(zhuǎn)到帖子詳情
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
// 發(fā)布
export function post(topic) {
return request({
url: '/post/create',
method: 'post',
data: topic
})
}
發(fā)送請求會被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>發(fā)布者:{{ topicUser.alias }}</span>
<el-divider direction="vertical" />
<span>查看:{{ topic.view }}</span>
</div>
</div>
<!--Markdown-->
<div id="preview" />
<!--標(biāo)簽-->
<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伪冰、帖子詳情---右邊側(cè)邊欄作者詳情
- community_front/src/api/follow.js
import request from '@/utils/request'
// 關(guān)注
export function follow(id) {
return request(({
url: `/relationship/subscribe/${id}`,
method: 'get'
}))
}
// 關(guān)注
export function unFollow(id) {
return request(({
url: '/relationship/unsubscribe/${id}',
method: 'get'
}))
}
// 驗證是否關(guān)注
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">???? 關(guān)于作者</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)"
>
已關(guān)注
</button>
<button v-else class="button is-link button-center is-fullwidth" @click="handleFollow(user.id)">
關(guān)注
</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吓懈、根據(jù)標(biāo)簽信息查出相關(guān)帖子
20、用戶中心
21靡狞、個人設(shè)置
22耻警、留言等 需要認(rèn)證后才能訪問
// 編輯
{
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')
}