From 76d0fdbd16fa7e939792b6ec0846a1bbcec54bd5 Mon Sep 17 00:00:00 2001 From: hs <873121290@qq.com> Date: Wed, 17 Sep 2025 18:05:47 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=82=E3=80=82=E3=80=82=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../safecheck_sign_detail.dart | 10 +- .../home/study/study_class_list_page.dart | 4 +- lib/pages/home/study/study_detail_page.dart | 360 ++++++++++-------- lib/pages/home/study/take_exam_page.dart | 12 +- .../special_wrok/MeasuresListWidget.dart | 322 ++++++++-------- .../dh_work/HotWorkDetailFormWidget.dart | 21 +- .../hot_safe_work_choose_page.dart | 18 - .../dh_work/hotwork_list_page.dart | 45 +-- .../blindboard_apply_detail.dart | 77 ++-- pubspec.yaml | 2 +- 10 files changed, 473 insertions(+), 398 deletions(-) delete mode 100644 lib/pages/home/tap/tabList/special_wrok/dh_work/dh_work_detai/hot_safe_work_choose_page.dart diff --git a/lib/pages/home/SafeCheck/CheckPersonSign/safecheck_sign_detail.dart b/lib/pages/home/SafeCheck/CheckPersonSign/safecheck_sign_detail.dart index dc2f56f..7a72547 100644 --- a/lib/pages/home/SafeCheck/CheckPersonSign/safecheck_sign_detail.dart +++ b/lib/pages/home/SafeCheck/CheckPersonSign/safecheck_sign_detail.dart @@ -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) { diff --git a/lib/pages/home/study/study_class_list_page.dart b/lib/pages/home/study/study_class_list_page.dart index 2976d44..d3b99f7 100644 --- a/lib/pages/home/study/study_class_list_page.dart +++ b/lib/pages/home/study/study_class_list_page.dart @@ -16,6 +16,7 @@ class _StudyClassListPageState extends State { late List _list = []; late String _classId = ''; late String _post_id = ''; + late Map _classInfo = {}; @override void initState() { super.initState(); @@ -32,6 +33,7 @@ class _StudyClassListPageState extends State { final result = await ApiService.getClassList(_classId, _post_id); if (result['result'] == 'success') { final List newList = result['varList'] ?? []; + _classInfo = result['classInfo'] ?? {}; setState(() { _list.addAll(newList); }); @@ -46,7 +48,7 @@ class _StudyClassListPageState extends State { 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 diff --git a/lib/pages/home/study/study_detail_page.dart b/lib/pages/home/study/study_detail_page.dart index fd98a5d..ceda9ee 100644 --- a/lib/pages/home/study/study_detail_page.dart +++ b/lib/pages/home/study/study_detail_page.dart @@ -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 createState() => _StudyDetailPageState(); @@ -54,6 +61,7 @@ class _StudyDetailPageState extends State int _currentNodeIndex = 0; bool _hasNodes = false; Duration _lastReported = Duration.zero; + bool _isFace = false; // 上报控制相关字段 bool _endReported = false; // 确保最终结束上报只执行一次 @@ -69,6 +77,7 @@ class _StudyDetailPageState extends State 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 } Future _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 return '$s%'; } - - Future _onVideoTap( Map data, bool hasNodes, @@ -191,9 +201,15 @@ class _StudyDetailPageState extends State 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 seconds: int.parse('${data['VIDEOTIME'] ?? '0'}'), ); } - } else { ToastUtil.showNormal(context, '课件文件资源已失效,请联系管理员'); } @@ -267,59 +282,69 @@ class _StudyDetailPageState extends State // 视频 await _getVideoPlayInfo(data['VIDEOCOURSEWARE_ID']); LoadingDialogHelper.hide(); - _startFaceTimer(); + if (_isFace) { + _startFaceTimer(); + } } }); } Future _navigateFaceIfNeeded(FutureOr 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( + 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( - 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 _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 }) 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 // 如果这是“非视频课件”(通过 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 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?; @@ -529,7 +566,8 @@ class _StudyDetailPageState extends State } } } 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 // 以下为视频上报原有逻辑:解析后端返回数据并更新进度 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 } } - - /// 开始考试 - Future _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 _exitTopRouteAndWait({int waitMs = 300}) async { - _exitTopRouteIfPresent(); - await Future.delayed(Duration(milliseconds: waitMs)); - } - - /// 锁定为竖屏 - Future _lockPortrait() async { - await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - } - - /// 恢复允许横竖 - Future _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 } Future _showFaceAuthOnce() async { + // 仅在人脸功能开启时调用( + if (!_isFace) return; // 先 cancel 定时器,避免跳转过程中再次触发 _faceTimer?.cancel(); _faceTimer = null; @@ -723,10 +695,14 @@ class _StudyDetailPageState extends State await _lockPortrait(); final passed = await pushPage( - 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 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 } } + /// 开始考试 + Future _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 _exitTopRouteAndWait({int waitMs = 300}) async { + _exitTopRouteIfPresent(); + await Future.delayed(Duration(milliseconds: waitMs)); + } + + /// 锁定为竖屏 + Future _lockPortrait() async { + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + } + + /// 恢复允许横竖 + Future _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 const SizedBox(width: 5), Text( (item['NAME'] ?? '').toString(), - style: const TextStyle( - fontSize: 15, - ), + style: const TextStyle(fontSize: 15), ), ], ), diff --git a/lib/pages/home/study/take_exam_page.dart b/lib/pages/home/study/take_exam_page.dart index 2914d59..bba221e 100644 --- a/lib/pages/home/study/take_exam_page.dart +++ b/lib/pages/home/study/take_exam_page.dart @@ -116,9 +116,6 @@ class _TakeExamPageState extends State { content: '您无考试次数!', ); if (ok) { - if (widget.jumpType == 2) { - Navigator.pop(context); - } Navigator.pop(context); }; } @@ -235,10 +232,17 @@ class _TakeExamPageState extends State { 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(); } } diff --git a/lib/pages/home/tap/tabList/special_wrok/MeasuresListWidget.dart b/lib/pages/home/tap/tabList/special_wrok/MeasuresListWidget.dart index 3eea840..09ebd58 100644 --- a/lib/pages/home/tap/tabList/special_wrok/MeasuresListWidget.dart +++ b/lib/pages/home/tap/tabList/special_wrok/MeasuresListWidget.dart @@ -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 _buildImageRows( - BuildContext context, - List paths, - String time, - double right, - ) { + BuildContext context, + List 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] : ''), ), diff --git a/lib/pages/home/tap/tabList/special_wrok/dh_work/HotWorkDetailFormWidget.dart b/lib/pages/home/tap/tabList/special_wrok/dh_work/HotWorkDetailFormWidget.dart index 87b28ec..7e5048c 100644 --- a/lib/pages/home/tap/tabList/special_wrok/dh_work/HotWorkDetailFormWidget.dart +++ b/lib/pages/home/tap/tabList/special_wrok/dh_work/HotWorkDetailFormWidget.dart @@ -167,13 +167,13 @@ class _HotWorkDetailFormWidgetState extends State { 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 { 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 { text: pd['VIDEONAME'] ?? '', ), - if (FormUtils.hasValue(pd, 'ANALYZE_TIME') && !widget.isEditable) ...[ - const Divider(), - ItemListWidget.OneRowStartButtonTitle( - label: '气体分析信息:', - text: pd['ANALYZE_USER_NAME'] ?? '', - onTap: widget.onAnalyzeTap, - ), - ], + ], ), ); diff --git a/lib/pages/home/tap/tabList/special_wrok/dh_work/dh_work_detai/hot_safe_work_choose_page.dart b/lib/pages/home/tap/tabList/special_wrok/dh_work/dh_work_detai/hot_safe_work_choose_page.dart deleted file mode 100644 index b66f8cc..0000000 --- a/lib/pages/home/tap/tabList/special_wrok/dh_work/dh_work_detai/hot_safe_work_choose_page.dart +++ /dev/null @@ -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 createState() => _HotSafeWorkChoosePageState(); -} - -class _HotSafeWorkChoosePageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: MyAppbar(title: 'dong'), - ); - } -} diff --git a/lib/pages/home/tap/tabList/special_wrok/dh_work/hotwork_list_page.dart b/lib/pages/home/tap/tabList/special_wrok/dh_work/hotwork_list_page.dart index 028aa4c..0262499 100644 --- a/lib/pages/home/tap/tabList/special_wrok/dh_work/hotwork_list_page.dart +++ b/lib/pages/home/tap/tabList/special_wrok/dh_work/hotwork_list_page.dart @@ -364,35 +364,33 @@ class _HotWorkListPageState extends State { 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 { 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, diff --git a/lib/pages/home/tap/tabList/special_wrok/mbcd_work/mbcd_work_detai/blindboard_apply_detail.dart b/lib/pages/home/tap/tabList/special_wrok/mbcd_work/mbcd_work_detai/blindboard_apply_detail.dart index 6ee503f..25721be 100644 --- a/lib/pages/home/tap/tabList/special_wrok/mbcd_work/mbcd_work_detai/blindboard_apply_detail.dart +++ b/lib/pages/home/tap/tabList/special_wrok/mbcd_work/mbcd_work_detai/blindboard_apply_detail.dart @@ -65,13 +65,17 @@ class _BlindboardApplyDetailState extends State { late List boardList = [ {'BOARD_MATERIAL': '', 'BOARD_SPECIFICATION': '', 'BOARD_NO': ''}, ]; + /// ------------------- 新增 ------------------- /// 视频监控摄像 late List videoMonitoringList = []; + /// 承包商列表 late List unitAllList = []; + /// 作业区域列表 late List workAreaList = []; + /// -------------------------------------- late Map signs = {}; late List> measuresList = []; @@ -86,8 +90,6 @@ class _BlindboardApplyDetailState extends State { final TextEditingController _relatedController = TextEditingController(); final TextEditingController _riskController = TextEditingController(); - - // 存储各单位的人员列表 final Map>> _personCache = {}; @@ -105,12 +107,11 @@ class _BlindboardApplyDetailState extends State { 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 { pd['RISK_IDENTIFICATION'] = _riskController.text.trim(); }); } + /// ---------------------------- 新增 -------------------------------- /// 视频监控摄像头 Future _chooseVideoManager() async { final choice = await BottomPicker.show( 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 { pd['VIDEONAME'] = choice; Map 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 { }); } } + /// 选择承包商 Future _chooseUnitManager() async { final choice = await BottomPicker.show( @@ -172,7 +175,7 @@ class _BlindboardApplyDetailState extends State { pd['UNITS_NAME'] = choice; Map 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 { }); } } - - + /// 选择经纬度 - Future _showLocationHandle() async{ + Future _showLocationHandle() async { if (!FormUtils.hasValue(pd, 'ELECTRONIC_FENCE_AREA_ID')) { ToastUtil.showNormal(context, '请选择作业区域'); return; @@ -194,10 +196,11 @@ class _BlindboardApplyDetailState extends State { setState(() { pd['LONGITUDE'] = mapData['longitue'] ?? ''; pd['LATITUDE'] = mapData['latitude'] ?? ''; - pd['LATITUDE_LONGITUDE'] = '${mapData['longitue'] ?? ''},${mapData['latitude'] ?? ''}'; + pd['LATITUDE_LONGITUDE'] = + '${mapData['longitue'] ?? ''},${mapData['latitude'] ?? ''}'; }); - } + /// 作业区域 Future _getWorkArea() async { //FocusHelper.clearFocus(context); @@ -208,18 +211,19 @@ class _BlindboardApplyDetailState extends State { 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 _getVideoList() async { final result = await ApiService.getVideomanagerList(); @@ -227,6 +231,7 @@ class _BlindboardApplyDetailState extends State { videoMonitoringList = result['varList'] ?? []; }); } + /// 获承包商列表 Future _getUnitListAll() async { final result = await ApiService.getUnitListAll(); @@ -234,7 +239,7 @@ class _BlindboardApplyDetailState extends State { 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 { 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 { ]; 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.GUARDIAN, @@ -478,7 +497,8 @@ class _BlindboardApplyDetailState extends State { 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 { 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 { } } - - /// 初始化拉取数据 Future _getData() async { final data = await ApiService.getHomeworkFindById( diff --git a/pubspec.yaml b/pubspec.yaml index 8bc9ce1..ab97769 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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