feat(web): 优化差旅详情、风险建议卡片与文档中心交互

- 拆分阶段风险建议卡片样式到独立文件
- 完善差旅申请审批对话框与详情视图交互
- 调整文档中心列表共享样式与状态筛选
- 同步应用外壳、视图初始化与系统状态 composables
This commit is contained in:
caoxiaozhu
2026-06-17 14:39:12 +08:00
parent a3e5295915
commit 0fac8b615f
19 changed files with 1415 additions and 558 deletions

View File

@@ -41,7 +41,7 @@
v-if="openFilterKey === 'status'"
class="document-filter-menu status-filter-menu"
role="listbox"
aria-label="单据状态"
aria-label="风险等级"
>
<button
v-for="option in statusFilterOptions"
@@ -187,7 +187,7 @@
<col class="col-title">
<col class="col-amount">
<col class="col-node">
<col class="col-status">
<col class="col-risk">
<col class="col-updated">
</colgroup>
<thead>
@@ -201,7 +201,7 @@
<th>事项</th>
<th>金额</th>
<th>当前环节</th>
<th>状态</th>
<th>风险等级</th>
<th>更新时间</th>
</tr>
</thead>
@@ -219,7 +219,18 @@
<td data-label="事项">{{ row.reason }}</td>
<td data-label="金额">{{ row.amountDisplay }}</td>
<td data-label="当前环节">{{ row.node }}</td>
<td data-label="状态"><span class="status-tag" :class="row.statusTone">{{ row.statusLabel }}</span></td>
<td data-label="风险等级">
<span class="risk-level-tags">
<span
v-for="tag in row.riskTags"
:key="tag.label"
class="risk-level-tag"
:class="tag.tone"
>
{{ tag.label }}
</span>
</span>
</td>
<td data-label="更新时间">{{ row.updatedAtDisplay }}</td>
</tr>
</tbody>
@@ -253,6 +264,7 @@ import {
fetchAllApprovalExpenseClaims,
fetchAllArchivedExpenseClaims
} from '../services/reimbursements.js'
import { countClaimRisks, resolveArchiveRiskTone } from '../utils/archiveCenterListFilters.js'
import { fetchNotificationStates, patchNotificationStates } from '../services/notificationStates.js'
import {
buildDocumentViewedStatePatch,
@@ -292,46 +304,52 @@ const DOCUMENT_CENTER_QUERY_KEYS = new Set([
'dc_start',
'dc_end'
])
const statusTabs = ['全部', '草稿', '待提交', '审批中', '待补充', '待付款', '已完成']
const riskLevelTabs = ['全部', '高风险', '中风险', '低风险', '无风险']
const RISK_TONE_META = {
high: { label: '高风险', tone: 'high' },
medium: { label: '中风险', tone: 'medium' },
low: { label: '低风险', tone: 'low' },
none: { label: '无风险', tone: 'none' }
}
const FILTER_CONFIG_BY_SCOPE = {
[DOCUMENT_SCOPE_ALL]: {
searchPlaceholder: '搜索单号、事项、费用场景...',
sceneFallbackLabel: '单据场景',
dateLabel: '单据时间',
statusTitle: '单据状态',
statusTabs,
statusTitle: '风险等级',
statusTabs: riskLevelTabs,
showDocumentType: true
},
[DOCUMENT_SCOPE_APPLICATION]: {
searchPlaceholder: '搜索申请单号、申请事项、申请场景...',
sceneFallbackLabel: '申请场景',
dateLabel: '申请时间',
statusTitle: '申请状态',
statusTabs: ['全部', '草稿', '审批中', '已完成'],
statusTitle: '风险等级',
statusTabs: riskLevelTabs,
showDocumentType: false
},
[DOCUMENT_SCOPE_REIMBURSEMENT]: {
searchPlaceholder: '搜索报销单号、报销事由、费用场景...',
sceneFallbackLabel: '费用场景',
dateLabel: '报销时间',
statusTitle: '报销状态',
statusTabs,
statusTitle: '风险等级',
statusTabs: riskLevelTabs,
showDocumentType: false
},
[DOCUMENT_SCOPE_REVIEW]: {
searchPlaceholder: '搜索审核单号、事项、当前环节...',
sceneFallbackLabel: '审核场景',
dateLabel: '审核时间',
statusTitle: '审核状态',
statusTabs: ['全部', '审批中', '待补充', '已完成'],
statusTitle: '风险等级',
statusTabs: riskLevelTabs,
showDocumentType: false
},
[DOCUMENT_SCOPE_ARCHIVE]: {
searchPlaceholder: '搜索归档单号、事项、费用场景...',
sceneFallbackLabel: '归档场景',
dateLabel: '归档时间',
statusTitle: '归档状态',
statusTabs: ['全部', '已付款', '已完成'],
statusTitle: '风险等级',
statusTabs: riskLevelTabs,
showDocumentType: false
}
}
@@ -458,7 +476,7 @@ const documentTypeFilterLabel = computed(() =>
const statusFilterOptions = computed(() =>
activeFilterConfig.value.statusTabs.map((tab) => ({
value: tab,
label: tab === '全部' ? '全部状态' : tab
label: tab === '全部' ? '全部风险' : tab
}))
)
@@ -546,7 +564,7 @@ const sceneFilterLabel = computed(() =>
)
const statusFilterLabel = computed(() =>
statusFilterOptions.value.find((item) => item.value === activeStatusTab.value)?.label || '全部状态'
statusFilterOptions.value.find((item) => item.value === activeStatusTab.value)?.label || '全部风险'
)
const filteredRows = computed(() => {
@@ -560,7 +578,8 @@ const filteredRows = computed(() => {
row.initiatorName,
row.reason,
row.node,
row.statusLabel
row.statusLabel,
row.riskLabel
].filter(Boolean).join('').toLowerCase().includes(keyword)
const matchesDocumentType =
@@ -569,10 +588,10 @@ const filteredRows = computed(() => {
|| row.documentTypeCode === activeDocumentType.value
const matchesScene = activeScene.value === SCENE_ALL || row.typeCode === activeScene.value
const matchesStatus = matchesStatusTab(row, activeStatusTab.value)
const matchesRiskLevel = matchesRiskLevelTab(row, activeStatusTab.value)
const matchesDateRange = matchesAppliedDateRange(row)
return matchesKeyword && matchesDocumentType && matchesScene && matchesStatus && matchesDateRange
return matchesKeyword && matchesDocumentType && matchesScene && matchesRiskLevel && matchesDateRange
}))
})
@@ -674,6 +693,7 @@ function buildDocumentRow(request, options = {}) {
const archived = Boolean(options.archived)
const statusGroup = resolveStatusGroup(normalized, archived)
const statusLabel = archived ? resolveArchivedStatusLabel(normalized) : resolveStatusLabel(normalized, statusGroup)
const riskMeta = buildDocumentRiskMeta(normalized)
const documentNo = normalized.documentNo || normalized.id || normalized.claimId || '待生成'
const claimId = normalized.claimId || normalized.id || documentNo
const createdAtSource = normalized.createdAt || normalized.submittedAt || normalized.applyTime || normalized.updatedAt
@@ -708,6 +728,10 @@ function buildDocumentRow(request, options = {}) {
statusGroup,
statusLabel,
statusTone: archived ? 'archived' : resolveStatusTone(normalized, statusGroup),
riskTone: riskMeta.tone,
riskLabel: riskMeta.label,
riskCount: riskMeta.count,
riskTags: riskMeta.tags,
source: options.source || 'owned',
archived,
createdAtDisplay: formatDocumentListTime(createdAtSource),
@@ -744,19 +768,48 @@ function resolveStatusTone(row, statusGroup) {
return row.approvalTone || 'neutral'
}
function matchesStatusTab(row, tab) {
function resolveDocumentRiskFlags(row) {
if (Array.isArray(row?.riskFlags)) {
return row.riskFlags
}
if (Array.isArray(row?.risk_flags_json)) {
return row.risk_flags_json
}
return []
}
function buildDocumentRiskMeta(row) {
const riskFlags = resolveDocumentRiskFlags(row)
const riskSummary = row?.riskSummary || row?.risk
const count = countClaimRisks(riskFlags, riskSummary)
if (!count) {
const meta = RISK_TONE_META.none
return {
...meta,
count: 0,
tags: [{ ...meta }]
}
}
const tone = resolveArchiveRiskTone(riskFlags, riskSummary)
const meta = RISK_TONE_META[tone] || RISK_TONE_META.medium
return {
...meta,
count,
tags: [{ tone: meta.tone, label: `${meta.label} ${count}` }]
}
}
function matchesRiskLevelTab(row, tab) {
if (activeScopeTab.value !== DOCUMENT_SCOPE_ARCHIVE && isArchivedDocumentRow(row)) {
return false
}
if (tab === '全部') return true
if (tab === '草稿') return row.statusGroup === 'draft'
if (tab === '待提交') return row.statusGroup === 'pending_submit'
if (tab === '审批中') return row.statusGroup === 'in_progress'
if (tab === '待补充') return row.statusGroup === 'supplement'
if (tab === '待付款') return row.statusGroup === 'pending_payment'
if (tab === '已付款') return row.statusLabel === '已付款' || row.node === '已付款'
if (tab === '已完成') return row.statusGroup === 'completed'
if (tab === '高风险') return row.riskTone === 'high'
if (tab === '中风险') return row.riskTone === 'medium'
if (tab === '低风险') return row.riskTone === 'low'
if (tab === '无风险') return row.riskTone === 'none'
return true
}