Files
X-Financial/web/src/composables/useAppShell.js

264 lines
6.7 KiB
JavaScript

import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useNavigation, navItems } from './useNavigation.js'
import { useRequests } from './useRequests.js'
import { useToast } from './useToast.js'
import { normalizeRequestForUi } from '../utils/requestViewModel.js'
function isPlaceholderValue(value) {
const text = String(value || '').trim()
if (!text) {
return true
}
return ['待补充', '暂无', '无', '未知', '处理中'].includes(text.replace(/\s+/g, ''))
}
function hasMissingAttachment(request) {
const expenseItems = Array.isArray(request?.expenseItems) ? request.expenseItems : []
if (expenseItems.length) {
return expenseItems.some((item) => !String(item?.invoiceId || item?.invoice_id || '').trim())
}
const attachmentSummary = String(request?.attachmentSummary || '').trim()
const secondaryStatusValue = String(request?.secondaryStatusValue || '').trim()
return /待|缺|未/.test(attachmentSummary) || /待|缺|未/.test(secondaryStatusValue)
}
function hasPendingInfo(request) {
if (!request) {
return false
}
if (request.approvalKey === 'draft' || request.approvalKey === 'supplement') {
return true
}
if (!Number.isFinite(Number(request.amountValue)) || Number(request.amountValue) <= 0) {
return true
}
return [
request.profileDepartment,
request.profilePosition,
request.profileGrade,
request.profileManager,
request.reason,
request.occurredDisplay
].some(isPlaceholderValue)
}
function resolveDetailAlertTone(request) {
if (request?.approvalKey === 'completed') return 'success'
if (request?.approvalKey === 'rejected') return 'danger'
return 'warning'
}
function buildDetailAlerts(request) {
if (!request) {
return []
}
const alerts = []
const nodeLabel = String(request.node || request.approval || '').trim()
if (nodeLabel) {
alerts.push({ label: nodeLabel, tone: resolveDetailAlertTone(request) })
}
if (hasMissingAttachment(request)) {
alerts.push({ label: '缺少票据', tone: 'warning' })
}
if (hasPendingInfo(request)) {
alerts.push({ label: '待补信息', tone: 'warning' })
}
return alerts.filter((item, index, list) => list.findIndex((entry) => entry.label === item.label) === index).slice(0, 3)
}
export function useAppShell() {
const route = useRoute()
const router = useRouter()
const smartEntryOpen = ref(false)
const smartEntryContext = ref({ prompt: '', source: 'requests', request: null, files: [], conversation: null })
const smartEntrySessionId = ref(0)
const { activeView, currentView, setView } = useNavigation()
const {
requests,
loading: requestsLoading,
error: requestsError,
search,
filters,
ranges,
activeRange,
filteredRequests,
approveRequest,
rejectRequest,
reload: reloadRequests
} = useRequests()
const { toast } = useToast()
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 detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : []))
const topBarView = computed(() => {
if (detailMode.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 }
smartEntrySessionId.value += 1
}
function openSmartEntry(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: payload.conversation ?? null
}
smartEntrySessionId.value += 1
}
function closeSmartEntry() {
smartEntryOpen.value = false
}
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' })
}
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()
}
async function handleRequestDeleted() {
await reloadRequests()
router.push({ name: 'app-requests' })
}
return {
activeRange,
activeView,
closeRequestDetail,
closeSmartEntry,
currentView,
customRange,
detailMode,
filteredRequests,
filters,
handleApprove,
handleDraftSaved,
handleNavigate,
handleReject,
handleRequestDeleted,
handleRequestUpdated,
navItems,
openRequestDetail,
openSmartEntry,
openTravelCreate,
ranges,
requestSummary,
requestsError,
requestsLoading,
reloadRequests,
requests,
search,
selectedRequest,
setView,
smartEntryContext,
smartEntryOpen,
smartEntrySessionId,
detailAlerts,
toast,
topBarView
}
}