|
|
@@ -0,0 +1,716 @@
|
|
|
+<template>
|
|
|
+ <view class="chat-container" :style="{
|
|
|
+ paddingTop: appStore.navbarHeight + 'px',
|
|
|
+ }">
|
|
|
+ <view class="mine_ybt_title flex-center bg_color_fff"
|
|
|
+ :style="{ height: appStore.navbarHeight + 'px',
|
|
|
+ paddingTop: appStore.statusBarHeight + 'px'}"
|
|
|
+ >
|
|
|
+ <!-- <text class="font_size35 bold">AI客服</text> -->
|
|
|
+ <agentCheck ref="agentCheckRef"></agentCheck>
|
|
|
+ </view>
|
|
|
+ <!-- 聊天消息区域 -->
|
|
|
+ <view class="chat-messages flex-column">
|
|
|
+ <!-- 工具栏 -->
|
|
|
+ <tools @addHuiHuaFn="addHuiHuaFn"></tools>
|
|
|
+ <!-- 消息列表 -->
|
|
|
+ <scroll-view
|
|
|
+ class="scrollViewRef flex_1"
|
|
|
+ scroll-y="true"
|
|
|
+ :scroll-top="scrollTop"
|
|
|
+ ref="scrollViewRef"
|
|
|
+ :scroll-with-animation="true"
|
|
|
+ @refresherrefresh="handlePullDownRefresh"
|
|
|
+ :refresher-enabled="true"
|
|
|
+ :refresher-triggered="triggered"
|
|
|
+ >
|
|
|
+ <messagesInfoDefault v-if="messages.length==0"
|
|
|
+ @sendMessage="sendMessage"
|
|
|
+ ref="messagesInfoDefaultRef" key="messagesInfoDefaultRef">
|
|
|
+ </messagesInfoDefault>
|
|
|
+ <messagesInfo
|
|
|
+ :messages="messages"
|
|
|
+ :isLoading="isLoading"
|
|
|
+ @imageLoaded="scrollToBottom"
|
|
|
+ :id="`msg-${messagesKey}`">
|
|
|
+ </messagesInfo>
|
|
|
+ </scroll-view>
|
|
|
+ </view>
|
|
|
+ <!-- 输入区域 -->
|
|
|
+ <view class="input-area">
|
|
|
+ <button class="image-btn" :disabled="isLoading" @click.stop.prevent="uploadImage">
|
|
|
+ <image class="image-icon" src="/static/img/service/tupian.png" model="aspectFit"></image>
|
|
|
+ </button>
|
|
|
+ <button v-if="isvoice"
|
|
|
+ class="record-btn"
|
|
|
+ :class="{ recording: isRecording }"
|
|
|
+ @touchstart="startRecord"
|
|
|
+ @touchend="stopRecord"
|
|
|
+ @touchmove="handleTouchMove"
|
|
|
+ @touchcancel="cancelRecord"
|
|
|
+ :disabled="isRecording && isCancel"
|
|
|
+ >
|
|
|
+ <view v-if="!isRecording">按住说话</view>
|
|
|
+ <view v-if="isRecording && !isCancel">松手发送,上移取消</view>
|
|
|
+ <view v-if="isRecording && isCancel">松手取消</view>
|
|
|
+ <text v-if="recordDuration>0"></text>
|
|
|
+ </button>
|
|
|
+ <input v-else
|
|
|
+ class="input-box"
|
|
|
+ :placeholder="isLoading?'努力回答中...':'发消息或按住说话'"
|
|
|
+ placeholder-style="color: #999"
|
|
|
+ v-model.trim="inputText"
|
|
|
+ :disabled="isLoading"
|
|
|
+ @confirm="sendMessage({chatType:0,msgContent:inputText})"
|
|
|
+ />
|
|
|
+ <button class="isvoice-btn" @click.stop.prevent="isvoice=!isvoice;authorizeRecord()" :disabled="isLoading">
|
|
|
+ <image class="mic-icon" src="/static/img/service/xiaoxi.png" v-if="isvoice" model="aspectFit"></image>
|
|
|
+ <image class="mic-icon" src="/static/img/service/maikefengyuyin.png" v-else model="aspectFit"></image>
|
|
|
+ </button>
|
|
|
+ <button class="send-btn" @click.stop.prevent="sendMessage({chatType:0,msgContent:inputText})" :disabled="isLoading" v-if="!isvoice">
|
|
|
+ <image class="send-icon" src="/static/img/service/send-icon.png"></image>
|
|
|
+ </button>
|
|
|
+ <view class="ai-tip">内容由AI生成,仅供参考</view>
|
|
|
+ </view>
|
|
|
+ <!-- 录音动画/提示 -->
|
|
|
+ <view
|
|
|
+ class="record-toast"
|
|
|
+ v-if="isRecording"
|
|
|
+ :class="{ cancel: isCancel }"
|
|
|
+ >
|
|
|
+ <image
|
|
|
+ v-if="!isCancel"
|
|
|
+ src="/static/img/voice/recording.gif"
|
|
|
+ class="toast-icon"
|
|
|
+ alt="录音中动画"
|
|
|
+ ></image>
|
|
|
+ <image
|
|
|
+ v-if="isCancel"
|
|
|
+ src="/static/img/voice/cancel.png"
|
|
|
+ class="toast-icon"
|
|
|
+ alt="取消录音图标"
|
|
|
+ ></image>
|
|
|
+ <text v-if="!isCancel">正在录音...({{60-recordDuration}})</text>
|
|
|
+ <text v-if="isCancel">松手取消发送</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, nextTick, watch } from 'vue';
|
|
|
+import { onShow,onHide,onLoad } from "@dcloudio/uni-app"
|
|
|
+import { chatHistoryDetails } from '@/api/ai.js';
|
|
|
+import messagesInfo from "./components/messagesInfo.vue";
|
|
|
+import messagesInfoDefault from "./components/messagesInfoDefault.vue";
|
|
|
+import tools from "./components/tools.vue";
|
|
|
+import agentCheck from "./components/agentCheck.vue";
|
|
|
+import {
|
|
|
+ HTTP_REQUEST_URL,
|
|
|
+ HTTP_REQUEST_URL_WS,
|
|
|
+ TOKENNAME
|
|
|
+} from '@/config/app';
|
|
|
+import { chooseImageOne,checkLoginShowModal,checkAiQuotaDailyModal,getUserInfo } from "@/utils/util.js";
|
|
|
+// 封装的websocket
|
|
|
+import WSClient from '@/utils/wsUtil.js';
|
|
|
+import { useAppStore } from '@/stores/app'
|
|
|
+import { useToast } from '@/hooks/useToast'
|
|
|
+import dayjs from "dayjs";
|
|
|
+const appStore = useAppStore();
|
|
|
+const { Toast } = useToast();
|
|
|
+const agentCheckRef = ref(null);
|
|
|
+const messagesInfoDefaultRef = ref(null);
|
|
|
+const newSession = ref(false);//是否新会话
|
|
|
+const historySession = ref('');//是否新会话
|
|
|
+
|
|
|
+// 添加滚动相关变量
|
|
|
+const scrollTimeout = ref(null);
|
|
|
+const scrollViewRef = ref(null);
|
|
|
+const scrollTop = ref(0);
|
|
|
+const messagesKey = ref(0);
|
|
|
+const triggered = ref(false);
|
|
|
+
|
|
|
+// 底部发送功能
|
|
|
+const inputText = ref('');
|
|
|
+const isLoading = ref(false);//思考中
|
|
|
+const isvoice = ref(false);//是否语音输入
|
|
|
+const wsClient = ref(null);
|
|
|
+
|
|
|
+// 模拟聊天消息数据
|
|
|
+const messages = ref([]);
|
|
|
+
|
|
|
+watch(() => appStore.agentId, (state) => {
|
|
|
+ messages.value = [];
|
|
|
+ getAdSearchFn()
|
|
|
+});
|
|
|
+
|
|
|
+onLoad((e)=>{
|
|
|
+ console.log('ai onLoad',e);
|
|
|
+})
|
|
|
+onShow(async()=>{
|
|
|
+ if(!await checkLoginShowModal())return;
|
|
|
+ nextTick(async ()=>{
|
|
|
+ // 初始化选择智能体
|
|
|
+ await agentCheckRef.value.initAgentId();
|
|
|
+ // 初始化完成后获取 agentId
|
|
|
+ console.log('agentId:', agentCheckRef.value.agentId);
|
|
|
+ if(appStore.msgContent){
|
|
|
+ sendMessage({chatType:0,msgContent:appStore.msgContent});
|
|
|
+ appStore.UPDATE_msgContent('');
|
|
|
+ }else if(appStore.sessionId){
|
|
|
+ handlePullDownRefresh();
|
|
|
+ }else{
|
|
|
+ // getAdSearchFn()
|
|
|
+ }
|
|
|
+
|
|
|
+ })
|
|
|
+
|
|
|
+ aiStartChatFn();
|
|
|
+});
|
|
|
+onHide(()=>{
|
|
|
+ isLoading.value = false;
|
|
|
+ cleanupResources();
|
|
|
+});
|
|
|
+// 新增:添加回话功能
|
|
|
+function addHuiHuaFn(){
|
|
|
+ newSession.value = true;
|
|
|
+ messages.value = [];
|
|
|
+ getAdSearchFn();
|
|
|
+}
|
|
|
+function getAdSearchFn(){
|
|
|
+ nextTick(()=>{
|
|
|
+ setTimeout(()=>{
|
|
|
+ messagesInfoDefaultRef.value.getAdSearchFn();
|
|
|
+ },50)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function authorizeRecord() {
|
|
|
+ uni.authorize({
|
|
|
+ scope: 'scope.record',
|
|
|
+ success() {
|
|
|
+ // uni.getRecorderManager();
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+function aiStartChatFn(){
|
|
|
+ cleanupResources();
|
|
|
+ wsClient.value = new WSClient({
|
|
|
+ url: `${HTTP_REQUEST_URL_WS}/api/websocket`,
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ "Authorization": 'Bearer '+appStore.token
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // 注册SSE事件
|
|
|
+ registerEvents();
|
|
|
+ wsClient.value.open({type:0,message:inputText.value})
|
|
|
+}
|
|
|
+
|
|
|
+//
|
|
|
+async function sendMessage({chatType,msgContent=""}){
|
|
|
+ console.log('sendMessage',wsClient.value);
|
|
|
+ // 登录检测
|
|
|
+ if(!await checkLoginShowModal())return;
|
|
|
+ // 今日免费提问次数检测
|
|
|
+ if(!await checkAiQuotaDailyModal())return;
|
|
|
+ if (!msgContent) {
|
|
|
+ Toast({title:'请输入内容'});
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ isLoading.value = true
|
|
|
+ messages.value.push({
|
|
|
+ msgContent,
|
|
|
+ chatType,// 0 (文本),1 (图片)
|
|
|
+ speakerType:0//0用户消息1AI消息
|
|
|
+ });
|
|
|
+ messages.value.push({
|
|
|
+ msgContent: "",
|
|
|
+ event: "start",//sse长链接的返回状态
|
|
|
+ chatType,
|
|
|
+ speakerType:1,
|
|
|
+ messageTimeNow:dayjs().format('HH:mm')
|
|
|
+ });
|
|
|
+ //type 0 (文本),1 (图片)
|
|
|
+ wsClient.value.send({
|
|
|
+ type:chatType,
|
|
|
+ message:msgContent,
|
|
|
+ agentId:agentCheckRef.value.agentId,
|
|
|
+ useBalance:true,
|
|
|
+ newSession:newSession.value,
|
|
|
+ historySession:historySession.value
|
|
|
+ });
|
|
|
+ messagesKey.value++;
|
|
|
+ inputText.value = '';
|
|
|
+ newSession.value = false;
|
|
|
+ // 每次收到新消息时滚动到底部
|
|
|
+ // 如果是图片,子组件里图片加载成功后触发
|
|
|
+ if(chatType!=1)scrollToBottom();
|
|
|
+}
|
|
|
+async function uploadImage(){
|
|
|
+ if(!await checkLoginShowModal())return;
|
|
|
+ const res = await chooseImageOne();
|
|
|
+ console.log('uploadImage',res);
|
|
|
+ if(res){
|
|
|
+ // userInfo.value[key] = res.fileName
|
|
|
+ sendMessage({chatType:1,msgContent:res.data})
|
|
|
+ }
|
|
|
+}
|
|
|
+function cleanupResources(closeSSE=true) {
|
|
|
+ // 关闭SSE连接
|
|
|
+ if (wsClient.value) {
|
|
|
+ // 移除所有事件监听
|
|
|
+ if (wsClient.value.callbacks) {
|
|
|
+ Object.keys(wsClient.value.callbacks).forEach(event => {
|
|
|
+ wsClient.value.callbacks[event] = [];
|
|
|
+ });
|
|
|
+ }
|
|
|
+ wsClient.value.close('clean');
|
|
|
+ wsClient.value = null;
|
|
|
+ }
|
|
|
+}
|
|
|
+function registerEvents(){
|
|
|
+ wsClient.value.on('open', (data) => {
|
|
|
+ console.log('ws连接成功',data);
|
|
|
+ });
|
|
|
+ wsClient.value.on('answer', (data) => {
|
|
|
+ console.log('answer',data.segment,messages.value.length - 1);
|
|
|
+ isLoading.value = false;
|
|
|
+ const lastIndex = messages.value.length - 1;
|
|
|
+ // 保存完整内容(累加片段)
|
|
|
+ // const newFullAnswer = messages.value[lastIndex].msgContent + data.segment;
|
|
|
+ messages.value[lastIndex].msgContent = messages.value[lastIndex].msgContent + data.segment;
|
|
|
+ messagesKey.value++;
|
|
|
+ console.log('answer',messages);
|
|
|
+ // 每次收到新消息时滚动到底部
|
|
|
+ scrollToBottom();
|
|
|
+ nextTick(()=>{
|
|
|
+ // 获取用户信息
|
|
|
+ getUserInfo();
|
|
|
+ })
|
|
|
+ });
|
|
|
+ wsClient.value.on('error', (err) => {
|
|
|
+ isLoading.value = false;
|
|
|
+ console.log('error',err);
|
|
|
+ // aiStartChatFn();
|
|
|
+ Toast({ title: JSON.stringify(err) || "服务器异常" });
|
|
|
+ });
|
|
|
+ wsClient.value.on('close', (data) => {
|
|
|
+ isLoading.value = false;
|
|
|
+ let lastIndex = messages.value.length - 1;
|
|
|
+ console.log('close',data,lastIndex);
|
|
|
+ // 判断关闭原因,取消,删除当前聊天内容,超时提示请求超时
|
|
|
+ switch (data.reason) {
|
|
|
+ case 'timeout': {
|
|
|
+ // 提示超时
|
|
|
+ // uni.showToast({
|
|
|
+ // title: '请求超时,请重试',
|
|
|
+ // icon: 'none'
|
|
|
+ // });
|
|
|
+ if(lastIndex>-1)messages.value[lastIndex].msgContent = messages.value[lastIndex].msgContent || "请求超时,请重试";
|
|
|
+ aiStartChatFn();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// 新增:滚动到底部的方法
|
|
|
+function scrollToBottom() {
|
|
|
+ nextTick(() => {
|
|
|
+ // if (scrollTimeout.value) return;
|
|
|
+ // scrollTimeout.value = setTimeout(() => {
|
|
|
+ uni.createSelectorQuery()
|
|
|
+ .select(`#msg-${messagesKey.value}`)
|
|
|
+ .boundingClientRect(rect => {
|
|
|
+ if (rect) {
|
|
|
+ scrollTop.value = rect.height;
|
|
|
+ // 使用更平滑的滚动方式
|
|
|
+ uni.pageScrollTo({
|
|
|
+ scrollTop: rect.bottom,
|
|
|
+ duration: 100
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .exec();
|
|
|
+ // scrollTimeout.value = null;
|
|
|
+ // }, 80); // 延长节流间隔至80ms,减少冲突
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/*******************************************历史数据********************************************************/
|
|
|
+
|
|
|
+const hitstoryParams = ref({
|
|
|
+ pageNum: 0,
|
|
|
+ pageSize: 10,
|
|
|
+ sessionId:appStore.sessionId,
|
|
|
+});
|
|
|
+// 重命名方法,避免与小程序生命周期冲突
|
|
|
+function handlePullDownRefresh() {
|
|
|
+ console.log('下拉刷新触发');
|
|
|
+ triggered.value = true;
|
|
|
+ hitstoryParams.value.pageNum++;
|
|
|
+ chatHistoryDetailsFn();
|
|
|
+}
|
|
|
+function chatHistoryDetailsFn(){
|
|
|
+ hitstoryParams.value.sessionId = appStore.sessionId;
|
|
|
+ chatHistoryDetails(hitstoryParams.value).then(res=>{
|
|
|
+ triggered.value = false;
|
|
|
+ console.log('chatHistoryFn',res);
|
|
|
+ const rows = res?.rows || [];
|
|
|
+ if(rows.length==0 && hitstoryParams.value.pageNum>0){
|
|
|
+ hitstoryParams.value.pageNum--;
|
|
|
+ // Toast({title:'没有更多历史记录了'});
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if(messages.value.length==res.total){
|
|
|
+ Toast({title:'没有更多历史记录了'});
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ messages.value = [...rows, ...messages.value]
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+/*******************************************语音功能********************************************************/
|
|
|
+
|
|
|
+const isRecording = ref(false);// 是否正在录音
|
|
|
+const isCancel = ref(false); // 是否取消录音
|
|
|
+const recordDuration = ref(0); // 录音时长(秒)
|
|
|
+const tempFilePath = ref(''); // 录音临时文件路径
|
|
|
+const recordTimer = ref(null); // 录音计时定时器
|
|
|
+const recorderManager = uni.getRecorderManager(); // 录音管理
|
|
|
+// 开始录音(触摸开始)
|
|
|
+const startRecord = async (e) => {
|
|
|
+ if(!await checkLoginShowModal())return;
|
|
|
+ // 防止冒泡导致的异常
|
|
|
+ e.stopPropagation();
|
|
|
+
|
|
|
+ // 初始化录音参数
|
|
|
+ const options = {
|
|
|
+ format: 'mp3', // 录音格式
|
|
|
+ sampleRate: 44100, // 采样率
|
|
|
+ numberOfChannels: 1, // 声道数
|
|
|
+ encodeBitRate: 96000 // 编码比特率
|
|
|
+ };
|
|
|
+
|
|
|
+ // 开始录音
|
|
|
+ recorderManager.start(options);
|
|
|
+ isRecording.value = true;
|
|
|
+ isCancel.value = false;
|
|
|
+ recordDuration.value = 0;
|
|
|
+
|
|
|
+ // 计时逻辑
|
|
|
+ recordTimer.value = setInterval(() => {
|
|
|
+ recordDuration.value++;
|
|
|
+ // 限制最大录音时长(如60秒)
|
|
|
+ if (recordDuration.value >= 60) {
|
|
|
+ stopRecord();
|
|
|
+ }
|
|
|
+ }, 1000);
|
|
|
+
|
|
|
+ // 监听录音错误
|
|
|
+ recorderManager.onError((err) => {
|
|
|
+ console.error('录音错误:', err);
|
|
|
+ cancelRecord();
|
|
|
+ uni.showToast({ title: '录音失败', icon: 'none' });
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 停止录音(触摸结束)
|
|
|
+const stopRecord = () => {
|
|
|
+ if (!isRecording.value) return;
|
|
|
+
|
|
|
+ // 清除计时
|
|
|
+ clearInterval(recordTimer.value);
|
|
|
+
|
|
|
+ // 停止录音
|
|
|
+ recorderManager.stop();
|
|
|
+ recorderManager.onStop((res) => {
|
|
|
+ tempFilePath.value = res.tempFilePath;
|
|
|
+ console.log('recorderManager',res.tempFilePath);
|
|
|
+
|
|
|
+ // 判断是否取消或录音过短
|
|
|
+ if (isCancel.value) {
|
|
|
+ uni.showToast({ title: '已取消发送', icon: 'none' });
|
|
|
+ } else if (recordDuration.value < 1) {
|
|
|
+ uni.showToast({ title: '录音时间太短', icon: 'none' });
|
|
|
+ } else {
|
|
|
+ // 上传录音并添加到列表
|
|
|
+ uploadVoice(res.tempFilePath);
|
|
|
+ }
|
|
|
+ // 重置状态
|
|
|
+ resetRecordState();
|
|
|
+ });
|
|
|
+
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+// 处理触摸移动(用于判断是否取消)
|
|
|
+const handleTouchMove = (e) => {
|
|
|
+ if (!isRecording.value) return;
|
|
|
+
|
|
|
+ // 获取按钮位置和触摸位置
|
|
|
+ const buttonRect = uni.createSelectorQuery().select('.record-btn').boundingClientRect();
|
|
|
+ buttonRect.exec((rects) => {
|
|
|
+ const rect = rects[0];
|
|
|
+ if (!rect) return;
|
|
|
+
|
|
|
+ // 计算触摸点与按钮的垂直距离(向上移动超过50px视为取消)
|
|
|
+ const touchY = e.touches[0].clientY;
|
|
|
+ const buttonTop = rect.top;
|
|
|
+ if (touchY < buttonTop - 50) {
|
|
|
+ isCancel.value = true;
|
|
|
+ } else {
|
|
|
+ isCancel.value = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 取消录音(触摸中断)
|
|
|
+const cancelRecord = () => {
|
|
|
+ if (!isRecording.value) return;
|
|
|
+
|
|
|
+ clearInterval(recordTimer.value);
|
|
|
+ recorderManager.stop();
|
|
|
+ resetRecordState();
|
|
|
+ uni.showToast({ title: '已取消发送', icon: 'none' });
|
|
|
+};
|
|
|
+
|
|
|
+// 重置录音状态
|
|
|
+const resetRecordState = () => {
|
|
|
+ isRecording.value = false;
|
|
|
+ isCancel.value = false;
|
|
|
+ recordDuration.value = 0;
|
|
|
+ tempFilePath.value = '';
|
|
|
+};
|
|
|
+
|
|
|
+// 上传录音到服务器
|
|
|
+const uploadVoice = (filePath) => {
|
|
|
+ if (!filePath) return;
|
|
|
+
|
|
|
+ uni.showLoading({ title: '发送中...' });
|
|
|
+
|
|
|
+ // 调用上传接口
|
|
|
+ uni.uploadFile({
|
|
|
+ url: `${HTTP_REQUEST_URL}/mini/chat/file/voiceUpload`, // 替换为你的后端接口
|
|
|
+ filePath,
|
|
|
+ name: 'file', // 后端接收文件的参数名
|
|
|
+ formData: {
|
|
|
+ duration: recordDuration.value // 携带录音时长
|
|
|
+ },
|
|
|
+ method: 'POST',
|
|
|
+ header: {
|
|
|
+ "Authorization": appStore.token
|
|
|
+ },
|
|
|
+ success: (res) => {
|
|
|
+ console.log('上传结果:', res);
|
|
|
+ const result = JSON.parse(res.data);
|
|
|
+ if (result.code === 200) {
|
|
|
+ // 上传成功,添加到消息列表
|
|
|
+ sendMessage({ chatType:0,msgContent:result.data });
|
|
|
+ // messages.value.push({
|
|
|
+ // id: Date.now(),
|
|
|
+ // isMine: true,
|
|
|
+ // avatar: '/static/avatar/user.png',
|
|
|
+ // duration: recordDuration.value,
|
|
|
+ // url: result.data.url, // 服务器返回的音频地址
|
|
|
+ // isPlaying: false
|
|
|
+ // });
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: '发送失败', icon: 'none' });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('语音上传失败:', err);
|
|
|
+ uni.showToast({ title: '发送失败', icon: 'none' });
|
|
|
+ },
|
|
|
+ complete: () => {
|
|
|
+ uni.hideLoading();
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.mine_ybt_title{
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+/* 录音区域 */
|
|
|
+.record-btn{
|
|
|
+ flex: 1;
|
|
|
+ height: 70rpx;
|
|
|
+ line-height: 70rpx;
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #333;
|
|
|
+
|
|
|
+}
|
|
|
+.record-area {
|
|
|
+ padding: 30rpx;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.record-btn {
|
|
|
+ flex: 1;
|
|
|
+ height: 70rpx;
|
|
|
+ line-height: 70rpx;
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #333;
|
|
|
+ background-color: #f2f2f2;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.record-btn.recording {
|
|
|
+ background-color: #ff4d4f;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.record-btn.cancel {
|
|
|
+ background-color: #999;
|
|
|
+}
|
|
|
+/* 整体容器样式 */
|
|
|
+.chat-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100vh;
|
|
|
+ // padding-bottom: 200rpx;
|
|
|
+ /* background-color: #f5f5f5; */
|
|
|
+}
|
|
|
+
|
|
|
+/* 聊天消息区域样式 */
|
|
|
+.chat-messages {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ .scrollViewRef{
|
|
|
+ // height: 100%;
|
|
|
+ padding: 16rpx;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+ // height: calc(100vh - 200rpx);
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+/* 输入区域样式 */
|
|
|
+.input-area {
|
|
|
+ // position: fixed;
|
|
|
+ // left: 0;
|
|
|
+ // bottom: 0;
|
|
|
+ // width: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background-color: #fff;
|
|
|
+ padding: 30rpx 40rpx;
|
|
|
+ border-top: 1rpx solid #e0e0e0;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+.ai-tip{
|
|
|
+ position: absolute;
|
|
|
+ bottom: 5rpx;
|
|
|
+ width: calc(100% - 80rpx);
|
|
|
+ text-align: center;
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图片图标样式 */
|
|
|
+.image-btn{
|
|
|
+ margin-right: 30rpx;
|
|
|
+ width: 40rpx;
|
|
|
+ height: 40rpx;
|
|
|
+ &[disabled]{
|
|
|
+ .image-icon{
|
|
|
+ opacity: 0.4;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .image-icon {
|
|
|
+ width: 100%;
|
|
|
+ height:100%;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 输入框样式 */
|
|
|
+.input-box {
|
|
|
+ flex: 1;
|
|
|
+ height: 70rpx;
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #333;
|
|
|
+ background-color: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+/* 麦克风图标样式 */
|
|
|
+.isvoice-btn{
|
|
|
+ width: 40rpx;
|
|
|
+ height: 40rpx;
|
|
|
+ margin: 0 40rpx;
|
|
|
+ &[disabled]{
|
|
|
+ .mic-icon{
|
|
|
+ opacity: 0.4;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .mic-icon {
|
|
|
+ width: 100%;
|
|
|
+ height:100%;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 发送按钮样式 */
|
|
|
+.send-btn {
|
|
|
+ width: 60rpx;
|
|
|
+ height: 60rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 0;
|
|
|
+ &[disabled]{
|
|
|
+ .send-icon{
|
|
|
+ opacity: 0.4;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 发送图标样式 */
|
|
|
+.send-icon {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+/* 录音提示 */
|
|
|
+.record-toast {
|
|
|
+ position: fixed;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ width: 300rpx;
|
|
|
+ height: 300rpx;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ background-color: rgba(0, 0, 0, 0.7);
|
|
|
+ color: #fff;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 999;
|
|
|
+}
|
|
|
+
|
|
|
+.record-toast.cancel {
|
|
|
+ background-color: rgba(255, 77, 79, 0.8);
|
|
|
+}
|
|
|
+
|
|
|
+.toast-icon {
|
|
|
+ width: 200rpx;
|
|
|
+ height: 200rpx;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+</style>
|