范围:本类 14 篇(
C03-00类横写 +C03-01至C03-13十三篇单体)之间的跨篇路由——近义组件怎么选、我撞到的症状归哪一篇。不负责:浮层的弹出时序与坐标求解(归A07与I02)、受控与非受控 hook 的内部实现(归I13)、卡片与面板的页签装配(归C02-07与C02-08)、层级编排与动画本体(归I10与I09)。另有一条现实约束要先声明:C03-00的 §五、§六、§七 三段尚未成文,本篇引用它时只引 §三 与 §四 的论点,不引它的坑编号。
导航类对外只回答一句话:当前是哪一项。这句话在本类没有任何共享实现。C03-00 把四套形态钉到了具体行号上,结论是「选中项的身份取自哪里」先把组件切成两个阵营——取 VNode 的 key 的(Menu 与 Tabs)与取渲染下标的(Steps 与 Breadcrumb 的序号链)(C03-00 论点 1)。导览要补的是其余三种身份策略:面板菜单在根层比较的是整个对象的深相等(C03-03 论点 6),右键菜单与面板菜单的递归子层各有一套「父键加下标」的位置键(C03-09 论点 3、C03-03 取舍三),锚点则把身份交给 href 字符串,由链接子组件在挂载时自报进一张运行时常量表(C03-10 论点 4)。所以选型的第一问不是「用哪个组件」,而是我的数据能不能在每次重建后仍给出同一个标识:不能的话,上面五种策略里只有深比较那一种能续住展开态,而它续住的代价是项内容自身一变就失联(C03-03 坑 3)。
第二件事是状态持有者落在四个不同层级,C03-00 §三 已把它画成图:Menu 在根组件、Tabs 在根组件的内层实例、Steps 在调用方(组件内部一个 ref 都没有)、Breadcrumb 在数据本身。本类其余各篇把这条轴拉得更长:分页的状态机干脆是 Options API 的 data() 加四个 watch,适配层一行响应式状态都没有(C03-07 论点 3);下拉的开关状态是「两处存、一条回流」(C03-08 论点 4);引导里同时存在两个互不同步的「是否打开」,一个是归一化过的、一个是裸 prop(C03-13 论点 3)。读任何一篇之前先定位「谁写、谁读」,比背 props 名单有用。
第三件事是选中落到视觉只有两条通道:类名通道与几何通道,后者额外引入一帧延迟与一张可能过期的量表(C03-00 论点 5)。走几何通道的是标签页的墨条与滚动位移(C03-05 论点 4)和锚点的墨条(C03-10 论点 7);步骤条是这条轴上的极端反例,它全程不读 DOM 尺寸,连线几何交给 CSS(C03-06 取舍三)。凡遇「指示条位置不对」这类症状,先判组件在哪条通道上,再决定是去排查量表过期还是排查类名生成——这两条排查路径互不通用。
第四件事是触发方式与逃逸能力,它把本类分成三条互不隶属的线:常驻内联(Menu 的 inline、PanelMenu、MegaMenu)、浮层(Dropdown、ContextMenu、Tour)、几何驱动(Anchor、Affix、BackTop)。三条线的分水岭只有一个问题:面板能不能逃出宿主的裁剪与层叠上下文。大图菜单明确没有接浮层三件套,面板是绝对定位的后代,没有翻转、不引 Portal、层级永远压在局部 zIndex: 1 上(C03-02 论点 5、C03-02 取舍一);右键菜单同样自建浮层,因为它的锚点是光标位置而不是元素矩形,接进 trigger 得先伪造一个零尺寸目标(C03-09 取舍第一条);真正把这条链交给 vc-trigger 的只有下拉与引导,而下拉的厂商层只带一张六键定位表,与全库通用那张十二键的键名集合不同(C03-08 取舍第二条)。
第五件事:受控语义在本类是三套而不是一套,I13 的口径在这里会被第三方状态旁路。大图菜单把 hover 排在受控分支之前,鼠标停留在菜单内的任何时刻受控值都被绕过(C03-02 论点 1);下拉里「点菜单项关闭」这条路径直接写本地状态,于是一个事件都不发(C03-08 论点 4);标签页曾经每一层都同时做 emit 与 callEvent,父层传下来的回调正是这次 emit 的目标,实测一次点击把 handler 放大到 16 次——2026-09-29 已改成每层单投(C03-05 论点 6、坑 2);固钉的状态干脆不是用户可控的值而是「上一次几何快照」,所以它有意不接受控 hook(C03-11 取舍第三条)。判形所需的五种谓词写法由 C03-00 论点 2 统一登记;而「选中」与「激活」是两条独立轴、本类只有 Menu 把两条都实现这件事,记在 C03-00 论点 3。
最后一件事必须写在选型表之前:本类的测试面基本不构成反馈。Menu 的形态切换用例处于 skip、测试里还在写一个源码中已不存在的内部字段(C03-01 检查点 3);锚点名义 8 个用例实跑 2 个(C03-10 坑 6);固钉存活的断言全在证明「某个不存在的属性依然是假」(C03-11 坑 7);步骤条、分页、下拉、引导四家只剩 mountTest 量级的用例(C03-06 §七、C03-07 §七、C03-08 §七、C03-13 §七)。因此 §四 每条改动路线都把「先补哪一条断言」排在「改哪个文件」之前。
| 要解决的问题 | 选它 | 而不是另一个 | 判据 |
|---|---|---|---|
| 站点主导航:多级嵌套 + 折叠续命 + 水平溢出收纳 | C03-01 Menu | C03-02 MegaMenu | Menu 把折叠实现成「改写 mode 为 vertical 再加一枚标记」,代价是所有 mode === 'inline' 判定在折叠期自动失效(C03-01 取舍一);MegaMenu 与 Menu 只共享命名约定、不共享状态实现,Menu 侧对选中轴的修正不会自动修好它(C03-02 取舍五) |
| 顶栏 hover 展开的多列宫格,且祖先没有可滚动容器 | C03-02 MegaMenu | C03-08 Dropdown | 面板全量常驻 DOM、由 CSS 决定可见,激活与否从不裁剪渲染(C03-02 论点 2);一旦需要逃逸 overflow、需要翻转与全局层级,MegaMenu 一条都给不出(C03-02 论点 5),那就只能换成接到 trigger 的下拉 |
| 侧栏 accordion:面板就地撑开、内容由 model 数据生成 | C03-03 PanelMenu | C02-09 Collapse | PanelMenu 的面板由数据生成,走不通 Collapse 那条「面板必须以子组件声明」的路径,于是重复实现了一整套折叠语义(C03-03 取舍二);由此读 Collapse 得到的动画结论在这里不成立(C03-03 论点 3) |
| 一个按钮背后只有几个命令 | C03-08 Dropdown | C03-01 Menu | 下拉的 overlay 里的 Menu 被 clone 两次、两次合并语义相反,所以「强制竖排」成立而「强制关闭回调」不成立(C03-08 论点 1);它看起来更干净的数据化入口 menu prop 只有声明、零读取点(C03-08 坑 1) |
| 光标位置的上下文操作 | C03-09 ContextMenu | C03-08 Dropdown | 坐标是 pageX 与 pageY 的一次直写,四条边界钳制之外没有任何定位算法,也就没有翻转与避让(C03-09 论点 2);global 模式只在挂载时决定挂不挂文档级监听,运行中改 prop 不生效(C03-09 坑 3) |
| 文档站目录:滚动时点亮当前章节并可点跳 | C03-10 Anchor | C03-11 Affix | 锚点的判定语义不是「谁在视口内」而是「谁越过了阈值线、取其中最靠下的一个」(C03-10 论点 2);它把钉住能力整体外包给固钉,代价是 offsetBottom 与 onChange 从锚点这一层永远触达不到(C03-10 取舍第二条) |
| 只把一块内容钉在视口边上,不参与任何「当前项」语义 | C03-11 Affix | C03-10 Anchor | 固钉必须做「目标顶边与占位顶边的带符号距离」算术,可见性观测给不出「现在该写多少 px」,所以它只能事件驱动(C03-11 取舍第一条);横向落位它从不写 left,祖先带 transform 时钉住项会跟着跑(C03-11 坑 4) |
| 页面级回顶:只要显隐加一次滚动 | C03-12 BackTop | C03-10 Anchor | 回顶只比较数值阈值、完全不测 DOM 位置,因此隐藏态不需要占位与还原(C03-12 取舍一);锚点则与 URL 彻底解耦,不写 hash、不读 hash、也不拦默认跳转(C03-10 论点 5) |
| 线性流程的进度指示与可点回退 | C03-06 Steps | C03-07 Pagination | 步骤条没有嵌套、没有溢出折叠、也不需要键盘在集合内移动,所以它可以省掉注册表(C03-06 取舍二);分页则相反,它的页码窗口需要「窗口加覆盖首末项加两条邻接排除」这套完整计量(C03-07 论点 4) |
| 数据集切片、页码与每页条数 | C03-07 Pagination | C03-05 Tabs 的溢出下拉 | 分页所有事件都不走 Vue 的 emit,emits 选项被注释掉了(C03-07 论点 2),与标签页「曾经每层投递两次」是两种相反的病(C03-05 论点 6,后者 2026-09-29 已修为单投);分页的全局配置通道还是断的,ConfigProvider 的 pagination 键是死键(C03-07 论点 6) |
| 同屏切换多个平级视图、切走的面板要保活 | C03-05 Tabs | C02-07 Card 的 tabList | 标签页的面板不是自己渲染的,而是把用户写的 TabPane 的 VNode 当数据载荷跨树传递,cloneElement 的默认覆盖语义会拿组件级值盖掉面板级值(C03-05 论点 1);卡片那侧只是把 tabList 翻译成一套 Tabs 节点,受控与非受控的差别被压缩成一次换键名(C02-07 论点 3) |
| 分步骤向导里「这一步填完才能下一步」的禁用推进 | C03-06 Steps | C02-09 Collapse 的逐段展开 | 步骤条的 current 是绝对序号且真源在调用方(C03-00 §三),禁用推进只需要管一个数;但它的 disabled 挡不住条目自带的 onClick(C03-06 坑 7),这一条必须自己补 |
两条需要单独说明的轴:
Tabs 与 Card 的「页签复用」不是 CardLike 复用。 仓内曾经流传「_shared/CardLike 被 Card 与 Panel 共同复用」的说法,C02-07 论点 6 已经把它否证:Card 与 CardLike 是平行副本,bordered、hoverable、loading 三个类名开关与整套 Token 分支被写了两遍且已经不等价(C02-07 取舍一);CardLike 的唯一消费者是 Panel(C02-08 论点 1)。所以「卡片带页签」这件事的真实依赖是 Card 直接值引用了重组件 Tabs(C02-07 取舍三),要改页签机制本体一律回到 C03-05,改 Card 那侧只会动到装配顺序;顺带两条既有后果是卡片内页签的 size 被硬写成 large(C02-07 坑 3)与 xy-card-head-tabs 是产出但零规则的死类名(C02-07 坑 10)。
「vc-overflow 是 Menu 与 Tabs 共用的溢出容器」这句横写篇的表述不成立,以 C03-05 为准。 这里发生了本目录唯一一次需要回源码仲裁的分歧:C03-00 §一 的依赖清单把 vc-overflow 写成「Menu 与 Tabs 的溢出容器」,C03-01 §七 的改动边界进一步要求「动 vc-overflow 时必须同时跑 Tabs,因为 TabNavList 也依赖它」;而 C03-05 §六 明确主张标签页用自研测量、不走 vc-overflow,并在检查点里给出了预期为零的判据。现 grep 两条:grep -rn -e vc-overflow packages/xiaoye-ui/src/tabs 输出 0 行;grep -rln -e vc-overflow packages/xiaoye-ui/src 只命中 menu/src/Menu.tsx、menu/src/SubMenu.tsx、menu/src/MenuItem.tsx、vc-select/Selector/MultipleSelector.tsx 四个文件。结论:标签页的溢出确实是独立的一套双向扫描算法(C03-05 论点 3),Tabs 目录对 vc-overflow 零直接依赖。但「改 vc-overflow 要顺带跑 Tabs」这条操作建议本身仍然有效,只是理由要换——耦合走的是「Tabs 的溢出下拉内嵌了一个 Menu 实例」这条二级路径(C03-00 论点 4、C03-01 §一 的被依赖清单),不是 Tabs 自己引了溢出容器。日后引用这条时请写「经 Menu 间接影响」,别再写「TabNavList 依赖 vc-overflow」。
C03-02 坑 7(DOM 上写着 menubar 与 menuitem 语义,却没有可聚焦目标);同族症状分别记在 C03-03 坑 5(Tab 进得去、方向键出不来)、C03-09 坑 1(键盘处理器声明成 emits 却从不 emit)、C03-05 坑 5(tablist 根本不支持方向键切换)、C03-12 坑 4(能聚焦不能激活)、C03-04 坑 6(无 href 的可点条目不可 Tab 到)。注意 Menu 的「方向键看起来没反应」是另一种成因:第二轴只有鼠标与焦点三个写入源(C03-01 坑 4)。C03-02 坑 1;同一条优先级链的另一半是 C03-02 坑 2(停在面板里点叶子不关闭)与 C03-02 坑 6(受控初值非空时没有外部点击关闭通道),三条都源于合并式激活态里 hover 排在最前(C03-02 论点 1)。C03-08 坑 4,非受控看 C03-08 坑 5,两条共用同一个根因:关闭路径绕过 onVisibleChange 直接写本地状态(C03-08 论点 4)。C03-13 坑 1;根因是组件里存在两个互不同步的「是否打开」(C03-13 论点 3)。同类「同一语义两个数据源」的还有 C03-13 坑 3(步骤级 arrow 只关掉面板里那个节点)。C03-03 坑 2(子层类名是硬编码字面量)、C03-09 坑 2(类名与注入前缀脱钩,菜单塌成裸列表)、C03-06 坑 8(图标类名由根前缀拼、样式表把名字写死);卡片与面板那侧的同一族版本在 C02-08 坑 1。C03-08 坑 1(menu)、C03-08 坑 2(Dropdown.Button 的 size)、C03-08 坑 6(白名单之外的键静默丢弃)、C03-07 坑 3(selectComponentClass 的声明与用法相反)、C03-10 坑 4(showInkInFixed 的选择器匹配不到任何元素)、C03-02 坑 8(死样式规则与死插槽名)、C03-01 坑 8(想经 ref 调方法拿到 undefined)。这批的共同判据是「类型面比实现面宽」,逐项核对办法见 A06。C03-03 坑 3(整对象深比较)、C03-09 坑 5(下标拼出的 key 随顺序漂移)、C03-09 坑 6(分隔符占下标不占视觉)、C03-06 坑 5(items 里的空位重排所有后续序号)、C03-04 坑 4(末项按对象引用判定,重复引用即误判)、C03-02 坑 4(不安全 URL 被降级成空 href,链接仍可点;后半句(空 href 会原地自跳、带 target 会开当前页副本)已在真实浏览器实测成立)。C03-05 坑 3(受控删掉当前活动标签后停在无面板态)、C03-07 坑 5(受控 current 与缩小的 total 分叉)、C03-01 坑 1(折叠一次就向 openChange 抛空数组,受控方据此回写会钉死展开态)、C03-01 坑 2(用 mode 直接切形态时展开态不延续,对应测试恰是 skip)。C03-12 坑 3(隐藏只做透明度,没有指针屏蔽)、C03-01 坑 7(body 里多出一棵隐藏菜单,按数量断言与快照全部失真)、C03-03 坑 6(折叠项仍被类名查询算进结果)、C03-03 论点 7(数据侧可见与 DOM 侧可见是两套口径)、C03-02 取舍二(面板全量常驻,代价由项数买单)。C03-01 坑 5(水平菜单首帧全展开再突然收成省略号)、C03-05 坑 7(左位置的窗口判定读写不同轴)、C03-05 坑 4(下拉里首按方向键跳到的不是第一项)、C03-05 坑 6(字体变化后墨条宽度与折叠集合双双过期)、C03-02 坑 5(列数不在映射表内时静默塌列)。C03-10 坑 1(隐藏目标伪装成已滚过阈值线、抢走高亮)、C03-10 坑 2(offsetTop 有四个消费方,无法单点调整)、C03-10 坑 7(容器漂移只在 onUpdated 里纠正)、C03-11 坑 3(target 指向挂载后才出现的节点时完全不生效)、C03-11 坑 5(内容高度在钉住后变化,占位不跟随)、C03-13 坑 4(目标比视口高时每轮重算都触发一次滚动)。C03-11 坑 6 也属这一族,但它的最后一环依赖渲染器语义、源码内没有可引用位置,深读篇已就地降级为条件句,引用时不要把它当已证事实。C03-05 坑 2(Tabs 四层,实测修前点一次标签 tabClick/change 各 16 次、点一次加号 edit 4 次)、C02-07 坑 9(Card 两层 = 2 次)、C12-04/C07-05/C06-03/C05-01/C05-03/C05-04/C05-06/C05-07/C03-13 各有同形站点,全部改完;机制是每层同时 emit 与调用同名 prop 回调(callEvent/callPropEvent/直接 props.onX?.() 三种写法),再叠上外层把用户处理器与自己的桥接 arrow 一起透传(mergeProps 并数组)。计数回归:tabs/event-dedupe.test.tsx 与 packages/xiaoye-ui/src/_util/__tests__/event-dispatch-count.test.tsx(23 条)。@change 投两遍 → C03-06 坑 10(2026-09-30 已修:修前实测 2 次、修后 1 次,回归 event-dispatch-count.test.tsx:414)。同一篇里还留着一条已撤回的假缺陷:初稿曾说「@change 根本没有投递路径」,那是探针点了当前项造成的假阴性——vc-steps/Steps.tsx:39 的 current !== next 会短路同值重发;而 emit('stepClick') 恰好命中容器传下来的 onStepClick prop,链路本来是通的。测这类事件必须点非当前项。C03-10 坑 6(名义 8 个用例实跑 2 个)、C03-11 坑 7(断言的属性根本不在暴露面上,所以永远为假)、C03-01 检查点 3(测试里仍在写源码中已不存在的内部字段)、C03-02 论点 2 末段的验证陷阱(面板常驻让「存在性」断言失去区分度)、C03-07 坑 1(emits 键名拼错,按事件声明审计会得到空名单)。disabled 或 ConfigProvider 在全局这一层被静默吃掉 → C03-07 坑 4(pagination 是死键、disabled 无接入点)、C03-08 坑 7(disabled 只清触发行为,挡不住受控展开)、C03-06 坑 7(disabled 挡不住条目自带的点击)、C03-02 坑 8(分组压根没有 disabled 字段,禁用样式无从命中)、C02-07 坑 3(size 与全局尺寸对卡片无视觉差);A07 坑 1 与坑 2 是浮层那侧的同族版本。C03-04 坑 1(默认渲染器写死 hash 形式,history 模式不可用)、C03-04 坑 2(params 缺键时名称侧仍显示 :id 字面量、路径侧出现 undefined 字样)、C03-04 坑 3(键名含正则元字符会让替换失配)、C03-10 坑 3(一次点击同时跑组件缓动与浏览器默认跳转,锚点子节点没有阻止默认行为)。routes 与插槽,或同时传 items 与子组件,其中一路静默消失 → C03-04 坑 8(routes 非空时默认插槽被忽略)、C03-06 坑 4(两路混写必然重复 key)、C03-06 坑 6(子组件标签里的文本被静默丢弃)、C03-08 坑 9(多节点插槽静默截断取首项)、C03-06 坑 9(窄视口下 direction 被响应式断点覆盖)。C03-01 取舍一(现取舍的全部补偿代码都挂在这条选择上)、C03-01 坑 1 与坑 2(缓存与恢复的两条 watch 是最脆的分支)、C03-00 论点 1(身份来源一旦改变,下标阵营的组件都要重算)。放开那条 skip 用例是动手的第一步,别最后一步才做。C03-02 论点 5 与取舍一(这等于给现有取舍翻案)、C03-02 论点 6(外部点击监听按合并后的激活态挂载,改动会同时打断它)、C03-02 坑 2(现有用例依赖「面板在容器内」这一前提)、I02(浮层时序本体)。C03-00 论点 2(五种谓词写法的普查)、C03-13 论点 3(归一化值与裸 prop 并存的样本)、C03-05 论点 6(双投递会让统一后的事件面立刻暴露)、C03-07 论点 2(分页底座完全不走 emit,是唯一需要单独谈的一家)、I13。C03-00 论点 4(三份实例与两套轴的总账)、C03-01 取舍二(不卸载改样式屏蔽的代价)、C03-05 论点 3(Tabs 那套独立算法,注意 §二 末尾的仲裁结论:它与 vc-overflow 无直接依赖)、C03-01 坑 7(body 镜像对断言与快照的影响)。C03-03 取舍一 与 C03-09 论点 3(两篇共用「下标即身份」这一族,改法必须一致,否则同一次数据重建在两边表现不同)、C03-03 坑 3、C03-09 坑 5 与坑 6(aria 与序号补偿靠手工减法)。C03-02 坑 7、C03-09 论点 7、C03-03 论点 5、C03-12 坑 4、C03-00 论点 3(先决定键盘作用在「选中轴」还是「激活轴」,Menu 的第二轴目前由鼠标与焦点驱动、不是方向键),再决定要不要一并修 C03-01 坑 4。C03-10 论点 3(视口相对量与文档坐标两套坐标系靠一次相加圆回来)、C03-11 论点 4(伪矩形让整套判据只依赖两个数)、C03-12 论点 1 与论点 2(目标归一的第四路是给测试留的后门;节流壳会把同帧后续事件的参数锁死)、C03-10 坑 7 与 C03-11 坑 3(容器漂移在两处各有一份,且互不共享重试)。C03-00 论点 5(两条通道的本质差别与那一帧延迟)、C03-05 论点 4(位移是真源、滚动是推导量,所以需要一个同步回调)、C03-10 论点 7(锚点靠一次 querySelector 加四次内联赋值)、C03-10 坑 4(改之前先确认那条 CSS 选择器现在根本命中不到元素)。C02-07 论点 2(头部是一个槽位装两段,页签条被塞在标题行之后)、C02-07 论点 3(受控与非受控靠换键名而不是传 undefined)、C03-05 论点 1(面板级开关会被组件级值覆盖)、C02-07 坑 10(想覆盖页签条样式时该用哪条选择器)。CardLike 那条路不要走,见 §二 的否证。C03-07 论点 6 与坑 4(注入链在最后一步断掉的具体位置)、C03-06 取舍一(双层结构带来两份 props 表与三份默认值,加默认值会静默改变可见行为)、C03-07 论点 1 与坑 7(适配层只写展开顺序,动 attrs 位置会同时影响图标自定义与样式哈希)。