|
@@ -301,21 +301,56 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="element-body">
|
|
<div class="element-body">
|
|
|
- <div class="element-tags-wrap" v-if="filteredEntities && filteredEntities.length > 0">
|
|
|
|
|
- <div
|
|
|
|
|
- v-for="entity in filteredEntities"
|
|
|
|
|
- :key="entity.id"
|
|
|
|
|
- class="var-tag"
|
|
|
|
|
- :class="[getEntityTypeClass(entity.type), { confirmed: entity.confirmed }]"
|
|
|
|
|
- :title="`${getEntityTypeName(entity.type)}: ${entity.text}`"
|
|
|
|
|
- @click="scrollToEntity(entity.id)"
|
|
|
|
|
- @dblclick="openEntityEditModal(entity)"
|
|
|
|
|
- >
|
|
|
|
|
- <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 class="entity-group my-entities" v-if="myEntities.length > 0">
|
|
|
|
|
+ <div class="entity-group-header">
|
|
|
|
|
+ <span class="group-title">✓ 我的要素</span>
|
|
|
|
|
+ <span class="group-count">{{ myEntities.length }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="element-tags-wrap">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="entity in myEntities"
|
|
|
|
|
+ :key="entity.id"
|
|
|
|
|
+ class="var-tag confirmed"
|
|
|
|
|
+ :class="[getEntityTypeClass(entity.type)]"
|
|
|
|
|
+ :title="`${getEntityTypeName(entity.type)}: ${entity.text}`"
|
|
|
|
|
+ @click="scrollToEntity(entity.id)"
|
|
|
|
|
+ @dblclick="openEntityEditModal(entity)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="tag-icon">{{ getEntityTypeIcon(entity.type) }}</span>
|
|
|
|
|
+ <span class="tag-name">{{ entity.text }}</span>
|
|
|
|
|
+ <span class="tag-status">✓</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- AI 识别(待采纳) -->
|
|
|
|
|
+ <div class="entity-group ai-suggestions" v-if="aiSuggestedEntities.length > 0">
|
|
|
|
|
+ <div class="entity-group-header">
|
|
|
|
|
+ <span class="group-title">💡 AI 识别</span>
|
|
|
|
|
+ <span class="group-count">{{ aiSuggestedEntities.length }}</span>
|
|
|
|
|
+ <div class="group-actions">
|
|
|
|
|
+ <el-button size="small" text type="primary" @click="adoptAllAiSuggestions">全部采纳</el-button>
|
|
|
|
|
+ <el-button size="small" text @click="ignoreAllAiSuggestions">全部忽略</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="element-tags-wrap">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="entity in aiSuggestedEntities"
|
|
|
|
|
+ :key="entity.id"
|
|
|
|
|
+ class="var-tag ai-suggestion"
|
|
|
|
|
+ :class="[getEntityTypeClass(entity.type)]"
|
|
|
|
|
+ :title="`${getEntityTypeName(entity.type)}: ${entity.text} - 点击采纳`"
|
|
|
|
|
+ @click="adoptEntity(entity)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="tag-icon">{{ getEntityTypeIcon(entity.type) }}</span>
|
|
|
|
|
+ <span class="tag-name">{{ entity.text }}</span>
|
|
|
|
|
+ <span class="tag-action">+</span>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 空状态提示 -->
|
|
|
<div class="element-hint" v-if="!entities || entities.length === 0">
|
|
<div class="element-hint" v-if="!entities || entities.length === 0">
|
|
|
选中文本后右键标记为实体
|
|
选中文本后右键标记为实体
|
|
|
</div>
|
|
</div>
|
|
@@ -1158,6 +1193,16 @@ const filteredEntities = computed(() => {
|
|
|
return allFilteredEntities.value || []
|
|
return allFilteredEntities.value || []
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 计算属性:我的要素(已确认)
|
|
|
|
|
+const myEntities = computed(() => {
|
|
|
|
|
+ return filteredEntities.value.filter(e => e.confirmed)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 计算属性:AI 识别的要素(未确认)
|
|
|
|
|
+const aiSuggestedEntities = computed(() => {
|
|
|
|
|
+ return filteredEntities.value.filter(e => !e.confirmed)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
// 切换类型筛选
|
|
// 切换类型筛选
|
|
|
function toggleEntityTypeFilter(type) {
|
|
function toggleEntityTypeFilter(type) {
|
|
|
if (entityTypeFilter.value === type) {
|
|
if (entityTypeFilter.value === type) {
|
|
@@ -1167,6 +1212,60 @@ function toggleEntityTypeFilter(type) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 采纳 AI 建议的实体
|
|
|
|
|
+function adoptEntity(entity) {
|
|
|
|
|
+ entity.confirmed = true
|
|
|
|
|
+ // 重新渲染文档以更新高亮
|
|
|
|
|
+ refreshDocumentHighlight()
|
|
|
|
|
+ ElMessage.success(`已采纳要素「${entity.text}」`)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 忽略 AI 建议的实体
|
|
|
|
|
+function ignoreEntity(entity) {
|
|
|
|
|
+ const index = entities.value.findIndex(e => e.id === entity.id)
|
|
|
|
|
+ if (index !== -1) {
|
|
|
|
|
+ entities.value.splice(index, 1)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 忽略所有 AI 建议
|
|
|
|
|
+function ignoreAllAiSuggestions() {
|
|
|
|
|
+ entities.value = entities.value.filter(e => e.confirmed)
|
|
|
|
|
+ ElMessage.success('已清除所有 AI 建议')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 采纳所有 AI 建议
|
|
|
|
|
+function adoptAllAiSuggestions() {
|
|
|
|
|
+ entities.value.forEach(e => {
|
|
|
|
|
+ if (!e.confirmed) {
|
|
|
|
|
+ e.confirmed = true
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ refreshDocumentHighlight()
|
|
|
|
|
+ ElMessage.success('已采纳所有 AI 建议')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 刷新文档高亮
|
|
|
|
|
+function refreshDocumentHighlight() {
|
|
|
|
|
+ if (blocks.value && blocks.value.length > 0) {
|
|
|
|
|
+ // 更新 blocks 中的 confirmed 状态
|
|
|
|
|
+ blocks.value.forEach(block => {
|
|
|
|
|
+ if (block.elements) {
|
|
|
|
|
+ block.elements.forEach(el => {
|
|
|
|
|
+ if (el.type === 'entity') {
|
|
|
|
|
+ const matchingEntity = entities.value.find(e => e.id === el.entityId)
|
|
|
|
|
+ if (matchingEntity) {
|
|
|
|
|
+ el.confirmed = matchingEntity.confirmed
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ // 重新渲染
|
|
|
|
|
+ documentContent.value = renderStructuredDocument({ blocks: blocks.value })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 获取实体类型名称(支持后端返回的英文类型)
|
|
// 获取实体类型名称(支持后端返回的英文类型)
|
|
|
function getEntityTypeName(type) {
|
|
function getEntityTypeName(type) {
|
|
|
const typeNames = {
|
|
const typeNames = {
|
|
@@ -1582,7 +1681,7 @@ function renderTable(table, entityMap) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 从 blocks 中构建实体映射
|
|
|
|
|
|
|
+ * 从 blocks 中构建实体映射(只包含已确认的实体用于高亮)
|
|
|
* 返回 { entityText: { entityId, entityType, confirmed } }
|
|
* 返回 { entityText: { entityId, entityType, confirmed } }
|
|
|
*/
|
|
*/
|
|
|
function buildEntityMap(blocks) {
|
|
function buildEntityMap(blocks) {
|
|
@@ -1592,7 +1691,8 @@ function buildEntityMap(blocks) {
|
|
|
if (!block.elements) return
|
|
if (!block.elements) return
|
|
|
|
|
|
|
|
block.elements.forEach(el => {
|
|
block.elements.forEach(el => {
|
|
|
- if (el.type === 'entity' && el.entityText) {
|
|
|
|
|
|
|
+ // 只有已确认的实体才会被高亮显示
|
|
|
|
|
+ if (el.type === 'entity' && el.entityText && el.confirmed) {
|
|
|
// 使用实体文本作为 key(可能有多个相同文本的实体)
|
|
// 使用实体文本作为 key(可能有多个相同文本的实体)
|
|
|
if (!entityMap.has(el.entityText)) {
|
|
if (!entityMap.has(el.entityText)) {
|
|
|
entityMap.set(el.entityText, [])
|
|
entityMap.set(el.entityText, [])
|
|
@@ -3918,6 +4018,56 @@ onUnmounted(() => {
|
|
|
|
|
|
|
|
.element-body {
|
|
.element-body {
|
|
|
padding: 0;
|
|
padding: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 实体分组
|
|
|
|
|
+ .entity-group {
|
|
|
|
|
+ .entity-group-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ padding-bottom: 8px;
|
|
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
|
|
+
|
|
|
|
|
+ .group-title {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: var(--text-1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .group-count {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--text-3);
|
|
|
|
|
+ background: var(--bg);
|
|
|
|
|
+ padding: 2px 8px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .group-actions {
|
|
|
|
|
+ margin-left: auto;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+
|
|
|
|
|
+ .el-button {
|
|
|
|
|
+ padding: 4px 8px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.ai-suggestions {
|
|
|
|
|
+ .entity-group-header {
|
|
|
|
|
+ border-bottom-style: dashed;
|
|
|
|
|
+
|
|
|
|
|
+ .group-title {
|
|
|
|
|
+ color: var(--text-2);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 要素标签容器 - V2 风格
|
|
// 要素标签容器 - V2 风格
|
|
@@ -3994,6 +4144,46 @@ onUnmounted(() => {
|
|
|
font-size: 10px;
|
|
font-size: 10px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ .tag-action {
|
|
|
|
|
+ color: var(--primary);
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ margin-left: 2px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 已确认的要素
|
|
|
|
|
+ &.confirmed {
|
|
|
|
|
+ background: var(--white);
|
|
|
|
|
+ border-color: var(--primary);
|
|
|
|
|
+
|
|
|
|
|
+ .tag-name {
|
|
|
|
|
+ color: var(--text-1);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // AI 建议的要素(虚线边框、淡色)
|
|
|
|
|
+ &.ai-suggestion {
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ border-style: dashed;
|
|
|
|
|
+ border-color: var(--border);
|
|
|
|
|
+ opacity: 0.85;
|
|
|
|
|
+
|
|
|
|
|
+ .tag-name {
|
|
|
|
|
+ color: var(--text-2);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ border-color: var(--primary);
|
|
|
|
|
+ border-style: solid;
|
|
|
|
|
+ background: var(--primary-light);
|
|
|
|
|
+
|
|
|
|
|
+ .tag-action {
|
|
|
|
|
+ transform: scale(1.2);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 动态要素样式(圆角)
|
|
// 动态要素样式(圆角)
|
|
|
&.dynamic {
|
|
&.dynamic {
|
|
|
border-radius: 14px;
|
|
border-radius: 14px;
|