117 Commits

Author SHA1 Message Date
wangxiaoshuang 47d7fb00e2 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is passing
2026-04-14 16:19:41 +08:00
wangxiaoshuang 312ab754e1 分割标记渲染结构变更 2026-04-14 16:19:36 +08:00
caiyiling 0ff5a21d04 同步列表更改
continuous-integration/drone/push Build is passing
2026-04-14 13:05:18 +08:00
wangxiaoshuang 5a7fa1fb77 分割部分问题解决
continuous-integration/drone/push Build is passing
2026-04-09 18:08:20 +08:00
wangxiaoshuang 5289c156c0 1
continuous-integration/drone/push Build is passing
2026-04-09 17:28:59 +08:00
wangxiaoshuang a68c27fd43 分割部分问题解决
continuous-integration/drone/push Build is running
2026-04-09 17:19:12 +08:00
wangxiaoshuang 641c2e6ade 直方图取值及样式修改
continuous-integration/drone/push Build is passing
2026-04-08 16:22:29 +08:00
wangxiaoshuang 5b4cc097bc 分割视图添加直方图
continuous-integration/drone/push Build is passing
2026-04-08 14:58:26 +08:00
wangxiaoshuang b1de662677 aws插件版本问题
continuous-integration/drone/push Build is passing
2026-04-07 17:11:04 +08:00
caiyiling d2d1063ed5 数据同步模块更改
continuous-integration/drone/push Build is passing
2026-04-03 17:27:12 +08:00
wangxiaoshuang 2bc4b32d3a Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is passing
2026-04-03 14:02:17 +08:00
wangxiaoshuang 366ec62cd7 1 2026-04-03 14:02:05 +08:00
caiyiling 866e2e5c6b Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is failing
2026-04-03 13:49:35 +08:00
caiyiling 16e2db37cd 文件级别列表更改 2026-04-03 13:48:38 +08:00
wangxiaoshuang ee6c5bf8a5 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is pending
2026-04-03 13:45:48 +08:00
wangxiaoshuang 48cfe99f12 echarts版本升级v4=>v6 2026-04-03 13:45:39 +08:00
caiyiling f65264a2a6 1
continuous-integration/drone/push Build is running
2026-04-03 13:45:02 +08:00
caiyiling 769c5f8440 任务级别列表更改
continuous-integration/drone/push Build is running
2026-04-03 13:42:14 +08:00
caiyiling 6321bddfd3 数据同步更改
continuous-integration/drone/push Build is passing
2026-04-03 13:33:46 +08:00
caiyiling e30fb2a041 1
continuous-integration/drone/push Build is passing
2026-04-02 17:37:24 +08:00
caiyiling 478adcde21 1
continuous-integration/drone/push Build is running
2026-04-02 17:35:20 +08:00
caiyiling 34ce5142f2 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is running
2026-04-02 17:33:03 +08:00
caiyiling 3b993bcd46 数据同步更改 2026-04-02 17:32:35 +08:00
wangxiaoshuang 3f2e39f46c 当加载Volume体数据时,页面显示的现在速度需要按照Series来计算。现在一直都是0。
continuous-integration/drone/push Build is passing
2026-04-02 15:01:32 +08:00
wangxiaoshuang d317402f85 1
continuous-integration/drone/push Build is passing
2026-04-02 14:40:24 +08:00
wangxiaoshuang 6cb63bdc59 浏览器窗口大小调整后,渲染的标记、比例尺等位置偏移
continuous-integration/drone/push Build is failing
2026-04-02 14:36:15 +08:00
wangxiaoshuang 8db641db16 分割重命名时默认值填入
continuous-integration/drone/push Build is failing
2026-04-02 14:07:58 +08:00
wangxiaoshuang 9da9515930 aws插件版本升级3.7.4=>3.726
continuous-integration/drone/push Build is passing
2026-04-02 09:33:38 +08:00
wangxiaoshuang 2be119de6b 分割橡皮檫国际化
continuous-integration/drone/push Build is passing
2026-04-01 15:23:20 +08:00
wangxiaoshuang c9f9615488 mpr切换单视口时分割列表报错问题
continuous-integration/drone/push Build is passing
2026-04-01 14:53:14 +08:00
wangxiaoshuang be137b3840 分割字段变更SegmentMumber=>SegmentNumber
continuous-integration/drone/push Build is passing
2026-04-01 12:21:07 +08:00
wangxiaoshuang d80101f86b 分割标记与分割段绑定错误问题
continuous-integration/drone/push Build is passing
2026-04-01 11:30:59 +08:00
wangxiaoshuang 3c0b084f7e 分割文件保存分割段与分割标记不一致问题
continuous-integration/drone/push Build is passing
2026-04-01 09:58:45 +08:00
wangxiaoshuang 8a88a64c36 比例尺页面放大缩小报错问题
continuous-integration/drone/push Build is failing
2026-04-01 09:35:36 +08:00
wangxiaoshuang 3d538d6e67 1
continuous-integration/drone/push Build is passing
2026-03-31 17:59:10 +08:00
wangxiaoshuang 67d2a83ec8 1
continuous-integration/drone/push Build is passing
2026-03-31 17:38:36 +08:00
wangxiaoshuang 2efb229c96 1
continuous-integration/drone/push Build is passing
2026-03-31 17:13:29 +08:00
wangxiaoshuang c930480336 体数据放大变形问题解决
continuous-integration/drone/push Build is passing
2026-03-31 16:47:16 +08:00
wangxiaoshuang b39ea3a032 分割标记数据值计算完成后才可保存
continuous-integration/drone/push Build is passing
2026-03-31 16:30:37 +08:00
wangxiaoshuang ff5aea17a5 样式修改
continuous-integration/drone/push Build is passing
2026-03-31 15:43:34 +08:00
wangxiaoshuang 477b6935ff 分割窗口放大页面跳转与分割片段默认跳转功能互不干扰
continuous-integration/drone/push Build is running
2026-03-31 15:41:45 +08:00
wangxiaoshuang 8b6432ad14 分割标记长短径的小数位数需要一致
continuous-integration/drone/push Build is passing
2026-03-31 15:23:23 +08:00
wangxiaoshuang ca35fe6df4 部分问题修复
continuous-integration/drone/push Build is passing
2026-03-31 15:17:13 +08:00
wangxiaoshuang 307c02725f 分割对象的体素统计以及长短径计算优化、分割组添加状态
continuous-integration/drone/push Build is passing
2026-03-31 13:47:06 +08:00
caiyiling 72d0494baa Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is passing
2026-03-31 10:07:50 +08:00
caiyiling d1017bc565 MIM关闭医学审核任务逻辑更改 2026-03-31 10:07:17 +08:00
wangxiaoshuang 76e6ef087f 分割标记的颜色需要随机,不能重复
continuous-integration/drone/push Build is passing
2026-03-30 17:33:52 +08:00
wangxiaoshuang c5c9e4364c 【dicom分割】分段菜单按钮点击后,再点击另一个分段菜单按钮,上一个展示的弹窗应该消失
continuous-integration/drone/push Build is passing
2026-03-30 17:17:17 +08:00
wangxiaoshuang 920d0b21c6 【dicom分割】当前分组选择为分组2,分组2没有标记数据,当切入mpr模式时,此时显示了分组1的标记,这里影像标记应该与右侧分割选择组同步
continuous-integration/drone/push Build is passing
2026-03-30 16:50:25 +08:00
wangxiaoshuang 3fe16b4166 体数据阅片四角信息
continuous-integration/drone/push Build is passing
2026-03-30 16:19:05 +08:00
wangxiaoshuang bc2d5463ff MPR视图的:十字线和窗宽窗位
continuous-integration/drone/push Build is passing
2026-03-30 16:10:56 +08:00
caiyiling c63811d55c Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is passing
2026-03-30 15:45:52 +08:00
caiyiling 832fa82d4b 融合伪彩更改 2026-03-30 15:45:16 +08:00
wangxiaoshuang b3ae801fe9 没有分割数据的Segmentation,从二维进入MPR时,会显示3个
continuous-integration/drone/push Build is passing
2026-03-30 14:36:43 +08:00
caiyiling 0dbd74270d Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is passing
2026-03-30 14:14:18 +08:00
caiyiling aae5968d74 nm/ct融合更改 2026-03-30 14:13:52 +08:00
wangxiaoshuang 79a7ad0cf7 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is passing
# Conflicts:
#	src/views/trials/trials-panel/reading/dicoms3D/components/VolumeViewport.vue
2026-03-30 13:29:16 +08:00
wangxiaoshuang a35309d9b5 普通标记在多个三维序列渲染问题 2026-03-30 13:27:24 +08:00
caiyiling a987ac086b 1
continuous-integration/drone/push Build is passing
2026-03-30 10:57:27 +08:00
caiyiling fdc2bc5f8a 1
continuous-integration/drone/push Build is running
2026-03-30 10:54:07 +08:00
caiyiling 84f68c79f0 1
continuous-integration/drone/push Build is running
2026-03-30 10:52:28 +08:00
caiyiling b77dc34523 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is running
2026-03-30 10:49:44 +08:00
caiyiling 91554e0e30 1 2026-03-30 10:48:04 +08:00
wangxiaoshuang 2efb7fd36a 普通标记在多个序列渲染问题
continuous-integration/drone/push Build is passing
2026-03-27 18:01:41 +08:00
caiyiling dbdec93efc Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is running
2026-03-27 17:59:51 +08:00
caiyiling e631ce73f2 伪彩组件更改 2026-03-27 17:59:35 +08:00
wangxiaoshuang 5931dcdc67 mpr加载
continuous-integration/drone/push Build is passing
2026-03-27 16:40:45 +08:00
wangxiaoshuang b55ffc0821 mpr加载
continuous-integration/drone/push Build is passing
2026-03-27 16:32:34 +08:00
caiyiling 8600054405 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is passing
2026-03-27 16:24:54 +08:00
caiyiling 9a88539294 融合视口调节透明度功能更改 2026-03-27 16:24:24 +08:00
wangxiaoshuang 09211c8841 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is passing
# Conflicts:
#	src/views/trials/trials-panel/reading/dicoms3D/components/PetCtViewport.vue
2026-03-27 15:47:24 +08:00
wangxiaoshuang 2724e0267f 三维调窗、分割保存 2026-03-27 15:46:40 +08:00
caiyiling a0e355d69b Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is passing
2026-03-27 13:53:45 +08:00
caiyiling 04a0a1ccc3 SPECT多帧图像融合更改 2026-03-27 13:52:59 +08:00
wangxiaoshuang c3e967acaa 分割标记序号修改
continuous-integration/drone/push Build is passing
2026-03-27 10:50:18 +08:00
wangxiaoshuang 740a0aa221 删除分割标记同步问题
continuous-integration/drone/push Build is passing
2026-03-27 10:31:06 +08:00
wangxiaoshuang e8f95d09ba 1
continuous-integration/drone/push Build is passing
2026-03-26 17:49:39 +08:00
wangxiaoshuang 2c305af49e 【dicom分割】删除已绑定问题的分割标记时,eicrf页面没有刷新状态
continuous-integration/drone/push Build is failing
2026-03-26 17:40:22 +08:00
wangxiaoshuang 5c76bda1e7 分割添加阈值工具
continuous-integration/drone/push Build is passing
2026-03-26 16:05:34 +08:00
wangxiaoshuang 0273601f7b Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is passing
2026-03-25 17:46:45 +08:00
wangxiaoshuang c3a154f762 分割长短径工具问题修复 2026-03-25 17:46:31 +08:00
caiyiling cdb97f6405 w/l预设模板移除CT Bone1
continuous-integration/drone/push Build is passing
2026-03-25 17:31:59 +08:00
caiyiling 4794429a57 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is passing
2026-03-25 16:51:31 +08:00
caiyiling 77a633d699 SPECT融合更改 2026-03-25 16:45:44 +08:00
wangxiaoshuang 64d1ebf350 导入分割数据与分段进行绑定
continuous-integration/drone/push Build is passing
2026-03-25 16:44:55 +08:00
wangxiaoshuang 5336cb5353 分割标记删除进行提示
continuous-integration/drone/push Build is passing
2026-03-25 14:11:52 +08:00
wangxiaoshuang 0a288b9f47 分割标记重命名问题修改
continuous-integration/drone/push Build is passing
2026-03-25 14:03:04 +08:00
wangxiaoshuang 674d483cbf Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is passing
2026-03-25 13:52:23 +08:00
wangxiaoshuang cbcb396441 分割标记跳转修改 2026-03-25 13:52:14 +08:00
caiyiling 9a3e972723 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is passing
2026-03-25 11:00:20 +08:00
caiyiling b593615e9e Lugano标准融合页面伪彩更改 2026-03-25 10:59:55 +08:00
wangxiaoshuang d30c9a5f3b 图像滚动问题解决
continuous-integration/drone/push Build is passing
2026-03-25 09:26:11 +08:00
caiyiling dd3d738eed Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is passing
2026-03-25 09:10:25 +08:00
caiyiling d9edaf4c27 OCT标准FCT平均值计算逻辑更改 2026-03-25 09:09:35 +08:00
wangxiaoshuang a5e0dd89ff 1
continuous-integration/drone/push Build is running
2026-03-25 09:07:21 +08:00
wangxiaoshuang 73f8f816b7 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is failing
2026-03-24 17:56:10 +08:00
wangxiaoshuang 19d5144b70 问题绑定分割标记后查看 2026-03-24 17:55:57 +08:00
caiyiling e0ecd3f4ac Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is running
2026-03-24 17:55:31 +08:00
caiyiling 30e0e374bc 调窗更改 2026-03-24 17:54:59 +08:00
wangxiaoshuang 25b909c660 分割默认工具功能修复
continuous-integration/drone/push Build is passing
2026-03-24 16:45:44 +08:00
caiyiling 5cb3e8e606 同步调窗更改
continuous-integration/drone/push Build is passing
2026-03-24 15:46:25 +08:00
wangxiaoshuang 619af2e874 分割问题绑定标记显示问题
continuous-integration/drone/push Build is passing
2026-03-24 15:27:17 +08:00
wangxiaoshuang 43f1736b05 1
continuous-integration/drone/push Build is passing
2026-03-24 15:12:49 +08:00
wangxiaoshuang c9be5678bd Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is running
2026-03-24 15:09:51 +08:00
wangxiaoshuang 4e5ecb3938 分割添加轮廓工具 2026-03-24 15:09:45 +08:00
caiyiling 7870c3d74b CT/NM融合更改
continuous-integration/drone/push Build is passing
2026-03-24 14:54:46 +08:00
wangxiaoshuang d07adc4948 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is passing
# Conflicts:
#	src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue
#	src/views/trials/trials-panel/reading/dicoms3D/components/StudyList.vue
2026-03-23 15:10:29 +08:00
wangxiaoshuang 266886fa64 阅片添加分割工具 2026-03-23 10:01:22 +08:00
caiyiling e012eba0c2 阅片页面样式更改
continuous-integration/drone/push Build is passing
2026-03-19 16:54:25 +08:00
caiyiling 75d296405b cornerstone版本升级
continuous-integration/drone/push Build is passing
2026-03-19 15:12:54 +08:00
caiyiling 07c92c48a4 融合图像筛选更改
continuous-integration/drone/push Build is passing
2026-03-13 14:11:52 +08:00
hang b5427dd17b 修改打包文件配置
continuous-integration/drone/push Build is passing
2026-03-13 13:49:27 +08:00
hang 385643643e uat合并main 导致打包文件被覆盖,重新编写 2026-03-13 13:47:23 +08:00
caiyiling 756f1293aa 融合图像补充模态为NM类型的影像 2026-03-13 13:35:38 +08:00
caiyiling eee19f0ae9 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main 2026-03-13 13:20:27 +08:00
caiyiling 78c0910db7 选择融合图像时扩充NM模态的影像 2026-03-13 13:18:24 +08:00
wangxiaoshuang 18a71725f9 自动化脚本变更
continuous-integration/drone/push Build is passing
2026-03-06 09:44:56 +08:00
132 changed files with 16043 additions and 11517 deletions
+35 -3
View File
@@ -1,7 +1,7 @@
kind: pipeline
type: ssh
name: ssh-linux-test-irc-publish
name: ssh-linux-uat-irc-publish
platform:
os: Linux
@@ -19,7 +19,7 @@ server:
steps:
- name: publish-uat-irc-web
commands:
- bash /opt/1panel/xc-deploy-new/devops-center/build-vue-then-publish.sh IRC_Web uat uat
- bash /opt/1panel/xc-deploy-new/devops-center/build-vue-then-publish.sh IRC_Web uat --mode uat
- name: notify-wecom
commands:
- echo $DRONE_COMMIT_AUTHOR "$DRONE_COMMIT_MESSAGE"
@@ -33,6 +33,38 @@ trigger:
- uat
---
kind: pipeline
type: ssh
name: ssh-linux-test-irc-publish
platform:
os: Linux
arch: 386
clone:
disable: true #禁用默认克隆
server:
host: 106.14.89.110
user: root
password:
from_secret: local_pwd
steps:
- name: publish-test-irc-web
commands:
- bash /opt/1panel/xc-deploy-new/devops-center/build-vue-then-publish.sh IRC_Web main
- name: notify-wecom
commands:
- echo $DRONE_COMMIT_AUTHOR "$DRONE_COMMIT_MESSAGE"
- bash /opt/1panel/xc-deploy-new/devops-center/drone-notify-wecom.sh "$DRONE_BUILD_STATUS" "$DRONE_REPO_NAME" "$DRONE_BRANCH" "$DRONE_BUILD_NUMBER" "4355b98e-1e72-4678-8dfb-2fc6ad0bf449" "$DRONE_COMMIT_MESSAGE" "$DRONE_COMMIT_AUTHOR" "Test_IRC_WEB" "irc.test.extimaging.com"
when:
status:
- success
- failure
trigger:
branch:
- main
+5328 -6019
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -14,19 +14,19 @@
"i18n:en": "node i18nGenerate.js lang=en keyCol=5 valCol=7"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.701.0",
"@cornerstonejs/adapters": "^2.19.7",
"@aws-sdk/client-s3": "3.726.1",
"@cornerstonejs/adapters": "^4.19.2",
"@cornerstonejs/polymorphic-segmentation": "4.19.2",
"@cornerstonejs/calculate-suv": "^1.1.0",
"@cornerstonejs/core": "^2.19.7",
"@cornerstonejs/dicom-image-loader": "^2.19.7",
"@cornerstonejs/tools": "^2.19.7",
"@cornerstonejs/core": "^4.19.2",
"@cornerstonejs/dicom-image-loader": "^4.19.2",
"@cornerstonejs/tools": "^4.19.2",
"@fingerprintjs/fingerprintjs": "^4.6.2",
"@icr/polyseg-wasm": "^0.4.0",
"@microsoft/signalr": "^8.0.7",
"@riophae/vue-treeselect": "^0.4.0",
"@vue-office/docx": "^1.6.2",
"@vue-office/excel": "^1.7.11",
"@vue/composition-api": "^1.7.2",
"ali-oss": "^6.17.1",
"axios": "^0.18.1",
"core-js": "^3.8.3",
@@ -38,7 +38,7 @@
"dcmjs": "^0.29.8",
"dicom-parser": "^1.8.9",
"dicomedit": "^0.1.0",
"echarts": "^4.8.0",
"echarts": "^6.0.0",
"element-ui": "^2.15.14",
"exceljs": "^4.4.0",
"file-saver": "^2.0.5",
@@ -60,11 +60,10 @@
"screenfull": "^6.0.2",
"sortablejs": "^1.15.5",
"streamsaver": "^2.0.6",
"svg-sprite-loader": "^4.1.3",
"svgo": "^1.2.2",
"v-viewer": "^1.7.4",
"vcrontab": "^0.3.5",
"vue": "^2.6.14",
"vue": "2.7.16",
"vue-clipboard2": "^0.3.3",
"vue-contextmenujs": "^1.4.11",
"vue-count-to": "^1.0.13",
@@ -98,9 +97,10 @@
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"sass": "^1.63.2",
"sass-loader": "^10.4.1",
"sass-loader": "^16.0.7",
"svg-sprite-loader": "^6.0.11",
"terser-webpack-plugin": "^5.3.10",
"vue-template-compiler": "^2.6.14",
"vue-template-compiler": "2.7.16",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2"
},
@@ -125,4 +125,4 @@
"not dead",
"not op_mini all"
]
}
}
+1 -2
View File
@@ -115,8 +115,7 @@ export default {
}
},
mounted() {
// this.show = process.env.VUE_APP_OSS_PATH === '/test/dist'
this.show = false
this.show = process.env.VUE_APP_OSS_PATH === '/test/dist'
Vue.prototype.$openI18n = this.openI18n
},
// watch: {
+41
View File
@@ -0,0 +1,41 @@
import request from '@/utils/request'
export function addOrUpdateFileUploadRecord(params) {
return request({
url: `/FileUploadRecord/addOrUpdateFileUploadRecord`,
method: 'post',
data: params
})
}
export function batchAddSyncFileTask(params) {
return request({
url: `/FileUploadRecord/batchAddSyncFileTask`,
method: 'post',
data: params
})
}
export function getSubjectUploadRecordList(params) {
return request({
url: `/FileUploadRecord/getSubjectUploadRecordList`,
method: 'post',
data: params
})
}
export function getFileUploadRecordList(params) {
return request({
url: `/FileUploadRecord/getFileUploadRecordList`,
method: 'post',
data: params
})
}
export function getUploadFileSyncRecordList(params) {
return request({
url: `/FileUploadRecord/getUploadFileSyncRecordList`,
method: 'post',
data: params
})
}
+89
View File
@@ -319,3 +319,92 @@ export function getReportsChartData(param) {
data: param
})
}
// 分割相关
// 获取分割组
export function getSegmentationList(param) {
return request({
url: `/Segmentation/getSegmentationList`,
method: 'post',
data: param
})
}
// 新增修改分割组
export function addOrUpdateSegmentation(param) {
return request({
url: `/Segmentation/addOrUpdateSegmentation`,
method: 'post',
data: param
})
}
// 删除分割组
export function deleteSegmentation(segmentationId) {
return request({
url: `/Segmentation/deleteSegmentation/${segmentationId}`,
method: 'delete'
})
}
// 获取分割
export function getSegmentList(param) {
return request({
url: `/Segmentation/getSegmentList`,
method: 'post',
data: param
})
}
// 新增修改分割
export function addOrUpdateSegment(param) {
return request({
url: `/Segmentation/addOrUpdateSegment`,
method: 'post',
data: param
})
}
// 删除分割
export function deleteSegment(segmentId) {
return request({
url: `/Segmentation/deleteSegment/${segmentId}`,
method: 'delete'
})
}
// 获取分割绑定关系
export function getSegmentBindingList(param) {
return request({
url: `/Segmentation/getSegmentBindingList`,
method: 'post',
data: param
})
}
// 保存分割绑定关系(可用作修改删除)
export function saveSegmentBindingAndAnswer(param) {
return request({
url: `/Segmentation/saveSegmentBindingAndAnswer`,
method: 'post',
data: param
})
}
// 获取表格问题配置
export function getReadingTableQuestionTrialById(params) {
return request({
url: `/ReadingQuestion/getReadingTableQuestionTrialById`,
method: 'post',
params
})
}
// 获取外层问题配置
export function getReadingQuestionTrialById(params) {
return request({
url: `/ReadingQuestion/getReadingQuestionTrialById`,
method: 'post',
params
})
}
// 修改分割组保存状态
export function changeSegmentationSavedStatus(data) {
return request({
url: `/Segmentation/changeSegmentationSavedStatus`,
method: 'post',
data
})
}
+14 -8
View File
@@ -74,10 +74,11 @@ export default {
zzSessionStorage.setItem('lastWorkbench', `${v.path}${query ? '?' : ''}${query}`)
var firstGoIn = this.trialsRouter.children.find(v => { return v.name === 'TrialsPanel' }).children[0]
if (this.trialsTab === '/trials/trials-panel') {
this.$router.replace({ path: `${firstGoIn.path}${query ? '?' : ''}${query}` })
var targetPath = firstGoIn.path
if (firstGoIn.children && firstGoIn.children.length > 0) {
this.$router.replace({ path: `${firstGoIn.children[0].path}${query ? '?' : ''}${query}` })
targetPath = firstGoIn.children[0].path
}
this.$router.replace({ path: `${targetPath}${query ? '?' : ''}${query}` })
}
// document.cookie = 'TrialId=' + this.$route.query.trialId + ';path=/'
},
@@ -112,10 +113,11 @@ export default {
this.getTrialList()
var firstGoIn = this.trialsRouter.children.find(v => { return v.name === 'TrialsPanel' }).children[0]
if (this.trialsTab === '/trials/trials-panel') {
this.$router.replace({ path: `${firstGoIn.path}${query ? '?' : ''}${query}` })
var targetPath = firstGoIn.path
if (firstGoIn.children && firstGoIn.children.length > 0) {
this.$router.replace({ path: `${firstGoIn.children[0].path}${query ? '?' : ''}${query}` })
targetPath = firstGoIn.children[0].path
}
this.$router.replace({ path: `${targetPath}${query ? '?' : ''}${query}` })
}
},
methods: {
@@ -201,15 +203,19 @@ export default {
if (~url.indexOf('?')) {
query = url.split('?')[1]
}
this.$router.push({ path: `${v.name}${query ? '?' : ''}${query}` })
var trialsPanelList = this.trialsRouter.children.find(v => { return v.name === 'TrialsPanel' }).children
var isHasChild = trialsPanelList.find(c => { return c.path === v.name }) && trialsPanelList.find(c => { return c.path === v.name }).children
if (trialsPanelList.find(c => { return c.path === v.name }) && trialsPanelList.find(c => { return c.path === v.name }).tabHiddn) {
var trialsPanelList = this.trialsRouter.children.find(item => { return item.name === 'TrialsPanel' }).children
var currentRoute = trialsPanelList.find(c => { return c.path === v.name })
if (currentRoute && currentRoute.tabHiddn) {
this.$router.push({ path: `${v.name}${query ? '?' : ''}${query}` })
return
}
var isHasChild = currentRoute && currentRoute.children
if (isHasChild && isHasChild.length > 0) {
this.trialsTabChild = isHasChild[0].path
this.$router.push({ path: `${this.trialsTabChild}${query ? '?' : ''}${query}` })
} else {
this.$router.push({ path: `${v.name}${query ? '?' : ''}${query}` })
}
} catch (e) {
console.log(e)
+44 -8
View File
@@ -8,20 +8,49 @@
<script>
import { getReportsChartData } from "@/api/reading"
let echarts = require('echarts/lib/echarts');
import * as echarts from 'echarts/core';
import { LineChart } from 'echarts/charts';
import {
TitleComponent,
TooltipComponent,
GridComponent,
DataZoomComponent,
LegendComponent,
DatasetComponent,
// 内置数据转换器组件 (filter, sort)
TransformComponent
} from 'echarts/components';
// 标签自动布局、全局过渡动画等特性
import { LabelLayout, UniversalTransition } from 'echarts/features';
// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from 'echarts/renderers';
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
DataZoomComponent,
LegendComponent,
LineChart,
LabelLayout,
UniversalTransition,
CanvasRenderer
]);
// let echarts = require('echarts/lib/echarts');
// 按需引入图表
// require('echarts/lib/chart/bar');
require('echarts/lib/chart/line');
// require('echarts/lib/chart/line');
// require('echarts/lib/chart/pie');
// require('echarts/lib/chart/scatter');
// 按需引入组件
require('echarts/lib/component/tooltip');
require('echarts/lib/component/title');
require('echarts/lib/component/legend');
require('echarts/lib/component/grid');
require('echarts/lib/component/dataZoom');
// require('echarts/lib/component/tooltip');
// require('echarts/lib/component/title');
// require('echarts/lib/component/legend');
// require('echarts/lib/component/grid');
// require('echarts/lib/component/dataZoom');
export default {
name: "readingChart",
props: {
@@ -151,7 +180,13 @@ export default {
text: obj.title,
textStyle: {
color: "#fff"
}
},
left: 0,
top: 0
},
grid: {
left: 50,
bottom: 30
},
tooltip: {
trigger: 'axis',
@@ -193,6 +228,7 @@ export default {
}
},
axisLine: {
show: true,
lineStyle: {
color: '#fff',
}
+1 -1
View File
@@ -116,7 +116,7 @@ export default {
display: none !important; /* 隐藏原生 radio 输入,但仍然允许交互 */
}
::v-deep.el-radio:focus:not(.is-focus):not(:active):not(.is-disabled)
::v-deep .el-radio:focus:not(.is-focus):not(:active):not(.is-disabled)
.el-radio__inner {
box-shadow: none !important;
}
@@ -712,12 +712,12 @@ export default {
var studyUid = data.string('x0020000d')
if (!studyUid) return resolve()
var pixelDataElement = data.elements.x7fe00010
if (!pixelDataElement && modality !== 'SR') return resolve()
if (!pixelDataElement && modality !== 'SR' && modality !== 'ECG') return resolve()
var studyIndex = 0
while (
studyIndex < scope.uploadQueues.length &&
scope.uploadQueues[studyIndex].dicomInfo.studyUid !== studyUid &&
(pixelDataElement || modality === 'SR') &&
(pixelDataElement || modality === 'SR' || modality === 'ECG') &&
modality != ''
) {
++studyIndex
@@ -1174,6 +1174,7 @@ export default {
},
}
let arr = []
let uploadBatchId = scope.$guid()
for (let i = 0; i < seriesList.length; i++) {
let v = seriesList[i]
let instanceList = []
@@ -1266,6 +1267,16 @@ export default {
) {
dicomInfo.uploadFileSize = dicomInfo.fileSize
}
},
{
fileName: o.file.name,
fileSize: o.file.size,
fileType: 'application/dicom',
uploadBatchId: uploadBatchId,
batchDataType: 5,
trialId: params.trialId,
subjectId: params.subjectId,
subjectVisitId: params.subjectVisitId
}
)
if (!res || !res.url) {
@@ -1289,7 +1300,17 @@ export default {
let OSSclient = scope.OSSclient
let seriesRes = await OSSclient.put(
thumbnailPath,
blob
blob,
{
fileName: `${v.seriesUid}.jpg`,
fileSize: blob.size,
fileType: 'image/jpeg',
uploadBatchId: uploadBatchId,
batchDataType: 6,
trialId: params.trialId,
subjectId: params.subjectId,
subjectVisitId: params.subjectVisitId
}
)
if (seriesRes && seriesRes.url) {
ImageResizePath = scope.$getObjectName(
@@ -1430,7 +1451,20 @@ export default {
}
let OSSclient = scope.OSSclient
try {
let seriesRes = await OSSclient.put(thumbnailPath, blob)
let seriesRes = await OSSclient.put(
thumbnailPath,
blob,
{
fileName: `${v.seriesUid}.jpg`,
fileSize: blob.size,
fileType: 'image/jpeg',
uploadBatchId: uploadBatchId,
batchDataType: 6,
trialId: params.trialId,
subjectId: params.subjectId,
subjectVisitId: params.subjectVisitId
}
)
if (seriesRes && seriesRes.url) {
o.ImageResizePath = scope.$getObjectName(seriesRes.url)
}
@@ -1448,6 +1482,7 @@ export default {
if (scope.IsImageSegment) {
params.IsImageSegmentLabel = true
}
params.UploadBatchId = uploadBatchId
addOrUpdateArchiveTaskStudy(params)
.then((res) => {
if (dicomInfo.failedFileCount === dicomInfo.fileCount) {
@@ -86,16 +86,16 @@ export default {
}
</script>
<style lang="scss" scoped>
::v-deep.el-tabs--left .el-tabs__header.is-left {
::v-deep .el-tabs--left .el-tabs__header.is-left {
margin-right: 0;
}
.uploadDicomAndNonedicom {
::v-deep.el-tabs--border-card>.el-tabs__header .el-tabs__item {
::v-deep .el-tabs--border-card>.el-tabs__header .el-tabs__item {
color: #909399;
}
::v-deep.el-tabs--border-card>.el-tabs__header .el-tabs__item.is-active {
::v-deep .el-tabs--border-card>.el-tabs__header .el-tabs__item.is-active {
color: #428bca;
background-color: #fff;
border-right-color: #dcdfe6;
@@ -548,8 +548,9 @@ export default {
})
if (res.IsSuccess) {
this.studyMonitorId = res.Result
let uploadBatchId = this.$guid()
for (let i = 0; i < num; i++) {
funArr.push(this.handleUploadTask(this.selectArr, i))
funArr.push(this.handleUploadTask(this.selectArr, i, uploadBatchId))
}
if (funArr.length > 0) {
let res = await Promise.all(funArr)
@@ -560,7 +561,7 @@ export default {
}
},
// 并发上传
async handleUploadTask(arr, index) {
async handleUploadTask(arr, index, uploadBatchId) {
if (!this.uploadVisible) return
let file = this.fileList.filter((item) => item.id === arr[index].id)[0]
file.status = 1
@@ -576,7 +577,7 @@ export default {
}
file.curPath = path
const fileData = await this.fileToBlob(file.file)
let res = await this.fileToOss(path, fileData, file)
let res = await this.fileToOss(path, fileData, file, uploadBatchId)
if (res) {
file.status = 2
this.successFileList.push({
@@ -610,13 +611,13 @@ export default {
}
let ind = arr.findIndex((item) => item.status === 0)
if (ind >= 0) {
return this.handleUploadTask(arr, ind)
return this.handleUploadTask(arr, ind, uploadBatchId)
} else {
return false
}
},
// file上传到oss
async fileToOss(path, file, item) {
async fileToOss(path, file, item, uploadBatchId) {
try {
let res = await this.OSSclient.multipartUpload(
{
@@ -629,6 +630,17 @@ export default {
if (item.uploadFileSize > file.fileSize) {
item.uploadFileSize = file.fileSize > 0 ? file.fileSize : 1
}
},
{
fileName: item.name,
fileSize: item.size,
fileType: item.fileType,
uploadBatchId: uploadBatchId,
batchDataType: 7,
trialId: this.$route.query.trialId,
subjectId: this.currentRow.SubjectId,
subjectVisitId: this.currentRow.SourceSubjectVisitId,
studyCode: this.SubjectCode
}
)
if (res) {
+16
View File
@@ -0,0 +1,16 @@
import resize from './resize'
const install = function (Vue) {
// 绑定v-adaptive指令
Vue.directive('resize', resize)
}
// if (window.Vue) {
// window['adaptive'] = adaptive
// // eslint-disable-next-line no-undef
// Vue.use(install)
// }
resize.install = install
export default resize
+19
View File
@@ -0,0 +1,19 @@
const map = new WeakMap()
const ob = new ResizeObserver(entries => {
for (const entry of entries) {
const handler = map.get(entry.target)
if (handler) {
const { inlineSize, blockSize } = entry.contentBoxSize[0]
handler({ width: inlineSize, height: blockSize })
}
}
})
export default {
bind(el, binding) {
map.set(el, binding.value)
ob.observe(el)
},
unbind(el) {
ob.unobserve(el)
}
}
+1
View File
@@ -0,0 +1 @@
<svg width="24px" height="24px" viewBox="0 0 24 24" class="h-6 w-6"><g id="tool-seg-brush" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><rect id="Rectangle" x="0" y="0" width="24" height="24"></rect><path d="M3.24640621,21.8286833 C3.09173375,21.7551472 2.99513748,21.5971513 3.00018895,21.4259625 C3.00524042,21.2547737 3.11098486,21.1027485 3.26972426,21.0384606 C5.3260304,20.2059201 4.66362518,17.8620247 5.27421252,16.6088957 C6.02197747,15.1026514 7.84114758,14.4766383 9.35746132,15.2037675 C13.9485253,17.4422999 8.48346644,24.3211232 3.24640621,21.8286833 Z" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M21.5968442,2.302022 C21.1256527,1.8826075 20.4094461,1.90228142 19.9619901,2.34693083 L9.69255027,12.5887345 C11.0536437,13.0051578 12.1843437,13.9616637 12.8206229,15.2349008 L21.7410706,3.93687606 C22.1347378,3.44008272 22.0714081,2.72222021 21.5968442,2.302022 Z" id="Path" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" class="h-6 w-6" color="#fff">
<path
d="M10.3945 12.6113C10.043 12.6113 9.78516 12.3945 9.78516 12.0371C9.78516 11.6855 10.043 11.4688 10.3945 11.4688H13.2012C13.5527 11.4688 13.8164 11.6855 13.8164 12.0371C13.8164 12.3945 13.5527 12.6113 13.2012 12.6113H10.3945Z"
fill="currentColor"></path>
<path opacity="0.5"
d="M18.312 6.84473C21.8473 8.88583 22.9225 13.6424 20.7134 17.4687C18.5042 21.2951 13.8473 22.7422 10.312 20.7011C9.51661 20.2419 8.84737 19.6439 8.31042 18.9512C11.3012 19.3598 14.4827 17.8251 16.1946 14.8598C17.9067 11.8945 17.6457 8.3712 15.7963 5.98538C16.6645 6.10409 17.5167 6.38556 18.312 6.84473Z"
fill="currentColor"></path>
<circle cx="13.7852" cy="13.4688" r="8" stroke="currentColor"></circle>
<path
d="M4.50586 8.22266C4.14844 8.22266 3.92578 7.9707 3.92578 7.60742V5.68555H2.10352C1.74609 5.68555 1.5 5.46875 1.5 5.11719C1.5 4.76562 1.74609 4.54297 2.10352 4.54297H3.92578V2.61523C3.92578 2.25195 4.14844 2 4.50586 2C4.85742 2 5.08008 2.25195 5.08008 2.61523V4.54297H6.89648C7.25391 4.54297 7.50586 4.76562 7.50586 5.11719C7.50586 5.46875 7.25391 5.68555 6.89648 5.68555H5.08008V7.60742C5.08008 7.9707 4.85742 8.22266 4.50586 8.22266Z"
fill="currentColor"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg width="18px" height="18px" viewBox="0 0 18 18" class="text-primary"><g id="view-fill" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Group-13"><rect id="Rectangle" x="0" y="0" width="18" height="18"></rect><rect id="Rectangle" fill="currentColor" x="2" y="2" width="14" height="14" rx="1"></rect></g></g></svg>

After

Width:  |  Height:  |  Size: 338 B

+1
View File
@@ -0,0 +1 @@
<svg width="18px" height="18px" viewBox="0 0 18 18" class="text-primary"><g id="view-outline-fill" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Group-13"><rect id="Rectangle" x="0" y="0" width="18" height="18"></rect><rect id="Rectangle" stroke="currentColor" x="1.5" y="1.5" width="15" height="15" rx="1"></rect><rect id="Rectangle" fill="currentColor" x="3.5" y="3.5" width="11" height="11" rx="1"></rect></g></g></svg>

After

Width:  |  Height:  |  Size: 446 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1775115523115" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5347" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M458.105263 970.105263h-215.578947V264.084211h215.578947V970.105263z m-161.68421-53.894737h107.789473V317.978947h-107.789473V916.210526z" fill="#ffffff" p-id="5348"></path><path d="M296.421053 970.105263h-215.578948V479.663158h215.578948V970.105263z m-161.684211-53.894737h107.789474V533.557895h-107.789474V916.210526zM943.157895 970.105263h-215.578948V479.663158h215.578948V970.105263z m-161.684211-53.894737h107.789474V533.557895h-107.789474V916.210526zM619.789474 970.105263h-215.578948V53.894737h215.578948v916.210526z m-161.684211-53.894737h107.789474V107.789474h-107.789474v808.421052z" fill="#ffffff" p-id="5349"></path><path d="M781.473684 970.105263h-215.578947V264.084211h215.578947V970.105263z m-161.68421-53.894737h107.789473V317.978947h-107.789473V916.210526z" fill="#ffffff" p-id="5350"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774331916694" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1627" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M198.4 960A134.4 134.4 0 0 1 64 825.6V198.4A134.4 134.4 0 0 1 198.4 64h268.8a44.8 44.8 0 0 1 44.8 44.8 44.8 44.8 0 0 1-44.8 44.8H198.4a44.8 44.8 0 0 0-44.8 44.8v627.2a44.8 44.8 0 0 0 44.8 44.8h627.2a44.8 44.8 0 0 0 44.8-44.8V556.8a44.8 44.8 0 0 1 44.8-44.8 44.8 44.8 0 0 1 44.8 44.8v268.8A134.4 134.4 0 0 1 825.6 960z m281.984-416.384a44.8 44.8 0 0 1 0-63.296L807.232 153.6H646.464a44.8 44.8 0 0 1-44.8-44.8 44.8 44.8 0 0 1 44.8-44.8h267.264a44.8 44.8 0 0 1 28.288 8.896l1.408 1.152 0.64 0.512 1.024 0.96a41.216 41.216 0 0 1 3.136 3.072l0.896 1.088 0.512 0.64 1.152 1.344a44.8 44.8 0 0 1 8.832 28.288V377.28a44.8 44.8 0 0 1-44.8 44.8 44.8 44.8 0 0 1-44.8-44.8V216.512l-326.4 326.4a44.8 44.8 0 0 1-32 13.248A44.8 44.8 0 0 1 480 543.616z" p-id="1628" fill="#2c2c2c"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg width="18px" height="18px" viewBox="0 0 18 18" class="text-primary"><g id="view-outline" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Group-13"><rect id="Rectangle" x="0" y="0" width="18" height="18"></rect><rect id="Rectangle" stroke="currentColor" x="1.5" y="1.5" width="15" height="15" rx="1"></rect></g></g></svg>

After

Width:  |  Height:  |  Size: 347 B

+1
View File
@@ -0,0 +1 @@
<svg width="24px" height="24px" viewBox="0 0 24 24" class="h-6 w-6"><g id="tool-seg-threshold" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><rect id="Rectangle" x="0" y="0" width="24" height="24"></rect><g id="Group" transform="translate(2.4184, 14.3673)" stroke="currentColor" stroke-width="1.5"><line x1="16.5816327" y1="2.13265306" x2="19.0816327" y2="2.13265306" id="Line-2" stroke-linecap="round"></line><line x1="0.0816326531" y1="2.13265306" x2="12.0816327" y2="2.13265306" id="Line-2" stroke-linecap="round"></line><circle id="Oval" cx="14.2244898" cy="2.09183673" r="2.09183673"></circle></g><g id="Group" transform="translate(11.7092, 7.4592) scale(-1, 1) translate(-11.7092, -7.4592)translate(1.9184, 5.3673)" stroke="currentColor" stroke-width="1.5"><line x1="16" y1="2.13265306" x2="19.0816327" y2="2.13265306" id="Line-2" stroke-linecap="round"></line><line x1="0.0816326531" y1="2.13265306" x2="11" y2="2.13265306" id="Line-2" stroke-linecap="round"></line><circle id="Oval" cx="13.2244898" cy="2.09183673" r="2.09183673"></circle></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+4 -2
View File
@@ -56,8 +56,10 @@ export default {
</script>
<style lang="scss" scoped>
@import "~@/styles/mixin.scss";
@import "~@/styles/variables.module.scss";
@use "@/styles/mixin.scss" as *;
@use "@/styles/variables.module.scss" as *;
// @import "~@/styles/mixin.scss";
// @import "~@/styles/variables.module.scss";
.app-wrapper {
@include clearfix;
+3
View File
@@ -70,6 +70,9 @@ import FB from '@/components/feedBack/index'
Vue.use(FB)
import FBT from '@/components/feedBackTable/index'
Vue.use(FBT)
import resize from '@/directive/resize/index'
// 表格自适应指令
Vue.use(resize)
import adaptive from '@/directive/adaptive/index'
// 表格自适应指令
Vue.use(adaptive)
-6
View File
@@ -156,12 +156,6 @@ export const constantRoutes = [
hidden: true,
component: () => import('@/views/trials/trials-panel/reading/dicoms/components/Fusion/PetCt')
},
{
path: '/fusion',
name: 'fusion',
hidden: true,
component: () => import('@/views/trials/trials-panel/reading/dicoms/components/Fusion/demo/index')
},
{
path: '/historyScreenshot',
+46 -39
View File
@@ -1,15 +1,16 @@
@import "./variables.module.scss";
@import "./mixin.scss";
@import "./transition.scss";
@import "./element-ui.scss";
@import "./sidebar.scss";
@use "./variables.module.scss";
@use "./mixin.scss";
@use "./transition.scss";
@use "./element-ui.scss";
@use "./sidebar.scss";
body {
height: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
// font-family:"Helvetica Neue", Helvetica, Arial, sans-serif;
// font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
// "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial,
@@ -205,133 +206,139 @@ body .el-table th.gutter {
.el-descriptions {
box-sizing: border-box;
font-size: 14px;
color: #303133
color: #303133;
}
.el-descriptions__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px
margin-bottom: 20px;
}
.el-descriptions__title {
font-size: 16px;
font-weight: 700
font-weight: 700;
}
.el-descriptions__body {
color: #606266;
background-color: #fff
background-color: #fff;
}
.el-descriptions__body .el-descriptions__table {
border-collapse: collapse;
width: 100%;
table-layout: fixed
table-layout: fixed;
}
.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell {
box-sizing: border-box;
text-align: left;
font-weight: 400;
line-height: 1.5
line-height: 1.5;
}
.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell.is-left {
text-align: left
.el-descriptions__body
.el-descriptions__table
.el-descriptions-item__cell.is-left {
text-align: left;
}
.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell.is-center {
text-align: center
.el-descriptions__body
.el-descriptions__table
.el-descriptions-item__cell.is-center {
text-align: center;
}
.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell.is-right {
text-align: right
.el-descriptions__body
.el-descriptions__table
.el-descriptions-item__cell.is-right {
text-align: right;
}
.el-descriptions .is-bordered {
table-layout: auto
table-layout: auto;
}
.el-descriptions .is-bordered .el-descriptions-item__cell {
border: 1px solid #ebeef5;
padding: 12px 10px
padding: 12px 10px;
}
.el-descriptions :not(.is-bordered) .el-descriptions-item__cell {
padding-bottom: 12px
padding-bottom: 12px;
}
.el-descriptions--medium.is-bordered .el-descriptions-item__cell {
padding: 10px
padding: 10px;
}
.el-descriptions--medium:not(.is-bordered) .el-descriptions-item__cell {
padding-bottom: 10px
padding-bottom: 10px;
}
.el-descriptions--small {
font-size: 12px
font-size: 12px;
}
.el-descriptions--small.is-bordered .el-descriptions-item__cell {
padding: 8px 10px
padding: 8px 10px;
}
.el-descriptions--small:not(.is-bordered) .el-descriptions-item__cell {
padding-bottom: 8px
padding-bottom: 8px;
}
.el-descriptions--mini {
font-size: 12px
font-size: 12px;
}
.el-descriptions--mini.is-bordered .el-descriptions-item__cell {
padding: 6px 10px
padding: 6px 10px;
}
.el-descriptions--mini:not(.is-bordered) .el-descriptions-item__cell {
padding-bottom: 6px
padding-bottom: 6px;
}
.el-descriptions-item {
vertical-align: top
vertical-align: top;
}
.el-descriptions-item__container {
display: flex
display: flex;
}
.el-descriptions-item__container .el-descriptions-item__content,
.el-descriptions-item__container .el-descriptions-item__label {
display: inline-flex;
align-items: baseline
align-items: baseline;
}
.el-descriptions-item__container .el-descriptions-item__content {
flex: 1
flex: 1;
}
.el-descriptions-item__label.has-colon:after {
content: ":";
position: relative;
top: -.5px
top: -0.5px;
}
.el-descriptions-item__label.is-bordered-label {
font-weight: 700;
color: #909399;
background: #fafafa
background: #fafafa;
}
.el-descriptions-item__label:not(.is-bordered-label) {
margin-right: 10px
margin-right: 10px;
}
.el-descriptions-item__content {
word-break: break-word;
overflow-wrap: break-word
overflow-wrap: break-word;
}
.feedBack-box {
@@ -350,7 +357,7 @@ body .el-table th.gutter {
top: 0;
width: 100%;
height: 100%;
opacity: .5;
opacity: 0.5;
background: #000;
z-index: 3999;
}
@@ -371,4 +378,4 @@ body .el-table th.gutter {
height: 20px !important;
vertical-align: -0.4em !important;
cursor: pointer;
}
}
+3 -2
View File
@@ -1,6 +1,6 @@
import Vue from 'vue'
import { anonymization } from './anonymization'
export const dcmUpload = async function (data, config, progressFn) {
export const dcmUpload = async function (data, config, progressFn, fileInfo) {
return new Promise(async resolve => {
try {
// let blob = await encoder(file, config)
@@ -8,7 +8,8 @@ export const dcmUpload = async function (data, config, progressFn) {
if (config) {
blob = await anonymization(data.file, config)
}
let res = await Vue.prototype.OSSclient.multipartUpload(Object.assign(data, { file: blob.blob }), progressFn)
let res = await Vue.prototype.OSSclient.multipartUpload(Object.assign(data, { file: blob.blob }), progressFn, fileInfo)
resolve({
...res,
image: blob.pixelDataElement
+1 -1
View File
@@ -8366,7 +8366,7 @@
for (var i = 0; i < valarr.length; i++) {
var checkValue = valarr[i],
checklen = lengths[i],
checklen = lengths[i] || 0,
isString = false,
displaylen = checklen;
+343
View File
@@ -0,0 +1,343 @@
import getNumberValues from "./getNumberValues";
import isNMReconstructable from "./isNMReconstructable";
/**
* Get a subpart of Image Type dicom tag defined by index
* @param {*} dataSet
* @param {*} index 0 based index of the subtype
*/
function getImageTypeSubItemFromDataset(dataSet, index) {
const imageType = dataSet.string("x00080008");
if (imageType) {
const subTypes = imageType.split("\\");
if (subTypes.length > index) {
return subTypes[index];
}
}
return undefined;
}
// DICOM 标准中针对 Enhanced CT/MR/PET 等“增强型多帧”(Enhanced Multi-frame)影像, 强制要求为每一帧显式记录真实的独立空间坐标 。
// - 它们的数据通常存放在: Per-frame Functional Groups Sequence (5200,9230) -> 对应的 Frame Item -> Plane Position Sequence (0020,9113) -> ImagePositionPatient (0020,0032) 。
function getSharedFunctionalGroupsDataSet(dataSet) {
const seq = dataSet?.elements?.x52009229;
if (!seq?.items?.length) {
return;
}
return seq.items[0].dataSet;
}
function getPerFrameFunctionalGroupsDataSet(dataSet, frameIndex) {
const seq = dataSet?.elements?.x52009230;
if (!seq?.items?.length) {
return;
}
if (
!Number.isInteger(frameIndex) ||
frameIndex < 0 ||
frameIndex >= seq.items.length
) {
return;
}
return seq.items[frameIndex].dataSet;
}
function getOrientationFromPlaneOrientationSequence(dataSet) {
const seq = dataSet?.elements?.x00209116;
if (!seq?.items?.length) {
return;
}
return getNumberValues(seq.items[0].dataSet, "x00200037", 6);
}
function getPositionFromPlanePositionSequence(dataSet) {
const seq = dataSet?.elements?.x00209113;
if (!seq?.items?.length) {
return;
}
return getNumberValues(seq.items[0].dataSet, "x00200032", 3);
}
function getPixelSpacingFromPixelMeasuresSequence(dataSet) {
const seq = dataSet?.elements?.x00289110;
if (!seq?.items?.length) {
return;
}
return getNumberValues(seq.items[0].dataSet, "x00280030", 2);
}
function getSliceThicknessFromPixelMeasuresSequence(dataSet) {
const seq = dataSet?.elements?.x00289110;
if (!seq?.items?.length) {
return;
}
if (!seq.items[0]?.dataSet?.elements?.x00180050) {
return;
}
return seq.items[0].dataSet.floatString("x00180050");
}
/**
* Extracts the orientation from NM multiframe dataset, if image type
* equal to RECON TOMO or RECON GATED TOMO
* @param {*} dataSet
* @returns
*/
function extractOrientationFromNMMultiframeDataset(dataSet) {
let imageOrientationPatient;
const modality = dataSet.string("x00080060");
if (modality?.includes("NM")) {
const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2);
if (imageSubType && isNMReconstructable(imageSubType)) {
if (dataSet.elements.x00540022) {
imageOrientationPatient = getNumberValues(
dataSet.elements.x00540022.items[0].dataSet,
"x00200037",
6
);
}
}
}
return imageOrientationPatient;
}
/**
* Extracts the position from NM multiframe dataset, if image type
* equal to RECON TOMO or RECON GATED TOMO
* @param {*} dataSet
* @returns
*/
function extractPositionFromNMMultiframeDataset(dataSet) {
let imagePositionPatient;
const modality = dataSet.string("x00080060");
if (modality?.includes("NM")) {
const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2);
if (imageSubType && isNMReconstructable(imageSubType)) {
if (dataSet.elements.x00540022) {
imagePositionPatient = getNumberValues(
dataSet.elements.x00540022.items[0].dataSet,
"x00200032",
3
);
}
}
}
return imagePositionPatient;
}
function isMultiFrame(dataSet) {
const numberOfFrames =
dataSet.string("x00280008") || dataSet.uint16("x00280008");
return numberOfFrames !== undefined && Number(numberOfFrames) > 1;
}
/**
* Extract orientation information from a dataset. It tries to get the orientation
* from the Detector Information Sequence (for NM images) if image type equal
* to RECON TOMO or RECON GATED TOMO
* @param {*} dataSet
* @returns
*/
function extractOrientationFromDataset(dataSet) {
// let imageOrientationPatient;
// if (isMultiFrame(dataSet)) {
// const perFrameDataSet = getPerFrameFunctionalGroupsDataSet(
// dataSet,
// frameIndex
// );
// imageOrientationPatient =
// getOrientationFromPlaneOrientationSequence(perFrameDataSet);
// if (!imageOrientationPatient) {
// const sharedDataSet = getSharedFunctionalGroupsDataSet(dataSet);
// imageOrientationPatient =
// getOrientationFromPlaneOrientationSequence(sharedDataSet);
// }
// }
// if (!imageOrientationPatient) {
// imageOrientationPatient = getNumberValues(dataSet, "x00200037", 6);
// }
let imageOrientationPatient = getNumberValues(dataSet, "x00200037", 6)
// Trying to get the orientation from the Plane Orientation Sequence
if (!imageOrientationPatient && dataSet.elements.x00209116) {
imageOrientationPatient = getNumberValues(
dataSet.elements.x00209116.items[0].dataSet,
"x00200037",
6
);
}
// If orientation not valid to this point, trying to get the orientation
// from the Detector Information Sequence (for NM images) with image type
// equal to RECON TOMO or RECON GATED TOMO
if (!imageOrientationPatient) {
imageOrientationPatient =
extractOrientationFromNMMultiframeDataset(dataSet);
}
return imageOrientationPatient;
}
/**
* Extract position information from a dataset. It tries to get the position
* from the Detector Information Sequence (for NM images) if image type equal
* to RECON TOMO or RECON GATED TOMO
* @param {*} dataSet
* @returns
*/
function extractPositionFromDataset(dataSet) {
// let imagePositionPatient;
// if (isMultiFrame(dataSet)) {
// const perFrameDataSet = getPerFrameFunctionalGroupsDataSet(
// dataSet,
// frameIndex
// );
// imagePositionPatient =
// getPositionFromPlanePositionSequence(perFrameDataSet);
// if (!imagePositionPatient) {
// const sharedDataSet = getSharedFunctionalGroupsDataSet(dataSet);
// imagePositionPatient =
// getPositionFromPlanePositionSequence(sharedDataSet);
// }
// }
// if (!imagePositionPatient) {
// imagePositionPatient = getNumberValues(dataSet, "x00200032", 3);
// }
let imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3);
// Trying to get the position from the Plane Position Sequence
if (!imagePositionPatient && dataSet.elements.x00209113) {
imagePositionPatient = getNumberValues(
dataSet.elements.x00209113.items[0].dataSet,
"x00200032",
3
);
}
// If position not valid to this point, trying to get the position
// from the Detector Information Sequence (for NM images)
if (!imagePositionPatient) {
imagePositionPatient = extractPositionFromNMMultiframeDataset(dataSet);
}
return imagePositionPatient;
}
/**
* Extract the pixelSpacing information. If exists, extracts this information
* from Pixel Measures Sequence
* @param {*} dataSet
* @returns
*/
function extractSpacingFromDataset(dataSet) {
// let pixelSpacing = getNumberValues(dataSet, 'x00280030', 2);
// // If pixelSpacing not valid to this point, trying to get the spacing
// // from the Pixel Measures Sequence
// if (!pixelSpacing && dataSet.elements.x00289110) {
// pixelSpacing = getNumberValues(
// dataSet.elements.x00289110.items[0].dataSet,
// 'x00280030',
// 2
// );
// }
// return pixelSpacing;
let rowPixelSpacing = null;
let columnPixelSpacing = null;
// let pixelSpacing;
// if (isMultiFrame(dataSet)) {
// const perFrameDataSet = getPerFrameFunctionalGroupsDataSet(
// dataSet,
// frameIndex
// );
// const sharedDataSet = getSharedFunctionalGroupsDataSet(dataSet);
// pixelSpacing =
// getPixelSpacingFromPixelMeasuresSequence(perFrameDataSet) ||
// getPixelSpacingFromPixelMeasuresSequence(sharedDataSet);
// }
// if (!pixelSpacing) {
// pixelSpacing =
// getNumberValues(dataSet, "x00280030", 2) ||
// getPixelSpacingFromPixelMeasuresSequence(dataSet);
// }
let pixelSpacing = getNumberValues(dataSet, 'x00280030', 2);
const imagePixelSpacing = getNumberValues(dataSet, "x00181164", 2);
const estimatedRadiographicMagnificationFactor = getNumberValues(
dataSet,
"x00181114",
2
);
if (pixelSpacing) {
rowPixelSpacing = pixelSpacing[0];
columnPixelSpacing = pixelSpacing[1];
} else if (imagePixelSpacing && estimatedRadiographicMagnificationFactor) {
rowPixelSpacing =
imagePixelSpacing[0] / estimatedRadiographicMagnificationFactor[0];
columnPixelSpacing =
imagePixelSpacing[1] / estimatedRadiographicMagnificationFactor[1];
} else if (imagePixelSpacing && !estimatedRadiographicMagnificationFactor) {
rowPixelSpacing = imagePixelSpacing[0];
columnPixelSpacing = imagePixelSpacing[1];
}
return rowPixelSpacing !== null
? [rowPixelSpacing, columnPixelSpacing]
: undefined;
}
/**
* Extract the sliceThickness information. If exists, extracts this information
* from Pixel Measures Sequence
* @param {*} dataSet
* @returns
*/
function extractSliceThicknessFromDataset(dataSet) {
let sliceThickness;
if (dataSet.elements.x00180050) {
sliceThickness = dataSet.floatString('x00180050');
}
else if (dataSet.elements.x00289110 &&
dataSet.elements.x00289110.items.length &&
dataSet.elements.x00289110.items[0].dataSet.elements.x00180050) {
sliceThickness =
dataSet.elements.x00289110.items[0].dataSet.floatString('x00180050');
}
// if (sliceThickness === undefined && isMultiFrame(dataSet)) {
// const perFrameDataSet = getPerFrameFunctionalGroupsDataSet(
// dataSet,
// frameIndex
// );
// const sharedDataSet = getSharedFunctionalGroupsDataSet(dataSet);
// sliceThickness =
// getSliceThicknessFromPixelMeasuresSequence(perFrameDataSet) ||
// getSliceThicknessFromPixelMeasuresSequence(sharedDataSet);
// }
return sliceThickness;
}
export {
getImageTypeSubItemFromDataset,
extractOrientationFromDataset,
extractPositionFromDataset,
extractSpacingFromDataset,
extractSliceThicknessFromDataset,
};
+24
View File
@@ -0,0 +1,24 @@
function getNumberValues(
dataSet,
tag,
minimumLength
){
const values = [];
const valueAsString = dataSet.string(tag);
if (!valueAsString) {
return;
}
const split = valueAsString.split('\\');
if (minimumLength && split.length < minimumLength) {
return;
}
for (let i = 0; i < split.length; i++) {
values.push(parseFloat(split[i]));
}
return values;
}
export default getNumberValues;
+3
View File
@@ -0,0 +1,3 @@
export default function isNMReconstructable(imageSubType) {
return imageSubType === 'RECON TOMO' || imageSubType === 'RECON GATED TOMO';
}
+67 -5
View File
@@ -1,4 +1,11 @@
import * as cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'
import {
getImageTypeSubItemFromDataset,
extractOrientationFromDataset,
extractPositionFromDataset,
extractSpacingFromDataset,
extractSliceThicknessFromDataset,
} from './extractPositioningFromDataset';
function parseImageId(imageId) {
// build a url by parsing out the url scheme and frame index from the imageId
const firstColonIndex = imageId.indexOf(':');
@@ -141,6 +148,19 @@ function populatePaletteColorLut(dataSet, imagePixelModule) {
imagePixelModule.bluePaletteColorLookupTableDescriptor
);
}
function getSpacingBetweenSlices(dataSet) {
if (dataSet?.elements?.x00180088) {
return dataSet.floatString('x00180088');
}
const pixelMeasuresSequence = dataSet?.elements?.x00289110;
if (
pixelMeasuresSequence?.items?.length &&
pixelMeasuresSequence.items[0]?.dataSet?.elements?.x00180088
) {
return pixelMeasuresSequence.items[0].dataSet.floatString('x00180088');
}
}
function metaDataProvider(type, imageId) {
const parsedImageId = parseImageId(imageId);
const dataSet = cornerstoneWADOImageLoader.wadouri.dataSetCacheManager.get(parsedImageId.url);
@@ -149,10 +169,47 @@ function metaDataProvider(type, imageId) {
return;
}
if (type === 'imagePlaneModule') {
const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6);
const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3);
const pixelSpacing = getNumberValues(dataSet, 'x00280030', 2);
// const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6);
// const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3);
// const pixelSpacing = getNumberValues(dataSet, 'x00280030', 2);
const imagePixelSpacing = getNumberValues(dataSet, 'x00181164', 2);
const frameIndex = parsedImageId.frame !== undefined ? parsedImageId.frame - 1 : undefined;
const imageOrientationPatient = extractOrientationFromDataset(dataSet, frameIndex);
let imagePositionPatient = extractPositionFromDataset(dataSet, frameIndex);
const pixelSpacing = extractSpacingFromDataset(dataSet, frameIndex);
const sliceThickness = extractSliceThicknessFromDataset(dataSet, frameIndex);
const modality = dataSet.string("x00080060");
if (modality && modality.includes('NM') && parsedImageId.frame !== undefined && parsedImageId.frame > 1) {
const spacingBetweenSlices = getSpacingBetweenSlices(dataSet);
const step = spacingBetweenSlices !== undefined ? spacingBetweenSlices : sliceThickness;
if (imageOrientationPatient && imagePositionPatient && step !== undefined && frameIndex !== undefined) {
const rowCosines = [
parseFloat(imageOrientationPatient[0]),
parseFloat(imageOrientationPatient[1]),
parseFloat(imageOrientationPatient[2]),
];
const columnCosines = [
parseFloat(imageOrientationPatient[3]),
parseFloat(imageOrientationPatient[4]),
parseFloat(imageOrientationPatient[5]),
];
const normal = [
rowCosines[1] * columnCosines[2] - rowCosines[2] * columnCosines[1],
rowCosines[2] * columnCosines[0] - rowCosines[0] * columnCosines[2],
rowCosines[0] * columnCosines[1] - rowCosines[1] * columnCosines[0],
];
const offset = frameIndex * parseFloat(step);
imagePositionPatient = [
parseFloat(imagePositionPatient[0]) + normal[0] * offset,
parseFloat(imagePositionPatient[1]) + normal[1] * offset,
parseFloat(imagePositionPatient[2]) + normal[2] * offset,
];
}
}
const estimatedRadiographicMagnificationFactor = getNumberValues(dataSet, 'x00181114', 2);
let columnPixelSpacing = null;
@@ -194,7 +251,7 @@ function metaDataProvider(type, imageId) {
rowCosines,
columnCosines,
imagePositionPatient,
sliceThickness: dataSet.floatString('x00180050'),
sliceThickness,
sliceLocation: dataSet.floatString('x00201041'),
pixelSpacing,
rowPixelSpacing,
@@ -224,6 +281,11 @@ function metaDataProvider(type, imageId) {
}
return imagePixelModule;
}
// if (type === "multiframeModule") {
// return {
// NumberOfFrames: dataSet.uint16("x00280008") || dataSet.string("x00280008")
// };
// }
// if (type === 'imagePixelModule') {
// return {
// samplesPerPixel: dataSet.uint16('x00280002'),
@@ -249,4 +311,4 @@ function metaDataProvider(type, imageId) {
// }
// }
}
export default metaDataProvider;
export default metaDataProvider;
+38 -8
View File
@@ -5,6 +5,7 @@ const stream = require('stream')
import Vue from 'vue'
import { customerHttp, OSSclose } from "@/utils/multipartUpload/oss"
import { exist, AWSclose } from "@/utils/multipartUpload/aws"
import { addOrUpdateFileUploadRecord } from '@/api/file'
const { GetObjectStoreToken } = require('../api/user.js')
const {
S3Client,
@@ -22,7 +23,6 @@ async function ossGenerateSTS() {
res = await GetObjectStoreToken()
localStorage.setItem('stsToken', JSON.stringify(res))
}
// res.Result.ObjectStoreUse = 'AWS';
Vue.prototype.OSSclientConfig = { ...res.Result[res.Result.ObjectStoreUse] }
Vue.prototype.OSSclientConfig.ObjectStoreUse = res.Result.ObjectStoreUse;
@@ -34,7 +34,8 @@ async function ossGenerateSTS() {
Vue.prototype.OSSclientConfig.timeout = 10 * 60 * 1000
let OSSclient = new OSS(Vue.prototype.OSSclientConfig)
Vue.prototype.OSSclient = {
put: async function (objectName, object) {
put: async function (objectName, object, fileInfo = {}) {
OSSclient = await RefreshClient(OSSclient)
return new Promise(async (resolve, reject) => {
try {
@@ -52,6 +53,15 @@ async function ossGenerateSTS() {
}
let res = await OSSclient.put(objectName, object)
if (res && res.url) {
const urlParams = new URLSearchParams(window.location.search)
const trialId = urlParams.get('trialId')
if (Object.keys(fileInfo).length !== 0) {
let params = Object.assign({path: objectName}, fileInfo)
addOrUpdateFileUploadRecord(params)
} else if (trialId) {
let params = { trialId }
addOrUpdateFileUploadRecord(params)
}
resolve({
name: objectName,
url: res.url
@@ -65,7 +75,7 @@ async function ossGenerateSTS() {
}
})
},
multipartUpload: async (data, progress) => {
multipartUpload: async (data, progress, fileInfo = {}) => {
OSSclient = await RefreshClient(OSSclient)
return new Promise(async (resolve, reject) => {
try {
@@ -95,6 +105,16 @@ async function ossGenerateSTS() {
}
let res = await customerHttp(OSSclient, data, progress);
if (res) {
const urlParams = new URLSearchParams(window.location.search)
const trialId = urlParams.get('trialId')
if (Object.keys(fileInfo).length !== 0) {
let params = Object.assign({path: data.path}, fileInfo)
addOrUpdateFileUploadRecord(params)
} else if (trialId) {
let params = { trialId }
addOrUpdateFileUploadRecord(params)
}
resolve({
name: data.path,
url: Vue.prototype.OSSclientConfig.viewEndpoint + decodeUtf8(res.name)
@@ -169,17 +189,17 @@ async function ossGenerateSTS() {
}
});
Vue.prototype.OSSclient = {
put: async function (objectName, object) {
put: async function (objectName, object, fileInfo = {}) {
let data = {
file: object,
path: objectName
}
aws = await RefreshClient(aws);
return uploadAWS(aws, data, () => { });
return uploadAWS(aws, data, () => { }, fileInfo);
},
multipartUpload: async (data, progress) => {
multipartUpload: async (data, progress, fileInfo = {}) => {
aws = await RefreshClient(aws);
return uploadAWS(aws, data, progress);
return uploadAWS(aws, data, progress, fileInfo);
},
close: () => {
AWSclose();
@@ -189,7 +209,7 @@ async function ossGenerateSTS() {
return
}
// AWS上传函数
function uploadAWS(aws, data, progress) {
function uploadAWS(aws, data, progress, fileInfo) {
return new Promise(async (resolve, reject) => {
try {
const { file, path } = data;
@@ -211,6 +231,16 @@ function uploadAWS(aws, data, progress) {
data.path = data.path.replace(`/${bucketName}/`, '');
await exist(aws, bucketName, data, progress, (path, status) => {
if (status === 'success') {
const urlParams = new URLSearchParams(window.location.search)
const trialId = urlParams.get('trialId')
if (Object.keys(fileInfo).length !== 0) {
let params = Object.assign({path: decodeUtf8(curPath)}, fileInfo)
addOrUpdateFileUploadRecord(params)
} else if (trialId) {
let params = { trialId }
addOrUpdateFileUploadRecord(params)
}
resolve({
name: decodeUtf8(curPath),
url: Vue.prototype.OSSclientConfig.viewEndpoint + decodeUtf8(curPath)
@@ -2,8 +2,8 @@
<div ref="mapbox" class="map-wrapper" />
</template>
<script>
import echarts from 'echarts'
import 'modules/echarts/map/js/world.js'
import * as echarts from 'echarts'
// import 'modules/echarts/map/js/world.js'
import { fontSize } from 'utils/fontsize'
export default {
props: {
@@ -14,7 +14,7 @@ export default {
watch: {
area() {
const that = this
this.$nextTick(function() {
this.$nextTick(function () {
that.myChart.resize()
})
}
@@ -56,7 +56,7 @@ export default {
}
]
]
var convertData = function(data) {
var convertData = function (data) {
var res = []
for (var i = 0; i < data.length; i++) {
var dataItem = data[i]
@@ -77,67 +77,67 @@ export default {
return res
}
var series = []
;[['Shanghai', BJData]].forEach(function(item, i) {
series.push(
{
type: 'lines',
zlevel: 1,
effect: {
show: true,
color: '#fff',
period: 6,
trailLength: 0.7,
symbolSize: fontSize(0.05)
},
lineStyle: {
normal: {
width: fontSize(0.03),
color: '#9ae5fc',
opacity: 0.02,
curveness: -0.2
}
},
data: convertData(item[1])
},
{
type: 'effectScatter',
coordinateSystem: 'geo',
zlevel: 3,
rippleEffect: {
// 涟漪特效
period: 5, // 动画时间,值越小速度越快
brushType: 'fill', // 波纹绘制方式 stroke, fill
scale: 5 // 波纹圆环最大限制,值越大波纹越大
},
label: {
normal: {
;[['Shanghai', BJData]].forEach(function (item, i) {
series.push(
{
type: 'lines',
zlevel: 1,
effect: {
show: true,
color: '#fce630',
position: 'right', // 显示位置
offset: [10, 0], // 偏移设置
fontSize: fontSize(0.16),
formatter: '{b}' // 圆环显示文字
color: '#fff',
period: 6,
trailLength: 0.7,
symbolSize: fontSize(0.05)
},
emphasis: {
show: true
}
},
symbol: 'circle',
symbolSize: function(val) {
return fontSize(0.08) // 圆环大小
lineStyle: {
normal: {
width: fontSize(0.03),
color: '#9ae5fc',
opacity: 0.02,
curveness: -0.2
}
},
data: convertData(item[1])
},
data: item[1].map(function(dataItem) {
return {
name: dataItem[0].name,
value: geoCoordMap[dataItem[0].name]
}
})
}
)
})
{
type: 'effectScatter',
coordinateSystem: 'geo',
zlevel: 3,
rippleEffect: {
// 涟漪特效
period: 5, // 动画时间,值越小速度越快
brushType: 'fill', // 波纹绘制方式 stroke, fill
scale: 5 // 波纹圆环最大限制,值越大波纹越大
},
label: {
normal: {
show: true,
color: '#fce630',
position: 'right', // 显示位置
offset: [10, 0], // 偏移设置
fontSize: fontSize(0.16),
formatter: '{b}' // 圆环显示文字
},
emphasis: {
show: true
}
},
symbol: 'circle',
symbolSize: function (val) {
return fontSize(0.08) // 圆环大小
},
data: item[1].map(function (dataItem) {
return {
name: dataItem[0].name,
value: geoCoordMap[dataItem[0].name]
}
})
}
)
})
var option = {
// backgroundColor: '#fff',
title: {
@@ -161,7 +161,7 @@ export default {
tooltip: {
trigger: 'item',
fontSize: fontSize(0.16),
formatter: function(params) {
formatter: function (params) {
if (typeof params.value === 'undefined') {
return ''
} else {
@@ -2,7 +2,7 @@
<div ref="trials_stats" style="width:100%;height:100%;" />
</template>
<script>
import echarts from 'echarts'
import * as echarts from 'echarts'
import { getTrialCountRank } from '@/api/dashboard'
import { fontSize } from 'utils/fontsize'
const Count = 5
@@ -2,7 +2,7 @@
<div ref="enroll_stats" style="width:100%;height:100%;" />
</template>
<script>
import echarts from 'echarts'
import * as echarts from 'echarts'
import { getEnrollDataByQuarter } from '@/api/dashboard'
import { fontSize } from 'utils/fontsize'
const Count = 6
@@ -2,7 +2,7 @@
<div ref="reviewers_stats" style="width:100%;height:100%;padding:5px;" />
</template>
<script>
import echarts from 'echarts'
import * as echarts from 'echarts'
import { getReviewersByRank } from '@/api/dashboard'
import { fontSize } from 'utils/fontsize'
export default {
@@ -2,7 +2,7 @@
<div ref="monthly_reading_stats" style="width:100%;height:100%;" />
</template>
<script>
import echarts from 'echarts'
import * as echarts from 'echarts'
import { getReadingDataByMonth } from '@/api/dashboard'
import { fontSize } from 'utils/fontsize'
const Count = 6
@@ -2,7 +2,7 @@
<div ref="reading_rank" class="reading-rank" />
</template>
<script>
import echarts from 'echarts'
import * as echarts from 'echarts'
import { getReadingDataRank } from '@/api/dashboard'
import { fontSize } from 'utils/fontsize'
const Count = 5
@@ -2,7 +2,7 @@
<div ref="reading_stats" style="width:100%;height:100%;padding-top:5px;" />
</template>
<script>
import echarts from 'echarts'
import * as echarts from 'echarts'
import { getReadingDataByType } from '@/api/dashboard'
import { fontSize } from 'utils/fontsize'
export default {
+2 -2
View File
@@ -965,8 +965,8 @@ export default {
}
</script>
<style lang="scss" scoped>
.imageBox {
<style lang="scss">
.viewerContainer .imageBox {
position: relative;
i {
+87 -90
View File
@@ -158,108 +158,105 @@
<div class="viewerSidethumbinner">
<el-collapse v-model="relationActiveName" @change="handelRelationActiveChange">
<template v-for="item in relationStudyListByVisitName">
<div :key="`${item.VisitName}`">
<div v-show="item.VisitName" class="text-desc" style="background-color: #1f1f1f;">
{{ item.VisitName }}
</div>
<template v-for="(study, studyIndex) in relationStudyList">
<el-collapse-item :key="`${study.StudyId}`" :name="`${study.StudyId}`"
v-if="study.VisitName === item.VisitName">
<template slot="title">
<div v-for="item in relationStudyListByVisitName" :key="`${item.VisitName}`">
<div v-show="item.VisitName" class="text-desc" style="background-color: #1f1f1f;">
{{ item.VisitName }}
</div>
<template v-for="(study, studyIndex) in relationStudyList">
<el-collapse-item :key="`${study.StudyId}`" :name="`${study.StudyId}`"
v-if="study.VisitName === item.VisitName">
<template slot="title">
<div class="text-desc">
{{ study.StudyCode }}
</div>
<!-- <div v-show="study.Description" class="text-desc">
{{ study.Description }}
</div> -->
<div v-show="study.SeriesCount" class="text-desc">
{{ study.Modalities }} : {{ study.SeriesCount }} Series
</div>
</template>
<div v-show="study.Description" class="text-desc" style="background-color: #1f1f1f;">
{{ study.Description }}
<div class="text-desc">
{{ study.StudyCode }}
</div>
<div class="viewerSidethumbs ps" style="position: relative;">
<div class="viewerSidethumbinner">
<div v-for="(seriesItem, index) in study.seriesList" :key="seriesItem.seriesId">
<div class="viewernavigatorwrapper"
style="position: relative;border:1px solid #434343;" series-type="relation"
@click="showRelationSeriesImage($event, seriesItem, studyIndex, index)">
<div class="imageBox" style="width: 72px;height:72px;">
<img class="image-preview" :src="seriesItem.previewImageUrl"
crossorigin="anonymous" alt="" style="width: 72px;height:72px;" fit="fill" />
<i class="el-icon-refresh" :title="$t('tip:refreshImage')"
@click.stop="refreshImage(item)"></i>
</div>
<!-- <div v-show="study.Description" class="text-desc">
{{ study.Description }}
</div> -->
<div class="viewernavitextwrapper">
<div
style="padding: 1px 5px 1px 1px;display: flex;justify-content: space-between;">
<div v-if="seriesItem.keySeries" style="color:red">
Key Images
</div>
<div v-else>#{{ seriesItem.seriesNumber }}</div>
<div v-if="seriesItem.isExistMutiFrames && seriesItem.instanceCount > 1">
<el-popover placement="right-start" trigger="click"
popper-class="instance_frame_wrapper">
<div class="frame_list">
<div v-for="(instance, idx) in seriesItem.instanceInfoList"
:key="instance.Id" class="frame_content"
:style="{ 'margin-bottom': idx < seriesItem.instanceInfoList.length - 1 ? '5px' : '0px' }"
@click="showMultiFrames(studyIndex, seriesItem, index, instance)">
<div>
<div>{{ instance.InstanceNumber }}</div>
<div>{{ `${instance.NumberOfFrames > 0 ? instance.NumberOfFrames : 1}
frame`
}}</div>
</div>
<div v-show="study.SeriesCount" class="text-desc">
{{ study.Modalities }} : {{ study.SeriesCount }} Series
</div>
</template>
<div v-show="study.Description" class="text-desc" style="background-color: #1f1f1f;">
{{ study.Description }}
</div>
<div class="viewerSidethumbs ps" style="position: relative;">
<div class="viewerSidethumbinner">
<div v-for="(seriesItem, index) in study.seriesList" :key="seriesItem.seriesId">
<div class="viewernavigatorwrapper"
style="position: relative;border:1px solid #434343;" series-type="relation"
@click="showRelationSeriesImage($event, seriesItem, studyIndex, index)">
<div class="imageBox" style="width: 72px;height:72px;">
<img class="image-preview" :src="seriesItem.previewImageUrl"
crossorigin="anonymous" alt="" style="width: 72px;height:72px;" fit="fill" />
<i class="el-icon-refresh" :title="$t('tip:refreshImage')"
@click.stop="refreshImage(item)"></i>
</div>
<div class="viewernavitextwrapper">
<div
style="padding: 1px 5px 1px 1px;display: flex;justify-content: space-between;">
<div v-if="seriesItem.keySeries" style="color:red">
Key Images
</div>
<div v-else>#{{ seriesItem.seriesNumber }}</div>
<div v-if="seriesItem.isExistMutiFrames && seriesItem.instanceCount > 1">
<el-popover placement="right-start" trigger="click"
popper-class="instance_frame_wrapper">
<div class="frame_list">
<div v-for="(instance, idx) in seriesItem.instanceInfoList"
:key="instance.Id" class="frame_content"
:style="{ 'margin-bottom': idx < seriesItem.instanceInfoList.length - 1 ? '5px' : '0px' }"
@click="showMultiFrames(studyIndex, seriesItem, index, instance)">
<div>
<div>{{ instance.InstanceNumber }}</div>
<div>{{ `${instance.NumberOfFrames > 0 ? instance.NumberOfFrames : 1}
frame`
}}</div>
</div>
</div>
<i slot="reference" class="el-icon-connection"
style="font-size: 15px;cursor: pointer;" />
</el-popover>
</div>
</div>
</div>
<i slot="reference" class="el-icon-connection"
style="font-size: 15px;cursor: pointer;" />
</el-popover>
</div>
<div v-show="seriesItem.instanceCount" style="padding: 1px;">
{{ seriesItem.modality }}: {{ seriesItem.instanceCount }} image
</div>
<div v-show="seriesItem.sliceThickness" style="padding: 1px;">
T: {{ seriesItem.sliceThickness }}
</div>
<el-tooltip v-show="seriesItem.description" class="item" effect="dark"
:content="seriesItem.description" placement="bottom">
<div v-show="seriesItem.description"
style="width: 120px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;padding: 1x;">
{{ seriesItem.description }}
</div>
</el-tooltip>
</div>
<div v-show="seriesItem.instanceCount" style="padding: 1px;">
{{ seriesItem.modality }}: {{ seriesItem.instanceCount }} image
</div>
<div v-show="seriesItem.sliceThickness" style="padding: 1px;">
T: {{ seriesItem.sliceThickness }}
</div>
<el-tooltip v-show="seriesItem.description" class="item" effect="dark"
:content="seriesItem.description" placement="bottom">
<div v-show="seriesItem.description"
style="width: 120px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;padding: 1x;">
{{ seriesItem.description }}
</div>
</el-tooltip>
</div>
<!-- <el-image
class="image-preview"
style="height:72px;width:72px;"
:src="seriesItem.previewImageUrl"
fit="fill"
/> -->
<div
v-if="seriesItem.prefetchInstanceCount > 0 && seriesItem.prefetchInstanceCount < seriesItem.instanceCount * 100">
<el-progress
:percentage="parseInt((seriesItem.prefetchInstanceCount / seriesItem.instanceCount).toFixed(2))" />
</div>
</div>
<!-- <el-image
class="image-preview"
style="height:72px;width:72px;"
:src="seriesItem.previewImageUrl"
fit="fill"
/> -->
<div
v-if="seriesItem.prefetchInstanceCount > 0 && seriesItem.prefetchInstanceCount < seriesItem.instanceCount * 100">
<el-progress
:percentage="parseInt((seriesItem.prefetchInstanceCount / seriesItem.instanceCount).toFixed(2))" />
</div>
</div>
</div>
</el-collapse-item>
</template>
</div>
</template>
</div>
</el-collapse-item>
</template>
</div>
</el-collapse>
</div>
@@ -1012,7 +1009,7 @@ export default {
</script>
<style lang="scss">
.imageBox {
.viewerContainer .imageBox {
position: relative;
i {
@@ -278,7 +278,7 @@ export default {
padding: 5px 20px;
margin-top: 5px;
background: #fff;
::v-deep.el-form-item {
::v-deep .el-form-item {
padding-bottom: 20px;
}
@@ -292,10 +292,10 @@ export default {
}
}
.form-label-width{
::v-deep.el-form-item__label{
::v-deep .el-form-item__label{
width: 140px;
}
::v-deep.el-form-item__content{
::v-deep .el-form-item__content{
margin-left: 140px;
}
}
@@ -304,7 +304,7 @@ export default {
text-align: center;
}
::v-deep.el-form-item {
::v-deep .el-form-item {
margin-bottom: 0px;
padding-top: 5px;
border-bottom: 1px solid #f5f7fa;
@@ -156,7 +156,7 @@ export default {
<style lang="scss" scoped>
.equipment_form_content{
padding: 0 10px;
::v-deep.el-form-item {
::v-deep .el-form-item {
margin-bottom: 0px;
padding: 5px 0 20px 0;
border-bottom: 1px solid #f5f7fa;
@@ -295,7 +295,7 @@ export default {
<style lang="scss" scoped>
.participants_form_content{
padding: 0 10px;
::v-deep.el-form-item {
::v-deep .el-form-item {
margin-bottom: 0px;
padding: 5px 0 20px 0;
border-bottom: 1px solid #f5f7fa;
@@ -309,7 +309,7 @@ export default {
<style lang="scss" scoped>
.participants_form_content{
padding: 0 10px;
::v-deep.el-form-item {
::v-deep .el-form-item {
margin-bottom: 0px;
padding: 5px 0 20px 0;
border-bottom: 1px solid #f5f7fa;
+2 -2
View File
@@ -381,7 +381,7 @@ export default {
padding: 5px 20px;
margin-top: 5px;
background: #fff;
::v-deep.el-form-item {
::v-deep .el-form-item {
padding-bottom: 20px;
}
}
@@ -398,7 +398,7 @@ export default {
text-align: center;
}
::v-deep.el-form-item {
::v-deep .el-form-item {
margin-bottom: 0px;
padding-top: 5px;
border-bottom: 1px solid #f5f7fa;
+3 -3
View File
@@ -357,7 +357,7 @@ export default {
overflow-y: auto;
}
::v-deep.el-card__body {
::v-deep .el-card__body {
padding: 10px;
}
@@ -365,13 +365,13 @@ export default {
// padding: 10px 20px 20px 20px;
// }
::v-deep.full-dialog-container {
::v-deep .full-dialog-container {
.el-dialog__body {
height: calc(100% - 80px);
}
}
::v-deep.dialog-container {
::v-deep .dialog-container {
// margin-top: 50px !important;
width: 75%;
height: 80%;
@@ -44,7 +44,7 @@ export default {
overflow: hidden;
display: flex;
flex-direction: column;
::v-deep.el-card__body {
::v-deep .el-card__body {
padding: 0px;
}
.trials-tab{
+4 -2
View File
@@ -28,8 +28,10 @@ export default {
</script>
<style lang="scss" scoped>
@import "~@/styles/mixin.scss";
@import "~@/styles/variables.module.scss";
// @import "~@/styles/mixin.scss";
// @import "~@/styles/variables.module.scss";
@use "@/styles/mixin.scss" as *;
@use "@/styles/variables.module.scss" as *;
.trials-wrapper {
@include clearfix;
@@ -2928,12 +2928,12 @@ export default {
}
.series-table {
::v-deep.el-table {
::v-deep .el-table {
background-color: #1e1e1e !important;
color: #dfdfdf;
}
::v-deep.el-table td.el-table__cell,
::v-deep .el-table td.el-table__cell,
.el-table th.el-table__cell.is-leaf {
border-bottom: 1px solid #dfdfdf;
}
@@ -2944,14 +2944,14 @@ export default {
background-color: #1e1e1e;
}
::v-deep.el-table__header-wrapper {
::v-deep .el-table__header-wrapper {
th {
background-color: #1e1e1e !important;
color: #dfdfdf;
}
}
::v-deep.el-table__body-wrapper {
::v-deep .el-table__body-wrapper {
tr {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -2962,7 +2962,7 @@ export default {
}
}
::v-deep.el-table__empty-block {
::v-deep .el-table__empty-block {
background-color: #1e1e1e !important;
}
}
@@ -319,23 +319,23 @@ export default {
</script>
<style lang="scss" scoped>
.series-table{
::v-deep.el-table{
::v-deep .el-table{
background-color: #1e1e1e !important;
color: #dfdfdf;
}
::v-deep.el-table td.el-table__cell, .el-table th.el-table__cell.is-leaf{
::v-deep .el-table td.el-table__cell, .el-table th.el-table__cell.is-leaf{
border-bottom: 1px solid #dfdfdf;
}
.el-table--border::after, .el-table--group::after, .el-table::before{
background-color: #1e1e1e;
}
::v-deep.el-table__header-wrapper{
::v-deep .el-table__header-wrapper{
th{
background-color: #1e1e1e !important;
color: #dfdfdf;
}
}
::v-deep.el-table__body-wrapper{
::v-deep .el-table__body-wrapper{
tr{
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -344,7 +344,7 @@ export default {
background-color: #1e1e1e !important;
}
}
::v-deep.el-table__empty-block{
::v-deep .el-table__empty-block{
background-color: #1e1e1e !important;
}
}
@@ -140,46 +140,8 @@
</div>
</el-tooltip>
<!-- 伪彩 -->
<el-tooltip class="item" effect="dark" :content="$t('trials:lugano:button:colormap')" placement="bottom">
<div class="colorBar" style="display:flex;justify-content: flex-start;align-items: center;position: relative;"
@mouseleave="isSlideMoving = false">
<div class="tool-wrapper" style="margin-right:0px" @click.stop="showColorBarPanel($event)"
@mouseleave="handleColorBarMouseout">
<div>
<!-- <canvas id="colorBarCanvas" /> -->
<div class="dropdown">
<div id="colorBar" class="icon" style="display: flex;align-items: center;width:266px">
<canvas id="colorBarCanvas" />
</div>
<!-- 伪彩 -->
<div class="text">{{ $t('trials:lugano:button:colormap') }}</div>
<div class="dropdown-content" style="width:266px">
<ul>
<li v-for="(colorMap, index) in colorMaps" :key="colorMap"
style="display: flex;align-items: center;margin-bottom:5px;padding:0 5px;justify-content: space-between;"
:class="{ activeLi: rgbPresetName === colorMap }" @click="setColorMap(colorMap)">
<canvas :id="`colorBarCanvas${index}`" />
<span style="margin-left:5px;font-size: 10px;">{{ colorMap }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
<div style="margin-left:-1px;border: 1px solid #424242;">
<el-input v-model="range" size="mini" style="width:120px" maxlength="3"
oninput="if(value){value=value.replace(/[^\d]/g,'')} if(value<=0){value=''}" @change="upperRangeChange">
<template slot="append">g/ml</template>
</el-input>
</div>
<div id="slider" style="position: absolute;left: 6px;top:5px;cursor: pointer;">
<div id="sliderBox" class="slider" style="height: 17px;width: 10px;background-color: #909399;" />
<div id="slider-position" style="color:#ddd;font-size: 12px;">0</div>
</div>
</div>
</el-tooltip>
<colorMap v-show="isFusion" ref="colorMap" :unit="fusionOverlayModality === 'NM' ? 'counts' : 'g/ml'"
:modality="fusionOverlayModality" @setColorMap="setColorMap" @voiChange="voiChange" />
<!-- 截屏 -->
<!-- <el-tooltip class="item" effect="dark" :content="`${$t('trials:reading:button:screenShot')}`" placement="bottom">
@@ -217,15 +179,14 @@
<Viewport ref="CT_AXIAL" :index="1" :active-index="activeIndex"
:is-reading-show-subject-info="isReadingShowSubjectInfo" :series-info="ctSeries"
:rendering-engine-id="renderingEngineId" viewport-id="CT_AXIAL" :volume="ctVolume"
:measure-datas="measureDatas" :style="1 === activeIndex ? viewportStyle : {}"
@upperRangeChange="upperRangeChange" />
:measure-datas="measureDatas" :style="1 === activeIndex ? viewportStyle : {}" />
<Viewport ref="PT_AXIAL" :index="2" :active-index="activeIndex"
:is-reading-show-subject-info="isReadingShowSubjectInfo" :series-info="petSeries"
:rendering-engine-id="renderingEngineId" viewport-id="PT_AXIAL" :volume="ptVolume"
:measure-datas="measureDatas" :style="2 === activeIndex ? viewportStyle : {}"
@upperRangeChange="upperRangeChange" />
<Viewport ref="FUSION_AXIAL" :index="3" :active-index="activeIndex"
:is-reading-show-subject-info="isReadingShowSubjectInfo" :series-info="petSeries"
:is-reading-show-subject-info="isReadingShowSubjectInfo" :series-info="petSeries" :ct-series-info="ctSeries"
:rendering-engine-id="renderingEngineId" viewport-id="FUSION_AXIAL" :volume="ptVolume"
:measure-datas="measureDatas" :rgb-preset-name="rgbPresetName" :style="3 === activeIndex ? viewportStyle : {}"
@upperRangeChange="upperRangeChange" />
@@ -306,6 +267,7 @@ import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunc
import { mat4, vec3 } from 'gl-matrix'
import html2canvas from 'html2canvas'
import readingChart from '@/components/readingChart'
import colorMap from '@/views/trials/trials-panel/reading/dicoms3D/components/colorMap.vue'
// import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'
// import vtkMath from '@kitware/vtk.js/Common/Core/Math'
// import CircleROITool from './tools/CircleROITool'
@@ -395,7 +357,8 @@ export default {
TableQuestions,
CustomWwwcForm,
FusionForm,
readingChart
readingChart,
colorMap
},
data() {
return {
@@ -441,6 +404,8 @@ export default {
upper: 6,
isSlideMoving: false,
viewportStyle: {},
isFusion: true,
fusionOverlayModality: 'PT',
defaultWwwc: [
{ label: this.$t('trials:reading:button:wwwcDefault'), val: -1, ww: null, wc: null }, //
{ label: this.$t('trials:reading:button:wwwcCustom'), val: 0, ww: null, wc: null }, //
@@ -458,7 +423,9 @@ export default {
activeCanvasWW: null,
activeCanvasWC: null,
fusion: { visible: false }, //
screenshotWindow: null
screenshotWindow: null,
hasVoiChanged: false,
lastUpper: null
// initFirstAnnotation:false
}
},
@@ -512,13 +479,13 @@ export default {
})
this.colorMaps = getColormapNames()
this.colorMaps.unshift('hsv')
this.$nextTick(() => {
this.renderColorMaps()
this.handleElementsClick()
this.initPage()
this.upperRangeChange(this.range)
this.initSlider()
})
this.$nextTick(() => {
// this.renderColorMaps()
this.handleElementsClick()
this.initPage()
// this.upperRangeChange(this.range)
// this.initSlider()
})
FusionEvent.$on('getAnnotations', () => {
console.log('getAnnotations')
@@ -744,6 +711,7 @@ export default {
return new Promise(resolve => {
getDicomSeriesInfo({ seriesId }).then(res => {
var series = res.Result
this.fusionOverlayModality = series.Modality
var imageIds = []
var instanceList = []
series.InstanceInfoList.forEach(instance => {
@@ -1057,7 +1025,7 @@ export default {
// volume to use for the WindowLevelTool for the fusion viewports
fusionToolGroup.addTool(WindowLevelTool.toolName, {
volumeId: ptVolumeId
volumeId: ptVolumeId2
});
[ctToolGroup, ptToolGroup, fusionToolGroup].forEach((toolGroup) => {
@@ -1627,148 +1595,141 @@ export default {
var slider = document.getElementById('slider')
var sliderBox = document.getElementById('sliderBox')
var container = document.getElementById('colorBarCanvas')
slider.addEventListener('mousedown', () => {
if (!slider || !sliderBox || !container) return
slider.addEventListener('mousedown', (e) => {
this.isSlideMoving = true
e.stopPropagation()
// document.onselectstart = function() { return false }//
// document.ondragstart = function() { return false }
})
document.addEventListener('mousemove', (e) => {
if (this.isSlideMoving) {
var containerWidth = container.clientWidth
var colorBarContainer = document.getElementById('colorBar')
if (!colorBarContainer) return
var containerWidth = colorBarContainer.clientWidth
var sliderWidth = sliderBox.clientWidth
var maxLeft = containerWidth - sliderWidth
var left = e.clientX - container.getBoundingClientRect().left
var position = null
position = left
//
var rect = colorBarContainer.getBoundingClientRect()
var left = e.clientX - rect.left - (sliderWidth / 2)
if (left < 0) {
left = 6
position = 0
left = 0
} else if (left > maxLeft) {
left = maxLeft + 6
position = maxLeft
left = maxLeft
}
slider.style.left = left + 'px'
var positionValue = document.getElementById('slider-position')
var upper = this.range
position = parseInt((position / maxLeft) * upper)
positionValue.textContent = position
var position = parseInt((left / maxLeft) * upper)
if (positionValue) {
positionValue.textContent = position
}
this.upper = position
this.voiChange(position)
if (this.sliderTimeout) {
cancelAnimationFrame(this.sliderTimeout)
}
this.sliderTimeout = requestAnimationFrame(() => {
this.voiChange(position)
})
}
})
document.addEventListener('mouseup', () => {
this.isSlideMoving = false
document.addEventListener('mouseup', (e) => {
if (this.isSlideMoving) {
this.isSlideMoving = false
if (this.sliderTimeout) {
cancelAnimationFrame(this.sliderTimeout)
}
this.voiChange(this.upper)
}
// document.onselectstart = null
// document.ondragstart = null
})
},
upperRangeChange(v) {
if (v === 0 || v < this.upper) {
return
}
var sliderBox = document.getElementById('sliderBox')
var container = document.getElementById('colorBarCanvas')
var containerWidth = container.clientWidth
var sliderWidth = sliderBox.clientWidth
var maxLeft = containerWidth - sliderWidth
var left = (this.upper / this.range) * maxLeft
if (left < 0) {
left = 6
} else if (left >= maxLeft) {
left = maxLeft + 6
}
var slider = document.getElementById('slider')
slider.style.left = left + 'px'
var positionValue = document.getElementById('slider-position')
positionValue.textContent = this.upper
},
renderColorMaps() {
this.createColorBar(this.rgbPresetName, 'colorBarCanvas', 256, 15)
this.$refs['FUSION_AXIAL'].setPreset(this.rgbPresetName)
this.colorMaps.forEach((e, index) => {
this.createColorBar(e, `colorBarCanvas${index}`, 180, 15)
})
},
getVOIRange(viewportId, volumeId) {
const defaultImageRange = { lower: -1000, upper: 1000 }
const viewport = (
renderingEngine.getViewport(viewportId)
)
const volumeActor = volumeId
? viewport.getActor(volumeId)
: viewport.getDefaultActor()
if (!volumeActor || !csUtils.isImageActor(volumeActor)) {
return defaultImageRange
}
const voiRange = (volumeActor.actor)
.getProperty()
.getRGBTransferFunction(0)
.getRange()
return voiRange[0] === 0 && voiRange[1] === 0
? defaultImageRange
: { lower: voiRange[0], upper: voiRange[1] }
},
voiChange(v) {
// console.log(this.lastUpper, this.hasVoiChanged, v)
if (this.lastUpper === v && this.hasVoiChanged) return;
this.lastUpper = v;
this.hasVoiChanged = true;
let viewportIds = ['FUSION_AXIAL', 'PT_AXIAL', 'PET_MIP_CORONAL']
viewportIds.map(viewportId => {
// const volumeId = viewportId === 'viewportId' ? ptVolumeId : ctVolumeId
const volumeId = ptVolumeId
const volumeId = viewportId === 'FUSION_AXIAL' ? ptVolumeId2 : ptVolumeId
const voiRange = { lower: 0, upper: v }
const viewport = (
renderingEngine.getViewport(viewportId)
)
if (!viewport) return
// const volumeId = viewport.getVolumeId()
// const viewportsContainingVolumeUID = csUtils.getViewportsWithVolumeId(
// volumeId,
// viewport.renderingEngineId
// )
viewport.setProperties({ voiRange }, volumeId)
viewport.render()
this.$refs[viewport.id].setWwWc()
// viewportsContainingVolumeUID.forEach((vp) => {
// vp.render()
// this.$refs[vp.id].setWwWc()
// console.log(vp.id)
// })
this.$refs[viewport.id] && this.$refs[viewport.id].setWwWc()
})
// colorMap
if (this.$refs.colorMap) {
this.$refs.colorMap.changeVoi(v)
}
},
// async setColorMap(rgbPresetName) {
// this.rgbPresetName = rgbPresetName
// if (this.$refs.FUSION_AXIAL) {
// this.$refs.FUSION_AXIAL.setPreset(this.rgbPresetName)
// this.$refs.FUSION_AXIAL.renderColorBar(this.rgbPresetName)
// }
// const viewport = getRenderingEngine(renderingEngineId)?.getViewport('FUSION_AXIAL')
// if (!viewport) return
// const actorEntry = viewport.getActors()?.find(e => String(e.uid || '').includes('PT_VOLUME_ID2'))
// if (!actorEntry?.actor) return
// const preset = rgbPresetName === 'hsv' ? vtkColorMaps.getPresetByName(rgbPresetName) : getColormap(rgbPresetName)
// const cfun = vtkColorTransferFunction.newInstance()
// const rgbPoints = preset?.RGBPoints
// if (Array.isArray(rgbPoints)) {
// for (let i = 0; i < rgbPoints.length; i += 4) {
// cfun.addRGBPoint(rgbPoints[i], rgbPoints[i + 1], rgbPoints[i + 2], rgbPoints[i + 3])
// }
// }
// cfun.setMappingRange(0, 5)
// actorEntry.actor.getProperty().setRGBTransferFunction(0, cfun)
// const ofun = vtkPiecewiseFunction.newInstance()
// ofun.addPoint(0, 0.0)
// ofun.addPoint(0.1, 0.9)
// ofun.addPoint(5, 1.0)
// actorEntry.actor.getProperty().setScalarOpacity(0, ofun)
// viewport.render()
// },
async setColorMap(rgbPresetName) {
this.rgbPresetName = rgbPresetName
let viewports = ['FUSION_AXIAL', 'PT_AXIAL', 'PET_MIP_CORONAL']
let viewports = ['FUSION_AXIAL']
viewports.map(v => {
this.$refs[v].setPreset(this.rgbPresetName)
this.$refs[v].renderColorBar(this.rgbPresetName)
this.createColorBar(this.rgbPresetName, 'colorBarCanvas', 256, 15)
const renderingEngine = getRenderingEngine(renderingEngineId)
const viewport = (
renderingEngine.getViewport(v)
)
viewport.setProperties({ colormap: { name: rgbPresetName } }, ptVolumeId)
viewport.render()
this.$refs[v] && this.$refs[v].setPreset(this.rgbPresetName)
this.$refs[v] && this.$refs[v].setColorMap(rgbPresetName)
})
// this.$refs['FUSION_AXIAL'].setPreset(this.rgbPresetName)
// this.$refs['FUSION_AXIAL'].renderColorBar(this.rgbPresetName)
// this.createColorBar(this.rgbPresetName, 'colorBarCanvas', 256, 15)
// const renderingEngine = getRenderingEngine(renderingEngineId)
},
upperRangeChange(v) {
if (this.upper === v) return
this.upper = v;
// const viewport = (
// renderingEngine.getViewport(viewportIds.FUSION.AXIAL)
// )
// viewport.setProperties({ colormap: { name: rgbPresetName } }, ptVolumeId)
// // viewport.setProperties({ colormap: { name: rgbPresetName }}, ctVolumeId)
// viewport.render()
// document.onselectstart = function() { return false }//
// document.ondragstart = function() { return false }
if (v === 0) {
return
}
this.voiChange(v)
// colorMap
if (this.$refs.colorMap) {
this.$refs.colorMap.changeVoi(v)
}
},
setPetColorMap(volumeInfo) {
const { volumeActor } = volumeInfo
@@ -1979,17 +1940,24 @@ export default {
// document.onselectstart = function() { return false }//
// document.ondragstart = function() { return false }
},
setWwwc(v) {
// this.changeMapperRange(v.wc, v.ww)
const renderingEngine = getRenderingEngine(renderingEngineId)
let viewportId = this.activeIndex === 1 ? 'CT_AXIAL' : this.activeIndex === 2 ? 'PT_AXIAL' : this.activeIndex === 3 ? 'FUSION_AXIAL' : 'PET_MIP_CORONAL'
const viewport = renderingEngine.getViewport(viewportId)
const lower = v.wc - v.ww / 2
const upper = v.wc + v.ww / 2 - 1
viewport.setProperties({ voiRange: { upper: upper, lower: lower } })
viewport.render()
this.customWwc.visible = false
},
// setWwwc(v) {
// // this.changeMapperRange(v.wc, v.ww)
// const renderingEngine = getRenderingEngine(renderingEngineId)
// let viewportId = this.activeIndex === 1 ? 'CT_AXIAL' : this.activeIndex === 2 ? 'PT_AXIAL' : this.activeIndex === 3 ? 'FUSION_AXIAL' : 'PET_MIP_CORONAL'
// const viewport = renderingEngine.getViewport(viewportId)
// const lower = v.wc - v.ww / 2
// const upper = v.wc + v.ww / 2 - 1
// viewport.setProperties({ voiRange: { upper: upper, lower: lower } })
// viewport.render()
// if (viewportId !== 'CT_AXIAL') {
// const nextUpper = Math.round(upper)
// this.upper = nextUpper
// if (this.$refs.colorMap) {
// this.$refs.colorMap.changeVoi(nextUpper)
// }
// }
// this.customWwc.visible = false
// },
changeMapperRange(wc, ww) {
var lower = wc - ww / 2.0
var upper = wc + ww / 2.0
@@ -2262,7 +2230,7 @@ export default {
background-color: #000;
padding: 5px 2px;
::v-deep.el-dialog {
::v-deep .el-dialog {
background: #1e1e1e;
border: 1px solid #ddd;
color: #ddd;
@@ -19,6 +19,8 @@
</div>
<div v-if="index !== 4">Image: #{{ `${seriesInfo.imageIdIndex + 1} / ${seriesInfo.imageMaxLength}` }}</div>
<div v-if="index === 1 || index === 2">{{ seriesInfo.modality }}</div>
<div v-if="index === 3">{{ ctSeriesInfo ? ctSeriesInfo.modality + ' / ' + seriesInfo.modality : seriesInfo.modality }}</div>
<div v-if="index === 4">MIP</div>
</div>
<!-- 描述信息 -->
<div
@@ -135,6 +137,12 @@ export default {
return {}
}
},
ctSeriesInfo: {
type: Object,
default() {
return null
}
},
renderingEngineId: {
type: String,
required: true
@@ -200,7 +208,6 @@ export default {
mounted() {
const digitPlaces = parseInt(this.$route.query.digitPlaces)
this.digitPlaces = digitPlaces === -1 ? 2 : digitPlaces
console.log(toolsUtilities)
this.subjectCode = this.$route.query.subjectCode
var element = document.getElementById(`viewport${this.index}`)
element.addEventListener(VOLUME_NEW_IMAGE, this.handleVolumeNewImage)
@@ -328,7 +335,7 @@ export default {
let properties = viewport.getProperties()
if (this.index === 3) {
// const volumeId = viewport.getVolumeId()
const volumeId = `cornerstoneStreamingImageVolume:PT_VOLUME_ID`
const volumeId = `cornerstoneStreamingImageVolume:PT_VOLUME_ID2`
properties = viewport.getProperties(volumeId)
}
if (properties && properties.voiRange) {
@@ -338,6 +345,10 @@ export default {
upper
)
this.imageInfo.wwwc = `${Math.round(windowWidth)}/${Math.round(windowCenter)}`
// PTMIP (CT)
if (this.index !== 1) {
this.$emit('upperRangeChange', Math.round(upper))
}
}
},
handleMouseMove(e) {
@@ -567,10 +578,17 @@ export default {
ctx.fillStyle = gradient
ctx.fillRect(0, 0, rectWidth, rectHeight)
},
setColorMap(presetName) {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
let volumeId = `cornerstoneStreamingImageVolume:PT_VOLUME_ID`
viewport.setProperties({ colormap: { name: presetName } }, volumeId)
viewport.render()
},
setWwWc() {
let properties = viewport.getProperties()
if (this.index === 3) {
properties = viewport.getProperties(`cornerstoneStreamingImageVolume:PT_VOLUME_ID`)
properties = viewport.getProperties(`cornerstoneStreamingImageVolume:PT_VOLUME_ID2`)
}
if (properties && properties.voiRange) {
var { lower, upper } = properties.voiRange
@@ -582,7 +600,10 @@ export default {
upper
)
this.imageInfo.wwwc = `${Math.round(windowWidth)}/${Math.round(windowCenter)}`
this.$emit('upperRangeChange', Math.round(windowWidth))
// PTMIP (CT)
if (this.index !== 1) {
this.$emit('upperRangeChange', Math.round(upper))
}
}
},
async getScreenshots() {
@@ -1,106 +0,0 @@
<template>
<div v-loading="loading" class="ecrf-wrapper">
<h4 style="color: #ddd;padding: 5px 0px;margin: 0;">
评估说明
</h4>
<el-form
ref="questions"
size="small"
:model="questionForm"
>
<el-form-item
label="访视点备注"
prop="note"
:rules="[
{ required: true,
message:$t('common:ruleMessage:specify'), trigger: ['blur', 'change']},
]"
>
<el-input
v-model="questionForm.note"
:disabled="readingTaskState >= 2"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4}"
maxlength="500"
/>
</el-form-item>
<el-form-item v-if="readingTaskState < 2">
<div style="text-align:right">
<el-button size="mini" @click="handleSave">{{ $t('common:button:save') }}</el-button>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'ECRF',
data() {
return {
loading: false,
questionForm: {
note: ''
},
readingTaskState: 1,
isBaseLineTask: false,
visitTaskId: ''
}
},
mounted() {
this.trialId = this.$route.query.trialId
this.visitTaskId = this.$route.query.visitTaskId
},
beforeDestroy() {
},
methods: {
handleSave() {
this.$refs['questions'].validate((valid) => {
if (!valid) return
})
},
}
}
</script>
<style lang="scss" scoped>
.ecrf-wrapper{
::v-deep .el-form-item__label{
color: #c3c3c3;
text-align: left;
}
::v-deep .el-input__inner{
background-color: transparent;
color: #ddd;
border: 1px solid #5e5e5e;
}
::v-deep .el-textarea__inner{
background-color: transparent;
color: #ddd;
border: 1px solid #5e5e5e;
}
::v-deep .el-form-item{
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-wrap: wrap;
}
::v-deep .el-form-item__content{
flex: 1;
}
::v-deep .el-button--mini, .el-button--mini.is-round {
padding: 7px 10px;
}
.el-form-item__content
.el-select{
width: 100%;
}
}
</style>
@@ -1,148 +0,0 @@
<template>
<div v-loading="loading" class="ecrf-wrapper">
<h4 style="color: #ddd;padding: 5px 0px;margin: 0;">
影像质量
</h4>
<el-form
ref="questions"
size="small"
:model="questionForm"
>
<el-form-item
label="影像质量"
prop="imageQuality"
:rules="[
{ required: true,
message:$t('common:ruleMessage:select'), trigger: ['blur', 'change']},
]"
>
<el-radio-group v-model="questionForm.imageQuality">
<el-radio
v-for="item of $d.ImageQualityEvaluation"
:key="item.id"
:label="item.value"
>
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="questionForm.imageQuality === 2"
label="影像质量问题"
prop="imageQualityIssues"
:rules="[
{ required: true,
message:$t('common:ruleMessage:select'), trigger: ['blur', 'change']},
]"
>
<el-select
v-model="questionForm.imageQualityIssues"
:disabled="readingTaskState >= 2"
clearable
>
<el-option
v-for="item of $d.ImageQualityIssues"
:key="item.id"
:value="item.value"
:label="item.label"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="questionForm.imageQualityIssues === 5"
label="影像质量备注"
prop="note"
:rules="[
{ required: true,
message:$t('common:ruleMessage:specify'), trigger: ['blur', 'change']},
]"
>
<el-input
v-model="questionForm.note"
:disabled="readingTaskState >= 2"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4}"
maxlength="500"
/>
</el-form-item>
<el-form-item v-if="readingTaskState < 2">
<div style="text-align:right">
<el-button size="mini" @click="handleSave">{{ $t('common:button:save') }}</el-button>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'ECRF',
data() {
return {
loading: false,
questionForm: {
imageQuality: null,
imageQualityIssues: null,
note: ''
},
readingTaskState: 1,
isBaseLineTask: false,
visitTaskId: ''
}
},
mounted() {
this.trialId = this.$route.query.trialId
},
beforeDestroy() {
},
methods: {
handleSave() {
this.$refs['questions'].validate((valid) => {
if (!valid) return
})
},
}
}
</script>
<style lang="scss" scoped>
.ecrf-wrapper{
::v-deep .el-form-item__label{
color: #c3c3c3;
text-align: left;
}
::v-deep .el-input__inner{
background-color: transparent;
color: #ddd;
border: 1px solid #5e5e5e;
}
::v-deep .el-textarea__inner{
background-color: transparent;
color: #ddd;
border: 1px solid #5e5e5e;
}
::v-deep .el-form-item{
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-wrap: wrap;
}
::v-deep .el-form-item__content{
flex: 1;
}
::v-deep .el-button--mini, .el-button--mini.is-round {
padding: 7px 10px;
}
.el-form-item__content
.el-select{
width: 100%;
}
}
</style>
@@ -1,354 +0,0 @@
<template>
<div class="measurement-wrapper">
<div class="container">
<h4 style="color: #ddd;padding: 5px 0px;margin: 0;">
核医学评估
</h4>
<div class="lesion_list">
<div class="flex-row" style="margin:3px 0;">
<div class="title">盆腔淋巴结</div>
</div>
<el-table
:data="questions"
style="width: 100%"
row-key="rowIndex"
:expand-row-keys="expands"
@expand-change="expandSelect"
size="mini"
>
<el-table-column type="expand">
<template slot-scope="props">
<el-form :ref="props.row.rowIndex" size="small" :model="props.row" label-width="80px" style="padding-right: 10px">
<el-form-item label="部位">
<el-input
v-model="props.row.part"
disabled
/>
</el-form-item>
<el-form-item
label="转移灶"
prop="distraction"
:rules="[
{ required: true,
message:$t('common:ruleMessage:select'), trigger: ['blur', 'change']},
]"
>
<el-select v-model="props.row.distraction" style="width:100%;">
<el-option label="阴性" :value="1"></el-option>
<el-option label="阳性" :value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item label="SUVmax">
<el-input
v-model="props.row.suvMax"
disabled
/>
</el-form-item>
<el-form-item >
<div style="text-align:right">
<el-button v-if="props.row.annotation" size="mini" @click="clearAnnotation(props.row)">{{ $t('trials:reading:button:removeMark') }}</el-button>
<el-button size="mini" @click="handleSave(props.row)">{{ $t('common:button:save') }}</el-button>
</div>
</el-form-item>
</el-form>
</template>
</el-table-column>
<el-table-column
label="部位"
prop="part">
</el-table-column>
<el-table-column
label="转移灶"
prop="distraction">
<template slot-scope="scope">
{{scope.row.distraction === 1 ? '阴性' : scope.row.distraction === 2 ? '阳性' : ''}}
</template>
</el-table-column>
<el-table-column
label="SUVmax"
prop="suvMax">
</el-table-column>
</el-table>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import FusionEvent from './../FusionEvent'
export default {
name: 'TableQuestionList',
components: {
},
data() {
return {
height: window.innerHeight - 140,
questions: [
{
rowIndex: '001',
part: '髂内',
distraction: '',
suvMax: null,
saveTypeEnum: 1,
annotation: null
},
{
rowIndex: '002',
part: '髂外',
distraction: '',
suvMax: null,
saveTypeEnum: 1,
annotation: null
},
{
rowIndex: '003',
part: '髂总',
distraction: '',
suvMax: null,
saveTypeEnum: 1,
annotation: null
},
{
rowIndex: '004',
part: '闭孔',
distraction: '',
suvMax: null,
saveTypeEnum: 1,
annotation: null
}
],
visitTaskId: '',
isCurrentTask: false,
loading: false,
readingTaskState: 1,
isBaseLineTask: false,
taskBlindName: '',
expands:[]
}
},
mounted() {
},
beforeDestroy() {
},
methods: {
expandSelect(row, expandedRows) {
this.expands = []
if (expandedRows.length > 0) {
row ? this.expands.push(row.rowIndex) : ''
}
if (this.expands.length > 0 && row.annotation) {
FusionEvent.$emit('imageLocation', { annotation: row.annotation })
}
},
async clearAnnotation(row) {
//
const confirm = await this.$confirm(
this.$t('trials:reading:warnning:msg47'),
{
type: 'warning',
distinguishCancelAndClose: true
}
)
if (confirm !== 'confirm') return
let i = this.questions.findIndex(i=>i.rowIndex === row.rowIndex)
FusionEvent.$emit('removeAnnotation', { annotation: this.questions[i].annotation })
this.questions[i].annotation = null
this.questions[i].suvMax = null
},
async handleSave(row) {
let loading = null
try {
let valid = await this.$refs[row.rowIndex].validate()
if (!valid) return
loading = this.$loading({ fullscreen: true })
setTimeout(() => {
//
loading.close()
this.$message.success(this.$t('common:message:savedSuccessfully'))
//
}, 2000);
} catch(e) {
if (loading) {
loading.close()
}
}
},
setActiveCollapse(annotationUID) {
for (let i = 0; i < this.questions.length; i++) {
let obj = this.questions[i]
if (obj.annotation && obj.annotation.data && obj.annotation.data.annotationUID === annotationUID) {
this.expands = [this.questions[i].rowIndex]
break
}
}
},
isCanActiveTool(toolName) {
if (this.expands.length > 0) {
let isExist = this.isExistMeasureData(this.expands[0])
return { isCanActiveTool: isExist, reason: '' }
} else {
return { isCanActiveTool: false, reason: '' }
}
},
isExistMeasureData(rowIndex) {
let i = this.questions.findIndex(i=>i.rowIndex === rowIndex)
if (this.questions[i].annotation) {
return false
} else {
return true
}
},
// modifyMeasuredData(measureObj) {
// if (measureObj.questionInfo) {
// this.activeItem.activeCollapseId = measureObj.questionInfo.QuestionId
// this.activeItem.activeRowIndex = String(measureObj.questionInfo.RowIndex)
// this.activeName = `${this.activeItem.activeCollapseId}_${this.activeItem.activeRowIndex}`
// const refName = `${this.activeItem.activeCollapseId}_${this.activeItem.activeRowIndex}`
// if (this.$refs[refName]) {
// this.$refs[refName][0].setMeasureData(measureObj.measureData)
// }
// }
// },
//
setMeasuredData(measureData) {
if (measureData.data && measureData.data.data && measureData.data.data.remark ) {
//
//
let remark = measureData.data.data.remark
let i = this.questions.findIndex(i=>i.part === remark)
this.questions[i].suvMax = measureData.suvMax
this.questions[i].annotation = measureData
this.expands = [this.questions[i].rowIndex]
} else {
if (this.expands.length > 0) {
let i = this.questions.findIndex(i=>i.rowIndex === this.expands[0])
if (measureData.data && measureData.data.data) {
measureData.data.data.remark = this.questions[i].part
}
this.questions[i].annotation = measureData
}
}
},
}
}
</script>
<style lang="scss" scoped>
.measurement-wrapper{
// .container{
// padding: 10px;
// }
.title{
padding: 5px;
font-weight: bold;
color: #ddd;
font-size: 15px;
}
.add-icon{
padding: 5px;
font-weight: bold;
color: #ddd;
font-size: 15px;
border: 1px solid #938b8b;
margin-bottom: 2px;
cursor: pointer;
}
.add-icon:hover{
background-color: #607d8b;
}
.flex-row{
display: flex;
flex-direction: row;
justify-content: space-between;
background-color: #424242;
}
.lesion_list{
position: relative;
::v-deep .el-table, .el-table__expanded-cell {
background-color: #000;
color: #fff;
border-color:#444444;
.el-form-item__label {
color: #fff;
}
.el-input__inner {
background-color: rgba(0, 0, 0, .1);
border-color: #606266;
color: #fff;
}
.el-input.is-disabled .el-input__inner {
background-color: #303133;
border-color: #444444;
color: #c0c4cc;
cursor: not-allowed;
}
}
::v-deep .el-table th, .el-table tr {
background-color: #000;
color: #fff;
border-color:#444444;
}
::v-deep .el-table__body tr > td{
background-color:#000 !important;
color: #fff;
border-color:#444444;
}
// ::v-deep .el-table__body tr:hover > td{
// background-color:#858282 !important;
// color: #fff;
// border-color:#444444;
// }
::v-deep .el-table--border th.gutter:last-of-type{
border: none;
}
::v-deep .el-card__header{
border: none;
padding: 10px;
}
}
.el-collapse{
border-bottom:none;
border-top:none;
::v-deep .el-collapse-item{
background-color: #000!important;
color: #ddd;
}
::v-deep .el-collapse-item__header{
background-color: #000!important;
color: #ddd;
border-bottom-color:#5a5a5a;
padding-left: 5px;
height: 35px;
line-height: 35px;
}
::v-deep .el-collapse-item__wrap{
background-color: #000!important;
color: #ddd;
}
::v-deep .el-collapse-item__content{
width:260px;
position: absolute;
top: 0px;
right: 0px;
// border: 1px solid #ffeb3b;
border: 1px solid #fff;
z-index: 1;
color: #ddd;
padding: 5px;
background-color:#1e1e1e;
}
}
}
</style>
@@ -1,690 +0,0 @@
<template>
<div
class="viewport_container"
:class="['item',activeIndex === index?'item_active':'']"
@mouseup="sliderMouseup"
>
<div :id="`viewport${index}`" ref="viewportCanvas" style="height: 100%;width:100%" />
<!-- 序列信息 -->
<div
v-if="seriesInfo.taskBlindName"
class="seriesInfo_wrapper"
:style="{color:index%2 == 1 ? '#ddd' : '#666'}"
>
<h2 v-if="isReadingShowSubjectInfo" class="taskInfo_container">
{{ subjectCode }} {{ seriesInfo.taskBlindName }}
</h2>
<div v-if="index === 1 || index === 2">
Series: #{{ seriesInfo.seriesNumber }}
</div>
<div v-if="index !== 4">Image: #{{ `${seriesInfo.imageIdIndex + 1} / ${seriesInfo.imageMaxLength}` }}</div>
<div v-if="index === 1 || index === 2">{{ seriesInfo.modality }}</div>
</div>
<!-- 描述信息 -->
<div
v-if="seriesInfo.description && (index === 1 || index === 2)"
class="descInfo_wrapper"
:style="{color:index%2 == 1 ? '#ddd' : '#666'}"
>
<div>{{ seriesInfo.description }}</div>
</div>
<!-- 图像信息 -->
<div
v-if="seriesInfo.description && (index === 1 || index === 2 || index === 3)"
class="imageInfo_wrapper_l"
:style="{color:index%2 == 1 ? '#ddd' : '#666'}"
>
<div v-show="mousePosition.index.length > 0">
Pos: {{ mousePosition.index[0] }}, {{ mousePosition.index[1] }}, {{ mousePosition.index[2] }}
</div>
<div v-if="(seriesInfo.modality === 'CT' || seriesInfo.modality === 'DR' || seriesInfo.modality === 'CR') && mousePosition.value">
HU: {{ mousePosition.value }}
</div>
<div v-else-if="(seriesInfo.modality === 'PT' && mousePosition.value)">
SUVbw(g/ml): {{ digitPlaces === -1 ?mousePosition.value.toFixed(3) :mousePosition.value.toFixed(digitPlaces) }}
</div>
<div v-else-if="mousePosition.value">
Density: {{ mousePosition.value }}
</div>
<div v-if="index === 1 || index === 2">
W*H: {{ imageInfo.size }}
</div>
<!-- <div v-if="index === 1 || index === 2">Zoom: {{ imageInfo.zoom }}</div> -->
</div>
<div
v-if="seriesInfo.description && (index === 1 || index === 2 || index === 3)"
class="imageInfo_wrapper_r"
:style="{color:index%2 == 1 ? '#ddd' : '#666'}"
>
<div v-show="imageInfo.location && index !== 3 ">Location: {{ `${imageInfo.location} mm` }}</div>
<div v-show="seriesInfo.sliceThickness && index !== 3">Slice Thickness: {{ `${Number(seriesInfo.sliceThickness).toFixed(2)} mm` }}</div>
<div v-show="imageInfo.wwwc ">WW/WL: {{ imageInfo.wwwc }}</div>
</div>
<div v-if="index !== 4" ref="sliderBox" class="slider_box" :style="{background: index === 2?'#ddd':'#333'}" @click.stop="goViewer($event)">
<div :style="{top: sliderBoxHeight + '%'}" class="box" @click.stop.prevent="() => {return}" @mousedown.stop="sliderMousedown($event)" />
</div>
<div style="position: absolute;left: 50%;top: 30px;color: #f44336;transform: translateX(-50%);">
{{ markers.top }}
</div>
<div style="position: absolute;top: 50%;right: 15px;color: #f44336;transform: translateY(-50%);">
{{ markers.right }}
</div>
<div style="position: absolute;left: 50%;bottom: 30px;color: #f44336;transform: translateX(-50%);">
{{ markers.bottom }}
</div>
<div style="position: absolute;top: 50%;left: 15px;color: #f44336;transform: translateY(-50%);">
{{ markers.left }}
</div>
<div v-if="presetName" class="color_bar">
<canvas id="colorBar_Canvas" />
</div>
<div v-if="index === 4" id="rotateBar" ref="rotateBar" class="rotate_slider_box" @click.stop="clickRotate($event)">
<div id="rotateSlider" :style="{left: rotateBarLeft + 'px'}" class="box" @click.stop.prevent="() => {return}" @mousedown.stop="rotateBarMousedown($event)" />
</div>
</div>
</template>
<script>
import {
getRenderingEngine,
metaData,
utilities,
// getOrCreateCanvas,
Enums } from '@cornerstonejs/core'
import {
utilities as toolsUtilities,
annotation,
Enums as toolsEnums
// cursors
} from '@cornerstonejs/tools'
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'
import { vec3, mat4 } from 'gl-matrix'
import html2canvas from 'html2canvas'
const { getColormap } = utilities.colormap
const {
VOLUME_NEW_IMAGE
// VOLUME_LOADED,
// IMAGE_VOLUME_MODIFIED,
// VOLUME_VIEWPORT_NEW_VOLUME
} = Enums.Events
var renderingEngine
var viewport
export default {
name: 'Viewport',
props: {
activeIndex: {
type: Number,
default: 0
},
index: {
// 1:ct 2:pet 3:fusion 4:mip
type: Number,
required: true
},
isReadingShowSubjectInfo: {
type: Boolean,
default: false
},
seriesInfo: {
type: Object,
default() {
return {}
}
},
renderingEngineId: {
type: String,
required: true
},
viewportId: {
type: String,
required: true
},
volume: {
type: Object,
default() {
return {}
}
},
measureDatas: {
type: Array,
default() {
return []
}
}
},
data() {
return {
subjectCode: '',
mousePosition: { index: [], value: null, modalityUnit: '', world: [], ctVal: null, ptVal: null },
digitPlaces: -1,
imageInfo: {
zoom: null,
size: null,
location: null,
sliceThickness: null,
wwwc: null
},
sliderBoxHeight: 0,
sliderInfo: {
oldB: null,
oldM: null,
isMove: false
},
rotateAngle: 0,
rotateBarLeft: 0,
rotateBarInfo: {
initX: null,
initLeft: null,
isMove: false
},
isFirstRender: true,
defaultWindowLevel: {},
presetName: '',
orientationMarkers: [],
originalMarkers: [],
markers: { top: '', right: '', bottom: '', left: '' }
}
},
watch: {
activeIndex: {
handler(v) {
console.log('activeIndex ', v)
}
}
},
mounted() {
const digitPlaces = parseInt(this.$route.query.digitPlaces)
this.digitPlaces = digitPlaces === -1 ? 2 : digitPlaces
// console.log(toolsUtilities)
this.subjectCode = this.$route.query.subjectCode
var element = document.getElementById(`viewport${this.index}`)
element.addEventListener(VOLUME_NEW_IMAGE, this.handleVolumeNewImage)
element.addEventListener(Enums.Events.CAMERA_MODIFIED, this.handleCameraModified)
element.addEventListener(Enums.Events.VOI_MODIFIED, this.handleVOIModified)
element.addEventListener(toolsEnums.Events.MOUSE_WHEEL, this.handletoolsMouseWheel)
element.addEventListener('CORNERSTONE_TOOLS_MOUSE_MOVE', this.handleMouseMove)
element.addEventListener('mouseleave', this.handleMouseLeave)
document.addEventListener('mouseup', () => {
this.sliderMouseup()
if (this.index === 4) {
this.rotateBarMouseup()
}
})
document.addEventListener('mousemove', (e) => {
this.sliderMousemove(e)
if (this.index === 4) {
this.rotateBarMousemove(e)
}
})
},
methods: {
handleVolumeNewImage(e) {
const { imageIndex } = e.detail
if (this.viewportId === e.detail.viewportId && this.index !== 4) {
this.seriesInfo.imageIdIndex = imageIndex
}
renderingEngine = getRenderingEngine(this.renderingEngineId)
viewport = renderingEngine.getViewport(this.viewportId)
if (viewport) {
var zoom = viewport.getZoom()
if (zoom) {
this.imageInfo.zoom = zoom.toFixed(4)
}
var imageId = viewport.getCurrentImageId()
if (imageId) {
const imagePlaneModule = metaData.get('imagePlaneModule', imageId)
this.imageInfo.size = `${imagePlaneModule.columns}*${imagePlaneModule.rows}`
this.imageInfo.location = imagePlaneModule.sliceLocation
this.getOrientationMarker()
this.sliderBoxHeight = imageIndex * 100 / (this.seriesInfo.imageMaxLength - 1)
}
var properties = viewport.getProperties()
if (properties && properties.voiRange) {
var { lower, upper } = properties.voiRange
const windowWidth = upper - lower
const windowCenter = (upper + lower) / 2
this.defaultWindowLevel.windowWidth = windowWidth
this.defaultWindowLevel.windowCenter = windowCenter
this.imageInfo.wwwc = `${Math.round(windowWidth)}/${Math.round(windowCenter)}`
}
if (this.presetName) {
this.renderColorBar(this.presetName)
}
}
},
getOrientationMarker() {
// eslint-disable-next-line no-unused-vars
const { viewUp, viewPlaneNormal } = viewport.getCamera()
const viewRight = vec3.create()
vec3.cross(viewRight, viewUp, viewPlaneNormal)
const columnCosines = [-viewUp[0], -viewUp[1], -viewUp[2]]
const rowCosines = viewRight
const rowString = toolsUtilities.orientation.getOrientationStringLPS(rowCosines)
const columnString = toolsUtilities.orientation.getOrientationStringLPS(columnCosines)
const oppositeRowString = toolsUtilities.orientation.invertOrientationStringLPS(rowString)
const oppositeColumnString = toolsUtilities.orientation.invertOrientationStringLPS(columnString)
this.markers.top = oppositeColumnString
this.markers.right = rowString
this.markers.bottom = columnString
this.markers.left = oppositeRowString
this.orientationMarkers = [oppositeColumnString, rowString, columnString, oppositeRowString]
if (this.originalMarkers.length === 0) {
this.originalMarkers = [...this.orientationMarkers]
}
},
rotateMarkers(type) {
var markers = [...this.orientationMarkers]
if (type === 4) {
// 90
this.orientationMarkers = markers.slice(1, 4).concat(markers[0])
} else if (type === 5) {
// 90
this.orientationMarkers = [markers[3]].concat(markers.slice(0, 3))
}
this.setMarkers()
},
resetMarks() {
this.orientationMarkers = [...this.originalMarkers]
this.setMarkers()
},
setMarkers() {
var markers = [...this.orientationMarkers]
for (const key in this.markers) {
var v = markers.shift(0)
this.markers[key] = v
}
},
handleCameraModified(e) {
renderingEngine = getRenderingEngine(this.renderingEngineId)
viewport = renderingEngine.getViewport(this.viewportId)
if (!viewport) return
var zoom = viewport.getZoom()
if (!zoom) return
this.imageInfo.zoom = zoom.toFixed(4)
// console.log(e)
},
handleVOIModified(e) {
renderingEngine = getRenderingEngine(this.renderingEngineId)
viewport = renderingEngine.getViewport(this.viewportId)
if (!viewport) return
var properties = viewport.getProperties()
if (properties && properties.voiRange) {
var { lower, upper } = properties.voiRange
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(
lower,
upper
)
this.imageInfo.wwwc = `${Math.round(windowWidth)}/${Math.round(windowCenter)}`
}
},
handleMouseMove(e) {
if (this.index !== 4) {
const { currentPoints } = e.detail
const worldPoint = currentPoints.world
const { imageData } = this.volume
const index = imageData.worldToIndex(worldPoint)
index[0] = Math.floor(index[0])
index[1] = Math.floor(index[1])
index[2] = Math.floor(index[2])
this.mousePosition.index = index
this.mousePosition.value = this.getValue(this.volume, worldPoint)
}
},
getValue(volume, worldPos) {
const { dimensions, scalarData, imageData } = volume
const index = imageData.worldToIndex(worldPos)
index[0] = Math.floor(index[0])
index[1] = Math.floor(index[1])
index[2] = Math.floor(index[2])
if (!utilities.indexWithinDimensions(index, dimensions)) {
return
}
const yMultiple = dimensions[0]
const zMultiple = dimensions[0] * dimensions[1]
const value = scalarData[index[2] * zMultiple + index[1] * yMultiple + index[0]]
return value
},
handleMouseLeave(e) {
this.mousePosition.index = []
this.mousePosition.value = null
},
goViewer(e) {
var height = e.offsetY * 100 / this.$refs['sliderBox'].clientHeight
this.sliderBoxHeight = height
var index = Math.trunc(this.seriesInfo.imageMaxLength * this.sliderBoxHeight / 100)
if (this.seriesInfo.imageIdIndex !== index) {
this.scroll(index)
}
},
sliderMousedown(e) {
var boxHeight = this.$refs['sliderBox'].clientHeight
this.sliderInfo.oldB = parseInt(e.srcElement.style.top) * boxHeight / 100
this.sliderInfo.oldM = e.clientY
this.sliderInfo.isMove = true
e.stopImmediatePropagation()
e.stopPropagation()
e.preventDefault()
},
sliderMousemove(e) {
if (!this.sliderInfo.isMove) return
var PX = this.sliderInfo.oldB - (this.sliderInfo.oldM - e.clientY)
var boxHeight = this.$refs['sliderBox'].clientHeight
//
if (PX < 0) return
if (PX > boxHeight) return
const height = PX * 100 / boxHeight
var index = Math.trunc(this.seriesInfo.imageMaxLength * height / 100)
index = index > this.seriesInfo.imageMaxLength ? this.seriesInfo.imageMaxLength : index < 0 ? 0 : index
this.sliderBoxHeight = height
if (this.seriesInfo.imageIdIndex !== index) {
this.scroll(index)
}
},
handletoolsMouseWheel(e) {
const { viewportId, wheel } = e.detail
if (viewportId === 'PET_MIP_CORONAL') {
const container = document.getElementById('rotateBar')
const slider = document.getElementById('rotateSlider')
const containerWidth = container.offsetWidth
const sliderWidth = slider.offsetWidth
const maxX = containerWidth - sliderWidth
const { direction } = wheel
var x = Math.trunc(30 * ((containerWidth - sliderWidth) / 360))
if (direction > 0 && (this.rotateBarLeft + x) > maxX) {
this.rotateBarLeft = x - (containerWidth - sliderWidth - this.rotateBarLeft)
} else if (direction < 0 && (this.rotateBarLeft < x)) {
this.rotateBarLeft = containerWidth - (x - this.rotateBarLeft + sliderWidth)
} else {
this.rotateBarLeft = x * direction + this.rotateBarLeft
}
}
},
rotateBarMousemove(e) {
//
if (!this.rotateBarInfo.isMove) return
const container = document.getElementById('rotateBar')
const slider = document.getElementById('rotateSlider')
const containerWidth = container.offsetWidth
const sliderWidth = slider.offsetWidth
let x = Math.trunc(e.clientX - this.rotateBarInfo.initX + this.rotateBarInfo.initLeft)
if (x < 0) x = 0
if (x > containerWidth - sliderWidth) x = containerWidth - sliderWidth
const deltaX = x - this.rotateBarLeft
const angle = Math.sin((deltaX * (360 / (containerWidth - sliderWidth))) * Math.PI / 180)
this.rotate(angle)
this.rotateBarLeft = x
},
rotateBarMousedown(e) {
console.log('rotateBarMousedown')
this.rotateBarInfo.initLeft = e.srcElement.offsetLeft
this.rotateBarInfo.initX = e.clientX
this.rotateBarInfo.isMove = true
e.stopImmediatePropagation()
e.stopPropagation()
e.preventDefault()
},
rotate(angle) {
renderingEngine = getRenderingEngine(this.renderingEngineId)
viewport = renderingEngine.getViewport(this.viewportId)
const camera = viewport.getCamera()
const { viewUp, position, focalPoint } = camera
const [cx, cy, cz] = focalPoint
const [ax, ay, az] = [0, 0, 1]
const newPosition = [0, 0, 0]
const newFocalPoint = [0, 0, 0]
const newViewUp = [0, 0, 0]
const transform = mat4.identity(new Float32Array(16))
mat4.translate(transform, transform, [cx, cy, cz])
mat4.rotate(transform, transform, angle, [ax, ay, az])
mat4.translate(transform, transform, [-cx, -cy, -cz])
vec3.transformMat4(newPosition, position, transform)
vec3.transformMat4(newFocalPoint, focalPoint, transform)
mat4.identity(transform)
mat4.rotate(transform, transform, angle, [ax, ay, az])
vec3.transformMat4(newViewUp, viewUp, transform)
viewport.setCamera({
position: newPosition,
viewUp: newViewUp,
focalPoint: newFocalPoint
})
viewport.render()
},
clickRotate(e) {
// console.log('clickRotate')
const container = document.getElementById('rotateBar')
const containerWidth = container.offsetWidth
const slider = document.getElementById('rotateSlider')
const sliderWidth = slider.offsetWidth
const x = Math.trunc(e.offsetX)
const deltaX = x - this.rotateBarLeft
const angle = Math.sin((deltaX * (360 / (containerWidth - sliderWidth))) * Math.PI / 180)
this.rotate(angle)
this.rotateBarLeft = x
},
async scroll(index) {
renderingEngine = getRenderingEngine(this.renderingEngineId)
viewport = renderingEngine.getViewport(this.viewportId)
const actorEntries = viewport.getActors()
if (!actorEntries) {
return
}
// const delta = index - this.seriesInfo.imageIdIndex
// toolsUtilities.scroll(viewport, {
// delta,
// volumeId: actorEntries.uid
// })
await utilities.jumpToSlice(viewport.element, {
imageIndex: index,
});
renderingEngine.render()
},
rotateBarMouseup(e) {
this.rotateBarInfo.isMove = false
},
sliderMouseup(e) {
this.sliderInfo.isMove = false
},
setAnnotation(imageId, element) {
this.measureDatas.forEach(item => {
if (item.OtherMeasureData) {
var { metadata, annotationUID } = item.OtherMeasureData
var { referencedImageId } = metadata
console.log(annotationUID, annotation.state.getAnnotation(annotationUID))
if (!annotation.state.getAnnotation(annotationUID) && referencedImageId === imageId) {
annotation.state.addAnnotation(item.OtherMeasureData, element)
}
}
})
},
setPreset(presetName) {
this.presetName = presetName
},
renderColorBar(presetName) {
var colorMap = null
if (presetName === 'hsv') {
colorMap = vtkColorMaps.getPresetByName(presetName)
} else {
colorMap = getColormap(presetName)
}
const rgbPoints = colorMap.RGBPoints
const canvas = document.getElementById('colorBar_Canvas')
const ctx = canvas.getContext('2d')
const canvasWidth = 160
const canvasHeight = 5
const rectWidth = 160
const rectHeight = canvasHeight
canvas.width = canvasWidth
canvas.height = canvasHeight
const gradient = ctx.createLinearGradient(0, 0, rectWidth, 0)
for (let i = 0; i < rgbPoints.length; i += 4) {
let position = 0
if (rgbPoints[0] === -1) {
position = (rgbPoints[i] + 1) / 2
} else {
position = rgbPoints[i]
}
const color = `rgb(${parseInt(rgbPoints[i + 1] * 255)}, ${parseInt(rgbPoints[i + 2] * 255)}, ${parseInt(rgbPoints[i + 3] * 255)})`
gradient.addColorStop(position, color)
}
ctx.fillStyle = gradient
ctx.fillRect(0, 0, rectWidth, rectHeight)
},
setWwWc() {
var properties = viewport.getProperties()
if (properties && properties.voiRange) {
var { lower, upper } = properties.voiRange
const windowWidth = upper - lower
const windowCenter = (upper + lower) / 2
this.imageInfo.wwwc = `${Math.round(windowWidth)}/${Math.round(windowCenter)}`
}
},
async getScreenshots() {
const divForDownloadViewport = document.querySelector(
`div[data-viewport-uid="FUSION_AXIAL"]`
)
// const divForDownloadViewport = document.querySelector(
// '.viewport_container'
// )
var canvas = await html2canvas(divForDownloadViewport)
var pictureBaseStr = canvas.toDataURL('image/png', 1)
return pictureBaseStr
}
}
}
</script>
<style lang="scss" scoped>
.viewport_container{
width:100%;
height: 100%;
.seriesInfo_wrapper {
position: absolute;
left: 10px;
top: 10px;
text-align: left;
font-size: 12px;
z-index: 1;
.taskInfo_container{
color:#f44336;
padding: 5px 0px;
margin: 0;
}
}
.descInfo_wrapper{
position: absolute;
right: 20px;
top: 10px;
text-align: right;
font-size: 12px;
z-index: 1;
}
.imageInfo_wrapper_l{
position: absolute;
left: 10px;
bottom: 5px;
text-align: left;
font-size: 12px;
z-index: 1;
}
.imageInfo_wrapper_r{
position: absolute;
right: 40px;
bottom: 5px;
text-align: right;
font-size: 12px;
z-index: 1;
}
.slider_box{
position: absolute;
right: 1px;
height: calc(100% - 120px);
transform: translateY(-50%);
top: calc(50% - 30px);
width: 10px;
background: #333;
cursor: pointer;
.box{
z-index:10;
background: #9e9e9e;
height: 20px;
width: 100%;
position: absolute;
top: 0;
cursor: move
}
}
.color_bar{
position: absolute;
// transform:translateY(-50%);
transform: rotate(-90deg);
transform-origin: right;
left: -150px;
top: 30%;
// transform-origin: top left;
z-index: 1;
// background: #f44336;
}
.rotate_slider_box{
position: absolute;
width: 380px;
height: 10px;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
background: #333;
cursor: pointer;
.box{
z-index:10;
background: #9e9e9e;
height: 100%;
width: 20px;
position: absolute;
top: 0;
cursor: move
}
}
// .my_slider_box:after{
// content: '';
// position: absolute;
// bottom: -20px;
// left: 0;
// height: 20px;
// width: 100%;
// background: #333;
// }
}
</style>
File diff suppressed because it is too large Load Diff
@@ -1,10 +1,31 @@
import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader'
import cornerstoneDICOMImageLoader from "@cornerstonejs/dicom-image-loader";
import {
getImageTypeSubItemFromDataset,
extractOrientationFromDataset,
extractPositionFromDataset,
extractSpacingFromDataset,
extractSliceThicknessFromDataset,
} from "@/utils/extractPositioningFromDataset";
import getNumberValues from "@/utils/getNumberValues";
import isNMReconstructable from "@/utils/isNMReconstructable";
function getSpacingBetweenSlices(dataSet) {
if (dataSet?.elements?.x00180088) {
return dataSet.floatString("x00180088");
}
const pixelMeasuresSequence = dataSet?.elements?.x00289110;
if (
pixelMeasuresSequence?.items?.length &&
pixelMeasuresSequence.items[0]?.dataSet?.elements?.x00180088
) {
return pixelMeasuresSequence.items[0].dataSet.floatString("x00180088");
}
}
function parseImageId(imageId) {
// build a url by parsing out the url scheme and frame index from the imageId
const firstColonIndex = imageId.indexOf(':');
const firstColonIndex = imageId.indexOf(":");
let url = imageId.substring(firstColonIndex + 1);
const frameIndex = url.indexOf('frame=');
const frameIndex = url.indexOf("frame=");
let frame;
@@ -21,127 +42,112 @@ function parseImageId(imageId) {
frame,
};
}
function getNumberValues(dataSet, tag, minimumLength) {
const values = [];
const valueAsString = dataSet.string(tag);
if (!valueAsString) {
function metaDataProvider(type, imageId) {
const parsedImageId = parseImageId(imageId);
const dataSet = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.get(
parsedImageId.url
);
if (!dataSet) {
return;
}
const split = valueAsString.split('\\');
if (type === "imagePlaneModule") {
const frameIndex = parsedImageId.frame !== undefined ? parsedImageId.frame - 1 : undefined;
// const imageOrientationPatient = extractOrientationFromDataset(dataSet, frameIndex);
// let imagePositionPatient = extractPositionFromDataset(dataSet, frameIndex);
// const pixelSpacing = extractSpacingFromDataset(dataSet, frameIndex);
// const sliceThickness = extractSliceThicknessFromDataset(dataSet, frameIndex);
const imageOrientationPatient = extractOrientationFromDataset(dataSet);
let imagePositionPatient = extractPositionFromDataset(dataSet);
const pixelSpacing = extractSpacingFromDataset(dataSet);
const sliceThickness = extractSliceThicknessFromDataset(dataSet);
const modality = dataSet.string("x00080060");
if (
modality &&
modality.includes("NM") &&
parsedImageId.frame !== undefined &&
parsedImageId.frame > 1
) {
// 坐标堆叠 (位置相同) :NM 多帧 DICOM 的空间位置 ( imagePositionPatient ) 通常只存在于 Detector Information Sequence 序列的第一个 Item 中。原来的代码在提取位置时,所有帧都被赋予了第一帧的位置。因为所有帧都挤在一个坐标上,3D 引擎无法根据厚度和法向量空间展开它们,导致间距 (Spacing) 为 0
// 当识别到 NM 多帧图像且属于多帧切片时,通过 RowCosines 与 ColumnCosines 的叉乘计算出切片的 法向量(Normal) ,并结合 帧索引 (frameIndex) 以及 Spacing Between Slices (0018,0088)(层间距) 或者 Slice Thickness (0018,0050)(层厚) ,自动推算补全后续每一帧的 imagePositionPatient 真实三维坐标
//
if (
imageOrientationPatient &&
imagePositionPatient &&
sliceThickness !== undefined
) {
const spacingBetweenSlices = getSpacingBetweenSlices(dataSet);
const step =
spacingBetweenSlices !== undefined ? spacingBetweenSlices : sliceThickness;
const rowCosines = [
parseFloat(imageOrientationPatient[0]),
parseFloat(imageOrientationPatient[1]),
parseFloat(imageOrientationPatient[2]),
];
const columnCosines = [
parseFloat(imageOrientationPatient[3]),
parseFloat(imageOrientationPatient[4]),
parseFloat(imageOrientationPatient[5]),
];
const normal = [
rowCosines[1] * columnCosines[2] - rowCosines[2] * columnCosines[1],
rowCosines[2] * columnCosines[0] - rowCosines[0] * columnCosines[2],
rowCosines[0] * columnCosines[1] - rowCosines[1] * columnCosines[0],
];
if (minimumLength && split.length < minimumLength) {
return;
}
for (let i = 0; i < split.length; i++) {
values.push(parseFloat(split[i]));
}
const offset = frameIndex * parseFloat(step);
return values;
}
function getImageTypeSubItemFromDataset(dataSet, index) {
const imageType = dataSet.string('x00080008');
if (imageType) {
const subTypes = imageType.split('\\');
if (subTypes.length > index) {
return subTypes[index];
imagePositionPatient = [
parseFloat(imagePositionPatient[0]) + normal[0] * offset,
parseFloat(imagePositionPatient[1]) + normal[1] * offset,
parseFloat(imagePositionPatient[2]) + normal[2] * offset,
];
}
}
return undefined;
}
function isNMReconstructable(imageSubType) {
return imageSubType === 'RECON TOMO' || imageSubType === 'RECON GATED TOMO';
}
function extractOrientationFromNMMultiframeDataset(dataSet) {
let imageOrientationPatient;
const modality = dataSet.string('x00080060');
if (modality?.includes('NM')) {
const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2);
if (imageSubType && isNMReconstructable(imageSubType)) {
if (dataSet.elements.x00540022) {
imageOrientationPatient = getNumberValues(dataSet.elements.x00540022.items[0].dataSet, 'x00200037', 6);
}
}
}
return imageOrientationPatient;
}
function extractOrientationFromDataset(dataSet) {
let imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6);
if (!imageOrientationPatient && dataSet.elements.x00209116) {
imageOrientationPatient = getNumberValues(dataSet.elements.x00209116.items[0].dataSet, 'x00200037', 6);
}
if (!imageOrientationPatient) {
imageOrientationPatient =
extractOrientationFromNMMultiframeDataset(dataSet);
}
return imageOrientationPatient;
}
function extractPositionFromNMMultiframeDataset(dataSet) {
let imagePositionPatient;
const modality = dataSet.string('x00080060');
if (modality?.includes('NM')) {
const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2);
if (imageSubType && isNMReconstructable(imageSubType)) {
if (dataSet.elements.x00540022) {
imagePositionPatient = getNumberValues(dataSet.elements.x00540022.items[0].dataSet, 'x00200032', 3);
}
}
}
return imagePositionPatient;
}
function extractPositionFromDataset(dataSet) {
let imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3);
if (!imagePositionPatient && dataSet.elements.x00209113) {
imagePositionPatient = getNumberValues(dataSet.elements.x00209113.items[0].dataSet, 'x00200032', 3);
}
if (!imagePositionPatient) {
imagePositionPatient = extractPositionFromNMMultiframeDataset(dataSet);
}
return imagePositionPatient;
}
function extractSliceThicknessFromDataset(dataSet) {
let sliceThickness;
if (dataSet.elements.x00180050) {
sliceThickness = dataSet.floatString('x00180050');
}
else if (dataSet.elements.x00289110 &&
dataSet.elements.x00289110.items.length &&
dataSet.elements.x00289110.items[0].dataSet.elements.x00180050) {
sliceThickness =
dataSet.elements.x00289110.items[0].dataSet.floatString('x00180050');
}
return sliceThickness;
}
function extractSpacingFromDataset(dataSet) {
let rowPixelSpacing = null
let columnPixelSpacing = null
let pixelSpacing = getNumberValues(dataSet, 'x00280030', 2);
const imagePixelSpacing = getNumberValues(dataSet, 'x00181164', 2);
const estimatedRadiographicMagnificationFactor = getNumberValues(dataSet, 'x00181114', 2);
if (pixelSpacing) {
rowPixelSpacing = pixelSpacing[0]
columnPixelSpacing = pixelSpacing[1]
}
// const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6);
// const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3);
// const pixelSpacing = extractSpacingFromDataset(dataSet);
const imagePixelSpacing = getNumberValues(dataSet, "x00181164", 2);
// let rowCosines = null;
// let columnCosines = null;
// if (imageOrientationPatient) {
// rowCosines = [
// parseFloat(imageOrientationPatient[0]),
// parseFloat(imageOrientationPatient[1]),
// parseFloat(imageOrientationPatient[2]),
// ];
// columnCosines = [
// parseFloat(imageOrientationPatient[3]),
// parseFloat(imageOrientationPatient[4]),
// parseFloat(imageOrientationPatient[5]),
// ];
// }
const estimatedRadiographicMagnificationFactor = getNumberValues(
dataSet,
"x00181114",
2
);
let columnPixelSpacing = null;
let rowPixelSpacing = null;
if (pixelSpacing) {
rowPixelSpacing = pixelSpacing[0];
columnPixelSpacing = pixelSpacing[1];
} else if (imagePixelSpacing && estimatedRadiographicMagnificationFactor) {
rowPixelSpacing = imagePixelSpacing[0] / estimatedRadiographicMagnificationFactor[0];
columnPixelSpacing = imagePixelSpacing[1] / estimatedRadiographicMagnificationFactor[1];
rowPixelSpacing =
imagePixelSpacing[0] / estimatedRadiographicMagnificationFactor[0];
columnPixelSpacing =
imagePixelSpacing[1] / estimatedRadiographicMagnificationFactor[1];
} else if (imagePixelSpacing && !estimatedRadiographicMagnificationFactor) {
rowPixelSpacing = imagePixelSpacing[0];
columnPixelSpacing = imagePixelSpacing[1];
}
return rowPixelSpacing !== null ? [rowPixelSpacing, columnPixelSpacing] : undefined
}
function metaDataProvider(type, imageId) {
const parsedImageId = parseImageId(imageId);
const dataSet = cornerstoneDICOMImageLoader.wadouri.dataSetCacheManager.get(parsedImageId.url)
if (!dataSet) {
return;
}
if (type === 'imagePlaneModule') {
const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6);
const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3);
const pixelSpacing = extractSpacingFromDataset(dataSet);
let rowCosines = null;
@@ -160,34 +166,35 @@ function metaDataProvider(type, imageId) {
];
}
return {
frameOfReferenceUID: dataSet.string('x00200052'),
rows: dataSet.uint16('x00280010'),
columns: dataSet.uint16('x00280011'),
frameOfReferenceUID: dataSet.string("x00200052"),
rows: dataSet.uint16("x00280010"),
columns: dataSet.uint16("x00280011"),
imageOrientationPatient,
rowCosines,
columnCosines,
imagePositionPatient,
sliceThickness: dataSet.floatString('x00180050'),
sliceLocation: dataSet.floatString('x00201041'),
pixelSpacing: pixelSpacing,
rowPixelSpacing: pixelSpacing ? pixelSpacing[0] : null,
columnPixelSpacing: pixelSpacing ? pixelSpacing[1] : null,
sliceThickness,
sliceLocation: dataSet.floatString("x00201041"),
pixelSpacing,
rowPixelSpacing,
columnPixelSpacing,
};
}
if (type === 'nmMultiframeGeometryModule') {
const modality = dataSet.string('x00080060');
const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2);
return {
modality,
imageType: dataSet.string('x00080008'),
imageSubType,
imageOrientationPatient: extractOrientationFromDataset(dataSet),
imagePositionPatient: extractPositionFromDataset(dataSet),
sliceThickness: extractSliceThicknessFromDataset(dataSet),
pixelSpacing: extractSpacingFromDataset(dataSet),
numberOfFrames: dataSet.uint16('x00280008'),
isNMReconstructable: isNMReconstructable(imageSubType) && modality.includes('NM'),
};
if (type === "nmMultiframeGeometryModule") {
const modality = dataSet.string("x00080060");
const imageSubType = getImageTypeSubItemFromDataset(dataSet, 2);
return {
modality,
imageType: dataSet.string("x00080008"),
imageSubType,
imageOrientationPatient: extractOrientationFromDataset(dataSet),
imagePositionPatient: extractPositionFromDataset(dataSet),
sliceThickness: extractSliceThicknessFromDataset(dataSet),
pixelSpacing: extractSpacingFromDataset(dataSet),
numberOfFrames: dataSet.uint16("x00280008"),
isNMReconstructable:
isNMReconstructable(imageSubType) && modality.includes("NM"),
};
}
}
export default metaDataProvider;
export default metaDataProvider;
@@ -1319,12 +1319,12 @@ export default {
.merge-table {
padding: 0 10px;
::v-deep.el-table {
::v-deep .el-table {
background-color: #1e1e1e !important;
color: #383838;
}
::v-deep.el-table td.el-table__cell,
::v-deep .el-table td.el-table__cell,
.el-table th.el-table__cell.is-leaf {
border-bottom: 1px solid #383838;
}
@@ -1335,7 +1335,7 @@ export default {
background-color: #1e1e1e;
}
::v-deep.el-table__header-wrapper {
::v-deep .el-table__header-wrapper {
th {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -1343,7 +1343,7 @@ export default {
}
}
::v-deep.el-table__body-wrapper {
::v-deep .el-table__body-wrapper {
tr {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -1354,7 +1354,7 @@ export default {
}
}
::v-deep.el-table__empty-block {
::v-deep .el-table__empty-block {
background-color: #1e1e1e !important;
}
@@ -989,21 +989,21 @@ export default {
padding: 5px 0;
}
::v-deep.el-table__fixed-right-patch {
::v-deep .el-table__fixed-right-patch {
background-color: #000 !important;
border-color: #444444;
}
::v-deep.el-table__fixed-body-wrapper tr:hover>td {
::v-deep .el-table__fixed-body-wrapper tr:hover>td {
background-color: #000 !important;
}
::v-deep.el-table--scrollable-x .el-table__body-wrapper {
::v-deep .el-table--scrollable-x .el-table__body-wrapper {
z-index: 2;
}
}
::v-deep.el-tag.el-tag--info {
::v-deep .el-tag.el-tag--info {
color: #000;
}
}
@@ -1436,12 +1436,12 @@ export default {
.merge-table {
padding: 0 10px;
::v-deep.el-table {
::v-deep .el-table {
background-color: #1e1e1e !important;
color: #383838;
}
::v-deep.el-table td.el-table__cell,
::v-deep .el-table td.el-table__cell,
.el-table th.el-table__cell.is-leaf {
border-bottom: 1px solid #383838;
}
@@ -1452,7 +1452,7 @@ export default {
background-color: #1e1e1e;
}
::v-deep.el-table__header-wrapper {
::v-deep .el-table__header-wrapper {
th {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -1460,7 +1460,7 @@ export default {
}
}
::v-deep.el-table__body-wrapper {
::v-deep .el-table__body-wrapper {
tr {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -1471,7 +1471,7 @@ export default {
}
}
::v-deep.el-table__empty-block {
::v-deep .el-table__empty-block {
background-color: #1e1e1e !important;
}
@@ -1436,12 +1436,12 @@ export default {
.merge-table {
padding: 0 10px;
::v-deep.el-table {
::v-deep .el-table {
background-color: #1e1e1e !important;
color: #383838;
}
::v-deep.el-table td.el-table__cell,
::v-deep .el-table td.el-table__cell,
.el-table th.el-table__cell.is-leaf {
border-bottom: 1px solid #383838;
}
@@ -1452,7 +1452,7 @@ export default {
background-color: #1e1e1e;
}
::v-deep.el-table__header-wrapper {
::v-deep .el-table__header-wrapper {
th {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -1460,7 +1460,7 @@ export default {
}
}
::v-deep.el-table__body-wrapper {
::v-deep .el-table__body-wrapper {
tr {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -1471,7 +1471,7 @@ export default {
}
}
::v-deep.el-table__empty-block {
::v-deep .el-table__empty-block {
background-color: #1e1e1e !important;
}
@@ -687,11 +687,27 @@ export default {
const m1 = this.qsForm[this.m1Id]
const m2 = this.qsForm[this.m2Id]
const m3 = this.qsForm[this.m3Id]
if (isNaN(parseFloat(m1)) || isNaN(parseFloat(m2)) || isNaN(parseFloat(m3))) {
this.$set(this.qsForm, this.avgId, null)
} else {
const avg = (parseFloat(m1) + parseFloat(m2) + parseFloat(m3)) / 3
// if (isNaN(parseFloat(m1)) || isNaN(parseFloat(m2)) || isNaN(parseFloat(m3))) {
// this.$set(this.qsForm, this.avgId, null)
// } else {
// const avg = (parseFloat(m1) + parseFloat(m2) + parseFloat(m3)) / 3
// this.$set(this.qsForm, this.avgId, this.numberToFixed(avg))
// }
//
const validNumbers = []
if (!isNaN(parseFloat(m1))) validNumbers.push(parseFloat(m1))
if (!isNaN(parseFloat(m2))) validNumbers.push(parseFloat(m2))
if (!isNaN(parseFloat(m3))) validNumbers.push(parseFloat(m3))
//
if (validNumbers.length > 0) {
// /
const sum = validNumbers.reduce((a, b) => a + b, 0)
const avg = sum / validNumbers.length
this.$set(this.qsForm, this.avgId, this.numberToFixed(avg))
} else {
//
this.$set(this.qsForm, this.avgId, null)
}
}
},
@@ -999,20 +1015,20 @@ export default {
padding: 5px 0;
}
::v-deep.el-table__fixed-right-patch {
::v-deep .el-table__fixed-right-patch {
background-color: #000 !important;
border-color: #444444;
}
::v-deep.el-table__fixed-body-wrapper tr:hover>td {
::v-deep .el-table__fixed-body-wrapper tr:hover>td {
background-color: #000 !important;
}
::v-deep.el-table--scrollable-x .el-table__body-wrapper {
::v-deep .el-table--scrollable-x .el-table__body-wrapper {
z-index: 2;
}
}
::v-deep.el-tag.el-tag--info {
::v-deep .el-tag.el-tag--info {
color: #000;
}
}
@@ -614,7 +614,7 @@ export default {
}
::v-deep.el-tabs {
::v-deep .el-tabs {
box-sizing: border-box;
padding: 0 5px;
height: 100%;
@@ -665,7 +665,7 @@ export default {
border: 1px solid #607d8b !important;
}
::v-deep.el-progress__text {
::v-deep .el-progress__text {
color: #ccc;
font-size: 12px;
}
@@ -1203,12 +1203,12 @@ export default {
.merge-table {
padding: 0 10px;
::v-deep.el-table {
::v-deep .el-table {
background-color: #1e1e1e !important;
color: #383838;
}
::v-deep.el-table td.el-table__cell,
::v-deep .el-table td.el-table__cell,
.el-table th.el-table__cell.is-leaf {
border-bottom: 1px solid #383838;
}
@@ -1219,7 +1219,7 @@ export default {
background-color: #1e1e1e;
}
::v-deep.el-table__header-wrapper {
::v-deep .el-table__header-wrapper {
th {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -1227,7 +1227,7 @@ export default {
}
}
::v-deep.el-table__body-wrapper {
::v-deep .el-table__body-wrapper {
tr {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -1238,7 +1238,7 @@ export default {
}
}
::v-deep.el-table__empty-block {
::v-deep .el-table__empty-block {
background-color: #1e1e1e !important;
}
@@ -1130,12 +1130,12 @@ export default {
.merge-table {
padding: 0 10px;
::v-deep.el-table {
::v-deep .el-table {
background-color: #1e1e1e !important;
color: #383838;
}
::v-deep.el-table td.el-table__cell,
::v-deep .el-table td.el-table__cell,
.el-table th.el-table__cell.is-leaf {
border-bottom: 1px solid #383838;
}
@@ -1146,7 +1146,7 @@ export default {
background-color: #1e1e1e;
}
::v-deep.el-table__header-wrapper {
::v-deep .el-table__header-wrapper {
th {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -1154,7 +1154,7 @@ export default {
}
}
::v-deep.el-table__body-wrapper {
::v-deep .el-table__body-wrapper {
tr {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -1165,7 +1165,7 @@ export default {
}
}
::v-deep.el-table__empty-block {
::v-deep .el-table__empty-block {
background-color: #1e1e1e !important;
}
@@ -952,7 +952,7 @@ export default {
background-color: #607d8b!important;
border: 1px solid #607d8b!important;
}
::v-deep.el-progress__text{
::v-deep .el-progress__text{
color: #ccc;
font-size: 12px;
}
@@ -1022,7 +1022,7 @@ export default {
}
}
::v-deep.el-collapse{
::v-deep .el-collapse{
border: none;
.el-collapse-item{
background-color: #000!important;
@@ -1043,7 +1043,7 @@ export default {
}
}
.sr-wrapper{
::v-deep.el-dialog{
::v-deep .el-dialog{
background: #fff !important;
border: 1px solid #ddd;
// color: #ddd;
@@ -1051,12 +1051,12 @@ export default {
color:#fff;
}
}
::v-deep.sr-dialog-container{
::v-deep .sr-dialog-container{
margin-top: 50px !important;
width:75%;
height:80%;
}
::v-deep.el-dialog__body{
::v-deep .el-dialog__body{
padding: 10px;
height: calc(100% - 50px);
}
@@ -1064,7 +1064,7 @@ export default {
position: relative;
}
.sr-full-dialog-container{
::v-deep.is-fullscreen .el-dialog__body{
::v-deep .is-fullscreen .el-dialog__body{
height: calc(100% - 50px);
}
}
@@ -887,7 +887,7 @@ export default {
background-color: #607d8b!important;
border: 1px solid #607d8b!important;
}
::v-deep.el-progress__text{
::v-deep .el-progress__text{
color: #ccc;
font-size: 12px;
}
@@ -956,7 +956,7 @@ export default {
}
}
}
::v-deep.el-collapse{
::v-deep .el-collapse{
border: none;
.el-collapse-item{
background-color: #000!important;
@@ -977,7 +977,7 @@ export default {
}
}
.sr-wrapper{
::v-deep.el-dialog{
::v-deep .el-dialog{
background: #fff !important;
border: 1px solid #ddd;
// color: #ddd;
@@ -985,12 +985,12 @@ export default {
color:#fff;
}
}
::v-deep.sr-dialog-container{
::v-deep .sr-dialog-container{
margin-top: 50px !important;
width:75%;
height:80%;
}
::v-deep.el-dialog__body{
::v-deep .el-dialog__body{
padding: 10px;
height: calc(100% - 50px);
}
@@ -998,7 +998,7 @@ export default {
position: relative;
}
.sr-full-dialog-container{
::v-deep.is-fullscreen .el-dialog__body{
::v-deep .is-fullscreen .el-dialog__body{
height: calc(100% - 50px);
}
}
@@ -529,7 +529,7 @@ export default {
}
</script>
<style lang="scss" scoped>
::v-deep.el-message-box__headerbtn {
::v-deep .el-message-box__headerbtn {
display: none;
}
@@ -541,7 +541,7 @@ export default {
box-sizing: border-box;
background-color: #000;
::v-deep.el-tabs {
::v-deep .el-tabs {
box-sizing: border-box;
height: 100%;
display: flex;
@@ -573,11 +573,11 @@ export default {
}
::v-deep.hot-keys-label {
::v-deep .hot-keys-label {
color: #dfdfdf !important;
}
::v-deep.shortcut-key-input span {
::v-deep .shortcut-key-input span {
color: #dfdfdf !important;
}
@@ -588,7 +588,7 @@ export default {
// }
// }
::v-deep.el-dialog {
::v-deep .el-dialog {
background: #1e1e1e;
border: 1px solid #ddd;
color: #ddd;
@@ -614,13 +614,13 @@ export default {
}
::v-deep.dialog-container {
::v-deep .dialog-container {
margin-top: 50px !important;
width: 75%;
height: 80%;
}
::v-deep.el-dialog__body {
::v-deep .el-dialog__body {
padding: 20px;
height: calc(100% - 70px);
}
@@ -636,7 +636,7 @@ export default {
}
.full-dialog-container {
::v-deep.is-fullscreen .el-dialog__body {
::v-deep .is-fullscreen .el-dialog__body {
height: calc(100% - 70px);
}
}
@@ -142,7 +142,7 @@ export default {
getStudyList(obj) {
if (obj) {
var studyList = obj.StudyList || []
studyList = studyList.filter(i => !i.IsCriticalSequence && (i.Modalities.indexOf('CT') !== -1 || i.Modalities.indexOf('MR') !== -1) && i.Modalities.indexOf('PT') !== -1)
studyList = studyList.filter(i => !i.IsCriticalSequence && (i.Modalities.indexOf('CT') !== -1 || i.Modalities.indexOf('MR') !== -1) && (i.Modalities.indexOf('PT') !== -1 || i.Modalities.indexOf('NM') !== -1))
if (studyList.length === 0) return
this.studyList = studyList
}
@@ -200,7 +200,7 @@ export default {
var series = seriesList.filter(series => series.Modality === 'CT' || series.Modality === 'MR')
this.ctSeries = series.sort((a, b) => b.instanceCount - a.instanceCount)
series = seriesList.filter(series => series.Modality === 'PT')
series = seriesList.filter(series => series.Modality === 'PT' || series.Modality === 'NM')
this.petSeries = series.sort((a, b) => b.instanceCount - a.instanceCount)
}
},
@@ -235,12 +235,12 @@ export default {
</script>
<style lang="scss" scoped>
.series-table {
::v-deep.el-table {
::v-deep .el-table {
background-color: #1e1e1e !important;
color: #dfdfdf;
}
::v-deep.el-table td.el-table__cell,
::v-deep .el-table td.el-table__cell,
.el-table th.el-table__cell.is-leaf {
border-bottom: 1px solid #dfdfdf;
}
@@ -251,14 +251,14 @@ export default {
background-color: #1e1e1e;
}
::v-deep.el-table__header-wrapper {
::v-deep .el-table__header-wrapper {
th {
background-color: #1e1e1e !important;
color: #dfdfdf;
}
}
::v-deep.el-table__body-wrapper {
::v-deep .el-table__body-wrapper {
tr {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -269,7 +269,7 @@ export default {
}
}
::v-deep.el-table__empty-block {
::v-deep .el-table__empty-block {
background-color: #1e1e1e !important;
}
}
@@ -0,0 +1,975 @@
<template>
<div ref="viewport-volume" class="viewport-wrapper" v-loading="loading" :element-loading-text="NSTip"
element-loading-background="rgba(0, 0, 0, 0.8)" @mouseup="sliderMouseup" @mousemove="sliderMousemove"
@mouseleave="sliderMouseleave" :style="{ color: '#ddd' }">
<div v-if="series && taskInfo" class="left-top-text">
<div v-if="taskInfo.IsExistsClinicalData" class="cd-info" :title="$t('trials:reading:button:clinicalData')">
<svg-icon style="cursor: pointer;" icon-class="documentation" class="svg-icon"
@click.stop="viewCD(series.TaskInfo.VisitTaskId)" />
</div>
<h2 v-if="taskInfo.IsReadingShowSubjectInfo && series.TaskInfo" class="subject-info">
{{ `${series.TaskInfo.SubjectCode} ${series.TaskInfo.TaskBlindName} ` }}
</h2>
<div>Series: #{{ series.SeriesNumber }}</div>
<div v-if="imageInfo.total">Image: #{{ `${series.SliceIndex + 1}/${imageInfo.total}` }}</div>
<div>{{ series.Modality }}</div>
</div>
<div v-if="series" class="right-top-text">
<div>{{ series.Description }}</div>
</div>
<div v-if="series" class="left-bottom-text">
<div v-show="mousePosition.index.length > 0">
Pos: {{ mousePosition.index[0] }}, {{ mousePosition.index[1] }}, {{ mousePosition.index[2] }}
</div>
<div
v-if="(series.Modality === 'CT' || series.Modality === 'DR' || series.Modality === 'CR') && mousePosition.value">
HU: {{ mousePosition.value }}
</div>
<div v-else-if="(series.Modality === 'PT' && mousePosition.value)">
SUVbw(g/ml): {{ digitPlaces === -1 ? mousePosition.value.toFixed(3) :
mousePosition.value.toFixed(digitPlaces)
}}
</div>
<div v-else-if="mousePosition.value">
Density: {{ mousePosition.value }}
</div>
<div v-show="imageInfo.size">
W*H: {{ imageInfo.size }}
</div>
</div>
<div v-if="series" class="right-bottom-text">
<div v-show="imageInfo.location">Location: {{
`${Number(imageInfo.location).toFixed(digitPlaces)} mm`
}}</div>
<div v-show="imageInfo.sliceThickness">Slice Thickness: {{
`${Number(imageInfo.sliceThickness).toFixed(digitPlaces)} mm`
}}</div>
<div v-show="imageInfo.wwwc">WW/WL: {{ imageInfo.wwwc }}</div>
</div>
<div class="orientation-top">
{{ markers.top }}
</div>
<div class="orientation-right">
{{ markers.right }}
</div>
<div class="orientation-bottom">
{{ markers.bottom }}
</div>
<div class="orientation-left">
{{ markers.left }}
</div>
<div ref="sliderBox" class="right-slider-box" @click.stop="clickSlider($event)">
<div :style="{ top: sliderInfo.height + '%' }" class="slider" @click.stop.prevent="() => { return }"
@mousedown.stop="sliderMousedown($event)" />
</div>
</div>
</template>
<script>
import {
metaData,
getRenderingEngine,
utilities as csUtils,
cache
} from '@cornerstonejs/core'
import * as cornerstoneTools from '@cornerstonejs/tools'
import { createImageIdsAndCacheMetaData } from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/createImageIdsAndCacheMetaData'
import setCtTransferFunctionForVolumeActor from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/setCtTransferFunctionForVolumeActor'
import { setCtMappingRange } from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/setCtTransferFunctionForVolumeActor'
import {
setPetTransferFunctionForVolumeActor
} from './helpers/index.js'
import { vec3, mat4 } from 'gl-matrix'
import DicomEvent from '@/views/trials/trials-panel/reading/dicoms/components/DicomEvent'
import {
renderSegmentation,
readingSegmentByConfig,
selectSegmentation,
selectSegment,
createSegmentationRepresentation,
viewSegmentation,
viewSegment,
jumpBidirectional,
viewBidirectional,
changeColor,
resetViewport
} from "./helpers/segmentations"
export default {
name: 'MPRViewport',
props: {
renderingEngineId: {
type: String,
required: true
},
viewportId: {
type: String,
required: true
},
viewportIndex: {
type: Number,
required: true
},
MPRInfo: {
type: Object,
default: () => {
return {}
}
},
histogramVisible: {
type: Boolean,
default: false
},
actionConfiguration: {
type: Object,
default: () => {
return {}
}
},
SegmentConfig: {
type: Object,
default: () => {
return {}
}
},
curSegSeries: {
type: Object,
default: () => {
return {}
}
},
segmentIndex: {
type: Number,
default: 0
},
segmentationId: {
type: String,
default: ''
}
},
data() {
return {
element: '',
series: {},
ctSeries: {},
petSeries: {},
taskInfo: null,
sliderInfo: {
oldB: null,
oldM: null,
isMove: false,
height: 0
},
mousePosition: {
index: [],
value: null,
modalityUnit: '',
world: []
},
imageInfo: {
zoom: null,
size: null,
location: null,
sliceThickness: null,
wwwc: null,
total: 0,
},
digitPlaces: 2,
orientationMarkers: [],
originalMarkers: [],
markers: { top: '', right: '', bottom: '', left: '' },
playClipState: false,
wwwcIdx: 2,
presetName: '',
volumeId: null,
defaultWindowLevel: {},
rotateAngle: 0,
rotateBarLeft: 0,
loading: false,
}
},
mounted() {
this.taskInfo = JSON.parse(sessionStorage.getItem('taskInfo'))
const digitPlaces = Number(localStorage.getItem('digitPlaces'))
this.digitPlaces = digitPlaces === -1 ? this.digitPlaces : digitPlaces
this.$nextTick(() => {
this.initViewport()
})
DicomEvent.$on('createSegmentationRepresentation', (segmentationId) => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
createSegmentationRepresentation(this.viewportId, segmentationId)
})
DicomEvent.$on('viewSegmentation', (obj) => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
viewSegmentation(obj, this.viewportId)
})
DicomEvent.$on('viewSegment', (obj) => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
viewSegment(obj, this.viewportId)
})
DicomEvent.$on('jumpBidirectional', (obj) => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
jumpBidirectional(obj, this.viewportId, this.volumeId)
})
DicomEvent.$on('viewBidirectional', (obj) => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
viewBidirectional(obj, this.viewportId)
})
DicomEvent.$on('changeColor', (obj) => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
changeColor(obj, this.viewportId)
})
DicomEvent.$on('resetViewport', () => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
resetViewport(this.viewportId)
})
DicomEvent.$on('renderSegmentation', async (viewportId) => {
// if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
if (this.viewportId !== viewportId) return false
await renderSegmentation(this.series, this.series.TaskInfo, this.viewportId, this.SegmentConfig, this.segmentationId, this.segmentIndex, this.renderingEngineId, null, this.actionConfiguration)
})
},
watch: {
MPRInfo: {
handler() {
if (!this.series.orientation) return false
switch (this.series.orientation) {
case 'AXIAL':
this.imageInfo.size = `${this.MPRInfo.SAGITTAL.imageNum}*${this.MPRInfo.CORONAL.imageNum}`
break;
case 'CORONAL':
this.imageInfo.size = `${this.MPRInfo.SAGITTAL.imageNum}*${this.MPRInfo.AXIAL.imageNum}`
break;
case 'SAGITTAL':
this.imageInfo.size = `${this.MPRInfo.CORONAL.imageNum}*${this.MPRInfo.AXIAL.imageNum}`
break;
}
},
deep: true
},
SegmentConfig: {
handler() {
if (!this.segmentationId) return false
if (!this.series.TaskInfo) return false
readingSegmentByConfig(this.series, this.series.TaskInfo, this.viewportId, this.segmentationId, this.SegmentConfig)
},
deep: true
},
segmentIndex() {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
if (this.segmentIndex <= 0) return false
selectSegment(this.viewportId, this.segmentationId, this.segmentIndex)
},
segmentationId() {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
if (!this.segmentationId) return false
selectSegmentation(this.viewportId, this.segmentationId)
readingSegmentByConfig(this.series, this.series.TaskInfo, this.viewportId, this.segmentationId, this.SegmentConfig)
}
},
methods: {
initViewport() {
this.element = this.$refs['viewport-volume']
const resizeObserver = new ResizeObserver(() => {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
if (renderingEngine) {
renderingEngine.resize(true, false)
}
})
this.element.oncontextmenu = (e) => e.preventDefault()
// resizeObserver.observe(this.element)
this.element.addEventListener("CORNERSTONE_VOLUME_NEW_IMAGE", this.stackNewImage)
this.element.addEventListener('CORNERSTONE_VOI_MODIFIED', this.voiModified)
this.element.addEventListener('wheel', (e) => {
if (this.histogramVisible) return false
// console.log('CORNERSTONE_STACK_VIEWPORT_SCROLL')
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
const currentImageIdIndex = viewport.getCurrentImageIdIndex()
const totalImages = this.imageInfo.total;
//
if (currentImageIdIndex >= totalImages - 1 && e.wheelDeltaY < 0) {
//
csUtils.jumpToSlice(viewport.element, { imageIndex: 0 });
} else if (currentImageIdIndex <= 0 && e.wheelDeltaY > 0) {
//
csUtils.jumpToSlice(viewport.element, { imageIndex: totalImages - 1 });
}
});
this.element.addEventListener('CORNERSTONE_TOOLS_MOUSE_MOVE', this.cornerstoneToolsMouseMove)
// this.element.addEventListener(cornerstoneTools.Enums.Events.MOUSE_WHEEL, this.handletoolsMouseWheel)
this.element.addEventListener('mouseleave', () => {
this.mousePosition.index = []
this.mousePosition.value = null
})
document.addEventListener('mouseup', () => {
this.sliderMouseup()
})
document.addEventListener('mousemove', (e) => {
this.sliderMousemove(e)
})
},
determineImagePlane(imageOrientationPatient) {
// imageOrientationPatient [rowX, rowY, rowZ, colX, colY, colZ]
// (rowX, rowY, rowZ)
const [rowX, rowY, rowZ] = imageOrientationPatient;
// X, Y, Z
const dotX = Math.abs(rowX);
const dotY = Math.abs(rowY);
const dotZ = Math.abs(rowZ);
//
const maxDot = Math.max(dotX, dotY, dotZ);
//
if (maxDot === dotX) {
// X 线 Y Z
// 线
//
// X-Y (rowZ ) Axial
// X-Z (rowY ) Sagittal
// 线线
// 线 = ×
const [colX, colY, colZ] = imageOrientationPatient.slice(3);
const normalX = rowY * colZ - rowZ * colY;
const normalY = rowZ * colX - rowX * colZ;
const normalZ = rowX * colY - rowY * colX;
const absNormalX = Math.abs(normalX);
const absNormalY = Math.abs(normalY);
const absNormalZ = Math.abs(normalZ);
const maxNormal = Math.max(absNormalX, absNormalY, absNormalZ);
if (maxNormal === absNormalZ) {
return 'AXIAL';
} else if (maxNormal === absNormalY) {
return 'SAGITTAL';
} else if (maxNormal === absNormalX) {
return 'CORONAL';
}
} else if (maxDot === dotY) {
// Y Coronal
return 'SAGITTAL';
} else if (maxDot === dotZ) {
// Z Sagittal
return 'CORONAL';
}
return 'unknown';
},
stackNewImage(e) {
const { detail } = e
this.series.SliceIndex = detail.imageIndex
this.sliderInfo.height = detail.imageIndex * 100 / detail.numberOfSlices
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
const zoom = viewport.getZoom()
this.imageInfo.zoom = zoom.toFixed(4)
let imageIds = viewport.getImageIds(this.volumeId)
let imageId = imageIds[0]
let volume = cache.getVolume(this.volumeId)
let spacing = volume ? volume.spacing : []
// if (this.series.orientation === 'AXIAL') imageId = viewport.getCurrentImageId()
if (imageId && volume) {
this.$emit('setMPRInfo', { type: this.series.orientation, key: "imageNum", value: detail.numberOfSlices })
const imagePlaneModule = metaData.get('imagePlaneModule', imageId)
let type = this.determineImagePlane(imagePlaneModule.imageOrientationPatient)
this.imageInfo.location = type === this.series.orientation ? imagePlaneModule.sliceLocation : ''
this.imageInfo.sliceThickness = type === this.series.orientation ? spacing[2] : spacing[0]
this.imageInfo.total = detail.numberOfSlices
this.getOrientationMarker()
let properties = viewport.getProperties(this.volumeId)
if (properties && properties.voiRange) {
var { lower, upper } = properties.voiRange
const windowWidth = upper - lower
const windowCenter = (upper + lower) / 2
this.defaultWindowLevel.windowWidth = windowWidth
this.defaultWindowLevel.windowCenter = windowCenter
this.imageInfo.wwwc = `${Math.round(windowWidth)}/${Math.round(windowCenter)}`
}
}
if (volume) {
const toolGroupId = 'share-viewport-volume'
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId)
toolGroup.setToolEnabled('ScaleOverlay')
}
// const toolGroupId = this.viewportId
},
setFullScreen(index) {
this.series.SliceIndex = index
setTimeout(() => {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(
this.viewportId
)
csUtils.jumpToSlice(viewport.element, { imageIndex: index })
viewport.render()
})
},
voiModified(e) {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
let properties = viewport.getProperties()
if (properties && properties.voiRange) {
var { lower, upper } = properties.voiRange
const { windowWidth, windowCenter } = csUtils.windowLevel.toWindowLevel(
lower,
upper
)
this.imageInfo.wwwc = `${Math.round(windowWidth)}/${Math.round(windowCenter)}`
this.$emit('upperRangeChange', Math.round(windowWidth))
}
},
getOrientationMarker() {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
const { viewUp, viewPlaneNormal } = viewport.getCamera()
const viewRight = vec3.create()
vec3.cross(viewRight, viewUp, viewPlaneNormal)
const columnCosines = [-viewUp[0], -viewUp[1], -viewUp[2]]
const rowCosines = viewRight
const rowString = cornerstoneTools.utilities.orientation.getOrientationStringLPS(rowCosines)
const columnString = cornerstoneTools.utilities.orientation.getOrientationStringLPS(columnCosines)
const oppositeRowString = cornerstoneTools.utilities.orientation.invertOrientationStringLPS(rowString)
const oppositeColumnString = cornerstoneTools.utilities.orientation.invertOrientationStringLPS(columnString)
this.markers.top = oppositeColumnString
this.markers.right = rowString
this.markers.bottom = columnString
this.markers.left = oppositeRowString
this.orientationMarkers = [oppositeColumnString, rowString, columnString, oppositeRowString]
if (this.originalMarkers.length === 0) {
this.originalMarkers = [...this.orientationMarkers]
}
},
setMarkers() {
const markers = [...this.orientationMarkers]
for (const key in this.markers) {
const v = markers.shift(0)
this.markers[key] = v
}
},
resetOrientationMarkers() {
if (this.originalMarkers.length > 0) {
this.orientationMarkers = [...this.originalMarkers]
this.setMarkers()
}
},
rotateOrientationMarkers(type) {
if (this.orientationMarkers.length > 0) {
if (type === 1) {
this.resetOrientationMarkers()
return
}
const markers = [...this.orientationMarkers]
if (type === 2) {
//
this.orientationMarkers[0] = markers[2]
this.orientationMarkers[2] = markers[0]
} else if (type === 3) {
//
this.orientationMarkers[1] = markers[3]
this.orientationMarkers[3] = markers[1]
} else if (type === 4) {
// 90
this.orientationMarkers = markers.slice(1, 4).concat(markers[0])
} else if (type === 5) {
// 90
this.orientationMarkers = [markers[3]].concat(markers.slice(0, 3))
}
this.setMarkers()
}
},
toggleClipPlay(isPlay, framesPerSecond) {
this.playClipState = isPlay
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
if (isPlay) {
cornerstoneTools.utilities.cine.playClip(viewport.element, { framesPerSecond, loop: true })
} else {
cornerstoneTools.utilities.cine.stopClip(viewport.element)
}
},
scrollPage(type) {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
const currentImageIdIndex = viewport.getCurrentImageIdIndex()
const numImages = viewport.getImageIds().length
let newImageIdIndex = null
if (type === 0) {
newImageIdIndex = 0
} else if (type === -1) {
newImageIdIndex = currentImageIdIndex === 0 ? currentImageIdIndex : currentImageIdIndex - 1
} else if (type === 1) {
newImageIdIndex = currentImageIdIndex === numImages - 1 ? currentImageIdIndex : currentImageIdIndex + 1
} else if (type === 99999) {
newImageIdIndex = numImages - 1
}
// viewport.setImageIdIndex(newImageIdIndex)
csUtils.jumpToSlice(viewport.element, { imageIndex: newImageIdIndex })
cornerstoneTools.utilities.cine.stopClip(viewport.element)
},
setZoom(ratio) {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
const zoom = viewport.getZoom()
if (ratio > 0) {
viewport.setZoom(zoom * 1.05)
} else {
viewport.setZoom(zoom / 1.05)
}
viewport.render()
},
resize(forceFitToWindow) {
console.log('resize: ', forceFitToWindow)
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
if (!forceFitToWindow) {
viewport.setZoom(0.5)
viewport.render()
} else {
viewport.setZoom(1)
viewport.render()
}
},
voiChange(v) {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const voiRange = { lower: 0, upper: v }
const viewport = renderingEngine.getViewport(this.viewportId)
if (!viewport) return
let volumeId = this.volumeId
const viewportsContainingVolumeUID = csUtils.getViewportsWithVolumeId(
volumeId,
viewport.renderingEngineId
)
viewport.setProperties({ voiRange }, volumeId)
viewportsContainingVolumeUID.forEach((vp) => {
vp.render()
// this.$refs[vp.id].voiModified()
this.voiModified()
})
},
setPreset(presetName) {
this.presetName = presetName
},
async createImageIdsAndCacheMetaData(obj) {
this.loading = true
try {
return await createImageIdsAndCacheMetaData({
modality: obj.Modality,
imageIds: obj.ImageIds
})
} finally {
this.loading = false
}
},
async setSeriesInfo(obj, isLocate = false) {
try {
let data = obj
if (this.series && data.Id === this.series.Id && data.Description === this.series.Description && !isLocate && !data.isLocation) {
data.SliceIndex = this.series.SliceIndex
}
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
if (isLocate) return csUtils.jumpToSlice(viewport.element, { imageIndex: data.SliceIndex });
this.series = {}
this.volumeId = data.SeriesInstanceUid
this.series = { ...data }
viewport
.setVolumes([{
volumeId: this.volumeId, callback: (r) => {
if (this.series.Modality === 'PT') {
setPetTransferFunctionForVolumeActor(r)
} else {
let volume = cache.getVolume(this.volumeId)
const voi = metaData.get('voiLutModule', volume._imageIds[Math.ceil((volume._imageIds.length - 1) / 2)])
setCtMappingRange(voi.windowWidth[0], voi.windowCenter[0])
setCtTransferFunctionForVolumeActor(r)
}
console.log("渲染成功")
}
}]).then(r => {
if (data.isLocation || !this.imageInfo.zoom) {
setTimeout(() => { csUtils.jumpToSlice(viewport.element, { imageIndex: data.SliceIndex }); })
}
})
viewport.render()
if (this.series.Modality === 'PT') {
setTimeout(() => {
viewport.resetCamera({ resetPan: true, resetZoom: true, resetToCenter: true })
viewport.resetProperties()
viewport.setProperties({ voiRange: { upper: 5, lower: 0 } })
viewport.render()
renderingEngine.render()
}, 100)
}
await renderSegmentation(this.series, this.series.TaskInfo, this.viewportId, this.SegmentConfig, this.segmentationId, this.segmentIndex, this.renderingEngineId, null, this.actionConfiguration)
} catch (e) {
console.log(e)
}
},
cornerstoneToolsMouseMove(e) {
const { currentPoints } = e.detail
const worldPoint = currentPoints.world
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
let referencedImageId = viewport.getCurrentImageId()
const data = viewport.getImageData()
if (!data) return
const { dimensions, imageData, metadata, voxelManager } = data
const index = imageData.worldToIndex(worldPoint)
index[0] = Math.floor(index[0])
index[1] = Math.floor(index[1])
index[2] = Math.floor(index[2])
this.mousePosition.index = index
const modality = metadata.Modality
let ijk = csUtils.transformWorldToIndex(imageData, worldPoint)
ijk = vec3.round(ijk, ijk);
if (csUtils.indexWithinDimensions(ijk, dimensions)) {
this.isHandleOutsideImage = false
let value = voxelManager.getAtIJKPoint(ijk)
ijk[2] = viewport.getCurrentImageIdIndex()
let modalityUnit
if (modality === 'US') {
const calibratedResults = cornerstoneTools.utilities.getCalibratedProbeUnitsAndValue(image, [ijk])
const hasEnhancedRegionValues = calibratedResults.values.every(
(value) => value !== null
)
value = (hasEnhancedRegionValues ? calibratedResults.values : value)
modalityUnit = hasEnhancedRegionValues
? calibratedResults.units
: 'raw';
} else {
const scalingModule = referencedImageId && metaData.get('scalingModule', referencedImageId)
const isSuvScaled = typeof scalingModule?.suvbw === 'number'
if (scalingModule && scalingModule.suvbw) {
const pixelUnitsOptions = {
isPreScaled: cornerstoneTools.utilities.viewport.isViewportPreScaled(viewport, referencedImageId),
isSuvScaled: isSuvScaled,
}
modalityUnit = cornerstoneTools.utilities.getPixelValueUnits(
modality,
referencedImageId,
pixelUnitsOptions
);
}
}
this.mousePosition.value = value
this.mousePosition.modalityUnit = modalityUnit
}
},
toggleTask(evt, visitTaskNum, i) {
this.$emit('activeViewport', this.viewportIndex)
const num = visitTaskNum + i
if (num >= 0 && num <= this.taskInfo.VisitNum) {
this.$emit('toggleTaskByViewport', { series: this.series, visitTaskNum: num })
}
evt.stopImmediatePropagation()
evt.stopPropagation()
evt.preventDefault()
},
viewCD(taskId) {
this.$emit('previewCD', taskId)
},
setWwwcIdx(idx) {
this.wwwcIdx = idx
},
clickSlider(e) {
const height = e.offsetY * 100 / this.$refs['sliderBox'].clientHeight
this.sliderInfo.height = height
let sliceIdx = Math.trunc(this.imageInfo.total * height / 100)
sliceIdx = sliceIdx >= this.imageInfo.total ? this.imageInfo.total : sliceIdx < 0 ? 0 : sliceIdx
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(
this.viewportId
)
// viewport.setImageIdIndex(sliceIdx)
csUtils.jumpToSlice(viewport.element, { imageIndex: sliceIdx })
viewport.render()
},
sliderMouseup(e) {
this.sliderInfo.isMove = false
this.$emit('contentMouseup', e)
},
sliderMousedown(e) {
const boxHeight = this.$refs['sliderBox'].clientHeight
this.sliderInfo.oldB = parseInt(e.srcElement.style.top) * boxHeight / 100
this.sliderInfo.oldM = e.clientY
this.sliderInfo.isMove = true
e.stopImmediatePropagation()
e.stopPropagation()
e.preventDefault()
},
sliderMousemove(e) {
if (!this.sliderInfo.isMove) return
const delta = this.sliderInfo.oldB - (this.sliderInfo.oldM - e.clientY)
const boxHeight = this.$refs['sliderBox'].clientHeight
if (delta < 0) return
if (delta > boxHeight) return
const height = delta * 100 / boxHeight
let sliceIdx = Math.trunc(this.imageInfo.total * height / 100)
sliceIdx = sliceIdx >= this.imageInfo.total ? this.imageInfo.total - 1 : sliceIdx < 0 ? 0 : sliceIdx
this.sliderInfo.height = height
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(
this.viewportId
)
// viewport.setImageIdIndex(sliceIdx)
csUtils.jumpToSlice(viewport.element, { imageIndex: sliceIdx })
viewport.render()
},
sliderMouseleave(e) {
if (!this.sliderInfo.isMove) return
this.sliderInfo.isMove = false
},
rotate(angle) {
let renderingEngine = getRenderingEngine(this.renderingEngineId)
let viewport = renderingEngine.getViewport(this.viewportId)
const camera = viewport.getCamera()
const { viewUp, position, focalPoint } = camera
const [cx, cy, cz] = focalPoint
const [ax, ay, az] = [0, 0, 1]
const newPosition = [0, 0, 0]
const newFocalPoint = [0, 0, 0]
const newViewUp = [0, 0, 0]
const transform = mat4.identity(new Float32Array(16))
mat4.translate(transform, transform, [cx, cy, cz])
mat4.rotate(transform, transform, angle, [ax, ay, az])
mat4.translate(transform, transform, [-cx, -cy, -cz])
vec3.transformMat4(newPosition, position, transform)
vec3.transformMat4(newFocalPoint, focalPoint, transform)
mat4.identity(transform)
mat4.rotate(transform, transform, angle, [ax, ay, az])
vec3.transformMat4(newViewUp, viewUp, transform)
viewport.setCamera({
position: newPosition,
viewUp: newViewUp,
focalPoint: newFocalPoint
})
viewport.render()
},
clickRotate(e) {
// console.log('clickRotate')
const container = document.getElementById('rotateBar')
const containerWidth = container.offsetWidth
const slider = document.getElementById('rotateSlider')
const sliderWidth = slider.offsetWidth
const x = Math.trunc(e.offsetX)
const deltaX = x - this.rotateBarLeft
const angle = Math.sin((deltaX * (360 / (containerWidth - sliderWidth))) * Math.PI / 180)
this.rotate(angle)
this.rotateBarLeft = x
},
preventDefault(e) {
e.stopImmediatePropagation()
e.stopPropagation()
e.preventDefault()
}
},
computed: {
NSTip() {
return `NS: ${this.$store.state.trials.downloadTip}`
}
},
}
</script>
<style lang="scss" scoped>
.viewport-wrapper {
width: 100%;
height: 100%;
position: relative;
cursor: default !important;
.left-top-text {
position: absolute;
left: 5px;
top: 5px;
// color: #ddd;
z-index: 1;
font-size: 12px;
.cd-info {
// color: #ddd;
font-size: 18px;
}
.subject-info {
color: #f44336;
padding: 5px 0px;
margin: 0;
}
}
.top-center-tool {
position: absolute;
left: 50%;
top: 5px;
transform: translateX(-50%);
z-index: 1;
.toggle-visit-container {
display: flex;
}
.arrw_icon {
width: 20px;
height: 20px;
background-color: #3f3f3f;
text-align: center;
line-height: 20px;
border-radius: 10%;
}
.arrow_text {
height: 20px;
line-height: 20px;
background-color: #00000057;
color: #fff;
padding: 0 10px;
font-size: 14px;
}
}
.right-top-text {
position: absolute;
right: 5px;
top: 5px;
// color: #ddd;
z-index: 1;
font-size: 12px;
}
.left-bottom-text {
position: absolute;
left: 5px;
bottom: 5px;
// color: #ddd;
z-index: 1;
font-size: 12px;
}
.right-bottom-text {
position: absolute;
right: 5px;
bottom: 5px;
// color: #ddd;
z-index: 1;
font-size: 12px;
}
.right-slider-box {
position: absolute;
right: 1px;
height: calc(100% - 140px);
transform: translateY(-50%);
top: calc(50% - 30px);
width: 10px;
background: #333;
z-index: 1;
cursor: pointer;
}
.right-slider-box:after {
content: '';
position: absolute;
bottom: -20px;
left: 0;
height: 20px;
width: 100%;
background: #333;
}
.slider {
height: 20px;
width: 100%;
position: absolute;
top: 0;
z-index: 10;
background: #9e9e9e;
cursor: move
}
.orientation-top {
position: absolute;
left: 50%;
top: 30px;
color: #f44336;
transform: translateX(-50%);
z-index: 1;
}
.orientation-bottom {
position: absolute;
left: 50%;
bottom: 15px;
color: #f44336;
transform: translateX(-50%);
z-index: 1;
}
.orientation-left {
position: absolute;
top: 50%;
left: 15px;
color: #f44336;
transform: translateY(-50%);
z-index: 1;
}
.orientation-right {
position: absolute;
top: 50%;
right: 15px;
color: #f44336;
transform: translateY(-50%);
z-index: 1;
}
}
.color_bar {
position: absolute;
transform: rotate(-90deg);
transform-origin: right;
left: -150px;
top: 30%;
z-index: 1;
}
.rotate_slider_box {
position: absolute;
width: 380px;
height: 10px;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
background: #333;
z-index: 10;
cursor: pointer;
.box {
z-index: 10;
background: #9e9e9e;
height: 100%;
width: 20px;
position: absolute;
top: 0;
cursor: move
}
}
</style>
@@ -1,7 +1,13 @@
<template>
<div ref="viewport-fusion" class="viewport-wrapper" v-loading="loading" :element-loading-text="NSTip"
element-loading-background="rgba(0, 0, 0, 0.8)" @mouseup="sliderMouseup" @mousemove="sliderMousemove"
@mouseleave="sliderMouseleave" :style="{ color: series.Modality === 'PT' || isMip ? '#666' : '#ddd' }">
@mouseleave="sliderMouseleave"
:style="{ color: series.Modality === 'PT' || series.Modality === 'NM' || isMip ? '#666' : '#ddd' }">
<div v-if="isFusion" class="opacity-slider-wrapper" @mousedown.stop @mousemove.stop @mouseup.stop @wheel.stop>
<div class="slider-title">{{ Math.round(fusionOpacity * 100) }}%</div>
<input type="range" min="0" max="1" step="0.01" v-model.number="fusionOpacity" @input="applyFusionOpacity"
class="opacity-slider" />
</div>
<div v-if="series && taskInfo" class="left-top-text">
<div v-if="taskInfo.IsExistsClinicalData && !isMip && !isFusion" class="cd-info"
:title="$t('trials:reading:button:clinicalData')">
@@ -12,32 +18,18 @@
{{ `${series.TaskInfo.SubjectCode} ${series.TaskInfo.TaskBlindName} ` }}
</h2>
<div v-if="!isMip && !isFusion">Series: #{{ series.SeriesNumber }}</div>
<div v-if="series.Stack && !isMip">Image: #{{ `${series.SliceIndex + 1}/${series.Stack.length}` }}</div>
<div v-if="series.Stack && !isMip">Image: #{{ `${series.SliceIndex + 1}/${imageInfo.total || series.Stack.length}` }}</div>
<div v-if="!isMip && !isFusion">{{ series.Modality }}</div>
<div v-if="isFusion">{{ series.Modality }} / {{ ctSeries.Modality }}</div>
<div v-if="isMip">MIP</div>
</div>
<!-- <div v-if="series && taskInfo && taskInfo.IsReadingTaskViewInOrder === 1 && series.TaskInfo && !isMip && !isFusion"
class="top-center-tool">
<div class="toggle-visit-container">
<div class="arrw_icon"
:style="{ cursor: series.TaskInfo.VisitTaskNum !== 0 ? 'pointer' : 'not-allowed', color: series.TaskInfo.VisitTaskNum !== 0 ? '#fff' : '#6b6b6b' }"
@click.stop.prevent="toggleTask($event, series.TaskInfo.VisitTaskNum, -1)"
@dblclick.stop="preventDefault($event)">
<i class="el-icon-caret-left" />
</div>
<div class="arrow_text">
{{ series.TaskInfo.TaskBlindName }}
</div>
<div class="arrw_icon"
:style="{ cursor: series.TaskInfo.VisitTaskNum < taskInfo.VisitNum ? 'pointer' : 'not-allowed', color: series.TaskInfo.VisitTaskNum < taskInfo.VisitNum ? '#fff' : '#6b6b6b' }"
@click.stop.prevent="toggleTask($event, series.TaskInfo.VisitTaskNum, 1)"
@dblclick.stop="preventDefault($event)">
<i class="el-icon-caret-right" />
</div>
</div>
</div> -->
<div v-if="series && !isMip && !isFusion" class="right-top-text">
<div>{{ series.Description }}</div>
</div>
<!-- <div v-if="isFusion && !isMip" class="fusion-order-toggle" @mousedown.stop @mouseup.stop
@click.stop="toggleFusionRenderOrder">
{{ fusionCtOnTop ? `${ctSeries.Modality}/${series.Modality}` : `${series.Modality}/${ctSeries.Modality}` }}
</div> -->
<div v-if="series" class="left-bottom-text">
<div v-show="mousePosition.index.length > 0 && !isMip">
Pos: {{ mousePosition.index[0] }}, {{ mousePosition.index[1] }}, {{ mousePosition.index[2] }}
@@ -104,10 +96,16 @@ import {
import * as cornerstoneTools from '@cornerstonejs/tools'
import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'
import { createImageIdsAndCacheMetaData } from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/createImageIdsAndCacheMetaData'
import setCtTransferFunctionForVolumeActor from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/setCtTransferFunctionForVolumeActor'
import { setPetColorMapTransferFunctionForVolumeActor } from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/setPetColorMapTransferFunctionForVolumeActor'
const { BlendModes, OrientationAxis } = Enums;
import {
setMipTransferFunctionForVolumeActor,
setPetTransferFunctionForVolumeActor
} from './helpers/index.js'
import setNmFusionColorMapTransferFunctionForVolumeActor from './helpers/setNmFusionColorMapTransferFunctionForVolumeActor'
const { BlendModes, OrientationAxis } = Enums;
const { getColormap } = csUtils.colormap;
import { vec3, mat4 } from 'gl-matrix'
export default {
@@ -134,6 +132,7 @@ export default {
petSeries: {},
isFusion: false,
isMip: false,
fusionCtOnTop: false,
taskInfo: null,
sliderInfo: {
oldB: null,
@@ -152,7 +151,8 @@ export default {
size: null,
location: null,
sliceThickness: null,
wwwc: null
wwwc: null,
total: null
},
digitPlaces: 2,
orientationMarkers: [],
@@ -172,7 +172,11 @@ export default {
isMove: false
},
ptVolumeId: null,
loading: false
loading: false,
Colorbar: null,
fusionOpacity: 0.95,
topFusionVolumeActor: null,
currentVoiUpper: null
}
},
mounted() {
@@ -220,8 +224,12 @@ export default {
},
stackNewImage(e) {
const { detail } = e
this.series.SliceIndex = detail.imageIndex
this.sliderInfo.height = detail.imageIndex * 100 / detail.numberOfSlices
if (this.series) {
this.series.SliceIndex = detail.imageIndex
}
this.imageInfo.total = detail.numberOfSlices
const maxIndex = detail.numberOfSlices > 1 ? detail.numberOfSlices - 1 : 1
this.sliderInfo.height = detail.imageIndex * 100 / maxIndex
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
const zoom = viewport.getZoom()
@@ -267,16 +275,29 @@ export default {
const viewport = renderingEngine.getViewport(this.viewportId)
let properties = viewport.getProperties()
if (this.isFusion) {
properties = viewport.getProperties(this.volumeId)
properties = viewport.getProperties(this.ptVolumeId || this.volumeId)
}
if (properties && properties.voiRange) {
var { lower, upper } = properties.voiRange
if ((!upper || upper < 1) && this.currentVoiUpper >= 1) {
upper = this.currentVoiUpper
} else if (upper) {
this.currentVoiUpper = upper
}
if (!upper || upper < 1) return
const { windowWidth, windowCenter } = csUtils.windowLevel.toWindowLevel(
lower,
upper
)
this.imageInfo.wwwc = `${Math.round(windowWidth)}/${Math.round(windowCenter)}`
this.$emit('upperRangeChange', Math.round(windowWidth))
if (this.series.Modality === 'PT' || this.series.Modality === 'NM' || this.isFusion) {
this.$emit('upperRangeChange', Math.round(upper))
}
if (this.isFusion && !this.fusionCtOnTop && this.topFusionVolumeActor) {
this.applyFusionOpacity()
}
}
},
getOrientationMarker() {
@@ -397,6 +418,8 @@ export default {
voiChange(v) {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const voiRange = { lower: 0, upper: v }
this.currentVoiUpper = v
const viewport = renderingEngine.getViewport(this.viewportId)
if (!viewport) return
let volumeId = this.isFusion ? this.ptVolumeId : this.volumeId
@@ -406,6 +429,11 @@ export default {
)
viewport.setProperties({ voiRange }, volumeId)
// if (this.isFusion && !this.fusionCtOnTop && this.topFusionVolumeActor) {
// this.applyFusionOpacity()
// }
viewportsContainingVolumeUID.forEach((vp) => {
vp.render()
// this.$refs[vp.id].voiModified()
@@ -445,6 +473,32 @@ export default {
ctx.fillStyle = gradient
ctx.fillRect(0, 0, rectWidth, rectHeight)
},
opacityChange(opacity) {
this.fusionOpacity = opacity
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
if (!viewport) return
if (this.isFusion) {
// const ctVolumeId = this.ctSeries?.SeriesInstanceUid
const ptVolumeId = this.ptVolumeId
viewport.setProperties({ colormap: { opacity: Number(opacity) } }, ptVolumeId)
// const topVolumeId = this.fusionCtOnTop ? ctVolumeId : ptVolumeId
// const bottomVolumeId = this.fusionCtOnTop ? ptVolumeId : ctVolumeId
// const topOpacity = Number(opacity)
// const bottomOpacity = 1 - topOpacity
// if (bottomVolumeId) {
// viewport.setProperties({ colormap: { opacity: bottomOpacity } }, bottomVolumeId)
// }
// if (topVolumeId) {
// viewport.setProperties({ colormap: { opacity: topOpacity } }, topVolumeId)
// }
}
viewport.render()
},
applyFusionOpacity() {
this.opacityChange(this.fusionOpacity)
},
setPreset(presetName) {
this.presetName = presetName
},
@@ -470,11 +524,109 @@ export default {
},
async createImageIdsAndCacheMetaData(obj) {
this.loading = true
await createImageIdsAndCacheMetaData({
modality: obj.Modality,
imageIds: obj.ImageIds
try {
return await createImageIdsAndCacheMetaData({
modality: obj.Modality,
imageIds: obj.ImageIds
})
} finally {
this.loading = false
}
},
getFusionVolumes() {
const ctVolumeId = this.ctSeries?.SeriesInstanceUid
const ptFusionVolumeId = this.ptVolumeId
if (!ctVolumeId || !ptFusionVolumeId) {
return []
}
const ctEntry = {
volumeId: ctVolumeId,
callback: (r) => {
setCtTransferFunctionForVolumeActor({ ...r, volumeId: ctVolumeId })
if (this.fusionCtOnTop) {
this.topFusionVolumeActor = r.volumeActor
}
this.applyFusionOpacity()
console.log("融合ct渲染成功")
}
}
const ptFusionEntry = {
volumeId: ptFusionVolumeId,
callback: (r) => {
if (this.series.Modality === 'NM') {
setNmFusionColorMapTransferFunctionForVolumeActor({
...r,
volumeId: ptFusionVolumeId,
})
} else {
setPetColorMapTransferFunctionForVolumeActor({
...r,
volumeId: ptFusionVolumeId,
})
}
if (!this.fusionCtOnTop) {
this.topFusionVolumeActor = r.volumeActor
}
this.applyFusionOpacity()
console.log("融合pet渲染成功")
}
}
const volumes = []
// if (this.series.Modality !== 'NM') {
// volumes.push({
// volumeId: this.volumeId,
// callback: (r) => {
// console.log("pet");
// }
// })
// }
volumes.push({
volumeId: this.volumeId,
callback: (r) => {
console.log("融合pet渲染成功");
}
})
this.loading = false
if (this.fusionCtOnTop) {
volumes.push(ptFusionEntry, ctEntry)
} else {
volumes.push(ctEntry, ptFusionEntry)
}
return volumes
},
async applyFusionRenderOrder() {
if (!this.isFusion) return
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine?.getViewport?.(this.viewportId)
if (!viewport) return
const volumes = this.getFusionVolumes()
if (!volumes.length) return
const camera = viewport.getCamera?.()
const savedVoiUpper = this.currentVoiUpper
await viewport.setVolumes(volumes)
if (camera) {
viewport.setCamera(camera)
}
if (savedVoiUpper) {
viewport.setProperties({ voiRange: { lower: 0, upper: savedVoiUpper } }, this.ptVolumeId)
}
viewport.render()
},
toggleFusionRenderOrder() {
if (!this.isFusion) return
this.fusionCtOnTop = !this.fusionCtOnTop
this.applyFusionRenderOrder()
this.$emit('upperRangeChange', Math.round(this.currentVoiUpper))
},
async setSeriesInfo(obj, isLocate = false, option = {}) {
try {
@@ -482,17 +634,18 @@ export default {
if (this.series && data.Id === this.series.Id && data.Description === this.series.Description && !isLocate) {
data.SliceIndex = this.series.SliceIndex
}
// this.series = { ...data }
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
if (isLocate) return csUtils.jumpToSlice(viewport.element, { imageIndex: data.SliceIndex });
this.volumeId = data.SeriesInstanceUid
this.ptVolumeId = null
this.series = {}
this.topFusionVolumeActor = null
let { isFusion, isMip, colorMap } = option
this.isFusion = isFusion;
this.isMip = isMip;
if (this.isFusion) {
this.fusionCtOnTop = false
this.$nextTick(() => {
this.renderColorBar(this.presetName)
})
@@ -500,32 +653,10 @@ export default {
let { ct, data } = obj
this.series = { ...data }
this.ctSeries = { ...ct }
this.petSeries = { ...data }
await viewport
.setVolumes([
{
volumeId: this.volumeId, callback: (r) => {
setPetColorMapTransferFunctionForVolumeActor(r)
console.log("融合pet渲染成功");
}
},
{
volumeId: ct.SeriesInstanceUid, callback: (r) => {
setCtTransferFunctionForVolumeActor(r)
console.log("融合ct渲染成功")
}
},
{
volumeId: this.ptVolumeId, callback: (r) => {
setPetColorMapTransferFunctionForVolumeActor(r)
console.log("融合pet渲染成功");
}
},
]).then(res => {
if (colorMap) {
this.setColorMap(this.presetName)
}
})
await viewport.setVolumes(this.getFusionVolumes())
} else {
this.series = { ...data }
if (this.isMip) {
@@ -540,7 +671,12 @@ export default {
.setVolumes([{
volumeId: this.volumeId,
callback: (r) => {
setPetColorMapTransferFunctionForVolumeActor(r)
if (this.series.Modality === 'NM') {
setMipTransferFunctionForVolumeActor({ ...r, volumeId: this.volumeId })
} else {
setPetTransferFunctionForVolumeActor(r)
}
// setPetColorMapTransferFunctionForVolumeActor(r)
console.log("mip渲染成功")
},
slabThickness,
@@ -548,51 +684,80 @@ export default {
defaultOptions: {
orientation: OrientationAxis.CORONAL
}
}]).then(res => {
if (colorMap) {
this.setColorMap(this.presetName)
}
if (isLocate) {
setTimeout(() => { csUtils.jumpToSlice(viewport.element, { imageIndex: data.SliceIndex }); })
}
})
}])
} else {
viewport
.setVolumes([{
volumeId: this.volumeId, callback: (r) => {
if (this.series.Modality === 'PT') {
setPetColorMapTransferFunctionForVolumeActor(r, true)
if (this.series.Modality === 'PT' || this.series.Modality === 'NM') {
// setPetColorMapTransferFunctionForVolumeActor(r, true)
setPetTransferFunctionForVolumeActor({ ...r, volumeId: this.volumeId })
} else {
setCtTransferFunctionForVolumeActor(r)
setCtTransferFunctionForVolumeActor({ ...r, volumeId: this.volumeId })
}
console.log("渲染成功")
}
}]).then(res => {
if (colorMap) {
this.setColorMap(this.presetName)
}
})
}])
}
}
viewport.render()
this.voiChange(this.currentVoiUpper)
} catch (e) {
console.log(e)
}
},
cornerstoneToolsMouseMove(e) {
const { currentPoints } = e.detail
const worldPoint = currentPoints.world
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
const imageData = viewport.getImageData()
if (!imageData) return
const index = imageData.imageData.worldToIndex(worldPoint)
index[0] = Math.floor(index[0])
index[1] = Math.floor(index[1])
index[2] = Math.floor(index[2])
this.mousePosition.index = index
try {
const { currentPoints } = e.detail
const worldPoint = currentPoints.world
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
let referencedImageId = viewport.getCurrentImageId()
const data = viewport.getImageData()
if (!data || !referencedImageId) return
const { dimensions, imageData, metadata, voxelManager } = data
const index = imageData.worldToIndex(worldPoint)
index[0] = Math.floor(index[0])
index[1] = Math.floor(index[1])
index[2] = Math.floor(index[2])
this.mousePosition.index = index
const modality = metadata.Modality
let ijk = csUtils.transformWorldToIndex(imageData, worldPoint)
ijk = vec3.round(ijk, ijk);
if (csUtils.indexWithinDimensions(ijk, dimensions)) {
this.isHandleOutsideImage = false
let value = voxelManager.getAtIJKPoint(ijk)
ijk[2] = viewport.getCurrentImageIdIndex()
let modalityUnit
if (modality === 'US') {
const calibratedResults = cornerstoneTools.utilities.getCalibratedProbeUnitsAndValue(image, [ijk])
const hasEnhancedRegionValues = calibratedResults.values.every(
(value) => value !== null
)
value = (hasEnhancedRegionValues ? calibratedResults.values : value)
modalityUnit = hasEnhancedRegionValues
? calibratedResults.units
: 'raw';
} else {
const scalingModule = referencedImageId && metaData.get('scalingModule', referencedImageId)
const isSuvScaled = typeof scalingModule?.suvbw === 'number'
const pixelUnitsOptions = {
isPreScaled: cornerstoneTools.utilities.viewport.isViewportPreScaled(viewport, referencedImageId),
isSuvScaled: isSuvScaled,
}
modalityUnit = cornerstoneTools.utilities.getPixelValueUnits(
modality,
referencedImageId,
pixelUnitsOptions
);
}
this.mousePosition.value = value
this.mousePosition.modalityUnit = modalityUnit
}
} catch(e) {
console.log(e)
}
},
toggleTask(evt, visitTaskNum, i) {
this.$emit('activeViewport', this.viewportIndex)
@@ -614,8 +779,10 @@ export default {
clickSlider(e) {
const height = e.offsetY * 100 / this.$refs['sliderBox'].clientHeight
this.sliderInfo.height = height
let sliceIdx = Math.trunc(this.series.Stack.length * height / 100)
sliceIdx = sliceIdx >= this.series.Stack.length ? this.series.Stack.length - 1 : sliceIdx < 0 ? 0 : sliceIdx
const totalSlices = this.imageInfo.total || this.series.Stack?.length || 1
const maxIndex = totalSlices > 1 ? totalSlices - 1 : 0
let sliceIdx = Math.round(maxIndex * height / 100)
sliceIdx = sliceIdx > maxIndex ? maxIndex : sliceIdx < 0 ? 0 : sliceIdx
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(
this.viewportId
@@ -630,7 +797,7 @@ export default {
},
sliderMousedown(e) {
const boxHeight = this.$refs['sliderBox'].clientHeight
this.sliderInfo.oldB = parseInt(e.srcElement.style.top) * boxHeight / 100
this.sliderInfo.oldB = this.sliderInfo.height * boxHeight / 100
this.sliderInfo.oldM = e.clientY
this.sliderInfo.isMove = true
e.stopImmediatePropagation()
@@ -644,8 +811,10 @@ export default {
if (delta < 0) return
if (delta > boxHeight) return
const height = delta * 100 / boxHeight
let sliceIdx = Math.trunc(this.series.Stack.length * height / 100)
sliceIdx = sliceIdx >= this.series.Stack.length ? this.series.Stack.length - 1 : sliceIdx < 0 ? 0 : sliceIdx
const totalSlices = this.imageInfo.total || this.series.Stack?.length || 1
const maxIndex = totalSlices > 1 ? totalSlices - 1 : 0
let sliceIdx = Math.round(maxIndex * height / 100)
sliceIdx = sliceIdx > maxIndex ? maxIndex : sliceIdx < 0 ? 0 : sliceIdx
this.sliderInfo.height = height
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(
@@ -751,11 +920,15 @@ export default {
e.stopImmediatePropagation()
e.stopPropagation()
e.preventDefault()
}
},
},
beforeDestroy() {
this.series = null
this.topFusionVolumeActor = null
},
computed: {
NSTip() {
return `NS: ${this.$store.state.trials.uploadTip}`
return `NS: ${this.$store.state.trials.downloadTip}`
}
},
}
@@ -767,6 +940,43 @@ export default {
position: relative;
cursor: default !important;
.opacity-slider-wrapper {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
background: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 4px;
width: 40px;
.slider-title {
font-size: 12px;
color: #ddd;
margin-bottom: 10px;
width: 30px;
text-align: center;
}
.opacity-slider {
-webkit-appearance: slider-vertical;
appearance: slider-vertical;
writing-mode: vertical-lr;
direction: rtl;
width: 10px;
height: 150px;
padding: 0;
margin: 0;
cursor: pointer;
outline: none;
}
}
.left-top-text {
position: absolute;
left: 5px;
@@ -826,6 +1036,20 @@ export default {
font-size: 12px;
}
.fusion-order-toggle {
position: absolute;
right: 5px;
top: 5px;
z-index: 2;
font-size: 12px;
padding: 2px 8px;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 2px;
cursor: pointer;
user-select: none;
}
.left-bottom-text {
position: absolute;
left: 5px;
@@ -943,4 +1167,4 @@ export default {
cursor: move
}
}
</style>
</style>
File diff suppressed because it is too large Load Diff
@@ -1986,12 +1986,12 @@ export default {
.merge-table {
padding: 0 10px;
::v-deep.el-table {
::v-deep .el-table {
background-color: #1e1e1e !important;
color: #383838;
}
::v-deep.el-table td.el-table__cell,
::v-deep .el-table td.el-table__cell,
.el-table th.el-table__cell.is-leaf {
border-bottom: 1px solid #383838;
}
@@ -2002,7 +2002,7 @@ export default {
background-color: #1e1e1e;
}
::v-deep.el-table__header-wrapper {
::v-deep .el-table__header-wrapper {
th {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -2010,7 +2010,7 @@ export default {
}
}
::v-deep.el-table__body-wrapper {
::v-deep .el-table__body-wrapper {
tr {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -2021,7 +2021,7 @@ export default {
}
}
::v-deep.el-table__empty-block {
::v-deep .el-table__empty-block {
background-color: #1e1e1e !important;
}
@@ -0,0 +1,285 @@
<template>
<el-form ref="segmentForm" :model="form" label-width="120px" label-position="left" :rules="rules">
<!-- 检查名称 -->
<el-form-item :label="$t('segment:form:label:studyName')" prop="taskBlindName">
<el-select v-model="form.studyId" clearable @change="(e) => handleChange(e, 'study')"
@clear="(e) => handleClear(e, 'study')">
<el-option v-for="item in studyList" :key="item.StudyId" :label="item.StudyCode"
:value="item.StudyId" />
</el-select>
</el-form-item>
<!-- 序列名称 -->
<el-form-item :label="$t('segment:form:label:seriesName')" prop="taskBlindName">
<el-select v-model="form.seriesId" clearable @change="(e) => handleChange(e, 'series')"
@clear="(e) => handleClear(e, 'series')">
<el-option v-for="item in seriesList" :key="item.Id"
:label="`#${item.SeriesNumber}/${item.Modality}/${item.Description}`" :value="item.Id" />
</el-select>
</el-form-item>
<!-- 分割组名称 -->
<el-form-item :label="$t('segment:form:label:segmentGroupName')" prop="segmentGroupId">
<el-select v-model="form.segmentGroupId" clearable @change="(e) => handleChange(e, 'segmentGroup')"
@clear="(e) => handleClear(e, 'segmentGroup')">
<el-option v-for="item in segmentGroupList" :key="item.Id" :label="item.SegmentationName"
:value="item.Id" />
</el-select>
</el-form-item>
<!-- 分割名称 -->
<el-form-item :label="$t('segment:form:label:segmentName')" prop="taskBlindName">
<el-select v-model="form.segmentId" clearable>
<el-option v-for="item in segmentList" :key="item.Id" :label="item.SegmentName" :value="item.Id" />
</el-select>
</el-form-item>
<el-form-item style="text-align:right;">
<!-- 取消 -->
<el-button size="mini" @click="handleCancel">{{ $t('common:button:cancel') }}</el-button>
<!-- 确认 -->
<el-button type="primary" size="mini" @click="handleSave" :disabled="!form.segmentId">
{{ $t('common:button:confirm') }}</el-button>
</el-form-item>
</el-form>
</template>
<script>
import { getSegmentationList, getSegmentList, getSegmentBindingList } from '@/api/reading'
export default {
name: 'FusionForm',
props: {
visible: {
type: Boolean,
default: false
},
visitInfo: {
type: Object,
default: () => {
return {}
}
}
},
data() {
return {
form: {
studyId: '',
seriesId: '',
segmentGroupId: '',
segmentId: ""
},
studyList: [],
seriesList: [],
segmentGroupList: [],
segmentList: [],
segmentionList: [],
series: {},
rules: {
segmentGroupId: [
{
validator: (rule, value, callback) => {
if (value) {
// callback(new Error(''));
let segmentGroup = this.segmentGroupList.find(item => item.Id === value)
if (!segmentGroup.IsSaved) {
callback(new Error(this.$t('segment:confirm:message:segmentGroupIsNotSave')));
} else {
callback();
}
} else {
callback();
}
}, trigger: ['blur', 'change']
}
]
}
}
},
// mounted() {
// this.init()
// },
methods: {
setSeries(series) {
this.series = series
},
async init() {
let studyList = Object.assign(this.visitInfo.StudyList, {})
let s = await this.getSegmentationList()
this.segmentionList = s
let StudyIds = s.map(item => item.StudyId)
let SeriesIds = s.map(item => item.SeriesId)
studyList = studyList.filter(item => StudyIds.includes(item.StudyId))
studyList.forEach(study => {
study.SeriesArr = study.SeriesList.filter(item => SeriesIds.includes(item.Id))
})
this.studyList = studyList
if (this.visitInfo.operateStateEnum === 21) {
let find = studyList.some(item => item.StudyId === this.series.StudyId)
if (find) {
this.form.studyId = this.series.StudyId
this.handleChange(null, 'study')
let findSeries = find.SeriesArr.some(item => item.Id === this.form.seriesId)
if (findSeries) {
this.form.seriesId = this.series.Id
this.handleChange(null, 'series')
}
}
}
if (this.visitInfo.operateStateEnum === 22) {
let o = {}
if (this.isTableQuestion) {
o.TableQuestionId = this.visitInfo.operateQuestionId
o.RowId = this.visitInfo.operateRowId
} else {
o.QuestionId = this.visitInfo.operateQuestionId
}
let list = await this.getSegmentBindingList(o)
let segmentGroup = this.segmentionList.find(item => item.Id === list[0].SegmentationId)
this.form.studyId = segmentGroup.StudyId
this.handleChange(null, 'study')
this.form.seriesId = segmentGroup.SeriesId
this.handleChange(null, 'series')
this.form.segmentGroupId = list[0].SegmentationId
this.handleChange(null, 'segmentGroup')
this.form.segmentId = list[0].SegmentId
}
},
async handleChange(e, key) {
if (key === 'study') {
console.log(this.studyList, 'this.studyList')
this.seriesList = this.studyList.find(item => item.StudyId === this.form.studyId).SeriesArr
}
if (key === 'series') {
this.segmentGroupList = this.segmentionList.filter(item => item.SeriesId === this.form.seriesId)
}
if (key === 'segmentGroup') {
let list = await this.getSegmentList(this.form.segmentGroupId)
this.segmentList = list.filter(item => item.SegmentJson)
}
},
handleClear(e, key) {
if (key === 'study') {
this.form.seriesId = null
this.form.segmentGroupId = null
this.form.segmentId = null
}
if (key === 'series') {
this.form.segmentGroupId = null
this.form.segmentId = null
}
if (key === 'segmentGroup') {
this.form.segmentId = null
}
},
handleCancel() {
this.$emit("update:visible", false)
},
async handleSave() {
let validate = await this.$refs.segmentForm.validate()
if (!validate) return false
let segment = this.segmentList.find(item => item.Id === this.form.segmentId)
if (segment.SegmentJson) {
let obj = JSON.parse(segment.SegmentJson)
segment.stats = obj.stats
segment.bidirectional = obj.bidirectional
}
this.$emit('handleSegmentSave', segment)
this.handleCancel()
},
//
async getSegmentationList() {
try {
let data = {
VisitTaskId: this.visitInfo.VisitTaskId,
PageSize: 9999,
PageIndex: 1,
}
this.loading = true;
let res = await getSegmentationList(data);
this.loading = false;
if (res.IsSuccess) {
return res.Result.CurrentPageData;
}
} catch (err) {
this.loading = false
console.log(err)
}
},
//
async getSegmentList(id) {
try {
let data = {
SegmentationId: id,
PageSize: 9999,
PageIndex: 1,
}
this.loading = true;
let res = await getSegmentList(data)
this.loading = false;
if (res.IsSuccess) {
return res.Result.CurrentPageData
}
} catch (err) {
this.loading = false
console.log(err)
}
},
//
async getSegmentBindingList(param = {}) {
try {
let data = {
VisitTaskId: this.visitInfo.VisitTaskId,
PageSize: 9999,
PageIndex: 1,
}
data = Object.assign(data, param)
let res = await getSegmentBindingList(data)
if (res.IsSuccess) {
return res.Result.CurrentPageData
}
} catch (err) {
console.log(err)
}
},
}
}
</script>
<style lang="scss" scoped>
.series-table {
::v-deep.el-table {
background-color: #1e1e1e !important;
color: #dfdfdf;
}
::v-deep.el-table td.el-table__cell,
.el-table th.el-table__cell.is-leaf {
border-bottom: 1px solid #dfdfdf;
}
.el-table--border::after,
.el-table--group::after,
.el-table::before {
background-color: #1e1e1e;
}
::v-deep.el-table__header-wrapper {
th {
background-color: #1e1e1e !important;
color: #dfdfdf;
}
}
::v-deep.el-table__body-wrapper {
tr {
background-color: #1e1e1e !important;
color: #dfdfdf;
}
tr:hover>td {
background-color: #1e1e1e !important;
}
}
::v-deep.el-table__empty-block {
background-color: #1e1e1e !important;
}
}
</style>
File diff suppressed because it is too large Load Diff
@@ -1,16 +1,10 @@
<template>
<div v-loading="loading" class="study-wrapper">
<div class="study-info">
<div
v-if="taskInfo && taskInfo.IsReadingShowSubjectInfo"
:title="taskInfo.SubjectCode"
>
<div v-if="taskInfo && taskInfo.IsReadingShowSubjectInfo" :title="taskInfo.SubjectCode">
{{ taskInfo.SubjectCode }}
</div>
<div
v-if="taskInfo && taskInfo.IsReadingShowSubjectInfo"
:title="visitTaskInfo.TaskBlindName"
>
<div v-if="taskInfo && taskInfo.IsReadingShowSubjectInfo" :title="visitTaskInfo.TaskBlindName">
{{ visitTaskInfo.TaskBlindName }}
</div>
</div>
@@ -18,15 +12,14 @@
<el-collapse v-model="activeNames">
<el-collapse-item v-for="(study, index) in studyList" :key="`${study.StudyId}`" :name="`${study.StudyId}`">
<template slot="title">
<div
v-if="!study.IsCriticalSequence"
class="dicom-desc"
>
<div v-if="!study.IsCriticalSequence" class="dicom-desc">
<template v-if="taskInfo && taskInfo.IsShowStudyName">
<div style="text-overflow: ellipsis;overflow: hidden;">
<span :title="study.StudyCode">{{ study.StudyCode }}</span>
<span v-if="study.StudyName" :title="study.StudyName" style="margin-left: 5px;">{{ study.StudyName }}</span>
<span v-else :title="study.Modalities" style="margin-left: 5px;">{{ `${study.Modalities} (${study.SeriesCount})` }}</span>
<span v-if="study.StudyName" :title="study.StudyName" style="margin-left: 5px;">{{ study.StudyName
}}</span>
<span v-else :title="study.Modalities" style="margin-left: 5px;">{{ `${study.Modalities}
(${study.SeriesCount})` }}</span>
</div>
<div v-if="study.StudyName" style="text-overflow: ellipsis;overflow: hidden;">
<span :title="study.Modalities">{{ `${study.Modalities} (${study.SeriesCount})` }}</span>
@@ -46,53 +39,38 @@
</div>
</template>
<div class="dicom-list-container">
<div
v-for="(series, i) in study.SeriesList"
:key="series.Id"
style="position:relative;margin-top:1px;"
@click="activeSeries(series, i, index)"
>
<div
:class="{'series-active': index === activeStudyIndex && i === activeSeriesIndex}"
class="series-wrapper"
>
<div v-for="(series, i) in study.SeriesList" :key="series.Id" style="position:relative;margin-top:1px;"
@click="activeSeries(series, i, index)">
<div :class="{ 'series-active': index === activeStudyIndex && i === activeSeriesIndex }"
class="series-wrapper">
<div class="series-image">
<el-image
style="width: 100%;height: 100%;"
<el-image style="width: 100%;height: 100%;"
:src="`${OSSclientConfig.basePath}${series.ImageResizePath || series.NoneDicomFileFirstFile}`"
fit="fill"
crossorigin="anonymous"
/>
fit="fill" crossorigin="anonymous" />
</div>
<div class="series-text">
<div v-if="series.IsExistMutiFrames && series.InstanceCount > 1"
style="position: absolute;right: 0;top: 0;">
<el-popover
placement="right"
trigger="hover"
popper-class="instance_frame_wrapper"
>
<div v-if="series.IsExistMutiFrames && series.InstanceCount > 1"
style="position: absolute;right: 0;top: 0;">
<el-popover placement="right" trigger="hover" popper-class="instance_frame_wrapper">
<div class="frame_list">
<div
v-for="(instance, idx) in series.InstanceInfoList"
:key="instance.Id"
class="frame_content"
:style="{'margin-bottom':idx<series.InstanceInfoList.length-1? '5px':'0px'}"
@click.stop="showMultiFrames(index,series, i, instance)"
>
<div v-for="(instance, idx) in series.InstanceInfoList" :key="instance.Id" class="frame_content"
:style="{ 'margin-bottom': idx < series.InstanceInfoList.length - 1 ? '5px' : '0px' }"
@click.stop="showMultiFrames(index, series, i, instance)">
<div>
<div>{{ instance.InstanceNumber }}</div>
<div>{{ `${instance.NumberOfFrames > 0 ? instance.KeyFramesList.length > 0 ? instance.KeyFramesList.length : instance.NumberOfFrames : 1} frame` }}</div>
<div>{{ `${instance.NumberOfFrames > 0 ? instance.KeyFramesList.length > 0 ?
instance.KeyFramesList.length : instance.NumberOfFrames : 1} frame` }}</div>
</div>
</div>
</div>
<i slot="reference" class="el-icon-connection" style="font-size: 15px;cursor: pointer;color: #ffeb3b;" />
<i slot="reference" class="el-icon-connection"
style="font-size: 15px;cursor: pointer;color: #ffeb3b;" />
</el-popover>
</div>
<div v-if="!study.IsCriticalSequence" class="text-desc" :title="series.SeriesNumber">
#{{ series.SeriesNumber }}
</div>
<div v-if="series.Description" class="text-desc" :title="series.Description">
{{ series.Description }}
@@ -104,17 +82,19 @@
<span v-show="series.LoadedImageCount < series.InstanceCount">
{{ series.Modality }}: {{ series.LoadedImageCount }}/{{ series.InstanceCount }} image
</span>
<span v-show="series.LoadedImageCount >= series.InstanceCount">{{ series.Modality }}: {{ series.InstanceCount }} image</span>
<span v-show="series.LoadedImageCount >= series.InstanceCount">{{ series.Modality }}: {{
series.InstanceCount
}} image</span>
</div>
<div style="line-height: 12px;">
<i v-show="series.IsBeMark || markedSeriesIds.includes(series.Id)" class="el-icon-star-on" style="font-size: 12px;color: #ff5722;" />
<i v-show="series.IsBeMark || markedSeriesIds.includes(series.Id)" class="el-icon-star-on"
style="font-size: 12px;color: #ff5722;" />
</div>
</div>
</div>
<div v-if="series.LoadedImageCount > 0 && series.LoadedImageCount < series.InstanceCount" style="width: 100%;">
<el-progress
:percentage="parseInt((series.LoadedImageProgress / series.InstanceCount).toFixed(2))"
/>
<div v-if="series.LoadedImageCount > 0 && series.LoadedImageCount < series.InstanceCount"
style="width: 100%;">
<el-progress :percentage="parseInt((series.LoadedImageProgress / series.InstanceCount).toFixed(2))" />
</div>
</div>
</div>
@@ -124,6 +104,7 @@
</div>
</template>
<script>
import DicomEvent from '@/views/trials/trials-panel/reading/dicoms/components/DicomEvent'
export default {
name: 'StudyList',
props: {
@@ -168,6 +149,7 @@ export default {
this.activeStudyIndex = studyIndex
this.activeSeriesIndex = seriesIndex
this.$emit('activeSeries', series)
DicomEvent.$emit('activeSeries', series)
},
activeStudy(id) {
if (this.activeNames.indexOf(id) > -1) return
@@ -179,6 +161,7 @@ export default {
const studyId = this.studyList[studyIndex].StudyId
if (!studyId) return
this.activeStudy(studyId)
DicomEvent.$emit('activeSeries', this.studyList[studyIndex].SeriesList[this.activeSeriesIndex])
},
showMultiFrames(studyIndex, series, seriesIndex, instance) {
let obj = Object.assign({}, series)
@@ -203,7 +186,7 @@ export default {
obj.ImageIds = imageIds
obj.SliceIndex = 0
this.$emit('showMultiFrame', obj)
},
getPreviousOrNextSeries(type, series) {
const seriseList = this.studyList.map(s => s.SeriesList).flat()
@@ -224,13 +207,14 @@ export default {
}
</script>
<style lang="scss" scoped>
.study-wrapper{
width:100%;
.study-wrapper {
width: 100%;
height: 100%;
overflow-y: hidden;
overflow-x: hidden;
display: flex;
flex-direction: column;
.study-info {
font-size: 16px;
font-weight: bold;
@@ -241,7 +225,8 @@ export default {
background-color: #4c4c4c;
height: 50px;
}
.dicom-desc{
.dicom-desc {
font-weight: bold;
font-size: 13px;
text-align: left;
@@ -258,19 +243,22 @@ export default {
touch-action: auto;
overflow-y: auto;
}
.series-active {
background-color: #607d8b!important;
border: 1px solid #607d8b!important;
background-color: #607d8b !important;
border: 1px solid #607d8b !important;
}
::v-deep.el-progress__text{
::v-deep .el-progress__text{
color: #ccc;
font-size: 12px;
}
.dicom-list-container{
.dicom-list-container {
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
.series-wrapper {
width: 100%;
padding: 5px;
@@ -279,21 +267,26 @@ export default {
align-items: center;
cursor: pointer;
background-color: #3a3a3a;
.el-progress__text{
.el-progress__text {
display: none;
}
.el-progress-bar{
padding-right:0px;
.el-progress-bar {
padding-right: 0px;
}
.series-image {
width: 50px;
height: 50px;
}
.series-text {
flex: 1;
padding-left: 5px;
color: #ddd;
position: relative;
.text-desc {
width: 100px;
white-space: nowrap;
@@ -306,27 +299,31 @@ export default {
}
}
::v-deep.el-collapse{
::v-deep .el-collapse{
border: none;
.el-collapse-item{
background-color: #000!important;
.el-collapse-item {
background-color: #000 !important;
color: #ddd;
}
.el-collapse-item__content{
padding-bottom:0px;
background-color: #000!important;
.el-collapse-item__content {
padding-bottom: 0px;
background-color: #000 !important;
}
.el-collapse-item__header{
background-color: #000!important;
.el-collapse-item__header {
background-color: #000 !important;
color: #ddd;
border-bottom-color:#5a5a5a;
border-bottom-color: #5a5a5a;
padding-left: 5px;
// height: 50px;
line-height: 20px;
}
}
::v-deep .el-progress-bar__inner{
::v-deep .el-progress-bar__inner {
transition: width 0s ease;
}
}
@@ -14,6 +14,25 @@
<div v-if="imageInfo.total">Image: #{{ `${series.SliceIndex + 1}/${imageInfo.total}` }}</div>
<div>{{ series.Modality }}</div>
</div>
<div v-if="series && taskInfo && taskInfo.IsReadingTaskViewInOrder === 1" class="top-center-tool">
<div class="toggle-visit-container">
<div class="arrw_icon"
:style="{ cursor: series.TaskInfo && series.TaskInfo.VisitTaskNum !== 0 ? 'pointer' : 'not-allowed', color: series.TaskInfo && series.TaskInfo.VisitTaskNum !== 0 ? '#fff' : '#6b6b6b' }"
@click.stop.prevent="toggleTask($event, series.TaskInfo.VisitTaskNum, -1)"
@dblclick.stop="preventDefault($event)">
<i class="el-icon-caret-left" />
</div>
<div class="arrow_text">
{{ series.TaskInfo ? series.TaskInfo.TaskBlindName : '' }}
</div>
<div class="arrw_icon"
:style="{ cursor: series.TaskInfo && series.TaskInfo.VisitTaskNum < taskInfo.VisitNum ? 'pointer' : 'not-allowed', color: series.TaskInfo && series.TaskInfo.VisitTaskNum < taskInfo.VisitNum ? '#fff' : '#6b6b6b' }"
@click.stop.prevent="toggleTask($event, series.TaskInfo.VisitTaskNum, 1)"
@dblclick.stop="preventDefault($event)">
<i class="el-icon-caret-right" />
</div>
</div>
</div>
<div v-if="series" class="right-top-text">
<div>{{ series.Description }}</div>
</div>
@@ -68,6 +87,8 @@
<script>
import {
metaData,
volumeLoader,
setVolumesForViewports,
getRenderingEngine,
utilities as csUtils,
cache
@@ -75,10 +96,27 @@ import {
import * as cornerstoneTools from '@cornerstonejs/tools'
import { createImageIdsAndCacheMetaData } from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/createImageIdsAndCacheMetaData'
import setCtTransferFunctionForVolumeActor from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/setCtTransferFunctionForVolumeActor'
import { setPetColorMapTransferFunctionForVolumeActor } from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/setPetColorMapTransferFunctionForVolumeActor'
import { setCtMappingRange } from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/setCtTransferFunctionForVolumeActor'
import { vec3, mat4 } from 'gl-matrix'
import {
setPetTransferFunctionForVolumeActor
} from './helpers/index.js'
import DicomEvent from '@/views/trials/trials-panel/reading/dicoms/components/DicomEvent'
import {
renderSegmentation,
readingSegmentByConfig,
selectSegmentation,
selectSegment,
createSegmentationRepresentation,
viewSegmentation,
viewSegment,
jumpBidirectional,
viewBidirectional,
changeColor,
resetViewport
} from "./helpers/segmentations"
export default {
name: 'ImageViewport',
name: 'MPRViewport',
props: {
renderingEngineId: {
type: String,
@@ -92,11 +130,35 @@ export default {
type: Number,
required: true
},
MPRInfo: {
histogramVisible: {
type: Boolean,
default: false
},
actionConfiguration: {
type: Object,
default: () => {
return {}
}
},
SegmentConfig: {
type: Object,
default: () => {
return {}
}
},
curSegSeries: {
type: Object,
default: () => {
return {}
}
},
segmentIndex: {
type: Number,
default: 0
},
segmentationId: {
type: String,
default: ''
}
},
data() {
@@ -125,7 +187,9 @@ export default {
sliceThickness: null,
wwwc: null,
total: 0,
sliceThickness: 0
sliceThickness: 0,
imageOrientationPatient: [],
imagePositionPatient: []
},
digitPlaces: 2,
orientationMarkers: [],
@@ -148,6 +212,39 @@ export default {
this.$nextTick(() => {
this.initViewport()
})
DicomEvent.$on('createSegmentationRepresentation', (segmentationId) => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
createSegmentationRepresentation(this.viewportId, segmentationId)
})
DicomEvent.$on('viewSegmentation', (obj) => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
viewSegmentation(obj, this.viewportId)
})
DicomEvent.$on('viewSegment', (obj) => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
viewSegment(obj, this.viewportId)
})
DicomEvent.$on('jumpBidirectional', (obj) => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
jumpBidirectional(obj, this.viewportId, this.volumeId)
})
DicomEvent.$on('viewBidirectional', (obj) => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
viewBidirectional(obj, this.viewportId)
})
DicomEvent.$on('changeColor', (obj) => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
changeColor(obj, this.viewportId)
})
DicomEvent.$on('resetViewport', () => {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
resetViewport(this.viewportId)
})
DicomEvent.$on('renderSegmentation', async (viewportId) => {
// if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
if (this.viewportId !== viewportId) return false
await renderSegmentation(this.series, this.series.TaskInfo, this.viewportId, this.SegmentConfig, this.segmentationId, this.segmentIndex, this.renderingEngineId, null, this.actionConfiguration)
})
},
watch: {
MPRInfo: {
@@ -166,6 +263,25 @@ export default {
}
},
deep: true
},
SegmentConfig: {
handler() {
if (!this.segmentationId) return false
if (!this.series.TaskInfo) return false
readingSegmentByConfig(this.series, this.series.TaskInfo, this.viewportId, this.segmentationId, this.SegmentConfig)
},
deep: true
},
segmentIndex() {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
if (this.segmentIndex <= 0) return false
selectSegment(this.viewportId, this.segmentationId, this.segmentIndex)
},
segmentationId() {
if (this.curSegSeries.Id !== this.series.Id || this.curSegSeries.VisitTaskId !== this.series.VisitTaskId) return false
if (!this.segmentationId) return false
selectSegmentation(this.viewportId, this.segmentationId)
readingSegmentByConfig(this.series, this.series.TaskInfo, this.viewportId, this.segmentationId, this.SegmentConfig)
}
},
methods: {
@@ -181,7 +297,9 @@ export default {
// resizeObserver.observe(this.element)
this.element.addEventListener("CORNERSTONE_VOLUME_NEW_IMAGE", this.stackNewImage)
this.element.addEventListener('CORNERSTONE_VOI_MODIFIED', this.voiModified)
this.element.addEventListener('CORNERSTONE_IMAGE_RENDERED', this.imageRendered)
this.element.addEventListener('wheel', (e) => {
if (this.histogramVisible) return false
console.log('CORNERSTONE_STACK_VIEWPORT_SCROLL')
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
@@ -189,10 +307,10 @@ export default {
const totalImages = this.imageInfo.total;
//
if (currentImageIdIndex >= totalImages - 1) {
if (currentImageIdIndex >= totalImages - 1 && e.wheelDeltaY < 0) {
//
csUtils.jumpToSlice(viewport.element, { imageIndex: 0 });
} else if (currentImageIdIndex <= 0) {
} else if (currentImageIdIndex <= 0 && e.wheelDeltaY > 0) {
//
csUtils.jumpToSlice(viewport.element, { imageIndex: totalImages - 1 });
}
@@ -272,19 +390,31 @@ export default {
const zoom = viewport.getZoom()
this.imageInfo.zoom = zoom.toFixed(4)
let imageIds = viewport.getImageIds(this.volumeId)
let imageId = imageIds[0]
let volume = cache.getVolume(this.volumeId)
let { spacing } = volume
// if (this.series.orientation === 'AXIAL') imageId = viewport.getCurrentImageId()
let imageId = imageIds[detail.imageIndex]
if (imageId) {
this.$emit('setMPRInfo', { type: this.series.orientation, key: "imageNum", value: detail.numberOfSlices })
const imagePlaneModule = metaData.get('imagePlaneModule', imageId)
let type = this.determineImagePlane(imagePlaneModule.imageOrientationPatient)
this.imageInfo.location = type === this.series.orientation ? imagePlaneModule.sliceLocation : ''
this.imageInfo.sliceThickness = type === this.series.orientation ? spacing[2] : spacing[0]
this.imageInfo.imageOrientationPatient = imagePlaneModule.imageOrientationPatient
this.imageInfo.imagePositionPatient = imagePlaneModule.imagePositionPatient
this.imageInfo.size = `${imagePlaneModule.columns}*${imagePlaneModule.rows}`
this.imageInfo.location = imagePlaneModule.sliceLocation
this.imageInfo.total = detail.numberOfSlices
let type = this.determineImagePlane(imagePlaneModule.imageOrientationPatient)
let volume = cache.getVolume(this.volumeId)
let spacing = volume ? volume.spacing : []
this.imageInfo.sliceThickness = type === 'AXIAL' ? spacing[2] : spacing[0]
this.getOrientationMarker()
let properties = viewport.getProperties(this.volumeId)
if (this.series && this.series.Id) {
let annotations = cornerstoneTools.annotation.state.getAllAnnotations().filter(item => item.metadata.toolName !== 'ScaleOverlay' && item.metadata.volumeId !== this.volumeId && !item.metadata.segmentationId && item.seriesId !== this.series.Id)
annotations.forEach(item => {
cornerstoneTools.annotation.state.removeAnnotation(item.annotationUID)
})
}
this.$emit('renderAnnotations', this.series)
let properties = viewport.getProperties()
if (this.isFusion) {
properties = viewport.getProperties(this.ptVolumeId)
}
if (properties && properties.voiRange) {
var { lower, upper } = properties.voiRange
const windowWidth = upper - lower
@@ -293,15 +423,15 @@ export default {
this.defaultWindowLevel.windowCenter = windowCenter
this.imageInfo.wwwc = `${Math.round(windowWidth)}/${Math.round(windowCenter)}`
}
const toolGroupId = this.viewportId
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId)
toolGroup.setToolEnabled('ScaleOverlay')
}
// const toolGroupId = this.viewportId
const toolGroupId = 'share-viewport-volume'
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroup(toolGroupId)
toolGroup.setToolEnabled('ScaleOverlay')
},
setFullScreen(index) {
setTimeout(() => {
this.series.SliceIndex = index
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(
this.viewportId
@@ -324,6 +454,22 @@ export default {
this.$emit('upperRangeChange', Math.round(windowWidth))
}
},
imageRendered(e) {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
const properties = viewport.getProperties()
if (properties && properties.voiRange) {
var { lower, upper } = properties.voiRange
const { windowWidth, windowCenter } = csUtils.windowLevel.toWindowLevel(
lower,
upper
)
this.imageInfo.wwwc = `${Math.round(windowWidth)}/${Math.round(windowCenter)}`
}
const zoom = viewport.getZoom()
this.imageInfo.zoom = zoom.toFixed(4)
},
getOrientationMarker() {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
@@ -462,11 +608,28 @@ export default {
},
async createImageIdsAndCacheMetaData(obj) {
this.loading = true
await createImageIdsAndCacheMetaData({
modality: obj.Modality,
imageIds: obj.ImageIds
try {
return await createImageIdsAndCacheMetaData({
modality: obj.Modality,
imageIds: obj.ImageIds
})
} finally {
this.loading = false
}
},
async getVolume(serie, isFusion = false) {
return new Promise(async res => {
let volumeId = `${isFusion ? 'fusion_' : ''}` + serie.SeriesInstanceUid;
let volume = null;
if (cache.getVolume(volumeId)) {
volume = cache.getVolume(volumeId)
} else {
let imageIds = await this.createImageIdsAndCacheMetaData(serie)
volume = await volumeLoader.createAndCacheVolume(volumeId, { imageIds: imageIds })
volume.load()
}
res({ volumeId, volume })
})
this.loading = false
},
async setSeriesInfo(obj, isLocate = false) {
try {
@@ -474,45 +637,96 @@ export default {
if (this.series && data.Id === this.series.Id && data.Description === this.series.Description && !isLocate && !data.isLocation) {
data.SliceIndex = this.series.SliceIndex
}
// console.log(data)
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
if (isLocate) return csUtils.jumpToSlice(viewport.element, { imageIndex: data.SliceIndex });
this.series = {}
this.volumeId = data.SeriesInstanceUid
let res = await this.getVolume(obj)
this.volumeId = res.volumeId
this.series = { ...data }
viewport
.setVolumes([{
volumeId: this.volumeId, callback: (r) => {
if (this.series.Modality === 'PT') {
setPetColorMapTransferFunctionForVolumeActor(r, true)
if (this.series.Modality === 'PT' || this.series.Modality === 'NM') {
setPetTransferFunctionForVolumeActor(r)
// viewport.setProperties({ voiRange: { upper: 5, lower: 0 } })
} else {
const voi = metaData.get('voiLutModule', res.volume._imageIds[Math.ceil((res.volume._imageIds.length - 1) / 2)])
setCtMappingRange(voi.windowWidth[0], voi.windowCenter[0])
setCtTransferFunctionForVolumeActor(r)
}
console.log("渲染成功")
}
}]).then(res => {
}]).then(r => {
if (data.isLocation) {
setTimeout(() => { csUtils.jumpToSlice(viewport.element, { imageIndex: data.SliceIndex }); })
}
})
viewport.render()
if (this.series.Modality === 'PT' || this.series.Modality === 'NM') {
setTimeout(() => {
viewport.resetCamera({ resetPan: true, resetZoom: true, resetToCenter: true })
viewport.resetProperties()
viewport.setProperties({ voiRange: { upper: 5, lower: 0 } })
viewport.render()
renderingEngine.render()
}, 100)
}
await renderSegmentation(this.series, this.series.TaskInfo, this.viewportId, this.SegmentConfig, this.segmentationId, this.segmentIndex, this.renderingEngineId, data.segment, this.actionConfiguration)
} catch (e) {
console.log(e)
}
},
cornerstoneToolsMouseMove(e) {
const { currentPoints } = e.detail
const worldPoint = currentPoints.world
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
const imageData = viewport.getImageData()
if (!imageData) return
const index = imageData.imageData.worldToIndex(worldPoint)
let referencedImageId = viewport.getCurrentImageId()
const data = viewport.getImageData()
if (!data) return
const { dimensions, imageData, metadata, voxelManager } = data
const index = imageData.worldToIndex(worldPoint)
index[0] = Math.floor(index[0])
index[1] = Math.floor(index[1])
index[2] = Math.floor(index[2])
this.mousePosition.index = index
const modality = metadata.Modality
let ijk = csUtils.transformWorldToIndex(imageData, worldPoint)
ijk = vec3.round(ijk, ijk);
if (csUtils.indexWithinDimensions(ijk, dimensions)) {
this.isHandleOutsideImage = false
let value = voxelManager.getAtIJKPoint(ijk)
ijk[2] = viewport.getCurrentImageIdIndex()
let modalityUnit
if (modality === 'US') {
const calibratedResults = cornerstoneTools.utilities.getCalibratedProbeUnitsAndValue(image, [ijk])
const hasEnhancedRegionValues = calibratedResults.values.every(
(value) => value !== null
)
value = (hasEnhancedRegionValues ? calibratedResults.values : value)
modalityUnit = hasEnhancedRegionValues
? calibratedResults.units
: 'raw';
} else {
const scalingModule = referencedImageId && metaData.get('scalingModule', referencedImageId)
const isSuvScaled = typeof scalingModule?.suvbw === 'number'
if (scalingModule && scalingModule.suvbw) {
const pixelUnitsOptions = {
isPreScaled: cornerstoneTools.utilities.viewport.isViewportPreScaled(viewport, referencedImageId),
isSuvScaled: isSuvScaled,
}
modalityUnit = cornerstoneTools.utilities.getPixelValueUnits(
modality,
referencedImageId,
pixelUnitsOptions
);
}
}
this.mousePosition.value = value
this.mousePosition.modalityUnit = modalityUnit
}
},
toggleTask(evt, visitTaskNum, i) {
this.$emit('activeViewport', this.viewportIndex)
@@ -629,7 +843,7 @@ export default {
},
computed: {
NSTip() {
return `NS: ${this.$store.state.trials.uploadTip}`
return `NS: ${this.$store.state.trials.downloadTip}`
}
},
}
@@ -30,8 +30,8 @@
</div>
</div>
</div>
<div style="margin-left:-1px;border: 1px solid #424242;">
<el-input v-model="range" size="mini" style="width:120px" maxlength="3"
<div v-if="modality !== 'NM'" style="margin-left:-1px;border: 1px solid #424242;">
<el-input v-model="range" size="mini" style="width:120px" :maxlength="maxLength"
oninput="if(value){value=value.replace(/[^\d]/g,'')} if(value<=0){value=''}"
@change="upperRangeChange">
<template slot="append">g/ml</template>
@@ -54,12 +54,24 @@ import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/C
const { registerColormap, getColormapNames, getColormap } = csUtils.colormap
export default {
name: "colorMap",
props: {
modality: {
type: String,
default: ''
},
maxLength: {
type: [Number, String],
default: 6
}
},
data() {
return {
colorMaps: [],
rgbPresetName: 'siemens',
range: 40,
upper: 6,
isSlideMoving: false,
req: null
}
},
mounted() {
@@ -72,9 +84,24 @@ export default {
this.renderColorMaps()
this.upperRangeChange(this.range)
this.initSlider()
// this.syncSliderPosition()
})
},
methods: {
syncSliderPosition(retry = 0) {
var sliderBox = document.getElementById('sliderBox')
var container = document.getElementById('colorBarCanvas')
if (!sliderBox || !container) return
var maxLeft = container.clientWidth - sliderBox.clientWidth
if (maxLeft <= 0) {
if (retry < 30) {
requestAnimationFrame(() => this.syncSliderPosition(retry + 1))
}
return
}
this.updateSliderPosition()
},
renderColorMaps() {
this.createColorBar(this.rgbPresetName, 'colorBarCanvas', 256, 15)
this.colorMaps.forEach((e, index) => {
@@ -84,61 +111,127 @@ export default {
voiChange(v) {
this.$emit('voiChange', v)
},
changeVoi(v) {
//
if (v === this.upper) return
var range = Number(this.range) || 0
var upper = Number(v) || 0
if (range <= 0) {
upper = 0
} else if (upper > range) {
upper = range
} else if (upper < 0) {
upper = 0
}
this.upper = upper
this.syncSliderPosition()
},
initSlider() {
var slider = document.getElementById('slider')
var sliderBox = document.getElementById('sliderBox')
var container = document.getElementById('colorBarCanvas')
if (!slider || !sliderBox || !container) return
slider.addEventListener('mousedown', () => {
this.isSlideMoving = true
})
document.addEventListener('mousemove', (e) => {
if (this.isSlideMoving) {
var containerWidth = container.clientWidth
var sliderWidth = sliderBox.clientWidth
var maxLeft = containerWidth - sliderWidth
var left = e.clientX - container.getBoundingClientRect().left
var position = null
position = left
if (left < 0) {
left = 6
position = 0
} else if (left > maxLeft) {
left = maxLeft + 6
position = maxLeft
}
if (this.req) return;
this.req = requestAnimationFrame(() => {
this.req = null;
var containerWidth = container.clientWidth
var sliderWidth = sliderBox.clientWidth
var maxLeft = containerWidth - sliderWidth
if (maxLeft <= 0) return
slider.style.left = left + 'px'
var positionValue = document.getElementById('slider-position')
var upper = this.range
position = parseInt((position / maxLeft) * upper)
positionValue.textContent = position
this.upper = position
this.voiChange(position)
var left = e.clientX - container.getBoundingClientRect().left
if (left < 0) {
left = 0
} else if (left > maxLeft) {
left = maxLeft
}
var position = left
slider.style.left = (left + 6) + 'px'
var positionValue = document.getElementById('slider-position')
var upper = Number(this.range) || 0
position = parseInt((position / maxLeft) * upper)
if (position > upper) position = upper
if (position < 0) position = 0
if (this.modality === 'NM') {
positionValue.textContent = upper > 0 ? Math.round((position / upper) * 100) + '%' : '0%'
} else {
positionValue.textContent = position
}
this.upper = position
this.voiChange(position)
});
}
})
document.addEventListener('mouseup', () => {
this.isSlideMoving = false
})
},
upperRangeChange(v) {
if (v === 0 || v < this.upper) {
return
}
updateSliderPosition() {
var sliderBox = document.getElementById('sliderBox')
var container = document.getElementById('colorBarCanvas')
if (!sliderBox || !container) return
var containerWidth = container.clientWidth
var sliderWidth = sliderBox.clientWidth
var maxLeft = containerWidth - sliderWidth
var left = (this.upper / this.range) * maxLeft
if (maxLeft <= 0) return
var range = Number(this.range) || 0
var upper = Number(this.upper) || 0
if (range <= 0) {
upper = 0
} else if (upper > range) {
upper = range
} else if (upper < 0) {
upper = 0
}
if (upper !== this.upper) {
this.upper = upper
}
var left = range > 0 ? (upper / range) * maxLeft : 0
if (left < 0) {
left = 6
left = 0
} else if (left >= maxLeft) {
left = maxLeft + 6
left = maxLeft
}
var slider = document.getElementById('slider')
slider.style.left = left + 'px'
if (slider) {
slider.style.left = (left + 6) + 'px'
}
var positionValue = document.getElementById('slider-position')
positionValue.textContent = this.upper
if (positionValue) {
if (this.modality === 'NM') {
positionValue.textContent = range > 0 ? Math.round((upper / range) * 100) + '%' : '0%'
} else {
positionValue.textContent = upper
}
}
},
upperRangeChange(v) {
if (this.upper === v) return
var nextRange = Number(v) || 0
if (nextRange <= 0) {
this.upper = 0
this.updateSliderPosition()
this.voiChange(0)
return
}
this.range = nextRange
if (this.upper > nextRange) {
this.upper = nextRange
this.voiChange(this.upper)
}
this.syncSliderPosition()
},
createColorBar(rgbPresetName, elId, width, height) {
var colorMap = null
@@ -185,6 +278,7 @@ export default {
init() {
this.createColorBar(this.rgbPresetName, 'colorBarCanvas', 256, 15)
this.$emit("setColorMap", this.rgbPresetName)
this.syncSliderPosition()
}
}
}
@@ -322,4 +416,4 @@ export default {
color: #ddd;
}
}
</style>
</style>
@@ -59,6 +59,7 @@
:parentQsId="parentQsId" :isBaseline="isBaseline" :reading-task-state="readingTaskState"
:question-form="QuestionsForm" :visit-task-id="visitTaskId" :criterion-id="criterionId"
:type="addOrEdit.type" :calculationList="calculationList" :questionsMarkStatus="questionsMarkStatus"
:questionsSegmentMarkStatus="questionsSegmentMarkStatus"
@formItemTableNumberChange="formItemTableNumberChange" @resetFormItemData="resetTableFormItemData"
@setFormItemData="setFormTableItemData" @operateImageMarker="operateImageMarker" @save="save"
@handleReadingChart="handleReadingChart" />
@@ -183,7 +184,49 @@
<el-input v-if="question.Type === 'increment'" v-model="questionForm[question.Id]" disabled />
<!-- 数值 -->
<template v-if="question.Type === 'number' && (question.ImageMarkEnum === 1 || question.ImageMarkEnum === 2)">
<div style="display: flex;flex-direction: row;justify-content: flex-start;align-items: center;">
<div style="display: flex;flex-direction: row;justify-content: flex-start;align-items: center;"
v-if="question.ImageMarkTypeEnum === 1">
<el-input type="text" @change="(val) => { formItemNumberChange(val, question) }"
@input="numberInput(question.Id)"
@blur="questionsSegmentMarkStatus[question.Id] ? () => { } : handleMarkedQsBlur(questionForm[question.Id], questionForm, question.Id, question)"
v-model="questionForm[question.Id]"
:title="questionsSegmentMarkStatus[question.Id] ? `${questionsSegmentMarkStatus[question.Id].SegmentationName}\n${questionsSegmentMarkStatus[question.Id].SegmentName}\n${questionForm[question.Id]}` : question.Remark"
:disabled="(questionsSegmentMarkStatus[question.Id] && question.ImageMarkEnum === 2) || question.ImageMarkEnum === 1 || readingTaskState === 2"
style="width: 150px;margin-right: 5px;">
<template v-if="question.Unit !== 0" slot="append">
{{ question.Unit !== 4 ? $fd('ValueUnit', question.Unit) : question.CustomUnit }}
</template>
</el-input>
<svg-icon v-if="question.ShowChartTypeEnum > 0 && taskInfo.IsReadingTaskViewInOrder === 1"
icon-class="readingChart" class="svg-icon svg-readingChart" @click.stop="(e) => handleReadingChart({
e,
data: {
QuestionId: question.Id,
QuestionName: question.QuestionName
}
})" />
<!-- 绑定 -->
<el-button v-if="readingTaskState < 2 && (!questionsSegmentMarkStatus[question.Id])" size="mini" type="text"
@click="operateImageMarker({ operateStateEnum: 21, question })">
{{ $t('dicom3D:CustomizeQuestionFormItem:button:bind') }}
</el-button>
<!-- 更改 -->
<el-button v-if="readingTaskState < 2 && (questionsSegmentMarkStatus[question.Id])" size="mini" type="text"
@click="operateImageMarker({ operateStateEnum: 22, question })">
{{ $t('dicom3D:CustomizeQuestionFormItem:button:edit') }}
</el-button>
<!-- 查看 -->
<el-button v-if="questionsSegmentMarkStatus[question.Id]" size="mini" type="text"
@click="operateImageMarker({ operateStateEnum: 23, question })">
{{ $t('dicom3D:CustomizeQuestionFormItem:button:view') }}
</el-button>
<!-- 移除 -->
<el-button v-if="readingTaskState < 2 && (questionsSegmentMarkStatus[question.Id])" size="mini" type="text"
@click="operateImageMarker({ operateStateEnum: 24, question })">
{{ $t('dicom3D:CustomizeQuestionFormItem:button:remove') }}
</el-button>
</div>
<div style="display: flex;flex-direction: row;justify-content: flex-start;align-items: center;" v-else>
<el-input type="text" @change="(val) => { formItemNumberChange(val, question) }"
@input="numberInput(question.Id)"
@blur="questionsMarkStatus[question.Id] && questionsMarkStatus[question.Id].isMarked ? () => { } : handleMarkedQsBlur(questionForm[question.Id], questionForm, question.Id, question)"
@@ -312,9 +355,10 @@
:isNoneDicom="isNoneDicom" :reading-task-state="readingTaskState" :question-form="questionForm"
:visit-task-id="visitTaskId" :criterion-id="criterionId" :calculationList="calculationList"
:questionMarkInfoList="questionMarkInfoList" :questionsMarkStatus="questionsMarkStatus"
@formItemNumberChange="formItemNumberChange" @setFormItemData="setFormItemData"
@resetFormItemData="resetFormItemData" @getQuestions="getQuestions" @operateImageMarker="operateImageMarker"
@unBindAnnotationToQuestion="unBindAnnotationToQuestion" @handleReadingChart="handleReadingChart" />
:questionsSegmentMarkStatus="questionsSegmentMarkStatus" @formItemNumberChange="formItemNumberChange"
@setFormItemData="setFormItemData" @resetFormItemData="resetFormItemData" @getQuestions="getQuestions"
@operateImageMarker="operateImageMarker" @unBindAnnotationToQuestion="unBindAnnotationToQuestion"
@handleReadingChart="handleReadingChart" @saveSegmentBindingAndAnswer="saveSegmentBindingAndAnswer" />
</template>
<!-- <base-model :config="addOrEdit"
@@ -397,6 +441,12 @@ export default {
return {}
}
},
questionsSegmentMarkStatus: {
type: Object,
default() {
return {}
}
},
isNoneDicom: {
type: Boolean,
default: false
@@ -556,8 +606,65 @@ export default {
this.operateImageMarker({ operateStateEnum, question: this.question, picturePath })
}
})
DicomEvent.$on('setTableQuestionAnswer', async (DATA) => {
let { id, answer, ParentQsId, data } = DATA
// console.log(ParentQsId, this.question.Id)
if (this.question.Id === ParentQsId) {
this.QuestionsForm[id] = answer
if (data.RowId) {
let i = this.AnswersList.findIndex(i => i.RowId === this.QuestionsForm.RowId)
this.AnswersList[i][id] = this.QuestionsForm[id]
this.$emit('setFormItemData', { key: this.question.Id, val: this.AnswersList, question: this.question })
this.formItemNumberChange(this.question.Id, true)
this.$emit('saveSegmentBindingAndAnswer', [data])
} else {
try {
// loading = this.$loading({ fullscreen: true })
let answers = []
let reg = new RegExp(/^[0-9a-zA-Z]{8}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{12}$/)
for (const k in this.QuestionsForm) {
if (reg.test(k)) {
if (answers.findIndex(i => i.tableQuestionId === k) === -1) {
answers.push({ tableQuestionId: k, answer: this.QuestionsForm[k] })
}
}
}
this.QuestionsList.forEach(k => {
if (reg.test(k.Id)) {
if (answers.findIndex(i => i.tableQuestionId === k.Id) === -1) {
answers.push({ tableQuestionId: k.Id, answer: '' })
}
}
})
let params = {
questionId: this.question.Id,
RowIndex: this.questionForm[this.question.Id].length + 1,
RowId: this.QuestionsForm.RowId ? this.QuestionsForm.RowId : '',
visitTaskId: this.visitTaskId,
trialId: this.$route.query.trialId,
answerList: answers
}
let res = await submitTableQuestion(params)
if (res.IsSuccess) {
this.QuestionsForm.RowId = res.Result.RowId
obj.rowId = res.Result.RowId
data.RowId = res.Result.RowId
this.AnswersList.push(this.QuestionsForm)
this.$emit('setFormItemData', { key: this.question.Id, val: this.AnswersList, question: this.question })
this.formItemNumberChange(this.question.Id, true)
this.$emit('saveSegmentBindingAndAnswer', [data])
}
} catch (e) {
console.log(e)
}
}
}
})
},
methods: {
saveSegmentBindingAndAnswer(list) {
this.$emit('saveSegmentBindingAndAnswer', list)
},
handleReadingChart(row) {
this.$emit('handleReadingChart', row)
},
@@ -1349,13 +1456,13 @@ export default {
getAnnotationProp(annotation, prop) {
if (!annotation) return
let referencedImageId = null
if (annotation.from) {
referencedImageId = `${annotation?.metadata?.volumeId}?sliceIndex=${annotation?.metadata?.sliceIndex}&viewPlaneNormal=${annotation?.metadata?.viewPlaneNormal.map(i => i == 0 ? 0 : i).join(',')}`
if (annotation.from || annotation.metadata.volumeId) {
referencedImageId = `${annotation?.metadata?.volumeId}?sliceIndex=${annotation?.metadata?.sliceIndex}&viewPlaneNormal=${annotation?.metadata?.viewPlaneNormal.map(i => i == 0 ? Number(0).toFixed(3) : Number(i).toFixed(3)).join(',')}`
} else {
referencedImageId = annotation?.metadata?.referencedImageId
}
if (!referencedImageId) return null
const cacheKey = annotation.from ? `volumeId:${referencedImageId}` : `imageId:${referencedImageId}`
const cacheKey = annotation.from || annotation.metadata.volumeId ? `volumeId:${referencedImageId}` : `imageId:${referencedImageId}`
const points = ['x', 'y', 'z'];
const cachedStats = annotation.markTool === "ArrowAnnotate" ? annotation.data?.handles?.points[0] : annotation.data?.cachedStats?.[cacheKey]
const hasProp = cachedStats
@@ -19,9 +19,10 @@
:isNoneDicom="isNoneDicom" :question="question" :question-form="questionForm"
:reading-task-state="readingTaskState" :criterion-id="criterionId" :calculation-list="calculationList"
:question-mark-info-list="questionMarkInfoList" :questions-mark-status="questionsMarkStatus"
:is-baseline="isBaseLineTask" @resetFormItemData="resetFormItemData" @setFormItemData="setFormItemData"
@getQuestions="getQuestions" @operateImageMarker="operateImageMarker"
@unBindAnnotationToQuestion="unBindAnnotationToQuestion" @handleReadingChart="handleReadingChart" />
:questionsSegmentMarkStatus="questionsSegmentMarkStatus" :is-baseline="isBaseLineTask"
@resetFormItemData="resetFormItemData" @setFormItemData="setFormItemData" @getQuestions="getQuestions"
@operateImageMarker="operateImageMarker" @unBindAnnotationToQuestion="unBindAnnotationToQuestion"
@handleReadingChart="handleReadingChart" @saveSegmentBindingAndAnswer="saveSegmentBindingAndAnswer" />
</template>
<el-form-item v-if="readingTaskState < 2">
@@ -58,7 +59,7 @@
<script>
import { getCustomTableQuestionAnswer, changeDicomReadingQuestionAnswer, submitVisitTaskQuestionsInDto, verifyVisitTaskQuestions, getQuestionCalculateRelation, saveTaskQuestion } from '@/api/trials'
import { setSkipReadingCache, resetReadingTask, saveTableQuestionMark, saveAnswerAndBindingNoneDicomMark, changePlottingScaleChangeAnswer } from '@/api/reading'
import { setSkipReadingCache, resetReadingTask, saveTableQuestionMark, saveAnswerAndBindingNoneDicomMark, changePlottingScaleChangeAnswer, getSegmentBindingList, saveSegmentBindingAndAnswer, getSegmentList, getSegmentationList } from '@/api/reading'
import const_ from '@/const/sign-code'
import QuestionFormItem from './QuestionFormItem'
import SignForm from '@/views/trials/components/newSignForm'
@@ -116,6 +117,7 @@ export default {
imageTool: '',
imageToolAttribute: '',
questionsMarkStatus: {},
questionsSegmentMarkStatus: {},
digitPlaces: 2,
questionImageToolAttributeInfo: {},
unSaveTargets: [],
@@ -133,6 +135,7 @@ export default {
this.digitPlaces = digitPlaces === -1 ? this.digitPlaces : digitPlaces
this.getQuestionCalculateRelation()
this.getQuestions(true)
this.initSegmentBinding()
DicomEvent.$on('opentableCol', (data) => {
let { visible } = data
this.isTableVisible = visible
@@ -433,8 +436,13 @@ export default {
REMOVE: 4, //
SAVE_OUTER: 5, //
UPDATE: 6, //
SAVE_TABLE: 7 //
SAVE_TABLE: 7, //
BIND_SEGMENT: 21, //
CHANGE_SEGMENT: 22, //
VIEW_SEGMENT: 23, //
REMOVE_SEGMENT: 24, //
}
// console.log(obj, 'obj')
const { question, operateStateEnum, rowId, answer } = obj
const { Id, IsTableQuestion, ImageTool, ImageToolAttribute, ParentQsId, RowId, QuestionName, QuestionEnName } = question
@@ -446,7 +454,7 @@ export default {
operateQuestionId: Id,
operateQuestionName: this.isEN ? QuestionEnName : QuestionName,
operateRowId: RowId,
operateParentQsId: ParentQsId
operateParentQsId: ParentQsId,
})
const stateHandlers = {
[STATE.BIND]: () => this.$emit('setReadingToolPassive'),
@@ -456,12 +464,182 @@ export default {
[STATE.REMOVE]: this.handleRemoveAnnotation,
[STATE.SAVE_OUTER]: this.isNoneDicom ? this.handleSaveNoneDicomOuterQuestions : this.handleSaveOuterQuestions,
[STATE.UPDATE]: this.handleUpdateValue,
[STATE.SAVE_TABLE]: this.isNoneDicom ? this.handleSaveNoneDicomTableQuestions : this.handleSaveTableQuestions
[STATE.SAVE_TABLE]: this.isNoneDicom ? this.handleSaveNoneDicomTableQuestions : this.handleSaveTableQuestions,
[STATE.BIND_SEGMENT]: this.handleBindSegment,
[STATE.CHANGE_SEGMENT]: this.handleBindSegment,
[STATE.VIEW_SEGMENT]: this.handleViewSegment,
[STATE.REMOVE_SEGMENT]: this.handleRemoveSegment,
}
const handler = stateHandlers[operateStateEnum]
handler && await handler.call(this, obj)
},
async handleViewSegment(obj) {
try {
let o = {}
if (this.isTableQuestion) {
o.TableQuestionId = this.operateQuestionId
o.RowId = this.operateRowId
} else {
o.QuestionId = this.operateQuestionId
}
let list = await this.getSegmentBindingList(o)
if (list && list.length > 0) {
let segment = await this.getSegmentList(list[0].SegmentId)
if (segment[0].SegmentJson) {
let obj = JSON.parse(segment[0].SegmentJson)
segment[0].stats = obj.stats
segment[0].bidirectional = obj.bidirectional
segment[0].segmentationId = segment[0].SegmentationId
segment[0].segmentIndex = segment[0].SegmentNumber
}
let segmentGroup = await this.getSegmentationList(list[0].SegmentationId)
this.$emit('viewCustomAnnotationSeries', {
visitTaskId: this.visitTaskId,
segment: segment[0],
segmentGroup: segmentGroup[0]
})
}
} catch (err) {
console.log(err)
}
},
handleBindSegment(obj) {
this.$emit("openSegmentForm", { visitInfo: this.visitInfo, operateStateEnum: obj.operateStateEnum, isTableQuestion: this.isTableQuestion, operateQuestionId: this.operateQuestionId, operateRowId: this.operateRowId })
},
handleSegmentSave(obj) {
let imageToolAttribute = this.imageToolAttribute
let answer = ''
if (imageToolAttribute === 'length' || imageToolAttribute === 'width') {
let s = {
length: "maxMajor",
width: 'maxMinor'
}
if (!obj.bidirectional || !obj.bidirectional.maxMajor) return this.$confirm(this.$t("segment:error:notValue"))
answer = obj.bidirectional[s[imageToolAttribute]] ? Number(obj.bidirectional[s[imageToolAttribute]]).toFixed(this.digitPlaces) : ''
} else {
if (!obj.stats) return this.$confirm(this.$t("segment:error:notValue"))
answer = obj.stats[imageToolAttribute] ? Number((obj.stats[imageToolAttribute]).value).toFixed(this.digitPlaces) : ''
}
let o = {
Answer: answer,
SegmentId: obj.Id,
SegmentationId: obj.SegmentationId,
QuestionId: this.isTableQuestion ? this.operateParentQsId : this.operateQuestionId,
RowId: this.isTableQuestion ? this.operateRowId : null,
TableQuestionId: this.isTableQuestion ? this.operateQuestionId : null,
VisitTaskId: this.visitTaskId,
}
if (this.isTableQuestion) {
return DicomEvent.$emit('setTableQuestionAnswer', { id: this.operateQuestionId, answer, ParentQsId: this.operateParentQsId, data: o })
} else {
this.$set(this.questionForm, this.operateQuestionId, answer)
}
this.saveSegmentBindingAndAnswer([o])
},
//
async handleRemoveSegment(obj) {
const { question } = obj
let confirm = await this.$confirm(this.$t('segment:confirm:sureDelete'))
if (!confirm) return false
this.$set(this.questionForm, question.Id, '')
let o = {
Answer: '',
SegmentId: null,
SegmentationId: null,
QuestionId: this.isTableQuestion ? this.operateParentQsId : this.operateQuestionId,
RowId: this.isTableQuestion ? this.operateRowId : null,
TableQuestionId: this.isTableQuestion ? this.operateQuestionId : null,
VisitTaskId: this.visitTaskId,
}
this.saveSegmentBindingAndAnswer([o])
},
//
async saveSegmentBindingAndAnswer(list) {
try {
let data = {
VisitTaskId: this.visitTaskId,
BindingList: list
}
let res = await saveSegmentBindingAndAnswer(data)
if (res.IsSuccess) {
this.initSegmentBinding()
}
} catch (err) {
console.log(err)
}
},
//
async initSegmentBinding() {
try {
let list = await this.getSegmentBindingList()
this.questionsSegmentMarkStatus = {}
list.forEach(item => {
if (item.TableQuestionId && item.RowId) {
this.$set(this.questionsSegmentMarkStatus, `${item.RowId}_${item.TableQuestionId}`, { SegmentId: item.SegmentId, SegmentationName: item.SegmentationName, SegmentName: item.SegmentName, })
} else {
this.$set(this.questionsSegmentMarkStatus, item.QuestionId, { SegmentId: item.SegmentId, SegmentationName: item.SegmentationName, SegmentName: item.SegmentName, })
}
})
} catch (err) {
console.log(err)
}
},
//
async getSegmentBindingList(param = {}) {
try {
let data = {
VisitTaskId: this.visitTaskId,
PageSize: 9999,
PageIndex: 1,
}
data = Object.assign(data, param)
let res = await getSegmentBindingList(data)
if (res.IsSuccess) {
return res.Result.CurrentPageData
}
} catch (err) {
console.log(err)
}
},
//
async getSegmentList(id) {
try {
let data = {
// SegmentationId: id,
PageSize: 9999,
PageIndex: 1,
}
if (id) data.Id = id
let res = await getSegmentList(data)
if (res.IsSuccess) {
return res.Result.CurrentPageData
}
} catch (err) {
console.log(err)
}
},
//
async getSegmentationList(id) {
try {
let data = {
VisitTaskId: this.visitInfo.VisitTaskId,
PageSize: 9999,
PageIndex: 1,
}
if (id) data.Id = id
let res = await getSegmentationList(data);
if (res.IsSuccess) {
return res.Result.CurrentPageData;
}
} catch (err) {
console.log(err)
}
},
async handleViewAnnotation(obj) {
const index = this.findMarkIndex(obj.question)
if (index === -1) return
@@ -1049,13 +1227,13 @@ export default {
getAnnotationProp(annotation, prop) {
if (!annotation || !prop) return
let referencedImageId = null
if (annotation.from) {
referencedImageId = `${annotation?.metadata?.volumeId}?sliceIndex=${annotation?.metadata?.sliceIndex}&viewPlaneNormal=${annotation?.metadata?.viewPlaneNormal.map(i => i == 0 ? 0 : i).join(',')}`
if (annotation.from || annotation.metadata.volumeId) {
referencedImageId = `${annotation?.metadata?.volumeId}?sliceIndex=${annotation?.metadata?.sliceIndex}&viewPlaneNormal=${annotation?.metadata?.viewPlaneNormal.map(i => i == 0 ? Number(0).toFixed(3) : Number(i).toFixed(3)).join(',')}`
} else {
referencedImageId = annotation?.metadata?.referencedImageId
}
if (!referencedImageId) return null
const cacheKey = annotation.from ? `volumeId:${referencedImageId}` : `imageId:${referencedImageId}`
const cacheKey = annotation.from || annotation.metadata.volumeId ? `volumeId:${referencedImageId}` : `imageId:${referencedImageId}`
const points = ['x', 'y', 'z']
const cachedStats = annotation.markTool === "ArrowAnnotate" ? annotation.data?.handles?.points[0] : annotation.data?.cachedStats?.[cacheKey]
const hasProp = cachedStats
@@ -107,7 +107,53 @@
<el-input v-if="question.Type === 'increment'" v-model="questionForm[question.Id]" disabled />
<!-- 数值 -->
<template v-if="question.Type === 'number' && (question.ImageMarkEnum === 1 || question.ImageMarkEnum === 2)">
<div style="display: flex;flex-direction: row;justify-content: flex-start;align-items: center;">
<div style="display: flex;flex-direction: row;justify-content: flex-start;align-items: center;"
v-if="question.ImageMarkTypeEnum === 1">
<el-input type="text" @change="(val) => { formItemNumberChange(val, question) }"
@input="numberInput(question.Id)"
@blur="questionsSegmentMarkStatus[rowId ? `${rowId}_${question.Id}` : question.Id] ? () => { } : handleMarkedQsBlur(questionForm[question.Id], questionForm, question.Id, question)"
v-model="questionForm[question.Id]"
:title="questionsSegmentMarkStatus[rowId ? `${rowId}_${question.Id}` : question.Id] ? `${questionsSegmentMarkStatus[rowId ? `${rowId}_${question.Id}` : question.Id].SegmentationName}\n${questionsSegmentMarkStatus[rowId ? `${rowId}_${question.Id}` : question.Id].SegmentName}\n${questionForm[question.Id]}` : question.Remark"
:disabled="(questionsSegmentMarkStatus[rowId ? `${rowId}_${question.Id}` : question.Id] && question.ImageMarkEnum === 2) || question.ImageMarkEnum === 1 || question.IsPreinstall"
style="width: 150px;margin-right: 5px;">
<template v-if="question.Unit !== 0" slot="append">
{{ question.Unit !== 4 ? $fd('ValueUnit', question.Unit) : question.CustomUnit }}
</template>
</el-input>
<svg-icon v-if="question.ShowChartTypeEnum > 0 && taskInfo.IsReadingTaskViewInOrder === 1"
icon-class="readingChart" class="svg-icon svg-readingChart" @click.stop="(e) => handleReadingChart({
e,
data: {
TableQuestionId: question.Id,
RowIndex: questionForm.RowIndex,
QuestionName: question.QuestionName
}
})" />
<!-- 绑定 -->
<el-button
v-if="readingTaskState < 2 && (!questionsSegmentMarkStatus[rowId ? `${rowId}_${question.Id}` : question.Id])"
size="mini" type="text" @click="operateImageMarker({ operateStateEnum: 21, question })">
{{ $t('dicom3D:CustomizeQuestionFormItem:button:bind') }}
</el-button>
<!-- 更改 -->
<el-button
v-if="readingTaskState < 2 && (questionsSegmentMarkStatus[rowId ? `${rowId}_${question.Id}` : question.Id])"
size="mini" type="text" @click="operateImageMarker({ operateStateEnum: 22, question })">
{{ $t('dicom3D:CustomizeQuestionFormItem:button:edit') }}
</el-button>
<!-- 查看 -->
<el-button v-if="questionsSegmentMarkStatus[rowId ? `${rowId}_${question.Id}` : question.Id]" size="mini"
type="text" @click="operateImageMarker({ operateStateEnum: 23, question })">
{{ $t('dicom3D:CustomizeQuestionFormItem:button:view') }}
</el-button>
<!-- 移除 -->
<el-button
v-if="readingTaskState < 2 && (questionsSegmentMarkStatus[rowId ? `${rowId}_${question.Id}` : question.Id])"
size="mini" type="text" @click="operateImageMarker({ operateStateEnum: 24, question })">
{{ $t('dicom3D:CustomizeQuestionFormItem:button:remove') }}
</el-button>
</div>
<div style="display: flex;flex-direction: row;justify-content: flex-start;align-items: center;" v-else>
<el-input type="text" @change="(val) => { formItemNumberChange(val, question) }"
@input="numberInput(question.Id)"
@blur="questionsMarkStatus[question.Id] && questionsMarkStatus[question.Id].isMarked ? () => { } : handleMarkedQsBlur(questionForm[question.Id], questionForm, question.Id, question)"
@@ -276,6 +322,12 @@ export default {
return {}
}
},
questionsSegmentMarkStatus: {
type: Object,
default() {
return {}
}
},
parentQsId: {
type: String,
default: ''
@@ -0,0 +1,8 @@
import setPetTransferFunctionForVolumeActor from "./setPetTransferFunctionForVolumeActor";
import setMipTransferFunctionForVolumeActor from "./setMipTransferFunctionForVolumeActor";
export {
setPetTransferFunctionForVolumeActor,
setMipTransferFunctionForVolumeActor
};
@@ -0,0 +1,430 @@
import * as cornerstoneTools from '@cornerstonejs/tools';
import * as cornerstone from "@cornerstonejs/core";
import dcmjs from '@/utils/dcmUpload/dcmjs'
import * as cornerstoneAdapters from "@cornerstonejs/adapters";
import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader'
import * as polySeg from '@cornerstonejs/polymorphic-segmentation'
cornerstoneTools.init({ addons: { polySeg } })
import { getSegmentationList, getSegmentList } from '@/api/reading'
import Vue from 'vue'
const {
ToolGroupManager,
Enums: csToolsEnums,
segmentation,
annotation,
LabelMapEditWithContourTool,
SegmentBidirectionalTool,
CrosshairsTool,
utilities: CStUtils,
} = cornerstoneTools;
const { MouseBindings, Events: toolsEvents } = csToolsEnums
const { segmentation: segmentationUtils } = CStUtils;
const { cache, getRenderingEngine, imageLoader, eventTarget, metaData, utilities: csUtils, volumeLoader } = cornerstone;
const { Cornerstone3D } = cornerstoneAdapters.adaptersSEG;
let viewportInfo = {}
let renderingEngineId = null
async function createSegmentation(toolGroupId, volumeId, segmentationId) {
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId) || ToolGroupManager.getToolGroup('share-viewport-volume')
toolGroup.setToolActive(
LabelMapEditWithContourTool.toolName,
);
if (!cache.getVolume(segmentationId)) {
await volumeLoader.createAndCacheDerivedLabelmapVolume(
volumeId,
{
volumeId: segmentationId
}
)
}
if (!segmentation.state.getSegmentation(segmentationId)) {
segmentation.addSegmentations([
{
segmentationId,
representation: {
type: cornerstoneTools.Enums.SegmentationRepresentations
.Labelmap,
data: {
volumeId: segmentationId
}
}
}
]);
}
}
async function createSegmentationRepresentation(viewportId, segmentationId) {
segmentation.addSegmentationRepresentations(viewportId, [
{
segmentationId,
type: csToolsEnums.SegmentationRepresentations.Labelmap,
},
])
}
async function readSegmentation(obj, series, segmentationId, isFile = false) {
let imageId = null
if (isFile) {
imageId = cornerstoneDICOMImageLoader.wadouri.fileManager.add(obj);
} else {
const imageIdObj = await cornerstoneDICOMImageLoader.wadouri.loadImage(`wadouri:${Vue.prototype.OSSclientConfig.basePath}${obj}`).promise
imageId = imageIdObj.imageId
}
const image = await imageLoader.loadAndCacheImage(imageId);
if (!image) {
return;
}
const instance = metaData.get("instance", imageId);
if (instance.Modality !== "SEG") {
console.error("This is not segmentation: " + file.name);
return;
}
const arrayBuffer = image.data.byteArray.buffer;
await loadSegmentation(arrayBuffer, series, segmentationId);
}
async function loadSegmentation(arrayBuffer, series, segmentationId) {
const generateToolState =
await Cornerstone3D.Segmentation.generateToolState(
series.ImageIds,
arrayBuffer,
metaData,
);
if (generateToolState.labelmapBufferArray.length !== 1) {
alert(
"Overlapping segments in your segmentation are not supported yet. You can turn on the skipOverlapping option but it will override the overlapping segments."
);
return;
}
// await createSegmentation(segmentationId);
let arr = []
generateToolState.segMetadata.data.forEach(item => {
if (item) {
let Target = JSON.parse(JSON.stringify(item))
arr.push(Target)
}
})
let mapping = {}
arr.forEach((item, index) => {
mapping[index + 1] = Number(item.SegmentNumber)
})
const megmentGroup =
segmentation.state.getSegmentation(segmentationId);
const { imageIds } = megmentGroup.representationData.Labelmap;
const derivedSegmentationImages = imageIds.map(imageId =>
cache.getImage(imageId)
);
const volumeScalarData = new Uint8Array(
generateToolState.labelmapBufferArray[0]
);
const remappedData = new Uint8Array(volumeScalarData.length);
for (let i = 0; i < volumeScalarData.length; i++) {
const value = volumeScalarData[i];
remappedData[i] = value === 0 ? 0 : (mapping[value] ? mapping[value] : value);
}
for (let i = 0; i < derivedSegmentationImages.length; i++) {
const voxelManager = derivedSegmentationImages[i].voxelManager;
const scalarData = voxelManager.getScalarData();
scalarData.set(
remappedData.slice(
i * scalarData.length,
(i + 1) * scalarData.length
)
);
voxelManager.setScalarData(scalarData);
}
}
function hex2Rgb(hexValue, alpha = 1) {
const rgx = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
const hex = hexValue.replace(rgx, (m, r, g, b) => r + r + g + g + b + b);
const rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!rgb) {
return hexValue;
}
const r = parseInt(rgb[1], 16),
g = parseInt(rgb[2], 16),
b = parseInt(rgb[3], 16);
return [r, g, b, alpha * 255];
}
function removeSegmentFromViewport(viewportId) {
return new Promise(resolve => {
if (viewportInfo[viewportId] && viewportInfo[viewportId].length > 0) {
viewportInfo[viewportId].forEach(async segmentationId => {
segmentation.removeSegmentation(segmentationId)
segmentation.state.removeSegmentation(segmentationId)
let annotations = annotation.state.getAllAnnotations().filter(item => item.metadata.segmentationId && segmentationId === item.metadata.segmentationId && item.metadata.segmentIndex && item.metadata.toolName === "SegmentBidirectional");
annotations.forEach(item => {
annotation.state.removeAnnotation(item.annotationUID)
})
})
}
viewportInfo[viewportId] = []
resolve(true)
})
}
function createSegmentConfiguration(segmentIndex, segmentationId, viewportId, actionConfiguration, otherSegments) {
const containedSegmentIndices = otherSegments
? { has: (segmentIndex) => otherSegments.indexOf(segmentIndex) !== -1 }
: undefined;
const colorConfig = segmentation.config.color.getSegmentIndexColor(
viewportId,
segmentationId,
segmentIndex
);
// Allow null style to skip style set
let color, activeColor;
if (colorConfig?.length) {
color = `rgb(${colorConfig.join(',')})`;
activeColor = color;
}
const style = {
color,
colorHighlightedActive: activeColor,
colorActive: activeColor,
textBoxColor: color,
textBoxColorActive: activeColor,
textBoxColorHighlightedActive: activeColor,
};
const label = otherSegments
? `Combined ${segmentIndex} with ${otherSegments.join(', ')}`
: `Segment ${segmentIndex}`;
actionConfiguration.contourBidirectional.data.segmentData.set(segmentIndex, {
containedSegmentIndices,
label,
style,
});
actionConfiguration.contourBidirectional.data.segmentationId = segmentationId
actionConfiguration.contourBidirectional.data.segmentIndex = segmentIndex
}
async function readingSegmentByConfig(series, visitInfo, viewportId, segmentationId, SegmentConfig) {
let data = {
SeriesId: series.Id,
VisitTaskId: visitInfo.VisitTaskId,
PageSize: 9999,
PageIndex: 1,
}
let res = await getSegmentationList(data);
if (res.IsSuccess) {
let list = res.Result.CurrentPageData;
changeInactiveSegmentShow(list, viewportId, segmentationId, SegmentConfig)
}
segmentation.config.style.setStyle(
{
type: csToolsEnums.SegmentationRepresentations.Labelmap,
},
{
renderFill: SegmentConfig.renderFill,
renderOutline: SegmentConfig.renderOutline,
outlineWidth: Number(SegmentConfig.outlineWidth),
fillAlpha: Number(SegmentConfig.fillAlpha),
}
)
}
function selectSegmentation(viewportId, segmentationId) {
segmentation.activeSegmentation.setActiveSegmentation(viewportId, segmentationId)
}
function selectSegment(viewportId, segmentationId, segmentIndex) {
selectSegmentation(viewportId, segmentationId)
segmentation.segmentIndex.setActiveSegmentIndex(segmentationId, segmentIndex);
}
async function changeInactiveSegmentShow(list, viewportId, segmentationId, SegmentConfig) {
let segmentList = list
segmentList.forEach(segment => {
segmentation.config.visibility.setSegmentationRepresentationVisibility(
viewportId,
{
segmentationId: segment.Id,
type: csToolsEnums.SegmentationRepresentations.Labelmap,
},
SegmentConfig.InactiveSegmentations.show
);
})
segmentation.config.visibility.setSegmentationRepresentationVisibility(
viewportId,
{
segmentationId: segmentationId,
type: csToolsEnums.SegmentationRepresentations.Labelmap,
},
true
);
let arr = []
for (let i = 0; i < segmentList.length; i++) {
let item = segmentList[i]
let params = {
SegmentationId: item.Id,
PageSize: 9999,
PageIndex: 1,
}
let r = await getSegmentList(params)
if (r.IsSuccess) {
let segments = r.Result.CurrentPageData
segments.forEach(s => {
let obj = {
segmentationId: item.Id,
segmentIndex: s.SegmentNumber,
view: SegmentConfig.InactiveSegmentations.show
}
if (item.Id === segmentationId) {
obj.view = true
}
arr.push(obj)
})
}
}
viewBidirectional(arr, viewportId)
}
function viewSegmentation(item, viewportId) {
segmentation.config.visibility.setSegmentationRepresentationVisibility(
viewportId,
{
segmentationId: item.segmentationId,
type: csToolsEnums.SegmentationRepresentations.Labelmap,
},
item.view
);
viewBidirectional(item.segments, viewportId)
}
async function jumpBidirectional(item, viewportId, volumeId) {
// DicomEvent.$emit('jumpBidirectional', item)
if (item.bidirectional) {
let an = annotation.state.getAllAnnotations().find(i => i.metadata.segmentationId === item.segmentationId && i.metadata.segmentIndex === item.segmentIndex && i.metadata.toolName === "SegmentBidirectional");
console.log(an, 'an')
if (!an) return false
const renderingEngine = getRenderingEngine(renderingEngineId)
const viewport = renderingEngine.getViewport(viewportId)
let key = Object.keys(an.data.cachedStats)[0]; // referencedImageId
if (key) {
let sliceIndex = key.split("?")[1].split("&")[0].split("=")[1]
csUtils.jumpToSlice(viewport.element, { imageIndex: sliceIndex });
} else {
const points = an.data.handles.points;
const worldPoint = points[0]; // 取一个点
let volume = cache.getVolume(volumeId)
let { imageData, numFrames } = volume
const ijk = imageData.worldToIndex(worldPoint);
const sliceIndex = Math.abs(Math.round(ijk[2]));
// console.log(sliceIndex, 'sliceIndex')
csUtils.jumpToSlice(viewport.element, { imageIndex: numFrames - sliceIndex - 1 });
}
}
}
function viewSegment(item, viewportId) {
segmentation.config.visibility.setSegmentIndexVisibility(viewportId, {
segmentationId: item.segmentationId,
type: csToolsEnums.SegmentationRepresentations.Labelmap,
}, item.segmentIndex, item.view)
viewBidirectional([item], viewportId)
}
function viewBidirectional(arr, viewportId) {
for (let j = 0; j < arr.length; j++) {
let item = arr[j]
let bidirectional = annotation.state.getAllAnnotations().find(i => i.metadata.segmentationId === item.segmentationId && i.metadata.segmentIndex === item.segmentIndex && i.metadata.toolName === "SegmentBidirectional");
// item.bidirectionalView = view
if (!bidirectional) continue
annotation.visibility.setAnnotationVisibility(bidirectional.annotationUID, item.view)
}
resetViewport(viewportId)
}
function resetViewport(viewportId) {
let renderingEngine = getRenderingEngine(renderingEngineId)
const viewport = renderingEngine.getViewport(viewportId)
viewport.render()
}
function changeColor(item, viewportId) {
segmentation.config.color.setSegmentIndexColor(viewportId, item.segmentationId, item.segmentIndex, hex2Rgb(item.color))
}
async function renderSegmentation(series, visitInfo, viewportId, SegmentConfig, segmentationId, segmentIndex, RenderingEngineId, Segment = null, actionConfiguration) {
try {
// console.log(segmentation, 'segmentation')
renderingEngineId = RenderingEngineId
await removeSegmentFromViewport(viewportId)
let data = {
SeriesId: series.Id,
VisitTaskId: visitInfo.VisitTaskId,
PageSize: 9999,
PageIndex: 1,
}
let res = await getSegmentationList(data);
if (res.IsSuccess) {
let list = res.Result.CurrentPageData;
for (let i = 0; i < list.length; i++) {
let item = list[i]
await createSegmentation(viewportId, series.SeriesInstanceUid, item.Id)
if (item.SEGUrl) await readSegmentation(item.SEGUrl, series, item.Id)
createSegmentationRepresentation(viewportId, item.Id)
if (!viewportInfo[viewportId]) {
viewportInfo[viewportId] = [item.Id]
} else {
viewportInfo[viewportId].push(item.Id)
}
let params = {
SegmentationId: item.Id,
PageSize: 9999,
PageIndex: 1,
}
let r = await getSegmentList(params)
if (r.IsSuccess) {
let segments = r.Result.CurrentPageData
segments.forEach(s => {
let SegmentJson = s.SegmentJson ? JSON.parse(s.SegmentJson) : {};
segmentation.segmentIndex.setActiveSegmentIndex(s.SegmentationId, s.SegmentNumber);
segmentation.config.color.setSegmentIndexColor(viewportId, s.SegmentationId, s.SegmentNumber, hex2Rgb(s.ColorRgb))
segmentation.segmentLocking.setSegmentIndexLocked(s.SegmentationId, s.SegmentNumber, true)
if (SegmentJson.bidirectional) {
let { majorAxis, minorAxis } = SegmentJson.bidirectional
// createSegmentConfiguration(s.SegmentNumber, s.SegmentationId, viewportId, actionConfiguration)
SegmentBidirectionalTool.hydrate(viewportId, [majorAxis, minorAxis], {
segmentIndex: s.SegmentNumber,
segmentationId: s.SegmentationId,
})
let an = annotation.state.getAllAnnotations().find(i => i.metadata.segmentationId === s.SegmentationId && i.metadata.segmentIndex === SegmentJson.bidirectional.segmentIndex && i.metadata.toolName === "SegmentBidirectional");
if (an) {
annotation.locking.setAnnotationLocked(an.annotationUID, true)
annotation.visibility.setAnnotationVisibility(an.annotationUID, item.bidirectionalView)
}
}
})
}
// this.$nextTick(() => {
// if (SEGMENT) {
// // console.log(SEGMENT, 'SEGMENT')
// return this.getBidirectional(obj.segments, SEGMENT)
// }
// this.getBidirectional(obj.segments, null, false)
// })
selectSegment(viewportId, segmentationId, segmentIndex)
if (Segment) {
jumpBidirectional(Segment, viewportId, series.SeriesInstanceUid)
}
}
}
readingSegmentByConfig(series, visitInfo, viewportId, segmentationId, SegmentConfig)
} catch (err) {
console.log(err)
}
}
export {
createSegmentation,
createSegmentationRepresentation,
readSegmentation,
renderSegmentation,
readingSegmentByConfig,
selectSegment,
selectSegmentation,
viewSegmentation,
viewSegment,
jumpBidirectional,
viewBidirectional,
changeColor,
resetViewport
}
@@ -0,0 +1,49 @@
import { cache, metaData, utilities } from "@cornerstonejs/core";
import vtkPiecewiseFunction from "@kitware/vtk.js/Common/DataModel/PiecewiseFunction";
export default function setMipTransferFunctionForVolumeActor({
volumeActor,
volumeId,
}) {
const mapper = volumeActor.getMapper?.();
if (mapper?.setSampleDistance) {
mapper.setSampleDistance(1.0);
}
const rgbTransferFunction = volumeActor
.getProperty()
.getRGBTransferFunction(0);
let range = null;
const imageVolume = volumeId ? cache.getVolume(volumeId) : null;
const imageId = imageVolume?.imageIds?.[0];
if (imageId) {
const voiLutModule = metaData.get("voiLutModule", imageId);
const rawCenter = Array.isArray(voiLutModule?.windowCenter)
? voiLutModule.windowCenter[0]
: voiLutModule?.windowCenter;
const rawWidth = Array.isArray(voiLutModule?.windowWidth)
? voiLutModule.windowWidth[0]
: voiLutModule?.windowWidth;
const center = Number(rawCenter);
const width = Number(rawWidth);
if (Number.isFinite(center) && Number.isFinite(width) && width > 0) {
const upper = center + width / 2;
range = [0, upper];
} else if (Number.isFinite(center)) {
range = [0, center];
}
}
if (!range) {
range = [0, 5];
}
rgbTransferFunction.setRange(range[0], range[1]);
utilities.invertRgbTransferFunction(rgbTransferFunction);
const upper = Number(range[1]);
if (Number.isFinite(upper) && upper > 0) {
const ofun = vtkPiecewiseFunction.newInstance();
ofun.addPoint(0, 0.0);
ofun.addPoint(upper * 0.02, 0.9);
ofun.addPoint(upper, 1.0);
volumeActor.getProperty().setScalarOpacity(0, ofun);
}
}
@@ -0,0 +1,39 @@
import vtkColorTransferFunction from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction";
import vtkPiecewiseFunction from "@kitware/vtk.js/Common/DataModel/PiecewiseFunction";
import { cache, metaData, utilities } from "@cornerstonejs/core";
const { getColormap } = utilities.colormap;
function getWindowCenterFromVolumeId(volumeId) {
if (!volumeId) return null;
const imageVolume = cache.getVolume?.(volumeId);
const imageId = imageVolume?.imageIds?.[0];
if (!imageId) return null;
const voiLutModule = metaData.get("voiLutModule", imageId);
const rawCenter = Array.isArray(voiLutModule?.windowCenter)
? voiLutModule.windowCenter[0]
: voiLutModule?.windowCenter;
const center = Number(rawCenter);
return Number.isFinite(center) ? center : null;
}
export default function setPetColorMapTransferFunctionForVolumeActor({
volumeActor,
volumeId,
preset,
}) {
const mapper = volumeActor.getMapper?.();
if (mapper?.setSampleDistance) {
mapper.setSampleDistance(1.0);
}
const cfun = vtkColorTransferFunction.newInstance();
const presetToUse = preset || getColormap("siemens");
cfun.applyColorMap(presetToUse);
const center = getWindowCenterFromVolumeId(volumeId);
const upper = center > 1 ? center : 5;
cfun.setMappingRange(1, upper);
volumeActor.getProperty().setRGBTransferFunction(0, cfun);
}
@@ -0,0 +1,11 @@
import { utilities } from '@cornerstonejs/core';
export default function setPetTransferFunction({ volumeActor }) {
const rgbTransferFunction = volumeActor
.getProperty()
.getRGBTransferFunction(0);
rgbTransferFunction.setRange(0, 5);
utilities.invertRgbTransferFunction(rgbTransferFunction);
}
File diff suppressed because it is too large Load Diff
@@ -2070,12 +2070,12 @@ export default {
.merge-table {
padding: 0 10px;
::v-deep.el-table {
::v-deep .el-table {
background-color: #1e1e1e !important;
color: #383838;
}
::v-deep.el-table td.el-table__cell,
::v-deep .el-table td.el-table__cell,
.el-table th.el-table__cell.is-leaf {
border-bottom: 1px solid #383838;
}
@@ -2086,7 +2086,7 @@ export default {
background-color: #1e1e1e;
}
::v-deep.el-table__header-wrapper {
::v-deep .el-table__header-wrapper {
th {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -2094,7 +2094,7 @@ export default {
}
}
::v-deep.el-table__body-wrapper {
::v-deep .el-table__body-wrapper {
tr {
background-color: #1e1e1e !important;
color: #dfdfdf;
@@ -2105,7 +2105,7 @@ export default {
}
}
::v-deep.el-table__empty-block {
::v-deep .el-table__empty-block {
background-color: #1e1e1e !important;
}
@@ -386,6 +386,35 @@ const config = {
'disabledReason': ''
},
],
'customizeStandardsSegmentDicom': [
// {
// 'name': 'Contour分割',
// 'icon': 'contour',
// 'toolName': 'Contour',
// 'props': ['length'],
// 'i18nKey': 'trials:reading:button:Contour',
// 'isDisabled': false,
// 'disabledReason': ''
// },
{
'name': 'Labelmap分割',
'icon': 'labelmap',
'toolName': 'Labelmap',
'props': ['max', 'min', 'volume', 'count', 'mean', 'stdDev', 'length', 'width'],
'i18nKey': 'trials:reading:button:Labelmap',
'isDisabled': false,
'disabledReason': ''
},
// {
// 'name': 'Surface分割',
// 'icon': 'surface',
// 'toolName': 'Surface',
// 'props': ['area', 'mean', 'max', 'stdDev'],
// 'i18nKey': 'trials:reading:button:Surface',
// 'isDisabled': false,
// 'disabledReason': ''
// },
],
}
const getTools = (criterionType) => {
const standard = config.standards.find(s => s.type === criterionType)
@@ -399,4 +428,8 @@ const getCustomizeStandardsNoneDicomTools = (toolNames) => {
const filteredTools = config.customizeStandardsNoneDicom.filter(item => toolNames.includes(item.toolName))
return filteredTools || []
}
export { config, getTools, getCustomizeStandardsTools, getCustomizeStandardsNoneDicomTools }
const getCustomizeStandardsSegmentDicomTools = (toolNames) => {
const filteredTools = config.customizeStandardsSegmentDicom.filter(item => toolNames.includes(item.toolName))
return filteredTools || []
}
export { config, getTools, getCustomizeStandardsTools, getCustomizeStandardsNoneDicomTools, getCustomizeStandardsSegmentDicomTools }
@@ -316,11 +316,13 @@ class ScaleOverlayTool extends AnnotationDisplayTool {
width: canvas.width / window.devicePixelRatio || 1,
height: canvas.height / window.devicePixelRatio || 1,
};
if(!annotation||!annotation.data) return false
const topLeft = annotation.data.handles.points[0];
const topRight = annotation.data.handles.points[1];
const bottomLeft = annotation.data.handles.points[2];
const bottomRight = annotation.data.handles.points[3];
const pointSet1 = [topLeft, bottomLeft, topRight, bottomRight];
if(!bottomLeft) return false
const worldWidthViewport = vec3.distance(bottomLeft, bottomRight);
const worldHeightViewport = vec3.distance(topLeft, bottomLeft);
const hscaleBounds = this.computeScaleBounds(canvasSize, 0.05, 0.05, location);
@@ -0,0 +1,264 @@
// import { getEnabledElement, utilities as csUtils, getEnabledElementByViewportId, utilities, } from '@cornerstonejs/core';
// import { addAnnotation, getAllAnnotations, getAnnotations, removeAnnotation, } from '../../stateManagement/annotation/annotationState';
// import { isAnnotationLocked } from '../../stateManagement/annotation/annotationLocking';
// import { isAnnotationVisible } from '../../stateManagement/annotation/annotationVisibility';
// import { drawLine as drawLineSvg, drawHandles as drawHandlesSvg, } from '../../drawingSvg';
// import { getViewportIdsWithToolToRender } from '../../utilities/viewportFilters';
// import { hideElementCursor } from '../../cursors/elementCursor';
// import triggerAnnotationRenderForViewportIds from '../../utilities/triggerAnnotationRenderForViewportIds';
// import BidirectionalTool from '../annotation/BidirectionalTool';
// import { getSegmentIndexColor } from '../../stateManagement/segmentation/config/segmentationColor';
import { utilities, getEnabledElementByViewportId, getEnabledElement } from '@cornerstonejs/core';
import * as cornerstoneTools from '@cornerstonejs/tools'
const {
annotation,
drawing,
utilities: TSUtilities,
BidirectionalTool,
cursors,
segmentation
} = cornerstoneTools
const { addAnnotation, getAllAnnotations, getAnnotations, removeAnnotation } = annotation.state
const { isAnnotationLocked } = annotation.locking
const { isAnnotationVisible } = annotation.visibility
const { drawLine: drawLineSvg, drawHandles: drawHandlesSvg } = drawing
const { getViewportIdsWithToolToRender } = TSUtilities.viewportFilters
const { triggerAnnotationRenderForViewportIds } = TSUtilities
const { hideElementCursor } = cursors.elementCursor
const { getSegmentIndexColor } = segmentation.config.color
class SegmentBidirectionalTool extends BidirectionalTool {
static { this.toolName = 'SegmentBidirectional'; }
constructor(toolProps = {}) {
super(toolProps);
this.renderAnnotation = (enabledElement, svgDrawingHelper) => {
let renderStatus = true;
const { viewport } = enabledElement;
const { element } = viewport;
const viewportId = viewport.id;
let annotations = getAnnotations(this.getToolName(), element);
if (!annotations?.length) {
return renderStatus;
}
annotations = this.filterInteractableAnnotationsForElement(element, annotations);
if (!annotations?.length) {
return renderStatus;
}
const targetId = this.getTargetId(viewport);
const renderingEngine = viewport.getRenderingEngine();
const styleSpecifier = {
toolGroupId: this.toolGroupId,
toolName: this.getToolName(),
viewportId: enabledElement.viewport.id,
};
for (let i = 0; i < annotations.length; i++) {
const annotation = annotations[i];
const { annotationUID, data } = annotation;
const { points, activeHandleIndex } = data.handles;
const canvasCoordinates = points.map((p) => viewport.worldToCanvas(p));
styleSpecifier.annotationUID = annotationUID;
const { segmentIndex, segmentationId } = annotation.metadata;
const { lineWidth, lineDash, shadow } = this.getAnnotationStyle({
annotation,
styleSpecifier,
});
const colorArray = getSegmentIndexColor(viewportId, segmentationId, segmentIndex) || [255, 255, 255];
// console.log(viewportId, segmentationId, segmentIndex, 'colorArray')
// console.log(colorArray, 'colorArray')
const color = `rgb(${colorArray.slice(0, 3).join(',')})`;
if (!data.cachedStats[targetId] ||
data.cachedStats[targetId].unit == null) {
data.cachedStats[targetId] = {
length: null,
width: null,
unit: null,
};
this._calculateCachedStats(annotation, renderingEngine, enabledElement);
}
else if (annotation.invalidated) {
this._throttledCalculateCachedStats(annotation, renderingEngine, enabledElement);
}
if (!viewport.getRenderingEngine()) {
console.warn('Rendering Engine has been destroyed');
return renderStatus;
}
let activeHandleCanvasCoords;
if (!isAnnotationVisible(annotationUID)) {
continue;
}
if (!isAnnotationLocked(annotationUID) &&
!this.editData &&
activeHandleIndex !== null) {
activeHandleCanvasCoords = [canvasCoordinates[activeHandleIndex]];
}
if (activeHandleCanvasCoords) {
const handleGroupUID = '0';
drawHandlesSvg(svgDrawingHelper, annotationUID, handleGroupUID, activeHandleCanvasCoords, {
color,
});
}
const dataId1 = `${annotationUID}-line-1`;
const dataId2 = `${annotationUID}-line-2`;
const lineUID = '0';
drawLineSvg(svgDrawingHelper, annotationUID, lineUID, canvasCoordinates[0], canvasCoordinates[1], {
color,
lineWidth,
lineDash,
shadow,
}, dataId1);
const secondLineUID = '1';
drawLineSvg(svgDrawingHelper, annotationUID, secondLineUID, canvasCoordinates[2], canvasCoordinates[3], {
color,
lineWidth,
lineDash,
shadow,
}, dataId2);
renderStatus = true;
const textLines = this.configuration.getTextLines(data, targetId);
if (!textLines || textLines.length === 0) {
continue;
}
if (!this.renderLinkedTextBoxAnnotation({
enabledElement,
svgDrawingHelper,
annotation,
styleSpecifier,
textLines,
canvasCoordinates,
})) {
continue;
}
}
return renderStatus;
};
}
addNewAnnotation(evt) {
const eventDetail = evt.detail;
const { currentPoints, element } = eventDetail;
const worldPos = currentPoints.world;
const enabledElement = getEnabledElement(element);
const { viewport } = enabledElement;
this.isDrawing = true;
const camera = viewport.getCamera();
const { viewPlaneNormal, viewUp } = camera;
const referencedImageId = this.getReferencedImageId(viewport, worldPos, viewPlaneNormal, viewUp);
const FrameOfReferenceUID = viewport.getFrameOfReferenceUID();
const annotation = {
highlighted: true,
invalidated: true,
metadata: {
toolName: this.getToolName(),
viewPlaneNormal: [...viewPlaneNormal],
viewUp: [...viewUp],
FrameOfReferenceUID,
referencedImageId,
...viewport.getViewReference({ points: [worldPos] }),
},
data: {
handles: {
points: [
[...worldPos],
[...worldPos],
[...worldPos],
[...worldPos],
],
textBox: {
hasMoved: false,
worldPosition: [0, 0, 0],
worldBoundingBox: {
topLeft: [0, 0, 0],
topRight: [0, 0, 0],
bottomLeft: [0, 0, 0],
bottomRight: [0, 0, 0],
},
},
activeHandleIndex: null,
},
label: '',
cachedStats: {},
},
};
addAnnotation(annotation, element);
const viewportIdsToRender = getViewportIdsWithToolToRender(element, this.getToolName());
this.editData = {
annotation,
viewportIdsToRender,
handleIndex: 1,
movingTextBox: false,
newAnnotation: true,
hasMoved: false,
};
this._activateDraw(element);
hideElementCursor(element);
evt.preventDefault();
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
return annotation;
}
static {
this.hydrate = (viewportId, axis, options) => {
const enabledElement = getEnabledElementByViewportId(viewportId);
if (!enabledElement) {
return;
}
const { viewport } = enabledElement;
const existingAnnotations = getAllAnnotations();
const toolAnnotations = existingAnnotations.filter((annotation) => annotation.metadata.toolName === 'SegmentBidirectional');
const existingAnnotation = toolAnnotations.find((annotation) => {
const { metadata } = annotation;
if (metadata.segmentIndex === options?.segmentIndex &&
metadata.segmentationId === options?.segmentationId) {
return true;
}
return false;
});
if (existingAnnotation) {
removeAnnotation(existingAnnotation.annotationUID);
}
const { FrameOfReferenceUID, referencedImageId, viewPlaneNormal, instance, } = this.hydrateBase(SegmentBidirectionalTool, enabledElement, axis[0], options);
const [majorAxis, minorAxis] = axis;
const [major0, major1] = majorAxis;
const [minor0, minor1] = minorAxis;
const points = [major0, major1, minor0, minor1];
const { toolInstance, ...serializableOptions } = options || {};
const annotation = {
annotationUID: options?.annotationUID || utilities.uuidv4(),
data: {
handles: {
points,
activeHandleIndex: null,
textBox: {
hasMoved: false,
worldPosition: [0, 0, 0],
worldBoundingBox: {
topLeft: [0, 0, 0],
topRight: [0, 0, 0],
bottomLeft: [0, 0, 0],
bottomRight: [0, 0, 0],
},
},
},
cachedStats: {},
},
highlighted: false,
autoGenerated: false,
invalidated: false,
isLocked: false,
isVisible: true,
metadata: {
segmentIndex: options?.segmentIndex,
segmentationId: options?.segmentationId,
toolName: instance.getToolName(),
viewPlaneNormal,
FrameOfReferenceUID,
referencedImageId,
...serializableOptions,
},
};
addAnnotation(annotation, viewport.element);
triggerAnnotationRenderForViewportIds([viewport.id]);
return annotation;
};
}
}
export default SegmentBidirectionalTool;
@@ -317,7 +317,7 @@ export default {
color: yellow;
}
}
::v-deep.el-button--text{
::v-deep .el-button--text{
color: #d0d0d0;
}
}
@@ -494,7 +494,7 @@ export default {
<style lang="scss" scoped>
.medical-feedback-ir{
::v-deep.search {
::v-deep .search {
padding: 0px !important;
}
}
@@ -209,24 +209,28 @@ export default {
},
methods: {
//
getMessageList() {
this.loading = true
var param = {
taskMedicalReviewId: this.taskMedicalReviewId
}
getMedicalReviewDialog(param).then(res => {
async getMessageList() {
try {
this.loading = true
var param = {
taskMedicalReviewId: this.taskMedicalReviewId
}
let res = await getMedicalReviewDialog(param)
this.otherInfo = res.OtherInfo
this.recordContent = res.Result
this.setScrollHeight()
this.loading = false
}).catch(() => {
} catch(e) {
console.log(e)
this.loading = false
})
}
},
setScrollHeight() {
setTimeout(() => {
var container = document.querySelectorAll('.chat-content')[0]
container.scrollTop = container.scrollHeight
if (container && container.scrollHeight) {
container.scrollTop = container.scrollHeight
}
}, 100)
},
//
@@ -366,7 +370,7 @@ export default {
color: yellow;
}
}
::v-deep.el-button--text{
::v-deep .el-button--text{
color: #d0d0d0;
}
}
@@ -80,26 +80,32 @@ export default {
handleClose() {
this.$emit('close')
},
handleSave() {
this.$refs['closeQCForm'].validate((valid) => {
if (!valid) return
this.loading = true
this.form.TaskMedicalReviewId = this.taskMedicalReviewId
this.form.IsClosedDialog = true
closedMedicalReviewDialog(this.form)
.then(res => {
this.loading = false
if (res.IsSuccess) {
// !
this.$message.success(this.$t('trials:qcQuality:message:closedSuccessfully'))
this.$emit('close')
this.$emit('refresh')
}
}).catch(() => {
this.loading = false
})
})
}
async handleSave() {
let validate = await this.$refs['closeQCForm'].validate()
if (!validate) return
this.form.TaskMedicalReviewId = this.taskMedicalReviewId
this.form.IsClosedDialog = true
this.$emit('closeAndSign', this.form)
},
// handleSave() {
// this.$refs['closeQCForm'].validate((valid) => {
// if (!valid) return
// this.loading = true
// this.form.TaskMedicalReviewId = this.taskMedicalReviewId
// this.form.IsClosedDialog = true
// closedMedicalReviewDialog(this.form)
// .then(res => {
// this.loading = false
// if (res.IsSuccess) {
// // !
// this.$message.success(this.$t('trials:qcQuality:message:closedSuccessfully'))
// this.$emit('refresh')
// }
// }).catch(() => {
// this.loading = false
// })
// })
// }
}
}
</script>
@@ -161,16 +161,16 @@ export default {
</script>
<style lang="scss" scoped>
.issues-form{
::v-deep.el-radio-group{
::v-deep .el-radio-group{
width:300px;
}
::v-deep.el-textarea{
::v-deep .el-textarea{
width:300px;
}
::v-deep.el-input{
::v-deep .el-input{
width:300px;
}
::v-deep.el-select{
::v-deep .el-select{
width:300px;
}
@@ -553,8 +553,8 @@
>
<CloseQC
:task-medical-review-id="currentRow.Id"
@closeAndSign="closeAndSign"
@close="closeQuestionVisible = false"
@refresh="refresh"
/>
</el-dialog>
<!--签名框 -->
@@ -645,7 +645,8 @@ export default {
signCode: null,
currentUser: zzSessionStorage.getItem('userName'),
signVisible: false,
timeList: []
timeList: [],
closeObj: null
}
},
watch: {
@@ -697,6 +698,12 @@ export default {
this.TrialReadingCriterionId = this.trialCriterionList[0].TrialReadingCriterionId
}).catch(() => {})
},
closeAndSign(obj) {
this.closeObj = Object.assign({}, obj)
const { MedicalAudit } = const_.processSignature
this.signCode = MedicalAudit
this.signVisible = true
},
//
closeSignDialog(isSign, signInfo) {
if (isSign) {
@@ -706,38 +713,39 @@ export default {
}
},
//
signConfirm(signInfo) {
this.loading = true
const params = {
data: {
taskMedicalReviewId: this.currentRow.Id
},
signInfo: signInfo
}
FinishMedicalReview(params).then(res => {
async signConfirm(signInfo) {
try {
this.loading = true
const params = {
data: {
taskMedicalReviewId: this.currentRow.Id,
isClosedDialog: this.closeObj.IsClosedDialog,
medicalDialogCloseEnum: this.closeObj.MedicalDialogCloseEnum,
dialogCloseReason: this.closeObj.DialogCloseReason,
},
signInfo: signInfo
}
let res = await FinishMedicalReview(params)
if (res.IsSuccess) {
await this.$refs['chatForm'].getMessageList()
this.$message.success(this.$t('common:message:savedSuccessfully'))
this.$refs['signForm'].btnLoading = false
this.signVisible = false
this.$nextTick(() => {
this.closeQuestionVisible = false
this.chatForm.visible = false
})
await this.getList()
this.$emit('nextTask', this.taskMedicalReviewId)
}
this.loading = false
this.getList()
.then(() => {
this.loading = true
this.$emit('nextTask', this.taskMedicalReviewId)
})
.catch(action => {
})
}).catch(_ => {
} catch(e) {
this.loading = false
if (this.$refs['signForm']) {
this.$refs['signForm'].btnLoading = false
}
})
}
},
changeTimeList() {
if (this.timeList) {

Some files were not shown because too many files have changed in this diff Show More