需求背景
CleanLearn 最早有两个独立的 AI 对话入口:“打字对话”(纯文字聊天)和“口语录音”(按住录音、松开发送)。用户需要在一个页面里切换输入方式,而不是跳来跳去。目标是把两个页面合并成一个,像微信那样——点击图标在键盘和麦克风之间切换。
合并方案
底部导航简化
原来的底部 Tab 有 4 个:对话 / 口语 / 场景 / 记录。合并后变为 3 个:对话 / 场景 / 记录,”口语”融入”对话”的语音模式里。
// 合并前
const TABS = [
{ key: 'chat', label: '对话' },
{ key: 'speak', label: '口语' }, // ← 去掉
{ key: 'scenario', label: '场景' },
{ key: 'history', label: '记录' },
]
// 合并后
const TABS = [
{ key: 'chat', label: '对话' }, // 文字+语音都在这里
{ key: 'scenario', label: '场景' },
{ key: 'history', label: '记录' },
]
输入模式一键切换
输入栏左侧放一个切换图标:文字模式下显示麦克风 🎤(点它切到语音),语音模式下显示键盘 ⌨(点它切回文字)。状态由一个简单的 useState 管理:
const [inputMode, setInputMode] = useState('text' | 'voice')('text')
// 文字模式 → 显示 ChatInput 输入框
// 语音模式 → 显示"按住说话"长条按钮
语音交互:按住说话,松开发送
类微信交互——一个满宽的胶囊形按钮,onTouchStart 开始录音,onTouchEnd 停止录音并自动 STT→发送:
// 按下 → 开始录音
const handleTouchStart = () => {
recorderRef.current?.start({
duration: 60000, // 最长60秒
sampleRate: 16000, // 16kHz
numberOfChannels: 1, // 单声道
format: 'wav', // WAV格式
})
setIsRecording(true)
}
// 松开 → 停止、转写、发送
const handleTouchEnd = () => {
recorderRef.current?.stop()
// onStop 回调中自动调 transcribeAudio() → sendMessage()
}
录音中显示红色脉冲动画 + 计时器(00:05…00:30…),按钮文案变为”松开 发送”,视觉反馈清晰。
UI 微调迭代
合并完成后,用户又提了几轮 UI 调整。这个过程挺有意思的——它展示了”用户要的往往比开发者理解的要少”:
第一轮:”删掉这个提示条”
用户说”修改 AI 语音提问你录音回答这一行”,我第一次理解成了要删掉整个”口语训练”场景卡片,结果用户说”其他场景还是会有”——原来用户只想删黄色的”AI 朗读已开启”提示条。我只删了提示条但没恢复场景卡片,用户纠正说”我只是让你删除这个提示条而已没让你删除功能”。赶紧恢复场景卡片,只删提示条,功能照旧。
第二轮:场景提示条和会话切换条
删掉”AI 朗读已开启”后,用户注意到还有黄色场景描述条(进入咖啡店/机场等场景时显示”在咖啡店用英语点单”)和顶部会话切换条(多个会话时横向滚动的标签)。场景描述条直接删掉;会话切换条因为和底部导航功能重复,也删掉,换成新的场景提示条——显示当前场景名称和描述,样式简洁。
第三轮:状态栏留白
聊天页用了 position: fixed; top: 0,导致顶部紧贴屏幕边缘,和真题模拟页面对比有明显的视觉差异。参考真题模拟页面的间距,加了 paddingTop: calc(44px + env(safe-area-inset-top)),给状态栏留出空间。
Git 分支管理
这个项目有明确的分支规范:
- master — 功能类改动(逻辑修复、API、store、hooks、工具函数)
- ui_change — UI 类改动(样式、布局、颜色、间距、视觉调整)
这轮 UI 改动都在 ui_change 分支上完成,每个 commit 粒度很小(一个提示条删除就是一个 commit),方便回溯。
经验教训
- 先确认用户意图再动手:用户说”删掉这一行”可能只是删一个提示条,不是删整个功能模块
- 小步提交:每个 UI 调整单独 commit,出了问题可以用
git revert 精准回滚
- UI 和逻辑分分支:
ui_change 和 master 分开,互不干扰
- 微信小程序 fixed 布局要手动处理安全区:
env(safe-area-inset-top) 在固定定位下不会自动生效
← 返回目录