From 69b917fa605be8ccd0984e5c095f24d6476dce95 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期五, 05 六月 2026 00:55:46 +0800
Subject: [PATCH] 1

---
 src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue |  857 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 857 insertions(+), 0 deletions(-)

diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
new file mode 100644
index 0000000..6880f3f
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
@@ -0,0 +1,857 @@
+<!-- 瀹℃壒妯℃澘锛氬彲閰嶇疆濉姤椤癸紝搴忓垪鍖栧埌 formConfig -->
+<template>
+  <div class="fce">
+    <div class="fce-hint">
+      <span class="fce-hint-label">濉姤鎻愮ず</span>
+      <el-input
+        v-model="inner.summaryPlaceholder"
+        placeholder="濡傦細璇峰~鍐欐姤閿�浜嬬敱銆侀噾棰濈瓑"
+        maxlength="200"
+        show-word-limit
+        @input="emitOut"
+      />
+    </div>
+
+    <div class="fce-panel">
+      <div class="fce-toolbar">
+        <div class="fce-toolbar-left">
+          <span class="fce-title">濉姤椤归厤缃�</span>
+          <el-tag v-if="inner.fields.length" size="small" type="info" effect="plain">
+            鍏� {{ inner.fields.length }} 椤�
+          </el-tag>
+        </div>
+        <div class="fce-toolbar-actions">
+          <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>
+                  鏆傛棤鍏朵粬瀹℃壒妯℃澘
+                </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>
+          </el-dropdown>
+          <el-button type="primary" size="small" :icon="Plus" @click="addField">娣诲姞濉姤椤�</el-button>
+        </div>
+      </div>
+
+      <el-empty
+        v-if="!inner.fields.length"
+        class="fce-empty"
+        description="鏆傛棤濉姤椤癸紝鍙坊鍔犳垨浠庡凡鏈夊鎵规ā鏉垮鍏�"
+        :image-size="72"
+      />
+
+      <div v-else class="fce-list">
+        <div
+          v-for="(field, index) in inner.fields"
+          :key="field._uid"
+          class="fce-card"
+          :class="{
+            'fce-card--required': field.required,
+            'fce-card--locked': isFieldLocked(field),
+          }"
+        >
+          <div class="fce-card-badge">{{ index + 1 }}</div>
+
+          <div class="fce-card-head">
+            <div class="fce-card-title">
+              <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 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>
+                </el-button>
+              </el-tooltip>
+              <el-tooltip content="涓嬬Щ" placement="top">
+                <el-button
+                  circle
+                  size="small"
+                  :disabled="index >= inner.fields.length - 1"
+                  @click="moveField(index, 1)"
+                >
+                  <el-icon><Bottom /></el-icon>
+                </el-button>
+              </el-tooltip>
+              <el-tooltip content="鍒犻櫎" placement="top">
+                <el-button circle size="small" type="danger" plain @click="removeField(index)">
+                  <el-icon><Delete /></el-icon>
+                </el-button>
+              </el-tooltip>
+            </div>
+          </div>
+
+          <div class="fce-section">
+            <span class="fce-section-title">鍩虹淇℃伅</span>
+            <el-row :gutter="16">
+              <el-col :span="8">
+                <el-form-item label="鏄剧ず鍚嶇О" required class="fce-field-item">
+                  <el-input
+                    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"
+                    :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%"
+                    :disabled="isFieldLocked(field)"
+                    @change="onTypeChange(field)"
+                  >
+                    <el-option
+                      v-for="t in FORM_FIELD_TYPE_OPTIONS"
+                      :key="t.value"
+                      :label="t.label"
+                      :value="t.value"
+                    />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </div>
+
+          <div class="fce-section">
+            <span class="fce-section-title">鏍¢獙涓庢牸寮�</span>
+            <el-row :gutter="16" align="middle">
+              <el-col :span="8">
+                <el-form-item label="鏄惁蹇呭~" class="fce-field-item fce-field-item--switch">
+                  <el-switch
+                    v-model="field.required"
+                    inline-prompt
+                    active-text="蹇呭~"
+                    inactive-text="閫夊~"
+                    :disabled="isFieldLocked(field)"
+                    @change="emitOut"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col v-if="field.type === 'textarea'" :span="8">
+                <el-form-item label="琛屾暟" class="fce-field-item">
+                  <el-input-number
+                    v-model="field.rows"
+                    :min="1"
+                    :max="10"
+                    controls-position="right"
+                    style="width: 100%"
+                    :disabled="isFieldLocked(field)"
+                    @change="emitOut"
+                  />
+                </el-form-item>
+              </el-col>
+              <template v-if="field.type === 'number'">
+                <el-col :span="8">
+                  <el-form-item label="鏈�灏忓��" class="fce-field-item">
+                    <el-input-number
+                      v-model="field.min"
+                      controls-position="right"
+                      style="width: 100%"
+                      :disabled="isFieldLocked(field)"
+                      @change="emitOut"
+                    />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="灏忔暟浣�" class="fce-field-item">
+                    <el-input-number
+                      v-model="field.precision"
+                      :min="0"
+                      :max="4"
+                      controls-position="right"
+                      style="width: 100%"
+                      :disabled="isFieldLocked(field)"
+                      @change="emitOut"
+                    />
+                  </el-form-item>
+                </el-col>
+              </template>
+            </el-row>
+          </div>
+
+          <div class="fce-section fce-section--default">
+            <span class="fce-section-title">榛樿鍊�</span>
+            <p class="fce-section-desc">閫夋嫨璇ユā鏉挎彁浜ゅ鎵规椂锛屽皢鑷姩棰勫~浠ヤ笅鍐呭锛堢敤鎴蜂粛鍙慨鏀癸級</p>
+            <el-input
+              v-if="field.type === 'text' || field.type === 'textarea'"
+              v-model="field.defaultValue"
+              :type="field.type === 'textarea' ? 'textarea' : 'text'"
+              :rows="field.type === 'textarea' ? 2 : undefined"
+              :placeholder="defaultPlaceholder(field)"
+              :disabled="isFieldLocked(field)"
+              clearable
+              @input="emitOut"
+            />
+            <el-input-number
+              v-else-if="field.type === 'number'"
+              v-model="field.defaultValue"
+              :min="field.min"
+              :precision="field.precision ?? 0"
+              controls-position="right"
+              placeholder="閫夊~"
+              style="width: 100%"
+              :disabled="isFieldLocked(field)"
+              @change="emitOut"
+            />
+            <el-date-picker
+              v-else-if="field.type === 'date'"
+              v-model="field.defaultValue"
+              type="date"
+              placeholder="閫夊~"
+              format="YYYY-MM-DD"
+              value-format="YYYY-MM-DD"
+              style="width: 100%"
+              :disabled="isFieldLocked(field)"
+              clearable
+              @change="emitOut"
+            />
+            <el-date-picker
+              v-else-if="field.type === 'datetimerange'"
+              v-model="field.defaultValue"
+              type="datetimerange"
+              range-separator="鑷�"
+              start-placeholder="寮�濮嬫椂闂�"
+              end-placeholder="缁撴潫鏃堕棿"
+              format="YYYY-MM-DD HH:mm:ss"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              style="width: 100%"
+              :disabled="isFieldLocked(field)"
+              clearable
+              @change="emitOut"
+            />
+            <el-select
+              v-else-if="field.type === 'select'"
+              v-model="field.defaultValue"
+              placeholder="閫夊~"
+              style="width: 100%"
+              clearable
+              filterable
+              :loading="optionSourceLoading"
+              :disabled="isFieldLocked(field)"
+              @change="emitOut"
+            >
+              <el-option
+                v-for="o in resolvedSelectOptions(field)"
+                :key="String(o.value)"
+                :label="o.label || o.value"
+                :value="o.value"
+              />
+            </el-select>
+          </div>
+
+          <div v-if="field.type === 'select'" class="fce-section fce-section--options">
+            <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%"
+                    :disabled="isFieldLocked(field)"
+                    @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"
+                  :disabled="isFieldLocked(field)"
+                  @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="鏄剧ず鏂囨湰"
+                  :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="isFieldLocked(field) || field.options.length <= 1"
+                  @click="removeOption(field, oi)"
+                />
+              </div>
+            </template>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { Bottom, Delete, Plus, Top } from "@element-plus/icons-vue";
+import {
+  getApprovalTemplateDetail,
+  listApprovalTemplate,
+  TEMPLATE_TYPE_BUILTIN,
+  TEMPLATE_TYPE_CUSTOM,
+} from "@/api/officeProcessAutomation/approvalTemplate.js";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { computed, reactive, ref, watch } from "vue";
+import {
+  mapEnabledFromApi,
+  unwrapTemplateDetail,
+  unwrapTemplateList,
+} from "../approveTemplateConstants.js";
+import {
+  FORM_FIELD_TYPE_OPTIONS,
+  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 },
+  /** 绂佺敤銆屼粠宸叉湁妯℃澘瀵煎叆銆� */
+  disableImport: { type: Boolean, default: false },
+  /** 绯荤粺鍐呯疆妯℃澘缂栬緫鏃讹紝鎵撳紑寮圭獥鍗冲瓨鍦ㄧ殑濉姤椤� _uid锛屼笉鍙敼鍒� */
+  lockedFieldUids: { type: Array, default: () => [] },
+});
+
+const emit = defineEmits(["update:modelValue"]);
+
+const inner = reactive(createEmptyFormConfigData());
+
+const { loading: optionSourceLoading, ensureForFields, getOptions } = useSelectOptionSources();
+
+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);
+}
+
+function defaultPlaceholder(field) {
+  const name = field.label || "璇ュ瓧娈�";
+  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 || "";
+  inner.fields = (src.fields || []).map((f) => ({
+    ...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() {
+  emit("update:modelValue", {
+    summaryPlaceholder: inner.summaryPlaceholder,
+    fields: inner.fields.map((f) => ({
+      _uid: f._uid,
+      key: f.key,
+      label: f.label,
+      type: f.type,
+      required: f.required,
+      rows: f.rows,
+      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 })),
+    })),
+  });
+}
+
+function cloneDefaultValue(f) {
+  if (f.type === "datetimerange" && Array.isArray(f.defaultValue)) {
+    return [...f.defaultValue];
+  }
+  return f.defaultValue;
+}
+
+watch(
+  () => props.modelValue,
+  (v) => syncFromProps(v),
+  { deep: true, immediate: true }
+);
+
+function addField() {
+  inner.fields.push(createEmptyFormField());
+  ensureForFields(inner.fields);
+  emitOut();
+}
+
+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;
+  emitOut();
+}
+
+function resetDefaultValueForType(field) {
+  if (field.type === "number") field.defaultValue = undefined;
+  else if (field.type === "datetimerange") field.defaultValue = [];
+  else field.defaultValue = "";
+}
+
+function onTypeChange(field) {
+  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();
+}
+
+function addOption(field) {
+  field.options.push({ label: "", value: "" });
+  emitOut();
+}
+
+function removeOption(field, oi) {
+  if (field.options.length <= 1) return;
+  field.options.splice(oi, 1);
+  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 (props.disableImport) return;
+  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>
+
+<style scoped>
+.fce {
+  width: 100%;
+}
+
+.fce-hint {
+  padding: 14px 16px;
+  margin-bottom: 14px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, var(--el-color-primary-light-9) 0%, var(--el-fill-color-blank) 100%);
+  border: 1px solid var(--el-color-primary-light-7);
+}
+
+.fce-hint-label {
+  display: block;
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  margin-bottom: 8px;
+}
+
+.fce-panel {
+  padding: 16px;
+  border-radius: 12px;
+  background: var(--el-fill-color-lighter);
+  border: 1px solid var(--el-border-color-lighter);
+}
+
+.fce-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  gap: 12px;
+  margin-bottom: 16px;
+}
+
+.fce-toolbar-left {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.fce-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.fce-toolbar-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.import-tag {
+  margin-left: 8px;
+  vertical-align: middle;
+}
+
+.fce-empty {
+  padding: 24px 0;
+}
+
+.fce-list {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+.fce-card {
+  position: relative;
+  padding: 16px 16px 12px;
+  border-radius: 12px;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
+  transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.fce-card:hover {
+  border-color: var(--el-color-primary-light-5);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
+}
+
+.fce-card--required {
+  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;
+  left: 16px;
+  min-width: 22px;
+  height: 22px;
+  padding: 0 6px;
+  border-radius: 11px;
+  background: var(--el-color-primary);
+  color: #fff;
+  font-size: 12px;
+  font-weight: 700;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 2px 6px rgba(var(--el-color-primary-rgb), 0.35);
+}
+
+.fce-card-head {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 14px;
+  padding-top: 4px;
+}
+
+.fce-card-title {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 8px;
+  min-width: 0;
+}
+
+.fce-card-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.fce-card-btns {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  flex-shrink: 0;
+}
+
+.fce-section {
+  margin-bottom: 12px;
+  padding-bottom: 12px;
+  border-bottom: 1px dashed var(--el-border-color-extra-light);
+}
+
+.fce-section:last-child {
+  margin-bottom: 0;
+  padding-bottom: 0;
+  border-bottom: none;
+}
+
+.fce-section-title {
+  display: block;
+  font-size: 12px;
+  font-weight: 600;
+  color: var(--el-text-color-secondary);
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  margin-bottom: 10px;
+}
+
+.fce-section-desc {
+  margin: -6px 0 10px;
+  font-size: 12px;
+  color: var(--el-text-color-placeholder);
+  line-height: 1.5;
+}
+
+.fce-section--default {
+  padding: 12px 14px;
+  border-radius: 8px;
+  background: var(--el-fill-color-lighter);
+  border-bottom: none;
+  margin-bottom: 0;
+}
+
+.fce-section--default .fce-section-title {
+  margin-bottom: 4px;
+  color: var(--el-color-primary);
+  text-transform: none;
+  letter-spacing: 0;
+  font-size: 13px;
+}
+
+.fce-section--options {
+  padding-top: 4px;
+  border-bottom: none;
+  margin-bottom: 0;
+}
+
+.fce-field-item {
+  margin-bottom: 0;
+}
+
+.fce-field-item :deep(.el-form-item__label) {
+  font-size: 13px;
+  color: var(--el-text-color-regular);
+}
+
+.fce-field-item--switch :deep(.el-form-item__content) {
+  line-height: 32px;
+}
+
+.fce-options-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 10px;
+}
+
+.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;
+  gap: 10px;
+  margin-bottom: 8px;
+  padding: 8px 10px;
+  border-radius: 8px;
+  background: var(--el-fill-color-lighter);
+}
+
+.fce-option-row:last-child {
+  margin-bottom: 0;
+}
+
+.fce-option-index {
+  flex-shrink: 0;
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  background: var(--el-color-info-light-8);
+  color: var(--el-text-color-secondary);
+  font-size: 11px;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.fce-option-value {
+  width: 140px;
+  flex-shrink: 0;
+}
+</style>

--
Gitblit v1.9.3