diff --git a/assets/study/copy-one.png b/assets/study/copy-one.png index e69de29..e8b05db 100644 Binary files a/assets/study/copy-one.png and b/assets/study/copy-one.png differ diff --git a/assets/study/err.png b/assets/study/err.png index e69de29..ef99366 100644 Binary files a/assets/study/err.png and b/assets/study/err.png differ diff --git a/assets/study/play.png b/assets/study/play.png index e69de29..ea6e626 100644 Binary files a/assets/study/play.png and b/assets/study/play.png differ diff --git a/assets/study/right.png b/assets/study/right.png index e69de29..0fa67d7 100644 Binary files a/assets/study/right.png and b/assets/study/right.png differ diff --git a/assets/study/time.png b/assets/study/time.png index e69de29..9510c6b 100644 Binary files a/assets/study/time.png and b/assets/study/time.png differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b73d0e7..b165f4c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -16,6 +16,8 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - pdfx (1.0.0): + - Flutter - photo_manager (3.7.1): - Flutter - FlutterMacOS @@ -38,6 +40,7 @@ DEPENDENCIES: - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - pdfx (from `.symlinks/plugins/pdfx/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) @@ -60,6 +63,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + pdfx: + :path: ".symlinks/plugins/pdfx/ios" photo_manager: :path: ".symlinks/plugins/photo_manager/ios" shared_preferences_foundation: @@ -78,6 +83,7 @@ SPEC CHECKSUMS: mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + pdfx: 77f4dddc48361fbb01486fa2bdee4532cbb97ef3 photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b diff --git a/lib/customWidget/custom_alert_dialog.dart b/lib/customWidget/custom_alert_dialog.dart index 1ecff54..6da2b87 100644 --- a/lib/customWidget/custom_alert_dialog.dart +++ b/lib/customWidget/custom_alert_dialog.dart @@ -20,6 +20,8 @@ class CustomAlertDialog extends StatelessWidget { @override Widget build(BuildContext context) { + final bool hasCancel = cancelText.trim().isNotEmpty; + return Dialog( backgroundColor: Colors.transparent, child: Container( @@ -27,76 +29,107 @@ class CustomAlertDialog extends StatelessWidget { color: Colors.white, borderRadius: BorderRadius.circular(5), ), - padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), + padding: EdgeInsets.zero, child: Column( mainAxisSize: MainAxisSize.min, children: [ - SizedBox(height: 20), + const SizedBox(height: 20), Text( title, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), textAlign: TextAlign.center, ), const SizedBox(height: 20), Padding( - padding: EdgeInsets.symmetric(horizontal: 30), + padding: const EdgeInsets.symmetric(horizontal: 30), child: Text( content, - style: const TextStyle(fontSize: 16, color: Colors.black45), + style: const TextStyle( + fontSize: 16, + color: Colors.black45, + ), textAlign: TextAlign.center, ), ), - const SizedBox(height: 20), const Divider(height: 1), - Row( - children: [ - Expanded( - child: InkWell( - onTap: () { - Navigator.of(context).pop(); - onCancel?.call(); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - alignment: Alignment.center, - child: Text( - cancelText, - style: const TextStyle( - fontWeight: FontWeight.w500, - color: Colors.black, - fontSize: 18, - ), - ), - ), - ), - ), - Container(width: 1, height: 48, color: Colors.grey[300]), - Expanded( - child: InkWell( - onTap: () { - Navigator.of(context).pop(); - onConfirm?.call(); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - alignment: Alignment.center, - child: Text( - confirmText, - style: const TextStyle( - color: Color(0xFF3874F6), - fontWeight: FontWeight.w500, - fontSize: 18, - ), - ), - ), - ), - ), - ], - ), + hasCancel ? _buildDoubleButtons(context) : _buildSingleButton(context), ], ), ), ); } + + Widget _buildDoubleButtons(BuildContext context) { + return Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + onCancel?.call(); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + alignment: Alignment.center, + child: Text( + cancelText, + style: const TextStyle( + fontWeight: FontWeight.w500, + color: Colors.black, + fontSize: 18, + ), + ), + ), + ), + ), + Container(width: 1, height: 48, color: Colors.grey[300]), + Expanded( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + onConfirm?.call(); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + alignment: Alignment.center, + child: Text( + confirmText, + style: const TextStyle( + color: Color(0xFF3874F6), // 蓝色字体 + fontWeight: FontWeight.w500, + fontSize: 18, + ), + ), + ), + ), + ), + ], + ); + } + + Widget _buildSingleButton(BuildContext context) { + return InkWell( + onTap: () { + Navigator.of(context).pop(); + onConfirm?.call(); + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + alignment: Alignment.center, + child: Text( + confirmText, + style: const TextStyle( + color: Color(0xFF3874F6), // 蓝色字体 + fontWeight: FontWeight.w500, + fontSize: 18, + ), + ), + ), + ); + } } diff --git a/lib/customWidget/custom_button.dart b/lib/customWidget/custom_button.dart index f41e432..8fe6aa6 100644 --- a/lib/customWidget/custom_button.dart +++ b/lib/customWidget/custom_button.dart @@ -30,7 +30,7 @@ class CustomButton extends StatelessWidget { child: Container( height: height ?? 50, // 默认高度50 padding: padding ?? const EdgeInsets.all(8), // 默认内边距 - margin: margin ?? const EdgeInsets.symmetric(horizontal: 8), // 默认外边距 + margin: margin ?? const EdgeInsets.symmetric(horizontal: 5), // 默认外边距 decoration: BoxDecoration( borderRadius: BorderRadius.circular(borderRadius), color: backgroundColor, diff --git a/lib/customWidget/hidden_roll_widget.dart b/lib/customWidget/hidden_roll_widget.dart new file mode 100644 index 0000000..0d7f4b8 --- /dev/null +++ b/lib/customWidget/hidden_roll_widget.dart @@ -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> hiddenList; + /// 每行高度 + final double rowHeight; + /// 同一时间可见的行数 + final int visibleCount; + /// 滚动间隔 + final Duration interval; + /// 点击回调,传递 HIDDEN_ID + final ValueChanged? 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 { + 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, + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/customWidget/remote_file_page.dart b/lib/customWidget/remote_file_page.dart new file mode 100644 index 0000000..9604fa6 --- /dev/null +++ b/lib/customWidget/remote_file_page.dart @@ -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 { + 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 _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>( + 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, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/customWidget/video_player_widget.dart b/lib/customWidget/video_player_widget.dart index a667e71..dfc13a8 100644 --- a/lib/customWidget/video_player_widget.dart +++ b/lib/customWidget/video_player_widget.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math'; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_player/video_player.dart'; @@ -38,7 +39,6 @@ class _VideoPlayerWidgetState extends State { super.initState(); _startHideTimer(); _startPositionTimer(); - if (widget.controller != null) { widget.controller!.addListener(_controllerListener); if (widget.controller!.value.isInitialized) { @@ -58,19 +58,16 @@ class _VideoPlayerWidgetState extends State { } void _controllerListener() { - if (mounted) setState(() {}); - - if (mounted && widget.controller != null) { - _updateControllerValues(); - } + if (!mounted) return; + _updateControllerValues(); } void _updateControllerValues() { - final controller = widget.controller!; + final c = widget.controller!; setState(() { - _isPlaying = controller.value.isPlaying; - _totalDuration = controller.value.duration; - _currentPosition = controller.value.position; + _isPlaying = c.value.isPlaying; + _totalDuration = c.value.duration; + _currentPosition = c.value.position; _sliderValue.value = _currentPosition.inMilliseconds.toDouble(); }); } @@ -93,14 +90,16 @@ class _VideoPlayerWidgetState extends State { void _startPositionTimer() { _positionTimer = Timer.periodic(const Duration(milliseconds: 200), (_) { - if (mounted && widget.controller != null && widget.controller!.value.isInitialized) { - setState(() { - _currentPosition = widget.controller!.value.position; - _totalDuration = widget.controller!.value.duration; - _isPlaying = widget.controller!.value.isPlaying; - _sliderValue.value = _currentPosition.inMilliseconds.toDouble(); - }); - } + if (!mounted || + widget.controller == null || + !widget.controller!.value.isInitialized) return; + setState(() { + final c = widget.controller!; + _currentPosition = c.value.position; + _totalDuration = c.value.duration; + _isPlaying = c.value.isPlaying; + _sliderValue.value = _currentPosition.inMilliseconds.toDouble(); + }); }); } @@ -112,215 +111,198 @@ class _VideoPlayerWidgetState extends State { void _togglePlayPause() { if (widget.controller == null) return; - - setState(() { - _isPlaying = !_isPlaying; - }); - - if (_isPlaying) { - widget.controller!.play(); - } else { - widget.controller!.pause(); - } + setState(() => _isPlaying = !_isPlaying); + if (_isPlaying) widget.controller!.play(); + else widget.controller!.pause(); _startHideTimer(); } void _enterFullScreen() { - // 锁定横屏 SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); - // 设置全屏模式 SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - Navigator.of(context).push(MaterialPageRoute( - builder: (ctx) => Scaffold( - backgroundColor: Colors.black, - // 使用SafeArea包裹整个全屏播放器 - body: SafeArea( - top: false, // 顶部不使用安全区域(状态栏区域) - bottom: false, // 底部不使用安全区域(导航栏区域) - child: Stack( - children: [ - // 全屏视频播放器 - VideoPlayerWidget( - controller: widget.controller, - coverUrl: widget.coverUrl, - aspectRatio: max( - widget.aspectRatio, - MediaQuery.of(ctx).size.width / MediaQuery.of(ctx).size.height - ), - allowSeek: widget.allowSeek, - isFullScreen: true, + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (ctx) => Scaffold( + backgroundColor: Colors.black, + body: SafeArea( + top: false, + bottom: false, + child: VideoPlayerWidget( + controller: widget.controller, + coverUrl: widget.coverUrl, + aspectRatio: max( + widget.aspectRatio, + MediaQuery.of(ctx).size.width / MediaQuery.of(ctx).size.height, ), - // 添加退出按钮,并包裹在SafeArea中 - SafeArea( - child: Align( - alignment: Alignment.topLeft, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: Colors.black38, - shape: BoxShape.circle, - ), - child: const Icon( - Icons.arrow_back, - color: Colors.white, - size: 20, - ), - ), - ), - ), - ), - ), - ], + allowSeek: widget.allowSeek, + isFullScreen: true, + ), ), ), ), - )).then((_) { - // 恢复竖屏 + ) + .then((_) { SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - // 恢复系统UI SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); }); } @override Widget build(BuildContext context) { - final screenSize = MediaQuery.of(context).size; - final fullScreenAspectRatio = max( - widget.aspectRatio, - screenSize.width / screenSize.height - ); + final screenW = MediaQuery.of(context).size.width; + final containerW = widget.isFullScreen ? double.infinity : screenW; + final containerH = widget.isFullScreen + ? double.infinity + : containerW / widget.aspectRatio; - return GestureDetector( - onTap: _toggleControls, - child: Stack( - fit: widget.isFullScreen ? StackFit.expand : StackFit.loose, - children: [ - // 视频播放区域 - if (widget.controller != null && widget.controller!.value.isInitialized) - AspectRatio( - aspectRatio: widget.isFullScreen ? fullScreenAspectRatio : widget.aspectRatio, - child: VideoPlayer(widget.controller!), - ) - else - Image.network( - widget.coverUrl, - fit: BoxFit.cover, - width: widget.isFullScreen ? double.infinity : null, - height: widget.isFullScreen ? double.infinity : null, - ), - - // 控制面板 - if (_visibleControls) - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - height: 50, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.bottomCenter, - end: Alignment.topCenter, - colors: [Colors.black.withOpacity(0.7), Colors.transparent], + return Center( + child: SizedBox( + width: containerW, + height: containerH, + child: GestureDetector( + behavior: HitTestBehavior.translucent, // ← 允许空白区域也响应 + onTap: _toggleControls, + child: Stack( + fit: StackFit.expand, + children: [ + // 视频或封面 + if (widget.controller != null && + widget.controller!.value.isInitialized) + FittedBox( + fit: BoxFit.contain, + alignment: Alignment.center, + child: SizedBox( + width: widget.controller!.value.size.width, + height: widget.controller!.value.size.height, + child: VideoPlayer(widget.controller!), ), - ), - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - padding: EdgeInsets.zero, - icon: Icon( - _isPlaying ? Icons.pause : Icons.play_arrow, - size: 28, - color: Colors.white, + ) + else + if (widget.coverUrl.length > 0) + Image.network( + widget.coverUrl, + fit: BoxFit.cover, + width: containerW, + height: containerH, + ), + + // 控制栏 + if (_visibleControls) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withOpacity(0.7), + Colors.transparent + ], ), - onPressed: _togglePlayPause, ), - const SizedBox(width: 0), - Expanded( - child: ValueListenableBuilder( - valueListenable: _sliderValue, - builder: (context, value, child) { - return SliderTheme( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + IconButton( + padding: EdgeInsets.zero, + icon: Icon( + _isPlaying ? Icons.pause : Icons.play_arrow, + size: 28, + color: Colors.white, + ), + onPressed: _togglePlayPause, + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: _sliderValue, + builder: (_, value, __) => SliderTheme( data: SliderTheme.of(context).copyWith( activeTrackColor: Colors.white, inactiveTrackColor: Colors.white54, thumbColor: Colors.white, overlayColor: Colors.white24, trackHeight: 2, - thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8), + thumbShape: RoundSliderThumbShape( + enabledThumbRadius: 8), ), - child: Slider( - value: value, - min: 0, - max: _totalDuration.inMilliseconds.toDouble(), - onChanged: widget.allowSeek && widget.controller != null - ? (v) { - widget.controller!.seekTo(Duration(milliseconds: v.toInt())); - setState(() { - _currentPosition = Duration(milliseconds: v.toInt()); - }); - _sliderValue.value = v; - _startHideTimer(); - } - : null, + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: Colors.white, // 活跃轨道颜色 + inactiveTrackColor: Colors.grey[400],// 非活跃轨道颜色 + thumbColor: Colors.white, // 滑块颜色 + overlayColor: Colors.white.withAlpha(0x33), // 滑块按下外圈 + disabledActiveTrackColor: Colors.white, // 禁用时也用同样的活跃轨道 + disabledInactiveTrackColor: Colors.grey[400], + disabledThumbColor: Colors.white, + ), + child: Slider( + value: value, + min: 0, + max: _totalDuration.inMilliseconds.toDouble(), + // 不管 allowSeek 如何,都不改变 onChanged + onChanged: (v) { + if (widget.allowSeek && widget.controller != null) { + widget.controller!.seekTo(Duration(milliseconds: v.toInt())); + setState(() => _currentPosition = Duration(milliseconds: v.toInt())); + _sliderValue.value = v; + _startHideTimer(); + } + }, + ), ), - ); - } - ), - ), - const SizedBox(width: 0), - // 使用固定宽度的文本容器,避免进度条跳动 - SizedBox( - width: 110, // 固定宽度,防止进度条长度变化 - child: Text( - '${_formatDuration(_currentPosition)} / ${_formatDuration(_totalDuration)}', - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontFeatures: [FontFeature.tabularFigures()], // 等宽字体 + ), + ), ), - ), + SizedBox( + width: 110, + child: Text( + '${_formatDuration(_currentPosition)} / ${_formatDuration(_totalDuration)}', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ), + IconButton( + padding: EdgeInsets.zero, + icon: Icon( + widget.isFullScreen + ? Icons.fullscreen_exit + : Icons.fullscreen, + size: 28, + color: Colors.white, + ), + onPressed: () { + widget.isFullScreen + ? Navigator.of(context).pop() + : _enterFullScreen(); + }, + ), + ], ), - const SizedBox(width: 0), - IconButton( - padding: EdgeInsets.zero, - icon: Icon( - widget.isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen, - size: 28, - color: Colors.white, - ), - onPressed: () { - widget.isFullScreen ? Navigator.of(context).pop() : _enterFullScreen(); - }, - ), - ], + ), ), - ), - ), - ], + ], + ), + ), ), ); } String _formatDuration(Duration d) { String twoDigits(int n) => n.toString().padLeft(2, '0'); - final hours = d.inHours; - final minutes = d.inMinutes.remainder(60); - final seconds = d.inSeconds.remainder(60); - - if (hours > 0) { - return '${twoDigits(hours)}:${twoDigits(minutes)}:${twoDigits(seconds)}'; - } - return '${twoDigits(minutes)}:${twoDigits(seconds)}'; + final h = d.inHours, m = d.inMinutes.remainder(60), s = d.inSeconds.remainder(60); + if (h > 0) return '${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}'; + return '${twoDigits(m)}:${twoDigits(s)}'; } -} \ No newline at end of file +} diff --git a/lib/http/ApiService.dart b/lib/http/ApiService.dart index db14d4f..4c3916e 100644 --- a/lib/http/ApiService.dart +++ b/lib/http/ApiService.dart @@ -35,7 +35,6 @@ class ApiService { static const String projectManagerUrl = 'https://pm.qhdsafety.com/zy-projectManage'; - // /// 人脸识别服务 // static const String baseFacePath = // "https://qaaqwh.qhdsafety.com/whb_stu_face/"; @@ -293,6 +292,8 @@ U6Hzm1ninpWeE+awIDAQAB }, ); } + + /// 获取视频信息 static Future> fnGetVideoPlayInfo(String VIDEOCOURSEWARE_ID) { return HttpManager().request( basePath, @@ -430,6 +431,91 @@ U6Hzm1ninpWeE+awIDAQAB ); } + /// 视频练习 + static Future> questionListByVideo(String VIDEOCOURSEWARE_ID) { + return HttpManager().request( + basePath, + '/app/edu/question/listAllByVideo', + method: Method.post, + data: { + 'VIDEOCOURSEWARE_ID':VIDEOCOURSEWARE_ID, + }, + ); + } + + /// 成绩查询 + static Future> 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> 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> getStartExam(Map data) { + return HttpManager().request( + basePath, + '/app/edu/stageexam/getExam', + method: Method.post, + data: { + ...data + }, + ); + } + /// 开始加强考试 + static Future> getStartStrengthenExam(Map data) { + return HttpManager().request( + basePath, + '/app/edu/stageexam/getStrengthenExam', + method: Method.post, + data: { + ...data + }, + ); + } + /// 加强学习视频 + static Future> getListStrengthenVideo(Map data) { + return HttpManager().request( + basePath, + '/app/edu/stagestudentrelation/listStrengthenVideo', + method: Method.post, + data: { + ...data + }, + ); + } + /// 考试提交 + static Future> 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 + }, + ); + } diff --git a/lib/http/HttpManager.dart b/lib/http/HttpManager.dart index a4d8237..ae27331 100644 --- a/lib/http/HttpManager.dart +++ b/lib/http/HttpManager.dart @@ -39,25 +39,26 @@ class HttpManager { _dio.interceptors ..add(LogInterceptor(request: true, responseBody: true, error: true)) ..add(InterceptorsWrapper(onError: (err, handler) { + // TODO 暂不处理 // 捕获401错误 - if (err.response?.statusCode == 401) { - // 触发全局登出回调 - onUnauthorized?.call(); - // 创建自定义异常 - final apiException = ApiException( - '提示', - '您的账号已在其他设备登录,已自动下线' - ); - // 直接抛出业务异常,跳过后续错误处理 - return handler.reject( - DioException( - requestOptions: err.requestOptions, - error: apiException, - response: err.response, - type: DioExceptionType.badResponse, - ), - ); - } + // if (err.response?.statusCode == 401) { + // // 触发全局登出回调 + // onUnauthorized?.call(); + // // 创建自定义异常 + // final apiException = ApiException( + // '提示', + // '您的账号已在其他设备登录,已自动下线' + // ); + // // 直接抛出业务异常,跳过后续错误处理 + // return handler.reject( + // DioException( + // requestOptions: err.requestOptions, + // error: apiException, + // response: err.response, + // type: DioExceptionType.badResponse, + // ), + // ); + // } handler.next(err); })); } diff --git a/lib/main.dart b/lib/main.dart index 90f249f..7060140 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -80,8 +80,8 @@ class MyApp extends StatelessWidget { }, theme: ThemeData( dividerTheme: const DividerThemeData( - color: Color(0xF1F1F1FF), - thickness: 1, // 线高 + color: Colors.black12, + thickness: .5, // 线高 indent: 0, // 左缩进 endIndent: 0, // 右缩进 ), @@ -97,6 +97,9 @@ class MyApp extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(8)), ), ), + progressIndicatorTheme: ProgressIndicatorThemeData( + color: Colors.blue, // 统一颜色 + ), ), // 根据登录状态决定初始页面 home: isLoggedIn ? const MainPage() : const LoginPage(), diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 763106f..c87f8da 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -12,6 +12,7 @@ import 'package:qhd_prevention/pages/home/work/danger_page.dart'; import 'package:qhd_prevention/pages/home/work/danger_wait_list_page.dart'; import 'package:qhd_prevention/pages/home/workSet_page.dart'; +import '../../customWidget/hidden_roll_widget.dart'; import '../../http/ApiService.dart'; import '../../tools/tools.dart'; @@ -77,9 +78,34 @@ class _HomePageState extends State { _buildWorkSection(context), const SizedBox(height: 10), ListItemFactory.createBuildSimpleSection("隐患播报"), + // ListItemFactory.createBuildSimpleSection("隐患播报"), + Container( + color: Colors.white, + child: FutureBuilder( + future: ApiService.getHiddenRoll(), + builder: (ctx, snap) { + if (snap.connectionState != ConnectionState.done) { + return Center( + child: SizedBox( + height: 30 * 5, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ); + } + if (snap.hasError || snap.data == null) return Text('加载失败'); + final list = + (snap.data!['hiddenList'] as List) + .cast>(); + return HiddenRollWidget(hiddenList: list); + }, + ), + ), + const SizedBox(height: 10), _buildPCDataSection(), - SizedBox(height: 50), + SizedBox(height: 20), ], ), ), @@ -130,7 +156,6 @@ class _HomePageState extends State { ); } - Widget _buildPCDataSection() { return Container( decoration: BoxDecoration( @@ -176,20 +201,15 @@ class _HomePageState extends State { // 你的导航逻辑 if (index == 0) { pushPage(UserinfoPage(), context); - } - else if (index == 1) { + } else if (index == 1) { pushPage(WorkSetPage(), context); - } - else if (index == 2) { + } else if (index == 2) { pushPage(RiskControlPage(), context); - } - else if (index == 3) { + } else if (index == 3) { pushPage(LowPage(), context); - } - else if (index == 7) { + } else if (index == 7) { pushPage(StudyGardenPage(), context); } - }, ); }).toList(), @@ -333,13 +353,13 @@ class _HomePageState extends State { if (index == 1) { pushPage(DangerPage(), context); } else if (index == 2) { - pushPage(DangerWaitListPage(DangerType.wait,2), context); + pushPage(DangerWaitListPage(DangerType.wait, 2), context); } else if (index == 3) { - pushPage(DangerWaitListPage(DangerType.expired,3), context); + pushPage(DangerWaitListPage(DangerType.expired, 3), context); } else if (index == 4) { - pushPage(DangerWaitListPage(DangerType.waitAcceptance,4), context); + pushPage(DangerWaitListPage(DangerType.waitAcceptance, 4), context); } else if (index == 5) { - pushPage(DangerWaitListPage(DangerType.acceptance,5), context); + pushPage(DangerWaitListPage(DangerType.acceptance, 5), context); } }, child: Container( @@ -443,16 +463,17 @@ class _HomePageState extends State { ]; _fetchData(); // 初始化时请求 - } + Future _fetchData() async { try { // “我的工作” 数量 final raw = await ApiService.getWork(); // 如果拿到的是 String,就 decode;如果本来就是 Map,就直接用 - final Map data = raw is String - ? json.decode(raw as String) as Map - : raw; + final Map data = + raw is String + ? json.decode(raw as String) as Map + : raw; final hidCount = data['hidCount'] as Map; setState(() { @@ -488,17 +509,18 @@ class _HomePageState extends State { "num": (hidCount['yys'] ?? 0).toString(), }, ]; - }); // 安全检查数 final checkJson = - await ApiService.getSafetyEnvironmentalInspectionCount(); + await ApiService.getSafetyEnvironmentalInspectionCount(); setState(() { int confirmCount = checkJson['confirmCount']['confirmCount']; int repulseCount = checkJson['repulseCount']['repulseCount']; - int repulseAndCheckCount = checkJson['repulseAndCheckCount']['repulseAndCheckCount']; + int repulseAndCheckCount = + checkJson['repulseAndCheckCount']['repulseAndCheckCount']; - _safetyEnvironmentalInspection = confirmCount + repulseCount + repulseAndCheckCount; + _safetyEnvironmentalInspection = + confirmCount + repulseCount + repulseAndCheckCount; }); // 特殊作业红点 @@ -509,8 +531,6 @@ class _HomePageState extends State { _eight_work_count += (item ?? 0) as int; } }); - - } catch (e) { // 出错时可以 Toast 或者在页面上显示错误状态 print('加载首页数据失败:$e'); diff --git a/lib/pages/home/study/strengthen_video_study_page.dart b/lib/pages/home/study/strengthen_video_study_page.dart new file mode 100644 index 0000000..708180c --- /dev/null +++ b/lib/pages/home/study/strengthen_video_study_page.dart @@ -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 { + VideoPlayerController? _videoController; + String _videoCoverUrl = ''; + List _videoList = []; + Map _info = {}; + + @override + void initState() { + super.initState(); + _fetchData(); + } + + @override + void dispose() { + _videoController?.removeListener(_controllerListener); + _videoController?.dispose(); + super.dispose(); + } + + Future _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 _processData(List data) { + for (var item in data) { + if (item['nodes'] != null) { + final node = (item['nodes'] as List) + .cast>() + .firstWhere((n) => n['IS_VIDEO'] == 0, orElse: () => {}); + if (node.isNotEmpty) return node; + } else if (item['IS_VIDEO'] == 0) { + return item; + } + } + return {}; + } + + Future _loadPlayInfo( + Map 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 _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); + }, + ), + + ), + ], + ), + ); + } + +} diff --git a/lib/pages/home/study/study_detail_page.dart b/lib/pages/home/study/study_detail_page.dart index c52ac4b..889e3be 100644 --- a/lib/pages/home/study/study_detail_page.dart +++ b/lib/pages/home/study/study_detail_page.dart @@ -9,10 +9,17 @@ import 'package:qhd_prevention/http/ApiService.dart'; import 'package:qhd_prevention/tools/tools.dart'; import 'package:video_player/video_player.dart'; +import '../../../customWidget/toast_util.dart'; import '../../../customWidget/video_player_widget.dart'; import '../../../http/HttpManager.dart'; import 'face_ecognition_page.dart'; +enum TakeExamType { + video_study, + strengththen, + list +} + class StudyDetailPage extends StatefulWidget { final Map studyDetailDetail; final String studentId; @@ -163,7 +170,7 @@ class _StudyDetailPageState extends State // document if (data['VIDEOFILES'] != null) { await pushPage( - StudyPractisePage(data['VIDEOCOURSEWARE_ID']), + StudyPractisePage(videoCoursewareId: data['VIDEOCOURSEWARE_ID']), context, ); await _submitPlayTime( @@ -212,20 +219,35 @@ class _StudyDetailPageState extends State _classId, widget.studentId, ); - final seen = (double.tryParse(prog['pd']?['RESOURCETIME']) ?? 0.0).toInt(); + final raw = prog['pd']?['RESOURCETIME']; + final seen = (() { + if (raw == null) return 0; + // 如果本身就是数字 + if (raw is num) return raw.toInt(); + // 否则转成字符串再 parse + final s = raw.toString(); + return (double.tryParse(s) ?? 0.0).toInt(); + })(); + + // 先销毁旧 controller _videoController?.removeListener(_onTimeUpdate); _videoController?.dispose(); + + // 创建新 controller _videoController = VideoPlayerController.networkUrl(Uri.parse(url)); await _videoController!.initialize(); + setState(() {}); + // 直接从上次播放点 seek,并立即播放 _videoController! ..seekTo(Duration(seconds: seen)) ..play() ..addListener(_onTimeUpdate); } + void _onTimeUpdate() { if (_videoController == null || !_videoController!.value.isPlaying) return; final curr = _videoController!.value.position; @@ -251,7 +273,7 @@ class _StudyDetailPageState extends State if (_currentVideoData == null) return; try { - final pd = (await ApiService.fnSubmitPlayTime( + final resData = (await ApiService.fnSubmitPlayTime( _currentVideoData!['VIDEOCOURSEWARE_ID'], _currentVideoData!['CURRICULUM_ID'], end ? '1' : '0', @@ -260,8 +282,8 @@ class _StudyDetailPageState extends State widget.studentId, _classCurriculumId, _classId, - ))['pd']!; - + )); + final pd = resData['pd'] ?? {}; // 更新进度显示 final comp = pd['PLAYCOUNT'] != null && pd['PLAYCOUNT'] > 0; final resT = pd['RESOURCETIME'] ?? seconds; @@ -293,14 +315,8 @@ class _StudyDetailPageState extends State ) ?? false; if (ok) { - final arguments = { - 'STAGEEXAMPAPERINPUT_ID': - pd['paper']['STAGEEXAMPAPERINPUT_ID'], - 'CLASS_ID': _classId, - 'STUDENT_ID': widget.studentId, - 'NUMBEROFEXAMS': pd['NUMBEROFEXAMS'], - }; - pushPage(TakeExamPage(arguments), context); + + _startExam(resData); } else { _videoController?.play(); } @@ -313,6 +329,40 @@ class _StudyDetailPageState extends State } } + /// 开始考试 + Future _startExam(Map resData) async { + Map pd = resData['pd'] ?? {}; + Map paper = resData['paper'] ?? {}; + setState(() { + _loading = true; + }); + final arguments = { + 'STAGEEXAMPAPERINPUT_ID': paper['STAGEEXAMPAPERINPUT_ID']??'', + 'STAGEEXAMPAPER_ID': paper['STAGEEXAMPAPER_ID']??'', + 'CLASS_ID': _classId, + 'POST_ID': pd['POST_ID'] ?? '', + 'STUDENT_ID': widget.studentId, + 'NUMBEROFEXAMS': pd['NUMBEROFEXAMS'] ?? '' + }; + print('--_startExam data---$arguments'); + + final data = await ApiService.getStartExam(arguments); + setState(() { + _loading = false; + }); + if (data['result'] == 'success') { + pushPage(TakeExamPage(examInfo: { + 'CLASS_ID':_classId, + 'POST_ID': pd['POST_ID'] ?? '', + 'STUDENT_ID': widget.studentId, + 'STRENGTHEN_PAPER_QUESTION_ID': paper['STAGEEXAMPAPERINPUT_ID']??'', + ...data + }, examType: TakeExamType.video_study), context); + + }else{ + ToastUtil.showError(context, '请求错误'); + } + } void _startFaceTimer() { _faceTimer = Timer.periodic(Duration(seconds: _faceTime), (_) async { @@ -362,7 +412,9 @@ class _StudyDetailPageState extends State children: [ SizedBox( height: 250, + width: screenWidth(context), child: VideoPlayerWidget( + allowSeek: false, controller: _videoController, coverUrl: _videoCoverUrl.isNotEmpty ? ApiService.baseImgPath + _videoCoverUrl @@ -493,7 +545,7 @@ class _StudyDetailPageState extends State CustomButton( onPressed: () => pushPage( - StudyPractisePage(m['VIDEOCOURSEWARE_ID']), + StudyPractisePage(videoCoursewareId: m['VIDEOCOURSEWARE_ID']), context, ), text: "课后练习", @@ -538,4 +590,3 @@ class _StudyDetailPageState extends State ); } } - diff --git a/lib/pages/home/study/study_my_task_page.dart b/lib/pages/home/study/study_my_task_page.dart index 18831ec..42bab65 100644 --- a/lib/pages/home/study/study_my_task_page.dart +++ b/lib/pages/home/study/study_my_task_page.dart @@ -3,8 +3,13 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:qhd_prevention/pages/home/study/strengthen_video_study_page.dart'; import 'package:qhd_prevention/pages/home/study/study_class_list_page.dart'; +import 'package:qhd_prevention/pages/home/study/study_detail_page.dart'; +import 'package:qhd_prevention/pages/home/study/take_exam_page.dart'; +import 'package:qhd_prevention/pages/home/study/video_study_detail_page.dart'; import 'package:qhd_prevention/tools/tools.dart'; +import '../../../customWidget/toast_util.dart'; import '../../../http/ApiService.dart'; import '../../mine/mine_sign_page.dart'; import '../../my_appbar.dart'; @@ -184,7 +189,7 @@ class _StudyMyTaskPageState extends State { final int studyState = int.tryParse(item['STUDYSTATE'] ?? '') ?? 0; final int stageExamState = int.tryParse(item['STAGEEXAMSTATE'] ?? '') ?? 0; final int strengthenExamState = - int.tryParse(item['STRENGTHENEXAMSTATE'] ?? '') ?? 0; + int.tryParse(item['STRENGTHENEXAMSTATE'] ?? '') ?? -1; final int numberOfExams = int.tryParse('${item['NUMBEROFEXAMS']}') ?? 0; final int ksCount = int.tryParse('${item['ksCount']}') ?? 0; final int examinationFlag = @@ -194,7 +199,7 @@ class _StudyMyTaskPageState extends State { final String isStrengthen = item['ISSTRENGTHEN'] ?? '0'; return Container( - margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, @@ -244,24 +249,8 @@ class _StudyMyTaskPageState extends State { ], ), Wrap( - spacing: 8, + spacing: 10, children: [ - // 考试详情 - if (stageExamState == 3) - CustomButton( - height: 36, - text: "考试详情", - padding: EdgeInsets.symmetric(horizontal: 20), - borderRadius: 18, - backgroundColor: Colors.blue, - onPressed: - () => Navigator.pushNamed( - context, - '/exam_details', - arguments: {'STUDENT_ID': item['STUDENT_ID']}, - ), - ), - // 加强学习 if (studyState >= 2 && stageExamState >= 2 && @@ -270,18 +259,17 @@ class _StudyMyTaskPageState extends State { CustomButton( height: 36, text: "加强学习", - padding: EdgeInsets.symmetric(horizontal: 20), + padding: EdgeInsets.symmetric(horizontal: 18), borderRadius: 18, backgroundColor: Colors.blue, onPressed: - () => Navigator.pushNamed( + () => pushPage( + StrengthenStudyPage( + classId: item['CLASS_ID'] ?? '', + postId: item['POST_ID'] ?? '', + studentId: item['STUDENT_ID'] ?? '', + ), context, - '/strengthen_video_study', - arguments: { - 'CLASS_ID': item['CLASS_ID'], - 'POST_ID': item['POST_ID'], - 'STUDENT_ID': item['STUDENT_ID'], - }, ), ), @@ -290,7 +278,7 @@ class _StudyMyTaskPageState extends State { CustomButton( height: 36, text: "立即学习", - padding: EdgeInsets.symmetric(horizontal: 20), + padding: EdgeInsets.symmetric(horizontal: 18), borderRadius: 18, backgroundColor: Colors.blue, onPressed: () { @@ -311,23 +299,29 @@ class _StudyMyTaskPageState extends State { CustomButton( height: 36, text: "立即考试", - padding: EdgeInsets.symmetric(horizontal: 20), + padding: EdgeInsets.symmetric(horizontal: 18), borderRadius: 18, backgroundColor: Colors.green, onPressed: - () => Navigator.pushNamed( - context, - '/course_exam', - arguments: { - 'STAGEEXAMPAPERINPUT_ID': - item['STAGEEXAMPAPERINPUT_ID'], - 'CLASS_ID': item['CLASS_ID'], - 'POST_ID': item['POST_ID'], - 'STUDENT_ID': item['STUDENT_ID'], - 'NUMBEROFEXAMS': numberOfExams, - 'entrySite': 'list', - }, + () => _startExam(item, TakeExamType.video_study), + ), + // 考试详情 + if (stageExamState == 3) + CustomButton( + height: 36, + text: "考试详情", + padding: EdgeInsets.symmetric(horizontal: 18), + borderRadius: 18, + backgroundColor: Colors.green, + onPressed: () { + pushPage( + VideoStudyDetailPage( + studentId: item['STUDENT_ID'] ?? '', + classId: item['CLASS_ID'] ?? '', ), + context, + ); + }, ), ], ), @@ -337,6 +331,39 @@ class _StudyMyTaskPageState extends State { ), ); } + /// 开始考试 + Future _startExam(Map resData, TakeExamType type) async { + setState(() { + _isLoading = true; + }); + final arguments = { + 'STAGEEXAMPAPERINPUT_ID': resData['STAGEEXAMPAPERINPUT_ID'] ?? '', + 'STAGEEXAMPAPER_ID': resData['STAGEEXAMPAPER_ID'] ?? '', + 'CLASS_ID': resData['CLASS_ID'] ?? '', + 'POST_ID': resData['POST_ID'] ?? '', + 'STUDENT_ID': resData['STUDENT_ID'] ?? '', + 'NUMBEROFEXAMS': resData['NUMBEROFEXAMS'] ?? '', + }; + print('--_startExam data---$arguments'); + + final data = await ApiService.getStartExam(arguments); + setState(() { + _isLoading = false; + }); + if (data['result'] == 'success') { + pushPage(TakeExamPage(examInfo: { + 'CLASS_ID': resData['CLASS_ID'] ?? '', + 'POST_ID': resData['POST_ID'] ?? '', + 'STUDENT_ID': resData['STUDENT_ID'] ?? '', + 'STRENGTHEN_PAPER_QUESTION_ID': resData['STAGEEXAMPAPERINPUT_ID'] ?? '', + ...data + }, examType: TakeExamType.video_study), context); + + }else{ + ToastUtil.showError(context, '请求错误'); + } + } + bool _onScroll(ScrollNotification n) { if (n.metrics.pixels > n.metrics.maxScrollExtent - 100 && diff --git a/lib/pages/home/study/study_practise_page.dart b/lib/pages/home/study/study_practise_page.dart index c0511c9..e6412f3 100644 --- a/lib/pages/home/study/study_practise_page.dart +++ b/lib/pages/home/study/study_practise_page.dart @@ -1,19 +1,330 @@ import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; +import '../../../http/ApiService.dart'; // 替换为实际路径 class StudyPractisePage extends StatefulWidget { - const StudyPractisePage(this.VIDEOCOURSEWARE_ID,{super.key}); - final String VIDEOCOURSEWARE_ID; + final String videoCoursewareId; + + const StudyPractisePage({Key? key, required this.videoCoursewareId}) + : super(key: key); + @override - State createState() => _StudyPractisePageState(); + _PracticePageState createState() => _PracticePageState(); } -class _StudyPractisePageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: MyAppbar(title: "课后练习"), - body: SizedBox(), +class Question { + final String questionDry; + final String questionType; // '1','2','3','4' + final Map options; + final String answer; + final String descr; + bool correctAnswerShow; + String checked; + + Question({ + required this.questionDry, + required this.questionType, + required this.options, + required this.answer, + required this.descr, + this.correctAnswerShow = false, + this.checked = '', + }); + + factory Question.fromJson(Map json) { + final type = json['QUESTIONTYPE'] as String; + Map 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 { + int current = 0; + List options = []; + bool loading = true; + final Map questionTypeMap = { + '1': '单选题', + '2': '多选题', + '3': '判断题', + '4': '填空题', + }; + + @override + void initState() { + super.initState(); + _getData(); + } + + Future _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 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++), + ), + ), + ], + ), + ], + ), + ), ); } } diff --git a/lib/pages/home/study/study_score_page.dart b/lib/pages/home/study/study_score_page.dart index 354436d..e5e5434 100644 --- a/lib/pages/home/study/study_score_page.dart +++ b/lib/pages/home/study/study_score_page.dart @@ -1,15 +1,259 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/tools/h_colors.dart'; +import '../../../http/ApiService.dart'; class StudyScorePage extends StatefulWidget { - const StudyScorePage({super.key}); + const StudyScorePage({Key? key}) : super(key: key); @override - State createState() => _StudyScorePageState(); + _StudyScorePageState createState() => _StudyScorePageState(); } class _StudyScorePageState extends State { + // 接口数据 + int joinNum = 0, passNum = 0, noPassNum = 0; + List 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 _fetchData() async { + setState(() => loading = true); + final res = await ApiService.pageTaskScoreByUser(showCount, currentPage); + setState(() => loading = false); + if (res != null && res['result'] == 'success') { + final varList = res['varList']; + if (varList is List) { + list.addAll(varList); + } + + // 强壮类型转换 + joinNum = _toInt(res['JOINNUM'], defaultValue: joinNum); + passNum = _toInt(res['PASSNUM'], defaultValue: passNum); + noPassNum = _toInt(res['NOPASSNUM'], defaultValue: noPassNum); + totalPage = _toInt(res['totalPage'], defaultValue: totalPage); + } + } + + int _toInt(dynamic value, {required int defaultValue}) { + if (value is int) return value; + if (value is String) { + return int.tryParse(value) ?? defaultValue; + } + return defaultValue; + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + Widget _buildStatItem(String label, int value) { + return Expanded( + child: Column( + children: [ + Text( + '$value', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w900, + color: Colors.white, + ), + ), + SizedBox(height: 4), + Text(label, style: TextStyle(fontSize: 15, color: Colors.white)), + ], + ), + ); + } + + Widget _buildListItem(dynamic item) { + // 根据 STAGEEXAMSTATE 渲染不同颜色 + Color stateColor; + String stateText; + switch (item['STAGEEXAMSTATE']) { + case '1': + stateColor = Color(0xff3377ff); + stateText = '待考试'; + break; + case '2': + stateColor = Color(0xff999999); + stateText = '考试未通过'; + break; + case '3': + stateColor = Color(0xff33c76d); + stateText = '考试通过'; + break; + default: + stateColor = Color(0xff999999); + stateText = '未参加'; + } + + // 解析成绩 + int score = _toInt(item['STAGEEXAMSCORE'], defaultValue: -1); + String scoreText = score >= 0 ? '$score' : '无'; + + return Card( + color: Colors.white, + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: InkWell( + onTap: () { + final state = item['STAGEEXAMSTATE']; + if (state == '2' || state == '3') { + Navigator.pushNamed(context, '/exam_details', arguments: item); + } + }, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '考试时长:${item['ANSWERSHEETTIME']} 分钟', + style: TextStyle(color: Colors.grey[700]), + ), + Text(stateText, style: TextStyle(color: stateColor)), + ], + ), + const Divider(height: 20), + _infoRow('培训任务名称', item['CLASS_NAME'] ?? ''), + _infoRow('试卷名称', item['EXAMNAME'] ?? ''), + _infoRow('岗位类型', item['POSTTYPE_NAME'] ?? ''), + _infoRow('考试成绩', scoreText), + ], + ), + ), + ), + ); + } + + Widget _infoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: TextStyle(color: Colors.grey[600])), + Flexible( + child: Text( + value, + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.right, + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { - return const Placeholder(); + return Scaffold( + appBar: MyAppbar(title: '成绩查询'), + backgroundColor: h_backGroundColor(), + body: Column( + children: [ + + Padding( + padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12), + child: Stack( + children: [ + SizedBox( + width: 1000, + height: 130, + child: Image.asset( + 'assets/study/bgimg1.png', + fit: BoxFit.fill, + ), + ), + Column( + children: [ + const SizedBox(height: 10), + const Text( + '我的成绩', + style: TextStyle( + fontSize: 20, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: const Divider(height: 20, color: Colors.white), + ), + Row( + children: [ + _buildStatItem('参加考试次数', joinNum), + _buildStatItem('合格次数', passNum), + _buildStatItem('不合格次数', noPassNum), + ], + ), + ], + ), + ], + ), + ), + + // 列表 + Expanded( + child: + list.isEmpty + ? Center( + child: Text('暂无数据', style: TextStyle(color: Colors.grey)), + ) + : ListView.builder( + controller: _scrollController, + itemCount: list.length + 1, + itemBuilder: (context, i) { + if (i < list.length) return _buildListItem(list[i]); + // 底部加载更多指示器 + return Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center( + child: + loading + ? CircularProgressIndicator() + : Text( + currentPage >= totalPage ? '没有更多了' : '', + ), + ), + ); + }, + ), + ), + ], + ), + ); } } diff --git a/lib/pages/home/study/take_exam_page.dart b/lib/pages/home/study/take_exam_page.dart index b6b7034..8e40d6a 100644 --- a/lib/pages/home/study/take_exam_page.dart +++ b/lib/pages/home/study/take_exam_page.dart @@ -1,18 +1,404 @@ +import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:qhd_prevention/pages/home/study/study_detail_page.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'dart:convert'; + +class Question { + final Map rawData; + final String questionDry; + final String questionType; + final Map options; + String checked; + + Question({ + required this.rawData, + required this.questionDry, + required this.questionType, + required this.options, + this.checked = '', + }); + + factory Question.fromJson(Map json) { + final type = json['QUESTIONTYPE'] as String? ?? '1'; + final 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? ?? ''; + } + } + final raw = Map.from(json); + return Question( + rawData: raw, + questionDry: json['QUESTIONDRY'] as String? ?? '', + questionType: type, + options: opts, + ); + } +} class TakeExamPage extends StatefulWidget { - const TakeExamPage(this.arguments,{super.key}); - final Map arguments; + const TakeExamPage({ + required this.examInfo, + required this.examType, + super.key, + }); + + final Map examInfo; + final TakeExamType examType; + @override State createState() => _TakeExamPageState(); } class _TakeExamPageState extends State { + late final List questions; + late final Map info; + int current = 0; + late int remainingSeconds; + Timer? _timer; + + final questionTypeMap = { + '1': '单选题', + '2': '多选题', + '3': '判断题', + '4': '填空题', + }; + + @override + void initState() { + super.initState(); + info = widget.examInfo['pd'] as Map? ?? {}; + final rawList = widget.examInfo['inputQue'] as List? ?? []; + questions = + rawList + .map((e) => Question.fromJson(e as Map)) + .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 _showTip(String content) { + return showDialog( + 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(',') : []; + 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( + context: context, + builder: + (_) => CustomAlertDialog( + title: '温馨提示', + content: '请确认是否交卷!', + cancelText: '取消', + onCancel: () {}, + onConfirm: _submit, + ), + ); + } + + Future _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( + context: context, + builder: + (_) => CustomAlertDialog( + title: '温馨提示', + content: + passed + ? '您的成绩为 $score 分,恭喜您通过本次考试,请继续保持!' + : '您的成绩为 $score 分,很遗憾您没有通过本次考试,请再接再厉!', + cancelText: '', + confirmText: '确定', + onConfirm: () { + Navigator.pop(context); + }, + ), + ); + } + } + + void _onTimeUp() { + ToastUtil.showError(context, '考试时间已结束'); + _submit(); + } + + Widget _buildOptions(Question q) { + if (q.questionType == '4') { + return TextField( + controller: TextEditingController(text: q.checked), + onChanged: (val) => q.checked = val, + maxLength: 255, + decoration: const InputDecoration( + hintText: '请输入内容', + border: OutlineInputBorder(), + ), + ); + } + final keys = q.questionType == '3' ? ['A', 'B'] : ['A', 'B', 'C', 'D']; + return Column( + children: + keys.map((key) { + final active = q.checked.split(',').contains(key); + return GestureDetector( + onTap: + () => _chooseTopic( + q.questionType == '3' + ? 'judge' + : (q.questionType == '2' ? 'multiple' : 'radio'), + key, + ), + child: Container( + margin: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: active ? Colors.blue : Colors.grey.shade200, + shape: BoxShape.circle, + ), + child: Text( + key, + style: TextStyle( + color: active ? Colors.white : Colors.black87, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Text( + q.options[key] ?? '', + style: const TextStyle(fontSize: 16), + ), + ), + ], + ), + ), + ); + }).toList(), + ); + } + + String get _formattedTime { + final m = remainingSeconds ~/ 60; + final s = remainingSeconds % 60; + return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; + } + @override Widget build(BuildContext context) { - return Scaffold( - appBar: MyAppbar(title: '开始考试'), + final q = questions.isNotEmpty ? questions[current] : null; + return PopScope( + canPop: false, // 禁用返回 + + child: Scaffold( + appBar: const MyAppbar(title: '课程考试', isBack: false,), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + Container( + width: double.infinity, + height: 120, + decoration: BoxDecoration( + image: const DecorationImage( + image: AssetImage('assets/study/bgimg1.png'), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(8), + ), + ), + Positioned.fill( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '考试科目:${info['EXAMNAME'] ?? ''}', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + '当前试题 ${current + 1}/${questions.length}', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + const SizedBox(height: 8), + Text( + '考试剩余时间:$_formattedTime', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 16), + if (q != null) ...[ + Text( + '${current + 1}. ${q.questionDry} (${questionTypeMap[q.questionType] ?? ''})', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + const SizedBox(height: 16), + _buildOptions(q), + ], + const Spacer(), + Row( + children: [ + if (current > 0) + Expanded( + child: CustomButton( + text: '上一题', + backgroundColor: Colors.grey.shade200, + textStyle: const TextStyle(color: Colors.black54), + onPressed: _previousQuestion, + ), + ), + if (current > 0 && current < questions.length - 1) + const SizedBox(width: 16), + if (current < questions.length - 1) + Expanded( + child: CustomButton( + text: '下一题', + backgroundColor: Colors.blue, + onPressed: _nextQuestion, + ), + ), + if (current == questions.length - 1) + Expanded( + child: CustomButton( + text: '交卷', + backgroundColor: Colors.blue, + onPressed: _confirmSubmit, + ), + ), + ], + ), + ], + ), + ), + ), ); } } diff --git a/lib/pages/home/study/video_study_detail_page.dart b/lib/pages/home/study/video_study_detail_page.dart new file mode 100644 index 0000000..a6f2102 --- /dev/null +++ b/lib/pages/home/study/video_study_detail_page.dart @@ -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 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 json) { + String type = json['QUESTIONTYPE'] as String? ?? '1'; + Map 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 { + bool loading = true; + List questions = []; + Map paperInfo = {}; + int current = 0; + final Map questionTypeMap = { + '1': '单选题', + '2': '多选题', + '3': '判断题', + '4': '填空题', + }; + + @override + void initState() { + super.initState(); + _fetchData(); + } + + Future _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 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++), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 5b00ffa..9a8e518 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.7.0" asn1lib: @@ -14,23 +14,23 @@ packages: description: name: asn1lib sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.6.5" async: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.flutter-io.cn" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.13.0" + version: "2.12.0" boolean_selector: dependency: transitive description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.2" camera: @@ -38,7 +38,7 @@ packages: description: name: camera sha256: d6ec2cbdbe2fa8f5e0d07d8c06368fe4effa985a4a5ddade9cc58a8cd849557d - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.11.2" camera_android_camerax: @@ -46,23 +46,23 @@ packages: description: name: camera_android_camerax sha256: "4b6c1bef4270c39df96402c4d62f2348c3bb2bbaefd0883b9dbd58f426306ad0" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.6.19" camera_avfoundation: dependency: transitive description: name: camera_avfoundation - sha256: "9e02b36c9c09a01edcb0f2bfc58a94ed38bbbf37907759d651707bb0f327a365" - url: "https://pub.flutter-io.cn" + sha256: b389be4a325742a3950e50475067d95a3de2fb32ba3f31bfcc62b0b6d19907a6 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "0.9.20+3" + version: "0.9.20+4" camera_platform_interface: dependency: transitive description: name: camera_platform_interface sha256: "2f757024a48696ff4814a789b0bd90f5660c0fb25f393ab4564fb483327930e2" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.10.0" camera_web: @@ -70,7 +70,7 @@ packages: description: name: camera_web sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.3.5" characters: @@ -78,7 +78,7 @@ packages: description: name: characters sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.4.0" clock: @@ -86,7 +86,7 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.2" collection: @@ -94,7 +94,7 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.19.1" connectivity_plus: @@ -102,7 +102,7 @@ packages: description: name: connectivity_plus sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.1.4" connectivity_plus_platform_interface: @@ -110,7 +110,7 @@ packages: description: name: connectivity_plus_platform_interface sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.1" convert: @@ -118,7 +118,7 @@ packages: description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.1.2" cross_file: @@ -126,7 +126,7 @@ packages: description: name: cross_file sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.3.4+2" crypto: @@ -134,7 +134,7 @@ packages: description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.6" csslib: @@ -142,7 +142,7 @@ packages: description: name: csslib sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.2" cupertino_icons: @@ -150,7 +150,7 @@ packages: description: name: cupertino_icons sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.8" dbus: @@ -158,7 +158,7 @@ packages: description: name: dbus sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.7.11" dio: @@ -166,7 +166,7 @@ packages: description: name: dio sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.8.0+1" dio_web_adapter: @@ -174,7 +174,7 @@ packages: description: name: dio_web_adapter sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.1" encrypt: @@ -182,7 +182,7 @@ packages: description: name: encrypt sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.0.3" extended_image: @@ -190,7 +190,7 @@ packages: description: name: extended_image sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "10.0.1" extended_image_library: @@ -198,23 +198,31 @@ packages: description: name: extended_image_library sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.0.1" + extension: + dependency: transitive + description: + name: extension + sha256: be3a6b7f8adad2f6e2e8c63c895d19811fcf203e23466c6296267941d0ff4f24 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.6.0" fake_async: dependency: transitive description: name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.flutter-io.cn" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "1.3.3" + version: "1.3.2" ffi: dependency: transitive description: name: ffi sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.4" file: @@ -222,7 +230,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "7.0.1" file_selector_linux: @@ -230,7 +238,7 @@ packages: description: name: file_selector_linux sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.9.3+2" file_selector_macos: @@ -238,7 +246,7 @@ packages: description: name: file_selector_macos sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.9.4+3" file_selector_platform_interface: @@ -246,7 +254,7 @@ packages: description: name: file_selector_platform_interface sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.6.2" file_selector_windows: @@ -254,9 +262,17 @@ packages: description: name: file_selector_windows sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.9.3+4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -267,7 +283,7 @@ packages: description: name: flutter_html sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.0" flutter_lints: @@ -275,7 +291,7 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.0.0" flutter_plugin_android_lifecycle: @@ -283,7 +299,7 @@ packages: description: name: flutter_plugin_android_lifecycle sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.28" flutter_test: @@ -301,7 +317,7 @@ packages: description: name: fluttertoast sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "8.2.12" html: @@ -309,7 +325,7 @@ packages: description: name: html sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.15.6" http: @@ -317,7 +333,7 @@ packages: description: name: http sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.4.0" http_client_helper: @@ -325,7 +341,7 @@ packages: description: name: http_client_helper sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.0" http_parser: @@ -333,7 +349,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.1.2" image_picker: @@ -341,7 +357,7 @@ packages: description: name: image_picker sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.2" image_picker_android: @@ -349,7 +365,7 @@ packages: description: name: image_picker_android sha256: "6fae381e6af2bbe0365a5e4ce1db3959462fa0c4d234facf070746024bb80c8d" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.8.12+24" image_picker_for_web: @@ -357,7 +373,7 @@ packages: description: name: image_picker_for_web sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.6" image_picker_ios: @@ -365,7 +381,7 @@ packages: description: name: image_picker_ios sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.8.12+2" image_picker_linux: @@ -373,7 +389,7 @@ packages: description: name: image_picker_linux sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.1+2" image_picker_macos: @@ -381,7 +397,7 @@ packages: description: name: image_picker_macos sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.1+2" image_picker_platform_interface: @@ -389,7 +405,7 @@ packages: description: name: image_picker_platform_interface sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.10.1" image_picker_windows: @@ -397,7 +413,7 @@ packages: description: name: image_picker_windows sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.1+1" intl: @@ -405,7 +421,7 @@ packages: description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.20.2" js: @@ -413,23 +429,23 @@ packages: description: name: js sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.7.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" - url: "https://pub.flutter-io.cn" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "10.0.9" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.9" leak_tracker_testing: @@ -437,7 +453,7 @@ packages: description: name: leak_tracker_testing sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.1" lints: @@ -445,7 +461,7 @@ packages: description: name: lints sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "5.1.1" list_counter: @@ -453,7 +469,7 @@ packages: description: name: list_counter sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.2" matcher: @@ -461,7 +477,7 @@ packages: description: name: matcher sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.12.17" material_color_utilities: @@ -469,7 +485,7 @@ packages: description: name: material_color_utilities sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.11.1" meta: @@ -477,7 +493,7 @@ packages: description: name: meta sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.16.0" mime: @@ -485,7 +501,7 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.0" mobile_scanner: @@ -493,7 +509,7 @@ packages: description: name: mobile_scanner sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "7.0.1" nested: @@ -501,7 +517,7 @@ packages: description: name: nested sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.0" nm: @@ -509,7 +525,7 @@ packages: description: name: nm sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.5.0" package_info_plus: @@ -517,7 +533,7 @@ packages: description: name: package_info_plus sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "8.3.0" package_info_plus_platform_interface: @@ -525,7 +541,7 @@ packages: description: name: package_info_plus_platform_interface sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.2.0" path: @@ -533,7 +549,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.9.1" path_provider: @@ -541,7 +557,7 @@ packages: description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.5" path_provider_android: @@ -549,7 +565,7 @@ packages: description: name: path_provider_android sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.2.17" path_provider_foundation: @@ -557,7 +573,7 @@ packages: description: name: path_provider_foundation sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.1" path_provider_linux: @@ -565,7 +581,7 @@ packages: description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.2.1" path_provider_platform_interface: @@ -573,7 +589,7 @@ packages: description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.2" path_provider_windows: @@ -581,15 +597,23 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.3.0" + pdfx: + dependency: "direct main" + description: + name: pdfx + sha256: "29db9b71d46bf2335e001f91693f2c3fbbf0760e4c2eb596bf4bafab211471c1" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.9.2" petitparser: dependency: transitive description: name: petitparser sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.1.0" photo_manager: @@ -597,7 +621,7 @@ packages: description: name: photo_manager sha256: a0d9a7a9bc35eda02d33766412bde6d883a8b0acb86bbe37dac5f691a0894e8a - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.7.1" photo_manager_image_provider: @@ -605,7 +629,7 @@ packages: description: name: photo_manager_image_provider sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.2.0" photo_view: @@ -613,7 +637,7 @@ packages: description: name: photo_view sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.15.0" platform: @@ -621,7 +645,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.1.6" plugin_platform_interface: @@ -629,7 +653,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.8" pointycastle: @@ -637,7 +661,7 @@ packages: description: name: pointycastle sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.9.1" provider: @@ -645,7 +669,7 @@ packages: description: name: provider sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.1.5" shared_preferences: @@ -653,7 +677,7 @@ packages: description: name: shared_preferences sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.5.3" shared_preferences_android: @@ -661,7 +685,7 @@ packages: description: name: shared_preferences_android sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.10" shared_preferences_foundation: @@ -669,7 +693,7 @@ packages: description: name: shared_preferences_foundation sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.5.4" shared_preferences_linux: @@ -677,7 +701,7 @@ packages: description: name: shared_preferences_linux sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.1" shared_preferences_platform_interface: @@ -685,7 +709,7 @@ packages: description: name: shared_preferences_platform_interface sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.1" shared_preferences_web: @@ -693,7 +717,7 @@ packages: description: name: shared_preferences_web sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.3" shared_preferences_windows: @@ -701,7 +725,7 @@ packages: description: name: shared_preferences_windows sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.1" simple_gesture_detector: @@ -709,7 +733,7 @@ packages: description: name: simple_gesture_detector sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.1" sky_engine: @@ -722,15 +746,23 @@ packages: description: name: source_span sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.12.1" stream_channel: @@ -738,7 +770,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.4" stream_transform: @@ -746,7 +778,7 @@ packages: description: name: stream_transform sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.1" string_scanner: @@ -754,15 +786,23 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.3.1" table_calendar: dependency: "direct main" description: name: table_calendar sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.2.0" term_glyph: @@ -770,7 +810,7 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.2.2" test_api: @@ -778,7 +818,7 @@ packages: description: name: test_api sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.7.4" typed_data: @@ -786,15 +826,31 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.4.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.1.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.5.1" vector_math: dependency: transitive description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.4" video_player: @@ -802,7 +858,7 @@ packages: description: name: video_player sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.10.0" video_player_android: @@ -810,7 +866,7 @@ packages: description: name: video_player_android sha256: "4a5135754a62dbc827a64a42ef1f8ed72c962e191c97e2d48744225c2b9ebb73" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.8.7" video_player_avfoundation: @@ -818,7 +874,7 @@ packages: description: name: video_player_avfoundation sha256: "9fedd55023249f3a02738c195c906b4e530956191febf0838e37d0dac912f953" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.8.0" video_player_platform_interface: @@ -826,7 +882,7 @@ packages: description: name: video_player_platform_interface sha256: cf2a1d29a284db648fd66cbd18aacc157f9862d77d2cc790f6f9678a46c1db5a - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.4.0" video_player_web: @@ -834,7 +890,7 @@ packages: description: name: video_player_web sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.4.0" visibility_detector: @@ -842,23 +898,23 @@ packages: description: name: visibility_detector sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.4.0+2" vm_service: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 - url: "https://pub.flutter-io.cn" + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "15.0.0" + version: "14.3.1" web: dependency: transitive description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.1" webview_flutter: @@ -866,7 +922,7 @@ packages: description: name: webview_flutter sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.13.0" webview_flutter_android: @@ -874,7 +930,7 @@ packages: description: name: webview_flutter_android sha256: "9573ad97890d199ac3ab32399aa33a5412163b37feb573eb5b0a76b35e9ffe41" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.8.2" webview_flutter_platform_interface: @@ -882,7 +938,7 @@ packages: description: name: webview_flutter_platform_interface sha256: f0dc2dc3a2b1e3a6abdd6801b9355ebfeb3b8f6cde6b9dc7c9235909c4a1f147 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.13.1" webview_flutter_wkwebview: @@ -890,7 +946,7 @@ packages: description: name: webview_flutter_wkwebview sha256: "71523b9048cf510cfa1fd4e0a3fa5e476a66e0884d5df51d59d5023dba237107" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.22.1" wechat_assets_picker: @@ -898,7 +954,7 @@ packages: description: name: wechat_assets_picker sha256: cafe3d32564ed3cacf9822f251941f7b44fe9885c17c8de4fca7e939a459e1ef - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "9.5.1" wechat_picker_library: @@ -906,23 +962,23 @@ packages: description: name: wechat_picker_library sha256: a42e09cb85b15fc9410f6a69671371cc60aa99c4a1f7967f6593a7f665f6f47a - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.5" win32: dependency: transitive description: name: win32 - sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" - url: "https://pub.flutter-io.cn" + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "5.14.0" + version: "5.13.0" xdg_directories: dependency: transitive description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.0" xml: @@ -930,9 +986,9 @@ packages: description: name: xml sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.flutter-io.cn" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.5.0" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.7.0 <4.0.0" flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index b8eae32..2fae557 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,8 @@ dependencies: camera: ^0.11.2 #富文本查看 flutter_html: ^3.0.0 + #pdf、word查看 + pdfx: ^2.9.2 dev_dependencies: @@ -96,6 +98,8 @@ flutter: - assets/js/ - assets/map/ - assets/tabbar/ + - assets/study/ + # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg