学习园地模块完成

main
hs 2025-07-22 13:34:34 +08:00
parent 74891e0384
commit 942da64c4a
24 changed files with 2472 additions and 485 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -16,6 +16,8 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- pdfx (1.0.0):
- Flutter
- photo_manager (3.7.1):
- Flutter
- FlutterMacOS
@ -38,6 +40,7 @@ DEPENDENCIES:
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- pdfx (from `.symlinks/plugins/pdfx/ios`)
- 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`)
@ -60,6 +63,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
pdfx:
:path: ".symlinks/plugins/pdfx/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/ios"
shared_preferences_foundation:
@ -78,6 +83,7 @@ SPEC CHECKSUMS:
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
pdfx: 77f4dddc48361fbb01486fa2bdee4532cbb97ef3
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b

View File

@ -20,6 +20,8 @@ class CustomAlertDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bool hasCancel = cancelText.trim().isNotEmpty;
return Dialog(
backgroundColor: Colors.transparent,
child: Container(
@ -27,76 +29,107 @@ class CustomAlertDialog extends StatelessWidget {
color: Colors.white,
borderRadius: BorderRadius.circular(5),
),
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
padding: EdgeInsets.zero,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 20),
const SizedBox(height: 20),
Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Padding(
padding: EdgeInsets.symmetric(horizontal: 30),
padding: const EdgeInsets.symmetric(horizontal: 30),
child: Text(
content,
style: const TextStyle(fontSize: 16, color: Colors.black45),
style: const TextStyle(
fontSize: 16,
color: Colors.black45,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 20),
const Divider(height: 1),
Row(
children: [
Expanded(
child: InkWell(
onTap: () {
Navigator.of(context).pop();
onCancel?.call();
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
alignment: Alignment.center,
child: Text(
cancelText,
style: const TextStyle(
fontWeight: FontWeight.w500,
color: Colors.black,
fontSize: 18,
),
),
),
),
),
Container(width: 1, height: 48, color: Colors.grey[300]),
Expanded(
child: InkWell(
onTap: () {
Navigator.of(context).pop();
onConfirm?.call();
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
alignment: Alignment.center,
child: Text(
confirmText,
style: const TextStyle(
color: Color(0xFF3874F6),
fontWeight: FontWeight.w500,
fontSize: 18,
),
),
),
),
),
],
),
hasCancel ? _buildDoubleButtons(context) : _buildSingleButton(context),
],
),
),
);
}
Widget _buildDoubleButtons(BuildContext context) {
return Row(
children: [
Expanded(
child: InkWell(
onTap: () {
Navigator.of(context).pop();
onCancel?.call();
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
alignment: Alignment.center,
child: Text(
cancelText,
style: const TextStyle(
fontWeight: FontWeight.w500,
color: Colors.black,
fontSize: 18,
),
),
),
),
),
Container(width: 1, height: 48, color: Colors.grey[300]),
Expanded(
child: InkWell(
onTap: () {
Navigator.of(context).pop();
onConfirm?.call();
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
alignment: Alignment.center,
child: Text(
confirmText,
style: const TextStyle(
color: Color(0xFF3874F6), //
fontWeight: FontWeight.w500,
fontSize: 18,
),
),
),
),
),
],
);
}
Widget _buildSingleButton(BuildContext context) {
return InkWell(
onTap: () {
Navigator.of(context).pop();
onConfirm?.call();
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 14),
alignment: Alignment.center,
child: Text(
confirmText,
style: const TextStyle(
color: Color(0xFF3874F6), //
fontWeight: FontWeight.w500,
fontSize: 18,
),
),
),
);
}
}

View File

@ -30,7 +30,7 @@ class CustomButton extends StatelessWidget {
child: Container(
height: height ?? 50, // 50
padding: padding ?? const EdgeInsets.all(8), //
margin: margin ?? const EdgeInsets.symmetric(horizontal: 8), //
margin: margin ?? const EdgeInsets.symmetric(horizontal: 5), //
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(borderRadius),
color: backgroundColor,

View File

@ -0,0 +1,143 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../http/ApiService.dart';
import '../../tools/tools.dart';
class HiddenRollWidget extends StatefulWidget {
///
final List<Map<String, dynamic>> hiddenList;
///
final double rowHeight;
///
final int visibleCount;
///
final Duration interval;
/// HIDDEN_ID
final ValueChanged<String>? onItemTap;
const HiddenRollWidget({
Key? key,
required this.hiddenList,
this.rowHeight = 35,
this.visibleCount = 5,
this.interval = const Duration(seconds: 3),
this.onItemTap,
}) : super(key: key);
@override
_HiddenRollWidgetState createState() => _HiddenRollWidgetState();
}
class _HiddenRollWidgetState extends State<HiddenRollWidget> {
late final ScrollController _ctrl;
late final Timer _timer;
int _currentIndex = 0;
@override
void initState() {
super.initState();
_ctrl = ScrollController();
_timer = Timer.periodic(widget.interval, (_) => _scrollToNext());
}
void _scrollToNext() {
if (!_ctrl.hasClients || widget.hiddenList.isEmpty) return;
_currentIndex++;
if (_currentIndex >= widget.hiddenList.length) {
_currentIndex = 0;
_ctrl.jumpTo(0);
} else {
_ctrl.animateTo(
widget.rowHeight * _currentIndex,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
}
}
@override
void dispose() {
_timer.cancel();
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// = *
return SizedBox(
height: widget.rowHeight * widget.visibleCount,
child: ListView.builder(
controller: _ctrl,
physics: const NeverScrollableScrollPhysics(),
itemExtent: widget.rowHeight,
itemCount: widget.hiddenList.length,
itemBuilder: (_, idx) {
final item = widget.hiddenList[idx];
//
String rawTime = item['CREATTIME'] ?? '';
DateTime? dt;
if (rawTime.isNotEmpty) {
try {
dt = DateTime.parse(rawTime.replaceAll('-', '/'));
} catch (_) {}
}
// MM-dd HH:mm
String displayTime;
if (dt != null) {
displayTime = DateFormat('MM-dd HH:mm').format(dt);
} else {
final parts = rawTime.split(' ');
if (parts.length >= 2) {
final datePart = parts[0];
final timePart = parts[1];
final mmdd = datePart.length >= 5 ? datePart.substring(5) : datePart;
final hm = timePart.length >= 5 ? timePart.substring(0, 5) : timePart;
displayTime = '$mmdd $hm';
} else {
displayTime = rawTime;
}
}
//
String descr = item['HIDDENDESCR'] ?? '';
final displayDescr = descr.length > 10 ? '${descr.substring(0, 10)}...' : descr;
return InkWell(
onTap: () => widget.onItemTap?.call(item['HIDDEN_ID'] as String),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 3,
child: Text(
displayDescr,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
flex: 2,
child: Text(
item['CREATORNAME'] ?? '',
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
flex: 2,
child: Text(
displayTime,
textAlign: TextAlign.right,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
},
),
);
}
}

View File

@ -0,0 +1,148 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:pdfx/pdfx.dart';
import 'package:path_provider/path_provider.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:dio/dio.dart';
import 'package:qhd_prevention/tools/tools.dart';
class RemoteFilePage extends StatefulWidget {
final String fileUrl;
final int countdownSeconds;
const RemoteFilePage({
Key? key,
required this.fileUrl,
this.countdownSeconds = 3,
}) : super(key: key);
@override
_RemoteFilePageState createState() => _RemoteFilePageState();
}
class _RemoteFilePageState extends State<RemoteFilePage> {
String? _localPath;
bool _isLoading = true;
bool _hasScrolledToBottom = false;
bool _timerFinished = false;
late int _secondsRemaining;
Timer? _countdownTimer;
late PdfControllerPinch _pdfController;
int _totalPages = 0;
@override
void initState() {
super.initState();
_secondsRemaining = widget.countdownSeconds;
_startCountdown();
_downloadAndLoad();
}
Future<void> _downloadAndLoad() async {
try {
final url = widget.fileUrl;
final filename = url.split('/').last;
final dir = await getTemporaryDirectory();
final filePath = '${dir.path}/$filename';
final dio = Dio();
final response = await dio.get<List<int>>(
url,
options: Options(responseType: ResponseType.bytes),
);
final file = File(filePath);
await file.writeAsBytes(response.data!);
// PDF
_pdfController = PdfControllerPinch(
document: PdfDocument.openFile(filePath),
);
setState(() {
_localPath = filePath;
_isLoading = false;
});
} catch (e) {
//
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('文件加载失败: \$e')),
);
}
}
void _startCountdown() {
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
if (_secondsRemaining > 1) {
_secondsRemaining--;
} else {
_secondsRemaining = 0;
_timerFinished = true;
_countdownTimer?.cancel();
}
});
});
}
@override
void dispose() {
_countdownTimer?.cancel();
if (!_isLoading) {
_pdfController.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final isButtonEnabled = _timerFinished && _hasScrolledToBottom;
return Scaffold(
appBar: MyAppbar(title: '资料学习'),
backgroundColor: Colors.white,
body: SafeArea(
child: Column(
children: [
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: PdfViewPinch(
controller: _pdfController,
scrollDirection: Axis.vertical,
onDocumentLoaded: (document) {
setState(() {
_totalPages = document.pagesCount;
});
},
onPageChanged: (page) {
if (page == _totalPages - 1) {
setState(() => _hasScrolledToBottom = true);
}
},
),
),
Padding(
padding: const EdgeInsets.all(16),
child: CustomButton(
backgroundColor: isButtonEnabled ? Colors.blue : Colors.grey,
text: isButtonEnabled
? '我已学习完毕'
: _secondsRemaining == 0 ? '我已学习完毕' : '$_secondsRemaining s我已学习完毕',
onPressed: isButtonEnabled
? () {
// TODO:
Navigator.pop(context);
}
: null,
),
),
],
),
),
);
}
}

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_player/video_player.dart';
@ -38,7 +39,6 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
super.initState();
_startHideTimer();
_startPositionTimer();
if (widget.controller != null) {
widget.controller!.addListener(_controllerListener);
if (widget.controller!.value.isInitialized) {
@ -58,19 +58,16 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
}
void _controllerListener() {
if (mounted) setState(() {});
if (mounted && widget.controller != null) {
_updateControllerValues();
}
if (!mounted) return;
_updateControllerValues();
}
void _updateControllerValues() {
final controller = widget.controller!;
final c = widget.controller!;
setState(() {
_isPlaying = controller.value.isPlaying;
_totalDuration = controller.value.duration;
_currentPosition = controller.value.position;
_isPlaying = c.value.isPlaying;
_totalDuration = c.value.duration;
_currentPosition = c.value.position;
_sliderValue.value = _currentPosition.inMilliseconds.toDouble();
});
}
@ -93,14 +90,16 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
void _startPositionTimer() {
_positionTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
if (mounted && widget.controller != null && widget.controller!.value.isInitialized) {
setState(() {
_currentPosition = widget.controller!.value.position;
_totalDuration = widget.controller!.value.duration;
_isPlaying = widget.controller!.value.isPlaying;
_sliderValue.value = _currentPosition.inMilliseconds.toDouble();
});
}
if (!mounted ||
widget.controller == null ||
!widget.controller!.value.isInitialized) return;
setState(() {
final c = widget.controller!;
_currentPosition = c.value.position;
_totalDuration = c.value.duration;
_isPlaying = c.value.isPlaying;
_sliderValue.value = _currentPosition.inMilliseconds.toDouble();
});
});
}
@ -112,215 +111,198 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
void _togglePlayPause() {
if (widget.controller == null) return;
setState(() {
_isPlaying = !_isPlaying;
});
if (_isPlaying) {
widget.controller!.play();
} else {
widget.controller!.pause();
}
setState(() => _isPlaying = !_isPlaying);
if (_isPlaying) widget.controller!.play();
else widget.controller!.pause();
_startHideTimer();
}
void _enterFullScreen() {
//
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
//
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
Navigator.of(context).push(MaterialPageRoute(
builder: (ctx) => Scaffold(
backgroundColor: Colors.black,
// 使SafeArea
body: SafeArea(
top: false, // 使
bottom: false, // 使
child: Stack(
children: [
//
VideoPlayerWidget(
controller: widget.controller,
coverUrl: widget.coverUrl,
aspectRatio: max(
widget.aspectRatio,
MediaQuery.of(ctx).size.width / MediaQuery.of(ctx).size.height
),
allowSeek: widget.allowSeek,
isFullScreen: true,
Navigator.of(context)
.push(
MaterialPageRoute(
builder: (ctx) => Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
top: false,
bottom: false,
child: VideoPlayerWidget(
controller: widget.controller,
coverUrl: widget.coverUrl,
aspectRatio: max(
widget.aspectRatio,
MediaQuery.of(ctx).size.width / MediaQuery.of(ctx).size.height,
),
// 退SafeArea
SafeArea(
child: Align(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.black38,
shape: BoxShape.circle,
),
child: const Icon(
Icons.arrow_back,
color: Colors.white,
size: 20,
),
),
),
),
),
),
],
allowSeek: widget.allowSeek,
isFullScreen: true,
),
),
),
),
)).then((_) {
//
)
.then((_) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// UI
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
});
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final fullScreenAspectRatio = max(
widget.aspectRatio,
screenSize.width / screenSize.height
);
final screenW = MediaQuery.of(context).size.width;
final containerW = widget.isFullScreen ? double.infinity : screenW;
final containerH = widget.isFullScreen
? double.infinity
: containerW / widget.aspectRatio;
return GestureDetector(
onTap: _toggleControls,
child: Stack(
fit: widget.isFullScreen ? StackFit.expand : StackFit.loose,
children: [
//
if (widget.controller != null && widget.controller!.value.isInitialized)
AspectRatio(
aspectRatio: widget.isFullScreen ? fullScreenAspectRatio : widget.aspectRatio,
child: VideoPlayer(widget.controller!),
)
else
Image.network(
widget.coverUrl,
fit: BoxFit.cover,
width: widget.isFullScreen ? double.infinity : null,
height: widget.isFullScreen ? double.infinity : null,
),
//
if (_visibleControls)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [Colors.black.withOpacity(0.7), Colors.transparent],
return Center(
child: SizedBox(
width: containerW,
height: containerH,
child: GestureDetector(
behavior: HitTestBehavior.translucent, //
onTap: _toggleControls,
child: Stack(
fit: StackFit.expand,
children: [
//
if (widget.controller != null &&
widget.controller!.value.isInitialized)
FittedBox(
fit: BoxFit.contain,
alignment: Alignment.center,
child: SizedBox(
width: widget.controller!.value.size.width,
height: widget.controller!.value.size.height,
child: VideoPlayer(widget.controller!),
),
),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
padding: EdgeInsets.zero,
icon: Icon(
_isPlaying ? Icons.pause : Icons.play_arrow,
size: 28,
color: Colors.white,
)
else
if (widget.coverUrl.length > 0)
Image.network(
widget.coverUrl,
fit: BoxFit.cover,
width: containerW,
height: containerH,
),
//
if (_visibleControls)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.7),
Colors.transparent
],
),
onPressed: _togglePlayPause,
),
const SizedBox(width: 0),
Expanded(
child: ValueListenableBuilder<double>(
valueListenable: _sliderValue,
builder: (context, value, child) {
return SliderTheme(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
IconButton(
padding: EdgeInsets.zero,
icon: Icon(
_isPlaying ? Icons.pause : Icons.play_arrow,
size: 28,
color: Colors.white,
),
onPressed: _togglePlayPause,
),
Expanded(
child: ValueListenableBuilder<double>(
valueListenable: _sliderValue,
builder: (_, value, __) => SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.white,
inactiveTrackColor: Colors.white54,
thumbColor: Colors.white,
overlayColor: Colors.white24,
trackHeight: 2,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
thumbShape: RoundSliderThumbShape(
enabledThumbRadius: 8),
),
child: Slider(
value: value,
min: 0,
max: _totalDuration.inMilliseconds.toDouble(),
onChanged: widget.allowSeek && widget.controller != null
? (v) {
widget.controller!.seekTo(Duration(milliseconds: v.toInt()));
setState(() {
_currentPosition = Duration(milliseconds: v.toInt());
});
_sliderValue.value = v;
_startHideTimer();
}
: null,
child: SliderTheme(
data: SliderTheme.of(context).copyWith(
activeTrackColor: Colors.white, //
inactiveTrackColor: Colors.grey[400],//
thumbColor: Colors.white, //
overlayColor: Colors.white.withAlpha(0x33), //
disabledActiveTrackColor: Colors.white, //
disabledInactiveTrackColor: Colors.grey[400],
disabledThumbColor: Colors.white,
),
child: Slider(
value: value,
min: 0,
max: _totalDuration.inMilliseconds.toDouble(),
// allowSeek onChanged
onChanged: (v) {
if (widget.allowSeek && widget.controller != null) {
widget.controller!.seekTo(Duration(milliseconds: v.toInt()));
setState(() => _currentPosition = Duration(milliseconds: v.toInt()));
_sliderValue.value = v;
_startHideTimer();
}
},
),
),
);
}
),
),
const SizedBox(width: 0),
// 使
SizedBox(
width: 110, //
child: Text(
'${_formatDuration(_currentPosition)} / ${_formatDuration(_totalDuration)}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontFeatures: [FontFeature.tabularFigures()], //
),
),
),
),
SizedBox(
width: 110,
child: Text(
'${_formatDuration(_currentPosition)} / ${_formatDuration(_totalDuration)}',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontFeatures: [FontFeature.tabularFigures()],
),
),
),
IconButton(
padding: EdgeInsets.zero,
icon: Icon(
widget.isFullScreen
? Icons.fullscreen_exit
: Icons.fullscreen,
size: 28,
color: Colors.white,
),
onPressed: () {
widget.isFullScreen
? Navigator.of(context).pop()
: _enterFullScreen();
},
),
],
),
const SizedBox(width: 0),
IconButton(
padding: EdgeInsets.zero,
icon: Icon(
widget.isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen,
size: 28,
color: Colors.white,
),
onPressed: () {
widget.isFullScreen ? Navigator.of(context).pop() : _enterFullScreen();
},
),
],
),
),
),
),
],
],
),
),
),
);
}
String _formatDuration(Duration d) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final hours = d.inHours;
final minutes = d.inMinutes.remainder(60);
final seconds = d.inSeconds.remainder(60);
if (hours > 0) {
return '${twoDigits(hours)}:${twoDigits(minutes)}:${twoDigits(seconds)}';
}
return '${twoDigits(minutes)}:${twoDigits(seconds)}';
final h = d.inHours, m = d.inMinutes.remainder(60), s = d.inSeconds.remainder(60);
if (h > 0) return '${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}';
return '${twoDigits(m)}:${twoDigits(s)}';
}
}
}

View File

@ -35,7 +35,6 @@ class ApiService {
static const String projectManagerUrl =
'https://pm.qhdsafety.com/zy-projectManage';
// ///
// static const String baseFacePath =
// "https://qaaqwh.qhdsafety.com/whb_stu_face/";
@ -293,6 +292,8 @@ U6Hzm1ninpWeE+awIDAQAB
},
);
}
///
static Future<Map<String, dynamic>> fnGetVideoPlayInfo(String VIDEOCOURSEWARE_ID) {
return HttpManager().request(
basePath,
@ -430,6 +431,91 @@ U6Hzm1ninpWeE+awIDAQAB
);
}
///
static Future<Map<String, dynamic>> questionListByVideo(String VIDEOCOURSEWARE_ID) {
return HttpManager().request(
basePath,
'/app/edu/question/listAllByVideo',
method: Method.post,
data: {
'VIDEOCOURSEWARE_ID':VIDEOCOURSEWARE_ID,
},
);
}
///
static Future<Map<String, dynamic>> pageTaskScoreByUser(int showCount, int currentPage) {
return HttpManager().request(
basePath,
'/app/edu/stagestudentrelation/pageTaskScoreByUser',
method: Method.post,
data: {
"CORPINFO_ID":SessionService.instance.corpinfoId,
"USER_ID":SessionService.instance.loginUserId,
"showCount": showCount,
"currentPage": currentPage
},
);
}
///
static Future<Map<String, dynamic>> getExamRecordByStuId(String STUDENT_ID, String CLASS_ID) {
return HttpManager().request(
basePath,
'/app/edu/stageexam/getExamRecordByStuId',
method: Method.post,
data: {
"STUDENT_ID":STUDENT_ID,
"CLASS_ID": CLASS_ID,
},
);
}
///
static Future<Map<String, dynamic>> getStartExam(Map data) {
return HttpManager().request(
basePath,
'/app/edu/stageexam/getExam',
method: Method.post,
data: {
...data
},
);
}
///
static Future<Map<String, dynamic>> getStartStrengthenExam(Map data) {
return HttpManager().request(
basePath,
'/app/edu/stageexam/getStrengthenExam',
method: Method.post,
data: {
...data
},
);
}
///
static Future<Map<String, dynamic>> getListStrengthenVideo(Map data) {
return HttpManager().request(
basePath,
'/app/edu/stagestudentrelation/listStrengthenVideo',
method: Method.post,
data: {
...data
},
);
}
///
static Future<Map<String, dynamic>> submitExam(Map data) {
return HttpManager().request(
basePath,
'/app/edu/stageexam/submit',
method: Method.post,
data: {
"USERNAME": SessionService.instance.loginUser?["USERNAME"]??"",
"USER_ID":SessionService.instance.loginUserId,
...data
},
);
}

View File

@ -39,25 +39,26 @@ class HttpManager {
_dio.interceptors
..add(LogInterceptor(request: true, responseBody: true, error: true))
..add(InterceptorsWrapper(onError: (err, handler) {
// TODO
// 401
if (err.response?.statusCode == 401) {
//
onUnauthorized?.call();
//
final apiException = ApiException(
'提示',
'您的账号已在其他设备登录,已自动下线'
);
//
return handler.reject(
DioException(
requestOptions: err.requestOptions,
error: apiException,
response: err.response,
type: DioExceptionType.badResponse,
),
);
}
// if (err.response?.statusCode == 401) {
// //
// onUnauthorized?.call();
// //
// final apiException = ApiException(
// '提示',
// '您的账号已在其他设备登录,已自动下线'
// );
// //
// return handler.reject(
// DioException(
// requestOptions: err.requestOptions,
// error: apiException,
// response: err.response,
// type: DioExceptionType.badResponse,
// ),
// );
// }
handler.next(err);
}));
}

View File

@ -80,8 +80,8 @@ class MyApp extends StatelessWidget {
},
theme: ThemeData(
dividerTheme: const DividerThemeData(
color: Color(0xF1F1F1FF),
thickness: 1, // 线
color: Colors.black12,
thickness: .5, // 线
indent: 0, //
endIndent: 0, //
),
@ -97,6 +97,9 @@ class MyApp extends StatelessWidget {
borderRadius: BorderRadius.all(Radius.circular(8)),
),
),
progressIndicatorTheme: ProgressIndicatorThemeData(
color: Colors.blue, //
),
),
//
home: isLoggedIn ? const MainPage() : const LoginPage(),

View File

@ -12,6 +12,7 @@ import 'package:qhd_prevention/pages/home/work/danger_page.dart';
import 'package:qhd_prevention/pages/home/work/danger_wait_list_page.dart';
import 'package:qhd_prevention/pages/home/workSet_page.dart';
import '../../customWidget/hidden_roll_widget.dart';
import '../../http/ApiService.dart';
import '../../tools/tools.dart';
@ -77,9 +78,34 @@ class _HomePageState extends State<HomePage> {
_buildWorkSection(context),
const SizedBox(height: 10),
ListItemFactory.createBuildSimpleSection("隐患播报"),
// ListItemFactory.createBuildSimpleSection("隐患播报"),
Container(
color: Colors.white,
child: FutureBuilder(
future: ApiService.getHiddenRoll(),
builder: (ctx, snap) {
if (snap.connectionState != ConnectionState.done) {
return Center(
child: SizedBox(
height: 30 * 5,
child: Center(
child: CircularProgressIndicator(),
),
),
);
}
if (snap.hasError || snap.data == null) return Text('加载失败');
final list =
(snap.data!['hiddenList'] as List)
.cast<Map<String, dynamic>>();
return HiddenRollWidget(hiddenList: list);
},
),
),
const SizedBox(height: 10),
_buildPCDataSection(),
SizedBox(height: 50),
SizedBox(height: 20),
],
),
),
@ -130,7 +156,6 @@ class _HomePageState extends State<HomePage> {
);
}
Widget _buildPCDataSection() {
return Container(
decoration: BoxDecoration(
@ -176,20 +201,15 @@ class _HomePageState extends State<HomePage> {
//
if (index == 0) {
pushPage(UserinfoPage(), context);
}
else if (index == 1) {
} else if (index == 1) {
pushPage(WorkSetPage(), context);
}
else if (index == 2) {
} else if (index == 2) {
pushPage(RiskControlPage(), context);
}
else if (index == 3) {
} else if (index == 3) {
pushPage(LowPage(), context);
}
else if (index == 7) {
} else if (index == 7) {
pushPage(StudyGardenPage(), context);
}
},
);
}).toList(),
@ -333,13 +353,13 @@ class _HomePageState extends State<HomePage> {
if (index == 1) {
pushPage(DangerPage(), context);
} else if (index == 2) {
pushPage(DangerWaitListPage(DangerType.wait,2), context);
pushPage(DangerWaitListPage(DangerType.wait, 2), context);
} else if (index == 3) {
pushPage(DangerWaitListPage(DangerType.expired,3), context);
pushPage(DangerWaitListPage(DangerType.expired, 3), context);
} else if (index == 4) {
pushPage(DangerWaitListPage(DangerType.waitAcceptance,4), context);
pushPage(DangerWaitListPage(DangerType.waitAcceptance, 4), context);
} else if (index == 5) {
pushPage(DangerWaitListPage(DangerType.acceptance,5), context);
pushPage(DangerWaitListPage(DangerType.acceptance, 5), context);
}
},
child: Container(
@ -443,16 +463,17 @@ class _HomePageState extends State<HomePage> {
];
_fetchData(); //
}
Future<void> _fetchData() async {
try {
//
final raw = await ApiService.getWork();
// String decode Map
final Map<String, dynamic> data = raw is String
? json.decode(raw as String) as Map<String, dynamic>
: raw;
final Map<String, dynamic> data =
raw is String
? json.decode(raw as String) as Map<String, dynamic>
: raw;
final hidCount = data['hidCount'] as Map<String, dynamic>;
setState(() {
@ -488,17 +509,18 @@ class _HomePageState extends State<HomePage> {
"num": (hidCount['yys'] ?? 0).toString(),
},
];
});
//
final checkJson =
await ApiService.getSafetyEnvironmentalInspectionCount();
await ApiService.getSafetyEnvironmentalInspectionCount();
setState(() {
int confirmCount = checkJson['confirmCount']['confirmCount'];
int repulseCount = checkJson['repulseCount']['repulseCount'];
int repulseAndCheckCount = checkJson['repulseAndCheckCount']['repulseAndCheckCount'];
int repulseAndCheckCount =
checkJson['repulseAndCheckCount']['repulseAndCheckCount'];
_safetyEnvironmentalInspection = confirmCount + repulseCount + repulseAndCheckCount;
_safetyEnvironmentalInspection =
confirmCount + repulseCount + repulseAndCheckCount;
});
//
@ -509,8 +531,6 @@ class _HomePageState extends State<HomePage> {
_eight_work_count += (item ?? 0) as int;
}
});
} catch (e) {
// Toast
print('加载首页数据失败:$e');

View File

@ -0,0 +1,236 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:qhd_prevention/customWidget/ItemWidgetFactory.dart';
import 'package:qhd_prevention/customWidget/remote_file_page.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/pages/home/study/study_detail_page.dart';
import 'package:qhd_prevention/pages/home/study/take_exam_page.dart';
import 'package:qhd_prevention/tools/tools.dart';
import 'package:video_player/video_player.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import '../../../customWidget/video_player_widget.dart';
///
class StrengthenStudyPage extends StatefulWidget {
final String classId;
final String postId;
final String studentId;
const StrengthenStudyPage({
super.key,
required this.classId,
required this.postId,
required this.studentId,
});
@override
_StrengthenStudyPageState createState() => _StrengthenStudyPageState();
}
class _StrengthenStudyPageState extends State<StrengthenStudyPage> {
VideoPlayerController? _videoController;
String _videoCoverUrl = '';
List<dynamic> _videoList = [];
Map<String, dynamic> _info = {};
@override
void initState() {
super.initState();
_fetchData();
}
@override
void dispose() {
_videoController?.removeListener(_controllerListener);
_videoController?.dispose();
super.dispose();
}
Future<void> _fetchData() async {
final res = await ApiService.getListStrengthenVideo({
'CLASS_ID': widget.classId,
'STUDENT_ID': widget.studentId,
'TYPE': 'APP',
});
if (res['result'] == 'success') {
setState(() {
_info = res['relation'];
_videoList = res['videoList'];
});
final first = _processData(_videoList);
_loadPlayInfo(first, loadPdf: false);
}
}
Map<String, dynamic> _processData(List<dynamic> data) {
for (var item in data) {
if (item['nodes'] != null) {
final node = (item['nodes'] as List)
.cast<Map<String, dynamic>>()
.firstWhere((n) => n['IS_VIDEO'] == 0, orElse: () => {});
if (node.isNotEmpty) return node;
} else if (item['IS_VIDEO'] == 0) {
return item;
}
}
return {};
}
Future<void> _loadPlayInfo(
Map<String, dynamic> row, {
required bool loadPdf,
}) async {
final id = row['VIDEOCOURSEWARE_ID'];
final isVideo = row['IS_VIDEO'];
if (isVideo == 0) {
final res = await ApiService.fnGetVideoPlayInfo(id);
if (res['result'] == 'success') {
setState(() => _videoCoverUrl = res['videoBase']?['coverURL'] ?? '');
_initVideo(
res['videoList']?[0]['playURL'] ?? '',
_videoCoverUrl,
);
} else {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(res['msg'] ?? '播放信息获取失败')));
}
} else if (loadPdf && row['VIDEOFILES'] != null) {
_videoController?.pause();
pushPage(
RemoteFilePage(
fileUrl: ApiService.baseImgPath + row['VIDEOFILES'],
countdownSeconds: 10,
),
context,
);
}
}
void _initVideo(String url, String cover) {
_videoController?.removeListener(_controllerListener);
_videoController?.dispose();
_videoController = VideoPlayerController.networkUrl(Uri.parse(url))
..initialize().then((_) {
_videoController!
..play()
..addListener(_controllerListener);
});
}
void _controllerListener() {
if (mounted) setState(() {});
}
String _formatDuration(dynamic secs) {
final total = (double.tryParse(secs.toString())?.toInt() ?? 0);
final h = total ~/ 3600;
final m = (total % 3600) ~/ 60;
final s = total % 60;
String two(int n) => n.toString().padLeft(2, '0');
return "${two(h)}:${two(m)}:${two(s)}";
}
///
Future<void> _startExam(TakeExamType type) async {
final arguments = {
'STRENGTHEN_STAGEEXAMPAPER_INPUT_ID': _info['STRENGTHEN_STAGEEXAMPAPER_INPUT_ID'],
'CLASS_ID': widget.classId,
'POST_ID': widget.postId,
'STUDENT_ID': widget.studentId,
};
print('--_startExam data---$arguments');
final data = await ApiService.getStartStrengthenExam(arguments);
if (data['result'] == 'success') {
pushPage(TakeExamPage(examInfo: {
'CLASS_ID':widget.classId,
'POST_ID': widget.postId,
'STUDENT_ID': widget.studentId,
'STRENGTHEN_PAPER_QUESTION_ID': _info['STRENGTHEN_STAGEEXAMPAPER_INPUT_ID'],
...data
}, examType: type), context);
}else{
ToastUtil.showError(context, '请求错误');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: MyAppbar(title: '加强学习课件'),
body: Column(
children: [
ListItemFactory.createBuildSimpleSection('加强学习课件'),
SizedBox(
width: double.infinity,
height: 250,
child: VideoPlayerWidget(
allowSeek: false,
controller: _videoController,
coverUrl:'',
aspectRatio: _videoController?.value.aspectRatio ?? 16 / 9,
),
),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _videoList.length,
itemBuilder: (_, i) {
final item = _videoList[i];
return GestureDetector(
onTap: () => _loadPlayInfo(item, loadPdf: true),
child: Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Image.asset(
item['IS_VIDEO'] == 0
? 'assets/study/play.png'
: 'assets/study/copy-one.png',
width: 20,
height: 20,
),
const SizedBox(width: 10),
Text(item['COURSEWARENAME'] ?? ''),
],
),
if (item['IS_VIDEO'] == 0)
Text(_formatDuration(item['VIDEOTIME'])),
],
),
),
);
},
),
),
Padding(
padding: const EdgeInsets.all(20),
child: CustomButton(
text: '效果评估考试',
backgroundColor: Colors.blue,
onPressed: () {
_videoController?.pause();
_startExam(TakeExamType.strengththen);
},
),
),
],
),
);
}
}

View File

@ -9,10 +9,17 @@ import 'package:qhd_prevention/http/ApiService.dart';
import 'package:qhd_prevention/tools/tools.dart';
import 'package:video_player/video_player.dart';
import '../../../customWidget/toast_util.dart';
import '../../../customWidget/video_player_widget.dart';
import '../../../http/HttpManager.dart';
import 'face_ecognition_page.dart';
enum TakeExamType {
video_study,
strengththen,
list
}
class StudyDetailPage extends StatefulWidget {
final Map studyDetailDetail;
final String studentId;
@ -163,7 +170,7 @@ class _StudyDetailPageState extends State<StudyDetailPage>
// document
if (data['VIDEOFILES'] != null) {
await pushPage(
StudyPractisePage(data['VIDEOCOURSEWARE_ID']),
StudyPractisePage(videoCoursewareId: data['VIDEOCOURSEWARE_ID']),
context,
);
await _submitPlayTime(
@ -212,20 +219,35 @@ class _StudyDetailPageState extends State<StudyDetailPage>
_classId,
widget.studentId,
);
final seen = (double.tryParse(prog['pd']?['RESOURCETIME']) ?? 0.0).toInt();
final raw = prog['pd']?['RESOURCETIME'];
final seen = (() {
if (raw == null) return 0;
//
if (raw is num) return raw.toInt();
// parse
final s = raw.toString();
return (double.tryParse(s) ?? 0.0).toInt();
})();
// controller
_videoController?.removeListener(_onTimeUpdate);
_videoController?.dispose();
// controller
_videoController = VideoPlayerController.networkUrl(Uri.parse(url));
await _videoController!.initialize();
setState(() {});
// seek
_videoController!
..seekTo(Duration(seconds: seen))
..play()
..addListener(_onTimeUpdate);
}
void _onTimeUpdate() {
if (_videoController == null || !_videoController!.value.isPlaying) return;
final curr = _videoController!.value.position;
@ -251,7 +273,7 @@ class _StudyDetailPageState extends State<StudyDetailPage>
if (_currentVideoData == null) return;
try {
final pd = (await ApiService.fnSubmitPlayTime(
final resData = (await ApiService.fnSubmitPlayTime(
_currentVideoData!['VIDEOCOURSEWARE_ID'],
_currentVideoData!['CURRICULUM_ID'],
end ? '1' : '0',
@ -260,8 +282,8 @@ class _StudyDetailPageState extends State<StudyDetailPage>
widget.studentId,
_classCurriculumId,
_classId,
))['pd']!;
));
final pd = resData['pd'] ?? {};
//
final comp = pd['PLAYCOUNT'] != null && pd['PLAYCOUNT'] > 0;
final resT = pd['RESOURCETIME'] ?? seconds;
@ -293,14 +315,8 @@ class _StudyDetailPageState extends State<StudyDetailPage>
) ??
false;
if (ok) {
final arguments = {
'STAGEEXAMPAPERINPUT_ID':
pd['paper']['STAGEEXAMPAPERINPUT_ID'],
'CLASS_ID': _classId,
'STUDENT_ID': widget.studentId,
'NUMBEROFEXAMS': pd['NUMBEROFEXAMS'],
};
pushPage(TakeExamPage(arguments), context);
_startExam(resData);
} else {
_videoController?.play();
}
@ -313,6 +329,40 @@ class _StudyDetailPageState extends State<StudyDetailPage>
}
}
///
Future<void> _startExam(Map resData) async {
Map pd = resData['pd'] ?? {};
Map paper = resData['paper'] ?? {};
setState(() {
_loading = true;
});
final arguments = {
'STAGEEXAMPAPERINPUT_ID': paper['STAGEEXAMPAPERINPUT_ID']??'',
'STAGEEXAMPAPER_ID': paper['STAGEEXAMPAPER_ID']??'',
'CLASS_ID': _classId,
'POST_ID': pd['POST_ID'] ?? '',
'STUDENT_ID': widget.studentId,
'NUMBEROFEXAMS': pd['NUMBEROFEXAMS'] ?? ''
};
print('--_startExam data---$arguments');
final data = await ApiService.getStartExam(arguments);
setState(() {
_loading = false;
});
if (data['result'] == 'success') {
pushPage(TakeExamPage(examInfo: {
'CLASS_ID':_classId,
'POST_ID': pd['POST_ID'] ?? '',
'STUDENT_ID': widget.studentId,
'STRENGTHEN_PAPER_QUESTION_ID': paper['STAGEEXAMPAPERINPUT_ID']??'',
...data
}, examType: TakeExamType.video_study), context);
}else{
ToastUtil.showError(context, '请求错误');
}
}
void _startFaceTimer() {
_faceTimer = Timer.periodic(Duration(seconds: _faceTime), (_) async {
@ -362,7 +412,9 @@ class _StudyDetailPageState extends State<StudyDetailPage>
children: [
SizedBox(
height: 250,
width: screenWidth(context),
child: VideoPlayerWidget(
allowSeek: false,
controller: _videoController,
coverUrl: _videoCoverUrl.isNotEmpty
? ApiService.baseImgPath + _videoCoverUrl
@ -493,7 +545,7 @@ class _StudyDetailPageState extends State<StudyDetailPage>
CustomButton(
onPressed:
() => pushPage(
StudyPractisePage(m['VIDEOCOURSEWARE_ID']),
StudyPractisePage(videoCoursewareId: m['VIDEOCOURSEWARE_ID']),
context,
),
text: "课后练习",
@ -538,4 +590,3 @@ class _StudyDetailPageState extends State<StudyDetailPage>
);
}
}

View File

@ -3,8 +3,13 @@ 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/strengthen_video_study_page.dart';
import 'package:qhd_prevention/pages/home/study/study_class_list_page.dart';
import 'package:qhd_prevention/pages/home/study/study_detail_page.dart';
import 'package:qhd_prevention/pages/home/study/take_exam_page.dart';
import 'package:qhd_prevention/pages/home/study/video_study_detail_page.dart';
import 'package:qhd_prevention/tools/tools.dart';
import '../../../customWidget/toast_util.dart';
import '../../../http/ApiService.dart';
import '../../mine/mine_sign_page.dart';
import '../../my_appbar.dart';
@ -184,7 +189,7 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
final int studyState = int.tryParse(item['STUDYSTATE'] ?? '') ?? 0;
final int stageExamState = int.tryParse(item['STAGEEXAMSTATE'] ?? '') ?? 0;
final int strengthenExamState =
int.tryParse(item['STRENGTHENEXAMSTATE'] ?? '') ?? 0;
int.tryParse(item['STRENGTHENEXAMSTATE'] ?? '') ?? -1;
final int numberOfExams = int.tryParse('${item['NUMBEROFEXAMS']}') ?? 0;
final int ksCount = int.tryParse('${item['ksCount']}') ?? 0;
final int examinationFlag =
@ -194,7 +199,7 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
final String isStrengthen = item['ISSTRENGTHEN'] ?? '0';
return Container(
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
@ -244,24 +249,8 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
],
),
Wrap(
spacing: 8,
spacing: 10,
children: [
//
if (stageExamState == 3)
CustomButton(
height: 36,
text: "考试详情",
padding: EdgeInsets.symmetric(horizontal: 20),
borderRadius: 18,
backgroundColor: Colors.blue,
onPressed:
() => Navigator.pushNamed(
context,
'/exam_details',
arguments: {'STUDENT_ID': item['STUDENT_ID']},
),
),
//
if (studyState >= 2 &&
stageExamState >= 2 &&
@ -270,18 +259,17 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
CustomButton(
height: 36,
text: "加强学习",
padding: EdgeInsets.symmetric(horizontal: 20),
padding: EdgeInsets.symmetric(horizontal: 18),
borderRadius: 18,
backgroundColor: Colors.blue,
onPressed:
() => Navigator.pushNamed(
() => pushPage(
StrengthenStudyPage(
classId: item['CLASS_ID'] ?? '',
postId: item['POST_ID'] ?? '',
studentId: item['STUDENT_ID'] ?? '',
),
context,
'/strengthen_video_study',
arguments: {
'CLASS_ID': item['CLASS_ID'],
'POST_ID': item['POST_ID'],
'STUDENT_ID': item['STUDENT_ID'],
},
),
),
@ -290,7 +278,7 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
CustomButton(
height: 36,
text: "立即学习",
padding: EdgeInsets.symmetric(horizontal: 20),
padding: EdgeInsets.symmetric(horizontal: 18),
borderRadius: 18,
backgroundColor: Colors.blue,
onPressed: () {
@ -311,23 +299,29 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
CustomButton(
height: 36,
text: "立即考试",
padding: EdgeInsets.symmetric(horizontal: 20),
padding: EdgeInsets.symmetric(horizontal: 18),
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',
},
() => _startExam(item, TakeExamType.video_study),
),
//
if (stageExamState == 3)
CustomButton(
height: 36,
text: "考试详情",
padding: EdgeInsets.symmetric(horizontal: 18),
borderRadius: 18,
backgroundColor: Colors.green,
onPressed: () {
pushPage(
VideoStudyDetailPage(
studentId: item['STUDENT_ID'] ?? '',
classId: item['CLASS_ID'] ?? '',
),
context,
);
},
),
],
),
@ -337,6 +331,39 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
),
);
}
///
Future<void> _startExam(Map resData, TakeExamType type) async {
setState(() {
_isLoading = true;
});
final arguments = {
'STAGEEXAMPAPERINPUT_ID': resData['STAGEEXAMPAPERINPUT_ID'] ?? '',
'STAGEEXAMPAPER_ID': resData['STAGEEXAMPAPER_ID'] ?? '',
'CLASS_ID': resData['CLASS_ID'] ?? '',
'POST_ID': resData['POST_ID'] ?? '',
'STUDENT_ID': resData['STUDENT_ID'] ?? '',
'NUMBEROFEXAMS': resData['NUMBEROFEXAMS'] ?? '',
};
print('--_startExam data---$arguments');
final data = await ApiService.getStartExam(arguments);
setState(() {
_isLoading = false;
});
if (data['result'] == 'success') {
pushPage(TakeExamPage(examInfo: {
'CLASS_ID': resData['CLASS_ID'] ?? '',
'POST_ID': resData['POST_ID'] ?? '',
'STUDENT_ID': resData['STUDENT_ID'] ?? '',
'STRENGTHEN_PAPER_QUESTION_ID': resData['STAGEEXAMPAPERINPUT_ID'] ?? '',
...data
}, examType: TakeExamType.video_study), context);
}else{
ToastUtil.showError(context, '请求错误');
}
}
bool _onScroll(ScrollNotification n) {
if (n.metrics.pixels > n.metrics.maxScrollExtent - 100 &&

View File

@ -1,19 +1,330 @@
import 'package:flutter/material.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
import '../../../http/ApiService.dart'; //
class StudyPractisePage extends StatefulWidget {
const StudyPractisePage(this.VIDEOCOURSEWARE_ID,{super.key});
final String VIDEOCOURSEWARE_ID;
final String videoCoursewareId;
const StudyPractisePage({Key? key, required this.videoCoursewareId})
: super(key: key);
@override
State<StudyPractisePage> createState() => _StudyPractisePageState();
_PracticePageState createState() => _PracticePageState();
}
class _StudyPractisePageState extends State<StudyPractisePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: MyAppbar(title: "课后练习"),
body: SizedBox(),
class Question {
final String questionDry;
final String questionType; // '1','2','3','4'
final Map<String, String> options;
final String answer;
final String descr;
bool correctAnswerShow;
String checked;
Question({
required this.questionDry,
required this.questionType,
required this.options,
required this.answer,
required this.descr,
this.correctAnswerShow = false,
this.checked = '',
});
factory Question.fromJson(Map<String, dynamic> json) {
final type = json['QUESTIONTYPE'] as String;
Map<String, String> opts = {};
if (type == '1' || type == '2' || type == '3') {
opts['A'] = json['OPTIONA'] as String? ?? '';
opts['B'] = json['OPTIONB'] as String? ?? '';
if (type != '3') {
opts['C'] = json['OPTIONC'] as String? ?? '';
opts['D'] = json['OPTIOND'] as String? ?? '';
}
}
return Question(
questionDry: json['QUESTIONDRY'] as String? ?? '',
questionType: type,
options: opts,
answer: json['ANSWER'] as String? ?? '',
descr: json['DESCR'] as String? ?? '',
);
}
}
class _PracticePageState extends State<StudyPractisePage> {
int current = 0;
List<Question> options = [];
bool loading = true;
final Map<String, String> questionTypeMap = {
'1': '单选题',
'2': '多选题',
'3': '判断题',
'4': '填空题',
};
@override
void initState() {
super.initState();
_getData();
}
Future<void> _getData() async {
setState(() => loading = true);
final res = await ApiService.questionListByVideo(widget.videoCoursewareId);
if (res['result'] == 'success') {
List list = res['varList'] as List;
options = list.map((e) => Question.fromJson(e)).toList();
} else {
options = [];
}
setState(() => loading = false);
}
void _chooseTopic(String type, String item) {
final q = options[current];
if (q.correctAnswerShow) return;
setState(() {
if (type == 'radio' || type == 'judge') {
q.checked = (q.checked == item) ? '' : item;
_correctAnswerShow();
} else if (type == 'multiple') {
List<String> arr = q.checked.isNotEmpty ? q.checked.split(',') : [];
if (arr.contains(item))
arr.remove(item);
else
arr.add(item);
arr.sort();
q.checked = arr.join(',');
}
});
}
void _correctAnswerShow() {
final q = options[current];
if (q.questionType == '2' && q.checked.split(',').length < 2) {
ToastUtil.showError(context, '多选题最少需要选择两个答案');
return;
}
if (q.checked.isNotEmpty) {
setState(() => q.correctAnswerShow = true);
}
}
Widget _buildOptions(Question q) {
switch (q.questionType) {
case '1':
case '3':
return Column(
children:
q.options.entries.map((e) {
bool isChecked = q.checked == e.key;
return _optionItem(
label: e.key,
text: e.value,
active: !q.correctAnswerShow && isChecked,
right: q.correctAnswerShow && q.answer == e.key && isChecked,
err: q.correctAnswerShow && q.answer != e.key && isChecked,
warning:
q.correctAnswerShow && q.answer == e.key && !isChecked,
onTap:
() => _chooseTopic(
q.questionType == '3' ? 'judge' : 'radio',
e.key,
),
);
}).toList(),
);
case '2':
return Column(
children: [
...q.options.entries.map((e) {
bool isChecked = q.checked.split(',').contains(e.key);
bool isCorrect = q.answer.split(',').contains(e.key);
return _optionItem(
label: e.key,
text: e.value,
multiple: true,
active: !q.correctAnswerShow && isChecked,
right: q.correctAnswerShow && isCorrect && isChecked,
err: q.correctAnswerShow && !isCorrect && isChecked,
warning: q.correctAnswerShow && isCorrect && !isChecked,
onTap: () => _chooseTopic('multiple', e.key),
);
}),
if (!q.correctAnswerShow)
ElevatedButton(
onPressed: _correctAnswerShow,
child: Text('确认答案'),
),
],
);
case '4':
return TextField(
maxLength: 255,
onChanged: (v) => q.checked = v,
decoration: InputDecoration(
hintText: '请输入内容',
border: OutlineInputBorder(),
),
);
default:
return SizedBox.shrink();
}
}
Widget _optionItem({
required String label,
required String text,
bool active = false,
bool right = false,
bool err = false,
bool warning = false,
bool multiple = false,
required VoidCallback onTap,
}) {
Color fg = Colors.black87;
Color bg = Colors.grey.shade200;
if (right) {
fg = Colors.green;
bg = Colors.green;
}
if (err) {
fg = Colors.red;
bg = Colors.red;
}
if (warning) {
fg = Colors.green;
bg = Colors.green;
}
if (active) fg = Colors.blue;
return GestureDetector(
onTap: onTap,
child: Container(
margin: EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
color:
multiple ? Colors.transparent : (active ? Colors.blue : bg),
shape: BoxShape.circle,
),
child:
multiple
? (right
? Icon(Icons.check_circle, color: Colors.green)
: err
? Icon(Icons.cancel, color: Colors.red)
: Text(label, style: TextStyle(color: fg)))
: Text(
label,
style: TextStyle(color: multiple ? fg : Colors.white),
),
),
SizedBox(width: 16),
Expanded(
child: Text(text, style: TextStyle(color: fg, fontSize: 16)),
),
],
),
),
);
}
String _renderAnswerText(Question q) {
if (q.questionType == '3') return q.checked == 'A' ? '' : '';
return q.checked;
}
@override
@override
Widget build(BuildContext context) {
final q = options.isNotEmpty ? options[current] : null;
return Scaffold(
appBar: MyAppbar(title: '课后练习'),
body:
loading
? Center(child: CircularProgressIndicator())
: options.isEmpty
? Center(child: Text('暂无数据'))
: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
SizedBox(
width: 1000,
height: 60,
child: Image.asset(
'assets/study/bgimg1.png',
fit: BoxFit.fill,
),
),
SizedBox(
height: 60,
child: Center(
child: Text(
'当前试题 ${current + 1}/${options.length}',
style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold),
),
),
)
],
),
SizedBox(height: 16),
if (q != null) ...[
Text(
'${current + 1}. ${q.questionDry} (${questionTypeMap[q.questionType]})',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
SizedBox(height: 16),
_buildOptions(q),
if (q.correctAnswerShow) ...[
Divider(),
Text('我的答案: ${_renderAnswerText(q)}'),
Text('正确答案: ${q.answer}'),
Text('权威解读: ${q.descr}'),
],
],
Spacer(),
Row(
children: [
if (current > 0)
Expanded(
child: CustomButton(
text: '上一题',
textStyle: TextStyle(color: Colors.black54),
backgroundColor: Colors.grey.shade200,
onPressed: () => setState(() => current--),
),
),
if (current > 0 && current < options.length - 1)
SizedBox(width: 16),
if (current < options.length - 1)
Expanded(
child: CustomButton(
text: '下一题',
backgroundColor: Colors.blue,
onPressed: () => setState(() => current++),
),
),
],
),
],
),
),
);
}
}

View File

@ -1,15 +1,259 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
import 'package:qhd_prevention/tools/h_colors.dart';
import '../../../http/ApiService.dart';
class StudyScorePage extends StatefulWidget {
const StudyScorePage({super.key});
const StudyScorePage({Key? key}) : super(key: key);
@override
State<StudyScorePage> createState() => _StudyScorePageState();
_StudyScorePageState createState() => _StudyScorePageState();
}
class _StudyScorePageState extends State<StudyScorePage> {
//
int joinNum = 0, passNum = 0, noPassNum = 0;
List<dynamic> list = [];
//
int showCount = 10;
int currentPage = 1;
int totalPage = 1;
bool loading = false;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_fetchData();
//
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 50 &&
!loading &&
currentPage < totalPage) {
currentPage++;
_fetchData();
}
});
}
Future<void> _fetchData() async {
setState(() => loading = true);
final res = await ApiService.pageTaskScoreByUser(showCount, currentPage);
setState(() => loading = false);
if (res != null && res['result'] == 'success') {
final varList = res['varList'];
if (varList is List) {
list.addAll(varList);
}
//
joinNum = _toInt(res['JOINNUM'], defaultValue: joinNum);
passNum = _toInt(res['PASSNUM'], defaultValue: passNum);
noPassNum = _toInt(res['NOPASSNUM'], defaultValue: noPassNum);
totalPage = _toInt(res['totalPage'], defaultValue: totalPage);
}
}
int _toInt(dynamic value, {required int defaultValue}) {
if (value is int) return value;
if (value is String) {
return int.tryParse(value) ?? defaultValue;
}
return defaultValue;
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Widget _buildStatItem(String label, int value) {
return Expanded(
child: Column(
children: [
Text(
'$value',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
color: Colors.white,
),
),
SizedBox(height: 4),
Text(label, style: TextStyle(fontSize: 15, color: Colors.white)),
],
),
);
}
Widget _buildListItem(dynamic item) {
// STAGEEXAMSTATE
Color stateColor;
String stateText;
switch (item['STAGEEXAMSTATE']) {
case '1':
stateColor = Color(0xff3377ff);
stateText = '待考试';
break;
case '2':
stateColor = Color(0xff999999);
stateText = '考试未通过';
break;
case '3':
stateColor = Color(0xff33c76d);
stateText = '考试通过';
break;
default:
stateColor = Color(0xff999999);
stateText = '未参加';
}
//
int score = _toInt(item['STAGEEXAMSCORE'], defaultValue: -1);
String scoreText = score >= 0 ? '$score' : '';
return Card(
color: Colors.white,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: InkWell(
onTap: () {
final state = item['STAGEEXAMSTATE'];
if (state == '2' || state == '3') {
Navigator.pushNamed(context, '/exam_details', arguments: item);
}
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'考试时长:${item['ANSWERSHEETTIME']} 分钟',
style: TextStyle(color: Colors.grey[700]),
),
Text(stateText, style: TextStyle(color: stateColor)),
],
),
const Divider(height: 20),
_infoRow('培训任务名称', item['CLASS_NAME'] ?? ''),
_infoRow('试卷名称', item['EXAMNAME'] ?? ''),
_infoRow('岗位类型', item['POSTTYPE_NAME'] ?? ''),
_infoRow('考试成绩', scoreText),
],
),
),
),
);
}
Widget _infoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(color: Colors.grey[600])),
Flexible(
child: Text(
value,
style: TextStyle(
color: Colors.black87,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.right,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return const Placeholder();
return Scaffold(
appBar: MyAppbar(title: '成绩查询'),
backgroundColor: h_backGroundColor(),
body: Column(
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12),
child: Stack(
children: [
SizedBox(
width: 1000,
height: 130,
child: Image.asset(
'assets/study/bgimg1.png',
fit: BoxFit.fill,
),
),
Column(
children: [
const SizedBox(height: 10),
const Text(
'我的成绩',
style: TextStyle(
fontSize: 20,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: const Divider(height: 20, color: Colors.white),
),
Row(
children: [
_buildStatItem('参加考试次数', joinNum),
_buildStatItem('合格次数', passNum),
_buildStatItem('不合格次数', noPassNum),
],
),
],
),
],
),
),
//
Expanded(
child:
list.isEmpty
? Center(
child: Text('暂无数据', style: TextStyle(color: Colors.grey)),
)
: ListView.builder(
controller: _scrollController,
itemCount: list.length + 1,
itemBuilder: (context, i) {
if (i < list.length) return _buildListItem(list[i]);
//
return Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(
child:
loading
? CircularProgressIndicator()
: Text(
currentPage >= totalPage ? '没有更多了' : '',
),
),
);
},
),
),
],
),
);
}
}

View File

@ -1,18 +1,404 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart';
import 'package:qhd_prevention/customWidget/custom_button.dart';
import 'package:qhd_prevention/customWidget/toast_util.dart';
import 'package:qhd_prevention/pages/home/study/study_detail_page.dart';
import 'package:qhd_prevention/pages/my_appbar.dart';
import 'package:qhd_prevention/http/ApiService.dart';
import 'dart:convert';
class Question {
final Map<String, dynamic> rawData;
final String questionDry;
final String questionType;
final Map<String, String> options;
String checked;
Question({
required this.rawData,
required this.questionDry,
required this.questionType,
required this.options,
this.checked = '',
});
factory Question.fromJson(Map<String, dynamic> json) {
final type = json['QUESTIONTYPE'] as String? ?? '1';
final opts = <String, String>{};
if (type == '1' || type == '2' || type == '3') {
opts['A'] = json['OPTIONA'] as String? ?? '';
opts['B'] = json['OPTIONB'] as String? ?? '';
if (type != '3') {
opts['C'] = json['OPTIONC'] as String? ?? '';
opts['D'] = json['OPTIOND'] as String? ?? '';
}
}
final raw = Map<String, dynamic>.from(json);
return Question(
rawData: raw,
questionDry: json['QUESTIONDRY'] as String? ?? '',
questionType: type,
options: opts,
);
}
}
class TakeExamPage extends StatefulWidget {
const TakeExamPage(this.arguments,{super.key});
final Map<String, dynamic> arguments;
const TakeExamPage({
required this.examInfo,
required this.examType,
super.key,
});
final Map<String, dynamic> examInfo;
final TakeExamType examType;
@override
State<TakeExamPage> createState() => _TakeExamPageState();
}
class _TakeExamPageState extends State<TakeExamPage> {
late final List<Question> questions;
late final Map<String, dynamic> info;
int current = 0;
late int remainingSeconds;
Timer? _timer;
final questionTypeMap = <String, String>{
'1': '单选题',
'2': '多选题',
'3': '判断题',
'4': '填空题',
};
@override
void initState() {
super.initState();
info = widget.examInfo['pd'] as Map<String, dynamic>? ?? {};
final rawList = widget.examInfo['inputQue'] as List<dynamic>? ?? [];
questions =
rawList
.map((e) => Question.fromJson(e as Map<String, dynamic>))
.toList();
final numberOfExams = widget.examInfo['NUMBEROFEXAMS'] as String? ?? '0';
WidgetsBinding.instance.addPostFrameCallback((_) {
if (numberOfExams == '-9999') {
_showTip('强化学习考试开始,限时${info['ANSWERSHEETTIME']}分钟,请注意答题时间!');
} else {
_showTip('您无考试次数!');
}
});
final minutes = info['ANSWERSHEETTIME'] as int? ?? 0;
remainingSeconds = minutes * 60;
_startTimer();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
Future<void> _showTip(String content) {
return showDialog<void>(
context: context,
builder:
(_) => CustomAlertDialog(
title: '温馨提示',
content: content,
cancelText: '',
confirmText: '确定',
onConfirm: () {},
),
);
}
bool _validateCurrentAnswer() {
final q = questions[current];
if (q.checked.isEmpty) {
_showTip('请对本题进行作答。');
return false;
}
if (q.questionType == '2' && q.checked.split(',').length < 2) {
_showTip('多选题最少需要选择两个答案。');
return false;
}
return true;
}
void _startTimer() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (remainingSeconds <= 0) {
timer.cancel();
_onTimeUp();
} else {
setState(() => remainingSeconds--);
}
});
}
void _chooseTopic(String type, String key) {
final q = questions[current];
if (type == 'radio' || type == 'judge') {
q.checked = (q.checked == key) ? '' : key;
} else {
final arr = q.checked.isNotEmpty ? q.checked.split(',') : <String>[];
if (arr.contains(key))
arr.remove(key);
else
arr.add(key);
arr.sort();
q.checked = arr.join(',');
}
setState(() {});
}
void _nextQuestion() {
if (!_validateCurrentAnswer()) return;
if (current < questions.length - 1) setState(() => current++);
}
void _previousQuestion() {
if (current > 0) setState(() => current--);
}
void _confirmSubmit() {
if (!_validateCurrentAnswer()) return;
showDialog<void>(
context: context,
builder:
(_) => CustomAlertDialog(
title: '温馨提示',
content: '请确认是否交卷!',
cancelText: '取消',
onCancel: () {},
onConfirm: _submit,
),
);
}
Future<void> _submit() async {
for (var q in questions) {
if (q.questionType == '2') q.checked = q.checked.replaceAll(',', '');
q.rawData['checked'] = q.checked;
}
final data = {
'STAGEEXAMPAPERINPUT_ID':
widget.examInfo['STRENGTHEN_PAPER_QUESTION_ID'],
'STUDENT_ID': widget.examInfo['STUDENT_ID'],
'CLASS_ID': widget.examInfo['CLASS_ID'],
'NUMBEROFEXAMS': widget.examInfo['NUMBEROFEXAMS'],
'entrySite': widget.examType.name,
'PASSSCORE': info['PASSSCORE'],
'EXAMSCORE': info['EXAMSCORE'],
'EXAMTIMEBEGIN': info['EXAMTIMEBEGIN'],
'options': jsonEncode(questions.map((q) => q.rawData).toList()),
};
final res = await ApiService.submitExam(data);
if (res['result'] == 'success') {
final score = res['examScore'] ?? '0';
final passed = res['examResult'] != '0';
showDialog<void>(
context: context,
builder:
(_) => CustomAlertDialog(
title: '温馨提示',
content:
passed
? '您的成绩为 $score 分,恭喜您通过本次考试,请继续保持!'
: '您的成绩为 $score 分,很遗憾您没有通过本次考试,请再接再厉!',
cancelText: '',
confirmText: '确定',
onConfirm: () {
Navigator.pop(context);
},
),
);
}
}
void _onTimeUp() {
ToastUtil.showError(context, '考试时间已结束');
_submit();
}
Widget _buildOptions(Question q) {
if (q.questionType == '4') {
return TextField(
controller: TextEditingController(text: q.checked),
onChanged: (val) => q.checked = val,
maxLength: 255,
decoration: const InputDecoration(
hintText: '请输入内容',
border: OutlineInputBorder(),
),
);
}
final keys = q.questionType == '3' ? ['A', 'B'] : ['A', 'B', 'C', 'D'];
return Column(
children:
keys.map((key) {
final active = q.checked.split(',').contains(key);
return GestureDetector(
onTap:
() => _chooseTopic(
q.questionType == '3'
? 'judge'
: (q.questionType == '2' ? 'multiple' : 'radio'),
key,
),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
color: active ? Colors.blue : Colors.grey.shade200,
shape: BoxShape.circle,
),
child: Text(
key,
style: TextStyle(
color: active ? Colors.white : Colors.black87,
),
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
q.options[key] ?? '',
style: const TextStyle(fontSize: 16),
),
),
],
),
),
);
}).toList(),
);
}
String get _formattedTime {
final m = remainingSeconds ~/ 60;
final s = remainingSeconds % 60;
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: MyAppbar(title: '开始考试'),
final q = questions.isNotEmpty ? questions[current] : null;
return PopScope(
canPop: false, //
child: Scaffold(
appBar: const MyAppbar(title: '课程考试', isBack: false,),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
Container(
width: double.infinity,
height: 120,
decoration: BoxDecoration(
image: const DecorationImage(
image: AssetImage('assets/study/bgimg1.png'),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(8),
),
),
Positioned.fill(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'考试科目:${info['EXAMNAME'] ?? ''}',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'当前试题 ${current + 1}/${questions.length}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
const SizedBox(height: 8),
Text(
'考试剩余时间:$_formattedTime',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
],
),
),
),
],
),
const SizedBox(height: 16),
if (q != null) ...[
Text(
'${current + 1}. ${q.questionDry} ${questionTypeMap[q.questionType] ?? ''}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
const SizedBox(height: 16),
_buildOptions(q),
],
const Spacer(),
Row(
children: [
if (current > 0)
Expanded(
child: CustomButton(
text: '上一题',
backgroundColor: Colors.grey.shade200,
textStyle: const TextStyle(color: Colors.black54),
onPressed: _previousQuestion,
),
),
if (current > 0 && current < questions.length - 1)
const SizedBox(width: 16),
if (current < questions.length - 1)
Expanded(
child: CustomButton(
text: '下一题',
backgroundColor: Colors.blue,
onPressed: _nextQuestion,
),
),
if (current == questions.length - 1)
Expanded(
child: CustomButton(
text: '交卷',
backgroundColor: Colors.blue,
onPressed: _confirmSubmit,
),
),
],
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,250 @@
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/http/ApiService.dart';
class VideoStudyDetailPage extends StatefulWidget {
final String studentId;
final String classId;
const VideoStudyDetailPage({Key? key, required this.studentId, required this.classId}) : super(key: key);
@override
_VideoStudyDetailPageState createState() => _VideoStudyDetailPageState();
}
class Question {
final String questionDry;
final String questionType;
final Map<String, String> options;
String answer;
final String answerRight;
final String descr;
bool answered;
Question({
required this.questionDry,
required this.questionType,
required this.options,
required this.answer,
required this.answerRight,
required this.descr,
this.answered = true, //
});
factory Question.fromJson(Map<String, dynamic> json) {
String type = json['QUESTIONTYPE'] as String? ?? '1';
Map<String, String> opts = {};
if (type == '1' || type == '2' || type == '3') {
opts['A'] = json['OPTIONA'] as String? ?? '';
opts['B'] = json['OPTIONB'] as String? ?? '';
if (type != '3') {
opts['C'] = json['OPTIONC'] as String? ?? '';
opts['D'] = json['OPTIOND'] as String? ?? '';
}
}
return Question(
questionDry: json['QUESTIONDRY'] as String? ?? '',
questionType: type,
options: opts,
answer: json['ANSWER'] as String? ?? '',
answerRight: json['ANSWERRIGHT'] as String? ?? json['ANSWER'] as String? ?? '',
descr: json['DESCR'] as String? ?? '',
);
}
}
class _VideoStudyDetailPageState extends State<VideoStudyDetailPage> {
bool loading = true;
List<Question> questions = [];
Map<String, dynamic> paperInfo = {};
int current = 0;
final Map<String, String> questionTypeMap = {
'1': '单选题',
'2': '多选题',
'3': '判断题',
'4': '填空题',
};
@override
void initState() {
super.initState();
_fetchData();
}
Future<void> _fetchData() async {
setState(() => loading = true);
final res = await ApiService.getExamRecordByStuId(widget.studentId, widget.classId);
if (res['result'] == 'success') {
var list = res['varList'] as List;
questions = list.map((e) => Question.fromJson(e)).toList();
//
for (var q in questions) {
q.answered = true;
}
paperInfo = res['paper'] ?? {};
}
setState(() => loading = false);
}
Widget _buildOptions(Question q) {
if (q.questionType == '4') {
return TextField(
controller: TextEditingController(text: q.answer),
readOnly: true,
maxLength: 255,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
);
}
List<String> keys = q.questionType == '3' ? ['A', 'B'] : ['A', 'B', 'C', 'D'];
return Column(
children: keys.map((key) {
bool isChecked = q.answer.split(',').contains(key);
bool isCorrect = q.answerRight.split(',').contains(key);
bool right = q.answered && isCorrect && isChecked;
bool err = q.answered && !isCorrect && isChecked;
bool warn = q.answered && isCorrect && !isChecked;
return Container(
margin: EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
color: right || warn
? Colors.green
: err ? Colors.red : Colors.grey.shade200,
shape: BoxShape.circle,
),
child: Text(
key,
style: TextStyle(
color: (right || err || warn) ? Colors.white : Colors.black87,
),
),
),
SizedBox(width: 16),
Expanded(
child: Text(
q.options[key] ?? '',
style: TextStyle(
color: right
? Colors.green
: err
? Colors.red
: warn
? Colors.green
: Colors.black87,
fontSize: 16,
),
),
),
],
),
);
}).toList(),
);
}
String _renderAnswerText(Question q) {
if (q.questionType == '3') return q.answer == 'A' ? '' : '';
return q.answer;
}
@override
Widget build(BuildContext context) {
final q = questions.isNotEmpty ? questions[current] : null;
return Scaffold(
appBar: MyAppbar(title: '课程练习详情'),
body: loading
? Center(child: CircularProgressIndicator())
: questions.isEmpty
? Center(child: Text('暂无数据'))
: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// &
Stack(
children: [
Container(
width: double.infinity,
height: 100,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/study/bgimg1.png'),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(8),
),
),
Positioned.fill(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'考试科目: ${paperInfo['EXAMNAME']}',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
),
Padding(padding: EdgeInsets.symmetric(horizontal: 15), child: Divider(color: Colors.white30, height: 20,),),
Text(
'当前试题 ${current + 1}/${questions.length}',
style: TextStyle(color: Colors.white, fontSize: 16),
),
],
)
),
),
],
),
SizedBox(height: 16),
//
if (q != null) ...[
Text(
'${current + 1}. ${q.questionDry} ${questionTypeMap[q.questionType]}',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
SizedBox(height: 16),
_buildOptions(q),
Divider(),
Text('我的答案: ${_renderAnswerText(q)}'),
Text('正确答案: ${q.answerRight}'),
Text('权威解读: ${q.descr}'),
],
Spacer(),
//
Row(
children: [
if (current > 0)
Expanded(
child: CustomButton(
text: '上一题',
backgroundColor: Colors.grey.shade200,
textStyle: TextStyle(color: Colors.black54),
onPressed: () => setState(() => current--),
),
),
if (current > 0 && current < questions.length - 1)
SizedBox(width: 16),
if (current < questions.length - 1)
Expanded(
child: CustomButton(
text: '下一题',
backgroundColor: Colors.blue,
onPressed: () => setState(() => current++),
),
),
],
),
],
),
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -64,6 +64,8 @@ dependencies:
camera: ^0.11.2
#富文本查看
flutter_html: ^3.0.0
#pdf、word查看
pdfx: ^2.9.2
dev_dependencies:
@ -96,6 +98,8 @@ flutter:
- assets/js/
- assets/map/
- assets/tabbar/
- assets/study/
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg