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:
Binary file not shown.
@@ -718,11 +718,10 @@
|
|||||||
.application-leader-opinion {
|
.application-leader-opinion {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 14px;
|
margin-top: 12px;
|
||||||
padding: 14px;
|
padding: 14px 0 4px;
|
||||||
border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), .16);
|
border-top: 1px solid #e5edf5;
|
||||||
background: linear-gradient(180deg, rgba(248, 251, 255, .98) 0%, #ffffff 100%);
|
background: #ffffff;
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .86);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-head {
|
.application-leader-opinion-head {
|
||||||
@@ -730,8 +729,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
color: #334155;
|
color: #334155;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@@ -742,209 +739,238 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-weight: 850;
|
font-weight: 700;
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-head span i {
|
.application-leader-opinion-head span i {
|
||||||
width: 28px;
|
color: #64748b;
|
||||||
height: 28px;
|
font-size: 17px;
|
||||||
display: inline-flex;
|
line-height: 1;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--theme-primary-active, #255b7d);
|
|
||||||
border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), .22);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(var(--theme-primary-rgb, 58, 124, 165), .08);
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-head strong {
|
.application-leader-opinion-head strong {
|
||||||
padding: 4px 10px;
|
color: #64748b;
|
||||||
border: 1px solid rgba(var(--theme-primary-rgb, 58, 124, 165), .18);
|
font-weight: 600;
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(var(--theme-primary-rgb, 58, 124, 165), .08);
|
|
||||||
color: var(--theme-primary-active);
|
|
||||||
font-weight: 800;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-timeline {
|
.application-leader-opinion-timeline {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 0;
|
||||||
padding-left: 18px;
|
padding: 2px 0 0;
|
||||||
}
|
|
||||||
|
|
||||||
.application-leader-opinion-timeline::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 6px;
|
|
||||||
bottom: 6px;
|
|
||||||
left: 5px;
|
|
||||||
width: 1px;
|
|
||||||
background: #dbe4ee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.application-leader-opinion-timeline.is-single {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.application-leader-opinion-timeline.is-single::before,
|
|
||||||
.application-leader-opinion-timeline.is-single .application-leader-opinion-event::before {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-event {
|
.application-leader-opinion-event {
|
||||||
--leader-opinion-tone: var(--theme-primary, #3a7ca5);
|
--leader-opinion-tone: #16a34a;
|
||||||
--leader-opinion-soft-bg: #f8fbff;
|
|
||||||
--leader-opinion-soft-border: #dbeafe;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
grid-template-columns: 104px 28px minmax(0, 1fr);
|
||||||
padding: 14px 16px 14px 18px;
|
gap: 12px;
|
||||||
border: 1px solid #dbe4ee;
|
padding: 0 0 16px;
|
||||||
border-left: 4px solid var(--leader-opinion-tone);
|
|
||||||
border-radius: 4px;
|
|
||||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
|
||||||
box-shadow: 0 14px 30px rgba(15, 23, 42, .08);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-event::before {
|
.application-leader-opinion-event:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-event-time {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
justify-items: end;
|
||||||
|
padding-top: 1px;
|
||||||
|
color: #64748b;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-event-time strong {
|
||||||
|
color: #334155;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-event-time em {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-event-rail {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-event-rail::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 17px;
|
top: 24px;
|
||||||
left: -18px;
|
bottom: -16px;
|
||||||
width: 9px;
|
left: 50%;
|
||||||
height: 9px;
|
width: 2px;
|
||||||
border: 2px solid #ffffff;
|
transform: translateX(-50%);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--theme-primary, #3a7ca5);
|
background: #e2e8f0;
|
||||||
box-shadow: 0 0 0 1px rgba(var(--theme-primary-rgb, 58, 124, 165), .34);
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-event:last-child .application-leader-opinion-event-rail::after {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-event.danger {
|
.application-leader-opinion-event.danger {
|
||||||
--leader-opinion-tone: #dc2626;
|
--leader-opinion-tone: #dc2626;
|
||||||
--leader-opinion-soft-bg: #fff7f7;
|
|
||||||
--leader-opinion-soft-border: #fecaca;
|
|
||||||
}
|
|
||||||
|
|
||||||
.application-leader-opinion-event.danger::before {
|
|
||||||
background: #dc2626;
|
|
||||||
box-shadow: 0 0 0 1px rgba(220, 38, 38, .32);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-event.success {
|
.application-leader-opinion-event.success {
|
||||||
--leader-opinion-tone: #16a34a;
|
--leader-opinion-tone: #16a34a;
|
||||||
--leader-opinion-soft-bg: #f0fdf4;
|
|
||||||
--leader-opinion-soft-border: #bbf7d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.application-leader-opinion-event.success::before {
|
|
||||||
background: #16a34a;
|
|
||||||
box-shadow: 0 0 0 1px rgba(22, 163, 74, .32);
|
|
||||||
}
|
|
||||||
|
|
||||||
.application-leader-opinion-event-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.application-leader-opinion-event-head span {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
color: #0f172a;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 850;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-event-status {
|
.application-leader-opinion-event-status {
|
||||||
min-height: 30px;
|
position: relative;
|
||||||
padding: 4px 10px;
|
z-index: 1;
|
||||||
border: 1px solid var(--leader-opinion-soft-border);
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--leader-opinion-tone);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--leader-opinion-soft-bg);
|
background: var(--leader-opinion-tone);
|
||||||
color: var(--leader-opinion-tone);
|
color: #ffffff;
|
||||||
|
box-shadow: 0 0 0 4px #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-event-head i {
|
.application-leader-opinion-event-status i {
|
||||||
color: currentColor;
|
font-size: 13px;
|
||||||
font-size: 16px;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-event.danger .application-leader-opinion-event-head i {
|
.application-leader-opinion-record {
|
||||||
color: #dc2626;
|
min-width: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.application-leader-opinion-event.success .application-leader-opinion-event-head i {
|
|
||||||
color: #16a34a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.application-leader-opinion-event-head time,
|
|
||||||
.application-leader-opinion-event footer {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 720;
|
|
||||||
}
|
|
||||||
|
|
||||||
.application-leader-opinion-event-head time {
|
|
||||||
padding: 4px 9px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #475569;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.application-leader-opinion-event-body {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 5px;
|
gap: 9px;
|
||||||
padding: 10px 12px;
|
padding: 0 0 16px;
|
||||||
border: 1px solid var(--leader-opinion-soft-border);
|
border-bottom: 1px solid #edf2f7;
|
||||||
border-radius: 4px;
|
background: #ffffff;
|
||||||
background: var(--leader-opinion-soft-bg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-event-body span {
|
.application-leader-opinion-event:last-child .application-leader-opinion-record {
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-record-head {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-record-title {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-record-title strong {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-record-title::after {
|
||||||
|
content: "";
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--leader-opinion-tone);
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-record-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-record-meta div {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-record-meta dt,
|
||||||
|
.application-leader-opinion-record-meta dd {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-record-meta dt {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-record-meta dd {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-record p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 9px 10px;
|
||||||
|
border: 1px solid #edf2f7;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-event-foot {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 850;
|
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-event-body p {
|
|
||||||
margin: 0;
|
|
||||||
color: #0f172a;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 850;
|
|
||||||
line-height: 1.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.application-leader-opinion-event footer {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.application-leader-opinion-event-foot span {
|
.application-leader-opinion-event-foot span {
|
||||||
min-height: 26px;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
color: inherit;
|
||||||
padding: 3px 9px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #475569;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.application-leader-opinion-event-foot i {
|
@media (max-width: 720px) {
|
||||||
color: var(--leader-opinion-tone);
|
.application-leader-opinion {
|
||||||
font-size: 14px;
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-event {
|
||||||
|
grid-template-columns: 76px 24px minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-event-status {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-event-time strong {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.application-leader-opinion-event-time em {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-expense-table {
|
.detail-expense-table {
|
||||||
|
|||||||
@@ -37,6 +37,42 @@ function formatDateTime(value) {
|
|||||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateLabel(value) {
|
||||||
|
const date = toDate(value)
|
||||||
|
if (!date) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimeLabel(value) {
|
||||||
|
const date = toDate(value)
|
||||||
|
if (!date) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveApprovalRole(event, returned) {
|
||||||
|
return resolveDisplayName(
|
||||||
|
event?.operator_position,
|
||||||
|
event?.operatorPosition,
|
||||||
|
event?.operator_title,
|
||||||
|
event?.operatorTitle,
|
||||||
|
event?.approver_position,
|
||||||
|
event?.approverPosition,
|
||||||
|
event?.approval_role,
|
||||||
|
event?.approvalRole,
|
||||||
|
event?.previous_approval_stage,
|
||||||
|
event?.previousApprovalStage
|
||||||
|
) || (returned ? '直属领导审批节点' : '直属领导')
|
||||||
|
}
|
||||||
|
|
||||||
function getRiskFlags(request) {
|
function getRiskFlags(request) {
|
||||||
const flags = request?.riskFlags || request?.risk_flags_json || []
|
const flags = request?.riskFlags || request?.risk_flags_json || []
|
||||||
return Array.isArray(flags) ? flags : []
|
return Array.isArray(flags) ? flags : []
|
||||||
@@ -102,6 +138,8 @@ export function buildLeaderApprovalEvents(request) {
|
|||||||
request?.managerName
|
request?.managerName
|
||||||
) || '直属领导'
|
) || '直属领导'
|
||||||
const time = formatDateTime(rawTime)
|
const time = formatDateTime(rawTime)
|
||||||
|
const dateLabel = formatDateLabel(rawTime) || '待记录'
|
||||||
|
const timeLabel = formatTimeLabel(rawTime)
|
||||||
const opinion = normalizeText(event.opinion)
|
const opinion = normalizeText(event.opinion)
|
||||||
|| normalizeText(event.leader_opinion || event.leaderOpinion)
|
|| normalizeText(event.leader_opinion || event.leaderOpinion)
|
||||||
|| normalizeText(event.reason)
|
|| normalizeText(event.reason)
|
||||||
@@ -115,7 +153,10 @@ export function buildLeaderApprovalEvents(request) {
|
|||||||
tone: returned ? 'danger' : 'success',
|
tone: returned ? 'danger' : 'success',
|
||||||
title: returned ? '领导退回' : '领导审批通过',
|
title: returned ? '领导退回' : '领导审批通过',
|
||||||
operator,
|
operator,
|
||||||
|
role: resolveApprovalRole(event, returned),
|
||||||
time,
|
time,
|
||||||
|
dateLabel,
|
||||||
|
timeLabel,
|
||||||
sortAt: rawTime,
|
sortAt: rawTime,
|
||||||
opinion,
|
opinion,
|
||||||
returnCount,
|
returnCount,
|
||||||
|
|||||||
@@ -164,24 +164,36 @@
|
|||||||
class="application-leader-opinion-event"
|
class="application-leader-opinion-event"
|
||||||
:class="event.tone"
|
:class="event.tone"
|
||||||
>
|
>
|
||||||
<div class="application-leader-opinion-event-head">
|
<time class="application-leader-opinion-event-time" :datetime="event.time || undefined">
|
||||||
<span class="application-leader-opinion-event-status">
|
<strong>{{ event.dateLabel }}</strong>
|
||||||
|
<em v-if="event.timeLabel">{{ event.timeLabel }}</em>
|
||||||
|
</time>
|
||||||
|
<div class="application-leader-opinion-event-rail">
|
||||||
|
<span class="application-leader-opinion-event-status" :title="event.title">
|
||||||
<i :class="event.type === 'returned' ? 'mdi mdi-arrow-u-left-top' : 'mdi mdi-check-circle-outline'"></i>
|
<i :class="event.type === 'returned' ? 'mdi mdi-arrow-u-left-top' : 'mdi mdi-check-circle-outline'"></i>
|
||||||
{{ event.title }}
|
|
||||||
</span>
|
</span>
|
||||||
<time v-if="event.time">{{ event.time }}</time>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="application-leader-opinion-event-body">
|
<div class="application-leader-opinion-record">
|
||||||
<span>意见</span>
|
<header class="application-leader-opinion-record-head">
|
||||||
|
<div class="application-leader-opinion-record-title">
|
||||||
|
<strong>{{ event.title }}</strong>
|
||||||
|
</div>
|
||||||
|
<dl class="application-leader-opinion-record-meta">
|
||||||
|
<div>
|
||||||
|
<dt>审批人</dt>
|
||||||
|
<dd>{{ event.operator }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>节点</dt>
|
||||||
|
<dd>{{ event.role }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</header>
|
||||||
<p>{{ event.opinion }}</p>
|
<p>{{ event.opinion }}</p>
|
||||||
|
<footer v-if="event.returnCount" class="application-leader-opinion-event-foot">
|
||||||
|
<span v-if="event.returnCount">第 {{ event.returnCount }} 次退回</span>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<footer class="application-leader-opinion-event-foot">
|
|
||||||
<span class="application-leader-opinion-operator">
|
|
||||||
<em>审批人</em>
|
|
||||||
{{ event.operator }}
|
|
||||||
</span>
|
|
||||||
<span v-if="event.returnCount">第 {{ event.returnCount }} 次退回</span>
|
|
||||||
</footer>
|
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function normalizeOcrDocuments(payload) {
|
|||||||
return documents.slice(0, MAX_OCR_DOCUMENTS).map((item) => ({
|
return documents.slice(0, MAX_OCR_DOCUMENTS).map((item) => ({
|
||||||
filename: item.filename,
|
filename: item.filename,
|
||||||
summary: item.summary,
|
summary: item.summary,
|
||||||
text: String(item.text || '').slice(0, 240),
|
text: String(item.text || ''),
|
||||||
avg_score: Number(item.avg_score || 0),
|
avg_score: Number(item.avg_score || 0),
|
||||||
line_count: Number(item.line_count || 0),
|
line_count: Number(item.line_count || 0),
|
||||||
document_type: String(item.document_type || 'other').trim() || 'other',
|
document_type: String(item.document_type || 'other').trim() || 'other',
|
||||||
|
|||||||
@@ -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.type), ['returned', 'approved'])
|
||||||
assert.deepEqual(events.map((event) => event.tone), ['danger', 'success'])
|
assert.deepEqual(events.map((event) => event.tone), ['danger', 'success'])
|
||||||
assert.equal(events[0].operator, 'Leader Li')
|
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].opinion, 'Need clearer budget explanation.')
|
||||||
assert.equal(events[0].returnCount, 1)
|
assert.equal(events[0].returnCount, 1)
|
||||||
assert.equal(events[0].time, '2026-05-25 09:00')
|
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)
|
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)
|
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 () => {
|
test('receipt files are collected through a single OCR persistence entry before draft association', async () => {
|
||||||
const files = [
|
const files = [
|
||||||
{ name: 'invoice.png' }
|
{ 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, /v-for="event in leaderApprovalEvents"/)
|
||||||
assert.match(detailTemplate, /class="application-leader-opinion-event"/)
|
assert.match(detailTemplate, /class="application-leader-opinion-event"/)
|
||||||
assert.match(detailTemplate, /event\.type === 'returned'/)
|
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-status"/)
|
||||||
assert.match(detailTemplate, /class="application-leader-opinion-event-body"/)
|
assert.match(detailTemplate, /:title="event\.title"/)
|
||||||
assert.match(detailTemplate, /审批意见/)
|
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-event-foot"/)
|
||||||
assert.match(detailTemplate, /class="application-leader-opinion-operator"/)
|
|
||||||
assert.doesNotMatch(detailTemplate, /leaderApprovalReadonlyText/)
|
assert.doesNotMatch(detailTemplate, /leaderApprovalReadonlyText/)
|
||||||
assert.doesNotMatch(detailTemplate, /\u5f85\u76f4\u5c5e\u9886\u5bfc\u586b\u5199\u5ba1\u6279\u610f\u89c1/)
|
assert.doesNotMatch(detailTemplate, /\u5f85\u76f4\u5c5e\u9886\u5bfc\u586b\u5199\u5ba1\u6279\u610f\u89c1/)
|
||||||
assert.match(detailTemplate, /领导意见/)
|
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-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(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 \{[\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;/)
|
||||||
assert.match(detailStyles, /\.application-leader-opinion-head span \{[\s\S]*display: inline-flex;[\s\S]*align-items: center;[\s\S]*gap: 8px;/)
|
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, /\.leader-approval-card/)
|
||||||
assert.doesNotMatch(detailStyles, /\.inline-leader-opinion/)
|
assert.doesNotMatch(detailStyles, /\.inline-leader-opinion/)
|
||||||
assert.match(detailStyles, /\.application-leader-opinion-timeline \{/)
|
assert.match(leaderOpinionTimelineRule, /gap: 0;/)
|
||||||
assert.match(detailStyles, /\.application-leader-opinion-timeline\.is-single \{[\s\S]*padding-left: 0;/)
|
assert.match(leaderOpinionTimelineRule, /padding: 2px 0 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(detailStyles, /\.application-leader-opinion-event \{/)
|
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(leaderOpinionEventRule, /grid-template-columns: 104px 28px minmax\(0, 1fr\);/)
|
||||||
assert.match(detailStyles, /\.application-leader-opinion-event-status \{[\s\S]*border-radius: 999px;/)
|
assert.match(leaderOpinionEventRule, /padding: 0 0 16px;/)
|
||||||
assert.match(detailStyles, /\.application-leader-opinion-event-body \{[\s\S]*background: var\(--leader-opinion-soft-bg/)
|
assert.doesNotMatch(leaderOpinionEventRule, /linear-gradient|box-shadow/)
|
||||||
assert.match(detailStyles, /\.application-leader-opinion-event-body p \{[\s\S]*font-size: 15px;/)
|
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-foot span \{[\s\S]*border-radius: 999px;/)
|
assert.match(detailStyles, /\.application-leader-opinion-event-rail \{[\s\S]*justify-content: center;/)
|
||||||
assert.match(detailStyles, /\.application-leader-opinion-event\.danger::before \{/)
|
assert.match(detailStyles, /\.application-leader-opinion-event-rail::after \{[\s\S]*width: 2px;[\s\S]*background: #e2e8f0;/)
|
||||||
assert.match(detailStyles, /\.application-leader-opinion-event\.success::before \{/)
|
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, /export function approveExpenseClaim\(claimId, payload = \{\}\)/)
|
||||||
assert.match(reimbursementService, /\/approve/)
|
assert.match(reimbursementService, /\/approve/)
|
||||||
|
|||||||
Reference in New Issue
Block a user