使用uniapp实现一个AI对话页面

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>

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容