一句话:这份导览只管
07-feedback类里九个组件(外加横写篇C07-00)之间的选型与坑路由;「一个浮层是怎么拼起来的」归 A07,「弹出时序与事件穿透」归 I02,「层级三套账本」归 I10,「焦点陷阱本体」归 C12-07,本导览只做指向,不重述它们的论证。
这一类有九件组件加一篇横写,但它们不是九件并列的东西。C07-00 的五行对照表只覆盖四件——Modal、Drawer、Tooltip、Popover——而它给出的第一个事实就是:这四件在源码里不是四个并列实现,而是两条互不相交的技术栈(C07-00 论点 1)。居中栈里 Modal 挂 vc-dialog、Drawer 挂 vc-drawer,两套底座之间零复用,这是被判为「当前最大的一致性债」的那件事(C07-02 §六);锚定栈是一条单向链,Popover 只是 Tooltip 的一层壳、自己一个状态都不持有(C07-04 论点 6),Tooltip 再往下压给 vc-trigger。剩下五件落在横写表之外,各有各的去处:Popconfirm 整层挂在锚定栈末端(C07-05 §六),Message 与 Notification 共用同一个 vc-notification 内核(C07-06 §一),Alert 与 Result 既不定位也不全局挂载(C07-08 §一、C07-09 §一)。
九件东西回答的其实是同一个问题:一段不属于当前布局的信息,什么时候出现、出现时谁让路、用完之后去哪儿。C07-00 把「出现—让路—去哪儿」拆成五个关注点(打开、关闭、焦点、滚动锁、层叠),并逐行核对四族各自的落点与异步量级(C07-00 §一、C07-00 论点 2)。本类剩下四件则各自只答其中一格:Message 与 Notification 答「用完之后去哪儿」(模块级实例字典),Alert 答「谁让路」(不让路,它折叠自己),Result 五格都不答,它是纯展示叶子(C07-09 §一)。
状态流向在这两条栈上是相反的,这是建立心智模型最省事的一条轴。居中栈把状态留在自己家里:Modal 的关闭语义分散在三个 animatedVisible 上(C07-01 论点 4),Drawer 用 canOpen 首帧门把开合切成两帧(C07-02 论点 2)。锚定栈把状态全部下压:Tooltip 只剩一个 innerOpen,而且受控与非受控是两条互斥写入路径(C07-03 论点 2),Popover 干脆不持有状态、连带丢掉了 Tooltip 的空内容保护(C07-04 论点 6)。告知家族再反过来一次:Notification 把实例按「前缀、方位、rtl」分桶存进模块级字典(C07-07 论点 2),Message 只记一个实例(C07-06 §五),而 Alert 与 Result 一个模块级可变全局量都不碰(C07-08 §六)——同一条「状态该住在哪一层」的谱系,本类横跨了「底座内、组件内、模块全局」三种答案。
归一的现状要如实划边界。真正已经归一的只有入口 prop 那一行:四族对外都收 open、把 visible 当废弃别名、在业务层折成一个内部值(C07-00 §五)。没归一的四行里,「打开」这一行归不了一,因为四族各防一个形态特有的动画竞态(C07-00 论点 2);「卸载」这一行由四套互不相干的闸门决定,方向甚至相反(C07-00 论点 3);「层叠」是静态 Token 偏移加两条死键协调者(C07-00 论点 6)。焦点这一行不在本导览的论证范围——「浮层家族没有接焦点陷阱、只有 Modal 自己手写 Tab 记账」这条判给 A07 坑 12,库内现成实现的机制判给 C12-07 论点 1,本类只在 §三 登记三个症状落点,不重复论证。
最后是本类最容易批量制造事故的一件事:同一族名字在不同栈里方向不同。destroyOnClose、forceRender、handler、destroyPopupOnHide、autoDestroy 这五个布尔分属四个文件,其中一个还是「强制常驻」的反向语义(C07-00 论点 3、C07-02 论点 6)。再加上一条已经存在的偏差:同一个 getContainer 喂给两条栈会得到不同的「要不要走 Portal」判定(C07-00 §五)。所以「关闭后 DOM 在不在」在本类没有跨组件答案,只能逐件核。下面 §二 解决选哪个,§三 解决撞见这个症状去哪一篇。
| 要解决的问题 | 选它 | 而不是另一个 | 判据 |
|---|---|---|---|
| 必须打断用户、拿到一个明确决定 | xy-modal(C07-01) | xy-popconfirm(C07-05) | Modal 的关闭判定有遮罩按下与抬起记账加双 sentinel 圈 Tab(C07-01 论点 2、C07-01 论点 3);Popconfirm 自己不生产弹层容器也不管焦点(C07-05 §一) |
| 内容一屏放不下,还要留着主页面上下文 | xy-drawer(C07-02) | xy-modal | 形态差异是硬的:贴边、单向尺寸、translate 动画(C07-02 §六);赔的是抽屉没有焦点圈定与归还(C07-02 论点 3) |
| 一行解释、出现即走、不接管焦点 | xy-tooltip(C07-03) | xy-popover(C07-04) | 两者共用同一条链与同一份默认值表(C07-00 论点 1、C07-04 论点 5),差别只在内容模型:Tooltip 非受控时空标题不弹(C07-03 论点 1),Popover 没有这层保护(C07-04 论点 6) |
| 就地二次确认,不想遮住触发它的那个元素 | xy-popconfirm | 用 xy-modal 装一个两按钮表单 | 整层复用 Popover,换来定位、动画、点外部关闭全部白拿(C07-05 §六);赔的是 content prop 与 content 槽同时变死 API(C07-05 论点 1) |
| 三秒即走的轻反馈,不需要用户处置 | message(C07-06) | notification(C07-07) | message 把内核默认值改成不可关,closeIcon 已构造却永不渲染(C07-06 §六)——要「用户能关掉」就必须换组件 |
| 带标题与正文、按方位堆叠、允许用户关掉 | notification | message | 一个方位一个容器,缓存键里带方位(C07-07 论点 2);message 侧的 placement 恒被包装层覆盖,改了也无效(C07-06 论点 3) |
| 提示要留在页面流里,跟内容一起服务端渲染 | alert(C07-08) | message | Alert 既不做浮层定位也不做全局挂载(C07-08 §一),且没有模块级可变全局量(C07-08 §六);赔的是关闭状态不持久化,「不再出现」得调用方自己接管(C07-08 §一) |
| 一整页只有这一个结论,含 403、404、500 | result(C07-09) | alert | 一个 status 喂两张互不相干的表:语义态走字体图标、异常码走整页插图(C07-09 论点 1);但非法 status 会直接渲染崩溃(C07-09 坑 1) |
| 要在组件树之外拉起弹层(拦截器、工具函数) | Modal.confirm 静态族 | useModal 加 contextHolder | 静态族自建 DocumentFragment 与一层 ConfigProvider,脱离挂载树也能工作(C07-01 论点 6);useModal 在 holder 渲染前调用会静默丢弹层且无告警(C07-01 坑 7) |
| 要求「关闭后 DOM 真的不留在页面上」 | xy-modal 加 destroyOnClose | 给锚定栈传 destroyTooltipOnHide | 只有居中栈那次会摘掉 Portal;trigger 链只把子树收成空、容器节点仍留在挂载点(C07-00 论点 3、I02 坑 3) |
读这张表有三个提醒。第一,「而不是另一个」列不代表另一个不能用,只代表它在这个判据上有代价:Modal 也能装表单,但它的焦点圈定与滚动锁是遮罩语义的一部分(C07-01 论点 2、C07-01 论点 5)。第二,Tooltip 与 Popover 之间没有第三种生命周期实现,所以选它们实际是在选内容模型和默认值,不是在选浮层机制(C07-00 论点 1、C07-04 论点 5)。第三,命令式与组件式两种用法不是风格偏好:命令式那条路径各自都有一份「静默失败」的分支,具体见 §三 的 B 组。
A 组:配置进不来(声明面活着,读取面死了)
useNotification({ top, maxCount, rtl }) 或 <App :notification> 传进去完全不生效 → C07-07 坑 1;同一条通道在 message 侧是通的,两边 Holder 的 props 表是 10 对 6 的复制未同步(C07-06 论点 1)。useMessage({ getContainer }) 无效,且已挂载的消息不会迁移容器 → C07-06 坑 3;同一份快照语义还命中 prefixCls,动态改前缀时一半更新一半不更新(C07-06 坑 4)。notification.open({ maxCount: 1 }) 仍然堆叠 → C07-07 坑 4(传给内核的是模块级那份,不是这条调用的参数)。config() 想把默认值改回默认,传 undefined 等于什么都没做 → C07-07 坑 7。content 写了没用,description 之类一路下泄却零消费 → C07-05 论点 1 与 C07-05 论点 3。level 与 levelMove 传了没效果 → C07-02 坑 2;Result.PRESENTED_IMAGE_* 与异常码分支下的 icon 是同族死 API(C07-09 坑 3、C07-09 坑 2)。vc-drawer 的 scrollLocker 与 Modal 的那条同名分支都是空转 → C07-02 论点 1 与 C07-01 论点 5,真锁只在 Portal 层,改这两处没用(C07-02 坑 6)。B 组:await、竞态与「调用没生效但不报错」
await message.open() 永不 resolve,而 await notification.open() 立刻返回却什么都没有 → C07-06 坑 1 与 C07-07 坑 5,两条相反的语义在 App 侧还有第三处登记(C12-01 坑 7)。useModal 在 contextHolder 渲染之前调用,弹层静默丢失 → C07-01 坑 7;它的补偿队列只兜 update 与 destroy,不兜「打开」(C07-01 论点 7)。destroy() 之后同一轮立刻 open(),新通知被清掉或旧容器残留 → C07-07 坑 3;键是同步删的,实例销毁在微任务里。message.config({ top }) 一调,屏上消息瞬间消失 → C07-06 坑 7。PurePanel 想动通知结构,线上毫无变化 → C07-07 坑 8(静态 API 自带一份等价 DOM,C07-07 论点 6)。C 组:焦点与键盘(本导览只做路由)
A07 坑 12,本类不复述论证;只登记三个症状落点:抽屉打开后 Tab 走光且关闭后焦点丢(C07-02 坑 1)、trigger="focus" 只是显示动作不是焦点接管(C07-00 坑 4)、Popconfirm 按 ESC 多出一次虚假的 openChange(false)(C07-05 坑 5)。库内现成实现自身的口径见 C12-07 论点 1。C07-01 坑 4,横写侧把这条登记为「归还被 mask 门控」的跨族问题(C07-00 坑 6)。C07-05 坑 6;它不读注入好的 disabled,而按钮读了。D 组:卸载语义与容器落点
C07-00 坑 2;handle 那条分支方向与其余三个相反(C07-02 坑 3、C07-02 论点 6)。getPopupContainer 让 Modal 走 Portal、Drawer 掉进 inline → C07-00 坑 1;两条栈的判据一个用假值一个用严格相等(C07-02 §五)。getContainer 指到局部滚动容器时到底锁不锁。 以 C07-00 论点 5(同 A07 坑 9)为准,C07-01 坑 5 的「没有滚动锁定」只说对了局部容器那一半——容器判据其实是一次空判,body 反被误锁。现跑判据 grep -n "container.value = " packages/xiaoye-ui/src/_util/PortalWrapper.tsx 只得两处赋值(一处置空、一处置本组件自建的默认容器),所以「等于 body」那一项恒假、整句退化成「容器存在即锁」;锁的执行侧选择器写死在 html body 上(C07-00 论点 5)。改法按 A07 坑 9 给的二选一补法走。C07-00 坑 3;三套账本的完整对照与逃逸点登记见 I10 论点 6。vc-dialog 或 vc-drawer,拿到上游前缀或字面 undefined 类名 → C07-01 坑 1 与 C07-02 坑 4;后者业务层路径不触发,且篇内自述只核到传参形状,属未穷尽核查,别当已验证结论用。E 组:样式、层级与动画契约
theme.components.Tooltip 对 Popover 零效果,或在 Popover 里看到 Tooltip 的深色底与宽度限制 → C07-04 坑 4(那个 data-* 短路开关是唯一的互斥机制,C07-04 论点 2)。zIndexPopup 组件 Token 看不到效果 → C07-05 坑 10:同一个弹层根节点同时带两条同特异度规则(C07-05 论点 6);Tooltip 恒压 Popover 是常数差(C07-04 坑 7)。C07-04 坑 3,两侧靠一个字面量相等连接(C07-04 论点 4)。-closing、-no-icon、-closable 改了没反应 → C07-08 坑 6(类名有、规则无);改 _util/transition.tsx 不生效是另一条:无扩展名导入被 .ts shim 重定向到副本(C07-08 坑 7)。afterClose 在单测或 SSR 里等不到回调 → C07-08 坑 1,transitionend 在这两种环境都不派发。F 组:事件双发、覆盖与来源不可分辨
okButtonProps 或 cancelButtonProps 里的 onClick 让按钮失灵 → C07-05 坑 1 与 C07-05 坑 2;Alert 的 role="alert" 被 attrs 顶掉是同族属性顺序问题(C07-08 坑 2)。@confirm 或 @cancel 被调用两次 → C07-05 坑 7(原先按未证降级,2026-09-29 已实测证实并修完):修前一次确定、一次取消各 2 次,修后各 1 次,计数回归 packages/xiaoye-ui/src/_util/__tests__/event-dispatch-count.test.tsx。处置不对称——取消侧只留 emit('cancel', e),确认侧反过来只保留 props 通道 return callEvent(props.onConfirm, e),因为内部 ActionButton 要用返回值决定是否关弹层,emit 拿不到返回值。@change 分不清是遮罩、ESC 还是关闭按钮 → C07-01 坑 3;底座把三种输入统一收进一个内部关闭口(C07-01 §五)。v-model:visible 打不开 Popconfirm,但事件会响 → C07-05 坑 4;同族「入向死、出向活且双触发」见 C07-05 论点 2,生产构建里连告警都没有。#okButton 之后异步语义整体消失:没有 loading、不自动关、可重复点 → C07-05 坑 8。G 组:触发时机、空内容与延迟
C07-03 坑 1 与 C07-04 坑 2,根因是渲染侧与事件侧两处守卫条件不一致(C07-03 论点 1)。C07-03 坑 5;那条内部标记全仓只被读、从未被写(C07-03 论点 6)。align.points 或 builtinPlacements 后箭头失去定位 → C07-03 坑 2;弹出后被自动翻转,动画原点仍是第一次那个角(C07-03 坑 4)。C07-00 坑 5,同一份延迟默认值散落在链的多层上。color 传预置名拿到的颜色比预期深一档 → C07-03 坑 6:预置色走类名取 -6 档、自定义色走内联原值,两条互斥路径(C07-03 论点 5)。C07-03 坑 7;destroyTooltipOnHide 的对象形态在公开类型上不可达(C07-03 坑 8)。测试面(如实,不夸大)。 本类的四个底座目录在开合节奏、卸载闸门、焦点、滚动锁、层级这五行上都没有对应测试(C07-00 §七);业务层侧的缺口分别登记在 C07-01 坑 6(销毁时序无测试网)、C07-02 坑 7(该篇自述未做穷尽核查)、C07-03 坑 3(非零延迟路径完全不执行)、C07-04 §七(插槽优先级与动画配对无断言)、C07-05 §七(确认取消与 ESC 无用例)。也就是说,上面 A 到 G 组的多数分支没有网,改动前的安全边界只能是手工回归「打开、局部滚动、拖窗、移出、重开、焦点回哪儿」整圈,外加各篇 §七 末尾给出的最小电池命令。
C07-00 论点 3(四套闸门与方向镜像)→ C07-02 论点 6 → I02 论点 6(trigger 链的两道不同源闸门)→ C07-00 坑 2。先把语义表写成文档,再谈合并实现。C07-02 论点 3 → C07-00 论点 4(唯一「库里有现成实现、只差接线」的一行)→ C12-07 论点 1 → A07 坑 12。注意归属:这条的论证在 A07,不要在组件篇重开。C07-00 论点 2(四种异步量级、Drawer 首帧门删不得)→ C07-02 论点 2 → I02 论点 3 → C07-00 坑 5。C07-00 论点 5 → A07 坑 9 → C07-01 坑 5(本类唯一一处需要按上面 D 组的仲裁口径改写)→ C07-02 坑 6。这一行是修 bug 而不是做抽象。C07-00 §五(同一件事四组事件名)→ C07-01 坑 3 → C07-05 坑 7 → C07-03 论点 2。涉及公开 API,按破坏性变更评估。C07-00 论点 6(静态偏移是唯一四族都遵守的约定)→ C07-05 论点 6 → C07-04 坑 7 → I10 论点 6。方向是「在 Token 层给相对档位」,不是再补第四套运行时栈。C07-06 论点 1(10 对 6 的两份 props 表)→ C07-07 论点 1 → C07-07 坑 9(两份无继承的参数类型)→ C12-01 论点 9。补一边不补另一边,本类已因此造过静默失效。C07-04 论点 1(类名前缀被夺走)→ C07-04 坑 4 → C07-05 论点 7(默认值写在四张表上)→ C07-03 §六(三份 props 表要长期同步,漏字段无人报错)。useModal 与 useDynamicModal、静态 message 与 hook):C07-01 论点 6 → C07-01 §六(复制而非提取的宿主容器)→ C07-06 论点 4 → C07-07 论点 3。C07-08 论点 1(双状态机加 expose 口)→ C07-08 坑 7 → C07-09 论点 1 → C07-09 坑 1(非法 status 直接崩,类型挡住了但运行时没挡)。