From 2d3530a7c11557a40807ad7c0b9e302ce6ce3a9f Mon Sep 17 00:00:00 2001
From: hsy <1029150275@qq.com>
Date: 星期五, 28 八月 2026 10:16:32 +0800
Subject: [PATCH] feat: 综合业务模块页面重构与交互完善

---
 src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js |  541 ++++++++++++++++++++++++++++-------------------------
 1 files changed, 287 insertions(+), 254 deletions(-)

diff --git a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js
index 79ffe6b..9d8b616 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js
+++ b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js
@@ -1,7 +1,33 @@
 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,
@@ -42,110 +68,13 @@
 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: "閲囪喘鎵撳嵃鏈虹榧撱�丄4绾哥瓑鍔炲叕鑰楁潗銆�",
-      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 });
@@ -153,56 +82,63 @@
   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));
 
@@ -219,6 +155,9 @@
     { 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 },
@@ -247,21 +186,22 @@
         {
           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" }],
@@ -290,21 +230,39 @@
 
   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) {
@@ -312,22 +270,21 @@
     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;
     }
@@ -339,17 +296,56 @@
       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"));
   }
 
@@ -393,27 +389,81 @@
 
   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) {
@@ -434,16 +484,20 @@
     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("");
@@ -469,66 +523,31 @@
       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) {
@@ -569,15 +588,17 @@
   }
 
   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() {
@@ -607,7 +628,15 @@
     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,
@@ -627,11 +656,13 @@
     formDialog,
     formRules,
     detailDialog,
+    detailLoading,
     detailRow,
     approveDialog,
     approveOpinion,
     applicantFormSearchLoading,
     applicantFormOptions,
+    deptOptions,
     flowUserOptions,
     detailTotalAmount,
     approvalRuleHint,
@@ -641,6 +672,7 @@
     remoteSearchApplicantForm,
     userSelectLabel,
     onApplicantChange,
+    onDeptChange,
     onExpenseCategoryChange,
     applyTemplate,
     onDetailAmountChange,
@@ -652,6 +684,7 @@
     openFormDialog,
     onFormClosed,
     submitForm,
+    submitSaving,
     openDetail,
     approvalActionLabel,
     submitApprove,

--
Gitblit v1.9.3