measure_view.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. import 'dart:developer';
  2. import 'package:fis_common/index.dart';
  3. import 'package:fis_jsonrpc/rpc.dart';
  4. import 'package:fis_measure/interfaces/process/workspace/exam_info.dart';
  5. import 'package:fis_measure/interfaces/process/workspace/measure_3d_view_controller.dart';
  6. import 'package:fis_measure/process/layout/configuration.dart';
  7. import 'package:fis_measure/process/workspace/measure_controller.dart';
  8. import 'package:fis_measure/process/workspace/measure_data_controller.dart';
  9. import 'package:fis_measure/process/workspace/measure_data_helper.dart';
  10. import 'package:fis_measure/process/workspace/measure_handler.dart';
  11. import 'package:fis_measure/process/workspace/measure_3d_view_controller.dart';
  12. import 'package:fis_measure/view/gesture/positioned_cursor.dart';
  13. import 'package:fis_measure/view/measure/measure_config/widgets/measure_configuration_style.dart';
  14. import 'package:fis_measure/view/measure/measure_images_bar.dart';
  15. import 'package:fis_measure/view/measure/measure_player.dart';
  16. import 'package:fis_measure/view/measure/measure_tool_panel.dart';
  17. import 'package:fis_measure/view/measure/measure_view_controller.dart';
  18. import 'package:fis_measure/view/player/control_board/operate_bar.dart';
  19. import 'package:fis_ui/index.dart';
  20. import 'package:fis_ui/interface/interactive_container.dart';
  21. import 'package:flutter/material.dart';
  22. import 'package:get/get.dart';
  23. /// 测量主页面
  24. class MeasureMainPage extends StatefulWidget
  25. implements FWidget, FInteractiveContainer {
  26. const MeasureMainPage(
  27. this.token,
  28. this.recordCode,
  29. this.patientCode,
  30. this.remedicalCode, {
  31. this.needRouterBack,
  32. this.enableLoopPlayback = false,
  33. this.onLoopPlaybackEnableChanged,
  34. this.remedicalAISelectedInfoCode,
  35. this.customWidgets = const [],
  36. this.hasQualityControlPermission = false,
  37. Key? key,
  38. }) : super(key: key);
  39. final String token;
  40. final String patientCode;
  41. final String remedicalCode;
  42. final String recordCode;
  43. final bool? needRouterBack; // 需要返回按钮【一版用于返回到报告编辑】
  44. final bool enableLoopPlayback;
  45. final ValueCallback<bool>? onLoopPlaybackEnableChanged;
  46. final String? remedicalAISelectedInfoCode;
  47. final List<FWidget> customWidgets;
  48. final bool hasQualityControlPermission;
  49. @override
  50. final String pageName = 'MeasureMainPage';
  51. @override
  52. State<StatefulWidget> createState() => _MeasureMainPageState();
  53. }
  54. class _MeasureMainPageState extends State<MeasureMainPage> {
  55. /// 病人及图像信息
  56. // String _curToken = '';
  57. String _curPatientCode = '';
  58. String _curRemedicalCode = '';
  59. String _curRecordCode = '';
  60. String? _curRemedicalAISelectedInfoCode = '';
  61. bool? _curNeedRouterBack;
  62. /// 数据
  63. final measureData = Get.find<MeasureDataController>();
  64. /// webview 控制器
  65. final measure3DViewController = Get.find<Measure3DViewController>();
  66. /// 测量项控制器
  67. final measureMetaController = Get.put(MeasureMetaController());
  68. double get devicePixelRatio =>
  69. kIsMobile ? 1 : MediaQuery.of(context).devicePixelRatio;
  70. /// 隐藏全屏loadding
  71. bool _hideFullScreenLoading = false;
  72. /// 图片loadding
  73. late bool imageLoading = false;
  74. late final measureHandler = Get.find<MeasureHandler>();
  75. /// 测量控制器
  76. late MeasureController measureController;
  77. /// 响应图片切换事件通知
  78. void _onNewImageLoadStatueChanged(sender, e) {
  79. imageLoading = e;
  80. setState(() {});
  81. }
  82. /// 响应全屏状态切换事件
  83. void _onChangeFullScreenState(sender, e) {
  84. setState(() {});
  85. }
  86. /// 响应工具面板状态切换事件
  87. void _onToolPanelStateChanged(sender, e) {
  88. setState(() {});
  89. }
  90. /// 在此处动态刷新测量窗口,而不是重置窗口
  91. void _onUpdateState(sender, Map<String, String> parameters) async {
  92. _curNeedRouterBack = false;
  93. bool needUpdate = false;
  94. if (_curRecordCode == (parameters['recordCode'] ?? '')) {
  95. if (_curRemedicalCode == (parameters['remedicalCode'] ?? '')) {
  96. } else {
  97. measureHandler.changeImageByRemedicalCode
  98. .emit(this, parameters['remedicalCode'] ?? '');
  99. }
  100. } else {
  101. needUpdate = true;
  102. }
  103. // _curToken = parameters['token'] ?? '';
  104. _curPatientCode = parameters['patientCode'] ?? '';
  105. _curRemedicalCode = parameters['remedicalCode'] ?? '';
  106. _curRecordCode = parameters['recordCode'] ?? '';
  107. _curRemedicalAISelectedInfoCode =
  108. parameters['remedicalAISelectedInfoCode'] ?? '';
  109. if (needUpdate) {
  110. await LayoutConfiguration.ins.loadData();
  111. List<RemedicalInfoDTO> remedicals = await _getCurRemedicals();
  112. _initMouseModuel();
  113. _initCarotidModuel(remedicals);
  114. measureData.remedicalList = remedicals;
  115. var remedicalInfo = await MeasureDataHelper.getImageInfo
  116. .call(_curRemedicalCode, _curRemedicalAISelectedInfoCode);
  117. if (remedicalInfo != null) {
  118. measureData.aiResults = remedicalInfo.diagnosisResult ?? '';
  119. if (remedicalInfo.terminalImages != null) {
  120. _hideFullScreenLoading = true;
  121. measureData.itemCurrentImage =
  122. measureData.chooseImageUrl(remedicalInfo.terminalImages!);
  123. getExamImageInfoList(remedicals);
  124. measureHandler.changeImageList.emit(this, remedicals);
  125. }
  126. }
  127. measureController.imageLoaded.removeListener(_onImageLoaded);
  128. measureController.imageLoaded.addListener(_onImageLoaded);
  129. }
  130. _setCurImageData();
  131. }
  132. /// 将当前的图像信息同步到 measureData 中
  133. void _setCurImageData() {
  134. measureData.measureInfoData = MeasureInfoData(
  135. patientCode: _curPatientCode,
  136. recordCode: _curRecordCode,
  137. remedicalCode: _curRemedicalCode,
  138. remedicalAISelectedInfoCode: _curRemedicalAISelectedInfoCode,
  139. );
  140. }
  141. /// 先加载图像布局配置,然后加载全局数据
  142. void _loadLayoutConfig() async {
  143. await LayoutConfiguration.ins.loadData();
  144. setState(() {
  145. _initData();
  146. });
  147. }
  148. @override
  149. void initState() {
  150. _curPatientCode = widget.patientCode;
  151. _curRemedicalCode = widget.remedicalCode;
  152. _curRecordCode = widget.recordCode;
  153. _curNeedRouterBack = widget.needRouterBack;
  154. _curRemedicalAISelectedInfoCode = widget.remedicalAISelectedInfoCode;
  155. _hideFullScreenLoading = false;
  156. _loadLayoutConfig();
  157. _setCurImageData();
  158. measureData.curMeasureDataChanged.addListener(_onUpdateState);
  159. measureHandler.onNewImageLoadStatueChanged
  160. .addListener(_onNewImageLoadStatueChanged);
  161. measureHandler.onChangeFullScreenState
  162. .addListener(_onChangeFullScreenState);
  163. measureHandler.onToolPanelStateChanged
  164. .addListener(_onToolPanelStateChanged);
  165. super.initState();
  166. }
  167. @override
  168. void dispose() {
  169. measureData.curMeasureDataChanged.removeListener(_onUpdateState);
  170. measureHandler.onNewImageLoadStatueChanged
  171. .removeListener(_onNewImageLoadStatueChanged);
  172. measureHandler.onChangeFullScreenState
  173. .removeListener(_onChangeFullScreenState);
  174. measureHandler.onToolPanelStateChanged
  175. .removeListener(_onToolPanelStateChanged);
  176. super.dispose();
  177. }
  178. @override
  179. FWidget build(BuildContext context) {
  180. FWidget body;
  181. if (!_hideFullScreenLoading) {
  182. const loadingWidget = FCenter(child: FCircularProgressIndicator());
  183. body = FRow(
  184. children: const [
  185. FExpanded(
  186. child: loadingWidget,
  187. ),
  188. ],
  189. );
  190. } else {
  191. body = FColumn(children: [
  192. FExpanded(
  193. child: FRow(
  194. children: [
  195. FOffstage(
  196. child: MeasureToolPanel(
  197. customWidgets: widget.customWidgets,
  198. ),
  199. offstage: measureHandler.fullScreenState ||
  200. !measureHandler.toolPanelState),
  201. const FVerticalDivider(),
  202. FExpanded(
  203. child: FColumn(
  204. mainAxisSize: MainAxisSize.max,
  205. children: [
  206. FExpanded(
  207. child: FStack(
  208. children: [
  209. if (!imageLoading)
  210. MeasureRightBoard(
  211. key: ValueKey(measureData.itemCurrentImage),
  212. ),
  213. if (imageLoading)
  214. FContainer(
  215. color: Colors.black,
  216. child: const FCenter(
  217. child: FCircularProgressIndicator(),
  218. ),
  219. ),
  220. ],
  221. )),
  222. // if (!measureHandler.fullScreenState) const MeasureImagesBar(),
  223. ],
  224. ),
  225. ),
  226. ],
  227. ),
  228. ),
  229. FOffstage(
  230. child: FRow(children: [
  231. FColumn(
  232. children: [
  233. const OperateBar(),
  234. _curNeedRouterBack ?? false
  235. ? FContainer(
  236. width: 300 / devicePixelRatio,
  237. padding:
  238. EdgeInsets.only(bottom: 20 / devicePixelRatio),
  239. child: FIconButton(
  240. businessParent: widget,
  241. name: "back",
  242. onPressed: () {
  243. Get.back();
  244. },
  245. icon: const FIcon(
  246. Icons.arrow_back,
  247. color: Colors.white,
  248. size: 36,
  249. ),
  250. ),
  251. )
  252. : const FSizedBox()
  253. ],
  254. ),
  255. FExpanded(
  256. child: MeasureImagesBar(
  257. hasQualityControlPermission:
  258. widget.hasQualityControlPermission,
  259. ),
  260. ),
  261. ]),
  262. offstage: measureHandler.fullScreenState),
  263. ]);
  264. }
  265. return FCenter(
  266. child: FContainer(
  267. color: _hideFullScreenLoading
  268. ? const Color.fromRGBO(70, 70, 70, 1)
  269. : Colors.black,
  270. child: body,
  271. ),
  272. );
  273. }
  274. /// 获取当前状态下的图像集
  275. Future<List<RemedicalInfoDTO>> _getCurRemedicals() async {
  276. List<RemedicalInfoDTO> remedicals = [];
  277. var value = await MeasureDataHelper.getRemedicalList(
  278. _curPatientCode,
  279. _curRecordCode,
  280. );
  281. for (var remedicalItemList in value) {
  282. remedicals.addAll(remedicalItemList.remedicalList ?? []);
  283. }
  284. return remedicals;
  285. }
  286. /// 初始化颈动脉模块
  287. /// [Carotid] ✅遍历出颈动脉信息列表,传给壳子
  288. void _initCarotidModuel(List<RemedicalInfoDTO> remedicals) {
  289. List<Ultra3DResourceInfo> ultra3DResourceInfos = [];
  290. for (var remedical in remedicals) {
  291. if (remedical.carotidResult != null) {
  292. ultra3DResourceInfos.add(Ultra3DResourceInfo(remedical.carotidResult!));
  293. }
  294. }
  295. measure3DViewController.ultra3DResourceInfoList = ultra3DResourceInfos;
  296. measure3DViewController.recordId = _curRecordCode;
  297. measure3DViewController.notifyShellLoadAllModel();
  298. }
  299. /// 初始化卡尺样式部分
  300. Future<void> _initMouseModuel() async {
  301. final mouseState = Get.put<IMouseState>(MouseState());
  302. MeasureSystemSettingDTO result = MeasureSystemSettingDTO(
  303. cursorSize: 16,
  304. shapeCursorSize: 30,
  305. showResultWindow: true,
  306. fontSize: 14,
  307. showCursorLine: true,
  308. showDepthGuideline: true,
  309. showBriefAnnotation: true,
  310. autoSnapDistance: '20',
  311. minCursorDistance: '20',
  312. annotationFontSize: 14,
  313. distanceUnit: Unit.cm,
  314. areaUnit: Unit.cm2,
  315. velocityUnit: Unit.cms,
  316. timeUnit: Unit.s,
  317. );
  318. try {
  319. result = await MeasureDataHelper.getMeasureSystemSettingAsync();
  320. } catch (e) {
  321. log("getMeasureSystemSettingAsync error: $e");
  322. }
  323. measureData.measureSystemSetting = result;
  324. measureData.checkErrorUnitConfig();
  325. measureData.checkErrorStyleConfig();
  326. mouseState.cursorType =
  327. getMeasureSystemSettingCursorType(result.cursorType);
  328. mouseState.cursorSize = result.cursorSize.toDouble();
  329. }
  330. /// 测量页全局数据首次初始化
  331. void _initData() async {
  332. List<RemedicalInfoDTO> remedicals = await _getCurRemedicals();
  333. _initMouseModuel();
  334. _initCarotidModuel(remedicals);
  335. measureData.remedicalList = remedicals;
  336. var remedicalInfo = await MeasureDataHelper.getImageInfo(
  337. _curRemedicalCode, _curRemedicalAISelectedInfoCode);
  338. if (remedicalInfo != null) {
  339. measureData.aiResults = remedicalInfo.diagnosisResult ?? '';
  340. if (remedicalInfo.terminalImages != null) {
  341. _hideFullScreenLoading = true;
  342. final imgInfo = remedicalInfo.terminalImages!;
  343. measureData.itemCurrentImage = measureData.chooseImageUrl(imgInfo);
  344. getExamImageInfoList(remedicals);
  345. }
  346. }
  347. measureController.imageLoaded.removeListener(_onImageLoaded);
  348. measureController.imageLoaded.addListener(_onImageLoaded);
  349. }
  350. /// 获取测量图片所需的图片组 并且写入控制器中 加载
  351. void getExamImageInfoList(List<RemedicalInfoDTO> remedicals) async {
  352. /// 检查中图片信息表
  353. List<ExamImageInfo> examImageInfoList = remedicals.map((e) {
  354. final imgInfo = e.terminalImages!;
  355. final vidUrl = measureData.chooseImageUrl(imgInfo);
  356. return ExamImageInfo(
  357. vidUrl,
  358. imgInfo.previewUrl!,
  359. remedicalCode: e.remedicalCode,
  360. );
  361. }).toList();
  362. Get.delete<MeasureController>();
  363. measureController = Get.put(MeasureController(
  364. "",
  365. imagesFetchFunc: (code) async {
  366. return examImageInfoList;
  367. },
  368. enableLoopPlayback: widget.enableLoopPlayback,
  369. onLoopPlaybackEnableChanged: widget.onLoopPlaybackEnableChanged,
  370. ));
  371. await measureController.load();
  372. final selectedImageIndex = examImageInfoList.indexWhere(
  373. (element) => element.url == measureData.itemCurrentImage,
  374. );
  375. if (selectedImageIndex > -1) {
  376. // 命中当前选择图像
  377. measureController.examInfo.selectedImageIndex = selectedImageIndex;
  378. }
  379. // MeasureLanguage.load(measureData.measureLanguage);
  380. }
  381. /// vid 切换时会触发到这里
  382. void _onImageLoaded(Object sender, ExamImageInfo? e) async {
  383. // measureHandler.changeImageLoaded = true;
  384. if (!mounted) return;
  385. final currentImage = measureData.remedicalList.firstWhereOrNull(
  386. (element) =>
  387. element.terminalImages!.imageUrl == e!.url ||
  388. element.terminalImages!.originImageUrl == e.url,
  389. );
  390. if (currentImage != null) {
  391. /// 获取图片详细信息
  392. var remedicalInfo = await MeasureDataHelper.getImageInfo(
  393. currentImage.remedicalCode ?? '',
  394. e!.remedicalCode,
  395. );
  396. if (remedicalInfo != null) {
  397. measureData.aiResults = remedicalInfo.diagnosisResult ?? '';
  398. measure3DViewController.initParams();
  399. /// ai 良恶性 判断是否有ai
  400. measureData.diagnosisConclusion = remedicalInfo.diagnosisConclusion;
  401. var surfaceFile = remedicalInfo.carotidResult?.surfaceFile;
  402. var cDNMdlFile = remedicalInfo.carotidResult?.cDNMdlFile;
  403. if (remedicalInfo.carotidResult != null &&
  404. surfaceFile.isNotNullOrEmpty &&
  405. cDNMdlFile.isNotNullOrEmpty) {
  406. /// [Carotid] ✅详情传入测量信息
  407. measure3DViewController.carotidResult = remedicalInfo.carotidResult!;
  408. /// [Carotid] ✅需要在此通知 controller 存在颈动脉信息
  409. measure3DViewController.exist3DData = true;
  410. measure3DViewController
  411. .handleChangeCarotid2DImage(remedicalInfo.recordCode!);
  412. } else {
  413. measure3DViewController.exist3DData = false;
  414. }
  415. // [Carotid] ✅只要更换图片都要切换到Vid 2D模式
  416. if (measure3DViewController.curMeasureMode != MeasureMode.vidMode) {
  417. measure3DViewController.backToVidMode();
  418. }
  419. try {
  420. if (e != null) {
  421. Future.delayed(const Duration(milliseconds: 100), () {
  422. measureController.playerController.play();
  423. });
  424. setState(() {
  425. _hideFullScreenLoading = true;
  426. measureHandler.newImageLoading = false;
  427. });
  428. }
  429. } catch (error) {
  430. setState(() {
  431. _hideFullScreenLoading = true;
  432. measureHandler.newImageLoading = false;
  433. });
  434. }
  435. }
  436. }
  437. }
  438. }