博客后台管理系统

环境搭建

技术栈

博客系统采用 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

运行项目

npm install
npm run dev

MacOS运行报错:

TypeError: fsevents.watch is not a function

解决方式:在package.json添加以下内容重新执行npm install && npm run dev即可解决

"optionalDependencies": {
"fsevents": "~2.3.2"
}

项目初始化

重命名项目

  1. 将目录名vue-admin-template重命名为vue-blog-admin
  2. 编辑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

// set ElementUI lang to EN
// Vue.use(ElementUI, { locale })
// 如果想要中文版 element-ui,按如下方式声明
Vue.use(ElementUI)

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.vueLogo图片拷贝到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.jstagsView状态管理模块添加到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-serverexpress的中间件body-parser导致的,表现为不带参数请求没有问题,带上参数就出现超时异常
解决方式:编辑mock/mock-server.js,完成之后重新运行项目测试。

function registerRoutes(app) {
for (const mock of mocksForServer) {
// app[mock.type](mock.url, mock.response)
// mockLastIndex = app._router.stack.length
app[mock.type](mock.url, bodyParser.json(), bodyParser.urlencoded({
extended: true
}), mock.response)
}
}

module.exports = app => {
// parse app.body
// https://expressjs.com/en/4x/api.html#req.body
// app.use(bodyParser.json())
// app.use(bodyParser.urlencoded({
// extended: true
// }))
}

文章分类模块

分类列表

列表数据接口

URL method description
/article/category/search post 文章类别分页条件查询列表

MockJS

{
"code": 20000,
"message": "查询成功",
"data": {
"total": "@integer(100, 200)", // 总记录数
"records|20": [{ //生成20条数据
"id|+1": 10, //初始值10开始,每条+1
"name": "@cname", // 随机一个名字
"sort": "@integer(0,9)", // 0-9间的数字
"remark": "@csentence(5, 15)",
"status|1": [0, 1], // 二选其一,注意数字不要用单引号"createDate": "@date", // 随机时间
}]
}
}

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.vuedata选项中添加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, // 返回 id=10 数据
"name": "Java",
"sort": "@integer(0,9)", // 0-9间的数字
"remark": "@csentence(5, 15)",
"status|1": [0, 1], // 二选其一,注意数字不要用单引号"createDate": "@date", // 随机时间
}
}

添加分类修改接口

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)", // 总记录数"records|20": [{ //生成20条数据
"id|+1": 10, //初始值10开始,每条+1
"categoryName": "@cname", //类别ID
"name": "@cname", // 随机一个标签名
"createDate": "@date", // 随机创建时间"updateDate": "@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": [{ // 产生8条数据
"id|+1": 10, //初始值10开始,每条+1, "name": "@cname", // 随机一个名字
"sort": "@integer(0,9)", // 0-9间的数字"remark": "@csentence(5, 15)",
"status": 1, // 查询正常状态
"createDate": "@date", // 随机时间
}]
}

API调用分类接口

src/api/label.js添加getNormalList方法获取所有正常状态的分类

getNormalList() {
return request({
url: `article/category/list`,
method: 'get'
})
}

src/views/label/index.vuedata中声明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, // 返回 id=8 数据
"categoryId|1": "@integer(10, 17)", // 类别Id 11到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": [{ //生成20条数据
"id|+1": 10, //初始值10开始,每条+1
"title": "@ctitle", // 标题
"viewCount": "@integer(0, 100000)", // 浏览次数
"thumhup": "@integer(0, 100000)", // 点赞数
"ispublic|1": [0, 1], // 0: 不公开 1:公开
"status|1": [0, 1, 2, 3], // 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


/**
* @desc 格式化日期字符串
* @param { Nubmer} - Date日期 , 时间不能大于当前时间,大于当前时间会返回“刚刚”。
* @returns { String } 格式化后的日期字符串
// 2012年01月10日 12:46
//刚刚
//16分钟前
//今天10:10
//昨天10:10
//02月10日 10:10:11
//2012年10月10日 10:10:11
*/
export function dateFormat(date) {
// new Date 在 ios safari浏览器有兼容性问题处理如下:
// ? 兼容safari : 兼容其他浏览器
const $this = new Date(date) === 'Invalid Date' ? new Date(date.substr(0, 19)) : new Date(date)

var timestamp = parseInt(Date.parse($this)) / 1000 // - 8 * 60 * 60; //(本地时间)东八区减去8小时;

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)
}
}
}
/**
* @param date
* @param format
*/
export function format(date, format = 'yyyy-MM-dd hh:mm:ss') {
// new Date 在 ios safari浏览器有兼容性问题处理如下:
// ? 兼容safari : 兼容其他浏览器
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, // 返回 id=1
"title": "@ctitle", // 标题
"labelIds|1-5": ['@integer(10, 24)'], //随机产生1到5个元素的数字数组,数字取值10到24间
"summary": "@csentence(10, 30)", // 简介,一段中文文本(10到30个字),
"imageUrl": "https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg",
"mdContent": "# 1号标题\n# 2号标题", // md内容
"htmlContent": "<h1><a id=\"1_0\"></a>1号标题</h1>\n<h1><a id=\"2_1\"></a>2号标题 < /h1 > \n ",
"ispublic|1": [0, 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属性做监听,当visibletrue时,则弹出了窗口,就调用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": [{ // 5个分类
"id|+1": 1, // 分类id, 初始值1开始,每条+1
"name": "@cname", // 分类名称
"labelList|3": [{ // 分类下的有3个标签
"id|+1": 10, // 标签id
"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中的auditSuccessauditFail分别调用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": [{ //生成20条数据
"id|+1": 10, //初始值10开始,每条+1
"title": "@ctitle", //广告标题
"imageUrl": "https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg",
"advertUrl": "www.mengxuegu.com", // 广告链接
"advertTarget|1": ["_blank", "_self"], // 广告跳转方式
"position": 1, // 广告位置(1:首页轮播)
"status|1": [0, 1], // 状态(1:正常,0:禁用)
"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() {
// 子组件关闭窗口调用改方法,然后将visible的值通过props的方法传递给子组件
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.vueedit组件中调用上传、删除图片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, // 返回 id=10 数据
"title": "@ctitle", //广告标题
"imageUrl": "https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg",
"advertUrl": "www.mengxuegu.com", // 广告链接
"advertTarget|1": ["_blank", "_self"], // 广告跳转方式
"position": 1, // 广告位置(1:首页轮播)
"status|1": [0, 1], // 状态(1:正常,0:禁用)
"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": [{ //生成10条数据
"id|+1": 1, //初始值1开始,每条+1
"parentId": 0,
"name": "@cname",
"url": "@domain",
"code": "@word",
"type": 1, // 类型(1目录,2菜单,3按钮)
"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], // 类型(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], // 类型(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": [{ //生成20条数据
"id|+1": 1, //初始值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": [
// 随机产生2到5个元素的数组,每个元素值是到20到30之间// 这里会有重复值,真实数据不会返回重复的。
"@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": [{ //生成20条数据
"id|+1": 1, //初始值1开始,每条+1
"username": "@name",
"password": "xxxxxxx",
"isAccountNonExpired|1": [0, 1], // 帐户是否过期(1 未过期,0已过期)
"isAccountNonLocked|1": [0, 1], // 帐户是否被锁定(1 未过期,0已过期)
"isCredentialsNonExpired|1": [0, 1], // 密码是否过期(1 未过期,0已过期)
"isEnabled|1": [0, 1], // 帐户是否可用(1 可用,0 删除用户)
"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], // 帐户是否过期(1 未过期,0已过期)
"isAccountNonLocked|1": [0, 1], // 帐户是否被锁定(1 未过期,0已过期)
"isCredentialsNonExpired|1": [0, 1], // 密码是否过期(1 未过期,0已过期)
"isEnabled": 1, // 帐户是否可用(1 可用,0 删除用户)
"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' // put 方式提交data,
})
}

编辑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' // 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>

设置用户角色

需求分析

  1. 点击设置角色按钮,复用角色列表组件src/views/role/edit.vue以弹出窗显示。
  2. 针对角色列表组件我们要进行一次重构,添加勾选框和设置角色按钮进行提交用户角色数据。
  3. 根据用户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": [
// 随机产生2到5个元素的数组,每个元素值是到1到20之间// 这里会有重复值,真实数据不会返回重复的。
"@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>

如果直接是访问 角色管理展示角色列表,则roleIdsnull
如果是用户管理弹出的角色列表,即使用户没有角色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-keyref属性在角色列表第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.jsupdatePassword提交表单数据。编辑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

npm install 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项目中已经实现了自适应效果,只要将对应代码拷贝引用即可。

  1. vue-element-admin-master/src/views/dashboard/admin/components/mixins目录文件拷贝到src/views/dashboard/components/mixins
  2. vue-element-admin-master/src/utils/index.js文件里的debounce函数,拷贝到src/utils/index.js文件中

src/views/dashboard/components/PanelGroup.vuePieChart.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

npm install 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'
})
},
// 查询近6个月发布的文章数
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_URLCookie认证信息保存域VUE_APP_AUTH_DOMAIN

# 开发环境,认证中心地址,需要以 `VUE_APP_` 开头
VUE_APP_AUTH_CENTER_URL = '//localhost:8080'
# 开发环境,认证信息保存在哪个域名下。需要以 `VUE_APP_` 开头。
VUE_APP_AUTH_DOMAIN = 'localhost'
# 生产环境,认证中心地址,需要以 `VUE_APP_` 开头
VUE_APP_AUTH_CENTER_URL = '//login.acaiblog.top'
# 生产环境,认证信息保存在哪个域名下。需要以 `VUE_APP_` 开头。
VUE_APP_AUTH_DOMAIN = '.acaiblog.top'

修改src\permission.js路由拦截器,如果没有token,则跳转认证客户端 http://localhost:7000

if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
// next(`/login?redirect=${to.path}`)
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'

// Cookie的key值
export const Key = {
accessTokenKey: 'accessToken', // 访问令牌在cookie的key值
refreshTokenKey: 'refreshToken', // 刷新令牌在cookie的key值
userInfoKey: 'userInfo'
}

class CookieClass {
constructor() {
this.domain = process.env.VUE_APP_COOKIE_DOMAIN // 域名
this.expireTime = 30 // 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.jscookie中获取accessToken 、userInfo

import { PcCookie, Key } from '@/utils/cookies'

const whiteList = ['/login'] // no redirect whitelist

router.beforeEach(async(to, from, next) => {
// 获取accessToken
const hasToken = PcCookie.get(Key.accessTokenKey)
if (hasToken) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
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 {
/* has no token*/
}
})

编辑认证客户端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 => {
// do something with request error
console.log(error) // for debug
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.vuelogout方法

<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.jserror处触请求认证客户端刷新令牌

service.interceptors.response.use(
response => {

},
error => {
console.log('err' + error) // for debug
if (error.response && error.response.status !== 401) {
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
// 401 发送刷新令牌请求
// 锁, 防止并发重复请求, true 还未请求,false 正在请求刷新
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来验证用户是否有权限执行该操作。若没有权限则抛出一个对应的状态码,前端检测到该状态码,做出相对应的操作。

具体实现

  1. 当用户登录后,会进行路由跳转到首页。跳转前,在路由拦截器里获取用户拥有的权限菜单。
  2. 使用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, // 引用时需要模块名,即当前文件名 menu/getUserMenu
state,
mutations,
actions
}

编辑src/store/getters.jsmenu.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) => {
// start progress bar
NProgress.start()

// set page title
document.title = getPageTitle(to.meta.title)

// determine whether the user has logged in
// const hasToken = getToken()
// 获取accessToken
const hasToken = PcCookie.get(Key.accessTokenKey)
if (hasToken) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done()
} else {
// 获取用户信息
// const hasGetUserInfo = store.getters.name
const hasGetUserInfo = PcCookie.get(Key.userInfoKey)
if (hasGetUserInfo) {
if (store.getters.init === false) {
store.dispatch('menu/GetUserMenu').then(() => {
next({ ...to, replace: true }) // 继承访问目标路由且不会留下history记录
})
} else {
next()
}
} else {
// 如果没有用户信息跳转到认证客户端
window.location.href = `${process.env.VUE_APP_AUTH_CENTER_URL}?redirect=${window.location.href}`
}
}
} else {
/* has no token*/

if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
// next(`/login?redirect=${to.path}`)
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) => {
// 添加权限指令 v-permission
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) {
// some 遇到 return true 就是终止遍历
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>
文章作者: 慕容峻才
文章链接: https://www.acaiblog.top/博客后台管理系统/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿才的博客
微信打赏
支付宝打赏