Files
xk-client-wx/subPackages/chat/chat.vue

1290 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="chat-container">
<!-- 顶部导航栏 -->
<view class="chat-header" @click="toggleDoctorDetail">
<view class="header-center">
<view class="doctor-info">
<text class="doctor-name">{{ doctorUserInfo.nick_name }}</text>
<text class="doctor-status">在线 · 心血管内科</text>
</view>
<u-icon :name="showDoctorDetail?'arrow-up':'arrow-down'" size="16" color="#298dff"></u-icon>
</view>
<view class="header-right">
<u-icon name="phone" size="24" color="#298dff" @click.stop="startCall('audio')"></u-icon>
<u-icon name="videocam" size="24" color="#298dff" @click.stop="startCall('video')"></u-icon>
</view>
<!-- 医生详情信息 -->
<view v-if="showDoctorDetail" class="doctor-detail-panel">
<view class="detail-item">
<text class="detail-label">职称</text>
<text>主任医师</text>
</view>
<view class="detail-item">
<text class="detail-label">医院</text>
<text>北京协和医院</text>
</view>
<view class="detail-item">
<text class="detail-label">擅长</text>
<text>冠心病高血压心力衰竭</text>
</view>
</view>
</view>
<!-- 聊天区域 -->
<scroll-view
class="chat-messages"
scroll-y="true"
:scroll-top="scrollTop"
scroll-with-animation
@scrolltoupper="loadHistory"
>
<view class="message-list">
<view v-for="(msg, index) in messages" :key="index" class="message-item">
<!-- 对方消息 -->
<view v-if="msg.sender !== currentUserId" class="other-message">
<image class="avatar" :src="doctorAvatar"></image>
<view class="message-content-wrapper">
<view class="message-content" :class="msg.type">
<image
v-if="msg.type === 'image'"
:src="msg.content"
mode="widthFix"
@click="previewImage(msg.content)"
></image>
<video
v-else-if="msg.type === 'video'"
:src="msg.content"
controls
class="video-player"
></video>
<view v-else-if="msg.type === 'audio'" class="audio-message" @click="playAudio(msg)">
<u-icon name="sound" size="20" color="#298dff"></u-icon>
<text class="duration">{{ msg.duration }}''</text>
<view class="wave-bars">
<view
v-for="(bar, i) in 5"
:key="i"
class="wave-bar"
:class="{'wave-bar-active': isPlaying(msg)}"
></view>
</view>
</view>
<text v-else>{{ msg.content }}</text>
</view>
<text class="message-time">{{ msg.time }}</text>
</view>
</view>
<!-- 我的消息 -->
<view v-else class="my-message">
<image class="avatar" :src="userAvatar"></image>
<view class="message-content-wrapper">
<view class="message-content" :class="msg.type">
<image
v-if="msg.type === 'image'"
:src="msg.content"
mode="widthFix"
@click="previewImage(msg.content)"
></image>
<video
v-else-if="msg.type === 'video'"
:src="msg.content"
controls
class="video-player"
></video>
<view v-else-if="msg.type === 'audio'" class="audio-message" @click="playAudio(msg)">
<view class="wave-bars">
<view
v-for="(bar, i) in 5"
:key="i"
class="wave-bar"
:class="{'wave-bar-active': isPlaying(msg)}"
></view>
</view>
<text class="duration">{{ msg.duration }}''</text>
<u-icon name="sound" size="20" color="#298dff"></u-icon>
</view>
<text v-else>{{ msg.content }}</text>
</view>
<text class="message-time">{{ msg.time }}</text>
</view>
</view>
</view>
</view>
<!-- 加载指示器 -->
<view v-if="loading" class="loading-indicator">
<u-loading mode="circle" color="#298dff"></u-loading>
</view>
</scroll-view>
<!-- 输入区域 -->
<view class="chat-input">
<!-- 工具按钮行 - 单独一行 -->
<view class="toolbar-row">
<!-- <view class="tool-btn" @click="toggleVoiceInput">-->
<!-- <u-icon :name="voiceInputActive ? 'mic-off' : 'mic'" size="24" color="#298dff"></u-icon>-->
<!-- <text>{{ voiceInputActive ? '键盘' : '语音' }}</text>-->
<!-- </view>-->
<view class="tool-btn" @click="openMediaPicker('image')">
<u-icon name="photo" size="24" color="#298dff"></u-icon>
<text>图片</text>
</view>
<view class="tool-btn" @click="openCamera">
<u-icon name="camera" size="24" color="#298dff"></u-icon>
<text>拍照</text>
</view>
<!-- <view class="tool-btn" @click="openMediaPicker('video')">-->
<!-- <u-icon name="videocam" size="24" color="#298dff"></u-icon>-->
<!-- <text>视频</text>-->
<!-- </view>-->
<view class="tool-btn" @click="sendFile">
<u-icon name="file-text" size="24" color="#298dff"></u-icon>
<text>文件</text>
</view>
<view class="tool-btn" @click="sendPrescription">
<u-icon name="file-medical" size="24" color="#298dff"></u-icon>
<text>处方</text>
</view>
</view>
<!-- 输入框行 -->
<view class="input-row">
<!-- 更多操作按钮 -->
<!-- <view class="action-btn" @click="showMoreActions = !showMoreActions">-->
<!-- <u-icon :name="showMoreActions ? 'close-circle' : 'plus-circle'" size="24" color="#298dff"></u-icon>-->
<!-- </view>-->
<view class="action-btn" @click="toggleVoiceInput">
<u-icon :name="voiceInputActive ? 'mic-off' : 'mic'" size="24" color="#298dff"></u-icon>
<!-- <text>{{ voiceInputActive ? '键盘' : '语音' }}</text>-->
</view>
<!-- 文本输入 -->
<input
v-if="!voiceInputActive"
v-model="newMessage"
class="message-input"
placeholder="输入消息..."
@confirm="sendTextMessage"
:adjust-position="false"
placeholder-style="color: #aaa;"
/>
<!-- 语音输入 -->
<view v-else class="voice-input"
@touchstart="startVoiceRecording"
@touchend="stopVoiceRecording"
@touchcancel="stopVoiceRecording">
<u-icon name="mic" size="24" color="#fff"></u-icon>
<text>{{ recording ? '松开结束' : '按住说话' }}</text>
<view v-if="recording" class="recording-indicator">
<view class="pulse"></view>
<text>{{ recordingTime }}s</text>
</view>
</view>
<!-- 发送按钮 -->
<button
v-if="newMessage.trim() && !voiceInputActive"
class="send-btn"
@click="sendTextMessage"
>
<u-icon name="arrow-up" size="20" color="#fff"></u-icon>
</button>
</view>
</view>
<!-- 录音提示 -->
<view v-if="showRecordingModal" class="recording-modal">
<view class="recording-content">
<view class="recording-icon">
<u-icon name="mic" size="100" style="margin-top: 20rpx;" color="#fff"></u-icon>
<view class="pulse-animation"></view>
</view>
<text class="recording-text">录音中... {{ recordingTime }}</text>
<text class="recording-tip">上划取消发送</text>
</view>
</view>
<!-- 音频播放器组件 -->
<audio-player ref="audioPlayer"
@play="handleAudioPlay"
@pause="handleAudioPause"
@ended="handleAudioEnd" />
</view>
</template>
<script>
import { webSocketManager } from '@/utils/ws/websocket';
import { ChatManager } from '@/store/chat/chat.js';
import {sendToUserApi} from "@/request/api/im";
import AudioPlayer from './components/AudioPlayer.vue';
export default {
components: {
AudioPlayer
},
data() {
return {
doctorUserInfo: {
id: 0,
avatar: '',
nick_name: '医生',
room_id: ''
},
showDoctorDetail: false, // 控制医生详情展开
currentUserId: '',
doctorId: 'doctor-1',
doctorAvatar: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250108/29bdb9560340373bb3096394a845afa5.jpg',
userAvatar: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250108/29bdb9560340373bb3096394a845afa5.jpg',
messages: [],
newMessage: '',
voiceInputActive: false,
showMoreActions: false,
recording: false,
recordingTime: 0,
recordingTimer: null,
showRecordingModal: false,
currentPlayingAudio: null,
scrollTop: 0,
page: 1,
pageSize: 20,
hasMore: true,
loading: false, // 新增加载状态
};
},
onLoad: function (option) {
this.doctorUserInfo = option;
if (option.avatar) {
this.doctorAvatar = option.avatar;
}
this.currentUserId = 'user-' + uni.getStorageSync('user_id') || 'user-2512';
this.doctorId = option.user_id;
// 进入聊天室时缓存房间ID
ChatManager.enterRoom(option.room_id);
// 重置当前房间的未读消息
ChatManager.resetUnreadCount(option.room_id);
// 从ChatManager获取房间消息
this.messages = ChatManager.getRoomMessages(option.room_id);
// 如果没有消息,添加示例消息
if (this.messages.length === 0) {
this.addExampleMessages();
}
uni.setNavigationBarTitle({
title: `${this.doctorUserInfo.nick_name}医生的对话`,
success: () => {
console.log('修改标题成功');
},
fail: () => {
console.log('修改标题失败');
},
complete: () => {
console.log('修改标题结束');
}
});
// 设置滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
},
onUnload() {
// 离开聊天室时清除房间缓存
ChatManager.leaveRoom();
// 停止音频播放
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.stop();
}
},
onShow() {
// 监听当前房间的新消息事件
uni.$on('new-room-message', this.handleNewRoomMessage);
},
onHide() {
// 移除消息监听
uni.$off('new-room-message', this.handleNewRoomMessage);
},
mounted() {
// 初始化录音管理器
this.recorderManager = uni.getRecorderManager();
this.initRecorder();
// 监听WebSocket消息
webSocketManager.addMessageHandler(this.handleSocketMessage);
},
beforeDestroy() {
// 移除WebSocket监听
webSocketManager.removeMessageHandler(this.handleSocketMessage);
},
methods: {
// 添加示例消息
addExampleMessages() {
const exampleMessages = [
{
id: '1',
sender: 'doctor-1',
type: 'text',
content: '您好,请问有什么可以帮您的?',
time: '09:30',
timestamp: Date.now() - 3600000
},
{
id: '2',
sender: 'user-252',
type: 'text',
content: '我这两天有点发烧,还有咳嗽。',
time: '09:31',
timestamp: Date.now() - 3540000
},
{
id: '3',
sender: 'doctor-1',
type: 'text',
content: '您有测量体温吗?最近有没有接触过感冒的人?',
time: '09:32',
timestamp: Date.now() - 3480000
},
{
id: '4',
sender: 'user-252',
type: 'image',
content: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250108/29bdb9560340373bb3096394a845afa5.jpg',
time: '09:33',
timestamp: Date.now() - 3420000
},
{
id: '5',
sender: 'doctor-1',
type: 'video',
content: 'https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/byted-player-videos/1.0.0/xgplayer-demo-720p.mp4',
time: '09:35',
timestamp: Date.now() - 3300000
},
{
id: '6',
sender: 'user-252',
type: 'audio',
content: 'https://er-sycdn.kuwo.cn/f7c3be4ff67cc7b135e38e8365479e06/687d941e/resource/30106/trackmedia/M800004emQMs09Z1lz.mp3',
duration: 15,
time: '09:36',
timestamp: Date.now() - 3240000
},
{
id: '7',
sender: 'doctor-1',
type: 'audio',
content: 'https://er-sycdn.kuwo.cn/f7c3be4ff67cc7b135e38e8365479e06/687d941e/resource/30106/trackmedia/M800004emQMs09Z1lz.mp3',
duration: 8,
time: '09:37',
timestamp: Date.now() - 3180000
}
];
// 添加到ChatManager和本地消息列表
exampleMessages.forEach(msg => {
ChatManager.addMessageToRoom(this.doctorUserInfo.room_id, msg);
});
this.messages = [...exampleMessages];
},
// 处理WebSocket消息
handleSocketMessage(data) {
// 将消息交给ChatManager统一处理
ChatManager.handleIncomingMessage(data);
},
// 处理当前房间的新消息事件
handleNewRoomMessage(message) {
// 处理不同类型的消息
const msgType = this.getMessageType(message.message_type);
const newMsg = {
id: message.id,
sender: message.sender_user_id,
type: msgType,
content: message.content,
time: this.formatTime(message.send_time),
timestamp: message.send_time
};
// 对于音频和视频消息,可能有额外字段
if (msgType === 'audio' || msgType === 'video') {
newMsg.duration = message.duration || 0;
}
// 添加到消息列表
this.addMessage(newMsg);
},
// 根据消息类型ID获取类型名称
getMessageType(typeId) {
const types = {
0: 'text',
1: 'image',
2: 'audio',
3: 'video',
4: 'file',
5: 'prescription'
};
return types[typeId] || 'text';
},
// 添加消息到列表
addMessage(message) {
// 避免重复添加
if (!this.messages.some(m => m.id === message.id)) {
this.messages.push(message);
this.scrollToBottom();
}
},
// 格式化时间
formatTime(timestamp) {
let date;
// 处理不同输入类型
if (typeof timestamp === 'number') {
// 数字直接作为时间戳处理
date = new Date(timestamp);
} else if (timestamp instanceof Date) {
// Date对象直接使用
date = timestamp;
} else if (typeof timestamp === 'string') {
// 字符串类型:尝试转换为时间戳
const parsed = Date.parse(timestamp);
if (isNaN(parsed)) {
// 转换失败则使用当前时间
date = new Date();
} else {
date = new Date(parsed);
}
} else {
// 其他类型使用当前时间
date = new Date();
}
// 格式化时间为 HH:MM
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
},
// 切换医生详情显示
toggleDoctorDetail() {
this.showDoctorDetail = !this.showDoctorDetail;
},
// 初始化录音
initRecorder() {
this.recorderManager.onStop(res => {
if (res.duration < 1000) {
uni.showToast({ title: '录音时间太短', icon: 'none' });
this.recording = false;
this.showRecordingModal = false;
clearInterval(this.recordingTimer);
return;
}
this.sendMessage({
type: 'audio',
content: 'https://er-sycdn.kuwo.cn/f7c3be4ff67cc7b135e38e8365479e06/687d941e/resource/30106/trackmedia/M800004emQMs09Z1lz.mp3',
duration: Math.floor(res.duration / 1000),
});
this.recording = false;
this.showRecordingModal = false;
clearInterval(this.recordingTimer);
});
this.recorderManager.onError((error) => {
console.error('录音错误:', error);
uni.showToast({ title: '录音失败,请重试', icon: 'none' });
this.recording = false;
this.showRecordingModal = false;
clearInterval(this.recordingTimer);
});
},
// 发送消息
sendMessage(message) {
const now = new Date();
const timeStr = this.formatTime(now);
// 构建完整消息对象
const fullMessage = {
...message,
id: Date.now().toString(),
sender: this.currentUserId,
receiver: this.doctorId,
room_id: this.doctorUserInfo.room_id,
time: timeStr,
timestamp: now.getTime()
};
// 添加到ChatManager
ChatManager.addMessageToRoom(this.doctorUserInfo.room_id, fullMessage);
// 构建请求数据
const messageTypeMap = {
text: 0,
image: 1,
audio: 2,
video: 3,
file: 4,
prescription: 5
};
const requestData = {
room_id: this.doctorUserInfo.room_id,
sender_user_id: this.currentUserId,
receiver_user_id: this.doctorId,
message_type: messageTypeMap[message.type] || 0,
message_content: message.content || '',
isFromHttp: true,
duration: message.duration || 0
};
sendToUserApi(requestData).then(res => {
console.log('发送成功:', res);
// 添加到本地消息列表
this.messages.push(fullMessage);
this.scrollToBottom();
}).catch(error => {
console.error('发送失败:', error);
});
},
// 发送文本消息
sendTextMessage() {
if (!this.newMessage.trim()) return;
this.sendMessage({
type: 'text',
content: this.newMessage
});
this.newMessage = '';
},
// 开始录音
startVoiceRecording() {
if (this.recording) return;
uni.authorize({
scope: 'scope.record',
success: () => {
this.startRecording();
},
fail: (e) => {
console.log('失败', e)
uni.showToast({
title: '需要录音权限',
icon: 'none'
});
}
});
},
startRecording() {
this.recording = true;
this.showRecordingModal = true;
this.recordingTime = 0;
this.recorderManager.start({
format: 'mp3',
sampleRate: 44100,
numberOfChannels: 1,
encodeBitRate: 192000
});
this.recordingTimer = setInterval(() => {
this.recordingTime++;
if (this.recordingTime >= 60) {
this.stopVoiceRecording();
}
}, 1000);
},
// 停止录音
stopVoiceRecording() {
if (!this.recording) return;
this.recorderManager.stop();
},
// 播放音频
playAudio(msg) {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.play({
id: msg.id,
src: msg.content
});
}
},
// 检查是否正在播放
isPlaying(msg) {
return this.$refs.audioPlayer && this.$refs.audioPlayer.isPlaying(msg.id);
},
// 音频播放事件
handleAudioPlay(audioId) {
// 更新UI
this.currentPlayingAudio = audioId;
},
handleAudioPause() {
this.currentPlayingAudio = null;
},
handleAudioEnd() {
this.currentPlayingAudio = null;
},
// 打开媒体选择器
openMediaPicker(type) {
this.showMoreActions = false;
if (type === 'image') {
uni.chooseImage({
count: 9,
sourceType: ['album', 'camera'],
success: res => {
res.tempFilePaths.forEach(path => {
this.sendMessage({
type: 'image',
content: path
});
});
}
});
} else if (type === 'video') {
uni.chooseVideo({
sourceType: ['album', 'camera'],
compressed: true,
maxDuration: 60,
success: res => {
this.sendMessage({
type: 'video',
content: res.tempFilePath,
duration: Math.floor(res.duration)
});
}
});
}
},
// 打开相机拍照
openCamera() {
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: res => {
this.sendMessage({
type: 'image',
content: res.tempFilePaths[0]
});
}
});
},
// 发送文件
sendFile() {
uni.chooseMessageFile({
count: 1,
type: 'all',
success: res => {
const file = res.tempFiles[0];
this.sendMessage({
type: 'file',
content: file.path,
fileName: file.name,
fileSize: file.size
});
}
});
},
// 发送处方
sendPrescription() {
// 模拟处方数据
const prescription = {
id: 'RX20230718001',
date: new Date().toLocaleDateString(),
items: [
{ name: '阿司匹林', dosage: '100mg', frequency: '每日一次' },
{ name: '维生素C', dosage: '500mg', frequency: '每日三次' }
]
};
this.sendMessage({
type: 'prescription',
content: JSON.stringify(prescription)
});
},
// 预览图片
previewImage(url) {
uni.previewImage({
urls: [url],
current: url
});
},
// 开始通话
startCall(type) {
uni.showToast({
title: `发起${type === 'audio' ? '语音' : '视频'}通话`,
icon: 'none'
});
},
// 切换语音输入
toggleVoiceInput() {
this.voiceInputActive = !this.voiceInputActive;
},
// 滚动到底部
scrollToBottom() {
this.$nextTick(() => {
this.scrollTop = Date.now();
});
},
// 加载历史消息
async loadHistory() {
if (!this.hasMore || this.loading) return;
this.loading = true;
try {
// 模拟加载历史消息
const newMessages = await new Promise(resolve => {
setTimeout(() => {
resolve([
{
id: 'h1',
sender: 'doctor-1',
type: 'text',
content: '这是更早的消息',
time: '09:20',
timestamp: Date.now() - 7200000
},
{
id: 'h2',
sender: 'user-252',
type: 'text',
content: '是的,这是历史消息',
time: '09:21',
timestamp: Date.now() - 7140000
}
]);
}, 500);
});
this.messages = [...newMessages, ...this.messages];
this.hasMore = false; // 假设没有更多历史消息
} finally {
this.loading = false;
}
}
}
};
</script>
<style lang="scss">
$primary-color: #298dff;
$doctor-bg: #e6f2ff;
$patient-bg: #e6f9ff;
$light-bg: #f5f9ff;
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: $light-bg;
overflow: hidden;
font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
}
/* 优化后的顶部导航栏 */
.chat-header {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: #fff;
border-bottom: 1px solid #eaeef5;
box-shadow: 0 2px 10px rgba(41, 141, 255, 0.1);
z-index: 20;
.header-center {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 40px;
.doctor-info {
display: flex;
flex-direction: column;
align-items: center;
}
.doctor-name {
font-size: 18px;
font-weight: bold;
color: #333;
}
.doctor-status {
font-size: 12px;
color: $primary-color;
}
}
.header-right {
display: flex;
justify-content: flex-end;
gap: 16px;
}
.doctor-detail-panel {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: #fff;
padding: 16px;
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
z-index: 10;
animation: slideDown 0.3s ease-out;
.detail-item {
padding: 8px 0;
font-size: 14px;
display: flex;
.detail-label {
font-weight: bold;
color: #666;
width: 60px;
}
}
}
}
@keyframes slideDown {
from {
transform: translateY(-10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.chat-messages {
flex: 1;
padding: 16px;
padding-bottom: 120px;
overflow: hidden;
background-color: #f5f9ff;
}
.message-list {
display: flex;
flex-direction: column;
}
.message-item {
margin-bottom: 24px;
transition: all 0.3s ease;
animation: fadeIn 0.4s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.other-message, .my-message {
display: flex;
align-items: flex-start;
margin-bottom: 16px;
margin-right: 50rpx;
.avatar {
width: 44px;
height: 44px;
border-radius: 50%;
flex-shrink: 0;
border: 1px solid #eaeef5;
background-color: #fff;
}
.message-content-wrapper {
max-width: 75%;
margin: 0 12px;
}
.message-content {
padding: 14px 18px;
border-radius: 18px;
position: relative;
font-size: 16px;
line-height: 1.5;
word-break: break-word;
box-shadow: 0 2px 8px rgba(41, 141, 255, 0.1);
transition: all 0.3s ease;
&.text {
background-color: #fff;
border: 1px solid #eaeef5;
}
&.image, &.video {
padding: 0;
background: transparent;
border-radius: 10px;
overflow: hidden;
border: 1px solid #eaeef5;
}
&.audio {
padding: 12px 16px;
background-color: #f0f7ff;
border: 1px solid #d0e6ff;
}
&.prescription, &.file {
background-color: #e6f2ff;
border: 1px solid #cce0ff;
}
}
.message-time {
font-size: 12px;
color: #888;
margin-top: 6px;
font-weight: 300;
}
}
.other-message {
.message-content {
background-color: $doctor-bg;
border-top-left-radius: 4px;
border: 1px solid #d0e6ff;
&:before {
content: '';
position: absolute;
left: -8px;
top: 14px;
width: 0;
height: 0;
border-top: 6px solid transparent;
border-right: 10px solid $doctor-bg;
border-bottom: 6px solid transparent;
}
}
}
.my-message {
flex-direction: row-reverse;
.message-content {
background-color: $patient-bg;
border-top-right-radius: 4px;
border: 1px solid #cce6ff;
&:after {
content: '';
position: absolute;
right: -8px;
top: 14px;
width: 0;
height: 0;
border-top: 6px solid transparent;
border-left: 10px solid $patient-bg;
border-bottom: 6px solid transparent;
}
}
}
.audio-message {
display: flex;
align-items: center;
.wave-bars {
display: flex;
align-items: center;
height: 28px;
margin: 0 10px;
.wave-bar {
width: 4px;
background-color: #a0c8ff;
margin: 0 2px;
border-radius: 2px;
height: 10px;
transition: height 0.3s ease;
&.wave-bar-active {
animation: wave 1s infinite;
height: 24px;
&:nth-child(1) { animation-delay: 0.1s; }
&:nth-child(2) { animation-delay: 0.2s; }
&:nth-child(3) { animation-delay: 0.3s; }
&:nth-child(4) { animation-delay: 0.4s; }
&:nth-child(5) { animation-delay: 0.5s; }
}
}
}
.duration {
font-size: 14px;
color: #298dff;
font-weight: 500;
}
}
@keyframes wave {
0%, 100% { height: 10px; }
50% { height: 24px; }
}
.video-player {
width: 260px;
height: 160px;
border-radius: 10px;
overflow: hidden;
}
.chat-input {
background-color: #fff;
border-top: 1px solid #eaeef5;
padding: 10px 16px;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
box-shadow: 0 -4px 12px rgba(41, 141, 255, 0.1);
}
/* 工具按钮行 */
.toolbar-row {
display: flex;
justify-content: space-around;
padding: 8px 0;
margin-bottom: 8px;
border-bottom: 1px solid #f0f7ff;
}
.tool-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 4px 8px;
border-radius: 8px;
transition: all 0.3s ease;
&:active {
background-color: #f0f7ff;
}
text {
font-size: 12px;
color: #555;
margin-top: 4px;
font-weight: 500;
}
}
/* 输入框行 */
.input-row {
display: flex;
align-items: center;
gap: 10px;
}
.action-btn {
width: 46px;
height: 46px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f7ff;
transition: all 0.3s ease;
&:active {
background-color: #d0e6ff;
transform: scale(0.95);
}
}
.message-input {
flex: 1;
height: 50px;
padding: 0 18px;
background-color: #f0f7ff;
border-radius: 25px;
font-size: 16px;
color: #333;
border: 1px solid #d0e6ff;
transition: all 0.3s ease;
&:focus {
border-color: $primary-color;
box-shadow: 0 0 0 2px rgba(41, 141, 255, 0.2);
}
}
.voice-input {
flex: 1;
height: 50px;
background: linear-gradient(135deg, #298dff, #3a9eff);
border-radius: 25px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 16px;
position: relative;
font-weight: 500;
text {
margin-left: 8px;
}
.recording-indicator {
position: absolute;
right: 16px;
display: flex;
align-items: center;
.pulse {
width: 10px;
height: 10px;
background-color: #ff4d4f;
border-radius: 50%;
margin-right: 6px;
animation: pulse 1.5s infinite;
}
text {
font-size: 14px;
color: #fff;
margin-left: 0;
}
}
}
.send-btn {
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, #298dff, #3a9eff);
display: flex;
align-items: center;
justify-content: center;
border: none;
box-shadow: 0 4px 10px rgba(41, 141, 255, 0.3);
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
box-shadow: 0 2px 6px rgba(41, 141, 255, 0.3);
}
}
.recording-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
.recording-content {
background-color: rgba(41, 141, 255, 0.9);
padding: 30px 40px;
border-radius: 20px;
text-align: center;
width: 280px;
//box-shadow: 0 10px 30px rgba(41, 141, 255, 0.4);
}
.recording-icon {
position: relative;
width: 80px;
height: 80px;
margin: 0 auto 20px;
.u-icon {
position: relative;
z-index: 2;
color: #fff;
}
}
.pulse-animation {
position: absolute;
top: 0;
left: 0;
width: 80px;
height: 80px;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 50%;
animation: pulse 1.5s infinite;
z-index: 1;
}
.recording-text {
color: white;
font-size: 18px;
display: block;
margin-bottom: 10px;
font-weight: 500;
}
.recording-tip {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
}
}
@keyframes pulse {
0% { transform: scale(0.9); opacity: 0.7; }
50% { transform: scale(1.1); opacity: 1; }
100% { transform: scale(0.9); opacity: 0.7; }
}
/* 加载指示器 */
.loading-indicator {
display: flex;
justify-content: center;
padding: 20px;
}
</style>