| | |
| | | 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 { |
| | | deleteFinReimbursement, |
| | | getFinReimbursementDetail, |
| | | listFinReimbursementPage, |
| | | persistFinReimbursement, |
| | | } from "@/api/officeProcessAutomation/finReimbursement.js"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { userListNoPageByTenantId, deptTreeSelect } from "@/api/system/user.js"; |
| | | import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue"; |
| | | import { |
| | | buildCostReimbursementSaveDto, |
| | | buildFinReimbursementListParams, |
| | | filterReimbursementRowsBySearch, |
| | | hasActiveReimbursementSearch, |
| | | canDeleteReimbursementRow, |
| | | canEditReimbursementRow, |
| | | enrichReimbursementListRowsWithApprovalFlow, |
| | | filterRowsByReimbursementType, |
| | | FIN_REIMBURSEMENT_TYPE, |
| | | mapCostReimbursementRow, |
| | | mapFinReimbursementDetailRow, |
| | | resolveReimbursementDeleteId, |
| | | unwrapFinReimbursementDetail, |
| | | unwrapFinReimbursementPage, |
| | | validateReimbursementApprovalNodes, |
| | | validateReimbursementPersistDto, |
| | | } from "../shared/finReimbursementMappers.js"; |
| | | import { consumeReimburseEditFromApprove } from "../shared/reimburseApproveBridge.js"; |
| | | import { |
| | | EXPENSE_CATEGORY_OPTIONS, |
| | | CATEGORY_TEMPLATES, |
| | |
| | | export function useCostReimburse() { |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const allRows = ref([ |
| | | { |
| | | id: "1", |
| | | reimburseNo: "CR202605100001", |
| | | applicantId: "mock_1", |
| | | employeeNo: "zhangsan", |
| | | employeeName: "张三", |
| | | applicantNo: "zhangsan", |
| | | applicantName: "张三", |
| | | expenseCategory: "office_procurement", |
| | | reimburseReason: "采购打印机硒鼓、A4纸等办公耗材。", |
| | | applyAmount: 680, |
| | | payee: "张三", |
| | | payeeAccount: "6222 **** **** 1234", |
| | | bankBranch: "中国工商银行杭州西湖支行", |
| | | expenseDetails: [ |
| | | { id: "d1", invoiceDate: "2026-05-08", expenseSubject: "office_supply", amount: 380, description: "A4复印纸" }, |
| | | { id: "d2", invoiceDate: "2026-05-08", expenseSubject: "office_supply", amount: 300, description: "硒鼓" }, |
| | | ], |
| | | attachmentList: [{ name: "采购发票.pdf", url: "/mock/invoice1.pdf" }], |
| | | approvalFlowNodes: demoFlowNodes(680, "office_procurement"), |
| | | currentNodeIndex: 0, |
| | | approvalResult: "pending", |
| | | rejectReason: "", |
| | | approvalRecords: [], |
| | | applyTime: "2026-05-10 09:15:00", |
| | | createTime: "2026-05-10 09:15:00", |
| | | deptId: "101", |
| | | deptName: "行政部", |
| | | }, |
| | | { |
| | | id: "2", |
| | | reimburseNo: "CR202605080002", |
| | | applicantId: "mock_2", |
| | | employeeNo: "lisi", |
| | | employeeName: "李四", |
| | | applicantNo: "lisi", |
| | | applicantName: "李四", |
| | | expenseCategory: "business_entertainment", |
| | | reimburseReason: "接待重点客户商务宴请。", |
| | | applyAmount: 3200, |
| | | payee: "李四", |
| | | payeeAccount: "6217 **** **** 5678", |
| | | bankBranch: "招商银行武汉光谷支行", |
| | | expenseDetails: [ |
| | | { id: "d3", invoiceDate: "2026-05-06", expenseSubject: "entertainment", amount: 3200, description: "客户宴请" }, |
| | | ], |
| | | attachmentList: [], |
| | | approvalFlowNodes: demoFlowNodes(3200, "business_entertainment").map((n, i) => ({ |
| | | ...n, |
| | | nodeStatus: i === 0 ? "error" : "wait", |
| | | approveOpinion: i === 0 ? "发票模糊需重传" : "", |
| | | approveTime: i === 0 ? "2026-05-09 14:20:00" : "", |
| | | })), |
| | | currentNodeIndex: 0, |
| | | approvalResult: "rejected", |
| | | rejectReason: "发票模糊需重传", |
| | | approvalRecords: [ |
| | | { operatorName: "直属上级", result: "rejected", opinion: "发票模糊需重传", time: "2026-05-09 14:20:00" }, |
| | | ], |
| | | applyTime: "2026-05-07 16:30:00", |
| | | createTime: "2026-05-07 16:30:00", |
| | | deptId: "102", |
| | | deptName: "销售部", |
| | | }, |
| | | { |
| | | id: "3", |
| | | reimburseNo: "CR202605050003", |
| | | applicantId: "mock_3", |
| | | employeeNo: "wangwu", |
| | | employeeName: "王五", |
| | | applicantNo: "wangwu", |
| | | applicantName: "王五", |
| | | expenseCategory: "communication", |
| | | reimburseReason: "5月因公话费报销。", |
| | | applyAmount: 198, |
| | | payee: "王五", |
| | | payeeAccount: "6228 **** **** 9012", |
| | | bankBranch: "中国建设银行成都高新支行", |
| | | expenseDetails: [ |
| | | { id: "d4", invoiceDate: "2026-05-05", expenseSubject: "phone", amount: 198, description: "话费账单" }, |
| | | ], |
| | | attachmentList: [{ name: "话费账单.jpg", url: "/mock/phone.jpg" }], |
| | | approvalFlowNodes: demoFlowNodes(198, "communication").map((n) => ({ |
| | | ...n, |
| | | nodeStatus: "finish", |
| | | approveOpinion: "同意", |
| | | approveTime: "2026-05-06 10:00:00", |
| | | })), |
| | | currentNodeIndex: 0, |
| | | approvalResult: "approved", |
| | | rejectReason: "", |
| | | approvalRecords: [{ operatorName: "直属上级", result: "approved", opinion: "同意", time: "2026-05-06 10:00:00" }], |
| | | applyTime: "2026-05-05 11:00:00", |
| | | createTime: "2026-05-05 11:00:00", |
| | | deptId: "103", |
| | | deptName: "技术部", |
| | | }, |
| | | ]); |
| | | const allRows = ref([]); |
| | | |
| | | const searchForm = reactive({ |
| | | applicantKeyword: "", |
| | | applyTimeFrom: "", |
| | | applyTimeTo: "", |
| | | deptId: undefined, |
| | | expenseCategory: "", |
| | | applyTimeRange: [], |
| | | }); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ current: 1, size: 10, total: 0 }); |
| | |
| | | const allUsersCache = ref([]); |
| | | const applicantFormSearchLoading = ref(false); |
| | | const applicantFormOptions = ref([]); |
| | | const deptOptions = ref([]); |
| | | const formRef = ref(); |
| | | const form = reactive(createEmptyForm()); |
| | | const formDialog = reactive({ visible: false, title: "", mode: "add", readonly: false }); |
| | | const detailDialog = reactive({ visible: false }); |
| | | const detailLoading = ref(false); |
| | | const detailRow = ref({}); |
| | | const approveDialog = reactive({ visible: false, row: null }); |
| | | const approveOpinion = ref(""); |
| | | const submitSaving = ref(false); |
| | | |
| | | 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.applyTimeFrom) { |
| | | list = list.filter((r) => { |
| | | const t = (r.applyTime || r.createTime || "").slice(0, 10); |
| | | return !t || t >= searchForm.applyTimeFrom; |
| | | }); |
| | | } |
| | | if (searchForm.applyTimeTo) { |
| | | list = list.filter((r) => { |
| | | const t = (r.applyTime || r.createTime || "").slice(0, 10); |
| | | return !t || t <= searchForm.applyTimeTo; |
| | | }); |
| | | } |
| | | 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).map((r) => ({ |
| | | const tableData = computed(() => |
| | | allRows.value.map((r) => ({ |
| | | ...r, |
| | | approvalFlowSummary: formatApprovalFlowSummary(r), |
| | | })); |
| | | }); |
| | | })) |
| | | ); |
| | | |
| | | async function fetchList() { |
| | | tableLoading.value = true; |
| | | try { |
| | | const res = await listFinReimbursementPage( |
| | | buildFinReimbursementListParams({ |
| | | page, |
| | | searchForm, |
| | | reimbursementType: FIN_REIMBURSEMENT_TYPE.COST, |
| | | }) |
| | | ); |
| | | const { records, total } = unwrapFinReimbursementPage(res); |
| | | const filtered = filterRowsByReimbursementType( |
| | | records, |
| | | FIN_REIMBURSEMENT_TYPE.COST |
| | | ); |
| | | let mapped = filtered.map(mapCostReimbursementRow); |
| | | mapped = await enrichReimbursementListRowsWithApprovalFlow( |
| | | mapped, |
| | | FIN_REIMBURSEMENT_TYPE.COST |
| | | ); |
| | | if (hasActiveReimbursementSearch(searchForm)) { |
| | | mapped = filterReimbursementRowsBySearch(mapped, searchForm); |
| | | } |
| | | allRows.value = mapped; |
| | | const dropped = records.length - filtered.length; |
| | | let nextTotal = |
| | | dropped > 0 ? Math.max(0, Number(total) - dropped) : Number(total); |
| | | if (hasActiveReimbursementSearch(searchForm)) { |
| | | nextTotal = mapped.length; |
| | | } |
| | | page.total = nextTotal; |
| | | } catch { |
| | | allRows.value = []; |
| | | page.total = 0; |
| | | proxy?.$modal?.msgError?.("费用报销列表加载失败"); |
| | | } finally { |
| | | tableLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser)); |
| | | |
| | |
| | | { label: "报销单号", prop: "reimburseNo", width: 150 }, |
| | | { label: "申请人编号", prop: "applicantNo", width: 110 }, |
| | | { label: "申请人", prop: "applicantName", minWidth: 90 }, |
| | | { label: "部门", prop: "applicantDeptName", minWidth: 100 }, |
| | | { label: "费用类型", prop: "expenseCategory", minWidth: 90, formatData: (v) => expenseCategoryLabel(v) }, |
| | | { label: "付款方", prop: "payer", minWidth: 100 }, |
| | | { label: "报销金额(元)", prop: "applyAmount", width: 110 }, |
| | | { label: "报销原因", prop: "reimburseReason", minWidth: 160, showOverflowTooltip: true }, |
| | | { label: "申请时间", prop: "applyTime", width: 165 }, |
| | |
| | | { |
| | | name: "编辑", |
| | | type: "text", |
| | | disabled: (row) => row.approvalResult === "pending" || row.approvalResult === "approved", |
| | | disabled: (row) => !canEditReimbursementRow(row), |
| | | clickFun: (row) => openFormDialog("edit", row), |
| | | }, |
| | | { name: "详情", type: "text", clickFun: (row) => openDetail(row) }, |
| | | { |
| | | name: "审批", |
| | | type: "text", |
| | | disabled: (row) => row.approvalResult !== "pending", |
| | | clickFun: (row) => openApprove(row), |
| | | name: "删除", |
| | | type: "danger", |
| | | disabled: (row) => !canDeleteReimbursementRow(row), |
| | | clickFun: (row) => confirmRemoveRow(row), |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | const formRules = { |
| | | deptId: [{ required: true, message: "请选择归属部门", trigger: "change" }], |
| | | applicantId: [{ required: true, message: "请选择员工", trigger: "change" }], |
| | | expenseCategory: [{ required: true, message: "请选择费用类型", trigger: "change" }], |
| | | reimburseReason: [{ required: true, message: "请填写报销原因", trigger: "blur" }], |
| | |
| | | |
| | | async function loadUserPool() { |
| | | try { |
| | | allUsersCache.value = unwrapArray(await userListNoPageByTenantId()); |
| | | allUsersCache.value = unwrapArray(await userListNoPageByTenantId({})); |
| | | } catch { |
| | | allUsersCache.value = []; |
| | | } |
| | | } |
| | | |
| | | function formatDeptOptions(list) { |
| | | return (list || []).map((d) => ({ |
| | | ...d, |
| | | rawLabel: d.label, |
| | | label: d.label, // 部门下拉不要显示id |
| | | children: d.children && d.children.length ? formatDeptOptions(d.children) : undefined, |
| | | })); |
| | | } |
| | | |
| | | async function loadDeptOptions() { |
| | | try { |
| | | const res = await deptTreeSelect(); |
| | | deptOptions.value = formatDeptOptions(res.data || []); |
| | | } catch { |
| | | deptOptions.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 ?? ""}`; |
| | | const empNo = employeeNoFromUser(u); |
| | | return `${nick || name || "用户"} - ${empNo}`; |
| | | } |
| | | |
| | | function userById(id) { |
| | | return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id)); |
| | | return applicantFormOptions.value.find((u) => String(u.userId ?? u.id) === String(id)); |
| | | } |
| | | |
| | | function employeeNoFromUser(u) { |
| | |
| | | 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); |
| | | const q = (query || "").trim(); |
| | | const params = {}; |
| | | if (form.deptId) { |
| | | params.deptId = form.deptId; |
| | | } |
| | | if (q) { |
| | | params.nickName = q; // You can pass nickName to backend |
| | | } |
| | | const res = await userListNoPageByTenantId(params); |
| | | applicantFormOptions.value = unwrapArray(res).filter(isActiveUser); |
| | | } catch { |
| | | applicantFormOptions.value = []; |
| | | } finally { |
| | | applicantFormSearchLoading.value = false; |
| | | } |
| | |
| | | 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 ?? ""; |
| | | const userDeptId = String(u.deptId ?? u.sysDeptId ?? ""); |
| | | if (userDeptId && form.deptId !== userDeptId) { |
| | | form.deptId = userDeptId; |
| | | const name = findDeptLabel(deptOptions.value, userDeptId); |
| | | form.deptName = name || (u.dept?.deptName ?? u.deptName ?? ""); |
| | | } |
| | | } else { |
| | | form.employeeName = ""; |
| | | form.employeeNo = ""; |
| | | } |
| | | } |
| | | |
| | | function findDeptLabel(nodes, id) { |
| | | if (!nodes) return null; |
| | | for (const node of nodes) { |
| | | if (String(node.id) === String(id)) return node.rawLabel || node.label; |
| | | if (node.children) { |
| | | const hit = findDeptLabel(node.children, id); |
| | | if (hit) return hit; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | function onDeptChange(deptId) { |
| | | if (deptId) { |
| | | const name = findDeptLabel(deptOptions.value, deptId); |
| | | if (name) form.deptName = name; |
| | | |
| | | remoteSearchApplicantForm(""); |
| | | if (form.applicantId) { |
| | | const u = userById(form.applicantId); |
| | | if (u && String(u.deptId ?? u.sysDeptId) !== String(deptId)) { |
| | | form.applicantId = ""; |
| | | onApplicantChange(""); |
| | | } |
| | | } |
| | | } else { |
| | | form.deptName = ""; |
| | | remoteSearchApplicantForm(""); |
| | | } |
| | | } |
| | | |
| | | function autoAssignApprovalFlow() { |
| | | const amount = Number(form.applyAmount) || detailTotalAmount.value || 0; |
| | | form.approvalFlowNodes = buildAutoApprovalFlow(amount, form.expenseCategory || "other"); |
| | | form.approvalFlowNodes = buildAutoApprovalFlow( |
| | | amount, |
| | | form.expenseCategory || "other", |
| | | form.approvalFlowNodes |
| | | ); |
| | | nextTick(() => formRef.value?.validateField?.("approvalFlowNodes")); |
| | | } |
| | | |
| | |
| | | |
| | | function handleQuery() { |
| | | page.current = 1; |
| | | tableLoading.value = true; |
| | | setTimeout(() => { |
| | | tableLoading.value = false; |
| | | }, 150); |
| | | return fetchList(); |
| | | } |
| | | |
| | | function resetSearch() { |
| | | searchForm.applicantKeyword = ""; |
| | | searchForm.applyTimeFrom = ""; |
| | | searchForm.applyTimeTo = ""; |
| | | handleQuery(); |
| | | } |
| | | |
| | | function pagination(obj) { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | return fetchList(); |
| | | } |
| | | |
| | | function openDetail(row) { |
| | | detailRow.value = { ...row }; |
| | | async function loadCostDetailRow(row) { |
| | | const id = resolveReimbursementDeleteId(row); |
| | | if (id == null) { |
| | | throw new Error("missing id"); |
| | | } |
| | | const res = await getFinReimbursementDetail(id); |
| | | const raw = unwrapFinReimbursementDetail(res); |
| | | return mapFinReimbursementDetailRow(raw, FIN_REIMBURSEMENT_TYPE.COST); |
| | | } |
| | | |
| | | async function openDetail(row) { |
| | | const id = resolveReimbursementDeleteId(row); |
| | | if (id == null) { |
| | | proxy?.$modal?.msgWarning?.("无法查看详情:缺少报销单 ID"); |
| | | return; |
| | | } |
| | | detailDialog.visible = true; |
| | | detailLoading.value = true; |
| | | detailRow.value = { ...row }; |
| | | try { |
| | | detailRow.value = await loadCostDetailRow(row); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.("加载详情失败"); |
| | | detailDialog.visible = false; |
| | | } finally { |
| | | detailLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function confirmRemoveRow(row) { |
| | | const id = resolveReimbursementDeleteId(row); |
| | | if (id == null) { |
| | | proxy?.$modal?.msgWarning?.("无法删除:缺少报销单 ID"); |
| | | return; |
| | | } |
| | | const title = row.reimburseNo || row.billNo || row.reimburseReason || "该报销单"; |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | `确定要删除「${title}」吗?删除后不可恢复。`, |
| | | "删除确认", |
| | | { |
| | | type: "warning", |
| | | confirmButtonText: "确定删除", |
| | | cancelButtonText: "取消", |
| | | distinguishCancelAndClose: true, |
| | | autofocus: false, |
| | | } |
| | | ); |
| | | } catch { |
| | | return; |
| | | } |
| | | try { |
| | | await deleteFinReimbursement([id]); |
| | | proxy?.$modal?.msgSuccess?.("删除成功"); |
| | | if (detailDialog.visible && resolveReimbursementDeleteId(detailRow.value) === id) { |
| | | detailDialog.visible = false; |
| | | } |
| | | await handleQuery(); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.("删除失败"); |
| | | } |
| | | } |
| | | |
| | | function openApprove(row) { |
| | |
| | | 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 }]; |
| | | let editRow = row; |
| | | try { |
| | | editRow = await loadCostDetailRow(row); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.("加载报销详情失败"); |
| | | return; |
| | | } |
| | | Object.assign(form, buildCostReimbursementSaveDto(editRow)); |
| | | formDialog.title = "编辑费用报销"; |
| | | await remoteSearchApplicantForm(""); |
| | | const u = userById(editRow.applicantId); |
| | | if (!u) { |
| | | applicantFormOptions.value.unshift({ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.applicantCode }); |
| | | } |
| | | } else { |
| | | form.approvalFlowNodes = buildAutoApprovalFlow(0, "other"); |
| | | remoteSearchApplicantForm(""); |
| | |
| | | return; |
| | | } |
| | | syncApplyAmountFromDetails(); |
| | | autoAssignApprovalFlow(); |
| | | |
| | | const payload = { |
| | | reimburseNo: form.reimburseNo || `CR${dayjs().format("YYYYMMDDHHmmss")}`, |
| | | applicantId: form.applicantId, |
| | | employeeNo: form.employeeNo, |
| | | employeeName: form.employeeName, |
| | | applicantNo: form.employeeNo, |
| | | applicantName: form.employeeName, |
| | | expenseCategory: form.expenseCategory, |
| | | reimburseReason: form.reimburseReason, |
| | | applyAmount: form.applyAmount, |
| | | payee: form.payee, |
| | | payeeAccount: form.payeeAccount, |
| | | bankBranch: form.bankBranch, |
| | | expenseDetails: JSON.parse(JSON.stringify(form.expenseDetails)), |
| | | attachmentList: JSON.parse(JSON.stringify(form.attachmentList || [])), |
| | | invoiceAttachments: (form.attachmentList || []).map((f, i) => ({ |
| | | id: f.id ?? f.uid ?? `inv_${Date.now()}_${i}`, |
| | | name: f.name || f.fileName || "未命名", |
| | | url: f.url || f.downloadURL || "", |
| | | })), |
| | | approvalFlowNodes: initApprovalFlowNodes(form.approvalFlowNodes), |
| | | currentNodeIndex: 0, |
| | | deptId: form.deptId, |
| | | deptName: form.deptName, |
| | | }; |
| | | |
| | | if (formDialog.mode === "add") { |
| | | const now = dayjs().format("YYYY-MM-DD HH:mm:ss"); |
| | | allRows.value.unshift({ |
| | | id: `local_${Date.now()}`, |
| | | ...payload, |
| | | approvalResult: "pending", |
| | | rejectReason: "", |
| | | approvalRecords: [], |
| | | applyTime: now, |
| | | createTime: now, |
| | | }); |
| | | 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, |
| | | rejectReason: prev.approvalResult === "rejected" ? "" : prev.rejectReason, |
| | | applyTime: prev.applyTime, |
| | | createTime: prev.createTime, |
| | | }; |
| | | } |
| | | proxy?.$modal?.msgSuccess?.("保存成功(本地模拟)"); |
| | | if (submitSaving.value) return; |
| | | const isEdit = formDialog.mode === "edit"; |
| | | const dto = buildCostReimbursementSaveDto(form); |
| | | const check = validateReimbursementPersistDto(dto, isEdit); |
| | | if (!check.ok) { |
| | | proxy?.$modal?.msgWarning?.(check.message); |
| | | return; |
| | | } |
| | | formDialog.visible = false; |
| | | handleQuery(); |
| | | const nodeCheck = validateReimbursementApprovalNodes(dto); |
| | | if (!nodeCheck.ok) { |
| | | proxy?.$modal?.msgWarning?.(nodeCheck.message); |
| | | return; |
| | | } |
| | | submitSaving.value = true; |
| | | try { |
| | | await persistFinReimbursement(dto, isEdit); |
| | | proxy?.$modal?.msgSuccess?.(isEdit ? "保存成功" : "提交成功"); |
| | | formDialog.visible = false; |
| | | await handleQuery(); |
| | | } catch { |
| | | proxy?.$modal?.msgError?.(isEdit ? "保存失败" : "提交失败"); |
| | | } finally { |
| | | submitSaving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function submitApprove(result) { |
| | |
| | | } |
| | | |
| | | 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} 条`); |
| | | const params = buildFinReimbursementListParams({ |
| | | page: { current: 1, size: 99999 }, // Just so the function doesn't crash, backend export ignores pagination anyway |
| | | searchForm, |
| | | reimbursementType: FIN_REIMBURSEMENT_TYPE.COST, |
| | | }); |
| | | // Remove pagination parameters since export doesn't need them |
| | | delete params.current; |
| | | delete params.size; |
| | | delete params["page.current"]; |
| | | delete params["page.size"]; |
| | | proxy.download("/finReimbursement/export", params, `费用报销导出_${dayjs().format("YYYYMMDDHHmmss")}.xlsx`); |
| | | } |
| | | |
| | | function handleImportClick() { |
| | |
| | | reader.readAsText(file, "utf-8"); |
| | | } |
| | | |
| | | onMounted(() => loadUserPool()); |
| | | onMounted(async () => { |
| | | loadUserPool(); |
| | | loadDeptOptions(); |
| | | await fetchList(); |
| | | const editPayload = consumeReimburseEditFromApprove(); |
| | | if (editPayload?.reimbursementId != null) { |
| | | await openFormDialog("edit", { reimbursementId: editPayload.reimbursementId }); |
| | | } |
| | | }); |
| | | |
| | | return { |
| | | Search, |
| | |
| | | formDialog, |
| | | formRules, |
| | | detailDialog, |
| | | detailLoading, |
| | | detailRow, |
| | | approveDialog, |
| | | approveOpinion, |
| | | applicantFormSearchLoading, |
| | | applicantFormOptions, |
| | | deptOptions, |
| | | flowUserOptions, |
| | | detailTotalAmount, |
| | | approvalRuleHint, |
| | |
| | | remoteSearchApplicantForm, |
| | | userSelectLabel, |
| | | onApplicantChange, |
| | | onDeptChange, |
| | | onExpenseCategoryChange, |
| | | applyTemplate, |
| | | onDetailAmountChange, |
| | |
| | | openFormDialog, |
| | | onFormClosed, |
| | | submitForm, |
| | | submitSaving, |
| | | openDetail, |
| | | approvalActionLabel, |
| | | submitApprove, |