学习园地模块完成
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 2.1 KiB |
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
),
|
||||
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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++),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ? '没有更多了' : '',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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++),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
310
pubspec.lock
|
@ -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
|
||||
|
||||
|
|