0419 0318
更新4
This commit is contained in:
@@ -32,8 +32,9 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
bool _isAdvancedFilterExpanded = false;
|
||||
double? _minAchievement;
|
||||
late final TextEditingController _minAchievementController;
|
||||
String? _filterFromVersion;
|
||||
String? _filterGenre;
|
||||
// 改为多选
|
||||
List<String> _filterFromVersions = [];
|
||||
List<String> _filterGenres = [];
|
||||
double? _selectedMinLevel;
|
||||
double? _selectedMaxLevel;
|
||||
int? _filterComboStatus;
|
||||
@@ -57,7 +58,7 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
|
||||
String get _currentDataSource => UserProvider.instance.scoreDataSource;
|
||||
String? get _currentSegaId => UserProvider.instance.selectedSegaId;
|
||||
String? get _currentCnUserName => UserProvider.instance.selectedCnUserName; // 新增
|
||||
String? get _currentCnUserName => UserProvider.instance.selectedCnUserName;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -148,7 +149,6 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
}
|
||||
}
|
||||
|
||||
/// 【核心修改】加载数据逻辑
|
||||
Future<void> _loadData({bool isInitialLoad = false}) async {
|
||||
if (isInitialLoad) {
|
||||
setState(() { _isLoading = true; });
|
||||
@@ -161,7 +161,6 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
final token = userProvider.token;
|
||||
if (token == null) throw "未登录,请先登录";
|
||||
|
||||
// 1. 获取歌曲元数据
|
||||
if (_allSongs.isEmpty || isInitialLoad) {
|
||||
final songs = await SongService.getAllSongs();
|
||||
_allSongs = songs;
|
||||
@@ -170,9 +169,7 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
_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";
|
||||
@@ -207,8 +204,6 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
}).toList();
|
||||
|
||||
} else {
|
||||
// --- 国服模式 ---
|
||||
// 【修改点】传递选中的用户名
|
||||
final scoreData = await SongService.getUserAllScores(token,name: _currentCnUserName);
|
||||
|
||||
if (scoreData.containsKey('userScoreAll_')) {
|
||||
@@ -243,13 +238,12 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
}
|
||||
}
|
||||
|
||||
/// 【核心修改】获取过滤并排序后的列表
|
||||
List<dynamic> get _filteredMusicList {
|
||||
bool isNoFilter = _searchQuery.isEmpty &&
|
||||
_filterLevelType == null &&
|
||||
_filterRank == null &&
|
||||
_filterFromVersion == null &&
|
||||
_filterGenre == null &&
|
||||
_filterFromVersions.isEmpty &&
|
||||
_filterGenres.isEmpty &&
|
||||
_selectedMinLevel == null &&
|
||||
_selectedMaxLevel == null &&
|
||||
_minAchievement == null &&
|
||||
@@ -315,10 +309,12 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
}
|
||||
|
||||
if (song != null) {
|
||||
if (_filterFromVersion != null && song.from != _filterFromVersion) return false;
|
||||
if (_filterGenre != null && song.genre != _filterGenre) return false;
|
||||
// 版本多选过滤
|
||||
if (_filterFromVersions.isNotEmpty && !_filterFromVersions.contains(song.from)) return false;
|
||||
// 流派多选过滤
|
||||
if (_filterGenres.isNotEmpty && !_filterGenres.contains(song.genre)) return false;
|
||||
} else {
|
||||
if (_filterFromVersion != null || _filterGenre != null) {
|
||||
if (_filterFromVersions.isNotEmpty || _filterGenres.isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -373,8 +369,8 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
_searchQuery = '';
|
||||
_filterLevelType = null;
|
||||
_filterRank = null;
|
||||
_filterFromVersion = null;
|
||||
_filterGenre = null;
|
||||
_filterFromVersions.clear();
|
||||
_filterGenres.clear();
|
||||
_selectedMinLevel = null;
|
||||
_selectedMaxLevel = null;
|
||||
_minAchievement = null;
|
||||
@@ -415,16 +411,13 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
);
|
||||
}
|
||||
|
||||
/// 【核心修改】构建数据源选择器,支持国服多账号
|
||||
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;
|
||||
|
||||
@@ -459,10 +452,10 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
flex: 1,
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButtonFormField<String>(
|
||||
// 如果是 Sega 模式,显示 SegaID;如果是国服模式,显示用户名
|
||||
value: isSegaMode
|
||||
? currentSegaId
|
||||
: (currentCnUserName != null && currentCnUserName.isNotEmpty ? currentCnUserName : null), decoration: InputDecoration(
|
||||
: (currentCnUserName != null && currentCnUserName.isNotEmpty ? currentCnUserName : null),
|
||||
decoration: InputDecoration(
|
||||
hintText: isSegaMode ? "选择卡片" : "选择账号",
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
|
||||
@@ -476,7 +469,6 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
);
|
||||
}).toList()
|
||||
: [
|
||||
// 国服选项:第一个是“全部/默认”,其余是具体用户名
|
||||
const DropdownMenuItem<String>(
|
||||
value: null,
|
||||
child: Text("全部/默认", overflow: TextOverflow.ellipsis),
|
||||
@@ -598,27 +590,31 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
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),
|
||||
child: _buildMultiSelectButton(
|
||||
title: "版本",
|
||||
selectedList: _filterFromVersions,
|
||||
allItems: _availableVersions.toList(),
|
||||
onConfirm: (selected) {
|
||||
setState(() {
|
||||
_filterFromVersions = selected;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
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),
|
||||
child: _buildMultiSelectButton(
|
||||
title: "流派",
|
||||
selectedList: _filterGenres,
|
||||
allItems: _availableGenres.toList(),
|
||||
onConfirm: (selected) {
|
||||
setState(() {
|
||||
_filterGenres = selected;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -707,6 +703,115 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
);
|
||||
}
|
||||
|
||||
// 多选弹窗组件
|
||||
Widget _buildMultiSelectButton({
|
||||
required String title,
|
||||
required List<String> selectedList,
|
||||
required List<String> allItems,
|
||||
required Function(List<String>) onConfirm,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
_showMultiSelectDialog(
|
||||
context: context,
|
||||
title: title,
|
||||
selected: List.from(selectedList),
|
||||
items: allItems,
|
||||
onConfirm: onConfirm,
|
||||
);
|
||||
},
|
||||
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(title, style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
selectedList.isEmpty
|
||||
? "全部"
|
||||
: selectedList.join(", "),
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 13),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 显示多选弹窗
|
||||
Future<void> _showMultiSelectDialog({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required List<String> selected,
|
||||
required List<String> items,
|
||||
required Function(List<String>) onConfirm,
|
||||
}) async {
|
||||
List<String> tempSelected = List.from(selected);
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text("选择$title"),
|
||||
content: SingleChildScrollView(
|
||||
child: StatefulBuilder( // <--- 添加 StatefulBuilder
|
||||
builder: (context, setState) { // <--- 这里的 setState 只用于刷新 Dialog 内容
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: items.map((item) {
|
||||
return CheckboxListTile(
|
||||
title: Text(item),
|
||||
value: tempSelected.contains(item),
|
||||
onChanged: (isChecked) {
|
||||
// 使用 StatefulBuilder 提供的 setState
|
||||
setState(() {
|
||||
if (isChecked == true) {
|
||||
tempSelected.add(item);
|
||||
} else {
|
||||
tempSelected.remove(item);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: const Text("取消"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
onConfirm(tempSelected);
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: const Text("确定"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
onConfirm([]);
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: const Text("重置", style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showLevelPickerDialog() {
|
||||
int targetMinIndex = 0;
|
||||
if (_selectedMinLevel != null) {
|
||||
@@ -884,8 +989,8 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
return _searchQuery.isNotEmpty ||
|
||||
_filterLevelType != null ||
|
||||
_filterRank != null ||
|
||||
_filterFromVersion != null ||
|
||||
_filterGenre != null ||
|
||||
_filterFromVersions.isNotEmpty ||
|
||||
_filterGenres.isNotEmpty ||
|
||||
_selectedMinLevel != null ||
|
||||
_selectedMaxLevel != null ||
|
||||
_minAchievement != null ||
|
||||
@@ -1145,77 +1250,7 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
),
|
||||
],
|
||||
),
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
subtitle: _buildScoreInfo(detail,songInfo!),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
@@ -1273,7 +1308,122 @@ class _ScorePageState extends State<ScorePage> with SingleTickerProviderStateMix
|
||||
}
|
||||
return Colors.black;
|
||||
}
|
||||
Widget _buildScoreInfo(Map<String, dynamic> score, SongModel song) {
|
||||
final dxScore = score['deluxscoreMax'] ?? 0;
|
||||
final playCount = score['playCount'] ?? 0;
|
||||
final type = score['type'] ?? '';
|
||||
final level = score['level'] ?? 0;
|
||||
final rating = score['rating'] ?? 0;
|
||||
|
||||
int comboStatus = score['comboStatus'] ?? 0;
|
||||
int syncStatus = score['syncStatus'] ?? 0;
|
||||
|
||||
int totalNotes = 0;
|
||||
if ((type == 'dx' && song.dx != null) || (type == 'sd' && song.sd != null)) {
|
||||
final levelKey = level.toString();
|
||||
final levelData = type == 'dx' ? song.dx![levelKey] : song.sd![levelKey];
|
||||
if (levelData != null && levelData['notes'] != null) {
|
||||
totalNotes = levelData['notes']['total'] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
final allDx = totalNotes * 3;
|
||||
final perc = allDx > 0 ? dxScore / allDx : 0.0;
|
||||
|
||||
String? comboIconPath;
|
||||
switch (comboStatus) {
|
||||
case 1: comboIconPath = "images/UI_MSS_MBase_Icon_FC.png"; break;
|
||||
case 2: comboIconPath = "images/UI_MSS_MBase_Icon_FCp.png"; break;
|
||||
case 3: comboIconPath = "images/UI_MSS_MBase_Icon_AP.png"; break;
|
||||
case 4: comboIconPath = "images/UI_MSS_MBase_Icon_APp.png"; break;
|
||||
}
|
||||
|
||||
String? syncIconPath;
|
||||
switch (syncStatus) {
|
||||
case 1: syncIconPath = "images/UI_MSS_MBase_Icon_FS.png"; break;
|
||||
case 2: syncIconPath = "images/UI_MSS_MBase_Icon_FSp.png"; break;
|
||||
case 3: syncIconPath = "images/UI_MSS_MBase_Icon_FSD.png"; break;
|
||||
case 4: syncIconPath = "images/UI_MSS_MBase_Icon_FSDp.png"; break;
|
||||
case 5: syncIconPath = "images/UI_MSS_MBase_Icon_Sync.png"; break;
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.score_rounded, size: 14, color: Colors.blueGrey),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
"DX: $dxScore / $allDx",
|
||||
style: const TextStyle(fontSize: 13, color: Colors.blueGrey, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
|
||||
const Icon(Icons.play_arrow_rounded, size: 14, color: Colors.grey),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
"$playCount 次",
|
||||
style: const TextStyle(fontSize: 13, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
|
||||
const Icon(Icons.star_rate_rounded, size: 14, color: Colors.amber),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
"Rating: $rating",
|
||||
style: const TextStyle(fontSize: 13,fontWeight: FontWeight.w500),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
if (comboIconPath != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Image.asset(comboIconPath, width: 30, height: 30),
|
||||
),
|
||||
|
||||
if (syncIconPath != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Image.asset(syncIconPath, width: 30, height: 30),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (perc >= 0.85) _dxStarIcon(perc),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _dxStarIcon(double perc) {
|
||||
String asset;
|
||||
|
||||
if (perc >= 0.97) {
|
||||
asset = "images/UI_GAM_Gauge_DXScoreIcon_05.png";
|
||||
} else if (perc >= 0.95) {
|
||||
asset = "images/UI_GAM_Gauge_DXScoreIcon_04.png";
|
||||
} else if (perc >= 0.93) {
|
||||
asset = "images/UI_GAM_Gauge_DXScoreIcon_03.png";
|
||||
} else if (perc >= 0.90) {
|
||||
asset = "images/UI_GAM_Gauge_DXScoreIcon_02.png";
|
||||
} else {
|
||||
asset = "images/UI_GAM_Gauge_DXScoreIcon_01.png";
|
||||
}
|
||||
|
||||
return Image.asset(
|
||||
asset,
|
||||
width: 30,
|
||||
height: 30,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
Color _getRankColor(int rank) {
|
||||
return Colors.blueGrey;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user