AppSidebar.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <script setup>
  2. import { computed, ref } from 'vue'
  3. import { useRoute, useRouter } from 'vue-router'
  4. import {
  5. RocketOutline,
  6. GridOutline,
  7. GitMergeOutline,
  8. PricetagsOutline,
  9. SettingsOutline,
  10. ChevronDownOutline,
  11. PlayCircleOutline,
  12. CubeOutline,
  13. DocumentTextOutline,
  14. ServerOutline,
  15. GitNetworkOutline,
  16. BookOutline,
  17. SparklesOutline,
  18. WarningOutline,
  19. AppsOutline
  20. } from '@vicons/ionicons5'
  21. import { NIcon } from 'naive-ui'
  22. const route = useRoute()
  23. const router = useRouter()
  24. const menuItems = [
  25. {
  26. group: '知识库管理',
  27. icon: BookOutline,
  28. children: [
  29. { key: '/kb/documents', label: '文档数据', icon: DocumentTextOutline },
  30. { key: '/kb/datasources', label: '结构化数据', icon: ServerOutline },
  31. { key: '/kb/graphs', label: '知识图谱', icon: GitNetworkOutline },
  32. { key: '/kb/rag', label: 'RAG', icon: SparklesOutline },
  33. { key: '/kb/rag-governance', label: '图谱治理', icon: SettingsOutline }
  34. ]
  35. },
  36. {
  37. group: '智能体管理',
  38. icon: RocketOutline,
  39. children: [
  40. { key: '/management', label: '技能管理', icon: GridOutline },
  41. { key: '/skill/generate', label: '技能生成', icon: SparklesOutline },
  42. { key: '/orchestration', label: '智能体编排', icon: GitMergeOutline },
  43. { key: '/templates', label: '智能体模板', icon: CubeOutline },
  44. { key: '/history', label: '运行记录', icon: PlayCircleOutline }
  45. ]
  46. },
  47. {
  48. group: '其他管理',
  49. icon: AppsOutline,
  50. children: [
  51. { key: '/tags', label: '标签管理', icon: PricetagsOutline },
  52. { key: '/models', label: '模型管理', icon: SettingsOutline },
  53. { key: '/error-patterns', label: '错误模式管理', icon: WarningOutline }
  54. ]
  55. }
  56. ]
  57. // 分组展开状态:按 group 名称独立控制
  58. const groupExpanded = ref({
  59. '知识库管理': true,
  60. '智能体管理': true,
  61. '其他管理': true
  62. })
  63. function toggleGroup(name) {
  64. groupExpanded.value[name] = !groupExpanded.value[name]
  65. }
  66. const activeKey = computed(() => route.path)
  67. // 判断分组中是否有激活项
  68. function isGroupActive(group) {
  69. return group.children?.some(c => activeKey.value.startsWith(c.key))
  70. }
  71. function navigateTo(path) {
  72. router.push(path)
  73. }
  74. </script>
  75. <template>
  76. <aside class="app-sidebar">
  77. <!-- Logo 区域 -->
  78. <div class="sidebar-logo">
  79. <div class="logo-icon">
  80. <svg width="28" height="28" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  81. <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="url(#logo-grad)" stroke-width="2" stroke-linecap="round"
  82. stroke-linejoin="round" />
  83. <path d="M2 17L12 22L22 17" stroke="url(#logo-grad)" stroke-width="2" stroke-linecap="round"
  84. stroke-linejoin="round" />
  85. <path d="M2 12L12 17L22 12" stroke="url(#logo-grad)" stroke-width="2" stroke-linecap="round"
  86. stroke-linejoin="round" />
  87. <defs>
  88. <linearGradient id="logo-grad" x1="2" y1="2" x2="22" y2="22">
  89. <stop stop-color="#38BDF8" />
  90. <stop offset="1" stop-color="#2563EB" />
  91. </linearGradient>
  92. </defs>
  93. </svg>
  94. </div>
  95. <div class="logo-text">
  96. <span class="logo-title">智能体融合管理</span>
  97. <span class="logo-subtitle"></span>
  98. </div>
  99. </div>
  100. <!-- 导航菜单 -->
  101. <nav class="sidebar-nav">
  102. <template v-for="item in menuItems" :key="item.key || item.group">
  103. <!-- 分组菜单 -->
  104. <template v-if="item.children">
  105. <div class="nav-group-header" :class="{ active: isGroupActive(item) }" @click="toggleGroup(item.group)">
  106. <div class="nav-group-left">
  107. <div class="nav-item-icon">
  108. <component :is="item.icon" />
  109. </div>
  110. <span class="nav-item-label">{{ item.group }}</span>
  111. </div>
  112. <div class="nav-group-arrow" :class="{ expanded: groupExpanded[item.group] }">
  113. <n-icon size="14">
  114. <ChevronDownOutline />
  115. </n-icon>
  116. </div>
  117. </div>
  118. <div v-show="groupExpanded[item.group]" class="nav-group-children">
  119. <div v-for="child in item.children" :key="child.key" class="nav-item sub-item"
  120. :class="{ active: activeKey === child.key }" @click="navigateTo(child.key)">
  121. <div class="nav-item-icon">
  122. <component :is="child.icon" />
  123. </div>
  124. <span class="nav-item-label">{{ child.label }}</span>
  125. <div v-if="activeKey === child.key" class="nav-item-indicator"></div>
  126. </div>
  127. </div>
  128. </template>
  129. <!-- 普通菜单项 -->
  130. <div v-else class="nav-item" :class="{ active: activeKey === item.key }" @click="navigateTo(item.key)">
  131. <div class="nav-item-icon">
  132. <component :is="item.icon" />
  133. </div>
  134. <span class="nav-item-label">{{ item.label }}</span>
  135. <div v-if="activeKey === item.key" class="nav-item-indicator"></div>
  136. </div>
  137. </template>
  138. </nav>
  139. <!-- 底部装饰 -->
  140. <div class="sidebar-footer">
  141. <div class="footer-line"></div>
  142. <div class="footer-text"></div>
  143. </div>
  144. </aside>
  145. </template>
  146. <style scoped>
  147. .app-sidebar {
  148. width: var(--sidebar-width);
  149. height: 100vh;
  150. background: var(--bg-sidebar);
  151. display: flex;
  152. flex-direction: column;
  153. border-right: 1px solid var(--border-color);
  154. position: relative;
  155. overflow: hidden;
  156. }
  157. /* Logo */
  158. .sidebar-logo {
  159. display: flex;
  160. align-items: center;
  161. gap: 12px;
  162. padding: 24px 20px 32px;
  163. }
  164. .logo-icon {
  165. width: 44px;
  166. height: 44px;
  167. display: flex;
  168. align-items: center;
  169. justify-content: center;
  170. background: rgba(37, 99, 235, 0.12);
  171. border-radius: var(--radius-md);
  172. border: 1px solid rgba(37, 99, 235, 0.2);
  173. }
  174. .logo-text {
  175. display: flex;
  176. flex-direction: column;
  177. }
  178. .logo-title {
  179. font-size: 18px;
  180. font-weight: 700;
  181. color: var(--text-primary);
  182. letter-spacing: 0.5px;
  183. }
  184. .logo-subtitle {
  185. font-size: 11px;
  186. color: var(--text-tertiary);
  187. margin-top: 2px;
  188. }
  189. /* 导航菜单 */
  190. .sidebar-nav {
  191. flex: 1;
  192. padding: 0 12px;
  193. display: flex;
  194. flex-direction: column;
  195. gap: 4px;
  196. }
  197. .nav-item {
  198. display: flex;
  199. align-items: center;
  200. gap: 12px;
  201. padding: 12px 16px;
  202. border-radius: var(--radius-md);
  203. cursor: pointer;
  204. transition: all var(--transition-normal);
  205. position: relative;
  206. color: var(--text-secondary);
  207. }
  208. .nav-item:hover {
  209. background: rgba(37, 99, 235, 0.08);
  210. color: var(--text-primary);
  211. }
  212. .nav-item.active {
  213. background: rgba(37, 99, 235, 0.12);
  214. color: var(--color-accent);
  215. }
  216. .nav-item.sub-item {
  217. padding: 10px 16px 10px 48px;
  218. }
  219. .nav-item-icon {
  220. width: 20px;
  221. height: 20px;
  222. display: flex;
  223. align-items: center;
  224. justify-content: center;
  225. font-size: 18px;
  226. }
  227. .nav-item-label {
  228. font-size: 14px;
  229. font-weight: 500;
  230. }
  231. .nav-item-indicator {
  232. position: absolute;
  233. left: 0;
  234. top: 50%;
  235. transform: translateY(-50%);
  236. width: 3px;
  237. height: 20px;
  238. background: var(--gradient-accent);
  239. border-radius: 0 3px 3px 0;
  240. box-shadow: 0 0 10px rgba(37, 99, 235, 0.5);
  241. }
  242. /* 分组 */
  243. .nav-group-header {
  244. display: flex;
  245. align-items: center;
  246. justify-content: space-between;
  247. padding: 12px 16px;
  248. border-radius: var(--radius-md);
  249. cursor: pointer;
  250. transition: all var(--transition-normal);
  251. color: var(--text-secondary);
  252. }
  253. .nav-group-header:hover {
  254. background: rgba(37, 99, 235, 0.08);
  255. color: var(--text-primary);
  256. }
  257. .nav-group-header.active {
  258. color: var(--text-primary);
  259. }
  260. .nav-group-left {
  261. display: flex;
  262. align-items: center;
  263. gap: 12px;
  264. }
  265. .nav-group-arrow {
  266. transition: transform 0.2s;
  267. color: var(--text-tertiary);
  268. }
  269. .nav-group-arrow.expanded {
  270. transform: rotate(180deg);
  271. }
  272. .nav-group-children {
  273. display: flex;
  274. flex-direction: column;
  275. gap: 2px;
  276. }
  277. /* 底部 */
  278. .sidebar-footer {
  279. padding: 16px 20px 24px;
  280. }
  281. .footer-line {
  282. height: 1px;
  283. background: var(--border-color);
  284. margin-bottom: 16px;
  285. }
  286. .footer-text {
  287. font-size: 11px;
  288. color: var(--text-muted);
  289. text-align: center;
  290. letter-spacing: 1px;
  291. text-transform: uppercase;
  292. }
  293. </style>