Browse Source

增加电子签名

mq 11 months ago
parent
commit
bf90b01b8c

+ 2 - 0
lib/base/base_state.dart

@@ -20,11 +20,13 @@ abstract class BaseState<T extends StatefulWidget> extends State<T> {
20 20
     String? titleIcon,
21 21
     bool autoLeading = true,
22 22
     PreferredSizeWidget? bottom,
23
+    double? toolbarHeight,
23 24
   }) {
24 25
     return AppBar(
25 26
       title: Text(title),
26 27
       bottom: bottom,
27 28
       automaticallyImplyLeading: autoLeading,
29
+      toolbarHeight: toolbarHeight,
28 30
     );
29 31
   }
30 32
 

+ 11 - 3
lib/model/rsp/sample_task_rsp.dart

@@ -111,7 +111,7 @@ class SampleTaskItem {
111 111
   num? jypzStatus; // 检验品质按钮0显示1不显示
112 112
   num? tqqk; // 天气情况
113 113
   List<NonghuItem>? codeSamplingNonghuList; // 扦样农户信息
114
-
114
+  String? xzqhCode; // 行政区划六位编码
115 115
 
116 116
   SampleTaskItem({
117 117
     this.id,
@@ -199,6 +199,8 @@ class SampleTaskItem {
199 199
     this.jyjgxxRespVOList,
200 200
     this.jypzStatus,
201 201
     this.tqqk,
202
+    this.codeSamplingNonghuList,
203
+    this.xzqhCode,
202 204
   });
203 205
 
204 206
   SampleTaskItem createUI() {
@@ -286,7 +288,7 @@ class SampleTaskItem {
286 288
     if (nonghuList != null && nonghuList.isNotEmpty) {
287 289
       map['codeSamplingNonghuList'] = nonghuList.map((e) => e.toReqJson()).toList();
288 290
     }
289
-
291
+    map['xzqhCode'] = xzqhCode;
290 292
     return map;
291 293
   }
292 294
 }
@@ -429,20 +431,27 @@ class UseMedicineItem {
429 431
     return map;
430 432
   }
431 433
 }
434
+
432 435
 @JsonSerializable(converters: [NumConverter(), StringConverter()])
433 436
 class NonghuItem {
434 437
   /// ID
435 438
   num? id;
439
+
436 440
   /// 被调查农户或合作社
437 441
   String? bdcnhhhzs;
442
+
438 443
   /// 扦样数量(公斤)
439 444
   num? qysl;
445
+
440 446
   /// 联系方式
441 447
   String? lxfs;
448
+
442 449
   /// 扦样代表数量(公斤)
443 450
   num? qydbsl;
451
+
444 452
   /// 收购扦样任务单ID
445 453
   num? zjCodeSamplingTaskDetailsSgjcId;
454
+
446 455
   /// 库存扦样任务单ID
447 456
   num? zjCodeSamplingTaskDetailsKcjcId;
448 457
 
@@ -471,7 +480,6 @@ class NonghuItem {
471 480
   }
472 481
 }
473 482
 
474
-
475 483
 @JsonSerializable(converters: [NumConverter(), StringConverter()])
476 484
 class SamplingTaskAllotSgjcItem {
477 485
   final num? id; //id

+ 6 - 4
lib/model/rsp/sample_task_rsp.g.dart

@@ -122,10 +122,11 @@ SampleTaskItem _$SampleTaskItemFromJson(Map<String, dynamic> json) =>
122 122
           .toList(),
123 123
       jypzStatus: const NumConverter().fromJson(json['jypzStatus']),
124 124
       tqqk: const NumConverter().fromJson(json['tqqk']),
125
-    )..codeSamplingNonghuList =
126
-        (json['codeSamplingNonghuList'] as List<dynamic>?)
127
-            ?.map((e) => NonghuItem.fromJson(e as Map<String, dynamic>))
128
-            .toList();
125
+      codeSamplingNonghuList: (json['codeSamplingNonghuList'] as List<dynamic>?)
126
+          ?.map((e) => NonghuItem.fromJson(e as Map<String, dynamic>))
127
+          .toList(),
128
+      xzqhCode: const StringConverter().fromJson(json['xzqhCode']),
129
+    );
129 130
 
130 131
 Map<String, dynamic> _$SampleTaskItemToJson(SampleTaskItem instance) =>
131 132
     <String, dynamic>{
@@ -221,6 +222,7 @@ Map<String, dynamic> _$SampleTaskItemToJson(SampleTaskItem instance) =>
221 222
       'jypzStatus': const NumConverter().toJson(instance.jypzStatus),
222 223
       'tqqk': const NumConverter().toJson(instance.tqqk),
223 224
       'codeSamplingNonghuList': instance.codeSamplingNonghuList,
225
+      'xzqhCode': const StringConverter().toJson(instance.xzqhCode),
224 226
     };
225 227
 
226 228
 EnterpriseItem _$EnterpriseItemFromJson(Map<String, dynamic> json) =>

+ 6 - 0
lib/network/api.dart

@@ -1,3 +1,5 @@
1
+import 'dart:io';
2
+
1 3
 import 'package:dio/dio.dart';
2 4
 import 'package:lszlgl/model/api_rsp.dart';
3 5
 import 'package:lszlgl/model/req/login_req.dart';
@@ -77,4 +79,8 @@ abstract class Api {
77 79
     @Query('uLevel') num uLevel, {
78 80
     @Query('id') num? id,
79 81
   });
82
+
83
+  /// 上传图片
84
+  @POST('/admin-api/infra/file/upload')
85
+  Future<void> upload(@Part(name: 'path', contentType: 'image/png') File path);
80 86
 }

+ 31 - 0
lib/network/api.g.dart

@@ -476,6 +476,37 @@ class _Api implements Api {
476 476
     return value;
477 477
   }
478 478
 
479
+  @override
480
+  Future<void> upload(File path) async {
481
+    const _extra = <String, dynamic>{};
482
+    final queryParameters = <String, dynamic>{};
483
+    final _headers = <String, dynamic>{};
484
+    final _data = FormData();
485
+    _data.files.add(MapEntry(
486
+      'path',
487
+      MultipartFile.fromFileSync(
488
+        path.path,
489
+        filename: path.path.split(Platform.pathSeparator).last,
490
+      ),
491
+    ));
492
+    await _dio.fetch<void>(_setStreamType<void>(Options(
493
+      method: 'POST',
494
+      headers: _headers,
495
+      extra: _extra,
496
+    )
497
+        .compose(
498
+          _dio.options,
499
+          '/admin-api/infra/file/upload',
500
+          queryParameters: queryParameters,
501
+          data: _data,
502
+        )
503
+        .copyWith(
504
+            baseUrl: _combineBaseUrls(
505
+          _dio.options.baseUrl,
506
+          baseUrl,
507
+        ))));
508
+  }
509
+
479 510
   RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
480 511
     if (T != dynamic &&
481 512
         !(requestOptions.responseType == ResponseType.bytes ||

+ 6 - 1
lib/network/base_dio.dart

@@ -75,7 +75,12 @@ class MyInterceptor extends Interceptor {
75 75
     map['url'] = options.uri;
76 76
     map['method'] = options.method;
77 77
     map['headers'] = options.headers;
78
-    map['data'] = jsonEncode(options.data);
78
+
79
+    if (options.data is List || options.data is Map) {
80
+      map['data'] = jsonEncode(options.data);
81
+    } else {
82
+      map['data'] = options.data.toString();
83
+    }
79 84
     if (options.queryParameters.isNotEmpty) map['queryParameters'] = options.queryParameters;
80 85
     if (options.extra.isNotEmpty) map['extra'] = options.extra;
81 86
     logger.i('ApiRequest: $map');

+ 2 - 0
lib/page/sample_task/reap_sample_detail/reap_sample_basic_detail_page.dart

@@ -169,10 +169,12 @@ class _ReapSampleBasicDetailPageState extends BaseLifecycleState<ReapSampleBasic
169 169
       String district = value['district'] as String;
170 170
       String street = value['street'] as String;
171 171
       String streetNumber = value['streetNumber'] as String;
172
+      String adCode = value['adCode'] as String;
172 173
       if (province == city) {
173 174
         city = district;
174 175
         district = '';
175 176
       }
177
+      data?.xzqhCode = adCode;
176 178
       // if (value['province'] != data?.sheng) {
177 179
       //   MyNavigator.showToast('扦样地点有误,请检查扦样人员所在地点。');
178 180
       //   return;

+ 16 - 0
lib/page/sample_task/reap_sample_detail/reap_sample_task_page.dart

@@ -76,6 +76,22 @@ class _ReapSampleTaskPageState extends BaseLifecycleState<ReapSampleTaskPage> wi
76 76
   }
77 77
 
78 78
   void submit() async {
79
+    /*
80
+    // 去签名
81
+    List<Uint8List?>? list = await MyRouter.startSignature();
82
+    if (list == null || list.isEmpty) return;
83
+    // 字节转文件
84
+    List<File> fileList = [];
85
+    for (int i = 0; i < list.length; i++) {
86
+      fileList.add(await FileUtils.convertUint8ListToFile(list[i]!, 'signatrue_$i.png'));
87
+    }
88
+    logger.d('图片:${fileList.map((e) async => await e.length()).toList()}');
89
+    // 上传图片
90
+    await MyApi.get().upload(fileList.first);
91
+    return;
92
+
93
+     */
94
+
79 95
     MyNavigator.showLoading();
80 96
     // 已分解
81 97
     pageStatus.value.data?.state = 2;

+ 145 - 1
lib/page/signature/signature_page.dart

@@ -1,6 +1,8 @@
1 1
 import 'package:flutter/material.dart';
2 2
 import 'package:flutter/services.dart';
3 3
 import 'package:lszlgl/base/base_lifecycle_state.dart';
4
+import 'package:signature/signature.dart';
5
+import 'package:lszlgl/widget/button.dart';
4 6
 
5 7
 class SignaturePageArgs {
6 8
   /// 签名数量
@@ -28,12 +30,82 @@ class SignaturePage extends StatefulWidget {
28 30
 }
29 31
 
30 32
 class _SignaturePageState extends BaseLifecycleState<SignaturePage> {
33
+  late int count;
34
+  late List<SignatureController> ctrlList;
35
+  late PageController pageCtrl;
36
+
37
+  final previousEnable = false.notifier<bool>();
38
+  final nextEnable = false.notifier<bool>();
39
+  final submitEnable = false.notifier<bool>();
40
+
41
+  /// 清除当前签名
42
+  void clearCurrent() {
43
+    ctrlList[pageCtrl.page!.toInt()].clear();
44
+  }
45
+
46
+  /// 上一个
47
+  void previousPage() {
48
+    pageCtrl.jumpToPage(pageCtrl.page!.toInt() - 1);
49
+    if (pageCtrl.page == 0) {
50
+      previousEnable.value = false;
51
+    }
52
+    nextEnable.value = true;
53
+    submitEnable.value = false;
54
+  }
55
+
56
+  /// 下一个
57
+  void nextPage() {
58
+    if (!checkCurrentPage()) return;
59
+    pageCtrl.jumpToPage(pageCtrl.page!.toInt() + 1);
60
+    if (pageCtrl.page == count - 1) {
61
+      nextEnable.value = false;
62
+      submitEnable.value = true;
63
+    }
64
+    previousEnable.value = true;
65
+  }
66
+
67
+  void submit() async {
68
+    if (!checkCurrentPage()) return;
69
+    // 取出所有签名资源
70
+    List<Uint8List?> images = [];
71
+    for (var ctrl in ctrlList) {
72
+      images.add(await ctrl.toPngBytes());
73
+    }
74
+    MyNavigator.pop(images);
75
+  }
76
+
77
+  bool checkCurrentPage() {
78
+    if (ctrlList[pageCtrl.page!.toInt()].isEmpty) {
79
+      MyNavigator.showToast('请先签名');
80
+      return false;
81
+    }
82
+    return true;
83
+  }
84
+
31 85
   @override
32 86
   void onInit() {
87
+    // 强制横屏
33 88
     SystemChrome.setPreferredOrientations([
34 89
       DeviceOrientation.landscapeLeft,
35 90
       DeviceOrientation.landscapeRight,
36 91
     ]);
92
+    // 创建签名列表
93
+    count = widget.args.count;
94
+    ctrlList = List.generate(
95
+      count,
96
+      (index) => SignatureController(
97
+        penStrokeWidth: 6,
98
+        penColor: Colors.black,
99
+        strokeJoin: StrokeJoin.round,
100
+        strokeCap: StrokeCap.round,
101
+      ),
102
+    ).toList();
103
+    pageCtrl = PageController();
104
+    if (count == 1) {
105
+      submitEnable.value = true;
106
+    } else {
107
+      nextEnable.value = true;
108
+    }
37 109
   }
38 110
 
39 111
   @override
@@ -42,10 +114,82 @@ class _SignaturePageState extends BaseLifecycleState<SignaturePage> {
42 114
       DeviceOrientation.portraitUp,
43 115
       DeviceOrientation.portraitDown,
44 116
     ]);
117
+    for (var ctrl in ctrlList) {
118
+      ctrl.dispose();
119
+    }
45 120
   }
46 121
 
47 122
   @override
48 123
   Widget build(BuildContext context) {
49
-    return const Placeholder();
124
+    return Scaffold(
125
+      body: Stack(
126
+        children: [
127
+          SizedBox(width: double.infinity, child: Image.asset(imgHomeTopBg, fit: BoxFit.cover)),
128
+          buildBody(),
129
+        ],
130
+      ),
131
+    );
132
+  }
133
+
134
+  Widget buildBody() {
135
+    return Column(
136
+      children: [
137
+        myAppBar(title: '电子签名', toolbarHeight: 32),
138
+        Expanded(
139
+          child: Container(
140
+            decoration: const BoxDecoration(
141
+              color: Colors.white,
142
+            ),
143
+            width: double.infinity,
144
+            child: PageView.builder(
145
+              controller: pageCtrl,
146
+              physics: const NeverScrollableScrollPhysics(),
147
+              itemBuilder: (_, index) => Signature(
148
+                controller: ctrlList[index],
149
+                backgroundColor: Colors.transparent,
150
+              ),
151
+            ),
152
+          ),
153
+        ),
154
+        Row(
155
+          children: [
156
+            Expanded(
157
+              child: MyButton(
158
+                '清除重写',
159
+                onTap: clearCurrent,
160
+                margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
161
+              ),
162
+            ),
163
+            previousEnable.builder((v) => v
164
+                ? Expanded(
165
+                    child: MyButton(
166
+                      '上一位',
167
+                      onTap: previousPage,
168
+                      margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
169
+                    ),
170
+                  )
171
+                : const SizedBox.shrink()),
172
+            nextEnable.builder((v) => v
173
+                ? Expanded(
174
+                    child: MyButton(
175
+                      '下一位',
176
+                      onTap: nextPage,
177
+                      margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
178
+                    ),
179
+                  )
180
+                : const SizedBox.shrink()),
181
+            submitEnable.builder((v) => v
182
+                ? Expanded(
183
+                    child: MyButton(
184
+                      '提交',
185
+                      onTap: submit,
186
+                      margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
187
+                    ),
188
+                  )
189
+                : const SizedBox.shrink()),
190
+          ],
191
+        )
192
+      ],
193
+    );
50 194
   }
51 195
 }

+ 14 - 0
lib/utils/file_utils.dart

@@ -0,0 +1,14 @@
1
+import 'dart:io';
2
+import 'dart:typed_data';
3
+
4
+import 'package:path_provider/path_provider.dart';
5
+
6
+class FileUtils {
7
+  static Future<File> convertUint8ListToFile(Uint8List data, String fileName) async {
8
+    final directory = await getApplicationCacheDirectory();
9
+    final filePath = '${directory.path}/$fileName';
10
+    final file = File(filePath);
11
+    await file.writeAsBytes(data);
12
+    return file;
13
+  }
14
+}

+ 64 - 8
pubspec.lock

@@ -33,6 +33,14 @@ packages:
33 33
       url: "https://pub.flutter-io.cn"
34 34
     source: hosted
35 35
     version: "4.0.3"
36
+  archive:
37
+    dependency: transitive
38
+    description:
39
+      name: archive
40
+      sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
41
+      url: "https://pub.flutter-io.cn"
42
+    source: hosted
43
+    version: "3.4.10"
36 44
   args:
37 45
     dependency: transitive
38 46
     description:
@@ -403,6 +411,14 @@ packages:
403 411
       url: "https://pub.flutter-io.cn"
404 412
     source: hosted
405 413
     version: "4.9.6+1"
414
+  flutter_svg:
415
+    dependency: transitive
416
+    description:
417
+      name: flutter_svg
418
+      sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
419
+      url: "https://pub.flutter-io.cn"
420
+    source: hosted
421
+    version: "2.0.10+1"
406 422
   flutter_test:
407 423
     dependency: "direct dev"
408 424
     description: flutter
@@ -445,14 +461,6 @@ packages:
445 461
       url: "https://pub.flutter-io.cn"
446 462
     source: hosted
447 463
     version: "2.3.1"
448
-  hand_signature:
449
-    dependency: "direct main"
450
-    description:
451
-      name: hand_signature
452
-      sha256: a76d56edd2b31bffb3998937be1c6b1eba98696f6e8d817381260c41da415ede
453
-      url: "https://pub.flutter-io.cn"
454
-    source: hosted
455
-    version: "3.0.2"
456 464
   http:
457 465
     dependency: transitive
458 466
     description:
@@ -477,6 +485,14 @@ packages:
477 485
       url: "https://pub.flutter-io.cn"
478 486
     source: hosted
479 487
     version: "4.0.2"
488
+  image:
489
+    dependency: transitive
490
+    description:
491
+      name: image
492
+      sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
493
+      url: "https://pub.flutter-io.cn"
494
+    source: hosted
495
+    version: "4.1.7"
480 496
   install_plugin:
481 497
     dependency: "direct main"
482 498
     description:
@@ -789,6 +805,14 @@ packages:
789 805
       url: "https://pub.flutter-io.cn"
790 806
     source: hosted
791 807
     version: "2.1.8"
808
+  pointycastle:
809
+    dependency: transitive
810
+    description:
811
+      name: pointycastle
812
+      sha256: "79fbafed02cfdbe85ef3fd06c7f4bc2cbcba0177e61b765264853d4253b21744"
813
+      url: "https://pub.flutter-io.cn"
814
+    source: hosted
815
+    version: "3.9.0"
792 816
   pool:
793 817
     dependency: transitive
794 818
     description:
@@ -917,6 +941,14 @@ packages:
917 941
       url: "https://pub.flutter-io.cn"
918 942
     source: hosted
919 943
     version: "1.0.4"
944
+  signature:
945
+    dependency: "direct main"
946
+    description:
947
+      name: signature
948
+      sha256: d072a766e9a40496296b7593c0d9dee2abded0bdcfb306d9962d0320a9763395
949
+      url: "https://pub.flutter-io.cn"
950
+    source: hosted
951
+    version: "5.4.1"
920 952
   sky_engine:
921 953
     dependency: transitive
922 954
     description: flutter
@@ -1074,6 +1106,30 @@ packages:
1074 1106
       url: "https://pub.flutter-io.cn"
1075 1107
     source: hosted
1076 1108
     version: "4.4.0"
1109
+  vector_graphics:
1110
+    dependency: transitive
1111
+    description:
1112
+      name: vector_graphics
1113
+      sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
1114
+      url: "https://pub.flutter-io.cn"
1115
+    source: hosted
1116
+    version: "1.1.11+1"
1117
+  vector_graphics_codec:
1118
+    dependency: transitive
1119
+    description:
1120
+      name: vector_graphics_codec
1121
+      sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
1122
+      url: "https://pub.flutter-io.cn"
1123
+    source: hosted
1124
+    version: "1.1.11+1"
1125
+  vector_graphics_compiler:
1126
+    dependency: transitive
1127
+    description:
1128
+      name: vector_graphics_compiler
1129
+      sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
1130
+      url: "https://pub.flutter-io.cn"
1131
+    source: hosted
1132
+    version: "1.1.11+1"
1077 1133
   vector_math:
1078 1134
     dependency: transitive
1079 1135
     description:

+ 1 - 1
pubspec.yaml

@@ -84,7 +84,7 @@ dependencies:
84 84
 #  mobile_scanner: ^5.0.0-beta.3
85 85
   qr_code_scanner: ^1.0.1
86 86
   # 手写签名
87
-  hand_signature: ^3.0.2
87
+  signature: ^5.4.1
88 88
 
89 89
 dev_dependencies:
90 90
   flutter_test: