From 19f2e3bdbe04e7ea79c6a0bdc8c7318d4837b189 Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期四, 28 五月 2026 17:36:45 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_pro_山西_晋和园
---
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js | 696 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 696 insertions(+), 0 deletions(-)
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js
new file mode 100644
index 0000000..c92d88c
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js
@@ -0,0 +1,696 @@
+import { Search } from "@element-plus/icons-vue";
+import dayjs from "dayjs";
+import {
+ deleteFinReimbursement,
+ getFinReimbursementDetail,
+ listFinReimbursementPage,
+ persistFinReimbursement,
+} from "@/api/officeProcessAutomation/finReimbursement.js";
+import { ElMessageBox } from "element-plus";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue";
+import {
+ buildFinReimbursementListParams,
+ filterReimbursementRowsBySearch,
+ hasActiveReimbursementSearch,
+ buildTravelReimbursementSaveDto,
+ canDeleteReimbursementRow,
+ canEditReimbursementRow,
+ enrichReimbursementListRowsWithApprovalFlow,
+ filterRowsByReimbursementType,
+ FIN_REIMBURSEMENT_TYPE,
+ mapFinReimbursementDetailRow,
+ mapTravelReimbursementRow,
+ resolveReimbursementDeleteId,
+ unwrapFinReimbursementDetail,
+ unwrapFinReimbursementPage,
+ validateReimbursementApprovalNodes,
+ validateReimbursementPersistDto,
+} from "../shared/finReimbursementMappers.js";
+import { consumeReimburseEditFromApprove } from "../shared/reimburseApproveBridge.js";
+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";
+}
+
+export function useTravelReimburse() {
+ const { proxy } = getCurrentInstance();
+
+ const allRows = ref([]);
+
+ const searchForm = reactive({ applicantKeyword: "" });
+ 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 detailLoading = ref(false);
+ const detailRow = ref({});
+ const approveDialog = reactive({ visible: false, row: null });
+ const approveOpinion = ref("");
+ const submitSaving = ref(false);
+
+ const tableData = computed(() => allRows.value);
+
+ async function fetchList() {
+ tableLoading.value = true;
+ try {
+ const res = await listFinReimbursementPage(
+ buildFinReimbursementListParams({
+ page,
+ searchForm,
+ reimbursementType: FIN_REIMBURSEMENT_TYPE.TRAVEL,
+ })
+ );
+ const { records, total } = unwrapFinReimbursementPage(res);
+ const filtered = filterRowsByReimbursementType(
+ records,
+ FIN_REIMBURSEMENT_TYPE.TRAVEL
+ );
+ let mapped = filtered.map(mapTravelReimbursementRow);
+ mapped = await enrichReimbursementListRowsWithApprovalFlow(
+ mapped,
+ FIN_REIMBURSEMENT_TYPE.TRAVEL
+ );
+ 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));
+
+ 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: 220,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ disabled: (row) => !canEditReimbursementRow(row),
+ clickFun: (row) => openFormDialog("edit", row),
+ },
+ { name: "璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ disabled: (row) => !canDeleteReimbursementRow(row),
+ clickFun: (row) => confirmRemoveRow(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;
+ return fetchList();
+ }
+
+ function resetSearch() {
+ searchForm.applicantKeyword = "";
+ handleQuery();
+ }
+
+ function pagination(obj) {
+ page.current = obj.page;
+ page.size = obj.limit;
+ return fetchList();
+ }
+
+ async function loadTravelDetailRow(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.TRAVEL);
+ }
+
+ 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 loadTravelDetailRow(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) {
+ 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) {
+ let editRow = row;
+ try {
+ editRow = await loadTravelDetailRow(row);
+ } catch {
+ proxy?.$modal?.msgError?.("鍔犺浇鎶ラ攢璇︽儏澶辫触");
+ return;
+ }
+ Object.assign(form, {
+ ...JSON.parse(JSON.stringify(editRow)),
+ reimbursementId: editRow.reimbursementId ?? editRow.id,
+ attachmentList: JSON.parse(JSON.stringify(editRow.attachmentList || editRow.invoiceAttachments || [])),
+ approvalFlowNodes: JSON.parse(JSON.stringify(editRow.approvalFlowNodes || [])),
+ expenseDetails: JSON.parse(JSON.stringify(editRow.expenseDetails || [])),
+ });
+ const u = userById(editRow.applicantId);
+ applicantFormOptions.value = u
+ ? [u]
+ : [{ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.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;
+ }
+ }
+ if (submitSaving.value) return;
+ const isEdit = formDialog.mode === "edit";
+ const dto = buildTravelReimbursementSaveDto(form, { computeTravelDays });
+ const check = validateReimbursementPersistDto(dto, isEdit);
+ if (!check.ok) {
+ proxy?.$modal?.msgWarning?.(check.message);
+ return;
+ }
+ 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) {
+ 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 = allRows.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?.("瑙f瀽澶辫触");
+ }
+ };
+ reader.readAsText(file, "utf-8");
+ }
+
+ onMounted(async () => {
+ loadUserPool();
+ await fetchList();
+ const editPayload = consumeReimburseEditFromApprove();
+ if (editPayload?.reimbursementId != null) {
+ await openFormDialog("edit", { reimbursementId: editPayload.reimbursementId });
+ }
+ });
+
+ return {
+ Search,
+ EXPENSE_SUBJECT_OPTIONS,
+ expenseSubjectLabel,
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ tableColumn,
+ importInputRef,
+ formRef,
+ form,
+ formDialog,
+ formRules,
+ detailDialog,
+ detailLoading,
+ 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,
+ submitSaving,
+ openDetail,
+ confirmRemoveRow,
+ openApprove,
+ approvalActionLabel,
+ submitApprove,
+ handleExport,
+ handleImportClick,
+ onImportFile,
+ };
+}
--
Gitblit v1.9.3