Просмотр исходного кода

feat(editor): 要素分组显示 + AI识别不高亮

核心改动:
- AI识别的要素不再在文档中高亮,只在右侧面板显示
- 只有用户确认的要素才会在文档中高亮

右侧面板分组:
- "我的要素"区域:显示已确认的要素(实线边框)
- "AI识别"区域:显示AI建议的要素(虚线边框、淡色)

交互功能:
- 点击AI建议的标签可采纳为"我的要素"
- 支持"全部采纳"和"全部忽略"操作
- 采纳后自动刷新文档高亮
何文松 3 недель назад
Родитель
Сommit
31cebd7f6a
1 измененных файлов с 205 добавлено и 15 удалено
  1. 205 15
      frontend/vue-demo/src/views/Editor.vue

+ 205 - 15
frontend/vue-demo/src/views/Editor.vue

@@ -301,21 +301,56 @@
             </div>
           </div>
           <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 class="element-hint" v-if="!entities || entities.length === 0">
               选中文本后右键标记为实体
             </div>
@@ -1158,6 +1193,16 @@ const filteredEntities = computed(() => {
   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) {
   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) {
   const typeNames = {
@@ -1582,7 +1681,7 @@ function renderTable(table, entityMap) {
 }
 
 /**
- * 从 blocks 中构建实体映射
+ * 从 blocks 中构建实体映射(只包含已确认的实体用于高亮)
  * 返回 { entityText: { entityId, entityType, confirmed } }
  */
 function buildEntityMap(blocks) {
@@ -1592,7 +1691,8 @@ function buildEntityMap(blocks) {
     if (!block.elements) return
     
     block.elements.forEach(el => {
-      if (el.type === 'entity' && el.entityText) {
+      // 只有已确认的实体才会被高亮显示
+      if (el.type === 'entity' && el.entityText && el.confirmed) {
         // 使用实体文本作为 key(可能有多个相同文本的实体)
         if (!entityMap.has(el.entityText)) {
           entityMap.set(el.entityText, [])
@@ -3918,6 +4018,56 @@ onUnmounted(() => {
 
   .element-body {
     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 风格
@@ -3994,6 +4144,46 @@ onUnmounted(() => {
       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 {
       border-radius: 14px;