spring
12 小时以前 930d38ed2a3c2131be3305a585602c7a5a275fe3
src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
@@ -1,24 +1,49 @@
import { Search } from "@element-plus/icons-vue";
import dayjs from "dayjs";
import { ElMessageBox } from "element-plus";
import { computed, reactive, ref, watch } from "vue";
import {
  addApprovalTemplate,
  deleteApprovalTemplate,
  getApprovalTemplateDetail,
  listApprovalTemplate,
  listApprovalTemplatePage,
  TEMPLATE_TYPE_BUILTIN,
  TEMPLATE_TYPE_CUSTOM,
  TEMPLATE_TYPE_OPTIONS,
  updateApprovalTemplate,
} from "@/api/officeProcessAutomation/approvalTemplate.js";
import { Search } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { reactive, ref } from "vue";
import {
  buildApprovalTemplateListParams,
  createEmptyTemplateForm,
  createInitialMockTemplates,
  flowNodesSummary,
  getBuiltinTemplates,
  loadStoredTemplates,
  mapBuiltinCardFromApi,
  mapTemplateFromApi,
  mapTemplateToApi,
  nodeSignModeLabel,
  saveStoredTemplates,
  templateTypeLabel,
  unwrapTemplateList,
  formatDisplayTime,
  unwrapTemplateDetail,
  validateTemplateForm,
} from "./approveTemplateConstants.js";
import { parseFormConfigToData } from "./formConfigUtils.js";
const LEGACY_STORAGE_KEY = "oa_approve_template_custom_v1";
function clearLegacyStorage() {
  try {
    localStorage.removeItem(LEGACY_STORAGE_KEY);
  } catch {
    /* ignore */
  }
}
export function useApproveTemplate() {
  const stored = loadStoredTemplates();
  const allTemplates = ref(stored?.length ? stored : createInitialMockTemplates());
  clearLegacyStorage();
  const activeTab = ref("custom");
  const builtinTemplates = getBuiltinTemplates();
  const builtinTemplates = ref([]);
  const builtinLoading = ref(false);
  const searchForm = reactive({
    keyword: "",
@@ -27,6 +52,7 @@
  const tableLoading = ref(false);
  const page = reactive({ current: 1, size: 10, total: 0 });
  const tableData = ref([]);
  const formDialog = reactive({ visible: false, title: "", mode: "add" });
  const form = reactive(createEmptyTemplateForm());
@@ -34,44 +60,22 @@
  const detailDialog = reactive({ visible: false });
  const detailRow = ref({});
  const filteredList = computed(() => {
    let list = [...allTemplates.value];
    const kw = (searchForm.keyword || "").trim().toLowerCase();
    if (kw) {
      list = list.filter((r) => {
        const name = (r.templateName || "").toLowerCase();
        const desc = (r.description || "").toLowerCase();
        return name.includes(kw) || desc.includes(kw);
      });
    }
    if (searchForm.enabledOnly) {
      list = list.filter((r) => r.enabled !== false);
    }
    return list.sort((a, b) => (String(a.updateTime) < String(b.updateTime) ? 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);
  });
  const detailLoading = ref(false);
  const formRules = {
    templateName: [{ required: true, message: "请输入模板名称", trigger: "blur" }],
    templateType: [{ required: true, message: "请选择模板类型", trigger: "change" }],
  };
  const tableColumn = ref([
    { label: "模板名称", prop: "templateName", minWidth: 140 },
    {
      label: "模板类型",
      prop: "templateType",
      width: 100,
      align: "center",
      formatData: (v) => templateTypeLabel(v),
    },
    { label: "说明", prop: "description", minWidth: 160, showOverflowTooltip: true },
    {
      label: "节点数",
@@ -96,31 +100,72 @@
      formatData: (v) => (v !== false ? "启用" : "停用"),
      formatType: (v) => (v !== false ? "success" : "info"),
    },
    { label: "更新时间", prop: "updateTime", width: 170 },
    {
      label: "创建时间",
      prop: "createdTime",
      width: 170,
      showOverflowTooltip: true,
      formatData: (v) => formatDisplayTime(v),
    },
    {
      label: "更新时间",
      prop: "updatedTime",
      width: 170,
      showOverflowTooltip: true,
      formatData: (v) => formatDisplayTime(v),
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 200,
      width: 220,
      operation: [
        { name: "详情", type: "text", clickFun: (row) => openDetail(row) },
        { name: "编辑", type: "text", clickFun: (row) => openFormDialog("edit", row) },
        { name: "删除", type: "text", clickFun: (row) => removeTemplate(row) },
        {
          name: "删除",
          type: "danger",
          link: true,
          clickFun: (row) => removeTemplate(row),
        },
      ],
    },
  ]);
  function persist() {
    saveStoredTemplates(allTemplates.value);
  async function loadBuiltinTemplates() {
    builtinLoading.value = true;
    try {
      const res = await listApprovalTemplate(TEMPLATE_TYPE_BUILTIN);
      builtinTemplates.value = unwrapTemplateList(res).map(mapBuiltinCardFromApi);
    } catch {
      builtinTemplates.value = [];
      ElMessage.warning("系统常用审批加载失败");
    } finally {
      builtinLoading.value = false;
    }
  }
  async function fetchTemplateList() {
    tableLoading.value = true;
    try {
      const res = await listApprovalTemplatePage(
        buildApprovalTemplateListParams({ page, searchForm })
      );
      const data = res?.data || {};
      tableData.value = (data.records || []).map(mapTemplateFromApi);
      page.total = Number(data.total || 0);
    } catch {
      tableData.value = [];
      page.total = 0;
    } finally {
      tableLoading.value = false;
    }
  }
  function handleQuery() {
    tableLoading.value = true;
    page.current = 1;
    setTimeout(() => {
      tableLoading.value = false;
    }, 150);
    fetchTemplateList();
  }
  function resetSearch() {
@@ -132,6 +177,7 @@
  function pagination({ page: p, limit }) {
    page.current = p;
    page.size = limit;
    fetchTemplateList();
  }
  function resetForm(row) {
@@ -145,6 +191,11 @@
      id: row.id,
      templateName: row.templateName || "",
      description: row.description || "",
      templateType: row.templateType ?? TEMPLATE_TYPE_CUSTOM,
      formConfig: row.formConfig || "",
      formConfigData: JSON.parse(
        JSON.stringify(row.formConfigData || parseFormConfigToData(row.formConfig))
      ),
      enabled: row.enabled !== false,
      flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [base.flowNodes[0]])),
    });
@@ -152,19 +203,27 @@
  function openFormDialog(mode, row) {
    formDialog.mode = mode;
    formDialog.title = mode === "add" ? "新建自定义审批模板" : "编辑自定义审批模板";
    formDialog.title = mode === "add" ? "新建审批模板" : "编辑审批模板";
    resetForm(mode === "edit" ? row : null);
    formDialog.visible = true;
  }
  function openDetail(row) {
    detailRow.value = { ...row };
  async function openDetail(row) {
    if (row?.id == null || row.id === "") {
      ElMessage.warning("无法查看详情:缺少模板 ID");
      return;
    }
    detailDialog.visible = true;
  }
  function isNameDuplicate(name, excludeId) {
    const n = (name || "").trim();
    return allTemplates.value.some((t) => t.templateName?.trim() === n && t.id !== excludeId);
    detailLoading.value = true;
    detailRow.value = {};
    try {
      const res = await getApprovalTemplateDetail(row.id);
      detailRow.value = mapTemplateFromApi(unwrapTemplateDetail(res));
    } catch {
      detailDialog.visible = false;
    } finally {
      detailLoading.value = false;
    }
  }
  async function submitForm() {
@@ -178,64 +237,70 @@
    if (!validated.ok) {
      return { message: validated.message };
    }
    if (isNameDuplicate(validated.name, form.id)) {
      return { message: "模板名称已存在,请更换名称" };
    if (formDialog.mode === "edit" && !form.id) {
      return { message: "缺少模板 ID,无法保存修改" };
    }
    const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
    if (formDialog.mode === "add") {
      allTemplates.value.unshift({
        id: `tpl_${Date.now()}`,
        templateName: validated.name,
        description: (form.description || "").trim(),
        enabled: form.enabled !== false,
        createTime: now,
        updateTime: now,
        flowNodes: validated.nodes,
      });
    } else {
      const hit = allTemplates.value.find((t) => t.id === form.id);
      if (!hit) return { message: "模板不存在或已删除" };
      hit.templateName = validated.name;
      hit.description = (form.description || "").trim();
      hit.enabled = form.enabled !== false;
      hit.flowNodes = validated.nodes;
      hit.updateTime = now;
    const dto = mapTemplateToApi(form);
    try {
      if (formDialog.mode === "add") {
        await addApprovalTemplate(dto);
      } else {
        await updateApprovalTemplate(dto);
      }
    } catch {
      return false;
    }
    persist();
    formDialog.visible = false;
    page.current = 1;
    await fetchTemplateList();
    if (dto.templateType === TEMPLATE_TYPE_BUILTIN) {
      await loadBuiltinTemplates();
    }
    return { ok: true };
  }
  async function removeTemplate(row) {
    if (row?.id == null || row.id === "") {
      ElMessage.warning("无法删除:缺少模板 ID");
      return;
    }
    const name = row.templateName || "未命名模板";
    try {
      await ElMessageBox.confirm(`确定删除模板「${row.templateName}」吗?`, "提示", {
        type: "warning",
        confirmButtonText: "删除",
        cancelButtonText: "取消",
      });
      await ElMessageBox.confirm(
        `确定要删除审批模板「${name}」吗?删除后不可恢复。`,
        "删除确认",
        {
          type: "warning",
          confirmButtonText: "确定删除",
          cancelButtonText: "取消",
          distinguishCancelAndClose: true,
          autofocus: false,
        }
      );
    } catch {
      return;
    }
    const idx = allTemplates.value.findIndex((t) => t.id === row.id);
    if (idx >= 0) {
      allTemplates.value.splice(idx, 1);
      persist();
    try {
      await deleteApprovalTemplate([row.id]);
      ElMessage.success("删除成功");
      await fetchTemplateList();
      if (row.templateType === TEMPLATE_TYPE_BUILTIN) {
        await loadBuiltinTemplates();
      }
    } catch {
      /* 错误由拦截器提示 */
    }
  }
  function toggleEnabled(row) {
    const hit = allTemplates.value.find((t) => t.id === row.id);
    if (!hit) return;
    hit.enabled = !hit.enabled;
    hit.updateTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
    persist();
  }
  return {
    Search,
    TEMPLATE_TYPE_OPTIONS,
    templateTypeLabel,
    activeTab,
    builtinTemplates,
    builtinLoading,
    loadBuiltinTemplates,
    fetchTemplateList,
    nodeSignModeLabel,
    flowNodesSummary,
    searchForm,
@@ -249,12 +314,12 @@
    formRules,
    detailDialog,
    detailRow,
    detailLoading,
    handleQuery,
    resetSearch,
    pagination,
    openFormDialog,
    openDetail,
    submitForm,
    toggleEnabled,
  };
}