diff --git a/backend/services/referral-service/src/api/controllers/index.ts b/backend/services/referral-service/src/api/controllers/index.ts index 246b61d7..359baa55 100644 --- a/backend/services/referral-service/src/api/controllers/index.ts +++ b/backend/services/referral-service/src/api/controllers/index.ts @@ -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'; diff --git a/backend/services/referral-service/src/api/controllers/team-pre-planting.controller.ts b/backend/services/referral-service/src/api/controllers/team-pre-planting.controller.ts new file mode 100644 index 00000000..ccbc2a71 --- /dev/null +++ b/backend/services/referral-service/src/api/controllers/team-pre-planting.controller.ts @@ -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'; + +/** + * 团队预种统计 — 公开 API(JWT 认证) + * + * [功能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(); + 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, + }; + } +} diff --git a/backend/services/referral-service/src/modules/api.module.ts b/backend/services/referral-service/src/modules/api.module.ts index 612c917d..0e9529e9 100644 --- a/backend/services/referral-service/src/modules/api.module.ts +++ b/backend/services/referral-service/src/modules/api.module.ts @@ -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 {} diff --git a/frontend/mobile-app/lib/core/constants/api_endpoints.dart b/frontend/mobile-app/lib/core/constants/api_endpoints.dart index 079e4143..d72a8754 100644 --- a/frontend/mobile-app/lib/core/constants/api_endpoints.dart +++ b/frontend/mobile-app/lib/core/constants/api_endpoints.dart @@ -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/* 完全独立 diff --git a/frontend/mobile-app/lib/core/services/referral_service.dart b/frontend/mobile-app/lib/core/services/referral_service.dart index da1ddf24..e7463b3c 100644 --- a/frontend/mobile-app/lib/core/services/referral_service.dart +++ b/frontend/mobile-app/lib/core/services/referral_service.dart @@ -377,4 +377,141 @@ class ReferralService { rethrow; } } + + // ========== [2026-03-02] 团队预种统计 ========== + + /// 获取团队预种统计(含直推明细) + /// + /// 调用 GET /referral/me/team-pre-planting (referral-service) + Future getTeamPrePlanting() async { + try { + debugPrint('获取团队预种统计...'); + final response = await _apiClient.get(ApiEndpoints.teamPrePlanting); + + if (response.statusCode == 200) { + final data = response.data as Map; + 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 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; + 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 json) { + return DirectPrePlantingInfo( + accountSequence: json['accountSequence']?.toString() ?? '', + selfPrePlantingPortions: json['selfPrePlantingPortions'] ?? 0, + ); + } +} + +/// 团队预种统计响应 +class TeamPrePlantingResponse { + final int selfPrePlantingPortions; + final int teamPrePlantingPortions; + final List directReferrals; + + TeamPrePlantingResponse({ + required this.selfPrePlantingPortions, + required this.teamPrePlantingPortions, + required this.directReferrals, + }); + + factory TeamPrePlantingResponse.fromJson(Map 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 json) { + return TeamMemberPrePlanting( + accountSequence: json['accountSequence']?.toString() ?? '', + selfPrePlantingPortions: json['selfPrePlantingPortions'] ?? 0, + ); + } +} + +/// 团队成员预种明细响应(分页) +class TeamPrePlantingMembersResponse { + final List members; + final int total; + final bool hasMore; + + TeamPrePlantingMembersResponse({ + required this.members, + required this.total, + required this.hasMore, + }); + + factory TeamPrePlantingMembersResponse.fromJson(Map json) { + return TeamPrePlantingMembersResponse( + members: (json['members'] as List? ?? []) + .map((e) => TeamMemberPrePlanting.fromJson(e)) + .toList(), + total: json['total'] ?? 0, + hasMore: json['hasMore'] ?? false, + ); + } } diff --git a/frontend/mobile-app/lib/features/pre_planting/presentation/pages/team_pre_planting_page.dart b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/team_pre_planting_page.dart new file mode 100644 index 00000000..adcdc928 --- /dev/null +++ b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/team_pre_planting_page.dart @@ -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 createState() => _TeamPrePlantingPageState(); +} + +class _TeamPrePlantingPageState extends ConsumerState { + // 统计数据 + int _selfPrePlantingPortions = 0; + int _teamPrePlantingPortions = 0; + List _directReferrals = []; + + // 团队成员明细(分页) + List _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 _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; + + // 检查是否有市/省公司角色 + 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 _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 _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), + ), + ), + ], + ), + ); + } +} diff --git a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart index d0b8a64e..fc720267 100644 --- a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart +++ b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart @@ -2124,10 +2124,53 @@ class _ProfilePageState extends ConsumerState { ), ), ), + 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" 或实际值 diff --git a/frontend/mobile-app/lib/routes/app_router.dart b/frontend/mobile-app/lib/routes/app_router.dart index 8a4a1acc..9bb40d79 100644 --- a/frontend/mobile-app/lib/routes/app_router.dart +++ b/frontend/mobile-app/lib/routes/app_router.dart @@ -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((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, diff --git a/frontend/mobile-app/lib/routes/route_names.dart b/frontend/mobile-app/lib/routes/route_names.dart index 741fe8b6..cccab651 100644 --- a/frontend/mobile-app/lib/routes/route_names.dart +++ b/frontend/mobile-app/lib/routes/route_names.dart @@ -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'; // 转让详情 diff --git a/frontend/mobile-app/lib/routes/route_paths.dart b/frontend/mobile-app/lib/routes/route_paths.dart index 8b4e1900..17d5748a 100644 --- a/frontend/mobile-app/lib/routes/route_paths.dart +++ b/frontend/mobile-app/lib/routes/route_paths.dart @@ -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'; // 转让详情