Files
jiapuapp/docs/superpowers/plans/2026-07-19-app-loading-essential-motion.md
T
2026-07-20 06:52:33 +08:00

7.2 KiB
Raw Blame History

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、聚焦测试和当轮内部证据。
  • 不改变 varianttextdescription 组件契约。
  • 不改变 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 现有 varianttextdescription 属性。

  • 产出:正常模式继续使用 app-loading-seal-breatheapp-loading-knot-breathe;减少动态效果模式使用 app-loading-seal-essential-pulseapp-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 PASSG01-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 后再次读取。预期:

  • reducedtrue

  • 两个 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

预期:两个合同均输出各自的 PASSgit diff --check 退出码为 0。不得把 Android/HBuilderX 写成已验证。

  • Step 8:回到同一加载态等待用户复核

保持浏览器停留在修正后的 G01 加载态,不自动切换到失败态。只有用户明确说加载态“通过”后,才继续 G01 下一状态;在此之前 G01 保持 [!]