App.vue 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. <template>
  2. <div data-theme="dark" class="flex flex-col h-screen">
  3. <!-- 顶部栏 -->
  4. <header class="navbar bg-base-200 min-h-0 px-4 py-2 shrink-0">
  5. <!-- Tab 切换 -->
  6. <div class="tabs tabs-boxed bg-base-300">
  7. <a
  8. class="tab tab-sm"
  9. :class="{ 'tab-active': activeTab === 'persona' }"
  10. @click="switchTab('persona')"
  11. >人设</a>
  12. <a
  13. class="tab tab-sm"
  14. :class="{ 'tab-active': activeTab === 'match' }"
  15. @click="switchTab('match')"
  16. >待解构帖子</a>
  17. </div>
  18. <!-- 图例 -->
  19. <div class="flex gap-3 text-xs text-base-content/60 ml-6 border-l border-base-content/20 pl-4">
  20. <span class="text-base-content font-medium">颜色:</span>
  21. <div class="flex items-center gap-1">
  22. <span class="w-2.5 h-2.5 rounded-full bg-dim-persona"></span>人设
  23. </div>
  24. <div class="flex items-center gap-1">
  25. <span class="w-2.5 h-2.5 rounded-full bg-dim-inspiration"></span>灵感点
  26. </div>
  27. <div class="flex items-center gap-1">
  28. <span class="w-2.5 h-2.5 rounded-full bg-dim-purpose"></span>目的点
  29. </div>
  30. <div class="flex items-center gap-1">
  31. <span class="w-2.5 h-2.5 rounded-full bg-dim-key"></span>关键点
  32. </div>
  33. </div>
  34. <div class="flex gap-3 text-xs text-base-content/60 ml-4 border-l border-base-content/20 pl-4">
  35. <span class="text-base-content font-medium">形状:</span>
  36. <div class="flex items-center gap-1">○ 标签</div>
  37. <div class="flex items-center gap-1">□ 分类/点</div>
  38. </div>
  39. <div class="flex gap-3 text-xs text-base-content/60 ml-4 border-l border-base-content/20 pl-4">
  40. <span class="text-base-content font-medium">填充:</span>
  41. <div class="flex items-center gap-1">◯ 帖子</div>
  42. <div class="flex items-center gap-1">● 人设</div>
  43. </div>
  44. <!-- 边图例 -->
  45. <div class="flex gap-3 text-xs text-base-content/60 ml-4 border-l border-base-content/20 pl-4">
  46. <span class="text-base-content font-medium">边:</span>
  47. <div v-for="(color, type) in edgeTypeColors" :key="type" class="flex items-center gap-1">
  48. <span class="w-4 h-0.5" :style="{ backgroundColor: color }"></span>
  49. <span>{{ type }}</span>
  50. </div>
  51. </div>
  52. </header>
  53. <!-- 主内容区 - 人设图谱 Tab -->
  54. <main v-if="activeTab === 'persona'" class="flex flex-1 overflow-hidden">
  55. <TreeView class="w-[420px] shrink-0 bg-base-200 border-r border-base-300" />
  56. <GraphView class="flex-1" />
  57. <!-- 详情侧边栏 -->
  58. <DetailPanel class="w-56 shrink-0 transition-all duration-200" :class="{ 'w-0 overflow-hidden': !hasSelection }" />
  59. </main>
  60. <!-- 主内容区 - 帖子匹配 Tab -->
  61. <main v-else-if="activeTab === 'match'" class="flex flex-1 overflow-hidden">
  62. <!-- 左侧:人设树 + 相关图(上下布局) -->
  63. <div
  64. class="shrink-0 bg-base-200 border-r border-base-300 flex flex-col transition-all duration-200"
  65. :class="getLeftPanelClass()"
  66. >
  67. <!-- 人设树(上部,占50%) -->
  68. <div
  69. class="flex flex-col transition-all duration-200 h-1/2 shrink-0"
  70. :class="{ 'h-full': store.expandedPanel === 'persona-tree', 'h-10': store.expandedPanel === 'graph' }"
  71. >
  72. <div class="flex items-center justify-between px-4 py-1 bg-base-300 text-xs text-base-content/60 shrink-0 border-b border-base-300">
  73. <span>人设树</span>
  74. <div class="flex gap-1">
  75. <button
  76. v-if="store.expandedPanel !== 'persona-tree'"
  77. @click="store.expandPanel('persona-tree')"
  78. class="btn btn-ghost btn-xs"
  79. title="放大"
  80. >⤢</button>
  81. <button
  82. v-if="store.expandedPanel !== 'default'"
  83. @click="store.resetLayout()"
  84. class="btn btn-ghost btn-xs"
  85. title="恢复"
  86. >⊡</button>
  87. </div>
  88. </div>
  89. <TreeView class="flex-1 min-h-0 overflow-auto" :hide-header="true" />
  90. </div>
  91. <!-- 相关图(下部,占50%) -->
  92. <div
  93. v-show="store.expandedPanel !== 'persona-tree'"
  94. class="flex-1 border-t border-base-300"
  95. :class="{ 'h-full': store.expandedPanel === 'graph' }"
  96. >
  97. <GraphView class="h-full" :show-expand="true" />
  98. </div>
  99. </div>
  100. <!-- 中间:推导图谱 -->
  101. <div
  102. class="shrink-0 bg-base-200 border-l border-base-300 transition-all duration-200"
  103. :class="getDerivationPanelClass()"
  104. >
  105. <DerivationView class="h-full" />
  106. </div>
  107. <!-- 右侧:帖子树(含匹配列表+详情) -->
  108. <div
  109. class="bg-base-200 border-l border-base-300 flex flex-col transition-all duration-200"
  110. :class="getPostTreeClass()"
  111. >
  112. <PostTreeView class="flex-1" :show-expand="true" />
  113. </div>
  114. </main>
  115. </div>
  116. </template>
  117. <script setup>
  118. import { ref, computed } from 'vue'
  119. import TreeView from './components/TreeView.vue'
  120. import GraphView from './components/GraphView.vue'
  121. import PostTreeView from './components/PostTreeView.vue'
  122. import DetailPanel from './components/DetailPanel.vue'
  123. import DerivationView from './components/DerivationView.vue'
  124. import { useGraphStore } from './stores/graph'
  125. import { edgeTypeColors } from './config/edgeStyle'
  126. const store = useGraphStore()
  127. // 当前激活的 Tab
  128. const activeTab = ref('match')
  129. // 是否有选中内容(用于控制侧边栏显示)
  130. const hasSelection = computed(() => store.selectedNode || store.selectedEdge)
  131. // 切换 Tab 时清除选中状态,避免干扰
  132. function switchTab(tab) {
  133. if (activeTab.value !== tab) {
  134. store.clearSelection()
  135. store.resetLayout()
  136. activeTab.value = tab
  137. }
  138. }
  139. // ==================== 布局类计算 ====================
  140. function getLeftPanelClass() {
  141. const panel = store.expandedPanel
  142. if (panel === 'post-tree') return 'w-0 opacity-0 overflow-hidden'
  143. if (panel === 'persona-tree' || panel === 'graph') return 'flex-1'
  144. return 'w-[360px]'
  145. }
  146. function getPersonaTreeClass() {
  147. const panel = store.expandedPanel
  148. if (panel === 'persona-tree') return 'flex-1'
  149. if (panel === 'graph') return 'h-10 shrink-0'
  150. // 默认:根据相关图状态调整
  151. if (store.selectedNodeId) return 'flex-1'
  152. return 'flex-1'
  153. }
  154. function getGraphClass() {
  155. const panel = store.expandedPanel
  156. if (panel === 'graph') return 'flex-1'
  157. if (store.selectedNodeId) return 'h-[280px] shrink-0'
  158. return 'h-10 shrink-0'
  159. }
  160. function getPostTreeClass() {
  161. const panel = store.expandedPanel
  162. if (panel === 'post-tree') return 'flex-1'
  163. if (panel === 'persona-tree' || panel === 'graph' || panel === 'derivation') return 'w-0 opacity-0 overflow-hidden'
  164. return 'flex-1'
  165. }
  166. function getDerivationPanelClass() {
  167. const panel = store.expandedPanel
  168. if (panel === 'derivation') return 'flex-1'
  169. if (panel === 'persona-tree' || panel === 'graph' || panel === 'post-tree') return 'w-0 opacity-0 overflow-hidden'
  170. return 'w-[400px]'
  171. }
  172. </script>