2026-05-06 11:00:38 +08:00
|
|
|
import { computed, ref } from 'vue'
|
2026-05-06 22:23:42 +08:00
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
|
|
|
import { useNavigation, navItems } from './useNavigation.js'
|
|
|
|
|
import { useRequests } from './useRequests.js'
|
|
|
|
|
import { useChat } from './useChat.js'
|
|
|
|
|
import { useToast } from './useToast.js'
|
|
|
|
|
import { documents } from '../data/requests.js'
|
2026-05-06 22:23:42 +08:00
|
|
|
import { normalizeRequestForUi } from '../utils/requestViewModel.js'
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
|
|
|
export function useAppShell() {
|
2026-05-06 22:23:42 +08:00
|
|
|
const route = useRoute()
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
2026-05-06 11:00:38 +08:00
|
|
|
const travelCreateMode = ref(false)
|
|
|
|
|
const smartEntryOpen = ref(false)
|
2026-05-12 06:39:26 +00:00
|
|
|
const smartEntryContext = ref({ prompt: '', source: 'requests', request: null, files: [], conversation: null })
|
2026-05-06 11:00:38 +08:00
|
|
|
const smartEntrySessionId = ref(0)
|
|
|
|
|
|
|
|
|
|
const { activeView, currentView, setView } = useNavigation()
|
2026-05-13 03:29:10 +00:00
|
|
|
const {
|
|
|
|
|
requests,
|
|
|
|
|
loading: requestsLoading,
|
|
|
|
|
error: requestsError,
|
|
|
|
|
search,
|
|
|
|
|
filters,
|
|
|
|
|
ranges,
|
|
|
|
|
activeRange,
|
|
|
|
|
filteredRequests,
|
|
|
|
|
approveRequest,
|
|
|
|
|
rejectRequest,
|
|
|
|
|
reload: reloadRequests
|
|
|
|
|
} = useRequests()
|
2026-05-12 01:27:49 +00:00
|
|
|
const {
|
|
|
|
|
messages,
|
|
|
|
|
draft,
|
|
|
|
|
uploadedFiles,
|
|
|
|
|
messageList,
|
|
|
|
|
activeCase,
|
|
|
|
|
prompts,
|
|
|
|
|
sending,
|
|
|
|
|
sendMessage,
|
|
|
|
|
handleUpload,
|
|
|
|
|
openChat,
|
|
|
|
|
openNewChat
|
2026-05-13 03:29:10 +00:00
|
|
|
} = useChat(activeView)
|
2026-05-06 22:23:42 +08:00
|
|
|
const { toast } = useToast()
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
|
|
|
const docSearch = ref('')
|
|
|
|
|
const customRange = ref({ start: '2024-07-06', end: '2024-07-12' })
|
2026-05-06 22:23:42 +08:00
|
|
|
const travelPrompts = ['生成差旅摘要', '识别报销风险', '核对审批链', '提取随附票据', '生成沟通建议']
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
const selectedRequest = computed(() => {
|
2026-05-06 22:23:42 +08:00
|
|
|
const requestId = String(route.params.requestId || '')
|
|
|
|
|
|
|
|
|
|
if (!requestId) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
const rawRequest = requests.value.find(
|
|
|
|
|
(item) => String(item.claimId || '').trim() === requestId || String(item.id || '').trim() === requestId
|
|
|
|
|
)
|
2026-05-06 22:23:42 +08:00
|
|
|
return normalizeRequestForUi(rawRequest)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const detailMode = computed(() => route.name === 'app-request-detail')
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
|
|
|
const topBarView = computed(() => {
|
|
|
|
|
if (detailMode.value) {
|
|
|
|
|
return {
|
2026-05-13 03:29:10 +00:00
|
|
|
title: '报销单详情',
|
|
|
|
|
desc: '查看报销明细、票据材料、审批进度与风险提示。'
|
2026-05-06 11:00:38 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-06 22:23:42 +08:00
|
|
|
|
2026-05-06 11:00:38 +08:00
|
|
|
return currentView.value
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
const requestSummary = computed(() =>
|
|
|
|
|
requests.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 }
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-06 11:00:38 +08:00
|
|
|
const filteredDocuments = computed(() => {
|
|
|
|
|
const key = docSearch.value.trim().toLowerCase()
|
2026-05-06 22:23:42 +08:00
|
|
|
return documents.filter((doc) => !key || `${doc.id}${doc.applicant}${doc.destination}${doc.type}`.toLowerCase().includes(key))
|
2026-05-06 11:00:38 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function handleApprove(request) {
|
2026-05-06 22:23:42 +08:00
|
|
|
const message = approveRequest(request)
|
|
|
|
|
toast(message)
|
2026-05-06 11:00:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleReject(request) {
|
2026-05-06 22:23:42 +08:00
|
|
|
const message = rejectRequest(request)
|
|
|
|
|
toast(message)
|
2026-05-06 11:00:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleNavigate(view) {
|
|
|
|
|
travelCreateMode.value = false
|
|
|
|
|
smartEntryOpen.value = false
|
|
|
|
|
setView(view)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleOpenChat(request) {
|
|
|
|
|
travelCreateMode.value = false
|
|
|
|
|
openChat(request)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openTravelCreate() {
|
|
|
|
|
smartEntryOpen.value = true
|
|
|
|
|
travelCreateMode.value = false
|
2026-05-12 06:39:26 +00:00
|
|
|
smartEntryContext.value = { prompt: '', source: 'topbar', request: null, files: [], conversation: null }
|
2026-05-06 11:00:38 +08:00
|
|
|
smartEntrySessionId.value += 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openSmartEntry(payload = {}) {
|
|
|
|
|
smartEntryOpen.value = true
|
|
|
|
|
travelCreateMode.value = false
|
2026-05-06 22:23:42 +08:00
|
|
|
|
2026-05-06 11:00:38 +08:00
|
|
|
smartEntryContext.value = {
|
|
|
|
|
prompt: payload.prompt ?? '',
|
|
|
|
|
source: payload.source ?? 'workbench',
|
2026-05-13 03:29:10 +00:00
|
|
|
request: payload.request ?? selectedRequest.value,
|
2026-05-12 06:39:26 +00:00
|
|
|
files: Array.isArray(payload.files) ? payload.files : [],
|
|
|
|
|
conversation: payload.conversation ?? null
|
2026-05-06 11:00:38 +08:00
|
|
|
}
|
|
|
|
|
smartEntrySessionId.value += 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeSmartEntry() {
|
|
|
|
|
smartEntryOpen.value = false
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
async function handleDraftSaved(payload = {}) {
|
|
|
|
|
const claimNo = String(payload.claimNo || payload.claim_no || '').trim()
|
|
|
|
|
smartEntryOpen.value = false
|
|
|
|
|
await reloadRequests()
|
|
|
|
|
toast(`${claimNo || '该'}单据已保存到草稿,请到报销页面查看。`)
|
|
|
|
|
router.push({ name: 'app-requests' })
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 11:00:38 +08:00
|
|
|
function openRequestDetail(request) {
|
2026-05-06 22:23:42 +08:00
|
|
|
router.push({
|
|
|
|
|
name: 'app-request-detail',
|
2026-05-13 03:29:10 +00:00
|
|
|
params: { requestId: request.claimId || request.id }
|
2026-05-06 22:23:42 +08:00
|
|
|
})
|
2026-05-06 11:00:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeRequestDetail() {
|
2026-05-06 22:23:42 +08:00
|
|
|
router.push({ name: 'app-requests' })
|
2026-05-06 11:00:38 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-13 03:29:10 +00:00
|
|
|
async function handleRequestUpdated() {
|
|
|
|
|
await reloadRequests()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleRequestDeleted() {
|
|
|
|
|
await reloadRequests()
|
|
|
|
|
router.push({ name: 'app-requests' })
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 11:00:38 +08:00
|
|
|
return {
|
|
|
|
|
activeCase,
|
|
|
|
|
activeRange,
|
|
|
|
|
activeView,
|
|
|
|
|
closeRequestDetail,
|
|
|
|
|
closeSmartEntry,
|
|
|
|
|
currentView,
|
|
|
|
|
customRange,
|
|
|
|
|
detailMode,
|
|
|
|
|
docSearch,
|
|
|
|
|
draft,
|
|
|
|
|
filteredDocuments,
|
|
|
|
|
filteredRequests,
|
|
|
|
|
filters,
|
|
|
|
|
handleApprove,
|
2026-05-13 03:29:10 +00:00
|
|
|
handleDraftSaved,
|
2026-05-06 11:00:38 +08:00
|
|
|
handleNavigate,
|
|
|
|
|
handleOpenChat,
|
|
|
|
|
handleReject,
|
2026-05-13 03:29:10 +00:00
|
|
|
handleRequestDeleted,
|
|
|
|
|
handleRequestUpdated,
|
2026-05-06 11:00:38 +08:00
|
|
|
handleUpload,
|
|
|
|
|
messageList,
|
|
|
|
|
messages,
|
|
|
|
|
navItems,
|
|
|
|
|
openChat,
|
|
|
|
|
openNewChat,
|
|
|
|
|
openRequestDetail,
|
|
|
|
|
openSmartEntry,
|
|
|
|
|
openTravelCreate,
|
|
|
|
|
prompts,
|
|
|
|
|
ranges,
|
2026-05-13 03:29:10 +00:00
|
|
|
requestSummary,
|
|
|
|
|
requestsError,
|
|
|
|
|
requestsLoading,
|
|
|
|
|
reloadRequests,
|
2026-05-06 11:00:38 +08:00
|
|
|
requests,
|
|
|
|
|
search,
|
2026-05-13 03:29:10 +00:00
|
|
|
selectedRequest,
|
2026-05-06 11:00:38 +08:00
|
|
|
sendMessage,
|
2026-05-12 01:27:49 +00:00
|
|
|
sending,
|
2026-05-06 11:00:38 +08:00
|
|
|
setView,
|
|
|
|
|
smartEntryContext,
|
|
|
|
|
smartEntryOpen,
|
|
|
|
|
smartEntrySessionId,
|
|
|
|
|
toast,
|
|
|
|
|
topBarView,
|
|
|
|
|
travelCreateMode,
|
|
|
|
|
travelPrompts,
|
|
|
|
|
uploadedFiles
|
|
|
|
|
}
|
|
|
|
|
}
|