7.2 KiB
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 的现有动画断言之后加入:
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.exe -NoProfile -ExecutionPolicy Bypass -File tests/app-loading-contract.ps1
预期:退出码非 0,错误首先指向缺少 app-loading-seal-essential-pulse,证明测试捕获的是当前完全静止行为,而不是语法或路径错误。
- Step 3:实现最小降动效透明度脉冲
在 components/AppLoading.vue 中保留现有正常模式关键帧,并把当前媒体查询替换为:
@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.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 在同一次检查中读取:
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:捕获并人工检查当前候选帧
使用当前唯一标签页保存:
docs/design/screens/runtime/2026-07-19/g01-approval/03-loading-essential-motion-412x915.png
人工检查:标题栏、背景、印牌、如意结、两行文案和底部导航均完整;没有列表、失败态或原生 Loading 串入。静态截图不作为动画运行证明,动画结论以 Step 5 的多时点数据为准。
- Step 7:运行受影响合同与空白检查
运行:
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 保持 [!]。