feat(web): 差旅申请详情进度 viewer 与审批/加载态组件增强

- 新增 requestProgressViewer,申请单在直属领导审批视角下将当前步骤展示为'等待批复',travel-request-detail/app-shell/useRequests 接入
- TravelRequestApprovalDialog 增强审批交互,TableLoadingState 补充表格加载占位,ConfirmDialog 扩展确认对话框能力
- useAppShell/useRequests/AppShellRouteView 配套适配申请详情跳转与会话状态
- 同步更新 requestProgressSteps、travel-request-detail-leader-approval、assistant-session-draft-delete、documents-center-status-filter、app-shell-financial-assistant-entry、request-progress-viewer 等测试
This commit is contained in:
caoxiaozhu
2026-06-21 22:49:58 +08:00
parent 8b3495455b
commit 24b5b71b0f
14 changed files with 295 additions and 49 deletions

View File

@@ -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, /buildWorkbenchSummary\(workbenchRequests\.value, currentUser\.value\)/)
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', () => {

View File

@@ -98,9 +98,9 @@ test('deleting an application draft marks AI workbench detail links as unavailab
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')
}
]

View File

@@ -16,6 +16,10 @@ const documentListSharedStyles = readFileSync(
fileURLToPath(new URL('../src/assets/styles/components/document-list-shared.css', import.meta.url)),
'utf8'
)
const tableLoadingState = readFileSync(
fileURLToPath(new URL('../src/components/shared/TableLoadingState.vue', import.meta.url)),
'utf8'
)
const reimbursementService = readFileSync(
fileURLToPath(new URL('../src/services/reimbursements.js', import.meta.url)),
'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="风险等级"/)
})
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', () => {
assert.match(documentsCenterView, /const DOCUMENT_SCOPE_ALL = '全部'/)
assert.match(documentsCenterView, /const DOCUMENT_SCOPE_APPLICATION = '申请单'/)

View 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分钟'
}
]
)
})

View File

@@ -19,7 +19,7 @@ const WAIT_SUBMIT = '\u5f85\u63d0\u4ea4'
const LINKED_APPLICATION = '\u5173\u8054\u5355\u636e'
const PAID = '\u5df2\u4ed8\u6b3e'
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_P8_EXECUTIVE_APPROVAL = '\u7b49\u5f85 P8 Executive \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.deepEqual(
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 === '财务审批'), 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.find((step) => step.label === WAIT_LEADER_LI_APPROVAL)?.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 批复')?.rawLabel, DIRECT_MANAGER_APPROVAL)
assert.equal(request.progressSteps.find((step) => step.label === '等待 Leader Li 批复')?.current, true)
})
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.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.current, true)
assert.equal(leaderStep.time, '停留 3小时15分钟')

View File

@@ -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, /:risk-confirm-items="approvalRiskConfirmItems"/)
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, /单据编号/)
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, /:confirm-disabled="confirmDisabled"/)
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, /: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 i \{[\s\S]*font-size: 18px;[\s\S]*line-height: 1;/)