| | |
| | | :title="dialogTitle" |
| | | :model-value="visible" |
| | | @update:model-value="handleCancel" |
| | | width="860px" |
| | | width="760px" |
| | | align-center |
| | | class="menu-dialog" |
| | | @closed="handleClosed" |
| | |
| | | v-model="form" |
| | | :items="formItems" |
| | | :rules="rules" |
| | | :span="width > 640 ? 12 : 24" |
| | | :span="24" |
| | | :gutter="20" |
| | | label-width="100px" |
| | | :show-reset="false" |
| | |
| | | > |
| | | <template #menuType> |
| | | <ElRadioGroup v-model="form.menuType" :disabled="disableMenuType"> |
| | | <ElRadioButton value="menu" label="menu">菜单</ElRadioButton> |
| | | <ElRadioButton value="button" label="button">按钮</ElRadioButton> |
| | | <ElRadioButton value="menu">菜单</ElRadioButton> |
| | | <ElRadioButton value="button">按钮</ElRadioButton> |
| | | </ElRadioGroup> |
| | | </template> |
| | | </ArtForm> |
| | | |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <ElButton @click="handleCancel">取 消</ElButton> |
| | | <ElButton type="primary" @click="handleSubmit">确 定</ElButton> |
| | | <ElButton @click="handleCancel">取消</ElButton> |
| | | <ElButton type="primary" @click="handleSubmit">确定</ElButton> |
| | | </span> |
| | | </template> |
| | | </ElDialog> |
| | |
| | | <script setup> |
| | | import ArtForm from '@/components/core/forms/art-form/index.vue' |
| | | |
| | | import { ElIcon, ElTooltip } from 'element-plus' |
| | | import { QuestionFilled } from '@element-plus/icons-vue' |
| | | import { formatMenuTitle } from '@/utils/router' |
| | | import { useWindowSize } from '@vueuse/core' |
| | | const { width } = useWindowSize() |
| | | const createLabelTooltip = (label, tooltip) => { |
| | | return () => |
| | | h('span', { class: 'flex items-center' }, [ |
| | | h('span', label), |
| | | h( |
| | | ElTooltip, |
| | | { |
| | | content: tooltip, |
| | | placement: 'top' |
| | | }, |
| | | () => h(ElIcon, { class: 'ml-0.5 cursor-help' }, () => h(QuestionFilled)) |
| | | ) |
| | | ]) |
| | | } |
| | | const createMenuFormState = () => ({ |
| | | menuType: 'menu', |
| | | id: null, |
| | | parentId: 0, |
| | | name: '', |
| | | route: '', |
| | | component: '', |
| | | authority: '', |
| | | icon: '', |
| | | sort: 0, |
| | | status: 1, |
| | | memo: '' |
| | | }) |
| | | |
| | | const props = defineProps({ |
| | | visible: { required: false, default: false }, |
| | | type: { required: false, default: 'menu' }, |
| | | lockType: { required: false, default: false } |
| | | lockType: { required: false, default: false }, |
| | | editData: { required: false, default: null }, |
| | | menuTreeOptions: { required: false, default: () => [] } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:visible', 'submit']) |
| | | const formRef = ref() |
| | | const isEdit = ref(false) |
| | | const form = reactive({ |
| | | menuType: 'menu', |
| | | id: 0, |
| | | name: '', |
| | | path: '', |
| | | label: '', |
| | | component: '', |
| | | icon: '', |
| | | isEnable: true, |
| | | sort: 1, |
| | | isMenu: true, |
| | | keepAlive: true, |
| | | isHide: false, |
| | | isHideTab: false, |
| | | link: '', |
| | | isIframe: false, |
| | | showBadge: false, |
| | | showTextBadge: '', |
| | | fixedTab: false, |
| | | activePath: '', |
| | | roles: [], |
| | | isFullPage: false, |
| | | authName: '', |
| | | authLabel: '', |
| | | authIcon: '', |
| | | authSort: 1 |
| | | }) |
| | | const rules = reactive({ |
| | | name: [ |
| | | { required: true, message: '请输入菜单名称', trigger: 'blur' }, |
| | | { min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' } |
| | | ], |
| | | path: [{ required: true, message: '请输入路由地址', trigger: 'blur' }], |
| | | label: [{ required: true, message: '输入权限标识', trigger: 'blur' }], |
| | | authName: [{ required: true, message: '请输入权限名称', trigger: 'blur' }], |
| | | authLabel: [{ required: true, message: '请输入权限标识', trigger: 'blur' }] |
| | | }) |
| | | const form = reactive(createMenuFormState()) |
| | | |
| | | const isEdit = computed(() => Boolean(form.id)) |
| | | const dialogTitle = computed(() => `${isEdit.value ? '编辑' : '新建'}${form.menuType === 'button' ? '按钮' : '菜单'}`) |
| | | const disableMenuType = computed(() => props.lockType || isEdit.value) |
| | | |
| | | const rules = computed(() => ({ |
| | | name: [{ required: true, message: form.menuType === 'button' ? '请输入权限名称' : '请输入菜单名称', trigger: 'blur' }], |
| | | route: |
| | | form.menuType === 'menu' |
| | | ? [{ required: true, message: '请输入路由地址', trigger: 'blur' }] |
| | | : [], |
| | | authority: |
| | | form.menuType === 'button' |
| | | ? [{ required: true, message: '请输入权限标识', trigger: 'blur' }] |
| | | : [] |
| | | })) |
| | | |
| | | const formItems = computed(() => { |
| | | const baseItems = [{ label: '菜单类型', key: 'menuType', span: 24 }] |
| | | const switchSpan = width.value < 640 ? 12 : 6 |
| | | const items = [ |
| | | { label: '菜单类型', key: 'menuType', span: 24 }, |
| | | { |
| | | label: '上级菜单', |
| | | key: 'parentId', |
| | | type: 'treeselect', |
| | | span: 24, |
| | | props: { |
| | | data: props.menuTreeOptions, |
| | | props: { |
| | | label: 'label', |
| | | value: 'value', |
| | | children: 'children' |
| | | }, |
| | | placeholder: '请选择上级菜单', |
| | | checkStrictly: true, |
| | | clearable: false, |
| | | defaultExpandAll: true |
| | | } |
| | | }, |
| | | { |
| | | label: form.menuType === 'button' ? '权限名称' : '菜单名称', |
| | | key: 'name', |
| | | type: 'input', |
| | | span: 24, |
| | | props: { |
| | | placeholder: form.menuType === 'button' ? '请输入权限名称' : '请输入菜单名称', |
| | | clearable: true |
| | | } |
| | | } |
| | | ] |
| | | |
| | | if (form.menuType === 'menu') { |
| | | return [ |
| | | ...baseItems, |
| | | { label: '菜单名称', key: 'name', type: 'input', props: { placeholder: '菜单名称' } }, |
| | | items.push( |
| | | { |
| | | label: createLabelTooltip( |
| | | '路由地址', |
| | | '一级菜单:以 / 开头的绝对路径(如 /dashboard)\n二级及以下:相对路径(如 console、user)' |
| | | ), |
| | | key: 'path', |
| | | label: '路由地址', |
| | | key: 'route', |
| | | type: 'input', |
| | | props: { placeholder: '如:/dashboard 或 console' } |
| | | span: 24, |
| | | props: { |
| | | placeholder: '请输入路由地址', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { label: '权限标识', key: 'label', type: 'input', props: { placeholder: '如:User' } }, |
| | | { |
| | | label: createLabelTooltip( |
| | | '组件路径', |
| | | '一级父级菜单:填写 /index/index\n具体页面:填写组件路径(如 /system/user)\n目录菜单:留空' |
| | | ), |
| | | label: '组件标识', |
| | | key: 'component', |
| | | type: 'input', |
| | | props: { placeholder: '如:/system/user 或留空' } |
| | | }, |
| | | { label: '图标', key: 'icon', type: 'input', props: { placeholder: '如:ri:user-line' } }, |
| | | { |
| | | label: createLabelTooltip( |
| | | '角色权限', |
| | | '仅用于前端权限模式:配置角色标识(如 R_SUPER、R_ADMIN)\n后端权限模式:无需配置' |
| | | ), |
| | | key: 'roles', |
| | | type: 'inputtag', |
| | | props: { placeholder: '输入角色标识后按回车,如:R_SUPER' } |
| | | }, |
| | | { |
| | | label: '菜单排序', |
| | | key: 'sort', |
| | | type: 'number', |
| | | props: { min: 1, controlsPosition: 'right', style: { width: '100%' } } |
| | | }, |
| | | { |
| | | label: '外部链接', |
| | | key: 'link', |
| | | type: 'input', |
| | | props: { placeholder: '如:https://www.example.com' } |
| | | }, |
| | | { |
| | | label: '文本徽章', |
| | | key: 'showTextBadge', |
| | | type: 'input', |
| | | props: { placeholder: '如:New、Hot' } |
| | | }, |
| | | { |
| | | label: createLabelTooltip( |
| | | '激活路径', |
| | | '用于详情页等隐藏菜单,指定高亮显示的父级菜单路径\n例如:用户详情页高亮显示"用户管理"菜单' |
| | | ), |
| | | key: 'activePath', |
| | | type: 'input', |
| | | props: { placeholder: '如:/system/user' } |
| | | }, |
| | | { label: '是否启用', key: 'isEnable', type: 'switch', span: switchSpan }, |
| | | { label: '页面缓存', key: 'keepAlive', type: 'switch', span: switchSpan }, |
| | | { label: '隐藏菜单', key: 'isHide', type: 'switch', span: switchSpan }, |
| | | { label: '是否内嵌', key: 'isIframe', type: 'switch', span: switchSpan }, |
| | | { label: '显示徽章', key: 'showBadge', type: 'switch', span: switchSpan }, |
| | | { label: '固定标签', key: 'fixedTab', type: 'switch', span: switchSpan }, |
| | | { label: '标签隐藏', key: 'isHideTab', type: 'switch', span: switchSpan }, |
| | | { label: '全屏页面', key: 'isFullPage', type: 'switch', span: switchSpan } |
| | | ] |
| | | } else { |
| | | return [ |
| | | ...baseItems, |
| | | { |
| | | label: '权限名称', |
| | | key: 'authName', |
| | | type: 'input', |
| | | props: { placeholder: '如:新增、编辑、删除' } |
| | | }, |
| | | { |
| | | label: '权限标识', |
| | | key: 'authLabel', |
| | | type: 'input', |
| | | props: { placeholder: '如:add、edit、delete' } |
| | | }, |
| | | { |
| | | label: '权限排序', |
| | | key: 'authSort', |
| | | type: 'number', |
| | | props: { min: 1, controlsPosition: 'right', style: { width: '100%' } } |
| | | span: 24, |
| | | props: { |
| | | placeholder: '请输入组件标识', |
| | | clearable: true |
| | | } |
| | | } |
| | | ] |
| | | ) |
| | | } |
| | | |
| | | items.push( |
| | | { |
| | | label: '权限标识', |
| | | key: 'authority', |
| | | type: 'input', |
| | | span: 24, |
| | | props: { |
| | | placeholder: '请输入权限标识', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '图标', |
| | | key: 'icon', |
| | | type: 'input', |
| | | span: 24, |
| | | props: { |
| | | placeholder: '请输入图标名称', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '排序', |
| | | key: 'sort', |
| | | type: 'number', |
| | | span: 24, |
| | | props: { |
| | | min: 0, |
| | | controlsPosition: 'right', |
| | | style: { width: '100%' } |
| | | } |
| | | }, |
| | | { |
| | | label: '状态', |
| | | key: 'status', |
| | | type: 'select', |
| | | span: 24, |
| | | props: { |
| | | placeholder: '请选择状态', |
| | | options: [ |
| | | { label: '启用', value: 1 }, |
| | | { label: '禁用', value: 0 } |
| | | ] |
| | | } |
| | | }, |
| | | { |
| | | label: '备注', |
| | | key: 'memo', |
| | | type: 'input', |
| | | span: 24, |
| | | props: { |
| | | type: 'textarea', |
| | | rows: 3, |
| | | placeholder: '请输入备注', |
| | | clearable: true |
| | | } |
| | | } |
| | | ) |
| | | |
| | | return items |
| | | }) |
| | | const dialogTitle = computed(() => { |
| | | const type = form.menuType === 'menu' ? '菜单' : '按钮' |
| | | return isEdit.value ? `编辑${type}` : `新建${type}` |
| | | }) |
| | | const disableMenuType = computed(() => { |
| | | if (isEdit.value) return true |
| | | if (!isEdit.value && form.menuType === 'menu' && props.lockType) return true |
| | | return false |
| | | }) |
| | | |
| | | const normalizeNumber = (value, fallback = 0) => { |
| | | if (value === '' || value === null || value === undefined) { |
| | | return fallback |
| | | } |
| | | const normalized = Number(value) |
| | | return Number.isNaN(normalized) ? fallback : normalized |
| | | } |
| | | |
| | | const resetForm = () => { |
| | | formRef.value?.reset() |
| | | form.menuType = 'menu' |
| | | Object.assign(form, createMenuFormState()) |
| | | formRef.value?.clearValidate?.() |
| | | } |
| | | |
| | | const loadFormData = () => { |
| | | if (!props.editData) return |
| | | isEdit.value = true |
| | | if (form.menuType === 'menu') { |
| | | const row = props.editData |
| | | form.id = row.id || 0 |
| | | form.name = formatMenuTitle(row.meta?.title || '') |
| | | form.path = row.path || '' |
| | | form.label = row.name || '' |
| | | form.component = row.component || '' |
| | | form.icon = row.meta?.icon || '' |
| | | form.sort = row.meta?.sort || 1 |
| | | form.isMenu = row.meta?.isMenu ?? true |
| | | form.keepAlive = row.meta?.keepAlive ?? false |
| | | form.isHide = row.meta?.isHide ?? false |
| | | form.isHideTab = row.meta?.isHideTab ?? false |
| | | form.isEnable = row.meta?.isEnable ?? true |
| | | form.link = row.meta?.link || '' |
| | | form.isIframe = row.meta?.isIframe ?? false |
| | | form.showBadge = row.meta?.showBadge ?? false |
| | | form.showTextBadge = row.meta?.showTextBadge || '' |
| | | form.fixedTab = row.meta?.fixedTab ?? false |
| | | form.activePath = row.meta?.activePath || '' |
| | | form.roles = row.meta?.roles || [] |
| | | form.isFullPage = row.meta?.isFullPage ?? false |
| | | } else { |
| | | const row = props.editData |
| | | form.authName = row.title || '' |
| | | form.authLabel = row.authMark || '' |
| | | form.authIcon = row.icon || '' |
| | | form.authSort = row.sort || 1 |
| | | resetForm() |
| | | form.menuType = props.type || 'menu' |
| | | |
| | | const row = props.editData |
| | | if (!row || typeof row !== 'object') { |
| | | return |
| | | } |
| | | |
| | | form.menuType = Number(row.type) === 1 ? 'button' : props.type || 'menu' |
| | | form.id = row.id ?? null |
| | | form.parentId = normalizeNumber(row.parentId, 0) |
| | | form.name = row.name || '' |
| | | form.route = row.route || '' |
| | | form.component = row.component || '' |
| | | form.authority = row.authority || row.meta?.authMark || '' |
| | | form.icon = row.icon || row.meta?.icon || '' |
| | | form.sort = normalizeNumber(row.sort ?? row.meta?.sort, 0) |
| | | form.status = normalizeNumber(row.status, row.meta?.isEnable === false ? 0 : 1) |
| | | form.memo = row.memo || '' |
| | | } |
| | | |
| | | const handleSubmit = async () => { |
| | | if (!formRef.value) return |
| | | try { |
| | | await formRef.value.validate() |
| | | emit('submit', { ...form }) |
| | | ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功`) |
| | | handleCancel() |
| | | emit('submit', { |
| | | ...form, |
| | | type: form.menuType === 'button' ? 1 : 0 |
| | | }) |
| | | } catch { |
| | | ElMessage.error('表单校验失败,请检查输入') |
| | | return |
| | | } |
| | | } |
| | | |
| | | const handleCancel = () => { |
| | | emit('update:visible', false) |
| | | } |
| | | |
| | | const handleClosed = () => { |
| | | resetForm() |
| | | isEdit.value = false |
| | | } |
| | | |
| | | watch( |
| | | () => props.visible, |
| | | (newVal) => { |
| | | if (newVal) { |
| | | form.menuType = props.type |
| | | (visible) => { |
| | | if (visible) { |
| | | loadFormData() |
| | | nextTick(() => { |
| | | if (props.editData) { |
| | | loadFormData() |
| | | } |
| | | formRef.value?.clearValidate?.() |
| | | }) |
| | | } |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.editData, |
| | | () => { |
| | | if (props.visible) { |
| | | loadFormData() |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.type, |
| | | (newType) => { |
| | | () => { |
| | | if (props.visible) { |
| | | form.menuType = newType |
| | | loadFormData() |
| | | } |
| | | } |
| | | ) |