环境搭建 技术栈 博客系统采用 Vue.js + ElementUI 来搭建博客后台管理系统,以下是用到的相关技术:
VueJS
Vuex
EasyMock
Axios
element-ui
element-admin
mavon-edit
安装运行element-admin 下载代码
git clone https://github.com/PanJiaChen/vue-admin-template.git
运行项目
MacOS运行报错:
TypeError: fsevents.watch is not a function
解决方式:在package.json
添加以下内容重新执行npm install && npm run dev
即可解决
"optionalDependencies" : { "fsevents" : "~2.3.2" }
项目初始化 重命名项目
将目录名vue-admin-template
重命名为vue-blog-admin
编辑package.json
,并删除package-lock.json
重新运行项目
{ "name" : "vue-blog-admin" , "version" : "4.4.0" , "description" : "vue blog admin" , "author" : "baocai.guo@qq.com" }
汉化 编辑src/main.js
Eslint语法检查 ESLint 是一个用来按照规则给出报告的代码检测工具,使用它可以避免低级错误和统一代码的风格,也保证了代码的可读性。如果需要取消Eslint语法检查编辑vue.config.js
中将lintOnSave
指定为false
即可
头部组件设置 在src/settings.js
修改页面title
、固定头部、显示Logo
module .exports = { title : '阿才的博客' , fixedHeader : true , sidebarLogo : true }
修改标题栏ico
图标,将自己的ico
图标替换在public/favicon.ico
(注意favicon.ico
文件名不要改) 修改Logo
图标与文字从布局组件src/layout/index.vue
定位找到Logo
组件src/layout/components/Sidebar/Logo.vue
将Logo
图片拷贝到src/assets/logo-new.png
,修改如下内容
data() { return { title: '阿才的博客', logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png' } }
路由配置 脚手架会自动根据路由配置来生成左侧菜单,编辑src/router/index.js
export const constantRoutes = [ { path : '/blog' , component : Layout , redirect : '/blog/article' , name : 'Blog' , meta : { title : '博客管理' , icon : 'el-icon-notebook-2' }, children : [ { path : 'article' , name : 'Article' , component : () => import ('@/views/article/index' ), meta : { title : '文章管理' , icon : 'el-icon-notebook-1' } }, { path : 'category' , name : 'Category' , component : () => import ('@/views/category/index' ), meta : { title : '分类管理' , icon : 'el-icon-s-order' } }, { path : 'label' , name : 'Label' , component : () => import ('@/views/label/index' ), meta : { title : '标签管理' , icon : 'el-icon-collection-tag' } } ] }, { path : 'advert' , component : layout, children : [ { path : 'index' , name : 'Advert' , component : () => import ('@/views/advert/index' ), meta : { title : '广告管理' , icon : 'el-icon-picture-outline-round' } } ] }, { path : '/system' , component : Layout , redirect : '/system/user' , name : 'System' , meta : { title : '系统管理' , icon : 'el-icon-setting' }, children : [ { path : 'user' , name : 'User' , component : () => import ('@/views/user/index' ), meta : { title : '用户管理' , icon : 'el-icon-user-solid' } }, { path : 'role' , name : 'Role' , component : () => import ('@/views/role/index' ), meta : { title : '角色管理' , icon : 'el-icon-coin' } }, { path : 'menu' , name : 'Menu' , component : () => import ('@/views/menu/index' ), meta : { title : '菜单管理' , icon : 'el-icon-menu' } } ] }, ]
面包屑Dashboard中文显示 因为面包屑第1个标题是在组件中写死的,编辑src/components/Breadcrumb/index.vue
中修改为首页
<script> export default { methods: { getBreadcrumb() { if (!this.isDashboard(first)) { matched = [{path: '/dashboard', meta: {title: '首页'}}].concat(matched) } } } } </script>
头部快捷导航(标签栏导航) 添加导航栏组件 拷贝https://github.com/PanJiaChen/vue-element-admin/tree/master/src/layout/components/TagsView目录到`layout/components/TagsView`目录 编辑src/layout/components/index.js
导出TagsView
组件
export { default as TagsView } from './TagsView'
将src\layout\components\TagsView\index.vue
组件中的计算属性routes
改为获取所有路由,因为我们当前没有permission
相关状态,如果不修改,刷新页面后标签栏不会保留已打开标签页
<script> export default { computed: { routes() { // return this.$store.state.permission.routes return this.$router.options.routes } } } </script>
编辑src/layout/index.vue
引用标签页导航组件TagsView
<template> <div :class="classObj" class="app-wrapper"> <div class="hasTagsView main-container"> <div :class="{'fixed-header':fixedHeader}"> <navbar /> <!-- 引用标签导航栏--> <tags-view/> </div> <app-main /> </div> </div> </template> <script> import { Navbar, Sidebar, AppMain, TagsView } from './components' export default { name: 'Layout', components: { Navbar, Sidebar, AppMain, TagsView } } </script>
缓存 路由配置meta.name
与视图组件中的name
值一致, 不然缓存可能会失效。 编辑src/layout/components/AppMain.vue
添加keep-alive
相关代码
<template> <section class="app-main"> <transition name="fade-transform" mode="out-in"> <!-- <router-view :key="key" />--> <keep-alive :include="cacheViews"> <router-view :key="key"/> </keep-alive> </transition> </section> </template> <script> export default { name: 'AppMain', computed: { key() { return this.$route.path }, cacheViews() { return this.$store.state.tagsView.cacheViews } } } </script> <style lang="scss" scoped> .hasTagsView{ .app-main{ min-height: calc(100vh - 84px); } .fixed-header+.app-main{ padding-top: 84px; } } </style>
状态管理 复制https://raw.githubusercontent.com/PanJiaChen/vue-element-admin/master/src/store/modules/tagsView.js到`src/store/modules/tagsView.js` 编辑src/store/getters.js
const getters = { visitedViews : state => state.tagsView .visitedViews , cachedViews : state => state.tagsView .cachedViews } export default getters
编辑src/store/index.js
将tagsView
状态管理模块添加到Store
实例中
import Vue from 'vue' import Vuex from 'vuex' import tagsView from '@/store/modules/tagsView' Vue.use(Vuex) const store = new Vuex.Store({ modules: { tagsView }, getters }) export default store
Affix固钉 当在声明路由是添加了
meta.affix属性,则当前
tag会被固定在
tags-view中(不可被删除)。编辑
src\router\index.js路由配置首页
meta.affix=true`
{ path: '/', component: Layout, redirect: '/dashboard', children: [ { path: 'dashboard', name: 'Dashboard', component: () => import ('@/views/dashboard/index'), meta: { title: 'Dashboard', icon: 'dashboard', affix: true } } ] }
点击标签导航栏右键刷新触发src\layout\components\TagsView\index.vue
组件中的refreshSelectedTag
方法,会跳转到404错误页,那是因为没有配置/redrect
路由。
{ path: '/redirect', component: Layout, hidden: true , children: [ { path: '/redirect/: path(.*)', component: () => import('@/views/redirect/index') } ] }
将src\views\redirect\index.vue
组件添加到项目中,重写向到刷新当前页
<script> export default { created() { const { params, query } = this.$route const { path } = params this.$router.replace({ path: '/' + path, query }) } } </script>
对接MockJS模拟数据 部署EasyMock 参考链接: EasyMock
调用接口数据 现在EasyMock创建项目和测试接口,编辑vue.config.js
文件中使用devServer.proxy
选项进行代理配置
devServer : { port : port, open : true , overlay : { warnings : false , errors : true }, before : require ('./mock/mock-server.js' ), proxy : { [process.env .VUE_APP_BASE_API ]: { target : 'http://localhost:7308/mock/6573d745983c2f002b1f684d/blog-admin' , changeOrigin : true , pathRewrite : { ['^' + process.env .VUE_APP_BASE_API ]: '' } } } }
创建MockJS接口
URL
method
description
MockJS
`/test
get
test
{"code":20000,'message':"删除成功"}
创建测试接口,src/api/test.js
import request from '@/utils/request' export default { test ( ){ return request ({ url : '/test' , method : 'get' }) } }
在vue组件中调用接口,编辑`
<script> import test from '@/api/test' export default { created ( ) { this .fetchData () }, methods : { fetchData ( ) { test.test ().then (response => { console .log ('response' , response) }) } } } </script>
POST请求携带参数请求超时 修改MockJS接口/test请求方式为post,编辑src/api/test.js
发送post请求时携带参数:
import request from '@/utils/request' export default { test ( ) { return request ({ url : '/test' , method : 'post' , data : { 'name' : 'acai' } }) } }
Axios 如果发送Post请求,并且带上请求参数时,会一直报请求超时,如下:Uncaught (in promise) Error: timeout of 5000ms exceeded
。 问题原因:mock-server
中express
的中间件body-parser
导致的,表现为不带参数请求没有问题,带上参数就出现超时异常 解决方式:编辑mock/mock-server.js
,完成之后重新运行项目测试。
function registerRoutes (app ) { for (const mock of mocksForServer) { app[mock.type ](mock.url , bodyParser.json (), bodyParser.urlencoded ({ extended : true }), mock.response ) } } module .exports = app => { }
文章分类模块 分类列表 列表数据接口
URL
method
description
/article/category/search
post
文章类别分页条件查询列表
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data" : { "total" : "@integer(100, 200)" , "records|20" : [ { "id|+1" : 10 , "name" : "@cname" , "sort" : "@integer(0,9)" , "remark" : "@csentence(5, 15)" , "status|1" : [ 0 , 1 ] , } ] } }
API调用接口 编辑src/api/category.js
import request from '@/utils/request' export default { getList (query, current = 1 , size = 20 ) { return request ({ url : `/article/category/search` , method : 'post' , data : { ...query, current, size } }) } }
编辑src/views/category/index.vue
组件,获取分类数据
<template> </template> <script> import api from '@/api/category' export default { data() { return { list: [], page: { total: 0, // 数据总数 current: 1, // 页码 size: 20 // 每页展示数据 }, query: {} // 查询条件 } }, created() { this.fetchData() }, methods: { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response =>{ this.list = response.data.records this.page.total = response.data.total console.log(this.page.total, this.list) }) } } } </script>
列表模版 编辑src/views/category/index.vue
<template> <el-table :data="list" border style="width: 98%"> <el-table-column type="selection" width="40"></el-table-column> <el-table-column prop="id" label="序号" width="180"></el-table-column> <el-table-column prop="name" label="分类名称" width="180"></el-table-column> <el-table-column prop="remark" label="备注" width="280"></el-table-column> <el-table-column prop="sort" label="排序" width="180"></el-table-column> <el-table-column prop="status" label="状态" width="180"></el-table-column> <el-table-column fixed="right" label="操作"> <template slot-scope="scope"> <el-button @click="handleView(scope.row.id)" type="text" size="small">查看</el-button> <el-button @click="handleEdit(scope.row.id)" type="text" size="small">编辑</el-button> </template> </el-table-column> </el-table> </template> <script> import api from '@/api/category' export default { data() { return { list: [], page: { total: 0, // 数据总数 current: 1, // 页码 size: 20 // 每页展示数据 }, query: {} // 查询条件 } }, created() { this.fetchData() }, methods: { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response =>{ this.list = response.data.records this.page.total = response.data.total }) }, handleView(id) { console.log('view', id) }, handleEdit(id) { console.log('edit', id) } } } </script> <style scoped> .el-table{ margin-left: 12px; } </style>
状态码转名称 渲染后发现状态码,我们将状态列以转为名称,并且使用<el-tag>
标签包裹,其中通过filters
选项来定义过滤器来实现样式转换。 定义statusFilter
过滤器转换样式
<script> export default { filters: { statusFilter(status) { // 样式 const statusMap = {0: 'danger', 1: 'success'} return statusMap[status] } } } </script>
重构模板中的状态
<template> <el-table-column align="center" prop="status" label="状态" width="180"> <template slot-scope="scope"> <el-tag :type="scope.row.status | statusFilter"> {{ scope.row.status ? '正常':'禁用' }} </el-tag> </template> </el-table-column> </template>
分页查询 修改src\views\category\index.vue
,在template
标签中添加分页组件
<template> <div> <el-pagination background layout="prev, pager, next" :total="this.page.total" @current-change="handleCurrentChange" :current-page="page.current"></el-pagination> </div> </template> <script> export default { methods: { handleCurrentChange(val) { console.log('val', val) this.page.current = val this.fetchData() } } } </script>
条件查询 <template> <div> <el-form :inline="true" :model="query" class="demo-form-inline" size="small"> <el-form-item label="分类名称"> <el-input v-model="query.name" placeholder="分类名称"></el-input> </el-form-item> <el-form-item label="状态"> <el-select v-model="query.status" clearable filterable style="width: 90px"> <el-option v-for="item in statusOption" :key="item.code" :label="item.name" :value="item.code"/> </el-select> </el-form-item> <el-form-item> <el-button icon="el-icon-search" type="primary" @click="queryData">查询</el-button> <el-button icon="el-icon-reset" type="primary" @click="reset">重置</el-button> <el-button icon="el-icon-circle-plus-outline" type="primary">新增</el-button> </el-form-item> </el-form> </div> </template> <script> export default { methods: { queryData() { this.page.current = 1 this.fetchData() }, reset() { this.query = {} this.fetchData() } } } </script>
新增分类 添加新增分类接口
URL
method
description
/article/category
post
新增类别
MockJS
{ "code" : 20000 , "message" : "新增成功" }
API接口调用 编辑src\api\category.js
添加调用新增接口的方法
add (data ) { return request ({ url : `/article/category` , method : 'post' , data }) }
编辑src\views\category\edit.vue
中的submitForm
方法中判断校验是否通过,通过了则调用submitData
异步方法提交数据,代码如下:
<script> import api from '@/api/category' export default { methods: { submitForm(formName) { this.$refs[formName].validate((valid) => { if (valid) { // 验证通过,提交数据 this.submitData() } else { // 验证未通过 return false } }) }, async submitData() { const response = await api.add(this.formData) if (response === 20000) { this.$message({ message: '保存成功', type: 'success' }) this.handleClose() } else { this.$message({ message: '保存失败', type: 'error' }) } } } } </script>
弹窗实现 新增和修改功能共用一个组件,我们将它作为子组件引入到列表查询父组件中。编辑src\views\category\edit.vue
<template> <div> <el-dialog :title="title" :visible.sync="visible" :before-close="handleClose" width="500px" center> <el-form status-icon ref="formData" :model="formData" label-width="100px" label-position="right" style="width: 400px"> <el-form-item label="分类名称" prop="name"> <el-input v-model="formData.name"/> </el-form-item> <el-form-item label="状态" prop="status"> <el-radio-group v-model="formData.status"> <el-radio :label="1">正常</el-radio> <el-radio :label="0">禁用</el-radio> </el-radio-group> </el-form-item> <el-form-item label="排序" prop="sort"> <el-input-number style="width: 300px" v-model="formData.sort" :min="1" :max="1000"/> </el-form-item> <el-form-item label="备注" prop="remark"> <el-input v-model="formData.remark" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" placeholder="请输入备注"/> </el-form-item> <el-form-item> <el-button type="primary" @click="submitData('formData')" size="mini">确定</el-button> <el-button size="mini" @click="handleClose">取消</el-button> </el-form-item> </el-form> </el-dialog> </div> </template>
打开弹窗需要接受父组件属性,使用props接受父组件的属性。编辑src/views/category/edit.vue
<script> export default { props: { visible: { type: Boolean, default: false }, title: { type: String, default: '' }, formData: { type: Object, default: {} }, remoteClose: Function }, methods: { handleClose() { this.$refs['formData'].resetFields() // 注意不可以通过 this.visible = false来关闭,因为它是父组件的属性 this.remoteClose() } } } </script>
父组件category/index.vue
引用category/edit.vue
子组件实现弹窗,编辑src/views/category/index.vue
<script> import api from '@/api/category' import Edit from '@/views/category/edit.vue' export default { // eslint-disable-next-line vue/no-unused-components components: {Edit} } </script>
模版中使用edit自组件,编辑src/views/category/index.vue
<template> <div> <edit :title="edit.title" :visible="edit.visible" :form-data="edit.formData" :remote-close="edit.remoteClose"/> </div> </template>
在data
选项中声明传递给子组件的属性、方法,编辑src/views/category/index.vue
<script> export default { data() { return { edit: { title: '', visible: false, formData: {} } } }, methods: { remoteClose() { this.edit.visible = false this.edit.formData = {} this.fetchData() } } } </script>
为新增按钮绑定点击事件@click="openAdd"
弹出窗口
<el-button icon="el-icon-circle-plus-outline" type="primary" @click="openAdd">新增</el-button>
<script> export default { methods: { openAdd() { this.edit.title = '新增分类' this.edit.visible = true } } } </script>
校验表单数据 编辑src/views/category/edit.vue
新增窗口的el-form
上绑定属性:rules="rules"
<template> <div> <el-dialog> <el-form :rules="rules"></el-form> </el-dialog> </div> </template>
编辑src/views/category/edit.vue
在data
选项中添加rules
属性进行校验
data() { return { rules: { name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }], status: [{ required: true, message: '请选择状态', trigger: 'change' }], sort: [{ required: true, message: '请输入排序号', trigger: 'change' }] } } },
提交表单数据 新增API接口
URL
method
description
/article/category
post
新增类别
MockJS
{ "code" : 20000 , "message" : "新增成功" }
API接口调用 在src\api\category.js
添加调用新增接口的方法
add (data ) { return request ({ url : `/article/category` , method : 'post' , data }) }
编辑src\views\category\edit.vue
中的submitForm
方法中判断校验是否通过,通过了则调用submitData
异步方法提交数据
async submitData ( ) { const response = await api.add (this .formData ) if (response === 20000 ) { this .$message({ message : '保存成功' , type : 'success' }) this .handleClose () } else { this .$message({ message : '保存失败' , type : 'error' }) } }
分类修改 添加分类查询接口
URL
method
description
/article/category/{id}
get
分类查询接口
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data" : { "id" : 10 , "name" : "Java" , "sort" : "@integer(0,9)" , "remark" : "@csentence(5, 15)" , "status|1" : [ 0 , 1 ] , } }
添加分类修改接口
URL
method
description
/article/category
put
分类修改接口
MockJS
{ "code" : 20000 , 'message': "修改成功" }
API调用接口 编辑src\api\category.js
添加通过ID查询方法getById
和更新方法update
import request from '@/utils/request' export default { getByid (id ) { return request ({ url : `/article/category/${id} ` , method : 'get' }) }, update (data ) { return request ({ url : `/article/category` , method : 'put' , data }) } }
编辑src\views\category\index.vue
中的handleEdit
方法
handleEdit (id ) { console .log ('edit' , id) api.getByid (id).then (response => { if (response.code === 20000 ) { this .edit .formData = response.data this .edit .title = '编辑分类' this .edit .visible = true } }) }
提交修改后的数据 在src/views/category/edit.vue
组件中的submitData
方法加上一个判断this.formData.id
存在则为更新,否则是新增操作。
async submitData ( ) { let response = null if (this .formData .id ) { response = await api.update (this .formData ) } else { response = await api.add (this .formData ) } if (response === 20000 ) { this .$message({ message : '保存成功' , type : 'success' }) this .handleClose () } else { this .$message({ message : '保存失败' , type : 'error' }) } }
删除分类 增加删除分类接口
URL
method
description
/article/category/{id}
delete
删除分类
MockJS
{ "code" : 20000 , "message" : "删除成功" }
API接口调用 编辑src\api\category.js
添加deleteById
方法
deleteById (id ) { return request ({ url : `/article/category/${id} ` , method : 'delete' }) }
编辑src\views\category\index.vue
中的handleDelete
方法
handleDelete (id ) { console .log ('view' , id) this .$confirm('确认删除这条记录?' , '提示' , { confirmButtonText : '确定' , cancelButtonText : '取消' , type : 'warning' }).then (() => { api.deleteById (id).then (response => { this .$message({ type : response.code === 20000 ? 'success' : 'error' , message : response.message }) this .fetchData () }) }).catch (() => { console .log ('取消操作' ) }) }
标签模块 标签列表 列表数据接口
URL
method
description
/article/label/search
post
标签列表查询接口
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data" : { "total" : "@integer(100, 200)" , "id|+1" : 10 , "categoryName" : "@cname" , "name" : "@cname" , "createDate" : "@date" , } }
API接口调用 编辑src/api/label.js
import request from '@/utils/request' export default { getList (query, current = 1 , size = 20 ) { return request ({ url : `/article/label/search` , method : 'post' , data : { ...query, current, size } }) } }
编辑src/views/label/index.vue
调用API接口
<script> import api from '@/api/label' export default { data() { return { query: {}, page: { total: 0, current: 1, size: 20 } } }, created() { this.fetchData() }, methods: { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response => { console.log(response) }) } } } </script>
列表模版 编辑src/views/label/index.vue
<template> <div> <el-table :data="list" border style="width: 98%"> <el-table-column prop="id" label="序号" width="180"></el-table-column> <el-table-column prop="name" label="标签名称" width="180"></el-table-column> <el-table-column prop="categoryName" label="分类名称"></el-table-column> <el-table-column fixed="right" label="操作"> <template slot-scope="scope"> <el-button @click="handleEdit(scope.row.id)" type="text" size="small">编辑</el-button> <el-button @click="handleDelete(scope.row.id)" type="text" size="small">删除</el-button> </template> </el-table-column> </el-table> </div> </template> <script> import api from '@/api/label' export default { data() { return { query: {}, page: { total: 0, current: 1, size: 20 }, list: [] } }, created() { this.fetchData() }, methods: { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response => { console.log(response) this.list = response.data.records }) }, handleEdit(id) { console.log('edit', id) }, handleDelete(id) { console.log('delete', id) } } } </script> <style scoped> .el-table{ margin-left: 10px; } </style>
标签查询 分页查询 修改src\views\label\index.vue
,在template
标签中添加分页组件
<template> <div> <el-pagination background layout="prev, pager, next" :total="this.page.total" @current-change="handleCurrentChange" :current-page="page.current"></el-pagination> </div> </template> <script> export default { methods: { handleCurrentChange(current) { console.log(current) } } } </script>
条件查询 查询分类数据接口
URL
method
description
/article/category/list
get
查询分类数据接口
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data|8" : [ { "id|+1" : 10 , "sort" : "@integer(0,9)" , "status" : 1 , "createDate" : "@date" , } ] }
API调用分类接口 在src/api/label.js
添加getNormalList
方法获取所有正常状态的分类
getNormalList ( ) { return request ({ url : `article/category/list` , method : 'get' }) }
src/views/label/index.vue
,data
中声明categoryList
变量,钩子中调用getCategoryList
,在methods
声明getCategoryList
获取所有正常状态的分类
<script> import api from '@/api/label' export default { data() { return { categoryList: [] } }, created() { this.getCategoryList() }, methods: { getCategoryList() { api.getNormalList().then(response => { console.log('category response', response) this.categoryList = response.data }) }, } } </script>
添加条件查询模版 修改src\views\label\index.vue
,增加条件查询模板代码
<template> <div> <el-form :inline="true" :model="query" class="demo-form-inline" size="small"> <el-form-item label="标签名称"> <el-input v-model="query.name" placeholder="标签名称"></el-input> </el-form-item> <el-form-item label="分类名称"> <el-select v-model="query.categoryId" clearable filterable style="width: 90px"> <el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id"/> </el-select> </el-form-item> <el-form-item> <el-button icon="el-icon-search" type="primary" @click="queryData">查询</el-button> <el-button icon="el-icon-reset" type="primary" @click="reset">重置</el-button> <el-button icon="el-icon-circle-plus-outline" type="primary" @click="openAdd">新增</el-button> </el-form-item> </el-form> </div> </template> <script> export default { methods: { queryData() { console.log('check') this.page.current = 1 this.fetchData() }, reset() { console.log('reset') }, openAdd() { console.log('add') } } } </script>
重置功能:在methods
选项中添加reload
方法,模板中触发调用此方法
<script> export default { methods: { reset() { console.log('reset') this.query = {} this.fetchData() } } } </script>
标签新增 新增窗口实现 创建新增和修改的组件文件src\views\label\edit.vue
<template> <el-dialog :title="title" :visible.sync="visible" :before-close="handleClose" width="400px" center> <el-form status-icon ref="formData" :model="formData" label-width="100px" label-position="right" style="width: 300px"> <el-form-item label="标签名称" prop="name"> <el-input v-model="formData.name"/> </el-form-item> <el-form-item label="分类名称" prop="categoryId"> <el-select v-model="formData.categoryId" clearable filterable> <el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id"/> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm('formData')" size="mini">确定</el-button> <el-button @click="handleClose" size="mini">取消</el-button> </el-form-item> </el-form> </el-dialog> </template> <script> // 新增功能子组件中引用的属性都通过父组件传递过来,下面我们将它们通过props 声明出来,并且在methods 中声明handleClose 关闭弹窗和submitForm 确定提交表单方法 export default { // 接受父组件传递数据 props: { categoryList: { type: Array, // eslint-disable-next-line vue/require-valid-default-prop default: [] }, visible: { type: Boolean, default: false }, title: { type: String, default: '' }, formData: { type: Object, // eslint-disable-next-line vue/require-valid-default-prop default: {} }, // 用来关闭窗口 remoteClose: Function }, methods: { handleClose(done) { console.log('close', done) }, submitForm(formName) { console.log('submitForm', formName) } } } </script>
列表引用新增组件 在父组件src/views/label/index.vue
引用src/views/label/edit.vue
子组件实现弹窗
<script> import api from '@/api/label' import Edit from '@/views/label/edit.vue' export default { components: { // eslint-disable-next-line vue/no-unused-components Edit } } </script>
模版中引用Edit组件
<template> <div> <!-- 条件查询--> <!-- 数据列表--> <!-- 分页组件--> <!-- 新增窗口--> <edit :category-list="categoryList" :title="edit.title" :visible="edit.visible" :form-data="edit.formData" :remote-close="remoteClose"/> </div> </template>
在data
选项中声明传递给子组件的属性、方法remoteClose
<script> import Edit from '@/views/label/edit.vue' export default { components: { // eslint-disable-next-line vue/no-unused-components Edit }, data() { return { edit: { title: '', visible: false, formData: {} } } }, methods: { remoteClose() { console.log('close') } } } </script>
为新增按钮绑定点击事件@click="openAdd"
弹出窗口
<template> <el-button icon="el-icon-circle-plus-outline" type="primary" @click="openAdd">新增</el-button> </template> <script> export default { methods: { openAdd() { console.log('add') this.edit.title = '新增标签' this.edit.visible = true } } } </script>
关闭弹出窗口 当点击取消按钮或者右上角X将关闭窗口。在edit.vue
子组件中触发list.vue
父组件的remoteClose
方法来关闭窗口。编辑src/views/label/edit.vue
<script> export default { handleClose(done) { console.log('close', done) this.$refs['formData'].resetFields() this.remoteClose() } } </script>
编辑src/views/label/index.vue
中将this.edit.visiable=false
关闭窗口,刷新数据
<script> export default { methods: { remoteClose() { console.log('close') this.edit.formData = {} this.edit.visible = false this.fetchData() } } } </script>
标签校验数据 编辑src/views/label/edit.vue
新增窗口的el-form 上绑定属性:rules=”rules”
<template> <el-form :rules="rules"></el-form> </template>
在data
选项中添加rules
属性进行校验
<script> export default { data() { return { rules: { name: [ // trigger: 'blur' 表示在失去焦点时触发验证 { required: true, message: '请输入标签名称', trigger: 'blur' } ], categoryId: [ { required: true, message: '请选择分类名称', trigger: 'blur' } ] } } }, } </script>
提交表单数据 添加标签API接口
URL
method
description
/article/label
post
添加标签API接口
MockJS
{ "code" : 20000 , "message" : "新增成功" }
API调用接口 在src\api\label.js
添加调用新增接口的方法
add (data ) { return request ({ url : `/article/label` , method : 'post' , data }) }
编辑src\views\label\edit.vue
中的submitForm
方法中判断校验是否通过,通过了则调用submitData
异步方法提交数据
<script> import api from '@/api/label' export default { methods: { handleClose(done) { console.log('close', done) this.$refs['formData'].resetFields() this.remoteClose() }, submitForm(formName) { console.log('submitForm', formName) this.$refs[formName].validate((valid) => { if (valid) { this.submitData() } else { // 验证未通过 return false } }) }, async submitData() { const response = await api.add(this.formData) if (response.code === 20000) { this.$message({ message: '保存成功', type: 'success' }) // 关闭窗口 this.handleClose() } else { this.$message({ message: '新增失败', type: 'error' }) } } } } </script>
标签修改 需求分析 当点击编辑按钮后,弹出编辑窗口,并查询出标签信息渲染。修改后点击确定 提交修改数据。
查询标签API接口
URL
method
description
/article/label/{id}
get
查询标签API接口
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data" : { "id" : 8 , "categoryId|1" : "@integer(10, 17)" , "name" : "@cname" , "createDate" : "@date" , "updateDate" : "@date" } }
提交修改数据API
URL
method
description
/article/label
put
提交修改数据API
MockJS
{ "code" : 20000 , 'message': "修改成功" }
API调用接口 编辑src\api\label.js
添加通过ID查询方法getById
和更新方法update
getByid (id ) { return request ({ url : `/article/label/{id}` , method : 'get' }) }, update (data ) { return request ({ url : `/article/label` , method : 'put' , data }) }
编辑src\views\label\index.vue
中的handleEdit
方法做如下修改
<script> import api from '@/api/label' export default { methods: { handleEdit(id) { console.log('edit', id) api.getByid(id).then(resposne => { if (resposne.code === 20000) { console.log('response', resposne.data) this.edit.formData = resposne.data // 打开弹窗 this.edit.title = '编辑标签' this.edit.visible = true } }) } } } </script>
提交修改数据 编辑src/views/label/edit.vue
组件中的submitData
方法加上一个判断this.formData.id
存在则为更新,否则是新增操作
<script> export default { methods: { async submitData() { let response = null if (this.formData.id) { response = await api.update(this.formData) } else { response = await api.add(this.formData) } if (response.code === 20000) { this.$message({ message: '保存成功', type: 'success' }) // 关闭窗口 this.handleClose() } else { this.$message({ message: '新增失败', type: 'error' }) } } } } </script>
标签删除 需求分析 当点击删除按钮后, 弹出提示框。点击确定后,执行删除并刷新列表数据
删除标签API接口
URL
method
description
/article/label/{id}
delete
删除标签API接口
MockJS
{ "code" : 20000 , "message" : "删除成功" }
API接口调用 编辑src\api\label.js
添加deleteById
方法
deleteById (id ) { return request ({ url : `/article/label/{id}` , method : 'delete' }) }
编辑src\views\label\index.vue
中的handleDelete
方法
<script> export default { methods: { handleDelete(id) { console.log('delete', id) this.$confirm('确认删除这条数据吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 确认 api.deleteById(id).then(response => { // 提示信息 this.$message({ type: response.code === 20000 ? 'success' : 'error', message: response.message }) this.fetchData() }) }).catch(() => { console.log('取消') }) } } } </script>
文章管理模块 文章列表 文章列表API接口
URL
method
description
/article/article/search
post
文章列表API接口
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data" : { "total" : "@integer(100, 200)" , "records|20" : [ { "id|+1" : 10 , "title" : "@ctitle" , "viewCount" : "@integer(0, 100000)" , "thumhup" : "@integer(0, 100000)" , "ispublic|1" : [ 0 , 1 ] , "status|1" : [ 0 , 1 , 2 , 3 ] , "updateDate" : "@date" , } ] } }
调用API接口 创建src/api/article.js
import request from '@/utils/request' export default { getList (query, current=1 , size=20 ) { return request ({ url : `/article/article/search` , method : 'post' , data : { ...query, current, size } }) } }
创建src/views/article/index.vue
<script> import api from '@/api/article' export default { data() { return { list: [], page: { total: 0, current: 1, size: 20 }, query: {} } }, created() { this.fetchData() }, methods: { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response => { this.list = response.data.records this.page.total = response.data.total console.log(this.page) }) } } } </script>
列表模版 编辑src/views/article/index.vue
<template> <div class="app-container"> <el-table :data="list" border height-current-row style="width: 96%"> <!-- type="index"获取索引值,从1开始 ,label显示标题,prop 数据字段名,width列宽--> <el-table-column align="center" type="index" label="序号" width="60"/> <el-table-column align="center" prop="title" label="文章标题"/> <el-table-column align="center" prop="viewCount" label="浏览量"/> <el-table-column align="center" prop="thumhup" label="点赞数"/> <el-table-column align="center" prop="ispublic" label="是否公开"/> <el-table-column align="center" prop="status" label="状态"/> <el-table-column align="center" prop="updateDate" label="更新时间"/> <el-table-column align="left" label="操作" width="220"> <template slot-scope="scope"> <el-button size="mini" v-if="scope.row.status === 1" @click="openAudit(scope.row.id)" type="success">审核</el-button> <el-button size="mini" v-if="scope.row.status !== 1" @click="openAudit(scope.row.id)" type="danger">删除</el-button> </template> </el-table-column> </el-table> </div> </template>
使用el-tag渲染ispublic和status
<template> <div class="app-container"> <el-table> <el-table-column align="center" prop="ispublic" label="是否公开"> <template slot-scope="scope"> <el-tag v-if="scope.row.ispublic === 0" type="danger">不公开</el-tag> <el-tag v-if="scope.row.ispublic === 1" type="success">公开</el-tag> </template> </el-table-column> <el-table-column align="center" prop="status" label="状态"> <template slot-scope="scope"> <el-tag v-if="scope.row.status === 1">未审核</el-tag> <el-tag v-if="scope.row.status === 2" type="success">审核通过</el-tag> <el-tag v-if="scope.row.status === 3" type="success">审核未通过</el-tag> <el-tag v-if="scope.row.status === 0" type="success">已删除</el-tag> </template> </el-table-column> </el-table> </div> </template>
格式化日期格式,创建src/utils/date.js
export function dateFormat (date ) { const $this = new Date (date) === 'Invalid Date' ? new Date (date.substr (0 , 19 )) : new Date (date) var timestamp = parseInt (Date .parse ($this)) / 1000 function zeroize (num ) { return (String (num).length === 1 ? '0' : '' ) + num } var curTimestamp = parseInt (new Date ().getTime () / 1000 ) var timestampDiff = curTimestamp - timestamp var curDate = new Date (curTimestamp * 1000 ) var tmDate = new Date (timestamp * 1000 ) const Y = tmDate.getFullYear (); const m = tmDate.getMonth () + 1 ; const d = tmDate.getDate () const H = tmDate.getHours (); const i = tmDate.getMinutes (); const s = tmDate.getSeconds () if (timestampDiff < 60 ) { return '刚刚' } else if (timestampDiff < 3600 ) { return Math .floor (timestampDiff / 60 ) + '分钟前' } else if (curDate.getFullYear () === Y && curDate.getMonth () + 1 === m && curDate.getDate () === d) { return '今天 ' + zeroize (H) + ':' + zeroize (i) + ':' + zeroize (s) } else { var newDate = new Date ((curTimestamp - 86400 ) * 1000 ) if (newDate.getFullYear () === Y && newDate.getMonth () + 1 === m && newDate.getDate () === d) { return '昨天 ' + zeroize (H) + ':' + zeroize (i) + ':' + zeroize (s); } else if (curDate.getFullYear () === Y) { return zeroize (m) + '月' + zeroize (d) + '日 ' + zeroize (H) + ':' + zeroize (i) + ':' + zeroize (s) } else { return Y + '年' + zeroize (m) + '月' + zeroize (d) + '日 ' + zeroize (H) + ':' + zeroize (i) + ':' + zeroize (s) } } } export function format (date, format = 'yyyy-MM-dd hh:mm:ss' ) { const $this = new Date (date) === 'Invalid Date' ? new Date (date.substr (0 , 19 )) : new Date (date) const o = { 'M+' : $this.getMonth () + 1 , 'd+' : $this.getDate (), 'h+' : $this.getHours (), 'm+' : $this.getMinutes (), 's+' : $this.getSeconds (), 'q+' : Math .floor (($this.getMonth () + 3 ) / 3 ), 'S' : $this.getMilliseconds () } if (/(y+)/ .test (format)) { format = format.replace (RegExp .$1 , ($this.getFullYear () + '' ).substr (4 - RegExp .$1 .length )) } for (var k in o) { if (new RegExp ('(' + k + ')' ).test (format)) { format = format.replace (RegExp .$1 , (RegExp .$1 .length === 1 ) ? (o[k]) : (('00' + o[k]).substr (('' + o[k]).length ))) } } return format }
编辑src/views/article/index.vue
格式化更新时间字符串
<template> <div class="app-container"> <el-table> <el-table-column align="center" prop="updateDate" label="更新时间"> <template slot-scope="scope"> {{ getFormat(scope.row.updateDate) }} </template> </el-table-column> </el-table> </div> </template> <script> import { format } from '@/utils/date' export default { methods: { getFormat(date) { return format(date) } } } </script>
分页查询 修改src\views\article\index.vue
,在template
标签中添加分页组件
<template> <el-pagination background layout="prev, pager, next" :total="this.page.total" @current-change="handleCurrentChange" :current-page="page.current"></el-pagination> </template> <script> export default { methods: { handleCurrentChange(val) { console.log(val) this.page.current = val this.fetchData() } } } </script>
条件查询 修改src\views\article\index.vue
,增加条件查询模板代码
<template> <div class="app-container"> <el-form :inline="true" size="small" ref="formSearch"> <el-form-item label="文章标题"> <el-input v-model.trim="query.title"></el-input> </el-form-item> <el-form-item label="状态"> <el-select v-model="query.status" clearable filterable style="width: 100%"> <el-option :value="1" label="未审核"></el-option> <el-option :value="2" label="审核通过"></el-option> <el-option :value="3" label="审核未通过"></el-option> <el-option :value="0" label="已删除"></el-option> </el-select> </el-form-item> <el-form-item> <el-button icon='el-icon-search' type="primary" @click="queryData">查询</el-button> <el-button icon='el-icon-search' type="info" @click="reset('formSearch')">重置</el-button> </el-form-item> </el-form> </div> </template> <script> export default { methods: { queryData() { console.log('queryData') this.page.current = 1 this.fetchData() }, reset(formName) { console.log('reset', formName) this.query = {} this.fetchData() } } } </script>
文章审核 需求分析 审核和查看详情功能共用一个组件,我们将它作为子组件引入到列表查询父组件中,下面先将组件定义出来。如果是审核功能,则显示审核通过和审核不通过按钮,如果是查看详情则不显示这两个按钮
审核组件 创建审核和查看详情的组件文件src/views/article/audit.vue
<template> <el-dialog :title="title" :visible.sync="visible" :before-close="handleClose" width="70%"/> </template> <script> export default { props: { id: null, // eslint-disable-next-line vue/require-prop-type-constructor isAudit: true, visible: { type: Boolean, default: false }, title: { type: String, default: '' }, remoteClose: Function }, methods: { handleClose() { this.remoteClose() } } } </script>
文章列表引用审核组件 在父组件article/index.vue
引用article/audit.vue
子组件实现弹窗,编辑src/views/article/index.vue
<template> <div class="app-container"> <audit :id="audit.id" :isAudit="audit.isAudit" :title="audit.title" :visible="audit.visible" :remote-close="remoteClose"></audit> </div> </template> <script> import api from '@/api/article' import { format } from '@/utils/date' import Audit from '@/views/article/audit.vue' export default { components: { Audit }, data() { return { audit: { id: null, isAudit: true, visible: false, title: '' } } }, methods: { remoteClose() { this.audit.visible = false this.fetchData() } } } </script>
在article/index.vue
中的openAudit
方法中打开弹窗 ,在remoteClose
方法关闭弹窗
<script> export default { methods: { openAudit(id) { console.log('打开审核弹窗', id) this.audit.id = id this.audit.isAudit = true this.audit.title = '审核文章' this.audit.visible = true } } } </script>
渲染审核文章弹窗模版 修改src/views/article/audit.vue
添加表单模板
<template> <el-dialog :title="title" :visible.sync="visible" :before-close="handleClose" width="70%"> <el-form ref="formData" :model="formData" label-width="100px" label-position="right"> <el-form-item label="标题"> <el-input v-model="formData.title" readonly/> </el-form-item> <el-form-item label="标签"></el-form-item> <el-form-item label="主图"> <img :src="formData.imageUrl" class="el-avatar" style="width: 178px" height="178px"> </el-form-item> <el-form-item label="是否公开"> <el-radio-group v-model="formData.ispublic" disabled> <el-radio :label="0">不公开</el-radio> <el-radio :label="1">公开</el-radio> </el-radio-group> </el-form-item> <el-form-item label="简介"> <el-input v-model="formData.summary" type="textarea" :autosize="{ minRows: 2 }" readonly/> </el-form-item> <el-form-item label="内容"></el-form-item> <el-form-item align="center" v-if="isAudit"> <el-button @click="auditSuccess()" type="primary">审核通过</el-button> <el-button @click="auditFailed()" type="danger">审核不通过</el-button> </el-form-item> </el-form> </el-dialog> </template> <script> export default { props: { id: null, // eslint-disable-next-line vue/require-prop-type-constructor isAudit: true, visible: { type: Boolean, default: false }, title: { type: String, default: '' }, remoteClose: Function }, data() { return { formData: {} } }, methods: { handleClose() { this.remoteClose() }, auditSuccess() { console.log('auditSuccess') }, auditFailed() { console.log('auditFailed') } } } </script>
查询文章数据API
URL
method
description
/article/article/{id}
get
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data" : { "id" : 1 , "title" : "@ctitle" , "labelIds|1-5" : [ '@integer(10 , 24 )'] , "summary" : "@csentence(10, 30)" , "imageUrl" : "https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg" , "mdContent" : "# 1号标题\n# 2号标题" , "htmlContent" : "<h1><a id=\"1_0\"></a>1号标题</h1>\n<h1><a id=\"2_1\"></a>2号标题 < /h1 > \n " , "ispublic|1" : [ 0 , 1 ] , "updateDate" : "@date" , "createDate" : "@date" } }
调用API接口 编辑 src\api\article.js
添加通过ID查询方法getById
getById (id ) { return request ({ url : `/article/article/{id}` , method : 'get' }) }
编辑src\views\article\audit.vue
添加getArticleById
方法获取文章详情。使用watch
选项对visiable
属性做监听,当visible
为true
时,则弹出了窗口,就调用this.getArticleById
获取数据
<script> import api from '@/api/article' export default { watch: { visible(val) { if (val) { this.getArticleById() } } }, props: { id: null, // eslint-disable-next-line vue/require-prop-type-constructor isAudit: true, visible: { type: Boolean, default: false }, title: { type: String, default: '' }, remoteClose: Function }, data() { return { formData: {} } }, methods: { getArticleById() { // id通过父组件传递过来的 api.getById(this.id).then(response => { this.formData = response.data }) } } } </script>
获取分类和标签数据 需求分析 当前标签行没有渲染数据,因为要根据查询出来的labelIds 回显标签。 查询出所有分类下的标签数据,来渲染到Cascader
多级选择器中;在通过labelIds
进行回显到 标签框中
标签分类API接口
URL
method
description
/article/category/label/list
get
获取所有正常状态的分类和标签
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data|5" : [ { "id|+1" : 1 , "name" : "@cname" , "labelList|3" : [ { "id|+1" : 10 , "name" : '@word' } ] } ] }
调用API接口 编辑src/api/category.js
添加调用接口代码如下
getCategoryAndLabel ( ) { return request ({ url : `/article/category/label/list` , method : 'get' }) }
编辑src\views\article\audit.vue
<script> import api from '@/api/article' import categoryApi from '@/api/category' export default { watch: { visible(val) { if (val) { this.getLabelOption() } } }, data() { return { labelOptions: [] } }, methods: { getLabelOption() { categoryApi.getCategoryAndLabel().then(response => { this.labelOptions = response.data }) } } } </script>
渲染审核文章标签 编辑src/views/article/audit.vue
修改标签行代码如下
<el-form-item label="标签"> <el-cascader disabled style="display: block;width: 30%" v-model="formData.labelIds" :options="labelOptions" :props="{ multiple: true, emitPath: false, children: 'labelList', value: 'id', label: 'name' }" /> </el-form-item>
文章内容使用md编辑器 安装md编辑器
npm install mavon-editor --save
mavon-editor
组件注册到文章核审窗口,编辑src/views/article/audit.vue
<template> <el-form-item label="内容"> <mavon-editor ref="md" v-model="formData.mdContent" :editable="false"/> </el-form-item> </template> <script> import api from '@/api/article' import categoryApi from '@/api/category' // 引入md组件和样式 import { mavonEditor } from 'mavon-editor' import 'mavon-editor/dist/css/index.css' export default { components: { // eslint-disable-next-line vue/no-unused-components mavonEditor } } </script>
提交审核 审核API接口
URL
method
description
/article/article/audit/success/{id}
get
文章审核API成功接口
MockJS
{ "code" : 20000 , "message" : "操作成功" }
URL
method
description
/article/article/audit/fail/{id}
get
文章审核API失败接口
MockJS
{ "code" : 20000 , "message" : "操作成功" }
调用API接口 编辑src\api\article.js
添加调用接口的方法
auditSuccess (id ) { return request ({ url : `/article/article/audit/success/{id}` , method : 'get' }) }, auditFailed (id ) { return request ({ url : `/article/article/audit/fail/{id}` , method : 'get' }) }
编辑src\views\article\audit.vue
中的auditSuccess
和auditFail
分别调用API接口。当点击审核通过或审核不通过按钮后, 弹出消息提示框。点击确定后,调用API接口,然后关闭窗口
<script> export default { methods: { auditSuccess() { console.log('auditSuccess') this.$confirm('确认审核通过吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { api.auditSuccess(this.id).then(response => { this.$message({ type: 'success', message: '审核文章提交成功' }) // 关闭窗口 this.remoteClose() }).catch(() => { console.log('审核文章API接口失败') }) }) }, auditFailed() { console.log('auditFailed') this.$confirm('确认审核通过吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { api.auditFailed(this.id).then(response => { this.$message({ type: 'success', message: '审核文章提交成功' }) // 关闭窗口 this.remoteClose() }).catch(() => { console.log('审核文章API接口失败') }) }) }, } } </script>
查看文章 查看文章详情功能直接复用审核组件src/views/article/audit.vue
,在点击查看按钮后,将this.audit.isAudit = false
,弹出窗口即可。编辑src/views/article/index.vue
添加查看按钮
<template> <div class="app-container"> <el-table> <el-table-column align="left" label="操作" width="220"> <template slot-scope="scope"> <el-button size="mini" @click="openView(scope.row.id)" type="primary">查看</el-button> <el-button size="mini" v-if="scope.row.status === 1" @click="openAudit(scope.row.id)" type="success">审核</el-button> <el-button size="mini" v-if="scope.row.status !== 1" @click="handleDelete(scope.row.id)" type="danger">删除</el-button> </template> </el-table-column> </el-table> </div> </template> <script> export default { methods: { openView(id) { this.audit.id = id this.audit.isAudit = false this.audit.title = '文章详情' this.audit.visible = true } } } </script>
删除文章 需求分析 当点击删除按钮后, 弹出提示框。点击确定后,会删除文章表和文章标签关系表中的数据(后台处理),并刷新列表数据
删除文章API接口
URL
method
description
/article/article/{id}
delete
根据ID删除文章
MockJS
{ "code" : 20000 , "message" : "删除成功" }
调用API接口 编辑src\api\article.js
添加deleteById
方法
deleteById (id ) { return request ({ url : `/article/article/{id}` , method : 'delete' }) }
编辑src\views\article\index.vue
中的handleDelete
方法
<script> export default { methods: { handleDelete(id) { console.log('delete', id) this.$confirm('确认删除这条记录吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { api.deleteById(id).then(response => { this.$message({ type: 'success', message: '删除成功' }) this.fetchData() }) }).catch(() => { console.log('取消删除') }) } } } </script>
广告模块 广告列表 广告列表API接口
URL
method
description
/article/advert/search
post
广告列表API接口
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data" : { "total" : "@integer(100, 200)" , "records|20" : [ { "id|+1" : 10 , "title" : "@ctitle" , "imageUrl" : "https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg" , "advertUrl" : "www.mengxuegu.com" , "advertTarget|1" : [ "_blank" , "_self" ] , "position" : 1 , "status|1" : [ 0 , 1 ] , "sort" : "@integer(1, 9)" , "createDate" : "@date" , "updateDate" : "@date" } ] } }
调用API接口 创建src/api/advert.js
import request from '@/utils/request' export default { getList (query, current=1 , size=20 ) { return request ({ url : `/article/advert/search` , method : 'post' , data : { ...query, current, size } }) } }
编辑src/views/advert/index.vue
<script> import api from '@/api/advert' export default { data() { return { query: {}, page: { total: 0, current: 1, size: 20 } } }, created() { this.fetchData() }, methods: { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response => { console.log('response', response) }) } } } </script>
渲染列表模版 编辑src/views/advert/index.vue
<template> <div class="app-container"> <el-table :data="list" border highlight-current-row style="width: 100%"> <el-table-column align="center" label="序号" prop="id" width="60"/> <el-table-column align="center" label="广告标题" prop="title"/> <el-table-column align="center" label="广告图片"> <template slot-scope="scope"> <el-image :src="scope.row.imageUrl" :preview-src-list="[scope.row.imageUrl]" style="width: 90px;height: 60px"/> </template> </el-table-column> <el-table-column align="center" label="广告链接" prop="advertUrl"/> <el-table-column align="center" label="状态"> <template slot-scope="scope"> <el-tag v-if="scope.row.status === 0" type="danger">禁用</el-tag> <el-tag v-if="scope.row.status === 1" type="success">正常</el-tag> </template> </el-table-column> <el-table-column align="center" label="排序" prop="sort"/> <el-table-column align="center" label="操作"> <template slot-scope="scope"> <el-button size="mini" type="primary" @click="handleEdit(scope.row.id)">编辑</el-button> <el-button size="mini" type="danger" @click="handleDelete(scope.row.id)">删除</el-button> </template> </el-table-column> </el-table> </div> </template> <script> export default { data() { return { list: [] } }, create() { this.fetchData() }, methods: { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response => { console.log('response', response) this.list = response.data.records }) }, handleEdit(id) { console.log('edit', id) }, handleDelete(id) { console.log('delete', id) } } } </script>
分页查询 编辑src\views\advert\index.vue
,在template
标签中添加分页组件
<template> <div class="app-container"> <el-pagination background layout="prev, pager, next" :total="this.page.total" :page-size="this.page.size" @current-change="handleCurrentChange" :current-page="page.current"></el-pagination> </div> </template> <script> export default { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response => { console.log('response', response) this.list = response.data.records this.page.total = response.data.total }) }, handleCurrentChange(val) { console.log('current', val) this.page.current = val this.fetchData() } } </script>
条件查询 编辑src\views\advert\index.vue
,增加条件查询模板
<template> <div class="app-container"> <el-form :inline="true" size="small" ref="formSearch"> <el-form-item label="广告标题"> <el-input v-model.trim="query.title"></el-input> </el-form-item> <el-form-item label="状态"> <el-select v-model="query.status" clearable filterable> <el-option v-for="item in statusOptions" :key="item.code" :label="item.name" :value="item.code"/> </el-select> </el-form-item> <el-form-item> <el-button icon='el-icon-search' type="primary" @click="queryData">查询</el-button> <el-button icon='el-icon-refresh' type="info" @click="reset('formSearch')">重置</el-button> <el-button icon='el-icon-add-location' type="primary" @click="add">新增</el-button> </el-form-item> </el-form> </div> </template> <script> import api from '@/api/advert' const statusOptions = [ { code: 0, name: '禁用' }, { code: 1, name: '正常' } ] export default { data() { return { statusOptions } }, methods: { queryData() { this.fetchData() }, reset(formName) { this.query = {} this.fetchData() }, add() { console.log('add') } } } </script>
广告新增 需求分析 点击新增按钮后,对话框形式弹出新增窗口。输入广告信息后,点击确定提交表单数据;
新增弹窗模版 创建新增和修改的组件文件src\views\advert\edit.vue
<template> <el-dialog :title="title" :visible.sync="visible" :before-close="handleClose" width="600px"> <el-form status-icon ref="formData" :model="formData" label-width="100px" label-position="right" style="width: 500px"> <el-form-item label="广告图片" prop="imageUrl"> <el-upload class="avatar-uploader" accept="image/*" action="" :show-file-list="false" :http-request="uploadMainImg"> <img v-if="formData.imageUrl" :src="formData.imageUrl" class="avatar"> <i v-else class="el-icon-plus avatar-uploader-icon"/> </el-upload> </el-form-item> <el-form-item label="广告标题" prop="title"> <el-input v-model="formData.title"/> </el-form-item> <el-form-item label="广告链接" prop="advertUrl"> <el-input v-model="formData.adverUrl"/> </el-form-item> <el-form-item label="跳转方式" prop="advertTarget"> <el-select v-model="formData.advertTarget" clearable style="width: 185px"> <el-option label="新窗口打开" value="_blank"/> <el-option label="当前窗口打开" value="_self"/> </el-select> </el-form-item> <el-form-item label="广告位置" prop="position"> <el-input v-model="formData.position"/> </el-form-item> <el-form-item label="状态" prop="status"> <el-radio-group v-model="formData.status"> <el-radio :label="1">正常</el-radio> <el-radio :label="0">禁用</el-radio> </el-radio-group> </el-form-item> <el-form-item label="排序" prop="sort"> <el-input-number style="width: 300px" v-model="formData.sort" :min="1" :max="100"/> </el-form-item> </el-form> <div slot="footer" class="dialog-footer"> <el-button size="mini" @click="handleClose">取消</el-button> <el-button size="mini" @click="submitForm('formData')">提交</el-button> </div> </el-dialog> </template> <script> export default { // 接受父组件传递数据 props: { visible: { type: Boolean, default: false }, title: { type: String, default: '' }, formData: { type: Object, default: {} }, remoteClose: Function }, data() { return { title: '', visible: false, formData: {} } }, methods: { uploadMainImg() { console.log('upload main img') }, handleClose() { console.log('close') } } } </script> <style scoped> .avatar-uploader .el-upload { border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; } .avatar-uploader .el-upload:hover { border-color: #409EFF; } .avatar-uploader-icon { font-size: 28px; color: #8c939d; width: 178px; height: 178px; line-height: 178px; text-align: center; } .avatar { width: 178px; height: 178px; display: block; } </style>
列表组件引用新增组件 编辑src/views/advert/index.vue
<template> <div class="app-container"> <edit :title="edit.title" :visible="edit.visible" :oldImageUrl="edit.oldImageUrl" :formData="edit.formData" :remote-close="remoteClose" /> </div> </template> <script> import Edit from '@/views/advert/edit.vue' export default { components: { Edit }, data() { return { edit: { title: '', visible: false, oldImageUrl: null, formData: { imageUrl: null } } } }, methods: { add() { console.log('add') this.edit.title = '新增广告' this.edit.visible = true }, remoteClose() { // 子组件关闭窗口调用改方法,然后将visible的值通过props的方法传递给子组件 console.log('remote close') this.edit.visible = false } } } </script>
关闭窗口 当点击取消按钮或者右上角X将关闭窗口。在edit.vue
子组件中触发index.vue
父组件的remoteClose
方法来关闭窗口。
handleClose ( ) { console .log ('close' ) this .remoteClose () }
在父组件index.vue中remoteClose方法中将visible的值传递给子组件
remoteClose ( ) { console .log ('remote close' ) this .edit .visible = false }
上传删除图片 上传图片API接口
URL
method
description
/article/file/upload
post
上传图片API接口
MockJS
{ "code" : 20000 , "message" : "上传成功" , "data" : "https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg" }
删除图片API接口
URL
method
description
/article/file/delete
delete
删除图片API接口
MockJS
{ "code" : 20000 , 'message': "删除成功" }
调用API接口 因为图片操作在多个模块中都要使用,所以我们为它单独创建个api文件src/api/common.js
import request from '@/utils/request' export default { uploadImg (data = {} ) { return request ({ url : `/article/file/upload` , method : 'post' , data }) }, deleteImg (imageUrl ) { return request ({ url : `/article/file/delete` , method : 'delete' , params : { 'fileUrl' : imageUrl } }) } }
编辑src/views/advert/edit.vue
edit组件中调用上传、删除图片API接口
<script> export default { methods: { uploadMainImg(file) { console.log('upload main img') // 封装表单对象 const data = new FormData() data.append('file', file.file) console.log('data', file) api.uploadImg(data).then(response => { // 防止多次上传把上传的图片删除 this.deleteImg() this.formData.imageUrl = response.data }).catch(() => { this.$message({ showClose: true, message: '图片上传失败', type: 'error' }) }) }, deleteImg() { if (this.formData.imageUrl && this.formData.imageUrl !== this.oldImageUrl) { api.deleteImg(this.formData.imageUrl) } }, } } </script>
校验表单数据 编辑edit.vue
新增窗口的el-form
上绑定属性:rules="rules"
<template> <el-dialog> <<el-form :rules="rules"></el-form> </el-dialog> </template>
在data
选项中添加rules
属性进行校验
<script> export default { data() { return { rules: { imageUrl: [ { required: true, message: '请上传广告图片', trigger: 'blur' } ], title: [ { required: true, message: '请输入广告标题', trigger: 'blur' } ], advertUrl: [ { required: true, message: '请输入广告链接', trigger: 'blur' } ], status: [ { required: true, message: '请选择状态', trigger: 'blur' } ], position: [ { required: true, message: '请输入广告位置', trigger: 'blur' } ], sort: [ { required: true, message: '请输入排序', trigger: 'blur' } ] } } }, } </script>
提交表单数据 当点击新增窗口中的确认按钮时, 提交表单数据,后台API服务接口响应新增成功或失败
新增广告API接口
URL
method
description
/article/advert
post
新增广告API接口
MockJS
{ "code" : 20000 , "message" : "新增成功" }
调用API接口 编辑src\api\advert.js
添加调用新增接口的方法
add (data ) { return request ({ url : `/article/advert` , method : 'post' , data }) }
编辑src\views\advert\edit.vue
中的submitForm
方法中判断校验是否通过,通过了则判断选择的类型,把对应类型中,不需要的到它设为 null。 调用submitData
异步方法提交数据
<script> export default { methods: { handleClose() { console.log('close') this.remoteClose() }, async submitData() { const response = await advertAPI.add(this.formData) if (response.code === 20000) { this.$message({ message: '保存成功', type: 'success' }) // 关闭窗口 this.handleClose() } else { this.$message({ message: '保存失败', type: 'error' }) } }, submitForm(formName) { this.$refs[formName].validate((valid) => { if (valid) { this.submitData() } else { return false } }) } } } </script>
广告修改 需求分析 当点击编辑按钮后,弹出编辑窗口,并查询出广告相关信息进行渲染。修改后点击确定提交修改后的数据。
查询广告API接口
URL
method
description
/article/advert/{id}
get
查询广告API接口
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data" : { "id" : 10 , "title" : "@ctitle" , "imageUrl" : "https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg" , "advertUrl" : "www.mengxuegu.com" , "advertTarget|1" : [ "_blank" , "_self" ] , "position" : 1 , "status|1" : [ 0 , 1 ] , "sort" : "@integer(1, 9)" , "createDate" : "@date" , "updateDate" : "@date" } }
修改广告数据API接口
URL
method
description
/article/advert
put
修改广告数据API接口
MockJS
{ "code" : 20000 , 'message': "修改成功" }
调用API接口 编辑src\api\advert.js
添加通过ID查询方法getById
和更新方法update
getById (id ) { return request ({ url : `/article/advert/{id}` , method : 'get' }) }, update (data ) { return request ({ url : `/article/advert` , method : 'put' , data }) }
编辑src\views\advert\index.vue
中的handleEdit
方法
<script> export default { methods: { handleEdit(id) { console.log('edit', id) api.getById(id).then(response => { if (response.code === 20000) { this.edit.formData = response.data this.edit.oldImageUrl = response.data.imageUrl } }) this.edit.title = '编辑广告' this.edit.visible = true } } } </script>
提交修改后的数据 编辑src/views/advert/edit.vue
组件中的submitData
方法加上一个判断this.formData.id
存在则为更新,否则是新增操作。
<script> export default { methods: { async submitData() { let response = null if (this.formData.id) { response = await advertAPI.update(this.formData) } else { response = await advertAPI.add(this.formData) } if (response.code === 20000) { this.$message({ message: '保存成功', type: 'success' }) // 关闭窗口 this.handleClose() } else { this.$message({ message: '保存失败', type: 'error' }) } }, submitForm(formName) { this.$refs[formName].validate((valid) => { if (valid) { this.submitData() } else { return false } }) } } } </script>
广告删除 需求分析 当点击删除按钮后, 弹出提示框,点击确定后,执行删除并刷新列表数据
删除广告API接口
URL
method
description
/article/advert/{id}
delete
删除广告API接口
MockJS
{ "code" : 20000 , "message" : "删除成功" }
调用API接口 编辑src\api\advert.js
添加deleteById
方法
deleteById (id ) { return request ({ url : `/article/advert/{id}` , method : 'delete' }) }
编辑src\views\advert\index.vue
中的handleDelete
方法
<script> export default { methods: { handleDelete(id) { console.log('delete', id) this.$confirm('确认删除这条记录吗', '提示', { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' }).then(() => { // 确认 api.deleteById(id).then(response => { this.$message({ type: response.code === 20000 ? 'success' : 'error', message: response.message }) // 刷新列表 this.fetchData() }) }).catch(() => { console.log('取消') }) }, } } </script>
菜单管理模块 菜单列表 需求分析 菜单列表无分页功能,因为有父子菜单关系,所以我们通过表格树形显示的数据 表格树形参考:https://element.eleme.cn/#/zh-CN/component/table#shu-xing-shu-ju-yu-lan-jia-zai 图标参考: https://element.eleme.cn/#/zh-CN/component/icon
查询菜单列表API
URL
method
description
/system/menu/search
post
查询菜单列表API
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data|10" : [ { "id|+1" : 1 , "parentId" : 0 , "name" : "@cname" , "url" : "@domain" , "code" : "@word" , "type" : 1 , "icon" : "@word" , "sort" : "@integer(1, 9)" , "createDate" : "@date" , "updateDate" : "@date" , "children|2-5" : [ { "id|+1" : 20 , "parentId|1-10" : 1 , "name" : "@cname" , "url" : "@domain" , "code" : "@word" , "type|1" : [ 2 , 3 ] , "icon" : "@word" , "sort" : "@integer(1, 9)" , "createDate" : "@date" , "updateDate" : "@date" , } ] } ] }
调用API接口 创建src/api/menu.js
import request from '@/utils/request' export default { getList (query ) { return request ({ url : `/system/menu/search` , method : 'post' , data : query }) } }
编辑src/views/menu/index.vue
调用API接口
<script> import api from '@/api/menu' export default { data() { return { list: [], query: {} } }, created() { this.fetchData() }, methods: { fetchData() { api.getList(this.query).then(response => { this.list = response.data console.log(this.list) }) } } } </script>
菜单列表模版 编辑src\views\menu\index.vue
<template> <div class="app-container"> <el-table :data="list" row-key="id" border height-current-row style="width: 100%"> <el-table-column align="center" type="index" label="序号" width="60"/> <el-table-column align="center" prop="name" label="名称"/> <el-table-column align="center" prop="url" label="请求地址"/> <el-table-column align="center" prop="code" label="权限标识"/> <el-table-column align="center" prop="type" label="类型"> <template slot-scope="scope"> <span v-if="scope.row.type === 1">目录</span> <span v-if="scope.row.type === 2">菜单</span> <span v-if="scope.row.type === 3">按钮</span> </template> </el-table-column> <el-table-column align="center" prop="code" label="图标"> <template slot-scope="scope"> <i :class="scope.row.icon"/> </template> </el-table-column> <el-table-column align="center" prop="sort" label="排序"/> <el-table-column align="center" label="操作" width="260"> <template slot-scope="scope"> <el-button @click="handleAdd(scope.row.id)" size="mini" type="primary" icon="el-icon-circle-plus-outline">新增</el-button> <el-button @click="handleEdit(scope.row.id)" size="mini" type="primary">编辑</el-button> <el-button @click="handleDelete(scope.row.id)" size="mini" type="primary">删除</el-button> </template> </el-table-column> </el-table> </div> </template> <script> export default { data() { return { list: [] } }, methods: { handleAdd() { console.log('add') }, handleEdit() { console.log('edit') }, handleDelete() { console.log('delete') } } } </script>
条件查询 编辑src\views\menu\index.vue
,增加条件查询模板代码
<template> <div class="app-container"> <el-form :inline="true" size="small"> <el-form-item label="菜单名称"> <el-input v-model.trim="query.name"/> </el-form-item> <el-form-item> <el-button icon="el-icon-search" type="primary" @click="fetchData">查询</el-button> <el-button icon="el-icon-refresh" type="info" @click="handleReset">重置</el-button> <el-button icon="el-icon-circle-plus-outline" type="primary" @click="handleAdd(0)">新增</el-button> </el-form-item> </el-form> </div> </template>
重置功能
<script> export default { methods: { handleReset() { console.log('reset') this.query = {} this.fetchData() } } } </script>
菜单新增 需求分析 菜单管理中有两处有新增按钮,表单区域的是新增一级菜单,传递的参数是0。表格区域的是新增子菜单,传递的是参数是当前菜单id。 当点击新增按钮后,都是对话框形式弹出新增窗口
新增菜单组件 新建src/views/menu/edit.vue
<template> <el-dialog :title="title" :visible.sync="visible" :before-close="handleClose" width="500px"> <el-form :rules="rules" ref="formData" :model="formData" label-width="100px" style="width: 400px" status-icon> <el-form-item label="类型" prop="type"> <el-radio-group v-model="formData.type"> <el-radio :label="1">目录</el-radio> <el-radio :label="2">菜单</el-radio> <!-- 如果 formData.parentId=0 是根菜单,就不显示按钮,注意是数字0,不是字符串没有引号--> <el-radio :label="3" v-if="formData.parentId !== 0">按钮</el-radio> </el-radio-group> </el-form-item> <el-form-item label="名称" prop="name"> <el-input v-model="formData.name"/> </el-form-item> <el-form-item label="权限标识" prop="code"> <el-input v-model="formData.code"/> </el-form-item> <el-form-item label="请求地址" prop="url" v-if="formData.type !== 3"> <el-input v-model="formData.url"/> </el-form-item> <el-form-item label="图标" prop="icon" v-if="formData.type !== 3"> <el-input v-model="formData.icon"/> </el-form-item> <el-form-item label="排序" prop="sort" v-if="formData.type !== 3"> <el-input-number v-model="formData.sort" :min="1" :max="100"/> </el-form-item> <el-form-item label="备注" prop="remark"> <el-input v-model="formData.remark" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" placeholder="请输入内容"/> </el-form-item> <el-form-item align="right"> <el-button size="mini" @click="handleClose">取消</el-button> <el-button size="mini" @click="submitForm('formData')">确定</el-button> </el-form-item> </el-form> </el-dialog> </template> <script> export default { props: { visible: { type: Boolean, default: false }, title: { type: String, default: '' }, formData: { type: Object, // eslint-disable-next-line vue/require-valid-default-prop default: {} }, remoteClose: Function }, methods: { handleClose() { console.log('close') }, submitData(formName) { console.log('submit', formName) } } } </script>
引用新增菜单组件 编辑父组件menu/index.vue
引用menu/edit.vue
子组件实现弹窗
<template> <div class="app-container"> <edit :title="edit.title" :visible="edit.visible" :formData="edit.formData" :remoteClose="remoteClose"/> </div> </template> <script> import Edit from '@/views/menu/edit.vue' export default { components: { Edit }, data() { return { edit: { title: '', visible: false, formData: {} } } }, methods: { remoteClose() { console.log('父组件关闭窗口') } } } </script>
两个新增弹出窗口按钮,绑定点击事件函数都是handleAdd,但是传的参数不一样,条件区域是 0; 列表区域是菜单id,新增到此id下的作为子菜单
<script> export default { methods: { handleAdd(id) { console.log('add') this.edit.formData.parentId = id this.edit.title = '新增菜单' this.edit.visible = true } } } </script>
测试点击新增按钮,弹窗是否能够打开
关闭弹窗 当点击取消按钮或者右上角 X 将关闭窗口。在edit.vue子组件中触发index.vue父组件的remoteClose
方法来关闭窗口。编辑src/views/menu/edit.vue
<script> export default { methods: { handleClose() { console.log('close') this.$refs['formData'].resetFields() // 因为 visible 是父组件的属性,所以要让父组件去改变值 this.remoteClose() }, } } </script>
编辑src/views/menu/index.vue
<script> export default { methods: { remoteClose() { console.log('父组件关闭窗口') this.edit.formData = {} this.edit.visible = false this.fetchData() } } } </script>
校验新增菜单表单数据 编辑src/views/menu/edit.vue
新增窗口的el-form
上绑定属性:rules="rules"
<template> <el-dialog> <el-form rules="rules"></el-form> </el-dialog> </template> <script> export default { data() { return { rules: { type: [{ required: true, message: '请选择类型', trigger: 'change' }], name: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }], code: [{ required: true, message: '请输入权限标识', trigger: 'blur' }] } } } } </script>
提交表单数据 当点击新增窗口中的确认按钮时, 提交表单数据,后台API服务接口响应新增成功或失败。
添加菜单API接口
URL
method
description
/system/menu
post
添加菜单API接口
MockJS
{ "code" : 20000 , "message" : "新增成功" }
调用API接口 编辑src\api\menu.js
添加调用新增接口的方法
add (data ) { return request ({ url : `/system/menu` , method : 'post' , data }) }
src\views\menu\edit.vue
中的submitForm
方法中判断校验是否通过,通过了则调用submitData
异步方法提交数据
<script> export default { methods: { async submitData() { const response = await api.add(this.formData) if (response.code === 20000) { this.$message({ message: '保存成功', type: 'success' }) this.handleClose() } else { this.$message({ message: '保存失败', type: 'error' }) } }, submitForm(formName) { this.$refs[formName].validate((valid) => { if (valid) { // 校验通过,判断下选择的类型,把对应不需要的把它清空 if (this.formData.type === 3) { this.formData.url = '' this.formData.icon = '' } // 提交数据 this.submitData() } else { return false } }) } } } </script>
菜单修改 需求分析 当点击编辑按钮后,弹出编辑窗口,并查询出菜单相关信息进行渲染。修改后点击确定提交修改后的数据
菜单API接口
查询菜单接口
URL
method
description
/system/menu/{id}
get
菜单查询API接口
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data" : { "id" : 20 , "parentId" : 1 , "name" : "@cname" , "url" : "@domain" , "code" : "@word" , "icon" : "@word" , "remark" : "@ctitle" , "type|1" : [ 1 , 2 , 3 ] , "sort" : "@integer(1, 9)" , "createDate" : "@date" , "updateDate" : "@date" , } }
修改菜单接口
URL
method
description
/system/menu
put
修改菜单接口
MockJS
{ "code" : 20000 , 'message': "修改成功" }
调用API接口 编辑src\api\menu.js
添加通过ID查询方法getById
和更新方法update
getById (id ) { return request ({ url : `/system/menu/{id}` , method : 'get' }) }, update (data ) { return request ({ url : `/system/menu` , method : 'put' , data }) }
编辑src\views\menu\index.vue
中的handleEdit
方法
<script> export default { methods: { handleEdit(id) { console.log('edit') api.getById(id).then(response => { if (response.code === 20000) { this.edit.formData = response.data this.edit.title = '编辑菜单' this.edit.visible = true } }) } } } </script>
提交修改菜单数据 编辑src/views/menu/edit.vue
组件中的submitData
方法加上一个判断this.formData.id
存在则为更新,否则是新增操作
<script> export default { methods: { async submitData() { let response = null if (this.formData.id) { response = await api.update(this.formData) } else { response = await api.add(this.formData) } if (response.code === 20000) { this.$message({ message: '保存成功', type: 'success' }) this.handleClose() } else { this.$message({ message: '保存失败', type: 'error' }) } } } } </script>
菜单删除 需求分析 当点击删除按钮后, 弹出提示框,点击确定后,执行删除并刷新列表数据确认消息弹框参考:https://element.eleme.cn/#/zh-CN/component/message-box#que-ren-xiao-xi
删除菜单API接口
URL
method
description
/system/menu/{id}
delete
删除菜单API接口
MockJS
{ "code" : 20000 , "message" : "删除成功" }
调用API接口 编辑src\api\menu.js
添加deleteById
方法
delete (id ) { return request ({ url : `/system/menu/{id}` , method : 'delete' }) }
编辑src\views\menu\index.vue
中的handleDelete
方法
<script> export default { methods: { handleDelete(id) { console.log('delete') this.$confirm('确认删除数据吗?', '提示', { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' }).then(() => { api.delete(id).then(response => { this.$message({ type: response.code === 20000 ? 'success' : 'error', message: response.message }) this.fetchData() }) }).catch(() => { console.log('取消') }) } } } </script>
角色管理模块 角色列表 列表API接口
URL
method
description
/system/role/search
post
角色条件分页列表数据接口
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data" : { "total" : "@integer(100, 200)" , "records|20" : [ { "id|+1" : 1 , "name" : "@cname" , "remark" : "@ctitle" , "createDate" : "@date" , "updateDate" : "@date" , } ] } }
调用API接口 创建src/api/role.js
import request from '@/utils/request' export default { getList (query, current = 1 , size = 20 ) { return request ({ url : `/system/role/search` , method : 'post' , data : { ...query, current, size } }) } }
创建src\views\role\index.vue
, 添加JS代码
<script> import api from '@/api/role' export default { data() { return { list: [], page: { total: 0, current: 1, size: 20 }, query: {} } }, created() { this.fetchData() }, methods: { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response => { this.list = response.data.records this.page.total = response.data.total console.log(this.list) }) } } } </script>
列表模版 编辑src/views/role/index.vue
<template> <div class="app-container"> <el-table :data="list" border highlight-current-row style="width: 100%"> <el-table-column align="center" type="index" label="序号" width="60"/> <el-table-column align="center" prop="name" label="角色名称"/> <el-table-column align="center" prop="remark" label="备注"/> <el-table-column align="center" label="操作"> <template slot-scope="scope"> <el-button @click="handlePermission(scope.row.id)" size="mini" type="success">分配权限</el-button> <el-button @click="handleEdit(scope.row.id)" size="mini">编辑</el-button> <el-button @click="handleDelete(scope.row.id)" size="mini" type="danger">删除</el-button> </template> </el-table-column> </el-table> </div> </template> <script> import api from '@/api/role' export default { data() { return { list: [], } }, created() { this.fetchData() }, methods: { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response => { this.list = response.data.records this.page.total = response.data.total console.log(this.list) }) }, handleEdit(id) { console.log('edit', id) }, handleDelete(id) { console.log('delete', id) } } } </script>
分页查询 编辑src\views\role\index.vue
,在template
标签中添加分页组件
<template> <div class="app-container"> <el-pagination background layout="prev, pager, next" :total="this.page.total" @current-change="handleCurrentChange" :current-page="page.current"></el-pagination> </div> </template> <script> import api from '@/api/role' export default { data() { return { list: [], page: { total: 0, current: 1, size: 20 } } }, created() { this.fetchData() }, methods: { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response => { this.list = response.data.records this.page.total = response.data.total console.log(this.list) }) }, handleCurrentChange(current) { console.log('change current', current) this.page.current = current this.fetchData() } } } </script>
条件查询 编辑src\views\role\index.vue
,增加条件查询模板代码
<template> <div class="app-container"> <el-form :inline="true" size="small" ref="formSearch"> <el-form-item label="角色名称"> <el-input v-model.trim="query.title"></el-input> </el-form-item> <el-form-item> <el-button icon='el-icon-search' type="primary" @click="queryData">查询</el-button> <el-button icon='el-icon-refresh' type="info" @click="reset('formSearch')">重置</el-button> <el-button icon='el-icon-add-location' type="primary" @click="add">新增</el-button> </el-form-item> </el-form> </div> </template> <script> export default { methods: { queryData() { console.log('query') this.page.current = 1 this.fetchData() }, reset() { console.log('reset') this.query = {} this.fetchData() }, add() { console.log('add') } } } </script>
角色新增 需求分析 点击新增按钮后,都是对话框形式弹出新增窗口; 输入信息后,点击确定提交表单数据;
新增角色组件 创建src/views/role/edit.vue
新增组件
<template> <el-dialog :title="title" :visible.sync="visible" :before-close="handleClose" width="500px"> <el-form ref="formData" :model="formData" label-width="100px" style="width: 400px" status-icon> <el-form-item label="名称" prop="name"> <el-input v-model="formData.name"/> </el-form-item> <el-form-item label="备注" prop="remark"> <el-input v-model="formData.remark" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" placeholder="请输入内容"/> </el-form-item> <el-form-item align="right"> <el-button size="mini" @click="handleClose">取消</el-button> <el-button size="mini" type="primary" @click="submitForm('formData')">提交</el-button> </el-form-item> </el-form> </el-dialog> </template> <script> export default { props: { title: { type: String, default: '' }, visible: { type: Boolean, default: false }, formData: { type: Object, default: {} }, remoteClose: Function }, methods: { handleClose() { console.log('close role edit') }, submitForm(formName) { console.log('submit form', formName) } } } </script>
列表引用新增组件 编辑src/views/role/index.vue
<template> <div class="app-container"> <edit :title="edit.title" :visible="edit.visible" :formData="edit.formData" :remoteClose="remoteClose"/> </div> </template> <script> import Edit from '@/views/role/edit.vue' export default { components: { Edit }, data() { return { edit: { title: '', visible: false, formData: {} } } }, methods: { remoteClose() { console.log('remote close') } } } </script>
测试:点击新增按钮是否可以打开弹窗
关闭弹窗窗口 编辑src/views/role/edit.vue
子组件中触发src/views/role/index.vue
父组件的remoteClose
方法来关闭窗口
<script> export default { props: { remoteClose: Function }, methods: { handleClose() { console.log('close role edit') this.$refs['formData'].resetFields() this.remoteClose() }, } } </script>
编辑src/views/role/index.vue
中将this.edit.visiable=false
关闭窗口,刷新数据。
<script> export default { methods: { remoteClose() { console.log('remote close') this.edit.formData = {} this.edit.visible = false this.fetchData() } } } </script>
校验表单数据 编辑src/views/role/edit.vue
新增窗口的el-form
上绑定属性:rules="rules"
<template> <el-dialog> <el-form :rule="rule"></el-form> </el-dialog> </template> <script> export default { data() { return { rules: { name: [{ required: true, message: '请输入名称', trigger: 'blur' }] } } } } </script>
提交表单数据
新增角色API接口
URL
method
description
/system/role
post
新增角色API接口
MockJS
{ "code" : 20000 , "message" : "新增成功" }
调用API接口
在src\api\role.js
添加调用新增接口的方法
add (data ) { return request ({ url : `/system/role` , method : 'post' , data }) }
编辑src\views\role\edit.vue
中的submitForm
方法中判断校验是否通过,通过了则调用submitData
异步方法提交数据
<script> import api from '@/api/role' export default { methods: { submitForm(formName) { console.log('submit form', formName) this.$refs[formName].validate((vaild) => { if (vaild) { console.log('valid') this.submitData() } else { return false } }) }, async submitData() { const response = await api.add(this.formData) console.log(response) if (response.code === 20000) { this.$message({ type: 'success', message: response.message }) this.handleClose() } else { this.$message({ type: 'error', message: response.message }) } } } } </script>
修改角色 需求分析 当点击编辑按钮后,弹出编辑窗口,并查询出菜单相关信息进行渲染。修改后点击确定提交修改后的数据。
角色API接口
根据ID获取角色数据API
URL
method
description
/system/role/{id}
get
根据ID获取角色数据
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data" : { "id" : 10 , "name" : "@cname" , "remark" : "@ctitle" , "createDate" : "@date" , "updateDate" : "@date" , } }
修改角色数据API
URL
method
description
/system/role
put
修改角色数据API
MockJS
{ "code" : 20000 , 'message': "修改成功" }
调用API接口
编辑src\api\role.js
添加通过ID查询方法getById
和更新方法update
getById (id ) { return request ({ url : `/system/role/{id}` , method : 'get' }) }, update (data ) { return request ({ url : `/system/role` , method : 'put' , data }) }
编辑角色 编辑src\views\role\index.vue
中的handleEdit
方法
<script> import api from '@/api/role' export default { methods: { handleEdit(id) { console.log('edit', id) api.getById(id).then(response => { if (response.code === 20000) { this.edit.formData = response.data this.edit.title = '编辑角色' this.edit.visible = true } }) }, } } </script>
提交修改数据 编辑src/views/role/edit.vue
组件中的submitData
方法加上一个判断this.formData.id
存在则为更新,否则是新增操作
<script> export default { methods: { async submitData() { let response = null if (this.formData.id) { response = await api.update(this.formData) } else { response = await api.add(this.formData) } if (response.code === 20000) { this.$message({ type: 'success', message: response.message }) this.handleClose() } else { this.$message({ type: 'error', message: response.message }) } } } } </script>
删除角色 需求分析 当点击删除按钮后, 弹出提示框,点击确定后,执行删除并刷新列表数据确认消息弹框参考:https://element.eleme.cn/#/zh-CN/component/message-box#que-ren-xiao-xi
删除角色API接口
URL
method
description
/system/role/{id}
delete
删除角色API接口
MockJS
{ "code" : 20000 , "message" : "删除成功" }
调用API接口 编辑src\api\role.js
添加deleteById
方法
deleteById (id ) { return request ({ url : `/system/role/{id}` , method : 'delete' }) }
编辑src\views\role\index.vue
中的handleDelete
方法
<script> export default { methods: { handleDelete(id) { console.log('delete', id) this.$confirm('确认删除这条角色数据吗?', '提示', { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' }).then(() => { api.deleteById(id).then(response => { this.$message({ type: response.code === 20000 ? 'success' : 'error', message: response.message }) this.fetchData() }) }).catch(() => { console.log('取消操作') }) } } } </script>
分配菜单角色权限 需求分析 查询所有菜单,以el-tree树组件展示参考:https://element.eleme.cn/#/zh-CN/component/tree 根据角色id查询此角色拥有的菜单权限,然后在菜单树进行勾选。
角色菜单分配组件 创建src/views/role/permission.vue
分配菜单权限组件
<template> <el-dialog :title="title" :visible.sync="visible" :before-close="handleClose" width="500px"> <el-form ref="formData" v-loading="loading" label-width="100px"> <!-- v-loading 值为 true 显示加载中--> <el-tree ref="tree" :data="menuList" :default-checked-keys="menuIds" :props="{ children: 'children', label: 'name' }" node-key="id" show-checkbox accordion highlight-current/> <!-- data 数据集合,default-checked-keys 默认勾选--> <!-- node-key 每个树节点用来作为唯一标识的属性--> <!-- show-checkbox 显示勾选框--> <!-- accordion 每次只打开一个同级树节点展开--> <!-- highlight-current 高亮当前选中节点--> <el-form-item align="center"> <el-button size="mini" @click="handleClose">取消</el-button> <el-button size="mini" type="primary" @click="submitForm('formData')">确认</el-button> </el-form-item> </el-form> </el-dialog> </template> <script> export default { name: 'Permission', props: { title: { type: String, default: '' }, visible: { type: Boolean, default: false }, remoteClose: Function }, data() { return { loading: false, menuList: [], menuIds: [] } }, methods: { handleClose() { console.log('close') }, submitForm(formName) { console.log('submit form', formName) } } } </script>
列表页面引用分配权限组件 编辑src/views/role/index.vue
引用role/permission.vue
子组件实现弹窗
<template> <div class="app-container"> <permission :title="permissions.title" :visible.sync="permissions.visible" :roleId="permissions.roleId" :remoteClose="remoteClose"/> </div> </template> <script> import Permission from '@/views/role/permission.vue' export default { components: { Permission }, data() { return { permissions: { title: '', visible: false, roleId: null } } }, created() { this.fetchData() }, methods: { handlePermission(id) { this.permissions.title = '分配角色权限' this.permissions.roleId = id this.permissions.visible = true }, remoteClose() { console.log('remote close') this.edit.formData = {} this.edit.visible = false this.fetchData() } } } </script>
测试:点击分配权限按钮打开分配权限弹窗
渲染分配角色权限弹窗数据 使用watch选项对visible属性监听,当visible属性改变后并且值为true(弹出窗口)时,去调用接口查询所有菜单,在api/menu.js
已经存在这个接口。编辑src/views/role/permission.vue
<script> import menuAPI from '@/api/menu' export default { watch: { visible: function(val) { if (val) { this.getMenuList() } } }, data() { return { loading: false, menuList: [], } }, methods: { getMenuList() { menuAPI.getList({}).then(response => { this.menuList = response.data this.loading = false }) } } } </script>
测试:点击角色管理》分配权限,数据是否渲染
根据角色设置菜单权限
需求分析
获取指定角色id所拥有的权限菜单ids,进行勾选
查询角色菜单API接口
URL
method
description
/system/role/{id}/menu/ids
get
查询角色菜单API接口
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data|2-5" : [ "@integer(20,30)" ] }
调用API接口
编辑src\api\role.js
添加调用接口的方法
getMenuIdByRoleId (id ) { return request ({ url : `/system/role/{id}/menu/ids` , method : 'get' }) }
编辑src\views\role\permission.vue
中的添加getMenuIdsByRoleId
方法调用接口
<script> import menuAPI from '@/api/menu' import roleAPI from '@/api/role' export default { name: 'Permission', props: { roleId: null, }, methods: { getMenuIdByRoleId(roleId) { roleAPI.getMenuIdByRoleId(roleId).then(response => { this.menuIds = response.data }) } } } </script>
测试:点击分配权限是否选勾
提交角色权限数据
保存角色权限数据API接口
URL
method
description
/system/role/{id}/menu/save
post
保存角色权限数据API接口
MockJS
{ "code" : 20000 , "message" : "新增成功" }
调用API接口
编辑src\api\role.js
添加调用新增角色权限接口的方法
saveRoleMenu (id,menuId ) { return request ({ url : `/system/role/{id}/menu/save` , method : 'post' , data : menuId }) }
编辑src\views\role\permission.vue
中的submitForm
方法中封装选中的菜单id和角色id,调用saveRoleMenu接口方法提交数据
<script> export default { methods: { submitForm(formName) { console.log('submit form', formName) // 获取所有选中节点的菜单 id const checkedMenuIds = this.$refs.tree.getCheckedKeys() // 调用保存角色菜单接口 roleAPI.saveRoleMenu(this.roleId, checkedMenuIds).then(response => { if (response.code === 20000) { // 提交成功, 关闭窗口, 刷新列表 this.$message({ message: '保存成功', type: 'success' }) // 关闭窗口 this.handleClose() } else { this.$message({ type: 'error', message: '保存失败' }) } }) } } } </script>
测试:打开分配角色权限窗口,点击提交查看是否成功提交
用户管理 用户列表 用户列表API接口
URL
method
description
/system/user/search
post
用户列表API接口
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data" : { "total" : "@integer(100, 200)" , "records|20" : [ { "id|+1" : 1 , "username" : "@name" , "password" : "xxxxxxx" , "isAccountNonExpired|1" : [ 0 , 1 ] , "isAccountNonLocked|1" : [ 0 , 1 ] , "isCredentialsNonExpired|1" : [ 0 , 1 ] , "isEnabled|1" : [ 0 , 1 ] , "nickName" : "@cname" , "imageUrl" : "http://mengxuegu.com/logo.png" , "mobile" : /1 \d{ 10 } /, "email" : "@email()" , "createDate" : "@date" , "updateDate" : "@date" , "pwdUpdateDate" : "@date" } ] } }
调用API接口 编辑src/api/user.js
添加调用接口代码。因为 user.js 是架手脚中之前就有的,里面代码是针对每个方法都单独导出的,而我们之前是直接导出一个默认对象。 所以就按 user.js 现有的方式,单独导出方法
export function getList (query, current = 1 , size = 20 ) { return request ({ url : `/system/user/search` , method : 'post' , data : { ...query, current, size } }) }
创建src\views\user\index.vue
, 添加JS代码
<script> import * as api from '@/api/user' export default { data() { return { query: {}, list: [], page: { total: 0, current: 1, size: 20 } } }, created() { this.fetchData() }, methods: { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response => { this.list = response.data.records this.page.total = response.data.total }) } } } </script>
用户管理组件模版 编辑src/views/user/index.vue
<template> <div class="app-container"> <!-- 条件查询 --> <el-form :inline="true" :model="query" size="mini"> <el-form-item label="用户名:"> <el-input v-model.trim="query.username" ></el-input> </el-form-item> <el-form-item label="手机号:"> <el-input v-model.trim="query.mobile" ></el-input> </el-form-item> <el-form-item> <el-button icon="el-icon-search" type="primary" @click="queryData">查询</el-button> <el-button icon="el-icon-refresh" @click="reset">重置</el-button> <el-button icon="el-icon-circle-plus-outline" type="primary" @click="openAdd" >新增</el-button> </el-form-item> </el-form> <el-table :data="list" stripe border style="width: 100%"> <el-table-column align="center" type="index" label="序号" width="60"></el-table-column> <el-table-column align="center" prop="username" label="用户名" ></el-table-column> <el-table-column align="center" prop="nickName" label="昵称" ></el-table-column> <el-table-column align="center" prop="mobile" label="手机号" ></el-table-column> <el-table-column align="center" prop="email" label="邮箱" ></el-table-column> <el-table-column align="center" prop="isAccountNonExpired" label="帐号过期" > <!-- (1 未过期,0已过期) --> <template slot-scope="scope"> <el-tag v-if="scope.row.isAccountNonExpired === 0" type="danger">过期</el-tag> <el-tag v-if="scope.row.isAccountNonExpired === 1" type="success">正常</el-tag> </template> </el-table-column> <el-table-column align="center" prop="isAccountNonLocked" label="帐号锁定" > <!-- (1 未锁定,0已锁定) --> <template slot-scope="scope"> <el-tag v-if="scope.row.isAccountNonLocked === 0" type="danger">锁定</el-tag> <el-tag v-if="scope.row.isAccountNonLocked === 1" type="success">正常</el-tag> </template> </el-table-column> <el-table-column align="center" prop="isCredentialsNonExpired" label="密码过期" > <!-- (1 未过期,0已过期) --> <template slot-scope="scope"> <el-tag v-if="scope.row.isCredentialsNonExpired === 0" type="danger">过期</el-tag> <el-tag v-if="scope.row.isCredentialsNonExpired === 1" type="success">正常</el-tag> </template> </el-table-column> <el-table-column align="center" prop="isEnabled" label="是否可用" > <!-- (1 可用,0 删除用户) --> <template slot-scope="scope"> <el-tag v-if="scope.row.isEnabled === 0" type="danger">已删除</el-tag> <el-tag v-if="scope.row.isEnabled === 1" type="success">可用</el-tag> </template> </el-table-column> <el-table-column align="center" label="操作" width="330"> <template slot-scope="scope" v-if="scope.row.isEnabled === 1"> <el-button type="success" @click="handleEdit(scope.row.id)" size="mini">编辑</el-button> <el-button type="danger" @click="handleDelete(scope.row.id)" size="mini">删除</el-button> <el-button type="primary" @click="handleRole(scope.row.id)" size="mini">设置角色</el-button> <el-button type="primary" @click="handlePwd(scope.row.id)" size="mini">密码修改</el-button> </template> </el-table-column> </el-table> <!-- 分页组件 --> <el-pagination background layout="prev, pager, next" :total="this.page.total" @current-change="handleCurrentChange" :current-page="page.current"></el-pagination> </div> </template> <script> import * as api from '@/api/user' export default { data() { return { query: {}, list: [], page: { total: 0, current: 1, size: 20 } } }, created() { this.fetchData() }, methods: { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response => { this.list = response.data.records this.page.total = response.data.total }) }, queryData() { console.log('query') }, reset() { console.log('reset') }, openAdd() { console.log('add') }, handleCurrentChange(current) { console.log('change', current) } } } </script>
用户新增 需求分析 点击新增按钮后,都是对话框形式弹出新增窗口;输入信息后,点击确定提交表单数据;
新增用户组件 创建新增和修改的组件文件: src\views\user\edit.vue
<template> <el-dialog :title="title" :visible.sync="visible" width="500px" :before-close="handleClose"> <el-form :rules="rules" ref="formData" :model="formData" label-width="100px" label-position="right" style="width: 400px" status-icon> <el-form-item label="用户名:" prop="username"> <el-input v-model="formData.username" maxlength="30"></el-input> </el-form-item> <el-form-item label="昵称:" prop="nickName"> <el-input v-model="formData.nickName" maxlength="50"></el-input> </el-form-item> <el-form-item label="手机号:" prop="mobile"> <el-input v-model="formData.mobile" maxlength="11"></el-input> </el-form-item> <el-form-item label="邮箱:" prop="email"> <el-input v-model="formData.email" maxlength="30"></el-input> </el-form-item> <el-form-item label="帐号过期:" prop="isAccountNonExpired"> <!-- (1 未过期,0已过期) --> <el-radio-group v-model="formData.isAccountNonExpired" > <el-radio :label="1" border>未过期</el-radio> <el-radio :label="0" border>已过期</el-radio> </el-radio-group> </el-form-item> <el-form-item label="密码过期:" prop="isCredentialsNonExpired"> <!-- (1 未过期,0已过期) --> <el-radio-group v-model="formData.isCredentialsNonExpired" > <el-radio :label="1" border>未过期</el-radio> <el-radio :label="0" border>已过期</el-radio> </el-radio-group> </el-form-item> <el-form-item label="帐号锁定:" prop="isAccountNonLocked"> <!-- (1 未锁定,0已锁定) --> <el-radio-group v-model="formData.isAccountNonLocked" > <el-radio :label="1" border>未锁定</el-radio> <el-radio :label="0" border>已锁定</el-radio> </el-radio-group> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm('formData')" size="mini">确定</el-button> <el-button size="mini" @click="handleClose">取消</el-button> </el-form-item> </el-form> </el-dialog> </template> <script> import * as api from '@/api/user' export default { props: { title: { // 弹窗的标题 type: String, default: '' }, visible: { // 弹出窗口,true弹出 type: Boolean, default: false }, formData: { // 提交表单数据 type: Object, // eslint-disable-next-line vue/require-valid-default-prop default: {} }, remoteClose: Function // 用于关闭窗口 }, data() { return { // 校验表单 rules: { username: [ { required: true, message: '请输入用户名', trigger: 'blur' }, { min: 6, max: 30, message: '长度在 6 到 30 个字符', trigger: 'blur' } ], nickName: [ { required: true, message: '请输入昵称', trigger: 'blur' } ], mobile: [ { required: true, message: '请输入手机号', trigger: 'blur' } ], isAccountNonExpired: [ { required: true, message: '请选择', trigger: 'change' } ], isCredentialsNonExpired: [ { required: true, message: '请选择', trigger: 'change' } ], isAccountNonLocked: [ { required: true, message: '请选择', trigger: 'change' } ] } } }, methods: { // 提交 submitForm(formName) { this.$refs[formName].validate((valid) => { if (valid) { // 校验通过,提交表单数据 this.submitData() } else { // console.log('error submit!!'); return false } }) }, async submitData() { let response = null if (this.formData.id) { // 编辑 response = await api.update(this.formData) } else { // 新增 // 初始密码与用户名一致 this.formData.password = this.formData.username response = await api.add(this.formData) } // eslint-disable-next-line no-cond-assign,no-constant-condition if (response.code = 20000) { this.$message({ message: '保存成功', type: 'success' }) this.handleClose() } else { this.$message({ message: '保存失败', type: 'error' }) } }, // 关闭窗口 handleClose() { this.$refs['formData'].resetFields() this.remoteClose() } } } </script>
列表页面引用新增组件 <template> <div class="app-container"> <edit :title="edit.title" :visible="edit.visible" :formData="edit.formData" :remoteClose="remoteClose"> </edit> </div> </template> <script> import Edit from '@/views/user/edit.vue' export default { components: { Edit }, data() { return { edit: { visible: false, formData: {} } } }, methods: { remoteClose() { this.edit.formData = {} this.edit.visible = false this.fetchData() } } } </script>
编辑src/views/user/index.vue
实现点击新增,打开新增窗口
<script> export default { methods: { openAdd() { console.log('add') this.edit.title = '新增用户' this.edit.visible = true }, } } </script>
用户修改 需求分析 当点击编辑按钮后,弹出编辑窗口,并查询出用户相关信息进行渲染。修改后点击确定提交修改后的数据。
查询用户API接口
URL
method
description
/system/user/{id}
get
查询用户API接口
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data" : { "id" : 1 , "username" : "@name" , "password" : "xxxxxxx" , "isAccountNonExpired|1" : [ 0 , 1 ] , "isAccountNonLocked|1" : [ 0 , 1 ] , "isCredentialsNonExpired|1" : [ 0 , 1 ] , "isEnabled" : 1 , "nickName" : "@cname" , "imageUrl" : "http://mengxuegu.com/logo.png" , "mobile" : /1 \d{ 10 } /, "email" : "@email()" , "createDate" : "@date" , "updateDate" : "@date" , "pwdUpdateDate" : "@date" } }
修改用户数据API接口
URL
method
description
/system/user
put
MockJS
{ "code" : 20000 , 'message': "修改成功" }
调用API接口 编辑src\api\user.js
添加通过ID查询方法getById
和更新方法update
export function getById (id ) { return request ({ url : `/system/user/${id} ` , method : 'get' }) } export function update (data ) { return request ({ url : `/system/user` , method : 'put' }) }
编辑src\views\user\index.vue
中的handleEdit
方法
<script> export default { methods: { handleEdit(id) { api.getById(id).then(response => { if (response.code === 20000) { this.edit.formData = response.data } this.edit.title = '编辑用户' this.edit.visible = true }) } } } </script>
用户删除 需求分析 当点击删除按钮后, 弹出提示框,点击确定后,执行删除并刷新列表数据确认消息弹框参考:https://element.eleme.cn/#/zh-CN/component/message-box#que-ren-xiao-xi
删除用户API接口
URL
method
description
/system/user/{id}
delete
删除用户API接口
MockJS
{ "code" : 20000 , "message" : "删除成功" }
调用API接口 编辑src\api\user.js
添加deleteById
方法
export function deleteById (id ) { return request ({ url : `/system/user/${id} ` , method : 'delete' }) }
编辑src\views\user\index.vue
中的handleDelete
方法做如下修改
<script> export default { methods: { handleDelete(id) { this.$confirm('确认删除这条记录吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 确认 api.deleteById(id).then(response => { // 提示信息 this.$message({ type: response.code === 20000 ? 'success' : 'error', message: response.message }) // 刷新列表 this.fetchData() }) }).catch(() => { // 取消删除,不理会 console.log('cancel') }) } } } </script>
设置用户角色 需求分析
点击设置角色按钮,复用角色列表组件src/views/role/edit.vue
以弹出窗显示。
针对角色列表组件我们要进行一次重构,添加勾选框和设置角色按钮进行提交用户角色数据。
根据用户id查询此用户拥有的角色,然后在角色列表中进行勾选。
用户列表引用角色列表组件 编辑父组件user/index.vue
引用role/index.vue
子组件实现弹窗
<template> <div class="app-container"> <el-dialog title="设置角色" :visible.sync="role.visible" width="65%"> <Role :roleIds="role.roleIds" @saveUserRole="saveUserRole"></Role> </el-dialog> </div> </template> <script> import Role from '@/views/role/index.vue' export default { components: { Role }, data() { return { role: { userId: null, roleIds: [], visible: false } } }, created() { this.fetchData() }, methods: { saveUserRole() { console.log('save user role') }, handleRole() { console.log('handleRole') this.role.visible = true } } } </script>
测试:点击设置角色,弹窗是否能够打开
查询用户角色
需求分析
获取指定用户id所拥有的角色ids,用于勾选用户现有的角色
查询用户角色API接口
URL
method
description
/system/user/{id}/role/ids
get
MockJS
{ "code" : 20000 , "message" : "查询成功" , "data|2-5" : [ "@integer(1,20)" ] }
调用API接口
编辑src\api\user.js
添加调用接口的方法
export function getRoleIdsByUserId (id ) { return request ({ url : `/system/user/${id} /role/ids` , method : 'get' }) }
编辑src\views\user\index.vue
中的handleRole
方法,调用user.js
声明的getRoleIdsByUserId
<script> export default { methods: { handleRole(id) { console.log('handleRole') this.role.userId = id api.getRoleIdsByUserId(id).then(response => { this.role.roleIds = response.data }) this.role.visible = true } } } </script>
重构角色列表组件 针对角色列表组件我们要进行一次重构,隐藏按钮。在role/index.vue
声明props
接收user/index.vue
父组件传递的roleIds
数据
<script> export default { props: { roleIds: null // 接收用户拥有角色ids,如果是用户管理弹出的,即使没有角色也是[],而不是会传null } } </script>
如果直接是访问 角色管理展示角色列表,则roleIds
为null
。 如果是用户管理弹出的角色列表,即使用户没有角色roleIds
也是[]
, 而不是会传null
。 当是弹出的角色列表,我们要把新增和操作列的按钮全部隐藏,就通过v-if="roleIds"
判断roleIds
是否为null
,为null
则显示,否则隐藏。对role/index.vue
作如下修改:
<template> <el-button v-if="!roleIds" type="primary" icon='el-icon-add-location' @click="add">新增</el-button> <el-table-column v-if="!roleIds" align="center" label="操作"> </template>
角色列表添加勾选框 el-table
加上row-key
和ref
属性在角色列表第1列加上勾选框, 即type="selection"
,并且使用v-if="roleIds"
当弹出时才显示。编辑src/views/role/index.vue
<template> <el-table row-key="id" ref="dataTable" :data="list" border highlight-current-row style="width: 100%"> <el-table-column v-if="roleIds" type="selection" reserve-selection align="center" width="60" > </template>
回显用户拥有的角色 使用watch选项对roleIds 属性的监听,当roleIds属性改变后 ,也就是重新打开窗口后就重新查询数据。编辑src/views/role/index.vue
<script> export default { watch: { roleIds() { this.query = {} this.queryData() } } } </script>
编辑role/index.vue
定义checkedRoles()
方法去勾选当前用户有的角色。对应在fetchData()
中获取数据成功后,调用this.checkedRoles()
去勾选角色。this.$refs.dataTable.clearSelection()
清空上一次的选择,不然重新打开窗口,之前的勾选的还是会保留;this.$refs.dataTable.toggleRowSelection(item, true)
勾选用户角色
<script> export default { methods: { fetchData() { api.getList(this.query, this.page.current, this.page.size).then(response => { this.list = response.data.records this.page.total = response.data.total // 列表有数据后,勾选用户角色 +++++ this.checkedRoles() }) }, checkedRoles() { // 清空上一次的选择 this.$refs.dataTable.clearSelection() if (this.roleIds) { // 取出列表中每个角色,与用户角色ids进行匹配,匹配上了则选中 this.list.forEach(item => { // 匹配上了 if (this.roleIds.indexOf(item.id) !== -1) { // 选中,注意在 el-table 加上 ref="dataTable" this.$refs.dataTable.toggleRowSelection(item, true) } }) } }, } } </script>
收集用户角色 收集选中的角色,在el-table
上添加事件@selection-change="handleSelectionChange"
每次勾选框变化时,会触发该事件,收集选中数据。被勾选的如果你点击下一页会被取消,只会收集你当前页勾选的编辑src/views/role/index.vue
<template> <div class="app-container"> <el-table row-key="id" ref="dataTable" @selection-change="handleSelectionChange"></el-table> </div> </template> <script> export default { data() { return { checkedRoleList: [] } }, methods: { handleSelectionChange(val) { this.checkedRoleList = val } } } </script>
编辑src/views/role/index.vue
的条件查询区域,添加 设置角色 按钮,并加上v-if="roleIds"
弹窗才显示,@click="handleUserRole"
提交角色
<template> <el-button v-if="!roleIds" type="success" icon='el-icon-add-location' @click="handleUserRole">设置角色</el-button> </template> <script> export default { methods: { handleUserRole() { const checkedRoleIds = [] // 遍历获取已选中的角色id this.checkedRoleList.forEach(item => { checkedRoleIds.push(item.id) }) // 触发父组件 user/index.vue 的 saveUserRole 事件保存用户角色数据 this.$emit('saveUserRole', checkedRoleIds) } } } </script>
保存用户角色数据
保存用户角色数据API接口
URL
method
description
/system/user/{id}/role/save
post
保存用户角色数据API接口
MockJS
{ "code" : 20000 , "message" : "新增成功" }
调用API接口
编辑src\api\user.js
添加调用新增用户角色接口的方法
export function saveUserRole (id, roleIds ) { return request ({ url : `/system/user/${id} /role/save` , method : 'post' , data : roleIds }) }
编辑src\views\user\index.vue
中的saveUserRole
方法中(在role/index.vue
中已触发了此事件方法)调用src\api\user.js
里的saveUserRole
接口方法提交数据
<script> export default { methods: { saveUserRole(roleIds) { console.log('save user role', roleIds) // 1. 保存用户角色信息 // eslint-disable-next-line no-undef api.saveUserRole(this.role.userId, roleIds).then(response => { if (response.code === 20000) { // 提交成功, 关闭窗口, 刷新列表 this.$message({ message: '保存成功', type: 'success' }) // 关闭窗口 this.role.visible = false } else { this.$message({ type: 'error', message: '保存失败' }) } }) } } } </script>
修改用户密码 需求分析 当点击修改密码按钮后,弹出修改密码窗口; 其中要发送请求校验原密码输入是否正确,并且如果是通过用户列表点击的修改密码按钮弹出来的,原密码输入框要被隐藏。如果是自己修改自己的密码,比如右上角点击修改密码,则就要显示出原密码框要求输入去校验;修改后点击确定提交修改后的密码数据。
修改密码API接口
URL
method
description
/system/user/password
put
修改密码API接口
MockJS
{ "code" : 20000 , "message" : "修改成功" }
调用API接口 编辑src\api\user.js
添加提交修改新密码接口的方法
export function updatePassword (data ) { return request ({ url : `/system/user/password` , method : 'put' , data }) }
修改密码组件 创建组件文件:src\views\user\password.vue
<template> <el-dialog :title="title" :visible.sync="visible" :before-close="handleClose" width="380px"> <el-form :rules="rules" ref="formData" :model="formData" label-width="100px" style="width: 300px"> <el-form-item label="新密码" prop="newPassword"> <el-input type="password" placeholder="请输入密码" v-model="formData.newPassword"/> </el-form-item> <el-form-item label="确认密码" prop="repPassword"> <el-input type="password" placeholder="请再次确认密码" v-model="formData.repPassword"/> </el-form-item> <el-form-item align="right"> <el-button size="mini" @click="handleClose">取 消</el-button> <el-button size="mini" type="primary" @click="submitForm('formData')">确 定</el-button> </el-form-item> </el-form> </el-dialog> </template> <script> export default { props: { userId: null, visible: { type: Boolean, default: false }, title: { type: String, default: '' }, remoteClose: Function }, data() { return { formData: {} } }, methods: { handleClose() { console.log('close') }, submitForm(formName) { console.log('submit form', formName) } } } </script>
列表引用密码组件 在父组件user/index.vue
引用user/password.vue
子组件实现弹窗导入password.vue
组件,并使用components
选项引用为子组件
<template> <div class="app-container"> <!-- 修改密码组件--> <Password title="修改密码" :userId="pwd.userId" :visible="pwd.visible" :remoteClose="remotePwdClose"/> </div> </template> <script> import Password from '@/views/user/password.vue' export default { components: { Password }, data() { return { pwd: { userId: null, visible: false } } }, methods: { remotePwdClose() { this.pwd.userId = null this.pwd.visible = false this.fetchData() } } } </script>
密码修改弹出窗口按钮绑定点击事件函数handlePwd , 通过用户id查询用户信息。编辑src/views/user/index.vue
<script> export default { methods: { handlePwd(id) { this.pwd.userId = id this.pwd.visible = true } } } </script>
关闭弹窗 当点击取消按钮或者右上角X将关闭窗口。 在password.vue
子组件中触发index.vue父组件的remoteClose事件来关闭窗口。password.vue
中触发父组件的remoteClose
事件,并重置清空表单数据。编辑src/views/user/password.vue
<script> export default { methods: { handleClose() { console.log('close') // 表单清空 this.$refs['formData'].resetFields() // 因为 visible 是父组件的属性,所以要让父组件去改变值 this.remoteClose() }, } } </script>
表单数据校验 编辑src/views/user/password.vue
新增窗口的el-form
上绑定属性:rules="rules"
<template> <el-form :rules="rules"></el-form> </template> <script> export default { data() { // 校验确认密码是否一致 const validateRepPassword = (rule, value, callback) => { if (value === this.formData.newPassword) { // 相等,则通过 callback() } else { callback(new Error('两次输入的密码不一致')) } } return { // 校验 rules: { newPassword: [{ required: true, message: '新密码不能为空', trigger: 'blur' }], repPassword: [ { required: true, message: '确认密码不能为空', trigger: 'blur' }, { validator: validateRepPassword, trigger: 'blur' } ] } } }, } </script>
提交修改密码表单数据 当点击窗口中的确认按钮时, 调用更新用户密码的接口,即:src\api\user.js
中updatePassword
提交表单数据。编辑src\views\user\password.vue
中的submitForm
方法中判断校验是否通过,通过了则调用api.updatePassword(this.formData)
提交数据
<script> import * as api from '@/api/user' export default { methods: { submitForm(formName) { console.log('submit form', formName) this.$refs[formName].validate((valid) => { if (valid) { // 发送请求更新 this.formData.userId = this.userId // 不要忘记赋值了 api.updatePassword(this.formData).then(response => { if (response.code === 20000) { // 提交成功 this.$message({ message: '修改成功', type: 'success' }) // 关闭窗口 this.handleClose() } else { this.$message({ type: 'error', message: response.message }) } }) } else { // 验证不通过 return false } }) } } } </script>
首页统计展示 EChars 图表 管理后台图表也是常见得需求。这里图表就只推荐 ECharts,功能齐全,社区 demo 也丰富。 官网 https://echarts.apache.org/ 示例:https://echarts.apache.org/examples/zh/index.html 教程:https://echarts.apache.org/zh/tutorial.html API:https://echarts.apache.org/zh/api.html#echarts 配置项:https://echarts.apache.org/zh/option.html#title 主题:https://echarts.apache.org/zh/download-theme.html
安装Echars包 安装 ECharts
文章统计-饼状图 文章统计-饼状图,参考:https://echarts.apache.org/examples/zh/editor.html?c=pie-simple 指定主题 macarons ,参考:https://echarts.apache.org/zh/download-theme.html 创建src/views/dashboard/components/PieChart.vue
封装饼状图组件
<template> <div ref="main" :class="className" :style="{height:height,width:width}" /> </template> <script> import * as echarts from 'echarts' // 导入图表主题 require('echarts/theme/macarons') export default { props: { className: { type: String, default: 'chart' }, width: { type: String, default: '100%' }, height: { type: String, default: '400px' }, legendData: { type: Array, default: () => ['前端', 'Java', '移动端', '大数据', '人工智能', '区块链'] }, seriesData: { // 柱状数据 type: Array, default: () => [ { value: 335, name: '前端' }, { value: 310, name: 'Java' }, { value: 234, name: '移动端' }, { value: 135, name: '大数据' }, { value: 1548, name: '人工智能' }, { value: 1548, name: '区块链' } ] } }, data() { return { chart: null } }, mounted() { this.$nextTick(() => { this.initChart() }) }, methods: { initChart() { // 初始化echart实例,指定主题 this.chart = echarts.init(this.$refs.main, 'macarons') this.chart.setOption({ title: { text: '文章统计', left: 'center' // 居中 }, tooltip: { // 鼠标放上去显示的格式 trigger: 'item', formatter: '{a} <br/>{b} : {c} ({d}%)' }, legend: { // 左上角数据 orient: 'vertical', // 垂直 left: 'left', // 左侧 data: this.legendData }, series: [ // 图数据 { name: '统计内容', // 鼠标放上去显示的文字 type: 'pie', // 饼图 radius: '55%', // 圆大小 center: ['50%', '50%'], // 饼图位置,左上 data: this.seriesData, // 饼图展示数据 emphasis: { itemStyle: { // 饼图样式 shadowBlur: 10, // 图形阴影的模糊大小 shadowOffsetX: 0, // 阴影水平方向上的偏移距离 shadowColor: 'rgba(0, 0, 0, 0.5)' // 阴影颜色 } } } ] }) } } } </script>
编辑views/dashboard/index.vue
引入PieChart.vue
饼状图子组件
<template> <div class="dashboard-container"> <el-row :gutter="40"> <el-col :xs="24" :sm="24" :lg="12"> <el-card> <!-- 饼状图--> <pie-chart/> </el-card> </el-col> </el-row> </div> </template> <script> import PieChart from './components/PieChart.vue' export default { components: { PieChart } } </script> <style lang="scss" scoped> .dashboard { &-container { margin: 30px; } &-text { font-size: 30px; line-height: 46px; } } </style>
测试:访问dashboard查看效果
近6个月文章统计-柱状图 近6个月发布的文章数-柱状图,参考:https://echarts.apache.org/examples/zh/editor.html?c=bar-tick-align 创建src/views/dashboard/components/BarChart.vue
封装柱状图组件
<template> <div ref="main" :class="className" :style="{ height: height, width: width }"/> </template> <script> import * as echarts from 'echarts' require('echarts/theme/macarons') export default { props: { className: { type: String, default: 'chart' }, width: { type: String, default: '100%' }, height: { type: String, default: '400px' }, xAxisData: { // x轴显示的年月 type: Array, default: () => ['2021-01', '2021-02', '2021-03', '2021-04', '2021-05', '2021-06'] }, seriesData: { // 柱状数据 type: Array, default: () => [10, 52, 200, 334, 390, 330] } }, data() { return { chart: null } }, mounted() { this.$nextTick(() => { this.initChart() }) }, beforeDestroy() { if (!this.chart) { return } this.chart.dispose() this.chart = null }, methods: { initChart() { console.log('init chart') this.chart = echarts.init(this.$refs.main, 'macarons') this.chart.setOption({ title: { // 标题 text: '近6个月发布的文章数', // 主标题 left: 'center' // 居中 }, tooltip: { // 提示框组件 trigger: 'axis', // 鼠标放柱子上事件 axisPointer: { // 坐标轴指示器,坐标轴触发有效, type: 'shadow' // 默认为直线(line),shadow(灰色背景) 可选为:'line' | 'shadow' } }, grid: { // 柱状图整体位置 left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: [ // x 轴 { type: 'category', data: this.xAxisData, axisTick: { alignWithLabel: true } } ], yAxis: [ // y 轴 { type: 'value' } ], series: [ // 显示的数据 { name: '发布数', // 悬浮提示内容 type: 'bar', // 柱状类型 barWidth: '60%', // 每个柱状宽度 data: this.seriesData } ] }) } } } </script>
在views/dashboard/index.vue
引入BarChart.vue
柱状图子组件
<template> <div class="dashboard-container"> <el-row :gutter="40"> <el-col :xs="24" :sm="24" :lg="12"> <el-card> <!-- 饼状图--> <bar-chart/> </el-card> </el-col> </el-row> </div> </template> <script> import BarChart from '@/views/dashboard/components/BarChart.vue' export default { components: { BarChart } } </script>
图标自适应效果 当缩小窗口时,饼图和柱状图不会自动自适应,会被遮挡住。因为ECharts
本身并不是自适应的,当你父级容器的宽度发生变化的时候需要手动调用它的.resize()
方法。 其中vue-element-admin-master
项目中已经实现了自适应效果,只要将对应代码拷贝引用即可。
将vue-element-admin-master/src/views/dashboard/admin/components/mixins
目录文件拷贝到src/views/dashboard/components/mixins
将vue-element-admin-master/src/utils/index.js
文件里的debounce
函数,拷贝到src/utils/index.js
文件中
在src/views/dashboard/components/PanelGroup.vue
和PieChart.vue
组件中添加如下代码
<script> import resize from '@/views/dashboard/components/mixins/resize' export default { mixins: [resize] } </script>
总用户、总文章、总问答统计展示 可以参考vue-element-admin-master
项目中的PanelGroup.vue
组件,将PanelGroup.vue
组件文件拷贝到src/views/dashboard/components/PanelGroup.vue
,然后进行改造。 注意:PanelGroup.vue
组件中使用了第三方组件vue-count-to
,所以需要安装此模块代码。 vue-count-to 使用参考:https://github.com/PanJiaChen/vue-countTo svg 图标可从阿里图标库下载:https://www.iconfont.cn/ 安装 vue-count-to
修改 PanelGroup.vue 后的代码如下
<template> <el-row :gutter="40" class="panel-group"> <el-col :xs="12" :sm="12" :lg="8" class="card-panel-col"> <div class="card-panel"> <div class="card-panel-icon-wrapper icon-people"> <svg-icon icon-class="peoples" class-name="card-panel-icon" /> </div> <div class="card-panel-description"> <div class="card-panel-text"> 总用户 </div> <count-to :start-val="0" :end-val="102400" :duration="2600" class="card-panel-num" /> </div> </div> </el-col> <el-col :xs="12" :sm="12" :lg="8" class="card-panel-col"> <div class="card-panel" @click="handleSetLineChartData('messages')"> <div class="card-panel-icon-wrapper icon-message"> <svg-icon icon-class="message" class-name="card-panel-icon" /> </div> <div class="card-panel-description"> <div class="card-panel-text"> 总文章 </div> <count-to :start-val="0" :end-val="articleTotal" :duration="3000" class="card-panel-num" /> </div> </div> </el-col> <el-col :xs="12" :sm="12" :lg="8" class="card-panel-col"> <div class="card-panel"> <div class="card-panel-icon-wrapper icon-money"> <svg-icon icon-class="money" class-name="card-panel-icon" /> </div> <div class="card-panel-description"> <div class="card-panel-text"> 总提问 </div> <count-to :start-val="0" :end-val="questionTotal" :duration="3200" class="card-panel-num" /> </div> </div> </el-col> </el-row> </template> <script> import CountTo from 'vue-count-to' export default { components: { CountTo }, props: { userTotal: { type: Number, default: 0 }, articleTotal: { type: Number, default: 0 }, questionTotal: { type: Number, default: 0 } }, methods: { handleSetLineChartData(type) { this.$emit('handleSetLineChartData', type) } } } </script> <style lang="scss" scoped> .panel-group { margin-top: 18px; .card-panel-col { margin-bottom: 32px; } .card-panel { height: 108px; cursor: pointer; font-size: 12px; position: relative; overflow: hidden; color: #666; background: #fff; box-shadow: 4px 4px 40px rgba(0, 0, 0, .05); border-color: rgba(0, 0, 0, .05); &:hover { .card-panel-icon-wrapper { color: #fff; } .icon-people { background: #40c9c6; } .icon-message { background: #36a3f7; } .icon-money { background: #f4516c; } .icon-shopping { background: #34bfa3 } } .icon-people { color: #40c9c6; } .icon-message { color: #36a3f7; } .icon-money { color: #f4516c; } .icon-shopping { color: #34bfa3 } .card-panel-icon-wrapper { float: left; margin: 14px 0 0 14px; padding: 16px; transition: all 0.38s ease-out; border-radius: 6px; } .card-panel-icon { float: left; font-size: 48px; } .card-panel-description { float: right; font-weight: bold; margin: 26px; margin-left: 0px; .card-panel-text { line-height: 18px; color: rgba(0, 0, 0, 0.45); font-size: 16px; margin-bottom: 12px; } .card-panel-num { font-size: 20px; } } } } @media (max-width:550px) { .card-panel-description { display: none; } .card-panel-icon-wrapper { float: none !important; width: 100%; height: 100%; margin: 0 !important; .svg-icon { display: block; margin: 14px auto !important; float: none !important; } } } </style>
在views/dashboard/index.vue
引入PanelGroup.vue
面板组件统计子组件
<template> <div class="dashboard-container"> <!-- 面板统计--> <panel-group/> </div> </template> <script> import PanelGroup from '@/views/dashboard/components/PanelGroup.vue' export default { components: { PanelGroup } } </script>
调用API接口动态渲染数据 API接口
查询用户总记录数
URL
method
description
/system/user/total
get
查询用户总记录数
MockJS
{ "code" : 20000 , "message" : "成功" , "data" : "@integer(10, 3000)" , }
查询文章总记录数
URL
method
description
/article/article/total
get
查询文章总记录数
MockJS
{ "code" : 20000 , "message" : "成功" , "data" : "@integer(10, 4000)" , }
查询提问总记录数
URL
method
description
/question/question/total
get
查询提问总记录数
MockJS
{ "code" : 20000 , "message" : "成功" , "data" : "@integer(10, 5000)" , }
查询各分类下的文章数
URL
method
description
/article/article/category/total
get
查询各分类下的文章数
MockJS
{ "code" : 20000 , "message" : "成功" , "data" : { "nameAndValueList" : [ { "name" : "Java" , "value" : "@integer(10, 10000)" } , { "name" : "人工智能" , "value" : "@integer(10, 10000)" } , { "name" : "前端" , "value" : "@integer(10, 10000)" } , { "name" : "区块链" , "value" : "@integer(10, 10000)" } , { "name" : "大数据" , "value" : "@integer(10, 10000)" } , { "name" : "游戏" , "value" : "@integer(10, 10000)" } , { "name" : "移动端" , "value" : "@integer(10, 10000)" } ] , "nameList" : [ "Java" , "人工智能" , "前端" , "区块链" , "大数据" , "游戏" , "移动端" ] } }
查询近6个月发布的文章数
URL
method
description
/article/article/month/total
get
MockJS
{ "code" : 20000 , "message" : "成功" , "data" : { "yearMonthList" : [ "2022-02" , "2022-03" , "2022-04" , "2022-05" , "2022-06" , "2022-07" ] , "aritcleTotalList|6" : [ "@integer(100, 10000)" ] } }
调用API接口 创建src/api/dashboard.js
import request from '@/utils/request' export default { getUserTotal ( ) { return request ({ url : `/system/user/total` , method : 'get' }) }, getArticleTotal ( ) { return request ({ url : `/article/article/total` , method : 'get' }) }, getQuestionTotal ( ) { return request ({ url : `/question/question/total` , method : 'get' }) }, getCategoryTotal ( ) { return request ({ url : `/article/article/category/total` , method : 'get' }) }, getMonthAritcleTotal ( ) { return request ({ url : `/article/article/month/total` , method : 'get' }) } }
渲染接口数据 编辑src/views/dashboard/index.vue
实现查询总用户、总文章、总问答
<template> <div class="dashboard-container"> <!-- 面板统计--> <panel-group :userTotal="userTotal" :articleTotal="articleTotal" :questionTotal="questionTotal"/> <el-row :gutter="40"> <el-col :xs="24" :sm="24" :lg="12"> <el-card> <!-- 饼状图文章统计--> <pie-chart v-if="flag" :legendData="categoryTotal.nameList" :seriesData="categoryTotal.nameAndValueList"/> </el-card> </el-col> <el-col :xs="24" :sm="24" :lg="12"> <el-card> <!-- 柱状图近6个月发布文章数--> <bar-chart v-if="flag" :xAxisData="monthAritcleTotal.yearMonthList" :seriesData="monthAritcleTotal.aritcleList"/> </el-card> </el-col> </el-row> </div> </template> <script> import api from '@/api/dashboard' import PieChart from './components/PieChart.vue' import BarChart from '@/views/dashboard/components/BarChart.vue' import PanelGroup from '@/views/dashboard/components/PanelGroup.vue' export default { components: { PieChart, BarChart, PanelGroup }, data() { return { userTotal: 0, articleTotal: 0, questionTotal: 0, flag: false, // 是否显示子组件,加载数据完成后 categoryTotal: {}, monthAritcleTotal: {} // 查询近6个月发布的文章数 } }, mounted() { this.getTotal() this.getArticleTotal() }, methods: { async getTotal() { // 总用户 const { data: userTotal } = await api.getUserTotal() this.userTotal = userTotal // 总文章 const { data: articleTotal } = await api.getArticleTotal() this.articleTotal = articleTotal // 总提问 const { data: questionTotal } = await api.getQuestionTotal() this.questionTotal = questionTotal }, async getArticleTotal() { // 每个分类下的文章数 const { data: categoryTotal } = await api.getCategoryTotal() this.categoryTotal = categoryTotal // 查询近6个月发布的文章数 const { data: monthAritcleTotal } = await api.getMonthAritcleTotal() this.monthAritcleTotal = monthAritcleTotal // 数据加载完成,显示子组件 this.flag = true } } } </script> <style lang="scss" scoped> .dashboard { &-container { margin: 30px; } &-text { font-size: 30px; line-height: 46px; } } </style>
前后端分离单点登录 参考:前后端分离单点登录
博客权限系统 登录功能 需求分析 重点核心关注src\permission.js
路由拦截器,如果没有token
,则跳转登录页。 登录后我们在路由拦截器中,从Cookie
中获取认证信息userInfo、access_token、refresh_token
。
实现跳转认证客户端 .env.development
和.env.production
分别添加认证中心URLVUE_APP_AUTH_CENTER_URL
和Cookie
认证信息保存域VUE_APP_AUTH_DOMAIN
VUE_APP_AUTH_CENTER_URL = '//localhost:8080' VUE_APP_AUTH_DOMAIN = 'localhost'
VUE_APP_AUTH_CENTER_URL = '//login.acaiblog.top' VUE_APP_AUTH_DOMAIN = '.acaiblog.top'
修改src\permission.js
路由拦截器,如果没有token
,则跳转认证客户端 http://localhost:7000
if (whiteList.indexOf (to.path ) !== -1 ) { next () } else { window .location .href = `${process.env.VUE_APP_AUTH_CENTER_URL} ?redirect=${window .location.href} ` NProgress .done () }
测试:打开浏览器隐私模式,访问blog-admin后台,会自动跳转到sso登录
路由拦截器获取认证信息 当登录成功后,我们要重写向回引发跳转到登录页的地址;当重写向回来后,我们可以从浏览器Cookie
中获取认证信息userInfo、access_token、refresh_token
;创建Cookie
工具类src/utils/cookie.js
import Cookies from 'js-cookie' export const Key = { accessTokenKey : 'accessToken' , refreshTokenKey : 'refreshToken' , userInfoKey : 'userInfo' } class CookieClass { constructor ( ) { this .domain = process.env .VUE_APP_COOKIE_DOMAIN this .expireTime = 30 } set (key, value, expires, path = '/' ) { CookieClass .checkKey (key) Cookies .set (key, value, { expires : expires || this .expireTime , path : path, domain : this .domain }) } get (key ) { CookieClass .checkKey (key) return Cookies .get (key) } remove (key, path = '/' ) { CookieClass .checkKey (key) Cookies .remove (key, { path : path, domain : this .domain }) } geteAll ( ) { Cookies .get () } static checkKey (key ) { if (!key) { throw new Error ('没有找到key。' ) } if (typeof key === 'object' ) { throw new Error ('key不能是一个对象。' ) } } } export const PcCookie = new CookieClass ()
在permission.js
从cookie
中获取accessToken 、userInfo
import { PcCookie , Key } from '@/utils/cookies' const whiteList = ['/login' ] router.beforeEach (async (to, from , next) => { const hasToken = PcCookie .get (Key .accessTokenKey ) if (hasToken) { if (to.path === '/login' ) { next ({ path : '/' }) NProgress .done () } else { const hasGetUserInfo = PcCookie .get (Key .userInfoKey ) if (hasGetUserInfo) { next () } else { window .location .href = `${process.env.VUE_APP_AUTH_CENTER_URL} ?redirect=${window .location.href} ` } } } else { } })
编辑认证客户端src/views/auth/login.vue
<script> export default { loginSubmit() { // 提交登录 this.$store.dispatch('UserLogin', this.loginData).then(response => { const { code, message } = response if(code === 20000) { // 跳转到原来的页面 console.log('route: ',this.$route.query.redirect) this.redirectURL = this.$route.query.redirect window.location.href = this.redirectURL }else { this.loginMessage = message } this.subState = false //提交完成 }).catch(error => { this.subState = false this.loginMessage = error }) } } </script>
请求头添加访问令牌 accessToken 针对每个请求,如果有访问令牌accessToken
, 请求头带上令牌Authorization: Bearer $token
修改src/utils/request.js
import { PcCookie , Key } from '@/utils/cookies' service.interceptors .request .use ( config => { const accessTokenKey = PcCookie .get (Key .accessTokenKey ) if (accessTokenKey) { config.headers .Authorization = 'Bearer ' + accessTokenKey } return config }, error => { console .log (error) return Promise .reject (error) } )
右上角显示头像信息 修改头部导航组件src/layout/components/Navbar.vue
引用userInfo.imageUrl
即可
<script> import { mapGetters } from 'vuex' import { PcCookie, Key } from '@/utils/cookies' export default { computed: { ...mapGetters([ 'sidebar' // 'avatar' ]), avatar() { return PcCookie.get(Key.userInfoKey) ? JSON.parse(PcCookie.get(Key.userInfoKey)).imageUrl : '' } } } </script>
退出系统 点击右上角退出系统,发送请求给认证中心 http://localhost:7000/logout?redirectURL=xxx 删除服务器用户登录数据,并将 cookie 中的用户数据清除。修改头部导航组件src/layout/components/Navbar.vue
的logout
方法
<script> export default { methods: { async logout() { // await this.$store.dispatch('user/logout') // this.$router.push(`/login?redirect=${this.$route.fullPath}`) window.location.href = `${process.env.VUE_APP_AUTH_CENTER_URL}/logout?redirectURL=${window.location.href}` } } } </script>
刷新令牌获取新的认证信息 当访问令牌 access_toke 过期,后台会响应状态码 401 ,通过刷新令牌 refresh_toke 获取新令牌。获取后重新发送引发获取新令牌的请求。
请求拦截401错误实现刷新令牌请求 所有的请求后台数据,都是通过在src/utils/request.js
封装的axios
对象进行发送请求,所以当调用后台接口后,都可以在此axios
对象对响应结果进行拦截。 当后台错误状态时,会进行error {}
配置处理,所以当响应401错误状态码时,在error
处进行拦截去进行通过刷新令牌获取新的认证信息:userInfo、access_token、refresh_token
。 刷新令牌获取新的认证信息,直接请求认证客户端 http://localhost:7000/refresh?redirectURL=xxx 编辑src/utils/request.js
的error
处触请求认证客户端刷新令牌
service.interceptors .response .use ( response => { }, error => { console .log ('err' + error) if (error.response && error.response .status !== 401 ) { Message ({ message : error.message , type : 'error' , duration : 5 * 1000 }) return Promise .reject (error) } let isLock = true if (isLock && PcCookie .get (Key .refreshTokenKey )) { isLock = false window .location .href = `${process.env.VUE_APP_AUTH_CENTER_URL} /refresh? redirectURL=${window .location.href} ` } else { window .location .href = `${process.env.VUE_APP_AUTH_CENTER_URL} ?redirectURL=${window .location.href} ` } return Promise .reject ('令牌过期,重新认证' ) } )
权限控制菜单和按钮 左侧菜单导航布局组件位于src/layout/components/Sidebar/index.vue
,其中Sidebar/SidebarItem.vue
渲染菜单子组件。vue-element-template默认是根据
src/router/index.js`中配置的路由表,来动态渲染菜单的。 前端能控制的权限都只是页面级的,不同权限的用户显示不同的侧边栏和限制其所能进入的页面,其实前端再怎么做权限控制都不是绝对安全的,后端的权限验证才是最核心最重要的。 后端则会验证每一个涉及请求的操作,验证其是否有该操作的权限,每一个后台的请求不管是get还是post都会让前端在请求header里面携带用户的 token,后端会根据该token来验证用户是否有权限执行该操作。若没有权限则抛出一个对应的状态码,前端检测到该状态码,做出相对应的操作。
具体实现
当用户登录后,会进行路由跳转到首页。跳转前,在路由拦截器里获取用户拥有的权限菜单。
使用vuex 权限菜单集合,根据vuex中可访问的菜单,渲染侧边栏组件。
查询用户菜单权限树API接口
URL
method
description
/system/menu/user/{userId}
get
查询用户菜单权限树API接口
MockJS
{ "code" : 20000 , "message" : "成功" , "data" : { "buttonList" : [ "article:search" , "article:audit" , "article:edit" , "article:delete" , "category:search" , "category:add" , "category:edit" , "category:detele" , "label:search" , "label:add" , "label:delete" , "label:edit" , "advert:search" , "advert:add" , "advert:edit" , "advert:delete" , "user:search" , "user:add" , "user:edit" , "user:delete" , "user:role" , "user:password" , "role:search" , "role:add" , "role:edit" , "role:delete" , "role:permission" , "menu:search" , "menu:add" , "menu:edit" , "menu:delete" ] , "menuTreeList" : [ { "id" : "11" , "parentId" : "0" , "name" : "首页" , "url" : "/dashboard" , "type" : 1 , "code" : "" , "icon" : "el-icon-s-home" , "sort" : 1 , "remark" : "" , "createDate" : "2023-08-08T03:11:11.000+0000" , "updateDate" : "2020-04-24T14:28:38.000+0000" , "children" : [ ] } , { "id" : "1251060960026705921" , "parentId" : "0" , "name" : "博客管理" , "url" : "" , "type" : 1 , "code" : "" , "icon" : "el-icon-notebook-2" , "sort" : 2 , "remark" : null , "createDate" : "2020-04-17T08:12:31.000+0000" , "updateDate" : "2020-04-24T14:29:29.000+0000" , "children" : [ { "id" : "1251061228965478402" , "parentId" : "1251060960026705921" , "name" : "文章管理" , "url" : "/blog/article" , "type" : 2 , "code" : "article" , "icon" : "el-icon-notebook-1" , "sort" : 1 , "remark" : null , "createDate" : "2020-04-17T08:13:35.000+0000" , "updateDate" : "2020-04-24T07:40:53.000+0000" , "children" : [ ] } , { "id" : "1251061181913776129" , "parentId" : "1251060960026705921" , "name" : "分类管理" , "url" : "/blog/category" , "type" : 2 , "code" : "category" , "icon" : "el-icon-s-order" , "sort" : 2 , "remark" : null , "createDate" : "2020-04-17T08:13:24.000+0000" , "updateDate" : "2020-04-24T02:33:14.000+0000" , "children" : [ ] } , { "id" : "1251061016268128258" , "parentId" : "1251060960026705921" , "name" : "标签管理" , "url" : "/blog/label" , "type" : 2 , "code" : "label" , "icon" : "el-icon-collection-tag" , "sort" : 3 , "remark" : null , "createDate" : "2020-04-17T08:12:44.000+0000" , "updateDate" : "2020-04-24T02:33:28.000+0000" , "children" : [ ] } ] } , { "id" : "1251071493878632450" , "parentId" : "0" , "name" : "广告管理" , "url" : "/advert/index" , "type" : 2 , "code" : "adver" , "icon" : "el-icon-picture-outline-round" , "sort" : 3 , "remark" : null , "createDate" : "2020-04-17T08:54:22.000+0000" , "updateDate" : "2020-04-24T14:20:46.000+0000" , "children" : [ ] } , { "id" : "17" , "parentId" : "0" , "name" : "系统管理" , "url" : "" , "type" : 1 , "code" : "system" , "icon" : "el-icon-setting" , "sort" : 4 , "remark" : null , "createDate" : "2023-08-08T03:11:11.000+0000" , "updateDate" : "2020-04-24T14:28:56.000+0000" , "children" : [ { "id" : "18" , "parentId" : "17" , "name" : "用户管理" , "url" : "/system/user" , "type" : 2 , "code" : "user" , "icon" : "el-icon-user-solid" , "sort" : null , "remark" : null , "createDate" : "2023-08-08T03:11:11.000+0000" , "updateDate" : "2023-08-09T07:26:28.000+0000" , "children" : [ ] } , { "id" : "23" , "parentId" : "17" , "name" : "角色管理" , "url" : "/system/role" , "type" : 2 , "code" : "role" , "icon" : "el-icon-coin" , "sort" : null , "remark" : null , "createDate" : "2023-08-08T03:11:11.000+0000" , "updateDate" : "2023-08-09T07:26:28.000+0000" , "children" : [ ] } , { "id" : "28" , "parentId" : "17" , "name" : "菜单管理" , "url" : "/system/menu" , "type" : 2 , "code" : "menu" , "icon" : "el-icon-menu" , "sort" : null , "remark" : null , "createDate" : "2023-08-08T03:11:11.000+0000" , "updateDate" : "2023-08-09T07:26:28.000+0000" , "children" : [ ] } ] } , { "id" : "1253513166296616961" , "parentId" : "0" , "name" : "梦学谷官网" , "url" : "http://www.mengxuegu.com" , "type" : 2 , "code" : "public" , "icon" : "el-icon-link" , "sort" : 5 , "remark" : null , "createDate" : "2020-04-24T02:36:42.000+0000" , "updateDate" : "2020-04-24T07:58:34.000+0000" , "children" : [ ] } ] } }
调用查询用户菜单权限API接口 在src/api/user.js
文件中添加如下方法
export function getUserMenuList (userId ) { return request ({ url : `/system/menu/user/${userId} ` , method : 'get' }) }
定义菜单Vuex状态管理 创建src/store/modules/menu.js
菜单Vuex
状态管理文件
import { PcCookie , Key } from '@/utils/cookies' import { getUserMenuList } from '@/api/user' import { resolve } from 'vue-count-to/webpack.config' const state = { init : false , menuList : [], buttonList : [] } const mutations = { SET_SYSTEM_MENU : (state, data ) => { state.init = true state.menuList = data.menuTreeList state.buttonList = data.menuButtonList } } const actions = { GetUserMenu ({ commit }) { return new Promise ((resolve, reject ) => { const userId = PcCookie .get (Key .userInfoKey ) ? JSON .parse (PcCookie .get (Key .userInfoKey )).uid : null if (userId) { getUserMenuList (userId).then (response => { commit ('SET_SYSTEM_MENU' , response.data ) resolve () }).catch (error => { reject (error) }) } }) } } export default { namespaced : true , state, mutations, actions }
编辑src/store/getters.js
将menu.js
菜单相关状态导出
const getters = { init : state => state.menu .init , menuList : state => state.menu .menuList , buttonList : state => state.menu .buttonList } export default getters
编辑src/store/index.js
导出menu.js
状态管理文件
import menu from '@/store/modules/menu' Vue .use (Vuex )const store = new Vuex .Store ({ modules : { menu }, getters }) export default store
路由权限拦截器 编辑src/permission.js
,当路由跳转前,会被路由拦截下来获取用户菜单,查看控制台打印的日志
router.beforeEach (async (to, from , next) => { NProgress .start () document .title = getPageTitle (to.meta .title ) const hasToken = PcCookie .get (Key .accessTokenKey ) if (hasToken) { if (to.path === '/login' ) { next ({ path : '/' }) NProgress .done () } else { const hasGetUserInfo = PcCookie .get (Key .userInfoKey ) if (hasGetUserInfo) { if (store.getters .init === false ) { store.dispatch ('menu/GetUserMenu' ).then (() => { next ({ ...to, replace : true }) }) } else { next () } } else { window .location .href = `${process.env.VUE_APP_AUTH_CENTER_URL} ?redirect=${window .location.href} ` } } } else { if (whiteList.indexOf (to.path ) !== -1 ) { next () } else { window .location .href = `${process.env.VUE_APP_AUTH_CENTER_URL} ?redirect=${window .location.href} ` NProgress .done () } } })
用户菜单渲染在左侧区域 左侧菜单布局组件位于src/layout/components/Sidebar/index.vue
,其中它的子组件Sidebar/SidebarItem.vue
是主要渲染菜单的。 编辑src/layout/components/Sidebar/index.vue
中的计算属性computed
的...mapGetters
中引入Vuex菜单状态menuList
。
<template> <div :class="{'has-logo':showLogo}"> <logo v-if="showLogo" :collapse="isCollapse" /> <el-scrollbar wrap-class="scrollbar-wrapper"> <el-menu> <sidebar-item v-for="menu in menuList" :key="menu.id" :item="menu"/> </el-menu> </el-scrollbar> </div> </template> <script> import { mapGetters } from 'vuex' export default { components: { SidebarItem, Logo }, computed: { ...mapGetters([ 'sidebar', 'menuList' ]) } } </script>
修改子组件src/layout/components/Sidebar/SidebarItem.vue
渲染菜单
<template> <div> <!-- 没有子菜单,即只有一级菜单,item 每个菜单对象--> <template v-if="!item.children || item.children.length === 0"> <!-- :to="item.url" 请求路径--> <app-link :to="item.url"> <!-- index 唯一标识--> <el-menu-item :index="item.url" :class="{'submenu-title-noDropdown':!isNest}"> <item :icon="item.icon" :title="item.name" /> </el-menu-item> </app-link> </template> <!-- 有子菜单, index 请求地址--> <el-submenu v-else :index="item.id" popper-append-to-body> <!-- slot=”title"标识它下面是一级菜单名称--> <template slot="title"> <item :icon="item.icon" :title="item.name" /> </template> <!-- 子菜单,再次引用自身组件, is-nest 显示箭头--> <sidebar-item v-for="child in item.children" :key="child.id" :is-nest="true" :item="child" class="nest-menu"/> </el-submenu> </div> </template>
测试:查看菜单是否被渲染
按钮级别权限控制 需求 通过自定义全局指令v-permission
来控制按钮的显示或隐藏。
注册全局指令 新建src/directive/index.js
文件,引入所有要注册的全局指令。
import permission from '@/directive/permission' export default (Vue ) => { Vue .directive ('permission' , permission) }
创建src/directive/permission/index.js
定义按钮权限指令
import store from '@/store' export default { inserted (el, binding ) { const { value } = binding const buttonList = store.getters && store.getters .buttonList if (value) { const hasPermission = buttonList.some (button => { return value === button }) if (!hasPermission) { el.parentNode && el.parentNode .removeChild (el) } } else { throw new Error (`需要指定权限标识!如 v-permission="article:add"` ) } } }
在src/main.js
中引入directive/index.js
文件并使用Vue.use()
全局注册。
import directive from '@/directive' Vue .use (directive)
使用v-permission指令 使用指令v-permission="'advert:add'"
,注意:双引号里面不要少了单引号
<el-button v-permission="'advert:add'" type="primary" size="mini" icon="el-icon-circle-plus-outline" @click="openAdd" >新增</el-button>