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