import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import '../../../model/song_model.dart'; import '../../../providers/user_provider.dart'; import '../../../service/song_service.dart'; import '../../../service/user_service.dart'; import '../../../widgets/score_progress_chart.dart'; class SongDetailPage extends StatefulWidget { final SongModel song; final Map> userScoreCache; const SongDetailPage({ super.key, required this.song, required this.userScoreCache, }); @override State createState() => _SongDetailPageState(); } class _SongDetailPageState extends State { String? _selectedType; // 缓存图表数据: Key为 "SD" / "DX" / "UT_realId" final Map>> _chartDataCache = {}; final Map _isLoadingChart = {}; @override void initState() { super.initState(); _initSelectedType(); } void _initSelectedType() { if (widget.song.sd != null && widget.song.sd!.isNotEmpty) { _selectedType = 'SD'; } else if (widget.song.dx != null && widget.song.dx!.isNotEmpty) { _selectedType = 'DX'; } else if (widget.song.ut != null && widget.song.ut!.isNotEmpty) { _selectedType = 'UT'; } } List _getAvailableTypes() { List types = []; if (widget.song.sd != null && widget.song.sd!.isNotEmpty) types.add('SD'); if (widget.song.dx != null && widget.song.dx!.isNotEmpty) types.add('DX'); if (widget.song.ut != null && widget.song.ut!.isNotEmpty) types.add('UT'); return types; } List> _getCurrentDifficulties() { List> all = []; if (_selectedType == 'SD' && widget.song.sd != null) { for (var d in widget.song.sd!.values) { all.add({'type': 'SD', 'diff': d}); } } else if (_selectedType == 'DX' && widget.song.dx != null) { for (var d in widget.song.dx!.values) { all.add({'type': 'DX', 'diff': d}); } } else if (_selectedType == 'UT' && widget.song.ut != null) { for (var d in widget.song.ut!.values) { all.add({'type': 'UT', 'diff': d}); } } all.sort((a, b) { int idA = a['diff']['level_id'] ?? 0; int idB = b['diff']['level_id'] ?? 0; return idA.compareTo(idB); }); return all; } // 核心:SD/DX 只加载一次,UT 每个谱面加载一次 Future _loadTypeChartData(String type) async { // 已经加载/正在加载 → 直接返回 if (_chartDataCache.containsKey(type) || _isLoadingChart[type] == true) { return; } setState(() { _isLoadingChart[type] = true; }); try { final userProvider = UserProvider.instance; final token = userProvider.token; if (token == null || token.isEmpty) { setState(() => _isLoadingChart[type] = false); return; } // 获取当前类型的 API ID int apiMusicId; if (type == 'SD') { apiMusicId = widget.song.id; } else if (type == 'DX') { apiMusicId = 10000 + widget.song.id; } else { setState(() => _isLoadingChart[type] = false); return; } // 调用一次 API,拿到全部难度数据 final logs = await UserService.getChartLog(token, [apiMusicId]); logs.sort((a, b) => a.time.compareTo(b.time)); // 转换数据 final chartList = logs.map((log) => { 'achievement': log.segaChartNew?.achievement ?? 0, 'time': log.time, 'scoreRank': log.segaChartNew?.scoreRank ?? 0, 'comboStatus': log.segaChartNew?.comboStatus ?? 0, 'syncStatus': log.segaChartNew?.syncStatus ?? 0, 'deluxscoreMax': log.segaChartNew?.deluxscoreMax ?? 0, 'playCount': log.segaChartNew?.playCount ?? 0, 'level': log.segaChartNew?.level ?? 0, // 保存难度等级用于过滤 }).toList(); setState(() { _chartDataCache[type] = chartList; _isLoadingChart[type] = false; }); } catch (e) { print("Load chart error: $e"); setState(() => _isLoadingChart[type] = false); } } // UT 单独加载 Future _loadUtChartData(String cacheKey, int realId) async { if (_chartDataCache.containsKey(cacheKey) || _isLoadingChart[cacheKey] == true) { return; } setState(() => _isLoadingChart[cacheKey] = true); try { final userProvider = UserProvider.instance; final token = userProvider.token; if (token == null || token.isEmpty) { setState(() => _isLoadingChart[cacheKey] = false); return; } final logs = await UserService.getChartLog(token, [realId]); logs.sort((a, b) => a.time.compareTo(b.time)); final chartList = logs.map((log) => { 'achievement': log.segaChartNew?.achievement ?? 0, 'time': log.time, 'scoreRank': log.segaChartNew?.scoreRank ?? 0, 'comboStatus': log.segaChartNew?.comboStatus ?? 0, 'syncStatus': log.segaChartNew?.syncStatus ?? 0, 'deluxscoreMax': log.segaChartNew?.deluxscoreMax ?? 0, 'playCount': log.segaChartNew?.playCount ?? 0, }).toList(); setState(() { _chartDataCache[cacheKey] = chartList; _isLoadingChart[cacheKey] = false; }); } catch (e) { print("Load UT chart error: $e"); setState(() => _isLoadingChart[cacheKey] = false); } } @override Widget build(BuildContext context) { final coverUrl = _getCoverUrl(widget.song.id); final diffs = _getCurrentDifficulties(); final availableTypes = _getAvailableTypes(); return Scaffold( appBar: AppBar(title: Text(widget.song.title ?? "歌曲详情")), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 歌曲封面 + 信息 Row( crossAxisAlignment: CrossAxisAlignment.start, // 修复:这里缺少了括号 children: [ ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.network( coverUrl, width: 120, height: 120, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container( width: 120, height: 120, color: Colors.grey[200], child: const Icon(Icons.music_note, size: 40), ), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "[${widget.song.id}] ${widget.song.title}", style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( "艺术家:${widget.song.artist ?? '未知'}", style: const TextStyle(fontSize: 14), ), Text( "流派:${widget.song.genre} | 版本:${widget.song.from}", style: const TextStyle(fontSize: 13, color: Colors.grey), ), Text( "BPM:${widget.song.bpm ?? '未知'}", style: const TextStyle(fontSize: 13, color: Colors.grey), ), ], ), ), ], ), const SizedBox(height: 24), // 难度选择器 if (availableTypes.length > 1) ...[ Row( children: [ const Text( "难度详情", style: TextStyle( fontSize: 17, fontWeight: FontWeight.bold, ), ), const SizedBox(width: 10), Expanded( child: CupertinoSlidingSegmentedControl( groupValue: _selectedType, backgroundColor: Colors.grey.shade200, thumbColor: Theme.of(context).primaryColor.withOpacity(0.8), children: { for (var type in availableTypes) type: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Text( type, style: TextStyle( color: _selectedType == type ? Colors.white : Colors.black87, fontWeight: FontWeight.bold, ), ), ), }, onValueChanged: (value) { if (value != null) { setState(() { _selectedType = value; }); } }, ), ), ], ), ] else ...[ const Text( "难度详情", style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold), ), ], const SizedBox(height: 12), // 难度列表 if (diffs.isEmpty) Center( child: Padding( padding: const EdgeInsets.all(20.0), child: Text( "暂无 ${_selectedType ?? ''} 谱面数据", style: const TextStyle(color: Colors.grey), ), ), ) else ...diffs.map((item) => _diffItem( type: item['type'], diff: item['diff'], )).toList(), const SizedBox(height: 30), ], ), ), ); } Widget _diffItem({required String type, required Map diff}) { int levelId = diff['level_id'] ?? 0; final double lvValue = double.tryParse(diff['level_value']?.toString() ?? '') ?? 0; final designer = diff['note_designer'] ?? "-"; final notes = diff['notes'] ?? {}; final total = notes['total'] ?? 0; final tap = notes['tap'] ?? 0; final hold = notes['hold'] ?? 0; final slide = notes['slide'] ?? 0; final brk = notes['break_'] ?? 0; int realId; String? utTitleName; String cacheKey = ""; bool hasScoreData = false; // 加载逻辑:SD/DX 只加载一次,UT 每个加载一次 if (type == 'UT') { realId = (diff['id'] as num?)?.toInt() ?? 0; final utTitleMap = widget.song.utTitle as Map?; if (utTitleMap != null) { final key = diff['id'].toString(); utTitleName = utTitleMap[key]?.toString(); } cacheKey = "UT_$realId"; WidgetsBinding.instance.addPostFrameCallback((_) { _loadUtChartData(cacheKey, realId); }); } else { realId = _getRealMusicId(type); cacheKey = type; // SD/DX 用类型做缓存 key WidgetsBinding.instance.addPostFrameCallback((_) { _loadTypeChartData(type); }); } // 图表数据:SD/DX 自动过滤当前难度,UT 直接使用 List> chartHistory = []; if (_chartDataCache.containsKey(cacheKey)) { if (type == 'UT') { chartHistory = _chartDataCache[cacheKey]!; } else { // SD/DX 从全量数据中过滤当前难度 chartHistory = _chartDataCache[cacheKey]! .where((e) => e['level'] == levelId) .toList(); } } hasScoreData = chartHistory.isNotEmpty; // 成绩信息 final score = widget.userScoreCache[realId]?[levelId]; bool hasUserScore = score != null; // 显示名称与颜色 String name = ""; Color color = Colors.grey; bool isBanquet = type == 'UT'; if (isBanquet) { color = Colors.pinkAccent; name = utTitleName ?? "UT 宴会谱"; } else { switch (levelId) { case 0: name = "$type Basic"; color = Colors.green; break; case 1: name = "$type Advanced"; color = Colors.yellow.shade700; break; case 2: name = "$type Expert"; color = Colors.red; break; case 3: name = "$type Master"; color = Colors.purple; break; case 4: name = "$type Re:Master"; color = Colors.purpleAccent.shade100; break; default: name = type; } } return Card( margin: const EdgeInsets.only(bottom: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.15), borderRadius: BorderRadius.circular(6), border: Border.all(color: color.withOpacity(0.3)), ), child: Text( isBanquet ? name : "$name (${lvValue.toStringAsFixed(1)})", style: TextStyle( color: color, fontWeight: FontWeight.bold, fontSize: 15, ), ), ), if (hasUserScore) const Icon(Icons.star, color: Colors.amber, size: 20), ], ), const SizedBox(height: 8), Text("谱师:$designer", style: const TextStyle(fontSize: 13)), const SizedBox(height: 4), Text( "物量:$total | TAP:$tap HOLD:$hold SLIDE:$slide BRK:$brk", style: const TextStyle(color: Colors.grey, fontSize: 12), ), const SizedBox(height: 12), // ✅ 修复:没有成绩直接显示文字,绝不显示加载圈 if (hasScoreData) Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text("推分进程", style: TextStyle(fontSize: 12, color: Colors.grey)), Text( "最近: ${(chartHistory.last['achievement'] / 10000.0).toStringAsFixed(4)}%", style: TextStyle(fontSize: 12, color: color), ), ], ), const SizedBox(height: 4), ScoreProgressChart( historyScores: chartHistory, lineColor: color, fillColor: color, ), ], ) else const Text("暂无历史记录", style: TextStyle(fontSize: 12, color: Colors.grey)), if (hasUserScore) ...[ const SizedBox(height: 10), const Divider(height: 1), const SizedBox(height: 6), _buildScoreInfo(score!), ], ], ), ), ); } Widget _buildScoreInfo(Map score) { final ach = (score['achievement'] ?? 0) / 10000; final rank = _getRankText(score['scoreRank'] ?? 0); final combo = _comboText(score['comboStatus'] ?? 0); final sync = _syncText(score['syncStatus'] ?? 0); final dxScore = score['deluxscoreMax'] ?? 0; final playCount = score['playCount'] ?? 0; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("你的成绩:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), const SizedBox(height: 4), Row( children: [ Text("达成率:${ach.toStringAsFixed(4)}%", style: TextStyle( color: _getColorByAchievement(ach), fontWeight: FontWeight.bold)), const Spacer(), Text("评级:$rank", style: TextStyle( color: _getColorByRank(rank), fontWeight: FontWeight.bold)), ], ), const SizedBox(height: 2), Text("Combo:$combo | Sync:$sync", style: const TextStyle(fontSize: 12, color: Colors.blueGrey)), Text("DX分数:$dxScore | 游玩次数:$playCount", style: const TextStyle(fontSize: 12, color: Colors.grey)), ], ); } Color _getColorByAchievement(double ach) { if (ach >= 100.5) return const Color(0xFFD4AF37); if (ach >= 100.0) return Colors.purple; if (ach >= 99.5) return Colors.purpleAccent; if (ach >= 99.0) return Colors.deepPurple; if (ach >= 98.0) return Colors.lightBlue; if (ach >= 97.0) return Colors.blue; return Colors.green; } Color _getColorByRank(String rank) { if (rank.contains("SSS")) return Colors.purple; if (rank.contains("SS")) return Colors.deepPurple; if (rank.contains("S")) return Colors.blue; return Colors.green; } String _getCoverUrl(int musicId) { int displayId = musicId % 10000; if (musicId >= 16000 && musicId <= 20000) { String idStr = displayId.toString().padLeft(6, '0'); return "https://u.mai2.link/jacket/UI_Jacket_$idStr.jpg"; } else { return "https://cdn.godserver.cn/resource/static/mai/cover/$displayId.png"; } } String _getRankText(int rank) { switch (rank) { case 13: return "SSS+"; case 12: return "SSS"; case 11: return "SS+"; case 10: return "SS"; case 9: return "S+"; case 8: return "S"; case 7: return "AAA"; case 6: return "AA"; case 5: return "A"; case 4: return "BBB"; case 3: return "BB"; case 2: return "B"; case 1: return "C"; default: return "D"; } } String _comboText(int s) { switch (s) { case 1: return "FC"; case 2: return "FC+"; case 3: return "AP"; case 4: return "AP+"; default: return "无"; } } String _syncText(int s) { switch (s) { case 1: return "FS"; case 2: return "FS+"; case 3: return "FDX"; case 4: return "FDX+"; default: return "无"; } } int _getRealMusicId(String type) { if (type == "SD") return widget.song.id; if (type == "DX") return 10000 + widget.song.id; return 100000 + widget.song.id; } }