From 40905cbd04c2e332cd4bc2b9e0c5b3e1da9cccfa Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 30 三月 2026 08:17:32 +0800
Subject: [PATCH] feat: complete rsf-design phase 1 integration

---
 rsf-design/src/views/system/menu/index.vue |  335 ++++++++++++++++++++++++++++++++++++++-----------------
 1 files changed, 233 insertions(+), 102 deletions(-)

diff --git a/rsf-design/src/views/system/menu/index.vue b/rsf-design/src/views/system/menu/index.vue
index 3a79c27..2bb409c 100644
--- a/rsf-design/src/views/system/menu/index.vue
+++ b/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"
@@ -19,7 +17,7 @@
         @refresh="handleRefresh"
       >
         <template #left>
-          <ElButton v-auth="'add'" @click="handleAddMenu" v-ripple> 娣诲姞鑿滃崟 </ElButton>
+          <ElButton v-auth="'add'" @click="handleAddMenu" v-ripple>娣诲姞鑿滃崟</ElButton>
           <ElButton @click="toggleExpand" v-ripple>
             {{ isExpanded ? '鏀惰捣' : '灞曞紑' }}
           </ElButton>
@@ -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',
+          {
+            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: 'meta.title',
-      label: '鑿滃崟鍚嶇О',
-      minWidth: 120,
-      formatter: (row) => getMenuDisplayTitle(row)
     },
     {
       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 = []
+    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(() => {

--
Gitblit v1.9.1