一句话:这一层管「被两个以上消费者共用的底座状态」——几何、可见、DOM 动作、值、派生顺序、渲染窗口、动画时序、叠序与文案;不管单个组件的 props 语义与外观(归
03-components对应类篇),也不管 monorepo 拓扑、生成物链路与注入链本身(归A01、A02、A05)。
基座不是「小一号的组件」,而是把某一种跨组件共享的状态独占到一个目录里,让所有消费者只剩一条写入路径。按「这份状态是什么」切,16 篇可以排成八格:浮层该在哪是几何状态,归 I01;该不该在、什么时候在是可见状态,归 I02;节点包含、可见性、动态样式注入与事件封装是 DOM 侧的公共动作,归 I03;用户敲到第几步是值状态,拆成 I04(输入基座)与 I13(其余控件的受控内核)两半;排序筛选分页谁先跑是派生顺序,归 I06;哪几行进视口是窗口状态,归 I05 与 I08;动画播到哪一拍是时序状态,归 I09;谁压住谁是叠序状态,归 I10;话术与日期语言是文案状态,归 I11 与 I07。剩下的 I12、I14、I15、I16 不持有状态,只约束别人怎么接线。
真正的判据不是「谁更底层」,而是这份状态有没有第二个写者。I08 坑 6 记下的是同一份窗口数学存了两份拷贝、且已经开始各修各的;I10 坑 8 记下的是同一件发号工作有两个起跳值不同的实现,而换一条 import 连类型错都不报。遇到「近义基座」,先数写入路径有几条,比争论职责边界管用。
状态流向是这一层最反直觉的地方:基座普遍是「渲染先行、测量后置、回执只补一次」,而不是响应式数据流。I05 坑 1 里列宽必须等 DOM 测回来才有值,缺任意一格就把整块表头藏起来;I01 坑 6 说动画期发出的定位请求不排队、直接丢弃;I02 §六 算出一次 hover 到动画开播最少跨两个宏任务加两帧 raf。排查时机类问题要按「谁在等谁」画箭头,而不是按依赖图。
分层在这一层只是约定,不是机制。I05 §六 记录 vendor 层的 Cell 反过来 import 业务层的 slot;I06 §六 记录一整套能跑、1,998 行的 composable 从来没被接进 setup,于是 table-core 的 flat 分支变成「测得最全但线上零引用」的孤儿(I06 坑 1、C08-02 论点 1)。vc-* 与业务目录之间没有编译期闸门,跨包侧更弱:I15 坑 7 说明 icons 连 typecheck 脚本都没有,I16 §七 说明 core、utils、metadata 三包零测试,而 metadata 的漂移只有在消费方构建时才暴露(I16 坑 4)。
双实现并存是常态而不是事故,这点必须写进心智模型:虚拟化两套(I08)、发号器两套(I10 §六 取舍三)、受控判定两套(I13 §六 取舍三)、事件监听封装与裸调用两套(I03 §六)、同名工具函数跨三层各一份(I03 坑 7)、motion 兼容转发层加整目录副本(I09 §六 取舍四)。选错的代价通常是静默的——I13 坑 3 里把 useState 换成 useMergedState 或反向替换都不报错,只让组件悄悄失去受控能力。
测试面要按运行器的 glob 判、不能按目录位置猜,而且「有测试」不等于「测的是线上那份」。几何与时序侧的 I01、I02、I05,内核目录侧的 I04 四层、I07 的 vc-picker、I08 的引擎 A,适配与工具侧的 I03、I12、I13、I15,再加上 I16 的三个包,都是零直接用例(各篇 §七 都做了两法交叉)。剩下的几处也不是安全网:I06 只有 1 个测试文件而它测的正是死分支,I08 的 353 行只护引擎 B 且不含窗口数学,I09 的 JS 半边有两条曲线用例、CSS 半边零,I11 唯一那个文件里两条真正有断言意图的用例整条 it.skip。所以本层所有改动路线共用一条前提:先数引用点,再数测试点,I03 坑 5 与 I13 坑 8 都是「看着该修、其实零消费者」的实例。
读法:左两列解决「这个症状归哪个基座」,第三列解决「这件事其实不归基座,归谁」——本层最常见的误判不是选错基座,而是拿着基座的结论去改组件,或拿着组件的观感问题来翻基座。判据列里的编号就是这一行的全部论证,展开在被引的那篇。
| 要解决的问题 | 选它 | 而不是另一个 | 判据 |
|---|---|---|---|
| 浮层落在哪个方位、越界要不要翻面夹紧 | I01 | 组件层自算 left/top,或改 Tooltip 的 placement 语义(C07-03) | placements 表把 placement 名翻成 points 对,基座与业务各存了一份反查(I01 §六 取舍 5、C07-03 论点 4);方位类名错配只在组件侧显形(I01 坑 3、C07-03 坑 2) |
| 什么时候弹、外部点击怎么算、渲染闸门归谁 | I02 | Modal 与 Drawer 的遮罩、滚动锁定、销毁时机(C07-01、C07-02) | Modal 的滚动锁走 Portal 那条链,Dialog 里的 scrollLocker 分支对本路径恒不执行(C07-01 论点 5、I03 坑 5) |
| 节点包含、可见性、动态 style 注入、事件封装 | I03 | packages/utils 或 _util 里的同名函数(I16 §六) | 同一个名字在三层各有一份、强度不同,IDE 自动导入选错也能编过(I03 坑 7) |
| 输入框的值、输入法提交、滚动位置 | I04 | allowClear、addon、校验态的落点(C04-01) | 基座只决定要不要 affix 那一层壳,allow-clear 的三个细节全在组件层(I04 坑 1、C04-01 论点 5) |
| 固定列与固定表头的 DOM 结构、列宽测量 | I05 | 三代表格本身怎么选、单元格定制走哪条通道(C08-00) | 三张表只在壳层同名,往下零行共享;vc-table 的唯一消费者就是 table(C08-00 论点 1) |
| 排序、筛选、分页、选择、展开的执行顺序 | I06 | data-table 的列 DSL、expose 面与 composable 接线(C08-02) | 线上排序在 DataTable.tsx 里,改 table-core 的 flat 分支一行都不动(I06 坑 1);那 12 个 composable 零消费者(C08-02 论点 1、I06 §六 取舍六) |
| 日期计算、面板矩阵、dayjs 之外的库适配 | I07 | preset 与 ranges 的取值和外观(C05-06) | presets 靠属性透传,其 value 不受 valueFormat 保护;I07 §五 已按 C05-06 论点 3 更正初稿说法 |
| 长列表条目高度不可预知(图片、富文本、可展开行) | I08 引擎 A vc-virtual-list | virtual-list 与 virtual-scroller(引擎 B) | 引擎 B 全程不测单项高度,声明与实际不符就永久漂移(I08 坑 3);按 key 寻址只有引擎 A 支持(I08 §六) |
| 条目恒定,且要横向、双向、懒加载或仅追加 | I08 引擎 B 的公开件 virtual-list | 引擎 A;或为了「内部件」去引 virtual-scroller | 引擎 A 连横向坐标轴都没有(I08 §六 能力矩阵);公开件与内部件是同一份码,选谁只取决于能不能依赖 vc-* 目录(I08 §六) |
| 展开收起的高度过渡 | I09 | Collapse 与 Tree 的 openAnimation 用法(C02-09、C09-01) | openAnimation 是整对象替换而不是改名口子(I09 坑 5);树的折叠只在默认前缀下侥幸可用(I09 坑 2) |
| 两个浮层互相压住、层级要跟着主题走 | I10 | 单个组件的 zIndex prop 与遮罩观感(C07-07、C03-13) | 受管三件套写的是行内样式,恒压过类选择器(I10 坑 1);Tour 与 Notification 是 token 带之间的比较,得回组件篇调(I10 坑 3) |
| 组件文案与语言切换的合并规则 | I11 | 把「页面上没值」当渲染 bug 去改组件 | 供给侧缺失要同时数读取点与供给点;「槽位有但没人读」的死槽位与「读了没人供给」互为镜像(I11 坑 7) |
| 把第三方 schema 库接进校验 | I12 | 本库规则写法、字段注册与错误渲染(C06-01) | errors 字典的值是库原生对象,直接塞给 Form 会渲染成对象(I12 坑 4);补一个 barrel 一次牵动四件事(I12 坑 7) |
| 受控与非受控的归一 | I13 的 useMergedState | 组件里手写 !== undefined 三元(C04-05、C04-03) | 手写那份多出两处统一实现里不会有的错(I04 §六 6.4),而且回流 watcher 连判定都能省掉(I13 坑 1);postState 返回新对象时去重整体失效(I13 坑 2) |
| 告警去重、能力探测时机、URL 消毒、SSR 抽样式 | I14 | 组件里的 SSR 判空与首屏缺样式(C12-06) | 公开 Portal 与浮层基座完全不通,挂载点是两套口径(C12-06 论点 1);顶层求值探测会把「不支持」烧进服务端结果(I14 坑 8) |
| 新增一个图标、改一个图标名 | I15 | 图标尺寸、旋转与观感对齐(C01-02) | 名字改动要同时覆盖四张表而没有任何机器校验(I15 坑 1);size 得走 font-size,spin 与 rotate 是死 API(I15 坑 2、I15 坑 4) |
| 自动导入报 Cannot find module 或整条组件缺失 | I16 | 生成物三件套与 pnpm gen:entries 本身(A02) | metadata 的键与生成物的键不等价,gen:entries 根本不写它(I16 坑 4);多词组件必须显式给 from(I16 坑 1) |
下面按症状分组,不做机制展开。同一条症状在基座篇与组件篇都出现过时,两边编号都给出来,改哪一边由 §二 的判据决定。
浮层位置与裁切
I01 坑 1(调用方浅合并把 overflow 整块替换,「只关 X」变成「X 与 Y 全关」);容器换了但节点没换是另一条,I01 坑 2。I01 坑 3;Tooltip 侧的同一症状登记在 C07-03 坑 2。v-show 为真却永不显示,控制台干净 → I02 坑 4 与 I01 坑 4 是同一条死锁的两端,链上没有任何超时兜底;移动端 forceAlign 空转是另一回事,看 I01 坑 8、I02 坑 5。I01 坑 6;想改这一条要同时动两处门禁,只改一处会造出新的死锁(同坑的改法段)。I01 §六 取舍 4(不监听 scroll 是有意的)、I01 坑 7。弹出、关闭与事件
I02 坑 8(showAction 与 action 不是一套词),再查根节点数 I02 坑 1,最后查预热 I02 坑 2(forceSubMenuRender 正踩在上面,链路五跳)。I02 坑 7(这条症状 2026-09-30 实测不成立:document 侧收的就是 mousedown,被内容截掉冒泡后判定函数根本不跑;该坑剩下的是代码事实——withModifiers 的 capture 是空操作,当前可用靠的是顺序巧合);Modal 用「按下与抬起」记账是组件层的另一套做法,看 C07-01 论点 3。I02 坑 3;destroyPopupOnHide 与 autoDestroy 都管不到那层 wrapper。I02 坑 6;表现与 I09 坑 3 同形,判据是「animation 对象有没有被就地改写」。表格
I06 坑 1(flat 分支生产零引用);这一族的半丢状态:档位锁死 I06 坑 2、v-model:multi-sort-meta 失同步 I06 坑 3、翻页丢展开 I06 坑 5、全选不等于本页 I06 坑 6、非受控过滤不成立 I06 坑 7。I05 坑 1、I05 坑 2、I05 坑 3,右固定列的取整缝是 I05 坑 4。I05 坑 7、I05 坑 8,两条都是「命令式写 DOM、渲染态不知情」的同族。I05 坑 5(一对方向相反的断链:一个有产无消、一个有消无产);子列写 fixed: undefined 吃掉分组继承是 I05 坑 6。showHeader = false 把固定在顶部的 summary 一起干掉 → I05 坑 9;data-xy-scrollable 恒不渲染是 I06 坑 4,目前无样式消费它,属潜伏缺陷。虚拟化(两类症状、两份实现)
I08 坑 1、条目根节点必须单元素且能承 ref 是 I08 坑 2;恒定高度走引擎 B,itemSize 与真实不符是 I08 坑 3、行内状态串行是 I08 坑 5。scrollTo 静默无效或定位后行号短暂错位 → 引擎 B 是 I08 坑 4(越界只校验下界、未挂载直接短路),引擎 A 的多帧重试语义见 I08 §六。I08 坑 6;组件侧的对应面是 C08-07 论点 1(孪生拷贝已各自分叉)与 C08-06 论点 3(一次 scrollToIndex 之后滞回缓冲被永久旁路)。改树或表格的行高导致错位,先读 C09-01 坑 1。动画与 reduced-motion
I09 坑 1(JS 类名协议与 CSS 生成是两条互不调用的链路)。I09 坑 3;同类表现为「前一个浮层永停在 motion」的是 I02 坑 6。-enter-from 永不命中 → I09 坑 6(from 阶段被改写为三段类名,只有 drawer 与 tabs 自己补齐)。I09 坑 7;自定义前缀后折叠动画静默失效是 I09 坑 2。prefers-reduced-motion → 这不是接线错,是根本没有通路:helper 已就位但零消费者,属设计缺口(I09 §六 取舍五);要连带决定静态检查落点时读 I14 §六。I09 坑 4。层级
z-index 完全不生效 → I10 坑 1(受管三件套走行内样式);调 zIndexPopupBase 后只有一半组件跟着动、相对关系翻转是 I10 坑 2。I10 坑 3;Tooltip 与引导面板同为 +70 时谁在上头源码未定义,叠序随交互路径变化是 I10 坑 4。I10 坑 5;调用方自己写的 zIndex 被抹平是 I10 坑 6,baseZIndex 传了没生效是 I10 坑 7。I10 坑 8;同一族的「选错同名工具」还有 I03 坑 7 与 I16 坑 3(两个事件总线是两个对象)。输入与受控
@input 读到的事件缺字段 → I04 坑 5(重放的是裸 Event,没有 InputEvent 字段)。e.target 不是真实节点 → I04 坑 4;:allow-clear="false" 反而多出一层壳是 I04 坑 1,组件侧同症状是 C04-01 坑 1。I04 坑 2;setScrollTop 抛错被吞、Firefox 滚动位只修一半是 I04 坑 3。@resize 永不触发 → I04 坑 6(观察器被 disabled 直接销毁);lazy 有两个方向相反的开关,混用即静默失效是 I04 坑 7。I13 坑 1,手写那份更彻底;onChange 重复触发是 I13 坑 2;事件里同步读回值是 I13 坑 4;ref 拿到的 DOM 跨周期失效是 I13 坑 5。size、direction 对某个组件零效果 → 根因是「给本该由注入链兜底的 prop 填了运行时默认值」,判形与活例见 A05 与 C11-01 论点 5(Spin 的 size)、C04-08(Rate 的 direction)。文案与日期
I11 坑 4(新增语言只写一个文件)、I11 坑 1(只传半个 lang 把整块覆盖掉)。Modal.confirm)的语言粘住上一页 → I11 坑 2;ConfigProvider.config({ locale }) 类型不收但运行时生效是 I11 坑 3。I11 坑 7,死槽位造成的假覆盖)。disabledDate 调用量爆表 → I07 坑 6;panelChange 恒触发是 I07 坑 1,灰格加 disabled 杀掉跨月选择是 I07 坑 4。I07 坑 5;换 date-fns 前要先补依赖与方言转换,是 I07 坑 2,宿主项目 dayjs().format('Wo') 被原型补丁影响是 I07 坑 7。工具层与运行时约束
I03 坑 1(第四个参数传错位置,反而拿到相反的默认值)与 I03 坑 2(capture 注册的没法用句柄解绑)。I03 坑 4;样式节点删不掉且无告警是 I03 坑 3;去 DOM 结构里找滚动条锁定时踩到死代码是 I03 坑 5。I03 坑 6(isEqual 把结构共享误判成环,目前潜伏)。[xiaoye-ui: 消息] undefined,或第二条用例收不到输出 → I14 坑 1 与 I14 坑 4;浏览器原生 ESM 直接导入即抛 process is not defined 是 I14 坑 2。I14 坑 3;字符串 SVG 图标服务端首屏为空是 I14 坑 6;flex gap 分支在 jsdom 永远为假是 I14 坑 7,探测时机写错位置的后果是 I14 坑 8。safeUrl 兜底后链接点了自跳转 → I14 坑 5(要判 isSafeUrl,不是判返回值真假)。包、名字与生成链
I15 坑 1;本地全绿、用户侧炸是 I15 坑 7,深路径写法两个环境报错还不一样是 I15 坑 8。<SearchIcon /> 类型过得了、运行时解析不到 → I15 坑 9;GlobalComponents 被无前缀名字污染,命名红线与图标包既有形状互斥。I15 坑 5(32 条近似映射);品牌图标传什么都不变是 I15 坑 3。_shared 或副本文件,产物毫无变化 → I16 坑 2、I03 坑 7、I09 §六 取舍四、I04 坑 3 是同一族「同一逻辑多份拷贝」,族谱见 §四 第 10 条。pnpm check:entries 全绿但自动导入缺一整条 → I16 坑 4 与 I16 坑 1;给 metadata 加 CI 却被主库拖进构建图是 I16 坑 5。sync: true 传了没效果 → I12 坑 1 与 I12 坑 2;raw: true 换库后字段被清空是 I12 坑 3,.js 与 .d.ts 分叉没人拦是 I12 坑 6。一处需要仲裁的跨篇矛盾(已按现状定调)
left/top 落地方案的「容器兼容性」理由站不住 → I01 §六 取舍 2 用 vc-trigger/Trigger.tsx:480 那个 width: 100% 加 top/left: 0 的容器论证「为什么不用 transform」,而 I02 坑 3 判定同一段实现从未执行。以 I02 为准:现 grep 判据是 grep -rn "getContainer" packages/xiaoye-ui/src/vc-trigger,只命中 :474 的 getContainer() 定义与 :698 的下传行,后者写的是 getContainer={getPopupContainer && (() => getPopupContainer(this.getRootDomNode()))},即未传该 prop 时整个值是 undefined,Trigger 自己的 getContainer() 零调用点;真正生效的默认容器是 PortalWrapper.tsx:64 那个不带任何定位样式的 defaultContainer(附加在 :92)。结论:left/top 与 transform 的取舍仍然成立,但理由要换成 transform-origin 被 Tooltip 动画占用这一条(同篇取舍 2 后半句),容器兼容性那一半不再可引用。left/top 换成 transform → 先读 I01 §六 取舍 2 与坑 6,再读 I02 坑 3(容器归谁)与 C07-03 论点 4(业务侧那份反查)。顺序不能换:transform-origin 的内联写入不解掉,缩放动画与定位平移会同台打架;而容器那条理由已被上面的仲裁作废。I01 §六 取舍 4、I01 坑 2、I14 坑 8。这三篇合起来给出「高频重排怎么降频、探测结果什么时候求值」两问的现状与赔价。PopupInner 的三段 pending 收敛成一个显式阶段机 → I02 §六、I02 坑 6、I09 坑 3。useVisibleStatus 已经是七态骨架,缺的是把 timer 纳进去;漏掉 animation 对象改写这一处会引入新的「永不停止的 motion」。prefers-reduced-motion → I09 §六 取舍五、I09 坑 1、I14 §六。前两段给现成 helper 与零接线的定性,第三段决定「忘注册动效」这条约束该由哪道静态检查兜。I10 §六 取舍三与坑 2、A03。公开性不对称(utils 版在包导出面上、_util 版不在)才是合并不动的实际原因,改签名前先看 A03 的 token 层级。data-table 接上 table-core 引擎 → I06 §六 取舍六、I06 坑 1、C08-02 论点 1。事件名漂移(change 与 value-change)是第一道必须补的坎,跳过去接线会让所有监听方静默失聪。I08 §六、I08 坑 6、C08-07 论点 1。高度模型冲突是根本性的(实测 Map 对 标量 itemSize),源码里没有统一计划;「合一」之前先回答横向能力与按 key 寻址这两条谁让步。aria 段 → I11 坑 5、I11 测试现状。这是一处跨组件共享的改动,data-table 族四个读取点(BodyCell、HeaderCheckbox、RowCheckbox、ColumnFilter)与 table/transfer/tabs/分页的读取点要一并回归,而本层唯一的 locale 测试只能测「不抛异常」。I12 §七 的四步最小闭环、I12 坑 7、C06-01。第 ① 步(映射成 Form 要的数组形状)不动,后面三步做完也只是让一条不可用的路径可用。useMergedState 任一行 → I13 §七 安全边界、I04 §六 6.4、C04-00。:26 的受控判定与 :36 的去重比较等价于改全站表单控件,而 12 个 vc-* 调用点在测试侧零暴露,只有 xy 层快照会间接变红。