feat(contract-signing): 增强签名功能
前端改进: - 签名页面添加红色醒目提示"请使用正楷书写您的真实姓名" - 签名前显示用户真实姓名供参照 - 实现全屏横向签名面板(自动切换横屏/竖屏) - 记录签名轨迹数据(每个点的坐标和时间戳、笔画顺序) 后端改进: - 扩展SignContractParams接口支持signatureTrace字段 - 控制器记录签名轨迹日志(笔画数、总时长) - 签名轨迹数据以JSON格式存储,作为法律凭证 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d4763ea5bf
commit
954f170bd4
|
|
@ -20,6 +20,33 @@ import { PdfGeneratorService } from '../../infrastructure/pdf/pdf-generator.serv
|
||||||
import { MinioStorageService } from '../../infrastructure/storage/minio-storage.service';
|
import { MinioStorageService } from '../../infrastructure/storage/minio-storage.service';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名轨迹点
|
||||||
|
*/
|
||||||
|
interface SignatureTracePoint {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
t: number; // 毫秒时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名笔画
|
||||||
|
*/
|
||||||
|
interface SignatureTraceStroke {
|
||||||
|
points: SignatureTracePoint[];
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名轨迹数据(用于法律凭证)
|
||||||
|
*/
|
||||||
|
interface SignatureTraceData {
|
||||||
|
strokes: SignatureTraceStroke[];
|
||||||
|
totalDuration: number; // 总签名时长(毫秒)
|
||||||
|
strokeCount: number; // 笔画数量
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 签署合同请求DTO
|
* 签署合同请求DTO
|
||||||
*/
|
*/
|
||||||
|
|
@ -36,6 +63,7 @@ interface SignContractDto {
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
};
|
};
|
||||||
|
signatureTrace?: SignatureTraceData; // 签名轨迹数据(可选)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -274,7 +302,12 @@ export class ContractSigningController {
|
||||||
signedPdfUrl = '';
|
signedPdfUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. 完成签署
|
// 8. 记录签名轨迹数据(如果有)
|
||||||
|
if (dto.signatureTrace) {
|
||||||
|
this.logger.log(`Signature trace: ${dto.signatureTrace.strokeCount} strokes, ${dto.signatureTrace.totalDuration}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 完成签署
|
||||||
await this.contractSigningService.signContract(orderNo, userId, {
|
await this.contractSigningService.signContract(orderNo, userId, {
|
||||||
signatureCloudUrl,
|
signatureCloudUrl,
|
||||||
signatureHash,
|
signatureHash,
|
||||||
|
|
@ -283,6 +316,7 @@ export class ContractSigningController {
|
||||||
deviceInfo: dto.deviceInfo,
|
deviceInfo: dto.deviceInfo,
|
||||||
userAgent,
|
userAgent,
|
||||||
location: dto.location,
|
location: dto.location,
|
||||||
|
signatureTrace: dto.signatureTrace ? JSON.stringify(dto.signatureTrace) : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -361,7 +395,12 @@ export class ContractSigningController {
|
||||||
signedPdfUrl = '';
|
signedPdfUrl = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. 完成补签
|
// 8. 记录签名轨迹数据(如果有)
|
||||||
|
if (dto.signatureTrace) {
|
||||||
|
this.logger.log(`Signature trace: ${dto.signatureTrace.strokeCount} strokes, ${dto.signatureTrace.totalDuration}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 完成补签
|
||||||
await this.contractSigningService.lateSignContract(orderNo, userId, {
|
await this.contractSigningService.lateSignContract(orderNo, userId, {
|
||||||
signatureCloudUrl,
|
signatureCloudUrl,
|
||||||
signatureHash,
|
signatureHash,
|
||||||
|
|
@ -370,6 +409,7 @@ export class ContractSigningController {
|
||||||
deviceInfo: dto.deviceInfo,
|
deviceInfo: dto.deviceInfo,
|
||||||
userAgent,
|
userAgent,
|
||||||
location: dto.location,
|
location: dto.location,
|
||||||
|
signatureTrace: dto.signatureTrace ? JSON.stringify(dto.signatureTrace) : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ export interface SignContractParams {
|
||||||
deviceInfo: DeviceInfo;
|
deviceInfo: DeviceInfo;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
location?: SigningLocation;
|
location?: SigningLocation;
|
||||||
|
signatureTrace?: string; // JSON 格式的签名轨迹数据(时间戳、笔画顺序)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContractSigningTask {
|
export class ContractSigningTask {
|
||||||
|
|
|
||||||
|
|
@ -404,12 +404,16 @@ class ContractSigningService {
|
||||||
String? userAgent,
|
String? userAgent,
|
||||||
double? latitude,
|
double? latitude,
|
||||||
double? longitude,
|
double? longitude,
|
||||||
|
Map<String, dynamic>? signatureTrace, // 签名轨迹数据
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
debugPrint('[ContractSigningService] 签署合同: $orderNo');
|
debugPrint('[ContractSigningService] 签署合同: $orderNo');
|
||||||
debugPrint('[ContractSigningService] 签名图片大小: ${signatureImage.length} bytes');
|
debugPrint('[ContractSigningService] 签名图片大小: ${signatureImage.length} bytes');
|
||||||
debugPrint('[ContractSigningService] IP: $ipAddress, 设备: $deviceInfo');
|
debugPrint('[ContractSigningService] IP: $ipAddress, 设备: $deviceInfo');
|
||||||
debugPrint('[ContractSigningService] 位置: lat=$latitude, lng=$longitude');
|
debugPrint('[ContractSigningService] 位置: lat=$latitude, lng=$longitude');
|
||||||
|
if (signatureTrace != null) {
|
||||||
|
debugPrint('[ContractSigningService] 签名轨迹: ${signatureTrace['strokeCount']} 笔画, ${signatureTrace['totalDuration']}ms');
|
||||||
|
}
|
||||||
|
|
||||||
// 将签名图片转为 base64
|
// 将签名图片转为 base64
|
||||||
final signatureBase64 = base64Encode(signatureImage);
|
final signatureBase64 = base64Encode(signatureImage);
|
||||||
|
|
@ -425,6 +429,7 @@ class ContractSigningService {
|
||||||
'location': latitude != null && longitude != null
|
'location': latitude != null && longitude != null
|
||||||
? {'latitude': latitude, 'longitude': longitude}
|
? {'latitude': latitude, 'longitude': longitude}
|
||||||
: null,
|
: null,
|
||||||
|
'signatureTrace': signatureTrace, // 签名轨迹数据
|
||||||
};
|
};
|
||||||
debugPrint('[ContractSigningService] 请求 URL: /planting/contract-signing/tasks/$orderNo/sign');
|
debugPrint('[ContractSigningService] 请求 URL: /planting/contract-signing/tasks/$orderNo/sign');
|
||||||
|
|
||||||
|
|
@ -470,12 +475,16 @@ class ContractSigningService {
|
||||||
String? userAgent,
|
String? userAgent,
|
||||||
double? latitude,
|
double? latitude,
|
||||||
double? longitude,
|
double? longitude,
|
||||||
|
Map<String, dynamic>? signatureTrace, // 签名轨迹数据
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
debugPrint('[ContractSigningService] 补签合同: $orderNo');
|
debugPrint('[ContractSigningService] 补签合同: $orderNo');
|
||||||
debugPrint('[ContractSigningService] 签名图片大小: ${signatureImage.length} bytes');
|
debugPrint('[ContractSigningService] 签名图片大小: ${signatureImage.length} bytes');
|
||||||
debugPrint('[ContractSigningService] IP: $ipAddress, 设备: $deviceInfo');
|
debugPrint('[ContractSigningService] IP: $ipAddress, 设备: $deviceInfo');
|
||||||
debugPrint('[ContractSigningService] 位置: lat=$latitude, lng=$longitude');
|
debugPrint('[ContractSigningService] 位置: lat=$latitude, lng=$longitude');
|
||||||
|
if (signatureTrace != null) {
|
||||||
|
debugPrint('[ContractSigningService] 签名轨迹: ${signatureTrace['strokeCount']} 笔画, ${signatureTrace['totalDuration']}ms');
|
||||||
|
}
|
||||||
|
|
||||||
final signatureBase64 = base64Encode(signatureImage);
|
final signatureBase64 = base64Encode(signatureImage);
|
||||||
debugPrint('[ContractSigningService] Base64 长度: ${signatureBase64.length}');
|
debugPrint('[ContractSigningService] Base64 长度: ${signatureBase64.length}');
|
||||||
|
|
@ -490,6 +499,7 @@ class ContractSigningService {
|
||||||
'location': latitude != null && longitude != null
|
'location': latitude != null && longitude != null
|
||||||
? {'latitude': latitude, 'longitude': longitude}
|
? {'latitude': latitude, 'longitude': longitude}
|
||||||
: null,
|
: null,
|
||||||
|
'signatureTrace': signatureTrace, // 签名轨迹数据
|
||||||
};
|
};
|
||||||
debugPrint('[ContractSigningService] 请求 URL: /planting/contract-signing/tasks/$orderNo/late-sign');
|
debugPrint('[ContractSigningService] 请求 URL: /planting/contract-signing/tasks/$orderNo/late-sign');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -274,9 +274,12 @@ class _ContractSigningPageState extends ConsumerState<ContractSigningPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 提交签名
|
/// 提交签名
|
||||||
Future<void> _submitSignature(Uint8List signatureImage) async {
|
Future<void> _submitSignature(Uint8List signatureImage, SignatureTraceData traceData) async {
|
||||||
if (_isSubmitting || _task == null) return;
|
if (_isSubmitting || _task == null) return;
|
||||||
|
|
||||||
|
// 记录轨迹数据到日志(用于调试)
|
||||||
|
debugPrint('[ContractSigningPage] 签名轨迹数据: ${traceData.strokeCount} 笔画, 总时长 ${traceData.totalDuration}ms');
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_showSignaturePad = false;
|
_showSignaturePad = false;
|
||||||
_isSubmitting = true;
|
_isSubmitting = true;
|
||||||
|
|
@ -318,6 +321,9 @@ class _ContractSigningPageState extends ConsumerState<ContractSigningPage> {
|
||||||
|
|
||||||
final service = ref.read(contractSigningServiceProvider);
|
final service = ref.read(contractSigningServiceProvider);
|
||||||
|
|
||||||
|
// 转换轨迹数据为 Map
|
||||||
|
final traceDataMap = traceData.toJson();
|
||||||
|
|
||||||
// 判断是正常签署还是补签
|
// 判断是正常签署还是补签
|
||||||
if (_task!.status == ContractSigningStatus.unsignedTimeout) {
|
if (_task!.status == ContractSigningStatus.unsignedTimeout) {
|
||||||
await service.lateSignContract(
|
await service.lateSignContract(
|
||||||
|
|
@ -328,6 +334,7 @@ class _ContractSigningPageState extends ConsumerState<ContractSigningPage> {
|
||||||
userAgent: userAgent,
|
userAgent: userAgent,
|
||||||
latitude: latitude,
|
latitude: latitude,
|
||||||
longitude: longitude,
|
longitude: longitude,
|
||||||
|
signatureTrace: traceDataMap,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await service.signContract(
|
await service.signContract(
|
||||||
|
|
@ -338,6 +345,7 @@ class _ContractSigningPageState extends ConsumerState<ContractSigningPage> {
|
||||||
userAgent: userAgent,
|
userAgent: userAgent,
|
||||||
latitude: latitude,
|
latitude: latitude,
|
||||||
longitude: longitude,
|
longitude: longitude,
|
||||||
|
signatureTrace: traceDataMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -934,16 +942,11 @@ class _ContractSigningPageState extends ConsumerState<ContractSigningPage> {
|
||||||
|
|
||||||
/// 构建签名面板
|
/// 构建签名面板
|
||||||
Widget _buildSignaturePad() {
|
Widget _buildSignaturePad() {
|
||||||
return Column(
|
// 横屏签名面板,不显示顶部 header
|
||||||
children: [
|
return SignaturePad(
|
||||||
_buildHeader(),
|
onSubmit: _submitSignature,
|
||||||
Expanded(
|
onCancel: () => setState(() => _showSignaturePad = false),
|
||||||
child: SignaturePad(
|
userName: _task?.userRealName,
|
||||||
onSubmit: _submitSignature,
|
|
||||||
onCancel: () => setState(() => _showSignaturePad = false),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,76 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// 签名轨迹点数据
|
||||||
|
class SignaturePoint {
|
||||||
|
final double x;
|
||||||
|
final double y;
|
||||||
|
final int timestamp; // 毫秒时间戳
|
||||||
|
|
||||||
|
SignaturePoint({
|
||||||
|
required this.x,
|
||||||
|
required this.y,
|
||||||
|
required this.timestamp,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'x': x,
|
||||||
|
'y': y,
|
||||||
|
't': timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 签名笔画数据
|
||||||
|
class SignatureStroke {
|
||||||
|
final List<SignaturePoint> points;
|
||||||
|
final int startTime;
|
||||||
|
final int endTime;
|
||||||
|
|
||||||
|
SignatureStroke({
|
||||||
|
required this.points,
|
||||||
|
required this.startTime,
|
||||||
|
required this.endTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'points': points.map((p) => p.toJson()).toList(),
|
||||||
|
'startTime': startTime,
|
||||||
|
'endTime': endTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 签名轨迹数据(用于法律凭证)
|
||||||
|
class SignatureTraceData {
|
||||||
|
final List<SignatureStroke> strokes;
|
||||||
|
final int totalDuration; // 总签名时长(毫秒)
|
||||||
|
final int strokeCount; // 笔画数量
|
||||||
|
|
||||||
|
SignatureTraceData({
|
||||||
|
required this.strokes,
|
||||||
|
required this.totalDuration,
|
||||||
|
required this.strokeCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'strokes': strokes.map((s) => s.toJson()).toList(),
|
||||||
|
'totalDuration': totalDuration,
|
||||||
|
'strokeCount': strokeCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// 签名面板组件
|
/// 签名面板组件
|
||||||
class SignaturePad extends StatefulWidget {
|
class SignaturePad extends StatefulWidget {
|
||||||
final Function(Uint8List) onSubmit;
|
final Function(Uint8List, SignatureTraceData) onSubmit;
|
||||||
final VoidCallback onCancel;
|
final VoidCallback onCancel;
|
||||||
|
final String? userName; // 用户姓名,用于显示参照
|
||||||
|
|
||||||
const SignaturePad({
|
const SignaturePad({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
required this.onCancel,
|
required this.onCancel,
|
||||||
|
this.userName,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -18,20 +78,56 @@ class SignaturePad extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SignaturePadState extends State<SignaturePad> {
|
class _SignaturePadState extends State<SignaturePad> {
|
||||||
/// 签名路径列表
|
/// 签名路径列表(用于绘制)
|
||||||
final List<List<Offset>> _strokes = [];
|
final List<List<Offset>> _strokes = [];
|
||||||
|
|
||||||
/// 当前笔画
|
/// 当前笔画
|
||||||
List<Offset> _currentStroke = [];
|
List<Offset> _currentStroke = [];
|
||||||
|
|
||||||
|
/// 签名轨迹数据(带时间戳)
|
||||||
|
final List<SignatureStroke> _traceStrokes = [];
|
||||||
|
|
||||||
|
/// 当前笔画的轨迹点
|
||||||
|
List<SignaturePoint> _currentTracePoints = [];
|
||||||
|
|
||||||
|
/// 当前笔画开始时间
|
||||||
|
int? _currentStrokeStartTime;
|
||||||
|
|
||||||
|
/// 签名开始时间
|
||||||
|
int? _signatureStartTime;
|
||||||
|
|
||||||
/// 是否有签名
|
/// 是否有签名
|
||||||
bool get _hasSignature => _strokes.isNotEmpty || _currentStroke.isNotEmpty;
|
bool get _hasSignature => _strokes.isNotEmpty || _currentStroke.isNotEmpty;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// 强制横屏显示
|
||||||
|
SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.landscapeLeft,
|
||||||
|
DeviceOrientation.landscapeRight,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
// 恢复竖屏
|
||||||
|
SystemChrome.setPreferredOrientations([
|
||||||
|
DeviceOrientation.portraitUp,
|
||||||
|
DeviceOrientation.portraitDown,
|
||||||
|
]);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
/// 清除签名
|
/// 清除签名
|
||||||
void _clear() {
|
void _clear() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_strokes.clear();
|
_strokes.clear();
|
||||||
_currentStroke.clear();
|
_currentStroke.clear();
|
||||||
|
_traceStrokes.clear();
|
||||||
|
_currentTracePoints.clear();
|
||||||
|
_currentStrokeStartTime = null;
|
||||||
|
_signatureStartTime = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,7 +146,15 @@ class _SignaturePadState extends State<SignaturePad> {
|
||||||
// 将签名转换为图片
|
// 将签名转换为图片
|
||||||
final image = await _renderSignatureImage();
|
final image = await _renderSignatureImage();
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
widget.onSubmit(image);
|
// 构建轨迹数据
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final totalDuration = _signatureStartTime != null ? now - _signatureStartTime! : 0;
|
||||||
|
final traceData = SignatureTraceData(
|
||||||
|
strokes: _traceStrokes,
|
||||||
|
totalDuration: totalDuration,
|
||||||
|
strokeCount: _traceStrokes.length,
|
||||||
|
);
|
||||||
|
widget.onSubmit(image, traceData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,168 +212,282 @@ class _SignaturePadState extends State<SignaturePad> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 处理笔画开始
|
||||||
|
void _onPanStart(DragStartDetails details) {
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
// 记录签名开始时间
|
||||||
|
_signatureStartTime ??= now;
|
||||||
|
|
||||||
|
// 记录笔画开始时间
|
||||||
|
_currentStrokeStartTime = now;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_currentStroke = [details.localPosition];
|
||||||
|
_currentTracePoints = [
|
||||||
|
SignaturePoint(
|
||||||
|
x: details.localPosition.dx,
|
||||||
|
y: details.localPosition.dy,
|
||||||
|
timestamp: now,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理笔画更新
|
||||||
|
void _onPanUpdate(DragUpdateDetails details) {
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_currentStroke.add(details.localPosition);
|
||||||
|
_currentTracePoints.add(
|
||||||
|
SignaturePoint(
|
||||||
|
x: details.localPosition.dx,
|
||||||
|
y: details.localPosition.dy,
|
||||||
|
timestamp: now,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理笔画结束
|
||||||
|
void _onPanEnd(DragEndDetails details) {
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (_currentStroke.isNotEmpty) {
|
||||||
|
_strokes.add(List.from(_currentStroke));
|
||||||
|
_currentStroke.clear();
|
||||||
|
|
||||||
|
// 保存轨迹数据
|
||||||
|
if (_currentTracePoints.isNotEmpty && _currentStrokeStartTime != null) {
|
||||||
|
_traceStrokes.add(
|
||||||
|
SignatureStroke(
|
||||||
|
points: List.from(_currentTracePoints),
|
||||||
|
startTime: _currentStrokeStartTime!,
|
||||||
|
endTime: now,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_currentTracePoints.clear();
|
||||||
|
_currentStrokeStartTime = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
color: const Color(0xFFFFF7E6),
|
color: const Color(0xFFFFF7E6),
|
||||||
child: Column(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// 标题
|
// 左侧操作区
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
width: 100,
|
||||||
child: const Text(
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
|
||||||
'请在下方区域签名',
|
child: Column(
|
||||||
style: TextStyle(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Color(0xFF5D4037),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 签名区域
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(
|
|
||||||
color: const Color(0xFFD4AF37),
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withValues(alpha: 0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: GestureDetector(
|
|
||||||
onPanStart: (details) {
|
|
||||||
setState(() {
|
|
||||||
_currentStroke = [details.localPosition];
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onPanUpdate: (details) {
|
|
||||||
setState(() {
|
|
||||||
_currentStroke.add(details.localPosition);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onPanEnd: (details) {
|
|
||||||
setState(() {
|
|
||||||
if (_currentStroke.isNotEmpty) {
|
|
||||||
_strokes.add(List.from(_currentStroke));
|
|
||||||
_currentStroke.clear();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: CustomPaint(
|
|
||||||
painter: _SignaturePainter(
|
|
||||||
strokes: _strokes,
|
|
||||||
currentStroke: _currentStroke,
|
|
||||||
),
|
|
||||||
child: Container(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 提示文字
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Text(
|
|
||||||
_hasSignature ? '已签名,点击确认提交' : '在白色区域内签名',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: _hasSignature ? const Color(0xFF4CAF50) : const Color(0xFF999999),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 操作按钮
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
children: [
|
||||||
// 取消按钮
|
// 取消按钮
|
||||||
Expanded(
|
GestureDetector(
|
||||||
child: GestureDetector(
|
onTap: widget.onCancel,
|
||||||
onTap: widget.onCancel,
|
child: Container(
|
||||||
child: Container(
|
width: 80,
|
||||||
height: 56,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: const Color(0xFFD4AF37),
|
color: const Color(0xFFD4AF37),
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: const Center(
|
),
|
||||||
child: Text(
|
child: const Center(
|
||||||
'取消',
|
child: Text(
|
||||||
style: TextStyle(
|
'取消',
|
||||||
fontSize: 16,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontSize: 14,
|
||||||
color: Color(0xFFD4AF37),
|
fontWeight: FontWeight.w600,
|
||||||
),
|
color: Color(0xFFD4AF37),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
|
||||||
// 清除按钮
|
// 清除按钮
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _clear,
|
onTap: _clear,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 56,
|
width: 80,
|
||||||
height: 56,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFFEBEE),
|
color: const Color(0xFFFFEBEE),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Row(
|
||||||
Icons.refresh,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
color: Color(0xFFE53935),
|
children: [
|
||||||
size: 24,
|
Icon(
|
||||||
|
Icons.refresh,
|
||||||
|
color: Color(0xFFE53935),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'清除',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFFE53935),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
],
|
||||||
// 确认按钮
|
),
|
||||||
Expanded(
|
),
|
||||||
child: GestureDetector(
|
// 中间签名区域
|
||||||
onTap: _submit,
|
Expanded(
|
||||||
child: Container(
|
child: Column(
|
||||||
height: 56,
|
children: [
|
||||||
decoration: BoxDecoration(
|
// 顶部提示和姓名显示
|
||||||
color: _hasSignature
|
Container(
|
||||||
? const Color(0xFFD4AF37)
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
: const Color(0xFFD4AF37).withValues(alpha: 0.5),
|
child: Column(
|
||||||
borderRadius: BorderRadius.circular(12),
|
children: [
|
||||||
boxShadow: _hasSignature
|
// 主提示文字
|
||||||
? [
|
const Text(
|
||||||
BoxShadow(
|
'请使用正楷书写您的真实姓名',
|
||||||
color: const Color(0xFFD4AF37).withValues(alpha: 0.3),
|
style: TextStyle(
|
||||||
blurRadius: 15,
|
fontSize: 16,
|
||||||
offset: const Offset(0, 8),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
color: Color(0xFFE53935),
|
||||||
]
|
),
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
child: const Center(
|
const SizedBox(height: 4),
|
||||||
child: Text(
|
// 显示用户姓名供参照
|
||||||
'确认签名',
|
if (widget.userName != null && widget.userName!.isNotEmpty)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'请签署:',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFF666666),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.userName!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF5D4037),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 签名区域
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.fromLTRB(0, 0, 0, 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: const Color(0xFFD4AF37),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: GestureDetector(
|
||||||
|
onPanStart: _onPanStart,
|
||||||
|
onPanUpdate: _onPanUpdate,
|
||||||
|
onPanEnd: _onPanEnd,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: _SignaturePainter(
|
||||||
|
strokes: _strokes,
|
||||||
|
currentStroke: _currentStroke,
|
||||||
|
),
|
||||||
|
child: Container(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 底部状态提示
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Text(
|
||||||
|
_hasSignature ? '已签名,点击右侧确认提交' : '在白色区域内签名',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: _hasSignature
|
||||||
|
? const Color(0xFF4CAF50)
|
||||||
|
: const Color(0xFF999999),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 右侧确认按钮
|
||||||
|
Container(
|
||||||
|
width: 100,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _submit,
|
||||||
|
child: Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _hasSignature
|
||||||
|
? const Color(0xFFD4AF37)
|
||||||
|
: const Color(0xFFD4AF37).withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(40),
|
||||||
|
boxShadow: _hasSignature
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color:
|
||||||
|
const Color(0xFFD4AF37).withValues(alpha: 0.3),
|
||||||
|
blurRadius: 15,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: const Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'确认',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue