← 返回
未分类

移动端聊天页面完整布局方案

React + MUI 移动端聊天页面完整布局方案 — 解决消息列表滚动裁切、输入框键盘适配、底部导航栏联动隐藏等核心问题。基于 flex 布局 + custom event 架构,兼容 Capacitor WebView。
React + MUI 移动端聊天页面完整布局方案 — 解决消息列表滚动裁切、输入框键盘适配、底部导航栏联动隐藏等核心问题。基于 flex 布局 + custom event 架构,兼容 Capacitor WebView。
码坚强
未分类 community v1.0.0 1 版本 94444.4 Key: 无需
★ 0
Stars
📥 17
下载
💾 0
安装
1
版本
#latest

概述

Mobile Chat Layout — 移动端聊天页面完整布局方案

概述

在 React + MUI 移动端(Capacitor WebView)中构建聊天页面时,会遇到一系列经典问题:

  • 消息列表滚动时顶部/底部被裁切
  • 输入框用 position: fixed 导致 flex 布局无法正确计算高度
  • 键盘弹起时底部导航栏未隐藏,输入框离键盘太远
  • flex 容器内 overflow: auto 吃 padding
  • 底部灰色间隙
  • Capacitor WebView 中光标无法在文字中间插入(受控组件每次 setState 光标跳末尾)
  • 导航栏隐藏后不恢复(DOM classList 读取 vs React 重渲染竞态)

本技能提供一套经过魅族 21 真机验证的完整解决方案。


核心架构

1. 整体布局:flex + normal flow(非 fixed)

错误做法:输入框用 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 防止被挤压

2. 消息列表顶部间距(避坑)

:在 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
  }}
>

3. 输入框底部间距(闲置 vs 键盘)

输入框底部需要动态间距:

  • 闲置时:留出导航栏高度(~56px)
  • 键盘弹起时:仅留 8px 贴边
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>

4. 输入框底部无灰边

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>

5. 导航栏显隐:CustomEvent + React State(非 DOM 读取)

核心问题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.resizewindow.resize 检测键盘开合。不同 Android ROM 行为差异巨大,容易导致键盘秒关、误判等问题。坚持用 onFocus/onBlur 驱动。

6. Capacitor WebView 光标修复(关键)

问题:在 Capacitor WebView 中,MUI 的 TextFieldInputBase 即使是"非受控"模式(defaultValue),其内部状态机也可能干扰原生光标位置,导致无法在文字中间插入。

最终方案纯原生