1.實現(xiàn)語音輸入(語音識別)
2.實現(xiàn)文字輸出時語音播放
3.實現(xiàn)語音播放的開始,暫停邏輯,語音播放時AI使用gif圖
4.實現(xiàn)對話問答模式
5.實現(xiàn)加載歷史記錄,每次加載3條,滑動到頂端每次再加載3條
6.實現(xiàn)每次輸出顯示最新條消息功能
代碼實現(xiàn):
只保留框架,具體業(yè)務已移除
<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>
<!-- 聊天消息區(qū)域 -->
<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狀態(tài) -->
<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>
<!-- 處理錯誤狀態(tài) -->
<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>
<!-- 文本輸入?yún)^(qū)域 -->
<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">發(fā)送</button> -->
<view>
<image class="send" :src="'/static/images/ai/send.png'" @click="sendTextMessage" mode="aspectFit" />
</view>
</view>
</view>
<!-- 語音輸入?yún)^(qū)域 -->
<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 ? '' : '松開發(fā)送' }}
</text>
<view class="slide-hint" v-show="!isCanceling">
<text>↑ 上移取消</text>
</view>
<view class="cancel-hint" v-show="isCanceling">
<text style="color: #e43d33; ">松開手指 取消發(fā)送</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, // 每頁顯示條數(shù)
hasMore: false, // 是否還有更多數(shù)據(jù)
loadingMore: false, // 是否正在加載更多
maxDisplayCount: 3, // 初始顯示消息數(shù)
lastMsgId: '', // 最后一條消息ID
loadedMessageCount: 0, // 已加載的消息數(shù)量
inputText: '', // 輸入框文本
aiAvatar: '/src/static/images/ai/AI.png', // AI頭像(默認靜態(tài))
aiAvatarPlaying: '/src/static/images/ai/AI.gif', // AI播放時的動態(tài)頭像
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', // 請?zhí)鎿Q為實際值
API_SECRET: '', // 請?zhí)鎿Q為實際值
API_KEY: '' // 請?zhí)鎿Q為實際值
},
downtime: -1, // 默認-1
downed: false,
disabled: false,
second,
scrollTop: 0,
oldScrollTop: 0, // 添加這個數(shù)據(jù)用于強制刷新滾動
isPlaying: false, // 播放狀態(tài)
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()
// 初始化音頻上下文
// 直接創(chuàng)建音頻上下文,不經(jīng)過 Vue 的響應式系統(tǒng)
const audioContext = uni.createInnerAudioContext();
// 安全地設置 obeyMuteSwitch 屬性
try {
audioContext.obeyMuteSwitch = false;
} catch (e) {
console.warn('無法設置 obeyMuteSwitch 屬性:', e);
}
// 將 audioContext 掛載到實例上
this.audioContext = audioContext;
// 添加事件監(jiān)聽器
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) {
// 檢查參數(shù)
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;
// 監(jiān)聽播放完成事件
const onEnded = () => {
// console.log('自動播放完成');
this.isPlaying = false;
this.currentPlayId = null;
if (this.playTimer) {
clearTimeout(this.playTimer);
this.playTimer = null;
}
// 移除事件監(jiān)聽器
this.audioContext.offEnded(onEnded);
};
// 監(jiān)聽播放錯誤事件
const onError = (res) => {
console.error('自動播放音頻錯誤:', res.errMsg);
this.isPlaying = false;
this.currentPlayId = null;
if (this.playTimer) {
clearTimeout(this.playTimer);
this.playTimer = null;
}
// 移除事件監(jiān)聽器
this.audioContext.offError(onError);
};
// 添加事件監(jiān)聽器
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;
}
}
},
// 手動切換播放狀態(tài)
// 修改 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();
// 設置播放狀態(tài),這會自動切換頭像
this.isPlaying = true;
// console.log('音頻播放已啟動');
// 添加播放完成的監(jiān)聽器
const onEnded = () => {
// console.log('音頻播放完成');
this.isPlaying = false;
this.currentPlayId = null;
if (this.playTimer) {
clearTimeout(this.playTimer);
this.playTimer = null;
}
// 移除監(jiān)聽器避免內(nèi)存泄漏
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();
// 設置暫停狀態(tài),這會自動切換頭像
this.isPlaying = false;
}
} catch (error) {
console.error('暫停音頻失敗:', error);
}
},
// 停止音頻
stopAudio() {
try {
if (this.audioContext) {
this.audioContext.stop();
}
} catch (error) {
console.error('停止音頻失敗:', error);
}
// 重置狀態(tài),這會自動切換頭像
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();
}
// 重置所有狀態(tài)
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('觸發(fā)取消狀態(tài)');
}
} else if (this.isCanceling) {
this.$set(this, 'isCanceling', false);
// console.log('取消狀態(tài)已重置');
}
},
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便于后續(xù)操作
},
// 公共方法:移除loading消息
removeLoadingMessage(loadingId) {
this.messageList = this.messageList.filter(msg => msg.id !== loadingId);
},
// 公共方法:更新loading為錯誤狀態(tài)
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,// 用戶回復內(nèi)容
}
// 模擬回復成功邏輯
// this.addNormalMessage('好的,正在為您執(zhí)行操作', '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) {
// 更新為錯誤狀態(tài)
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);
// 轉換歷史數(shù)據(jù)
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) {
// 消息總數(shù)小于等于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) {
// 計算需要添加的消息數(shù)量(最多添加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); // 模擬網(wǎng)絡延遲
},
// AI歷史數(shù)據(jù)處理
// 將歷史數(shù)據(jù)轉換為 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) {
// 計算需要添加的消息數(shù)量(最多添加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];
// 更新已加載消息數(shù)量
this.loadedMessageCount = endIndex;
// 判斷是否還有更多消息
this.hasMore = startIndex > 0;
} else {
this.hasMore = false;
}
this.loadingMore = false;
}, 500); // 模擬網(wǎng)絡延遲
},
// 處理滾動到頂部加載更多
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();
}
// 重置播放狀態(tài)
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>