From a9d97b150701e634bdb751eab277696abd136cca Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 16 六月 2026 14:39:47 +0800
Subject: [PATCH] 君歌app 1.依照web端功能修改
---
src/pages/oa/ReimburseManage/reimburse-form/useFinReimburseForm.js | 448 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 448 insertions(+), 0 deletions(-)
diff --git a/src/pages/oa/ReimburseManage/reimburse-form/useFinReimburseForm.js b/src/pages/oa/ReimburseManage/reimburse-form/useFinReimburseForm.js
new file mode 100644
index 0000000..489f598
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/reimburse-form/useFinReimburseForm.js
@@ -0,0 +1,448 @@
+import { computed, reactive, ref } from "vue";
+import { userListNoPageByTenantId } from "@/api/system/user";
+import useUserStore from "@/store/modules/user";
+import { persistFinReimbursement } from "@/api/oa/finReimbursement.js";
+import {
+ isActiveUser,
+ unwrapUserList,
+ userAvatarColor,
+ userSelectLabel,
+ userSubLabel,
+} from "../../_utils/userPickerUtils.js";
+import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
+import {
+ buildCostReimbursementSaveDto,
+ buildTravelReimbursementSaveDto,
+ fetchFinReimbursementFormDetail,
+ getReimbursementTypeByModuleKey,
+ validateReimbursementApprovalNodes,
+ validateReimbursementPersistDto,
+} from "../../_utils/finReimbursementMappers.js";
+import {
+ applyCategoryTemplate,
+ createEmptyCostForm,
+ EXPENSE_CATEGORY_OPTIONS,
+ EXPENSE_SUBJECT_OPTIONS as COST_SUBJECT_OPTIONS,
+ createEmptyExpenseDetail as createCostDetail,
+} from "../_utils/costReimburseUtils.js";
+import {
+ computeTravelDays,
+ createEmptyExpenseDetail,
+ createEmptyTravelForm,
+ detectTravelTier,
+ EXPENSE_SUBJECT_OPTIONS,
+ getTravelStandardByTier,
+} from "../_utils/travelReimburseUtils.js";
+
+// 寤惰繜鍒濆鍖� userStore锛岄伩鍏嶅湪妯″潡鍔犺浇鏃惰皟鐢�
+let userStore = null;
+function getUserStore() {
+ if (!userStore) {
+ userStore = useUserStore();
+ }
+ return userStore;
+}
+
+function buildOverBudgetWarnings(f, detailTotal, hotelLimit, transportLimit, mealLimit) {
+ const warnings = [];
+ const bySubject = { transport: 0, hotel: 0, meal: 0, other: 0 };
+ (f.expenseDetails || []).forEach(d => {
+ const key = d.expenseSubject || "other";
+ bySubject[key] = (bySubject[key] || 0) + (Number(d.amount) || 0);
+ });
+ if (bySubject.transport > transportLimit && transportLimit > 0) {
+ warnings.push(`浜ら�氳垂 ${bySubject.transport} 鍏冭秴鍑烘爣鍑� ${transportLimit} 鍏僠);
+ }
+ if (bySubject.hotel > hotelLimit && hotelLimit > 0) {
+ warnings.push(`浣忓璐� ${bySubject.hotel} 鍏冭秴鍑洪檺棰� ${hotelLimit} 鍏僠);
+ }
+ if (bySubject.meal > mealLimit && mealLimit > 0) {
+ warnings.push(`椁愰ギ璐� ${bySubject.meal} 鍏冭秴鍑虹敓娲昏ˉ璐村缓璁� ${mealLimit} 鍏僠);
+ }
+ const std = getTravelStandardByTier(f.travelTier);
+ if (Number(f.hotelStandard) > std.hotelPerNight) {
+ warnings.push(`閰掑簵鏍囧噯 ${f.hotelStandard} 鍏�/鏅氶珮浜�${std.label}鏍囧噯 ${std.hotelPerNight} 鍏�/鏅歚);
+ }
+ const apply = Number(f.applyAmount) || detailTotal;
+ const standardTotal = transportLimit + hotelLimit + mealLimit;
+ if (apply > standardTotal && standardTotal > 0) {
+ warnings.push(`鐢宠鎬婚 ${apply} 鍏冮珮浜庡樊鏃呮爣鍑嗗悎璁$害 ${standardTotal} 鍏僠);
+ }
+ return warnings;
+}
+
+export function useFinReimburseForm(moduleKeyRef, modeRef) {
+ const isTravel = computed(
+ () => moduleKeyRef.value === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE
+ );
+
+ const form = reactive(
+ moduleKeyRef.value === APPROVAL_MODULE_KEYS.COST_REIMBURSE
+ ? createEmptyCostForm()
+ : createEmptyTravelForm()
+ );
+
+ const submitting = ref(false);
+ const loading = ref(false);
+ const allUsersCache = ref([]);
+ const showApplicantPicker = ref(false);
+ const applicantDisplaySub = computed(() => {
+ if (!form.applicantId) return "鐐瑰嚮閫夋嫨鐢宠浜�";
+ const u = userById(form.applicantId);
+ if (u) return userSubLabel(u) || form.employeeNo || "";
+ return form.employeeNo ? `宸ュ彿 ${form.employeeNo}` : "";
+ });
+ const applicantAvatarColor = computed(() =>
+ userAvatarColor(form.employeeName || form.employeeNo || "")
+ );
+ const showCategorySheet = ref(false);
+ const showSubjectSheet = ref(false);
+ const editingDetailIndex = ref(-1);
+ const pickApplicantId = ref("");
+ const pickCategoryValue = ref("");
+ const pickSubjectValue = ref("");
+
+ const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser));
+
+ const travelDaysDisplay = computed(() => {
+ if (!isTravel.value) return "";
+ const d = computeTravelDays(form.travelStartTime, form.travelEndTime);
+ return d == null ? "" : String(d);
+ });
+
+ const travelTierLabel = computed(() => {
+ if (!isTravel.value) return "";
+ const std = getTravelStandardByTier(form.travelTier || detectTravelTier(form.destination));
+ return `鎸�${std.label}鏍囧噯`;
+ });
+
+ const suggestedLivingSubsidy = computed(() => {
+ const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || form.hotelDays || 0;
+ const std = getTravelStandardByTier(form.travelTier);
+ return Math.round(std.mealPerDay * days * 100) / 100;
+ });
+
+ const suggestedTransportSubsidy = computed(() => {
+ const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || 0;
+ const std = getTravelStandardByTier(form.travelTier);
+ return Math.round(std.transportPerDay * days * 100) / 100;
+ });
+
+ const suggestedHotelLimit = computed(() => {
+ const nights = form.hotelDays || 0;
+ const perNight = form.hotelStandard ?? getTravelStandardByTier(form.travelTier).hotelPerNight;
+ return Math.round(perNight * nights * 100) / 100;
+ });
+
+ const detailTotalAmount = computed(() => {
+ const sum = (form.expenseDetails || []).reduce((s, d) => s + (Number(d.amount) || 0), 0);
+ return Math.round(sum * 100) / 100;
+ });
+
+ const overBudgetWarnings = computed(() => {
+ if (!isTravel.value) return [];
+ return buildOverBudgetWarnings(
+ form,
+ detailTotalAmount.value,
+ suggestedHotelLimit.value,
+ suggestedTransportSubsidy.value,
+ suggestedLivingSubsidy.value
+ );
+ });
+
+ const expenseSubjectOptions = computed(() =>
+ isTravel.value ? EXPENSE_SUBJECT_OPTIONS : COST_SUBJECT_OPTIONS
+ );
+
+ const categoryActions = computed(() =>
+ EXPENSE_CATEGORY_OPTIONS.map(x => ({ name: x.label, value: x.value }))
+ );
+
+ const categoryLabel = computed(() => {
+ const hit = EXPENSE_CATEGORY_OPTIONS.find(x => x.value === form.expenseCategory);
+ return hit?.label || "璇烽�夋嫨璐圭敤绫诲瀷";
+ });
+
+ async function loadUserPool() {
+ try {
+ allUsersCache.value = unwrapUserList(await userListNoPageByTenantId());
+ } catch {
+ allUsersCache.value = [];
+ }
+ }
+
+ function userLabel(u) {
+ return userSelectLabel(u);
+ }
+
+ function userById(id) {
+ return allUsersCache.value.find(u => String(u.userId ?? u.id) === String(id));
+ }
+
+ function employeeNoFromUser(u) {
+ if (!u) return "";
+ return u.userName ?? u.userCode ?? u.jobNumber ?? u.workNo ?? (u.userId != null ? String(u.userId) : "");
+ }
+
+ function fillApplicantFromUser(u) {
+ if (!u) return;
+ form.applicantId = u.userId ?? u.id;
+ form.employeeName = u.nickName || u.userName || "";
+ form.employeeNo = employeeNoFromUser(u);
+ form.payee = form.payee || form.employeeName;
+ form.deptId = String(u.deptId ?? u.sysDeptId ?? "");
+ form.deptName = u.dept?.deptName ?? u.deptName ?? "";
+ }
+
+ function onApplicantPicked(uidOrUser) {
+ const u =
+ typeof uidOrUser === "object" && uidOrUser
+ ? uidOrUser
+ : userById(uidOrUser);
+ fillApplicantFromUser(u);
+ }
+
+ /** 鏂板鏃堕粯璁ゅ甫鍑哄綋鍓嶇櫥褰曚汉锛屽噺灏戦�変汉姝ラ */
+ function tryApplyCurrentUser() {
+ if (modeRef.value === "edit" || form.applicantId) return;
+ const store = getUserStore();
+ const id = store.id;
+ if (!id) return;
+ let u = userById(id);
+ if (!u) {
+ u = {
+ userId: id,
+ nickName: store.nickName,
+ userName: store.name,
+ };
+ }
+ fillApplicantFromUser(u);
+ }
+
+ function recalcTravelStandards() {
+ if (!isTravel.value) return;
+ form.travelTier = detectTravelTier(form.destination);
+ const std = getTravelStandardByTier(form.travelTier);
+ if (form.hotelStandard == null || form.hotelStandard === "" || form.hotelStandard === 0) {
+ form.hotelStandard = std.hotelPerNight;
+ }
+ const days = computeTravelDays(form.travelStartTime, form.travelEndTime);
+ if (days != null) {
+ form.travelDays = days;
+ if (form.hotelDays == null || form.hotelDays === "") {
+ form.hotelDays = Math.max(0, days - 1);
+ }
+ if (form.livingSubsidy == null || form.livingSubsidy === "" || form.livingSubsidy === 0) {
+ form.livingSubsidy = suggestedLivingSubsidy.value;
+ }
+ }
+ form.needSpecialApproval = overBudgetWarnings.value.length > 0;
+ }
+
+ function syncApplyAmountFromDetails() {
+ form.applyAmount = detailTotalAmount.value;
+ recalcTravelStandards();
+ }
+
+ function addExpenseDetail() {
+ const row = isTravel.value ? createEmptyExpenseDetail() : createCostDetail();
+ form.expenseDetails.push(row);
+ }
+
+ function removeExpenseDetail(index) {
+ form.expenseDetails.splice(index, 1);
+ recalcTravelStandards();
+ }
+
+ function applyTemplate(category) {
+ applyCategoryTemplate(form, category);
+ syncApplyAmountFromDetails();
+ }
+
+ function resetFormForModule() {
+ const empty = isTravel.value ? createEmptyTravelForm() : createEmptyCostForm();
+ Object.keys(form).forEach(k => delete form[k]);
+ Object.assign(form, empty);
+ if (!form.approvalFlowNodes?.length) {
+ form.approvalFlowNodes = [
+ { approverId: "", approverName: "", nodeOrder: 1, signMode: "countersign" },
+ ];
+ }
+ }
+
+ async function loadEdit(reimbursementId) {
+ loading.value = true;
+ try {
+ if (!allUsersCache.value.length) await loadUserPool();
+ const row = await fetchFinReimbursementFormDetail(
+ { reimbursementId },
+ moduleKeyRef.value
+ );
+ if (row?.moduleKey && row.moduleKey !== moduleKeyRef.value) {
+ moduleKeyRef.value = row.moduleKey;
+ }
+ Object.assign(form, JSON.parse(JSON.stringify(row)), {
+ reimbursementId: row.reimbursementId ?? row.id,
+ expenseDetails: JSON.parse(JSON.stringify(row.expenseDetails || [])),
+ approvalFlowNodes: JSON.parse(
+ JSON.stringify(
+ row.approvalFlowNodes?.length
+ ? row.approvalFlowNodes
+ : [{ approverId: "", approverName: "", nodeOrder: 1, signMode: "countersign" }]
+ )
+ ),
+ attachmentList: JSON.parse(JSON.stringify(row.attachmentList || [])),
+ });
+ if (!isTravel.value && form.expenseCategory) {
+ /* 宸茬敱 mapCost 杞负 value */
+ }
+ recalcTravelStandards();
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ async function initForm() {
+ resetFormForModule();
+ if (!allUsersCache.value.length) await loadUserPool();
+ if (modeRef.value !== "edit") {
+ form.approvalFlowNodes = [
+ { approverId: "", approverName: "", nodeOrder: 1, signMode: "countersign" },
+ ];
+ tryApplyCurrentUser();
+ }
+ }
+
+ function validateForm() {
+ if (!form.applicantId) {
+ uni.showToast({ title: "璇烽�夋嫨鍛樺伐", icon: "none" });
+ return false;
+ }
+ if (!(form.reimburseReason || "").trim()) {
+ uni.showToast({ title: "璇峰~鍐欐姤閿�鍘熷洜", icon: "none" });
+ return false;
+ }
+ if (isTravel.value) {
+ if (!form.travelStartTime) {
+ uni.showToast({ title: "璇烽�夋嫨鍑哄樊寮�濮嬫椂闂�", icon: "none" });
+ return false;
+ }
+ if (!form.travelEndTime) {
+ uni.showToast({ title: "璇烽�夋嫨鍑哄樊缁撴潫鏃堕棿", icon: "none" });
+ return false;
+ }
+ if (computeTravelDays(form.travelStartTime, form.travelEndTime) == null) {
+ uni.showToast({ title: "缁撴潫鏃堕棿椤绘櫄浜庡紑濮嬫椂闂�", icon: "none" });
+ return false;
+ }
+ if (!(form.departurePlace || "").trim()) {
+ uni.showToast({ title: "璇峰~鍐欏嚭宸湴", icon: "none" });
+ return false;
+ }
+ if (!(form.destination || "").trim()) {
+ uni.showToast({ title: "璇峰~鍐欑洰鐨勫湴", icon: "none" });
+ return false;
+ }
+ } else if (!form.expenseCategory) {
+ uni.showToast({ title: "璇烽�夋嫨璐圭敤绫诲瀷", icon: "none" });
+ return false;
+ }
+ if (form.applyAmount === "" || form.applyAmount == null) {
+ uni.showToast({ title: "璇峰~鍐欑敵璇烽噾棰�", icon: "none" });
+ return false;
+ }
+ if (!(form.payee || "").trim()) {
+ uni.showToast({ title: "璇峰~鍐欐敹娆句汉", icon: "none" });
+ return false;
+ }
+ if (!(form.expenseDetails || []).length) {
+ uni.showToast({ title: "璇疯嚦灏戞坊鍔犱竴鏉℃姤閿�鏄庣粏", icon: "none" });
+ return false;
+ }
+ const nodes = form.approvalFlowNodes || [];
+ if (!nodes.length || nodes.some(n => n.approverId == null || n.approverId === "")) {
+ uni.showToast({ title: "姣忎釜瀹℃壒鑺傜偣椤婚�夋嫨瀹℃壒浜�", icon: "none" });
+ return false;
+ }
+ return true;
+ }
+
+ async function submitForm() {
+ if (!validateForm()) return;
+ recalcTravelStandards();
+ if (isTravel.value && form.needSpecialApproval) {
+ const ok = await new Promise(resolve => {
+ uni.showModal({
+ title: "瓒呮敮鎻愰啋",
+ content: "瀛樺湪瓒呮敮椤癸紝鎻愪氦鍚庡皢鏍囪涓洪渶鐗规壒锛屾槸鍚︾户缁紵",
+ success: r => resolve(!!r.confirm),
+ });
+ });
+ if (!ok) return;
+ }
+ const isEdit = modeRef.value === "edit";
+ const dto = isTravel.value
+ ? buildTravelReimbursementSaveDto(form, { computeTravelDays })
+ : buildCostReimbursementSaveDto(form);
+ const check = validateReimbursementPersistDto(dto, isEdit);
+ if (!check.ok) {
+ uni.showToast({ title: check.message, icon: "none" });
+ return;
+ }
+ const nodeCheck = validateReimbursementApprovalNodes(dto);
+ if (!nodeCheck.ok) {
+ uni.showToast({ title: nodeCheck.message, icon: "none" });
+ return;
+ }
+ submitting.value = true;
+ try {
+ await persistFinReimbursement(dto, isEdit);
+ uni.showToast({ title: isEdit ? "淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛", icon: "success" });
+ return true;
+ } catch {
+ uni.showToast({ title: isEdit ? "淇濆瓨澶辫触" : "鎻愪氦澶辫触", icon: "none" });
+ return false;
+ } finally {
+ submitting.value = false;
+ }
+ }
+
+ return {
+ form,
+ isTravel,
+ submitting,
+ loading,
+ flowUserOptions,
+ travelDaysDisplay,
+ travelTierLabel,
+ suggestedLivingSubsidy,
+ suggestedTransportSubsidy,
+ suggestedHotelLimit,
+ detailTotalAmount,
+ overBudgetWarnings,
+ expenseSubjectOptions,
+ categoryActions,
+ categoryLabel,
+ showApplicantPicker,
+ applicantDisplaySub,
+ applicantAvatarColor,
+ showCategorySheet,
+ showSubjectSheet,
+ editingDetailIndex,
+ pickCategoryValue,
+ pickSubjectValue,
+ loadUserPool,
+ userLabel,
+ onApplicantPicked,
+ tryApplyCurrentUser,
+ recalcTravelStandards,
+ syncApplyAmountFromDetails,
+ addExpenseDetail,
+ removeExpenseDetail,
+ applyTemplate,
+ initForm,
+ loadEdit,
+ submitForm,
+ getReimbursementTypeByModuleKey,
+ };
+}
--
Gitblit v1.9.3