feat: 完善文档中心与报销申请交互及侧边栏重构

后端优化编排器报销查询和本体检测精度,增强报销单草稿保
存和附件回填逻辑,前端重构侧边栏组件支持折叠和图标导
航,完善文档中心状态筛选和详情提示,报销创建和审批详情
页优化会话管理和费用明细交互,新增助手应用服务和预设动
作工具函数,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-25 13:35:39 +08:00
parent 50b1c3f9a9
commit d0e946cf47
59 changed files with 5117 additions and 416 deletions

View File

@@ -193,7 +193,10 @@
</thead>
<tbody>
<tr v-for="row in visibleRows" :key="row.documentKey" @click="openDocument(row)">
<td><strong class="doc-id">{{ row.documentNo }}</strong></td>
<td>
<span v-if="row.isNewDocument" class="new-document-badge">NEW</span>
<strong class="doc-id">{{ row.documentNo }}</strong>
</td>
<td>{{ row.createdAtDisplay }}</td>
<td v-if="showStayTimeColumn">{{ row.stayTimeDisplay }}</td>
<td><span class="doc-kind-tag" :class="row.documentTypeCode">{{ row.documentTypeLabel }}</span></td>
@@ -259,31 +262,31 @@ import TableEmptyState from '../components/shared/TableEmptyState.vue'
import TableLoadingState from '../components/shared/TableLoadingState.vue'
import { mapExpenseClaimToRequest } from '../composables/useRequests.js'
import { fetchApprovalExpenseClaims, fetchArchivedExpenseClaims } from '../services/reimbursements.js'
import {
extractDateText,
formatDocumentListTime,
resolveDocumentSortTime,
resolveDocumentStayTimeDisplay
} from '../utils/documentCenterTime.js'
import { countNewDocuments, isNewDocument, markDocumentViewed, readDocumentScope, readViewedDocumentKeys, writeDocumentScope } from '../utils/documentCenterNewState.js'
import { extractDateText, formatDocumentListTime, resolveDocumentSortTime, resolveDocumentStayTimeDisplay } from '../utils/documentCenterTime.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_APPLICATION,
DOCUMENT_SCOPE_REIMBURSEMENT,
DOCUMENT_SCOPE_REVIEW,
DOCUMENT_SCOPE_ARCHIVE
]
const scopeTabs = [DOCUMENT_SCOPE_ALL, DOCUMENT_SCOPE_APPLICATION, DOCUMENT_SCOPE_REIMBURSEMENT, DOCUMENT_SCOPE_REVIEW, DOCUMENT_SCOPE_ARCHIVE]
const statusTabs = ['全部', '草稿', '待提交', '审批中', '待补充', '已完成']
const FILTER_CONFIG_BY_SCOPE = {
[DOCUMENT_SCOPE_ALL]: {
searchPlaceholder: '搜索单号、事项、费用场景...',
sceneFallbackLabel: '单据场景',
dateLabel: '单据时间',
statusTitle: '单据状态',
statusTabs,
showDocumentType: true
},
[DOCUMENT_SCOPE_APPLICATION]: {
searchPlaceholder: '搜索申请单号、申请事项、申请场景...',
sceneFallbackLabel: '申请场景',
@@ -339,7 +342,7 @@ const emit = defineEmits([
'summary-change'
])
const activeScopeTab = ref(DOCUMENT_SCOPE_APPLICATION)
const activeScopeTab = ref(readDocumentScope(DOCUMENT_SCOPE_ALL, scopeTabs))
const activeStatusTab = ref('全部')
const activeDocumentType = ref(DOCUMENT_TYPE_ALL)
const activeScene = ref(SCENE_ALL)
@@ -357,6 +360,7 @@ const archiveRows = ref([])
const approvalRows = ref([])
const supportingLoading = ref(false)
const supportingError = ref('')
const viewedDocumentKeys = ref(readViewedDocumentKeys())
const activeFilterConfig = computed(() =>
FILTER_CONFIG_BY_SCOPE[activeScopeTab.value] || FILTER_CONFIG_BY_SCOPE[DOCUMENT_SCOPE_APPLICATION]
@@ -389,13 +393,14 @@ const ownedRows = computed(() =>
.filter(Boolean)
)
const allSummaryRows = computed(() => mergeDocumentRows([...ownedRows.value, ...approvalRows.value, ...archiveRows.value]))
const nonArchivedRows = computed(() => mergeDocumentRows([...ownedRows.value, ...approvalRows.value]))
const scopeNewCountMap = computed(() => ({
[DOCUMENT_SCOPE_APPLICATION]: allSummaryRows.value.filter((row) => row.documentTypeCode === DOCUMENT_TYPE_APPLICATION).length,
[DOCUMENT_SCOPE_REIMBURSEMENT]: ownedRows.value.filter((row) => row.documentTypeCode === DOCUMENT_TYPE_REIMBURSEMENT).length,
[DOCUMENT_SCOPE_REVIEW]: approvalRows.value.length,
[DOCUMENT_SCOPE_ARCHIVE]: archiveRows.value.length
[DOCUMENT_SCOPE_ALL]: countNewDocuments(nonArchivedRows.value, viewedDocumentKeys.value),
[DOCUMENT_SCOPE_APPLICATION]: countNewDocuments(nonArchivedRows.value.filter((row) => row.documentTypeCode === DOCUMENT_TYPE_APPLICATION), viewedDocumentKeys.value),
[DOCUMENT_SCOPE_REIMBURSEMENT]: countNewDocuments(ownedRows.value.filter((row) => row.documentTypeCode === DOCUMENT_TYPE_REIMBURSEMENT), viewedDocumentKeys.value),
[DOCUMENT_SCOPE_REVIEW]: countNewDocuments(approvalRows.value, viewedDocumentKeys.value),
[DOCUMENT_SCOPE_ARCHIVE]: countNewDocuments(archiveRows.value, viewedDocumentKeys.value)
}))
const scopeTabItems = computed(() =>
@@ -407,8 +412,10 @@ const scopeTabItems = computed(() =>
)
const activeScopeRows = computed(() => {
if (activeScopeTab.value === DOCUMENT_SCOPE_ALL) return nonArchivedRows.value
if (activeScopeTab.value === DOCUMENT_SCOPE_APPLICATION) {
return allSummaryRows.value.filter((row) => row.documentTypeCode === DOCUMENT_TYPE_APPLICATION)
return nonArchivedRows.value.filter((row) => row.documentTypeCode === DOCUMENT_TYPE_APPLICATION)
}
if (activeScopeTab.value === DOCUMENT_SCOPE_REIMBURSEMENT) {
@@ -423,7 +430,7 @@ const activeScopeRows = computed(() => {
return archiveRows.value
}
return allSummaryRows.value.filter((row) => row.documentTypeCode === DOCUMENT_TYPE_APPLICATION)
return nonArchivedRows.value
})
const sceneFilterOptions = computed(() => {
@@ -487,7 +494,7 @@ const showStayTimeColumn = computed(() =>
)
const documentSummary = computed(() => {
const rows = allSummaryRows.value
const rows = nonArchivedRows.value
return {
total: rows.length,
toSubmit: rows.filter((row) => ['draft', 'pending_submit'].includes(row.statusGroup)).length,
@@ -507,9 +514,9 @@ const emptyState = computed(() => {
title: '当前还没有申请单数据',
desc: '费用申请功能接入后,差旅、会务、办公采购等前置申请会统一汇总到这里。',
icon: 'mdi mdi-file-sign-outline',
actionLabel: '发起申请',
actionIcon: 'mdi mdi-file-plus-outline',
tone: 'sky',
actionLabel: '',
actionIcon: '',
tone: 'emerald',
artLabel: 'APPLY',
tips: ['旧报销中心仍保留', '申请批准后可继续发起报销']
}
@@ -522,9 +529,9 @@ const emptyState = computed(() => {
? '可以清空当前分类下的筛选条件后再看看。'
: '当前视角暂无可展示单据,可以切换其他视角或发起一笔报销。',
icon: filtered ? 'mdi mdi-magnify-scan' : 'mdi mdi-file-document-multiple-outline',
actionLabel: filtered ? '清空筛选' : '发起报销',
actionIcon: filtered ? 'mdi mdi-filter-remove-outline' : 'mdi mdi-plus-circle-outline',
tone: filtered ? 'sky' : 'emerald',
actionLabel: '',
actionIcon: '',
tone: 'emerald',
artLabel: filtered ? 'FILTER' : 'DOCS',
tips: ['单据中心已接入当前报销单据', '归档视角会同步归档中心数据']
}
@@ -543,13 +550,17 @@ function buildDocumentRow(request, options = {}) {
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 documentTypeCode = normalized.documentTypeCode || DOCUMENT_TYPE_REIMBURSEMENT
const documentTypeLabel =
normalized.documentTypeLabel
|| (documentTypeCode === DOCUMENT_TYPE_APPLICATION ? '申请单' : '报销单')
return {
...normalized,
rawRequest: request,
documentKey: `${options.source || 'owned'}:${claimId || documentNo}`,
documentTypeCode: DOCUMENT_TYPE_REIMBURSEMENT,
documentTypeLabel: '报销单',
documentTypeCode,
documentTypeLabel,
claimId,
documentNo,
node: archived ? '财务归档' : (normalized.node || normalized.workflowNode || '待提交'),
@@ -560,6 +571,7 @@ function buildDocumentRow(request, options = {}) {
archived,
createdAtDisplay: formatDocumentListTime(createdAtSource),
stayTimeDisplay: resolveDocumentStayTimeDisplay(normalized),
isNewDocument: isNewDocument({ ...normalized, source: options.source || 'owned', claimId, documentNo }, viewedDocumentKeys.value),
updatedAtDisplay: formatDocumentListTime(updatedAtSource),
sortTime: resolveDocumentSortTime(updatedAtSource)
}
@@ -703,6 +715,8 @@ function changePageSize(size) {
}
function openDocument(row) {
writeDocumentScope(activeScopeTab.value, scopeTabs)
viewedDocumentKeys.value = markDocumentViewed(row, viewedDocumentKeys.value)
emit('open-document', row.rawRequest || row)
}