zhou zhou
19 小时以前 40905cbd04c2e332cd4bc2b9e0c5b3e1da9cccfa
rsf-design/src/views/system/menu/index.vue
@@ -1,7 +1,6 @@
<!-- 菜单管理页面 -->
<template>
  <div class="menu-page art-full-height">
    <!-- 搜索栏 -->
    <ArtSearchBar
      v-model="formFilters"
      :items="formItems"
@@ -11,7 +10,6 @@
    />
    <ElCard class="art-table-card">
      <!-- 表格头部 -->
      <ArtTableHeader
        :showZebra="false"
        :loading="loading"
@@ -28,7 +26,7 @@
      <ArtTable
        ref="tableRef"
        rowKey="path"
        rowKey="id"
        :loading="loading"
        :columns="columns"
        :data="filteredTableData"
@@ -37,12 +35,12 @@
        :default-expand-all="false"
      />
      <!-- 菜单弹窗 -->
      <MenuDialog
        v-model:visible="dialogVisible"
        :type="dialogType"
        :editData="editData"
        :lockType="lockMenuType"
        :menuTreeOptions="menuTreeOptions"
        @submit="handleSubmit"
      />
    </ElCard>
@@ -56,22 +54,34 @@
  import ArtSvgIcon from '@/components/core/base/art-svg-icon/index.vue'
  import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
  import { useTableColumns } from '@/hooks/core/useTableColumns'
  import { fetchGetMenuList } from '@/api/system-manage'
  import { ElTag, ElMessageBox } from 'element-plus'
  import {
    fetchDeleteMenu,
    fetchGetMenuList,
    fetchSaveMenu,
    fetchUpdateMenu
  } from '@/api/system-manage'
  import { ElTag, ElMessage, ElMessageBox } from 'element-plus'
  defineOptions({ name: 'Menus' })
  const loading = ref(false)
  const isExpanded = ref(false)
  const tableRef = ref()
  const dialogVisible = ref(false)
  const dialogType = ref('menu')
  const editData = ref(null)
  const lockMenuType = ref(false)
  const lockMenuType = ref(true)
  const tableData = ref([])
  const menuTreeOptions = ref([])
  const initialSearchState = {
    name: '',
    route: ''
  }
  const formFilters = reactive({ ...initialSearchState })
  const appliedFilters = reactive({ ...initialSearchState })
  const formItems = computed(() => [
    {
      label: '菜单名称',
@@ -86,44 +96,88 @@
      props: { clearable: true }
    }
  ])
  onMounted(() => {
    getMenuList()
  })
  const getMenuList = async () => {
  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()
      tableData.value = list
      const list = await fetchGetMenuList({})
      tableData.value = Array.isArray(list) ? list : []
      menuTreeOptions.value = [
        {
          label: '顶级菜单',
          value: 0,
          children: normalizeMenuTreeOptions(tableData.value)
        }
      ]
    } catch (error) {
      throw error instanceof Error ? error : new Error('获取菜单失败')
      ElMessage.error(error?.message || '获取菜单失败')
    } finally {
      loading.value = false
    }
  }
  onMounted(() => {
    loadMenuResources()
  })
  const hasNestedMenus = (row) => Array.isArray(row.children) && row.children.some((child) => !child.meta?.isAuthButton)
  const getMenuTypeTag = (row) => {
    if (row.meta?.isAuthButton) return 'danger'
    if (row.children?.length) return 'info'
    if (row.meta?.link && row.meta?.isIframe) return 'success'
    if (row.path) return 'primary'
    if (row.meta?.link) return 'warning'
    return 'info'
    if (row.meta?.isAuthButton || Number(row.type) === 1) return 'danger'
    if (hasNestedMenus(row)) return 'info'
    return 'primary'
  }
  const getMenuTypeText = (row) => {
    if (row.meta?.isAuthButton) return '按钮'
    if (row.children?.length) return '目录'
    if (row.meta?.link && row.meta?.isIframe) return '内嵌'
    if (row.path) return '菜单'
    if (row.meta?.link) return '外链'
    return '未知'
    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: '图标预览',
@@ -134,52 +188,64 @@
        if (!icon) return h('span', { class: 'text-g-400' }, '-')
        return h('div', { class: 'flex items-center justify-center' }, [
          h(ArtSvgIcon, { icon, class: 'text-base text-g-700' })
        ])
      }
    },
        return h(
          'div',
    {
      prop: 'meta.title',
      label: '菜单名称',
      minWidth: 120,
      formatter: (row) => getMenuDisplayTitle(row)
            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: '菜单类型',
      formatter: (row) => {
        return h(ElTag, { type: getMenuTypeTag(row) }, () => getMenuTypeText(row))
      }
      width: 110,
      formatter: (row) =>
        h(ElTag, { type: getMenuTypeTag(row), effect: 'light' }, () => getMenuTypeText(row))
    },
    {
      prop: 'path',
      prop: 'route',
      label: '路由',
      minWidth: 180,
      formatter: (row) => {
        if (row.meta?.isAuthButton) return ''
        return row.meta?.link || row.path || ''
        return row.route || ''
      }
    },
    {
      prop: 'meta.authList',
      prop: 'authority',
      label: '权限标识',
      minWidth: 180,
      formatter: (row) => {
        if (row.meta?.isAuthButton) {
          return row.meta?.authMark || ''
          return row.authority || row.meta?.authMark || ''
        }
        if (!row.meta?.authList?.length) return ''
        if (!row.meta?.authList?.length) return row.authority || ''
        return `${row.meta.authList.length} 个权限标识`
      }
    },
    {
      prop: 'date',
      label: '编辑时间',
      formatter: () => '2022-3-12 12:00:00'
      prop: 'sort',
      label: '排序',
      width: 90
    },
    {
      prop: 'status',
      label: '状态',
      formatter: () => h(ElTag, { type: 'success' }, () => '启用')
      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',
@@ -187,7 +253,7 @@
      width: 180,
      align: 'right',
      formatter: (row) => {
        const buttonStyle = { style: 'text-align: right' }
        const buttonStyle = { class: 'flex justify-end' }
        if (row.meta?.isAuthButton) {
          return h('div', buttonStyle, [
            h(ArtButtonTable, {
@@ -196,14 +262,14 @@
            }),
            h(ArtButtonTable, {
              type: 'delete',
              onClick: () => handleDeleteAuth()
              onClick: () => handleDeleteAuth(row)
            })
          ])
        }
        return h('div', buttonStyle, [
          h(ArtButtonTable, {
            type: 'add',
            onClick: () => handleAddAuth(),
            onClick: () => handleAddAuth(row),
            title: '新增权限'
          }),
          h(ArtButtonTable, {
@@ -212,25 +278,13 @@
          }),
          h(ArtButtonTable, {
            type: 'delete',
            onClick: () => handleDeleteMenu()
            onClick: () => handleDeleteMenu(row)
          })
        ])
      }
    }
    },
  ])
  const tableData = ref([])
  const handleReset = () => {
    Object.assign(formFilters, { ...initialSearchState })
    Object.assign(appliedFilters, { ...initialSearchState })
    getMenuList()
  }
  const handleSearch = () => {
    Object.assign(appliedFilters, { ...formFilters })
    getMenuList()
  }
  const handleRefresh = () => {
    getMenuList()
  }
  const deepClone = (obj) => {
    if (obj === null || typeof obj !== 'object') return obj
    if (obj instanceof Date) return new Date(obj)
@@ -243,6 +297,7 @@
    }
    return cloned
  }
  const convertAuthListToChildren = (items) => {
    return items.map((item) => {
      const clonedItem = deepClone(item)
@@ -251,13 +306,17 @@
      }
      if (item.meta?.authList?.length) {
        const authChildren = item.meta.authList.map((auth) => ({
          path: `${item.path}_auth_${auth.authMark}`,
          name: `${String(item.name)}_auth_${auth.authMark}`,
          ...deepClone(auth),
          route: auth.route || '',
          component: auth.component || '',
          meta: {
            title: auth.title,
            authMark: auth.authMark,
            isAuthButton: true,
            parentPath: item.path
            parentPath: item.path,
            icon: auth.icon,
            sort: auth.sort,
            isEnable: normalizeNumber(auth.status, 1) === 1
          }
        }))
        clonedItem.children = clonedItem.children?.length
@@ -267,15 +326,17 @@
      return clonedItem
    })
  }
  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 menuPath = (item.path || '').toLowerCase()
      const menuRoute = String(item.route || item.path || item.authority || '').toLowerCase()
      const nameMatch = !searchName || menuTitle.includes(searchName)
      const routeMatch = !searchRoute || menuPath.includes(searchRoute)
      const routeMatch = !searchRoute || menuRoute.includes(searchRoute)
      if (item.children?.length) {
        const matchedChildren = searchMenu(item.children)
        if (matchedChildren.length > 0) {
@@ -285,77 +346,147 @@
          continue
        }
      }
      if (nameMatch && routeMatch) {
        results.push(deepClone(item))
      }
    }
    return results
  }
  const filteredTableData = computed(() => {
    const searchedData = searchMenu(tableData.value)
    return convertAuthListToChildren(searchedData)
  })
  const closeDialog = () => {
    dialogVisible.value = false
    editData.value = null
  }
  const handleAddMenu = () => {
    dialogType.value = 'menu'
    editData.value = null
    lockMenuType.value = true
    dialogVisible.value = true
  }
  const handleAddAuth = () => {
    dialogType.value = 'menu'
    editData.value = null
    lockMenuType.value = false
  const handleAddAuth = (row) => {
    dialogType.value = 'button'
    editData.value = {
      parentId: row.id,
      type: 1,
      status: 1,
      sort: 0
    }
    lockMenuType.value = true
    dialogVisible.value = true
  }
  const handleEditMenu = (row) => {
    dialogType.value = 'menu'
    editData.value = row
    lockMenuType.value = true
    dialogVisible.value = true
  }
  const handleEditAuth = (row) => {
    dialogType.value = 'button'
    editData.value = {
      title: row.meta?.title,
      authMark: row.meta?.authMark
    }
    lockMenuType.value = false
    editData.value = row
    lockMenuType.value = true
    dialogVisible.value = true
  }
  const handleSubmit = (formData) => {
    console.log('提交数据:', formData)
    getMenuList()
  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 handleDeleteMenu = async () => {
  }
  const handleSubmit = async (formData) => {
    const payload = buildMenuSubmitPayload(formData)
    if (payload.id && payload.id === payload.parentId) {
      ElMessage.error('上级菜单不能选择当前菜单')
      return
    }
    try {
      await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', {
      if (payload.id) {
        await fetchUpdateMenu(payload)
        ElMessage.success('修改成功')
      } else {
        await fetchSaveMenu(payload)
        ElMessage.success('新增成功')
      }
      closeDialog()
      await loadMenuResources()
    } catch (error) {
      ElMessage.error(error?.message || '提交失败')
    }
  }
  const handleDeleteMenu = async (row) => {
    try {
      await ElMessageBox.confirm(
        `确定要删除菜单「${formatMenuTitle(row.meta?.title || row.name || '')}」吗?删除后无法恢复`,
        '删除确认',
        {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }
      )
      await fetchDeleteMenu(row.id)
      ElMessage.success('删除成功')
      await loadMenuResources()
    } catch (error) {
      if (error !== 'cancel') {
        ElMessage.error(error?.message || '删除失败')
      }
    }
  }
  const handleDeleteAuth = async (row) => {
    try {
      await ElMessageBox.confirm(`确定要删除权限「${row.name || row.authority || row.id}」吗?删除后无法恢复`, '删除确认', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      })
      await fetchDeleteMenu(row.id)
      ElMessage.success('删除成功')
      getMenuList()
      await loadMenuResources()
    } catch (error) {
      if (error !== 'cancel') {
        ElMessage.error('删除失败')
        ElMessage.error(error?.message || '删除失败')
      }
    }
  }
  const handleDeleteAuth = async () => {
    try {
      await ElMessageBox.confirm('确定要删除该权限吗?删除后无法恢复', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      })
      ElMessage.success('删除成功')
      getMenuList()
    } catch (error) {
      if (error !== 'cancel') {
        ElMessage.error('删除失败')
  const handleReset = () => {
    Object.assign(formFilters, { ...initialSearchState })
    Object.assign(appliedFilters, { ...initialSearchState })
    loadMenuResources()
      }
  const handleSearch = () => {
    Object.assign(appliedFilters, { ...formFilters })
    }
  const handleRefresh = () => {
    loadMenuResources()
  }
  const toggleExpand = () => {
    isExpanded.value = !isExpanded.value
    nextTick(() => {