From 942da64c4adde8ad2de1ca8b11b4c1106f0c4ba2 Mon Sep 17 00:00:00 2001 From: hs <873121290@qq.com> Date: Tue, 22 Jul 2025 13:34:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=A6=E4=B9=A0=E5=9B=AD=E5=9C=B0=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/study/copy-one.png | Bin 0 -> 4000 bytes assets/study/err.png | Bin 0 -> 2200 bytes assets/study/play.png | Bin 0 -> 7085 bytes assets/study/right.png | Bin 0 -> 2140 bytes assets/study/time.png | Bin 0 -> 2136 bytes ios/Podfile.lock | 6 + lib/customWidget/custom_alert_dialog.dart | 135 +++--- lib/customWidget/custom_button.dart | 2 +- lib/customWidget/hidden_roll_widget.dart | 143 +++++++ lib/customWidget/remote_file_page.dart | 148 +++++++ lib/customWidget/video_player_widget.dart | 356 ++++++++-------- lib/http/ApiService.dart | 88 +++- lib/http/HttpManager.dart | 37 +- lib/main.dart | 7 +- lib/pages/home/home_page.dart | 70 ++-- .../study/strengthen_video_study_page.dart | 236 +++++++++++ lib/pages/home/study/study_detail_page.dart | 81 +++- lib/pages/home/study/study_my_task_page.dart | 109 +++-- lib/pages/home/study/study_practise_page.dart | 329 ++++++++++++++- lib/pages/home/study/study_score_page.dart | 252 ++++++++++- lib/pages/home/study/take_exam_page.dart | 394 +++++++++++++++++- .../home/study/video_study_detail_page.dart | 250 +++++++++++ pubspec.lock | 310 ++++++++------ pubspec.yaml | 4 + 24 files changed, 2472 insertions(+), 485 deletions(-) create mode 100644 lib/customWidget/hidden_roll_widget.dart create mode 100644 lib/customWidget/remote_file_page.dart create mode 100644 lib/pages/home/study/strengthen_video_study_page.dart create mode 100644 lib/pages/home/study/video_study_detail_page.dart diff --git a/assets/study/copy-one.png b/assets/study/copy-one.png index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e8b05db68fc1f5c59947cc0c9170c7928c8dc85a 100644 GIT binary patch literal 4000 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 t5IH550tFUmfcla zeW<92X8@~4foeRk6}2meMHhw8TM1O*a;zt7l=tPRYuOFT615nA7KoU8SMDZn1{6Rl5NTmXw9=s`< zLYxG~a(SO(nNCc63WCT%5=o^}5mmlKSdmPk(CKs%nM$Hk2_^(VnJGgAYJyDZF{8kR zlp=*hj!0k`U{(|e;S7X{H)Z;J2~zoIS()-vn@kNOsReQpg-AA+Gy~-G|36eJ{ft&3 zvCtp+{!d|Le5M>C#X?FrLm@H^F3H0jN)B=qkN|-d@i4q&ri;-jFaj%6U^&3yn7c*? zR&A8X#IQ=a`Xh(W2g7AbL?9DE;an!(q(YQP#9$zu&8Bnx1AMt;e+q?1W^p)dmcL&} zUqK+>42Yl$=wGY2PF^OyZ6#QzpHtqia9m%&2Nms zZ<1vr>#pT3C^m$)=s3Ns7YFsA*1j8aS@%-z-96W>AiwakF8NqhA`y?J8z_cML%KnS z6*;uYv=~{5>iC>n!3LZf%-P6=pJ~Qi47moREx6;RM(3;FvODxZuf4v1jPh?FO9x-K z(aVIP2lUX3`AY!Ln`hoA!iwfUhm5%^t{u|!sCzU?E4;e+8A?+Emt}=?Tjbl> z9}uSnO)kx0l;d92O%)#J9BE0Z%)Rbc7OLluNh_8;#lV>NEtYdA)>HFG2+!NkjST?a z$CjdH!NalEIvWv_aU>SY_#)?w#ffcA5#M{^`0EN2ANj{O9D6uce!i&k^n-5)bWva2 z_mCAH93MIqLa}hIN&aVrw@^~*uAP$@eg&r^Ux*sUY<`3@QK* zc(*&wW!SUx=zM07>pM-EvDCe8q$^3@l*HddbaK%IO;${|)zX#{SKcV59h_*nuJ&oI zA2FV-nRG23Fcvl9v_KW5k~PxtU4kE%HuR8bTXJP=?fVaTOv{7V%f+EhK#fy^U4kX; zd)IF}fBlIaJoHA2j9U!`-L?@H=?W)qj=rz9@44^1tt19L_g&pyeUR+~Rgq0mz}AQZ z-C9iZ;<-Eu+UJ)jU&hZ45dcP*z|~74cai%_0PtFU=5sdP+sY}OBd0CboT^!lDkY#h zdR?A+EVpv%o7j7mT&r|+k1kGq(?!@XNVtG5e8Iss3-677rNe%Otp!IM0&eGYt^O{# zt7NRF{D;=*YVbF!D5ZefLUyjjTy(gV*@{igy_3I$`~=f?HU!NGKG&J6^W3w6XLqU3 z_ToJKdUeI%MdzwjefNEN#h*6Hw)s^Mi;wgE>|se$Bx9-}+(k77!@{D+?ZY^bf}6zg3 zwsPTSD1BX^{gVX3TZ{hFUHN70M>2`G zu?F56%fnvd9M)Z9BXY#=M&g#K<2-AO4f{@4mzcSlUeR?-jeVd?L%cd|LBrZ*SSKua>11 z+PG)C^(;E6e6ZhX;IVjW-}VK#A3HoA=CG3-CL01>&!v}Ttlv4>Ot3ngR^wJN7*}vX zzj(#f{mXfUv@KEO4UP9g3`U1NWV@`Vf3r<@Zcuz!*c5YU;&@|qPn%KE*B>9#k1^#;YWy{w+_0t;*=Ahi&*nL%lUhB;N3gID<-1F?j GqW=PrCx=P^ literal 0 HcmV?d00001 diff --git a/assets/study/play.png b/assets/study/play.png index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ea6e626ccb9a31f1d02892ff2e89e52cfeeca600 100644 GIT binary patch literal 7085 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 X98^0&+>QD700000NkvXXu0mjfrH50+ literal 0 HcmV?d00001 diff --git a/assets/study/right.png b/assets/study/right.png index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0fa67d7b13734da48e603967e718658dd8c45ee4 100644 GIT binary patch literal 2140 zcmaJ?c~ld39v>heV6X-O1JW`^K$MUR2uXx0FdzZK5hFy6JVG*r6p{(aXacw>0R-VG z$R$h^PSyYb*s#MOu3-%_zLpk-?>~6JW5eQzhVaoHNHVGxDq$c{j3mJzTPBpiTv#a1 zNWTU91Av*kl*dQ;oJf`kk&%Q(3`rwX7}x;dzg43Uic(<|OoAm+c>rPl$_)Z26$cRF zeK{14A{b7QhGi<@9hp%)QD&-$DJE?F6!h1y3<5G36@nUBnq0-w1Q6covJ89UHJJdu zgP^Gagg=wwb7H_?Lf;OgF-cUqw-1v^1Zfm1gG^zNsoq{xI?IQ`qS3(j z55W*kDNbf_q0slS45t7>3W_RNWU^YVCaJwih*CnPGMP*=g+`{)ybK60RfZfDYP{sC z4ND3TtP&}u3RH^7L8GED3E77R5Dc0AS%OUQK~}DM-zG!D$Qq%7OeIl_B`pCtoc|A% z$v&V}C>Q=K-~TDB;$@N*;owEp;&_1wj#23Zejm zgNs(9H-wMEd&Nmu;I9_!wDyOUrZpyuT#CIawkZy837!Yv}D{0i(a>^Toj} zkFEvG@8>5x(?X@XM2v@7bS`(;-p+Hd-pWU7p@=(cUeNVp^LS!x*civeskTvHmHf1Z zRKG=IjeWZKM6Cdx00o@~+ln8X=`a_!MdyzFE%Yqr`GjXPPC$Qk>vPLfZyXe>uFCj9 ztoxo_84GTYk=(+*E=j$`fJsRhI%B!HQ@U_OmugK&azA6XlHgD}H+wBIKk_ISGl;}r zE)Edh{+G=?EGyqjJKk5<;(PDpSZD0D6M2Lr55_8s!o>`S8X%Wbj9>X;7W5#^G>DEh zG(rnO<1>dg$~T#0wdPN|X?rGrslV9kSMk*B@j4!Kq%Ciw>)XjOv83@MpYp=N2ZS

tA)Wuq0`e^Ux{EavX`k+}Qri_0_KFy4F8J zz5zadkYgETJ1*s5vI-sVWYhY#xI8W5<3~ngzI2&NSx7xL>sXnMJFrlR53l}6w?@)% z;#rZD`Kf;ASdM2>5;n^^{NLR#@h9d_Dg{0G-|b^-0jq%ppuER10Y`SGUUn&hVVP-u$-UhwliU0Keqz_4M-hH_$c+M@#YeXGR4wQeX>N!27bOvMHgR_O zXBUQ>j`+HDIl%f+KbBzQXZlcAVJQPPy@tJ!KyB~W5(h{)$)=LYgcY;+>!S^s)wX=6 zVXwT=cxQC}*_nC$mCpW*HteYX)davQQnk77LdPjoRB}KZ)14OWS^J{f*54z!9~6Y{g=0z}H5^96e>tRl@q?wTJ^vTJE+$2;%-x`E*0NM|PwHMY!_ zTf+C;bC?|5cC5f-k0U)ian+7qmkBOJORG>=2==Yuy~ggXpk>=NHM^e1{^Z*7X6}69 zXSr*W*N?qC_kA-bsDFO0D6#zdCl|2mTm{2yL2+T$RK?101HZJhxuWB~r&fr&xmYUJ zlmOT^sQcse-#TuRr*6l|likHna81W%_MC28yW8}lxq!V<`-+hl*QI!w;a`pYq;*^O z)`{SGvus?e?qS)4;)S4&jky?3U{H5V)F!p+yHJ8$+ipvovQcbVhr^a&Wt@6U_V4PZ z*MVQzsG4kDbDFHHT3+--5-wFStppK=FgkqGp(}w9<)E}(?fIRp=73VG9#{=wdMMu;>Qj|?L7 zVU}%m@Q>=7OpLzc`q?wx=|fFZ&R^-l5&U{!m%q!NsvxF& v-(L)NI%>W4c*pLzrn?y>Kj;#ad(|}x4*LKB zUVI+YFfZ>oXj%CB-Y;-X&MgR)LF#;is zL{fDP53@VS<#PlQvHb@QjYjb0i$NBj4fv9YaHR^8%jFQf+;A9*H^vK1LE|tOZ?YQ& zizeZ`$XHhlhC=r8-r$l2>;xXb2RFE!zqq7LxhgyGgv!WdK*UW1I24h92m4SqfxCGw z_)Yabaygsl;<+gorA!8;8t#7$d*e#!Al3Ghb(M!t<_Gvn$BUHK#-K8&5Qt`{FWHkW z9iA;rjtNaLc{s*>k>A`SQ!9Gn7_@}DKgbI&AJEAT%pg2kXkeLPBazkK2tO{xY<$iD zWqdPVR$`Zy=IRpk+5>gf>CB`2f1Jy@5gQUdrroApn)C7vGjZQ?szIsPeI7ry^uBa# zDfwT06JcRt>ITOeb=`NrVZ_wUWp;J>q24BF-1f9wC3Z<3r2cVHL#OJ}rFCOyu1#vf z#T^z^{gMi!TYj2plL`lm1CU{g)v}}T?>!f{|EK%6QeB(V*8wyYi_;VP_~1Uc2hwLgx*N8N5VTwwRfi8SL`;S%y%dA zi~h~C-Ko}>BVK%Ubm44AcGtcg8Dc#3hUrhxVz5_X-w)N~c}&3yI!--(ko#y*GyVQ` zpB^u3m&9=CDmGBPZTe(fUPhhTwBpt=A8X_31$J`nuP2OJO8QK=?@B7~!P-aB>CA~m zZRn{B$EuD}+w5O7xTy^`e`D05*#^0+&=BT50m{#&;T1gj*zz9J%;N;gq+`0+N-%ua zmp$ZK+?#Ag-QU|&=G3$H^UBqGYsCe)>M>4i8O`P@wRJkJLt)8W+uozOQyew?g=vO! z-+1X21#O|?mQC5p)&eRq-4gq-27_H^`D+#W=yy-+O>-j%x8{@;AwQ9oJtgC+3|^X2xA#7oQ zJ{N7$y_6R~8jG2HF8n%x9nWe!d5Cq5$EEtlA4>1|1Kb*RDJR*-ZK`Vb*8QP78S~D| z`VK?da=*6&OnD(AU$!h?1k?Mxt|P!9y~kA%IbZ4-Uf|JhZdcSl4tf=e6WboE_u%%1 z7Tq179@ynt);MBp);MA7Ar1MVju8>|qH#w=W!9;iBN+ijWvAoo5S>X+I;YKdi9(v6w}w~yt>O6b$?eoB z^Vxj&irDNC>Tu5GBOWn%4C0mtKD*>ZbMv)kQ)fh`WxM?A>V~-2b?=7vxaWh7g)%<5 zKs{RiG_-E?%9^fsC&Ze0yt;tw5*TRKbT|FP#9-O^=Xuu86<~Pf}w~U@LHHf$cd4@6_Nn8dj&HIMwN&e@a zj^aDVn`&oQ99FXE!sf(Z)XH+#+gEG!e*Zj04dT{CI05xk>rwsdeZ2$7^> 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