Jelajahi Sumber

完成智能音乐生成部分

XSXS 6 bulan lalu
induk
melakukan
fb3df4f578

+ 125 - 86
components/MultiSelectPopup/MultiSelectPopup.vue

@@ -1,33 +1,35 @@
 <template>
 	<view v-if="visible" class="popup-overlay" @click.self="closePopup">
-		<view class="popup-content">
-			<view class="popup-header">
-				<text class="popup-action popup-cancel" @click="closePopup">取消</text>
-				<text class="popup-action popup-confirm" @click="confirmSelection">确定</text>
-			</view>
-			<view class="picker-container">
-				<scroll-view scroll-y class="column parent-column">
-					<view v-for="(parent, index) in options" :key="index" 
-					      class="column-item parent-item"
-					      :class="{ 'active': activeParentIndex === index }"
-					      @click="setActiveParent(index)">
+		<view class="popup-content" @click.stop>
+			<!-- Parent Categories as Tabs -->
+			<view class="parent-tabs">
+				<scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false"
+					:scroll-into-view="'tab-' + activeParentIndex - 1" scroll-with-animation>
+					<view v-for="(parent, index) in options" :key="index" :id="'tab-' + index" class="tab-item"
+						:class="{ 'active': activeParentIndex === index }" @click="setActiveParent(index)">
 						{{ parent.label }}
 					</view>
 				</scroll-view>
-				<scroll-view scroll-y class="column child-column">
-					<view v-if="activeParentIndex !== null && currentChildOptions.length > 0">
-						<view v-for="(child, index) in currentChildOptions" :key="index"
-						      class="column-item child-item"
-						      :class="{ 'selected': selectedChildValues.includes(child.value) }"
-						      @click="selectChild(child)">
-							{{ child.label }}
-							<view v-if="selectedChildValues.includes(child.value)" class="checkmark">✓</view>
-						</view>
-					</view>
-					<view v-else-if="activeParentIndex !== null && currentChildOptions.length === 0" class="no-children-text">
-						暂无选项
+			</view>
+
+			<!-- Child Items as Tags in a Grid -->
+			<scroll-view scroll-y class="child-tags-container">
+				<view class="tags-grid" v-if="activeParentIndex !== null && currentChildOptions.length > 0">
+					<view v-for="(child, cIndex) in currentChildOptions" :key="child.value + '-' + cIndex"
+						class="tag-item" :class="{ 'selected': selectedChildValues.includes(child.value) }"
+						@click="selectChild(child)">
+						{{ child.label }}
 					</view>
-				</scroll-view>
+				</view>
+				<view v-else-if="activeParentIndex !== null && currentChildOptions.length === 0"
+					class="no-children-text">
+					暂无子选项
+				</view>
+			</scroll-view>
+
+			<!-- Confirm Button -->
+			<view class="popup-footer">
+				<view class="confirm-btn" @click="confirmSelection">确认</view>
 			</view>
 		</view>
 	</view>
@@ -39,7 +41,7 @@ export default {
 	props: {
 		initialOptions: {
 			type: Array,
-			default: () => [] 
+			default: () => []
 		},
 		initialSelectedValues: { // Optional: to pre-select items
 			type: Array,
@@ -67,10 +69,12 @@ export default {
 			immediate: true,
 			handler(newVal) {
 				this.options = JSON.parse(JSON.stringify(newVal));
+				// Ensure selectedChildValues is initialized from props when options load
 				this.selectedChildValues = [...this.initialSelectedValues];
-				
+
 				if (this.options.length > 0) {
-					let preselectedParentIndex = 0; 
+					let preselectedParentIndex = 0;
+					// Find the first parent that has any of the initially selected children
 					for (let i = 0; i < this.options.length; i++) {
 						const parent = this.options[i];
 						if (parent.children && parent.children.some(c => this.initialSelectedValues.includes(c.value))) {
@@ -142,7 +146,7 @@ export default {
 	left: 0;
 	width: 100%;
 	height: 100%;
-	background-color: rgba(0, 0, 0, 0.5);
+	background-color: rgba(0, 0, 0, 0.7); // Darker overlay
 	display: flex;
 	align-items: flex-end;
 	justify-content: center;
@@ -150,96 +154,131 @@ export default {
 }
 
 .popup-content {
-	background-color: white;
+	background:url("../../static/makedetail/ai-bg.jpg") center top/100% 100%; 
 	width: 100%;
-	max-height: 50vh; /* Adjusted max height */
+	max-height: 70vh; // Increased height
+	min-height: 40vh;
 	border-top-left-radius: 20rpx;
 	border-top-right-radius: 20rpx;
 	display: flex;
 	flex-direction: column;
 	animation: slideUp 0.3s ease-out;
+	padding: 0 30rpx; // Add horizontal padding to content
+	box-sizing: border-box;
+	border-radius: 28rpx 28rpx 0 0;
 }
 
 @keyframes slideUp {
 	from {
 		transform: translateY(100%);
 	}
+
 	to {
 		transform: translateY(0);
 	}
 }
 
-.popup-header {
-	display: flex;
-	justify-content: space-between;
-	align-items: center;
-	padding: 25rpx 30rpx;
-	border-bottom: 1rpx solid #eee;
-}
+.parent-tabs {
+	width: 100%;
+	border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
+	margin-bottom: 20rpx;
+	padding-top: 30rpx; // Space at the top
 
-.popup-action {
-	font-size: 30rpx;
-	cursor: pointer;
-}
+	.tabs-scroll {
+		white-space: nowrap;
+		width: 100%;
+	}
 
-.popup-cancel {
-	color: #666;
-}
+	.tab-item {
+		display: inline-block;
+		padding: 20rpx 30rpx;
+		font-size: 28rpx;
+		color: rgba(255, 255, 255, 0.7);
+		cursor: pointer;
+		position: relative;
 
-.popup-confirm {
-	color: #007aff; // Blue color for confirm
-}
+		&.active {
+			color: #FFFFFF;
+			font-weight: bold;
 
-.picker-container {
-	display: flex;
-	flex: 1;
-	height: calc(50vh - 81rpx); /* Max height minus header height */
-	overflow: hidden; /* Prevent overall container from scrolling */
+			&::after {
+				content: '';
+				position: absolute;
+				bottom: 0;
+				left: 50%;
+				transform: translateX(-50%);
+				width: 40rpx;
+				height: 6rpx;
+				background-color: #FFFFFF;
+				border-radius: 3rpx;
+			}
+		}
+	}
 }
 
-.column {
-	height: 100%;
+.child-tags-container {
+	flex: 1;
 	overflow-y: auto;
-	padding: 10rpx 0;
-}
+	padding-bottom: 20rpx;
+ 
 
-.parent-column {
-	flex: 1;
-	background-color: #f8f8f8;
-	border-right: 1rpx solid #eee;
-}
+	.tags-grid {
+		display: flex;
+		flex-wrap: wrap;
+		gap: 20rpx; // Spacing between tags
+	}
 
-.child-column {
-	flex: 1.5; // Child column can be wider
-	background-color: #fff;
+	.tag-item {
+		background-color: rgba(255, 255, 255, 0.1);
+		color: rgba(255, 255, 255, 0.8);
+		padding: 15rpx 30rpx;
+		font-size: 26rpx;
+		border-radius: 40rpx;
+		cursor: pointer;
+		border: 2rpx solid transparent;
+		transition: all 0.2s ease;
+		text-align: center;
+		min-width: 120rpx; // Ensure tags have some minimum width
+		box-sizing: border-box;
+		border: 2rpx solid transparent;
+		&.selected {
+			border-color: #ACF934;
+			color: #ACF934;
+		}
+	}
 }
 
-.column-item {
-	padding: 25rpx 30rpx;
-	font-size: 28rpx;
-	cursor: pointer;
+.no-children-text {
+	padding: 40rpx;
 	text-align: center;
-	border-bottom: 1rpx solid #f0f0f0; /* Light border for items */
+	color: rgba(255, 255, 255, 0.5);
+	font-size: 26rpx;
 }
 
-.parent-item.active {
-	background-color: #fff; 
-	color: #007aff; 
-	font-weight: bold;
-	border-right: 4rpx solid #007aff; /* Indicate active parent */
-	margin-right: -1rpx; /* Overlap border */
-}
+.popup-footer {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	padding: 30rpx 0;
+	border-top: 1rpx solid rgba(255, 255, 255, 0.1);
 
-.child-item.selected {
-	color: #007aff;
-	font-weight: bold;
-	background-color: #e6f2ff; /* Light blue background for selected child */
-}
+	.confirm-btn { 
+		background:url("../../static/makedetail/ai-btn-bg.png") center  /100% 100%; 
+	
+		color: #FFFFFF;
+		font-size: 32rpx;
+		font-weight: bold;
+		text-align: center;
+		width: 100%;
+		max-width: 600rpx;
+		padding: 25rpx 0;
+		border-radius: 50rpx;
+		cursor: pointer;
+		transition: opacity 0.2s ease;
 
-.no-children-text {
-	padding: 40rpx;
-	text-align: center;
-	color: #999;
-	font-size: 26rpx;
+		&:active {
+			opacity: 0.8;
+		}
+	}
 }
 </style>

+ 1 - 1
pages/makedetail/intelligentMusicProduction.scss

@@ -136,7 +136,7 @@ page {
         width: 112rpx;
         position: absolute;
         bottom: 0;
-        left: 20rpx;
+        left: 22rpx;
       }
   }
   // padding-top: 30rpx;

+ 193 - 45
pages/makedetail/intelligentMusicProduction.vue

@@ -16,14 +16,14 @@
 
 			<!-- <view class="navbar-reserveASeat"> </view>  -->
 			<!-- 聊天内容区 -->
-			<scroll-view class="chat-content" :scroll-into-view="toView" scroll-y
+			<scroll-view class="chat-content" :scroll-into-view="toView" scroll-y scroll-with-animation="true"
 				:style="{ height: stateType === 3 ? 'calc(100% - 180rpx)' : `calc(100% - ${370 + textareaHeight}rpx)` }"
 				@scroll="onChatScroll">
 				<template v-if="messages && messages.length > 0">
 					<!-- <scroll-view class="chat-content" scroll-y> -->
 					<view v-for="(msg, idx) in messages" :key="idx" :class="['chat-bubble', msg.role]">
 						<template v-if="msg.role === 'user'">
-							{{ msg.content }}
+							<uv-text size="32rpx" color="rgba(255, 255, 255, 0.7)" :text="msg.content"></uv-text>
 						</template>
 						<template v-else-if="msg.role === 'ai'">
 							<view class="ai-bubble-row">
@@ -39,6 +39,7 @@
 									<view>
 										可以根据自己的喜好点击下方的选项,快速开始创作🎼,
 									</view>
+								 
 									<div @click="onCateSent('纯音乐')" class="btn-box"> 纯音乐
 									</div>
 									<div @click="onCateSent('AI生成歌词')" class="btn-box" style="margin:0 20rpx"> AI生成歌词
@@ -51,20 +52,28 @@
 									<text>{{ msg.content }}</text>
 								</view>
 								<!-- 第三句话 -->
-								<view class="ai-bubble-content" v-else-if="msg.type == 2">
+								<view class="ai-bubble-content"
+									style="border-radius: 12rpx 36rpx 36rpx 36rpx;border: 1px solid rgba(255,255,255,0.1);"
+									v-else-if="msg.type == 2">
 									<!-- <text>{{ msg.content }}</text> -->
-									<textarea v-model="lyricData" class="lyric-editor" auto-height
-										:adjust-position="false" placeholder="修改歌词..." maxlength="-1" />
-									<div @click="onLyricSent()" class="btn-box"> 保存歌词
-									</div>
+									<view class="lyrics-input-title">
+										<view>歌词</view>
+										<view class="right-btn" :class="{'right-btn-active': isLyricConfirmActive}" @click="onLyricSent()" @touchstart="isLyricConfirmActive = true" @touchend="isLyricConfirmActive = false" @touchcancel="isLyricConfirmActive = false">确认</view>
+									</view>
+									<scroll-view scroll-y class="lyricsInputBox" style=""> 
+										<textarea  v-if="idx == (messages.length - 1 )" v-model="lyricData" class="lyric-editor" auto-height
+											:adjust-position="false" placeholder="修改歌词..." maxlength="-1" />
+											<uv-text v-else size="32rpx" color="rgba(255, 255, 255, 0.7)" :text="msg.content"></uv-text>
+									</scroll-view>
+
 								</view>
 								<view class="ai-bubble-content" v-else-if="msg.type == 4">
-									<view>请输入音乐的风格</view> 
+									<view>请输入音乐的风格</view>
 									<br>
 									<view>
 										也可以根据自己的喜好点击下方的选项,快速开始创作🎼,
 									</view>
-									<div @click="openTagPopup()" class="btn-box"> 
+									<div @click="openTagPopup()" class="btn-box">
 										选择标签
 									</div>
 								</view>
@@ -79,8 +88,8 @@
 									</template>
 </view> -->
 
-								<view v-else class="ai-bubble-content">
-									<text>OK!~小萌正在根据你的描述生成形象中</text><br>
+								<view v-else-if="msg.type == 20" class="ai-bubble-content">
+									<text>OK!~小萌开始生成音乐啦!</text><br>
 									<div v-if="msg.isStartGenerating && msg.startGeneratingId == 0" class="btn-box">
 										点击查看
 										({{ countdown }}) s</div>
@@ -94,7 +103,7 @@
 					</view>
 					<view id="bottom-anchor"></view>
 					<view v-if="error" class="chat-bubble ai error">{{ error }}</view>
-					<view style="height: 200rpx; width:100vw;"></view>
+					<view  :style="{ height: stateType !== 3 || musicGenre == '自定义歌词' ? '200rpx' : '80rpx' }"></view>
 					<image v-if="showToBottomBtn && keyboardHeight === 0" class="to-bottom-btn" @click="scrollToBottom"
 						src="../../static/makedetail/toBottomBtn.png"></image>
 				</template>
@@ -108,7 +117,7 @@
 			</scroll-view>
 			<!-- <view class="bom-reserveASeat"></view> -->
 
-			<view v-if="stateType !== 3" class="bom-box"
+			<view v-if="stateType !== 3 || musicGenre == '自定义歌词'" class="bom-box"
 				:style="{ bottom: 0 + 'px', height: `${190 + textareaHeight}rpx` }">
 				<view class="bom-box-bg">
 					<c-lottie ref="cLottieRef" :src='"/static/lottie/xiaomeng.json"' class="icon-img" height="108rpx"
@@ -140,9 +149,10 @@
 				<view class="footer-tip">内容由AI生成,禁用相关功能请联系管理员。</view>
 			</view>
 		</view>
-		 
+
 		<DialogBox ref="DialogBox"></DialogBox>
-		<multi-select-popup ref="tagPopup" :initial-options="tagOptions" @selection-confirmed="handleTagSelection" @popup-closed="handlePopupClosed"></multi-select-popup>
+		<multi-select-popup ref="tagPopup" :initial-options="tagOptions" @selection-confirmed="handleTagSelection"
+			@popup-closed="handlePopupClosed"></multi-select-popup>
 	</view>
 </template>
 <script>
@@ -182,11 +192,12 @@ export default {
 			isStartGenerating: false,
 			countdown: 0,
 			stateType: 1,
-			stateTypeArray: ['clear', 'cate', 'getLyrics', 'setLyrics', 'getTags', 'setTags', 'setContent','','','','','setContent'],
+			isLyricConfirmActive: false, // 用于歌词确认按钮的点击反馈
+			stateTypeArray: ['clear', 'cate', 'getLyrics', 'setLyrics', 'getTags', 'setTags', 'setContent', '', '', '', '', 'setContent'],
 			content: '',
 			musicGenre: '',
 			lyricData: '',
-			tagsData: '', 
+			tagsData: '',
 			tagOptions: [], // 用于存储多选弹窗的选项 
 			// type = clear   清除|cate 第一句话选择 |getLyrics 获取歌词(需要上传描述)|setLyrics 修改歌词(再去判断是否有* 有重复没有下一步 没有展示标签)|setTags(修改标签) (下发一个static 失败有提示 成功就正常生成)|getTags 获取标签
 		};
@@ -196,7 +207,12 @@ export default {
 		// /Work/streamAnswerMusic
 		// {type:'',content:''}
 		// type = clear  清除|cate 第一句话选择 |getLyrics 获取歌词(需要上传描述)|setLyrics 修改歌词(再去判断是否有* 有重复没有下一步 没有展示标签)|setTags(修改标签) (下发一个static 失败有提示 成功就正常生成)|getTags 获取标签
-
+		formatMsgContent(content) {
+			if (!content) return [];
+			// 将换行符替换为 <br/>
+			return content.replace(/\n/g, '<br/>');
+			;
+		},
 		scrollToBottom() {
 			this.toView = '';
 			this.$nextTick(() => {
@@ -209,10 +225,10 @@ export default {
 			try {
 				// 发送初始化消息
 				websocket.onOpen(() => {
-					setTimeout(()  => {
-						websocket.send(JSON.stringify({ type: 'getTags', content: '' }));
-					})
-				},1000);
+					// setTimeout(() => {
+					// 	websocket.send(JSON.stringify({ type: 'getTags', content: '' }));
+					// }, 1000)
+				});
 
 				await websocket.connect('wss://e.zhichao.art/Gapi/Work/streamAnswerMusic', {
 					uuid: getApp().globalData.uuid
@@ -236,7 +252,7 @@ export default {
 					// type=result,content=ID
 
 
-					
+
 					// 111111111111111111111111111111111
 					// if (type === 'success' && content === 'OKOKOK') {
 					// 	// 音乐生成开始,AI消息加载中提示
@@ -264,15 +280,18 @@ export default {
 							if (this.musicGenre == '纯音乐') {
 								// 跳过获取 修改 歌词
 								this.stateType = 11;
-								this.messages.push({ role: 'ai', type: 1, content: '输入描述生成音乐' }); 
-							} else {
-								this.stateType = 2;
+								this.messages.push({ role: 'ai', type: 1, content: '输入描述生成音乐' });
 							}
 
+
 							if (this.musicGenre == 'AI生成歌词') {
 								this.messages.push({ role: 'ai', type: 1, content: '请描述歌词' });
 								this.stateType = 2;
 							}
+							if (this.musicGenre == '自定义歌词') {
+								this.messages.push({ role: 'ai', type: 1, content: '请输入歌词' });
+								this.stateType = 3;
+							}
 
 							console.log(type, content);
 
@@ -312,7 +331,7 @@ export default {
 					if (type == 'lyrics' && content) {
 						console.log('获取到歌词', content);
 						// 替换加载中的消息
-						const lastMessageIndex = this.messages.length -1;
+						const lastMessageIndex = this.messages.length - 1;
 						// 替换加载中的消息
 						this.stopLoadingAnimation();
 						// Find and remove the loading message
@@ -322,23 +341,62 @@ export default {
 								break;
 							}
 						}
+ 					// 移除 isProcessing 的消息
+						for (let i = this.messages.length - 1; i >= 0; i--) {
+							if (this.messages[i].isProcessing) {
+								this.messages.splice(i, 1);
+								break;
+							}
+						}
 						this.messages.push({ role: 'ai', type: 2, content: content });
 						this.stateType = 3;
 						this.lyricData = content;
 						console.log(this.messages, 24);
 
 					}
-					if (type == 'tags' && content  ) { 
+					if (type == 'tags' && content) {
+						// 移除 isProcessing 的消息
+						for (let i = this.messages.length - 1; i >= 0; i--) {
+							if (this.messages[i].isProcessing) {
+								this.messages.splice(i, 1);
+								break;
+							}
+						}
 						// 此时歌词合法 下一步获取标签
-						this.stateType = 4; 
+						this.stateType = 4;
 						this.messages.push({ role: 'ai', type: 4, content: '请输入标签' });
 					}
-					if(type == 'getTags' && content && !this.tagsData ){ //确保只处理一次 
-						// 获取标签成功
-						this.tagsData = JSON.parse(content);
-						console.log('获取到标签', this.tagsData);
-						// 将获取到的标签数据转换为 MultiSelectPopup 需要的格式
-						this.tagOptions = this.formatTagOptions(this.tagsData); 
+					// if (type == 'getTags' && content && !this.tagsData) { //确保只处理一次 
+					// 	// 获取标签成功
+					// 	this.tagsData = JSON.parse(content);
+					// 	console.log('获取到标签', this.tagsData);
+					// 	// 将获取到的标签数据转换为 MultiSelectPopup 需要的格式
+					// 	this.tagOptions = this.formatTagOptions(this.tagsData);
+					// }
+					// {"type":"success","content":"OKOKOK"}	 
+					// {"type":"result","content":"208"}
+					if (type == 'success' && (content == 'OKOKOK' || content == 'OK')) {
+						// 音乐生成开始,AI消息加载中提示
+						this.messages.push({ role: 'ai', type: 20, content: '', isStartGenerating: true, startGeneratingId: 0 });
+						this.isStartGenerating = true;
+						this.countdownFun(3);
+						this.startLoadingAnimation();
+					}
+					if (type == 'result' && content) {
+						// 生成完成,AI消息中显示立即查看按钮
+						this.stopLoadingAnimation();
+						const aiMsg = this.messages.find(msg => msg.isStartGenerating);
+						if (aiMsg) {
+							aiMsg.isStartGenerating = false;
+							aiMsg.startGeneratingId = content;
+						}
+						this.isStartGenerating = false;
+					}
+					if (type == 'error' && content) {
+						uni.showToast({
+							title: content,
+							icon: 'none'
+						})
 					}
 					this.scrollToBottom();
 				});
@@ -389,7 +447,10 @@ export default {
 			console.log('发送消息:', content);
 
 			this.resetState();
-			this.messages.push({ role: 'user', content });
+			if (this.stateTypeArray[this.stateType] != 'setLyrics' || this.musicGenre == '自定义歌词') {
+
+				this.messages.push({ role: 'user', content });
+			}
 
 			try {
 				this.isLoading = true;
@@ -407,7 +468,7 @@ export default {
 				}
 				// 发送消息
 				const messageType = this.stateTypeArray[this.stateType];
-				
+
 				let messageToSend = { type: messageType, content: content };
 
 				// 特殊处理 stateType 11 (纯音乐描述)
@@ -422,6 +483,9 @@ export default {
 					this.messages.push({ role: 'ai', type: 1, content: this.loadingText + this.loadingDots, isGeneratingLyrics: true });
 					this.startLoadingAnimation();
 					this.scrollToBottom();
+				} else if (messageType === 'setLyrics') {
+					this.messages.push({ role: 'ai', type: 1, content: '正在处理中...', isProcessing: true });
+					this.scrollToBottom();
 				}
 
 			} catch (error) {
@@ -436,7 +500,7 @@ export default {
 				const userTagInput = this.question.trim();
 				this.messages.push({ role: 'user', content: userTagInput });
 				websocket.send(JSON.stringify({ type: 'setTags', content: userTagInput }));
-				this.messages.push({ role: 'ai', type: 1, content: `好的,已选择标签:${userTagInput},小萌开始生成音乐啦!` });
+				// this.messages.push({ role: 'ai', type: 1, content: `好的,已选择标签:${userTagInput}` });
 				this.question = '';
 				this.scrollToBottom();
 				return; // 阻止后续的 startStreamAnswer 调用
@@ -459,7 +523,9 @@ export default {
 					title: '歌词有"*",请修改后再保存',
 					icon: 'none'
 				});
+				return;
 			}
+			this.messages[this.messages.length - 1].content = this.lyricData;
 			this.startStreamAnswer(this.lyricData);
 		},
 		// 打开标签选择弹窗
@@ -493,16 +559,16 @@ export default {
 			console.log('选中的标签:', selectedValues);
 			// 处理选中的标签,例如发送到后端
 			// 示例:将选中的标签数组转换为字符串发送
-			const tagsString = selectedValues.join(','); 
+			const tagsString = selectedValues.join(',');
 			websocket.send(JSON.stringify({ type: 'setTags', content: tagsString }));
 			// 可以在这里添加AI回复,告知用户标签已选择,正在生成音乐等
-			this.messages.push({ role: 'ai', type: 1, content: `好的,已选择标签:${tagsString},小萌开始生成音乐啦!` });
+			// this.messages.push({ role: 'ai', type: 1, content: `好的,已选择标签:${tagsString},小萌开始生成音乐啦!` });
 			this.scrollToBottom();
 		},
 		handlePopupClosed() {
 			// 如果用户关闭了弹窗但没有选择任何标签,可以提供一个默认行为或提示
 			// 例如,如果没有选择标签,可以发送一个空数组或特定指令
-		 
+
 		},
 		startLoadingAnimation() {
 			let dotCount = 1;
@@ -582,12 +648,13 @@ export default {
 			});
 		},
 		onChatScroll(e) {
-			const threshold = 600; // 离底部1.0417rem以内不显示按钮
+			const threshold = 800; // 离底部1.0417rem以内不显示按钮
 			const {
 				scrollHeight,
 				scrollTop
 			} = e.detail;
-			const clientHeight = e.detail.clientHeight || e.detail.height || 0;
+			const clientHeight = e.detail.clientHeight || e.detail.height || 0; 
+			
 			if (scrollHeight - scrollTop - clientHeight > threshold) {
 				this.showToBottomBtn = true;
 			} else {
@@ -639,16 +706,25 @@ export default {
 							this.messages.push({ role: 'user', content: res.data.cate });
 
 						}
+						if (res.data.cate == '纯音乐') {
+							// 跳过获取 修改 歌词
+							this.messages.push({ role: 'ai', type: 1, content: '输入描述生成音乐' });
+							this.stateType = 11;
+						}
 						if (res.data.cate == 'AI生成歌词') {
 							this.messages.push({ role: 'ai', type: 1, content: '请描述歌词' });
 							this.stateType = 2;
 						}
+						if (res.data.cate == '自定义歌词') {
+							this.messages.push({ role: 'ai', type: 1, content: '请输入歌词' });
+							this.stateType = 3;
+						}
 						if (res.data.content) {
 							this.messages.push({ role: 'user', type: 1, content: res.data.content });
 						}
 						// AI生成歌词逻辑
 						if (res.data.lyrics) {
-							this.messages.push({ role: 'ai', type: 2, content: res.data.lyrics });
+							this.messages.push({ role: 'ai', type: 2, content:res.data.lyrics});
 							this.stateType = 3;
 							this.lyricData = res.data.lyrics;
 						}
@@ -710,6 +786,17 @@ export default {
 				})
 				.then(() => {
 					websocket.send(JSON.stringify({ type: 'clear', content: '' }));
+					this.toView = ''
+					this.showToBottomBtn = false
+					this.textareaHeight = 0
+					this.isConnected = false
+					this.isStartGenerating = false
+					this.countdown = 0
+					this.stateType = 1
+					this.stateTypeArray = ['clear', 'cate', 'getLyrics', 'setLyrics', 'getTags', 'setTags', 'setContent', '', '', '', '', 'setContent']
+					this.content = ''
+					this.musicGenre = ''
+					this.lyricData = ''
 					this.retrieveHistoricalRecords()
 				});
 
@@ -728,9 +815,37 @@ export default {
 					this.goPage('/pages/makedetail/makeMusicDetail')
 				});
 
-		}
+		},
+		getTags() {
+				let that = this
+				uni.request({
+					url: this.$apiHost + '/Work/getTags',
+					method: 'GET',
+					header: {
+						'content-type': 'application/json',
+						'sign': getApp().globalData.headerSign
+					},
+					data: {
+						uuid: getApp().globalData.uuid
+					},
+					success: (res) => {
+						console.log("获取标签:", res.data);
+						if (res.data && res.data.tags) {
+							this.tagOptions = this.formatTagOptions(res.data.tags);
+						}
+					},
+					fail: (err) => {
+						console.log('获取标签失败:', err);
+						uni.showToast({
+							title: '获取标签失败',
+							icon: 'none'
+						});
+					}
+				})
+			},
 	},
 	async created() {
+		this.getTags();
 		await this.initWebSocket();
 		this.retrieveHistoricalRecords();
 		this.timer = setInterval(() => {
@@ -767,6 +882,39 @@ export default {
 <style lang="scss">
 @import './intelligentLifeChart.scss';
 
+.lyricsInputBox {
+	width: 100%;
+	height: calc(100vh - 455rpx);
+	box-sizing: border-box;
+	padding: 20rpx;
+}
+ 
+.lyrics-input-title {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	height: 35px;
+	background: rgba(255, 255, 255, 0.1);
+	border-radius: 12rpx 36rpx 0 0;
+	padding-left: 20rpx;
+	padding-right: 24rpx;
+	font-size: 32rpx;
+
+	.right-btn {
+		font-size: 26rpx;
+		background: rgba(255, 255, 255, 0.15);
+		border-radius: 22rpx;
+		border: 2rpx solid rgba(255, 255, 255, 0.15);
+		padding: 4rpx 32rpx;
+		transition: background-color 0.2s ease, transform 0.1s ease; // 添加过渡效果
+	}
+
+	.right-btn-active {
+		background: rgba(255, 255, 255, 0.3); // 点击时的深色背景
+		transform: scale(0.98); // 点击时的缩小效果
+	}
+}
+
 .lyric-editor {
 	width: 100%;
 	background-color: transparent;

+ 6 - 3
pages/makedetail/makeMusicDetail.scss

@@ -380,15 +380,15 @@
           }
         }
       }
-      .tabs {
-        display: flex;
-        justify-content: flex-start;
+      .tabs { 
         padding: 20rpx 20rpx;
         box-sizing: border-box;
         background: #ffffff;
         padding-bottom: 30rpx;
         line-height: 1;
         padding-left: 0;
+        white-space: nowrap;
+        width: 100%;
         padding-bottom: 10rpx;
         text {
           padding: 12rpx 38rpx;
@@ -400,6 +400,9 @@
           position: relative;
           left: 0;
           top: 0;
+          display: inline-block;
+          margin-right: 10rpx;
+
           transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
           // box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
 

+ 99 - 29
pages/makedetail/makeMusicDetail.vue

@@ -54,7 +54,7 @@
 			<view class="input-section" style="margin-top: 0;">
 				<text class="label">歌曲名称</text>
 				<input type="text" placeholder="请输入名称..." class="input-field" maxlength="30" v-model="songName"
-					:disabled="doYouWantToEdit()" />
+					:disabled="doYouWantToEdit()" style="padding-right: 90rpx;" />
 				<text class="count lyricCount">{{ songName.length }}/30</text>
 			</view>
 
@@ -72,7 +72,7 @@
 			</view>
 
 			<!-- 音乐风格选择 -->
-			<view class="style-section" style=" animation: fadeIn-data-v-032d5e1e 0.5s ease;">
+			<!-- <view class="style-section" style=" animation: fadeIn-data-v-032d5e1e 0.5s ease;">
 				<text class="label">音乐风格</text>
 				<view class="tabs">
 					<text :class="{ 'active': selectedTab === 'emotion' }" @click="selectTab('emotion')">
@@ -103,7 +103,29 @@
 						{{ tag }}
 					</text>
 				</view>
+			</view> -->
+			<view class="style-section" style=" animation: fadeIn-data-v-032d5e1e 0.5s ease;">
+				<text class="label">音乐风格</text> 
+					<scroll-view scroll-x class="tabs" :show-scrollbar="false"
+					  scroll-with-animation :scroll-into-view="'tab-' +( activeParentIndex -1)">
+			 
+					<block v-for="(tab ,index) in tagOptions" :key="tab.name">
+						<text :id="'tab-' + index"  :class="{ 'active': selectedTab === tab.name }" @click="selectTab(tab.name,index)">
+							{{ tab.name }}
+							<view class="indicator-triangle">
+							</view>
+						</text>
+					</block>
+				</scroll-view>
+				<view class="tags">
+					<text v-for="(tag, index) in currentTags" :key="index"
+						:class="['tag', { active: selectedTags[selectedTab] && selectedTags[selectedTab].includes(tag.name) }]"
+						@click="doYouWantToEdit() ? state() : toggleTag(tag)">
+						{{ tag.name }}
+					</text>
+				</view>
 			</view>
+			 
 		</view>
 
 		<!-- 底部按钮 -->
@@ -135,25 +157,19 @@ export default {
 		return {
 			songName: '',
 			lyrics: '',
-			selectedTags: {
-				emotion: [],
-				genre: [],
-				era: [],
-				instrument: []
-			},
+			selectedTags: {},
 			textareaHeight: 120,
 			minHeight: 120,
-			selectedTab: 'emotion',
-			tagOptions: {
-				emotion: ['欢快', '悲伤', '积极', '浪漫', '忧郁', '华丽', '闪耀', '神秘', '惊怒', '紧张', '恐怖', '平静'],
-				genre: ['流行', '摇滚', '民谣', '电子', 'R&B', '嘻哈', '古典', '爵士'],
-				era: ['80年代', '90年代', '00年代', '10年代', '20年代'],
-				instrument: ['钢琴', '吉他', '贝斯', '鼓', '小提琴', '萨克斯', '电子合成器']
-			},
+			selectedTab: '', // 初始为空,将在 getTags 成功后设置第一个tab
+			tagOptions: [],
+			allTagsMap: {}, // 用于快速查找标签所属的分类
+			selectedTabObject: null, // 用于存储当前选中的tab对象,包含children
 			inQueue: false,//是否创作中
 			queuing: false,//是否排队中
 			queueMessage: '',
 			myinfo: {},
+			activeParentIndex: 0,
+
 			step: {
 				name: "makeMusicGuide",
 				guideList: [
@@ -193,6 +209,7 @@ export default {
 	onLoad(e) {
 		// this.checkQueueStatus();
 		this.getMyInfo();
+		this.getTags(); // Call getTags on load
 		if (e.id) {
 			this.getQueueDetail(e.id)
 		} 
@@ -200,7 +217,8 @@ export default {
 	},
 	computed: {
 		currentTags() {
-			return this.selectedTab ? this.tagOptions[this.selectedTab] : [];
+			// 确保 selectedTabObject 存在且有 children 属性
+			return this.selectedTabObject && this.selectedTabObject.children ? this.selectedTabObject.children : [];
 		},
 		...mapState('switchingModule', ['isRecharge', 'isGuiding'])
 	},
@@ -344,10 +362,16 @@ export default {
 
 			this.textareaHeight = newHeight;
 		},
-		selectTab(tab) {
-			if (this.selectedTab !== tab) {
-				this.selectedTab = tab;
-				// 不再清空已选择的标签
+		selectTab(tabName ,index) {
+			this.activeParentIndex = index;
+			if (this.selectedTab !== tabName) {
+				this.selectedTab = tabName;
+				// 找到对应的tab对象并存储
+				this.selectedTabObject = this.tagOptions.find(tab => tab.name === tabName);
+				// 确保当前tab的selectedTags数组已初始化
+				if (!this.selectedTags[tabName]) {
+					this.$set(this.selectedTags, tabName, []);
+				}
 			}
 		},
 		state() {
@@ -365,8 +389,11 @@ export default {
 		},
 
 		toggleTag(tag) {
-			if (this.selectedTags[this.selectedTab].includes(tag)) {
-				this.selectedTags[this.selectedTab] = this.selectedTags[this.selectedTab].filter(t => t !== tag);
+			const tagName = tag.name;
+			const currentTabSelectedTags = this.selectedTags[this.selectedTab];
+
+			if (currentTabSelectedTags.includes(tagName)) {
+				this.selectedTags[this.selectedTab] = currentTabSelectedTags.filter(t => t !== tagName);
 			} else {
 				// 计算所有已选标签的总数
 				const totalSelectedTags = Object.values(this.selectedTags).reduce((total, tags) => total + tags.length, 0);
@@ -377,7 +404,7 @@ export default {
 					});
 					return;
 				}
-				this.selectedTags[this.selectedTab].push(tag);
+				currentTabSelectedTags.push(tagName);
 			}
 		},
 		getQueueDetail(id) {
@@ -403,13 +430,16 @@ export default {
 						that.songName = song_name
 						that.lyrics = lyrics
 
+						// 根据新的tagOptions结构处理style
 						const styles = style.split(',');
-						styles.forEach(tag => {
-							for (const [key, tags] of Object.entries(that.tagOptions)) {
-								if (tags.includes(tag)) {
-									that.selectedTags[key].push(tag);
-									break;
+						that.selectedTags = {}; // 清空之前的选择
+						styles.forEach(tagName => {
+							const categoryName = that.allTagsMap[tagName];
+							if (categoryName) {
+								if (!that.selectedTags[categoryName]) {
+									that.$set(that.selectedTags, categoryName, []);
 								}
+								that.selectedTags[categoryName].push(tagName);
 							}
 						});
 
@@ -432,6 +462,46 @@ export default {
 				}
 			})
 		},
+		getTags() {
+				let that = this
+				uni.request({
+					url: this.$apiHost + '/Work/getTags',
+					method: 'GET',
+					header: {
+						'content-type': 'application/json',
+						'sign': getApp().globalData.headerSign
+					},
+					data: {
+						uuid: getApp().globalData.uuid
+					},
+					success: (res) => {
+						console.log("获取标签:", res.data);
+						if (res.data && res.data.tags) {
+							that.tagOptions = res.data.tags;
+
+							// 初始化 allTagsMap 和 selectedTags
+							that.tagOptions.forEach(category => {
+								that.$set(that.selectedTags, category.name, []);
+								category.children.forEach(tag => {
+									that.allTagsMap[tag.name] = category.name;
+								});
+							});
+
+							// 默认选中第一个tab
+							if (that.tagOptions.length > 0) {
+								that.selectTab(that.tagOptions[0].name);
+							}
+						}
+					},
+					fail: (err) => {
+						console.log('获取标签失败:', err);
+						uni.showToast({
+							title: '获取标签失败',
+							icon: 'none'
+						});
+					}
+				})
+			},
 		goPage(page) {
 			uni.navigateTo({
 				url: page,
@@ -443,4 +513,4 @@ export default {
 
 <style lang="scss">
 @import './makeMusicDetail.scss';
-</style>
+</style>

TEMPAT SAMPAH
static/makedetail/ai-bg.jpg


TEMPAT SAMPAH
static/makedetail/ai-btn-bg.png