yyb
9 小时以前 efc0c3a697969503634138d7881543f4099b81ca
src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
@@ -21,12 +21,20 @@
          </el-tag>
        </div>
        <div class="fce-toolbar-actions">
          <el-dropdown trigger="click" @command="applyPreset">
            <el-button size="small">从预设导入</el-button>
          <el-dropdown trigger="click" @visible-change="onImportDropdownVisible" @command="importFromTemplate">
            <el-button size="small" :loading="templateImportLoading">从已有模板导入</el-button>
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item v-for="p in FORM_CONFIG_PRESETS" :key="p.key" :command="p.key">
                  {{ p.label }}
                <el-dropdown-item v-if="!templateImportOptions.length" disabled>
                  暂无其他审批模板
                </el-dropdown-item>
                <el-dropdown-item
                  v-for="t in templateImportOptions"
                  :key="t.id"
                  :command="t.id"
                >
                  <span>{{ t.label }}</span>
                  <el-tag v-if="!t.enabled" size="small" type="info" class="import-tag">已停用</el-tag>
                </el-dropdown-item>
              </el-dropdown-menu>
            </template>
@@ -38,7 +46,7 @@
      <el-empty
        v-if="!inner.fields.length"
        class="fce-empty"
        description="暂无填报项,可添加或从预设快速导入"
        description="暂无填报项,可添加或从已有审批模板导入"
        :image-size="72"
      />
@@ -219,10 +227,12 @@
              placeholder="选填"
              style="width: 100%"
              clearable
              filterable
              :loading="optionSourceLoading"
              @change="emitOut"
            >
              <el-option
                v-for="o in field.options.filter((x) => x.value !== '' && x.value != null)"
                v-for="o in resolvedSelectOptions(field)"
                :key="String(o.value)"
                :label="o.label || o.value"
                :value="o.value"
@@ -231,28 +241,52 @@
          </div>
          <div v-if="field.type === 'select'" class="fce-section fce-section--options">
            <div class="fce-options-head">
              <span class="fce-section-title">下拉选项</span>
              <el-button type="primary" link size="small" :icon="Plus" @click="addOption(field)">
                添加选项
              </el-button>
            </div>
            <div
              v-for="(opt, oi) in field.options"
              :key="oi"
              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-button
                type="danger"
                link
                :icon="Delete"
                :disabled="field.options.length <= 1"
                @click="removeOption(field, oi)"
              />
            </div>
            <span class="fce-section-title">下拉选项</span>
            <el-row :gutter="16" class="fce-source-row">
              <el-col :span="12">
                <el-form-item label="选项来源" class="fce-field-item">
                  <el-select
                    v-model="field.optionSource"
                    style="width: 100%"
                    @change="onOptionSourceChange(field)"
                  >
                    <el-option
                      v-for="s in SELECT_OPTION_SOURCE_OPTIONS"
                      :key="s.value"
                      :label="s.label"
                      :value="s.value"
                    />
                  </el-select>
                </el-form-item>
              </el-col>
            </el-row>
            <p v-if="isDynamicOptionSource(field.optionSource)" class="fce-source-tip">
              {{ optionSourceDesc(field.optionSource) }}。提交审批时将自动加载最新数据,无需手动维护选项。
            </p>
            <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>
              </div>
              <div
                v-for="(opt, oi) in field.options"
                :key="oi"
                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-button
                  type="danger"
                  link
                  :icon="Delete"
                  :disabled="field.options.length <= 1"
                  @click="removeOption(field, oi)"
                />
              </div>
            </template>
          </div>
        </div>
      </div>
@@ -262,23 +296,47 @@
<script setup>
import { Bottom, Delete, Plus, Top } from "@element-plus/icons-vue";
import { reactive, watch } from "vue";
import {
  FORM_CONFIG_PRESETS,
  getApprovalTemplateDetail,
  listApprovalTemplate,
  TEMPLATE_TYPE_BUILTIN,
  TEMPLATE_TYPE_CUSTOM,
} from "@/api/officeProcessAutomation/approvalTemplate.js";
import { ElMessage, ElMessageBox } from "element-plus";
import { reactive, ref, watch } from "vue";
import {
  mapEnabledFromApi,
  unwrapTemplateDetail,
  unwrapTemplateList,
} from "../approveTemplateConstants.js";
import {
  FORM_FIELD_TYPE_OPTIONS,
  applyFormConfigPreset,
  createEmptyFormConfigData,
  createEmptyFormField,
  formFieldTypeLabel,
  parseFormConfigToData,
} from "../formConfigUtils.js";
import {
  SELECT_OPTION_SOURCE,
  SELECT_OPTION_SOURCE_OPTIONS,
  isDynamicOptionSource,
} from "../selectOptionSource.js";
import { useSelectOptionSources } from "../useSelectOptionSources.js";
const props = defineProps({
  modelValue: { type: Object, default: () => createEmptyFormConfigData() },
  /** 编辑当前模板时排除自身,避免从自己导入 */
  excludeTemplateId: { type: [String, Number], default: null },
});
const emit = defineEmits(["update:modelValue"]);
const inner = reactive(createEmptyFormConfigData());
const { loading: optionSourceLoading, ensureForFields, getOptions } = useSelectOptionSources();
const templateImportOptions = ref([]);
const templateImportLoading = ref(false);
function typeLabel(type) {
  return formFieldTypeLabel(type);
@@ -289,6 +347,15 @@
  return `选填,选择模板时将预填${name}`;
}
function optionSourceDesc(source) {
  return SELECT_OPTION_SOURCE_OPTIONS.find((x) => x.value === source)?.desc || "";
}
function resolvedSelectOptions(field) {
  if (field.type !== "select") return [];
  return getOptions(field);
}
function syncFromProps(v) {
  const src = v || createEmptyFormConfigData();
  inner.summaryPlaceholder = src.summaryPlaceholder || "";
@@ -296,8 +363,10 @@
    ...createEmptyFormField(),
    ...f,
    _uid: f._uid || createEmptyFormField()._uid,
    optionSource: f.optionSource || SELECT_OPTION_SOURCE.STATIC,
    options: (f.options || [{ label: "", value: "" }]).map((o) => ({ ...o })),
  }));
  ensureForFields(inner.fields);
}
function emitOut() {
@@ -313,6 +382,7 @@
      min: f.min,
      precision: f.precision,
      defaultValue: cloneDefaultValue(f),
      optionSource: f.optionSource || SELECT_OPTION_SOURCE.STATIC,
      options: (f.options || []).map((o) => ({ label: o.label, value: o.value })),
    })),
  });
@@ -333,6 +403,7 @@
function addField() {
  inner.fields.push(createEmptyFormField());
  ensureForFields(inner.fields);
  emitOut();
}
@@ -357,10 +428,23 @@
}
function onTypeChange(field) {
  if (field.type === "select" && (!field.options || !field.options.length)) {
    field.options = [{ label: "", value: "" }];
  if (field.type === "select") {
    if (!field.optionSource) field.optionSource = SELECT_OPTION_SOURCE.STATIC;
    if (!field.options || !field.options.length) {
      field.options = [{ label: "", value: "" }];
    }
    ensureForFields(inner.fields);
  }
  resetDefaultValueForType(field);
  emitOut();
}
function onOptionSourceChange(field) {
  field.defaultValue = "";
  if (!isDynamicOptionSource(field.optionSource) && (!field.options || !field.options.length)) {
    field.options = [{ label: "", value: "" }];
  }
  ensureForFields(inner.fields);
  emitOut();
}
@@ -375,10 +459,66 @@
  emitOut();
}
function applyPreset(key) {
  const data = applyFormConfigPreset(key);
  syncFromProps(data);
  emitOut();
async function loadTemplateImportOptions() {
  templateImportLoading.value = true;
  try {
    const [customRes, builtinRes] = await Promise.all([
      listApprovalTemplate(TEMPLATE_TYPE_CUSTOM),
      listApprovalTemplate(TEMPLATE_TYPE_BUILTIN),
    ]);
    const excludeId =
      props.excludeTemplateId != null && props.excludeTemplateId !== ""
        ? String(props.excludeTemplateId)
        : "";
    templateImportOptions.value = [...unwrapTemplateList(customRes), ...unwrapTemplateList(builtinRes)]
      .filter((row) => row?.id != null && String(row.id) !== excludeId)
      .map((row) => ({
        id: row.id,
        label: row.templateName || `模板 #${row.id}`,
        enabled: mapEnabledFromApi(row.enabled),
      }));
  } catch {
    templateImportOptions.value = [];
    ElMessage.error("加载审批模板列表失败");
  } finally {
    templateImportLoading.value = false;
  }
}
function onImportDropdownVisible(visible) {
  if (visible) loadTemplateImportOptions();
}
async function importFromTemplate(templateId) {
  if (!templateId) return;
  if (inner.fields.length) {
    try {
      await ElMessageBox.confirm("将覆盖当前填报项配置,是否继续?", "从模板导入", {
        type: "warning",
        confirmButtonText: "继续导入",
        cancelButtonText: "取消",
      });
    } catch {
      return;
    }
  }
  templateImportLoading.value = true;
  try {
    const res = await getApprovalTemplateDetail(templateId);
    const row = unwrapTemplateDetail(res);
    const data = parseFormConfigToData(row?.formConfig);
    if (!data.fields?.length) {
      ElMessage.warning("该模板未配置填报项");
      return;
    }
    syncFromProps(data);
    emitOut();
    ElMessage.success(`已导入「${row.templateName || "模板"}」的填报项`);
  } catch {
    ElMessage.error("加载模板详情失败");
  } finally {
    templateImportLoading.value = false;
  }
}
</script>
@@ -435,6 +575,10 @@
  display: flex;
  align-items: center;
  gap: 8px;
}
.import-tag {
  margin-left: 8px;
  vertical-align: middle;
}
.fce-empty {
@@ -585,10 +729,28 @@
  margin-bottom: 10px;
}
.fce-options-head .fce-section-title {
.fce-options-head .fce-section-title,
.fce-options-head .fce-section-subtitle {
  margin-bottom: 0;
}
.fce-section-subtitle {
  font-size: 12px;
  font-weight: 600;
  color: var(--el-text-color-secondary);
}
.fce-source-row {
  margin-bottom: 4px;
}
.fce-source-tip {
  margin: 0 0 10px;
  font-size: 12px;
  color: var(--el-text-color-secondary);
  line-height: 1.5;
}
.fce-option-row {
  display: flex;
  align-items: center;