upload_page.dart 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. import 'dart:io';
  2. import 'package:auto_route/annotations.dart';
  3. import 'package:dio/dio.dart';
  4. import 'package:eitc_erm_dental_flutter/entity/clinic_info.dart';
  5. import 'package:eitc_erm_dental_flutter/funcs.dart';
  6. import 'package:eitc_erm_dental_flutter/global.dart';
  7. import 'package:eitc_erm_dental_flutter/http/gzkj_response.dart';
  8. import 'package:eitc_erm_dental_flutter/http/http.dart';
  9. import 'package:flutter/material.dart';
  10. import 'package:flutter_screenutil/flutter_screenutil.dart';
  11. import 'package:percent_indicator/percent_indicator.dart' as pi;
  12. import '../../entity/history_item_info.dart';
  13. ///上传页面
  14. @RoutePage(name: "uploadRoute")
  15. class UploadPage extends StatefulWidget {
  16. //医院信息
  17. final ClinicInfo clinicInfo;
  18. ///要上传的历史记录列表
  19. final List<HistoryItemInfo> uploadList;
  20. ///是否是从查看页面上传
  21. final bool isFromView;
  22. const UploadPage(
  23. {super.key,
  24. required this.uploadList,
  25. required this.clinicInfo,
  26. required this.isFromView});
  27. @override
  28. State<UploadPage> createState() => _UploadPageState();
  29. }
  30. class _UploadPageState extends State<UploadPage> {
  31. ///是否正在上传
  32. bool _isUploading = false;
  33. ///是否显示停止上传dialog
  34. bool _isStopDialogShow = false;
  35. ///上传进度
  36. double _uploadProgress = 0.0;
  37. CancelToken? cancelToken;
  38. ///上传索引
  39. int _uploadingIndex = 0;
  40. //上传数量
  41. int _uploadCount = 0;
  42. @override
  43. void initState() {
  44. super.initState();
  45. screenDisableRotate();
  46. WidgetsBinding.instance.addPostFrameCallback((_) => _startUpload());
  47. }
  48. @override
  49. void dispose() {
  50. super.dispose();
  51. cancelToken?.cancel();
  52. cancelToken = null;
  53. //如果是从查看页面上传的,就需要恢复屏幕旋转
  54. if (widget.isFromView) {
  55. screenEnableRotate();
  56. }
  57. }
  58. @override
  59. Widget build(BuildContext context) {
  60. return PopScope(
  61. canPop: !_isUploading,
  62. onPopInvokedWithResult: _onPop,
  63. child: Scaffold(
  64. resizeToAvoidBottomInset: false,
  65. appBar: _getAppBar(),
  66. body: SafeArea(
  67. child: Padding(
  68. padding: EdgeInsets.only(top: 52.h),
  69. child: _isUploading
  70. ? _UploadingView(
  71. progress: _uploadProgress,
  72. fileIndex: _uploadingIndex,
  73. fileCount: _uploadCount,
  74. )
  75. : SizedBox(),
  76. )),
  77. ));
  78. }
  79. ///获取appbar
  80. AppBar _getAppBar() {
  81. return AppBar(
  82. title: Text(getS().upload),
  83. );
  84. }
  85. ///返回控制
  86. void _onPop(bool didPop, dynamic result) async {
  87. if (didPop) {
  88. return;
  89. }
  90. _showStopDialog();
  91. }
  92. ///显示停止弹窗
  93. void _showStopDialog() async {
  94. _isStopDialogShow = true;
  95. bool? bo = await showDialog<bool>(
  96. context: context,
  97. barrierDismissible: false,
  98. builder: (ctx) => AlertDialog(
  99. title: Text(getS().hint),
  100. content: Text(getS().stopUploadAlert),
  101. actions: [
  102. TextButton(
  103. onPressed: () => Navigator.pop(ctx, false),
  104. child: Text(getS().cancel)),
  105. TextButton(
  106. onPressed: () => Navigator.pop(ctx, true),
  107. child: Text(getS().confirm)),
  108. ],
  109. ));
  110. _isStopDialogShow = false;
  111. if (bo == null || !bo) {
  112. return;
  113. }
  114. _stopUpload();
  115. }
  116. ///关闭退出弹窗
  117. void _dismissStopDialog() {
  118. if (!_isStopDialogShow) {
  119. return;
  120. }
  121. Navigator.pop(context, false);
  122. }
  123. ///开始上传
  124. void _startUpload() {
  125. _doUpload();
  126. }
  127. ///停止上传
  128. void _stopUpload() {
  129. _dismissStopDialog();
  130. setState(() {
  131. _isUploading = false;
  132. _uploadProgress = 0.0;
  133. cancelToken?.cancel();
  134. cancelToken = null;
  135. });
  136. }
  137. ///执行上传
  138. void _doUpload() async {
  139. List<HistoryItemInfo> toUploadList = [];
  140. Stream<HistoryItemInfo> stream = Stream.fromIterable(widget.uploadList);
  141. await for (HistoryItemInfo info in stream) {
  142. //检查文件是否存在
  143. if (await File(info.path).exists()) {
  144. toUploadList.add(info);
  145. }
  146. }
  147. //没有可上传的文件
  148. if (toUploadList.isEmpty) {
  149. _showNothingToUpload();
  150. return;
  151. }
  152. _uploadCount = toUploadList.length;
  153. logd("上传数量=$_uploadCount");
  154. stream = Stream.fromIterable(toUploadList);
  155. //刷新页面
  156. setState(() {
  157. _isUploading = true;
  158. });
  159. await for (HistoryItemInfo info in stream) {
  160. File file = File(info.path);
  161. String suffix = file.path.substring(file.path.lastIndexOf("."));
  162. String fileName =
  163. "HS${info.prefixInfo?.idCard ?? ""}_${info.time}$suffix";
  164. MultipartFile part =
  165. await MultipartFile.fromFile(file.path, filename: fileName);
  166. cancelToken = CancelToken();
  167. String deviceModel =
  168. info.prefixInfo?.wifi.replaceAll(deviceWifiPrefix, "") ?? "";
  169. Map<String, dynamic> map = {
  170. "deviceID": deviceModel,
  171. "applicationKey": uploadApplicationKey,
  172. "idCardNo": info.prefixInfo?.idCard ?? "",
  173. "fileType": uploadFileType,
  174. "fileMedium": suffix,
  175. "fileName": fileName,
  176. "chunkData": part,
  177. };
  178. FormData formData = FormData.fromMap(map);
  179. logd("上传数据=$map");
  180. try {
  181. GzkjResponse<dynamic> response = await Http.instance.uploadGzkj(
  182. widget.clinicInfo.clinicApiUrl ?? "",
  183. data: formData,
  184. cancelToken: cancelToken,
  185. onSendProgress: _onUploadProgress,
  186. fromJsonT: (_) => 0);
  187. if (!response.isSuccess) {
  188. _showUploadExceptionDialog(
  189. response.message ?? getS().uploadFailedPleaseRetry);
  190. logd("上传失败,msg=${response.message}");
  191. break;
  192. } else {
  193. //都上传完了
  194. if (_uploadingIndex >= _uploadCount - 1) {
  195. _showUploadSuccessDialog();
  196. break;
  197. } else {
  198. //刷新界面
  199. setState(() {
  200. _uploadProgress = 0.0;
  201. _uploadingIndex++;
  202. logd("上传下一个,index=$_uploadingIndex");
  203. });
  204. }
  205. }
  206. } catch (e) {
  207. loge("上传失败", error: e);
  208. _showUploadExceptionDialog(getS().uploadFailedPleaseRetry);
  209. break;
  210. }
  211. }
  212. }
  213. ///显示上传成功弹窗
  214. void _showUploadSuccessDialog() async {
  215. _dismissStopDialog();
  216. await showDialog(
  217. context: context,
  218. builder: (ctx) => AlertDialog(
  219. title: Text(getS().hint),
  220. content: Text(getS().uploadSuccess),
  221. actions: [
  222. TextButton(
  223. onPressed: () => Navigator.pop(ctx),
  224. child: Text(getS().confirm))
  225. ],
  226. ));
  227. if (mounted) {
  228. Navigator.pop(context);
  229. }
  230. }
  231. ///显示上传错误弹窗
  232. void _showUploadExceptionDialog(String str) async {
  233. if (!_isUploading) {
  234. return;
  235. }
  236. _dismissStopDialog();
  237. if (context.mounted) {
  238. await showDialog(
  239. context: context,
  240. builder: (ctx) {
  241. return AlertDialog(
  242. title: Text(getS().uploadFailed),
  243. content: Text(str),
  244. actions: [
  245. TextButton(
  246. onPressed: () => Navigator.pop(ctx),
  247. child: Text(getS().confirm))
  248. ],
  249. );
  250. });
  251. if (mounted) {
  252. Navigator.pop(context);
  253. }
  254. }
  255. }
  256. ///显示没有可上传的文件的提示框
  257. void _showNothingToUpload() async {
  258. _isUploading = false;
  259. if (context.mounted) {
  260. await showDialog(
  261. context: context,
  262. builder: (ctx) {
  263. return AlertDialog(
  264. title: Text(getS().hint),
  265. content: Text(getS().noFileCanUpload),
  266. actions: [
  267. TextButton(
  268. onPressed: () => Navigator.pop(ctx),
  269. child: Text(getS().close))
  270. ],
  271. );
  272. });
  273. if (mounted) {
  274. Navigator.pop(context);
  275. }
  276. }
  277. }
  278. //假的接口错误
  279. void _fakeInterfaceError() async {
  280. await Future.delayed(const Duration(seconds: 2));
  281. _showUploadExceptionDialog(getS().uploadFailedByInterfaceError);
  282. }
  283. ///上传进度
  284. void _onUploadProgress(int count, int total) {
  285. double progress = count.toDouble() / total.toDouble();
  286. /*logd(
  287. "上传进度,count=$count,total=$total,progress=$progress,uploadingIndex=$_uploadingIndex,uploadCount=$_uploadCount");*/
  288. setState(() {
  289. _uploadProgress = progress;
  290. });
  291. }
  292. }
  293. ///上传中视图
  294. class _UploadingView extends StatelessWidget {
  295. ///进度,范围0到1
  296. final double progress;
  297. final int fileIndex;
  298. final int fileCount;
  299. const _UploadingView(
  300. {required this.progress,
  301. required this.fileIndex,
  302. required this.fileCount});
  303. @override
  304. Widget build(BuildContext context) {
  305. return Padding(
  306. padding: EdgeInsets.symmetric(horizontal: 30.w),
  307. child: Column(
  308. mainAxisSize: MainAxisSize.min,
  309. children: [
  310. Text(
  311. getS().uploadingHint,
  312. style: Theme.of(context).textTheme.titleMedium,
  313. ),
  314. SizedBox(
  315. height: 40.h,
  316. ),
  317. Column(
  318. crossAxisAlignment: CrossAxisAlignment.end,
  319. children: [
  320. pi.LinearPercentIndicator(
  321. percent: progress,
  322. lineHeight: 15.h,
  323. progressColor: Theme.of(context).colorScheme.primary,
  324. barRadius: Radius.circular(15.h),
  325. padding: EdgeInsets.zero,
  326. center: Text(
  327. "${(progress * 100.0).round()}%",
  328. style: TextStyle(fontSize: 12.sp, color: Colors.white),
  329. )),
  330. SizedBox(
  331. height: 5.h,
  332. ),
  333. Text("${fileIndex + 1}/$fileCount")
  334. ],
  335. ),
  336. ],
  337. ),
  338. );
  339. }
  340. }