yyb
6 小时以前 df5efb2ca2b0cf74d9160ffe2b6c215c4ddc9c99
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,
@@ -42,105 +64,7 @@
export function useCostReimburse() {
  const { proxy } = getCurrentInstance();
  const allRows = ref([
    {
      id: "1",
      reimburseNo: "CR202605100001",
      applicantId: "mock_1",
      employeeNo: "zhangsan",
      employeeName: "张三",
      applicantNo: "zhangsan",
      applicantName: "张三",
      expenseCategory: "office_procurement",
      reimburseReason: "采购打印机硒鼓、A4纸等办公耗材。",
      applyAmount: 680,
      payee: "张三",
      payeeAccount: "6222 **** **** 1234",
      bankBranch: "中国工商银行杭州西湖支行",
      expenseDetails: [
        { id: "d1", invoiceDate: "2026-05-08", expenseSubject: "office_supply", amount: 380, description: "A4复印纸" },
        { id: "d2", invoiceDate: "2026-05-08", expenseSubject: "office_supply", amount: 300, description: "硒鼓" },
      ],
      attachmentList: [{ name: "采购发票.pdf", url: "/mock/invoice1.pdf" }],
      approvalFlowNodes: demoFlowNodes(680, "office_procurement"),
      currentNodeIndex: 0,
      approvalResult: "pending",
      rejectReason: "",
      approvalRecords: [],
      applyTime: "2026-05-10 09:15:00",
      createTime: "2026-05-10 09:15:00",
      deptId: "101",
      deptName: "行政部",
    },
    {
      id: "2",
      reimburseNo: "CR202605080002",
      applicantId: "mock_2",
      employeeNo: "lisi",
      employeeName: "李四",
      applicantNo: "lisi",
      applicantName: "李四",
      expenseCategory: "business_entertainment",
      reimburseReason: "接待重点客户商务宴请。",
      applyAmount: 3200,
      payee: "李四",
      payeeAccount: "6217 **** **** 5678",
      bankBranch: "招商银行武汉光谷支行",
      expenseDetails: [
        { id: "d3", invoiceDate: "2026-05-06", expenseSubject: "entertainment", amount: 3200, description: "客户宴请" },
      ],
      attachmentList: [],
      approvalFlowNodes: demoFlowNodes(3200, "business_entertainment").map((n, i) => ({
        ...n,
        nodeStatus: i === 0 ? "error" : "wait",
        approveOpinion: i === 0 ? "发票模糊需重传" : "",
        approveTime: i === 0 ? "2026-05-09 14:20:00" : "",
      })),
      currentNodeIndex: 0,
      approvalResult: "rejected",
      rejectReason: "发票模糊需重传",
      approvalRecords: [
        { operatorName: "直属上级", result: "rejected", opinion: "发票模糊需重传", time: "2026-05-09 14:20:00" },
      ],
      applyTime: "2026-05-07 16:30:00",
      createTime: "2026-05-07 16:30:00",
      deptId: "102",
      deptName: "销售部",
    },
    {
      id: "3",
      reimburseNo: "CR202605050003",
      applicantId: "mock_3",
      employeeNo: "wangwu",
      employeeName: "王五",
      applicantNo: "wangwu",
      applicantName: "王五",
      expenseCategory: "communication",
      reimburseReason: "5月因公话费报销。",
      applyAmount: 198,
      payee: "王五",
      payeeAccount: "6228 **** **** 9012",
      bankBranch: "中国建设银行成都高新支行",
      expenseDetails: [
        { id: "d4", invoiceDate: "2026-05-05", expenseSubject: "phone", amount: 198, description: "话费账单" },
      ],
      attachmentList: [{ name: "话费账单.jpg", url: "/mock/phone.jpg" }],
      approvalFlowNodes: demoFlowNodes(198, "communication").map((n) => ({
        ...n,
        nodeStatus: "finish",
        approveOpinion: "同意",
        approveTime: "2026-05-06 10:00:00",
      })),
      currentNodeIndex: 0,
      approvalResult: "approved",
      rejectReason: "",
      approvalRecords: [{ operatorName: "直属上级", result: "approved", opinion: "同意", time: "2026-05-06 10:00:00" }],
      applyTime: "2026-05-05 11:00:00",
      createTime: "2026-05-05 11:00:00",
      deptId: "103",
      deptName: "技术部",
    },
  ]);
  const allRows = ref([]);
  const searchForm = reactive({
    applicantKeyword: "",
@@ -157,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));
@@ -247,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),
        },
      ],
    },
@@ -393,10 +308,7 @@
  function handleQuery() {
    page.current = 1;
    tableLoading.value = true;
    setTimeout(() => {
      tableLoading.value = false;
    }, 150);
    return fetchList();
  }
  function resetSearch() {
@@ -409,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) {
@@ -434,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("");
@@ -471,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) {
@@ -569,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");
@@ -607,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,
@@ -627,6 +574,7 @@
    formDialog,
    formRules,
    detailDialog,
    detailLoading,
    detailRow,
    approveDialog,
    approveOpinion,
@@ -652,6 +600,7 @@
    openFormDialog,
    onFormClosed,
    submitForm,
    submitSaving,
    openDetail,
    approvalActionLabel,
    submitApprove,