使用uniapp實現(xiàn)一個AI對話頁面

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>

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容