0417 0022
更新
This commit is contained in:
@@ -1,21 +1,27 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:unionapp/providers/user_provider.dart';
|
import 'package:unionapp/providers/user_provider.dart';
|
||||||
import 'home_screen.dart'; // 1. 导入我们写好的主界面
|
import 'home_screen.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
void main() async {
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
HardwareKeyboard.instance.clearState();
|
||||||
|
|
||||||
|
// 🔥 不 await!直接取实例
|
||||||
final userProvider = UserProvider.instance;
|
final userProvider = UserProvider.instance;
|
||||||
await userProvider.initUser();
|
|
||||||
|
// 🔥 后台异步初始化,不卡界面
|
||||||
|
userProvider.initUser();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ChangeNotifierProvider.value(
|
ChangeNotifierProvider.value(
|
||||||
value: userProvider,
|
value: userProvider,
|
||||||
child: const MaterialApp(home: MyApp()),
|
child: const MyApp(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyApp({super.key});
|
const MyApp({super.key});
|
||||||
|
|
||||||
@@ -23,38 +29,24 @@ class MyApp extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Glass Nav Demo',
|
title: 'Glass Nav Demo',
|
||||||
// 2. 配置主题
|
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
|
|
||||||
// 🔑 关键点:背景色不能是纯白或不透明的纯色
|
|
||||||
// 建议设置一个浅灰色或带渐变的背景,这样玻璃效果才明显
|
|
||||||
scaffoldBackgroundColor: const Color(0xFFF0F2F5),
|
scaffoldBackgroundColor: const Color(0xFFF0F2F5),
|
||||||
|
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
seedColor: Colors.blue,
|
seedColor: Colors.blue,
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 3. 配置暗色模式主题(可选,但推荐)
|
|
||||||
darkTheme: ThemeData(
|
darkTheme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
// 暗色模式下,背景深一点,玻璃效果会更像“磨砂黑玻璃”
|
|
||||||
scaffoldBackgroundColor: const Color(0xFF121212),
|
scaffoldBackgroundColor: const Color(0xFF121212),
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
seedColor: Colors.blue,
|
seedColor: Colors.blue,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 4. 跟随系统自动切换亮/暗模式
|
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.system,
|
||||||
|
|
||||||
// 5. 设置首页为 HomeScreen
|
|
||||||
home: const HomeScreen(),
|
home: const HomeScreen(),
|
||||||
|
|
||||||
// 去掉默认的 debug 标签(可选)
|
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ class SongModel {
|
|||||||
final String from;
|
final String from;
|
||||||
|
|
||||||
// 难度详情映射 key通常是 "0"(Basic), "1"(Advanced) 等,或者 ut 的特殊id
|
// 难度详情映射 key通常是 "0"(Basic), "1"(Advanced) 等,或者 ut 的特殊id
|
||||||
final Map<String, dynamic>? dxLevels;
|
final Map<String, dynamic>? dx;
|
||||||
final Map<String, dynamic>? sdLevels;
|
final Map<String, dynamic>? sd;
|
||||||
final Map<String, dynamic>? utLevels;
|
final Map<String, dynamic>? ut;
|
||||||
|
|
||||||
SongModel({
|
SongModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -30,9 +30,9 @@ class SongModel {
|
|||||||
required this.bpm,
|
required this.bpm,
|
||||||
required this.releaseDate,
|
required this.releaseDate,
|
||||||
required this.from,
|
required this.from,
|
||||||
this.dxLevels,
|
this.dx,
|
||||||
this.sdLevels,
|
this.sd,
|
||||||
this.utLevels,
|
this.ut,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory SongModel.fromJson(Map<String, dynamic> json) {
|
factory SongModel.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -49,9 +49,9 @@ class SongModel {
|
|||||||
bpm: json['bpm'] ?? 0,
|
bpm: json['bpm'] ?? 0,
|
||||||
releaseDate: json['releaseDate'] ?? '',
|
releaseDate: json['releaseDate'] ?? '',
|
||||||
from: json['from'] ?? '',
|
from: json['from'] ?? '',
|
||||||
dxLevels: json['dx'],
|
dx: json['dx'],
|
||||||
sdLevels: json['sd'],
|
sd: json['sd'],
|
||||||
utLevels: json['ut'],
|
ut: json['ut'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../service/recommendation_helper.dart';
|
||||||
|
import '../../service/song_service.dart';
|
||||||
import '../../tool/gradientText.dart';
|
import '../../tool/gradientText.dart';
|
||||||
// 注意:如果 UserPage, SongListPage 等只是作为内部卡片展示,不需要再 import 用于 Navigator push
|
|
||||||
// 但如果其他卡片还需要跳转,保留 import 即可
|
|
||||||
import '../user/userpage.dart';
|
import '../user/userpage.dart';
|
||||||
import '../songlistpage.dart';
|
import '../songlistpage.dart';
|
||||||
import '../scorelist.dart';
|
import '../scorelist.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import '../../providers/user_provider.dart';
|
import '../../providers/user_provider.dart';
|
||||||
|
import '../../model/song_model.dart';
|
||||||
|
|
||||||
class HomePage extends StatelessWidget {
|
class HomePage extends StatelessWidget {
|
||||||
// ✅ 1. 添加回调函数参数
|
|
||||||
final Function(int)? onSwitchTab;
|
final Function(int)? onSwitchTab;
|
||||||
|
|
||||||
const HomePage({super.key, this.onSwitchTab});
|
const HomePage({super.key, this.onSwitchTab});
|
||||||
@@ -57,7 +57,7 @@ class HomePage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(0), // 修正语法错误,原代码可能有误
|
borderRadius: BorderRadius.circular(0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 54,
|
width: 54,
|
||||||
height: 54,
|
height: 54,
|
||||||
@@ -89,13 +89,10 @@ class HomePage extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
// ✅ 2. 修改:用户中心 -> 切换到 Tab 3 (UserPage)
|
|
||||||
_buildCardWithTitle(
|
_buildCardWithTitle(
|
||||||
context: context,
|
context: context,
|
||||||
title: "用户中心",
|
title: "用户中心",
|
||||||
icon: Icons.person_outline,
|
icon: Icons.person_outline,
|
||||||
// 不再传递 targetPage,而是传递 targetIndex
|
|
||||||
targetIndex: 3,
|
targetIndex: 3,
|
||||||
gradient: const LinearGradient(
|
gradient: const LinearGradient(
|
||||||
colors: [Colors.black, Colors.grey],
|
colors: [Colors.black, Colors.grey],
|
||||||
@@ -106,12 +103,11 @@ class HomePage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
|
|
||||||
// 歌曲列表 (保持原有跳转逻辑或自行定义)
|
|
||||||
_buildCardWithTitle(
|
_buildCardWithTitle(
|
||||||
context: context,
|
context: context,
|
||||||
title: "歌曲列表",
|
title: "歌曲列表",
|
||||||
icon: Icons.music_note_outlined,
|
icon: Icons.music_note_outlined,
|
||||||
targetPage: const SongListPage(), // 假设这个还是用 Push 跳转
|
targetPage: const SongListPage(),
|
||||||
gradient: const LinearGradient(
|
gradient: const LinearGradient(
|
||||||
colors: [Color(0xFFff9a9e), Color(0xFFfecfef)],
|
colors: [Color(0xFFff9a9e), Color(0xFFfecfef)],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
@@ -121,7 +117,6 @@ class HomePage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
|
|
||||||
// ✅ 3. 修改:成绩管理 -> 切换到 Tab 1 (ScorePage)
|
|
||||||
_buildCardWithTitle(
|
_buildCardWithTitle(
|
||||||
context: context,
|
context: context,
|
||||||
title: "成绩管理",
|
title: "成绩管理",
|
||||||
@@ -136,7 +131,6 @@ class HomePage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
|
|
||||||
// 娱乐功能
|
|
||||||
_buildCardWithTitle(
|
_buildCardWithTitle(
|
||||||
context: context,
|
context: context,
|
||||||
title: "娱乐功能",
|
title: "娱乐功能",
|
||||||
@@ -151,7 +145,6 @@ class HomePage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
|
|
||||||
// 评分列表
|
|
||||||
_buildCardWithTitle(
|
_buildCardWithTitle(
|
||||||
context: context,
|
context: context,
|
||||||
title: "评分列表",
|
title: "评分列表",
|
||||||
@@ -171,27 +164,176 @@ class HomePage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// ====================== 海报 ======================
|
// ====================== 海报 ======================
|
||||||
// 假设 PosterImage 是你自定义的一个 Widget
|
|
||||||
const PosterImage(imageUrl: 'https://cdn.godserver.cn/post/post%20unionapp1.png'),
|
const PosterImage(imageUrl: 'https://cdn.godserver.cn/post/post%20unionapp1.png'),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// ====================== 新增:用户数据展示卡片 ======================
|
// ====================== 用户数据展示卡片 ======================
|
||||||
const _UserInfoCard(),
|
const _UserInfoCard(),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 左侧:智能推荐乐曲(占 6 份宽度)
|
||||||
|
Expanded(
|
||||||
|
flex: 6,
|
||||||
|
child: const _RecommendedSongsSection(),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 20), // 左右间距
|
||||||
|
// 右侧:快捷按钮区域(占 4 份宽度)
|
||||||
|
Expanded(
|
||||||
|
flex: 6,
|
||||||
|
child: _buildQuickActionButtons(), // 快捷按钮组件
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10), // 左右间距
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// 右侧快捷按钮区域(你可以自由修改图标、文字、点击事件)
|
||||||
|
Widget _buildQuickActionButtons() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"快捷操作",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
// 按钮网格
|
||||||
|
GridView.count(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
crossAxisCount: 2, // 一行 2 个按钮
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
childAspectRatio: 1.0,
|
||||||
|
children: [
|
||||||
|
_quickActionItem(
|
||||||
|
icon: Icons.download,
|
||||||
|
label: "更新成绩",
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Colors.lightBlueAccent.withAlpha(10), Colors.pinkAccent.withOpacity(0.1)],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
color: Colors.pinkAccent.shade100,
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
_quickActionItem(
|
||||||
|
icon: Icons.stars_outlined,
|
||||||
|
label: "ADX",
|
||||||
|
color: Colors.white,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Colors.blueAccent, Colors.pinkAccent.shade100],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.topRight,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blueAccent.withOpacity(0.3),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(2, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
|
||||||
// ✅ 4. 修改构建方法,支持 targetIndex 和 targetPage 两种模式
|
// 橙色渐变
|
||||||
|
_quickActionItem(
|
||||||
|
icon: Icons.generating_tokens,
|
||||||
|
label: "ReiSasol",
|
||||||
|
color: Colors.white,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Colors.orange, Colors.deepOrangeAccent],
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.orange.withOpacity(0.3),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(2, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
_quickActionItem(
|
||||||
|
icon: Icons.link,
|
||||||
|
label: "友站",
|
||||||
|
color: Colors.white,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Colors.grey[600]!, Colors.grey[400]!],
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.2),
|
||||||
|
blurRadius: 6,
|
||||||
|
offset: const Offset(1, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onTap: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 修改后的组件方法
|
||||||
|
Widget _quickActionItem({
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required Color color,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
// 新增:渐变背景(可选)
|
||||||
|
Gradient? gradient,
|
||||||
|
// 新增:阴影(可选)
|
||||||
|
List<BoxShadow>? boxShadow,
|
||||||
|
}) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
// 优先级:渐变 > 纯色背景
|
||||||
|
color: gradient == null ? Colors.black.withOpacity(0.1) : null,
|
||||||
|
gradient: gradient,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: boxShadow,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), // 优化内边距
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: color, size: 28),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: color,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
Widget _buildCardWithTitle({
|
Widget _buildCardWithTitle({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required String title,
|
required String title,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
int? targetIndex, // 新增:如果是切换 Tab,传这个
|
int? targetIndex,
|
||||||
Widget? targetPage, // 保留:如果是页面跳转,传这个
|
Widget? targetPage,
|
||||||
required LinearGradient gradient,
|
required LinearGradient gradient,
|
||||||
required Color shadowColor,
|
required Color shadowColor,
|
||||||
}) {
|
}) {
|
||||||
@@ -201,10 +343,8 @@ class HomePage extends StatelessWidget {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (targetIndex != null && onSwitchTab != null) {
|
if (targetIndex != null && onSwitchTab != null) {
|
||||||
// ✅ 执行 Tab 切换
|
|
||||||
onSwitchTab!(targetIndex);
|
onSwitchTab!(targetIndex);
|
||||||
} else if (targetPage != null) {
|
} else if (targetPage != null) {
|
||||||
// ✅ 执行页面跳转
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => targetPage),
|
MaterialPageRoute(builder: (context) => targetPage),
|
||||||
@@ -269,7 +409,7 @@ class HomePage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================== 新增:用户数据卡片组件 ======================
|
// ====================== 用户数据卡片 ======================
|
||||||
class _UserInfoCard extends StatelessWidget {
|
class _UserInfoCard extends StatelessWidget {
|
||||||
const _UserInfoCard();
|
const _UserInfoCard();
|
||||||
|
|
||||||
@@ -278,19 +418,18 @@ class _UserInfoCard extends StatelessWidget {
|
|||||||
final userProvider = Provider.of<UserProvider>(context);
|
final userProvider = Provider.of<UserProvider>(context);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
// 内边距和海报完全一致,保证同宽
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 17.0),
|
padding: const EdgeInsets.symmetric(horizontal: 17.0),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
color: Colors.white.withOpacity(0.8),
|
color: Colors.white,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.pink.shade100,
|
color: Colors.lightBlueAccent.shade100,
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 6),
|
offset: const Offset(0, 2),
|
||||||
spreadRadius: 1,
|
spreadRadius: 3,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -298,22 +437,8 @@ class _UserInfoCard extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 标题
|
|
||||||
const Text(
|
|
||||||
"当前用户信息",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
color: Colors.purpleAccent,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 5, thickness: 1),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
|
|
||||||
// 用户信息行
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
// 头像
|
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(0),
|
borderRadius: BorderRadius.circular(0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@@ -331,14 +456,12 @@ class _UserInfoCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 18),
|
const SizedBox(width: 18),
|
||||||
|
|
||||||
// 文字信息
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
GradientText(
|
GradientText(
|
||||||
data:"用户名:${userProvider.username}",
|
data:"用户名:${userProvider.username} ",
|
||||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
gradientLayers: [
|
gradientLayers: [
|
||||||
GradientLayer(
|
GradientLayer(
|
||||||
@@ -349,7 +472,19 @@ class _UserInfoCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
GradientText(
|
||||||
|
data:"Ra${userProvider.user?.rating} ",
|
||||||
|
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
|
||||||
|
gradientLayers: [
|
||||||
|
GradientLayer(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Colors.deepPurple, Colors.pinkAccent],
|
||||||
|
),
|
||||||
|
blendMode: BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 5, thickness: 1),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
userProvider.username == "未登录"
|
userProvider.username == "未登录"
|
||||||
@@ -374,6 +509,270 @@ class _UserInfoCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====================== 🎵 修复完成:推荐乐曲组件 ======================
|
||||||
|
class _RecommendedSongsSection extends StatelessWidget {
|
||||||
|
const _RecommendedSongsSection();
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> _loadData(UserProvider userProvider) async {
|
||||||
|
// 加载歌曲 + 用户数据
|
||||||
|
final allSongs = await SongService.getAllSongs();
|
||||||
|
List<dynamic> userMusicList = [];
|
||||||
|
final token = userProvider.token;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final scoreData = await SongService.getUserAllScores(
|
||||||
|
token!,
|
||||||
|
name: userProvider.selectedCnUserName,
|
||||||
|
);
|
||||||
|
if (scoreData.containsKey('userScoreAll_')) {
|
||||||
|
userMusicList = scoreData['userScoreAll_']['userMusicList'] ?? [];
|
||||||
|
} else if (scoreData.containsKey('userMusicList')) {
|
||||||
|
userMusicList = scoreData['userMusicList'] ?? [];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
final int currentRating = userProvider.user?.rating ?? 0;
|
||||||
|
final int estimatedB35Min = (currentRating / 50).toInt();
|
||||||
|
|
||||||
|
final recommended = RecommendationHelper.getSmartRecommendations(
|
||||||
|
allSongs: allSongs,
|
||||||
|
userMusicList: userMusicList,
|
||||||
|
userRating: currentRating, // 传入非空 int
|
||||||
|
b35MinRating: estimatedB35Min, // 传入非空 int
|
||||||
|
count: 6,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
'songs': recommended,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final userProvider = context.read<UserProvider>();
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.music_note, color: Colors.purpleAccent),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
"为你推荐",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
FutureBuilder<Map<String, dynamic>>(
|
||||||
|
future: _loadData(userProvider),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
if (snapshot.hasError || !snapshot.hasData) {
|
||||||
|
return const Center(child: Text("加载推荐失败"));
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<SongModel> songs = snapshot.data!['songs'] ?? [];
|
||||||
|
if (songs.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(20.0),
|
||||||
|
child: Text("暂无推荐歌曲,快去游玩更多曲目吧!"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: songs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _SongItemCard(song: songs[index]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SongItemCard extends StatelessWidget {
|
||||||
|
final SongModel song;
|
||||||
|
|
||||||
|
const _SongItemCard({required this.song});
|
||||||
|
|
||||||
|
String _getCoverUrl(int musicId) {
|
||||||
|
int displayId = musicId % 10000;
|
||||||
|
// 注意:这里逻辑可能需要根据你的实际资源调整,通常 DX 歌曲 ID > 10000
|
||||||
|
if (musicId >= 10000) {
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取难度颜色
|
||||||
|
Color _getLevelColor(int levelIndex) {
|
||||||
|
switch (levelIndex) {
|
||||||
|
case 0: return Colors.green; // Basic
|
||||||
|
case 1: return Colors.blue; // Advanced
|
||||||
|
case 2: return Colors.yellow[700]!; // Expert
|
||||||
|
case 3: return Colors.red; // Master
|
||||||
|
case 4: return Colors.purple; // Re:Master
|
||||||
|
default: return Colors.grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// 假设我们要显示 Master (3) 和 Re:Master (4) 的难度
|
||||||
|
// 你需要从 song.sd 或 song.dx 中解析出具体的 level_value (定数)
|
||||||
|
double? masterLv = _getLevelValue(song, 3);
|
||||||
|
double? reMasterLv = _getLevelValue(song, 4);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: 140,
|
||||||
|
margin: const EdgeInsets.only(right: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.15),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// ✅ 1. 图片优化:增加 loadingBuilder 和 cacheWidth
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 120,
|
||||||
|
width: double.infinity,
|
||||||
|
child: Image.network(
|
||||||
|
_getCoverUrl(song.id),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
// 关键优化:指定缓存宽度,减少内存占用和解码时间
|
||||||
|
cacheWidth: 280,
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
child: Center(child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
value: loadingProgress.expectedTotalBytes != null
|
||||||
|
? loadingProgress.cumulativeBytesLoaded /
|
||||||
|
loadingProgress.expectedTotalBytes!
|
||||||
|
: null,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorBuilder: (_, __, ___) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
child: const Icon(Icons.music_note, size: 40, color: Colors.grey),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Text(
|
||||||
|
song.title ?? "未知歌曲",
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 艺术家 (修复了重复显示的问题)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Text(
|
||||||
|
song.artist ?? "未知艺术家",
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
|
// ✅ 2. 底部难度标签
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (masterLv != null)
|
||||||
|
_buildLevelTag("MAS", masterLv, Colors.purple),
|
||||||
|
if (reMasterLv != null) ...[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
_buildLevelTag("ReM", reMasterLv, Colors.deepPurple),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLevelTag(String prefix, double level, Color color) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: color.withOpacity(0.5), width: 0.5),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"$prefix ${level.toStringAsFixed(1)}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法:获取定数
|
||||||
|
double? _getLevelValue(SongModel song, int levelIndex) {
|
||||||
|
Map ?diffMap = song.dx;
|
||||||
|
if (diffMap==null|| diffMap.isEmpty) {
|
||||||
|
diffMap = song.sd;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = diffMap?["$levelIndex"] ?? diffMap?[levelIndex];
|
||||||
|
if (data is Map && data["level_value"] != null) {
|
||||||
|
return (data["level_value"] as num).toDouble();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PosterImage extends StatelessWidget {
|
class PosterImage extends StatelessWidget {
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
|
|
||||||
@@ -385,7 +784,6 @@ class PosterImage extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
maxWidth: 800,
|
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -426,5 +824,4 @@ class PosterImage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -8,6 +7,7 @@ import 'package:dio/dio.dart';
|
|||||||
|
|
||||||
import '../../model/user_model.dart';
|
import '../../model/user_model.dart';
|
||||||
import '../../providers/user_provider.dart';
|
import '../../providers/user_provider.dart';
|
||||||
|
import '../../service/sega_service.dart';
|
||||||
import 'login_page.dart';
|
import 'login_page.dart';
|
||||||
|
|
||||||
class UserPage extends StatefulWidget {
|
class UserPage extends StatefulWidget {
|
||||||
@@ -27,12 +27,10 @@ class _UserPageState extends State<UserPage> {
|
|||||||
final _lxKeyController = TextEditingController();
|
final _lxKeyController = TextEditingController();
|
||||||
static const _pinkColor = Color(0xFFFFC0D6);
|
static const _pinkColor = Color(0xFFFFC0D6);
|
||||||
|
|
||||||
// 开关状态
|
|
||||||
bool _isDisagreeRecommend = false;
|
bool _isDisagreeRecommend = false;
|
||||||
bool _isDisagreeFriend = false;
|
bool _isDisagreeFriend = false;
|
||||||
Map<String, dynamic>? _radarData;
|
Map<String, dynamic>? _radarData;
|
||||||
|
|
||||||
// 获取雷达图方法
|
|
||||||
Future<void> _loadRadarData() async {
|
Future<void> _loadRadarData() async {
|
||||||
final provider = Provider.of<UserProvider>(context, listen: false);
|
final provider = Provider.of<UserProvider>(context, listen: false);
|
||||||
try {
|
try {
|
||||||
@@ -53,10 +51,11 @@ class _UserPageState extends State<UserPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_loadRadarData;
|
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_loadRadarData();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final provider = Provider.of<UserProvider>(context, listen: false);
|
final provider = Provider.of<UserProvider>(context, listen: false);
|
||||||
provider.fetchSexTags();
|
provider.fetchSexTags();
|
||||||
@@ -75,7 +74,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
_dfPwdController.text = user?.dfPassword ?? '';
|
_dfPwdController.text = user?.dfPassword ?? '';
|
||||||
_lxKeyController.text = user?.lxKey ?? '';
|
_lxKeyController.text = user?.lxKey ?? '';
|
||||||
|
|
||||||
// 初始化开关
|
|
||||||
_isDisagreeRecommend = user?.isDisagreeRecommend ?? false;
|
_isDisagreeRecommend = user?.isDisagreeRecommend ?? false;
|
||||||
_isDisagreeFriend = user?.isDisagreeFriend ?? false;
|
_isDisagreeFriend = user?.isDisagreeFriend ?? false;
|
||||||
}
|
}
|
||||||
@@ -92,7 +90,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 添加打开链接的方法
|
|
||||||
Future<void> _launchURL(String urlString) async {
|
Future<void> _launchURL(String urlString) async {
|
||||||
final Uri url = Uri.parse(urlString);
|
final Uri url = Uri.parse(urlString);
|
||||||
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
|
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
|
||||||
@@ -108,7 +105,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
final username = _dfUserController.text.trim();
|
final username = _dfUserController.text.trim();
|
||||||
final password = _dfPwdController.text.trim();
|
final password = _dfPwdController.text.trim();
|
||||||
|
|
||||||
// 1. 判空校验
|
|
||||||
if (username.isEmpty || password.isEmpty) {
|
if (username.isEmpty || password.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text("请输入水鱼用户名和密码")),
|
const SnackBar(content: Text("请输入水鱼用户名和密码")),
|
||||||
@@ -116,7 +112,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 显示加载提示
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
@@ -127,7 +122,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 3. 创建Dio实例,发送POST请求
|
|
||||||
final dio = Dio();
|
final dio = Dio();
|
||||||
final response = await dio.post(
|
final response = await dio.post(
|
||||||
'https://maimai.diving-fish.com/api/maimaidxprober/login',
|
'https://maimai.diving-fish.com/api/maimaidxprober/login',
|
||||||
@@ -135,18 +129,15 @@ class _UserPageState extends State<UserPage> {
|
|||||||
"username": username,
|
"username": username,
|
||||||
"password": password,
|
"password": password,
|
||||||
},
|
},
|
||||||
// 可选:设置请求超时
|
|
||||||
options: Options(
|
options: Options(
|
||||||
sendTimeout: const Duration(seconds: 10),
|
sendTimeout: const Duration(seconds: 10),
|
||||||
receiveTimeout: const Duration(seconds: 10),
|
receiveTimeout: const Duration(seconds: 10),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. 解析返回结果
|
|
||||||
final Map<String, dynamic> result = response.data;
|
final Map<String, dynamic> result = response.data;
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
// 登录成功
|
|
||||||
if (result['message'] == "登录成功") {
|
if (result['message'] == "登录成功") {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
@@ -155,9 +146,7 @@ class _UserPageState extends State<UserPage> {
|
|||||||
duration: Duration(seconds: 1),
|
duration: Duration(seconds: 1),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// TODO: 验证成功后的逻辑(保存信息、跳转页面等)
|
|
||||||
} else {
|
} else {
|
||||||
// 账号密码错误
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(result['message'] ?? "验证失败"),
|
content: Text(result['message'] ?? "验证失败"),
|
||||||
@@ -168,7 +157,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
// 5. Dio网络错误处理(无网络、超时、服务器异常)
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
String errorMsg = "网络请求失败";
|
String errorMsg = "网络请求失败";
|
||||||
if (e.type == DioExceptionType.connectionTimeout) {
|
if (e.type == DioExceptionType.connectionTimeout) {
|
||||||
@@ -187,14 +175,12 @@ class _UserPageState extends State<UserPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 其他未知错误
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text("未知错误,请重试"),
|
content: Text("未知错误,请重试"),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
duration: Duration(seconds: 1),
|
duration: Duration(seconds: 1),
|
||||||
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -213,8 +199,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final dio = Dio();
|
final dio = Dio();
|
||||||
|
|
||||||
// 发起请求
|
|
||||||
final response = await dio.get(
|
final response = await dio.get(
|
||||||
'https://maimai.lxns.net/api/v0/user/maimai/player',
|
'https://maimai.lxns.net/api/v0/user/maimai/player',
|
||||||
options: Options(
|
options: Options(
|
||||||
@@ -224,7 +208,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ✅ 关键:解析外层响应 + 内层 data
|
|
||||||
final result = LuoxueResponse.fromJson(response.data);
|
final result = LuoxueResponse.fromJson(response.data);
|
||||||
|
|
||||||
if (result.success && mounted) {
|
if (result.success && mounted) {
|
||||||
@@ -233,12 +216,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
content: Text("✅ 验证成功!玩家:${result.data.name}\nDX Rating:${result.data.rating}"),
|
content: Text("✅ 验证成功!玩家:${result.data.name}\nDX Rating:${result.data.rating}"),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 在这里可以使用完整的玩家数据
|
|
||||||
// result.data.name
|
|
||||||
// result.data.rating
|
|
||||||
// result.data.friendCode
|
|
||||||
// result.data.trophy.name
|
|
||||||
}
|
}
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
String msg = "验证失败";
|
String msg = "验证失败";
|
||||||
@@ -303,6 +280,288 @@ class _UserPageState extends State<UserPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showSyncProgressDialog(BuildContext context, ValueNotifier<String> status) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
content: ValueListenableBuilder<String>(
|
||||||
|
valueListenable: status,
|
||||||
|
builder: (_, text, __) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
Expanded(child: Text(text)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _verifyBoundSega(SegaCard card) async {
|
||||||
|
final segaId = card.segaId;
|
||||||
|
final pwd = card.password;
|
||||||
|
final type = card.type;
|
||||||
|
|
||||||
|
if (segaId == null || pwd == null || type == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("账号信息不完整")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("正在验证 SegaID…")));
|
||||||
|
|
||||||
|
final region = type == "jp" ? Region.jp : Region.intl;
|
||||||
|
final data = await SegaService.verifyOnlyLogin(
|
||||||
|
region: region,
|
||||||
|
segaId: segaId,
|
||||||
|
password: pwd,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("✅ 验证成功:${data['user']['name']}"), backgroundColor: Colors.green),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} on NetImportError catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("❌ ${e.message}"), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("验证失败")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshBoundSegaScore(SegaCard card) async {
|
||||||
|
final segaId = card.segaId;
|
||||||
|
final pwd = card.password;
|
||||||
|
final type = card.type;
|
||||||
|
|
||||||
|
if (segaId == null || pwd == null || type == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("账号信息不完整")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final statusNotifier = ValueNotifier<String>("准备同步成绩...");
|
||||||
|
if (mounted) _showSyncProgressDialog(context, statusNotifier);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final region = type == "jp" ? Region.jp : Region.intl;
|
||||||
|
final data = await SegaService.fetchAndSync(
|
||||||
|
region: region,
|
||||||
|
segaId: segaId,
|
||||||
|
password: pwd,
|
||||||
|
onProgress: (status, percent) {
|
||||||
|
statusNotifier.value = status;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 同步到后端
|
||||||
|
final provider = Provider.of<UserProvider>(context, listen: false);
|
||||||
|
await provider.syncSegaScore(data);
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
if (mounted) Navigator.pop(context);
|
||||||
|
|
||||||
|
// ✅【正确提示:上传成功】
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("✅ 成绩同步成功(已上传至服务器)"),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
} on NetImportError catch (e) {
|
||||||
|
if (mounted) Navigator.pop(context);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("❌ ${e.message}"), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) Navigator.pop(context);
|
||||||
|
if (mounted) {
|
||||||
|
// ❌【上传失败提示】
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("❌ 成绩同步失败(服务器上传失败)"),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showAddSegaSheet() {
|
||||||
|
final idController = TextEditingController();
|
||||||
|
final pwdController = TextEditingController();
|
||||||
|
Region selectedRegion = Region.intl;
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
|
||||||
|
builder: (ctx) => Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(20, 20, 20, MediaQuery.of(ctx).viewInsets.bottom + 20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text("添加 Sega 账号", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: idController,
|
||||||
|
decoration: const InputDecoration(labelText: "SegaID", isDense: true, border: OutlineInputBorder()),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: pwdController,
|
||||||
|
obscureText: true,
|
||||||
|
decoration: const InputDecoration(labelText: "密码", isDense: true, border: OutlineInputBorder()),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
DropdownButtonFormField<Region>(
|
||||||
|
value: selectedRegion,
|
||||||
|
decoration: const InputDecoration(border: OutlineInputBorder(), isDense: true),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: Region.intl, child: Text("国际服")),
|
||||||
|
DropdownMenuItem(value: Region.jp, child: Text("日服")),
|
||||||
|
],
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v != null) selectedRegion = v;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final id = idController.text.trim();
|
||||||
|
final pwd = pwdController.text.trim();
|
||||||
|
if (id.isEmpty || pwd.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("请输入完整信息")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final newCard = SegaCard(
|
||||||
|
segaId: id,
|
||||||
|
password: pwd,
|
||||||
|
type: selectedRegion.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
final provider = Provider.of<UserProvider>(context, listen: false);
|
||||||
|
final user = provider.user;
|
||||||
|
if (user != null) {
|
||||||
|
final list = user.segaCards ?? [];
|
||||||
|
final newList = List<SegaCard>.from(list)..add(newCard);
|
||||||
|
|
||||||
|
final updatedUser = UserModel(
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
userId: user.userId,
|
||||||
|
teamId: user.teamId,
|
||||||
|
email: user.email,
|
||||||
|
password: user.password,
|
||||||
|
twoFactorKey: user.twoFactorKey,
|
||||||
|
apiKey: user.apiKey,
|
||||||
|
apiBindKey: user.apiBindKey,
|
||||||
|
protectRole: user.protectRole,
|
||||||
|
risks: user.risks,
|
||||||
|
mcName: user.mcName,
|
||||||
|
userName2userId: user.userName2userId,
|
||||||
|
lxKey: user.lxKey,
|
||||||
|
dfUsername: user.dfUsername,
|
||||||
|
dfPassword: user.dfPassword,
|
||||||
|
nuoId: user.nuoId,
|
||||||
|
botId: user.botId,
|
||||||
|
spasolBotId: user.spasolBotId,
|
||||||
|
githubId: user.githubId,
|
||||||
|
rating: user.rating,
|
||||||
|
ratingMax: user.ratingMax,
|
||||||
|
iconId: user.iconId,
|
||||||
|
plateId: user.plateId,
|
||||||
|
plateIds: user.plateIds,
|
||||||
|
frameId: user.frameId,
|
||||||
|
charaSlots: user.charaSlots,
|
||||||
|
qiandaoDay: user.qiandaoDay,
|
||||||
|
inviter: user.inviter,
|
||||||
|
successLogoutTime: user.successLogoutTime,
|
||||||
|
lastLoginTime: user.lastLoginTime,
|
||||||
|
friendIds: user.friendIds,
|
||||||
|
bio: user.bio,
|
||||||
|
friendBio: user.friendBio,
|
||||||
|
sex: user.sex,
|
||||||
|
isDisagreeRecommend: user.isDisagreeRecommend,
|
||||||
|
isDisagreeFriend: user.isDisagreeFriend,
|
||||||
|
points: user.points,
|
||||||
|
planPoints: user.planPoints,
|
||||||
|
cardIds: user.cardIds,
|
||||||
|
userCards: user.userCards,
|
||||||
|
tags: user.tags,
|
||||||
|
useBeta: user.useBeta,
|
||||||
|
useNuo: user.useNuo,
|
||||||
|
useServer: user.useServer,
|
||||||
|
useB50Type: user.useB50Type,
|
||||||
|
userHot: user.userHot,
|
||||||
|
chatInGroupNumbers: user.chatInGroupNumbers,
|
||||||
|
sc: user.sc,
|
||||||
|
id2pcNuo: user.id2pcNuo,
|
||||||
|
mai2links: user.mai2links,
|
||||||
|
key2KeychipEn: user.key2KeychipEn,
|
||||||
|
key2key2KeychipEn: user.key2key2KeychipEn,
|
||||||
|
mai2link: user.mai2link,
|
||||||
|
userRegion: user.userRegion,
|
||||||
|
rinUsernameOrEmail: user.rinUsernameOrEmail,
|
||||||
|
rinPassword: user.rinPassword,
|
||||||
|
rinChusanUser: user.rinChusanUser,
|
||||||
|
segaCards: newList,
|
||||||
|
placeList: user.placeList,
|
||||||
|
lastKeyChip: user.lastKeyChip,
|
||||||
|
token: user.token,
|
||||||
|
timesRegionData: user.timesRegionData,
|
||||||
|
yearTotal: user.yearTotal,
|
||||||
|
yearTotalComment: user.yearTotalComment,
|
||||||
|
userCollCardMap: user.userCollCardMap,
|
||||||
|
collName2musicIds: user.collName2musicIds,
|
||||||
|
ai: user.ai,
|
||||||
|
pkScore: user.pkScore,
|
||||||
|
pkScoreStr: user.pkScoreStr,
|
||||||
|
pkScoreReality: user.pkScoreReality,
|
||||||
|
pkUserId: user.pkUserId,
|
||||||
|
limitPkTimestamp: user.limitPkTimestamp,
|
||||||
|
hasAcceptPk: user.hasAcceptPk,
|
||||||
|
pkPlayNum: user.pkPlayNum,
|
||||||
|
pkWin: user.pkWin,
|
||||||
|
userData: user.userData,
|
||||||
|
banState: user.banState,
|
||||||
|
);
|
||||||
|
|
||||||
|
provider.updateUser(updatedUser);
|
||||||
|
await provider.saveUserInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("✅ 添加成功")));
|
||||||
|
Navigator.pop(ctx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text("保存"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final userProvider = Provider.of<UserProvider>(context);
|
final userProvider = Provider.of<UserProvider>(context);
|
||||||
@@ -312,11 +571,8 @@ class _UserPageState extends State<UserPage> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("用户中心")),
|
appBar: AppBar(title: const Text("用户中心")),
|
||||||
body: Center(
|
body: Center(
|
||||||
|
|
||||||
child: Transform.translate(
|
child: Transform.translate(
|
||||||
// Offset(水平, 垂直)
|
offset: const Offset(0, -40),
|
||||||
// 垂直向上:负值
|
|
||||||
offset: const Offset(0, -40), // 往上移 10 像素
|
|
||||||
child: _buildLoginCard(
|
child: _buildLoginCard(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
@@ -379,14 +635,10 @@ class _UserPageState extends State<UserPage> {
|
|||||||
_buildRadarChartSection(),
|
_buildRadarChartSection(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_buildSaveButton(userProvider),
|
_buildSaveButton(userProvider),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildLogoutButton(context, userProvider),
|
_buildLogoutButton(context, userProvider),
|
||||||
const SizedBox(height: 100),
|
const SizedBox(height: 100),
|
||||||
|
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -396,7 +648,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 修改 _buildScoreCheckerCard以添加按钮
|
|
||||||
Widget _buildScoreCheckerCard(UserModel user) {
|
Widget _buildScoreCheckerCard(UserModel user) {
|
||||||
return _webCard(
|
return _webCard(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -408,7 +659,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// --- 水鱼部分 ---
|
|
||||||
const Text("水鱼查分器", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
|
const Text("水鱼查分器", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextField(
|
TextField(
|
||||||
@@ -430,7 +680,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// 水鱼操作按钮行
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
@@ -443,14 +692,13 @@ class _UserPageState extends State<UserPage> {
|
|||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.open_in_new, size: 18),
|
icon: const Icon(Icons.open_in_new, size: 18),
|
||||||
label: const Text("跳转水鱼官网"),
|
label: const Text("跳转水鱼官网"),
|
||||||
onPressed: () => _launchURL("https://www.diving-fish.com/"), // 替换为实际水鱼官网地址
|
onPressed: () => _launchURL("https://www.diving-fish.com/"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// --- 落雪部分 ---
|
|
||||||
const Text("落雪查分器", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
|
const Text("落雪查分器", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextField(
|
TextField(
|
||||||
@@ -462,7 +710,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// 落雪操作按钮行
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
@@ -475,7 +722,7 @@ class _UserPageState extends State<UserPage> {
|
|||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.open_in_new, size: 18),
|
icon: const Icon(Icons.open_in_new, size: 18),
|
||||||
label: const Text("跳转落雪官网"),
|
label: const Text("跳转落雪官网"),
|
||||||
onPressed: () => _launchURL("https://maimai.lxns.net/"), // 替换为实际落雪官网地址
|
onPressed: () => _launchURL("https://maimai.lxns.net/"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -525,9 +772,9 @@ class _UserPageState extends State<UserPage> {
|
|||||||
color: Colors.pinkAccent,
|
color: Colors.pinkAccent,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: const Text(
|
||||||
"舞萌账号" ,
|
"舞萌账号",
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
style: TextStyle(color: Colors.white, fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
@@ -535,7 +782,7 @@ class _UserPageState extends State<UserPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -712,7 +959,7 @@ class _UserPageState extends State<UserPage> {
|
|||||||
image: NetworkImage(
|
image: NetworkImage(
|
||||||
"https://cdn.godserver.cn/resource/static/coll/Chara/UI_Chara_$charaId.png",
|
"https://cdn.godserver.cn/resource/static/coll/Chara/UI_Chara_$charaId.png",
|
||||||
),
|
),
|
||||||
fit: BoxFit.cover,
|
fit:BoxFit.cover,
|
||||||
colorFilter: ColorFilter.mode(
|
colorFilter: ColorFilter.mode(
|
||||||
Colors.black.withOpacity(0.4), BlendMode.darken),
|
Colors.black.withOpacity(0.4), BlendMode.darken),
|
||||||
),
|
),
|
||||||
@@ -799,7 +1046,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
friendBio: user.friendBio,
|
friendBio: user.friendBio,
|
||||||
sex: _sexController.text.trim(),
|
sex: _sexController.text.trim(),
|
||||||
|
|
||||||
// 开关已正确保存
|
|
||||||
isDisagreeRecommend: _isDisagreeRecommend,
|
isDisagreeRecommend: _isDisagreeRecommend,
|
||||||
isDisagreeFriend: _isDisagreeFriend,
|
isDisagreeFriend: _isDisagreeFriend,
|
||||||
|
|
||||||
@@ -876,13 +1122,16 @@ class _UserPageState extends State<UserPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text("SegaID 账号",
|
const Text(
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
"SegaID 账号",
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
if (user.segaCards != null && user.segaCards!.isNotEmpty)
|
if (user.segaCards != null && user.segaCards!.isNotEmpty)
|
||||||
...user.segaCards!.map((card) {
|
...user.segaCards!.map((SegaCard card) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
@@ -897,13 +1146,38 @@ class _UserPageState extends State<UserPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(card.segaId ?? ""),
|
Expanded(child: Text(card.segaId ?? "")),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => _verifyBoundSega(card),
|
||||||
|
child: const Text("验证", style: TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => _refreshBoundSegaScore(card),
|
||||||
|
child: const Text("刷新成绩", style: TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}).toList(),
|
||||||
|
|
||||||
if (user.segaCards == null || user.segaCards!.isEmpty)
|
if (user.segaCards == null || user.segaCards!.isEmpty)
|
||||||
const Text("暂无 SegaID"),
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text("暂无 SegaID"),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: _showAddSegaSheet,
|
||||||
|
icon: const Icon(Icons.add, size: 18),
|
||||||
|
label: const Text("添加 Sega 账号"),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1055,7 +1329,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// 1. 头部图标与标题
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -1089,7 +1362,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// 2. 登录好处列表 (Value Proposition)
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -1125,7 +1397,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// 3. 登录按钮
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 50,
|
height: 50,
|
||||||
@@ -1159,7 +1430,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助方法:构建单个权益项
|
|
||||||
Widget _buildBenefitItem({
|
Widget _buildBenefitItem({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String title,
|
required String title,
|
||||||
@@ -1212,17 +1482,15 @@ class _UserPageState extends State<UserPage> {
|
|||||||
child: const Text("点击加载雷达图数据"),
|
child: const Text("点击加载雷达图数据"),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
// 使用我们自定义的雷达图
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 300,
|
height: 300,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: CustomRadarChart(
|
child: CustomRadarChart(
|
||||||
// 传入你的数据,确保 Map<String, double>
|
|
||||||
data: _radarData!.map((key, value) =>
|
data: _radarData!.map((key, value) =>
|
||||||
MapEntry(key.toString(), double.tryParse(value.toString()) ?? 0.0)
|
MapEntry(key.toString(), double.tryParse(value.toString()) ?? 0.0)
|
||||||
),
|
),
|
||||||
maxValue: 1.3, // 强制最大值为 1.5,不再自动缩放
|
maxValue: 1.3,
|
||||||
lineColor: Colors.grey.shade200, // 极淡的网格线,视觉上接近“无”
|
lineColor: Colors.grey.shade200,
|
||||||
areaColor: Colors.pink.withOpacity(0.15),
|
areaColor: Colors.pink.withOpacity(0.15),
|
||||||
borderColor: Colors.pinkAccent,
|
borderColor: Colors.pinkAccent,
|
||||||
),
|
),
|
||||||
@@ -1230,9 +1498,6 @@ class _UserPageState extends State<UserPage> {
|
|||||||
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// ————————————————————————————————
|
|
||||||
// 下面的列表保持不变
|
|
||||||
// ————————————————————————————————
|
|
||||||
if (_radarData != null)
|
if (_radarData != null)
|
||||||
GridView.count(
|
GridView.count(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
@@ -1260,12 +1525,11 @@ class _UserPageState extends State<UserPage> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
key,
|
key,
|
||||||
style: const TextStyle(fontSize: 12,
|
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||||
fontWeight: FontWeight.w500),
|
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
value.toStringAsFixed(2),
|
value.toStringAsFixed(2),
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -193,4 +193,32 @@ class UserProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> syncSegaScore(Map<String, dynamic> data) async {
|
||||||
|
if (_token == null || _user == null) {
|
||||||
|
throw "请先登录";
|
||||||
|
}
|
||||||
|
if (_selectedSegaId == null || _selectedSegaId!.isEmpty) {
|
||||||
|
throw "请先选择要同步的 SegaID";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用上传
|
||||||
|
final result = await UserService.uploadSegaRating(
|
||||||
|
_token!,
|
||||||
|
_selectedSegaId!,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result["code"] == 200) {
|
||||||
|
print("✅ 同步成功:${result["msg"]}");
|
||||||
|
} else {
|
||||||
|
print("❌ 同步失败:${result["msg"]}");
|
||||||
|
throw result["msg"] ?? "同步失败";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("❌ 同步异常:$e");
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import '../model/song_model.dart';
|
||||||
|
|
||||||
|
class RecommendationHelper {
|
||||||
|
|
||||||
|
/// 基于 Java MusicService 逻辑的智能推荐
|
||||||
|
///
|
||||||
|
/// [allSongs]: 全量歌曲库 (SongModel)
|
||||||
|
/// [userMusicList]: 用户成绩列表 (dynamic)
|
||||||
|
/// - 情况A: List<Map>,每个 Map 包含 musicId, level, achievement, dx_rating
|
||||||
|
/// - 情况B: List<Map>,每个 Map 包含 userMusicDetailList (嵌套结构)
|
||||||
|
/// [userRating]: 用户当前总 Rating (DX Rating),用于确定推荐难度区间
|
||||||
|
/// [b35MinRating]: 用户当前 B35 中最低的那首曲子的 Rating (用于判断推分是否有意义)
|
||||||
|
/// [count]: 推荐数量
|
||||||
|
static List<SongModel> getSmartRecommendations({
|
||||||
|
required List<SongModel> allSongs,
|
||||||
|
required List<dynamic> userMusicList,
|
||||||
|
required int userRating,
|
||||||
|
required int b35MinRating,
|
||||||
|
int count = 6,
|
||||||
|
}) {
|
||||||
|
if (allSongs.isEmpty) return [];
|
||||||
|
|
||||||
|
final random = Random();
|
||||||
|
|
||||||
|
// 1. 解析用户成绩,构建映射: Key = "musicId_level", Value = { achievement, rating }
|
||||||
|
// 使用 HashMap 提高查找速度
|
||||||
|
final Map<String, Map<String, dynamic>> playedMap = {};
|
||||||
|
|
||||||
|
for (var group in userMusicList) {
|
||||||
|
if (group is! Map) continue;
|
||||||
|
|
||||||
|
List<dynamic> details = [];
|
||||||
|
|
||||||
|
// 兼容两种数据结构:
|
||||||
|
if (group.containsKey('userMusicDetailList')) {
|
||||||
|
details = group['userMusicDetailList'] as List? ?? [];
|
||||||
|
} else if (group.containsKey('achievement') || group.containsKey('dx_rating')) {
|
||||||
|
// 如果当前 group 本身就是成绩对象
|
||||||
|
details = [group];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.isEmpty) continue;
|
||||||
|
|
||||||
|
for (var detail in details) {
|
||||||
|
if (detail is! Map) continue;
|
||||||
|
|
||||||
|
// 提取关键字段
|
||||||
|
final int musicId = detail['musicId'] ?? detail['id'] ?? 0;
|
||||||
|
if(musicId>16000) continue;
|
||||||
|
final int level = detail['level'] ?? detail['levelIndex'] ?? 3; // 默认 Master
|
||||||
|
final int achievement = detail['achievement'] ?? 0;
|
||||||
|
// 确保 rating 是 double
|
||||||
|
final dynamic rawRating = detail['dx_rating'] ?? detail['rating'] ?? 0;
|
||||||
|
final double rating = rawRating is num ? rawRating.toDouble() : 0.0;
|
||||||
|
|
||||||
|
if (musicId == 0) continue;
|
||||||
|
|
||||||
|
String key = "${musicId}_${level}";
|
||||||
|
|
||||||
|
// 只保留该难度下的最高成绩
|
||||||
|
if (!playedMap.containsKey(key) || achievement > (playedMap[key]!['achievement'] as int)) {
|
||||||
|
playedMap[key] = {
|
||||||
|
'musicId': musicId,
|
||||||
|
'level': level,
|
||||||
|
'achievement': achievement,
|
||||||
|
'rating': rating,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 计算推荐难度区间 (基于 Java logic)
|
||||||
|
double minRecommendedLevel = 0.0;
|
||||||
|
|
||||||
|
if (userRating >= 15500) {
|
||||||
|
minRecommendedLevel = 13.6;
|
||||||
|
} else if (userRating >= 15000) {
|
||||||
|
minRecommendedLevel = 13.0;
|
||||||
|
} else if (userRating >= 14500) {
|
||||||
|
minRecommendedLevel = 12.5;
|
||||||
|
} else if (userRating >= 14000) {
|
||||||
|
minRecommendedLevel = 12.0;
|
||||||
|
} else if (userRating >= 13000) {
|
||||||
|
minRecommendedLevel = 11.5;
|
||||||
|
} else if (userRating >= 12000) {
|
||||||
|
minRecommendedLevel = 11.0;
|
||||||
|
} else if (userRating >= 11000) {
|
||||||
|
minRecommendedLevel = 10.5;
|
||||||
|
} else {
|
||||||
|
minRecommendedLevel = 9.0; // 新手保护线
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SongModel> candidatesForImprovement = []; // 用于推分
|
||||||
|
List<SongModel> candidatesNew = []; // 新曲/未玩
|
||||||
|
|
||||||
|
// 用于快速判断是否已加入结果集,避免 O(N^2) 的 contains 检查
|
||||||
|
final Set<int> addedSongIds = {};
|
||||||
|
|
||||||
|
for (var song in allSongs) {
|
||||||
|
// 过滤无效 ID
|
||||||
|
if (song.id < 100) continue;
|
||||||
|
|
||||||
|
// 获取 Master (Level 3) 的定数,如果没有则获取 Expert (Level 2)
|
||||||
|
double? masterLevel = _getSongLevel(song, 3);
|
||||||
|
double? expertLevel = _getSongLevel(song, 2);
|
||||||
|
|
||||||
|
double? targetLevel;
|
||||||
|
int targetLevelIndex = 3; // 默认优先推荐 Master
|
||||||
|
|
||||||
|
if (masterLevel != null) {
|
||||||
|
targetLevel = masterLevel;
|
||||||
|
} else if (expertLevel != null) {
|
||||||
|
targetLevel = expertLevel;
|
||||||
|
targetLevelIndex = 2;
|
||||||
|
} else {
|
||||||
|
continue; // 没有可用难度数据
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 检查是否已游玩 ---
|
||||||
|
bool isPlayed = false;
|
||||||
|
Map<String, dynamic>? bestChart;
|
||||||
|
|
||||||
|
// 尝试直接匹配 (SD 或 基础ID)
|
||||||
|
String keyDirect = "${song.id}_$targetLevelIndex";
|
||||||
|
if (playedMap.containsKey(keyDirect)) {
|
||||||
|
isPlayed = true;
|
||||||
|
bestChart = playedMap[keyDirect];
|
||||||
|
}
|
||||||
|
// 尝试匹配 DX 偏移 (DX 歌曲 ID 通常 +10000)
|
||||||
|
else {
|
||||||
|
int dxId = song.id + 10000;
|
||||||
|
String keyDx = "${dxId}_$targetLevelIndex";
|
||||||
|
if (playedMap.containsKey(keyDx)) {
|
||||||
|
isPlayed = true;
|
||||||
|
bestChart = playedMap[keyDx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlayed && bestChart != null) {
|
||||||
|
// --- 策略 A: 推分逻辑 (Improvement) ---
|
||||||
|
int currentAch = bestChart['achievement'] as int;
|
||||||
|
|
||||||
|
// 如果已经 AP+ (100.5%),跳过
|
||||||
|
if (currentAch >= 1005000) continue;
|
||||||
|
|
||||||
|
// 计算下一个档位的目标达成率
|
||||||
|
double nextTargetAch = 0;
|
||||||
|
const List<double> targets = [97.0, 98.0, 99.0, 99.5, 100.0, 100.5];
|
||||||
|
|
||||||
|
double currentPercent = currentAch / 10000.0;
|
||||||
|
for (double t in targets) {
|
||||||
|
if (currentPercent < t) {
|
||||||
|
nextTargetAch = t;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextTargetAch == 0) continue;
|
||||||
|
|
||||||
|
// 计算目标 Rating
|
||||||
|
int targetRating = _calculateRating(targetLevel!, nextTargetAch);
|
||||||
|
|
||||||
|
// 核心判断:如果推分后的 Rating 大于当前 B35 最低分,则值得推荐
|
||||||
|
if (targetRating > b35MinRating) {
|
||||||
|
candidatesForImprovement.add(song);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// --- 策略 B: 新曲逻辑 (New) ---
|
||||||
|
// 判断定数是否在用户能力范围内
|
||||||
|
if (targetLevel != null && targetLevel >= minRecommendedLevel && targetLevel <= 15.0) {
|
||||||
|
candidatesNew.add(song);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 混合推荐结果
|
||||||
|
List<SongModel> result = [];
|
||||||
|
|
||||||
|
// 60% 来自“提升空间大”的曲子
|
||||||
|
candidatesForImprovement.shuffle(random);
|
||||||
|
int improveCount = (count * 0.6).round();
|
||||||
|
for (int i = 0; i < improveCount && i < candidatesForImprovement.length; i++) {
|
||||||
|
final song = candidatesForImprovement[i];
|
||||||
|
result.add(song);
|
||||||
|
addedSongIds.add(song.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 40% 来自“新曲/潜力曲”
|
||||||
|
candidatesNew.shuffle(random);
|
||||||
|
int remaining = count - result.length;
|
||||||
|
for (int i = 0; i < remaining && i < candidatesNew.length; i++) {
|
||||||
|
final song = candidatesNew[i];
|
||||||
|
// 避免重复添加(虽然逻辑上 New 和 Improve 不重叠,但防万一)
|
||||||
|
if (!addedSongIds.contains(song.id)) {
|
||||||
|
result.add(song);
|
||||||
|
addedSongIds.add(song.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不够,用纯随机补齐
|
||||||
|
if (result.length < count) {
|
||||||
|
// 复制一份并打乱,避免修改原数组
|
||||||
|
List<SongModel> fallback = List.from(allSongs);
|
||||||
|
fallback.shuffle(random);
|
||||||
|
|
||||||
|
for (var s in fallback) {
|
||||||
|
if (result.length >= count) break;
|
||||||
|
// 使用 Set 进行 O(1) 查找,避免 List.contains 的 O(N) 查找
|
||||||
|
if (!addedSongIds.contains(s.id)) {
|
||||||
|
result.add(s);
|
||||||
|
addedSongIds.add(s.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.take(count).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 辅助:获取歌曲指定难度的定数 (Level Value)
|
||||||
|
static double? _getSongLevel(SongModel song, int levelIndex) {
|
||||||
|
// levelIndex: 0=Basic, 1=Advanced, 2=Expert, 3=Master, 4=Re:Master
|
||||||
|
|
||||||
|
// 1. 尝试从 SD (Standard) 获取
|
||||||
|
if (song.sd != null && song.sd is Map) {
|
||||||
|
final sdMap = song.sd as Map;
|
||||||
|
// 键可能是字符串 "3" 或整数 3,这里做兼容
|
||||||
|
var data = sdMap["$levelIndex"] ?? sdMap[levelIndex];
|
||||||
|
if (data is Map && data.containsKey("level_value")) {
|
||||||
|
final val = data["level_value"];
|
||||||
|
if (val is num) return val.toDouble();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 尝试从 DX 获取
|
||||||
|
if (song.dx != null && song.dx is Map) {
|
||||||
|
final dxMap = song.dx as Map;
|
||||||
|
var data = dxMap["$levelIndex"] ?? dxMap[levelIndex];
|
||||||
|
if (data is Map && data.containsKey("level_value")) {
|
||||||
|
final val = data["level_value"];
|
||||||
|
if (val is num) return val.toDouble();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 辅助:根据定数和达成率计算 Rating (完全复刻 Java getRatingChart)
|
||||||
|
static int _calculateRating(double diff, double achievementPercent) {
|
||||||
|
double sys = 22.4;
|
||||||
|
double ach = achievementPercent; // 例如 99.5
|
||||||
|
|
||||||
|
if (ach >= 100.5000) {
|
||||||
|
return (diff * 22.512).floor();
|
||||||
|
}
|
||||||
|
if (ach == 100.4999) {
|
||||||
|
sys = 22.2;
|
||||||
|
} else if (ach >= 100.0000) {
|
||||||
|
sys = 21.6;
|
||||||
|
} else if (ach == 99.9999) {
|
||||||
|
sys = 21.4;
|
||||||
|
} else if (ach >= 99.5000) {
|
||||||
|
sys = 21.1;
|
||||||
|
} else if (ach >= 99.0000) {
|
||||||
|
sys = 20.8;
|
||||||
|
} else if (ach >= 98.0000) {
|
||||||
|
sys = 20.3;
|
||||||
|
} else if (ach >= 97.0000) {
|
||||||
|
sys = 20.0;
|
||||||
|
} else if (ach >= 94.0000) {
|
||||||
|
sys = 16.8;
|
||||||
|
} else if (ach >= 90.0000) {
|
||||||
|
sys = 15.2;
|
||||||
|
} else if (ach >= 80.0000) {
|
||||||
|
sys = 13.6;
|
||||||
|
} else if (ach >= 75.0000) {
|
||||||
|
sys = 12.0;
|
||||||
|
} else if (ach >= 70.0000) {
|
||||||
|
sys = 11.2;
|
||||||
|
} else if (ach >= 60.0000) {
|
||||||
|
sys = 9.6;
|
||||||
|
} else if (ach >= 50.0000) {
|
||||||
|
sys = 8.0;
|
||||||
|
} else {
|
||||||
|
sys = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sys == 0.0) return 0;
|
||||||
|
|
||||||
|
// Java: (int) (diff * sys * achievement / 100)
|
||||||
|
return (diff * sys * ach / 100).floor();
|
||||||
|
}
|
||||||
|
}
|
||||||
701
lib/service/sega_service.dart
Normal file
701
lib/service/sega_service.dart
Normal file
@@ -0,0 +1,701 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:dio/io.dart';
|
||||||
|
import 'package:html/parser.dart' as html_parser;
|
||||||
|
import 'package:html/dom.dart' as dom;
|
||||||
|
|
||||||
|
typedef void OnProgress(String status, double? percent);
|
||||||
|
|
||||||
|
class SegaService {
|
||||||
|
static Future<Map<String, dynamic>> fetchAndSync({
|
||||||
|
required Region region,
|
||||||
|
required String segaId,
|
||||||
|
required String password,
|
||||||
|
OnProgress? onProgress,
|
||||||
|
}) async {
|
||||||
|
print("[SegaService] fetchAndSync 开始执行,区域:${region.name}");
|
||||||
|
print("[SegaService] SegaID:$segaId");
|
||||||
|
|
||||||
|
final MaimaiNetClient client;
|
||||||
|
if (region == Region.jp) {
|
||||||
|
client = MaimaiNETJpClient();
|
||||||
|
print("[SegaService] 使用日服客户端");
|
||||||
|
} else {
|
||||||
|
client = MaimaiNETIntlClient();
|
||||||
|
print("[SegaService] 使用国际服客户端");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
onProgress?.call("正在登录 Sega 账号...", null);
|
||||||
|
await client.login(AuthParams(id: segaId, password: password));
|
||||||
|
print("[SegaService] 登录成功");
|
||||||
|
|
||||||
|
onProgress?.call("登录成功,获取用户信息...", null);
|
||||||
|
final userInfo = await client.fetchUserInfo();
|
||||||
|
print("[SegaService] 用户信息获取成功:${userInfo.name}");
|
||||||
|
|
||||||
|
onProgress?.call("开始拉取歌曲成绩...", null);
|
||||||
|
final musicRecords = await client.fetchMusicRecords(onProgress: onProgress);
|
||||||
|
print("[SegaService] 歌曲成绩获取完成,总数:${musicRecords.length}");
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user": userInfo.toJson(),
|
||||||
|
"music": musicRecords.map((e) => e.toJson()).toList(),
|
||||||
|
"region": region.name,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
print("[SegaService] fetchAndSync 发生错误:$e");
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> verifyOnlyLogin({
|
||||||
|
required Region region,
|
||||||
|
required String segaId,
|
||||||
|
required String password,
|
||||||
|
}) async {
|
||||||
|
print("[SegaService] verifyOnlyLogin 开始验证登录,区域:${region.name}");
|
||||||
|
print("[SegaService] 验证账号:$segaId");
|
||||||
|
|
||||||
|
final MaimaiNetClient client;
|
||||||
|
if (region == Region.jp) {
|
||||||
|
client = MaimaiNETJpClient();
|
||||||
|
} else {
|
||||||
|
client = MaimaiNETIntlClient();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await client.login(AuthParams(id: segaId, password: password));
|
||||||
|
print("[SegaService] 验证登录成功");
|
||||||
|
final userInfo = await client.fetchUserInfo();
|
||||||
|
print("[SegaService] 验证成功,用户名:${userInfo.name}");
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user": userInfo.toJson(),
|
||||||
|
"music": [],
|
||||||
|
"region": region.name,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
print("[SegaService] 验证登录失败:$e");
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Region { jp, intl }
|
||||||
|
|
||||||
|
class NetImportError implements Exception {
|
||||||
|
final String code;
|
||||||
|
final String message;
|
||||||
|
NetImportError(this.code, [String? msg]) : message = msg ?? code;
|
||||||
|
@override
|
||||||
|
String toString() => 'NetImportError($code): $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthParams {
|
||||||
|
final String id;
|
||||||
|
final String password;
|
||||||
|
AuthParams({required this.id, required this.password});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SheetInfo {
|
||||||
|
final String songId;
|
||||||
|
final String type;
|
||||||
|
final String difficulty;
|
||||||
|
|
||||||
|
SheetInfo({required this.songId, required this.type, required this.difficulty});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'songId': songId,
|
||||||
|
'type': type,
|
||||||
|
'difficulty': difficulty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class DxScore {
|
||||||
|
final int achieved;
|
||||||
|
final int total;
|
||||||
|
DxScore({required this.achieved, required this.total});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'achieved': achieved,
|
||||||
|
'total': total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Achievement {
|
||||||
|
final int rate;
|
||||||
|
final DxScore dxScore;
|
||||||
|
final List<String> flags;
|
||||||
|
|
||||||
|
Achievement({required this.rate, required this.dxScore, required this.flags});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'rate': rate,
|
||||||
|
'dxScore': dxScore.toJson(),
|
||||||
|
'flags': flags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class MusicRecord {
|
||||||
|
final SheetInfo sheet;
|
||||||
|
final Achievement achievement;
|
||||||
|
|
||||||
|
MusicRecord({required this.sheet, required this.achievement});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'sheet': sheet.toJson(),
|
||||||
|
'achievement': achievement.toJson(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserInfo {
|
||||||
|
final String avatar;
|
||||||
|
final String name;
|
||||||
|
final Map<String, String> trophy;
|
||||||
|
final String courseIcon;
|
||||||
|
final String classIcon;
|
||||||
|
final String plateUrl;
|
||||||
|
|
||||||
|
UserInfo({
|
||||||
|
required this.avatar,
|
||||||
|
required this.name,
|
||||||
|
required this.trophy,
|
||||||
|
required this.courseIcon,
|
||||||
|
required this.classIcon,
|
||||||
|
required this.plateUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'avatar': avatar,
|
||||||
|
'name': name,
|
||||||
|
'trophy': trophy,
|
||||||
|
'courseIcon': courseIcon,
|
||||||
|
'classIcon': classIcon,
|
||||||
|
'plateUrl': plateUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class URLs {
|
||||||
|
static const Map<String, String> INTL = {
|
||||||
|
"LOGIN_PAGE": "https://lng-tgk-aime-gw.am-all.net/common_auth/login?site_id=maimaidxex&redirect_url=https://maimaidx-eng.com/maimai-mobile/&back_url=https://maima.sega.com/",
|
||||||
|
"LOGIN_ENDPOINT": "https://lng-tgk-aime-gw.am-all.net/common_auth/login/sid",
|
||||||
|
"RECORD_RECENT_PAGE": "https://maimaidx-eng.com/maimai-mobile/record",
|
||||||
|
"RECORD_MUSICS_PAGE": "https://maimaidx-eng.com/maimai-mobile/record/musicGenre/search/",
|
||||||
|
"HOME": "https://maimaidx-eng.com/maimai-mobile/home/",
|
||||||
|
"NAMEPLATE": "https://maimaidx-eng.com/maimai-mobile/collection/nameplate/",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const Map<String, String> JP = {
|
||||||
|
"LOGIN_PAGE": "https://maimaidx.jp/maimai-mobile/",
|
||||||
|
"LOGIN_ENDPOINT": "https://maimaidx.jp/maimai-mobile/submit/",
|
||||||
|
"LOGIN_AIMELIST": "https://maimaidx.jp/maimai-mobile/aimeList/",
|
||||||
|
"LOGIN_AIMELIST_SUBMIT": "https://maimaidx.jp/maimai-mobile/aimeList/submit/?idx=0",
|
||||||
|
"HOMEPAGE": "https://maimaidx.jp/maimai-mobile/home/",
|
||||||
|
"RECORD_RECENT_PAGE": "https://maimaidx.jp/maimai-mobile/record",
|
||||||
|
"RECORD_MUSICS_PAGE": "https://maimaidx.jp/maimai-mobile/record/musicGenre/search/",
|
||||||
|
"NAMEPLATE": "https://maimaidx.jp/maimai-mobile/collection/nameplate/",
|
||||||
|
};
|
||||||
|
|
||||||
|
static const List<String> ERROR_URLS = [
|
||||||
|
"https://maimaidx-eng.com/maimai-mobile/error/",
|
||||||
|
"https://maimaidx.jp/maimai-mobile/error/"
|
||||||
|
];
|
||||||
|
|
||||||
|
static const List<String> MAINTENANCE_TEXTS = [
|
||||||
|
"定期メンテナンス中です",
|
||||||
|
"Sorry, servers are under maintenance."
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class MaimaiNetClient {
|
||||||
|
late Dio dio;
|
||||||
|
late Map<String, String> urls;
|
||||||
|
|
||||||
|
final Map<String, String> _cookies = {};
|
||||||
|
|
||||||
|
MaimaiNetClient() {
|
||||||
|
dio = Dio();
|
||||||
|
_setupDio();
|
||||||
|
print("[MaimaiNetClient] 客户端初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupDio() {
|
||||||
|
print("[MaimaiNetClient] 配置 Dio");
|
||||||
|
|
||||||
|
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
|
||||||
|
final client = HttpClient();
|
||||||
|
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
|
||||||
|
return client;
|
||||||
|
};
|
||||||
|
|
||||||
|
dio.interceptors.add(InterceptorsWrapper(
|
||||||
|
onRequest: (options, handler) {
|
||||||
|
print("[DIO] 请求:${options.method} ${options.uri}");
|
||||||
|
if (_cookies.isNotEmpty) {
|
||||||
|
final cookieStr = _cookies.entries.map((e) => '${e.key}=${e.value}').join('; ');
|
||||||
|
options.headers['Cookie'] = cookieStr;
|
||||||
|
}
|
||||||
|
return handler.next(options);
|
||||||
|
},
|
||||||
|
onResponse: (response, handler) {
|
||||||
|
print("[DIO] 响应:${response.statusCode} ${response.requestOptions.uri}");
|
||||||
|
final setCookies = response.headers['set-cookie'];
|
||||||
|
if (setCookies != null) {
|
||||||
|
for (var cookieHeader in setCookies) {
|
||||||
|
if (cookieHeader.contains('=')) {
|
||||||
|
final parts = cookieHeader.split(';')[0].split('=');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
final key = parts[0].trim();
|
||||||
|
final value = parts.sublist(1).join('=').trim();
|
||||||
|
if (key.isNotEmpty) {
|
||||||
|
_cookies[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handler.next(response);
|
||||||
|
},
|
||||||
|
onError: (DioException e, handler) {
|
||||||
|
print("[DIO] 请求错误:$e");
|
||||||
|
return handler.next(e);
|
||||||
|
}));
|
||||||
|
|
||||||
|
dio.options.headers = {
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
||||||
|
"Accept-Language": "ja;q=0.9,en;q=0.8",
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
||||||
|
"Sec-Fetch-Dest": "document",
|
||||||
|
"Sec-Fetch-Mode": "navigate",
|
||||||
|
"Upgrade-Insecure-Requests": "1",
|
||||||
|
};
|
||||||
|
|
||||||
|
dio.options.connectTimeout = const Duration(seconds: 10);
|
||||||
|
dio.options.receiveTimeout = const Duration(seconds: 10);
|
||||||
|
dio.options.followRedirects = false;
|
||||||
|
dio.options.maxRedirects = 0;
|
||||||
|
dio.options.validateStatus = (status) => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dom.Document> fetchAsSoupWithRedirects(String url, {Map<String, dynamic>? queryParameters}) async {
|
||||||
|
print("[fetchAsSoup] 开始请求(支持重定向):$url 参数:$queryParameters");
|
||||||
|
|
||||||
|
String currentUrl = url;
|
||||||
|
int redirectCount = 0;
|
||||||
|
const maxRedirects = 5;
|
||||||
|
|
||||||
|
while (redirectCount < maxRedirects) {
|
||||||
|
try {
|
||||||
|
final response = await dio.get(currentUrl, queryParameters: queryParameters);
|
||||||
|
|
||||||
|
if (response.data is String) {
|
||||||
|
checkMaintenance(response.data.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([301, 302, 303, 307, 308].contains(response.statusCode)) {
|
||||||
|
final location = response.headers.value('location');
|
||||||
|
if (location != null) {
|
||||||
|
print("[fetchAsSoup] 重定向 -> $location");
|
||||||
|
currentUrl = location;
|
||||||
|
redirectCount++;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
print("[fetchAsSoup] 重定向无 location");
|
||||||
|
throw NetImportError("REDIRECT_WITHOUT_LOCATION");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode! >= 400) {
|
||||||
|
print("[fetchAsSoup] HTTP 错误:${response.statusCode}");
|
||||||
|
throw NetImportError("HTTP_ERROR_${response.statusCode}", "Failed to fetch $url");
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[fetchAsSoup] 请求完成,解析 HTML");
|
||||||
|
return html_parser.parse(response.data);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
print("[fetchAsSoup] Dio 异常:$e");
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print("[fetchAsSoup] 重定向次数过多");
|
||||||
|
throw NetImportError("TOO_MANY_REDIRECTS");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dom.Document> fetchAsSoup(String url, {Map<String, dynamic>? queryParameters}) async {
|
||||||
|
return fetchAsSoupWithRedirects(url, queryParameters: queryParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkMaintenance(String text) {
|
||||||
|
for (final maintText in URLs.MAINTENANCE_TEXTS) {
|
||||||
|
if (text.contains(maintText)) {
|
||||||
|
print("[检查维护] 检测到服务器维护!");
|
||||||
|
throw NetImportError("NET_MAINTENANCE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> login(AuthParams auth);
|
||||||
|
Future<UserInfo> fetchUserInfo();
|
||||||
|
Future<List<MusicRecord>> fetchMusicRecords({OnProgress? onProgress});
|
||||||
|
|
||||||
|
SheetInfo? parseSheetInfo(dom.Element div) {
|
||||||
|
final songIdEl = div.querySelector(".music_name_block");
|
||||||
|
if (songIdEl == null) return null;
|
||||||
|
final songId = songIdEl.text.trim();
|
||||||
|
|
||||||
|
String? type;
|
||||||
|
if (div.querySelector(".music_kind_icon_dx._btn_on") != null) {
|
||||||
|
type = "dx";
|
||||||
|
} else if (div.querySelector(".music_kind_icon_standard._btn_on") != null) {
|
||||||
|
type = "standard";
|
||||||
|
} else {
|
||||||
|
final typeIcon = div.querySelector(".music_kind_icon");
|
||||||
|
if (typeIcon != null) {
|
||||||
|
final src = typeIcon.attributes['src'] ?? '';
|
||||||
|
if (src.contains('music_dx.png')) type = 'dx';
|
||||||
|
else if (src.contains('music_standard.png')) type = 'standard';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? difficulty;
|
||||||
|
final diffIcon = div.querySelector(".h_20.f_l");
|
||||||
|
if (diffIcon != null) {
|
||||||
|
final src = diffIcon.attributes['src'] ?? '';
|
||||||
|
final match = RegExp(r'diff_(.*)\.png').firstMatch(src);
|
||||||
|
if (match != null) {
|
||||||
|
difficulty = match.group(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (difficulty == "utage") type = "utage";
|
||||||
|
if (songId.isEmpty || type == null || difficulty == null) return null;
|
||||||
|
|
||||||
|
return SheetInfo(songId: songId, type: type, difficulty: difficulty);
|
||||||
|
}
|
||||||
|
|
||||||
|
Achievement parseAchievement(dom.Element div) {
|
||||||
|
final rateEl = div.querySelector(".music_score_block.w_112");
|
||||||
|
final rateStr = rateEl?.text.trim().replaceAll('%', '').replaceAll('.', '') ?? '0';
|
||||||
|
final rate = int.tryParse(rateStr) ?? 0;
|
||||||
|
|
||||||
|
final dxScoreEl = div.querySelector(".music_score_block.w_190");
|
||||||
|
final dxScoreText = dxScoreEl?.text.trim() ?? "0 / 0";
|
||||||
|
final parts = dxScoreText.split('/');
|
||||||
|
final achieved = int.tryParse(parts[0].trim().replaceAll(',', '')) ?? 0;
|
||||||
|
final total = parts.length > 1 ? (int.tryParse(parts[1].trim().replaceAll(',', '')) ?? 0) : 0;
|
||||||
|
|
||||||
|
final flags = <String>[];
|
||||||
|
final flagImages = div.querySelectorAll("form img.f_r");
|
||||||
|
final flagMatchers = {
|
||||||
|
"fullCombo": "fc.png",
|
||||||
|
"fullCombo+": "fcplus.png",
|
||||||
|
"allPerfect": "ap.png",
|
||||||
|
"allPerfect+": "applus.png",
|
||||||
|
"syncPlay": "sync.png",
|
||||||
|
"fullSync": "fs.png",
|
||||||
|
"fullSync+": "fsplus.png",
|
||||||
|
"fullSyncDX": "fsd.png",
|
||||||
|
"fullSyncDX+": "fsdplus.png",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (final img in flagImages) {
|
||||||
|
final src = img.attributes['src'] ?? '';
|
||||||
|
for (final entry in flagMatchers.entries) {
|
||||||
|
if (src.contains(entry.value)) {
|
||||||
|
flags.add(entry.key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Achievement(
|
||||||
|
rate: rate,
|
||||||
|
dxScore: DxScore(achieved: achieved, total: total),
|
||||||
|
flags: flags,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MaimaiNETJpClient extends MaimaiNetClient {
|
||||||
|
MaimaiNETJpClient() {
|
||||||
|
urls = URLs.JP;
|
||||||
|
print("[MaimaiNETJpClient] 日服客户端初始化");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> login(AuthParams auth) async {
|
||||||
|
print("[日服客户端] 开始登录流程");
|
||||||
|
|
||||||
|
final soup = await fetchAsSoup(urls["LOGIN_PAGE"]!);
|
||||||
|
final tokenInput = soup.querySelector("input[name='token']");
|
||||||
|
if (tokenInput == null) {
|
||||||
|
print("[日服客户端] 未找到 token,登录失败");
|
||||||
|
throw NetImportError("TOKEN_ERROR");
|
||||||
|
}
|
||||||
|
final token = tokenInput.attributes['value'];
|
||||||
|
print("[日服客户端] 获取 token 成功:$token");
|
||||||
|
|
||||||
|
final loginData = {
|
||||||
|
"segaId": auth.id,
|
||||||
|
"password": auth.password,
|
||||||
|
"save_cookie": "on",
|
||||||
|
"token": token!,
|
||||||
|
};
|
||||||
|
|
||||||
|
final response = await dio.post(
|
||||||
|
urls["LOGIN_ENDPOINT"]!,
|
||||||
|
data: FormData.fromMap(loginData),
|
||||||
|
options: Options(
|
||||||
|
contentType: Headers.formUrlEncodedContentType,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final location = response.headers.value('location');
|
||||||
|
if (location != null && URLs.ERROR_URLS.any((e) => location.contains(e))) {
|
||||||
|
print("[日服客户端] 账号或密码错误");
|
||||||
|
throw NetImportError("INVALID_CREDENTIALS");
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[日服客户端] 登录接口请求成功,处理跳转");
|
||||||
|
await fetchAsSoup(urls["LOGIN_AIMELIST"]!);
|
||||||
|
await fetchAsSoup(urls["LOGIN_AIMELIST_SUBMIT"]!);
|
||||||
|
await fetchAsSoup(urls["HOMEPAGE"]!);
|
||||||
|
print("[日服客户端] 登录流程全部完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UserInfo> fetchUserInfo() async {
|
||||||
|
print("[日服客户端] 获取用户信息");
|
||||||
|
final soup = await fetchAsSoup(urls["HOMEPAGE"]!);
|
||||||
|
final avatarImg = soup.querySelector('img[loading="lazy"].w_112.f_l');
|
||||||
|
final avatar = avatarImg?.attributes['src'] ?? '';
|
||||||
|
final nameEl = soup.querySelector(".name_block");
|
||||||
|
final name = nameEl?.text.trim() ?? '';
|
||||||
|
|
||||||
|
String trophyClass = "Normal";
|
||||||
|
String trophyName = "";
|
||||||
|
final trophyBlock = soup.querySelector("div.trophy_block");
|
||||||
|
if (trophyBlock != null) {
|
||||||
|
for (final c in trophyBlock.classes) {
|
||||||
|
if (["trophy_Normal", "trophy_Silver", "trophy_Gold", "trophy_Rainbow"].contains(c)) {
|
||||||
|
trophyClass = c.replaceFirst('trophy_', '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final trophyInner = trophyBlock.querySelector(".trophy_inner_block span");
|
||||||
|
if (trophyInner != null) trophyName = trophyInner.text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
final courseImg = soup.querySelector('img[src*="course_rank_"]');
|
||||||
|
final courseIcon = courseImg?.attributes['src'] ?? '';
|
||||||
|
final classImg = soup.querySelector('img[src*="class_rank_"]');
|
||||||
|
final classIcon = classImg?.attributes['src'] ?? '';
|
||||||
|
|
||||||
|
String plateUrl = "";
|
||||||
|
try {
|
||||||
|
print("[日服客户端] 获取铭牌");
|
||||||
|
final plateSoup = await fetchAsSoup(urls["NAMEPLATE"]!);
|
||||||
|
final plateImg = plateSoup.querySelector('img[loading="lazy"].w_396.m_r_10');
|
||||||
|
if (plateImg != null) plateUrl = plateImg.attributes['src'] ?? '';
|
||||||
|
} catch (e) {
|
||||||
|
print("[日服客户端] 获取铭牌失败:$e");
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[日服客户端] 用户信息解析完成:$name");
|
||||||
|
return UserInfo(
|
||||||
|
avatar: avatar,
|
||||||
|
name: name,
|
||||||
|
trophy: {'rarity': trophyClass, 'text': trophyName},
|
||||||
|
courseIcon: courseIcon,
|
||||||
|
classIcon: classIcon,
|
||||||
|
plateUrl: plateUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 【修复点】只拉一遍,不统计 ======================
|
||||||
|
@override
|
||||||
|
Future<List<MusicRecord>> fetchMusicRecords({OnProgress? onProgress}) async {
|
||||||
|
print("[日服客户端] 开始获取所有歌曲成绩(修复版:仅拉取1次)");
|
||||||
|
final records = <MusicRecord>[];
|
||||||
|
|
||||||
|
final difficulties = [
|
||||||
|
("0", "Basic"),
|
||||||
|
("1", "Advanced"),
|
||||||
|
("2", "Expert"),
|
||||||
|
("3", "Master"),
|
||||||
|
("4", "Remaster"),
|
||||||
|
("10", "Utage"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final diff in difficulties) {
|
||||||
|
try {
|
||||||
|
onProgress?.call("正在拉取:${diff.$2}", null);
|
||||||
|
final url = urls["RECORD_MUSICS_PAGE"]!;
|
||||||
|
final soup = await fetchAsSoup(url, queryParameters: {"genre": "99", "diff": diff.$1});
|
||||||
|
final items = soup.querySelectorAll(".w_450.m_15.p_r.f_0");
|
||||||
|
|
||||||
|
for (final item in items) {
|
||||||
|
final sheet = parseSheetInfo(item);
|
||||||
|
if (sheet != null) {
|
||||||
|
final achievement = parseAchievement(item);
|
||||||
|
records.add(MusicRecord(sheet: sheet, achievement: achievement));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print("[日服客户端] ${diff.$2} 完成,累计:${records.length} 首");
|
||||||
|
} catch (e) {
|
||||||
|
print("[日服客户端] ${diff.$2} 拉取失败:$e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[日服客户端] 所有成绩解析完成,总数:${records.length}");
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MaimaiNETIntlClient extends MaimaiNetClient {
|
||||||
|
MaimaiNETIntlClient() {
|
||||||
|
urls = URLs.INTL;
|
||||||
|
dio.options.headers["Referer"] = URLs.INTL["LOGIN_PAGE"];
|
||||||
|
print("[MaimaiNETIntlClient] 国际服客户端初始化");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> login(AuthParams auth) async {
|
||||||
|
print("[国际服客户端] 开始登录流程");
|
||||||
|
|
||||||
|
await fetchAsSoup(urls["LOGIN_PAGE"]!);
|
||||||
|
print("[国际服客户端] 访问登录页面成功");
|
||||||
|
|
||||||
|
final loginData = {
|
||||||
|
"sid": auth.id,
|
||||||
|
"password": auth.password,
|
||||||
|
"retention": "1",
|
||||||
|
};
|
||||||
|
|
||||||
|
final response = await dio.post(
|
||||||
|
urls["LOGIN_ENDPOINT"]!,
|
||||||
|
data: FormData.fromMap(loginData),
|
||||||
|
options: Options(
|
||||||
|
contentType: Headers.formUrlEncodedContentType,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final location = response.headers.value('location');
|
||||||
|
if (location != null) {
|
||||||
|
print("[国际服客户端] 登录后跳转:$location");
|
||||||
|
await fetchAsSoupWithRedirects(location);
|
||||||
|
}
|
||||||
|
await fetchAsSoup(urls["HOME"]!);
|
||||||
|
print("[国际服客户端] 登录流程完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UserInfo> fetchUserInfo() async {
|
||||||
|
print("[国际服客户端] 获取用户信息");
|
||||||
|
final soup = await fetchAsSoup(urls["HOME"]!);
|
||||||
|
final avatarImg = soup.querySelector('img[loading="lazy"].w_112');
|
||||||
|
final avatar = avatarImg?.attributes['src'] ?? '';
|
||||||
|
final nameEl = soup.querySelector(".name_block");
|
||||||
|
final name = nameEl?.text.trim() ?? '';
|
||||||
|
|
||||||
|
String trophyClass = "Normal";
|
||||||
|
String trophyName = "";
|
||||||
|
final trophyEl = soup.querySelector("div.trophy_block");
|
||||||
|
if (trophyEl != null) {
|
||||||
|
for (final c in trophyEl.classes) {
|
||||||
|
if (["trophy_Normal", "trophy_Silver", "trophy_Gold", "trophy_Rainbow"].contains(c)) {
|
||||||
|
trophyClass = c.replaceFirst('trophy_', '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final trophyInner = trophyEl.querySelector(".trophy_inner_block span");
|
||||||
|
if (trophyInner != null) trophyName = trophyInner.text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
final courseImg = soup.querySelector('img[src*="course/course_rank_"]');
|
||||||
|
final courseIcon = courseImg?.attributes['src'] ?? '';
|
||||||
|
final classImg = soup.querySelector('img[src*="class/class_rank_"]');
|
||||||
|
final classIcon = classImg?.attributes['src'] ?? '';
|
||||||
|
|
||||||
|
String plateUrl = "";
|
||||||
|
try {
|
||||||
|
print("[国际服客户端] 获取铭牌");
|
||||||
|
final plateSoup = await fetchAsSoup(urls["NAMEPLATE"]!);
|
||||||
|
final plateImg = plateSoup.querySelector('img[loading="lazy"].w_396.m_r_10');
|
||||||
|
if (plateImg != null) plateUrl = plateImg.attributes['src'] ?? '';
|
||||||
|
} catch (e) {
|
||||||
|
print("[国际服客户端] 获取铭牌失败:$e");
|
||||||
|
}
|
||||||
|
|
||||||
|
print("[国际服客户端] 用户信息解析完成:$name");
|
||||||
|
return UserInfo(
|
||||||
|
avatar: avatar,
|
||||||
|
name: name,
|
||||||
|
trophy: {'rarity': trophyClass, 'text': trophyName},
|
||||||
|
courseIcon: courseIcon,
|
||||||
|
classIcon: classIcon,
|
||||||
|
plateUrl: plateUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<MusicRecord>> fetchMusicRecords({OnProgress? onProgress}) async {
|
||||||
|
print("[国际服客户端] 开始获取所有歌曲成绩");
|
||||||
|
final difficulties = [
|
||||||
|
("0", "Basic"),
|
||||||
|
("1", "Advanced"),
|
||||||
|
("2", "Expert"),
|
||||||
|
("3", "Master"),
|
||||||
|
("4", "Remaster"),
|
||||||
|
("10", "Utage"),
|
||||||
|
];
|
||||||
|
|
||||||
|
int total = 0;
|
||||||
|
int current = 0;
|
||||||
|
|
||||||
|
final futures = difficulties.map((diff) async {
|
||||||
|
onProgress?.call("正在并发拉取难度..", null);
|
||||||
|
final url = urls["RECORD_MUSICS_PAGE"]!;
|
||||||
|
final soup = await fetchAsSoup(url, queryParameters: {"genre": "99", "diff": diff.$1});
|
||||||
|
final items = soup.querySelectorAll(".w_450.m_15.p_r.f_0");
|
||||||
|
final list = <MusicRecord>[];
|
||||||
|
|
||||||
|
for (final item in items) {
|
||||||
|
final sheet = parseSheetInfo(item);
|
||||||
|
if (sheet != null) {
|
||||||
|
final achievement = parseAchievement(item);
|
||||||
|
list.add(MusicRecord(sheet: sheet, achievement: achievement));
|
||||||
|
}
|
||||||
|
current++;
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
|
final results = await Future.wait(futures);
|
||||||
|
final all = results.expand((e) => e).toList();
|
||||||
|
print("[国际服客户端] 所有成绩解析完成,总数:${all.length}");
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> fetchNetRecords(Region region, AuthParams auth) async {
|
||||||
|
print("[fetchNetRecords] 外部接口调用,区域:${region.name}");
|
||||||
|
final MaimaiNetClient client;
|
||||||
|
if (region == Region.jp) {
|
||||||
|
client = MaimaiNETJpClient();
|
||||||
|
} else {
|
||||||
|
client = MaimaiNETIntlClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.login(auth);
|
||||||
|
final userInfo = await client.fetchUserInfo();
|
||||||
|
final musicRecords = await client.fetchMusicRecords();
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user": userInfo.toJson(),
|
||||||
|
"music": musicRecords.map((e) => e.toJson()).toList(),
|
||||||
|
"recent": [],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -283,4 +283,28 @@ class UserService {
|
|||||||
throw _getErrorMessage(e);
|
throw _getErrorMessage(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 上传 Sega 成绩数据
|
||||||
|
static Future<Map<String, dynamic>> uploadSegaRating(
|
||||||
|
String token,
|
||||||
|
String segaId,
|
||||||
|
Map<String, dynamic> segaResult,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final res = await _dio.post(
|
||||||
|
'$baseUrl/api/union/segaReisaRating',
|
||||||
|
queryParameters: {
|
||||||
|
"segaId": segaId,
|
||||||
|
},
|
||||||
|
data: segaResult,
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
"Authorization": token,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return Map<String, dynamic>.from(res.data);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw _getErrorMessage(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -21,14 +21,14 @@
|
|||||||
/* End PBXAggregateTarget section */
|
/* End PBXAggregateTarget section */
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
1115E964EE7344044EA87340 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB4F5A4FE440294B3214794A /* Pods_Runner.framework */; };
|
|
||||||
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
|
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
|
||||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
||||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||||
3706021F7EEB2AC8761EF83E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80F792EE2D670777FD7CA688 /* Pods_RunnerTests.framework */; };
|
347C8783FFA8594D3475C1B1 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8403A1BE4D0285716060FA7E /* Pods_RunnerTests.framework */; };
|
||||||
|
D001A0C52A7A968BD1BBDC89 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D349F9576E7496D04E1998B2 /* Pods_Runner.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -62,7 +62,8 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
03E70FFEBD3ABEFEA74186E9 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
061A6DA9B784560E62EA378E /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
1E3EDE577A6C1F411A56772B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||||
@@ -79,15 +80,14 @@
|
|||||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||||
439331F7BBC4C5200C47CD53 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
716AF5A705D0555D88DF2B29 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
79CAB73FB66F4C32DE422CDE /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||||
80F792EE2D670777FD7CA688 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
8403A1BE4D0285716060FA7E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
925906409C141F8E699BDA54 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
B439C3977658BF3659A03040 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
9AC33D131F9E1EA9EB03F15A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
D48F6E62A7A5E80DDD97417B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
C52A2A0CB2BACFFF5FF7072B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
E0D55815B799E5060DE15BB1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
D349F9576E7496D04E1998B2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
EB4F5A4FE440294B3214794A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
3706021F7EEB2AC8761EF83E /* Pods_RunnerTests.framework in Frameworks */,
|
347C8783FFA8594D3475C1B1 /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -103,13 +103,27 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
1115E964EE7344044EA87340 /* Pods_Runner.framework in Frameworks */,
|
D001A0C52A7A968BD1BBDC89 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
10C14F25FB53CAE6F47361B9 /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C52A2A0CB2BACFFF5FF7072B /* Pods-Runner.debug.xcconfig */,
|
||||||
|
9AC33D131F9E1EA9EB03F15A /* Pods-Runner.release.xcconfig */,
|
||||||
|
925906409C141F8E699BDA54 /* Pods-Runner.profile.xcconfig */,
|
||||||
|
061A6DA9B784560E62EA378E /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
|
1E3EDE577A6C1F411A56772B /* Pods-RunnerTests.release.xcconfig */,
|
||||||
|
716AF5A705D0555D88DF2B29 /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
331C80D6294CF71000263BE5 /* RunnerTests */ = {
|
331C80D6294CF71000263BE5 /* RunnerTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -137,7 +151,7 @@
|
|||||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||||
33CC10EE2044A3C60003C045 /* Products */,
|
33CC10EE2044A3C60003C045 /* Products */,
|
||||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||||
643F9CEC1C25FED65C6CBC14 /* Pods */,
|
10C14F25FB53CAE6F47361B9 /* Pods */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -185,25 +199,11 @@
|
|||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
643F9CEC1C25FED65C6CBC14 /* Pods */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
B439C3977658BF3659A03040 /* Pods-Runner.debug.xcconfig */,
|
|
||||||
E0D55815B799E5060DE15BB1 /* Pods-Runner.release.xcconfig */,
|
|
||||||
439331F7BBC4C5200C47CD53 /* Pods-Runner.profile.xcconfig */,
|
|
||||||
79CAB73FB66F4C32DE422CDE /* Pods-RunnerTests.debug.xcconfig */,
|
|
||||||
D48F6E62A7A5E80DDD97417B /* Pods-RunnerTests.release.xcconfig */,
|
|
||||||
03E70FFEBD3ABEFEA74186E9 /* Pods-RunnerTests.profile.xcconfig */,
|
|
||||||
);
|
|
||||||
name = Pods;
|
|
||||||
path = Pods;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
EB4F5A4FE440294B3214794A /* Pods_Runner.framework */,
|
D349F9576E7496D04E1998B2 /* Pods_Runner.framework */,
|
||||||
80F792EE2D670777FD7CA688 /* Pods_RunnerTests.framework */,
|
8403A1BE4D0285716060FA7E /* Pods_RunnerTests.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -215,7 +215,7 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
A3B693C9D43FF0298BC47F87 /* [CP] Check Pods Manifest.lock */,
|
2C72CB55051DF188C184CCE5 /* [CP] Check Pods Manifest.lock */,
|
||||||
331C80D1294CF70F00263BE5 /* Sources */,
|
331C80D1294CF70F00263BE5 /* Sources */,
|
||||||
331C80D2294CF70F00263BE5 /* Frameworks */,
|
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||||
331C80D3294CF70F00263BE5 /* Resources */,
|
331C80D3294CF70F00263BE5 /* Resources */,
|
||||||
@@ -234,13 +234,13 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
837EC17E2DC8DD04D788FAEB /* [CP] Check Pods Manifest.lock */,
|
D60F7A1773BBDE533CA61546 /* [CP] Check Pods Manifest.lock */,
|
||||||
33CC10E92044A3C60003C045 /* Sources */,
|
33CC10E92044A3C60003C045 /* Sources */,
|
||||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||||
33CC10EB2044A3C60003C045 /* Resources */,
|
33CC10EB2044A3C60003C045 /* Resources */,
|
||||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||||
000CA14DDCCC50A5CBB6774D /* [CP] Embed Pods Frameworks */,
|
38C831F56EAB5041148AF6D2 /* [CP] Embed Pods Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -323,21 +323,26 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
000CA14DDCCC50A5CBB6774D /* [CP] Embed Pods Frameworks */ = {
|
2C72CB55051DF188C184CCE5 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
);
|
);
|
||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
|
||||||
);
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||||
@@ -378,7 +383,24 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||||
};
|
};
|
||||||
837EC17E2DC8DD04D788FAEB /* [CP] Check Pods Manifest.lock */ = {
|
38C831F56EAB5041148AF6D2 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
D60F7A1773BBDE533CA61546 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -400,28 +422,6 @@
|
|||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
A3B693C9D43FF0298BC47F87 /* [CP] Check Pods Manifest.lock */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputFileListPaths = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
|
||||||
"${PODS_ROOT}/Manifest.lock",
|
|
||||||
);
|
|
||||||
name = "[CP] Check Pods Manifest.lock";
|
|
||||||
outputFileListPaths = (
|
|
||||||
);
|
|
||||||
outputPaths = (
|
|
||||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -473,7 +473,7 @@
|
|||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
331C80DB294CF71000263BE5 /* Debug */ = {
|
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 79CAB73FB66F4C32DE422CDE /* Pods-RunnerTests.debug.xcconfig */;
|
baseConfigurationReference = 061A6DA9B784560E62EA378E /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -488,7 +488,7 @@
|
|||||||
};
|
};
|
||||||
331C80DC294CF71000263BE5 /* Release */ = {
|
331C80DC294CF71000263BE5 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = D48F6E62A7A5E80DDD97417B /* Pods-RunnerTests.release.xcconfig */;
|
baseConfigurationReference = 1E3EDE577A6C1F411A56772B /* Pods-RunnerTests.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
@@ -503,7 +503,7 @@
|
|||||||
};
|
};
|
||||||
331C80DD294CF71000263BE5 /* Profile */ = {
|
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 03E70FFEBD3ABEFEA74186E9 /* Pods-RunnerTests.profile.xcconfig */;
|
baseConfigurationReference = 716AF5A705D0555D88DF2B29 /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
|||||||
@@ -4,13 +4,11 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.server</key>
|
<key>com.apple.security.network.server</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@@ -97,6 +97,14 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.7"
|
version: "3.0.7"
|
||||||
|
csslib:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: csslib
|
||||||
|
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -344,6 +352,14 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.2"
|
||||||
|
html:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: html
|
||||||
|
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "0.15.6"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ dev_dependencies:
|
|||||||
flutter_map: ^6.1.0 # 地图组件
|
flutter_map: ^6.1.0 # 地图组件
|
||||||
latlong2: ^0.9.0 # flutter_map 依赖的坐标库
|
latlong2: ^0.9.0 # flutter_map 依赖的坐标库
|
||||||
geolocator: ^14.0.2
|
geolocator: ^14.0.2
|
||||||
|
html: ^0.15.4
|
||||||
geocoding: ^4.0.0
|
geocoding: ^4.0.0
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
|
|||||||
Reference in New Issue
Block a user