feat(referral+mobile): 功能6 — App 团队预种数量展示

纯新增实现,不修改任何现有业务逻辑,对现有系统零风险。

## 后端 — referral-service

新建 TeamPrePlantingController(JWT 认证),2 个公开端点:

1. GET /referral/me/team-pre-planting
   - 返回当前用户的个人预种份数、团队预种总量
   - 返回直推成员列表及每人的预种份数
   - 从 TeamStatistics 表读取(CDC 事件维护的数据)

2. GET /referral/me/team-pre-planting/members?limit=20&offset=0
   - 分页返回全部团队成员的预种明细(仅有预种份数 > 0 的成员)
   - 使用 ancestor_path 数组查询所有下级用户
   - JOIN team_statistics 获取每人的 selfPrePlantingPortions

Kong 网关无需修改(/api/v1/referral/* 已覆盖)。

## 前端 — Flutter mobile-app

新建 TeamPrePlantingPage 页面:
- 顶部统计卡片:个人预种 + 团队预种总量
- 直推预种明细列表(所有用户可见)
- 全部团队成员预种明细(仅市/省公司管理者可见,分页加载更多)
- 普通用户看到锁定提示"仅市公司/省公司管理者可查看"

入口:个人中心页预种按钮行新增绿色「团队预种」按钮。

## 文件清单

新建文件:
- backend/.../controllers/team-pre-planting.controller.ts(核心后端控制器)
- frontend/.../pages/team_pre_planting_page.dart(Flutter 团队预种页面)

微量修改(仅追加新行):
- controllers/index.ts: +1 行 export
- api.module.ts: +2 行 import/注册
- api_endpoints.dart: +2 行端点常量
- referral_service.dart: +4 模型类 +2 API 方法
- route_paths.dart, route_names.dart: +1 行路由定义
- app_router.dart: +1 import +1 GoRoute
- profile_page.dart: 预种按钮行追加第三个按钮

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-02 07:15:17 -08:00
parent d3969710be
commit a55201b3b3
10 changed files with 1024 additions and 0 deletions

View File

@ -4,3 +4,4 @@ export * from './internal-team-statistics.controller';
export * from './internal-referral-chain.controller';
export * from './internal-pre-planting-stats.controller';
export * from './health.controller';
export * from './team-pre-planting.controller';

View File

@ -0,0 +1,225 @@
import {
Controller,
Get,
Query,
UseGuards,
Logger,
ParseIntPipe,
DefaultValuePipe,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiQuery,
} from '@nestjs/swagger';
import { JwtAuthGuard } from '../guards';
import { CurrentUser } from '../decorators';
import { PrismaService } from '../../infrastructure/database/prisma.service';
/**
* APIJWT
*
* [6] App
* -
* - /
*
*
*/
@ApiTags('Team Pre-Planting')
@Controller('referral')
export class TeamPrePlantingController {
private readonly logger = new Logger(TeamPrePlantingController.name);
constructor(private readonly prisma: PrismaService) {}
/**
*
*
*
*/
@Get('me/team-pre-planting')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: '获取团队预种统计(含直推明细)' })
@ApiResponse({
status: 200,
description: '团队预种统计',
schema: {
type: 'object',
properties: {
selfPrePlantingPortions: { type: 'number', description: '个人预种份数' },
teamPrePlantingPortions: { type: 'number', description: '团队预种总量(含自己和所有下级)' },
directReferrals: {
type: 'array',
items: {
type: 'object',
properties: {
accountSequence: { type: 'string' },
selfPrePlantingPortions: { type: 'number' },
},
},
},
},
},
})
async getTeamPrePlanting(
@CurrentUser('accountSequence') accountSequence: string,
) {
this.logger.log(`[getTeamPrePlanting] accountSequence=${accountSequence}`);
// 1. 查当前用户的 referral relationship
const relationship = await this.prisma.referralRelationship.findUnique({
where: { accountSequence },
select: { userId: true },
});
if (!relationship) {
return {
selfPrePlantingPortions: 0,
teamPrePlantingPortions: 0,
directReferrals: [],
};
}
const userId = relationship.userId;
// 2. 查当前用户的 TeamStatistics
const myStats = await this.prisma.teamStatistics.findUnique({
where: { userId },
select: {
selfPrePlantingPortions: true,
teamPrePlantingPortions: true,
},
});
// 3. 查直推列表
const directReferrals = await this.prisma.referralRelationship.findMany({
where: { referrerId: userId },
select: { userId: true, accountSequence: true },
});
// 4. 批量查直推的 TeamStatistics
let directReferralStats: Array<{ accountSequence: string; selfPrePlantingPortions: number }> = [];
if (directReferrals.length > 0) {
const directUserIds = directReferrals.map((r) => r.userId);
const directTeamStats = await this.prisma.teamStatistics.findMany({
where: { userId: { in: directUserIds } },
select: { userId: true, selfPrePlantingPortions: true },
});
const userIdToPortions = new Map<bigint, number>();
for (const s of directTeamStats) {
userIdToPortions.set(s.userId, s.selfPrePlantingPortions);
}
directReferralStats = directReferrals.map((r) => ({
accountSequence: r.accountSequence,
selfPrePlantingPortions: userIdToPortions.get(r.userId) ?? 0,
}));
// 按预种份数降序排列
directReferralStats.sort((a, b) => b.selfPrePlantingPortions - a.selfPrePlantingPortions);
}
return {
selfPrePlantingPortions: myStats?.selfPrePlantingPortions ?? 0,
teamPrePlantingPortions: myStats?.teamPrePlantingPortions ?? 0,
directReferrals: directReferralStats,
};
}
/**
*
*
* /使
*
*/
@Get('me/team-pre-planting/members')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: '获取团队成员预种明细(分页)' })
@ApiQuery({ name: 'limit', required: false, type: Number, description: '每页数量 (默认20, 最大100)' })
@ApiQuery({ name: 'offset', required: false, type: Number, description: '偏移量 (默认0)' })
@ApiResponse({
status: 200,
description: '团队成员预种明细',
schema: {
type: 'object',
properties: {
members: {
type: 'array',
items: {
type: 'object',
properties: {
accountSequence: { type: 'string' },
selfPrePlantingPortions: { type: 'number' },
},
},
},
total: { type: 'number' },
hasMore: { type: 'boolean' },
},
},
})
async getTeamPrePlantingMembers(
@CurrentUser('accountSequence') accountSequence: string,
@Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number,
@Query('offset', new DefaultValuePipe(0), ParseIntPipe) offset: number,
) {
this.logger.log(`[getTeamPrePlantingMembers] accountSequence=${accountSequence}, limit=${limit}, offset=${offset}`);
// 限制 limit 范围
const safeLimit = Math.min(Math.max(limit, 1), 100);
// 1. 查当前用户的 userId
const relationship = await this.prisma.referralRelationship.findUnique({
where: { accountSequence },
select: { userId: true },
});
if (!relationship) {
return { members: [], total: 0, hasMore: false };
}
const userId = relationship.userId;
// 2. 用原生 SQL 查 ancestor_path 包含当前 userId 的所有下级
// JOIN team_statistics 获取预种份数,只返回 > 0 的
const countResult = await this.prisma.$queryRaw<[{ count: bigint }]>`
SELECT COUNT(*) as count
FROM referral_relationships rr
JOIN team_statistics ts ON rr.user_id = ts.user_id
WHERE ${userId} = ANY(rr.ancestor_path)
AND ts.self_pre_planting_portions > 0
`;
const total = Number(countResult[0]?.count ?? 0);
if (total === 0) {
return { members: [], total: 0, hasMore: false };
}
const members = await this.prisma.$queryRaw<
Array<{ account_sequence: string; self_pre_planting_portions: number }>
>`
SELECT rr.account_sequence, ts.self_pre_planting_portions
FROM referral_relationships rr
JOIN team_statistics ts ON rr.user_id = ts.user_id
WHERE ${userId} = ANY(rr.ancestor_path)
AND ts.self_pre_planting_portions > 0
ORDER BY ts.self_pre_planting_portions DESC
LIMIT ${safeLimit} OFFSET ${offset}
`;
return {
members: members.map((m) => ({
accountSequence: m.account_sequence,
selfPrePlantingPortions: m.self_pre_planting_portions,
})),
total,
hasMore: offset + safeLimit < total,
};
}
}

View File

@ -9,6 +9,7 @@ import {
InternalReferralChainController,
InternalPrePlantingStatsController,
HealthController,
TeamPrePlantingController,
} from '../api';
import { InternalReferralController } from '../api/controllers/referral.controller';
@ -22,6 +23,7 @@ import { InternalReferralController } from '../api/controllers/referral.controll
InternalPrePlantingStatsController,
HealthController,
InternalReferralController,
TeamPrePlantingController,
],
})
export class ApiModule {}

View File

@ -97,6 +97,10 @@ class ApiEndpoints {
static const String pendingActions = '/user/pending-actions';
static const String pendingActionsComplete = '/user/pending-actions'; // POST /:id/complete
// [2026-03-02] (-> Referral Service)
static const String teamPrePlanting = '$referral/me/team-pre-planting';
static const String teamPrePlantingMembers = '$referral/me/team-pre-planting/members';
// [2026-02-17] (-> Planting Service / PrePlantingModule)
// 1887 USDT/ 10 1
// /planting/*

View File

@ -377,4 +377,141 @@ class ReferralService {
rethrow;
}
}
// ========== [2026-03-02] ==========
///
///
/// GET /referral/me/team-pre-planting (referral-service)
Future<TeamPrePlantingResponse> getTeamPrePlanting() async {
try {
debugPrint('获取团队预种统计...');
final response = await _apiClient.get(ApiEndpoints.teamPrePlanting);
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
debugPrint('团队预种统计获取成功: self=${data['selfPrePlantingPortions']}, team=${data['teamPrePlantingPortions']}');
return TeamPrePlantingResponse.fromJson(data);
}
throw Exception('获取团队预种统计失败');
} catch (e) {
debugPrint('获取团队预种统计失败: $e');
rethrow;
}
}
///
///
/// GET /referral/me/team-pre-planting/members (referral-service)
Future<TeamPrePlantingMembersResponse> getTeamPrePlantingMembers({
int limit = 20,
int offset = 0,
}) async {
try {
debugPrint('获取团队成员预种明细: limit=$limit, offset=$offset');
final response = await _apiClient.get(
ApiEndpoints.teamPrePlantingMembers,
queryParameters: {
'limit': limit,
'offset': offset,
},
);
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
debugPrint('团队成员预种明细获取成功: total=${data['total']}');
return TeamPrePlantingMembersResponse.fromJson(data);
}
throw Exception('获取团队成员预种明细失败');
} catch (e) {
debugPrint('获取团队成员预种明细失败: $e');
rethrow;
}
}
}
// ========== [2026-03-02] ==========
///
class DirectPrePlantingInfo {
final String accountSequence;
final int selfPrePlantingPortions;
DirectPrePlantingInfo({
required this.accountSequence,
required this.selfPrePlantingPortions,
});
factory DirectPrePlantingInfo.fromJson(Map<String, dynamic> json) {
return DirectPrePlantingInfo(
accountSequence: json['accountSequence']?.toString() ?? '',
selfPrePlantingPortions: json['selfPrePlantingPortions'] ?? 0,
);
}
}
///
class TeamPrePlantingResponse {
final int selfPrePlantingPortions;
final int teamPrePlantingPortions;
final List<DirectPrePlantingInfo> directReferrals;
TeamPrePlantingResponse({
required this.selfPrePlantingPortions,
required this.teamPrePlantingPortions,
required this.directReferrals,
});
factory TeamPrePlantingResponse.fromJson(Map<String, dynamic> json) {
return TeamPrePlantingResponse(
selfPrePlantingPortions: json['selfPrePlantingPortions'] ?? 0,
teamPrePlantingPortions: json['teamPrePlantingPortions'] ?? 0,
directReferrals: (json['directReferrals'] as List? ?? [])
.map((e) => DirectPrePlantingInfo.fromJson(e))
.toList(),
);
}
}
///
class TeamMemberPrePlanting {
final String accountSequence;
final int selfPrePlantingPortions;
TeamMemberPrePlanting({
required this.accountSequence,
required this.selfPrePlantingPortions,
});
factory TeamMemberPrePlanting.fromJson(Map<String, dynamic> json) {
return TeamMemberPrePlanting(
accountSequence: json['accountSequence']?.toString() ?? '',
selfPrePlantingPortions: json['selfPrePlantingPortions'] ?? 0,
);
}
}
///
class TeamPrePlantingMembersResponse {
final List<TeamMemberPrePlanting> members;
final int total;
final bool hasMore;
TeamPrePlantingMembersResponse({
required this.members,
required this.total,
required this.hasMore,
});
factory TeamPrePlantingMembersResponse.fromJson(Map<String, dynamic> json) {
return TeamPrePlantingMembersResponse(
members: (json['members'] as List? ?? [])
.map((e) => TeamMemberPrePlanting.fromJson(e))
.toList(),
total: json['total'] ?? 0,
hasMore: json['hasMore'] ?? false,
);
}
}

View File

@ -0,0 +1,597 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/di/injection_container.dart';
import '../../../../core/services/referral_service.dart';
import '../../../../core/services/authorization_service.dart';
/// [2026-03-02] 6
///
///
/// -
/// -
/// - /
class TeamPrePlantingPage extends ConsumerStatefulWidget {
const TeamPrePlantingPage({super.key});
@override
ConsumerState<TeamPrePlantingPage> createState() => _TeamPrePlantingPageState();
}
class _TeamPrePlantingPageState extends ConsumerState<TeamPrePlantingPage> {
//
int _selfPrePlantingPortions = 0;
int _teamPrePlantingPortions = 0;
List<DirectPrePlantingInfo> _directReferrals = [];
//
List<TeamMemberPrePlanting> _teamMembers = [];
int _memberTotal = 0;
bool _memberHasMore = false;
int _memberOffset = 0;
static const int _memberPageSize = 20;
//
bool _isLoading = true;
String? _errorMessage;
bool _hasCompanyRole = false;
bool _isLoadingMembers = false;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final referralService = ref.read(referralServiceProvider);
final authorizationService = ref.read(authorizationServiceProvider);
// +
final results = await Future.wait([
referralService.getTeamPrePlanting(),
authorizationService.getMyAuthorizations(),
]);
final teamPrePlanting = results[0] as TeamPrePlantingResponse;
final authorizations = results[1] as List<AuthorizationResponse>;
// /
final hasRole = authorizations.any((auth) =>
auth.status != AuthorizationStatus.revoked &&
(auth.roleType == RoleType.authCityCompany ||
auth.roleType == RoleType.cityCompany ||
auth.roleType == RoleType.authProvinceCompany ||
auth.roleType == RoleType.provinceCompany));
if (mounted) {
setState(() {
_selfPrePlantingPortions = teamPrePlanting.selfPrePlantingPortions;
_teamPrePlantingPortions = teamPrePlanting.teamPrePlantingPortions;
_directReferrals = teamPrePlanting.directReferrals;
_hasCompanyRole = hasRole;
_isLoading = false;
});
//
if (hasRole) {
_loadTeamMembers();
}
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
_errorMessage = '加载失败: $e';
});
}
}
}
Future<void> _loadTeamMembers() async {
if (_isLoadingMembers) return;
setState(() => _isLoadingMembers = true);
try {
final referralService = ref.read(referralServiceProvider);
final result = await referralService.getTeamPrePlantingMembers(
limit: _memberPageSize,
offset: _memberOffset,
);
if (mounted) {
setState(() {
_teamMembers.addAll(result.members);
_memberTotal = result.total;
_memberHasMore = result.hasMore;
_isLoadingMembers = false;
});
}
} catch (e) {
if (mounted) {
setState(() => _isLoadingMembers = false);
}
debugPrint('加载团队成员预种明细失败: $e');
}
}
Future<void> _loadMoreMembers() async {
if (_isLoadingMembers || !_memberHasMore) return;
_memberOffset += _memberPageSize;
_loadTeamMembers();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFFFF7E6), Color(0xFFEAE0C8)],
),
),
child: SafeArea(
child: Column(
children: [
_buildHeader(),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator(color: Color(0xFFD4AF37)))
: _errorMessage != null
? _buildError()
: RefreshIndicator(
color: const Color(0xFFD4AF37),
onRefresh: () async {
_teamMembers.clear();
_memberOffset = 0;
await _loadData();
},
child: _buildContent(),
),
),
],
),
),
),
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
GestureDetector(
onTap: () => context.pop(),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.arrow_back_ios_new, size: 16, color: Color(0xFF5D4037)),
),
),
const Expanded(
child: Text(
'团队预种',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
fontFamily: 'Inter',
color: Color(0xFF5D4037),
),
),
),
const SizedBox(width: 36),
],
),
);
}
Widget _buildError() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Color(0xFF5D4037)),
const SizedBox(height: 12),
Text(
_errorMessage ?? '加载失败',
style: const TextStyle(fontSize: 14, color: Color(0xFF5D4037)),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
_teamMembers.clear();
_memberOffset = 0;
_loadData();
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFD4AF37),
foregroundColor: Colors.white,
),
child: const Text('重试'),
),
],
),
);
}
Widget _buildContent() {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
const SizedBox(height: 8),
//
_buildStatsCards(),
const SizedBox(height: 20),
//
_buildDirectReferralsSection(),
const SizedBox(height: 20),
//
_buildTeamMembersSection(),
const SizedBox(height: 20),
],
);
}
Widget _buildStatsCards() {
return Row(
children: [
Expanded(
child: _buildStatCard(
'个人预种',
'$_selfPrePlantingPortions',
'',
const Color(0xFFD4AF37),
Icons.person_outline,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'团队预种总量',
'$_teamPrePlantingPortions',
'',
const Color(0xFF2E7D32),
Icons.groups_outlined,
),
),
],
);
}
Widget _buildStatCard(String label, String value, String suffix, Color color, IconData icon) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 18, color: color),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontSize: 13,
fontFamily: 'Inter',
color: color.withValues(alpha: 0.7),
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
fontFamily: 'Inter',
color: color,
),
),
const SizedBox(width: 4),
Text(
suffix,
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
color: color.withValues(alpha: 0.6),
),
),
],
),
],
),
);
}
Widget _buildDirectReferralsSection() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Row(
children: [
const Icon(Icons.people_outline, size: 18, color: Color(0xFFD4AF37)),
const SizedBox(width: 6),
const Text(
'直推预种明细',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
fontFamily: 'Inter',
color: Color(0xFF5D4037),
),
),
const Spacer(),
Text(
'${_directReferrals.length}',
style: const TextStyle(
fontSize: 13,
fontFamily: 'Inter',
color: Color(0xFF8D6E63),
),
),
],
),
),
const Divider(height: 1, color: Color(0xFFF5F0E8)),
if (_directReferrals.isEmpty)
const Padding(
padding: EdgeInsets.all(24),
child: Center(
child: Text(
'暂无直推成员',
style: TextStyle(fontSize: 14, color: Color(0xFFBDBDBD)),
),
),
)
else
..._directReferrals.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return _buildMemberRow(
item.accountSequence,
item.selfPrePlantingPortions,
isLast: index == _directReferrals.length - 1,
);
}),
],
),
);
}
Widget _buildTeamMembersSection() {
if (!_hasCompanyRole) {
//
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE0D6C2), width: 1),
),
child: const Column(
children: [
Icon(Icons.lock_outline, size: 32, color: Color(0xFFBDBDBD)),
SizedBox(height: 8),
Text(
'团队成员预种明细',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
fontFamily: 'Inter',
color: Color(0xFF8D6E63),
),
),
SizedBox(height: 4),
Text(
'仅市公司/省公司管理者可查看完整团队预种ID明细',
style: TextStyle(fontSize: 13, color: Color(0xFFBDBDBD)),
textAlign: TextAlign.center,
),
],
),
);
}
// /
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Row(
children: [
const Icon(Icons.groups_outlined, size: 18, color: Color(0xFF2E7D32)),
const SizedBox(width: 6),
const Text(
'全部团队成员预种',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
fontFamily: 'Inter',
color: Color(0xFF5D4037),
),
),
const Spacer(),
Text(
'$_memberTotal 人有预种',
style: const TextStyle(
fontSize: 13,
fontFamily: 'Inter',
color: Color(0xFF8D6E63),
),
),
],
),
),
const Divider(height: 1, color: Color(0xFFF5F0E8)),
if (_teamMembers.isEmpty && !_isLoadingMembers)
const Padding(
padding: EdgeInsets.all(24),
child: Center(
child: Text(
'暂无团队成员有预种',
style: TextStyle(fontSize: 14, color: Color(0xFFBDBDBD)),
),
),
)
else ...[
..._teamMembers.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return _buildMemberRow(
item.accountSequence,
item.selfPrePlantingPortions,
isLast: index == _teamMembers.length - 1 && !_memberHasMore,
);
}),
if (_isLoadingMembers)
const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFFD4AF37),
),
),
),
)
else if (_memberHasMore)
GestureDetector(
onTap: _loadMoreMembers,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14),
child: const Center(
child: Text(
'加载更多...',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
color: Color(0xFFD4AF37),
fontWeight: FontWeight.w600,
),
),
),
),
),
],
],
),
);
}
Widget _buildMemberRow(String accountSequence, int portions, {bool isLast = false}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: isLast
? null
: const Border(bottom: BorderSide(color: Color(0xFFF5F0E8), width: 0.5)),
),
child: Row(
children: [
//
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: portions > 0
? const Color(0xFFD4AF37).withValues(alpha: 0.1)
: const Color(0xFFBDBDBD).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.person_outline,
size: 18,
color: portions > 0 ? const Color(0xFFD4AF37) : const Color(0xFFBDBDBD),
),
),
const SizedBox(width: 12),
//
Expanded(
child: Text(
accountSequence,
style: const TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Color(0xFF5D4037),
letterSpacing: 0.5,
),
),
),
//
Text(
'$portions',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
fontFamily: 'Inter',
color: portions > 0 ? const Color(0xFFD4AF37) : const Color(0xFFBDBDBD),
),
),
],
),
);
}
}

View File

@ -2124,10 +2124,53 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
),
),
),
const SizedBox(width: 8),
// [2026-03-02] 6
Expanded(
child: GestureDetector(
onTap: _goToTeamPrePlanting,
child: Container(
height: 44,
decoration: BoxDecoration(
color: const Color(0xFF2E7D32).withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: const Color(0xFF2E7D32).withValues(alpha: 0.2),
width: 1,
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.groups_outlined,
color: Color(0xFF2E7D32),
size: 18,
),
SizedBox(width: 6),
Text(
'团队预种',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Color(0xFF2E7D32),
),
),
],
),
),
),
),
],
);
}
/// [2026-03-02] 6
void _goToTeamPrePlanting() {
context.push(RoutePaths.teamPrePlanting);
}
///
Widget _buildMainContentCard() {
// Widget "0"

View File

@ -49,6 +49,8 @@ import '../features/pre_planting/presentation/pages/pre_planting_purchase_page.d
import '../features/pre_planting/presentation/pages/pre_planting_position_page.dart';
import '../features/pre_planting/presentation/pages/pre_planting_merge_detail_page.dart';
import '../features/pre_planting/presentation/pages/pre_planting_merge_signing_page.dart';
// [2026-03-02] 6
import '../features/pre_planting/presentation/pages/team_pre_planting_page.dart';
// [2026-02-19]
import '../features/transfer/presentation/pages/transfer_list_page.dart';
import '../features/transfer/presentation/pages/transfer_detail_page.dart';
@ -477,6 +479,13 @@ final appRouterProvider = Provider<GoRouter>((ref) {
},
),
// [2026-03-02] Team Pre-Planting Page ( - 6)
GoRoute(
path: RoutePaths.teamPrePlanting,
name: RouteNames.teamPrePlanting,
builder: (context, state) => const TeamPrePlantingPage(),
),
// [2026-02-19] Transfer List Page ( - )
GoRoute(
path: RoutePaths.transferList,

View File

@ -61,6 +61,9 @@ class RouteNames {
static const prePlantingMergeDetail = 'pre-planting-merge'; //
static const prePlantingMergeSigning = 'pre-planting-merge-signing'; //
// [2026-03-02] Team Pre-Planting ()
static const teamPrePlanting = 'team-pre-planting';
// [2026-02-19] Transfer ()
static const transferList = 'transfer-list'; //
static const transferDetail = 'transfer-detail'; //

View File

@ -61,6 +61,9 @@ class RoutePaths {
static const prePlantingMergeDetail = '/pre-planting/merge'; //
static const prePlantingMergeSigning = '/pre-planting/merge-signing'; //
// [2026-03-02] Team Pre-Planting ()
static const teamPrePlanting = '/pre-planting/team';
// [2026-02-19] Transfer ()
static const transferList = '/transfer/list'; //
static const transferDetail = '/transfer/detail'; //