This commit is contained in:
李远 2026-03-11 09:54:52 +08:00
parent a2afd0a787
commit 7e8d963095
4 changed files with 48 additions and 51 deletions

View File

@ -6,12 +6,11 @@ export interface MenuFormParams {
path: string; // 前端路由 path: string; // 前端路由
api: string; // 后端接口路由 api: string; // 后端接口路由
icon: string; // 菜单图标 icon: string; // 菜单图标
parentId: number | string; // 上级菜单IDnumber类型
sort: number; // 排序number类型
type: number; // 类型1菜单2按钮number类型
status?: number; // 状态1启用2禁用可选编辑时用
id?: number; // 菜单ID仅编辑/删除时传)
parent_id?: string | null; // 上级菜单IDnumber类型 parent_id?: string | null; // 上级菜单IDnumber类型
sort: number | null; // 排序number类型
type: number | null; // 类型1菜单2按钮number类型
status?: number | null; // 状态1启用2禁用可选编辑时用
id?: number | null; // 菜单ID仅编辑/删除时传)
} }
/** 删除菜单参数类型 */ /** 删除菜单参数类型 */

View File

@ -33,8 +33,8 @@
</MenuTable> </MenuTable>
</div> </div>
<!-- 菜单新增/编辑弹窗 --> <!-- 菜单新增/编辑弹窗 -->
<MenuDialog :visible="dialogVisible" :title="dialogTitle" :type="dialogType" @confirm="handleSubmit" @cancel="handleCancel" <MenuDialog :visible="dialogVisible" :title="dialogTitle" :type="dialogType" @confirm="handleSubmit"
@close="handleClose"> @cancel="handleCancel" @close="handleClose">
<MenuForm :form-data="menuForm" :form-rules="menuFormRules" :form-items="MenuFormItemOptions" <MenuForm :form-data="menuForm" :form-rules="menuFormRules" :form-items="MenuFormItemOptions"
:options-map="menuFormOptionsMap" ref="menuFormRef" /> :options-map="menuFormOptionsMap" ref="menuFormRef" />
</MenuDialog> </MenuDialog>
@ -50,7 +50,7 @@ import MenuForm from '@/components/common/Form.vue'
import { MenuTableColumns, MenuiconOptions, menuiconMap, MenuFormItemOptions } from './options' import { MenuTableColumns, MenuiconOptions, menuiconMap, MenuFormItemOptions } from './options'
import { rules } from './rules' import { rules } from './rules'
import { getMenuApi,createMenuApi,editMenuApi,deleteMenuApi } from '@/api/modules/menu' import { getMenuApi, createMenuApi, editMenuApi, deleteMenuApi } from '@/api/modules/menu'
// header // header
const menuTitle = ref('菜单管理') const menuTitle = ref('菜单管理')
const menuHeaderButtons = [ const menuHeaderButtons = [
@ -69,21 +69,20 @@ const dialogType = ref('add')
// //
const menuForm = ref({ const menuForm = ref({
name: '', // name: '', //
nameValue: '', // parentName: '', //
path: '', // path: '', //
api: '', // api: '', //
icon: '', // icon: '', //
sort: null, // 1 sort: null, // 1
type: null // type: null, //
status: null, //
}) })
const menuFormRef = ref<FormInstance>() const menuFormRef = ref<FormInstance>()
// //
const menuFormRules = rules(menuForm.value, MenuiconOptions) const menuFormRules = rules(menuForm.value, MenuiconOptions)
// // icon
const menuFormOptionsMap = ref({ const menuFormOptionsMap = ref({
'parentName': [ 'parentName': [] as any[],
{ label: '一级菜单', value: '1' },
],
'icon': MenuiconOptions, 'icon': MenuiconOptions,
'type': [ 'type': [
{ label: '菜单', value: 1 }, { label: '菜单', value: 1 },
@ -113,7 +112,7 @@ const filterMenus = (menuList: any[]) => {
return enabledMenus return enabledMenus
} }
// //
onMounted(async () => { const fetchMenuList = async () => {
try { try {
const data = await getMenuApi() const data = await getMenuApi()
menuTree.value = filterMenus(data) menuTree.value = filterMenus(data)
@ -123,12 +122,17 @@ onMounted(async () => {
value: item.id ?? '' value: item.id ?? ''
})) }))
menuFormOptionsMap.value.parentName = [ menuFormOptionsMap.value.parentName = [
{ label: '一级菜单', value: '1' }, { label: '一级菜单', value: "0" },
...parentNameOptions ...parentNameOptions
] ]
} catch (err) { } catch (err) {
console.error('获取菜单列表失败') console.error('获取菜单列表失败', err)
ElMessage.error('获取菜单列表失败,请稍后重试')
} }
}
//
onMounted(async () => {
await fetchMenuList()
}) })
/** /**
@ -138,16 +142,23 @@ const handleEditMenu = (row: any) => {
dialogVisible.value = true dialogVisible.value = true
dialogTitle.value = '编辑菜单' dialogTitle.value = '编辑菜单'
dialogType.value = 'edit' dialogType.value = 'edit'
menuForm.value = { ...row, menuForm.value = {
parentName: row.name ...row,
} id: row.id, // id
console.log('编辑菜单', row) parentName: row.name,
status: row.status ?? 1, //
}
} }
const handleDeleteMenu = (row: any) => { const handleDeleteMenu = async (row: any) => {
console.log('删除菜单', row) try {
await deleteMenuApi({id: row.id})
await fetchMenuList()
} catch (err) {
console.error('删除菜单失败')
}
} }
const handleToggleStatus = (row: any) => { const handleToggleStatus = (row: any) => {
console.log('切换菜单状态', row) row.status = row.status === 1 ? 2 : 1
} }
/** /**
@ -159,16 +170,21 @@ const handleSubmit = async () => {
await menuFormRef.value?.validate() await menuFormRef.value?.validate()
if (dialogType.value === 'add') { if (dialogType.value === 'add') {
const params = { const params = {
...menuForm.value, name: menuForm.value.name, //
parent_id: '0' parent_id: menuForm.value.parentName, // id
path: menuForm.value.path, //
api: menuForm.value.api, //
icon: menuForm.value.icon, //
sort: menuForm.value.sort ?? 1, // 1
type: menuForm.value.type ?? 1, //
} }
await createMenuApi(params) await createMenuApi(params)
await fetchMenuList()
} else { } else {
await editMenuApi(menuForm.value) await editMenuApi(menuForm.value)
await fetchMenuList()
} }
dialogVisible.value = false dialogVisible.value = false
//
// onMounted()
} catch (err) { } catch (err) {
console.log('表单验证失败') console.log('表单验证失败')
return return

View File

@ -42,6 +42,6 @@ export const MenuFormItemOptions = [
{ prop: 'path', label: '路由路径', type: 'input' as const, placeholder: '请输入路由路径' }, { prop: 'path', label: '路由路径', type: 'input' as const, placeholder: '请输入路由路径' },
{ prop: 'api', label: '后端路由', type: 'input' as const, placeholder: '请输入后端路由' }, { prop: 'api', label: '后端路由', type: 'input' as const, placeholder: '请输入后端路由' },
{ prop: 'icon', label: '图标名称', type: 'select' as const, placeholder: '请选择图标名称' }, { prop: 'icon', label: '图标名称', type: 'select' as const, placeholder: '请选择图标名称' },
{ prop: 'sort', label: '排序', type: 'number' as const, placeholder: '请输入排序' }, { prop: 'sort', label: '排序', type: 'number' as const, placeholder: '请输入排序,数字越小越靠前' },
{ prop: 'type', label: '菜单类型', type: 'radio' as const, placeholder: '请选择菜单类型' } { prop: 'type', label: '菜单类型', type: 'radio' as const, placeholder: '请选择菜单类型' }
] ]

View File

@ -8,38 +8,20 @@ export const rules = (menuForm: any, iconOptions: any[]): FormRules => {
{ min: 2, max: 20, message: '菜单名称长度在 2 到 20 个字符之间', trigger: 'blur' } { min: 2, max: 20, message: '菜单名称长度在 2 到 20 个字符之间', trigger: 'blur' }
], ],
// 2. 父级菜单必填默认0无需手动填 // 2. 父级菜单必填默认0无需手动填
parentId: [{ required: true, message: '请选择父级菜单', trigger: 'change' } parentName: [{ required: true, message: '请选择父级菜单', trigger: 'change' }
], ],
// 3. 路由路径:必填 + 格式校验(以/开头,仅包含字母/数字/下划线/斜杠) // 3. 路由路径:必填 + 格式校验(以/开头,仅包含字母/数字/下划线/斜杠)
path: [ path: [
{ required: true, message: '请输入路由路径', trigger: 'blur' }, { required: true, message: '请输入路由路径', trigger: 'blur' },
{ pattern: /^\/[a-zA-Z0-9_\/]+$/, message: '路由路径必须以/开头,仅包含字母、数字、下划线和斜杠', trigger: 'blur' } { pattern: /^\/[a-zA-Z0-9_\/]+$/, message: '路由路径必须以/开头,仅包含字母、数字、下划线和斜杠', trigger: 'blur' }
], ],
// 4. 后端路由:可选,但填了就必须符合格式 // 4. 后端路由:必填 + 格式校验(以/开头,仅包含字母/数字/下划线/斜杠)
api: [ api: [
{ required: true, pattern: /(^$)|(^\/[a-zA-Z0-9_\/]+$)/, message: '后端路由若填写,必须以/开头,仅包含字母、数字、下划线和斜杠', trigger: 'blur' } { required: true, pattern: /^\/[a-zA-Z0-9_\/]+$/, message: '后端路由必须以/开头,仅包含字母、数字、下划线和斜杠', trigger: 'blur' }
], ],
// 5. 图标名称:可选,但填了就必须是指定范围(匹配你提供的图标列表) // 5. 图标名称:可选,但填了就必须是指定范围(匹配你提供的图标列表)
icon: [ icon: [
{ {required: true, message: '请选择图标名称', trigger: 'blur'}
validator: (rule, value, callback) => {
// 一级菜单必须选图标
if (menuForm.parentId === "0" && !value) {
callback(new Error("一级菜单必须选择图标"))
}
// 若填写了图标,必须是指定范围内的
else if (
value &&
!iconOptions.some(item => item.value === value)
) {
callback(new Error("请选择预设图标"))
}
else {
callback()
}
},
trigger: "change",
},
], ],
// 6. 排序:必填 + 数字 + 最小值0 // 6. 排序:必填 + 数字 + 最小值0
sort: [ sort: [