| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071 |
- <template>
- <div class="editor-page">
- <!-- 工具栏 -->
- <div class="editor-toolbar">
- <el-button :icon="ArrowLeft" @click="goBack">返回</el-button>
- <el-input
- v-model="reportTitle"
- class="title-input"
- placeholder="请输入报告标题"
- />
- <span class="save-status" v-if="saved">✓ 已保存</span>
- <div class="toolbar-right">
- <el-button :icon="Clock">版本</el-button>
- <el-button :icon="Share">分享</el-button>
- <el-divider direction="vertical" />
- <el-button type="primary" :icon="Check" @click="handleSave">保存</el-button>
- </div>
- </div>
- <!-- 主体 -->
- <div class="editor-body">
- <!-- 左侧文件面板 -->
- <div class="left-panel">
- <div class="panel-header">
- <span>📁 来源文件</span>
- <span class="file-count">{{ sourceFiles.length }}个</span>
- </div>
- <div class="panel-body">
- <!-- 上传区 -->
- <el-upload
- class="upload-zone"
- drag
- action="/api/v1/parse/upload"
- :on-success="handleFileUpload"
- :show-file-list="false"
- >
- <div class="upload-content">
- <div class="upload-icon">📄</div>
- <div class="upload-text">拖拽或点击上传</div>
- <div class="upload-hint">支持 PDF / Word / Excel</div>
- </div>
- </el-upload>
- <!-- 来源文件列表 -->
- <div class="file-list">
- <div
- v-for="file in sourceFiles"
- :key="file.id"
- class="file-item"
- :class="{ active: selectedFile?.id === file.id }"
- @click="selectFile(file)"
- >
- <span class="file-icon">{{ getFileIcon(file) }}</span>
- <div class="file-info">
- <div class="file-name">{{ file.alias }}</div>
- <div class="file-meta">
- <span v-if="file.required" class="required">必需</span>
- <span v-else>可选</span>
- </div>
- </div>
- <el-button
- size="small"
- :icon="Delete"
- circle
- @click.stop="removeSourceFile(file)"
- />
- </div>
- </div>
- <!-- 添加来源文件定义 -->
- <el-button
- class="add-source-btn"
- :icon="Plus"
- @click="showAddSourceDialog = true"
- >
- 添加来源文件定义
- </el-button>
- </div>
- </div>
- <!-- 中间编辑区 -->
- <div class="center-panel">
- <div class="editor-title-bar">
- <h2>{{ reportTitle }}</h2>
- <div class="view-toggle">
- <el-radio-group v-model="viewMode" size="small">
- <el-radio-button label="edit">📝 编辑</el-radio-button>
- <el-radio-button label="preview">👁 预览</el-radio-button>
- </el-radio-group>
- </div>
- <el-button :icon="Share" circle @click="showGraphModal = true" />
- </div>
- <div class="editor-scroll" ref="editorRef">
- <div
- class="editor-content"
- contenteditable="true"
- @mouseup="handleTextSelection"
- v-html="documentContent"
- />
- </div>
- </div>
- <!-- 右侧变量面板 -->
- <div class="right-panel">
- <!-- 变量管理 -->
- <div class="element-section">
- <div class="element-header">
- <span class="element-title">
- 🏷️ 变量管理
- <span class="element-count">({{ variables.length }})</span>
- </span>
- <el-button size="small" :icon="Plus" @click="showAddVariableDialog = true">
- 添加
- </el-button>
- </div>
- <div class="element-body">
- <div class="element-tags-wrap">
- <div
- v-for="variable in variables"
- :key="variable.id"
- class="var-tag"
- :class="variable.category"
- @click="editVariable(variable)"
- >
- <span class="tag-icon">{{ getCategoryIcon(variable.category) }}</span>
- <span class="tag-name">{{ variable.displayName }}</span>
- </div>
- </div>
- <div class="element-hint" v-if="variables.length === 0">
- 选中文本后右键标记为变量
- </div>
- </div>
- </div>
- <!-- 按类别分组显示 -->
- <div class="category-section" v-for="(vars, category) in groupedVariables" :key="category">
- <div class="category-header">
- <span
- class="category-dot"
- :style="{ background: getCategoryColor(category) }"
- />
- <span>{{ getCategoryLabel(category) }}</span>
- <span class="category-count">{{ vars.length }}</span>
- </div>
- <div class="category-items">
- <div
- v-for="v in vars"
- :key="v.id"
- class="category-item"
- @click="editVariable(v)"
- >
- <span>{{ v.displayName }}</span>
- <span class="item-value">{{ v.exampleValue || '-' }}</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 右键菜单 -->
- <div
- v-show="contextMenuVisible"
- class="context-menu"
- :style="{ left: contextMenuPos.x + 'px', top: contextMenuPos.y + 'px' }"
- >
- <div class="context-menu-item" @click="markAsVariable('entity')">
- <span class="icon">🏢</span>
- <span>标记为核心实体</span>
- </div>
- <div class="context-menu-item" @click="markAsVariable('concept')">
- <span class="icon">💡</span>
- <span>标记为概念/技术</span>
- </div>
- <div class="context-menu-item" @click="markAsVariable('data')">
- <span class="icon">📊</span>
- <span>标记为数据/指标</span>
- </div>
- <div class="context-menu-item" @click="markAsVariable('location')">
- <span class="icon">📍</span>
- <span>标记为地点/组织</span>
- </div>
- <div class="context-menu-item" @click="markAsVariable('asset')">
- <span class="icon">📑</span>
- <span>标记为资源模板</span>
- </div>
- </div>
- <!-- 添加来源文件对话框 -->
- <el-dialog v-model="showAddSourceDialog" title="添加来源文件定义" width="400">
- <el-form :model="newSourceFile" label-width="80px">
- <el-form-item label="文件别名" required>
- <el-input v-model="newSourceFile.alias" placeholder="如:可研批复" />
- </el-form-item>
- <el-form-item label="描述">
- <el-input v-model="newSourceFile.description" placeholder="文件描述" />
- </el-form-item>
- <el-form-item label="是否必需">
- <el-switch v-model="newSourceFile.required" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="showAddSourceDialog = false">取消</el-button>
- <el-button type="primary" @click="addSourceFile">添加</el-button>
- </template>
- </el-dialog>
- <!-- 添加/编辑变量对话框 -->
- <el-dialog v-model="showVariableDialog" :title="editingVariable ? '编辑变量' : '添加变量'" width="500">
- <el-form :model="variableForm" label-width="100px">
- <el-form-item label="变量名" required>
- <el-input v-model="variableForm.name" placeholder="程序用名,如 project_name" />
- </el-form-item>
- <el-form-item label="显示名称" required>
- <el-input v-model="variableForm.displayName" placeholder="用户可见名称" />
- </el-form-item>
- <el-form-item label="类别">
- <el-select v-model="variableForm.category" style="width: 100%">
- <el-option label="核心实体" value="entity" />
- <el-option label="概念/技术" value="concept" />
- <el-option label="数据/指标" value="data" />
- <el-option label="地点/组织" value="location" />
- <el-option label="资源模板" value="asset" />
- </el-select>
- </el-form-item>
- <el-form-item label="示例值">
- <el-input v-model="variableForm.exampleValue" placeholder="文档中的原始值" />
- </el-form-item>
- <el-form-item label="来源类型">
- <el-select v-model="variableForm.sourceType" style="width: 100%">
- <el-option label="从来源文件提取" value="document" />
- <el-option label="手动输入" value="manual" />
- <el-option label="引用其他变量" value="reference" />
- <el-option label="固定值" value="fixed" />
- </el-select>
- </el-form-item>
- <el-form-item label="来源文件" v-if="variableForm.sourceType === 'document'">
- <el-select v-model="variableForm.sourceFileAlias" style="width: 100%">
- <el-option
- v-for="sf in sourceFiles"
- :key="sf.id"
- :label="sf.alias"
- :value="sf.alias"
- />
- </el-select>
- </el-form-item>
- <el-form-item label="提取方式" v-if="variableForm.sourceType === 'document'">
- <el-select v-model="variableForm.extractType" style="width: 100%">
- <el-option label="直接提取" value="direct" />
- <el-option label="AI 字段提取" value="ai_extract" />
- <el-option label="AI 总结" value="ai_summarize" />
- </el-select>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="showVariableDialog = false">取消</el-button>
- <el-button
- v-if="editingVariable"
- type="danger"
- @click="deleteVariable"
- >
- 删除
- </el-button>
- <el-button type="primary" @click="saveVariable">保存</el-button>
- </template>
- </el-dialog>
- <!-- 知识图谱弹窗 -->
- <el-dialog v-model="showGraphModal" title="🔗 知识图谱" width="900">
- <div class="graph-container">
- <div class="graph-legend">
- <div class="legend-title">图例</div>
- <div class="legend-item">
- <span class="legend-dot entity"></span>
- <span>核心实体</span>
- </div>
- <div class="legend-item">
- <span class="legend-dot concept"></span>
- <span>概念/技术</span>
- </div>
- <div class="legend-item">
- <span class="legend-dot data"></span>
- <span>数据/指标</span>
- </div>
- <div class="legend-item">
- <span class="legend-dot location"></span>
- <span>地点/组织</span>
- </div>
- </div>
- <div class="graph-body">
- <div class="graph-placeholder">
- <el-icon size="64" color="#ccc"><Connection /></el-icon>
- <p>知识图谱可视化(开发中)</p>
- </div>
- </div>
- </div>
- </el-dialog>
- </div>
- </template>
- <script setup>
- import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
- import { useRouter, useRoute } from 'vue-router'
- import {
- ArrowLeft, Clock, Share, Check, Plus, Delete, Connection
- } from '@element-plus/icons-vue'
- import { ElMessage } from 'element-plus'
- import { useTemplateStore } from '@/stores/template'
- import { documentApi } from '@/api'
- const router = useRouter()
- const route = useRoute()
- const templateStore = useTemplateStore()
- const templateId = route.params.templateId
- const reportTitle = ref('')
- const viewMode = ref('edit')
- const saved = ref(true)
- const editorRef = ref(null)
- const loading = ref(false)
- // 来源文件(从 API 获取)
- const sourceFiles = ref([])
- const selectedFile = ref(null)
- const showAddSourceDialog = ref(false)
- const newSourceFile = reactive({
- alias: '',
- description: '',
- required: true
- })
- // 变量(从 API 获取)
- const variables = ref([])
- // 加载模板数据
- onMounted(async () => {
- await fetchTemplateData()
- })
- async function fetchTemplateData() {
- loading.value = true
- try {
- await templateStore.fetchTemplateDetail(templateId)
-
- // 设置模板标题
- reportTitle.value = templateStore.currentTemplate?.name || '未命名模板'
-
- // 设置来源文件
- sourceFiles.value = templateStore.sourceFiles || []
-
- // 设置变量
- variables.value = templateStore.variables || []
-
- // 根据 baseDocumentId 获取文档结构化内容
- const baseDocumentId = templateStore.currentTemplate?.baseDocumentId
- if (baseDocumentId) {
- try {
- const structuredDoc = await documentApi.getStructured(baseDocumentId)
- // 将结构化文档的 blocks 转换为 HTML 内容
- if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
- // 优先使用 markedHtml(带实体标注),其次使用 html
- documentContent.value = structuredDoc.blocks
- .map(block => block.markedHtml || block.html || block.plainText || '')
- .join('')
- } else {
- documentContent.value = emptyPlaceholder
- }
- } catch (docError) {
- console.warn('获取文档内容失败:', docError)
- documentContent.value = emptyPlaceholder
- }
- } else {
- documentContent.value = emptyPlaceholder
- }
- } catch (error) {
- console.error('加载模板失败:', error)
- ElMessage.error('加载模板失败')
- } finally {
- loading.value = false
- }
- }
- const showVariableDialog = ref(false)
- const showAddVariableDialog = ref(false)
- const editingVariable = ref(null)
- const variableForm = reactive({
- name: '',
- displayName: '',
- category: 'entity',
- exampleValue: '',
- sourceType: 'document',
- sourceFileAlias: '',
- extractType: 'direct'
- })
- // 右键菜单
- const contextMenuVisible = ref(false)
- const contextMenuPos = reactive({ x: 0, y: 0 })
- const selectedText = ref('')
- const selectionRange = ref(null)
- // 知识图谱
- const showGraphModal = ref(false)
- // 文档内容(从 API 获取或空白)
- const documentContent = ref('')
- // 空白模板时的占位提示
- const emptyPlaceholder = `
- <div class="empty-editor-placeholder">
- <h2>📝 开始编辑您的模板</h2>
- <p>这是一个空白模板。您可以:</p>
- <ul>
- <li>在左侧添加来源文件定义</li>
- <li>在右侧面板添加变量</li>
- <li>直接在此处编辑模板内容</li>
- <li>选中文本后右键将其标记为变量</li>
- </ul>
- </div>
- `
- // 计算属性
- const groupedVariables = computed(() => {
- const groups = {}
- variables.value.forEach(v => {
- const cat = v.category || 'other'
- if (!groups[cat]) groups[cat] = []
- groups[cat].push(v)
- })
- return groups
- })
- // 方法
- function goBack() {
- router.back()
- }
- function handleSave() {
- saved.value = true
- ElMessage.success('保存成功')
- }
- function getFileIcon(file) {
- return '📄'
- }
- function selectFile(file) {
- selectedFile.value = file
- }
- async function removeSourceFile(file) {
- try {
- await templateStore.deleteSourceFile(file.id)
- sourceFiles.value = sourceFiles.value.filter(f => f.id !== file.id)
- ElMessage.success('删除成功')
- } catch (error) {
- ElMessage.error('删除失败: ' + error.message)
- }
- }
- async function addSourceFile() {
- if (!newSourceFile.alias) {
- ElMessage.warning('请输入文件别名')
- return
- }
- try {
- const sf = await templateStore.addSourceFile(templateId, newSourceFile)
- sourceFiles.value.push(sf)
- showAddSourceDialog.value = false
- Object.assign(newSourceFile, { alias: '', description: '', required: true })
- ElMessage.success('添加成功')
- } catch (error) {
- ElMessage.error('添加失败: ' + error.message)
- }
- }
- function getCategoryIcon(category) {
- const icons = {
- entity: '🏢',
- concept: '💡',
- data: '📊',
- location: '📍',
- asset: '📑'
- }
- return icons[category] || '📌'
- }
- function getCategoryColor(category) {
- const colors = {
- entity: '#1890ff',
- concept: '#722ed1',
- data: '#52c41a',
- location: '#faad14',
- asset: '#eb2f96'
- }
- return colors[category] || '#8c8c8c'
- }
- function getCategoryLabel(category) {
- const labels = {
- entity: '核心实体',
- concept: '概念/技术',
- data: '数据/指标',
- location: '地点/组织',
- asset: '资源模板'
- }
- return labels[category] || '其他'
- }
- function editVariable(variable) {
- editingVariable.value = variable
- Object.assign(variableForm, variable)
- showVariableDialog.value = true
- }
- async function saveVariable() {
- if (!variableForm.name || !variableForm.displayName) {
- ElMessage.warning('请填写必要字段')
- return
- }
- try {
- if (editingVariable.value) {
- // 更新
- const updated = await templateStore.updateVariable(editingVariable.value.id, variableForm)
- Object.assign(editingVariable.value, updated)
- ElMessage.success('更新成功')
- } else {
- // 新增
- const newVar = await templateStore.addVariable(templateId, variableForm)
- variables.value.push(newVar)
- ElMessage.success('添加成功')
- }
- showVariableDialog.value = false
- resetVariableForm()
- } catch (error) {
- ElMessage.error('保存失败: ' + error.message)
- }
- }
- async function deleteVariable() {
- if (editingVariable.value) {
- try {
- await templateStore.deleteVariable(editingVariable.value.id)
- variables.value = variables.value.filter(v => v.id !== editingVariable.value.id)
- showVariableDialog.value = false
- resetVariableForm()
- ElMessage.success('删除成功')
- } catch (error) {
- ElMessage.error('删除失败: ' + error.message)
- }
- }
- }
- function resetVariableForm() {
- editingVariable.value = null
- Object.assign(variableForm, {
- name: '',
- displayName: '',
- category: 'entity',
- exampleValue: '',
- sourceType: 'document',
- sourceFileAlias: '',
- extractType: 'direct'
- })
- }
- function handleTextSelection(event) {
- const selection = window.getSelection()
- const text = selection.toString().trim()
- if (text) {
- selectedText.value = text
- selectionRange.value = selection.getRangeAt(0)
- contextMenuPos.x = event.clientX
- contextMenuPos.y = event.clientY
- contextMenuVisible.value = true
- } else {
- contextMenuVisible.value = false
- }
- }
- function markAsVariable(category) {
- if (!selectedText.value) return
- // 生成变量名
- const varName = 'var_' + Date.now()
- // 添加变量
- variables.value.push({
- id: Date.now().toString(),
- name: varName,
- displayName: selectedText.value.slice(0, 20),
- category,
- exampleValue: selectedText.value,
- sourceType: 'document'
- })
- // 关闭菜单
- contextMenuVisible.value = false
- selectedText.value = ''
- ElMessage.success('变量标记成功')
- }
- function handleFileUpload(response) {
- if (response.code === 200) {
- ElMessage.success('文件上传成功')
- }
- }
- // 点击其他地方关闭右键菜单
- function handleClickOutside(event) {
- if (!event.target.closest('.context-menu')) {
- contextMenuVisible.value = false
- }
- }
- onMounted(() => {
- document.addEventListener('click', handleClickOutside)
- })
- onUnmounted(() => {
- document.removeEventListener('click', handleClickOutside)
- })
- </script>
- <style lang="scss" scoped>
- .editor-page {
- height: calc(100vh - 56px);
- display: flex;
- flex-direction: column;
- background: var(--bg);
- }
- .editor-toolbar {
- height: 56px;
- background: #fff;
- border-bottom: 1px solid var(--border);
- display: flex;
- align-items: center;
- padding: 0 16px;
- gap: 16px;
- flex-shrink: 0;
- .title-input {
- width: 300px;
- :deep(.el-input__wrapper) {
- box-shadow: none;
- background: transparent;
- &:hover {
- background: var(--bg);
- }
- }
- }
- .save-status {
- color: var(--success);
- font-size: 13px;
- }
- .toolbar-right {
- margin-left: auto;
- display: flex;
- gap: 8px;
- align-items: center;
- }
- }
- .editor-body {
- flex: 1;
- display: flex;
- overflow: hidden;
- }
- .left-panel {
- width: 260px;
- background: #fff;
- border-right: 1px solid var(--border);
- display: flex;
- flex-direction: column;
- flex-shrink: 0;
- .panel-header {
- padding: 14px 16px;
- border-bottom: 1px solid var(--border);
- font-size: 13px;
- font-weight: 600;
- display: flex;
- justify-content: space-between;
- .file-count {
- color: var(--text-3);
- font-weight: normal;
- }
- }
- .panel-body {
- flex: 1;
- overflow-y: auto;
- padding: 12px;
- }
- }
- .upload-zone {
- border: 2px dashed var(--border);
- border-radius: 10px;
- margin-bottom: 16px;
- :deep(.el-upload-dragger) {
- padding: 20px;
- border: none;
- background: transparent;
- }
- .upload-content {
- text-align: center;
- }
- .upload-icon {
- font-size: 32px;
- margin-bottom: 8px;
- }
- .upload-text {
- font-size: 13px;
- color: var(--text-2);
- }
- .upload-hint {
- font-size: 11px;
- color: var(--text-3);
- }
- }
- .file-list {
- margin-bottom: 16px;
- }
- .file-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 12px;
- background: #fff;
- border: 1px solid var(--border);
- border-radius: 8px;
- margin-bottom: 8px;
- cursor: pointer;
- transition: all 0.2s;
- &:hover, &.active {
- border-color: var(--primary);
- background: var(--primary-light);
- }
- .file-icon {
- font-size: 24px;
- }
- .file-info {
- flex: 1;
- min-width: 0;
- .file-name {
- font-size: 12px;
- font-weight: 500;
- }
- .file-meta {
- font-size: 11px;
- color: var(--text-3);
- .required {
- color: var(--danger);
- }
- }
- }
- }
- .add-source-btn {
- width: 100%;
- }
- .center-panel {
- flex: 1;
- display: flex;
- flex-direction: column;
- background: #fff;
- overflow: hidden;
- .editor-title-bar {
- padding: 16px 24px;
- border-bottom: 1px solid var(--border);
- display: flex;
- align-items: center;
- gap: 12px;
- h2 {
- flex: 1;
- font-size: 18px;
- font-weight: 600;
- }
- }
- .editor-scroll {
- flex: 1;
- overflow-y: auto;
- padding: 24px 32px;
- }
- .editor-content {
- max-width: 800px;
- margin: 0 auto;
- outline: none;
- :deep(h1) {
- font-size: 24px;
- font-weight: 700;
- margin-bottom: 24px;
- }
- :deep(h2) {
- font-size: 18px;
- font-weight: 600;
- margin: 28px 0 16px;
- }
- :deep(p) {
- margin-bottom: 16px;
- line-height: 1.8;
- }
- :deep(ul) {
- margin-bottom: 16px;
- padding-left: 24px;
- li {
- margin-bottom: 8px;
- }
- }
- }
- }
- .right-panel {
- width: 320px;
- background: #fff;
- border-left: 1px solid var(--border);
- overflow-y: auto;
- flex-shrink: 0;
- }
- .element-section {
- border-bottom: 1px solid var(--border);
- .element-header {
- padding: 14px 16px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- .element-title {
- font-size: 13px;
- font-weight: 600;
- .element-count {
- color: var(--text-3);
- font-weight: normal;
- }
- }
- }
- .element-body {
- padding: 0 16px 16px;
- }
- .element-tags-wrap {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }
- .element-hint {
- font-size: 12px;
- color: var(--text-3);
- text-align: center;
- padding: 20px;
- }
- }
- .category-section {
- padding: 12px 16px;
- border-bottom: 1px solid var(--border);
- .category-header {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 12px;
- font-weight: 600;
- margin-bottom: 10px;
- .category-dot {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- }
- .category-count {
- color: var(--text-3);
- font-weight: normal;
- background: var(--bg);
- padding: 2px 8px;
- border-radius: 10px;
- }
- }
- .category-items {
- .category-item {
- display: flex;
- justify-content: space-between;
- padding: 8px 12px;
- background: var(--bg);
- border-radius: 6px;
- margin-bottom: 6px;
- cursor: pointer;
- font-size: 12px;
- transition: all 0.2s;
- &:hover {
- background: var(--primary-light);
- }
- .item-value {
- color: var(--text-3);
- }
- }
- }
- }
- .context-menu {
- position: fixed;
- min-width: 180px;
- background: #fff;
- border-radius: 10px;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
- z-index: 3000;
- overflow: hidden;
- .context-menu-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 14px;
- font-size: 13px;
- cursor: pointer;
- transition: all 0.15s;
- &:hover {
- background: var(--primary-light);
- color: var(--primary);
- }
- .icon {
- font-size: 14px;
- }
- }
- }
- .graph-container {
- height: 500px;
- position: relative;
- background: linear-gradient(135deg, #f8fafc 0%, #f0f4f8 100%);
- border-radius: 8px;
- .graph-legend {
- position: absolute;
- top: 16px;
- left: 16px;
- background: #fff;
- border-radius: 8px;
- padding: 12px 16px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- .legend-title {
- font-size: 12px;
- font-weight: 600;
- margin-bottom: 8px;
- color: var(--text-2);
- }
- .legend-item {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 11px;
- color: var(--text-2);
- margin-bottom: 4px;
- }
- .legend-dot {
- width: 12px;
- height: 12px;
- border-radius: 50%;
- &.entity { background: var(--primary); }
- &.concept { background: #722ed1; }
- &.data { background: var(--success); }
- &.location { background: var(--warning); }
- }
- }
- .graph-body {
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- .graph-placeholder {
- text-align: center;
- color: var(--text-3);
- p {
- margin-top: 12px;
- }
- }
- }
- }
- // 空白编辑器占位提示样式
- :deep(.empty-editor-placeholder) {
- padding: 60px 40px;
- text-align: center;
- color: var(--text-2);
- h2 {
- font-size: 24px;
- margin-bottom: 20px;
- color: var(--text-1);
- }
- p {
- font-size: 15px;
- margin-bottom: 16px;
- }
- ul {
- list-style: none;
- padding: 0;
- text-align: left;
- max-width: 300px;
- margin: 0 auto;
- li {
- padding: 8px 0;
- padding-left: 24px;
- position: relative;
- font-size: 14px;
- &::before {
- content: '✓';
- position: absolute;
- left: 0;
- color: var(--primary);
- }
- }
- }
- }
- </style>
|