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:
hailin 2026-02-28 10:35:22 -08:00
parent 2ad1936126
commit b1e5e6b29f
9 changed files with 685 additions and 109 deletions

View File

@ -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: '合并记录详情' })

View File

@ -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 + 使
* *

View File

@ -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}');

View File

@ -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();
} }

View File

@ -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,
),
);
}
}
} }
/// ///

View File

@ -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),
);
}
}

View File

@ -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,

View File

@ -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'; //

View File

@ -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'; //