Compare commits
2 Commits
3b74a330a3
...
24b5b71b0f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24b5b71b0f | ||
|
|
8b3495455b |
Binary file not shown.
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) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card) {
|
||||||
|
--ai-document-card-head-bg: rgba(37, 99, 235, 0.075);
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 1px solid rgba(203, 213, 225, 0.76);
|
border: 0;
|
||||||
border-left: 0;
|
border-radius: 14px;
|
||||||
border-radius: 12px;
|
background-color: #ffffff;
|
||||||
background: rgba(255, 255, 255, 0.96);
|
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:
|
box-shadow:
|
||||||
|
inset 0 0 0 1px rgba(203, 213, 225, 0.5),
|
||||||
0 1px 2px rgba(15, 23, 42, 0.035),
|
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;
|
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: 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.72);
|
box-shadow:
|
||||||
background: #ffffff;
|
inset 0 0 0 1px rgba(148, 163, 184, 0.46),
|
||||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.065);
|
0 1px 2px rgba(15, 23, 42, 0.04),
|
||||||
|
0 18px 38px rgba(15, 23, 42, 0.07);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1194,8 +1201,8 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 13px 18px;
|
padding: 13px 18px 13px 20px;
|
||||||
background: rgba(37, 99, 235, 0.11);
|
background: var(--ai-document-card-head-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__status) {
|
.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) {
|
.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) {
|
.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) {
|
.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) {
|
.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) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__body) {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 15px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 16px 18px 18px;
|
padding: 15px 18px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__reason) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__reason) {
|
||||||
@@ -1271,12 +1278,68 @@
|
|||||||
color: #991b1b;
|
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) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__details) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 12px 28px;
|
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) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__field) {
|
||||||
@@ -1288,7 +1351,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workbench-ai-answer-markdown :deep(.ai-document-card__field--action) {
|
.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) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__label) {
|
||||||
@@ -1373,109 +1440,6 @@
|
|||||||
font-weight: 850;
|
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) {
|
.workbench-ai-answer-markdown :deep(.ai-html-action-link.is-disabled) {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -1484,12 +1448,6 @@
|
|||||||
box-shadow: none;
|
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) {
|
.workbench-ai-answer-markdown :deep(.ai-html-image-frame) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1547,6 +1505,29 @@
|
|||||||
color: #1e40af;
|
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 {
|
@keyframes workbenchDocumentCardReveal {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -1566,15 +1547,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@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) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -1595,15 +1567,17 @@
|
|||||||
gap: 10px;
|
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) {
|
.workbench-ai-answer-markdown :deep(.ai-document-card__field) {
|
||||||
grid-template-columns: 76px minmax(0, 1fr);
|
grid-template-columns: 76px minmax(0, 1fr);
|
||||||
gap: 10px;
|
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) {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -727,8 +727,15 @@ import {
|
|||||||
buildAiDocumentQueryConditionSummary,
|
buildAiDocumentQueryConditionSummary,
|
||||||
buildAiDocumentQueryMessage,
|
buildAiDocumentQueryMessage,
|
||||||
filterAiDocumentQueryRecords,
|
filterAiDocumentQueryRecords,
|
||||||
|
mergeAiDocumentQueryPayloads,
|
||||||
resolveAiDocumentQueryIntent
|
resolveAiDocumentQueryIntent
|
||||||
} from '../../utils/aiDocumentQueryModel.js'
|
} from '../../utils/aiDocumentQueryModel.js'
|
||||||
|
import {
|
||||||
|
AI_APPLICATION_DETAIL_HREF_PREFIX,
|
||||||
|
buildAiDocumentDetailRequest,
|
||||||
|
parseAiApplicationDetailHref,
|
||||||
|
parseAiDocumentDetailHref
|
||||||
|
} from '../../utils/aiDocumentDetailReference.js'
|
||||||
import {
|
import {
|
||||||
buildRequiredApplicationActions,
|
buildRequiredApplicationActions,
|
||||||
buildRequiredApplicationMissingText,
|
buildRequiredApplicationMissingText,
|
||||||
@@ -780,9 +787,6 @@ const INLINE_ANSWER_STREAM_CHUNK_SIZE = 6
|
|||||||
const INLINE_ANSWER_STREAM_DELAY_MS = 24
|
const INLINE_ANSWER_STREAM_DELAY_MS = 24
|
||||||
const INLINE_AUTO_SCROLL_THRESHOLD = 96
|
const INLINE_AUTO_SCROLL_THRESHOLD = 96
|
||||||
const INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS = 260
|
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 {
|
const {
|
||||||
applicationPreviewEditor,
|
applicationPreviewEditor,
|
||||||
resolveApplicationPreviewEditorControl,
|
resolveApplicationPreviewEditorControl,
|
||||||
@@ -1269,6 +1273,27 @@ function normalizeInlineApplicationResultTableCell(value, fallback = '-') {
|
|||||||
return text || 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 = '') {
|
function buildInlineApplicationActionDetailHref(reference = '') {
|
||||||
const source = reference && typeof reference === 'object' ? reference : { reference }
|
const source = reference && typeof reference === 'object' ? reference : { reference }
|
||||||
const claimId = String(source.claimId || source.claim_id || source.id || '').trim()
|
const claimId = String(source.claimId || source.claim_id || source.id || '').trim()
|
||||||
@@ -1289,11 +1314,63 @@ function buildInlineApplicationActionDetailHref(reference = '') {
|
|||||||
|
|
||||||
function resolveInlineApplicationActionDocumentInfo(draftPayload = {}) {
|
function resolveInlineApplicationActionDocumentInfo(draftPayload = {}) {
|
||||||
const source = draftPayload && typeof draftPayload === 'object' ? 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 {
|
return {
|
||||||
claimNo: String(source.claim_no || source.claimNo || source.document_no || source.documentNo || '').trim(),
|
claimNo: String(source.claim_no || source.claimNo || source.document_no || source.documentNo || '').trim(),
|
||||||
claimId: String(source.claim_id || source.claimId || source.id || '').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(),
|
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(
|
documentTypeLabel: String(
|
||||||
source.document_type_label ||
|
source.document_type_label ||
|
||||||
source.documentTypeLabel ||
|
source.documentTypeLabel ||
|
||||||
@@ -1311,10 +1388,11 @@ function buildInlineApplicationResultTable(draftPayload = {}, options = {}) {
|
|||||||
const reference = info.claimNo || info.claimId
|
const reference = info.claimNo || info.claimId
|
||||||
const href = buildInlineApplicationActionDetailHref(info)
|
const href = buildInlineApplicationActionDetailHref(info)
|
||||||
const actionText = href ? `[查看](${href})` : '-'
|
const actionText = href ? `[查看](${href})` : '-'
|
||||||
|
const statusLabel = normalizeInlineApplicationStatusLabel(info.statusLabel, options.statusLabel)
|
||||||
return [
|
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')
|
].join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1331,7 +1409,7 @@ function buildInlineApplicationPreviewActionResultText(actionType, payload = {})
|
|||||||
stageLabel: approvalStage || '直属领导审批',
|
stageLabel: approvalStage || '直属领导审批',
|
||||||
documentTypeLabel: '出差申请'
|
documentTypeLabel: '出差申请'
|
||||||
}),
|
}),
|
||||||
'需要查看完整详情时,请点击列表最后一列的“查看”进入单据详情。'
|
'需要查看完整详情时,请点击卡片“操作”行的“查看”进入单据详情。'
|
||||||
].filter(Boolean).join('\n\n')
|
].filter(Boolean).join('\n\n')
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
@@ -1342,7 +1420,7 @@ function buildInlineApplicationPreviewActionResultText(actionType, payload = {})
|
|||||||
stageLabel: '待提交',
|
stageLabel: '待提交',
|
||||||
documentTypeLabel: '出差申请'
|
documentTypeLabel: '出差申请'
|
||||||
}),
|
}),
|
||||||
'后续请点击表格最后一列的“查看”进入详情页继续核对。'
|
'后续请点击卡片“操作”行的“查看”进入详情页继续核对。'
|
||||||
].filter(Boolean).join('\n\n')
|
].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) {
|
function handleAiAnswerMarkdownClick(event) {
|
||||||
const target = event?.target
|
const target = event?.target
|
||||||
const link = target?.closest?.('a[href^="#ai-open-document-detail:"], a[href^="#ai-open-application-detail:"]')
|
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) {
|
async function handleAiDocumentQueryIntent(prompt, pendingMessage) {
|
||||||
const intent = resolveAiDocumentQueryIntent(prompt)
|
const intent = resolveAiDocumentQueryIntent(prompt)
|
||||||
if (!intent) {
|
if (!intent) {
|
||||||
@@ -2072,7 +2123,7 @@ async function handleAiDocumentQueryIntent(prompt, pendingMessage) {
|
|||||||
{
|
{
|
||||||
eventId: 'document-query-fetch',
|
eventId: 'document-query-fetch',
|
||||||
title: '查询业务单据接口',
|
title: '查询业务单据接口',
|
||||||
content: intent.source === 'approval' ? '等待调用待我审核单据接口。' : '等待调用我名下单据接口。',
|
content: resolveAiDocumentQueryFetchPendingText(intent),
|
||||||
status: 'pending'
|
status: 'pending'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2090,9 +2141,7 @@ async function handleAiDocumentQueryIntent(prompt, pendingMessage) {
|
|||||||
event.eventId === 'document-query-fetch'
|
event.eventId === 'document-query-fetch'
|
||||||
? {
|
? {
|
||||||
...event,
|
...event,
|
||||||
content: intent.source === 'approval'
|
content: resolveAiDocumentQueryFetchRunningText(intent),
|
||||||
? '正在查询待我审核的单据,接口范围为待办/待审单据列表。'
|
|
||||||
: '正在查询我名下的单据,接口范围为当前用户可见单据列表。',
|
|
||||||
status: 'running'
|
status: 'running'
|
||||||
}
|
}
|
||||||
: event
|
: event
|
||||||
@@ -2100,9 +2149,7 @@ async function handleAiDocumentQueryIntent(prompt, pendingMessage) {
|
|||||||
await updateAiDocumentQueryThinking(pendingMessage, thinkingEvents)
|
await updateAiDocumentQueryThinking(pendingMessage, thinkingEvents)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = intent.source === 'approval'
|
const payload = await fetchAiDocumentQueryPayload(intent)
|
||||||
? await fetchApprovalExpenseClaims({ page: 1, pageSize: 100 })
|
|
||||||
: await fetchExpenseClaims({ page: 1, pageSize: 100 })
|
|
||||||
const rawCount = extractExpenseClaimItems(payload).length
|
const rawCount = extractExpenseClaimItems(payload).length
|
||||||
const filteredRecords = filterAiDocumentQueryRecords(payload, intent)
|
const filteredRecords = filterAiDocumentQueryRecords(payload, intent)
|
||||||
thinkingEvents = completeAiDocumentQueryEvent(
|
thinkingEvents = completeAiDocumentQueryEvent(
|
||||||
|
|||||||
@@ -321,6 +321,60 @@ function handleCancel() {
|
|||||||
max-height: min(420px, calc(100dvh - 292px));
|
max-height: min(420px, calc(100dvh - 292px));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shared-confirm-card--approval {
|
||||||
|
width: min(460px, calc(100vw - 40px));
|
||||||
|
gap: 10px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-color: rgba(var(--theme-primary-rgb, 58, 124, 165), 0.14);
|
||||||
|
border-radius: 6px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.99), rgba(248, 250, 252, 0.98));
|
||||||
|
box-shadow:
|
||||||
|
0 18px 40px rgba(15, 23, 42, 0.15),
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.88) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-confirm-card--approval .shared-confirm-badge {
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0 9px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-confirm-card--approval h4 {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.38;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-confirm-card--approval p {
|
||||||
|
max-width: 34em;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-confirm-card--approval .shared-confirm-body {
|
||||||
|
gap: 8px;
|
||||||
|
max-height: min(270px, calc(100dvh - 238px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-confirm-card--approval .shared-confirm-actions {
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-confirm-card--approval .shared-confirm-btn {
|
||||||
|
min-width: 118px;
|
||||||
|
min-height: 38px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-confirm-card--approval .shared-confirm-btn.confirm.primary {
|
||||||
|
box-shadow: 0 10px 20px rgba(var(--theme-primary-rgb, 58, 124, 165), 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
.shared-confirm-card--destructive {
|
.shared-confirm-card--destructive {
|
||||||
width: min(420px, calc(100vw - 40px));
|
width: min(420px, calc(100vw - 40px));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -8,14 +8,13 @@
|
|||||||
:aria-label="ariaLabel"
|
:aria-label="ariaLabel"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
<FloatingLightBandWindow
|
<div class="table-loading-card">
|
||||||
:icon="icon"
|
<span class="table-loading-spinner" aria-hidden="true"></span>
|
||||||
:message="message"
|
<div class="table-loading-copy">
|
||||||
:motion="motion"
|
<strong v-if="title">{{ title }}</strong>
|
||||||
:title="title"
|
<p v-if="message">{{ message }}</p>
|
||||||
:tone="tone"
|
</div>
|
||||||
:variant="variant"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
@@ -23,8 +22,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import FloatingLightBandWindow from './FloatingLightBandWindow.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
variant: {
|
variant: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -59,6 +56,52 @@ const ariaLabel = computed(() => [props.title, props.message].filter(Boolean).jo
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-loading-card {
|
||||||
|
width: min(420px, 100%);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
box-shadow:
|
||||||
|
0 14px 34px rgba(15, 23, 42, 0.08),
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.9) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-spinner {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 3px solid rgba(var(--theme-primary-rgb, 58, 124, 165), 0.16);
|
||||||
|
border-top-color: var(--theme-primary-active, #255b7d);
|
||||||
|
border-radius: 999px;
|
||||||
|
animation: table-loading-spin 820ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-copy {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-copy strong {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 850;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading-copy p {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
.table-loading-anchor {
|
.table-loading-anchor {
|
||||||
display: block;
|
display: block;
|
||||||
width: 0;
|
width: 0;
|
||||||
@@ -76,9 +119,7 @@ const ariaLabel = computed(() => [props.title, props.message].filter(Boolean).jo
|
|||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: rgba(248, 250, 252, 0.08);
|
background: rgba(248, 250, 252, 0.10);
|
||||||
backdrop-filter: blur(0.5px);
|
|
||||||
-webkit-backdrop-filter: blur(0.5px);
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +168,35 @@ const ariaLabel = computed(() => [props.title, props.message].filter(Boolean).jo
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-loading.banner {
|
.table-loading.banner {
|
||||||
display: block;
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #255b7d;
|
color: #255b7d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-loading.banner .table-loading-card {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-loading.banner .table-loading-spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes table-loading-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.table-loading-spinner {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
:open="open"
|
:open="open"
|
||||||
:badge="badge"
|
:badge="badge"
|
||||||
badge-tone="info"
|
badge-tone="info"
|
||||||
|
size="approval"
|
||||||
|
actions-align="end"
|
||||||
:title="title"
|
:title="title"
|
||||||
:description="description"
|
:description="description"
|
||||||
cancel-text="返回核对"
|
cancel-text="返回核对"
|
||||||
@@ -105,8 +107,8 @@ function handleRiskConfirmedChange(event) {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.approval-risk-confirm-panel {
|
.approval-risk-confirm-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
border: 1px solid #fed7aa;
|
border: 1px solid #fed7aa;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #fff7ed;
|
background: #fff7ed;
|
||||||
@@ -147,7 +149,7 @@ function handleRiskConfirmedChange(event) {
|
|||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
gap: 9px;
|
gap: 9px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 8px;
|
padding: 7px 8px;
|
||||||
border: 1px solid rgba(251, 146, 60, 0.28);
|
border: 1px solid rgba(251, 146, 60, 0.28);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -207,8 +209,8 @@ function handleRiskConfirmedChange(event) {
|
|||||||
|
|
||||||
.approval-opinion-field {
|
.approval-opinion-field {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
margin-top: 14px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.approval-opinion-field > span {
|
.approval-opinion-field > span {
|
||||||
@@ -232,15 +234,15 @@ function handleRiskConfirmedChange(event) {
|
|||||||
|
|
||||||
.approval-opinion-field textarea {
|
.approval-opinion-field textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 96px;
|
min-height: 74px;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
padding: 12px;
|
padding: 10px 11px;
|
||||||
border: 1px solid #d7e0ea;
|
border: 1px solid #d7e0ea;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.55;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +258,7 @@ function handleRiskConfirmedChange(event) {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.approval-opinion-field small span {
|
.approval-opinion-field small span {
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ export function useAppShell() {
|
|||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
|
||||||
const customRange = ref(createCurrentYearDateRange())
|
const customRange = ref(createCurrentYearDateRange())
|
||||||
|
const workbenchRequests = computed(() =>
|
||||||
|
mergeWorkbenchRequests(requests.value, workbenchApprovalRequests.value)
|
||||||
|
)
|
||||||
|
|
||||||
const selectedRequest = computed(() => {
|
const selectedRequest = computed(() => {
|
||||||
const requestId = String(route.params.requestId || '')
|
const requestId = String(route.params.requestId || '')
|
||||||
@@ -86,9 +89,7 @@ export function useAppShell() {
|
|||||||
return snapshot
|
return snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawRequest = requests.value.find(
|
const rawRequest = workbenchRequests.value.find((item) => isSameRequestIdentity(item, requestId))
|
||||||
(item) => String(item.claimId || '').trim() === requestId || String(item.id || '').trim() === requestId
|
|
||||||
)
|
|
||||||
const normalizedRequest = normalizeRequestForUi(rawRequest)
|
const normalizedRequest = normalizeRequestForUi(rawRequest)
|
||||||
if (normalizedRequest) {
|
if (normalizedRequest) {
|
||||||
return normalizedRequest
|
return normalizedRequest
|
||||||
@@ -239,10 +240,6 @@ export function useAppShell() {
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const workbenchRequests = computed(() =>
|
|
||||||
mergeWorkbenchRequests(requests.value, workbenchApprovalRequests.value)
|
|
||||||
)
|
|
||||||
|
|
||||||
const workbenchSummary = computed(() =>
|
const workbenchSummary = computed(() =>
|
||||||
buildWorkbenchSummary(workbenchRequests.value, currentUser.value)
|
buildWorkbenchSummary(workbenchRequests.value, currentUser.value)
|
||||||
)
|
)
|
||||||
@@ -641,6 +638,7 @@ export function useAppShell() {
|
|||||||
openTravelCreate,
|
openTravelCreate,
|
||||||
ranges,
|
ranges,
|
||||||
requestSummary,
|
requestSummary,
|
||||||
|
workbenchRequests,
|
||||||
workbenchSummary,
|
workbenchSummary,
|
||||||
requestsError,
|
requestsError,
|
||||||
requestsLoading,
|
requestsLoading,
|
||||||
|
|||||||
@@ -739,7 +739,16 @@ function resolveProgressDisplayLabel(label, documentTypeCode, claim, approvalMet
|
|||||||
if (
|
if (
|
||||||
documentTypeCode !== DOCUMENT_TYPE_APPLICATION
|
documentTypeCode !== DOCUMENT_TYPE_APPLICATION
|
||||||
&& approvalMeta.key !== 'completed'
|
&& approvalMeta.key !== 'completed'
|
||||||
&& (normalizedLabel === '直属领导审批' || normalizedLabel === '财务审批')
|
&& normalizedLabel === '直属领导审批'
|
||||||
|
&& workflowNode.includes(normalizedLabel.replace(/审批$/, ''))
|
||||||
|
) {
|
||||||
|
return '等待批复'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
documentTypeCode !== DOCUMENT_TYPE_APPLICATION
|
||||||
|
&& approvalMeta.key !== 'completed'
|
||||||
|
&& normalizedLabel === '财务审批'
|
||||||
&& workflowNode.includes(normalizedLabel.replace(/审批$/, ''))
|
&& workflowNode.includes(normalizedLabel.replace(/审批$/, ''))
|
||||||
) {
|
) {
|
||||||
return `等待 ${resolveReimbursementApproverName(claim, normalizedLabel)} 批复`
|
return `等待 ${resolveReimbursementApproverName(claim, normalizedLabel)} 批复`
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ const DELETED_APPLICATION_DETAIL_HREF_PREFIX = '#ai-deleted-application-detail:'
|
|||||||
const DOCUMENT_DETAIL_HREF_PREFIX = '#ai-open-document-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_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 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([
|
const TRUSTED_HTML_ALLOWED_TAGS = new Set([
|
||||||
'section',
|
'section',
|
||||||
'article',
|
'article',
|
||||||
@@ -518,6 +530,29 @@ function hasMeaningfulTableValue(value = '') {
|
|||||||
return Boolean(text && text !== '-')
|
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 = []) {
|
function isDocumentRecordTable(normalizedHeader = []) {
|
||||||
return (
|
return (
|
||||||
normalizedHeader.includes('单据编号') &&
|
normalizedHeader.includes('单据编号') &&
|
||||||
@@ -526,15 +561,32 @@ function isDocumentRecordTable(normalizedHeader = []) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRecordMeta(label = '', value = '') {
|
function renderDocumentCardField(label = '', value = '', options = {}) {
|
||||||
if (!hasMeaningfulTableValue(value)) {
|
if (!hasMeaningfulTableValue(value)) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
const valueClass = options.valueClass ? ` ${options.valueClass}` : ''
|
||||||
return [
|
return [
|
||||||
'<span class="ai-html-record-meta-item">',
|
`<div class="ai-document-card__field${options.fieldClass ? ` ${options.fieldClass}` : ''}">`,
|
||||||
`<small>${escapeHtml(label)}</small>`,
|
`<span class="ai-document-card__label">${escapeHtml(label)}</span>`,
|
||||||
`<b>${renderInlineHtml(value)}</b>`,
|
`<strong class="ai-document-card__value${valueClass}">${renderInlineHtml(value)}</strong>`,
|
||||||
'</span>'
|
'</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('')
|
].join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,32 +595,50 @@ function renderDocumentRecordList(header = [], bodyRows = []) {
|
|||||||
const items = bodyRows.map((row) => {
|
const items = bodyRows.map((row) => {
|
||||||
const documentType = resolveTableCell(row, normalizedHeader, ['单据类型'])
|
const documentType = resolveTableCell(row, normalizedHeader, ['单据类型'])
|
||||||
const documentNo = resolveTableCell(row, normalizedHeader, ['单据编号'])
|
const documentNo = resolveTableCell(row, normalizedHeader, ['单据编号'])
|
||||||
const applyTime = resolveTableCell(row, normalizedHeader, ['申请时间'])
|
const applyTime = resolveTableCell(row, normalizedHeader, ['申请时间', '日期', '时间'])
|
||||||
const status = 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 stage = resolveTableCell(row, normalizedHeader, ['当前节点'])
|
||||||
const reason = resolveTableCell(row, normalizedHeader, ['事由'])
|
const reason = resolveTableCell(row, normalizedHeader, ['事由'])
|
||||||
const action = 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 [
|
return [
|
||||||
'<article class="ai-html-record-item" role="listitem">',
|
`<article class="ai-document-card ${tone}" role="listitem" aria-label="单据详情">`,
|
||||||
'<div class="ai-html-record-main">',
|
'<header class="ai-document-card__head">',
|
||||||
hasMeaningfulTableValue(documentType) ? `<span class="ai-html-record-kicker">${renderInlineHtml(documentType)}</span>` : '',
|
`<strong class="ai-document-card__reason">${renderInlineHtml(title)}</strong>`,
|
||||||
hasMeaningfulTableValue(documentNo) ? `<strong class="ai-html-record-id">${renderInlineHtml(documentNo)}</strong>` : '',
|
hasMeaningfulTableValue(status) ? `<span class="ai-document-card__status">${renderInlineHtml(status)}</span>` : '',
|
||||||
hasMeaningfulTableValue(reason) ? `<p class="ai-html-record-reason">${renderInlineHtml(reason)}</p>` : '',
|
'</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>',
|
||||||
'<div class="ai-html-record-meta">',
|
|
||||||
renderRecordMeta('申请时间', applyTime),
|
|
||||||
renderRecordMeta('状态', status),
|
|
||||||
renderRecordMeta('当前节点', stage),
|
|
||||||
'</div>',
|
'</div>',
|
||||||
hasMeaningfulTableValue(action) ? `<div class="ai-html-record-action">${renderInlineHtml(action)}</div>` : '',
|
|
||||||
'</article>'
|
'</article>'
|
||||||
].join('')
|
].join('')
|
||||||
}).filter(Boolean)
|
}).filter(Boolean)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'<div class="ai-html-record-list" role="list">',
|
'<section class="ai-document-card-list" role="list" aria-label="单据结果">',
|
||||||
...items,
|
...items,
|
||||||
'</div>'
|
'</section>'
|
||||||
].join('')
|
].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 { extractExpenseClaimItems } from '../services/reimbursements.js'
|
||||||
|
import { buildAiDocumentDetailHref } from './aiDocumentDetailReference.js'
|
||||||
import { isApplicationDocumentNo } from './documentClassification.js'
|
import { isApplicationDocumentNo } from './documentClassification.js'
|
||||||
|
|
||||||
const DOCUMENT_QUERY_LIMIT = 8
|
const DOCUMENT_QUERY_LIMIT = 8
|
||||||
@@ -66,6 +67,14 @@ function normalizeText(value) {
|
|||||||
return String(value ?? '').trim()
|
return String(value ?? '').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveStatusDisplayLabel(value = '') {
|
||||||
|
const text = normalizeText(value)
|
||||||
|
if (!text) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return STATUS_LABELS[text.toLowerCase()] || text
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(value = '') {
|
function escapeHtml(value = '') {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@@ -279,9 +288,15 @@ function resolveSource(prompt) {
|
|||||||
sourceLabel: '待我审核的单据'
|
sourceLabel: '待我审核的单据'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (/我名下|我发起|我提交|我创建|我的申请|我的报销/.test(text)) {
|
||||||
|
return {
|
||||||
|
source: 'mine',
|
||||||
|
sourceLabel: '我的单据'
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
source: 'mine',
|
source: 'accessible',
|
||||||
sourceLabel: '我的单据'
|
sourceLabel: '我可见的单据'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,6 +338,36 @@ function resolveClaimId(claim = {}) {
|
|||||||
return normalizeText(claim.id || claim.claim_id || claim.claimId || resolveDocumentNo(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 = {}) {
|
function resolveDocumentTypeCode(claim = {}) {
|
||||||
const explicitType = normalizeText(
|
const explicitType = normalizeText(
|
||||||
claim.document_type_code
|
claim.document_type_code
|
||||||
@@ -346,7 +391,10 @@ function resolveDocumentTypeCode(claim = {}) {
|
|||||||
|
|
||||||
function resolveStatusLabel(claim = {}) {
|
function resolveStatusLabel(claim = {}) {
|
||||||
const key = normalizeText(claim.status || claim.state || claim.approval_status || claim.approvalStatus).toLowerCase()
|
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()
|
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 = {}) {
|
function resolveReason(claim = {}) {
|
||||||
return normalizeText(claim.reason || claim.business_reason || claim.description || claim.title || claim.note) || '未填写事由'
|
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
|
return date ? date.getTime() : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeRecord(claim = {}) {
|
function normalizeRecord(claim = {}, intent = {}) {
|
||||||
const documentType = resolveDocumentTypeCode(claim)
|
const documentType = resolveDocumentTypeCode(claim)
|
||||||
const documentNo = resolveDocumentNo(claim)
|
const documentNo = resolveDocumentNo(claim)
|
||||||
const date = resolveRecordDate(claim)
|
const date = resolveRecordDate(claim)
|
||||||
@@ -577,7 +647,13 @@ function normalizeRecord(claim = {}) {
|
|||||||
const reason = resolveReason(claim)
|
const reason = resolveReason(claim)
|
||||||
const expenseTypeCode = resolveExpenseTypeCode(claim)
|
const expenseTypeCode = resolveExpenseTypeCode(claim)
|
||||||
const typeLabel = resolveExpenseTypeLabel(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 ownerLabel = resolveOwnerLabel(claim)
|
||||||
const departmentLabel = resolveDepartmentLabel(claim)
|
const departmentLabel = resolveDepartmentLabel(claim)
|
||||||
const locationLabel = resolveLocationLabel(claim)
|
const locationLabel = resolveLocationLabel(claim)
|
||||||
@@ -593,9 +669,11 @@ function normalizeRecord(claim = {}) {
|
|||||||
time: resolveTimeLabel(claim, date),
|
time: resolveTimeLabel(claim, date),
|
||||||
dateKey: date,
|
dateKey: date,
|
||||||
updatedTime: updatedDate || '未显示',
|
updatedTime: updatedDate || '未显示',
|
||||||
statusKey: resolveStatusKey(claim),
|
statusKey,
|
||||||
statusLabel,
|
statusLabel,
|
||||||
statusTone: resolveStatusTone(statusLabel),
|
statusTone: resolveStatusTone(statusLabel),
|
||||||
|
querySource,
|
||||||
|
isApprovalTask,
|
||||||
reason,
|
reason,
|
||||||
amountLabel: resolveAmountLabel(claim),
|
amountLabel: resolveAmountLabel(claim),
|
||||||
amountValue: resolveAmountValue(claim),
|
amountValue: resolveAmountValue(claim),
|
||||||
@@ -653,7 +731,7 @@ function matchesAmountFilter(record = {}, amountFilter = null) {
|
|||||||
|
|
||||||
export function filterAiDocumentQueryRecords(claimsPayload, intent = {}) {
|
export function filterAiDocumentQueryRecords(claimsPayload, intent = {}) {
|
||||||
const rows = extractExpenseClaimItems(claimsPayload)
|
const rows = extractExpenseClaimItems(claimsPayload)
|
||||||
.map((claim) => normalizeRecord(claim))
|
.map((claim) => normalizeRecord(claim, intent))
|
||||||
.filter((record) => (
|
.filter((record) => (
|
||||||
!intent?.documentType ||
|
!intent?.documentType ||
|
||||||
intent.documentType === 'all' ||
|
intent.documentType === 'all' ||
|
||||||
@@ -669,50 +747,62 @@ export function filterAiDocumentQueryRecords(claimsPayload, intent = {}) {
|
|||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDocumentDetailHref(record = {}) {
|
function buildDocumentCardFieldHtml(label = '', value = '', options = {}) {
|
||||||
const reference = normalizeText(record.documentNo || record.claimNo || record.claimId || record.id)
|
const text = normalizeText(value)
|
||||||
return reference ? `#ai-open-document-detail:${encodeURIComponent(reference)}` : ''
|
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 = {}) {
|
function buildDocumentCardHtml(record = {}) {
|
||||||
const href = buildDocumentDetailHref(record)
|
const href = buildAiDocumentDetailHref(record)
|
||||||
const typeClass = record.documentType === 'application' ? 'application' : 'reimbursement'
|
const typeClass = record.documentType === 'application' ? 'application' : 'reimbursement'
|
||||||
|
const approvalTaskClass = record.isApprovalTask ? ' ai-document-card--approval-task' : ''
|
||||||
const statusTone = record.statusTone || 'is-pending'
|
const statusTone = record.statusTone || 'is-pending'
|
||||||
const amountLabel = record.documentType === 'application' ? '预计金额' : '报销金额'
|
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]
|
const ownerText = [record.ownerLabel, record.departmentLabel]
|
||||||
.filter((item) => item && item !== '未显示')
|
.filter((item) => item && item !== '未显示')
|
||||||
.join(' · ') || '未显示'
|
.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 [
|
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">',
|
'<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>`,
|
`<span class="ai-document-card__status">${escapeHtml(record.statusLabel)}</span>`,
|
||||||
'</header>',
|
'</header>',
|
||||||
'<div class="ai-document-card__body">',
|
'<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__details">',
|
||||||
'<div class="ai-document-card__field">',
|
detailsHtml,
|
||||||
'<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>',
|
|
||||||
'</div>',
|
'</div>',
|
||||||
'</div>',
|
'</div>',
|
||||||
'</article>'
|
'</article>'
|
||||||
|
|||||||
@@ -623,6 +623,8 @@ function stripKnownContextFromReason(value, context = {}) {
|
|||||||
.replace(/(?:地点|业务地点|发生地点|目的地)\s*[::]\s*(?=[,,、。;;\s]|$)/gu, '')
|
.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}\s*(?:至|到|~|—|–|--)\s*20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/gu, '')
|
||||||
.replace(/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(/(?:出差|申请)?\d+\s*天/gu, '')
|
||||||
.replace(/(?:用户预估费用|预估费用|预计总费用|预计费用|预计金额|申请金额|预算|金额|费用)\s*[::]?\s*\d+(?:\.\d+)?\s*(?:元|块|人民币)?/gu, '')
|
.replace(/(?:用户预估费用|预估费用|预计总费用|预计费用|预计金额|申请金额|预算|金额|费用)\s*[::]?\s*\d+(?:\.\d+)?\s*(?:元|块|人民币)?/gu, '')
|
||||||
.replace(/(?:高铁|动车|火车|铁路|飞机|机票|航班|轮船|船票|客轮|渡轮|出租车|的士|网约车|打车|自驾)/gu, '')
|
.replace(/(?:高铁|动车|火车|铁路|飞机|机票|航班|轮船|船票|客轮|渡轮|出租车|的士|网约车|打车|自驾)/gu, '')
|
||||||
|
|||||||
22
web/src/utils/requestProgressViewer.js
Normal file
22
web/src/utils/requestProgressViewer.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export function resolveProgressStepsForViewer(steps = [], options = {}) {
|
||||||
|
const safeSteps = Array.isArray(steps) ? steps : []
|
||||||
|
if (!options.isApplicationDocument || !options.isCurrentDirectManagerApprover) {
|
||||||
|
return safeSteps
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeSteps.map((step) => {
|
||||||
|
if (!step?.current || step?.rawLabel !== '直属领导审批') {
|
||||||
|
return step
|
||||||
|
}
|
||||||
|
const nextLabel = '等待批复'
|
||||||
|
const currentLabel = String(step.label || '').trim()
|
||||||
|
const title = String(step.title || '').trim()
|
||||||
|
return {
|
||||||
|
...step,
|
||||||
|
label: nextLabel,
|
||||||
|
title: currentLabel && title.includes(currentLabel)
|
||||||
|
? title.replace(currentLabel, nextLabel)
|
||||||
|
: title
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -258,6 +258,7 @@ import TravelRequestDetailView from './TravelRequestDetailView.vue'
|
|||||||
import { useAppShell } from '../composables/useAppShell.js'
|
import { useAppShell } from '../composables/useAppShell.js'
|
||||||
import { useSystemState } from '../composables/useSystemState.js'
|
import { useSystemState } from '../composables/useSystemState.js'
|
||||||
import { filterNavItemsByAccess, isPlatformAdminUser } from '../utils/accessControl.js'
|
import { filterNavItemsByAccess, isPlatformAdminUser } from '../utils/accessControl.js'
|
||||||
|
import { isBusinessDocumentReference } from '../utils/aiDocumentDetailReference.js'
|
||||||
import { loadAiWorkbenchConversationHistory, saveAiWorkbenchConversation } from '../utils/aiWorkbenchConversationStore.js'
|
import { loadAiWorkbenchConversationHistory, saveAiWorkbenchConversation } from '../utils/aiWorkbenchConversationStore.js'
|
||||||
|
|
||||||
const employeeSummary = ref(null)
|
const employeeSummary = ref(null)
|
||||||
@@ -346,6 +347,7 @@ const {
|
|||||||
openTravelCreate,
|
openTravelCreate,
|
||||||
ranges,
|
ranges,
|
||||||
requestSummary,
|
requestSummary,
|
||||||
|
workbenchRequests,
|
||||||
workbenchSummary,
|
workbenchSummary,
|
||||||
requestsError,
|
requestsError,
|
||||||
requestsLoading,
|
requestsLoading,
|
||||||
@@ -411,12 +413,24 @@ const resolvedDetailKpis = computed(() => (
|
|||||||
))
|
))
|
||||||
|
|
||||||
function openWorkbenchDocument(payload = {}) {
|
function openWorkbenchDocument(payload = {}) {
|
||||||
const requestId = String(payload.claimId || payload.id || payload.claimNo || payload.documentNo || '').trim()
|
const payloadClaimId = String(payload.claimId || payload.claim_id || '').trim()
|
||||||
|
const payloadId = String(payload.id || '').trim()
|
||||||
|
const payloadClaimNo = String(
|
||||||
|
payload.claimNo ||
|
||||||
|
payload.claim_no ||
|
||||||
|
payload.documentNo ||
|
||||||
|
payload.document_no ||
|
||||||
|
''
|
||||||
|
).trim()
|
||||||
|
const requestId = String(payloadClaimId || payloadId || payloadClaimNo).trim()
|
||||||
if (!requestId) {
|
if (!requestId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = requests.value.find((item) => (
|
const requestCandidates = Array.isArray(workbenchRequests.value) && workbenchRequests.value.length
|
||||||
|
? workbenchRequests.value
|
||||||
|
: requests.value
|
||||||
|
const request = requestCandidates.find((item) => (
|
||||||
String(item.claimId || '').trim() === requestId
|
String(item.claimId || '').trim() === requestId
|
||||||
|| String(item.id || '').trim() === requestId
|
|| String(item.id || '').trim() === requestId
|
||||||
|| String(item.claimNo || '').trim() === requestId
|
|| String(item.claimNo || '').trim() === requestId
|
||||||
@@ -429,12 +443,15 @@ function openWorkbenchDocument(payload = {}) {
|
|||||||
)
|
)
|
||||||
? 'workbench'
|
? 'workbench'
|
||||||
: ''
|
: ''
|
||||||
|
const payloadIdIsBusinessNo = isBusinessDocumentReference(payloadId)
|
||||||
|
const fallbackClaimId = payloadClaimId || (payloadClaimNo || payloadIdIsBusinessNo ? '' : payloadId || requestId)
|
||||||
|
const fallbackClaimNo = payloadClaimNo || (payloadIdIsBusinessNo ? payloadId : fallbackClaimId ? '' : requestId)
|
||||||
const detailPayload = request || {
|
const detailPayload = request || {
|
||||||
...payload,
|
...payload,
|
||||||
id: payload.id || requestId,
|
id: payloadId || fallbackClaimId || fallbackClaimNo || requestId,
|
||||||
claimId: payload.claimId || requestId,
|
claimId: fallbackClaimId,
|
||||||
claimNo: payload.claimNo || payload.documentNo || requestId,
|
claimNo: fallbackClaimNo,
|
||||||
documentNo: payload.documentNo || requestId,
|
documentNo: String(payload.documentNo || payload.document_no || fallbackClaimNo || requestId).trim(),
|
||||||
detailLookupOnly: true
|
detailLookupOnly: true
|
||||||
}
|
}
|
||||||
openRequestDetail(detailPayload, { returnTo })
|
openRequestDetail(detailPayload, { returnTo })
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
buildApplicationDetailFactItems,
|
buildApplicationDetailFactItems,
|
||||||
buildRelatedApplicationFactItems
|
buildRelatedApplicationFactItems
|
||||||
} from '../../utils/expenseApplicationDetail.js'
|
} from '../../utils/expenseApplicationDetail.js'
|
||||||
|
import { resolveProgressStepsForViewer } from '../../utils/requestProgressViewer.js'
|
||||||
import { isArchivedRequestView, normalizeRequestForUi } from '../../utils/requestViewModel.js'
|
import { isArchivedRequestView, normalizeRequestForUi } from '../../utils/requestViewModel.js'
|
||||||
import {
|
import {
|
||||||
buildAiAdviceViewModel,
|
buildAiAdviceViewModel,
|
||||||
@@ -1027,11 +1028,15 @@ export default {
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
const progressSteps = computed(() =>
|
const progressSteps = computed(() => {
|
||||||
Array.isArray(request.value.progressSteps) && request.value.progressSteps.length
|
const sourceSteps = Array.isArray(request.value.progressSteps) && request.value.progressSteps.length
|
||||||
? request.value.progressSteps
|
? request.value.progressSteps
|
||||||
: buildFallbackProgressSteps(request.value)
|
: buildFallbackProgressSteps(request.value)
|
||||||
)
|
return resolveProgressStepsForViewer(sourceSteps, {
|
||||||
|
isApplicationDocument: isApplicationDocument.value,
|
||||||
|
isCurrentDirectManagerApprover: isCurrentDirectManagerApprover.value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const currentProgressRingMotion = {
|
const currentProgressRingMotion = {
|
||||||
initial: {
|
initial: {
|
||||||
|
|||||||
@@ -47,18 +47,30 @@ test('AI conversation renderer supports tables and escapes unsafe HTML', () => {
|
|||||||
|
|
||||||
test('AI conversation renderer renders application detail action links as buttons', () => {
|
test('AI conversation renderer renders application detail action links as buttons', () => {
|
||||||
const rendered = renderAiConversationHtml([
|
const rendered = renderAiConversationHtml([
|
||||||
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 操作 |',
|
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 日期 | 地点 | 事由 | 操作 |',
|
||||||
'| --- | --- | --- | --- | --- |',
|
'| --- | --- | --- | --- | --- | --- | --- | --- |',
|
||||||
'| 出差申请 | AP-OVERLAP | 草稿 | 待提交 | [查看](#ai-open-application-detail:AP-OVERLAP) |'
|
'| 出差申请 | AP-OVERLAP | submitted | 直属领导审批 | 2026-02-20 至 2026-02-23 | 上海 | 辅助国网仿生产服务器部署 | [查看](#ai-open-application-detail:AP-OVERLAP) |'
|
||||||
].join('\n'))
|
].join('\n'))
|
||||||
|
|
||||||
assert.match(rendered, /<div class="ai-html-record-list" role="list">/)
|
assert.match(rendered, /<section class="ai-document-card-list" role="list" aria-label="单据结果">/)
|
||||||
assert.match(rendered, /<article class="ai-html-record-item" role="listitem">/)
|
assert.match(rendered, /<article class="ai-document-card is-pending" role="listitem" aria-label="单据详情">/)
|
||||||
assert.match(rendered, /<strong class="ai-html-record-id">AP-OVERLAP<\/strong>/)
|
assert.match(rendered, /<strong class="ai-document-card__reason">出差申请<\/strong>/)
|
||||||
assert.match(rendered, /class="ai-html-action-link ai-html-action-link-application"/)
|
assert.match(rendered, /<span class="ai-document-card__status">审批中<\/span>/)
|
||||||
|
assert.match(rendered, /<div class="ai-document-card__summary">/)
|
||||||
|
assert.match(rendered, /<span class="ai-document-card__label">日期<\/span>/)
|
||||||
|
assert.match(rendered, /2026-02-20 至 2026-02-23/)
|
||||||
|
assert.match(rendered, /<span class="ai-document-card__label">当前节点<\/span>/)
|
||||||
|
assert.match(rendered, /直属领导审批/)
|
||||||
|
assert.match(rendered, /<span class="ai-document-card__label">地点<\/span>/)
|
||||||
|
assert.match(rendered, /上海/)
|
||||||
|
assert.match(rendered, /<span class="ai-document-card__label">事由<\/span>/)
|
||||||
|
assert.match(rendered, /辅助国网仿生产服务器部署/)
|
||||||
|
assert.match(rendered, /<strong class="ai-document-card__value ai-document-card__number">AP-OVERLAP<\/strong>/)
|
||||||
|
assert.match(rendered, /class="ai-html-action-link ai-document-card__action ai-html-action-link-application"/)
|
||||||
assert.match(rendered, /data-ai-action="open-application-detail"/)
|
assert.match(rendered, /data-ai-action="open-application-detail"/)
|
||||||
assert.match(rendered, /href="#ai-open-application-detail:AP-OVERLAP"/)
|
assert.match(rendered, /href="#ai-open-application-detail:AP-OVERLAP"/)
|
||||||
assert.doesNotMatch(rendered, /<table>/)
|
assert.doesNotMatch(rendered, /<table>/)
|
||||||
|
assert.doesNotMatch(rendered, /ai-html-record-item/)
|
||||||
assert.doesNotMatch(rendered, /target="_blank"[\s\S]{0,120}#ai-open-application-detail/)
|
assert.doesNotMatch(rendered, /target="_blank"[\s\S]{0,120}#ai-open-application-detail/)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -69,7 +81,7 @@ test('AI conversation renderer renders deleted application detail actions as dis
|
|||||||
'| 出差申请 | AP-20260620-DRAFT | 已删除 | 已删除 | [草稿已删除](#ai-deleted-application-detail:claim-draft-1) |'
|
'| 出差申请 | AP-20260620-DRAFT | 已删除 | 已删除 | [草稿已删除](#ai-deleted-application-detail:claim-draft-1) |'
|
||||||
].join('\n'))
|
].join('\n'))
|
||||||
|
|
||||||
assert.match(rendered, /class="ai-html-action-link ai-html-action-link-application is-disabled"/)
|
assert.match(rendered, /class="ai-html-action-link ai-document-card__action ai-html-action-link-application is-disabled"/)
|
||||||
assert.match(rendered, /aria-disabled="true"/)
|
assert.match(rendered, /aria-disabled="true"/)
|
||||||
assert.match(rendered, /data-ai-action="deleted-application-detail"/)
|
assert.match(rendered, /data-ai-action="deleted-application-detail"/)
|
||||||
assert.doesNotMatch(rendered, /href="#ai-deleted-application-detail/)
|
assert.doesNotMatch(rendered, /href="#ai-deleted-application-detail/)
|
||||||
@@ -82,12 +94,16 @@ test('AI conversation renderer turns application conflict tables into record lis
|
|||||||
'| AP-20260620063557-4JU2MWEF | 2026-02-20 至 2026-02-23 | 审批中 | 辅助国网仿生产服务器部署 | [查看](#ai-open-application-detail:AP-20260620063557-4JU2MWEF) |'
|
'| AP-20260620063557-4JU2MWEF | 2026-02-20 至 2026-02-23 | 审批中 | 辅助国网仿生产服务器部署 | [查看](#ai-open-application-detail:AP-20260620063557-4JU2MWEF) |'
|
||||||
].join('\n'))
|
].join('\n'))
|
||||||
|
|
||||||
assert.match(rendered, /<div class="ai-html-record-list" role="list">/)
|
assert.match(rendered, /<section class="ai-document-card-list" role="list" aria-label="单据结果">/)
|
||||||
assert.match(rendered, /申请时间/)
|
assert.match(rendered, /<article class="ai-document-card is-pending" role="listitem" aria-label="单据详情">/)
|
||||||
|
assert.match(rendered, /<div class="ai-document-card__summary">/)
|
||||||
|
assert.match(rendered, /<span class="ai-document-card__label">日期<\/span>/)
|
||||||
assert.match(rendered, /2026-02-20 至 2026-02-23/)
|
assert.match(rendered, /2026-02-20 至 2026-02-23/)
|
||||||
|
assert.match(rendered, /<span class="ai-document-card__label">当前节点<\/span>/)
|
||||||
assert.match(rendered, /辅助国网仿生产服务器部署/)
|
assert.match(rendered, /辅助国网仿生产服务器部署/)
|
||||||
assert.match(rendered, /<div class="ai-html-record-action">/)
|
assert.match(rendered, /ai-document-card__field--action/)
|
||||||
assert.doesNotMatch(rendered, /<table>/)
|
assert.doesNotMatch(rendered, /<table>/)
|
||||||
|
assert.doesNotMatch(rendered, /ai-html-record-item/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('AI conversation renderer renders document detail action links as buttons', () => {
|
test('AI conversation renderer renders document detail action links as buttons', () => {
|
||||||
|
|||||||
51
web/tests/ai-document-detail-reference.test.mjs
Normal file
51
web/tests/ai-document-detail-reference.test.mjs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildAiDocumentDetailRequest,
|
||||||
|
parseAiApplicationDetailHref,
|
||||||
|
parseAiDocumentDetailHref
|
||||||
|
} from '../src/utils/aiDocumentDetailReference.js'
|
||||||
|
|
||||||
|
test('AI detail request keeps business application number out of claimId for legacy links', () => {
|
||||||
|
const detailReference = parseAiApplicationDetailHref('#ai-open-application-detail:AP-202606200001-ABCDEFGH')
|
||||||
|
const request = buildAiDocumentDetailRequest(detailReference)
|
||||||
|
|
||||||
|
assert.deepEqual(detailReference, {
|
||||||
|
reference: 'AP-202606200001-ABCDEFGH',
|
||||||
|
documentType: 'application'
|
||||||
|
})
|
||||||
|
assert.equal(request.id, 'AP-202606200001-ABCDEFGH')
|
||||||
|
assert.equal(request.claimId, '')
|
||||||
|
assert.equal(request.claimNo, 'AP-202606200001-ABCDEFGH')
|
||||||
|
assert.equal(request.documentNo, 'AP-202606200001-ABCDEFGH')
|
||||||
|
assert.equal(request.documentTypeCode, 'application')
|
||||||
|
assert.equal(request.detailLookupOnly, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('AI detail request uses explicit claim_id as lookup identity', () => {
|
||||||
|
const detailReference = parseAiDocumentDetailHref(
|
||||||
|
'#ai-open-document-detail:claim_id%3Dapproval-1%26claim_no%3DAP-APPROVAL-001'
|
||||||
|
)
|
||||||
|
const request = buildAiDocumentDetailRequest(detailReference)
|
||||||
|
|
||||||
|
assert.deepEqual(detailReference, {
|
||||||
|
reference: 'AP-APPROVAL-001',
|
||||||
|
claimId: 'approval-1',
|
||||||
|
claimNo: 'AP-APPROVAL-001'
|
||||||
|
})
|
||||||
|
assert.equal(request.id, 'approval-1')
|
||||||
|
assert.equal(request.claimId, 'approval-1')
|
||||||
|
assert.equal(request.claimNo, 'AP-APPROVAL-001')
|
||||||
|
assert.equal(request.documentNo, 'AP-APPROVAL-001')
|
||||||
|
assert.equal(request.documentTypeCode, 'application')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('AI detail request treats non-number references as internal claim ids', () => {
|
||||||
|
const request = buildAiDocumentDetailRequest({ reference: 'approval-internal-id' })
|
||||||
|
|
||||||
|
assert.equal(request.id, 'approval-internal-id')
|
||||||
|
assert.equal(request.claimId, 'approval-internal-id')
|
||||||
|
assert.equal(request.claimNo, '')
|
||||||
|
assert.equal(request.documentNo, 'approval-internal-id')
|
||||||
|
})
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
buildAiDocumentQueryConditionSummary,
|
buildAiDocumentQueryConditionSummary,
|
||||||
buildAiDocumentQueryMessage,
|
buildAiDocumentQueryMessage,
|
||||||
filterAiDocumentQueryRecords,
|
filterAiDocumentQueryRecords,
|
||||||
|
mergeAiDocumentQueryPayloads,
|
||||||
resolveAiDocumentQueryIntent
|
resolveAiDocumentQueryIntent
|
||||||
} from '../src/utils/aiDocumentQueryModel.js'
|
} from '../src/utils/aiDocumentQueryModel.js'
|
||||||
import { renderAiConversationHtml } from '../src/utils/aiConversationHtmlRenderer.js'
|
import { renderAiConversationHtml } from '../src/utils/aiConversationHtmlRenderer.js'
|
||||||
@@ -55,9 +56,9 @@ const claims = [
|
|||||||
test('AI document query intent detects my document list questions', () => {
|
test('AI document query intent detects my document list questions', () => {
|
||||||
const intent = resolveAiDocumentQueryIntent('我现在有哪些单据?', { today })
|
const intent = resolveAiDocumentQueryIntent('我现在有哪些单据?', { today })
|
||||||
|
|
||||||
assert.equal(intent?.source, 'mine')
|
assert.equal(intent?.source, 'accessible')
|
||||||
assert.equal(intent?.documentType, 'all')
|
assert.equal(intent?.documentType, 'all')
|
||||||
assert.equal(intent?.sourceLabel, '我的单据')
|
assert.equal(intent?.sourceLabel, '我可见的单据')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('AI document query intent detects approval document questions', () => {
|
test('AI document query intent detects approval document questions', () => {
|
||||||
@@ -67,6 +68,38 @@ test('AI document query intent detects approval document questions', () => {
|
|||||||
assert.equal(intent?.sourceLabel, '待我审核的单据')
|
assert.equal(intent?.sourceLabel, '待我审核的单据')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('AI document query keeps explicit own-document scope separate from accessible documents', () => {
|
||||||
|
const intent = resolveAiDocumentQueryIntent('我名下有哪些单据?', { today })
|
||||||
|
|
||||||
|
assert.equal(intent?.source, 'mine')
|
||||||
|
assert.equal(intent?.sourceLabel, '我的单据')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('AI document query merges own and approval records for generic document list questions', () => {
|
||||||
|
const intent = resolveAiDocumentQueryIntent('我现在有哪些单据?', { today })
|
||||||
|
const approvalClaims = [{
|
||||||
|
id: 'approval-1',
|
||||||
|
claim_no: 'CL-APPROVAL-001',
|
||||||
|
document_type_code: 'reimbursement',
|
||||||
|
expense_type: 'meeting',
|
||||||
|
status: 'pending',
|
||||||
|
approval_stage: '直属领导审批',
|
||||||
|
reason: '待我审核的会议费',
|
||||||
|
employee_name: '李文静',
|
||||||
|
department_name: '市场部',
|
||||||
|
occurred_at: '2026-02-22T09:00:00Z',
|
||||||
|
amount: 880
|
||||||
|
}]
|
||||||
|
const merged = mergeAiDocumentQueryPayloads(claims, { items: approvalClaims, querySource: 'approval' }, [claims[0]])
|
||||||
|
const records = filterAiDocumentQueryRecords(merged, intent)
|
||||||
|
|
||||||
|
assert.equal(intent?.source, 'accessible')
|
||||||
|
assert.deepEqual(
|
||||||
|
records.map((record) => record.documentNo),
|
||||||
|
['CL-20260305001', 'CL-APPROVAL-001', 'CL-20260221001', 'AP-20260220001']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test('AI document query filters by month and document type', () => {
|
test('AI document query filters by month and document type', () => {
|
||||||
const intent = resolveAiDocumentQueryIntent('我2月有哪些申请单?', { today })
|
const intent = resolveAiDocumentQueryIntent('我2月有哪些申请单?', { today })
|
||||||
const records = filterAiDocumentQueryRecords(claims, intent)
|
const records = filterAiDocumentQueryRecords(claims, intent)
|
||||||
@@ -136,26 +169,64 @@ test('AI document query message renders html document cards with detail actions'
|
|||||||
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, /<div class="ai-document-card__summary">/)
|
||||||
assert.match(message, /<div class="ai-document-card__details">/)
|
assert.match(message, /<div class="ai-document-card__details">/)
|
||||||
|
assert.match(message, /<span class="ai-document-card__label">日期<\/span>/)
|
||||||
|
assert.match(message, /2026-02-20/)
|
||||||
|
assert.match(message, /<span class="ai-document-card__label">预计金额<\/span>/)
|
||||||
|
assert.match(message, /<strong class="ai-document-card__value ai-document-card__amount">¥3,000\.00<\/strong>/)
|
||||||
|
assert.doesNotMatch(message, /<span class="ai-document-card__label">当前节点<\/span>/)
|
||||||
assert.match(message, /<span class="ai-document-card__label">单据类型<\/span>/)
|
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">申请单 · 差旅费用申请<\/strong>/)
|
||||||
|
assert.match(message, /<span class="ai-document-card__label">地点<\/span>/)
|
||||||
|
assert.match(message, /上海/)
|
||||||
|
assert.match(message, /<span class="ai-document-card__label">事由<\/span>/)
|
||||||
|
assert.match(message, /辅助国网仿生产服务器部署/)
|
||||||
assert.match(message, /<span class="ai-document-card__label">申请人<\/span>/)
|
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">曹小筑 · 交付部<\/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__value ai-document-card__number">AP-20260220001<\/strong>/)
|
||||||
assert.match(message, /<strong class="ai-document-card__amount">¥3,000\.00<\/strong>/)
|
|
||||||
assert.match(message, /<div class="ai-document-card__field ai-document-card__field--action">/)
|
assert.match(message, /<div class="ai-document-card__field ai-document-card__field--action">/)
|
||||||
assert.doesNotMatch(message, /ai-document-card__meta/)
|
assert.doesNotMatch(message, /ai-document-card__meta/)
|
||||||
assert.doesNotMatch(message, /ai-document-card__meta-item/)
|
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:claim_id%3Dapp-1%26claim_no%3DAP-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, /<span class="ai-document-card__status">审批中<\/span>/)
|
||||||
|
assert.match(message, /href="#ai-open-document-detail:claim_id%3Dclaim-1%26claim_no%3DCL-20260221001"/)
|
||||||
|
assert.doesNotMatch(message, />submitted</)
|
||||||
assert.doesNotMatch(message, /\| 单据编号 \|/)
|
assert.doesNotMatch(message, /\| 单据编号 \|/)
|
||||||
assert.doesNotMatch(message, /^> /m)
|
assert.doesNotMatch(message, /^> /m)
|
||||||
assert.doesNotMatch(message, /\*\*查询范围\*\*/)
|
assert.doesNotMatch(message, /\*\*查询范围\*\*/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('AI document query highlights approval-task cards and shows pending review status', () => {
|
||||||
|
const intent = resolveAiDocumentQueryIntent('我有哪些审核单', { today })
|
||||||
|
const message = buildAiDocumentQueryMessage(intent, [{
|
||||||
|
id: 'approval-1',
|
||||||
|
claim_no: 'AP-APPROVAL-001',
|
||||||
|
document_type_code: 'application',
|
||||||
|
expense_type: 'travel_application',
|
||||||
|
status: 'submitted',
|
||||||
|
approval_stage: '直属领导审批',
|
||||||
|
reason: '参加相关残联会议',
|
||||||
|
employee_name: '曹笑竹',
|
||||||
|
department_name: '技术部',
|
||||||
|
location: '上海',
|
||||||
|
occurred_at: '2026-02-20T09:00:00Z',
|
||||||
|
amount: 2120
|
||||||
|
}])
|
||||||
|
|
||||||
|
assert.match(message, /ai-document-card--application ai-document-card--approval-task is-pending/)
|
||||||
|
assert.match(message, /<span class="ai-document-card__status">待审批<\/span>/)
|
||||||
|
assert.match(message, /<span class="ai-document-card__label">预计金额<\/span>/)
|
||||||
|
assert.match(message, /<strong class="ai-document-card__value ai-document-card__amount">¥2,120\.00<\/strong>/)
|
||||||
|
assert.doesNotMatch(message, /<span class="ai-document-card__label">当前节点<\/span>/)
|
||||||
|
assert.match(message, /<span class="ai-document-card__label">单据类型<\/span>/)
|
||||||
|
assert.match(message, /<strong class="ai-document-card__value">申请单 · 差旅费用申请<\/strong>/)
|
||||||
|
assert.match(message, /href="#ai-open-document-detail:claim_id%3Dapproval-1%26claim_no%3DAP-APPROVAL-001"/)
|
||||||
|
})
|
||||||
|
|
||||||
test('AI document query html cards render as trusted card markup', () => {
|
test('AI document query html cards render as trusted card markup', () => {
|
||||||
const intent = resolveAiDocumentQueryIntent('我2月有哪些单据?', { today })
|
const intent = resolveAiDocumentQueryIntent('我2月有哪些单据?', { today })
|
||||||
const rendered = renderAiConversationHtml(buildAiDocumentQueryMessage(intent, claims))
|
const rendered = renderAiConversationHtml(buildAiDocumentQueryMessage(intent, claims))
|
||||||
@@ -169,7 +240,7 @@ test('AI document query html cards render as trusted card markup', () => {
|
|||||||
assert.match(rendered, /class="ai-document-card__field"/)
|
assert.match(rendered, /class="ai-document-card__field"/)
|
||||||
assert.match(rendered, /class="ai-document-card__label"/)
|
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:claim_id%3Dclaim-1%26claim_no%3DCL-20260221001"/)
|
||||||
assert.doesNotMatch(rendered, /ai-document-card__meta/)
|
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>/)
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ test('workbench summary merges approval inbox requests without polluting documen
|
|||||||
assert.match(appShellComposable, /const workbenchRequests = computed\(\(\) =>[\s\S]*mergeWorkbenchRequests\(requests\.value, workbenchApprovalRequests\.value\)/)
|
assert.match(appShellComposable, /const workbenchRequests = computed\(\(\) =>[\s\S]*mergeWorkbenchRequests\(requests\.value, workbenchApprovalRequests\.value\)/)
|
||||||
assert.match(appShellComposable, /buildWorkbenchSummary\(workbenchRequests\.value, currentUser\.value\)/)
|
assert.match(appShellComposable, /buildWorkbenchSummary\(workbenchRequests\.value, currentUser\.value\)/)
|
||||||
assert.match(appShellRouteView, /<DocumentsCenterView[\s\S]*:filtered-requests="requests"/)
|
assert.match(appShellRouteView, /<DocumentsCenterView[\s\S]*:filtered-requests="requests"/)
|
||||||
assert.doesNotMatch(appShellRouteView, /<DocumentsCenterView[\s\S]*workbenchRequests/)
|
assert.doesNotMatch(appShellRouteView, /<DocumentsCenterView(?:(?!\/>)[\s\S])*workbenchRequests/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('workbench progress refreshes after homepage create or detail updates', () => {
|
test('workbench progress refreshes after homepage create or detail updates', () => {
|
||||||
|
|||||||
@@ -98,9 +98,9 @@ test('deleting an application draft marks AI workbench detail links as unavailab
|
|||||||
content: [
|
content: [
|
||||||
'### 申请草稿已保存',
|
'### 申请草稿已保存',
|
||||||
'',
|
'',
|
||||||
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 操作 |',
|
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 日期 | 地点 | 事由 | 金额 | 操作 |',
|
||||||
'| --- | --- | --- | --- | --- |',
|
'| --- | --- | --- | --- | --- | --- | --- | --- | --- |',
|
||||||
'| 出差申请 | AP-20260620-DRAFT | 草稿 | 待提交 | [查看](#ai-open-application-detail:claim_id%3Dclaim-draft-1%26claim_no%3DAP-20260620-DRAFT) |'
|
'| 出差申请 | AP-20260620-DRAFT | 草稿 | 待提交 | 2026-02-20 至 2026-02-23 | 上海 | 辅助国网仿生产服务器部署 | ¥2,600.00 | [查看](#ai-open-application-detail:claim_id%3Dclaim-draft-1%26claim_no%3DAP-20260620-DRAFT) |'
|
||||||
].join('\n')
|
].join('\n')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ const documentListSharedStyles = readFileSync(
|
|||||||
fileURLToPath(new URL('../src/assets/styles/components/document-list-shared.css', import.meta.url)),
|
fileURLToPath(new URL('../src/assets/styles/components/document-list-shared.css', import.meta.url)),
|
||||||
'utf8'
|
'utf8'
|
||||||
)
|
)
|
||||||
|
const tableLoadingState = readFileSync(
|
||||||
|
fileURLToPath(new URL('../src/components/shared/TableLoadingState.vue', import.meta.url)),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
const reimbursementService = readFileSync(
|
const reimbursementService = readFileSync(
|
||||||
fileURLToPath(new URL('../src/services/reimbursements.js', import.meta.url)),
|
fileURLToPath(new URL('../src/services/reimbursements.js', import.meta.url)),
|
||||||
'utf8'
|
'utf8'
|
||||||
@@ -38,6 +42,16 @@ test('documents center keeps only the top scope tabs and renders risk level as a
|
|||||||
assert.match(documentsCenterView, /aria-label="风险等级"/)
|
assert.match(documentsCenterView, /aria-label="风险等级"/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('documents center loading state uses a compact spinner instead of light band progress', () => {
|
||||||
|
assert.match(documentsCenterView, /<TableLoadingState[\s\S]*title="单据数据同步中"[\s\S]*floating/)
|
||||||
|
assert.match(tableLoadingState, /class="table-loading-spinner"/)
|
||||||
|
assert.match(tableLoadingState, /@keyframes table-loading-spin/)
|
||||||
|
assert.match(tableLoadingState, /\.table-loading-card \{[\s\S]*width: min\(420px, 100%\);[\s\S]*display: inline-flex;/)
|
||||||
|
assert.match(tableLoadingState, /\.table-loading-spinner \{[\s\S]*border-top-color: var\(--theme-primary-active/)
|
||||||
|
assert.match(tableLoadingState, /@media \(prefers-reduced-motion: reduce\) \{[\s\S]*animation: none;/)
|
||||||
|
assert.doesNotMatch(tableLoadingState, /FloatingLightBandWindow/)
|
||||||
|
})
|
||||||
|
|
||||||
test('documents center top tabs start from all and show document category labels', () => {
|
test('documents center top tabs start from all and show document category labels', () => {
|
||||||
assert.match(documentsCenterView, /const DOCUMENT_SCOPE_ALL = '全部'/)
|
assert.match(documentsCenterView, /const DOCUMENT_SCOPE_ALL = '全部'/)
|
||||||
assert.match(documentsCenterView, /const DOCUMENT_SCOPE_APPLICATION = '申请单'/)
|
assert.match(documentsCenterView, /const DOCUMENT_SCOPE_APPLICATION = '申请单'/)
|
||||||
|
|||||||
26
web/tests/expense-application-preview-reason.test.mjs
Normal file
26
web/tests/expense-application-preview-reason.test.mjs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildLocalApplicationPreview
|
||||||
|
} from '../src/utils/expenseApplicationPreview.js'
|
||||||
|
|
||||||
|
test('application preview keeps compact travel meeting reason without date or location prefix', () => {
|
||||||
|
const preview = buildLocalApplicationPreview(
|
||||||
|
'2月20-23日去上海出差参加相关残联会议',
|
||||||
|
{
|
||||||
|
name: '曹笑竹',
|
||||||
|
departmentName: '技术部',
|
||||||
|
position: '财务智能化产品经理',
|
||||||
|
managerName: '向万红',
|
||||||
|
grade: 'P5'
|
||||||
|
},
|
||||||
|
{ today: '2026-06-09' }
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(preview.fields.time, '2026-02-20 至 2026-02-23')
|
||||||
|
assert.equal(preview.fields.days, '4天')
|
||||||
|
assert.equal(preview.fields.location, '上海')
|
||||||
|
assert.equal(preview.fields.reason, '参加相关残联会议')
|
||||||
|
assert.doesNotMatch(preview.fields.reason, /2月20|23日|上海|出差/)
|
||||||
|
})
|
||||||
49
web/tests/request-progress-viewer.test.mjs
Normal file
49
web/tests/request-progress-viewer.test.mjs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import { resolveProgressStepsForViewer } from '../src/utils/requestProgressViewer.js'
|
||||||
|
|
||||||
|
test('progress viewer keeps approver name for applicant view', () => {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
label: '等待 李经理 批复',
|
||||||
|
rawLabel: '直属领导审批',
|
||||||
|
current: true,
|
||||||
|
title: '当前等待 李经理 批复已停留 3小时15分钟'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveProgressStepsForViewer(steps, {
|
||||||
|
isApplicationDocument: true,
|
||||||
|
isCurrentDirectManagerApprover: false
|
||||||
|
}),
|
||||||
|
steps
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('progress viewer hides approver name for current direct manager approval view', () => {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
label: '等待 李经理 批复',
|
||||||
|
rawLabel: '直属领导审批',
|
||||||
|
current: true,
|
||||||
|
title: '当前等待 李经理 批复已停留 3小时15分钟'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
resolveProgressStepsForViewer(steps, {
|
||||||
|
isApplicationDocument: true,
|
||||||
|
isCurrentDirectManagerApprover: true
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: '等待批复',
|
||||||
|
rawLabel: '直属领导审批',
|
||||||
|
current: true,
|
||||||
|
title: '当前等待批复已停留 3小时15分钟'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -19,7 +19,7 @@ const WAIT_SUBMIT = '\u5f85\u63d0\u4ea4'
|
|||||||
const LINKED_APPLICATION = '\u5173\u8054\u5355\u636e'
|
const LINKED_APPLICATION = '\u5173\u8054\u5355\u636e'
|
||||||
const PAID = '\u5df2\u4ed8\u6b3e'
|
const PAID = '\u5df2\u4ed8\u6b3e'
|
||||||
const ARCHIVED = '\u5df2\u5f52\u6863'
|
const ARCHIVED = '\u5df2\u5f52\u6863'
|
||||||
const WAIT_LEADER_LI_APPROVAL = '\u7b49\u5f85 Leader Li \u6279\u590d'
|
const WAIT_APPROVAL = '\u7b49\u5f85\u6279\u590d'
|
||||||
const WAIT_BUDGET_ZHAO_APPROVAL = '\u7b49\u5f85 \u8d75\u9884\u7b97 \u6279\u590d'
|
const WAIT_BUDGET_ZHAO_APPROVAL = '\u7b49\u5f85 \u8d75\u9884\u7b97 \u6279\u590d'
|
||||||
const WAIT_BUDGET_P8_EXECUTIVE_APPROVAL = '\u7b49\u5f85 P8 Executive \u6279\u590d'
|
const WAIT_BUDGET_P8_EXECUTIVE_APPROVAL = '\u7b49\u5f85 P8 Executive \u6279\u590d'
|
||||||
const WAIT_FINANCE_FIONA_APPROVAL = '\u7b49\u5f85 Fiona Finance \u6279\u590d'
|
const WAIT_FINANCE_FIONA_APPROVAL = '\u7b49\u5f85 Fiona Finance \u6279\u590d'
|
||||||
@@ -160,14 +160,14 @@ test('application claims are mapped as application documents', () => {
|
|||||||
assert.equal(request.expenseTableSummary, '预计金额已随申请提交')
|
assert.equal(request.expenseTableSummary, '预计金额已随申请提交')
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
request.progressSteps.map((step) => step.label),
|
request.progressSteps.map((step) => step.label),
|
||||||
[CREATE_APPLICATION, WAIT_LEADER_LI_APPROVAL, APPLICATION_LINK_STATUS, ARCHIVED]
|
[CREATE_APPLICATION, '等待 Leader Li 批复', APPLICATION_LINK_STATUS, ARCHIVED]
|
||||||
)
|
)
|
||||||
assert.equal(request.progressSteps.some((step) => step.label === 'AI预审'), false)
|
assert.equal(request.progressSteps.some((step) => step.label === 'AI预审'), false)
|
||||||
assert.equal(request.progressSteps.some((step) => step.label === '财务审批'), false)
|
assert.equal(request.progressSteps.some((step) => step.label === '财务审批'), false)
|
||||||
assert.equal(request.progressSteps.some((step) => step.label === DIRECT_MANAGER_APPROVAL), false)
|
assert.equal(request.progressSteps.some((step) => step.label === DIRECT_MANAGER_APPROVAL), false)
|
||||||
assert.equal(request.progressSteps.some((step) => step.label === BUDGET_MANAGER_APPROVAL), false)
|
assert.equal(request.progressSteps.some((step) => step.label === BUDGET_MANAGER_APPROVAL), false)
|
||||||
assert.equal(request.progressSteps.find((step) => step.label === WAIT_LEADER_LI_APPROVAL)?.rawLabel, DIRECT_MANAGER_APPROVAL)
|
assert.equal(request.progressSteps.find((step) => step.label === '等待 Leader Li 批复')?.rawLabel, DIRECT_MANAGER_APPROVAL)
|
||||||
assert.equal(request.progressSteps.find((step) => step.label === WAIT_LEADER_LI_APPROVAL)?.current, true)
|
assert.equal(request.progressSteps.find((step) => step.label === '等待 Leader Li 批复')?.current, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('travel application detail splits trip time into departure and return rows', () => {
|
test('travel application detail splits trip time into departure and return rows', () => {
|
||||||
@@ -1241,7 +1241,8 @@ test('current direct manager step shows how long the claim has stayed there', ()
|
|||||||
|
|
||||||
assert.equal(submitStep.time, '王五提交')
|
assert.equal(submitStep.time, '王五提交')
|
||||||
assert.match(submitStep.detail, /2026-05-20/)
|
assert.match(submitStep.detail, /2026-05-20/)
|
||||||
assert.equal(leaderStep.label, '等待 李经理 批复')
|
assert.equal(leaderStep.label, '等待批复')
|
||||||
|
assert.doesNotMatch(leaderStep.label, /李经理/)
|
||||||
assert.equal(leaderStep.rawLabel, '直属领导审批')
|
assert.equal(leaderStep.rawLabel, '直属领导审批')
|
||||||
assert.equal(leaderStep.current, true)
|
assert.equal(leaderStep.current, true)
|
||||||
assert.equal(leaderStep.time, '停留 3小时15分钟')
|
assert.equal(leaderStep.time, '停留 3小时15分钟')
|
||||||
|
|||||||
@@ -146,6 +146,8 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
|
|||||||
assert.match(detailTemplate, /v-model:risk-confirmed="approvalRiskConfirmed"/)
|
assert.match(detailTemplate, /v-model:risk-confirmed="approvalRiskConfirmed"/)
|
||||||
assert.match(detailTemplate, /:risk-confirm-items="approvalRiskConfirmItems"/)
|
assert.match(detailTemplate, /:risk-confirm-items="approvalRiskConfirmItems"/)
|
||||||
assert.doesNotMatch(detailTemplate, /:next-stage="approvalNextStage"/)
|
assert.doesNotMatch(detailTemplate, /:next-stage="approvalNextStage"/)
|
||||||
|
assert.match(approvalDialog, /size="approval"/)
|
||||||
|
assert.match(approvalDialog, /actions-align="end"/)
|
||||||
assert.doesNotMatch(approvalDialog, /submit-confirm-summary/)
|
assert.doesNotMatch(approvalDialog, /submit-confirm-summary/)
|
||||||
assert.doesNotMatch(approvalDialog, /单据编号/)
|
assert.doesNotMatch(approvalDialog, /单据编号/)
|
||||||
assert.doesNotMatch(approvalDialog, /当前节点/)
|
assert.doesNotMatch(approvalDialog, /当前节点/)
|
||||||
@@ -184,8 +186,14 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
|
|||||||
assert.match(approvalDialog, /update:risk-confirmed/)
|
assert.match(approvalDialog, /update:risk-confirmed/)
|
||||||
assert.match(approvalDialog, /:confirm-disabled="confirmDisabled"/)
|
assert.match(approvalDialog, /:confirm-disabled="confirmDisabled"/)
|
||||||
assert.match(approvalDialog, /props\.opinionRequired && !currentOpinion\.value\.trim\(\)/)
|
assert.match(approvalDialog, /props\.opinionRequired && !currentOpinion\.value\.trim\(\)/)
|
||||||
|
assert.match(approvalDialog, /\.approval-opinion-field \{[\s\S]*gap: 6px;[\s\S]*margin-top: 8px;/)
|
||||||
|
assert.match(approvalDialog, /\.approval-opinion-field textarea \{[\s\S]*min-height: 74px;/)
|
||||||
assert.match(confirmDialog, /confirmDisabled:\s*\{\s*type:\s*Boolean,\s*default:\s*false\s*\}/)
|
assert.match(confirmDialog, /confirmDisabled:\s*\{\s*type:\s*Boolean,\s*default:\s*false\s*\}/)
|
||||||
assert.match(confirmDialog, /:disabled="busy \|\| confirmDisabled"/)
|
assert.match(confirmDialog, /:disabled="busy \|\| confirmDisabled"/)
|
||||||
|
assert.match(confirmDialog, /\.shared-confirm-card--approval \{[\s\S]*width: min\(460px, calc\(100vw - 40px\)\);/)
|
||||||
|
assert.match(confirmDialog, /\.shared-confirm-card--approval h4 \{[\s\S]*font-size: 20px;/)
|
||||||
|
assert.match(confirmDialog, /\.shared-confirm-card--approval \.shared-confirm-body \{[\s\S]*max-height: min\(270px, calc\(100dvh - 238px\)\);/)
|
||||||
|
assert.match(confirmDialog, /\.shared-confirm-card--approval \.shared-confirm-btn \{[\s\S]*min-width: 118px;[\s\S]*min-height: 38px;/)
|
||||||
|
|
||||||
assert.match(detailStyles, /\.detail-card-title-with-icon \{[\s\S]*display: inline-flex;[\s\S]*align-items: center;[\s\S]*gap: 8px;/)
|
assert.match(detailStyles, /\.detail-card-title-with-icon \{[\s\S]*display: inline-flex;[\s\S]*align-items: center;[\s\S]*gap: 8px;/)
|
||||||
assert.match(detailStyles, /\.detail-card-title-with-icon i \{[\s\S]*font-size: 18px;[\s\S]*line-height: 1;/)
|
assert.match(detailStyles, /\.detail-card-title-with-icon i \{[\s\S]*font-size: 18px;[\s\S]*line-height: 1;/)
|
||||||
|
|||||||
@@ -84,7 +84,10 @@ test('AI mode handles document query prompts locally before steward planning', (
|
|||||||
assert.match(aiMode, /async function handleAiDocumentQueryIntent/)
|
assert.match(aiMode, /async function handleAiDocumentQueryIntent/)
|
||||||
assert.match(aiMode, /buildAiDocumentQueryConditionSummary/)
|
assert.match(aiMode, /buildAiDocumentQueryConditionSummary/)
|
||||||
assert.match(aiMode, /filterAiDocumentQueryRecords\(payload, intent\)/)
|
assert.match(aiMode, /filterAiDocumentQueryRecords\(payload, intent\)/)
|
||||||
|
assert.match(aiMode, /mergeAiDocumentQueryPayloads/)
|
||||||
assert.match(aiMode, /fetchApprovalExpenseClaims/)
|
assert.match(aiMode, /fetchApprovalExpenseClaims/)
|
||||||
|
assert.match(aiMode, /Promise\.all\(\[/)
|
||||||
|
assert.match(aiMode, /fetchAiDocumentQueryPayload\(intent\)/)
|
||||||
assert.match(aiMode, /buildAiDocumentQueryMessage/)
|
assert.match(aiMode, /buildAiDocumentQueryMessage/)
|
||||||
assert.match(aiMode, /AI_DOCUMENT_QUERY_STEP_DELAY_MS/)
|
assert.match(aiMode, /AI_DOCUMENT_QUERY_STEP_DELAY_MS/)
|
||||||
assert.match(aiMode, /async function updateAiDocumentQueryThinking/)
|
assert.match(aiMode, /async function updateAiDocumentQueryThinking/)
|
||||||
@@ -149,6 +152,16 @@ test('AI mode handles application preview save and submit through buttons or tex
|
|||||||
assert.match(aiMode, /#ai-open-application-detail:/)
|
assert.match(aiMode, /#ai-open-application-detail:/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('AI mode keeps missing application fields editable in the preview table without quick template action', () => {
|
||||||
|
assert.match(aiMode, /function buildInlineApplicationPreviewSuggestedActions\(applicationPreview = \{\}, draftPayload = null\)/)
|
||||||
|
assert.match(aiMode, /label:\s*'保存草稿'/)
|
||||||
|
assert.match(aiMode, /function handleInlineApplicationPreviewTextAction\(prompt\)/)
|
||||||
|
assert.doesNotMatch(aiMode, /label:\s*'快速模板'/)
|
||||||
|
assert.doesNotMatch(aiMode, /action_type:\s*'prefill_composer'/)
|
||||||
|
assert.doesNotMatch(aiMode, /buildInlineApplicationPreviewTemplatePrefill/)
|
||||||
|
assert.doesNotMatch(aiMode, /applyInlineApplicationPreviewTemplateText/)
|
||||||
|
})
|
||||||
|
|
||||||
test('AI mode waits for submit confirmation before adding submit action to the conversation', () => {
|
test('AI mode waits for submit confirmation before adding submit action to the conversation', () => {
|
||||||
const executeStart = aiMode.indexOf('async function executeInlineApplicationPreviewAction')
|
const executeStart = aiMode.indexOf('async function executeInlineApplicationPreviewAction')
|
||||||
const executeEnd = aiMode.indexOf('\nfunction handleInlineApplicationPreviewTextAction', executeStart)
|
const executeEnd = aiMode.indexOf('\nfunction handleInlineApplicationPreviewTextAction', executeStart)
|
||||||
@@ -179,8 +192,14 @@ test('AI mode waits for submit confirmation before adding submit action to the c
|
|||||||
|
|
||||||
test('AI mode formats saved application draft as a detail table without continuing submit flow', () => {
|
test('AI mode formats saved application draft as a detail table without continuing submit flow', () => {
|
||||||
assert.match(aiMode, /function buildInlineApplicationResultTable\(draftPayload = \{\}, options = \{\}\)/)
|
assert.match(aiMode, /function buildInlineApplicationResultTable\(draftPayload = \{\}, options = \{\}\)/)
|
||||||
assert.match(aiMode, /\| 单据类型 \| 单据编号 \| 单据状态 \| 当前节点 \| 操作 \|/)
|
assert.match(aiMode, /function normalizeInlineApplicationStatusLabel\(value, fallback = ''\)/)
|
||||||
|
assert.match(aiMode, /submitted:\s*'审批中'/)
|
||||||
|
assert.match(aiMode, /const statusLabel = normalizeInlineApplicationStatusLabel\(info\.statusLabel, options\.statusLabel\)/)
|
||||||
|
assert.match(aiMode, /\| 单据类型 \| 单据编号 \| 单据状态 \| 当前节点 \| 日期 \| 地点 \| 事由 \| 金额 \| 操作 \|/)
|
||||||
assert.match(aiMode, /\[查看\]\(\$\{href\}\)/)
|
assert.match(aiMode, /\[查看\]\(\$\{href\}\)/)
|
||||||
|
assert.match(aiMode, /dateLabel:\s*rangeText \|\| dateText \|\| resolveBodyField\(\['时间', '日期', '申请时间'\]\) \|\| '待补充'/)
|
||||||
|
assert.match(aiMode, /locationLabel:[\s\S]*resolveBodyField\(\['地点', '目的地'\]\) \|\| '待补充'/)
|
||||||
|
assert.match(aiMode, /reasonLabel:[\s\S]*resolveBodyField\(\['事由', '事件', '申请事由'\]\) \|\| '待补充'/)
|
||||||
assert.match(aiMode, /buildInlineApplicationActionDetailHref\(info\)/)
|
assert.match(aiMode, /buildInlineApplicationActionDetailHref\(info\)/)
|
||||||
assert.match(aiMode, /params\.set\('claim_id', claimId\)/)
|
assert.match(aiMode, /params\.set\('claim_id', claimId\)/)
|
||||||
assert.match(aiMode, /params\.set\('claim_no', claimNo\)/)
|
assert.match(aiMode, /params\.set\('claim_no', claimNo\)/)
|
||||||
|
|||||||
@@ -266,32 +266,25 @@ test('AI mode screen follows the approved reference structure', () => {
|
|||||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-query-summary\)/)
|
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-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-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\) \{[\s\S]*url\("\.\.\/\.\.\/ai-document-card-bg\.png"\);/)
|
||||||
|
assert.doesNotMatch(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card\)::before/)
|
||||||
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__head\) \{[\s\S]*background: var\(--ai-document-card-head-bg\);/)
|
||||||
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\.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__summary\)/)
|
||||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__details\)/)
|
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__field\)/)
|
||||||
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__field--wide\)/)
|
||||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-document-card__label\)/)
|
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(
|
assert.match(
|
||||||
aiModeStyles,
|
aiModeStyles,
|
||||||
/\.workbench-ai-answer-markdown :deep\(\.ai-document-card__details\) \{[\s\S]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\);/
|
/\.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-document-card__field--action \.ai-document-card__action\)/)
|
||||||
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-item\)/)
|
|
||||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-record-meta\)/)
|
|
||||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-record-action \.ai-html-action-link\)/)
|
|
||||||
assert.match(
|
|
||||||
aiModeStyles,
|
|
||||||
/\.workbench-ai-answer-markdown :deep\(\.ai-html-record-item\)\s*\{[\s\S]*grid-template-columns:\s*minmax\(220px,\s*1\.15fr\)\s*minmax\(260px,\s*0\.85fr\)\s*auto;/
|
|
||||||
)
|
|
||||||
assert.match(
|
|
||||||
aiModeStyles,
|
|
||||||
/\.workbench-ai-answer-markdown :deep\(\.ai-html-record-action \.ai-html-action-link\)[\s\S]*background:\s*#2563eb;/
|
|
||||||
)
|
|
||||||
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-image-frame\)/)
|
assert.match(aiModeStyles, /\.workbench-ai-answer-markdown :deep\(\.ai-html-image-frame\)/)
|
||||||
assert.match(aiMode, /import \{ fetchSettings \} from '\.\.\/\.\.\/services\/settings\.js'/)
|
assert.match(aiMode, /import \{ fetchSettings \} from '\.\.\/\.\.\/services\/settings\.js'/)
|
||||||
assert.match(aiMode, /import \{ fetchStewardPlan, fetchStewardPlanStream \} from '\.\.\/\.\.\/services\/steward\.js'/)
|
assert.match(aiMode, /import \{ fetchStewardPlan, fetchStewardPlanStream \} from '\.\.\/\.\.\/services\/steward\.js'/)
|
||||||
@@ -320,7 +313,7 @@ test('AI mode screen follows the approved reference structure', () => {
|
|||||||
assert.match(aiMode, /const hasServerStreamedContent = Boolean\(String\(pendingMessage\.content \|\| ''\)\.trim\(\)\)/)
|
assert.match(aiMode, /const hasServerStreamedContent = Boolean\(String\(pendingMessage\.content \|\| ''\)\.trim\(\)\)/)
|
||||||
assert.match(aiMode, /if \(!hasServerStreamedContent\) \{[\s\S]*await streamInlineAssistantContent\(pendingMessage\.id, finalMessageText\)[\s\S]*\}/)
|
assert.match(aiMode, /if \(!hasServerStreamedContent\) \{[\s\S]*await streamInlineAssistantContent\(pendingMessage\.id, finalMessageText\)[\s\S]*\}/)
|
||||||
assert.match(aiMode, /if \(actionType === AI_APPLICATION_ACTION_SUBMIT\) \{[\s\S]*buildInlineApplicationResultTable\(draftPayload/)
|
assert.match(aiMode, /if \(actionType === AI_APPLICATION_ACTION_SUBMIT\) \{[\s\S]*buildInlineApplicationResultTable\(draftPayload/)
|
||||||
assert.match(aiMode, /需要查看完整详情时,请点击列表最后一列的“查看”进入单据详情。/)
|
assert.match(aiMode, /需要查看完整详情时,请点击卡片“操作”行的“查看”进入单据详情。/)
|
||||||
assert.doesNotMatch(aiMode, /\*\*申请单号:\*\*/)
|
assert.doesNotMatch(aiMode, /\*\*申请单号:\*\*/)
|
||||||
assert.doesNotMatch(aiMode, /createInlineMessage\('assistant', buildStewardPlanMessageText\(plan\)/)
|
assert.doesNotMatch(aiMode, /createInlineMessage\('assistant', buildStewardPlanMessageText\(plan\)/)
|
||||||
assert.doesNotMatch(aiMode, /runOrchestrator\(/)
|
assert.doesNotMatch(aiMode, /runOrchestrator\(/)
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ const aiMode = readFileSync(
|
|||||||
fileURLToPath(new URL('../src/components/business/PersonalWorkbenchAiMode.vue', import.meta.url)),
|
fileURLToPath(new URL('../src/components/business/PersonalWorkbenchAiMode.vue', import.meta.url)),
|
||||||
'utf8'
|
'utf8'
|
||||||
)
|
)
|
||||||
|
const aiDetailReference = readFileSync(
|
||||||
|
fileURLToPath(new URL('../src/utils/aiDocumentDetailReference.js', import.meta.url)),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
|
||||||
test('workbench document detail keeps workbench as the return target', () => {
|
test('workbench document detail keeps workbench as the return target', () => {
|
||||||
assert.match(workbench, /source:\s*'workbench'/)
|
assert.match(workbench, /source:\s*'workbench'/)
|
||||||
@@ -35,12 +39,16 @@ test('workbench document detail keeps workbench as the return target', () => {
|
|||||||
assert.match(appShellComposable, /router\.push\(\{ name: 'app-documents', query: buildDocumentReturnQuery\(\) \}\)/)
|
assert.match(appShellComposable, /router\.push\(\{ name: 'app-documents', query: buildDocumentReturnQuery\(\) \}\)/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('AI detail links wait for full document detail instead of rendering a half snapshot', () => {
|
test('AI detail links resolve real claim identity before opening document detail', () => {
|
||||||
assert.match(aiMode, /detailLookupOnly:\s*true/)
|
assert.match(aiMode, /buildAiDocumentDetailRequest/)
|
||||||
assert.match(aiMode, /params\.get\('claim_id'\)/)
|
assert.match(aiMode, /parseAiApplicationDetailHref/)
|
||||||
assert.match(aiMode, /params\.get\('claim_no'\)/)
|
assert.match(aiMode, /parseAiDocumentDetailHref/)
|
||||||
assert.match(aiMode, /claimId:\s*claimId \|\| reference/)
|
assert.match(aiDetailReference, /detailLookupOnly:\s*true/)
|
||||||
assert.match(aiMode, /claimNo:\s*claimNo \|\| reference/)
|
assert.match(aiDetailReference, /params\.get\('claim_id'\)/)
|
||||||
|
assert.match(aiDetailReference, /params\.get\('claim_no'\)/)
|
||||||
|
assert.match(aiDetailReference, /isBusinessDocumentReference/)
|
||||||
|
assert.match(aiDetailReference, /const claimId = explicitClaimId \|\| \(!referenceIsBusinessNo \? reference : ''\)/)
|
||||||
|
assert.match(aiDetailReference, /const claimNo = explicitClaimNo \|\| \(referenceIsBusinessNo \? reference : ''\)/)
|
||||||
assert.match(
|
assert.match(
|
||||||
appShell,
|
appShell,
|
||||||
/v-else-if="activeView === 'documents' && detailMode && !selectedRequest"[\s\S]*正在加载完整单据详情/
|
/v-else-if="activeView === 'documents' && detailMode && !selectedRequest"[\s\S]*正在加载完整单据详情/
|
||||||
@@ -49,10 +57,16 @@ test('AI detail links wait for full document detail instead of rendering a half
|
|||||||
appShell,
|
appShell,
|
||||||
/const detailPayload = request \|\| \{[\s\S]*detailLookupOnly:\s*true[\s\S]*\}/
|
/const detailPayload = request \|\| \{[\s\S]*detailLookupOnly:\s*true[\s\S]*\}/
|
||||||
)
|
)
|
||||||
|
assert.match(appShell, /isBusinessDocumentReference/)
|
||||||
|
assert.match(appShell, /const requestCandidates = Array\.isArray\(workbenchRequests\.value\)/)
|
||||||
|
assert.match(appShell, /claimId:\s*fallbackClaimId/)
|
||||||
|
assert.match(appShell, /claimNo:\s*fallbackClaimNo/)
|
||||||
assert.match(
|
assert.match(
|
||||||
appShellComposable,
|
appShellComposable,
|
||||||
/const isDetailLookupOnlyRequest = isDetailLookupOnlyPayload\(request\)[\s\S]*selectedRequestSnapshot\.value = isDetailLookupOnlyRequest \? null : request \|\| null/
|
/const isDetailLookupOnlyRequest = isDetailLookupOnlyPayload\(request\)[\s\S]*selectedRequestSnapshot\.value = isDetailLookupOnlyRequest \? null : request \|\| null/
|
||||||
)
|
)
|
||||||
|
assert.match(appShellComposable, /const workbenchRequests = computed/)
|
||||||
|
assert.match(appShellComposable, /workbenchRequests\.value\.find\(\(item\) => isSameRequestIdentity\(item, requestId\)\)/)
|
||||||
assert.match(
|
assert.match(
|
||||||
appShellComposable,
|
appShellComposable,
|
||||||
/void refreshSelectedRequestDetail\(isDetailLookupOnlyRequest \? requestId : request\)/
|
/void refreshSelectedRequestDetail\(isDetailLookupOnlyRequest \? requestId : request\)/
|
||||||
|
|||||||
Reference in New Issue
Block a user