feat: 优化差旅报销预审流程与个人工作台 UI 体系

- 完善 user_agent_application 申请差旅报销预审槽位与消息组装
- 增强预算助理报告与风险建议卡片交互
- 重构登录页视觉样式与移动端响应式适配
- 优化个人工作台、文档中心、政策中心、员工管理等页面布局
- 拆分 travelRequestDetailPreReviewModel 为 advice/submit 模型
- 补充报销草稿、风险复核、Item Sync 与模板执行器测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-02 14:01:51 +08:00
parent 92444e7eae
commit ca691f3ee0
107 changed files with 5663 additions and 1542 deletions

View File

@@ -44,6 +44,7 @@ const FLOW_DURATION_SECOND_FIELDS = [
const FLOW_DURATION_AUTO_FIELDS = ['duration', 'elapsed', 'latency', 'execution_time']
const FLOW_STARTED_AT_FIELDS = ['started_at', 'start_time', 'created_at', 'queued_at']
const FLOW_FINISHED_AT_FIELDS = ['finished_at', 'completed_at', 'ended_at', 'end_time', 'updated_at']
const FLOW_RUN_DETAIL_REFRESH_TIMEOUT_MS = 3000
function normalizeDurationValue(value, unit = 'ms') {
if (value === null || value === undefined || value === '') {
@@ -598,7 +599,7 @@ export function useTravelReimbursementFlow({
}
startFlowStep('pre-submit-review', {
title: 'AI预审与风险识别',
title: '自动检测与风险识别',
tool: 'ExpenseClaimService.submit_claim',
detail: '正在校验财务规则、风险规则和审批路径...'
})
@@ -665,6 +666,14 @@ export function useTravelReimbursementFlow({
tool: config.tool,
detail: config.detail
})
if (['save_draft', 'link_to_existing_draft', 'create_new_claim_from_documents'].includes(reviewAction)) {
startFlowStep('draft-risk-review', {
title: '草稿风险识别',
tool: 'RuleEngine',
detail: '正在校验申请单关联、票据完整性、金额口径和行程一致性...'
})
}
}
function isApplicationSessionActive() {
@@ -685,6 +694,15 @@ export function useTravelReimbursementFlow({
)
}
function isDuplicateApplicationPayload(payload) {
if (!isApplicationSessionActive()) {
return false
}
const result = payload?.result && typeof payload.result === 'object' ? payload.result : {}
const answer = String(result.answer || result.message || '').trim()
return answer.includes('已存在申请单') && answer.includes('系统没有重复创建')
}
function buildApplicationSubmitSuccessDetail(payload) {
const result = payload?.result && typeof payload.result === 'object' ? payload.result : {}
const draftPayload = result.draft_payload && typeof result.draft_payload === 'object'
@@ -697,6 +715,55 @@ export function useTravelReimbursementFlow({
: `申请单提交成功,当前节点:${approvalStage}`
}
function buildApplicationDuplicateDetail(payload) {
const result = payload?.result && typeof payload.result === 'object' ? payload.result : {}
const answer = String(result.answer || result.message || '').trim()
const claimNo = answer.match(/AP-\d{14}-[A-HJ-NP-Z2-9]{8}/)?.[0] || ''
return claimNo
? `已拦截重复申请,已有申请单:${claimNo}`
: '已拦截重复申请,未创建新申请单'
}
function isSavedReimbursementDraftPayload(payload) {
const result = payload?.result && typeof payload.result === 'object' ? payload.result : {}
const draftPayload = result.draft_payload && typeof result.draft_payload === 'object'
? result.draft_payload
: payload?.draft_payload && typeof payload.draft_payload === 'object'
? payload.draft_payload
: null
return Boolean(
draftPayload
&& String(draftPayload.status || '').trim() === 'draft'
&& String(draftPayload.draft_type || '').trim() !== 'expense_application'
)
}
function summarizeDraftRiskReviewDetail(payload) {
const result = payload?.result && typeof payload.result === 'object' ? payload.result : {}
const reviewPayload = result.review_payload && typeof result.review_payload === 'object'
? result.review_payload
: {}
const riskCount = Array.isArray(reviewPayload.risk_briefs)
? reviewPayload.risk_briefs.length
: Array.isArray(result.risk_flags)
? result.risk_flags.length
: 0
const missingCount = Array.isArray(reviewPayload.missing_slots)
? reviewPayload.missing_slots.length
: 0
const issueParts = []
if (riskCount) {
issueParts.push(`${riskCount} 条风险/异常提醒`)
}
if (missingCount) {
issueParts.push(`${missingCount} 项待补充信息`)
}
if (issueParts.length) {
return `已完成草稿规则校验,识别到 ${issueParts.join('、')},可进入详情核对后继续提交。`
}
return '已完成草稿规则校验,暂未发现明确风险;可继续上传票据或进入详情核对。'
}
function shouldHideToolCall(toolCall) {
const toolType = String(toolCall?.tool_type || '').toLowerCase()
const toolName = String(toolCall?.tool_name || '').toLowerCase()
@@ -750,9 +817,10 @@ export function useTravelReimbursementFlow({
response.submission_blocked ||
String(response.status || '').trim() === 'submitted' ||
responseMessage.includes('AI预审') ||
responseMessage.includes('自动检测') ||
responseMessage.includes('审批')
) {
return { key: 'pre-submit-review', title: 'AI预审与风险识别', tool: 'ExpenseClaimService.submit_claim' }
return { key: 'pre-submit-review', title: '自动检测与风险识别', tool: 'ExpenseClaimService.submit_claim' }
}
if (responseMessage.includes('关联')) {
return { key: 'attachment-association', title: '票据关联草稿', tool: toolCall?.tool_name || 'ExpenseClaimService' }
@@ -782,7 +850,7 @@ export function useTravelReimbursementFlow({
: `已完成财务规则、风险规则和审批路径校验,提交至${response.approval_stage || '审批环节'}`
}
if (response.submission_blocked) {
return summarizeVisibleToolText(response.message) || 'AI预审发现待补充项,暂未提交审批'
return summarizeVisibleToolText(response.message) || '自动检测发现待补充项,暂未提交审批'
}
return (
summarizeVisibleToolText(response.message || response.summary || response.result_summary)
@@ -861,6 +929,30 @@ export function useTravelReimbursementFlow({
if (!answer && !payload?.result) {
return
}
if (isSubmittedApplicationPayload(payload)) {
completePendingFlowStep(
'application-submit-success',
buildApplicationSubmitSuccessDetail(payload),
null,
{ title: '申请单提交成功', tool: 'ApplicationSubmit' }
)
}
if (isDuplicateApplicationPayload(payload)) {
completePendingFlowStep(
'application-submit-success',
buildApplicationDuplicateDetail(payload),
null,
{ title: '重复申请已拦截', tool: 'ApplicationSubmit' }
)
}
if (isSavedReimbursementDraftPayload(payload)) {
completePendingFlowStep(
'draft-risk-review',
summarizeDraftRiskReviewDetail(payload),
null,
{ title: '草稿风险识别', tool: 'RuleEngine' }
)
}
const sceneSelectionPending = isExpenseSceneSelectionResult(payload)
flowSteps.value
.filter((step) => ![FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status))
@@ -871,14 +963,6 @@ export function useTravelReimbursementFlow({
: resolveFlowStepDetail({ ...step, status: FLOW_STEP_STATUS_COMPLETED })
completeFlowStep(step.key, detail)
})
if (isSubmittedApplicationPayload(payload)) {
completePendingFlowStep(
'application-submit-success',
buildApplicationSubmitSuccessDetail(payload),
null,
{ title: '申请单提交成功', tool: 'ApplicationSubmit' }
)
}
const runFinishedAt = resolveFinishedTimestamp(run)
flowFinishedAt.value = flowSteps.value.some(
(step) => ![FLOW_STEP_STATUS_COMPLETED, FLOW_STEP_STATUS_FAILED].includes(step.status)
@@ -893,7 +977,15 @@ export function useTravelReimbursementFlow({
}
flowRefreshBusy.value = true
try {
const run = await fetchAgentRunDetail(flowRunId.value)
const run = await Promise.race([
fetchAgentRunDetail(flowRunId.value),
new Promise((resolve) => {
globalThis.setTimeout(() => resolve(null), FLOW_RUN_DETAIL_REFRESH_TIMEOUT_MS)
})
])
if (!run) {
return null
}
mergeFlowRunDetail(run)
return run
} catch (error) {