Files
UnionApp/lib/pages/user/userpage.dart
spasolreisa 9ce601aa8d initial
2026-04-16 14:26:52 +08:00

1626 lines
50 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 'login_page.dart';
class UserPage extends StatefulWidget {
const UserPage({super.key});
@override
State<UserPage> createState() => _UserPageState();
}
class _UserPageState extends State<UserPage> {
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<String, dynamic>? _radarData;
// 获取雷达图方法
Future<void> _loadRadarData() async {
final provider = Provider.of<UserProvider>(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() {
_loadRadarData;
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final provider = Provider.of<UserProvider>(context, listen: false);
provider.fetchSexTags();
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final user = Provider.of<UserProvider>(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();
}
// 2. 添加打开链接的方法
Future<void> _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<void> _verifyShuiyu() async {
final username = _dfUserController.text.trim();
final password = _dfPwdController.text.trim();
// 1. 判空校验
if (username.isEmpty || password.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("请输入水鱼用户名和密码")),
);
return;
}
// 2. 显示加载提示
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("正在验证水鱼账号..."),
duration: Duration(seconds: 1),
),
);
}
try {
// 3. 创建Dio实例发送POST请求
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),
),
);
// 4. 解析返回结果
final Map<String, dynamic> result = response.data;
if (mounted) {
// 登录成功
if (result['message'] == "登录成功") {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("水鱼账号验证成功!"),
backgroundColor: Colors.green,
duration: Duration(seconds: 1),
),
);
// TODO: 验证成功后的逻辑(保存信息、跳转页面等)
} else {
// 账号密码错误
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result['message'] ?? "验证失败"),
duration: Duration(seconds: 1),
backgroundColor: Colors.red,
),
);
}
}
} on DioException catch (e) {
// 5. Dio网络错误处理无网络、超时、服务器异常
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<void> _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,
},
),
);
// ✅ 关键:解析外层响应 + 内层 data
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}"),
),
);
// 在这里可以使用完整的玩家数据
// result.data.name
// result.data.rating
// result.data.friendCode
// result.data.trophy.name
}
} 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<UserProvider>(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),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
final userProvider = Provider.of<UserProvider>(context);
final UserModel? user = userProvider.user;
if (user == null) {
return Scaffold(
appBar: AppBar(title: const Text("用户中心")),
body: Center(
child: Transform.translate(
// Offset(水平, 垂直)
// 垂直向上:负值
offset: const Offset(0, -40), // 往上移 10 像素
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(),
);
}
// 5. 修改 _buildScoreCheckerCard以添加按钮
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: Text(
"舞萌账号" ,
style: const 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<void> _refreshUserInfo() async {
final provider = Provider.of<UserProvider>(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((card) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
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),
Text(card.segaId ?? ""),
],
),
);
}),
if (user.segaCards == null || user.segaCards!.isEmpty)
const Text("暂无 SegaID"),
],
),
);
}
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<Widget> 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<bool> 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: [
// 1. 头部图标与标题
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),
// 2. 登录好处列表 (Value Proposition)
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),
// 3. 登录按钮
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(
// 传入你的数据,确保 Map<String, double>
data: _radarData!.map((key, value) =>
MapEntry(key.toString(), double.tryParse(value.toString()) ?? 0.0)
),
maxValue: 1.3, // 强制最大值为 1.5,不再自动缩放
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: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
);
}).toList(),
),
],
),
);
}
}
class CustomRadarChart extends StatelessWidget {
final Map<String, double> 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<String, double> 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 = <Offset>[];
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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) {
return LuoxueFrame(
id: json['id'] ?? 0,
name: json['name'] ?? '',
genre: json['genre'] ?? '',
);
}
}