[移动端] 会话划到底时输入框上浮约 168px:能否让 composer 对"末尾外部流内元素"免疫? #6940
Replies: 2 comments
|
补一份完整复盘(含"没有 DevTools 时怎么定位"的过程、两版修复的取舍、以及规避插件的实现): 移动端划到底输入框会上浮 168px:一次没有 DevTools 的定位与修复
1. 现象在会话里往上划到底(把消息列表滚到末尾)时,整个输入区(输入卡片 + 底部
2. 先看代码:输入框是怎么贴底的
.wSkVaW_root[data-phase=active]{overflow:hidden}
.wSkVaW_body{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}
.wSkVaW_scrollBody{scrollbar-gutter:stable;flex-direction:column;flex:1;min-height:0;display:flex;overflow-y:auto}
/* 关键两行 */
.wSkVaW_root[data-phase=active] .wSkVaW_viewArea{flex:1 0 auto;min-height:auto}
.wSkVaW_root[data-phase=active] .wSkVaW_composerSeat{z-index:7;position:sticky;bottom:0}
<div className={css.scrollBody} data-conversation-scroll>
{sessionId !== undefined ? renderSlot("conversation.session", {}) : null}
<div ref={seatResizeRef} className={css.composerSeat} data-composer-seat="">{composer}</div>
</div>也就是说:输入框是滚动容器的最后一个子元素,用 3. 语义陷阱:sticky 只能往上拉,不能往下推
所以只要"内容末端"落在输入框下方(哪怕只有 1px),输入框就会停在原位、下方留白。这正是"划到底"才触发的原因:滚动位置越接近末尾,越容易暴露末尾那段多余的空间。 设 采样数据完美吻合这条线性关系(见第 5 节:修正量 17 → 34 → 53 → … → 168 一路递增)。 所以真正要问的是:seat 后面为什么还有东西? 4. 排除法:不是 DSH 上游的问题
于是基本可以确定:这个垫片来自 DSH Mobile App 的 WebView 注入(Android 侧常见做法:给软键盘预留空间, 5. 没有 DevTools 怎么做诊断这台设备上跑的是 App 内的 WebView,没有 Chrome DevTools,rootfs 里也没有可用的浏览器/无头内核。于是用了两条现成通道做"现场埋点":
采样内容: 5 次上传、每次 300 条采样,结构完全一致: 至此根因闭环:垫片把"内容末端"顶到了 seat 下方 168px,划到底时这段尾巴必然露出来,而 sticky 无法把输入框推回去。 复现也可以手动做,一行就够(移动端连不上 DevTools 时,可以把结果画在页面角标上): const seat = document.querySelector('[data-composer-seat]');
const sc = seat.closest('[data-conversation-scroll]');
console.log([...sc.children].map(k =>
`${k.className || k.tagName}:${Math.round(k.getBoundingClientRect().height)}`));
sc.scrollTop = sc.scrollHeight; // 划到底
const gap = sc.getBoundingClientRect().bottom - seat.getBoundingClientRect().bottom;
console.log('seat 高于容器底边', Math.round(gap), 'px'); // 有垫片时 ≈ 垫片高度6. 修复:两版,从"能动"到"能打"v1(已废弃):逐帧兜底。 每帧量一次 seat 与容器底边,
v2(当前采用):把尾巴从布局上抵消掉。 测量 seat 之后流内兄弟的总高度,用负的下外边距抵消,让 seat 重新成为最后一个流内盒子: // 只测一次几何,写成 CSS 变量 / 内联样式
let trail = 0;
const kids = scroller.children;
for (let i = kids.length - 1; i >= 0; i--) {
const kid = kids[i];
if (kid === seat) break;
const cs = getComputedStyle(kid);
if (cs.position === 'absolute' || cs.position === 'fixed' || cs.display === 'none') continue;
const h = kid.getBoundingClientRect().height;
if (h > 0) trail += h;
}
if (trail > 0) seat.style.marginBottom = `-${Math.round(trail)}px`;为什么这样更好:
7. 交付形态
安装(和任何 DSH 插件一样): dsh plugin --profile web add link:/path/to/dsh-composer-pin-fix
# 或发布到 registry 后:dsh plugin --profile web add dsh-composer-pin-fix
# 改完 bundle 层需要重启一次服务真机验证:
8. 给维护者/其他 App 的提示
附录:这次用到的诊断片段/* 追加到某个"宿主按 mtime 热读"的浏览器端脚本末尾即可生效,无需重启 */
;(function () {
const snap = (tag) => {
const seat = document.querySelector('[data-composer-seat]');
const sc = seat && seat.closest('[data-conversation-scroll]');
if (!seat || !sc) return;
const s = seat.getBoundingClientRect(), c = sc.getBoundingClientRect();
console.log(tag,
'scrollTop', Math.round(sc.scrollTop), '/', sc.scrollHeight - sc.clientHeight,
'seatBottom', Math.round(s.bottom), 'scrollerBottom', Math.round(c.bottom),
'gap', Math.round(c.bottom - s.bottom),
'children', [...sc.children].map(k =>
`${k.className || k.tagName}:${Math.round(k.getBoundingClientRect().height)}`).join(' ~ '));
};
['touchstart', 'touchend', 'scroll'].forEach(e =>
window.addEventListener(e, () => snap(e), { capture: true, passive: true }));
snap('init');
})();(真机上把上面的 |
|
结论:确认。composer 是滚动容器内的最后一个流内子节点、以 源码证据: .root[data-phase='active'] .composerSeat,
.embeddedBody[data-content-phase='active'] .composerSeat {
position: sticky;
bottom: 0;seat 的 JSX 在 两条建议的取舍:方案 2(把 seat 移出滚动容器)是结构性改动,直接推翻上面注释确立的设计(seat 在滚动体内预留高度、view composer overlay 骑 padding box),不是纯 bug fix;方案 1(测量 seat 之后流内兄弟、负 margin 抵消)不动结构、纯布局修正,与现状最贴合。选哪个属维护者决定,我只从代码角度说:方案 1 的侵入面最小。你实现的插件(测量 + 负 margin、滚动路径零测量)思路正确;风险点你也已覆盖——需在 resize / 未验证:真机采样数据与垫片来源推断(那些是你的测量;"注入者身份"你已自标为推断)。 如果你能提供垫片节点插入的时机(是否随键盘弹出出现),对确认根因闭环会很有帮助。 |
Uh oh!
There was an error while loading. Please reload this page.
在 Android WebView 宿主上遇到一个 composer 贴底失效的问题:定位到根因后想请维护者确认,这层防护值不值得做在上游。下面是现象、代码级定位与真机采样数据。
环境
0.1.5-rc.1(web profile)@deepseek-ai/dsh-client-ui-conversation0.1.5-rc.1com.dshmobile.app)桌面浏览器端我没能复现(那边没有这个垫片),触发条件看起来与 WebView 宿主注入的 DOM 有关。
复现步骤
N 轮 … tok/s统计行)整体上浮,下方出现一片空白;往回滚一点又贴回底部。实际:划到底时 composer 停在内容末端上方,浮起量 = 其后方流内内容的总高度(本机为 168px)。
期望:composer 始终贴在滚动容器底边,不因外部插入的元素而漂移。
定位与根因
输入区是滚动容器的最后一个子元素,用
sticky; bottom: 0贴底(
packages/client/ui-conversation/src/client/ConversationRoot.module.css):ConversationRoot往滚动容器里只渲染两个子节点:但真机上的滚动容器有三个子节点,第三个是无 class 的
DIV,高 168px(=
--dsh-composer-height152 + 16),且是参与布局的流内元素:它不在 DSH 的任何包中产生(我搜过
data-composer-seat/data-conversation-scroll/所有
createPortal/ 所有 index 注入点,都不符合),因此判断来自 WebView 宿主的注入(大概率为键盘预留空间)。
position: sticky; bottom: 0的约束是单向的:于是设
trailing= seat 之后流内内容总高度:采样数据与该线性关系完全一致(修正量 17 → 34 → 53 → … → 168 递增)。
现场数据(真机采样,多次一致)
影响面
我对比过
0.1.5-rc.1/0.1.5-rc.2/0.1.6-alpha.1,composer 相关 CSS 内容逐字节相同,0.1.6-alpha.1的 JS diff 也未见定位相关改动 —— 也就是说升级 DSH 不会解决。建议(上游可选的两条改法)
margin-bottom: calc(-1 * var(--dsh-composer-trailing, 0px)),由脚本测量 seat 之后的流内兄弟高度写入该变量(0 时无副作用)。这属于纯布局修正,滚动不经过 JS。
顺带一个请求:如果 DSH 有官方推荐的"宿主向页面注入元素"的约定/钩子,欢迎在文档里写明,
这样移动端宿主就不必往滚动容器里塞垫片了 —— DSH 的 composer 本身是流内 sticky,已经为消息列表预留了高度,
再叠一个
composer 高度 + 16的垫片是重复预留。附:无 DevTools 时的自查代码
附:我方的临时规避(仅作参考,不代表上游方案)
按上面建议 1 做了一个独立插件:测量 seat 之后的流内兄弟高度,写入 CSS 变量并负 margin 抵消;
不删、不改宿主注入的垫片,其键盘预留高度原样保留;垫片不存在时自动休眠。真机验证:划到底贴合、快速甩动不抖。
All reactions