Vue实战管理系统第六章
Vue实现狗尾草博客后台管理系统第六章
本章节内容
- 文章列表
- 文章详情
- 草稿箱
- 文章发布。
本章节内容呢,开发的很是随意哈,因为多数就是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挂载到全局,是可以直接使用的了。
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就可以了
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
文章详情
这里的主要功能呢就是根据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>
这里有一个重点,需要大家着重记一下的是,表单的校验,我们在添加好了以后,往往在需要提交的时候去进行判断,将不符合规则的表单给提示出来。不会使用的人,便会去写很多的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 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>
这里呢,狗尾草选择使用了
草稿箱
这里的草稿箱呢,其实表面上看和列表页是一样的。但是呢。文章没有写完的依旧可以放在草稿箱中。待发布的也可以放在草稿箱中,这也就是像个完全不同功能的模块了。
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.不要为了封装而封装。避免过度封装。适用才是王道。
下一章节
- 分页的处理
- 登录密码的加密处理
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!