增加了tabs
This commit is contained in:
		
							parent
							
								
									ddf90424ba
								
							
						
					
					
						commit
						8430eb509c
					
				| @ -46,194 +46,202 @@ | ||||
|   </el-aside> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { ref, computed, onMounted } from "vue"; | ||||
| import { useRouter, useRoute } from "vue-router"; | ||||
| import { useAllDataStore } from "@/stores"; | ||||
| import { getAllMenus } from "@/api/menu"; | ||||
| <script setup> | ||||
| import { ref, computed, onMounted, defineEmits } from 'vue'; | ||||
| import { useRouter, useRoute } from 'vue-router'; | ||||
| import { useAllDataStore } from '@/stores'; | ||||
| import { getAllMenus } from '@/api/menu'; | ||||
| 
 | ||||
| export default { | ||||
|   name: "CommonAside", | ||||
|   setup() { | ||||
|     const router = useRouter(); | ||||
|     const route = useRoute(); | ||||
|     const list = ref([]); | ||||
|     const loading = ref(false); | ||||
| const emit = defineEmits(['menu-click']); | ||||
| 
 | ||||
|     const store = useAllDataStore(); | ||||
|     const isCollapse = computed(() => store.state.isCollapse); | ||||
|     const width = computed(() => store.state.isCollapse ? '64px' : '180px'); | ||||
| const router = useRouter(); | ||||
| const route = useRoute(); | ||||
| const list = ref([]); | ||||
| const loading = ref(false); | ||||
| 
 | ||||
|     // 主题颜色(用于 Element Plus 组件,需要响应式) | ||||
|     // 初始值设为可见的默认值,避免初始化时不可见 | ||||
|     const asideBgColor = ref('#0081ff'); | ||||
|     const asideTextColor = ref('#ffffff'); | ||||
| const store = useAllDataStore(); | ||||
| const isCollapse = computed(() => store.state.isCollapse); | ||||
| const width = computed(() => store.state.isCollapse ? '64px' : '180px'); | ||||
| const asideBgColor = ref('#0081ff'); | ||||
| const asideTextColor = ref('#ffffff'); | ||||
| 
 | ||||
|     // 更新主题颜色 | ||||
|     const updateThemeColors = () => { | ||||
|       try { | ||||
|         const root = document.documentElement; | ||||
|         const bgColor = getComputedStyle(root).getPropertyValue('--aside-bg-color').trim(); | ||||
|         const textColor = getComputedStyle(root).getPropertyValue('--aside-text-color').trim(); | ||||
| // 更新主题颜色 | ||||
| const updateThemeColors = () => { | ||||
|   try { | ||||
|     const root = document.documentElement; | ||||
|     const bgColor = getComputedStyle(root).getPropertyValue('--aside-bg-color').trim(); | ||||
|     const textColor = getComputedStyle(root).getPropertyValue('--aside-text-color').trim(); | ||||
|      | ||||
|         // 只有当获取到有效值时才更新 | ||||
|         if (bgColor) { | ||||
|           asideBgColor.value = bgColor; | ||||
|     // 只有当获取到有效值时才更新 | ||||
|     if (bgColor) { | ||||
|       asideBgColor.value = bgColor; | ||||
|     } | ||||
|     if (textColor) { | ||||
|       asideTextColor.value = textColor; | ||||
|     } | ||||
|   } catch (e) { | ||||
|     console.warn('更新主题颜色失败:', e); | ||||
|     // 出错时保持默认值 | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // 将后端数据转换为前端需要的格式 | ||||
| const transformMenuData = (menus) => { | ||||
|   // 首先映射字段格式 | ||||
|   const mappedMenus = menus.map(menu => ({ | ||||
|     id: menu.id, | ||||
|     path: menu.path, | ||||
|     icon: menu.icon || 'fa-circle', | ||||
|     label: menu.name, | ||||
|     route: menu.path, | ||||
|     parentId: menu.parentId || 0, | ||||
|     order: menu.order || 0, | ||||
|     children: [] | ||||
|   })); | ||||
| 
 | ||||
|   // 构建树形结构 | ||||
|   const menuMap = new Map(); | ||||
|   const rootMenus = []; | ||||
| 
 | ||||
|   // 先创建所有菜单的映射 | ||||
|   mappedMenus.forEach(menu => { | ||||
|     menuMap.set(menu.id, menu); | ||||
|   }); | ||||
| 
 | ||||
|   // 构建树形结构 | ||||
|   mappedMenus.forEach(menu => { | ||||
|     if (menu.parentId === 0) { | ||||
|       rootMenus.push(menu); | ||||
|     } else { | ||||
|       const parent = menuMap.get(menu.parentId); | ||||
|       if (parent) { | ||||
|         if (!parent.children) { | ||||
|           parent.children = []; | ||||
|         } | ||||
|         if (textColor) { | ||||
|           asideTextColor.value = textColor; | ||||
|         } | ||||
|       } catch (e) { | ||||
|         console.warn('更新主题颜色失败:', e); | ||||
|         // 出错时保持默认值 | ||||
|         parent.children.push(menu); | ||||
|       } else { | ||||
|         // 如果找不到父节点,作为根节点处理 | ||||
|         rootMenus.push(menu); | ||||
|       } | ||||
|     }; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|     // 将后端数据转换为前端需要的格式 | ||||
|     const transformMenuData = (menus) => { | ||||
|       // 首先映射字段格式 | ||||
|       const mappedMenus = menus.map(menu => ({ | ||||
|         id: menu.id, | ||||
|         path: menu.path, | ||||
|         icon: menu.icon || 'fa-circle', | ||||
|         label: menu.name, | ||||
|         route: menu.path, | ||||
|         parentId: menu.parentId || 0, | ||||
|         order: menu.order || 0, | ||||
|         children: [] | ||||
|       })); | ||||
|   // 按 order 排序(确保排序正确) | ||||
|   const sortMenus = (menus) => { | ||||
|     if (!menus || menus.length === 0) return; | ||||
|      | ||||
|       // 构建树形结构 | ||||
|       const menuMap = new Map(); | ||||
|       const rootMenus = []; | ||||
|     // 对当前层级的菜单进行排序 | ||||
|     menus.sort((a, b) => { | ||||
|       const orderA = Number(a.order) ?? 999999; // 没有order的排在最后 | ||||
|       const orderB = Number(b.order) ?? 999999; | ||||
|       const diff = orderA - orderB; | ||||
|        | ||||
|       // 先创建所有菜单的映射 | ||||
|       mappedMenus.forEach(menu => { | ||||
|         menuMap.set(menu.id, menu); | ||||
|       }); | ||||
| 
 | ||||
|       // 构建树形结构 | ||||
|       mappedMenus.forEach(menu => { | ||||
|         if (menu.parentId === 0) { | ||||
|           rootMenus.push(menu); | ||||
|         } else { | ||||
|           const parent = menuMap.get(menu.parentId); | ||||
|           if (parent) { | ||||
|             if (!parent.children) { | ||||
|               parent.children = []; | ||||
|             } | ||||
|             parent.children.push(menu); | ||||
|           } else { | ||||
|             // 如果找不到父节点,作为根节点处理 | ||||
|             rootMenus.push(menu); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       // 按 order 排序(确保排序正确) | ||||
|       const sortMenus = (menus) => { | ||||
|         if (!menus || menus.length === 0) return; | ||||
|          | ||||
|         // 对当前层级的菜单进行排序 | ||||
|         menus.sort((a, b) => { | ||||
|           const orderA = Number(a.order) ?? 999999; // 没有order的排在最后 | ||||
|           const orderB = Number(b.order) ?? 999999; | ||||
|           const diff = orderA - orderB; | ||||
|            | ||||
|           // 如果 order 相同,按 id 排序(保证稳定性) | ||||
|           if (diff === 0) { | ||||
|             return (a.id || 0) - (b.id || 0); | ||||
|           } | ||||
|            | ||||
|           return diff; | ||||
|         }); | ||||
|          | ||||
|         // 递归排序子菜单 | ||||
|         menus.forEach(menu => { | ||||
|           if (menu.children && menu.children.length > 0) { | ||||
|             sortMenus(menu.children); | ||||
|           } | ||||
|         }); | ||||
|       }; | ||||
| 
 | ||||
|       // 先排序根菜单 | ||||
|       sortMenus(rootMenus); | ||||
|        | ||||
|       // console.log('排序后的根菜单:', rootMenus.map(m => ({ name: m.label, order: m.order }))); | ||||
|        | ||||
|       return rootMenus; | ||||
|     }; | ||||
| 
 | ||||
|     // 获取菜单数据 | ||||
|     const fetchMenus = async () => { | ||||
|       loading.value = true; | ||||
|       try { | ||||
|         // 优先从 localStorage 读取 | ||||
|         const cachedMenus = localStorage.getItem('menuData'); | ||||
|         let menuData = null; | ||||
|          | ||||
|         if (cachedMenus) { | ||||
|           try { | ||||
|             menuData = JSON.parse(cachedMenus); | ||||
|             // console.log('从缓存读取菜单数据'); | ||||
|           } catch (e) { | ||||
|             console.warn('缓存菜单数据解析失败:', e); | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         // 如果缓存中没有或解析失败,从接口获取 | ||||
|         if (!menuData) { | ||||
|           const res = await getAllMenus(); | ||||
|           if (res && res.success && res.data) { | ||||
|             menuData = res.data; | ||||
|             // 保存到缓存 | ||||
|             localStorage.setItem('menuData', JSON.stringify(menuData)); | ||||
|           } else { | ||||
|             console.error('获取菜单失败:', res?.message || '未知错误'); | ||||
|             list.value = []; | ||||
|             loading.value = false; | ||||
|             return; | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         // 转换并排序菜单数据 | ||||
|         const transformedMenus = transformMenuData(menuData); | ||||
|         // console.log('转换后的菜单数据:', transformedMenus); | ||||
|         // console.log('菜单顺序(按 order 排序):', transformedMenus.map(m => ({ name: m.label, order: m.order, id: m.id }))); | ||||
|         list.value = transformedMenus; | ||||
|       } catch (error) { | ||||
|         console.error('获取菜单异常:', error); | ||||
|         list.value = []; | ||||
|       } finally { | ||||
|         loading.value = false; | ||||
|       // 如果 order 相同,按 id 排序(保证稳定性) | ||||
|       if (diff === 0) { | ||||
|         return (a.id || 0) - (b.id || 0); | ||||
|       } | ||||
|     }; | ||||
|        | ||||
|     // 组件挂载时获取菜单 | ||||
|     onMounted(() => { | ||||
|       // 先更新主题颜色,确保组件可见 | ||||
|       updateThemeColors(); | ||||
|        | ||||
|       // 延迟一点获取菜单,确保主题已初始化 | ||||
|       setTimeout(() => { | ||||
|         fetchMenus(); | ||||
|       }, 100); | ||||
|        | ||||
|       // 监听主题变化事件 | ||||
|       window.addEventListener('theme-change', updateThemeColors); | ||||
|        | ||||
|       // 监听 CSS 变量变化(MutationObserver) | ||||
|       const observer = new MutationObserver(updateThemeColors); | ||||
|       observer.observe(document.documentElement, { | ||||
|         attributes: true, | ||||
|         attributeFilter: ['data-theme'] | ||||
|       }); | ||||
|       return diff; | ||||
|     }); | ||||
|      | ||||
|     // 计算属性:统一排序所有菜单项(不再区分有无子菜单) | ||||
|     const sortedMenuList = computed(() => { | ||||
|       // 创建副本并排序,确保按 order 排序 | ||||
|       const sorted = [...list.value].sort((a, b) => { | ||||
|     // 递归排序子菜单 | ||||
|     menus.forEach(menu => { | ||||
|       if (menu.children && menu.children.length > 0) { | ||||
|         sortMenus(menu.children); | ||||
|       } | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   // 先排序根菜单 | ||||
|   sortMenus(rootMenus); | ||||
|    | ||||
|   // console.log('排序后的根菜单:', rootMenus.map(m => ({ name: m.label, order: m.order }))); | ||||
|    | ||||
|   return rootMenus; | ||||
| }; | ||||
| 
 | ||||
| // 获取菜单数据 | ||||
| const fetchMenus = async () => { | ||||
|   loading.value = true; | ||||
|   try { | ||||
|     // 优先从 localStorage 读取 | ||||
|     const cachedMenus = localStorage.getItem('menuData'); | ||||
|     let menuData = null; | ||||
|      | ||||
|     if (cachedMenus) { | ||||
|       try { | ||||
|         menuData = JSON.parse(cachedMenus); | ||||
|         // console.log('从缓存读取菜单数据'); | ||||
|       } catch (e) { | ||||
|         console.warn('缓存菜单数据解析失败:', e); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // 如果缓存中没有或解析失败,从接口获取 | ||||
|     if (!menuData) { | ||||
|       const res = await getAllMenus(); | ||||
|       if (res && res.success && res.data) { | ||||
|         menuData = res.data; | ||||
|         // 保存到缓存 | ||||
|         localStorage.setItem('menuData', JSON.stringify(menuData)); | ||||
|       } else { | ||||
|         console.error('获取菜单失败:', res?.message || '未知错误'); | ||||
|         list.value = []; | ||||
|         loading.value = false; | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // 转换并排序菜单数据 | ||||
|     const transformedMenus = transformMenuData(menuData); | ||||
|     // console.log('转换后的菜单数据:', transformedMenus); | ||||
|     // console.log('菜单顺序(按 order 排序):', transformedMenus.map(m => ({ name: m.label, order: m.order, id: m.id }))); | ||||
|     list.value = transformedMenus; | ||||
|   } catch (error) { | ||||
|     console.error('获取菜单异常:', error); | ||||
|     list.value = []; | ||||
|   } finally { | ||||
|     loading.value = false; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // 组件挂载时获取菜单 | ||||
| onMounted(() => { | ||||
|   // 先更新主题颜色,确保组件可见 | ||||
|   updateThemeColors(); | ||||
|    | ||||
|   // 延迟一点获取菜单,确保主题已初始化 | ||||
|   setTimeout(() => { | ||||
|     fetchMenus(); | ||||
|   }, 100); | ||||
|    | ||||
|   // 监听主题变化事件 | ||||
|   window.addEventListener('theme-change', updateThemeColors); | ||||
|    | ||||
|   // 监听 CSS 变量变化(MutationObserver) | ||||
|   const observer = new MutationObserver(updateThemeColors); | ||||
|   observer.observe(document.documentElement, { | ||||
|     attributes: true, | ||||
|     attributeFilter: ['data-theme'] | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| // 计算属性:统一排序所有菜单项(不再区分有无子菜单) | ||||
| const sortedMenuList = computed(() => { | ||||
|   // 创建副本并排序,确保按 order 排序 | ||||
|   const sorted = [...list.value].sort((a, b) => { | ||||
|     const orderA = Number(a.order) ?? 999999; | ||||
|     const orderB = Number(b.order) ?? 999999; | ||||
|     if (orderA === orderB) { | ||||
|       return (a.id || 0) - (b.id || 0); | ||||
|     } | ||||
|     return orderA - orderB; | ||||
|   }); | ||||
|    | ||||
|   // 确保子菜单也排序 | ||||
|   sorted.forEach(menu => { | ||||
|     if (menu.children && menu.children.length > 0) { | ||||
|       menu.children.sort((a, b) => { | ||||
|         const orderA = Number(a.order) ?? 999999; | ||||
|         const orderB = Number(b.order) ?? 999999; | ||||
|         if (orderA === orderB) { | ||||
| @ -241,60 +249,33 @@ export default { | ||||
|         } | ||||
|         return orderA - orderB; | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
|    | ||||
|       // 确保子菜单也排序 | ||||
|       sorted.forEach(menu => { | ||||
|         if (menu.children && menu.children.length > 0) { | ||||
|           menu.children.sort((a, b) => { | ||||
|             const orderA = Number(a.order) ?? 999999; | ||||
|             const orderB = Number(b.order) ?? 999999; | ||||
|             if (orderA === orderB) { | ||||
|               return (a.id || 0) - (b.id || 0); | ||||
|             } | ||||
|             return orderA - orderB; | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
|   return sorted; | ||||
| }); | ||||
| 
 | ||||
|       return sorted; | ||||
|     }); | ||||
| 
 | ||||
|     // 菜单点击事件处理 | ||||
|     const handleMenuSelect = (index) => { | ||||
|       const menuItem = findMenuItemByPath(list.value, index); | ||||
|       if (menuItem && menuItem.route) { | ||||
|         router.push(menuItem.route); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     // 递归查找菜单项 | ||||
|     const findMenuItemByPath = (menus, path) => { | ||||
|       for (const menu of menus) { | ||||
|         if (menu.path === path) { | ||||
|           return menu; | ||||
|         } | ||||
|         if (menu.children && menu.children.length > 0) { | ||||
|           const found = findMenuItemByPath(menu.children, path); | ||||
|           if (found) return found; | ||||
|         } | ||||
|       } | ||||
|       return null; | ||||
|     }; | ||||
| 
 | ||||
|     return { | ||||
|       list, | ||||
|       sortedMenuList, | ||||
|       store, | ||||
|       isCollapse, | ||||
|       width, | ||||
|       loading, | ||||
|       handleMenuSelect, | ||||
|       route, | ||||
|       asideBgColor, | ||||
|       asideTextColor | ||||
|     }; | ||||
| // 菜单点击事件:emit 通知父组件 | ||||
| const handleMenuSelect = (index) => { | ||||
|   const menuItem = findMenuItemByPath(list.value, index); | ||||
|   if (menuItem && menuItem.route) { | ||||
|     emit('menu-click', menuItem); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // 递归查找菜单项 | ||||
| const findMenuItemByPath = (menus, path) => { | ||||
|   for (const menu of menus) { | ||||
|     if (menu.path === path) { | ||||
|       return menu; | ||||
|     } | ||||
|     if (menu.children && menu.children.length > 0) { | ||||
|       const found = findMenuItemByPath(menu.children, path); | ||||
|       if (found) return found; | ||||
|     } | ||||
|   } | ||||
|   return null; | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style scoped lang="less"> | ||||
|  | ||||
| @ -1,24 +1,96 @@ | ||||
| import { defineStore } from 'pinia' | ||||
| import { ref, computed, reactive } from 'vue' | ||||
| import { defineStore } from 'pinia'; | ||||
| import { ref, computed, reactive } from 'vue'; | ||||
| 
 | ||||
| // 初始化state数据
 | ||||
| // ========== 全局状态 Store ==========
 | ||||
| function initState() { | ||||
|     return { | ||||
|         isCollapse: false, | ||||
|     }; | ||||
|   return { | ||||
|     isCollapse: false, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export const useAllDataStore = defineStore('allData', () => { | ||||
|     const state = reactive(initState()); | ||||
|     const count = ref(0); | ||||
|     const doubleCount = computed(() => count.value * 2); | ||||
|     function increment() { | ||||
|         count.value++; | ||||
|   const state = reactive(initState()); | ||||
|   const count = ref(0); | ||||
|   const doubleCount = computed(() => count.value * 2); | ||||
|   function increment() { | ||||
|     count.value++; | ||||
|   } | ||||
|   return { | ||||
|     state, | ||||
|     count, | ||||
|     doubleCount, | ||||
|     increment, | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| // ========== 多标签页 Tabs Store ==========
 | ||||
| import { defineStore as defineTabsStore } from 'pinia'; | ||||
| import { ref as vueRef } from 'vue'; | ||||
| 
 | ||||
| /** | ||||
|  * 多标签页Tabs状态管理 | ||||
|  * tabList每个tab结构: { | ||||
|  *   title: 标签显示名, | ||||
|  *   fullPath: 路由路径(唯一key), | ||||
|  *   name: 路由name, | ||||
|  *   icon: 图标(可选) | ||||
|  * } | ||||
|  */ | ||||
| export const useTabsStore = defineTabsStore('tabs', () => { | ||||
|   // 固定首页tab
 | ||||
|   const defaultDashboardPath = '/dashboard'; | ||||
|   const tabList = vueRef([ | ||||
|     { title: '首页', fullPath: defaultDashboardPath, name: 'Dashboard' }, | ||||
|   ]); | ||||
|   const activeTab = vueRef(defaultDashboardPath); | ||||
| 
 | ||||
|   // 添加tab,若已存在则激活
 | ||||
|   function addTab(tab) { | ||||
|     const exist = tabList.value.find((t) => t.fullPath === tab.fullPath); | ||||
|     if (!exist) { | ||||
|       tabList.value.push(tab); | ||||
|     } | ||||
|     return { | ||||
|         state, | ||||
|         count, | ||||
|         doubleCount, | ||||
|         increment | ||||
|     activeTab.value = tab.fullPath; | ||||
|   } | ||||
| 
 | ||||
|   // 删除指定tab并切换激活tab
 | ||||
|   function removeTab(fullPath) { | ||||
|     const idx = tabList.value.findIndex((t) => t.fullPath === fullPath); | ||||
|     if (idx > -1) { | ||||
|       tabList.value.splice(idx, 1); | ||||
|       // 只在关闭当前激活tab时切换激活tab
 | ||||
|       if (activeTab.value === fullPath) { | ||||
|         if (tabList.value.length > 0) { | ||||
|           // 优先激活右侧(如无则激活左侧)
 | ||||
|           const newIdx = idx >= tabList.value.length ? tabList.value.length - 1 : idx; | ||||
|           activeTab.value = tabList.value[newIdx].fullPath; | ||||
|         } else { | ||||
|           // 全部关闭,兜底首页
 | ||||
|           activeTab.value = defaultDashboardPath; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| }) | ||||
|   } | ||||
| 
 | ||||
|   // 关闭其他,只留首页和当前激活tab
 | ||||
|   function closeOthers() { | ||||
|     tabList.value = tabList.value.filter( | ||||
|       (t) => t.fullPath === defaultDashboardPath || t.fullPath === activeTab.value | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   // 关闭全部,只留首页
 | ||||
|   function closeAll() { | ||||
|     tabList.value = tabList.value.filter((t) => t.fullPath === defaultDashboardPath); | ||||
|     activeTab.value = defaultDashboardPath; | ||||
|   } | ||||
| 
 | ||||
|   return { | ||||
|     tabList, | ||||
|     activeTab, | ||||
|     addTab, | ||||
|     removeTab, | ||||
|     closeOthers, | ||||
|     closeAll, | ||||
|   }; | ||||
| }); | ||||
| @ -1,18 +1,148 @@ | ||||
| <script setup> | ||||
| import CommonAside from "@/components/CommonAside.vue"; | ||||
| import CommonHeader from "@/components/CommonHeader.vue"; | ||||
| import CommonAside from '@/components/CommonAside.vue'; | ||||
| import CommonHeader from '@/components/CommonHeader.vue'; | ||||
| import { useTabsStore } from '@/stores'; | ||||
| import { useRouter, useRoute } from 'vue-router'; | ||||
| import { ref, watch, reactive, nextTick } from 'vue'; | ||||
| import { More, DArrowRight } from '@element-plus/icons-vue' | ||||
| 
 | ||||
| const tabsStore = useTabsStore(); | ||||
| const router = useRouter(); | ||||
| const route = useRoute(); | ||||
| const defaultDashboardPath = '/dashboard'; | ||||
| 
 | ||||
| // 1. 侧栏菜单点击:加入/激活Tab并切换路由 | ||||
| const handleAsideMenuClick = (menuItem) => { | ||||
|   tabsStore.addTab({ | ||||
|     title: menuItem.label, | ||||
|     fullPath: menuItem.path, | ||||
|     name: menuItem.label, | ||||
|     icon: menuItem.icon | ||||
|   }); | ||||
|   if (route.fullPath !== menuItem.path) { | ||||
|     router.push(menuItem.path); | ||||
|   } | ||||
| }; | ||||
| const tabsCloseTab = (targetKey) => { | ||||
|   tabsStore.removeTab(targetKey); | ||||
|   if (route.fullPath !== tabsStore.activeTab) { | ||||
|     router.push(tabsStore.activeTab); | ||||
|   } | ||||
| }; | ||||
| const closeOthers = () => { | ||||
|   tabsStore.closeOthers(); | ||||
|   if (!tabsStore.tabList.find(tab => tab.fullPath === route.fullPath)) { | ||||
|     router.push(tabsStore.activeTab); | ||||
|   } | ||||
| }; | ||||
| const closeAll = () => { | ||||
|   tabsStore.closeAll(); | ||||
|   router.push(defaultDashboardPath); | ||||
| }; | ||||
| // 主动监听tab激活,保证切tab时内容区切换 | ||||
| watch( | ||||
|   () => tabsStore.activeTab, | ||||
|   (newVal) => { | ||||
|     if (newVal && router.currentRoute.value.fullPath !== newVal) { | ||||
|       router.push(newVal); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
| 
 | ||||
| // ========== 右键菜单逻辑 ========== // | ||||
| const contextMenu = reactive({ | ||||
|   visible: false, | ||||
|   x: 0, | ||||
|   y: 0, | ||||
|   tab: null, | ||||
| }); | ||||
| const contextDropdownRef = ref(null); | ||||
| 
 | ||||
| const onTabContextMenu = (event, tab) => { | ||||
|   event.preventDefault(); | ||||
|   contextMenu.visible = true; | ||||
|   contextMenu.x = event.clientX; | ||||
|   contextMenu.y = event.clientY; | ||||
|   contextMenu.tab = tab; | ||||
|   nextTick(() => { | ||||
|     // 溢出优化可扩展 | ||||
|   }); | ||||
|   document.body.addEventListener('click', hideContextMenu, { once: true }); | ||||
| }; | ||||
| function hideContextMenu() { | ||||
|   contextMenu.visible = false; | ||||
|   contextMenu.tab = null; | ||||
| } | ||||
| function closeTabContextTab() { | ||||
|   if (contextMenu.tab && contextMenu.tab.fullPath !== defaultDashboardPath) { | ||||
|     tabsStore.removeTab(contextMenu.tab.fullPath); | ||||
|   } | ||||
|   hideContextMenu(); | ||||
| } | ||||
| function closeOthersContextTab() { | ||||
|   if (contextMenu.tab) { | ||||
|     tabsStore.activeTab = contextMenu.tab.fullPath; | ||||
|     tabsStore.closeOthers(); | ||||
|   } | ||||
|   hideContextMenu(); | ||||
| } | ||||
| function closeAllTabs() { | ||||
|   tabsStore.closeAll(); | ||||
|   hideContextMenu(); | ||||
|   router.push(defaultDashboardPath); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div class="common-layout"> | ||||
|     <el-container class="main-container"> | ||||
|       <common-aside /> | ||||
|       <common-aside @menu-click="handleAsideMenuClick" /> | ||||
|       <el-container> | ||||
|         <el-header class="main-header"> | ||||
|           <common-header /> | ||||
|         </el-header> | ||||
|         <el-main class="right-main"> | ||||
|           <router-view /> | ||||
|           <div class="multi-tabs-wrapper"> | ||||
|             <el-tabs | ||||
|               v-model="tabsStore.activeTab" | ||||
|               type="card" | ||||
|               class="multi-tabs" | ||||
|               closable | ||||
|               @tab-remove="tabsCloseTab" | ||||
|             > | ||||
|               <el-tab-pane | ||||
|                 v-for="tab in tabsStore.tabList" | ||||
|                 :key="tab.fullPath" | ||||
|                 :label="tab.title" | ||||
|                 :name="tab.fullPath" | ||||
|                 :closable="tab.fullPath !== defaultDashboardPath" | ||||
|                 @contextmenu="onTabContextMenu($event, tab)" | ||||
|               /> | ||||
|             </el-tabs> | ||||
|             <!-- 跟随浮动到 tabs 最右侧的批量按钮 --> | ||||
|             <div class="floated-tabs-extra-btn"> | ||||
|               <el-dropdown> | ||||
|                 <span class="extra-action-btn"> | ||||
|                   <el-icon><DArrowRight /></el-icon> | ||||
|                 </span> | ||||
|                 <template #dropdown> | ||||
|                   <el-dropdown-menu> | ||||
|                     <el-dropdown-item @click="closeOthers">关闭其他</el-dropdown-item> | ||||
|                     <el-dropdown-item @click="closeAll">关闭全部</el-dropdown-item> | ||||
|                   </el-dropdown-menu> | ||||
|                 </template> | ||||
|               </el-dropdown> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <!-- 主内容毛玻璃卡片容器 --> | ||||
|           <div class="main-panel glass-card"> | ||||
|             <keep-alive :max="10"> | ||||
|               <router-view v-slot="{ Component }"> | ||||
|                 <component :is="Component" /> | ||||
|               </router-view> | ||||
|             </keep-alive> | ||||
|           </div> | ||||
|         </el-main> | ||||
|       </el-container> | ||||
|     </el-container> | ||||
| @ -20,33 +150,61 @@ import CommonHeader from "@/components/CommonHeader.vue"; | ||||
| </template> | ||||
| 
 | ||||
| <style scoped lang="less"> | ||||
| .common-layout, | ||||
| .main-container { | ||||
| .common-layout, .main-container { | ||||
|   height: 100vh; | ||||
|   width: 100%; | ||||
|   display: flex; | ||||
|   background-color: var(--bg-color-page); | ||||
|   transition: background-color 0.3s ease; | ||||
| 
 | ||||
|   :deep(.el-aside) { | ||||
|     display: block !important; | ||||
|     visibility: visible !important; | ||||
|     height: 100vh; | ||||
|   } | ||||
| 
 | ||||
|   .main-header { | ||||
|     background-color: var(--header-bg-color, #0081ff); | ||||
|     transition: background-color 0.3s ease; | ||||
|     height: 60px; | ||||
|     padding: 0; | ||||
|   } | ||||
|    | ||||
|   .right-main { | ||||
|     background-color: var(--bg-color-page); | ||||
|     color: var(--text-color-primary); | ||||
|     transition: background-color 0.3s ease, color 0.3s ease; | ||||
|     padding: 20px; | ||||
|     overflow-y: auto; | ||||
|     .multi-tabs-wrapper { | ||||
|       position: relative; | ||||
|       zoom: 1; | ||||
|       min-height: 45px; | ||||
|     } | ||||
|     .floated-tabs-extra-btn { | ||||
|       float: right; | ||||
|       margin-top: -40px; | ||||
|       margin-right: 12px; | ||||
|       z-index: 10; | ||||
|     } | ||||
|     .extra-action-btn { | ||||
|       display: inline-flex; | ||||
|       align-items: center; | ||||
|       font-size: 20px; | ||||
|       cursor: pointer; | ||||
|       color: #888; | ||||
|       background: none; | ||||
|       border: none; | ||||
|       padding: 0 8px; | ||||
|       border-radius: 4px; | ||||
|       transition: color 0.2s; | ||||
|       &:hover { | ||||
|         color: #409eff; | ||||
|         background: none; | ||||
|       } | ||||
|     } | ||||
|     .more-menu { | ||||
|       font-size: 13px; | ||||
|       margin-left: 10px; | ||||
|       cursor: pointer; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | ||||
							
								
								
									
										0
									
								
								pc/src/views/apps/knowledge/category/edit.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								pc/src/views/apps/knowledge/category/edit.vue
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										13
									
								
								pc/src/views/apps/knowledge/category/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								pc/src/views/apps/knowledge/category/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| <template> | ||||
|     <div> | ||||
| 123 | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup> | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| 
 | ||||
| </style> | ||||
| @ -67,10 +67,20 @@ | ||||
|     <div class="knowledge-repos"> | ||||
|       <div class="repos-header"> | ||||
|         <h2>知识库列表</h2> | ||||
|         <el-button type="primary" @click="handleCreate" class="create-btn"> | ||||
|           <i class="fa-solid fa-plus"></i> | ||||
|           新建知识库 | ||||
|         </el-button> | ||||
|         <div> | ||||
|           <el-button type="primary" @click="handleCreate" class="create-btn"> | ||||
|             <i class="fa-solid fa-plus"></i> | ||||
|             新建知识库 | ||||
|           </el-button> | ||||
|           <el-button type="primary" @click="handleCategory" class="create-btn"> | ||||
|             <i class="fas fa-folder-open"></i> | ||||
|             分类管理 | ||||
|           </el-button> | ||||
|           <el-button type="primary" @click="handleTags" class="create-btn"> | ||||
|             <i class="fas fa-tags"></i> | ||||
|             标签管理 | ||||
|           </el-button> | ||||
|         </div> | ||||
|       </div> | ||||
| 
 | ||||
|       <div class="repos-grid"> | ||||
| @ -314,6 +324,16 @@ function handleEdit(repo: Knowledge) { | ||||
|   router.push(`/apps/knowledge/edit/${repo.id}`); | ||||
| } | ||||
| 
 | ||||
| function handleCategory() { | ||||
|   // 跳转到分类管理页面,使用路径跳转 | ||||
|   router.push(`/apps/knowledge/category`); | ||||
| } | ||||
| 
 | ||||
| function handleTags() { | ||||
|   // 跳转到标签管理页面,使用路径跳转 | ||||
|   router.push(`/apps/knowledge/tags`); | ||||
| } | ||||
| 
 | ||||
| function handleDelete(repo: Knowledge) { | ||||
|   ElMessageBox.confirm(`确认删除知识「${repo.title}」?`, "提示", { | ||||
|     confirmButtonText: "确定", | ||||
| @ -457,7 +477,7 @@ onMounted(() => { | ||||
|   .search-input { | ||||
|     flex: 1; | ||||
| 
 | ||||
|     :deep(.el-input__wrapper){ | ||||
|     :deep(.el-input__wrapper) { | ||||
|       border: none !important; | ||||
|     } | ||||
| 
 | ||||
| @ -706,7 +726,8 @@ onMounted(() => { | ||||
|             color: var(--primary-color); | ||||
|             border-color: var(--border-color); | ||||
|             font-weight: 500; | ||||
|             transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; | ||||
|             transition: background-color 0.3s ease, color 0.3s ease, | ||||
|               border-color 0.3s ease; | ||||
|           } | ||||
| 
 | ||||
|           .repo-owner { | ||||
| @ -744,7 +765,8 @@ onMounted(() => { | ||||
|           border-color: var(--border-color-lighter); | ||||
|           font-size: 12px; | ||||
|           padding: 4px 12px; | ||||
|           transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; | ||||
|           transition: background-color 0.3s ease, color 0.3s ease, | ||||
|             border-color 0.3s ease; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										0
									
								
								pc/src/views/apps/knowledge/tag/edit.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								pc/src/views/apps/knowledge/tag/edit.vue
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								pc/src/views/apps/knowledge/tag/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								pc/src/views/apps/knowledge/tag/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -145,7 +145,7 @@ | ||||
|         > | ||||
|           <el-input | ||||
|             v-model="currentMenu.ComponentPath" | ||||
|             placeholder="例如:@/views/settings/index.vue" | ||||
|             placeholder="例如:/apps/knowledge/index.vue" | ||||
|           /> | ||||
|         </el-form-item> | ||||
| 
 | ||||
| @ -440,10 +440,15 @@ const saveMenu = async () => { | ||||
|   const valid = await menuFormRef.value.validate(); | ||||
|   if (!valid) return; | ||||
| 
 | ||||
|   // 解决后端时间字段问题:过滤掉 CreateTime/UpdateTime | ||||
|   const payload = { ...currentMenu.value }; | ||||
|   delete payload.CreateTime; | ||||
|   delete payload.UpdateTime; | ||||
| 
 | ||||
|   try { | ||||
|     if (currentMenu.value.Id === 0) { | ||||
|       // 新增菜单 | ||||
|       const result = await createMenu(currentMenu.value as Menu); | ||||
|       const result = await createMenu(payload as Menu); | ||||
|       if (result.success) { | ||||
|         ElMessage.success("菜单添加成功"); | ||||
|         dialogVisible.value = false; | ||||
| @ -455,7 +460,7 @@ const saveMenu = async () => { | ||||
|       // 编辑菜单 | ||||
|       const result = await updateMenu( | ||||
|         currentMenu.value.Id!, | ||||
|         currentMenu.value as Menu | ||||
|         payload as Menu | ||||
|       ); | ||||
|       if (result.success) { | ||||
|         ElMessage.success("更新成功"); | ||||
|  | ||||
| @ -167,7 +167,7 @@ func (c *KnowledgeController) Update() { | ||||
| 	c.Data["json"] = map[string]interface{}{ | ||||
| 		"code":    0, | ||||
| 		"message": "更新成功", | ||||
| 		"data":    nil, | ||||
| 		// "data":    nil, | ||||
| 	} | ||||
| 	c.ServeJSON() | ||||
| } | ||||
|  | ||||
| @ -105,7 +105,7 @@ func (c *MenuController) UpdateMenu() { | ||||
| 		c.Data["json"] = map[string]interface{}{ | ||||
| 			"success": true, | ||||
| 			"message": "更新菜单成功", | ||||
| 			"data":    menu, | ||||
| 			// "data":    menu, | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -248,7 +248,7 @@ func UpdateKnowledge(id int, k *Knowledge) error { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// 更新字段(不包含category_name,因为它是从联查获取的) | ||||
| 	// 更新字段 | ||||
| 	knowledge.Title = k.Title | ||||
| 	knowledge.CategoryId = k.CategoryId | ||||
| 	knowledge.Tags = k.Tags | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user