本章節內容前端
文章列表vue
文章詳情node
草稿箱react
文章發佈。ios
本章節內容呢,開發的非常隨意哈,由於多數就是element-ui的使用,熟悉的童鞋,是能夠很快完成本章節的內容的。vue-router
爲啥文章模塊會有這麼多東西呢?element-ui
由於狗尾草想着之後,文章若是是待發布的話就須要一個地方去存放起來,一開始刪除的文章呢,也將會被移入到草稿箱中,這樣的話,文章就不會被隨便的更改啦。axios
api
是否是感受很是輕鬆,一個table就能夠搞定,數組
這裏的代碼呢,我就直接貼出來,由於沒有什麼值得注意的地方都是基礎。
article>list.vue
<template> <div class="article-wrap"> <el-table :data="articleList" height="100%" stripe> <el-table-column prop="id" align="center" label="文章編號"> </el-table-column> <el-table-column prop="create_time" align="center" label="建立時間"> <template slot-scope="scope"> {{$moment(scope.row.create_time).format('YYYY-MM-DD HH:mm')}} </template> </el-table-column> <el-table-column prop="tags" align="center" label="標籤"> <template slot-scope="scope"> {{$utils.formatTableFont(scope.row.tags)}} </template> </el-table-column> <el-table-column prop="title" align="center" label="標題"> <template slot-scope="scope"> {{$utils.formatTableFont(scope.row.title)}} </template> </el-table-column> <el-table-column prop="title_image" align="center" label="標題圖片"> <template slot-scope="scope"> <img v-if="scope.row.title_image" class="title-img" :src="scope.row.title_image" /> <span v-else>-</span> </template> </el-table-column> <el-table-column prop="reader_number" align="center" label="閱讀數"> <template slot-scope="scope"> {{$utils.formatTableData(scope.row.reader_number)}} </template> </el-table-column> <el-table-column prop="good_number" align="center" label="點贊數"> <template slot-scope="scope"> {{$utils.formatTableData(scope.row.good_number)}} </template> </el-table-column> <el-table-column label="操做" align="center" fixed="right"> <template slot-scope="scope"> <el-button size="mini" @click.stop="$router.push({path:'/article/detail',query:{articleId:scope.row.id,status:1}})">編輯</el-button> </template> </el-table-column> </el-table> </div> </template> <script> export default { data() { return { articleList: [], params: { searchParams: '', page: 1, size: 10, status: 1 }, } }, methods: { // 獲取文章列表 async getArticleList() { try { const { articleData } = await this.$http.getRequest('/article/api/v1/article_list',this.params); this.articleList = articleData; } catch(err) { throw new Error('獲取文章列表失敗',err); } } }, mounted() { this.getArticleList(); } } </script> <style lang="less" scoped> .article-wrap { height: 100%; overflow: hidden; /deep/.title-img { width: 90px; height: 90px; } } </style>
這裏呢,接口呢,都已經完成了。這類先不作說明,後面會單獨將node.js抽離出來的哈。
不過呢,這裏的formatTableData方法呢,是由於咱們在作表格數據顯示的時候呢,會有沒有數據的情景,因此這裏。我封裝了一個方法,專門的針對表格的數據進行一個處理,在沒有數據的時候呢就顯示'-',若是是數字類型的呢,這裏就顯示0
給你們把方法貼出來,至於若是把方法掛在到全局,前面有講到啦。這裏也不須要這樣作,由於直接方法utils文件中,utils掛載到全局,是能夠直接使用的了。
import * as http from './http'; import VueCookies from 'vue-cookies' import moment from 'moment'; import utils from './plugins'; const install = (Vue, opts = {}) => { if (install.installed) return; Vue.prototype.$http = http; Vue.prototype.$cookies = VueCookies; Vue.prototype.$moment = moment; Vue.prototype.$utils = utils; } export default install
utils>index.js
/** * @description 封裝的工具類 * @author chaizhiyang */ class Util { /** * 保留小數點後兩位 * @param {Number} data 須要處理的數值 * @return {Number} 保留兩位小數的數值 * @author Czy 2018-10-25 */ returnFloat(data) { return data.toFixed(2) } //el-table表格數據的處理 formatTableFont(val) { //格式化數據,爲空或0或null時,顯示無 let formatTableData; if (!val) { formatTableData = "-"; } else { formatTableData = val; } return formatTableData; }; //el-table表格數據的處理 formatTableData(val) { //格式化數據,爲空或0或null時,顯示無 let formatTableData; if (!val) { formatTableData = "0"; } else { formatTableData = val; } return formatTableData; }; // 返回性別 sexStatus(status) { if (!status) return switch (status) { case 1: return '男'; break; case 2: return '女'; break; default: return '未知'; break; } } /** * 正則驗證 * @param {Number,String} str 須要驗證的內容如:手機號,郵箱等 * @param {String} type 須要正則驗證的類型 * @return {Boolean} true: 正則經過,輸入無誤。false: 正則驗證失敗,輸入有誤 * @author Czy 2018-10-25 */ checkStr(str, type) { switch (type) { case 'phone': //手機號碼 return /^1[3|4|5|7|8][0-9]{9}$/.test(str); case 'tel': //座機 return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str); case 'card': //身份證 return /^\d{15}|\d{18}$/.test(str); case 'account': //帳號 ,長度4~16之間,只能包含數字,中文,字母和下劃線 return /^(\w|[\u4E00-\u9FA5])*$/.test(str); case 'pwd': //密碼以字母開頭,長度在6~18之間,只能包含字母、數字和下劃線 return /^[a-zA-Z]\w{6,18}$/.test(str); case 'postal': //郵政編碼 return /[1-9]\d{5}(?!\d)/.test(str); case 'QQ': //QQ號 return /^[1-9][0-9]{4,9}$/.test(str); case 'email': //郵箱 return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str); case 'money': //金額(小數點2位) return /^\d*(?:\.\d{0,2})?$/.test(str); case 'URL': //網址 return /(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/.test(str); case 'IP': //IP return /((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))/.test(str); case 'date': //日期時間 return /^(\d{4})\-(\d{2})\-(\d{2}) (\d{2})(?:\:\d{2}|:(\d{2}):(\d{2}))$/.test(str) || /^(\d{4})\-(\d{2})\-(\d{2})$/.test(str); case 'number': //數字 return /^[0-9]$/.test(str); case 'english': //英文 return /^[a-zA-Z]+$/.test(str); case 'chinese': //中文 return /^[\u4E00-\u9FA5]+$/.test(str); case 'lower': //小寫 return /^[a-z]+$/.test(str); case 'upper': //大寫 return /^[A-Z]+$/.test(str); case 'HTML': //HTML標記 return /<("[^"]*"|'[^']*'|[^'">])*>/.test(str); default: return true; } } /** * 類型判斷 * @param {*} o 進行判斷的內容 * @return {Boolean} true: 是該類型,false: 不是該類型 * @author Czy 2018-10-25 */ isString(o) { //是否字符串 return Object.prototype.toString.call(o).slice(8, -1) === 'String' } isNumber(o) { //是否數字 return Object.prototype.toString.call(o).slice(8, -1) === 'Number' } isObj(o) { //是否對象 return Object.prototype.toString.call(o).slice(8, -1) === 'Object' } isArray(o) { //是否數組 return Object.prototype.toString.call(o).slice(8, -1) === 'Array' } isDate(o) { //是否時間 return Object.prototype.toString.call(o).slice(8, -1) === 'Date' } isBoolean(o) { //是否boolean return Object.prototype.toString.call(o).slice(8, -1) === 'Boolean' } isFunction(o) { //是否函數 return Object.prototype.toString.call(o).slice(8, -1) === 'Function' } isNull(o) { //是否爲null return Object.prototype.toString.call(o).slice(8, -1) === 'Null' } isUndefined(o) { //是否undefined return Object.prototype.toString.call(o).slice(8, -1) === 'Undefined' } isFalse(o) { if (o == '' || o == undefined || o == null || o == 'null' || o == 'undefined' || o == 0 || o == false || o == NaN) { return true } return false } isTrue(o) { return !this.isFalse(o) } } export default new Util();
這裏直接掛在到全局。使用方法呢就是this.$utils.func就能夠了
這裏的主要功能呢就是根據id去回顯該文章的全部信息,並能夠進行修改,刪除,移入草稿箱等得操做。這裏呢,由於詳情和發佈是相同的,因此呢,這裏也就發一份。固然了就有同窗問我,爲何不講兩個頁面放到一個頁面中呢。
這裏給你們解釋一下哈:
article>publish
<template> <section class="wraper"> <el-form ref="form" :model="form" label-width="92px" :rules="rules"> <!--S 標題 --> <admin-title :title="title.tit1"></admin-title> <el-form-item label="Article Title" prop="title"> <el-col :span="6"> <el-input v-model="form.title"></el-input> </el-col> </el-form-item> <el-form-item label="Title Image" prop="title_image" > <el-col :span="6"> <el-input v-model="form.title_image"></el-input> </el-col> </el-form-item> <admin-title :title="title.tit2"></admin-title> <el-form-item label="Article Tags" prop="tags"> <el-col :span="6"> <el-select style="width: 100%;" v-model="form.tags" multiple filterable allow-create default-first-option placeholder="請選擇文章標籤"> <el-option v-for="item in tagList" :key="item.id" :label="item.tag" :value="item.id"> </el-option> </el-select> </el-col> </el-form-item> <admin-title :title="title.tit3"></admin-title> <el-form-item label="Abstract" prop="describe" align="left"> <textarea class="abstract" v-bind:maxlength="190" v-model="form.describe" rows="5" cols="100" type="text" name="abstract"> </textarea> <span style="font-size:16px;"><font style="color: #3576e0;">{{190 - form.describe.length}}</font>/190</span> </el-form-item> <el-form-item label="Content" prop="content"> <mavon-editor v-model="form.content"/> </el-form-item> <el-form-item align="left"> <el-col> <el-button type="primary" @click.native="handleSubmit('rules')" :loading="buttonLoading.publishLoading">文章發佈</el-button> <el-button type="primary" @click.native="handleMoveDraft('rules')" :loading="buttonLoading.draftLoading">保存草稿</el-button> </el-col> </el-form-item> </el-form> </section> </template> <script> import AdminTitle from '@/components/commons/Title'; export default { components: { AdminTitle, }, watch: { 'form.describe'(curVal, oldVal) { if (curVal.length > this.textNum) { this.textareaValue = String(curVal).slice(0, this.textNum); } } }, data() { return { title: { tit1: '文章標題', tit2: '文章標籤', tit3: '文章摘要', }, //標題 form: { title: '', tags: [], title_image: '', describe: '', content: '', status: 1, }, //提交數據 tagList: [], //標籤選擇器 textNum: 200, previewMarkdown: '<h1>測試</h1>', buttonLoading: { publishLoading: false, draftLoading: false }, rules: { title: [ { required: true, message: '請輸入文章標題', trigger: 'blur'} ], title_img: [ { required: false, message: '請輸入標題圖片', trigger: 'blur'} ], tags: [ { required: false, message: '請選擇文章標籤', trigger: 'change'} ], describe: [ { required: true, message: '請輸入文章摘要', trigger: ['change','blur']} ], content: [ { required: true, message: '請輸入文章內容', trigger: ['blur','change']} ] }, // 表單規則校驗 } }, methods: { //發佈文章 async handleSubmit() { let isOk = this.validata(); if(!isOk) { return ; } this.form.status = 1; this.publishLoading = true; try { const result = await this.$http.postRequest('/article/api/v1/article_add',this.form); this.publishLoading = false; this.$message({ type: 'success', message: '文章發佈成功!' }) this.$router.push({ path: '/article/list' }) } catch(err) { throw new Error('文章更新失敗',err); this.publishLoading = false; } }, // 保存草稿 async handleMoveDraft() { this.form.status = 2; this.publishLoading = true; try { const result = await this.$http.postRequest('/article/api/v1/article_add',this.form); this.publishLoading = false; this.$message({ type: 'success', message: '保存草稿箱成功!' }) this.$router.push({ path: '/article/draft' }) } catch(err) { this.publishLoading = false; throw new Error('保存草稿失敗',err); } }, // 表單校驗 validata() { let isForm; this.$refs.form.validate(valid => { isForm = valid; }); if (!isForm) { return false; } return true; }, // 獲取文章全部標籤 getTags() { let hash = {}; let arr = []; axios.get('/article/api/v1/articleTags') .then(res => { arr = res.reduce((item,next) => { hash[next.tag] ? '' : hash[next.tag] = true && item.push(next); return item; },[]); this.tagList = arr; }) } }, } </script> <style lang="less" scoped> .wraper { width: 100%; height: 100%; .abstract { padding: 10px; font-size: 14px; } /deep/.el-form-item__label { text-align: left; padding-right: 0; } } </style>
可是element_ui明明已經給了一個合理的解決方案了。你們就要學會去使用,給咱們帶來便捷!
// 表單校驗 validata() { let isForm; this.$refs.form.validate(valid => { isForm = valid; }); if (!isForm) { return false; } return true; },
封裝的一個subtitle組件
compoents/commons/Title.vue
<template> <p class="title">{{title}}</p> </template> <script> export default { name: "AdminTitle", props: { title: String }, data () { return { }; } }; </script> <style lang="less"> .title { display: flex; align-items: center; margin: 20px 0; color: #333; position: relative; &::before { content: ""; display: inline-block; position: absolute; left: -15px; width: 2px; height: 13px; background-color: #3576e0; border-radius: 1px; } } </style>
這裏呢,狗尾草選擇使用了<mavon-editor v-model="form.content"/>富文本編輯器,富文本編輯器不少哈。這裏就不作特殊說明,有使用遇到坎坷的童鞋呢,能夠留言諮詢哦。(你們能夠查看後期個人react前端文章詳情的回顯效果)
這裏的草稿箱呢,其實表面上看和列表頁是同樣的。可是呢。文章沒有寫完的依舊能夠放在草稿箱中。待發布的也能夠放在草稿箱中,這也就是像個徹底不一樣功能的模塊了。
article>draft.vue
<template> <div class="article-wrap"> <el-table :data="articleList" height="100%" stripe> <el-table-column prop="id" align="center" label="文章編號"> </el-table-column> <el-table-column prop="create_time" align="center" label="建立時間"> <template slot-scope="scope"> {{$moment(scope.row.create_time).format('YYYY-MM-DD HH:mm')}} </template> </el-table-column> <el-table-column prop="tags" align="center" label="標籤"> <template slot-scope="scope"> {{$utils.formatTableFont(scope.row.tags)}} </template> </el-table-column> <el-table-column prop="title" align="center" label="標題"> <template slot-scope="scope"> {{$utils.formatTableFont(scope.row.title)}} </template> </el-table-column> <el-table-column prop="title_image" align="center" label="標題圖片"> <template slot-scope="scope"> <img v-if="scope.row.title_image" class="title-img" :src="scope.row.title_image" /> <span v-else>-</span> </template> </el-table-column> <el-table-column prop="reader_number" align="center" label="閱讀數"> <template slot-scope="scope"> {{$utils.formatTableData(scope.row.reader_number)}} </template> </el-table-column> <el-table-column prop="good_number" align="center" label="點贊數"> <template slot-scope="scope"> {{$utils.formatTableData(scope.row.good_number)}} </template> </el-table-column> <el-table-column label="操做" align="center" fixed="right"> <template slot-scope="scope"> <el-button size="mini" @click.stop="$router.push({path:'/article/detail',query:{articleId:scope.row.id,status:2}})">編輯</el-button> <el-button size="mini" type="danger" @click.stop="handleDeleteDraft(scope.row.id)">刪除</el-button> </template> </el-table-column> </el-table> </div> </template> <script> export default { data() { return { articleList: [], params: { searchParams: '', page: 1, size: 10, status: 2 } } }, methods: { //獲取文章列表 async getArticleList() { try { const { articleData } = await this.$http.getRequest('/article/api/v1/article_list',this.params); this.articleList = articleData; } catch(err) { throw new Error('獲取文章列表失敗',err); } }, handleDeleteDraft(id) { this.$confirm('此操做將永久刪除該文章,不可復原, 是否繼續?', '刪除提示', { confirmButtonText: '肯定', cancelButtonText: '取消', type: 'warning' }).then(async () => { try { const result = await this.$http.postRequest('/article/api/v1/article_delete',{ id }); this.$message({ type: 'success', message: '文章已刪除!' }) this.getArticleList(); } catch(err) { throw new Error('刪除草稿失敗',err); } this.$message({ type: 'success', message: '刪除成功!' }); }).catch(() => { this.$message({ type: 'info', message: '已取消刪除' }); }); } }, mounted() { this.getArticleList(); } } </script> <style lang="less" scoped> .article-wrap { height: 100%; overflow: hidden; /deep/.title-img { width: 90px; height: 90px; } } </style>
這裏給你們理一下這裏的思路哈。
文章列表可編輯,編輯時,可選擇將文章進行更新發布或者移入草稿箱。發佈沒有啥說的,移入草稿箱呢,其實也就是將該文章的狀態進行更改。
最後呢,附上更改後的路由
router>index.js
import Vue from 'vue' import Router from 'vue-router' // import HelloWorld from '@/components/HelloWorld' Vue.use(Router) const _import = file => () => import('@/pages/' + file + '.vue'); const _import_ = file => () => import('@/components/' + file + '.vue'); const asyncRouterMap = []; const constantRouterMap = [ { path: '/login', name: 'Login', component: _import('login/index'), }, { path: '/', name: '概況', component: _import_('commons/Layout'), redirect: '/index', children: [ { path: '/index', name: '總覽', component: _import('home/index'), meta: { isAlive: false, auth: true, title: '概況數據' } } ] }, { path: '/article', name: '文章', component: _import_('commons/Layout'), redirect: '/article/publish', children: [ { path: '/article/publish', name: '文章發佈', component: _import('article/publish'), meta: { auth: true, isAlive: true, isFooter: false, title: '文章發佈' } }, { path: '/article/list', name: '列表', component: _import('article/list'), meta: { auth: true, isAlive: false, isFooter: true, title: '列表' } }, { path: '/article/draft', name: '草稿箱', component: _import('article/draft'), meta: { auth: true, isAlive: false, isFooter: true, title: '草稿箱' } }, { path: '/article/detail', name: '文章詳情', component: _import('article/detail'), meta: { auth: true, isAlive: false, isFooter: false, title: '文章詳情' } } ] }, { path: '/404', name: '404', component: _import('error/index'), meta: { title: "請求頁面未找到", auth: false }, }, { path: '*', meta: { title: "請求頁面未找到", auth: false }, redirect: '/404' } ]; const router = new Router({ mode: 'history', routes: constantRouterMap, linkActiveClass: "router-link-active", }); export default router
1.表單提交時的校驗。
2.不要爲了封裝而封裝。避免過分封裝。適用纔是王道。