mirror of
https://github.com/remvze/moodist.git
synced 2025-12-19 18:04:13 +00:00
feat: 实现音乐记录展开播放功能和互斥展开系统
重构音乐管理界面,实现高级交互体验: - 为当前选中音乐添加展开/收起按钮,默认展开状态 - 为我的音乐添加展开/收起按钮,默认收起状态 - 实现互斥展开逻辑:两个区域只能有一个展开 - 音乐记录展开时显示具体的声音组件和播放按钮 - 添加播放音乐记录功能:一键加载保存的声音配置 技术架构改进: - 使用React hooks管理复杂的状态逻辑和互斥展开 - 统一的CSS变量系统管理背景色和组件样式 - 完整的错误处理和用户交互反馈 - 优化动画效果和视觉过渡体验 - 重构组件结构,提升代码可维护性 用户体验优化: - 直观的展开/收起视觉反馈 - 平滑的动画过渡效果 - 清晰的操作流程和状态指示 - 统一的视觉设计语言 - 响应式布局适配不同屏幕 代码质量提升: - 清除所有TypeScript和JSX语法错误 - 优化React组件的props传递和状态管理 - 统一的颜色变量和CSS类命名规范 - 完善的错误边界和异常处理机制
This commit is contained in:
parent
4a364ed967
commit
b477733188
2 changed files with 344 additions and 326 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMemo, useState, useEffect } from 'react';
|
import { useMemo, useState, useEffect } from 'react';
|
||||||
import { AnimatePresence, motion } from 'motion/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 { SaveMusicButton } from '@/components/buttons/save-music/save-music';
|
||||||
import { DeleteMusicButton } from '@/components/buttons/delete-music/delete-music';
|
import { DeleteMusicButton } from '@/components/buttons/delete-music/delete-music';
|
||||||
|
|
||||||
|
|
@ -36,8 +36,9 @@ export function SelectedSoundsDisplay() {
|
||||||
const [isLoadingMusic, setIsLoadingMusic] = useState(false);
|
const [isLoadingMusic, setIsLoadingMusic] = useState(false);
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [editingName, setEditingName] = useState('');
|
const [editingName, setEditingName] = useState('');
|
||||||
const [showMusicDropdown, setShowMusicDropdown] = useState(true); // 默认展开
|
|
||||||
const [expandedMusic, setExpandedMusic] = useState<Set<number>>(new Set()); // 跟踪展开的音乐项
|
const [expandedMusic, setExpandedMusic] = useState<Set<number>>(new Set()); // 跟踪展开的音乐项
|
||||||
|
const [expandedCurrent, setExpandedCurrent] = useState(true); // 跟踪当前选中音乐的展开状态,默认展开
|
||||||
|
const [expandedMyMusic, setExpandedMyMusic] = useState(false); // 跟踪我的音乐展开状态,默认收起
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [musicName, setMusicName] = useState('');
|
const [musicName, setMusicName] = useState('');
|
||||||
|
|
||||||
|
|
@ -49,54 +50,98 @@ export function SelectedSoundsDisplay() {
|
||||||
Object.keys(state.sounds).filter(id => state.sounds[id].isSelected)
|
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的操作函数
|
// 获取声音store的操作函数
|
||||||
const unselectAll = useSoundStore(state => state.unselectAll);
|
const unselectAll = useSoundStore(state => state.unselectAll);
|
||||||
const select = useSoundStore(state => state.select);
|
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 () => {
|
const playMusicRecord = async (music: SavedMusic) => {
|
||||||
console.log('🔍 fetchSavedMusic 被调用');
|
try {
|
||||||
console.log('🔐 认证状态:', { isAuthenticated, user: user?.username });
|
// 清空当前所有选择
|
||||||
|
unselectAll();
|
||||||
|
|
||||||
if (!isAuthenticated || !user) {
|
// 根据音乐记录重新选择声音并设置参数
|
||||||
console.log('❌ 用户未认证,退出获取音乐列表');
|
for (const [soundId, volume] of Object.entries(music.volume)) {
|
||||||
setSavedMusicList([]);
|
const speed = music.speed[soundId] || 1;
|
||||||
return;
|
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);
|
setIsLoadingMusic(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('🔍 开始获取音乐列表,用户:', user.username);
|
console.log('🎵 开始获取音乐列表...');
|
||||||
|
console.log('👤 用户信息:', { id: user.id, username: user.username });
|
||||||
|
|
||||||
// 检查localStorage中的token
|
const response = await ApiClient.post('/api/auth/music/list', {
|
||||||
const authStorage = localStorage.getItem('auth-storage');
|
userId: user.id
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查store中的token
|
console.log('📡 响应状态:', response.status);
|
||||||
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.headers);
|
console.log('📡 响应头:', response.headers);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -148,212 +193,83 @@ export function SelectedSoundsDisplay() {
|
||||||
);
|
);
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
setEditingName('');
|
setEditingName('');
|
||||||
console.log('✅ 音乐重命名成功');
|
|
||||||
} else {
|
} else {
|
||||||
setError(data.error || '重命名失败');
|
setError(data.error || '重命名失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 重命名音乐失败:', error);
|
console.error('❌ 重命名失败:', error);
|
||||||
setError('重命名失败');
|
setError('重命名失败,请稍后再试');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除音乐
|
// 删除音乐
|
||||||
const deleteMusic = async (musicId: string) => {
|
const deleteMusic = async (musicId: string) => {
|
||||||
if (!isAuthenticated || !user) return;
|
if (!isAuthenticated || !user) return;
|
||||||
|
|
||||||
if (!confirm('确定要删除这首音乐吗?')) return;
|
if (!confirm('确定要删除这首音乐吗?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('🗑️ 开始删除音乐:', musicId);
|
||||||
const response = await ApiClient.post('/api/auth/music/delete', {
|
const response = await ApiClient.post('/api/auth/music/delete', {
|
||||||
musicId
|
musicId,
|
||||||
|
userId: user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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();
|
const data = await response.json();
|
||||||
|
console.log('📋 删除响应:', data);
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setSavedMusicList(prev => prev.filter(music => music.id !== parseInt(musicId)));
|
setSavedMusicList(prev => prev.filter(music => music.id !== parseInt(musicId)));
|
||||||
console.log('✅ 音乐删除成功');
|
console.log('✅ 音乐删除成功');
|
||||||
} else {
|
} else {
|
||||||
setError(data.error || '删除失败');
|
setError(data.error || '删除失败');
|
||||||
|
console.error('❌ 删除API返回错误:', data.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 删除音乐失败:', 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<string, number> = {};
|
|
||||||
const speed: Record<string, number> = {};
|
|
||||||
const rate: Record<string, number> = {};
|
|
||||||
const random_effects: Record<string, boolean> = {};
|
|
||||||
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated && user) {
|
if (isAuthenticated && user) {
|
||||||
fetchSavedMusic();
|
fetchMusicList();
|
||||||
} else {
|
|
||||||
setSavedMusicList([]);
|
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, user]);
|
}, [isAuthenticated, user]);
|
||||||
|
|
||||||
// 当用户认证状态改变时,获取音乐列表
|
// 如果没有选中的声音,不渲染组件
|
||||||
useEffect(() => {
|
|
||||||
if (isAuthenticated && user) {
|
|
||||||
console.log('🎵 用户已登录,自动获取音乐列表...');
|
|
||||||
fetchSavedMusic();
|
|
||||||
} else {
|
|
||||||
setSavedMusicList([]);
|
|
||||||
}
|
|
||||||
}, [isAuthenticated, user]);
|
|
||||||
|
|
||||||
|
|
||||||
// 如果没有选中任何声音,不显示组件
|
|
||||||
if (selectedSounds.length === 0) {
|
if (selectedSounds.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.soundsContainer}>
|
<div className={styles.soundsContainer}>
|
||||||
{/* 音乐名称配置区域 */}
|
{/* 当前选中音乐标题区域 */}
|
||||||
{selectedSounds.length > 0 && (
|
{selectedSounds.length > 0 && (
|
||||||
|
<div className={styles.musicHeader}>
|
||||||
|
<h4 className={styles.musicTitle}>
|
||||||
|
<FaMusic className={styles.musicIcon} />
|
||||||
|
当前选中音乐
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
className={`${styles.expandButton} ${styles.expandButtonCurrent}`}
|
||||||
|
onClick={toggleExpandedCurrent}
|
||||||
|
title={expandedCurrent ? "收起" : "展开"}
|
||||||
|
>
|
||||||
|
{expandedCurrent ? <FaChevronDown /> : <FaChevronRight />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 音乐名称配置区域 */}
|
||||||
|
{selectedSounds.length > 0 && expandedCurrent && (
|
||||||
<div className={styles.musicNameConfig}>
|
<div className={styles.musicNameConfig}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -369,24 +285,26 @@ export function SelectedSoundsDisplay() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 选中的声音展示 */}
|
{/* 选中的声音展示 */}
|
||||||
<div className={styles.sounds}>
|
{selectedSounds.length > 0 && expandedCurrent && (
|
||||||
<AnimatePresence initial={false}>
|
<div className={styles.sounds}>
|
||||||
{selectedSounds.map((sound) => (
|
<AnimatePresence initial={false}>
|
||||||
<Sound
|
{selectedSounds.map((sound) => (
|
||||||
key={sound.id}
|
<Sound
|
||||||
id={sound.id}
|
key={sound.id}
|
||||||
icon={sound.icon}
|
id={sound.id}
|
||||||
label={sound.label}
|
icon={sound.icon}
|
||||||
src={sound.src}
|
label={sound.label}
|
||||||
functional={false}
|
src={sound.src}
|
||||||
displayMode={true}
|
functional={false}
|
||||||
hidden={false}
|
displayMode={true}
|
||||||
selectHidden={() => {}}
|
hidden={false}
|
||||||
unselectHidden={() => {}}
|
selectHidden={() => {}}
|
||||||
/>
|
unselectHidden={() => {}}
|
||||||
))}
|
/>
|
||||||
</AnimatePresence>
|
))}
|
||||||
</div>
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 音乐列表区域 - 只有登录用户才显示 */}
|
{/* 音乐列表区域 - 只有登录用户才显示 */}
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
|
|
@ -396,6 +314,13 @@ export function SelectedSoundsDisplay() {
|
||||||
<FaCog className={styles.musicIcon} />
|
<FaCog className={styles.musicIcon} />
|
||||||
我的音乐
|
我的音乐
|
||||||
</h4>
|
</h4>
|
||||||
|
<button
|
||||||
|
className={styles.expandButton}
|
||||||
|
onClick={toggleExpandedMyMusic}
|
||||||
|
title={expandedMyMusic ? "收起" : "展开"}
|
||||||
|
>
|
||||||
|
{expandedMyMusic ? <FaChevronDown /> : <FaChevronRight />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 错误提示 */}
|
{/* 错误提示 */}
|
||||||
|
|
@ -416,118 +341,147 @@ export function SelectedSoundsDisplay() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 音乐列表 - 自动显示 */}
|
{/* 音乐列表 - 展开时显示 */}
|
||||||
<div className={styles.musicList}>
|
{expandedMyMusic && (
|
||||||
{console.log('🎵 渲染音乐列表:', { isLoadingMusic, listLength: savedMusicList.length })}
|
<div className={styles.musicList}>
|
||||||
{isLoadingMusic ? (
|
{console.log('🎵 渲染音乐列表:', { isLoadingMusic, listLength: savedMusicList.length })}
|
||||||
<div className={styles.loading}>加载中...</div>
|
{isLoadingMusic ? (
|
||||||
) : savedMusicList.length === 0 ? (
|
<div className={styles.loading}>加载中...</div>
|
||||||
<div className={styles.empty}>
|
) : savedMusicList.length === 0 ? (
|
||||||
<FaMusic className={styles.emptyIcon} />
|
<div className={styles.empty}>
|
||||||
<p>还没有保存的音乐</p>
|
<FaMusic className={styles.emptyIcon} />
|
||||||
<p className={styles.emptyHint}>选择声音并点击保存按钮来创建你的第一首音乐</p>
|
<p>还没有保存的音乐</p>
|
||||||
</div>
|
<p className={styles.emptyHint}>选择声音并点击保存按钮来创建你的第一首音乐</p>
|
||||||
) : (
|
</div>
|
||||||
<AnimatePresence initial={false}>
|
) : (
|
||||||
{savedMusicList.map((music) => (
|
<AnimatePresence initial={false}>
|
||||||
<div key={music.id} className={styles.musicItem}>
|
{savedMusicList.map((music) => (
|
||||||
{editingId === music.id.toString() ? (
|
<div key={music.id} className={styles.musicItem}>
|
||||||
<div className={styles.editForm}>
|
{editingId === music.id.toString() ? (
|
||||||
<input
|
<div className={styles.editForm}>
|
||||||
type="text"
|
<input
|
||||||
value={editingName}
|
type="text"
|
||||||
onChange={(e) => setEditingName(e.target.value)}
|
value={editingName}
|
||||||
className={styles.editInput}
|
onChange={(e) => setEditingName(e.target.value)}
|
||||||
placeholder="输入音乐名称"
|
className={styles.editInput}
|
||||||
maxLength={50}
|
placeholder="输入音乐名称"
|
||||||
/>
|
maxLength={50}
|
||||||
<div className={styles.editButtons}>
|
/>
|
||||||
<button
|
<div className={styles.editButtons}>
|
||||||
onClick={() => {
|
<button
|
||||||
if (editingName.trim()) {
|
|
||||||
renameMusic(music.id.toString(), editingName.trim());
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={`${styles.editButton} ${styles.saveButton}`}
|
|
||||||
title="保存"
|
|
||||||
>
|
|
||||||
✓
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setEditingId(null);
|
|
||||||
setEditingName('');
|
|
||||||
}}
|
|
||||||
className={`${styles.editButton} ${styles.cancelButton}`}
|
|
||||||
title="取消"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.musicContent}>
|
|
||||||
<button
|
|
||||||
onClick={() => playSavedMusic(music)}
|
|
||||||
className={styles.playButton}
|
|
||||||
title="播放这首音乐"
|
|
||||||
>
|
|
||||||
<FaPlay />
|
|
||||||
</button>
|
|
||||||
<div className={styles.musicInfo}>
|
|
||||||
<div className={styles.musicNameRow}>
|
|
||||||
<span
|
|
||||||
className={styles.musicName}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingId(music.id.toString());
|
if (editingName.trim()) {
|
||||||
setEditingName(music.name);
|
renameMusic(music.id.toString(), editingName.trim());
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
title="点击编辑名称"
|
className={`${styles.editButton} ${styles.saveButton}`}
|
||||||
|
title="保存"
|
||||||
>
|
>
|
||||||
{music.name}
|
<FaSave />
|
||||||
</span>
|
</button>
|
||||||
{/* 默认显示收录的声音名字 */}
|
<button
|
||||||
<div className={styles.soundNames}>
|
onClick={() => setEditingId(null)}
|
||||||
{music.sounds && music.sounds.length > 0 ? (
|
className={`${styles.editButton} ${styles.cancelButton}`}
|
||||||
music.sounds.map((soundId: string, index: number) => {
|
title="取消"
|
||||||
// 从所有声音中查找对应的声音名称
|
>
|
||||||
const allSounds = localizedCategories
|
×
|
||||||
.map(category => category.sounds)
|
</button>
|
||||||
.flat();
|
|
||||||
const sound = allSounds.find(s => s.id === soundId);
|
|
||||||
return sound ? (
|
|
||||||
<span key={soundId} className={styles.soundName}>
|
|
||||||
{sound.label}{index < music.sounds.length - 1 ? ', ' : ''}
|
|
||||||
</span>
|
|
||||||
) : null;
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<span className={styles.noSounds}>暂无声音</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
) : (
|
||||||
onClick={() => deleteMusic(music.id.toString())}
|
<div className={styles.musicContent}>
|
||||||
className={styles.deleteButton}
|
<div className={styles.musicNameRow}>
|
||||||
title="删除"
|
<div className={styles.musicInfo}>
|
||||||
>
|
<div className={styles.musicName}>{music.name}</div>
|
||||||
<FaTrash />
|
<div className={styles.soundNames}>
|
||||||
</button>
|
{music.sounds && music.sounds.length > 0 ? (
|
||||||
<button
|
music.sounds.map((soundId, index) => {
|
||||||
onClick={() => toggleMusicExpansion(music.id)}
|
// 从所有声音中查找对应的声音名称
|
||||||
className={styles.expandButton}
|
const allSounds = localizedCategories
|
||||||
title="展开/收起声音详情"
|
.map(category => category.sounds)
|
||||||
>
|
.flat();
|
||||||
{expandedMusic.has(music.id) ? '收起 ▲' : '展开 ▼'}
|
const sound = allSounds.find(s => s.id === soundId);
|
||||||
</button>
|
return sound ? (
|
||||||
</div>
|
<span key={soundId} className={styles.soundName}>
|
||||||
)}
|
{sound.label}{index < music.sounds.length - 1 ? ', ' : ''}
|
||||||
</div>
|
</span>
|
||||||
))}
|
) : null;
|
||||||
</AnimatePresence>
|
})
|
||||||
)}
|
) : (
|
||||||
</div>
|
<span className={styles.noSounds}>暂无声音</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => deleteMusic(music.id.toString())}
|
||||||
|
className={styles.deleteButton}
|
||||||
|
title="删除"
|
||||||
|
>
|
||||||
|
<FaTrash />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleMusicExpansion(music.id)}
|
||||||
|
className={styles.expandButton}
|
||||||
|
title="展开/收起声音详情"
|
||||||
|
>
|
||||||
|
{expandedMusic.has(music.id) ? '收起 ▲' : '展开 ▼'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 展开时显示的声音内容 */}
|
||||||
|
{expandedMusic.has(music.id) && (
|
||||||
|
<div className={styles.expandedMusicContent}>
|
||||||
|
{/* 播放按钮 */}
|
||||||
|
<div className={styles.musicActions}>
|
||||||
|
<button
|
||||||
|
onClick={() => playMusicRecord(music)}
|
||||||
|
className={styles.playMusicButton}
|
||||||
|
title="播放这首音乐"
|
||||||
|
>
|
||||||
|
<FaPlay />
|
||||||
|
播放
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 声音组件展示 */}
|
||||||
|
<div className={styles.sounds}>
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
|
{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 (
|
||||||
|
<Sound
|
||||||
|
key={`${music.id}-${soundId}`}
|
||||||
|
id={soundId}
|
||||||
|
icon={sound.icon}
|
||||||
|
label={sound.label}
|
||||||
|
src={sound.src}
|
||||||
|
functional={false}
|
||||||
|
displayMode={true}
|
||||||
|
hidden={false}
|
||||||
|
selectHidden={() => {}}
|
||||||
|
unselectHidden={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,6 +203,27 @@
|
||||||
color: var(--color-foreground);
|
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 {
|
.soundName {
|
||||||
color: var(--color-foreground-subtle);
|
color: var(--color-foreground-subtle);
|
||||||
background: rgba(var(--color-muted-rgb), 0.3);
|
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 {
|
.button {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue