mirror of
https://github.com/remvze/moodist.git
synced 2025-12-18 01:14:17 +00:00
feat: 升级到 v3.0.0 - 独立音乐播放系统与 Docker 数据持久化
🎉 版本升级: 2.5.0 → 3.0.0 🎵 音乐播放系统重构: - 独立的音乐播放系统,不影响当前选中声音 - 修复 React Hooks 调用错误 - 使用直接 Howl API 实现音频控制 - 添加播放/停止状态视觉反馈 - 组件显示逻辑完全分离 🐳 Docker 部署优化: - 所有 compose 文件添加 SQLite 数据库挂载 - 支持 WAL 模式和并发写入 - 数据持久化,容器重启不丢失数据 - 创建详细的 Docker 数据库挂载文档 🎨 UI/UX 改进: - 修复当前选中声音与音乐列表显示互斥问题 - 播放按钮状态动态显示 - 组件模块完全独立展示 🗄️ 数据库性能优化: - 启用 WAL 模式提高并发性能 - 优化 SQLite 配置参数 - 添加详细日志和错误处理 📦 新增文件: - docker-database-mount.md: Docker 数据库挂载说明文档
This commit is contained in:
parent
26f6619904
commit
a3c95ec19b
12 changed files with 350 additions and 88 deletions
BIN
data/users.db
BIN
data/users.db
Binary file not shown.
BIN
data/users.db-shm
Normal file
BIN
data/users.db-shm
Normal file
Binary file not shown.
BIN
data/users.db-wal
Normal file
BIN
data/users.db-wal
Normal file
Binary file not shown.
|
|
@ -31,6 +31,8 @@ services:
|
|||
- .:/app
|
||||
- /app/node_modules # 防止node_modules被覆盖
|
||||
- moodist-dist:/app/dist
|
||||
# 挂载 SQLite 数据库文件目录
|
||||
- ./data:/app/data:rw
|
||||
|
||||
# 工作目录
|
||||
working_dir: /app
|
||||
|
|
|
|||
|
|
@ -51,7 +51,14 @@ services:
|
|||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
# 只读根文件系统
|
||||
# 数据卷挂载
|
||||
volumes:
|
||||
# 挂载 SQLite 数据库文件目录(需要读写权限)
|
||||
- ./data:/app/data:rw
|
||||
# 挂载临时目录用于 SQLite WAL 文件
|
||||
- moodist-temp:/tmp:rw
|
||||
|
||||
# 只读根文件系统(除了数据目录)
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /var/cache/nginx
|
||||
|
|
@ -76,6 +83,10 @@ services:
|
|||
profiles:
|
||||
- proxy
|
||||
|
||||
volumes:
|
||||
moodist-temp:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
moodist-network:
|
||||
driver: bridge
|
||||
|
|
|
|||
|
|
@ -8,3 +8,8 @@ services:
|
|||
restart: always
|
||||
ports:
|
||||
- '8080:8080'
|
||||
volumes:
|
||||
# 挂载 SQLite 数据库文件和 WAL 文件
|
||||
- ./data:/app/data:rw
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
|
|
|
|||
113
docker-database-mount.md
Normal file
113
docker-database-mount.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Docker 数据库挂载说明
|
||||
|
||||
## 概述
|
||||
|
||||
本项目已配置 SQLite 数据库文件挂载,确保数据在容器重启后不会丢失。
|
||||
|
||||
## 数据库文件位置
|
||||
|
||||
SQLite 数据库文件位于项目的 `./data` 目录中:
|
||||
- `./data/users.db` - 主数据库文件
|
||||
- `./data/users.db-wal` - Write-Ahead Log 文件
|
||||
- `./data/users.db-shm` - 共享内存文件
|
||||
|
||||
## Docker Compose 配置
|
||||
|
||||
### 1. 基础配置 (`docker-compose.yml`)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
moodist:
|
||||
volumes:
|
||||
# 挂载 SQLite 数据库文件和 WAL 文件
|
||||
- ./data:/app/data:rw
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
```
|
||||
|
||||
### 2. 优化配置 (`docker-compose.optimized.yml`)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
moodist:
|
||||
volumes:
|
||||
# 挂载 SQLite 数据库文件目录(需要读写权限)
|
||||
- ./data:/app/data:rw
|
||||
# 挂载临时目录用于 SQLite WAL 文件
|
||||
- moodist-temp:/tmp:rw
|
||||
|
||||
volumes:
|
||||
moodist-temp:
|
||||
driver: local
|
||||
```
|
||||
|
||||
### 3. 开发配置 (`docker-compose.dev.yml`)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
moodist-dev:
|
||||
volumes:
|
||||
# 挂载 SQLite 数据库文件目录
|
||||
- ./data:/app/data:rw
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 启动服务
|
||||
|
||||
```bash
|
||||
# 生产环境
|
||||
docker-compose up -d
|
||||
|
||||
# 优化环境
|
||||
docker-compose -f docker-compose.optimized.yml up -d
|
||||
|
||||
# 开发环境
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
### 数据持久化
|
||||
|
||||
- 数据库文件会自动创建在 `./data` 目录中
|
||||
- 容器重启或重新创建后数据不会丢失
|
||||
- 支持数据库备份和迁移
|
||||
|
||||
### 备份数据库
|
||||
|
||||
```bash
|
||||
# 备份数据库
|
||||
cp ./data/users.db ./data/users.db.backup.$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
# 查看数据库文件
|
||||
ls -la ./data/
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限问题**: 确保 `./data` 目录有正确的读写权限
|
||||
2. **WAL 模式**: SQLite 使用 WAL (Write-Ahead Logging) 模式,会产生额外的 WAL 和 SHM 文件
|
||||
3. **并发访问**: Docker 挂载确保文件系统的一致性
|
||||
4. **备份策略**: 建议定期备份数据库文件
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 数据库锁定问题
|
||||
如果遇到数据库锁定错误:
|
||||
1. 停止容器:`docker-compose down`
|
||||
2. 删除 WAL 文件:`rm ./data/users.db-wal ./data/users.db-shm`
|
||||
3. 重新启动容器:`docker-compose up -d`
|
||||
|
||||
### 权限问题
|
||||
如果遇到权限错误:
|
||||
```bash
|
||||
# 设置正确的目录权限
|
||||
sudo chown -R 1000:1000 ./data
|
||||
chmod 755 ./data
|
||||
```
|
||||
|
||||
## 开发环境注意事项
|
||||
|
||||
开发环境中,数据库文件会被实时同步到本地文件系统,便于:
|
||||
- 调试和测试
|
||||
- 数据分析
|
||||
- 快速重置测试数据
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "moodist",
|
||||
"type": "module",
|
||||
"version": "2.5.0",
|
||||
"version": "3.0.0",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo, useState, useEffect } from 'react';
|
||||
import { useMemo, useState, useEffect, useRef } from 'react';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { FaSave, FaPlay, FaTrash, FaEdit, FaCog, FaSignOutAlt, FaMusic, FaChevronDown, FaChevronRight } from 'react-icons/fa/index';
|
||||
import { FaSave, FaPlay, FaStop, 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';
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ import { useLocalizedSounds } from '@/hooks/useLocalizedSounds';
|
|||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { ApiClient } from '@/lib/api-client';
|
||||
import { Howl } from 'howler';
|
||||
|
||||
import { Sound } from '@/components/sounds/sound';
|
||||
import styles from '../sounds/sounds.module.css';
|
||||
|
|
@ -42,6 +43,11 @@ export function SelectedSoundsDisplay() {
|
|||
const [error, setError] = useState<string | null>(null);
|
||||
const [musicName, setMusicName] = useState('');
|
||||
|
||||
// 独立的音乐播放状态
|
||||
const [currentlyPlayingMusic, setCurrentlyPlayingMusic] = useState<SavedMusic | null>(null);
|
||||
const musicHowlInstances = useRef<Record<string, Howl>>({});
|
||||
const [isPlayingMusic, setIsPlayingMusic] = useState(false);
|
||||
|
||||
// 获取声音store
|
||||
const sounds = useSoundStore(state => state.sounds);
|
||||
|
||||
|
|
@ -65,34 +71,112 @@ export function SelectedSoundsDisplay() {
|
|||
}
|
||||
};
|
||||
|
||||
// 获取声音store的操作函数
|
||||
const unselectAll = useSoundStore(state => state.unselectAll);
|
||||
const select = useSoundStore(state => state.select);
|
||||
// 获取声音store的操作函数(仅用于控制主要播放状态)
|
||||
const play = useSoundStore(state => state.play);
|
||||
const pause = useSoundStore(state => state.pause);
|
||||
|
||||
// 播放音乐记录 - 清空当前选择并加载音乐的声音配置
|
||||
// 停止音乐播放
|
||||
const stopMusic = () => {
|
||||
console.log('🛑 停止音乐播放');
|
||||
|
||||
// 停止所有音乐相关的 Howl 实例
|
||||
Object.values(musicHowlInstances.current).forEach(howlInstance => {
|
||||
if (howlInstance) {
|
||||
howlInstance.stop();
|
||||
howlInstance.unload();
|
||||
}
|
||||
});
|
||||
|
||||
musicHowlInstances.current = {};
|
||||
setCurrentlyPlayingMusic(null);
|
||||
setIsPlayingMusic(false);
|
||||
};
|
||||
|
||||
// 播放音乐记录 - 使用独立的音乐播放系统,不影响当前选中声音
|
||||
const playMusicRecord = async (music: SavedMusic) => {
|
||||
try {
|
||||
// 清空当前所有选择
|
||||
unselectAll();
|
||||
console.log('🎵 开始播放音乐:', music.name);
|
||||
console.log('🎵 音乐数据:', {
|
||||
sounds: music.sounds,
|
||||
volume: music.volume,
|
||||
speed: music.speed,
|
||||
rate: music.rate,
|
||||
random_effects: music.random_effects
|
||||
});
|
||||
|
||||
// 根据音乐记录重新选择声音并设置参数
|
||||
for (const [soundId, volume] of Object.entries(music.volume)) {
|
||||
const speed = music.speed[soundId] || 1;
|
||||
// 先停止当前播放的音乐
|
||||
stopMusic();
|
||||
|
||||
// 停止主要的选中声音播放(但不改变选中状态)
|
||||
pause();
|
||||
|
||||
// 获取所有声音数据
|
||||
const allSounds = localizedCategories
|
||||
.map(category => category.sounds)
|
||||
.flat();
|
||||
|
||||
// 创建所有声音的 Howl 实例
|
||||
const howlPromises: Promise<Howl>[] = [];
|
||||
|
||||
for (const soundId of music.sounds) {
|
||||
const soundData = allSounds.find(s => s.id === soundId);
|
||||
if (!soundData || !soundData.src) continue;
|
||||
|
||||
const volume = music.volume[soundId] || 0.5;
|
||||
const rate = music.rate[soundId] || 1;
|
||||
const randomEffect = music.random_effects[soundId] || false;
|
||||
const speed = music.speed[soundId] || 1;
|
||||
|
||||
// 选择声音并设置参数
|
||||
select(soundId, {
|
||||
volume,
|
||||
speed,
|
||||
rate,
|
||||
randomEffect
|
||||
console.log(`🔊 创建音乐声音: ${soundId}`, { volume, rate, speed });
|
||||
|
||||
// 创建 Howl 实例的 Promise
|
||||
const howlPromise = new Promise<Howl>((resolve, reject) => {
|
||||
const howl = new Howl({
|
||||
src: [soundData.src],
|
||||
loop: true,
|
||||
volume: volume,
|
||||
rate: rate,
|
||||
preload: true,
|
||||
onload: () => {
|
||||
console.log(`✅ 声音加载完成: ${soundId}`);
|
||||
resolve(howl);
|
||||
},
|
||||
onloaderror: (id, error) => {
|
||||
console.error(`❌ 声音加载失败: ${soundId}`, error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 保存实例引用
|
||||
musicHowlInstances.current[soundId] = howl;
|
||||
});
|
||||
|
||||
howlPromises.push(howlPromise);
|
||||
}
|
||||
|
||||
console.log(`🎵 播放音乐记录: ${music.name}`);
|
||||
// 等待所有声音加载完成
|
||||
console.log('⏳ 等待所有声音加载...');
|
||||
await Promise.all(howlPromises);
|
||||
console.log('✅ 所有声音加载完成,开始播放');
|
||||
|
||||
// 播放所有声音
|
||||
Object.values(musicHowlInstances.current).forEach(howlInstance => {
|
||||
if (howlInstance && howlInstance.state() === 'loaded') {
|
||||
howlInstance.play();
|
||||
}
|
||||
});
|
||||
|
||||
// 设置播放状态
|
||||
setCurrentlyPlayingMusic(music);
|
||||
setIsPlayingMusic(true);
|
||||
|
||||
// 展开对应的音乐记录
|
||||
setExpandedMusic(new Set([music.id]));
|
||||
setExpandedCurrent(false); // 收起当前选中声音模块
|
||||
|
||||
console.log(`✅ 播放音乐记录完成: ${music.name}`);
|
||||
} catch (error) {
|
||||
console.error('❌ 播放音乐记录失败:', error);
|
||||
stopMusic();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -253,6 +337,13 @@ export function SelectedSoundsDisplay() {
|
|||
}
|
||||
}, [isAuthenticated, user]);
|
||||
|
||||
// 组件卸载时清理音乐播放
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
stopMusic();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 监听音乐列表数量,超过5个时默认收起
|
||||
useEffect(() => {
|
||||
if (savedMusicList.length > 5) {
|
||||
|
|
@ -262,66 +353,68 @@ export function SelectedSoundsDisplay() {
|
|||
}
|
||||
}, [savedMusicList.length]);
|
||||
|
||||
// 如果没有选中的声音,不渲染组件
|
||||
if (selectedSounds.length === 0) {
|
||||
// 如果既没有选中声音,也没有音乐列表,则不渲染组件
|
||||
if (selectedSounds.length === 0 && (!isAuthenticated || savedMusicList.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{/* 当前选中声音模块 */}
|
||||
<div className={styles.currentSoundsModule}>
|
||||
<div className={styles.currentSoundsHeader}>
|
||||
<h4 className={styles.currentSoundsTitle}>
|
||||
<FaMusic className={styles.musicIcon} />
|
||||
当前选中的声音
|
||||
</h4>
|
||||
<button
|
||||
className={`${styles.expandButton} ${styles.expandButtonCurrent}`}
|
||||
onClick={toggleExpandedCurrent}
|
||||
title={expandedCurrent ? "收起" : "展开"}
|
||||
>
|
||||
{expandedCurrent ? <FaChevronDown /> : <FaChevronRight />}
|
||||
</button>
|
||||
{/* 当前选中声音模块 - 只有选中声音时才显示 */}
|
||||
{selectedSounds.length > 0 && (
|
||||
<div className={styles.currentSoundsModule}>
|
||||
<div className={styles.currentSoundsHeader}>
|
||||
<h4 className={styles.currentSoundsTitle}>
|
||||
<FaMusic className={styles.musicIcon} />
|
||||
当前选中的声音
|
||||
</h4>
|
||||
<button
|
||||
className={`${styles.expandButton} ${styles.expandButtonCurrent}`}
|
||||
onClick={toggleExpandedCurrent}
|
||||
title={expandedCurrent ? "收起" : "展开"}
|
||||
>
|
||||
{expandedCurrent ? <FaChevronDown /> : <FaChevronRight />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 音乐名称配置区域 */}
|
||||
{expandedCurrent && (
|
||||
<div className={styles.musicNameConfig}>
|
||||
<input
|
||||
type="text"
|
||||
value={musicName}
|
||||
onChange={(e) => setMusicName(e.target.value)}
|
||||
placeholder="音乐名称"
|
||||
className={styles.musicNameInput}
|
||||
maxLength={50}
|
||||
/>
|
||||
<SaveMusicButton />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 选中的声音展示 */}
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 音乐名称配置区域 */}
|
||||
{expandedCurrent && (
|
||||
<div className={styles.musicNameConfig}>
|
||||
<input
|
||||
type="text"
|
||||
value={musicName}
|
||||
onChange={(e) => setMusicName(e.target.value)}
|
||||
placeholder="音乐名称"
|
||||
className={styles.musicNameInput}
|
||||
maxLength={50}
|
||||
/>
|
||||
<SaveMusicButton />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 选中的声音展示 */}
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 音乐列表模块 - 只有登录用户且有音乐时才显示 */}
|
||||
{isAuthenticated && savedMusicList.length > 0 && (
|
||||
|
|
@ -400,6 +493,21 @@ export function SelectedSoundsDisplay() {
|
|||
</div>
|
||||
) : (
|
||||
<div className={styles.musicContent}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (currentlyPlayingMusic?.id === music.id) {
|
||||
stopMusic();
|
||||
} else {
|
||||
playMusicRecord(music);
|
||||
}
|
||||
}}
|
||||
className={`${styles.playButton} ${
|
||||
currentlyPlayingMusic?.id === music.id ? styles.playing : ''
|
||||
}`}
|
||||
title={currentlyPlayingMusic?.id === music.id ? "停止播放" : "播放这首音乐"}
|
||||
>
|
||||
{currentlyPlayingMusic?.id === music.id ? <FaStop /> : <FaPlay />}
|
||||
</button>
|
||||
<div className={styles.musicInfo}>
|
||||
<div className={styles.musicName}>{music.name}</div>
|
||||
<div className={styles.soundNames}>
|
||||
|
|
@ -443,18 +551,6 @@ export function SelectedSoundsDisplay() {
|
|||
{/* 展开时显示的声音内容 */}
|
||||
{expandedMusic.has(music.id) && (
|
||||
<div className={styles.expandedMusicContent}>
|
||||
{/* 播放按钮 */}
|
||||
<div className={styles.expandedMusicActions}>
|
||||
<button
|
||||
onClick={() => playMusicRecord(music)}
|
||||
className={styles.playMusicButton}
|
||||
title="播放这首音乐"
|
||||
>
|
||||
<FaPlay />
|
||||
播放
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 声音组件展示 */}
|
||||
<div className={styles.sounds}>
|
||||
<AnimatePresence initial={false}>
|
||||
|
|
|
|||
|
|
@ -181,6 +181,15 @@
|
|||
background: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
.playButton.playing {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.playButton.playing:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
/* 音乐名称 */
|
||||
.musicName {
|
||||
font-size: 14px;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,17 @@ export function getDatabase(): Database.Database {
|
|||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
db = new Database(dbPath);
|
||||
// 以读写模式打开数据库,启用WAL模式提高并发性能
|
||||
db = new Database(dbPath, {
|
||||
readonly: false,
|
||||
fileMustExist: false,
|
||||
verbose: console.log // 添加SQL执行日志
|
||||
});
|
||||
|
||||
// 启用WAL模式,提高并发写入性能
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('synchronous = NORMAL');
|
||||
db.pragma('cache_size = 1000');
|
||||
|
||||
// 创建用户表
|
||||
db.exec(`
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ export const POST: APIRoute = async ({ request }) => {
|
|||
const { data } = bodyResult;
|
||||
const { name, sounds, volume, speed, rate, random_effects } = data;
|
||||
|
||||
console.log('🎵 保存音乐请求:', { userId: user?.id, name, soundsCount: sounds?.length });
|
||||
|
||||
// 验证输入
|
||||
if (!name || !sounds || !Array.isArray(sounds)) {
|
||||
return new Response(JSON.stringify({
|
||||
|
|
@ -47,6 +49,8 @@ export const POST: APIRoute = async ({ request }) => {
|
|||
random_effects: random_effects || {},
|
||||
});
|
||||
|
||||
console.log('✅ 音乐保存成功:', { id: music.id, name: music.name });
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
message: '音乐保存成功',
|
||||
|
|
@ -61,6 +65,18 @@ export const POST: APIRoute = async ({ request }) => {
|
|||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 保存音乐API错误:', error);
|
||||
|
||||
// 处理特定的数据库错误
|
||||
if (error instanceof Error && error.message.includes('readonly')) {
|
||||
return new Response(JSON.stringify({
|
||||
error: '数据库权限错误,请检查文件权限'
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
return handleApiError(error, '保存音乐');
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue