添加菜单 权限 等页面

This commit is contained in:
浅念
2025-08-13 19:27:49 +08:00
parent e797cb24de
commit f5deb7b8f7
15 changed files with 2297 additions and 20 deletions
+696
View File
@@ -0,0 +1,696 @@
<template>
<ArtTableFullScreen>
<div class="menu-page" id="table-full-screen">
<!-- 搜索栏 -->
<ArtSearchBar
v-model:filter="formFilters"
:items="formItems"
:showExpand="false"
@reset="handleReset"
@search="handleSearch"
></ArtSearchBar>
<ElCard shadow="never" class="art-table-card">
<!-- 表格头部 -->
<ArtTableHeader :showZebra="false" v-model:columns="columnChecks" @refresh="handleRefresh">
<template #left>
<!-- 按钮权限:后端控制模式,使用自定义指令 -->
<ElButton v-auth="'system@menu_add'" @click="showModel('menu', null)" v-ripple>
添加菜单
</ElButton>
<ElButton @click="toggleExpand" v-ripple>
{{ isExpanded ? '收起' : '展开' }}
</ElButton>
<!-- 按钮权限:前端控制模式,使用 hasAuth 方法 -->
<!-- <ElButton v-if="hasAuth('B_CODE1')" @click="showModel('menu', null, true)" v-ripple>
添加菜单
</ElButton> -->
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
rowKey="path"
ref="tableRef"
:loading="loading"
:data="filteredTableData"
:marginTop="10"
:stripe="false"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
<ElDialog :title="dialogTitle" v-model="dialogVisible" width="700px" align-center>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="85px">
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="资源类型" prop="type">
<el-select v-model="form.type">
<el-option label="目录" value="directory" />
<el-option label="菜单" value="menu" />
<el-option label="权限" value="permission" />
</el-select>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="资源名称" prop="name">
<ElInput v-model="form.name" placeholder="资源名称"></ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="权限标识" prop="code">
<ElInput v-model="form.code" placeholder="权限标识"></ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="图标" prop="icon">
<ArtIconSelector v-model="form.icon" :iconType="iconType" width="100%" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="form.type !== 'permission'">
<ElCol :span="12" v-if="form.type !== 'permission'">
<ElFormItem label="路由地址" prop="url">
<ElInput v-model="form.url" placeholder="路由地址"></ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="打开方式" prop="open_type">
<ElSelect v-model="form.open_type" class="account-select">
<el-option label="内部" value="internal" />
<el-option label="外部" value="external" />
<el-option label="嵌入" value="iframe" />
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="form.type !== 'permission'">
<ElCol :span="12">
<ElFormItem label="资源排序" prop="sort" style="width: 100%">
<ElInputNumber
v-model="form.sort"
style="width: 100%"
@change="handleChange"
:min="1"
controls-position="right"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="外部链接" prop="link">
<ElInput
v-model="form.link"
placeholder="外部链接/内嵌地址(https://www.baidu.com)"
></ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20">
<ElCol :span="12">
<ElFormItem label="系统" prop="system_id">
<ElSelect v-model="form.system_id" class="account-select">
<ElOption
v-for="account in systemsList"
:key="account.id"
:label="account.name"
:value="account.id"
>
<span>{{ account.name }}</span>
</ElOption>
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="父级" prop="parent_id">
<el-tree-select
v-model="form.parent_id"
:data="parentList"
:props="{
value: 'id',
label: 'name',
children: 'children'
}"
check-strictly
:render-after-expand="false"
style="width: 240px"
/>
<!-- <ElSelect v-model="form.parent_id" class="account-select">
<ElOption
v-for="account in parentList"
:key="account.id"
:label="account.name"
:value="account.id"
>
<span>{{ account.name }}</span>
</ElOption>
</ElSelect> -->
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="20" v-if="form.type !== 'permission'">
<ElCol :span="5">
<ElFormItem label="是否启用" prop="status">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0"></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="页面缓存" prop="keep_alive">
<ElSwitch
v-model="form.keep_alive"
:active-value="1"
:inactive-value="0"
></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="是否隐藏" prop="is_hide">
<ElSwitch v-model="form.is_hide" :active-value="1" :inactive-value="0"></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="标签页是否隐藏" prop="is_hide_tab" label-width="120px">
<ElSwitch
v-model="form.is_hide_tab"
:active-value="1"
:inactive-value="0"
></ElSwitch>
</ElFormItem>
</ElCol>
<!-- <ElCol :span="5">
<ElFormItem label="是否内嵌" prop="isMenu">
<ElSwitch v-model="form.isIframe"></ElSwitch>
</ElFormItem>
</ElCol> -->
</ElRow>
<ElRow :gutter="20" v-if="form.type !== 'permission'">
<ElCol :span="5">
<ElFormItem label="是否首页" prop="is_home">
<ElSwitch v-model="form.is_home" :active-value="1" :inactive-value="0"></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="10">
<ElFormItem label="不需要认证" prop="no_auth" label-width="120px">
<ElSwitch v-model="form.no_auth" :active-value="1" :inactive-value="0"></ElSwitch>
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
<template #footer>
<span class="dialog-footer">
<ElButton @click="dialogVisible = false">取 消</ElButton>
<ElButton type="primary" @click="submitForm()">确 定</ElButton>
</span>
</template>
</ElDialog>
</ElCard>
</div>
</ArtTableFullScreen>
</template>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { IconTypeEnum } from '@/enums/appEnum'
import { formatMenuTitle } from '@/router/utils/utils'
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
import { useCheckedColumns } from '@/composables/useCheckedColumns'
import { ElPopover, ElButton } from 'element-plus'
import { AppRouteRecord } from '@/types/router'
import { useAuth } from '@/composables/useAuth'
import { SearchFormItem } from '@/types'
import { systemService } from '@/api/systemApi'
import { UserService } from '@/api/usersApi'
defineOptions({ name: 'Menus' })
const { hasAuth } = useAuth()
// const { menuList } = storeToRefs(useMenuStore())
const menuList = ref<any[]>([])
const loading = ref(false)
//系统
const systemsList = ref<any>()
const getsystemsList = async () => {
const response = await UserService.getsystemsList()
systemsList.value = response || []
}
//父级
const parentList = ref<any>()
const getparentList = async () => {
const response = await systemService.getMenuList()
parentList.value = response || []
}
// 定义表单搜索初始值
const initialSearchState = {
name: '',
route: ''
}
// 响应式表单数据
const formFilters = reactive({ ...initialSearchState })
// 增加实际应用的搜索条件状态
const appliedFilters = reactive({ ...initialSearchState })
// 重置表单
const handleReset = () => {
Object.assign(formFilters, { ...initialSearchState })
Object.assign(appliedFilters, { ...initialSearchState })
getTableData()
}
// 搜索处理
const handleSearch = () => {
// 将当前输入的筛选条件应用到实际搜索
Object.assign(appliedFilters, { ...formFilters })
getTableData()
}
// 表单配置项
const formItems: SearchFormItem[] = [
{
label: '菜单名称',
prop: 'name',
type: 'input',
config: {
clearable: true
}
},
{
label: '路由地址',
prop: 'route',
type: 'input',
config: {
clearable: true
}
}
]
// 构建菜单类型标签
const buildMenuTypeTag = (row: AppRouteRecord) => {
if (row.children && row.children.length > 0) {
return 'info'
} else if (row.meta?.link && row.meta?.isIframe) {
return 'success'
} else if (row.path) {
return 'primary'
} else if (row.meta?.link) {
return 'warning'
}
}
// 构建菜单类型文本
const buildMenuTypeText = (row: AppRouteRecord) => {
if (row.children && row.children.length > 0) {
return '目录'
} else if (row.meta?.link && row.meta?.isIframe) {
return '内嵌'
} else if (row.path) {
return '菜单'
} else if (row.meta?.link) {
return '外链'
}
}
// 动态列配置
const { columnChecks, columns } = useCheckedColumns(() => [
{
prop: 'meta.title',
label: '菜单名称',
minWidth: 120,
formatter: (row: AppRouteRecord) => {
return formatMenuTitle(row.meta?.title)
}
},
{
prop: 'type',
label: '菜单类型',
formatter: (row: AppRouteRecord) => {
return h(ElTag, { type: buildMenuTypeTag(row) }, () => buildMenuTypeText(row))
}
},
{
prop: 'path',
label: '路由',
formatter: (row: AppRouteRecord) => {
return row.meta?.link || row.path || ''
}
},
{
prop: 'meta.authList',
label: '可操作权限',
formatter: (row: AppRouteRecord) => {
return h(
'div',
{},
row.meta.authList?.map((item: { title: string; auth_mark: string }, index: number) => {
return h(
ElPopover,
{
placement: 'top-start',
title: '操作',
width: 200,
trigger: 'click',
key: index
},
{
default: () =>
h('div', { style: 'margin: 0; text-align: right' }, [
h(
ElButton,
{
size: 'small',
type: 'primary',
onClick: () => showModel('button', item, false, row)
},
{ default: () => '编辑' }
),
h(
ElButton,
{
size: 'small',
type: 'danger',
onClick: () => deleteAuth(item)
},
{ default: () => '删除' }
)
]),
reference: () => h(ElButton, { class: 'small-btn' }, { default: () => item.title })
}
)
})
)
}
},
{
prop: 'date',
label: '编辑时间',
formatter: () => '2022-3-12 12:00:00'
},
{
prop: 'status',
label: '隐藏菜单',
formatter: (row) => {
return h(ElTag, { type: row.meta.isHide ? 'danger' : 'info' }, () =>
row.meta.isHide ? '是' : '否'
)
}
},
{
prop: 'operation',
label: '操作',
width: 180,
formatter: (row: AppRouteRecord) => {
return h('div', [
// 后端模式权限标识
hasAuth('system@menu_add') &&
h(ArtButtonTable, {
type: 'add',
onClick: () => showModel('menu', row, true)
}),
hasAuth('system@menu_update') &&
h(ArtButtonTable, {
type: 'edit',
onClick: () => showDialog('edit', row)
}),
hasAuth('system@menu_destroy') &&
h(ArtButtonTable, {
type: 'delete',
onClick: () => deleteMenu(row)
})
])
}
}
])
const handleRefresh = () => {
getTableData()
}
const dialogVisible = ref(false)
const form = reactive({
// 菜单
system_id: '',
type: 'menu', // 资源类型
parent_id: '',
name: '',
url: '',
code: '',
icon: '',
status: 1,
sort: 1,
is_home: 0,
keep_alive: 1,
is_hide: 0,
is_hide_tab: 0,
no_auth: 0,
link: '',
open_type: 'internal'
})
const iconType = ref(IconTypeEnum.UNICODE)
// const labelPosition = ref('menu')
const rules = reactive<FormRules>({
name: [
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
system_id: [{ required: true, message: '请选择系统', trigger: 'blur' }],
type: [{ required: true, message: '请选择资源类型', trigger: 'blur' }],
code: [{ required: true, message: '输入权限标识', trigger: 'blur' }]
})
const tableData = ref<AppRouteRecord[]>([])
onMounted(() => {
getTableData()
})
const getTableData = async () => {
loading.value = true
// const { res } = await systemService.getMenutreeList()
await systemService.getMenutreeList().then((res) => {
console.log(res)
menuList.value = res
})
// console.log(res)
// menuList.value = data
// 基于 appliedFilters 在这里进行过滤,并将结果赋值给 tableData
const searchName = appliedFilters.name?.toLowerCase().trim() || ''
const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
const filterMenus = (items: AppRouteRecord[]): AppRouteRecord[] => {
return items.reduce((acc: AppRouteRecord[], item: AppRouteRecord) => {
// 浅拷贝节点,避免直接修改原数据
const copy: AppRouteRecord = { ...item }
if (copy.children && copy.children.length > 0) {
copy.children = filterMenus(copy.children)
}
const menuTitle = formatMenuTitle(copy.meta?.title || '').toLowerCase()
const menuPath = (copy.path || '').toLowerCase()
const nameMatch = !searchName || menuTitle.includes(searchName)
const routeMatch = !searchRoute || menuPath.includes(searchRoute)
const hasChildren = Array.isArray(copy.children) && copy.children.length > 0
if (hasChildren || (nameMatch && routeMatch)) {
acc.push(copy)
}
return acc
}, [])
}
tableData.value = filterMenus(menuList.value)
loading.value = false
}
// 过滤后的表格数据改为来源于 getTableData 的结果
const filteredTableData = computed(() => tableData.value)
const isEdit = ref(false)
const formRef = ref<FormInstance>()
const dialogTitle = computed(() => {
// const type = labelPosition.value === 'menu' ? '菜单' : '权限'
return isEdit.value ? `编辑资源` : `新建资源`
})
const showDialog = (type: string, row: AppRouteRecord) => {
showModel('menu', row)
}
const handleChange = () => {}
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
try {
if (isEdit.value) {
const payload = { ...form, id: itemid.value }
await systemService.menuEdit(payload as any)
} else {
await systemService.menuAdd(form)
}
ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功`)
dialogVisible.value = false
// 刷新表格数据
getTableData()
} catch {
ElMessage.error(`${isEdit.value ? '编辑' : '新增'}失败`)
}
}
})
}
const itemid = ref('')
const showModel = (type: string, row?: any, padd: boolean = false, data?: any) => {
getsystemsList()
getparentList()
dialogVisible.value = true
// labelPosition.value = type
isEdit.value = false
// lockMenuType.value = lock
resetForm()
itemid.value = ''
if (data) {
isEdit.value = true
form.type = 'permission'
form.system_id = data.meta.systemId
form.parent_id = data.id
form.name = row.title
form.code = row.authMark
itemid.value = row.id
} else if (padd) {
nextTick(() => {
form.parent_id = row.id
form.system_id = row.meta.systemId
})
} else if (row && !padd) {
isEdit.value = true
nextTick(() => {
// 回显数据
itemid.value = row.id
form.system_id = row.meta.systemId
form.parent_id = row.meta.parentId == 0 ? '' : row.meta.parentId
form.name = row.name
form.type = row.meta.type
form.url = row.path
form.code = row.meta.code
form.icon = row.meta.icon
form.sort = row.meta.sort
form.is_home = row.meta.isHome
form.keep_alive = row.meta.keepAlive
form.is_hide = row.meta.isHide
form.is_hide_tab = row.meta.isHideTab
form.no_auth = row.meta.noAuth
form.status = row.meta.status
form.link = row.meta.link
form.open_type = row.meta.openType
})
}
}
const resetForm = () => {
formRef.value?.resetFields()
Object.assign(form, {
// 菜单
system_id: '',
type: 'menu', // 资源类型
parent_id: '',
name: '',
url: '',
code: '',
icon: '',
status: 1,
sort: 1,
is_home: 0,
keep_alive: 1,
is_hide: 0,
is_hide_tab: 0,
no_auth: 0,
link: '',
open_type: 'internal'
})
}
const deleteMenu = async (item: any) => {
try {
await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const res = await systemService.menuDelete({ id: item.id })
ElMessage.success(res?.message || '删除成功!')
// 刷新表格数据
getTableData()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
const deleteAuth = async (item: any) => {
try {
await ElMessageBox.confirm('确定要删除该权限吗?删除后无法恢复', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const res = await systemService.menuDelete({ id: item.id })
ElMessage.success(res?.message || '删除成功!')
// 刷新表格数据
getTableData()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
// 添加一个控制变量
// const lockMenuType = ref(false)
const isExpanded = ref(false)
const tableRef = ref()
const toggleExpand = () => {
isExpanded.value = !isExpanded.value
nextTick(() => {
if (tableRef.value) {
tableRef.value[isExpanded.value ? 'expandAll' : 'collapseAll']()
}
})
}
</script>
<style lang="scss" scoped>
.menu-page {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
:deep(.small-btn) {
height: 22px !important;
padding: 0 5px !important;
font-size: 10px !important;
margin-bottom: 5px;
}
:deep(.el-dialog__footer) {
z-index: inherit;
.el-button > span {
z-index: inherit;
}
}
}
</style>