人脸、学习部分逻辑

main
hs 2025-07-17 16:10:46 +08:00
parent 668aeeb828
commit 1aeb4610b7
14 changed files with 1315 additions and 250 deletions

View File

@ -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

View File

@ -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
),
}
);
}
}

View File

@ -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);
}
// msgUSER_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());
}
}
}

View File

@ -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});

View File

@ -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;
}

View File

@ -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),
],
),
);

View File

@ -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']),
],
),
);
}
}

View File

@ -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]),
),
),
),

View File

@ -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(),
);
}
}

View File

@ -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;

View File

@ -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(

View File

@ -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';
}

View File

@ -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:

View File

@ -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