import 'dart:io'; 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 'package:share_plus/share_plus.dart'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; class ScorePage extends StatefulWidget { const ScorePage({Key? key}) : super(key: key); @override State createState() => _ScorePageState(); } class _ScorePageState extends State with SingleTickerProviderStateMixin { bool _isLoading = true; bool _isRefreshing = false; String _errorMessage = ''; List _allSongs = []; Map _songMap = {}; List _userMusicList = []; // --- 搜索与筛选状态 --- String _searchQuery = ''; int? _filterLevelType; int? _filterRank; bool _isAdvancedFilterExpanded = false; double? _minAchievement; late final TextEditingController _minAchievementController; String? _filterFromVersion; String? _filterGenre; double? _selectedMinLevel; double? _selectedMaxLevel; int? _filterComboStatus; int? _filterSyncStatus; static const List _levelOptions = [ 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0, 10.5, 11.0, 11.5, 12.0, 12.5, 13.0, 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7, 13.8, 13.9, 14.0, 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7, 14.8, 14.9, 15.0, ]; late FixedExtentScrollController _minLevelScrollController; late FixedExtentScrollController _maxLevelScrollController; Set _availableVersions = {}; Set _availableGenres = {}; late AnimationController _animationController; late Animation _animation; String get _currentDataSource => UserProvider.instance.scoreDataSource; String? get _currentSegaId => UserProvider.instance.selectedSegaId; String? get _currentCnUserName => UserProvider.instance.selectedCnUserName; // 新增 @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); _animation = CurvedAnimation( parent: _animationController, curve: Curves.easeInOut, ); _minAchievementController = TextEditingController(); _minLevelScrollController = FixedExtentScrollController(initialItem: 0); _maxLevelScrollController = FixedExtentScrollController(initialItem: _levelOptions.length - 1); UserProvider.instance.addListener(_onProviderChanged); _loadData(isInitialLoad: true); } void _onProviderChanged() { if (mounted) { _loadData(isInitialLoad: true); } } @override void dispose() { UserProvider.instance.removeListener(_onProviderChanged); _minAchievementController.dispose(); _minLevelScrollController.dispose(); _maxLevelScrollController.dispose(); _animationController.dispose(); super.dispose(); } String _normalizeString(String? str) { if (str == null) return ''; return str.replaceAll(' ', '').replaceAll('\u3000', '').toLowerCase(); } int _normalizeSongId(int musicId) { if (musicId < 10000) return musicId; if (musicId >= 100000) { int candidate = musicId - 100000; if (candidate > 0 && candidate < 10000) return candidate; return musicId % 10000; } if (musicId >= 10000 && musicId < 100000) { return musicId % 10000; } return musicId; } Map _getSongDisplayInfo(int musicId) { int standardId = _normalizeSongId(musicId); SongModel? song = _songMap[standardId]; String? displayTitle; if (song == null) song = _songMap[musicId]; if (song == null && musicId >= 10000) song = _songMap[musicId - 10000]; if (song != null) { if (musicId >= 100000 && song.utTitle != null) { if (song.utTitle is String) { displayTitle = song.utTitle as String; } else if (song.utTitle is Map) { final map = song.utTitle as Map; displayTitle = map['ultra'] ?? map['tera'] ?? map.values.first.toString(); } } displayTitle ??= song.title; } else { displayTitle = "Unknown Song ($musicId)"; } return {'song': song, 'title': displayTitle, 'standardId': standardId}; } 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"; } } /// 【核心修改】加载数据逻辑 Future _loadData({bool isInitialLoad = false}) async { if (isInitialLoad) { setState(() { _isLoading = true; }); } else { setState(() { _isRefreshing = true; }); } try { final userProvider = UserProvider.instance; final token = userProvider.token; if (token == null) throw "未登录,请先登录"; // 1. 获取歌曲元数据 if (_allSongs.isEmpty || isInitialLoad) { final songs = await SongService.getAllSongs(); _allSongs = songs; _songMap = {for (var song in songs) song.id: song}; _availableVersions = songs.map((s) => s.from).where((v) => v.isNotEmpty).toSet(); _availableGenres = songs.map((s) => s.genre).where((g) => g.isNotEmpty).toSet(); } // 2. 获取用户成绩数据 if (_currentDataSource == 'sega') { // --- Sega ID 模式 --- final segaId = _currentSegaId; if (segaId == null || segaId.isEmpty) { throw "请选择一个有效的 Sega ID"; } final rawData = await UserService.getSegaRatingData(token, segaId); final segaId2chartlist = rawData['segaId2chartlist'] as Map?; List rawDetails = []; if (segaId2chartlist != null && segaId2chartlist.containsKey(segaId)) { final dynamic content = segaId2chartlist[segaId]; if (content is List) { rawDetails = content; } } Map> groupedByMusicId = {}; for (var detail in rawDetails) { if (detail is Map && detail.containsKey('musicId')) { int mId = detail['musicId']; if (!groupedByMusicId.containsKey(mId)) { groupedByMusicId[mId] = []; } groupedByMusicId[mId]!.add(detail); } } _userMusicList = groupedByMusicId.entries.map((entry) { return { 'userMusicDetailList': entry.value, 'referenceMusicId': entry.key }; }).toList(); } else { // --- 国服模式 --- // 【修改点】传递选中的用户名 final scoreData = await SongService.getUserAllScores(token,name: _currentCnUserName); if (scoreData.containsKey('userScoreAll_')) { _userMusicList = scoreData['userScoreAll_']['userMusicList'] ?? []; } else if (scoreData.containsKey('userMusicList')) { _userMusicList = scoreData['userMusicList'] ?? []; } else { _userMusicList = []; } } setState(() { _errorMessage = ''; }); } catch (e) { debugPrint("❌ Load Data Error: $e"); if (isInitialLoad) { setState(() { _errorMessage = e.toString(); }); } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("刷新失败: $e"), duration: const Duration(seconds: 2)), ); } } } finally { if (mounted) { setState(() { _isLoading = false; _isRefreshing = false; }); } } } /// 【核心修改】获取过滤并排序后的列表 List get _filteredMusicList { bool isNoFilter = _searchQuery.isEmpty && _filterLevelType == null && _filterRank == null && _filterFromVersion == null && _filterGenre == null && _selectedMinLevel == null && _selectedMaxLevel == null && _minAchievement == null && _filterComboStatus == null && _filterSyncStatus == null; List result; if (isNoFilter) { result = List.from(_userMusicList); } else { final normalizedQuery = _normalizeString(_searchQuery); double? minAchVal = _minAchievement; result = _userMusicList.where((group) { final details = group['userMusicDetailList'] as List? ?? []; if (details.isEmpty) return false; final firstDetail = details[0]; final int rawMusicId = firstDetail['musicId']; final info = _getSongDisplayInfo(rawMusicId); final SongModel? song = info['song'] as SongModel?; if (_searchQuery.isNotEmpty) { bool matchesSearch = false; final int stdId = info['standardId'] as int; final String idSearchStr = "$rawMusicId $stdId"; if (idSearchStr.contains(_searchQuery)) matchesSearch = true; String titleToSearch = info['title'] as String; if (song == null && firstDetail.containsKey('musicName')) { titleToSearch = firstDetail['musicName'] ?? titleToSearch; } final String normTitle = _normalizeString(titleToSearch); if (normTitle.contains(normalizedQuery)) matchesSearch = true; final String normArtist = _normalizeString(song?.artist); if (!matchesSearch && normArtist.contains(normalizedQuery)) matchesSearch = true; if (!matchesSearch && song != null) { for (var alias in song.albums) { if (_normalizeString(alias).contains(normalizedQuery)) { matchesSearch = true; break; } } } if (!matchesSearch && firstDetail.containsKey('alias')) { final dynamic aliasData = firstDetail['alias']; if (aliasData is List) { for (var a in aliasData) { if (a != null && _normalizeString(a.toString()).contains(normalizedQuery)) { matchesSearch = true; break; } } } } if (!matchesSearch) return false; } if (song != null) { if (_filterFromVersion != null && song.from != _filterFromVersion) return false; if (_filterGenre != null && song.genre != _filterGenre) return false; } else { if (_filterFromVersion != null || _filterGenre != null) { return false; } } bool hasMatchingDetail = details.any((detail) { if (_filterLevelType != null && detail['level'] != _filterLevelType) return false; if (_filterRank != null && detail['scoreRank'] != _filterRank) return false; double currentLevel = (detail['level_info'] ?? 0).toDouble(); if (_selectedMinLevel != null && currentLevel < _selectedMinLevel!) return false; if (_selectedMaxLevel != null && currentLevel > _selectedMaxLevel!) return false; double currentAch = (detail['achievement'] ?? 0).toDouble(); if (minAchVal != null) { if (currentAch / 10000 < minAchVal) return false; } if (_filterComboStatus != null && detail['comboStatus'] != _filterComboStatus) return false; if (_filterSyncStatus != null && detail['syncStatus'] != _filterSyncStatus) return false; return true; }); return hasMatchingDetail; }).toList(); } result.sort((a, b) { final detailsA = a['userMusicDetailList'] as List? ?? []; final detailsB = b['userMusicDetailList'] as List? ?? []; int maxRatingA = 0; for (var d in detailsA) { int r = d['rating'] ?? 0; if (r > maxRatingA) maxRatingA = r; } int maxRatingB = 0; for (var d in detailsB) { int r = d['rating'] ?? 0; if (r > maxRatingB) maxRatingB = r; } return maxRatingB.compareTo(maxRatingA); }); return result; } void _resetFilters() { setState(() { _searchQuery = ''; _filterLevelType = null; _filterRank = null; _filterFromVersion = null; _filterGenre = null; _selectedMinLevel = null; _selectedMaxLevel = null; _minAchievement = null; _filterComboStatus = null; _filterSyncStatus = null; }); _minAchievementController.clear(); _minLevelScrollController.jumpToItem(0); _maxLevelScrollController.jumpToItem(_levelOptions.length - 1); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("我的成绩"), actions: [ if (_isRefreshing) const Padding( padding: EdgeInsets.all(8.0), child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)), ) else IconButton(icon: const Icon(Icons.refresh), onPressed: () => _loadData(isInitialLoad: false)) ], ), body: Column( children: [ _buildDataSourceSelector(), _buildFilterBar(), Expanded( child: Container( child: _buildBody(), ), ), ], ), ); } /// 【核心修改】构建数据源选择器,支持国服多账号 Widget _buildDataSourceSelector() { final userProvider = UserProvider.instance; final isSegaMode = userProvider.scoreDataSource == 'sega'; // 国服相关数据 final cnUserNames = userProvider.availableCnUserNames; final currentCnUserName = userProvider.selectedCnUserName; // Sega相关数据 final segaCards = userProvider.availableSegaCards; final currentSegaId = userProvider.selectedSegaId; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), color: Theme.of(context).cardColor, child: Row( children: [ const Text("数据源:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), const SizedBox(width: 8), Expanded( child: ToggleButtons( isSelected: [!isSegaMode, isSegaMode], onPressed: (index) async { final newSource = index == 0 ? 'cn' : 'sega'; await userProvider.setScoreDataSource(newSource); }, borderRadius: BorderRadius.circular(8), selectedBorderColor: Theme.of(context).primaryColor, selectedColor: Colors.white, fillColor: Colors.deepPurple, color: Colors.grey, constraints: const BoxConstraints(minHeight: 30, minWidth: 70), children: const [ Text("国服"), Text("INTL/JP"), ], ), ), const SizedBox(width: 8), Expanded( flex: 1, child: DropdownButtonHideUnderline( child: DropdownButtonFormField( // 如果是 Sega 模式,显示 SegaID;如果是国服模式,显示用户名 value: isSegaMode ? currentSegaId : (currentCnUserName != null && currentCnUserName.isNotEmpty ? currentCnUserName : null), decoration: InputDecoration( hintText: isSegaMode ? "选择卡片" : "选择账号", border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), isDense: false, ), items: isSegaMode ? segaCards.map((card) { return DropdownMenuItem( value: card.segaId, child: Text(card.segaId ?? "Unknown ID", overflow: TextOverflow.ellipsis), ); }).toList() : [ // 国服选项:第一个是“全部/默认”,其余是具体用户名 const DropdownMenuItem( value: null, child: Text("全部/默认", overflow: TextOverflow.ellipsis), ), ...cnUserNames.map((name) { return DropdownMenuItem( value: name, child: Text(name, overflow: TextOverflow.ellipsis), ); }).toList() ], onChanged: (val) async { if (isSegaMode) { await userProvider.setSelectedSegaId(val); } else { await userProvider.setSelectedCnUserName(val); } }, ), ), ), ], ), ); } Widget _buildFilterBar() { return Container( color: Theme.of(context).cardColor, child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), child: Row( children: [ Expanded( child: TextField( decoration: InputDecoration( hintText: "搜歌名/别名/艺术家/ID...", prefixIcon: const Icon(Icons.search), border: OutlineInputBorder(borderRadius: BorderRadius.circular(25)), contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16), isDense: true, ), onChanged: (val) { setState(() { _searchQuery = val; }); }, ), ), IconButton( icon: Icon(_isAdvancedFilterExpanded ? Icons.expand_less : Icons.expand_more), onPressed: () { setState(() { _isAdvancedFilterExpanded = !_isAdvancedFilterExpanded; if (_isAdvancedFilterExpanded) { _animationController.forward(); } else { _animationController.reverse(); } }); }, ), if (_hasActiveFilters()) IconButton( icon: const Icon(Icons.clear, color: Colors.redAccent), onPressed: _resetFilters, ) ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Row( children: [ Expanded( child: _buildDropdown( label: "难度", value: _filterLevelType, items: const [ DropdownMenuItem(value: null, child: Text("全部")), DropdownMenuItem(value: 0, child: Text("Basic")), DropdownMenuItem(value: 1, child: Text("Advanced")), DropdownMenuItem(value: 2, child: Text("Expert")), DropdownMenuItem(value: 3, child: Text("Master")), DropdownMenuItem(value: 4, child: Text("Re:Master")), ], onChanged: (val) => setState(() => _filterLevelType = val), ), ), const SizedBox(width: 8), Expanded( child: _buildDropdown( label: "评级", value: _filterRank, items: const [ DropdownMenuItem(value: null, child: Text("全部")), DropdownMenuItem(value: 13, child: Text("SSS+")), DropdownMenuItem(value: 12, child: Text("SSS")), DropdownMenuItem(value: 10, child: Text("SS")), DropdownMenuItem(value: 8, child: Text("S")), DropdownMenuItem(value: 5, child: Text("A")), ], onChanged: (val) => setState(() => _filterRank = val), ), ), ], ), ), SizeTransition( sizeFactor: _animation, axisAlignment: -1.0, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.withAlpha(20), border: Border(top: BorderSide(color: Colors.grey.shade300)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: _buildDropdown( label: "版本", value: _filterFromVersion, items: [ const DropdownMenuItem(value: null, child: Text("全部")), ..._availableVersions.map((v) => DropdownMenuItem(value: v, child: Text(v))).toList() ], onChanged: (val) => setState(() => _filterFromVersion = val), ), ), const SizedBox(width: 8), Expanded( child: _buildDropdown( label: "流派", value: _filterGenre, items: [ const DropdownMenuItem(value: null, child: Text("全部")), ..._availableGenres.map((g) => DropdownMenuItem(value: g, child: Text(g))).toList() ], onChanged: (val) => setState(() => _filterGenre = val), ), ), ], ), const SizedBox(height: 8), InkWell( onTap: _showLevelPickerDialog, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("定数范围", style: TextStyle(fontWeight: FontWeight.bold)), Text(_getLevelRangeText(), style: TextStyle(color: Colors.grey[600], fontSize: 14)), const Icon(Icons.arrow_drop_down, color: Colors.grey), ], ), ), ), const SizedBox(height: 8), TextField( controller: _minAchievementController, decoration: const InputDecoration( labelText: "最小达成率 (%)", isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), onChanged: (val) { setState(() { if (val.isEmpty) { _minAchievement = null; } else { _minAchievement = double.tryParse(val); } }); }, ), const SizedBox(height: 8), Row( children: [ Expanded( child: _buildDropdown( label: "Combo", value: _filterComboStatus, items: const [ DropdownMenuItem(value: null, child: Text("全部")), DropdownMenuItem(value: 0, child: Text("无")), DropdownMenuItem(value: 1, child: Text("FC")), DropdownMenuItem(value: 2, child: Text("FC+")), DropdownMenuItem(value: 3, child: Text("AP")), DropdownMenuItem(value: 4, child: Text("AP+")), ], onChanged: (val) => setState(() => _filterComboStatus = val), ), ), const SizedBox(width: 8), Expanded( child: _buildDropdown( label: "Sync", value: _filterSyncStatus, items: const [ DropdownMenuItem(value: null, child: Text("全部")), DropdownMenuItem(value: 0, child: Text("无")), DropdownMenuItem(value: 1, child: Text("FS")), DropdownMenuItem(value: 2, child: Text("FS+")), DropdownMenuItem(value: 3, child: Text("FDX")), DropdownMenuItem(value: 4, child: Text("FDX+")), DropdownMenuItem(value: 5, child: Text("Sync")), ], onChanged: (val) => setState(() => _filterSyncStatus = val), ), ), ], ), ], ), ), ), ], ), ); } void _showLevelPickerDialog() { int targetMinIndex = 0; if (_selectedMinLevel != null) { int idx = _levelOptions.indexOf(_selectedMinLevel!); if (idx != -1) targetMinIndex = idx; } int targetMaxIndex = _levelOptions.length - 1; if (_selectedMaxLevel != null) { int idx = _levelOptions.indexOf(_selectedMaxLevel!); if (idx != -1) targetMaxIndex = idx; } showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (context) { WidgetsBinding.instance.addPostFrameCallback((_) { _minLevelScrollController.jumpToItem(targetMinIndex); _maxLevelScrollController.jumpToItem(targetMaxIndex); }); return Container( height: 300, padding: const EdgeInsets.only(top: 16), child: Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton( onPressed: () { setState(() { _selectedMinLevel = null; _selectedMaxLevel = null; }); Navigator.pop(context); }, child: const Text("重置", style: TextStyle(color: Colors.red)), ), const Text("选择定数范围", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), TextButton( onPressed: () { final minIndex = _minLevelScrollController.selectedItem; final maxIndex = _maxLevelScrollController.selectedItem; double minVal = _levelOptions[minIndex]; double maxVal = _levelOptions[maxIndex]; if (minVal > maxVal) { final temp = minVal; minVal = maxVal; maxVal = temp; } setState(() { _selectedMinLevel = minVal; _selectedMaxLevel = maxVal; }); Navigator.pop(context); }, child: const Text("确定", style: TextStyle(fontWeight: FontWeight.bold)), ), ], ), ), const Divider(), Expanded( child: Row( children: [ Expanded( child: Column( children: [ const Padding(padding: EdgeInsets.only(bottom: 8), child: Text("最小定数", style: TextStyle(fontSize: 12, color: Colors.grey))), Expanded( child: CupertinoPicker( scrollController: _minLevelScrollController, itemExtent: 40, onSelectedItemChanged: (index) {}, children: _levelOptions.map((level) { return Center( child: Text( level.toStringAsFixed(1), style: const TextStyle(fontSize: 18,color: Colors.redAccent), ), ); }).toList(), ), ), ], ), ), Expanded( child: Column( children: [ const Padding(padding: EdgeInsets.only(bottom: 8), child: Text("最大定数", style: TextStyle(fontSize: 12, color: Colors.grey))), Expanded( child: CupertinoPicker( scrollController: _maxLevelScrollController, itemExtent: 40, onSelectedItemChanged: (index) {}, children: _levelOptions.map((level) { return Center( child: Text( level.toStringAsFixed(1), style: const TextStyle(fontSize: 18,color: Colors.green), ), ); }).toList(), ), ), ], ), ), ], ), ), ], ), ); }, ); } String _getLevelRangeText() { if (_selectedMinLevel == null && _selectedMaxLevel == null) { return "全部"; } final min = _selectedMinLevel?.toStringAsFixed(1) ?? "1.0"; final max = _selectedMaxLevel?.toStringAsFixed(1) ?? "15.0"; return "$min ~ $max"; } Widget _buildDropdown({ required String label, required dynamic value, required List> items, required ValueChanged onChanged, }) { return DropdownButtonFormField( value: value, isExpanded: true, decoration: InputDecoration( labelText: label, border: const OutlineInputBorder(), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), isDense: false, ), style: Theme.of(context).textTheme.bodyMedium, items: items.map((item) { String textContent = ''; if (item.child is Text) { textContent = (item.child as Text).data ?? ''; } else if (item.child is String) { textContent = item.child as String; } return DropdownMenuItem( value: item.value, child: Container( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Text( textContent, softWrap: true, overflow: TextOverflow.visible, style: Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.2), ), ), ); }).toList(), onChanged: onChanged, ); } bool _hasActiveFilters() { return _searchQuery.isNotEmpty || _filterLevelType != null || _filterRank != null || _filterFromVersion != null || _filterGenre != null || _selectedMinLevel != null || _selectedMaxLevel != null || _minAchievement != null || _filterComboStatus != null || _filterSyncStatus != null; } Widget _buildBody() { if (_isLoading && _userMusicList.isEmpty) { if (_allSongs.isEmpty) { return const Center(child: CircularProgressIndicator()); } return const Center(child: CircularProgressIndicator()); } if (_errorMessage.isNotEmpty && _userMusicList.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(_errorMessage, style: const TextStyle(color: Colors.red)), const SizedBox(height: 20), ElevatedButton(onPressed: () => _loadData(isInitialLoad: true), child: const Text("重试")) ], ), ); } final filteredList = _filteredMusicList; if (filteredList.isEmpty) { return const Center(child: Text("没有找到匹配的成绩")); } final stats = _filterStats; final songCount = stats['songCount'] as int; final scoreCount = stats['scoreCount'] as int; final maxRating = stats['maxRating'] as int; return Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Column( children: [ const Text("歌曲", style: TextStyle(color: Colors.grey, fontSize: 12)), const SizedBox(height: 2), Text("$songCount", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), ], ), Column( children: [ const Text("成绩", style: TextStyle(color: Colors.grey, fontSize: 12)), const SizedBox(height: 2), Text("$scoreCount", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), ], ), Column( children: [ const Text("最高Rating", style: TextStyle(color: Colors.grey, fontSize: 12)), const SizedBox(height: 2), Text("$maxRating", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), ], ), ], ), ), ), Expanded( child: ListView.builder( itemCount: filteredList.length, cacheExtent: 500, itemBuilder: (context, index) { final musicGroup = filteredList[index]; final details = musicGroup['userMusicDetailList'] as List? ?? []; if (details.isEmpty) return const SizedBox.shrink(); final firstDetail = details[0]; final int musicId = firstDetail['musicId']; final info = _getSongDisplayInfo(musicId); final SongModel? songInfo = info['song'] as SongModel?; String rawTitle = info['title'] as String; if (songInfo == null && firstDetail.containsKey('musicName')) { rawTitle = firstDetail['musicName'] ?? rawTitle; } final String displayTitle = "[$musicId] $rawTitle"; final String artist = songInfo?.artist ?? (firstDetail['artist'] ?? 'Unknown Artist'); final String coverUrl = _getCoverUrl(musicId); return Card( margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), clipBehavior: Clip.antiAlias, elevation: 6, shadowColor: Colors.purpleAccent.withOpacity(0.3), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: ExpansionTile( tilePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), leading: GestureDetector( onTap: () { _shareImageViaSystem(coverUrl, rawTitle); }, child: ClipRRect( borderRadius: BorderRadius.circular(4), child: Stack( children: [ Image.network( coverUrl, width: 50, height: 50, fit: BoxFit.cover, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) return child; return Container( width: 50, height: 50, color: Colors.grey[200], child: const Center( child: SizedBox( width: 15, height: 15, child: CircularProgressIndicator( strokeWidth: 2))), ); }, errorBuilder: (context, error, stackTrace) { return Container( width: 50, height: 50, color: Colors.grey[300], child: const Icon(Icons.music_note, color: Colors.grey), ); }, ), Positioned( right: 0, bottom: 0, child: Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), borderRadius: const BorderRadius.only( topLeft: Radius.circular(4), ), ), child: const Icon( Icons.share, color: Colors.white, size: 10, ), ), ) ], ), ), ), title: Text( displayTitle, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Text( artist, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: songInfo == null ? Colors.orange : null), ), children: details.map((detail) { int comboStatus = detail['comboStatus'] ?? 0; String comboText = ""; Color comboColor = Colors.grey; IconData? comboIcon; if (comboStatus == 1) { comboText = "FC"; comboColor = Colors.green; comboIcon = Icons.star; } else if (comboStatus == 2) { comboText = "FC+"; comboColor = Colors.green; comboIcon = Icons.diamond; } else if (comboStatus == 3) { comboText = "AP"; comboColor = Colors.deepOrange; comboIcon = Icons.diamond; }else if (comboStatus == 4) { comboText = "AP+"; comboColor = Colors.purpleAccent; comboIcon = Icons.diamond; } int syncStatus = detail['syncStatus'] ?? 0; String syncText = ""; Color syncColor = Colors.grey; IconData? syncIcon; if (syncStatus == 1) { syncText = "FS"; syncColor = Colors.lightBlueAccent; syncIcon = Icons.check_circle_outline; } else if (syncStatus == 2) { syncText = "FS+"; syncColor = Colors.lightBlueAccent; syncIcon = Icons.check_circle; } else if (syncStatus == 3) { syncText = "FDX"; syncColor = Colors.orangeAccent; syncIcon = Icons.check_circle; } else if (syncStatus == 4) { syncText = "FDX+"; syncColor = Colors.orangeAccent; syncIcon = Icons.auto_awesome; } else if (syncStatus == 5) { syncText = "Sync"; syncColor = Colors.blueAccent; syncIcon = Icons.auto_awesome; } return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), title: Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.white70, borderRadius: BorderRadius.circular(4), ), child: Text( _getLevelName(detail['level']), style: TextStyle( color: _getLevelColor(detail['level']), fontWeight: FontWeight.bold, fontSize: 12 ), ), ), const SizedBox(width: 8), Text( "Lv.${detail['level_info']}", style: const TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.w500), ), ], ), subtitle: Padding( padding: const EdgeInsets.only(top: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.play_circle_outline, size: 16, color: Colors.grey[600]), const SizedBox(width: 4), Text( "Play: ${detail['playCount'] ?? 0}", style: TextStyle(fontSize: 12, color: Colors.grey[700]), ), const SizedBox(width: 12), Icon(Icons.score, size: 16, color: Colors.grey[600]), const SizedBox(width: 4), Text( "DX: ${detail['deluxscoreMax'] ?? 0}", style: TextStyle(fontSize: 12, color: Colors.grey[700]), ), const SizedBox(width: 12), Icon(Icons.attach_file, size: 16, color: Colors.grey[600]), const SizedBox(width: 4), Text( "Rating: ${detail['rating'] ?? 0}", style: TextStyle(fontSize: 12, color: Colors.grey[700]), ), ], ), const SizedBox(height: 4), Row( children: [ if (comboStatus > 0) Container( margin: const EdgeInsets.only(right: 8), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), border: Border.all(color: comboColor.withAlpha(100)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (comboIcon != null) Icon(comboIcon, size: 12, color: comboColor), if (comboIcon != null) const SizedBox(width: 2), Text(comboText, style: TextStyle(fontSize: 14, color: comboColor, fontWeight: FontWeight.bold)), ], ), ), if (syncStatus > 0) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), border: Border.all(color: syncColor.withAlpha(100)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (syncIcon != null) Icon(syncIcon, size: 12, color: syncColor), if (syncIcon != null) const SizedBox(width: 2), Text(syncText, style: TextStyle(fontSize: 14, color: syncColor, fontWeight: FontWeight.bold)), ], ), ), ], ), ], ), ), trailing: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( "${(detail['achievement'] / 10000).toStringAsFixed(4)}%", style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: _getRankColor(detail['scoreRank']), borderRadius: BorderRadius.circular(4), ), child: Text( _getRankText(detail['scoreRank']), style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold), ), ) ], ), ); }).toList(), ), ); }, ), ), ], ); } String _getLevelName(dynamic level) { switch (level) { case 0: return "BAS"; case 1: return "ADV"; case 2: return "EXP"; case 3: return "MAS"; case 4: return "ReM"; default: return "Lv.$level"; } } Color _getLevelColor(dynamic level) { if (level is int) { switch (level) { case 0: return Colors.green; case 1: return Colors.yellow[700]!; case 2: return Colors.red; case 3: return Colors.purple; case 4: return Colors.purpleAccent.withOpacity(0.4); default: return Colors.black; } } return Colors.black; } Color _getRankColor(int rank) { return Colors.blueGrey; } 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"; } } Map get _filterStats { final list = _filteredMusicList; int songCount = list.length; int scoreCount = 0; int maxRating = 0; for (var group in list) { final details = group['userMusicDetailList'] as List? ?? []; scoreCount += details.length; for (var d in details) { int r = d['rating'] ?? 0; if (r > maxRating) maxRating = r; } } return { 'songCount': songCount, 'scoreCount': scoreCount, 'maxRating': maxRating, }; } Future _shareImageViaSystem(String imageUrl, String title) async { if (imageUrl.isEmpty) return; try { final dio = Dio(); final response = await dio.get>( imageUrl, options: Options(responseType: ResponseType.bytes), ); if (response.data == null || response.data!.isEmpty) { throw Exception("图片数据为空"); } final tempDir = await getTemporaryDirectory(); final ext = imageUrl.endsWith('.jpg') || imageUrl.endsWith('.jpeg') ? 'jpg' : 'png'; final fileName = 'shared_img_${DateTime.now().millisecondsSinceEpoch}.$ext'; final file = File('${tempDir.path}/$fileName'); await file.writeAsBytes(response.data!); await Share.shareXFiles( [XFile(file.path)], text: "推荐歌曲:$title", ); } catch (e) { debugPrint("分享失败: $e"); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("分享失败: $e")), ); } } } }