feat(web): AI 文档详情引用解析与查询卡片增强
- 新增 aiDocumentDetailReference,统一解析 #ai-open-document-detail / #ai-open-application-detail 引用,兼容 A/R/D 短格式与 AP-/RE-/AD- 旧格式单号,提供 isBusinessDocumentReference 判定 - aiDocumentQueryModel 文档卡片接入详情引用,按申请单/报销单生成对应 href,HTML 渲染器识别单据记录表格并生成卡片链接 - PersonalWorkbenchAiMode 处理文档详情点击跳转,卡片样式重构为结构化布局并更新背景资源 - expenseApplicationPreview 补充事由字段,同步新增/更新 ai-document-detail-reference、document-query-model、html-renderer、workbench-ai-mode 等测试 - 更新公司通信费报销规则表
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 354 KiB |
@@ -1151,27 +1151,34 @@
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card) {
|
||||
--ai-document-card-head-bg: rgba(37, 99, 235, 0.075);
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(203, 213, 225, 0.76);
|
||||
border-left: 0;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
background-color: #ffffff;
|
||||
background-image:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.72), rgba(255, 255, 255, 0.9)),
|
||||
url("../../ai-document-card-bg.png");
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(203, 213, 225, 0.5),
|
||||
0 1px 2px rgba(15, 23, 42, 0.035),
|
||||
0 10px 26px rgba(15, 23, 42, 0.045);
|
||||
0 14px 34px rgba(15, 23, 42, 0.05);
|
||||
color: #334155;
|
||||
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: box-shadow 180ms ease, transform 180ms ease;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card:hover) {
|
||||
border-color: rgba(148, 163, 184, 0.72);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.065);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(148, 163, 184, 0.46),
|
||||
0 1px 2px rgba(15, 23, 42, 0.04),
|
||||
0 18px 38px rgba(15, 23, 42, 0.07);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -1194,8 +1201,8 @@
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
padding: 13px 18px;
|
||||
background: rgba(37, 99, 235, 0.11);
|
||||
padding: 13px 18px 13px 20px;
|
||||
background: var(--ai-document-card-head-bg);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__status) {
|
||||
@@ -1213,15 +1220,15 @@
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-success .ai-document-card__head) {
|
||||
background: rgba(22, 163, 74, 0.1);
|
||||
background: rgba(22, 163, 74, 0.08);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-warning .ai-document-card__head) {
|
||||
background: rgba(217, 119, 6, 0.12);
|
||||
background: rgba(217, 119, 6, 0.09);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-danger .ai-document-card__head) {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card.is-pending .ai-document-card__status) {
|
||||
@@ -1242,9 +1249,9 @@
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__body) {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
gap: 15px;
|
||||
min-width: 0;
|
||||
padding: 16px 18px 18px;
|
||||
padding: 15px 18px 18px;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__reason) {
|
||||
@@ -1271,12 +1278,68 @@
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--application) {
|
||||
--ai-document-card-head-bg: rgba(37, 99, 235, 0.075);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--application .ai-document-card__head) {
|
||||
background: var(--ai-document-card-head-bg);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--application .ai-document-card__reason) {
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--reimbursement) {
|
||||
--ai-document-card-head-bg: rgba(13, 148, 136, 0.075);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--reimbursement .ai-document-card__head) {
|
||||
background: var(--ai-document-card-head-bg);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--reimbursement .ai-document-card__reason) {
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--approval-task) {
|
||||
--ai-document-card-head-bg: rgba(245, 158, 11, 0.1);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(245, 158, 11, 0.18),
|
||||
0 1px 2px rgba(120, 53, 15, 0.04),
|
||||
0 14px 34px rgba(120, 53, 15, 0.06);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--approval-task .ai-document-card__head) {
|
||||
background: var(--ai-document-card-head-bg);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--approval-task .ai-document-card__reason) {
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card--approval-task .ai-document-card__status) {
|
||||
min-height: 26px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(245, 158, 11, 0.18);
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__summary),
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__details) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 28px;
|
||||
padding-top: 2px;
|
||||
border-top: 1px solid rgba(203, 213, 225, 0.76);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__summary) {
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid rgba(203, 213, 225, 0.76);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__details) {
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__field) {
|
||||
@@ -1288,7 +1351,11 @@
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__field--action) {
|
||||
grid-column: 1 / -1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__field--wide) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__label) {
|
||||
@@ -1373,109 +1440,6 @@
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-list) {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-item) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1.15fr) minmax(260px, 0.85fr) auto;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 15px 16px;
|
||||
border: 1px solid rgba(203, 213, 225, 0.86);
|
||||
border-left: 3px solid #60a5fa;
|
||||
border-radius: 14px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.9));
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.045);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-main) {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-kicker) {
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
color: #1d4ed8;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-id) {
|
||||
color: #0f172a;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 15px;
|
||||
font-weight: 860;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-reason) {
|
||||
color: #475569;
|
||||
font-size: 14px;
|
||||
font-weight: 660;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-meta) {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-meta-item) {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-meta-item small) {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-weight: 760;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-meta-item b) {
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
font-weight: 780;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-action) {
|
||||
justify-self: end;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-action .ai-html-action-link) {
|
||||
min-height: 32px;
|
||||
padding: 0 15px;
|
||||
border-radius: 10px;
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-action .ai-html-action-link:hover) {
|
||||
background: #1d4ed8;
|
||||
color: #ffffff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-action-link.is-disabled) {
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
@@ -1484,12 +1448,6 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-action .ai-html-action-link.is-disabled:hover) {
|
||||
background: rgba(100, 116, 139, 0.14);
|
||||
color: #64748b;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-image-frame) {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
@@ -1547,6 +1505,29 @@
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__field--action .ai-document-card__action) {
|
||||
min-height: 26px;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #1d4ed8;
|
||||
font-size: 14px;
|
||||
font-weight: 820;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__field--action .ai-document-card__action:hover) {
|
||||
background: transparent;
|
||||
color: #1e40af;
|
||||
text-decoration: underline;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__field--action .ai-document-card__action.is-disabled) {
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
@keyframes workbenchDocumentCardReveal {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -1566,15 +1547,6 @@
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-item) {
|
||||
grid-template-columns: 1fr;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-html-record-action) {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card) {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1595,15 +1567,17 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.workbench-ai-answer-markdown :deep(.ai-document-card__summary) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.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) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -727,8 +727,15 @@ import {
|
||||
buildAiDocumentQueryConditionSummary,
|
||||
buildAiDocumentQueryMessage,
|
||||
filterAiDocumentQueryRecords,
|
||||
mergeAiDocumentQueryPayloads,
|
||||
resolveAiDocumentQueryIntent
|
||||
} from '../../utils/aiDocumentQueryModel.js'
|
||||
import {
|
||||
AI_APPLICATION_DETAIL_HREF_PREFIX,
|
||||
buildAiDocumentDetailRequest,
|
||||
parseAiApplicationDetailHref,
|
||||
parseAiDocumentDetailHref
|
||||
} from '../../utils/aiDocumentDetailReference.js'
|
||||
import {
|
||||
buildRequiredApplicationActions,
|
||||
buildRequiredApplicationMissingText,
|
||||
@@ -780,9 +787,6 @@ const INLINE_ANSWER_STREAM_CHUNK_SIZE = 6
|
||||
const INLINE_ANSWER_STREAM_DELAY_MS = 24
|
||||
const INLINE_AUTO_SCROLL_THRESHOLD = 96
|
||||
const INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS = 260
|
||||
const AI_DOCUMENT_DETAIL_HREF_PREFIX = '#ai-open-document-detail:'
|
||||
const AI_APPLICATION_DETAIL_HREF_PREFIX = '#ai-open-application-detail:'
|
||||
|
||||
const {
|
||||
applicationPreviewEditor,
|
||||
resolveApplicationPreviewEditorControl,
|
||||
@@ -1269,6 +1273,27 @@ function normalizeInlineApplicationResultTableCell(value, fallback = '-') {
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
const INLINE_APPLICATION_STATUS_LABELS = {
|
||||
draft: '草稿',
|
||||
submitted: '审批中',
|
||||
pending: '待处理',
|
||||
approved: '已审批',
|
||||
completed: '已完成',
|
||||
archived: '已归档',
|
||||
returned: '已退回',
|
||||
rejected: '已驳回',
|
||||
pending_payment: '待付款',
|
||||
paid: '已付款'
|
||||
}
|
||||
|
||||
function normalizeInlineApplicationStatusLabel(value, fallback = '') {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return fallback
|
||||
}
|
||||
return INLINE_APPLICATION_STATUS_LABELS[text.toLowerCase()] || text
|
||||
}
|
||||
|
||||
function buildInlineApplicationActionDetailHref(reference = '') {
|
||||
const source = reference && typeof reference === 'object' ? reference : { reference }
|
||||
const claimId = String(source.claimId || source.claim_id || source.id || '').trim()
|
||||
@@ -1289,11 +1314,63 @@ function buildInlineApplicationActionDetailHref(reference = '') {
|
||||
|
||||
function resolveInlineApplicationActionDocumentInfo(draftPayload = {}) {
|
||||
const source = draftPayload && typeof draftPayload === 'object' ? draftPayload : {}
|
||||
const body = String(source.body || source.markdown || '').trim()
|
||||
const resolveBodyField = (labels = []) => {
|
||||
for (const label of labels) {
|
||||
const pattern = new RegExp(`${label}\\s*[::]\\s*([^\\n|]+)`, 'u')
|
||||
const match = body.match(pattern)
|
||||
if (match?.[1]) {
|
||||
return String(match[1]).replace(/\*\*/g, '').trim()
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
const startDate = String(source.start_date || source.startDate || source.trip_start_date || source.tripStartDate || '').trim()
|
||||
const endDate = String(source.end_date || source.endDate || source.trip_end_date || source.tripEndDate || '').trim()
|
||||
const dateText = String(
|
||||
source.business_time ||
|
||||
source.businessTime ||
|
||||
source.time ||
|
||||
source.occurred_at ||
|
||||
source.occurredAt ||
|
||||
source.apply_time ||
|
||||
source.applyTime ||
|
||||
''
|
||||
).trim()
|
||||
const rangeText = startDate && endDate && startDate !== endDate
|
||||
? `${startDate} 至 ${endDate}`
|
||||
: startDate || endDate
|
||||
return {
|
||||
claimNo: String(source.claim_no || source.claimNo || source.document_no || source.documentNo || '').trim(),
|
||||
claimId: String(source.claim_id || source.claimId || source.id || '').trim(),
|
||||
statusLabel: String(source.status_label || source.statusLabel || source.status || '').trim(),
|
||||
statusLabel: normalizeInlineApplicationStatusLabel(source.status_label || source.statusLabel || source.status),
|
||||
approvalStage: String(source.approval_stage || source.approvalStage || '').trim(),
|
||||
dateLabel: rangeText || dateText || resolveBodyField(['时间', '日期', '申请时间']) || '待补充',
|
||||
locationLabel: String(
|
||||
source.location ||
|
||||
source.application_location ||
|
||||
source.applicationLocation ||
|
||||
source.destination ||
|
||||
source.destination_city ||
|
||||
source.destinationCity ||
|
||||
''
|
||||
).trim() || resolveBodyField(['地点', '目的地']) || '待补充',
|
||||
reasonLabel: String(
|
||||
source.reason ||
|
||||
source.business_reason ||
|
||||
source.businessReason ||
|
||||
source.description ||
|
||||
source.title ||
|
||||
''
|
||||
).trim() || resolveBodyField(['事由', '事件', '申请事由']) || '待补充',
|
||||
amountLabel: String(
|
||||
source.amount ||
|
||||
source.application_amount ||
|
||||
source.applicationAmount ||
|
||||
source.estimated_amount ||
|
||||
source.estimatedAmount ||
|
||||
''
|
||||
).trim() || resolveBodyField(['金额', '预计金额', '申请金额']) || '-',
|
||||
documentTypeLabel: String(
|
||||
source.document_type_label ||
|
||||
source.documentTypeLabel ||
|
||||
@@ -1311,10 +1388,11 @@ function buildInlineApplicationResultTable(draftPayload = {}, options = {}) {
|
||||
const reference = info.claimNo || info.claimId
|
||||
const href = buildInlineApplicationActionDetailHref(info)
|
||||
const actionText = href ? `[查看](${href})` : '-'
|
||||
const statusLabel = normalizeInlineApplicationStatusLabel(info.statusLabel, options.statusLabel)
|
||||
return [
|
||||
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 操作 |',
|
||||
'| --- | --- | --- | --- | --- |',
|
||||
`| ${normalizeInlineApplicationResultTableCell(info.documentTypeLabel || options.documentTypeLabel, '出差申请')} | ${normalizeInlineApplicationResultTableCell(reference)} | ${normalizeInlineApplicationResultTableCell(info.statusLabel || options.statusLabel)} | ${normalizeInlineApplicationResultTableCell(info.approvalStage || options.stageLabel)} | ${actionText} |`
|
||||
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 日期 | 地点 | 事由 | 金额 | 操作 |',
|
||||
'| --- | --- | --- | --- | --- | --- | --- | --- | --- |',
|
||||
`| ${normalizeInlineApplicationResultTableCell(info.documentTypeLabel || options.documentTypeLabel, '出差申请')} | ${normalizeInlineApplicationResultTableCell(reference)} | ${normalizeInlineApplicationResultTableCell(statusLabel)} | ${normalizeInlineApplicationResultTableCell(info.approvalStage || options.stageLabel)} | ${normalizeInlineApplicationResultTableCell(info.dateLabel)} | ${normalizeInlineApplicationResultTableCell(info.locationLabel)} | ${normalizeInlineApplicationResultTableCell(info.reasonLabel)} | ${normalizeInlineApplicationResultTableCell(info.amountLabel, '-')} | ${actionText} |`
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
@@ -1331,7 +1409,7 @@ function buildInlineApplicationPreviewActionResultText(actionType, payload = {})
|
||||
stageLabel: approvalStage || '直属领导审批',
|
||||
documentTypeLabel: '出差申请'
|
||||
}),
|
||||
'需要查看完整详情时,请点击列表最后一列的“查看”进入单据详情。'
|
||||
'需要查看完整详情时,请点击卡片“操作”行的“查看”进入单据详情。'
|
||||
].filter(Boolean).join('\n\n')
|
||||
}
|
||||
return [
|
||||
@@ -1342,7 +1420,7 @@ function buildInlineApplicationPreviewActionResultText(actionType, payload = {})
|
||||
stageLabel: '待提交',
|
||||
documentTypeLabel: '出差申请'
|
||||
}),
|
||||
'后续请点击表格最后一列的“查看”进入详情页继续核对。'
|
||||
'后续请点击卡片“操作”行的“查看”进入详情页继续核对。'
|
||||
].filter(Boolean).join('\n\n')
|
||||
}
|
||||
|
||||
@@ -1935,74 +2013,6 @@ async function fetchInlineStewardPlan(messageId, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseAiDocumentDetailHref(href = '') {
|
||||
const value = String(href || '').trim()
|
||||
if (!value.startsWith(AI_DOCUMENT_DETAIL_HREF_PREFIX)) {
|
||||
return null
|
||||
}
|
||||
const encodedReference = value.slice(AI_DOCUMENT_DETAIL_HREF_PREFIX.length)
|
||||
if (!encodedReference) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const reference = decodeURIComponent(encodedReference).trim()
|
||||
return reference ? { reference } : null
|
||||
} catch {
|
||||
return { reference: encodedReference }
|
||||
}
|
||||
}
|
||||
|
||||
function parseAiApplicationDetailHref(href = '') {
|
||||
const value = String(href || '').trim()
|
||||
if (!value.startsWith(AI_APPLICATION_DETAIL_HREF_PREFIX)) {
|
||||
return null
|
||||
}
|
||||
const encodedReference = value.slice(AI_APPLICATION_DETAIL_HREF_PREFIX.length)
|
||||
if (!encodedReference) {
|
||||
return null
|
||||
}
|
||||
let reference = ''
|
||||
try {
|
||||
reference = decodeURIComponent(encodedReference).trim()
|
||||
} catch {
|
||||
reference = encodedReference.trim()
|
||||
}
|
||||
if (!reference) {
|
||||
return null
|
||||
}
|
||||
const params = new URLSearchParams(reference)
|
||||
const claimId = String(params.get('claim_id') || '').trim()
|
||||
const claimNo = String(params.get('claim_no') || '').trim()
|
||||
if (claimId || claimNo) {
|
||||
return {
|
||||
reference: claimNo || claimId,
|
||||
claimId,
|
||||
claimNo
|
||||
}
|
||||
}
|
||||
return { reference }
|
||||
}
|
||||
|
||||
function buildAiDocumentDetailRequest(detailReference = {}) {
|
||||
const reference = String(detailReference.reference || '').trim()
|
||||
const claimId = String(detailReference.claimId || detailReference.claim_id || '').trim()
|
||||
const claimNo = String(detailReference.claimNo || detailReference.claim_no || '').trim()
|
||||
const lookupReference = claimId || reference
|
||||
const displayReference = claimNo || reference
|
||||
const isApplication = /^APP?-/i.test(displayReference) || Boolean(claimId || claimNo)
|
||||
return {
|
||||
id: lookupReference,
|
||||
claimId: claimId || reference,
|
||||
claimNo: claimNo || reference,
|
||||
documentNo: displayReference,
|
||||
documentType: isApplication ? 'application' : 'reimbursement',
|
||||
documentTypeCode: isApplication ? 'application' : 'reimbursement',
|
||||
detailLookupOnly: true,
|
||||
source: 'workbench',
|
||||
returnTo: 'workbench'
|
||||
}
|
||||
}
|
||||
|
||||
function handleAiAnswerMarkdownClick(event) {
|
||||
const target = event?.target
|
||||
const link = target?.closest?.('a[href^="#ai-open-document-detail:"], a[href^="#ai-open-application-detail:"]')
|
||||
@@ -2055,6 +2065,47 @@ function failAiDocumentQueryEvents(events) {
|
||||
}))
|
||||
}
|
||||
|
||||
function resolveAiDocumentQueryFetchPendingText(intent = {}) {
|
||||
if (intent.source === 'approval') {
|
||||
return '等待调用待我审核单据接口。'
|
||||
}
|
||||
if (intent.source === 'mine') {
|
||||
return '等待调用我名下单据接口。'
|
||||
}
|
||||
return '等待同时调用我名下单据和待我审核单据接口。'
|
||||
}
|
||||
|
||||
function resolveAiDocumentQueryFetchRunningText(intent = {}) {
|
||||
if (intent.source === 'approval') {
|
||||
return '正在查询待我审核的单据,接口范围为待办/待审单据列表。'
|
||||
}
|
||||
if (intent.source === 'mine') {
|
||||
return '正在查询我名下的单据,接口范围为当前用户本人单据列表。'
|
||||
}
|
||||
return '正在查询我可见的单据,接口范围包含我名下单据和待我审核单据列表。'
|
||||
}
|
||||
|
||||
async function fetchAiDocumentQueryPayload(intent = {}) {
|
||||
const requestParams = { page: 1, pageSize: 100 }
|
||||
if (intent.source === 'approval') {
|
||||
return fetchApprovalExpenseClaims(requestParams)
|
||||
}
|
||||
if (intent.source === 'mine') {
|
||||
return fetchExpenseClaims(requestParams)
|
||||
}
|
||||
const [ownPayload, approvalPayload] = await Promise.all([
|
||||
fetchExpenseClaims(requestParams),
|
||||
fetchApprovalExpenseClaims(requestParams)
|
||||
])
|
||||
return mergeAiDocumentQueryPayloads(
|
||||
ownPayload,
|
||||
{
|
||||
items: extractExpenseClaimItems(approvalPayload),
|
||||
querySource: 'approval'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function handleAiDocumentQueryIntent(prompt, pendingMessage) {
|
||||
const intent = resolveAiDocumentQueryIntent(prompt)
|
||||
if (!intent) {
|
||||
@@ -2072,7 +2123,7 @@ async function handleAiDocumentQueryIntent(prompt, pendingMessage) {
|
||||
{
|
||||
eventId: 'document-query-fetch',
|
||||
title: '查询业务单据接口',
|
||||
content: intent.source === 'approval' ? '等待调用待我审核单据接口。' : '等待调用我名下单据接口。',
|
||||
content: resolveAiDocumentQueryFetchPendingText(intent),
|
||||
status: 'pending'
|
||||
},
|
||||
{
|
||||
@@ -2090,9 +2141,7 @@ async function handleAiDocumentQueryIntent(prompt, pendingMessage) {
|
||||
event.eventId === 'document-query-fetch'
|
||||
? {
|
||||
...event,
|
||||
content: intent.source === 'approval'
|
||||
? '正在查询待我审核的单据,接口范围为待办/待审单据列表。'
|
||||
: '正在查询我名下的单据,接口范围为当前用户可见单据列表。',
|
||||
content: resolveAiDocumentQueryFetchRunningText(intent),
|
||||
status: 'running'
|
||||
}
|
||||
: event
|
||||
@@ -2100,9 +2149,7 @@ async function handleAiDocumentQueryIntent(prompt, pendingMessage) {
|
||||
await updateAiDocumentQueryThinking(pendingMessage, thinkingEvents)
|
||||
|
||||
try {
|
||||
const payload = intent.source === 'approval'
|
||||
? await fetchApprovalExpenseClaims({ page: 1, pageSize: 100 })
|
||||
: await fetchExpenseClaims({ page: 1, pageSize: 100 })
|
||||
const payload = await fetchAiDocumentQueryPayload(intent)
|
||||
const rawCount = extractExpenseClaimItems(payload).length
|
||||
const filteredRecords = filterAiDocumentQueryRecords(payload, intent)
|
||||
thinkingEvents = completeAiDocumentQueryEvent(
|
||||
|
||||
@@ -25,6 +25,18 @@ const DELETED_APPLICATION_DETAIL_HREF_PREFIX = '#ai-deleted-application-detail:'
|
||||
const DOCUMENT_DETAIL_HREF_PREFIX = '#ai-open-document-detail:'
|
||||
const TRUSTED_HTML_BLOCK_RE = /<!--\s*ai-trusted-html:start\s*-->\s*([\s\S]*?)\s*<!--\s*ai-trusted-html:end\s*-->/g
|
||||
const TRUSTED_HTML_PLACEHOLDER_PREFIX = 'AI_TRUSTED_HTML_BLOCK_'
|
||||
const DOCUMENT_STATUS_LABELS = {
|
||||
draft: '草稿',
|
||||
submitted: '审批中',
|
||||
pending: '待处理',
|
||||
approved: '已审批',
|
||||
completed: '已完成',
|
||||
archived: '已归档',
|
||||
returned: '已退回',
|
||||
rejected: '已驳回',
|
||||
pending_payment: '待付款',
|
||||
paid: '已付款'
|
||||
}
|
||||
const TRUSTED_HTML_ALLOWED_TAGS = new Set([
|
||||
'section',
|
||||
'article',
|
||||
@@ -518,6 +530,29 @@ function hasMeaningfulTableValue(value = '') {
|
||||
return Boolean(text && text !== '-')
|
||||
}
|
||||
|
||||
function normalizeDocumentStatusLabel(status = '') {
|
||||
const text = String(status || '').trim()
|
||||
if (!text || text === '-') {
|
||||
return ''
|
||||
}
|
||||
return DOCUMENT_STATUS_LABELS[text.toLowerCase()] || text
|
||||
}
|
||||
|
||||
function resolveDocumentRecordTone(status = '', stage = '') {
|
||||
const normalizedStatus = normalizeDocumentStatusLabel(status)
|
||||
const text = `${normalizedStatus || String(status || '')} ${String(stage || '')}`.trim()
|
||||
if (/已删除|已驳回|驳回|拒绝|失败/.test(text)) {
|
||||
return 'is-danger'
|
||||
}
|
||||
if (/已审批|审批通过|已完成|已归档|已付款|已支付|可通过/.test(text)) {
|
||||
return 'is-success'
|
||||
}
|
||||
if (/草稿|待提交|待补充|已退回|退回/.test(text)) {
|
||||
return 'is-warning'
|
||||
}
|
||||
return 'is-pending'
|
||||
}
|
||||
|
||||
function isDocumentRecordTable(normalizedHeader = []) {
|
||||
return (
|
||||
normalizedHeader.includes('单据编号') &&
|
||||
@@ -526,15 +561,32 @@ function isDocumentRecordTable(normalizedHeader = []) {
|
||||
)
|
||||
}
|
||||
|
||||
function renderRecordMeta(label = '', value = '') {
|
||||
function renderDocumentCardField(label = '', value = '', options = {}) {
|
||||
if (!hasMeaningfulTableValue(value)) {
|
||||
return ''
|
||||
}
|
||||
const valueClass = options.valueClass ? ` ${options.valueClass}` : ''
|
||||
return [
|
||||
'<span class="ai-html-record-meta-item">',
|
||||
`<small>${escapeHtml(label)}</small>`,
|
||||
`<b>${renderInlineHtml(value)}</b>`,
|
||||
'</span>'
|
||||
`<div class="ai-document-card__field${options.fieldClass ? ` ${options.fieldClass}` : ''}">`,
|
||||
`<span class="ai-document-card__label">${escapeHtml(label)}</span>`,
|
||||
`<strong class="ai-document-card__value${valueClass}">${renderInlineHtml(value)}</strong>`,
|
||||
'</div>'
|
||||
].join('')
|
||||
}
|
||||
|
||||
function renderDocumentCardAction(action = '') {
|
||||
if (!hasMeaningfulTableValue(action)) {
|
||||
return ''
|
||||
}
|
||||
const actionHtml = renderInlineHtml(action).replace(
|
||||
/class="ai-html-action-link\s+/g,
|
||||
'class="ai-html-action-link ai-document-card__action '
|
||||
)
|
||||
return [
|
||||
'<div class="ai-document-card__field ai-document-card__field--action">',
|
||||
'<span class="ai-document-card__label">操作</span>',
|
||||
actionHtml,
|
||||
'</div>'
|
||||
].join('')
|
||||
}
|
||||
|
||||
@@ -543,32 +595,50 @@ function renderDocumentRecordList(header = [], bodyRows = []) {
|
||||
const items = bodyRows.map((row) => {
|
||||
const documentType = resolveTableCell(row, normalizedHeader, ['单据类型'])
|
||||
const documentNo = resolveTableCell(row, normalizedHeader, ['单据编号'])
|
||||
const applyTime = resolveTableCell(row, normalizedHeader, ['申请时间'])
|
||||
const status = resolveTableCell(row, normalizedHeader, ['单据状态', '状态'])
|
||||
const applyTime = resolveTableCell(row, normalizedHeader, ['申请时间', '日期', '时间'])
|
||||
const location = resolveTableCell(row, normalizedHeader, ['地点', '目的地'])
|
||||
const amount = resolveTableCell(row, normalizedHeader, ['金额', '预计金额', '报销金额'])
|
||||
const status = normalizeDocumentStatusLabel(resolveTableCell(row, normalizedHeader, ['单据状态', '状态']))
|
||||
const stage = resolveTableCell(row, normalizedHeader, ['当前节点'])
|
||||
const reason = resolveTableCell(row, normalizedHeader, ['事由'])
|
||||
const action = resolveTableCell(row, normalizedHeader, ['操作'])
|
||||
const tone = resolveDocumentRecordTone(status, stage)
|
||||
const title = documentType || reason || documentNo || '单据详情'
|
||||
const summarySecondField = amount
|
||||
? renderDocumentCardField('金额', amount, { valueClass: 'ai-document-card__amount' })
|
||||
: renderDocumentCardField('当前节点', stage || status || '待确认')
|
||||
const summaryHtml = [
|
||||
renderDocumentCardField('日期', applyTime || '待补充'),
|
||||
summarySecondField
|
||||
].join('')
|
||||
const detailsHtml = [
|
||||
renderDocumentCardField('地点', location || '待补充'),
|
||||
renderDocumentCardField('单据编号', documentNo, { valueClass: 'ai-document-card__number' }),
|
||||
renderDocumentCardField('事由', reason || '待补充'),
|
||||
amount ? renderDocumentCardField('当前节点', stage || status || '待确认') : '',
|
||||
renderDocumentCardAction(action),
|
||||
renderDocumentCardField('单据类型', documentType)
|
||||
].join('')
|
||||
return [
|
||||
'<article class="ai-html-record-item" role="listitem">',
|
||||
'<div class="ai-html-record-main">',
|
||||
hasMeaningfulTableValue(documentType) ? `<span class="ai-html-record-kicker">${renderInlineHtml(documentType)}</span>` : '',
|
||||
hasMeaningfulTableValue(documentNo) ? `<strong class="ai-html-record-id">${renderInlineHtml(documentNo)}</strong>` : '',
|
||||
hasMeaningfulTableValue(reason) ? `<p class="ai-html-record-reason">${renderInlineHtml(reason)}</p>` : '',
|
||||
`<article class="ai-document-card ${tone}" role="listitem" aria-label="单据详情">`,
|
||||
'<header class="ai-document-card__head">',
|
||||
`<strong class="ai-document-card__reason">${renderInlineHtml(title)}</strong>`,
|
||||
hasMeaningfulTableValue(status) ? `<span class="ai-document-card__status">${renderInlineHtml(status)}</span>` : '',
|
||||
'</header>',
|
||||
'<div class="ai-document-card__body">',
|
||||
summaryHtml ? `<div class="ai-document-card__summary">${summaryHtml}</div>` : '',
|
||||
'<div class="ai-document-card__details">',
|
||||
detailsHtml,
|
||||
'</div>',
|
||||
'<div class="ai-html-record-meta">',
|
||||
renderRecordMeta('申请时间', applyTime),
|
||||
renderRecordMeta('状态', status),
|
||||
renderRecordMeta('当前节点', stage),
|
||||
'</div>',
|
||||
hasMeaningfulTableValue(action) ? `<div class="ai-html-record-action">${renderInlineHtml(action)}</div>` : '',
|
||||
'</article>'
|
||||
].join('')
|
||||
}).filter(Boolean)
|
||||
|
||||
return [
|
||||
'<div class="ai-html-record-list" role="list">',
|
||||
'<section class="ai-document-card-list" role="list" aria-label="单据结果">',
|
||||
...items,
|
||||
'</div>'
|
||||
'</section>'
|
||||
].join('')
|
||||
}
|
||||
|
||||
|
||||
141
web/src/utils/aiDocumentDetailReference.js
Normal file
141
web/src/utils/aiDocumentDetailReference.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import { isApplicationDocumentNo } from './documentClassification.js'
|
||||
|
||||
export const AI_DOCUMENT_DETAIL_HREF_PREFIX = '#ai-open-document-detail:'
|
||||
export const AI_APPLICATION_DETAIL_HREF_PREFIX = '#ai-open-application-detail:'
|
||||
|
||||
const SHORT_BUSINESS_DOCUMENT_NO_PATTERN = /^(?:A|R|D)[A-HJ-NP-Z2-9]{8}$/
|
||||
const LEGACY_BUSINESS_DOCUMENT_NO_PATTERN = /^(?:AP|APP|RE|AD|EXP|CL|BX)-/
|
||||
|
||||
function normalizeText(value = '') {
|
||||
return String(value ?? '').trim()
|
||||
}
|
||||
|
||||
export function isBusinessDocumentReference(value = '') {
|
||||
const text = normalizeText(value)
|
||||
return Boolean(
|
||||
text &&
|
||||
(
|
||||
SHORT_BUSINESS_DOCUMENT_NO_PATTERN.test(text) ||
|
||||
LEGACY_BUSINESS_DOCUMENT_NO_PATTERN.test(text)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function parseDetailReferencePayload(reference = '', options = {}) {
|
||||
const text = normalizeText(reference)
|
||||
if (!text) {
|
||||
return null
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(text)
|
||||
const claimId = normalizeText(params.get('claim_id') || params.get('claimId'))
|
||||
const claimNo = normalizeText(
|
||||
params.get('claim_no') ||
|
||||
params.get('claimNo') ||
|
||||
params.get('document_no') ||
|
||||
params.get('documentNo')
|
||||
)
|
||||
const documentType = normalizeText(options.documentType)
|
||||
|
||||
if (claimId || claimNo) {
|
||||
return {
|
||||
reference: claimNo || claimId,
|
||||
claimId,
|
||||
claimNo,
|
||||
...(documentType ? { documentType } : {})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
reference: text,
|
||||
...(documentType ? { documentType } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function parseAiDetailHref(href = '', prefix = '', options = {}) {
|
||||
const value = normalizeText(href)
|
||||
if (!value.startsWith(prefix)) {
|
||||
return null
|
||||
}
|
||||
const encodedReference = value.slice(prefix.length)
|
||||
if (!encodedReference) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return parseDetailReferencePayload(decodeURIComponent(encodedReference), options)
|
||||
} catch {
|
||||
return parseDetailReferencePayload(encodedReference, options)
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAiDocumentDetailHref(href = '') {
|
||||
return parseAiDetailHref(href, AI_DOCUMENT_DETAIL_HREF_PREFIX)
|
||||
}
|
||||
|
||||
export function parseAiApplicationDetailHref(href = '') {
|
||||
return parseAiDetailHref(href, AI_APPLICATION_DETAIL_HREF_PREFIX, { documentType: 'application' })
|
||||
}
|
||||
|
||||
function resolveExplicitClaimId(source = {}) {
|
||||
const claimNo = normalizeText(source.claimNo || source.claim_no || source.documentNo || source.document_no)
|
||||
const rawClaimId = normalizeText(source.claimId || source.claim_id || source.id)
|
||||
if (!rawClaimId || rawClaimId === claimNo || isBusinessDocumentReference(rawClaimId)) {
|
||||
return ''
|
||||
}
|
||||
return rawClaimId
|
||||
}
|
||||
|
||||
export function buildAiDocumentDetailHref(source = {}, options = {}) {
|
||||
const prefix = normalizeText(options.prefix) || AI_DOCUMENT_DETAIL_HREF_PREFIX
|
||||
const claimId = resolveExplicitClaimId(source)
|
||||
const claimNo = normalizeText(source.claimNo || source.claim_no || source.documentNo || source.document_no)
|
||||
const fallback = normalizeText(source.reference)
|
||||
|
||||
if (claimId || claimNo) {
|
||||
const params = new URLSearchParams()
|
||||
if (claimId) {
|
||||
params.set('claim_id', claimId)
|
||||
}
|
||||
if (claimNo) {
|
||||
params.set('claim_no', claimNo)
|
||||
}
|
||||
return `${prefix}${encodeURIComponent(params.toString())}`
|
||||
}
|
||||
|
||||
return fallback ? `${prefix}${encodeURIComponent(fallback)}` : ''
|
||||
}
|
||||
|
||||
export function buildAiDocumentDetailRequest(detailReference = {}) {
|
||||
const reference = normalizeText(detailReference.reference)
|
||||
const explicitClaimId = resolveExplicitClaimId(detailReference)
|
||||
const explicitClaimNo = normalizeText(
|
||||
detailReference.claimNo ||
|
||||
detailReference.claim_no ||
|
||||
detailReference.documentNo ||
|
||||
detailReference.document_no
|
||||
)
|
||||
const referenceIsBusinessNo = isBusinessDocumentReference(reference)
|
||||
const claimId = explicitClaimId || (!referenceIsBusinessNo ? reference : '')
|
||||
const claimNo = explicitClaimNo || (referenceIsBusinessNo ? reference : '')
|
||||
const lookupReference = claimId || claimNo || reference
|
||||
const displayReference = claimNo || reference || lookupReference
|
||||
const documentTypeCode = normalizeText(
|
||||
detailReference.documentTypeCode ||
|
||||
detailReference.document_type_code ||
|
||||
detailReference.documentType ||
|
||||
detailReference.document_type
|
||||
).toLowerCase()
|
||||
const isApplication = documentTypeCode === 'application' || isApplicationDocumentNo(displayReference)
|
||||
|
||||
return {
|
||||
id: lookupReference,
|
||||
claimId,
|
||||
claimNo,
|
||||
documentNo: displayReference,
|
||||
documentType: isApplication ? 'application' : 'reimbursement',
|
||||
documentTypeCode: isApplication ? 'application' : 'reimbursement',
|
||||
detailLookupOnly: true,
|
||||
source: 'workbench',
|
||||
returnTo: 'workbench'
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { extractExpenseClaimItems } from '../services/reimbursements.js'
|
||||
import { buildAiDocumentDetailHref } from './aiDocumentDetailReference.js'
|
||||
import { isApplicationDocumentNo } from './documentClassification.js'
|
||||
|
||||
const DOCUMENT_QUERY_LIMIT = 8
|
||||
@@ -66,6 +67,14 @@ function normalizeText(value) {
|
||||
return String(value ?? '').trim()
|
||||
}
|
||||
|
||||
function resolveStatusDisplayLabel(value = '') {
|
||||
const text = normalizeText(value)
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
return STATUS_LABELS[text.toLowerCase()] || text
|
||||
}
|
||||
|
||||
function escapeHtml(value = '') {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
@@ -279,9 +288,15 @@ function resolveSource(prompt) {
|
||||
sourceLabel: '待我审核的单据'
|
||||
}
|
||||
}
|
||||
if (/我名下|我发起|我提交|我创建|我的申请|我的报销/.test(text)) {
|
||||
return {
|
||||
source: 'mine',
|
||||
sourceLabel: '我的单据'
|
||||
}
|
||||
}
|
||||
return {
|
||||
source: 'mine',
|
||||
sourceLabel: '我的单据'
|
||||
source: 'accessible',
|
||||
sourceLabel: '我可见的单据'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +338,36 @@ function resolveClaimId(claim = {}) {
|
||||
return normalizeText(claim.id || claim.claim_id || claim.claimId || resolveDocumentNo(claim))
|
||||
}
|
||||
|
||||
export function mergeAiDocumentQueryPayloads(...payloads) {
|
||||
const records = []
|
||||
const seen = new Set()
|
||||
|
||||
payloads.forEach((payload) => {
|
||||
const querySource = normalizeText(payload?.querySource || payload?.aiQuerySource || payload?.ai_query_source)
|
||||
extractExpenseClaimItems(payload).forEach((claim) => {
|
||||
const normalizedClaim = querySource
|
||||
? { ...claim, ai_query_source: querySource }
|
||||
: claim
|
||||
const documentNo = resolveDocumentNo(normalizedClaim)
|
||||
const claimId = resolveClaimId(normalizedClaim)
|
||||
const documentType = resolveDocumentTypeCode(normalizedClaim)
|
||||
const stableId = documentNo || claimId
|
||||
if (!stableId) {
|
||||
records.push(normalizedClaim)
|
||||
return
|
||||
}
|
||||
const key = `${documentType}:${stableId}`
|
||||
if (seen.has(key)) {
|
||||
return
|
||||
}
|
||||
seen.add(key)
|
||||
records.push(normalizedClaim)
|
||||
})
|
||||
})
|
||||
|
||||
return records
|
||||
}
|
||||
|
||||
function resolveDocumentTypeCode(claim = {}) {
|
||||
const explicitType = normalizeText(
|
||||
claim.document_type_code
|
||||
@@ -346,7 +391,10 @@ function resolveDocumentTypeCode(claim = {}) {
|
||||
|
||||
function resolveStatusLabel(claim = {}) {
|
||||
const key = normalizeText(claim.status || claim.state || claim.approval_status || claim.approvalStatus).toLowerCase()
|
||||
return normalizeText(claim.status_label || claim.statusLabel || claim.approval_stage || claim.approvalStage) || STATUS_LABELS[key] || '待确认'
|
||||
const explicitLabel = resolveStatusDisplayLabel(
|
||||
claim.status_label || claim.statusLabel || claim.approval_stage || claim.approvalStage
|
||||
)
|
||||
return explicitLabel || STATUS_LABELS[key] || '待确认'
|
||||
}
|
||||
|
||||
// 状态语义化分类,驱动卡片着色:进行中 / 正向终态 / 需关注 / 异常终态
|
||||
@@ -368,6 +416,28 @@ function resolveStatusKey(claim = {}) {
|
||||
return normalizeText(claim.status || claim.state || claim.approval_status || claim.approvalStatus).toLowerCase()
|
||||
}
|
||||
|
||||
function resolveRecordQuerySource(claim = {}, intent = {}) {
|
||||
return normalizeText(
|
||||
claim.ai_query_source
|
||||
|| claim.aiQuerySource
|
||||
|| claim.querySource
|
||||
|| (intent.source === 'approval' ? 'approval' : '')
|
||||
)
|
||||
}
|
||||
|
||||
function isApprovalTaskClaim(claim = {}, intent = {}) {
|
||||
return resolveRecordQuerySource(claim, intent) === 'approval'
|
||||
}
|
||||
|
||||
function isPendingApprovalStatus(statusKey = '', statusLabel = '') {
|
||||
const normalizedStatusKey = normalizeText(statusKey).toLowerCase()
|
||||
const normalizedStatusLabel = normalizeText(statusLabel)
|
||||
if (['approved', 'completed', 'archived', 'paid', 'rejected', 'returned', 'cancelled'].includes(normalizedStatusKey)) {
|
||||
return false
|
||||
}
|
||||
return !/已审批|已批准|审批通过|已完成|已付款|已支付|已归档|已驳回|已退回|已拒绝|拒绝|取消/.test(normalizedStatusLabel)
|
||||
}
|
||||
|
||||
function resolveReason(claim = {}) {
|
||||
return normalizeText(claim.reason || claim.business_reason || claim.description || claim.title || claim.note) || '未填写事由'
|
||||
}
|
||||
@@ -569,7 +639,7 @@ function toTimestamp(dateText) {
|
||||
return date ? date.getTime() : 0
|
||||
}
|
||||
|
||||
function normalizeRecord(claim = {}) {
|
||||
function normalizeRecord(claim = {}, intent = {}) {
|
||||
const documentType = resolveDocumentTypeCode(claim)
|
||||
const documentNo = resolveDocumentNo(claim)
|
||||
const date = resolveRecordDate(claim)
|
||||
@@ -577,7 +647,13 @@ function normalizeRecord(claim = {}) {
|
||||
const reason = resolveReason(claim)
|
||||
const expenseTypeCode = resolveExpenseTypeCode(claim)
|
||||
const typeLabel = resolveExpenseTypeLabel(claim)
|
||||
const statusLabel = resolveStatusLabel(claim)
|
||||
const statusKey = resolveStatusKey(claim)
|
||||
const rawStatusLabel = resolveStatusLabel(claim)
|
||||
const querySource = resolveRecordQuerySource(claim, intent)
|
||||
const isApprovalTask = isApprovalTaskClaim(claim, intent)
|
||||
const statusLabel = isApprovalTask && isPendingApprovalStatus(statusKey, rawStatusLabel)
|
||||
? '待审批'
|
||||
: rawStatusLabel
|
||||
const ownerLabel = resolveOwnerLabel(claim)
|
||||
const departmentLabel = resolveDepartmentLabel(claim)
|
||||
const locationLabel = resolveLocationLabel(claim)
|
||||
@@ -593,9 +669,11 @@ function normalizeRecord(claim = {}) {
|
||||
time: resolveTimeLabel(claim, date),
|
||||
dateKey: date,
|
||||
updatedTime: updatedDate || '未显示',
|
||||
statusKey: resolveStatusKey(claim),
|
||||
statusKey,
|
||||
statusLabel,
|
||||
statusTone: resolveStatusTone(statusLabel),
|
||||
querySource,
|
||||
isApprovalTask,
|
||||
reason,
|
||||
amountLabel: resolveAmountLabel(claim),
|
||||
amountValue: resolveAmountValue(claim),
|
||||
@@ -653,7 +731,7 @@ function matchesAmountFilter(record = {}, amountFilter = null) {
|
||||
|
||||
export function filterAiDocumentQueryRecords(claimsPayload, intent = {}) {
|
||||
const rows = extractExpenseClaimItems(claimsPayload)
|
||||
.map((claim) => normalizeRecord(claim))
|
||||
.map((claim) => normalizeRecord(claim, intent))
|
||||
.filter((record) => (
|
||||
!intent?.documentType ||
|
||||
intent.documentType === 'all' ||
|
||||
@@ -669,50 +747,62 @@ export function filterAiDocumentQueryRecords(claimsPayload, intent = {}) {
|
||||
return rows
|
||||
}
|
||||
|
||||
function buildDocumentDetailHref(record = {}) {
|
||||
const reference = normalizeText(record.documentNo || record.claimNo || record.claimId || record.id)
|
||||
return reference ? `#ai-open-document-detail:${encodeURIComponent(reference)}` : ''
|
||||
function buildDocumentCardFieldHtml(label = '', value = '', options = {}) {
|
||||
const text = normalizeText(value)
|
||||
if (!text || text === '-') {
|
||||
return ''
|
||||
}
|
||||
const valueClass = options.valueClass ? ` ${options.valueClass}` : ''
|
||||
const fieldClass = options.fieldClass ? ` ${options.fieldClass}` : ''
|
||||
return [
|
||||
`<div class="ai-document-card__field${fieldClass}">`,
|
||||
`<span class="ai-document-card__label">${escapeHtml(label)}</span>`,
|
||||
`<strong class="ai-document-card__value${valueClass}">${escapeHtml(text)}</strong>`,
|
||||
'</div>'
|
||||
].join('')
|
||||
}
|
||||
|
||||
function buildDocumentCardHtml(record = {}) {
|
||||
const href = buildDocumentDetailHref(record)
|
||||
const href = buildAiDocumentDetailHref(record)
|
||||
const typeClass = record.documentType === 'application' ? 'application' : 'reimbursement'
|
||||
const approvalTaskClass = record.isApprovalTask ? ' ai-document-card--approval-task' : ''
|
||||
const statusTone = record.statusTone || 'is-pending'
|
||||
const amountLabel = record.documentType === 'application' ? '预计金额' : '报销金额'
|
||||
const documentTypeText = [record.documentTypeLabel, record.typeLabel].filter(Boolean).join(' · ')
|
||||
const title = record.typeLabel || record.documentTypeLabel || record.reason || '单据详情'
|
||||
const ownerText = [record.ownerLabel, record.departmentLabel]
|
||||
.filter((item) => item && item !== '未显示')
|
||||
.join(' · ') || '未显示'
|
||||
const summaryHtml = [
|
||||
buildDocumentCardFieldHtml('日期', record.time || '待补充'),
|
||||
buildDocumentCardFieldHtml(amountLabel, record.amountLabel, { valueClass: 'ai-document-card__amount' })
|
||||
].join('')
|
||||
const detailsHtml = [
|
||||
buildDocumentCardFieldHtml('地点', record.locationLabel || '待补充'),
|
||||
buildDocumentCardFieldHtml('单据编号', record.documentNo || '未编号单据', { valueClass: 'ai-document-card__number' }),
|
||||
buildDocumentCardFieldHtml('事由', record.reason || '待补充'),
|
||||
buildDocumentCardFieldHtml('申请人', ownerText),
|
||||
[
|
||||
'<div class="ai-document-card__field ai-document-card__field--action">',
|
||||
'<span class="ai-document-card__label">操作</span>',
|
||||
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>`
|
||||
: '<span class="ai-document-card__value">暂无详情</span>',
|
||||
'</div>'
|
||||
].join(''),
|
||||
buildDocumentCardFieldHtml('单据类型', documentTypeText)
|
||||
].join('')
|
||||
|
||||
return [
|
||||
`<article class="ai-document-card ai-document-card--${typeClass} ${statusTone}" aria-label="单据详情">`,
|
||||
`<article class="ai-document-card ai-document-card--${typeClass}${approvalTaskClass} ${statusTone}" aria-label="单据详情">`,
|
||||
'<header class="ai-document-card__head">',
|
||||
`<strong class="ai-document-card__reason">${escapeHtml(record.reason)}</strong>`,
|
||||
`<strong class="ai-document-card__reason">${escapeHtml(title)}</strong>`,
|
||||
`<span class="ai-document-card__status">${escapeHtml(record.statusLabel)}</span>`,
|
||||
'</header>',
|
||||
'<div class="ai-document-card__body">',
|
||||
summaryHtml ? `<div class="ai-document-card__summary">${summaryHtml}</div>` : '',
|
||||
'<div class="ai-document-card__details">',
|
||||
'<div class="ai-document-card__field">',
|
||||
'<span class="ai-document-card__label">单据类型</span>',
|
||||
`<strong class="ai-document-card__value">${escapeHtml(record.documentTypeLabel)} · ${escapeHtml(record.typeLabel)}</strong>`,
|
||||
'</div>',
|
||||
'<div class="ai-document-card__field">',
|
||||
`<span class="ai-document-card__label">${escapeHtml(amountLabel)}</span>`,
|
||||
`<strong class="ai-document-card__amount">${escapeHtml(record.amountLabel)}</strong>`,
|
||||
'</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
|
||||
? `<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>',
|
||||
'</div>',
|
||||
detailsHtml,
|
||||
'</div>',
|
||||
'</div>',
|
||||
'</article>'
|
||||
|
||||
@@ -623,6 +623,8 @@ function stripKnownContextFromReason(value, context = {}) {
|
||||
.replace(/(?:地点|业务地点|发生地点|目的地)\s*[::]\s*(?=[,,、。;;\s]|$)/gu, '')
|
||||
.replace(/20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}\s*(?:至|到|~|—|–|--)\s*20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/gu, '')
|
||||
.replace(/20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/gu, '')
|
||||
.replace(/(?:\d{1,2}月)?\d{1,2}日?\s*(?:至|到|~|—|–|--|-)\s*(?:\d{1,2}月)?\d{1,2}日?/gu, '')
|
||||
.replace(/\d{1,2}月\d{1,2}日?/gu, '')
|
||||
.replace(/(?:出差|申请)?\d+\s*天/gu, '')
|
||||
.replace(/(?:用户预估费用|预估费用|预计总费用|预计费用|预计金额|申请金额|预算|金额|费用)\s*[::]?\s*\d+(?:\.\d+)?\s*(?:元|块|人民币)?/gu, '')
|
||||
.replace(/(?:高铁|动车|火车|铁路|飞机|机票|航班|轮船|船票|客轮|渡轮|出租车|的士|网约车|打车|自驾)/gu, '')
|
||||
|
||||
Reference in New Issue
Block a user