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:
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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/)
|
||||
|
||||
Reference in New Issue
Block a user