Kaynağa Gözat

feat(frontend): 要素弹出框增加溯源卡片 + mousedown修复点击问题

何文松 15 saat önce
ebeveyn
işleme
5dbd45fc32
1 değiştirilmiş dosya ile 105 ekleme ve 4 silme
  1. 105 4
      frontend/vue-demo/src/views/Editor.vue

+ 105 - 4
frontend/vue-demo/src/views/Editor.vue

@@ -202,7 +202,7 @@
                 spellcheck="false"
                 v-html="docHtml"
                 @input="onDocInput"
-                @click="onDocClick"
+                @mousedown="onDocClick"
                 ref="docPaperRef"
               ></div>
 
@@ -221,6 +221,7 @@
               v-if="highlightPopover.visible"
               class="element-popover"
               :style="{ top: highlightPopover.y + 'px', left: highlightPopover.x + 'px' }"
+              @mousedown.stop
             >
               <div class="popover-header">
                 <span class="popover-label">{{ highlightPopover.elementName }}</span>
@@ -240,6 +241,23 @@
                   <span class="popover-field-label">原始值:</span>
                   <span class="popover-original">{{ highlightPopover.originalValue }}</span>
                 </div>
+                <!-- 溯源卡片 -->
+                <div class="popover-rules" v-if="popoverRelatedRules.length > 0">
+                  <span class="popover-field-label">来源规则:</span>
+                  <div
+                    v-for="rule in popoverRelatedRules"
+                    :key="rule.id"
+                    class="rule-trace-card"
+                  >
+                    <span class="rule-trace-action" :class="'action-' + rule.actionType">{{ ruleActionLabel(rule.actionType) }}</span>
+                    <div class="rule-trace-info">
+                      <div class="rule-trace-name">{{ rule.ruleName }}</div>
+                      <div v-if="rule.inputs && rule.inputs.length" class="rule-trace-sources">
+                        <span v-for="inp in rule.inputs" :key="inp.inputId" class="rule-trace-att">📎 {{ inp.sourceName }}</span>
+                      </div>
+                    </div>
+                  </div>
+                </div>
               </div>
               <div class="popover-footer">
                 <el-button size="small" @click="highlightPopover.visible = false">关闭</el-button>
@@ -1125,6 +1143,26 @@ function stripValueKeyPrefix(valueElementKey) {
   return idx >= 0 ? valueElementKey.substring(idx + 1) : valueElementKey
 }
 
+// 当前弹出框要素关联的规则列表
+const popoverRelatedRules = computed(() => {
+  const key = highlightPopover.elementKey
+  if (!key) return []
+  return rules.value.filter(r => {
+    const rk = stripValueKeyPrefix(r.elementKey)
+    return rk === key
+  })
+})
+
+function ruleActionLabel(actionType) {
+  const map = {
+    quote: '引用',
+    summary: 'AI总结',
+    table_extract: '表格提取',
+    use_entity_value: '实体值',
+  }
+  return map[actionType] || actionType
+}
+
 // 构建要素值映射表,分为长文本、短文本、静态文本三类
 function buildElementValueMap() {
   const longTexts = []    // paragraph/table 类型的长文本要素
@@ -1741,21 +1779,22 @@ function onDocInput() {
   saved.value = false
 }
 
-// 点击文档中的高亮要素
+// 点击文档中的高亮要素(mousedown 比 click 更可靠,在 contenteditable 容器内 e.target 更准确)
 function onDocClick(e) {
+  console.log('[onDocClick] target:', e.target.tagName, e.target.className, 'closest:', e.target.closest('.elem-highlight')?.dataset?.elemKey)
   const target = e.target.closest('.elem-highlight') || e.target.closest('.elem-highlight-wrap') || e.target.closest('.elem-highlight-table')
   if (!target) {
     highlightPopover.visible = false
     return
   }
-
   const elemKey = target.dataset.elemKey
   const valueId = target.dataset.valueId
   const elem = elements.value.find(el => el.elementKey === elemKey)
   const val = values.value.find(v => String(v.valueId) === String(valueId)) ||
               values.value.find(v => stripValueKeyPrefix(v.elementKey) === elemKey)
 
-  if (!elem) return
+  if (!elem || elem.elementType === 'static') return
+  e.preventDefault()
 
   const rect = target.getBoundingClientRect()
   const scrollEl = editorRef.value
@@ -3702,6 +3741,7 @@ onMounted(async () => {
     overflow-y: auto;
     padding: 40px 48px;
     background: var(--white);
+    position: relative;
   }
 
   .editor-content {
@@ -5678,6 +5718,67 @@ onMounted(async () => {
     }
   }
 
+  .popover-rules {
+    margin-top: 10px;
+    border-top: 1px dashed var(--border);
+    padding-top: 10px;
+
+    .rule-trace-card {
+      display: flex;
+      align-items: flex-start;
+      gap: 8px;
+      padding: 6px 8px;
+      background: var(--bg);
+      border-radius: var(--radius-sm);
+      border: 1px solid var(--border);
+      margin-bottom: 6px;
+
+      &:last-child { margin-bottom: 0; }
+
+      .rule-trace-action {
+        flex-shrink: 0;
+        font-size: 10px;
+        font-weight: 600;
+        padding: 2px 6px;
+        border-radius: 10px;
+        line-height: 18px;
+
+        &.action-quote         { background: #e6f4ff; color: #1677ff; }
+        &.action-summary       { background: #f6ffed; color: #52c41a; }
+        &.action-table_extract { background: #fff7e6; color: #fa8c16; }
+        &.action-use_entity_value { background: #f0f0f0; color: #666; }
+      }
+
+      .rule-trace-info {
+        flex: 1;
+        min-width: 0;
+
+        .rule-trace-name {
+          font-size: 12px;
+          color: var(--text-1);
+          white-space: nowrap;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+
+        .rule-trace-sources {
+          margin-top: 3px;
+          display: flex;
+          flex-direction: column;
+          gap: 2px;
+
+          .rule-trace-att {
+            font-size: 11px;
+            color: var(--text-3);
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+          }
+        }
+      }
+    }
+  }
+
   .popover-footer {
     display: flex;
     justify-content: flex-end;