feat: 增加差旅报销标准测算和财务终审流程

新增差旅报销测算接口及 Spreadsheet 规则解析,审批流程拆分
直属领导审批与财务终审两阶段并细分权限,修复 PDF 文本层
缺失时自动回退 OCR,提交后清理关联会话,前端适配审批流
交互并补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-21 09:28:33 +08:00
parent 002bf4f756
commit 8f65661809
43 changed files with 4366 additions and 410 deletions

View File

@@ -17,7 +17,12 @@ import {
uploadExpenseClaimItemAttachment,
updateExpenseClaimItem
} from '../../services/reimbursements.js'
import { canManageExpenseClaims, canReturnExpenseClaims } from '../../utils/accessControl.js'
import {
canApproveLeaderExpenseClaims,
canManageExpenseClaims,
canReturnExpenseClaims,
isFinanceUser
} from '../../utils/accessControl.js'
import { normalizeRequestForUi } from '../../utils/requestViewModel.js'
import {
buildAiAdviceViewModel,
@@ -82,7 +87,7 @@ function resolveLocationDisplay(value, expenseType) {
function buildFallbackProgressSteps() {
return [
{ index: 1, label: '保存草稿', time: '已完成', done: true, active: true },
{ index: 1, label: '创建单据', time: '已完成', done: true, active: true },
{ index: 2, label: '待提交', time: '进行中', active: true, current: true },
{ index: 3, label: 'AI预审', time: '待处理' },
{ index: 4, label: '直属领导审批', time: '待处理' },
@@ -486,20 +491,51 @@ export default {
const node = String(request.value.node || request.value.approvalStage || '').trim()
return node === '直属领导审批'
})
const showLeaderApprovalPanel = computed(() =>
Boolean(props.approvalMode)
&& request.value.approvalKey === 'in_progress'
&& isDirectManagerApprovalStage.value
&& Boolean(request.value.claimId)
)
const isFinanceApprovalStage = computed(() => {
const node = String(request.value.node || request.value.approvalStage || '').trim()
return node === '财务审批'
})
const canReturnRequest = computed(() =>
canReturnExpenseClaims(currentUser.value)
&& request.value.approvalKey === 'in_progress'
&& Boolean(request.value.claimId)
)
const canApproveRequest = computed(() =>
showLeaderApprovalPanel.value
&& canReturnExpenseClaims(currentUser.value)
Boolean(props.approvalMode)
&& request.value.approvalKey === 'in_progress'
&& Boolean(request.value.claimId)
&& (
(
isDirectManagerApprovalStage.value
&& canApproveLeaderExpenseClaims(currentUser.value)
)
|| (
isFinanceApprovalStage.value
&& isFinanceUser(currentUser.value)
)
)
)
const showLeaderApprovalPanel = computed(() => canApproveRequest.value)
const approvalOpinionTitle = computed(() => (isFinanceApprovalStage.value ? '财务意见' : '领导意见'))
const approvalOpinionPlaceholder = computed(() =>
isFinanceApprovalStage.value
? '请输入财务终审意见,可补充票据核验、金额一致性或入账关注点。'
: '请输入审批意见,可补充核实情况、费用合理性或后续财务关注点。'
)
const approvalOpinionHint = computed(() =>
isFinanceApprovalStage.value ? '审核通过后将进入归档入账。' : '审批通过后将流转至财务审批。'
)
const approvalConfirmBadge = computed(() => (isFinanceApprovalStage.value ? '财务终审' : '领导审批'))
const approvalConfirmDescription = computed(() =>
isFinanceApprovalStage.value
? '确认后该报销单会完成财务终审并进入归档入账,请确认票据、金额与财务意见无误。'
: '确认后该报销单会从直属领导审批流转到财务审批,请确认申请信息与领导意见无误。'
)
const approvalNextStage = computed(() => (isFinanceApprovalStage.value ? '归档入账' : '财务审批'))
const approvalSuccessToast = computed(() =>
isFinanceApprovalStage.value
? `${request.value.id} 已完成财务终审,进入归档入账。`
: `${request.value.id} 已审批通过,流转至财务审批。`
)
const deleteActionLabel = computed(() => (isDraftRequest.value ? '删除草稿' : '删除单据'))
const deleteDialogTitle = computed(() => `确认${deleteActionLabel.value} ${request.value.id} 吗?`)
@@ -564,7 +600,7 @@ export default {
},
{
key: 'date',
label: '日期',
label: '单据申请日期',
value: request.value.applyTime || request.value.occurredDisplay,
icon: 'mdi mdi-calendar-month-outline',
valueClass: ''
@@ -1011,12 +1047,23 @@ export default {
try {
const payload = await uploadExpenseClaimItemAttachment(request.value.claimId, item.id, file)
expenseAttachmentMeta[item.id] = payload?.attachment || null
applyLocalExpenseItemPatch(item.id, {
const recognizedItemAmount = Number(payload?.item_amount ?? payload?.itemAmount)
const itemPatch = {
invoiceId: String(payload?.invoice_id || '').trim(),
attachmentHint: String(payload?.attachment?.file_name || file.name || '').trim()
}
if (Number.isFinite(recognizedItemAmount) && recognizedItemAmount > 0) {
itemPatch.itemAmount = recognizedItemAmount
itemPatch.amount = formatCurrency(recognizedItemAmount)
}
applyLocalExpenseItemPatch(item.id, {
...itemPatch
})
if (editingExpenseId.value === item.id) {
expenseEditor.invoiceId = String(payload?.invoice_id || '').trim()
if (Number.isFinite(recognizedItemAmount) && recognizedItemAmount > 0) {
expenseEditor.itemAmount = String(recognizedItemAmount)
}
}
emit('request-updated', { claimId: request.value.claimId })
@@ -1322,7 +1369,7 @@ export default {
}
if (!canApproveRequest.value) {
toast('当前节点不支持领导审批通过。')
toast('当前节点不支持审批通过。')
return
}
@@ -1345,7 +1392,7 @@ export default {
}
if (!canApproveRequest.value) {
toast('当前节点不支持领导审批通过。')
toast('当前节点不支持审批通过。')
approveConfirmDialogOpen.value = false
return
}
@@ -1357,7 +1404,7 @@ export default {
})
approveConfirmDialogOpen.value = false
leaderOpinion.value = ''
toast(`${request.value.id} 已审批通过,流转至财务审批。`)
toast(approvalSuccessToast.value)
emit('request-updated', { claimId: request.value.claimId })
} catch (error) {
toast(error?.message || '审批通过失败,请稍后重试。')
@@ -1396,6 +1443,12 @@ export default {
attachmentPreviewUrl,
approveBusy,
approveConfirmDialogOpen,
approvalConfirmBadge,
approvalConfirmDescription,
approvalNextStage,
approvalOpinionHint,
approvalOpinionPlaceholder,
approvalOpinionTitle,
canDeleteRequest,
canManageCurrentClaim,
canNavigateAttachmentPreview,