diff --git a/backend/services/mining-admin-service/src/application/services/dashboard.service.ts b/backend/services/mining-admin-service/src/application/services/dashboard.service.ts index 246ea136..c72324d0 100644 --- a/backend/services/mining-admin-service/src/application/services/dashboard.service.ts +++ b/backend/services/mining-admin-service/src/application/services/dashboard.service.ts @@ -536,13 +536,14 @@ export class DashboardService { if (miningResponse.ok) { const miningResult = await miningResponse.json(); const miningData = miningResult.data || miningResult; - // 使用 remainingDistribution 计算已分配 - // 总量 50亿 - 剩余 = 已分配 + // 使用 distributionPool - remainingDistribution 计算已分配 + // 分配池是 200万(不是100亿),remainingDistribution 是剩余待分配量 + // 已分配 = 分配池 - 剩余 const distributionPool = new Decimal( - miningData.distributionPool || '5000000000', + miningData.distributionPool || '2000000', ); const remaining = new Decimal( - miningData.remainingDistribution || '5000000000', + miningData.remainingDistribution || '2000000', ); totalDistributed = distributionPool.minus(remaining).toString(); } diff --git a/frontend/mining-app/lib/core/di/injection.dart b/frontend/mining-app/lib/core/di/injection.dart index 94662c2e..1133d422 100644 --- a/frontend/mining-app/lib/core/di/injection.dart +++ b/frontend/mining-app/lib/core/di/injection.dart @@ -7,6 +7,7 @@ import '../../data/datasources/remote/auth_remote_datasource.dart'; import '../../data/datasources/remote/mining_remote_datasource.dart'; import '../../data/datasources/remote/trading_remote_datasource.dart'; import '../../data/datasources/remote/contribution_remote_datasource.dart'; +import '../../data/datasources/remote/referral_remote_datasource.dart'; import '../../data/repositories/mining_repository_impl.dart'; import '../../data/repositories/trading_repository_impl.dart'; import '../../data/repositories/contribution_repository_impl.dart'; @@ -47,6 +48,11 @@ Future configureDependencies() async { () => AuthRemoteDataSourceImpl(client: getIt()), ); + // Referral Data Source + getIt.registerLazySingleton( + () => ReferralRemoteDataSourceImpl(client: getIt()), + ); + // Repositories getIt.registerLazySingleton( () => MiningRepositoryImpl( diff --git a/frontend/mining-app/lib/core/network/api_endpoints.dart b/frontend/mining-app/lib/core/network/api_endpoints.dart index 85ba9a43..af233be5 100644 --- a/frontend/mining-app/lib/core/network/api_endpoints.dart +++ b/frontend/mining-app/lib/core/network/api_endpoints.dart @@ -78,4 +78,10 @@ class ApiEndpoints { // Mining Wallet Service 2.0 (Kong路由: /api/v2/mining-wallet) static const String sharePoolBalance = '/api/v2/mining-wallet/pool-accounts/share-pool-balance'; + + // Referral Service 2.0 (Kong路由: /api/v2/referral) + static const String referralMe = '/api/v2/referral/me'; + static const String referralDirects = '/api/v2/referral/me/direct-referrals'; + static String userDirectReferrals(String accountSequence) => + '/api/v2/referral/user/$accountSequence/direct-referrals'; } diff --git a/frontend/mining-app/lib/core/router/app_router.dart b/frontend/mining-app/lib/core/router/app_router.dart index b1b78438..586b0435 100644 --- a/frontend/mining-app/lib/core/router/app_router.dart +++ b/frontend/mining-app/lib/core/router/app_router.dart @@ -19,6 +19,7 @@ import '../../presentation/pages/asset/receive_shares_page.dart'; import '../../presentation/pages/c2c/c2c_market_page.dart'; import '../../presentation/pages/c2c/c2c_publish_page.dart'; import '../../presentation/pages/c2c/c2c_order_detail_page.dart'; +import '../../presentation/pages/profile/team_page.dart'; import '../../presentation/widgets/main_shell.dart'; import '../../presentation/providers/user_providers.dart'; import 'routes.dart'; @@ -145,6 +146,10 @@ final appRouterProvider = Provider((ref) { return C2cOrderDetailPage(orderNo: orderNo); }, ), + GoRoute( + path: Routes.myTeam, + builder: (context, state) => const TeamPage(), + ), ShellRoute( builder: (context, state, child) => MainShell(child: child), routes: [ diff --git a/frontend/mining-app/lib/core/router/routes.dart b/frontend/mining-app/lib/core/router/routes.dart index 95977a5b..6968521a 100644 --- a/frontend/mining-app/lib/core/router/routes.dart +++ b/frontend/mining-app/lib/core/router/routes.dart @@ -19,4 +19,6 @@ class Routes { static const String c2cMarket = '/c2c-market'; static const String c2cPublish = '/c2c-publish'; static const String c2cOrderDetail = '/c2c-order-detail'; + // 团队路由 + static const String myTeam = '/my-team'; } diff --git a/frontend/mining-app/lib/data/datasources/remote/referral_remote_datasource.dart b/frontend/mining-app/lib/data/datasources/remote/referral_remote_datasource.dart new file mode 100644 index 00000000..553498ab --- /dev/null +++ b/frontend/mining-app/lib/data/datasources/remote/referral_remote_datasource.dart @@ -0,0 +1,104 @@ +import 'package:flutter/foundation.dart'; +import '../../../core/network/api_client.dart'; +import '../../../core/network/api_endpoints.dart'; +import '../../models/referral_model.dart'; + +abstract class ReferralRemoteDataSource { + /// 获取当前用户推荐信息 + Future getMyReferralInfo(); + + /// 获取当前用户直推列表 + Future getDirectReferrals({ + int limit = 50, + int offset = 0, + }); + + /// 获取指定用户的直推列表(用于伞下树懒加载) + Future getUserDirectReferrals({ + required String accountSequence, + int limit = 100, + int offset = 0, + }); +} + +class ReferralRemoteDataSourceImpl implements ReferralRemoteDataSource { + final ApiClient client; + + ReferralRemoteDataSourceImpl({required this.client}); + + @override + Future getMyReferralInfo() async { + try { + debugPrint('获取推荐信息...'); + final response = await client.get(ApiEndpoints.referralMe); + + if (response.statusCode == 200) { + final data = response.data as Map; + debugPrint('推荐信息获取成功: directReferralCount=${data['directReferralCount']}'); + return ReferralInfoResponse.fromJson(data); + } + + throw Exception('获取推荐信息失败'); + } catch (e) { + debugPrint('获取推荐信息失败: $e'); + rethrow; + } + } + + @override + Future getDirectReferrals({ + int limit = 50, + int offset = 0, + }) async { + try { + debugPrint('获取直推列表...'); + final response = await client.get( + ApiEndpoints.referralDirects, + queryParameters: { + 'limit': limit, + 'offset': offset, + }, + ); + + if (response.statusCode == 200) { + final data = response.data as Map; + debugPrint('直推列表获取成功: total=${data['total']}'); + return DirectReferralsResponse.fromJson(data); + } + + throw Exception('获取直推列表失败'); + } catch (e) { + debugPrint('获取直推列表失败: $e'); + rethrow; + } + } + + @override + Future getUserDirectReferrals({ + required String accountSequence, + int limit = 100, + int offset = 0, + }) async { + try { + debugPrint('获取用户直推列表: accountSequence=$accountSequence'); + final response = await client.get( + ApiEndpoints.userDirectReferrals(accountSequence), + queryParameters: { + 'limit': limit, + 'offset': offset, + }, + ); + + if (response.statusCode == 200) { + final data = response.data as Map; + debugPrint('用户直推列表获取成功: total=${data['total']}'); + return DirectReferralsResponse.fromJson(data); + } + + throw Exception('获取用户直推列表失败'); + } catch (e) { + debugPrint('获取用户直推列表失败: $e'); + rethrow; + } + } +} diff --git a/frontend/mining-app/lib/data/models/referral_model.dart b/frontend/mining-app/lib/data/models/referral_model.dart new file mode 100644 index 00000000..0cfdc58d --- /dev/null +++ b/frontend/mining-app/lib/data/models/referral_model.dart @@ -0,0 +1,104 @@ +/// 推荐信息响应 (来自 referral-service) +class ReferralInfoResponse { + final String userId; + final String referralCode; + final String? referrerId; + final int referralChainDepth; + final int directReferralCount; + final int totalTeamCount; + final int personalPlantingCount; + final int teamPlantingCount; + final double leaderboardScore; + final int? leaderboardRank; + final DateTime createdAt; + + ReferralInfoResponse({ + required this.userId, + required this.referralCode, + this.referrerId, + required this.referralChainDepth, + required this.directReferralCount, + required this.totalTeamCount, + required this.personalPlantingCount, + required this.teamPlantingCount, + required this.leaderboardScore, + this.leaderboardRank, + required this.createdAt, + }); + + factory ReferralInfoResponse.fromJson(Map json) { + return ReferralInfoResponse( + userId: json['userId']?.toString() ?? '', + referralCode: json['referralCode'] ?? '', + referrerId: json['referrerId']?.toString(), + referralChainDepth: json['referralChainDepth'] ?? 0, + directReferralCount: json['directReferralCount'] ?? 0, + totalTeamCount: json['totalTeamCount'] ?? 0, + personalPlantingCount: json['personalPlantingCount'] ?? 0, + teamPlantingCount: json['teamPlantingCount'] ?? 0, + leaderboardScore: (json['leaderboardScore'] ?? 0).toDouble(), + leaderboardRank: json['leaderboardRank'], + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt']) + : DateTime.now(), + ); + } +} + +/// 直推成员信息 +class DirectReferralInfo { + final String userId; + final String accountSequence; // 账户序列号(新格式: D + YYMMDD + 5位序号),用于显示 + final String referralCode; + final int personalPlantingCount; // 个人认种量 + final int teamPlantingCount; // 团队认种量 + final int directReferralCount; // 直推人数(用于判断是否有下级) + final DateTime joinedAt; + + DirectReferralInfo({ + required this.userId, + required this.accountSequence, + required this.referralCode, + required this.personalPlantingCount, + required this.teamPlantingCount, + required this.directReferralCount, + required this.joinedAt, + }); + + factory DirectReferralInfo.fromJson(Map json) { + return DirectReferralInfo( + userId: json['userId']?.toString() ?? '', + accountSequence: json['accountSequence']?.toString() ?? '', + referralCode: json['referralCode'] ?? '', + personalPlantingCount: json['personalPlantingCount'] ?? 0, + teamPlantingCount: json['teamPlantingCount'] ?? 0, + directReferralCount: json['directReferralCount'] ?? 0, + joinedAt: json['joinedAt'] != null + ? DateTime.parse(json['joinedAt']) + : DateTime.now(), + ); + } +} + +/// 直推列表响应 +class DirectReferralsResponse { + final List referrals; + final int total; + final bool hasMore; + + DirectReferralsResponse({ + required this.referrals, + required this.total, + required this.hasMore, + }); + + factory DirectReferralsResponse.fromJson(Map json) { + return DirectReferralsResponse( + referrals: (json['referrals'] as List? ?? []) + .map((e) => DirectReferralInfo.fromJson(e)) + .toList(), + total: json['total'] ?? 0, + hasMore: json['hasMore'] ?? false, + ); + } +} diff --git a/frontend/mining-app/lib/presentation/pages/profile/team_page.dart b/frontend/mining-app/lib/presentation/pages/profile/team_page.dart new file mode 100644 index 00000000..d939dcbb --- /dev/null +++ b/frontend/mining-app/lib/presentation/pages/profile/team_page.dart @@ -0,0 +1,283 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/di/injection.dart'; +import '../../../data/datasources/remote/referral_remote_datasource.dart'; +import '../../widgets/team_tree_widget.dart'; +import '../../providers/user_providers.dart'; +import '../../providers/profile_providers.dart'; + +class TeamPage extends ConsumerStatefulWidget { + const TeamPage({super.key}); + + @override + ConsumerState createState() => _TeamPageState(); +} + +class _TeamPageState extends ConsumerState { + // 设计色彩 + static const Color _orange = Color(0xFFFF6B00); + static const Color _darkText = Color(0xFF1F2937); + static const Color _grayText = Color(0xFF6B7280); + static const Color _bgGray = Color(0xFFF3F4F6); + + TeamTreeNode? _rootNode; + bool _isLoading = true; + String? _error; + + @override + void initState() { + super.initState(); + _loadRootNode(); + } + + Future _loadRootNode() async { + final user = ref.read(userNotifierProvider); + final stats = ref.read(userStatsProvider).valueOrNull; + + if (user.accountSequence == null) { + setState(() { + _isLoading = false; + _error = '用户信息未加载'; + }); + return; + } + + try { + final dataSource = getIt(); + final referralInfo = await dataSource.getMyReferralInfo(); + + setState(() { + _rootNode = TeamTreeNode.createRoot( + accountSequence: user.accountSequence!, + personalPlantingCount: referralInfo.personalPlantingCount, + teamPlantingCount: referralInfo.teamPlantingCount, + directReferralCount: referralInfo.directReferralCount, + ); + _isLoading = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _error = e.toString(); + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: _bgGray, + appBar: AppBar( + title: const Text( + '我的团队', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: _darkText, + ), + ), + backgroundColor: Colors.white, + elevation: 0, + centerTitle: true, + iconTheme: const IconThemeData(color: _darkText), + ), + body: _buildBody(), + ); + } + + Widget _buildBody() { + if (_isLoading) { + return const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(_orange), + ), + ); + } + + if (_error != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 64, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + '加载失败', + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + _error!, + style: TextStyle( + fontSize: 14, + color: Colors.grey[500], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + setState(() { + _isLoading = true; + _error = null; + }); + _loadRootNode(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: _orange, + foregroundColor: Colors.white, + ), + child: const Text('重试'), + ), + ], + ), + ); + } + + if (_rootNode == null) { + return const Center( + child: Text('暂无数据'), + ); + } + + return Column( + children: [ + // 统计信息卡片 + _buildStatsCard(), + // 说明文字 + _buildInstructions(), + // 团队树 + Expanded( + child: Container( + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: TeamTreeWidget( + rootNode: _rootNode!, + referralDataSource: getIt(), + ), + ), + ), + ), + ], + ); + } + + Widget _buildStatsCard() { + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildStatItem( + '个人认种', + '${_rootNode?.personalPlantingCount ?? 0} 棵', + Icons.eco, + ), + _buildDivider(), + _buildStatItem( + '团队认种', + '${_rootNode?.teamPlantingCount ?? 0} 棵', + Icons.groups, + ), + _buildDivider(), + _buildStatItem( + '直推人数', + '${_rootNode?.directReferralCount ?? 0} 人', + Icons.person_add, + ), + ], + ), + ); + } + + Widget _buildStatItem(String label, String value, IconData icon) { + return Column( + children: [ + Icon( + icon, + color: _orange, + size: 24, + ), + const SizedBox(height: 8), + Text( + value, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: _darkText, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: _grayText, + ), + ), + ], + ); + } + + Widget _buildDivider() { + return Container( + width: 1, + height: 50, + color: _bgGray, + ); + } + + Widget _buildInstructions() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: _orange.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: _orange, + size: 18, + ), + const SizedBox(width: 8), + const Expanded( + child: Text( + '点击节点展开/收起下级成员,长按查看详情', + style: TextStyle( + fontSize: 12, + color: _grayText, + ), + ), + ), + ], + ), + ); + } +} diff --git a/frontend/mining-app/lib/presentation/widgets/team_tree_widget.dart b/frontend/mining-app/lib/presentation/widgets/team_tree_widget.dart new file mode 100644 index 00000000..2f6e481f --- /dev/null +++ b/frontend/mining-app/lib/presentation/widgets/team_tree_widget.dart @@ -0,0 +1,550 @@ +import 'package:flutter/material.dart'; +import '../../data/models/referral_model.dart'; +import '../../data/datasources/remote/referral_remote_datasource.dart'; + +/// 伞下树节点数据模型 +class TeamTreeNode { + final String userId; + final String accountSequence; + final int personalPlantingCount; + final int teamPlantingCount; + final int directReferralCount; + List? children; + bool isExpanded; + bool isLoading; + + TeamTreeNode({ + required this.userId, + required this.accountSequence, + required this.personalPlantingCount, + required this.teamPlantingCount, + required this.directReferralCount, + this.children, + this.isExpanded = false, + this.isLoading = false, + }); + + /// 是否有下级 + bool get hasChildren => directReferralCount > 0; + + /// 从 DirectReferralInfo 创建 + factory TeamTreeNode.fromDirectReferralInfo(DirectReferralInfo info) { + return TeamTreeNode( + userId: info.userId, + accountSequence: info.accountSequence, + personalPlantingCount: info.personalPlantingCount, + teamPlantingCount: info.teamPlantingCount, + directReferralCount: info.directReferralCount, + ); + } + + /// 创建根节点(自己) + factory TeamTreeNode.createRoot({ + required String accountSequence, + required int personalPlantingCount, + required int teamPlantingCount, + required int directReferralCount, + }) { + return TeamTreeNode( + userId: '', + accountSequence: accountSequence, + personalPlantingCount: personalPlantingCount, + teamPlantingCount: teamPlantingCount, + directReferralCount: directReferralCount, + ); + } +} + +/// 伞下树组件 +class TeamTreeWidget extends StatefulWidget { + final TeamTreeNode rootNode; + final ReferralRemoteDataSource referralDataSource; + + const TeamTreeWidget({ + super.key, + required this.rootNode, + required this.referralDataSource, + }); + + @override + State createState() => _TeamTreeWidgetState(); +} + +class _TeamTreeWidgetState extends State { + // 缓存已加载的节点数据 + final Map> _childrenCache = {}; + + // 节点框的尺寸 + static const double nodeWidth = 80.0; + static const double nodeHeight = 60.0; + static const double nodeHorizontalSpacing = 12.0; + static const double nodeVerticalSpacing = 40.0; + + // 当前容器宽度(由 LayoutBuilder 传递) + double _containerWidth = 0; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + // 使用实际容器宽度而不是屏幕宽度 + _containerWidth = constraints.maxWidth; + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: _containerWidth, + ), + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: _buildTreeLevel([widget.rootNode], 0), + ), + ), + ), + ), + ); + }, + ); + } + + /// 构建树的一层 + Widget _buildTreeLevel(List nodes, int level) { + if (nodes.isEmpty) return const SizedBox.shrink(); + + // 显示所有节点,允许左右滚动 + return Column( + children: [ + // 当前层的节点 + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < nodes.length; i++) ...[ + _buildNodeWithChildren(nodes[i]), + if (i < nodes.length - 1) SizedBox(width: nodeHorizontalSpacing), + ], + ], + ), + ], + ); + } + + /// 构建单个节点及其子节点 + Widget _buildNodeWithChildren(TeamTreeNode node) { + return Column( + children: [ + // 节点本身 + _buildNodeBox(node), + // 如果展开了且有子节点,显示连线和子节点 + if (node.isExpanded && node.children != null && node.children!.isNotEmpty) ...[ + // 连接线 + CustomPaint( + size: Size(_calculateSubtreeWidth(node.children!), nodeVerticalSpacing), + painter: _TreeLinePainter( + parentWidth: nodeWidth, + childCount: node.children!.length, + childWidth: nodeWidth, + spacing: nodeHorizontalSpacing, + ), + ), + // 子节点层 + _buildTreeLevel(node.children!, 0), + ], + ], + ); + } + + /// 计算子树宽度 + double _calculateSubtreeWidth(List children) { + if (children.isEmpty) return nodeWidth; + return children.length * nodeWidth + (children.length - 1) * nodeHorizontalSpacing; + } + + /// 构建节点框 + Widget _buildNodeBox(TeamTreeNode node) { + return GestureDetector( + onTap: () => _handleNodeTap(node), + onLongPress: () => _showNodeDetails(node), + child: Container( + width: nodeWidth, + height: nodeHeight, + decoration: BoxDecoration( + color: const Color(0xFFFFF5E6), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFFFF6B00), + width: 1, + ), + boxShadow: const [ + BoxShadow( + color: Color(0x1A000000), // 10% opacity black + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: node.isLoading + ? const Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Color(0xFFFF6B00)), + ), + ), + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 序列号(简化显示) + Text( + _formatAccountSequence(node.accountSequence), + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + // 个人/团队认种数 + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + '${node.personalPlantingCount}/${node.teamPlantingCount}', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: Color(0xCC5D4037), + ), + ), + ), + const SizedBox(height: 4), + // 展开/收起按钮 + if (node.hasChildren) + Container( + width: 20, + height: 16, + decoration: BoxDecoration( + color: const Color(0xFFFF6B00), + borderRadius: BorderRadius.circular(4), + ), + child: Center( + child: Text( + node.isExpanded ? '−' : '+', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + height: 1, + ), + ), + ), + ), + ], + ), + ), + ); + } + + /// 格式化账户序列号(简化显示) + String _formatAccountSequence(String seq) { + if (seq.length > 8) { + // D25121300001 -> ...00001 + return '...${seq.substring(seq.length - 5)}'; + } + return seq; + } + + /// 处理节点点击 + Future _handleNodeTap(TeamTreeNode node) async { + if (!node.hasChildren) return; + + if (node.isExpanded) { + // 收起节点 + setState(() { + node.isExpanded = false; + }); + } else { + // 展开节点 + if (node.children != null) { + // 已经加载过,直接展开 + setState(() { + node.isExpanded = true; + }); + } else { + // 需要加载子节点 + await _loadChildren(node); + } + } + } + + /// 加载子节点 + Future _loadChildren(TeamTreeNode node) async { + // 检查缓存 + if (_childrenCache.containsKey(node.accountSequence)) { + setState(() { + node.children = _childrenCache[node.accountSequence]; + node.isExpanded = true; + }); + return; + } + + // 显示加载状态 + setState(() { + node.isLoading = true; + }); + + try { + final response = await widget.referralDataSource.getUserDirectReferrals( + accountSequence: node.accountSequence, + ); + + final children = response.referrals + .map((info) => TeamTreeNode.fromDirectReferralInfo(info)) + .toList(); + + // 缓存结果 + _childrenCache[node.accountSequence] = children; + + if (mounted) { + setState(() { + node.children = children; + node.isExpanded = true; + node.isLoading = false; + }); + } + } catch (e) { + debugPrint('加载子节点失败: $e'); + if (mounted) { + setState(() { + node.isLoading = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('加载失败: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + /// 显示节点详情弹窗 + void _showNodeDetails(TeamTreeNode node) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) => _NodeDetailsSheet( + node: node, + onExpandTap: node.hasChildren + ? () { + Navigator.pop(context); + _handleNodeTap(node); + } + : null, + ), + ); + } +} + +/// 树形连接线绘制器 +class _TreeLinePainter extends CustomPainter { + final double parentWidth; + final int childCount; + final double childWidth; + final double spacing; + + _TreeLinePainter({ + required this.parentWidth, + required this.childCount, + required this.childWidth, + required this.spacing, + }); + + @override + void paint(Canvas canvas, Size size) { + if (childCount == 0) return; + + final paint = Paint() + ..color = const Color(0xFFFF6B00) + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke; + + // 父节点中心点 + final parentCenterX = size.width / 2; + const parentBottomY = 0.0; + + // 中间连接点 + final midY = size.height / 2; + + // 计算子节点位置 + final totalChildrenWidth = childCount * childWidth + (childCount - 1) * spacing; + final startX = (size.width - totalChildrenWidth) / 2; + + // 从父节点向下画垂直线到中间 + canvas.drawLine( + Offset(parentCenterX, parentBottomY), + Offset(parentCenterX, midY), + paint, + ); + + if (childCount == 1) { + // 只有一个子节点,直接画直线到底部 + canvas.drawLine( + Offset(parentCenterX, midY), + Offset(parentCenterX, size.height), + paint, + ); + } else { + // 多个子节点,画水平线 + final firstChildCenterX = startX + childWidth / 2; + final lastChildCenterX = startX + (childCount - 1) * (childWidth + spacing) + childWidth / 2; + + canvas.drawLine( + Offset(firstChildCenterX, midY), + Offset(lastChildCenterX, midY), + paint, + ); + + // 从水平线向下画垂直线到每个子节点 + for (int i = 0; i < childCount; i++) { + final childCenterX = startX + i * (childWidth + spacing) + childWidth / 2; + canvas.drawLine( + Offset(childCenterX, midY), + Offset(childCenterX, size.height), + paint, + ); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +/// 节点详情弹窗 +class _NodeDetailsSheet extends StatelessWidget { + final TeamTreeNode node; + final VoidCallback? onExpandTap; + + const _NodeDetailsSheet({ + required this.node, + this.onExpandTap, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题栏 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '成员详情', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Color(0xFF5D4037), + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: const Icon( + Icons.close, + color: Color(0xFF999999), + size: 24, + ), + ), + ], + ), + const SizedBox(height: 20), + + // 序列号 + _buildDetailRow('序列号', node.accountSequence), + const SizedBox(height: 12), + + // 个人认种数 + _buildDetailRow('个人认种', '${node.personalPlantingCount} 棵'), + const SizedBox(height: 12), + + // 团队认种数 + _buildDetailRow('团队认种', '${node.teamPlantingCount} 棵'), + const SizedBox(height: 12), + + // 直推人数 + _buildDetailRow('直推人数', '${node.directReferralCount} 人'), + const SizedBox(height: 24), + + // 操作按钮 + if (onExpandTap != null) + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: onExpandTap, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF6B00), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + node.isExpanded ? '收起下级' : '展开下级 (${node.directReferralCount}人)', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + + // 底部安全区域 + SizedBox(height: MediaQuery.of(context).padding.bottom + 8), + ], + ), + ); + } + + Widget _buildDetailRow(String label, String value) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF8B5A2B), + ), + ), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + ), + ), + ], + ); + } +}