From 4b92774cc81c5fe47bd210cea7a51f5118bf7e60 Mon Sep 17 00:00:00 2001 From: "1147192855@qq.com" <1147192855@qq.com> Date: Fri, 14 Jun 2024 17:17:24 +0800 Subject: [PATCH] no message --- .../job/marking_models/do_paper_bus.dart | 22 ++ .../do_test_questions_image_info.dart | 14 ++ .../lib/common/mixins/event_bus_mixin.dart | 31 +++ .../lib/common/utils/event_bus_utils.dart | 26 ++ .../components/bottom_operation_bar.dart | 20 +- .../components/question_paper_view.dart | 232 +++++++++++------- making_school_asignment_app/pubspec.yaml | 1 + 7 files changed, 257 insertions(+), 89 deletions(-) create mode 100644 making_school_asignment_app/lib/common/job/marking_models/do_paper_bus.dart create mode 100644 making_school_asignment_app/lib/common/mixins/event_bus_mixin.dart create mode 100644 making_school_asignment_app/lib/common/utils/event_bus_utils.dart diff --git a/making_school_asignment_app/lib/common/job/marking_models/do_paper_bus.dart b/making_school_asignment_app/lib/common/job/marking_models/do_paper_bus.dart new file mode 100644 index 0000000..30db848 --- /dev/null +++ b/making_school_asignment_app/lib/common/job/marking_models/do_paper_bus.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'do_paper_bus.g.dart'; + +// 批阅页面底部操作栏BUG +@JsonSerializable() +class BottomOperationBar extends Object { + @JsonKey(name: 'revoke_pre_step') // 撤销上一步 + bool revokePreStep; + + @JsonKey(name: 'revoke_all') // 撤销全部 + bool revokeAll; + + BottomOperationBar({ + required this.revokePreStep, + required this.revokeAll, + }); + + factory BottomOperationBar.fromJson(Map srcJson) => _$BottomOperationBarFromJson(srcJson); + + Map toJson() => _$BottomOperationBarToJson(this); +} diff --git a/making_school_asignment_app/lib/common/job/marking_models/do_test_questions_image_info.dart b/making_school_asignment_app/lib/common/job/marking_models/do_test_questions_image_info.dart index 9cc0c64..3d402e4 100644 --- a/making_school_asignment_app/lib/common/job/marking_models/do_test_questions_image_info.dart +++ b/making_school_asignment_app/lib/common/job/marking_models/do_test_questions_image_info.dart @@ -4,6 +4,9 @@ part 'do_test_questions_image_info.g.dart'; @JsonSerializable() class TestQuestionsImageInfo extends Object { + @JsonKey(name: 'templateId') + int? templateId; + @JsonKey(name: 'boxHeight') double boxHeight; @@ -28,6 +31,12 @@ class TestQuestionsImageInfo extends Object { @JsonKey(name: 'remainingHeight') // 视图剩余高度 实际展示图片展示高度下 剩余高度.(若伟负数就是超出视图的高度值) double remainingHeight; + @JsonKey(name: 'imageHeightOffsetStart') // 顶部坐标Y + double imageHeightOffsetStart; + + @JsonKey(name: 'imageHeightOffsetend') // 底部坐标Y + double imageHeightOffsetend; + // 基准 @JsonKey(name: 'scaleRatio') double scaleRatio; @@ -42,17 +51,22 @@ class TestQuestionsImageInfo extends Object { required this.boxHeight, required this.imageWidth, required this.imageHeight, + this.templateId, this.scaleRatio = 1, this.zoom = 1, this.actualImgWidth = 0, this.actualImgHeight = 0, this.remainingHeight = 0, + this.imageHeightOffsetStart = 0, + this.imageHeightOffsetend = 0, }) { // 图片已视图宽为基准,高度自适应可滑动 图片的实际宽高都需要乘此基准值 scaleRatio = boxWidth / imageWidth; actualImgWidth = imageWidth * scaleRatio; actualImgHeight = imageHeight * scaleRatio; remainingHeight = boxHeight - actualImgHeight; + imageHeightOffsetStart = remainingHeight / 2; + imageHeightOffsetend = imageHeightOffsetStart + actualImgHeight; } factory TestQuestionsImageInfo.fromJson(Map srcJson) => _$TestQuestionsImageInfoFromJson(srcJson); diff --git a/making_school_asignment_app/lib/common/mixins/event_bus_mixin.dart b/making_school_asignment_app/lib/common/mixins/event_bus_mixin.dart new file mode 100644 index 0000000..9c01517 --- /dev/null +++ b/making_school_asignment_app/lib/common/mixins/event_bus_mixin.dart @@ -0,0 +1,31 @@ +// event_bus 混入类 +import 'dart:async'; + +import 'package:event_bus/event_bus.dart'; +import 'package:making_school_asignment_app/common/utils/event_bus_utils.dart'; + +mixin EventBusMixin { + StreamSubscription? _subscription; + static final EventBusUtils _exampleUtil = EventBusUtils(); + static final EventBus _eBus = _exampleUtil.getEventBus(); + + /* + * 发起事件总线监听 + * @param {Function} callback 回调函数 + */ + /// 发起事件总线监听 @param {Function} callback 回调函数 + StreamSubscription eventOn({required void Function(T) callback}) { + _subscription = _eBus.on().listen(callback); + return _subscription!; + } + + // 触发事件总线 + void eventFire({required T model}) { + _exampleUtil.toFire(model); + } + + // 关闭监听总线事件 + void eventCancel() { + _subscription?.cancel(); + } +} diff --git a/making_school_asignment_app/lib/common/utils/event_bus_utils.dart b/making_school_asignment_app/lib/common/utils/event_bus_utils.dart new file mode 100644 index 0000000..b7451d1 --- /dev/null +++ b/making_school_asignment_app/lib/common/utils/event_bus_utils.dart @@ -0,0 +1,26 @@ +import 'package:event_bus/event_bus.dart'; + +//订阅者回调签名 +typedef EventCallback = void Function(dynamic arg); + +///事件总线 +class EventBusUtils { + late final EventBus _eventBus; + + // 单例模式 + static final EventBusUtils _instance = EventBusUtils._internal(); + factory EventBusUtils() => _instance; + EventBusUtils._internal() { + _eventBus = EventBus(); + } + + // 获取实例 + EventBus getEventBus() { + return _eventBus; + } + + // 发起事件 + void toFire(T model) { + _eventBus.fire(model); + } +} diff --git a/making_school_asignment_app/lib/page/home_page/children/homework_review/components/bottom_operation_bar.dart b/making_school_asignment_app/lib/page/home_page/children/homework_review/components/bottom_operation_bar.dart index 3d9dde5..b021c3f 100644 --- a/making_school_asignment_app/lib/page/home_page/children/homework_review/components/bottom_operation_bar.dart +++ b/making_school_asignment_app/lib/page/home_page/children/homework_review/components/bottom_operation_bar.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:making_school_asignment_app/common/job/marking_models/do_paper_bus.dart'; +import 'package:making_school_asignment_app/common/mixins/event_bus_mixin.dart'; import 'package:making_school_asignment_app/common/utils/anti_shake_throttling.dart'; import 'package:making_school_asignment_app/common/utils/utils.dart'; import 'package:making_school_asignment_app/page/global_widget/my_text.dart'; @@ -16,7 +18,7 @@ class BottomAnnotationSwitch extends StatefulWidget { State createState() => _BottomAnnotationSwitchJobState(); } -class _BottomAnnotationSwitchJobState extends State with SingleTickerProviderStateMixin { +class _BottomAnnotationSwitchJobState extends State with SingleTickerProviderStateMixin, EventBusMixin { late AnimationController _animationController; // 动画 final _homeworkLogic = Get.find(); final _logicControl = Get.find().annotationState; @@ -39,6 +41,7 @@ class _BottomAnnotationSwitchJobState extends State with _animationController.reverse(); } }); + super.initState(); } @@ -48,6 +51,8 @@ class _BottomAnnotationSwitchJobState extends State with ..removeListener(toUp) ..dispose(); _opControllisten?.cancel(); + + eventCancel(); super.dispose(); } @@ -132,7 +137,7 @@ class _BottomAnnotationSwitchJobState extends State with ], ), ), - Container(width: double.infinity, color: Colors.white, height: 0.5.h), + Container(width: double.infinity, color: Colors.white, height: 0.35.h), Expanded( child: Row( children: [ @@ -142,19 +147,26 @@ class _BottomAnnotationSwitchJobState extends State with width: double.infinity, height: double.infinity, child: IconButton( - onPressed: () => easyThrottle('homework_bottom_action_bar_annotations', () {}), + onPressed: () => easyThrottle( + 'homework_bottom_action_bar_annotations', + () => eventFire(model: BottomOperationBar(revokeAll: false, revokePreStep: true)), + ), icon: Icon(const IconData(0xe638, fontFamily: "AlibabaIcon"), size: iconSize, color: defaultColor), ), ), ), Container(width: 0.3.w, height: double.infinity, color: Colors.white), + // 全部撤销 Expanded( child: SizedBox( width: double.infinity, height: double.infinity, child: IconButton( icon: Icon(const IconData(0xe637, fontFamily: "AlibabaIcon"), size: iconSize, color: defaultColor), - onPressed: () => easyThrottle('homework_bottom_action_bar_annotations', () {}), + onPressed: () => easyThrottle( + 'homework_bottom_action_bar_annotations', + () => eventFire(model: BottomOperationBar(revokeAll: true, revokePreStep: false)), + ), ), ), ), diff --git a/making_school_asignment_app/lib/page/home_page/children/homework_review/components/question_paper_view.dart b/making_school_asignment_app/lib/page/home_page/children/homework_review/components/question_paper_view.dart index e28519f..10ec41f 100644 --- a/making_school_asignment_app/lib/page/home_page/children/homework_review/components/question_paper_view.dart +++ b/making_school_asignment_app/lib/page/home_page/children/homework_review/components/question_paper_view.dart @@ -6,9 +6,11 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:functional_widget_annotation/functional_widget_annotation.dart'; import 'package:get/get.dart'; import 'package:making_school_asignment_app/common/config/request_config.dart'; +import 'package:making_school_asignment_app/common/job/marking_models/do_paper_bus.dart'; import 'package:making_school_asignment_app/common/job/marking_models/do_paper_details_param.dart'; import 'package:making_school_asignment_app/common/job/marking_models/do_paper_details_result.dart'; import 'package:making_school_asignment_app/common/job/marking_models/do_test_questions_image_info.dart'; +import 'package:making_school_asignment_app/common/mixins/event_bus_mixin.dart'; import 'package:making_school_asignment_app/common/utils/anti_shake_throttling.dart'; import 'package:making_school_asignment_app/common/utils/cached_network_img.dart'; import 'package:making_school_asignment_app/page/global_widget/my_text.dart'; @@ -30,6 +32,7 @@ class QuestionPaperView extends StatefulWidget { class _QuestionPaperViewState extends State { final logic = Get.find(); final sateData = Get.find().state; + final annotationState = Get.find().annotationState; @override Widget build(BuildContext context) { @@ -44,7 +47,7 @@ class _QuestionPaperViewState extends State { return Stack( children: [ // 主图 - $QuestionImageView(maxWidth, maxHeight, sateData), + QuestionImageView(maxWidth, maxHeight, sateData, annotationState), // 继续批阅按钮 Positioned(right: 3.w, bottom: 4.h, child: const $ContinueToReview(isFloatingAction: true)), // 上一题按钮 @@ -111,9 +114,10 @@ Widget $questionNumberView(BuildContext context, HomeworkReviewState sateData) { final scrollControllerNum = useScrollController(); // 试题题号区域 useEffect(() { - StreamSubscription listenVal = sateData.slide.listen((e) => scrollControllerNum.jumpTo(e)); + var listenVal = sateData.slide.listen((e) { + if (e != scrollControllerNum.offset) scrollControllerNum.jumpTo(e); + }); - // 返回一个清理函数,在组件销毁时移除监听 return () { listenVal.cancel(); }; @@ -141,8 +145,12 @@ Widget $questionNumberView(BuildContext context, HomeworkReviewState sateData) { var imageVal = sateData.imageScale.value; if (imageVal == null) return const SizedBox(); var studentQuestions = sateData.studentQuestions.value; - print('书哈哈哈...'); - return Padding( + + var boxHeight = imageVal.boxHeight; + var actualImgHeight = imageVal.actualImgHeight; // 实际图片高度 + + return Container( + height: boxHeight > actualImgHeight ? boxHeight : actualImgHeight, padding: EdgeInsets.only(top: imageVal.remainingHeight > 0 ? imageVal.remainingHeight / 2 : 0), child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -276,85 +284,139 @@ Widget $scoringQuestionsView(BuildContext context, StudentQuestions item, double } // 试题图片视图 -@hwidget -Widget $questionImageView(double maxWidth, double maxHeight, HomeworkReviewState sateData) { - final scrollControllerQuestion = useScrollController(); // 试题图片区域 - var vnHandWritings = useValueNotifier>([]); - ImageStream? imageStream; - var imageStreamListener = useState(ImageStreamListener((ImageInfo info, bool _) { - WidgetsBinding.instance.addPostFrameCallback((_) { - sateData.imageScale.value = TestQuestionsImageInfo( - boxWidth: maxWidth, - boxHeight: maxHeight, - imageWidth: info.image.width.toDouble(), - imageHeight: info.image.height.toDouble(), - url: sateData.data.value!.zgtAnswer, - ); - }); - })); +class QuestionImageView extends HookWidget with EventBusMixin { + final double maxWidth; + final double maxHeight; + final HomeworkReviewState sateData; + final HomeworkReviewAnnotationsControlState annotationState; + QuestionImageView(this.maxWidth, this.maxHeight, this.sateData, this.annotationState, {super.key}); - // 可选:添加滚动监听 - useEffect(() { - scrollControllerQuestion.addListener(() { - // 执行滚动相关的逻辑,例如打印滚动位置 - sateData.slide.value = scrollControllerQuestion.offset; - }); - // 返回一个清理函数,在组件销毁时移除监听 - return () { - imageStream?.removeListener(imageStreamListener.value); - }; - }, []); + /// 获取数组指定倒数具体值的下标 + int _findTargetIndex(List list, T target, [int reciprocal = 2]) { + List indexs = []; + for (int i = list.length - 1; i >= 0; i--) { + if (list[i] == target) { + indexs.add(i); + if (indexs.length == reciprocal) return i; + } + } + return -1; + } - print('这里是加载了..............'); - return Container( - height: maxHeight, - // padding: EdgeInsets.only(bottom: 2.h, top: 2.h), - alignment: Alignment.center, - child: SingleChildScrollView( - controller: scrollControllerQuestion, - physics: const BouncingScrollPhysics(), - padding: EdgeInsets.zero, - scrollDirection: Axis.vertical, // 设置垂直滚动 - child: Obx(() { - var imageUrl = sateData.data.value?.zgtAnswer; - if (imageUrl == null) return const SizedBox(); + @override + Widget build(BuildContext context) { + final scrollControllerQuestion = useScrollController(); // 试题图片区域 + var vnHandWritings = useValueNotifier>([]); - return Container( - decoration: BoxDecoration(boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.2), - offset: Offset(-6.r, 1.r), //阴影x轴偏移量 - blurRadius: 10.r, //阴影模糊程度 - spreadRadius: 8.r //阴影扩散程度 - ) - ]), - child: Listener( - behavior: HitTestBehavior.opaque, - onPointerUp: (PointerUpEvent details) {}, - onPointerMove: (PointerMoveEvent details) {}, - child: RepaintBoundary( - child: CustomPaint( - foregroundPainter: DrawingPainter(ctrl: vnHandWritings), + ImageStream? imageStream; + var imageStreamListener = useState(ImageStreamListener((ImageInfo info, bool _) { + WidgetsBinding.instance.addPostFrameCallback((_) { + sateData.imageScale.value = TestQuestionsImageInfo( + templateId: sateData.data.value?.templateId, + boxWidth: maxWidth, + boxHeight: maxHeight, + imageWidth: info.image.width.toDouble(), + imageHeight: info.image.height.toDouble(), + url: sateData.data.value!.zgtAnswer, + ); + }); + })); + + // 可选:添加滚动监听 + useEffect(() { + eventOn(callback: (BottomOperationBar e) async { + var annotationsData = vnHandWritings.value; // 批注数据 + if (e.revokeAll) { + // 情况全部 + if (annotationsData.isNotEmpty) { + bool? res = await showDialog( + context: context, + builder: (context1) { + return AlertDialog(content: quickText("是否撤销上次批阅批注痕迹"), actions: [ + TextButton(child: quickText("取消"), onPressed: () => Navigator.pop(context1, false)), + TextButton(child: quickText("确定", color: Theme.of(context).primaryColor), onPressed: () => Navigator.pop(context1, true)) + ]); + }, + ); + if (res == true) vnHandWritings.value = []; + } + return; + } + + // 返回上一步 + var index = _findTargetIndex(annotationsData, null); + if (index != -1) { + annotationsData = annotationsData.sublist(0, index + 1); + annotationsData = List.from(annotationsData); + } else { + annotationsData = []; + } + vnHandWritings.value = annotationsData; + }); + + scrollControllerQuestion.addListener(() => sateData.slide.value = scrollControllerQuestion.offset); // 执行滚动相关的逻辑,例如打印滚动位置 + // 返回一个清理函数,在组件销毁时移除监听 + return () { + imageStream?.removeListener(imageStreamListener.value); + }; + }, []); + + return Container( + height: maxHeight, + // padding: EdgeInsets.only(bottom: 2.h, top: 2.h), + alignment: Alignment.center, + child: Obx(() { + var imageUrl = sateData.data.value?.zgtAnswer; + if (imageUrl == null) return const SizedBox(); + + return SingleChildScrollView( + controller: scrollControllerQuestion, + physics: !annotationState.pen.value ? const BouncingScrollPhysics() : const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + scrollDirection: Axis.vertical, // 设置垂直滚动 + child: Container( + decoration: BoxDecoration( + boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.2), offset: Offset(-6.r, 1.r), blurRadius: 10.r, spreadRadius: 8.r)]), + child: Listener( + behavior: HitTestBehavior.opaque, + onPointerUp: (PointerUpEvent details) { + // 处理单个触摸点抬起的逻辑 + // activePointers--; + // globalPosition = null; + var imageScale = sateData.imageScale.value; + if (imageScale == null || !annotationState.pen.value) return; + vnHandWritings.value.add(null); // 增加空点以分隔不同的线段 + }, + onPointerMove: (PointerMoveEvent event) { + var imageScale = sateData.imageScale.value; + if (imageScale == null || !annotationState.pen.value) return; + Offset localPosition = event.localPosition; + var dy = localPosition.dy; + if (dy > imageScale.actualImgHeight || dy < 0) return; // 检查笔记是否超出图片范围 + + vnHandWritings.value = List.from(vnHandWritings.value)..add(localPosition); + }, child: RepaintBoundary( - child: $TheCachedNetworkImage( - (context, imageProvider) { - print('图片加载了..............'); - Image imageWidget = Image(image: imageProvider, fit: BoxFit.fitWidth); - imageStream?.removeListener(imageStreamListener.value); - imageStream = imageWidget.image.resolve(const ImageConfiguration())..addListener(imageStreamListener.value); - - return imageWidget; - }, - imageUrl: RequestConfig.imgUrl + imageUrl, + child: CustomPaint( + foregroundPainter: DrawingPainter(ctrl: vnHandWritings), + child: RepaintBoundary( + child: $TheCachedNetworkImage( + (context, imageProvider) { + Image imageWidget = Image(image: imageProvider, fit: BoxFit.fitWidth); + imageStream?.removeListener(imageStreamListener.value); + imageStream = imageWidget.image.resolve(const ImageConfiguration())..addListener(imageStreamListener.value); + return imageWidget; + }, + imageUrl: RequestConfig.imgUrl + imageUrl, + ), + ), ), ), ), ), - ), - ); - }), - ), - ); + ); + })); + } } // 批注框 @@ -363,23 +425,23 @@ class DrawingPainter extends CustomPainter { final Paint paintBrush = Paint() ..color = Colors.red ..strokeCap = StrokeCap.round - ..strokeWidth = 1.5.r; + ..strokeWidth = 0.6.r; DrawingPainter({required this.ctrl}) : super(repaint: ctrl); @override void paint(Canvas canvas, Size size) { var points = ctrl.value; var pointsLength = points.length; - // for (int i = 0; i < pointsLength; i++) { - // GestureRecording item = points[i]; - // Offset? offsetData = item.data; - // Offset? nextOffsetData = pointsLength - 1 == i ? null : points[i + 1].data; - // if (offsetData != null && nextOffsetData != null) { - // canvas.drawLine(offsetData, nextOffsetData, paintBrush); - // } - // } + for (int i = 0; i < pointsLength; i++) { + Offset? offsetData = points[i]; + Offset? nextOffsetData = pointsLength - 1 == i ? null : points[i + 1]; + if (offsetData != null && nextOffsetData != null) canvas.drawLine(offsetData, nextOffsetData, paintBrush); + } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; + + @override + bool shouldRebuildSemantics(CustomPainter oldDelegate) => false; } diff --git a/making_school_asignment_app/pubspec.yaml b/making_school_asignment_app/pubspec.yaml index a28333a..8d38353 100644 --- a/making_school_asignment_app/pubspec.yaml +++ b/making_school_asignment_app/pubspec.yaml @@ -82,6 +82,7 @@ dependencies: easy_debounce: ^2.0.3 # 防抖节流 flutter_hooks: ^0.20.5 flutter_spinkit: ^5.2.1 + event_bus: ^2.0.0 dev_dependencies: flutter_test: