Ai_GirlFriend/xuniYou/pages/chat/index.vue
2026-02-01 11:05:35 +08:00

2605 lines
65 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view>
<view class="body">
<view class="body_content">
<uni-nav-bar fixed statusBar background-color="transparent" :border="false" color="#ffffff" right-icon="gear"
@clickRight="setUp" :showMenuButtonWidth="true">
<!-- 左侧插槽 -->
<template v-slot:left>
<view class="custom_left">
<view class="left_content">
<image class="left_return" @click="back" src="/static/images/chat_return.png" mode="widthFix">
</image>
<view class="left_module">
<image class="left_avatar"
:src="loverBasicList.image_url ? loverBasicList.image_url : '/static/images/avatar.png'"
mode="aspectFill"></image>
<view class="left_count">
<view class="left_dight faj" @click="tointimacy">
<image src="/static/images/intimacy_logo.png" mode="widthFix"></image>
<text class="faj">{{ level ? level : '0' }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
</uni-nav-bar>
<image class="back"
:src="chatBackground"
mode="aspectFill"></image>
<scroll-view class="list" scroll-y="true" :scroll-top="scrollTop" scroll-with-animation="true"
@scrolltoupper="onScrollToUpper">
<!-- 聊天消息列表 -->
<view class="message-list">
<!-- 动态渲染消息列表 -->
<view v-for="(item, index) in sessionInitList.messages" :key="index" class="message-item"
:class="{ 'left-message': item.role === 'lover', 'right-message': item.role !== 'lover' }">
<image class="avatar" :src="item.role === 'lover'
? (loverBasicList.image_url ? loverBasicList.image_url : '/static/images/avatar.png')
: (myavatar ? myavatar : '/static/images/avatar.png')" mode="aspectFill">
</image>
<view class="message-content">
<view class="message-bubble">
<!-- 视频消息特殊处理 -->
<view v-if="isVideoMessage(item.content)" class="video-message-container">
<!-- 提取并显示文本部分 -->
<view class="message-text">{{ extractTextFromVideoMessage(item.content) }}</view>
<!-- 视频部分 -->
<view class="message-video-container">
<!-- #ifdef APP -->
<view v-html="item.contentVideo"></view>
<!-- #endif -->
<!-- #ifdef MP -->
<video class="message-video" :src="extractVideoUrlFromMessage(item.content)"
@play="onVideoPlay(item.id)" @pause="onVideoPause(item.id)"
@longpress="onVideoLongPress(item)"></video>
<!-- #endif -->
<!-- {{ item.contentVideo }} -->
</view>
</view>
<view v-else-if="isVideoMessage1(item.content)" class="video-message-container">
<!-- 提取并显示文本部分 -->
<view class="message-text">{{ item.content }}</view>
<!-- 视频部分 -->
<view class="">
<uni-icons class="spinner-cycle-box" type="spinner-cycle" size="28"></uni-icons>
</view>
</view>
<!-- 普通图片消息 -->
<view v-else-if="isImageUrl(item.content)" class="message-image-container">
<image class="message-image" :src="item.content" mode="aspectFit"
@click="previewImage(item.content)"></image>
</view>
<!-- 其他视频URL消息非格式化消息 -->
<view v-else-if="isVideoUrl(item.content)" class="message-video-container">
<video class="message-video" :src="item.content" @play="onVideoPlay(item.id)"
@pause="onVideoPause(item.id)" @longpress="onVideoLongPress(item)"></video>
</view>
<!-- 普通文本消息 -->
<view v-else class="message-text">{{ item.content }}</view>
<!-- 语音容器独立存在仅对lover角色显示 -->
<view
v-if="item.role === 'lover' && !isVideoMessage(item.content) && !isVideoMessage1(item.content) && !isVideoUrl(item.content)"
class="voice-container" @click="playVoice(item.id)">
<view class="voice-wave"
:class="{ 'playing': currentPlayingId === item.id, 'black-wave': currentPlayingId !== item.id }">
<view class="wave-bar wave-bar-1"></view>
<view class="wave-bar wave-bar-2"></view>
<view class="wave-bar wave-bar-3"></view>
<view class="wave-bar wave-bar-4"></view>
<view class="wave-bar wave-bar-5"></view>
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部按钮区域 -->
<view class="textbox fa">
<image class="textbox_voice" src="/static/images/chat_voice.png" mode="widthFix" @click="alertState = true">
</image>
<view class="btn_content" style="display: flex;align-items: center;">
<view class="btn_module faj" @click="chatSingClick(1)">
<image class="textbox_image" src="/static/images/chat_a6.png" mode="widthFix"></image>
</view>
<view class="btn_module faj" @click="chatSingClick(2)">
<image class="textbox_image" style="background-color: #fff;border-radius: 10rpx;"
src="/static/images/chat_a7.png" mode="widthFix"></image>
</view>
</view>
<view class="textbox_content fa f1">
<input class="f1" v-model="form.message" placeholder="与她聊天" placeholder-class="textbox_input"
@confirm="sendMessage" confirm-type="send" :cursor-spacing="15" />
<image class="textbox_add" src="/static/images/chat_add.png" mode="widthFix" @click="toggleBottomBtns">
</image>
</view>
<view class="textbox_send faj" @click="sendingMessage">发送</view>
</view>
<view class="black"></view>
</view>
<!-- 底部按钮区域,通过 v-show 控制显示/隐藏 -->
<view class="btn fa sb" v-if="showBottomBtns">
<view class="btn_content">
<view class="btn_module faj" @click="pictureClick">
<image class="btn_image" src="/static/images/chat_a1.png" mode="widthFix"></image>
<image class="btn_lock" v-if="level < 3" src="/static/images/chat_lock.png" mode="widthFix">
</image>
</view>
<view class="btn_title">照片</view>
</view>
<view class="btn_content" @click="togift">
<view class="btn_module faj">
<image class="btn_image" src="/static/images/chat_a2.png" mode="widthFix"></image>
</view>
<view class="btn_title">礼物</view>
</view>
<view class="btn_content">
<view class="btn_module faj" @click="tochatPhone">
<image class="btn_image" src="/static/images/chat_a3.png" mode="widthFix"></image>
<image class="btn_lock" v-if="level < 2" src="/static/images/chat_lock.png" mode="widthFix">
</image>
</view>
<view class="btn_title">通话</view>
</view>
<!-- <view class="btn_content">
<view class="btn_module faj" @click="tochatDiary">
<image class="btn_image" src="/static/images/chat_a4.png" mode="widthFix"></image>
<image class="btn_lock" v-if="level < 5" src="/static/images/chat_lock.png" mode="widthFix">
</image>
</view>
<view class="btn_title">日记</view>
</view> -->
<!-- <view class="btn_content" @click="chatSingClick">
<view class="btn_module faj">
<image class="btn_image" src="/static/images/chat_a6.png" mode="widthFix"></image>
<image class="btn_lock" v-if="level < 5" src="/static/images/chat_lock.png" mode="widthFix">
</image>
</view>
<view class="btn_title">载歌载舞</view>
</view> -->
</view>
</view>
<!-- 录音弹框 -->
<view class="alert" v-if="alertState">
<view class="alert_hide" @click="alertState = false"></view>
<view v-if="videoState" class="alert_opt fa sb">
<view class="alert_a1"></view>
<view class="alert_a2"></view>
<view class="alert_a3"></view>
<view class="alert_a4"></view>
<view class="alert_a5"></view>
<view class="alert_a5"></view>
<view class="alert_a4"></view>
<view class="alert_a3"></view>
<view class="alert_a2"></view>
<view class="alert_a1"></view>
</view>
<view class="alert_module">
<view class="alert_title">按住说话</view>
<view @touchstart='touchStart' @touchend='touchEnd' class="alert_btn faj">
<image src="../../static/images/chat_text.png" />
</view>
</view>
</view>
<!-- 唱歌/跳舞弹框提示 -->
<view class="chatSing faj" v-if="chatSingStats">
<view class="chatSing_content">
<image src="/static/images/close.png" mode="widthFix" @click="closechatSing()"></image>
<view class="chatSing_detail fa">
<input type="nickchatSing" class="f1" v-model="messageprompt" placeholder="请输入跳舞详情"
placeholder-class="chatSing_input" />
</view>
<view class="chatSing_sure faj"><text class="faj" @click="savechatSing()">确定</text></view>
</view>
</view>
<!-- 歌曲列表 -->
<view class="chatSing faj" v-if="chatSingStats1">
<view class="" style="position: relative;background: #FFFFFF;border-radius: 20rpx;padding: 60rpx;">
<image src="/static/images/close.png" mode="widthFix"
style="position: absolute;right: 20rpx;top: 20rpx;width: 30rpx;height: 30rpx;"
@click="chatSingStats1 = false"></image>
<view class="" style="width: 70vw;max-height: 100vw;overflow: auto;margin-top: 40rpx;">
<view class="fa" style="justify-content: space-between;margin-bottom: 30rpx;"
v-for="(item,index) in singSongsList" :key="index" @click="selectSong(item,index)">
<view class="" style="font-size: 28rpx;">{{item.title}}</view>
<image style="width: 40rpx;height: 40rpx;"
:src="item.id == songId?'/static/images/selectA.png':'/static/images/select.png'" mode=""></image>
</view>
</view>
<view class="chatSing_sure faj"><text class="faj" @click="savechatSing1()">确定</text></view>
</view>
</view>
<view class="bottom" v-if="bottomStats">
<view class="bottom_content">
<view class="bottom_title">发布动态
<image src="/static/images/close.png" @click="noClick()"></image>
</view>
<video class="bottom_video faj" :src="currentVideoUrl ? currentVideoUrl : ''" mode="widthFix"
v-if="currentVideoUrl"></video>
<view class="bottom_module">
<input class="f1" v-model="dynamicShareform.content" placeholder="请输入动态内容" placeholder-class="bottom_input" />
</view>
<view class="bottom_item fa sb">
<view class="bottom_btn faj" @click="noClick()">取消</view>
<view class="bottom_btn faj" @click="yesClick()">确认</view>
</view>
</view>
</view>
<ai ref="aiRef" @renderText="renderText" />
</view>
</template>
<script>
import {
SessionInit,
SessionSend,
AddBond,
SessionMessages,
ChatMessagesTts,
SessionSendImage,
DanceGenerate,
DanceGenerateTask,
DynamicShare,
SingSongs,
SingGenerate,
SingGenerateTask
} from '@/utils/api.js'
import notHave from '@/components/not-have.vue';
import topSafety from '@/components/top-safety.vue';
import ai from '@/components/ai.vue';
//#ifdef MP-WEIXIN
const plugin = requirePlugin('WechatSI');
console.log('plugin:', plugin)
const manager = plugin.getRecordRecognitionManager();
// #endif
export default {
components: {
notHave,
topSafety,
ai,
},
data() {
return {
// 可以在这里添加聊天数据
// messages: [],
loverBasicList: uni.getStorageSync('loverBasicList'),
myavatar: uni.getStorageSync('userinfo').avatar,
level: uni.getStorageSync('userinfo').level,
getMenuInfoList: '',
form: {
message: '',
session_id: '',
},
addBondform: {
type: '',
num: '',
},
sessionMessagesform: {
session_id: '',
page: 1,
size: 15,
},
chatMessagesTtsform: {
id: '',
},
sessionSendImageform: {
session_id: '',
image_url: '',
},
dynamicShareform: {
source_message_id: '',
content: '',
},
sessionInitList: '',
scrollTop: 0,
scrollTimer: null,
showBottomBtns: false, // 控制底部按钮显示/隐藏
loadingMore: false, // 控制是否正在加载更多
noMoreData: false, // 控制是否还有更多数据
lastScrollTop: 0, // 记录上次滚动位置
currentAudioContext: null, // 添加当前音频上下文实例
recorderManager: null, // 录音管理器
isRecording: false, // 是否正在录音
voiceStartTime: 0, // 录音开始时间
voiceCancel: false, // 是否取消录音
chatMessagesTtsList: '',
currentPlayingId: null, // 当前正在播放的语音ID
isPlaying: false, // 播放状态
alertState: false,
onState: true,
videoState: false,
shouldAutoScroll: true, // 添加自动滚动标志
chatSingStats: false, //跳舞
chatSingStats1: false, //唱歌
messageprompt: '',
bottomStats: false,
currentVideoUrl: '', // 添加当前视频URL变量
singSongsList: [], //歌曲列表
songId: 0,
chatBackground: '' // 聊天背景
}
},
onLoad() {
// #ifdef MP
this.initSI();
// #endif
},
onShow() {
// #ifdef MP-WEIXIN
this.getMenuInfo()
// #endif
this.sessionInit() //恋人聊天初始化
// #ifndef H5
this.initRecorder() //初始化录音管理器H5不支持
// #endif
this.getSingSongs() //歌曲列表
this.loadChatBackground() //加载聊天背景
// this.initAudio()
},
onUnload() {
this.stopCurrentAudio();
},
onHide() {
this.stopCurrentAudio();
},
methods: {
// 加载聊天背景
loadChatBackground() {
const savedBg = uni.getStorageSync('chat_background');
if (savedBg) {
this.chatBackground = savedBg;
} else {
// 默认背景
this.chatBackground = this.loverBasicList.image_url || 'https://nvlovers.oss-cn-qingdao.aliyuncs.com/uploads/20251226/39c6f8899c15f60fc59207835f95e07a.png';
}
},
initAudio() {
// 销毁旧的音频上下文(如果存在)
if (this.audioContext) {
this.audioContext.destroy();
}
// 创建新的音频上下文
this.audioContext = uni.createInnerAudioContext()
this.audioContext.volume = 1
// 设置音频源类型
this.audioContext.srcType = 'auto'
// 添加音频事件监听
this.audioContext.onError((err) => {
console.error('音频播放失败:', err)
console.error('错误详情:', JSON.stringify(err))
uni.showToast({
title: '音频播放失败',
icon: 'none'
})
})
this.audioContext.onPlay(() => {
console.log('音频开始播放')
})
this.audioContext.onEnded(() => {
console.log('音频播放结束')
})
this.audioContext.onCanplay(() => {
console.log('音频可以播放了')
})
},
initSI() {
const that = this;
// 有新的识别内容返回,则会调用此事件
manager.onRecognize = function(res) {
console.log(res);
};
// 正常开始录音识别时会调用此事件
manager.onStart = function(res) {
console.log('成功开始录音识别', res);
// 开始录音时-抖动一下手机
wx.vibrateShort({
type: 'medium'
});
};
// 识别错误事件
manager.onError = function(res) {
console.error('error msg', res);
const tips = {
'-30003': '说话时间间隔太短,无法识别语音',
'-30004': '没有听清,请再说一次~',
'-30011': '上个录音正在识别中,请稍后尝试',
};
const retcode = res?.retcode.toString();
retcode &&
wx.showToast({
title: tips[`${retcode}`],
icon: 'none',
duration: 2000,
});
};
//识别结束事件
manager.onStop = function(res) {
console.log('..............结束录音', res);
console.log('录音临时文件地址 -->', res.tempFilePath);
console.log('录音总时长 -->', res.duration, 'ms');
console.log('文件大小 --> ', res.fileSize, 'B');
console.log('语音内容 --> ', res.result);
if (res.result === '') {
wx.showModal({
title: '提示',
content: '没有听清,请再说一次~',
showCancel: false,
});
return;
}
that.form.message = res.result
// that.audioContext.src = res.tempFilePath;
// that.audioContext.play();
};
},
renderText(res){
console.log('renderText返回的',res)
this.form.message = res
},
touchStart() {
this.videoState = true;
// #ifdef MP
// 语音识别开始
manager.start({
duration: 30000,
lang: 'zh_CN',
});
// #endif
// #ifdef APP
// this.recorderManager.start({
// format: 'mp3',
// sampleRate: 16000,
// numberOfChannels: 1
// });
this.$refs.aiRef.openMedia();
// #endif
},
// 手指触摸动作-结束录制
touchEnd() {
this.videoState = false;
this.alertState = false;
// #ifdef MP
// 语音识别结束
manager.stop();
// #endif
// #ifdef APP
// this.recorderManager.stop();
this.$refs.aiRef.stopMedia();
// #endif
},
// 恋人聊天初始化
sessionInit() {
SessionInit().then(res => {
if (res.code == 1) {
this.sessionInitList = res.data
console.log(this.sessionInitList)
this.sessionInitList.messages.forEach((item,index)=>{
let src = this.extractVideoUrlFromMessage(item.content)
console.log(src)
item.contentVideo = `<video src="${src}" :webkit-playsinline="false" controls initialTime="1.5s" style="width:300rpx;height:200px;border-radius: 10px;"></video>`
})
this.form.session_id = res.data.session_id
this.sessionMessagesform.session_id = res.data.session_id
this.sessionSendImageform.session_id = res.data.session_id
// 只在初始化时滚动到底部
this.$nextTick(() => {
this.scrollToBottom();
})
} else {
uni.showToast({
title: res.msg,
icon: 'none',
position: 'top'
})
}
})
},
sessionSend() {
SessionSend(this.form).then(res => {
if (res.code == 1) {
uni.showToast({
title: '发送成功',
icon: 'none',
position: 'top'
})
this.addBond()
this.form.message = '';
this.form.session_id = '';
this.$refs.aiRef.renderText = ''
this.$refs.aiRef.resultTextTemp = ''
this.$refs.aiRef.renderText = ''
// 重新获取会话数据以包含AI回复这会自动滚动到底部
// 为确保滚动到最新消息,我们稍微延迟一下滚动
this.refreshSessionData(true); // 传递参数表示需要滚动到底部
} else {
uni.showToast({
title: res.msg,
icon: 'none',
position: 'top'
})
}
})
},
chatMessagesTts() {
ChatMessagesTts(this.chatMessagesTtsform.id).then(res => {
console.log('TTS文字转语音:', res)
if (res.code == 1) {
this.chatMessagesTtsList = res.data
this.chatMessagesTtsform.id = ''
this.chatMessagesTtsPlay(this.chatMessagesTtsList.tts_url)
} else {
uni.showToast({
title: res.msg,
icon: 'none',
position: 'top'
})
}
})
},
sessionSendImage() {
SessionSendImage(this.sessionSendImageform).then(res => {
if (res.code == 1) {
uni.showToast({
title: '发送成功',
icon: 'none',
position: 'top'
})
this.sessionSendImageform.session_id = ''
this.sessionSendImageform.image_url = ''
this.addBond()
// 重新获取会话数据以包含AI回复这会自动滚动到底部
// 为确保滚动到最新消息,我们稍微延迟一下滚动
this.refreshSessionData(true); // 传递参数表示需要滚动到底部
} else {
uni.showToast({
title: res.msg,
icon: 'none',
position: 'top'
})
}
})
},
// 生成跳舞视频
danceGenerate() {
let that = this
DanceGenerate({
prompt: this.messageprompt
}).then(res => {
if (res.code == 1) {
this.messageprompt = ''
// this.addBond()
// // 重新获取会话数据以包含AI回复这会自动滚动到底部
// // 为确保滚动到最新消息,我们稍微延迟一下滚动
this.refreshSessionData(true); // 传递参数表示需要滚动到底部
// 轮询监听视频生成任务结果
that.pollImageSegmentResult(res.data.generation_task_id)
} else {
uni.showToast({
title: res.msg,
icon: 'none',
position: 'top'
})
}
})
},
// 轮询监听任务结果
async pollImageSegmentResult(job_id) {
const that = this;
// 重置轮询计数
that.pollingAttempts = 0;
// 如果已有轮询在运行,先清除
if (that.pollingTimer) {
clearTimeout(that.pollingTimer);
that.pollingTimer = null;
}
// 定义轮询函数
const doPoll = () => {
that.pollingAttempts++;
// 检查是否超过最大轮询次数
if (that.pollingAttempts > 20) {
uni.hideLoading();
uni.showToast({
title: '处理超时,请稍后重试',
icon: 'none'
});
that.pollingTimer = null;
return;
}
// 请求接口
DanceGenerateTask(job_id).then(res => {
console.log('视频生成查询结果:', res)
console.log(`第 ${that.pollingAttempts} 次轮询,任务结果:`, res);
if (res.code == 1) {
const data = res.data;
// status "RUN" 表示视频生成中,"succeeded" 表示视频生成成功
if (data.status == 'succeeded') {
// 任务成功,停止轮询
uni.hideLoading();
// that.result_url = data.ResultVideoUrl
this.addBond()
// // 重新获取会话数据以包含AI回复这会自动滚动到底部
// // 为确保滚动到最新消息,我们稍微延迟一下滚动
this.refreshSessionData(true); // 传递参数表示需要滚动到底部
that.pollingTimer = null;
} else if (data.status == 'failed') {
// 任务失败
uni.hideLoading();
uni.showToast({
title: data.error_msg,
icon: 'none'
});
that.pollingTimer = null;
} else {
// 任务处理中,继续轮询
console.log(`任务处理中 (${data.state}),${4000 / 1000}秒后重新查询...`);
that.pollingTimer = setTimeout(doPoll, 4000);
}
} else {
uni.hideLoading()
uni.showToast({
title: res.data,
icon: 'none',
duration: 5000
})
}
})
};
// 启动轮询
console.log('开始轮询监听视频生成任务...');
doPoll();
},
chatMessagesTtsPlay(url) {
// 如果当前有正在播放的音频,先停止它
if (this.currentAudioContext) {
this.currentAudioContext.stop();
this.currentAudioContext.destroy(); // 释放资源
}
// 创建新的音频上下文
this.currentAudioContext = uni.createInnerAudioContext();
this.currentAudioContext.src = url; // 设置音频源
// 播放音频
this.currentAudioContext.play();
// 监听播放结束事件,播放结束后清空当前音频上下文
this.currentAudioContext.onEnded(() => {
console.log('音频播放结束');
if (this.currentAudioContext) {
this.currentAudioContext.destroy();
this.currentAudioContext = null;
}
this.currentPlayingId = null; // 清除播放状态
this.isPlaying = false; // 设置播放状态为false
});
// 监听播放错误事件
this.currentAudioContext.onError((err) => {
console.error('音频播放失败:', err);
if (this.currentAudioContext) {
this.currentAudioContext.destroy();
this.currentAudioContext = null;
}
this.currentPlayingId = null; // 清除播放状态
this.isPlaying = false; // 设置播放状态为false
uni.showToast({
title: '播放失败',
icon: 'none'
});
});
},
stopCurrentAudio() {
if (this.currentAudioContext) {
this.currentAudioContext.stop();
this.currentAudioContext.destroy();
this.currentAudioContext = null;
}
this.currentPlayingId = null;
this.isPlaying = false;
},
// 专门用于刷新会话数据的方法,可选择是否滚动到底部
refreshSessionData(shouldScrollToBottom = false) {
SessionInit().then(res => {
if (res.code == 1) {
const oldMessageCount = this.sessionInitList.messages ? this.sessionInitList.messages.length : 0;
this.sessionInitList = res.data
// 重新为所有消息生成contentVideo属性
this.sessionInitList.messages.forEach((item,index)=>{
let src = this.extractVideoUrlFromMessage(item.content)
item.contentVideo = `<video src="${src}" controls initialTime="1.5s" style="width:300rpx;height:200px;border-radius: 10px;"></video>`
})
this.form.session_id = res.data.session_id
this.sessionMessagesform.session_id = res.data.session_id
// 确保在数据更新后再滚动到底部
this.$nextTick(() => {
if (shouldScrollToBottom) {
// 延迟滚动以确保新消息已渲染
setTimeout(() => {
this.scrollToBottom();
}, 150);
}
})
}
})
},
onScrollToUpper() {
// 防止重复加载
if (this.loadingMore || this.noMoreData) {
return;
}
// 开始加载更多数据
this.loadMoreMessages();
},
loadMoreMessages() {
if (this.noMoreData) return;
this.loadingMore = true;
// 更新页码
this.sessionMessagesform.page += 1;
SessionMessages(this.sessionMessagesform).then(res => {
if (res.code == 1) {
if (res.data && res.data.messages && res.data.messages.length > 0) {
// 将新消息添加到现有消息列表的前面(因为是向上加载历史消息)
const newMessages = res.data.messages;
this.sessionInitList.messages = [...newMessages, ...this.sessionInitList.messages];
// 检查是否还有更多数据
if (!res.data.has_more) {
this.noMoreData = true;
}
} else {
// 没有更多数据
this.noMoreData = true;
}
} else {
uni.showToast({
title: res.msg,
icon: 'none',
position: 'top'
});
// 出错时页码回退
this.sessionMessagesform.page -= 1;
}
}).catch(err => {
console.error('加载更多消息失败:', err);
// 出错时页码回退
this.sessionMessagesform.page -= 1;
}).finally(() => {
this.loadingMore = false;
});
},
addBond() {
AddBond(this.addBondform).then(res => {
console.log('添加', res)
if (res.code == 1) {
this.addBondform.type = '';
this.addBondform.num = '';
} else {
uni.showToast({
title: res.msg,
icon: 'none',
position: 'top'
})
}
})
},
sessionMessages() {
SessionMessages(this.sessionMessagesform).then(res => {
if (res.code == 1) {
} else {
uni.showToast({
title: res.msg,
icon: 'none',
position: 'top'
})
}
})
},
sendMessage() {
// 检查消息是否为空
if (this.form.message == '') {
uni.showToast({
title: '请输入消息内容',
icon: 'none'
});
return;
}
uni.showLoading({
title: '发送中...'
});
this.addBondform.type = 1;
this.addBondform.num = 1;
// 发送消息的API调用
this.sessionSend();
},
scrollToBottom() {
this.$nextTick(() => {
// 获取最新的消息元素并滚动到它
const lastIndex = this.sessionInitList.messages.length - 1;
if (lastIndex >= 0) {
setTimeout(() => {
const query = uni.createSelectorQuery();
query.select('.message-list').boundingClientRect();
query.selectViewport().scrollOffset();
query.exec((res) => {
// 直接滚动到容器底部
this.scrollTop = 999999; // 设置一个足够大的值
});
}, 100);
}
});
},
// 在滚动事件中控制是否需要自动滚动
onScroll(e) {
const currentScrollTop = e.detail.scrollTop;
// 如果用户主动向上滚动查看历史消息,禁用自动滚动
if (currentScrollTop > this.lastScrollTop) {
this.shouldAutoScroll = false;
} else {
// 当用户滚动回底部附近时,重新启用自动滚动
if (currentScrollTop < 100) {
this.shouldAutoScroll = true;
}
}
this.lastScrollTop = currentScrollTop;
},
pictureClick() {
// 检查用户等级是否大于等于3级
// if (this.level < 3) {
// uni.showToast({
// title: '达到Lv.3才可以解锁发送图片',
// icon: 'none',
// position: 'top'
// });
// return; // 如果等级不足,直接返回,不执行后续操作
// }
uni.chooseImage({
count: 1, // 最多可以选择的图片张数默认1
sourceType: ['camera', 'album'], // 选择图片的来源,相机或相册
success: (res) => {
console.log(res.tempFilePaths[0])
uni.uploadFile({
url: this.baseURL + '/api/common/upload',
header: {
token: uni.getStorageSync("token") || "",
'content-type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
filePath: res.tempFilePaths[0],
name: 'file',
}).then((res) => {
let data = JSON.parse(res.data)
let item = {
url: data.data.fullurl
}
console.log(item)
this.sessionSendImageform.image_url = item.url
this.addBondform.type = 1;
this.addBondform.num = 1;
this.sessionSendImage()
}).catch(
(response) => {
console.error('网络请求失败', response);
}
)
},
fail: function(error) {
console.log(error);
}
});
},
sendGift() {
uni.navigateTo({
url: '/pages/index/gift'
});
},
back() {
uni.navigateBack({
delta: 1,
});
},
setUp() {
uni.navigateTo({
url: '/pages/chat/setUp?session_id=' + this.form.session_id
})
},
tointimacy() {
uni.navigateTo({
url: '/pages/index/intimacy'
})
},
togift() {
uni.navigateTo({
url: '/pages/index/gift'
});
},
tochatPhone() {
// if (this.level >= 2) {
// uni.navigateTo({
// url: '/pages/chat/chatPhone'
// });
// } else {
// uni.showToast({
// title: '达到Lv.2才可以解锁通话',
// icon: 'none',
// position: 'top'
// })
// }
uni.navigateTo({
url: '/pages/chat/phone'
});
},
tochatDiary() {
// if (this.level >= 5) {
// uni.navigateTo({
// url: '/pages/chat/chatDiary'
// });
// } else {
// uni.showToast({
// title: '达到Lv.5才可以解锁日记',
// icon: 'none',
// position: 'top'
// })
// }
uni.navigateTo({
url: '/pages/chat/chatDiary'
});
},
getMenuInfo() {
const menuButtonInfo = uni.getMenuButtonBoundingClientRect();
const systemInfo = uni.getSystemInfoSync();
// 胶囊宽度
const capsuleWidth = menuButtonInfo.width;
// 胶囊距离右侧的距离 = 屏幕宽度 - 胶囊右边界的x坐标
const distanceFromRight = systemInfo.windowWidth - menuButtonInfo.right;
this.getMenuInfoList = capsuleWidth + distanceFromRight;
return {
width: capsuleWidth,
distanceFromRight: distanceFromRight,
menuButtonInfo: menuButtonInfo
};
},
toggleBottomBtns() {
// 切换底部按钮显示/隐藏状态
this.showBottomBtns = !this.showBottomBtns;
},
sendingMessage() {
// 检查消息是否为空
if (this.form.message == '') {
uni.showToast({
title: '请输入消息内容',
icon: 'none'
});
return;
}
uni.showLoading({
title: '发送中...'
});
this.addBondform.type = 1;
this.addBondform.num = 1;
// 发送消息的API调用
this.sessionSend();
},
playVoice(id) {
// 如果点击的是当前正在播放的语音,则停止播放
if (this.currentPlayingId === id && this.isPlaying) {
this.stopCurrentAudio();
return;
}
this.chatMessagesTtsform.id = id;
this.currentPlayingId = id; // 设置当前播放ID
this.isPlaying = true; // 设置播放状态
this.chatMessagesTts();
},
// 初始化录音管理器
initRecorder() {
// #ifdef H5
console.log('H5环境不支持录音管理器');
return;
// #endif
this.recorderManager = uni.getRecorderManager();
console.log('this.recorderManager', )
this.recorderManager.onStart(() => {
console.log('录音开始');
this.isRecording = true;
this.voiceStartTime = Date.now();
this.voiceCancel = false;
});
this.recorderManager.onPause(() => {
console.log('录音暂停');
});
this.recorderManager.onResume(() => {
console.log('录音继续');
});
this.recorderManager.onStop((res) => {
console.log('录音结束', res);
this.isRecording = false;
const duration = Date.now() - this.voiceStartTime;
// 如果录音时间太短,取消发送
if (duration < 1000) {
uni.showToast({
title: '录音时间太短',
icon: 'none'
});
return;
}
if (!this.voiceCancel) {
console.log('处理录音文件',)
// 处理录音文件
this.handleVoiceRecord(res.tempFilePath,res.fileSize);
}
});
this.recorderManager.onError((res) => {
console.error('录音失败', res);
this.isRecording = false;
uni.showToast({
title: '录音失败: ' + res.errMsg,
icon: 'none'
});
});
},
// 切换语音输入模式
toggleVoiceInput() {
if (this.isRecording) {
// 如果正在录音,则停止录音
this.stopRecording();
} else {
// 开始录音
this.startRecording();
}
},
// 开始录音
startRecording() {
const options = {
duration: 60000, // 最大录音时长单位ms
sampleRate: 16000, // 采样率
numberOfChannels: 1, // 录音通道数
encodeBitRate: 48000, // 编码码率
format: 'mp3', // 音频格式
frameSize: 50, // 指定帧大小,单位 KB
};
this.recorderManager.start(options);
uni.showToast({
title: '开始录音,请说话...',
icon: 'none'
});
},
// 停止录音
stopRecording() {
this.recorderManager.stop();
},
async getAccessToken(apiKey, secretKey) {
const result = await uni.request({
url: `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${apiKey}&client_secret=${secretKey}`,
method: 'POST'
});
console.log('result', result)
return result.data.access_token;
},
async readFile(filePath) {
console.log('111',filePath) //_doc/uniapp_temp_1768467761590/recorder/1768467765566.wav
const fs = uni.getFileSystemManager();
// 将回调式API转换为Promise式API
return new Promise((resolve, reject) => {
fs.readFile({
filePath: filePath,
encoding: 'base64',
success: (res) => {
console.log('readFile success:', res)
resolve(res);
},
fail: (err) => {
console.error('readFile fail:', err)
reject(err);
}
});
});
},
// App 端专用:使用 plus.io 读取录音文件为 Base64
async readFileAsBase64(filePath) {
// 1. 去掉 file:// 前缀App 端路径是 file:// 开头的)
let path = filePath.replace('file://', '');
console.log('读取文件路径:', path)
return new Promise((resolve, reject) => {
// 2. 使用 plus.io 请求文件系统
plus.io.requestFileSystem(
plus.io.PUBLIC_DOWNLOADS, // 使用公共下载目录(根据实际情况调整)
(fs) => {
// 3. 获取文件路径
fs.root.getFile(
path,
{ create: false },
(fileEntry) => {
// 4. 创建文件读取器
fileEntry.file((file) => {
const reader = new plus.io.FileReader();
// 5. 设置读取完成的回调
reader.onloadend = (e) => {
console.log('文件读取完成',e);
// 6. 获取 Base64 数据(去掉 data:xxx/xxx;base64, 前缀)
const base64 = e.target.result.split(',')[1];
console.log('base64',base64);
resolve(base64);
};
// 7. 设置读取错误的回调
reader.onerror = (e) => {
reject(`文件读取失败: ${e.target.error.message}`);
};
// 8. 以 DataURL 格式读取文件(自动转换为 Base64
reader.readAsDataURL(file);
}, (e) => {
reject(`获取文件失败: ${e.message}`);
});
}, (e) => {
reject(`获取文件入口失败: ${e.message}`);
}
);
}, (e) => {
reject(`请求文件系统失败: ${e.message}`);
}
);
});
},
// 处理录音记录
async handleVoiceRecord(tempFilePath,fileSize) {
console.log('录音文件路径:', tempFilePath,'fileSize',fileSize);
// 显示加载提示
uni.showLoading({
title: '正在识别语音...'
});
try {
// 1. 获取访问令牌
const token = await this.getAccessToken('OquhIAdglSa2oescqXH7ZUmC', 'LOylGQlc5M895MoXq27zGLT2tmbtHcMl');
console.log('token', token)
// 2. 读取音频文件
// const fileData = await this.readFile(tempFilePath);
const fileData = await this.readFileAsBase64(tempFilePath);
console.log('fileData:', fileData)
// 3. 调用百度语音识别API
// 确保speech参数是正确的base64编码字符串
// const speechData = typeof fileData.data === 'string' ? fileData.data.trim() : '';
let a = uni.getSystemInfoSync().deviceId
console.log('a:',a)
// 调用百度语音识别API
const cuid = uni.getSystemInfoSync().deviceId || 'unknown_device';
console.log('token:', token);
console.log('cuid:', cuid);
console.log('fileData长度:', fileData.length);
console.log('录音格式:', 'mp3');
const result = await uni.request({
url: `https://vop.baidu.com/server_api`,
data: {
'format': 'mp3', // 与录音格式保持一致
'rate': 16000,
'channel': 1,
'len': fileSize, // 使用正确的文件大小
'speech': fileData,
'cuid': cuid,
'token': token // 只在JSON数据中传递token
},
method: 'POST',
header: {
'Content-Type': 'application/json' // 百度语音API要求JSON格式
}
});
console.log('语音识别结果:', result)
// 4. 处理识别结果
if (result.data && result.data.err_no === 0) {
// 识别成功
const voiceText = result.data.result[0];
console.log('识别到的文本:', voiceText);
// 将识别结果设置到输入框
this.form.message = voiceText;
// 自动发送消息
this.sendMessage();
} else {
// 识别失败
const errorMsg = result.data?.err_msg || '识别失败';
console.error('语音识别失败:', errorMsg);
uni.showToast({
title: `识别失败: ${errorMsg}`,
icon: 'none',
duration: 3000
});
}
} catch (error) {
// 处理异常
console.error('语音处理失败:', error);
uni.showToast({
title: '语音处理失败,请重试',
icon: 'none',
duration: 3000
});
} finally {
// 隐藏加载提示
uni.hideLoading();
}
},
initWebSocket(token, tempFilePath) {
console.log('initWebSocket', token, tempFilePath)
const ws = new WebSocket(`wss://vop.baidu.com/websocket_api?token=${token}`);
console.log('ws', ws)
ws.onopen = () => {
console.log('WebSocket连接已建立');
// 1. 发送开始识别的配置信息
const params = {
"common": {
"app_id": 7404135,
},
"business": {
"language": "zh",
"domain": "general",
"accent": "mandarin",
"format": "mp3"
},
"data": {
"status": 0
}
};
ws.send(JSON.stringify(params));
console.log('已发送配置信息');
// 2. 读取并分块发送语音数据
const fs = uni.getFileSystemManager();
// 使用异步方式获取文件信息
fs.getFileInfo({
filePath: tempFilePath,
success: (res) => {
const fileSize = res.size;
const chunkSize = 8192; // 8KB每块
let offset = 0;
console.log('开始发送语音数据,文件大小:', fileSize);
// 定义发送下一块的函数
const sendNextChunk = () => {
if (offset >= fileSize) {
console.log('语音数据发送完成');
return;
}
const end = Math.min(offset + chunkSize, fileSize);
try {
// 使用同步方式读取文件块
const chunk = fs.readFileSync(tempFilePath, {
position: offset, // uni-app中使用position而非offset
length: end - offset,
encoding: 'base64'
});
// 发送语音数据块
const dataParams = {
"data": {
"status": offset + chunkSize >= fileSize ? 2 : 1,
"format": "mp3",
"audio": chunk
}
};
ws.send(JSON.stringify(dataParams));
offset += chunkSize;
// 短暂延迟,避免发送过快
setTimeout(sendNextChunk, 10);
} catch (error) {
console.error('读取文件块失败:', error);
ws.close();
}
};
// 开始发送第一块
sendNextChunk();
},
fail: (error) => {
console.error('获取文件信息失败:', error);
ws.close();
}
});
};
ws.onmessage = (e) => {
console.log('收到WebSocket消息:', e.data);
try {
const data = JSON.parse(e.data);
// 检查是否有错误
if (data.err_no !== 0) {
console.error('识别出错:', data.err_msg);
uni.showToast({
title: '语音识别失败',
icon: 'none'
});
return;
}
// 处理识别结果
if (data.result) {
console.log('识别结果:', data.result);
// 将识别结果设置为消息内容
this.form.message = data.result;
// 自动发送消息
this.sendMessage();
}
} catch (error) {
console.error('解析WebSocket消息失败:', error);
}
};
ws.onerror = (error) => {
console.error('WebSocket连接错误:', error);
uni.showToast({
title: '语音识别连接失败',
icon: 'none'
});
};
ws.onclose = () => {
console.log('WebSocket连接已关闭');
};
// 不再需要返回ws因为在当前实现中没有外部代码需要控制这个连接
// 如果未来需要从外部控制WebSocket连接可以取消注释下面这行
// return ws;
},
handleVoiceRecord0(tempFilePath) {
console.log('录音文件路径:', tempFilePath);
// 这里可以上传录音文件到服务器或直接发送
// 为演示目的,我们先显示一个提示
uni.showLoading({
title: '正在发送语音...'
});
// TODO: 实现语音文件上传逻辑
// 上传语音文件后,将语音消息添加到聊天列表
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '语音发送成功',
icon: 'success'
});
// 添加语音消息到聊天列表
// const newMessage = {
// role: 'user', // 或根据实际情况设置
// content: '[语音消息]',
// voice_path: tempFilePath,
// timestamp: Date.now()
// };
// this.sessionInitList.messages.push(newMessage);
// this.scrollToBottom();
}, 1000);
},
chatSingClick(e) {
if (e == 1) {
this.chatSingStats = true;
} else {
this.chatSingStats1 = true;
}
// // 发送消息的API调用
},
// 跳舞生成
savechatSing() {
if (this.messageprompt == '') {
uni.showToast({
title: '请输入跳舞详情',
icon: 'none'
});
return;
}
this.addBondform.type = 1;
this.addBondform.num = 1;
this.chatSingStats = false;
this.danceGenerate();
console.log(this.messageprompt);
},
closechatSing() {
this.chatSingStats = false;
this.messageprompt = '';
},
// 选择歌曲
selectSong(item, index) {
this.songId = item.id
},
savechatSing1() {
if (!this.songId) {
return uni.showToast({
title: '请选择歌曲',
icon: 'none'
});
}
this.addBondform.type = 1;
this.addBondform.num = 1;
this.chatSingStats1 = false;
this.singGenerate()
},
// 生成跳舞视频
singGenerate() {
let that = this
SingGenerate({
song_id: this.songId
}).then(res => {
if (res.code == 1) {
this.songId = 0
// this.addBond()
// // 重新获取会话数据以包含AI回复这会自动滚动到底部
// // 为确保滚动到最新消息,我们稍微延迟一下滚动
this.refreshSessionData(true); // 传递参数表示需要滚动到底部
// 轮询监听视频生成任务结果
that.pollImageSegmentResult1(res.data.generation_task_id)
} else {
uni.showToast({
title: res.msg,
icon: 'none',
position: 'top'
})
}
})
},
async pollImageSegmentResult1(job_id) {
const that = this;
// 重置轮询计数
that.pollingAttempts1 = 0;
// 如果已有轮询在运行,先清除
if (that.pollingTimer1) {
clearTimeout(that.pollingTimer1);
that.pollingTimer1 = null;
}
// 定义轮询函数
const doPoll = () => {
that.pollingAttempts1++;
// 检查是否超过最大轮询次数
if (that.pollingAttempts1 > 20) {
uni.hideLoading();
uni.showToast({
title: '处理超时,请稍后重试',
icon: 'none'
});
that.pollingTimer1 = null;
return;
}
// 请求接口
SingGenerateTask(job_id).then(res => {
console.log('视频生成查询结果:', res)
console.log(`${that.pollingAttempts1} 次轮询,任务结果:`, res);
if (res.code == 1) {
const data = res.data;
// status "RUN" 表示视频生成中,"succeeded" 表示视频生成成功
if (data.status == 'succeeded') {
// 任务成功,停止轮询
uni.hideLoading();
// that.result_url = data.ResultVideoUrl
this.addBond()
// // 重新获取会话数据以包含AI回复这会自动滚动到底部
// // 为确保滚动到最新消息,我们稍微延迟一下滚动
this.refreshSessionData(true); // 传递参数表示需要滚动到底部
that.pollingTimer1 = null;
} else if (data.status == 'failed') {
// 任务失败
uni.hideLoading();
uni.showToast({
title: data.error_msg,
icon: 'none'
});
that.pollingTimer1 = null;
} else {
// 任务处理中,继续轮询
console.log(`任务处理中 (${data.state}),${4000 / 1000}秒后重新查询...`);
that.pollingTimer1 = setTimeout(doPoll, 4000);
}
} else {
uni.hideLoading()
uni.showToast({
title: res.data,
icon: 'none',
duration: 5000
})
}
})
};
// 启动轮询
console.log('开始轮询监听视频生成任务...');
doPoll();
},
isImageUrl(url) {
if (!url || typeof url !== 'string') {
return false;
}
// 检查是否为图片URL通过扩展名判断
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
const lowerCaseUrl = url.toLowerCase();
return imageExtensions.some(ext => lowerCaseUrl.includes(ext));
},
// 预览图片
previewImage(current) {
uni.previewImage({
urls: [current],
current: current
});
},
isVideoUrl(url) {
if (!url || typeof url !== 'string') {
return false;
}
// 检查是否为视频URL通过扩展名判断
const videoExtensions = ['.mp4', '.mov', '.avi', '.wmv', '.flv', '.webm', '.m3u8', '.mpg', '.mpeg'];
const lowerCaseUrl = url.toLowerCase();
return videoExtensions.some(ext => lowerCaseUrl.includes(ext));
},
// 视频播放事件处理
onVideoPlay(id) {
// 处理视频播放逻辑
console.log('视频开始播放:', id);
},
onVideoPause(id) {
// 处理视频暂停逻辑
console.log('视频暂停:', id);
},
// 判断是否为视频消息格式
isVideoMessage(content) {
if (!content || typeof content !== 'string') {
return false;
}
// 检查是否包含视频URL的特定格式
// return content.includes('为你生成了一段跳舞视频,点击查看:') &&
return (content.includes('为你生成了一段') || content.includes('已生成部分视频')) &&
(content.includes('.mp4') || content.includes('.MP4'));
},
// 判断是否为视频消息格式
isVideoMessage1(content) {
if (!content || typeof content !== 'string') {
return false;
}
// 检查是否包含视频URL的特定格式
// return content.includes('正在为你生成跳舞视频,完成后会自动更新此消息')
return content.includes('正在为你生成')
},
// 从视频消息中提取文本部分
extractTextFromVideoMessage(content) {
if (!this.isVideoMessage(content)) {
return content; // 如果不是视频消息格式,返回原内容
}
// 查找URL开始的位置
const urlStartIndex = content.lastIndexOf('') + 1; // 找到最后一个冒号的位置
if (urlStartIndex > 0) {
return content.substring(0, urlStartIndex - 1); // 返回冒号前的部分
}
return content;
},
// 从视频消息中提取视频URL
extractVideoUrlFromMessage(content) {
if (!this.isVideoMessage(content)) {
return '';
}
// 查找URL开始的位置
const urlStartIndex = content.lastIndexOf('') + 1; // 找到最后一个冒号的位置
if (urlStartIndex > 0) {
return content.substring(urlStartIndex).trim();
}
return '';
},
bottomClick() {
if (this.bottomStats) {
this.bottomStats = false
} else {
this.bottomStats = true
}
},
// 添加视频长按处理方法
onVideoLongPress(item) {
console.log('视频长按事件触发消息ID:', item.id);
// 将视频消息的ID赋值给dynamicShareform.source_message_id
this.dynamicShareform.source_message_id = item.id;
// 提取视频URL并存储以便在弹窗中使用
if (this.isVideoMessage(item.content)) {
this.currentVideoUrl = this.extractVideoUrlFromMessage(item.content);
} else if (this.isVideoUrl(item.content)) {
this.currentVideoUrl = item.content;
} else {
this.currentVideoUrl = '';
}
// 显示分享弹窗
this.bottomStats = true;
console.log('显示分享弹窗消息ID:', item.id);
},
dynamicShare() {
DynamicShare(this.dynamicShareform).then(res => {
if (res.code == 1) {
uni.showToast({
title: '分享成功',
icon: 'success',
position: 'top'
})
this.bottomStats = false
this.dynamicShareform.content = ''
this.dynamicShareform.source_message_id = ''
this.currentVideoUrl = '' // 分享完成后清空当前视频URL
} else {
uni.showToast({
title: res.msg,
icon: 'none',
position: 'top'
})
}
})
},
noClick() {
this.bottomStats = false
this.dynamicShareform.content = ''
this.dynamicShareform.source_message_id = ''
this.currentVideoUrl = '' // 清空当前视频URL
},
yesClick() {
console.log(this.dynamicShareform);
this.dynamicShare()
},
getSingSongs() {
SingSongs({}).then(res => {
if (res.code == 1) {
this.singSongsList = res.data.songs
}
})
},
// 停止轮询(供外部调用,如页面销毁时)
stopPolling() {
if (this.pollingTimer) {
clearTimeout(this.pollingTimer);
this.pollingTimer = null;
console.log('已停止轮询监听');
}
if (this.pollingTimer1) {
clearTimeout(this.pollingTimer1);
this.pollingTimer1 = null;
console.log('已停止轮询监听1');
}
},
},
// 页面卸载时停止轮询
onUnload() {
this.stopPolling();
},
}
</script>
<style>
page {
height: 100%;
/* opacity: 0.7; */
}
/* .uni-navbar__header-btns-left.data-v-26544265 {
width: 0 !important;
} */
.uni-navbar__header-btns-left {
width: 100% !important;
}
</style>
<style scoped>
.body {
position: relative;
height: 100vh;
display: flex;
flex-direction: column;
}
.back {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: block;
}
.custom_left {
position: relative;
display: flex;
align-items: center;
height: 100%;
z-index: 10;
}
.left_content {
position: relative;
display: flex;
align-items: center;
}
.left_return {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
}
.left_module {
position: relative;
display: flex;
align-items: center;
}
.left_avatar {
width: 68rpx;
height: 68rpx;
display: block;
border-radius: 100rpx;
}
.left_count {
position: absolute;
right: -30rpx;
bottom: 0;
}
.left_count image {
position: relative;
}
.left_dight {
position: relative;
width: 40rpx;
height: 40rpx;
}
.left_dight image {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
}
.left_dight text {
position: relative;
width: 48rpx;
height: 48rpx;
}
.body_content {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
padding: 0 0 40rpx 0;
min-height: 0;
}
.list {
flex: 1;
position: relative;
z-index: 2;
padding: 20rpx 0;
overflow-y: auto;
}
.message-list {
padding: 20rpx;
}
.message-item {
display: flex;
margin-bottom: 30rpx;
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 10rpx;
flex-shrink: 0;
}
/* 左侧消息(对方/AI */
.left-message {
justify-content: flex-start;
}
.left-message .avatar {
margin-right: 20rpx;
}
.left-message .message-bubble {
background: #E1C7FF;
color: #000000;
}
/* 右侧消息(自己) */
.right-message {
justify-content: flex-end;
}
.right-message .avatar {
order: 2;
/* 将头像移到右边 */
margin-left: 20rpx;
}
.right-message .message-content {
order: 1;
/* 将消息内容移到左边 */
}
.right-message .message-bubble {
background: rgba(0, 0, 0, 0.9);
color: #FFFFFF;
}
.message-content {
max-width: 70%;
}
.message-bubble {
padding: 20rpx;
border-radius: 10rpx;
position: relative;
}
.message-text {
font-size: 28rpx;
line-height: 40rpx;
word-break: break-all;
}
.message-image {
max-width: 200rpx;
max-height: 200rpx;
border-radius: 10rpx;
}
.message-image-container {
max-width: 70%;
}
.message-video {
max-width: 300rpx;
max-height: 200rpx;
border-radius: 10rpx;
}
.message-video-container {
max-width: 70%;
}
.textbox {
position: relative;
padding: 0 25rpx 0 25rpx;
z-index: 2;
}
.textbox_voice {
margin: 0 14rpx 0 0;
width: 60rpx;
height: 60rpx;
display: block;
}
.textbox_content {
position: relative;
padding: 10rpx 25rpx;
background: rgba(123, 123, 123, 0.5);
border-radius: 12rpx;
}
.textbox_content input {
font-weight: 500;
font-size: 30rpx;
color: #FFFFFF;
line-height: 50rpx;
}
.textbox_image {
margin: 0 14rpx 0 0;
width: 68rpx;
height: 68rpx;
display: block;
border-radius: 10rpx;
}
.textbox_input {
color: #FFFFFF;
}
.textbox_send {
margin: 0 0px 0 14rpx;
padding: 15rpx 30rpx;
font-weight: 400;
font-size: 28rpx;
color: #FFFFFF;
line-height: 50rpx;
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%);
border-radius: 12rpx;
}
.textbox_add {
width: 60rpx;
height: 60rpx;
display: block;
}
.black {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 420rpx;
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, #4A4A4A 100%);
}
.btn {
position: relative;
padding: 42rpx 50rpx 62rpx 50rpx;
background: #FFFFFF;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-column-gap: 50rpx;
grid-row-gap: 40rpx;
}
.btn_content {
position: relative;
}
.btn_module {
position: relative;
}
.btn_image {
width: 88rpx;
height: 88rpx;
display: block;
}
.btn_lock {
position: absolute;
right: 20rpx;
top: 0;
width: 32rpx;
height: 24rpx;
}
.btn_title {
margin: 5rpx 0 0 0;
font-weight: 400;
font-size: 28rpx;
color: #222222;
line-height: 50rpx;
text-align: center;
}
/* .uni-navbar__header-btns-right {
padding-right: var(--right-padding, 0);
} */
.voice-container {
display: flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
cursor: pointer;
margin-left: 10rpx;
}
.voice-wave {
display: flex;
align-items: center;
justify-content: space-around;
width: 40rpx;
height: 20rpx;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.voice-wave.playing {
opacity: 1;
visibility: visible;
}
.voice-wave.black-wave {
opacity: 1;
visibility: visible;
}
.video-message-container {
display: flex;
flex-direction: column;
}
.video-message-container .message-text {
margin-bottom: 10rpx;
/* 文字和视频之间的间距 */
}
.wave-bar {
width: 3rpx;
height: 10rpx;
background-color: #ffffff;
border-radius: 1.5rpx;
animation: wave 1.2s infinite ease-in-out;
transform-origin: bottom;
transition: height 0.3s ease;
}
/* 播放状态的动画效果 */
.voice-wave.playing .wave-bar {
background-color: #ffffff;
animation: wave 1.2s infinite ease-in-out;
}
/* 非播放状态的灰色样式 */
.voice-wave.black-wave .wave-bar {
background-color: #999999;
animation: none;
}
/* 波形动画 - 两边高中间低 */
@keyframes wave {
0%,
100% {
transform: scaleY(0.4);
background-color: #ffffff;
}
20% {
transform: scaleY(0.8);
background-color: #e0e0e0;
}
40% {
transform: scaleY(1);
background-color: #c0c0c0;
}
60% {
transform: scaleY(0.7);
background-color: #e0e0e0;
}
80% {
transform: scaleY(0.5);
background-color: #ffffff;
}
}
/* 非播放状态下波形动画 */
.voice-wave.black-wave.playing .wave-bar {
animation: wave 1.2s infinite ease-in-out;
}
.voice-wave.black-wave.playing .wave-bar {
background-color: #999999;
}
/* 为不同波形条设置不同的动画延迟,创建更自然的波形效果 */
.wave-bar-1 {
animation-delay: 0s;
height: 20rpx;
/* 最高 */
}
.wave-bar-2 {
animation-delay: 0.2s;
height: 15rpx;
/* 较高 */
}
.wave-bar-3 {
animation-delay: 0.4s;
height: 25rpx;
/* 最低 */
}
.wave-bar-4 {
animation-delay: 0.6s;
height: 15rpx;
/* 较高 */
}
.wave-bar-5 {
animation-delay: 0.8s;
height: 20rpx;
/* 最高 */
}
/* 播放状态下的颜色 */
.voice-wave.playing .wave-bar-1 {
background-color: #ff7eb9;
height: 20rpx;
/* 最高 */
}
.voice-wave.playing .wave-bar-2 {
background-color: #ff7eb9;
height: 15rpx;
/* 较高 */
}
.voice-wave.playing .wave-bar-3 {
background-color: #ff7eb9;
height: 25rpx;
/* 最低 */
}
.voice-wave.playing .wave-bar-4 {
background-color: #ff7eb9;
height: 15rpx;
/* 较高 */
}
.voice-wave.playing .wave-bar-5 {
background-color: #ff7eb9;
height: 20rpx;
/* 最高 */
}
/* 非播放状态下的颜色 - 保持两边高中间低的形状 */
.voice-wave.black-wave .wave-bar-1 {
background-color: #000000;
height: 20rpx;
/* 最高 */
}
.voice-wave.black-wave .wave-bar-2 {
background-color: #000000;
height: 15rpx;
/* 较高 */
}
.voice-wave.black-wave .wave-bar-3 {
background-color: #000000;
height: 25rpx;
/* 最低 */
}
.voice-wave.black-wave .wave-bar-4 {
background-color: #000000;
height: 15rpx;
/* 较高 */
}
.voice-wave.black-wave .wave-bar-5 {
background-color: #000000;
height: 20rpx;
/* 最高 */
}
.alert {
position: fixed;
height: 100%;
width: 100%;
top: 0;
left: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 10;
}
.alert_hide {
position: absolute;
height: 100%;
width: 100%;
top: 0;
}
.alert_module {
position: absolute;
width: 100%;
padding: 50rpx 0 100rpx 0;
background: #fff;
bottom: 0;
border-radius: 30rpx 30rpx 0 0;
}
.alert_title {
font-size: 30rpx;
text-align: center;
color: #333;
}
.alert_opt {
position: absolute;
left: 0;
right: 0;
top: -300rpx;
bottom: 0;
margin: auto auto;
height: 100rpx;
width: 350rpx;
background: #fff;
padding: 0 30rpx;
box-sizing: border-box;
border-radius: 30rpx;
}
.alert_a1 {
position: relative;
height: 90rpx;
width: 10rpx;
background: #ff9b9f;
animation: alert_a1 0.5s infinite linear;
}
@keyframes alert_a1 {
0% {
height: 90rpx;
}
50% {
height: 10rpx;
}
100% {
height: 90rpx;
}
}
.alert_a2 {
position: relative;
height: 90rpx;
width: 10rpx;
background: #ff9b9f;
animation: alert_a2 1s infinite linear;
}
@keyframes alert_a2 {
0% {
height: 90rpx;
}
50% {
height: 10rpx;
}
100% {
height: 90rpx;
}
}
.alert_a3 {
position: relative;
height: 90rpx;
width: 10rpx;
background: #ff9b9f;
animation: alert_a3 1.5s infinite linear;
}
@keyframes alert_a3 {
0% {
height: 90rpx;
}
50% {
height: 10rpx;
}
100% {
height: 90rpx;
}
}
.alert_a4 {
position: relative;
height: 90rpx;
width: 10rpx;
background: #ff9b9f;
animation: alert_a4 2s infinite linear;
}
@keyframes alert_a4 {
0% {
height: 90rpx;
}
50% {
height: 10rpx;
}
100% {
height: 90rpx;
}
}
.alert_a5 {
position: relative;
height: 90rpx;
width: 10rpx;
background: #ff9b9f;
animation: alert_a5 2.5s infinite linear;
}
@keyframes alert_a5 {
0% {
height: 90rpx;
}
50% {
height: 10rpx;
}
100% {
height: 90rpx;
}
}
.alert_btn {
position: relative;
height: 150rpx;
width: 150rpx;
box-shadow: 0 0 10rpx #d0d0d0;
margin: 50rpx auto 0 auto;
border-radius: 100rpx;
}
.alert_btn image {
height: 60rpx;
width: 60rpx;
}
/* 旋转动画 */
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 旋转加载器样式 */
.spinner-cycle-box {
display: inline-block;
animation: spin 3s linear infinite;
}
.chatSing {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
padding: 0 40rpx;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.2);
z-index: 2;
}
.chatSing_content {
position: relative;
background: #FFFFFF;
border-radius: 20rpx;
padding: 60rpx;
}
.chatSing_content image {
position: absolute;
right: 20rpx;
top: 20rpx;
width: 30rpx;
height: 30rpx;
}
.chatSing_title {
font-weight: 500;
font-size: 32rpx;
color: #161616;
line-height: 38rpx;
text-align: center;
}
.chatSing_detail {
position: relative;
margin: 20rpx 0 0 0;
}
.chatSing_detail input {
width: 400rpx;
padding: 15rpx;
font-weight: 400;
font-size: 26rpx;
color: #161616;
line-height: 50rpx;
border-radius: 12rpx;
border: 1px solid #989898;
}
.chatSing_input {
color: #161616;
}
.chatSing_sure {
margin: 20rpx 0 0 0;
}
.chatSing_sure text {
padding: 15rpx 60rpx;
font-weight: 400;
font-size: 26rpx;
color: #FFFFFF;
line-height: 50rpx;
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%);
border-radius: 10rpx;
}
.bottom {
position: fixed;
width: 100%;
height: 100%;
left: 0;
bottom: 0;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.2);
z-index: 2;
}
.bottom_content {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(180deg, #EDD6F5 0%, #FFFFFF 100%);
border-radius: 40rpx 40rpx 0rpx 0rpx;
padding: 46rpx 80rpx 68rpx 80rpx;
}
.bottom_title {
position: relative;
font-weight: 500;
font-size: 36rpx;
color: #333333;
line-height: 50rpx;
text-align: center;
}
.bottom_title image {
position: absolute;
right: -20rpx;
top: 0;
bottom: 0;
width: 22rpx;
height: 22rpx;
margin: auto 0;
}
.bottom_video {
margin: 50rpx auto 0 auto;
width: 272rpx;
height: 272rpx;
border-radius: 20rpx;
}
.bottom_module {
position: relative;
margin: 20rpx 0 0 0;
padding: 0 50rpx;
}
.bottom_module input {
padding: 20rpx 30rpx;
font-weight: 500;
font-size: 30rpx;
color: #8449FE;
line-height: 50rpx;
background: #FAFAFA;
border-radius: 12rpx;
}
.bottom_input {
color: #8449FE;
}
.bottom_item {
position: relative;
margin: 42rpx 0 0 0;
}
.bottom_btn {
position: relative;
padding: 15rpx 112rpx;
font-weight: 500;
font-size: 30rpx;
color: #8449FE;
line-height: 50rpx;
border-radius: 12rpx;
border: 2rpx solid #817EFE;
}
.bottom_btn:nth-child(2) {
color: #FFFFFF;
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%), #D8D8D8;
}
</style>