yyb
20 小时以前 07d766a545881be779de94a800f6494ec46c1001
模板类型接口获取
已修改5个文件
688 ■■■■ 文件已修改
src/api/basicData/enum.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/officeProcessAutomation/approvalTemplate.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue 552 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/enum.js
@@ -1,5 +1,13 @@
import request from "@/utils/request.js";
/** 审批模板类型等通用枚举(TypeEnums) */
export function getTypeEnums() {
    return request({
        url: '/basic/enum/TypeEnums',
        method: 'get'
    })
}
export function findAllStockRecordTypeOptions() {
    return request({
        url: '/basic/enum/stockRecordType',
src/api/officeProcessAutomation/approvalTemplate.js
@@ -4,11 +4,6 @@
export const TEMPLATE_TYPE_BUILTIN = 0;
export const TEMPLATE_TYPE_CUSTOM = 1;
export const TEMPLATE_TYPE_OPTIONS = [
  { value: TEMPLATE_TYPE_BUILTIN, label: "系统内置" },
  { value: TEMPLATE_TYPE_CUSTOM, label: "自定义" },
];
/** 查询所有审批模板 */
export function listApprovalTemplate(type) {
  return request({
src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
@@ -1,8 +1,5 @@
import dayjs from "dayjs";
import {
  TEMPLATE_TYPE_CUSTOM,
  TEMPLATE_TYPE_OPTIONS,
} from "@/api/officeProcessAutomation/approvalTemplate.js";
import { TEMPLATE_TYPE_CUSTOM } from "@/api/officeProcessAutomation/approvalTemplate.js";
import { APPROVAL_TYPE_OPTIONS } from "../approve-list/approveListConstants.js";
import {
  buildFormConfigJson,
@@ -10,14 +7,6 @@
  parseFormConfigToData,
  validateFormConfigData,
} from "./formConfigUtils.js";
export { TEMPLATE_TYPE_OPTIONS };
export function templateTypeLabel(type) {
  if (type == null || type === "") return "—";
  const n = Number(type);
  return TEMPLATE_TYPE_OPTIONS.find((x) => x.value === n)?.label || "—";
}
/** 节点内审批方式:会签 / 或签 */
export const NODE_SIGN_MODE_OPTIONS = [
@@ -165,6 +154,7 @@
    enabled: mapEnabledFromApi(row.enabled),
    enabledRaw: row.enabled,
    templateType: row.templateType != null ? Number(row.templateType) : undefined,
    businessType: row.businessType ?? "",
    formConfig: row.formConfig,
    formConfigData: parseFormConfigToData(row.formConfig),
    createdUser: row.createdUser,
@@ -183,7 +173,8 @@
    templateName: (form.templateName || "").trim(),
    description: (form.description || "").trim(),
    enabled: mapEnabledToApi(form.enabled),
    templateType: form.templateType ?? TEMPLATE_TYPE_CUSTOM,
    templateType: TEMPLATE_TYPE_CUSTOM,
    businessType: form.businessType ?? "",
    formConfig: buildFormConfigJson(form.formConfigData),
    nodes: nodes.map((n, i) => {
      const node = {
@@ -248,6 +239,7 @@
    templateName: "",
    description: "",
    templateType: TEMPLATE_TYPE_CUSTOM,
    businessType: "",
    formConfig: "",
    formConfigData: createEmptyFormConfigData(),
    enabled: true,
@@ -277,6 +269,9 @@
export function validateTemplateForm(form) {
  const name = (form.templateName || "").trim();
  if (!name) return { ok: false, message: "请填写模板名称" };
  if (form.businessType == null || form.businessType === "") {
    return { ok: false, message: "请选择模板类型" };
  }
  const nodes = normalizeFlowNodes(form.flowNodes);
  if (!nodes.length) return { ok: false, message: "请至少配置一个审批节点" };
  for (let i = 0; i < nodes.length; i++) {
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
@@ -1,432 +1,724 @@
<!--OA模块:审批模板-->
<template>
  <div class="app-container approve-template-page">
    <el-tabs v-model="activeTab" class="template-tabs">
      <el-tab-pane label="系统常用审批" name="builtin">
        <el-alert type="info" show-icon :closable="false" class="mb16">
          <template #title>系统预置模板</template>
          <template #default>
            以下为 OA 模块内置的常用审批类型,填报字段与默认审批方式由系统维护;提交审批时可直接选用。
          </template>
        </el-alert>
        <div v-loading="builtinLoading" class="builtin-grid">
          <template v-if="builtinTemplates.length">
            <div v-for="item in builtinTemplates" :key="item.key" class="builtin-card">
            <span class="builtin-label">{{ item.label }}</span>
            <p class="builtin-summary">{{ item.summary }}</p>
            <div class="builtin-meta">
              <el-tag size="small" effect="plain">{{ item.fieldCount }} 个填报项</el-tag>
              <el-tag size="small" type="warning" effect="plain">
                默认{{ item.defaultMode === "or_sign" ? "或签" : "与签" }}
              </el-tag>
              <el-tag size="small" type="info" effect="plain">只读</el-tag>
            </div>
            </div>
          </template>
          <el-empty v-else-if="!builtinLoading" description="暂无系统常用审批模板" :image-size="80" />
        </div>
      </el-tab-pane>
      <el-tab-pane label="自定义审批模板" name="custom">
        <div class="search_form mb20">
          <div class="search_fields">
            <span class="search_title">模板名称:</span>
            <el-input
              v-model="searchForm.keyword"
              style="width: 220px"
              placeholder="搜索名称或说明"
              clearable
              :prefix-icon="Search"
              @keyup.enter="handleQuery"
            />
            <el-checkbox v-model="searchForm.enabledOnly" class="ml12" @change="handleQuery">
              仅显示启用
            </el-checkbox>
            <el-button type="primary" :icon="Search" class="ml10" @click="handleQuery">搜索</el-button>
            <el-button :icon="RefreshRight" @click="resetSearch">重置</el-button>
          </div>
          <div class="search_actions">
            <el-button type="primary" :icon="Plus" @click="openFormDialog('add')">新建模板</el-button>
          </div>
        </div>
    <div class="search_form mb20">
        <div class="table_list">
          <PIMTable
            rowKey="id"
            :column="tableColumn"
            :tableData="tableData"
            :page="page"
            :isSelection="false"
            :tableLoading="tableLoading"
            :total="page.total"
            @pagination="pagination"
          />
        </div>
      </el-tab-pane>
    </el-tabs>
      <div class="search_fields">
        <span class="search_title">模板名称:</span>
        <el-input
          v-model="searchForm.keyword"
          style="width: 220px"
          placeholder="搜索名称或说明"
          clearable
          :prefix-icon="Search"
          @keyup.enter="handleQuery"
        />
        <el-checkbox v-model="searchForm.enabledOnly" class="ml12" @change="handleQuery">
          仅显示启用
        </el-checkbox>
        <el-button type="primary" :icon="Search" class="ml10" @click="handleQuery">搜索</el-button>
        <el-button :icon="RefreshRight" @click="resetSearch">重置</el-button>
      </div>
      <div class="search_actions">
        <el-button type="primary" :icon="Plus" @click="openFormDialog('add')">新建模板</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="false"
        :tableLoading="tableLoading"
        :total="page.total"
        @pagination="pagination"
      />
    </div>
    <!-- 新建 / 编辑 -->
    <el-dialog
      v-model="formDialog.visible"
      :title="formDialog.title"
      width="1020px"
      append-to-body
      destroy-on-close
      class="template-form-dialog"
      @closed="formRef?.resetFields?.()"
      @closed="onFormDialogClosed"
    >
      <el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
      <el-form
        v-if="formDialog.visible"
        ref="formRef"
        :model="form"
        :rules="formRules"
        label-width="100px"
      >
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="模板名称" prop="templateName">
              <el-input v-model="form.templateName" placeholder="如:项目立项审批" maxlength="50" show-word-limit />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="模板类型" prop="templateType">
              <el-select v-model="form.templateType" placeholder="请选择" style="width: 100%">
            <el-form-item label="模板类型" prop="businessType">
              <el-select v-model="form.businessType" placeholder="请选择" style="width: 100%">
                <el-option
                  v-for="opt in TEMPLATE_TYPE_OPTIONS"
                  :key="opt.value"
                  v-for="opt in templateTypeOptions"
                  :key="`tpl-type-${opt.value}`"
                  :label="opt.label"
                  :value="opt.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="启用状态">
              <el-switch v-model="form.enabled" active-text="启用" inactive-text="停用" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="模板说明">
          <el-input
            v-model="form.description"
            type="textarea"
            :rows="2"
            placeholder="简要说明该模板的适用场景"
            maxlength="200"
            show-word-limit
          />
        </el-form-item>
        <el-form-item label="填报配置">
          <FormConfigEditor v-model="form.formConfigData" />
          <p class="flow-tip">配置提交审批时需填写的表单项,保存后写入 formConfig(JSON)。</p>
        </el-form-item>
        <el-form-item label="审批流程" required>
          <TemplateFlowEditor v-model="form.flowNodes" :user-options="flowUserOptions" />
          <p class="flow-tip">
            按顺序流转:可为每个节点添加多名审批人;会签需全部通过,或签任一人通过即可进入下一节点。
          </p>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="onSubmitForm">保 存</el-button>
        <el-button @click="formDialog.visible = false">取 消</el-button>
      </template>
    </el-dialog>
    <!-- 详情 -->
    <el-dialog v-model="detailDialog.visible" title="模板详情" width="880px" append-to-body destroy-on-close>
      <div v-loading="detailLoading" class="detail-dialog-body">
      <el-descriptions :column="2" border>
        <el-descriptions-item label="模板名称">{{ detailRow.templateName }}</el-descriptions-item>
        <el-descriptions-item label="模板类型">{{ templateTypeLabel(detailRow.templateType) }}</el-descriptions-item>
        <el-descriptions-item label="模板类型">{{ templateTypeLabel(detailRow.businessType) }}</el-descriptions-item>
        <el-descriptions-item label="状态">
          <el-tag :type="detailRow.enabled !== false ? 'success' : 'info'" size="small">
            {{ detailRow.enabled !== false ? "启用" : "停用" }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="说明" :span="2">{{ detailRow.description || "—" }}</el-descriptions-item>
        <el-descriptions-item label="填报提示" :span="2">
          {{ detailFormConfig.summaryPlaceholder || "—" }}
        </el-descriptions-item>
        <el-descriptions-item label="创建人">{{ detailRow.createdUserName || "—" }}</el-descriptions-item>
        <el-descriptions-item label="创建时间">{{ formatDisplayTime(detailRow.createdTime) }}</el-descriptions-item>
        <el-descriptions-item label="更新时间">{{ formatDisplayTime(detailRow.updatedTime) }}</el-descriptions-item>
      </el-descriptions>
      <el-divider content-position="left">填报项({{ detailFormConfig.fields?.length || 0 }} 项)</el-divider>
      <el-table
        v-if="detailFormConfig.fields?.length"
        :data="detailFormConfig.fields"
        border
        size="small"
        class="mb16"
      >
        <el-table-column prop="label" label="显示名称" min-width="120" />
        <el-table-column prop="key" label="字段标识" min-width="100" />
        <el-table-column label="类型" width="100">
          <template #default="{ row }">{{ formFieldTypeLabel(row.type) }}</template>
        </el-table-column>
        <el-table-column label="必填" width="70" align="center">
          <template #default="{ row }">{{ row.required !== false ? "是" : "否" }}</template>
        </el-table-column>
        <el-table-column label="默认值" min-width="120" show-overflow-tooltip>
          <template #default="{ row }">{{ formatDefaultValueDisplay(row) }}</template>
        </el-table-column>
      </el-table>
      <el-empty v-else description="未配置填报项" :image-size="48" class="mb16" />
      <el-divider content-position="left">审批流程({{ detailRow.flowNodes?.length || 0 }} 个节点)</el-divider>
      <div v-if="detailRow.flowNodes?.length" class="detail-flow">
        <div v-for="(node, index) in detailRow.flowNodes" :key="index" class="detail-node">
          <div class="detail-node-head">
            <span class="detail-node-order">节点 {{ index + 1 }}</span>
            <el-tag size="small" :type="node.signMode === 'or_sign' ? 'warning' : 'primary'">
              {{ nodeSignModeLabel(node.signMode) }}
            </el-tag>
          </div>
          <div class="detail-approvers">
            <el-tag
              v-for="a in node.approvers"
              :key="String(a.approverId)"
              class="detail-approver-tag"
              effect="plain"
            >
              {{ a.approverName || "—" }}
            </el-tag>
            <span v-if="!node.approvers?.length" class="text-muted">未配置审批人</span>
          </div>
          <el-icon v-if="index < detailRow.flowNodes.length - 1" class="detail-arrow"><ArrowRight /></el-icon>
        </div>
      </div>
      <el-empty v-else description="暂无流程节点" :image-size="60" />
      </div>
      <template #footer>
        <el-button @click="detailDialog.visible = false">关 闭</el-button>
        <el-button type="primary" @click="editFromDetail">编 辑</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ArrowRight, Document, Plus, RefreshRight } from "@element-plus/icons-vue";
import { ArrowRight, Plus, RefreshRight } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { computed, onMounted, ref } from "vue";
import { computed, nextTick, onMounted, ref } from "vue";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import FormConfigEditor from "./components/FormConfigEditor.vue";
import TemplateFlowEditor from "./components/TemplateFlowEditor.vue";
import { formatDisplayTime } from "./approveTemplateConstants.js";
import { formatDefaultValueDisplay, formFieldTypeLabel, parseFormConfigToData } from "./formConfigUtils.js";
import { useApproveTemplate } from "./useApproveTemplate.js";
const at = useApproveTemplate();
const {
  Search,
  TEMPLATE_TYPE_OPTIONS,
  templateTypeOptions,
  loadTemplateTypeOptions,
  templateTypeLabel,
  activeTab,
  builtinTemplates,
  builtinLoading,
  loadBuiltinTemplates,
  nodeSignModeLabel,
  searchForm,
  tableLoading,
  page,
  tableData,
  tableColumn,
  formDialog,
  form,
  formRef,
  formRules,
  detailDialog,
  detailRow,
  detailLoading,
  fetchTemplateList,
  handleQuery,
  resetSearch,
  pagination,
  openFormDialog,
  openDetail,
  submitForm,
} = at;
} = useApproveTemplate();
const flowUserOptions = ref([]);
const detailFormConfig = computed(() =>
  parseFormConfigToData(detailRow.value?.formConfigData ?? detailRow.value?.formConfig)
);
function unwrapArray(payload) {
  if (Array.isArray(payload)) return payload;
  if (payload?.data && Array.isArray(payload.data)) return payload.data;
  if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
  return [];
}
function isActiveUser(u) {
  if (u.delFlag === "2" || u.delFlag === 2) return false;
  if (u.status == null) return true;
  return String(u.status) === "0";
}
async function loadUsers() {
  try {
    const res = await userListNoPageByTenantId();
    flowUserOptions.value = unwrapArray(res).filter(isActiveUser);
  } catch {
    flowUserOptions.value = [];
  }
}
async function onSubmitForm() {
  const ret = await submitForm();
  if (ret?.message) {
    ElMessage.warning(ret.message);
    return;
  }
  if (ret?.ok) ElMessage.success("保存成功");
}
function editFromDetail() {
  const row = detailRow.value;
  detailDialog.visible = false;
  openFormDialog("edit", row);
function onFormDialogClosed() {
  formRef.value?.resetFields?.();
}
async function editFromDetail() {
  const row = detailRow.value;
  detailDialog.visible = false;
  await nextTick();
  openFormDialog("edit", row);
}
onMounted(() => {
  loadUsers();
  loadBuiltinTemplates();
  loadTemplateTypeOptions();
  fetchTemplateList();
});
</script>
<style scoped>
.mb20 {
  margin-bottom: 20px;
}
.mb16 {
  margin-bottom: 16px;
}
.mb16.el-empty {
  padding: 8px 0;
}
.ml10 {
  margin-left: 10px;
}
.ml12 {
  margin-left: 12px;
}
.page-header .header-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 18px;
  font-weight: 600;
  color: var(--el-text-color-primary);
  margin-bottom: 8px;
}
.title-icon {
  font-size: 22px;
  color: var(--el-color-primary);
}
.header-desc {
  margin: 0;
  font-size: 13px;
  color: var(--el-text-color-secondary);
  line-height: 1.6;
  max-width: 920px;
}
.search_form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.search_fields {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
}
.search_actions {
  display: flex;
  gap: 8px;
}
.builtin-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
}
.builtin-card {
  padding: 14px 16px;
  border: 1px solid var(--el-border-color-lighter);
  border-radius: var(--radius-md, 8px);
  background: var(--el-fill-color-blank);
}
.builtin-label {
  font-size: 15px;
  font-weight: 600;
  color: var(--el-text-color-primary);
}
.builtin-summary {
  margin: 8px 0 10px;
  font-size: 12px;
  color: var(--el-text-color-secondary);
  line-height: 1.5;
  min-height: 36px;
}
.builtin-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.flow-tip {
  font-size: 12px;
  color: var(--el-text-color-secondary);
  margin: 8px 0 0;
  line-height: 1.5;
}
.detail-flow {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 8px;
}
.detail-node {
  position: relative;
  min-width: 180px;
  max-width: 240px;
  padding: 12px;
  border: 1px solid var(--el-border-color-lighter);
  border-radius: 8px;
  background: var(--el-fill-color-lighter);
}
.detail-node-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}
.detail-node-order {
  font-weight: 600;
  font-size: 13px;
}
.detail-approvers {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
.detail-approver-tag {
  margin: 0;
}
.detail-arrow {
  position: absolute;
  right: -20px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--el-text-color-placeholder);
}
.detail-dialog-body {
  min-height: 120px;
}
.text-muted {
  font-size: 12px;
  color: var(--el-text-color-placeholder);
}
.template-form-dialog :deep(.el-dialog__body) {
  padding-top: 8px;
}
</style>
src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
@@ -2,13 +2,11 @@
  addApprovalTemplate,
  deleteApprovalTemplate,
  getApprovalTemplateDetail,
  listApprovalTemplate,
  listApprovalTemplatePage,
  TEMPLATE_TYPE_BUILTIN,
  TEMPLATE_TYPE_CUSTOM,
  TEMPLATE_TYPE_OPTIONS,
  updateApprovalTemplate,
} from "@/api/officeProcessAutomation/approvalTemplate.js";
import { getTypeEnums } from "@/api/basicData/enum.js";
import { Search } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { reactive, ref } from "vue";
@@ -16,12 +14,9 @@
  buildApprovalTemplateListParams,
  createEmptyTemplateForm,
  flowNodesSummary,
  mapBuiltinCardFromApi,
  mapTemplateFromApi,
  mapTemplateToApi,
  nodeSignModeLabel,
  templateTypeLabel,
  unwrapTemplateList,
  formatDisplayTime,
  unwrapTemplateDetail,
  validateTemplateForm,
@@ -29,6 +24,44 @@
import { parseFormConfigToData } from "./formConfigUtils.js";
const LEGACY_STORAGE_KEY = "oa_approve_template_custom_v1";
const FALLBACK_TEMPLATE_TYPE_OPTIONS = [
  { value: 0, label: "系统内置" },
  { value: 1, label: "自定义" },
];
function unwrapEnumList(data) {
  if (Array.isArray(data)) return data;
  if (!data || typeof data !== "object") return [];
  if (Array.isArray(data.TypeEnums)) return data.TypeEnums;
  if (Array.isArray(data.typeEnums)) return data.typeEnums;
  const nested = Object.values(data).find((v) => Array.isArray(v));
  return nested || [];
}
function normalizeTypeEnumOptions(data) {
  return unwrapEnumList(data)
    .map((item) => {
      const rawValue = item?.value ?? item?.code ?? item?.businessType ?? item?.dictValue ?? item?.key;
      if (rawValue == null || rawValue === "") return null;
      const num = Number(rawValue);
      const value =
        typeof rawValue === "number" || (Number.isFinite(num) && String(rawValue).trim() !== "")
          ? num
          : rawValue;
      const label =
        item?.label ?? item?.name ?? item?.desc ?? item?.dictLabel ?? item?.text ?? String(value);
      return { label, value };
    })
    .filter(Boolean);
}
function matchTemplateTypeValue(options, type) {
  if (type == null || type === "") return false;
  return options.some(
    (x) => x.value === type || x.value === Number(type) || String(x.value) === String(type)
  );
}
function clearLegacyStorage() {
  try {
@@ -41,9 +74,15 @@
export function useApproveTemplate() {
  clearLegacyStorage();
  const activeTab = ref("custom");
  const builtinTemplates = ref([]);
  const builtinLoading = ref(false);
  const templateTypeOptions = ref([...FALLBACK_TEMPLATE_TYPE_OPTIONS]);
  function templateTypeLabel(type) {
    if (type == null || type === "") return "—";
    const hit = templateTypeOptions.value.find(
      (x) => x.value === type || x.value === Number(type) || String(x.value) === String(type)
    );
    return hit?.label || "—";
  }
  const searchForm = reactive({
    keyword: "",
@@ -58,20 +97,33 @@
  const form = reactive(createEmptyTemplateForm());
  const formRef = ref();
  async function loadTemplateTypeOptions() {
    try {
      const res = await getTypeEnums();
      const list = normalizeTypeEnumOptions(res?.data);
      templateTypeOptions.value = list.length ? list : [...FALLBACK_TEMPLATE_TYPE_OPTIONS];
    } catch {
      templateTypeOptions.value = [...FALLBACK_TEMPLATE_TYPE_OPTIONS];
    }
    if (!matchTemplateTypeValue(templateTypeOptions.value, form.businessType)) {
      form.businessType = templateTypeOptions.value[0]?.value ?? "";
    }
  }
  const detailDialog = reactive({ visible: false });
  const detailRow = ref({});
  const detailLoading = ref(false);
  const formRules = {
    templateName: [{ required: true, message: "请输入模板名称", trigger: "blur" }],
    templateType: [{ required: true, message: "请选择模板类型", trigger: "change" }],
    businessType: [{ required: true, message: "请选择模板类型", trigger: "change" }],
  };
  const tableColumn = ref([
    { label: "模板名称", prop: "templateName", minWidth: 140 },
    {
      label: "模板类型",
      prop: "templateType",
      prop: "businessType",
      width: 100,
      align: "center",
      formatData: (v) => templateTypeLabel(v),
@@ -133,19 +185,6 @@
    },
  ]);
  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 {
@@ -191,7 +230,7 @@
      id: row.id,
      templateName: row.templateName || "",
      description: row.description || "",
      templateType: row.templateType ?? TEMPLATE_TYPE_CUSTOM,
      businessType: row.businessType ?? "",
      formConfig: row.formConfig || "",
      formConfigData: JSON.parse(
        JSON.stringify(row.formConfigData || parseFormConfigToData(row.formConfig))
@@ -253,9 +292,6 @@
    formDialog.visible = false;
    page.current = 1;
    await fetchTemplateList();
    if (dto.templateType === TEMPLATE_TYPE_BUILTIN) {
      await loadBuiltinTemplates();
    }
    return { ok: true };
  }
@@ -284,9 +320,6 @@
      await deleteApprovalTemplate([row.id]);
      ElMessage.success("删除成功");
      await fetchTemplateList();
      if (row.templateType === TEMPLATE_TYPE_BUILTIN) {
        await loadBuiltinTemplates();
      }
    } catch {
      /* 错误由拦截器提示 */
    }
@@ -294,12 +327,9 @@
  return {
    Search,
    TEMPLATE_TYPE_OPTIONS,
    templateTypeOptions,
    loadTemplateTypeOptions,
    templateTypeLabel,
    activeTab,
    builtinTemplates,
    builtinLoading,
    loadBuiltinTemplates,
    fetchTemplateList,
    nodeSignModeLabel,
    flowNodesSummary,