技术文章

Vue3 + Iframe 实战:打造企业级流程配置中心

基于 Vue3、TypeScript 与 Ant Design Vue,搭建支持字段配置与流程设计器嵌入的企业级流程配置中心。

veg blog iOS 26 UI 技术内容

 Vue3 + Iframe 实战:打造企业级流程配置中心(附完整代码)

在 SaaS 化合同管理系统中,如何让用户自定义每个业务模块的审批流程?本文将带你从零开始,基于 Vue3 + TypeScript + Ant Design Vue ,实现一个支持“字段配置 + 流程设计”的一体化配置中心,并解决 Iframe 嵌入、状态回传、多来源返回等核心难题。


🎯 项目背景

我们的合同管理系统需要支持高度定制化:

  • 不同客户可启用/禁用不同功能模块(如“合同用印”、“合同变更”)
  • 每个模块的表单字段可自由配置显示/编辑/必填
  • 每个模块可独立配置审批流程图

为此,我们设计了「合同业务设置」页面,包含三个核心子功能:

  1. 功能模块开关 → 控制哪些模块可用
  2. 字段配置面板 → 控制每个字段的展示规则
  3. 流程设计器嵌入 → 通过 Iframe 集成第三方流程引擎

💡 核心架构设计

1. 页面结构概览

            
              <template> <div class="contract-settings"> <!-- 左侧:功能模块列表 --> <a-menu v-model:selectedKeys="selectedModule" mode="vertical"> <a-menu-item key="contract-new">合同新建</a-menu-item> <a-menu-item key="contract-seal">合同用印</a-menu-item> <!-- ...其他模块 --> </a-menu> <!-- 中间:字段配置区域 --> <div class="field-config"> <a-tabs v-model:activeKey="activeTab"> <a-tab-pane key="fields" tab="字段设置"> <FieldConfigTable :fields="currentFields" /> </a-tab-pane> <a-tab-pane key="other" tab="其他设置"> <OtherSettings :moduleId="selectedModule[0]" /> </a-tab-pane> </a-tabs> </div> <!-- 右侧:选项内容预览 --> <div class="preview-panel"> <Empty description="暂无选项内容" v-if="!hasOptions" /> <OptionList :opti v-else /> </div> </div> </template>
            
          

2. 功能模块开关组件

            
              <!-- components/ModuleSwitch.vue --> <template> <div class="module-switch"> <div v-for="module in modules" :key="module.id" class="module-item" :class="{ active: selectedModule.includes(module.key) }" @click="toggleModule(module.key)" > <span>{{ module.name }}</span> <a-switch :checked="module.enabled" @change="(val) => handleSwitchChange(module.id, val)" /> </div> </div> </template> <script setup lang="ts"> import { ref, watch } from 'vue' const props = defineProps<{ modules: Array<{ id: number; name: string; key: string; enabled: boolean }> }>() const emit = defineEmits(['update:selectedModules', 'switch-change']) const selectedModule = ref<string[]>([]) const toggleModule = (key: string) => { if (selectedModule.value.includes(key)) { selectedModule.value = selectedModule.value.filter(k => k !== key) } else { selectedModule.value.push(key) } emit('update:selectedModules', selectedModule.value) } const handleSwitchChange = (moduleId: number, enabled: boolean) => { emit('switch-change', { moduleId, enabled }) } </script> <style scoped> .module-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; cursor: pointer; border-radius: 4px; transition: all 0.3s; } .module-item.active { background: #e6f7ff; color: #1890ff; } </style>
            
          

3. 字段配置表格组件

            
              <!-- components/FieldConfigTable.vue --> <template> <a-table :columns="columns" :data-source="fields" row-key="id" :pagination="false" size="small" > <template #bodyCell="{ column, record }"> <template v-if="column.dataIndex === 'show'"> <a-checkbox :checked="record.show" @change="(e) => handleFieldChange(record.id, 'show', e.target.checked)" /> </template> <template v-else-if="column.dataIndex === 'edit'"> <a-checkbox :checked="record.editable" @change="(e) => handleFieldChange(record.id, 'editable', e.target.checked)" :disabled="!record.show" /> </template> <template v-else-if="column.dataIndex === 'required'"> <a-checkbox :checked="record.required" @change="(e) => handleFieldChange(record.id, 'required', e.target.checked)" :disabled="!record.show" /> </template> </template> </a-table> </template> <script setup lang="ts"> import type { TableColumnsType } from 'ant-design-vue' interface FieldConfig { id: number fieldName: string show: boolean editable: boolean required: boolean } const props = defineProps<{ fields: FieldConfig[] }>() const emit = defineEmits(['field-change']) const columns: TableColumnsType<FieldConfig> = [ { title: '字段名称', dataIndex: 'fieldName', key: 'fieldName' }, { title: '显示', dataIndex: 'show', key: 'show', width: 80 }, { title: '编辑', dataIndex: 'edit', key: 'edit', width: 80 }, { title: '必填', dataIndex: 'required', key: 'required', width: 80 } ] const handleFieldChange = (fieldId: number, field: keyof FieldConfig, value: boolean) => { emit('field-change', { fieldId, field, value }) } </script>
            
          

4. 流程配置入口与 Iframe 嵌入

            
              <!-- components/ProcessConfigEntry.vue --> <template> <div class="process-config-entry"> <div class="config-header"> <span>流程审批配置</span> <a-switch :checked="processEnabled" @change="handleSwitchChange" /> <a-button type="link" :disabled="!processEnabled" @click="openProcessDesigner" > {{ hasConfigured ? '已配置' : '去配置流程' }} </a-button> </div> </div> </template> <script setup lang="ts"> import { useRouter } from 'vue-router' const router = useRouter() const props = defineProps<{ processEnabled: boolean hasConfigured: boolean moduleId: string taskTypeId?: string }>() const emit = defineEmits(['switch-change']) const handleSwitchChange = (checked: boolean) => { emit('switch-change', checked) } const openProcessDesigner = () => { const query: Record<string, string> = { from: 'contract-settings', moduleId: props.moduleId, processEnabled: String(props.processEnabled) } if (props.taskTypeId) { query.taskTypeId = props.taskTypeId } if (props.hasConfigured) { // 编辑模式 - 需要从后端获取 processKey // 这里简化处理,实际应调用 API 获取 query.isCreate = '0' query.processKey = 'existing_process_key_123' // 示例 } else { query.isCreate = '1' } router.push({ path: '/setting/contract-settings/process-config', query }) } </script> <style scoped> .config-header { display: flex; align-items: center; gap: 16px; padding: 16px; background: #fafafa; border-radius: 4px; } </style>
            
          

5. 主页面逻辑整合

            
              <!-- views/ContractSettings.vue --> <template> <div class="contract-settings-page"> <a-card> <template #title> <div class="page-title"> <h2>合同业务设置</h2> <a-button type="primary" @click="saveAllConfig">保存配置</a-button> </div> </template> <div class="settings-container"> <!-- 左侧模块选择 --> <ModuleSwitch :modules="moduleList" v-model:selected-modules="selectedModules" @switch-change="handleModuleSwitch" /> <!-- 中间配置区 --> <div class="config-area"> <a-tabs v-model:activeKey="activeTab"> <a-tab-pane key="fields" tab="字段设置"> <FieldConfigTable :fields="currentFields" @field-change="handleFieldChange" /> </a-tab-pane> <a-tab-pane key="other" tab="其他设置"> <ProcessConfigEntry :process-enabled="currentProcessEnabled" :has-c :module-id="selectedModules[0]" @switch-change="handleProcessSwitch" /> <RichTextEditor v-model="businessInstruction" placeholder="请输入业务办理说明..." /> </a-tab-pane> </a-tabs> </div> <!-- 右侧预览 --> <div class="preview-area"> <h3>选项内容</h3> <Empty v-if="!currentOptions.length" description="暂无选项内容" /> <OptionList v-else :opti /> </div> </div> </a-card> </div> </template> <script setup lang="ts"> import { ref, computed, onMounted } from 'vue' import { message } from 'ant-design-vue' import ModuleSwitch from '@/components/ModuleSwitch.vue' import FieldConfigTable from '@/components/FieldConfigTable.vue' import ProcessConfigEntry from '@/components/ProcessConfigEntry.vue' import RichTextEditor from '@/components/RichTextEditor.vue' import OptionList from '@/components/OptionList.vue' import { FindContractModuleConfig, SaveContractModuleConfig } from '@/api/contract' // 状态定义 const moduleList = ref([ { id: 1, name: '合同新建', key: 'contract-new', enabled: true }, { id: 2, name: '合同用印', key: 'contract-seal', enabled: false }, { id: 3, name: '合同签订', key: 'contract-sign', enabled: true }, // ...更多模块 ]) const selectedModules = ref(['contract-new']) const activeTab = ref('fields') const currentFields = ref([]) const currentProcessEnabled = ref(false) const currentHasConfigured = ref(false) const businessInstruction = ref('') const currentOptions = ref([]) // 计算当前选中模块的配置 const currentModule = computed(() => moduleList.value.find(m => m.key === selectedModules.value[0]) ) // 加载配置 onMounted(async () => { await loadModuleConfig(selectedModules.value[0]) }) // 监听模块切换 watch(selectedModules, async (newVal) => { if (newVal.length > 0) { await loadModuleConfig(newVal[0]) } }) // 加载单个模块配置 const loadModuleConfig = async (moduleKey: string) => { try { const [err, res] = await to(FindContractModuleConfig({ moduleKey })) if (!err && res) { currentFields.value = res.fields || [] currentProcessEnabled.value = res.processEnabled || false currentHasConfigured.value = res.hasConfigured || false businessInstruction.value = res.businessInstruction || '' currentOptions.value = res.options || [] } } catch (error) { message.error('加载配置失败') } } // 处理字段变更 const handleFieldChange = ({ fieldId, field, value }: any) => { const fieldItem = currentFields.value.find(f => f.id === fieldId) if (fieldItem) { fieldItem[field] = value } } // 处理模块开关变更 const handleModuleSwitch = ({ moduleId, enabled }: any) => { const module = moduleList.value.find(m => m.id === moduleId) if (module) { module.enabled = enabled } } // 处理流程开关变更 const handleProcessSwitch = (enabled: boolean) => { currentProcessEnabled.value = enabled } // 保存所有配置 const saveAllConfig = async () => { try { const configData = { moduleKey: selectedModules.value[0], fields: currentFields.value, processEnabled: currentProcessEnabled.value, businessInstruction: businessInstruction.value, options: currentOptions.value } const [err] = await to(SaveContractModuleConfig(configData)) if (!err) { message.success('配置保存成功') } else { message.error('配置保存失败') } } catch (error) { message.error('配置保存异常') } } </script> <style scoped> .contract-settings-page { padding: 24px; } .page-title { display: flex; justify-content: space-between; align-items: center; } .settings-container { display: grid; grid-template-columns: 200px 1fr 300px; gap: 24px; margin-top: 24px; } .config-area { background: #fff; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.09); } .preview-area { background: #fafafa; border-radius: 4px; padding: 16px; } </style>
            
          

🔧 关键技术点详解

1. Iframe 动态加载与通信

ProcessConfig.vue 中,我们通过以下方式实现与第三方流程系统的无缝对接:

            
              // 注册全局回调函数 window.afterCreateProcessDesign = async (obj: { fKey: string, fName: string }) => { console.log('流程设计完成:', obj) const from = route.query.from as string if (from === 'contract-settings') { // 保存状态到 localStorage const state = { selectedModuleId: route.query.moduleId, processTypeId: route.query.processTypeId, isFromContractSettings: true, needReopen: true, timestamp: Date.now(), processKey: obj.fKey, approvalEnabled: route.query.processEnabled === 'true', configured: true } localStorage.setItem('contractSettingsState', JSON.stringify(state)) // 延迟返回 setTimeout(() => { router.push('/setting/contract-settings') }, 1000) } } // 动态创建 iframe const createIframe = (url: string, name: string) => { const iframeBox = document.getElementById('iframe-box') if (!iframeBox) return iframeBox.innerHTML = `<iframe style="width: 100%; border: 0; height: 800px;" scrolling="auto" src="${url}" name="${name}" />` // 监听加载完成 iframeBox.onload = () => { setTimeout(() => { // 填充流程名称 const iframeDoc = iframeBox.contentDocument || iframeBox.contentWindow?.document if (iframeDoc) { const input = iframeDoc.querySelector('#pname input') const display = iframeDoc.getElementById('flow-name') if (input && display) { input.value = route.query.flowName || '' display.textContent = route.query.flowName || '' } } }, 500) } }
            
          

2. 状态持久化策略

使用 localStorage 存储复杂状态,确保页面刷新或跳转后仍能恢复:

            
              // 保存状态 const saveState = (state: any) => { localStorage.setItem('contractSettingsState', JSON.stringify({ ...state, timestamp: Date.now() })) } // 恢复状态 const restoreState = () => { const stateStr = localStorage.getItem('contractSettingsState') if (!stateStr) return null const state = JSON.parse(stateStr) // 清理过期状态(超过1小时) if (Date.now() - state.timestamp > 3600000) { localStorage.removeItem('contractSettingsState') return null } return state }
            
          

3. 防抖与节流优化

对于频繁触发的配置变更,使用防抖避免重复请求:

            
              import { debounce } from 'lodash-es' // 创建防抖函数 const debouncedSave = debounce(async () => { await saveAllConfig() }, 1000) // 在字段变更时调用 const handleFieldChange = ({ fieldId, field, value }: any) => { // 更新本地状态 updateLocalState(fieldId, field, value) // 触发防抖保存 debouncedSave() }
            
          

✅ 总结

本方案实现了:

  • ✅ 模块化配置管理
  • ✅ 字段级权限控制
  • ✅ 第三方流程引擎无缝集成
  • ✅ 状态持久化与恢复
  • ✅ 优雅的用户体验

通过这套架构,我们可以轻松扩展到其他业务模块的配置管理,真正实现了“一次开发,多处复用”。

💡 提示 :在生产环境中,建议增加配置版本管理、操作日志记录、权限校验等功能,以提升系统的安全性和可追溯性。


技术栈 :Vue3 | TypeScript | Vite | Ant Design Vue | Vuex
适用场景 :SaaS 平台、低代码系统、企业级后台管理系统

如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!有任何问题也欢迎在评论区交流~