日拱一卒无有尽, 功不唐捐终入海
一、接口封装
新建 src / api / menu.ts
文件
import request from '@/utils/request'
import { IFormData } from './types/form'
import type { Menu } from './types/menu'
// 获取权限列表
export const getMenus = (params: {
is_show: 0 | 1 | ''
keyword: string
}) => {
return request<Menu[]>({
method: 'GET',
url: '/setting/menus',
params
})
}
// 添加权限
export const createMenu = (data: {
path: number[]
} & Omit<Menu, 'id' | 'children' | 'is_del' | 'path'>) => {
return request({
method: 'POST',
url: '/setting/menus',
data
})
}
// 获取添加权限规则表单
export const getMenuTree = () => {
return request<IFormData>({
method: 'GET',
url: '/setting/menus/create'
}).then(data => {
const findData = data.rules.find(item => item.field === 'menu_list')
return (findData && findData.props && findData.props.data) || []
})
}
// 修改权限规则
export const updateMenu = (id: number, data: { path: number[] } & Omit<Menu, 'id' | 'children' | 'is_del' | 'path'>) => {
return request({
method: 'PUT',
url: `/setting/menus/${id}`,
data
})
}
// 删除权限
export const deleteMenu = (id: number) => {
return request({
method: 'DELETE',
url: `/setting/menus/${id}`
})
}
// 获取单个规则
export const getMenu = (id: number) => {
return request<{
path: number[]
} & Omit<Menu, 'path'>>({
method: 'GET',
url: `/setting/menus/${id}`
})
}
// 修改状态
export const updateMenuStatus = (id: number, isShow: 0 | 1) => {
return request({
method: 'PUT',
url: `/setting/menus/show/${id}`,
data: {
is_show: isShow
}
})
}
新建 src / api / types / menu.ts
文件
export interface Menu {
id: number
pid: number
icon: string
menu_name: string
module: string
controller: string
action: string
api_url: string
methods: string
params: string
sort: number
is_show: 0 | 1
is_show_path: number
access: number
menu_path: string
path: string
auth_type: 1 | 2
header: string
is_header: number
unique_auth: string
is_del: number
statusLoading?: boolean
children: Menu[]
}
二、折叠表格插件
// 安装(右上角选择 vue3 版本(V4)
npm install xe-utils@3 vxe-table@next
全局引用:
编辑 src / main.ts
文件
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { store, key } from './store'
import elementPlus from './plugins/element-plus'
import 'xe-utils'
import VXETable from 'vxe-table'
// 全局样式
import './styles/index.scss'
import 'vxe-table/lib/style.css'
createApp(App)
.use(router)
.use(store, key)
.use(elementPlus, { size: 'small', zIndex: 2000 })
.use(VXETable)
.mount('#app')
三、规则列表
新建 src / views / setting / permission / rule / index.vue
文件
<template>
<!-- <page-container> -->
<el-card>
<template #header>
数据筛选
</template>
<el-form
:inline="true"
ref="form"
:model="listParams"
:disabled="listLoading"
@submit.prevent="handleQuery"
>
<el-form-item label="状态">
<el-select
v-model="listParams.is_show"
placeholder="请选择"
clearable
>
<el-option
label="全部"
value=""
/>
<el-option
label="显示"
:value="1"
/>
<el-option
label="不显示"
:value="0"
/>
</el-select>
</el-form-item>
<el-form-item label="规则名称">
<el-input
v-model="listParams.keyword"
clearable
placeholder="请输入规则名称"
/>
</el-form-item>
<el-form-item>
<el-button native-type="submit">
查询
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card>
<template #header>
<el-button
type="primary"
@click="formVisible = true"
>
添加规则
</el-button>
</template>
<!--
启用树菜单:
1. data 数据需要是树结构
2. 给 vxe-table 组件设置 row-id
3. 给 vxe-column 设置 tree-node
-->
<vxe-table
:data="list"
row-id="id"
:tree-config="{ children: 'children' }"
v-loading="listLoading"
>
<vxe-column
field="id"
title="ID"
/>
<vxe-column
field="menu_name"
title="名称"
tree-node
/>
<vxe-column
title="接口路径"
>
<template #default="{ row }">
{{ row.api_url ? `[${row.methods}] ${row.api_url}` : '' }}
</template>
</vxe-column>
<vxe-column
field="unique_auth"
title="前端权限"
/>
<vxe-column
field="menu_path"
title="页面路由"
/>
<vxe-column title="状态">
<template #default="{ row }">
<el-switch
v-model="row.is_show"
active-color="#13ce66"
inactive-color="#ff4949"
:active-value="1"
:inactive-value="0"
:loading="row.statusLoading"
@change="handleStatusChange(row)"
/>
</template>
</vxe-column>
<vxe-column
title="操作"
min-width="100"
>
<template #default="scope">
<el-button
type="text"
@click="handleCreate(scope.row.id)"
>
添加规则
</el-button>
<el-button
type="text"
@click="handleUpdate(scope.row.id)"
>
编辑
</el-button>
<el-popconfirm
title="确认删除吗?"
@confirm="handleDelete(scope.row.id)"
>
<template #reference>
<el-button type="text">
删除
</el-button>
</template>
</el-popconfirm>
</template>
</vxe-column>
</vxe-table>
</el-card>
<!-- </page-container> -->
<rule-form
v-model="formVisible"
v-model:rule-id="ruleId"
v-model:pid="pid"
@success="handleFormSuccess"
/>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import { getMenus, deleteMenu, updateMenuStatus } from '@/api/menu'
import type { Menu } from '@/api/types/menu'
import { ElMessage } from 'element-plus'
import RuleForm from './RuleForm.vue'
const list = ref<Menu[]>([]) // 列表数据
const listLoading = ref(true)
const listParams = reactive({ // 列表数据查询参数
keyword: '',
is_show: '' as 0 | 1 | ''
})
const formVisible = ref(false)
const ruleId = ref<number | null>(null)
const pid = ref<number | null>(null)
onMounted(() => {
loadList()
})
const loadList = async () => {
listLoading.value = true
const data = await getMenus(listParams).finally(() => {
listLoading.value = false
})
data.forEach(item => {
item.statusLoading = false // 控制切换状态的 loading 效果
})
list.value = data
}
const handleQuery = async () => {
loadList()
}
const handleDelete = async (id: number) => {
await deleteMenu(id)
ElMessage.success('删除成功')
loadList()
}
const handleStatusChange = async (item: Menu) => {
item.statusLoading = true
await updateMenuStatus(item.id, item.is_show).finally(() => {
item.statusLoading = false
})
ElMessage.success(`${item.is_show === 1 ? '启用' : '禁用'}成功`)
}
const handleUpdate = (id: number) => {
ruleId.value = id
formVisible.value = true
}
const handleFormSuccess = () => {
formVisible.value = false
loadList()
}
const handleCreate = (id: number) => {
pid.value = id
formVisible.value = true
}
</script>
<style lang="scss" scoped></style>
四、新建 / 编辑 弹窗
新建 src / views / setting / permission / rule / RuleForm.vue
文件
<template>
<!-- :confirm="handleSubmit" -->
<el-dialog
ref="dialog"
:title="props.ruleId ? '编辑规则' : '添加规则'"
width="60%"
append-to-body
@closed="handleDialogClosed"
@open="handleDialogOpen"
>
<el-form
label-width="110px"
v-loading="formLoading"
:model="formData"
:rules="formRules"
ref="form"
:validate-on-rule-change="false"
>
<el-row>
<el-col :span="24">
<el-form-item label="类型">
<el-radio-group v-model="formData.auth_type">
<el-radio :label="2">
接口
</el-radio>
<el-radio :label="1">
菜单(只显示三级)
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item
label="名称"
prop="menu_name"
>
<el-input v-model="formData.menu_name" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="父级分类"
prop="path"
>
<el-cascader
v-model="formData.path"
:options="menus"
clearable
:props="{ checkStrictly: true }"
@change="handleChange"
/>
</el-form-item>
</el-col>
<template v-if="formData.auth_type === 2">
<el-col :span="12">
<el-form-item
label="请求方式"
prop="methods"
>
<el-select v-model="formData.methods">
<el-option
v-for="item in methods"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="接口地址"
prop="api_url"
>
<el-input v-model="formData.api_url" />
</el-form-item>
</el-col>
</template>
<template v-else>
<el-col :span="12">
<el-form-item
label="接口参数"
prop="params"
>
<el-input v-model="formData.params" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="路由路径"
prop="menu_path"
>
<el-input v-model="formData.menu_path" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="图标"
prop="icon"
>
<el-input v-model="formData.icon" />
</el-form-item>
</el-col>
</template>
<el-col :span="12">
<el-form-item
label="权限标识"
prop="unique_auth"
>
<el-input v-model="formData.unique_auth" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="排序"
prop="sort"
>
<el-input v-model.number="formData.sort" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="状态"
prop="is_show"
>
<el-radio-group v-model="formData.is_show">
<el-radio :label="0">
关闭
</el-radio>
<el-radio :label="1">
开启
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="是否展示"
prop="is_show_path"
>
<el-radio-group v-model="formData.is_show_path">
<el-radio :label="0">
否
</el-radio>
<el-radio :label="1">
是
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button
type="primary"
@click="handleSubmit"
>确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import * as menuApi from '@/api/menu'
import { ElMessage } from 'element-plus'
import type { Menu } from '@/api/types/menu'
import type { PropType } from 'vue'
import type { IElForm, IElDialog } from '@/types/element-plus'
const props = defineProps({
ruleId: {
type: Number as PropType<number | null>,
default: null
},
pid: {
type: Number as PropType<number | null>,
default: null
}
})
const emit = defineEmits(['success', 'update:rule-id', 'update:pid'])
const form = ref<IElForm | null>(null)
const formData = ref<{ path: number[] } & Omit<Menu, 'id' | 'children' | 'is_del' | 'path'>>({
auth_type: 1,
menu_name: '',
pid: 0,
params: '',
controller: '',
module: '',
action: '',
icon: '',
path: [],
menu_path: '',
api_url: '',
methods: '',
unique_auth: '',
header: '',
is_header: 0,
sort: 0,
access: 0,
is_show: 0,
is_show_path: 1
})
const menuRules = {
menu_path: [
{ message: '请输入路由路径', required: true, trigger: 'change' }
],
unique_auth: [
{ message: '请输入权限标识', required: true, trigger: 'change' }
]
}
const apiRules = {
methods: [
{ message: '请选择请求方式', required: true, trigger: 'change' }
],
api_url: [
{ message: '请输入接口地址', required: true, trigger: 'change' }
]
}
const commonRules = {
menu_name: [
{ message: '请输入按钮名称', required: true, trigger: 'change' }
]
}
const formRules = computed(() => {
// 清除验证结果
form.value?.clearValidate()
return formData.value.auth_type === 1
? { ...menuRules, ...commonRules }
: { ...apiRules, ...commonRules }
})
const menus = ref<Menu[]>([])
const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].map(item => ({
label: item,
value: item
}))
const formLoading = ref(false)
const handleDialogOpen = async () => {
formLoading.value = true
await Promise.all([
loadMenus(),
loadMenu(),
setDefaultMenuPath()
]).finally(() => {
formLoading.value = false
})
}
const loadMenus = async () => {
const data = await menuApi.getMenuTree()
menus.value = data
}
const setDefaultMenuPath = async () => {
if (props.pid) {
const menu = await menuApi.getMenu(props.pid)
formData.value.pid = props.pid
formData.value.path = [...menu.path, props.pid]
}
}
const loadMenu = async () => {
if (!props.ruleId) {
return
}
const data = await menuApi.getMenu(props.ruleId)
data.path = [
...data.path,
data.id
]
formData.value = data
}
const handleChange = (value: any) => {
formData.value.pid = value[value.length - 1]
}
const handleSubmit = async () => { // 确认
const valid = await form.value?.validate()
if (!valid) return
if (props.ruleId) {
await menuApi.updateMenu(props.ruleId, formData.value)
} else {
await menuApi.createMenu(formData.value)
}
ElMessage.success('保存成功')
emit('success')
}
const dialog = ref<IElDialog | null>(null)
const handleCancel = () => { // 取消
if (dialog.value) {
dialog.value.visible = false
}
}
const handleDialogClosed = () => {
emit('update:rule-id', null)
emit('update:pid', null)
form.value?.clearValidate() // 清除验证结果
form.value?.resetFields() // 清除表单数据
}
</script>
<style lang="scss" scoped></style>