原文以一个受理样品表格组件为例,对比 Vue3 中静态渲染与动态渲染组件的写法。核心场景是 CommonApplySampleTable 根据 acceptType 在四类表格组件之间切换:水质检材、公共场所、消毒监测和其他。静态写法把 v-if、v-else-if、v-else 直接写在四个组件标签上;动态写法用 computed 返回当前应渲染的组件对象,再交给 <component :is> 渲染。两者最终展示的表格一致,区别主要在模板重复度、选择逻辑位置和维护方式。
静态渲染的原始结构如下,每个分支都重复相同的 props:- <ApplySampleSZTable v-if='waterSampleTypes.includes(acceptType)' :apply-samples='localApplySamples' :table-scroll-top='scrollTop' :operation-command='operateCommand' />
- <ApplySampleCSTable v-else-if='publicPlaceTypes.includes(acceptType)' :apply-samples='localApplySamples' :table-scroll-top='scrollTop' :operation-command='operateCommand' />
- <ApplySampleXDTable v-else-if='disinfectionTypes.includes(acceptType)' :apply-samples='localApplySamples' :table-scroll-top='scrollTop' :operation-command='operateCommand' />
- <ApplySampleOtherTable v-else :apply-samples='localApplySamples' :table-scroll-top='scrollTop' :operation-command='operateCommand' />
复制代码 这段代码依赖 waterSampleTypes、publicPlaceTypes、disinfectionTypes 三个类型数组判断 acceptType 命中哪类组件,未命中时落到 ApplySampleOtherTable。分支多且 props 一致时,模板会显得冗长。
动态渲染把选择逻辑收进 computed:- const currentTableComponent = computed(() => {
- if (waterSampleTypes.includes(acceptType.value)) return ApplySampleSZTable;
- if (publicPlaceTypes.includes(acceptType.value)) return ApplySampleCSTable;
- if (disinfectionTypes.includes(acceptType.value)) return ApplySampleXDTable;
- return ApplySampleOtherTable;
- });
复制代码 模板只保留一个动态组件入口:- <component :is='currentTableComponent' :apply-samples='localApplySamples' :table-scroll-top='scrollTop' :operation-command='operateCommand' />
复制代码 这里有两个关键点:computed 中 acceptType 是 Ref,必须访问 acceptType.value;返回的是组件对象,不是字符串。四个表格组件从 @/components 导入,动态组件的 props 与原静态分支保持一致。适用场景上,分支多、各分支 props 相同、组件由 acceptType 这类数据决定时,动态写法更集中;分支少或各分支需要不同 props、指令和位置时,静态写法更直观。
CommonApplySampleTable.vue 使用 <script setup lang='ts'> 和 defineOptions 定义组件名。它通过 symbols 注入 applySamples、acceptType、operateCommand、scrollTop、apply、applySampleSelection,以及 transmitApplySamplesSymbol 回传方法。组件内部维护 localApplySamples、currentApplySample、percentage 和 customColors,并定义 waterSampleTypes、publicPlaceTypes、disinfectionTypes 三个类型数组。为避免祖先组件与本地数据互相触发更新,代码使用 isUpdatingFromAncestry 作为同步隔离标志。
关键 watch 逻辑如下:- watch(
- () => applySamples.value,
- async () => {
- isUpdatingFromAncestry = true;
- localApplySamples.value = cloneDeep(toRaw(applySamples.value)) as ApplySampleResponse[];
- await nextTick();
- isUpdatingFromAncestry = false;
- },
- { deep: true, immediate: true }
- );
- watch(
- () => localApplySamples.value,
- () => {
- if (!isUpdatingFromAncestry) {
- sendApplySamples(localApplySamples.value);
- }
- },
- { deep: true }
- );
复制代码 第一个 watch 监听祖先传入的 applySamples,深拷贝到本地,immediate 让首次查看详情时也能加载数据;第二个 watch 监听本地列表,只有非祖先更新才回传,避免循环同步。nextTick 后再复位标志,是为了等本轮 DOM 更新完成。
删除样品采用乐观更新:先从本地过滤数据并清空选集,再调用 batchDeleteApplySamples;失败时恢复整个数组和选集。核心逻辑是:- const originalSamples = [...localApplySamples.value];
- const originalSelection = [...applySampleSelection.value];
- localApplySamples.value = localApplySamples.value.filter((item) => !ids.includes(item.id));
- applySampleSelection.value = [];
- sendApplySamples(localApplySamples.value);
- try {
- await batchDeleteApplySamples(ids);
- } catch (error) {
- localApplySamples.value = originalSamples;
- applySampleSelection.value = originalSelection;
- sendApplySamples(localApplySamples.value);
- console.error('删除样品失败:', error);
- ElMessage.error('删除样品失败,数据已恢复');
- }
复制代码 新增样品时,sampleNo 由 localApplySamples.value.length + 1 生成,再用 padStart(2, '0') 补成两位编号,例如 01、02。代码调用 addBlankApplySample 后会取得自增 id,id 为 0 表示该样品编号已存在,此时用 ElMessage.warning 提示编号重复。复增样品需要先选中 currentApplySample,再 cloneDeep 复制,重新生成 newSampleNo,并用 find 检查本地编号;随后用 lodash-es 的 omit 去掉 id,转成 ApplySampleCreateRequest,再调用 addApplySampleService。
对外暴露方面,组件通过 provide(transmitCurrentApplySampleSymbol, receiveCurrentApplySample) 向后代提供接收当前选择行的方法,通过 defineExpose 暴露 addApplySample、deleteApplySample、copyAddApplySample 给父组件模板引用。模板中进度条用 v-show='percentage > 0' 控制显示,动态组件接收 localApplySamples、scrollTop 和 operateCommand;容器样式设置 height: 100%,用于继承父级确定高度并显示 el-table 内置滚动条。
动态组件也用于菜单和图标。原文用 <component :is='isFold ? Expand : Fold' /> 替代 Expand 与 Fold 的 v-if/v-else 写法;菜单图标通过 <component :is='m1.icon ? MenuIconMap[m1.icon] : MenuIconMap[`Management`]' /> 按 icon 映射选择组件,未配置时回退到 Management。菜单结构上,parentId 为 0 或 null 时渲染 el-sub-menu,否则渲染 el-menu-item。
排查这类实现时,优先检查 computed 是否访问了 acceptType.value、返回值是否为组件对象、组件是否已正确导入,以及模板 props 是否与静态分支一致。出现数据循环同步时,重点看 isUpdatingFromAncestry 是否在 nextTick 后复位、两个 watch 是否都设置了 deep。删除失败后若界面数据异常,应确认 originalSamples 与 originalSelection 是否同时回滚并再次 sendApplySamples。新增或复增时若提示编号重复,应检查 sampleNo 的生成规则、后端返回 id 是否为 0,以及复增前是否执行了 find 校验。样式上若表格没有出现内置滚动条,应检查外层容器是否真正继承到确定高度。 |