diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d1e408d..b73d0e7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - camera_avfoundation (0.0.1): + - Flutter - connectivity_plus (0.0.1): - Flutter - Flutter (1.0.0) @@ -23,8 +25,12 @@ PODS: - video_player_avfoundation (0.0.1): - Flutter - FlutterMacOS + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS DEPENDENCIES: + - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - Flutter (from `Flutter`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) @@ -35,8 +41,11 @@ DEPENDENCIES: - photo_manager (from `.symlinks/plugins/photo_manager/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) EXTERNAL SOURCES: + camera_avfoundation: + :path: ".symlinks/plugins/camera_avfoundation/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" Flutter: @@ -57,8 +66,11 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" video_player_avfoundation: :path: ".symlinks/plugins/video_player_avfoundation/darwin" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: + camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436 connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 @@ -69,6 +81,7 @@ SPEC CHECKSUMS: photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b + webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 PODFILE CHECKSUM: 4305caec6b40dde0ae97be1573c53de1882a07e5 diff --git a/lib/http/ApiService.dart b/lib/http/ApiService.dart index eeb53dd..0c1ade8 100644 --- a/lib/http/ApiService.dart +++ b/lib/http/ApiService.dart @@ -1,39 +1,48 @@ +import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:qhd_prevention/tools/tools.dart'; import 'HttpManager.dart'; class ApiService { - // static const String basePath = "http://192.168.0.25:28199/"; - // static const String basePath = "http://192.168.20.240:8500/integrated_whb"; - // static const String baseFacePath = "http://192.168.0.25:38199/"; - // 人脸识别服务 - // static const String baseFacePath = "https://qaaqwh.qhdsafety.com/whb_stu_face/"; - // static const String basePath = "https://qaaqwh.qhdsafety.com/integrated_whb/"; + /// 人脸识别服务 + // static const String baseFacePath = + // "https://qaaqwh.qhdsafety.com/whb_stu_face/"; + // + // /// 登录及其他管理后台接口 + // static const String basePath = "https://qaaqwh.qhdsafety.com/integrated_whb"; + // + // /// 图片文件服务 // static const String baseImgPath = "https://file.zcloudchina.com/YTHFile"; - // static const String adminPath = "https://qaaqwh.qhdsafety.com/integrated_whb/"; - // static const String projectManagerUrl = 'https://pm.qhdsafety.com/zy-projectManage/'; - // static const String publicKey = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUoHAavCikaZxjlDM6Km8cX+ye78F4oF39AcEfnE1p2Yn9pJ9WFxYZ4Vkh6F8SKMi7k4nYsKceqB1RwG996SvHQ5C3pM3nbXCP4K15ad6QhN4a7lzlbLhiJcyIKszvvK8ncUDw8mVQ0j/2mwxv05yH6LN9OKU6Hzm1ninpWeE+awIDAQAB' + // + // /// 管理后台统一路径 + // static const String adminPath = + // "https://qaaqwh.qhdsafety.com/integrated_whb/"; + // + // /// 项目管理系统 + // static const String projectManagerUrl = + // 'https://pm.qhdsafety.com/zy-projectManage'; /// 人脸识别服务 static const String baseFacePath = - "https://qaaqwh.qhdsafety.com/whb_stu_face/"; + "http://192.168.0.25:38199/"; /// 登录及其他管理后台接口 - static const String basePath = "https://qaaqwh.qhdsafety.com/integrated_whb"; + static const String basePath = "http://192.168.20.240:8500/integrated_whb/"; /// 图片文件服务 static const String baseImgPath = "https://file.zcloudchina.com/YTHFile"; /// 管理后台统一路径 static const String adminPath = - "https://qaaqwh.qhdsafety.com/integrated_whb/"; + "http://192.168.20.240:8500/integrated_whb/"; /// 项目管理系统 static const String projectManagerUrl = 'https://pm.qhdsafety.com/zy-projectManage'; - /// RSA 公钥 + /// RSA 秘钥 static const publicKey = ''' -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUoHAavCikaZxjlDM6Km8cX+ye @@ -220,7 +229,7 @@ U6Hzm1ninpWeE+awIDAQAB }, ); } -/// TODO -------------–-------------------- 首页学习园地 -------------–-------------------- +// TODO -------------–-------------------- 首页学习园地 -------------–-------------------- /// 我的任务列表 static Future> getStudyList(int page) { return HttpManager().request( @@ -251,6 +260,133 @@ U6Hzm1ninpWeE+awIDAQAB }, ); } + /// 学习详情视频列表 + static Future> getStudyDetailList(String CLASS_ID, String CLASSCURRICULUM_ID, String STUDENT_ID) { + print(CLASS_ID + '---' + CLASSCURRICULUM_ID + '---' + STUDENT_ID+ '---' + SessionService.instance.corpinfoId! + '---' + SessionService.instance.loginUserId!); + return HttpManager().request( + basePath, + '/app/edu/stagestudentrelation/getMyTask', + method: Method.post, + data: { + 'CLASSCURRICULUM_ID': CLASSCURRICULUM_ID, + 'CLASS_ID' : CLASS_ID, + 'STUDENT_ID':STUDENT_ID, + 'CORPINFO_ID': SessionService.instance.corpinfoId, + 'USER_ID': SessionService.instance.loginUserId, + + }, + ); + } + static Future> fnGetVideoPlayInfo(String VIDEOCOURSEWARE_ID) { + return HttpManager().request( + basePath, + '/app/edu/audioOrVideo/getVideoPlayInfoApp', + method: Method.post, + data: { + 'VIDEOCOURSEWARE_ID': VIDEOCOURSEWARE_ID, + 'CORPINFO_ID': SessionService.instance.corpinfoId, + 'USER_ID': SessionService.instance.loginUserId, + + }, + ); + } + static Future> fnSetUserFaceTime(String FACE_TIME) { + return HttpManager().request( + baseFacePath, + '/app/user/setUserFaceTime', + method: Method.post, + data: { + 'loading':false, + 'FACE_TIME': FACE_TIME, + 'CORPINFO_ID': SessionService.instance.corpinfoId, + 'USER_ID': SessionService.instance.loginUserId, + + }, + ); + } + static Future> fnGetVideoPlayProgress(String VIDEOCOURSEWARE_ID, String CURRICULUM_ID, String CLASS_ID, String STUDENT_ID) { + return HttpManager().request( + basePath, + '/app/edu/coursestudyvideorecord/getVideoProgress', + method: Method.post, + data: { + 'VIDEOCOURSEWARE_ID': VIDEOCOURSEWARE_ID, + 'CURRICULUM_ID' : CURRICULUM_ID, + 'CLASS_ID':CLASS_ID, + 'STUDENT_ID':STUDENT_ID, + 'CORPINFO_ID': SessionService.instance.corpinfoId, + 'USER_ID': SessionService.instance.loginUserId, + + }, + ); + } + /// 上报播放进度或结束 + static Future> fnSubmitPlayTime(String VIDEOCOURSEWARE_ID, String CURRICULUM_ID,String IS_END,int RESOURCETIME,String CHAPTER_ID,String STUDENT_ID,String CLASSCURRICULUM_ID, String CLASS_ID) { + return HttpManager().request( + basePath, + '/app/edu/coursestudyvideorecord/save', + method: Method.post, + data: { + 'VIDEOCOURSEWARE_ID': VIDEOCOURSEWARE_ID, + 'CURRICULUM_ID': CURRICULUM_ID, + 'CHAPTER_ID': CHAPTER_ID, + 'RESOURCETIME': RESOURCETIME, + 'IS_END':IS_END, + 'CLASS_ID': CLASS_ID, + 'CLASSCURRICULUM_ID': CLASSCURRICULUM_ID, + 'STUDENT_ID': STUDENT_ID, + 'loading': false, + 'USER_NAME': SessionService.instance.username, + 'CORPINFO_ID': SessionService.instance.corpinfoId, + 'USER_ID': SessionService.instance.loginUserId, + + }, + ); + } + /// 人脸比对上传接口 + /// + /// [imagePath] 本地图片文件路径 + /// 返回后端完整 JSON + static Future> getUserFace(String imagePath, String studentId) async { + final file = File(imagePath); + if (!await file.exists()) { + throw ApiException('file_not_found', '图片不存在:$imagePath'); + } + final fileName = file.path.split(Platform.pathSeparator).last; + + return HttpManager().uploadFaceImage( + baseUrl: baseFacePath, + path: '/app/user/compareFaceForH5V2', + fromData: { + 'USER_ID' : SessionService.instance.loginUserId, + 'STUDENT_ID' : studentId, + 'CORPINFO_ID' : SessionService.instance.corpinfoId, + 'FFILE' : await MultipartFile.fromFile( + file.path, + filename: fileName + ) + }, + + ); + } + /// 签名上传 + static Future> signUpdate(String signBase64, String CLASS_ID, String STAGESTUDENTRELATION_ID) { + return HttpManager().request( + basePath, + '/app/edu/stagestudentrelation/sign', + method: Method.post, + data: { + 'FFILE':signBase64, + 'STUDYSTATE':1, + 'CLASS_ID' : CLASS_ID, + 'STAGESTUDENTRELATION_ID': STAGESTUDENTRELATION_ID, + 'OPERATOR': SessionService.instance.username, + 'USER_ID': SessionService.instance.loginUserId, + }, + ); + } + + @@ -307,7 +443,29 @@ U6Hzm1ninpWeE+awIDAQAB ); } + /// 更新人脸信息 + static Future> reloadMyFace(String imagePath) async { + final file = File(imagePath); + if (!await file.exists()) { + throw ApiException('file_not_found', '图片不存在:$imagePath'); + } + final fileName = file.path.split(Platform.pathSeparator).last; + + return HttpManager().uploadFaceImage( + baseUrl: basePath, + path: '/app/user/editUserFaceV2', + fromData: { + 'CORPINFO_ID': SessionService.instance.corpinfoId, + 'USER_ID': SessionService.instance.loginUserId, + 'FFILE': await MultipartFile.fromFile( + file.path, + filename: fileName + ), + } + ); + } } + diff --git a/lib/http/HttpManager.dart b/lib/http/HttpManager.dart index 8a09aab..a068696 100644 --- a/lib/http/HttpManager.dart +++ b/lib/http/HttpManager.dart @@ -1,6 +1,7 @@ +import 'dart:io'; import 'package:dio/dio.dart'; -/// 统一接口异常 +/// 全局接口异常 class ApiException implements Exception { final String result; final String message; @@ -12,13 +13,14 @@ class ApiException implements Exception { /// HTTP 方法枚举 enum Method { get, post, put, delete } +/// HTTP 管理器 单例 class HttpManager { HttpManager._internal() { _dio = Dio(BaseOptions( connectTimeout: const Duration(milliseconds: 10000), receiveTimeout: const Duration(milliseconds: 10000), headers: { - 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Type': Headers.formUrlEncodedContentType, }, )); _initInterceptors(); @@ -32,17 +34,12 @@ class HttpManager { _dio.interceptors ..add(LogInterceptor(request: true, responseBody: true, error: true)) ..add(InterceptorsWrapper(onError: (err, handler) { - // 全局错误处理,可根据 err.response?.statusCode 或 err.type 操作 + // 全局错误处理,可根据 err.response?.statusCode 或 err.type 做不同处理 handler.next(err); })); } - /// 通用 request 方法,返回完整后台 JSON - /// baseUrl: 基础路径,如 basePath - /// path: 接口路径,如 '/admin/check' - /// method: HTTP 方法,默认 POST - /// data: Form 表单参数 - /// params: URL 查询参数 + /// 通用请求方法,返回完整后台 JSON Future> request( String baseUrl, String path, { @@ -57,28 +54,46 @@ class HttpManager { method: method.name.toUpperCase(), contentType: Headers.formUrlEncodedContentType, ); + try { switch (method) { case Method.get: - resp = await _dio.get(url, - queryParameters: params, cancelToken: cancelToken, options: options); + resp = await _dio.get( + url, + queryParameters: params, + cancelToken: cancelToken, + options: options, + ); break; case Method.put: - resp = await _dio.put(url, - data: data, queryParameters: params, cancelToken: cancelToken, options: options); + resp = await _dio.put( + url, + data: data, + queryParameters: params, + cancelToken: cancelToken, + options: options, + ); break; case Method.delete: - resp = await _dio.delete(url, - queryParameters: params, cancelToken: cancelToken, options: options); + resp = await _dio.delete( + url, + queryParameters: params, + cancelToken: cancelToken, + options: options, + ); break; case Method.post: - default: - resp = await _dio.post(url, - data: data, queryParameters: params, cancelToken: cancelToken, options: options); + resp = await _dio.post( + url, + data: data, + queryParameters: params, + cancelToken: cancelToken, + options: options, + ); } - } on DioError catch (e) { + } on DioException catch (e) { // 网络或服务器错误 - throw ApiException('network_error', e.message ?? ""); + throw ApiException('network_error', e.message ?? e.toString()); } // 解析返回 JSON @@ -91,9 +106,37 @@ class HttpManager { // 非 success 都抛异常 throw ApiException(result ?? 'unknown', msg); } - // 返回完整数据,包括 msg、USER_ID 等 return json; } - } +/// 上传图片扩展 +extension HttpManagerUpload on HttpManager { + Future> uploadFaceImage({ + required String baseUrl, + required String path, + required Map fromData, + CancelToken? cancelToken, + }) async { + final form = FormData.fromMap(fromData); + try { + final resp = await _dio.post( + baseUrl + path, + data: form, + cancelToken: cancelToken, + options: Options( + method: Method.post.name.toUpperCase(), + contentType: 'multipart/form-data', + ), + ); + + final json = resp.data is Map + ? resp.data as Map + : {}; + + return json; + } on DioException catch (e) { + throw ApiException('network_error', e.message ?? e.toString()); + } + } +} diff --git a/lib/pages/app/Danger_paicha/check_record_list_page.dart b/lib/pages/app/Danger_paicha/check_record_list_page.dart index 7c4d77c..18bf4e7 100644 --- a/lib/pages/app/Danger_paicha/check_record_list_page.dart +++ b/lib/pages/app/Danger_paicha/check_record_list_page.dart @@ -1,14 +1,10 @@ -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:qhd_prevention/customWidget/danner_repain_item.dart'; import 'package:qhd_prevention/customWidget/department_picker.dart'; import 'package:qhd_prevention/customWidget/search_bar_widget.dart'; -import 'package:qhd_prevention/pages/home/scan_page.dart'; -import 'package:qhd_prevention/pages/home/work/risk_list_page.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; -import 'package:qhd_prevention/tools/SmallWidget.dart'; -import 'package:qhd_prevention/tools/tools.dart'; + class CheckRecordListPage extends StatefulWidget { const CheckRecordListPage({super.key}); diff --git a/lib/pages/home/study/face_ecognition_page.dart b/lib/pages/home/study/face_ecognition_page.dart new file mode 100644 index 0000000..60226f0 --- /dev/null +++ b/lib/pages/home/study/face_ecognition_page.dart @@ -0,0 +1,195 @@ +import 'dart:async'; +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import '../../../http/ApiService.dart'; + +/// 人脸识别模式 +enum FaceMode { auto, manual } + +class FaceRecognitionPage extends StatefulWidget { + final String studentId; + final FaceMode mode; + + const FaceRecognitionPage({ + Key? key, + this.studentId = '', + required this.mode, + }) : super(key: key); + + @override + _FaceRecognitionPageState createState() => _FaceRecognitionPageState(); +} + +class _FaceRecognitionPageState extends State { + CameraController? _cameraController; + Timer? _timer; + int _attempts = 0; + String _message = ''; + String _tip = '请将人脸置于圆圈内'; + + static const int _maxAttempts = 8; + static const Duration _interval = Duration(seconds: 2); + + bool get _isManualMode => widget.mode == FaceMode.manual; + + @override + void initState() { + super.initState(); + _initCamera(); + } + + @override + void dispose() { + _timer?.cancel(); + _cameraController?.dispose(); + super.dispose(); + } + + Future _initCamera() async { + final cams = await availableCameras(); + final front = cams.firstWhere((c) => c.lensDirection == CameraLensDirection.front); + _cameraController = CameraController(front, ResolutionPreset.medium, enableAudio: false); + await _cameraController!.initialize(); + if (!mounted) return; + setState(() {}); + + if (!_isManualMode) { + _timer = Timer.periodic(_interval, (_) => _captureAndUpload()); + } + } + + Future _captureAndUpload() async { + if (_isManualMode) { + setState(() => _message = '请将人脸置于圆圈内'); + } else { + if (_attempts >= _maxAttempts) return _onTimeout(); + _attempts++; + } + + try { + final pic = await _cameraController!.takePicture(); + final res = await ApiService.getUserFace(pic.path, widget.studentId); + if (res['result'] == 'success') { + _onSuccess(); + } else { + setState(() => _message = '识别失败,请重试'); + } + } catch (_) { + setState(() => _message = '发生错误,请重试'); + } + } + + Future _captureAndReload() async { + setState(() => _message = '上传中...'); + try { + final pic = await _cameraController!.takePicture(); + final res = await ApiService.reloadMyFace(pic.path,); + if (res['result'] == 'success') { + _onSuccess(); + } else { + setState(() => _message = '验证失败,请重试'); + } + } catch (_) { + setState(() => _message = '发生错误,请重试'); + } + } + + void _onSuccess() { + _timer?.cancel(); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('验证成功'))); + Future.delayed(const Duration(milliseconds: 800), () => Navigator.of(context).pop(true)); + } + + void _onTimeout() { + _timer?.cancel(); + setState(() => _message = '人脸超时,请重新识别!'); + Future.delayed(const Duration(seconds: 3), () => Navigator.of(context).pop(false)); + } + + @override + Widget build(BuildContext context) { + if (_cameraController == null || !_cameraController!.value.isInitialized) { + return const Scaffold( + backgroundColor: Colors.white, + body: Center(child: CircularProgressIndicator()), + ); + } + final previewSize = _cameraController!.value.previewSize!; + final previewAspect = previewSize.height / previewSize.width; + final radius = (screenWidth(context) - 100) / 2; + + return Scaffold( + backgroundColor: Colors.white, + appBar: MyAppbar(title: '人脸识别'), + body: Stack( + children: [ + Positioned.fill(child: Container(color: Colors.white)), + + Transform.translate( + offset: const Offset(0, -100), + child: Stack( + children: [ + Center( + child: ClipOval( + child: AspectRatio( + aspectRatio: previewAspect, + child: CameraPreview(_cameraController!), + ), + ), + ), + Positioned.fill( + child: CustomPaint( + painter: _WhiteMaskPainter(radius: radius), + ), + ), + ], + ), + ), + + Align( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.only(top: 250), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _tip, + style: const TextStyle(fontSize: 18, color: Colors.black87), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + if (_isManualMode) + CustomButton(text: '拍照/上传', backgroundColor: Colors.blue,onPressed: _captureAndReload,) + ], + ), + ), + ), + ], + ), + ); + } +} + +class _WhiteMaskPainter extends CustomPainter { + final double radius; + _WhiteMaskPainter({required this.radius}); + + @override + void paint(Canvas canvas, Size size) { + canvas.saveLayer(Offset.zero & size, Paint()); + canvas.drawRect(Offset.zero & size, Paint()..color = Colors.white); + canvas.drawCircle( + size.center(Offset.zero), + radius, + Paint()..blendMode = BlendMode.clear, + ); + canvas.restore(); + } + + @override + bool shouldRepaint(covariant CustomPainter old) => false; +} diff --git a/lib/pages/home/study/study_class_list_page.dart b/lib/pages/home/study/study_class_list_page.dart index 569ab06..6b52fe5 100644 --- a/lib/pages/home/study/study_class_list_page.dart +++ b/lib/pages/home/study/study_class_list_page.dart @@ -6,18 +6,21 @@ import 'package:qhd_prevention/pages/my_appbar.dart'; import 'package:qhd_prevention/tools/tools.dart'; class StudyClassListPage extends StatefulWidget { - const StudyClassListPage(this.classId, this.POST_ID, {super.key}); - final String classId; - final String POST_ID; + const StudyClassListPage(this.studyData, {super.key}); + final Map studyData; @override State createState() => _StudyClassListPageState(); } class _StudyClassListPageState extends State { late List _list = []; + late String _classId = ''; + late String _post_id = ''; @override void initState() { super.initState(); + _classId = widget.studyData['CLASS_ID'] ?? ''; + _post_id = widget.studyData['POST_ID'] ?? ''; WidgetsBinding.instance.addPostFrameCallback((_) { _getData(); }); @@ -26,7 +29,7 @@ class _StudyClassListPageState extends State { Future _getData() async { LoadingDialogHelper.show(context); try { - final result = await ApiService.getClassList(widget.classId, widget.POST_ID); + final result = await ApiService.getClassList(_classId, _post_id); if (result['result'] == 'success') { final List newList = result['varList'] ?? []; setState(() { @@ -46,8 +49,16 @@ class _StudyClassListPageState extends State { appBar: MyAppbar(title: "课程列表"), body: SafeArea( child: _list.isEmpty - ? Center(child: Text('暂无数据')) - : ListView.builder( + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset('assets/images/null.png', width: 150), + SizedBox(height: 12), + Text('暂无数据', style: TextStyle(color: Colors.grey)), + ], + ), + ) : ListView.builder( itemCount: _list.length, itemBuilder: (context, index) { return _buildItem(_list[index]); @@ -88,32 +99,41 @@ class _StudyClassListPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - item['CURRICULUMNAME'] ?? '', + item['CURRICULUMNAME'], style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), - const SizedBox(height: 4), - Text( - item['CURRICULUMINTRODUCE'] ?? '', - softWrap: true, - overflow: TextOverflow.visible, - style: const TextStyle(color: Colors.black54), + Row( + // crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: Text( + item['CURRICULUMINTRODUCE'] ?? '', + softWrap: true, + overflow: TextOverflow.visible, + style: const TextStyle(color: Colors.black54), + ),), + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(), + CustomButton( + text: "立即学习", + backgroundColor: Colors.blue, + height: 38, + onPressed: () { + pushPage(StudyDetailPage(item, widget.studyData['STUDENT_ID']), context); + }, + ), + ], + ) + ], ), + ], ), ), - const SizedBox(width: 10), - Container( - height: 80, // 确保与图片高度一致 - alignment: Alignment.bottomRight, - child: CustomButton( - text: "立即学习", - backgroundColor: Colors.blue, - height: 38, - onPressed: () { - pushPage(StudyDetailPage(item), context); - }, - ), - ), + // const SizedBox(width: 5), + ], ), ); diff --git a/lib/pages/home/study/study_detail_page.dart b/lib/pages/home/study/study_detail_page.dart index 5734eac..1825d43 100644 --- a/lib/pages/home/study/study_detail_page.dart +++ b/lib/pages/home/study/study_detail_page.dart @@ -1,43 +1,504 @@ +import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; +import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/pages/home/study/study_practise_page.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/tools/tools.dart'; -import '../../../customWidget/custom_alert_dialog.dart'; +import 'face_ecognition_page.dart'; class StudyDetailPage extends StatefulWidget { - const StudyDetailPage(this.detail, {super.key}); + final Map studyDetailDetail; + final String studentId; - final Map detail; + const StudyDetailPage(this.studyDetailDetail, this.studentId, {super.key}); @override State createState() => _StudyDetailPageState(); } -class _StudyDetailPageState extends State { +class _StudyDetailPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + VideoPlayerController? _videoController; + + Map? _info; + List _videoList = []; + bool _loading = true; + + Timer? _faceTimer; + bool _throttleFlag = false; + late int _faceTime; + late String _classId; + late String _classCurriculumId; + + Map? _currentVideoData; + int _currentFirstIndex = 0; + int _currentNodeIndex = 0; + bool _hasNodes = false; + Duration _lastReported = Duration.zero; + @override void initState() { - // TODO: implement initState super.initState(); + _classId = widget.studyDetailDetail['CLASS_ID'] ?? ''; + _classCurriculumId = + widget.studyDetailDetail['CLASSCURRICULUM_ID'] ?? ''; + _tabController = TabController(length: 2, vsync: this); WidgetsBinding.instance.addPostFrameCallback((_) { - showDialog( - context: context, - builder: - (context) => CustomAlertDialog( - title: "提示", - content: "重要提醒:尊敬的用户,根据规定我们会在您学习过程中多次进行人脸识别认证,为了保护您的隐私请您在摄像设备视野内确保衣冠整齐。", - cancelText: "取消", - confirmText: "同意并继续", - onCancel: () {}, - onConfirm: () {}, - ), - ); + _showFaceIntro(); + _loadData(); }); } + @override + void dispose() { + _tabController.dispose(); + _videoController?.removeListener(_onTimeUpdate); + _videoController?.dispose(); + _faceTimer?.cancel(); + super.dispose(); + } + + Future _showFaceIntro() async { + await showDialog( + context: context, + builder: (_) => CustomAlertDialog( + title: '温馨提示', + content: + '重要提醒:尊敬的用户,根据规定我们会在您学习过程中多次进行人脸识别认证,为了保护您的隐私请您在摄像设备视野内确保衣冠整齐。', + cancelText: '取消', + confirmText: '同意并继续', + onCancel: () => Navigator.of(context).pop(), + onConfirm: () => {}, + ), + ); + } + + Future _loadData() async { + try { + final res = await ApiService.getStudyDetailList( + _classId, + _classCurriculumId, + widget.studentId, + ); + final pd = res['pd'] ?? {}; + _faceTime = int.tryParse(pd['FACE_TIME']?.toString() ?? '10') ?? 10; + _videoList = List.from(pd['VIDEOLIST'] ?? []); + // 计算 percent + for (var item in _videoList) { + if (item['nodes'] is List && (item['nodes'] as List).isNotEmpty) { + for (var node in item['nodes']) { + node['percent'] = _calcPercentStr( + playCount: node['PLAYCOUNT'], + resourceTime: node['RESOURCETIME'], + videoTime: node['VIDEOTIME'], + ); + } + } else { + item['percent'] = _calcPercentStr( + playCount: item['PLAYCOUNT'], + resourceTime: item['RESOURCETIME'], + videoTime: item['VIDEOTIME'], + ); + } + } + setState(() { + _info = pd; + _loading = false; + }); + } catch (_) { + setState(() => _loading = false); + } + } + + String _calcPercentStr({ + required dynamic playCount, + required dynamic resourceTime, + required dynamic videoTime, + }) { + final seen = int.tryParse('$playCount') ?? 0; + if (seen > 0) return '100%'; + final resT = double.tryParse('$resourceTime') ?? 0.0; + final vidT = double.tryParse('$videoTime') ?? 0.0; + if (vidT == 0) return '0%'; + final pct = ((resT / vidT) * 100).clamp(0, 100).toStringAsFixed(0); + return '$pct%'; + } + + Future _onVideoTap( + Map data, bool hasNodes, int fi, int ni) async { + // 停掉上一轮人脸定时 + _faceTimer?.cancel(); + + // 上报上一个视频进度 + if (_currentVideoData != null && _lastReported > Duration.zero) { + await _submitPlayTime(end: false, seconds: _lastReported.inSeconds); + } + _lastReported = Duration.zero; + + _currentVideoData = data; + _hasNodes = hasNodes; + _currentFirstIndex = fi; + _currentNodeIndex = ni; + + // 先暂停任何正在播放的视频 + _videoController?.pause(); + + // 统一认证逻辑 + await _navigateFaceIfNeeded(() async { + if ((data['IS_VIDEO'] ?? 0) == 1) { + // 资料 + if (data['VIDEOFILES'] != null) { + await pushPage( + StudyPractisePage(data['VIDEOCOURSEWARE_ID']), + context, + ); + await _submitPlayTime(end: true, seconds: int.parse(data['VIDEOTIME'] ?? '0')); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('课件文件资源已失效,请联系管理员')), + ); + } + } else { + // 视频 + await _getVideoPlayInfo(data['VIDEOCOURSEWARE_ID']); + // 播放并启动定时 + _videoController! + ..play() + ..addListener(_onTimeUpdate); + _startFaceTimer(); + } + }); + } + + Future _navigateFaceIfNeeded(FutureOr Function() onPass) async { + if (_info?['ISFACE'] == '1') { + final passed = await pushPage( + FaceRecognitionPage( + studentId: widget.studentId, + mode: FaceMode.auto, + ), + context, + ); + if (passed == true) { + await onPass(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('人脸验证未通过,无法继续')), + ); + } + } else { + await onPass(); + } + } + + Future _getVideoPlayInfo(String vidId) async { + final res = await ApiService.fnGetVideoPlayInfo(vidId); + final url = res['videoList']?[0]?['playURL'] ?? ''; + final prog = await ApiService.fnGetVideoPlayProgress( + vidId, + _currentVideoData!['CURRICULUM_ID'], + _classId, + widget.studentId, + ); + final seen = prog['pd']?['RESOURCETIME'] ?? 0; + _videoController?.removeListener(_onTimeUpdate); + _videoController?.dispose(); + _videoController = VideoPlayerController.networkUrl(Uri.parse(url)) + ..initialize().then((_) { + setState(() { + _videoController! + ..seekTo(Duration(seconds: seen)) + ..play() + ..addListener(_onTimeUpdate); + }); + }); + } + + void _onTimeUpdate() { + if (_videoController == null || !_videoController!.value.isPlaying) return; + final curr = _videoController!.value.position; + if (!_throttleFlag && (curr - _lastReported).inSeconds >= 5) { + _throttleFlag = true; + _lastReported = curr; + _submitPlayTime(end: false, seconds: curr.inSeconds) + .whenComplete(() => Future.delayed(const Duration(seconds: 1), () { + _throttleFlag = false; + })); + } + } + + Future _submitPlayTime({ + required bool end, + required int seconds, + }) async { + if (_currentVideoData == null) return; + final pd = (await ApiService.fnSubmitPlayTime( + _currentVideoData!['VIDEOCOURSEWARE_ID'], + _currentVideoData!['CURRICULUM_ID'], + end ? '1' : '0', + seconds, + _currentVideoData!['CHAPTER_ID'], + widget.studentId, + _classCurriculumId, + _classId, + ))['pd']!; + // 更新列表 percent + final comp = pd['PLAYCOUNT'] != null && pd['PLAYCOUNT'] > 0; + final resT = pd['RESOURCETIME'] ?? seconds; + final pct = comp + ? 100 + : (resT / (_currentVideoData!['VIDEOTIME'] ?? 1) * 100).clamp(0, 100); + final str = '${pct.floor()}%'; + setState(() { + if (_hasNodes) { + _videoList[_currentFirstIndex]['nodes'][_currentNodeIndex] + ['percent'] = str; + } else { + _videoList[_currentFirstIndex]['percent'] = str; + } + }); + + // 结束且可考试 + if (end && pd['CANEXAM'] == '1') { + _videoController?.pause(); + final ok = await showDialog( + context: context, + builder: (_) => CustomAlertDialog( + title: '提示', + content: '当前任务内所有课程均已学完,是否直接参加考试?', + confirmText: '是', + cancelText: '否', + ), + ) ?? + false; + if (ok) { + Navigator.of(context).pushReplacementNamed( + '/course_exam', + arguments: { + 'STAGEEXAMPAPERINPUT_ID': pd['paper']['STAGEEXAMPAPERINPUT_ID'], + 'CLASS_ID': _classId, + 'STUDENT_ID': widget.studentId, + 'NUMBEROFEXAMS': pd['NUMBEROFEXAMS'], + }, + ); + } else { + _videoController?.play(); + } + } + } + + void _startFaceTimer() { + _faceTimer = Timer.periodic(Duration(seconds: _faceTime), (_) { + _videoController?.pause(); + _showFaceAuthOnce(); + }); + } + + Future _showFaceAuthOnce() async { + final passed = await pushPage( + FaceRecognitionPage( + studentId: widget.studentId, + mode: FaceMode.auto, + ), + context, + ); + if (passed == true) { + _videoController?.play(); + _faceTimer?.cancel(); + _startFaceTimer(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('人脸验证未通过,视频已暂停')), + ); + } + } + + String secondsCount(dynamic sec) { + final s = int.tryParse('$sec') ?? 0; + final h = s ~/ 3600; + final m = (s % 3600) ~/ 60; + final ss = s % 60; + return '${h.toString().padLeft(2, '0')}:' + '${m.toString().padLeft(2, '0')}:' + '${ss.toString().padLeft(2, '0')}'; + } + @override Widget build(BuildContext context) { + if (_loading) { + return Scaffold( + appBar: MyAppbar(title: '学习详情'), + body: const Center(child: CircularProgressIndicator()), + ); + } + final info = _info!; return Scaffold( - appBar: MyAppbar(title: "学习详情"), - body: Column(children: []), + appBar: MyAppbar(title: '学习详情'), + body: SafeArea( + child: Column( + children: [ + SizedBox( + height: 250, + child: _videoController != null && + _videoController!.value.isInitialized + ? AspectRatio( + aspectRatio: _videoController!.value.aspectRatio, + child: VideoPlayer(_videoController!), + ) + : Image.network( + ApiService.baseImgPath + (info['COVERPATH'] ?? ''), + fit: BoxFit.cover, + width: double.infinity, + ), + ), + Container( + width: double.infinity, + color: Colors.white, + padding: const EdgeInsets.all(10.0), + child: Text( + info['CURRICULUMNAME'] ?? '', + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 10), + Container( + color: Colors.white, + child: TabBar( + indicatorColor: Colors.blue, + labelStyle: + const TextStyle(fontSize: 16, color: Colors.black87), + controller: _tabController, + tabs: const [Tab(text: '课件目录'), Tab(text: '详情')], + ), + ), + const SizedBox(height: 10), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildVideoList(), + _buildDetailView(info), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildVideoList() { + return ListView.builder( + itemCount: _videoList.length, + itemBuilder: (ctx, idx) { + final item = _videoList[idx] as Map; + final nodes = item['nodes'] as List?; + if (nodes != null && nodes.isNotEmpty) { + return ExpansionTile( + title: Text(item['NAME'] ?? ''), + children: nodes + .asMap() + .entries + .map((e) => _buildVideoItem( + e.value as Map, + true, + idx, + e.key, + )) + .toList(), + ); + } + return _buildVideoItem(item, false, idx, 0); + }, + ); + } + + Widget _buildVideoItem( + Map m, bool hasNodes, int fi, int ni) { + return Container( + color: Colors.white, + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.file_copy_rounded, + color: Colors.grey, size: 20), + Expanded( + child: Text(m['NAME'] ?? '', + style: const TextStyle(fontSize: 14))), + ], + ), + const SizedBox(height: 10), + GestureDetector( + onTap: () => _onVideoTap(m, hasNodes, fi, ni), + child: Container( + height: 80, + padding: + const EdgeInsets.symmetric(vertical: 10, horizontal: 8), + decoration: BoxDecoration( + color: const Color(0xFFFAFAFA), + borderRadius: BorderRadius.circular(5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text(m['COURSEWARENAME'] ?? '', + style: const TextStyle(fontSize: 14)), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("进度:${m['percent']}", + style: const TextStyle(color: Colors.blue)), + if (m['IS_VIDEO'] == 0) ...[ + Text(secondsCount(m['VIDEOTIME'])), + const Icon(Icons.play_circle, color: Colors.blue), + ], + CustomButton( + onPressed: () => + pushPage(StudyPractisePage(m['VIDEOCOURSEWARE_ID']), context), + text: "课后练习", + backgroundColor: Colors.blue, + height: 30, + textStyle: + const TextStyle(fontSize: 12, color: Colors.white), + padding: const EdgeInsets.symmetric( + vertical: 2, horizontal: 12), + borderRadius: 15, + ), + ], + ), + ], + ), + ), + ) + ], + ), + ); + } + + Widget _buildDetailView(Map info) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(info['CURRICULUMINTRODUCE'] ?? '', + style: const TextStyle(fontSize: 16)), + const SizedBox(height: 16), + if (info['COVERPATH'] != null) + Image.network(ApiService.baseImgPath + info['COVERPATH']), + ], + ), ); } } diff --git a/lib/pages/home/study/study_my_task_page.dart b/lib/pages/home/study/study_my_task_page.dart index 984001d..18831ec 100644 --- a/lib/pages/home/study/study_my_task_page.dart +++ b/lib/pages/home/study/study_my_task_page.dart @@ -1,9 +1,12 @@ import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:qhd_prevention/customWidget/custom_button.dart'; import 'package:qhd_prevention/pages/home/study/study_class_list_page.dart'; import 'package:qhd_prevention/tools/tools.dart'; import '../../../http/ApiService.dart'; +import '../../mine/mine_sign_page.dart'; import '../../my_appbar.dart'; class StudyMyTaskPage extends StatefulWidget { @@ -15,17 +18,19 @@ class StudyMyTaskPage extends StatefulWidget { class _StudyMyTaskPageState extends State { int _page = 1; + final int _showCount = 10; bool _isLoading = false; bool _hasMore = true; + int _totalPage = 1; List _list = []; Timer? _timer; + late DateTime _now; // 用于“立即学习”的起始时间判断 @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _getStudyList(); - }); + _now = DateTime.now(); + WidgetsBinding.instance.addPostFrameCallback((_) => _getStudyList()); _startCountdownTimer(); } @@ -35,16 +40,13 @@ class _StudyMyTaskPageState extends State { super.dispose(); } + /// 每秒减少 remainingSeconds,用于倒计时显示 void _startCountdownTimer() { _timer = Timer.periodic(Duration(seconds: 1), (_) { setState(() { for (var item in _list) { - if (item['remainingSeconds'] != null && - item['remainingSeconds'] > 0) { - item['remainingSeconds']--; - } else { - item['remainingSeconds'] = 0; - } + final rs = item['remainingSeconds'] as int? ?? 0; + item['remainingSeconds'] = rs > 0 ? rs - 1 : 0; } }); }); @@ -56,18 +58,20 @@ class _StudyMyTaskPageState extends State { if (!loadMore) LoadingDialogHelper.show(context); try { - final result = await ApiService.getStudyList(_page); - if (result['result'] == 'success') { - final List newList = result['varList'] ?? []; + final res = await ApiService.getStudyList(_page); + if (res['result'] == 'success') { + final List varList = res['varList'] ?? []; + _totalPage = res['totalPage'] ?? 1; - final totalResult = result['page']['totalResult'] ?? 10; + // 计算初始倒计时 final now = DateTime.now(); - for (var item in newList) { - final endTimeStr = item['END_TIME'] ?? ''; + for (var item in varList) { + final endStr = + (item['END_TIME'] as String?)?.replaceAll('-', '/') ?? ''; try { - final endTime = DateTime.parse(endTimeStr); - final seconds = endTime.difference(now).inSeconds; - item['remainingSeconds'] = seconds > 0 ? seconds : 0; + final end = DateTime.parse(endStr); + final diff = end.difference(now).inSeconds; + item['remainingSeconds'] = diff > 0 ? diff : 0; } catch (_) { item['remainingSeconds'] = 0; } @@ -75,16 +79,16 @@ class _StudyMyTaskPageState extends State { setState(() { if (loadMore) { - _list.addAll(newList); + _list.addAll(varList); } else { - _list = newList; + _list = varList; } - _hasMore = _list.length <= totalResult; + _hasMore = _page < _totalPage; if (_hasMore) _page++; }); } } catch (e) { - print('加载出错: $e'); + debugPrint('加载出错: $e'); } finally { if (!loadMore) LoadingDialogHelper.hide(context); _isLoading = false; @@ -94,163 +98,236 @@ class _StudyMyTaskPageState extends State { String formatSeconds(int seconds) { if (seconds <= 0) return '00天 00:00:00'; final d = Duration(seconds: seconds); - final days = d.inDays.toString().padLeft(2, '0'); - final hours = (d.inHours % 24).toString().padLeft(2, '0'); - final minutes = (d.inMinutes % 60).toString().padLeft(2, '0'); - final secs = (d.inSeconds % 60).toString().padLeft(2, '0'); - return "$days天 $hours:$minutes:$secs"; + return '${d.inDays.toString().padLeft(2, '0')}天 ' + '${(d.inHours % 24).toString().padLeft(2, '0')}:' + '${(d.inMinutes % 60).toString().padLeft(2, '0')}:' + '${(d.inSeconds % 60).toString().padLeft(2, '0')}'; + } + + Color _stateColor(String code) { + switch (code) { + case '1': + return Colors.blue; + case '2': + return Colors.green; + default: + return Colors.grey; + } + } + + String _stateText(String code) { + switch (code) { + case '0': + return '未学习'; + case '1': + return '学习中'; + case '2': + return '已学完'; + case '3': + return '已完成'; + case '4': + return '未完成'; + case '5': + return '待评估'; + case '6': + return '评估未合格'; + default: + return '未知'; + } + } + + void _onTapSign(Map item) async { + final String? imagePath = await Navigator.push( + context, + MaterialPageRoute(builder: (_) => MineSignPage()), + ); + + // 用户有签名并取到了 imagePath + if (imagePath != null && imagePath.isNotEmpty) { + // 上传 + await _uploadSignAndNavigate(item, imagePath); + } + } + + Future _uploadSignAndNavigate(Map item, String imagePath) async { + try { + final File file = File(imagePath); + final List bytes = await file.readAsBytes(); + final String signBase64 = base64Encode(bytes); + + final result = await ApiService.signUpdate( + signBase64, + item['CLASS_ID'], + item['STAGESTUDENTRELATION_ID'], + ); + + if (result['result'] == 'success') { + pushPage(StudyClassListPage(item), context); + } + } catch (e) { + // 捕获并提示错误 + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('上传出错:$e'))); + } } Widget _buildItem(Map item) { - Color stateColor(String code) { - switch (code) { - case '1': - return Colors.blue; - case '2': - return Colors.green; - default: - return Colors.grey; - } - } + final now = _now; + final start = DateTime.tryParse(item['START_TIME'] ?? ''); + final end = DateTime.tryParse(item['END_TIME'] ?? ''); + final nowOk = + start != null && end != null + ? now.isAfter(start) && now.isBefore(end) + : false; - String stateText(String code) { - switch (code) { - case '0': - return '未学习'; - case '1': - return '学习中'; - case '2': - return '已学完'; - case '3': - return '已完成'; - case '4': - return '未完成'; - case '5': - return '待评估'; - case '6': - return '评估未合格'; - default: - return '未知'; - } - } + final int studyState = int.tryParse(item['STUDYSTATE'] ?? '') ?? 0; + final int stageExamState = int.tryParse(item['STAGEEXAMSTATE'] ?? '') ?? 0; + final int strengthenExamState = + int.tryParse(item['STRENGTHENEXAMSTATE'] ?? '') ?? 0; + final int numberOfExams = int.tryParse('${item['NUMBEROFEXAMS']}') ?? 0; + final int ksCount = int.tryParse('${item['ksCount']}') ?? 0; + final int examinationFlag = + item['EXAMINATION'] is int + ? item['EXAMINATION'] as int + : int.tryParse('${item['EXAMINATION']}') ?? 0; + final String isStrengthen = item['ISSTRENGTHEN'] ?? '0'; - final startTime = DateTime.tryParse(item['START_TIME'] ?? ''); - final endTime = DateTime.tryParse(item['END_TIME'] ?? ''); - final now = DateTime.now(); return Container( margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.grey.shade200, - blurRadius: 5, - offset: Offset(0, 2), - ), - ], + boxShadow: [BoxShadow(color: Colors.grey.shade200, blurRadius: 5)], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 顶部:名称 + 状态 Row( - spacing: 15, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( - '培训任务名称: ${item['NAME'] ?? ''}', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + '培训任务名称: ${item['NAME']}', + style: const TextStyle(fontWeight: FontWeight.bold), ), ), Text( - stateText(item['STUDYSTATE'] ?? ''), - style: TextStyle( - color: stateColor(item['STUDYSTATE'] ?? ''), - fontSize: 14, - ), + _stateText(item['STUDYSTATE']), + style: TextStyle(color: _stateColor(item['STUDYSTATE'])), ), ], ), - Divider(height: 15), - Text( - '岗位类型:${item['POSTTYPE_NAME'] ?? ''}', - style: TextStyle(fontSize: 14), - ), - Text( - '培训时间:${item['START_TIME'] ?? ''} 至 ${item['END_TIME'] ?? ''}', - style: TextStyle(fontSize: 14), - ), - Divider(height: 15), + + const Divider(), + + // 中部:岗位类型 + 培训时间 + Text('岗位类型:${item['POSTTYPE_NAME']}'), + Text('培训时间:${item['START_TIME']} 至 ${item['END_TIME']}'), + + const Divider(), + + // 底部:倒计时 + 按钮组 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - Icon(Icons.access_time, size: 18, color: Colors.grey), - SizedBox(width: 5), + const Icon(Icons.access_time, size: 18, color: Colors.grey), + const SizedBox(width: 4), Text( formatSeconds(item['remainingSeconds'] ?? 0), - style: TextStyle(fontSize: 12,), + style: const TextStyle(fontSize: 12), ), ], ), Wrap( spacing: 8, children: [ - if ((item['STUDYSTATE'] ?? '') == '2' && - (item['STAGEEXAMSTATE'] ?? '') == '3') + // 考试详情 + if (stageExamState == 3) CustomButton( - onPressed: () {}, - text: "考试详情", - backgroundColor: Colors.blue, - borderRadius: 18, height: 36, - padding: EdgeInsets.symmetric(horizontal: 20), - ), - if ((item['STUDYSTATE'] ?? '') == '2' && - int.tryParse(item['STAGEEXAMSTATE'] ?? '0')! >= 2 && - (item['ISSTRENGTHEN'] == '1' || - item['ISSTRENGTHEN'] == '2') && - item['STRENGTHENEXAMSTATE'] == '0') - CustomButton( - onPressed: () {}, text: "考试详情", - backgroundColor: Colors.blue, - borderRadius: 18, - height: 36, padding: EdgeInsets.symmetric(horizontal: 20), + borderRadius: 18, + backgroundColor: Colors.blue, + onPressed: + () => Navigator.pushNamed( + context, + '/exam_details', + arguments: {'STUDENT_ID': item['STUDENT_ID']}, + ), ), - if ((int.tryParse(item['STUDYSTATE'] ?? '0') ?? 0) <= 1 && - item['STATE'] == '5') - if (startTime != null && - endTime != null && - now.isAfter(startTime) && - now.isBefore(endTime)) - CustomButton( - onPressed: () { - pushPage(StudyClassListPage(item['CLASS_ID'] ?? '', item['POST_ID'] ?? ''), context); - }, - text: "立即学习", - backgroundColor: Colors.blue, - borderRadius: 18, - height: 36, - padding: EdgeInsets.symmetric(horizontal: 20), - ), - if ((item['STUDYSTATE'] ?? '') == '2' && - item['STATE'] != '6' && - item['EXAMINATION'] == 1 && - item['STAGEEXAMSTATE'] == '1' && - (item['ksCount'] ?? 0) < (item['NUMBEROFEXAMS'] ?? 1)) + // 加强学习 + if (studyState >= 2 && + stageExamState >= 2 && + (isStrengthen == '1' || isStrengthen == '2') && + strengthenExamState == 0) CustomButton( - onPressed: () {}, - text: "立即考试", - backgroundColor: Colors.green, - borderRadius: 18, height: 36, + text: "加强学习", padding: EdgeInsets.symmetric(horizontal: 20), + borderRadius: 18, + backgroundColor: Colors.blue, + onPressed: + () => Navigator.pushNamed( + context, + '/strengthen_video_study', + arguments: { + 'CLASS_ID': item['CLASS_ID'], + 'POST_ID': item['POST_ID'], + 'STUDENT_ID': item['STUDENT_ID'], + }, + ), + ), + + // 立即学习 + if (studyState <= 1 && item['STATE'] == '5' && nowOk) + CustomButton( + height: 36, + text: "立即学习", + padding: EdgeInsets.symmetric(horizontal: 20), + borderRadius: 18, + backgroundColor: Colors.blue, + onPressed: () { + if (studyState == 0) { + _onTapSign(item); + } else { + pushPage(StudyClassListPage(item), context); + } + }, + ), + + // 立即考试 + if (studyState == 2 && + item['STATE'] != '6' && + examinationFlag == 1 && + stageExamState == 1 && + ksCount < numberOfExams) + CustomButton( + height: 36, + text: "立即考试", + padding: EdgeInsets.symmetric(horizontal: 20), + borderRadius: 18, + backgroundColor: Colors.green, + onPressed: + () => Navigator.pushNamed( + context, + '/course_exam', + arguments: { + 'STAGEEXAMPAPERINPUT_ID': + item['STAGEEXAMPAPERINPUT_ID'], + 'CLASS_ID': item['CLASS_ID'], + 'POST_ID': item['POST_ID'], + 'STUDENT_ID': item['STUDENT_ID'], + 'NUMBEROFEXAMS': numberOfExams, + 'entrySite': 'list', + }, + ), ), ], ), @@ -261,33 +338,37 @@ class _StudyMyTaskPageState extends State { ); } - void _loadMoreIfNeeded(ScrollNotification notification) { - if (notification.metrics.pixels >= - notification.metrics.maxScrollExtent - 100) { - if (_hasMore && !_isLoading) { - _getStudyList(loadMore: true); - } + bool _onScroll(ScrollNotification n) { + if (n.metrics.pixels > n.metrics.maxScrollExtent - 100 && + _hasMore && + !_isLoading) { + _getStudyList(loadMore: true); } + return false; } @override Widget build(BuildContext context) { return Scaffold( - appBar: MyAppbar(title: '学习园地'), + appBar: MyAppbar(title: '我的学习'), body: SafeArea( child: NotificationListener( - onNotification: (notification) { - _loadMoreIfNeeded(notification); - return false; - }, + onNotification: _onScroll, child: _list.isEmpty - ? Center(child: Text('暂无数据')) + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset('assets/images/null.png', width: 150), + SizedBox(height: 12), + Text('暂无数据', style: TextStyle(color: Colors.grey)), + ], + ), + ) : ListView.builder( itemCount: _list.length, - itemBuilder: (context, index) { - return _buildItem(_list[index]); - }, + itemBuilder: (_, i) => _buildItem(_list[i]), ), ), ), diff --git a/lib/pages/home/study/study_practise_page.dart b/lib/pages/home/study/study_practise_page.dart new file mode 100644 index 0000000..c0511c9 --- /dev/null +++ b/lib/pages/home/study/study_practise_page.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; + +class StudyPractisePage extends StatefulWidget { + const StudyPractisePage(this.VIDEOCOURSEWARE_ID,{super.key}); + final String VIDEOCOURSEWARE_ID; + @override + State createState() => _StudyPractisePageState(); +} + +class _StudyPractisePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: MyAppbar(title: "课后练习"), + body: SizedBox(), + ); + } +} diff --git a/lib/pages/home/work/dangerTypeItems/wait/danner_repair.dart b/lib/pages/home/work/dangerTypeItems/wait/danner_repair.dart index 57d1c6c..b23251d 100644 --- a/lib/pages/home/work/dangerTypeItems/wait/danner_repair.dart +++ b/lib/pages/home/work/dangerTypeItems/wait/danner_repair.dart @@ -31,7 +31,7 @@ class _DannerRepairState extends State { final _workController = TextEditingController(); final _otherController = TextEditingController(); - var _selectData = DateTime.now(); + late var _selectData = DateTime.now(); @override void dispose() { @@ -90,7 +90,6 @@ class _DannerRepairState extends State { initialDate: DateTime.now(), onCancel: () => Navigator.of(context).pop(), onConfirm: (selected) { - print('选中日期: $selected'); Navigator.of(context).pop(); setState(() { _selectData = selected; diff --git a/lib/pages/mine/mine_set_page.dart b/lib/pages/mine/mine_set_page.dart index 79d1ca1..e19b227 100644 --- a/lib/pages/mine/mine_set_page.dart +++ b/lib/pages/mine/mine_set_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:qhd_prevention/pages/home/study/face_ecognition_page.dart'; import 'package:qhd_prevention/pages/login_page.dart'; import 'package:qhd_prevention/pages/mine/mine_first_sign_page.dart'; import 'package:qhd_prevention/pages/mine/mine_set_pwd_page.dart'; @@ -30,6 +31,13 @@ class MineSetPage extends StatelessWidget { pushPage(MineSetPwdPage(), context); }, ), + Divider(height: 1, color: Colors.black12), + GestureDetector( + child: _setItemWidget("更新人脸信息"), + onTap: () { + pushPage(FaceRecognitionPage(studentId: '', mode: FaceMode.manual,), context); + }, + ), Divider(height: 1, color: Colors.black12), GestureDetector( diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 9d3c986..1c3df1b 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -1,6 +1,4 @@ -import 'dart:ui'; import 'dart:math'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -14,10 +12,12 @@ double screenWidth(BuildContext context) { return screenWidth; } -void pushPage(Widget page, BuildContext context) { - Navigator.push(context, MaterialPageRoute(builder: (context) => page)); +Future pushPage(Widget page, BuildContext context) { + return Navigator.push( + context, + MaterialPageRoute(builder: (_) => page), + ); } - void present(Widget page, BuildContext context) { Navigator.push( context, @@ -266,3 +266,30 @@ class LoadingDialogHelper { } } } +/// 将秒数转换为 “HH:MM:SS” 格式 +String secondsCount(dynamic seconds) { + // 先尝试解析出一个 double 值 + double totalSeconds; + if (seconds == null) { + totalSeconds = 0; + } else if (seconds is num) { + totalSeconds = seconds.toDouble(); + } else { + // seconds 是字符串或其他,尝试 parse + totalSeconds = double.tryParse(seconds.toString()) ?? 0.0; + } + + // 取整秒,向下取整 + final int secs = totalSeconds.floor(); + + final int h = (secs ~/ 3600) % 24; + final int m = (secs ~/ 60) % 60; + final int s = secs % 60; + + // padLeft 保证两位数 + final String hh = h.toString().padLeft(2, '0'); + final String mm = m.toString().padLeft(2, '0'); + final String ss = s.toString().padLeft(2, '0'); + + return '$hh:$mm:$ss'; +} diff --git a/pubspec.lock b/pubspec.lock index 2ba0c65..9325767 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -14,7 +14,7 @@ packages: description: name: asn1lib sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.6.5" async: @@ -33,6 +33,46 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.2" + camera: + dependency: "direct main" + description: + name: camera + sha256: d6ec2cbdbe2fa8f5e0d07d8c06368fe4effa985a4a5ddade9cc58a8cd849557d + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.11.2" + camera_android_camerax: + dependency: transitive + description: + name: camera_android_camerax + sha256: "4b6c1bef4270c39df96402c4d62f2348c3bb2bbaefd0883b9dbd58f426306ad0" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.6.19" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "9e02b36c9c09a01edcb0f2bfc58a94ed38bbbf37907759d651707bb0f327a365" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.9.20+3" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.10.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.3.5" characters: dependency: transitive description: @@ -78,7 +118,7 @@ packages: description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.1.2" cross_file: @@ -126,7 +166,7 @@ packages: description: name: dio sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.8.0+1" dio_web_adapter: @@ -134,7 +174,7 @@ packages: description: name: dio_web_adapter sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.1" encrypt: @@ -142,7 +182,7 @@ packages: description: name: encrypt sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.0.3" extended_image: @@ -253,7 +293,7 @@ packages: description: name: fluttertoast sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "8.2.12" html: @@ -581,7 +621,7 @@ packages: description: name: pointycastle sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.9.1" provider: @@ -685,6 +725,14 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 594e1c2..3401787 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,12 +60,9 @@ dependencies: #网页页面加载 webview_flutter: ^4.4.0 path_provider: ^2.0.1 + camera: ^0.11.2 - #网页页面加载 - webview_flutter: ^4.4.0 - path_provider: ^2.0.1 - dev_dependencies: flutter_test: sdk: flutter