Ai_GirlFriend/xuniYou/pages/chat/index_with_tabs.vue.backup

3057 lines
74 KiB
Plaintext
Raw Normal View History

2026-02-01 10:56:35 +08:00
<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>
<!-- Tab 栏 -->
<view class="tab-bar" :style="{ top: tabBarTop }">
<scroll-view class="tab-scroll" scroll-x="true" :scroll-left="tabScrollLeft" scroll-with-animation>
<view class="tab-list">
<view
v-for="(tab, index) in tabList"
:key="index"
class="tab-item"
:class="{ 'active': currentTab === index }"
@click="switchTab(index)">
<text class="tab-text">{{ tab.name }}</text>
<view v-if="currentTab === index" class="tab-indicator"></view>
</view>
</view>
</scroll-view>
</view>
<!-- 聊天内容区域 -->
<view v-show="currentTab === 0" class="tab-content">
<image class="back"
:src="loverBasicList.image_url ? loverBasicList.image_url : 'https://nvlovers.oss-cn-qingdao.aliyuncs.com/uploads/20251226/39c6f8899c15f60fc59207835f95e07a.png'"
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>
<!-- 底部按钮区域,通过 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>
</view>
<!-- 唱歌页面 -->
<view v-show="currentTab === 1" class="tab-content">
<view class="feature-page">
<view class="feature-title">唱歌功能</view>
<view class="feature-desc">让她为你唱一首歌</view>
<view class="song-list">
<view class="song-item" v-for="(item,index) in singSongsList" :key="index" @click="selectSongDirect(item)">
<view class="song-info">
<text class="song-title">{{item.title}}</text>
</view>
<image class="song-icon" src="/static/images/chat_a6.png" mode="widthFix"></image>
</view>
</view>
</view>
</view>
<!-- 跳舞页面 -->
<view v-show="currentTab === 2" class="tab-content">
<view class="feature-page">
<view class="feature-title">跳舞功能</view>
<view class="feature-desc">让她为你跳一支舞</view>
<view class="dance-input">
<input type="text" v-model="dancePrompt" placeholder="描述你想看的舞蹈动作" class="dance-input-field" />
<view class="dance-btn" @click="requestDance">生成舞蹈</view>
</view>
</view>
</view>
<!-- 换服装页面 -->
<view v-show="currentTab === 3" class="tab-content">
<view class="feature-page">
<view class="feature-title">换服装</view>
<view class="feature-desc">为她挑选不同的服装</view>
<view class="outfit-grid">
<view class="outfit-item" v-for="i in 6" :key="i">
<image class="outfit-image" src="/static/images/avatar.png" mode="aspectFill"></image>
<text class="outfit-name">服装 {{i}}</text>
</view>
</view>
</view>
</view>
<!-- 刷礼物页面 -->
<view v-show="currentTab === 4" class="tab-content">
<view class="feature-page">
<view class="feature-title">送礼物</view>
<view class="feature-desc">送她一份心意</view>
<view class="gift-grid">
<view class="gift-item" v-for="i in 8" :key="i" @click="sendGift(i)">
<image class="gift-image" src="/static/images/chat_a2.png" mode="widthFix"></image>
<text class="gift-name">礼物 {{i}}</text>
<text class="gift-price">{{i * 10}} 金币</text>
</view>
</view>
</view>
</view>
<!-- 商城页面 -->
<view v-show="currentTab === 5" class="tab-content">
<view class="feature-page">
<view class="feature-title">商城</view>
<view class="feature-desc">购买更多功能和道具</view>
<view class="shop-list">
<view class="shop-item" v-for="i in 4" :key="i">
<view class="shop-info">
<text class="shop-title">套餐 {{i}}</text>
<text class="shop-desc">包含多种功能和道具</text>
</view>
<view class="shop-price">
<text class="price-value">¥{{i * 30}}</text>
<view class="shop-buy-btn">购买</view>
</view>
</view>
</view>
</view>
</view>
<!-- 短剧页面 -->
<view v-show="currentTab === 6" class="tab-content">
<view class="feature-page">
<view class="feature-title">短剧</view>
<view class="feature-desc">观看精彩短剧内容</view>
<view class="drama-list">
<view class="drama-item" v-for="i in 3" :key="i">
<image class="drama-cover" src="/static/images/avatar.png" mode="aspectFill"></image>
<view class="drama-info">
<text class="drama-title">短剧标题 {{i}}</text>
<text class="drama-desc">精彩剧情简介...</text>
<view class="drama-play-btn">播放</view>
</view>
</view>
</view>
</view>
</view>
</view> <!-- 闭合 body_content -->
</view> <!-- 闭合 body -->
<!-- 录音弹框 -->
<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 {
// Tab 相关
currentTab: 0,
tabScrollLeft: 0,
tabBarTop: '88px', // Tab 栏顶部位置
tabList: [
{ name: '聊天', icon: '' },
{ name: '唱歌', icon: '' },
{ name: '跳舞', icon: '' },
{ name: '换服装', icon: '' },
{ name: '刷礼物', icon: '' },
{ name: '商城', icon: '' },
{ name: '短剧', icon: '' }
],
dancePrompt: '', // 跳舞描述
// 可以在这里添加聊天数据
// 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
}
},
onLoad() {
// #ifdef MP
this.initSI();
// #endif
},
onShow() {
// #ifdef MP-WEIXIN
this.getMenuInfo()
// #endif
this.sessionInit() //恋人聊天初始化
this.initRecorder() //初始化录音管理器
this.getSingSongs() //歌曲列表
// this.initAudio()
},
onUnload() {
this.stopCurrentAudio();
},
onHide() {
this.stopCurrentAudio();
},
methods: {
// Tab 切换方法
switchTab(index) {
this.currentTab = index;
// 计算滚动位置,使当前 tab 居中
const tabWidth = 120; // 每个 tab 的宽度rpx
const screenWidth = 750; // 屏幕宽度rpx
this.tabScrollLeft = Math.max(0, (index * tabWidth) - (screenWidth / 2) + (tabWidth / 2));
},
// 直接选择歌曲并生成
selectSongDirect(song) {
this.songId = song.id;
uni.showLoading({
title: '生成中...'
});
SingGenerate({
song_id: song.id
}).then(res => {
if (res.code == 1) {
this.getSingGenerateTask(res.data.task_id);
} else {
uni.hideLoading();
uni.showToast({
title: res.msg,
icon: 'none'
});
}
});
},
// 请求跳舞
requestDance() {
if (!this.dancePrompt || !this.dancePrompt.trim()) {
uni.showToast({
title: '请输入舞蹈描述',
icon: 'none'
});
return;
}
this.messageprompt = this.dancePrompt;
this.savechatSing();
},
// 送礼物
sendGift(giftId) {
uni.showToast({
title: `送出礼物 ${giftId}`,
icon: 'success'
});
// 这里可以调用送礼物的 API
},
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() {
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>
/* Tab 栏样式 */
.tab-bar {
position: fixed;
left: 0;
right: 0;
z-index: 999;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
padding: 0;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.tab-scroll {
width: 100%;
white-space: nowrap;
}
.tab-list {
display: inline-flex;
padding: 0 20rpx;
}
.tab-item {
position: relative;
padding: 20rpx 30rpx;
margin: 0 10rpx;
cursor: pointer;
transition: all 0.3s;
}
.tab-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.6);
transition: all 0.3s;
white-space: nowrap;
}
.tab-item.active .tab-text {
color: #FFFFFF;
font-weight: bold;
font-size: 32rpx;
}
.tab-indicator {
position: absolute;
bottom: 10rpx;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 6rpx;
background: linear-gradient(90deg, #E1C7FF, #B794F6);
border-radius: 3rpx;
}
/* Tab 内容区域 */
.tab-content {
flex: 1;
position: relative;
overflow-y: auto;
width: 100%;
height: 100%;
}
/* 功能页面通用样式 */
.feature-page {
padding: 40rpx;
min-height: 100%;
}
.feature-title {
font-size: 48rpx;
font-weight: bold;
color: #FFFFFF;
text-align: center;
margin-bottom: 20rpx;
text-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.3);
}
.feature-desc {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
text-align: center;
margin-bottom: 60rpx;
}
/* 歌曲列表样式 */
.song-list {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20rpx;
padding: 20rpx;
}
.song-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 15rpx;
margin-bottom: 20rpx;
}
.song-info {
flex: 1;
}
.song-title {
font-size: 32rpx;
color: #FFFFFF;
}
.song-icon {
width: 60rpx;
height: 60rpx;
}
/* 跳舞输入样式 */
.dance-input {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20rpx;
padding: 40rpx;
}
.dance-input-field {
width: 100%;
padding: 30rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 15rpx;
color: #FFFFFF;
font-size: 28rpx;
margin-bottom: 30rpx;
}
.dance-btn {
width: 100%;
padding: 30rpx;
background: linear-gradient(90deg, #E1C7FF, #B794F6);
border-radius: 15rpx;
text-align: center;
color: #FFFFFF;
font-size: 32rpx;
font-weight: bold;
}
/* 服装网格样式 */
.outfit-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30rpx;
}
.outfit-item {
text-align: center;
}
.outfit-image {
width: 100%;
height: 200rpx;
border-radius: 15rpx;
background: rgba(255, 255, 255, 0.1);
margin-bottom: 15rpx;
}
.outfit-name {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
}
/* 礼物网格样式 */
.gift-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 30rpx;
}
.gift-item {
text-align: center;
padding: 20rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 15rpx;
}
.gift-image {
width: 80rpx;
height: 80rpx;
margin-bottom: 10rpx;
}
.gift-name {
font-size: 24rpx;
color: #FFFFFF;
display: block;
margin-bottom: 5rpx;
}
.gift-price {
font-size: 20rpx;
color: #FFD700;
}
/* 商城列表样式 */
.shop-list {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20rpx;
padding: 20rpx;
}
.shop-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 15rpx;
margin-bottom: 20rpx;
}
.shop-info {
flex: 1;
}
.shop-title {
font-size: 32rpx;
color: #FFFFFF;
display: block;
margin-bottom: 10rpx;
}
.shop-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
}
.shop-price {
display: flex;
align-items: center;
gap: 20rpx;
}
.price-value {
font-size: 36rpx;
color: #FFD700;
font-weight: bold;
}
.shop-buy-btn {
padding: 15rpx 30rpx;
background: linear-gradient(90deg, #E1C7FF, #B794F6);
border-radius: 10rpx;
color: #FFFFFF;
font-size: 24rpx;
}
/* 短剧列表样式 */
.drama-list {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20rpx;
padding: 20rpx;
}
.drama-item {
display: flex;
padding: 20rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 15rpx;
margin-bottom: 20rpx;
}
.drama-cover {
width: 200rpx;
height: 280rpx;
border-radius: 10rpx;
background: rgba(255, 255, 255, 0.1);
margin-right: 20rpx;
}
.drama-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.drama-title {
font-size: 32rpx;
color: #FFFFFF;
font-weight: bold;
margin-bottom: 10rpx;
}
.drama-desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
line-height: 1.5;
}
.drama-play-btn {
align-self: flex-start;
padding: 15rpx 40rpx;
background: linear-gradient(90deg, #E1C7FF, #B794F6);
border-radius: 10rpx;
color: #FFFFFF;
font-size: 24rpx;
}
.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: 140rpx 0 40rpx 0; /* 顶部留出导航栏+Tab栏的空间 */
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>