学习园地模块完成
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):
|
- path_provider_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- pdfx (1.0.0):
|
||||||
|
- Flutter
|
||||||
- photo_manager (3.7.1):
|
- photo_manager (3.7.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
@ -38,6 +40,7 @@ DEPENDENCIES:
|
||||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
|
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
|
- pdfx (from `.symlinks/plugins/pdfx/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/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: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
|
pdfx:
|
||||||
|
:path: ".symlinks/plugins/pdfx/ios"
|
||||||
photo_manager:
|
photo_manager:
|
||||||
:path: ".symlinks/plugins/photo_manager/ios"
|
:path: ".symlinks/plugins/photo_manager/ios"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
|
@ -78,6 +83,7 @@ SPEC CHECKSUMS:
|
||||||
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||||
|
pdfx: 77f4dddc48361fbb01486fa2bdee4532cbb97ef3
|
||||||
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
|
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
|
||||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||||
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
|
||||||
|
|
|
@ -20,6 +20,8 @@ class CustomAlertDialog extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final bool hasCancel = cancelText.trim().isNotEmpty;
|
||||||
|
|
||||||
return Dialog(
|
return Dialog(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
child: Container(
|
child: Container(
|
||||||
|
@ -27,76 +29,107 @@ class CustomAlertDialog extends StatelessWidget {
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
padding: EdgeInsets.zero,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 30),
|
padding: const EdgeInsets.symmetric(horizontal: 30),
|
||||||
child: Text(
|
child: Text(
|
||||||
content,
|
content,
|
||||||
style: const TextStyle(fontSize: 16, color: Colors.black45),
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.black45,
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Row(
|
hasCancel ? _buildDoubleButtons(context) : _buildSingleButton(context),
|
||||||
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 _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(
|
child: Container(
|
||||||
height: height ?? 50, // 默认高度50
|
height: height ?? 50, // 默认高度50
|
||||||
padding: padding ?? const EdgeInsets.all(8), // 默认内边距
|
padding: padding ?? const EdgeInsets.all(8), // 默认内边距
|
||||||
margin: margin ?? const EdgeInsets.symmetric(horizontal: 8), // 默认外边距
|
margin: margin ?? const EdgeInsets.symmetric(horizontal: 5), // 默认外边距
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(borderRadius),
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
color: backgroundColor,
|
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:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'dart:ui';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
@ -38,7 +39,6 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
||||||
super.initState();
|
super.initState();
|
||||||
_startHideTimer();
|
_startHideTimer();
|
||||||
_startPositionTimer();
|
_startPositionTimer();
|
||||||
|
|
||||||
if (widget.controller != null) {
|
if (widget.controller != null) {
|
||||||
widget.controller!.addListener(_controllerListener);
|
widget.controller!.addListener(_controllerListener);
|
||||||
if (widget.controller!.value.isInitialized) {
|
if (widget.controller!.value.isInitialized) {
|
||||||
|
@ -58,19 +58,16 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _controllerListener() {
|
void _controllerListener() {
|
||||||
if (mounted) setState(() {});
|
if (!mounted) return;
|
||||||
|
_updateControllerValues();
|
||||||
if (mounted && widget.controller != null) {
|
|
||||||
_updateControllerValues();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateControllerValues() {
|
void _updateControllerValues() {
|
||||||
final controller = widget.controller!;
|
final c = widget.controller!;
|
||||||
setState(() {
|
setState(() {
|
||||||
_isPlaying = controller.value.isPlaying;
|
_isPlaying = c.value.isPlaying;
|
||||||
_totalDuration = controller.value.duration;
|
_totalDuration = c.value.duration;
|
||||||
_currentPosition = controller.value.position;
|
_currentPosition = c.value.position;
|
||||||
_sliderValue.value = _currentPosition.inMilliseconds.toDouble();
|
_sliderValue.value = _currentPosition.inMilliseconds.toDouble();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -93,14 +90,16 @@ class _VideoPlayerWidgetState extends State<VideoPlayerWidget> {
|
||||||
|
|
||||||
void _startPositionTimer() {
|
void _startPositionTimer() {
|
||||||
_positionTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
|
_positionTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
|
||||||
if (mounted && widget.controller != null && widget.controller!.value.isInitialized) {
|
if (!mounted ||
|
||||||
setState(() {
|
widget.controller == null ||
|
||||||
_currentPosition = widget.controller!.value.position;
|
!widget.controller!.value.isInitialized) return;
|
||||||
_totalDuration = widget.controller!.value.duration;
|
setState(() {
|
||||||
_isPlaying = widget.controller!.value.isPlaying;
|
final c = widget.controller!;
|
||||||
_sliderValue.value = _currentPosition.inMilliseconds.toDouble();
|
_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() {
|
void _togglePlayPause() {
|
||||||
if (widget.controller == null) return;
|
if (widget.controller == null) return;
|
||||||
|
setState(() => _isPlaying = !_isPlaying);
|
||||||
setState(() {
|
if (_isPlaying) widget.controller!.play();
|
||||||
_isPlaying = !_isPlaying;
|
else widget.controller!.pause();
|
||||||
});
|
|
||||||
|
|
||||||
if (_isPlaying) {
|
|
||||||
widget.controller!.play();
|
|
||||||
} else {
|
|
||||||
widget.controller!.pause();
|
|
||||||
}
|
|
||||||
_startHideTimer();
|
_startHideTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _enterFullScreen() {
|
void _enterFullScreen() {
|
||||||
// 锁定横屏
|
|
||||||
SystemChrome.setPreferredOrientations([
|
SystemChrome.setPreferredOrientations([
|
||||||
DeviceOrientation.landscapeLeft,
|
DeviceOrientation.landscapeLeft,
|
||||||
DeviceOrientation.landscapeRight,
|
DeviceOrientation.landscapeRight,
|
||||||
]);
|
]);
|
||||||
// 设置全屏模式
|
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||||
|
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
Navigator.of(context)
|
||||||
builder: (ctx) => Scaffold(
|
.push(
|
||||||
backgroundColor: Colors.black,
|
MaterialPageRoute(
|
||||||
// 使用SafeArea包裹整个全屏播放器
|
builder: (ctx) => Scaffold(
|
||||||
body: SafeArea(
|
backgroundColor: Colors.black,
|
||||||
top: false, // 顶部不使用安全区域(状态栏区域)
|
body: SafeArea(
|
||||||
bottom: false, // 底部不使用安全区域(导航栏区域)
|
top: false,
|
||||||
child: Stack(
|
bottom: false,
|
||||||
children: [
|
child: VideoPlayerWidget(
|
||||||
// 全屏视频播放器
|
controller: widget.controller,
|
||||||
VideoPlayerWidget(
|
coverUrl: widget.coverUrl,
|
||||||
controller: widget.controller,
|
aspectRatio: max(
|
||||||
coverUrl: widget.coverUrl,
|
widget.aspectRatio,
|
||||||
aspectRatio: max(
|
MediaQuery.of(ctx).size.width / MediaQuery.of(ctx).size.height,
|
||||||
widget.aspectRatio,
|
|
||||||
MediaQuery.of(ctx).size.width / MediaQuery.of(ctx).size.height
|
|
||||||
),
|
|
||||||
allowSeek: widget.allowSeek,
|
|
||||||
isFullScreen: true,
|
|
||||||
),
|
),
|
||||||
// 添加退出按钮,并包裹在SafeArea中
|
allowSeek: widget.allowSeek,
|
||||||
SafeArea(
|
isFullScreen: true,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)).then((_) {
|
)
|
||||||
// 恢复竖屏
|
.then((_) {
|
||||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||||
// 恢复系统UI
|
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final screenSize = MediaQuery.of(context).size;
|
final screenW = MediaQuery.of(context).size.width;
|
||||||
final fullScreenAspectRatio = max(
|
final containerW = widget.isFullScreen ? double.infinity : screenW;
|
||||||
widget.aspectRatio,
|
final containerH = widget.isFullScreen
|
||||||
screenSize.width / screenSize.height
|
? double.infinity
|
||||||
);
|
: containerW / widget.aspectRatio;
|
||||||
|
|
||||||
return GestureDetector(
|
return Center(
|
||||||
onTap: _toggleControls,
|
child: SizedBox(
|
||||||
child: Stack(
|
width: containerW,
|
||||||
fit: widget.isFullScreen ? StackFit.expand : StackFit.loose,
|
height: containerH,
|
||||||
children: [
|
child: GestureDetector(
|
||||||
// 视频播放区域
|
behavior: HitTestBehavior.translucent, // ← 允许空白区域也响应
|
||||||
if (widget.controller != null && widget.controller!.value.isInitialized)
|
onTap: _toggleControls,
|
||||||
AspectRatio(
|
child: Stack(
|
||||||
aspectRatio: widget.isFullScreen ? fullScreenAspectRatio : widget.aspectRatio,
|
fit: StackFit.expand,
|
||||||
child: VideoPlayer(widget.controller!),
|
children: [
|
||||||
)
|
// 视频或封面
|
||||||
else
|
if (widget.controller != null &&
|
||||||
Image.network(
|
widget.controller!.value.isInitialized)
|
||||||
widget.coverUrl,
|
FittedBox(
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.contain,
|
||||||
width: widget.isFullScreen ? double.infinity : null,
|
alignment: Alignment.center,
|
||||||
height: widget.isFullScreen ? double.infinity : null,
|
child: SizedBox(
|
||||||
),
|
width: widget.controller!.value.size.width,
|
||||||
|
height: widget.controller!.value.size.height,
|
||||||
// 控制面板
|
child: VideoPlayer(widget.controller!),
|
||||||
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],
|
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
else
|
||||||
child: Row(
|
if (widget.coverUrl.length > 0)
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
Image.network(
|
||||||
children: [
|
widget.coverUrl,
|
||||||
IconButton(
|
fit: BoxFit.cover,
|
||||||
padding: EdgeInsets.zero,
|
width: containerW,
|
||||||
icon: Icon(
|
height: containerH,
|
||||||
_isPlaying ? Icons.pause : Icons.play_arrow,
|
),
|
||||||
size: 28,
|
|
||||||
color: Colors.white,
|
// 控制栏
|
||||||
|
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),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
Expanded(
|
child: Row(
|
||||||
child: ValueListenableBuilder<double>(
|
children: [
|
||||||
valueListenable: _sliderValue,
|
IconButton(
|
||||||
builder: (context, value, child) {
|
padding: EdgeInsets.zero,
|
||||||
return SliderTheme(
|
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(
|
data: SliderTheme.of(context).copyWith(
|
||||||
activeTrackColor: Colors.white,
|
activeTrackColor: Colors.white,
|
||||||
inactiveTrackColor: Colors.white54,
|
inactiveTrackColor: Colors.white54,
|
||||||
thumbColor: Colors.white,
|
thumbColor: Colors.white,
|
||||||
overlayColor: Colors.white24,
|
overlayColor: Colors.white24,
|
||||||
trackHeight: 2,
|
trackHeight: 2,
|
||||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
|
thumbShape: RoundSliderThumbShape(
|
||||||
|
enabledThumbRadius: 8),
|
||||||
),
|
),
|
||||||
child: Slider(
|
child: SliderTheme(
|
||||||
value: value,
|
data: SliderTheme.of(context).copyWith(
|
||||||
min: 0,
|
activeTrackColor: Colors.white, // 活跃轨道颜色
|
||||||
max: _totalDuration.inMilliseconds.toDouble(),
|
inactiveTrackColor: Colors.grey[400],// 非活跃轨道颜色
|
||||||
onChanged: widget.allowSeek && widget.controller != null
|
thumbColor: Colors.white, // 滑块颜色
|
||||||
? (v) {
|
overlayColor: Colors.white.withAlpha(0x33), // 滑块按下外圈
|
||||||
widget.controller!.seekTo(Duration(milliseconds: v.toInt()));
|
disabledActiveTrackColor: Colors.white, // 禁用时也用同样的活跃轨道
|
||||||
setState(() {
|
disabledInactiveTrackColor: Colors.grey[400],
|
||||||
_currentPosition = Duration(milliseconds: v.toInt());
|
disabledThumbColor: Colors.white,
|
||||||
});
|
),
|
||||||
_sliderValue.value = v;
|
child: Slider(
|
||||||
_startHideTimer();
|
value: value,
|
||||||
}
|
min: 0,
|
||||||
: null,
|
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 _formatDuration(Duration d) {
|
||||||
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||||
final hours = d.inHours;
|
final h = d.inHours, m = d.inMinutes.remainder(60), s = d.inSeconds.remainder(60);
|
||||||
final minutes = d.inMinutes.remainder(60);
|
if (h > 0) return '${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}';
|
||||||
final seconds = d.inSeconds.remainder(60);
|
return '${twoDigits(m)}:${twoDigits(s)}';
|
||||||
|
|
||||||
if (hours > 0) {
|
|
||||||
return '${twoDigits(hours)}:${twoDigits(minutes)}:${twoDigits(seconds)}';
|
|
||||||
}
|
|
||||||
return '${twoDigits(minutes)}:${twoDigits(seconds)}';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,6 @@ class ApiService {
|
||||||
static const String projectManagerUrl =
|
static const String projectManagerUrl =
|
||||||
'https://pm.qhdsafety.com/zy-projectManage';
|
'https://pm.qhdsafety.com/zy-projectManage';
|
||||||
|
|
||||||
|
|
||||||
// /// 人脸识别服务
|
// /// 人脸识别服务
|
||||||
// static const String baseFacePath =
|
// static const String baseFacePath =
|
||||||
// "https://qaaqwh.qhdsafety.com/whb_stu_face/";
|
// "https://qaaqwh.qhdsafety.com/whb_stu_face/";
|
||||||
|
@ -293,6 +292,8 @@ U6Hzm1ninpWeE+awIDAQAB
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取视频信息
|
||||||
static Future<Map<String, dynamic>> fnGetVideoPlayInfo(String VIDEOCOURSEWARE_ID) {
|
static Future<Map<String, dynamic>> fnGetVideoPlayInfo(String VIDEOCOURSEWARE_ID) {
|
||||||
return HttpManager().request(
|
return HttpManager().request(
|
||||||
basePath,
|
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
|
_dio.interceptors
|
||||||
..add(LogInterceptor(request: true, responseBody: true, error: true))
|
..add(LogInterceptor(request: true, responseBody: true, error: true))
|
||||||
..add(InterceptorsWrapper(onError: (err, handler) {
|
..add(InterceptorsWrapper(onError: (err, handler) {
|
||||||
|
// TODO 暂不处理
|
||||||
// 捕获401错误
|
// 捕获401错误
|
||||||
if (err.response?.statusCode == 401) {
|
// if (err.response?.statusCode == 401) {
|
||||||
// 触发全局登出回调
|
// // 触发全局登出回调
|
||||||
onUnauthorized?.call();
|
// onUnauthorized?.call();
|
||||||
// 创建自定义异常
|
// // 创建自定义异常
|
||||||
final apiException = ApiException(
|
// final apiException = ApiException(
|
||||||
'提示',
|
// '提示',
|
||||||
'您的账号已在其他设备登录,已自动下线'
|
// '您的账号已在其他设备登录,已自动下线'
|
||||||
);
|
// );
|
||||||
// 直接抛出业务异常,跳过后续错误处理
|
// // 直接抛出业务异常,跳过后续错误处理
|
||||||
return handler.reject(
|
// return handler.reject(
|
||||||
DioException(
|
// DioException(
|
||||||
requestOptions: err.requestOptions,
|
// requestOptions: err.requestOptions,
|
||||||
error: apiException,
|
// error: apiException,
|
||||||
response: err.response,
|
// response: err.response,
|
||||||
type: DioExceptionType.badResponse,
|
// type: DioExceptionType.badResponse,
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
handler.next(err);
|
handler.next(err);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,8 +80,8 @@ class MyApp extends StatelessWidget {
|
||||||
},
|
},
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
dividerTheme: const DividerThemeData(
|
dividerTheme: const DividerThemeData(
|
||||||
color: Color(0xF1F1F1FF),
|
color: Colors.black12,
|
||||||
thickness: 1, // 线高
|
thickness: .5, // 线高
|
||||||
indent: 0, // 左缩进
|
indent: 0, // 左缩进
|
||||||
endIndent: 0, // 右缩进
|
endIndent: 0, // 右缩进
|
||||||
),
|
),
|
||||||
|
@ -97,6 +97,9 @@ class MyApp extends StatelessWidget {
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
progressIndicatorTheme: ProgressIndicatorThemeData(
|
||||||
|
color: Colors.blue, // 统一颜色
|
||||||
|
),
|
||||||
),
|
),
|
||||||
// 根据登录状态决定初始页面
|
// 根据登录状态决定初始页面
|
||||||
home: isLoggedIn ? const MainPage() : const LoginPage(),
|
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/work/danger_wait_list_page.dart';
|
||||||
import 'package:qhd_prevention/pages/home/workSet_page.dart';
|
import 'package:qhd_prevention/pages/home/workSet_page.dart';
|
||||||
|
|
||||||
|
import '../../customWidget/hidden_roll_widget.dart';
|
||||||
import '../../http/ApiService.dart';
|
import '../../http/ApiService.dart';
|
||||||
import '../../tools/tools.dart';
|
import '../../tools/tools.dart';
|
||||||
|
|
||||||
|
@ -77,9 +78,34 @@ class _HomePageState extends State<HomePage> {
|
||||||
_buildWorkSection(context),
|
_buildWorkSection(context),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
ListItemFactory.createBuildSimpleSection("隐患播报"),
|
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),
|
const SizedBox(height: 10),
|
||||||
_buildPCDataSection(),
|
_buildPCDataSection(),
|
||||||
SizedBox(height: 50),
|
SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -130,7 +156,6 @@ class _HomePageState extends State<HomePage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Widget _buildPCDataSection() {
|
Widget _buildPCDataSection() {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
@ -176,20 +201,15 @@ class _HomePageState extends State<HomePage> {
|
||||||
// 你的导航逻辑
|
// 你的导航逻辑
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
pushPage(UserinfoPage(), context);
|
pushPage(UserinfoPage(), context);
|
||||||
}
|
} else if (index == 1) {
|
||||||
else if (index == 1) {
|
|
||||||
pushPage(WorkSetPage(), context);
|
pushPage(WorkSetPage(), context);
|
||||||
}
|
} else if (index == 2) {
|
||||||
else if (index == 2) {
|
|
||||||
pushPage(RiskControlPage(), context);
|
pushPage(RiskControlPage(), context);
|
||||||
}
|
} else if (index == 3) {
|
||||||
else if (index == 3) {
|
|
||||||
pushPage(LowPage(), context);
|
pushPage(LowPage(), context);
|
||||||
}
|
} else if (index == 7) {
|
||||||
else if (index == 7) {
|
|
||||||
pushPage(StudyGardenPage(), context);
|
pushPage(StudyGardenPage(), context);
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
@ -333,13 +353,13 @@ class _HomePageState extends State<HomePage> {
|
||||||
if (index == 1) {
|
if (index == 1) {
|
||||||
pushPage(DangerPage(), context);
|
pushPage(DangerPage(), context);
|
||||||
} else if (index == 2) {
|
} else if (index == 2) {
|
||||||
pushPage(DangerWaitListPage(DangerType.wait,2), context);
|
pushPage(DangerWaitListPage(DangerType.wait, 2), context);
|
||||||
} else if (index == 3) {
|
} else if (index == 3) {
|
||||||
pushPage(DangerWaitListPage(DangerType.expired,3), context);
|
pushPage(DangerWaitListPage(DangerType.expired, 3), context);
|
||||||
} else if (index == 4) {
|
} else if (index == 4) {
|
||||||
pushPage(DangerWaitListPage(DangerType.waitAcceptance,4), context);
|
pushPage(DangerWaitListPage(DangerType.waitAcceptance, 4), context);
|
||||||
} else if (index == 5) {
|
} else if (index == 5) {
|
||||||
pushPage(DangerWaitListPage(DangerType.acceptance,5), context);
|
pushPage(DangerWaitListPage(DangerType.acceptance, 5), context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
|
@ -443,16 +463,17 @@ class _HomePageState extends State<HomePage> {
|
||||||
];
|
];
|
||||||
|
|
||||||
_fetchData(); // 初始化时请求
|
_fetchData(); // 初始化时请求
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchData() async {
|
Future<void> _fetchData() async {
|
||||||
try {
|
try {
|
||||||
// “我的工作” 数量
|
// “我的工作” 数量
|
||||||
final raw = await ApiService.getWork();
|
final raw = await ApiService.getWork();
|
||||||
// 如果拿到的是 String,就 decode;如果本来就是 Map,就直接用
|
// 如果拿到的是 String,就 decode;如果本来就是 Map,就直接用
|
||||||
final Map<String, dynamic> data = raw is String
|
final Map<String, dynamic> data =
|
||||||
? json.decode(raw as String) as Map<String, dynamic>
|
raw is String
|
||||||
: raw;
|
? json.decode(raw as String) as Map<String, dynamic>
|
||||||
|
: raw;
|
||||||
|
|
||||||
final hidCount = data['hidCount'] as Map<String, dynamic>;
|
final hidCount = data['hidCount'] as Map<String, dynamic>;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -488,17 +509,18 @@ class _HomePageState extends State<HomePage> {
|
||||||
"num": (hidCount['yys'] ?? 0).toString(),
|
"num": (hidCount['yys'] ?? 0).toString(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
});
|
});
|
||||||
// 安全检查数
|
// 安全检查数
|
||||||
final checkJson =
|
final checkJson =
|
||||||
await ApiService.getSafetyEnvironmentalInspectionCount();
|
await ApiService.getSafetyEnvironmentalInspectionCount();
|
||||||
setState(() {
|
setState(() {
|
||||||
int confirmCount = checkJson['confirmCount']['confirmCount'];
|
int confirmCount = checkJson['confirmCount']['confirmCount'];
|
||||||
int repulseCount = checkJson['repulseCount']['repulseCount'];
|
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;
|
_eight_work_count += (item ?? 0) as int;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 出错时可以 Toast 或者在页面上显示错误状态
|
// 出错时可以 Toast 或者在页面上显示错误状态
|
||||||
print('加载首页数据失败:$e');
|
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:qhd_prevention/tools/tools.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
import '../../../customWidget/toast_util.dart';
|
||||||
import '../../../customWidget/video_player_widget.dart';
|
import '../../../customWidget/video_player_widget.dart';
|
||||||
import '../../../http/HttpManager.dart';
|
import '../../../http/HttpManager.dart';
|
||||||
import 'face_ecognition_page.dart';
|
import 'face_ecognition_page.dart';
|
||||||
|
|
||||||
|
enum TakeExamType {
|
||||||
|
video_study,
|
||||||
|
strengththen,
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
class StudyDetailPage extends StatefulWidget {
|
class StudyDetailPage extends StatefulWidget {
|
||||||
final Map studyDetailDetail;
|
final Map studyDetailDetail;
|
||||||
final String studentId;
|
final String studentId;
|
||||||
|
@ -163,7 +170,7 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
||||||
// document
|
// document
|
||||||
if (data['VIDEOFILES'] != null) {
|
if (data['VIDEOFILES'] != null) {
|
||||||
await pushPage(
|
await pushPage(
|
||||||
StudyPractisePage(data['VIDEOCOURSEWARE_ID']),
|
StudyPractisePage(videoCoursewareId: data['VIDEOCOURSEWARE_ID']),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
await _submitPlayTime(
|
await _submitPlayTime(
|
||||||
|
@ -212,20 +219,35 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
||||||
_classId,
|
_classId,
|
||||||
widget.studentId,
|
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?.removeListener(_onTimeUpdate);
|
||||||
_videoController?.dispose();
|
_videoController?.dispose();
|
||||||
|
|
||||||
|
// 创建新 controller
|
||||||
_videoController = VideoPlayerController.networkUrl(Uri.parse(url));
|
_videoController = VideoPlayerController.networkUrl(Uri.parse(url));
|
||||||
await _videoController!.initialize();
|
await _videoController!.initialize();
|
||||||
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|
||||||
|
// 直接从上次播放点 seek,并立即播放
|
||||||
_videoController!
|
_videoController!
|
||||||
..seekTo(Duration(seconds: seen))
|
..seekTo(Duration(seconds: seen))
|
||||||
..play()
|
..play()
|
||||||
..addListener(_onTimeUpdate);
|
..addListener(_onTimeUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _onTimeUpdate() {
|
void _onTimeUpdate() {
|
||||||
if (_videoController == null || !_videoController!.value.isPlaying) return;
|
if (_videoController == null || !_videoController!.value.isPlaying) return;
|
||||||
final curr = _videoController!.value.position;
|
final curr = _videoController!.value.position;
|
||||||
|
@ -251,7 +273,7 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
||||||
if (_currentVideoData == null) return;
|
if (_currentVideoData == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final pd = (await ApiService.fnSubmitPlayTime(
|
final resData = (await ApiService.fnSubmitPlayTime(
|
||||||
_currentVideoData!['VIDEOCOURSEWARE_ID'],
|
_currentVideoData!['VIDEOCOURSEWARE_ID'],
|
||||||
_currentVideoData!['CURRICULUM_ID'],
|
_currentVideoData!['CURRICULUM_ID'],
|
||||||
end ? '1' : '0',
|
end ? '1' : '0',
|
||||||
|
@ -260,8 +282,8 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
||||||
widget.studentId,
|
widget.studentId,
|
||||||
_classCurriculumId,
|
_classCurriculumId,
|
||||||
_classId,
|
_classId,
|
||||||
))['pd']!;
|
));
|
||||||
|
final pd = resData['pd'] ?? {};
|
||||||
// 更新进度显示
|
// 更新进度显示
|
||||||
final comp = pd['PLAYCOUNT'] != null && pd['PLAYCOUNT'] > 0;
|
final comp = pd['PLAYCOUNT'] != null && pd['PLAYCOUNT'] > 0;
|
||||||
final resT = pd['RESOURCETIME'] ?? seconds;
|
final resT = pd['RESOURCETIME'] ?? seconds;
|
||||||
|
@ -293,14 +315,8 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
||||||
) ??
|
) ??
|
||||||
false;
|
false;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
final arguments = {
|
|
||||||
'STAGEEXAMPAPERINPUT_ID':
|
_startExam(resData);
|
||||||
pd['paper']['STAGEEXAMPAPERINPUT_ID'],
|
|
||||||
'CLASS_ID': _classId,
|
|
||||||
'STUDENT_ID': widget.studentId,
|
|
||||||
'NUMBEROFEXAMS': pd['NUMBEROFEXAMS'],
|
|
||||||
};
|
|
||||||
pushPage(TakeExamPage(arguments), context);
|
|
||||||
} else {
|
} else {
|
||||||
_videoController?.play();
|
_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() {
|
void _startFaceTimer() {
|
||||||
_faceTimer = Timer.periodic(Duration(seconds: _faceTime), (_) async {
|
_faceTimer = Timer.periodic(Duration(seconds: _faceTime), (_) async {
|
||||||
|
@ -362,7 +412,9 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 250,
|
height: 250,
|
||||||
|
width: screenWidth(context),
|
||||||
child: VideoPlayerWidget(
|
child: VideoPlayerWidget(
|
||||||
|
allowSeek: false,
|
||||||
controller: _videoController,
|
controller: _videoController,
|
||||||
coverUrl: _videoCoverUrl.isNotEmpty
|
coverUrl: _videoCoverUrl.isNotEmpty
|
||||||
? ApiService.baseImgPath + _videoCoverUrl
|
? ApiService.baseImgPath + _videoCoverUrl
|
||||||
|
@ -493,7 +545,7 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
||||||
CustomButton(
|
CustomButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
() => pushPage(
|
() => pushPage(
|
||||||
StudyPractisePage(m['VIDEOCOURSEWARE_ID']),
|
StudyPractisePage(videoCoursewareId: m['VIDEOCOURSEWARE_ID']),
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
text: "课后练习",
|
text: "课后练习",
|
||||||
|
@ -538,4 +590,3 @@ class _StudyDetailPageState extends State<StudyDetailPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,13 @@ import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:qhd_prevention/customWidget/custom_button.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_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 'package:qhd_prevention/tools/tools.dart';
|
||||||
|
import '../../../customWidget/toast_util.dart';
|
||||||
import '../../../http/ApiService.dart';
|
import '../../../http/ApiService.dart';
|
||||||
import '../../mine/mine_sign_page.dart';
|
import '../../mine/mine_sign_page.dart';
|
||||||
import '../../my_appbar.dart';
|
import '../../my_appbar.dart';
|
||||||
|
@ -184,7 +189,7 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
|
||||||
final int studyState = int.tryParse(item['STUDYSTATE'] ?? '') ?? 0;
|
final int studyState = int.tryParse(item['STUDYSTATE'] ?? '') ?? 0;
|
||||||
final int stageExamState = int.tryParse(item['STAGEEXAMSTATE'] ?? '') ?? 0;
|
final int stageExamState = int.tryParse(item['STAGEEXAMSTATE'] ?? '') ?? 0;
|
||||||
final int strengthenExamState =
|
final int strengthenExamState =
|
||||||
int.tryParse(item['STRENGTHENEXAMSTATE'] ?? '') ?? 0;
|
int.tryParse(item['STRENGTHENEXAMSTATE'] ?? '') ?? -1;
|
||||||
final int numberOfExams = int.tryParse('${item['NUMBEROFEXAMS']}') ?? 0;
|
final int numberOfExams = int.tryParse('${item['NUMBEROFEXAMS']}') ?? 0;
|
||||||
final int ksCount = int.tryParse('${item['ksCount']}') ?? 0;
|
final int ksCount = int.tryParse('${item['ksCount']}') ?? 0;
|
||||||
final int examinationFlag =
|
final int examinationFlag =
|
||||||
|
@ -194,7 +199,7 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
|
||||||
final String isStrengthen = item['ISSTRENGTHEN'] ?? '0';
|
final String isStrengthen = item['ISSTRENGTHEN'] ?? '0';
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
@ -244,24 +249,8 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 10,
|
||||||
children: [
|
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 &&
|
if (studyState >= 2 &&
|
||||||
stageExamState >= 2 &&
|
stageExamState >= 2 &&
|
||||||
|
@ -270,18 +259,17 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
|
||||||
CustomButton(
|
CustomButton(
|
||||||
height: 36,
|
height: 36,
|
||||||
text: "加强学习",
|
text: "加强学习",
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
padding: EdgeInsets.symmetric(horizontal: 18),
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: Colors.blue,
|
||||||
onPressed:
|
onPressed:
|
||||||
() => Navigator.pushNamed(
|
() => pushPage(
|
||||||
|
StrengthenStudyPage(
|
||||||
|
classId: item['CLASS_ID'] ?? '',
|
||||||
|
postId: item['POST_ID'] ?? '',
|
||||||
|
studentId: item['STUDENT_ID'] ?? '',
|
||||||
|
),
|
||||||
context,
|
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(
|
CustomButton(
|
||||||
height: 36,
|
height: 36,
|
||||||
text: "立即学习",
|
text: "立即学习",
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
padding: EdgeInsets.symmetric(horizontal: 18),
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
backgroundColor: Colors.blue,
|
backgroundColor: Colors.blue,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@ -311,23 +299,29 @@ class _StudyMyTaskPageState extends State<StudyMyTaskPage> {
|
||||||
CustomButton(
|
CustomButton(
|
||||||
height: 36,
|
height: 36,
|
||||||
text: "立即考试",
|
text: "立即考试",
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
padding: EdgeInsets.symmetric(horizontal: 18),
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
onPressed:
|
onPressed:
|
||||||
() => Navigator.pushNamed(
|
() => _startExam(item, TakeExamType.video_study),
|
||||||
context,
|
),
|
||||||
'/course_exam',
|
// 考试详情
|
||||||
arguments: {
|
if (stageExamState == 3)
|
||||||
'STAGEEXAMPAPERINPUT_ID':
|
CustomButton(
|
||||||
item['STAGEEXAMPAPERINPUT_ID'],
|
height: 36,
|
||||||
'CLASS_ID': item['CLASS_ID'],
|
text: "考试详情",
|
||||||
'POST_ID': item['POST_ID'],
|
padding: EdgeInsets.symmetric(horizontal: 18),
|
||||||
'STUDENT_ID': item['STUDENT_ID'],
|
borderRadius: 18,
|
||||||
'NUMBEROFEXAMS': numberOfExams,
|
backgroundColor: Colors.green,
|
||||||
'entrySite': 'list',
|
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) {
|
bool _onScroll(ScrollNotification n) {
|
||||||
if (n.metrics.pixels > n.metrics.maxScrollExtent - 100 &&
|
if (n.metrics.pixels > n.metrics.maxScrollExtent - 100 &&
|
||||||
|
|
|
@ -1,19 +1,330 @@
|
||||||
import 'package:flutter/material.dart';
|
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 'package:qhd_prevention/pages/my_appbar.dart';
|
||||||
|
import '../../../http/ApiService.dart'; // 替换为实际路径
|
||||||
|
|
||||||
class StudyPractisePage extends StatefulWidget {
|
class StudyPractisePage extends StatefulWidget {
|
||||||
const StudyPractisePage(this.VIDEOCOURSEWARE_ID,{super.key});
|
final String videoCoursewareId;
|
||||||
final String VIDEOCOURSEWARE_ID;
|
|
||||||
|
const StudyPractisePage({Key? key, required this.videoCoursewareId})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StudyPractisePage> createState() => _StudyPractisePageState();
|
_PracticePageState createState() => _PracticePageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StudyPractisePageState extends State<StudyPractisePage> {
|
class Question {
|
||||||
@override
|
final String questionDry;
|
||||||
Widget build(BuildContext context) {
|
final String questionType; // '1','2','3','4'
|
||||||
return Scaffold(
|
final Map<String, String> options;
|
||||||
appBar: MyAppbar(title: "课后练习"),
|
final String answer;
|
||||||
body: SizedBox(),
|
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 {
|
class StudyScorePage extends StatefulWidget {
|
||||||
const StudyScorePage({super.key});
|
const StudyScorePage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StudyScorePage> createState() => _StudyScorePageState();
|
_StudyScorePageState createState() => _StudyScorePageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StudyScorePageState extends State<StudyScorePage> {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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: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/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 {
|
class TakeExamPage extends StatefulWidget {
|
||||||
const TakeExamPage(this.arguments,{super.key});
|
const TakeExamPage({
|
||||||
final Map<String, dynamic> arguments;
|
required this.examInfo,
|
||||||
|
required this.examType,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Map<String, dynamic> examInfo;
|
||||||
|
final TakeExamType examType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TakeExamPage> createState() => _TakeExamPageState();
|
State<TakeExamPage> createState() => _TakeExamPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TakeExamPageState extends State<TakeExamPage> {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
final q = questions.isNotEmpty ? questions[current] : null;
|
||||||
appBar: MyAppbar(title: '开始考试'),
|
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
|
camera: ^0.11.2
|
||||||
#富文本查看
|
#富文本查看
|
||||||
flutter_html: ^3.0.0
|
flutter_html: ^3.0.0
|
||||||
|
#pdf、word查看
|
||||||
|
pdfx: ^2.9.2
|
||||||
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
@ -96,6 +98,8 @@ flutter:
|
||||||
- assets/js/
|
- assets/js/
|
||||||
- assets/map/
|
- assets/map/
|
||||||
- assets/tabbar/
|
- assets/tabbar/
|
||||||
|
- assets/study/
|
||||||
|
|
||||||
# - images/a_dot_burr.jpeg
|
# - images/a_dot_burr.jpeg
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
|