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:
parent
3ed17bb4eb
commit
906279ee8a
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue