intelligentMusicProduction.vue 31 KB

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