| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import dayjs from "dayjs"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref, watch } from "vue"; |
| | | import { |
| | | EXPENSE_SUBJECT_OPTIONS, |
| | | expenseSubjectLabel, |
| | | statusLabel, |
| | | statusTagType, |
| | | detectTravelTier, |
| | | getTravelStandardByTier, |
| | | computeTravelDays, |
| | | createEmptyExpenseDetail, |
| | | createEmptyForm, |
| | | initApprovalFlowNodes, |
| | | advanceApprovalFlow, |
| | | rejectApprovalFlow, |
| | | mockDeptBudget, |
| | | normalizeImportedRow, |
| | | } from "./travelReimburseUtils.js"; |
| | | |
| | | function unwrapArray(payload) { |
| | | if (Array.isArray(payload)) return payload; |
| | | if (payload?.data && Array.isArray(payload.data)) return payload.data; |
| | | if (payload?.rows && Array.isArray(payload.rows)) return payload.rows; |
| | | return []; |
| | | } |
| | | |
| | | function isActiveUser(u) { |
| | | if (u.delFlag === "2" || u.delFlag === 2) return false; |
| | | if (u.status == null) return true; |
| | | return String(u.status) === "0"; |
| | | } |
| | | |
| | | function demoFlowNodes(names = ["é¨é¨ä¸»ç®¡", "è´¢å¡å®¡æ ¸"]) { |
| | | return names.map((name, i) => ({ |
| | | approverId: `mock_${i + 1}`, |
| | | approverName: name, |
| | | sortOrder: i + 1, |
| | | nodeOrder: i + 1, |
| | | nodeStatus: i === 0 ? "process" : "wait", |
| | | approveOpinion: "", |
| | | approveTime: "", |
| | | })); |
| | | } |
| | | |
| | | export function useTravelReimburse() { |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const allRows = ref([ |
| | | { |
| | | id: "1", |
| | | reimburseNo: "TR202605090001", |
| | | applicantId: "mock_1", |
| | | employeeNo: "zhangsan", |
| | | employeeName: "å¼ ä¸", |
| | | applicantNo: "zhangsan", |
| | | applicantName: "å¼ ä¸", |
| | | reimburseReason: "èµ´ä¸æµ·åå è¡ä¸å±ä¼åå®¢æ·æè®¿ã", |
| | | travelStartTime: "2026-05-10 08:00:00", |
| | | travelEndTime: "2026-05-13 18:00:00", |
| | | travelDays: 4, |
| | | departurePlace: "æå·", |
| | | destination: "䏿µ·", |
| | | hotelStandard: 600, |
| | | hotelDays: 3, |
| | | livingSubsidy: 400, |
| | | applyAmount: 4580, |
| | | payee: "å¼ ä¸", |
| | | expenseDetails: [ |
| | | { id: "d1", invoiceDate: "2026-05-10", expenseSubject: "transport", amount: 553, description: "é«éå¾è¿" }, |
| | | { id: "d2", invoiceDate: "2026-05-11", expenseSubject: "hotel", amount: 1680, description: "é
åºä½å®¿" }, |
| | | ], |
| | | attachmentList: [{ name: "é«é票.pdf", url: "/mock/invoice1.pdf" }], |
| | | invoiceAttachments: [{ name: "é«é票.pdf", url: "/mock/invoice1.pdf" }], |
| | | approvalFlowNodes: demoFlowNodes(), |
| | | currentNodeIndex: 0, |
| | | approvalResult: "pending", |
| | | rejectReason: "", |
| | | approvalRecords: [], |
| | | needSpecialApproval: false, |
| | | deptId: "101", |
| | | deptName: "éå®é¨", |
| | | travelTier: "tier1", |
| | | createTime: "2026-05-09 10:20:00", |
| | | }, |
| | | { |
| | | id: "2", |
| | | reimburseNo: "TR202605080002", |
| | | applicantId: "mock_2", |
| | | employeeNo: "lisi", |
| | | employeeName: "æå", |
| | | applicantNo: "lisi", |
| | | applicantName: "æå", |
| | | reimburseReason: "æé½åå
¬å¸ææ¯æ¯æã", |
| | | travelStartTime: "2026-05-05 09:00:00", |
| | | travelEndTime: "2026-05-07 17:00:00", |
| | | travelDays: 3, |
| | | departurePlace: "æ¦æ±", |
| | | destination: "æé½", |
| | | hotelStandard: 450, |
| | | hotelDays: 2, |
| | | livingSubsidy: 240, |
| | | applyAmount: 2100, |
| | | payee: "æå", |
| | | expenseDetails: [{ id: "d3", invoiceDate: "2026-05-06", expenseSubject: "meal", amount: 180, description: "å·¥ä½é¤" }], |
| | | attachmentList: [], |
| | | invoiceAttachments: [], |
| | | approvalFlowNodes: demoFlowNodes().map((n, i) => ({ ...n, nodeStatus: "finish", approveOpinion: "åæ", approveTime: "2026-05-08 11:00:00" })), |
| | | currentNodeIndex: 1, |
| | | approvalResult: "approved", |
| | | rejectReason: "", |
| | | approvalRecords: [{ operatorName: "é¨é¨ä¸»ç®¡", result: "approved", opinion: "åæ", time: "2026-05-08 10:00:00" }], |
| | | needSpecialApproval: false, |
| | | deptId: "102", |
| | | deptName: "ææ¯é¨", |
| | | travelTier: "tier2", |
| | | createTime: "2026-05-07 16:00:00", |
| | | }, |
| | | ]); |
| | | |
| | | const searchForm = reactive({ applicantKeyword: "", travelStartFrom: "", travelEndTo: "" }); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ current: 1, size: 10, total: 0 }); |
| | | const importInputRef = ref(null); |
| | | const allUsersCache = ref([]); |
| | | const applicantFormSearchLoading = ref(false); |
| | | const applicantFormOptions = ref([]); |
| | | const formRef = ref(); |
| | | const form = reactive(createEmptyForm()); |
| | | const formDialog = reactive({ visible: false, title: "", mode: "add", readonly: false }); |
| | | const detailDialog = reactive({ visible: false }); |
| | | const detailRow = ref({}); |
| | | const approveDialog = reactive({ visible: false, row: null }); |
| | | const approveOpinion = ref(""); |
| | | |
| | | const filteredList = computed(() => { |
| | | let list = [...allRows.value]; |
| | | const kw = (searchForm.applicantKeyword || "").trim().toLowerCase(); |
| | | if (kw) { |
| | | list = list.filter((r) => { |
| | | const name = (r.applicantName || r.employeeName || "").toLowerCase(); |
| | | const no = (r.applicantNo || r.employeeNo || "").toLowerCase(); |
| | | return name.includes(kw) || no.includes(kw); |
| | | }); |
| | | } |
| | | if (searchForm.travelStartFrom) { |
| | | list = list.filter((r) => !r.travelStartTime || r.travelStartTime.slice(0, 10) >= searchForm.travelStartFrom); |
| | | } |
| | | if (searchForm.travelEndTo) { |
| | | list = list.filter((r) => !r.travelEndTime || r.travelEndTime.slice(0, 10) <= searchForm.travelEndTo); |
| | | } |
| | | return list.sort((a, b) => (String(a.createTime) < String(b.createTime) ? 1 : -1)); |
| | | }); |
| | | |
| | | watch( |
| | | filteredList, |
| | | (list) => { |
| | | page.total = list.length; |
| | | const maxPage = Math.max(1, Math.ceil(list.length / page.size) || 1); |
| | | if (page.current > maxPage) page.current = maxPage; |
| | | }, |
| | | { immediate: true } |
| | | ); |
| | | |
| | | const tableData = computed(() => { |
| | | const start = (page.current - 1) * page.size; |
| | | return filteredList.value.slice(start, start + page.size); |
| | | }); |
| | | |
| | | const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser)); |
| | | |
| | | const travelDaysDisplay = computed(() => { |
| | | const d = computeTravelDays(form.travelStartTime, form.travelEndTime); |
| | | return d == null ? "" : String(d); |
| | | }); |
| | | |
| | | const travelTierLabel = computed(() => { |
| | | const std = getTravelStandardByTier(form.travelTier || detectTravelTier(form.destination)); |
| | | return `æ${std.label}æ å`; |
| | | }); |
| | | |
| | | const suggestedLivingSubsidy = computed(() => { |
| | | const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || form.hotelDays || 0; |
| | | const std = getTravelStandardByTier(form.travelTier); |
| | | return Math.round(std.mealPerDay * days * 100) / 100; |
| | | }); |
| | | |
| | | const suggestedTransportSubsidy = computed(() => { |
| | | const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || 0; |
| | | const std = getTravelStandardByTier(form.travelTier); |
| | | return Math.round(std.transportPerDay * days * 100) / 100; |
| | | }); |
| | | |
| | | const suggestedHotelLimit = computed(() => { |
| | | const nights = form.hotelDays || 0; |
| | | const perNight = form.hotelStandard ?? getTravelStandardByTier(form.travelTier).hotelPerNight; |
| | | return Math.round(perNight * nights * 100) / 100; |
| | | }); |
| | | |
| | | const detailTotalAmount = computed(() => { |
| | | const sum = (form.expenseDetails || []).reduce((s, d) => s + (Number(d.amount) || 0), 0); |
| | | return Math.round(sum * 100) / 100; |
| | | }); |
| | | |
| | | const overBudgetWarnings = computed(() => buildOverBudgetWarnings(form, detailTotalAmount.value, suggestedHotelLimit.value, suggestedTransportSubsidy.value, suggestedLivingSubsidy.value)); |
| | | |
| | | const budgetHint = computed(() => { |
| | | if (!form.deptId) return { visible: false }; |
| | | const b = mockDeptBudget(form.deptId); |
| | | const apply = Number(form.applyAmount) || detailTotalAmount.value || 0; |
| | | const after = b.remainingAmount - apply; |
| | | return { |
| | | visible: true, |
| | | type: after < 0 ? "error" : "info", |
| | | title: `é¨é¨é¢ç®èå¨ï¼${form.deptName || b.deptId}ï¼`, |
| | | description: `年度é¢ç® ${b.totalBudget} å
ï¼å·²ç¨ ${b.usedAmount} å
ï¼å©ä½ ${b.remainingAmount} å
ï¼æ¬åç³è¯·åé¢è®¡å©ä½ ${Math.round(after * 100) / 100} å
ã`, |
| | | }; |
| | | }); |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "æ¥éåå·", prop: "reimburseNo", width: 150 }, |
| | | { label: "ç³è¯·äººç¼å·", prop: "applicantNo", width: 110 }, |
| | | { label: "ç³è¯·äºº", prop: "applicantName", minWidth: 90 }, |
| | | { label: "åºå·®å¼å§", prop: "travelStartTime", width: 165 }, |
| | | { label: "åºå·®ç»æ", prop: "travelEndTime", width: 165 }, |
| | | { label: "å建æ¶é´", prop: "createTime", width: 165 }, |
| | | { |
| | | label: "ç¶æ", |
| | | prop: "approvalResult", |
| | | width: 100, |
| | | dataType: "tag", |
| | | formatData: (v) => statusLabel(v), |
| | | formatType: (v) => statusTagType(v), |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 200, |
| | | operation: [ |
| | | { name: "ç¼è¾", type: "text", disabled: (row) => row.approvalResult === "pending" || row.approvalResult === "approved", clickFun: (row) => openFormDialog("edit", row) }, |
| | | { name: "详æ
", type: "text", clickFun: (row) => openDetail(row) }, |
| | | { name: "审æ¹", type: "text", disabled: (row) => row.approvalResult !== "pending", clickFun: (row) => openApprove(row) }, |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | const formRules = { |
| | | applicantId: [{ required: true, message: "è¯·éæ©åå·¥", trigger: "change" }], |
| | | reimburseReason: [{ required: true, message: "è¯·å¡«åæ¥éåå ", trigger: "blur" }], |
| | | travelStartTime: [{ required: true, message: "è¯·éæ©åºå·®å¼å§æ¶é´", trigger: "change" }], |
| | | travelEndTime: [ |
| | | { required: true, message: "è¯·éæ©åºå·®ç»ææ¶é´", trigger: "change" }, |
| | | { |
| | | validator: (_r, val, cb) => { |
| | | if (!form.travelStartTime || !val) { cb(); return; } |
| | | if (computeTravelDays(form.travelStartTime, val) == null) cb(new Error("ç»ææ¶é´é¡»æäºå¼å§æ¶é´")); |
| | | else cb(); |
| | | }, |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | departurePlace: [{ required: true, message: "请填ååºå·®å°", trigger: "blur" }], |
| | | destination: [{ required: true, message: "请填åç®çå°", trigger: "blur" }], |
| | | applyAmount: [{ required: true, message: "请填åç³è¯·éé¢", trigger: "blur" }], |
| | | payee: [{ required: true, message: "è¯·å¡«åæ¶æ¬¾äºº", trigger: "blur" }], |
| | | approvalFlowNodes: [ |
| | | { |
| | | validator: (_r, _v, cb) => { |
| | | const nodes = form.approvalFlowNodes || []; |
| | | if (!nodes.length) { cb(new Error("请è³å°é
ç½®ä¸ä¸ªå®¡æ¹èç¹")); return; } |
| | | if (nodes.some((n) => n.approverId == null || n.approverId === "")) { cb(new Error("æ¯ä¸ªèç¹é¡»éæ©å®¡æ¹äºº")); return; } |
| | | cb(); |
| | | }, |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | function buildOverBudgetWarnings(f, detailTotal, hotelLimit, transportLimit, mealLimit) { |
| | | const warnings = []; |
| | | const bySubject = { transport: 0, hotel: 0, meal: 0, other: 0 }; |
| | | (f.expenseDetails || []).forEach((d) => { |
| | | const key = d.expenseSubject || "other"; |
| | | bySubject[key] = (bySubject[key] || 0) + (Number(d.amount) || 0); |
| | | }); |
| | | if (bySubject.transport > transportLimit && transportLimit > 0) { |
| | | warnings.push(`交éè´¹ ${bySubject.transport} å
è¶
åºæ å ${transportLimit} å
`); |
| | | } |
| | | if (bySubject.hotel > hotelLimit && hotelLimit > 0) { |
| | | warnings.push(`ä½å®¿è´¹ ${bySubject.hotel} å
è¶
åºéé¢ ${hotelLimit} å
`); |
| | | } |
| | | if (bySubject.meal > mealLimit && mealLimit > 0) { |
| | | warnings.push(`é¤é¥®è´¹ ${bySubject.meal} å
è¶
åºç活补贴建议 ${mealLimit} å
`); |
| | | } |
| | | const std = getTravelStandardByTier(f.travelTier); |
| | | if (f.hotelStandard > std.hotelPerNight) { |
| | | warnings.push(`é
åºæ å ${f.hotelStandard} å
/æé«äº${std.label}æ å ${std.hotelPerNight} å
/æ`); |
| | | } |
| | | const apply = Number(f.applyAmount) || detailTotal; |
| | | const standardTotal = transportLimit + hotelLimit + mealLimit; |
| | | if (apply > standardTotal && standardTotal > 0) { |
| | | warnings.push(`ç³è¯·æ»é¢ ${apply} å
é«äºå·®æ
æ åå计约 ${standardTotal} å
`); |
| | | } |
| | | return warnings; |
| | | } |
| | | |
| | | async function loadUserPool() { |
| | | try { |
| | | allUsersCache.value = unwrapArray(await userListNoPageByTenantId()); |
| | | } catch { |
| | | allUsersCache.value = []; |
| | | } |
| | | } |
| | | |
| | | function userSelectLabel(u) { |
| | | const nick = u.nickName || ""; |
| | | const name = u.userName || ""; |
| | | if (nick && name && nick !== name) return `${nick}ï¼${name}ï¼`; |
| | | return nick || name || `ç¨æ·${u.userId ?? u.id ?? ""}`; |
| | | } |
| | | |
| | | function userById(id) { |
| | | return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id)); |
| | | } |
| | | |
| | | function employeeNoFromUser(u) { |
| | | if (!u) return ""; |
| | | return u.userName ?? u.userCode ?? u.jobNumber ?? u.workNo ?? (u.userId != null ? String(u.userId) : ""); |
| | | } |
| | | |
| | | function filterUsersByQuery(query) { |
| | | const list = allUsersCache.value.filter(isActiveUser); |
| | | const q = (query || "").trim().toLowerCase(); |
| | | if (!q) return [...list]; |
| | | return list.filter((u) => { |
| | | const nick = (u.nickName || "").toLowerCase(); |
| | | const uname = (u.userName || "").toLowerCase(); |
| | | return nick.includes(q) || uname.includes(q); |
| | | }); |
| | | } |
| | | |
| | | async function remoteSearchApplicantForm(query) { |
| | | applicantFormSearchLoading.value = true; |
| | | try { |
| | | if (!allUsersCache.value.length) await loadUserPool(); |
| | | applicantFormOptions.value = filterUsersByQuery(query); |
| | | } finally { |
| | | applicantFormSearchLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | function onApplicantChange(uid) { |
| | | const u = userById(uid); |
| | | if (u) { |
| | | form.employeeName = u.nickName || u.userName || ""; |
| | | form.employeeNo = employeeNoFromUser(u); |
| | | form.payee = form.payee || form.employeeName; |
| | | form.deptId = String(u.deptId ?? u.sysDeptId ?? ""); |
| | | form.deptName = u.dept?.deptName ?? u.deptName ?? ""; |
| | | } else { |
| | | form.employeeName = ""; |
| | | form.employeeNo = ""; |
| | | } |
| | | } |
| | | |
| | | function recalcTravelStandards() { |
| | | form.travelTier = detectTravelTier(form.destination); |
| | | const std = getTravelStandardByTier(form.travelTier); |
| | | if (form.hotelStandard == null || form.hotelStandard === 0) form.hotelStandard = std.hotelPerNight; |
| | | const days = computeTravelDays(form.travelStartTime, form.travelEndTime); |
| | | if (days != null) { |
| | | form.travelDays = days; |
| | | if (form.hotelDays == null) form.hotelDays = Math.max(0, days - 1); |
| | | if (form.livingSubsidy == null || form.livingSubsidy === 0) form.livingSubsidy = suggestedLivingSubsidy.value; |
| | | } |
| | | form.needSpecialApproval = buildOverBudgetWarnings(form, detailTotalAmount.value, suggestedHotelLimit.value, suggestedTransportSubsidy.value, suggestedLivingSubsidy.value).length > 0; |
| | | } |
| | | |
| | | function onTravelRangeChange() { |
| | | recalcTravelStandards(); |
| | | nextTick(() => formRef.value?.validateField?.("travelEndTime")); |
| | | } |
| | | |
| | | function onDetailAmountChange() { |
| | | recalcTravelStandards(); |
| | | } |
| | | |
| | | function onApprovalFlowChange() { |
| | | nextTick(() => formRef.value?.validateField?.("approvalFlowNodes")); |
| | | } |
| | | |
| | | function addExpenseDetail() { |
| | | form.expenseDetails.push(createEmptyExpenseDetail()); |
| | | } |
| | | |
| | | function removeExpenseDetail(index) { |
| | | form.expenseDetails.splice(index, 1); |
| | | recalcTravelStandards(); |
| | | } |
| | | |
| | | function mapAttachmentList(list) { |
| | | return (list || []).map((f, i) => ({ |
| | | id: f.id ?? f.uid ?? `inv_${Date.now()}_${i}`, |
| | | name: f.name || f.fileName || f.originalFilename || "æªå½å", |
| | | url: f.url || f.downloadURL || f.previewURL || f.previewUrl || "", |
| | | })); |
| | | } |
| | | |
| | | function syncApplyAmountFromDetails() { |
| | | form.applyAmount = detailTotalAmount.value; |
| | | recalcTravelStandards(); |
| | | } |
| | | |
| | | function handleQuery() { |
| | | page.current = 1; |
| | | tableLoading.value = true; |
| | | setTimeout(() => { tableLoading.value = false; }, 150); |
| | | } |
| | | |
| | | function resetSearch() { |
| | | searchForm.applicantKeyword = ""; |
| | | searchForm.travelStartFrom = ""; |
| | | searchForm.travelEndTo = ""; |
| | | handleQuery(); |
| | | } |
| | | |
| | | function pagination(obj) { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | } |
| | | |
| | | function openDetail(row) { |
| | | detailRow.value = { ...row }; |
| | | detailDialog.visible = true; |
| | | } |
| | | |
| | | function openApprove(row) { |
| | | approveDialog.row = { ...row }; |
| | | approveDialog.visible = true; |
| | | } |
| | | |
| | | function approvalActionLabel(v) { |
| | | if (v === "approved") return "éè¿"; |
| | | if (v === "rejected") return "驳å"; |
| | | return "æäº¤"; |
| | | } |
| | | |
| | | async function openFormDialog(mode, row) { |
| | | formDialog.mode = mode; |
| | | formDialog.readonly = false; |
| | | formDialog.title = mode === "add" ? "æ°å¢å·®æ
æ¥é" : "ç¼è¾å·®æ
æ¥é"; |
| | | if (!allUsersCache.value.length) await loadUserPool(); |
| | | Object.assign(form, createEmptyForm()); |
| | | if (mode === "edit" && row) { |
| | | Object.assign(form, { |
| | | ...JSON.parse(JSON.stringify(row)), |
| | | attachmentList: JSON.parse(JSON.stringify(row.attachmentList || row.invoiceAttachments || [])), |
| | | approvalFlowNodes: JSON.parse(JSON.stringify(row.approvalFlowNodes || [])), |
| | | expenseDetails: JSON.parse(JSON.stringify(row.expenseDetails || [])), |
| | | }); |
| | | const u = userById(row.applicantId); |
| | | applicantFormOptions.value = u ? [u] : [{ userId: row.applicantId, nickName: row.employeeName, userName: row.employeeNo }]; |
| | | } else { |
| | | form.approvalFlowNodes = [{ approverId: null, approverName: "", sortOrder: 1, nodeOrder: 1 }]; |
| | | remoteSearchApplicantForm(""); |
| | | } |
| | | formDialog.visible = true; |
| | | nextTick(() => { |
| | | formRef.value?.clearValidate?.(); |
| | | recalcTravelStandards(); |
| | | }); |
| | | } |
| | | |
| | | function onFormClosed() { |
| | | formRef.value?.resetFields?.(); |
| | | } |
| | | |
| | | async function submitForm() { |
| | | try { |
| | | await formRef.value?.validate?.(); |
| | | } catch { |
| | | return; |
| | | } |
| | | if (!(form.expenseDetails || []).length) { |
| | | proxy?.$modal?.msgWarning?.("请è³å°æ·»å 䏿¡æ¥éæç»"); |
| | | return; |
| | | } |
| | | recalcTravelStandards(); |
| | | if (form.needSpecialApproval) { |
| | | try { |
| | | await proxy.$modal.confirm("åå¨è¶
æ¯é¡¹ï¼æäº¤åå°æ 记为éç¹æ¹ï¼æ¯å¦ç»§ç»ï¼"); |
| | | } catch { |
| | | return; |
| | | } |
| | | } |
| | | const days = computeTravelDays(form.travelStartTime, form.travelEndTime); |
| | | const payload = { |
| | | reimburseNo: form.reimburseNo || `TR${dayjs().format("YYYYMMDDHHmmss")}`, |
| | | applicantId: form.applicantId, |
| | | employeeNo: form.employeeNo, |
| | | employeeName: form.employeeName, |
| | | applicantNo: form.employeeNo, |
| | | applicantName: form.employeeName, |
| | | reimburseReason: form.reimburseReason, |
| | | travelStartTime: form.travelStartTime, |
| | | travelEndTime: form.travelEndTime, |
| | | travelDays: days, |
| | | departurePlace: form.departurePlace, |
| | | destination: form.destination, |
| | | hotelStandard: form.hotelStandard, |
| | | hotelDays: form.hotelDays, |
| | | livingSubsidy: form.livingSubsidy, |
| | | applyAmount: form.applyAmount, |
| | | payee: form.payee, |
| | | expenseDetails: JSON.parse(JSON.stringify(form.expenseDetails)), |
| | | attachmentList: JSON.parse(JSON.stringify(form.attachmentList || [])), |
| | | invoiceAttachments: mapAttachmentList(form.attachmentList), |
| | | approvalFlowNodes: initApprovalFlowNodes(form.approvalFlowNodes), |
| | | currentNodeIndex: 0, |
| | | needSpecialApproval: form.needSpecialApproval, |
| | | deptId: form.deptId, |
| | | deptName: form.deptName, |
| | | travelTier: form.travelTier, |
| | | }; |
| | | if (formDialog.mode === "add") { |
| | | allRows.value.unshift({ |
| | | id: `local_${Date.now()}`, |
| | | ...payload, |
| | | approvalResult: "pending", |
| | | rejectReason: "", |
| | | approvalRecords: [], |
| | | createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }); |
| | | proxy?.$modal?.msgSuccess?.("æäº¤æåï¼å·²è¿å
¥å®¡æ¹ï¼æ¬å°æ¨¡æï¼"); |
| | | } else { |
| | | const idx = allRows.value.findIndex((r) => r.id === form.id); |
| | | if (idx !== -1) { |
| | | const prev = allRows.value[idx]; |
| | | allRows.value[idx] = { |
| | | ...prev, |
| | | ...payload, |
| | | id: form.id, |
| | | approvalResult: prev.approvalResult === "rejected" ? "pending" : prev.approvalResult, |
| | | approvalFlowNodes: initApprovalFlowNodes(form.approvalFlowNodes), |
| | | currentNodeIndex: 0, |
| | | createTime: prev.createTime, |
| | | }; |
| | | } |
| | | proxy?.$modal?.msgSuccess?.("ä¿åæåï¼æ¬å°æ¨¡æï¼"); |
| | | } |
| | | formDialog.visible = false; |
| | | handleQuery(); |
| | | } |
| | | |
| | | async function submitApprove(result) { |
| | | const row = approveDialog.row; |
| | | if (!row) return; |
| | | if (result === "rejected" && !(approveOpinion.value || "").trim()) { |
| | | proxy?.$modal?.msgWarning?.("驳å须填åå®¡æ¹æè§"); |
| | | return; |
| | | } |
| | | const idx = allRows.value.findIndex((r) => r.id === row.id); |
| | | if (idx === -1) return; |
| | | const cur = allRows.value[idx]; |
| | | const operatorName = "å½å审æ¹äºº"; |
| | | const record = { |
| | | operatorName, |
| | | result, |
| | | opinion: approveOpinion.value || (result === "approved" ? "åæ" : "驳å"), |
| | | time: dayjs().format("YYYY-MM-DD HH:mm:ss"), |
| | | }; |
| | | const records = [...(cur.approvalRecords || []), record]; |
| | | let flowUpdate; |
| | | if (result === "approved") { |
| | | flowUpdate = advanceApprovalFlow(cur, approveOpinion.value); |
| | | } else { |
| | | flowUpdate = rejectApprovalFlow(cur, approveOpinion.value); |
| | | } |
| | | allRows.value[idx] = { |
| | | ...cur, |
| | | approvalFlowNodes: flowUpdate.nodes, |
| | | currentNodeIndex: flowUpdate.currentNodeIndex, |
| | | approvalResult: flowUpdate.approvalResult, |
| | | rejectReason: flowUpdate.rejectReason ?? cur.rejectReason, |
| | | approvalRecords: records, |
| | | }; |
| | | proxy?.$modal?.msgSuccess?.(result === "approved" ? "å·²éè¿" : "已驳å"); |
| | | approveDialog.visible = false; |
| | | handleQuery(); |
| | | } |
| | | |
| | | function handleExport() { |
| | | const data = filteredList.value; |
| | | const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" }); |
| | | const url = URL.createObjectURL(blob); |
| | | const a = document.createElement("a"); |
| | | a.href = url; |
| | | a.download = `å·®æ
æ¥é导åº_${dayjs().format("YYYYMMDDHHmmss")}.json`; |
| | | a.click(); |
| | | URL.revokeObjectURL(url); |
| | | proxy?.$modal?.msgSuccess?.(`å·²å¯¼åº ${data.length} æ¡`); |
| | | } |
| | | |
| | | function handleImportClick() { |
| | | importInputRef.value?.click?.(); |
| | | } |
| | | |
| | | function onImportFile(e) { |
| | | const file = e.target.files?.[0]; |
| | | e.target.value = ""; |
| | | if (!file) return; |
| | | const reader = new FileReader(); |
| | | reader.onload = () => { |
| | | try { |
| | | const parsed = JSON.parse(String(reader.result || "")); |
| | | const arr = Array.isArray(parsed) ? parsed : parsed?.rows || parsed?.data; |
| | | if (!Array.isArray(arr) || !arr.length) { |
| | | proxy?.$modal?.msgWarning?.("导å
¥æ ¼å¼é¡»ä¸ºå·®æ
æ¥é JSON æ°ç»"); |
| | | return; |
| | | } |
| | | arr.forEach((raw, i) => allRows.value.unshift(normalizeImportedRow(raw, i))); |
| | | proxy?.$modal?.msgSuccess?.(`æå导å
¥ ${arr.length} æ¡`); |
| | | handleQuery(); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.("è§£æå¤±è´¥"); |
| | | } |
| | | }; |
| | | reader.readAsText(file, "utf-8"); |
| | | } |
| | | |
| | | onMounted(() => loadUserPool()); |
| | | |
| | | return { |
| | | Search, |
| | | EXPENSE_SUBJECT_OPTIONS, |
| | | expenseSubjectLabel, |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | tableColumn, |
| | | importInputRef, |
| | | formRef, |
| | | form, |
| | | formDialog, |
| | | formRules, |
| | | detailDialog, |
| | | detailRow, |
| | | approveDialog, |
| | | approveOpinion, |
| | | applicantFormSearchLoading, |
| | | applicantFormOptions, |
| | | flowUserOptions, |
| | | travelDaysDisplay, |
| | | travelTierLabel, |
| | | suggestedLivingSubsidy, |
| | | suggestedTransportSubsidy, |
| | | suggestedHotelLimit, |
| | | detailTotalAmount, |
| | | overBudgetWarnings, |
| | | budgetHint, |
| | | handleQuery, |
| | | resetSearch, |
| | | pagination, |
| | | remoteSearchApplicantForm, |
| | | userSelectLabel, |
| | | onApplicantChange, |
| | | recalcTravelStandards, |
| | | onTravelRangeChange, |
| | | onDetailAmountChange, |
| | | onApprovalFlowChange, |
| | | addExpenseDetail, |
| | | removeExpenseDetail, |
| | | syncApplyAmountFromDetails, |
| | | openFormDialog, |
| | | onFormClosed, |
| | | submitForm, |
| | | openDetail, |
| | | openApprove, |
| | | approvalActionLabel, |
| | | submitApprove, |
| | | handleExport, |
| | | handleImportClick, |
| | | onImportFile, |
| | | }; |
| | | } |