|
@@ -109,14 +109,14 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 右侧变量面板 -->
|
|
|
|
|
|
|
+ <!-- 右侧要素面板 -->
|
|
|
<div class="right-panel">
|
|
<div class="right-panel">
|
|
|
- <!-- 变量管理 -->
|
|
|
|
|
|
|
+ <!-- 要素管理(展示文档中识别的实体) -->
|
|
|
<div class="element-section">
|
|
<div class="element-section">
|
|
|
<div class="element-header">
|
|
<div class="element-header">
|
|
|
<span class="element-title">
|
|
<span class="element-title">
|
|
|
- 🏷️ 变量管理
|
|
|
|
|
- <span class="element-count">({{ variables.length }})</span>
|
|
|
|
|
|
|
+ 🏷️ 要素管理
|
|
|
|
|
+ <span class="element-count">({{ entities.length }})</span>
|
|
|
</span>
|
|
</span>
|
|
|
<el-button size="small" :icon="Plus" @click="showAddVariableDialog = true">
|
|
<el-button size="small" :icon="Plus" @click="showAddVariableDialog = true">
|
|
|
添加
|
|
添加
|
|
@@ -125,18 +125,19 @@
|
|
|
<div class="element-body">
|
|
<div class="element-body">
|
|
|
<div class="element-tags-wrap">
|
|
<div class="element-tags-wrap">
|
|
|
<div
|
|
<div
|
|
|
- v-for="variable in variables"
|
|
|
|
|
- :key="variable.id"
|
|
|
|
|
|
|
+ v-for="entity in entities"
|
|
|
|
|
+ :key="entity.id"
|
|
|
class="var-tag"
|
|
class="var-tag"
|
|
|
- :class="variable.category"
|
|
|
|
|
- @click="editVariable(variable)"
|
|
|
|
|
|
|
+ :class="getEntityTypeClass(entity.type)"
|
|
|
|
|
+ @click="scrollToEntity(entity.id)"
|
|
|
>
|
|
>
|
|
|
- <span class="tag-icon">{{ getCategoryIcon(variable.category) }}</span>
|
|
|
|
|
- <span class="tag-name">{{ variable.displayName }}</span>
|
|
|
|
|
|
|
+ <span class="tag-icon">{{ getEntityTypeIcon(entity.type) }}</span>
|
|
|
|
|
+ <span class="tag-name">{{ entity.text }}</span>
|
|
|
|
|
+ <span class="tag-status" v-if="entity.confirmed">✓</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="element-hint" v-if="variables.length === 0">
|
|
|
|
|
- 选中文本后右键标记为变量
|
|
|
|
|
|
|
+ <div class="element-hint" v-if="entities.length === 0">
|
|
|
|
|
+ 文档解析后将展示识别的要素
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -341,6 +342,44 @@ const newSourceFile = reactive({
|
|
|
// 变量(从 API 获取)
|
|
// 变量(从 API 获取)
|
|
|
const variables = ref([])
|
|
const variables = ref([])
|
|
|
|
|
|
|
|
|
|
+// 文档中的实体(从 blocks 的 elements 中提取)
|
|
|
|
|
+const entities = ref([])
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 从结构化文档的 blocks 中提取所有实体
|
|
|
|
|
+ */
|
|
|
|
|
+function extractEntitiesFromBlocks(blocks) {
|
|
|
|
|
+ const entityList = []
|
|
|
|
|
+ const entityMap = new Map() // 用于去重
|
|
|
|
|
+
|
|
|
|
|
+ if (!blocks || !Array.isArray(blocks)) {
|
|
|
|
|
+ return entityList
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const block of blocks) {
|
|
|
|
|
+ if (!block.elements || !Array.isArray(block.elements)) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const element of block.elements) {
|
|
|
|
|
+ if (element.type === 'entity' && element.entityId) {
|
|
|
|
|
+ // 使用 entityId 去重
|
|
|
|
|
+ if (!entityMap.has(element.entityId)) {
|
|
|
|
|
+ entityMap.set(element.entityId, true)
|
|
|
|
|
+ entityList.push({
|
|
|
|
|
+ id: element.entityId,
|
|
|
|
|
+ text: element.entityText || '',
|
|
|
|
|
+ type: element.entityType || 'ENTITY',
|
|
|
|
|
+ confirmed: element.confirmed || false
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return entityList
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 加载模板数据
|
|
// 加载模板数据
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
await fetchTemplateData()
|
|
await fetchTemplateData()
|
|
@@ -368,15 +407,20 @@ async function fetchTemplateData() {
|
|
|
// 将结构化文档的 blocks 和 images 合并渲染
|
|
// 将结构化文档的 blocks 和 images 合并渲染
|
|
|
if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
|
|
if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
|
|
|
documentContent.value = renderStructuredDocument(structuredDoc)
|
|
documentContent.value = renderStructuredDocument(structuredDoc)
|
|
|
|
|
+ // 提取文档中的实体
|
|
|
|
|
+ entities.value = extractEntitiesFromBlocks(structuredDoc.blocks)
|
|
|
} else {
|
|
} else {
|
|
|
documentContent.value = emptyPlaceholder
|
|
documentContent.value = emptyPlaceholder
|
|
|
|
|
+ entities.value = []
|
|
|
}
|
|
}
|
|
|
} catch (docError) {
|
|
} catch (docError) {
|
|
|
console.warn('获取文档内容失败:', docError)
|
|
console.warn('获取文档内容失败:', docError)
|
|
|
documentContent.value = emptyPlaceholder
|
|
documentContent.value = emptyPlaceholder
|
|
|
|
|
+ entities.value = []
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
documentContent.value = emptyPlaceholder
|
|
documentContent.value = emptyPlaceholder
|
|
|
|
|
+ entities.value = []
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('加载模板失败:', error)
|
|
console.error('加载模板失败:', error)
|
|
@@ -504,6 +548,8 @@ async function handleRegenerateBlocks() {
|
|
|
const structuredDoc = await documentApi.getStructured(baseDocumentId)
|
|
const structuredDoc = await documentApi.getStructured(baseDocumentId)
|
|
|
if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
|
|
if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
|
|
|
documentContent.value = renderStructuredDocument(structuredDoc)
|
|
documentContent.value = renderStructuredDocument(structuredDoc)
|
|
|
|
|
+ // 重新提取实体
|
|
|
|
|
+ entities.value = extractEntitiesFromBlocks(structuredDoc.blocks)
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('重新生成失败:', error)
|
|
console.error('重新生成失败:', error)
|
|
@@ -580,6 +626,72 @@ function getCategoryLabel(category) {
|
|
|
return labels[category] || '其他'
|
|
return labels[category] || '其他'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 根据实体类型获取图标
|
|
|
|
|
+ */
|
|
|
|
|
+function getEntityTypeIcon(type) {
|
|
|
|
|
+ const icons = {
|
|
|
|
|
+ 'PERSON': '👤',
|
|
|
|
|
+ 'ORGANIZATION': '🏢',
|
|
|
|
|
+ 'LOCATION': '📍',
|
|
|
|
|
+ 'DATE': '📅',
|
|
|
|
|
+ 'TIME': '⏰',
|
|
|
|
|
+ 'MONEY': '💰',
|
|
|
|
|
+ 'PERCENT': '📊',
|
|
|
|
|
+ 'PRODUCT': '📦',
|
|
|
|
|
+ 'EVENT': '📋',
|
|
|
|
|
+ 'FACILITY': '🏭',
|
|
|
|
|
+ 'GPE': '🌍',
|
|
|
|
|
+ 'LAW': '⚖️',
|
|
|
|
|
+ 'WORK_OF_ART': '🎨',
|
|
|
|
|
+ 'LANGUAGE': '🗣️',
|
|
|
|
|
+ 'QUANTITY': '🔢',
|
|
|
|
|
+ 'ORDINAL': '🔢',
|
|
|
|
|
+ 'CARDINAL': '🔢',
|
|
|
|
|
+ 'ENTITY': '🏷️'
|
|
|
|
|
+ }
|
|
|
|
|
+ return icons[type?.toUpperCase()] || '🏷️'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 根据实体类型获取样式类名
|
|
|
|
|
+ */
|
|
|
|
|
+function getEntityTypeClass(type) {
|
|
|
|
|
+ const typeMap = {
|
|
|
|
|
+ 'PERSON': 'entity-person',
|
|
|
|
|
+ 'ORGANIZATION': 'entity-org',
|
|
|
|
|
+ 'LOCATION': 'entity-location',
|
|
|
|
|
+ 'DATE': 'entity-date',
|
|
|
|
|
+ 'TIME': 'entity-date',
|
|
|
|
|
+ 'MONEY': 'entity-data',
|
|
|
|
|
+ 'PERCENT': 'entity-data',
|
|
|
|
|
+ 'PRODUCT': 'entity-product',
|
|
|
|
|
+ 'EVENT': 'entity-event',
|
|
|
|
|
+ 'FACILITY': 'entity-org',
|
|
|
|
|
+ 'GPE': 'entity-location',
|
|
|
|
|
+ 'LAW': 'entity-law'
|
|
|
|
|
+ }
|
|
|
|
|
+ return typeMap[type?.toUpperCase()] || 'entity-default'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 滚动到文档中的指定实体
|
|
|
|
|
+ */
|
|
|
|
|
+function scrollToEntity(entityId) {
|
|
|
|
|
+ const editorEl = document.querySelector('.editor-content')
|
|
|
|
|
+ if (!editorEl) return
|
|
|
|
|
+
|
|
|
|
|
+ const entitySpan = editorEl.querySelector(`[data-entity-id="${entityId}"]`)
|
|
|
|
|
+ if (entitySpan) {
|
|
|
|
|
+ entitySpan.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
|
|
|
+ // 添加高亮闪烁效果
|
|
|
|
|
+ entitySpan.classList.add('entity-highlight-flash')
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ entitySpan.classList.remove('entity-highlight-flash')
|
|
|
|
|
+ }, 2000)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function editVariable(variable) {
|
|
function editVariable(variable) {
|
|
|
editingVariable.value = variable
|
|
editingVariable.value = variable
|
|
|
Object.assign(variableForm, variable)
|
|
Object.assign(variableForm, variable)
|
|
@@ -954,6 +1066,80 @@ onUnmounted(() => {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
flex-wrap: wrap;
|
|
|
gap: 8px;
|
|
gap: 8px;
|
|
|
|
|
+ max-height: 300px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .var-tag {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ padding: 4px 10px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ background: var(--bg);
|
|
|
|
|
+ border: 1px solid var(--border);
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ border-color: var(--primary);
|
|
|
|
|
+ background: var(--primary-light);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tag-icon {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tag-name {
|
|
|
|
|
+ max-width: 120px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .tag-status {
|
|
|
|
|
+ color: #52c41a;
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 实体类型样式
|
|
|
|
|
+ &.entity-person {
|
|
|
|
|
+ border-color: #1890ff;
|
|
|
|
|
+ background: rgba(24, 144, 255, 0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+ &.entity-org {
|
|
|
|
|
+ border-color: #722ed1;
|
|
|
|
|
+ background: rgba(114, 46, 209, 0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+ &.entity-location {
|
|
|
|
|
+ border-color: #faad14;
|
|
|
|
|
+ background: rgba(250, 173, 20, 0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+ &.entity-date {
|
|
|
|
|
+ border-color: #13c2c2;
|
|
|
|
|
+ background: rgba(19, 194, 194, 0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+ &.entity-data {
|
|
|
|
|
+ border-color: #52c41a;
|
|
|
|
|
+ background: rgba(82, 196, 26, 0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+ &.entity-product {
|
|
|
|
|
+ border-color: #eb2f96;
|
|
|
|
|
+ background: rgba(235, 47, 150, 0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+ &.entity-event {
|
|
|
|
|
+ border-color: #fa8c16;
|
|
|
|
|
+ background: rgba(250, 140, 22, 0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+ &.entity-law {
|
|
|
|
|
+ border-color: #2f54eb;
|
|
|
|
|
+ background: rgba(47, 84, 235, 0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+ &.entity-default {
|
|
|
|
|
+ border-color: #8c8c8c;
|
|
|
|
|
+ background: rgba(140, 140, 140, 0.1);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.element-hint {
|
|
.element-hint {
|
|
@@ -964,6 +1150,16 @@ onUnmounted(() => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 实体高亮闪烁效果
|
|
|
|
|
+@keyframes entity-flash {
|
|
|
|
|
+ 0%, 100% { background-color: inherit; }
|
|
|
|
|
+ 50% { background-color: #ffe58f; }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.entity-highlight-flash {
|
|
|
|
|
+ animation: entity-flash 0.5s ease-in-out 3;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.category-section {
|
|
.category-section {
|
|
|
padding: 12px 16px;
|
|
padding: 12px 16px;
|
|
|
border-bottom: 1px solid var(--border);
|
|
border-bottom: 1px solid var(--border);
|