人脸、学习部分逻辑
parent
668aeeb828
commit
1aeb4610b7
|
@ -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
|
||||
|
||||
|
|
|
@ -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<Map<String, dynamic>> getStudyList(int page) {
|
||||
return HttpManager().request(
|
||||
|
@ -251,6 +260,133 @@ U6Hzm1ninpWeE+awIDAQAB
|
|||
},
|
||||
);
|
||||
}
|
||||
/// 学习详情视频列表
|
||||
static Future<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Map<String, dynamic>> 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<Map<String, dynamic>> uploadFaceImage({
|
||||
required String baseUrl,
|
||||
required String path,
|
||||
required Map<String, dynamic> 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<String, dynamic>
|
||||
? resp.data as Map<String, dynamic>
|
||||
: <String, dynamic>{};
|
||||
|
||||
return json;
|
||||
} on DioException catch (e) {
|
||||
throw ApiException('network_error', e.message ?? e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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<FaceRecognitionPage> {
|
||||
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<void> _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<void> _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<void> _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;
|
||||
}
|
|
@ -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<StudyClassListPage> createState() => _StudyClassListPageState();
|
||||
}
|
||||
|
||||
class _StudyClassListPageState extends State<StudyClassListPage> {
|
||||
late List<dynamic> _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<StudyClassListPage> {
|
|||
Future<void> _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<dynamic> newList = result['varList'] ?? [];
|
||||
setState(() {
|
||||
|
@ -46,8 +49,16 @@ class _StudyClassListPageState extends State<StudyClassListPage> {
|
|||
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<StudyClassListPage> {
|
|||
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),
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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<StudyDetailPage> createState() => _StudyDetailPageState();
|
||||
}
|
||||
|
||||
class _StudyDetailPageState extends State<StudyDetailPage> {
|
||||
class _StudyDetailPageState extends State<StudyDetailPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
VideoPlayerController? _videoController;
|
||||
|
||||
Map<String, dynamic>? _info;
|
||||
List<dynamic> _videoList = [];
|
||||
bool _loading = true;
|
||||
|
||||
Timer? _faceTimer;
|
||||
bool _throttleFlag = false;
|
||||
late int _faceTime;
|
||||
late String _classId;
|
||||
late String _classCurriculumId;
|
||||
|
||||
Map<String, dynamic>? _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<void> _showFaceIntro() async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (_) => CustomAlertDialog(
|
||||
title: '温馨提示',
|
||||
content:
|
||||
'重要提醒:尊敬的用户,根据规定我们会在您学习过程中多次进行人脸识别认证,为了保护您的隐私请您在摄像设备视野内确保衣冠整齐。',
|
||||
cancelText: '取消',
|
||||
confirmText: '同意并继续',
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
onConfirm: () => {},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _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<void> _onVideoTap(
|
||||
Map<String, dynamic> 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<void> _navigateFaceIfNeeded(FutureOr<void> Function() onPass) async {
|
||||
if (_info?['ISFACE'] == '1') {
|
||||
final passed = await pushPage<bool>(
|
||||
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<void> _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<void> _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<bool>(
|
||||
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<void> _showFaceAuthOnce() async {
|
||||
final passed = await pushPage<bool>(
|
||||
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<String, dynamic>;
|
||||
final nodes = item['nodes'] as List<dynamic>?;
|
||||
if (nodes != null && nodes.isNotEmpty) {
|
||||
return ExpansionTile(
|
||||
title: Text(item['NAME'] ?? ''),
|
||||
children: nodes
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => _buildVideoItem(
|
||||
e.value as Map<String, dynamic>,
|
||||
true,
|
||||
idx,
|
||||
e.key,
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
return _buildVideoItem(item, false, idx, 0);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVideoItem(
|
||||
Map<String, dynamic> 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']),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<StudyMyTaskPage> {
|
||||
int _page = 1;
|
||||
final int _showCount = 10;
|
||||
bool _isLoading = false;
|
||||
bool _hasMore = true;
|
||||
int _totalPage = 1;
|
||||
List<dynamic> _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<StudyMyTaskPage> {
|
|||
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<StudyMyTaskPage> {
|
|||
if (!loadMore) LoadingDialogHelper.show(context);
|
||||
|
||||
try {
|
||||
final result = await ApiService.getStudyList(_page);
|
||||
if (result['result'] == 'success') {
|
||||
final List<dynamic> newList = result['varList'] ?? [];
|
||||
final res = await ApiService.getStudyList(_page);
|
||||
if (res['result'] == 'success') {
|
||||
final List<dynamic> 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<StudyMyTaskPage> {
|
|||
|
||||
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<StudyMyTaskPage> {
|
|||
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<String>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => MineSignPage()),
|
||||
);
|
||||
|
||||
// 用户有签名并取到了 imagePath
|
||||
if (imagePath != null && imagePath.isNotEmpty) {
|
||||
// 上传
|
||||
await _uploadSignAndNavigate(item, imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _uploadSignAndNavigate(Map item, String imagePath) async {
|
||||
try {
|
||||
final File file = File(imagePath);
|
||||
final List<int> 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<StudyMyTaskPage> {
|
|||
);
|
||||
}
|
||||
|
||||
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<ScrollNotification>(
|
||||
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]),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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<StudyPractisePage> createState() => _StudyPractisePageState();
|
||||
}
|
||||
|
||||
class _StudyPractisePageState extends State<StudyPractisePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: MyAppbar(title: "课后练习"),
|
||||
body: SizedBox(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@ class _DannerRepairState extends State<DannerRepair> {
|
|||
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<DannerRepair> {
|
|||
initialDate: DateTime.now(),
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
onConfirm: (selected) {
|
||||
print('选中日期: $selected');
|
||||
Navigator.of(context).pop();
|
||||
setState(() {
|
||||
_selectData = selected;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<T?> pushPage<T>(Widget page, BuildContext context) {
|
||||
return Navigator.push<T>(
|
||||
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';
|
||||
}
|
||||
|
|
62
pubspec.lock
62
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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue