intelligentMusicProduction.vue 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  1. <template>
  2. <view class="dialog-generation">
  3. <view class="subject-matter-ofText" :style="{ paddingBottom: keyboardHeight + 'px' }">
  4. <!-- 顶部导航栏 -->
  5. <div class="navbar">
  6. <view class="navbar-left">
  7. <image @click="goBack()" class="back" src="../../static/vip/hy_icon_jiantou.png"></image>
  8. <view class="elf-name">精灵·小萌</view>
  9. <image class="deepseek" src="../../static/makedetail/deepseek-logo.png"></image>
  10. </view>
  11. <view class="navbar-right">
  12. <image @click="switchToNormal()" class="primary" src="../../static/makedetail/primary.png"></image>
  13. <image @click="newChar()" class="createChat" src="../../static/makedetail/createChat.png"></image>
  14. </view>
  15. </div>
  16. <!-- <view class="navbar-reserveASeat"> </view> -->
  17. <!-- 聊天内容区 -->
  18. <!-- <scroll-view class="chat-content" :scroll-into-view="toView" scroll-y scroll-with-animation="true"
  19. :style="{ height: stateType === 3 ? 'calc(100% - 180rpx)' : `calc(100% - ${370 + textareaHeight}rpx)` }"
  20. @scroll="onChatScroll"> -->
  21. <scroll-view class="chat-content" :scroll-into-view="toView" scroll-y scroll-with-animation="true"
  22. :style="{ height: `calc(100% - ${370 + textareaHeight}rpx)` }" @scroll="onChatScroll">
  23. <template v-if="messages && messages.length > 0">
  24. <!-- <scroll-view class="chat-content" scroll-y> -->
  25. <view v-for="(msg, idx) in messages" :key="idx" :class="['chat-bubble', msg.role]">
  26. <template v-if="msg.role === 'user'">
  27. <uv-text size="32rpx" color="rgba(255, 255, 255, 0.7)" :text="msg.content"></uv-text>
  28. </template>
  29. <template v-else-if="msg.role === 'ai'">
  30. <view class="ai-bubble-row">
  31. <view class="ai-avatar">
  32. <image src="../../static/makedetail/characterProfilePicture.png" mode="aspectFill">
  33. </image>
  34. </view>
  35. <!-- 第一句话 -->
  36. <view v-if="msg.type == 0" class="ai-bubble-content">
  37. <view>欢迎来到萌创星球AI写歌🎉🎉🎉!这里能满足你创作音乐🎸、释放灵感🎵,创造独属于你的音乐旋律!
  38. </view>
  39. <br>
  40. <view>
  41. 可以根据自己的喜好点击下方的选项,快速开始创作🎼,
  42. </view>
  43. <div @click="onCateSent('纯音乐')" class="btn-box"> 纯音乐
  44. </div>
  45. <div @click="onCateSent('AI生成歌词')" class="btn-box" style="margin:0 20rpx"> AI生成歌词
  46. </div>
  47. <div @click="onCateSent('自定义歌词')" class="btn-box"> 自定义歌词
  48. </div>
  49. </view>
  50. <!-- 第二句话 -->
  51. <view class="ai-bubble-content" v-else-if="msg.type == 1">
  52. <text>{{ msg.content }}</text>
  53. </view>
  54. <!-- 第三句话 -->
  55. <view class="ai-bubble-content"
  56. style="border-radius: 12rpx 36rpx 36rpx 36rpx;border: 1px solid rgba(255,255,255,0.1);"
  57. v-else-if="msg.type == 2">
  58. <!-- <text>{{ msg.content }}</text> -->
  59. <view class="lyrics-input-title">
  60. <view>歌词</view>
  61. <view class="right-btn" :class="{'right-btn-active': isLyricConfirmActive}"
  62. @click="onLyricSent()" @touchstart="isLyricConfirmActive = true"
  63. @touchend="isLyricConfirmActive = false"
  64. @touchcancel="isLyricConfirmActive = false">确认</view>
  65. </view>
  66. <scroll-view scroll-y class="lyricsInputBox" style="">
  67. <textarea v-if="idx == (messages.length - 1 )" v-model="lyricData"
  68. class="lyric-editor" auto-height :adjust-position="false"
  69. placeholder="修改歌词..." maxlength="-1" />
  70. <uv-text v-else size="32rpx" color="rgba(255, 255, 255, 0.7)"
  71. :text="msg.content"></uv-text>
  72. </scroll-view>
  73. </view>
  74. <view class="ai-bubble-content" v-else-if="msg.type == 4">
  75. <view>请输入音乐的风格</view>
  76. <br>
  77. <view>
  78. 也可以根据自己的喜好点击下方的选项,快速开始创作🎼,
  79. </view>
  80. <div @click="openTagPopup()" class="btn-box">
  81. 选择标签
  82. </div>
  83. </view>
  84. <!-- <view class="ai-bubble-content" v-else-if="msg.type == 1">
  85. <template v-if="idx === lastAiIndex && isLoading">
  86. <text>{{ displayText }}</text>
  87. <text v-if="isLoading" class="loading-dot">...</text>
  88. </template>
  89. <template v-else>
  90. <text>{{ msg.content }}</text>
  91. </template>
  92. </view> -->
  93. <view v-else-if="msg.type == 20" class="ai-bubble-content">
  94. <text>OK!~小萌开始生成音乐啦!</text><br>
  95. <div v-if="msg.isStartGenerating && msg.startGeneratingId == 0" class="btn-box">
  96. 点击查看
  97. ({{ countdown }}) s</div>
  98. <div v-else
  99. @click="goPage(`/pages/makedetail/makeMusicDetail?id=${msg.startGeneratingId}`)"
  100. class="btn-box"> 点击查看
  101. </div>
  102. </view>
  103. </view>
  104. </template>
  105. </view>
  106. <view id="bottom-anchor"></view>
  107. <view v-if="error" class="chat-bubble ai error">{{ error }}</view>
  108. <view :style="{ height: stateType !== 3 || musicGenre == '自定义歌词' ? '200rpx' : '80rpx' }"></view>
  109. <image v-if="showToBottomBtn && keyboardHeight === 0" class="to-bottom-btn" @click="scrollToBottom"
  110. src="../../static/makedetail/toBottomBtn.png"></image>
  111. </template>
  112. <template v-else>
  113. <view class="chat-content-empty">
  114. <image src="../../static/makedetail/characterProfilePicture.png" mode="aspectFill"></image>
  115. <view class="chat-content-empty-title">嗨!我是创梦精灵</view>
  116. <view class="chat-content-empty-desc">与我聊聊你想要生成的角色吧!</view>
  117. </view>
  118. </template>
  119. </scroll-view>
  120. <!-- <view class="bom-reserveASeat"></view> -->
  121. <view v-if="stateType !== 3 || musicGenre == '自定义歌词'" class="bom-box"
  122. :style="{ bottom: 0 + 'px', height: `${190 + textareaHeight}rpx` }">
  123. <view class="bom-box-bg">
  124. <c-lottie ref="cLottieRef" :src='"/static/lottie/xiaomeng.json"' class="icon-img" height="108rpx"
  125. width="112rpx" :loop="true" :autoPlay="false"></c-lottie>
  126. </view>
  127. <!-- 底部输入区 -->
  128. <view class="input-bar">
  129. <!-- <image class="icon-img" src="../../static/makedetail/characterProfilePicture.png" mode="aspectFill">
  130. </image> -->
  131. <textarea :autoHeight="true" class="input-box" :adjust-position="false" v-model="question"
  132. :disabled="isLoading" placeholder="给我发送消息吧..." maxlength="400" @input="onTextareaInput"
  133. confirm-type="search" @confirm="onSend" rows="1"
  134. style="overflow-y:auto;max-height:176rpx;min-height:44rpx;" />
  135. <!-- <button class="send-btn" :disabled="isLoading || !question.trim()" @click="onSend">{{ isLoading ?
  136. '生成中...' :
  137. '发送' }}</button> -->
  138. <image v-if="isLoading" class="stop" src="../../static/makedetail/stop.png" mode=""
  139. @click="stopStreamAnswer" />
  140. <image v-else-if="!isLoading && !question.trim() && keyboardHeight === 0" class="keyboard"
  141. src="../../static/makedetail/keyboard.png" mode="" />
  142. <image v-else-if="!isLoading && question.trim() && keyboardHeight !== 0" class="send"
  143. src="../../static/makedetail/send.png" mode="" @click="onSend" />
  144. </view>
  145. <!-- 底部提示 -->
  146. <view class="footer-tip">内容由AI生成,禁用相关功能请联系管理员。</view>
  147. </view>
  148. </view>
  149. <DialogBox ref="DialogBox"></DialogBox>
  150. <multi-select-popup ref="tagPopup" :initial-options="tagOptions" @selection-confirmed="handleTagSelection"
  151. @popup-closed="handlePopupClosed"></multi-select-popup>
  152. </view>
  153. </template>
  154. <script>
  155. import websocket from '@/common/websocket.js';
  156. import MultiSelectPopup from '@/components/MultiSelectPopup/MultiSelectPopup.vue';
  157. export default {
  158. components: {
  159. MultiSelectPopup
  160. },
  161. data() {
  162. return {
  163. question: '',
  164. answer: '',
  165. displayText: '',
  166. isComplete: false,
  167. isLoading: false,
  168. loadingText: '正在生成歌词,请稍候',
  169. loadingDots: '...',
  170. loadingTimer: null,
  171. error: null,
  172. retryCount: 0,
  173. maxRetries: 3,
  174. timer: null,
  175. lastResponseTime: 0,
  176. timeout: 30000,
  177. typingSpeed: 50,
  178. messages: [],
  179. lastAiIndex: -1,
  180. keyboardHeight: 0,
  181. statusBarHeight: 0,
  182. windowBottom: 0,
  183. toView: '',
  184. showToBottomBtn: false,
  185. textareaHeight: 0,
  186. isConnected: false,
  187. isStartGenerating: false,
  188. countdown: 0,
  189. stateType: 1,
  190. isLyricConfirmActive: false, // 用于歌词确认按钮的点击反馈
  191. stateTypeArray: ['clear', 'cate', 'getLyrics', 'setLyrics', 'getTags', 'setTags', 'setContent', '', '', '',
  192. '', 'setContent'
  193. ],
  194. content: '',
  195. musicGenre: '',
  196. lyricData: '',
  197. tagsData: '',
  198. tagOptions: [], // 用于存储多选弹窗的选项
  199. // type = clear 清除|cate 第一句话选择 |getLyrics 获取歌词(需要上传描述)|setLyrics 修改歌词(再去判断是否有* 有重复没有下一步 没有展示标签)|setTags(修改标签) (下发一个static 失败有提示 成功就正常生成)|getTags 获取标签
  200. };
  201. },
  202. methods: {
  203. // /Work/streamAnswerMusicLast
  204. // /Work/streamAnswerMusic
  205. // {type:'',content:''}
  206. // type = clear 清除|cate 第一句话选择 |getLyrics 获取歌词(需要上传描述)|setLyrics 修改歌词(再去判断是否有* 有重复没有下一步 没有展示标签)|setTags(修改标签) (下发一个static 失败有提示 成功就正常生成)|getTags 获取标签
  207. formatMsgContent(content) {
  208. if (!content) return [];
  209. // 将换行符替换为 <br/>
  210. return content.replace(/\n/g, '<br/>');;
  211. },
  212. scrollToBottom() {
  213. this.toView = '';
  214. this.$nextTick(() => {
  215. this.toView = 'bottom-anchor';
  216. });
  217. },
  218. async initWebSocket() {
  219. if (this.isConnected) return;
  220. try {
  221. // 发送初始化消息
  222. websocket.onOpen(() => {
  223. // setTimeout(() => {
  224. // websocket.send(JSON.stringify({ type: 'getTags', content: '' }));
  225. // }, 1000)
  226. });
  227. await websocket.connect('wss://e.zhichao.art/Gapi/Work/streamAnswerMusic', {
  228. uuid: getApp().globalData.uuid
  229. });
  230. this.isConnected = true;
  231. // 设置消息处理回调
  232. websocket.onMessage((data) => {
  233. console.log("data:", data);
  234. if (data && data.includes('{')) {
  235. var data = JSON.parse(data);
  236. }
  237. var type = data && data.type || '';
  238. var content = data && data.content || '';
  239. // type = error,
  240. // content=no_cate(首次分类没有设置)
  241. // content="歌词不合规,请重新输入"|"歌词中有违规禁词,请修改!"
  242. // type='lyrics',content=具体歌词
  243. // type=success,content='OKOKOK':提交成功
  244. // type=result,content=ID
  245. // 111111111111111111111111111111111
  246. // if (type === 'success' && content === 'OKOKOK') {
  247. // // 音乐生成开始,AI消息加载中提示
  248. // const aiMsg = this.messages[this.lastAiIndex];
  249. // if (aiMsg) {
  250. // aiMsg.isStartGenerating = true;
  251. // aiMsg.content = '音乐生成中,请稍候...';
  252. // }
  253. // this.isStartGenerating = true;
  254. // this.countdownFun(20);
  255. // }
  256. // if (type === 'result' && content) {
  257. // // 音乐生成中,显示跳转到音乐详情页按钮
  258. // const aiMsg = this.messages[this.lastAiIndex];
  259. // if (aiMsg) {
  260. // aiMsg.isStartGenerating = false;
  261. // aiMsg.startGeneratingId = content;
  262. // aiMsg.content = '音乐已生成,点击跳转到详情页';
  263. // }
  264. // }
  265. if (this.isLoading) {
  266. if (type == 'cate' && content == 'success') {
  267. // {"type":"setContent","content":content}
  268. // 此时 第第一步选择完成
  269. if (this.musicGenre == '纯音乐') {
  270. // 跳过获取 修改 歌词
  271. this.stateType = 11;
  272. this.messages.push({
  273. role: 'ai',
  274. type: 1,
  275. content: '输入描述生成音乐'
  276. });
  277. }
  278. if (this.musicGenre == 'AI生成歌词') {
  279. this.messages.push({
  280. role: 'ai',
  281. type: 1,
  282. content: '请描述歌词'
  283. });
  284. this.stateType = 2;
  285. }
  286. if (this.musicGenre == '自定义歌词') {
  287. this.messages.push({
  288. role: 'ai',
  289. type: 1,
  290. content: '请输入歌词'
  291. });
  292. this.stateType = 3;
  293. }
  294. console.log(type, content);
  295. // {"type":"lyrics","content":"《古风之约》\n\n青山绿水间 桃花映人面\n清风拂衣袖 相思绕指尖\n亭台楼阁畔 琴声悠扬传\n往事如烟云 飘散在天边\n\n红墙绿瓦下 谁在等归雁\n明月照窗前 孤影难入眠\n一纸素笺上 写满了思念\n岁月如流水 匆匆又一年\n\n我与你共赴 这一场古风之约\n看那春花秋月 浪漫又缠绵\n执手相看泪眼 无语凝噎\n只愿与你相伴 直到永远\n\n我与你共赴 这一场古风之约\n听那琵琶弦上 倾诉着哀怨\n举杯对饮成三人 醉在花间\n只愿与你相守 岁岁年年"}
  296. }
  297. // if (data.includes('[DONE]')) {
  298. // // 结束生成
  299. // this.completeAnswer();
  300. // } else if (data.includes('ID:')) {
  301. // // 提取ID
  302. // const aiMsg = this.messages[this.lastAiIndex];
  303. // if (aiMsg) {
  304. // aiMsg.startGeneratingId = data.split(':')[1];
  305. // }
  306. // } else if (data.includes('OKOKOK')) {
  307. // const aiMsg = this.messages[this.lastAiIndex];
  308. // if (aiMsg) {
  309. // aiMsg.isStartGenerating = true;
  310. // }
  311. // this.isStartGenerating = true;
  312. // this.countdownFun(20);
  313. // } else {
  314. // const aiMsg = this.messages[this.lastAiIndex];
  315. // if (aiMsg) {
  316. // aiMsg.isStartGenerating = false;
  317. // this.displayText += data;
  318. // this.answer += data;
  319. // aiMsg.content += data;
  320. // this.retryCount = 0;
  321. // this.lastResponseTime = Date.now();
  322. // this.scrollToBottom();
  323. // }
  324. // }
  325. this.isLoading = false;
  326. }
  327. if (type == 'lyrics' && content) {
  328. console.log('获取到歌词', content);
  329. // 替换加载中的消息
  330. const lastMessageIndex = this.messages.length - 1;
  331. // 替换加载中的消息
  332. this.stopLoadingAnimation();
  333. // Find and remove the loading message
  334. for (let i = this.messages.length - 1; i >= 0; i--) {
  335. if (this.messages[i].isGeneratingLyrics) {
  336. this.messages.splice(i, 1);
  337. break;
  338. }
  339. }
  340. // 移除 isProcessing 的消息
  341. for (let i = this.messages.length - 1; i >= 0; i--) {
  342. if (this.messages[i].isProcessing) {
  343. this.messages.splice(i, 1);
  344. break;
  345. }
  346. }
  347. this.messages.push({
  348. role: 'ai',
  349. type: 2,
  350. content: content
  351. });
  352. this.stateType = 3;
  353. this.lyricData = content;
  354. console.log(this.messages, 24);
  355. }
  356. if (type == 'tags' && content) {
  357. // 移除 isProcessing 的消息
  358. for (let i = this.messages.length - 1; i >= 0; i--) {
  359. if (this.messages[i].isProcessing) {
  360. this.messages.splice(i, 1);
  361. break;
  362. }
  363. }
  364. // 此时歌词合法 下一步获取标签
  365. this.stateType = 4;
  366. this.messages.push({
  367. role: 'ai',
  368. type: 4,
  369. content: '请输入标签'
  370. });
  371. }
  372. // if (type == 'getTags' && content && !this.tagsData) { //确保只处理一次
  373. // // 获取标签成功
  374. // this.tagsData = JSON.parse(content);
  375. // console.log('获取到标签', this.tagsData);
  376. // // 将获取到的标签数据转换为 MultiSelectPopup 需要的格式
  377. // this.tagOptions = this.formatTagOptions(this.tagsData);
  378. // }
  379. // {"type":"success","content":"OKOKOK"}
  380. // {"type":"result","content":"208"}
  381. if (type == 'success' && (content == 'OKOKOK' || content == 'OK')) {
  382. // 音乐生成开始,AI消息加载中提示
  383. this.messages.push({
  384. role: 'ai',
  385. type: 20,
  386. content: '',
  387. isStartGenerating: true,
  388. startGeneratingId: 0
  389. });
  390. this.isStartGenerating = true;
  391. this.countdownFun(3);
  392. this.startLoadingAnimation();
  393. }
  394. if (type == 'result' && content) {
  395. // 生成完成,AI消息中显示立即查看按钮
  396. this.stopLoadingAnimation();
  397. const aiMsg = this.messages.find(msg => msg.isStartGenerating);
  398. if (aiMsg) {
  399. aiMsg.isStartGenerating = false;
  400. aiMsg.startGeneratingId = content;
  401. }
  402. this.isStartGenerating = false;
  403. }
  404. if (type == 'error' && content) {
  405. uni.showToast({
  406. title: content,
  407. icon: 'none'
  408. })
  409. }
  410. this.scrollToBottom();
  411. });
  412. // 设置错误处理回调
  413. websocket.onError((error) => {
  414. console.error('WebSocket错误:', error);
  415. this.isConnected = false;
  416. this.handleError(error);
  417. });
  418. // 设置关闭处理回调
  419. websocket.onClose(() => {
  420. console.log('WebSocket已关闭');
  421. this.isConnected = false;
  422. this.isLoading = false;
  423. });
  424. } catch (error) {
  425. console.error('WebSocket初始化失败:', error);
  426. this.isConnected = false;
  427. this.handleError(error);
  428. }
  429. },
  430. async startStreamAnswer(content) {
  431. if (!content.trim()) {
  432. uni.showToast({
  433. title: '请输入问题',
  434. icon: 'none'
  435. });
  436. return;
  437. }
  438. // 检查连接状态,如果断开则重连
  439. if (!this.isConnected) {
  440. try {
  441. await this.initWebSocket();
  442. } catch (error) {
  443. console.error('重连失败:', error);
  444. uni.showToast({
  445. title: '连接已断开,请重试',
  446. icon: 'none'
  447. });
  448. return;
  449. }
  450. }
  451. console.log('发送消息:', content);
  452. this.resetState();
  453. if (this.stateTypeArray[this.stateType] != 'setLyrics' || this.musicGenre == '自定义歌词') {
  454. this.messages.push({
  455. role: 'user',
  456. content
  457. });
  458. }
  459. try {
  460. this.isLoading = true;
  461. if (false) {
  462. let aiMsg = {
  463. role: 'ai',
  464. content: '',
  465. isStartGenerating: false,
  466. startGeneratingId: 0
  467. };
  468. this.messages.push(aiMsg);
  469. this.lastAiIndex = this.messages.length - 1;
  470. this.content = content;
  471. }
  472. // 发送消息
  473. const messageType = this.stateTypeArray[this.stateType];
  474. let messageToSend = {
  475. type: messageType,
  476. content: content
  477. };
  478. // 特殊处理 stateType 11 (纯音乐描述)
  479. if (this.stateType === 11) {
  480. messageToSend = {
  481. type: 'setContent',
  482. content: content
  483. };
  484. }
  485. websocket.send(JSON.stringify(messageToSend));
  486. // 如果是获取歌词,则显示加载中
  487. if (messageType === 'getLyrics') {
  488. this.messages.push({
  489. role: 'ai',
  490. type: 1,
  491. content: this.loadingText + this.loadingDots,
  492. isGeneratingLyrics: true
  493. });
  494. this.startLoadingAnimation();
  495. this.scrollToBottom();
  496. } else if (messageType === 'setLyrics') {
  497. this.messages.push({
  498. role: 'ai',
  499. type: 1,
  500. content: '正在处理中...',
  501. isProcessing: true
  502. });
  503. this.scrollToBottom();
  504. }
  505. } catch (error) {
  506. console.error('发送消息失败:', error);
  507. this.handleError(error);
  508. }
  509. },
  510. onSend() {
  511. if (!this.question.trim() || this.isLoading) return;
  512. if (this.stateType === 4) { // 当前状态是等待输入标签
  513. const userTagInput = this.question.trim();
  514. this.messages.push({
  515. role: 'user',
  516. content: userTagInput
  517. });
  518. websocket.send(JSON.stringify({
  519. type: 'setTags',
  520. content: userTagInput
  521. }));
  522. // this.messages.push({ role: 'ai', type: 1, content: `好的,已选择标签:${userTagInput}` });
  523. this.question = '';
  524. this.scrollToBottom();
  525. return; // 阻止后续的 startStreamAnswer 调用
  526. }
  527. this.startStreamAnswer(this.question);
  528. this.question = '';
  529. },
  530. // 发送第一步指令
  531. onCateSent(content) {
  532. if (this.isLoading || this.musicGenre) return;
  533. this.startStreamAnswer(content);
  534. this.musicGenre = content;
  535. },
  536. // 发送歌词逻辑
  537. onLyricSent() {
  538. if (this.isLoading) return;
  539. if (this.lyricData.includes('*')) {
  540. uni.showToast({
  541. title: '歌词有"*",请修改后再保存',
  542. icon: 'none'
  543. });
  544. return;
  545. }
  546. this.messages[this.messages.length - 1].content = this.lyricData;
  547. this.startStreamAnswer(this.lyricData);
  548. },
  549. // 打开标签选择弹窗
  550. openTagPopup() {
  551. this.$refs.tagPopup.openPopup();
  552. },
  553. formatTagOptions(tagsData) {
  554. console.log('格式化标签选项', tagsData);
  555. // tagsData 的预期格式:
  556. // [
  557. // { "name": "情感", "children": [ { "name": "欢快", "children": [] }, ... ] },
  558. // ...
  559. // ]
  560. // 需要转换为 MultiSelectPopup 需要的格式:
  561. // [
  562. // { label: '情感', expanded: true, children: [{ label: '欢快', value: '欢快', selected: false }, ...] },
  563. // ...
  564. // ]
  565. if (!Array.isArray(tagsData)) return [];
  566. return tagsData.map(parentTag => ({
  567. label: parentTag.name,
  568. expanded: true, // 默认展开父选项
  569. children: parentTag.children.map(childTag => ({
  570. label: childTag.name,
  571. value: childTag.name, // 通常value和label相同,或根据实际情况设置
  572. selected: false
  573. }))
  574. }));
  575. },
  576. handleTagSelection(selectedValues) {
  577. console.log('选中的标签:', selectedValues);
  578. // 处理选中的标签,例如发送到后端
  579. // 示例:将选中的标签数组转换为字符串发送
  580. const tagsString = selectedValues.join(',');
  581. websocket.send(JSON.stringify({
  582. type: 'setTags',
  583. content: tagsString
  584. }));
  585. // 可以在这里添加AI回复,告知用户标签已选择,正在生成音乐等
  586. // this.messages.push({ role: 'ai', type: 1, content: `好的,已选择标签:${tagsString},小萌开始生成音乐啦!` });
  587. this.scrollToBottom();
  588. },
  589. handlePopupClosed() {
  590. // 如果用户关闭了弹窗但没有选择任何标签,可以提供一个默认行为或提示
  591. // 例如,如果没有选择标签,可以发送一个空数组或特定指令
  592. },
  593. startLoadingAnimation() {
  594. let dotCount = 1;
  595. this.loadingTimer = setInterval(() => {
  596. dotCount++;
  597. if (dotCount > 3) {
  598. dotCount = 1;
  599. }
  600. this.loadingDots = '.'.repeat(dotCount);
  601. // Update the loading message content if it exists
  602. const loadingMessage = this.messages.find(msg => msg.isGeneratingLyrics);
  603. if (loadingMessage) {
  604. loadingMessage.content = this.loadingText + this.loadingDots;
  605. }
  606. }, 500); // 更新频率,例如每500毫秒
  607. },
  608. stopLoadingAnimation() {
  609. if (this.loadingTimer) {
  610. clearInterval(this.loadingTimer);
  611. this.loadingTimer = null;
  612. }
  613. this.loadingDots = '...'; // Reset to default
  614. },
  615. resetState() {
  616. this.answer = '';
  617. this.displayText = '';
  618. this.isComplete = false;
  619. this.error = null;
  620. this.retryCount = 0;
  621. this.lastResponseTime = Date.now();
  622. },
  623. handleError(error) {
  624. if (this.retryCount < this.maxRetries) {
  625. this.retryCount++;
  626. this.retryRequest();
  627. } else {
  628. this.error = '请求失败,请稍后重试';
  629. this.isLoading = false;
  630. this.stopLoadingAnimation();
  631. uni.showToast({
  632. title: this.error,
  633. icon: 'none'
  634. });
  635. }
  636. },
  637. retryRequest() {
  638. uni.showToast({
  639. title: `正在重试 (${this.retryCount}/${this.maxRetries})`,
  640. icon: 'none'
  641. });
  642. setTimeout(() => {
  643. this.startStreamAnswer(this.question);
  644. }, 1000 * this.retryCount);
  645. },
  646. completeAnswer() {
  647. this.isComplete = true;
  648. this.isLoading = false;
  649. this.stopLoadingAnimation();
  650. },
  651. stopStreamAnswer() {
  652. if (this.timer) {
  653. clearTimeout(this.timer);
  654. this.timer = null;
  655. }
  656. this.isLoading = false;
  657. this.stopLoadingAnimation();
  658. },
  659. checkTimeout() {
  660. if (Date.now() - this.lastResponseTime > this.timeout) {
  661. this.handleError(new Error('请求超时'));
  662. }
  663. },
  664. goBack() {
  665. uni.navigateBack({
  666. delta: 1
  667. });
  668. },
  669. onChatScroll(e) {
  670. const threshold = 800; // 离底部1.0417rem以内不显示按钮
  671. const {
  672. scrollHeight,
  673. scrollTop
  674. } = e.detail;
  675. const clientHeight = e.detail.clientHeight || e.detail.height || 0;
  676. if (scrollHeight - scrollTop - clientHeight > threshold) {
  677. this.showToBottomBtn = true;
  678. } else {
  679. this.showToBottomBtn = false;
  680. }
  681. },
  682. onTextareaInput(e) {
  683. console.log(e.detail);
  684. // 获取textarea的实际高度
  685. const query = uni.createSelectorQuery().in(this);
  686. query.select('.input-box').boundingClientRect(data => {
  687. if (data) {
  688. // 将px转换为rpx (假设设计稿是750rpx宽度)
  689. const height = (data.height * 750) / uni.getSystemInfoSync().windowWidth;
  690. // 减去基础高度90rpx,得到额外增加的高度
  691. this.textareaHeight = Math.max(0, height - 90);
  692. // 滚动到底部
  693. this.scrollToBottom();
  694. }
  695. }).exec();
  696. },
  697. retrieveHistoricalRecords() {
  698. uni.request({
  699. url: this.$apiHost + '/Work/streamAnswerMusicLast',
  700. method: 'GET',
  701. header: {
  702. 'content-type': 'application/json',
  703. 'sign': getApp().globalData.headerSign
  704. },
  705. data: {
  706. uuid: getApp().globalData.uuid,
  707. task_type: 2
  708. },
  709. success: (res) => {
  710. // step 0 未开始 1 已选择过第一次的选项 2 获取到歌词
  711. // cate 选择类型 lyrics 历史记录
  712. console.log("获取历史记录:", res.data);
  713. if (res.data.success === "yes") {
  714. this.messages = [];
  715. this.musicGenre = res.data.cate;
  716. // 未开始
  717. this.messages.push({
  718. role: 'ai',
  719. type: 0,
  720. content: ' '
  721. });
  722. if (res.data.step >= 1) {
  723. // 获取到歌词
  724. this.messages.push({
  725. role: 'user',
  726. content: res.data.cate
  727. });
  728. }
  729. if (res.data.cate == '纯音乐') {
  730. // 跳过获取 修改 歌词
  731. this.messages.push({
  732. role: 'ai',
  733. type: 1,
  734. content: '输入描述生成音乐'
  735. });
  736. this.stateType = 11;
  737. }
  738. if (res.data.cate == 'AI生成歌词') {
  739. this.messages.push({
  740. role: 'ai',
  741. type: 1,
  742. content: '请描述歌词'
  743. });
  744. this.stateType = 2;
  745. }
  746. if (res.data.cate == '自定义歌词') {
  747. this.messages.push({
  748. role: 'ai',
  749. type: 1,
  750. content: '请输入歌词'
  751. });
  752. this.stateType = 3;
  753. }
  754. if (res.data.content) {
  755. this.messages.push({
  756. role: 'user',
  757. type: 1,
  758. content: res.data.content
  759. });
  760. }
  761. // AI生成歌词逻辑
  762. if (res.data.lyrics) {
  763. this.messages.push({
  764. role: 'ai',
  765. type: 2,
  766. content: res.data.lyrics
  767. });
  768. this.stateType = 3;
  769. this.lyricData = res.data.lyrics;
  770. }
  771. } else {
  772. this.messages = []
  773. }
  774. console.log("获取历史记录:", this.messages);
  775. },
  776. fail: (err) => {
  777. console.log('获取历史记录失败', err);
  778. uni.showToast({
  779. title: '获取历史记录失败',
  780. icon: 'none'
  781. });
  782. }
  783. })
  784. },
  785. // 重新计算元素高度
  786. recalculateHeights() {
  787. // 重新计算textarea高度
  788. const query = uni.createSelectorQuery().in(this);
  789. query.select('.input-box').boundingClientRect(data => {
  790. if (data) {
  791. const height = (data.height * 750) / uni.getSystemInfoSync().windowWidth;
  792. this.textareaHeight = Math.max(0, height - 90);
  793. }
  794. }).exec();
  795. },
  796. countdownFun(n) {
  797. if (this.timer) {
  798. clearInterval(this.timer);
  799. }
  800. this.countdown = n;
  801. // 倒计时
  802. this.timer = setInterval(() => {
  803. this.countdown--;
  804. if (this.countdown <= 0) {
  805. clearInterval(this.timer);
  806. }
  807. }, 1000);
  808. },
  809. goPage(page) {
  810. uni.navigateTo({
  811. url: page,
  812. });
  813. },
  814. newChar() {
  815. this.$refs["DialogBox"]
  816. .confirm({
  817. title: "是否创建新对话",
  818. content: "立即结束当前对话内容,开启新的对话?",
  819. DialogType: "inquiry",
  820. btn1: "取消",
  821. btn2: "确定",
  822. animation: 0,
  823. })
  824. .then(() => {
  825. websocket.send(JSON.stringify({
  826. type: 'clear',
  827. content: ''
  828. }));
  829. this.toView = ''
  830. this.showToBottomBtn = false
  831. this.textareaHeight = 0
  832. this.isConnected = false
  833. this.isStartGenerating = false
  834. this.countdown = 0
  835. this.stateType = 1
  836. this.stateTypeArray = ['clear', 'cate', 'getLyrics', 'setLyrics', 'getTags', 'setTags',
  837. 'setContent', '', '', '', '', 'setContent'
  838. ]
  839. this.content = ''
  840. this.musicGenre = ''
  841. this.lyricData = ''
  842. this.retrieveHistoricalRecords()
  843. });
  844. },
  845. switchToNormal() {
  846. this.$refs["DialogBox"]
  847. .confirm({
  848. title: "是否切回常规模式",
  849. content: "切换至普通常规模式进行创作?",
  850. DialogType: "inquiry",
  851. btn1: "取消",
  852. btn2: "确定",
  853. animation: 0,
  854. })
  855. .then(() => {
  856. this.goPage('/pages/makedetail/makeMusicDetail')
  857. });
  858. },
  859. getTags() {
  860. let that = this
  861. uni.request({
  862. url: this.$apiHost + '/Work/getTags',
  863. method: 'GET',
  864. header: {
  865. 'content-type': 'application/json',
  866. 'sign': getApp().globalData.headerSign
  867. },
  868. data: {
  869. uuid: getApp().globalData.uuid
  870. },
  871. success: (res) => {
  872. console.log("获取标签:", res.data);
  873. if (res.data && res.data.tags) {
  874. this.tagOptions = this.formatTagOptions(res.data.tags);
  875. }
  876. },
  877. fail: (err) => {
  878. console.log('获取标签失败:', err);
  879. uni.showToast({
  880. title: '获取标签失败',
  881. icon: 'none'
  882. });
  883. }
  884. })
  885. },
  886. },
  887. async created() {
  888. this.getTags();
  889. await this.initWebSocket();
  890. this.retrieveHistoricalRecords();
  891. this.timer = setInterval(() => {
  892. if (this.isLoading) {
  893. this.checkTimeout();
  894. }
  895. }, 1000);
  896. uni.onKeyboardHeightChange(res => {
  897. this.keyboardHeight = res.height;
  898. if (res.height === 0) {
  899. this.$refs.cLottieRef.call('stop')
  900. } else {
  901. this.$refs.cLottieRef.call('play')
  902. }
  903. this.$nextTick(() => {
  904. this.recalculateHeights();
  905. this.scrollToBottom();
  906. });
  907. });
  908. const systemInfo = uni.getSystemInfoSync();
  909. this.statusBarHeight = systemInfo.statusBarHeight;
  910. this.windowBottom = systemInfo.safeAreaInsets ? systemInfo.safeAreaInsets.bottom : 0;
  911. },
  912. beforeDestroy() {
  913. websocket.close();
  914. this.stopStreamAnswer();
  915. }
  916. }
  917. </script>
  918. <style lang="scss">
  919. @import './intelligentLifeChart.scss';
  920. .lyricsInputBox {
  921. width: 100%;
  922. height: calc(100vh - 455rpx);
  923. box-sizing: border-box;
  924. padding: 20rpx;
  925. }
  926. .lyrics-input-title {
  927. display: flex;
  928. justify-content: space-between;
  929. align-items: center;
  930. height: 35px;
  931. background: rgba(255, 255, 255, 0.1);
  932. border-radius: 12rpx 36rpx 0 0;
  933. padding-left: 20rpx;
  934. padding-right: 24rpx;
  935. font-size: 32rpx;
  936. .right-btn {
  937. font-size: 26rpx;
  938. background: rgba(255, 255, 255, 0.15);
  939. border-radius: 22rpx;
  940. border: 2rpx solid rgba(255, 255, 255, 0.15);
  941. padding: 4rpx 32rpx;
  942. transition: background-color 0.2s ease, transform 0.1s ease; // 添加过渡效果
  943. }
  944. .right-btn-active {
  945. background: rgba(255, 255, 255, 0.3); // 点击时的深色背景
  946. transform: scale(0.98); // 点击时的缩小效果
  947. }
  948. }
  949. .lyric-editor {
  950. width: 100%;
  951. background-color: transparent;
  952. border: none;
  953. padding: 0;
  954. margin-top: 10rpx;
  955. color: rgba(255, 255, 255, 0.7);
  956. font-size: 32rpx;
  957. }
  958. .to-bottom-btn {
  959. position: fixed;
  960. right: 50%;
  961. transform: translateX(50%);
  962. bottom: 20rpx;
  963. width: 60rpx;
  964. height: 60rpx;
  965. bottom: 220rpx;
  966. z-index: 999;
  967. opacity: .75;
  968. transition: opacity 0.2s;
  969. }
  970. .ai-bubble-row {
  971. display: flex;
  972. align-items: flex-start;
  973. }
  974. .ai-avatar {
  975. width: 60rpx;
  976. height: 60rpx;
  977. border-radius: 50%;
  978. margin-right: 20rpx;
  979. border: solid 2rpx rgba(238, 238, 238, .3);
  980. display: flex;
  981. align-items: flex-end;
  982. justify-content: center;
  983. image {
  984. width: 52rpx;
  985. height: 52rpx;
  986. }
  987. }
  988. .ai-bubble-content {
  989. flex: 1;
  990. }
  991. </style>