第十七章:移动端适配实战
17 — 移动端适配实战
本章讲解本项目的移动端适配。
重点:Chrome 100vh 坑、iOS 安全区、抽屉式侧边栏、跨组件通信。
1. 适配策略总览
| 断点 | 行为 |
|---|---|
| > 768px | 桌面布局:Sidebar 作为 flex 子项永久参与布局 |
| ≤ 768px | 移动布局:Sidebar 变 fixed 抽屉,点击 hamburger 滑出 |
项目里所有响应式 CSS 用 @media (max-width: 768px) 切分。
2. 核心问题:Chrome 100vh 坑
2.1 问题
Chrome 移动端的 100vh 包含地址栏高度(约 56px),而地址栏在滚动时会收起/展开。所以用 100vh 确定的底部边界在 Chrome 里比实际可见区域高出地址栏的高度,输入框被推到屏幕之外。
夸克浏览器正确处理了动态视口高度(用了 dvh 或动态计算),所以显示正常。
2.2 修复:用 position: fixed; inset: 0 绑定真实视口
文件:src/styles/global.css
1 | html, |
fixed + inset: 0 绑定的是浏览器真实可见区域,无论地址栏收起还是展开,四个边都跟视口重合。Chrome 和夸克的行为就一致了。
2.3 对比:三种方案
| 方案 | Chrome 行为 | 问题 |
|---|---|---|
height: 100vh | 高度 = 屏幕 + 地址栏 | 输入框被地址栏盖住 |
height: 100dvh | 高度 = 动态视口(不含地址栏) | 部分 Android 浏览器不支持 |
✅ position: fixed; inset: 0 | 绑定真实可见区域 | 兼容性最好,行为最稳定 |
2.4 完整布局链(修复后)
1 | /* App.module.css */ |
关键点:
position: fixed锁定整个应用到真实视口box-sizing: border-box+padding: env(safe-area-inset-*)适配 iOS 刘海/底部 home indicatoroverflow: hidden防止任何子元素溢出屏幕
3. iOS 安全区适配
3.1 三个安全区
| 变量 | 用途 | 典型设备 |
|---|---|---|
env(safe-area-inset-top) | 顶部刘海 | iPhone 14 Pro 等 |
env(safe-area-inset-bottom) | 底部 home indicator | 所有带底部条 |
env(safe-area-inset-left/right) | 横屏时左右 | iPad Pro |
3.2 项目里如何用
在 global.css 预定义 CSS 变量:
1 | html { |
在组件里引用(用 var() 包裹支持 fallback):
1 | /* InputArea.module.css */ |
max() 保证 padding 至少是 space-2(8px),有安全区时取大值。
3.3 必须在 <meta viewport> 加 viewport-fit=cover
1 | <!-- index.html --> |
不加这个,env(safe-area-inset-*) 永远是 0。
4. 抽屉式 Sidebar(移动端核心改造)
4.1 桌面端 vs 移动端结构差异
1 | /* Sidebar.module.css */ |
4.2 移动端专属:背景遮罩
1 | .backdrop { |
关键:背景色用 --bg-overlay(50-65% 透明),不要用太深的纯色 + blur(会让背后内容完全看不见)。
4.3 z-index 层级
1 | /* tokens.css */ |
层级关系:
- 背景水墨装饰(
z: 0) - 主内容(
z: 1) - 顶栏(
z: 50,浮在主内容之上) - 抽屉背景遮罩(
z: 80) - 抽屉(
z: 90) - 弹出层(
z: 100,主题面板)
5. 输入区移动端适配
5.1 iOS 防自动缩放
1 | .area textarea { |
5.2 防止 textarea 撑破容器
1 | .area textarea { |
关键:min-width: 0 在 flex 子项上至关重要 — 默认 min-width 是 auto(基于内容宽度),会导致 flex 子项即使有 flex: 1 也无法被压缩到内容宽度以下。
5.3 极小屏按钮缩小
1 | @media (max-width: 380px) { |
6. Agent 选择器移动端
问题:5 个 Agent 在窄屏排成一行会溢出。
解决:横向滚动 + 隐藏滚动条:
1 | .selector { |
flex-shrink: 0 防止子项被压缩,overflow-x: auto 触发横向滚动。
7. 豆豆 logo 永远在屏幕最左上
7.1 桌面端 sidebar 展开时
豆豆 logo 在 Sidebar 头部(屏幕最左 = sidebar 最左)。
1 | // Sidebar.tsx |
7.2 桌面端 sidebar 收起时
豆豆 logo 在 Topbar 头部(屏幕最左 = topbar 最左)。
1 | // Topbar.tsx |
7.3 移动端
豆豆 logo 在 Topbar 头部(移动端 sidebar 是抽屉,不参与布局)。
8. 跨组件通信(CustomEvent)
8.1 为什么需要
问题:Topbar 在 main 内,Sidebar 在 main 外。它们是兄弟组件,但都需要 sidebarOpen 状态。
不优雅的解法:提升到 App 层
1 | // App.tsx |
问题:组件内部 state 提升到 App 后,App 包含过多状态。
8.2 优雅解法:CustomEvent
文件:themeStore.ts(也导出通信函数)
1 | // themeStore.ts |
1 | // Topbar.tsx |
1 | // App.tsx |
8.3 项目里的两个 CustomEvent
| 事件名 | 发送方 | 接收方 | 用途 |
|---|---|---|---|
dodo:sidebar-toggle | Topbar / Sidebar | Topbar / Sidebar | 同步 sidebar 展开状态 |
dodo:open-theme-switcher | Topbar | App | 打开主题切换面板 |
8.4 模式总结
1 | // 发送方 |
优势:
- 解耦兄弟组件
- 不需要把状态提升到共同父级
- 事件名带命名空间(
dodo:),避免冲突
9. 移动端调试技巧
9.1 Chrome DevTools 模拟移动端
- F12 打开 DevTools
- 点击左上角手机图标(或 Ctrl+Shift+M)
- 选择设备(如 iPhone 14 Pro)
- 切换顶部地址栏模拟滚动
9.2 真实设备测试
1 | # 让手机和电脑在同一 WiFi |
9.3 vConsole(移动端调试神器)
如果想看移动端 console:
1 | npm install vconsole |
1 | // main.tsx |
DevTools 元素检查器在移动端不方便,vConsole 提供浮动的 console 面板。
10. 常见移动端 Bug 与修复
Bug 1:Chrome 底部输入框被地址栏盖住
✅ 已修复:用 position: fixed; inset: 0 替代 100vh
Bug 2:iOS 输入框 focus 时页面被缩放
✅ 已修复:input/textarea 字号 ≥ 16px
Bug 3:侧边栏打开时全局变磨砂玻璃
✅ 已修复:遮罩用 50-65% 半透明背景 + blur,而不是深色 + blur
Bug 4:移动端 sidebar 按钮被破坏
✅ 已修复:移动端用 @media 重写按钮大小,桌面/移动两套样式
Bug 5:Agent 选择器溢出
✅ 已修复:overflow-x: auto + flex-shrink: 0
11. 一段话总结
移动端适配三大要点:
- 真实视口:
position: fixed; inset: 0替代100vh(修 Chrome bug)- iOS 安全区:
env(safe-area-inset-*)+viewport-fit=cover- 抽屉式 Sidebar:桌面 flex 子项 / 移动 fixed 抽屉
跨组件通信:CustomEvent 解决兄弟组件状态共享,比 prop drilling 干净。
输入区防溢出:flex: 1 1 0; min-width: 0+box-sizing: border-box+overflow: hidden。
iOS 防缩放:input/textarea 字号 ≥ 16px。
接下来
继续阅读 18-项目组件详解.md — 理解每个组件的代码细节。




