From d248f924436b436bfe8171d7698be464bb894709 Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 18 Feb 2026 05:43:17 -0800 Subject: [PATCH] =?UTF-8?q?feat(pre-planting):=20Mobile=20App=20=E9=A2=84?= =?UTF-8?q?=E7=A7=8D=E5=90=88=E5=B9=B6=E8=AF=A6=E6=83=85=E9=A1=B5=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [2026-02-17] 预种合并详情页面 (pre_planting_merge_detail_page.dart) 完整功能: - 合并信息卡片:合并号、合并时间、份数→树数、总价值、省市 - 合同签署状态卡片:待签署/已签署/已过期,含签署时间 - 挖矿状态卡片:已开启/未开启,含开启时间 - 来源订单列表:编号圆标 + 订单号 + 金额,逐条展示 5 笔订单 - 签约确认弹窗:列出签约后解锁的权限(交易/提现/授权/挖矿) - 底部签约按钮:仅待签署状态显示,含加载状态 - 签约成功后自动刷新页面状态 UI 风格与全局一致:渐变背景、金色主色调、卡片容器 Co-Authored-By: Claude Opus 4.6 --- .../pages/pre_planting_merge_detail_page.dart | 786 +++++++++++++++++- 1 file changed, 781 insertions(+), 5 deletions(-) diff --git a/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_merge_detail_page.dart b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_merge_detail_page.dart index 772c14e4..ffeda36e 100644 --- a/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_merge_detail_page.dart +++ b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_merge_detail_page.dart @@ -1,18 +1,794 @@ 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'; -/// [2026-02-17] 预种合并详情页面(占位 - 待完整实现) +// ============================================ +// [2026-02-17] 预种合并详情页面 +// ============================================ +// +// 显示一次合并(5 份 → 1 棵树)的完整详情,包括: +// - 合并信息:合并号、合并时间、树数 +// - 合同签署状态:待签署 / 已签署(含签署时间) +// - 挖矿状态:未开启 / 已开启(含开启时间) +// - 来源订单列表:组成本次合并的 5 笔预种订单号 +// - 签约按钮:待签署状态下显示,点击触发合同签署 +// +// === 页面入口 === +// 从 PrePlantingPositionPage 的合并记录列表点击进入 +// 路由参数:mergeNo(合并记录号) +// +// === 合同签署流程 === +// 点击签约 → 调用 signMergeContract API → 刷新页面 +// 签约后:解锁交易/提现/授权限制 + 开启挖矿 + +/// 预种合并详情页面 /// -/// 功能:显示合并记录详情、合同签署状态、挖矿状态、来源订单列表 -class PrePlantingMergeDetailPage extends StatelessWidget { +/// 显示合并记录详情、合同签署状态、挖矿状态, +/// 并提供合同签署入口。 +class PrePlantingMergeDetailPage extends ConsumerStatefulWidget { final String mergeNo; const PrePlantingMergeDetailPage({super.key, required this.mergeNo}); + @override + ConsumerState createState() => + _PrePlantingMergeDetailPageState(); +} + +class _PrePlantingMergeDetailPageState + extends ConsumerState { + /// 合并详情数据 + PrePlantingMerge? _merge; + + /// 是否正在加载 + bool _isLoading = true; + + /// 是否正在签约 + bool _isSigning = false; + + /// 加载错误信息 + String? _errorMessage; + + @override + void initState() { + super.initState(); + _loadDetail(); + } + + // ============================================ + // 数据加载 + // ============================================ + + /// 加载合并详情 + Future _loadDetail() async { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + final service = ref.read(prePlantingServiceProvider); + final merge = await service.getMergeDetail(widget.mergeNo); + + setState(() { + _merge = merge; + _isLoading = false; + }); + } catch (e) { + debugPrint('[PrePlantingMergeDetail] 加载详情失败: $e'); + setState(() { + _isLoading = false; + _errorMessage = '加载详情失败,请重试'; + }); + } + } + + /// 签署合同 + Future _signContract() async { + if (_isSigning || _merge == null) return; + + // 确认弹窗 + final confirmed = await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + backgroundColor: const Color(0xFFFFF7E6), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: const Text( + '确认签署合同', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Color(0xFF5D4037), + ), + ), + content: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '签署合同后将:', + style: TextStyle( + fontSize: 15, + color: Color(0xFF5D4037), + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8), + _BulletPoint('解锁 eUSDT 交易权限'), + _BulletPoint('解锁提现权限'), + _BulletPoint('解锁授权申请权限'), + _BulletPoint('开启挖矿收益分配'), + SizedBox(height: 12), + Text( + '此操作不可撤销,请确认。', + style: TextStyle( + fontSize: 13, + color: Color(0xFF745D43), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: const Text( + '取消', + style: TextStyle(color: Color(0xFF745D43), fontSize: 16), + ), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(true), + child: const Text( + '确认签署', + style: TextStyle( + color: Color(0xFFD4AF37), + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + ); + + if (confirmed != true || !mounted) return; + + setState(() => _isSigning = true); + + try { + final service = ref.read(prePlantingServiceProvider); + final updatedMerge = await service.signMergeContract(widget.mergeNo); + + debugPrint('[PrePlantingMergeDetail] 签约成功: ${updatedMerge.mergeNo}'); + + setState(() { + _merge = updatedMerge; + _isSigning = false; + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('合同签署成功!交易、提现和授权权限已解锁'), + backgroundColor: Color(0xFF4CAF50), + ), + ); + } + } catch (e) { + debugPrint('[PrePlantingMergeDetail] 签约失败: $e'); + if (mounted) { + setState(() => _isSigning = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('签约失败: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + /// 返回上一页 + void _goBack() { + context.pop(); + } + + // ============================================ + // UI 构建 + // ============================================ + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('合并详情')), - body: Center(child: Text('合并详情页 - $mergeNo - 开发中')), + 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), + ), + ), + ), + ], + ), + ); + } + + /// 加载中 + Widget _buildLoadingState() { + return const Center( + child: CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xFFD4AF37), + ), + ); + } + + /// 错误状态 + Widget _buildErrorState() { + return Center( + child: GestureDetector( + onTap: _loadDetail, + 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() { + final merge = _merge!; + + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 合并信息卡片 + _buildMergeInfoCard(merge), + const SizedBox(height: 16), + // 合同状态卡片 + _buildContractStatusCard(merge), + const SizedBox(height: 16), + // 挖矿状态卡片 + _buildMiningStatusCard(merge), + const SizedBox(height: 16), + // 来源订单列表 + _buildSourceOrdersCard(merge), + ], + ), + ), + ), + ), + // 底部签约按钮(仅待签署时显示) + if (merge.contractStatus == PrePlantingContractStatus.pending) + _buildSignButton(), + ], + ); + } + + /// 合并信息卡片 + Widget _buildMergeInfoCard(PrePlantingMerge merge) { + return _buildCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 顶部图标 + 标题 + Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFFD4AF37).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(24), + ), + child: const Icon( + Icons.park, + color: Color(0xFFD4AF37), + size: 28, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '预种合并 · 1 棵树', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 2), + Text( + merge.mergeNo, + style: TextStyle( + fontSize: 13, + color: const Color(0xFF745D43).withValues(alpha: 0.7), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + const Divider(height: 1, color: Color(0x338B5A2B)), + const SizedBox(height: 16), + // 信息行 + _buildDetailRow('合并时间', _formatDateTime(merge.mergedAt)), + const SizedBox(height: 8), + _buildDetailRow('合并份数', '${merge.sourceOrderNos.length} 份 → 1 棵树'), + const SizedBox(height: 8), + _buildDetailRow( + '总价值', + '${(merge.sourceOrderNos.length * 3171).toString()} USDT', + ), + if (merge.selectedProvince != null) ...[ + const SizedBox(height: 8), + _buildDetailRow( + '省市', + '${merge.selectedProvince} · ${merge.selectedCity ?? "-"}', + ), + ], + ], + ), + ); + } + + /// 合同状态卡片 + Widget _buildContractStatusCard(PrePlantingMerge merge) { + final isPending = + merge.contractStatus == PrePlantingContractStatus.pending; + final isSigned = + merge.contractStatus == PrePlantingContractStatus.signed; + + return _buildCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + isSigned ? Icons.check_circle : Icons.edit_document, + color: isSigned + ? const Color(0xFF2E7D32) + : const Color(0xFFE65100), + size: 24, + ), + const SizedBox(width: 8), + const Text( + '合同签署', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + const Spacer(), + _buildStatusBadge( + isPending ? '待签署' : (isSigned ? '已签署' : '已过期'), + isSigned + ? const Color(0xFF2E7D32) + : (isPending + ? const Color(0xFFE65100) + : const Color(0xFF757575)), + ), + ], + ), + if (merge.contractSignedAt != null) ...[ + const SizedBox(height: 8), + Text( + '签署时间:${_formatDateTime(merge.contractSignedAt!)}', + style: TextStyle( + fontSize: 13, + color: const Color(0xFF745D43).withValues(alpha: 0.7), + ), + ), + ], + if (isPending) ...[ + const SizedBox(height: 8), + const Text( + '签署合同后将解锁交易、提现和授权权限', + style: TextStyle( + fontSize: 13, + color: Color(0xFFE65100), + ), + ), + ], + ], + ), + ); + } + + /// 挖矿状态卡片 + Widget _buildMiningStatusCard(PrePlantingMerge merge) { + final isMining = merge.isMiningEnabled; + + return _buildCard( + child: Row( + children: [ + Icon( + isMining ? Icons.flash_on : Icons.flash_off_outlined, + color: + isMining ? const Color(0xFFD4AF37) : const Color(0xFF757575), + size: 24, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '算力挖矿', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + if (merge.miningEnabledAt != null) + Text( + '开启时间:${_formatDateTime(merge.miningEnabledAt!)}', + style: TextStyle( + fontSize: 13, + color: const Color(0xFF745D43).withValues(alpha: 0.7), + ), + ), + ], + ), + ), + _buildStatusBadge( + isMining ? '已开启' : '未开启', + isMining ? const Color(0xFF2E7D32) : const Color(0xFF757575), + ), + ], + ), + ); + } + + /// 来源订单列表卡片 + Widget _buildSourceOrdersCard(PrePlantingMerge merge) { + return _buildCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '来源订单(${merge.sourceOrderNos.length} 笔)', + style: const TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w600, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 12), + ...merge.sourceOrderNos.asMap().entries.map((entry) { + final index = entry.key; + final orderNo = entry.value; + return Padding( + padding: EdgeInsets.only( + bottom: index < merge.sourceOrderNos.length - 1 ? 8 : 0, + ), + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: + const Color(0xFFD4AF37).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: Text( + '${index + 1}', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFFD4AF37), + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + orderNo, + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + color: Color(0xFF5D4037), + ), + overflow: TextOverflow.ellipsis, + ), + ), + const Text( + '3,171 USDT', + style: TextStyle( + fontSize: 13, + color: Color(0xFF745D43), + ), + ), + ], + ), + ); + }), + ], + ), + ); + } + + /// 底部签约按钮 + Widget _buildSignButton() { + return Container( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), + child: GestureDetector( + onTap: _isSigning ? null : _signContract, + child: Container( + width: double.infinity, + height: 56, + decoration: BoxDecoration( + color: _isSigning + ? const Color(0xFFD4AF37).withValues(alpha: 0.5) + : const Color(0xFFD4AF37), + borderRadius: BorderRadius.circular(12), + boxShadow: _isSigning + ? null + : const [ + BoxShadow( + color: Color(0x1A000000), + blurRadius: 15, + offset: Offset(0, 10), + ), + BoxShadow( + color: Color(0x1A000000), + blurRadius: 6, + offset: Offset(0, 4), + ), + ], + ), + child: Center( + child: _isSigning + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( + '签署合同', + style: TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + height: 1.56, + color: Colors.white, + ), + ), + ), + ), + ), + ); + } + + // ============================================ + // 通用组件 + // ============================================ + + /// 通用卡片容器 + Widget _buildCard({required Widget child}) { + return 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: child, + ); + } + + /// 详情行(标签 + 值) + Widget _buildDetailRow(String label, String value) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF745D43), + ), + ), + Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF5D4037), + ), + ), + ], + ); + } + + /// 状态徽章 + Widget _buildStatusBadge(String text, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + text, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ); + } + + /// 格式化日期时间 + 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')}'; + } +} + +/// 项目列表的圆点文字组件(用于确认弹窗) +class _BulletPoint extends StatelessWidget { + final String text; + const _BulletPoint(this.text); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(top: 6), + child: Icon( + Icons.check_circle_outline, + color: Color(0xFFD4AF37), + size: 16, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + text, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF5D4037), + height: 1.5, + ), + ), + ), + ], + ), ); } }