import 'dart:math'; import 'package:fis_i18n/i18n.dart'; import 'package:fis_jsonrpc/rpc.dart'; import 'package:fis_lib_business_components/index.dart'; import 'package:fis_lib_report/pages/theme.dart'; import 'package:fis_lib_report/report_edit.dart'; import 'package:fis_theme/theme.dart'; import 'package:fis_ui/base_define/page.dart'; import 'package:fis_ui/index.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:fis_common/index.dart'; import 'package:fis_measure/process/workspace/rpc_bridge.dart'; import 'package:vitalapp/pages/consultation_record_view/widgets/button_group.dart'; import 'package:vitalapp/pages/image_report_inner_view/widgets/cloud_image_item.dart'; import 'package:vitalapp/pages/patient/list/widgets/tab_button.dart'; import 'package:vitalapp/pages/patient/list/widgets/tab_button_group.dart'; import 'package:vitalapp/pages/report_edit/controller.dart'; import 'package:vitalapp/pages/report_edit/widgets/vocabulary_entry_report.dart'; import 'package:vitalapp/store/store.dart'; class ReporteditPage extends GetView implements FPage { const ReporteditPage({this.pageName = 'ReporteditPage', Key? key}) : super(key: key); @override final String pageName; @override Widget build(BuildContext context) { return _DesktopLayout(this); } } class _DesktopLayout extends StatefulWidget { ///父级节点 final FPage businessParent; _DesktopLayout(this.businessParent); @override State<_DesktopLayout> createState() => _DesktopLayoutState(); } class _DesktopLayoutState extends State<_DesktopLayout> { final controller = Get.find(); final ScrollController _scrollController = ScrollController(); double _scrollSensitivityFactor = 1.0; @override void initState() { if (kIsMobile) { _scrollSensitivityFactor = 10.0; } initFTheme(); super.initState(); } void initFTheme() async { final theme = AppTheme(themeMode: ThemeMode.system); await FTheme.init(theme); } @override Widget build(BuildContext context) { if (Store.user.token?.isEmpty ?? true) { controller.initParameters(); } final body = _buildBodyView(); return Scaffold( body: Stack( children: [ body, Positioned( right: 0, top: 0, bottom: 60, child: Obx(() { if (controller.state.isEntryDisplayed) { return VocabularyEntryReport(onClose: () { controller.state.isEntryDisplayed = false; }); } else { return FSizedBox(); } }), ), ], ), ); } ///报告内容视图 Widget _buildReportContentView(BuildContext context) { final state = controller.state; var body = Obx(() { final jsonStr = controller.state.selectedTemplate.reportTemplateJson ?? ''; if (jsonStr.isEmpty) { return FSizedBox( width: kIsMobile ? 540 : 810, ); } return Container( color: Colors.white, margin: EdgeInsets.only(top: 60), child: Listener( onPointerSignal: (pointerSignal) { if (pointerSignal is PointerScrollEvent) { final newOffset = _scrollController.offset + pointerSignal.scrollDelta.dy * _scrollSensitivityFactor; if (newOffset <= _scrollController.position.maxScrollExtent && newOffset >= _scrollController.position.minScrollExtent) { _scrollController.jumpTo(newOffset); } } }, child: ListView( controller: _scrollController, shrinkWrap: true, children: [ FReportEditPage( reporter: Store.user.displayName, reportDate: DateTime.now(), jsonStr: jsonStr, onSelect: controller.onSelect, patinentAge: state.patientAge, patinentName: state.patientName, patinentSex: state.patientSex, selectEntry: i18nBook.remedical.selectWord.t, revoke: i18nBook.common.revoke.t, selectImageHint: i18nBook.remedical.clickAndSelectImage.t, ), ], ), ), ); }); return Stack( children: [ body, Positioned( left: 15, top: 15, child: Obx(() { ///编辑报告不支持修改模板 if (controller.state.templates.isEmpty || controller.state.reportCode.isNotEmpty) { return FSizedBox(); } return FSelect( source: controller.state.templates, width: 320, optionValueExtractor: (t) { return t.value; }, optionLabelExtractor: (t) { return t.title; }, textColor: Colors.black, value: controller.state.selectedTemplate.reportTemplateCode, onSelectChanged: (value, index) async { controller.onSelectedTemplateChange(value); }, ); }), ), ], ); } ///图像展示 Widget _buildImagsView(BuildContext context) { return Obx( () { if (controller.state.selectImageTabIndex == 0) { return _buildCloudImagesView(context); } else if (controller.state.selectImageTabIndex == 1) { if (controller.state.remedicalMeasured.length == 0) { return FExpanded( child: NoDataWidget(), ); } return _buildMeasureImageOrAiImageContentView(context); } else if (controller.state.selectImageTabIndex == 3) { if (controller.state.remedicalAISelectedInfos.length == 0) { return FExpanded( child: NoDataWidget(), ); } return _buildAiImagesView(context); } return SizedBox(); }, ); } ///一排操作按钮(提交、预览、返回) Widget _buildOperateLine() { var buttonStyle = ButtonStyle( padding: MaterialStateProperty.all(EdgeInsets.symmetric(vertical: 15)), backgroundColor: MaterialStateProperty.all(Colors.white), foregroundColor: MaterialStateProperty.all(Colors.black), ); final mediaQuery = MediaQuery.of(context); ///屏幕缩放比例 final devicePixelRatio = mediaQuery.devicePixelRatio; final textStyle = TextStyle( fontSize: (devicePixelRatio > 1 && !i18nBook.isCurrentChinese) ? 10 : 14, ); return SizedBox( height: 50, child: ButtonGroup( buttonPadding: 25, children: [ ElevatedButton( onPressed: () async { await controller.submitReport(); }, style: ButtonStyle( padding: MaterialStateProperty.all( EdgeInsets.symmetric(vertical: 15), ), ), child: FText( i18nBook.common.submit.t, style: textStyle, ), ), ElevatedButton( onPressed: () async { await controller.refreshImages(); setState(() {}); }, style: buttonStyle, child: FText( i18nBook.common.refresh.t, style: textStyle, ), ), ElevatedButton( onPressed: () async { Get.back(); }, style: buttonStyle, child: FText( i18nBook.common.back.t, style: textStyle, ), ), ], ), ); } ///云端图像 FWidget _buildCloudImagesView(BuildContext context) { final leftWidth = (MediaQuery.of(context).size.width - 90) / 3; final imageWidth = kIsMobile ? 300.0 : (leftWidth / 3 + 10); bool carotidLeftIsNotEmpty = controller.state.cloudCarotidLeftImages.isNotEmpty; bool carotidRightIsNotEmpty = controller.state.cloudCarotidRightImages.isNotEmpty; List children = []; if (controller.state.remedicalItemList.isNotEmpty) { children.add( _buildGridCloudImagesView( imageWidth, controller.state.remedicalItemList), ); } if (carotidLeftIsNotEmpty || carotidRightIsNotEmpty) { children.add(_buildCarotidTabs()); if (controller.state.carotidVAS == CarotidScanTypeEnum.CarotidLeft) { children.add(_buildGridCloudImagesView( imageWidth, controller.state.cloudCarotidLeftImages)); } else if (controller.state.carotidVAS == CarotidScanTypeEnum.CarotidRight) { children.add(_buildGridCloudImagesView( imageWidth, controller.state.cloudCarotidRightImages)); } } if (children.length == 0) { return FExpanded( child: NoDataWidget(), ); } return FExpanded( child: FContainer( width: leftWidth, color: Colors.white, child: FListView( shrinkWrap: true, children: children, ), ), ); } FWidget _buildCarotidTabs() { bool carotidLeftIsNotEmpty = controller.state.cloudCarotidLeftImages.isNotEmpty; bool carotidRightIsNotEmpty = controller.state.cloudCarotidRightImages.isNotEmpty; return FContainer( margin: EdgeInsets.only(bottom: 5, top: 15, left: 10), child: FRow( children: [ if (carotidLeftIsNotEmpty) ...[ _buildInkWell( CarotidScanTypeEnum.CarotidLeft, ), ], if (carotidRightIsNotEmpty) ...[ _buildInkWell( CarotidScanTypeEnum.CarotidRight, ) ], ], ), ); } FWidget _buildInkWell( CarotidScanTypeEnum carotidScanType, ) { final isChooseLeftCarotid = controller.state.carotidVAS == carotidScanType; final isLeftCarotid = carotidScanType == CarotidScanTypeEnum.CarotidLeft; return FInkWell( onTap: () { controller.state.carotidVAS = carotidScanType; }, child: FContainer( decoration: BoxDecoration( border: Border.all( width: 0.5, color: Colors.grey, ), color: isChooseLeftCarotid ? const Color(0xff2c77e5) : Colors.transparent, borderRadius: isLeftCarotid ? BorderRadius.only( topLeft: Radius.circular(4), bottomLeft: Radius.circular(4), ) : BorderRadius.only( topRight: Radius.circular(4), bottomRight: Radius.circular(4), ), ), padding: EdgeInsets.symmetric( horizontal: 20, vertical: 5, ), child: FText( isLeftCarotid ? i18nBook.remedical.leftNeck.t : i18nBook.remedical.rightNeck.t, style: TextStyle( color: isChooseLeftCarotid ? Colors.white : Colors.black, ), ), ), ); } void _handleDoubleClick( String imageUrl, int index, String remedicalCode, String? remedicalAISelectedInfoCode, { VidImageSource vidImageSource = VidImageSource.Remedical, }) { controller.enterVidMeasurePage( imageUrl, index, remedicalCode, remedicalAISelectedInfoCode, ); controller.getRemedicalList(); } ///测量图像 Widget _buildMeasureImageOrAiImageContentView(BuildContext context, [bool? isAiImage = false]) { final leftWidth = (MediaQuery.of(context).size.width - 90) * 7 / 15; final imageWidth = kIsMobile ? 300.0 : (leftWidth / 3 - 40); return Expanded( child: Container( width: leftWidth, child: Obx(() { return GridView.builder( controller: ScrollController(), itemCount: isAiImage! ? controller.state.remedicalAISelectedInfos.length : controller.state.remedicalMeasured.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, ), itemBuilder: (BuildContext context, int index) { return _buildMeasurelImage( index, imageWidth, ); }, ); }), ), ); } /// AI图像 Widget _buildAiImagesView(BuildContext context) { final leftWidth = (MediaQuery.of(context).size.width - 90) * 7 / 15; final imageWidth = leftWidth / 3 + 10; return Expanded( child: Container( width: leftWidth, color: Colors.white, child: Obx( () => FGridView.builder( controller: ScrollController(), itemCount: controller.state.remedicalAISelectedInfos.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, ), itemBuilder: (BuildContext context, int index) { var imageRemedicalAISelectedInfo = controller.state.remedicalAISelectedInfos[index]; var imageInfo = RemedicalInfoDTO( recordCode: imageRemedicalAISelectedInfo.recordCode, remedicalCode: imageRemedicalAISelectedInfo.remedicalAISelectedInfoCode, diagnosisConclusion: imageRemedicalAISelectedInfo.diagnosisConclusion, diagnosisOrgans: imageRemedicalAISelectedInfo.diagnosisOrgans, terminalImages: TerminalImageDTO( previewUrl: imageRemedicalAISelectedInfo.previewFileToken, imageUrl: imageRemedicalAISelectedInfo.aICdnFileToken, coverImageUrl: imageRemedicalAISelectedInfo.aIFileToken, originImageUrl: imageRemedicalAISelectedInfo.orginalFileToken, ), ); var terminalImage = imageInfo.terminalImages ?? TerminalImageDTO(); return CloudImageItem( index, imageInfo, onTap: () { var coverImageUrl = terminalImage.coverImageUrl ?? ''; if (coverImageUrl.isNullOrEmpty) { coverImageUrl = terminalImage.previewUrl ?? ''; } controller.onSelect.emit(this, coverImageUrl); final selectedImages = controller.state.selectedImages; if (selectedImages.contains(coverImageUrl)) { controller.state.selectedImages.remove(coverImageUrl); } else { controller.state.selectedImages.add(coverImageUrl); } }, onDoubleTap: () { _handleDoubleClick( terminalImage.imageUrl ?? '', index, imageInfo.remedicalCode ?? '', imageRemedicalAISelectedInfo.remedicalAISelectedInfoCode, vidImageSource: VidImageSource.AiResultModifier, ); }, imageWidth: imageWidth, // description: translation_delegate_helper.translateDescription( // imageInfo.application, // imageInfo.applicationCategory, // ), isSelected: controller.state.selectedImages .contains(terminalImage.coverImageUrl), onSelectedInputChange: controller.onSelectedInputChange, ); }, ), ), ), ); } /// 单个图像 Widget _buildMeasurelImage( int index, double imageWidth, ) { return Obx(() { final measureImage = controller.state.remedicalMeasured[index]; final fileToken = measureImage.measuredFileToken ?? ''; final previewFileToken = measureImage.previewFileToken ?? ''; bool _isSelect = controller.state.selectedImages.contains(fileToken); return FStack( children: [ FMouseRegion( cursor: SystemMouseCursors.click, child: FGestureDetector( businessParent: this.widget.businessParent, name: "measureImage:$index", onTap: () { controller.onMersureImageTap(fileToken, index); }, child: FContainer( decoration: BoxDecoration( border: Border.all( width: 4, color: _isSelect ? const Color(0xff2c77e5) : Colors.transparent), color: Colors.black, ), width: imageWidth, height: imageWidth, margin: const EdgeInsets.all(10), child: FImage.network( previewFileToken, fit: BoxFit.fitWidth, width: imageWidth, height: imageWidth, ), ), ), ), FPositioned( left: 20, top: 15, child: FText( (index + 1).toString(), style: TextStyle(color: Colors.white), ), ), ], ); }); } FWidget _buildGridCloudImagesView( double imageWidth, List remedicalItemList) { return FGridView.builder( itemCount: remedicalItemList.length, shrinkWrap: true, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, ), itemBuilder: (BuildContext context, int index) { var imageInfo = remedicalItemList[index]; var terminalImage = remedicalItemList[index].terminalImages ?? TerminalImageDTO(); return Obx(() { bool isSelected = controller.state.selectedImages .contains(terminalImage.coverImageUrl); return CloudImageItem( index, imageInfo, onTap: () { var coverImageUrl = terminalImage.coverImageUrl ?? ''; if (coverImageUrl.isNullOrEmpty) { coverImageUrl = terminalImage.previewUrl ?? ''; } controller.onSelect.emit(this, coverImageUrl); final selectedImages = controller.state.selectedImages; if (selectedImages.contains(coverImageUrl)) { controller.state.selectedImages.remove(coverImageUrl); } else { controller.state.selectedImages.add(coverImageUrl); } }, onDoubleTap: () { _handleDoubleClick( terminalImage.imageUrl ?? '', index, imageInfo.remedicalCode ?? '', imageInfo.remedicalCode ?? '', ); }, imageWidth: imageWidth, // description: translation_delegate_helper.translateDescription( // imageInfo.application, // imageInfo.applicationCategory, // ), isSelected: isSelected, onSelectedInputChange: controller.onSelectedInputChange, ); }); }, ); } Widget _buildBodyView() { var padding = EdgeInsets.symmetric(vertical: 8, horizontal: 0); final allWidth = MediaQuery.of(context).size.width; // 930 = 左侧菜单栏(90)+ 报告编辑区域(540)+ 分割线(30) final fittedWidth = allWidth - (kIsMobile ? 670 : 930); // 600 是图像区域最大宽度限制,防止在大分辨率下,图像区域过大造成不美观 const maxWidth = 400.0; final imagePoolWidth = min(fittedWidth, maxWidth); return Container( decoration: BoxDecoration( border: Border.all( width: 1, color: Colors.grey, )), child: Row( children: [ Expanded(child: _buildReportContentView(context)), const VerticalDivider( width: 1, color: Colors.grey, ), const SizedBox(width: 10), Container( width: imagePoolWidth, child: Column( children: [ const SizedBox(height: 10), Container( child: Obx( () { return TabButtonGroup( [ TabButton( padding: padding, tabTitle: i18nBook.remedical.cloudImageShort.t, index: 0, activeIndex: controller.state.selectImageTabIndex, onCallbackTap: () { controller.state.selectImageTabIndex = 0; RPCBridge.ins.source = VidImageSource.Remedical; }, ), FSizedBox(width: 10), TabButton( padding: padding, tabTitle: i18nBook.measure.measureImageShort.t, index: 1, activeIndex: controller.state.selectImageTabIndex, onCallbackTap: () { controller.remedicalsController .findRemedicalMeasuredInfoAsync(); controller.state.selectImageTabIndex = 1; }, ), if (controller.state.consultationCode.isNotEmpty) ...[ FSizedBox(width: 10), TabButton( padding: padding, tabTitle: i18nBook.realTimeConsultation .consultationImageShort.t, index: 2, activeIndex: controller.state.selectImageTabIndex, onCallbackTap: () { controller.state.selectImageTabIndex = 2; }, ) ], ], ); }, ), ), _buildImagsView(context), const SizedBox(height: 5), _buildOperateLine(), const SizedBox(height: 15), ], ), ), ], ), ); } }