upload_page.dart 10.0 KB

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