| | |
| | | </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> |
| | |
| | | <el-empty |
| | | v-if="!inner.fields.length" |
| | | class="fce-empty" |
| | | description="暂无填报项,可添加或从预设快速导入" |
| | | description="暂无填报项,可添加或从已有审批模板导入" |
| | | :image-size="72" |
| | | /> |
| | | |
| | |
| | | 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" |
| | |
| | | </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> |
| | |
| | | |
| | | <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); |
| | |
| | | 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 || ""; |
| | |
| | | ...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() { |
| | |
| | | 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 addField() { |
| | | inner.fields.push(createEmptyFormField()); |
| | | ensureForFields(inner.fields); |
| | | emitOut(); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | 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(); |
| | | } |
| | | |
| | |
| | | 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> |
| | | |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .import-tag { |
| | | margin-left: 8px; |
| | | vertical-align: middle; |
| | | } |
| | | |
| | | .fce-empty { |
| | |
| | | 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; |