|
|
@@ -339,19 +339,45 @@
|
|
|
<el-button size="small" text @click="ignoreAllAiSuggestions">忽略全部</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ <div class="element-option ai-highlight-option">
|
|
|
+ <el-switch
|
|
|
+ v-model="showAiSuggestionInDocument"
|
|
|
+ size="small"
|
|
|
+ @change="refreshDocumentHighlight"
|
|
|
+ />
|
|
|
+ <span class="option-label">正文中高亮 AI 建议</span>
|
|
|
+ </div>
|
|
|
<div class="element-body">
|
|
|
<div class="element-tags-wrap ai-tags">
|
|
|
<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.stop="adoptEntity(entity)"
|
|
|
+ :class="[getEntityTypeClass(entity.type), { 'is-pending': pendingConfirmEntity?.id === entity.id }]"
|
|
|
+ :title="`${getEntityTypeName(entity.type)}: ${entity.text} - 点击定位,点击 + 确认采纳/忽略`"
|
|
|
+ @click.stop="selectEntityForConfirm(entity)"
|
|
|
>
|
|
|
<span class="tag-icon">{{ getEntityTypeIcon(entity.type) }}</span>
|
|
|
<span class="tag-name">{{ entity.text }}</span>
|
|
|
- <span class="tag-action">+</span>
|
|
|
+ <el-popover
|
|
|
+ trigger="click"
|
|
|
+ placement="top"
|
|
|
+ :width="220"
|
|
|
+ popper-class="ai-confirm-popover"
|
|
|
+ >
|
|
|
+ <template #default>
|
|
|
+ <div class="ai-confirm-popover-content">
|
|
|
+ <div class="ai-confirm-text">确认「{{ entity.text }}」?</div>
|
|
|
+ <div class="ai-confirm-actions">
|
|
|
+ <el-button size="small" type="primary" @click="confirmEntityFromPopover(entity)">采纳</el-button>
|
|
|
+ <el-button size="small" @click="ignoreEntityFromPopover(entity)">忽略</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template #reference>
|
|
|
+ <span class="tag-action" @click.stop>+</span>
|
|
|
+ </template>
|
|
|
+ </el-popover>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -594,30 +620,36 @@
|
|
|
<el-form-item label="示例值">
|
|
|
<el-input v-model="variableForm.exampleValue" placeholder="文档中的原始值" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="来源类型">
|
|
|
- <el-select v-model="variableForm.sourceType" style="width: 100%">
|
|
|
- <el-option label="从来源文件提取" value="document" />
|
|
|
- <el-option label="手动输入" value="manual" />
|
|
|
- <el-option label="引用其他变量" value="reference" />
|
|
|
- <el-option label="固定值" value="fixed" />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="来源文件" v-if="variableForm.sourceType === 'document'">
|
|
|
- <el-select v-model="variableForm.sourceFileAlias" style="width: 100%">
|
|
|
- <el-option
|
|
|
- v-for="sf in sourceFiles"
|
|
|
- :key="sf.id"
|
|
|
- :label="sf.alias"
|
|
|
- :value="sf.alias"
|
|
|
- />
|
|
|
- </el-select>
|
|
|
+ <el-form-item label="要素类型">
|
|
|
+ <el-radio-group v-model="variableForm.sourceType">
|
|
|
+ <el-radio value="document">动态(有填充源 + 填充规则)</el-radio>
|
|
|
+ <el-radio value="fixed">静态(固定规则)</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="提取方式" v-if="variableForm.sourceType === 'document'">
|
|
|
- <el-select v-model="variableForm.extractType" style="width: 100%">
|
|
|
- <el-option label="直接提取" value="direct" />
|
|
|
- <el-option label="AI 字段提取" value="ai_extract" />
|
|
|
- <el-option label="AI 总结" value="ai_summarize" />
|
|
|
- </el-select>
|
|
|
+ <template v-if="variableForm.sourceType === 'document'">
|
|
|
+ <el-form-item label="填充源">
|
|
|
+ <el-select v-model="variableForm.sourceFileAlias" style="width: 100%" placeholder="选择来源文件">
|
|
|
+ <el-option
|
|
|
+ v-for="sf in sourceFiles"
|
|
|
+ :key="sf.id"
|
|
|
+ :label="sf.alias"
|
|
|
+ :value="sf.alias"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="填充规则">
|
|
|
+ <el-select v-model="variableForm.extractType" style="width: 100%">
|
|
|
+ <el-option label="直接提取" value="direct" />
|
|
|
+ <el-option label="AI 总结(从几个文本段)" value="ai_summarize" />
|
|
|
+ <el-option label="表格 OCR 提取(某个 PDF 的某页)" value="table_ocr" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item v-if="variableForm.extractType === 'table_ocr'" label="页码">
|
|
|
+ <el-input-number v-model="variableForm.extractConfigPage" :min="1" placeholder="PDF 页码" style="width: 100%" />
|
|
|
+ </el-form-item>
|
|
|
+ </template>
|
|
|
+ <el-form-item v-else label="固定值">
|
|
|
+ <el-input v-model="variableForm.fixedValue" placeholder="静态要素的固定值" />
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
@@ -686,8 +718,8 @@
|
|
|
</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 size="small" :type="getElementIsDynamic(row) ? 'warning' : 'info'">
|
|
|
+ {{ getElementIsDynamic(row) ? '动态' : '静态' }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
@@ -706,9 +738,14 @@
|
|
|
/>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column prop="source" label="填充源" width="140">
|
|
|
+ <el-table-column prop="source" label="填充源" width="120">
|
|
|
<template #default="{ row }">
|
|
|
- <span class="element-source">{{ row.source || row.sourceFile || '文档' }}</span>
|
|
|
+ <span class="element-source">{{ getElementFillSource(row) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="fillRule" label="填充规则" width="160">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span class="element-fill-rule">{{ getElementFillRuleLabel(row) }}</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="操作" width="80" fixed="right">
|
|
|
@@ -974,6 +1011,9 @@ async function switchReport(report) {
|
|
|
|
|
|
// 清空文档内容,显示空白提示
|
|
|
blocks.value = []
|
|
|
+ documentParagraphs.value = []
|
|
|
+ documentImages.value = []
|
|
|
+ documentTables.value = []
|
|
|
tocItems.value = []
|
|
|
documentContent.value = emptyPlaceholder
|
|
|
entities.value = []
|
|
|
@@ -1010,6 +1050,9 @@ function unselectReport() {
|
|
|
|
|
|
// 清空文档内容
|
|
|
blocks.value = []
|
|
|
+ documentParagraphs.value = []
|
|
|
+ documentImages.value = []
|
|
|
+ documentTables.value = []
|
|
|
tocItems.value = []
|
|
|
documentContent.value = emptyPlaceholder
|
|
|
entities.value = []
|
|
|
@@ -1032,16 +1075,26 @@ async function loadDocumentById(documentId) {
|
|
|
|
|
|
if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
|
|
|
blocks.value = structuredDoc.blocks
|
|
|
- documentContent.value = renderStructuredDocument(structuredDoc)
|
|
|
- entities.value = extractEntitiesFromBlocks(structuredDoc.blocks)
|
|
|
+ documentParagraphs.value = structuredDoc.paragraphs || []
|
|
|
+ documentImages.value = structuredDoc.images || []
|
|
|
+ documentTables.value = structuredDoc.tables || []
|
|
|
+ const extractedEntities = extractEntitiesFromBlocks(structuredDoc.blocks)
|
|
|
+ entities.value = extractedEntities
|
|
|
+ documentContent.value = renderStructuredDocument({ ...structuredDoc, entities: extractedEntities })
|
|
|
} else {
|
|
|
blocks.value = []
|
|
|
+ documentParagraphs.value = []
|
|
|
+ documentImages.value = []
|
|
|
+ documentTables.value = []
|
|
|
documentContent.value = emptyPlaceholder
|
|
|
entities.value = []
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.warn('加载文档失败:', error)
|
|
|
blocks.value = []
|
|
|
+ documentParagraphs.value = []
|
|
|
+ documentImages.value = []
|
|
|
+ documentTables.value = []
|
|
|
documentContent.value = emptyPlaceholder
|
|
|
entities.value = []
|
|
|
} finally {
|
|
|
@@ -1205,6 +1258,10 @@ const newSourceFile = reactive({
|
|
|
|
|
|
// 文档结构块(用于生成目录等)
|
|
|
const blocks = ref([])
|
|
|
+// 与 blocks 同源的段落/图片/表格(用于 refreshDocumentHighlight 时保留排版格式)
|
|
|
+const documentParagraphs = ref([])
|
|
|
+const documentImages = ref([])
|
|
|
+const documentTables = ref([])
|
|
|
|
|
|
// 目录数据(从 API 获取)
|
|
|
const tocItems = ref([])
|
|
|
@@ -1354,6 +1411,20 @@ const myEntities = computed(() => {
|
|
|
return filteredEntities.value.filter(e => e.confirmed)
|
|
|
})
|
|
|
|
|
|
+// 报告要素 Tab:dynamic | static
|
|
|
+const elementTab = ref('dynamic')
|
|
|
+
|
|
|
+// 动态要素(有填充源 + 填充规则:直接提取 / AI总结 / 表格OCR提取)
|
|
|
+const dynamicEntities = computed(() => myEntities.value.filter(e => getElementIsDynamic(e)))
|
|
|
+
|
|
|
+// 静态要素(固定规则)
|
|
|
+const staticEntities = computed(() => myEntities.value.filter(e => !getElementIsDynamic(e)))
|
|
|
+
|
|
|
+// 切换报告要素 Tab
|
|
|
+function switchElementTab(tab) {
|
|
|
+ elementTab.value = tab
|
|
|
+}
|
|
|
+
|
|
|
// 计算属性:AI 识别的要素(未确认)
|
|
|
const aiSuggestedEntities = computed(() => {
|
|
|
return filteredEntities.value.filter(e => !e.confirmed)
|
|
|
@@ -1371,11 +1442,84 @@ function toggleEntityTypeFilter(type) {
|
|
|
// 采纳 AI 建议的实体
|
|
|
function adoptEntity(entity) {
|
|
|
entity.confirmed = true
|
|
|
- // 重新渲染文档以更新高亮
|
|
|
refreshDocumentHighlight()
|
|
|
ElMessage.success(`已采纳要素「${entity.text}」`)
|
|
|
}
|
|
|
|
|
|
+// 点击 AI 建议:在文档中单独高亮该要素,并显示采纳/忽略确认栏(不直接添加)
|
|
|
+function selectEntityForConfirm(entity) {
|
|
|
+ clearPendingConfirmHighlight()
|
|
|
+ pendingConfirmEntity.value = entity
|
|
|
+ // 未开启「正文高亮 AI 建议」时,先重渲染正文使当前待确认实体出现,再跳转并高亮
|
|
|
+ if (!showAiSuggestionInDocument.value) {
|
|
|
+ refreshDocumentHighlight()
|
|
|
+ // 正文 DOM 由 watch(documentContent) 在 nextTick 里更新,需再等一帧再跳转
|
|
|
+ nextTick(() => {
|
|
|
+ nextTick(() => {
|
|
|
+ scrollToEntityAndHighlightForConfirm(entity.id)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ nextTick(() => {
|
|
|
+ scrollToEntityAndHighlightForConfirm(entity.id)
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 滚动到文档中的实体并添加「待确认」高亮(仅该要素)
|
|
|
+function scrollToEntityAndHighlightForConfirm(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-pending-confirm')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 移除文档中「待确认」高亮
|
|
|
+function clearPendingConfirmHighlight() {
|
|
|
+ const editorEl = document.querySelector('.editor-content')
|
|
|
+ if (!editorEl) return
|
|
|
+ editorEl.querySelectorAll('.entity-pending-confirm').forEach(el => el.classList.remove('entity-pending-confirm'))
|
|
|
+}
|
|
|
+
|
|
|
+// 用户确认采纳当前待确认的实体
|
|
|
+function confirmPendingEntity() {
|
|
|
+ if (!pendingConfirmEntity.value) return
|
|
|
+ adoptEntity(pendingConfirmEntity.value)
|
|
|
+ clearPendingConfirmHighlight()
|
|
|
+ pendingConfirmEntity.value = null
|
|
|
+}
|
|
|
+
|
|
|
+// 用户选择忽略当前待确认的实体
|
|
|
+function cancelPendingEntity() {
|
|
|
+ if (!pendingConfirmEntity.value) return
|
|
|
+ ignoreEntity(pendingConfirmEntity.value)
|
|
|
+ clearPendingConfirmHighlight()
|
|
|
+ pendingConfirmEntity.value = null
|
|
|
+ refreshDocumentHighlight()
|
|
|
+}
|
|
|
+
|
|
|
+// 从「+」按钮悬浮框中采纳实体
|
|
|
+function confirmEntityFromPopover(entity) {
|
|
|
+ adoptEntity(entity)
|
|
|
+ clearPendingConfirmHighlight()
|
|
|
+ if (pendingConfirmEntity.value?.id === entity.id) {
|
|
|
+ pendingConfirmEntity.value = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 从「+」按钮悬浮框中忽略实体
|
|
|
+function ignoreEntityFromPopover(entity) {
|
|
|
+ ignoreEntity(entity)
|
|
|
+ clearPendingConfirmHighlight()
|
|
|
+ if (pendingConfirmEntity.value?.id === entity.id) {
|
|
|
+ pendingConfirmEntity.value = null
|
|
|
+ }
|
|
|
+ refreshDocumentHighlight()
|
|
|
+}
|
|
|
+
|
|
|
// 忽略 AI 建议的实体
|
|
|
function ignoreEntity(entity) {
|
|
|
const index = entities.value.findIndex(e => e.id === entity.id)
|
|
|
@@ -1401,7 +1545,7 @@ function adoptAllAiSuggestions() {
|
|
|
ElMessage.success('已采纳所有 AI 建议')
|
|
|
}
|
|
|
|
|
|
-// 刷新文档高亮
|
|
|
+// 刷新文档高亮(保留段落/图片/表格等排版格式)
|
|
|
function refreshDocumentHighlight() {
|
|
|
if (blocks.value && blocks.value.length > 0) {
|
|
|
// 更新 blocks 中的 confirmed 状态
|
|
|
@@ -1417,8 +1561,14 @@ function refreshDocumentHighlight() {
|
|
|
})
|
|
|
}
|
|
|
})
|
|
|
- // 重新渲染
|
|
|
- documentContent.value = renderStructuredDocument({ blocks: blocks.value })
|
|
|
+ // 重新渲染时传入 paragraphs/images/tables,避免丢失排版格式
|
|
|
+ documentContent.value = renderStructuredDocument({
|
|
|
+ blocks: blocks.value,
|
|
|
+ paragraphs: documentParagraphs.value,
|
|
|
+ images: documentImages.value,
|
|
|
+ tables: documentTables.value,
|
|
|
+ entities: entities.value
|
|
|
+ })
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1587,7 +1737,9 @@ const variableForm = reactive({
|
|
|
exampleValue: '',
|
|
|
sourceType: 'document',
|
|
|
sourceFileAlias: '',
|
|
|
- extractType: 'direct'
|
|
|
+ extractType: 'direct',
|
|
|
+ extractConfigPage: undefined,
|
|
|
+ fixedValue: ''
|
|
|
})
|
|
|
|
|
|
// 右键菜单
|
|
|
@@ -1612,11 +1764,11 @@ const savingElements = ref(false)
|
|
|
const filteredElementsList = computed(() => {
|
|
|
let list = myEntities.value || []
|
|
|
|
|
|
- // 按类型筛选
|
|
|
+ // 按类型筛选:动态 = 有填充源+填充规则,静态 = 固定规则
|
|
|
if (elementsTypeFilter.value === 'dynamic') {
|
|
|
- list = list.filter(e => e.isDynamic)
|
|
|
+ list = list.filter(e => getElementIsDynamic(e))
|
|
|
} else if (elementsTypeFilter.value === 'static') {
|
|
|
- list = list.filter(e => !e.isDynamic)
|
|
|
+ list = list.filter(e => !getElementIsDynamic(e))
|
|
|
}
|
|
|
|
|
|
// 按关键词搜索
|
|
|
@@ -1651,6 +1803,32 @@ function getDataTypeTagType(dataType) {
|
|
|
return typeMap[dataType] || ''
|
|
|
}
|
|
|
|
|
|
+// 要素是否动态:有填充源+填充规则为动态,固定规则为静态
|
|
|
+function getElementIsDynamic(row) {
|
|
|
+ if (row.sourceType !== undefined && row.sourceType !== null) {
|
|
|
+ return row.sourceType !== 'fixed'
|
|
|
+ }
|
|
|
+ return (row.isDynamic !== false)
|
|
|
+}
|
|
|
+
|
|
|
+// 要素填充源展示:动态为来源文件别名,静态为「固定值」
|
|
|
+function getElementFillSource(row) {
|
|
|
+ if (row.sourceType === 'fixed') return '固定值'
|
|
|
+ return row.sourceFileAlias || row.source || row.sourceFile || '文档'
|
|
|
+}
|
|
|
+
|
|
|
+// 要素填充规则展示:直接提取 / AI总结 / 表格OCR提取 / 固定值
|
|
|
+function getElementFillRuleLabel(row) {
|
|
|
+ if (row.sourceType === 'fixed') return '固定值'
|
|
|
+ const labels = {
|
|
|
+ direct: '直接提取',
|
|
|
+ ai_summarize: 'AI总结(从几个文本段)',
|
|
|
+ table_ocr: '表格OCR提取(某个PDF的某页)',
|
|
|
+ ai_extract: 'AI字段提取'
|
|
|
+ }
|
|
|
+ return labels[row.extractType] || '直接提取'
|
|
|
+}
|
|
|
+
|
|
|
// 要素值变更
|
|
|
function onElementValueChange(element) {
|
|
|
// 标记为已修改
|
|
|
@@ -1718,6 +1896,12 @@ async function saveAllElements() {
|
|
|
const showEntityEditModal = ref(false)
|
|
|
const editingEntity = ref(null)
|
|
|
|
|
|
+// AI 建议点击后待确认的实体(点击后高亮文档中的该要素,由用户选择采纳/忽略)
|
|
|
+const pendingConfirmEntity = ref(null)
|
|
|
+
|
|
|
+// 是否在正文中高亮显示 AI 建议(未确认实体),默认关闭
|
|
|
+const showAiSuggestionInDocument = ref(false)
|
|
|
+
|
|
|
/**
|
|
|
* 打开实体编辑弹窗
|
|
|
*/
|
|
|
@@ -1830,8 +2014,9 @@ function renderStructuredDocument(structuredDoc) {
|
|
|
// 将所有元素合并
|
|
|
const allElements = []
|
|
|
|
|
|
- // 从 blocks 中提取实体映射(按文本内容匹配)
|
|
|
- const entityMap = buildEntityMap(blocks)
|
|
|
+ // 从 blocks 中提取实体映射;若 structuredDoc.entities 存在则用其过滤(如加载时),否则用当前 entities
|
|
|
+ const entitiesForMap = structuredDoc.entities ?? entities.value
|
|
|
+ const entityMap = buildEntityMap(blocks, entitiesForMap)
|
|
|
|
|
|
// 检查 paragraphs 是否有 runs(带格式信息)
|
|
|
const hasParagraphsWithRuns = paragraphs.some(p => p.runs && p.runs.length > 0)
|
|
|
@@ -1952,28 +2137,37 @@ function renderTable(table, entityMap) {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 从 blocks 中构建实体映射(只包含已确认的实体用于高亮)
|
|
|
- * 返回 { entityText: { entityId, entityType, confirmed } }
|
|
|
+ * 从 blocks 中构建实体映射(包含已确认与未确认实体,用于高亮与定位)
|
|
|
+ * 若传入 entitiesList,仅包含仍在列表中的实体(忽略后不再显示)
|
|
|
+ * 返回 Map<entityText, [{ entityId, entityType, confirmed }]>
|
|
|
*/
|
|
|
-function buildEntityMap(blocks) {
|
|
|
+function buildEntityMap(blocks, entitiesList) {
|
|
|
const entityMap = new Map()
|
|
|
|
|
|
blocks.forEach(block => {
|
|
|
if (!block.elements) return
|
|
|
|
|
|
block.elements.forEach(el => {
|
|
|
- // 只有已确认的实体才会被高亮显示
|
|
|
- if (el.type === 'entity' && el.entityText && el.confirmed) {
|
|
|
- // 使用实体文本作为 key(可能有多个相同文本的实体)
|
|
|
- if (!entityMap.has(el.entityText)) {
|
|
|
- entityMap.set(el.entityText, [])
|
|
|
- }
|
|
|
- entityMap.get(el.entityText).push({
|
|
|
- entityId: el.entityId,
|
|
|
- entityType: el.entityType,
|
|
|
- confirmed: el.confirmed
|
|
|
- })
|
|
|
+ if (el.type !== 'entity' || !el.entityText) return
|
|
|
+ // 若传入了实体列表,只包含仍在列表中的实体(被忽略的不再显示)
|
|
|
+ if (entitiesList && entitiesList.length > 0) {
|
|
|
+ const inList = entitiesList.some(e => e && e.id === el.entityId)
|
|
|
+ if (!inList) return
|
|
|
+ }
|
|
|
+ // 未确认的 AI 建议:仅当开关打开、或该实体为当前待确认项时在正文中显示(未开开关时也可跳转并高亮)
|
|
|
+ const confirmed = el.confirmed || false
|
|
|
+ if (!confirmed && !showAiSuggestionInDocument.value) {
|
|
|
+ const isPendingConfirm = pendingConfirmEntity.value && pendingConfirmEntity.value.id === el.entityId
|
|
|
+ if (!isPendingConfirm) return
|
|
|
+ }
|
|
|
+ if (!entityMap.has(el.entityText)) {
|
|
|
+ entityMap.set(el.entityText, [])
|
|
|
}
|
|
|
+ entityMap.get(el.entityText).push({
|
|
|
+ entityId: el.entityId,
|
|
|
+ entityType: el.entityType,
|
|
|
+ confirmed
|
|
|
+ })
|
|
|
})
|
|
|
})
|
|
|
|
|
|
@@ -2133,7 +2327,9 @@ function escapeNonEntityText(text) {
|
|
|
* 渲染实体高亮标签
|
|
|
*/
|
|
|
function renderEntityHighlight(text, entity) {
|
|
|
- const cssClass = getEntityCssClass(entity.entityType)
|
|
|
+ const baseClass = getEntityCssClass(entity.entityType)
|
|
|
+ const pendingClass = entity.confirmed ? '' : ' ai-suggestion-pending'
|
|
|
+ const cssClass = baseClass + pendingClass
|
|
|
const confirmedMark = entity.confirmed ? ' ✓' : ''
|
|
|
|
|
|
return `<span class="${cssClass}" ` +
|
|
|
@@ -2438,10 +2634,13 @@ async function handleRegenerateBlocks() {
|
|
|
// 重新加载文档内容
|
|
|
const structuredDoc = await documentApi.getStructured(baseDocumentId)
|
|
|
if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
|
|
|
- blocks.value = structuredDoc.blocks // 更新 blocks
|
|
|
- documentContent.value = renderStructuredDocument(structuredDoc)
|
|
|
- // 重新提取实体
|
|
|
- entities.value = extractEntitiesFromBlocks(structuredDoc.blocks)
|
|
|
+ blocks.value = structuredDoc.blocks
|
|
|
+ documentParagraphs.value = structuredDoc.paragraphs || []
|
|
|
+ documentImages.value = structuredDoc.images || []
|
|
|
+ documentTables.value = structuredDoc.tables || []
|
|
|
+ const extractedEntities = extractEntitiesFromBlocks(structuredDoc.blocks)
|
|
|
+ entities.value = extractedEntities
|
|
|
+ documentContent.value = renderStructuredDocument({ ...structuredDoc, entities: extractedEntities })
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('重新生成失败:', error)
|
|
|
@@ -2637,7 +2836,17 @@ function scrollToEntity(entityId) {
|
|
|
|
|
|
function editVariable(variable) {
|
|
|
editingVariable.value = variable
|
|
|
- Object.assign(variableForm, variable)
|
|
|
+ Object.assign(variableForm, {
|
|
|
+ name: variable.name ?? '',
|
|
|
+ displayName: variable.displayName ?? '',
|
|
|
+ category: variable.category ?? 'entity',
|
|
|
+ exampleValue: variable.exampleValue ?? '',
|
|
|
+ sourceType: variable.sourceType ?? 'document',
|
|
|
+ sourceFileAlias: variable.sourceFileAlias ?? '',
|
|
|
+ extractType: variable.extractType ?? 'direct',
|
|
|
+ extractConfigPage: variable.extractConfig?.page,
|
|
|
+ fixedValue: variable.sourceType === 'fixed' ? (variable.exampleValue ?? '') : ''
|
|
|
+ })
|
|
|
showVariableDialog.value = true
|
|
|
}
|
|
|
|
|
|
@@ -2647,15 +2856,26 @@ async function saveVariable() {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ const payload = {
|
|
|
+ name: variableForm.name,
|
|
|
+ displayName: variableForm.displayName,
|
|
|
+ category: variableForm.category,
|
|
|
+ exampleValue: variableForm.sourceType === 'fixed' ? variableForm.fixedValue : variableForm.exampleValue,
|
|
|
+ sourceType: variableForm.sourceType,
|
|
|
+ sourceFileAlias: variableForm.sourceType === 'document' ? variableForm.sourceFileAlias : undefined,
|
|
|
+ extractType: variableForm.sourceType === 'document' ? variableForm.extractType : undefined,
|
|
|
+ extractConfig: variableForm.sourceType === 'document' && variableForm.extractType === 'table_ocr' && variableForm.extractConfigPage
|
|
|
+ ? { page: variableForm.extractConfigPage }
|
|
|
+ : undefined
|
|
|
+ }
|
|
|
+
|
|
|
try {
|
|
|
if (editingVariable.value) {
|
|
|
- // 更新
|
|
|
- const updated = await templateStore.updateVariable(editingVariable.value.id, variableForm)
|
|
|
+ const updated = await templateStore.updateVariable(editingVariable.value.id, payload)
|
|
|
Object.assign(editingVariable.value, updated)
|
|
|
ElMessage.success('更新成功')
|
|
|
} else {
|
|
|
- // 新增
|
|
|
- const newVar = await templateStore.addVariable(templateId, variableForm)
|
|
|
+ const newVar = await templateStore.addVariable(templateId, payload)
|
|
|
variables.value.push(newVar)
|
|
|
ElMessage.success('添加成功')
|
|
|
}
|
|
|
@@ -2690,7 +2910,9 @@ function resetVariableForm() {
|
|
|
exampleValue: '',
|
|
|
sourceType: 'document',
|
|
|
sourceFileAlias: '',
|
|
|
- extractType: 'direct'
|
|
|
+ extractType: 'direct',
|
|
|
+ extractConfigPage: undefined,
|
|
|
+ fixedValue: ''
|
|
|
})
|
|
|
}
|
|
|
|
|
|
@@ -2935,8 +3157,12 @@ async function refreshDocumentContent() {
|
|
|
const structuredDoc = await documentApi.getStructured(baseDocumentId)
|
|
|
if (structuredDoc && structuredDoc.blocks && structuredDoc.blocks.length > 0) {
|
|
|
blocks.value = structuredDoc.blocks
|
|
|
- documentContent.value = renderStructuredDocument(structuredDoc)
|
|
|
- entities.value = extractEntitiesFromBlocks(structuredDoc.blocks)
|
|
|
+ documentParagraphs.value = structuredDoc.paragraphs || []
|
|
|
+ documentImages.value = structuredDoc.images || []
|
|
|
+ documentTables.value = structuredDoc.tables || []
|
|
|
+ const extractedEntities = extractEntitiesFromBlocks(structuredDoc.blocks)
|
|
|
+ entities.value = extractedEntities
|
|
|
+ documentContent.value = renderStructuredDocument({ ...structuredDoc, entities: extractedEntities })
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('刷新文档内容失败:', error)
|
|
|
@@ -4155,6 +4381,18 @@ onUnmounted(() => {
|
|
|
background: rgba(47, 84, 235, 0.1);
|
|
|
&:hover { background: #2f54eb; color: white; }
|
|
|
}
|
|
|
+
|
|
|
+ // 未确认的 AI 建议(文档中虚线样式)
|
|
|
+ &.ai-suggestion-pending {
|
|
|
+ border-style: dashed;
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 点击 AI 建议后,文档中该要素的「待确认」高亮
|
|
|
+ &.entity-pending-confirm {
|
|
|
+ box-shadow: 0 0 0 2px #1890ff;
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -4261,6 +4499,18 @@ onUnmounted(() => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ .element-option.ai-highlight-option {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 8px 16px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--text-2);
|
|
|
+ .option-label {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
.element-tags-wrap {
|
|
|
max-height: 300px;
|
|
|
}
|
|
|
@@ -4518,7 +4768,16 @@ onUnmounted(() => {
|
|
|
&.entity-default {
|
|
|
border-left: 3px solid #8c8c8c;
|
|
|
}
|
|
|
+
|
|
|
+ // 当前正在确认的 AI 建议 tag
|
|
|
+ &.is-pending {
|
|
|
+ border-color: var(--primary);
|
|
|
+ background: var(--primary-light);
|
|
|
+ border-style: solid;
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ // AI 建议确认栏已移至「+」按钮的悬浮框内,此处样式仅作保留注释
|
|
|
|
|
|
.element-hint {
|
|
|
font-size: 12px;
|
|
|
@@ -5197,3 +5456,20 @@ onUnmounted(() => {
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
+
|
|
|
+<style lang="scss">
|
|
|
+/* AI 确认悬浮框(挂载在 body,需全局样式) */
|
|
|
+.ai-confirm-popover.el-popper {
|
|
|
+ .ai-confirm-popover-content {
|
|
|
+ .ai-confirm-text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--text-2);
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+ .ai-confirm-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|