feat(pre-planting): Mobile App 预种持仓页面完整实现

[2026-02-17] 预种持仓页面 (pre_planting_position_page.dart)

完整功能:
- 持仓概览卡片:累计份数、待合并份数、已合成树数(三列统计)
- 合并进度条:当前 N/5 份进度可视化 + 文字提示
- 省市信息显示(已锁定的省市)
- Tab 切换:预种订单 / 合并记录
- 预种订单列表:订单号、份数、金额、状态标签(待支付/已支付/已合并)
- 合并记录列表:合并号、树数、来源订单数、合同状态标签
- 待签约合并记录高亮显示 + "点击签署合同"提示
- 点击合并记录跳转到合并详情页
- 顶部快捷购买按钮
- 下拉刷新、错误重试、空状态提示

UI 风格与全局一致:渐变背景、金色主色调、卡片阴影

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-18 05:41:34 -08:00
parent 8a4508fe0d
commit 99f5070552
1 changed files with 814 additions and 5 deletions

View File

@ -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<PrePlantingPositionPage> createState() =>
_PrePlantingPositionPageState();
}
class _PrePlantingPositionPageState
extends ConsumerState<PrePlantingPositionPage>
with SingleTickerProviderStateMixin {
// === ===
static const int _portionsPerTree = 5;
// === ===
///
PrePlantingPosition? _position;
///
List<PrePlantingOrder> _orders = [];
///
List<PrePlantingMerge> _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<void> _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<PrePlantingOrder>;
_merges = results[2] as List<PrePlantingMerge>;
_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>(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')}';
}
}