import 'dart:developer'; import 'package:fis_common/index.dart'; import 'package:fis_jsonrpc/rpc.dart'; import 'package:fis_measure/interfaces/process/workspace/exam_info.dart'; import 'package:fis_measure/interfaces/process/workspace/measure_3d_view_controller.dart'; import 'package:fis_measure/process/layout/configuration.dart'; import 'package:fis_measure/process/workspace/measure_controller.dart'; import 'package:fis_measure/process/workspace/measure_data_controller.dart'; import 'package:fis_measure/process/workspace/measure_data_helper.dart'; import 'package:fis_measure/process/workspace/measure_handler.dart'; import 'package:fis_measure/process/workspace/measure_3d_view_controller.dart'; import 'package:fis_measure/view/gesture/positioned_cursor.dart'; import 'package:fis_measure/view/measure/measure_config/widgets/measure_configuration_style.dart'; import 'package:fis_measure/view/measure/measure_images_bar.dart'; import 'package:fis_measure/view/measure/measure_player.dart'; import 'package:fis_measure/view/measure/measure_tool_panel.dart'; import 'package:fis_measure/view/measure/measure_view_controller.dart'; import 'package:fis_measure/view/player/control_board/operate_bar.dart'; import 'package:fis_ui/index.dart'; import 'package:fis_ui/interface/interactive_container.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; /// 测量主页面 class MeasureMainPage extends StatefulWidget implements FWidget, FInteractiveContainer { const MeasureMainPage( this.token, this.recordCode, this.patientCode, this.remedicalCode, { this.needRouterBack, this.enableLoopPlayback = false, this.onLoopPlaybackEnableChanged, this.remedicalAISelectedInfoCode, this.customWidgets = const [], this.hasQualityControlPermission = false, this.dispose, Key? key, }) : super(key: key); final String token; final String patientCode; final String remedicalCode; final String recordCode; final bool? needRouterBack; // 需要返回按钮【一版用于返回到报告编辑】 final bool enableLoopPlayback; final ValueCallback<bool>? onLoopPlaybackEnableChanged; final String? remedicalAISelectedInfoCode; final List<FWidget> customWidgets; final bool hasQualityControlPermission; final void Function()? dispose; @override final String pageName = 'MeasureMainPage'; @override State<StatefulWidget> createState() => _MeasureMainPageState(); } class _MeasureMainPageState extends State<MeasureMainPage> { /// 病人及图像信息 // String _curToken = ''; String _curPatientCode = ''; String _curRemedicalCode = ''; String _curRecordCode = ''; String? _curRemedicalAISelectedInfoCode = ''; bool? _curNeedRouterBack; /// 数据 final measureData = Get.find<MeasureDataController>(); /// webview 控制器 final measure3DViewController = Get.find<Measure3DViewController>(); /// 测量项控制器 final measureMetaController = Get.put(MeasureMetaController()); double get devicePixelRatio => kIsMobile ? 1 : MediaQuery.of(context).devicePixelRatio; /// 隐藏全屏loadding bool _hideFullScreenLoading = false; /// 图片loadding late bool imageLoading = false; late final measureHandler = Get.find<MeasureHandler>(); /// 测量控制器 late MeasureController measureController; /// 响应图片切换事件通知 void _onNewImageLoadStatueChanged(sender, e) { imageLoading = e; setState(() {}); } /// 响应全屏状态切换事件 void _onChangeFullScreenState(sender, e) { setState(() {}); } /// 响应工具面板状态切换事件 void _onToolPanelStateChanged(sender, e) { setState(() {}); } /// 在此处动态刷新测量窗口,而不是重置窗口 void _onUpdateState(sender, Map<String, String> parameters) async { _curNeedRouterBack = false; bool needUpdate = false; if (_curRecordCode == (parameters['recordCode'] ?? '')) { if (_curRemedicalCode == (parameters['remedicalCode'] ?? '')) { } else { measureHandler.changeImageByRemedicalCode .emit(this, parameters['remedicalCode'] ?? ''); } } else { needUpdate = true; } // _curToken = parameters['token'] ?? ''; _curPatientCode = parameters['patientCode'] ?? ''; _curRemedicalCode = parameters['remedicalCode'] ?? ''; _curRecordCode = parameters['recordCode'] ?? ''; _curRemedicalAISelectedInfoCode = parameters['remedicalAISelectedInfoCode'] ?? ''; if (needUpdate) { await LayoutConfiguration.ins.loadData(); List<RemedicalInfoDTO> remedicals = await _getCurRemedicals(); _initMouseModuel(); _initCarotidModuel(remedicals); measureData.remedicalList = remedicals; var remedicalInfo = await MeasureDataHelper.getImageInfo .call(_curRemedicalCode, _curRemedicalAISelectedInfoCode); if (remedicalInfo != null) { measureData.aiResults = remedicalInfo.diagnosisResult ?? ''; if (remedicalInfo.terminalImages != null) { _hideFullScreenLoading = true; measureData.itemCurrentImage = measureData.chooseImageUrl(remedicalInfo.terminalImages!); getExamImageInfoList(remedicals); measureHandler.changeImageList.emit(this, remedicals); } } measureController.imageLoaded.removeListener(_onImageLoaded); measureController.imageLoaded.addListener(_onImageLoaded); } _setCurImageData(); } /// 将当前的图像信息同步到 measureData 中 void _setCurImageData() { measureData.measureInfoData = MeasureInfoData( patientCode: _curPatientCode, recordCode: _curRecordCode, remedicalCode: _curRemedicalCode, remedicalAISelectedInfoCode: _curRemedicalAISelectedInfoCode, ); } /// 先加载图像布局配置,然后加载全局数据 void _loadLayoutConfig() async { await LayoutConfiguration.ins.loadData(); setState(() { _initData(); }); } @override void initState() { _curPatientCode = widget.patientCode; _curRemedicalCode = widget.remedicalCode; _curRecordCode = widget.recordCode; _curNeedRouterBack = widget.needRouterBack; _curRemedicalAISelectedInfoCode = widget.remedicalAISelectedInfoCode; _hideFullScreenLoading = false; _loadLayoutConfig(); _setCurImageData(); measureData.curMeasureDataChanged.addListener(_onUpdateState); measureHandler.onNewImageLoadStatueChanged .addListener(_onNewImageLoadStatueChanged); measureHandler.onChangeFullScreenState .addListener(_onChangeFullScreenState); measureHandler.onToolPanelStateChanged .addListener(_onToolPanelStateChanged); super.initState(); } @override void dispose() { measureData.curMeasureDataChanged.removeListener(_onUpdateState); measureHandler.onNewImageLoadStatueChanged .removeListener(_onNewImageLoadStatueChanged); measureHandler.onChangeFullScreenState .removeListener(_onChangeFullScreenState); measureHandler.onToolPanelStateChanged .removeListener(_onToolPanelStateChanged); widget.dispose?.call(); super.dispose(); } @override FWidget build(BuildContext context) { FWidget body; if (!_hideFullScreenLoading) { const loadingWidget = FCenter(child: FCircularProgressIndicator()); body = FRow( children: const [ FExpanded( child: loadingWidget, ), ], ); } else { body = FColumn(children: [ FExpanded( child: FRow( children: [ FOffstage( child: MeasureToolPanel( customWidgets: widget.customWidgets, ), offstage: measureHandler.fullScreenState || !measureHandler.toolPanelState), const FVerticalDivider(), FExpanded( child: FColumn( mainAxisSize: MainAxisSize.max, children: [ FExpanded( child: FStack( children: [ if (!imageLoading) MeasureRightBoard( key: ValueKey(measureData.itemCurrentImage), ), if (imageLoading) FContainer( color: Colors.black, child: const FCenter( child: FCircularProgressIndicator(), ), ), ], )), // if (!measureHandler.fullScreenState) const MeasureImagesBar(), ], ), ), ], ), ), FOffstage( child: FRow(children: [ FColumn( children: [ const OperateBar(), _curNeedRouterBack ?? false ? FContainer( width: 300 / devicePixelRatio, padding: EdgeInsets.only(bottom: 20 / devicePixelRatio), child: FIconButton( businessParent: widget, name: "back", onPressed: () { Get.back(); }, icon: const FIcon( Icons.arrow_back, color: Colors.white, size: 36, ), ), ) : const FSizedBox() ], ), FExpanded( child: MeasureImagesBar( hasQualityControlPermission: widget.hasQualityControlPermission, ), ), ]), offstage: measureHandler.fullScreenState), ]); } return FCenter( child: FContainer( color: _hideFullScreenLoading ? const Color.fromRGBO(70, 70, 70, 1) : Colors.black, child: body, ), ); } /// 获取当前状态下的图像集 Future<List<RemedicalInfoDTO>> _getCurRemedicals() async { List<RemedicalInfoDTO> remedicals = []; var value = await MeasureDataHelper.getRemedicalList( _curPatientCode, _curRecordCode, ); for (var remedicalItemList in value) { remedicals.addAll(remedicalItemList.remedicalList ?? []); } return remedicals; } /// 初始化颈动脉模块 /// [Carotid] ✅遍历出颈动脉信息列表,传给壳子 void _initCarotidModuel(List<RemedicalInfoDTO> remedicals) { List<Ultra3DResourceInfo> ultra3DResourceInfos = []; for (var remedical in remedicals) { if (remedical.carotidResult != null) { ultra3DResourceInfos.add(Ultra3DResourceInfo(remedical.carotidResult!)); } } measure3DViewController.ultra3DResourceInfoList = ultra3DResourceInfos; measure3DViewController.recordId = _curRecordCode; measure3DViewController.notifyShellLoadAllModel(); } /// 初始化卡尺样式部分 Future<void> _initMouseModuel() async { final mouseState = Get.put<IMouseState>(MouseState()); MeasureSystemSettingDTO result = MeasureSystemSettingDTO( cursorSize: 16, shapeCursorSize: 30, showResultWindow: true, fontSize: 14, showCursorLine: true, showDepthGuideline: true, showBriefAnnotation: true, autoSnapDistance: '20', minCursorDistance: '20', annotationFontSize: 14, distanceUnit: Unit.cm, areaUnit: Unit.cm2, velocityUnit: Unit.cms, timeUnit: Unit.s, ); try { result = await MeasureDataHelper.getMeasureSystemSettingAsync(); } catch (e) { log("getMeasureSystemSettingAsync error: $e"); } measureData.measureSystemSetting = result; measureData.checkErrorUnitConfig(); measureData.checkErrorStyleConfig(); mouseState.cursorType = getMeasureSystemSettingCursorType(result.cursorType); mouseState.cursorSize = result.cursorSize.toDouble(); } /// 测量页全局数据首次初始化 void _initData() async { List<RemedicalInfoDTO> remedicals = await _getCurRemedicals(); _initMouseModuel(); _initCarotidModuel(remedicals); measureData.remedicalList = remedicals; var remedicalInfo = await MeasureDataHelper.getImageInfo( _curRemedicalCode, _curRemedicalAISelectedInfoCode); if (remedicalInfo != null) { measureData.aiResults = remedicalInfo.diagnosisResult ?? ''; if (remedicalInfo.terminalImages != null) { _hideFullScreenLoading = true; final imgInfo = remedicalInfo.terminalImages!; measureData.itemCurrentImage = measureData.chooseImageUrl(imgInfo); getExamImageInfoList(remedicals); } } measureController.imageLoaded.removeListener(_onImageLoaded); measureController.imageLoaded.addListener(_onImageLoaded); } /// 获取测量图片所需的图片组 并且写入控制器中 加载 void getExamImageInfoList(List<RemedicalInfoDTO> remedicals) async { /// 检查中图片信息表 List<ExamImageInfo> examImageInfoList = remedicals.map((e) { final imgInfo = e.terminalImages!; final vidUrl = measureData.chooseImageUrl(imgInfo); return ExamImageInfo( vidUrl, imgInfo.previewUrl!, remedicalCode: e.remedicalCode, ); }).toList(); Get.delete<MeasureController>(); measureController = Get.put(MeasureController( "", imagesFetchFunc: (code) async { return examImageInfoList; }, enableLoopPlayback: widget.enableLoopPlayback, onLoopPlaybackEnableChanged: widget.onLoopPlaybackEnableChanged, )); await measureController.load(); final selectedImageIndex = examImageInfoList.indexWhere( (element) => element.url == measureData.itemCurrentImage, ); if (selectedImageIndex > -1) { // 命中当前选择图像 measureController.examInfo.selectedImageIndex = selectedImageIndex; } // MeasureLanguage.load(measureData.measureLanguage); } /// vid 切换时会触发到这里 void _onImageLoaded(Object sender, ExamImageInfo? e) async { // measureHandler.changeImageLoaded = true; if (!mounted) return; final currentImage = measureData.remedicalList.firstWhereOrNull( (element) => element.terminalImages!.imageUrl == e!.url || element.terminalImages!.originImageUrl == e.url, ); if (currentImage != null) { /// 获取图片详细信息 var remedicalInfo = await MeasureDataHelper.getImageInfo( currentImage.remedicalCode ?? '', e!.remedicalCode, ); if (remedicalInfo != null) { measureData.aiResults = remedicalInfo.diagnosisResult ?? ''; measure3DViewController.initParams(); /// ai 良恶性 判断是否有ai measureData.diagnosisConclusion = remedicalInfo.diagnosisConclusion; var surfaceFile = remedicalInfo.carotidResult?.surfaceFile; var cDNMdlFile = remedicalInfo.carotidResult?.cDNMdlFile; if (remedicalInfo.carotidResult != null && surfaceFile.isNotNullOrEmpty && cDNMdlFile.isNotNullOrEmpty) { /// [Carotid] ✅详情传入测量信息 measure3DViewController.carotidResult = remedicalInfo.carotidResult!; /// [Carotid] ✅需要在此通知 controller 存在颈动脉信息 measure3DViewController.exist3DData = true; measure3DViewController .handleChangeCarotid2DImage(remedicalInfo.recordCode!); } else { measure3DViewController.exist3DData = false; } // [Carotid] ✅只要更换图片都要切换到Vid 2D模式 if (measure3DViewController.curMeasureMode != MeasureMode.vidMode) { measure3DViewController.backToVidMode(); } try { if (e != null) { Future.delayed(const Duration(milliseconds: 100), () { measureController.playerController.play(); }); setState(() { _hideFullScreenLoading = true; measureHandler.newImageLoading = false; }); } } catch (error) { setState(() { _hideFullScreenLoading = true; measureHandler.newImageLoading = false; }); } } } } }