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