feat: 风险可见性控制与差旅详情页交互优化

- 新增风险可见性工具函数与风险日趋势图表组件
- 优化差旅请求详情页费用模型与视图交互
- 完善顶部导航栏样式与应用壳路由逻辑
- 补充风险可见性、风险看板与差旅详情测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-03 22:15:45 +08:00
parent 75d5c178e1
commit 87da5df91b
17 changed files with 809 additions and 168 deletions

View File

@@ -2,9 +2,10 @@ import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useNavigation, navItems } from './useNavigation.js'
import { useRequests } from './useRequests.js'
import { useSystemState } from './useSystemState.js'
import { mapExpenseClaimToRequest, useRequests } from './useRequests.js'
import { useSystemState } from './useSystemState.js'
import { useToast } from './useToast.js'
import { fetchExpenseClaimDetail } from '../services/reimbursements.js'
import { fetchOntologyParse } from '../services/ontology.js'
import { fetchLatestConversation } from '../services/orchestrator.js'
import { clearAssistantSessionSnapshotForDraftClaim } from '../utils/assistantSessionSnapshot.js'
@@ -16,6 +17,7 @@ import {
resolveWorkbenchSessionTypeFromOntology
} 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'
@@ -62,36 +64,29 @@ export function useAppShell() {
const { currentUser } = useSystemState()
const { toast } = useToast()
const customRange = ref({ start: '2024-07-06', end: '2024-07-12' })
const customRange = ref(createCurrentYearDateRange())
const selectedRequest = computed(() => {
const requestId = String(route.params.requestId || '')
if (!requestId) {
return null
}
const rawRequest = requests.value.find(
(item) => String(item.claimId || '').trim() === requestId || String(item.id || '').trim() === requestId
)
const normalizedRequest = normalizeRequestForUi(rawRequest)
if (normalizedRequest) {
return normalizedRequest
}
const snapshot = normalizeRequestForUi(selectedRequestSnapshot.value)
if (
snapshot
&& (
String(snapshot.claimId || '').trim() === requestId
|| String(snapshot.id || '').trim() === requestId
|| String(snapshot.documentNo || '').trim() === requestId
)
) {
return snapshot
}
return null
const selectedRequest = computed(() => {
const requestId = String(route.params.requestId || '')
if (!requestId) {
return null
}
const snapshot = normalizeRequestForUi(selectedRequestSnapshot.value)
if (isSameRequestIdentity(snapshot, requestId)) {
return snapshot
}
const rawRequest = requests.value.find(
(item) => String(item.claimId || '').trim() === requestId || String(item.id || '').trim() === requestId
)
const normalizedRequest = normalizeRequestForUi(rawRequest)
if (normalizedRequest) {
return normalizedRequest
}
return null
})
const detailMode = computed(() => route.name === 'app-document-detail')
@@ -110,9 +105,81 @@ export function useAppShell() {
return reloadRequests()
}
function isSameRequestIdentity(request, requestId) {
const normalizedId = String(requestId || '').trim()
if (!request || !normalizedId) {
return false
}
return [
request.claimId,
request.id,
request.claimNo,
request.documentNo
].some((value) => String(value || '').trim() === normalizedId)
}
function resolveRequestDetailLookupId(requestOrId = selectedRequestSnapshot.value) {
if (typeof requestOrId === 'string') {
return requestOrId.trim()
}
return String(
requestOrId?.claimId
|| requestOrId?.claim_id
|| requestOrId?.id
|| requestOrId?.claimNo
|| requestOrId?.claim_no
|| ''
).trim()
}
function upsertRequestSnapshot(nextRequest) {
if (!nextRequest) {
return
}
selectedRequestSnapshot.value = nextRequest
const nextIdValues = [
nextRequest.claimId,
nextRequest.id,
nextRequest.claimNo,
nextRequest.documentNo
].map((item) => String(item || '').trim()).filter(Boolean)
const nextIdSet = new Set(nextIdValues)
const index = requests.value.findIndex((item) => [
item.claimId,
item.id,
item.claimNo,
item.documentNo
].some((value) => nextIdSet.has(String(value || '').trim())))
if (index >= 0) {
requests.value.splice(index, 1, nextRequest)
}
}
async function refreshSelectedRequestDetail(requestOrId = selectedRequestSnapshot.value) {
const lookupId = resolveRequestDetailLookupId(requestOrId)
if (!lookupId) {
return
}
try {
const payload = await fetchExpenseClaimDetail(lookupId)
const mappedRequest = mapExpenseClaimToRequest(payload)
const routeRequestId = String(route.params.requestId || '').trim()
if (!routeRequestId || isSameRequestIdentity(mappedRequest, routeRequestId) || routeRequestId === lookupId) {
upsertRequestSnapshot(mappedRequest)
}
} catch {
// 保留当前快照,避免详情刷新失败时把页面置空。
}
}
watch(
() => [activeView.value, route.name],
([view]) => {
if (route.name === 'app-document-detail') {
void ensureRequestsLoaded()
void refreshSelectedRequestDetail(String(route.params.requestId || ''))
return
}
if (view === 'documents') {
void reloadDocumentCenterRequests()
return
@@ -425,6 +492,7 @@ export function useAppShell() {
params: { requestId: request.claimId || request.id },
query: buildDocumentDetailQuery(options)
})
void refreshSelectedRequestDetail(request)
}
function closeRequestDetail() {
@@ -438,6 +506,7 @@ export function useAppShell() {
async function handleRequestUpdated() {
await reloadRequests()
await refreshSelectedRequestDetail(String(route.params.requestId || ''))
}
async function handleRequestDeleted(payload = {}) {