2586 lines
64 KiB
Vue
2586 lines
64 KiB
Vue
<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="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>
|
||
</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
|
||
}
|
||
},
|
||
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: {
|
||
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>
|
||
.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> |