增量提交
This commit is contained in:
377
Claw/client/wechat_app/custom-tab-bar/index.js
Normal file
377
Claw/client/wechat_app/custom-tab-bar/index.js
Normal file
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* 自定义底部导航栏组件 - 公众号客服交互模式
|
||||
*
|
||||
* 功能清单:
|
||||
* 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])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
1
Claw/client/wechat_app/custom-tab-bar/index.json
Normal file
1
Claw/client/wechat_app/custom-tab-bar/index.json
Normal file
@@ -0,0 +1 @@
|
||||
{"component": true, "usingComponents": {}}
|
||||
125
Claw/client/wechat_app/custom-tab-bar/index.wxml
Normal file
125
Claw/client/wechat_app/custom-tab-bar/index.wxml
Normal file
@@ -0,0 +1,125 @@
|
||||
<!--
|
||||
自定义底部导航栏 - 公众号客服交互模式
|
||||
|
||||
两种状态:
|
||||
状态1(默认): [首页] [设备] [任务] [⌨]
|
||||
状态2(聊天模式): [☰菜单] [🔊/⌨] [输入框........] [🎤] [😀] [↗/+](自动根据当前页面判断)
|
||||
|
||||
交互:
|
||||
点击⌨ → switchTab 跳到聊天页,tab-bar 自动进入状态2
|
||||
点击☰ → switchTab 跳回首页,tab-bar 自动回到状态1
|
||||
-->
|
||||
<view class="tab-bar {{showChatPanel ? 'chat-mode' : ''}}" style="bottom: {{tabBarBottom}}px;">
|
||||
|
||||
<!-- ====== 状态1:普通导航模式(默认)====== -->
|
||||
<block wx:if="{{!showChatPanel}}">
|
||||
<!-- 左侧3个Tab -->
|
||||
<view
|
||||
wx:for="{{tabList}}"
|
||||
wx:key="index"
|
||||
class="tab-item {{currentTab === index ? 'active' : ''}}"
|
||||
data-index="{{index}}"
|
||||
data-path="{{item.pagePath}}"
|
||||
bindtap="onSwitchTab"
|
||||
>
|
||||
<text class="tab-text">{{item.text}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 右侧键盘按钮:跳转到聊天页 -->
|
||||
<view class="keyboard-btn" bindtap="goToChat">
|
||||
<view class="kb-circle">
|
||||
<text class="kb-icon">⌨</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- ====== 状态2:聊天输入模式(点击键盘后)====== -->
|
||||
<block wx:else>
|
||||
<!-- 左侧:菜单按钮(跳回首页) -->
|
||||
<view class="chat-tool-btn" bindtap="goHome">
|
||||
<text class="tool-icon">☰</text>
|
||||
</view>
|
||||
|
||||
<!-- 左侧2:语音/文字切换按钮 -->
|
||||
<view class="chat-tool-btn" wx:if="{{!voiceMode}}" bindtap="switchToVoiceMode">
|
||||
<text class="tool-icon">🔊</text>
|
||||
</view>
|
||||
<view class="chat-tool-btn" wx:else bindtap="switchToTextMode">
|
||||
<text class="tool-icon">⌨</text>
|
||||
</view>
|
||||
|
||||
<!-- 中间:文本输入框 / 语音录制按钮 -->
|
||||
<input
|
||||
class="chat-input"
|
||||
type="text"
|
||||
value="{{inputText}}"
|
||||
placeholder="请输入您的问题..."
|
||||
placeholder-class="placeholder-style"
|
||||
confirm-type="send"
|
||||
bindinput="onInputChange"
|
||||
bindconfirm="sendTextMessage"
|
||||
bindfocus="onInputFocus"
|
||||
bindblur="onInputBlur"
|
||||
bindkeyboardheightchange="onKeyboardHeightChange"
|
||||
focus="{{inputFocus}}"
|
||||
adjust-position="{{false}}"
|
||||
wx:if="{{!voiceMode}}"
|
||||
/>
|
||||
|
||||
<!-- 语音模式:按住说话 -->
|
||||
<view
|
||||
class="voice-btn {{isRecording ? 'recording' : ''}}"
|
||||
bindtouchstart="startRecordVoice"
|
||||
bindtouchend="stopRecordVoice"
|
||||
bindtouchcancel="cancelRecordVoice"
|
||||
wx:if="{{voiceMode}}"
|
||||
>
|
||||
<text class="voice-text">{{isRecording ? '松开发送' : '按住 说话'}}</text>
|
||||
<view class="record-wave" wx:if="{{isRecording}}">
|
||||
<view class="wave wave1"></view>
|
||||
<view class="wave wave2"></view>
|
||||
<view class="wave wave3"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧1:语音转文字按钮(点击开始/停止识别) -->
|
||||
<view
|
||||
class="chat-tool-btn send-area {{isVoiceToText ? 'voice-active' : ''}}"
|
||||
wx:if="{{!voiceMode}}"
|
||||
bindtap="toggleVoiceToText"
|
||||
>
|
||||
<text class="tool-icon send-icon {{isVoiceToText ? 'send-icon-active' : 'send-icon'}}">🎤</text>
|
||||
</view>
|
||||
<view class="chat-tool-btn send-area disabled" wx:else>
|
||||
<text class="tool-icon">🎤</text>
|
||||
</view>
|
||||
|
||||
<!-- 右侧2:表情按钮 -->
|
||||
<view class="chat-tool-btn" bindtap="onTapEmoji">
|
||||
<text class="tool-icon">😀</text>
|
||||
</view>
|
||||
|
||||
<!-- 右侧3:发送按钮(有内容时)或 更多按钮(无内容时) -->
|
||||
<view
|
||||
wx:if="{{hasInput}}"
|
||||
style="width:96rpx;height:80rpx;display:flex;align-items:center;justify-content:center;"
|
||||
bindtap="sendTextMessage"
|
||||
>
|
||||
<view style="width:88rpx;height:72rpx;background:linear-gradient(135deg,#1677FF,#0958D9);border-radius:16rpx;display:flex;align-items:center;justify-content:center;box-shadow:0 4rpx 14rpx rgba(22,119,255,0.3);">
|
||||
<text style="font-size:38rpx;color:#ffffff;font-weight:bold;line-height:1;">↗</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="chat-tool-btn" bindtap="onTapMore">
|
||||
<text class="tool-icon">+</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- ====== 语音播放中提示 ====== -->
|
||||
<view class="voice-playing-toast" wx:if="{{isPlayingVoice}}">
|
||||
<view class="playing-wave">
|
||||
<view class="p-bar p1"></view><view class="p-bar p2"></view>
|
||||
<view class="p-bar p3"></view><view class="p-bar p4"></view><view class="p-bar p5"></view>
|
||||
</view>
|
||||
<text class="playing-text">正在播放...</text>
|
||||
</view>
|
||||
472
Claw/client/wechat_app/custom-tab-bar/index.wxss
Normal file
472
Claw/client/wechat_app/custom-tab-bar/index.wxss
Normal file
@@ -0,0 +1,472 @@
|
||||
/**
|
||||
* 自定义底部导航栏 - 公众号客服交互模式
|
||||
*
|
||||
* 两种状态:
|
||||
* 状态1(普通):[首页] [设备] [任务] [⌨]
|
||||
* 状态2(聊天):[☰] [🔊/⌨] [输入框........] [🎤] [😀] [+]
|
||||
*
|
||||
* 统一配色方案:
|
||||
* 主题色:#1677FF(蓝),用于高亮、选中态、交互反馈
|
||||
* 背景:#FFFFFF(白)/ #1A1A1A(深黑)
|
||||
* 边框:#E8E8E8(浅灰)/ #2E2E2E(深灰)
|
||||
* 文字:#333333(深灰)/ #999999(灰)/ #E8E8E8(浅白)
|
||||
*/
|
||||
|
||||
/* ============================================================
|
||||
一、设计令牌(统一颜色变量)
|
||||
============================================================ */
|
||||
|
||||
/* 状态1(普通模式)*/
|
||||
.tab-bar {
|
||||
/* 背景与边框 */
|
||||
--bg-primary: #ffffff;
|
||||
--border-primary: #e8e8e8;
|
||||
/* Tab 文字 */
|
||||
--tab-inactive: #999999;
|
||||
--tab-active: #333333;
|
||||
/* 键盘按钮 */
|
||||
--kb-border: #bbbbbb;
|
||||
--kb-icon: #666666;
|
||||
--kb-border-left: #f0f0f0;
|
||||
/* 聊天模式工具按钮(浅色背景用深色图标) */
|
||||
--tool-bg: #f5f5f5;
|
||||
--tool-icon: #555555;
|
||||
}
|
||||
|
||||
/* 聊天模式下:导航栏保持白色不变,工具按钮区用浅灰底色区分 */
|
||||
.tab-bar.chat-mode {
|
||||
/* 背景与边框 — 导航栏保持白色,与整体布局统一 */
|
||||
--bg-primary: #ffffff;
|
||||
--border-primary: #e8e8e8;
|
||||
/* Tab 文字 */
|
||||
--tab-inactive: #999999;
|
||||
--tab-active: #333333;
|
||||
/* 键盘按钮 */
|
||||
--kb-border: #bbbbbb;
|
||||
--kb-icon: #666666;
|
||||
--kb-border-left: #f0f0f0;
|
||||
/* 聊天工具按钮 — 浅灰底色,与白色导航栏融合但不突兀 */
|
||||
--tool-bg: #f0f0f0;
|
||||
--tool-icon: #555555;
|
||||
}
|
||||
|
||||
/* 全局统一色值 */
|
||||
page {
|
||||
/* 主题蓝(唯一品牌色) */
|
||||
--brand-blue: #1677FF;
|
||||
--brand-blue-dark: #0958D9;
|
||||
--brand-blue-light: #73ADFF;
|
||||
--brand-blue-glow: rgba(22, 119, 255, 0.8);
|
||||
/* 深色面板 */
|
||||
--bg-dark: #0d0d0d;
|
||||
--bg-dark-2: #1e1e1e;
|
||||
--bg-dark-3: #2e2e2e;
|
||||
/* 文字(深色模式) */
|
||||
--text-dark: #e8e8e8;
|
||||
--text-dark-secondary: #cccccc;
|
||||
--text-dark-muted: #888888;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
二、底部导航栏容器(固定在底部)
|
||||
============================================================ */
|
||||
.tab-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 100rpx;
|
||||
background-color: var(--bg-primary);
|
||||
border-top: 1rpx solid var(--border-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
/* 聊天模式下切换为深色背景 */
|
||||
.tab-bar.chat-mode {
|
||||
background-color: var(--bg-primary);
|
||||
border-top: 1rpx solid var(--border-primary);
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
二、状态1:普通导航模式
|
||||
============================================================ */
|
||||
|
||||
/* ---- Tab菜单项(3个平分剩余空间)---- */
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-item:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 28rpx;
|
||||
color: var(--tab-inactive);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tab-item.active .tab-text {
|
||||
color: var(--tab-active);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ---- 右侧键盘按钮(固定窄宽度)---- */
|
||||
.keyboard-btn {
|
||||
width: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
border-left: 1rpx solid var(--kb-border-left);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.keyboard-btn:active {
|
||||
opacity: 0.5;
|
||||
background-color: var(--border-primary);
|
||||
}
|
||||
|
||||
.kb-circle {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid var(--kb-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.kb-icon {
|
||||
font-size: 26rpx;
|
||||
color: var(--kb-icon);
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
三、状态2:聊天输入模式
|
||||
============================================================ */
|
||||
|
||||
/* ---- 工具按钮(通用:菜单/语音/表情/更多等)---- */
|
||||
.chat-tool-btn {
|
||||
flex-shrink: 0;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
/* 浅灰底色,融入白色导航栏 */
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.chat-tool-btn:active {
|
||||
opacity: 0.6;
|
||||
background-color: rgba(0,0,0,0.08) !important;
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
font-size: 40rpx;
|
||||
line-height: 1;
|
||||
color: var(--tool-icon);
|
||||
}
|
||||
|
||||
.send-area .tool-icon {
|
||||
font-size: 36rpx;
|
||||
color: var(--tool-icon);
|
||||
}
|
||||
|
||||
.send-area.disabled {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
/* ---- 🎤 语音转文字激活状态(高亮)---- */
|
||||
.send-icon {
|
||||
transition: all 0.2s ease;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.send-icon-active {
|
||||
animation: voice-pulse 1.5s ease-in-out infinite;
|
||||
filter: drop-shadow(0 0 8px var(--brand-blue-glow));
|
||||
}
|
||||
|
||||
@keyframes voice-pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
filter: drop-shadow(0 0 6px var(--brand-blue-glow));
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.15);
|
||||
filter: drop-shadow(0 0 14px var(--brand-blue));
|
||||
}
|
||||
}
|
||||
|
||||
/* 🎤激活时按钮高亮(浅色背景下的蓝色边框) */
|
||||
.voice-active {
|
||||
background: rgba(22, 119, 255, 0.1) !important;
|
||||
border: 1.5px solid rgba(22, 119, 255, 0.6) !important;
|
||||
border-radius: 12rpx !important;
|
||||
box-shadow: 0 0 10px rgba(22, 119, 255, 0.25) !important;
|
||||
}
|
||||
|
||||
/* ---- 发送按钮(替换+号) ---- */
|
||||
.send-action-btn {
|
||||
background: #1677FF !important;
|
||||
border-radius: 16rpx !important;
|
||||
width: 96rpx !important;
|
||||
box-shadow: 0 4rpx 14rpx rgba(22, 119, 255, 0.3);
|
||||
}
|
||||
|
||||
.send-action-btn:active {
|
||||
opacity: 0.85;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.send-icon-img {
|
||||
width: 42rpx;
|
||||
height: 42rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.send-arrow-icon {
|
||||
font-size: 38rpx;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.send-label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
/* ---- 输入框 ---- */
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
height: 68rpx;
|
||||
min-width: 0;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
margin: 0 4rpx;
|
||||
}
|
||||
|
||||
.placeholder-style {
|
||||
color: #999999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* ---- 语音录制按钮 ---- */
|
||||
.voice-btn {
|
||||
flex: 1;
|
||||
height: 68rpx;
|
||||
min-width: 0;
|
||||
border-radius: 12rpx;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin: 0 4rpx;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.voice-btn:active,
|
||||
.voice-btn.recording {
|
||||
background-color: var(--brand-blue);
|
||||
box-shadow: 0 2rpx 16rpx rgba(22, 119, 255, 0.35);
|
||||
}
|
||||
|
||||
.voice-text {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.voice-btn.recording .voice-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 录音波纹动画 */
|
||||
.record-wave {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wave {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
border: 3rpx solid rgba(22, 119, 255, 0.3);
|
||||
transform: translate(-50%, -50%);
|
||||
animation: waveExpand 1.5s ease-out infinite;
|
||||
}
|
||||
|
||||
.wave2 { animation-delay: 0.5s; }
|
||||
.wave3 { animation-delay: 1s; }
|
||||
|
||||
@keyframes waveExpand {
|
||||
from { width: 60rpx; height: 60rpx; opacity: 1; border-width: 4rpx; }
|
||||
to { width: 200rpx; height: 200rpx; opacity: 0; border-width: 1rpx; }
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
四、聊天消息面板(覆盖在页面内容上方)
|
||||
============================================================ */
|
||||
|
||||
.chat-overlay {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(100rpx + env(safe-area-inset-bottom));
|
||||
top: 0;
|
||||
z-index: 99998;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 消息列表 */
|
||||
.chat-msg-list {
|
||||
flex: 1;
|
||||
background-color: var(--bg-dark);
|
||||
padding: 24rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.msg-bottom-pad {
|
||||
height: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ---- 单行消息 ---- */
|
||||
.msg-row {
|
||||
display: flex;
|
||||
margin-bottom: 24rpx;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.msg-user {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
/* 头像 */
|
||||
.msg-avatar {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: bold;
|
||||
flex-shrink: 0;
|
||||
margin: 0 10rpx;
|
||||
}
|
||||
|
||||
.bot-avatar {
|
||||
background: linear-gradient(135deg, var(--brand-blue), var(--brand-blue-dark));
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
background: linear-gradient(135deg, var(--brand-blue-light), var(--brand-blue));
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 消息气泡 */
|
||||
.msg-bubble {
|
||||
max-width: 65%;
|
||||
padding: 18rpx 22rpx;
|
||||
border-radius: 18rpx;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.bot-bubble {
|
||||
background-color: var(--bg-dark-2);
|
||||
border: 2rpx solid rgba(255,255,255,0.06);
|
||||
border-radius: 4rpx 18rpx 18rpx 18rpx;
|
||||
}
|
||||
|
||||
.user-bubble {
|
||||
background: linear-gradient(135deg, var(--brand-blue), var(--brand-blue-dark));
|
||||
border-radius: 18rpx 4rpx 18rpx 18rpx;
|
||||
box-shadow: 0 4rpx 14rpx rgba(22, 119, 255, 0.22);
|
||||
}
|
||||
|
||||
.msg-text {
|
||||
font-size: 27rpx;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.bot-bubble .msg-text { color: var(--text-dark); }
|
||||
.user-bubble .msg-text { color: #ffffff; }
|
||||
|
||||
|
||||
/* ============================================================
|
||||
五、语音播放中提示
|
||||
============================================================ */
|
||||
|
||||
.voice-playing-toast {
|
||||
position: fixed;
|
||||
top: 160rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: rgba(0,0,0,0.85);
|
||||
border-radius: 20rpx;
|
||||
padding: 22rpx 36rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
z-index: 20000;
|
||||
backdrop-filter: blur(10rpx);
|
||||
}
|
||||
|
||||
.playing-wave {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
height: 38rpx;
|
||||
}
|
||||
|
||||
.p-bar {
|
||||
width: 6rpx;
|
||||
background-color: var(--brand-blue);
|
||||
border-radius: 3rpx;
|
||||
animation: barBounce 0.6s ease-in-out infinite alternate;
|
||||
}
|
||||
.p1{height:16rpx;animation-delay:0s} .p2{height:28rpx;animation-delay:.1s}
|
||||
.p3{height:38rpx;animation-delay:.2s} .p4{height:26rpx;animation-delay:.3s}
|
||||
.p5{height:18rpx;animation-delay:.4s}
|
||||
|
||||
@keyframes barBounce {
|
||||
from{transform:scaleY(.4)} to{transform:scaleY(1)}
|
||||
}
|
||||
|
||||
.playing-text {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-dark-secondary);
|
||||
}
|
||||
Reference in New Issue
Block a user