import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:dio/dio.dart'; import '../../model/user_model.dart'; import '../../providers/user_provider.dart'; import '../../service/sega_service.dart'; import 'login_page.dart'; class UserPage extends StatefulWidget { const UserPage({super.key}); @override State createState() => _UserPageState(); } class _UserPageState extends State { final _nameController = TextEditingController(); final _emailController = TextEditingController(); final _sexController = TextEditingController(); final _bioController = TextEditingController(); final _dfUserController = TextEditingController(); final _dfPwdController = TextEditingController(); final _lxKeyController = TextEditingController(); static const _pinkColor = Color(0xFFFFC0D6); bool _isDisagreeRecommend = false; bool _isDisagreeFriend = false; Map? _radarData; Future _loadRadarData() async { final provider = Provider.of(context, listen: false); try { final data = await provider.fetchRadarData("684a6ee7f62aed83538ded34"); setState(() { _radarData = data; }); if(mounted){ ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("雷达图数据加载成功 ✅")), ); } } catch (e) { if(mounted){ ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("加载失败:$e")), ); } } } @override void initState() { super.initState(); _loadRadarData(); WidgetsBinding.instance.addPostFrameCallback((_) { final provider = Provider.of(context, listen: false); provider.fetchSexTags(); }); } @override void didChangeDependencies() { super.didChangeDependencies(); final user = Provider.of(context).user; _nameController.text = user?.name ?? ''; _emailController.text = user?.email ?? ''; _sexController.text = user?.sex ?? ''; _bioController.text = user?.bio ?? ''; _dfUserController.text = user?.dfUsername ?? ''; _dfPwdController.text = user?.dfPassword ?? ''; _lxKeyController.text = user?.lxKey ?? ''; _isDisagreeRecommend = user?.isDisagreeRecommend ?? false; _isDisagreeFriend = user?.isDisagreeFriend ?? false; } @override void dispose() { _nameController.dispose(); _emailController.dispose(); _sexController.dispose(); _bioController.dispose(); _dfUserController.dispose(); _dfPwdController.dispose(); _lxKeyController.dispose(); super.dispose(); } Future _launchURL(String urlString) async { final Uri url = Uri.parse(urlString); if (!await launchUrl(url, mode: LaunchMode.externalApplication)) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('无法打开链接: $urlString')), ); } } } Future _verifyShuiyu() async { final username = _dfUserController.text.trim(); final password = _dfPwdController.text.trim(); if (username.isEmpty || password.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("请输入水鱼用户名和密码")), ); return; } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("正在验证水鱼账号..."), duration: Duration(seconds: 1), ), ); } try { final dio = Dio(); final response = await dio.post( 'https://maimai.diving-fish.com/api/maimaidxprober/login', data: { "username": username, "password": password, }, options: Options( sendTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), ), ); final Map result = response.data; if (mounted) { if (result['message'] == "登录成功") { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("水鱼账号验证成功!"), backgroundColor: Colors.green, duration: Duration(seconds: 1), ), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(result['message'] ?? "验证失败"), duration: Duration(seconds: 1), backgroundColor: Colors.red, ), ); } } } on DioException catch (e) { if (mounted) { String errorMsg = "网络请求失败"; if (e.type == DioExceptionType.connectionTimeout) { errorMsg = "连接超时,请检查网络"; } else if (e.type == DioExceptionType.receiveTimeout) { errorMsg = "服务器响应超时"; } else if (e.type == DioExceptionType.connectionError) { errorMsg = "网络连接异常,请检查网络"; } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(errorMsg), backgroundColor: Colors.red, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("未知错误,请重试"), backgroundColor: Colors.red, duration: Duration(seconds: 1), ), ); } } } Future _verifyLuoxue() async { final key = _lxKeyController.text.trim(); if (key.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("请输入落雪 lxKey")), ); return; } try { final dio = Dio(); final response = await dio.get( 'https://maimai.lxns.net/api/v0/user/maimai/player', options: Options( headers: { 'X-User-Token': key, }, ), ); final result = LuoxueResponse.fromJson(response.data); if (result.success && mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text("✅ 验证成功!玩家:${result.data.name}\nDX Rating:${result.data.rating}"), ), ); } } on DioException catch (e) { String msg = "验证失败"; if (e.response?.statusCode == 401) { msg = "❌ lxKey 无效或已过期"; } else if (e.type == DioExceptionType.connectionError) { msg = "📶 网络连接失败"; } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(msg)), ); } } } void _showSexSelectDialog() { final provider = Provider.of(context, listen: false); final tags = provider.sexTags; showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( "选择性别标签", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 16), if (tags.isEmpty) const Padding( padding: EdgeInsets.all(20), child: Text("暂无性别标签"), ), Wrap( spacing: 8, runSpacing: 8, children: tags.map((tag) { return ActionChip( label: Text(tag), onPressed: () { _sexController.text = tag; Navigator.pop(context); }, ); }).toList(), ), const SizedBox(height: 30), ], ), ); }, ); } void _showSyncProgressDialog(BuildContext context, ValueNotifier status) { showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( content: ValueListenableBuilder( valueListenable: status, builder: (_, text, __) { return Row( children: [ const CircularProgressIndicator(), const SizedBox(width: 20), Expanded(child: Text(text)), ], ); }, ), ), ); } Future _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 _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("准备同步成绩..."); 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(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( 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(context, listen: false); final user = provider.user; if (user != null) { final list = user.segaCards ?? []; final newList = List.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 Widget build(BuildContext context) { final userProvider = Provider.of(context); final UserModel? user = userProvider.user; if (user == null) { return Scaffold( appBar: AppBar(title: const Text("用户中心")), body: Center( child: Transform.translate( offset: const Offset(0, -40), child: _buildLoginCard( onTap: () { Navigator.push( context, MaterialPageRoute(builder: (_) => const LoginPage()), ); }, ), ) ), floatingActionButton: _buildFloatingBackButton(), ); } return Scaffold( body: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20), child: Center( child: Container( constraints: const BoxConstraints(maxWidth: 900), child: Column( children: [ _buildProfileHeader(context, userProvider, user), const SizedBox(height: 24), if (user.userCards != null && user.userCards!.isNotEmpty) _buildUserCardSlider(userProvider, user), const SizedBox(height: 24), _buildSectionCard([ _buildItem("用户ID", user.id.toString()), _buildEditableItem("用户名", _nameController), _buildEditableItem("邮箱", _emailController), _buildEditableSexItem(), _buildEditableItem("个性签名", _bioController), _buildItem("积分", user.points.toString()), _buildItem("B50方案", user.useB50Type.toString()), _buildCopyableItem(context, "API Key", user.apiKey), _buildCopyableItem(context, "绑定 API Key", user.apiBindKey), _buildItem("UserID", "****${user.userId.toString().substring(4)}"), _buildItem("上次登录", _formatLastLoginTime(user.lastLoginTime)), ]), const SizedBox(height: 20), _buildScoreCheckerCard(user), const SizedBox(height: 20), _buildSectionCard([ _buildSwitchItem("禁止被推荐", _isDisagreeRecommend, (v) { setState(() => _isDisagreeRecommend = v); }), _buildSwitchItem("禁止加好友", _isDisagreeFriend, (v) { setState(() => _isDisagreeFriend = v); }), ]), const SizedBox(height: 20), _buildSegaCard(user), const SizedBox(height: 20), _buildRadarChartSection(), const SizedBox(height: 20), _buildSaveButton(userProvider), const SizedBox(height: 12), _buildLogoutButton(context, userProvider), const SizedBox(height: 100), ], ), ), ), ), floatingActionButton: _buildFloatingBackButton(), ); } Widget _buildScoreCheckerCard(UserModel user) { return _webCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "第三方查分器绑定", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 16), const Text("水鱼查分器", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), const SizedBox(height: 8), TextField( controller: _dfUserController, decoration: const InputDecoration( labelText: "用户名", isDense: true, border: OutlineInputBorder(), ), ), const SizedBox(height: 8), TextField( controller: _dfPwdController, obscureText: true, decoration: const InputDecoration( labelText: "密码", isDense: true, border: OutlineInputBorder(), ), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton.icon( icon: const Icon(Icons.check_circle_outline, size: 18), label: const Text("验证水鱼账号"), onPressed: _verifyShuiyu, ), const SizedBox(width: 8), TextButton.icon( icon: const Icon(Icons.open_in_new, size: 18), label: const Text("跳转水鱼官网"), onPressed: () => _launchURL("https://www.diving-fish.com/"), ), ], ), const SizedBox(height: 16), const Text("落雪查分器", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), const SizedBox(height: 8), TextField( controller: _lxKeyController, decoration: const InputDecoration( labelText: "lxKey", isDense: true, border: OutlineInputBorder(), ), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton.icon( icon: const Icon(Icons.check_circle_outline, size: 18), label: const Text("验证落雪账号"), onPressed: _verifyLuoxue, ), const SizedBox(width: 8), TextButton.icon( icon: const Icon(Icons.open_in_new, size: 18), label: const Text("跳转落雪官网"), onPressed: () => _launchURL("https://maimai.lxns.net/"), ), ], ), const SizedBox(height: 16), const Text("Mai2Links 账号", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), const SizedBox(height: 8), if (user.mai2links != null && user.mai2links!.isNotEmpty) ...user.mai2links!.keys.map((name) { final isUser = name.toLowerCase().contains("user"); return Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: isUser ? Colors.purpleAccent : Colors.green, borderRadius: BorderRadius.circular(4), ), child: Text( isUser ? "用户账号" : "机台账号", style: const TextStyle(color: Colors.white, fontSize: 12), ), ), const SizedBox(width: 10), Text("mai2Link用户 · $name"), ], ), ); }) else const Text("无mai2links绑定"), const SizedBox(height: 20), const Text("国服多账号", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), const SizedBox(height: 6), if(user.userName2userId!=null && user.userName2userId!.isNotEmpty) ...user.userName2userId!.keys.map((name) { return Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.pinkAccent, borderRadius: BorderRadius.circular(4), ), child: const Text( "舞萌账号", style: TextStyle(color: Colors.white, fontSize: 12), ), ), const SizedBox(width: 10), Text("国服用户 - $name"), ], ), ); }) ], ), ); } String _formatLastLoginTime(int? timestamp) { if (timestamp == null || timestamp == 0) return "未登录"; final date = DateTime.fromMillisecondsSinceEpoch(timestamp) .toUtc() .add(const Duration(hours: 8)); return date.toString().substring(0, 19); } Widget _buildEditableSexItem() { return Padding( padding: const EdgeInsets.symmetric(vertical: 7), child: Row( children: [ const SizedBox(width: 120, child: Text("性别")), Expanded( child: TextField( controller: _sexController, decoration: InputDecoration( isDense: true, border: const UnderlineInputBorder(), suffixIcon: IconButton( icon: const Icon(Icons.arrow_drop_down, size: 20), onPressed: _showSexSelectDialog, ), ), enabled: true, ), ), ], ), ); } Widget _buildEditableItem(String label, TextEditingController controller) { return Padding( padding: const EdgeInsets.symmetric(vertical: 7), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox(width: 120, child: Text(label)), Expanded( child: TextField( controller: controller, decoration: const InputDecoration( isDense: true, border: UnderlineInputBorder(), ), ), ), ], ), ); } Future _refreshUserInfo() async { final provider = Provider.of(context, listen: false); await provider.initUser(); final user = provider.user; if (user != null) { _nameController.text = user.name ?? ''; _emailController.text = user.email ?? ''; _sexController.text = user.sex ?? ''; _bioController.text = user.bio ?? ''; _dfUserController.text = user.dfUsername ?? ''; _dfPwdController.text = user.dfPassword ?? ''; _lxKeyController.text = user.lxKey ?? ''; setState(() { _isDisagreeRecommend = user.isDisagreeRecommend ?? false; _isDisagreeFriend = user.isDisagreeFriend ?? false; }); } await provider.fetchSexTags(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("用户信息已刷新 ✅")), ); } } Widget _buildProfileHeader( BuildContext context, UserProvider provider, UserModel user) { final isDark = Theme.of(context).brightness == Brightness.dark; return _webCard( child: Row( children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( provider.avatarUrl, width: 80, height: 80, fit: BoxFit.cover, ), ), const SizedBox(width: 20), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( user.name ?? "Unknown", style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold), ), const SizedBox(height: 6), Text( "Rating: ${user.rating ?? 0}", style: TextStyle( fontSize: 15, color: isDark ? Colors.grey[400] : Colors.grey[600]), ), ], ), ), ], ), ); } Widget _buildFloatingBackButton() { return Padding( padding: const EdgeInsets.only(bottom: 100), child: FloatingActionButton( elevation: 6, backgroundColor: Colors.black87, onPressed: () async { await _refreshUserInfo(); if (mounted) { Navigator.maybePop(context); } }, child: const Icon(Icons.refresh, color: Colors.white), ), ); } Widget _buildUserCardSlider(UserProvider provider, UserModel user) { return _webCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("用户卡牌", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 12), SizedBox( height: 340, child: PageView.builder( itemCount: user.userCards!.length, itemBuilder: (context, index) { final card = user.userCards![index]; return _buildSingleCard(card); }, ), ), ], ), ); } Widget _buildSingleCard(dynamic card) { final charaId = card.charaId?.toString().padLeft(6, '0') ?? "000000"; final iconId = card.iconId?.toString().padLeft(6, '0') ?? "000000"; return Container( margin: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(9), image: DecorationImage( image: NetworkImage( "https://cdn.godserver.cn/resource/static/coll/Chara/UI_Chara_$charaId.png", ), fit:BoxFit.cover, colorFilter: ColorFilter.mode( Colors.black.withOpacity(0.4), BlendMode.darken), ), ), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.end, children: [ ClipRRect( borderRadius: BorderRadius.circular(6), child: Image.network( "https://cdn.godserver.cn/resource/static/coll/Icon/UI_Icon_$iconId.png", width: 50, height: 50, ), ), const SizedBox(height: 8), Text( card.username ?? "", style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600), ), Text("Rating: ${card.rating ?? 0}", style: const TextStyle(color: Colors.white70)), ], ), ), ); } Widget _buildSaveButton(UserProvider provider) { return SizedBox( width: double.infinity, height: 50, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.greenAccent.shade400, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), onPressed: () async { final user = provider.user; if (user == null) return; final newUser = UserModel( id: user.id, name: _nameController.text.trim(), userId: user.userId, teamId: user.teamId, email: _emailController.text.trim(), 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: _lxKeyController.text.trim(), dfUsername: _dfUserController.text.trim(), dfPassword: _dfPwdController.text.trim(), 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: _bioController.text.trim(), friendBio: user.friendBio, sex: _sexController.text.trim(), isDisagreeRecommend: _isDisagreeRecommend, isDisagreeFriend: _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: user.segaCards, 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(newUser); try { await provider.saveUserInfo(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("保存成功 ✅")), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("保存失败:${e.toString()}")), ); } } }, child: const Text( "保存修改", style: TextStyle(color: Colors.black87, fontSize: 16), ), ), ); } Widget _buildSegaCard(UserModel user) { return _webCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( "SegaID 账号", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 10), if (user.segaCards != null && user.segaCards!.isNotEmpty) ...user.segaCards!.map((SegaCard card) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( color: (card.type ?? "intl") == "jp" ? Colors.purple : Colors.blue, borderRadius: BorderRadius.circular(4), ), child: Text( card.type == "jp" ? "日服" : "国际服", style: const TextStyle(color: Colors.white, fontSize: 12), ), ), const SizedBox(width: 10), 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) 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 账号"), ), ), ], ), ); } Widget _webCard({required Widget child}) { return Builder( builder: (context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Container( width: double.infinity, decoration: BoxDecoration( color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( color: isDark ? Colors.grey[700]! : Colors.grey[200]!, width: 1), boxShadow: [ BoxShadow( color: Colors.black12.withOpacity(isDark ? 0.3 : 0.08), blurRadius: 12, offset: const Offset(0, 2), ), BoxShadow( color: Colors.greenAccent.withOpacity(0.2), blurRadius: 15, spreadRadius: 3, offset: const Offset(0, 3), ), ], ), padding: const EdgeInsets.all(20), child: child, ); }, ); } Widget _buildSectionCard(List children) { return _webCard(child: Column(children: children)); } Widget _buildCopyableItem( BuildContext context, String label, String? value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: InkWell( borderRadius: BorderRadius.circular(2), onTap: () { if (value == null || value.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("暂无绑定 API Key"))); return; } Clipboard.setData(ClipboardData(text: value)); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("已复制:$value"))); }, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(width: 120, child: Text(label)), Expanded( child: Text( value ?? "-", style: const TextStyle( fontSize: 12, color: Colors.blueAccent, decoration: TextDecoration.underline), ), ), const SizedBox(width: 6), const Icon(Icons.copy, size: 14, color: Colors.grey), ], ), ), ); } Widget _buildItem(String label, String? value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 7), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(width: 120, child: Text(label)), Expanded(child: Text(value ?? "-")), ], ), ); } Widget _buildSwitchItem(String title, bool value, ValueChanged onChanged) { return SwitchListTile( contentPadding: EdgeInsets.zero, title: Text(title), value: value, onChanged: onChanged, ); } Widget _buildLogoutButton(BuildContext context, UserProvider provider) { return SizedBox( width: double.infinity, height: 50, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.redAccent, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), onPressed: () async { await provider.logout(); if (context.mounted) { Navigator.maybePop(context); } }, child: const Text("退出登录", style: TextStyle(color: Colors.white, fontSize: 16)), ), ); } Widget _buildLoginCard({required VoidCallback onTap}) { return Builder( builder: (context) { final isDark = Theme.of(context).brightness == Brightness.dark; final primaryColor = _pinkColor; return Container( width: double.infinity, constraints: const BoxConstraints(maxWidth: 400), padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: isDark ? Colors.grey[850] : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all( color: isDark ? Colors.grey[700]! : Colors.grey[200]!, width: 1, ), boxShadow: [ BoxShadow( color: Colors.pinkAccent.withOpacity(isDark ? 0.4 : 0.1), blurRadius: 20, spreadRadius: 10, offset: const Offset(0, 10), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: primaryColor.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon( Icons.account_circle_outlined, size: 48, color: primaryColor, ), ), const SizedBox(height: 16), Text( "加入Union", style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: isDark ? Colors.white : Colors.black87, ), ), const SizedBox(height: 8), Text( "登录以解锁完整功能", style: TextStyle( fontSize: 14, color: isDark ? Colors.grey[400] : Colors.grey[600], ), textAlign: TextAlign.center, ), const SizedBox(height: 24), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isDark ? Colors.grey[800] : Colors.grey[50], borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildBenefitItem( icon: Icons.sync, title: "多端数据同步", desc: "水鱼/落雪查分器一键绑定", isDark: isDark, ), const Divider(height: 16), _buildBenefitItem( icon: Icons.palette, title: "无需其他查分器即可使用", desc: "不依赖水鱼&落雪,同时支持日服与国际服全套服务", isDark: isDark, ), const Divider(height: 16), _buildBenefitItem( icon: Icons.insights, title: "深度数据分析", desc: "查看 B50,Rating,歌曲详细推分 趋势与详情", isDark: isDark, ), ], ), ), const SizedBox(height: 24), SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: onTap, style: ElevatedButton.styleFrom( backgroundColor: primaryColor, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "立即登录 / 注册", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), SizedBox(width: 8), Icon(Icons.arrow_forward, size: 18), ], ), ), ), ], ), ); }, ); } Widget _buildBenefitItem({ required IconData icon, required String title, required String desc, required bool isDark, }) { return Row( children: [ Icon(icon, size: 20, color: isDark ? Colors.grey[400] : Colors.grey[600]), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black87, ), ), Text( desc, style: TextStyle( fontSize: 12, color: isDark ? Colors.grey[500] : Colors.grey[500], ), ), ], ), ), ], ); } Widget _buildRadarChartSection() { return _webCard( child: Column( children: [ const Text( "能力雷达图", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), const SizedBox(height: 16), if (_radarData == null) TextButton( onPressed: _loadRadarData, child: const Text("点击加载雷达图数据"), ) else SizedBox( height: 300, width: double.infinity, child: CustomRadarChart( data: _radarData!.map((key, value) => MapEntry(key.toString(), double.tryParse(value.toString()) ?? 0.0) ), maxValue: 1.3, lineColor: Colors.grey.shade200, areaColor: Colors.pink.withOpacity(0.15), borderColor: Colors.pinkAccent, ), ), const SizedBox(height: 20), if (_radarData != null) GridView.count( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), crossAxisCount: 3, childAspectRatio: 2.4, crossAxisSpacing: 8, mainAxisSpacing: 8, children: _radarData!.entries.map((e) { final key = e.key.toString(); final value = double.tryParse(e.value.toString()) ?? 0.0; final isHigh = value >= 1.0; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all( color: isHigh ? Colors.pink.shade200 : Colors.grey.shade300, width: 1, ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( key, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), ), Text( value.toStringAsFixed(2), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), ], ), ); }).toList(), ), ], ), ); } } class CustomRadarChart extends StatelessWidget { final Map data; // 维度名 -> 数值 final double maxValue; // 最大值,用于计算比例 final Color lineColor; // 网格线颜色 final Color areaColor; // 填充区域颜色 final Color borderColor; // 边框线条颜色 const CustomRadarChart({ super.key, required this.data, this.maxValue = 1.3, this.lineColor = const Color(0xFFE0E0E0), // 极淡的灰色,几乎看不见 this.areaColor = const Color(0x40FF69B4), // 半透明粉色 this.borderColor = const Color(0xFFEC407A), // 实线粉色 }); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { // 取宽高较小者作为绘图区域,保证是正方形 final size = math.min(constraints.maxWidth, constraints.maxHeight); return SizedBox( width: size, height: size, child: CustomPaint( painter: _RadarPainter( data: data, maxValue: maxValue, lineColor: lineColor, areaColor: areaColor, borderColor: borderColor, ), ), ); }, ); } } class _RadarPainter extends CustomPainter { final Map data; final double maxValue; final Color lineColor; final Color areaColor; final Color borderColor; _RadarPainter({ required this.data, required this.maxValue, required this.lineColor, required this.areaColor, required this.borderColor, }); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); // 半径留一点 padding,防止文字贴边 final radius = math.min(size.width, size.height) / 2 * 0.85; final count = data.length; if (count == 0) return; final angleStep = (2 * math.pi) / count; // --- 1. 绘制背景网格 (同心多边形) --- final gridPaint = Paint() ..color = lineColor ..style = PaintingStyle.stroke ..strokeWidth = 1.0; // 画 5 层网格 (0.2, 0.4, 0.6, 0.8, 1.0 比例) for (int i = 1; i <= 5; i++) { final levelRadius = radius * (i / 5); final path = Path(); for (int j = 0; j < count; j++) { final angle = j * angleStep - math.pi / 2; // -pi/2 让第一个点朝上 final x = center.dx + levelRadius * math.cos(angle); final y = center.dy + levelRadius * math.sin(angle); if (j == 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } } path.close(); canvas.drawPath(path, gridPaint); } // --- 2. 绘制辐射轴线 (从中心到顶点) --- // 如果你连这个轴线也不想要,可以注释掉这一段 for (int i = 0; i < count; i++) { final angle = i * angleStep - math.pi / 2; final x = center.dx + radius * math.cos(angle); final y = center.dy + radius * math.sin(angle); canvas.drawLine( center, Offset(x, y), gridPaint, // 使用同样的淡色 ); } // --- 3. 绘制数据区域 --- final dataPath = Path(); final dataPoints = []; for (int i = 0; i < count; i++) { final key = data.keys.elementAt(i); final value = data[key] ?? 0.0; // 计算当前值的比例半径 final ratio = (value / maxValue).clamp(0.0, 1.0); final currentRadius = radius * ratio; final angle = i * angleStep - math.pi / 2; final x = center.dx + currentRadius * math.cos(angle); final y = center.dy + currentRadius * math.sin(angle); dataPoints.add(Offset(x, y)); if (i == 0) { dataPath.moveTo(x, y); } else { dataPath.lineTo(x, y); } } dataPath.close(); // 填充颜色 final fillPaint = Paint() ..color = areaColor ..style = PaintingStyle.fill; canvas.drawPath(dataPath, fillPaint); // 描边颜色 final borderPaint = Paint() ..color = borderColor ..style = PaintingStyle.stroke ..strokeWidth = 2.0 ..strokeJoin = StrokeJoin.round; // 圆角连接,更美观 canvas.drawPath(dataPath, borderPaint); // --- 4. 绘制顶点小圆点 (可选,增加精致感) --- final dotPaint = Paint() ..color = Colors.white ..style = PaintingStyle.fill; final dotBorderPaint = Paint() ..color = borderColor ..style = PaintingStyle.stroke ..strokeWidth = 1.5; for (var point in dataPoints) { canvas.drawCircle(point, 3.5, dotPaint); canvas.drawCircle(point, 3.5, dotBorderPaint); } final textPainter = TextPainter( textAlign: TextAlign.center, textDirection: TextDirection.ltr, ); for (int i = 0; i < count; i++) { final key = data.keys.elementAt(i); final angle = i * angleStep - math.pi / 2; // 文字位置再往外扩一点 final labelRadius = radius + 20; final x = center.dx + labelRadius * math.cos(angle); final y = center.dy + labelRadius * math.sin(angle); textPainter.text = TextSpan( text: key, style: const TextStyle(fontSize: 12,color: Colors.pinkAccent), ); textPainter.layout(); // 修正文字居中 textPainter.paint(canvas, Offset(x - textPainter.width / 2, y - textPainter.height / 2)); } } @override bool shouldRepaint(covariant _RadarPainter oldDelegate) { return oldDelegate.data != data || oldDelegate.maxValue != maxValue; } } /// 最外层响应结构 class LuoxueResponse { final bool success; final int code; final LuoxuePlayer data; LuoxueResponse({ required this.success, required this.code, required this.data, }); factory LuoxueResponse.fromJson(Map json) { return LuoxueResponse( success: json['success'] ?? false, code: json['code'] ?? 0, data: LuoxuePlayer.fromJson(json['data'] ?? {}), ); } } /// 玩家信息 class LuoxuePlayer { final String name; final int rating; final int friendCode; final int courseRank; final int classRank; final int star; final String uploadTime; final LuoxueTrophy trophy; final LuoxueIcon icon; final LuoxueNamePlate namePlate; final LuoxueFrame frame; LuoxuePlayer({ required this.name, required this.rating, required this.friendCode, required this.courseRank, required this.classRank, required this.star, required this.uploadTime, required this.trophy, required this.icon, required this.namePlate, required this.frame, }); factory LuoxuePlayer.fromJson(Map json) { return LuoxuePlayer( name: json['name'] ?? '', rating: json['rating'] ?? 0, friendCode: json['friend_code'] ?? 0, courseRank: json['course_rank'] ?? 0, classRank: json['class_rank'] ?? 0, star: json['star'] ?? 0, uploadTime: json['upload_time'] ?? '', trophy: LuoxueTrophy.fromJson(json['trophy'] ?? {}), icon: LuoxueIcon.fromJson(json['icon'] ?? {}), namePlate: LuoxueNamePlate.fromJson(json['name_plate'] ?? {}), frame: LuoxueFrame.fromJson(json['frame'] ?? {}), ); } } /// 称号 class LuoxueTrophy { final int id; final String name; final String genre; final String color; LuoxueTrophy({ required this.id, required this.name, required this.genre, required this.color, }); factory LuoxueTrophy.fromJson(Map json) { return LuoxueTrophy( id: json['id'] ?? 0, name: json['name'] ?? '', genre: json['genre'] ?? '', color: json['color'] ?? '', ); } } /// 头像 class LuoxueIcon { final int id; final String name; final String genre; LuoxueIcon({ required this.id, required this.name, required this.genre, }); factory LuoxueIcon.fromJson(Map json) { return LuoxueIcon( id: json['id'] ?? 0, name: json['name'] ?? '', genre: json['genre'] ?? '', ); } } /// 姓名框 class LuoxueNamePlate { final int id; final String name; final String genre; LuoxueNamePlate({ required this.id, required this.name, required this.genre, }); factory LuoxueNamePlate.fromJson(Map json) { return LuoxueNamePlate( id: json['id'] ?? 0, name: json['name'] ?? '', genre: json['genre'] ?? '', ); } } /// 背景 class LuoxueFrame { final int id; final String name; final String genre; LuoxueFrame({ required this.id, required this.name, required this.genre, }); factory LuoxueFrame.fromJson(Map json) { return LuoxueFrame( id: json['id'] ?? 0, name: json['name'] ?? '', genre: json['genre'] ?? '', ); } }