123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969 |
- <template>
- <view class="dialog-generation">
- <view class="subject-matter-ofText" :style="{ paddingBottom: keyboardHeight + 'px' }">
- <!-- 顶部导航栏 -->
- <div class="navbar">
- <view class="navbar-left">
- <image @click="goBack()" class="back" src="../../static/vip/hy_icon_jiantou.png"></image>
- <view class="elf-name">精灵·小萌</view>
- <image class="deepseek" src="../../static/makedetail/deepseek-logo.png"></image>
- </view>
- <view class="navbar-right">
- <image @click="switchToNormal()" class="primary" src="../../static/makedetail/primary.png"></image>
- <image @click="newChar()" class="createChat" src="../../static/makedetail/createChat.png"></image>
- </view>
- </div>
- <!-- <view class="navbar-reserveASeat"> </view> -->
- <!-- 聊天内容区 -->
- <!-- <scroll-view class="chat-content" :scroll-into-view="toView" scroll-y scroll-with-animation="true"
- :style="{ height: stateType === 3 ? 'calc(100% - 180rpx)' : `calc(100% - ${370 + textareaHeight}rpx)` }"
- @scroll="onChatScroll"> -->
- <scroll-view class="chat-content" :scroll-into-view="toView" scroll-y scroll-with-animation="true"
- :style="{ height: `calc(100% - ${370 + textareaHeight}rpx)` }"
- @scroll="onChatScroll">
- <template v-if="messages && messages.length > 0">
- <!-- <scroll-view class="chat-content" scroll-y> -->
- <view v-for="(msg, idx) in messages" :key="idx" :class="['chat-bubble', msg.role]">
- <template v-if="msg.role === 'user'">
- <uv-text size="32rpx" color="rgba(255, 255, 255, 0.7)" :text="msg.content"></uv-text>
- </template>
- <template v-else-if="msg.role === 'ai'">
- <view class="ai-bubble-row">
- <view class="ai-avatar">
- <image src="../../static/makedetail/characterProfilePicture.png" mode="aspectFill">
- </image>
- </view>
- <!-- 第一句话 -->
- <view v-if="msg.type == 0" class="ai-bubble-content">
- <view>欢迎来到萌创星球AI写歌🎉🎉🎉!这里能满足你创作音乐🎸、释放灵感🎵,创造独属于你的音乐旋律!
- </view>
- <br>
- <view>
- 可以根据自己的喜好点击下方的选项,快速开始创作🎼,
- </view>
-
- <div @click="onCateSent('纯音乐')" class="btn-box"> 纯音乐
- </div>
- <div @click="onCateSent('AI生成歌词')" class="btn-box" style="margin:0 20rpx"> AI生成歌词
- </div>
- <div @click="onCateSent('自定义歌词')" class="btn-box"> 自定义歌词
- </div>
- </view>
- <!-- 第二句话 -->
- <view class="ai-bubble-content" v-else-if="msg.type == 1">
- <text>{{ msg.content }}</text>
- </view>
- <!-- 第三句话 -->
- <view class="ai-bubble-content"
- style="border-radius: 12rpx 36rpx 36rpx 36rpx;border: 1px solid rgba(255,255,255,0.1);"
- v-else-if="msg.type == 2">
- <!-- <text>{{ msg.content }}</text> -->
- <view class="lyrics-input-title">
- <view>歌词</view>
- <view class="right-btn" :class="{'right-btn-active': isLyricConfirmActive}" @click="onLyricSent()" @touchstart="isLyricConfirmActive = true" @touchend="isLyricConfirmActive = false" @touchcancel="isLyricConfirmActive = false">确认</view>
- </view>
- <scroll-view scroll-y class="lyricsInputBox" style="">
- <textarea v-if="idx == (messages.length - 1 )" v-model="lyricData" class="lyric-editor" auto-height
- :adjust-position="false" placeholder="修改歌词..." maxlength="-1" />
- <uv-text v-else size="32rpx" color="rgba(255, 255, 255, 0.7)" :text="msg.content"></uv-text>
- </scroll-view>
- </view>
- <view class="ai-bubble-content" v-else-if="msg.type == 4">
- <view>请输入音乐的风格</view>
- <br>
- <view>
- 也可以根据自己的喜好点击下方的选项,快速开始创作🎼,
- </view>
- <div @click="openTagPopup()" class="btn-box">
- 选择标签
- </div>
- </view>
- <!-- <view class="ai-bubble-content" v-else-if="msg.type == 1">
- <template v-if="idx === lastAiIndex && isLoading">
- <text>{{ displayText }}</text>
- <text v-if="isLoading" class="loading-dot">...</text>
- </template>
- <template v-else>
- <text>{{ msg.content }}</text>
- </template>
- </view> -->
- <view v-else-if="msg.type == 20" class="ai-bubble-content">
- <text>OK!~小萌开始生成音乐啦!</text><br>
- <div v-if="msg.isStartGenerating && msg.startGeneratingId == 0" class="btn-box">
- 点击查看
- ({{ countdown }}) s</div>
- <div v-else
- @click="goPage(`/pages/makedetail/makeMusicDetail?id=${msg.startGeneratingId}`)"
- class="btn-box"> 点击查看
- </div>
- </view>
- </view>
- </template>
- </view>
- <view id="bottom-anchor"></view>
- <view v-if="error" class="chat-bubble ai error">{{ error }}</view>
- <view :style="{ height: stateType !== 3 || musicGenre == '自定义歌词' ? '200rpx' : '80rpx' }"></view>
- <image v-if="showToBottomBtn && keyboardHeight === 0" class="to-bottom-btn" @click="scrollToBottom"
- src="../../static/makedetail/toBottomBtn.png"></image>
- </template>
- <template v-else>
- <view class="chat-content-empty">
- <image src="../../static/makedetail/characterProfilePicture.png" mode="aspectFill"></image>
- <view class="chat-content-empty-title">嗨!我是创梦精灵</view>
- <view class="chat-content-empty-desc">与我聊聊你想要生成的角色吧!</view>
- </view>
- </template>
- </scroll-view>
- <!-- <view class="bom-reserveASeat"></view> -->
- <view v-if="stateType !== 3 || musicGenre == '自定义歌词'" class="bom-box"
- :style="{ bottom: 0 + 'px', height: `${190 + textareaHeight}rpx` }">
- <view class="bom-box-bg">
- <c-lottie ref="cLottieRef" :src='"/static/lottie/xiaomeng.json"' class="icon-img" height="108rpx"
- width="112rpx" :loop="true" :autoPlay="false"></c-lottie>
- </view>
- <!-- 底部输入区 -->
- <view class="input-bar">
- <!-- <image class="icon-img" src="../../static/makedetail/characterProfilePicture.png" mode="aspectFill">
- </image> -->
- <textarea :autoHeight="true" class="input-box" :adjust-position="false" v-model="question"
- :disabled="isLoading" placeholder="给我发送消息吧..." maxlength="400" @input="onTextareaInput"
- confirm-type="search" @confirm="onSend" rows="1"
- style="overflow-y:auto;max-height:176rpx;min-height:44rpx;" />
- <!-- <button class="send-btn" :disabled="isLoading || !question.trim()" @click="onSend">{{ isLoading ?
- '生成中...' :
- '发送' }}</button> -->
- <image v-if="isLoading" class="stop" src="../../static/makedetail/stop.png" mode=""
- @click="stopStreamAnswer" />
- <image v-else-if="!isLoading && !question.trim() && keyboardHeight === 0" class="keyboard"
- src="../../static/makedetail/keyboard.png" mode="" />
- <image v-else-if="!isLoading && question.trim() && keyboardHeight !== 0" class="send"
- src="../../static/makedetail/send.png" mode="" @click="onSend" />
- </view>
- <!-- 底部提示 -->
- <view class="footer-tip">内容由AI生成,禁用相关功能请联系管理员。</view>
- </view>
- </view>
- <DialogBox ref="DialogBox"></DialogBox>
- <multi-select-popup ref="tagPopup" :initial-options="tagOptions" @selection-confirmed="handleTagSelection"
- @popup-closed="handlePopupClosed"></multi-select-popup>
- </view>
- </template>
- <script>
- import websocket from '@/common/websocket.js';
- import MultiSelectPopup from '@/components/MultiSelectPopup/MultiSelectPopup.vue';
- export default {
- components: {
- MultiSelectPopup
- },
- data() {
- return {
- question: '',
- answer: '',
- displayText: '',
- isComplete: false,
- isLoading: false,
- loadingText: '正在生成歌词,请稍候',
- loadingDots: '...',
- loadingTimer: null,
- error: null,
- retryCount: 0,
- maxRetries: 3,
- timer: null,
- lastResponseTime: 0,
- timeout: 30000,
- typingSpeed: 50,
- messages: [],
- lastAiIndex: -1,
- keyboardHeight: 0,
- statusBarHeight: 0,
- windowBottom: 0,
- toView: '',
- showToBottomBtn: false,
- textareaHeight: 0,
- isConnected: false,
- isStartGenerating: false,
- countdown: 0,
- stateType: 1,
- isLyricConfirmActive: false, // 用于歌词确认按钮的点击反馈
- stateTypeArray: ['clear', 'cate', 'getLyrics', 'setLyrics', 'getTags', 'setTags', 'setContent', '', '', '', '', 'setContent'],
- content: '',
- musicGenre: '',
- lyricData: '',
- tagsData: '',
- tagOptions: [], // 用于存储多选弹窗的选项
- // type = clear 清除|cate 第一句话选择 |getLyrics 获取歌词(需要上传描述)|setLyrics 修改歌词(再去判断是否有* 有重复没有下一步 没有展示标签)|setTags(修改标签) (下发一个static 失败有提示 成功就正常生成)|getTags 获取标签
- };
- },
- methods: {
- // /Work/streamAnswerMusicLast
- // /Work/streamAnswerMusic
- // {type:'',content:''}
- // type = clear 清除|cate 第一句话选择 |getLyrics 获取歌词(需要上传描述)|setLyrics 修改歌词(再去判断是否有* 有重复没有下一步 没有展示标签)|setTags(修改标签) (下发一个static 失败有提示 成功就正常生成)|getTags 获取标签
- formatMsgContent(content) {
- if (!content) return [];
- // 将换行符替换为 <br/>
- return content.replace(/\n/g, '<br/>');
- ;
- },
- scrollToBottom() {
- this.toView = '';
- this.$nextTick(() => {
- this.toView = 'bottom-anchor';
- });
- },
- async initWebSocket() {
- if (this.isConnected) return;
- try {
- // 发送初始化消息
- websocket.onOpen(() => {
- // setTimeout(() => {
- // websocket.send(JSON.stringify({ type: 'getTags', content: '' }));
- // }, 1000)
- });
- await websocket.connect('wss://e.zhichao.art/Gapi/Work/streamAnswerMusic', {
- uuid: getApp().globalData.uuid
- });
- this.isConnected = true;
- // 设置消息处理回调
- websocket.onMessage((data) => {
- console.log("data:", data);
- if (data && data.includes('{')) {
- var data = JSON.parse(data);
- }
- var type = data && data.type || '';
- var content = data && data.content || '';
-
- // type = error,
- // content=no_cate(首次分类没有设置)
- // content="歌词不合规,请重新输入"|"歌词中有违规禁词,请修改!"
- // type='lyrics',content=具体歌词
- // type=success,content='OKOKOK':提交成功
- // type=result,content=ID
- // 111111111111111111111111111111111
- // if (type === 'success' && content === 'OKOKOK') {
- // // 音乐生成开始,AI消息加载中提示
- // const aiMsg = this.messages[this.lastAiIndex];
- // if (aiMsg) {
- // aiMsg.isStartGenerating = true;
- // aiMsg.content = '音乐生成中,请稍候...';
- // }
- // this.isStartGenerating = true;
- // this.countdownFun(20);
- // }
- // if (type === 'result' && content) {
- // // 音乐生成中,显示跳转到音乐详情页按钮
- // const aiMsg = this.messages[this.lastAiIndex];
- // if (aiMsg) {
- // aiMsg.isStartGenerating = false;
- // aiMsg.startGeneratingId = content;
- // aiMsg.content = '音乐已生成,点击跳转到详情页';
- // }
- // }
- if (this.isLoading) {
- if (type == 'cate' && content == 'success') {
- // {"type":"setContent","content":content}
- // 此时 第第一步选择完成
- if (this.musicGenre == '纯音乐') {
- // 跳过获取 修改 歌词
- this.stateType = 11;
- this.messages.push({ role: 'ai', type: 1, content: '输入描述生成音乐' });
- }
- if (this.musicGenre == 'AI生成歌词') {
- this.messages.push({ role: 'ai', type: 1, content: '请描述歌词' });
- this.stateType = 2;
- }
- if (this.musicGenre == '自定义歌词') {
- this.messages.push({ role: 'ai', type: 1, content: '请输入歌词' });
- this.stateType = 3;
- }
- console.log(type, content);
- // {"type":"lyrics","content":"《古风之约》\n\n青山绿水间 桃花映人面\n清风拂衣袖 相思绕指尖\n亭台楼阁畔 琴声悠扬传\n往事如烟云 飘散在天边\n\n红墙绿瓦下 谁在等归雁\n明月照窗前 孤影难入眠\n一纸素笺上 写满了思念\n岁月如流水 匆匆又一年\n\n我与你共赴 这一场古风之约\n看那春花秋月 浪漫又缠绵\n执手相看泪眼 无语凝噎\n只愿与你相伴 直到永远\n\n我与你共赴 这一场古风之约\n听那琵琶弦上 倾诉着哀怨\n举杯对饮成三人 醉在花间\n只愿与你相守 岁岁年年"}
- }
- // if (data.includes('[DONE]')) {
- // // 结束生成
- // this.completeAnswer();
- // } else if (data.includes('ID:')) {
- // // 提取ID
- // const aiMsg = this.messages[this.lastAiIndex];
- // if (aiMsg) {
- // aiMsg.startGeneratingId = data.split(':')[1];
- // }
- // } else if (data.includes('OKOKOK')) {
- // const aiMsg = this.messages[this.lastAiIndex];
- // if (aiMsg) {
- // aiMsg.isStartGenerating = true;
- // }
- // this.isStartGenerating = true;
- // this.countdownFun(20);
- // } else {
- // const aiMsg = this.messages[this.lastAiIndex];
- // if (aiMsg) {
- // aiMsg.isStartGenerating = false;
- // this.displayText += data;
- // this.answer += data;
- // aiMsg.content += data;
- // this.retryCount = 0;
- // this.lastResponseTime = Date.now();
- // this.scrollToBottom();
- // }
- // }
- this.isLoading = false;
- }
- if (type == 'lyrics' && content) {
- console.log('获取到歌词', content);
- // 替换加载中的消息
- const lastMessageIndex = this.messages.length - 1;
- // 替换加载中的消息
- this.stopLoadingAnimation();
- // Find and remove the loading message
- for (let i = this.messages.length - 1; i >= 0; i--) {
- if (this.messages[i].isGeneratingLyrics) {
- this.messages.splice(i, 1);
- break;
- }
- }
- // 移除 isProcessing 的消息
- for (let i = this.messages.length - 1; i >= 0; i--) {
- if (this.messages[i].isProcessing) {
- this.messages.splice(i, 1);
- break;
- }
- }
- this.messages.push({ role: 'ai', type: 2, content: content });
- this.stateType = 3;
- this.lyricData = content;
- console.log(this.messages, 24);
- }
- if (type == 'tags' && content) {
- // 移除 isProcessing 的消息
- for (let i = this.messages.length - 1; i >= 0; i--) {
- if (this.messages[i].isProcessing) {
- this.messages.splice(i, 1);
- break;
- }
- }
- // 此时歌词合法 下一步获取标签
- this.stateType = 4;
- this.messages.push({ role: 'ai', type: 4, content: '请输入标签' });
- }
- // if (type == 'getTags' && content && !this.tagsData) { //确保只处理一次
- // // 获取标签成功
- // this.tagsData = JSON.parse(content);
- // console.log('获取到标签', this.tagsData);
- // // 将获取到的标签数据转换为 MultiSelectPopup 需要的格式
- // this.tagOptions = this.formatTagOptions(this.tagsData);
- // }
- // {"type":"success","content":"OKOKOK"}
- // {"type":"result","content":"208"}
- if (type == 'success' && (content == 'OKOKOK' || content == 'OK')) {
- // 音乐生成开始,AI消息加载中提示
- this.messages.push({ role: 'ai', type: 20, content: '', isStartGenerating: true, startGeneratingId: 0 });
- this.isStartGenerating = true;
- this.countdownFun(3);
- this.startLoadingAnimation();
- }
- if (type == 'result' && content) {
- // 生成完成,AI消息中显示立即查看按钮
- this.stopLoadingAnimation();
- const aiMsg = this.messages.find(msg => msg.isStartGenerating);
- if (aiMsg) {
- aiMsg.isStartGenerating = false;
- aiMsg.startGeneratingId = content;
- }
- this.isStartGenerating = false;
- }
- if (type == 'error' && content) {
- uni.showToast({
- title: content,
- icon: 'none'
- })
- }
- this.scrollToBottom();
- });
- // 设置错误处理回调
- websocket.onError((error) => {
- console.error('WebSocket错误:', error);
- this.isConnected = false;
- this.handleError(error);
- });
- // 设置关闭处理回调
- websocket.onClose(() => {
- console.log('WebSocket已关闭');
- this.isConnected = false;
- this.isLoading = false;
- });
- } catch (error) {
- console.error('WebSocket初始化失败:', error);
- this.isConnected = false;
- this.handleError(error);
- }
- },
- async startStreamAnswer(content) {
- if (!content.trim()) {
- uni.showToast({
- title: '请输入问题',
- icon: 'none'
- });
- return;
- }
- // 检查连接状态,如果断开则重连
- if (!this.isConnected) {
- try {
- await this.initWebSocket();
- } catch (error) {
- console.error('重连失败:', error);
- uni.showToast({
- title: '连接已断开,请重试',
- icon: 'none'
- });
- return;
- }
- }
- console.log('发送消息:', content);
- this.resetState();
- if (this.stateTypeArray[this.stateType] != 'setLyrics' || this.musicGenre == '自定义歌词') {
- this.messages.push({ role: 'user', content });
- }
- try {
- this.isLoading = true;
- if (false) {
- let aiMsg = {
- role: 'ai',
- content: '',
- isStartGenerating: false,
- startGeneratingId: 0
- };
- this.messages.push(aiMsg);
- this.lastAiIndex = this.messages.length - 1;
- this.content = content;
- }
- // 发送消息
- const messageType = this.stateTypeArray[this.stateType];
- let messageToSend = { type: messageType, content: content };
- // 特殊处理 stateType 11 (纯音乐描述)
- if (this.stateType === 11) {
- messageToSend = { type: 'setContent', content: content };
- }
- websocket.send(JSON.stringify(messageToSend));
- // 如果是获取歌词,则显示加载中
- if (messageType === 'getLyrics') {
- this.messages.push({ role: 'ai', type: 1, content: this.loadingText + this.loadingDots, isGeneratingLyrics: true });
- this.startLoadingAnimation();
- this.scrollToBottom();
- } else if (messageType === 'setLyrics') {
- this.messages.push({ role: 'ai', type: 1, content: '正在处理中...', isProcessing: true });
- this.scrollToBottom();
- }
- } catch (error) {
- console.error('发送消息失败:', error);
- this.handleError(error);
- }
- },
- onSend() {
- if (!this.question.trim() || this.isLoading) return;
- if (this.stateType === 4) { // 当前状态是等待输入标签
- const userTagInput = this.question.trim();
- this.messages.push({ role: 'user', content: userTagInput });
- websocket.send(JSON.stringify({ type: 'setTags', content: userTagInput }));
- // this.messages.push({ role: 'ai', type: 1, content: `好的,已选择标签:${userTagInput}` });
- this.question = '';
- this.scrollToBottom();
- return; // 阻止后续的 startStreamAnswer 调用
- }
- this.startStreamAnswer(this.question);
- this.question = '';
- },
- // 发送第一步指令
- onCateSent(content) {
- if (this.isLoading || this.musicGenre) return;
- this.startStreamAnswer(content);
- this.musicGenre = content;
- },
- // 发送歌词逻辑
- onLyricSent() {
- if (this.isLoading) return;
- if (this.lyricData.includes('*')) {
- uni.showToast({
- title: '歌词有"*",请修改后再保存',
- icon: 'none'
- });
- return;
- }
- this.messages[this.messages.length - 1].content = this.lyricData;
- this.startStreamAnswer(this.lyricData);
- },
- // 打开标签选择弹窗
- openTagPopup() {
- this.$refs.tagPopup.openPopup();
- },
- formatTagOptions(tagsData) {
- console.log('格式化标签选项', tagsData);
- // tagsData 的预期格式:
- // [
- // { "name": "情感", "children": [ { "name": "欢快", "children": [] }, ... ] },
- // ...
- // ]
- // 需要转换为 MultiSelectPopup 需要的格式:
- // [
- // { label: '情感', expanded: true, children: [{ label: '欢快', value: '欢快', selected: false }, ...] },
- // ...
- // ]
- if (!Array.isArray(tagsData)) return [];
- return tagsData.map(parentTag => ({
- label: parentTag.name,
- expanded: true, // 默认展开父选项
- children: parentTag.children.map(childTag => ({
- label: childTag.name,
- value: childTag.name, // 通常value和label相同,或根据实际情况设置
- selected: false
- }))
- }));
- },
- handleTagSelection(selectedValues) {
- console.log('选中的标签:', selectedValues);
- // 处理选中的标签,例如发送到后端
- // 示例:将选中的标签数组转换为字符串发送
- const tagsString = selectedValues.join(',');
- websocket.send(JSON.stringify({ type: 'setTags', content: tagsString }));
- // 可以在这里添加AI回复,告知用户标签已选择,正在生成音乐等
- // this.messages.push({ role: 'ai', type: 1, content: `好的,已选择标签:${tagsString},小萌开始生成音乐啦!` });
- this.scrollToBottom();
- },
- handlePopupClosed() {
- // 如果用户关闭了弹窗但没有选择任何标签,可以提供一个默认行为或提示
- // 例如,如果没有选择标签,可以发送一个空数组或特定指令
- },
- startLoadingAnimation() {
- let dotCount = 1;
- this.loadingTimer = setInterval(() => {
- dotCount++;
- if (dotCount > 3) {
- dotCount = 1;
- }
- this.loadingDots = '.'.repeat(dotCount);
- // Update the loading message content if it exists
- const loadingMessage = this.messages.find(msg => msg.isGeneratingLyrics);
- if (loadingMessage) {
- loadingMessage.content = this.loadingText + this.loadingDots;
- }
- }, 500); // 更新频率,例如每500毫秒
- },
- stopLoadingAnimation() {
- if (this.loadingTimer) {
- clearInterval(this.loadingTimer);
- this.loadingTimer = null;
- }
- this.loadingDots = '...'; // Reset to default
- },
- resetState() {
- this.answer = '';
- this.displayText = '';
- this.isComplete = false;
- this.error = null;
- this.retryCount = 0;
- this.lastResponseTime = Date.now();
- },
- handleError(error) {
- if (this.retryCount < this.maxRetries) {
- this.retryCount++;
- this.retryRequest();
- } else {
- this.error = '请求失败,请稍后重试';
- this.isLoading = false;
- this.stopLoadingAnimation();
- uni.showToast({
- title: this.error,
- icon: 'none'
- });
- }
- },
- retryRequest() {
- uni.showToast({
- title: `正在重试 (${this.retryCount}/${this.maxRetries})`,
- icon: 'none'
- });
- setTimeout(() => {
- this.startStreamAnswer(this.question);
- }, 1000 * this.retryCount);
- },
- completeAnswer() {
- this.isComplete = true;
- this.isLoading = false;
- this.stopLoadingAnimation();
- },
- stopStreamAnswer() {
- if (this.timer) {
- clearTimeout(this.timer);
- this.timer = null;
- }
- this.isLoading = false;
- this.stopLoadingAnimation();
- },
- checkTimeout() {
- if (Date.now() - this.lastResponseTime > this.timeout) {
- this.handleError(new Error('请求超时'));
- }
- },
- goBack() {
- uni.navigateBack({
- delta: 1
- });
- },
- onChatScroll(e) {
- const threshold = 800; // 离底部1.0417rem以内不显示按钮
- const {
- scrollHeight,
- scrollTop
- } = e.detail;
- const clientHeight = e.detail.clientHeight || e.detail.height || 0;
-
- if (scrollHeight - scrollTop - clientHeight > threshold) {
- this.showToBottomBtn = true;
- } else {
- this.showToBottomBtn = false;
- }
- },
- onTextareaInput(e) {
- console.log(e.detail);
- // 获取textarea的实际高度
- const query = uni.createSelectorQuery().in(this);
- query.select('.input-box').boundingClientRect(data => {
- if (data) {
- // 将px转换为rpx (假设设计稿是750rpx宽度)
- const height = (data.height * 750) / uni.getSystemInfoSync().windowWidth;
- // 减去基础高度90rpx,得到额外增加的高度
- this.textareaHeight = Math.max(0, height - 90);
- // 滚动到底部
- this.scrollToBottom();
- }
- }).exec();
- },
- retrieveHistoricalRecords() {
- uni.request({
- url: this.$apiHost + '/Work/streamAnswerMusicLast',
- method: 'GET',
- header: {
- 'content-type': 'application/json',
- 'sign': getApp().globalData.headerSign
- },
- data: {
- uuid: getApp().globalData.uuid,
- task_type: 2
- },
- success: (res) => {
- // step 0 未开始 1 已选择过第一次的选项 2 获取到歌词
- // cate 选择类型 lyrics 历史记录
- console.log("获取历史记录:", res.data);
- if (res.data.success === "yes") {
- this.messages = [];
- this.musicGenre = res.data.cate;
- // 未开始
- this.messages.push({ role: 'ai', type: 0, content: ' ' });
- if (res.data.step >= 1) {
- // 获取到歌词
- this.messages.push({ role: 'user', content: res.data.cate });
- }
- if (res.data.cate == '纯音乐') {
- // 跳过获取 修改 歌词
- this.messages.push({ role: 'ai', type: 1, content: '输入描述生成音乐' });
- this.stateType = 11;
- }
- if (res.data.cate == 'AI生成歌词') {
- this.messages.push({ role: 'ai', type: 1, content: '请描述歌词' });
- this.stateType = 2;
- }
- if (res.data.cate == '自定义歌词') {
- this.messages.push({ role: 'ai', type: 1, content: '请输入歌词' });
- this.stateType = 3;
- }
- if (res.data.content) {
- this.messages.push({ role: 'user', type: 1, content: res.data.content });
- }
- // AI生成歌词逻辑
- if (res.data.lyrics) {
- this.messages.push({ role: 'ai', type: 2, content:res.data.lyrics});
- this.stateType = 3;
- this.lyricData = res.data.lyrics;
- }
- } else {
- this.messages = []
- }
- console.log("获取历史记录:", this.messages);
- },
- fail: (err) => {
- console.log('获取历史记录失败', err);
- uni.showToast({
- title: '获取历史记录失败',
- icon: 'none'
- });
- }
- })
- },
- // 重新计算元素高度
- recalculateHeights() {
- // 重新计算textarea高度
- const query = uni.createSelectorQuery().in(this);
- query.select('.input-box').boundingClientRect(data => {
- if (data) {
- const height = (data.height * 750) / uni.getSystemInfoSync().windowWidth;
- this.textareaHeight = Math.max(0, height - 90);
- }
- }).exec();
- },
- countdownFun(n) {
- if (this.timer) {
- clearInterval(this.timer);
- }
- this.countdown = n;
- // 倒计时
- this.timer = setInterval(() => {
- this.countdown--;
- if (this.countdown <= 0) {
- clearInterval(this.timer);
- }
- }, 1000);
- },
- goPage(page) {
- uni.navigateTo({
- url: page,
- });
- },
- newChar() {
- this.$refs["DialogBox"]
- .confirm({
- title: "是否创建新对话",
- content: "立即结束当前对话内容,开启新的对话?",
- DialogType: "inquiry",
- btn1: "取消",
- btn2: "确定",
- animation: 0,
- })
- .then(() => {
- websocket.send(JSON.stringify({ type: 'clear', content: '' }));
- this.toView = ''
- this.showToBottomBtn = false
- this.textareaHeight = 0
- this.isConnected = false
- this.isStartGenerating = false
- this.countdown = 0
- this.stateType = 1
- this.stateTypeArray = ['clear', 'cate', 'getLyrics', 'setLyrics', 'getTags', 'setTags', 'setContent', '', '', '', '', 'setContent']
- this.content = ''
- this.musicGenre = ''
- this.lyricData = ''
- this.retrieveHistoricalRecords()
- });
- },
- switchToNormal() {
- this.$refs["DialogBox"]
- .confirm({
- title: "是否切回常规模式",
- content: "切换至普通常规模式进行创作?",
- DialogType: "inquiry",
- btn1: "取消",
- btn2: "确定",
- animation: 0,
- })
- .then(() => {
- this.goPage('/pages/makedetail/makeMusicDetail')
- });
- },
- getTags() {
- let that = this
- uni.request({
- url: this.$apiHost + '/Work/getTags',
- method: 'GET',
- header: {
- 'content-type': 'application/json',
- 'sign': getApp().globalData.headerSign
- },
- data: {
- uuid: getApp().globalData.uuid
- },
- success: (res) => {
- console.log("获取标签:", res.data);
- if (res.data && res.data.tags) {
- this.tagOptions = this.formatTagOptions(res.data.tags);
- }
- },
- fail: (err) => {
- console.log('获取标签失败:', err);
- uni.showToast({
- title: '获取标签失败',
- icon: 'none'
- });
- }
- })
- },
- },
- async created() {
- this.getTags();
- await this.initWebSocket();
- this.retrieveHistoricalRecords();
- this.timer = setInterval(() => {
- if (this.isLoading) {
- this.checkTimeout();
- }
- }, 1000);
- uni.onKeyboardHeightChange(res => {
- this.keyboardHeight = res.height;
- if (res.height === 0) {
- this.$refs.cLottieRef.call('stop')
- } else {
- this.$refs.cLottieRef.call('play')
- }
- this.$nextTick(() => {
- this.recalculateHeights();
- this.scrollToBottom();
- });
- });
- const systemInfo = uni.getSystemInfoSync();
- this.statusBarHeight = systemInfo.statusBarHeight;
- this.windowBottom = systemInfo.safeAreaInsets ? systemInfo.safeAreaInsets.bottom : 0;
- },
- beforeDestroy() {
- websocket.close();
- this.stopStreamAnswer();
- }
- }
- </script>
- <style lang="scss">
- @import './intelligentLifeChart.scss';
- .lyricsInputBox {
- width: 100%;
- height: calc(100vh - 455rpx);
- box-sizing: border-box;
- padding: 20rpx;
- }
-
- .lyrics-input-title {
- display: flex;
- justify-content: space-between;
- align-items: center;
- height: 35px;
- background: rgba(255, 255, 255, 0.1);
- border-radius: 12rpx 36rpx 0 0;
- padding-left: 20rpx;
- padding-right: 24rpx;
- font-size: 32rpx;
- .right-btn {
- font-size: 26rpx;
- background: rgba(255, 255, 255, 0.15);
- border-radius: 22rpx;
- border: 2rpx solid rgba(255, 255, 255, 0.15);
- padding: 4rpx 32rpx;
- transition: background-color 0.2s ease, transform 0.1s ease; // 添加过渡效果
- }
- .right-btn-active {
- background: rgba(255, 255, 255, 0.3); // 点击时的深色背景
- transform: scale(0.98); // 点击时的缩小效果
- }
- }
- .lyric-editor {
- width: 100%;
- background-color: transparent;
- border: none;
- padding: 0;
- margin-top: 10rpx;
- color: rgba(255, 255, 255, 0.7);
- font-size: 32rpx;
- }
- .to-bottom-btn {
- position: fixed;
- right: 50%;
- transform: translateX(50%);
- bottom: 20rpx;
- width: 60rpx;
- height: 60rpx;
- bottom: 220rpx;
- z-index: 999;
- opacity: .75;
- transition: opacity 0.2s;
- }
- .ai-bubble-row {
- display: flex;
- align-items: flex-start;
- }
- .ai-avatar {
- width: 60rpx;
- height: 60rpx;
- border-radius: 50%;
- margin-right: 20rpx;
- border: solid 2rpx rgb(238, 238, 238, .3);
- display: flex;
- align-items: flex-end;
- justify-content: center;
- image {
- width: 52rpx;
- height: 52rpx;
- }
- }
- .ai-bubble-content {
- flex: 1;
- }
- </style>
|