diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 35d9cfa7..dd43080b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -331,7 +331,29 @@ "Bash(git revert:*)", "Bash(node -e \"\nconst { ethers } = require\\(''ethers''\\);\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\nconst TO_ADDRESS = ''0x0ec001ed6233b7959d7a251e2792621e4707c35f'';\n\nasync function transfer\\(\\) {\n const provider = new ethers.JsonRpcProvider\\(KAVA_TESTNET_RPC\\);\n const wallet = new ethers.Wallet\\(privateKey, provider\\);\n \n const abi = [''function transfer\\(address to, uint256 amount\\) returns \\(bool\\)'', ''function balanceOf\\(address\\) view returns \\(uint256\\)''];\n const contract = new ethers.Contract\\(USDT_CONTRACT, abi, wallet\\);\n \n // 1,020,000,000 USDT \\(10亿2千万\\) = 1020000000 * 1e6 \\(6 decimals\\)\n const amount = BigInt\\(1020000000\\) * BigInt\\(1000000\\);\n \n console.log\\(''Transferring 1,020,000,000 USDT to'', TO_ADDRESS\\);\n const tx = await contract.transfer\\(TO_ADDRESS, amount, { gasLimit: 100000 }\\);\n console.log\\(''TX Hash:'', tx.hash\\);\n await tx.wait\\(\\);\n \n const newBalance = await contract.balanceOf\\(TO_ADDRESS\\);\n console.log\\(''New balance:'', Number\\(newBalance\\) / 1e6, ''USDT''\\);\n}\n\ntransfer\\(\\).catch\\(e => console.error\\(''Error:'', e.message\\)\\);\n\")", "Bash(node -e \"\nconst { ethers } = require\\(''ethers''\\);\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\nconst TO_ADDRESS = ''0x323AA5bd8101Ad97B724dc1584479219c7660628'';\n\nasync function transfer\\(\\) {\n const provider = new ethers.JsonRpcProvider\\(KAVA_TESTNET_RPC\\);\n const wallet = new ethers.Wallet\\(privateKey, provider\\);\n \n const abi = [''function transfer\\(address to, uint256 amount\\) returns \\(bool\\)'', ''function balanceOf\\(address\\) view returns \\(uint256\\)''];\n const contract = new ethers.Contract\\(USDT_CONTRACT, abi, wallet\\);\n \n // 2,000,000,000 USDT \\(20亿\\) = 2000000000 * 1e6 \\(6 decimals\\)\n const amount = BigInt\\(2000000000\\) * BigInt\\(1000000\\);\n \n console.log\\(''Transferring 2,000,000,000 USDT to'', TO_ADDRESS\\);\n const tx = await contract.transfer\\(TO_ADDRESS, amount, { gasLimit: 100000 }\\);\n console.log\\(''TX Hash:'', tx.hash\\);\n await tx.wait\\(\\);\n \n const newBalance = await contract.balanceOf\\(TO_ADDRESS\\);\n console.log\\(''New balance:'', Number\\(newBalance\\) / 1e6, ''USDT''\\);\n}\n\ntransfer\\(\\).catch\\(e => console.error\\(''Error:'', e.message\\)\\);\n\")", - "Bash(unzip:*)" + "Bash(unzip:*)", + "Bash(node -e \"\nconst { ethers } = require\\(''ethers''\\);\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\nconst TO_ADDRESS = ''0x97Da65a7eCC4bC3EEF8473369b68a1cCda7cDE3f'';\n\nasync function transfer\\(\\) {\n const provider = new ethers.JsonRpcProvider\\(KAVA_TESTNET_RPC\\);\n const wallet = new ethers.Wallet\\(privateKey, provider\\);\n \n const abi = [''function transfer\\(address to, uint256 amount\\) returns \\(bool\\)'', ''function balanceOf\\(address\\) view returns \\(uint256\\)''];\n const contract = new ethers.Contract\\(USDT_CONTRACT, abi, wallet\\);\n \n // 10,000,000,000 USDT \\(100亿\\) = 10000000000 * 1e6 \\(6 decimals\\)\n const amount = BigInt\\(10000000000\\) * BigInt\\(1000000\\);\n \n console.log\\(''Transferring 10,000,000,000 USDT to'', TO_ADDRESS\\);\n const tx = await contract.transfer\\(TO_ADDRESS, amount, { gasLimit: 100000 }\\);\n console.log\\(''TX Hash:'', tx.hash\\);\n await tx.wait\\(\\);\n \n const newBalance = await contract.balanceOf\\(TO_ADDRESS\\);\n console.log\\(''New balance:'', Number\\(newBalance\\) / 1e6, ''USDT''\\);\n}\n\ntransfer\\(\\).catch\\(e => console.error\\(''Error:'', e.message\\)\\);\n\")", + "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(mobile-app\\): 优化数字显示组件防止自动换行\n\n使用 FittedBox\\(fit: BoxFit.scaleDown\\) 包装所有可变数字显示组件,\n确保当数字位数多时自动缩小字号而不是换行,提升用户视觉体验。\n\n优化的页面和组件:\n- stickman_race_widget: 火柴人标签、排名列表数量、进度百分比\n- team_tree_widget: 节点认种数、省略节点数量、详情弹窗\n- ranking_page: 龙虎榜团队认种量\n- trading_page: DST余额、绿积分余额\n- profile_page: 各类收益金额、奖励项金额\n- withdraw_usdt_page: 提款页余额\n- deposit_usdt_page: 充值页余额\n- ledger_detail_page: 净收益、收支概览、流水金额\n- authorization_apply_page: 累计认种数\n- planting_quantity_page: 可用余额\n- mining_page: 用户序列号\n- account_switch_page: 账号用户名、序列号\n- wallet_created_page: 钱包地址信息\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" status)", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" diff backend/services/identity-service/src/application/commands/index.ts)", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" log --oneline -5)", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" add backend/services/identity-service/src/api/dto/request/change-password.dto.ts backend/services/identity-service/src/api/dto/request/index.ts backend/services/identity-service/src/api/controllers/user-account.controller.ts backend/services/identity-service/src/application/commands/index.ts backend/services/identity-service/src/application/services/user-application.service.ts frontend/mobile-app/lib/core/services/auth_event_service.dart frontend/mobile-app/lib/app.dart frontend/mobile-app/lib/core/network/api_client.dart frontend/mobile-app/lib/core/services/account_service.dart frontend/mobile-app/lib/core/services/multi_account_service.dart frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart frontend/mobile-app/lib/features/security/presentation/pages/change_password_page.dart frontend/mobile-app/lib/routes/app_router.dart)", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" commit -m \"$\\(cat <<''EOF''\nfeat\\(auth\\): 实现修改密码API和Token过期自动跳转登录\n\n后端:\n- 新增 ChangePasswordCommand 和 ChangePasswordDto\n- 新增 POST /user/change-password 接口\n- 实现 changePassword\\(\\) 方法,验证旧密码后更新新密码\n\n前端:\n- 新增 AuthEventService 认证事件服务,处理 token 过期事件\n- api_client 在 token 刷新失败时发送过期事件\n- App 监听认证事件,token 过期时清除账号状态并跳转登录页\n- splash_page 优化路由逻辑:退出登录后跳转手机登录页而非向导页\n- change_password_page 调用真实 API 修改密码\n- account_service 新增 changePassword\\(\\) 方法\n- multi_account_service 退出登录时清除 phoneNumber 和 isPasswordSet\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" push)", + "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(email\\): 实现邮箱绑定/解绑功能\n\n后端:\n- 新增 EmailService 邮件发送服务,支持 Gmail SMTP\n- 新增 EmailCode 数据模型用于存储邮箱验证码\n- UserAccount 添加 email 字段\n- 新增 API 接口:\n - GET /user/email-status 获取邮箱绑定状态\n - POST /user/send-email-code 发送邮箱验证码\n - POST /user/bind-email 绑定邮箱\n - POST /user/unbind-email 解绑邮箱\n- 新增 DTOs: SendEmailCodeDto, BindEmailDto, UnbindEmailDto\n- 新增 Commands: SendEmailCodeCommand, BindEmailCommand, UnbindEmailCommand\n\n前端:\n- account_service 新增邮箱相关方法和 EmailStatus 类\n- bind_email_page 更新为使用真实 API:\n - 绑定/更换邮箱功能\n - 独立的解绑验证码输入和倒计时\n - 解绑确认对话框\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" diff backend/services/identity-service/src/infrastructure/external/sms/sms.service.ts)", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" add backend/services/identity-service/src/infrastructure/external/sms/sms.service.ts)", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" commit -m \"$\\(cat <<''EOF''\nfix\\(sms\\): 增强短信发送重试机制\n\n- 最大重试次数从 2 次增加到 4 次\n- 基础延迟从 3 秒增加到 6 秒\n- 最大延迟从 10 秒增加到 30 秒\n\n这些调整提高了短信发送在网络不稳定情况下的成功率\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" diff --stat)", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" log --oneline -3)", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" commit -m \"$\\(cat <<''EOF''\nfeat\\(authorization\\): 实现火柴人排名用户详情查看功能\n\n后端:\n- identity-service: 新增内部API获取用户详情\\(手机号、邮箱、KYC等\\)\n- referral-service: 新增内部API获取用户团队统计\\(直推人数、伞下用户数、认种数量\\)\n- authorization-service: \n - 新增用户公开资料和私密资料API\n - 聚合identity-service和referral-service数据\n - 省团队以上权限可查看私密信息\\(脱敏处理\\)\n\n前端:\n- 新增UserProfileDialog弹窗组件,支持查看用户详情\n- stickman_race_widget: 排名列表项可点击查看用户详情\n- authorization_service: 新增getUserProfile/getUserPrivateProfile方法\n\n用户详情包括:\n- 基本信息: 用户ID、昵称、头像、注册时间、所在地区\n- 团队数据: 推荐人、直推人数、伞下用户数、个人/团队认种数\n- 授权信息: 授权类型、权益激活状态\n- 联系信息\\(特权用户可见\\): 手机号、邮箱、真实姓名\\(脱敏\\)\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" add backend/services/authorization-service/src/application/services/authorization-application.service.ts)", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" commit -m \"$\\(cat <<''EOF''\nfix\\(authorization\\): 暂时禁止所有用户查看私密资料\n\n由于系统尚未实现权限管理功能,暂时将 checkPrivateProfileAccess\n始终返回 false,禁止所有用户查看其他用户的手机号、邮箱等隐私信息。\n\n后续实现权限系统后可恢复原有逻辑。\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" status --short)", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" add backend/services/identity-service/src/api/controllers/internal.controller.ts)", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" commit -m \"$\\(cat <<''EOF''\nfix\\(identity\\): 使用Prisma直接查询用户详情\n\ngetUserDetailBySequence 方法改用 Prisma 直接查询数据库,\n以获取 email 和 realName 等领域模型中未暴露的字段。\n\n之前的实现通过领域模型 UserAccount 访问这些字段会导致编译错误,\n因为领域模型没有直接暴露这些属性。\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" add backend/api-gateway/kong.yml frontend/mobile-app/lib/core/services/notification_service.dart)", + "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" commit -m \"$\\(cat <<''EOF''\nfix\\(notification\\): 修复通知中心API路径\n\n问题: 前端调用 /admin-service/mobile/notifications 路径不存在于Kong网关\n\n修复:\n1. Kong网关添加 /api/v1/mobile/notifications 路由到 admin-service\n2. 前端 NotificationService 修正 API 路径:\n - /admin-service/mobile/notifications -> /mobile/notifications\n - /admin-service/mobile/notifications/unread-count -> /mobile/notifications/unread-count\n - /admin-service/mobile/notifications/mark-read -> /mobile/notifications/mark-read\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")" ], "deny": [], "ask": [] diff --git a/backend/services/authorization-service/src/infrastructure/external/identity-service.client.ts b/backend/services/authorization-service/src/infrastructure/external/identity-service.client.ts index 60438965..7a0994c1 100644 --- a/backend/services/authorization-service/src/infrastructure/external/identity-service.client.ts +++ b/backend/services/authorization-service/src/infrastructure/external/identity-service.client.ts @@ -102,12 +102,14 @@ export class IdentityServiceClient implements OnModuleInit { try { this.logger.debug(`[HTTP] GET /internal/users/${accountSequence}`); - const response = await this.httpClient.get( + const response = await this.httpClient.get<{ success: boolean; data: UserInfo } | UserInfo>( `/api/v1/internal/users/${accountSequence}`, ); - if (response.data) { - return response.data; + // 处理可能的两种响应格式(带包装或不带包装) + const data = (response.data as any)?.data || response.data; + if (data) { + return data as UserInfo; } } catch (error) { this.logger.error(`[HTTP] Failed to get user info for ${accountSequence}:`, error); diff --git a/frontend/mobile-app/lib/features/authorization/presentation/widgets/user_profile_dialog.dart b/frontend/mobile-app/lib/features/authorization/presentation/widgets/user_profile_dialog.dart index 068757de..ec7644d9 100644 --- a/frontend/mobile-app/lib/features/authorization/presentation/widgets/user_profile_dialog.dart +++ b/frontend/mobile-app/lib/features/authorization/presentation/widgets/user_profile_dialog.dart @@ -1,43 +1,45 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:intl/intl.dart'; import '../../../../core/services/authorization_service.dart'; import '../../../../core/di/injection_container.dart'; -/// 用户资料详情弹窗 -class UserProfileDialog extends ConsumerStatefulWidget { +/// 用户资料详情页面 +class UserProfilePage extends ConsumerStatefulWidget { /// 目标用户的账户序列号 final String accountSequence; /// 用户昵称(用于显示加载中标题) final String? nickname; - const UserProfileDialog({ + const UserProfilePage({ super.key, required this.accountSequence, this.nickname, }); - /// 显示用户资料弹窗 + /// 导航到用户资料页面 static Future show( BuildContext context, { required String accountSequence, String? nickname, }) { - return showDialog( - context: context, - builder: (context) => UserProfileDialog( - accountSequence: accountSequence, - nickname: nickname, + return Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => UserProfilePage( + accountSequence: accountSequence, + nickname: nickname, + ), ), ); } @override - ConsumerState createState() => _UserProfileDialogState(); + ConsumerState createState() => _UserProfilePageState(); } -class _UserProfileDialogState extends ConsumerState { +class _UserProfilePageState extends ConsumerState { UserProfileResponse? _profile; bool _isLoading = true; String? _error; @@ -95,75 +97,124 @@ class _UserProfileDialogState extends ConsumerState { @override Widget build(BuildContext context) { - return Dialog( - backgroundColor: Colors.transparent, - insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), - child: Container( - constraints: const BoxConstraints(maxWidth: 400, maxHeight: 600), - decoration: BoxDecoration( - color: const Color(0xFFF5F0E6), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.2), - blurRadius: 20, - offset: const Offset(0, 10), + return Scaffold( + backgroundColor: const Color(0xFFF5F0E6), + body: CustomScrollView( + slivers: [ + // 自定义AppBar + SliverAppBar( + expandedHeight: 200, + pinned: true, + backgroundColor: const Color(0xFFD4AF37), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildHeader(), - Flexible( - child: _isLoading - ? _buildLoading() - : _error != null - ? _buildError() - : _buildContent(), + flexibleSpace: FlexibleSpaceBar( + background: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFFD4AF37), Color(0xFFB8860B)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: _isLoading || _profile == null + ? const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : _buildHeaderContent(), + ), ), - ], - ), + ), + // 内容区域 + SliverToBoxAdapter( + child: _isLoading + ? _buildLoading() + : _error != null + ? _buildError() + : _buildContent(), + ), + ], ), ); } - Widget _buildHeader() { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Color(0xFFD4AF37), Color(0xFFB8860B)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Row( - children: [ - const Icon(Icons.person, color: Colors.white, size: 24), - const SizedBox(width: 12), - Expanded( - child: Text( - widget.nickname ?? '用户详情', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, + Widget _buildHeaderContent() { + final profile = _profile!; + return SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 60, 20, 20), + child: Row( + children: [ + // 头像 + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 3, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipOval( + child: _buildAvatar(profile.avatarUrl, size: 80), ), - overflow: TextOverflow.ellipsis, ), - ), - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.close, color: Colors.white), - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ], + const SizedBox(width: 20), + // 昵称和标签 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + profile.nickname, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 6, + runSpacing: 4, + children: profile.authorizations.map((auth) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: auth.benefitActive + ? Colors.white.withValues(alpha: 0.3) + : Colors.white.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + auth.displayTitle, + style: const TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ); + }).toList(), + ), + ], + ), + ), + ], + ), ), ); } @@ -171,7 +222,7 @@ class _UserProfileDialogState extends ConsumerState { Widget _buildLoading() { return const Center( child: Padding( - padding: EdgeInsets.all(40), + padding: EdgeInsets.all(60), child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Color(0xFFD4AF37)), ), @@ -182,28 +233,45 @@ class _UserProfileDialogState extends ConsumerState { Widget _buildError() { return Center( child: Padding( - padding: const EdgeInsets.all(24), + padding: const EdgeInsets.all(40), child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon( - Icons.error_outline, - color: Colors.red, - size: 48, + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.error_outline, + color: Colors.red, + size: 40, + ), ), - const SizedBox(height: 16), + const SizedBox(height: 20), Text( _error ?? '加载失败', style: const TextStyle( - fontSize: 14, + fontSize: 16, color: Color(0xFF5D4037), ), textAlign: TextAlign.center, ), - const SizedBox(height: 16), - TextButton( + const SizedBox(height: 24), + ElevatedButton.icon( onPressed: _loadProfile, - child: const Text('重试'), + icon: const Icon(Icons.refresh), + label: const Text('重试'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFD4AF37), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), ), ], ), @@ -213,18 +281,19 @@ class _UserProfileDialogState extends ConsumerState { Widget _buildContent() { final profile = _profile!; - return SingleChildScrollView( + return Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 用户头像和基本信息 - _buildUserHeader(profile), + // 统计数据卡片 + _buildStatsCard(profile), const SizedBox(height: 20), // 基本信息卡片 _buildInfoCard( title: '基本信息', + icon: Icons.info_outline, children: [ _buildInfoRow('用户ID', profile.accountSequence), _buildInfoRow('注册时间', _formatDate(profile.registeredAtDateTime)), @@ -236,22 +305,11 @@ class _UserProfileDialogState extends ConsumerState { ), const SizedBox(height: 16), - // 团队数据卡片 - _buildInfoCard( - title: '团队数据', - children: [ - _buildInfoRow('直推人数', '${profile.directReferralCount} 人'), - _buildInfoRow('伞下用户', '${profile.umbrellaUserCount} 人'), - _buildInfoRow('个人认种', '${profile.personalPlantingCount} 棵'), - _buildInfoRow('团队认种', '${profile.teamPlantingCount} 棵'), - ], - ), - const SizedBox(height: 16), - // 授权信息 if (profile.authorizations.isNotEmpty) ...[ _buildInfoCard( title: '授权信息', + icon: Icons.verified_user_outlined, children: profile.authorizations.map((auth) { return _buildAuthorizationRow(auth); }).toList(), @@ -263,7 +321,7 @@ class _UserProfileDialogState extends ConsumerState { if (_hasPrivilege && profile.hasPrivateInfo) _buildInfoCard( title: '联系信息', - icon: Icons.lock, + icon: Icons.lock_outline, children: [ if (profile.phoneNumber != null) _buildInfoRow('手机号', profile.phoneNumber!), @@ -275,89 +333,145 @@ class _UserProfileDialogState extends ConsumerState { _buildInfoRow('KYC状态', _formatKycStatus(profile.kycStatus!)), ], ), + + const SizedBox(height: 40), ], ), ); } - Widget _buildUserHeader(UserProfileResponse profile) { - return Row( - children: [ - // 头像 - Container( - width: 64, - height: 64, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: const Color(0xFFD4AF37), - width: 2, - ), + Widget _buildStatsCard(UserProfileResponse profile) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), ), - child: ClipOval( - child: profile.avatarUrl != null - ? Image.network( - profile.avatarUrl!, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => _buildDefaultAvatar(), - ) - : _buildDefaultAvatar(), + ], + ), + child: Row( + children: [ + _buildStatItem( + icon: Icons.people_outline, + label: '直推人数', + value: '${profile.directReferralCount}', + color: const Color(0xFF4CAF50), ), - ), - const SizedBox(width: 16), - // 昵称和标签 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - profile.nickname, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Color(0xFF5D4037), - ), - ), - const SizedBox(height: 4), - Wrap( - spacing: 6, - runSpacing: 4, - children: profile.authorizations.map((auth) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: auth.benefitActive - ? const Color(0xFFD4AF37).withValues(alpha: 0.2) - : Colors.grey.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - auth.displayTitle, - style: TextStyle( - fontSize: 11, - color: auth.benefitActive - ? const Color(0xFFD4AF37) - : Colors.grey, - fontWeight: FontWeight.w500, - ), - ), - ); - }).toList(), - ), - ], + _buildStatDivider(), + _buildStatItem( + icon: Icons.account_tree_outlined, + label: '伞下用户', + value: '${profile.umbrellaUserCount}', + color: const Color(0xFF2196F3), ), - ), - ], + _buildStatDivider(), + _buildStatItem( + icon: Icons.eco_outlined, + label: '个人认种', + value: '${profile.personalPlantingCount}', + color: const Color(0xFF8BC34A), + ), + _buildStatDivider(), + _buildStatItem( + icon: Icons.forest_outlined, + label: '团队认种', + value: '${profile.teamPlantingCount}', + color: const Color(0xFFD4AF37), + ), + ], + ), ); } - Widget _buildDefaultAvatar() { + Widget _buildStatItem({ + required IconData icon, + required String label, + required String value, + required Color color, + }) { + return Expanded( + child: Column( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon(icon, color: color, size: 20), + ), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF8B7355), + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + Widget _buildStatDivider() { return Container( + width: 1, + height: 50, + color: const Color(0xFFE0E0E0), + ); + } + + /// 构建头像 - 支持SVG数据、URL或默认头像 + Widget _buildAvatar(String? avatarUrl, {double size = 64}) { + if (avatarUrl == null || avatarUrl.isEmpty) { + return _buildDefaultAvatar(size: size); + } + + // 检测是否是SVG字符串 + if (avatarUrl.contains(' _buildDefaultAvatar(size: size), + ); + } + + Widget _buildDefaultAvatar({double size = 64}) { + return Container( + width: size, + height: size, color: const Color(0xFFD4AF37).withValues(alpha: 0.3), - child: const Icon( + child: Icon( Icons.person, - size: 32, - color: Color(0xFFD4AF37), + size: size * 0.5, + color: const Color(0xFFD4AF37), ), ); } @@ -369,13 +483,17 @@ class _UserProfileDialogState extends ConsumerState { }) { return Container( width: double.infinity, - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.8), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xFFD4AF37).withValues(alpha: 0.3), - ), + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -383,20 +501,30 @@ class _UserProfileDialogState extends ConsumerState { Row( children: [ if (icon != null) ...[ - Icon(icon, size: 16, color: const Color(0xFFD4AF37)), - const SizedBox(width: 6), + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: const Color(0xFFD4AF37).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, size: 18, color: const Color(0xFFD4AF37)), + ), + const SizedBox(width: 12), ], Text( title, style: const TextStyle( - fontSize: 14, + fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF5D4037), ), ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 16), + const Divider(height: 1, color: Color(0xFFEEEEEE)), + const SizedBox(height: 16), ...children, ], ), @@ -405,16 +533,16 @@ class _UserProfileDialogState extends ConsumerState { Widget _buildInfoRow(String label, String value) { return Padding( - padding: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.only(bottom: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - width: 80, + width: 90, child: Text( label, style: const TextStyle( - fontSize: 13, + fontSize: 14, color: Color(0xFF8B7355), ), ), @@ -423,7 +551,7 @@ class _UserProfileDialogState extends ConsumerState { child: Text( value, style: const TextStyle( - fontSize: 13, + fontSize: 14, color: Color(0xFF5D4037), fontWeight: FontWeight.w500, ), @@ -436,32 +564,42 @@ class _UserProfileDialogState extends ConsumerState { Widget _buildAuthorizationRow(UserAuthorizationInfo auth) { return Padding( - padding: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ Container( - width: 8, - height: 8, + width: 10, + height: 10, decoration: BoxDecoration( shape: BoxShape.circle, color: auth.benefitActive ? Colors.green : Colors.grey, ), ), - const SizedBox(width: 8), + const SizedBox(width: 12), Expanded( child: Text( auth.displayTitle, style: const TextStyle( - fontSize: 13, + fontSize: 14, color: Color(0xFF5D4037), ), ), ), - Text( - auth.benefitActive ? '权益激活' : '权益未激活', - style: TextStyle( - fontSize: 12, - color: auth.benefitActive ? Colors.green : Colors.grey, + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: auth.benefitActive + ? Colors.green.withValues(alpha: 0.1) + : Colors.grey.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + auth.benefitActive ? '权益激活' : '权益未激活', + style: TextStyle( + fontSize: 12, + color: auth.benefitActive ? Colors.green : Colors.grey, + fontWeight: FontWeight.w500, + ), ), ), ], @@ -486,3 +624,20 @@ class _UserProfileDialogState extends ConsumerState { } } } + +/// 为了向后兼容,保留 UserProfileDialog 作为别名 +/// @deprecated 请使用 UserProfilePage.show() 替代 +class UserProfileDialog { + /// 显示用户资料页面 + static Future show( + BuildContext context, { + required String accountSequence, + String? nickname, + }) { + return UserProfilePage.show( + context, + accountSequence: accountSequence, + nickname: nickname, + ); + } +}