fix(admin): correct distributed shares calculation to use 2M pool
The dashboard was incorrectly using 5 billion as the distribution pool default when calculating already distributed shares. The actual mining distribution pool is 2 million shares, not 100 billion. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
20a90fce4c
commit
64ccb8162a
|
|
@ -536,13 +536,14 @@ export class DashboardService {
|
||||||
if (miningResponse.ok) {
|
if (miningResponse.ok) {
|
||||||
const miningResult = await miningResponse.json();
|
const miningResult = await miningResponse.json();
|
||||||
const miningData = miningResult.data || miningResult;
|
const miningData = miningResult.data || miningResult;
|
||||||
// 使用 remainingDistribution 计算已分配
|
// 使用 distributionPool - remainingDistribution 计算已分配
|
||||||
// 总量 50亿 - 剩余 = 已分配
|
// 分配池是 200万(不是100亿),remainingDistribution 是剩余待分配量
|
||||||
|
// 已分配 = 分配池 - 剩余
|
||||||
const distributionPool = new Decimal(
|
const distributionPool = new Decimal(
|
||||||
miningData.distributionPool || '5000000000',
|
miningData.distributionPool || '2000000',
|
||||||
);
|
);
|
||||||
const remaining = new Decimal(
|
const remaining = new Decimal(
|
||||||
miningData.remainingDistribution || '5000000000',
|
miningData.remainingDistribution || '2000000',
|
||||||
);
|
);
|
||||||
totalDistributed = distributionPool.minus(remaining).toString();
|
totalDistributed = distributionPool.minus(remaining).toString();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import '../../data/datasources/remote/auth_remote_datasource.dart';
|
||||||
import '../../data/datasources/remote/mining_remote_datasource.dart';
|
import '../../data/datasources/remote/mining_remote_datasource.dart';
|
||||||
import '../../data/datasources/remote/trading_remote_datasource.dart';
|
import '../../data/datasources/remote/trading_remote_datasource.dart';
|
||||||
import '../../data/datasources/remote/contribution_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/mining_repository_impl.dart';
|
||||||
import '../../data/repositories/trading_repository_impl.dart';
|
import '../../data/repositories/trading_repository_impl.dart';
|
||||||
import '../../data/repositories/contribution_repository_impl.dart';
|
import '../../data/repositories/contribution_repository_impl.dart';
|
||||||
|
|
@ -47,6 +48,11 @@ Future<void> configureDependencies() async {
|
||||||
() => AuthRemoteDataSourceImpl(client: getIt<ApiClient>()),
|
() => AuthRemoteDataSourceImpl(client: getIt<ApiClient>()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Referral Data Source
|
||||||
|
getIt.registerLazySingleton<ReferralRemoteDataSource>(
|
||||||
|
() => ReferralRemoteDataSourceImpl(client: getIt<ApiClient>()),
|
||||||
|
);
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
getIt.registerLazySingleton<MiningRepository>(
|
getIt.registerLazySingleton<MiningRepository>(
|
||||||
() => MiningRepositoryImpl(
|
() => MiningRepositoryImpl(
|
||||||
|
|
|
||||||
|
|
@ -78,4 +78,10 @@ class ApiEndpoints {
|
||||||
|
|
||||||
// Mining Wallet Service 2.0 (Kong路由: /api/v2/mining-wallet)
|
// Mining Wallet Service 2.0 (Kong路由: /api/v2/mining-wallet)
|
||||||
static const String sharePoolBalance = '/api/v2/mining-wallet/pool-accounts/share-pool-balance';
|
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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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_market_page.dart';
|
||||||
import '../../presentation/pages/c2c/c2c_publish_page.dart';
|
import '../../presentation/pages/c2c/c2c_publish_page.dart';
|
||||||
import '../../presentation/pages/c2c/c2c_order_detail_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/widgets/main_shell.dart';
|
||||||
import '../../presentation/providers/user_providers.dart';
|
import '../../presentation/providers/user_providers.dart';
|
||||||
import 'routes.dart';
|
import 'routes.dart';
|
||||||
|
|
@ -145,6 +146,10 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
return C2cOrderDetailPage(orderNo: orderNo);
|
return C2cOrderDetailPage(orderNo: orderNo);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.myTeam,
|
||||||
|
builder: (context, state) => const TeamPage(),
|
||||||
|
),
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
builder: (context, state, child) => MainShell(child: child),
|
builder: (context, state, child) => MainShell(child: child),
|
||||||
routes: [
|
routes: [
|
||||||
|
|
|
||||||
|
|
@ -19,4 +19,6 @@ class Routes {
|
||||||
static const String c2cMarket = '/c2c-market';
|
static const String c2cMarket = '/c2c-market';
|
||||||
static const String c2cPublish = '/c2c-publish';
|
static const String c2cPublish = '/c2c-publish';
|
||||||
static const String c2cOrderDetail = '/c2c-order-detail';
|
static const String c2cOrderDetail = '/c2c-order-detail';
|
||||||
|
// 团队路由
|
||||||
|
static const String myTeam = '/my-team';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<ReferralInfoResponse> getMyReferralInfo();
|
||||||
|
|
||||||
|
/// 获取当前用户直推列表
|
||||||
|
Future<DirectReferralsResponse> getDirectReferrals({
|
||||||
|
int limit = 50,
|
||||||
|
int offset = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 获取指定用户的直推列表(用于伞下树懒加载)
|
||||||
|
Future<DirectReferralsResponse> getUserDirectReferrals({
|
||||||
|
required String accountSequence,
|
||||||
|
int limit = 100,
|
||||||
|
int offset = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReferralRemoteDataSourceImpl implements ReferralRemoteDataSource {
|
||||||
|
final ApiClient client;
|
||||||
|
|
||||||
|
ReferralRemoteDataSourceImpl({required this.client});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ReferralInfoResponse> getMyReferralInfo() async {
|
||||||
|
try {
|
||||||
|
debugPrint('获取推荐信息...');
|
||||||
|
final response = await client.get(ApiEndpoints.referralMe);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = response.data as Map<String, dynamic>;
|
||||||
|
debugPrint('推荐信息获取成功: directReferralCount=${data['directReferralCount']}');
|
||||||
|
return ReferralInfoResponse.fromJson(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception('获取推荐信息失败');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('获取推荐信息失败: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DirectReferralsResponse> 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<String, dynamic>;
|
||||||
|
debugPrint('直推列表获取成功: total=${data['total']}');
|
||||||
|
return DirectReferralsResponse.fromJson(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception('获取直推列表失败');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('获取直推列表失败: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DirectReferralsResponse> 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<String, dynamic>;
|
||||||
|
debugPrint('用户直推列表获取成功: total=${data['total']}');
|
||||||
|
return DirectReferralsResponse.fromJson(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception('获取用户直推列表失败');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('获取用户直推列表失败: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, dynamic> 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<String, dynamic> 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<DirectReferralInfo> referrals;
|
||||||
|
final int total;
|
||||||
|
final bool hasMore;
|
||||||
|
|
||||||
|
DirectReferralsResponse({
|
||||||
|
required this.referrals,
|
||||||
|
required this.total,
|
||||||
|
required this.hasMore,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory DirectReferralsResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return DirectReferralsResponse(
|
||||||
|
referrals: (json['referrals'] as List? ?? [])
|
||||||
|
.map((e) => DirectReferralInfo.fromJson(e))
|
||||||
|
.toList(),
|
||||||
|
total: json['total'] ?? 0,
|
||||||
|
hasMore: json['hasMore'] ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<TeamPage> createState() => _TeamPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TeamPageState extends ConsumerState<TeamPage> {
|
||||||
|
// 设计色彩
|
||||||
|
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<void> _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<ReferralRemoteDataSource>();
|
||||||
|
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<Color>(_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<ReferralRemoteDataSource>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<TeamTreeNode>? 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<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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue