增强审批模板功能,新增内置模板类型支持,优化模板编辑和导入逻辑,确保内置模板不可编辑和删除,提升用户体验和代码可维护性。
已修改4个文件
163 ■■■■ 文件已修改
src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue 93 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
@@ -1,6 +1,9 @@
import dayjs from "dayjs";
import { getTypeEnums } from "@/api/basicData/enum.js";
import { TEMPLATE_TYPE_CUSTOM } from "@/api/officeProcessAutomation/approvalTemplate.js";
import {
  TEMPLATE_TYPE_BUILTIN,
  TEMPLATE_TYPE_CUSTOM,
} from "@/api/officeProcessAutomation/approvalTemplate.js";
import { APPROVAL_TYPE_OPTIONS } from "../approve-list/approveListConstants.js";
import {
  buildFormConfigJson,
@@ -42,6 +45,11 @@
  } catch {
    return [];
  }
}
/** 是否为系统内置模板(templateType === 0) */
export function isBuiltinTemplate(row) {
  return Number(row?.templateType) === TEMPLATE_TYPE_BUILTIN;
}
/** 节点内审批方式:会签 / 或签 */
@@ -222,7 +230,8 @@
    templateName: (form.templateName || "").trim(),
    description: (form.description || "").trim(),
    enabled: mapEnabledToApi(form.enabled),
    templateType: TEMPLATE_TYPE_CUSTOM,
    templateType:
      form.templateType != null ? Number(form.templateType) : TEMPLATE_TYPE_CUSTOM,
    businessType: form.businessType ?? "",
    formConfig: buildFormConfigJson(form.formConfigData),
    nodes: nodes.map((n, i) => {
@@ -254,13 +263,10 @@
  return dto;
}
export function buildApprovalTemplateListParams({ page, searchForm, templateType = TEMPLATE_TYPE_CUSTOM }) {
export function buildApprovalTemplateListParams({ page, searchForm }) {
  const params = {
    current: page.current,
    size: page.size,
    templateType: searchForm?.templateType != null && searchForm.templateType !== ""
      ? searchForm.templateType
      : templateType,
  };
  const kw = (searchForm?.keyword || "").trim();
  if (kw) params.templateName = kw;
@@ -290,6 +296,7 @@
    templateName: "",
    description: "",
    templateType: TEMPLATE_TYPE_CUSTOM,
    lockedFormFieldUids: [],
    businessType: "",
    formConfig: "",
    formConfigData: createEmptyFormConfigData(),
src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
@@ -21,8 +21,15 @@
          </el-tag>
        </div>
        <div class="fce-toolbar-actions">
          <el-dropdown trigger="click" @visible-change="onImportDropdownVisible" @command="importFromTemplate">
            <el-button size="small" :loading="templateImportLoading">从已有模板导入</el-button>
          <el-dropdown
            trigger="click"
            :disabled="disableImport"
            @visible-change="onImportDropdownVisible"
            @command="importFromTemplate"
          >
            <el-button size="small" :loading="templateImportLoading" :disabled="disableImport">
              从已有模板导入
            </el-button>
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item v-if="!templateImportOptions.length" disabled>
@@ -55,7 +62,10 @@
          v-for="(field, index) in inner.fields"
          :key="field._uid"
          class="fce-card"
          :class="{ 'fce-card--required': field.required }"
          :class="{
            'fce-card--required': field.required,
            'fce-card--locked': isFieldLocked(field),
          }"
        >
          <div class="fce-card-badge">{{ index + 1 }}</div>
@@ -64,8 +74,9 @@
              <span class="fce-card-name">{{ field.label || `填报项 ${index + 1}` }}</span>
              <el-tag size="small" effect="light" type="primary">{{ typeLabel(field.type) }}</el-tag>
              <el-tag v-if="field.required" size="small" type="danger" effect="plain">必填</el-tag>
              <el-tag v-if="isFieldLocked(field)" size="small" type="info" effect="plain">内置项</el-tag>
            </div>
            <div class="fce-card-btns">
            <div v-if="!isFieldLocked(field)" class="fce-card-btns">
              <el-tooltip content="上移" placement="top">
                <el-button circle size="small" :disabled="index === 0" @click="moveField(index, -1)">
                  <el-icon><Top /></el-icon>
@@ -98,18 +109,30 @@
                    v-model="field.label"
                    placeholder="如:报销说明"
                    maxlength="50"
                    :disabled="isFieldLocked(field)"
                    @input="emitOut"
                  />
                </el-form-item>
              </el-col>
              <el-col :span="8">
                <el-form-item label="字段标识" required class="fce-field-item">
                  <el-input v-model="field.key" placeholder="如:summary" maxlength="50" @input="emitOut" />
                  <el-input
                    v-model="field.key"
                    placeholder="如:summary"
                    maxlength="50"
                    :disabled="isFieldLocked(field)"
                    @input="emitOut"
                  />
                </el-form-item>
              </el-col>
              <el-col :span="8">
                <el-form-item label="控件类型" class="fce-field-item">
                  <el-select v-model="field.type" style="width: 100%" @change="onTypeChange(field)">
                  <el-select
                    v-model="field.type"
                    style="width: 100%"
                    :disabled="isFieldLocked(field)"
                    @change="onTypeChange(field)"
                  >
                    <el-option
                      v-for="t in FORM_FIELD_TYPE_OPTIONS"
                      :key="t.value"
@@ -132,6 +155,7 @@
                    inline-prompt
                    active-text="必填"
                    inactive-text="选填"
                    :disabled="isFieldLocked(field)"
                    @change="emitOut"
                  />
                </el-form-item>
@@ -144,6 +168,7 @@
                    :max="10"
                    controls-position="right"
                    style="width: 100%"
                    :disabled="isFieldLocked(field)"
                    @change="emitOut"
                  />
                </el-form-item>
@@ -155,6 +180,7 @@
                      v-model="field.min"
                      controls-position="right"
                      style="width: 100%"
                      :disabled="isFieldLocked(field)"
                      @change="emitOut"
                    />
                  </el-form-item>
@@ -167,6 +193,7 @@
                      :max="4"
                      controls-position="right"
                      style="width: 100%"
                      :disabled="isFieldLocked(field)"
                      @change="emitOut"
                    />
                  </el-form-item>
@@ -184,6 +211,7 @@
              :type="field.type === 'textarea' ? 'textarea' : 'text'"
              :rows="field.type === 'textarea' ? 2 : undefined"
              :placeholder="defaultPlaceholder(field)"
              :disabled="isFieldLocked(field)"
              clearable
              @input="emitOut"
            />
@@ -195,6 +223,7 @@
              controls-position="right"
              placeholder="选填"
              style="width: 100%"
              :disabled="isFieldLocked(field)"
              @change="emitOut"
            />
            <el-date-picker
@@ -205,6 +234,7 @@
              format="YYYY-MM-DD"
              value-format="YYYY-MM-DD"
              style="width: 100%"
              :disabled="isFieldLocked(field)"
              clearable
              @change="emitOut"
            />
@@ -218,6 +248,7 @@
              format="YYYY-MM-DD HH:mm:ss"
              value-format="YYYY-MM-DD HH:mm:ss"
              style="width: 100%"
              :disabled="isFieldLocked(field)"
              clearable
              @change="emitOut"
            />
@@ -229,6 +260,7 @@
              clearable
              filterable
              :loading="optionSourceLoading"
              :disabled="isFieldLocked(field)"
              @change="emitOut"
            >
              <el-option
@@ -248,6 +280,7 @@
                  <el-select
                    v-model="field.optionSource"
                    style="width: 100%"
                    :disabled="isFieldLocked(field)"
                    @change="onOptionSourceChange(field)"
                  >
                    <el-option
@@ -266,7 +299,14 @@
            <template v-if="!isDynamicOptionSource(field.optionSource)">
              <div class="fce-options-head">
                <span class="fce-section-subtitle">手动选项</span>
                <el-button type="primary" link size="small" :icon="Plus" @click="addOption(field)">
                <el-button
                  type="primary"
                  link
                  size="small"
                  :icon="Plus"
                  :disabled="isFieldLocked(field)"
                  @click="addOption(field)"
                >
                  添加选项
                </el-button>
              </div>
@@ -276,13 +316,24 @@
                class="fce-option-row"
              >
                <span class="fce-option-index">{{ oi + 1 }}</span>
                <el-input v-model="opt.label" placeholder="显示文本" @input="emitOut" />
                <el-input v-model="opt.value" placeholder="选项值" class="fce-option-value" @input="emitOut" />
                <el-input
                  v-model="opt.label"
                  placeholder="显示文本"
                  :disabled="isFieldLocked(field)"
                  @input="emitOut"
                />
                <el-input
                  v-model="opt.value"
                  placeholder="选项值"
                  class="fce-option-value"
                  :disabled="isFieldLocked(field)"
                  @input="emitOut"
                />
                <el-button
                  type="danger"
                  link
                  :icon="Delete"
                  :disabled="field.options.length <= 1"
                  :disabled="isFieldLocked(field) || field.options.length <= 1"
                  @click="removeOption(field, oi)"
                />
              </div>
@@ -303,7 +354,7 @@
  TEMPLATE_TYPE_CUSTOM,
} from "@/api/officeProcessAutomation/approvalTemplate.js";
import { ElMessage, ElMessageBox } from "element-plus";
import { reactive, ref, watch } from "vue";
import { computed, reactive, ref, watch } from "vue";
import {
  mapEnabledFromApi,
  unwrapTemplateDetail,
@@ -327,6 +378,10 @@
  modelValue: { type: Object, default: () => createEmptyFormConfigData() },
  /** 编辑当前模板时排除自身,避免从自己导入 */
  excludeTemplateId: { type: [String, Number], default: null },
  /** 禁用「从已有模板导入」 */
  disableImport: { type: Boolean, default: false },
  /** 系统内置模板编辑时,打开弹窗即存在的填报项 _uid,不可改删 */
  lockedFieldUids: { type: Array, default: () => [] },
});
const emit = defineEmits(["update:modelValue"]);
@@ -337,6 +392,14 @@
const templateImportOptions = ref([]);
const templateImportLoading = ref(false);
const lockedUidSet = computed(
  () => new Set((props.lockedFieldUids || []).filter(Boolean))
);
function isFieldLocked(field) {
  return field?._uid != null && lockedUidSet.value.has(field._uid);
}
function typeLabel(type) {
  return formFieldTypeLabel(type);
@@ -408,13 +471,16 @@
}
function removeField(index) {
  if (isFieldLocked(inner.fields[index])) return;
  inner.fields.splice(index, 1);
  emitOut();
}
function moveField(index, delta) {
  if (isFieldLocked(inner.fields[index])) return;
  const next = index + delta;
  if (next < 0 || next >= inner.fields.length) return;
  if (isFieldLocked(inner.fields[next])) return;
  const t = inner.fields[index];
  inner.fields[index] = inner.fields[next];
  inner.fields[next] = t;
@@ -486,6 +552,7 @@
}
function onImportDropdownVisible(visible) {
  if (props.disableImport) return;
  if (visible) loadTemplateImportOptions();
}
@@ -610,6 +677,10 @@
  border-left: 3px solid var(--el-color-danger-light-3);
}
.fce-card--locked {
  background: var(--el-fill-color-light);
}
.fce-card-badge {
  position: absolute;
  top: -10px;
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
@@ -114,7 +114,13 @@
            <el-form-item label="模板名称" prop="templateName">
              <el-input v-model="form.templateName" placeholder="如:项目立项审批" maxlength="50" show-word-limit />
              <el-input
                v-model="form.templateName"
                placeholder="如:项目立项审批"
                maxlength="50"
                show-word-limit
                :disabled="isEditingBuiltin"
              />
            </el-form-item>
@@ -124,7 +130,12 @@
            <el-form-item label="模板类型" prop="businessType">
              <el-select v-model="form.businessType" placeholder="请选择" style="width: 100%">
              <el-select
                v-model="form.businessType"
                placeholder="请选择"
                style="width: 100%"
                :disabled="isEditingBuiltin"
              >
                <el-option
@@ -178,7 +189,12 @@
        <el-form-item label="填报配置">
          <FormConfigEditor v-model="form.formConfigData" :exclude-template-id="form.id" />
          <FormConfigEditor
            v-model="form.formConfigData"
            :exclude-template-id="form.id"
            :disable-import="isEditingBuiltin"
            :locked-field-uids="isEditingBuiltin ? form.lockedFormFieldUids : []"
          />
          <p class="flow-tip">配置提交审批时需填写的表单项,保存后写入 formConfig(JSON)。</p>
@@ -459,6 +475,8 @@
  formRules,
  isEditingBuiltin,
  detailDialog,
  detailRow,
src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
@@ -3,17 +3,18 @@
  deleteApprovalTemplate,
  getApprovalTemplateDetail,
  listApprovalTemplatePage,
  TEMPLATE_TYPE_CUSTOM,
  TEMPLATE_TYPE_BUILTIN,
  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 { computed, reactive, ref } from "vue";
import {
  buildApprovalTemplateListParams,
  createEmptyTemplateForm,
  fetchBusinessTypeOptions,
  flowNodesSummary,
  isBuiltinTemplate,
  mapTemplateFromApi,
  mapTemplateToApi,
  nodeSignModeLabel,
@@ -58,6 +59,10 @@
  const formDialog = reactive({ visible: false, title: "", mode: "add" });
  const form = reactive(createEmptyTemplateForm());
  const formRef = ref();
  const isEditingBuiltin = computed(
    () => formDialog.mode === "edit" && Number(form.templateType) === TEMPLATE_TYPE_BUILTIN
  );
  async function loadTemplateTypeOptions() {
    try {
@@ -140,6 +145,7 @@
          name: "删除",
          type: "danger",
          link: true,
          disabled: (row) => isBuiltinTemplate(row),
          clickFun: (row) => removeTemplate(row),
        },
      ],
@@ -186,16 +192,22 @@
      Object.assign(form, base);
      return;
    }
    const formConfigData = JSON.parse(
      JSON.stringify(row.formConfigData || parseFormConfigToData(row.formConfig))
    );
    const builtin = isBuiltinTemplate(row);
    Object.assign(form, {
      ...base,
      id: row.id,
      templateName: row.templateName || "",
      description: row.description || "",
      templateType: row.templateType != null ? Number(row.templateType) : base.templateType,
      businessType: row.businessType ?? "",
      formConfig: row.formConfig || "",
      formConfigData: JSON.parse(
        JSON.stringify(row.formConfigData || parseFormConfigToData(row.formConfig))
      ),
      formConfigData,
      lockedFormFieldUids: builtin
        ? (formConfigData.fields || []).map((f) => f._uid).filter(Boolean)
        : [],
      enabled: row.enabled !== false,
      flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [base.flowNodes[0]])),
      storageBlobDTOs: JSON.parse(JSON.stringify(row.storageBlobDTOs || [])),
@@ -258,6 +270,10 @@
  }
  async function removeTemplate(row) {
    if (isBuiltinTemplate(row)) {
      ElMessage.warning("系统内置模板不允许删除");
      return;
    }
    if (row?.id == null || row.id === "") {
      ElMessage.warning("无法删除:缺少模板 ID");
      return;
@@ -304,6 +320,7 @@
    form,
    formRef,
    formRules,
    isEditingBuiltin,
    detailDialog,
    detailRow,
    detailLoading,