moodist/src/components/saved-music-list/saved-music-list.tsx
zl e01092d97e feat: 完整实现音乐保存与管理系统 v2.6.0
🎵 新增音乐保存功能
- 实现用户音乐配置保存到SQLite数据库
- 支持保存音量、速度、频率和随机效果等完整配置
- 添加音乐重命名和删除功能

🎨 完善用户界面体验
- 新增保存按钮UI,集成到SelectedSoundsDisplay组件
- 实现SavedMusicList组件,显示用户保存的音乐列表
- 支持一键播放已保存的音乐配置

🔧 优化认证系统架构
- 修复API密码认证问题,添加sessionPassword机制
- 改进错误处理和用户反馈
- 优化用户菜单位置和z-index层级问题

🛠️ 技术改进
- 扩展SQLite数据库,新增saved_music表
- 创建完整的音乐管理API接口(保存/列表/重命名/删除)
- 增强用户认证状态管理,支持会话密码
- 优化CSS样式和动画效果

🎯 用户体验提升
- 修复用户菜单层级遮挡问题
- 重新设计用户菜单位置到左侧展开
- 添加退出登录功能和个人设置预留
- 完善登录提示和错误反馈机制

📝 数据库变更
- 添加saved_music表,存储用户音乐配置
- 支持JSON格式存储复杂的音频参数
- 实现用户关联和权限控制

这次提交实现了完整的音乐保存与管理系统,用户现在可以:
1. 保存当前声音配置为"音乐"
2. 在左侧查看和管理保存的音乐
3. 一键恢复之前的音乐配置
4. 重命名或删除不需要的音乐
5. 享受更好的用户界面体验
2025-11-17 17:17:11 +08:00

333 lines
No EOL
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import { FaMusic, FaEdit, FaTrash, FaPlay } from 'react-icons/fa';
import { AnimatePresence } from 'motion/react';
import { useAuthStore } from '@/stores/auth';
import { useSoundStore } from '@/stores/sound';
import { useTranslation } from '@/hooks/useTranslation';
import type { SavedMusic } from '@/lib/database';
import styles from './saved-music-list.module.css';
interface SavedMusicListProps {
onMusicSelect?: (music: SavedMusic) => void;
}
export function SavedMusicList({ onMusicSelect }: SavedMusicListProps) {
const { t } = useTranslation();
const { isAuthenticated, user, sessionPassword } = useAuthStore();
const [savedMusicList, setSavedMusicList] = useState<SavedMusic[]>([]);
const [loading, setLoading] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [editingName, setEditingName] = useState('');
const [error, setError] = useState<string | null>(null);
// 获取声音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 () => {
if (!isAuthenticated || !user || !sessionPassword) return;
setLoading(true);
setError(null);
try {
const response = await fetch('/api/auth/music/list', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: user.username,
password: sessionPassword, // 使用会话密码
}),
});
if (!response.ok) {
throw new Error('获取音乐列表失败');
}
const data = await response.json();
if (data.success) {
setSavedMusicList(data.musicList || []);
} else {
setError(data.error || '获取音乐列表失败');
}
} catch (err) {
console.error('获取音乐列表错误:', err);
setError('获取音乐列表失败,请稍后再试');
} finally {
setLoading(false);
}
};
// 重命名音乐
const renameMusic = async (musicId: string, newName: string) => {
if (!isAuthenticated || !user) return;
try {
const response = await fetch('/api/auth/music/rename', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
musicId,
name: newName,
username: user.username,
password: sessionPassword,
}),
});
if (!response.ok) {
throw new Error('重命名失败');
}
const data = await response.json();
if (data.success) {
// 更新本地状态
setSavedMusicList(prev =>
prev.map(music =>
music.id === musicId ? { ...music, name: newName } : music
)
);
setEditingId(null);
setEditingName('');
} else {
setError(data.error || '重命名失败');
}
} catch (err) {
console.error('重命名音乐错误:', err);
setError('重命名失败,请稍后再试');
}
};
// 删除音乐
const deleteMusic = async (musicId: string) => {
if (!isAuthenticated || !user) return;
if (!confirm('确定要删除这首音乐吗?')) {
return;
}
try {
const response = await fetch('/api/auth/music/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
musicId,
username: user.username,
password: sessionPassword,
}),
});
if (!response.ok) {
throw new Error('删除失败');
}
const data = await response.json();
if (data.success) {
// 从本地状态中移除
setSavedMusicList(prev => prev.filter(music => music.id !== musicId));
} else {
setError(data.error || '删除失败');
}
} catch (err) {
console.error('删除音乐错误:', err);
setError('删除失败,请稍后再试');
}
};
// 播放保存的音乐
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); // store中存储的是0-1的范围
// 设置速度
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();
// 通知父组件音乐已被选中
if (onMusicSelect) {
onMusicSelect(music);
}
}, 100);
};
// 开始编辑名称
const startEditing = (music: SavedMusic) => {
setEditingId(music.id);
setEditingName(music.name);
};
// 保存编辑
const saveEdit = () => {
if (editingId && editingName.trim()) {
renameMusic(editingId, editingName.trim());
}
};
// 取消编辑
const cancelEdit = () => {
setEditingId(null);
setEditingName('');
setError(null);
};
// 当用户认证状态改变时,获取音乐列表
useEffect(() => {
if (isAuthenticated && user && sessionPassword) {
fetchSavedMusic();
} else {
setSavedMusicList([]);
}
}, [isAuthenticated, user, sessionPassword]);
// 如果用户未登录,不显示组件
if (!isAuthenticated) {
return null;
}
return (
<div className={styles.savedMusicList}>
<h3 className={styles.title}>
<FaMusic className={styles.titleIcon} />
</h3>
{error && (
<div className={styles.error}>
{error}
<button onClick={() => setError(null)} className={styles.errorClose}>×</button>
</div>
)}
{loading ? (
<div className={styles.loading}>...</div>
) : savedMusicList.length === 0 ? (
<div className={styles.empty}>
<FaMusic className={styles.emptyIcon} />
<p></p>
<p className={styles.emptyHint}></p>
</div>
) : (
<div className={styles.musicItems}>
<AnimatePresence initial={false}>
{savedMusicList.map((music) => (
<div key={music.id} className={styles.musicItem}>
{editingId === music.id ? (
<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={saveEdit}
className={`${styles.editButton} ${styles.saveButton}`}
title="保存"
>
</button>
<button
onClick={cancelEdit}
className={`${styles.editButton} ${styles.cancelButton}`}
title="取消"
>
×
</button>
</div>
</div>
) : (
<>
<div className={styles.musicInfo}>
<button
onClick={() => playSavedMusic(music)}
className={styles.playButton}
title="播放这首音乐"
>
<FaPlay />
</button>
<span
className={styles.musicName}
onClick={() => startEditing(music)}
title="点击编辑名称"
>
{music.name}
</span>
</div>
<div className={styles.musicActions}>
<button
onClick={() => startEditing(music)}
className={styles.actionButton}
title="编辑名称"
>
<FaEdit />
</button>
<button
onClick={() => deleteMusic(music.id)}
className={`${styles.actionButton} ${styles.deleteButton}`}
title="删除"
>
<FaTrash />
</button>
</div>
</>
)}
</div>
))}
</AnimatePresence>
</div>
)}
</div>
);
}