diff --git a/app/api/songs/route.ts b/app/api/songs/route.ts new file mode 100644 index 0000000..c002122 --- /dev/null +++ b/app/api/songs/route.ts @@ -0,0 +1,147 @@ +import { NextRequest, NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; +import { Song, SongFetchParams } from '@/app/types'; + +// 歌曲数据缓存 +let songsCache: Song[] | null = null; +let cacheTimestamp: number = 0; +const CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存 + +// 读取歌曲数据 +async function readSongsData(): Promise { + const now = Date.now(); + + // 使用缓存 + if (songsCache && (now - cacheTimestamp) < CACHE_DURATION) { + return songsCache; + } + + try { + const filePath = path.join(process.cwd(), 'app/lib/songs/songs.json'); + const fileContent = fs.readFileSync(filePath, 'utf-8'); + const songs = JSON.parse(fileContent) as Song[]; + + // 更新缓存 + songsCache = songs; + cacheTimestamp = now; + + return songs; + } catch (error) { + console.error('Error reading songs data:', error); + throw new Error('Failed to read songs data'); + } +} + +// 过滤歌曲数据 +function filterSongs(songs: Song[], params: SongFetchParams): Song[] { + let filtered = [...songs]; + + // 按ID过滤 + if (params.id !== undefined) { + filtered = filtered.filter(song => song.id === params.id); + } + + // 按标题过滤(支持模糊匹配) + if (params.title) { + const searchTerm = params.title.toLowerCase(); + filtered = filtered.filter(song => + song.title.toLowerCase().includes(searchTerm) + ); + } + + // 按中文标题过滤(支持模糊匹配) + if (params.title_cn) { + const searchTerm = params.title_cn.toLowerCase(); + filtered = filtered.filter(song => + song.title_cn.toLowerCase().includes(searchTerm) + ); + } + + // 按等级过滤 + if (params.level) { + filtered = filtered.filter(song => + Object.keys(song.level).includes(params.level!) + ); + } + + return filtered; +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + + // 解析查询参数 + const params: SongFetchParams = { + id: searchParams.has('id') ? parseInt(searchParams.get('id')!) : undefined, + title: searchParams.get('title') || undefined, + title_cn: searchParams.get('title_cn') || undefined, + level: searchParams.get('level') || undefined, + page: searchParams.has('page') ? parseInt(searchParams.get('page')!) : 1, + limit: searchParams.has('limit') ? parseInt(searchParams.get('limit')!) : 20, + }; + + // 读取歌曲数据 + const allSongs = await readSongsData(); + + // 过滤歌曲 + let filteredSongs = filterSongs(allSongs, params); + + // 计算分页 + const page = params.page || 1; + const limit = params.limit || 20; + const skip = (page - 1) * limit; + const total = filteredSongs.length; + const totalPages = Math.ceil(total / limit); + + // 应用分页 + const paginatedSongs = filteredSongs.slice(skip, skip + limit); + + // 构建响应 + const responseData = { + success: true, + data: { + songs: paginatedSongs, + pagination: { + page, + limit, + total, + totalPages, + hasNextPage: page < totalPages, + hasPrevPage: page > 1, + }, + }, + }; + + // 如果是单个ID查询且未找到,返回404 + if (params.id !== undefined && filteredSongs.length === 0) { + return NextResponse.json( + { success: false, error: `Song with id ${params.id} not found` }, + { status: 404 } + ); + } + + return NextResponse.json(responseData); + + } catch (error) { + console.error('Get songs error:', error); + const errorMessage = error instanceof Error ? error.message : 'Internal server error'; + return NextResponse.json( + { success: false, error: errorMessage }, + { status: 500 } + ); + } +} + +// 添加 OPTIONS 方法处理 CORS 预检请求 +export async function OPTIONS() { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); +} diff --git a/app/challenge-generator/page.tsx b/app/challenge-generator/page.tsx new file mode 100644 index 0000000..0546ef0 --- /dev/null +++ b/app/challenge-generator/page.tsx @@ -0,0 +1,253 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Navbar from '@/app/components/Navbar'; +import ChallengeForm from '@/app/components/ChallengeForm'; +import ChallengeList from '@/app/components/ChallengeList'; +import { Challenge, SongOption } from '@/app/types'; +import { fetchSongs } from '@/app/lib/api'; +import { Download, Trash2, Plus, FileText } from 'lucide-react'; + +export default function ChallengeGeneratorPage() { + const [challenges, setChallenges] = useState([]); + const [songOptions, setSongOptions] = useState([]); + const [loading, setLoading] = useState(false); + const [darkMode, setDarkMode] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + + // 初始化:加载歌曲选项 + useEffect(() => { + loadSongOptions(); + }, []); + + // 加载歌曲选项 + const loadSongOptions = async () => { + setLoading(true); + try { + const { songs } = await fetchSongs({ limit: 100 }); + const options: SongOption[] = songs.map(song => ({ + id: song.id, + title: song.title, + title_cn: song.title_cn, + levels: Object.keys(song.level) + })); + setSongOptions(options); + } catch (error) { + console.error('加载歌曲选项失败:', error); + } finally { + setLoading(false); + } + }; + + // 搜索歌曲 + const handleSearchSongs = async (query: string) => { + setSearchQuery(query); + if (!query.trim()) { + loadSongOptions(); + return; + } + + setLoading(true); + try { + const { songs } = await fetchSongs({ title: query, limit: 50 }); + const options: SongOption[] = songs.map(song => ({ + id: song.id, + title: song.title, + title_cn: song.title_cn, + levels: Object.keys(song.level) + })); + setSongOptions(options); + } catch (error) { + console.error('搜索歌曲失败:', error); + } finally { + setLoading(false); + } + }; + + // 添加课题 + const handleAddChallenge = (challenge: Challenge) => { + setChallenges(prev => [...prev, challenge]); + }; + + // 删除课题 + const handleDeleteChallenge = (id: string) => { + setChallenges(prev => prev.filter(challenge => challenge.id !== id)); + }; + + // 更新课题 + const handleUpdateChallenge = (updatedChallenge: Challenge) => { + setChallenges(prev => prev.map(challenge => + challenge.id === updatedChallenge.id ? updatedChallenge : challenge + )); + }; + + // 清空所有课题 + const handleClearAll = () => { + if (challenges.length > 0 && window.confirm('确定要清空所有课题吗?')) { + setChallenges([]); + } + }; + + // 生成并下载JSON + const handleGenerateJSON = () => { + if (challenges.length === 0) { + alert('请先添加至少一个课题'); + return; + } + + const data = { + challenges: challenges, + generatedAt: new Date().toISOString(), + totalChallenges: challenges.length + }; + + const jsonString = JSON.stringify(data, null, 2); + const blob = new Blob([jsonString], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `课题生成_${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + // 切换暗色模式 + const toggleDarkMode = () => { + setDarkMode(prevDarkMode => { + const newDarkMode = !prevDarkMode; + if (newDarkMode) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + return newDarkMode; + }); + }; + + return ( +
+ {/* 导航栏 */} + + + {/* 主内容 */} +
+ {/* 页面标题 */} +
+

+ + 课题生成器 +

+

+ 创建和管理太鼓达人课题,支持多个课题生成和JSON导出 +

+
+ +
+ {/* 左侧:表单区域 */} +
+
+
+

+ 添加新课题 +

+ +
+ + +
+
+ + {/* 右侧:课题列表和操作区域 */} +
+ {/* 操作按钮 */} +
+
+ + + + +
+ 已添加课题: + + {challenges.length} 个 + +
+
+
+ + {/* 课题列表 */} +
+
+

+ 课题列表 +

+ + 点击课题可编辑 + +
+ + {challenges.length === 0 ? ( +
+
+ +
+

+ 暂无课题 +

+

+ 请在左侧表单中添加第一个课题 +

+
+ ) : ( + + )} +
+ + {/* JSON预览(可选) */} + {challenges.length > 0 && ( +
+

+ JSON预览 +

+
+
+                                        {JSON.stringify({ challenges }, null, 2)}
+                                    
+
+
+ )} +
+
+
+
+ ); +} diff --git a/app/components/ChallengeForm.tsx b/app/components/ChallengeForm.tsx new file mode 100644 index 0000000..1df8368 --- /dev/null +++ b/app/components/ChallengeForm.tsx @@ -0,0 +1,380 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Challenge, ChallengeFormData, ChallengeReward, SongOption } from '@/app/types'; +import { fetchSongs } from '@/app/lib/api'; +import { Search, Music, Star, Target, Gift, MessageSquare, ChevronDown, Loader2, Plus } from 'lucide-react'; + +interface ChallengeFormProps { + songOptions: SongOption[]; + loading: boolean; + searchQuery: string; + onSearch: (query: string) => void; + onSubmit: (challenge: Challenge) => void; + darkMode: boolean; +} + +export default function ChallengeForm({ + songOptions, + loading, + searchQuery, + onSearch, + onSubmit, + darkMode +}: ChallengeFormProps) { + const [formData, setFormData] = useState({ + reward: '15币' + }); + + const [selectedSong, setSelectedSong] = useState(null); + const [availableDifficulties, setAvailableDifficulties] = useState([]); + const [selectedDifficulty, setSelectedDifficulty] = useState(''); + const [stars, setStars] = useState(0); + const [showSongDropdown, setShowSongDropdown] = useState(false); + const [songDetails, setSongDetails] = useState(null); + + // 奖励选项 + const rewardOptions: ChallengeReward[] = ['15币', '30币', '45币', '其他奖励']; + + // 当选择歌曲时,获取歌曲详情和可用难度 + useEffect(() => { + if (formData.songId && songOptions.length > 0) { + const song = songOptions.find(s => s.id === formData.songId); + if (song) { + setSelectedSong(song); + setAvailableDifficulties(song.levels); + + // 获取歌曲详情以显示星数 + fetchSongDetails(song.id); + } + } else { + setSelectedSong(null); + setAvailableDifficulties([]); + setSelectedDifficulty(''); + setStars(0); + } + }, [formData.songId, songOptions]); + + // 当选择难度时,更新星数 + useEffect(() => { + if (selectedDifficulty && songDetails) { + const levelData = songDetails.level[selectedDifficulty]; + if (levelData) { + setStars(levelData.constant); + } + } + }, [selectedDifficulty, songDetails]); + + // 获取歌曲详情 + const fetchSongDetails = async (songId: number) => { + try { + const { songs } = await fetchSongs({ id: songId }); + if (songs.length > 0) { + setSongDetails(songs[0]); + } + } catch (error) { + console.error('获取歌曲详情失败:', error); + } + }; + + // 处理表单提交 + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + const challenge: Challenge = { + id: `challenge_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + songId: formData.songId!, + songTitle: selectedSong?.title || '', + songTitleCn: selectedSong?.title_cn || '', + difficulty: selectedDifficulty, + stars: stars, + requiredScore: formData.requiredScore || 0, + reward: formData.reward, + customReward: formData.reward === '其他奖励' ? formData.customReward : undefined, + notes: formData.notes, + createdAt: new Date().toISOString() + }; + + onSubmit(challenge); + resetForm(); + }; + + // 验证表单 + const validateForm = (): boolean => { + if (!formData.songId) { + alert('请选择歌曲'); + return false; + } + if (!selectedDifficulty) { + alert('请选择难度'); + return false; + } + if (!formData.requiredScore || formData.requiredScore <= 0) { + alert('请输入有效的过关分数'); + return false; + } + if (formData.reward === '其他奖励' && !formData.customReward?.trim()) { + alert('请输入其他奖励内容'); + return false; + } + return true; + }; + + // 重置表单 + const resetForm = () => { + setFormData({ + reward: '15币' + }); + setSelectedSong(null); + setSelectedDifficulty(''); + setStars(0); + setSongDetails(null); + }; + + // 处理歌曲选择 + const handleSongSelect = (song: SongOption) => { + setFormData(prev => ({ ...prev, songId: song.id })); + setShowSongDropdown(false); + }; + + // 处理难度选择 + const handleDifficultySelect = (difficulty: string) => { + setSelectedDifficulty(difficulty); + }; + + // 处理奖励选择 + const handleRewardSelect = (reward: ChallengeReward) => { + setFormData(prev => ({ ...prev, reward })); + }; + + return ( +
+ {/* 歌曲选择 */} +
+ + +
+ {/* 搜索框 */} +
+
+ + onSearch(e.target.value)} + placeholder="搜索歌曲..." + className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+ {loading && ( + + )} +
+ + {/* 歌曲选择下拉框 */} +
+ + + {showSongDropdown && ( +
+ {songOptions.length === 0 ? ( +
+ {loading ? '加载中...' : '未找到歌曲'} +
+ ) : ( + songOptions.map(song => ( + + )) + )} +
+ )} +
+
+
+ + {/* 难度选择 */} + {availableDifficulties.length > 0 && ( +
+ +
+ {availableDifficulties.map(difficulty => ( + + ))} +
+
+ )} + + {/* 星数显示 */} + {stars > 0 && ( +
+
+
+ + 星数 +
+
+ {stars.toFixed(1)} +
+
+

+ 该难度对应的星数(constant值) +

+
+ )} + + {/* 过关分数 */} +
+ + setFormData(prev => ({ + ...prev, + requiredScore: e.target.value ? parseFloat(e.target.value) : undefined + }))} + placeholder="例如:10.5 表示 10.5万分" + className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+ + {/* 奖励选择 */} +
+ +
+ {rewardOptions.map(reward => ( + + ))} +
+ + {/* 其他奖励输入框 */} + {formData.reward === '其他奖励' && ( +
+ setFormData(prev => ({ ...prev, customReward: e.target.value }))} + placeholder="请输入其他奖励内容..." + className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+ )} +
+ + {/* 注释 */} +
+ +