feat(web): update composables
- useAppShell.js: update app shell composable - useNavigation.js: update navigation composable - useRequests.js: update requests composable
This commit is contained in:
@@ -18,8 +18,19 @@ export function useAppShell() {
|
|||||||
const smartEntrySessionId = ref(0)
|
const smartEntrySessionId = ref(0)
|
||||||
|
|
||||||
const { activeView, currentView, setView } = useNavigation()
|
const { activeView, currentView, setView } = useNavigation()
|
||||||
const { requests, search, filters, ranges, activeRange, filteredRequests, approveRequest, rejectRequest } =
|
const {
|
||||||
useRequests()
|
requests,
|
||||||
|
loading: requestsLoading,
|
||||||
|
error: requestsError,
|
||||||
|
search,
|
||||||
|
filters,
|
||||||
|
ranges,
|
||||||
|
activeRange,
|
||||||
|
filteredRequests,
|
||||||
|
approveRequest,
|
||||||
|
rejectRequest,
|
||||||
|
reload: reloadRequests
|
||||||
|
} = useRequests()
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
draft,
|
draft,
|
||||||
@@ -32,22 +43,23 @@ export function useAppShell() {
|
|||||||
handleUpload,
|
handleUpload,
|
||||||
openChat,
|
openChat,
|
||||||
openNewChat
|
openNewChat
|
||||||
} =
|
} = useChat(activeView)
|
||||||
useChat(activeView)
|
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
|
||||||
const docSearch = ref('')
|
const docSearch = ref('')
|
||||||
const customRange = ref({ start: '2024-07-06', end: '2024-07-12' })
|
const customRange = ref({ start: '2024-07-06', end: '2024-07-12' })
|
||||||
const travelPrompts = ['生成差旅摘要', '识别报销风险', '核对审批链', '提取随附票据', '生成沟通建议']
|
const travelPrompts = ['生成差旅摘要', '识别报销风险', '核对审批链', '提取随附票据', '生成沟通建议']
|
||||||
|
|
||||||
const selectedTravelRequest = computed(() => {
|
const selectedRequest = computed(() => {
|
||||||
const requestId = String(route.params.requestId || '')
|
const requestId = String(route.params.requestId || '')
|
||||||
|
|
||||||
if (!requestId) {
|
if (!requestId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawRequest = requests.value.find((item) => String(item.id) === requestId)
|
const rawRequest = requests.value.find(
|
||||||
|
(item) => String(item.claimId || '').trim() === requestId || String(item.id || '').trim() === requestId
|
||||||
|
)
|
||||||
return normalizeRequestForUi(rawRequest)
|
return normalizeRequestForUi(rawRequest)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -56,14 +68,40 @@ export function useAppShell() {
|
|||||||
const topBarView = computed(() => {
|
const topBarView = computed(() => {
|
||||||
if (detailMode.value) {
|
if (detailMode.value) {
|
||||||
return {
|
return {
|
||||||
title: '差旅申请详情',
|
title: '报销单详情',
|
||||||
desc: '查看申请单、票据、审批意见与风控提示。'
|
desc: '查看报销明细、票据材料、审批进度与风险提示。'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentView.value
|
return currentView.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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 }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const filteredDocuments = computed(() => {
|
const filteredDocuments = computed(() => {
|
||||||
const key = docSearch.value.trim().toLowerCase()
|
const key = docSearch.value.trim().toLowerCase()
|
||||||
return documents.filter((doc) => !key || `${doc.id}${doc.applicant}${doc.destination}${doc.type}`.toLowerCase().includes(key))
|
return documents.filter((doc) => !key || `${doc.id}${doc.applicant}${doc.destination}${doc.type}`.toLowerCase().includes(key))
|
||||||
@@ -104,7 +142,7 @@ export function useAppShell() {
|
|||||||
smartEntryContext.value = {
|
smartEntryContext.value = {
|
||||||
prompt: payload.prompt ?? '',
|
prompt: payload.prompt ?? '',
|
||||||
source: payload.source ?? 'workbench',
|
source: payload.source ?? 'workbench',
|
||||||
request: payload.request ?? selectedTravelRequest.value,
|
request: payload.request ?? selectedRequest.value,
|
||||||
files: Array.isArray(payload.files) ? payload.files : [],
|
files: Array.isArray(payload.files) ? payload.files : [],
|
||||||
conversation: payload.conversation ?? null
|
conversation: payload.conversation ?? null
|
||||||
}
|
}
|
||||||
@@ -115,10 +153,18 @@ export function useAppShell() {
|
|||||||
smartEntryOpen.value = false
|
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) {
|
function openRequestDetail(request) {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'app-request-detail',
|
name: 'app-request-detail',
|
||||||
params: { requestId: request.id }
|
params: { requestId: request.claimId || request.id }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +172,15 @@ export function useAppShell() {
|
|||||||
router.push({ name: 'app-requests' })
|
router.push({ name: 'app-requests' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRequestUpdated() {
|
||||||
|
await reloadRequests()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRequestDeleted() {
|
||||||
|
await reloadRequests()
|
||||||
|
router.push({ name: 'app-requests' })
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeCase,
|
activeCase,
|
||||||
activeRange,
|
activeRange,
|
||||||
@@ -141,9 +196,12 @@ export function useAppShell() {
|
|||||||
filteredRequests,
|
filteredRequests,
|
||||||
filters,
|
filters,
|
||||||
handleApprove,
|
handleApprove,
|
||||||
|
handleDraftSaved,
|
||||||
handleNavigate,
|
handleNavigate,
|
||||||
handleOpenChat,
|
handleOpenChat,
|
||||||
handleReject,
|
handleReject,
|
||||||
|
handleRequestDeleted,
|
||||||
|
handleRequestUpdated,
|
||||||
handleUpload,
|
handleUpload,
|
||||||
messageList,
|
messageList,
|
||||||
messages,
|
messages,
|
||||||
@@ -155,9 +213,13 @@ export function useAppShell() {
|
|||||||
openTravelCreate,
|
openTravelCreate,
|
||||||
prompts,
|
prompts,
|
||||||
ranges,
|
ranges,
|
||||||
|
requestSummary,
|
||||||
|
requestsError,
|
||||||
|
requestsLoading,
|
||||||
|
reloadRequests,
|
||||||
requests,
|
requests,
|
||||||
search,
|
search,
|
||||||
selectedTravelRequest,
|
selectedRequest,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
sending,
|
sending,
|
||||||
setView,
|
setView,
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ export const navItems = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'requests',
|
id: 'requests',
|
||||||
label: '申请单',
|
label: '个人报销',
|
||||||
navHint: '查看和管理申请单',
|
navHint: '查看和管理个人报销',
|
||||||
icon: icons.list,
|
icon: icons.list,
|
||||||
title: '差旅申请与单据',
|
title: '个人报销',
|
||||||
desc: '集中查看申请单状态、处理进度和风险提示。'
|
desc: '集中查看草稿、审批进度、票据状态与风险提示。'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'approval',
|
id: 'approval',
|
||||||
|
|||||||
@@ -1,60 +1,463 @@
|
|||||||
import { computed, reactive, ref } from 'vue'
|
import { computed, reactive, ref } from 'vue'
|
||||||
import { initialRequests } from '../data/requests.js'
|
|
||||||
|
import { fetchExpenseClaims } from '../services/reimbursements.js'
|
||||||
|
|
||||||
|
const EXPENSE_TYPE_LABELS = {
|
||||||
|
travel: '差旅费',
|
||||||
|
entertainment: '业务招待费',
|
||||||
|
office: '办公费',
|
||||||
|
meeting: '会务费',
|
||||||
|
training: '培训费',
|
||||||
|
hotel: '住宿费',
|
||||||
|
transport: '交通费',
|
||||||
|
meal: '餐费',
|
||||||
|
other: '其他费用'
|
||||||
|
}
|
||||||
|
|
||||||
|
const REIMBURSEMENT_PROGRESS_LABELS = [
|
||||||
|
'保存草稿',
|
||||||
|
'待提交',
|
||||||
|
'AI验审',
|
||||||
|
'直属领导审批',
|
||||||
|
'财务审批',
|
||||||
|
'归档入账'
|
||||||
|
]
|
||||||
|
|
||||||
|
function parseNumber(value) {
|
||||||
|
const nextValue = Number(value)
|
||||||
|
return Number.isFinite(nextValue) ? nextValue : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDate(value) {
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextDate = new Date(value)
|
||||||
|
return Number.isNaN(nextDate.getTime()) ? null : nextDate
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(value) {
|
||||||
|
const nextDate = toDate(value)
|
||||||
|
if (!nextDate) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const year = nextDate.getFullYear()
|
||||||
|
const month = String(nextDate.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(nextDate.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value) {
|
||||||
|
const nextDate = toDate(value)
|
||||||
|
if (!nextDate) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = String(nextDate.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(nextDate.getMinutes()).padStart(2, '0')
|
||||||
|
return `${formatDate(nextDate)} ${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAmount(value) {
|
||||||
|
return new Intl.NumberFormat('zh-CN', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'CNY',
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: Number.isInteger(value) ? 0 : 2
|
||||||
|
}).format(parseNumber(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTypeLabel(typeCode) {
|
||||||
|
return EXPENSE_TYPE_LABELS[String(typeCode || '').trim()] || EXPENSE_TYPE_LABELS.other
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveApprovalMeta(status) {
|
||||||
|
const normalized = String(status || '').trim().toLowerCase()
|
||||||
|
|
||||||
|
if (normalized === 'draft') {
|
||||||
|
return { key: 'draft', label: '草稿', tone: 'draft' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['supplement', 'returned'].includes(normalized)) {
|
||||||
|
return { key: 'supplement', label: '待补充', tone: 'warning' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['approved', 'completed', 'paid'].includes(normalized)) {
|
||||||
|
return { key: 'completed', label: '已完成', tone: 'success' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['rejected', 'cancelled'].includes(normalized)) {
|
||||||
|
return { key: 'rejected', label: '已退回', tone: 'danger' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key: 'in_progress', label: '审批中', tone: 'info' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveWorkflowNode(claim, approvalMeta) {
|
||||||
|
const rawNode = String(claim?.approval_stage || '').trim()
|
||||||
|
|
||||||
|
if (rawNode) {
|
||||||
|
if (rawNode === '审批流转') {
|
||||||
|
return 'AI验审'
|
||||||
|
}
|
||||||
|
if (rawNode === '待补充') {
|
||||||
|
return approvalMeta.key === 'draft' ? '待提交' : 'AI验审'
|
||||||
|
}
|
||||||
|
return rawNode
|
||||||
|
}
|
||||||
|
|
||||||
|
if (approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' || approvalMeta.key === 'rejected') {
|
||||||
|
return '待提交'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (approvalMeta.key === 'completed') {
|
||||||
|
return '归档入账'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'AI验审'
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyRiskFlag(value) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value || typeof value !== 'object') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of ['message', 'label', 'reason', 'name']) {
|
||||||
|
const nextValue = String(value[key] || '').trim()
|
||||||
|
if (nextValue) {
|
||||||
|
return nextValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRiskSummary(riskFlags) {
|
||||||
|
if (!Array.isArray(riskFlags) || !riskFlags.length) {
|
||||||
|
return '无'
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = riskFlags.map((item) => stringifyRiskFlag(item)).filter(Boolean)
|
||||||
|
return items.length ? items.join(';') : '无'
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOccurredDisplay(claim) {
|
||||||
|
const itemDates = Array.isArray(claim?.items)
|
||||||
|
? claim.items.map((item) => formatDate(item?.item_date)).filter(Boolean)
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (!itemDates.length) {
|
||||||
|
return formatDate(claim?.occurred_at) || '待补充'
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedDates = [...new Set(itemDates)].sort()
|
||||||
|
if (sortedDates.length === 1) {
|
||||||
|
return sortedDates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${sortedDates[0]} ~ ${sortedDates[sortedDates.length - 1]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveProgressCurrentIndex(approvalMeta, workflowNode) {
|
||||||
|
const normalizedNode = String(workflowNode || '').trim()
|
||||||
|
|
||||||
|
if (approvalMeta.key === 'completed') {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedNode.includes('归档') || normalizedNode.includes('入账')) {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
if (normalizedNode.includes('财务')) {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
normalizedNode.includes('直属领导')
|
||||||
|
|| normalizedNode.includes('领导审批')
|
||||||
|
|| normalizedNode.includes('部门负责人')
|
||||||
|
|| normalizedNode.includes('负责人审批')
|
||||||
|
) {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
if (normalizedNode.includes('AI验审') || normalizedNode.includes('审批流转')) {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if (normalizedNode.includes('待提交')) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' || approvalMeta.key === 'rejected') {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProgressSteps(approvalMeta, workflowNode) {
|
||||||
|
const currentIndex = resolveProgressCurrentIndex(approvalMeta, workflowNode)
|
||||||
|
const currentTime =
|
||||||
|
approvalMeta.key === 'completed'
|
||||||
|
? '已完成'
|
||||||
|
: approvalMeta.key === 'supplement'
|
||||||
|
? '待补充'
|
||||||
|
: approvalMeta.key === 'rejected'
|
||||||
|
? '已退回'
|
||||||
|
: '进行中'
|
||||||
|
|
||||||
|
return REIMBURSEMENT_PROGRESS_LABELS.map((label, index) => {
|
||||||
|
if (approvalMeta.key === 'completed') {
|
||||||
|
return {
|
||||||
|
index: index + 1,
|
||||||
|
label,
|
||||||
|
time: '已完成',
|
||||||
|
done: true,
|
||||||
|
active: true,
|
||||||
|
current: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < currentIndex) {
|
||||||
|
return {
|
||||||
|
index: index + 1,
|
||||||
|
label,
|
||||||
|
time: '已完成',
|
||||||
|
done: true,
|
||||||
|
active: true,
|
||||||
|
current: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === currentIndex) {
|
||||||
|
return {
|
||||||
|
index: index + 1,
|
||||||
|
label,
|
||||||
|
time: currentTime,
|
||||||
|
done: false,
|
||||||
|
active: true,
|
||||||
|
current: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
index: index + 1,
|
||||||
|
label,
|
||||||
|
time: '待处理',
|
||||||
|
done: false,
|
||||||
|
active: false,
|
||||||
|
current: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExpenseItems(claim, riskSummary) {
|
||||||
|
if (!Array.isArray(claim?.items)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return claim.items.map((item, index) => {
|
||||||
|
const attachments = String(item?.invoice_id || '').trim() ? [String(item.invoice_id).trim()] : []
|
||||||
|
const itemTypeLabel = resolveTypeLabel(item?.item_type || claim?.expense_type)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(item?.id || `${claim?.id || 'claim'}-item-${index}`),
|
||||||
|
time: formatDate(item?.item_date) || '待补充',
|
||||||
|
itemDate: formatDate(item?.item_date) || '',
|
||||||
|
itemType: String(item?.item_type || claim?.expense_type || '').trim() || 'other',
|
||||||
|
itemReason: String(item?.item_reason || claim?.reason || '').trim(),
|
||||||
|
itemLocation: String(item?.item_location || claim?.location || '').trim(),
|
||||||
|
itemAmount: parseNumber(item?.item_amount),
|
||||||
|
invoiceId: String(item?.invoice_id || '').trim(),
|
||||||
|
dayLabel: claim?.expense_type === 'travel' ? `第 ${index + 1} 项` : '业务发生项',
|
||||||
|
name: itemTypeLabel,
|
||||||
|
category: itemTypeLabel,
|
||||||
|
desc: String(item?.item_reason || claim?.reason || '').trim() || '待补充',
|
||||||
|
detail: String(item?.item_location || claim?.location || '').trim() || '待补充',
|
||||||
|
amount: formatAmount(item?.item_amount),
|
||||||
|
status: attachments.length ? '已识别' : '待补充',
|
||||||
|
tone: attachments.length ? 'ok' : 'bad',
|
||||||
|
attachmentStatus: attachments.length ? `${attachments.length} 份附件` : '待上传',
|
||||||
|
attachmentHint: attachments.length ? attachments[0] : '暂无关联票据',
|
||||||
|
attachmentTone: attachments.length ? 'ok' : 'missing',
|
||||||
|
attachments,
|
||||||
|
riskLabel: riskSummary === '无' ? '无' : '待关注',
|
||||||
|
riskText: riskSummary === '无' ? '' : riskSummary,
|
||||||
|
riskTone: riskSummary === '无' ? 'low' : 'medium'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapExpenseClaimToRequest(claim) {
|
||||||
|
const typeCode = String(claim?.expense_type || '').trim() || 'other'
|
||||||
|
const typeLabel = resolveTypeLabel(typeCode)
|
||||||
|
const approvalMeta = resolveApprovalMeta(claim?.status)
|
||||||
|
const workflowNode = resolveWorkflowNode(claim, approvalMeta)
|
||||||
|
const invoiceCount = Math.max(0, parseNumber(claim?.invoice_count))
|
||||||
|
const riskSummary = buildRiskSummary(claim?.risk_flags_json)
|
||||||
|
const expenseItems = buildExpenseItems(claim, riskSummary)
|
||||||
|
const applyDateTime = claim?.submitted_at || claim?.created_at
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(claim?.claim_no || claim?.id || '').trim(),
|
||||||
|
claimNo: String(claim?.claim_no || claim?.id || '').trim(),
|
||||||
|
claimId: String(claim?.id || '').trim(),
|
||||||
|
status: String(claim?.status || '').trim(),
|
||||||
|
person: String(claim?.employee_name || '').trim() || '待补充',
|
||||||
|
dept: String(claim?.department_name || '').trim() || '待补充',
|
||||||
|
departmentName: String(claim?.department_name || '').trim() || '待补充',
|
||||||
|
employeeName: String(claim?.employee_name || '').trim() || '待补充',
|
||||||
|
entity: '',
|
||||||
|
typeCode,
|
||||||
|
typeLabel,
|
||||||
|
detailVariant: typeCode === 'travel' ? 'travel' : 'general',
|
||||||
|
title: String(claim?.reason || '').trim() || `${typeLabel}报销`,
|
||||||
|
sceneLabel: typeLabel,
|
||||||
|
sceneTarget: String(claim?.location || '').trim() || '待补充',
|
||||||
|
location: String(claim?.location || '').trim() || '待补充',
|
||||||
|
relatedCustomer: '',
|
||||||
|
occurredDisplay: buildOccurredDisplay(claim),
|
||||||
|
occurredAt: claim?.occurred_at || '',
|
||||||
|
applyTime: formatDateTime(applyDateTime) || '待补充',
|
||||||
|
submittedAt: applyDateTime || '',
|
||||||
|
createdAt: claim?.created_at || '',
|
||||||
|
amount: parseNumber(claim?.amount),
|
||||||
|
workflowNode,
|
||||||
|
approvalKey: approvalMeta.key,
|
||||||
|
approvalStatus: approvalMeta.label,
|
||||||
|
approvalTone: approvalMeta.tone,
|
||||||
|
secondaryStatusLabel: typeCode === 'travel' ? '行程状态' : '票据状态',
|
||||||
|
secondaryStatusValue: invoiceCount > 0 ? `已关联 ${invoiceCount} 张票据` : '待上传票据',
|
||||||
|
secondaryStatusTone: invoiceCount > 0 ? 'success' : 'warning',
|
||||||
|
riskSummary,
|
||||||
|
attachmentSummary: invoiceCount > 0 ? `${invoiceCount} 张票据` : '无',
|
||||||
|
expenseTableSummary: expenseItems.length
|
||||||
|
? (invoiceCount > 0
|
||||||
|
? `共 ${expenseItems.length} 条费用明细,已关联 ${invoiceCount} 张票据`
|
||||||
|
: `共 ${expenseItems.length} 条费用明细,待补充票据`)
|
||||||
|
: '暂无费用明细',
|
||||||
|
note: String(claim?.reason || '').trim(),
|
||||||
|
progressSteps: buildProgressSteps(approvalMeta, workflowNode),
|
||||||
|
expenseItems
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWeekStart(date) {
|
||||||
|
const nextDate = new Date(date)
|
||||||
|
const day = nextDate.getDay() || 7
|
||||||
|
nextDate.setHours(0, 0, 0, 0)
|
||||||
|
nextDate.setDate(nextDate.getDate() - day + 1)
|
||||||
|
return nextDate
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRangeMatch(activeRange, item) {
|
||||||
|
if (activeRange === 'custom' || activeRange === '本月') {
|
||||||
|
if (activeRange !== '本月') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetDate = toDate(item?.submittedAt || item?.createdAt || item?.occurredAt)
|
||||||
|
if (!targetDate) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const targetDay = formatDate(targetDate)
|
||||||
|
|
||||||
|
if (activeRange === '今日') {
|
||||||
|
return targetDay === formatDate(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeRange === '本周') {
|
||||||
|
const weekStart = getWeekStart(now)
|
||||||
|
const nextWeekStart = new Date(weekStart)
|
||||||
|
nextWeekStart.setDate(nextWeekStart.getDate() + 7)
|
||||||
|
return targetDate >= weekStart && targetDate < nextWeekStart
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeRange === '本月') {
|
||||||
|
return (
|
||||||
|
targetDate.getFullYear() === now.getFullYear()
|
||||||
|
&& targetDate.getMonth() === now.getMonth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
export function useRequests() {
|
export function useRequests() {
|
||||||
const requests = ref(initialRequests)
|
const requests = ref([])
|
||||||
const entityMap = {
|
const loading = ref(false)
|
||||||
'Northstar China Ltd.': 'Northstar China Ltd.',
|
const error = ref('')
|
||||||
'Northstar Singapore Pte.': 'Northstar Singapore Pte.',
|
|
||||||
'Northstar US Inc.': 'Northstar US Inc.'
|
|
||||||
}
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const filters = reactive({ entity: '全部主体', category: '全部费用', risk: '全部风险' })
|
const filters = reactive({ entity: '全部主体', category: '全部类型', risk: '全部状态' })
|
||||||
const ranges = ['今日', '本周', '本月']
|
const ranges = ['今日', '本周', '本月']
|
||||||
const activeRange = ref('本周')
|
const activeRange = ref('本周')
|
||||||
|
|
||||||
const filteredRequests = computed(() => {
|
const filteredRequests = computed(() => {
|
||||||
const key = search.value.trim().toLowerCase()
|
const key = search.value.trim().toLowerCase()
|
||||||
|
|
||||||
return requests.value.filter((item) => {
|
return requests.value.filter((item) => {
|
||||||
const matchesSearch = !key || `${item.id}${item.person}${item.category}${item.risk}`.toLowerCase().includes(key)
|
const searchText = [
|
||||||
const matchesEntity = filters.entity === '全部主体' || item.entity === entityMap[filters.entity]
|
item.id,
|
||||||
const matchesCategory = filters.category === '全部费用' || item.category === filters.category
|
item.person,
|
||||||
const matchesRisk = filters.risk === '全部风险'
|
item.typeLabel,
|
||||||
|| (filters.risk === '高风险' && item.status === 'danger')
|
item.title,
|
||||||
|| (filters.risk === '需解释' && item.status === 'warning')
|
item.sceneTarget,
|
||||||
|| (filters.risk === '低风险' && item.status === 'success')
|
item.riskSummary
|
||||||
const matchesRange = activeRange.value === 'custom'
|
]
|
||||||
|| activeRange.value === '本月'
|
.filter(Boolean)
|
||||||
|| (activeRange.value === '本周' && item.range !== '本月')
|
.join('')
|
||||||
|| (activeRange.value === '今日' && item.range === '今日')
|
.toLowerCase()
|
||||||
return matchesSearch && matchesEntity && matchesCategory && matchesRisk && matchesRange
|
|
||||||
|
const matchesSearch = !key || searchText.includes(key)
|
||||||
|
const matchesRange = resolveRangeMatch(activeRange.value, item)
|
||||||
|
|
||||||
|
return matchesSearch && matchesRange
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function updateRequest(requestId, updates) {
|
async function reload() {
|
||||||
requests.value = requests.value.map((item) => (item.id === requestId ? { ...item, ...updates } : item))
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await fetchExpenseClaims()
|
||||||
|
requests.value = Array.isArray(payload) ? payload.map((item) => mapExpenseClaimToRequest(item)) : []
|
||||||
|
} catch (nextError) {
|
||||||
|
requests.value = []
|
||||||
|
error.value = nextError instanceof Error ? nextError.message : '个人报销列表加载失败。'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function approveRequest(request) {
|
function approveRequest(request) {
|
||||||
updateRequest(request.id, {
|
return `${request.id} 未执行本地状态变更,列表当前只展示后端真实数据。`
|
||||||
verdict: '已通过',
|
|
||||||
status: 'success',
|
|
||||||
risk: '已完成人工确认'
|
|
||||||
})
|
|
||||||
return `${request.id} 已标记为通过,审计日志已更新。`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rejectRequest(request) {
|
function rejectRequest(request) {
|
||||||
updateRequest(request.id, {
|
return `${request.id} 未执行本地状态变更,列表当前只展示后端真实数据。`
|
||||||
verdict: '已退回补件',
|
|
||||||
status: 'danger',
|
|
||||||
risk: '待申请人补充差旅行程与票据'
|
|
||||||
})
|
|
||||||
return `${request.id} 已退回,系统将通知申请人补充材料。`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void reload()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requests, search, filters, ranges, activeRange,
|
requests,
|
||||||
filteredRequests, approveRequest, rejectRequest
|
loading,
|
||||||
|
error,
|
||||||
|
search,
|
||||||
|
filters,
|
||||||
|
ranges,
|
||||||
|
activeRange,
|
||||||
|
filteredRequests,
|
||||||
|
approveRequest,
|
||||||
|
rejectRequest,
|
||||||
|
reload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user