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 * 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
|
||||
*/
|
||||
|
|
@ -36,6 +63,7 @@ interface SignContractDto {
|
|||
latitude?: number;
|
||||
longitude?: number;
|
||||
};
|
||||
signatureTrace?: SignatureTraceData; // 签名轨迹数据(可选)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -274,7 +302,12 @@ export class ContractSigningController {
|
|||
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, {
|
||||
signatureCloudUrl,
|
||||
signatureHash,
|
||||
|
|
@ -283,6 +316,7 @@ export class ContractSigningController {
|
|||
deviceInfo: dto.deviceInfo,
|
||||
userAgent,
|
||||
location: dto.location,
|
||||
signatureTrace: dto.signatureTrace ? JSON.stringify(dto.signatureTrace) : undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -361,7 +395,12 @@ export class ContractSigningController {
|
|||
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, {
|
||||
signatureCloudUrl,
|
||||
signatureHash,
|
||||
|
|
@ -370,6 +409,7 @@ export class ContractSigningController {
|
|||
deviceInfo: dto.deviceInfo,
|
||||
userAgent,
|
||||
location: dto.location,
|
||||
signatureTrace: dto.signatureTrace ? JSON.stringify(dto.signatureTrace) : undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export interface SignContractParams {
|
|||
deviceInfo: DeviceInfo;
|
||||
userAgent: string;
|
||||
location?: SigningLocation;
|
||||
signatureTrace?: string; // JSON 格式的签名轨迹数据(时间戳、笔画顺序)
|
||||
}
|
||||
|
||||
export class ContractSigningTask {
|
||||
|
|
|
|||
|
|
@ -404,12 +404,16 @@ class ContractSigningService {
|
|||
String? userAgent,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
Map<String, dynamic>? signatureTrace, // 签名轨迹数据
|
||||
}) async {
|
||||
try {
|
||||
debugPrint('[ContractSigningService] 签署合同: $orderNo');
|
||||
debugPrint('[ContractSigningService] 签名图片大小: ${signatureImage.length} bytes');
|
||||
debugPrint('[ContractSigningService] IP: $ipAddress, 设备: $deviceInfo');
|
||||
debugPrint('[ContractSigningService] 位置: lat=$latitude, lng=$longitude');
|
||||
if (signatureTrace != null) {
|
||||
debugPrint('[ContractSigningService] 签名轨迹: ${signatureTrace['strokeCount']} 笔画, ${signatureTrace['totalDuration']}ms');
|
||||
}
|
||||
|
||||
// 将签名图片转为 base64
|
||||
final signatureBase64 = base64Encode(signatureImage);
|
||||
|
|
@ -425,6 +429,7 @@ class ContractSigningService {
|
|||
'location': latitude != null && longitude != null
|
||||
? {'latitude': latitude, 'longitude': longitude}
|
||||
: null,
|
||||
'signatureTrace': signatureTrace, // 签名轨迹数据
|
||||
};
|
||||
debugPrint('[ContractSigningService] 请求 URL: /planting/contract-signing/tasks/$orderNo/sign');
|
||||
|
||||
|
|
@ -470,12 +475,16 @@ class ContractSigningService {
|
|||
String? userAgent,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
Map<String, dynamic>? signatureTrace, // 签名轨迹数据
|
||||
}) async {
|
||||
try {
|
||||
debugPrint('[ContractSigningService] 补签合同: $orderNo');
|
||||
debugPrint('[ContractSigningService] 签名图片大小: ${signatureImage.length} bytes');
|
||||
debugPrint('[ContractSigningService] IP: $ipAddress, 设备: $deviceInfo');
|
||||
debugPrint('[ContractSigningService] 位置: lat=$latitude, lng=$longitude');
|
||||
if (signatureTrace != null) {
|
||||
debugPrint('[ContractSigningService] 签名轨迹: ${signatureTrace['strokeCount']} 笔画, ${signatureTrace['totalDuration']}ms');
|
||||
}
|
||||
|
||||
final signatureBase64 = base64Encode(signatureImage);
|
||||
debugPrint('[ContractSigningService] Base64 长度: ${signatureBase64.length}');
|
||||
|
|
@ -490,6 +499,7 @@ class ContractSigningService {
|
|||
'location': latitude != null && longitude != null
|
||||
? {'latitude': latitude, 'longitude': longitude}
|
||||
: null,
|
||||
'signatureTrace': signatureTrace, // 签名轨迹数据
|
||||
};
|
||||
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;
|
||||
|
||||
// 记录轨迹数据到日志(用于调试)
|
||||
debugPrint('[ContractSigningPage] 签名轨迹数据: ${traceData.strokeCount} 笔画, 总时长 ${traceData.totalDuration}ms');
|
||||
|
||||
setState(() {
|
||||
_showSignaturePad = false;
|
||||
_isSubmitting = true;
|
||||
|
|
@ -318,6 +321,9 @@ class _ContractSigningPageState extends ConsumerState<ContractSigningPage> {
|
|||
|
||||
final service = ref.read(contractSigningServiceProvider);
|
||||
|
||||
// 转换轨迹数据为 Map
|
||||
final traceDataMap = traceData.toJson();
|
||||
|
||||
// 判断是正常签署还是补签
|
||||
if (_task!.status == ContractSigningStatus.unsignedTimeout) {
|
||||
await service.lateSignContract(
|
||||
|
|
@ -328,6 +334,7 @@ class _ContractSigningPageState extends ConsumerState<ContractSigningPage> {
|
|||
userAgent: userAgent,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
signatureTrace: traceDataMap,
|
||||
);
|
||||
} else {
|
||||
await service.signContract(
|
||||
|
|
@ -338,6 +345,7 @@ class _ContractSigningPageState extends ConsumerState<ContractSigningPage> {
|
|||
userAgent: userAgent,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
signatureTrace: traceDataMap,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -934,16 +942,11 @@ class _ContractSigningPageState extends ConsumerState<ContractSigningPage> {
|
|||
|
||||
/// 构建签名面板
|
||||
Widget _buildSignaturePad() {
|
||||
return Column(
|
||||
children: [
|
||||
_buildHeader(),
|
||||
Expanded(
|
||||
child: SignaturePad(
|
||||
onSubmit: _submitSignature,
|
||||
onCancel: () => setState(() => _showSignaturePad = false),
|
||||
),
|
||||
),
|
||||
],
|
||||
// 横屏签名面板,不显示顶部 header
|
||||
return SignaturePad(
|
||||
onSubmit: _submitSignature,
|
||||
onCancel: () => setState(() => _showSignaturePad = false),
|
||||
userName: _task?.userRealName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,76 @@
|
|||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
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 {
|
||||
final Function(Uint8List) onSubmit;
|
||||
final Function(Uint8List, SignatureTraceData) onSubmit;
|
||||
final VoidCallback onCancel;
|
||||
final String? userName; // 用户姓名,用于显示参照
|
||||
|
||||
const SignaturePad({
|
||||
super.key,
|
||||
required this.onSubmit,
|
||||
required this.onCancel,
|
||||
this.userName,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -18,20 +78,56 @@ class SignaturePad extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SignaturePadState extends State<SignaturePad> {
|
||||
/// 签名路径列表
|
||||
/// 签名路径列表(用于绘制)
|
||||
final List<List<Offset>> _strokes = [];
|
||||
|
||||
/// 当前笔画
|
||||
List<Offset> _currentStroke = [];
|
||||
|
||||
/// 签名轨迹数据(带时间戳)
|
||||
final List<SignatureStroke> _traceStrokes = [];
|
||||
|
||||
/// 当前笔画的轨迹点
|
||||
List<SignaturePoint> _currentTracePoints = [];
|
||||
|
||||
/// 当前笔画开始时间
|
||||
int? _currentStrokeStartTime;
|
||||
|
||||
/// 签名开始时间
|
||||
int? _signatureStartTime;
|
||||
|
||||
/// 是否有签名
|
||||
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() {
|
||||
setState(() {
|
||||
_strokes.clear();
|
||||
_currentStroke.clear();
|
||||
_traceStrokes.clear();
|
||||
_currentTracePoints.clear();
|
||||
_currentStrokeStartTime = null;
|
||||
_signatureStartTime = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +146,15 @@ class _SignaturePadState extends State<SignaturePad> {
|
|||
// 将签名转换为图片
|
||||
final image = await _renderSignatureImage();
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: const Color(0xFFFFF7E6),
|
||||
child: Column(
|
||||
child: Row(
|
||||
children: [
|
||||
// 标题
|
||||
// 左侧操作区
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: const Text(
|
||||
'请在下方区域签名',
|
||||
style: TextStyle(
|
||||
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(
|
||||
width: 100,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// 取消按钮
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: widget.onCancel,
|
||||
child: Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFD4AF37),
|
||||
width: 1,
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: widget.onCancel,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFD4AF37),
|
||||
width: 1,
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFFD4AF37),
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFFD4AF37),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 清除按钮
|
||||
GestureDetector(
|
||||
onTap: _clear,
|
||||
child: Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
width: 80,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFEBEE),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.refresh,
|
||||
color: Color(0xFFE53935),
|
||||
size: 24,
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
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,
|
||||
child: Container(
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: _hasSignature
|
||||
? const Color(0xFFD4AF37)
|
||||
: const Color(0xFFD4AF37).withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: _hasSignature
|
||||
? [
|
||||
BoxShadow(
|
||||
color: const Color(0xFFD4AF37).withValues(alpha: 0.3),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
],
|
||||
),
|
||||
),
|
||||
// 中间签名区域
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部提示和姓名显示
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
// 主提示文字
|
||||
const Text(
|
||||
'请使用正楷书写您的真实姓名',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFE53935),
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'确认签名',
|
||||
const SizedBox(height: 4),
|
||||
// 显示用户姓名供参照
|
||||
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(
|
||||
fontSize: 16,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in New Issue