feat(pre-planting): Mobile App 预种合并详情页完整实现

[2026-02-17] 预种合并详情页面 (pre_planting_merge_detail_page.dart)

完整功能:
- 合并信息卡片:合并号、合并时间、份数→树数、总价值、省市
- 合同签署状态卡片:待签署/已签署/已过期,含签署时间
- 挖矿状态卡片:已开启/未开启,含开启时间
- 来源订单列表:编号圆标 + 订单号 + 金额,逐条展示 5 笔订单
- 签约确认弹窗:列出签约后解锁的权限(交易/提现/授权/挖矿)
- 底部签约按钮:仅待签署状态显示,含加载状态
- 签约成功后自动刷新页面状态

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

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

View File

@ -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
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('合并详情')),
body: Center(child: Text('合并详情页 - $mergeNo - 开发中')),
ConsumerState<PrePlantingMergeDetailPage> createState() =>
_PrePlantingMergeDetailPageState();
}
class _PrePlantingMergeDetailPageState
extends ConsumerState<PrePlantingMergeDetailPage> {
///
PrePlantingMerge? _merge;
///
bool _isLoading = true;
///
bool _isSigning = false;
///
String? _errorMessage;
@override
void initState() {
super.initState();
_loadDetail();
}
// ============================================
//
// ============================================
///
Future<void> _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<void> _signContract() async {
if (_isSigning || _merge == null) return;
//
final confirmed = await showDialog<bool>(
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(
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,
),
),
),
],
),
);
}
}