feat: 增强风险规则生成引擎与预算中心页面
后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块, 优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强 报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图 组件,重构审计页面和风险规则测试对话框交互,完善文档中心 和报销创建页面细节,补充单元测试覆盖。
This commit is contained in:
@@ -49,6 +49,31 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="message.recognitionDocuments?.length" class="risk-sim-recognition-debug">
|
||||
<span>单据识别明细</span>
|
||||
<article
|
||||
v-for="document in message.recognitionDocuments"
|
||||
:key="`${message.id}-${document.filename}`"
|
||||
>
|
||||
<header>
|
||||
<strong>{{ document.filename || '临时单据' }}</strong>
|
||||
<em>{{ formatDocumentMeta(document) }}</em>
|
||||
</header>
|
||||
<p v-if="document.summary">摘要:{{ document.summary }}</p>
|
||||
<div v-if="document.document_fields?.length" class="risk-sim-debug-field-list">
|
||||
<b
|
||||
v-for="field in document.document_fields"
|
||||
:key="`${document.filename}-${field.key}-${field.value}`"
|
||||
>
|
||||
{{ field.label }}[{{ field.key }}]:{{ field.value }}
|
||||
</b>
|
||||
</div>
|
||||
<p v-if="document.text" class="risk-sim-debug-ocr-text">
|
||||
OCR原文:{{ trimDebugText(document.text, 800) }}
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-if="message.result" class="risk-sim-result-card" :class="message.result.severity">
|
||||
<div class="risk-sim-result-head">
|
||||
<div>
|
||||
@@ -75,6 +100,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="buildRecognizedFieldRows(message.result).length"
|
||||
class="risk-sim-recognized-fields"
|
||||
>
|
||||
<span>规则实际取用字段</span>
|
||||
<ul>
|
||||
<li v-for="field in buildRecognizedFieldRows(message.result)" :key="field.key">
|
||||
<strong>{{ field.label }}</strong>
|
||||
<em>{{ field.source }}</em>
|
||||
<b>{{ field.value }}</b>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="buildEvidenceItems(message.result).length" class="risk-sim-evidence">
|
||||
<span>判断依据</span>
|
||||
<ul>
|
||||
@@ -262,6 +301,16 @@ import {
|
||||
formatTestError,
|
||||
formatTime
|
||||
} from './riskRuleTestDialogUtils.js'
|
||||
import {
|
||||
buildDocumentBrief,
|
||||
buildEvidenceItems as buildEvidenceItemsModel,
|
||||
buildRecognizedFieldRows as buildRecognizedFieldRowsModel,
|
||||
buildResultFields as buildResultFieldsModel,
|
||||
formatDocumentMeta,
|
||||
formatFieldLabel,
|
||||
resolveFileStatusLabel,
|
||||
trimDebugText
|
||||
} from './riskRuleTestDialogDisplay.js'
|
||||
|
||||
const props = defineProps({
|
||||
open: {
|
||||
@@ -568,8 +617,9 @@ async function recognizeTemporaryFiles(files, activeSessionId) {
|
||||
messages.value.push(buildMessage(
|
||||
'assistant',
|
||||
recognizedCount
|
||||
? `已完成 ${recognizedCount} 份临时单据识别。请核对右侧识别字段,字段不足时可以直接在输入框补充。`
|
||||
: '上传文件没有提取到足够字段,暂不能直接执行规则。请在输入框补充票据城市、金额、发票号等关键信息。'
|
||||
? `已完成 ${recognizedCount} 份临时单据识别。下面会展示 OCR 结构化字段和原文片段,请先核对这些信息;字段不足时可以直接在输入框补充。`
|
||||
: '上传文件没有提取到足够字段。下面仍会展示 OCR 返回内容,方便判断是票据质量问题还是字段映射问题。请在输入框补充城市、金额、发票号等关键信息。',
|
||||
{ recognitionDocuments: documents }
|
||||
))
|
||||
} catch (error) {
|
||||
if (!isActiveSession(activeSessionId)) return
|
||||
@@ -601,52 +651,15 @@ function buildMessage(role, text, extra = {}) {
|
||||
}
|
||||
|
||||
function buildResultFields(result) {
|
||||
const values = result?.field_values && typeof result.field_values === 'object'
|
||||
? result.field_values
|
||||
: {}
|
||||
return Object.entries(values).slice(0, 8).map(([key, value]) => ({
|
||||
key,
|
||||
label: formatFieldLabel(fields.value.find((field) => field.key === key) || { key }),
|
||||
value: Array.isArray(value) ? value.join('、') : String(value ?? '-')
|
||||
}))
|
||||
return buildResultFieldsModel(result, fields.value)
|
||||
}
|
||||
|
||||
function buildRecognizedFieldRows(result) {
|
||||
return buildRecognizedFieldRowsModel(result, fields.value)
|
||||
}
|
||||
|
||||
function buildEvidenceItems(result) {
|
||||
const evidence = result?.evidence && typeof result.evidence === 'object'
|
||||
? result.evidence
|
||||
: {}
|
||||
const items = []
|
||||
if (Array.isArray(evidence.failed_conditions)) {
|
||||
evidence.failed_conditions.slice(0, 3).forEach((condition) => {
|
||||
const left = Array.isArray(condition.left_values) ? condition.left_values.join('、') : '-'
|
||||
const right = Array.isArray(condition.right_values) ? condition.right_values.join('、') : '-'
|
||||
items.push(`${formatFieldName(condition.left)}:${left};${formatFieldName(condition.right)}:${right}`)
|
||||
})
|
||||
}
|
||||
if (Array.isArray(evidence.missing_fields)) {
|
||||
evidence.missing_fields.slice(0, 5).forEach((field) => {
|
||||
items.push(`${formatFieldName(field)} 缺失`)
|
||||
})
|
||||
}
|
||||
if (Array.isArray(evidence.keyword_hits)) {
|
||||
items.push(`命中关键词:${evidence.keyword_hits.join('、')}`)
|
||||
}
|
||||
if (evidence.condition_summary) {
|
||||
items.push(String(evidence.condition_summary))
|
||||
}
|
||||
return [...new Set(items)].slice(0, 5)
|
||||
}
|
||||
|
||||
function formatFieldLabel(field) {
|
||||
const key = String(field?.key || '').trim()
|
||||
const label = String(field?.display || field?.label || '').trim()
|
||||
if (!key) return label || '-'
|
||||
if (!label || label === key) return key
|
||||
return label.includes(`[${key}]`) ? label : `${label}[${key}]`
|
||||
}
|
||||
|
||||
function formatFieldName(key) {
|
||||
return formatFieldLabel(fields.value.find((field) => field.key === key) || { key })
|
||||
return buildEvidenceItemsModel(result, fields.value)
|
||||
}
|
||||
|
||||
function toAttachmentPayload(file) {
|
||||
@@ -713,23 +726,6 @@ function documentHasMeaningfulText(document) {
|
||||
)
|
||||
}
|
||||
|
||||
function buildDocumentBrief(document) {
|
||||
const fields = Array.isArray(document?.document_fields) ? document.document_fields : []
|
||||
if (fields.length) {
|
||||
return fields.slice(0, 4).map((field) => `${field.label}:${field.value}`).join(';')
|
||||
}
|
||||
return String(document?.summary || document?.text || '未提取到结构化字段').slice(0, 120)
|
||||
}
|
||||
|
||||
function resolveFileStatusLabel(file) {
|
||||
return file.statusText || {
|
||||
pending: '待发送',
|
||||
recognizing: '识别中',
|
||||
recognized: '已识别',
|
||||
failed: '识别失败'
|
||||
}[file.status] || '待识别'
|
||||
}
|
||||
|
||||
function buildRecognitionStepDescription() {
|
||||
if (!requiresAttachment.value) return '当前规则不需要附件,直接根据文字测试事实抽取字段。'
|
||||
if (recognitionBusy.value) return '正在读取临时附件并提取 OCR 字段。'
|
||||
|
||||
Reference in New Issue
Block a user