Vue实战狗尾草博客管理平台第六章
源码地址:https://github.com/18291907191/gwc_manage
本章节内容
文章列表
文章详情
草稿箱
文章发布。
本章节内容呢,开发的很是随意哈,因为多数就是element-ui的使用,熟悉的童鞋,是可以很快完成本章节的内容的。
为啥文章模块会有这么多东西呢?
因为狗尾草想着以后,文章如果是待发布的话就需要一个地方去存放起来,一开始删除的文章呢,也将会被移入到草稿箱中,这样的话,文章就不会被随便的更改啦。
文章列表
是不是感觉非常轻松,一个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.不要为了封装而封装。避免过度封装。适用才是王道。
-------------------------------------------
个性签名:海到无边天作岸,山登绝顶人为峰!
如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!