Compare commits
117 Commits
v2.1.0
..
47d7fb00e2
| Author | SHA1 | Date | |
|---|---|---|---|
| 47d7fb00e2 | |||
| 312ab754e1 | |||
| 0ff5a21d04 | |||
| 5a7fa1fb77 | |||
| 5289c156c0 | |||
| a68c27fd43 | |||
| 641c2e6ade | |||
| 5b4cc097bc | |||
| b1de662677 | |||
| d2d1063ed5 | |||
| 2bc4b32d3a | |||
| 366ec62cd7 | |||
| 866e2e5c6b | |||
| 16e2db37cd | |||
| ee6c5bf8a5 | |||
| 48cfe99f12 | |||
| f65264a2a6 | |||
| 769c5f8440 | |||
| 6321bddfd3 | |||
| e30fb2a041 | |||
| 478adcde21 | |||
| 34ce5142f2 | |||
| 3b993bcd46 | |||
| 3f2e39f46c | |||
| d317402f85 | |||
| 6cb63bdc59 | |||
| 8db641db16 | |||
| 9da9515930 | |||
| 2be119de6b | |||
| c9f9615488 | |||
| be137b3840 | |||
| d80101f86b | |||
| 3c0b084f7e | |||
| 8a88a64c36 | |||
| 3d538d6e67 | |||
| 67d2a83ec8 | |||
| 2efb229c96 | |||
| c930480336 | |||
| b39ea3a032 | |||
| ff5aea17a5 | |||
| 477b6935ff | |||
| 8b6432ad14 | |||
| ca35fe6df4 | |||
| 307c02725f | |||
| 72d0494baa | |||
| d1017bc565 | |||
| 76e6ef087f | |||
| c5c9e4364c | |||
| 920d0b21c6 | |||
| 3fe16b4166 | |||
| bc2d5463ff | |||
| c63811d55c | |||
| 832fa82d4b | |||
| b3ae801fe9 | |||
| 0dbd74270d | |||
| aae5968d74 | |||
| 79a7ad0cf7 | |||
| a35309d9b5 | |||
| a987ac086b | |||
| fdc2bc5f8a | |||
| 84f68c79f0 | |||
| b77dc34523 | |||
| 91554e0e30 | |||
| 2efb7fd36a | |||
| dbdec93efc | |||
| e631ce73f2 | |||
| 5931dcdc67 | |||
| b55ffc0821 | |||
| 8600054405 | |||
| 9a88539294 | |||
| 09211c8841 | |||
| 2724e0267f | |||
| a0e355d69b | |||
| 04a0a1ccc3 | |||
| c3e967acaa | |||
| 740a0aa221 | |||
| e8f95d09ba | |||
| 2c305af49e | |||
| 5c76bda1e7 | |||
| 0273601f7b | |||
| c3a154f762 | |||
| cdb97f6405 | |||
| 4794429a57 | |||
| 77a633d699 | |||
| 64d1ebf350 | |||
| 5336cb5353 | |||
| 0a288b9f47 | |||
| 674d483cbf | |||
| cbcb396441 | |||
| 9a3e972723 | |||
| b593615e9e | |||
| d30c9a5f3b | |||
| dd3d738eed | |||
| d9edaf4c27 | |||
| a5e0dd89ff | |||
| 73f8f816b7 | |||
| 19d5144b70 | |||
| e0ecd3f4ac | |||
| 30e0e374bc | |||
| 25b909c660 | |||
| 5cb3e8e606 | |||
| 619af2e874 | |||
| 43f1736b05 | |||
| c9be5678bd | |||
| 4e5ecb3938 | |||
| 7870c3d74b | |||
| d07adc4948 | |||
| 266886fa64 | |||
| e012eba0c2 | |||
| 75d296405b | |||
| 07c92c48a4 | |||
| b5427dd17b | |||
| 385643643e | |||
| 756f1293aa | |||
| eee19f0ae9 | |||
| 78c0910db7 | |||
| 18a71725f9 |
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function isNMReconstructable(imageSubType) {
|
||||
return imageSubType === 'RECON TOMO' || imageSubType === 'RECON GATED TOMO';
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -965,8 +965,8 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.imageBox {
|
||||
<style lang="scss">
|
||||
.viewerContainer .imageBox {
|
||||
position: relative;
|
||||
|
||||
i {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)}`
|
||||
// 当为PT或融合视口或MIP视口时,触发外部联动 (排除纯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))
|
||||
// 当为PT或融合视口或MIP视口时,触发外部联动 (排除纯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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||