view.dart 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. import 'dart:convert';
  2. import 'dart:io';
  3. import 'package:flutter/foundation.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:get/get.dart';
  6. import 'package:image_picker/image_picker.dart';
  7. import 'package:uuid/uuid.dart';
  8. import 'package:vitalapp/architecture/storage/storage.dart';
  9. import 'package:vitalapp/components/appbar.dart';
  10. import 'package:fis_common/logger/logger.dart';
  11. import 'package:vitalapp/rpc.dart';
  12. import 'dart:ui' as ui;
  13. import 'package:universal_html/html.dart' as html;
  14. import 'controller.dart';
  15. /// 签字板
  16. class SignatureBoardPage extends GetView<SignatureBoardController> {
  17. SignatureBoardPage({super.key});
  18. final canvasController = _SignatureBoardController();
  19. @override
  20. Widget build(BuildContext context) {
  21. final params = Get.parameters;
  22. final String title = params["title"] ?? "设置签名";
  23. return WillPopScope(
  24. onWillPop: () async {
  25. return false;
  26. },
  27. child: Scaffold(
  28. appBar: VAppBar(
  29. context: context,
  30. titleText: title,
  31. ),
  32. body: Stack(
  33. children: [
  34. _SignatureBoard(controller: canvasController),
  35. Positioned(
  36. right: 32,
  37. bottom: 0,
  38. top: 0,
  39. child: _buildFloatActions(context),
  40. ),
  41. ],
  42. ),
  43. ),
  44. );
  45. }
  46. Widget _buildFloatActions(BuildContext context) {
  47. return Column(
  48. mainAxisSize: MainAxisSize.min,
  49. mainAxisAlignment: MainAxisAlignment.center,
  50. crossAxisAlignment: CrossAxisAlignment.center,
  51. children: [
  52. SizedBox(
  53. width: 90,
  54. height: 90,
  55. child: ElevatedButton(
  56. style: ButtonStyle(
  57. foregroundColor: const MaterialStatePropertyAll(Colors.white),
  58. backgroundColor: const MaterialStatePropertyAll(Colors.grey),
  59. shape: MaterialStatePropertyAll(
  60. RoundedRectangleBorder(
  61. borderRadius: BorderRadius.circular(45),
  62. side: BorderSide.none,
  63. ),
  64. ),
  65. ),
  66. onPressed: () {
  67. canvasController.revocation();
  68. },
  69. child: const Text(
  70. "撤销",
  71. style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
  72. ),
  73. ),
  74. ),
  75. const SizedBox(height: 32),
  76. SizedBox(
  77. width: 90,
  78. height: 90,
  79. child: ElevatedButton(
  80. style: ButtonStyle(
  81. foregroundColor: const MaterialStatePropertyAll(Colors.white),
  82. backgroundColor: const MaterialStatePropertyAll(Colors.red),
  83. shape: MaterialStatePropertyAll(
  84. RoundedRectangleBorder(
  85. borderRadius: BorderRadius.circular(45),
  86. side: BorderSide.none,
  87. ),
  88. ),
  89. ),
  90. onPressed: () {
  91. canvasController.clear();
  92. },
  93. child: const Text(
  94. "清除",
  95. style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
  96. ),
  97. ),
  98. ),
  99. const SizedBox(height: 32),
  100. SizedBox(
  101. width: 90,
  102. height: 90,
  103. child: ElevatedButton(
  104. style: ButtonStyle(
  105. foregroundColor: const MaterialStatePropertyAll(Colors.white),
  106. backgroundColor:
  107. MaterialStatePropertyAll(Theme.of(context).primaryColor),
  108. shape: MaterialStatePropertyAll(
  109. RoundedRectangleBorder(
  110. borderRadius: BorderRadius.circular(45),
  111. side: BorderSide.none,
  112. ),
  113. ),
  114. ),
  115. onPressed: () async {
  116. final result = await canvasController.getImageUrl();
  117. Get.back(result: result);
  118. },
  119. child: const Text(
  120. "保存",
  121. style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
  122. ),
  123. ),
  124. ),
  125. ],
  126. );
  127. }
  128. }
  129. class _SignatureBoardController extends ChangeNotifier {
  130. List<Offset?> points = [];
  131. SignaturePainter? painter;
  132. Size? size;
  133. /// 清除
  134. void clear() {
  135. points = [];
  136. notifyListeners();
  137. }
  138. /// 撤销
  139. void revocation() {
  140. int index = points.lastIndexOf(null);
  141. if (index == points.length - 1) {
  142. points = points.take(index).toList();
  143. index = points.lastIndexOf(null);
  144. }
  145. if (index < 0) {
  146. points = [];
  147. } else {
  148. points = points.take(index).toList();
  149. }
  150. notifyListeners();
  151. }
  152. XFile? convertBase64ToXFile(String base64Image) {
  153. try {
  154. final bytes = base64Decode(base64Image);
  155. final tempDir = Directory.systemTemp;
  156. final tempPath = tempDir.path;
  157. final imageId = const Uuid().v4().replaceAll('-', '');
  158. final filePath = '$tempPath/$imageId';
  159. File(filePath).writeAsBytesSync(bytes);
  160. return XFile(filePath);
  161. } catch (e) {
  162. print('Error converting base64 to XFile: $e');
  163. return null;
  164. }
  165. }
  166. html.File? convertBase64ToFile(String base64Image) {
  167. try {
  168. final bytes = base64Decode(base64Image);
  169. const mimeType = 'image/jpeg'; // 替换为您的图片类型
  170. final blob = html.Blob([bytes], mimeType);
  171. final file = html.File([blob], 'image.jpg'); // 替换为您的文件名
  172. return file;
  173. } catch (e) {
  174. print('Error converting base64 to File: $e');
  175. return null;
  176. }
  177. }
  178. Future<String?> getImageUrl() async {
  179. final imageBase64 = await getImageBase64();
  180. String? url;
  181. if (kIsWeb) {
  182. final file = convertBase64ToFile(imageBase64!);
  183. url = await rpc.storage.webUpload(file!);
  184. } else {
  185. final xFile = convertBase64ToXFile(imageBase64!);
  186. url = await rpc.storage.upload(xFile!);
  187. }
  188. return url;
  189. }
  190. /// 获取图片base64字符串
  191. Future<String?> getImageBase64() async {
  192. try {
  193. final recorder = ui.PictureRecorder();
  194. final canvas = Canvas(recorder);
  195. painter!.paint(canvas, size!);
  196. // Paint paint = Paint()
  197. // ..color = Colors.black
  198. // ..strokeCap = StrokeCap.round
  199. // ..strokeWidth = 8.0;
  200. // for (int i = 0; i < points.length - 1; i++) {
  201. // if (points[i] != null && points[i + 1] != null) {
  202. // canvas.drawLine(points[i]!, points[i + 1]!, paint);
  203. // }
  204. // }
  205. // 将绘制的内容转换为图像
  206. final picture = recorder.endRecording();
  207. final image =
  208. await picture.toImage(size!.width.toInt(), size!.height.toInt());
  209. // 将图像保存到文件
  210. final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
  211. if (byteData != null) {
  212. String base64Image = base64Encode(byteData.buffer.asUint8List());
  213. return base64Image;
  214. }
  215. } catch (e) {
  216. logger.e("_SignatureBoardController getImageBase64 error.", e);
  217. }
  218. return null;
  219. }
  220. }
  221. class _SignatureBoard extends StatefulWidget {
  222. final _SignatureBoardController controller;
  223. const _SignatureBoard({required this.controller});
  224. @override
  225. _SignatureBoardState createState() => _SignatureBoardState();
  226. }
  227. class _SignatureBoardState extends State<_SignatureBoard> {
  228. List<Offset?> get _points => widget.controller.points;
  229. @override
  230. void initState() {
  231. widget.controller.addListener(_onUpdate);
  232. super.initState();
  233. }
  234. @override
  235. void dispose() {
  236. widget.controller.removeListener(_onUpdate);
  237. super.dispose();
  238. }
  239. void _onUpdate() {
  240. setState(() {});
  241. }
  242. @override
  243. Widget build(BuildContext context) {
  244. return GestureDetector(
  245. onPanUpdate: (DragUpdateDetails details) {
  246. setState(() {
  247. RenderBox renderBox = context.findRenderObject() as RenderBox;
  248. Offset localPosition =
  249. renderBox.globalToLocal(details.globalPosition);
  250. widget.controller.points = List.from(_points)..add(localPosition);
  251. });
  252. },
  253. onPanEnd: (DragEndDetails details) {
  254. widget.controller.points.add(null);
  255. },
  256. child: LayoutBuilder(builder: (context, c) {
  257. final size = Size(c.maxWidth, c.maxHeight);
  258. final painter = SignaturePainter(points: _points);
  259. widget.controller.size = size;
  260. widget.controller.painter = painter;
  261. return CustomPaint(
  262. painter: painter,
  263. size: size,
  264. );
  265. }),
  266. );
  267. }
  268. }
  269. class SignaturePainter extends CustomPainter {
  270. List<Offset?> points;
  271. SignaturePainter({required this.points});
  272. @override
  273. void paint(Canvas canvas, Size size) {
  274. Paint paint = Paint()
  275. ..color = Colors.black
  276. ..strokeCap = StrokeCap.round
  277. ..strokeWidth = 8.0;
  278. for (int i = 0; i < points.length - 1; i++) {
  279. if (points[i] != null && points[i + 1] != null) {
  280. canvas.drawLine(points[i]!, points[i + 1]!, paint);
  281. }
  282. }
  283. }
  284. @override
  285. bool shouldRepaint(SignaturePainter oldDelegate) => true;
  286. }