From 8a4508fe0dfbbcc444bc4ecf6629f31c2040c84d Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 18 Feb 2026 05:38:44 -0800 Subject: [PATCH] =?UTF-8?q?feat(pre-planting):=20Mobile=20App=20=E9=A2=84?= =?UTF-8?q?=E7=A7=8D=E8=B4=AD=E4=B9=B0=E9=A1=B5=E9=9D=A2=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=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_purchase_page.dart) 完整功能: - 并行加载数据(余额 + 配置 + 资格 + 持仓) - 余额卡片:显示绿积分可用余额,支持刷新 - 合并进度卡片:显示当前 N/5 份进度条 + 已合成树数 - 省市选择:首次购买使用 city_pickers 选择,续购自动锁定复用 - 份数选择器:+/- 按钮 + 输入框,自动校验余额和资格限制 - 价格明细:单价 3171 USDT、最大可购买数、本次总价 - 购买确认弹窗:含合并预告(购买后将自动合成提示) - 开关关闭禁用态:显示不可购买原因 - 错误重试、加载中状态完备 UI 风格与现有认种页面 (planting_quantity_page) 完全一致: - 渐变背景 (#FFF7E6 → #EAE0C8) - 金色主色调 (#D4AF37) - 棕色文字 (#5D4037) - 卡片容器带阴影 Co-Authored-By: Claude Opus 4.6 --- .../pages/pre_planting_purchase_page.dart | 1374 ++++++++++++++++- 1 file changed, 1368 insertions(+), 6 deletions(-) diff --git a/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_purchase_page.dart b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_purchase_page.dart index 2eb016ed..cb4b4620 100644 --- a/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_purchase_page.dart +++ b/frontend/mobile-app/lib/features/pre_planting/presentation/pages/pre_planting_purchase_page.dart @@ -1,17 +1,1379 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:city_pickers/city_pickers.dart'; +import '../../../../core/di/injection_container.dart'; +import '../../../../core/services/pre_planting_service.dart'; -/// [2026-02-17] 预种计划购买页面(占位 - 待完整实现) +// ============================================ +// [2026-02-17] 预种计划购买页面 +// ============================================ +// +// 预种计划(拼种/团购计划)的购买页面: +// - 用户以 3171 USDT/份 购买预种份额 +// - 累计 5 份自动合成 1 棵树 +// - 首次购买需选择省市(后续复用,不可更改) +// - 显示余额、份数选择、合并进度、价格明细 +// +// === 页面流程 === +// 1. 加载数据:余额 + 预种配置 + 购买资格 + 持仓信息 +// 2. 首次购买:用户选择省市 → 选择份数 → 确认购买 +// 3. 续购:自动复用已有省市 → 选择份数 → 确认购买 +// 4. 购买成功:后端自动扣款、分配权益、检查合并 +// +// === 与现有认种页面的关系 === +// planting_quantity_page.dart → 整棵树认种(15831 USDT/棵) +// 本页面 → 预种份额购买(3171 USDT/份),完全独立的流程 + +/// 预种计划购买页面 /// -/// 功能:选择购买份数(通常 1 份)、显示余额、确认支付 3171 USDT -/// 首次购买需选择省市,续购自动复用。 -class PrePlantingPurchasePage extends StatelessWidget { +/// 用户可以购买预种份额(3171 USDT/份),累计 5 份自动合成 1 棵树。 +/// 首次购买需选择省市(后续购买自动复用)。 +class PrePlantingPurchasePage extends ConsumerStatefulWidget { const PrePlantingPurchasePage({super.key}); + @override + ConsumerState createState() => + _PrePlantingPurchasePageState(); +} + +class _PrePlantingPurchasePageState + extends ConsumerState { + // === 常量 === + + /// 每份预种价格(USDT) + static const double _pricePerPortion = 3171.0; + + /// 合并所需份数 + static const int _portionsPerTree = 5; + + // === 状态变量 === + + /// 可用余额(绿积分 / USDT) + double _availableBalance = 0.0; + + /// 当前选择的购买份数 + int _quantity = 1; + + /// 是否正在加载初始数据 + bool _isLoading = true; + + /// 是否正在提交购买 + bool _isPurchasing = false; + + /// 加载错误信息 + String? _errorMessage; + + /// 预种功能配置(开关状态) + PrePlantingConfig? _config; + + /// 购买资格信息 + PrePlantingEligibility? _eligibility; + + /// 当前持仓信息(用于判断首次购买 vs 续购) + PrePlantingPosition? _position; + + // === 省市选择(首次购买时使用)=== + + /// 选中的省份名称 + String? _selectedProvinceName; + + /// 选中的省份代码 + String? _selectedProvinceCode; + + /// 选中的城市名称 + String? _selectedCityName; + + /// 选中的城市代码 + String? _selectedCityCode; + + /// 份数输入框控制器 + final TextEditingController _quantityController = + TextEditingController(text: '1'); + + /// 份数输入框焦点 + final FocusNode _quantityFocusNode = FocusNode(); + + /// 是否显示超出警告(输入份数超过最大可购买数) + bool _showExceedWarning = false; + + @override + void initState() { + super.initState(); + _loadData(); + _quantityController.addListener(_onQuantityChanged); + } + + @override + void dispose() { + _quantityController.removeListener(_onQuantityChanged); + _quantityController.dispose(); + _quantityFocusNode.dispose(); + super.dispose(); + } + + // ============================================ + // 数据加载 + // ============================================ + + /// 并行加载所有初始数据:余额、配置、资格、持仓 + Future _loadData() async { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + final walletService = ref.read(walletServiceProvider); + final prePlantingService = ref.read(prePlantingServiceProvider); + + // 并行加载所有数据 + final results = await Future.wait([ + walletService.getMyWallet(), // [0] 钱包余额 + prePlantingService.getConfig(), // [1] 预种配置 + prePlantingService.checkEligibility(), // [2] 购买资格 + prePlantingService.getMyPosition().catchError((_) => // [3] 持仓(可能 404) + PrePlantingPosition( + totalPortions: 0, + availablePortions: 0, + mergedPortions: 0, + totalTreesMerged: 0, + ), + ), + ]); + + final walletResponse = results[0]; + final config = results[1] as PrePlantingConfig; + final eligibility = results[2] as PrePlantingEligibility; + final position = results[3] as PrePlantingPosition; + + // 使用钱包中的 USDT 可用余额 + final balance = (walletResponse as dynamic).balances.usdt.available as double; + + setState(() { + _availableBalance = balance; + _config = config; + _eligibility = eligibility; + _position = position; + _isLoading = false; + + // 续购时自动填入已保存的省市 + if (position.hasProvinceCity) { + _selectedProvinceCode = position.provinceCode; + _selectedProvinceName = position.provinceName ?? position.provinceCode; + _selectedCityCode = position.cityCode; + _selectedCityName = position.cityName ?? position.cityCode; + } + + // 根据余额自动计算默认份数(默认 1 份) + final maxQty = _maxQuantity; + _quantity = maxQty > 0 ? 1 : 0; + _updateQuantityText(_quantity); + }); + } catch (e) { + debugPrint('[PrePlantingPurchase] 加载数据失败: $e'); + setState(() { + _isLoading = false; + _errorMessage = '加载数据失败,请重试'; + }); + } + } + + // ============================================ + // 份数选择逻辑 + // ============================================ + + /// 最大可购买份数(根据余额计算,并受资格限制) + int get _maxQuantity { + // 基于余额的最大份数 + int balanceMax = (_availableBalance / _pricePerPortion).floor(); + + // 如果有资格限制(开关关闭时的凑满限制),取较小值 + if (_eligibility?.maxAdditional != null) { + balanceMax = balanceMax.clamp(0, _eligibility!.maxAdditional!); + } + + return balanceMax; + } + + /// 是否为首次购买(无持仓记录或无省市信息) + bool get _isFirstPurchase => + _position == null || !_position!.hasProvinceCity; + + /// 是否需要选择省市(首次购买才需要) + bool get _needsProvinceCity => _isFirstPurchase; + + /// 是否已选择省市 + bool get _hasSelectedProvinceCity => + _selectedProvinceCode != null && _selectedCityCode != null; + + /// 输入框数量变化监听 + void _onQuantityChanged() { + final text = _quantityController.text; + if (text.isEmpty) { + setState(() { + _quantity = 0; + _showExceedWarning = false; + }); + return; + } + + final inputQty = int.tryParse(text) ?? 0; + setState(() { + _quantity = inputQty; + _showExceedWarning = inputQty > _maxQuantity; + }); + } + + /// 更新输入框显示的数量 + void _updateQuantityText(int qty) { + final newText = qty > 0 ? qty.toString() : ''; + if (_quantityController.text != newText) { + _quantityController.text = newText; + _quantityController.selection = TextSelection.fromPosition( + TextPosition(offset: newText.length), + ); + } + } + + /// 减少份数 + void _decreaseQuantity() { + if (_quantity > 1) { + final newQty = _quantity - 1; + setState(() { + _quantity = newQty; + _showExceedWarning = false; + }); + _updateQuantityText(newQty); + } + } + + /// 增加份数 + void _increaseQuantity() { + if (_quantity < _maxQuantity) { + final newQty = _quantity + 1; + setState(() { + _quantity = newQty; + _showExceedWarning = false; + }); + _updateQuantityText(newQty); + } + } + + bool get _canDecrease => _quantity > 1; + bool get _canIncrease => _quantity < _maxQuantity; + + // ============================================ + // 省市选择 + // ============================================ + + /// 显示省市选择器(使用 city_pickers,与现有认种一致) + Future _showCityPicker() async { + // 续购时省市已锁定,不允许重新选择 + if (!_needsProvinceCity) return; + + final result = await CityPickers.showCityPicker( + context: context, + cancelWidget: const Text( + '取消', + style: TextStyle(color: Color(0xFF745D43), fontSize: 16), + ), + confirmWidget: const Text( + '确定', + style: TextStyle( + color: Color(0xFFD4AF37), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + height: 300, + showType: ShowType.pc, // 只显示省市两级 + barrierDismissible: true, + ); + + if (result != null) { + setState(() { + _selectedProvinceName = result.provinceName; + _selectedProvinceCode = result.provinceId; + _selectedCityName = result.cityName; + _selectedCityCode = result.cityId; + }); + } + } + + // ============================================ + // 购买提交 + // ============================================ + + /// 是否可以提交购买 + bool get _canPurchase { + if (_isLoading || _isPurchasing) return false; + if (_quantity <= 0 || _quantity > _maxQuantity) return false; + if (_eligibility != null && !_eligibility!.canPurchase) return false; + if (!_hasSelectedProvinceCity) return false; + return true; + } + + /// 确认购买 + Future _confirmPurchase() async { + if (!_canPurchase) return; + + // 显示确认弹窗 + final confirmed = await _showConfirmDialog(); + if (confirmed != true || !mounted) return; + + setState(() => _isPurchasing = true); + + try { + final prePlantingService = ref.read(prePlantingServiceProvider); + + // 创建预种订单(后端自动完成:扣款 → 分配权益 → 检查合并) + final response = await prePlantingService.createOrder( + portionCount: _quantity, + // 首次购买时传省市信息,续购时后端自动复用 + provinceCode: _isFirstPurchase ? _selectedProvinceCode : null, + provinceName: _isFirstPurchase ? _selectedProvinceName : null, + cityCode: _isFirstPurchase ? _selectedCityCode : null, + cityName: _isFirstPurchase ? _selectedCityName : null, + ); + + debugPrint( + '[PrePlantingPurchase] 购买成功: orderNo=${response.orderNo}, ' + 'portions=${response.portionCount}, total=${response.totalAmount}', + ); + + if (mounted) { + // 显示成功提示 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '购买成功!已购买 ${response.portionCount} 份预种(${response.totalAmount.toInt()} USDT)', + ), + backgroundColor: const Color(0xFF4CAF50), + ), + ); + + // 返回上一页(Profile 页面会自动刷新) + context.pop(true); + } + } catch (e) { + debugPrint('[PrePlantingPurchase] 购买失败: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('购买失败: $e'), + backgroundColor: Colors.red, + ), + ); + } + } finally { + if (mounted) { + setState(() => _isPurchasing = false); + } + } + } + + /// 显示购买确认弹窗 + Future _showConfirmDialog() { + final totalAmount = _quantity * _pricePerPortion; + final currentPortions = _position?.availablePortions ?? 0; + final afterPortions = currentPortions + _quantity; + final willMerge = afterPortions >= _portionsPerTree; + + return 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: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDialogRow('购买份数', '$_quantity 份'), + const SizedBox(height: 8), + _buildDialogRow('单价', '${_pricePerPortion.toInt()} USDT/份'), + const SizedBox(height: 8), + _buildDialogRow( + '总金额', + '${_formatNumber(totalAmount)} USDT', + highlight: true, + ), + const SizedBox(height: 8), + _buildDialogRow( + '省市', + '${_selectedProvinceName ?? "-"} · ${_selectedCityName ?? "-"}', + ), + if (_position != null) ...[ + const Divider(height: 24, color: Color(0x338B5A2B)), + _buildDialogRow( + '购买后累计', + '$afterPortions / $_portionsPerTree 份', + ), + if (willMerge) ...[ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFFD4AF37).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + ), + child: const Row( + children: [ + Icon(Icons.park, color: Color(0xFFD4AF37), size: 20), + SizedBox(width: 8), + Expanded( + child: Text( + '购买后将自动合成 1 棵树!', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFFD4AF37), + ), + ), + ), + ], + ), + ), + ], + ], + ], + ), + 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, + ), + ), + ), + ], + ), + ); + } + + /// 确认弹窗内的信息行 + Widget _buildDialogRow(String label, String value, {bool highlight = false}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF745D43), + ), + ), + Text( + value, + style: TextStyle( + fontSize: highlight ? 16 : 14, + fontWeight: highlight ? FontWeight.w700 : FontWeight.w500, + color: highlight ? const Color(0xFFD4AF37) : const Color(0xFF5D4037), + ), + ), + ], + ); + } + + // ============================================ + // 工具方法 + // ============================================ + + /// 格式化数字显示(千分位 + 2位小数) + String _formatNumber(double number) { + final parts = number.toStringAsFixed(2).split('.'); + final intPart = parts[0].replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]},', + ); + return '$intPart.${parts[1]}'; + } + + /// 返回上一页 + void _goBack() { + context.pop(); + } + + // ============================================ + // 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(), + ), + if (!_isLoading && _errorMessage == null) _buildBottomButton(), + ], + ), + ), + ), + ); + } + + /// 顶部导航栏 + 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: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xFFD4AF37), + ), + SizedBox(height: 16), + Text( + '加载预种信息...', + style: TextStyle( + fontSize: 14, + color: Color(0xFF745D43), + ), + ), + ], + ), + ); + } + + /// 错误状态(点击重试) + 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() { + // 不可购买状态(开关关闭且无资格) + if (_eligibility != null && !_eligibility!.canPurchase) { + return _buildDisabledState(); + } + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + // 余额卡片 + _buildBalanceCard(), + const SizedBox(height: 16), + // 合并进度卡片(有持仓时显示) + if (_position != null && _position!.totalPortions > 0) + _buildMergeProgressCard(), + if (_position != null && _position!.totalPortions > 0) + const SizedBox(height: 16), + // 省市选择区域 + _buildProvinceCitySection(), + const SizedBox(height: 24), + // 份数选择标题 + const Text( + '选择购买份数', + style: TextStyle( + fontSize: 24, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + height: 1.25, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 8), + // 份数选择器 + _buildQuantitySelector(), + const SizedBox(height: 8), + // 价格信息 + _buildPriceInfo(), + const SizedBox(height: 24), + ], + ), + ), + ); + } + + /// 不可购买的禁用状态 + Widget _buildDisabledState() { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.lock_outline, + color: const Color(0xFFD4AF37).withValues(alpha: 0.5), + size: 64, + ), + const SizedBox(height: 16), + const Text( + '预种计划', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w700, + color: Color(0xFF5D4037), + ), + ), + const SizedBox(height: 8), + Text( + _eligibility?.message ?? '预种功能待开启', + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + color: Color(0xFF745D43), + height: 1.5, + ), + ), + ], + ), + ), + ); + } + + /// 余额卡片 + Widget _buildBalanceCard() { + 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: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '可用余额 (绿积分)', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + height: 1.5, + color: Color(0xFF745D43), + ), + ), + GestureDetector( + onTap: _loadData, + child: const Icon( + Icons.refresh, + color: Color(0xFFD4AF37), + size: 20, + ), + ), + ], + ), + const SizedBox(height: 3), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + _formatNumber(_availableBalance), + style: const TextStyle( + fontSize: 36, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + height: 1.25, + letterSpacing: -0.54, + color: Color(0xFF5D4037), + ), + ), + ), + ], + ), + ); + } + + /// 合并进度卡片(显示当前 N/5 份的进度) + Widget _buildMergeProgressCard() { + final available = _position!.availablePortions; + final treesMerged = _position!.totalTreesMerged; + final progress = available / _portionsPerTree; + + 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: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题行 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '合并进度', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + height: 1.5, + color: Color(0xFF745D43), + ), + ), + if (treesMerged > 0) + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFD4AF37).withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '已合成 $treesMerged 棵树', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Color(0xFFD4AF37), + ), + ), + ), + ], + ), + const SizedBox(height: 12), + // 进度条 + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: progress, + minHeight: 8, + backgroundColor: const Color(0xFFEAE0C8), + valueColor: + const AlwaysStoppedAnimation(Color(0xFFD4AF37)), + ), + ), + const SizedBox(height: 8), + // 进度文字 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '当前 $available / $_portionsPerTree 份', + style: const TextStyle( + fontSize: 14, + color: Color(0xFF5D4037), + ), + ), + Text( + '还需 ${_portionsPerTree - available} 份合成 1 棵树', + style: const TextStyle( + fontSize: 14, + color: Color(0xFF745D43), + ), + ), + ], + ), + ], + ), + ); + } + + /// 省市选择区域 + Widget _buildProvinceCitySection() { + // 续购时显示已锁定的省市(不可更改) + final bool isLocked = !_needsProvinceCity; + + 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: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题 + Row( + children: [ + Text( + isLocked ? '省市信息(已锁定)' : '选择省市(首次购买)', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + height: 1.5, + color: isLocked + ? const Color(0xFF5D4037).withValues(alpha: 0.6) + : const Color(0xFF5D4037), + ), + ), + if (isLocked) ...[ + const SizedBox(width: 8), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFD4AF37).withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '已锁定', + style: TextStyle( + fontSize: 10, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + color: Color(0xFFD4AF37), + ), + ), + ), + ], + ], + ), + const SizedBox(height: 12), + // 省市选择框 / 已锁定显示 + GestureDetector( + onTap: isLocked ? null : _showCityPicker, + child: Container( + width: double.infinity, + padding: + const EdgeInsets.symmetric(horizontal: 17, vertical: 13), + decoration: BoxDecoration( + color: isLocked ? const Color(0xFFF5F5F5) : Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isLocked + ? const Color(0x338B5A2B) + : const Color(0x4D8B5A2B), + width: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _hasSelectedProvinceCity + ? '$_selectedProvinceName · $_selectedCityName' + : '点击选择省份和城市', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + height: 1.5, + color: _hasSelectedProvinceCity + ? (isLocked + ? const Color(0xFF5D4037) + .withValues(alpha: 0.6) + : const Color(0xFF5D4037)) + : const Color(0xFF5D4037).withValues(alpha: 0.5), + ), + ), + Icon( + isLocked + ? Icons.lock_outline + : Icons.keyboard_arrow_down, + color: isLocked + ? const Color(0xFF8B5A2B).withValues(alpha: 0.5) + : const Color(0xFF8B5A2B), + size: 24, + ), + ], + ), + ), + ), + // 首次购买时的警告提示 + if (!isLocked) ...[ + const SizedBox(height: 8), + const Row( + children: [ + Icon( + Icons.info_outline, + color: Color(0xFFD4AF37), + size: 16, + ), + SizedBox(width: 4), + Expanded( + child: Text( + '省市选择后不可修改,请选择身份证所在省市', + style: TextStyle( + fontSize: 12, + color: Color(0xFF745D43), + ), + ), + ), + ], + ), + ], + ], + ), + ); + } + + /// 份数选择器 + Widget _buildQuantitySelector() { + return Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + 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: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 减少按钮 + _buildQuantityButton( + icon: '-', + onTap: _canDecrease ? _decreaseQuantity : null, + enabled: _canDecrease, + ), + const SizedBox(width: 24), + // 份数输入框 + SizedBox( + width: 80, + child: TextField( + controller: _quantityController, + focusNode: _quantityFocusNode, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 36, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + height: 1.5, + color: _showExceedWarning + ? const Color(0xFFE65100) + : const Color(0xFF5D4037), + ), + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: _showExceedWarning + ? const Color(0xFFE65100) + : const Color(0xFFD4AF37), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: _showExceedWarning + ? const Color(0xFFE65100) + : const Color(0xFFD4AF37), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: _showExceedWarning + ? const Color(0xFFE65100) + : const Color(0xFFD4AF37), + width: 2, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 8, + ), + isDense: true, + ), + ), + ), + const SizedBox(width: 24), + // 增加按钮 + _buildQuantityButton( + icon: '+', + onTap: _canIncrease ? _increaseQuantity : null, + enabled: _canIncrease, + ), + ], + ), + ), + // 超出警告 + if (_showExceedWarning) + Padding( + padding: const EdgeInsets.only(top: 12), + child: Container( + width: double.infinity, + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: const Color(0xFFFFF3E0), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0xFFE65100), + width: 1, + ), + ), + child: Row( + children: [ + const Icon( + Icons.warning_amber_rounded, + color: Color(0xFFE65100), + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + '输入份数 $_quantity 超过最大可购买数量 $_maxQuantity,请调整', + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + height: 1.5, + color: Color(0xFFE65100), + ), + ), + ), + ], + ), + ), + ), + ], + ); + } + + /// 数量调节按钮(复用认种页面样式) + Widget _buildQuantityButton({ + required String icon, + required VoidCallback? onTap, + required bool enabled, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFFFFF7E6), + borderRadius: BorderRadius.circular(9999), + boxShadow: const [ + BoxShadow( + color: Color(0x1A000000), + blurRadius: 3, + offset: Offset(0, 1), + ), + BoxShadow( + color: Color(0x1A000000), + blurRadius: 2, + offset: Offset(0, 1), + ), + ], + ), + child: Opacity( + opacity: enabled ? 1.0 : 0.4, + child: Center( + child: Text( + icon, + style: const TextStyle( + fontSize: 30, + fontFamily: 'Inter', + fontWeight: FontWeight.w400, + height: 1.5, + color: Color(0xFF745D43), + ), + ), + ), + ), + ), + ); + } + + /// 价格信息 + Widget _buildPriceInfo() { + final totalAmount = _quantity * _pricePerPortion; + + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 单价 + Text( + '每份价格:${_pricePerPortion.toInt()} 绿积分', + style: const TextStyle( + fontSize: 16, + fontFamily: 'Inter', + height: 1.5, + color: Color(0xFF000000), + ), + ), + const SizedBox(height: 7), + // 最大可购买 + Text( + '最大可购买:$_maxQuantity 份', + style: const TextStyle( + fontSize: 16, + fontFamily: 'Inter', + height: 1.5, + color: Color(0xFF000000), + ), + ), + const SizedBox(height: 7), + // 本次总价 + Row( + children: [ + const Text( + '本次总价:', + style: TextStyle( + fontSize: 16, + fontFamily: 'Inter', + height: 1.5, + color: Color(0xFF000000), + ), + ), + Text( + '${_formatNumber(totalAmount)} 绿积分', + style: const TextStyle( + fontSize: 16, + fontFamily: 'Inter', + fontWeight: FontWeight.w700, + height: 1.5, + color: Color(0xFFD4AF37), + ), + ), + ], + ), + const SizedBox(height: 7), + // 合并提示 + Text( + '每 $_portionsPerTree 份自动合成 1 棵树(${_formatNumber(_pricePerPortion * _portionsPerTree)} USDT)', + style: const TextStyle( + fontSize: 14, + fontFamily: 'Inter', + height: 1.5, + color: Color(0xFF745D43), + ), + ), + ], + ), + ); + } + + /// 底部按钮 + Widget _buildBottomButton() { + // 不可购买时不显示按钮 + if (_eligibility != null && !_eligibility!.canPurchase) { + return const SizedBox.shrink(); + } + + return Container( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), + child: GestureDetector( + onTap: _canPurchase ? _confirmPurchase : null, + child: Container( + width: double.infinity, + height: 56, + decoration: BoxDecoration( + color: _canPurchase + ? const Color(0xFFD4AF37) + : const Color(0xFFD4AF37).withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + boxShadow: _canPurchase + ? const [ + BoxShadow( + color: Color(0x1A000000), + blurRadius: 15, + offset: Offset(0, 10), + ), + BoxShadow( + color: Color(0x1A000000), + blurRadius: 6, + offset: Offset(0, 4), + ), + ] + : null, + ), + child: Center( + child: _isPurchasing + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : Text( + _hasSelectedProvinceCity + ? '确认购买 $_quantity 份' + : '请先选择省市', + style: const TextStyle( + fontSize: 18, + fontFamily: 'Inter', + fontWeight: FontWeight.w500, + height: 1.56, + color: Colors.white, + ), + ), + ), + ), + ), ); } }