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:
parent
d3969710be
commit
a55201b3b3
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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<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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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/* 完全独立
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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" 或实际值
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'; // 转让详情
|
||||
|
|
|
|||
|
|
@ -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'; // 转让详情
|
||||
|
|
|
|||
Loading…
Reference in New Issue