diff --git a/wetty/src/assets/scss/styles.scss b/wetty/src/assets/scss/styles.scss index c161768..a134e10 100644 --- a/wetty/src/assets/scss/styles.scss +++ b/wetty/src/assets/scss/styles.scss @@ -20,6 +20,8 @@ body { .xterm { .xterm-viewport { - overflow-y: hidden; + overscroll-behavior-y: contain !important; + -webkit-overflow-scrolling: touch !important; + will-change: transform, scroll-position; } } diff --git a/wetty/src/client/wetty.ts b/wetty/src/client/wetty.ts index c03f1dd..141d577 100644 --- a/wetty/src/client/wetty.ts +++ b/wetty/src/client/wetty.ts @@ -8,7 +8,7 @@ import { disconnect } from './wetty/disconnect'; import { overlay } from './wetty/disconnect/elements'; import { verifyPrompt } from './wetty/disconnect/verify'; import { FileDownloader } from './wetty/download'; -import { mobileKeyboard } from './wetty/mobile'; +import { mobileKeyboard, initMobileViewport } from './wetty/mobile'; import { socket } from './wetty/socket'; import { terminal, Term } from './wetty/term'; @@ -33,6 +33,7 @@ if (_.isUndefined(term)) { term.resizeTerm(); term.focus(); mobileKeyboard(); + initMobileViewport(term); const fileDownloader = new FileDownloader(); socket diff --git a/wetty/src/client/wetty/mobile.ts b/wetty/src/client/wetty/mobile.ts index 936f70c..2fd9e5f 100644 --- a/wetty/src/client/wetty/mobile.ts +++ b/wetty/src/client/wetty/mobile.ts @@ -8,7 +8,51 @@ export function mobileKeyboard(): void { screen.setAttribute('autocorrect', 'false'); screen.setAttribute('autocomplete', 'false'); screen.setAttribute('autocapitalize', 'false'); - /* - term.scrollPort_.screen_.setAttribute('contenteditable', 'false'); - */ +} + +export function initMobileViewport(term: any): void { + // Safari/iOS 上阻止页面随着键盘弹出连带回弹上卷 + document.body.style.position = 'fixed'; + document.body.style.width = '100%'; + document.body.style.height = '100%'; + document.body.style.overflow = 'hidden'; + + // 劫持容器,阻止橡皮筋翻页穿透 + document.addEventListener('touchmove', (e) => { + if (!(e.target as Element).closest('.xterm-viewport')) { + e.preventDefault(); + } + }, { passive: false }); + + if (window.visualViewport) { + const handleResize = () => { + // 计算键盘弹起遮挡区域(针对不同平台浏览器处理) + let bottomInset = 0; + if (window.visualViewport) { + bottomInset = Math.max( + 0, + window.innerHeight - (window.visualViewport.height + window.visualViewport.offsetTop) + ); + } + + const termWrapper = document.getElementById('terminal'); // 或获取最近的包装器 + if (termWrapper) { + if (bottomInset > 0) { + termWrapper.style.paddingBottom = `${bottomInset}px`; + } else { + termWrapper.style.paddingBottom = '0px'; + } + } + + // 调整完包裹层高度后,通知 xterm 重新按新的可用高度进行行列计算 + setTimeout(() => { + if (term && term.fitAddon) { + term.fitAddon.fit(); + } + }, 50); + }; + + window.visualViewport.addEventListener('resize', handleResize); + window.visualViewport.addEventListener('scroll', handleResize); + } }