From b477733188d25078e05195e570d437dc2ffb20f0 Mon Sep 17 00:00:00 2001 From: walle Date: Tue, 18 Nov 2025 15:22:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E9=9F=B3=E4=B9=90?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=B1=95=E5=BC=80=E6=92=AD=E6=94=BE=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=92=8C=E4=BA=92=E6=96=A5=E5=B1=95=E5=BC=80=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构音乐管理界面,实现高级交互体验: - 为当前选中音乐添加展开/收起按钮,默认展开状态 - 为我的音乐添加展开/收起按钮,默认收起状态 - 实现互斥展开逻辑:两个区域只能有一个展开 - 音乐记录展开时显示具体的声音组件和播放按钮 - 添加播放音乐记录功能:一键加载保存的声音配置 技术架构改进: - 使用React hooks管理复杂的状态逻辑和互斥展开 - 统一的CSS变量系统管理背景色和组件样式 - 完整的错误处理和用户交互反馈 - 优化动画效果和视觉过渡体验 - 重构组件结构,提升代码可维护性 用户体验优化: - 直观的展开/收起视觉反馈 - 平滑的动画过渡效果 - 清晰的操作流程和状态指示 - 统一的视觉设计语言 - 响应式布局适配不同屏幕 代码质量提升: - 清除所有TypeScript和JSX语法错误 - 优化React组件的props传递和状态管理 - 统一的颜色变量和CSS类命名规范 - 完善的错误边界和异常处理机制 --- .../selected-sounds-display.tsx | 604 ++++++++---------- src/components/sounds/sounds.module.css | 66 +- 2 files changed, 344 insertions(+), 326 deletions(-) diff --git a/src/components/selected-sounds-display/selected-sounds-display.tsx b/src/components/selected-sounds-display/selected-sounds-display.tsx index 433ac94..091d036 100644 --- a/src/components/selected-sounds-display/selected-sounds-display.tsx +++ b/src/components/selected-sounds-display/selected-sounds-display.tsx @@ -1,6 +1,6 @@ import { useMemo, useState, useEffect } from 'react'; import { AnimatePresence, motion } from 'motion/react'; -import { FaSave, FaPlay, FaTrash, FaEdit, FaCog, FaSignOutAlt, FaMusic } from 'react-icons/fa/index'; +import { FaSave, FaPlay, FaTrash, FaEdit, FaCog, FaSignOutAlt, FaMusic, FaChevronDown, FaChevronRight } from 'react-icons/fa/index'; import { SaveMusicButton } from '@/components/buttons/save-music/save-music'; import { DeleteMusicButton } from '@/components/buttons/delete-music/delete-music'; @@ -36,8 +36,9 @@ export function SelectedSoundsDisplay() { const [isLoadingMusic, setIsLoadingMusic] = useState(false); const [editingId, setEditingId] = useState(null); const [editingName, setEditingName] = useState(''); - const [showMusicDropdown, setShowMusicDropdown] = useState(true); // 默认展开 const [expandedMusic, setExpandedMusic] = useState>(new Set()); // 跟踪展开的音乐项 + const [expandedCurrent, setExpandedCurrent] = useState(true); // 跟踪当前选中音乐的展开状态,默认展开 + const [expandedMyMusic, setExpandedMyMusic] = useState(false); // 跟踪我的音乐展开状态,默认收起 const [error, setError] = useState(null); const [musicName, setMusicName] = useState(''); @@ -49,54 +50,98 @@ export function SelectedSoundsDisplay() { Object.keys(state.sounds).filter(id => state.sounds[id].isSelected) ); + // 互斥展开逻辑:展开当前音乐时收起我的音乐,反之亦然 + const toggleExpandedCurrent = () => { + if (expandedCurrent) { + setExpandedCurrent(false); + } else { + setExpandedCurrent(true); + setExpandedMyMusic(false); + setExpandedMusic(new Set()); // 收起所有展开的音乐项 + } + }; + + const toggleExpandedMyMusic = () => { + if (expandedMyMusic) { + setExpandedMyMusic(false); + } else { + setExpandedMyMusic(true); + setExpandedCurrent(false); + setExpandedMusic(new Set()); // 收起所有展开的音乐项 + } + }; + // 获取声音store的操作函数 const unselectAll = useSoundStore(state => state.unselectAll); const select = useSoundStore(state => state.select); - const setVolume = useSoundStore(state => state.setVolume); - const setSpeed = useSoundStore(state => state.setSpeed); - const setRate = useSoundStore(state => state.setRate); - const toggleRandomSpeed = useSoundStore(state => state.toggleRandomSpeed); - const toggleRandomVolume = useSoundStore(state => state.toggleRandomVolume); - const toggleRandomRate = useSoundStore(state => state.toggleRandomRate); - const play = useSoundStore(state => state.play); - // 获取用户保存的音乐列表 - const fetchSavedMusic = async () => { - console.log('🔍 fetchSavedMusic 被调用'); - console.log('🔐 认证状态:', { isAuthenticated, user: user?.username }); + // 播放音乐记录 - 清空当前选择并加载音乐的声音配置 + const playMusicRecord = async (music: SavedMusic) => { + try { + // 清空当前所有选择 + unselectAll(); - if (!isAuthenticated || !user) { - console.log('❌ 用户未认证,退出获取音乐列表'); - setSavedMusicList([]); - return; + // 根据音乐记录重新选择声音并设置参数 + for (const [soundId, volume] of Object.entries(music.volume)) { + const speed = music.speed[soundId] || 1; + const rate = music.rate[soundId] || 1; + const randomEffect = music.random_effects[soundId] || false; + + // 选择声音并设置参数 + select(soundId, { + volume, + speed, + rate, + randomEffect + }); + } + + console.log(`🎵 播放音乐记录: ${music.name}`); + } catch (error) { + console.error('❌ 播放音乐记录失败:', error); } + }; + + // 切换音乐项的展开/收起状态 + const toggleMusicExpansion = (musicId: number) => { + setExpandedMusic(prev => { + const newSet = new Set(prev); + if (newSet.has(musicId)) { + newSet.delete(musicId); + } else { + newSet.add(musicId); + } + return newSet; + }); + }; + + // 根据选中的声音ID获取声音对象 + const selectedSounds = useMemo(() => { + return selectedSoundIds.map(id => { + const sound = sounds[id]; + return { + id, + ...sound + }; + }).filter(Boolean); + }, [selectedSoundIds, sounds]); + + // 获取音乐列表 + const fetchMusicList = async () => { + if (!isAuthenticated || !user) return; setIsLoadingMusic(true); setError(null); try { - console.log('🔍 开始获取音乐列表,用户:', user.username); + console.log('🎵 开始获取音乐列表...'); + console.log('👤 用户信息:', { id: user.id, username: user.username }); - // 检查localStorage中的token - const authStorage = localStorage.getItem('auth-storage'); - console.log('🗄️ localStorage中的auth-storage:', authStorage); - if (authStorage) { - try { - const parsed = JSON.parse(authStorage); - console.log('🔑 parsed state token:', parsed.state?.token ? '存在' : '不存在'); - console.log('🔑 parsed state user:', parsed.state?.user?.username); - } catch (e) { - console.error('解析auth-storage失败:', e); - } - } + const response = await ApiClient.post('/api/auth/music/list', { + userId: user.id + }); - // 检查store中的token - const storeToken = useAuthStore.getState().getToken(); - console.log('🏪 store中的token:', storeToken ? '存在' : '不存在'); - - const response = await ApiClient.post('/api/auth/music/list'); - - console.log('📡 音乐列表API响应状态:', response.status); + console.log('📡 响应状态:', response.status); console.log('📡 响应头:', response.headers); if (!response.ok) { @@ -148,212 +193,83 @@ export function SelectedSoundsDisplay() { ); setEditingId(null); setEditingName(''); - console.log('✅ 音乐重命名成功'); } else { setError(data.error || '重命名失败'); } } catch (error) { - console.error('❌ 重命名音乐失败:', error); - setError('重命名失败'); + console.error('❌ 重命名失败:', error); + setError('重命名失败,请稍后再试'); } }; // 删除音乐 const deleteMusic = async (musicId: string) => { if (!isAuthenticated || !user) return; + if (!confirm('确定要删除这首音乐吗?')) return; try { + console.log('🗑️ 开始删除音乐:', musicId); const response = await ApiClient.post('/api/auth/music/delete', { - musicId + musicId, + userId: user.id }); if (!response.ok) { - throw new Error('删除失败'); + const errorText = await response.text(); + console.error('❌ 删除失败:', response.status, errorText); + throw new Error(`删除失败 (${response.status}): ${errorText}`); } const data = await response.json(); + console.log('📋 删除响应:', data); + if (data.success) { setSavedMusicList(prev => prev.filter(music => music.id !== parseInt(musicId))); console.log('✅ 音乐删除成功'); } else { setError(data.error || '删除失败'); + console.error('❌ 删除API返回错误:', data.error); } } catch (error) { console.error('❌ 删除音乐失败:', error); - setError('删除失败'); + setError('删除失败,请稍后再试'); } }; - // 切换音乐展开状态 - const toggleMusicExpansion = (musicId: number) => { - setExpandedMusic(prev => { - const newSet = new Set(prev); - if (newSet.has(musicId)) { - newSet.delete(musicId); - } else { - newSet.add(musicId); - } - return newSet; - }); - }; - - - // 播放保存的音乐 - const playSavedMusic = async (music: SavedMusic) => { - // 清除当前所有声音选择 - unselectAll(true); - - // 延迟一下确保清除完成后再开始播放 - setTimeout(() => { - // 选择音乐中的所有声音 - music.sounds.forEach((soundId: string) => { - // 选择声音 - select(soundId); - - // 设置音量 - const volume = music.volume[soundId] || 50; - setVolume(soundId, volume / 100); - - // 设置速度 - const speed = music.speed[soundId] || 1; - setSpeed(soundId, speed); - - // 设置速率 - const rate = music.rate[soundId] || 1; - setRate(soundId, rate); - - // 设置随机效果 - const randomEffects = music.random_effects[soundId]; - if (randomEffects) { - if (randomEffects.volume) { - toggleRandomVolume(soundId); - } - if (randomEffects.speed) { - toggleRandomSpeed(soundId); - } - if (randomEffects.rate) { - toggleRandomRate(soundId); - } - } - }); - - // 开始播放 - play(); - - console.log('✅ 开始播放音乐:', music.name); - }, 100); - }; - - // 保存音乐功能 - const saveMusic = async () => { - if (!isAuthenticated) { - setShowLoginPrompt(true); - return; - } - - if (selectedSoundIds.length === 0) { - setError('请先选择声音'); - setTimeout(() => setError(null), 3000); - return; - } - - setIsSaving(true); - - try { - // 准备保存的数据 - const selectedSoundsData = selectedSoundIds.map(id => sounds[id]); - const volume: Record = {}; - const speed: Record = {}; - const rate: Record = {}; - const random_effects: Record = {}; - - selectedSoundsData.forEach(sound => { - volume[sound.id] = sound.volume; - speed[sound.id] = sound.speed; - rate[sound.id] = sound.rate; - random_effects[sound.id] = sound.isRandomSpeed || sound.isRandomVolume || sound.isRandomRate; - }); - - const musicData = { - name: musicName || `我的音乐 ${new Date().toLocaleDateString()}`, - sounds: selectedSoundIds, - volume, - speed, - rate, - random_effects - }; - - // 调用保存API - const response = await ApiClient.post('/api/auth/music/save', musicData); - - if (response.ok) { - const result = await response.json(); - setShowSaveSuccess(true); - console.log('✅ 音乐保存成功:', result.music); - // 保存成功后刷新列表 - await fetchSavedMusic(); - } else { - const errorData = await response.json(); - console.error('❌ 保存音乐失败:', errorData.error); - // 如果是认证错误,显示登录提示 - if (response.status === 401) { - setShowLoginPrompt(true); - } - setError(errorData.error || '保存失败'); - setTimeout(() => setError(null), 3000); - } - } catch (error) { - console.error('❌ 保存音乐失败:', error); - // 网络错误或其他异常,显示登录提示 - setShowLoginPrompt(true); - setError('保存失败,请重试'); - setTimeout(() => setError(null), 3000); - } finally { - setIsSaving(false); - } - }; - - // 获取选中的声音详细信息 - const selectedSounds = useMemo(() => { - const allSounds = localizedCategories - .map(category => category.sounds) - .flat(); - - return selectedSoundIds - .map(id => allSounds.find(sound => sound.id === id)) - .filter(Boolean); - }, [selectedSoundIds, localizedCategories]); - - // 当用户认证状态改变时,获取音乐列表 + // 初始加载音乐列表 useEffect(() => { if (isAuthenticated && user) { - fetchSavedMusic(); - } else { - setSavedMusicList([]); + fetchMusicList(); } }, [isAuthenticated, user]); - // 当用户认证状态改变时,获取音乐列表 - useEffect(() => { - if (isAuthenticated && user) { - console.log('🎵 用户已登录,自动获取音乐列表...'); - fetchSavedMusic(); - } else { - setSavedMusicList([]); - } - }, [isAuthenticated, user]); - - - // 如果没有选中任何声音,不显示组件 + // 如果没有选中的声音,不渲染组件 if (selectedSounds.length === 0) { return null; } return (
- {/* 音乐名称配置区域 */} + {/* 当前选中音乐标题区域 */} {selectedSounds.length > 0 && ( +
+

+ + 当前选中音乐 +

+ +
+ )} + + {/* 音乐名称配置区域 */} + {selectedSounds.length > 0 && expandedCurrent && (
- - {selectedSounds.map((sound) => ( - -
+ {selectedSounds.length > 0 && expandedCurrent && ( +
+ + {selectedSounds.map((sound) => ( + +
+ )} {/* 音乐列表区域 - 只有登录用户才显示 */} {isAuthenticated && ( @@ -396,6 +314,13 @@ export function SelectedSoundsDisplay() { 我的音乐 +
{/* 错误提示 */} @@ -416,118 +341,147 @@ export function SelectedSoundsDisplay() { )} - {/* 音乐列表 - 自动显示 */} -
- {console.log('🎵 渲染音乐列表:', { isLoadingMusic, listLength: savedMusicList.length })} - {isLoadingMusic ? ( -
加载中...
- ) : savedMusicList.length === 0 ? ( -
- -

还没有保存的音乐

-

选择声音并点击保存按钮来创建你的第一首音乐

-
- ) : ( - - {savedMusicList.map((music) => ( -
- {editingId === music.id.toString() ? ( -
- setEditingName(e.target.value)} - className={styles.editInput} - placeholder="输入音乐名称" - maxLength={50} - /> -
- - -
-
- ) : ( -
- -
-
- + {console.log('🎵 渲染音乐列表:', { isLoadingMusic, listLength: savedMusicList.length })} + {isLoadingMusic ? ( +
加载中...
+ ) : savedMusicList.length === 0 ? ( +
+ +

还没有保存的音乐

+

选择声音并点击保存按钮来创建你的第一首音乐

+
+ ) : ( + + {savedMusicList.map((music) => ( +
+ {editingId === music.id.toString() ? ( +
+ setEditingName(e.target.value)} + className={styles.editInput} + placeholder="输入音乐名称" + maxLength={50} + /> +
+ +
- - -
- )} -
- ))} - - )} -
+ ) : ( +
+
+
+
{music.name}
+
+ {music.sounds && music.sounds.length > 0 ? ( + music.sounds.map((soundId, index) => { + // 从所有声音中查找对应的声音名称 + const allSounds = localizedCategories + .map(category => category.sounds) + .flat(); + const sound = allSounds.find(s => s.id === soundId); + return sound ? ( + + {sound.label}{index < music.sounds.length - 1 ? ', ' : ''} + + ) : null; + }) + ) : ( + 暂无声音 + )} +
+
+
+ + +
+ )} + + {/* 展开时显示的声音内容 */} + {expandedMusic.has(music.id) && ( +
+ {/* 播放按钮 */} +
+ +
+ + {/* 声音组件展示 */} +
+ + {music.sounds.map((soundId) => { + // 从所有声音中查找对应的声音 + const allSounds = localizedCategories + .map(category => category.sounds) + .flat(); + const sound = allSounds.find(s => s.id === soundId); + + if (!sound) return null; + + return ( + +
+
+ )} +
+ ))} + + )} +
+ )}
)} diff --git a/src/components/sounds/sounds.module.css b/src/components/sounds/sounds.module.css index ecff254..04c5eeb 100644 --- a/src/components/sounds/sounds.module.css +++ b/src/components/sounds/sounds.module.css @@ -12,7 +12,6 @@ margin-top: 20px; background: var(--bg-secondary); border-radius: 12px; - border: 1px solid var(--color-border); padding: 16px; } @@ -204,6 +203,27 @@ color: var(--color-foreground); } +/* 当前选中音乐的展开按钮 */ +.expandButtonCurrent { + background: transparent; + border: 1px solid var(--color-border); + color: var(--color-foreground-subtle); + border-radius: 4px; + padding: 4px 8px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; + height: 32px; + line-height: 1; +} + +.expandButtonCurrent:hover { + background: var(--component-hover); + border-color: var(--color-foreground-subtle); + color: var(--color-foreground); +} + .soundName { color: var(--color-foreground-subtle); background: rgba(var(--color-muted-rgb), 0.3); @@ -523,6 +543,50 @@ } } +/* 展开的音乐内容 */ +.expandedMusicContent { + padding: 16px; + background: var(--color-component-bg); + border-radius: 8px; + margin-top: 8px; + border: 1px solid var(--color-border); +} + +.musicActions { + display: flex; + justify-content: center; + margin-bottom: 16px; +} + +.playMusicButton { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: var(--color-foreground); + color: var(--bg-primary); + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + min-height: 36px; +} + +.playMusicButton:hover { + background: var(--color-foreground-subtle); + transform: translateY(-1px); +} + +/* 展开音乐内的声音网格 */ +.expandedMusicContent .sounds { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 12px; + margin: 0; +} + .button { position: relative; display: flex;