/** * 自定义底部导航栏组件 - 公众号客服交互模式 * * 功能清单: * 1. 3个Tab切换(首页/设备/任务) * 2. 右侧键盘图标 → 跳转到聊天页 * 3. 聊天输入模式 - 文字模式(默认) * 4. 左侧🔊小喇叭 → 切换到语音录制模式 * 5. 语音模式下左侧⌨键盘 → 切回文字模式 * 6. 按住说话 → 松开发送 * 7. 语音转文字(模拟) */ Component({ data: { // ---- TabBar 相关 ---- currentTab: 0, tabList: [ { pagePath: '/pages/index/index', icon: '/assets/icons/home.png', selectedIcon: '/assets/icons/home-active.png', text: '首页' }, { pagePath: '/pages/chat/chat', icon: '/assets/icons/chat.png', selectedIcon: '/assets/icons/chat-active.png', text: '设备' }, { pagePath: '/pages/task/task', icon: '/assets/icons/task.png', selectedIcon: '/assets/icons/task-active.png', text: '任务' } ], // ---- 聊天面板相关 ---- showChatPanel: false, // ---- 输入模式 ---- voiceMode: false, inputText: '', hasInput: false, inputFocus: true, // ---- 语音录制相关 ---- isRecording: false, recordStartTime: 0, recorderManager: null, tempFilePath: '', // ---- 语音转文字 ---- isVoiceToText: false, // ---- 软键盘适配 ---- keyboardHeight: 0, tabBarBottom: 0, }, lifetimes: { attached() { this._initRecorder() this._initAudioPlayer() this._calcKeyboardStyle() } }, pageLifetimes: { show() { const pages = getCurrentPages() const currentPage = pages[pages.length - 1] const isChatPage = currentPage && currentPage.route === 'pages/chat/chat' console.log('[TabBar] 页面显示:', currentPage ? currentPage.route : '未知', '→', isChatPage ? '聊天模式' : '导航模式') this.setData({ showChatPanel: isChatPage }) if (!isChatPage) { this.setData({ keyboardHeight: 0, inputText: '', hasInput: false }) this._calcKeyboardStyle() } } }, methods: { /* ========================================================== 一、TabBar 切换 ========================================================== */ onSwitchTab(e) { const { index, path } = e.currentTarget.dataset if (index === this.data.currentTab) return console.log('[TabBar] 切换到:', this.data.tabList[index].text) this.setData({ currentTab: index }) wx.switchTab({ url: path, fail: (err) => { console.error('[TabBar] 页面跳转失败:', err) this.setData({ currentTab: index === 0 ? 1 : 0 }) } }) }, /* ========================================================== 二、聊天页面跳转 ========================================================== */ goToChat() { this._stopRecordingIfNeeded() wx.switchTab({ url: '/pages/chat/chat' }) }, goHome() { this._stopRecordingIfNeeded() this.setData({ keyboardHeight: 0, inputText: '', hasInput: false, voiceMode: false, isRecording: false, isVoiceToText: false, inputFocus: false }) this._calcKeyboardStyle() wx.switchTab({ url: '/pages/index/index' }) }, /* ========================================================== 三、输入模式切换(文字 ↔ 语音) ========================================================== */ switchToVoiceMode() { console.log('[输入] 切换到语音模式') wx.hideKeyboard() this.setData({ voiceMode: true, inputFocus: false }) wx.showToast({ title: '已切换为语音模式', icon: 'none', duration: 1000 }) }, switchToTextMode() { console.log('[输入] 切换到文字模式') this._stopRecordingIfNeeded() this.setData({ voiceMode: false, isRecording: false, inputFocus: true }) wx.showToast({ title: '已切换为文字模式', icon: 'none', duration: 1000 }) }, /* ========================================================== 四、文字消息收发 ========================================================== */ onInputChange(e) { const val = e.detail.value this.setData({ inputText: val, hasInput: !!val.trim() }) }, _calcKeyboardStyle() { const sys = wx.getSystemInfoSync() const rpxToPx = sys.windowWidth / 750 const tabH = Math.round(100 * rpxToPx) const kbH = this.data.keyboardHeight this.setData({ tabBarBottom: kbH }) }, onInputFocus(e) { const h = e.detail.height || 0 console.log('[键盘] focus 高度:', h, 'px') if (h > 0) { this.setData({ keyboardHeight: h }) this._calcKeyboardStyle() } }, onKeyboardHeightChange(e) { const h = e.detail.height || 0 console.log('[键盘] 实时高度:', h, 'px') this.setData({ keyboardHeight: h }) this._calcKeyboardStyle() }, onInputBlur() { this.setData({ keyboardHeight: 0 }) this._calcKeyboardStyle() }, sendTextMessage() { const text = this.data.inputText.trim() if (!text) { wx.showToast({ title: '请输入消息', icon: 'none' }) return } this.setData({ inputText: '', hasInput: false }) const pages = getCurrentPages() const chatPage = pages.find(p => p.route === 'pages/chat/chat') if (chatPage && chatPage.sendMessage) { chatPage.sendMessage(text) } }, /* ========================================================== 五、语音录制功能 ========================================================== */ _initRecorder() { try { const manager = wx.getRecorderManager() manager.onStart(() => { console.log('[录音] 开始录制') this.setData({ isRecording: true, recordStartTime: Date.now() }) }) manager.onStop((res) => { console.log('[录音] 录制结束:', res.tempFilePath, res.duration + 'ms') const duration = res.duration || (Date.now() - this.data.recordStartTime) this.setData({ isRecording: false, tempFilePath: res.tempFilePath || '' }) if (duration < 500) { console.log('[录音] 录制时间过短,忽略') return } this._sendVoiceMessage(res.tempFilePath || this.data.tempFilePath, duration) }) manager.onError((err) => { console.error('[录音] 错误:', err) this.setData({ isRecording: false }) wx.showToast({ title: '录音失败: ' + (err.errMsg || '未知错误'), icon: 'none' }) }) this.setData({ recorderManager: manager }) } catch (e) { console.warn('[录音] 初始化失败,可能不支持录音:', e) this.setData({ recorderManager: null }) } }, startRecordVoice() { const mgr = this.data.recorderManager if (!mgr) { wx.showToast({ title: '录音功能不可用', icon: 'none' }) return } console.log('[录音] 手指按下,开始录音...') wx.vibrateShort({ type: 'medium' }) mgr.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000, format: 'mp3' }) }, stopRecordVoice() { const mgr = this.data.recorderManager if (!mgr || !this.data.isRecording) return console.log('[录音] 手指松开,停止录音') wx.vibrateShort({ type: 'light' }) mgr.stop() }, cancelRecordVoice() { const mgr = this.data.recorderManager if (!mgr || !this.data.isRecording) return console.log('[录音] 取消录音') wx.vibrateShort({ type: 'light' }) mgr.stop() this.setData({ isRecording: false, tempFilePath: '' }) wx.showToast({ title: '已取消发送', icon: 'none' }) }, _stopRecordingIfNeeded() { if (this.data.isRecording && this.data.recorderManager) { try { this.data.recorderManager.stop() } catch (e) { /* ignore */ } this.setData({ isRecording: false }) } }, _sendVoiceMessage(tempFilePath, durationMs) { const durationSec = Math.ceil(durationMs / 1000) const pages = getCurrentPages() const chatPage = pages.find(p => p.route === 'pages/chat/chat') if (!chatPage || !chatPage.sendMessage) return chatPage.sendMessage(`[语音消息 ${durationSec}秒]`) setTimeout(() => { chatPage.sendMessage('🎤 我已经收到您的语音消息了。目前语音识别功能还在完善中,您也可以直接打字告诉我需要什么帮助哦~') }, 1200) }, /* ========================================================== 六、语音转文字(点击🎤按钮触发) ========================================================== */ toggleVoiceToText() { if (this.data.isVoiceToText) { this.stopVoiceToText() } else { this.startVoiceToText() } }, startVoiceToText() { console.log('[语音转文字] 🎤 开始识别') const mgr = this.data.recorderManager if (!mgr) { wx.showToast({ title: '语音功能不可用', icon: 'none' }) return } this.setData({ isVoiceToText: true, inputFocus: false, inputText: '' }) wx.vibrateShort({ type: 'medium' }) wx.showToast({ title: '请说话...', icon: 'none', duration: 1500 }) mgr.start({ duration: 30000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000, format: 'mp3' }) }, stopVoiceToText() { console.log('[语音转文字] ⏹ 停止识别') wx.vibrateShort({ type: 'light' }) wx.showToast({ title: '正在识别...', icon: 'loading', duration: 2000 }) const mgr = this.data.recorderManager if (mgr && this.data.tempFilePath) { try { mgr.stop() } catch (e) { /* ignore */ } } this.setData({ isVoiceToText: false }) this._recognizeVoice(this.data.tempFilePath) }, _recognizeVoice(audioPath) { if (!audioPath) { console.warn('[语音转文字] 没有音频文件,跳过识别') return } /* // 方案一:调用微信同声传译插件(需在 app.json 声明 plugin) const plugin = requirePlugin('WechatSI') const manager = plugin.getRecordRecognitionManager() manager.onStop((res) => { const text = res.result || '' if (text.trim()) { this.setData({ inputText: text, inputFocus: true }) } }) manager.onError((err) => { wx.showToast({ title: '识别失败', icon: 'none' }) }) */ // 方案二:模拟返回(开发调试用) setTimeout(() => { const mockResults = ['帮我查看一下设备状态', '打开客厅灯', '今天的任务有哪些', '你好'] const recognizedText = mockResults[Math.floor(Math.random() * mockResults.length)] this.setData({ inputText: recognizedText, hasInput: !!recognizedText.trim(), inputFocus: true }) wx.hideToast() console.log(`[语音转文字] ✅ 模拟识别结果已写入: "${recognizedText}"`) }, 1500) }, /* ========================================================== 七、语音播放 ========================================================== */ _initAudioPlayer() { try { const ctx = wx.createInnerAudioContext() ctx.onPlay(() => { this.setData({ isPlayingVoice: true }) }) ctx.onEnded(() => { this.setData({ isPlayingVoice: false }) }) ctx.onError((err) => { console.error('[播放] 错误:', err) this.setData({ isPlayingVoice: false }) }) this.setData({ innerAudioContext: ctx }) } catch (e) { this.setData({ innerAudioContext: null }) } }, /* ========================================================== 八、工具按钮 ========================================================== */ onTapEmoji() { console.log('[输入] 表情按钮点击') wx.showToast({ title: '表情功能开发中', icon: 'none' }) }, onRightBtnTap() { if (this.data.hasInput) { this.sendTextMessage() } else { this.onTapMore() } }, onTapMore() { wx.showActionSheet({ itemList: ['发送图片', '拍摄照片', '发送文件', '位置信息'], success: (res) => { const actions = ['image', 'camera', 'file', 'location'] console.log('[更多] 选择了:', actions[res.tapIndex]) } }) } } })