zhou zhou
15 小时以前 40905cbd04c2e332cd4bc2b9e0c5b3e1da9cccfa
rsf-design/src/views/system/menu/modules/menu-dialog.vue
@@ -3,7 +3,7 @@
    :title="dialogTitle"
    :model-value="visible"
    @update:model-value="handleCancel"
    width="860px"
    width="760px"
    align-center
    class="menu-dialog"
    @closed="handleClosed"
@@ -13,7 +13,7 @@
      v-model="form"
      :items="formItems"
      :rules="rules"
      :span="width > 640 ? 12 : 24"
      :span="24"
      :gutter="20"
      label-width="100px"
      :show-reset="false"
@@ -21,16 +21,16 @@
    >
      <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>
@@ -39,248 +39,252 @@
<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()
      }
    }
  )