Skip to content

feat(ui): 管理后台弹窗接入原生容器变形(container transform)动画#209

Merged
g1331 merged 11 commits into
masterfrom
feat/container-morph-dialog
Jun 13, 2026
Merged

feat(ui): 管理后台弹窗接入原生容器变形(container transform)动画#209
g1331 merged 11 commits into
masterfrom
feat/container-morph-dialog

Conversation

@g1331

@g1331 g1331 commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Summary

给管理后台的「从卡片 / 列表行点开的 编辑·查看·删除·新建」弹窗接入「容器变形」(container transform)动画:点某张卡片的编辑,弹窗从那张卡片的位置和形状「长」出来;关闭时再「收」回原处——动画从哪儿来就回哪儿去,类似手机上点开应用图标展开成全屏的效果。

实现基于浏览器原生 View Transitions APIdocument.startViewTransition + flushSync + CSS view-transition-name),不引入任何 JS 动画库,契合本项目刻意保持的零 JS 动画库栈。不支持 View Transitions 的浏览器或开启「减少动态效果」时,自动回退到现有 zoom/slide 动画,而非瞬时弹出。

Related Issue

无(UI 体验增强,无关联 issue)。

Type of Change

  • New feature (non-breaking change that adds functionality)
  • Tests (adding or updating tests)

Changes

基础设施

  • 新增 useContainerMorph hook(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.tsxalert-dialog.tsx)新增可选 morph / morphName:开启时去掉默认 animate-in/out + zoom + slide 运动类(保留布局与居中、把运动交给 VT),并在 style 上设 view-transition-name。默认关闭,现有 30+ 处调用零行为变化。

接入域(弹窗 state 承载页 + 触发表格/卡片 + 弹窗组件三层接线)

  • 上游 upstreams(编辑 / 新建 / 删除 / 测试连接,作为完整样板)
  • API 密钥 keys(编辑 / 撤销)
  • 会员门户 portal 密钥(新建 / 编辑 / 撤销)
  • 头部补偿规则 header-compensation(新建 / 编辑 / 删除)
  • 用户管理 users(编辑 / 改用户名 / 重置密码 / 配置上游 / 分配密钥 / 删除)
  • CLIProxyAPI cliproxy(实例新建/编辑/删除、账号查看详情/编辑字段/删除)
  • 计费 billing(价格覆盖重置/删除确认)

直接位于行 / 卡片内的按钮用 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 skipped
  • 类型检查通过(pnpm exec tsc --noEmit
  • Lint / 格式通过(改动文件 eslint + prettier --check 全绿;提交均经 pre-commit 的 prettier/eslint/tsc 钩子)
  • 手动测试:上游域样板已在浏览器确认手感(卡片→弹窗变形展开、关闭收回原位),其余域复用同一 hook 与同一接线模式

新增/更新测试:use-container-morph hook 单测、各表格组件回调附带源元素的回归断言、failover-error-type-labels i18n 契约测试、upstream-morph E2E。

Checklist

  • Code follows the project's coding standards
  • Tests have been added where necessary
  • Documentation has been updated (if applicable)(纯 UI 行为增强,无需文档变更)
  • Changes do not introduce security vulnerabilities
  • Commit messages follow conventions

Additional Notes

  • 选用原生 View Transitions 而非 framer-motion layoutId 的关键原因:VT 在全页面快照层面工作,源卡片在列表里、弹窗经 Radix Portal 挂在 <body>,二者不在同一 DOM 树也能 morph,恰好绕开 Radix Portal 这一最大障碍;且零运行时依赖。
  • 降级路径已覆盖:morph={canMorph} 透传,不支持 VT 或 reduced-motion 时保留现有 zoom/slide 动画。

g1331 added 10 commits June 13, 2026 17:50
基于浏览器原生 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

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.16667% with 37 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.48%. Comparing base (01c7578) to head (44b724f).
✅ All tests successful. No failed tests found.

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     
Flag Coverage Δ
verify 73.48% <69.16%> (-0.01%) ⬇️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@g1331 g1331 merged commit 1db96c2 into master Jun 13, 2026
12 checks passed
@g1331 g1331 deleted the feat/container-morph-dialog branch June 13, 2026 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant