十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue3+TypeScript 企业级前端业务组件封装:Schema、字段权限与附件双向绑定

Vue3+TypeScript 企业级前端业务组件封装:Schema、字段权限与附件双向绑定 Vue3TypeScript 企业级前端业务组件封装Schema、字段权限与附件双向绑定文档地址https://ruoyioffice.com源码1·GitHubhttps://github.com/yuqing2026/ruoyi-office源码2·GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office源码3·Giteehttps://gitee.com/yqzy1688/ruoyi-office微信17156169080备注「RuoYi Office」真正难复用的不是输入框而是“审批中的输入框”同一字段在发起人、审批人、已办查看、抄送查看中可能分别是必填、可写、只读和隐藏附件还要同时支持上传、预览、下载、删除与只读切换。本文不讲简单的 UI 包装而是拆解一套可落地的 Vue3TypeScript 企业级业务组件模型。▲ 组件封装的核心不是把模板搬进公共目录而是把 Schema、流程状态和字段权限合并成一个确定的渲染结果引言为什么“把代码复制成组件”仍然会失控业务团队通常从一个朴素方案开始把表头、表单、附件表格和底部按钮抽成几个.vue文件。前十个页面看起来很顺利到第五十个页面时却会出现四类问题表象根因最终后果每个页面都传十几个布尔值组件只抽了 DOM没有抽状态模型Props 组合爆炸边界不可推理disabled被 watcher 反复覆盖页面状态、字段权限、控件自身规则没有优先级审批人该填的字段填不了父组件回填一次用户刚输的值消失整包setValues覆盖了表单内部状态低频随机丢数据最难排查附件删不掉或删错行表格克隆行对象代码却用对象引用比较上传、编辑、回显后行为不一致所以企业级组件要解决的不是“少写几行模板”而是三个更深的问题契约稳定业务页只传领域数据不感知组件内部实现策略可合并页面状态、流程权限和字段自身规则有明确优先级数据不互相踩踏外部回填、内部编辑、异步附件各走可预测的数据通道。一、先分层原子组件、平台组件与业务组件不能混在一起1.1 三层职责层次典型内容应该知道什么不应该知道什么UI 原子层Input、Select、DatePicker、Table样式、交互、可访问性单据、审批、权限平台能力层Schema Form、Grid、Modal、File Preview通用数据驱动能力某张业务表企业业务层BasicForm、AttachmentList、审批轨迹单据状态、字段权限、流程上下文用印或应收的特有字段BasicForm的定位是企业业务层的“页面骨架”。它统一表头、Tab、流程进度、审批记录、Schema 表单和底部动作但不负责“印章怎么选”“应收金额怎么算”。后者仍留在业务页面。这条边界非常重要复用稳定机制不复用偶然业务。1.2 一个页面只保留三类代码以用印申请为例业务页主要保留SealApplyBill领域类型与接口调用useFormSchema()描述字段、控件和校验印章选择弹窗等模块特有交互。表头、审批 Tab、撤回/取回、字段权限和附件表格都交给公共组件。▲ 用印申请单业务字段各不相同但表头、状态、Tab、分区标题、附件和底部动作保持一致二、TypeScript 契约先把组件“允许什么”说清楚2.1 Props 不等于参数越多越灵活一个可维护的组件契约应围绕稳定概念组织而不是把每个按钮都变成开关。interfaceProps{headerData?:HeaderData;formData?:Recordstring,any;formSchema?:VbenFormSchema[];fieldPermission?:BusinessFieldPermission;processDefinitionKey?:string;processStatus?:number;viewType?:string;isApproval?:boolean;disabled?:boolean;columns?:number;}这里的参数可以分成四组参数组代表字段作用单据上下文headerData单号、标题、申请人、组织、流程实例表单描述formData、formSchema数据与结构分离权限上下文fieldPermission、isApproval当前节点能看什么、改什么页面状态viewType、processStatus、disabled新建、待办、已办、抄送等视图相比“隐藏提交按钮、隐藏撤回按钮、隐藏删除按钮……”无限增长状态上下文能让组件自己推导大部分 UI。少数业务差异再通过hideXxx兜底不让例外反过来主导模型。2.2 Emits 只表达业务意图组件不应该直接调用某个业务模块的保存接口。它只发出稳定的意图constemitdefineEmits([close,save,submit,revoke,reCreate,delete,withdraw,valuesChange,]);submit不是一个裸点击事件它还会携带提交原因和发起人自选审批人emit(submit,{reason:submitReasonForm.value.reason,startUserSelectAssignees:submitAssignees.value,});这样做有两个收益组件掌握通用审批交互业务页不重复造弹窗最终 API 调用仍由业务页负责组件不会耦合具体模块。2.3defineExpose是受控能力口不是把内部全部暴露父页面确实需要在提交前调用校验、获取表单值或者审批后刷新轨迹。可显式暴露最小能力defineExpose({refreshAllData,asyncgetFormValues(){returnformApi?awaitformApi.getValues():{};},asyncvalidateForm(){returnformApi?awaitformApi.validate():{valid:true};},asyncsetFormValues(values:any,shouldValidatefalse){returnformApi?awaitformApi.setValues(values,true,shouldValidate):undefined;},});不要暴露整个formApi。否则业务页面会绕过组件规则直接改 Schema、清校验、改禁用状态公共组件很快只剩一层壳。三、Schema 不是配置表而是字段策略的中间表示3.1 为什么要用 Schema传统模板把字段结构、控件、规则和布局散落在template中。Schema 把这些信息变成数据{fieldName:cause,label:用章事由,component:Textarea,rules:required,componentProps:{rows:3,placeholder:请输入用章事由,},formItemClass:col-span-4,}一旦字段变成数据组件才能统一施加权限、只读、隐藏、必填和布局策略。3.2 权限合并需要明确优先级最终字段状态不是简单的disabled props.disabled。它至少有五个来源抄送、已办等视图强制只读页面显式disabled流程节点配置的字段权限字段自身componentProps.disabled字段动态函数根据其他值计算出的禁用状态。推荐优先级是字段自身强制禁用 流程节点 HIDDEN / READONLY / EDITABLE / REQUIRED 页面全局只读 默认 Schema组件中的关键逻辑如下constresolvedSchemacomputedVbenFormSchema[]((){returnprops.formSchema.map((schema){constfieldNameschema.fieldName;consthiddenisFieldHidden(fieldName);constrequiredisFieldRequired(fieldName);constownDisabledschema.componentProps?.disabledtrue;constfieldDisabledhidden?effectiveDisabled.value:ownDisabled?true:isFieldEditable(fieldName)?false:isFieldReadonly(fieldName)?true:effectiveDisabled.value;return{...schema,hide:hidden||!!schema.hide,rules:required?required:hidden?undefined:schema.rules,componentProps:{...schema.componentProps,disabled:fieldDisabled},};});});最容易被忽略的一点是EDITABLE必须能覆盖页面的普通只读状态否则审批节点配置了“允许审批人补充字段”实际页面仍然不可编辑。3.3 函数型componentProps不能被静态合并破坏有些字段会根据其他字段动态返回组件属性componentProps:(values)({disabled:values.useType!2,options:buildOptions(values.companyId),})这时不能把它当普通对象展开。正确做法是包装原函数再合并最终权限constoriginalschema.componentPropsasFunction;return{...schema,componentProps:(values:any,formApi:any){constresolvedoriginal(values,formApi);return{...resolved,disabled:resolved?.disabledtrue?true:fieldDisabled,};},};这保证业务字段自身的动态约束仍然有效同时保留流程层的权限控制。四、为什么只保留一个 Schema Watcher4.1 多 watcher 的竞争问题常见实现会分别监听disabled变化fieldPermission变化formSchema变化processStatus变化。每个 watcher 都调用updateSchema()。当审批详情和业务数据并行返回时执行顺序取决于异步时机最后一次写入可能把前面的权限覆盖掉。更稳定的方式是先用一个computed合并所有输入再用唯一 watcher 同步watch(resolvedSchema,(newSchema){if(!formApi){if(newSchema.length0)initForm();return;}if(newSchema.length0){formApi.updateSchema(newSchema);}},{deep:true},);这实际上把“多条命令式更新”改成了“一个声明式结果”最终 Schema f(原始 Schema, 页面状态, 字段权限, 审批上下文)只要输入相同结果就相同调试成本会显著下降。4.2 规则变更和数据变更要分开Schema watcher 只更新规则数据 watcher 只更新值。不要在同一个 watcher 中既updateSchema又setValues否则字段重建、校验触发和数据覆盖会互相影响。五、局部 Patch避免父组件回填覆盖用户输入5.1 一个真实的丢值场景用户先手动选择“调入公司”随后业务页异步查到“调出公司名称”把新的formData整包传回组件。若组件直接formApi.setValues(props.formData);父组件旧快照中的“调入公司”就会覆盖用户刚选的新值。5.2 只同步真实变化且属于 Schema 的字段组件先比较前后快照再构建 patchconstchangedKeysgetChangedFormDataKeys(newData,prevFormDataSnapshot.value,);prevFormDataSnapshot.valuecloneDeep(newData);constpatch:Recordstring,any{};for(constkeyofchangedKeys){if(formFieldNames.has(key)){patch[key]newData[key];}}if(Object.keys(patch).length0){awaitformApi.setValues(patch);}这里还有两个细节用cloneDeep因为附件扩展字段可能含Date或函数structuredClone不一定能处理过滤非 Schema 字段避免附件数组、流程快照等扩展数据触发表单重置。这是一条可以推广到所有复杂表单的原则外部状态回填必须是 patch而不是 replace。六、附件组件双向绑定最容易被低估的部分6.1 附件不是 Upload 的简单包装一个企业附件组件至少需要能力关键问题上传数量、类型、大小、并发完成回显已保存附件与临时附件共存编辑备注、排序、删除预览图片、PDF、Office 文件和不支持格式降级下载私有文件先换取可访问 URL只读动态隐藏上传、删除和可编辑备注在同一套组件下应收单也能获得与 OA 单据一致的附件交互。▲ 财务应收单与用印申请字段完全不同但复用了同一套页面骨架和附件组件6.2v-model回流必须防止 watch 死循环组件内部维护tableData变化后 emit 新数组父组件接收后又更新modelValue。没有方向标记就会来回触发letinternalUpdatefalse;functionhandleUpdateValue(){internalUpdatetrue;emit(update:modelValue,tableData.value.map((item)({...item})),);}watch(()props.modelValue,async(attachments){if(internalUpdate){internalUpdatefalse;return;}tableData.valueattachments.map((item)ensureRowKey({...item}));awaitreloadAttachmentGrid();},{deep:true},);注意 emit 时复制每一项避免父子组件共享对象引用后发生“没 emit 但外部数据已被改”的隐式副作用。6.3 表格行不能靠对象引用识别表格组件可能克隆行对象item row并不可靠。附件组件使用稳定rowKey并依次回退到id、fileUrl和组合字段functionisSameAttachment(item:Attachment,row:Attachment){if(item.rowKeyrow.rowKey){returnitem.rowKeyrow.rowKey;}if(item.id!nullrow.id!null){returnitem.idrow.id;}if(item.fileUrlrow.fileUrl){returnitem.fileUrlrow.fileUrl;}returnitem.fileNamerow.fileNameitem.fileSizerow.fileSizeString(item.uploadTime??)String(row.uploadTime??);}这是“页面偶尔删错附件”与“长期稳定运行”的分水岭。七、把审批上下文也收进组件而不是每页复制7.1 同一业务数据不同视图语义viewType不只是路由来源它决定页面能力视图数据可见性字段编辑底部动作新建/我的完整按业务规则保存、提交、删除待办完整按节点字段权限审批动作已办完整强制只读取回后端判定可取回时抄送完整强制只读关闭组件对已办和抄送做兜底只读避免某个业务页漏传disabled后出现越权编辑。7.2 审批信息与业务表单共享一个上下文▲ 同一个 BasicForm 根据流程实例加载审批进度与记录业务页无需重复实现轨迹查询和展示当headerData.processInstanceId存在时组件自动加载流程图、审批详情和任务列表已办视图还会向后端查询是否可取回。后端是最终授权者前端只负责展示结果不能仅靠按钮隐藏决定安全。八、组件 API 的设计检查表8.1 Props是否围绕稳定领域概念而不是无限布尔开关是否提供安全默认值对象和数组默认值是否用工厂函数是否区分“全局只读”和“字段自身禁用”8.2 Emits是否表达业务意图而不是暴露内部 DOM 事件Payload 是否有 TypeScript 类型是否避免组件直接调用具体业务 API8.3 Expose是否只暴露校验、取值、刷新等必要能力是否避免把整个内部 API 暴露给父组件8.4 响应式同步多输入是否先computed合并再单点同步外部回填是否局部 patch双向绑定是否区分内部更新与外部更新列表项是否有跨克隆稳定的 key九、这种封装什么时候不适用不是所有页面都应该塞进BasicForm。以下页面更适合独立实现大屏、驾驶舱、甘特图等强布局页面Excel 式高密度编辑器画布、流程设计器、富文本编辑器交互主导而非单据主导的聊天、看板页面。判断标准不是“能不能套进去”而是“套进去后业务页是否只剩领域差异”。如果为了复用需要大量插槽、几十个布尔值和样式穿透说明抽象边界错了。十、技术亮点总结设计要点实现方式业务价值类型化契约Props / Emits / Expose 分工调用方式可发现、可重构Schema 中间表示字段结构数据化权限和状态可统一变换单一策略合并resolvedSchemacomputed消除多 watcher 竞争权限优先级自身规则 节点权限 页面状态防止错误可写或不可写局部数据同步changed keys schema 过滤不覆盖用户正在编辑的值附件稳定身份rowKey 多级回退避免表格克隆导致删错受控双向绑定internal update 标记避免循环和隐式共享引用审批上下文复用轨迹、流程图、取回状态统一加载大幅减少业务页重复代码十一、快速体验在线演示https://ruoyioffice.com/web/账号 admin / admin123推荐体验路径进入 OA → 印章管理 → 用印申请打开一张审批中的申请观察统一表头和状态切换“单据信息 / 审批信息 / 流程图”对比待办、已办和我的申请中的字段与按钮再进入财务 → 应收应付 → 应收单对比两个业务域的表单分区、附件区和底部动作。源码仓库GitHubhttps://github.com/yuqing2026/ruoyi-officeGitCodehttps://gitcode.com/zhouzhongyan/ruoyi-officeGiteehttps://gitee.com/yqzy1688/ruoyi-office常见问题FAQVue3 企业级组件是不是封装得越大越好不是。组件应该收口稳定机制而不是吞掉所有业务。表头、字段权限、附件和审批上下文稳定适合复用模块特有选择器、金额计算和领域校验应留在业务页。Schema 表单会不会比手写模板更难调试简单表单确实没必要强行 Schema 化当字段需要统一叠加权限、只读和动态校验时Schema 能把最终规则收敛为一个可检查的数据结果反而更容易定位问题。为什么字段权限不能只在后端控制后端必须做最终权限校验但前端仍要正确呈现隐藏、只读、可写和必填状态。两者职责不同前端保证体验后端保证安全。Vue3 的 v-model 为什么会出现死循环父组件更新modelValue子组件 watch 后再次 emit若没有区分更新来源就会往返触发。可以使用内部更新标记或采用单向数据流加显式提交。如何判断一个组件是否值得沉淀为公共组件至少被三个业务场景复用并且它们共享的是规则和状态模型而不只是相似样式同时新增一个业务时无需修改大量公共组件分支。结语企业级组件封装的最终目标不是让页面文件变短而是让状态变得可推理任何一个字段为什么隐藏、为什么必填、为什么只读都能从契约和优先级中得到唯一答案。当类型契约、Schema 中间表示、权限合并、局部 patch 和稳定行身份建立起来后OA、财务、人力、资产等不同业务才能真正共享一套可靠的前端底座。想要体验 RuoYi Office 的强大功能在线演示https://ruoyioffice.com/web/账号 admin / admin123源码仓库GitHubhttps://github.com/yuqing2026/ruoyi-office GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office Giteehttps://gitee.com/yqzy1688/ruoyi-office技术咨询添加微信17156169080备注「RuoYi Office」⭐如果觉得不错请给个 Star 支持一下
返回列表