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:
@@ -1094,30 +1094,84 @@
|
|||||||
list-style: decimal;
|
list-style: decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-query-summary) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 12px;
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 2px 0 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-query-summary__label) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: auto;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 760;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-query-summary__scope) {
|
||||||
|
min-width: 0;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 860;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-query-summary__count) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-query-summary__count strong) {
|
||||||
|
color: #1d4ed8;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card-list) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card-list) {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
margin-top: 18px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card) {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 0;
|
||||||
padding: 16px 18px;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(226, 232, 240, 0.9);
|
padding: 0;
|
||||||
border-left: 3px solid #cbd5e1;
|
border: 1px solid rgba(203, 213, 225, 0.76);
|
||||||
|
border-left: 0;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #ffffff;
|
background: rgba(255, 255, 255, 0.96);
|
||||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.04);
|
box-shadow:
|
||||||
|
0 1px 2px rgba(15, 23, 42, 0.035),
|
||||||
|
0 10px 26px rgba(15, 23, 42, 0.045);
|
||||||
color: #334155;
|
color: #334155;
|
||||||
animation: workbenchDocumentCardReveal 360ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
animation: workbenchDocumentCardReveal 360ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
||||||
transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
|
transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card:hover) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card:hover) {
|
||||||
border-color: rgba(148, 163, 184, 0.7);
|
border-color: rgba(148, 163, 184, 0.72);
|
||||||
box-shadow: 0 2px 4px rgba(15, 23, 42, 0.05), 0 8px 20px rgba(15, 23, 42, 0.07);
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.065);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1133,186 +1187,161 @@
|
|||||||
animation-delay: 120ms;
|
animation-delay: 120ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 状态语义色:左侧边条颜色随状态变化,一眼判断当前阶段 */
|
/* 状态语义色:头部浅底色和状态文字随单据状态变化 */
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-pending) {
|
|
||||||
border-left-color: #2563eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-success) {
|
|
||||||
border-left-color: #16a34a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-warning) {
|
|
||||||
border-left-color: #d97706;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-danger) {
|
|
||||||
border-left-color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 卡片头部:状态 + 类型(左) · 单据编号(右) */
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__head) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__head) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
padding: 13px 18px;
|
||||||
|
background: rgba(37, 99, 235, 0.11);
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__head-left) {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
min-width: 0;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__status) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__status) {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 22px;
|
min-height: 24px;
|
||||||
padding: 0 9px;
|
padding: 0;
|
||||||
border-radius: 6px;
|
border-radius: 0;
|
||||||
background: rgba(148, 163, 184, 0.16);
|
background: transparent;
|
||||||
color: #475569;
|
color: #1d4ed8;
|
||||||
font-size: 12px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 860;
|
||||||
line-height: 1.2;
|
line-height: 1.3;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-card.is-success .ai-document-card__head) {
|
||||||
|
background: rgba(22, 163, 74, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-card.is-warning .ai-document-card__head) {
|
||||||
|
background: rgba(217, 119, 6, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-card.is-danger .ai-document-card__head) {
|
||||||
|
background: rgba(220, 38, 38, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-pending .ai-document-card__status) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card.is-pending .ai-document-card__status) {
|
||||||
background: rgba(37, 99, 235, 0.1);
|
|
||||||
color: #1d4ed8;
|
color: #1d4ed8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-success .ai-document-card__status) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card.is-success .ai-document-card__status) {
|
||||||
background: rgba(22, 163, 74, 0.1);
|
|
||||||
color: #15803d;
|
color: #15803d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-warning .ai-document-card__status) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card.is-warning .ai-document-card__status) {
|
||||||
background: rgba(217, 119, 6, 0.1);
|
|
||||||
color: #b45309;
|
color: #b45309;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-danger .ai-document-card__status) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card.is-danger .ai-document-card__status) {
|
||||||
background: rgba(220, 38, 38, 0.1);
|
|
||||||
color: #b91c1c;
|
color: #b91c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__type) {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__number) {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
color: #94a3b8;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.3;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 卡片主体:事由(主焦点) + 申请人/部门(次焦点) */
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__body) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__body) {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 14px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
padding: 16px 18px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__reason) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__reason) {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
color: #0f172a;
|
min-width: 0;
|
||||||
font-size: 16px;
|
color: #1e40af;
|
||||||
font-weight: 700;
|
font-size: 15px;
|
||||||
|
font-weight: 760;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 1;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__owner-line) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card.is-success .ai-document-card__reason) {
|
||||||
display: flex;
|
color: #166534;
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__owner) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card.is-warning .ai-document-card__reason) {
|
||||||
color: #1e293b;
|
color: #92400e;
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__dept) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card.is-danger .ai-document-card__reason) {
|
||||||
color: #64748b;
|
color: #991b1b;
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__dot) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__details) {
|
||||||
color: #cbd5e1;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 卡片底部:辅助元信息(左) · 金额(右) · 操作 */
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__foot) {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
padding-top: 12px;
|
|
||||||
border-top: 1px solid rgba(226, 232, 240, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__meta) {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__meta-item) {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__amount-block) {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: end;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 1px;
|
gap: 12px 28px;
|
||||||
flex: 0 0 auto;
|
padding-top: 2px;
|
||||||
|
border-top: 1px solid rgba(203, 213, 225, 0.76);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__amount-label) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__field) {
|
||||||
color: #94a3b8;
|
display: grid;
|
||||||
font-size: 11px;
|
grid-template-columns: 86px minmax(0, 1fr);
|
||||||
font-weight: 500;
|
align-items: center;
|
||||||
line-height: 1.2;
|
gap: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-card__field--action) {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-card__label) {
|
||||||
|
color: #8a94a6;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 640;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-card__value) {
|
||||||
|
min-width: 0;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 720;
|
||||||
|
line-height: 1.45;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__amount) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__amount) {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 17px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 900;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-card__number) {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 740;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__action) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__action) {
|
||||||
flex: 0 0 auto;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: fit-content;
|
||||||
|
min-height: 26px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #1d4ed8;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 820;
|
||||||
|
box-shadow: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-card__action:hover) {
|
||||||
|
background: transparent;
|
||||||
|
color: #1e40af;
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.markdown-table-wrap),
|
.workbench-ai-answer-markdown :deep(.markdown-table-wrap),
|
||||||
@@ -1547,31 +1576,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card) {
|
||||||
padding: 14px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__head) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__head) {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-card__body) {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-card__details) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-card__field) {
|
||||||
|
grid-template-columns: 76px minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-ai-answer-markdown :deep(.ai-document-card__field--action) {
|
||||||
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__number) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__number) {
|
||||||
flex-basis: 100%;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__foot) {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__amount-block) {
|
|
||||||
justify-items: start;
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__action) {
|
|
||||||
order: 3;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-application-preview {
|
.workbench-ai-application-preview {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { fetchOntologyParse } from '../services/ontology.js'
|
|||||||
import { fetchLatestConversation } from '../services/orchestrator.js'
|
import { fetchLatestConversation } from '../services/orchestrator.js'
|
||||||
import { markAiWorkbenchConversationDraftDeleted } from '../utils/aiWorkbenchConversationStore.js'
|
import { markAiWorkbenchConversationDraftDeleted } from '../utils/aiWorkbenchConversationStore.js'
|
||||||
import { clearAssistantSessionSnapshotForDraftClaim } from '../utils/assistantSessionSnapshot.js'
|
import { clearAssistantSessionSnapshotForDraftClaim } from '../utils/assistantSessionSnapshot.js'
|
||||||
|
import { isApplicationDocumentNo } from '../utils/documentClassification.js'
|
||||||
import {
|
import {
|
||||||
ASSISTANT_SCOPE_SESSION_STEWARD,
|
ASSISTANT_SCOPE_SESSION_STEWARD,
|
||||||
buildUnsupportedBusinessScopeConversation,
|
buildUnsupportedBusinessScopeConversation,
|
||||||
@@ -428,8 +429,7 @@ export function useAppShell() {
|
|||||||
return (
|
return (
|
||||||
documentType === 'application'
|
documentType === 'application'
|
||||||
|| documentType === 'expense_application'
|
|| documentType === 'expense_application'
|
||||||
|| normalizedClaimNo.startsWith('AP-')
|
|| isApplicationDocumentNo(normalizedClaimNo)
|
||||||
|| normalizedClaimNo.startsWith('APP-')
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { computed, reactive, ref } from 'vue'
|
import { computed, reactive, ref } from 'vue'
|
||||||
|
|
||||||
import { fetchAllExpenseClaims } from '../services/reimbursements.js'
|
import { fetchAllExpenseClaims } from '../services/reimbursements.js'
|
||||||
|
import { isApplicationDocumentNo } from '../utils/documentClassification.js'
|
||||||
import { filterActionableRiskFlags, normalizeRiskFlagTone } from '../utils/riskFlags.js'
|
import { filterActionableRiskFlags, normalizeRiskFlagTone } from '../utils/riskFlags.js'
|
||||||
|
|
||||||
const EXPENSE_TYPE_LABELS = {
|
const EXPENSE_TYPE_LABELS = {
|
||||||
@@ -212,8 +213,7 @@ function resolveDocumentTypeMeta(claim, typeCode) {
|
|||||||
const isApplication =
|
const isApplication =
|
||||||
explicitType === DOCUMENT_TYPE_APPLICATION
|
explicitType === DOCUMENT_TYPE_APPLICATION
|
||||||
|| explicitType === 'expense_application'
|
|| explicitType === 'expense_application'
|
||||||
|| claimNo.startsWith('AP-')
|
|| isApplicationDocumentNo(claimNo)
|
||||||
|| claimNo.startsWith('APP-')
|
|
||||||
|| normalizedType === 'application'
|
|| normalizedType === 'application'
|
||||||
|| normalizedType.endsWith('_application')
|
|| normalizedType.endsWith('_application')
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { extractExpenseClaimItems } from '../services/reimbursements.js'
|
import { extractExpenseClaimItems } from '../services/reimbursements.js'
|
||||||
|
import { isApplicationDocumentNo } from './documentClassification.js'
|
||||||
|
|
||||||
const DOCUMENT_QUERY_LIMIT = 8
|
const DOCUMENT_QUERY_LIMIT = 8
|
||||||
|
|
||||||
@@ -336,8 +337,7 @@ function resolveDocumentTypeCode(claim = {}) {
|
|||||||
|| explicitType === 'expense_application'
|
|| explicitType === 'expense_application'
|
||||||
|| expenseType === 'application'
|
|| expenseType === 'application'
|
||||||
|| expenseType.endsWith('_application')
|
|| expenseType.endsWith('_application')
|
||||||
|| documentNo.startsWith('AP-')
|
|| isApplicationDocumentNo(documentNo)
|
||||||
|| documentNo.startsWith('APP-')
|
|
||||||
) {
|
) {
|
||||||
return 'application'
|
return 'application'
|
||||||
}
|
}
|
||||||
@@ -679,49 +679,66 @@ function buildDocumentCardHtml(record = {}) {
|
|||||||
const typeClass = record.documentType === 'application' ? 'application' : 'reimbursement'
|
const typeClass = record.documentType === 'application' ? 'application' : 'reimbursement'
|
||||||
const statusTone = record.statusTone || 'is-pending'
|
const statusTone = record.statusTone || 'is-pending'
|
||||||
const amountLabel = record.documentType === 'application' ? '预计金额' : '报销金额'
|
const amountLabel = record.documentType === 'application' ? '预计金额' : '报销金额'
|
||||||
|
const ownerText = [record.ownerLabel, record.departmentLabel]
|
||||||
// footer 左侧辅助元信息:业务地点(可选)+ 时间
|
.filter((item) => item && item !== '未显示')
|
||||||
const metaParts = []
|
.join(' · ') || '未显示'
|
||||||
if (record.locationLabel) {
|
|
||||||
metaParts.push(`<span class="ai-document-card__meta-item">${escapeHtml(record.locationLabel)}</span>`)
|
|
||||||
}
|
|
||||||
metaParts.push(`<span class="ai-document-card__meta-item">${escapeHtml(record.time || '待补充')}</span>`)
|
|
||||||
const metaHtml = `<div class="ai-document-card__meta">${metaParts.join('<span class="ai-document-card__dot">·</span>')}</div>`
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
`<article class="ai-document-card ai-document-card--${typeClass} ${statusTone}" aria-label="单据详情">`,
|
`<article class="ai-document-card ai-document-card--${typeClass} ${statusTone}" aria-label="单据详情">`,
|
||||||
'<header class="ai-document-card__head">',
|
'<header class="ai-document-card__head">',
|
||||||
'<div class="ai-document-card__head-left">',
|
`<strong class="ai-document-card__reason">${escapeHtml(record.reason)}</strong>`,
|
||||||
`<span class="ai-document-card__status">${escapeHtml(record.statusLabel)}</span>`,
|
`<span class="ai-document-card__status">${escapeHtml(record.statusLabel)}</span>`,
|
||||||
`<span class="ai-document-card__type">${escapeHtml(record.documentTypeLabel)} · ${escapeHtml(record.typeLabel)}</span>`,
|
|
||||||
'</div>',
|
|
||||||
`<span class="ai-document-card__number">${escapeHtml(record.documentNo || '未编号单据')}</span>`,
|
|
||||||
'</header>',
|
'</header>',
|
||||||
'<div class="ai-document-card__body">',
|
'<div class="ai-document-card__body">',
|
||||||
`<strong class="ai-document-card__reason">${escapeHtml(record.reason)}</strong>`,
|
'<div class="ai-document-card__details">',
|
||||||
'<div class="ai-document-card__owner-line">',
|
'<div class="ai-document-card__field">',
|
||||||
`<span class="ai-document-card__owner">${escapeHtml(record.ownerLabel)}</span>`,
|
'<span class="ai-document-card__label">单据类型</span>',
|
||||||
'<span class="ai-document-card__dot">·</span>',
|
`<strong class="ai-document-card__value">${escapeHtml(record.documentTypeLabel)} · ${escapeHtml(record.typeLabel)}</strong>`,
|
||||||
`<span class="ai-document-card__dept">${escapeHtml(record.departmentLabel)}</span>`,
|
|
||||||
'</div>',
|
'</div>',
|
||||||
'</div>',
|
'<div class="ai-document-card__field">',
|
||||||
'<footer class="ai-document-card__foot">',
|
`<span class="ai-document-card__label">${escapeHtml(amountLabel)}</span>`,
|
||||||
metaHtml,
|
|
||||||
'<div class="ai-document-card__amount-block">',
|
|
||||||
`<span class="ai-document-card__amount-label">${escapeHtml(amountLabel)}</span>`,
|
|
||||||
`<strong class="ai-document-card__amount">${escapeHtml(record.amountLabel)}</strong>`,
|
`<strong class="ai-document-card__amount">${escapeHtml(record.amountLabel)}</strong>`,
|
||||||
'</div>',
|
'</div>',
|
||||||
|
'<div class="ai-document-card__field">',
|
||||||
|
'<span class="ai-document-card__label">申请人</span>',
|
||||||
|
`<strong class="ai-document-card__value">${escapeHtml(ownerText)}</strong>`,
|
||||||
|
'</div>',
|
||||||
|
'<div class="ai-document-card__field">',
|
||||||
|
'<span class="ai-document-card__label">单据编号</span>',
|
||||||
|
`<strong class="ai-document-card__value ai-document-card__number">${escapeHtml(record.documentNo || '未编号单据')}</strong>`,
|
||||||
|
'</div>',
|
||||||
|
'<div class="ai-document-card__field ai-document-card__field--action">',
|
||||||
|
'<span class="ai-document-card__label">操作</span>',
|
||||||
href
|
href
|
||||||
? `<a class="ai-html-action-link ai-html-action-link-document ai-document-card__action" data-ai-action="open-document-detail" href="${escapeHtml(href)}">查看详情</a>`
|
? `<a class="ai-html-action-link ai-html-action-link-document ai-document-card__action" data-ai-action="open-document-detail" href="${escapeHtml(href)}">查看详情</a>`
|
||||||
: '',
|
: '<span class="ai-document-card__value">暂无详情</span>',
|
||||||
'</footer>',
|
'</div>',
|
||||||
|
'</div>',
|
||||||
|
'</div>',
|
||||||
'</article>'
|
'</article>'
|
||||||
].join('')
|
].join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDocumentCardsHtml(records = []) {
|
function buildDocumentQuerySummaryHtml(scopeText = '', totalCount = 0, visibleCount = 0) {
|
||||||
|
return [
|
||||||
|
'<section class="ai-document-query-summary" aria-label="单据查询范围">',
|
||||||
|
'<span class="ai-document-query-summary__label">查询范围</span>',
|
||||||
|
`<strong class="ai-document-query-summary__scope">${escapeHtml(scopeText || '相关单据')}</strong>`,
|
||||||
|
'<span class="ai-document-query-summary__count">',
|
||||||
|
`共 <strong>${escapeHtml(String(totalCount))}</strong> 张`,
|
||||||
|
'</span>',
|
||||||
|
'<span class="ai-document-query-summary__count">',
|
||||||
|
`展示最近 <strong>${escapeHtml(String(visibleCount))}</strong> 张`,
|
||||||
|
'</span>',
|
||||||
|
'</section>'
|
||||||
|
].join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDocumentCardsHtml(records = [], options = {}) {
|
||||||
|
const querySummaryHtml = options.querySummaryHtml || ''
|
||||||
return [
|
return [
|
||||||
'<!-- ai-trusted-html:start -->',
|
'<!-- ai-trusted-html:start -->',
|
||||||
|
querySummaryHtml,
|
||||||
'<section class="ai-document-card-list" aria-label="单据查询结果">',
|
'<section class="ai-document-card-list" aria-label="单据查询结果">',
|
||||||
...records.map((record) => buildDocumentCardHtml(record)),
|
...records.map((record) => buildDocumentCardHtml(record)),
|
||||||
'</section>',
|
'</section>',
|
||||||
@@ -772,9 +789,9 @@ export function buildAiDocumentQueryMessage(intent = {}, claimsPayload = []) {
|
|||||||
const lines = [
|
const lines = [
|
||||||
'### 已查询到相关单据',
|
'### 已查询到相关单据',
|
||||||
'',
|
'',
|
||||||
`**查询范围**:${scopeText || '相关单据'};共找到 **${records.length}** 张,先展示最近 **${visibleRecords.length}** 张。`,
|
buildDocumentCardsHtml(visibleRecords, {
|
||||||
'',
|
querySummaryHtml: buildDocumentQuerySummaryHtml(scopeText, records.length, visibleRecords.length)
|
||||||
buildDocumentCardsHtml(visibleRecords)
|
})
|
||||||
]
|
]
|
||||||
|
|
||||||
if (records.length > visibleRecords.length) {
|
if (records.length > visibleRecords.length) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { filterActionableRiskFlags, normalizeRiskFlagTone } from './riskFlags.js'
|
import { filterActionableRiskFlags, normalizeRiskFlagTone } from './riskFlags.js'
|
||||||
import { canViewRiskForContext } from './riskVisibility.js'
|
import { canViewRiskForContext } from './riskVisibility.js'
|
||||||
|
import { isApplicationDocumentNo } from './documentClassification.js'
|
||||||
|
|
||||||
const SYSTEM_GENERATED_EXPENSE_TYPES = new Set(['travel_allowance'])
|
const SYSTEM_GENERATED_EXPENSE_TYPES = new Set(['travel_allowance'])
|
||||||
const LOCATION_REQUIRED_EXPENSE_TYPES = new Set(['travel', 'meeting', 'entertainment'])
|
const LOCATION_REQUIRED_EXPENSE_TYPES = new Set(['travel', 'meeting', 'entertainment'])
|
||||||
@@ -37,8 +38,7 @@ function isApplicationDocumentRequest(request) {
|
|||||||
return (
|
return (
|
||||||
documentType === 'application'
|
documentType === 'application'
|
||||||
|| documentType === 'expense_application'
|
|| documentType === 'expense_application'
|
||||||
|| claimNo.startsWith('AP-')
|
|| isApplicationDocumentNo(claimNo)
|
||||||
|| claimNo.startsWith('APP-')
|
|
||||||
|| typeCode === 'application'
|
|| typeCode === 'application'
|
||||||
|| typeCode.endsWith('_application')
|
|| typeCode.endsWith('_application')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
const APPLICATION_DOCUMENT_NO_PATTERN = /^A[A-HJ-NP-Z2-9]{8}$/i
|
||||||
|
|
||||||
|
export function isApplicationDocumentNo(value) {
|
||||||
|
const claimNo = String(value || '').trim().toUpperCase()
|
||||||
|
return (
|
||||||
|
APPLICATION_DOCUMENT_NO_PATTERN.test(claimNo)
|
||||||
|
|| claimNo.startsWith('AP-')
|
||||||
|
|| claimNo.startsWith('APP-')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function isApplicationRequestLike(value) {
|
export function isApplicationRequestLike(value) {
|
||||||
const explicitType = String(
|
const explicitType = String(
|
||||||
value?.documentTypeCode
|
value?.documentTypeCode
|
||||||
@@ -14,8 +25,7 @@ export function isApplicationRequestLike(value) {
|
|||||||
return (
|
return (
|
||||||
explicitType === 'application'
|
explicitType === 'application'
|
||||||
|| explicitType === 'expense_application'
|
|| explicitType === 'expense_application'
|
||||||
|| claimNo.startsWith('AP-')
|
|| isApplicationDocumentNo(claimNo)
|
||||||
|| claimNo.startsWith('APP-')
|
|
||||||
|| typeCode === 'application'
|
|| typeCode === 'application'
|
||||||
|| typeCode.endsWith('_application')
|
|| typeCode.endsWith('_application')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
isFinanceUser,
|
isFinanceUser,
|
||||||
isPlatformAdminUser
|
isPlatformAdminUser
|
||||||
} from './accessControl.js'
|
} from './accessControl.js'
|
||||||
|
import { isApplicationDocumentNo } from './documentClassification.js'
|
||||||
|
|
||||||
const APPLICATION_STAGE_ALIASES = new Set([
|
const APPLICATION_STAGE_ALIASES = new Set([
|
||||||
'expense_application',
|
'expense_application',
|
||||||
@@ -159,8 +160,7 @@ export function isApplicationRiskStageRequest(request = {}) {
|
|||||||
return (
|
return (
|
||||||
documentType === 'application' ||
|
documentType === 'application' ||
|
||||||
documentType === 'expense_application' ||
|
documentType === 'expense_application' ||
|
||||||
claimNo.startsWith('AP-') ||
|
isApplicationDocumentNo(claimNo) ||
|
||||||
claimNo.startsWith('APP-') ||
|
|
||||||
typeCode === 'application' ||
|
typeCode === 'application' ||
|
||||||
typeCode.endsWith('_application')
|
typeCode.endsWith('_application')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -273,7 +273,27 @@ const sidebarCollapsed = ref(false)
|
|||||||
const sidebarCollapsedBeforeAiMode = ref(false)
|
const sidebarCollapsedBeforeAiMode = ref(false)
|
||||||
const mobileSidebarOpen = ref(false)
|
const mobileSidebarOpen = ref(false)
|
||||||
const overviewDashboard = ref('finance')
|
const overviewDashboard = ref('finance')
|
||||||
const workbenchMode = ref('traditional')
|
const { companyProfile, currentUser, logout } = useSystemState()
|
||||||
|
|
||||||
|
function resolveDefaultWorkbenchMode(user) {
|
||||||
|
return isPlatformAdminUser(user) ? 'traditional' : 'ai'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveWorkbenchUserKey(user = {}) {
|
||||||
|
const roleCodes = Array.isArray(user?.roleCodes) ? user.roleCodes.join(',') : ''
|
||||||
|
return [
|
||||||
|
user?.id,
|
||||||
|
user?.userId,
|
||||||
|
user?.username,
|
||||||
|
user?.account,
|
||||||
|
user?.name,
|
||||||
|
user?.role,
|
||||||
|
roleCodes,
|
||||||
|
user?.isAdmin ? 'admin' : 'user'
|
||||||
|
].map((item) => String(item || '').trim()).join('|')
|
||||||
|
}
|
||||||
|
|
||||||
|
const workbenchMode = ref(resolveDefaultWorkbenchMode(currentUser.value))
|
||||||
const aiSidebarCommandSeq = ref(0)
|
const aiSidebarCommandSeq = ref(0)
|
||||||
const aiSidebarCommand = ref({ seq: 0, type: '', payload: null })
|
const aiSidebarCommand = ref({ seq: 0, type: '', payload: null })
|
||||||
const aiActiveConversationId = ref('')
|
const aiActiveConversationId = ref('')
|
||||||
@@ -343,7 +363,6 @@ const {
|
|||||||
topBarView
|
topBarView
|
||||||
} = useAppShell()
|
} = useAppShell()
|
||||||
|
|
||||||
const { companyProfile, currentUser, logout } = useSystemState()
|
|
||||||
const PRODUCT_DISPLAY_NAME = '易财费控'
|
const PRODUCT_DISPLAY_NAME = '易财费控'
|
||||||
const ENTERPRISE_DISPLAY_NAME = '远光软件股份有限公司'
|
const ENTERPRISE_DISPLAY_NAME = '远光软件股份有限公司'
|
||||||
const filteredNavItems = computed(() => filterNavItemsByAccess(navItems, currentUser.value))
|
const filteredNavItems = computed(() => filterNavItemsByAccess(navItems, currentUser.value))
|
||||||
@@ -496,7 +515,14 @@ function handleLogout() {
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => currentUser.value,
|
() => currentUser.value,
|
||||||
(user) => {
|
(user, previousUser) => {
|
||||||
|
if (resolveWorkbenchUserKey(user) !== resolveWorkbenchUserKey(previousUser)) {
|
||||||
|
const nextMode = resolveDefaultWorkbenchMode(user)
|
||||||
|
workbenchMode.value = nextMode
|
||||||
|
if (nextMode === 'ai') {
|
||||||
|
sidebarCollapsed.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
aiConversationHistory.value = loadAiWorkbenchConversationHistory(user || {})
|
aiConversationHistory.value = loadAiWorkbenchConversationHistory(user || {})
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isApplicationDocumentNo } from '../../utils/documentClassification.js'
|
||||||
|
|
||||||
const REQUIRED_APPLICATION_EXPENSE_TYPES = new Set(['travel', 'meal'])
|
const REQUIRED_APPLICATION_EXPENSE_TYPES = new Set(['travel', 'meal'])
|
||||||
|
|
||||||
const APPLICATION_TYPE_ALIASES = {
|
const APPLICATION_TYPE_ALIASES = {
|
||||||
@@ -302,8 +304,7 @@ export function isExpenseApplicationClaim(claim) {
|
|||||||
|
|
||||||
return documentType === 'application'
|
return documentType === 'application'
|
||||||
|| documentType === 'expense_application'
|
|| documentType === 'expense_application'
|
||||||
|| claimNo.startsWith('AP-')
|
|| isApplicationDocumentNo(claimNo)
|
||||||
|| claimNo.startsWith('APP-')
|
|
||||||
|| expenseType === 'application'
|
|| expenseType === 'application'
|
||||||
|| expenseType.endsWith('_application')
|
|| expenseType.endsWith('_application')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isApplicationDocumentNo } from '../../utils/documentClassification.js'
|
||||||
|
|
||||||
function normalizeText(value) {
|
function normalizeText(value) {
|
||||||
return String(value || '').trim()
|
return String(value || '').trim()
|
||||||
}
|
}
|
||||||
@@ -22,7 +24,7 @@ function isApplicationDocumentRequest(requestModel) {
|
|||||||
|| requestModel?.document_type
|
|| requestModel?.document_type
|
||||||
).toLowerCase()
|
).toLowerCase()
|
||||||
const claimNo = normalizeText(requestModel?.claimNo || requestModel?.claim_no || requestModel?.documentNo).toUpperCase()
|
const claimNo = normalizeText(requestModel?.claimNo || requestModel?.claim_no || requestModel?.documentNo).toUpperCase()
|
||||||
return documentType === 'application' || claimNo.startsWith('AP-') || claimNo.startsWith('APP-')
|
return documentType === 'application' || isApplicationDocumentNo(claimNo)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isHotelExpenseItem(item) {
|
function isHotelExpenseItem(item) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isApplicationDocumentNo } from '../../utils/documentClassification.js'
|
||||||
|
|
||||||
function normalizeText(value) {
|
function normalizeText(value) {
|
||||||
return String(value || '').trim()
|
return String(value || '').trim()
|
||||||
}
|
}
|
||||||
@@ -112,7 +114,7 @@ export function resolveRequestBusinessStage(request = {}) {
|
|||||||
|| request?.document_no
|
|| request?.document_no
|
||||||
|| request?.id
|
|| request?.id
|
||||||
).toUpperCase()
|
).toUpperCase()
|
||||||
if (claimNo.startsWith('AP-') || claimNo.startsWith('APP-')) {
|
if (isApplicationDocumentNo(claimNo)) {
|
||||||
return 'expense_application'
|
return 'expense_application'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isApplicationDocumentNo } from '../../utils/documentClassification.js'
|
||||||
|
|
||||||
export const EXPENSE_TYPE_OPTIONS = [
|
export const EXPENSE_TYPE_OPTIONS = [
|
||||||
{ value: 'travel', label: '差旅费' },
|
{ value: 'travel', label: '差旅费' },
|
||||||
{ value: 'train_ticket', label: '火车票' },
|
{ value: 'train_ticket', label: '火车票' },
|
||||||
@@ -83,8 +85,7 @@ export function isApplicationDocumentRequest(request) {
|
|||||||
return (
|
return (
|
||||||
documentType === 'application'
|
documentType === 'application'
|
||||||
|| documentType === 'expense_application'
|
|| documentType === 'expense_application'
|
||||||
|| claimNo.startsWith('AP-')
|
|| isApplicationDocumentNo(claimNo)
|
||||||
|| claimNo.startsWith('APP-')
|
|
||||||
|| typeCode === 'application'
|
|| typeCode === 'application'
|
||||||
|| typeCode.endsWith('_application')
|
|| typeCode.endsWith('_application')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -718,7 +718,7 @@ export function useTravelReimbursementFlow({
|
|||||||
function buildApplicationDuplicateDetail(payload) {
|
function buildApplicationDuplicateDetail(payload) {
|
||||||
const result = payload?.result && typeof payload.result === 'object' ? payload.result : {}
|
const result = payload?.result && typeof payload.result === 'object' ? payload.result : {}
|
||||||
const answer = String(result.answer || result.message || '').trim()
|
const answer = String(result.answer || result.message || '').trim()
|
||||||
const claimNo = answer.match(/AP-\d{14}-[A-HJ-NP-Z2-9]{8}/)?.[0] || ''
|
const claimNo = answer.match(/A[A-HJ-NP-Z2-9]{8}|AP-\d{14}-[A-HJ-NP-Z2-9]{8}|APP-\d{8}-[A-Z0-9]{6}/)?.[0] || ''
|
||||||
return claimNo
|
return claimNo
|
||||||
? `已拦截重复申请,已有申请单:${claimNo}`
|
? `已拦截重复申请,已有申请单:${claimNo}`
|
||||||
: '已拦截重复申请,未创建新申请单'
|
: '已拦截重复申请,未创建新申请单'
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
collectReceiptFiles
|
collectReceiptFiles
|
||||||
} from './travelReimbursementAttachmentModel.js'
|
} from './travelReimbursementAttachmentModel.js'
|
||||||
import { resolveAssistantScopeGuard } from '../../utils/assistantSessionScope.js'
|
import { resolveAssistantScopeGuard } from '../../utils/assistantSessionScope.js'
|
||||||
|
import { isApplicationDocumentNo } from '../../utils/documentClassification.js'
|
||||||
import {
|
import {
|
||||||
APPLICATION_TRANSPORT_MODE_OPTIONS,
|
APPLICATION_TRANSPORT_MODE_OPTIONS,
|
||||||
applyApplicationBusinessTimeContext,
|
applyApplicationBusinessTimeContext,
|
||||||
@@ -148,8 +149,7 @@ function isApplicationClaimRecord(claim) {
|
|||||||
expenseType === 'application' ||
|
expenseType === 'application' ||
|
||||||
expenseType === 'expense_application' ||
|
expenseType === 'expense_application' ||
|
||||||
expenseType.endsWith('_application') ||
|
expenseType.endsWith('_application') ||
|
||||||
claimNo.startsWith('AP-') ||
|
isApplicationDocumentNo(claimNo) ||
|
||||||
claimNo.startsWith('APP-') ||
|
|
||||||
Boolean(extractApplicationDetailFromClaim(claim))
|
Boolean(extractApplicationDetailFromClaim(claim))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,24 +127,33 @@ test('AI document query message renders html document cards with detail actions'
|
|||||||
|
|
||||||
assert.match(message, /### 已查询到相关单据/)
|
assert.match(message, /### 已查询到相关单据/)
|
||||||
assert.match(message, /<!-- ai-trusted-html:start -->/)
|
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="单据查询结果">/)
|
assert.match(message, /<section class="ai-document-card-list" aria-label="单据查询结果">/)
|
||||||
// 申请单 app-1 状态为 approved → is-success 语义类
|
// 申请单 app-1 状态为 approved → is-success 语义类
|
||||||
assert.match(message, /<article class="ai-document-card ai-document-card--application is-success" aria-label="单据详情">/)
|
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, /<header class="ai-document-card__head">/)
|
||||||
assert.match(message, /<span class="ai-document-card__status">已审批<\/span>/)
|
assert.match(message, /<span class="ai-document-card__status">已审批<\/span>/)
|
||||||
assert.match(message, /<strong class="ai-document-card__reason">辅助国网仿生产服务器部署<\/strong>/)
|
assert.match(message, /<strong class="ai-document-card__reason">辅助国网仿生产服务器部署<\/strong>/)
|
||||||
assert.match(message, /<span class="ai-document-card__owner">曹小筑<\/span>/)
|
assert.match(message, /<div class="ai-document-card__details">/)
|
||||||
assert.match(message, /<span class="ai-document-card__dept">交付部<\/span>/)
|
assert.match(message, /<span class="ai-document-card__label">单据类型<\/span>/)
|
||||||
assert.match(message, /<span class="ai-document-card__number">AP-20260220001<\/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, /<strong class="ai-document-card__amount">¥3,000\.00<\/strong>/)
|
||||||
assert.match(message, /<div class="ai-document-card__meta">/)
|
assert.match(message, /<div class="ai-document-card__field ai-document-card__field--action">/)
|
||||||
assert.match(message, /<span class="ai-document-card__meta-item">上海<\/span>/)
|
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"/)
|
assert.match(message, /href="#ai-open-document-detail:AP-20260220001"/)
|
||||||
// 报销单 claim-1 状态为 submitted → is-pending 语义类
|
// 报销单 claim-1 状态为 submitted → is-pending 语义类
|
||||||
assert.match(message, /<article class="ai-document-card ai-document-card--reimbursement is-pending" aria-label="单据详情">/)
|
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.match(message, /href="#ai-open-document-detail:CL-20260221001"/)
|
||||||
assert.doesNotMatch(message, /\| 单据编号 \|/)
|
assert.doesNotMatch(message, /\| 单据编号 \|/)
|
||||||
assert.doesNotMatch(message, /^> /m)
|
assert.doesNotMatch(message, /^> /m)
|
||||||
|
assert.doesNotMatch(message, /\*\*查询范围\*\*/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('AI document query html cards render as trusted card markup', () => {
|
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))
|
const rendered = renderAiConversationHtml(buildAiDocumentQueryMessage(intent, claims))
|
||||||
|
|
||||||
assert.match(rendered, /<h3 class="ai-html-title">已查询到相关单据<\/h3>/)
|
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, /<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, /<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__head"/)
|
||||||
assert.match(rendered, /class="ai-document-card__meta"/)
|
assert.match(rendered, /class="ai-document-card__details"/)
|
||||||
assert.match(rendered, /class="ai-document-card__meta-item"/)
|
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, /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.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, /<section class="ai-document-card-list/)
|
||||||
assert.doesNotMatch(rendered, /<blockquote>/)
|
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)
|
const orbIconBuffer = readFileSync(orbIconAsset)
|
||||||
|
|
||||||
test('app shell owns the workbench mode and wires it through topbar and content', () => {
|
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, /function toggleWorkbenchMode\(\)/)
|
||||||
assert.match(appShell, /const nextMode = workbenchMode\.value === 'ai' \? 'traditional' : 'ai'/)
|
assert.match(appShell, /const nextMode = workbenchMode\.value === 'ai' \? 'traditional' : 'ai'/)
|
||||||
assert.match(appShell, /sidebarCollapsedBeforeAiMode\.value = sidebarCollapsed\.value/)
|
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.doesNotMatch(aiMode, /message\.pending \?/)
|
||||||
assert.match(aiMode, /继续和小财管家对话\.\.\./)
|
assert.match(aiMode, /继续和小财管家对话\.\.\./)
|
||||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\)/)
|
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__head\)/)
|
||||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__body\)/)
|
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__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-action-link\)/)
|
||||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-table-wrap\)/)
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-table-wrap\)/)
|
||||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-record-list\)/)
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-record-list\)/)
|
||||||
|
|||||||
Reference in New Issue
Block a user