1.实现语音输入(语音识别)
2.实现文字输出时语音播放
3.实现语音播放的开始,暂停逻辑,语音播放时AI使用gif图
4.实现对话问答模式
5.实现加载历史记录,每次加载3条,滑动到顶端每次再加载3条
6.实现每次输出显示最新条消息功能
代码实现:
只保留框架,具体业务已移除
<template>
<view class="container">
<view class="chat-container">
<view class="header">
<view class="chat-black">
<view class="black">
<image class="black-img" src="/src/static/images/ai/black.png" @click="handleBack"></image>
</view>
<!-- <view class="more">
<image class="more-img" src="/src/static/images/ai/more.png"></image>
</view> -->
</view>
<view class="chat-title">
<view class="chat-header">
<view class="title">{{ title }}</view>
</view>
<view class="chat-text">
<view>
我是小i,任何问题都可以找我咨询哦,快来聊聊吧!
</view>
</view>
</view>
</view>
<!-- 聊天消息区域 -->
<view class="messages">
<scroll-view ref="chatScroll" class="chat-area" scroll-y="true" :scroll-top="scrollTop"
scroll-with-animation @scrolltoupper="handleScrolltoupper">
<!-- 添加"加载更多"按钮 -->
<view v-if="hasMore" class="load-more-container" @click="loadMoreMessages">
<view class="load-more-btn">
<text>点击加载更多历史消息</text>
</view>
</view>
<!-- 原有的加载提示 -->
<view v-if="loadingMore" class="loading-more">
正在加载历史消息...
</view>
<!-- 消息列表 -->
<view v-for="(item, index) in messageList" :key="index" class="message-item" :id="item.id"
:class="item.type === 'user' ? 'user-message' : 'ai-message'">
<!-- 修改AI消息的头像显示 -->
<image v-if="item.type === 'ai' && (isPlaying && currentPlayId === item.id)" class="avatar"
src="/src/static/images/ai/AI.gif"></image>
<image v-if="item.type === 'ai' && (!isPlaying || currentPlayId !== item.id)" class="avatar"
src="/src/static/images/ai/AI.png"></image>
<!-- 处理loading状态 -->
<div v-if="item?.status === 'loading'" class="message-content">
<span>正在处理中 </span>
<image style="width: 65rpx;height: 26rpx;" src="/src/static/images/ai/wait.gif">
</image>
</div>
<!-- 处理错误状态 -->
<div v-else-if="item?.status === 'error'" class="message-content">
<span>{{ item.content }}</span>
</div>
<!-- 处理正常消息 -->
<view v-else class="message-content">
{{ item.content }}
<view class="playing" v-if="item.type === 'ai' && item?.status != 'loading'"
@click="togglePlay(item)">
<image style="width: 26rpx;height: 26rpx;" v-if="isPlaying && currentPlayId === item.id"
src="/src/static/images/ai/stop.png">
</image>
<image style="width: 26rpx;height: 26rpx;" v-else
src="/src/static/images/ai/startPlaying.png">
</image>
<image style="width: 68rpx;height: 30rpx;object-fit: cover;"
v-if="isPlaying && currentPlayId === item.id"
src="/src/static/images/ai/voicePlayback.gif"></image>
<image style="width: 48rpx;height: 30rpx;margin-left: 10rpx;" v-else
src="/src/static/images/ai/stopPlaying.png"></image>
</view>
</view>
<image v-if="item.type === 'user'" class="avatar" src="/src/static/images/ai/user.png"></image>
</view>
</scroll-view>
</view>
<!-- 文本输入区域 -->
<view class="text-input-container" v-if="inputMode === 'text'">
<view class="switchingIcon">
<image class="mode-switch-btn" :src="'/static/images/ai/voice.png'" @click="inputMode = 'voice'"
mode="aspectFit" />
</view>
<input v-model="inputText" placeholder="输入消息..." @confirm="sendTextMessage" adjust-position="true" />
<!-- <button @click="sendTextMessage">发送</button> -->
<view>
<image class="send" :src="'/static/images/ai/send.png'" @click="sendTextMessage" mode="aspectFit" />
</view>
</view>
</view>
<!-- 语音输入区域 -->
<view class="voice-input-container" v-if="inputMode === 'voice'">
<view class="voice-preview" v-if="isRecording" :class="{ canceling: isCanceling }">
<view class="voice-text-preview">
{{ inputText || '正在聆听...' }}
</view>
<view class="voice-controls">
<text :style="{ color: isCanceling ? '#e43d33' : '#666' }">
{{ isCanceling ? '' : '松开发送' }}
</text>
<view class="slide-hint" v-show="!isCanceling">
<text>↑ 上移取消</text>
</view>
<view class="cancel-hint" v-show="isCanceling">
<text style="color: #e43d33; ">松开手指 取消发送</text>
</view>
</view>
</view>
<view style="display: flex;">
<view class="switchingIcon">
<image class="mode-switch-btn" :src="'/static/images/ai/keyboard.png'" @click="inputMode = 'text'"
mode="aspectFit" />
</view>
<!-- <button class="voice-btn" @touchstart="startVoiceInput" @touchend="endVoiceInput"
@touchmove="handleTouchMove">
{{ isRecording ? '松开结束' : '按住说话' }}
</button> -->
<wd-button class="voice-btn" @touchstart="startVoiceInput" @touchend="endVoiceInput"
@touchmove="handleTouchMove">
<image class="voice-btn-image" v-if="isRecording" :src="this.voiceInput" mode="aspectFit">
</image>
<span v-else>按住说话</span>
</wd-button>
</view>
</view>
<yue-asr-xf ref="yueAsrRefs" :options="optionsxf" @countDown="countDown" @result="resultMsg" @onStop="onStop"
@onOpen="onOpen" @change="change"></yue-asr-xf>
</view>
</template>
<script>
import { useConfigStore } from '@/store/config';
export default {
data() {
const second = 60;
return {
title: 'Hi,您好呀~',
msg: '转文字',
messageList: [], // 聊天消息列表
allMessages: [], // 存储所有消息
currentPage: 1, // 当前页码
pageSize: 3, // 每页显示条数
hasMore: false, // 是否还有更多数据
loadingMore: false, // 是否正在加载更多
maxDisplayCount: 3, // 初始显示消息数
lastMsgId: '', // 最后一条消息ID
loadedMessageCount: 0, // 已加载的消息数量
inputText: '', // 输入框文本
aiAvatar: '/src/static/images/ai/AI.png', // AI头像(默认静态)
aiAvatarPlaying: '/src/static/images/ai/AI.gif', // AI播放时的动态头像
userAvatar: '/src/static/images/ai/user.png', // 用户头像
voiceInput: '/src/static/images/ai/voiceInput.gif',
inputMode: 'voice', // 输入模式:voice/text
isRecording: false, // 是否正在录音
isCanceling: false, // 是否正在取消
optionsxf: {
receordingDuration: second,
APPID: '123456', // 请替换为实际值
API_SECRET: '', // 请替换为实际值
API_KEY: '' // 请替换为实际值
},
downtime: -1, // 默认-1
downed: false,
disabled: false,
second,
scrollTop: 0,
oldScrollTop: 0, // 添加这个数据用于强制刷新滚动
isPlaying: false, // 播放状态
currentPlayId: null, // 当前播放的消息ID
playTimer: null, // 播放定时器
audioContext: null, // 音频上下文
};
},
onLoad() {
// #ifdef APP
plus.android.requestPermissions([
"android.permission.RECORD_AUDIO"
], (e) => { }, (e) => { })
// #endif
const configStore = useConfigStore()
this.FIsFlagTeaching = configStore.FIsFlagTeaching
console.log('this.FIsFlagTeaching', this.FIsFlagTeaching);
// this.serverProcduceCall()
// 初始化音频上下文
// 直接创建音频上下文,不经过 Vue 的响应式系统
const audioContext = uni.createInnerAudioContext();
// 安全地设置 obeyMuteSwitch 属性
try {
audioContext.obeyMuteSwitch = false;
} catch (e) {
console.warn('无法设置 obeyMuteSwitch 属性:', e);
}
// 将 audioContext 挂载到实例上
this.audioContext = audioContext;
// 添加事件监听器
this.audioContext.onEnded(() => {
// 音频播放结束时的处理
this.isPlaying = false;
this.currentPlayId = null;
if (this.playTimer) {
clearTimeout(this.playTimer);
this.playTimer = null;
}
});
this.audioContext.onError((res) => {
console.error('音频播放错误:', res.errMsg);
this.isPlaying = false;
this.currentPlayId = null;
if (this.playTimer) {
clearTimeout(this.playTimer);
this.playTimer = null;
}
});
},
onShow() {
this.getReplyLogList()// 查询历史记录
},
methods: {
resumeUi() {
this.downed = false;
this.downtime = -1;
this.disabled = false;
this.downtime = this.second;
},
start() {
if (this.disabled) {
return;
}
console.log("开始")
this.downed = true;
this.$refs.yueAsrRefs.start();
this.disabled = true;
},
end() {
console.log("结束")
this.$refs.yueAsrRefs.end();
},
countDown(e) {
console.log('countDown', e);
this.downtime = e;
},
onOpen(e) {
console.log('onOpen', e);
},
change(e) {
console.log('change', e);
},
resultMsg(e) {
this.inputText = e;
console.log('resultMsg', e);
},
onStop(e) {
console.log('onStop', e);
this.resumeUi();
if (this.inputText.trim()) {
this.sendTextMessage();
}
},
// 用户输入消息处理
sendTextMessage() {
if (!this.inputText.trim()) return;
const userMsg = {
type: 'user',
content: this.inputText,
id: 'msg_' + Date.now()
};
this.messageList.push(userMsg);
this.lastMsgId = userMsg.id;
this.scrollToBottom();
this.dialogue(this.inputText)
this.inputText = '';
},
// 自动播放消息
autoPlayMessage(audioUrl) {
// 检查参数
if (!audioUrl) {
console.warn('音频URL为空,无法播放');
return;
}
// 如果当前有音频正在播放,先停止
if (this.audioContext) {
this.audioContext.stop();
}
// 清除之前的定时器
if (this.playTimer) {
clearTimeout(this.playTimer);
this.playTimer = null;
}
try {
console.log('准备自动播放音频:', audioUrl);
if (this.audioContext) {
// 设置音频源
this.audioContext.src = audioUrl;
// 监听播放完成事件
const onEnded = () => {
// console.log('自动播放完成');
this.isPlaying = false;
this.currentPlayId = null;
if (this.playTimer) {
clearTimeout(this.playTimer);
this.playTimer = null;
}
// 移除事件监听器
this.audioContext.offEnded(onEnded);
};
// 监听播放错误事件
const onError = (res) => {
console.error('自动播放音频错误:', res.errMsg);
this.isPlaying = false;
this.currentPlayId = null;
if (this.playTimer) {
clearTimeout(this.playTimer);
this.playTimer = null;
}
// 移除事件监听器
this.audioContext.offError(onError);
};
// 添加事件监听器
this.audioContext.onEnded(onEnded);
this.audioContext.onError(onError);
// 开始播放
this.audioContext.play();
this.isPlaying = true;
console.log('音频自动播放已启动');
// 设置10秒后停止播放(作为保险机制)
// this.playTimer = setTimeout(() => {
// console.log('自动播放超时,停止播放');
// this.stopAudio();
// this.isPlaying = false;
// this.currentPlayId = null;
// this.playTimer = null;
// }, 10000);
}
} catch (error) {
console.error('播放音频失败:', error);
this.isPlaying = false;
this.currentPlayId = null;
if (this.playTimer) {
clearTimeout(this.playTimer);
this.playTimer = null;
}
}
},
// 手动切换播放状态
// 修改 togglePlay 方法,支持传入自定义音频地址
togglePlay(message) {
// 如果正在自动播放,清除定时器
if (this.playTimer) {
clearTimeout(this.playTimer);
this.playTimer = null;
}
if (this.currentPlayId === message.id && this.isPlaying) {
// 如果点击的是当前正在播放的消息,则暂停
this.pauseAudio();
this.isPlaying = false;
// 注意:这里不要将 currentPlayId 设为 null,以便知道是哪个消息被暂停
} else {
// 停止当前正在播放的音频(如果有的话)
if (this.audioContext) {
this.audioContext.stop();
}
// 使用消息对象中的audio字段作为音频地址
const audioUrl = message.FResponseAudioFile;
if (audioUrl) {
// 播放音频
this.playAudio(audioUrl);
this.isPlaying = true;
this.currentPlayId = message.id;
}
}
},
// 播放音频
playAudio(audioUrl) {
try {
// console.log('准备播放音频:', audioUrl);
if (this.audioContext) {
this.audioContext.src = audioUrl;
this.audioContext.play();
// 设置播放状态,这会自动切换头像
this.isPlaying = true;
// console.log('音频播放已启动');
// 添加播放完成的监听器
const onEnded = () => {
// console.log('音频播放完成');
this.isPlaying = false;
this.currentPlayId = null;
if (this.playTimer) {
clearTimeout(this.playTimer);
this.playTimer = null;
}
// 移除监听器避免内存泄漏
this.audioContext.offEnded(onEnded);
};
this.audioContext.onEnded(onEnded);
}
} catch (error) {
console.error('播放音频失败:', error);
this.isPlaying = false;
this.currentPlayId = null;
}
},
// 暂停音频
pauseAudio() {
try {
if (this.audioContext) {
this.audioContext.pause();
// 设置暂停状态,这会自动切换头像
this.isPlaying = false;
}
} catch (error) {
console.error('暂停音频失败:', error);
}
},
// 停止音频
stopAudio() {
try {
if (this.audioContext) {
this.audioContext.stop();
}
} catch (error) {
console.error('停止音频失败:', error);
}
// 重置状态,这会自动切换头像
this.isPlaying = false;
this.currentPlayId = null;
},
// 滚动到最下面
scrollToBottom() {
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this)
query.selectAll('.message-item').boundingClientRect(res => {
if (res && res.length > 0) {
const totalHeight = res.reduce((sum, cur) => sum + cur.height, 0)
this.scrollTop = Number(totalHeight + 999999)
}
}).exec()
})
},
// 停止录音
endVoiceInput() {
setTimeout(() => {
// 确保停止录音
this.$refs.yueAsrRefs.end();
if (this.isCanceling) {
this.inputText = '';
} else if (this.inputText.trim()) {
this.sendTextMessage();
}
// 重置所有状态
this.isRecording = false;
this.isCanceling = false;
this.startY = null;
}, 2000)
},
// 触摸移动距离判断
handleTouchMove(e) {
if (!this.isRecording) return;
const touch = e.touches[0];
if (!this.startY) {
this.startY = touch.pageY;
return;
}
const currentY = touch.pageY;
const moveDistance = this.startY - currentY;
// console.log('触摸移动距离:', moveDistance, 'startY:', this.startY, 'currentY:', currentY);
if (moveDistance > 50) {
if (!this.isCanceling) {
this.$set(this, 'isCanceling', true);
// console.log('触发取消状态');
}
} else if (this.isCanceling) {
this.$set(this, 'isCanceling', false);
// console.log('取消状态已重置');
}
},
startVoiceInput() {
this.startY = null; // 重置起始坐标
this.isRecording = true;
this.isCanceling = false;
this.$refs.yueAsrRefs.start();
},
focusInput() {
// #ifdef H5
this.$refs.input.focus();
// #endif
},
// 返回
handleBack() {
uni.navigateBack({})
},
// 接口部分
// 公共方法:显示loading消息
showLoadingMessage() {
const loadingMessage = {
id: 'loading_' + Date.now(), // 特殊ID便于识别
type: 'ai',
status: 'loading',
content: ''
};
this.messageList.push(loadingMessage);
// 添加滚动到底部的调用
this.$nextTick(() => {
this.scrollToBottom();
});
return loadingMessage.id; // 返回ID便于后续操作
},
// 公共方法:移除loading消息
removeLoadingMessage(loadingId) {
this.messageList = this.messageList.filter(msg => msg.id !== loadingId);
},
// 公共方法:更新loading为错误状态
updateLoadingToError(loadingId, errorMsg = '请求失败,请重试') {
// const loadingMsgIndex = this.messageList.findIndex(msg => msg.id === loadingId);
// if (loadingMsgIndex > -1) {
// this.messageList[loadingMsgIndex].status = 'error';
// this.messageList[loadingMsgIndex].content = errorMsg;
// }
const loadingMessage = {
id: 'loading_' + Date.now(), // 特殊ID便于识别
type: 'ai',
status: 'error',
content: errorMsg
};
this.messageList.push(loadingMessage);
// 滚动到最新消息
this.scrollToBottom();
},
// 公共方法:添加普通消息
addNormalMessage(content, type = 'ai', audioUrl = null, addlog) {
this.messageList.push({
id: Date.now(),
type,
status: 'completed',
content,
FResponseAudioFile: audioUrl // 添加音频地址字段
});
console.log('公共方法:添加普通消息');
// console.log('messageList2', this.messageList);
if (addlog === 1) {
// 增加日志
this.addReplyLog(content, audioUrl)
} else if (addlog === 2) {
// 添加日志
this.addReplyLog(content, audioUrl, 2)
}
// 滚动到最新消息
this.scrollToBottom();
// 移除loading消息
this.removeLoadingMessage(this.loadingId);
// 自动播放新消息(移除条件编译限制)
if (audioUrl) {
this.$nextTick(() => {
this.autoPlayMessage(audioUrl);
});
}
},
// AI对话接口
async dialogue(inputText) {
// 显示loading
this.loadingId = this.showLoadingMessage();
// console.log('messageList1', this.messageList);
const parms = {
ReplyType: this.todoReplyType,// 操作类型
ReplyContent: inputText,// 用户回复内容
}
// 模拟回复成功逻辑
// this.addNormalMessage('好的,正在为您执行操作', 'ai', '', 0)
// this.judgmentProcess('1')
try {
const res = await doAIReplyHandle(parms);
console.log('messageList', this.messageList);
const resRaw = JSON.parse(res.Data)
if (resRaw.Code === '2000') {
// this.removeLoadingMessage(this.loadingId);
const resData = JSON.parse(resRaw.Data)
console.log(resData);
// 显示实际回复
this.addNormalMessage(resData.Message, 'ai', '', 0)
// .then(() => {
// // 回复成功逻辑
// this.judgmentProcess(resData.SelectItem)
// })
console.log('resData.Message', resData.Message);
// 回复成功逻辑
this.judgmentProcess(resData.SelectItem)
} else {
console.log('AI请求失败');
this.updateLoadingToError(this.loadingId);
}
} catch (error) {
// 更新为错误状态
this.updateLoadingToError(this.loadingId, '请求失败,请重试');
}
},
// 查询历史记录
async getReplyLogList() {
console.log('查询历史记录');
try {
const res = await getReplyLogListByPage();
const resRaw = JSON.parse(res.Data)
// const resRaw = JSON.parse(JSON.parse(res.Data).Data)
// console.log(resRaw);
if (resRaw.Code === '2000') {
if (resRaw.Data !== '[]') {
const resData = JSON.parse(resRaw.Data)
// console.log(resData);
this.ReplyType = resData.at(-1).ReplyType
// console.log('this.ReplyType', this.ReplyType);
// 转换历史数据
this.allMessages = this.convertHistoryToMessages(resData);
// 初始化显示消息(最后3条)
this.initDisplayedMessages();
// 滚动到底部
// this.$nextTick(() => {
// this.scrollToBottom();
// });
}
// 调用待办接口 2
console.log('#############查询历史记录');
this.serverProcduceCall();
}
} catch (error) {
// 错误
console.error('获取历史记录失败:', error);
this.updateLoadingToError(this.loadingId);
this.hasMore = false;
}
},
// 初始化显示消息
initDisplayedMessages() {
const totalMessages = this.allMessages.length;
if (totalMessages <= this.maxDisplayCount) {
// 消息总数小于等于3条,全部显示
this.messageList = [...this.allMessages];
this.hasMore = false;
this.loadedMessageCount = totalMessages;
} else {
// 只显示最后3条消息
this.messageList = this.allMessages.slice(-this.maxDisplayCount);
this.hasMore = true;
this.loadedMessageCount = this.maxDisplayCount;
}
// 确保滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
});
},
// 加载更多消息的方法
loadMoreMessages() {
if (this.loadingMore || !this.hasMore) return;
this.loadingMore = true;
// 模拟加载延迟
setTimeout(() => {
const currentLength = this.messageList.length;
const totalLength = this.allMessages.length;
if (currentLength < totalLength) {
// 计算需要添加的消息数量(最多添加3条)
const startIndex = Math.max(0, currentLength - this.maxDisplayCount);
const endIndex = currentLength;
const messagesToAdd = this.allMessages.slice(startIndex, endIndex);
// 添加到消息列表开头
this.messageList = [...messagesToAdd, ...this.messageList];
// 判断是否还有更多消息
this.hasMore = startIndex > 0;
} else {
this.hasMore = false;
}
this.loadingMore = false;
}, 500); // 模拟网络延迟
},
// AI历史数据处理
// 将历史数据转换为 messageList 格式
convertHistoryToMessages(historyData) {
const messages = [];
// 按 FId 升序排列(从最早到最晚)
// const sortedData = [...historyData].sort((a, b) => a.FId - b.FId);
const sortedData = [...historyData]
// console.log('sortedData',sortedData);
sortedData.forEach((item, index) => {
// 添加用户消息 (如果 ReplyContent 不为空)
if (item.ReplyContent) {
messages.push({
id: `user_${item.FId}`,
type: 'user',
content: item.ReplyContent,
status: 'completed'
});
}
// 添加AI消息
let aiContent = item.FResponseContent;
// 如果 FResponseContent 是 JSON 字符串,解析并提取 Message 字段
try {
const responseObj = JSON.parse(item.FResponseContent);
if (responseObj.Message) {
aiContent = responseObj.Message;
}
} catch (e) {
// 如果不是JSON格式,保持原样
}
// console.log('aiContent',aiContent);
messages.push({
id: `ai_${item.FId}`,
type: 'ai',
content: aiContent,
status: 'completed',
FResponseAudioFile: item.FResponseAudioFile
});
});
return messages;
},
// 加载消息(分页)
loadMessages() {
if (!this.hasMore || this.loadingMore) return;
this.loadingMore = true;
// 模拟加载延迟
setTimeout(() => {
const totalLength = this.allMessages.length;
if (this.loadedMessageCount < totalLength) {
// 计算需要添加的消息数量(最多添加3条)
const startIndex = Math.max(0, this.loadedMessageCount - this.maxDisplayCount);
const endIndex = this.loadedMessageCount;
const messagesToAdd = this.allMessages.slice(startIndex, endIndex);
// 添加到消息列表开头
this.messageList = [...messagesToAdd, ...this.messageList];
// 更新已加载消息数量
this.loadedMessageCount = endIndex;
// 判断是否还有更多消息
this.hasMore = startIndex > 0;
} else {
this.hasMore = false;
}
this.loadingMore = false;
}, 500); // 模拟网络延迟
},
// 处理滚动到顶部加载更多
handleScrolltoupper() {
if (this.hasMore && !this.loadingMore) {
this.loadMessages();
}
},
// 调用语音合成
async callVoiceConju(FResponseContent, status, flow) {
const params = {
text: FResponseContent,
voice: "1",
}
console.log('语音合成params', params);
const res = await voiceConju(params);
// console.log('callVoiceConju', res);
if (res.Code === '2000') {
const resRaw = JSON.parse(res.Data)
console.log('语音合成', resRaw);
const configStore = useConfigStore()
const audioUrl = `http:xxxxxxxxxxx/AudioPlay/${resRaw.Data}.mp3`;
// 立即播放音频
// this.$nextTick(() => {
// this.togglePlay(newMessage);
// });
// 添加消息
this.addNormalMessage(FResponseContent, 'ai', audioUrl, 1);
} else {
console.log('合成失败');
}
},
},
// 添加 beforeDestroy 钩子
beforeUnmount() {
// 清除定时器
this.stopStatusCheck();
if (this.playTimer) {
clearTimeout(this.playTimer);
this.playTimer = null;
}
// 停止音频播放并销毁音频上下文
if (this.audioContext) {
this.audioContext.stop();
this.audioContext.destroy();
}
// 重置播放状态
this.isPlaying = false;
this.currentPlayId = null;
},
};
</script>
<style lang="scss">
.container {
display: flex;
flex-direction: column;
width: 100%;
height: 100vh;
background-color: #f5f5f5;
background: url('/static/images/ai/aiBG.png') no-repeat;
background-size: cover;
// background-position: center;
.chat-container {
// padding: 0 28rpx;
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
.header {
flex-shrink: 0;
}
.messages {
flex: 1 1 auto;
overflow: hidden;
display: flex;
flex-direction: column;
}
.chat-black {
margin-top: 60px;
display: flex;
justify-content: space-between;
padding: 0 28rpx;
.black {
.black-img {
width: 18rpx;
height: 32rpx;
}
}
.more {
.more-img {
width: 32rpx;
height: 32rpx;
}
}
}
.chat-title {
margin-top: 80rpx;
padding: 0 28rpx;
margin-bottom: 40rpx;
.chat-header {
width: 266rpx;
height: 53rpx;
font-family: PingFang SC;
font-weight: 600;
font-size: 38rpx;
color: #333333;
line-height: 38rpx;
}
.chat-text {
width: 310rpx;
height: 71rpx;
font-family: PingFang SC;
font-weight: 400;
font-size: 24rpx;
color: #626579;
line-height: 34rpx;
}
}
}
}
.chat-area {
flex: 1;
padding: 0 28rpx 28rpx 28rpx;
box-sizing: border-box;
overflow-y: auto;
height: 100%;
margin-bottom: 100rpx;
}
.message-item {
display: flex;
margin-top: 30rpx;
align-items: flex-start;
// margin-right: 40rpx;
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
.message-content {
max-width: 70%;
// flex:1;
padding: 20rpx;
border-radius: 10rpx;
font-size: 28rpx;
line-height: 1.5;
margin: 0 10rpx;
.playing {
width: 112rpx;
height: 40rpx;
border-radius: 20rpx;
// padding: 5rpx 10rpx;
padding-left: 10rpx;
padding-top: 3rpx;
margin-top: 10rpx;
background-color: #EDF2FE;
}
}
}
.uni-scroll-view-content .message-item:first-child {
margin: 0;
}
.user-message {
justify-content: flex-end;
.message-content {
background-color: #4D81F1;
color: white;
margin-left: 20rpx;
border-radius: 24rpx 24rpx 0rpx 24rpx;
}
}
.ai-message {
justify-content: flex-start;
.message-content {
background-color: white;
color: #333;
margin-right: 20rpx;
border-radius: 0rpx 24rpx 24rpx 24rpx;
}
}
.text-input-container {
display: flex;
padding: 20rpx;
// background-color: white;
// border-top: 1rpx solid #eee;
width: 100%;
position: absolute;
bottom: 0;
left: 0;
box-sizing: border-box;
input {
flex: 1;
padding: 20rpx;
// border: 1rpx solid #ddd;
border-radius: 50rpx;
font-size: 28rpx;
margin-right: 20rpx;
background-color: #fff;
height: 36rpx !important;
}
button {
padding: 0 40rpx;
background-color: #1989fa;
color: white;
border: none;
border-radius: 50rpx;
}
.uni-input-placeholder .input-placeholder {
font-family: PingFang SC;
font-weight: 400;
font-size: 28rpx;
color: #999999;
// line-height: 28rpx;
}
.send {
width: 76rpx;
height: 76rpx;
}
}
.voice-input-container {
// background-color: white;
padding: 20rpx;
// border-top: 1rpx solid #eee;
width: 100%;
position: absolute;
bottom: 0;
box-sizing: border-box;
.voice-preview {
background-color: #f9f9f9;
border-radius: 10rpx;
padding: 20rpx;
// margin-bottom: 20rpx;
transition: all 0.3s;
&.canceling {
// background-color: #ffeeee;
.voice-controls {
color: #e43d33;
}
}
.voice-controls {
text-align: center;
// font-size: 28rpx;
margin-top: 30rpx;
margin-bottom: 10rpx;
display: flex;
justify-content: center;
align-items: center;
font-family: PingFang SC;
font-weight: 400;
font-size: 24rpx;
color: #7F7F7F;
line-height: 24rpx;
.slide-hint {
// font-size: 24rpx;
// color: #999;
// margin-top: 10rpx;
}
.cancel-hint {
// font-size: 24rpx;
// margin-top: 10rpx;
}
}
.voice-text-preview {
min-height: 80rpx;
padding: 20rpx;
background-color: white;
border-radius: 10rpx;
color: #333;
}
}
// .voice-btn {
// width: 100%;
// background-color: #1989fa;
// color: white;
// border: none;
// border-radius: 42rpx;
// font-size: 32rpx;
// text-align: center;
// }
.voice-btn {
width: 100%;
background-color: #1989fa;
color: white;
border: none;
border-radius: 42rpx;
font-size: 32rpx;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
padding: 20rpx 0;
.voice-btn-image {
display: flex;
justify-content: center;
align-items: center;
width: 380rpx;
height: 72rpx;
}
}
.wd-button {
height: 76rpx !important;
}
}
.switchingIcon {
// width: 70rpx;
// height: 70rpx;
// margin: 10rpx;
width: 76rpx;
height: 76rpx;
background: rgba(255, 255, 255);
border-radius: 50%;
display: flex; // 添加flex布局
align-items: center; // 垂直居中
justify-content: center; // 水平居中
margin-right: 14rpx;
.mode-switch-btn {
width: 40rpx;
height: 40rpx;
}
}
.load-more-container {
display: flex;
justify-content: center;
padding: 20rpx;
.load-more-btn {
// padding: 15rpx 30rpx;
// background-color: #f0f0f0;
// border-radius: 30rpx;
font-size: 26rpx;
color: #666;
&:active {
// background-color: #e0e0e0;
}
}
}
.loading-more {
text-align: center;
padding: 20rpx;
font-size: 24rpx;
color: #999;
}
</style>