refactor: enforce 800 line source limits
This commit is contained in:
@@ -265,7 +265,6 @@ import {
|
||||
fetchAllApprovalExpenseClaims,
|
||||
fetchAllArchivedExpenseClaims
|
||||
} from '../services/reimbursements.js'
|
||||
import { countClaimRisks, resolveArchiveRiskTone } from '../utils/archiveCenterListFilters.js'
|
||||
import { fetchNotificationStates, patchNotificationStates } from '../services/notificationStates.js'
|
||||
import {
|
||||
buildDocumentViewedStatePatch,
|
||||
@@ -279,88 +278,16 @@ import {
|
||||
readViewedDocumentKeys,
|
||||
writeDocumentScope
|
||||
} from '../utils/documentCenterNewState.js'
|
||||
import { sortDocumentRowsByLatestTime } from '../utils/documentCenterSort.js'
|
||||
import { extractDateText, formatDocumentListTime, resolveDocumentSortTime, resolveDocumentStayTimeDisplay } from '../utils/documentCenterTime.js'
|
||||
import { excludeArchivedDocumentRows, filterApplicationScopeNewRows, isArchivedDocumentRow, prepareApplicationScopeRows } from '../utils/documentCenterRows.js'
|
||||
import { normalizeRequestForUi } from '../utils/requestViewModel.js'
|
||||
const DOCUMENT_TYPE_ALL = 'all'
|
||||
const DOCUMENT_TYPE_APPLICATION = 'application'
|
||||
const DOCUMENT_TYPE_REIMBURSEMENT = 'reimbursement'
|
||||
const SCENE_ALL = 'all'
|
||||
const DOCUMENT_SCOPE_ALL = '全部'
|
||||
const DOCUMENT_SCOPE_APPLICATION = '申请单'
|
||||
const DOCUMENT_SCOPE_REIMBURSEMENT = '报销单'
|
||||
const DOCUMENT_SCOPE_REVIEW = '审核单'
|
||||
const DOCUMENT_SCOPE_ARCHIVE = '归档'
|
||||
const scopeTabs = [DOCUMENT_SCOPE_ALL, DOCUMENT_SCOPE_APPLICATION, DOCUMENT_SCOPE_REIMBURSEMENT, DOCUMENT_SCOPE_REVIEW, DOCUMENT_SCOPE_ARCHIVE]
|
||||
const DOCUMENT_LOADING_MIN_VISIBLE_MS = 720
|
||||
const DOCUMENT_CENTER_QUERY_KEYS = new Set([
|
||||
'dc_page',
|
||||
'dc_page_size',
|
||||
'dc_scope',
|
||||
'dc_status',
|
||||
'dc_doc_type',
|
||||
'dc_scene',
|
||||
'dc_q',
|
||||
'dc_start',
|
||||
'dc_end'
|
||||
])
|
||||
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: riskLevelTabs,
|
||||
showDocumentType: true
|
||||
},
|
||||
[DOCUMENT_SCOPE_APPLICATION]: {
|
||||
searchPlaceholder: '搜索申请单号、申请事项、申请场景...',
|
||||
sceneFallbackLabel: '申请场景',
|
||||
dateLabel: '申请时间',
|
||||
statusTitle: '风险等级',
|
||||
statusTabs: riskLevelTabs,
|
||||
showDocumentType: false
|
||||
},
|
||||
[DOCUMENT_SCOPE_REIMBURSEMENT]: {
|
||||
searchPlaceholder: '搜索报销单号、报销事由、费用场景...',
|
||||
sceneFallbackLabel: '费用场景',
|
||||
dateLabel: '报销时间',
|
||||
statusTitle: '风险等级',
|
||||
statusTabs: riskLevelTabs,
|
||||
showDocumentType: false
|
||||
},
|
||||
[DOCUMENT_SCOPE_REVIEW]: {
|
||||
searchPlaceholder: '搜索审核单号、事项、当前环节...',
|
||||
sceneFallbackLabel: '审核场景',
|
||||
dateLabel: '审核时间',
|
||||
statusTitle: '风险等级',
|
||||
statusTabs: riskLevelTabs,
|
||||
showDocumentType: false
|
||||
},
|
||||
[DOCUMENT_SCOPE_ARCHIVE]: {
|
||||
searchPlaceholder: '搜索归档单号、事项、费用场景...',
|
||||
sceneFallbackLabel: '归档场景',
|
||||
dateLabel: '归档时间',
|
||||
statusTitle: '风险等级',
|
||||
statusTabs: riskLevelTabs,
|
||||
showDocumentType: false
|
||||
}
|
||||
}
|
||||
const pageSizeOptions = [10, 20, 50].map((size) => ({ label: `${size} 条/页`, value: size }))
|
||||
const pageSizeValues = pageSizeOptions.map((item) => item.value)
|
||||
const documentTypeOptions = [
|
||||
{ value: DOCUMENT_TYPE_ALL, label: '单据类型' },
|
||||
{ value: DOCUMENT_TYPE_APPLICATION, label: '申请单' },
|
||||
{ value: DOCUMENT_TYPE_REIMBURSEMENT, label: '报销单' }
|
||||
]
|
||||
import { excludeArchivedDocumentRows, filterApplicationScopeNewRows, prepareApplicationScopeRows } from '../utils/documentCenterRows.js'
|
||||
import {
|
||||
DOCUMENT_CENTER_QUERY_KEYS, DOCUMENT_LOADING_MIN_VISIBLE_MS, DOCUMENT_SCOPE_ALL,
|
||||
DOCUMENT_SCOPE_APPLICATION, DOCUMENT_SCOPE_ARCHIVE, DOCUMENT_SCOPE_REIMBURSEMENT,
|
||||
DOCUMENT_SCOPE_REVIEW, DOCUMENT_TYPE_ALL, DOCUMENT_TYPE_APPLICATION,
|
||||
DOCUMENT_TYPE_REIMBURSEMENT, FILTER_CONFIG_BY_SCOPE, SCENE_ALL,
|
||||
buildDocumentCenterEmptyState, buildDocumentRow, documentTypeOptions,
|
||||
filterDocumentRows, hasDocumentCenterActiveFilters, mergeDocumentRows,
|
||||
pageSizeOptions, pageSizeValues, routeQueryEquals, scopeTabs
|
||||
} from '../utils/documentCenterViewModel.js'
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const props = defineProps({
|
||||
@@ -440,14 +367,6 @@ function buildDocumentCenterRouteQuery() {
|
||||
return nextQuery
|
||||
}
|
||||
|
||||
function routeQueryEquals(left, right) {
|
||||
const leftEntries = Object.entries(left || {}).map(([key, value]) => [key, Array.isArray(value) ? value.join(',') : String(value ?? '')])
|
||||
const rightEntries = Object.entries(right || {}).map(([key, value]) => [key, Array.isArray(value) ? value.join(',') : String(value ?? '')])
|
||||
if (leftEntries.length !== rightEntries.length) return false
|
||||
const rightMap = new Map(rightEntries)
|
||||
return leftEntries.every(([key, value]) => rightMap.get(key) === value)
|
||||
}
|
||||
|
||||
const initialScopeTab = resolveInitialScopeTab()
|
||||
const initialAppliedStart = readDocumentCenterQueryText('dc_start')
|
||||
const initialAppliedEnd = readDocumentCenterQueryText('dc_end')
|
||||
@@ -494,7 +413,11 @@ const dateRangeLabel = computed(() => {
|
||||
const ownedRows = computed(() =>
|
||||
excludeArchivedDocumentRows(
|
||||
props.filteredRequests
|
||||
.map((item) => buildDocumentRow(item, { source: 'owned' }))
|
||||
.map((item) => buildDocumentRow(item, {
|
||||
source: 'owned',
|
||||
currentUser: currentUser.value,
|
||||
viewedDocumentKeys: viewedDocumentKeys.value
|
||||
}))
|
||||
.filter(Boolean)
|
||||
)
|
||||
)
|
||||
@@ -570,33 +493,16 @@ const statusFilterLabel = computed(() =>
|
||||
statusFilterOptions.value.find((item) => item.value === activeStatusTab.value)?.label || '全部风险'
|
||||
)
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const keyword = listKeyword.value.trim().toLowerCase()
|
||||
|
||||
return sortDocumentRowsByLatestTime(activeScopeRows.value.filter((row) => {
|
||||
const matchesKeyword = !keyword || [
|
||||
row.documentNo,
|
||||
row.documentTypeLabel,
|
||||
row.typeLabel,
|
||||
row.initiatorName,
|
||||
row.reason,
|
||||
row.node,
|
||||
row.statusLabel,
|
||||
row.riskLabel
|
||||
].filter(Boolean).join('').toLowerCase().includes(keyword)
|
||||
|
||||
const matchesDocumentType =
|
||||
!showDocumentTypeFilter.value
|
||||
|| activeDocumentType.value === DOCUMENT_TYPE_ALL
|
||||
|| row.documentTypeCode === activeDocumentType.value
|
||||
|
||||
const matchesScene = activeScene.value === SCENE_ALL || row.typeCode === activeScene.value
|
||||
const matchesRiskLevel = matchesRiskLevelTab(row, activeStatusTab.value)
|
||||
const matchesDateRange = matchesAppliedDateRange(row)
|
||||
|
||||
return matchesKeyword && matchesDocumentType && matchesScene && matchesRiskLevel && matchesDateRange
|
||||
}))
|
||||
})
|
||||
const filteredRows = computed(() => filterDocumentRows(activeScopeRows.value, {
|
||||
keyword: listKeyword.value,
|
||||
showDocumentTypeFilter: showDocumentTypeFilter.value,
|
||||
activeDocumentType: activeDocumentType.value,
|
||||
activeScene: activeScene.value,
|
||||
activeStatusTab: activeStatusTab.value,
|
||||
activeScopeTab: activeScopeTab.value,
|
||||
appliedStart: appliedStart.value,
|
||||
appliedEnd: appliedEnd.value
|
||||
}))
|
||||
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(filteredRows.value.length / pageSize.value)))
|
||||
const pageSummary = computed(() => `共 ${filteredRows.value.length} 条,目前第 ${currentPage.value} / ${totalPages.value} 页`)
|
||||
@@ -636,229 +542,22 @@ const documentSummary = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const emptyState = computed(() => {
|
||||
const filtered = hasActiveFilters()
|
||||
if (
|
||||
activeScopeTab.value === DOCUMENT_SCOPE_APPLICATION
|
||||
|| activeDocumentType.value === DOCUMENT_TYPE_APPLICATION
|
||||
) {
|
||||
return {
|
||||
eyebrow: '申请单',
|
||||
title: '当前还没有申请单数据',
|
||||
desc: '费用申请功能接入后,差旅、会务、办公采购等前置申请会统一汇总到这里。',
|
||||
icon: 'mdi mdi-file-sign-outline',
|
||||
actionLabel: '',
|
||||
actionIcon: '',
|
||||
tone: 'theme',
|
||||
artLabel: 'APPLY',
|
||||
tips: ['申请、报销、审批与归档统一在此查看', '申请批准后可继续发起报销']
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
eyebrow: filtered ? '筛选结果为空' : '单据中心',
|
||||
title: filtered ? '没有符合当前条件的单据' : `“${activeScopeTab.value}”里暂时没有单据`,
|
||||
desc: filtered
|
||||
? '可以清空当前分类下的筛选条件后再看看。'
|
||||
: '当前视角暂无可展示单据,可以切换其他视角或发起一笔报销。',
|
||||
icon: filtered ? 'mdi mdi-magnify-scan' : 'mdi mdi-file-document-multiple-outline',
|
||||
actionLabel: '',
|
||||
actionIcon: '',
|
||||
tone: 'theme',
|
||||
artLabel: filtered ? 'FILTER' : 'DOCS',
|
||||
tips: ['单据中心已接入当前报销单据', '归档视角会同步已归档数据']
|
||||
}
|
||||
})
|
||||
|
||||
function resolveArchivedDocumentNode(normalized, documentTypeCode) {
|
||||
if (documentTypeCode === DOCUMENT_TYPE_APPLICATION) {
|
||||
return '申请归档'
|
||||
}
|
||||
if (normalized.status === 'paid' || normalized.approvalStatus === '已付款') {
|
||||
return '已付款'
|
||||
}
|
||||
return normalized.node || normalized.workflowNode || '财务归档'
|
||||
}
|
||||
|
||||
function resolveArchivedStatusLabel(normalized) {
|
||||
if (normalized.status === 'paid' || normalized.approvalStatus === '已付款' || normalized.node === '已付款') {
|
||||
return '已付款'
|
||||
}
|
||||
return '已归档'
|
||||
}
|
||||
|
||||
function buildDocumentRow(request, options = {}) {
|
||||
const normalized = normalizeRequestForUi(request)
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
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
|
||||
const updatedAtSource = normalized.updatedAt || normalized.submittedAt || normalized.createdAt || normalized.applyTime
|
||||
const createdSortTime = resolveDocumentSortTime(createdAtSource)
|
||||
const updatedSortTime = resolveDocumentSortTime(updatedAtSource)
|
||||
const documentTypeCode = normalized.documentTypeCode || DOCUMENT_TYPE_REIMBURSEMENT
|
||||
const documentTypeLabel =
|
||||
normalized.documentTypeLabel
|
||||
|| (documentTypeCode === DOCUMENT_TYPE_APPLICATION ? '申请单' : '报销单')
|
||||
const initiatorName = String(
|
||||
normalized.person
|
||||
|| normalized.employeeName
|
||||
|| normalized.profileName
|
||||
|| normalized.applicant
|
||||
|| request?.employee_name
|
||||
|| request?.employeeName
|
||||
|| request?.person
|
||||
|| ''
|
||||
).trim() || '待补充'
|
||||
|
||||
return {
|
||||
...normalized,
|
||||
rawRequest: request,
|
||||
documentKey: `${options.source || 'owned'}:${claimId || documentNo}`,
|
||||
documentTypeCode,
|
||||
documentTypeLabel,
|
||||
claimId,
|
||||
documentNo,
|
||||
initiatorName,
|
||||
node: archived ? resolveArchivedDocumentNode(normalized, documentTypeCode) : (normalized.node || normalized.workflowNode || '待提交'),
|
||||
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),
|
||||
stayTimeDisplay: resolveDocumentStayTimeDisplay(normalized),
|
||||
isNewDocument: archived
|
||||
? false
|
||||
: isNewDocument({ ...normalized, source: options.source || 'owned', claimId, documentNo }, viewedDocumentKeys.value),
|
||||
updatedAtDisplay: formatDocumentListTime(updatedAtSource),
|
||||
createdSortTime,
|
||||
updatedSortTime,
|
||||
sortTime: Math.max(createdSortTime, updatedSortTime)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveStatusGroup(row, archived) {
|
||||
if (archived) return 'completed'
|
||||
if (row.approvalKey === 'draft') return 'draft'
|
||||
if (row.approvalKey === 'supplement' && row.status === 'returned') return 'pending_submit'
|
||||
if (row.approvalKey === 'supplement') return 'supplement'
|
||||
if (row.approvalKey === 'pending_payment') return 'pending_payment'
|
||||
if (row.approvalKey === 'in_progress') return 'in_progress'
|
||||
if (row.approvalKey === 'completed') return 'completed'
|
||||
return 'other'
|
||||
}
|
||||
|
||||
function resolveStatusLabel(row, statusGroup) {
|
||||
if (statusGroup === 'pending_submit') return '待提交'
|
||||
if (statusGroup === 'pending_payment') return '待付款'
|
||||
return row.approval || row.approvalStatus || '处理中'
|
||||
}
|
||||
|
||||
function resolveStatusTone(row, statusGroup) {
|
||||
if (statusGroup === 'pending_submit') return 'warning'
|
||||
return row.approvalTone || 'neutral'
|
||||
}
|
||||
|
||||
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 viewerOptions = currentUser.value
|
||||
? { request: row || {}, currentUser: currentUser.value }
|
||||
: null
|
||||
const count = countClaimRisks(riskFlags, riskSummary, viewerOptions)
|
||||
if (!count) {
|
||||
const meta = RISK_TONE_META.none
|
||||
return {
|
||||
...meta,
|
||||
count: 0,
|
||||
tags: [{ ...meta }]
|
||||
}
|
||||
}
|
||||
|
||||
const tone = resolveArchiveRiskTone(riskFlags, riskSummary, viewerOptions)
|
||||
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.riskTone === 'high'
|
||||
if (tab === '中风险') return row.riskTone === 'medium'
|
||||
if (tab === '低风险') return row.riskTone === 'low'
|
||||
if (tab === '无风险') return row.riskTone === 'none'
|
||||
return true
|
||||
}
|
||||
|
||||
function matchesAppliedDateRange(row) {
|
||||
if (!appliedStart.value || !appliedEnd.value) {
|
||||
return true
|
||||
}
|
||||
|
||||
const date = extractDateText(row.updatedAt || row.submittedAt || row.createdAt || row.applyTime)
|
||||
return Boolean(date) && date >= appliedStart.value && date <= appliedEnd.value
|
||||
}
|
||||
|
||||
function mergeDocumentRows(rows) {
|
||||
const rowMap = new Map()
|
||||
|
||||
rows.filter(Boolean).forEach((row) => {
|
||||
const key = row.claimId || row.documentNo || row.documentKey
|
||||
const current = rowMap.get(key)
|
||||
if (!current || resolveSourcePriority(row) >= resolveSourcePriority(current)) {
|
||||
rowMap.set(key, row)
|
||||
}
|
||||
})
|
||||
|
||||
return sortDocumentRowsByLatestTime(Array.from(rowMap.values()))
|
||||
}
|
||||
|
||||
function resolveSourcePriority(row) {
|
||||
if (row.archived) return 3
|
||||
if (row.source === 'approval') return 2
|
||||
return 1
|
||||
}
|
||||
const emptyState = computed(() => buildDocumentCenterEmptyState({
|
||||
hasActiveFilters: hasActiveFilters(),
|
||||
activeScopeTab: activeScopeTab.value,
|
||||
activeDocumentType: activeDocumentType.value
|
||||
}))
|
||||
|
||||
function hasActiveFilters() {
|
||||
return Boolean(
|
||||
listKeyword.value.trim()
|
||||
|| activeStatusTab.value !== '全部'
|
||||
|| (showDocumentTypeFilter.value && activeDocumentType.value !== DOCUMENT_TYPE_ALL)
|
||||
|| activeScene.value !== SCENE_ALL
|
||||
|| appliedStart.value
|
||||
|| appliedEnd.value
|
||||
)
|
||||
return hasDocumentCenterActiveFilters({
|
||||
listKeyword: listKeyword.value,
|
||||
activeStatusTab: activeStatusTab.value,
|
||||
showDocumentTypeFilter: showDocumentTypeFilter.value,
|
||||
activeDocumentType: activeDocumentType.value,
|
||||
activeScene: activeScene.value,
|
||||
appliedStart: appliedStart.value,
|
||||
appliedEnd: appliedEnd.value
|
||||
})
|
||||
}
|
||||
|
||||
function toggleFilter(key) {
|
||||
@@ -993,7 +692,11 @@ async function loadSupportingRows() {
|
||||
approvalRows.value = excludeArchivedDocumentRows(
|
||||
extractExpenseClaimItems(approvalResult.value)
|
||||
.map((item) => mapExpenseClaimToRequest(item))
|
||||
.map((item) => buildDocumentRow(item, { source: 'approval' }))
|
||||
.map((item) => buildDocumentRow(item, {
|
||||
source: 'approval',
|
||||
currentUser: currentUser.value,
|
||||
viewedDocumentKeys: viewedDocumentKeys.value
|
||||
}))
|
||||
.filter(Boolean)
|
||||
)
|
||||
} else {
|
||||
@@ -1003,7 +706,12 @@ async function loadSupportingRows() {
|
||||
if (archiveResult.status === 'fulfilled') {
|
||||
archiveRows.value = extractExpenseClaimItems(archiveResult.value)
|
||||
.map((item) => mapExpenseClaimToRequest(item))
|
||||
.map((item) => buildDocumentRow(item, { source: 'archive', archived: true }))
|
||||
.map((item) => buildDocumentRow(item, {
|
||||
source: 'archive',
|
||||
archived: true,
|
||||
currentUser: currentUser.value,
|
||||
viewedDocumentKeys: viewedDocumentKeys.value
|
||||
}))
|
||||
.filter(Boolean)
|
||||
} else {
|
||||
archiveRows.value = []
|
||||
|
||||
Reference in New Issue
Block a user