zhou zhou
5 天以前 aaf8a50511d77dbc209ca93bbba308c21179a8bc
rsf-design/src/views/system/menu/index.vue
@@ -51,8 +51,7 @@
  import MenuDialog from './modules/menu-dialog.vue'
  import { formatMenuTitle } from '@/utils/router'
  import ArtSvgIcon from '@/components/core/base/art-svg-icon/index.vue'
  import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
  import { useTableColumns } from '@/hooks/core/useTableColumns'
  import {
    fetchDeleteMenu,
@@ -60,7 +59,16 @@
    fetchSaveMenu,
    fetchUpdateMenu
  } from '@/api/system-manage'
  import { ElTag, ElMessage, ElMessageBox } from 'element-plus'
  import { ElMessage, ElMessageBox } from 'element-plus'
  import { createMenuTableColumns } from './menuTable.columns'
  import {
    buildMenuSubmitPayload,
    buildMenuTreeOptions,
    createMenuSearchState,
    expandMenuAuthChildren,
    filterMenuTree,
    getMenuDisplayTitle
  } from './menuPage.helpers'
  defineOptions({ name: 'Menus' })
@@ -74,10 +82,7 @@
  const tableData = ref([])
  const menuTreeOptions = ref([])
  const initialSearchState = {
    name: '',
    route: ''
  }
  const initialSearchState = createMenuSearchState()
  const formFilters = reactive({ ...initialSearchState })
  const appliedFilters = reactive({ ...initialSearchState })
@@ -97,39 +102,19 @@
    }
  ])
  const normalizeNumber = (value, fallback = 0) => {
    if (value === '' || value === null || value === undefined) {
      return fallback
    }
    const normalized = Number(value)
    return Number.isNaN(normalized) ? fallback : normalized
  }
  const normalizeMenuTreeOptions = (nodes = []) => {
    if (!Array.isArray(nodes)) {
      return []
    }
    return nodes
      .map((node) => ({
        label: formatMenuTitle(node.meta?.title || node.name || ''),
        value: normalizeNumber(node.id, 0),
        children: normalizeMenuTreeOptions(node.children)
      }))
  }
  const loadMenuResources = async () => {
    loading.value = true
    try {
      const list = await fetchGetMenuList({})
      const list = await guardRequestWithMessage(fetchGetMenuList({}), null, {
        timeoutMessage: '菜单加载超时,已停止等待'
      })
      if (list === null) {
        tableData.value = []
        menuTreeOptions.value = []
        return
      }
      tableData.value = Array.isArray(list) ? list : []
      menuTreeOptions.value = [
        {
          label: '顶级菜单',
          value: 0,
          children: normalizeMenuTreeOptions(tableData.value)
        }
      ]
      menuTreeOptions.value = buildMenuTreeOptions(tableData.value, formatMenuTitle)
    } catch (error) {
      ElMessage.error(error?.message || '获取菜单失败')
    } finally {
@@ -141,237 +126,35 @@
    loadMenuResources()
  })
  const hasNestedMenus = (row) => Array.isArray(row.children) && row.children.some((child) => !child.meta?.isAuthButton)
  const getMenuTypeTag = (row) => {
    if (row.meta?.isAuthButton || Number(row.type) === 1) return 'danger'
    if (hasNestedMenus(row)) return 'info'
    return 'primary'
  }
  const getMenuTypeText = (row) => {
    if (row.meta?.isAuthButton || Number(row.type) === 1) return '按钮'
    if (hasNestedMenus(row)) return '目录'
    return '菜单'
  }
  const getStatusMeta = (status) => {
    return normalizeNumber(status, 1) === 1
      ? { text: '启用', type: 'success' }
      : { text: '禁用', type: 'danger' }
  }
  const getMenuDisplayTitle = (row) => {
    const titleKey = row.meta?.title || row.name || ''
    const normalizedTitleKey =
      titleKey && !String(titleKey).includes('.') ? `menu.${titleKey}` : titleKey
    return formatMenuTitle(normalizedTitleKey)
  }
  const getMenuDisplayIcon = (row) => row.meta?.icon || row.icon || ''
  const { columnChecks, columns } = useTableColumns(() => [
    {
      prop: 'meta.title',
      label: '菜单名称',
      minWidth: 180,
      formatter: (row) => getMenuDisplayTitle(row)
    },
    {
      prop: 'meta.icon',
      label: '图标预览',
      width: 96,
      align: 'center',
      formatter: (row) => {
        const icon = getMenuDisplayIcon(row)
        if (!icon) return h('span', { class: 'text-g-400' }, '-')
        return h(
          'div',
          {
            class:
              'mx-auto flex h-8 w-8 items-center justify-center rounded-md border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)]'
          },
          [h(ArtSvgIcon, { icon, class: 'text-base text-g-700' })]
        )
      }
    },
    {
      prop: 'type',
      label: '菜单类型',
      width: 110,
      formatter: (row) =>
        h(ElTag, { type: getMenuTypeTag(row), effect: 'light' }, () => getMenuTypeText(row))
    },
    {
      prop: 'route',
      label: '路由',
      minWidth: 180,
      formatter: (row) => {
        if (row.meta?.isAuthButton) return ''
        return row.route || ''
      }
    },
    {
      prop: 'authority',
      label: '权限标识',
      minWidth: 180,
      formatter: (row) => {
        if (row.meta?.isAuthButton) {
          return row.authority || row.meta?.authMark || ''
        }
        if (!row.meta?.authList?.length) return row.authority || ''
        return `${row.meta.authList.length} 个权限标识`
      }
    },
    {
      prop: 'sort',
      label: '排序',
      width: 90
    },
    {
      prop: 'status',
      label: '状态',
      width: 100,
      formatter: (row) => {
        const statusMeta = getStatusMeta(row.status)
        return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
      }
    },
    {
      prop: 'memo',
      label: '备注',
      minWidth: 180,
      showOverflowTooltip: true,
      formatter: (row) => row.memo || '-'
    },
    {
      prop: 'operation',
      label: '操作',
      width: 180,
      align: 'right',
      formatter: (row) => {
        const buttonStyle = { class: 'flex justify-end' }
        if (row.meta?.isAuthButton) {
          return h('div', buttonStyle, [
            h(ArtButtonTable, {
              type: 'edit',
              onClick: () => handleEditAuth(row)
            }),
            h(ArtButtonTable, {
              type: 'delete',
              onClick: () => handleDeleteAuth(row)
            })
          ])
        }
        return h('div', buttonStyle, [
          h(ArtButtonTable, {
            type: 'add',
            onClick: () => handleAddAuth(row),
            title: '新增权限'
          }),
          h(ArtButtonTable, {
            type: 'edit',
            onClick: () => handleEditMenu(row)
          }),
          h(ArtButtonTable, {
            type: 'delete',
            onClick: () => handleDeleteMenu(row)
          })
        ])
      }
    },
  ])
  const deepClone = (obj) => {
    if (obj === null || typeof obj !== 'object') return obj
    if (obj instanceof Date) return new Date(obj)
    if (Array.isArray(obj)) return obj.map((item) => deepClone(item))
    const cloned = {}
    for (const key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        cloned[key] = deepClone(obj[key])
      }
    }
    return cloned
  }
  const convertAuthListToChildren = (items) => {
    return items.map((item) => {
      const clonedItem = deepClone(item)
      if (clonedItem.children?.length) {
        clonedItem.children = convertAuthListToChildren(clonedItem.children)
      }
      if (item.meta?.authList?.length) {
        const authChildren = item.meta.authList.map((auth) => ({
          ...deepClone(auth),
          route: auth.route || '',
          component: auth.component || '',
          meta: {
            title: auth.title,
            authMark: auth.authMark,
            isAuthButton: true,
            parentPath: item.path,
            icon: auth.icon,
            sort: auth.sort,
            isEnable: normalizeNumber(auth.status, 1) === 1
          }
        }))
        clonedItem.children = clonedItem.children?.length
          ? [...clonedItem.children, ...authChildren]
          : authChildren
      }
      return clonedItem
  const { columnChecks, columns } = useTableColumns(() =>
    createMenuTableColumns({
      titleFormatter: formatMenuTitle,
      handleAddAuth,
      handleEditAuth,
      handleDeleteAuth,
      handleEditMenu,
      handleDeleteMenu
    })
  }
  const searchMenu = (items) => {
    const results = []
    for (const item of items) {
      const searchName = appliedFilters.name?.toLowerCase().trim() || ''
      const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
      const menuTitle = getMenuDisplayTitle(item).toLowerCase()
      const menuRoute = String(item.route || item.path || item.authority || '').toLowerCase()
      const nameMatch = !searchName || menuTitle.includes(searchName)
      const routeMatch = !searchRoute || menuRoute.includes(searchRoute)
      if (item.children?.length) {
        const matchedChildren = searchMenu(item.children)
        if (matchedChildren.length > 0) {
          const clonedItem = deepClone(item)
          clonedItem.children = matchedChildren
          results.push(clonedItem)
          continue
        }
      }
      if (nameMatch && routeMatch) {
        results.push(deepClone(item))
      }
    }
    return results
  }
  )
  const filteredTableData = computed(() => {
    const searchedData = searchMenu(tableData.value)
    return convertAuthListToChildren(searchedData)
    const searchedData = filterMenuTree(tableData.value, appliedFilters, formatMenuTitle)
    return expandMenuAuthChildren(searchedData)
  })
  const closeDialog = () => {
  function closeDialog() {
    dialogVisible.value = false
    editData.value = null
  }
  const handleAddMenu = () => {
  function handleAddMenu() {
    dialogType.value = 'menu'
    editData.value = null
    lockMenuType.value = true
    dialogVisible.value = true
  }
  const handleAddAuth = (row) => {
  function handleAddAuth(row) {
    dialogType.value = 'button'
    editData.value = {
      parentId: row.id,
@@ -383,37 +166,21 @@
    dialogVisible.value = true
  }
  const handleEditMenu = (row) => {
  function handleEditMenu(row) {
    dialogType.value = 'menu'
    editData.value = row
    lockMenuType.value = true
    dialogVisible.value = true
  }
  const handleEditAuth = (row) => {
  function handleEditAuth(row) {
    dialogType.value = 'button'
    editData.value = row
    lockMenuType.value = true
    dialogVisible.value = true
  }
  const buildMenuSubmitPayload = (formData) => {
    return {
      ...(formData.id ? { id: normalizeNumber(formData.id, 0) } : {}),
      parentId: normalizeNumber(formData.parentId, 0),
      name: String(formData.name || '').trim(),
      route: String(formData.route || '').trim(),
      component: String(formData.component || '').trim(),
      authority: String(formData.authority || '').trim(),
      icon: String(formData.icon || '').trim(),
      sort: normalizeNumber(formData.sort, 0),
      status: normalizeNumber(formData.status, 1),
      memo: String(formData.memo || '').trim(),
      type: formData.menuType === 'button' ? 1 : 0
    }
  }
  const handleSubmit = async (formData) => {
  async function handleSubmit(formData) {
    const payload = buildMenuSubmitPayload(formData)
    if (payload.id && payload.id === payload.parentId) {
      ElMessage.error('上级菜单不能选择当前菜单')
@@ -435,10 +202,10 @@
    }
  }
  const handleDeleteMenu = async (row) => {
  async function handleDeleteMenu(row) {
    try {
      await ElMessageBox.confirm(
        `确定要删除菜单「${formatMenuTitle(row.meta?.title || row.name || '')}」吗?删除后无法恢复`,
        `确定要删除菜单「${getMenuDisplayTitle(row, formatMenuTitle)}」吗?删除后无法恢复`,
        '删除确认',
        {
          confirmButtonText: '确定',
@@ -456,7 +223,7 @@
    }
  }
  const handleDeleteAuth = async (row) => {
  async function handleDeleteAuth(row) {
    try {
      await ElMessageBox.confirm(`确定要删除权限「${row.name || row.authority || row.id}」吗?删除后无法恢复`, '删除确认', {
        confirmButtonText: '确定',
@@ -473,21 +240,21 @@
    }
  }
  const handleReset = () => {
  function handleReset() {
    Object.assign(formFilters, { ...initialSearchState })
    Object.assign(appliedFilters, { ...initialSearchState })
    loadMenuResources()
  }
  const handleSearch = () => {
  function handleSearch() {
    Object.assign(appliedFilters, { ...formFilters })
  }
  const handleRefresh = () => {
  function handleRefresh() {
    loadMenuResources()
  }
  const toggleExpand = () => {
  function toggleExpand() {
    isExpanded.value = !isExpanded.value
    nextTick(() => {
      if (tableRef.value?.elTableRef && filteredTableData.value) {