Files
EchoChat/docs/plans/2026-04-24-phase2e-2-task15-ui-polish.plan.md
bujinyuan f41e63ba6b docs(phase2e-2): 补齐顶层文档进度同步,Phase 2e-2 会议 MVP 全面切
用户准备开始真实用户测试,此 commit 对"六份文档同步"之外遗漏的上层/入口文档
做二次补齐,保证所有出现阶段状态描述的文档都与 CURRENT_STATUS.md 一致。

补齐范围:
- README.md
  · "MVP 功能规划" 原把 Phase 2 / Phase 3 标记为"待开始",与实际进度严重偏离
  · 整段重写为"开发进度"小节,按 Phase 1 / 2a-d / 2e-1 / 2e-2 分别列出 
    已完成状态与关键交付;2e-3 / Phase 3 / P2 推迟项独立列为"待启动(规划中)"
  · "文档导航"扩展 5 条新入口:Phase 2e 路线图 / 2e-2 设计 / 2e-2 实施 /
    2e-2 代码审查 / 2e-2 验收报告 / 会议 MVP 部署指南
- docs/plans/2026-02-27-echochat-system-design.md(整体系统设计方案)
  · §Phase 2e 行:追加"2e-1  / 2e-2  / 2e-3 📋 待启动"状态徽标
  · §Phase 2e-2:从"📋 设计阶段完成,代码开发待启动"改为
    " 已完成(2026-04-24,Task 0-16 全量落地 + 代码审查 4 P0 + 8 P1 + 7 P2 + 7 Nit 闭环)"
- docs/plans/2026-04-21-phase2e-2-design.md(Phase 2e-2 设计文档)
  · 顶部"状态 / 最后更新"由"📋 设计阶段(待评审后进入代码开发)"切 
  · §一 文档定位的"本文档"行同步切 
- docs/plans/2026-04-24-phase2e-2-task15-ui-polish.plan.md(Task 15 计划)
  · 顶部"状态"由"📋 设计阶段(等待用户 Review)"切 
  · 追加"交付记录"行指向 CURRENT_STATUS.md Task 15/16 交付条目
- docs/deployment/meeting-mvp.md(会议 MVP 部署指南)
  · 顶部元信息追加阶段  标记 + Task 16 追加的 REDIS_PASSWORD × redis.conf
    requirepass 联动校验指引

验证:workspace grep "📋 设计阶段|🚧 代码开发|待开始" 在顶层 README/docs 下
已无遗留进度描述与实际状态冲突(仅 README 新增的"Phase 2e-3 / Phase 3 — 待启动
(规划中)"章节为规划项,符合预期)。

Made-with: Cursor
2026-04-23 17:59:32 +08:00

173 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 2e-2 Task 15UI 打磨 + 主持人权限四件套补齐
> **状态:** ✅ 已完成2026-04-24随 Task 16 收官一并切 ✅)
> **上级实施计划:** [docs/plans/2026-04-21-phase2e-2-implementation.plan.md §Task 15](./2026-04-21-phase2e-2-implementation.plan.md)
> **设计依据:** [Phase 2e-2 设计文档 §9.1 / §9.2 / §3.3 D08](./2026-04-21-phase2e-2-design.md)
> **分支:** `feature/phase2e-2-meeting-mvp`
> **预估工时:** 1.5-2 人日
> **范围锁定:** 满配6 条原创特色 + 静音他人 + 4 屏设计文档 + 桌面端恒浮窗 + RTP stats 双源说话者探测)
> **交付记录:** 已合并到 Task 15 commits + Task 16 P0/P1 修复批次;实际回归问题已随 Task 16 P0/P1media ownership、成员列表状态同步、consumer 精确清理)一并闭环。详见 [CURRENT_STATUS.md Task 15 / 16 交付条目](../progress/CURRENT_STATUS.md)。
---
## 一、目标
把设计文档 §9.2 定义的 EchoChat 会议原创特色 **6 条** 全部落地到代码,补齐主持人权限四件套的最后一件(静音他人),同时把会议模块 4 个核心屏幕的「设计 SSOT」写进 `design-system/echochat/pages/meeting-*.md`,让设计产物与代码实现可持续对齐。
不在本期做:
- 不做分布式 SFU / simulcast 档位切换
- 不做 `meeting.room.update`(会议主题 / 入会静音开关在线修改)
- 不做加入时视频块从网格外滑入(候选 §9.2 第 5 条,已被确认归入本期但作为低优先级,若工时紧可削减)
---
## 二、改动清单
### 2.1 新建文件5
| 路径 | 作用 |
|---|---|
| `design-system/echochat/pages/meeting-home.md` | 会议 Hub 首页(即时会议入口 + 加入会议)视觉规范 |
| `design-system/echochat/pages/meeting-preview.md` | 设备预览页(大预览 + 右侧设备列表 + 音量波浪)视觉规范 |
| `design-system/echochat/pages/meeting-room.md` | 会议室主页面(浮窗 / 流光 / 静音氛围色 / 柔性网格 / 波浪网络徽标)视觉规范 |
| `design-system/echochat/pages/meeting-invite.md` | 邀请弹窗(复制链接 / 会议号 / 联系人选择器)视觉规范 |
| `frontend/src/components/meeting/SelfVideoFloat.vue` | 自视频浮窗(桌面端恒浮窗 + 四角吸附拖拽 + 图钉切换回网格)|
### 2.2 改动文件7
| 路径 | 改动点 |
|---|---|
| `frontend/src/components/meeting/VideoTile.vue` | 流光轮廓 CSS `conic-gradient` + `@keyframes` 动效;`speaking` class 由上层喂入 |
| `frontend/src/components/meeting/VideoGrid.vue` | 2 人左右 65/35 非等分3 人大/小/小三角布局4+ 人维持等分;剔除本地 tile由浮窗承接|
| `frontend/src/components/meeting/MeetingToolbar.vue` | 新增 `:all-muted` prop背景色绑定冷蓝渐变`all-muted=true` 时)|
| `frontend/src/components/meeting/MemberPanel.vue` | 操作菜单新增「请他静音 / 请他开麦」(仅 host + 非自己 + 当前音频开/关)|
| `frontend/src/components/meeting/NetworkBadge.vue` | 4 条竖柱 → 3 条 SVG 波浪线 + 相位差动画;`level` prop 语义保留 |
| `frontend/src/store/meeting.js` | 新增 `muteMember(uid, mute)` action + `speakingMap` state新增 `_startSpeakingDetection` / `_stopSpeakingDetection` 内部方法;`isAllMuted` getter |
| `frontend/src/pages/meeting/room.vue` | 组装 `SelfVideoFloat` + `all-muted` 绑定 + speaking 下发到 tile离会弹窗未改 |
**动效性能目标:** 所有 `@keyframes` 限制在 `transform` / `opacity` / `border-image` / `background` 属性上,不触发 layoutChrome Performance 面板滚动会议室 10 秒,保持 60fps、无 long task > 50ms。
---
## 三、关键技术点
### 3.1 说话者流光轮廓
- CSS 实现:`border: 2rpx solid transparent` + `background-image: conic-gradient(from ...)` 切换为 `linear-gradient(#1e293b,#1e293b) padding-box, conic-gradient(...) border-box``@keyframes flow` 旋转 `--flow-angle``@property --flow-angle` 声明,不触发 layout
- 触发:`VideoTile.vue` 接收 `isSpeaking` prop上层 `room.vue` 通过 `store.speakingMap[userId]` 投影。
- 兼容Safari 不支持 `@property` 时 fallback 为静态蓝色环形边(现有 `.speaking` 样式)。
### 3.2 说话者探测RTP stats + WebAudio 双源)
- **远端**`store._startSpeakingDetection()` 每 500ms 遍历 `remoteConsumers[uid].audio.rtpReceiver`,调用 `getSynchronizationSources()`(标准 W3C APIChrome/Firefox/Edge 支持),读 `audioLevel`0-1 线性);阈值 0.05 以上且持续 ≥ 1 个 tick 判定为 `speaking=true`< 阈值连续 2 tick `speaking=false`防抖)。
- **本地**`localProducers.audio` 拿不到 `rtpReceiver`因为它是 sender)。本地用 `AudioContext` + `MediaStreamAudioSourceNode` + `AnalyserNode.getFloatTimeDomainData` RMS音量门限归一化到同一 0-1 区间
- **资源释放**`leave()` / `stopLocalAudio()` / `_clearRemoteConsumer()` 必须调 `_stopSpeakingDetection()`轮询用 `setInterval` + Pinia 内持有 handle避免泄漏
- **H5 Only**整段逻辑包在 `#ifdef H5` H5 平台 `speakingMap` 空对象UI 兜底为 never speaking
### 3.3 柔性网格
- `VideoGrid.vue` `tiles.length` 选择 layout class
- `layout-1`全屏单块
- `layout-2`左右 65/35横向 flex说话者自动成为大块Task 16 本期先固定"首个 tile 为大块"
- `layout-3`左上大块 65%右侧两个小块各 50% 高度叠放
- `layout-4+`继续 `repeat(n, 1fr)` 等分
- 小屏`<= 750px`统一回退到纵向等分现有逻辑保留
- 本地 tile `SelfVideoFloat` 渲染`VideoGrid` 只展示 `tiles.filter(t => !t.isLocal)`N-1 个远端)。所以实际 layout 判断基于 `tiles.length - 1`
### 3.4 自视频浮窗SelfVideoFloat.vue
- 尺寸默认 `240rpx × 150rpx`16:10桌面端 `280×180px`圆角 `16rpx` + 阴影 `0 12rpx 32rpx rgba(0,0,0,0.45)`
- 位置默认右下角距视频区边缘 `24rpx`
- 拖拽`@mousedown` + `@touchstart` 注册 `mousemove/touchmove` 全局监听释放时计算 4 角距离`transform: translate(x,y)` `transition: 0.22s cubic-bezier(0.2,0.8,0.2,1)` 吸附
- 图钉按钮右上角 pin 图标点击切换 `floatMode` `gridMode`gridMode 时浮窗隐藏本地 tile 回到 Grid
- 持久化`floatMode` 存入 `meetingStore.uiPrefs`Pinia 内存不落盘下次进会议默认浮窗
- Z-index浮窗 120低于 toolbar(210) 低于 MemberPanel(200)高于 VideoGrid
### 3.5 静音氛围色
- `room.vue` 新增 `allMuted = computed(() => tiles.length >= 2 && tiles.every(t => !t.audioEnabled))`
- `MeetingToolbar` 接收 `:all-muted``style: background` 绑定
- 默认`rgba(17, 24, 39, 0.88)`保持现有
- 全员静音`linear-gradient(to top, rgba(30, 58, 138, 0.88), rgba(30, 64, 175, 0.78))`冷蓝
- 过渡`transition: background 0.4s ease`
- 单人情况只有自己不触发避免独自加入就冷色
### 3.6 NetworkBadge 3 条波浪
- 改为 3 SVG `<path d="M 0 8 C 2 4, 4 12, 6 8 S 10 4, 12 8 S 16 12, 18 8" />` 波浪曲线
- 动画`@keyframes wave { 0% { transform: translateX(0) } 100% { transform: translateX(-6px) } }`3 条相位差 0 / -0.3s / -0.6s
- level 可见条数4=3条 / 3=2条 / 2=1条 / 1=1条短 / 0=静态红色中划线
- 颜色映射保持`#10B981` / `#22C55E` / `#F59E0B` / `#EF4444`
### 3.7 静音他人(主持人权限第 4 件)
- 后端能力已就绪`meeting.member.state.changed` WS 事件带 `target_user_id` 参数host 身份校验在 `MeetingSignalService.OnMemberStateChanged` 已实现
- 前端 store
```js
async muteMember(uid, mute = true) {
if (!this.isHost) throw new Error('仅主持人可操作')
const payload = { target_user_id: uid, audio_enabled: !mute }
await wsService.sendWithAck('meeting.member.state.changed', payload, 3000)
}
```
- `MemberPanel` 操作菜单:
- host 视角下每行尾部三点菜单新增 2 项:
- 若 `p.audio_enabled === true`:「请他静音」(灰色,非 danger
- 若 `p.audio_enabled === false`:「请他开麦」(灰色)
- 现有「转让主持人 / 踢出会议」保留
- 被静音侧 UX后端 `meeting.member.state.changed` 广播回来时store `_onMemberStateChanged` 已识别 `target_user_id === 我自己`,直接调 `stopLocalAudio()` 停本地 producer本期补一个 toast 提示「主持人请你静音」
---
## 四、任务拆分
| # | 任务 | 工时 | 产出 |
|---|---|---|---|
| T15.1 | 4 屏 design-system 文档 | 0.3 人日 | `design-system/echochat/pages/meeting-{home,preview,room,invite}.md` |
| T15.2 | VideoTile 流光轮廓 + CSS @property fallback | 0.15 人日 | VideoTile.vue 改 |
| T15.3 | 说话者探测双源store + 资源释放 + 防抖) | 0.3 人日 | `store/meeting.js` + VideoTile 接线 |
| T15.4 | VideoGrid 柔性网格 2/3 人布局 | 0.15 人日 | VideoGrid.vue 改 |
| T15.5 | SelfVideoFloat.vue 浮窗 + 拖拽 + 吸附 + 图钉 | 0.3 人日 | 新文件 + room.vue 接入 |
| T15.6 | MeetingToolbar 静音氛围色 + allMuted 绑定 | 0.1 人日 | Toolbar + room.vue |
| T15.7 | NetworkBadge 3 条波浪 SVG + level 映射 | 0.15 人日 | NetworkBadge.vue 重写 |
| T15.8 | 静音他人store action + MemberPanel 菜单 + 被静音 toast | 0.2 人日 | store / MemberPanel / room.vue |
| T15.9 | 本机 Playwright 截图回归4 屏 + 流光 + 浮窗吸附 + 静音氛围) | 0.15 人日 | 截图归档 |
| T15.10 | 进度文档同步 + commit + push | 0.1 人日 | CURRENT_STATUS / implementation.plan / project-context |
| **合计** | | **1.9 人日** | |
---
## 五、检查点(对齐设计文档 §9 + 实施计划 §Task 15 验收)
- [ ] 4 屏 `design-system/echochat/pages/meeting-*.md` 全部落盘,包含 Layout / Color / Typography / Motion 四栏
- [ ] 说话者流光轮廓在双人会议中稳定触发,阈值 `audioLevel >= 0.05`,防抖 1 in / 2 out
- [ ] 2 人 65/35 / 3 人三角布局视觉与设计一致4+ 人保持等分
- [ ] 自视频浮窗拖拽松手后 0.22s 内吸附到最近四角;桌面端 `Cmd+Drag` 不触发系统 drag
- [ ] 图钉按钮可切换 float ↔ gridgrid 模式本地 tile 回到网格里
- [ ] 全员静音时 Toolbar 冷蓝渐变,恢复说话立即回暖;单人独处不触发
- [ ] NetworkBadge 3 条波浪流动level 4/3/2/1/0 对应可见条数 3/2/1/1(短)/0
- [ ] 主持人菜单显示「请他静音 / 请他开麦」,非主持人不可见;被请对方收到 toast 并本地静音
- [ ] Chrome Performance 面板会议室滚动 10 秒 60fps无 long task > 50ms
- [ ] `npm run build:h5` 无新增警告;`ReadLints` 零错误
- [ ] Playwright 截图 4 张归档到 `.playwright-mcp/task15/`home / preview / room / invite
---
## 六、风险
| 风险 | 严重度 | 缓解 |
|---|---|---|
| `@property --flow-angle` Safari 17- 不支持 | 低 | CSS `@supports` + fallback 静态蓝环 |
| `getSynchronizationSources()` 在 Firefox 返回空数组 | 中 | `if (sources.length === 0)` 回退到 WebAudio |
| 自视频浮窗 `mousemove` 全局监听内存泄漏 | 中 | `useEventListener` 模式 + `onBeforeUnmount` 显式 removeListener |
| 静音氛围色在"全员非 host 静音但 host 未静音"时也触发,可能误伤 | 低 | 条件用 `tiles.length >= 2 && tiles.every(t => !t.audioEnabled)`host 未静音时不成立 |
| 被主持人静音的用户以为"自己麦克风故障" | 中 | 收到静音指令后显示 `uni.showToast({ title: '主持人请你静音' })` 持续 2s |
---
## 七、变更记录
| 日期 | 变更 |
|---|---|
| 2026-04-24 | 初稿 |