增量提交

This commit is contained in:
zqm
2026-04-21 13:46:20 +08:00
parent f64209cb04
commit 09eb6fb1bd
44 changed files with 4411 additions and 931 deletions

View 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])
}
})
}
}
})

View File

@@ -0,0 +1 @@
{"component": true, "usingComponents": {}}

View 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>

View 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);
}