0419 0318

更新4
This commit is contained in:
spasolreisa
2026-04-19 03:18:42 +08:00
parent d4bbc424c6
commit 74e47971ca
25 changed files with 1221 additions and 371 deletions

View File

@@ -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;
}