From a6014b84ee159315d503b356a25499755ce9b58d Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 6 Sep 2026 13:22:06 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=A1=8C=E9=9D=A2=E7=AB=AF?= =?UTF-8?q?=E6=8B=96=E5=8A=A8=E6=82=AC=E6=B5=AE=E9=80=9A=E8=AF=9D=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E5=90=8E=E6=94=B6=E7=AA=84=E6=B5=8F=E8=A7=88=E5=99=A8?= =?UTF-8?q?=E8=87=B3=E7=A7=BB=E5=8A=A8=E7=AB=AF=E6=96=AD=E7=82=B9=EF=BC=8C?= =?UTF-8?q?=E9=80=9A=E8=AF=9D=E6=9D=A1=E5=8D=A1=E5=9C=A8=E6=97=A7=E6=A1=8C?= =?UTF-8?q?=E9=9D=A2=E5=9D=90=E6=A0=87=E4=B8=8D=E8=B7=9F=E9=9A=8F=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E7=AB=AF=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:视频通话最小化后处于桌面宽度(>640px)时,拖动悬浮小窗到任意位置(dragStart 会把 定位方式从 CSS 的 right/auto-left 切换成显式 left/top 内联样式,方便拖拽期间自由移动), 之后把浏览器窗口收窄到 640px 以下(真实场景:桌面用户把窗口拖窄到半屏和另一个应用并排), 悬浮条不会像预期那样贴顶变成通栏胶囊(base.css 里 #video-container.minimized:not(.audio-minimized) 的移动端规则:left:0/right:auto/ top:var(--caption-panel-height)/width:100%/height:11rem),而是继续停在拖拽结束时的旧 桌面坐标——因为内联 style 的优先级恒高于任何样式表选择器,不管后者的媒体查询是否匹配, 移动端规则里的 left/right/top 全部被内联值盖住,只有从未被内联样式接触过的 width/height 还能生效,导致宽/高不成比例、位置错位。 修复:CallPanel 新增 handleWindowResize,监听 window resize;每次触发时如果 matchMedia('(width<=640px)') 命中且当前处于 minimized,就清掉 containerRef 上手动写入的 left/right/top 内联样式,把定位权交还给样式表,让移动端规则完整生效。componentDidMount/ componentWillUnmount 里和已有的 pointerdown/move/up 监听一起注册/清理。从未拖拽过时这个 清理是空操作,不影响现有行为。 验证:真实 CallPanel 实例(React Fiber 直接获取),伪造 canvas/AudioContext 媒体流跑通整 个通话中状态,用 computer 工具做真实指针拖拽把悬浮窗从默认右上角拖到别处(确认 style.left/top 已写入、right:auto),再用 resize_window 把视口收窄到 375px 宽并确认应用 自身的 resize 监听正确触发(displayMobile 状态同步更新)——收窄后 style 只剩 "cursor: grab",left/right/top 均被清空,getComputedStyle 显示 left:0/top:53.33px(= var(--caption-panel-height)按当前根字号换算)/width:375px(100%)/height:146.66px(11rem), 与移动端样式表规则完全一致;再放宽回桌面宽度确认悬浮窗正常回到默认右上角18rem方框位置, 未引入回归。 Co-Authored-By: Claude Sonnet 5 --- src/widgets/call-panel.jsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/widgets/call-panel.jsx b/src/widgets/call-panel.jsx index 4f3762be..55fef792 100644 --- a/src/widgets/call-panel.jsx +++ b/src/widgets/call-panel.jsx @@ -175,6 +175,23 @@ class CallPanel extends React.PureComponent { this.containerRef.current.style.cursor = 'grab'; } }; + // dragStart's inline left/top/right (set the first time the minimized bubble is dragged, see + // above) always win over base.css's #video-container.minimized:not(.audio-minimized) mobile + // rule -- inline style beats any stylesheet selector regardless of media query. A desktop user + // who drags the bubble somewhere and then narrows the window below 640px (a real everyday + // action -- snapping the browser to half the screen, say) would otherwise be stuck with the + // bubble pinned at its old desktop-width position/size instead of docking as the intended + // mobile full-width strip. Clearing the manual offsets once the mobile breakpoint applies lets + // the stylesheet rule take back over; harmless (a no-op) whenever nothing was ever dragged. + this.handleWindowResize = () => { + if (this.props.minimized && this.containerRef && this.containerRef.current && + window.matchMedia('(width<=640px)').matches) { + const el = this.containerRef.current; + el.style.removeProperty('left'); + el.style.removeProperty('right'); + el.style.removeProperty('top'); + } + }; } componentDidMount() { @@ -191,6 +208,7 @@ class CallPanel extends React.PureComponent { this.containerRef.current.addEventListener('pointermove', this.drag); this.containerRef.current.addEventListener('pointerup', this.dragEnd); } + window.addEventListener('resize', this.handleWindowResize); } componentWillUnmount() { @@ -199,6 +217,7 @@ class CallPanel extends React.PureComponent { topic.onInfo = this.previousOnInfo; this.stop(); + window.removeEventListener('resize', this.handleWindowResize); if (this.containerRef) { this.containerRef.current.removeEventListener('pointerdown', this.dragStart); this.containerRef.current.removeEventListener('pointermove', this.drag);