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