yyb
8 小时以前 4ab0be7d4441f378add1f242b168d80fb27e65fe
src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
@@ -1,7 +1,6 @@
import {
  getApprovalTemplateDetail,
  listApprovalTemplate,
  TEMPLATE_TYPE_BUILTIN,
  TEMPLATE_TYPE_CUSTOM,
} from "@/api/officeProcessAutomation/approvalTemplate.js";
import {
@@ -14,16 +13,26 @@
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,
  mapEnabledFromApi,
  mapTemplateFromApi,
  unwrapTemplateDetail,
  unwrapTemplateList,
} from "../approve-template/approveTemplateConstants.js";
import { buildSubmitTemplateFromRow } from "../approve-template/formConfigUtils.js";
import {
  buildFormPayloadRules,
  buildTemplateBindingFromDetail,
  validateTemplateBinding,
} from "../approve-shared/approvalTemplateBindingUtils.js";
import {
  APPROVAL_STATUS_SEARCH_OPTIONS,
  APPROVAL_TYPE_OPTIONS,
  approvalStatusLabel,
  approvalStatusTagType,
@@ -32,25 +41,34 @@
  buildApproveInstanceDto,
  buildEditFormFromInstanceRow,
  buildInstanceDto,
  clearLegacyApproveListStorage,
  createEmptySubmitForm,
  mapInstanceFromApi,
  mapSubmitTemplateCard,
  validateSubmitFlowNodes,
  matchBusinessTypeValue,
  unwrapInstancePage,
} from "./approveListConstants.js";
export function useApproveList() {
  clearLegacyApproveListStorage();
  const { proxy } = getCurrentInstance() || {};
  const userStore = useUserStore();
  const tableData = ref([]);
  const submitTemplateCards = ref([]);
  const searchBusinessTypeOptions = ref([]);
  const submitBusinessTypeOptions = ref([]);
  const allSubmitTemplates = ref([]);
  const selectedBusinessType = ref("");
  const submitTemplatesLoading = ref(false);
  const submitTemplateCards = computed(() => {
    if (selectedBusinessType.value == null || selectedBusinessType.value === "") return [];
    return allSubmitTemplates.value.filter((card) =>
      matchBusinessTypeValue(card.businessType, selectedBusinessType.value)
    );
  });
  const searchForm = reactive({
    approvalType: "",
    applicantKeyword: "",
    businessType: "",
    status: "",
    createTimeRange: [],
  });
@@ -64,6 +82,16 @@
  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);
  const submitForm = reactive(createEmptySubmitForm(""));
@@ -75,9 +103,22 @@
    if (submitDialog.mode === "edit") {
      return `修改${activeTemplate.value?.label || submitForm.templateName || "审批"}`;
    }
    if (submitDialog.step === 1) return "选择审批模板";
    if (submitDialog.step === 1) return "选择模板类型";
    if (submitDialog.step === 2) return `选择审批模板${businessTypeLabel(selectedBusinessType.value) ? `(${businessTypeLabel(selectedBusinessType.value)})` : ""}`;
    return `提交${activeTemplate.value?.label || "审批"}`;
  });
  const selectedBusinessTypeLabel = computed(() => businessTypeLabel(selectedBusinessType.value));
  function businessTypeLabel(type) {
    if (type == null || type === "") return "";
    const hit = submitBusinessTypeOptions.value.find((x) => matchBusinessTypeValue(x.value, type));
    return hit?.label || "";
  }
  function countTemplatesByBusinessType(type) {
    return allSubmitTemplates.value.filter((card) => matchBusinessTypeValue(card.businessType, type)).length;
  }
  const activeTemplate = computed(() => submitForm.templateSnapshot || null);
@@ -88,27 +129,15 @@
    return submitForm.formFieldDefs || [];
  });
  const submitFormRules = computed(() => {
    const rules = {
      templateKey: [{ required: true, message: "请选择审批类型", trigger: "change" }],
    };
    submitFormFields.value.forEach((f) => {
      if (!f.required) return;
      if (f.type === "number") {
        rules[`formPayload.${f.key}`] = [{ required: true, message: `请填写${f.label}`, trigger: "blur" }];
      } else if (f.type === "datetimerange") {
        rules[`formPayload.${f.key}`] = [{ required: true, message: `请选择${f.label}`, trigger: "change" }];
      } else {
        rules[`formPayload.${f.key}`] = [{ required: true, message: `请填写${f.label}`, trigger: "blur" }];
      }
    });
    return rules;
  });
  const submitFormRules = computed(() => ({
    templateKey: [{ required: true, message: "请选择审批类型", trigger: "change" }],
    ...buildFormPayloadRules(submitFormFields.value),
  }));
  const tableColumn = ref([
    { label: "申请人编号", prop: "applicantNo", width: 110 },
    { label: "申请人名称", prop: "applicantName", minWidth: 100 },
    { label: "业务类型", prop: "businessName", minWidth: 120 },
    { label: "模板类型", prop: "businessName", minWidth: 120 },
    {
      label: "审批类型",
      prop: "approvalType",
@@ -187,17 +216,17 @@
  async function loadSubmitTemplates() {
    submitTemplatesLoading.value = true;
    try {
      const [builtinRes, customRes] = await Promise.all([
        listApprovalTemplate(TEMPLATE_TYPE_BUILTIN),
      const [typeOptions, customRes] = await Promise.all([
        fetchBusinessTypeOptions(),
        listApprovalTemplate(TEMPLATE_TYPE_CUSTOM),
      ]);
      const merged = [
        ...unwrapTemplateList(builtinRes),
        ...unwrapTemplateList(customRes),
      ].filter((row) => mapEnabledFromApi(row.enabled));
      submitTemplateCards.value = merged.map(mapSubmitTemplateCard);
      submitBusinessTypeOptions.value = typeOptions;
      allSubmitTemplates.value = unwrapTemplateList(customRes)
        .filter((row) => mapEnabledFromApi(row.enabled))
        .map(mapSubmitTemplateCard);
    } catch {
      submitTemplateCards.value = [];
      submitBusinessTypeOptions.value = [];
      allSubmitTemplates.value = [];
      ElMessage.error("加载审批模板失败");
    } finally {
      submitTemplatesLoading.value = false;
@@ -210,10 +239,18 @@
  }
  function resetSearch() {
    searchForm.approvalType = "";
    searchForm.applicantKeyword = "";
    searchForm.businessType = "";
    searchForm.status = "";
    searchForm.createTimeRange = [];
    handleQuery();
  }
  async function loadSearchBusinessTypeOptions() {
    try {
      searchBusinessTypeOptions.value = await fetchBusinessTypeOptions();
    } catch {
      searchBusinessTypeOptions.value = [];
    }
  }
  function pagination({ page: p, limit }) {
@@ -222,20 +259,58 @@
    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() {
    submitDialog.mode = "add";
    submitDialog.step = 1;
    selectedBusinessType.value = "";
    submitEditRow.value = null;
    Object.assign(submitForm, createEmptySubmitForm(""));
  }
@@ -246,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) {
@@ -256,7 +345,7 @@
      return;
    }
    submitDialog.mode = "edit";
    submitDialog.step = 2;
    submitDialog.step = 3;
    submitEditRow.value = { ...row };
    Object.assign(submitForm, buildEditFormFromInstanceRow(row));
    submitDialog.visible = true;
@@ -267,19 +356,14 @@
    submitTemplatesLoading.value = true;
    try {
      const res = await getApprovalTemplateDetail(card.id);
      const mapped = mapTemplateFromApi(unwrapTemplateDetail(res));
      const tpl = {
        ...buildSubmitTemplateFromRow(mapped),
        templateId: mapped.id,
      };
      const base = createEmptySubmitForm(String(card.id), tpl, mapped.flowNodes);
      const applied = buildTemplateBindingFromDetail(res);
      Object.assign(submitForm, {
        ...base,
        templateName: mapped.templateName || tpl.label || "",
        templateSnapshot: tpl,
        formFieldDefs: tpl.fields || [],
        templateKey: String(card.id),
        ...applied,
        businessType:
          applied.businessType ?? card.businessType ?? selectedBusinessType.value,
      });
      submitDialog.step = 2;
      submitDialog.step = 3;
    } catch {
      ElMessage.error("加载模板详情失败");
    } finally {
@@ -287,8 +371,22 @@
    }
  }
  function backToTemplatePick() {
  function onBusinessTypePick(type) {
    if (!countTemplatesByBusinessType(type)) {
      ElMessage.warning("该类型下暂无可用审批模板");
      return;
    }
    selectedBusinessType.value = type;
    submitDialog.step = 2;
  }
  function backToBusinessTypePick() {
    selectedBusinessType.value = "";
    submitDialog.step = 1;
  }
  function backToTemplatePick() {
    submitDialog.step = 2;
  }
  async function submitInstanceForm() {
@@ -304,9 +402,9 @@
      return false;
    }
    if (!activeTemplate.value) return false;
    const flowCheck = validateSubmitFlowNodes(submitForm.flowNodes);
    if (!flowCheck.ok) {
      ElMessage.warning(flowCheck.message);
    const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes });
    if (!bindingCheck.ok) {
      ElMessage.warning(bindingCheck.message);
      return false;
    }
    if (!submitForm.templateId) {
@@ -321,7 +419,7 @@
          submitForm,
          activeTemplate: activeTemplate.value,
          userStore,
          flowNodes: flowCheck.nodes,
          flowNodes: bindingCheck.nodes,
        })
      );
      submitDialog.visible = false;
@@ -343,9 +441,9 @@
      return false;
    }
    if (!activeTemplate.value) return false;
    const flowCheck = validateSubmitFlowNodes(submitForm.flowNodes);
    if (!flowCheck.ok) {
      ElMessage.warning(flowCheck.message);
    const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes });
    if (!bindingCheck.ok) {
      ElMessage.warning(bindingCheck.message);
      return false;
    }
    if (!submitForm.instanceId) {
@@ -359,7 +457,7 @@
        buildInstanceDto({
          submitForm,
          activeTemplate: activeTemplate.value,
          flowNodes: flowCheck.nodes,
          flowNodes: bindingCheck.nodes,
          existingRow: submitEditRow.value,
        })
      );
@@ -414,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 };
@@ -451,6 +572,9 @@
  return {
    Search,
    APPROVAL_TYPE_OPTIONS,
    APPROVAL_STATUS_SEARCH_OPTIONS,
    searchBusinessTypeOptions,
    loadSearchBusinessTypeOptions,
    approvalTypeLabel,
    approvalStatusLabel,
    approvalStatusTagType,
@@ -462,9 +586,12 @@
    tableColumn,
    detailDialog,
    detailRow,
    reimburseDialog,
    approveDialog,
    approveOpinion,
    approveSubmitting,
    submitReimburseApprove,
    isReimburseApprovalInstance,
    submitDialog,
    isSubmitEdit,
    submitDialogTitle,
@@ -474,7 +601,12 @@
    activeTemplate,
    submitFormFields,
    submitFormRules,
    submitBusinessTypeOptions,
    submitTemplateCards,
    selectedBusinessType,
    selectedBusinessTypeLabel,
    businessTypeLabel,
    countTemplatesByBusinessType,
    submitTemplatesLoading,
    handleQuery,
    resetSearch,
@@ -482,7 +614,9 @@
    resetSubmitDialogState,
    openSubmitDialog,
    openEditDialog,
    onBusinessTypePick,
    onTemplatePick,
    backToBusinessTypePick,
    backToTemplatePick,
    submitInstanceForm,
    submitNewApproval,