一、核心适配目标键盘弹出时输入框不被键盘遮挡页面布局自动上推聊天消息区域不挤压变形输入消息后自动滚动到最新消息兼容手机底部安全区避免输入框被底部横条遮挡二、你的具体实现方案核心代码逻辑1. 禁用 input 自动上推页面基础配置input :adjust-positionfalse /作用关闭 uni-app 默认的键盘自动上推页面功能手动控制布局避免默认上推导致的页面错乱。2. 监听键盘高度变化核心监听input keyboardheightchangechangeHeight /let keyboardHeight ref(0) function changeHeight(e) { keyboardHeight.value e.detail.height // 实时获取键盘高度 if (keyboardHeight.value 0) scrollToBottom() }作用精准监听键盘弹出 / 收起的实时高度赋值给响应式变量keyboardHeight。3. 占位符撑开页面防遮挡核心view classkeyboard-placeholder :style{ height: keyboardHeight px }/view作用键盘弹出时占位符高度 键盘高度把页面底部撑开让输入框自然浮在键盘上方不被遮挡键盘收起时高度归 0页面恢复原样。4. 安全区兼容处理view classinput-area :class{ safeArea: keyboardHeight ! 0 }.input-area { margin-bottom: env(safe-area-inset-bottom); // 默认适配底部安全区 } .safeArea { margin-bottom: 0px !important; // 键盘弹出时取消安全区间距避免双重间距 }作用无键盘时适配手机底部安全区键盘弹出时关闭安全区边距防止输入框和键盘之间出现多余空隙。5. 联动聊天自动滚动到底部键盘高度变化时自动调用scrollToBottom()保证输入消息、接收消息时始终显示最新内容。三、方案整体原理一句话关闭系统自动上推 → 监听键盘实时高度 → 用占位符动态撑开页面底部 → 配合安全区样式 → 实现输入框不被键盘遮挡。四、你的方案优点纯原生实现无第三方依赖兼容所有 uni-app 端微信小程序、App、H5精准适配根据键盘实际高度动态调整适配不同手机 / 不同输入法高度布局稳定不会挤压消息列表页面不会抖动、变形安全区兼容解决了全面屏手机底部遮挡问题联动聊天场景键盘弹出自动滚动到最新消息体验流畅五、代码中存在的小问题可优化点样式语法错误transform: height 0.1s ease; // 错误 transition: height 0.3s ease; // 正确高度过渡动画键盘收起时未重置高度keyboardHeight只在弹出时赋值收起时未重置为 0可能导致占位符残留空白。防抖缺失快速弹出 / 收起键盘时keyboardheightchange会频繁触发可能造成页面抖动。六、完整版代码template view classcontainer view classheader text classtitleWebSocket 测试{{ keyboardHeight }}/text view classheader-actions button classconnect-btn v-if!isConnected clickconnectWebSocket 连接 /button button classdisconnect-btn v-else clickdisconnectWebSocket 断开 /button view classstatus :class{ connected: isConnected, disconnected: !isConnected } view classstatus-dot/view text{{ isConnected ? 已连接 : 未连接 }}/text /view /view /view scroll-view classmessage-list scroll-y :scroll-into-viewscrollToId scroll-with-animation view v-for(msg, index) in messages :keyindex :idmsg- index classmessage-item :class{ message-sent: msg.type send, message-received: msg.type receive } view classmessage-content text{{ msg.content }}/text /view text classmessage-time{{ msg.time }}/text /view /scroll-view view classinput-area :class{ safeArea: keyboardHeight ! 0 } input v-modelinputMessage :adjust-positionfalse keyboardheightchangechangeHeight classmessage-input placeholder请输入消息... confirmsendMessage confirm-hold / button classsend-btn :disabled!inputMessage.trim() || !isConnected clicksendMessage 发送 /button /view view classkeyboard-placeholder :style{ height: keyboardHeight px }/view /view /template script setup langts import { ref, onUnmounted, nextTick } from vue interface Message { type: send | receive content: string time: string } let keyboardHeight ref(0) function changeHeight(e: { detail: { height: number } }) { keyboardHeight.value e.detail.height if (keyboardHeight.value 0) scrollToBottom() } const isConnected ref(false) const inputMessage ref() const messages refMessage[]([]) const scrollToId ref() let socketTask: UniApp.SocketTask | null null const getTime () { const now new Date() return ${now.getHours().toString().padStart(2, 0)}:${now.getMinutes().toString().padStart(2, 0)}:${now.getSeconds().toString().padStart(2, 0)} } const scrollToBottom () { scrollToId.value nextTick(() { if (messages.value.length 0) { scrollToId.value msg- (messages.value.length - 1) console.log(scrollToId.value) } }) } const connectWebSocket () { socketTask uni.connectSocket({ url: wss://echo.websocket.org, success: () { console.log(WebSocket连接成功) }, fail: (err) { console.error(WebSocket连接失败, err) uni.showToast({ title: 连接失败, icon: none }) } }) socketTask.onOpen(() { isConnected.value true uni.showToast({ title: 连接成功, icon: success }) messages.value.push({ type: receive, content: WebSocket 连接已建立, time: getTime() }) scrollToBottom() }) socketTask.onMessage((res) { messages.value.push({ type: receive, content: res.data, time: getTime() }) scrollToBottom() }) socketTask.onClose(() { isConnected.value false messages.value.push({ type: receive, content: WebSocket 连接已断开, time: getTime() }) scrollToBottom() }) socketTask.onError((err) { console.error(WebSocket错误, err) uni.showToast({ title: 连接错误, icon: none }) }) } const disconnectWebSocket () { if (socketTask) { socketTask.close({ success: () { isConnected.value false uni.showToast({ title: 已断开连接, icon: none }) } }) socketTask null } } const sendMessage () { if (!inputMessage.value.trim() || !isConnected.value) return const message inputMessage.value.trim() messages.value.push({ type: send, content: message, time: getTime() }) scrollToBottom() socketTask?.send({ data: message, success: () { inputMessage.value }, fail: (err) { console.error(发送失败, err) uni.showToast({ title: 发送失败, icon: none }) } }) } onUnmounted(() { disconnectWebSocket() }) /script style langscss scoped .container { height: 100%; display: flex; flex-direction: column; background-color: #f5f5f5; transform: height 0.1s ease; } .header { padding: 20rpx 30rpx; background-color: #409eff; display: flex; justify-content: space-between; align-items: center; } .title { font-size: 34rpx; color: #fff; font-weight: bold; } .header-actions { display: flex; align-items: center; gap: 15rpx; } .connect-btn, .disconnect-btn { padding: 10rpx 25rpx; border-radius: 20rpx; font-size: 26rpx; color: #fff; border: none; [disabled] { opacity: 0.7; } } .connect-btn { background-color: #67c23a; } .disconnect-btn { background-color: #f56c6c; } .status { display: flex; align-items: center; gap: 10rpx; padding: 10rpx 20rpx; border-radius: 20rpx; font-size: 24rpx; color: #fff; .connected { background-color: rgba(72, 187, 120, 0.8); } .disconnected { background-color: rgba(245, 108, 108, 0.8); } } .status-dot { width: 16rpx; height: 16rpx; border-radius: 50%; background-color: #fff; } .message-list { flex: 1; width: calc(100vw - 40rpx); min-height: 0; overflow-y: auto; padding: 20rpx; } .message-item { display: flex; flex-direction: column; margin-bottom: 30rpx; max-width: 70%; .message-sent { align-items: flex-end; margin-left: auto; } .message-received { align-items: flex-start; margin-right: auto; } } .message-content { padding: 20rpx 30rpx; border-radius: 30rpx; font-size: 28rpx; word-break: break-all; .message-sent { background-color: #409eff; color: #fff; border-bottom-right-radius: 10rpx; } .message-received { background-color: #fff; color: #333; border-bottom-left-radius: 10rpx; } } .message-time { font-size: 22rpx; color: #999; margin-top: 10rpx; .message-sent { text-align: right; } .message-received { text-align: left; } } .input-area { padding: 20rpx; background-color: #be2222; display: flex; gap: 20rpx; margin-bottom: env(safe-area-inset-bottom); } .safeArea { margin-bottom: 0px !important; } .message-input { flex: 1; height: 80rpx; padding: 0 30rpx; border-radius: 40rpx; background-color: #f5f5f5; font-size: 28rpx; } .send-btn { width: 140rpx; height: 80rpx; border-radius: 40rpx; background-color: #409eff; color: #fff; font-size: 28rpx; display: flex; align-items: center; justify-content: center; border: none; [disabled] { background-color: #ccc; } } .keyboard-placeholder { transform: height 0.3s ease; } /style