import 'package:flutter/material.dart'; import '../../data/models/referral_model.dart'; import '../../data/datasources/remote/referral_remote_datasource.dart'; /// 伞下树节点数据模型 class TeamTreeNode { final String accountSequence; final int personalPlantingCount; final int teamPlantingCount; final int directReferralCount; List? children; bool isExpanded; bool isLoading; TeamTreeNode({ 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( 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( 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), ), ), ), ), ], ); } }