measure_page_test.dart 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. import 'dart:typed_data';
  2. import 'package:fis_jsonrpc/rpc.dart';
  3. import 'package:fis_measure/interfaces/enums/annotation.dart';
  4. import 'package:fis_measure/interfaces/process/items/item.dart';
  5. import 'package:fis_measure/interfaces/process/items/item_metas.dart';
  6. import 'package:fis_measure/interfaces/process/items/terms.dart';
  7. import 'package:fis_measure/interfaces/process/items/types.dart';
  8. import 'package:fis_measure/interfaces/process/player/play_controller.dart';
  9. import 'package:fis_measure/interfaces/process/standard_line/calibration.dart';
  10. import 'package:fis_measure/interfaces/process/visuals/visual_area.dart';
  11. import 'package:fis_measure/interfaces/process/workspace/application.dart';
  12. import 'package:fis_measure/interfaces/process/workspace/exam_info.dart';
  13. import 'package:fis_measure/interfaces/process/workspace/measure_controller.dart';
  14. import 'package:fis_measure/process/items/item_meta_convert.dart';
  15. import 'package:fis_measure/process/workspace/measure_3d_view_controller.dart';
  16. import 'package:fis_measure/process/workspace/measure_controller.dart';
  17. import 'package:fis_measure/process/workspace/measure_data_controller.dart';
  18. import 'package:fis_measure/view/measure/measure_main_view.dart';
  19. import 'package:flutter/material.dart';
  20. import 'package:get/get.dart';
  21. import 'package:vid/us/vid_us_mode.dart';
  22. import 'item_create_test.dart';
  23. import 'process/workspace/measure_handler.dart';
  24. class MeasureDataTester {
  25. static Future<List<RemedicalItemList>> getRemedicalList(
  26. String patientCode,
  27. String recordCode,
  28. String token,
  29. ) async {
  30. return [];
  31. }
  32. static Future<RemedicalInfoDTO?> getImageInfo(
  33. String remedicalCode,
  34. String token,
  35. ) async {
  36. return null;
  37. }
  38. static Future<MeasureApplicationDTO?> getMeasureApplication(
  39. dynamic args) async {
  40. return null;
  41. }
  42. static Future<bool> saveUserDefinedMeasureApplicationAsync(
  43. dynamic args) async {
  44. return true;
  45. }
  46. static Future<bool?> saveImage(
  47. Uint8List imageBytes,
  48. String patientCode,
  49. String recordCode,
  50. String remedicalCode,
  51. String measuredData,
  52. ) async {
  53. return null;
  54. }
  55. static Future<void> saveMeasureSystemSettingAsync(
  56. MeasureSystemSettingDTO measureSystemSetting) async {
  57. return;
  58. }
  59. static Future<MeasureSystemSettingDTO?> getMeasureSystemSettingAsync() async {
  60. return null;
  61. }
  62. static Future<String> shareImage(String vid) async {
  63. return '';
  64. }
  65. static Future<CommentItemResultDTO?> getCommentsByApplicationAsync(
  66. String applicationName, String categoryName) async {
  67. return null;
  68. }
  69. static Future<PresetCommentItemResultDTO?> getPresetCommentsAsync() async {
  70. return null;
  71. }
  72. static Future<bool?> resetUserCommentsAsync(
  73. String applicationName, String categoryName) async {
  74. return null;
  75. }
  76. static Future<bool?> saveUserDefinedCommentsAsync(
  77. String applicationName,
  78. String categoryName,
  79. List<CommentItemDTO>? add,
  80. List<CommentItemDTO>? delete,
  81. List<UpdateCommentItemDTO>? update) async {
  82. return null;
  83. }
  84. }
  85. class MeasureTestPage extends StatefulWidget {
  86. const MeasureTestPage({Key? key}) : super(key: key);
  87. // ignore: non_constant_identifier_names
  88. static List<dynamic> MetaDTOList = [];
  89. @override
  90. State<StatefulWidget> createState() => _MeasureTestPageState();
  91. }
  92. class _MeasureTestPageState extends State<MeasureTestPage> {
  93. static const C_LINEAR_TISSUE =
  94. // "http://cdn-bj.fis.plus/9F066341FA874E21B48CDE247C13D495.vid"; //B TVI TD
  95. // "http://cdn-bj.fis.plus/974BABA5113640639FD749E06DD7DA5B.vid"; //B CF CW
  96. "http://cdn-bj.fis.plus/0B344F48BA574ECD82B7FEDB8848421A.vid"; //单幅TM
  97. // "http://cdn-bj.fis.plus/3379F38302884C2991D90FBDFB0DEA7E.dat"; //单幅TM(2)
  98. // "http://cdn-bj.fis.plus/6A99AD2530864616B64355A8EA9AE3EC.vid";
  99. // "http://cdn-bj.fis.plus/F26C6E5D57A7472A97E9EB543DF0D16C.vid"; // 单幅Convex
  100. // "http://cdn-bj.fis.plus/6B6E069659D14E7299EB9F6EFCDE9C8C.vid"; //双幅单TissueConvex
  101. // "http://cdn-bj.fis.plus/062643B82365437DB95F3811580AF3ED.vid"; //四幅单模式
  102. // "http://cdn-bj.fis.plus/EA90D146049D416E8E466B7446E00001.vid"; //四幅Doppler
  103. // "http://cdn-bj.fis.plus/3rd_linearTvTissue2.vid"; //魔盒
  104. // "http://cdn-bj.fis.plus/81FFF8E5E078473FA687FBE81C4869B1.vid"; // 魔盒TV
  105. // "http://cdn-bj.fis.plus/7B450708A2784B1490304C82787349BE.vid";// 胎儿Zoom
  106. static const C_CONVEX_TISSUE =
  107. "http://cdn-bj.fis.plus/FEB1AAE5D9C24839BEE31B16E8CB450A.vid";
  108. final _3dc = Get.put<Measure3DViewController>(Measure3DViewController());
  109. final datac = Get.put<MeasureDataController>(MeasureDataController(
  110. MeasureDataTester.getRemedicalList,
  111. MeasureDataTester.getImageInfo,
  112. MeasureDataTester.getMeasureApplication,
  113. MeasureDataTester.saveUserDefinedMeasureApplicationAsync,
  114. MeasureDataTester.saveImage,
  115. MeasureDataTester.saveMeasureSystemSettingAsync,
  116. MeasureDataTester.getMeasureSystemSettingAsync,
  117. MeasureDataTester.shareImage,
  118. MeasureDataTester.getCommentsByApplicationAsync,
  119. MeasureDataTester.saveUserDefinedCommentsAsync,
  120. MeasureDataTester.resetUserCommentsAsync,
  121. MeasureDataTester.getPresetCommentsAsync,
  122. ));
  123. final measureHandler = Get.put(MeasureHandler());
  124. final controller = Get.put<IMeasureController>(MeasureController(
  125. "12345",
  126. imagesFetchFunc: (code) async {
  127. return <ExamImageInfo>[
  128. ExamImageInfo(C_LINEAR_TISSUE, C_LINEAR_TISSUE),
  129. ExamImageInfo(C_CONVEX_TISSUE, C_CONVEX_TISSUE)
  130. ];
  131. },
  132. ));
  133. bool loaded = false;
  134. int opType = 0;
  135. bool useArrowAnnotation = false;
  136. @override
  137. void initState() {
  138. measureHandler.changeImageLoaded;
  139. controller.load().then((value) {
  140. // 加载指定图像
  141. controller.examInfo.selectedImageIndex = 0;
  142. });
  143. controller.imageLoaded.addListener(onImageLoaded);
  144. super.initState();
  145. }
  146. @override
  147. void dispose() {
  148. controller.imageLoaded.removeListener(onImageLoaded);
  149. controller.dispose();
  150. Get.delete<IMeasureController>();
  151. super.dispose();
  152. }
  153. void onImageLoaded(Object sender, ExamImageInfo? e) {
  154. if (!mounted) return;
  155. if (e != null) {
  156. Future.delayed(const Duration(milliseconds: 100), () {
  157. controller.playerController.play();
  158. });
  159. setState(() {
  160. loaded = true;
  161. });
  162. }
  163. }
  164. @override
  165. Widget build(BuildContext context) {
  166. Widget body;
  167. if (!loaded) {
  168. const loadingWidget = Center(child: CircularProgressIndicator());
  169. body = Row(
  170. children: const [
  171. SizedBox(
  172. width: 300,
  173. child: loadingWidget,
  174. ),
  175. VerticalDivider(),
  176. Expanded(child: loadingWidget),
  177. ],
  178. );
  179. } else {
  180. body = Row(
  181. key: ValueKey(controller.examInfo.selectedImageIndex),
  182. children: [
  183. opType == 1
  184. ? const _MeasureLeftAnnotation()
  185. : const _MeasureLeftBoard(),
  186. const VerticalDivider(),
  187. const Expanded(
  188. child: MeasureRightBoard(),
  189. ),
  190. ],
  191. );
  192. }
  193. return Scaffold(
  194. backgroundColor: const Color.fromARGB(255, 53, 55, 51),
  195. appBar: AppBar(
  196. actions: [
  197. TextButton(
  198. onPressed: () {
  199. Get.find<IApplication>().clearRecords();
  200. },
  201. child: const Text(
  202. "清空",
  203. style: TextStyle(
  204. color: Colors.white,
  205. ),
  206. ),
  207. ),
  208. TextButton(
  209. onPressed: () {
  210. Get.find<IApplication>().undoRecord();
  211. },
  212. child: const Text(
  213. "撤销",
  214. style: TextStyle(
  215. color: Colors.white,
  216. ),
  217. ),
  218. ),
  219. TextButton(
  220. onPressed: () {
  221. final c = Get.find<IStandardLineCalibrationController>();
  222. if (c.isEditing) {
  223. c.cancelEdit();
  224. } else {
  225. c.enterEditMode();
  226. }
  227. },
  228. child: const Text(
  229. "校准线",
  230. style: TextStyle(
  231. color: Colors.white,
  232. ),
  233. ),
  234. ),
  235. TextButton.icon(
  236. onPressed: () {
  237. setState(() {
  238. if (useArrowAnnotation) {
  239. useArrowAnnotation = false;
  240. controller.workingApplication.switchAnnotation();
  241. } else {
  242. useArrowAnnotation = true;
  243. controller.workingApplication
  244. .switchAnnotation(AnnotationType.arrow);
  245. }
  246. });
  247. },
  248. icon: const Icon(Icons.arrow_right_alt_sharp),
  249. label: Text(
  250. "箭头",
  251. style: TextStyle(
  252. color: useArrowAnnotation ? Colors.amber : Colors.white,
  253. ),
  254. ),
  255. ),
  256. TextButton(
  257. onPressed: () {
  258. setState(() {
  259. opType = opType == 1 ? 0 : 1;
  260. });
  261. },
  262. child: Text(
  263. opType == 1 ? "注释" : "测量",
  264. style: const TextStyle(
  265. color: Colors.white,
  266. ),
  267. ),
  268. ),
  269. TextButton(
  270. onPressed: () {
  271. if (controller.examInfo.selectedImageIndex == 0) return;
  272. controller.examInfo.selectedImageIndex = 0;
  273. },
  274. child: Text(
  275. '线阵',
  276. style: TextStyle(
  277. color: controller.examInfo.selectedImageIndex == 0
  278. ? Colors.amber
  279. : Colors.white,
  280. ),
  281. ),
  282. ),
  283. TextButton(
  284. onPressed: () {
  285. if (controller.examInfo.selectedImageIndex == 1) return;
  286. controller.examInfo.selectedImageIndex = 1;
  287. },
  288. child: Text(
  289. '扇阵',
  290. style: TextStyle(
  291. color: controller.examInfo.selectedImageIndex == 1
  292. ? Colors.amber
  293. : Colors.white,
  294. ),
  295. ),
  296. ),
  297. ],
  298. leading: IconButton(
  299. onPressed: () {
  300. Navigator.of(context).pop();
  301. },
  302. icon: const Icon(Icons.arrow_back),
  303. ),
  304. ),
  305. body: body,
  306. floatingActionButton: _ModeTips(),
  307. floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
  308. );
  309. }
  310. }
  311. class MeasureRightBoard extends StatefulWidget {
  312. const MeasureRightBoard({Key? key}) : super(key: key);
  313. @override
  314. State<StatefulWidget> createState() => _MeasureRightBoardState();
  315. }
  316. class _MeasureRightBoardState extends State<MeasureRightBoard> {
  317. final playerController = Get.find<IPlayerController>();
  318. @override
  319. Widget build(BuildContext context) {
  320. return Container(
  321. padding: const EdgeInsets.all(8).copyWith(left: 0),
  322. child: Stack(
  323. children: const [
  324. MeasureMainView(),
  325. _PlayerTips(),
  326. ],
  327. ),
  328. );
  329. }
  330. }
  331. class _PlayerTips extends StatefulWidget {
  332. const _PlayerTips({Key? key}) : super(key: key);
  333. @override
  334. State<StatefulWidget> createState() => _PlayerTipsState();
  335. }
  336. class _PlayerTipsState extends State<_PlayerTips> {
  337. final playerController = Get.find<IPlayerController>();
  338. bool loading = false;
  339. String content = 'xxxxxxxxxxxxxxxxxxxx';
  340. @override
  341. void initState() {
  342. playerController.frameLoadStateChanged.addListener(_onLoadStateChanged);
  343. super.initState();
  344. }
  345. @override
  346. void dispose() {
  347. playerController.frameLoadStateChanged.removeListener(_onLoadStateChanged);
  348. super.dispose();
  349. }
  350. void _onLoadStateChanged(sender, bool e) {
  351. loading = e;
  352. if (loading) {
  353. Future.delayed(
  354. const Duration(milliseconds: 100),
  355. () {
  356. setState(() {
  357. content = loading ? "Loading。。。" : "";
  358. });
  359. },
  360. );
  361. } else {
  362. setState(() {
  363. content = "";
  364. });
  365. }
  366. }
  367. @override
  368. Widget build(BuildContext context) {
  369. return Positioned(
  370. child: Center(
  371. child: Text(
  372. content,
  373. style: const TextStyle(
  374. color: Colors.white,
  375. fontSize: 28,
  376. ),
  377. ),
  378. ),
  379. top: Get.size.height * 0.44,
  380. left: 0,
  381. right: 0,
  382. );
  383. }
  384. }
  385. class _MeasureLeftBoard extends StatefulWidget {
  386. const _MeasureLeftBoard({Key? key}) : super(key: key);
  387. @override
  388. State<StatefulWidget> createState() => _MeasureLeftBoardState();
  389. }
  390. class _MeasureLeftBoardState extends State<_MeasureLeftBoard> {
  391. // ignore: non_constant_identifier_names
  392. static final C_SUPPORTED_ITEMS = <String>[
  393. MeasureTerms.Distance,
  394. MeasureTerms.Perimeter,
  395. MeasureTerms.Area,
  396. MeasureTerms.Angle,
  397. MeasureTerms.Depth,
  398. MeasureTerms.Volume,
  399. MeasureTerms.Stenosis,
  400. MeasureTerms.AbRatio,
  401. MeasureTerms.RUV,
  402. //
  403. MeasureTypes.AreaPerimeterEllipse,
  404. MeasureTypes.AreaPerimeterPolyline,
  405. MeasureTypes.AreaPerimeterSpline,
  406. ];
  407. // ignore: non_constant_identifier_names
  408. static final C_SUPPORTED_M_ITEMS = <String>[
  409. MeasureTerms.VerticalDistance,
  410. MeasureTerms.Timespan,
  411. MeasureTerms.Depth,
  412. MeasureTerms.Stenosis,
  413. MeasureTerms.AbRatio,
  414. MeasureTerms.Slope,
  415. MeasureTerms.TAMAX,
  416. MeasureTerms.Velocity,
  417. MeasureTerms.Acceleration,
  418. MeasureTerms.PSED,
  419. MeasureTerms.RI,
  420. MeasureTerms.MaxPG,
  421. "LV TEI Index",
  422. "AV Ratio",
  423. MeasureTerms.HeartRate,
  424. // MeasureTerms.PHT,
  425. "MV PHT",
  426. "Qp/Qs",
  427. ];
  428. late final List<String> passeItems;
  429. final scrollController = ScrollController();
  430. final application = Get.find<IApplication>();
  431. String modeType = 'Tissue';
  432. int activeIndex = 0;
  433. List<ItemMeta> workingItems = [];
  434. @override
  435. void initState() {
  436. // passeItems = C_SUPPORTED_ITEMS;
  437. // passeItems = TestItems.C_DISTANCE_ITEMS;
  438. passeItems = TestItems.C_TEST_ITEMS;
  439. loadItems();
  440. application.visualAreaChanged.addListener(_visualAreaChanged);
  441. super.initState();
  442. }
  443. @override
  444. dispose() {
  445. application.visualAreaChanged.removeListener(_visualAreaChanged);
  446. super.dispose();
  447. }
  448. void loadItems() {
  449. workingItems = [];
  450. var names = (modeType == "TissueTM" || modeType == "Doppler")
  451. ? C_SUPPORTED_M_ITEMS
  452. : passeItems;
  453. final workingItemDtos =
  454. MeasureTestPage.MetaDTOList.where((e) => names.contains(e['Name']));
  455. for (var map in workingItemDtos) {
  456. final dto = ItemMetaDTO.fromJson(map);
  457. final item = ItemMetaConverter(dto).output();
  458. workingItems.add(item);
  459. }
  460. }
  461. void _visualAreaChanged(sender, IVisualArea e) {
  462. if (mounted) {
  463. _setAvailableModes(e.mode.modeType.toString().split('.')[1]);
  464. // changeItemByMeta(0); //暂时不要自动切测量项
  465. setState(() {});
  466. }
  467. }
  468. void _setAvailableModes(String name) {
  469. modeType = name;
  470. loadItems();
  471. }
  472. @override
  473. Widget build(BuildContext context) {
  474. return Container(
  475. width: 300,
  476. padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
  477. child: Scrollbar(
  478. controller: scrollController,
  479. isAlwaysShown: true,
  480. child: ListView.separated(
  481. controller: scrollController,
  482. itemCount: workingItems.length,
  483. itemBuilder: (BuildContext context, int index) {
  484. final meta = workingItems[index];
  485. final active = index == activeIndex;
  486. return active
  487. ? ElevatedButton(
  488. onPressed: () {
  489. changeItemByMeta(index);
  490. },
  491. child: Text(meta.name),
  492. style: ElevatedButton.styleFrom(
  493. fixedSize: const Size.fromHeight(50),
  494. ),
  495. )
  496. : OutlinedButton(
  497. onPressed: () => changeItemByMeta(index),
  498. child: Text(meta.name),
  499. style: OutlinedButton.styleFrom(
  500. fixedSize: const Size.fromHeight(50),
  501. ),
  502. );
  503. },
  504. separatorBuilder: (BuildContext context, int index) {
  505. return const SizedBox(height: 8);
  506. },
  507. ),
  508. ),
  509. );
  510. }
  511. void changeItemByMeta(int index) {
  512. setState(() {
  513. activeIndex = index;
  514. });
  515. final meta = workingItems[index];
  516. application.switchItem(meta);
  517. print(application.activeMeasureItem?.meta.name);
  518. // handle combo item
  519. if (application.activeMeasureItem != null) {
  520. final item = application.activeMeasureItem!;
  521. if (item is ITopMeasureItem) {
  522. item.switchChild(0);
  523. }
  524. }
  525. }
  526. }
  527. class _MeasureLeftAnnotation extends StatefulWidget {
  528. const _MeasureLeftAnnotation({Key? key}) : super(key: key);
  529. @override
  530. State<StatefulWidget> createState() => _MeasureLeftAnnotationState();
  531. }
  532. class _MeasureLeftAnnotationState extends State<_MeasureLeftAnnotation> {
  533. // ignore: non_constant_identifier_names
  534. static final C_SUPPORTED_TEXTS = <String>[
  535. "肝左叶",
  536. "胆囊",
  537. "脾脏",
  538. "结石",
  539. "积液",
  540. ];
  541. final scrollController = ScrollController();
  542. final application = Get.find<IApplication>();
  543. @override
  544. void initState() {
  545. // application.switchAnnotation(AnnotationType.label, C_SUPPORTED_TEXTS[0]);
  546. // application.switchAnnotation(AnnotationType.arrow);
  547. application.switchAnnotation(AnnotationType.input);
  548. super.initState();
  549. }
  550. @override
  551. Widget build(BuildContext context) {
  552. return Container(
  553. width: 300,
  554. padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
  555. child: Scrollbar(
  556. controller: scrollController,
  557. isAlwaysShown: true,
  558. child: ListView.separated(
  559. controller: scrollController,
  560. itemCount: C_SUPPORTED_TEXTS.length,
  561. itemBuilder: (BuildContext context, int index) {
  562. final name = C_SUPPORTED_TEXTS[index];
  563. const style = TextStyle(color: Colors.white, fontSize: 16);
  564. const dragStyle = TextStyle(color: Colors.amber, fontSize: 18);
  565. // TODO: melon - set drag cursor after version updated up then 3.0
  566. // https://github.com/flutter/flutter/pull/100475
  567. return Draggable<String>(
  568. data: name,
  569. dragAnchorStrategy: (data, context, offset) {
  570. // return offset - Offset(120, 14);
  571. return Offset.zero;
  572. },
  573. child: OutlinedButton(
  574. child: Text(name, style: style),
  575. onPressed: () {
  576. application.switchAnnotation(AnnotationType.label, name);
  577. },
  578. style: OutlinedButton.styleFrom(
  579. shape: RoundedRectangleBorder(
  580. borderRadius: BorderRadius.circular(4),
  581. ),
  582. side: BorderSide(color: Colors.grey.shade100),
  583. fixedSize: const Size.fromHeight(44),
  584. ),
  585. ),
  586. feedback: Material(
  587. color: Colors.transparent,
  588. child: Text(name, style: dragStyle),
  589. ),
  590. onDragStarted: () {
  591. application.switchAnnotation(AnnotationType.label, name);
  592. },
  593. );
  594. },
  595. separatorBuilder: (BuildContext context, int index) {
  596. return const SizedBox(height: 8);
  597. },
  598. ),
  599. ),
  600. );
  601. }
  602. }
  603. class _ModeTips extends StatefulWidget {
  604. @override
  605. State<StatefulWidget> createState() => _ModeTipsState();
  606. }
  607. class _ModeTipsState extends State<_ModeTips> {
  608. IApplication? application;
  609. @override
  610. void initState() {
  611. Future.delayed(const Duration(milliseconds: 100), () {
  612. application = Get.find<IApplication>();
  613. application!.visualsLoaded.addListener(_onVisualsLoaded);
  614. });
  615. super.initState();
  616. }
  617. @override
  618. void dispose() {
  619. application!.visualsLoaded.removeListener(_onVisualsLoaded);
  620. super.dispose();
  621. }
  622. void _onVisualsLoaded(Object sender, void e) {
  623. setState(() {});
  624. }
  625. @override
  626. Widget build(BuildContext context) {
  627. if (application == null) return const Text("Wait");
  628. return Material(
  629. child: Text(application!.avaliableModes.map((e) => e.name).join('/')),
  630. );
  631. }
  632. }