2026-05-15 06:57:07 +00:00
|
|
|
|
import { computed, ref } from 'vue'
|
|
|
|
|
|
|
2026-05-20 14:21:56 +08:00
|
|
|
|
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
|
|
|
|
|
|
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
|
2026-05-15 06:57:07 +00:00
|
|
|
|
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
|
|
|
|
|
|
import { mapExpenseClaimToRequest } from '../../composables/useRequests.js'
|
|
|
|
|
|
import { useSystemState } from '../../composables/useSystemState.js'
|
2026-05-20 14:21:56 +08:00
|
|
|
|
import { useToast } from '../../composables/useToast.js'
|
|
|
|
|
|
import { deleteExpenseClaim, fetchExpenseClaims, returnExpenseClaim } from '../../services/reimbursements.js'
|
|
|
|
|
|
import { canManageExpenseClaims } from '../../utils/accessControl.js'
|
2026-05-15 06:57:07 +00:00
|
|
|
|
|
|
|
|
|
|
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 resolveRiskItems(request) {
|
|
|
|
|
|
const riskFlags = Array.isArray(request?.riskFlags) ? request.riskFlags : []
|
|
|
|
|
|
const items = riskFlags
|
|
|
|
|
|
.map((item) => {
|
|
|
|
|
|
const tone = resolveRiskTone([item], '')
|
|
|
|
|
|
const text = String(item?.message || item?.label || item?.reason || '').trim()
|
|
|
|
|
|
if (!text) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
text,
|
|
|
|
|
|
level: tone === 'high' ? '高' : tone === 'medium' ? '中' : '低',
|
|
|
|
|
|
tone,
|
|
|
|
|
|
icon: tone === 'high' ? 'mdi mdi-alert-circle' : tone === 'medium' ? 'mdi mdi-alert' : 'mdi mdi-shield-check'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
|
|
|
|
|
|
if (items.length) {
|
|
|
|
|
|
return items
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const summary = String(request?.riskSummary || '').trim()
|
|
|
|
|
|
if (summary && summary !== '无') {
|
|
|
|
|
|
return summary.split(';').filter(Boolean).map((text) => ({
|
|
|
|
|
|
text,
|
|
|
|
|
|
level: '中',
|
|
|
|
|
|
tone: 'medium',
|
|
|
|
|
|
icon: 'mdi mdi-alert'
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
2026-05-20 09:36:01 +08:00
|
|
|
|
text: 'AI预审已通过,当前未发现额外风险。',
|
2026-05-15 06:57:07 +00:00
|
|
|
|
level: '低',
|
|
|
|
|
|
tone: 'low',
|
|
|
|
|
|
icon: 'mdi mdi-shield-check'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveAttachmentMeta(name) {
|
|
|
|
|
|
const normalized = String(name || '').trim()
|
|
|
|
|
|
const lowerName = normalized.toLowerCase()
|
|
|
|
|
|
if (lowerName.endsWith('.pdf')) {
|
|
|
|
|
|
return { icon: 'mdi mdi-file-pdf-box', iconClass: 'pdf' }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/\.(png|jpg|jpeg|webp|bmp)$/i.test(lowerName)) {
|
|
|
|
|
|
return { icon: 'mdi mdi-image', iconClass: 'img' }
|
|
|
|
|
|
}
|
|
|
|
|
|
return { icon: 'mdi mdi-file-document-outline', iconClass: 'file' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildAttachments(expenseItems) {
|
|
|
|
|
|
const seen = new Set()
|
|
|
|
|
|
const attachments = []
|
|
|
|
|
|
|
|
|
|
|
|
for (const item of Array.isArray(expenseItems) ? expenseItems : []) {
|
|
|
|
|
|
for (const fileName of Array.isArray(item?.attachments) ? item.attachments : []) {
|
|
|
|
|
|
const normalized = String(fileName || '').trim()
|
|
|
|
|
|
if (!normalized || seen.has(normalized)) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
seen.add(normalized)
|
|
|
|
|
|
attachments.push({
|
|
|
|
|
|
name: normalized,
|
|
|
|
|
|
size: '已识别',
|
|
|
|
|
|
...resolveAttachmentMeta(normalized)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (attachments.length) {
|
|
|
|
|
|
return attachments
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '当前无附件',
|
|
|
|
|
|
size: '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-file-document-outline',
|
|
|
|
|
|
iconClass: 'miss',
|
|
|
|
|
|
missing: true
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 buildHeroSummaryItems(request) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ label: '单号', value: request.id || '-', icon: 'mdi mdi-pound-box-outline' },
|
|
|
|
|
|
{ label: '报销类型', value: request.typeLabel || '-', icon: 'mdi mdi-briefcase-outline' },
|
|
|
|
|
|
{ label: '业务地点', value: request.sceneTarget || '待补充', icon: 'mdi mdi-map-marker-outline' },
|
|
|
|
|
|
{ label: '发生时间', value: request.occurredDisplay || '待补充', icon: 'mdi mdi-calendar-range' },
|
|
|
|
|
|
{ label: '票据关联', value: request.attachmentSummary || '无', icon: 'mdi mdi-paperclip' },
|
|
|
|
|
|
{ label: '事由', value: request.title || '待补充', icon: 'mdi mdi-text-box-outline' }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildFlowItems(request) {
|
|
|
|
|
|
return Array.isArray(request?.progressSteps)
|
|
|
|
|
|
? request.progressSteps.map((item) => ({
|
|
|
|
|
|
label: item.label,
|
|
|
|
|
|
desc: item.current ? '当前处理节点' : item.done ? '已完成' : '待处理',
|
|
|
|
|
|
time: item.time,
|
|
|
|
|
|
icon: item.current ? 'mdi mdi-circle-slice-8' : item.done ? 'mdi mdi-check' : 'mdi mdi-circle-outline',
|
|
|
|
|
|
current: item.current,
|
|
|
|
|
|
pending: !item.done && !item.current
|
|
|
|
|
|
}))
|
|
|
|
|
|
: []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function canCurrentUserProcessRequest(request, currentUser) {
|
|
|
|
|
|
const node = String(request?.workflowNode || '').trim()
|
|
|
|
|
|
const currentName = String(currentUser?.name || '').trim()
|
|
|
|
|
|
const applicantName = String(request?.person || request?.employeeName || '').trim()
|
|
|
|
|
|
|
|
|
|
|
|
if (currentName && applicantName && currentName === applicantName) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 14:21:56 +08:00
|
|
|
|
if (canManageExpenseClaims(currentUser)) {
|
|
|
|
|
|
return true
|
2026-05-15 06:57:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
node.includes('直属领导')
|
|
|
|
|
|
|| node.includes('领导审批')
|
|
|
|
|
|
|| node.includes('部门负责人')
|
|
|
|
|
|
|| node.includes('负责人审批')
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildApprovalRow(request) {
|
|
|
|
|
|
const riskTone = resolveRiskTone(request.riskFlags, request.riskSummary)
|
|
|
|
|
|
const riskItems = resolveRiskItems(request)
|
|
|
|
|
|
const expenseItems = Array.isArray(request.expenseItems) ? request.expenseItems : []
|
|
|
|
|
|
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',
|
|
|
|
|
|
heroSummaryItems: buildHeroSummaryItems(request),
|
|
|
|
|
|
summaryItems: buildHeroSummaryItems(request).slice(2),
|
|
|
|
|
|
progressSteps: Array.isArray(request.progressSteps) ? request.progressSteps : [],
|
|
|
|
|
|
expenseItems,
|
|
|
|
|
|
attachments: buildAttachments(expenseItems),
|
|
|
|
|
|
riskItems,
|
|
|
|
|
|
flowItems: buildFlowItems(request)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
2026-05-15 06:57:07 +00:00
|
|
|
|
name: 'ApprovalCenterView',
|
|
|
|
|
|
components: {
|
2026-05-20 14:21:56 +08:00
|
|
|
|
ConfirmDialog,
|
|
|
|
|
|
TableLoadingState,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
TableEmptyState
|
|
|
|
|
|
},
|
|
|
|
|
|
setup() {
|
|
|
|
|
|
const { currentUser } = useSystemState()
|
2026-05-20 14:21:56 +08:00
|
|
|
|
const { toast } = useToast()
|
2026-05-06 11:00:38 +08:00
|
|
|
|
const activeTab = ref('全部待审')
|
2026-05-15 06:57:07 +00:00
|
|
|
|
const selectedClaimId = ref('')
|
2026-05-06 11:00:38 +08:00
|
|
|
|
const expandedExpenseId = ref(null)
|
2026-05-15 06:57:07 +00:00
|
|
|
|
const listKeyword = ref('')
|
|
|
|
|
|
const rows = ref([])
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const error = ref('')
|
2026-05-20 14:21:56 +08:00
|
|
|
|
const actionBusy = ref(false)
|
|
|
|
|
|
const returnDialogOpen = ref(false)
|
|
|
|
|
|
const deleteDialogOpen = ref(false)
|
2026-05-15 06:57:07 +00:00
|
|
|
|
|
|
|
|
|
|
const selectedRow = computed({
|
|
|
|
|
|
get() {
|
|
|
|
|
|
return rows.value.find((row) => row.claimId === selectedClaimId.value) || null
|
|
|
|
|
|
},
|
|
|
|
|
|
set(value) {
|
|
|
|
|
|
selectedClaimId.value = value?.claimId || ''
|
|
|
|
|
|
expandedExpenseId.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
|
|
|
|
|
const visibleRows = computed(() => {
|
2026-05-15 06:57:07 +00:00
|
|
|
|
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) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
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 showTable = computed(() => !loading.value && !error.value && visibleRows.value.length > 0)
|
|
|
|
|
|
const showEmpty = computed(() => !loading.value && !error.value && visibleRows.value.length === 0)
|
2026-05-20 14:21:56 +08:00
|
|
|
|
const canManageClaims = computed(() => canManageExpenseClaims(currentUser.value))
|
2026-05-15 06:57:07 +00:00
|
|
|
|
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: ['分页与表格只在有数据时展示', '空态页面会保留当前页签上下文说明']
|
|
|
|
|
|
}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
const approvalSteps = computed(() => selectedRow.value?.progressSteps || [])
|
|
|
|
|
|
const summaryItems = computed(() => selectedRow.value?.summaryItems || [])
|
|
|
|
|
|
const heroSummaryItems = computed(() => selectedRow.value?.heroSummaryItems || [])
|
|
|
|
|
|
const expenseItems = computed(() => selectedRow.value?.expenseItems || [])
|
|
|
|
|
|
const expenseTotal = computed(() => selectedRow.value?.amount || formatCurrency(0))
|
|
|
|
|
|
const uploadedExpenseCount = computed(
|
|
|
|
|
|
() => expenseItems.value.filter((item) => Array.isArray(item?.attachments) && item.attachments.length).length
|
|
|
|
|
|
)
|
|
|
|
|
|
const attachments = computed(() => selectedRow.value?.attachments || [])
|
|
|
|
|
|
const riskItems = computed(() => selectedRow.value?.riskItems || [])
|
|
|
|
|
|
const flowItems = computed(() => selectedRow.value?.flowItems || [])
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
|
|
|
|
|
const currentProgressRingMotion = {
|
|
|
|
|
|
initial: {
|
|
|
|
|
|
scale: 1,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
opacity: 0.34
|
2026-05-06 11:00:38 +08:00
|
|
|
|
},
|
|
|
|
|
|
enter: {
|
|
|
|
|
|
scale: [1, 1.42, 1.78],
|
|
|
|
|
|
opacity: [0.34, 0.16, 0],
|
|
|
|
|
|
transition: {
|
|
|
|
|
|
duration: 3.2,
|
|
|
|
|
|
repeat: Infinity,
|
|
|
|
|
|
repeatType: 'loop',
|
|
|
|
|
|
repeatDelay: 0.85,
|
|
|
|
|
|
ease: 'easeOut',
|
2026-05-15 06:57:07 +00:00
|
|
|
|
times: [0, 0.5, 1]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
function showExpenseRisk(item) {
|
|
|
|
|
|
return ['medium', 'high'].includes(String(item?.riskTone || '').trim())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleExpenseAttachments(id) {
|
|
|
|
|
|
expandedExpenseId.value = expandedExpenseId.value === id ? null : id
|
|
|
|
|
|
}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
function handleEmptyAction() {
|
|
|
|
|
|
if (!rows.value.length) {
|
|
|
|
|
|
void reload()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
activeTab.value = '全部待审'
|
|
|
|
|
|
}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-20 14:21:56 +08:00
|
|
|
|
function handleReturnSelected() {
|
|
|
|
|
|
if (!selectedRow.value?.claimId || !canManageClaims.value || actionBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
returnDialogOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleDeleteSelected() {
|
|
|
|
|
|
if (!selectedRow.value?.claimId || !canManageClaims.value || actionBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
deleteDialogOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeReturnDialog() {
|
|
|
|
|
|
if (!actionBusy.value) {
|
|
|
|
|
|
returnDialogOpen.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeDeleteDialog() {
|
|
|
|
|
|
if (!actionBusy.value) {
|
|
|
|
|
|
deleteDialogOpen.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function confirmReturnSelected() {
|
|
|
|
|
|
const row = selectedRow.value
|
|
|
|
|
|
if (!row?.claimId || actionBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
actionBusy.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await returnExpenseClaim(row.claimId, {
|
2026-05-20 14:32:35 +08:00
|
|
|
|
reason: '审批中心退回,请申请人调整后重新提交。'
|
2026-05-20 14:21:56 +08:00
|
|
|
|
})
|
2026-05-20 14:32:35 +08:00
|
|
|
|
toast(`${row.id} 已退回待提交。`)
|
2026-05-20 14:21:56 +08:00
|
|
|
|
returnDialogOpen.value = false
|
|
|
|
|
|
selectedClaimId.value = ''
|
|
|
|
|
|
await reload()
|
|
|
|
|
|
} catch (nextError) {
|
|
|
|
|
|
toast(nextError?.message || '退回单据失败,请稍后重试。')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionBusy.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function confirmDeleteSelected() {
|
|
|
|
|
|
const row = selectedRow.value
|
|
|
|
|
|
if (!row?.claimId || actionBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
actionBusy.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = await deleteExpenseClaim(row.claimId)
|
|
|
|
|
|
toast(payload?.message || `${row.id} 报销单已删除。`)
|
|
|
|
|
|
deleteDialogOpen.value = false
|
|
|
|
|
|
selectedClaimId.value = ''
|
|
|
|
|
|
await reload()
|
|
|
|
|
|
} catch (nextError) {
|
|
|
|
|
|
toast(nextError?.message || '删除单据失败,请稍后重试。')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
actionBusy.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
async function reload() {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
error.value = ''
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
try {
|
|
|
|
|
|
const payload = await fetchExpenseClaims()
|
|
|
|
|
|
const mappedRows = Array.isArray(payload)
|
|
|
|
|
|
? payload
|
|
|
|
|
|
.map((item) => mapExpenseClaimToRequest(item))
|
|
|
|
|
|
.filter((item) => item.approvalKey === 'in_progress')
|
|
|
|
|
|
.filter((item) => canCurrentUserProcessRequest(item, currentUser.value))
|
|
|
|
|
|
.map((item) => buildApprovalRow(item))
|
|
|
|
|
|
: []
|
|
|
|
|
|
rows.value = mappedRows
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 06:57:07 +00:00
|
|
|
|
void reload()
|
2026-05-06 11:00:38 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
activeTab,
|
|
|
|
|
|
selectedRow,
|
|
|
|
|
|
expandedExpenseId,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
listKeyword,
|
2026-05-06 11:00:38 +08:00
|
|
|
|
tabs,
|
|
|
|
|
|
filters,
|
|
|
|
|
|
rows,
|
|
|
|
|
|
visibleRows,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
showTable,
|
|
|
|
|
|
showEmpty,
|
2026-05-20 14:21:56 +08:00
|
|
|
|
actionBusy,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
approvalEmptyState,
|
2026-05-06 11:00:38 +08:00
|
|
|
|
approvalSteps,
|
2026-05-20 14:21:56 +08:00
|
|
|
|
canManageClaims,
|
|
|
|
|
|
closeDeleteDialog,
|
|
|
|
|
|
closeReturnDialog,
|
|
|
|
|
|
confirmDeleteSelected,
|
|
|
|
|
|
confirmReturnSelected,
|
|
|
|
|
|
deleteDialogOpen,
|
2026-05-06 11:00:38 +08:00
|
|
|
|
summaryItems,
|
|
|
|
|
|
heroSummaryItems,
|
|
|
|
|
|
currentProgressRingMotion,
|
|
|
|
|
|
expenseItems,
|
|
|
|
|
|
expenseTotal,
|
|
|
|
|
|
uploadedExpenseCount,
|
|
|
|
|
|
showExpenseRisk,
|
|
|
|
|
|
toggleExpenseAttachments,
|
|
|
|
|
|
attachments,
|
|
|
|
|
|
riskItems,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
flowItems,
|
|
|
|
|
|
handleEmptyAction,
|
2026-05-20 14:21:56 +08:00
|
|
|
|
handleDeleteSelected,
|
|
|
|
|
|
handleReturnSelected,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
loading,
|
|
|
|
|
|
error,
|
2026-05-20 14:21:56 +08:00
|
|
|
|
returnDialogOpen,
|
2026-05-15 06:57:07 +00:00
|
|
|
|
reload
|
2026-05-06 11:00:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|