Files
art-design/src/views/admin/system/menu/index.vue
T
2025-09-16 15:57:17 +08:00

774 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>
<el-segmented
v-model="yearVal"
size="large"
:options="segmentedOptions"
@change="searchArticleByYear"
>
<!-- <template #default="scope">
<div class="flex flex-col items-center gap-2 p-2">
<div>{{ scope.item.label }}</div>
</div>
</template> -->
</el-segmented>
<!-- 按钮权限:后端控制模式,使用自定义指令 -->
<!-- <ElButton
v-auth="'system@menu_add'"
@click="showModel('menu', null)"
v-ripple
style="margin-left: 20px"
>
添加菜单
</ElButton>
<ElButton @click="toggleExpand" v-ripple>
{{ isExpanded ? '收起' : '展开' }}
</ElButton> -->
<!-- 按钮权限:前端控制模式,使用 hasAuth 方法 -->
<!-- <ElButton v-if="hasAuth('B_CODE1')" @click="showModel('menu', null, true)" v-ripple>
添加菜单
</ElButton> -->
</template>
<template #right>
<div class="btnadds" @click="showModel('menu', null)" v-auth="'system@menu_add'">
<i class="iconfont-sys">&#xe83e;</i>
</div>
</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="true"
:inactive-value="false"
></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="是否隐藏" prop="is_hide">
<ElSwitch
v-model="form.is_hide"
:active-value="true"
:inactive-value="false"
></ElSwitch>
</ElFormItem>
</ElCol>
<ElCol :span="5">
<ElFormItem label="标签页是否隐藏" prop="is_hide_tab" label-width="120px">
<ElSwitch
v-model="form.is_hide_tab"
:active-value="true"
:inactive-value="false"
></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'
const yearVal = ref<any>('')
defineOptions({ name: 'Menus' })
const { hasAuth } = useAuth()
// const { menuList } = storeToRefs(useMenuStore())
const menuList = ref<any[]>([])
const loading = ref(false)
const systemsListtab = ref<any>([])
const segmentedOptions = computed(() =>
Array.isArray(systemsListtab.value)
? systemsListtab.value.map((item: any) => ({ label: item.name, value: item.id }))
: []
)
const getsystemsListtab = async () => {
const response = await UserService.getsystemsList()
const list = (response as any)?.data?.records ?? (response as any)?.records ?? response ?? []
systemsListtab.value = Array.isArray(list) ? list : []
if (!yearVal.value && systemsListtab.value.length > 0) {
yearVal.value = systemsListtab.value[0].id
}
}
//系统
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' }
],
type: [{ required: true, message: '请选择资源类型', trigger: 'blur' }],
code: [{ required: true, message: '输入权限标识', trigger: 'blur' }]
})
const tableData = ref<AppRouteRecord[]>([])
onMounted(() => {
getsystemsListtab()
getTableData()
})
const searchArticleByYear = () => {
getTableData()
}
// const handleSegmentedClick = (item: any) => {
// // 若点击的是当前项也强制刷新
// yearVal.value = item?.value ?? yearVal.value
// getTableData()
// }
const getTableData = async () => {
loading.value = true
// 清空旧数据,避免视觉上出现追加
menuList.value = []
tableData.value = []
// 并发安全:本次请求标识
requestCounter += 1
const reqId = requestCounter
const params: any = {
system_id: yearVal.value || '1'
}
try {
const res = await systemService.getMenutreeList(params)
// 若在等待过程中又发起了新请求,则丢弃本次结果
if (reqId !== requestCounter) return
menuList.value = Array.isArray(res) ? res : []
} catch (e) {
console.error(e)
menuList.value = []
}
// 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()
// 数据请求并发保护计数器
let requestCounter = 0
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;
}
}
}
.btnadds {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
margin-left: 10px;
color: var(--art-gray-700);
cursor: pointer;
background-color: rgba(var(--art-gray-200-rgb), 0.8);
border-radius: 6px;
transition: color 0.3s;
transition: all 0.3s;
}
</style>