# AppLoading 必要加载动效实施计划 > **执行要求:** 使用 `superpowers:executing-plans` 在当前会话内逐步执行。用户明确禁止多代理、worktree、`git add`、commit、push、reset 和 checkout。 **目标:** 修复 G01 在 `prefers-reduced-motion: reduce` 下加载态完全静止的问题,使印牌和如意结保留清晰的透明度呼吸,同时不产生缩放、旋转或位移。 **架构:** 继续由 `components/AppLoading.vue` 单一维护加载动效,不在 G01 复制样式。正常模式保持现有动画;减少动态效果模式改用独立的透明度关键帧,并强制 `transform: none`。测试先锁定旧行为必须失败,再验证静态契约和当前 Chrome 运行时证据。 **技术栈:** Vue 3、uni-app、SCSS、PowerShell 契约测试、Chrome DevTools Protocol、Node.js。 ## 全局约束 - 只修改 G01 加载态直接依赖的公共 `AppLoading`、聚焦测试和当轮内部证据。 - 不改变 `variant`、`text`、`description` 组件契约。 - 不改变 G01 状态逻辑、布局、背景、文案、标题栏或底部导航。 - 不新增图片、CSS 图形、JavaScript 定时器或业务接口。 - `prefers-reduced-motion: reduce` 下只允许透明度变化,`transform` 必须为 `none`。 - H5 截图只是内部候选;Android/HBuilderX 仍未验证。 - 不执行任何 Git 写操作。 --- ### Task 1:修复并验证减少动态效果模式的必要加载反馈 **文件:** - 修改:`tests/app-loading-contract.ps1` - 修改:`components/AppLoading.vue` - 验证:`tests/g01-loading-state-contract.ps1` - 验证:`tests/g-series-app-loading-contract.ps1` - 验证:`tests/module-app-loading-contract.ps1` - 内部证据:`docs/design/screens/runtime/2026-07-19/g01-approval/03-loading-essential-motion-412x915.png` **接口:** - 消费:`AppLoading` 现有 `variant`、`text`、`description` 属性。 - 产出:正常模式继续使用 `app-loading-seal-breathe`/`app-loading-knot-breathe`;减少动态效果模式使用 `app-loading-seal-essential-pulse`/`app-loading-knot-essential-pulse`。 - 不增加新的组件属性、事件或页面状态。 - [ ] **Step 1:先补充会失败的公共契约** 在 `tests/app-loading-contract.ps1` 的现有动画断言之后加入: ```powershell Assert-Match -Content $component -Pattern '@keyframes\s+app-loading-seal-essential-pulse' -Message 'Reduced-motion AppLoading must retain an essential seal opacity pulse' Assert-Match -Content $component -Pattern '@keyframes\s+app-loading-knot-essential-pulse' -Message 'Reduced-motion AppLoading must retain an essential knot opacity pulse' Assert-Match -Content $component -Pattern '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?\.app-loading__seal\s*\{[^}]*animation:\s*app-loading-seal-essential-pulse\s+1\.2s\s+ease-in-out\s+infinite;[^}]*transform:\s*none;' -Message 'Reduced-motion seal must pulse opacity without transform motion' Assert-Match -Content $component -Pattern '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?\.app-loading__knot\s*\{[^}]*animation:\s*app-loading-knot-essential-pulse\s+1\.2s\s+ease-in-out\s+\.2s\s+infinite;[^}]*transform:\s*none;' -Message 'Reduced-motion knot must pulse opacity without transform motion' if ($component -match '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?animation:\s*none') { throw 'Reduced-motion AppLoading must not become completely static' } ``` - [ ] **Step 2:运行聚焦契约并确认 RED** 运行: ```powershell powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1 ``` 预期:退出码非 0,错误首先指向缺少 `app-loading-seal-essential-pulse`,证明测试捕获的是当前完全静止行为,而不是语法或路径错误。 - [ ] **Step 3:实现最小降动效透明度脉冲** 在 `components/AppLoading.vue` 中保留现有正常模式关键帧,并把当前媒体查询替换为: ```scss @keyframes app-loading-seal-essential-pulse { 0%, 100% { opacity: .58; } 50% { opacity: 1; } } @keyframes app-loading-knot-essential-pulse { 0%, 100% { opacity: .28; } 50% { opacity: .86; } } @media (prefers-reduced-motion: reduce) { .app-loading__seal { animation: app-loading-seal-essential-pulse 1.2s ease-in-out infinite; transform: none; } .app-loading__knot { animation: app-loading-knot-essential-pulse 1.2s ease-in-out .2s infinite; transform: none; } } ``` 不得修改模板、属性、尺寸、文案或正常模式关键帧。 - [ ] **Step 4:运行聚焦契约并确认 GREEN** 运行: ```powershell powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1 powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1 ``` 预期:分别输出 `APP-LOADING-CONTRACT PASS` 和 `G01-LOADING-STATE-CONTRACT PASS`,退出码均为 0。 - [ ] **Step 5:在当前唯一 Chrome 标签页验证真实动画** 保持 G01 `state=loading` 和 412×915 视口。通过 9222 CDP 在同一次检查中读取: ```js const seal = document.querySelector('.app-loading__seal') const knot = document.querySelector('.app-loading__knot') const sealStyle = getComputedStyle(seal) const knotStyle = getComputedStyle(knot) ({ reduced: matchMedia('(prefers-reduced-motion: reduce)').matches, sealAnimation: sealStyle.animationName, sealTransform: sealStyle.transform, sealOpacity: sealStyle.opacity, sealAnimations: seal.getAnimations().length, knotAnimation: knotStyle.animationName, knotTransform: knotStyle.transform, knotOpacity: knotStyle.opacity, knotAnimations: knot.getAnimations().length }) ``` 等待约 420ms 后再次读取。预期: - `reduced` 为 `true`; - 两个 `animationName` 分别以新的 essential pulse 名称开头;Vue scoped CSS 可在运行时追加哈希后缀; - 两个 `getAnimations().length` 均大于 0; - 两次读取的透明度不同; - 两个 `transform` 均为 `none`; - 页面仍为 G01 加载态。 - [ ] **Step 6:捕获并人工检查当前候选帧** 使用当前唯一标签页保存: ```text docs/design/screens/runtime/2026-07-19/g01-approval/03-loading-essential-motion-412x915.png ``` 人工检查:标题栏、背景、印牌、如意结、两行文案和底部导航均完整;没有列表、失败态或原生 Loading 串入。静态截图不作为动画运行证明,动画结论以 Step 5 的多时点数据为准。 - [ ] **Step 7:运行受影响合同与空白检查** 运行: ```powershell powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g-series-app-loading-contract.ps1 powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/module-app-loading-contract.ps1 git diff --check -- components/AppLoading.vue tests/app-loading-contract.ps1 docs/superpowers/specs/2026-07-19-app-loading-essential-motion-design.md docs/superpowers/plans/2026-07-19-app-loading-essential-motion.md ``` 预期:两个合同均输出各自的 `PASS`,`git diff --check` 退出码为 0。不得把 Android/HBuilderX 写成已验证。 - [ ] **Step 8:回到同一加载态等待用户复核** 保持浏览器停留在修正后的 G01 加载态,不自动切换到失败态。只有用户明确说加载态“通过”后,才继续 G01 下一状态;在此之前 G01 保持 `[!]`。