From df5efb2ca2b0cf74d9160ffe2b6c215c4ddc9c99 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期四, 21 五月 2026 17:48:17 +0800
Subject: [PATCH] 差旅报销费用报销

---
 src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js |  275 ++++++++++++++++++++++++++++++++++--------------------
 1 files changed, 171 insertions(+), 104 deletions(-)

diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js
index 847e54f..9aa6294 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js
@@ -1,7 +1,29 @@
 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, watch } from "vue";
+import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue";
+import {
+  buildFinReimbursementListParams,
+  buildTravelReimbursementSaveDto,
+  canDeleteReimbursementRow,
+  canEditReimbursementRow,
+  filterRowsByReimbursementType,
+  FIN_REIMBURSEMENT_TYPE,
+  mapFinReimbursementDetailRow,
+  mapTravelReimbursementRow,
+  resolveReimbursementDeleteId,
+  unwrapFinReimbursementDetail,
+  unwrapFinReimbursementPage,
+  validateReimbursementPersistDto,
+} from "../shared/finReimbursementMappers.js";
+import { consumeReimburseEditFromApprove } from "../shared/reimburseApproveBridge.js";
 import {
   EXPENSE_SUBJECT_OPTIONS,
   expenseSubjectLabel,
@@ -48,43 +70,38 @@
   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.travelStartFrom) {
-      list = list.filter((r) => !r.travelStartTime || r.travelStartTime.slice(0, 10) >= searchForm.travelStartFrom);
-    }
-    if (searchForm.travelEndTo) {
-      list = list.filter((r) => !r.travelEndTime || r.travelEndTime.slice(0, 10) <= searchForm.travelEndTo);
-    }
-    return list.sort((a, b) => (String(a.createTime) < String(b.createTime) ? 1 : -1));
-  });
+  const tableData = computed(() => allRows.value);
 
-  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);
-  });
+  async function fetchList() {
+    tableLoading.value = true;
+    try {
+      const res = await listFinReimbursementPage(
+        buildFinReimbursementListParams({
+          page,
+          searchForm,
+          reimbursementType: FIN_REIMBURSEMENT_TYPE.TRAVEL,
+        })
+      );
+      const { records, total } = unwrapFinReimbursementPage(res);
+      allRows.value = filterRowsByReimbursementType(
+        records,
+        FIN_REIMBURSEMENT_TYPE.TRAVEL
+      ).map(mapTravelReimbursementRow);
+      page.total = total;
+    } catch {
+      allRows.value = [];
+      page.total = 0;
+      proxy?.$modal?.msgError?.("宸梾鎶ラ攢鍒楄〃鍔犺浇澶辫触");
+    } finally {
+      tableLoading.value = false;
+    }
+  }
 
   const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser));
 
@@ -156,11 +173,21 @@
       label: "鎿嶄綔",
       align: "center",
       fixed: "right",
-      width: 200,
+      width: 220,
       operation: [
-        { name: "缂栬緫", type: "text", disabled: (row) => row.approvalResult === "pending" || row.approvalResult === "approved", clickFun: (row) => openFormDialog("edit", row) },
+        {
+          name: "缂栬緫",
+          type: "text",
+          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),
+        },
       ],
     },
   ]);
@@ -334,8 +361,7 @@
 
   function handleQuery() {
     page.current = 1;
-    tableLoading.value = true;
-    setTimeout(() => { tableLoading.value = false; }, 150);
+    return fetchList();
   }
 
   function resetSearch() {
@@ -348,11 +374,70 @@
   function pagination(obj) {
     page.current = obj.page;
     page.size = obj.limit;
+    return fetchList();
   }
 
-  function openDetail(row) {
-    detailRow.value = { ...row };
+  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) {
@@ -373,14 +458,24 @@
     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(row)),
-        attachmentList: JSON.parse(JSON.stringify(row.attachmentList || row.invoiceAttachments || [])),
-        approvalFlowNodes: JSON.parse(JSON.stringify(row.approvalFlowNodes || [])),
-        expenseDetails: JSON.parse(JSON.stringify(row.expenseDetails || [])),
+        ...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(row.applicantId);
-      applicantFormOptions.value = u ? [u] : [{ userId: row.applicantId, nickName: row.employeeName, userName: row.employeeNo }];
+      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("");
@@ -414,63 +509,25 @@
         return;
       }
     }
-    const days = computeTravelDays(form.travelStartTime, form.travelEndTime);
-    const payload = {
-      reimburseNo: form.reimburseNo || `TR${dayjs().format("YYYYMMDDHHmmss")}`,
-      applicantId: form.applicantId,
-      employeeNo: form.employeeNo,
-      employeeName: form.employeeName,
-      applicantNo: form.employeeNo,
-      applicantName: form.employeeName,
-      reimburseReason: form.reimburseReason,
-      travelStartTime: form.travelStartTime,
-      travelEndTime: form.travelEndTime,
-      travelDays: days,
-      departurePlace: form.departurePlace,
-      destination: form.destination,
-      hotelStandard: form.hotelStandard,
-      hotelDays: form.hotelDays,
-      livingSubsidy: form.livingSubsidy,
-      applyAmount: form.applyAmount,
-      payee: form.payee,
-      expenseDetails: JSON.parse(JSON.stringify(form.expenseDetails)),
-      attachmentList: JSON.parse(JSON.stringify(form.attachmentList || [])),
-      invoiceAttachments: mapAttachmentList(form.attachmentList),
-      approvalFlowNodes: initApprovalFlowNodes(form.approvalFlowNodes),
-      currentNodeIndex: 0,
-      needSpecialApproval: form.needSpecialApproval,
-      deptId: form.deptId,
-      deptName: form.deptName,
-      travelTier: form.travelTier,
-    };
-    if (formDialog.mode === "add") {
-      allRows.value.unshift({
-        id: `local_${Date.now()}`,
-        ...payload,
-        approvalResult: "pending",
-        rejectReason: "",
-        approvalRecords: [],
-        createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
-      });
-      proxy?.$modal?.msgSuccess?.("鎻愪氦鎴愬姛");
-    } else {
-      const idx = allRows.value.findIndex((r) => r.id === form.id);
-      if (idx !== -1) {
-        const prev = allRows.value[idx];
-        allRows.value[idx] = {
-          ...prev,
-          ...payload,
-          id: form.id,
-          approvalResult: prev.approvalResult === "rejected" ? "pending" : prev.approvalResult,
-          approvalFlowNodes: initApprovalFlowNodes(form.approvalFlowNodes),
-          currentNodeIndex: 0,
-          createTime: prev.createTime,
-        };
-      }
-      proxy?.$modal?.msgSuccess?.("淇濆瓨鎴愬姛");
+    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;
     }
-    formDialog.visible = false;
-    handleQuery();
+    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) {
@@ -511,7 +568,7 @@
   }
 
   function handleExport() {
-    const data = filteredList.value;
+    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");
@@ -549,7 +606,14 @@
     reader.readAsText(file, "utf-8");
   }
 
-  onMounted(() => loadUserPool());
+  onMounted(async () => {
+    loadUserPool();
+    await fetchList();
+    const editPayload = consumeReimburseEditFromApprove();
+    if (editPayload?.reimbursementId != null) {
+      await openFormDialog("edit", { reimbursementId: editPayload.reimbursementId });
+    }
+  });
 
   return {
     Search,
@@ -566,6 +630,7 @@
     formDialog,
     formRules,
     detailDialog,
+    detailLoading,
     detailRow,
     approveDialog,
     approveOpinion,
@@ -596,7 +661,9 @@
     openFormDialog,
     onFormClosed,
     submitForm,
+    submitSaving,
     openDetail,
+    confirmRemoveRow,
     openApprove,
     approvalActionLabel,
     submitApprove,

--
Gitblit v1.9.3