From e34e2c965c8b365b5a2d8c538dbc4acd701e702e Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Fri, 5 Jun 2026 23:33:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20=E5=A4=9A=E8=A1=8C=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=97=B6=E4=BF=9D=E6=8C=81=E5=85=89=E6=A0=87=E5=8F=AF?= =?UTF-8?q?=E8=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adjustTextareaSize 重算高度会打乱 scrollTop,导致多行输入时光标被滚出视野。 新增 keepEditorSelectionVisible:高度重设后,聚焦态用编辑器 coordsAtPos 算出 光标位置并滚回可视区(上下留 lineHeight*0.3 padding),非聚焦态恢复原 scrollTop。 Co-Authored-By: Claude Opus 4.8 (1M context) --- static/src/components/input/InputComposer.vue | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/static/src/components/input/InputComposer.vue b/static/src/components/input/InputComposer.vue index 373ec70..17dfd97 100644 --- a/static/src/components/input/InputComposer.vue +++ b/static/src/components/input/InputComposer.vue @@ -1528,6 +1528,9 @@ const adjustTextareaSize = () => { return; } const target = root; + const previousScrollTop = target.scrollTop; + const selectionShouldStayVisible = + document.activeElement === target || target.contains(document.activeElement); target.style.height = 'auto'; const computedStyle = window.getComputedStyle(target); const lineHeight = parseFloat(computedStyle.lineHeight || '20') || 20; @@ -1537,11 +1540,58 @@ const adjustTextareaSize = () => { const multiline = targetHeight > lineHeight * 1.4; applyLineMetrics(lines, multiline); target.style.height = `${targetHeight}px`; + keepEditorSelectionVisible(target, { + previousScrollTop, + selectionShouldStayVisible, + padding: Math.max(4, lineHeight * 0.3) + }); nextTick(() => { emitComposerHeight(); }); }; +const keepEditorSelectionVisible = ( + target: HTMLElement, + options: { previousScrollTop: number; selectionShouldStayVisible: boolean; padding: number } +) => { + const maxScrollTop = Math.max(0, target.scrollHeight - target.clientHeight); + if (maxScrollTop <= 0) { + target.scrollTop = 0; + return; + } + + if (!options.selectionShouldStayVisible) { + target.scrollTop = Math.min(maxScrollTop, Math.max(0, options.previousScrollTop)); + return; + } + + requestAnimationFrame(() => { + const instance = editor.value; + if (!instance) { + target.scrollTop = Math.min(maxScrollTop, Math.max(0, options.previousScrollTop)); + return; + } + + try { + const caretRect = instance.view.coordsAtPos(instance.state.selection.head); + const targetRect = target.getBoundingClientRect(); + const padding = options.padding; + const currentMaxScrollTop = Math.max(0, target.scrollHeight - target.clientHeight); + let nextScrollTop = target.scrollTop; + + if (caretRect.bottom > targetRect.bottom - padding) { + nextScrollTop += caretRect.bottom - targetRect.bottom + padding; + } else if (caretRect.top < targetRect.top + padding) { + nextScrollTop -= targetRect.top + padding - caretRect.top; + } + + target.scrollTop = Math.min(currentMaxScrollTop, Math.max(0, nextScrollTop)); + } catch { + target.scrollTop = Math.min(maxScrollTop, Math.max(0, options.previousScrollTop)); + } + }); +}; + const onInput = (event: Event) => { const target = event.target as HTMLTextAreaElement; if (!props.isConnected || props.inputLocked) {