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;