Files
UnionApp/lib/pages/user/userpage.dart
spasolreisa fed18d264a 0417 0022
更新
2026-04-17 00:22:43 +08:00

1890 lines
60 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 '../../service/sega_service.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() {
super.initState();
_loadRadarData();
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();
}
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();
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<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),
),
);
} 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<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,
},
),
);
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<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),
],
),
);
},
);
}
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
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: 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<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((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<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: [
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<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'] ?? '',
);
}
}