。。。。
							parent
							
								
									6ca5a27418
								
							
						
					
					
						commit
						76d0fdbd16
					
				|  | @ -352,6 +352,13 @@ setState(() { | |||
|   } | ||||
| 
 | ||||
|   Widget _mainWidget() { | ||||
|     bool isShowCheck = false; | ||||
|     if (FormUtils.hasValue(inspectedForm, 'hiddenList')) { | ||||
|       List list = inspectedForm['hiddenList']; | ||||
|       if (list.isEmpty) { | ||||
|         isShowCheck = true; | ||||
|       } | ||||
|     } | ||||
|     return ListView( | ||||
|       children: [ | ||||
|         Column( | ||||
|  | @ -380,8 +387,9 @@ setState(() { | |||
|                                   : false, | ||||
|                           yesLabel: '同意', | ||||
|                           noLabel: '申辩', | ||||
|                           isEdit: true, | ||||
|                           isEdit: isShowCheck ? true : false, | ||||
|                           isRequired: false, | ||||
|                           text: inspectedForm['INSPECTION_STATUS'] == '3' ? '同意' : '申辩', | ||||
|                           horizontalPadding: 3, | ||||
|                           verticalPadding: 0, | ||||
|                           onChanged: (val) { | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ class _StudyClassListPageState extends State<StudyClassListPage> { | |||
|   late List<dynamic> _list = []; | ||||
|   late String _classId = ''; | ||||
|   late String _post_id = ''; | ||||
|   late Map _classInfo = {}; | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|  | @ -32,6 +33,7 @@ class _StudyClassListPageState extends State<StudyClassListPage> { | |||
|       final result = await ApiService.getClassList(_classId, _post_id); | ||||
|       if (result['result'] == 'success') { | ||||
|         final List<dynamic> newList = result['varList'] ?? []; | ||||
|         _classInfo = result['classInfo'] ?? {}; | ||||
|         setState(() { | ||||
|           _list.addAll(newList); | ||||
|         }); | ||||
|  | @ -46,7 +48,7 @@ class _StudyClassListPageState extends State<StudyClassListPage> { | |||
|     final result = await ApiService.getVideoPermissions(); | ||||
|     if (result['result'] == 'success') { | ||||
|       SessionService.instance.setStudyToken(result['token'] ?? ''); | ||||
|       pushPage(StudyDetailPage(item, widget.studyData['STUDENT_ID'],widget.studyData), context); | ||||
|       pushPage(StudyDetailPage(_classInfo, item, widget.studyData['STUDENT_ID'],widget.studyData), context); | ||||
|     } | ||||
|   } | ||||
|   @override | ||||
|  |  | |||
|  | @ -24,10 +24,17 @@ enum TakeExamType { video_study, strengththen, list } | |||
| 
 | ||||
| class StudyDetailPage extends StatefulWidget { | ||||
|   final Map studyDetailDetail; | ||||
|   final Map classInfo; | ||||
|   final String studentId; | ||||
|   final Map studyData; | ||||
| 
 | ||||
|   const StudyDetailPage(this.studyDetailDetail, this.studentId, this.studyData,{super.key}); | ||||
|   const StudyDetailPage( | ||||
|     this.classInfo, | ||||
|     this.studyDetailDetail, | ||||
|     this.studentId, | ||||
|     this.studyData, { | ||||
|     super.key, | ||||
|   }); | ||||
| 
 | ||||
|   @override | ||||
|   State<StudyDetailPage> createState() => _StudyDetailPageState(); | ||||
|  | @ -54,6 +61,7 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|   int _currentNodeIndex = 0; | ||||
|   bool _hasNodes = false; | ||||
|   Duration _lastReported = Duration.zero; | ||||
|   bool _isFace = false; | ||||
| 
 | ||||
|   // 上报控制相关字段 | ||||
|   bool _endReported = false; // 确保最终结束上报只执行一次 | ||||
|  | @ -69,6 +77,7 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|     WakelockPlus.enable(); | ||||
|     _classId = widget.studyDetailDetail['CLASS_ID'] ?? ''; | ||||
|     _classCurriculumId = widget.studyDetailDetail['CLASSCURRICULUM_ID'] ?? ''; | ||||
|     _isFace = widget.classInfo['ISFACE'] == '1' ? true : false; | ||||
|     _tabController = TabController(length: 2, vsync: this); | ||||
|     WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|       _showFaceIntro(); | ||||
|  | @ -99,16 +108,19 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|   } | ||||
| 
 | ||||
|   Future<void> _showFaceIntro() async { | ||||
|     final ok = await CustomAlertDialog.showConfirm( | ||||
|       context, | ||||
|       title: '温馨提示', | ||||
|       content: '重要提醒:尊敬的用户,根据规定我们会在您学习过程中多次进行人脸识别认证,为了保护您的隐私请您在摄像设备视野内确保衣冠整齐。', | ||||
|       cancelText: '取消', | ||||
|       confirmText: '同意并继续', | ||||
|     ); | ||||
|     if (!ok) { | ||||
|       Navigator.of(context).pop(); | ||||
|       return; | ||||
|     if (_isFace) { | ||||
|       final ok = await CustomAlertDialog.showConfirm( | ||||
|         context, | ||||
|         title: '温馨提示', | ||||
|         content: | ||||
|             '重要提醒:尊敬的用户,根据规定我们会在您学习过程中多次进行人脸识别认证,为了保护您的隐私请您在摄像设备视野内确保衣冠整齐。', | ||||
|         cancelText: '取消', | ||||
|         confirmText: '同意并继续', | ||||
|       ); | ||||
|       if (!ok) { | ||||
|         Navigator.of(context).pop(); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -179,8 +191,6 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|     return '$s%'; | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   Future<void> _onVideoTap( | ||||
|     Map<String, dynamic> data, | ||||
|     bool hasNodes, | ||||
|  | @ -191,9 +201,15 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|       debugPrint('_onVideoTap ignored because a video is loading'); | ||||
|       return; | ||||
|     } | ||||
|     // 后端清除人脸计时 | ||||
|     await ApiService.fnClearUserFaceTime(); | ||||
|     _faceTimer?.cancel(); | ||||
|     if (_isFace) { | ||||
|       try { | ||||
|         await ApiService.fnClearUserFaceTime(); | ||||
|       } catch (e, st) { | ||||
|         debugPrint('fnClearUserFaceTime error: $e\n$st'); | ||||
|       } | ||||
|       _faceTimer?.cancel(); | ||||
|       _faceTimer = null; | ||||
|     } | ||||
| 
 | ||||
|     // 如果当前有视频播放且有未上报的进度,先做一次快照上报(避免丢失) | ||||
|     if (_currentVideoData != null && _lastReported > Duration.zero) { | ||||
|  | @ -252,7 +268,6 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|               seconds: int.parse('${data['VIDEOTIME'] ?? '0'}'), | ||||
|             ); | ||||
|           } | ||||
| 
 | ||||
|         } else { | ||||
|           ToastUtil.showNormal(context, '课件文件资源已失效,请联系管理员'); | ||||
|         } | ||||
|  | @ -267,59 +282,69 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|         // 视频 | ||||
|         await _getVideoPlayInfo(data['VIDEOCOURSEWARE_ID']); | ||||
|         LoadingDialogHelper.hide(); | ||||
|         _startFaceTimer(); | ||||
|         if (_isFace) { | ||||
|           _startFaceTimer(); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   Future<void> _navigateFaceIfNeeded(FutureOr<void> Function() onPass) async { | ||||
|     if (_info?['ISFACE'] == '1') { | ||||
|       LoadingDialogHelper.show(); | ||||
|       final resData = await ApiService.fnGetUserFace(); | ||||
|       LoadingDialogHelper.hide(); | ||||
|       final pd_data = resData['pd']; | ||||
|       if (FormUtils.hasValue(pd_data, 'USERAVATARURL')) { | ||||
|         // 先退出可能的全屏并锁定为竖屏,避免横屏导致人脸页面布局错乱 | ||||
|     if (!_isFace) { | ||||
|       await onPass(); | ||||
|       return; | ||||
|     } | ||||
|     LoadingDialogHelper.show(); | ||||
|     final resData = await ApiService.fnGetUserFace(); | ||||
|     LoadingDialogHelper.hide(); | ||||
|     final pd_data = resData['pd']; | ||||
|     if (FormUtils.hasValue(pd_data, 'USERAVATARURL')) { | ||||
|       // 先退出可能的全屏并锁定为竖屏,避免横屏导致人脸页面布局错乱 | ||||
|       await _exitTopRouteAndWait(); | ||||
|       await _lockPortrait(); | ||||
|       final passed = await pushPage<bool>( | ||||
|         FaceRecognitionPage( | ||||
|           studentId: widget.studentId, | ||||
|           VIDEOCOURSEWARE_ID: "", | ||||
|           CURRICULUM_ID: "", | ||||
|           CHAPTER_ID: "", | ||||
|           CLASS_ID: "", | ||||
|           mode: FaceMode.auto, | ||||
|         ), | ||||
|         context, | ||||
|       ); | ||||
|       await _restoreDefaultOrientations(); | ||||
| 
 | ||||
|       if (passed == true) { | ||||
|         LoadingDialogHelper.show(); | ||||
|         await ApiService.fnSetUserFaceTime(_faceTime); | ||||
|         await onPass(); | ||||
|       } else { | ||||
|         ToastUtil.showError(context, '人脸验证未通过,无法继续'); | ||||
|       } | ||||
|     } else { | ||||
|       final ok = await CustomAlertDialog.showConfirm( | ||||
|         context, | ||||
|         title: '温馨提示', | ||||
|         content: '您当前还未进行人脸认证,请先进行认证', | ||||
|         cancelText: '取消', | ||||
|       ); | ||||
|       if (ok) { | ||||
|         await _exitTopRouteAndWait(); | ||||
|         await _lockPortrait(); | ||||
|         final passed = await pushPage<bool>( | ||||
|           FaceRecognitionPage(studentId: widget.studentId, | ||||
|               VIDEOCOURSEWARE_ID:"",CURRICULUM_ID:"", | ||||
|               CHAPTER_ID: "",CLASS_ID: "", | ||||
|               mode: FaceMode.auto), | ||||
|         await pushPage( | ||||
|           const FaceRecognitionPage( | ||||
|             studentId: '', | ||||
|             VIDEOCOURSEWARE_ID: '', | ||||
|             CURRICULUM_ID: '', | ||||
|             CHAPTER_ID: '', | ||||
|             CLASS_ID: '', | ||||
|             mode: FaceMode.manual, | ||||
|           ), | ||||
|           context, | ||||
|         ); | ||||
|         await _restoreDefaultOrientations(); | ||||
| 
 | ||||
|         if (passed == true) { | ||||
|           LoadingDialogHelper.show(); | ||||
|           await ApiService.fnSetUserFaceTime(_faceTime); | ||||
|           await onPass(); | ||||
|         } else { | ||||
|           ToastUtil.showError(context, '人脸验证未通过,无法继续'); | ||||
|         } | ||||
|       } else { | ||||
|         final ok = await CustomAlertDialog.showConfirm( | ||||
|           context, | ||||
|           title: '温馨提示', | ||||
|           content: '您当前还未进行人脸认证,请先进行认证', | ||||
|           cancelText: '取消', | ||||
|         ); | ||||
|         if (ok) { | ||||
|           await _exitTopRouteAndWait(); | ||||
|           await _lockPortrait(); | ||||
|           await pushPage( | ||||
|             const FaceRecognitionPage(studentId: '', | ||||
|                 VIDEOCOURSEWARE_ID: '',CURRICULUM_ID: '', | ||||
|                 CHAPTER_ID: '',CLASS_ID: '', | ||||
|                 mode: FaceMode.manual), | ||||
|             context, | ||||
|           ); | ||||
|           await _restoreDefaultOrientations(); | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       await onPass(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | @ -471,8 +496,16 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|               _exitTopRouteIfPresent(); | ||||
|             }); | ||||
| 
 | ||||
|         ApiService.fnClearUserFaceTime(); | ||||
|         _faceTimer?.cancel(); | ||||
|         // 仅当开启人脸功能时,清除人脸计时并取消定时器 | ||||
|         if (_isFace) { | ||||
|           try { | ||||
|             ApiService.fnClearUserFaceTime(); | ||||
|           } catch (e, st) { | ||||
|             debugPrint('fnClearUserFaceTime error on end: $e\n$st'); | ||||
|           } | ||||
|           _faceTimer?.cancel(); | ||||
|           _faceTimer = null; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | @ -484,7 +517,8 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|   }) async { | ||||
|     // snapshot 必须包含 VIDEOCOURSEWARE_ID | ||||
|     if (snapshot['VIDEOCOURSEWARE_ID'] == null || | ||||
|         snapshot['VIDEOCOURSEWARE_ID'] == '') return; | ||||
|         snapshot['VIDEOCOURSEWARE_ID'] == '') | ||||
|       return; | ||||
| 
 | ||||
|     // 如果已经上报结束并且当前不是结束上报,则跳过非结束上报(原逻辑) | ||||
|     if (_endReported && !end) return; | ||||
|  | @ -504,7 +538,9 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
| 
 | ||||
|     // 如果这是“非视频课件”(通过 snapshot['IS_VIDEO']==1 标记), | ||||
|     // 则按照“学完即 100%”的规则单次上报并把 UI 设置为 100%。 | ||||
|     final isNonVideo = (snapshot['IS_VIDEO'] != null && snapshot['IS_VIDEO'].toString() == '1'); | ||||
|     final isNonVideo = | ||||
|         (snapshot['IS_VIDEO'] != null && | ||||
|             snapshot['IS_VIDEO'].toString() == '1'); | ||||
| 
 | ||||
|     const int maxRetries = 2; | ||||
|     int attempt = 0; | ||||
|  | @ -520,7 +556,8 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|           if (mounted) { | ||||
|             setState(() { | ||||
|               if (snapshot['IS_NODE'] == true) { | ||||
|                 final fi = snapshot['FIRST_INDEX'] as int? ?? _currentFirstIndex; | ||||
|                 final fi = | ||||
|                     snapshot['FIRST_INDEX'] as int? ?? _currentFirstIndex; | ||||
|                 final ni = snapshot['NODE_INDEX'] as int? ?? _currentNodeIndex; | ||||
|                 if (fi >= 0 && fi < _videoList.length) { | ||||
|                   final nodes = _videoList[fi]['nodes'] as List<dynamic>?; | ||||
|  | @ -529,7 +566,8 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|                   } | ||||
|                 } | ||||
|               } else { | ||||
|                 final fi = snapshot['FIRST_INDEX'] as int? ?? _currentFirstIndex; | ||||
|                 final fi = | ||||
|                     snapshot['FIRST_INDEX'] as int? ?? _currentFirstIndex; | ||||
|                 if (fi >= 0 && fi < _videoList.length) { | ||||
|                   _videoList[fi]['percent'] = str; | ||||
|                 } | ||||
|  | @ -558,15 +596,20 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
| 
 | ||||
|         // 以下为视频上报原有逻辑:解析后端返回数据并更新进度 | ||||
|         final resTraw = pd['RESOURCETIME']; | ||||
|         final resT = (resTraw is num) ? resTraw.toDouble() : double.tryParse('$resTraw') ?? seconds.toDouble(); | ||||
|         final resT = | ||||
|             (resTraw is num) | ||||
|                 ? resTraw.toDouble() | ||||
|                 : double.tryParse('$resTraw') ?? seconds.toDouble(); | ||||
| 
 | ||||
|         final videoTimeRaw = snapshot['VIDEOTIME']; | ||||
|         final videoTime = (videoTimeRaw is String) | ||||
|             ? double.tryParse(videoTimeRaw) ?? 1.0 | ||||
|             : (videoTimeRaw is num ? videoTimeRaw.toDouble() : 1.0); | ||||
|         final videoTime = | ||||
|             (videoTimeRaw is String) | ||||
|                 ? double.tryParse(videoTimeRaw) ?? 1.0 | ||||
|                 : (videoTimeRaw is num ? videoTimeRaw.toDouble() : 1.0); | ||||
| 
 | ||||
|         final comp = pd['PLAYCOUNT'] != null && pd['PLAYCOUNT'] > 0; | ||||
|         final pctDouble = comp ? 100.0 : ((resT / (videoTime > 0 ? videoTime : 1.0)) * 100.0); | ||||
|         final pctDouble = | ||||
|             comp ? 100.0 : ((resT / (videoTime > 0 ? videoTime : 1.0)) * 100.0); | ||||
|         // 保留两位小数并去掉不必要末尾 | ||||
|         double pctClamped = pctDouble.clamp(0.00, 100.00); | ||||
|         // 四舍五入到 2 位小数 | ||||
|  | @ -621,83 +664,10 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|     } | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   /// 开始考试 | ||||
|   Future<void> _startExam(Map resData) async { | ||||
|     Map pd = resData['pd'] ?? {}; | ||||
|     Map paper = resData['paper'] ?? {}; | ||||
|     setState(() { | ||||
|       _loading = true; | ||||
|     }); | ||||
|     final arguments = { | ||||
|       'STAGEEXAMPAPERINPUT_ID': paper['STAGEEXAMPAPERINPUT_ID'] ?? '', | ||||
|       'STAGEEXAMPAPER_ID': paper['STAGEEXAMPAPER_ID'] ?? '', | ||||
|       'CLASS_ID': _classId, | ||||
|       'POST_ID': pd['POST_ID'] ?? '', | ||||
|       'STUDENT_ID': widget.studentId, | ||||
|       'NUMBEROFEXAMS': pd['NUMBEROFEXAMS'] ?? '', | ||||
|     }; | ||||
|     print('--_startExam data---$arguments'); | ||||
| 
 | ||||
|     final data = await ApiService.getStartExam(arguments); | ||||
|     setState(() { | ||||
|       _loading = false; | ||||
|     }); | ||||
|     if (data['result'] == 'success') { | ||||
|       pushPage( | ||||
|         TakeExamPage( | ||||
|           examInfo: { | ||||
|             'CLASS_ID': _classId, | ||||
|             'POST_ID': pd['POST_ID'] ?? '', | ||||
|             'STUDENT_ID': widget.studentId, | ||||
|             'STRENGTHEN_PAPER_QUESTION_ID': | ||||
|                 paper['STAGEEXAMPAPERINPUT_ID'] ?? '', | ||||
|             ...data, | ||||
|           }, | ||||
|           examType: TakeExamType.video_study, jumpType: 2, | ||||
|         ), | ||||
|         context, | ||||
|       ); | ||||
|     } else { | ||||
|       ToastUtil.showError(context, '请求错误'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   void _exitTopRouteIfPresent() { | ||||
|     final route = ModalRoute.of(context); | ||||
|     if (route == null) return; | ||||
|     // 当当前路由不是最上层时,说明有别的 route 在上面(通常是全屏播放器) | ||||
|     if (!route.isCurrent) { | ||||
|       // 只尝试 pop 一次(安全) | ||||
|       try { | ||||
|         Navigator.of(context).maybePop(); | ||||
|       } catch (_) { | ||||
|         // 忽略异常 | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// 退出顶部路由并短暂等待(用于退出全屏播放器等) | ||||
|   Future<void> _exitTopRouteAndWait({int waitMs = 300}) async { | ||||
|     _exitTopRouteIfPresent(); | ||||
|     await Future.delayed(Duration(milliseconds: waitMs)); | ||||
|   } | ||||
| 
 | ||||
|   /// 锁定为竖屏 | ||||
|   Future<void> _lockPortrait() async { | ||||
|     await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); | ||||
|   } | ||||
| 
 | ||||
|   /// 恢复允许横竖 | ||||
|   Future<void> _restoreDefaultOrientations() async { | ||||
|     await SystemChrome.setPreferredOrientations([ | ||||
|       DeviceOrientation.portraitUp, | ||||
|       DeviceOrientation.landscapeLeft, | ||||
|       DeviceOrientation.landscapeRight, | ||||
|     ]); | ||||
|   } | ||||
| 
 | ||||
|   void _startFaceTimer() { | ||||
|     // 仅在人脸功能开启时启动计时器 | ||||
|     if (!_isFace) return; | ||||
| 
 | ||||
|     _faceTimer = Timer.periodic(Duration(seconds: _faceTime), (_) async { | ||||
|       final res = await ApiService.fnGetUserFaceTime(); | ||||
|       final isPlaying = _videoController?.value.isPlaying == true; | ||||
|  | @ -714,6 +684,8 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|   } | ||||
| 
 | ||||
|   Future<void> _showFaceAuthOnce() async { | ||||
|     // 仅在人脸功能开启时调用( | ||||
|     if (!_isFace) return; | ||||
|     // 先 cancel 定时器,避免跳转过程中再次触发 | ||||
|     _faceTimer?.cancel(); | ||||
|     _faceTimer = null; | ||||
|  | @ -723,10 +695,14 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|     await _lockPortrait(); | ||||
| 
 | ||||
|     final passed = await pushPage<bool>( | ||||
|       FaceRecognitionPage(studentId: widget.studentId, | ||||
|           VIDEOCOURSEWARE_ID: "",CURRICULUM_ID: "", | ||||
|           CHAPTER_ID: "",CLASS_ID: "", | ||||
|           mode: FaceMode.auto), | ||||
|       FaceRecognitionPage( | ||||
|         studentId: widget.studentId, | ||||
|         VIDEOCOURSEWARE_ID: "", | ||||
|         CURRICULUM_ID: "", | ||||
|         CHAPTER_ID: "", | ||||
|         CLASS_ID: "", | ||||
|         mode: FaceMode.auto, | ||||
|       ), | ||||
|       context, | ||||
|     ); | ||||
| 
 | ||||
|  | @ -768,7 +744,9 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|         debugPrint('Error resuming playback after face auth: $e\n$st'); | ||||
|       } | ||||
|       // 恢复人脸验证计时器 | ||||
|       _startFaceTimer(); | ||||
|       if (_isFace) { | ||||
|         _startFaceTimer(); | ||||
|       } | ||||
|     } else { | ||||
|       ToastUtil.showError(context, '人脸验证未通过,无法继续'); | ||||
|       if (_videoController != null) { | ||||
|  | @ -785,6 +763,82 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// 开始考试 | ||||
|   Future<void> _startExam(Map resData) async { | ||||
|     Map pd = resData['pd'] ?? {}; | ||||
|     Map paper = resData['paper'] ?? {}; | ||||
|     setState(() { | ||||
|       _loading = true; | ||||
|     }); | ||||
|     final arguments = { | ||||
|       'STAGEEXAMPAPERINPUT_ID': paper['STAGEEXAMPAPERINPUT_ID'] ?? '', | ||||
|       'STAGEEXAMPAPER_ID': paper['STAGEEXAMPAPER_ID'] ?? '', | ||||
|       'CLASS_ID': _classId, | ||||
|       'POST_ID': pd['POST_ID'] ?? '', | ||||
|       'STUDENT_ID': widget.studentId, | ||||
|       'NUMBEROFEXAMS': pd['NUMBEROFEXAMS'] ?? '', | ||||
|     }; | ||||
|     print('--_startExam data---$arguments'); | ||||
| 
 | ||||
|     final data = await ApiService.getStartExam(arguments); | ||||
|     setState(() { | ||||
|       _loading = false; | ||||
|     }); | ||||
|     if (data['result'] == 'success') { | ||||
|       pushPage( | ||||
|         TakeExamPage( | ||||
|           examInfo: { | ||||
|             'CLASS_ID': _classId, | ||||
|             'POST_ID': pd['POST_ID'] ?? '', | ||||
|             'STUDENT_ID': widget.studentId, | ||||
|             'STRENGTHEN_PAPER_QUESTION_ID': | ||||
|                 paper['STAGEEXAMPAPERINPUT_ID'] ?? '', | ||||
|             ...data, | ||||
|           }, | ||||
|           examType: TakeExamType.video_study, | ||||
|           jumpType: 3, | ||||
|         ), | ||||
|         context, | ||||
|       ); | ||||
|     } else { | ||||
|       ToastUtil.showError(context, '请求错误'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   void _exitTopRouteIfPresent() { | ||||
|     final route = ModalRoute.of(context); | ||||
|     if (route == null) return; | ||||
|     // 当当前路由不是最上层时,说明有别的 route 在上面(通常是全屏播放器) | ||||
|     if (!route.isCurrent) { | ||||
|       // 只尝试 pop 一次(安全) | ||||
|       try { | ||||
|         Navigator.of(context).maybePop(); | ||||
|       } catch (_) { | ||||
|         // 忽略异常 | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// 退出顶部路由并短暂等待(用于退出全屏播放器等) | ||||
|   Future<void> _exitTopRouteAndWait({int waitMs = 300}) async { | ||||
|     _exitTopRouteIfPresent(); | ||||
|     await Future.delayed(Duration(milliseconds: waitMs)); | ||||
|   } | ||||
| 
 | ||||
|   /// 锁定为竖屏 | ||||
|   Future<void> _lockPortrait() async { | ||||
|     await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); | ||||
|   } | ||||
| 
 | ||||
|   /// 恢复允许横竖 | ||||
|   Future<void> _restoreDefaultOrientations() async { | ||||
|     await SystemChrome.setPreferredOrientations([ | ||||
|       DeviceOrientation.portraitUp, | ||||
|       DeviceOrientation.landscapeLeft, | ||||
|       DeviceOrientation.landscapeRight, | ||||
|     ]); | ||||
|   } | ||||
| 
 | ||||
|   Widget _buildVideoOrCover(double containerW, double containerH) { | ||||
|     final c = _videoController; | ||||
|     if (c != null && c.value.isInitialized) { | ||||
|  | @ -896,9 +950,7 @@ class _StudyDetailPageState extends State<StudyDetailPage> | |||
|                     const SizedBox(width: 5), | ||||
|                     Text( | ||||
|                       (item['NAME'] ?? '').toString(), | ||||
|                       style: const TextStyle( | ||||
|                         fontSize: 15, | ||||
|                       ), | ||||
|                       style: const TextStyle(fontSize: 15), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|  |  | |||
|  | @ -116,9 +116,6 @@ class _TakeExamPageState extends State<TakeExamPage> { | |||
|           content: '您无考试次数!', | ||||
|         ); | ||||
|         if (ok) { | ||||
|           if (widget.jumpType == 2) { | ||||
|             Navigator.pop(context); | ||||
|           } | ||||
|           Navigator.pop(context); | ||||
|         }; | ||||
|       } | ||||
|  | @ -235,10 +232,17 @@ class _TakeExamPageState extends State<TakeExamPage> { | |||
|         content: | ||||
|         passed | ||||
|             ? '您的成绩为 $score 分,恭喜您通过本次考试,请继续保持!' | ||||
|             : '您的成绩为 $score 分,很遗憾您没有通过本次考试,请再接再厉!', | ||||
|             : '您的成绩为 $score 分,很遗憾您没有通过本次考试,请再接再  厉!', | ||||
|         cancelText: '', | ||||
|         confirmText: '确定', | ||||
|       ); | ||||
|       if (widget.jumpType == 2) { | ||||
|         Navigator.pop(context); | ||||
|       } | ||||
|       if (widget.jumpType == 3) { | ||||
|         Navigator.pop(context); | ||||
|         Navigator.pop(context); | ||||
|       } | ||||
|       Navigator.of(context).pop(); | ||||
|     } | ||||
|   } | ||||
|  |  | |||
|  | @ -68,8 +68,8 @@ class MeasuresListWidget extends StatelessWidget { | |||
| 
 | ||||
|                 // 期望的固定列宽(可以根据需要微调) | ||||
|                 double col0Fixed = 40; // 第一列:序号(较窄) | ||||
|                 double col2Fixed = 80; // 第三列:操作(保留较宽以放按钮) | ||||
|                 double col3Fixed = 80; // 第四列:确认人(头像列) | ||||
|                 double col2Fixed = 70; // 第三列:操作(保留较宽以放按钮) | ||||
|                 double col3Fixed = 60; // 第四列:确认人(头像列) | ||||
| 
 | ||||
|                 // 如果不显示第4列(isAllowEdit == true),则第4列宽为0,不计入固定总和 | ||||
|                 final showCol3 = !isAllowEdit; | ||||
|  | @ -112,7 +112,8 @@ class MeasuresListWidget extends StatelessWidget { | |||
|                 } | ||||
| 
 | ||||
|                 return Table( | ||||
|                   defaultVerticalAlignment: TableCellVerticalAlignment.middle, | ||||
|                   // 改为 top 对齐,保证多行内容时单元格从顶部开始布局 | ||||
|                   defaultVerticalAlignment: TableCellVerticalAlignment.top, | ||||
|                   columnWidths: columnWidths, | ||||
|                   border: TableBorder( | ||||
|                     horizontalInside: BorderSide(color: Colors.grey.shade300), | ||||
|  | @ -123,8 +124,8 @@ class MeasuresListWidget extends StatelessWidget { | |||
|                     TableRow( | ||||
|                       decoration: BoxDecoration(color: Colors.grey.shade100), | ||||
|                       children: [ | ||||
|                         const Padding( | ||||
|                           padding: EdgeInsets.all(2), | ||||
|                         Padding( | ||||
|                           padding: const EdgeInsets.all(6), | ||||
|                           child: Center( | ||||
|                             child: Text( | ||||
|                               '序号', | ||||
|  | @ -132,8 +133,8 @@ class MeasuresListWidget extends StatelessWidget { | |||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                         const Padding( | ||||
|                           padding: EdgeInsets.all(8), | ||||
|                         Padding( | ||||
|                           padding: const EdgeInsets.all(8), | ||||
|                           child: Center( | ||||
|                             child: Text( | ||||
|                               '安全措施', | ||||
|  | @ -142,13 +143,14 @@ class MeasuresListWidget extends StatelessWidget { | |||
|                           ), | ||||
|                         ), | ||||
|                         Padding( | ||||
|                           padding: const EdgeInsets.all(2), | ||||
|                           padding: const EdgeInsets.all(6), | ||||
|                           child: Center( | ||||
|                             child: Text( | ||||
|                               isAllowEdit ? '操作' : '是否\n涉及', | ||||
|                               style: const TextStyle( | ||||
|                                 fontWeight: FontWeight.bold, | ||||
|                               ), | ||||
|                               textAlign: TextAlign.center, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|  | @ -168,41 +170,48 @@ class MeasuresListWidget extends StatelessWidget { | |||
|                     for (var item in measuresList) | ||||
|                       TableRow( | ||||
|                         children: [ | ||||
|                           // 序号 | ||||
|                           Padding( | ||||
|                             padding: const EdgeInsets.all(8), | ||||
|                             padding: const EdgeInsets.all(6), | ||||
|                             child: Center( | ||||
|                               child: Text('${measuresList.indexOf(item) + 1}'), | ||||
|                               child: | ||||
|                               Text('${measuresList.indexOf(item) + 1}'), | ||||
|                             ), | ||||
|                           ), | ||||
| 
 | ||||
|                           // 第二列:措施 + 签名 + 问题答案 | ||||
|                           Padding( | ||||
|                             padding: const EdgeInsets.all(8), | ||||
|                             padding: const EdgeInsets.all(6), | ||||
|                             child: Column( | ||||
|                               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                               children: [ | ||||
|                                 // 主要安全措施 | ||||
|                                 // 主要安全措施(允许多行) | ||||
|                                 Text( | ||||
|                                   item['PROTECTIVE_MEASURES'] as String? ?? '', | ||||
|                                   softWrap: true, | ||||
|                                 ), | ||||
|                                 // 问题1~4 + 答案(可编辑或只读) | ||||
|                                 for (var i = 1; i <= 4; i++) | ||||
|                                   if ((item['QUESTION$i'] as String?) | ||||
|                                           ?.isNotEmpty ?? | ||||
|                                       ?.isNotEmpty ?? | ||||
|                                       false) | ||||
|                                     _buildQnA(item, i), | ||||
|                                 // 操作图片 | ||||
|                                 if (item.containsKey('IMG_PATH') && | ||||
|                                     (item['IMG_PATH'] as String).isNotEmpty && | ||||
|                                     isShowSign) | ||||
|                                   Row( | ||||
|                                     children: [ | ||||
|                                       ..._buildImageRows( | ||||
|                                         context, | ||||
|                                         (item['IMG_PATH'] as String).split(','), | ||||
|                                         '', | ||||
|                                         8, | ||||
|                                       ), | ||||
|                                     ], | ||||
|                                   Padding( | ||||
|                                     padding: const EdgeInsets.only(top: 6), | ||||
|                                     child: Row( | ||||
|                                       children: [ | ||||
|                                         ..._buildImageRows( | ||||
|                                           context, | ||||
|                                           (item['IMG_PATH'] as String).split(','), | ||||
|                                           '', | ||||
|                                           8, | ||||
|                                         ), | ||||
|                                       ], | ||||
|                                     ), | ||||
|                                   ), | ||||
|                               ], | ||||
|                             ), | ||||
|  | @ -210,92 +219,93 @@ class MeasuresListWidget extends StatelessWidget { | |||
| 
 | ||||
|                           // 第三列:状态文字/按钮 + 操作图片 | ||||
|                           Padding( | ||||
|                             padding: const EdgeInsets.all(8), | ||||
|                             padding: const EdgeInsets.all(0), | ||||
|                             child: Column( | ||||
|                               mainAxisAlignment: MainAxisAlignment.center, | ||||
|                               children: [ | ||||
|                                 // 签字或状态 | ||||
|                                 isAllowEdit | ||||
|                                     ? TextButton( | ||||
|                                       onPressed: () { | ||||
|                                         onSign?.call(item); | ||||
|                                       }, | ||||
|                                       child: Text( | ||||
|                                         (item['SIGN_ITEM'] ?? '') | ||||
|                                                 .toString() | ||||
|                                                 .isNotEmpty | ||||
|                                             ? '已签字' | ||||
|                                             : '签字', | ||||
|                                         style: TextStyle( | ||||
|                                           color: | ||||
|                                               (item['SIGN_ITEM'] ?? '') | ||||
|                                                       .toString() | ||||
|                                                       .isNotEmpty | ||||
|                                                   ? Colors.grey.shade600 | ||||
|                                                   : Colors.blue, | ||||
|                                         ), | ||||
|                                       ), | ||||
|                                     ) | ||||
|                                     : Text( | ||||
|                                       (item['STATUS'] as String?) == '-1' | ||||
|                                           ? '不涉及' | ||||
|                                           : '涉及', | ||||
|                                       style: TextStyle( | ||||
|                                         color: | ||||
|                                             (item['STATUS'] as String?) == '-1' | ||||
|                                                 ? Colors.black | ||||
|                                                 : Colors.black, | ||||
|                                       ), | ||||
|                                   onPressed: () { | ||||
|                                     onSign?.call(item); | ||||
|                                   }, | ||||
|                                   child: Text( | ||||
|                                     (item['SIGN_ITEM'] ?? '') | ||||
|                                         .toString() | ||||
|                                         .isNotEmpty | ||||
|                                         ? '已签字' | ||||
|                                         : '签字', | ||||
|                                     style: TextStyle( | ||||
|                                       color: (item['SIGN_ITEM'] ?? '') | ||||
|                                           .toString() | ||||
|                                           .isNotEmpty | ||||
|                                           ? Colors.grey.shade600 | ||||
|                                           : Colors.blue, | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                 ) | ||||
|                                     : Text( | ||||
|                                   (item['STATUS'] as String?) == '-1' | ||||
|                                       ? '不涉及' | ||||
|                                       : '涉及', | ||||
|                                   style: const TextStyle( | ||||
|                                     color: Colors.black, | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ), | ||||
|                           ), | ||||
| 
 | ||||
|                           // 第四列:确认人(只在非编辑模式显示) | ||||
|                           if (!isAllowEdit) | ||||
|                             Column( | ||||
|                               crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                               children: [ | ||||
|                                 if (item.containsKey('SIGN_PATH') && | ||||
|                                     (item['SIGN_PATH'] as String).isNotEmpty) | ||||
|                                   ...((item['SIGN_PATH'] as String) | ||||
|                                       .split(',') | ||||
|                                       .map((s) => s.trim()) | ||||
|                                       .where((s) => s.isNotEmpty)).map((path) { | ||||
|                                     final imageUrl = '$baseImgPath$path'; | ||||
|                                     return Padding( | ||||
|                                       padding: const EdgeInsets.only(top: 5), | ||||
|                                       child: Center( | ||||
|                                         child: GestureDetector( | ||||
|                                           onTap: () { | ||||
|                                             presentOpaque( | ||||
|                                               SingleImageViewer( | ||||
|                                                 imageUrl: imageUrl, | ||||
|                                               ), | ||||
|                                               context, | ||||
|                                             ); | ||||
|                                           }, | ||||
|                                           child: Image.network( | ||||
|                                             imageUrl, | ||||
|                                             width: 40, | ||||
|                                             height: 40, | ||||
|                                             fit: BoxFit.fill, | ||||
|                                             errorBuilder: | ||||
|                                                 (_, __, ___) => Container( | ||||
|                                                   width: 40, | ||||
|                                                   height: 40, | ||||
|                                                   color: Colors.grey.shade200, | ||||
|                                                   child: const Icon( | ||||
|                                                     Icons.broken_image, | ||||
|                                                     size: 18, | ||||
|                                                     color: Colors.grey, | ||||
|                                                   ), | ||||
|                             Padding( | ||||
|                               padding: const EdgeInsets.all(6), | ||||
|                               child: Column( | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                                 children: [ | ||||
|                                   if (item.containsKey('SIGN_PATH') && | ||||
|                                       (item['SIGN_PATH'] as String).isNotEmpty) | ||||
|                                     ...((item['SIGN_PATH'] as String) | ||||
|                                         .split(',') | ||||
|                                         .map((s) => s.trim()) | ||||
|                                         .where((s) => s.isNotEmpty)).map((path) { | ||||
|                                       final imageUrl = '$baseImgPath$path'; | ||||
|                                       return Padding( | ||||
|                                         padding: const EdgeInsets.only(top: 5), | ||||
|                                         child: Center( | ||||
|                                           child: GestureDetector( | ||||
|                                             onTap: () { | ||||
|                                               presentOpaque( | ||||
|                                                 SingleImageViewer( | ||||
|                                                   imageUrl: imageUrl, | ||||
|                                                 ), | ||||
|                                                 context, | ||||
|                                               ); | ||||
|                                             }, | ||||
|                                             child: Image.network( | ||||
|                                               imageUrl, | ||||
|                                               width: 40, | ||||
|                                               height: 40, | ||||
|                                               fit: BoxFit.fill, | ||||
|                                               errorBuilder: | ||||
|                                                   (_, __, ___) => | ||||
|                                                   Container( | ||||
|                                                     width: 40, | ||||
|                                                     height: 40, | ||||
|                                                     color: Colors.grey.shade200, | ||||
|                                                     child: const Icon( | ||||
|                                                       Icons.broken_image, | ||||
|                                                       size: 18, | ||||
|                                                       color: Colors.grey, | ||||
|                                                     ), | ||||
|                                                   ), | ||||
|                                             ), | ||||
|                                           ), | ||||
|                                         ), | ||||
|                                       ), | ||||
|                                     ); | ||||
|                                   }).toList(), | ||||
|                               ], | ||||
|                                       ); | ||||
|                                     }).toList(), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                         ], | ||||
|                       ), | ||||
|  | @ -319,39 +329,46 @@ class MeasuresListWidget extends StatelessWidget { | |||
|       return Padding( | ||||
|         padding: const EdgeInsets.only(top: 8), | ||||
|         child: Row( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, // 顶部对齐,允许左侧文本换行并撑高 | ||||
|           children: [ | ||||
|             Expanded( | ||||
|               flex: 2, | ||||
|               child: Text( | ||||
|                 '$question:', | ||||
|                 style: const TextStyle(fontWeight: FontWeight.w800), | ||||
|               child: Container( | ||||
|                 padding: const EdgeInsets.only(right: 8), // 给文本和输入框一些间距 | ||||
|                 child: Text( | ||||
|                   '$question:', | ||||
|                   softWrap: true, | ||||
|                   maxLines: 2, | ||||
|                   overflow: TextOverflow.ellipsis, // 超出显示省略号(你可以改为 visible 或 null) | ||||
|                   style: const TextStyle(fontWeight: FontWeight.w800), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             Expanded( | ||||
|               flex: 1, | ||||
|               child: TextFormField( | ||||
|                 initialValue: answer, | ||||
|                 textAlign: TextAlign.center, | ||||
|                 // 输入文字居中对齐 | ||||
|                 keyboardType: TextInputType.number, | ||||
|                 inputFormatters: [FilteringTextInputFormatter.digitsOnly], | ||||
|                 decoration: InputDecoration( | ||||
|                   isDense: true, | ||||
|                   contentPadding: const EdgeInsets.symmetric( | ||||
|                     vertical: 8, | ||||
|                     horizontal: 4, | ||||
|                   ), | ||||
|                   // 自定义边框颜色 | ||||
|                   enabledBorder: OutlineInputBorder( | ||||
|                     borderSide: BorderSide(color: Colors.grey.shade300), | ||||
|                   ), | ||||
|                   focusedBorder: OutlineInputBorder( | ||||
|                     borderSide: BorderSide(color: Colors.grey.shade300), | ||||
|               child: SizedBox( | ||||
|                 child: TextFormField( | ||||
|                   initialValue: answer, | ||||
|                   textAlign: TextAlign.center, | ||||
|                   keyboardType: TextInputType.number, | ||||
|                   inputFormatters: [FilteringTextInputFormatter.digitsOnly], | ||||
|                   decoration: InputDecoration( | ||||
|                     isDense: true, | ||||
|                     contentPadding: const EdgeInsets.symmetric( | ||||
|                       vertical: 8, | ||||
|                       horizontal: 4, | ||||
|                     ), | ||||
|                     enabledBorder: OutlineInputBorder( | ||||
|                       borderSide: BorderSide(color: Colors.grey.shade300), | ||||
|                     ), | ||||
|                     focusedBorder: OutlineInputBorder( | ||||
|                       borderSide: BorderSide(color: Colors.grey.shade300), | ||||
|                     ), | ||||
|                   ), | ||||
|                   onChanged: (val) { | ||||
|                     item[key] = val; | ||||
|                   }, | ||||
|                 ), | ||||
|                 onChanged: (val) { | ||||
|                   item[key] = val; | ||||
|                 }, | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|  | @ -359,25 +376,27 @@ class MeasuresListWidget extends StatelessWidget { | |||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     // 只读模式 | ||||
|     // 只读模式:左侧允许换行,右侧显示答案(靠右) | ||||
|     return Padding( | ||||
|       padding: const EdgeInsets.only(top: 8), | ||||
|       child: Column( | ||||
|       padding: const EdgeInsets.only(top: 8, bottom: 6), | ||||
|       child: Row( | ||||
|         children: [ | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 12), | ||||
|             child: Row( | ||||
|               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|               children: [ | ||||
|                 Text( | ||||
|                   '$question:', | ||||
|                   style: const TextStyle(fontWeight: FontWeight.w800), | ||||
|                 ), | ||||
|                 Text(answer.isNotEmpty ? answer : '0'), | ||||
|               ], | ||||
|           Expanded( | ||||
|             flex: 3, | ||||
|             child: Text( | ||||
|               '$question:', | ||||
|               style: const TextStyle(fontWeight: FontWeight.w800), | ||||
|               softWrap: true, | ||||
|             ), | ||||
|           ), | ||||
|           const SizedBox(width: 12), | ||||
|           Expanded( | ||||
|             flex: 1, | ||||
|             child: Text( | ||||
|               answer.isNotEmpty ? answer : '0', | ||||
|               textAlign: TextAlign.right, | ||||
|             ), | ||||
|           ), | ||||
|           const Divider(), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|  | @ -385,11 +404,11 @@ class MeasuresListWidget extends StatelessWidget { | |||
| 
 | ||||
|   /// 构造一组图片 + 可选时间文本行 | ||||
|   List<Widget> _buildImageRows( | ||||
|     BuildContext context, | ||||
|     List<String> paths, | ||||
|     String time, | ||||
|     double right, | ||||
|   ) { | ||||
|       BuildContext context, | ||||
|       List<String> paths, | ||||
|       String time, | ||||
|       double right, | ||||
|       ) { | ||||
|     if (paths.isEmpty) return []; | ||||
| 
 | ||||
|     const int imagesPerRow = 4; | ||||
|  | @ -423,21 +442,19 @@ class MeasuresListWidget extends StatelessWidget { | |||
|                   fit: BoxFit.fill, | ||||
|                   errorBuilder: | ||||
|                       (_, __, ___) => Container( | ||||
|                         width: imageSize, | ||||
|                         height: imageSize, | ||||
|                         color: Colors.grey.shade200, | ||||
|                         child: const Icon( | ||||
|                           Icons.broken_image, | ||||
|                           size: 18, | ||||
|                           color: Colors.grey, | ||||
|                         ), | ||||
|                       ), | ||||
|                     width: imageSize, | ||||
|                     height: imageSize, | ||||
|                     color: Colors.grey.shade200, | ||||
|                     child: const Icon( | ||||
|                       Icons.broken_image, | ||||
|                       size: 18, | ||||
|                       color: Colors.grey, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               if (i != rowPaths.length - 1) SizedBox(width: spacing), | ||||
|             ], | ||||
|             // 如果希望每行右侧也填充占位使宽度平均,可加 Expanded 占位 | ||||
|             // Expanded(child: SizedBox()), | ||||
|           ], | ||||
|         ), | ||||
|       ); | ||||
|  | @ -445,6 +462,7 @@ class MeasuresListWidget extends StatelessWidget { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /// 其他安全防护措施表格组件 | ||||
| class OtherMeasuresWidget extends StatelessWidget { | ||||
|   /// 其他安全防护措施数据列表 | ||||
|  | @ -854,7 +872,7 @@ class SignaturesListWidget extends StatelessWidget { | |||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.all(8), | ||||
|             padding: const EdgeInsets.all(5), | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|  | @ -862,7 +880,7 @@ class SignaturesListWidget extends StatelessWidget { | |||
|                   label, | ||||
|                   style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|                 ), | ||||
|                 const SizedBox(height: 4), | ||||
|                 const SizedBox(height: 2), | ||||
|                 TextField( | ||||
|                   controller: TextEditingController(text: descr), | ||||
|                   maxLines: null, | ||||
|  | @ -873,7 +891,7 @@ class SignaturesListWidget extends StatelessWidget { | |||
|             ), | ||||
|           ), | ||||
|           Padding( | ||||
|             padding: const EdgeInsets.all(8), | ||||
|             padding: const EdgeInsets.all(5), | ||||
|             child: Text('$personDes: $name'), | ||||
|           ), | ||||
|           for (var i = 0; i < signPaths.length; i++) | ||||
|  | @ -897,7 +915,7 @@ class SignaturesListWidget extends StatelessWidget { | |||
|                           (_, __, ___) => const Icon(Icons.broken_image), | ||||
|                     ), | ||||
|                   ), | ||||
|                   const SizedBox(width: 16), | ||||
|                   const SizedBox(width: 10), | ||||
|                   Expanded( | ||||
|                     child: Text(i < signTimes.length ? signTimes[i] : ''), | ||||
|                   ), | ||||
|  |  | |||
|  | @ -167,13 +167,13 @@ class _HotWorkDetailFormWidgetState extends State<HotWorkDetailFormWidget> { | |||
|             hintText: '请选择动火人及证书编号', | ||||
|             onTap: widget.onChooseHotworkUser, | ||||
|           ), | ||||
|           if (FormUtils.hasValue(pd, 'WORK_USER_DEPARTMENT_NAME') && | ||||
|           if (FormUtils.hasValue(pd, 'UNIT_NAME') && | ||||
|               !widget.isEditable) ...[ | ||||
|             const Divider(), | ||||
|             ItemListWidget.singleLineTitleText( | ||||
|               label: '作业单位:', | ||||
|               isEditable: false, | ||||
|               text: pd['WORK_USER_DEPARTMENT_NAME'] ?? '', | ||||
|               text: pd['UNIT_NAME'] ?? '', | ||||
|             ), | ||||
|           ], | ||||
|           if (FormUtils.hasValue(pd, 'CONFIRM_USER_NAME') && | ||||
|  | @ -186,6 +186,14 @@ class _HotWorkDetailFormWidgetState extends State<HotWorkDetailFormWidget> { | |||
|               text: pd['CONFIRM_USER_NAME'] ?? '', | ||||
|             ), | ||||
|           ], | ||||
|           if (FormUtils.hasValue(pd, 'ANALYZE_TIME') && !widget.isEditable) ...[ | ||||
|             const Divider(), | ||||
|             ItemListWidget.OneRowStartButtonTitle( | ||||
|               label: '气体分析信息:', | ||||
|               text: pd['ANALYZE_USER_NAME'] ?? '', | ||||
|               onTap: widget.onAnalyzeTap, | ||||
|             ), | ||||
|           ], | ||||
|           const Divider(), | ||||
|           ItemListWidget.twoRowButtonTitleText( | ||||
|             label: '关联其他特殊作业及安全作业票编号', | ||||
|  | @ -388,14 +396,7 @@ class _HotWorkDetailFormWidgetState extends State<HotWorkDetailFormWidget> { | |||
|             text: pd['VIDEONAME'] ?? '', | ||||
|           ), | ||||
| 
 | ||||
|           if (FormUtils.hasValue(pd, 'ANALYZE_TIME') && !widget.isEditable) ...[ | ||||
|             const Divider(), | ||||
|             ItemListWidget.OneRowStartButtonTitle( | ||||
|               label: '气体分析信息:', | ||||
|               text: pd['ANALYZE_USER_NAME'] ?? '', | ||||
|               onTap: widget.onAnalyzeTap, | ||||
|             ), | ||||
|           ], | ||||
| 
 | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|  |  | |||
|  | @ -1,18 +0,0 @@ | |||
| import 'package:flutter/material.dart'; | ||||
| import 'package:qhd_prevention/pages/my_appbar.dart'; | ||||
| 
 | ||||
| class HotSafeWorkChoosePage extends StatefulWidget { | ||||
|   const HotSafeWorkChoosePage({super.key}); | ||||
| 
 | ||||
|   @override | ||||
|   State<HotSafeWorkChoosePage> createState() => _HotSafeWorkChoosePageState(); | ||||
| } | ||||
| 
 | ||||
| class _HotSafeWorkChoosePageState extends State<HotSafeWorkChoosePage> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       appBar: MyAppbar(title: 'dong'), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | @ -364,35 +364,33 @@ class _HotWorkListPageState extends State<HotWorkListPage> { | |||
|                       overflow: TextOverflow.visible, | ||||
|                     ), | ||||
|                   ), | ||||
|                   const SizedBox(width: 8), | ||||
|                   Expanded( | ||||
|                     child: item['APPROVE_USER_NAME'] != null | ||||
|                         ? Text( | ||||
|                       "审批部门负责人: ${item['APPROVE_USER_NAME'] ?? ''}", | ||||
|                       textAlign: TextAlign.right, | ||||
|                       softWrap: true, | ||||
|                       maxLines: null, | ||||
|                       overflow: TextOverflow.visible, | ||||
|                     ) | ||||
|                         : const SizedBox.shrink(), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
| 
 | ||||
|               Row( | ||||
|                 children: [ | ||||
|                   Expanded( | ||||
|                     child: Text( | ||||
|                       "安全交底人: ${item['CONFESS_USER_NAME'] ?? ''}", | ||||
|                       softWrap: true, | ||||
|                       maxLines: null, | ||||
|                       textAlign: TextAlign.right, | ||||
| 
 | ||||
|                       overflow: TextOverflow.visible, | ||||
|                     ), | ||||
|                   ), | ||||
|                   const SizedBox(width: 8), | ||||
|                 ], | ||||
|               ), | ||||
| 
 | ||||
|               Row( | ||||
|                 children: [ | ||||
|                   Expanded( | ||||
|                     child: Text( | ||||
|                       "接受交底人: ${item['ACCEPT_CONFESS_USER_NAME'] ?? ''}", | ||||
|                       softWrap: true, | ||||
|                       maxLines: null, | ||||
|                       overflow: TextOverflow.visible, | ||||
|                     ), | ||||
| 
 | ||||
|                   ), | ||||
|                   Expanded( | ||||
|                     child: Text( | ||||
|                       "作业负责人: ${item['CONFIRM_USER_NAME'] ?? ''}", | ||||
|                       textAlign: TextAlign.right, | ||||
|                       softWrap: true, | ||||
|                       maxLines: null, | ||||
|  | @ -404,19 +402,10 @@ class _HotWorkListPageState extends State<HotWorkListPage> { | |||
| 
 | ||||
|               Row( | ||||
|                 children: [ | ||||
|                   Expanded( | ||||
|                     child: Text( | ||||
|                       "作业负责人: ${item['CONFIRM_USER_NAME'] ?? ''}", | ||||
|                       softWrap: true, | ||||
|                       maxLines: null, | ||||
|                       overflow: TextOverflow.visible, | ||||
|                     ), | ||||
|                   ), | ||||
|                   const SizedBox(width: 8), | ||||
|                   Expanded( | ||||
|                     child: Text( | ||||
|                       "所在单位负责人: ${item['LEADER_USER_NAME'] ?? ''}", | ||||
|                       textAlign: TextAlign.right, | ||||
|                       textAlign: TextAlign.left, | ||||
|                       softWrap: true, | ||||
|                       maxLines: null, | ||||
|                       overflow: TextOverflow.visible, | ||||
|  |  | |||
|  | @ -65,13 +65,17 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|   late List<dynamic> boardList = [ | ||||
|     {'BOARD_MATERIAL': '', 'BOARD_SPECIFICATION': '', 'BOARD_NO': ''}, | ||||
|   ]; | ||||
| 
 | ||||
|   /// ------------------- 新增 ------------------- | ||||
|   /// 视频监控摄像 | ||||
|   late List<dynamic> videoMonitoringList = []; | ||||
| 
 | ||||
|   /// 承包商列表 | ||||
|   late List<dynamic> unitAllList = []; | ||||
| 
 | ||||
|   /// 作业区域列表 | ||||
|   late List<dynamic> workAreaList = []; | ||||
| 
 | ||||
|   /// -------------------------------------- | ||||
|   late Map<String, dynamic> signs = {}; | ||||
|   late List<Map<String, dynamic>> measuresList = []; | ||||
|  | @ -86,8 +90,6 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|   final TextEditingController _relatedController = TextEditingController(); | ||||
|   final TextEditingController _riskController = TextEditingController(); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   // 存储各单位的人员列表 | ||||
|   final Map<EditUserType, List<Map<String, dynamic>>> _personCache = {}; | ||||
| 
 | ||||
|  | @ -105,12 +107,11 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|       pd['APPLY_USER_ID'] = SessionService.instance.loginUserId; | ||||
|       pd['APPLY_USER_NAME'] = SessionService.instance.username; | ||||
|       pd['IS_CONTRACTOR_WORK'] = '0'; | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     _getVideoList(); | ||||
|     _getUnitListAll(); | ||||
|      | ||||
| 
 | ||||
|     _nameController.addListener(() { | ||||
|       setState(() { | ||||
|         pd['NAME'] = _nameController.text.trim(); | ||||
|  | @ -132,15 +133,16 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|       pd['RISK_IDENTIFICATION'] = _riskController.text.trim(); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /// ---------------------------- 新增 -------------------------------- | ||||
|   /// 视频监控摄像头 | ||||
|   Future<void> _chooseVideoManager() async { | ||||
|     final choice = await BottomPicker.show<String>( | ||||
|       context, | ||||
|       items: | ||||
|       videoMonitoringList | ||||
|           .map((item) => item['VIDEONAME'] as String) | ||||
|           .toList(), | ||||
|           videoMonitoringList | ||||
|               .map((item) => item['VIDEONAME'] as String) | ||||
|               .toList(), | ||||
|       itemBuilder: (item) => Text(item, textAlign: TextAlign.center), | ||||
|       initialIndex: 0, | ||||
|     ); | ||||
|  | @ -149,7 +151,7 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|         pd['VIDEONAME'] = choice; | ||||
| 
 | ||||
|         Map<String, dynamic> result = videoMonitoringList.firstWhere( | ||||
|               (item) => item['VIDEONAME'] == choice, | ||||
|           (item) => item['VIDEONAME'] == choice, | ||||
|           orElse: () => {}, // 避免找不到时报错 | ||||
|         ); | ||||
|         if (FormUtils.hasValue(result, 'VIDEOMANAGER_ID')) { | ||||
|  | @ -159,6 +161,7 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// 选择承包商 | ||||
|   Future<void> _chooseUnitManager() async { | ||||
|     final choice = await BottomPicker.show<String>( | ||||
|  | @ -172,7 +175,7 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|         pd['UNITS_NAME'] = choice; | ||||
| 
 | ||||
|         Map<String, dynamic> result = unitAllList.firstWhere( | ||||
|               (item) => item['UNITS_NAME'] == choice, | ||||
|           (item) => item['UNITS_NAME'] == choice, | ||||
|           orElse: () => {}, // 避免找不到时报错 | ||||
|         ); | ||||
|         if (FormUtils.hasValue(result, 'UNITS_ID')) { | ||||
|  | @ -182,10 +185,9 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|       }); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|    | ||||
| 
 | ||||
|   /// 选择经纬度 | ||||
|   Future<void> _showLocationHandle() async{ | ||||
|   Future<void> _showLocationHandle() async { | ||||
|     if (!FormUtils.hasValue(pd, 'ELECTRONIC_FENCE_AREA_ID')) { | ||||
|       ToastUtil.showNormal(context, '请选择作业区域'); | ||||
|       return; | ||||
|  | @ -194,10 +196,11 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|     setState(() { | ||||
|       pd['LONGITUDE'] = mapData['longitue'] ?? ''; | ||||
|       pd['LATITUDE'] = mapData['latitude'] ?? ''; | ||||
|       pd['LATITUDE_LONGITUDE'] = '${mapData['longitue'] ?? ''},${mapData['latitude'] ?? ''}'; | ||||
|       pd['LATITUDE_LONGITUDE'] = | ||||
|           '${mapData['longitue'] ?? ''},${mapData['latitude'] ?? ''}'; | ||||
|     }); | ||||
| 
 | ||||
|   } | ||||
| 
 | ||||
|   /// 作业区域 | ||||
|   Future<void> _getWorkArea() async { | ||||
|     //FocusHelper.clearFocus(context); | ||||
|  | @ -208,18 +211,19 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|       backgroundColor: Colors.transparent, | ||||
|       builder: | ||||
|           (_) => WorkAreaPicker( | ||||
|         onSelected: (String id, String POSITIONS, String name) { | ||||
|           setState(() { | ||||
|             pd['ELECTRONIC_FENCE_AREA_ID'] = id; | ||||
|             pd['POSITIONS'] = POSITIONS; | ||||
|             pd['PLS_NAME'] = name; | ||||
|           }); | ||||
|         }, | ||||
|       ), | ||||
|             onSelected: (String id, String POSITIONS, String name) { | ||||
|               setState(() { | ||||
|                 pd['ELECTRONIC_FENCE_AREA_ID'] = id; | ||||
|                 pd['POSITIONS'] = POSITIONS; | ||||
|                 pd['PLS_NAME'] = name; | ||||
|               }); | ||||
|             }, | ||||
|           ), | ||||
|     ).then((_) { | ||||
|       //FocusHelper.clearFocus(context); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /// 获取摄像头列表 | ||||
|   Future<void> _getVideoList() async { | ||||
|     final result = await ApiService.getVideomanagerList(); | ||||
|  | @ -227,6 +231,7 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|       videoMonitoringList = result['varList'] ?? []; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   /// 获承包商列表 | ||||
|   Future<void> _getUnitListAll() async { | ||||
|     final result = await ApiService.getUnitListAll(); | ||||
|  | @ -234,7 +239,7 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|       unitAllList = result['varList'] ?? []; | ||||
|     }); | ||||
|   } | ||||
|    | ||||
| 
 | ||||
|   /// ------------------------------------------------------------ | ||||
|   void set_pd_DEPARTMENT_ID(EditUserType type, String id) { | ||||
|     pd['${type.name}_DEPARTMENT_ID'] = id; | ||||
|  | @ -316,7 +321,7 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|           isEditable: isEditable, | ||||
|           isClean: isClean, | ||||
|           onTapClean: () { | ||||
|               ToastUtil.showNormal(context, '已清除'); | ||||
|             ToastUtil.showNormal(context, '已清除'); | ||||
|             setState(() { | ||||
|               set_pd_USER_ID(type, ''); | ||||
|               set_pd_USER_Name(type, ''); | ||||
|  | @ -440,9 +445,23 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|     ]; | ||||
|     final level = pd['WORK_TYPE'] ?? ''; | ||||
|     print('---level-$level'); | ||||
|     int index = 0; | ||||
|     for (Map item in boardList) { | ||||
| 
 | ||||
|       if (!FormUtils.hasValue(item, 'BOARD_MATERIAL')) { | ||||
|         ToastUtil.showNormal(context, '请填写盲板抽堵参数第${index+1}项材质'); | ||||
|         return; | ||||
|       } | ||||
|       if (!FormUtils.hasValue(item, 'BOARD_SPECIFICATION')) { | ||||
|         ToastUtil.showNormal(context, '请填写盲板抽堵参数第${index+1}项规格'); | ||||
|         return; | ||||
|       } | ||||
|       if (!FormUtils.hasValue(item, 'BOARD_NO')) { | ||||
|         ToastUtil.showNormal(context, '请填写盲板抽堵参数第${index+1}项编号'); | ||||
|         return; | ||||
|       } | ||||
|       index += 1; | ||||
|     } | ||||
| 
 | ||||
|     /// 各项负责人校验 | ||||
|     final unitRules = <EditUserType>[ | ||||
|       EditUserType.GUARDIAN, | ||||
|  | @ -478,7 +497,8 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (pd['IS_CONTRACTOR_WORK'] == '1' && !FormUtils.hasValue(pd, 'UNITS_ID')) { | ||||
|       if (pd['IS_CONTRACTOR_WORK'] == '1' && | ||||
|           !FormUtils.hasValue(pd, 'UNITS_ID')) { | ||||
|         ToastUtil.showNormal(context, '请选择承包商'); | ||||
|         return; | ||||
|       } | ||||
|  | @ -510,7 +530,8 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|     pd['ACTION_USER'] = SessionService.instance.username; | ||||
|     pd['APPLY_STATUS'] = status; | ||||
|     pd['boardList'] = jsonEncode(boardList).toString(); | ||||
|     pd['SPECIAL_WORK'] = FormUtils.hasValue(pd, 'SPECIAL_WORK') ? pd['SPECIAL_WORK'] : '无'; | ||||
|     pd['SPECIAL_WORK'] = | ||||
|         FormUtils.hasValue(pd, 'SPECIAL_WORK') ? pd['SPECIAL_WORK'] : '无'; | ||||
|     // 提交参数 | ||||
|     if (msg == 'add') { | ||||
|       pd['STEP_ID'] = status; | ||||
|  | @ -548,8 +569,6 @@ class _BlindboardApplyDetailState extends State<BlindboardApplyDetail> { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   /// 初始化拉取数据 | ||||
|   Future<void> _getData() async { | ||||
|     final data = await ApiService.getHomeworkFindById( | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | |||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||
| # In Windows, build-name is used as the major, minor, and patch parts | ||||
| # of the product and file versions while build-number is used as the build suffix. | ||||
| version: 2.1.2+9 | ||||
| version: 2.1.2+10 | ||||
| 
 | ||||
| environment: | ||||
|   sdk: ^3.7.0 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue