feat(web): 差旅领导意见事件结构化与申请审批信息增强

- applicationApproval 新增按日期/时间/审批角色拆分格式化,buildLeaderApprovalEvents 补充 dateLabel/timeLabel/roleLabel 字段
- TravelRequestDetailView 领导意见事件改为日期+时间+审批人结构化展示,travel-request-detail-view.css 重构对应样式
- travelReimbursementAttachmentModel 微调附件标识,同步更新 application-approval-info、travel-request-detail-leader-approval、attachment-association-confirmation 测试
- 更新公司通信费报销规则表
This commit is contained in:
caoxiaozhu
2026-06-21 23:24:09 +08:00
parent 88e91a5900
commit 669d22e71f
8 changed files with 329 additions and 186 deletions

View File

@@ -83,9 +83,12 @@ test('buildLeaderApprovalEvents returns leader return and approval timeline in e
assert.deepEqual(events.map((event) => event.type), ['returned', 'approved'])
assert.deepEqual(events.map((event) => event.tone), ['danger', 'success'])
assert.equal(events[0].operator, 'Leader Li')
assert.equal(events[0].role, '直属领导审批节点')
assert.equal(events[0].opinion, 'Need clearer budget explanation.')
assert.equal(events[0].returnCount, 1)
assert.equal(events[0].time, '2026-05-25 09:00')
assert.equal(events[0].dateLabel, '2026-05-25')
assert.equal(events[0].timeLabel, '09:00')
assert.equal(Object.hasOwn(events[0], 'sortAt'), false)
})

View File

@@ -210,6 +210,47 @@ test('OCR receipt folder ids are kept for final draft attachment association', (
assert.equal(Object.getOwnPropertyDescriptor(files[0], 'receiptId')?.enumerable, false)
})
test('OCR documents keep full recognized text for backend context', () => {
const longText = [
'增值税电子发票',
'购买方名称:远光软件股份有限公司',
'销售方名称:上海高铁服务有限公司',
'项目名称:客运服务',
'出发地:武汉',
'到达地:上海',
'乘车日期2026-02-20',
'车次G1234',
'座位等级:二等座',
'金额354.00元',
'税额10.62元',
'发票号码12345678901234567890',
'开票日期2026-02-21',
'购买方纳税人识别号91440400618256625E',
'销售方纳税人识别号91310000132234123X',
'备注:本票据用于差旅报销,请核对出发城市、到达城市、车次、座位等级、金额、税额和电子客票号。',
'电子客票号E1234567890'
].join('\n')
assert.ok(longText.length > 240)
const documents = normalizeOcrDocuments({
documents: [
{
filename: 'train-ticket.pdf',
text: longText,
summary: '铁路电子客票 武汉-上海',
document_fields: [
{ key: 'amount', label: '金额', value: '354.00元' },
{ key: 'ticket_no', label: '电子客票号', value: 'E1234567890' }
]
}
]
})
assert.equal(documents[0].text, longText)
assert.match(documents[0].text, /电子客票号E1234567890/)
})
test('receipt files are collected through a single OCR persistence entry before draft association', async () => {
const files = [
{ name: 'invoice.png' }

View File

@@ -129,11 +129,20 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
assert.match(detailTemplate, /v-for="event in leaderApprovalEvents"/)
assert.match(detailTemplate, /class="application-leader-opinion-event"/)
assert.match(detailTemplate, /event\.type === 'returned'/)
assert.match(detailTemplate, /class="application-leader-opinion-event-time"/)
assert.match(detailTemplate, /event\.dateLabel/)
assert.match(detailTemplate, /event\.timeLabel/)
assert.match(detailTemplate, /class="application-leader-opinion-event-rail"/)
assert.match(detailTemplate, /class="application-leader-opinion-event-status"/)
assert.match(detailTemplate, /class="application-leader-opinion-event-body"/)
assert.match(detailTemplate, /审批意见/)
assert.match(detailTemplate, /:title="event\.title"/)
assert.match(detailTemplate, /class="application-leader-opinion-record"/)
assert.match(detailTemplate, /class="application-leader-opinion-record-head"/)
assert.match(detailTemplate, /class="application-leader-opinion-record-title"/)
assert.match(detailTemplate, /class="application-leader-opinion-record-meta"/)
assert.match(detailTemplate, /event\.operator/)
assert.match(detailTemplate, /event\.role/)
assert.match(detailTemplate, /event\.opinion/)
assert.match(detailTemplate, /class="application-leader-opinion-event-foot"/)
assert.match(detailTemplate, /class="application-leader-opinion-operator"/)
assert.doesNotMatch(detailTemplate, /leaderApprovalReadonlyText/)
assert.doesNotMatch(detailTemplate, /\u5f85\u76f4\u5c5e\u9886\u5bfc\u586b\u5199\u5ba1\u6279\u610f\u89c1/)
assert.match(detailTemplate, /领导意见/)
@@ -200,22 +209,33 @@ test('approval-mode detail collects leader opinion inside confirm dialog before
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;/)
const leaderOpinionPanelRule = detailStyles.match(/^\.application-leader-opinion \{[\s\S]*?\n\}/m)?.[0] ?? ''
const leaderOpinionTimelineRule = detailStyles.match(/^\.application-leader-opinion-timeline \{[\s\S]*?\n\}/m)?.[0] ?? ''
const leaderOpinionEventRule = detailStyles.match(/^\.application-leader-opinion-event \{[\s\S]*?\n\}/m)?.[0] ?? ''
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, /\.application-leader-opinion-head span \{[\s\S]*display: inline-flex;[\s\S]*align-items: center;[\s\S]*gap: 8px;/)
assert.match(leaderOpinionPanelRule, /border-top: 1px solid #e5edf5;/)
assert.match(leaderOpinionPanelRule, /background: #ffffff;/)
assert.doesNotMatch(leaderOpinionPanelRule, /linear-gradient|box-shadow/)
assert.doesNotMatch(detailStyles, /\.leader-approval-card/)
assert.doesNotMatch(detailStyles, /\.inline-leader-opinion/)
assert.match(detailStyles, /\.application-leader-opinion-timeline \{/)
assert.match(detailStyles, /\.application-leader-opinion-timeline\.is-single \{[\s\S]*padding-left: 0;/)
assert.match(detailStyles, /\.application-leader-opinion-timeline\.is-single::before,[\s\S]*\.application-leader-opinion-timeline\.is-single \.application-leader-opinion-event::before \{[\s\S]*display: none;/)
assert.match(leaderOpinionTimelineRule, /gap: 0;/)
assert.match(leaderOpinionTimelineRule, /padding: 2px 0 0;/)
assert.match(detailStyles, /\.application-leader-opinion-event \{/)
assert.match(detailStyles, /\.application-leader-opinion-event \{[\s\S]*border-left: 4px solid var\(--leader-opinion-tone/)
assert.match(detailStyles, /\.application-leader-opinion-event-status \{[\s\S]*border-radius: 999px;/)
assert.match(detailStyles, /\.application-leader-opinion-event-body \{[\s\S]*background: var\(--leader-opinion-soft-bg/)
assert.match(detailStyles, /\.application-leader-opinion-event-body p \{[\s\S]*font-size: 15px;/)
assert.match(detailStyles, /\.application-leader-opinion-event-foot span \{[\s\S]*border-radius: 999px;/)
assert.match(detailStyles, /\.application-leader-opinion-event\.danger::before \{/)
assert.match(detailStyles, /\.application-leader-opinion-event\.success::before \{/)
assert.match(leaderOpinionEventRule, /grid-template-columns: 104px 28px minmax\(0, 1fr\);/)
assert.match(leaderOpinionEventRule, /padding: 0 0 16px;/)
assert.doesNotMatch(leaderOpinionEventRule, /linear-gradient|box-shadow/)
assert.match(detailStyles, /\.application-leader-opinion-event-time \{[\s\S]*justify-items: end;[\s\S]*font-variant-numeric: tabular-nums;/)
assert.match(detailStyles, /\.application-leader-opinion-event-rail \{[\s\S]*justify-content: center;/)
assert.match(detailStyles, /\.application-leader-opinion-event-rail::after \{[\s\S]*width: 2px;[\s\S]*background: #e2e8f0;/)
assert.match(detailStyles, /\.application-leader-opinion-event-status \{[\s\S]*width: 22px;[\s\S]*height: 22px;[\s\S]*border-radius: 999px;/)
assert.match(detailStyles, /\.application-leader-opinion-record \{[\s\S]*border-bottom: 1px solid #edf2f7;[\s\S]*background: #ffffff;/)
assert.match(detailStyles, /\.application-leader-opinion-record-meta \{[\s\S]*display: flex;[\s\S]*gap: 6px 16px;/)
assert.match(detailStyles, /\.application-leader-opinion-record p \{[\s\S]*background: #f8fafc;[\s\S]*font-size: 14px;/)
assert.match(detailStyles, /\.application-leader-opinion-event\.danger \{[\s\S]*--leader-opinion-tone: #dc2626;/)
assert.match(detailStyles, /\.application-leader-opinion-event\.success \{[\s\S]*--leader-opinion-tone: #16a34a;/)
assert.match(reimbursementService, /export function approveExpenseClaim\(claimId, payload = \{\}\)/)
assert.match(reimbursementService, /\/approve/)