Przeglądaj źródła

add:菜单管理

zoie 2 tygodni temu
rodzic
commit
10495f6bf7

+ 23 - 0
src/api/menu/index.ts

@@ -0,0 +1,23 @@
+import $http from '../index'
+
+// 获取子系统列表
+export const User_Sys_List = (params: any) => $http.post('/user/Power/Sys_List', params)
+
+// 获取菜单列表
+export const Menu_List = (params: any) => $http.post('/user/Menu/List', params)
+
+// 获取用户菜单列表
+export const Menu_User_List = (params: any) => $http.post('/user/Menu/User_List', params)
+
+// 获取菜单详情
+export const Menu_Get = (params: any) => $http.post('/user/Menu/Get', params)
+
+// 添加菜单
+export const Menu_Add = (params: any) => $http.post('/user/Menu/Add', params)
+
+// 编辑菜单
+export const Menu_Edit = (params: any) => $http.post('/user/Menu/Edit', params)
+
+// 删除菜单
+export const Menu_Del = (params: any) => $http.post('/user/Menu/Del', params)
+

+ 948 - 0
src/views/account/menu/Menu.vue

@@ -0,0 +1,948 @@
+<script setup lang="ts">
+import {
+  User_Sys_List,
+  Menu_List,
+  Menu_Get,
+  Menu_Add,
+  Menu_Edit,
+  Menu_Del
+} from '@/api/menu/index'
+import { GlobalStore } from '@/stores/index'
+import { ref, reactive, nextTick, onMounted, computed } from 'vue'
+import Drawer from '@/components/Drawer/index.vue'
+import Dialog from '@/components/dialog/Dialog.vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { Edit, Delete, Plus, Search, ArrowUp, ArrowDown } from '@element-plus/icons-vue'
+import { ElNotification, ElMessageBox, ElMessage, TabsPaneContext } from 'element-plus'
+import { icons } from '@/utils/common'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+
+interface InSys {
+  T_sys: string
+  T_name: string
+}
+
+interface Menu {
+  Id: number
+  T_mid: number
+  T_name: string
+  T_permission: string
+  T_icon: string
+  T_uri: string
+  T_type: 'M' | 'B'
+  T_sort: number
+  T_State: number
+  Children?: Menu[]
+  apis?: API[]
+}
+
+interface API {
+  Id: number
+  T_Menu_Id: number
+  T_name: string
+  T_uri: string
+  T_method: string
+  T_enable: number
+}
+
+interface MenuWithAPIs extends Menu {
+  apis?: API[]
+}
+
+const globalStore = GlobalStore()
+const User_tokey = globalStore.GET_User_tokey
+const formLabelWidth = ref('120px')
+const ruleFormRef = ref<FormInstance>()
+const apiFormRef = ref<FormInstance>()
+const drawerRef = ref<InstanceType<typeof Drawer> | null>(null)
+const dialogRef = ref<InstanceType<typeof Dialog> | null>(null)
+const tableRef = ref()
+const iconDialogVisible = ref(false)
+
+const SysList = ref<InSys[]>([])
+const tableKey = ref(0) // 用于强制重新渲染表格
+const menuDir = reactive({
+  activeName: null as string | null, // 选中的tabs
+  MenuData: [] as InSys[], // tabs导航栏
+  menuTree: [] as Menu[], // 菜单树
+  defaultExpandedKeys: [] as number[], // 默认展开的节点
+  isExpanded: false, // 是否展开所有节点(默认折叠)
+  expandedKeys: [] as number[] // 当前展开的节点ID列表
+})
+
+// 图标选择器相关
+const iconType = ref<'element' | 'custom'>('element')
+const selectedIcon = ref('')
+const iconSearchText = ref('')
+
+// Element Plus 图标列表
+const elementIcons = computed(() => {
+  const iconList = Object.keys(ElementPlusIconsVue)
+  if (iconSearchText.value) {
+    return iconList.filter(icon => 
+      icon.toLowerCase().includes(iconSearchText.value.toLowerCase())
+    )
+  }
+  return iconList
+})
+
+// 自定义图标列表
+const customIcons = computed(() => {
+  const iconList = Object.keys(icons)
+  if (iconSearchText.value) {
+    return iconList.filter(icon => 
+      icon.toLowerCase().includes(iconSearchText.value.toLowerCase())
+    )
+  }
+  return iconList
+})
+
+
+const isNew = ref(true)
+const currentMenu = ref<Menu | null>(null)
+const currentParentId = ref<number>(0)
+const originalApis = ref<Array<{ T_name: string; T_uri: string; T_method: string }>>([])
+
+// 菜单表单
+const menuForm = reactive({
+  T_name: '',
+  T_mid: 0,
+  T_permission: '',
+  T_icon: '',
+  T_uri: '',
+  T_type: 'M' as 'M' | 'B',
+  T_sort: 0
+})
+
+// API表单
+const apiForm = reactive({
+  apis: [] as Array<{ T_name: string; T_uri: string; T_method: string }>
+})
+
+const menuRules = reactive<FormRules>({
+  T_name: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
+  T_code: [{ required: true, message: '请选择系统', trigger: 'change' }]
+})
+
+// 获取子系统列表
+const getSysList = async () => {
+  const { Data } = await User_Sys_List({ User_tokey })
+  menuDir.MenuData = (Data as InSys[]) || []
+  if (menuDir.MenuData && menuDir.MenuData.length > 0) {
+    menuDir.activeName = menuDir.MenuData[0].T_sys
+    await getMenuList(menuDir.activeName)
+  }
+}
+
+// 提取所有菜单ID
+const extractAllIds = (menus: Menu[]): number[] => {
+  let ids: number[] = []
+  menus.forEach(menu => {
+    ids.push(menu.Id)
+    if (menu.Children && menu.Children.length > 0) {
+      ids = ids.concat(extractAllIds(menu.Children))
+    }
+  })
+  return ids
+}
+
+// 提取所有菜单行(扁平化)
+const extractAllRows = (menus: Menu[]): Menu[] => {
+  let rows: Menu[] = []
+  menus.forEach(menu => {
+    rows.push(menu)
+    if (menu.Children && menu.Children.length > 0) {
+      rows = rows.concat(extractAllRows(menu.Children))
+    }
+  })
+  return rows
+}
+
+// 一键折叠/展开
+const toggleExpandAll = () => {
+  // 切换展开状态
+  menuDir.isExpanded = !menuDir.isExpanded
+  
+  // 更新 tableKey 强制重新渲染表格,这样 default-expand-all 会重新生效
+  tableKey.value++
+}
+
+// 获取菜单列表
+const getMenuList = async (T_code: string) => {
+  try {
+    const { Data } = await Menu_List({ T_code })
+    if (Data && (Data as any).Data) {
+      menuDir.menuTree = (Data as any).Data as Menu[]
+      // 默认折叠所有节点
+      menuDir.defaultExpandedKeys = []
+      menuDir.expandedKeys = []
+      // 重置展开状态为折叠
+      menuDir.isExpanded = false
+    }
+  } catch (error) {
+    console.error('获取菜单列表失败:', error)
+  }
+}
+
+// 点击切换tabs
+const handleClick = async (tab: TabsPaneContext) => {
+  menuDir.activeName = tab.props.name as string
+  await getMenuList(menuDir.activeName)
+}
+
+// 打开添加菜单抽屉
+const openAddDrawer = (parentMenu?: Menu) => {
+  isNew.value = true
+  currentMenu.value = null
+  currentParentId.value = parentMenu ? parentMenu.Id : 0
+  
+  // 重置表单
+  Object.assign(menuForm, {
+    T_name: '',
+    T_mid: currentParentId.value,
+    T_permission: '',
+    T_icon: '',
+    T_uri: '',
+    T_type: 'M',
+    T_sort: 0
+  })
+  apiForm.apis = []
+  originalApis.value = []
+  
+  nextTick(() => {
+    drawerRef.value?.openDrawer()
+  })
+}
+
+// 打开编辑菜单抽屉
+const openEditDrawer = async (menu: Menu) => {
+  isNew.value = false
+  currentMenu.value = menu
+  
+  try {
+    // 获取菜单详情(包含API)
+    const { Data } = await Menu_Get({ 
+      id: menu.Id, 
+      T_code: menuDir.activeName || '' 
+    })
+    
+      if (Data && (Data as any).Data) {
+        const menuDetail = (Data as any).Data as MenuWithAPIs
+        // 处理图标:如果后端返回的图标包含 \,需要保留用于提交,但展示时去掉
+        const iconValue = menuDetail.T_icon || ''
+        Object.assign(menuForm, {
+          T_name: menuDetail.T_name,
+          T_mid: menuDetail.T_mid,
+          T_permission: menuDetail.T_permission || '',
+          T_icon: iconValue, // 保留原始值(可能包含 \)
+          T_uri: menuDetail.T_uri || '',
+          T_type: menuDetail.T_type,
+          T_sort: menuDetail.T_sort
+        })
+      
+      // 设置API列表
+      if (menuDetail.apis && menuDetail.apis.length > 0) {
+        apiForm.apis = menuDetail.apis.map((api: API) => ({
+          T_name: api.T_name,
+          T_uri: api.T_uri,
+          T_method: api.T_method
+        }))
+        // 保存原始API列表,用于判断是否修改
+        originalApis.value = JSON.parse(JSON.stringify(apiForm.apis))
+      } else {
+        apiForm.apis = []
+        originalApis.value = []
+      }
+    }
+  } catch (error) {
+    console.error('获取菜单详情失败:', error)
+    ElMessage.error('获取菜单详情失败')
+  }
+  
+  nextTick(() => {
+    drawerRef.value?.openDrawer()
+  })
+}
+
+// 添加API行
+const addApiRow = () => {
+  apiForm.apis.push({
+    T_name: '',
+    T_uri: '',
+    T_method: 'POST'
+  })
+}
+
+// 删除API行
+const removeApiRow = (index: number) => {
+  apiForm.apis.splice(index, 1)
+}
+
+// 提交菜单(添加/编辑)
+const submitMenu = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  
+  formEl.validate(async valid => {
+    if (valid) {
+      try {
+        const params: any = {
+          ...menuForm,
+          T_code: menuDir.activeName
+        }
+        
+        let res: any
+        if (isNew.value) {
+          // 添加菜单时,如果有API,转换为JSON字符串
+          if (apiForm.apis && apiForm.apis.length > 0) {
+            const validApis = apiForm.apis.filter(
+              api => api.T_name && api.T_uri
+            )
+            if (validApis.length > 0) {
+              params.T_apis = JSON.stringify(validApis)
+            }
+          }
+          // 添加菜单时,必须传递 T_mid
+          res = await Menu_Add(params)
+        } else {
+          // 编辑菜单时,不传递 T_mid(除非需要修改父级)
+          params.id = currentMenu.value?.Id
+          // 如果父级ID没有变化,可以不传递 T_mid
+          if (params.T_mid === currentMenu.value?.T_mid) {
+            delete params.T_mid
+          }
+          
+          // 编辑菜单时,只有API发生变化时才传递 T_apis
+          if (apiForm.apis && apiForm.apis.length > 0) {
+            const validApis = apiForm.apis.filter(
+              api => api.T_name && api.T_uri
+            )
+            // 检查API是否发生变化
+            const apisChanged = JSON.stringify(validApis.sort()) !== JSON.stringify(originalApis.value.sort())
+            if (apisChanged && validApis.length > 0) {
+              params.T_apis = JSON.stringify(validApis)
+            }
+          } else if (originalApis.value.length > 0) {
+            // 如果原来有API,现在清空了,需要传递空数组来删除
+            params.T_apis = JSON.stringify([])
+          }
+          
+          res = await Menu_Edit(params)
+        }
+        
+        if (res.Code === 200) {
+          ElNotification.success({
+            title: isNew.value ? '添加菜单' : '编辑菜单',
+            message: '操作成功!',
+            position: 'bottom-right'
+          })
+          
+          // 检查用户权限,如果 T_menu === "*" 则刷新页面
+          const userInfo = globalStore.GET_User_Info
+          if (userInfo?.Power?.T_menu === '*') {
+            // 延迟刷新,确保通知显示
+            setTimeout(() => {
+              window.location.reload()
+            }, 500)
+            return
+          }
+          
+          nextTick(() => {
+            drawerRef.value?.closeDrawer()
+            getMenuList(menuDir.activeName || '')
+            resetForm(ruleFormRef.value)
+            isNew.value = true
+          })
+        }
+      } catch (error) {
+        console.error('操作失败:', error)
+      }
+    }
+  })
+}
+
+// 删除菜单
+const deleteMenu = (menu: Menu) => {
+  ElMessageBox.confirm('您确定要删除该菜单吗?', '警告', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      try {
+        const res: any = await Menu_Del({ 
+          id: menu.Id, 
+          T_code: menuDir.activeName 
+        })
+        if (res.Code === 200) {
+          ElMessage.success('删除成功!')
+          nextTick(() => {
+            getMenuList(menuDir.activeName || '')
+          })
+        }
+      } catch (error) {
+        console.error('删除失败:', error)
+      }
+    })
+    .catch(() => {
+      ElMessage.info('已取消删除')
+    })
+}
+
+// 重置表单
+const resetForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  formEl.resetFields()
+  Object.assign(menuForm, {
+    T_name: '',
+    T_mid: 0,
+    T_permission: '',
+    T_icon: '',
+    T_uri: '',
+    T_type: 'M',
+    T_sort: 0
+  })
+  apiForm.apis = []
+  originalApis.value = []
+}
+
+// 抽屉关闭回调
+const callbackDrawer = (done: () => void) => {
+  resetForm(ruleFormRef.value)
+  done()
+}
+
+// 判断是否为 Element Plus 图标
+const isElementIcon = (icon: string) => {
+  if (!icon) return false
+  return !/ue/g.test(icon)
+}
+
+// 打开图标选择器
+const openIconPicker = () => {
+  // 展示时去掉 \ 前缀
+  selectedIcon.value = processIconForDisplay(menuForm.T_icon)
+  iconDialogVisible.value = true
+  iconSearchText.value = ''
+}
+
+// 选择图标
+const selectIcon = (icon: string) => {
+  selectedIcon.value = icon
+  // 如果是自定义图标(不是 Element Plus 图标,也不是 icon- 开头的),需要加上 \ 传递给后端
+  if (!isElementIcon(icon) && !icon.startsWith('icon-')) {
+    // 如果图标在 icons 对象中,需要加上 \ 前缀
+    if (icons[icon]) {
+      menuForm.T_icon = '\\' + icon
+    } else {
+      menuForm.T_icon = icon
+    }
+  } else {
+    menuForm.T_icon = icon
+  }
+  iconDialogVisible.value = false
+}
+
+// 处理图标值(去掉 \ 前缀用于展示)
+const processIconForDisplay = (icon: string): string => {
+  if (!icon) return ''
+  // 如果图标值以 \ 开头,去掉 \ 用于展示
+  if (icon.startsWith('\\') && icon.length > 1) {
+    return icon.substring(1)
+  }
+  return icon
+}
+
+// 渲染图标
+const renderIcon = (icon: string) => {
+  if (!icon) return ''
+  const displayIcon = processIconForDisplay(icon)
+  
+  if (isElementIcon(displayIcon)) {
+    const IconComponent = (ElementPlusIconsVue as any)[displayIcon]
+    return IconComponent ? IconComponent : null
+  } else {
+    // 如果是 \ 开头的,去掉 \ 后查找
+    return icons[displayIcon] || ''
+  }
+}
+
+// 初始化
+onMounted(() => {
+  getSysList()
+})
+</script>
+
+<template>
+  <div class="menu">
+    <el-card>
+      <template #header>
+        <div class="card-header">
+          <span>菜单管理</span>
+        </div>
+      </template>
+      
+      <el-tabs 
+        v-model="menuDir.activeName" 
+        class="demo-tabs" 
+        tab-position="left" 
+        @tab-click="handleClick"
+      >
+        <el-tab-pane 
+          v-for="item in menuDir.MenuData" 
+          :key="item.T_sys"
+          :label="item.T_name" 
+          :name="item.T_sys"
+        >
+          <div class="menu-table-container">
+            <div class="table-toolbar">
+              <el-button 
+                type="primary" 
+                :icon="Plus" 
+                @click="openAddDrawer()"
+              >
+                添加菜单
+              </el-button>
+              <el-button 
+                :icon="menuDir.isExpanded ? ArrowUp : ArrowDown"
+                @click="toggleExpandAll"
+              >
+                {{ menuDir.isExpanded ? '一键折叠' : '一键展开' }}
+              </el-button>
+            </div>
+            <el-table
+              ref="tableRef"
+              :key="`table-${tableKey}`"
+              :data="menuDir.menuTree"
+              row-key="Id"
+              :tree-props="{ children: 'Children' }"
+              :default-expand-all="menuDir.isExpanded"
+              style="width: 100%"
+            >
+              <el-table-column prop="T_name" label="菜单名称" width="200" show-overflow-tooltip>
+                <template #default="{ row }">
+                  <div class="menu-name-cell">
+                    <span class="menu-icon">
+                      <el-icon v-if="row.T_icon && isElementIcon(processIconForDisplay(row.T_icon))" style="font-size: 16px; margin-right: 8px;">
+                        <component :is="renderIcon(row.T_icon)" />
+                      </el-icon>
+                      <i 
+                        v-else-if="row.T_icon && processIconForDisplay(row.T_icon).startsWith('icon-')" 
+                        :class="['iconfont', processIconForDisplay(row.T_icon)]" 
+                        style="font-size: 16px; margin-right: 8px;"
+                      ></i>
+                      <i 
+                        v-else-if="row.T_icon" 
+                        class="iconfont" 
+                        style="font-size: 16px; margin-right: 8px;"
+                      >{{ renderIcon(row.T_icon) }}</i>
+                    </span>
+                    <span>{{ row.T_name }}</span>
+                  </div>
+                </template>
+              </el-table-column>
+              
+              <el-table-column prop="T_permission" label="权限" width="200" show-overflow-tooltip>
+                <template #default="{ row }">
+                  <span>{{ row.T_permission || '-' }}</span>
+                </template>
+              </el-table-column>
+              
+              <el-table-column prop="T_type" label="类型" width="120">
+                <template #default="{ row }">
+                  <el-tag v-if="row.T_type === 'M'" type="primary" size="small">菜单</el-tag>
+                  <el-tag v-else-if="row.T_type === 'B'" type="warning" size="small">按钮</el-tag>
+                  <span v-else>-</span>
+                </template>
+              </el-table-column>
+              
+              <el-table-column prop="T_sort" label="排序" width="100">
+                <template #default="{ row }">
+                  <span>{{ row.T_sort }}</span>
+                </template>
+              </el-table-column>
+              
+              <el-table-column prop="T_uri" label="路由路径" min-width="200" show-overflow-tooltip>
+                <template #default="{ row }">
+                  <span>{{ row.T_uri || '-' }}</span>
+                </template>
+              </el-table-column>
+              
+              <el-table-column label="操作" width="250" fixed="right">
+                <template #default="{ row }">
+                  <el-button 
+                    link 
+                    type="primary" 
+                    size="small" 
+                    :icon="Plus" 
+                    @click="openAddDrawer(row)"
+                  >
+                    添加子菜单
+                  </el-button>
+                  <el-button 
+                    link 
+                    type="primary" 
+                    size="small" 
+                    :icon="Edit" 
+                    @click="openEditDrawer(row)"
+                  >
+                    编辑
+                  </el-button>
+                  <el-button 
+                    link 
+                    type="danger" 
+                    size="small" 
+                    :icon="Delete" 
+                    @click="deleteMenu(row)"
+                  >
+                    删除
+                  </el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+
+    <!-- 添加/编辑菜单抽屉 -->
+    <Drawer ref="drawerRef" :handleClose="callbackDrawer" size="50%">
+      <template #header="{ params }">
+        <h4 :id="params.titleId" :class="params.titleClass">
+          {{ isNew ? '添加' : '编辑' }} - 菜单
+        </h4>
+      </template>
+      
+      <el-form 
+        ref="ruleFormRef" 
+        :model="menuForm" 
+        :rules="menuRules"
+        label-position="right"
+      >
+        <el-form-item label="菜单名称:" :label-width="formLabelWidth" prop="T_name">
+          <el-input 
+            v-model="menuForm.T_name" 
+            type="text" 
+            autocomplete="off" 
+            placeholder="请输入菜单名称" 
+          />
+        </el-form-item>
+        
+        <el-form-item label="父级菜单:" :label-width="formLabelWidth">
+          <el-input 
+            :value="currentParentId === 0 ? '顶级菜单' : `父级ID: ${currentParentId}`"
+            disabled
+          />
+        </el-form-item>
+        
+        <el-form-item label="权限标识:" :label-width="formLabelWidth">
+          <el-input 
+            v-model="menuForm.T_permission" 
+            type="text" 
+            autocomplete="off" 
+            placeholder="请输入权限标识" 
+          />
+        </el-form-item>
+        
+        <el-form-item label="图标:" :label-width="formLabelWidth">
+          <div class="icon-selector">
+            <el-input 
+              v-model="menuForm.T_icon" 
+              type="text" 
+              autocomplete="off" 
+              placeholder="请选择图标" 
+              readonly
+              style="width: 200px; margin-right: 10px;"
+            >
+              <template #prefix>
+                <el-icon v-if="menuForm.T_icon && isElementIcon(processIconForDisplay(menuForm.T_icon))" style="font-size: 16px;">
+                  <component :is="renderIcon(menuForm.T_icon)" />
+                </el-icon>
+                <i 
+                  v-else-if="menuForm.T_icon && processIconForDisplay(menuForm.T_icon).startsWith('icon-')" 
+                  :class="['iconfont', processIconForDisplay(menuForm.T_icon)]" 
+                  style="font-size: 16px;"
+                ></i>
+                <i 
+                  v-else-if="menuForm.T_icon" 
+                  class="iconfont" 
+                  style="font-size: 16px;"
+                >{{ renderIcon(menuForm.T_icon) }}</i>
+              </template>
+            </el-input>
+            <el-button type="primary" @click="openIconPicker">选择图标</el-button>
+          </div>
+        </el-form-item>
+        
+        <el-form-item label="路由路径:" :label-width="formLabelWidth">
+          <el-input 
+            v-model="menuForm.T_uri" 
+            type="text" 
+            autocomplete="off" 
+            placeholder="请输入路由路径" 
+          />
+        </el-form-item>
+        
+        <el-form-item label="菜单类型:" :label-width="formLabelWidth">
+          <el-radio-group v-model="menuForm.T_type">
+            <el-radio label="M">菜单</el-radio>
+            <el-radio label="B">按钮</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        
+        <el-form-item label="排序值:" :label-width="formLabelWidth">
+          <el-input-number 
+            v-model="menuForm.T_sort" 
+            :min="-999" 
+            :max="999"
+            placeholder="排序值,越小越靠前"
+          />
+        </el-form-item>
+        
+        <el-form-item label="关联API:" :label-width="formLabelWidth">
+          <div class="api-list">
+            <div 
+              v-for="(api, index) in apiForm.apis" 
+              :key="index" 
+              class="api-item"
+            >
+              <el-input 
+                v-model="api.T_name" 
+                placeholder="API名称" 
+                style="width: 30%; margin-right: 10px"
+              />
+              <el-input 
+                v-model="api.T_uri" 
+                placeholder="API路径" 
+                style="width: 40%; margin-right: 10px"
+              />
+              <el-select 
+                v-model="api.T_method" 
+                placeholder="请求方式"
+                style="width: 20%; margin-right: 10px"
+              >
+                <el-option label="GET" value="GET" />
+                <el-option label="POST" value="POST" />
+                <el-option label="PUT" value="PUT" />
+                <el-option label="DELETE" value="DELETE" />
+              </el-select>
+              <el-button 
+                type="danger" 
+                :icon="Delete" 
+                circle 
+                @click="removeApiRow(index)"
+              />
+            </div>
+            <el-button 
+              type="primary" 
+              :icon="Plus" 
+              @click="addApiRow"
+              style="margin-top: 10px"
+            >
+              添加API
+            </el-button>
+          </div>
+        </el-form-item>
+        
+        <el-form-item :label-width="formLabelWidth">
+          <el-button 
+            v-if="isNew" 
+            type="primary" 
+            @click="submitMenu(ruleFormRef)"
+          >
+            添加
+          </el-button>
+          <el-button 
+            v-else 
+            type="primary" 
+            @click="submitMenu(ruleFormRef)"
+          >
+            修改
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </Drawer>
+
+    <!-- 图标选择器对话框 -->
+    <el-dialog v-model="iconDialogVisible" title="选择图标" width="70%">
+      <div class="icon-picker">
+        <div class="icon-picker-header">
+          <el-radio-group v-model="iconType">
+            <el-radio label="element">Element Plus 图标</el-radio>
+            <el-radio label="custom">自定义图标</el-radio>
+          </el-radio-group>
+          <el-input
+            v-model="iconSearchText"
+            placeholder="搜索图标"
+            style="width: 200px; margin-left: 20px;"
+            clearable
+          >
+            <template #prefix>
+              <el-icon><Search /></el-icon>
+            </template>
+          </el-input>
+        </div>
+        
+        <div class="icon-list">
+          <!-- Element Plus 图标 -->
+          <template v-if="iconType === 'element'">
+            <div
+              v-for="icon in elementIcons"
+              :key="icon"
+              class="icon-item"
+              :class="{ active: selectedIcon === icon }"
+              @click="selectIcon(icon)"
+            >
+              <el-icon style="font-size: 24px;">
+                <component :is="(ElementPlusIconsVue as any)[icon]" />
+              </el-icon>
+              <div class="icon-name">{{ icon }}</div>
+            </div>
+          </template>
+          
+          <!-- 自定义图标 -->
+          <template v-else>
+            <div
+              v-for="icon in customIcons"
+              :key="icon"
+              class="icon-item"
+              :class="{ active: selectedIcon === icon }"
+              @click="selectIcon(icon)"
+            >
+              <i class="iconfont" style="font-size: 24px;">{{ icons[icon] }}</i>
+              <div class="icon-name">{{ icon }}</div>
+            </div>
+          </template>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped lang="scss">
+@import './index.scss';
+@import '@/styles/var.scss';
+
+.menu {
+  @include f-direction;
+  
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+  
+  .menu-table-container {
+    padding: 20px;
+    min-height: 400px;
+    width: 100%;
+    box-sizing: border-box;
+    
+    .table-toolbar {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 16px;
+    }
+    
+    :deep(.el-table) {
+      width: 100%;
+    }
+    
+    :deep(.el-table__body-wrapper) {
+      width: 100%;
+    }
+    
+    :deep(.el-table__header-wrapper) {
+      width: 100%;
+    }
+    
+    // 确保树形表格的展开/折叠按钮在左边
+    :deep(.el-table__expand-icon) {
+      margin-right: 8px;
+    }
+    
+    // 确保菜单名称单元格内容正确对齐
+    :deep(.el-table__cell) {
+      .cell {
+        display: flex;
+        align-items: center;
+      }
+    }
+    
+    .menu-name-cell {
+      display: flex;
+      align-items: center;
+      flex: 1;
+      
+      .menu-icon {
+        display: inline-flex;
+        align-items: center;
+        margin-right: 8px;
+      }
+    }
+  }
+  
+  .icon-selector {
+    display: flex;
+    align-items: center;
+  }
+  
+  .icon-picker {
+    .icon-picker-header {
+      display: flex;
+      align-items: center;
+      margin-bottom: 20px;
+    }
+    
+    .icon-list {
+      display: grid;
+      grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+      gap: 10px;
+      max-height: 500px;
+      overflow-y: auto;
+      padding: 10px;
+      
+      .icon-item {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        padding: 15px;
+        border: 1px solid #dcdfe6;
+        border-radius: 4px;
+        cursor: pointer;
+        transition: all 0.3s;
+        
+        &:hover {
+          border-color: #409eff;
+          background-color: #ecf5ff;
+        }
+        
+        &.active {
+          border-color: #409eff;
+          background-color: #ecf5ff;
+        }
+        
+        .icon-name {
+          margin-top: 8px;
+          font-size: 12px;
+          color: #606266;
+          text-align: center;
+          word-break: break-all;
+        }
+      }
+    }
+  }
+  
+  .api-list {
+    width: 100%;
+    
+    .api-item {
+      display: flex;
+      align-items: center;
+      margin-bottom: 10px;
+    }
+  }
+}
+</style>
+

+ 18 - 0
src/views/account/menu/index.scss

@@ -0,0 +1,18 @@
+.menu {
+  padding: 20px;
+  
+  .el-card {
+    min-height: 600px;
+  }
+  
+  .demo-tabs {
+    .el-tabs__content {
+      padding: 20px;
+    }
+    
+    :deep(.el-tab-pane) {
+      width: 100%;
+    }
+  }
+}
+

+ 42 - 76
src/views/account/roles/Roles.vue

@@ -16,7 +16,7 @@ import { ElTree } from 'element-plus'
 import TableBase from '@/components/TableBase/index.vue'
 import type { FormInstance, FormRules } from 'element-plus'
 import type { ColumnProps } from '@/components/TableBase/interface/index'
-import { Edit, Delete, Operation } from '@element-plus/icons-vue'
+import { Edit, Delete, Operation, ArrowUp, ArrowDown } from '@element-plus/icons-vue'
 import { ElNotification, ElMessageBox, ElMessage,TabsPaneContext  } from 'element-plus'
 
 interface InSys {
@@ -26,10 +26,8 @@ interface InSys {
   checkList: string[]
 }
 
-const menuMap = new Map()
 const isNew = ref(true)
 let currentVal: any = {}
-const SysList = ref<InSys[]>([])
 const globalStore = GlobalStore()
 const User_tokey = globalStore.GET_User_tokey
 const formLabelWidth = ref('100px')
@@ -59,7 +57,8 @@ const menuDir:any = reactive({
   MenuData:[],//tabs导航栏
   tabsMenu:[],//tab展开导航
   defaultKeys:[],//默认选中
-  selectData:[]//勾选项
+  selectData:[],//勾选项
+  isExpanded: false // 是否展开所有节点
 })
 const getSysList = async () => {
   const { Data } = await User_Sys_List({ User_tokey })
@@ -77,7 +76,6 @@ const handleClick = async(tab: TabsPaneContext, event: Event) => {
   const {Data:resIt} = await getMenuList(menuDir.activeName)
   menuDir.tabsMenu = resIt.Menu
   menuDir.defaultKeys = resIt.Menu_checked?resIt.Menu_checked:[]
-
 }
 
 //调用当前权限的子选项
@@ -89,29 +87,6 @@ const getMenuList = async (code: string) => {
   }
 }
 
-const permissionArr = ref<string[]>([])
-// const getPermissionArr = (menu: any) => {
-//   const { Menu, Menu_checked } = menu
-//   const { Children, T_permission } = Menu[0]
-//   getCurrentFlatMenu(Children, Menu_checked, T_permission)
-// }
-
-// const getCurrentFlatMenu = (children: any[], arr: number[], permission: string) => {
-//   const fatherMenu = menuMap.get(permission)
-
-//   children.forEach((item: any) => {
-//     const index = arr.findIndex((num: any) => num === item.Id)
-//     if (item.Children) {
-//       getCurrentFlatMenu(item.Children, arr, permission)
-//     }
-//     if (index !== -1 && !item.Children) {
-//       permissionArr.value.push(item.T_permission)
-//       fatherMenu.children.push(item)
-//     }
-//   })
-// }
-const setCheckedTreeKeys = (arr: string[]) => treeRef.value?.setCheckedKeys(arr, true)
-
 // 添加角色
 const form = reactive({
   name: '',
@@ -196,8 +171,8 @@ const searchHandle = () => {
 }
 
 // 重写角色权限
-const treeRef = ref<InstanceType<typeof ElTree>>()
-const menuList = ref<any[]>([])
+const treeRefMap = new Map<string, InstanceType<typeof ElTree>>()
+const treeKey = ref(0) // 用于强制重新渲染树组件
 
 /**
  * 返回拼接的 M + Id
@@ -234,49 +209,30 @@ const submit = async (data: any) => {
     // dialog.value?.DialogClose()
   }
 }
-let fatherDataCopy: any = {}
-// const getMenuChildren = (menuchildren: any, T_permission: string) => {
-//   let fatherData = getFatherData(T_permission)
-//   const { children } = fatherData
-
-//   if (!menuchildren?.length) {
-//     const index = children.findIndex((child: any) => child.T_permission === menuchildren.T_permission)
-//     if (index === -1) children.push({ ...menuchildren })
-//   } else {
-//     menuchildren.forEach((item: any) => {
-//       if (item.Children) {
-//         getMenuChildren(item, T_permission)
-//       } else {
-//         const index = children.findIndex((child: any) => item.T_permission === child.T_permission)
-//         if (index === -1) children.push({ ...item })
-//       }
-//     })
-//   }
-// }
-const getFatherData = (T_permission: string) => {
-  let fatherData = menuMap.get(T_permission)
-  if (['/base', '/property', '/inventory', '/salesManage'].includes(T_permission)) {
-    fatherData = menuMap.get('/stock')
-  }
-  return fatherData
-}
-
-const extractIds = (obj:any)=>{
-    let ids = [obj.Id]; // 初始化数组,包含当前对象的Id  
-    // 检查Children是否存在且为数组  
-    if (Array.isArray(obj.Children)) {  
-        // 遍历Children数组  
-        obj.Children.forEach((child:any) => {  
-            // 递归调用extractIds,并将返回的ids数组与当前ids数组合并  
-            ids = ids.concat(extractIds(child));  
-        });  
-    }  
 
-    return ids; 
+/**
+ * 一键展开/折叠所有节点
+ */
+const toggleExpandAll = () => {
+  menuDir.isExpanded = !menuDir.isExpanded
+  // 更新 treeKey 强制重新渲染树组件,这样 default-expand-all 会重新生效
+  treeKey.value++
+  
+  // 重新渲染后需要重新设置勾选状态
+  nextTick(() => {
+    setTimeout(() => {
+      const currentTreeRef = treeRefMap.get(menuDir.activeName)
+      if (currentTreeRef && menuDir.defaultKeys.length > 0) {
+        currentTreeRef.setCheckedKeys(menuDir.defaultKeys, false)
+      }
+    }, 50)
+  })
 }
 
-
 const checkChange = (data: any, check:any) => {
+    // 获取所有被勾选的节点(包括半选节点和全选节点)
+    // checkedKeys 只包含全选节点,halfCheckedKeys 包含半选节点
+    // 提交时需要所有全选的节点
     menuDir.defaultKeys = check!.checkedKeys
 }
 
@@ -285,9 +241,8 @@ const checkChange = (data: any, check:any) => {
  */
 const dialogCloseCallback = () => {
   nextTick(() => {
-    menuMap.clear()
-    permissionArr.value = []
-    fatherDataCopy = {}
+    treeRefMap.clear()
+    menuDir.isExpanded = false // 重置展开状态
   })
 }
 
@@ -317,16 +272,27 @@ const dialogCloseCallback = () => {
     </TableBase>
     <Dialog ref="dialog" width="50%" :handleClose="dialogCloseCallback" draggable>
       <template #header>
-        <h3>编辑权限</h3>
+        <div style="display: flex; justify-content: space-between; align-items: center;">
+          <h3 style="margin: 0;">编辑权限</h3>
+          <el-button 
+            :icon="menuDir.isExpanded ? ArrowUp : ArrowDown"
+            @click="toggleExpandAll"
+            size="small"
+          >
+            {{ menuDir.isExpanded ? '一键折叠' : '一键展开' }}
+          </el-button>
+        </div>
       </template>
       <el-tabs v-model="menuDir.activeName" class="demo-tabs" tabPosition="left" @tab-click="handleClick">
         <el-tab-pane :label="item.T_name" :name="item.T_sys" v-for="item,index in menuDir.MenuData" :key="index">
           <el-tree
-            :data="menuDir.tabsMenu"
+            :ref="(el: any) => { if (el) treeRefMap.set(item.T_sys, el) }"
+            :key="`tree-${item.T_sys}-${treeKey}`"
+            :data="item.T_sys === menuDir.activeName ? menuDir.tabsMenu : []"
             :props="{ label: 'T_name', children: 'Children' }"
             node-key="Id"
-            :default-expanded-keys="menuDir.defaultKeys"
-            :default-checked-keys="menuDir.defaultKeys"
+            :default-expand-all="menuDir.isExpanded"
+            :default-checked-keys="item.T_sys === menuDir.activeName ? menuDir.defaultKeys : []"
             show-checkbox
             @check="checkChange"
           />