Răsfoiți Sursa

feat(editor): 实现报告要素管理弹窗

功能特点:
- 表格形式展示所有用户确认的要素
- 支持按名称/类型搜索
- 支持按动态/静态要素类型筛选
- 分页显示,可配置每页条数
- 可直接在表格中编辑要素的"新值"
- 支持删除要素
- "保存全部"批量保存修改

要素类型:
- 动态要素:从文档或其他文件总结/提取的内容
- 静态要素:固定内容

入口:
- 右侧面板"我的要素"标题可点击打开
- 添加"管理"按钮快速打开
何文松 3 săptămâni în urmă
părinte
comite
fbe854e496
1 a modificat fișierele cu 307 adăugiri și 4 ștergeri
  1. 307 4
      frontend/vue-demo/src/views/Editor.vue

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

@@ -262,13 +262,18 @@
         <!-- 我的要素(已确认的实体) -->
         <!-- 我的要素(已确认的实体) -->
         <div class="element-section">
         <div class="element-section">
           <div class="element-header">
           <div class="element-header">
-            <span class="element-title">
+            <span class="element-title" @click="showElementsModal = true" style="cursor: pointer;" title="点击管理所有要素">
               🏷️ 我的要素
               🏷️ 我的要素
               <span class="element-count">({{ myEntities?.length || 0 }})</span>
               <span class="element-count">({{ myEntities?.length || 0 }})</span>
             </span>
             </span>
-            <el-button size="small" :icon="Plus" @click="showAddVariableDialog = true">
-              添加
-            </el-button>
+            <div class="header-actions">
+              <el-button size="small" text @click="showElementsModal = true" title="管理要素">
+                管理
+              </el-button>
+              <el-button size="small" type="primary" :icon="Plus" @click="showAddVariableDialog = true">
+                添加
+              </el-button>
+            </div>
           </div>
           </div>
           <!-- 搜索和筛选 -->
           <!-- 搜索和筛选 -->
           <div class="element-filter" v-if="myEntities && myEntities.length > 0">
           <div class="element-filter" v-if="myEntities && myEntities.length > 0">
@@ -628,6 +633,111 @@
       </template>
       </template>
     </el-dialog>
     </el-dialog>
 
 
+    <!-- 报告要素管理弹窗 -->
+    <el-dialog 
+      v-model="showElementsModal" 
+      title="报告要素" 
+      width="1100"
+      class="elements-modal"
+      :close-on-click-modal="false"
+    >
+      <div class="elements-modal-content">
+        <!-- 搜索栏 -->
+        <div class="elements-search">
+          <el-input 
+            v-model="elementsSearchKeyword"
+            placeholder="搜索要素名称 / 类型..."
+            :prefix-icon="Search"
+            clearable
+          />
+          <div class="elements-type-filter">
+            <el-radio-group v-model="elementsTypeFilter" size="small">
+              <el-radio-button value="">全部</el-radio-button>
+              <el-radio-button value="dynamic">动态要素</el-radio-button>
+              <el-radio-button value="static">静态要素</el-radio-button>
+            </el-radio-group>
+          </div>
+        </div>
+        
+        <!-- 要素表格 -->
+        <div class="elements-table-wrap">
+          <el-table 
+            :data="paginatedElements" 
+            style="width: 100%"
+            max-height="400"
+            stripe
+          >
+            <el-table-column prop="name" label="名称" width="140">
+              <template #default="{ row }">
+                <span class="element-name">{{ row.text || row.name }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="description" label="描述" width="160">
+              <template #default="{ row }">
+                <span class="element-desc">{{ row.description || '-' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="dataType" label="类型" width="80">
+              <template #default="{ row }">
+                <el-tag size="small" :type="getDataTypeTagType(row.dataType)">
+                  {{ row.dataType || '文本' }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="elementType" label="要素类型" width="100">
+              <template #default="{ row }">
+                <el-tag size="small" :type="row.isDynamic ? 'warning' : 'info'">
+                  {{ row.isDynamic ? '动态' : '静态' }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="originalValue" label="原值" width="120">
+              <template #default="{ row }">
+                <span class="original-value">{{ row.originalValue || row.text || '-' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="newValue" label="新值" width="140">
+              <template #default="{ row }">
+                <el-input 
+                  v-model="row.newValue" 
+                  size="small" 
+                  placeholder="输入新值"
+                  @change="onElementValueChange(row)"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column prop="source" label="填充源" width="140">
+              <template #default="{ row }">
+                <span class="element-source">{{ row.source || row.sourceFile || '文档' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="80" fixed="right">
+              <template #default="{ row }">
+                <el-button size="small" :icon="Delete" circle @click="deleteElement(row)" title="删除" />
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        
+        <!-- 分页 -->
+        <div class="elements-pagination">
+          <el-pagination
+            v-model:current-page="elementsCurrentPage"
+            v-model:page-size="elementsPageSize"
+            :page-sizes="[10, 20, 50]"
+            :total="filteredElementsList.length"
+            layout="total, sizes, prev, pager, next"
+            small
+          />
+        </div>
+      </div>
+      
+      <template #footer>
+        <el-button @click="showElementsModal = false">取消</el-button>
+        <el-button type="primary" @click="saveAllElements" :loading="savingElements">保存全部</el-button>
+      </template>
+    </el-dialog>
+
     <!-- 知识图谱弹窗 -->
     <!-- 知识图谱弹窗 -->
     <el-dialog v-model="showGraphModal" title="🔗 知识图谱" width="900">
     <el-dialog v-model="showGraphModal" title="🔗 知识图谱" width="900">
       <div class="graph-container">
       <div class="graph-container">
@@ -1490,6 +1600,120 @@ const selectionRange = ref(null)
 // 知识图谱
 // 知识图谱
 const showGraphModal = ref(false)
 const showGraphModal = ref(false)
 
 
+// 报告要素管理弹窗
+const showElementsModal = ref(false)
+const elementsSearchKeyword = ref('')
+const elementsTypeFilter = ref('')
+const elementsCurrentPage = ref(1)
+const elementsPageSize = ref(10)
+const savingElements = ref(false)
+
+// 计算属性:过滤后的要素列表(用于弹窗)
+const filteredElementsList = computed(() => {
+  let list = myEntities.value || []
+  
+  // 按类型筛选
+  if (elementsTypeFilter.value === 'dynamic') {
+    list = list.filter(e => e.isDynamic)
+  } else if (elementsTypeFilter.value === 'static') {
+    list = list.filter(e => !e.isDynamic)
+  }
+  
+  // 按关键词搜索
+  if (elementsSearchKeyword.value) {
+    const keyword = elementsSearchKeyword.value.toLowerCase()
+    list = list.filter(e => 
+      (e.text || e.name || '').toLowerCase().includes(keyword) ||
+      (e.type || '').toLowerCase().includes(keyword) ||
+      (e.description || '').toLowerCase().includes(keyword)
+    )
+  }
+  
+  return list
+})
+
+// 计算属性:分页后的要素列表
+const paginatedElements = computed(() => {
+  const start = (elementsCurrentPage.value - 1) * elementsPageSize.value
+  const end = start + elementsPageSize.value
+  return filteredElementsList.value.slice(start, end)
+})
+
+// 获取数据类型对应的标签类型
+function getDataTypeTagType(dataType) {
+  const typeMap = {
+    '文本': '',
+    '金额': 'success',
+    '日期': 'warning',
+    '数字': 'info',
+    '百分比': 'danger'
+  }
+  return typeMap[dataType] || ''
+}
+
+// 要素值变更
+function onElementValueChange(element) {
+  // 标记为已修改
+  element.isModified = true
+}
+
+// 删除要素
+function deleteElement(element) {
+  ElMessageBox.confirm(
+    `确定要删除要素「${element.text || element.name}」吗?`,
+    '删除确认',
+    {
+      confirmButtonText: '删除',
+      cancelButtonText: '取消',
+      type: 'warning'
+    }
+  ).then(() => {
+    const index = entities.value.findIndex(e => e.id === element.id)
+    if (index !== -1) {
+      entities.value.splice(index, 1)
+    }
+    ElMessage.success('已删除')
+    refreshDocumentHighlight()
+  }).catch(() => {})
+}
+
+// 保存所有要素
+async function saveAllElements() {
+  savingElements.value = true
+  try {
+    // 找出所有被修改的要素
+    const modifiedElements = filteredElementsList.value.filter(e => e.isModified)
+    
+    if (modifiedElements.length === 0) {
+      ElMessage.info('没有需要保存的修改')
+      showElementsModal.value = false
+      savingElements.value = false
+      return
+    }
+    
+    // TODO: 调用 API 保存修改
+    // await Promise.all(modifiedElements.map(e => variableApi.update(e.id, e)))
+    
+    // 清除修改标记
+    modifiedElements.forEach(e => {
+      e.isModified = false
+      // 如果有新值,更新文本
+      if (e.newValue && e.newValue !== e.text) {
+        e.text = e.newValue
+      }
+    })
+    
+    ElMessage.success(`已保存 ${modifiedElements.length} 个要素`)
+    refreshDocumentHighlight()
+    showElementsModal.value = false
+  } catch (error) {
+    console.error('保存要素失败:', error)
+    ElMessage.error('保存失败')
+  } finally {
+    savingElements.value = false
+  }
+}
+
 // 实体编辑弹窗
 // 实体编辑弹窗
 const showEntityEditModal = ref(false)
 const showEntityEditModal = ref(false)
 const editingEntity = ref(null)
 const editingEntity = ref(null)
@@ -4732,6 +4956,85 @@ onUnmounted(() => {
   }
   }
 }
 }
 
 
+// ==========================================
+// 报告要素管理弹窗样式
+// ==========================================
+.elements-modal {
+  :deep(.el-dialog__header) {
+    padding: 16px 20px;
+    border-bottom: 1px solid var(--border);
+    margin-right: 0;
+  }
+  
+  :deep(.el-dialog__body) {
+    padding: 0;
+  }
+  
+  :deep(.el-dialog__footer) {
+    padding: 12px 20px;
+    border-top: 1px solid var(--border);
+  }
+}
+
+.elements-modal-content {
+  .elements-search {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    padding: 16px 20px;
+    border-bottom: 1px solid var(--border);
+    background: var(--bg);
+    
+    .el-input {
+      max-width: 300px;
+    }
+  }
+  
+  .elements-table-wrap {
+    padding: 0;
+    
+    :deep(.el-table) {
+      .element-name {
+        font-weight: 500;
+        color: var(--text-1);
+      }
+      
+      .element-desc {
+        color: var(--text-3);
+        font-size: 12px;
+      }
+      
+      .original-value {
+        color: var(--text-2);
+        font-size: 12px;
+      }
+      
+      .element-source {
+        color: var(--primary);
+        font-size: 12px;
+      }
+      
+      .el-input__wrapper {
+        box-shadow: none;
+        background: var(--bg);
+        border-radius: var(--radius-sm);
+        
+        &:hover, &.is-focus {
+          background: var(--white);
+          box-shadow: 0 0 0 1px var(--primary);
+        }
+      }
+    }
+  }
+  
+  .elements-pagination {
+    display: flex;
+    justify-content: flex-end;
+    padding: 12px 20px;
+    border-top: 1px solid var(--border);
+  }
+}
+
 // ==========================================
 // ==========================================
 // 新建报告对话框样式
 // 新建报告对话框样式
 // ==========================================
 // ==========================================