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

12 KiB
Raw Blame History

Phase 2e-2 Task 15UI 打磨 + 主持人权限四件套补齐

状态: 已完成2026-04-24随 Task 16 收官一并切 上级实施计划: docs/plans/2026-04-21-phase2e-2-implementation.plan.md §Task 15 设计依据: Phase 2e-2 设计文档 §9.1 / §9.2 / §3.3 D08 分支: 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 交付条目


一、目标

把设计文档 §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 支持),读 audioLevel0-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.vuetiles.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 × 150rpx16: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 图标,点击切换 floatModegridModegridMode 时浮窗隐藏、本地 tile 回到 Grid 里
  • 持久化:floatMode 存入 meetingStore.uiPrefsPinia 内存,不落盘;下次进会议默认浮窗)
  • Z-index浮窗 120低于 toolbar(210) 低于 MemberPanel(200),高于 VideoGrid

3.5 静音氛围色

  • room.vue 新增 allMuted = computed(() => tiles.length >= 2 && tiles.every(t => !t.audioEnabled))
  • MeetingToolbar 接收 :all-mutedstyle: 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
    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 初稿