在 React + MUI 移动端(Capacitor WebView)中构建聊天页面时,会遇到一系列经典问题:
position: fixed 导致 flex 布局无法正确计算高度overflow: auto 吃 padding本技能提供一套经过魅族 21 真机验证的完整解决方案。
错误做法:输入框用 position: fixed; bottom: 76px,消息列表用 flex: 1 估算高度。
正确做法:全部放入 normal flow flex 列:
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
{/* 消息列表 */}
<Box sx={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
{messages.map(...)}
</Box>
{/* 输入框 */}
<Box sx={{ flexShrink: 0 }}>
<textarea />
</Box>
</Box>
关键点:
minHeight: 0 — 必须设置,否则 flex 子元素不会收缩,消息列表会撑破父容器overflow: 'hidden' 在外层,保证内容不溢出flexShrink: 0 防止被挤压坑:在 overflow: auto 容器中使用 display: flex + gap 时,padding-top 可能被浏览器裁切。
解法:用普通 block 布局 + CSS 选择器控制间距:
<Box
ref={listRef}
sx={{
flex: 1, minHeight: 0, overflowY: 'auto',
px: 2, pb: 2,
'& > *:first-of-type': { mt: 7 }, // 第一条消息顶部 56px
'& > * + *': { mt: 2 }, // 消息间 16px
}}
>
输入框底部需要动态间距:
const [focused, setFocused] = useState(false);
<Box sx={{ flexShrink: 0 }}>
<Box sx={{ px: 2, py: 1.5, pb: focused ? '8px' : '56px' }}>
<textarea
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
/>
</Box>
</Box>
将 background 放在外层 wrapper 而非内层,让背景色覆盖到导航栏上方:
<Box sx={{
flexShrink: 0,
borderTop: '1px solid rgba(0,0,0,0.06)',
background: '#ffffff', // 外层背景
backdropFilter: 'blur(20px)',
}}>
<Box sx={{ px: 2, py: 1.5, pb: '56px' }}>
<textarea /> // 内层只负责 padding
</Box>
</Box>
核心问题:document.body.classList.add('keyboard-open') 不会触发 React 组件重渲染。用 forceUpdate + DOM 读取存在时序竞态,导航栏时有时无。
最终方案:CustomEvent.detail 传递明确 boolean 状态,BottomNav 用 useState 接收。
AIAssistant 端(发送明确状态):
useEffect(() => {
if (focused) {
document.body.classList.add('keyboard-open');
} else {
document.body.classList.remove('keyboard-open');
}
// 传递明确 boolean,不依赖 DOM 读取
window.dispatchEvent(new CustomEvent('keyboardStateChange', { detail: { open: focused } }));
return () => {
document.body.classList.remove('keyboard-open');
window.dispatchEvent(new CustomEvent('keyboardStateChange', { detail: { open: false } }));
};
}, [focused]);
BottomNav 端(React State 接收):
import { useState, useEffect } from 'react';
export default function BottomNav() {
const [keyboardOpen, setKeyboardOpen] = useState(false);
useEffect(() => {
const handler = (e) => setKeyboardOpen(e.detail?.open ?? false);
window.addEventListener('keyboardStateChange', handler);
return () => window.removeEventListener('keyboardStateChange', handler);
}, []);
const shouldHide = HIDE_PATHS.some((re) => re.test(location.pathname)) || keyboardOpen;
if (shouldHide) return null;
return (/* 导航栏 UI */);
}
⚠️ 不推荐:用 visualViewport.resize 或 window.resize 检测键盘开合。不同 Android ROM 行为差异巨大,容易导致键盘秒关、误判等问题。坚持用 onFocus/onBlur 驱动。
问题:在 Capacitor WebView 中,MUI 的 TextField 和 InputBase 即使是"非受控"模式(defaultValue),其内部状态机也可能干扰原生光标位置,导致无法在文字中间插入。
最终方案:纯原生 ,零 React/MUI 干预:
const inputRef = useRef(null);
// 不要 onChange handler
// 不要 value 绑定
// 发送时直接读 DOM
const handleSend = async () => {
const text = (inputRef.current?.value || '').trim();
if (!text || sending) return;
setMessages((prev) => [...prev, { role: 'user', content: text }]);
// 清空也直接操作 DOM
if (inputRef.current) inputRef.current.value = '';
// ...发送逻辑
};
// JSX —— 原生 textarea,仅套 MUI sx 样式
<Box
component="textarea"
ref={inputRef}
defaultValue=""
placeholder="输入消息..."
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
onKeyDown={handleKeyDown}
rows={1}
sx={{
flex: 1,
fontSize: '0.9rem',
lineHeight: 1.5,
fontFamily: 'inherit',
px: 1.5, py: 1,
borderRadius: 3,
bgcolor: 'rgba(255,255,255,0.04)',
color: '#e0e0e0',
border: '1px solid rgba(0,180,216,0.2)',
outline: 'none',
resize: 'none',
'&:hover': { borderColor: '#00B4D8' },
'&:focus': { borderColor: '#00D4FF' },
}}
/>
关键原则:
onChange handler(即使只写 ref,React 合成事件也可能干扰)value propinputRef.current.valueinputRef.current.value = ''<Box component="main" sx={{
flex: 1,
paddingTop: '8px',
paddingBottom: '80px',
overflow: 'hidden',
}}>
<Box sx={{ height: '100%', overflow: 'hidden' }}>
<Box sx={{ height: '100%', overflowY: 'auto' }}>
<Routes>
<Route path="/ai" element={
<Box sx={{ height: '100%', overflow: 'hidden', position: 'relative' }}>
<AIAssistant />
</Box>
} />
</Routes>
</Box>
</Box>
</Box>
将以上各部分组合即可得到一个完整的、生产就绪的聊天页面布局。
| 文件 | 作用 |
|---|---|
| ------ | ------ |
src/app/pages/AIAssistant.jsx | 聊天主页面(textarea + flex 布局) |
src/app/components/BottomNav.jsx | 底部导航栏(CustomEvent + useState) |
src/app/App.jsx | 路由容器 |
| 坑 | 原因 | 方案 |
|---|---|---|
| ---- | ------ | ------ |
| overflow:auto 内 flex+gap 吃 padding | 浏览器 flex 实现差异 | block 布局 + CSS selector |
| position:fixed 输入框 | flex 无法计算真实高度 | normal flow flex 列 |
| forceUpdate + DOM 读取导航栏状态 | React 重渲染 vs DOM 更新竞态 | CustomEvent.detail + React state |
| visualViewport.resize 检测键盘 | 不同 Android ROM 行为差异巨大 | 不用!只用 onFocus/onBlur |
| MUI TextField 光标跳末尾 | MUI 内部状态机干扰 WebView | 原生 零干预 |
共 1 个版本