import { computed, ref, watch } from 'vue' import TableLoadingState from '../../components/shared/TableLoadingState.vue' import TableEmptyState from '../../components/shared/TableEmptyState.vue' import { useApprovalInbox } from '../../composables/useApprovalInbox.js' import { useSystemState } from '../../composables/useSystemState.js' import { fetchApprovalExpenseClaims } from '../../services/reimbursements.js' import { listPendingApprovalRequests } from '../../utils/approvalInbox.js' import TravelRequestDetailView from '../TravelRequestDetailView.vue' const DEFAULT_SLA_HOURS = 24 const tabs = ['全部待审', '高风险', '即将超时', '已处理'] const filters = ['法人主体', '费用类型', '风险等级', '金额区间', '所属部门'] const RISK_LABELS = { low: '低风险', medium: '中风险', high: '高风险' } function toDate(value) { if (!value) { return null } const nextDate = new Date(value) return Number.isNaN(nextDate.getTime()) ? null : nextDate } function formatCurrency(value) { const amount = Number(value) return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY', minimumFractionDigits: 0, maximumFractionDigits: Number.isInteger(amount) ? 0 : 2 }).format(Number.isFinite(amount) ? amount : 0) } function resolveRiskTone(riskFlags, riskSummary) { if (Array.isArray(riskFlags)) { const severities = riskFlags .map((item) => String(item?.severity || '').trim().toLowerCase()) .filter(Boolean) if (severities.includes('high')) { return 'high' } if (severities.includes('medium')) { return 'medium' } if (severities.includes('low')) { return 'low' } } if (String(riskSummary || '').trim() && String(riskSummary || '').trim() !== '无') { return 'medium' } return 'low' } function resolveSlaMeta(submittedAt) { const startAt = toDate(submittedAt) if (!startAt) { return { label: '待处理', tone: 'safe', urgent: false } } const deadline = new Date(startAt.getTime() + DEFAULT_SLA_HOURS * 60 * 60 * 1000) const diffMs = deadline.getTime() - Date.now() if (diffMs <= 0) { return { label: '已超时', tone: 'danger', urgent: true } } const diffHours = diffMs / (60 * 60 * 1000) const diffMinutes = Math.max(1, Math.ceil(diffMs / (60 * 1000))) const label = diffHours >= 1 ? `${diffHours.toFixed(diffHours >= 10 ? 0 : 1)}h` : `${diffMinutes}m` if (diffHours <= 2) { return { label, tone: 'danger', urgent: true } } if (diffHours <= 8) { return { label, tone: 'warning', urgent: false } } return { label, tone: 'safe', urgent: false } } function buildApprovalRow(request) { const riskTone = resolveRiskTone(request.riskFlags, request.riskSummary) const slaMeta = resolveSlaMeta(request.submittedAt || request.createdAt) const statusTone = slaMeta.urgent ? 'urgent' : 'pending' return { ...request, applicant: request.person, avatar: String(request.person || '?').trim().slice(0, 1) || '?', department: request.dept, type: request.typeLabel, amount: formatCurrency(request.amount), time: request.applyTime, risk: RISK_LABELS[riskTone] || RISK_LABELS.low, riskTone, sla: slaMeta.label, slaTone: slaMeta.tone, node: request.workflowNode || '审批中', status: statusTone === 'urgent' ? '即将超时' : '待审批', statusTone, spotlight: riskTone === 'high' || statusTone === 'urgent' } } export default { name: 'ApprovalCenterView', components: { TravelRequestDetailView, TableLoadingState, TableEmptyState }, setup() { const { currentUser } = useSystemState() const { markClaimViewed, syncPendingClaimIds } = useApprovalInbox() const activeTab = ref('全部待审') const selectedClaimId = ref('') const listKeyword = ref('') const rows = ref([]) const loading = ref(false) const error = ref('') watch( () => selectedClaimId.value, (claimId) => { if (claimId) { markClaimViewed(claimId) } } ) const selectedRow = computed({ get() { return rows.value.find((row) => row.claimId === selectedClaimId.value) || null }, set(value) { selectedClaimId.value = value?.claimId || '' } }) const visibleRows = computed(() => { let filteredRows = rows.value if (activeTab.value === '高风险') { filteredRows = filteredRows.filter((row) => row.riskTone === 'high') } else if (activeTab.value === '即将超时') { filteredRows = filteredRows.filter((row) => row.statusTone === 'urgent') } else if (activeTab.value === '已处理') { filteredRows = [] } if (listKeyword.value.trim()) { const keyword = listKeyword.value.trim().toLowerCase() filteredRows = filteredRows.filter((row) => ( String(row.id || '').toLowerCase().includes(keyword) || String(row.applicant || '').toLowerCase().includes(keyword) || String(row.department || '').toLowerCase().includes(keyword) || String(row.type || '').toLowerCase().includes(keyword) || String(row.amount || '').toLowerCase().includes(keyword) )) } return filteredRows }) const showEmpty = computed(() => !loading.value && !error.value && visibleRows.value.length === 0) const approvalEmptyState = computed(() => { if (!rows.value.length) { return { eyebrow: '审批中心', title: '当前没有待审批单据', desc: '进入直属领导或财务审批节点的报销单会自动汇总到这里,后续可继续处理或跟踪。', icon: 'mdi mdi-clipboard-check-outline', actionLabel: null, actionIcon: null, tone: 'slate', artLabel: 'QUEUE', tips: ['当前仅展示你有权限处理的单据', '高风险和即将超时单据会优先高亮'] } } return { eyebrow: '状态列表为空', title: `“${activeTab.value}”里暂时没有单据`, desc: activeTab.value === '已处理' ? '当前视图还没有已处理审批数据,可以先回到全部待审继续处理。' : '可以切换到其他状态查看,或返回全部待审列表继续处理。', icon: activeTab.value === '已处理' ? 'mdi mdi-archive-clock-outline' : 'mdi mdi-view-list-outline', actionLabel: '查看全部待审', actionIcon: 'mdi mdi-format-list-bulleted', tone: activeTab.value === '已处理' ? 'amber' : 'sky', artLabel: activeTab.value === '已处理' ? 'DONE' : 'FILTER', tips: ['分页与表格只在有数据时展示', '空态页面会保留当前页签上下文说明'] } }) function handleEmptyAction() { if (!rows.value.length) { void reload() return } activeTab.value = '全部待审' } function closeSelectedDetail() { selectedClaimId.value = '' } async function handleDetailUpdated() { selectedClaimId.value = '' await reload() } async function handleDetailDeleted() { selectedClaimId.value = '' await reload() } async function reload() { loading.value = true error.value = '' try { const payload = await fetchApprovalExpenseClaims() const pendingRequests = listPendingApprovalRequests(payload, currentUser.value) const mappedRows = pendingRequests.map((item) => buildApprovalRow(item)) rows.value = mappedRows syncPendingClaimIds(mappedRows.map((item) => item.claimId)) if (!mappedRows.some((item) => item.claimId === selectedClaimId.value)) { selectedClaimId.value = '' } } catch (nextError) { rows.value = [] selectedClaimId.value = '' error.value = nextError instanceof Error ? nextError.message : '审批中心加载失败。' } finally { loading.value = false } } void reload() return { activeTab, approvalEmptyState, closeSelectedDetail, error, filters, handleDetailDeleted, handleDetailUpdated, handleEmptyAction, listKeyword, loading, reload, rows, selectedRow, showEmpty, tabs, visibleRows } } }