资讯动态

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

发布时间:2026/8/24 0:37:35 来源:尧图企业网站定制
Vue3 Iframe 实战打造企业级流程配置中心附完整代码在 SaaS 化合同管理系统中如何让用户自定义每个业务模块的审批流程本文将带你从零开始基于Vue3 TypeScript Ant Design Vue实现一个支持“字段配置 流程设计”的一体化配置中心并解决 Iframe 嵌入、状态回传、多来源返回等核心难题。 项目背景我们的合同管理系统需要支持高度定制化不同客户可启用/禁用不同功能模块如“合同用印”、“合同变更”每个模块的表单字段可自由配置显示/编辑/必填每个模块可独立配置审批流程图为此我们设计了「合同业务设置」页面包含三个核心子功能功能模块开关→ 控制哪些模块可用字段配置面板→ 控制每个字段的展示规则流程设计器嵌入→ 通过 Iframe 集成第三方流程引擎 核心架构设计1. 页面结构概览template div classcontract-settings !-- 左侧功能模块列表 -- a-menu v-model:selectedKeysselectedModule modevertical a-menu-item keycontract-new合同新建/a-menu-item a-menu-item keycontract-seal合同用印/a-menu-item !-- ...其他模块 -- /a-menu !-- 中间字段配置区域 -- div classfield-config a-tabs v-model:activeKeyactiveTab a-tab-pane keyfields tab字段设置 FieldConfigTable :fieldscurrentFields / /a-tab-pane a-tab-pane keyother tab其他设置 OtherSettings :moduleIdselectedModule[0] / /a-tab-pane /a-tabs /div !-- 右侧选项内容预览 -- div classpreview-panel Empty description暂无选项内容 v-if!hasOptions / OptionList :optionscurrentOptions v-else / /div /div /template2. 功能模块开关组件!-- components/ModuleSwitch.vue -- template div classmodule-switch div v-formodule in modules :keymodule.id classmodule-item :class{ active: selectedModule.includes(module.key) } clicktoggleModule(module.key) span{{ module.name }}/span a-switch :checkedmodule.enabled change(val) handleSwitchChange(module.id, val) / /div /div /template script setup langts 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 refstring[]([]) 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; } /style3. 字段配置表格组件!-- components/FieldConfigTable.vue -- template a-table :columnscolumns :data-sourcefields row-keyid :paginationfalse sizesmall template #bodyCell{ column, record } template v-ifcolumn.dataIndex show a-checkbox :checkedrecord.show change(e) handleFieldChange(record.id, show, e.target.checked) / /template template v-else-ifcolumn.dataIndex edit a-checkbox :checkedrecord.editable change(e) handleFieldChange(record.id, editable, e.target.checked) :disabled!record.show / /template template v-else-ifcolumn.dataIndex required a-checkbox :checkedrecord.required change(e) handleFieldChange(record.id, required, e.target.checked) :disabled!record.show / /template /template /a-table /template script setup langts 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: TableColumnsTypeFieldConfig [ { 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 }) } /script4. 流程配置入口与 Iframe 嵌入!-- components/ProcessConfigEntry.vue -- template div classprocess-config-entry div classconfig-header span流程审批配置/span a-switch :checkedprocessEnabled changehandleSwitchChange / a-button typelink :disabled!processEnabled clickopenProcessDesigner {{ hasConfigured ? 已配置 : 去配置流程 }} /a-button /div /div /template script setup langts 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: Recordstring, 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; } /style5. 主页面逻辑整合!-- views/ContractSettings.vue -- template div classcontract-settings-page a-card template #title div classpage-title h2合同业务设置/h2 a-button typeprimary clicksaveAllConfig保存配置/a-button /div /template div classsettings-container !-- 左侧模块选择 -- ModuleSwitch :modulesmoduleList v-model:selected-modulesselectedModules switch-changehandleModuleSwitch / !-- 中间配置区 -- div classconfig-area a-tabs v-model:activeKeyactiveTab a-tab-pane keyfields tab字段设置 FieldConfigTable :fieldscurrentFields field-changehandleFieldChange / /a-tab-pane a-tab-pane keyother tab其他设置 ProcessConfigEntry :process-enabledcurrentProcessEnabled :has-configuredcurrentHasConfigured :module-idselectedModules[0] switch-changehandleProcessSwitch / RichTextEditor v-modelbusinessInstruction placeholder请输入业务办理说明... / /a-tab-pane /a-tabs /div !-- 右侧预览 -- div classpreview-area h3选项内容/h3 Empty v-if!currentOptions.length description暂无选项内容 / OptionList v-else :optionscurrentOptions / /div /div /a-card /div /template script setup langts 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 stylewidth: 100%; border: 0; height: 800px; scrollingauto 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 平台、低代码系统、企业级后台管理系统如果你觉得这篇文章对你有帮助欢迎点赞、收藏、转发有任何问题也欢迎在评论区交流

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价