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 || ''))
}