feat: 同步报销流程与工作台改动

This commit is contained in:
caoxiaozhu
2026-06-09 08:32:00 +00:00
parent e124e4bbcb
commit 25724c354f
64 changed files with 6518 additions and 687 deletions

View File

@@ -5,7 +5,7 @@ import { useNavigation, navItems } from './useNavigation.js'
import { mapExpenseClaimToRequest, useRequests } from './useRequests.js'
import { useSystemState } from './useSystemState.js'
import { useToast } from './useToast.js'
import { fetchExpenseClaimDetail } from '../services/reimbursements.js'
import { fetchAllApprovalExpenseClaims, fetchExpenseClaimDetail } from '../services/reimbursements.js'
import { fetchOntologyParse } from '../services/ontology.js'
import { fetchLatestConversation } from '../services/orchestrator.js'
import { clearAssistantSessionSnapshotForDraftClaim } from '../utils/assistantSessionSnapshot.js'
@@ -18,20 +18,20 @@ import {
} from '../utils/workbenchAssistantIntent.js'
import { buildWorkbenchSummary } from '../utils/workbenchSummary.js'
import { createCurrentYearDateRange } from '../utils/dateRangeDefaults.js'
const SESSION_TYPE_EXPENSE = 'expense'
const SMART_ENTRY_SOURCE_APPLICATION = 'application'
const SMART_ENTRY_SOURCE_REIMBURSEMENT = 'topbar'
export function useAppShell() {
const route = useRoute()
const router = useRouter()
const smartEntryOpen = ref(false)
const smartEntryContext = ref({
prompt: '',
export function useAppShell() {
const route = useRoute()
const router = useRouter()
const smartEntryOpen = ref(false)
const smartEntryContext = ref({
prompt: '',
source: 'documents',
request: null,
request: null,
files: [],
conversation: null,
scope: null,
@@ -45,16 +45,17 @@ export function useAppShell() {
const smartEntryInvalidatedDraftClaimId = ref('')
const selectedRequestSnapshot = ref(null)
const documentCenterRefreshToken = ref(0)
const { activeView, currentView, setView } = useNavigation()
const {
requests,
loading: requestsLoading,
error: requestsError,
search,
filters,
ranges,
activeRange,
const workbenchApprovalRequests = ref([])
const { activeView, currentView, setView } = useNavigation()
const {
requests,
loading: requestsLoading,
error: requestsError,
search,
filters,
ranges,
activeRange,
filteredRequests,
approveRequest,
rejectRequest,
@@ -65,7 +66,7 @@ export function useAppShell() {
const { toast } = useToast()
const customRange = ref(createCurrentYearDateRange())
const selectedRequest = computed(() => {
const requestId = String(route.params.requestId || '')
@@ -105,6 +106,40 @@ export function useAppShell() {
return reloadRequests()
}
async function reloadWorkbenchApprovalRequests() {
try {
const payload = await fetchAllApprovalExpenseClaims()
workbenchApprovalRequests.value = Array.isArray(payload)
? payload.map((item) => mapExpenseClaimToRequest(item))
: []
} catch {
workbenchApprovalRequests.value = []
}
}
async function reloadWorkbenchRequests() {
const [payload] = await Promise.all([
reloadRequests({ silent: true }),
reloadWorkbenchApprovalRequests()
])
return payload
}
function resolveWorkbenchRequestKey(request) {
return String(request?.claimId || request?.id || request?.claimNo || '').trim()
}
function mergeWorkbenchRequests(primaryRequests = [], approvalRequests = []) {
const merged = new Map()
for (const request of [...primaryRequests, ...approvalRequests]) {
const key = resolveWorkbenchRequestKey(request)
if (key) {
merged.set(key, request)
}
}
return Array.from(merged.values())
}
function isSameRequestIdentity(request, requestId) {
const normalizedId = String(requestId || '').trim()
if (!request || !normalizedId) {
@@ -185,16 +220,20 @@ export function useAppShell() {
return
}
if (view === 'workbench') {
void ensureRequestsLoaded()
void reloadWorkbenchRequests()
}
},
{ immediate: true }
)
const workbenchSummary = computed(() =>
buildWorkbenchSummary(requests.value, currentUser.value)
)
const workbenchRequests = computed(() =>
mergeWorkbenchRequests(requests.value, workbenchApprovalRequests.value)
)
const workbenchSummary = computed(() =>
buildWorkbenchSummary(workbenchRequests.value, currentUser.value)
)
const topBarView = computed(() => {
if (detailMode.value) {
const request = selectedRequest.value || {}
@@ -207,46 +246,46 @@ export function useAppShell() {
: '查看报销明细、票据材料、审批进度与风险提示。'
}
}
return currentView.value
})
const requestSummary = computed(() =>
filteredRequests.value.reduce(
(summary, item) => {
const request = normalizeRequestForUi(item)
if (!request) {
return summary
}
summary.total += 1
if (request.approvalKey === 'draft') {
summary.draft += 1
} else if (request.approvalKey === 'in_progress') {
summary.inProgress += 1
} else if (request.approvalKey === 'supplement') {
summary.supplement += 1
} else if (request.approvalKey === 'completed') {
summary.completed += 1
}
return summary
},
{ total: 0, draft: 0, inProgress: 0, supplement: 0, completed: 0 }
)
)
function handleApprove(request) {
const message = approveRequest(request)
toast(message)
}
function handleReject(request) {
const message = rejectRequest(request)
toast(message)
}
const requestSummary = computed(() =>
filteredRequests.value.reduce(
(summary, item) => {
const request = normalizeRequestForUi(item)
if (!request) {
return summary
}
summary.total += 1
if (request.approvalKey === 'draft') {
summary.draft += 1
} else if (request.approvalKey === 'in_progress') {
summary.inProgress += 1
} else if (request.approvalKey === 'supplement') {
summary.supplement += 1
} else if (request.approvalKey === 'completed') {
summary.completed += 1
}
return summary
},
{ total: 0, draft: 0, inProgress: 0, supplement: 0, completed: 0 }
)
)
function handleApprove(request) {
const message = approveRequest(request)
toast(message)
}
function handleReject(request) {
const message = rejectRequest(request)
toast(message)
}
function handleNavigate(view) {
smartEntryOpen.value = false
const shouldRefreshCurrentDocumentCenter =
@@ -258,7 +297,7 @@ export function useAppShell() {
void reloadDocumentCenterRequests()
}
}
function openFinancialAssistantCreate(source) {
if (smartEntryOpen.value) {
smartEntryRevealToken.value += 1
@@ -287,28 +326,28 @@ export function useAppShell() {
function openExpenseApplicationCreate() {
openFinancialAssistantCreate(SMART_ENTRY_SOURCE_APPLICATION)
}
function resolveCurrentUserId() {
const user = currentUser.value || {}
return String(user.username || user.name || 'anonymous').trim() || 'anonymous'
}
function resolveSmartEntryClaimScope(payload = {}) {
const request = payload.request && typeof payload.request === 'object' ? payload.request : null
const payloadScope = payload.scope && typeof payload.scope === 'object' ? payload.scope : null
const claimId = String(
payloadScope?.claimId ||
payloadScope?.claim_id ||
request?.claimId ||
request?.claim_id ||
''
).trim()
if (!claimId) {
return null
}
return { type: 'claim', claimId }
}
function resolveCurrentUserId() {
const user = currentUser.value || {}
return String(user.username || user.name || 'anonymous').trim() || 'anonymous'
}
function resolveSmartEntryClaimScope(payload = {}) {
const request = payload.request && typeof payload.request === 'object' ? payload.request : null
const payloadScope = payload.scope && typeof payload.scope === 'object' ? payload.scope : null
const claimId = String(
payloadScope?.claimId ||
payloadScope?.claim_id ||
request?.claimId ||
request?.claim_id ||
''
).trim()
if (!claimId) {
return null
}
return { type: 'claim', claimId }
}
function isDetailClaimScopedPayload(payload = {}) {
return String(payload.source || '').trim() === 'detail' && Boolean(resolveSmartEntryClaimScope(payload))
}
@@ -451,7 +490,7 @@ export function useAppShell() {
const status = String(payload.status || payload.claimStatus || '').trim()
const approvalStage = String(payload.approvalStage || payload.approval_stage || '').trim()
const isApplicationDocument = isApplicationDocumentPayload(payload, claimNo)
await reloadRequests()
await reloadWorkbenchRequests()
if (status === 'submitted') {
if (isApplicationDocument) {
toast(`${claimNo || '该'}申请单已提交${approvalStage ? `,当前节点:${approvalStage}` : ',等待直属领导审批'}`)
@@ -505,7 +544,7 @@ export function useAppShell() {
}
async function handleRequestUpdated() {
await reloadRequests()
await reloadWorkbenchRequests()
await refreshSelectedRequestDetail(String(route.params.requestId || ''))
}

View File

@@ -18,9 +18,7 @@ import {
import {
metricBlueprints,
systemMetricBlueprints,
systemDashboardTotals as fallbackSystemDashboardTotals,
systemAgentDailyRatio as fallbackSystemAgentDailyRatio,
systemLoginWave as fallbackSystemLoginWave,
systemTokenDailyWave as fallbackSystemTokenDailyWave,
systemUsageDurationSummary as fallbackSystemUsageDurationSummary,
systemUserTokenUsage as fallbackSystemUserTokenUsage,
@@ -78,6 +76,25 @@ const emptyFinanceBudgetMetrics = [
{ label: '预警预算池', value: '0 个', detail: '超支 0 个', tone: 'success', icon: 'mdi mdi-alert-outline' }
]
const emptySystemDashboardTotals = {
toolCalls: 0,
modelTokens: 0,
onlineUsers: 0,
avgOnlineMinutes: 0,
executionSuccessRate: 0,
positiveFeedback: 0,
negativeFeedback: 0,
failedRuns: 0,
toolCallsChange: 0,
modelTokensChange: 0
}
const emptySystemLoginWave = {
labels: Array.from({ length: 24 }, (_, hour) => `${String(hour).padStart(2, '0')}:00`),
loginUsers: Array.from({ length: 24 }, () => 0),
interactions: Array.from({ length: 24 }, () => 0)
}
function parseLocalDate(value) {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || '').trim())
if (!match) {
@@ -439,13 +456,13 @@ export function useOverviewView(options = {}) {
})
const systemDashboardTotals = computed(() => (
systemDashboardPayload.value?.totals || fallbackSystemDashboardTotals
systemDashboardPayload.value?.totals || emptySystemDashboardTotals
))
const systemAgentDailyRatio = computed(() => (
systemDashboardPayload.value?.agentDailyRatio || fallbackSystemAgentDailyRatio
))
const systemLoginWave = computed(() => (
systemDashboardPayload.value?.loginWave || fallbackSystemLoginWave
systemDashboardPayload.value?.loginWave || emptySystemLoginWave
))
const systemTokenDailyWave = computed(() => (
systemDashboardPayload.value?.tokenDailyWave || fallbackSystemTokenDailyWave

View File

@@ -1,6 +1,6 @@
import { computed, reactive, ref } from 'vue'
import { fetchExpenseClaims } from '../services/reimbursements.js'
import { fetchAllExpenseClaims } from '../services/reimbursements.js'
import { filterActionableRiskFlags, normalizeRiskFlagTone } from '../utils/riskFlags.js'
const EXPENSE_TYPE_LABELS = {
@@ -72,7 +72,6 @@ const APPLICATION_PROGRESS_LABELS = [
'创建申请',
'直属领导审批',
'预算管理者审批',
'审批完成',
APPLICATION_LINK_STATUS_STEP_LABEL,
ARCHIVED_STEP_LABEL
]
@@ -80,7 +79,6 @@ const APPLICATION_PROGRESS_LABELS = [
const APPLICATION_PROGRESS_LABELS_WITHOUT_BUDGET = [
'创建申请',
'直属领导审批',
'审批完成',
APPLICATION_LINK_STATUS_STEP_LABEL,
ARCHIVED_STEP_LABEL
]
@@ -595,17 +593,17 @@ function resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode) {
const normalizedNode = String(workflowNode || '').trim()
if (approvalMeta.key === 'completed') {
return normalizedNode.includes(APPLICATION_ARCHIVE_STAGE_LABEL) ? 4 : 3
return normalizedNode.includes(APPLICATION_ARCHIVE_STAGE_LABEL) ? 3 : 2
}
if (normalizedNode.includes(APPLICATION_ARCHIVE_STAGE_LABEL) || normalizedNode.includes(ARCHIVED_STEP_LABEL)) {
return 4
return 3
}
if (normalizedNode.includes(APPLICATION_LINK_STATUS_STEP_LABEL)) {
return 3
return 2
}
if (normalizedNode.includes('审批完成') || normalizedNode.includes('申请完成')) {
return 3
return 2
}
if (normalizedNode.includes('预算')) {
return 2
@@ -693,6 +691,36 @@ function resolveApplicationApproverName(claim) {
) || '直属领导'
}
function resolveReimbursementApproverName(claim, label) {
const stepLabel = normalizeText(label)
if (stepLabel === '直属领导审批') {
return resolveDisplayName(
claim?.manager_name,
claim?.managerName,
claim?.profile_manager,
claim?.profileManager,
claim?.direct_manager_name,
claim?.directManagerName
) || '直属领导'
}
if (stepLabel === '财务审批') {
const routeEvent = findReimbursementFinanceRouteEvent(claim)
return resolveDisplayName(
claim?.finance_approver_name,
claim?.financeApproverName,
routeEvent?.next_approver_name,
routeEvent?.nextApproverName,
routeEvent?.finance_approver_name,
routeEvent?.financeApproverName,
claim?.finance_owner_name,
claim?.financeOwnerName
) || '财务'
}
return stepLabel.replace(/审批$/, '') || '审批人'
}
function resolveApplicationBudgetApproverName(claim) {
const routeEvent = findApprovalEventForStep(claim, '直属领导审批')
return resolveDisplayName(
@@ -708,6 +736,15 @@ function resolveApplicationBudgetApproverName(claim) {
function resolveProgressDisplayLabel(label, documentTypeCode, claim, approvalMeta) {
const normalizedLabel = normalizeText(label)
const workflowNode = normalizeText(claim?.approval_stage || claim?.workflowNode)
if (
documentTypeCode !== DOCUMENT_TYPE_APPLICATION
&& approvalMeta.key !== 'completed'
&& (normalizedLabel === '直属领导审批' || normalizedLabel === '财务审批')
&& workflowNode.includes(normalizedLabel.replace(/审批$/, ''))
) {
return `等待 ${resolveReimbursementApproverName(claim, normalizedLabel)} 批复`
}
if (
documentTypeCode === DOCUMENT_TYPE_APPLICATION
&& approvalMeta.key !== 'completed'
@@ -796,6 +833,24 @@ function findApprovalEventForStep(claim, label) {
return getLatestEvent(events)
}
function findReimbursementFinanceRouteEvent(claim) {
return getLatestEvent(
getRiskFlags(claim).filter((flag) => {
if (!flag || typeof flag !== 'object') {
return false
}
const source = normalizeText(flag.source)
if (!['manual_approval', 'budget_approval'].includes(source)) {
return false
}
const nextStage = normalizeText(flag.next_approval_stage || flag.nextApprovalStage)
return nextStage.includes('财务')
})
)
}
function findLatestReturnEvent(claim) {
return getLatestEvent(
getRiskFlags(claim).filter((flag) => (
@@ -1066,13 +1121,39 @@ function findMergedApplicationBudgetApprovalEvent(claim) {
source === 'manual_approval'
&& eventType === 'expense_application_approval'
&& previousStage.includes('直属领导')
&& nextStage.includes('审批完成')
&& (
nextStage.includes('审批完成')
|| nextStage.includes(APPLICATION_LINK_STATUS_STEP_LABEL)
|| nextStage.includes('申请完成')
)
&& mergedFlag
)
})
)
}
function resolveBudgetRouteResult(flag, routeDecision = {}) {
if (routeDecision && typeof routeDecision === 'object') {
const routeBudgetResult = routeDecision.budget_result || routeDecision.budgetResult
if (routeBudgetResult && typeof routeBudgetResult === 'object') {
return routeBudgetResult
}
}
const flagBudgetResult = flag?.budget_result || flag?.budgetResult
return flagBudgetResult && typeof flagBudgetResult === 'object' ? flagBudgetResult : {}
}
function applicationBudgetRouteMeetsThreshold(flag, routeDecision = {}) {
const budgetResult = resolveBudgetRouteResult(flag, routeDecision)
const metrics = budgetResult.metrics && typeof budgetResult.metrics === 'object' ? budgetResult.metrics : {}
const overBudgetAmount = parseNumber(metrics.over_budget_amount ?? metrics.overBudgetAmount)
const afterUsageRate = parseNumber(metrics.after_usage_rate ?? metrics.afterUsageRate)
const claimAmountRatio = parseNumber(metrics.claim_amount_ratio ?? metrics.claimAmountRatio)
return overBudgetAmount > 0 || Math.max(afterUsageRate, claimAmountRatio) >= 90
}
function applicationRequiresBudgetReviewStep(claim, workflowNode) {
const node = normalizeText(workflowNode || claim?.approval_stage || claim?.workflowNode)
if (node.includes('预算')) {
@@ -1087,24 +1168,22 @@ function applicationRequiresBudgetReviewStep(claim, workflowNode) {
const source = normalizeText(flag.source)
const eventType = normalizeText(flag.event_type || flag.eventType)
const previousStage = normalizeText(flag.previous_approval_stage || flag.previousApprovalStage)
const nextStage = normalizeText(flag.next_approval_stage || flag.nextApprovalStage)
const routeDecision = flag.route_decision || flag.routeDecision || {}
if (source === 'approval_routing' && flag.requires_budget_review === true) {
return true
return applicationBudgetRouteMeetsThreshold(flag, flag)
}
if (
routeDecision
&& typeof routeDecision === 'object'
&& routeDecision.requires_budget_review === true
) {
return true
return applicationBudgetRouteMeetsThreshold(flag, routeDecision)
}
return (
source === 'budget_approval'
|| eventType === 'expense_application_budget_approval'
|| previousStage.includes('预算')
|| nextStage.includes('预算')
)
})
}
@@ -1273,7 +1352,7 @@ function buildProgressSteps(approvalMeta, workflowNode, claim = {}, options = {}
? hasApplicationReturnStep
? ['创建申请', '直属领导审批', '退回', '待提交']
: hasMergedApplicationBudgetApproval
? ['创建申请', '直属领导审批', '审批完成', APPLICATION_LINK_STATUS_STEP_LABEL, ARCHIVED_STEP_LABEL]
? ['创建申请', '直属领导审批', APPLICATION_LINK_STATUS_STEP_LABEL, ARCHIVED_STEP_LABEL]
: shouldShowApplicationBudgetStep
? APPLICATION_PROGRESS_LABELS
: APPLICATION_PROGRESS_LABELS_WITHOUT_BUDGET
@@ -1512,6 +1591,8 @@ export function mapExpenseClaimToRequest(claim) {
employeePosition: String(claim?.employee_position || '').trim(),
employeeGrade: String(claim?.employee_grade || '').trim(),
managerName: resolveDisplayName(claim?.manager_name),
financeApproverName: resolveDisplayName(claim?.finance_approver_name, claim?.financeApproverName),
financeOwnerName: resolveDisplayName(claim?.finance_owner_name, claim?.financeOwnerName),
budgetApproverName: resolveDisplayName(claim?.budget_approver_name, claim?.budgetApproverName),
budgetApproverGrade: String(claim?.budget_approver_grade || claim?.budgetApproverGrade || '').trim(),
budgetApproverRoleCode: String(claim?.budget_approver_role_code || claim?.budgetApproverRoleCode || '').trim(),
@@ -1665,19 +1746,26 @@ export function useRequests() {
})
})
async function reload() {
loading.value = true
error.value = ''
async function reload(options = {}) {
const silent = Boolean(options?.silent)
if (!silent) {
loading.value = true
error.value = ''
}
try {
const payload = await fetchExpenseClaims()
const payload = await fetchAllExpenseClaims()
requests.value = Array.isArray(payload) ? payload.map((item) => mapExpenseClaimToRequest(item)) : []
loaded.value = true
} catch (nextError) {
requests.value = []
if (!silent) {
requests.value = []
}
error.value = nextError instanceof Error ? nextError.message : '个人报销列表加载失败。'
} finally {
loading.value = false
if (!silent) {
loading.value = false
}
}
}