feat(web): AI 文档查询卡片重构与单号判定统一
- documentClassification 抽出 isApplicationDocumentNo,统一兼容 AP-/APP- 旧格式与 A+8 新格式,aiDocumentQueryModel 复用 - aiDocumentQueryModel 文档卡片改为结构化字段布局(单据类型/金额/申请人/编号/操作),新增查询范围摘要区,渲染走 HTML 信任块 - AppShellRouteView/useAppShell/useRequests/detailAlerts/riskVisibility 等差旅详情模型适配单号判定 - 同步更新 ai-document-query-model/workbench-ai-mode-switch 测试,新增 document-classification 测试
This commit is contained in:
@@ -127,24 +127,33 @@ test('AI document query message renders html document cards with detail actions'
|
||||
|
||||
assert.match(message, /### 已查询到相关单据/)
|
||||
assert.match(message, /<!-- ai-trusted-html:start -->/)
|
||||
assert.match(message, /<section class="ai-document-query-summary" aria-label="单据查询范围">/)
|
||||
assert.match(message, /<span class="ai-document-query-summary__label">查询范围<\/span>/)
|
||||
assert.match(message, /<strong class="ai-document-query-summary__scope">/)
|
||||
assert.match(message, /<span class="ai-document-query-summary__count">/)
|
||||
assert.match(message, /<section class="ai-document-card-list" aria-label="单据查询结果">/)
|
||||
// 申请单 app-1 状态为 approved → is-success 语义类
|
||||
assert.match(message, /<article class="ai-document-card ai-document-card--application is-success" aria-label="单据详情">/)
|
||||
assert.match(message, /<header class="ai-document-card__head">/)
|
||||
assert.match(message, /<span class="ai-document-card__status">已审批<\/span>/)
|
||||
assert.match(message, /<strong class="ai-document-card__reason">辅助国网仿生产服务器部署<\/strong>/)
|
||||
assert.match(message, /<span class="ai-document-card__owner">曹小筑<\/span>/)
|
||||
assert.match(message, /<span class="ai-document-card__dept">交付部<\/span>/)
|
||||
assert.match(message, /<span class="ai-document-card__number">AP-20260220001<\/span>/)
|
||||
assert.match(message, /<div class="ai-document-card__details">/)
|
||||
assert.match(message, /<span class="ai-document-card__label">单据类型<\/span>/)
|
||||
assert.match(message, /<strong class="ai-document-card__value">申请单 · 差旅费用申请<\/strong>/)
|
||||
assert.match(message, /<span class="ai-document-card__label">申请人<\/span>/)
|
||||
assert.match(message, /<strong class="ai-document-card__value">曹小筑 · 交付部<\/strong>/)
|
||||
assert.match(message, /<strong class="ai-document-card__value ai-document-card__number">AP-20260220001<\/strong>/)
|
||||
assert.match(message, /<strong class="ai-document-card__amount">¥3,000\.00<\/strong>/)
|
||||
assert.match(message, /<div class="ai-document-card__meta">/)
|
||||
assert.match(message, /<span class="ai-document-card__meta-item">上海<\/span>/)
|
||||
assert.match(message, /<div class="ai-document-card__field ai-document-card__field--action">/)
|
||||
assert.doesNotMatch(message, /ai-document-card__meta/)
|
||||
assert.doesNotMatch(message, /ai-document-card__meta-item/)
|
||||
assert.match(message, /href="#ai-open-document-detail:AP-20260220001"/)
|
||||
// 报销单 claim-1 状态为 submitted → is-pending 语义类
|
||||
assert.match(message, /<article class="ai-document-card ai-document-card--reimbursement is-pending" aria-label="单据详情">/)
|
||||
assert.match(message, /href="#ai-open-document-detail:CL-20260221001"/)
|
||||
assert.doesNotMatch(message, /\| 单据编号 \|/)
|
||||
assert.doesNotMatch(message, /^> /m)
|
||||
assert.doesNotMatch(message, /\*\*查询范围\*\*/)
|
||||
})
|
||||
|
||||
test('AI document query html cards render as trusted card markup', () => {
|
||||
@@ -152,13 +161,16 @@ test('AI document query html cards render as trusted card markup', () => {
|
||||
const rendered = renderAiConversationHtml(buildAiDocumentQueryMessage(intent, claims))
|
||||
|
||||
assert.match(rendered, /<h3 class="ai-html-title">已查询到相关单据<\/h3>/)
|
||||
assert.match(rendered, /<section class="ai-document-query-summary" aria-label="单据查询范围">/)
|
||||
assert.match(rendered, /<section class="ai-document-card-list" aria-label="单据查询结果">/)
|
||||
assert.match(rendered, /<article class="ai-document-card ai-document-card--application is-success" aria-label="单据详情">/)
|
||||
assert.match(rendered, /class="ai-document-card__head"/)
|
||||
assert.match(rendered, /class="ai-document-card__meta"/)
|
||||
assert.match(rendered, /class="ai-document-card__meta-item"/)
|
||||
assert.match(rendered, /class="ai-document-card__details"/)
|
||||
assert.match(rendered, /class="ai-document-card__field"/)
|
||||
assert.match(rendered, /class="ai-document-card__label"/)
|
||||
assert.match(rendered, /class="ai-html-action-link ai-html-action-link-document ai-document-card__action"/)
|
||||
assert.match(rendered, /href="#ai-open-document-detail:CL-20260221001"/)
|
||||
assert.doesNotMatch(rendered, /ai-document-card__meta/)
|
||||
assert.doesNotMatch(rendered, /<section class="ai-document-card-list/)
|
||||
assert.doesNotMatch(rendered, /<blockquote>/)
|
||||
})
|
||||
|
||||
20
web/tests/document-classification.test.mjs
Normal file
20
web/tests/document-classification.test.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
isApplicationDocumentNo,
|
||||
isApplicationRequestLike
|
||||
} from '../src/utils/documentClassification.js'
|
||||
|
||||
test('application document number detection supports short and legacy formats', () => {
|
||||
assert.equal(isApplicationDocumentNo('A7K3M9Q2P'), true)
|
||||
assert.equal(isApplicationDocumentNo('AP-20260525103045-ABCDEFGH'), true)
|
||||
assert.equal(isApplicationDocumentNo('APP-20260525-ABC123'), true)
|
||||
assert.equal(isApplicationDocumentNo('R7K3M9Q2P'), false)
|
||||
assert.equal(isApplicationDocumentNo('RE-20260525103045-HGFEDCBA'), false)
|
||||
})
|
||||
|
||||
test('application request classification can rely on a short document number', () => {
|
||||
assert.equal(isApplicationRequestLike({ claim_no: 'A7K3M9Q2P' }), true)
|
||||
assert.equal(isApplicationRequestLike({ claim_no: 'R7K3M9Q2P' }), false)
|
||||
})
|
||||
@@ -172,7 +172,10 @@ const orbIconPngAsset = fileURLToPath(
|
||||
const orbIconBuffer = readFileSync(orbIconAsset)
|
||||
|
||||
test('app shell owns the workbench mode and wires it through topbar and content', () => {
|
||||
assert.match(appShell, /const workbenchMode = ref\('traditional'\)/)
|
||||
assert.match(appShell, /function resolveDefaultWorkbenchMode\(user\)\s*\{[\s\S]*isPlatformAdminUser\(user\)[\s\S]*'traditional'[\s\S]*'ai'/)
|
||||
assert.match(appShell, /const workbenchMode = ref\(resolveDefaultWorkbenchMode\(currentUser\.value\)\)/)
|
||||
assert.doesNotMatch(appShell, /const workbenchMode = ref\('traditional'\)/)
|
||||
assert.match(appShell, /watch\(\s*\(\) => currentUser\.value,[\s\S]*resolveDefaultWorkbenchMode\(user\)/)
|
||||
assert.match(appShell, /function toggleWorkbenchMode\(\)/)
|
||||
assert.match(appShell, /const nextMode = workbenchMode\.value === 'ai' \? 'traditional' : 'ai'/)
|
||||
assert.match(appShell, /sidebarCollapsedBeforeAiMode\.value = sidebarCollapsed\.value/)
|
||||
@@ -260,11 +263,21 @@ test('AI mode screen follows the approved reference structure', () => {
|
||||
assert.doesNotMatch(aiMode, /message\.pending \?/)
|
||||
assert.match(aiMode, /继续和小财管家对话\.\.\./)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-query-summary\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-query-summary__scope\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card-list\) \{[\s\S]*gap:\s*16px;/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__head\) \{[\s\S]*background: rgba\(37, 99, 235, 0\.11\);/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\.is-success \.ai-document-card__head\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__head\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__body\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__foot\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__details\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__field\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__label\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__amount\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__meta\)/)
|
||||
assert.match(
|
||||
aiModeStyles,
|
||||
/\.workbench-ai-answer-markdown :deep\(\.ai-document-card__details\) \{[\s\S]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\);/
|
||||
)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-action-link\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-table-wrap\)/)
|
||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-record-list\)/)
|
||||
|
||||
Reference in New Issue
Block a user