mirror of
https://github.com/remvze/moodist.git
synced 2025-12-18 01:14:17 +00:00
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. 享受更好的用户界面体验
This commit is contained in:
parent
f00263d18c
commit
e01092d97e
14 changed files with 1440 additions and 53 deletions
|
|
@ -11,6 +11,7 @@ import { Container } from '@/components/container';
|
|||
import { StoreConsumer } from '@/components/store-consumer';
|
||||
import { Buttons } from '@/components/buttons';
|
||||
import { SelectedSoundsDisplay } from '@/components/selected-sounds-display';
|
||||
import { SavedMusicList } from '@/components/saved-music-list';
|
||||
import { Categories } from '@/components/categories';
|
||||
import { SharedModal } from '@/components/modals/shared';
|
||||
import { Toolbar } from '@/components/toolbar';
|
||||
|
|
@ -99,6 +100,7 @@ export function App() {
|
|||
<div id="app" />
|
||||
<Buttons />
|
||||
<SelectedSoundsDisplay />
|
||||
<SavedMusicList />
|
||||
<Categories categories={allCategories} />
|
||||
</Container>
|
||||
|
||||
|
|
|
|||
|
|
@ -241,9 +241,9 @@
|
|||
/* 用户菜单样式 */
|
||||
.userMenu {
|
||||
position: fixed;
|
||||
top: 70px;
|
||||
right: 20px;
|
||||
z-index: 999;
|
||||
top: 20px;
|
||||
right: 180px; /* 改为左侧展开,在headerControls的左边 */
|
||||
z-index: 1001; /* 提高层级,确保在最上层 */
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
|
|
@ -282,21 +282,50 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logoutButton {
|
||||
padding: 4px 8px;
|
||||
font-size: var(--font-xsm);
|
||||
color: #ef4444;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
.userActions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.logoutButton:hover {
|
||||
.userActionButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: var(--color-foreground);
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.userActionButton:hover {
|
||||
background: var(--component-hover);
|
||||
}
|
||||
|
||||
.userActionButton.logoutButton {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.userActionButton.logoutButton:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.userActionButton .icon {
|
||||
font-size: 14px;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 暗色主题下的特殊样式 */
|
||||
:global(.dark-theme) .headerControls {
|
||||
background: var(--bg-secondary);
|
||||
|
|
@ -435,8 +464,8 @@
|
|||
}
|
||||
|
||||
.userMenu {
|
||||
top: 60px;
|
||||
right: 15px;
|
||||
top: 15px;
|
||||
right: 155px; /* 移动端适配左侧展开 */
|
||||
}
|
||||
|
||||
.authFormOverlay {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { FaGlobe, FaSun, FaMoon, FaUser } from 'react-icons/fa/index';
|
||||
import { FaGlobe, FaSun, FaMoon, FaUser, FaSignOutAlt, FaCog } from 'react-icons/fa/index';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
|
@ -49,6 +49,20 @@ export function LanguageSwitcher({ className }: LanguageSwitcherProps) {
|
|||
};
|
||||
}, [showUserMenu]);
|
||||
|
||||
// 监听显示登录表单的自定义事件
|
||||
useEffect(() => {
|
||||
const handleShowLoginForm = () => {
|
||||
setShowAuthForm(true);
|
||||
setIsLogin(true); // 默认显示登录表单
|
||||
};
|
||||
|
||||
document.addEventListener('showLoginForm', handleShowLoginForm);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('showLoginForm', handleShowLoginForm);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 主题切换逻辑
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
|
|
@ -292,26 +306,50 @@ export function LanguageSwitcher({ className }: LanguageSwitcherProps) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 用户菜单 - 下拉菜单 */}
|
||||
{isAuthenticated && showUserMenu && (
|
||||
<div className={styles.userMenu}>
|
||||
<div className={styles.userInfo}>
|
||||
<div className={styles.userAvatar}>
|
||||
{user?.username.charAt(0).toUpperCase()}
|
||||
{/* 用户菜单 - 左侧展开菜单 */}
|
||||
<AnimatePresence>
|
||||
{isAuthenticated && showUserMenu && (
|
||||
<motion.div
|
||||
className={styles.userMenu}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className={styles.userInfo}>
|
||||
<div className={styles.userAvatar}>
|
||||
{user?.username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<span className={styles.userName}>{user?.username}</span>
|
||||
</div>
|
||||
<span className={styles.userName}>{user?.username}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleLogout();
|
||||
setShowUserMenu(false);
|
||||
}}
|
||||
className={styles.logoutButton}
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.userActions}>
|
||||
<button
|
||||
className={`${styles.userActionButton}`}
|
||||
onClick={() => {
|
||||
// 这里可以添加个人设置功能
|
||||
setShowUserMenu(false);
|
||||
showNotificationMessage('个人设置功能开发中...', 'success');
|
||||
}}
|
||||
>
|
||||
<FaCog className={styles.icon} />
|
||||
个人设置
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`${styles.userActionButton} ${styles.logoutButton}`}
|
||||
onClick={() => {
|
||||
handleLogout();
|
||||
setShowUserMenu(false);
|
||||
}}
|
||||
>
|
||||
<FaSignOutAlt className={styles.icon} />
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 提示通知 */}
|
||||
{showNotification && (
|
||||
|
|
|
|||
1
src/components/saved-music-list/index.ts
Normal file
1
src/components/saved-music-list/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { SavedMusicList } from './saved-music-list';
|
||||
331
src/components/saved-music-list/saved-music-list.module.css
Normal file
331
src/components/saved-music-list/saved-music-list.module.css
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
.savedMusicList {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.titleIcon {
|
||||
color: var(--color-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
background: var(--bg-error, rgba(239, 68, 68, 0.1));
|
||||
color: var(--color-error, #ef4444);
|
||||
border: 1px solid var(--color-error, #ef4444);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.errorClose {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-error, #ef4444);
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-left: 8px;
|
||||
font-weight: bold;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.errorClose:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: var(--color-foreground-subtle);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 32px 20px;
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 32px;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.empty p {
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.emptyHint {
|
||||
font-size: 13px !important;
|
||||
color: var(--color-muted) !important;
|
||||
margin-top: 8px !important;
|
||||
}
|
||||
|
||||
.musicItems {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.musicItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: var(--component-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.musicItem:hover {
|
||||
background: var(--component-hover);
|
||||
border-color: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
.musicInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.playButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--color-foreground);
|
||||
color: var(--bg-primary);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.playButton:hover {
|
||||
background: var(--color-foreground-subtle);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.playButton:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.musicName {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: var(--color-foreground);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.musicName:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.musicActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.musicItem:hover .musicActions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: transparent;
|
||||
color: var(--color-foreground-subtle);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.actionButton:hover {
|
||||
background: var(--component-hover);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.actionButton.deleteButton:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: var(--color-error, #ef4444);
|
||||
}
|
||||
|
||||
.editForm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.editInput {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--color-foreground);
|
||||
font-size: 14px;
|
||||
min-width: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.editInput:focus {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.editButtons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.editButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
background: var(--color-foreground);
|
||||
color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.saveButton:hover {
|
||||
background: var(--color-foreground-subtle);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
background: var(--color-muted);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.cancelButton:hover {
|
||||
background: var(--color-foreground-subtle);
|
||||
color: var(--bg-primary);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 640px) {
|
||||
.title {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.musicItem {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.playButton {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.musicName {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.musicActions {
|
||||
opacity: 1; /* 移动端始终显示操作按钮 */
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.musicItem {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* 焦点可访问性 */
|
||||
.musicItem:focus-within {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.editInput:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.editButton:focus-visible,
|
||||
.actionButton:focus-visible,
|
||||
.playButton:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
333
src/components/saved-music-list/saved-music-list.tsx
Normal file
333
src/components/saved-music-list/saved-music-list.tsx
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { FaSave } from 'react-icons/fa/index';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
import { useLocalizedSounds } from '@/hooks/useLocalizedSounds';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
import { Sound } from '@/components/sounds/sound';
|
||||
import styles from '../sounds/sounds.module.css';
|
||||
|
|
@ -11,12 +13,93 @@ import styles from '../sounds/sounds.module.css';
|
|||
export function SelectedSoundsDisplay() {
|
||||
const { t } = useTranslation();
|
||||
const localizedCategories = useLocalizedSounds();
|
||||
const { isAuthenticated, user, login, sessionPassword } = useAuthStore();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||
const [showSaveSuccess, setShowSaveSuccess] = useState(false);
|
||||
|
||||
// 获取声音store
|
||||
const sounds = useSoundStore(state => state.sounds);
|
||||
|
||||
// 获取选中的声音
|
||||
const selectedSoundIds = useSoundStore(state =>
|
||||
Object.keys(state.sounds).filter(id => state.sounds[id].isSelected)
|
||||
);
|
||||
|
||||
// 保存音乐功能
|
||||
const saveMusic = async () => {
|
||||
if (!isAuthenticated) {
|
||||
setShowLoginPrompt(true);
|
||||
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;
|
||||
});
|
||||
|
||||
// 检查是否有sessionPassword
|
||||
if (!sessionPassword) {
|
||||
console.error('会话密码丢失,请重新登录');
|
||||
setShowLoginPrompt(true);
|
||||
setIsSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const musicData = {
|
||||
name: `我的音乐 ${new Date().toLocaleDateString()}`,
|
||||
sounds: selectedSoundIds,
|
||||
volume,
|
||||
speed,
|
||||
rate,
|
||||
random_effects,
|
||||
username: user?.username,
|
||||
password: sessionPassword // 使用会话密码
|
||||
};
|
||||
|
||||
// 调用保存API
|
||||
const response = await fetch('/api/auth/music/save', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(musicData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
setShowSaveSuccess(true);
|
||||
setTimeout(() => setShowSaveSuccess(false), 2000);
|
||||
console.log('✅ 音乐保存成功:', result.music);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
console.error('❌ 保存音乐失败:', errorData.error);
|
||||
// 如果是认证错误,显示登录提示
|
||||
if (response.status === 401) {
|
||||
setShowLoginPrompt(true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 保存音乐失败:', error);
|
||||
// 网络错误或其他异常,显示登录提示
|
||||
setShowLoginPrompt(true);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取选中的声音详细信息
|
||||
const selectedSounds = useMemo(() => {
|
||||
const allSounds = localizedCategories
|
||||
|
|
@ -34,23 +117,67 @@ export function SelectedSoundsDisplay() {
|
|||
}
|
||||
|
||||
return (
|
||||
<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 className={styles.soundsContainer}>
|
||||
<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>
|
||||
|
||||
{/* 保存按钮区域 */}
|
||||
<div className={styles.saveSection}>
|
||||
<button
|
||||
className={`${styles.saveButton} ${isSaving ? styles.saving : ''}`}
|
||||
onClick={saveMusic}
|
||||
disabled={isSaving || selectedSounds.length === 0}
|
||||
title={isAuthenticated ? '保存当前音乐配置' : '请先登录后再保存'}
|
||||
>
|
||||
<FaSave />
|
||||
<span>
|
||||
{isSaving ? '保存中...' : '保存音乐'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* 保存成功提示 */}
|
||||
{showSaveSuccess && (
|
||||
<div className={styles.saveSuccess}>
|
||||
✓ 音乐保存成功!
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 登录提示 */}
|
||||
{showLoginPrompt && (
|
||||
<div className={styles.loginPrompt}>
|
||||
<p>请先登录后再保存音乐</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowLoginPrompt(false);
|
||||
// 触发LanguageSwitcher的登录表单
|
||||
const event = new CustomEvent('showLoginForm', { bubbles: true });
|
||||
document.dispatchEvent(event);
|
||||
}}
|
||||
>
|
||||
去登录
|
||||
</button>
|
||||
<button onClick={() => setShowLoginPrompt(false)}>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,6 +5,128 @@
|
|||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.soundsContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.saveSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.saveButton:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #059669, #047857);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.saveButton:disabled {
|
||||
background: var(--color-muted);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.saveButton.saving {
|
||||
background: var(--color-muted);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.saveSuccess {
|
||||
padding: 8px 16px;
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.loginPrompt {
|
||||
padding: 16px;
|
||||
background: var(--component-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--color-foreground);
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.loginPrompt p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
.loginPrompt button {
|
||||
padding: 8px 16px;
|
||||
margin: 0 4px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.loginPrompt button:first-child {
|
||||
background: var(--color-foreground);
|
||||
color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.loginPrompt button:last-child {
|
||||
background: transparent;
|
||||
color: var(--color-foreground);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.loginPrompt button:hover:first-child {
|
||||
background: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
.loginPrompt button:hover:last-child {
|
||||
background: var(--component-hover);
|
||||
border-color: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateY(-10px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,29 @@ export interface CreateUserData {
|
|||
password: string;
|
||||
}
|
||||
|
||||
export interface SavedMusic {
|
||||
id: number;
|
||||
user_id: number;
|
||||
name: string;
|
||||
sounds: string; // JSON string of sound IDs
|
||||
volume: string; // JSON string of volume settings
|
||||
speed: string; // JSON string of speed settings
|
||||
rate: string; // JSON string of rate settings
|
||||
random_effects: string; // JSON string of random effects settings
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreateMusicData {
|
||||
user_id: number;
|
||||
name: string;
|
||||
sounds: string[];
|
||||
volume: Record<string, number>;
|
||||
speed: Record<string, number>;
|
||||
rate: Record<string, number>;
|
||||
random_effects: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export function getDatabase(): Database.Database {
|
||||
if (!db) {
|
||||
// 创建数据库文件路径
|
||||
|
|
@ -39,6 +62,23 @@ export function getDatabase(): Database.Database {
|
|||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// 创建音乐保存表
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS saved_music (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
sounds TEXT NOT NULL,
|
||||
volume TEXT NOT NULL,
|
||||
speed TEXT NOT NULL,
|
||||
rate TEXT NOT NULL,
|
||||
random_effects TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
return db;
|
||||
|
|
@ -105,4 +145,59 @@ export function getUserByUsername(username: string): User | null {
|
|||
const user = database.prepare('SELECT id, username, created_at FROM users WHERE username = ?').get(username) as User | null;
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
// 音乐保存相关函数
|
||||
export async function createMusic(musicData: CreateMusicData): Promise<SavedMusic> {
|
||||
const database = getDatabase();
|
||||
|
||||
const stmt = database.prepare(`
|
||||
INSERT INTO saved_music (user_id, name, sounds, volume, speed, rate, random_effects)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
musicData.user_id,
|
||||
musicData.name,
|
||||
JSON.stringify(musicData.sounds),
|
||||
JSON.stringify(musicData.volume),
|
||||
JSON.stringify(musicData.speed),
|
||||
JSON.stringify(musicData.rate),
|
||||
JSON.stringify(musicData.random_effects)
|
||||
);
|
||||
|
||||
const music = database.prepare('SELECT * FROM saved_music WHERE id = ?').get(result.lastInsertRowid) as SavedMusic;
|
||||
|
||||
return music;
|
||||
}
|
||||
|
||||
export function getUserMusic(userId: number): SavedMusic[] {
|
||||
const database = getDatabase();
|
||||
|
||||
const musicList = database.prepare('SELECT * FROM saved_music WHERE user_id = ? ORDER BY created_at DESC').all(userId) as SavedMusic[];
|
||||
|
||||
return musicList;
|
||||
}
|
||||
|
||||
export function updateMusicName(musicId: number, name: string, userId: number): boolean {
|
||||
const database = getDatabase();
|
||||
|
||||
const stmt = database.prepare(`
|
||||
UPDATE saved_music
|
||||
SET name = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND user_id = ?
|
||||
`);
|
||||
|
||||
const result = stmt.run(name, musicId, userId);
|
||||
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
export function deleteMusic(musicId: number, userId: number): boolean {
|
||||
const database = getDatabase();
|
||||
|
||||
const stmt = database.prepare('DELETE FROM saved_music WHERE id = ? AND user_id = ?');
|
||||
const result = stmt.run(musicId, userId);
|
||||
|
||||
return result.changes > 0;
|
||||
}
|
||||
69
src/pages/api/auth/music/delete.ts
Normal file
69
src/pages/api/auth/music/delete.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import { deleteMusic } from '@/lib/database';
|
||||
import { authenticateUser } from '@/lib/database';
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const body = await request.text();
|
||||
|
||||
if (!body.trim()) {
|
||||
return new Response(JSON.stringify({ error: '请求体不能为空' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const { musicId, username, password } = JSON.parse(body);
|
||||
|
||||
// 验证输入
|
||||
if (!musicId || !username || !password) {
|
||||
return new Response(JSON.stringify({ error: '音乐ID和用户信息不能为空' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 验证用户身份
|
||||
const user = authenticateUser(username, password);
|
||||
if (!user) {
|
||||
return new Response(JSON.stringify({ error: '用户认证失败' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 删除音乐记录
|
||||
const success = deleteMusic(musicId, user.id);
|
||||
|
||||
if (!success) {
|
||||
return new Response(JSON.stringify({ error: '音乐不存在或无权限删除' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
message: '音乐删除成功'
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('删除音乐错误:', error);
|
||||
|
||||
let errorMessage = '删除音乐失败,请稍后再试';
|
||||
|
||||
if (error instanceof SyntaxError && error.message.includes('JSON')) {
|
||||
errorMessage = '请求格式错误';
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ error: errorMessage }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
};
|
||||
75
src/pages/api/auth/music/list.ts
Normal file
75
src/pages/api/auth/music/list.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import { getUserMusic } from '@/lib/database';
|
||||
import { authenticateUser } from '@/lib/database';
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const body = await request.text();
|
||||
|
||||
if (!body.trim()) {
|
||||
return new Response(JSON.stringify({ error: '请求体不能为空' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const { username, password } = JSON.parse(body);
|
||||
|
||||
// 验证输入
|
||||
if (!username || !password) {
|
||||
return new Response(JSON.stringify({ error: '用户名和密码不能为空' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 验证用户身份
|
||||
const user = authenticateUser(username, password);
|
||||
if (!user) {
|
||||
return new Response(JSON.stringify({ error: '用户认证失败' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 获取用户音乐列表
|
||||
const musicList = getUserMusic(user.id);
|
||||
|
||||
// 解析JSON字段
|
||||
const formattedMusicList = musicList.map(music => ({
|
||||
id: music.id,
|
||||
name: music.name,
|
||||
sounds: JSON.parse(music.sounds),
|
||||
volume: JSON.parse(music.volume),
|
||||
speed: JSON.parse(music.speed),
|
||||
rate: JSON.parse(music.rate),
|
||||
random_effects: JSON.parse(music.random_effects),
|
||||
created_at: music.created_at,
|
||||
updated_at: music.updated_at
|
||||
}));
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
musicList: formattedMusicList
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取音乐列表错误:', error);
|
||||
|
||||
let errorMessage = '获取音乐列表失败,请稍后再试';
|
||||
|
||||
if (error instanceof SyntaxError && error.message.includes('JSON')) {
|
||||
errorMessage = '请求格式错误';
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ error: errorMessage }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
};
|
||||
69
src/pages/api/auth/music/rename.ts
Normal file
69
src/pages/api/auth/music/rename.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import { updateMusicName } from '@/lib/database';
|
||||
import { authenticateUser } from '@/lib/database';
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const body = await request.text();
|
||||
|
||||
if (!body.trim()) {
|
||||
return new Response(JSON.stringify({ error: '请求体不能为空' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const { musicId, name, username, password } = JSON.parse(body);
|
||||
|
||||
// 验证输入
|
||||
if (!musicId || !name || !username || !password) {
|
||||
return new Response(JSON.stringify({ error: '音乐ID、新名称和用户信息不能为空' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 验证用户身份
|
||||
const user = authenticateUser(username, password);
|
||||
if (!user) {
|
||||
return new Response(JSON.stringify({ error: '用户认证失败' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 更新音乐名称
|
||||
const success = updateMusicName(musicId, name, user.id);
|
||||
|
||||
if (!success) {
|
||||
return new Response(JSON.stringify({ error: '音乐不存在或无权限修改' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
message: '音乐名称更新成功'
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('重命名音乐错误:', error);
|
||||
|
||||
let errorMessage = '重命名音乐失败,请稍后再试';
|
||||
|
||||
if (error instanceof SyntaxError && error.message.includes('JSON')) {
|
||||
errorMessage = '请求格式错误';
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ error: errorMessage }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
};
|
||||
74
src/pages/api/auth/music/save.ts
Normal file
74
src/pages/api/auth/music/save.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import { createMusic } from '@/lib/database';
|
||||
import { authenticateUser } from '@/lib/database';
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const body = await request.text();
|
||||
|
||||
if (!body.trim()) {
|
||||
return new Response(JSON.stringify({ error: '请求体不能为空' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const { name, sounds, volume, speed, rate, random_effects, username, password } = JSON.parse(body);
|
||||
|
||||
// 验证输入
|
||||
if (!name || !sounds || !username || !password) {
|
||||
return new Response(JSON.stringify({ error: '音乐名称、声音配置和用户信息不能为空' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 验证用户身份
|
||||
const user = authenticateUser(username, password);
|
||||
if (!user) {
|
||||
return new Response(JSON.stringify({ error: '用户认证失败' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// 创建音乐记录
|
||||
const music = await createMusic({
|
||||
user_id: user.id,
|
||||
name,
|
||||
sounds,
|
||||
volume: volume || {},
|
||||
speed: speed || {},
|
||||
rate: rate || {},
|
||||
random_effects: random_effects || {},
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
music: {
|
||||
id: music.id,
|
||||
name: music.name,
|
||||
created_at: music.created_at
|
||||
}
|
||||
}), {
|
||||
status: 201,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存音乐错误:', error);
|
||||
|
||||
let errorMessage = '保存音乐失败,请稍后再试';
|
||||
|
||||
if (error instanceof SyntaxError && error.message.includes('JSON')) {
|
||||
errorMessage = '请求格式错误';
|
||||
} else if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ error: errorMessage }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -12,6 +12,7 @@ interface AuthState {
|
|||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
sessionPassword: string | null; // 仅当前会话使用的密码,不持久化
|
||||
}
|
||||
|
||||
interface AuthStore extends AuthState {
|
||||
|
|
@ -56,6 +57,7 @@ export const useAuthStore = create<AuthStore>()(
|
|||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
sessionPassword: null,
|
||||
|
||||
// Actions
|
||||
login: async (userData) => {
|
||||
|
|
@ -72,6 +74,14 @@ export const useAuthStore = create<AuthStore>()(
|
|||
error: null,
|
||||
});
|
||||
|
||||
set({
|
||||
user,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
sessionPassword: userData.password, // 保存密码用于当前会话的API调用
|
||||
});
|
||||
|
||||
console.log('✅ 用户登录成功:', user.username);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '登录失败';
|
||||
|
|
@ -80,6 +90,7 @@ export const useAuthStore = create<AuthStore>()(
|
|||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: errorMessage,
|
||||
sessionPassword: null,
|
||||
});
|
||||
console.error('❌ 登录失败:', error);
|
||||
throw error;
|
||||
|
|
@ -100,6 +111,14 @@ export const useAuthStore = create<AuthStore>()(
|
|||
error: null,
|
||||
});
|
||||
|
||||
set({
|
||||
user,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
sessionPassword: userData.password, // 保存密码用于当前会话的API调用
|
||||
});
|
||||
|
||||
console.log('✅ 用户注册成功:', user.username);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '注册失败';
|
||||
|
|
@ -108,6 +127,7 @@ export const useAuthStore = create<AuthStore>()(
|
|||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: errorMessage,
|
||||
sessionPassword: null,
|
||||
});
|
||||
console.error('❌ 注册失败:', error);
|
||||
throw error;
|
||||
|
|
@ -120,6 +140,7 @@ export const useAuthStore = create<AuthStore>()(
|
|||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
sessionPassword: null, // 清除会话密码
|
||||
});
|
||||
console.log('✅ 用户已登出');
|
||||
},
|
||||
|
|
@ -171,6 +192,7 @@ export const useAuthStore = create<AuthStore>()(
|
|||
partialize: (state) => ({
|
||||
user: state.user,
|
||||
isAuthenticated: state.isAuthenticated,
|
||||
// 不包含 sessionPassword,仅存储在内存中
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue