Editor.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071
  1. <template>
  2. <div class="editor-page">
  3. <!-- 工具栏 -->
  4. <div class="editor-toolbar">
  5. <el-button :icon="ArrowLeft" @click="goBack">返回</el-button>
  6. <el-input
  7. v-model="reportTitle"
  8. class="title-input"
  9. placeholder="请输入报告标题"
  10. />
  11. <span class="save-status" v-if="saved">✓ 已保存</span>
  12. <div class="toolbar-right">
  13. <el-button :icon="Clock">版本</el-button>
  14. <el-button :icon="Share">分享</el-button>
  15. <el-divider direction="vertical" />
  16. <el-button type="primary" :icon="Check" @click="handleSave">保存</el-button>
  17. </div>
  18. </div>
  19. <!-- 主体 -->
  20. <div class="editor-body">
  21. <!-- 左侧文件面板 -->
  22. <div class="left-panel">
  23. <div class="panel-header">
  24. <span>📁 来源文件</span>
  25. <span class="file-count">{{ sourceFiles.length }}个</span>
  26. </div>
  27. <div class="panel-body">
  28. <!-- 上传区 -->
  29. <el-upload
  30. class="upload-zone"
  31. drag
  32. action="/api/v1/parse/upload"
  33. :on-success="handleFileUpload"
  34. :show-file-list="false"
  35. >
  36. <div class="upload-content">
  37. <div class="upload-icon">📄</div>
  38. <div class="upload-text">拖拽或点击上传</div>
  39. <div class="upload-hint">支持 PDF / Word / Excel</div>
  40. </div>
  41. </el-upload>
  42. <!-- 来源文件列表 -->
  43. <div class="file-list">
  44. <div
  45. v-for="file in sourceFiles"
  46. :key="file.id"
  47. class="file-item"
  48. :class="{ active: selectedFile?.id === file.id }"
  49. @click="selectFile(file)"
  50. >
  51. <span class="file-icon">{{ getFileIcon(file) }}</span>
  52. <div class="file-info">
  53. <div class="file-name">{{ file.alias }}</div>
  54. <div class="file-meta">
  55. <span v-if="file.required" class="required">必需</span>
  56. <span v-else>可选</span>
  57. </div>
  58. </div>
  59. <el-button
  60. size="small"
  61. :icon="Delete"
  62. circle
  63. @click.stop="removeSourceFile(file)"
  64. />
  65. </div>
  66. </div>
  67. <!-- 添加来源文件定义 -->
  68. <el-button
  69. class="add-source-btn"
  70. :icon="Plus"
  71. @click="showAddSourceDialog = true"
  72. >
  73. 添加来源文件定义
  74. </el-button>
  75. </div>
  76. </div>
  77. <!-- 中间编辑区 -->
  78. <div class="center-panel">
  79. <div class="editor-title-bar">
  80. <h2>{{ reportTitle }}</h2>
  81. <div class="view-toggle">
  82. <el-radio-group v-model="viewMode" size="small">
  83. <el-radio-button label="edit">📝 编辑</el-radio-button>
  84. <el-radio-button label="preview">👁 预览</el-radio-button>
  85. </el-radio-group>
  86. </div>
  87. <el-button :icon="Share" circle @click="showGraphModal = true" />
  88. </div>
  89. <div class="editor-scroll" ref="editorRef">
  90. <div
  91. class="editor-content"
  92. contenteditable="true"
  93. @mouseup="handleTextSelection"
  94. v-html="documentContent"
  95. />
  96. </div>
  97. </div>
  98. <!-- 右侧变量面板 -->
  99. <div class="right-panel">
  100. <!-- 变量管理 -->
  101. <div class="element-section">
  102. <div class="element-header">
  103. <span class="element-title">
  104. 🏷️ 变量管理
  105. <span class="element-count">({{ variables.length }})</span>
  106. </span>
  107. <el-button size="small" :icon="Plus" @click="showAddVariableDialog = true">
  108. 添加
  109. </el-button>
  110. </div>
  111. <div class="element-body">
  112. <div class="element-tags-wrap">
  113. <div
  114. v-for="variable in variables"
  115. :key="variable.id"
  116. class="var-tag"
  117. :class="variable.category"
  118. @click="editVariable(variable)"
  119. >
  120. <span class="tag-icon">{{ getCategoryIcon(variable.category) }}</span>
  121. <span class="tag-name">{{ variable.displayName }}</span>
  122. </div>
  123. </div>
  124. <div class="element-hint" v-if="variables.length === 0">
  125. 选中文本后右键标记为变量
  126. </div>
  127. </div>
  128. </div>
  129. <!-- 按类别分组显示 -->
  130. <div class="category-section" v-for="(vars, category) in groupedVariables" :key="category">
  131. <div class="category-header">
  132. <span
  133. class="category-dot"
  134. :style="{ background: getCategoryColor(category) }"
  135. />
  136. <span>{{ getCategoryLabel(category) }}</span>
  137. <span class="category-count">{{ vars.length }}</span>
  138. </div>
  139. <div class="category-items">
  140. <div
  141. v-for="v in vars"
  142. :key="v.id"
  143. class="category-item"
  144. @click="editVariable(v)"
  145. >
  146. <span>{{ v.displayName }}</span>
  147. <span class="item-value">{{ v.exampleValue || '-' }}</span>
  148. </div>
  149. </div>
  150. </div>
  151. </div>
  152. </div>
  153. <!-- 右键菜单 -->
  154. <div
  155. v-show="contextMenuVisible"
  156. class="context-menu"
  157. :style="{ left: contextMenuPos.x + 'px', top: contextMenuPos.y + 'px' }"
  158. >
  159. <div class="context-menu-item" @click="markAsVariable('entity')">
  160. <span class="icon">🏢</span>
  161. <span>标记为核心实体</span>
  162. </div>
  163. <div class="context-menu-item" @click="markAsVariable('concept')">
  164. <span class="icon">💡</span>
  165. <span>标记为概念/技术</span>
  166. </div>
  167. <div class="context-menu-item" @click="markAsVariable('data')">
  168. <span class="icon">📊</span>
  169. <span>标记为数据/指标</span>
  170. </div>
  171. <div class="context-menu-item" @click="markAsVariable('location')">
  172. <span class="icon">📍</span>
  173. <span>标记为地点/组织</span>
  174. </div>
  175. <div class="context-menu-item" @click="markAsVariable('asset')">
  176. <span class="icon">📑</span>
  177. <span>标记为资源模板</span>
  178. </div>
  179. </div>
  180. <!-- 添加来源文件对话框 -->
  181. <el-dialog v-model="showAddSourceDialog" title="添加来源文件定义" width="400">
  182. <el-form :model="newSourceFile" label-width="80px">
  183. <el-form-item label="文件别名" required>
  184. <el-input v-model="newSourceFile.alias" placeholder="如:可研批复" />
  185. </el-form-item>
  186. <el-form-item label="描述">
  187. <el-input v-model="newSourceFile.description" placeholder="文件描述" />
  188. </el-form-item>
  189. <el-form-item label="是否必需">
  190. <el-switch v-model="newSourceFile.required" />
  191. </el-form-item>
  192. </el-form>
  193. <template #footer>
  194. <el-button @click="showAddSourceDialog = false">取消</el-button>
  195. <el-button type="primary" @click="addSourceFile">添加</el-button>
  196. </template>
  197. </el-dialog>
  198. <!-- 添加/编辑变量对话框 -->
  199. <el-dialog v-model="showVariableDialog" :title="editingVariable ? '编辑变量' : '添加变量'" width="500">
  200. <el-form :model="variableForm" label-width="100px">
  201. <el-form-item label="变量名" required>
  202. <el-input v-model="variableForm.name" placeholder="程序用名,如 project_name" />
  203. </el-form-item>
  204. <el-form-item label="显示名称" required>
  205. <el-input v-model="variableForm.displayName" placeholder="用户可见名称" />
  206. </el-form-item>
  207. <el-form-item label="类别">
  208. <el-select v-model="variableForm.category" style="width: 100%">
  209. <el-option label="核心实体" value="entity" />
  210. <el-option label="概念/技术" value="concept" />
  211. <el-option label="数据/指标" value="data" />
  212. <el-option label="地点/组织" value="location" />
  213. <el-option label="资源模板" value="asset" />
  214. </el-select>
  215. </el-form-item>
  216. <el-form-item label="示例值">
  217. <el-input v-model="variableForm.exampleValue" placeholder="文档中的原始值" />
  218. </el-form-item>
  219. <el-form-item label="来源类型">
  220. <el-select v-model="variableForm.sourceType" style="width: 100%">
  221. <el-option label="从来源文件提取" value="document" />
  222. <el-option label="手动输入" value="manual" />
  223. <el-option label="引用其他变量" value="reference" />
  224. <el-option label="固定值" value="fixed" />
  225. </el-select>
  226. </el-form-item>
  227. <el-form-item label="来源文件" v-if="variableForm.sourceType === 'document'">
  228. <el-select v-model="variableForm.sourceFileAlias" style="width: 100%">
  229. <el-option
  230. v-for="sf in sourceFiles"
  231. :key="sf.id"
  232. :label="sf.alias"
  233. :value="sf.alias"
  234. />
  235. </el-select>
  236. </el-form-item>
  237. <el-form-item label="提取方式" v-if="variableForm.sourceType === 'document'">
  238. <el-select v-model="variableForm.extractType" style="width: 100%">
  239. <el-option label="直接提取" value="direct" />
  240. <el-option label="AI 字段提取" value="ai_extract" />
  241. <el-option label="AI 总结" value="ai_summarize" />
  242. </el-select>
  243. </el-form-item>
  244. </el-form>
  245. <template #footer>
  246. <el-button @click="showVariableDialog = false">取消</el-button>
  247. <el-button
  248. v-if="editingVariable"
  249. type="danger"
  250. @click="deleteVariable"
  251. >
  252. 删除
  253. </el-button>
  254. <el-button type="primary" @click="saveVariable">保存</el-button>
  255. </template>
  256. </el-dialog>
  257. <!-- 知识图谱弹窗 -->
  258. <el-dialog v-model="showGraphModal" title="🔗 知识图谱" width="900">
  259. <div class="graph-container">
  260. <div class="graph-legend">
  261. <div class="legend-title">图例</div>
  262. <div class="legend-item">
  263. <span class="legend-dot entity"></span>
  264. <span>核心实体</span>
  265. </div>
  266. <div class="legend-item">
  267. <span class="legend-dot concept"></span>
  268. <span>概念/技术</span>
  269. </div>
  270. <div class="legend-item">
  271. <span class="legend-dot data"></span>
  272. <span>数据/指标</span>
  273. </div>
  274. <div class="legend-item">
  275. <span class="legend-dot location"></span>
  276. <span>地点/组织</span>
  277. </div>
  278. </div>
  279. <div class="graph-body">
  280. <div class="graph-placeholder">
  281. <el-icon size="64" color="#ccc"><Connection /></el-icon>
  282. <p>知识图谱可视化(开发中)</p>
  283. </div>
  284. </div>
  285. </div>
  286. </el-dialog>
  287. </div>
  288. </template>
  289. <script setup>
  290. import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
  291. import { useRouter, useRoute } from 'vue-router'
  292. import {
  293. ArrowLeft, Clock, Share, Check, Plus, Delete, Connection
  294. } from '@element-plus/icons-vue'
  295. import { ElMessage } from 'element-plus'
  296. import { useTemplateStore } from '@/stores/template'
  297. import { documentApi } from '@/api'
  298. const router = useRouter()
  299. const route = useRoute()
  300. const templateStore = useTemplateStore()
  301. const templateId = route.params.templateId
  302. const reportTitle = ref('')
  303. const viewMode = ref('edit')
  304. const saved = ref(true)
  305. const editorRef = ref(null)
  306. const loading = ref(false)
  307. // 来源文件(从 API 获取)
  308. const sourceFiles = ref([])
  309. const selectedFile = ref(null)
  310. const showAddSourceDialog = ref(false)
  311. const newSourceFile = reactive({
  312. alias: '',
  313. description: '',
  314. required: true
  315. })
  316. // 变量(从 API 获取)
  317. const variables = ref([])
  318. // 加载模板数据
  319. onMounted(async () => {
  320. await fetchTemplateData()
  321. })
  322. async function fetchTemplateData() {
  323. loading.value = true
  324. try {
  325. await templateStore.fetchTemplateDetail(templateId)
  326. // 设置模板标题
  327. reportTitle.value = templateStore.currentTemplate?.name || '未命名模板'
  328. // 设置来源文件
  329. sourceFiles.value = templateStore.sourceFiles || []
  330. // 设置变量
  331. variables.value = templateStore.variables || []
  332. // 根据 baseDocumentId 获取文档结构化内容
  333. const baseDocumentId = templateStore.currentTemplate?.baseDocumentId
  334. if (baseDocumentId) {
  335. try {
  336. const structuredDoc = await documentApi.getStructured(baseDocumentId)
  337. // 将结构化文档的 blocks 转换为 HTML 内容
  338. if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
  339. // 优先使用 markedHtml(带实体标注),其次使用 html
  340. documentContent.value = structuredDoc.blocks
  341. .map(block => block.markedHtml || block.html || block.plainText || '')
  342. .join('')
  343. } else {
  344. documentContent.value = emptyPlaceholder
  345. }
  346. } catch (docError) {
  347. console.warn('获取文档内容失败:', docError)
  348. documentContent.value = emptyPlaceholder
  349. }
  350. } else {
  351. documentContent.value = emptyPlaceholder
  352. }
  353. } catch (error) {
  354. console.error('加载模板失败:', error)
  355. ElMessage.error('加载模板失败')
  356. } finally {
  357. loading.value = false
  358. }
  359. }
  360. const showVariableDialog = ref(false)
  361. const showAddVariableDialog = ref(false)
  362. const editingVariable = ref(null)
  363. const variableForm = reactive({
  364. name: '',
  365. displayName: '',
  366. category: 'entity',
  367. exampleValue: '',
  368. sourceType: 'document',
  369. sourceFileAlias: '',
  370. extractType: 'direct'
  371. })
  372. // 右键菜单
  373. const contextMenuVisible = ref(false)
  374. const contextMenuPos = reactive({ x: 0, y: 0 })
  375. const selectedText = ref('')
  376. const selectionRange = ref(null)
  377. // 知识图谱
  378. const showGraphModal = ref(false)
  379. // 文档内容(从 API 获取或空白)
  380. const documentContent = ref('')
  381. // 空白模板时的占位提示
  382. const emptyPlaceholder = `
  383. <div class="empty-editor-placeholder">
  384. <h2>📝 开始编辑您的模板</h2>
  385. <p>这是一个空白模板。您可以:</p>
  386. <ul>
  387. <li>在左侧添加来源文件定义</li>
  388. <li>在右侧面板添加变量</li>
  389. <li>直接在此处编辑模板内容</li>
  390. <li>选中文本后右键将其标记为变量</li>
  391. </ul>
  392. </div>
  393. `
  394. // 计算属性
  395. const groupedVariables = computed(() => {
  396. const groups = {}
  397. variables.value.forEach(v => {
  398. const cat = v.category || 'other'
  399. if (!groups[cat]) groups[cat] = []
  400. groups[cat].push(v)
  401. })
  402. return groups
  403. })
  404. // 方法
  405. function goBack() {
  406. router.back()
  407. }
  408. function handleSave() {
  409. saved.value = true
  410. ElMessage.success('保存成功')
  411. }
  412. function getFileIcon(file) {
  413. return '📄'
  414. }
  415. function selectFile(file) {
  416. selectedFile.value = file
  417. }
  418. async function removeSourceFile(file) {
  419. try {
  420. await templateStore.deleteSourceFile(file.id)
  421. sourceFiles.value = sourceFiles.value.filter(f => f.id !== file.id)
  422. ElMessage.success('删除成功')
  423. } catch (error) {
  424. ElMessage.error('删除失败: ' + error.message)
  425. }
  426. }
  427. async function addSourceFile() {
  428. if (!newSourceFile.alias) {
  429. ElMessage.warning('请输入文件别名')
  430. return
  431. }
  432. try {
  433. const sf = await templateStore.addSourceFile(templateId, newSourceFile)
  434. sourceFiles.value.push(sf)
  435. showAddSourceDialog.value = false
  436. Object.assign(newSourceFile, { alias: '', description: '', required: true })
  437. ElMessage.success('添加成功')
  438. } catch (error) {
  439. ElMessage.error('添加失败: ' + error.message)
  440. }
  441. }
  442. function getCategoryIcon(category) {
  443. const icons = {
  444. entity: '🏢',
  445. concept: '💡',
  446. data: '📊',
  447. location: '📍',
  448. asset: '📑'
  449. }
  450. return icons[category] || '📌'
  451. }
  452. function getCategoryColor(category) {
  453. const colors = {
  454. entity: '#1890ff',
  455. concept: '#722ed1',
  456. data: '#52c41a',
  457. location: '#faad14',
  458. asset: '#eb2f96'
  459. }
  460. return colors[category] || '#8c8c8c'
  461. }
  462. function getCategoryLabel(category) {
  463. const labels = {
  464. entity: '核心实体',
  465. concept: '概念/技术',
  466. data: '数据/指标',
  467. location: '地点/组织',
  468. asset: '资源模板'
  469. }
  470. return labels[category] || '其他'
  471. }
  472. function editVariable(variable) {
  473. editingVariable.value = variable
  474. Object.assign(variableForm, variable)
  475. showVariableDialog.value = true
  476. }
  477. async function saveVariable() {
  478. if (!variableForm.name || !variableForm.displayName) {
  479. ElMessage.warning('请填写必要字段')
  480. return
  481. }
  482. try {
  483. if (editingVariable.value) {
  484. // 更新
  485. const updated = await templateStore.updateVariable(editingVariable.value.id, variableForm)
  486. Object.assign(editingVariable.value, updated)
  487. ElMessage.success('更新成功')
  488. } else {
  489. // 新增
  490. const newVar = await templateStore.addVariable(templateId, variableForm)
  491. variables.value.push(newVar)
  492. ElMessage.success('添加成功')
  493. }
  494. showVariableDialog.value = false
  495. resetVariableForm()
  496. } catch (error) {
  497. ElMessage.error('保存失败: ' + error.message)
  498. }
  499. }
  500. async function deleteVariable() {
  501. if (editingVariable.value) {
  502. try {
  503. await templateStore.deleteVariable(editingVariable.value.id)
  504. variables.value = variables.value.filter(v => v.id !== editingVariable.value.id)
  505. showVariableDialog.value = false
  506. resetVariableForm()
  507. ElMessage.success('删除成功')
  508. } catch (error) {
  509. ElMessage.error('删除失败: ' + error.message)
  510. }
  511. }
  512. }
  513. function resetVariableForm() {
  514. editingVariable.value = null
  515. Object.assign(variableForm, {
  516. name: '',
  517. displayName: '',
  518. category: 'entity',
  519. exampleValue: '',
  520. sourceType: 'document',
  521. sourceFileAlias: '',
  522. extractType: 'direct'
  523. })
  524. }
  525. function handleTextSelection(event) {
  526. const selection = window.getSelection()
  527. const text = selection.toString().trim()
  528. if (text) {
  529. selectedText.value = text
  530. selectionRange.value = selection.getRangeAt(0)
  531. contextMenuPos.x = event.clientX
  532. contextMenuPos.y = event.clientY
  533. contextMenuVisible.value = true
  534. } else {
  535. contextMenuVisible.value = false
  536. }
  537. }
  538. function markAsVariable(category) {
  539. if (!selectedText.value) return
  540. // 生成变量名
  541. const varName = 'var_' + Date.now()
  542. // 添加变量
  543. variables.value.push({
  544. id: Date.now().toString(),
  545. name: varName,
  546. displayName: selectedText.value.slice(0, 20),
  547. category,
  548. exampleValue: selectedText.value,
  549. sourceType: 'document'
  550. })
  551. // 关闭菜单
  552. contextMenuVisible.value = false
  553. selectedText.value = ''
  554. ElMessage.success('变量标记成功')
  555. }
  556. function handleFileUpload(response) {
  557. if (response.code === 200) {
  558. ElMessage.success('文件上传成功')
  559. }
  560. }
  561. // 点击其他地方关闭右键菜单
  562. function handleClickOutside(event) {
  563. if (!event.target.closest('.context-menu')) {
  564. contextMenuVisible.value = false
  565. }
  566. }
  567. onMounted(() => {
  568. document.addEventListener('click', handleClickOutside)
  569. })
  570. onUnmounted(() => {
  571. document.removeEventListener('click', handleClickOutside)
  572. })
  573. </script>
  574. <style lang="scss" scoped>
  575. .editor-page {
  576. height: calc(100vh - 56px);
  577. display: flex;
  578. flex-direction: column;
  579. background: var(--bg);
  580. }
  581. .editor-toolbar {
  582. height: 56px;
  583. background: #fff;
  584. border-bottom: 1px solid var(--border);
  585. display: flex;
  586. align-items: center;
  587. padding: 0 16px;
  588. gap: 16px;
  589. flex-shrink: 0;
  590. .title-input {
  591. width: 300px;
  592. :deep(.el-input__wrapper) {
  593. box-shadow: none;
  594. background: transparent;
  595. &:hover {
  596. background: var(--bg);
  597. }
  598. }
  599. }
  600. .save-status {
  601. color: var(--success);
  602. font-size: 13px;
  603. }
  604. .toolbar-right {
  605. margin-left: auto;
  606. display: flex;
  607. gap: 8px;
  608. align-items: center;
  609. }
  610. }
  611. .editor-body {
  612. flex: 1;
  613. display: flex;
  614. overflow: hidden;
  615. }
  616. .left-panel {
  617. width: 260px;
  618. background: #fff;
  619. border-right: 1px solid var(--border);
  620. display: flex;
  621. flex-direction: column;
  622. flex-shrink: 0;
  623. .panel-header {
  624. padding: 14px 16px;
  625. border-bottom: 1px solid var(--border);
  626. font-size: 13px;
  627. font-weight: 600;
  628. display: flex;
  629. justify-content: space-between;
  630. .file-count {
  631. color: var(--text-3);
  632. font-weight: normal;
  633. }
  634. }
  635. .panel-body {
  636. flex: 1;
  637. overflow-y: auto;
  638. padding: 12px;
  639. }
  640. }
  641. .upload-zone {
  642. border: 2px dashed var(--border);
  643. border-radius: 10px;
  644. margin-bottom: 16px;
  645. :deep(.el-upload-dragger) {
  646. padding: 20px;
  647. border: none;
  648. background: transparent;
  649. }
  650. .upload-content {
  651. text-align: center;
  652. }
  653. .upload-icon {
  654. font-size: 32px;
  655. margin-bottom: 8px;
  656. }
  657. .upload-text {
  658. font-size: 13px;
  659. color: var(--text-2);
  660. }
  661. .upload-hint {
  662. font-size: 11px;
  663. color: var(--text-3);
  664. }
  665. }
  666. .file-list {
  667. margin-bottom: 16px;
  668. }
  669. .file-item {
  670. display: flex;
  671. align-items: center;
  672. gap: 10px;
  673. padding: 10px 12px;
  674. background: #fff;
  675. border: 1px solid var(--border);
  676. border-radius: 8px;
  677. margin-bottom: 8px;
  678. cursor: pointer;
  679. transition: all 0.2s;
  680. &:hover, &.active {
  681. border-color: var(--primary);
  682. background: var(--primary-light);
  683. }
  684. .file-icon {
  685. font-size: 24px;
  686. }
  687. .file-info {
  688. flex: 1;
  689. min-width: 0;
  690. .file-name {
  691. font-size: 12px;
  692. font-weight: 500;
  693. }
  694. .file-meta {
  695. font-size: 11px;
  696. color: var(--text-3);
  697. .required {
  698. color: var(--danger);
  699. }
  700. }
  701. }
  702. }
  703. .add-source-btn {
  704. width: 100%;
  705. }
  706. .center-panel {
  707. flex: 1;
  708. display: flex;
  709. flex-direction: column;
  710. background: #fff;
  711. overflow: hidden;
  712. .editor-title-bar {
  713. padding: 16px 24px;
  714. border-bottom: 1px solid var(--border);
  715. display: flex;
  716. align-items: center;
  717. gap: 12px;
  718. h2 {
  719. flex: 1;
  720. font-size: 18px;
  721. font-weight: 600;
  722. }
  723. }
  724. .editor-scroll {
  725. flex: 1;
  726. overflow-y: auto;
  727. padding: 24px 32px;
  728. }
  729. .editor-content {
  730. max-width: 800px;
  731. margin: 0 auto;
  732. outline: none;
  733. :deep(h1) {
  734. font-size: 24px;
  735. font-weight: 700;
  736. margin-bottom: 24px;
  737. }
  738. :deep(h2) {
  739. font-size: 18px;
  740. font-weight: 600;
  741. margin: 28px 0 16px;
  742. }
  743. :deep(p) {
  744. margin-bottom: 16px;
  745. line-height: 1.8;
  746. }
  747. :deep(ul) {
  748. margin-bottom: 16px;
  749. padding-left: 24px;
  750. li {
  751. margin-bottom: 8px;
  752. }
  753. }
  754. }
  755. }
  756. .right-panel {
  757. width: 320px;
  758. background: #fff;
  759. border-left: 1px solid var(--border);
  760. overflow-y: auto;
  761. flex-shrink: 0;
  762. }
  763. .element-section {
  764. border-bottom: 1px solid var(--border);
  765. .element-header {
  766. padding: 14px 16px;
  767. display: flex;
  768. align-items: center;
  769. justify-content: space-between;
  770. .element-title {
  771. font-size: 13px;
  772. font-weight: 600;
  773. .element-count {
  774. color: var(--text-3);
  775. font-weight: normal;
  776. }
  777. }
  778. }
  779. .element-body {
  780. padding: 0 16px 16px;
  781. }
  782. .element-tags-wrap {
  783. display: flex;
  784. flex-wrap: wrap;
  785. gap: 8px;
  786. }
  787. .element-hint {
  788. font-size: 12px;
  789. color: var(--text-3);
  790. text-align: center;
  791. padding: 20px;
  792. }
  793. }
  794. .category-section {
  795. padding: 12px 16px;
  796. border-bottom: 1px solid var(--border);
  797. .category-header {
  798. display: flex;
  799. align-items: center;
  800. gap: 8px;
  801. font-size: 12px;
  802. font-weight: 600;
  803. margin-bottom: 10px;
  804. .category-dot {
  805. width: 10px;
  806. height: 10px;
  807. border-radius: 50%;
  808. }
  809. .category-count {
  810. color: var(--text-3);
  811. font-weight: normal;
  812. background: var(--bg);
  813. padding: 2px 8px;
  814. border-radius: 10px;
  815. }
  816. }
  817. .category-items {
  818. .category-item {
  819. display: flex;
  820. justify-content: space-between;
  821. padding: 8px 12px;
  822. background: var(--bg);
  823. border-radius: 6px;
  824. margin-bottom: 6px;
  825. cursor: pointer;
  826. font-size: 12px;
  827. transition: all 0.2s;
  828. &:hover {
  829. background: var(--primary-light);
  830. }
  831. .item-value {
  832. color: var(--text-3);
  833. }
  834. }
  835. }
  836. }
  837. .context-menu {
  838. position: fixed;
  839. min-width: 180px;
  840. background: #fff;
  841. border-radius: 10px;
  842. box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  843. z-index: 3000;
  844. overflow: hidden;
  845. .context-menu-item {
  846. display: flex;
  847. align-items: center;
  848. gap: 10px;
  849. padding: 10px 14px;
  850. font-size: 13px;
  851. cursor: pointer;
  852. transition: all 0.15s;
  853. &:hover {
  854. background: var(--primary-light);
  855. color: var(--primary);
  856. }
  857. .icon {
  858. font-size: 14px;
  859. }
  860. }
  861. }
  862. .graph-container {
  863. height: 500px;
  864. position: relative;
  865. background: linear-gradient(135deg, #f8fafc 0%, #f0f4f8 100%);
  866. border-radius: 8px;
  867. .graph-legend {
  868. position: absolute;
  869. top: 16px;
  870. left: 16px;
  871. background: #fff;
  872. border-radius: 8px;
  873. padding: 12px 16px;
  874. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  875. .legend-title {
  876. font-size: 12px;
  877. font-weight: 600;
  878. margin-bottom: 8px;
  879. color: var(--text-2);
  880. }
  881. .legend-item {
  882. display: flex;
  883. align-items: center;
  884. gap: 8px;
  885. font-size: 11px;
  886. color: var(--text-2);
  887. margin-bottom: 4px;
  888. }
  889. .legend-dot {
  890. width: 12px;
  891. height: 12px;
  892. border-radius: 50%;
  893. &.entity { background: var(--primary); }
  894. &.concept { background: #722ed1; }
  895. &.data { background: var(--success); }
  896. &.location { background: var(--warning); }
  897. }
  898. }
  899. .graph-body {
  900. height: 100%;
  901. display: flex;
  902. align-items: center;
  903. justify-content: center;
  904. .graph-placeholder {
  905. text-align: center;
  906. color: var(--text-3);
  907. p {
  908. margin-top: 12px;
  909. }
  910. }
  911. }
  912. }
  913. // 空白编辑器占位提示样式
  914. :deep(.empty-editor-placeholder) {
  915. padding: 60px 40px;
  916. text-align: center;
  917. color: var(--text-2);
  918. h2 {
  919. font-size: 24px;
  920. margin-bottom: 20px;
  921. color: var(--text-1);
  922. }
  923. p {
  924. font-size: 15px;
  925. margin-bottom: 16px;
  926. }
  927. ul {
  928. list-style: none;
  929. padding: 0;
  930. text-align: left;
  931. max-width: 300px;
  932. margin: 0 auto;
  933. li {
  934. padding: 8px 0;
  935. padding-left: 24px;
  936. position: relative;
  937. font-size: 14px;
  938. &::before {
  939. content: '✓';
  940. position: absolute;
  941. left: 0;
  942. color: var(--primary);
  943. }
  944. }
  945. }
  946. }
  947. </style>