rwadurian/frontend/mining-app/lib/presentation/widgets/team_tree_widget.dart

547 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<TeamTreeNode>? 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<TeamTreeWidget> createState() => _TeamTreeWidgetState();
}
class _TeamTreeWidgetState extends State<TeamTreeWidget> {
// 缓存已加载的节点数据
final Map<String, List<TeamTreeNode>> _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<TeamTreeNode> 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<TeamTreeNode> 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>(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<void> _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<void> _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),
),
),
),
),
],
);
}
}