measure_view.dart 16 KB

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