feat(pre-planting): 合并合同走完整签署流程(PDF展示+手写签名)
- planting-service: 新增 GET /merges/:mergeNo/contract-pdf 接口,复用现有 PDF 模板 - planting-service: PrePlantingApplicationService 注入 PdfGeneratorService/IdentityServiceClient - pre_planting_service.dart: 新增 downloadMergeContractPdf,signMergeContract 简化返回值 - 新建 PrePlantingMergeSigningPage:PDF展示→滚动到底→确认法律效力→手写签名→提交 - pending_contracts_page: 合并卡片点击跳签名页(prePlantingMergeSigning) - pre_planting_merge_detail_page: 签署按钮跳签名页,移除直接调用逻辑 - 新增路由 /pre-planting/merge-signing/:mergeNo Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2ad1936126
commit
b1e5e6b29f
|
|
@ -4,10 +4,12 @@ import {
|
||||||
Get,
|
Get,
|
||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
|
Res,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
Req,
|
Req,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
StreamableFile,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
|
|
@ -15,6 +17,7 @@ import {
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
|
import { Response } from 'express';
|
||||||
import { PrePlantingApplicationService } from '../../application/services/pre-planting-application.service';
|
import { PrePlantingApplicationService } from '../../application/services/pre-planting-application.service';
|
||||||
import { PurchasePrePlantingDto } from '../dto/request/purchase-pre-planting.dto';
|
import { PurchasePrePlantingDto } from '../dto/request/purchase-pre-planting.dto';
|
||||||
import { SignPrePlantingContractDto } from '../dto/request/sign-pre-planting-contract.dto';
|
import { SignPrePlantingContractDto } from '../dto/request/sign-pre-planting-contract.dto';
|
||||||
|
|
@ -100,6 +103,25 @@ export class PrePlantingController {
|
||||||
return this.prePlantingService.getMerges(userId);
|
return this.prePlantingService.getMerges(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('merges/:mergeNo/contract-pdf')
|
||||||
|
@ApiOperation({ summary: '下载合并合同预览 PDF' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: 'PDF 文件' })
|
||||||
|
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '合并记录不存在' })
|
||||||
|
async getMergeContractPdf(
|
||||||
|
@Req() req: AuthenticatedRequest,
|
||||||
|
@Param('mergeNo') mergeNo: string,
|
||||||
|
@Res({ passthrough: true }) res: Response,
|
||||||
|
): Promise<StreamableFile> {
|
||||||
|
const userId = BigInt(req.user.id);
|
||||||
|
const pdfBuffer = await this.prePlantingService.getMergeContractPdf(userId, mergeNo);
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Disposition': `inline; filename="merge-contract-${mergeNo}.pdf"`,
|
||||||
|
'Content-Length': pdfBuffer.length,
|
||||||
|
});
|
||||||
|
return new StreamableFile(pdfBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('merges/:mergeNo')
|
@Get('merges/:mergeNo')
|
||||||
@ApiOperation({ summary: '获取单条合并记录详情' })
|
@ApiOperation({ summary: '获取单条合并记录详情' })
|
||||||
@ApiResponse({ status: HttpStatus.OK, description: '合并记录详情' })
|
@ApiResponse({ status: HttpStatus.OK, description: '合并记录详情' })
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import { PrePlantingRewardService } from './pre-planting-reward.service';
|
||||||
import { PrePlantingAdminClient } from '../../infrastructure/external/pre-planting-admin.client';
|
import { PrePlantingAdminClient } from '../../infrastructure/external/pre-planting-admin.client';
|
||||||
import { TreePricingAdminClient } from '../../../infrastructure/external/tree-pricing-admin.client';
|
import { TreePricingAdminClient } from '../../../infrastructure/external/tree-pricing-admin.client';
|
||||||
import { EventPublisherService } from '../../../infrastructure/kafka/event-publisher.service';
|
import { EventPublisherService } from '../../../infrastructure/kafka/event-publisher.service';
|
||||||
|
import { PdfGeneratorService } from '../../../infrastructure/pdf/pdf-generator.service';
|
||||||
|
import { IdentityServiceClient } from '../../../infrastructure/external/identity-service.client';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrePlantingApplicationService {
|
export class PrePlantingApplicationService {
|
||||||
|
|
@ -32,6 +34,8 @@ export class PrePlantingApplicationService {
|
||||||
private readonly adminClient: PrePlantingAdminClient,
|
private readonly adminClient: PrePlantingAdminClient,
|
||||||
private readonly treePricingClient: TreePricingAdminClient,
|
private readonly treePricingClient: TreePricingAdminClient,
|
||||||
private readonly eventPublisher: EventPublisherService,
|
private readonly eventPublisher: EventPublisherService,
|
||||||
|
private readonly pdfGeneratorService: PdfGeneratorService,
|
||||||
|
private readonly identityServiceClient: IdentityServiceClient,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -390,6 +394,37 @@ export class PrePlantingApplicationService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成合并合同预览 PDF(用于前端展示)
|
||||||
|
*/
|
||||||
|
async getMergeContractPdf(userId: bigint, mergeNo: string): Promise<Buffer> {
|
||||||
|
const merge = await this.prisma.$transaction(async (tx) => {
|
||||||
|
const m = await this.mergeRepo.findByMergeNo(tx, mergeNo);
|
||||||
|
if (!m || m.userId !== userId) {
|
||||||
|
throw new NotFoundException(`合并记录不存在: ${mergeNo}`);
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取用户 KYC 信息
|
||||||
|
const kycInfo = await this.identityServiceClient.getUserKycInfo(
|
||||||
|
merge.accountSequence,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 生成北京时间日期
|
||||||
|
const now = new Date(Date.now() + 8 * 60 * 60 * 1000);
|
||||||
|
const signingDate = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-${String(now.getUTCDate()).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
return this.pdfGeneratorService.generateContractPdf({
|
||||||
|
contractNo: mergeNo,
|
||||||
|
userRealName: kycInfo?.realName || '未认证',
|
||||||
|
userIdCard: kycInfo?.idCardNumber || '',
|
||||||
|
userPhone: kycInfo?.phoneNumber || '',
|
||||||
|
treeCount: merge.treeCount,
|
||||||
|
signingDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取预种资格信息(供内部 API + 移动端购买页使用)
|
* 获取预种资格信息(供内部 API + 移动端购买页使用)
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import '../network/api_client.dart';
|
import '../network/api_client.dart';
|
||||||
|
|
||||||
|
|
@ -495,10 +497,36 @@ class PrePlantingService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 下载合并合同预览 PDF
|
||||||
|
Future<Uint8List> downloadMergeContractPdf(
|
||||||
|
String mergeNo, {
|
||||||
|
void Function(int received, int total)? onProgress,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
debugPrint('[PrePlantingService] 下载合并合同PDF: mergeNo=$mergeNo');
|
||||||
|
final response = await _apiClient.get(
|
||||||
|
'/pre-planting/merges/$mergeNo/contract-pdf',
|
||||||
|
options: Options(
|
||||||
|
responseType: ResponseType.bytes,
|
||||||
|
receiveTimeout: const Duration(seconds: 120),
|
||||||
|
),
|
||||||
|
onReceiveProgress: onProgress,
|
||||||
|
);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
debugPrint('[PrePlantingService] PDF下载成功: ${response.data.length} bytes');
|
||||||
|
return Uint8List.fromList(response.data as List<int>);
|
||||||
|
}
|
||||||
|
throw Exception('下载PDF失败: ${response.statusCode}');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[PrePlantingService] 下载合并合同PDF失败: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 签署合并合同
|
/// 签署合并合同
|
||||||
///
|
///
|
||||||
/// 5 份合并后需要签约,签约后解锁交易/提现/授权限制
|
/// 5 份合并后需要签约,签约后解锁交易/提现/授权限制
|
||||||
Future<PrePlantingMerge> signMergeContract(String mergeNo) async {
|
Future<void> signMergeContract(String mergeNo) async {
|
||||||
try {
|
try {
|
||||||
debugPrint('[PrePlantingService] 签署合并合同: mergeNo=$mergeNo');
|
debugPrint('[PrePlantingService] 签署合并合同: mergeNo=$mergeNo');
|
||||||
final response = await _apiClient.post(
|
final response = await _apiClient.post(
|
||||||
|
|
@ -507,9 +535,8 @@ class PrePlantingService {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = response.data as Map<String, dynamic>;
|
|
||||||
debugPrint('[PrePlantingService] 合同签署成功');
|
debugPrint('[PrePlantingService] 合同签署成功');
|
||||||
return PrePlantingMerge.fromJson(data);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Exception('签署合同失败: ${response.statusCode}');
|
throw Exception('签署合同失败: ${response.statusCode}');
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ class _PendingContractsPageState extends ConsumerState<PendingContractsPage> {
|
||||||
|
|
||||||
/// 签署预种合并合同(跳转到合并详情页)
|
/// 签署预种合并合同(跳转到合并详情页)
|
||||||
Future<void> _signPrePlantingMerge(PrePlantingMerge merge) async {
|
Future<void> _signPrePlantingMerge(PrePlantingMerge merge) async {
|
||||||
await context.push('${RoutePaths.prePlantingMergeDetail}/${merge.mergeNo}');
|
await context.push('${RoutePaths.prePlantingMergeSigning}/${merge.mergeNo}');
|
||||||
// 返回后刷新,合并可能已签署
|
// 返回后刷新,合并可能已签署
|
||||||
_loadTasks();
|
_loadTasks();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../../../core/di/injection_container.dart';
|
import '../../../../core/di/injection_container.dart';
|
||||||
import '../../../../core/services/pre_planting_service.dart';
|
import '../../../../core/services/pre_planting_service.dart';
|
||||||
|
import '../../../../routes/route_paths.dart';
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// [2026-02-17] 预种合并详情页面
|
// [2026-02-17] 预种合并详情页面
|
||||||
|
|
@ -85,112 +86,14 @@ class _PrePlantingMergeDetailPageState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 签署合同
|
/// 签署合同 → 跳转到完整签合同页(PDF 展示 + 手写签名)
|
||||||
Future<void> _signContract() async {
|
Future<void> _signContract() async {
|
||||||
if (_isSigning || _merge == null) return;
|
if (_merge == null) return;
|
||||||
|
await context.push(
|
||||||
// 确认弹窗
|
'${RoutePaths.prePlantingMergeSigning}/${widget.mergeNo}',
|
||||||
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('解锁绿积分交易权限'),
|
|
||||||
_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;
|
if (mounted) _loadDetail();
|
||||||
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 返回上一页
|
/// 返回上一页
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,576 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_pdfview/flutter_pdfview.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import '../../../../core/di/injection_container.dart';
|
||||||
|
import '../../../../core/services/pre_planting_service.dart';
|
||||||
|
import '../../../contract_signing/presentation/widgets/signature_pad.dart';
|
||||||
|
|
||||||
|
/// 预种合并合同签署页面
|
||||||
|
///
|
||||||
|
/// 流程与普通认种完全一致:
|
||||||
|
/// PDF 展示 → 滚动到底部 → 确认法律效力 → 手写签名 → 提交
|
||||||
|
class PrePlantingMergeSigningPage extends ConsumerStatefulWidget {
|
||||||
|
final String mergeNo;
|
||||||
|
|
||||||
|
const PrePlantingMergeSigningPage({
|
||||||
|
super.key,
|
||||||
|
required this.mergeNo,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<PrePlantingMergeSigningPage> createState() =>
|
||||||
|
_PrePlantingMergeSigningPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PrePlantingMergeSigningPageState
|
||||||
|
extends ConsumerState<PrePlantingMergeSigningPage> {
|
||||||
|
bool _isLoading = true;
|
||||||
|
bool _isPdfLoading = true;
|
||||||
|
String? _pdfPath;
|
||||||
|
int _totalPages = 0;
|
||||||
|
int _currentPage = 0;
|
||||||
|
bool _hasScrolledToBottom = false;
|
||||||
|
bool _hasAcknowledged = false;
|
||||||
|
bool _isSubmitting = false;
|
||||||
|
bool _showSignaturePad = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
int _downloadProgress = 0;
|
||||||
|
int _downloadRetryCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadPdf();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (_pdfPath != null) {
|
||||||
|
File(_pdfPath!).delete().catchError((e) => File(_pdfPath!));
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPdf() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
_isPdfLoading = true;
|
||||||
|
_downloadProgress = 0;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final service = ref.read(prePlantingServiceProvider);
|
||||||
|
final pdfBytes = await service.downloadMergeContractPdf(
|
||||||
|
widget.mergeNo,
|
||||||
|
onProgress: (received, total) {
|
||||||
|
if (total > 0 && mounted) {
|
||||||
|
final progress = ((received / total) * 100).round();
|
||||||
|
setState(() => _downloadProgress = progress);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
final tempFile =
|
||||||
|
File('${tempDir.path}/merge_contract_${widget.mergeNo}.pdf');
|
||||||
|
await tempFile.writeAsBytes(pdfBytes);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_pdfPath = tempFile.path;
|
||||||
|
_isPdfLoading = false;
|
||||||
|
_downloadProgress = 100;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[MergeSigningPage] PDF加载失败: $e');
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isPdfLoading = false;
|
||||||
|
_downloadRetryCount++;
|
||||||
|
_errorMessage = '加载合同失败,请点击重试';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPageChanged(int? page, int? total) {
|
||||||
|
if (page == null || total == null) return;
|
||||||
|
setState(() {
|
||||||
|
_currentPage = page;
|
||||||
|
_totalPages = total;
|
||||||
|
});
|
||||||
|
if (page == total - 1 && !_hasScrolledToBottom) {
|
||||||
|
setState(() => _hasScrolledToBottom = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSignatureDialog() {
|
||||||
|
if (!_hasAcknowledged) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('请先确认合同法律效力'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _showSignaturePad = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submitSignature(
|
||||||
|
Uint8List signatureImage, SignatureTraceData traceData) async {
|
||||||
|
if (_isSubmitting) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_showSignaturePad = false;
|
||||||
|
_isSubmitting = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final service = ref.read(prePlantingServiceProvider);
|
||||||
|
await service.signMergeContract(widget.mergeNo);
|
||||||
|
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
final outerContext = context;
|
||||||
|
final canPopBack = outerContext.canPop();
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.check_circle, color: Colors.green, size: 28),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('签署成功'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: const Text('合同已签署完成,挖矿权益已开启!'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
if (canPopBack) {
|
||||||
|
outerContext.pop(true);
|
||||||
|
} else {
|
||||||
|
outerContext.go('/profile');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('完成'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _isSubmitting = false);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('签署失败: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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: _showSignaturePad
|
||||||
|
? _buildSignaturePad()
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
_buildHeader(),
|
||||||
|
if (!_isLoading && _pdfPath != null) _buildInfoBar(),
|
||||||
|
Expanded(child: _buildContent()),
|
||||||
|
if (!_isLoading && !_isPdfLoading && _errorMessage == null)
|
||||||
|
_buildBottomActions(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: () => context.pop(false),
|
||||||
|
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: () => context.pop(false),
|
||||||
|
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 _buildInfoBar() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFFE8F5E9),
|
||||||
|
border:
|
||||||
|
Border(bottom: BorderSide(color: Color(0xFF4CAF50), width: 1)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.park_outlined, color: Color(0xFF4CAF50), size: 18),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'合并编号: ${widget.mergeNo}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Color(0xFF2E7D32),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_totalPages > 0)
|
||||||
|
Text(
|
||||||
|
'${_currentPage + 1}/$_totalPages',
|
||||||
|
style: const TextStyle(fontSize: 12, color: Color(0xFF666666)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent() {
|
||||||
|
if (_isLoading) {
|
||||||
|
return const Center(
|
||||||
|
child: CircularProgressIndicator(color: Color(0xFFD4AF37)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_errorMessage != null) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.cloud_download_outlined,
|
||||||
|
color: Color(0xFFD4AF37), size: 56),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: const TextStyle(fontSize: 16, color: Color(0xFF666666)),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _loadPdf,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFFD4AF37),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 32, vertical: 12)),
|
||||||
|
icon: const Icon(Icons.refresh, color: Colors.white),
|
||||||
|
label: const Text('重试',
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 16)),
|
||||||
|
),
|
||||||
|
if (_downloadRetryCount > 0)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
child: Text(
|
||||||
|
'已自动重试 $_downloadRetryCount 次',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12, color: Color(0xFF999999)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isPdfLoading || _pdfPath == null) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(
|
||||||
|
value:
|
||||||
|
_downloadProgress > 0 ? _downloadProgress / 100 : null,
|
||||||
|
color: const Color(0xFFD4AF37),
|
||||||
|
strokeWidth: 6,
|
||||||
|
backgroundColor: const Color(0xFFE0E0E0),
|
||||||
|
),
|
||||||
|
if (_downloadProgress > 0)
|
||||||
|
Text(
|
||||||
|
'$_downloadProgress%',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFFD4AF37)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
_downloadProgress > 0
|
||||||
|
? '正在下载合同 $_downloadProgress%'
|
||||||
|
: '正在加载合同...',
|
||||||
|
style:
|
||||||
|
const TextStyle(fontSize: 14, color: Color(0xFF666666)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 6,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: PDFView(
|
||||||
|
filePath: _pdfPath!,
|
||||||
|
enableSwipe: true,
|
||||||
|
swipeHorizontal: false,
|
||||||
|
autoSpacing: true,
|
||||||
|
pageFling: true,
|
||||||
|
pageSnap: true,
|
||||||
|
fitPolicy: FitPolicy.WIDTH,
|
||||||
|
onRender: (pages) {
|
||||||
|
setState(() => _totalPages = pages ?? 0);
|
||||||
|
},
|
||||||
|
onPageChanged: _onPageChanged,
|
||||||
|
onError: (error) {
|
||||||
|
setState(() => _errorMessage = 'PDF 加载失败: $error');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!_hasScrolledToBottom && _totalPages > 0)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.swipe, color: Color(0xFFD4AF37), size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'请滑动至最后一页阅读完整合同 (${_currentPage + 1}/$_totalPages)',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFFD4AF37),
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBottomActions() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, -2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (!_hasAcknowledged)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _hasScrolledToBottom
|
||||||
|
? () => setState(() => _hasAcknowledged = true)
|
||||||
|
: null,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFE0E0E0),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: _hasScrolledToBottom
|
||||||
|
? Border.all(
|
||||||
|
color: const Color(0xFFD4AF37), width: 2)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'我已阅读并同意上述协议,确认其具有法律效力',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: _hasScrolledToBottom
|
||||||
|
? const Color(0xFF333333)
|
||||||
|
: const Color(0xFF999999),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!_hasScrolledToBottom)
|
||||||
|
const Text(
|
||||||
|
'(请先阅读完合同)',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: Color(0xFF999999)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_hasAcknowledged)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.check_circle,
|
||||||
|
color: Color(0xFF4CAF50), size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'已确认协议法律效力',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFF4CAF50),
|
||||||
|
fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _hasAcknowledged && !_isSubmitting
|
||||||
|
? _showSignatureDialog
|
||||||
|
: null,
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _hasAcknowledged
|
||||||
|
? const Color(0xFF4CAF50)
|
||||||
|
: const Color(0xFF4CAF50).withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: _hasAcknowledged
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color:
|
||||||
|
const Color(0xFF4CAF50).withValues(alpha: 0.3),
|
||||||
|
blurRadius: 15,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: _isSubmitting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2, color: Colors.white),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'签署合同',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontFamily: 'Inter',
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSignaturePad() {
|
||||||
|
return SignaturePad(
|
||||||
|
onSubmit: _submitSignature,
|
||||||
|
onCancel: () => setState(() => _showSignaturePad = false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -48,6 +48,7 @@ import '../features/pending_actions/presentation/pages/pending_actions_page.dart
|
||||||
import '../features/pre_planting/presentation/pages/pre_planting_purchase_page.dart';
|
import '../features/pre_planting/presentation/pages/pre_planting_purchase_page.dart';
|
||||||
import '../features/pre_planting/presentation/pages/pre_planting_position_page.dart';
|
import '../features/pre_planting/presentation/pages/pre_planting_position_page.dart';
|
||||||
import '../features/pre_planting/presentation/pages/pre_planting_merge_detail_page.dart';
|
import '../features/pre_planting/presentation/pages/pre_planting_merge_detail_page.dart';
|
||||||
|
import '../features/pre_planting/presentation/pages/pre_planting_merge_signing_page.dart';
|
||||||
// [2026-02-19] 纯新增:树转让页面
|
// [2026-02-19] 纯新增:树转让页面
|
||||||
import '../features/transfer/presentation/pages/transfer_list_page.dart';
|
import '../features/transfer/presentation/pages/transfer_list_page.dart';
|
||||||
import '../features/transfer/presentation/pages/transfer_detail_page.dart';
|
import '../features/transfer/presentation/pages/transfer_detail_page.dart';
|
||||||
|
|
@ -466,6 +467,16 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// [2026-02-28] Pre-Planting Merge Signing Page (预种计划 - 合并合同签署)
|
||||||
|
GoRoute(
|
||||||
|
path: '${RoutePaths.prePlantingMergeSigning}/:mergeNo',
|
||||||
|
name: RouteNames.prePlantingMergeSigning,
|
||||||
|
builder: (context, state) {
|
||||||
|
final mergeNo = state.pathParameters['mergeNo'] ?? '';
|
||||||
|
return PrePlantingMergeSigningPage(mergeNo: mergeNo);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// [2026-02-19] Transfer List Page (树转让 - 记录列表)
|
// [2026-02-19] Transfer List Page (树转让 - 记录列表)
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RoutePaths.transferList,
|
path: RoutePaths.transferList,
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,8 @@ class RouteNames {
|
||||||
// [2026-02-17] Pre-Planting (预种计划)
|
// [2026-02-17] Pre-Planting (预种计划)
|
||||||
static const prePlantingPurchase = 'pre-planting-purchase'; // 购买页
|
static const prePlantingPurchase = 'pre-planting-purchase'; // 购买页
|
||||||
static const prePlantingPosition = 'pre-planting-position'; // 持仓页
|
static const prePlantingPosition = 'pre-planting-position'; // 持仓页
|
||||||
static const prePlantingMergeDetail = 'pre-planting-merge'; // 合并详情页
|
static const prePlantingMergeDetail = 'pre-planting-merge'; // 合并详情页
|
||||||
|
static const prePlantingMergeSigning = 'pre-planting-merge-signing'; // 合并合同签署页
|
||||||
|
|
||||||
// [2026-02-19] Transfer (树转让)
|
// [2026-02-19] Transfer (树转让)
|
||||||
static const transferList = 'transfer-list'; // 转让记录列表
|
static const transferList = 'transfer-list'; // 转让记录列表
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ class RoutePaths {
|
||||||
static const prePlantingPurchase = '/pre-planting/purchase'; // 购买页
|
static const prePlantingPurchase = '/pre-planting/purchase'; // 购买页
|
||||||
static const prePlantingPosition = '/pre-planting/position'; // 持仓页
|
static const prePlantingPosition = '/pre-planting/position'; // 持仓页
|
||||||
static const prePlantingMergeDetail = '/pre-planting/merge'; // 合并详情页
|
static const prePlantingMergeDetail = '/pre-planting/merge'; // 合并详情页
|
||||||
|
static const prePlantingMergeSigning = '/pre-planting/merge-signing'; // 合并合同签署页
|
||||||
|
|
||||||
// [2026-02-19] Transfer (树转让)
|
// [2026-02-19] Transfer (树转让)
|
||||||
static const transferList = '/transfer/list'; // 转让记录列表
|
static const transferList = '/transfer/list'; // 转让记录列表
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue