feat(#1741): 批量预览卡片缩小至3/5 + 每个视频播放都有声音(配音全挂载+播放互斥+静音按钮) #1742

Merged
auto-approve-bot merged 1 commits from feat/1741-preview-grid-audio into develop 2026-09-06 15:41:36 +08:00
Owner

问题

Issue #1741(staging 实测):

  1. 批量预览网格固定 2 列,9:16 卡片被列宽撑满,单卡几乎占满整屏,一屏看不了几个视频
  2. 视频 2/3… 播放完全无声:voiceAudioUrl={i === 0 ? voiceAudioUrl : undefined} 配音只挂第 1 个变体;且所有 <video> 硬编码 muted,无配音时素材原声也被静音

改动

卡片缩小至约 3/5(generate.css

  • .xx-canvas-gridgrid-template-columns: repeat(auto-fill, minmax(200px, 220px)) + 居中:宽屏自动排 3 列(step4 预览列约 700~900px 内容宽下),中屏 2 列,窄屏(≤960px)单列限宽 320 居中
  • 卡片面积约为原来的 0.36(≈3/5),9:16 比例不变、画面不裁切
  • 控制条/片段指示器/静音按钮 compact 尺寸适配(26px 按钮、紧凑 padding),播放与勾选热区不缩到难点

每个视频都有声音(FrontendPreviewPlayer.tsx / CanvasPreviewGrid.tsx

  1. 配音挂所有变体:去掉 i === 0 限制,N 个实例都传 voiceAudioUrl(同一条 URL 浏览器缓存,不重复下载);现有 audio 随播放/暂停/seek/片段切换同步逻辑对每个实例生效
  2. 音轨与成片一致:有配音时 video 保持 muted(素材原声不与配音混音);无配音时 video 取消 muted,素材原声兜底,任何情况播放都有声(顺带修了单视频无配音也静音的旧问题)
  3. 播放互斥:新增 activePlayToken / onPlayTokenChange(token = variantSeed)。点某卡片播放→上报自身 token,Grid 内所有实例同步;token≠自身且正在播放的实例 effect 里自动暂停(视频+配音都停);暂停/播完上报 null 释放
  4. 静音按钮:每张卡片左上角喇叭按钮(AudioOutlined/AudioMutedOutlined),可单独静音;默认有声;有配音时静音配音 Audio,无配音时静音素材 video
  5. 自动播放策略不变:声音在点击手势后触发,play() reject 静默 .catch 兜底不阻塞画面
  6. 单视频模式不传互斥 props,行为零回归

测试(+12 例,全量 705 passed)

  • CanvasPreviewGrid.audio.test.tsx(5 例,mock player):N=3 每个变体都拿到 voiceAudioUrl;互斥 props 接线;实例2播放→所有实例 token=2(其他自动暂停);暂停上报→token 释放;全部 has-voice
  • FrontendPreviewPlayer.audio.test.tsx(7 例,mock scheduler 控制播放态):有配音 video muted / 无配音 video 不 muted;静音按钮切换 muted+aria;互斥 effect(token 变他人且播放中→pause,切回不重复暂停);未播放不暂停;点播放上报自身 token 并触发 toggle;每卡片独立静音按钮
  • tsc / eslint(0 warning)/ prettier / build 全绿

验收

staging 第3步批量预览:卡片明显变小、一屏 3 个;视频 1/2/3 分别点播放都能听到配音,同时只有一个在响;喇叭按钮可单独静音;无配音素材播放有原声。

Closes #1741

## 问题 Issue #1741(staging 实测): 1. 批量预览网格固定 2 列,9:16 卡片被列宽撑满,单卡几乎占满整屏,一屏看不了几个视频 2. 视频 2/3… 播放完全无声:`voiceAudioUrl={i === 0 ? voiceAudioUrl : undefined}` 配音只挂第 1 个变体;且所有 `<video>` 硬编码 `muted`,无配音时素材原声也被静音 ## 改动 ### 卡片缩小至约 3/5(`generate.css`) - `.xx-canvas-grid` 改 `grid-template-columns: repeat(auto-fill, minmax(200px, 220px))` + 居中:宽屏自动排 3 列(step4 预览列约 700~900px 内容宽下),中屏 2 列,窄屏(≤960px)单列限宽 320 居中 - 卡片面积约为原来的 0.36(≈3/5),9:16 比例不变、画面不裁切 - 控制条/片段指示器/静音按钮 compact 尺寸适配(26px 按钮、紧凑 padding),播放与勾选热区不缩到难点 ### 每个视频都有声音(`FrontendPreviewPlayer.tsx` / `CanvasPreviewGrid.tsx`) 1. **配音挂所有变体**:去掉 `i === 0` 限制,N 个实例都传 `voiceAudioUrl`(同一条 URL 浏览器缓存,不重复下载);现有 audio 随播放/暂停/seek/片段切换同步逻辑对每个实例生效 2. **音轨与成片一致**:有配音时 video 保持 `muted`(素材原声不与配音混音);**无配音时 video 取消 muted,素材原声兜底**,任何情况播放都有声(顺带修了单视频无配音也静音的旧问题) 3. **播放互斥**:新增 `activePlayToken` / `onPlayTokenChange`(token = variantSeed)。点某卡片播放→上报自身 token,Grid 内所有实例同步;token≠自身且正在播放的实例 effect 里自动暂停(视频+配音都停);暂停/播完上报 null 释放 4. **静音按钮**:每张卡片左上角喇叭按钮(`AudioOutlined`/`AudioMutedOutlined`),可单独静音;默认有声;有配音时静音配音 Audio,无配音时静音素材 video 5. 自动播放策略不变:声音在点击手势后触发,play() reject 静默 `.catch` 兜底不阻塞画面 6. 单视频模式不传互斥 props,行为零回归 ## 测试(+12 例,全量 705 passed) - `CanvasPreviewGrid.audio.test.tsx`(5 例,mock player):N=3 每个变体都拿到 voiceAudioUrl;互斥 props 接线;实例2播放→所有实例 token=2(其他自动暂停);暂停上报→token 释放;全部 has-voice - `FrontendPreviewPlayer.audio.test.tsx`(7 例,mock scheduler 控制播放态):有配音 video muted / 无配音 video 不 muted;静音按钮切换 muted+aria;互斥 effect(token 变他人且播放中→pause,切回不重复暂停);未播放不暂停;点播放上报自身 token 并触发 toggle;每卡片独立静音按钮 - tsc / eslint(0 warning)/ prettier / build 全绿 ## 验收 staging 第3步批量预览:卡片明显变小、一屏 3 个;视频 1/2/3 分别点播放都能听到配音,同时只有一个在响;喇叭按钮可单独静音;无配音素材播放有原声。 Closes #1741
xiaoxia added 1 commit 2026-09-06 15:35:35 +08:00
feat(#1741): 批量预览卡片缩小至3/5 + 每个视频播放都有声音(配音全挂载+互斥+静音按钮)
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m53s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m55s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m6s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m12s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m14s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m28s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m32s
AI Code Review / AI Code Review (pull_request) Successful in 3m32s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 5m23s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m27s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 8s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 14s
154ebccfa7
卡片尺寸:
- xx-canvas-grid 改 auto-fill minmax(200px,220px) 宽屏3列/窄屏1列限宽320居中,
  卡片面积约原来 0.36(≈3/5),保持 9:16 不变形;控制条/指示器 compact 适配

音频(视频2/3 无声修复):
- 去掉 voiceAudioUrl 仅挂 index 0 限制,每个变体都挂配音(同 URL 浏览器缓存)
- video 硬编码 muted 改为:有配音时 muted(避免混音)、无配音时不 muted(素材原声兜底)
- 播放互斥:activePlayToken/onPlayTokenChange,点某卡片播放其他卡片自动暂停(视频+配音)
- 每卡片左上角加静音/有声切换按钮(AudioOutlined/AudioMutedOutlined),默认有声
- 单视频模式不传互斥 props,行为不变

测试 +12:配音全挂载/互斥联动/暂停释放(Grid 5)、muted 逻辑/静音切换/
互斥 effect/播放权上报/独立按钮(Player 7);全量 705 passed

🚀 预览环境已部署

项目 详情
PR号 #1742
预览链接 https://pr-1742.preview.xiaoxiajianji.com
API环境 staging

💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。

🔄 每次提交新代码后预览环境会自动更新。

🗑️ PR 关闭或合并后,预览环境会自动清理。

🚀 **预览环境已部署** | 项目 | 详情 | |------|------| | PR号 | #1742 | | 预览链接 | [https://pr-1742.preview.xiaoxiajianji.com](https://pr-1742.preview.xiaoxiajianji.com) | | API环境 | staging | > 💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。 > > 🔄 每次提交新代码后预览环境会自动更新。 > > 🗑️ PR 关闭或合并后,预览环境会自动清理。
auto-approve-bot approved these changes 2026-09-06 15:38:07 +08:00
auto-approve-bot left a comment
Collaborator

CI全绿,自动审批通过。

CI全绿,自动审批通过。
auto-approve-bot approved these changes 2026-09-06 15:38:08 +08:00
auto-approve-bot left a comment
Collaborator

CI全绿,自动审批通过。

CI全绿,自动审批通过。
Collaborator

【阻塞级判定】

  • 是否存在阻塞级问题:否
  • 阻塞级问题数量:0 个

📊 审查概览

  • 整体评价:通过
  • 建议级问题数量:2 个

🔴 阻塞级问题(必须修复)

💡 改进建议(不阻塞合并)

  1. [FrontendPreviewPlayer.tsx: 426] React Hooks 依赖项风险

    • 具体内容:在 useEffect 中使用了 canvasState.isPlayingcanvasControlsvideoPauseisPlaying,但通过 eslint-disable-next-line 将其排除在依赖数组之外。虽然这可能是为了避免每次播放状态变化都触发暂停逻辑(仅在 Token 变化时触发),但如果 canvasControlsvideoPause 的引用在重渲染时发生了变化(例如上层 hook 返回了新的引用),这里将调用过期的闭包函数。建议确认这些引用的稳定性,或者使用 useRef 来保存这些函数以确保始终调用最新版本。
  2. [FrontendPreviewPlayer.tsx: 785] 内联样式维护性

    • 具体内容:新增的静音按钮使用了大量的内联样式(style={{ ... }}),导致代码臃肿且难以复用或统一调整主题。建议将样式提取到 CSS 文件或使用 CSS-in-JS 方案(如 styled-components 或 emotion)进行管理,以提高代码的可维护性。

良好实践

  • 逻辑严密性:播放互斥逻辑设计合理,通过父组件管理 activePlayToken 并下发,子组件监听 Token 变化自动暂停,有效解决了多路音频重叠问题。
  • 状态同步muted 状态与 hasVoice 的组合逻辑清晰,准确实现了“有配音时视频静音,无配音时视频原声兜底”的需求。
  • 测试覆盖:新增的单元测试覆盖了核心的播放互斥、音频挂载及静音逻辑,Mock 策略得当,能有效验证功能正确性。
  • CSS 响应式布局:使用 repeat(auto-fill, minmax(...)) 实现了较为灵活的网格布局,配合媒体查询适配了不同屏幕尺寸。

格式检查通过 | 逻辑审查通过 | 性能良好


🤖 由 AI 代码审查机器人自动生成 | 2026-09-06 07:39:09 | 模型:

### 【阻塞级判定】 - 是否存在阻塞级问题:否 - 阻塞级问题数量:0 个 ### 📊 审查概览 - 整体评价:通过 - 建议级问题数量:2 个 ### 🔴 阻塞级问题(必须修复) 无 ### 💡 改进建议(不阻塞合并) 1. **[FrontendPreviewPlayer.tsx: 426] React Hooks 依赖项风险** - 具体内容:在 `useEffect` 中使用了 `canvasState.isPlaying`、`canvasControls`、`videoPause`、`isPlaying`,但通过 `eslint-disable-next-line` 将其排除在依赖数组之外。虽然这可能是为了避免每次播放状态变化都触发暂停逻辑(仅在 Token 变化时触发),但如果 `canvasControls` 或 `videoPause` 的引用在重渲染时发生了变化(例如上层 hook 返回了新的引用),这里将调用过期的闭包函数。建议确认这些引用的稳定性,或者使用 `useRef` 来保存这些函数以确保始终调用最新版本。 2. **[FrontendPreviewPlayer.tsx: 785] 内联样式维护性** - 具体内容:新增的静音按钮使用了大量的内联样式(`style={{ ... }}`),导致代码臃肿且难以复用或统一调整主题。建议将样式提取到 CSS 文件或使用 CSS-in-JS 方案(如 styled-components 或 emotion)进行管理,以提高代码的可维护性。 ### ✅ 良好实践 - **逻辑严密性**:播放互斥逻辑设计合理,通过父组件管理 `activePlayToken` 并下发,子组件监听 Token 变化自动暂停,有效解决了多路音频重叠问题。 - **状态同步**:`muted` 状态与 `hasVoice` 的组合逻辑清晰,准确实现了“有配音时视频静音,无配音时视频原声兜底”的需求。 - **测试覆盖**:新增的单元测试覆盖了核心的播放互斥、音频挂载及静音逻辑,Mock 策略得当,能有效验证功能正确性。 - **CSS 响应式布局**:使用 `repeat(auto-fill, minmax(...))` 实现了较为灵活的网格布局,配合媒体查询适配了不同屏幕尺寸。 --- ✅ 格式检查通过 | ✅ 逻辑审查通过 | ✅ 性能良好 --- <sub>🤖 由 AI 代码审查机器人自动生成 | 2026-09-06 07:39:09 | 模型: </sub> <!-- AI_CODE_REVIEW_AUTO_COMMENT -->
auto-approve-bot merged commit 6638f8b29e into develop 2026-09-06 15:41:36 +08:00
auto-approve-bot deleted branch feat/1741-preview-grid-audio 2026-09-06 15:41:37 +08:00

🗑️ 预览环境已清理

PR #1742 已关闭或合并,对应的预览环境已被清理。

如有需要,可以重新打开 PR 来重新生成预览环境。

🗑️ **预览环境已清理** PR #1742 已关闭或合并,对应的预览环境已被清理。 > 如有需要,可以重新打开 PR 来重新生成预览环境。
Sign in to join this conversation.