feat(ui): 管理后台弹窗接入原生容器变形(container transform)动画#209
Merged
Conversation
基于浏览器原生 View Transitions API(document.startViewTransition + flushSync), 封装 feature-detect、prefers-reduced-motion 检测与 view-transition-name 的 enter/exit 时序编排,供后续弹窗实现「从来源元素变形展开、关闭收回」动画复用。 附单测覆盖:降级路径、enter/exit 的 name 设置时机、reduced-motion 跳过。
给 DialogContent / AlertDialogContent 增加可选 morph 与 morphName 属性。 morph 为真时去掉默认的 zoom/slide 进出场动画类(运动交由 View Transition 接管), 并在 style 上挂载 view-transition-name。morph 默认 false,现有 30+ 处调用行为不变。
- 上游卡片标记 data-morph-source,编辑/删除按钮把卡片元素作为 morph 源传出 - 上游页用 useContainerMorph 串联打开(enter)/关闭(exit),关闭时收回原卡片或新建按钮 - UpstreamFormDialog / DeleteUpstreamDialog 透传 morph,并把 canMorph 作为开关 (不支持 View Transitions 或 reduced-motion 时回退到原 zoom 动画) - globals.css 为具名过渡对齐 motion 令牌的时长与缓动,reduced-motion 下兜底关闭 - 更新 upstreams-table 组件测试,校验回调新增的 source 参数锚定到卡片
`getErrorTypeLabel` 误用不存在的 `requestLogs.retryErrorType` namespace,
导致控制台报 MISSING_MESSAGE 并把标签回退成原始 key。改为 logs-table.tsx
已验证的写法:useTranslations("logs") + t(`retryErrorType.${type}`)。
当前 next-intl 版本不支持 namespace 内含点号,故必须用顶层 namespace + 点号键。
补充 tests/unit/i18n 回归测试,用真实 next-intl + 真实消息文件复刻该调用形态,
对全部 FAILOVER_ERROR_TYPES 在 en/zh-CN 下断言能解析出真实译文。
沿用上游域样板:keys-table 每行(移动卡片 / 桌面 TableRow)加 data-morph-source,
编辑/撤销按钮 onClick 经 closest 取行元素作为变形源;keys 页面接入 useContainerMorph
串接 enter/exit,弹窗透传 morph={canMorph}。
- edit-key-dialog:DialogContent 加 morph/morphName(默认 morph-key-form)
- revoke-key-dialog:DialogContent 加 morph(morphName 固定 morph-key-revoke)
- globals.css:新增 morph-key-form / morph-key-revoke 具名过渡
- keys-table.test:回调签名增加 source 参数,断言源元素带 data-morph-source
create/show 密钥弹窗暂跳过(CreateKeyDialog 自治且成功后链式弹 ShowKeyDialog,
无离散源元素,需状态提升重构,不在本次范围)。
- portal/keys 页面接入 useContainerMorph:新建按钮、表格行编辑/撤销分别串 enter/exit - portal-keys-table:TableRow 加 data-morph-source,编辑/撤销按钮 closest 取行作源 - portal-key-dialog:DialogContent 加 morph/morphName(create 用 morph-portal-key-create, edit 用 morph-portal-key-edit;两实例独立挂载,名称分开避免任何快照冲突) - portal-revoke-key-dialog:AlertDialogContent 加 morph(morph-portal-key-revoke) - globals.css:新增三个门户密钥具名过渡 - portal-keys-table.test:回调签名增加 source 参数并断言 data-morph-source
header-compensation 页内联弹窗接入 useContainerMorph: - RuleCard 根 div 加 data-morph-source,编辑/删除按钮 closest 取卡片作源 - 新建按钮、编辑、删除分别串 enter/exit(create/edit 共用 morph-compensation-form, delete 用 morph-compensation-delete) - RuleFormDialog:DialogContent 加 morph;删除 AlertDialogContent 加 morph/morphName - globals.css:新增两个补偿规则具名过渡 删除确认沿用上游域先例(DeleteUpstreamDialog 同样变形),与同卡片的编辑入口对称。
用户行的全部操作(编辑/改名/重置密码/配置上游/分配密钥/删除)都在 DropdownMenu 内, 菜单内容经 Portal 挂到 body,closest 取不到行,故 users-table 用行级 ref-map 按 user.id 收集 <tr> 作为变形源。同一时刻只开一个弹窗,六个弹窗共用单个 view-transition-name morph-user-row,避免割裂的「部分变形、部分不变」体验。 - users-table:TableRow 加 data-morph-source + ref 回调;六个回调签名增加 source 参数 - users 页面:openDialog 接收 source 串 enter,closeDialog 串 exit,六弹窗透传 morph (openDialog 非柯里化 + JSX 内联箭头调用,规避 react-hooks/refs 渲染期解柯里化告警) - 六个弹窗组件(含 AlertDialog 形态的删除确认)DialogContent/AlertDialogContent 加 morph/morphName(默认 morph-user-row) - globals.css:新增 morph-user-row 具名过渡 - users-table.test:回调断言增加 source 参数并校验 data-morph-source 新建用户弹窗暂跳过(CreateUserDialog 自治、含 DialogTrigger,需状态提升,与 keys 一致)。
将 CLIProxyAPI 实例与 OAuth 账号的「编辑·查看·删除」弹窗接入原生 View Transitions 容器变形:从所在行/新建按钮展开、关闭收回原处。 实例侧:新建按钮、实例行作为变形源,新建/编辑表单弹窗与删除确认弹窗 共用 morph-cliproxy-instance;账号侧:账号行作为变形源,查看详情、编辑 字段、删除确认三者互斥、共用 morph-cliproxy-account。模型列表、映射上游、 OAuth 登录、上传文件、连通性检测等流程/操作类弹窗按既定语义不做变形。 操作入口均在 DropdownMenu(内容经 Portal 挂到 body,closest 取不到行), 故沿用按 id 收集行元素的 ref-map 方案取变形源。globals.css 追加两个具名 过渡。组件补充行元素作为变形源的回归断言。
价格目录各模型行的「重置/删除手动覆盖价格」按钮(RotateCcw)作为变形源, 确认弹窗从该按钮展开、关闭收回原处;批量重置无单一源元素,弹窗单独淡入 (useContainerMorph 对 source 为空安全降级)。globals.css 追加具名过渡 morph-billing-override-reset。
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #209 +/- ##
==========================================
- Coverage 73.49% 73.48% -0.01%
==========================================
Files 186 187 +1
Lines 12762 12855 +93
Branches 4244 4283 +39
==========================================
+ Hits 9379 9447 +68
- Misses 2094 2114 +20
- Partials 1289 1294 +5
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
给管理后台的「从卡片 / 列表行点开的 编辑·查看·删除·新建」弹窗接入「容器变形」(container transform)动画:点某张卡片的编辑,弹窗从那张卡片的位置和形状「长」出来;关闭时再「收」回原处——动画从哪儿来就回哪儿去,类似手机上点开应用图标展开成全屏的效果。
实现基于浏览器原生 View Transitions API(
document.startViewTransition+flushSync+ CSSview-transition-name),不引入任何 JS 动画库,契合本项目刻意保持的零 JS 动画库栈。不支持 View Transitions 的浏览器或开启「减少动态效果」时,自动回退到现有 zoom/slide 动画,而非瞬时弹出。Related Issue
无(UI 体验增强,无关联 issue)。
Type of Change
Changes
基础设施
useContainerMorphhook(src/hooks/use-container-morph.ts):暴露startMorph(apply, { source, name, mode })与canMorph。按view-transition-name快照时序唯一性约束,分别编排 enter(源元素→弹窗)与 exit(弹窗→源元素)两个方向 name 的设置/清除时机,并用transition.finished.finally+ 卸载 effect 三重兜底杜绝 name 泄漏。reduced-motion 检测复用项目已验证的useSyncExternalStore+matchMedia模式,SSR / jsdom 安全。Dialog/AlertDialog原语(src/components/ui/dialog.tsx、alert-dialog.tsx)新增可选morph/morphName:开启时去掉默认animate-in/out+zoom+slide运动类(保留布局与居中、把运动交给 VT),并在 style 上设view-transition-name。默认关闭,现有 30+ 处调用零行为变化。接入域(弹窗 state 承载页 + 触发表格/卡片 + 弹窗组件三层接线)
直接位于行 / 卡片内的按钮用
event.currentTarget.closest("[data-morph-source]")取源;操作入口在 DropdownMenu(内容经 Radix Portal 挂到<body>,closest 取不到行)的域(users、cliproxy)改用按实体 id 收集行元素的 ref-map 取源。同一页面同一时刻只开一个弹窗,故多个互斥弹窗共用单个view-transition-name。判定边界:仅接入「从某个实体的行/卡片点开、针对该实体本身的 编辑/查看/删除/新建」弹窗;连接测试、OAuth 登录、上传文件、上游映射、模型列表、批量操作、登出确认等流程/操作类弹窗不接入。
i18n 顺带修复:故障规则编辑器把
useTranslations("requestLogs.retryErrorType")改为useTranslations("logs")+t("retryErrorType.${type}"),修正MISSING_MESSAGE控制台报错(requestLogs顶层 namespace 不存在,且 next-intl 不支持带点号的 namespace),并补真实 next-intl + 真实 messages 的契约回归测试。src/app/globals.css:为各具名过渡配置时长/缓动(对齐项目 motion 令牌),对源元素与大表单长宽比差异用object-fit: cover避免内容拉伸;reduced-motion 下整体关闭 VT 动画兜底。Test Plan
pnpm test:run):196 个测试文件、3001 passed / 1 skippedpnpm exec tsc --noEmit)eslint+prettier --check全绿;提交均经 pre-commit 的 prettier/eslint/tsc 钩子)新增/更新测试:
use-container-morphhook 单测、各表格组件回调附带源元素的回归断言、failover-error-type-labelsi18n 契约测试、upstream-morph E2E。Checklist
Additional Notes
layoutId的关键原因:VT 在全页面快照层面工作,源卡片在列表里、弹窗经 Radix Portal 挂在<body>,二者不在同一 DOM 树也能 morph,恰好绕开 Radix Portal 这一最大障碍;且零运行时依赖。morph={canMorph}透传,不支持 VT 或 reduced-motion 时保留现有 zoom/slide 动画。