Jelajahi Sumber

feat(编辑器): 将"变量管理"改为"要素管理",展示文档中的实体

1. 重命名为"要素管理",展示NER识别的实体列表
2. 从结构化文档的blocks中提取实体(extractEntitiesFromBlocks)
3. 实体按类型显示不同的图标和颜色样式
4. 点击实体可滚动到文档中对应位置并高亮闪烁
5. 重新生成后自动更新实体列表
6. 保留"添加"按钮功能
何文松 4 minggu lalu
induk
melakukan
be120d2aaa
1 mengubah file dengan 208 tambahan dan 12 penghapusan
  1. 208 12
      frontend/vue-demo/src/views/Editor.vue

+ 208 - 12
frontend/vue-demo/src/views/Editor.vue

@@ -109,14 +109,14 @@
         </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 class="element-count">({{ entities.length }})</span>
             </span>
             <el-button size="small" :icon="Plus" @click="showAddVariableDialog = true">
               添加
@@ -125,18 +125,19 @@
           <div class="element-body">
             <div class="element-tags-wrap">
               <div
-                v-for="variable in variables"
-                :key="variable.id"
+                v-for="entity in entities"
+                :key="entity.id"
                 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 class="element-hint" v-if="variables.length === 0">
-              选中文本后右键标记为变量
+            <div class="element-hint" v-if="entities.length === 0">
+              文档解析后将展示识别的要素
             </div>
           </div>
         </div>
@@ -341,6 +342,44 @@ const newSourceFile = reactive({
 // 变量(从 API 获取)
 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 () => {
   await fetchTemplateData()
@@ -368,15 +407,20 @@ async function fetchTemplateData() {
         // 将结构化文档的 blocks 和 images 合并渲染
         if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
           documentContent.value = renderStructuredDocument(structuredDoc)
+          // 提取文档中的实体
+          entities.value = extractEntitiesFromBlocks(structuredDoc.blocks)
         } else {
           documentContent.value = emptyPlaceholder
+          entities.value = []
         }
       } catch (docError) {
         console.warn('获取文档内容失败:', docError)
         documentContent.value = emptyPlaceholder
+        entities.value = []
       }
     } else {
       documentContent.value = emptyPlaceholder
+      entities.value = []
     }
   } catch (error) {
     console.error('加载模板失败:', error)
@@ -504,6 +548,8 @@ async function handleRegenerateBlocks() {
     const structuredDoc = await documentApi.getStructured(baseDocumentId)
     if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
       documentContent.value = renderStructuredDocument(structuredDoc)
+      // 重新提取实体
+      entities.value = extractEntitiesFromBlocks(structuredDoc.blocks)
     }
   } catch (error) {
     console.error('重新生成失败:', error)
@@ -580,6 +626,72 @@ function getCategoryLabel(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) {
   editingVariable.value = variable
   Object.assign(variableForm, variable)
@@ -954,6 +1066,80 @@ onUnmounted(() => {
     display: flex;
     flex-wrap: wrap;
     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 {
@@ -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 {
   padding: 12px 16px;
   border-bottom: 1px solid var(--border);