fix(expenses): refine operation record audit UI
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# 费用操作记录图标缺失与企业级样式收敛
|
||||
|
||||
日期:2026-07-20
|
||||
文档路径:document/development/2026-07-20/dev-logs/bugs/expense-operation-record-icon-enterprise-ui.md
|
||||
|
||||
## 修复记录
|
||||
- 09:58:修复费用操作记录 AI 预审图标缺失,并将记录区收敛为企业级审计日志样式。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream 为 `origin/main`;`HEAD..origin/main` 无新提交;本地相对 upstream ahead 19 个提交。
|
||||
- 本地 ahead 摘要:`07241b46 fix(docker): manage local postgres in default compose`;`787bc3a4 feat(platform): close AI expense value loop`;`242d68c3 feat(approval): add task workflow and waiver decisions`;`28b834ed fix(approval): replay immutable action responses`;`4940ebc4 feat(approval): add safe risk disposition workflow`;`ee88a36b feat(ai): add tenant-safe hierarchical expense learning`;`6bdf65bc feat(expenses): add authoritative pre-review workflow`;`ae3f02c3 feat(expense): add persistent zero-entry receipt association`;`54754b55 feat(ai): add personal expense application memory`;`211f85d9 feat(ai): unify verified expense application workflow`;`5b246307 feat(ai): issue verified application preview decisions`;`a662cfe6 feat(ai): add expense application feedback ledger`;`5ed34c2b feat(expenses): backfill historical claims into expense cases`;`11275e4b fix(migrations): enforce schema ownership safety`;`1347366b feat(expenses): secure timeline and draft events`;`22669a90 feat(expenses): show unified expense event timeline`;`a616b30c fix(expenses): unify AI application submission transaction`;`653eda05 feat(auth): add opaque bearer sessions`;`661990b2 feat(expenses): add transactional expense case events`。
|
||||
- 修改:将 MDI 7.4.47 不存在的 `mdi-shield-search-outline` 替换为 `mdi-shield-check-outline`,并把申请/报销提交图标改为语义更准确的 `mdi-file-send-outline`;补充图标映射回归断言。
|
||||
- 修改:将费用操作记录改为“发生时间 / 事件 / 操作人”三列审计日志,图标并入事件列;移除绿色“最新”成功态误用,改为中性记录数和“最新优先”元数据;统一紧凑行高、方形状态图标、标准字重、语义色与显式响应式网格。
|
||||
- 修改:补充 `aria-labelledby`、`aria-busy`、图标读屏隐藏、操作人完整值提示和刷新按钮键盘焦点样式;事件倒序排序仅对有限时间戳执行数值比较。
|
||||
- 操作:通过项目脚本写入本 bug 日志;生成桌面最终截图、聚焦截图和设计对照证据,并更新根目录 `design-qa.md`。
|
||||
- 验证:`node --test --test-timeout=60000 tests/expense-case-timeline.test.mjs` 通过,13/13;`npm run build` 通过;`git diff --check` 通过;真实申请 `AT4UHTKWU` 浏览器刷新后仍展示两条记录、三列表头与两个有效图标,未进入加载失败状态。
|
||||
- 影响:修复费用申请与报销单 AI 预审事件的空白图标;统一费用操作记录的信息密度、状态语义、可访问性和企业后台视觉风格,不改变后端事件协议或审批业务顺序。
|
||||
@@ -2,13 +2,12 @@
|
||||
min-width: 0;
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.expense-case-timeline-head,
|
||||
.expense-case-timeline-title-row,
|
||||
.expense-case-meta,
|
||||
.expense-case-event-meta {
|
||||
.expense-case-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -25,26 +24,26 @@
|
||||
|
||||
.expense-case-timeline h4 {
|
||||
margin: 0;
|
||||
color: #0f172a;
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
font-weight: 850;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.expense-case-timeline-head p {
|
||||
margin: 6px 0 0;
|
||||
color: #64748b;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.expense-case-stage {
|
||||
padding: 3px 8px;
|
||||
padding: 2px 7px;
|
||||
border: 1px solid rgba(var(--theme-primary-rgb), .2);
|
||||
border-radius: 999px;
|
||||
border-radius: 4px;
|
||||
background: var(--theme-primary-soft);
|
||||
color: var(--theme-primary-active);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.expense-case-meta {
|
||||
@@ -55,22 +54,24 @@
|
||||
.expense-case-meta span + span::before {
|
||||
content: "·";
|
||||
margin: 0 7px;
|
||||
color: #cbd5e1;
|
||||
color: var(--line);
|
||||
}
|
||||
|
||||
.expense-case-refresh {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 7px 11px;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
min-height: 36px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -79,6 +80,11 @@
|
||||
color: var(--theme-primary-active);
|
||||
}
|
||||
|
||||
.expense-case-refresh:focus-visible {
|
||||
outline: 2px solid var(--theme-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.expense-case-refresh:disabled {
|
||||
cursor: wait;
|
||||
opacity: .62;
|
||||
@@ -95,10 +101,10 @@
|
||||
gap: 12px;
|
||||
min-height: 58px;
|
||||
padding: 12px 14px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-soft);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -116,32 +122,57 @@
|
||||
}
|
||||
|
||||
.expense-case-state strong {
|
||||
color: #334155;
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.expense-case-state.error {
|
||||
border-color: #fecaca;
|
||||
background: #fff7f7;
|
||||
border-color: var(--danger-line);
|
||||
background: var(--danger-soft);
|
||||
}
|
||||
|
||||
.expense-case-state.error > i {
|
||||
color: #dc2626;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.expense-case-state button {
|
||||
flex: 0 0 auto;
|
||||
padding: 6px 9px;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
border-radius: 4px;
|
||||
background: var(--theme-primary);
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expense-case-event-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.expense-case-event-columns,
|
||||
.expense-case-event {
|
||||
display: grid;
|
||||
grid-template-columns: 148px minmax(280px, 1fr) minmax(160px, 200px);
|
||||
align-items: center;
|
||||
column-gap: 20px;
|
||||
}
|
||||
|
||||
.expense-case-event-columns {
|
||||
min-height: 34px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--surface-soft);
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.expense-case-event-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -149,84 +180,110 @@
|
||||
}
|
||||
|
||||
.expense-case-event {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 0 0 18px;
|
||||
min-height: 70px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.expense-case-event:last-child {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.expense-case-event:not(:last-child)::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 31px;
|
||||
bottom: 2px;
|
||||
left: 16px;
|
||||
width: 2px;
|
||||
background: #e2e8f0;
|
||||
.expense-case-event-main {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.expense-case-event-icon {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex: 0 0 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(var(--theme-primary-rgb), .2);
|
||||
border-radius: 999px;
|
||||
border-radius: 5px;
|
||||
background: var(--theme-primary-soft);
|
||||
color: var(--theme-primary-active);
|
||||
font-size: 17px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.expense-case-event-icon > i {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.expense-case-event.success .expense-case-event-icon {
|
||||
border-color: #bbf7d0;
|
||||
background: #f0fdf4;
|
||||
color: #15803d;
|
||||
border-color: var(--success-line);
|
||||
background: var(--success-soft);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.expense-case-event.warning .expense-case-event-icon {
|
||||
border-color: #fed7aa;
|
||||
background: #fff7ed;
|
||||
color: #c2410c;
|
||||
border-color: var(--warning-line);
|
||||
background: var(--warning-soft);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.expense-case-event.danger .expense-case-event-icon {
|
||||
border-color: var(--danger-line);
|
||||
background: var(--danger-soft);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.expense-case-event-time {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.expense-case-event-copy {
|
||||
min-width: 0;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.expense-case-event-copy > strong {
|
||||
display: block;
|
||||
color: #1e293b;
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.expense-case-event-copy > p {
|
||||
margin: 4px 0 7px;
|
||||
color: #475569;
|
||||
margin: 4px 0 0;
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.expense-case-event-meta {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 14px;
|
||||
color: #94a3b8;
|
||||
.expense-case-event-actor {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 5px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.expense-case-event-meta span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
.expense-case-event-actor span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.expense-case-event-columns,
|
||||
.expense-case-event {
|
||||
grid-template-columns: 132px minmax(220px, 1fr) minmax(140px, 168px);
|
||||
column-gap: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@@ -239,7 +296,8 @@
|
||||
}
|
||||
|
||||
.expense-case-refresh {
|
||||
padding: 7px 9px;
|
||||
width: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.expense-case-state {
|
||||
@@ -250,4 +308,44 @@
|
||||
.expense-case-state button {
|
||||
margin-left: 33px;
|
||||
}
|
||||
|
||||
.expense-case-timeline-body {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.expense-case-event-columns {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.expense-case-event-table {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.expense-case-event {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 7px;
|
||||
min-height: 0;
|
||||
padding: 13px 14px;
|
||||
}
|
||||
|
||||
.expense-case-event-time {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
padding-left: 40px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.expense-case-event-main {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.expense-case-event-actor {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
justify-content: flex-start;
|
||||
padding-left: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<section class="expense-case-timeline" aria-label="费用事件时间线">
|
||||
<section class="expense-case-timeline" aria-labelledby="expense-case-timeline-title">
|
||||
<div class="expense-case-timeline-head">
|
||||
<div>
|
||||
<div class="expense-case-timeline-title-row">
|
||||
<h4>费用事件时间线</h4>
|
||||
<h4 id="expense-case-timeline-title">费用操作记录</h4>
|
||||
<span v-if="timeline" class="expense-case-stage">{{ timeline.stageLabel }}</span>
|
||||
</div>
|
||||
<p v-if="timeline" class="expense-case-meta">
|
||||
<span>{{ timeline.caseNo || '统一费用事件' }}</span>
|
||||
<span>已串联 {{ timeline.linkCount }} 张业务单据</span>
|
||||
<span v-if="displayedEvents.length">共 {{ displayedEvents.length }} 条记录</span>
|
||||
<span v-if="displayedEvents.length">最新优先</span>
|
||||
</p>
|
||||
<p v-else>记录费用从申请、审批、报销到付款的真实办理轨迹。</p>
|
||||
</div>
|
||||
@@ -19,12 +21,12 @@
|
||||
aria-label="刷新费用事件时间线"
|
||||
@click="refreshExpenseCaseTimeline"
|
||||
>
|
||||
<i :class="loading ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
|
||||
<i :class="loading ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'" aria-hidden="true"></i>
|
||||
<span>刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="expense-case-timeline-body" aria-live="polite">
|
||||
<div class="expense-case-timeline-body" :aria-busy="loading">
|
||||
<div v-if="loading && !timeline" class="expense-case-state loading">
|
||||
<i class="mdi mdi-loading mdi-spin"></i>
|
||||
<span>正在同步真实费用事件</span>
|
||||
@@ -55,34 +57,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ol v-else-if="timeline" class="expense-case-event-list">
|
||||
<li
|
||||
v-for="event in timeline.events"
|
||||
:key="event.id"
|
||||
class="expense-case-event"
|
||||
:class="event.tone"
|
||||
>
|
||||
<span class="expense-case-event-icon" aria-hidden="true">
|
||||
<i :class="event.icon"></i>
|
||||
</span>
|
||||
<div class="expense-case-event-copy">
|
||||
<strong>{{ event.label }}</strong>
|
||||
<p>{{ event.summary }}</p>
|
||||
<div class="expense-case-event-meta">
|
||||
<span><i class="mdi mdi-account-outline"></i>{{ event.actorLabel }}</span>
|
||||
<time :datetime="event.occurredAt || undefined">{{ event.occurredAtLabel }}</time>
|
||||
<div v-else-if="timeline" class="expense-case-event-table">
|
||||
<div class="expense-case-event-columns" aria-hidden="true">
|
||||
<span>发生时间</span>
|
||||
<span>事件</span>
|
||||
<span>操作人</span>
|
||||
</div>
|
||||
<ol class="expense-case-event-list">
|
||||
<li
|
||||
v-for="event in displayedEvents"
|
||||
:key="event.id"
|
||||
class="expense-case-event"
|
||||
:class="event.tone"
|
||||
>
|
||||
<time
|
||||
class="expense-case-event-time"
|
||||
:datetime="event.occurredAt || undefined"
|
||||
>
|
||||
{{ event.occurredAtLabel }}
|
||||
</time>
|
||||
<div class="expense-case-event-main">
|
||||
<span class="expense-case-event-icon" aria-hidden="true">
|
||||
<i :class="event.icon"></i>
|
||||
</span>
|
||||
<div class="expense-case-event-copy">
|
||||
<strong>{{ event.label }}</strong>
|
||||
<p>{{ event.summary }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="expense-case-event-actor" :title="event.actorLabel">
|
||||
<i class="mdi mdi-account-outline" aria-hidden="true"></i>
|
||||
<span>{{ event.actorLabel }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { toRef } from 'vue'
|
||||
import { computed, toRef } from 'vue'
|
||||
|
||||
import { useExpenseCaseTimeline } from '../../composables/requests/useExpenseCaseTimeline.js'
|
||||
import { orderExpenseCaseEventsForDisplay } from '../../utils/expenseCaseTimeline.js'
|
||||
|
||||
const props = defineProps({
|
||||
claimId: { type: String, default: '' },
|
||||
@@ -99,6 +117,8 @@ const {
|
||||
claimId: toRef(props, 'claimId'),
|
||||
refreshKey: toRef(props, 'refreshKey')
|
||||
})
|
||||
|
||||
const displayedEvents = computed(() => orderExpenseCaseEventsForDisplay(timeline.value?.events))
|
||||
</script>
|
||||
|
||||
<style scoped src="../../assets/styles/components/travel-request-expense-case-timeline.css"></style>
|
||||
|
||||
@@ -27,8 +27,8 @@ const EVENT_PRESENTATION = {
|
||||
claim_draft_created: { label: '报销草稿已创建', icon: 'mdi mdi-file-plus-outline', tone: 'info' },
|
||||
claim_draft_updated: { label: '报销草稿已更新', icon: 'mdi mdi-file-edit-outline', tone: 'info' },
|
||||
historical_claim_imported: { label: '历史单据已纳入统一费用事件', icon: 'mdi mdi-history', tone: 'info' },
|
||||
application_submitted: { label: '费用申请已提交', icon: 'mdi mdi-send-outline', tone: 'info' },
|
||||
claim_submitted: { label: '报销单已提交', icon: 'mdi mdi-send-outline', tone: 'info' },
|
||||
application_submitted: { label: '费用申请已提交', icon: 'mdi mdi-file-send-outline', tone: 'info' },
|
||||
claim_submitted: { label: '报销单已提交', icon: 'mdi mdi-file-send-outline', tone: 'info' },
|
||||
approval_stage_completed: { label: '审批节点已完成', icon: 'mdi mdi-account-check-outline', tone: 'success' },
|
||||
application_approved: { label: '费用申请已审批', icon: 'mdi mdi-check-decagram-outline', tone: 'success' },
|
||||
claim_approved: { label: '报销单已审批', icon: 'mdi mdi-check-decagram-outline', tone: 'success' },
|
||||
@@ -37,8 +37,8 @@ const EVENT_PRESENTATION = {
|
||||
reimbursement_draft_generated: { label: '报销草稿已自动生成', icon: 'mdi mdi-robot-outline', tone: 'success' },
|
||||
receipt_received: { label: '票据已进入费用事件', icon: 'mdi mdi-receipt-text-plus-outline', tone: 'info' },
|
||||
attachment_associated: { label: '票据已归集到报销草稿', icon: 'mdi mdi-file-link-outline', tone: 'success' },
|
||||
application_pre_review_completed: { label: '费用申请 AI 预审已完成', icon: 'mdi mdi-shield-search-outline', tone: 'success' },
|
||||
claim_pre_review_completed: { label: '报销单 AI 预审已完成', icon: 'mdi mdi-shield-search-outline', tone: 'success' },
|
||||
application_pre_review_completed: { label: '费用申请 AI 预审已完成', icon: 'mdi mdi-shield-check-outline', tone: 'success' },
|
||||
claim_pre_review_completed: { label: '报销单 AI 预审已完成', icon: 'mdi mdi-shield-check-outline', tone: 'success' },
|
||||
payment_completed: { label: '付款已完成', icon: 'mdi mdi-cash-check', tone: 'success' },
|
||||
application_archived: { label: '费用申请已归档', icon: 'mdi mdi-archive-check-outline', tone: 'success' }
|
||||
}
|
||||
@@ -228,7 +228,7 @@ function normalizeEvent(event, index) {
|
||||
export function buildExpenseCaseTimelineViewModel(payload = {}) {
|
||||
const events = Array.isArray(payload?.events)
|
||||
? payload.events.map(normalizeEvent).sort((left, right) => {
|
||||
if (left.sortTimestamp !== null && right.sortTimestamp !== null) {
|
||||
if (Number.isFinite(left.sortTimestamp) && Number.isFinite(right.sortTimestamp)) {
|
||||
return left.sortTimestamp - right.sortTimestamp || left.sourceIndex - right.sourceIndex
|
||||
}
|
||||
return left.sourceIndex - right.sourceIndex
|
||||
@@ -247,6 +247,17 @@ export function buildExpenseCaseTimelineViewModel(payload = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export function orderExpenseCaseEventsForDisplay(events = []) {
|
||||
return Array.isArray(events)
|
||||
? [...events].sort((left, right) => {
|
||||
if (Number.isFinite(left.sortTimestamp) && Number.isFinite(right.sortTimestamp)) {
|
||||
return right.sortTimestamp - left.sortTimestamp || left.sourceIndex - right.sourceIndex
|
||||
}
|
||||
return left.sourceIndex - right.sourceIndex
|
||||
})
|
||||
: []
|
||||
}
|
||||
|
||||
export function isExpenseCaseNotAvailableError(error) {
|
||||
return Number(error?.status) === 404
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import { effectScope, nextTick, ref } from 'vue'
|
||||
|
||||
import {
|
||||
buildExpenseCaseTimelineViewModel,
|
||||
isExpenseCaseNotAvailableError
|
||||
isExpenseCaseNotAvailableError,
|
||||
orderExpenseCaseEventsForDisplay
|
||||
} from '../src/utils/expenseCaseTimeline.js'
|
||||
import { fetchExpenseCaseTimeline } from '../src/services/expenseCases.js'
|
||||
import { useExpenseCaseTimeline } from '../src/composables/requests/useExpenseCaseTimeline.js'
|
||||
@@ -69,6 +70,7 @@ test('expense case timeline maps and sorts the real business event chain', () =>
|
||||
assert.equal(timeline.linkCount, 2)
|
||||
assert.deepEqual(timeline.events.map((event) => event.id), ['event-1', 'event-2'])
|
||||
assert.equal(timeline.events[0].label, '费用申请已提交')
|
||||
assert.equal(timeline.events[0].icon, 'mdi mdi-file-send-outline')
|
||||
assert.match(timeline.events[0].summary, /草稿 → 已提交/)
|
||||
assert.match(timeline.events[0].summary, /直属领导审批/)
|
||||
assert.equal(timeline.events[0].actorLabel, 'employee@example.com')
|
||||
@@ -139,6 +141,7 @@ test('expense case timeline explains receipt collection, association and AI pre-
|
||||
assert.match(timeline.events[0].summary, /上海出差高铁票\.pdf/)
|
||||
assert.match(timeline.events[1].summary, /BX-20260713-001/)
|
||||
assert.equal(timeline.events[2].tone, 'warning')
|
||||
assert.equal(timeline.events[2].icon, 'mdi mdi-shield-check-outline')
|
||||
assert.match(timeline.events[2].summary, /1 条重大风险/)
|
||||
})
|
||||
|
||||
@@ -246,6 +249,37 @@ test('expense case timeline keeps equal-time server order and hides unknown even
|
||||
assert.doesNotMatch(JSON.stringify(timeline.events[0]), /private-correlation|pending/)
|
||||
})
|
||||
|
||||
test('expense case records show newer events first and preserve equal-time server order', () => {
|
||||
const timeline = buildExpenseCaseTimelineViewModel({
|
||||
events: [
|
||||
{
|
||||
id: 'older',
|
||||
event_type: 'application_submitted',
|
||||
occurred_at: '2026-07-17T14:54:00Z'
|
||||
},
|
||||
{
|
||||
id: 'newer-first',
|
||||
event_type: 'application_pre_review_completed',
|
||||
occurred_at: '2026-07-18T02:00:00Z'
|
||||
},
|
||||
{
|
||||
id: 'newer-second',
|
||||
event_type: 'claim_submitted',
|
||||
occurred_at: '2026-07-18T02:00:00Z'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const displayedEvents = orderExpenseCaseEventsForDisplay(timeline.events)
|
||||
|
||||
assert.deepEqual(
|
||||
displayedEvents.map((event) => event.id),
|
||||
['newer-first', 'newer-second', 'older']
|
||||
)
|
||||
assert.equal(displayedEvents[0].icon, 'mdi mdi-shield-check-outline')
|
||||
assert.deepEqual(timeline.events.map((event) => event.id), ['older', 'newer-first', 'newer-second'])
|
||||
})
|
||||
|
||||
test('expense case service safely encodes claim identifiers', async () => {
|
||||
const originalFetch = globalThis.fetch
|
||||
let requestedUrl = ''
|
||||
@@ -351,7 +385,11 @@ test('travel request detail consumes the real expense case endpoint', () => {
|
||||
assert.match(detailView, /:refresh-key="expenseCaseTimelineRefreshKey"/)
|
||||
assert.match(progressCard, /<TravelRequestExpenseCaseTimeline/)
|
||||
assert.match(progressCard, /TravelRequestExpenseCaseTimeline/)
|
||||
assert.match(timelineComponent, /费用事件时间线/)
|
||||
assert.match(timelineComponent, /费用操作记录/)
|
||||
assert.match(timelineComponent, /发生时间/)
|
||||
assert.match(timelineComponent, /操作人/)
|
||||
assert.doesNotMatch(timelineComponent, /expense-case-latest/)
|
||||
assert.match(timelineComponent, /orderExpenseCaseEventsForDisplay/)
|
||||
assert.match(timelineComponent, /当前单据尚未纳入统一费用事件/)
|
||||
assert.match(expenseCaseService, /\/expense-cases\/by-claim\//)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user