fix(mobile): 修复用户详情页头像和用户ID显示问题

- 修复avatarUrl为SVG数据时的渲染问题,添加SVG检测逻辑
- 修复后端getUserInfoBySequence响应解析,处理包装格式
- 将用户详情弹窗改为全屏页面,提供更好的用户体验
- 新增统计数据卡片展示直推人数、伞下用户、认种数量
- 改进卡片样式,添加图标和阴影效果

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-24 01:28:28 -08:00
parent 3ed17bb4eb
commit 906279ee8a
3 changed files with 375 additions and 196 deletions

View File

@ -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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\nEOF\n\\)\")"
],
"deny": [],
"ask": []

View File

@ -102,12 +102,14 @@ export class IdentityServiceClient implements OnModuleInit {
try {
this.logger.debug(`[HTTP] GET /internal/users/${accountSequence}`);
const response = await this.httpClient.get<UserInfo>(
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);

View File

@ -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<void> 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<UserProfileDialog> createState() => _UserProfileDialogState();
ConsumerState<UserProfilePage> createState() => _UserProfilePageState();
}
class _UserProfileDialogState extends ConsumerState<UserProfileDialog> {
class _UserProfilePageState extends ConsumerState<UserProfilePage> {
UserProfileResponse? _profile;
bool _isLoading = true;
String? _error;
@ -95,75 +97,124 @@ class _UserProfileDialogState extends ConsumerState<UserProfileDialog> {
@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<Color>(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<UserProfileDialog> {
Widget _buildLoading() {
return const Center(
child: Padding(
padding: EdgeInsets.all(40),
padding: EdgeInsets.all(60),
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
@ -182,28 +233,45 @@ class _UserProfileDialogState extends ConsumerState<UserProfileDialog> {
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<UserProfileDialog> {
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<UserProfileDialog> {
),
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<UserProfileDialog> {
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<UserProfileDialog> {
_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('<svg') || avatarUrl.startsWith('<?xml')) {
return SvgPicture.string(
avatarUrl,
width: size,
height: size,
fit: BoxFit.cover,
);
}
// URL加载
return Image.network(
avatarUrl,
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => _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<UserProfileDialog> {
}) {
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<UserProfileDialog> {
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<UserProfileDialog> {
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<UserProfileDialog> {
child: Text(
value,
style: const TextStyle(
fontSize: 13,
fontSize: 14,
color: Color(0xFF5D4037),
fontWeight: FontWeight.w500,
),
@ -436,32 +564,42 @@ class _UserProfileDialogState extends ConsumerState<UserProfileDialog> {
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> {
}
}
}
/// UserProfileDialog
/// @deprecated 使 UserProfilePage.show()
class UserProfileDialog {
///
static Future<void> show(
BuildContext context, {
required String accountSequence,
String? nickname,
}) {
return UserProfilePage.show(
context,
accountSequence: accountSequence,
nickname: nickname,
);
}
}