378 lines
12 KiB
JavaScript
378 lines
12 KiB
JavaScript
/**
|
||
* 自定义底部导航栏组件 - 公众号客服交互模式
|
||
*
|
||
* 功能清单:
|
||
* 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])
|
||
}
|
||
})
|
||
}
|
||
|
||
}
|
||
})
|