。。。。
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,7 +107,6 @@ 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();
|
||||
|
@ -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')) {
|
||||
|
@ -183,9 +186,8 @@ 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();
|
||||
|
@ -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