1.建表语句(mysql)
menu
CREATE TABLE `menu` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键(自增id)',
`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',
`gmt_modify` datetime DEFAULT NULL COMMENT '更新时间',
`name` varchar(125) DEFAULT NULL COMMENT '路径名称',
`path` varchar(125) DEFAULT NULL COMMENT '路径',
`parent_id` bigint(20) DEFAULT NULL COMMENT '父节点id',
`sort` int(11) DEFAULT NULL COMMENT '排序(从小到大)',
`icon` varchar(55) DEFAULT NULL COMMENT '菜单图标',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=60 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='菜单'
;
menu_group
CREATE TABLE `menu_group` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键(自增id)',
`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',
`gmt_modify` datetime DEFAULT NULL COMMENT '更新时间',
`name` varchar(125) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '菜单组名称',
`menu_ids` varchar(555) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '菜单id',
PRIMARY KEY (`id`),
UNIQUE KEY `u_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='菜单组'
;
2.菜单列表(前端)
vue
<template>
<div class="app-container">
<div class="filter-container">
<el-select v-model="listQuery.params.parentId" class="filter-item" placeholder="请选择父节点">
<el-option v-for="item in calendarTypeOptions3" :key="item.key" :label="item.display_name" :value="item.key" />
</el-select>
<el-input v-model="listQuery.params.name" :placeholder="$t('caiDanGuanLiCaiDanLieBiao.name')" style="width: 200px;margin-left: 10px;" class="filter-item" @keyup.enter.native="handleFilter" />
<el-input v-model="listQuery.params.path" :placeholder="$t('caiDanGuanLiCaiDanLieBiao.path')" style="width: 200px;margin-left: 10px;" class="filter-item" @keyup.enter.native="handleFilter" />
<el-button v-waves class="filter-item" type="primary" icon="el-icon-search" @click="handleFilter">
{{ $t('table.search') }}
</el-button>
<el-button v-waves class="filter-item" type="primary" @click="handleReset">
{{ $t('common.reset') }}
</el-button>
<el-button class="filter-item" style="margin-left: 10px;float: right;" type="primary" icon="el-icon-edit" @click="handleCreate">
{{ $t('table.add') }}
</el-button>
</div>
<el-table
:key="tableKey"
v-loading="listLoading"
:data="list"
border
fit
highlight-current-row
style="width: 100%;"
>
<el-table-column :label="$t('caiDanGuanLiCaiDanLieBiao.name')" align="center">
<template slot-scope="{row}">
<span>{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('caiDanGuanLiCaiDanLieBiao.path')" align="center">
<template slot-scope="{row}">
<span>{{ row.path }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('caiDanGuanLiCaiDanLieBiao.parentId')" align="center">
<template slot-scope="{row}">
<span>{{ row.parentName }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('caiDanGuanLiCaiDanLieBiao.sort')" align="center">
<template slot-scope="{row}">
<span>{{ row.sort }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('caiDanGuanLiCaiDanLieBiao.icon')" align="center">
<template slot-scope="{row}">
<svg-icon :icon-class="row.icon" />
</template>
</el-table-column>
<el-table-column :label="$t('table.actions')" align="center" width="230" class-name="small-padding fixed-width">
<template slot-scope="{row,$index}">
<el-button type="primary" size="mini" @click="handleUpdate(row)">
{{ $t('table.edit') }}
</el-button>
<el-button v-if="row.status!='deleted'" size="mini" type="danger" @click="handleDelete(row,$index)">
{{ $t('table.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="listQuery.pageNo" :limit.sync="listQuery.pageSize" @pagination="getList" />
<el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
<el-form ref="dataForm" :rules="rules" :model="temp" label-position="rigth" autocomplete="on" label-width="100px" style="width: 400px; margin-left:50px;">
<el-form-item :label="$t('caiDanGuanLiCaiDanLieBiao.name')" prop="name">
<el-input v-model="temp.name" />
</el-form-item>
<el-form-item :label="$t('caiDanGuanLiCaiDanLieBiao.path')" prop="path">
<el-input v-model="temp.path" />
</el-form-item>
<el-form-item :label="$t('caiDanGuanLiCaiDanLieBiao.parentId')" prop="parentId">
<el-select v-model="temp.parentId" class="filter-item" placeholder="请选择父节点">
<el-option v-for="item in calendarTypeOptions2" :key="item.key" :label="item.display_name" :value="item.key" />
</el-select>
</el-form-item>
<el-form-item :label="$t('caiDanGuanLiCaiDanLieBiao.sort')" prop="sort">
<el-input v-model="temp.sort" />
</el-form-item>
<el-form-item :label="$t('caiDanGuanLiCaiDanLieBiao.icon')" prop="icon">
<el-select v-model="temp.icon" class="filter-item" placeholder="请选择菜单图标">
<el-option v-for="item in calendarTypeOptions" :key="item.key" :label="item.display_name" :value="item.key">
<svg-icon :icon-class="item.key" />
</el-option>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">
{{ $t('table.cancel') }}
</el-button>
<el-button type="primary" @click="dialogStatus==='create'?create():modify()">
{{ $t('table.confirm') }}
</el-button>
</div>
</el-dialog>
<el-dialog :visible.sync="dialogPvVisible" title="Reading statistics">
<el-table :data="pvData" border fit highlight-current-row style="width: 100%">
<el-table-column prop="key" label="Channel" />
<el-table-column prop="pv" label="Pv" />
</el-table>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogPvVisible = false">{{ $t('table.confirm') }}</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { pages, add, update, del, listParent } from '@/api/caiDanGuanLi/caiDanLieBiao'
import waves from '@/directive/waves' // waves directive
import Pagination from '@/components/Pagination' // secondary package based on el-pagination
export default {
name: 'ComplexTable',
components: { Pagination },
directives: { waves },
filters: {
statusFilter(status) {
const statusMap = {
published: 'success',
draft: 'info',
deleted: 'danger'
}
return statusMap[status]
}
},
data() {
return {
currentId: -2,
switchValue: true,
tableKey: 0,
list: null,
total: 0,
listLoading: true,
listQuery: {
pageNo: 1,
pageSize: 20,
params: {
name: '' || undefined,
path: '' || undefined,
parentId: '' || undefined,
sort: '' || undefined,
icon: '' || undefined
}
},
calendarTypeOptions: this.getIcons(),
calendarTypeOptions2: this.getParents(),
calendarTypeOptions3: this.getParents(),
importanceOptions: [1, 2, 3],
sortOptions: [{ label: 'ID Ascending', key: '+id' }, { label: 'ID Descending', key: '-id' }],
statusOptions: ['published', 'draft', 'deleted'],
showReviewer: false,
temp: {
id: '' || undefined,
name: '' || undefined,
path: '' || undefined,
parentId: '' || undefined,
sort: '' || undefined,
icon: '' || undefined
},
dialogFormVisible: false,
dialogStatus: '',
textMap: {
update: '编辑',
create: '添加'
},
dialogPvVisible: false,
pvData: [],
rules: {
name: [{ required: true, message: '[名称]不能为空', trigger: 'change' }],
path: [{ required: true, message: '[路径]不能为空', trigger: 'change' }],
parentId: [{ required: true, message: '[父节点]不能为空', trigger: 'change' }],
sort: [{ required: true, message: '[排序]不能为空', trigger: 'change' }],
icon: [{ required: true, message: '[图标]不能为空', trigger: 'change' }]
},
downloadLoading: false
}
},
created() {
this.getList()
},
methods: {
getParents() {
var rs = []
listParent(-2).then(response => {
response.data.forEach(d => {
var o = { key: d.id, display_name: d.name }
rs.push(o)
})
})
return rs
},
getParents2() {
var rs = []
listParent(this.currentId).then(response => {
response.data.forEach(d => {
var o = { key: d.id, display_name: d.name }
rs.push(o)
})
})
return rs
},
getList() {
this.listLoading = true
pages(this.listQuery).then(response => {
this.list = response.data.list
this.total = response.data.total
this.listLoading = false
})
},
handleReset() {
this.listQuery.pageNo = 1
this.listQuery.params.name = null
this.listQuery.params.path = null
this.listQuery.params.parentId = null
this.listQuery.params.sort = null
this.getList()
},
handleFilter() {
this.listQuery.pageNo = 1
this.getList()
},
resetTemp() {
this.temp = {
userName: undefined,
account: undefined,
phone: undefined,
roleNames: undefined,
password: undefined
}
},
handleCreate() {
this.switchValue = false
this.resetTemp()
this.dialogStatus = 'create'
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs['dataForm'].clearValidate()
})
this.calendarTypeOptions2 = this.getParents()
},
create() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
add(this.temp).then(() => {
this.list.unshift(this.temp)
this.dialogFormVisible = false
this.$notify({
title: '成功',
message: '创建成功',
type: 'success',
duration: 2000
})
this.getList()
})
}
})
},
handleUpdate(row) {
this.temp = Object.assign({}, row) // copy obj
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs['dataForm'].clearValidate()
})
this.dialogStatus = 'update'
this.currentId = row.id
this.calendarTypeOptions2 = this.getParents2()
},
modify() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
const tempData = Object.assign({}, this.temp)
update(tempData).then(() => {
const index = this.list.findIndex(v => v.id === this.temp.id)
this.list.splice(index, 1, this.temp)
this.dialogFormVisible = false
this.$notify({
title: '成功',
message: '更新成功',
type: 'success',
duration: 2000
})
this.getList()
})
}
})
},
handleDelete(row, index) {
const req = { id: row.id }
this.$confirm('确定删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
del(req).then(() => {
this.$notify({
title: '成功',
message: '删除成功',
type: 'success',
duration: 2000
})
this.getList()
})
})
},
getIcons() {
var rs = []
var o1 = { key: 'component', display_name: 'component' }
var o2 = { key: 'bug', display_name: 'bug' }
var o3 = { key: 'chart', display_name: 'chart' }
var o4 = { key: 'clipboard', display_name: 'clipboard' }
var o5 = { key: '404', display_name: '404' }
var o6 = { key: 'dashboard', display_name: 'dashboard' }
var o7 = { key: 'documentation', display_name: 'documentation' }
var o8 = { key: 'drag', display_name: 'drag' }
var o9 = { key: 'edit', display_name: 'edit' }
var o10 = { key: 'education', display_name: 'education' }
var o11 = { key: 'email', display_name: 'email' }
var o12 = { key: 'example', display_name: 'example' }
var o13 = { key: 'excel', display_name: 'excel' }
var o14 = { key: 'exit-fullscreen', display_name: 'exit-fullscreen' }
var o15 = { key: 'eye-open', display_name: 'eye-open' }
var o16 = { key: 'eye', display_name: 'eye' }
var o17 = { key: 'form', display_name: 'form' }
var o18 = { key: 'fullscreen', display_name: 'fullscreen' }
var o19 = { key: 'guide', display_name: 'guide' }
var o20 = { key: 'icon', display_name: 'icon' }
var o21 = { key: 'international', display_name: 'international' }
var o22 = { key: 'language', display_name: 'language' }
var o23 = { key: 'link', display_name: 'link' }
var o24 = { key: 'list', display_name: 'list' }
var o25 = { key: 'lock', display_name: 'lock' }
var o26 = { key: 'message', display_name: 'message' }
var o27 = { key: 'money', display_name: 'money' }
var o28 = { key: 'nested', display_name: 'nested' }
var o29 = { key: 'password', display_name: 'password' }
var o30 = { key: 'pdf', display_name: 'pdf' }
var o31 = { key: 'people', display_name: 'people' }
var o32 = { key: 'peoples', display_name: 'peoples' }
var o33 = { key: 'qq', display_name: 'qq' }
var o34 = { key: 'search', display_name: 'search' }
var o35 = { key: 'shopping', display_name: 'shopping' }
var o36 = { key: 'size', display_name: 'size' }
var o37 = { key: 'skill', display_name: 'skill' }
var o38 = { key: 'star', display_name: 'star' }
var o39 = { key: 'tab', display_name: 'tab' }
var o40 = { key: 'table', display_name: 'table' }
var o41 = { key: 'theme', display_name: 'theme' }
var o42 = { key: 'tree-table', display_name: 'tree-table' }
var o43 = { key: 'tree', display_name: 'tree' }
var o44 = { key: 'user', display_name: 'user' }
var o45 = { key: 'wechat', display_name: 'wechat' }
var o46 = { key: 'zip', display_name: 'zip' }
rs.push(o1)
rs.push(o2)
rs.push(o3)
rs.push(o4)
rs.push(o5)
rs.push(o6)
rs.push(o7)
rs.push(o8)
rs.push(o9)
rs.push(o10)
rs.push(o11)
rs.push(o12)
rs.push(o13)
rs.push(o14)
rs.push(o15)
rs.push(o16)
rs.push(o17)
rs.push(o18)
rs.push(o19)
rs.push(o20)
rs.push(o21)
rs.push(o22)
rs.push(o23)
rs.push(o24)
rs.push(o25)
rs.push(o26)
rs.push(o27)
rs.push(o28)
rs.push(o29)
rs.push(o30)
rs.push(o31)
rs.push(o32)
rs.push(o33)
rs.push(o34)
rs.push(o35)
rs.push(o36)
rs.push(o37)
rs.push(o38)
rs.push(o39)
rs.push(o40)
rs.push(o41)
rs.push(o42)
rs.push(o43)
rs.push(o44)
rs.push(o45)
rs.push(o46)
return rs
}
}
}
</script>
api
import request from '@/utils/request'
export function listParent(id) {
return request({
url: '/qjjk-api/v1/menu/list-parent?id=' + id,
method: 'get'
})
}
export function pages(data) {
return request({
url: '/qjjk-api/v1/menu/pages',
headers: { 'Content-Type': 'application/json' },
method: 'post',
data
})
}
export function add(data) {
return request({
url: '/qjjk-api/v1/menu',
headers: { 'Content-Type': 'application/json' },
method: 'post',
data
})
}
export function update(data) {
return request({
url: '/qjjk-api/v1/menu',
headers: { 'Content-Type': 'application/json' },
method: 'put',
data
})
}
export function del(data) {
return request({
url: '/qjjk-api/v1/menu',
method: 'delete',
params: data
})
}
3.菜单组列表
vue
<template>
<div class="app-container">
<div class="filter-container">
<el-input v-model="listQuery.params.name" placeholder="菜单组名称" style="width: 200px;" class="filter-item" @keyup.enter.native="handleFilter" />
<el-button v-waves class="filter-item" type="primary" icon="el-icon-search" @click="handleFilter">
{{ $t('table.search') }}
</el-button>
<el-button v-waves class="filter-item" type="primary" @click="handleReset">
{{ $t('common.reset') }}
</el-button>
<el-button class="filter-item" style="margin-left: 10px;float: right;" type="primary" icon="el-icon-edit" @click="handleCreate">
{{ $t('table.add') }}
</el-button>
</div>
<el-table
:key="tableKey"
v-loading="listLoading"
:data="list"
border
fit
highlight-current-row
style="width: 100%;"
>
<el-table-column label="菜单组名称" align="center">
<template slot-scope="{row}">
<span>{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column label="菜单列表" align="center">
<template slot-scope="{row}">
<span>{{ row.menuNames }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.actions')" align="center" width="230" class-name="small-padding fixed-width">
<template slot-scope="{row,$index}">
<el-button type="primary" size="mini" @click="handleUpdate(row)">
{{ $t('table.edit') }}
</el-button>
<el-button v-if="row.status!='deleted'" size="mini" type="danger" @click="handleDelete(row,$index)">
{{ $t('table.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="listQuery.pageNo" :limit.sync="listQuery.pageSize" @pagination="getList" />
<el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
<el-form ref="dataForm" :rules="rules" :model="temp" label-position="rigth" autocomplete="on" label-width="100px" style="width: 400px; margin-left:50px;">
<el-form-item label="菜单组名称" prop="name">
<el-input v-model="temp.name" />
</el-form-item>
<el-form-item label="菜单数">
<el-tree ref="tree" :check-strictly="checkStrictly" :data="routesData" :props="defaultProps" show-checkbox node-key="path" class="permission-tree" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">
{{ $t('table.cancel') }}
</el-button>
<el-button type="primary" @click="dialogStatus==='create'?create():modify()">
{{ $t('table.confirm') }}
</el-button>
</div>
</el-dialog>
<el-dialog :visible.sync="dialogPvVisible" title="Reading statistics">
<el-table :data="pvData" border fit highlight-current-row style="width: 100%">
<el-table-column prop="key" label="Channel" />
<el-table-column prop="pv" label="Pv" />
</el-table>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogPvVisible = false">{{ $t('table.confirm') }}</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import waves from '@/directive/waves'
import Pagination from '@/components/Pagination' // secondary package based on el-pagination
import { pages, add, update, del } from '@/api/caiDanGuanLi/caiDanZuLieBiao'
import { getListMenuTree } from '@/api/quanXianKongZhi/quanXianKongZhi'
import { Message } from 'element-ui'
const defaultRole = {
key: '',
name: '',
description: '',
routes: []
}
export default {
components: { Pagination },
directives: { waves },
data() {
return {
listQuery: {
pageNo: 1,
pageSize: 20,
params: {
name: '' || undefined,
menuId: '' || undefined
}
},
tableKey: 0,
list: null,
total: 0,
listLoading: true,
temp: {
name: '' || undefined,
menuIds: '' || undefined
},
dialogFormVisible: false,
dialogStatus: '',
textMap: {
update: '编辑',
create: '添加'
},
dialogPvVisible: false,
pvData: [],
rules: {
name: [{ required: true, message: '[菜单组名称]不能为空', trigger: 'change' }]
},
role: Object.assign({}, defaultRole),
routes: [],
rolesList: [],
dialogVisible: false,
dialogType: 'new',
checkStrictly: false,
defaultProps: {
children: 'children',
label: 'title'
}
}
},
computed: {
routesData() {
return this.routes
}
},
created() {
this.getRoutes()
this.getList()
},
methods: {
async getRoutes() {
// 菜单列表
getListMenuTree().then(response => {
this.routes = response.data
})
},
getList() {
this.listLoading = true
pages(this.listQuery).then(response => {
this.list = response.data.list
this.total = response.data.total
this.listLoading = false
})
},
handleReset() {
this.listQuery.pageNo = 1
this.listQuery.params.name = null
this.listQuery.params.menuId = null
this.getList()
},
handleFilter() {
this.listQuery.pageNo = 1
this.getList()
},
resetTemp() {
this.temp = {
name: undefined,
menuId: undefined
}
},
handleCreate() {
this.switchValue = false
this.resetTemp()
this.dialogStatus = 'create'
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs['dataForm'].clearValidate()
})
this.$nextTick(() => {
this.$refs.tree.setCheckedNodes([])
this.checkStrictly = false
})
},
create() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
const checkedKeys = this.$refs.tree.getCheckedKeys()
if (checkedKeys.length === 0){
Message({
message: '请勾选菜单',
type: 'error',
duration: 1 * 1000
})
}
this.temp.menuIds = JSON.stringify(checkedKeys)
console.log(checkedKeys)
add(this.temp).then(() => {
this.list.unshift(this.temp)
this.dialogFormVisible = false
this.$notify({
title: '成功',
message: '创建成功',
type: 'success',
duration: 2000
})
this.getList()
})
}
})
},
handleUpdate(row) {
this.temp = Object.assign({}, row) // copy obj
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs['dataForm'].clearValidate()
})
this.dialogStatus = 'update'
// 回显
this.checkStrictly = true
const checkedNodes = []
console.log(JSON.parse(row.menuIds))
JSON.parse(row.menuIds).forEach(id => {
const path = { path: id}
checkedNodes.push(path)
})
this.$nextTick(() => {
this.$refs.tree.setCheckedNodes(checkedNodes)
this.checkStrictly = false
})
},
modify() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
const checkedKeys = this.$refs.tree.getCheckedKeys()
if (checkedKeys.length === 0){
Message({
message: '请勾选菜单',
type: 'error',
duration: 1 * 1000
})
return null
}
const tempData = Object.assign({}, this.temp)
tempData.menuIds = JSON.stringify(checkedKeys)
update(tempData).then(() => {
const index = this.list.findIndex(v => v.id === this.temp.id)
this.list.splice(index, 1, this.temp)
this.dialogFormVisible = false
this.$notify({
title: '成功',
message: '更新成功',
type: 'success',
duration: 2000
})
this.getList()
})
}
})
},
handleDelete(row, index) {
const req = { id: row.id }
this.$confirm('确定删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
del(req).then(() => {
this.$notify({
title: '成功',
message: '删除成功',
type: 'success',
duration: 2000
})
this.getList()
})
})
}
}
}
</script>
api
import request from '@/utils/request'
export function listMenuGroups() {
return request({
url: '/qjjk-api/v1/menu_group/list-all',
method: 'get'
})
}
export function pages(data) {
return request({
url: '/qjjk-api/v1/menu_group/pages',
headers: { 'Content-Type': 'application/json' },
method: 'post',
data
})
}
export function add(data) {
return request({
url: '/qjjk-api/v1/menu_group',
headers: { 'Content-Type': 'application/json' },
method: 'post',
data
})
}
export function update(data) {
return request({
url: '/qjjk-api/v1/menu_group',
headers: { 'Content-Type': 'application/json' },
method: 'put',
data
})
}
export function del(data) {
return request({
url: '/qjjk-api/v1/menu_group',
method: 'delete',
params: data
})
}