前后端分離多用戶社區(qū)項目實戰(zhàn)--- 前端頁面開發(fā)

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')
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市甸怕,隨后出現(xiàn)的幾起案子甘穿,更是在濱河造成了極大的恐慌,老刑警劉巖梢杭,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件温兼,死亡現(xiàn)場離奇詭異,居然都是意外死亡武契,警方通過查閱死者的電腦和手機募判,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門荡含,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人届垫,你說我怎么就攤上這事释液。” “怎么了装处?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵误债,是天一觀的道長。 經(jīng)常有香客問我妄迁,道長寝蹈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任判族,我火速辦了婚禮,結(jié)果婚禮上项戴,老公的妹妹穿的比我還像新娘形帮。我一直安慰自己,他們只是感情好周叮,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布辩撑。 她就那樣靜靜地躺著,像睡著了一般仿耽。 火紅的嫁衣襯著肌膚如雪合冀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天项贺,我揣著相機與錄音君躺,去河邊找鬼。 笑死开缎,一個胖子當(dāng)著我的面吹牛棕叫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奕删,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼俺泣,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了完残?” 一聲冷哼從身側(cè)響起伏钠,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谨设,沒想到半個月后熟掂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡扎拣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年打掘,在試婚紗的時候發(fā)現(xiàn)自己被綠了华畏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡尊蚁,死狀恐怖亡笑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情横朋,我是刑警寧澤仑乌,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站琴锭,受9級特大地震影響晰甚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜决帖,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一厕九、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧地回,春花似錦扁远、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至细睡,卻和暖如春谷羞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背溜徙。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工湃缎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蠢壹。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓雁歌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親知残。 傳聞我的和親對象是個殘疾皇子靠瞎,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容