一句话:这份导览管「把状态可视化出来」这一族(Spin、Progress、Skeleton、Statistic 与 Countdown、MeterGroup、Timeline、Descriptions、Typography)的近义选型与坑路由,不管任何单个组件的内部机制(机制归
C11-01至C11-08),也不管进出场动画协议(归 I09)与浮层栈(归 C07-00)。
本类九个组件在用户眼里是同一件事——「一块会动的信息区」,但在源码里它们分属三种互不相容的更新驱动源,分类轴是「谁持有时间」。Skeleton、Spin、Empty 把时间整个外包给浏览器:CSS 关键帧自循环,运行期一次 JS 都不跑(C11-00 论点 1)。Progress 把时间交给调用方:每推一次 prop 就触发一次 CSS 补间,本身没有心跳(C11-00 论点 3)。Countdown 是全类唯一把时钟握在组件手里的,setInterval 加 $forceUpdate(C11-00 论点 4)。这条轴不是修辞,它直接决定四件工程后果:精度上限、能不能停下来、能不能被主题改写、能不能在服务端复现(C11-00 论点 5)。
但档位不是组件边界,这是选型时最容易搞错的一层。同一个 Progress 靠一个 status="active" 就能从补间档跳进关键帧档,反向也成立(C11-00 §五)。同一个 Spin 在包裹态里同时踩两档:指示器走无限关键帧且显隐硬切,被包裹内容的变暗与面纱的浮现反倒都是真补间(C11-01 论点 4)。steps 分支更进一步,它把连续量直接量化成离散格,于是「推得多快」对它毫无意义(C11-00 论点 3)。所以本类的第一问从来不是「用哪个组件」,而是「这条呈现允许被推多快、需不需要一个能真正停下来的出口」。
第二根轴是数据密度,它把后四个组件串成一个阶梯。Statistic 只处理一个标量,并且明确不做数值过渡、不做本地化、不做四舍五入(C11-04 §一)。MeterGroup 把一组离散值映射到同一条轨道的分段几何,只做仿射夹取,不做加权也不做归一到 100(C11-05 §一)。Timeline 把一组条目摊成垂直轴,全部状态来自 props 与 slots,无内部响应式、无事件回流(C11-06 §一)。Descriptions 处理的是键值对,它的技术含量集中在两条算术上——column 从响应式对象降成一个数字,span 从数字变成每行剩余列宽的消费(C11-07 §一)。这四篇都在 §一 用「明确不负责」的格式把边界钉死,选型的判据基本就写在那四段里。
第三根轴是状态流向,而本类的整体答案是「单向、无回流」,一旦某个组件出现了回流,坑就跟着出现。Skeleton 连 emit、expose 与具名插槽都没有(C11-03 §五),Spin 没有任何对外事件(C11-01 §五)。真正有回流面的是两处:Countdown 的 change 与 finish,以及 Typography 的编辑与复制。前者的心跳不产生状态、只产生「读取时刻」,于是中途改 value 时显示的差值按新目标算、事件里的差值按旧目标算(C11-00 坑 1、C11-04 坑 3——同一件事的两次独立取证)。后者把同步 execCommand 的返回值丢掉,于是「已发起」被当成「已成功」(C11-08 坑 3)。
第四条贯穿线是本类最需要被记住的:降级与可访问性没有统一通路。prefers-reduced-motion 在全库只有一个孤岛式 helper、零调用点(I09 论点 6),落到本类的具体伤害面是三处无限循环——Skeleton 每 1.4 秒一次的横扫与 Spin 的四点循环(C11-00 论点 5)。这个结论的判形命令在 C11-03 论点 7 被重新取证并修正过一次:早期那条大小写敏感的命令匹配不到驼峰标识符、不具证伪力,现在以该篇的两条复核命令为准。数值播报这一侧同样是空的:Progress 全目录唯一的 aria 命中只有 role="progressbar",读屏拿不到数值(C11-02 §一);MeterGroup 确实写了 aria-valuenow,但走的是聚合口径,min ≠ 0 时与画面数学不等价(C11-05 坑 1)。也就是说「让辅助技术读到真实数字」这件事,本类没有一件组件能直接做到。
最后一条是契约面比实现面宽。这族组件的公开 props 与插槽里,有一批在运行期根本不被读取:progressStatus 被同名 computed 遮蔽成死 prop(C11-00 坑 5),Statistic 的 formatter 契约缺第二个实参(C11-00 坑 6),Descriptions 的 Item 是一个从不被实例化的 prop 容器(C11-07 论点 3),MeterGroup 声明了 icon 插槽却从不读它(C11-05 坑 8)。再叠上生成物的口径——global.d.ts 按目录登记、不认静态子成员,于是运行时能解析的标签在类型面上不存在(C11-08 坑 6、A02 论点 8)。选型表能帮你挑对组件,挑对之后能不能真的生效,得看下面这一节。
| 要解决的问题 | 选它 | 而不是另一个 | 判据 |
|---|---|---|---|
数据还没到,但子内容里的 ref、生命周期与曝光埋点必须保住 | C11-01 Spin 包裹态 | C11-03 Skeleton | 包裹态是否挂载由「插槽非空」这一件事副产物式决定,内容容器带稳定 key 始终在 DOM 里(C11-01 论点 1);Skeleton 的骨架分支从不渲染插槽,loading 反复切就反复重建子组件(C11-03 坑 6) |
| 首屏还没有任何东西,只想让版面不塌 | C11-03 Skeleton | C11-01 Spin 盖住空区 | Skeleton 的 loading 是三态,判定式 loading || props.loading === undefined 让「没写」也等于「在加载」,语义整个写在 prop 声明里(C11-03 论点 1);Spin 的 spinning 被 initDefaultProps 填成 true,裸写就在转(C11-01 论点 5) |
| 有一个 0 到 100 的数,且会被高频推送 | C11-02 Progress 的 circle 分支 | 同一组件的 line 与 steps 分支 | 只有 circle 持有 path 的 DOM 引用集合,能读自己的重绘节奏并在高于十次每秒时把补间归零;line 与 steps 拿不到可写节点,高频推送下只能一路处于追赶态(C11-00 论点 3、C11-02 论点 5) |
| 只需要表达「在忙」,不需要表达比例,且要能延迟显隐避免闪烁 | C11-01 Spin | C11-03 Skeleton | Spin 的 delay 是全类唯一带时间的契约,且到点即弃,只作用在开始那一刻(C11-00 §五);Skeleton 的循环时长虽然是 Token 位,却是 mergeToken 里的字面量,主题层改不动它的速度(C11-00 论点 1) |
| 一组共享同一条轨道的区间值(配额、占比、多资源水位) | C11-05 MeterGroup | 拿多段 C11-02 Progress 拼 | MeterGroup 只做仿射夹取,不归一化到 100、不加权,也不依赖 vc-progress(C11-05 §一);Progress 的「多段」只有 circle 描边能画,而 success 改变的是切片而不是状态,同一条语义在三条绘制路径上有三套互不相干的实现(C11-02 论点 3) |
| 要让读屏软件播报数值 | 目前没有合格答案,只能选 MeterGroup 并强制 min 为 0 | C11-02 Progress | Progress 全目录唯一的 aria 命中是根节点的 role="progressbar",没有 aria-valuenow 与 aria-valuemin,读屏拿不到数值(C11-02 §一);MeterGroup 有 aria-valuenow,但聚合口径与逐项口径在 min ≠ 0 时数学不等价(C11-05 论点 1、C11-05 坑 1) |
| 键值对详情(要固定列数、要跨列)vs 时序流水(要轴点颜色与末项虚线) | 前者 C11-07 Descriptions,后者 C11-06 Timeline | 用 Descriptions 摆流水,或用 Timeline 摆详情 | Descriptions 的「一行几列」是 DOM 结构级响应式而非 CSS 判档,同一个 span 在非 bordered、bordered、vertical 三种模式下指向三种不同宽度(C11-07 论点 1、C11-07 论点 5);Timeline 的位置类名由容器读子 vnode 原始 props 产出,position 被 mode 无条件短路(C11-06 论点 1) |
| 就地编辑一段纯文本 vs 编辑一个表单字段 | 前者用 C11-08 Typography 的 editable;后者归 C06-05 与 C06-06 | 把 Typography 的 editable 当表单控件用 | Typography 的编辑能力是本仓自研的 TextArea 加按键状态机,不是任何第三方内核的封装(C11-08 论点 1、C11-08 §一);字段级控件与 Form 的契约是三重断裂(C06-05 论点 5),而 Inplace 又是 InlineEdit 状态机的逐行派生副本(C06-06 论点 1) |
| 要展示真实剩余时间,并且需要一个能真正停止更新的出口 | C11-04 Statistic.Countdown | 用 Progress 表达「还剩多久」 | 心跳档的显示值不来自累加而来自渲染期读墙上时钟,因此免疫数值漂移,也是三档里唯一提供真正可停止更新的一档(C11-00 论点 4);代价是采样精度受 1000 / 30 钉死,毫秒位每帧跳 30 到 40(C11-00 坑 2) |
| 需要一条「关掉动画」的降级出口 | 现在只有 Spin 的 indicator 传 null 与 Progress 改掉 status 两条间接路 | 等一个统一的 motion 协议或 reduced-motion 兜底 | 三处无限循环的周期分属「取 Token 派生值、写在 mergeToken 里、内联在样式对象里」三个层级,第三层连改写入口都没有(C11-00 论点 1、C11-00 坑 4);prefers-reduced-motion 只有一个零调用 helper,本类的降级点只能落在样式生成函数里(C11-00 论点 5、C11-03 论点 7) |
A 组:传了 props 或插槽,运行期静默无效(API 面存活、实现死亡)
:progress-status 类型层面能过、运行期零效果,类名仍由 status 与 percent 推导 → C11-00 坑 5;同类还有 C11-02 坑 3(status="active" 在 circle 与 dashboard 上静默无效,因为规则只挂在 line 分支的 -bg::before)、C11-04 坑 7(Statistic 默认插槽声明了但渲染函数从不读)、C11-04 坑 8(给 Countdown 传 precision 与 group-separator 全部无效,因为 formatter 被强行替换)、C11-05 坑 8(#icon 插槽是文档里的死档)、C11-06 坑 1(条目的 position 被 mode 短路)、C11-07 坑 2(改 Descriptions.Item 的 span 默认值没有任何效果,因为 Item 从不实例化)、C11-08 坑 7(<xy-link component="..."> 静默无效,而同族另外三件有效)。formatter 里读 config 永远是 undefined → C11-00 坑 6;根因是 Formatter 类型声明两个入参而唯一调用点只传一个,Countdown 自己的包装函数反倒按两参解构,说明两处代码按不同时期的契约写。B 组:组件自己给 prop 填了默认值,全局配置被静默吃掉
ConfigProvider 或 Form 把全局尺寸设成 small,Spin 的指示器不变小 → C11-01 坑 2(机制在 C11-01 论点 5);同族的另一件是 C11-07 坑 7(Descriptions 的 size 自带 'default' 硬默认值,且 setup 根本没解构 size,双重断路),以及 C11-06 的对照正面例(Timeline 不取 disabled 是正确取舍而非断链,判据是它确实没有可停用的交互,见 C11-06 §一)。反例对照与机制族谱见 C04-08 论点 1 与 A05 论点 3。C 组:子件脱离父件就裸奔(样式没注册、前缀写死、类型面不存在)
Skeleton.Title 得到零高度空标签,而且模板里合法的标签不是你以为的那个 → C11-03 坑 5;同族是 C11-05 坑 7(xy-meter-group-label 前缀写死、样式不注册、百分比文本消失)、C11-06 坑 7(脱离容器的 xy-timeline-item 无圆点无连接线)、C11-08 坑 6(四个子组件标签运行时能解析、类型面不存在)。这四条合起来是生成物口径问题,不是单件缺陷,见 A02 论点 8 与 A02 论点 11。prefixCls 会让只有这一个条目脱离样式表 → C11-07 坑 4;这是「喂给样式 hook 的 prefixCls 不是本组件自己的」那条检查的反向形态——父件的 hook 输入是对的,错在下传旁路。class 与 style 的宿主会随 loading 翻转换人 → C11-03 论点 6。D 组:主题覆盖通道名存实亡
theme.components.Progress 的 token 覆盖被静默丢弃 → C11-02 坑 5;同族还有 C11-06 坑 6(空 ComponentToken 加上 styleFn 内部二次 mergeToken)、C11-04 论点 7(statistic 在 Token 体系里只是类型占位)、C11-05 论点 6(三个 ComponentToken 声明的键从未进入主题登记表)。机制层结论统一归 A03 论点 4,不要在个案里重述。motionDuration* 不会改变本类三条无限循环的速度 → C11-00 坑 4。C11-02 坑 6,这是全库唯一绕过 Token 层直取色板的组件。E 组:数值或时间语义被静默改写,画面看起来「对」但值不对
precision 让它四舍五入,它其实是截断 → C11-04 坑 1;value 给字符串就以为能分组 → C11-04 坑 2(这条是故意的,别顺手改成抛错)。NaN,或 max === min 时,轨道上少一段而图例还在,控制台零告警 → C11-05 坑 2;给 min 传非零值后无障碍播报与画面从此分家 → C11-05 坑 1。success.percent 会改写整体 status、与 percent 抢方向盘 → C11-02 坑 2;type="line" 加了 steps 之后 success 整段失效 → C11-02 坑 1。paragraph.rows 传字符串数字会静默变一行、传非整数直接抛 RangeError → C11-03 坑 2;loading="false" 不带冒号会让骨架永远不消失 → C11-03 坑 1。F 组:事件与真值不是同一个来源
value,屏幕上的文本按新目标走而 change 报的是旧目标 → C11-00 坑 1 与 C11-04 坑 3(两篇独立取证到同一处闭包快照,可互为佐证);用它驱动进度条或埋点会直接写脏数据。value 已是过去,onFinish 一次都不触发 → C11-04 坑 4;归零后 33 毫秒内卸载会补发一次 finish → C11-04 坑 5,回调必须写成幂等。C11-08 坑 3;连点两次复制,成功态会提前 3 秒复位 → C11-08 坑 4。G 组:SSR 与水合
Statistic.Countdown 时首帧文本与属性两处都必然 mismatch,这是架构性的而非能用 nextTick 绕开 → C11-00 坑 7;根因是渲染期直接读时钟,加一个服务端时刻落到 data-for-update 属性上。该属性在现有 $forceUpdate 链里究竟必需还是冗余,深读篇未能取证(见 C11-04 §六 的标注),本篇按「未证实」搬运,不要当结论用。column 恒退化为 3 列(含不写 column 的情形) → C11-07 坑 1。ellipsis 给出不同类名,而本该拦住它的 clientRendered 字段是一个零读取点的死门闩 → C11-08 坑 5;能力探测在模块加载时求值、无 DOM 一律给假,机制归 I14 论点 4。C11-00 论点 5 与 C11-02 论点 7。H 组:可访问性与交互降级
C11-01 坑 1;遮挡只靠 pointer-events,而 pointer-events 不参与焦点序列。焦点圈定这一层本库只有 Modal 一家做了,见 C07-00 论点 4。disabled 在 Typography 只产出一个类名,编辑与复制图标照样能点 → C11-08 坑 1;它命中共则的判据 ① 而非 ②(对照 C06-05 论点 5 是 ① 与 ② 都中、C06-06 论点 2 是更重的双层形态)。顺带一条:即使补上转发,TransButton 的键盘激活路径仍不看 disabled,要真禁用得在 Base 侧早退。C11-00 坑 3,但作用域要按 C11-01 论点 4 收紧:硬切只限指示器 .xy-spin 那一侧,内容容器与面纱都是带补间的,两处不能混谈。C11-04 坑 6(默认值硬编码,组件不读 locale);误以为 -rtl 与 -skeleton 两条选择器有效 → C11-04 坑 11;Skeleton 发出来的 xy-skeleton-rtl 类名不承担任何职责,方向实际由逻辑属性翻转 → C11-03 论点 7。format 返回长文本会溢出,因为尾部文字靠一个硬编码的 2em 沟槽排版 → C11-02 坑 8 与 C11-02 论点 8。I 组:几何与布局外包给了外部,改样式前要先确认祖先链
C11-01 坑 3;给 <Spin style="…"> 写的内联样式只落在指示器而不是外壳或容器 → C11-01 坑 4;删掉 key="loading" 那层匿名 div,覆盖层会丢绝对定位 → C11-01 坑 6(选择器点名「子之孙」)。C11-05 坑 4;用了 #meter 插槽就得自己负责宽高,组件不再写任何几何 → C11-05 坑 3。该篇已注明竖排的最终解析值未取运行期证据,本篇按「依赖调用方给祖先高度」这一结构性结论搬运,不下运行时断言。orientation 或 labelPosition 拼错不报错,且各有一种静默变形态的落点(后者会让图例整体消失) → C11-05 坑 5;color 传一个包含预设色名子串的值会两条路都断 → C11-06 坑 2。C11-03 坑 3;想启用 Skeleton.Image 的微光却发现三个 prop 被整体删掉 → C11-03 坑 4 与 C11-03 论点 4。J 组:正确性挂在书写顺序上
C11-06 坑 5,这是本类最脆的一处:语义正确性挂在对象字面量的书写顺序上,没有任何机制保护。TimelineItem 补 inheritAttrs: false,会静默丢掉全部位置类与末项类 → C11-06 坑 4,因为容器是靠 cloneVNode 往子 vnode 写 class、再靠默认 attrs 透传落地的。K 组:测试面给的假安全感(改本类之前必读)
vitest 全绿而回归不受保护 → C11-03 坑 7(名义 5 个用例、文件内断言数为 0)。index.test.js 名义 3 个 it 只有 1 个有活断言,且唯一覆盖 attrs 落点与叠层隔离的两行被整段注释 → C11-01 论点 6 与 C11-01 坑 4。expect( 计数为 0,且套件名写成 Tag 与 tag 的同名套件并列 → C11-06 §七。expect,而响应式档位由测试 polyfill 决定(只有 xs 可能为真)→ C11-07 §七。vi.mock 让复制恒返回真,于是「复制失败也显示成功」这条坑不可能被现有测试发现;Text.tsx 连 import 都没有 → C11-08 坑 8。useTransitionDuration 的归零判据、gapPosition 四个方向分支都没有测试,vc-progress/ 目录内没有任何测试文件 → C11-00 §七 与 C11-02 §七。C11-03 坑 6、C11-03 论点 6、C11-01 论点 1。核心不是视觉而是两件事:子内容在 loading 为真时整块不渲染,以及两个分支的 DOM 宿主不同构。C11-02 论点 5、C11-00 论点 3、C11-00 坑 5。先决定走 circle 还是 line,因为高频归零出口只有 circle 有;再确认你没有在用一个被同名 computed 遮蔽的死 prop 表达状态。C11-00 论点 1、C11-00 §六、C11-03 论点 7、I09 论点 6。改动点只在样式生成函数里,判形命令以 C11-03 论点 7 那两条为准。C11-04 论点 5、C11-00 论点 4、C11-04 坑 3 与 坑 4 与 坑 5。动手前先补一个 fake timers 用例:C11-00 §七 明确记录整条心跳链路当前零测试保护。size 或 direction → C11-01 论点 5、C11-07 论点 6、C04-08 论点 1、A05 论点 3。先查这个 prop 有没有被 initDefaultProps 填默认值,这是本类最常见的静默断链成因。C11-05 坑 7、C11-06 坑 7、C11-03 坑 5、C11-08 坑 6,再加 A02 论点 8。四件事必须一起补齐:样式注册、前缀走 getPrefixCls、独立样式根、生成物类型面(且不能手改生成物)。C11-02 坑 5、C11-06 论点 7、C11-05 论点 6、A03 论点 4。注意 styleFn 内部二次 mergeToken 会把覆盖整个吃掉,只补 ComponentToken 接口不够。C11-07 论点 1、C11-07 坑 3、C11-07 坑 8、C02-01 论点 2。三件事连着一根线:订阅只在 onBeforeMount 建立、守卫只挡写入不重新注册、退订打在错误对象上导致监听器泄漏(后两条与栅格同源)。C11-08 论点 4、C11-08 坑 2、C11-08 坑 3、I14 论点 4。CSS 与 JS 两条路径由六个开关合谋分流,而分流点依赖一个模块级单例 DOM 与两个加载期求值的能力常量。C11-03 坑 7、C11-01 论点 6、C11-08 坑 8、C11-07 §七。本类的测试普遍是「文件存在但断言为空」的形态,pnpm test:unit 变绿不能作为改动的许可证,尤其是样式层与几何层。