2026-01-31 19:15:41 +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>
|
|
|
|
|
|
<image class="back"
|
2026-02-01 11:05:35 +08:00
|
|
|
|
:src="chatBackground"
|
2026-01-31 19:15:41 +08:00
|
|
|
|
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">
|
2026-02-01 11:48:12 +08:00
|
|
|
|
<!-- AI消息添加编辑按钮 -->
|
|
|
|
|
|
<view v-if="item.role === 'lover'" class="edit-icon" @click="onMessageLongPress(item)">
|
|
|
|
|
|
<text>✏️</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="message-bubble" @longpress="onMessageLongPress(item)">
|
2026-01-31 19:15:41 +08:00
|
|
|
|
<!-- 视频消息特殊处理 -->
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<!-- 普通文本消息 -->
|
2026-02-01 11:54:01 +08:00
|
|
|
|
<view v-else class="message-text" :class="{ 'thinking-text': item.isThinking }">
|
|
|
|
|
|
{{ item.content }}
|
|
|
|
|
|
</view>
|
2026-01-31 19:15:41 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 语音容器独立存在,仅对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>
|
2026-02-01 10:56:03 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 录音弹框 -->
|
2026-01-31 19:15:41 +08:00
|
|
|
|
<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" />
|
2026-02-01 11:48:12 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 消息编辑弹窗 -->
|
|
|
|
|
|
<view class="edit-modal" v-if="editModalVisible" @click="editModalVisible = false">
|
|
|
|
|
|
<view class="edit-content" @click.stop>
|
|
|
|
|
|
<view class="edit-title">编辑消息</view>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
v-model="editingMessage.content"
|
|
|
|
|
|
class="edit-textarea"
|
|
|
|
|
|
placeholder="请输入消息内容"
|
|
|
|
|
|
maxlength="4000">
|
|
|
|
|
|
</textarea>
|
|
|
|
|
|
<view class="edit-tip">编辑后将更新AI的记忆,下次对话时生效</view>
|
|
|
|
|
|
<view class="edit-buttons">
|
|
|
|
|
|
<view class="edit-btn cancel" @click="cancelEdit">取消</view>
|
|
|
|
|
|
<view class="edit-btn confirm" @click="confirmEdit">确认</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-01-31 19:15:41 +08:00
|
|
|
|
</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: [], //歌曲列表
|
2026-02-01 11:05:35 +08:00
|
|
|
|
songId: 0,
|
2026-02-01 11:48:12 +08:00
|
|
|
|
chatBackground: '', // 聊天背景
|
|
|
|
|
|
// 消息编辑相关
|
|
|
|
|
|
editModalVisible: false,
|
|
|
|
|
|
editingMessage: null,
|
|
|
|
|
|
baseURL: 'http://127.0.0.1:8080',
|
|
|
|
|
|
baseURLPy: 'http://127.0.0.1:8000',
|
2026-02-01 11:54:01 +08:00
|
|
|
|
// 思考时间相关
|
|
|
|
|
|
messageSentTime: 0, // 消息发送时间戳
|
2026-01-31 19:15:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onLoad() {
|
|
|
|
|
|
// #ifdef MP
|
|
|
|
|
|
this.initSI();
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
},
|
|
|
|
|
|
onShow() {
|
|
|
|
|
|
// #ifdef MP-WEIXIN
|
|
|
|
|
|
this.getMenuInfo()
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
this.sessionInit() //恋人聊天初始化
|
2026-02-01 11:05:35 +08:00
|
|
|
|
// #ifndef H5
|
|
|
|
|
|
this.initRecorder() //初始化录音管理器(H5不支持)
|
|
|
|
|
|
// #endif
|
2026-01-31 19:15:41 +08:00
|
|
|
|
this.getSingSongs() //歌曲列表
|
2026-02-01 11:05:35 +08:00
|
|
|
|
this.loadChatBackground() //加载聊天背景
|
2026-01-31 19:15:41 +08:00
|
|
|
|
// this.initAudio()
|
|
|
|
|
|
},
|
|
|
|
|
|
onUnload() {
|
|
|
|
|
|
this.stopCurrentAudio();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onHide() {
|
|
|
|
|
|
this.stopCurrentAudio();
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
2026-02-01 11:05:35 +08:00
|
|
|
|
// 加载聊天背景
|
|
|
|
|
|
loadChatBackground() {
|
|
|
|
|
|
const savedBg = uni.getStorageSync('chat_background');
|
|
|
|
|
|
if (savedBg) {
|
|
|
|
|
|
this.chatBackground = savedBg;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 默认背景
|
|
|
|
|
|
this.chatBackground = this.loverBasicList.image_url || 'https://nvlovers.oss-cn-qingdao.aliyuncs.com/uploads/20251226/39c6f8899c15f60fc59207835f95e07a.png';
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-01-31 19:15:41 +08:00
|
|
|
|
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) {
|
2026-02-01 11:54:01 +08:00
|
|
|
|
// 计算已经过去的时间
|
|
|
|
|
|
const elapsedTime = Date.now() - this.messageSentTime;
|
|
|
|
|
|
const minThinkingTime = 5000; // 最少5秒思考时间
|
|
|
|
|
|
const remainingTime = Math.max(0, minThinkingTime - elapsedTime);
|
|
|
|
|
|
|
|
|
|
|
|
// 延迟刷新,确保至少显示5秒思考中
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
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);
|
|
|
|
|
|
}, remainingTime);
|
2026-01-31 19:15:41 +08:00
|
|
|
|
} else {
|
2026-02-01 11:54:01 +08:00
|
|
|
|
// 发送失败,立即移除思考中消息
|
|
|
|
|
|
this.sessionInitList.messages = this.sessionInitList.messages.filter(
|
|
|
|
|
|
msg => !msg.isThinking
|
|
|
|
|
|
);
|
2026-01-31 19:15:41 +08:00
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: res.msg,
|
|
|
|
|
|
icon: 'none',
|
|
|
|
|
|
position: 'top'
|
2026-02-01 11:54:01 +08:00
|
|
|
|
});
|
2026-01-31 19:15:41 +08:00
|
|
|
|
}
|
2026-02-01 11:54:01 +08:00
|
|
|
|
}).catch(err => {
|
|
|
|
|
|
// 发送失败,立即移除思考中消息
|
|
|
|
|
|
this.sessionInitList.messages = this.sessionInitList.messages.filter(
|
|
|
|
|
|
msg => !msg.isThinking
|
|
|
|
|
|
);
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '发送失败',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-01-31 19:15:41 +08:00
|
|
|
|
},
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 11:54:01 +08:00
|
|
|
|
// 记录发送时间,用于计算最小思考时间
|
|
|
|
|
|
this.messageSentTime = Date.now();
|
|
|
|
|
|
|
|
|
|
|
|
// 立即添加一条"思考中"的临时消息
|
|
|
|
|
|
const thinkingMessage = {
|
|
|
|
|
|
id: 'thinking_' + Date.now(),
|
|
|
|
|
|
role: 'lover',
|
|
|
|
|
|
content: '思考中...',
|
|
|
|
|
|
isThinking: true, // 标记为思考中状态
|
|
|
|
|
|
created_at: new Date().toISOString()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 添加到消息列表
|
|
|
|
|
|
if (!this.sessionInitList.messages) {
|
|
|
|
|
|
this.sessionInitList.messages = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
this.sessionInitList.messages.push(thinkingMessage);
|
|
|
|
|
|
|
|
|
|
|
|
// 滚动到底部
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
this.scrollToBottom();
|
2026-01-31 19:15:41 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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() {
|
2026-02-01 13:39:28 +08:00
|
|
|
|
// 跳转到主页(使用 reLaunch 清空页面栈)
|
|
|
|
|
|
uni.reLaunch({
|
|
|
|
|
|
url: '/pages/index/index'
|
2026-01-31 19:15:41 +08:00
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
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() {
|
2026-02-01 11:05:35 +08:00
|
|
|
|
// #ifdef H5
|
|
|
|
|
|
console.log('H5环境不支持录音管理器');
|
|
|
|
|
|
return;
|
|
|
|
|
|
// #endif
|
|
|
|
|
|
|
2026-01-31 19:15:41 +08:00
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-02-01 11:48:12 +08:00
|
|
|
|
// 长按消息
|
|
|
|
|
|
onMessageLongPress(message) {
|
|
|
|
|
|
// 只能编辑AI的消息
|
|
|
|
|
|
if (message.role !== 'lover') {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.editingMessage = {
|
|
|
|
|
|
id: message.id,
|
|
|
|
|
|
content: message.content,
|
|
|
|
|
|
seq: message.seq
|
|
|
|
|
|
};
|
|
|
|
|
|
this.editModalVisible = true;
|
|
|
|
|
|
},
|
|
|
|
|
|
// 取消编辑
|
|
|
|
|
|
cancelEdit() {
|
|
|
|
|
|
this.editModalVisible = false;
|
|
|
|
|
|
this.editingMessage = null;
|
|
|
|
|
|
},
|
|
|
|
|
|
// 确认编辑
|
|
|
|
|
|
confirmEdit() {
|
|
|
|
|
|
if (!this.editingMessage.content.trim()) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '消息内容不能为空',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uni.showLoading({ title: '更新中...' });
|
|
|
|
|
|
|
|
|
|
|
|
uni.request({
|
|
|
|
|
|
url: this.baseURLPy + `/chat/messages/${this.editingMessage.id}/edit`,
|
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
header: {
|
|
|
|
|
|
'token': uni.getStorageSync("token") || "",
|
|
|
|
|
|
'Authorization': (uni.getStorageSync("token") ? ('Bearer ' + uni.getStorageSync("token")) : ""),
|
|
|
|
|
|
'X-User-Id': '84'
|
|
|
|
|
|
},
|
|
|
|
|
|
data: {
|
|
|
|
|
|
new_content: this.editingMessage.content,
|
|
|
|
|
|
regenerate_reply: false // 不重新生成回复,只更新记忆
|
|
|
|
|
|
},
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
|
if (res.data.code === 1) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '编辑成功',
|
|
|
|
|
|
icon: 'success'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 刷新消息列表
|
|
|
|
|
|
this.sessionInit();
|
|
|
|
|
|
this.editModalVisible = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: res.data.msg || '编辑失败',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (error) => {
|
|
|
|
|
|
uni.hideLoading();
|
|
|
|
|
|
console.error('编辑消息失败', error);
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '编辑失败',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
2026-01-31 19:15:41 +08:00
|
|
|
|
},
|
|
|
|
|
|
// 页面卸载时停止轮询
|
|
|
|
|
|
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%;
|
2026-02-01 11:48:12 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 编辑图标 */
|
|
|
|
|
|
.edit-icon {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: -50rpx; /* AI消息在左边,图标也在左边 */
|
|
|
|
|
|
width: 40rpx;
|
|
|
|
|
|
height: 40rpx;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.9);
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.edit-icon text {
|
|
|
|
|
|
font-size: 24rpx;
|
2026-01-31 19:15:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-bubble {
|
|
|
|
|
|
padding: 20rpx;
|
|
|
|
|
|
border-radius: 10rpx;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-text {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
line-height: 40rpx;
|
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 11:54:01 +08:00
|
|
|
|
/* 思考中状态的文本样式 */
|
|
|
|
|
|
.thinking-text {
|
|
|
|
|
|
color: #999 !important;
|
|
|
|
|
|
font-style: italic;
|
|
|
|
|
|
animation: thinking-pulse 1.5s ease-in-out infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes thinking-pulse {
|
|
|
|
|
|
0%, 100% {
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
50% {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 19:15:41 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2026-02-01 11:48:12 +08:00
|
|
|
|
|
|
|
|
|
|
/* 消息编辑弹窗样式 */
|
|
|
|
|
|
.edit-modal {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
z-index: 9999;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.edit-content {
|
|
|
|
|
|
width: 80%;
|
|
|
|
|
|
max-width: 600rpx;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 20rpx;
|
|
|
|
|
|
padding: 40rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.edit-title {
|
|
|
|
|
|
font-size: 32rpx;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.edit-textarea {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-height: 200rpx;
|
|
|
|
|
|
padding: 20rpx;
|
|
|
|
|
|
border: 1rpx solid #ddd;
|
|
|
|
|
|
border-radius: 10rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.edit-tip {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
margin-bottom: 30rpx;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.edit-buttons {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.edit-btn {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 20rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
border-radius: 10rpx;
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.edit-btn.cancel {
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.edit-btn.confirm {
|
|
|
|
|
|
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
2026-01-31 19:15:41 +08:00
|
|
|
|
</style>
|