import { computed, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useApprovalInbox } from './useApprovalInbox.js' import { useNavigation, navItems } from './useNavigation.js' import { useRequests } from './useRequests.js' import { useSystemState } from './useSystemState.js' import { useToast } from './useToast.js' import { fetchLatestConversation } from '../services/orchestrator.js' import { clearAssistantSessionSnapshotForDraftClaim } from '../utils/assistantSessionSnapshot.js' import { buildDetailAlerts } from '../utils/detailAlerts.js' import { normalizeRequestForUi } from '../utils/requestViewModel.js' import { buildWorkbenchSummary } from '../utils/workbenchSummary.js' const SESSION_TYPE_EXPENSE = 'expense' export function useAppShell() { const route = useRoute() const router = useRouter() const smartEntryOpen = ref(false) const smartEntryContext = ref({ prompt: '', source: 'requests', request: null, files: [], conversation: null, scope: null }) const smartEntrySessionId = ref(0) const smartEntryInvalidatedDraftClaimId = ref('') const { activeView, currentView, setView } = useNavigation() const { requests, loading: requestsLoading, error: requestsError, search, filters, ranges, activeRange, filteredRequests, approveRequest, rejectRequest, reload: reloadRequests } = useRequests() const { currentUser } = useSystemState() const { toast } = useToast() const { refreshApprovalInbox } = useApprovalInbox() const customRange = ref({ start: '2024-07-06', end: '2024-07-12' }) 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 ) return normalizeRequestForUi(rawRequest) }) const detailMode = computed(() => route.name === 'app-request-detail') const logDetailMode = computed(() => route.name === 'app-log-detail') const detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : [])) const requestsListActive = computed(() => activeView.value === 'requests' && !detailMode.value) const workbenchActive = computed(() => activeView.value === 'workbench') watch(requestsListActive, (isActive, wasActive) => { if (isActive && !wasActive) { void reloadRequests() } }) watch(workbenchActive, (isActive, wasActive) => { if (isActive && !wasActive) { void reloadRequests() } }) const workbenchSummary = computed(() => buildWorkbenchSummary(requests.value, currentUser.value) ) const topBarView = computed(() => { if (detailMode.value) { return { title: '报销单详情', desc: '查看报销明细、票据材料、审批进度与风险提示。' } } if (logDetailMode.value) { return { title: '日志详情', desc: '查看单条日志的解析结果、上下文信息与原始记录。' } } 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) } function handleNavigate(view) { smartEntryOpen.value = false setView(view) } function openTravelCreate() { smartEntryOpen.value = true smartEntryContext.value = { prompt: '', source: 'topbar', request: null, files: [], conversation: null, scope: null } smartEntrySessionId.value += 1 } 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)) } async function resolveSmartEntryConversation(payload = {}) { if (payload.conversation) { return payload.conversation } if (isDetailClaimScopedPayload(payload)) { return null } if (!payload.restoreLatestConversation) { return null } try { const latestPayload = await fetchLatestConversation(resolveCurrentUserId(), SESSION_TYPE_EXPENSE, { preferRecoverable: true }) return latestPayload?.found ? latestPayload.conversation || null : null } catch (error) { console.warn('Failed to restore latest expense conversation for smart entry:', error) toast(error?.message || '恢复最近报销会话失败,请稍后重试。') return null } } async function openSmartEntry(payload = {}) { const conversation = await resolveSmartEntryConversation(payload) const scope = resolveSmartEntryClaimScope(payload) smartEntryOpen.value = true smartEntryContext.value = { prompt: payload.prompt ?? '', source: payload.source ?? 'workbench', request: payload.request ?? selectedRequest.value, files: Array.isArray(payload.files) ? payload.files : [], conversation, scope } smartEntrySessionId.value += 1 } function closeSmartEntry() { smartEntryOpen.value = false } async function handleDraftSaved(payload = {}) { const claimNo = String(payload.claimNo || payload.claim_no || '').trim() const status = String(payload.status || payload.claimStatus || '').trim() const approvalStage = String(payload.approvalStage || payload.approval_stage || '').trim() await reloadRequests() if (status === 'submitted') { smartEntryOpen.value = false void refreshApprovalInbox() toast(`${claimNo || '该'}单据已完成 AI预审${approvalStage ? `,当前节点:${approvalStage}` : ',并已提交审批'}。`) router.push({ name: 'app-requests' }) return } toast(`${claimNo || '该'}单据已保存为草稿,可继续上传票据或补充信息。`) } function openRequestDetail(request) { router.push({ name: 'app-request-detail', params: { requestId: request.claimId || request.id } }) } function closeRequestDetail() { router.push({ name: 'app-requests' }) } async function handleRequestUpdated() { await reloadRequests() void refreshApprovalInbox() } async function handleRequestDeleted(payload = {}) { const deletedClaimId = String(payload.claimId || payload.claim_id || '').trim() if (deletedClaimId) { clearAssistantSessionSnapshotForDraftClaim(resolveCurrentUserId(), deletedClaimId, SESSION_TYPE_EXPENSE) smartEntryInvalidatedDraftClaimId.value = deletedClaimId } await reloadRequests() void refreshApprovalInbox() router.push({ name: 'app-requests' }) } return { activeRange, activeView, closeRequestDetail, closeSmartEntry, currentView, customRange, detailMode, logDetailMode, filteredRequests, filters, handleApprove, handleDraftSaved, handleNavigate, handleReject, handleRequestDeleted, handleRequestUpdated, navItems, openRequestDetail, openSmartEntry, openTravelCreate, ranges, requestSummary, workbenchSummary, requestsError, requestsLoading, reloadRequests, requests, search, selectedRequest, setView, smartEntryContext, smartEntryInvalidatedDraftClaimId, smartEntryOpen, smartEntrySessionId, detailAlerts, toast, topBarView } }