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/ApproveManage/approve-list/index.vue                           |  103 +++
 src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/travelReimburseUtils.js       |    7 
 src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue                     |   15 
 src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementMappers.js              |  623 ++++++++++++++++++++
 src/views/officeProcessAutomation/ReimburseManage/shared/reimburseApproveBridge.js               |  124 ++++
 src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementDetailExtras.js         |  152 ++++
 src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js                   |  102 +++
 src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js         |  275 +++++---
 src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js             |  281 +++++---
 src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js           |    7 
 src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js             |    2 
 src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue                       |   15 
 src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/components/DetailPanel.vue      |    5 
 src/views/officeProcessAutomation/ReimburseManage/shared/components/FinReimburseApprovePanel.vue |   70 ++
 src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue    |    7 
 src/api/officeProcessAutomation/finReimbursement.js                                              |   71 ++
 16 files changed, 1,620 insertions(+), 239 deletions(-)

diff --git a/src/api/officeProcessAutomation/finReimbursement.js b/src/api/officeProcessAutomation/finReimbursement.js
new file mode 100644
index 0000000..84c3560
--- /dev/null
+++ b/src/api/officeProcessAutomation/finReimbursement.js
@@ -0,0 +1,71 @@
+import request from "@/utils/request";
+
+/** 鍒嗛〉鏌ヨ璐㈠姟鎶ラ攢 GET /finReimbursement/listPage */
+export function listFinReimbursementPage(params) {
+  return request({
+    url: "/finReimbursement/listPage",
+    method: "get",
+    params,
+  });
+}
+
+/** 璇︽儏 query锛歋pring 缁戝畾 finReimbursementDto.id锛屽嬁鐢� finReimbursementDto[id] */
+function buildFinReimbursementDetailParams(idOrDto) {
+  const raw =
+    typeof idOrDto === "object" && idOrDto !== null
+      ? idOrDto.id ?? idOrDto.reimbursementId
+      : idOrDto;
+  return {
+    "finReimbursementDto.id": raw,
+    id: raw,
+  };
+}
+
+/** 鏌ヨ璐㈠姟鎶ラ攢璇︽儏 GET /finReimbursement/detail */
+export function getFinReimbursementDetail(idOrDto) {
+  return request({
+    url: "/finReimbursement/detail",
+    method: "get",
+    params: buildFinReimbursementDetailParams(idOrDto),
+  });
+}
+
+/** 鏂板璐㈠姟鎶ラ攢 POST /finReimbursement/save */
+export function saveFinReimbursement(finReimbursementDto) {
+  return request({
+    url: "/finReimbursement/save",
+    method: "post",
+    data: finReimbursementDto,
+  });
+}
+
+/** 淇敼璐㈠姟鎶ラ攢 POST /finReimbursement/update */
+export function updateFinReimbursement(finReimbursementDto) {
+  return request({
+    url: "/finReimbursement/update",
+    method: "post",
+    data: finReimbursementDto,
+  });
+}
+
+/** 鍒犻櫎璐㈠姟鎶ラ攢 DELETE /finReimbursement/delete锛坆ody 涓� ID 鏁扮粍锛� */
+export function deleteFinReimbursement(ids) {
+  const idList = (Array.isArray(ids) ? ids : [ids]).filter(
+    (id) => id != null && id !== ""
+  );
+  return request({
+    url: "/finReimbursement/delete",
+    method: "delete",
+    data: idList,
+  });
+}
+
+/** 鏂板璧� save锛屼慨鏀硅蛋 update锛堜笌鎺ュ彛鏂囨。涓�鑷达級 */
+export function persistFinReimbursement(finReimbursementDto, isEdit = false) {
+  if (isEdit) {
+    return updateFinReimbursement(finReimbursementDto);
+  }
+  const payload = { ...finReimbursementDto };
+  delete payload.id;
+  return saveFinReimbursement(payload);
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
index 4aa3c61..96f7158 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
@@ -475,7 +475,7 @@
     applicantId: row.applicantId,
     applicantNo: row.applicantId != null ? String(row.applicantId) : "",
     applicantName: row.applicantName || "",
-    approvalType: row.templateName || "",
+    approvalType: row.approvalType || row.templateName || "",
     unread: Boolean(row.isApprove) && approvalStatus === "pending",
     isApprove: Boolean(row.isApprove),
     approvalStatus,
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
index eba9586..bbfa56a 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
@@ -223,6 +223,64 @@
       </template>
     </el-dialog>
 
+    <!-- 宸梾/璐圭敤鎶ラ攢璇︽儏锛堝鎵瑰垪琛級 -->
+    <el-dialog
+      v-model="reimburseDialog.visible"
+      :title="reimburseDialog.mode === 'approve' ? reimburseApproveTitle : reimburseDetailTitle"
+      width="1000px"
+      append-to-body
+      destroy-on-close
+      @closed="approveOpinion = ''"
+    >
+      <FinReimburseApprovePanel
+        :mode="reimburseDialog.mode"
+        :module-key="reimburseDialog.moduleKey"
+        :reimburse-row="reimburseDialog.reimburseRow"
+        :loading="reimburseDialog.loading"
+        v-model:approve-opinion="approveOpinion"
+      />
+      <template #footer>
+        <template v-if="reimburseDialog.mode === 'approve'">
+          <el-button
+            type="success"
+            :loading="approveSubmitting"
+            @click="onReimburseApprove('approved')"
+          >
+            閫� 杩�
+          </el-button>
+          <el-button
+            type="danger"
+            :loading="approveSubmitting"
+            @click="onReimburseApprove('rejected')"
+          >
+            椹� 鍥�
+          </el-button>
+          <el-button :disabled="approveSubmitting" @click="reimburseDialog.visible = false">
+            鍙� 娑�
+          </el-button>
+        </template>
+        <template v-else>
+          <el-button
+            v-if="reimburseDialog.instanceRow?.approvalStatus === 'pending'"
+            @click="openEditFromReimburseDetail"
+          >
+            淇� 鏀�
+          </el-button>
+          <el-button
+            v-if="
+              reimburseDialog.instanceRow?.approvalStatus === 'pending' &&
+              reimburseDialog.instanceRow?.isApprove
+            "
+            type="primary"
+            @click="openReimburseApproveFromDetail"
+          >
+            鍘诲鎵�
+          </el-button>
+          <el-button type="primary" @click="reimburseDialog.visible = false">鍏� 闂�</el-button>
+        </template>
+      </template>
+    </el-dialog>
+
     <!-- 瀹℃壒鎿嶄綔 -->
     <el-dialog
       v-model="approveDialog.visible"
@@ -277,7 +335,9 @@
 <script setup>
 import { Plus, RefreshRight } from "@element-plus/icons-vue";
 import { ElMessage } from "element-plus";
-import { onMounted, ref } from "vue";
+import { computed, onMounted, ref } from "vue";
+import { APPROVAL_MODULE_KEYS } from "../approve-shared/approvalModuleRegistry.js";
+import FinReimburseApprovePanel from "../../ReimburseManage/shared/components/FinReimburseApprovePanel.vue";
 import ApprovalTemplateFormSection from "../approve-shared/components/ApprovalTemplateFormSection.vue";
 import ApprovalTemplatePicker from "../approve-shared/components/ApprovalTemplatePicker.vue";
 import { useFlowUserOptions } from "../approve-shared/useFlowUserOptions.js";
@@ -309,9 +369,11 @@
   tableColumn,
   detailDialog,
   detailRow,
+  reimburseDialog,
   approveDialog,
   approveOpinion,
   approveSubmitting,
+  submitReimburseApprove,
   submitDialog,
   isSubmitEdit,
   submitDialogTitle,
@@ -342,8 +404,30 @@
   if (ok) ElMessage.success(isSubmitEdit.value ? "淇敼鎴愬姛" : "瀹℃壒宸叉彁浜�");
 }
 
+const reimburseDetailTitle = computed(() =>
+  reimburseDialog.moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE
+    ? "璐圭敤鎶ラ攢璇︽儏"
+    : "宸梾鎶ラ攢璇︽儏"
+);
+const reimburseApproveTitle = computed(() =>
+  reimburseDialog.moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE
+    ? "璐圭敤鎶ラ攢瀹℃壒"
+    : "宸梾鎶ラ攢瀹℃壒"
+);
+
 async function onApprove(result) {
   const ret = await submitApprove(result);
+  if (ret?.needOpinion) {
+    ElMessage.warning("椹冲洖鏃惰濉啓瀹℃壒鎰忚");
+    return;
+  }
+  if (ret?.ok) {
+    ElMessage.success(result === "approved" ? "宸查�氳繃" : "宸查┏鍥�");
+  }
+}
+
+async function onReimburseApprove(result) {
+  const ret = await submitReimburseApprove(result);
   if (ret?.needOpinion) {
     ElMessage.warning("椹冲洖鏃惰濉啓瀹℃壒鎰忚");
     return;
@@ -357,10 +441,10 @@
   return formatDisplayTime(time) || "鈥�";
 }
 
-function openApproveFromDetail() {
+async function openApproveFromDetail() {
   const row = detailRow.value;
   detailDialog.visible = false;
-  openApprove(row);
+  await openApprove(row);
 }
 
 function openEditFromDetail() {
@@ -369,6 +453,19 @@
   openEditDialog(row);
 }
 
+function openEditFromReimburseDetail() {
+  const row = reimburseDialog.instanceRow;
+  reimburseDialog.visible = false;
+  if (row) openEditDialog(row);
+}
+
+async function openReimburseApproveFromDetail() {
+  const row = reimburseDialog.instanceRow;
+  if (!row) return;
+  reimburseDialog.mode = "approve";
+  approveOpinion.value = "";
+}
+
 onMounted(() => {
   loadFlowUsers();
   loadSearchBusinessTypeOptions();
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
index 3523ef4..67b9213 100644
--- a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
@@ -13,7 +13,13 @@
 import useUserStore from "@/store/modules/user";
 import { Search } from "@element-plus/icons-vue";
 import { ElMessage, ElMessageBox } from "element-plus";
-import { computed, reactive, ref } from "vue";
+import { computed, getCurrentInstance, reactive, ref } from "vue";
+import {
+  inferReimburseModuleKeyFromInstance,
+  loadReimburseDetailForInstance,
+  navigateToReimburseManageForEdit,
+  resolveFinReimbursementIdFromInstance,
+} from "../../ReimburseManage/shared/reimburseApproveBridge.js";
 import {
   fetchBusinessTypeOptions,
   formatDisplayTime,
@@ -43,6 +49,7 @@
 } from "./approveListConstants.js";
 
 export function useApproveList() {
+  const { proxy } = getCurrentInstance() || {};
   const userStore = useUserStore();
 
   const tableData = ref([]);
@@ -74,6 +81,16 @@
   const approveDialog = reactive({ visible: false, row: null });
   const approveOpinion = ref("");
   const approveSubmitting = ref(false);
+
+  /** 宸梾/璐圭敤鎶ラ攢涓撶敤璇︽儏銆佸鎵瑰脊绐� */
+  const reimburseDialog = reactive({
+    visible: false,
+    mode: "detail",
+    moduleKey: "",
+    loading: false,
+    reimburseRow: {},
+    instanceRow: null,
+  });
 
   const submitDialog = reactive({ visible: false, step: 1, mode: "add" });
   const submitEditRow = ref(null);
@@ -242,15 +259,52 @@
     fetchApprovalList();
   }
 
-  function openDetail(row) {
+  async function openReimburseDetail(row, mode) {
+    const moduleKey = inferReimburseModuleKeyFromInstance(row);
+    if (!moduleKey) return false;
+    reimburseDialog.mode = mode;
+    reimburseDialog.moduleKey = moduleKey;
+    reimburseDialog.instanceRow = row;
+    reimburseDialog.visible = true;
+    reimburseDialog.loading = true;
+    reimburseDialog.reimburseRow = {};
+    try {
+      const { reimburseRow, moduleKey: resolvedMk } =
+        await loadReimburseDetailForInstance(row, moduleKey);
+      reimburseDialog.moduleKey = resolvedMk || moduleKey;
+      reimburseDialog.reimburseRow = reimburseRow;
+      return true;
+    } catch {
+      ElMessage.error("鍔犺浇鎶ラ攢璇︽儏澶辫触");
+      reimburseDialog.visible = false;
+      return false;
+    } finally {
+      reimburseDialog.loading = false;
+    }
+  }
+
+  async function openDetail(row) {
+    if (isReimburseApprovalInstance(row)) {
+      await openReimburseDetail(row, "detail");
+      return;
+    }
     detailRow.value = { ...row };
     detailDialog.visible = true;
   }
 
-  function openApprove(row) {
+  async function openApprove(row) {
+    if (inferReimburseModuleKeyFromInstance(row)) {
+      approveOpinion.value = "";
+      await openReimburseDetail(row, "approve");
+      return;
+    }
     approveDialog.row = { ...row };
     approveOpinion.value = "";
     approveDialog.visible = true;
+  }
+
+  function isReimburseApprovalInstance(row) {
+    return Boolean(inferReimburseModuleKeyFromInstance(row));
   }
 
   function resetSubmitDialogState() {
@@ -267,9 +321,23 @@
     loadSubmitTemplates();
   }
 
-  function openEditDialog(row) {
+  async function openEditDialog(row) {
     if (row?.approvalStatus !== "pending") {
       ElMessage.warning("浠呭鏍镐腑鐨勫鎵瑰彲淇敼");
+      return;
+    }
+    const moduleKey = inferReimburseModuleKeyFromInstance(row);
+    if (moduleKey) {
+      const rid = resolveFinReimbursementIdFromInstance(row);
+      if (rid == null) {
+        ElMessage.warning("鏃犳硶淇敼锛氱己灏戞姤閿�鍗� ID");
+        return;
+      }
+      try {
+        await navigateToReimburseManageForEdit(proxy?.$router, moduleKey, rid);
+      } catch {
+        ElMessage.warning("鏈壘鍒板樊鏃�/璐圭敤鎶ラ攢鑿滃崟璺敱锛岃浠庡乏渚ц彍鍗曡繘鍏ュ悗鍐嶇紪杈�");
+      }
       return;
     }
     if (!row?.id) {
@@ -444,6 +512,29 @@
     }
   }
 
+  async function submitReimburseApprove(result) {
+    const row = reimburseDialog.instanceRow;
+    if (!row?.id) return { ok: false };
+    if (result === "rejected" && !(approveOpinion.value || "").trim()) {
+      return { needOpinion: true };
+    }
+    if (approveSubmitting.value) return { ok: false };
+    approveSubmitting.value = true;
+    try {
+      await approveApprovalInstance(
+        buildApproveInstanceDto(row, result, approveOpinion.value)
+      );
+      reimburseDialog.visible = false;
+      await fetchApprovalList();
+      return { ok: true, result };
+    } catch {
+      ElMessage.error("瀹℃壒鎿嶄綔澶辫触");
+      return { ok: false };
+    } finally {
+      approveSubmitting.value = false;
+    }
+  }
+
   async function submitApprove(result) {
     const row = approveDialog.row;
     if (!row?.id) return { ok: false };
@@ -495,9 +586,12 @@
     tableColumn,
     detailDialog,
     detailRow,
+    reimburseDialog,
     approveDialog,
     approveOpinion,
     approveSubmitting,
+    submitReimburseApprove,
+    isReimburseApprovalInstance,
     submitDialog,
     isSubmitEdit,
     submitDialogTitle,
diff --git a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/components/DetailPanel.vue b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/components/DetailPanel.vue
index bfe1b68..4db16a7 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/components/DetailPanel.vue
+++ b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/components/DetailPanel.vue
@@ -50,7 +50,10 @@
 });
 
 const attachmentFiles = computed(() => {
-  const list = props.row?.attachmentList?.length ? props.row.attachmentList : props.row?.invoiceAttachments;
+  const list =
+    props.row?.attachmentList ||
+    props.row?.storageBlobVOList ||
+    props.row?.invoiceAttachments;
   return Array.isArray(list) ? list : [];
 });
 
diff --git a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js
index 012e4d8..2d88cd5 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js
+++ b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js
@@ -107,14 +107,19 @@
 }
 
 export function statusLabel(v) {
+  if (v === "draft") return "鑽夌";
   if (v === "approved") return "宸查�氳繃";
+  if (v === "paid") return "宸蹭粯娆�";
   if (v === "rejected") return "宸查┏鍥�";
+  if (v === "cancelled") return "宸叉挙鍥�";
   return "瀹℃牳涓�";
 }
 
 export function statusTagType(v) {
-  if (v === "approved") return "success";
+  if (v === "draft") return "info";
+  if (v === "approved" || v === "paid") return "success";
   if (v === "rejected") return "danger";
+  if (v === "cancelled") return "info";
   return "warning";
 }
 
diff --git a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
index b384569..4b33707 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
+++ b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
@@ -1,4 +1,4 @@
-<!--OA妯″潡锛氳垂鐢ㄦ姤閿�-->
+<!--OA妯″潡锛氳垂鐢ㄦ姤閿�锛堝垪琛� /finReimbursement/listPage锛宺eimbursementType=2锛�-->
 <template>
   <div class="app-container">
     <div class="search_form mb20">
@@ -318,13 +318,21 @@
         </el-card>
       </el-form>
       <template #footer>
-        <el-button v-if="!formDialog.readonly" type="primary" @click="submitForm">鎻� 浜�</el-button>
+        <el-button
+          v-if="!formDialog.readonly"
+          type="primary"
+          :loading="submitSaving"
+          @click="submitForm"
+        >
+          鎻� 浜�
+        </el-button>
         <el-button @click="formDialog.visible = false">{{ formDialog.readonly ? "鍏� 闂�" : "鍙� 娑�" }}</el-button>
       </template>
     </el-dialog>
 
     <!-- 璇︽儏 -->
     <el-dialog v-model="detailDialog.visible" title="璐圭敤鎶ラ攢璇︽儏" width="900px" append-to-body destroy-on-close>
+      <div v-loading="detailLoading">
       <DetailPanel :row="detailRow" />
       <el-divider content-position="left">瀹℃壒娴佺▼</el-divider>
       <ApprovalFlowProgress :nodes="detailRow.approvalFlowNodes" :current-index="detailRow.currentNodeIndex ?? 0" />
@@ -340,6 +348,7 @@
         </el-timeline-item>
       </el-timeline>
       <el-empty v-else description="鏆傛棤瀹℃壒璁板綍" :image-size="60" />
+      </div>
       <template #footer>
         <el-button type="primary" @click="detailDialog.visible = false">鍏� 闂�</el-button>
       </template>
@@ -406,6 +415,7 @@
   formDialog,
   formRules,
   detailDialog,
+  detailLoading,
   detailRow,
   approveDialog,
   approveOpinion,
@@ -431,6 +441,7 @@
   openFormDialog,
   onFormClosed,
   submitForm,
+  submitSaving,
   approvalActionLabel,
   submitApprove,
   handleExport,
diff --git a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js
index a37ee4e..638d533 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js
+++ b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.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 {
+  buildCostReimbursementSaveDto,
+  buildFinReimbursementListParams,
+  canDeleteReimbursementRow,
+  canEditReimbursementRow,
+  filterRowsByReimbursementType,
+  FIN_REIMBURSEMENT_TYPE,
+  mapCostReimbursementRow,
+  mapFinReimbursementDetailRow,
+  resolveReimbursementDeleteId,
+  unwrapFinReimbursementDetail,
+  unwrapFinReimbursementPage,
+  validateReimbursementPersistDto,
+} from "../shared/finReimbursementMappers.js";
+import { consumeReimburseEditFromApprove } from "../shared/reimburseApproveBridge.js";
 import {
   EXPENSE_CATEGORY_OPTIONS,
   CATEGORY_TEMPLATES,
@@ -59,52 +81,43 @@
   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);
+      allRows.value = filterRowsByReimbursementType(
+        records,
+        FIN_REIMBURSEMENT_TYPE.COST
+      ).map(mapCostReimbursementRow);
+      page.total = total;
+    } catch {
+      allRows.value = [];
+      page.total = 0;
+      proxy?.$modal?.msgError?.("璐圭敤鎶ラ攢鍒楄〃鍔犺浇澶辫触");
+    } finally {
+      tableLoading.value = false;
+    }
+  }
 
   const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser));
 
@@ -149,15 +162,15 @@
         {
           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),
         },
       ],
     },
@@ -295,10 +308,7 @@
 
   function handleQuery() {
     page.current = 1;
-    tableLoading.value = true;
-    setTimeout(() => {
-      tableLoading.value = false;
-    }, 150);
+    return fetchList();
   }
 
   function resetSearch() {
@@ -311,11 +321,70 @@
   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) {
@@ -336,16 +405,24 @@
     if (!allUsersCache.value.length) await loadUserPool();
     Object.assign(form, createEmptyForm());
     if (mode === "edit" && row) {
+      let editRow = row;
+      try {
+        editRow = await loadCostDetailRow(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);
+      const u = userById(editRow.applicantId);
       applicantFormOptions.value = u
         ? [u]
-        : [{ userId: row.applicantId, nickName: row.employeeName, userName: row.employeeNo }];
+        : [{ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.employeeNo }];
     } else {
       form.approvalFlowNodes = buildAutoApprovalFlow(0, "other");
       remoteSearchApplicantForm("");
@@ -373,64 +450,25 @@
     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();
+    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) {
@@ -471,7 +509,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");
@@ -509,7 +547,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,
@@ -529,6 +574,7 @@
     formDialog,
     formRules,
     detailDialog,
+    detailLoading,
     detailRow,
     approveDialog,
     approveOpinion,
@@ -554,6 +600,7 @@
     openFormDialog,
     onFormClosed,
     submitForm,
+    submitSaving,
     openDetail,
     approvalActionLabel,
     submitApprove,
diff --git a/src/views/officeProcessAutomation/ReimburseManage/shared/components/FinReimburseApprovePanel.vue b/src/views/officeProcessAutomation/ReimburseManage/shared/components/FinReimburseApprovePanel.vue
new file mode 100644
index 0000000..98b895d
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/shared/components/FinReimburseApprovePanel.vue
@@ -0,0 +1,70 @@
+<!-- 宸梾/璐圭敤鎶ラ攢锛氬鎵瑰垪琛ㄥ唴璇︽儏/瀹℃壒寮圭獥鍐呭锛堜笌鎶ラ攢椤靛脊绐椾竴鑷达級 -->
+<template>
+  <div v-loading="loading">
+    <TravelDetailPanel v-if="isTravel" :row="reimburseRow" />
+    <CostDetailPanel v-else :row="reimburseRow" />
+
+    <el-divider content-position="left">娴佺▼杩涘害</el-divider>
+    <ApprovalFlowProgress
+      :nodes="reimburseRow.approvalFlowNodes"
+      :current-index="reimburseRow.currentNodeIndex ?? 0"
+    />
+
+    <template v-if="mode === 'detail'">
+      <el-divider content-position="left">瀹℃壒璁板綍锛堝叏娴佺▼鐣欑棔锛�</el-divider>
+      <el-timeline v-if="reimburseRow.approvalRecords?.length">
+        <el-timeline-item
+          v-for="(rec, i) in reimburseRow.approvalRecords"
+          :key="i"
+          :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'"
+          :timestamp="rec.time"
+        >
+          {{ rec.operatorName }} 鈥� {{ actionLabel(rec.result) }}锛歿{ rec.opinion || "鏃犳剰瑙�" }}
+        </el-timeline-item>
+      </el-timeline>
+      <el-empty v-else description="鏆傛棤瀹℃壒璁板綍" :image-size="60" />
+    </template>
+
+    <el-form v-else label-width="100px" class="mt16">
+      <el-form-item label="瀹℃壒鎰忚">
+        <el-input
+          :model-value="approveOpinion"
+          type="textarea"
+          :rows="3"
+          maxlength="500"
+          show-word-limit
+          :placeholder="isTravel ? '閫氳繃鍙暀绌猴紱椹冲洖璇峰~鍐欏師鍥�' : '閫氳繃鍙暀绌猴紱椹冲洖璇峰~鍐欏叿浣撳師鍥狅紙濡傦細鍙戠エ妯$硦闇�閲嶄紶锛�'"
+          @update:model-value="$emit('update:approveOpinion', $event)"
+        />
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { isTravelReimbursementType } from "../finReimbursementMappers.js";
+import ApprovalFlowProgress from "../../travel-reimburse/components/ApprovalFlowProgress.vue";
+import CostDetailPanel from "../../cost-reimburse/components/DetailPanel.vue";
+import TravelDetailPanel from "../../travel-reimburse/components/DetailPanel.vue";
+
+const props = defineProps({
+  mode: { type: String, default: "detail" },
+  moduleKey: { type: String, default: "" },
+  reimburseRow: { type: Object, default: () => ({}) },
+  loading: { type: Boolean, default: false },
+  approveOpinion: { type: String, default: "" },
+});
+
+defineEmits(["update:approveOpinion"]);
+
+const isTravel = computed(() =>
+  isTravelReimbursementType(props.reimburseRow?.reimbursementType ?? props.moduleKey)
+);
+
+function actionLabel(v) {
+  if (v === "approved") return "閫氳繃";
+  if (v === "rejected") return "椹冲洖";
+  return "鎻愪氦";
+}
+</script>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementDetailExtras.js b/src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementDetailExtras.js
new file mode 100644
index 0000000..dd9a1ac
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementDetailExtras.js
@@ -0,0 +1,152 @@
+import { formatDisplayTime } from "../../ApproveManage/approve-template/approveTemplateConstants.js";
+import {
+  mapRecordResultFromApi,
+  mapRecordsFromApi,
+  mapTasksToFlowNodes,
+} from "../../ApproveManage/approve-list/approveListConstants.js";
+
+function taskStatusToNodeStatus(taskStatus) {
+  const s = String(taskStatus ?? "").toUpperCase();
+  if (["APPROVED", "COMPLETED", "FINISHED", "PASSED", "AGREE"].includes(s)) {
+    return "finish";
+  }
+  if (["REJECTED", "REJECT", "REFUSE", "REFUSED"].includes(s)) {
+    return "error";
+  }
+  if (["PENDING", "IN_APPROVAL", "PROCESS", "PROCESSING"].includes(s)) {
+    return "process";
+  }
+  return "wait";
+}
+
+/** storageBlobVOList 鈫� 椤甸潰闄勪欢鍒楄〃 */
+export function mapReimbursementAttachments(source = {}) {
+  const list =
+    source.storageBlobVOList ||
+    source.storageBlobDTOs ||
+    source.storageBlobDTOS ||
+    source.storageBlobVOS ||
+    source.attachmentList ||
+    source.invoiceAttachments ||
+    [];
+  if (!Array.isArray(list)) return [];
+  return list.map((b, i) => ({
+    ...b,
+    id: b.id ?? b.blobId ?? `att_${i}`,
+    name:
+      b.fileName ||
+      b.originalFilename ||
+      b.originalFileName ||
+      b.blobName ||
+      b.name ||
+      "闄勪欢",
+    url:
+      b.url ||
+      b.fileUrl ||
+      b.downloadUrl ||
+      b.downloadURL ||
+      b.previewUrl ||
+      b.previewURL ||
+      b.link ||
+      "",
+  }));
+}
+
+/** 瀹℃壒璁板綍鏉ヨ嚜 tasks锛堟瘡鏉′换鍔′竴鏉$暀鐥曪級 */
+export function mapTasksToApprovalRecords(tasks) {
+  const list = Array.isArray(tasks) ? tasks : [];
+  return list
+    .map((t, index) => ({
+      id: t.id ?? index,
+      operatorName: t.approverName || t.operatorName || t.createUserName || "鈥�",
+      result: mapRecordResultFromApi(
+        t.approveAction ?? t.taskStatus ?? t.status
+      ),
+      opinion: t.approveComment || t.comment || t.opinion || "",
+      time: formatDisplayTime(
+        t.approveTime || t.finishTime || t.updateTime || t.createTime || ""
+      ),
+      levelNo: t.levelNo ?? t.taskLevel,
+      raw: t,
+    }))
+    .sort((a, b) => {
+      const la = Number(a.levelNo ?? 0);
+      const lb = Number(b.levelNo ?? 0);
+      if (la !== lb) return la - lb;
+      return String(a.time).localeCompare(String(b.time));
+    });
+}
+
+/** tasks 鈫� ApprovalFlowProgress 鑺傜偣 */
+export function mapTasksToApprovalFlowNodes(tasks) {
+  const grouped = mapTasksToFlowNodes(tasks);
+  return grouped.map((node, i) => {
+    const approvers = node.approvers || [];
+    const statuses = approvers.map(a =>
+      taskStatusToNodeStatus(a.taskStatus ?? a.status)
+    );
+    let nodeStatus = "wait";
+    if (statuses.includes("error")) nodeStatus = "error";
+    else if (statuses.length && statuses.every(s => s === "finish")) {
+      nodeStatus = "finish";
+    } else if (statuses.includes("process")) nodeStatus = "process";
+
+    const names = approvers.map(a => a.approverName).filter(Boolean).join("銆�");
+    const opinions = approvers
+      .map(a => a.approveComment)
+      .filter(Boolean)
+      .join("锛�");
+
+    return {
+      nodeOrder: node.nodeOrder ?? node.levelNo ?? i + 1,
+      sortOrder: node.nodeOrder ?? node.levelNo ?? i + 1,
+      approverName: names || "鈥�",
+      approveOpinion: opinions,
+      approveTime: approvers.find(a => a.approveTime)?.approveTime || "",
+      nodeStatus,
+      signMode: node.signMode,
+    };
+  });
+}
+
+export function computeApprovalFlowCurrentIndex(approvalFlowNodes = []) {
+  const list = approvalFlowNodes || [];
+  const processing = list.findIndex(n => n.nodeStatus === "process");
+  if (processing >= 0) return processing;
+  const errorIdx = list.findIndex(n => n.nodeStatus === "error");
+  if (errorIdx >= 0) return errorIdx;
+  return list.filter(n => n.nodeStatus === "finish").length;
+}
+
+/** 璇︽儏 DTO 琛ュ厖 tasks / 闄勪欢 / 瀹℃壒璁板綍 */
+export function applyFinReimbursementDetailEnrichment(mapped, raw = {}) {
+  if (!mapped || typeof mapped !== "object") return mapped;
+  const source = { ...raw, ...mapped };
+  const tasks = Array.isArray(source.tasks) ? source.tasks : [];
+  const attachments = mapReimbursementAttachments(source);
+  const approvalRecords = tasks.length
+    ? mapTasksToApprovalRecords(tasks)
+    : mapRecordsFromApi(source.records || source.approvalRecords);
+  const approvalFlowNodes = tasks.length
+    ? mapTasksToApprovalFlowNodes(tasks)
+    : mapped.approvalFlowNodes || [];
+  const currentNodeIndex = computeApprovalFlowCurrentIndex(approvalFlowNodes);
+  const rejectReason =
+    approvalRecords.find(r => r.result === "rejected")?.opinion ||
+    source.rejectReason ||
+    "";
+
+  return {
+    ...mapped,
+    tasks,
+    storageBlobVOList: attachments,
+    attachmentList: attachments,
+    invoiceAttachments: attachments,
+    approvalRecords,
+    records: tasks.length ? tasks : source.records,
+    approvalFlowNodes,
+    currentNodeIndex,
+    rejectReason,
+    flowNodes: tasks.length ? mapTasksToFlowNodes(tasks) : mapped.flowNodes,
+  };
+}
diff --git a/src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementMappers.js b/src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementMappers.js
new file mode 100644
index 0000000..7a82873
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementMappers.js
@@ -0,0 +1,623 @@
+import dayjs from "dayjs";
+import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
+import { mapSignModeToApi } from "../../ApproveManage/approve-template/approveTemplateConstants.js";
+import { EXPENSE_CATEGORY_OPTIONS } from "../cost-reimburse/costReimburseUtils.js";
+import { EXPENSE_SUBJECT_OPTIONS } from "../travel-reimburse/travelReimburseUtils.js";
+import { applyFinReimbursementDetailEnrichment } from "./finReimbursementDetailExtras.js";
+
+/** 鎶ラ攢绫诲瀷锛�1-宸梾鎶ラ攢锛�2-璐圭敤鎶ラ攢 */
+export const FIN_REIMBURSEMENT_TYPE = {
+  TRAVEL: "1",
+  COST: "2",
+};
+
+const REIMBURSEMENT_TYPE_LABEL = {
+  [FIN_REIMBURSEMENT_TYPE.TRAVEL]: "宸梾鎶ラ攢",
+  [FIN_REIMBURSEMENT_TYPE.COST]: "璐圭敤鎶ラ攢",
+};
+
+/** 褰掍竴鍖栨姤閿�绫诲瀷锛�1-宸梾锛�2-璐圭敤 */
+export function normalizeReimbursementType(val) {
+  const s = String(val ?? "").trim();
+  if (s === "1" || s === FIN_REIMBURSEMENT_TYPE.TRAVEL) {
+    return FIN_REIMBURSEMENT_TYPE.TRAVEL;
+  }
+  if (s === "2" || s === FIN_REIMBURSEMENT_TYPE.COST) {
+    return FIN_REIMBURSEMENT_TYPE.COST;
+  }
+  return "";
+}
+
+export function reimbursementTypeLabel(type) {
+  return REIMBURSEMENT_TYPE_LABEL[normalizeReimbursementType(type)] || "鈥�";
+}
+
+export function getModuleKeyByReimbursementType(type) {
+  const t = normalizeReimbursementType(type);
+  if (t === FIN_REIMBURSEMENT_TYPE.TRAVEL) {
+    return APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE;
+  }
+  if (t === FIN_REIMBURSEMENT_TYPE.COST) {
+    return APPROVAL_MODULE_KEYS.COST_REIMBURSE;
+  }
+  return "";
+}
+
+/** 浼樺厛鎺ュ彛 reimbursementType锛屽叾娆¢〉闈� moduleKey / 鍏ュ弬 */
+export function resolveReimbursementType(raw, fallback) {
+  const fromApi = normalizeReimbursementType(raw?.reimbursementType);
+  if (fromApi) return fromApi;
+  return (
+    normalizeReimbursementType(fallback) ||
+    getReimbursementTypeByModuleKey(fallback) ||
+    ""
+  );
+}
+
+export function isTravelReimbursementType(type) {
+  return (
+    resolveReimbursementType({ reimbursementType: type }, type) ===
+    FIN_REIMBURSEMENT_TYPE.TRAVEL
+  );
+}
+
+export function filterRowsByReimbursementType(rows, expectedType) {
+  const expected = normalizeReimbursementType(expectedType);
+  if (!expected) return rows || [];
+  return (rows || []).filter((row) => {
+    const t = resolveReimbursementType(row, expected);
+    return t === expected;
+  });
+}
+
+export function getReimbursementTypeByModuleKey(moduleKey) {
+  if (moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE) {
+    return FIN_REIMBURSEMENT_TYPE.TRAVEL;
+  }
+  if (moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE) {
+    return FIN_REIMBURSEMENT_TYPE.COST;
+  }
+  return "";
+}
+
+export function unwrapFinReimbursementPage(res) {
+  const data = res?.data ?? res;
+  if (!data || typeof data !== "object") {
+    return { records: [], total: 0 };
+  }
+  if (Array.isArray(data.records)) {
+    return { records: data.records, total: Number(data.total ?? 0) };
+  }
+  const nested = data.data;
+  if (nested && typeof nested === "object" && Array.isArray(nested.records)) {
+    return { records: nested.records, total: Number(nested.total ?? 0) };
+  }
+  return { records: [], total: 0 };
+}
+
+/** 璇︽儏鎺ュ彛 data 瑙e寘 */
+export function unwrapFinReimbursementDetail(res) {
+  const data = res?.data ?? res;
+  if (!data || typeof data !== "object") return {};
+  if (data.billNo != null || data.id != null || data.reimbursementType != null) {
+    return data;
+  }
+  const nested = data.data;
+  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
+    return nested;
+  }
+  if (data.finReimbursementDto && typeof data.finReimbursementDto === "object") {
+    return data.finReimbursementDto;
+  }
+  return data;
+}
+
+/** 璇︽儏鏌ヨ鍙傛暟锛坬uery finReimbursementDto锛� */
+export function buildFinReimbursementDetailParams(id) {
+  const raw = id?.id != null ? id.id : id;
+  const n = toNumber(raw);
+  return { finReimbursementDto: { id: n != null ? n : raw } };
+}
+
+/** 璇︽儏 DTO 鈫� 椤甸潰琛岋紙鎸� reimbursementType 鏄犲皠锛屽惈 tasks / storageBlobVOList锛� */
+export function mapFinReimbursementDetailRow(raw, reimbursementTypeOrModuleKey) {
+  const type = resolveReimbursementType(raw, reimbursementTypeOrModuleKey);
+  let mapped = {};
+  if (type === FIN_REIMBURSEMENT_TYPE.TRAVEL) {
+    mapped = mapTravelReimbursementRow(raw);
+  } else if (type === FIN_REIMBURSEMENT_TYPE.COST) {
+    mapped = mapCostReimbursementRow(raw);
+  } else {
+    mapped = raw || {};
+  }
+  return {
+    ...applyFinReimbursementDetailEnrichment(mapped, raw),
+    reimbursementType: type,
+    reimbursementTypeLabel: reimbursementTypeLabel(type),
+    moduleKey: getModuleKeyByReimbursementType(type),
+  };
+}
+
+/** 鍗曟嵁鐘舵�� 鈫� 椤甸潰 approvalResult锛堝吋瀹� statusLabel锛� */
+export function mapBillStatusToApprovalResult(billStatus) {
+  const upper = String(billStatus ?? "").trim().toUpperCase();
+  if (upper === "DRAFT") return "draft";
+  if (upper === "IN_APPROVAL") return "pending";
+  if (upper === "APPROVED") return "approved";
+  if (upper === "REJECTED") return "rejected";
+  if (upper === "WITHDRAWN") return "cancelled";
+  if (upper === "PAID") return "paid";
+  return "pending";
+}
+
+function pickApplicantQuery(searchForm = {}) {
+  const kw = (searchForm.applicantKeyword || "").trim();
+  if (!kw) return {};
+  if (/[\u4e00-\u9fa5]/.test(kw)) return { applicantName: kw };
+  return { applicantCode: kw };
+}
+
+/** 缁勮 listPage 鏌ヨ鍙傛暟锛坧age + finReimbursementDto锛� */
+export function buildFinReimbursementListParams({
+  page,
+  searchForm,
+  reimbursementType,
+  extraDto = {},
+}) {
+  const dto = {
+    reimbursementType,
+    ...pickApplicantQuery(searchForm),
+    ...(extraDto && typeof extraDto === "object" ? extraDto : {}),
+  };
+
+  if (searchForm?.billStatus) {
+    dto.billStatus = searchForm.billStatus;
+  }
+
+  const range =
+    searchForm?.createTimeRange ??
+    searchForm?.applyDateRange ??
+    (searchForm?.applyTimeFrom || searchForm?.applyTimeTo
+      ? [searchForm.applyTimeFrom, searchForm.applyTimeTo]
+      : null);
+
+  if (Array.isArray(range) && range[0]) {
+    dto.createTimeStart = range[0];
+  }
+  if (Array.isArray(range) && range[1]) {
+    dto.createTimeEnd = range[1];
+  }
+
+  if (reimbursementType === FIN_REIMBURSEMENT_TYPE.TRAVEL) {
+    if (searchForm?.travelStartFrom) {
+      dto.startTimeStart = searchForm.travelStartFrom;
+    }
+    if (searchForm?.travelEndTo) {
+      dto.endTimeEnd = searchForm.travelEndTo;
+    }
+  }
+
+  return {
+    page: {
+      current: page.current,
+      size: page.size,
+    },
+    finReimbursementDto: dto,
+  };
+}
+
+function pickTravelField(obj, keys) {
+  if (!obj || typeof obj !== "object") return "";
+  for (const key of keys) {
+    const v = obj[key];
+    if (v != null && v !== "") return v;
+  }
+  return "";
+}
+
+/** 鍏煎 list/detail 澶氱宸梾瀛愬璞$粨鏋� */
+export function pickTravelFromRow(row) {
+  if (!row || typeof row !== "object") return {};
+  const nested =
+    (row.travel && typeof row.travel === "object" ? row.travel : null) ||
+    row.finReimbursementTravel ||
+    row.finReimbursementTravelDto ||
+    row.travelDto ||
+    row.travelVO ||
+    {};
+  const src =
+    nested && typeof nested === "object" && Object.keys(nested).length
+      ? nested
+      : row;
+  return {
+    startTime: pickTravelField(src, [
+      "startTime",
+      "travelStartTime",
+      "startDate",
+      "travelStartDate",
+      "departureTime",
+    ]),
+    endTime: pickTravelField(src, [
+      "endTime",
+      "travelEndTime",
+      "endDate",
+      "travelEndDate",
+      "returnTime",
+    ]),
+    travelDays: src.travelDays,
+    departureCity: pickTravelField(src, [
+      "departureCity",
+      "departurePlace",
+      "departure",
+    ]),
+    destinationCity: pickTravelField(src, [
+      "destinationCity",
+      "destination",
+      "destinationPlace",
+    ]),
+    hotelStandard: src.hotelStandard,
+    lodgingDays: src.lodgingDays ?? src.hotelDays,
+    mealAllowance: src.mealAllowance ?? src.livingSubsidy,
+    transportAllowance: src.transportAllowance ?? src.transportSubsidy,
+    lodgingLimit: src.lodgingLimit,
+    withinStandard: src.withinStandard,
+    standardTag: src.standardTag || "",
+    id: src.id,
+    reimbursementId: src.reimbursementId,
+  };
+}
+
+/** 鍒楄〃/璇︽儏鏃堕棿灞曠ず锛圛SO 鈫� YYYY-MM-DD HH:mm:ss锛� */
+export function formatReimbursementDateTime(val) {
+  if (val == null || val === "") return "";
+  const d = dayjs(val);
+  if (!d.isValid()) return String(val);
+  const raw = String(val);
+  const hasTime = raw.includes("T") || /:\d{2}/.test(raw);
+  return hasTime ? d.format("YYYY-MM-DD HH:mm:ss") : d.format("YYYY-MM-DD");
+}
+
+/** 鎺ュ彛琛� 鈫� 宸梾鎶ラ攢鍒楄〃琛岋紙鍏煎 useTravelReimburse 瀛楁锛� */
+export function mapTravelReimbursementRow(row) {
+  if (!row) return {};
+  const travel = pickTravelFromRow(row);
+  const details = Array.isArray(row.details) ? row.details : [];
+
+  const base = {
+    ...row,
+    id: row.id,
+    reimbursementId: row.id,
+    approvalInstanceId: row.approvalInstanceId,
+    reimburseNo: row.billNo || "",
+    applicantId: row.applicantId,
+    applicantNo: row.applicantCode || "",
+    applicantName: row.applicantName || "",
+    employeeNo: row.applicantCode || "",
+    employeeName: row.applicantName || "",
+    applicantDeptName: row.applicantDeptName || "",
+    reimburseReason: row.reason || "",
+    travelStartTime: formatReimbursementDateTime(travel.startTime),
+    travelEndTime: formatReimbursementDateTime(travel.endTime),
+    travelDays: travel.travelDays,
+    departurePlace: travel.departureCity || "",
+    destination: travel.destinationCity || "",
+    hotelStandard: travel.hotelStandard,
+    hotelDays: travel.lodgingDays,
+    livingSubsidy: travel.mealAllowance,
+    transportSubsidy: travel.transportAllowance,
+    lodgingLimit: travel.lodgingLimit,
+    needSpecialApproval: travel.withinStandard === "0" || travel.withinStandard === 0,
+    standardTag: travel.standardTag || "",
+    applyAmount: row.applyAmount,
+    payee: row.payeeName || "",
+    payeeAccount: row.payeeAccount || "",
+    payeeBank: row.payeeBank || "",
+    billStatus: row.billStatus,
+    approvalResult: mapBillStatusToApprovalResult(row.billStatus),
+    createTime: formatReimbursementDateTime(row.createTime),
+    expenseDetails: details.map((d) => ({
+      ...d,
+      expenseSubject: d.expenseCategory,
+    })),
+    travel:
+      row.travel && typeof row.travel === "object" && Object.keys(row.travel).length
+        ? row.travel
+        : travel,
+    details,
+    nodes: row.nodes || [],
+    approvalFlowNodes: mapNodesToFormFlow(row.nodes),
+    tasks: row.tasks || [],
+  };
+  return base;
+}
+
+/** 鎺ュ彛琛� 鈫� 璐圭敤鎶ラ攢鍒楄〃琛岋紙鍏煎 useCostReimburse 瀛楁锛� */
+export function mapCostReimbursementRow(row) {
+  if (!row) return {};
+  const details = Array.isArray(row.details) ? row.details : [];
+
+  return {
+    ...row,
+    id: row.id,
+    reimbursementId: row.id,
+    approvalInstanceId: row.approvalInstanceId,
+    reimburseNo: row.billNo || "",
+    applicantId: row.applicantId,
+    applicantNo: row.applicantCode || "",
+    applicantName: row.applicantName || "",
+    employeeNo: row.applicantCode || "",
+    employeeName: row.applicantName || "",
+    applicantDeptName: row.applicantDeptName || "",
+    reimburseReason: row.reason || "",
+    expenseCategory: row.expenseType || "",
+    applyAmount: row.applyAmount,
+    applyTime: row.createTime || "",
+    payee: row.payeeName || "",
+    payeeAccount: row.payeeAccount || "",
+    bankBranch: row.payeeBank || "",
+    billStatus: row.billStatus,
+    approvalResult: mapBillStatusToApprovalResult(row.billStatus),
+    createTime: formatReimbursementDateTime(row.createTime),
+    expenseDetails: details.map((d) => ({
+      ...d,
+      expenseSubject: d.expenseCategory,
+    })),
+    details,
+    nodes: row.nodes || [],
+    approvalFlowNodes: mapNodesToFormFlow(row.nodes),
+    tasks: row.tasks || [],
+  };
+}
+
+function toNumber(val) {
+  if (val == null || val === "") return undefined;
+  const n = Number(val);
+  return Number.isNaN(n) ? undefined : n;
+}
+
+function expenseSubjectToCategory(subject) {
+  const hit = EXPENSE_SUBJECT_OPTIONS.find((x) => x.value === subject);
+  return hit?.label || subject || "";
+}
+
+function expenseCategoryToType(category) {
+  const hit = EXPENSE_CATEGORY_OPTIONS.find((x) => x.value === category);
+  return hit?.label || category || "";
+}
+
+/** 鎺ュ彛 nodes 鈫� 椤甸潰瀹℃壒娴侊紙鍗曞鎵逛汉鑺傜偣锛� */
+export function mapNodesToFormFlow(nodes = []) {
+  return (Array.isArray(nodes) ? nodes : []).map((n, i) => {
+    const first = Array.isArray(n.approvers) ? n.approvers[0] : null;
+    return {
+      ...n,
+      nodeOrder: n.levelNo ?? n.nodeOrder ?? i + 1,
+      signMode: String(n.approveType || "").toUpperCase() === "OR" ? "or_sign" : "countersign",
+      approverId: first?.approverId ?? n.approverId ?? null,
+      approverName: first?.approverName ?? n.approverName ?? "",
+    };
+  });
+}
+
+/** 椤甸潰瀹℃壒鑺傜偣 鈫� 鎺ュ彛 nodes */
+export function mapApprovalFlowNodesToApi(nodes = [], templateId) {
+  const list = Array.isArray(nodes) ? nodes : [];
+  return list
+    .map((n, i) => {
+      let approvers = [];
+      if (Array.isArray(n.approvers) && n.approvers.length) {
+        approvers = n.approvers
+          .filter((a) => a?.approverId != null && a.approverId !== "")
+          .map((a, idx) => ({
+            id: a.id,
+            nodeId: a.nodeId,
+            templateId: a.templateId ?? templateId,
+            approverId: toNumber(a.approverId) ?? a.approverId,
+            approverName: a.approverName || "",
+            sortNo: a.sortNo ?? idx + 1,
+          }));
+      } else if (n.approverId != null && n.approverId !== "") {
+        approvers = [
+          {
+            approverId: toNumber(n.approverId) ?? n.approverId,
+            approverName: n.approverName || "",
+            sortNo: 1,
+          },
+        ];
+      }
+      if (!approvers.length) return null;
+
+      const node = {
+        levelNo: n.levelNo ?? n.nodeOrder ?? i + 1,
+        approveType: n.approveType || mapSignModeToApi(n.signMode),
+        approvers,
+      };
+      if (n.id != null) node.id = n.id;
+      if (n.templateId != null) node.templateId = n.templateId;
+      else if (templateId != null) node.templateId = templateId;
+      return node;
+    })
+    .filter(Boolean);
+}
+
+function mapDetailsToApi(details = []) {
+  return (details || []).map((d, i) => {
+    const item = {
+      rowNo: d.rowNo ?? i + 1,
+      invoiceDate: d.invoiceDate || undefined,
+      expenseCategory: expenseSubjectToCategory(d.expenseSubject ?? d.expenseCategory),
+      amount: toNumber(d.amount),
+      description: d.description || "",
+      invoiceNo: d.invoiceNo || undefined,
+      invoiceType: d.invoiceType || undefined,
+      invoiceAmount: toNumber(d.invoiceAmount),
+      taxRate: toNumber(d.taxRate),
+      taxAmount: toNumber(d.taxAmount),
+      remark: d.remark || undefined,
+    };
+    if (d.id != null && !String(d.id).startsWith("ed_")) {
+      item.id = toNumber(d.id) ?? d.id;
+    }
+    if (d.reimbursementId != null) item.reimbursementId = toNumber(d.reimbursementId);
+    return item;
+  });
+}
+
+function sumDetailAmount(details = []) {
+  const sum = (details || []).reduce((s, d) => s + (Number(d.amount) || 0), 0);
+  return Math.round(sum * 100) / 100;
+}
+
+/** 淇敼鏃惰ˉ榻愪富琛ㄤ笌瀛愯〃鍏宠仈 ID */
+function applyReimbursementRelations(dto) {
+  const rid = dto?.id;
+  if (rid == null) return dto;
+  if (dto.travel && typeof dto.travel === "object") {
+    dto.travel.reimbursementId = rid;
+  }
+  if (Array.isArray(dto.details)) {
+    dto.details.forEach((d) => {
+      d.reimbursementId = rid;
+    });
+  }
+  return dto;
+}
+
+function resolveReimbursementId(form) {
+  const rawId = form?.reimbursementId ?? form?.id;
+  if (rawId == null || rawId === "" || String(rawId).startsWith("local_")) {
+    return undefined;
+  }
+  return toNumber(rawId) ?? rawId;
+}
+
+/** 宸梾琛ㄥ崟 鈫� FinReimbursementDto */
+export function buildTravelReimbursementSaveDto(form, { computeTravelDays } = {}) {
+  const details = mapDetailsToApi(form.expenseDetails);
+  const detailTotal = sumDetailAmount(form.expenseDetails);
+  const applyAmount = toNumber(form.applyAmount) ?? detailTotal;
+  const travelDays =
+    form.travelDays != null
+      ? toNumber(form.travelDays)
+      : computeTravelDays?.(form.travelStartTime, form.travelEndTime);
+
+  const dto = {
+    reimbursementType: FIN_REIMBURSEMENT_TYPE.TRAVEL,
+    expenseType: "宸梾璐�",
+    applicantId: toNumber(form.applicantId),
+    applicantCode: form.employeeNo || form.applicantNo || "",
+    applicantName: form.employeeName || form.applicantName || "",
+    applicantDeptId: toNumber(form.applicantDeptId),
+    applicantDeptName: form.applicantDeptName || form.deptName || "",
+    reason: (form.reimburseReason || "").trim(),
+    applyAmount,
+    detailTotalAmount: detailTotal,
+    payeeName: form.payee || "",
+    payeeAccount: form.payeeAccount || undefined,
+    payeeBank: form.payeeBank || undefined,
+    billStatus: "IN_APPROVAL",
+    deptId: toNumber(form.deptId),
+    travel: {
+      startTime: form.travelStartTime || undefined,
+      endTime: form.travelEndTime || undefined,
+      travelDays,
+      departureCity: form.departurePlace || "",
+      destinationCity: form.destination || "",
+      hotelStandard: toNumber(form.hotelStandard),
+      lodgingDays: toNumber(form.hotelDays),
+      mealAllowance: toNumber(form.livingSubsidy),
+      transportAllowance: toNumber(form.transportSubsidy),
+      lodgingLimit: toNumber(form.lodgingLimit),
+      standardTag: form.standardTag || (form.needSpecialApproval ? "瓒呮爣鐗规壒" : "鍦ㄦ爣鍑嗚寖鍥村唴"),
+      withinStandard: form.needSpecialApproval ? "0" : "1",
+    },
+    details,
+    nodes: mapApprovalFlowNodesToApi(form.approvalFlowNodes, form.templateId),
+  };
+
+  const id = resolveReimbursementId(form);
+  if (id != null) dto.id = id;
+  if (form.billNo || form.reimburseNo) {
+    dto.billNo = form.billNo || form.reimburseNo;
+  }
+  if (form.approvalInstanceId != null) {
+    dto.approvalInstanceId = toNumber(form.approvalInstanceId);
+  }
+  if (form.approveProcessId != null) {
+    dto.approveProcessId = toNumber(form.approveProcessId);
+  }
+  if (form.travel?.id != null) dto.travel.id = toNumber(form.travel.id);
+
+  return applyReimbursementRelations(dto);
+}
+
+/** 璐圭敤琛ㄥ崟 鈫� FinReimbursementDto */
+export function buildCostReimbursementSaveDto(form) {
+  const details = mapDetailsToApi(form.expenseDetails);
+  const detailTotal = sumDetailAmount(form.expenseDetails);
+  const applyAmount = toNumber(form.applyAmount) ?? detailTotal;
+
+  const dto = {
+    reimbursementType: FIN_REIMBURSEMENT_TYPE.COST,
+    expenseType: expenseCategoryToType(form.expenseCategory),
+    applicantId: toNumber(form.applicantId),
+    applicantCode: form.employeeNo || form.applicantNo || "",
+    applicantName: form.employeeName || form.applicantName || "",
+    applicantDeptId: toNumber(form.applicantDeptId),
+    applicantDeptName: form.applicantDeptName || form.deptName || "",
+    reason: (form.reimburseReason || "").trim(),
+    applyAmount,
+    detailTotalAmount: detailTotal,
+    payeeName: form.payee || "",
+    payeeAccount: form.payeeAccount || "",
+    payeeBank: form.bankBranch || form.payeeBank || "",
+    billStatus: "IN_APPROVAL",
+    deptId: toNumber(form.deptId),
+    details,
+    nodes: mapApprovalFlowNodesToApi(form.approvalFlowNodes, form.templateId),
+  };
+
+  const id = resolveReimbursementId(form);
+  if (id != null) dto.id = id;
+  if (form.billNo || form.reimburseNo) {
+    dto.billNo = form.billNo || form.reimburseNo;
+  }
+  if (form.approvalInstanceId != null) {
+    dto.approvalInstanceId = toNumber(form.approvalInstanceId);
+  }
+  if (form.approveProcessId != null) {
+    dto.approveProcessId = toNumber(form.approveProcessId);
+  }
+
+  return applyReimbursementRelations(dto);
+}
+
+/** 鍒楄〃琛屼富閿紙鍒犻櫎/淇敼鐢� fin_reimbursement.id锛� */
+export function resolveReimbursementDeleteId(row) {
+  const raw = row?.reimbursementId ?? row?.id;
+  if (raw == null || raw === "" || String(raw).startsWith("local_")) {
+    return undefined;
+  }
+  const n = toNumber(raw);
+  return n != null ? n : raw;
+}
+
+/** 鏄惁鍏佽鍒犻櫎锛堝鎵逛腑銆佸凡閫氳繃銆佸凡浠樻涓嶅彲鍒狅級 */
+export function canDeleteReimbursementRow(row) {
+  const key = mapBillStatusToApprovalResult(
+    row?.billStatus ?? row?.approvalResult ?? row?.status
+  );
+  return key !== "pending" && key !== "approved" && key !== "paid";
+}
+
+/** 鏄惁鍏佽缂栬緫锛堜笌鍒犻櫎瑙勫垯涓�鑷达級 */
+export function canEditReimbursementRow(row) {
+  return canDeleteReimbursementRow(row);
+}
+
+/** 淇敼鍦烘櫙蹇呴』甯︿富閿� ID */
+export function validateReimbursementPersistDto(dto, isEdit) {
+  if (!isEdit) return { ok: true };
+  if (dto?.id != null && dto.id !== "") return { ok: true };
+  return { ok: false, message: "鏃犳硶淇敼锛氱己灏戞姤閿�鍗� ID" };
+}
diff --git a/src/views/officeProcessAutomation/ReimburseManage/shared/reimburseApproveBridge.js b/src/views/officeProcessAutomation/ReimburseManage/shared/reimburseApproveBridge.js
new file mode 100644
index 0000000..664d646
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/shared/reimburseApproveBridge.js
@@ -0,0 +1,124 @@
+import { getFinReimbursementDetail } from "@/api/officeProcessAutomation/finReimbursement.js";
+import { matchBusinessTypeValue } from "../../ApproveManage/approve-list/approveListConstants.js";
+import {
+  APPROVAL_MODULE_KEYS,
+  getApprovalModuleConfig,
+} from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
+import {
+  getModuleKeyByReimbursementType,
+  mapFinReimbursementDetailRow,
+  resolveReimbursementType,
+  unwrapFinReimbursementDetail,
+} from "./finReimbursementMappers.js";
+
+export const REIMBURSE_EDIT_FROM_APPROVE_KEY = "oa_reimburse_edit_from_approve";
+
+const REIMBURSE_MODULE_KEYS = [
+  APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE,
+  APPROVAL_MODULE_KEYS.COST_REIMBURSE,
+];
+
+/** 瀹℃壒瀹炰緥鏄惁宸梾/璐圭敤鎶ラ攢 */
+export function inferReimburseModuleKeyFromInstance(row) {
+  if (!row) return "";
+  for (const moduleKey of REIMBURSE_MODULE_KEYS) {
+    const cfg = getApprovalModuleConfig(moduleKey);
+    if (!cfg) continue;
+    if (
+      cfg.businessType != null &&
+      cfg.businessType !== "" &&
+      matchBusinessTypeValue(row.businessType, cfg.businessType)
+    ) {
+      return moduleKey;
+    }
+    if (matchBusinessTypeValue(row.businessType, cfg.approvalType)) {
+      return moduleKey;
+    }
+    const text = `${row.templateName || ""}${row.title || ""}${row.businessName || ""}`;
+    if ((cfg.typeLabels || []).some((l) => l && text.includes(l))) {
+      return moduleKey;
+    }
+  }
+  return "";
+}
+
+export function isReimburseApprovalInstance(row) {
+  return Boolean(inferReimburseModuleKeyFromInstance(row));
+}
+
+/** 瀹℃壒瀹炰緥鍏宠仈鐨� fin_reimbursement.id */
+export function resolveFinReimbursementIdFromInstance(row) {
+  const raw = row?.businessId ?? row?.formPayload?.reimbursementId;
+  if (raw == null || raw === "") return undefined;
+  const n = Number(raw);
+  return Number.isNaN(n) ? raw : n;
+}
+
+/** 鎷夊彇鎶ラ攢璇︽儏骞舵槧灏勪负宸梾/璐圭敤椤甸潰琛岋紙浠ユ帴鍙� reimbursementType 涓哄噯锛� */
+export async function loadReimburseDetailForInstance(instanceRow, moduleKey) {
+  const mk = moduleKey || inferReimburseModuleKeyFromInstance(instanceRow);
+  const id = resolveFinReimbursementIdFromInstance(instanceRow);
+  if (id == null) {
+    throw new Error("missing reimbursement id");
+  }
+  const res = await getFinReimbursementDetail(id);
+  const raw = unwrapFinReimbursementDetail(res);
+  const reimburseRow = mapFinReimbursementDetailRow(raw, mk);
+  const reimbursementType = resolveReimbursementType(raw, mk);
+  const resolvedMk =
+    getModuleKeyByReimbursementType(reimbursementType) || mk;
+  return {
+    reimburseRow,
+    instanceRow,
+    moduleKey: resolvedMk,
+    reimbursementType,
+  };
+}
+
+export function stashReimburseEditFromApprove(moduleKey, reimbursementId) {
+  sessionStorage.setItem(
+    REIMBURSE_EDIT_FROM_APPROVE_KEY,
+    JSON.stringify({ moduleKey, reimbursementId })
+  );
+}
+
+export function consumeReimburseEditFromApprove() {
+  const raw = sessionStorage.getItem(REIMBURSE_EDIT_FROM_APPROVE_KEY);
+  if (!raw) return null;
+  sessionStorage.removeItem(REIMBURSE_EDIT_FROM_APPROVE_KEY);
+  try {
+    return JSON.parse(raw);
+  } catch {
+    return null;
+  }
+}
+
+/** 浠庡凡娉ㄥ唽璺敱瑙f瀽宸梾/璐圭敤鎶ラ攢鑿滃崟 path锛堥伩鍏嶅啓姝� path 瀵艰嚧 404锛� */
+export function resolveReimburseManageRoutePath(router, moduleKey) {
+  if (!router?.getRoutes) return "";
+  const needle =
+    moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE
+      ? "travel-reimburse"
+      : moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE
+        ? "cost-reimburse"
+        : "";
+  if (!needle) return "";
+  const labelHint =
+    moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE ? "宸梾" : "璐圭敤";
+  const hit = router.getRoutes().find((r) => {
+    const path = r.path || "";
+    if (path.includes(needle)) return true;
+    const title = r.meta?.title || "";
+    return title.includes(labelHint) && title.includes("鎶ラ攢");
+  });
+  return hit?.path || "";
+}
+
+export async function navigateToReimburseManageForEdit(router, moduleKey, reimbursementId) {
+  stashReimburseEditFromApprove(moduleKey, reimbursementId);
+  const path = resolveReimburseManageRoutePath(router, moduleKey);
+  if (!path) {
+    throw new Error("route not found");
+  }
+  await router.push(path);
+}
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue
index d09e580..2c1d8a4 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue
@@ -58,9 +58,10 @@
 });
 
 const attachmentFiles = computed(() => {
-  const list = props.row?.attachmentList?.length
-    ? props.row.attachmentList
-    : props.row?.invoiceAttachments;
+  const list =
+    props.row?.attachmentList ||
+    props.row?.storageBlobVOList ||
+    props.row?.invoiceAttachments;
   return Array.isArray(list) ? list : [];
 });
 
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
index 2e81e18..9318231 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
@@ -1,4 +1,4 @@
-<!--OA妯″潡锛氬樊鏃呮姤閿�-->
+<!--OA妯″潡锛氬樊鏃呮姤閿�锛堝垪琛� /finReimbursement/listPage锛宺eimbursementType=1锛�-->
 <template>
   <div class="app-container">
     <div class="search_form mb20">
@@ -369,13 +369,21 @@
         </el-card>
       </el-form>
       <template #footer>
-        <el-button v-if="!formDialog.readonly" type="primary" @click="submitForm">鎻� 浜�</el-button>
+        <el-button
+          v-if="!formDialog.readonly"
+          type="primary"
+          :loading="submitSaving"
+          @click="submitForm"
+        >
+          鎻� 浜�
+        </el-button>
         <el-button @click="formDialog.visible = false">{{ formDialog.readonly ? "鍏� 闂�" : "鍙� 娑�" }}</el-button>
       </template>
     </el-dialog>
 
     <!-- 璇︽儏 -->
     <el-dialog v-model="detailDialog.visible" title="宸梾鎶ラ攢璇︽儏" width="900px" append-to-body destroy-on-close>
+      <div v-loading="detailLoading">
       <DetailPanel :row="detailRow" />
       <ApprovalFlowProgress
         class="mt16"
@@ -394,6 +402,7 @@
         </el-timeline-item>
       </el-timeline>
       <el-empty v-else description="鏆傛棤瀹℃壒璁板綍" :image-size="60" />
+      </div>
       <template #footer>
         <el-button type="primary" @click="detailDialog.visible = false">鍏� 闂�</el-button>
       </template>
@@ -458,6 +467,7 @@
   formDialog,
   formRules,
   detailDialog,
+  detailLoading,
   detailRow,
   approveDialog,
   approveOpinion,
@@ -488,6 +498,7 @@
   openFormDialog,
   onFormClosed,
   submitForm,
+  submitSaving,
   openDetail,
   approvalActionLabel,
   submitApprove,
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/travelReimburseUtils.js b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/travelReimburseUtils.js
index 2505ce3..6c94c61 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/travelReimburseUtils.js
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/travelReimburseUtils.js
@@ -15,14 +15,19 @@
 }
 
 export function statusLabel(v) {
+  if (v === "draft") return "鑽夌";
   if (v === "approved") return "閫氳繃";
+  if (v === "paid") return "宸蹭粯娆�";
   if (v === "rejected") return "椹冲洖";
+  if (v === "cancelled") return "宸叉挙鍥�";
   return "瀹℃牳涓�";
 }
 
 export function statusTagType(v) {
-  if (v === "approved") return "success";
+  if (v === "draft") return "info";
+  if (v === "approved" || v === "paid") return "success";
   if (v === "rejected") return "danger";
+  if (v === "cancelled") return "info";
   return "warning";
 }
 
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