From 1160de5142cd2bc08ebc61c247a4857f0c4ab7f1 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期五, 22 五月 2026 10:45:11 +0800
Subject: [PATCH] 特色功能:丰富报销清单并添加审批流程详情——新增功能以丰富报销清单行内容,为费用和差旅报销提供审批流程详情。——引入新的实用函数来处理审批流程节点和汇总信息。——更新组件以利用丰富后的审批流程数据,从而更好地展示审批进度。
---
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js | 374 ++++++++++++++++++++++++++--------------------------
1 files changed, 187 insertions(+), 187 deletions(-)
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js
index 9125d64..4726117 100644
--- a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js
@@ -1,7 +1,31 @@
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,
+ enrichReimbursementListRowsWithApprovalFlow,
+ filterRowsByReimbursementType,
+ FIN_REIMBURSEMENT_TYPE,
+ mapFinReimbursementDetailRow,
+ mapTravelReimbursementRow,
+ resolveReimbursementDeleteId,
+ unwrapFinReimbursementDetail,
+ unwrapFinReimbursementPage,
+ validateReimbursementApprovalNodes,
+ validateReimbursementPersistDto,
+} from "../shared/finReimbursementMappers.js";
+import { consumeReimburseEditFromApprove } from "../shared/reimburseApproveBridge.js";
import {
EXPENSE_SUBJECT_OPTIONS,
expenseSubjectLabel,
@@ -32,92 +56,10 @@
return String(u.status) === "0";
}
-function demoFlowNodes(names = ["閮ㄩ棬涓荤", "璐㈠姟瀹℃牳"]) {
- return names.map((name, i) => ({
- approverId: `mock_${i + 1}`,
- approverName: name,
- sortOrder: i + 1,
- nodeOrder: i + 1,
- nodeStatus: i === 0 ? "process" : "wait",
- approveOpinion: "",
- approveTime: "",
- }));
-}
-
export function useTravelReimburse() {
const { proxy } = getCurrentInstance();
- const allRows = ref([
- {
- id: "1",
- reimburseNo: "TR202605090001",
- applicantId: "mock_1",
- employeeNo: "zhangsan",
- employeeName: "寮犱笁",
- applicantNo: "zhangsan",
- applicantName: "寮犱笁",
- reimburseReason: "璧翠笂娴峰弬鍔犺涓氬睍浼氬強瀹㈡埛鎷滆銆�",
- travelStartTime: "2026-05-10 08:00:00",
- travelEndTime: "2026-05-13 18:00:00",
- travelDays: 4,
- departurePlace: "鏉窞",
- destination: "涓婃捣",
- hotelStandard: 600,
- hotelDays: 3,
- livingSubsidy: 400,
- applyAmount: 4580,
- payee: "寮犱笁",
- expenseDetails: [
- { id: "d1", invoiceDate: "2026-05-10", expenseSubject: "transport", amount: 553, description: "楂橀搧寰�杩�" },
- { id: "d2", invoiceDate: "2026-05-11", expenseSubject: "hotel", amount: 1680, description: "閰掑簵浣忓" },
- ],
- attachmentList: [{ name: "楂橀搧绁�.pdf", url: "/mock/invoice1.pdf" }],
- invoiceAttachments: [{ name: "楂橀搧绁�.pdf", url: "/mock/invoice1.pdf" }],
- approvalFlowNodes: demoFlowNodes(),
- currentNodeIndex: 0,
- approvalResult: "pending",
- rejectReason: "",
- approvalRecords: [],
- needSpecialApproval: false,
- deptId: "101",
- deptName: "閿�鍞儴",
- travelTier: "tier1",
- createTime: "2026-05-09 10:20:00",
- },
- {
- id: "2",
- reimburseNo: "TR202605080002",
- applicantId: "mock_2",
- employeeNo: "lisi",
- employeeName: "鏉庡洓",
- applicantNo: "lisi",
- applicantName: "鏉庡洓",
- reimburseReason: "鎴愰兘鍒嗗叕鍙告妧鏈敮鎸併��",
- travelStartTime: "2026-05-05 09:00:00",
- travelEndTime: "2026-05-07 17:00:00",
- travelDays: 3,
- departurePlace: "姝︽眽",
- destination: "鎴愰兘",
- hotelStandard: 450,
- hotelDays: 2,
- livingSubsidy: 240,
- applyAmount: 2100,
- payee: "鏉庡洓",
- expenseDetails: [{ id: "d3", invoiceDate: "2026-05-06", expenseSubject: "meal", amount: 180, description: "宸ヤ綔椁�" }],
- attachmentList: [],
- invoiceAttachments: [],
- approvalFlowNodes: demoFlowNodes().map((n, i) => ({ ...n, nodeStatus: "finish", approveOpinion: "鍚屾剰", approveTime: "2026-05-08 11:00:00" })),
- currentNodeIndex: 1,
- approvalResult: "approved",
- rejectReason: "",
- approvalRecords: [{ operatorName: "閮ㄩ棬涓荤", result: "approved", opinion: "鍚屾剰", time: "2026-05-08 10:00:00" }],
- needSpecialApproval: false,
- deptId: "102",
- deptName: "鎶�鏈儴",
- travelTier: "tier2",
- createTime: "2026-05-07 16:00:00",
- },
- ]);
+ const allRows = ref([]);
const searchForm = reactive({ applicantKeyword: "", travelStartFrom: "", travelEndTo: "" });
const tableLoading = ref(false);
@@ -130,43 +72,46 @@
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);
+ const filtered = filterRowsByReimbursementType(
+ records,
+ FIN_REIMBURSEMENT_TYPE.TRAVEL
+ );
+ let mapped = filtered.map(mapTravelReimbursementRow);
+ mapped = await enrichReimbursementListRowsWithApprovalFlow(
+ mapped,
+ FIN_REIMBURSEMENT_TYPE.TRAVEL
+ );
+ allRows.value = mapped;
+ const dropped = records.length - filtered.length;
+ page.total =
+ dropped > 0 ? Math.max(0, Number(total) - dropped) : Number(total);
+ } catch {
+ allRows.value = [];
+ page.total = 0;
+ proxy?.$modal?.msgError?.("宸梾鎶ラ攢鍒楄〃鍔犺浇澶辫触");
+ } finally {
+ tableLoading.value = false;
+ }
+ }
const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser));
@@ -238,11 +183,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),
+ },
],
},
]);
@@ -416,8 +371,7 @@
function handleQuery() {
page.current = 1;
- tableLoading.value = true;
- setTimeout(() => { tableLoading.value = false; }, 150);
+ return fetchList();
}
function resetSearch() {
@@ -430,11 +384,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) {
@@ -455,14 +468,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("");
@@ -496,63 +519,30 @@
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();
+ const nodeCheck = validateReimbursementApprovalNodes(dto);
+ if (!nodeCheck.ok) {
+ proxy?.$modal?.msgWarning?.(nodeCheck.message);
+ return;
+ }
+ submitSaving.value = true;
+ try {
+ await persistFinReimbursement(dto, isEdit);
+ proxy?.$modal?.msgSuccess?.(isEdit ? "淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛");
+ formDialog.visible = false;
+ await handleQuery();
+ } catch {
+ proxy?.$modal?.msgError?.(isEdit ? "淇濆瓨澶辫触" : "鎻愪氦澶辫触");
+ } finally {
+ submitSaving.value = false;
+ }
}
async function submitApprove(result) {
@@ -593,7 +583,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");
@@ -631,7 +621,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,
@@ -648,6 +645,7 @@
formDialog,
formRules,
detailDialog,
+ detailLoading,
detailRow,
approveDialog,
approveOpinion,
@@ -678,7 +676,9 @@
openFormDialog,
onFormClosed,
submitForm,
+ submitSaving,
openDetail,
+ confirmRemoveRow,
openApprove,
approvalActionLabel,
submitApprove,
--
Gitblit v1.9.3