diff --git a/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_position_page.dart b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_position_page.dart index 037dd47c..05f3640b 100644 --- a/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_position_page.dart +++ b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_position_page.dart @@ -1,16 +1,825 @@ 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/pre_planting_service.dart'; +import '../../../../routes/route_paths.dart'; -/// [2026-02-17] 预种持仓页面(占位 - 待完整实现) +// ============================================ +// [2026-02-17] 预种持仓页面 +// ============================================ +// +// 显示用户的预种计划持仓信息,包括: +// - 持仓概览卡片:累计份数、待合并份数、已合成树数 +// - 合并进度条:当前 N/5 份的进度可视化 +// - 订单列表:所有预种订单记录(按时间倒序) +// - 合并记录列表:已完成合并的树记录(可点击查看详情/签约) +// +// === 页面结构 === +// ┌─ Header(顶部导航) +// ├─ Position Summary(持仓概览 + 进度条) +// ├─ Tab: 预种订单 / 合并记录 +// └─ ListView(订单或合并记录卡片列表) +// +// === 与购买页面的关系 === +// 本页面纯展示,不含购买操作。 +// 购买入口在 Profile 页面或本页顶部按钮跳转到 PrePlantingPurchasePage。 + +/// 预种持仓页面 /// -/// 功能:显示累计份数、合并进度条(N/5)、已合成树数、订单列表 -class PrePlantingPositionPage extends StatelessWidget { +/// 展示用户的预种持仓概览、合并进度、订单列表和合并记录。 +class PrePlantingPositionPage extends ConsumerStatefulWidget { const PrePlantingPositionPage({super.key}); + @override + ConsumerState createState() => + _PrePlantingPositionPageState(); +} + +class _PrePlantingPositionPageState + extends ConsumerState + with SingleTickerProviderStateMixin { + // === 常量 === + static const int _portionsPerTree = 5; + + // === 状态 === + + /// 持仓信息 + PrePlantingPosition? _position; + + /// 预种订单列表 + List _orders = []; + + /// 合并记录列表 + List _merges = []; + + /// 是否正在加载 + bool _isLoading = true; + + /// 加载错误信息 + String? _errorMessage; + + /// Tab 控制器(订单 / 合并记录) + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + _loadData(); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + // ============================================ + // 数据加载 + // ============================================ + + /// 并行加载持仓、订单、合并记录 + Future _loadData() async { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + final service = ref.read(prePlantingServiceProvider); + + final results = await Future.wait([ + service.getMyPosition().catchError((_) => PrePlantingPosition( + totalPortions: 0, + availablePortions: 0, + mergedPortions: 0, + totalTreesMerged: 0, + )), + service.getMyOrders(), + service.getMyMerges(), + ]); + + setState(() { + _position = results[0] as PrePlantingPosition; + _orders = results[1] as List; + _merges = results[2] as List; + _isLoading = false; + }); + } catch (e) { + debugPrint('[PrePlantingPosition] 加载数据失败: $e'); + setState(() { + _isLoading = false; + _errorMessage = '加载数据失败,请重试'; + }); + } + } + + /// 返回上一页 + void _goBack() { + context.pop(); + } + + /// 跳转到购买页面 + void _goToPurchase() { + context.push(RoutePaths.prePlantingPurchase); + } + + /// 跳转到合并详情页 + void _goToMergeDetail(String mergeNo) { + context.push('${RoutePaths.prePlantingMergeDetail}/$mergeNo'); + } + + // ============================================ + // UI 构建 + // ============================================ + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('预种持仓')), - body: const Center(child: Text('预种持仓页 - 开发中')), + body: Container( + width: double.infinity, + height: double.infinity, + 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 + ? _buildLoadingState() + : _errorMessage != null + ? _buildErrorState() + : _buildContent(), + ), + ], + ), + ), + ), ); } + + /// 顶部导航栏 + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFFFF7E6).withValues(alpha: 0.8), + ), + child: Row( + children: [ + GestureDetector( + onTap: _goBack, + child: Container( + width: 32, + height: 32, + alignment: Alignment.center, + child: const Icon( + Icons.arrow_back_ios, + color: Color(0xFFD4AF37), + size: 20, + ), + ), + ), + const SizedBox(width: 4), + GestureDetector( + onTap: _goBack, + child: const Text( + '返回', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + height: 1.5, + color: Color(0xFFD4AF37), + ), + ), + ), + const SizedBox(width: 42), + const Expanded( + child: Text( + '预种持仓', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 1.25, + letterSpacing: -0.27, + color: Color(0xFF5D4037), + ), + ), + ), + // 购买按钮 + GestureDetector( + onTap: _goToPurchase, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFD4AF37), + borderRadius: BorderRadius.circular(16), + ), + child: const Text( + '购买', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ); + } + + /// 加载中状态 + Widget _buildLoadingState() { + return const Center( + child: CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xFFD4AF37), + ), + ); + } + + /// 错误状态 + Widget _buildErrorState() { + return Center( + child: GestureDetector( + onTap: _loadData, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline, color: Color(0xFFE65100), size: 48), + const SizedBox(height: 16), + Text( + _errorMessage!, + style: const TextStyle(fontSize: 16, color: Color(0xFFE65100)), + ), + const SizedBox(height: 8), + const Text( + '点击重试', + style: TextStyle( + fontSize: 14, + color: Color(0xFFD4AF37), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } + + /// 主内容区域 + Widget _buildContent() { + return RefreshIndicator( + onRefresh: _loadData, + color: const Color(0xFFD4AF37), + child: Column( + children: [ + // 持仓概览 + 进度条 + _buildPositionSummary(), + const SizedBox(height: 8), + // Tab 切换 + _buildTabBar(), + // Tab 内容 + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildOrdersList(), + _buildMergesList(), + ], + ), + ), + ], + ), + ); + } + + /// 持仓概览卡片 + Widget _buildPositionSummary() { + final pos = _position!; + final available = pos.availablePortions; + final progress = available / _portionsPerTree; + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0x99FFFFFF), + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x1A000000), + blurRadius: 6, + offset: Offset(0, 4), + ), + BoxShadow( + color: Color(0x1A000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 三列统计数据 + Row( + children: [ + _buildStatItem('累计份数', '${pos.totalPortions}'), + _buildStatDivider(), + _buildStatItem('待合并', '$available'), + _buildStatDivider(), + _buildStatItem('已合成树', '${pos.totalTreesMerged}'), + ], + ), + const SizedBox(height: 16), + // 合并进度条 + const Text( + '合并进度', + style: TextStyle( + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + color: Color(0xFF745D43), + ), + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: progress, + minHeight: 10, + backgroundColor: const Color(0xFFEAE0C8), + valueColor: + const AlwaysStoppedAnimation(Color(0xFFD4AF37)), + ), + ), + const SizedBox(height: 6), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$available / $_portionsPerTree 份', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + Text( + available == 0 + ? '开始购买即可积累' + : '还需 ${_portionsPerTree - available} 份合成 1 棵树', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF745D43), + ), + ), + ], + ), + // 省市信息(如有) + if (pos.hasProvinceCity) ...[ + const SizedBox(height: 12), + Row( + children: [ + const Icon( + Icons.location_on_outlined, + color: Color(0xFF745D43), + size: 16, + ), + const SizedBox(width: 4), + Text( + '${pos.provinceName ?? pos.provinceCode} · ${pos.cityName ?? pos.cityCode}', + style: const TextStyle( + fontSize: 13, + color: Color(0xFF745D43), + ), + ), + ], + ), + ], + ], + ), + ), + ); + } + + /// 统计项 + Widget _buildStatItem(String label, String value) { + return Expanded( + child: Column( + children: [ + Text( + value, + style: const TextStyle( + fontSize: 28, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 4), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF745D43), + ), + ), + ], + ), + ); + } + + /// 统计项分隔线 + Widget _buildStatDivider() { + return Container( + width: 1, + height: 40, + color: const Color(0x338B5A2B), + ); + } + + /// Tab 栏 + Widget _buildTabBar() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Color(0x338B5A2B), width: 1), + ), + ), + child: TabBar( + controller: _tabController, + labelColor: const Color(0xFFD4AF37), + unselectedLabelColor: const Color(0xFF745D43), + indicatorColor: const Color(0xFFD4AF37), + indicatorWeight: 2, + labelStyle: const TextStyle( + fontSize: 15, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + ), + unselectedLabelStyle: const TextStyle( + fontSize: 15, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + ), + tabs: [ + Tab(text: '预种订单 (${_orders.length})'), + Tab(text: '合并记录 (${_merges.length})'), + ], + ), + ); + } + + /// 订单列表 + Widget _buildOrdersList() { + if (_orders.isEmpty) { + return _buildEmptyState('暂无预种订单', '购买预种份额后,订单将在此显示'); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _orders.length, + itemBuilder: (context, index) => _buildOrderCard(_orders[index]), + ); + } + + /// 合并记录列表 + Widget _buildMergesList() { + if (_merges.isEmpty) { + return _buildEmptyState('暂无合并记录', '累计 5 份后将自动合成 1 棵树'); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _merges.length, + itemBuilder: (context, index) => _buildMergeCard(_merges[index]), + ); + } + + /// 空状态 + Widget _buildEmptyState(String title, String subtitle) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.inbox_outlined, + color: const Color(0xFF745D43).withValues(alpha: 0.4), + size: 48, + ), + const SizedBox(height: 12), + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: TextStyle( + fontSize: 13, + color: const Color(0xFF745D43).withValues(alpha: 0.7), + ), + ), + ], + ), + ); + } + + /// 预种订单卡片 + Widget _buildOrderCard(PrePlantingOrder order) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0x99FFFFFF), + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 顶部:订单号 + 状态标签 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + order.orderNo, + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + _buildStatusLabel(order.status), + ], + ), + const SizedBox(height: 10), + // 中部:份数 + 金额 + Row( + children: [ + _buildInfoChip( + Icons.layers_outlined, + '${order.portionCount} 份', + ), + const SizedBox(width: 16), + _buildInfoChip( + Icons.monetization_on_outlined, + '${order.totalAmount.toInt()} USDT', + ), + ], + ), + const SizedBox(height: 10), + // 底部:时间 + Text( + _formatDateTime(order.paidAt ?? order.createdAt), + style: TextStyle( + fontSize: 12, + color: const Color(0xFF745D43).withValues(alpha: 0.7), + ), + ), + ], + ), + ); + } + + /// 合并记录卡片 + Widget _buildMergeCard(PrePlantingMerge merge) { + return GestureDetector( + onTap: () => _goToMergeDetail(merge.mergeNo), + child: Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0x99FFFFFF), + borderRadius: BorderRadius.circular(12), + border: merge.contractStatus == PrePlantingContractStatus.pending + ? Border.all( + color: const Color(0xFFD4AF37).withValues(alpha: 0.5), + width: 1, + ) + : null, + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 顶部:合并号 + 合同状态 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Icon(Icons.park, color: Color(0xFFD4AF37), size: 20), + const SizedBox(width: 8), + Text( + merge.mergeNo, + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + ], + ), + _buildContractStatusLabel(merge.contractStatus), + ], + ), + const SizedBox(height: 10), + // 中部:信息 + Row( + children: [ + _buildInfoChip(Icons.park_outlined, '${merge.treeCount} 棵树'), + const SizedBox(width: 16), + _buildInfoChip( + Icons.layers_outlined, + '${merge.sourceOrderNos.length} 份合并', + ), + ], + ), + // 待签约提示 + if (merge.contractStatus == PrePlantingContractStatus.pending) ...[ + const SizedBox(height: 10), + Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFFD4AF37).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.edit_document, color: Color(0xFFD4AF37), size: 16), + SizedBox(width: 6), + Text( + '点击签署合同', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Color(0xFFD4AF37), + ), + ), + ], + ), + ), + ], + const SizedBox(height: 10), + // 底部:时间 + Text( + '合并时间:${_formatDateTime(merge.mergedAt)}', + style: TextStyle( + fontSize: 12, + color: const Color(0xFF745D43).withValues(alpha: 0.7), + ), + ), + ], + ), + ), + ); + } + + /// 订单状态标签 + Widget _buildStatusLabel(PrePlantingOrderStatus status) { + late String text; + late Color bgColor; + late Color textColor; + + switch (status) { + case PrePlantingOrderStatus.created: + text = '待支付'; + bgColor = const Color(0xFFFFF3E0); + textColor = const Color(0xFFE65100); + break; + case PrePlantingOrderStatus.paid: + text = '已支付'; + bgColor = const Color(0xFFE8F5E9); + textColor = const Color(0xFF2E7D32); + break; + case PrePlantingOrderStatus.merged: + text = '已合并'; + bgColor = const Color(0xFFD4AF37).withValues(alpha: 0.15); + textColor = const Color(0xFFD4AF37); + break; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + text, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + ); + } + + /// 合同状态标签 + Widget _buildContractStatusLabel(PrePlantingContractStatus status) { + late String text; + late Color bgColor; + late Color textColor; + + switch (status) { + case PrePlantingContractStatus.pending: + text = '待签约'; + bgColor = const Color(0xFFFFF3E0); + textColor = const Color(0xFFE65100); + break; + case PrePlantingContractStatus.signed: + text = '已签约'; + bgColor = const Color(0xFFE8F5E9); + textColor = const Color(0xFF2E7D32); + break; + case PrePlantingContractStatus.expired: + text = '已过期'; + bgColor = const Color(0xFFEEEEEE); + textColor = const Color(0xFF757575); + break; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + text, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + ); + } + + /// 信息小标签(图标 + 文字) + Widget _buildInfoChip(IconData icon, String text) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: const Color(0xFF745D43), size: 16), + const SizedBox(width: 4), + Text( + text, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF745D43), + ), + ), + ], + ); + } + + /// 格式化日期时间 + String _formatDateTime(DateTime dt) { + return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-' + '${dt.day.toString().padLeft(2, '0')} ' + '${dt.hour.toString().padLeft(2, '0')}:' + '${dt.minute.toString().padLeft(2, '0')}'; + } }