intelligentMusicProduction.vue 25 KB

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