mirror of
https://github.com/remvze/moodist.git
synced 2025-12-19 01:44:15 +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 { 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<string | null>(null);
|
||||
const [editingName, setEditingName] = useState('');
|
||||
const [showMusicDropdown, setShowMusicDropdown] = useState(true); // 默认展开
|
||||
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 [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<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(() => {
|
||||
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 (
|
||||
<div className={styles.soundsContainer}>
|
||||
{/* 音乐名称配置区域 */}
|
||||
{/* 当前选中音乐标题区域 */}
|
||||
{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}>
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -369,24 +285,26 @@ export function SelectedSoundsDisplay() {
|
|||
)}
|
||||
|
||||
{/* 选中的声音展示 */}
|
||||
<div className={styles.sounds}>
|
||||
<AnimatePresence initial={false}>
|
||||
{selectedSounds.map((sound) => (
|
||||
<Sound
|
||||
key={sound.id}
|
||||
id={sound.id}
|
||||
icon={sound.icon}
|
||||
label={sound.label}
|
||||
src={sound.src}
|
||||
functional={false}
|
||||
displayMode={true}
|
||||
hidden={false}
|
||||
selectHidden={() => {}}
|
||||
unselectHidden={() => {}}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
{selectedSounds.length > 0 && expandedCurrent && (
|
||||
<div className={styles.sounds}>
|
||||
<AnimatePresence initial={false}>
|
||||
{selectedSounds.map((sound) => (
|
||||
<Sound
|
||||
key={sound.id}
|
||||
id={sound.id}
|
||||
icon={sound.icon}
|
||||
label={sound.label}
|
||||
src={sound.src}
|
||||
functional={false}
|
||||
displayMode={true}
|
||||
hidden={false}
|
||||
selectHidden={() => {}}
|
||||
unselectHidden={() => {}}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 音乐列表区域 - 只有登录用户才显示 */}
|
||||
{isAuthenticated && (
|
||||
|
|
@ -396,6 +314,13 @@ export function SelectedSoundsDisplay() {
|
|||
<FaCog className={styles.musicIcon} />
|
||||
我的音乐
|
||||
</h4>
|
||||
<button
|
||||
className={styles.expandButton}
|
||||
onClick={toggleExpandedMyMusic}
|
||||
title={expandedMyMusic ? "收起" : "展开"}
|
||||
>
|
||||
{expandedMyMusic ? <FaChevronDown /> : <FaChevronRight />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
|
|
@ -416,118 +341,147 @@ export function SelectedSoundsDisplay() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 音乐列表 - 自动显示 */}
|
||||
<div className={styles.musicList}>
|
||||
{console.log('🎵 渲染音乐列表:', { isLoadingMusic, listLength: savedMusicList.length })}
|
||||
{isLoadingMusic ? (
|
||||
<div className={styles.loading}>加载中...</div>
|
||||
) : savedMusicList.length === 0 ? (
|
||||
<div className={styles.empty}>
|
||||
<FaMusic className={styles.emptyIcon} />
|
||||
<p>还没有保存的音乐</p>
|
||||
<p className={styles.emptyHint}>选择声音并点击保存按钮来创建你的第一首音乐</p>
|
||||
</div>
|
||||
) : (
|
||||
<AnimatePresence initial={false}>
|
||||
{savedMusicList.map((music) => (
|
||||
<div key={music.id} className={styles.musicItem}>
|
||||
{editingId === music.id.toString() ? (
|
||||
<div className={styles.editForm}>
|
||||
<input
|
||||
type="text"
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
className={styles.editInput}
|
||||
placeholder="输入音乐名称"
|
||||
maxLength={50}
|
||||
/>
|
||||
<div className={styles.editButtons}>
|
||||
<button
|
||||
onClick={() => {
|
||||
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}
|
||||
{/* 音乐列表 - 展开时显示 */}
|
||||
{expandedMyMusic && (
|
||||
<div className={styles.musicList}>
|
||||
{console.log('🎵 渲染音乐列表:', { isLoadingMusic, listLength: savedMusicList.length })}
|
||||
{isLoadingMusic ? (
|
||||
<div className={styles.loading}>加载中...</div>
|
||||
) : savedMusicList.length === 0 ? (
|
||||
<div className={styles.empty}>
|
||||
<FaMusic className={styles.emptyIcon} />
|
||||
<p>还没有保存的音乐</p>
|
||||
<p className={styles.emptyHint}>选择声音并点击保存按钮来创建你的第一首音乐</p>
|
||||
</div>
|
||||
) : (
|
||||
<AnimatePresence initial={false}>
|
||||
{savedMusicList.map((music) => (
|
||||
<div key={music.id} className={styles.musicItem}>
|
||||
{editingId === music.id.toString() ? (
|
||||
<div className={styles.editForm}>
|
||||
<input
|
||||
type="text"
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
className={styles.editInput}
|
||||
placeholder="输入音乐名称"
|
||||
maxLength={50}
|
||||
/>
|
||||
<div className={styles.editButtons}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingId(music.id.toString());
|
||||
setEditingName(music.name);
|
||||
if (editingName.trim()) {
|
||||
renameMusic(music.id.toString(), editingName.trim());
|
||||
}
|
||||
}}
|
||||
title="点击编辑名称"
|
||||
className={`${styles.editButton} ${styles.saveButton}`}
|
||||
title="保存"
|
||||
>
|
||||
{music.name}
|
||||
</span>
|
||||
{/* 默认显示收录的声音名字 */}
|
||||
<div className={styles.soundNames}>
|
||||
{music.sounds && music.sounds.length > 0 ? (
|
||||
music.sounds.map((soundId: string, index: number) => {
|
||||
// 从所有声音中查找对应的声音名称
|
||||
const allSounds = localizedCategories
|
||||
.map(category => category.sounds)
|
||||
.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>
|
||||
<FaSave />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingId(null)}
|
||||
className={`${styles.editButton} ${styles.cancelButton}`}
|
||||
title="取消"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.musicContent}>
|
||||
<div className={styles.musicNameRow}>
|
||||
<div className={styles.musicInfo}>
|
||||
<div className={styles.musicName}>{music.name}</div>
|
||||
<div className={styles.soundNames}>
|
||||
{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 ? (
|
||||
<span key={soundId} className={styles.soundName}>
|
||||
{sound.label}{index < music.sounds.length - 1 ? ', ' : ''}
|
||||
</span>
|
||||
) : null;
|
||||
})
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue