Files
2026-07-15 16:13:27 +08:00

326 lines
9.6 KiB
Vue
Raw Permalink 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>
<div class="form-page">
<!-- 页面头部 -->
<el-page-header @back="handleBack" :title="isEdit ? '编辑菜单' : '新增菜单'" />
<el-divider />
<!-- 表单卡片 -->
<el-card class="form-card" shadow="never">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<!-- 基本信息 -->
<el-divider content-position="left">
<span class="form-section-title">基本信息</span>
</el-divider>
<!-- 所属系统新增时可切换编辑时展示不可修改 -->
<el-form-item label="所属系统" prop="systemId">
<el-select
v-model="form.systemId"
placeholder="请选择系统"
:disabled="isEdit"
style="width: 100%"
@change="onSystemChange"
>
<el-option
v-for="sys in systemList"
:key="sys.systemId"
:label="sys.systemName"
:value="sys.systemId"
/>
</el-select>
</el-form-item>
<el-form-item label="上级菜单">
<el-tree-select
v-model="form.parentId"
:data="parentTreeData"
:props="{ value: 'id', label: 'menuName', children: 'children' }"
node-key="id"
placeholder="请选择上级菜单(留空为顶级)"
clearable
check-strictly
style="width: 100%"
/>
</el-form-item>
<el-form-item label="菜单名称" prop="menuName">
<el-input v-model="form.menuName" placeholder="请输入菜单名称" />
</el-form-item>
<el-form-item label="菜单类型" prop="menuType">
<el-radio-group v-model="form.menuType">
<el-radio-button :value="1">目录</el-radio-button>
<el-radio-button :value="2">菜单</el-radio-button>
<el-radio-button :value="3">按钮</el-radio-button>
</el-radio-group>
</el-form-item>
<!-- 按钮类型只需权限标识 -->
<template v-if="form.menuType === 3">
<el-divider content-position="left">
<span class="form-section-title">按钮权限</span>
</el-divider>
<el-form-item label="权限标识" prop="permission">
<el-input v-model="form.permission" placeholder="如: system:user:add" />
</el-form-item>
</template>
<!-- 目录/菜单类型显示路由和图标 -->
<template v-else>
<el-divider content-position="left">
<span class="form-section-title">其他信息</span>
</el-divider>
<el-form-item label="路由路径" :prop="form.menuType !== 1 ? 'menuPath' : undefined">
<el-input v-model="form.menuPath" placeholder="请输入路由路径" />
</el-form-item>
<el-form-item label="组件路径" v-if="form.menuType === 2">
<el-input v-model="form.component" placeholder="请输入组件路径" />
</el-form-item>
<el-form-item label="图标">
<IconPicker v-model="form.icon" />
</el-form-item>
<el-form-item label="权限标识" v-if="form.menuType === 2">
<el-input v-model="form.permission" placeholder="可选,按钮权限标识" />
</el-form-item>
</template>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁用" />
</el-form-item>
<!-- 按钮区域 -->
<el-divider />
<div class="form-footer">
<el-button @click="handleBack">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</div>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import request from '@/utils/request'
import IconPicker from '@/components/IconPicker.vue'
interface SystemItem {
id: string
systemId: string
systemName: string
}
const iconList = [
'HomeFilled','Monitor','User','UserFilled','Document','Notebook','Tickets',
'Setting','Menu','List','Link','Collection','Avatar','DataLine','Connection',
'Switch','SwitchButton','ArrowRight','ArrowDown','Search','Plus','Edit',
'Delete','Check','Close','CircleClose','Remove','Expand','Fold',
'Key','Lock','InfoFilled','CircleCheckFilled',
]
interface MenuNode {
id: string
menuName: string
children?: MenuNode[]
}
const route = useRoute()
const router = useRouter()
const isEdit = ref(false)
const editId = ref<string>()
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
/** 系统列表 */
const systemList = ref<SystemItem[]>([])
/** 上级菜单树 */
const parentTreeData = ref<MenuNode[]>([])
/** 表单数据 */
const form = reactive({
systemId: undefined as string | undefined,
parentId: null as string | null,
menuName: '',
menuPath: '',
component: '',
icon: '',
menuType: 2,
permission: '',
sortOrder: 0,
status: 1,
})
/** 校验规则(按钮类型需要权限标识,目录/菜单需要路由路径) */
const rules = computed<FormRules>(() => ({
systemId: [{ required: true, message: '请选择所属系统', trigger: 'change' }],
menuName: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
permission: form.menuType === 3
? [{ required: true, message: '请输入权限标识', trigger: 'blur' }]
: [],
menuPath: form.menuType === 2
? [{ required: true, message: '请输入路由路径', trigger: 'blur' }]
: [],
menuType: [{ required: true, message: '请选择菜单类型', trigger: 'change' }],
sortOrder: [{ required: true, message: '请输入排序', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
}))
/** 加载系统列表 */
async function fetchSystemList() {
try {
const res = await request.get('/zgapi/v1/auth/system/list')
systemList.value = res.data ?? []
} catch {
systemList.value = []
}
}
/** 加载菜单树(用于上级菜单选择) */
async function loadMenuTree(systemId: string) {
if (!systemId) {
parentTreeData.value = []
return
}
try {
const res = await request.get('/zgapi/v1/admin/menu/tree', { params: { systemId } })
parentTreeData.value = res.data ?? []
} catch {
parentTreeData.value = []
}
}
/** 切换系统时重新加载菜单树 */
function onSystemChange(systemId: string | undefined) {
form.parentId = null
if (systemId) {
loadMenuTree(systemId)
} else {
parentTreeData.value = []
}
}
/** 加载已有数据(编辑模式) */
async function loadData() {
const id = route.params.id as string
if (!id) {
// 新增模式,从 query 读取预设参数
isEdit.value = false
const querySystemId = route.query.systemId as string | undefined
const queryParentId = route.query.parentId as string | undefined
if (querySystemId) {
form.systemId = querySystemId
loadMenuTree(form.systemId)
}
if (queryParentId) {
form.parentId = queryParentId
}
return
}
isEdit.value = true
editId.value = id
try {
const res = await request.get(`/zgapi/v1/admin/menu/${editId.value}`)
const data = res.data
if (data) {
form.systemId = data.systemId
// 先加载菜单树,再赋值其他字段(确保 tree-select 数据就绪)
if (data.systemId) {
await loadMenuTree(data.systemId)
}
Object.assign(form, {
parentId: data.parentId ?? null,
menuName: data.menuName ?? '',
menuPath: data.menuPath ?? '',
component: data.component ?? '',
icon: data.icon ?? '',
menuType: data.menuType ?? 2,
permission: data.permission ?? '',
sortOrder: data.sortOrder ?? 0,
status: data.status ?? 1,
})
setTimeout(() => formRef.value?.clearValidate(), 0)
}
} catch {
ElMessage.error('加载菜单信息失败')
router.back()
}
}
function handleBack() {
router.back()
}
async function handleSubmit() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitLoading.value = true
try {
if (isEdit.value) {
await request.put(`/zgapi/v1/admin/menu/${editId.value}`, form)
ElMessage.success('编辑成功')
} else {
await request.post('/zgapi/v1/admin/menu', form)
ElMessage.success('新增成功')
}
router.back()
} catch {
// 错误由拦截器处理
} finally {
submitLoading.value = false
}
}
onMounted(async () => {
await fetchSystemList()
await loadData()
setTimeout(() => formRef.value?.clearValidate(), 0)
})
</script>
<style scoped>
.form-page {
display: flex;
flex-direction: column;
gap: 0;
}
:deep(.el-page-header) {
margin-bottom: 4px;
}
:deep(.el-page-header + .el-divider) {
margin: 0 0 20px 0;
}
.form-card {
max-width: 700px;
margin: 0 auto;
width: 100%;
}
.form-section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.form-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
@media (max-width: 768px) {
.form-card { max-width: 100%; }
.form-footer { flex-direction: column; }
.form-footer .el-button { width: 100%; }
:deep(.el-form-item__label) { width: 70px !important; }
}
</style>