源码地址:https://github.com/18291907191/gwc_manage

Vue实现狗尾草博客后台管理系统第六章

本章节内容

  1. 文章列表

  2. 文章详情

  3. 草稿箱

  4. 文章发布。

本章节内容呢,开发的很是随意哈,因为多数就是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: ,
size: ,
status:
},
}
},
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: %;
overflow: hidden;
/deep/.title-img {
width: 90px;
height: 90px;
}
}
</style>

这里呢,接口呢,都已经完成了。这类先不做说明,后面会单独将node.js抽离出来的哈。

不过呢,这里的formatTableData方法呢,是因为我们在做表格数据显示的时候呢,会有没有数据的情景,所以这里。我封装了一个方法,专门的针对表格的数据进行一个处理,在没有数据的时候呢就显示'-',如果是数字类型的呢,这里就显示0

给大家把方法贴出来,至于如果把方法挂在到全局,前面有讲到啦。这里也不需要这样做,因为直接方法utils文件中,utils挂载到全局,是可以直接使用的了。

utils>plugins.js

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()
} //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 = "";
} else {
formatTableData = val;
}
return formatTableData;
}; // 返回性别
sexStatus(status) {
if (!status) return
switch (status) {
case :
return '男';
break;
case :
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 /^[||||][-]{}$/.test(str);
case 'tel': //座机
return /^(\d{,}-\d{,})(-\d{,})?$/.test(str);
case 'card': //身份证
return /^\d{}|\d{}$/.test(str);
case 'account': //账号 ,长度4~16之间,只能包含数字,中文,字母和下划线
return /^(\w|[\u4E00-\u9FA5])*$/.test(str);
case 'pwd': //密码以字母开头,长度在6~18之间,只能包含字母、数字和下划线
return /^[a-zA-Z]\w{,}$/.test(str);
case 'postal': //邮政编码
return /[-]\d{}(?!\d)/.test(str);
case 'QQ': //QQ号
return /^[-][-]{,}$/.test(str);
case 'email': //邮箱
return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str);
case 'money': //金额(小数点2位)
return /^\d*(?:\.\d{,})?$/.test(str);
case 'URL': //网址
return /(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/.test(str);
case 'IP': //IP
return /((?:(?:[-]|[-]\\d|[]?\\d?\\d)\\.){}(?:[-]|[-]\\d|[]?\\d?\\d))/.test(str);
case 'date': //日期时间
return /^(\d{})\-(\d{})\-(\d{}) (\d{})(?:\:\d{}|:(\d{}):(\d{}))$/.test(str) || /^(\d{})\-(\d{})\-(\d{})$/.test(str);
case 'number': //数字
return /^[-]$/.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(, -) === 'String'
} isNumber(o) { //是否数字
return Object.prototype.toString.call(o).slice(, -) === 'Number'
} isObj(o) { //是否对象
return Object.prototype.toString.call(o).slice(, -) === 'Object'
} isArray(o) { //是否数组
return Object.prototype.toString.call(o).slice(, -) === 'Array'
} isDate(o) { //是否时间
return Object.prototype.toString.call(o).slice(, -) === 'Date'
} isBoolean(o) { //是否boolean
return Object.prototype.toString.call(o).slice(, -) === 'Boolean'
} isFunction(o) { //是否函数
return Object.prototype.toString.call(o).slice(, -) === 'Function'
} isNull(o) { //是否为null
return Object.prototype.toString.call(o).slice(, -) === 'Null'
} isUndefined(o) { //是否undefined
return Object.prototype.toString.call(o).slice(, -) === 'Undefined'
} isFalse(o) {
if (o == '' || o == undefined || o == null || o == 'null' || o == 'undefined' || o == || 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="">
<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="">
<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="">
<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="" v-model="form.describe" rows="" cols="" type="text" name="abstract">
</textarea>
<span style="font-size:16px;"><font style="color: #3576e0;">{{ - form.describe.length}}</font>/</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(, this.textNum);
}
}
},
data() {
return {
title: {
tit1: '文章标题',
tit2: '文章标签',
tit3: '文章摘要',
}, //标题
form: {
title: '',
tags: [],
title_image: '',
describe: '',
content: '',
status: ,
}, //提交数据
tagList: [], //标签选择器
textNum: ,
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 = ;
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 = ;
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: %;
height: %;
.abstract {
padding: 10px;
font-size: 14px;
}
/deep/.el-form-item__label {
text-align: left;
padding-right: ;
}
} </style>

这里有一个重点,需要大家着重记一下的是,表单的校验,我们在添加好了以后,往往在需要提交的时候去进行判断,将不符合规则的表单给提示出来。不会使用的人,便会去写很多的if判断,this.$message({type:'error',message:'xxx'})的方法给出来,

但是element_ui明明已经给了一个合理的解决方案了。大家就要学会去使用,给我们带来便捷!

  // 表单校验
validata() {
let isForm;
this.$refs.form.validate(valid => {
isForm = valid;
});
if (!isForm) {
return false;
}
return true;
},

这块的表单校验,通过给 form表单起一个名称,在提交的时候,调用validate方法就可以方便的达到校验表单的效果。根据返回结果去判断是否继续往下执行就可以啦。get到了有木有。

封装的一个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 ;
color: #;
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: ,
size: ,
status:
}
}
},
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: %;
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: '',
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.不要为了封装而封装。避免过度封装。适用才是王道。

下一章节

Vuex的进阶使用

最新文章

  1. Whatbeg&#39;s blog 文章列表
  2. [译]针对科学数据处理的统计学习教程(scikit-learn教程2)
  3. Java程序运行时,数据都保存到什么地方?
  4. BeeFree - 在线轻松创建电子邮件消息
  5. C++开发的基于UDP协议的聊天工具
  6. c# winform 打包部署 自定义界面 或设置开机启动
  7. Css3 - 动画旋转
  8. Java之绘制艺术图案
  9. (三)phpcms之文件目录
  10. 算法中的增长率(Rate of Growth)是什么意思?
  11. JS模式---发布、订阅模式
  12. haproxy反向代理配置示例
  13. Python二级-----------程序冲刺5
  14. 使用 Canal 实现数据异构
  15. 【轻松前端之旅】CSS入门
  16. Mac 下重新安装配置ibm Lotus 邮箱
  17. sort-归并排序
  18. python中的面向对象学习以及类的封装(这篇文章初学者一定要好好看)
  19. O​r​a​c​l​e​ ​1​1​g​ ​客​户​端​安​装​及​p​l​s​q​l​配​置
  20. 关于RabbitMQ一点

热门文章

  1. Flask 教程 第八章:粉丝
  2. kvm2
  3. SQLServer之GROUP BY语句
  4. 通过JTAG对比内核启动后text/rodata段内容
  5. golang中的pflag示例
  6. np.unique( )的用法
  7. 80道最新java基础部分面试题(七)
  8. HTML网页自动跳转(重定向)
  9. 匿名内部类和局部内部类只能访问final变量的原因
  10. WPF 精修篇 DataGrid 筛选