Compare commits
147 Commits
v2.0.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 | |||
| 87332ae9d8 | |||
| 401762dca7 | |||
| 7b694d4afb | |||
| 6e285d900b | |||
| c5d3b44c2c | |||
| cf4dbc2914 | |||
| 3cef5afebd | |||
| 060b61eafb | |||
| 0d3571f672 | |||
| 8b3bd101b6 | |||
| da252ae4df | |||
| fdb7251b62 | |||
| 1f51bbd178 | |||
| 597ca07fcf | |||
| b43c75dd9f | |||
| 94f3e6c757 | |||
| 1d95cd15f2 | |||
| a1f9283262 | |||
| a0abba71dd | |||
| 02d85e19a6 | |||
| 9ad916f860 | |||
| e0094c01d8 | |||
| 0eb2135185 | |||
| 8ddfe34f72 | |||
| c603d10ff5 | |||
| 3fca2e76cb | |||
| c0ae4732fc | |||
| 4ec0a178b4 | |||
| 443a31df97 | |||
| d21c01076d |
@@ -1,4 +1,39 @@
|
||||
|
||||
kind: pipeline
|
||||
type: ssh
|
||||
name: ssh-linux-uat-irc-publish
|
||||
|
||||
platform:
|
||||
os: Linux
|
||||
arch: 386
|
||||
|
||||
clone:
|
||||
disable: true #禁用默认克隆
|
||||
|
||||
server:
|
||||
host: 101.132.253.119
|
||||
user: root
|
||||
password:
|
||||
from_secret: local_pwd
|
||||
|
||||
steps:
|
||||
- name: publish-uat-irc-web
|
||||
commands:
|
||||
- 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"
|
||||
- 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" "Uat_IRC_WEB" "irc.uat.extimaging.com"
|
||||
when:
|
||||
status:
|
||||
- success
|
||||
- failure
|
||||
trigger:
|
||||
branch:
|
||||
- uat
|
||||
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: ssh
|
||||
name: ssh-linux-test-irc-publish
|
||||
@@ -30,9 +65,6 @@ steps:
|
||||
- failure
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
|
||||
|
||||
|
||||
- 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@
|
||||
</div>
|
||||
</el-drawer>
|
||||
<feedBack v-if="$route.matched.length > 0" />
|
||||
<timeTag />
|
||||
<!-- <timeTag /> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -94,12 +94,15 @@ import {
|
||||
} from '@/api/dictionary/dictionary'
|
||||
import { getTrialExtralConfig } from '@/api/trials'
|
||||
import feedBack from '@/views/trials/trials-layout/components/feedBack'
|
||||
import timeTag from '@/components/timeTag'
|
||||
// import timeTag from '@/components/timeTag'
|
||||
import Vue from 'vue'
|
||||
import i18n from './lang'
|
||||
export default {
|
||||
name: 'App',
|
||||
components: { feedBack, timeTag },
|
||||
components: {
|
||||
feedBack,
|
||||
// timeTag
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drawer: false,
|
||||
@@ -112,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)
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<el-table-column :label="$t('common:action:action')" fixed="right" width="150">
|
||||
<template slot-scope="scope">
|
||||
<!--预览--->
|
||||
<el-button circle icon="el-icon-view" :title="$t('download:button:preview')"
|
||||
<el-button circle icon="el-icon-view" :title="$t('download:button:preview')" :disabled="forbid"
|
||||
@click.stop="preview(scope.row)" />
|
||||
<!--下载--->
|
||||
<el-button circle icon="el-icon-download" :title="$t('download:button:download')"
|
||||
@@ -111,6 +111,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
forbid: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
SubjectId: {
|
||||
type: String,
|
||||
default: '',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<template slot-scope="scope">
|
||||
<div class="btnBox">
|
||||
<!--上传--->
|
||||
<form id="inputForm" :ref="`uploadForm_${scope.row.Id}`" enctype="multipart/form-data">
|
||||
<form id="inputForm" :ref="`uploadForm_${scope.row.Id}`" enctype="multipart/form-data" v-if="!forbid">
|
||||
<div class="form-group" style="margin-right: 10px">
|
||||
<div :id="`directoryInputWrapper_${scope.row.Id}`" class="btn btn-link file-input">
|
||||
<el-button circle icon="el-icon-upload2" :disabled="btnLoading" :loading="btnLoading"
|
||||
@@ -69,7 +69,7 @@
|
||||
</el-table>
|
||||
<div style="margin: 10px 0" class="top">
|
||||
<span>{{ $t('upload:dicom:uploadTitle') }}</span>
|
||||
<div class="btnBox">
|
||||
<div class="btnBox" v-if="!forbid">
|
||||
<span style="margin-right: 10px">
|
||||
{{ $store.state.trials.uploadTip }}
|
||||
</span>
|
||||
@@ -354,6 +354,10 @@ export default {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
forbid: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
TaskId: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -708,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
|
||||
@@ -1170,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 = []
|
||||
@@ -1262,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) {
|
||||
@@ -1285,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(
|
||||
@@ -1426,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)
|
||||
}
|
||||
@@ -1444,6 +1482,7 @@ export default {
|
||||
if (scope.IsImageSegment) {
|
||||
params.IsImageSegmentLabel = true
|
||||
}
|
||||
params.UploadBatchId = uploadBatchId
|
||||
addOrUpdateArchiveTaskStudy(params)
|
||||
.then((res) => {
|
||||
if (dicomInfo.failedFileCount === dicomInfo.fileCount) {
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<el-tab-pane :label="$t('uploadDicomAndNonedicom:label:dicom')" name="dicom">
|
||||
<dicomFile v-if="activeName === 'dicom'" :SubjectId="SubjectId" :SubjectCode="SubjectCode"
|
||||
:Criterion="Criterion" :TaskId="VisitTaskId" :isUpload.sync="isUpload"
|
||||
:isReadingTaskViewInOrder="isReadingTaskViewInOrder" :IsImageSegment="IsImageSegment" />
|
||||
:isReadingTaskViewInOrder="isReadingTaskViewInOrder" :IsImageSegment="IsImageSegment" :forbid="forbid" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('uploadDicomAndNonedicom:label:nonedicom')" name="nonedicom">
|
||||
<nonedicomFile v-if="activeName === 'nonedicom'" :SubjectId="SubjectId" :SubjectCode="SubjectCode"
|
||||
:Criterion="Criterion" :VisitTaskId="VisitTaskId" :isUpload.sync="isUpload"
|
||||
:IsImageSegment="IsImageSegment" />
|
||||
:Criterion="Criterion" :VisitTaskId="VisitTaskId" :isUpload.sync="isUpload" :IsImageSegment="IsImageSegment"
|
||||
:forbid="forbid" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
@@ -29,6 +29,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
forbid: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
SubjectId: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -82,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;
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
<el-button circle :disabled="scope.row.UploadedFileCount <= 0" icon="el-icon-view"
|
||||
:title="$t('upload:nonedicom:button:preview')" @click.stop="handlePreviewNoneDicomFiles(scope.row)" />
|
||||
<!--上传--->
|
||||
<el-button circle icon="el-icon-upload2" :title="$t('upload:nonedicom:button:upload')"
|
||||
<el-button circle icon="el-icon-upload2" :title="$t('upload:nonedicom:button:upload')" v-if="!forbid"
|
||||
@click.native.prevent="handleUpload(scope.row)" />
|
||||
<!--删除--->
|
||||
<el-button :disabled="scope.row.UploadedFileCount <= 0 ||
|
||||
@@ -239,6 +239,10 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
forbid: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
SubjectCode: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -544,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)
|
||||
@@ -556,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
|
||||
@@ -572,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({
|
||||
@@ -606,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(
|
||||
{
|
||||
@@ -625,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 {
|
||||
|
||||
@@ -37,7 +37,7 @@ export default {
|
||||
// console.log(type, No);
|
||||
if (type !== "Chrome" && type !== "Edge") {
|
||||
this.tip = this.$t("browser:tip:changeBorwser");
|
||||
this.getSystemInfo()
|
||||
// this.getSystemInfo()
|
||||
return (this.visible = true);
|
||||
}
|
||||
let res = await this.getInfo();
|
||||
@@ -53,10 +53,10 @@ export default {
|
||||
this.tip += "、";
|
||||
}
|
||||
});
|
||||
this.getSystemInfo()
|
||||
// this.getSystemInfo()
|
||||
return (this.visible = true);
|
||||
}
|
||||
this.getSystemInfo()
|
||||
// this.getSystemInfo()
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -1,109 +1,63 @@
|
||||
<template>
|
||||
<base-model :config="model_cfg">
|
||||
<template slot="dialog-body">
|
||||
<el-form
|
||||
ref="NoticeForm"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="180px"
|
||||
size="small"
|
||||
>
|
||||
<el-form-item :label="$t('system:notice:label:NoticeLevel')" prop="NoticeLevelEnum">
|
||||
<el-form ref="NoticeForm" :model="form" :rules="rules" label-width="180px" size="small">
|
||||
<el-form-item :label="$t('system:notice:label:NoticeLevel')" prop="NoticeLevelEnum">
|
||||
<el-select v-model="form.NoticeLevelEnum" clearable size="small">
|
||||
<el-option
|
||||
v-for="item of dict.type.NoteLevel"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.raw.Code * 1"
|
||||
/>
|
||||
<el-option v-for="item of dict.type.NoteLevel" :key="item.value" :label="item.label"
|
||||
:value="item.raw.Code * 1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('system:notice:label:NoticeType')" prop="NoticeTypeEnum">
|
||||
<el-select v-model="form.NoticeTypeEnum" clearable size="small">
|
||||
<el-option
|
||||
v-for="item of dict.type.NoteType"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.raw.Code * 1"
|
||||
/>
|
||||
<el-option v-for="item of dict.type.NoteType" :key="item.value" :label="item.label"
|
||||
:value="item.raw.Code * 1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('system:notice:label:ApplicableProject')" prop="ApplicableProjectEnum">
|
||||
<el-select v-model="form.ApplicableProjectEnum" clearable size="small">
|
||||
<el-option
|
||||
v-for="item of dict.type.NoticeApplicableTrial"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.raw.Code * 1"
|
||||
/>
|
||||
<el-select v-model="form.ApplicableProjectEnum" clearable size="small">
|
||||
<el-option v-for="item of dict.type.NoticeApplicableTrial" :key="item.value" :label="item.label"
|
||||
:value="item.raw.Code * 1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('system:notice:label:NoticeUserTypeIdList')" prop="NoticeUserTypeIdList">
|
||||
<el-select v-model="form.NoticeUserTypeIdList" multiple clearable size="small">
|
||||
<el-option
|
||||
v-for="item of roleList"
|
||||
:key="item.Id"
|
||||
:label="item.UserTypeShortName"
|
||||
:value="item.Id"
|
||||
/>
|
||||
<el-option v-for="item of roleList" :key="item.Id" :label="item.UserTypeShortName" :value="item.Id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('system:notice:label:NoticeMode')" prop="NoticeModeEnum">
|
||||
<el-select v-model="form.NoticeModeEnum" clearable size="small">
|
||||
<el-option
|
||||
v-for="item of dict.type.NoticeMode"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.raw.Code * 1"
|
||||
/>
|
||||
<el-option v-for="item of dict.type.NoticeMode" :key="item.value" :label="item.label"
|
||||
:value="item.raw.Code * 1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('system:notice:label:StartDate')" prop="StartDate">
|
||||
<el-date-picker
|
||||
v-model="form.StartDate"
|
||||
type="datetime"
|
||||
/>
|
||||
<el-date-picker v-model="form.StartDate" type="datetime" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('system:notice:label:EndDate')" prop="EndDate">
|
||||
<el-date-picker
|
||||
v-model="form.EndDate"
|
||||
type="datetime"
|
||||
/>
|
||||
<el-form-item :label="$t('system:notice:label:EndDate')" prop="EndDate">
|
||||
<el-date-picker v-model="form.EndDate" type="datetime" :default-time="'23:59:59'" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!form.Id" :label="$t('system:notice:label:IsPush')" prop="IsPush">
|
||||
<el-switch
|
||||
v-model="form.IsPush"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
/>
|
||||
<el-switch v-model="form.IsPush" :active-value="true" :inactive-value="false" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('system:notice:label:NoticeContent')" prop="NoticeContent">
|
||||
<el-input
|
||||
v-model="form.NoticeContent"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 4}"
|
||||
/>
|
||||
<el-form-item :label="$t('system:notice:label:NoticeContent')" prop="NoticeContent">
|
||||
<el-input v-model="form.NoticeContent" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('system:notice:label:file')">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
action
|
||||
:before-upload="beforeUpload"
|
||||
:http-request="handleUploadFile"
|
||||
:on-preview="handlePreview"
|
||||
:on-remove="handleRemoveFile"
|
||||
:show-file-list="true"
|
||||
:limit="1"
|
||||
:file-list="fileList"
|
||||
>
|
||||
<el-button size="small" type="primary" :disabled="fileList.length > 0">{{$t('common:button:upload')}}</el-button>
|
||||
<el-form-item :label="$t('system:notice:label:file')">
|
||||
<el-upload class="upload-demo" action :before-upload="beforeUpload" :http-request="handleUploadFile"
|
||||
:on-preview="handlePreview" :on-remove="handleRemoveFile" :show-file-list="true" :limit="1"
|
||||
:file-list="fileList">
|
||||
<el-button size="small" type="primary"
|
||||
:disabled="fileList.length > 0">{{ $t('common:button:upload') }}</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
<template slot="dialog-footer">
|
||||
<el-button :disabled="btnLoading" size="small" type="primary" @click="handleCancle">{{ $t('common:button:cancel') }}</el-button>
|
||||
<el-button size="small" type="primary" :loading="btnLoading" @click="handleSave">{{ $t('common:button:save') }}</el-button>
|
||||
<el-button :disabled="btnLoading" size="small" type="primary" @click="handleCancle">{{ $t('common:button:cancel')
|
||||
}}</el-button>
|
||||
<el-button size="small" type="primary" :loading="btnLoading" @click="handleSave">{{ $t('common:button:save')
|
||||
}}</el-button>
|
||||
</template>
|
||||
</base-model>
|
||||
</template>
|
||||
@@ -178,7 +132,7 @@ export default {
|
||||
this.loading = true
|
||||
var file = await this.fileToBlob(param.file)
|
||||
const res = await this.OSSclient.put(`/System/NoticeAttachment/${param.file.name}`, file)
|
||||
this.fileList.push({ name: param.file.name, path: this.$getObjectName(res.url), fullPath: this.$getObjectName(res.url), url: this.$getObjectName(res.url)})
|
||||
this.fileList.push({ name: param.file.name, path: this.$getObjectName(res.url), fullPath: this.$getObjectName(res.url), url: this.$getObjectName(res.url) })
|
||||
this.form.Path = this.$getObjectName(res.url)
|
||||
this.form.FileName = param.file.name
|
||||
this.loading = false
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<template>
|
||||
<div class="trials-navbar" style="position: relative">
|
||||
<div class="leftMenu">
|
||||
<img v-if="language === 'zh'" src="@/assets/zzlogo2.png" alt="" />
|
||||
<img
|
||||
v-else-if="NODE_ENV === 'usa'"
|
||||
v-if="NODE_ENV === 'usa'"
|
||||
src="@/assets/title-logo.png"
|
||||
alt=""
|
||||
class="title-logo"
|
||||
/>
|
||||
<img v-else src="@/assets/zzlogo4.png" alt="" />
|
||||
<img v-else src="@/assets/system.png" alt="" style="width: 150px;height: 50px;margin-bottom: -7px;"/>
|
||||
<span style="white-space: nowrap" v-if="NODE_ENV !== 'usa'">
|
||||
<!-- 中心影像系统(EICS) -->
|
||||
{{ $t('trials:trials:title:eics') }}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -495,17 +495,18 @@
|
||||
<h2 v-else style="color:#ddd">
|
||||
Developing...
|
||||
</h2>
|
||||
<div v-if="iseCRFShowInDicomReading && currentReadingTaskState < 2" v-show="listShow" class="form-footer">
|
||||
<el-button type="primary" size="small" @click="skipTask">
|
||||
<!-- 跳过 -->
|
||||
{{ $t('trials:readingReport:button:skip') }}
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="submit">
|
||||
<!-- 提交 -->
|
||||
{{ $t('common:button:submit') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iseCRFShowInDicomReading && currentReadingTaskState < 2" v-show="listShow" class="form-footer">
|
||||
<el-button type="primary" size="small" @click="skipTask">
|
||||
<!-- 跳过 -->
|
||||
{{ $t('trials:readingReport:button:skip') }}
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="submit">
|
||||
<!-- 提交 -->
|
||||
{{ $t('common:button:submit') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
</transition>
|
||||
|
||||
|
||||
@@ -704,6 +705,7 @@ import { getAutoCutNextTask } from '@/api/user'
|
||||
import const_ from '@/const/sign-code'
|
||||
import { changeURLStatic } from '@/utils/history.js'
|
||||
import SystemInfo from "@/utils/systemInfo";
|
||||
import md5 from 'js-md5'
|
||||
export default {
|
||||
name: 'DicomViewer',
|
||||
components: {
|
||||
@@ -987,6 +989,7 @@ export default {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
console.log(this.iseCRFShowInDicomReading, this.currentReadingTaskState, this.listShow)
|
||||
this.getHotKeys()
|
||||
this.getWwcTpl()
|
||||
this.getTrialCriterion()
|
||||
@@ -1138,6 +1141,7 @@ export default {
|
||||
let windowHeight = document.documentElement.clientHeight;
|
||||
this.AspectRatio = windowWidth / windowHeight
|
||||
};
|
||||
this.getSystemInfoReading()
|
||||
},
|
||||
beforeDestroy() {
|
||||
DicomEvent.$off('updateImage')
|
||||
@@ -2501,6 +2505,30 @@ export default {
|
||||
|
||||
})
|
||||
},
|
||||
async getSystemInfoReading() {
|
||||
return new Promise(async resolve => {
|
||||
let whitelisting = localStorage.getItem('whitelisting') ? JSON.parse(localStorage.getItem('whitelisting')) : []
|
||||
let user = md5(sessionStorage.getItem('identityUserId'))
|
||||
let r = whitelisting.some(item => item === user)
|
||||
if (r) return resolve(true)
|
||||
const systemInfo = new SystemInfo();
|
||||
const allInfo = systemInfo.getAllInfo();
|
||||
let deviceMemory = allInfo.hardware.deviceMemory; // 设备内存
|
||||
let { width, height } = allInfo.screen; // 分辨率
|
||||
// let discrete = allInfo.webgl.gpuType.discrete; // 是否独立显卡
|
||||
// let estimatedMemory = allInfo.webgl.memoryInfo.estimatedMemory; // 显卡内存
|
||||
// parseFloat(deviceMemory) < 16 ||
|
||||
if (width < 1920 || height < 1080) {
|
||||
let res = await this.$confirm(this.$t('browser:tip:Configuration'))
|
||||
whitelisting.push(user)
|
||||
localStorage.setItem('whitelisting', JSON.stringify(whitelisting))
|
||||
resolve(res)
|
||||
} else {
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -2786,7 +2814,7 @@ export default {
|
||||
|
||||
.form-footer {
|
||||
background: #000;
|
||||
padding: 10px 0;
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -2900,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;
|
||||
}
|
||||
@@ -2916,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;
|
||||
@@ -2934,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div v-for="item in qs.Childrens" :key="item.Id">
|
||||
<div v-if="item.Type === 'basicTable'" class="flex-row" style="margin:3px 0;">
|
||||
<div class="title">{{ item.QuestionName }}</div>
|
||||
<div v-if="item.LesionType === 102 && readingTaskState < 2">
|
||||
<div v-if="item.LesionType === 104 && readingTaskState < 2">
|
||||
<div class="add-icon" @click.prevent="downloadTpl">
|
||||
<i class="el-icon-download" />
|
||||
</div>
|
||||
@@ -49,15 +49,6 @@
|
||||
</div>
|
||||
<el-table v-if="item.Type === 'basicTable' && item.TableQuestions" :ref="item.Id"
|
||||
:data="item.TableQuestions.Answers">
|
||||
<!-- <el-table-column :label="$t('CustomizeQuestionFormItem:label:OrderMark')" width="60px" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
{{ getLesionName(item.OrderMark,scope.row.RowIndex) }}
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<!-- <el-table-column
|
||||
type="index"
|
||||
width="40px"
|
||||
/> -->
|
||||
<el-table-column v-for="q of item.TableQuestions.Questions" :key="q.Id" :prop="q.Id"
|
||||
:label="q.QuestionName" show-overflow-tooltip :render-header="renderHeader">
|
||||
<template slot-scope="scope">
|
||||
@@ -76,14 +67,14 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="readingTaskState < 2 && (item.LesionType === 102 || item.LesionType === 103)"
|
||||
<el-table-column v-if="readingTaskState < 2 && (item.LesionType === 104)"
|
||||
:label="$t('common:action:action')" width="90px" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="handleAddOrEdit('edit', item, scope.$index)">
|
||||
{{ $t('common:button:edit') }}
|
||||
</el-button>
|
||||
<el-button type="text" size="mini" @click="handleDelete(item, scope.$index)"
|
||||
v-if="item.LesionType === 102">
|
||||
v-if="item.LesionType === 104">
|
||||
{{ $t('common:button:delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -96,10 +87,12 @@
|
||||
:prop="item.Id" :rules="[
|
||||
{
|
||||
required: (item.IsRequired === 0 || (item.IsRequired === 1 && item.RelevanceId && (item.RelevanceValue.includes(questionForm[item.RelevanceId])))) && item.Type !== 'group' && item.Type !== 'summary',
|
||||
message: ['radio', 'select', 'checkbox'].includes(item.Type) ? $t('common:ruleMessage:select') : $t('common:ruleMessage:specify'), trigger: ['blur', 'change']
|
||||
message: ['radio', 'select', 'checkbox'].includes(item.Type) ? $t('common:ruleMessage:select') : (item.QuestionType === 1016 || item.QuestionType === 1017) ? $t('trials:reading:ruleMessage:validatorNumberInput') : $t('common:ruleMessage:specify'), trigger: ['blur', 'change']
|
||||
},
|
||||
{
|
||||
validator: item.Type === 'number' && !item.TypeValue && (item.QuestionType === 1016 || item.QuestionType === 1017) ? validatorNumberOrNEInput : (rule, value, callback) => { callback() }, trigger: ['blur', 'change']
|
||||
}
|
||||
]">
|
||||
<!-- 数值类型 -->
|
||||
<template v-if="item.Type === 'textarea'">
|
||||
<el-input v-model="questionForm[item.Id]" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }"
|
||||
:disabled="!isCurrentTask || readingTaskState >= 2"
|
||||
@@ -107,9 +100,28 @@
|
||||
</template>
|
||||
<template v-if="item.Type === 'number'">
|
||||
<div style="display: flex;justify-content: space-between;">
|
||||
<el-input-number v-model="questionForm[item.Id]"
|
||||
<el-input
|
||||
v-if="item.QuestionType === 1016 || item.QuestionType === 1017"
|
||||
type="text"
|
||||
@change="(val) => { formItemChange(val, item) }"
|
||||
@input="numberOrNEInput(item.Id)"
|
||||
@blur="handleNumberOrNEBlur(item)"
|
||||
v-model.trim="questionForm[item.Id]" :disabled="readingTaskState === 2">
|
||||
<template slot="append" v-if="item.Unit !== 0">{{ item.Unit !== 4 ? $fd('ValueUnit', item.Unit) : item.CustomUnit }}</template>
|
||||
<template slot="append" v-else-if="item.ValueType === 2">%</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
v-else
|
||||
@change="(val) => { formItemChange(val, item) }"
|
||||
@input="numberInput(item.Id)"
|
||||
@blur="handleBlur(questionForm[item.Id], questionForm, item)"
|
||||
v-model.trim="questionForm[item.Id]" :disabled="readingTaskState === 2">
|
||||
<template slot="append" v-if="item.Unit !== 0">{{ item.Unit !== 4 ? $fd('ValueUnit', item.Unit) : item.CustomUnit }}</template>
|
||||
<template slot="append" v-else-if="item.ValueType === 2">%</template>
|
||||
</el-input>
|
||||
<!-- <el-input-number v-model="questionForm[item.Id]"
|
||||
:disabled="!isCurrentTask || readingTaskState >= 2" :precision="0"
|
||||
@change="((val) => { formItemChange(val, item) })" />
|
||||
@change="((val) => { formItemChange(val, item) })" /> -->
|
||||
<svg-icon v-if="item.ShowChartTypeEnum > 0" icon-class="readingChart"
|
||||
class="svg-icon svg-readingChart" @click.stop="(e) => handleReadingChart({
|
||||
e,
|
||||
@@ -120,6 +132,15 @@
|
||||
})" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="item.Type === 'calculation'">
|
||||
<el-input v-model="questionForm[item.Id]"
|
||||
disabled type="number" @change="((val) => { formItemChange(val, item) })">
|
||||
<template slot="append" v-if="item.Unit !== 0">
|
||||
{{ item.Unit !== 4 ? $fd('ValueUnit', item.Unit) : item.CustomUnit }}
|
||||
</template>
|
||||
<template slot="append" v-else-if="item.ValueType === 2">%</template>
|
||||
</el-input>
|
||||
</template>
|
||||
<template v-if="item.Type === 'select'">
|
||||
<el-select v-model="questionForm[item.Id]" :disabled="!isCurrentTask || readingTaskState >= 2"
|
||||
:multiple="item.OptionTypeEnum === 1" @change="((val) => { formItemChange(val, item) })">
|
||||
@@ -256,7 +277,10 @@ export default {
|
||||
qsForm: {},
|
||||
diffId: '',
|
||||
eemId: '',
|
||||
lumenId: ''
|
||||
lumenId: '',
|
||||
sRoiStartDistanceId: '',
|
||||
sRoiEndDistanceId: '',
|
||||
sRoiDistanceId: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -332,6 +356,7 @@ export default {
|
||||
}
|
||||
this.questions = this.visitTaskList[i].ReadingQuestions
|
||||
this.setQuestionForm(this.questions)
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs['ecrf'].getQuestions(this.visitTaskId)
|
||||
this.$refs['ecrf2'].getQuestions(this.visitTaskId)
|
||||
@@ -343,9 +368,21 @@ export default {
|
||||
const type = ['number', 'radio', 'select', 'input', 'textarea', 'calculation']
|
||||
questions.forEach(item => {
|
||||
if (type.includes(item.Type)) {
|
||||
const answer = item.Type === 'select' && item.OptionTypeEnum === 1 && item.Answer ? JSON.parse(item.Answer) : item.Type === 'number' || item.Type === 'calculation' ? isNaN(parseFloat(item.Answer)) ? null : parseFloat(item.Answer) : item.Answer
|
||||
const answer = item.Type === 'select' && item.OptionTypeEnum === 1 && item.Answer ? JSON.parse(item.Answer) : item.Answer
|
||||
this.$set(this.questionForm, item.Id, answer)
|
||||
}
|
||||
if (item.QuestionType === 1013) {
|
||||
// ROI起始回撤距离
|
||||
this.sRoiStartDistanceId = item.Id
|
||||
}
|
||||
if (item.QuestionType === 1014) {
|
||||
// ROI终止回撤距离
|
||||
this.sRoiEndDistanceId = item.Id
|
||||
}
|
||||
if (item.QuestionType === 1015) {
|
||||
// ROI段落总长度
|
||||
this.sRoiDistanceId = item.Id
|
||||
}
|
||||
if (item.Childrens.length > 0) {
|
||||
this.setQuestionForm(item.Childrens)
|
||||
}
|
||||
@@ -369,6 +406,7 @@ export default {
|
||||
item.TableQuestions.Answers[idx][i] = String(obj.anwsers[i])
|
||||
}
|
||||
}
|
||||
|
||||
if (item.Childrens.length > 0) {
|
||||
this.setQuestions(item.Childrens, obj)
|
||||
}
|
||||
@@ -460,12 +498,16 @@ export default {
|
||||
},
|
||||
formItemChange(val, item) {
|
||||
this.formChanged = true
|
||||
if (item.Type === 'number') {
|
||||
this.limitBlur(item.Id, item.ValueType)
|
||||
}
|
||||
// if (item.Type === 'number') {
|
||||
// this.limitBlur(item.Id, item.ValueType)
|
||||
// }
|
||||
if (item.Childrens && item.Childrens.length > 0) {
|
||||
this.resetChild(item.Childrens)
|
||||
}
|
||||
if ((item.QuestionType === 1013 || item.QuestionType === 1014) && !isNaN(parseFloat(this.questionForm[this.sRoiStartDistanceId])) && !isNaN(parseFloat(this.questionForm[this.sRoiEndDistanceId]))) {
|
||||
let num = parseFloat(this.questionForm[this.sRoiEndDistanceId]) - parseFloat(this.questionForm[this.sRoiStartDistanceId])
|
||||
this.$set(this.questionForm, this.sRoiDistanceId, parseFloat(num).toFixed(this.digitPlaces))
|
||||
}
|
||||
},
|
||||
resetChild(obj) {
|
||||
obj.forEach(i => {
|
||||
@@ -483,12 +525,84 @@ export default {
|
||||
if (valueType === 0) {
|
||||
this.$set(this.questionForm, qId, parseInt(value))
|
||||
} else if (valueType === 3) {
|
||||
this.$set(this.questionForm, qId, parseFloat(value))
|
||||
this.$set(this.questionForm, qId, value)
|
||||
} else {
|
||||
this.$set(this.questionForm, qId, this.numberToFixed(value))
|
||||
}
|
||||
}
|
||||
},
|
||||
validatorNumberOrNEInput(rule, value, callback) {
|
||||
let reg = new RegExp(/^(?:-?(?:(?:0|[1-9]\d*)(?:\.\d+)?|\.\d+)|NE)$/, 'g')
|
||||
if (value === '') {
|
||||
callback();
|
||||
} else {
|
||||
if (!reg.test(value)) {
|
||||
callback(new Error(this.$t('trials:reading:ruleMessage:validatorNumberInput')));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
},
|
||||
numberOrNEInput(id) {
|
||||
this.formChanged = true
|
||||
// this.questionForm[id] = this.questionForm[id].toUpperCase();
|
||||
if (!this.questionForm[id]) {
|
||||
return
|
||||
}
|
||||
let value = this.questionForm[id].toUpperCase().replace(/[^0-9.NE]/g, '')
|
||||
if (value.startsWith('N')) {
|
||||
if (value.length > 2) {
|
||||
value = value.substring(0, 2)
|
||||
}
|
||||
if (value.length === 2 && value[1] !== 'E') {
|
||||
value = 'N'
|
||||
}
|
||||
} else {
|
||||
value = value.replace(/[NE]/g, '')
|
||||
}
|
||||
this.questionForm[id] = value
|
||||
this.$set(this.questionForm, id, value)
|
||||
},
|
||||
numberInput(id) {
|
||||
this.formChanged = true
|
||||
let value = this.questionForm[id].replace(/[^\d.]/g, '')
|
||||
this.$set(this.questionForm, id, value)
|
||||
},
|
||||
handleNumberOrNEBlur(item) {
|
||||
if (this.questionForm[item.Id] && !isNaN(parseFloat(this.questionForm[item.Id]))) {
|
||||
if (item.ValueType === 3) {
|
||||
// 文本
|
||||
this.$set(this.questionForm, item.Id, value)
|
||||
} else {
|
||||
const num = parseFloat(this.questionForm[item.Id])
|
||||
if (!isNaN(num)) {
|
||||
this.$set(this.questionForm, item.Id, parseFloat(num).toFixed(this.digitPlaces))
|
||||
if (item.ValueType === 0) {
|
||||
// 整型
|
||||
this.$set(this.questionForm, item.Id, parseInt(num))
|
||||
} else if (item.ValueType === 1 || item.ValueType === 2){
|
||||
// 小数、百分比
|
||||
this.$set(this.questionForm, item.Id, parseFloat(num).toFixed(this.digitPlaces))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
handleBlur(value, a, item) {
|
||||
if (!value || item.ValueType === 3) return false
|
||||
const num = parseFloat(this.questionForm[item.Id])
|
||||
if (!isNaN(num)) {
|
||||
this.$set(this.questionForm, item.Id, parseFloat(num).toFixed(this.digitPlaces))
|
||||
if (item.ValueType === 0) {
|
||||
// 整型
|
||||
this.$set(this.questionForm, item.Id, parseInt(num))
|
||||
} else if (item.ValueType === 1 || item.ValueType === 2){
|
||||
// 小数、百分比
|
||||
this.$set(this.questionForm, item.Id, parseFloat(num).toFixed(this.digitPlaces))
|
||||
}
|
||||
}
|
||||
},
|
||||
numberToFixed(v) {
|
||||
if (this.digitPlaces > -1) {
|
||||
return isNaN(parseFloat(v)) ? null : `${parseFloat(v).toFixed(this.digitPlaces)}`
|
||||
@@ -645,11 +759,7 @@ export default {
|
||||
}
|
||||
const res = await submitTableQuestion(params)
|
||||
if (res.IsSuccess) {
|
||||
// this.refreshQuestions()
|
||||
await this.getReadingQuestionAndAnswer(this.visitTaskId)
|
||||
this.$refs['ecrf'].getQuestions(this.visitTaskId, true)
|
||||
this.$refs['ecrf2'].getQuestions(this.visitTaskId, true)
|
||||
this.$refs['ecrf3'].getQuestions(this.visitTaskId, true)
|
||||
this.refreshQuestions()
|
||||
DicomEvent.$emit('getReportInfo', true)
|
||||
// 保存成功!
|
||||
this.addOrEdit.visible = false
|
||||
@@ -680,7 +790,8 @@ export default {
|
||||
}
|
||||
const res = await deleteReadingRowAnswer(param)
|
||||
if (res.IsSuccess) {
|
||||
await this.getReadingQuestionAndAnswer(this.visitTaskId)
|
||||
// await this.getReadingQuestionAndAnswer(this.visitTaskId)
|
||||
this.refreshQuestions()
|
||||
DicomEvent.$emit('getReportInfo', true)
|
||||
// '删除成功!'
|
||||
this.$message.success(this.$t('common:message:deletedSuccessfully'))
|
||||
@@ -695,7 +806,7 @@ export default {
|
||||
uploadTpl(lesionType, TableName) {
|
||||
this.upload.lesionType = lesionType
|
||||
this.upload.TableName = TableName
|
||||
this.upload.title = `导入( ${this.$fd('LesionType', lesionType)} )`
|
||||
this.upload.title = `${this.$fd('LesionType', lesionType)}`
|
||||
this.upload.visible = true
|
||||
},
|
||||
async downloadTpl() {
|
||||
@@ -709,7 +820,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async uploadDlgClose() {
|
||||
await this.getReadingQuestionAndAnswer(this.visitTaskId)
|
||||
this.refreshQuestions()
|
||||
DicomEvent.$emit('getReportInfo', true)
|
||||
this.upload.visible = false
|
||||
},
|
||||
@@ -727,9 +838,8 @@ export default {
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.measurement-wrapper {
|
||||
height: 100%;
|
||||
height: calc(100% - 50px);
|
||||
overflow-y: auto;
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
|
||||
@@ -879,18 +989,22 @@ 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 {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -56,11 +56,11 @@ export default {
|
||||
var data = new FormData()
|
||||
data.append('file', param.file)
|
||||
data.append('visitTaskId', this.visitTaskId)
|
||||
data.append('readingImportType', 0)
|
||||
data.append('readingImportType', 3)
|
||||
data.append('TableName', this.TableName)
|
||||
await readingImport(data)
|
||||
this.$emit('close')
|
||||
this.$message.success('导入成功!')
|
||||
this.$message.success(this.$t('common:message:savedSuccessfully'))
|
||||
loading.close()
|
||||
} catch (e) {
|
||||
loading.close()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div v-for="item in qs.Childrens" :key="item.Id">
|
||||
<div v-if="item.Type === 'basicTable'" class="flex-row" style="margin:3px 0;">
|
||||
<div class="title">{{ item.QuestionName }}</div>
|
||||
<div v-if="(item.LesionType === 112 || item.LesionType === 111) && readingTaskState < 2">
|
||||
<div v-if="(item.LesionType === 104) && readingTaskState < 2">
|
||||
<div class="add-icon" @click.prevent="downloadTpl(item.LesionType)">
|
||||
<i class="el-icon-download" />
|
||||
</div>
|
||||
@@ -49,15 +49,6 @@
|
||||
</div>
|
||||
<el-table v-if="item.Type === 'basicTable' && item.TableQuestions" :ref="item.Id"
|
||||
:data="item.TableQuestions.Answers" max-height="600">
|
||||
<!-- <el-table-column :label="$t('CustomizeQuestionFormItem:label:OrderMark')" width="60px" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
{{ getLesionName(item.OrderMark,scope.row.RowIndex) }}
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<!-- <el-table-column
|
||||
type="index"
|
||||
width="40px"
|
||||
/> -->
|
||||
<el-table-column v-for="q of item.TableQuestions.Questions" :key="q.Id" :prop="q.Id"
|
||||
:label="q.QuestionName" show-overflow-tooltip :render-header="renderHeader">
|
||||
<template slot-scope="scope">
|
||||
@@ -76,13 +67,13 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="readingTaskState < 2" :label="$t('common:action:action')" width="90px"
|
||||
<el-table-column v-if="readingTaskState < 2 && item.LesionType === 104" :label="$t('common:action:action')" width="90px"
|
||||
fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="handleAddOrEdit('edit', item, scope.$index)">
|
||||
{{ $t('common:button:edit') }}
|
||||
</el-button>
|
||||
<el-button v-if="item.LesionType === 112 || item.LesionType === 111" type="text" size="mini"
|
||||
<el-button type="text" size="mini"
|
||||
@click="handleDelete(item, scope.$index)">
|
||||
{{ $t('common:button:delete') }}
|
||||
</el-button>
|
||||
@@ -96,8 +87,11 @@
|
||||
:prop="item.Id" :rules="[
|
||||
{
|
||||
required: (item.IsRequired === 0 || (item.IsRequired === 1 && item.RelevanceId && (item.RelevanceValue.includes(questionForm[item.RelevanceId])))) && item.Type !== 'group' && item.Type !== 'summary',
|
||||
message: ['radio', 'select', 'checkbox'].includes(item.Type) ? $t('common:ruleMessage:select') : $t('common:ruleMessage:specify'), trigger: ['blur', 'change']
|
||||
message: ['radio', 'select', 'checkbox'].includes(item.Type) ? $t('common:ruleMessage:select') : (item.QuestionType === 1016 || item.QuestionType === 1017) ? $t('trials:reading:ruleMessage:validatorNumberInput') : $t('common:ruleMessage:specify'), trigger: ['blur', 'change']
|
||||
},
|
||||
{
|
||||
validator: item.Type === 'number' && !item.TypeValue && (item.QuestionType === 1016 || item.QuestionType === 1017) ? validatorNumberOrNEInput : (rule, value, callback) => { callback() }, trigger: ['blur', 'change']
|
||||
}
|
||||
]">
|
||||
<!-- 数值类型 -->
|
||||
<template v-if="item.Type === 'textarea'">
|
||||
@@ -112,6 +106,49 @@
|
||||
:label="dict.label" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-if="item.Type === 'number'">
|
||||
<div style="display: flex;justify-content: space-between;">
|
||||
<el-input
|
||||
v-if="item.QuestionType === 1016 || item.QuestionType === 1017"
|
||||
type="text"
|
||||
@change="(val) => { formItemChange(val, item) }"
|
||||
@input="numberOrNEInput(item.Id)"
|
||||
@blur="handleNumberOrNEBlur(item)"
|
||||
v-model.trim="questionForm[item.Id]" :disabled="readingTaskState === 2">
|
||||
<template slot="append" v-if="item.Unit !== 0">{{ item.Unit !== 4 ? $fd('ValueUnit', item.Unit) : item.CustomUnit }}</template>
|
||||
<template slot="append" v-else-if="item.ValueType === 2">%</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
v-else
|
||||
@change="(val) => { formItemChange(val, item) }"
|
||||
@input="numberInput(item.Id)"
|
||||
@blur="handleBlur(questionForm[item.Id], questionForm, item)"
|
||||
v-model.trim="questionForm[item.Id]" :disabled="readingTaskState === 2">
|
||||
<template slot="append" v-if="item.Unit !== 0">{{ item.Unit !== 4 ? $fd('ValueUnit', item.Unit) : item.CustomUnit }}</template>
|
||||
<template slot="append" v-else-if="item.ValueType === 2">%</template>
|
||||
</el-input>
|
||||
<!-- <el-input-number v-model="questionForm[item.Id]"
|
||||
:disabled="!isCurrentTask || readingTaskState >= 2" :precision="0"
|
||||
@change="((val) => { formItemChange(val, item) })" /> -->
|
||||
<svg-icon v-if="item.ShowChartTypeEnum > 0" icon-class="readingChart"
|
||||
class="svg-icon svg-readingChart" @click.stop="(e) => handleReadingChart({
|
||||
e,
|
||||
data: {
|
||||
QuestionId: item.Id,
|
||||
QuestionName: item.QuestionName
|
||||
}
|
||||
})" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="item.Type === 'calculation'">
|
||||
<el-input v-model="questionForm[item.Id]"
|
||||
disabled type="number" @change="((val) => { formItemChange(val, item) })">
|
||||
<template slot="append" v-if="item.Unit !== 0">
|
||||
{{ item.Unit !== 4 ? $fd('ValueUnit', item.Unit) : item.CustomUnit }}
|
||||
</template>
|
||||
<template slot="append" v-else-if="item.ValueType === 2">%</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-form-item>
|
||||
<template v-if="item.Childrens && item.Childrens.length > 0">
|
||||
<template v-for="child in item.Childrens">
|
||||
@@ -241,7 +278,10 @@ export default {
|
||||
m1Id: '',
|
||||
m2Id: '',
|
||||
m3Id: '',
|
||||
avgId: ''
|
||||
avgId: '',
|
||||
sRoiStartDistanceId: '',
|
||||
sRoiEndDistanceId: '',
|
||||
sRoiDistanceId: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -328,9 +368,21 @@ export default {
|
||||
const type = ['number', 'radio', 'select', 'input', 'textarea', 'calculation']
|
||||
questions.forEach(item => {
|
||||
if (type.includes(item.Type)) {
|
||||
const answer = item.Type === 'select' && item.OptionTypeEnum === 1 && item.Answer ? JSON.parse(item.Answer) : item.Type === 'number' || item.Type === 'calculation' ? isNaN(parseFloat(item.Answer)) ? null : parseFloat(item.Answer) : item.Answer
|
||||
const answer = item.Type === 'select' && item.OptionTypeEnum === 1 && item.Answer ? JSON.parse(item.Answer) : item.Answer
|
||||
this.$set(this.questionForm, item.Id, answer)
|
||||
}
|
||||
if (item.QuestionType === 1013) {
|
||||
// ROI起始回撤距离
|
||||
this.sRoiStartDistanceId = item.Id
|
||||
}
|
||||
if (item.QuestionType === 1014) {
|
||||
// ROI终止回撤距离
|
||||
this.sRoiEndDistanceId = item.Id
|
||||
}
|
||||
if (item.QuestionType === 1015) {
|
||||
// ROI段落总长度
|
||||
this.sRoiDistanceId = item.Id
|
||||
}
|
||||
if (item.Childrens.length > 0) {
|
||||
this.setQuestionForm(item.Childrens)
|
||||
}
|
||||
@@ -443,13 +495,17 @@ export default {
|
||||
setMeasuredData(measureData) {
|
||||
|
||||
},
|
||||
formItemChange(val, question) {
|
||||
formItemChange(val, item) {
|
||||
this.formChanged = true
|
||||
if (question.Type === 'number') {
|
||||
this.limitBlur(question.Id, question.ValueType)
|
||||
// if (item.Type === 'number') {
|
||||
// this.limitBlur(item.Id, item.ValueType)
|
||||
// }
|
||||
if (item.Childrens && item.Childrens.length > 0) {
|
||||
this.resetChild(item.Childrens)
|
||||
}
|
||||
if (question.Childrens && question.Childrens.length > 0) {
|
||||
this.resetChild(question.Childrens)
|
||||
if ((item.QuestionType === 1013 || item.QuestionType === 1014) && !isNaN(parseFloat(this.questionForm[this.sRoiStartDistanceId])) && !isNaN(parseFloat(this.questionForm[this.sRoiEndDistanceId]))) {
|
||||
let num = parseFloat(this.questionForm[this.sRoiEndDistanceId]) - parseFloat(this.questionForm[this.sRoiStartDistanceId])
|
||||
this.$set(this.questionForm, this.sRoiDistanceId, parseFloat(num).toFixed(this.digitPlaces))
|
||||
}
|
||||
},
|
||||
resetChild(obj) {
|
||||
@@ -471,6 +527,78 @@ export default {
|
||||
this.$set(this.questionForm, qId, this.numberToFixed(value))
|
||||
}
|
||||
},
|
||||
validatorNumberOrNEInput(rule, value, callback) {
|
||||
let reg = new RegExp(/^(?:-?(?:(?:0|[1-9]\d*)(?:\.\d+)?|\.\d+)|NE)$/, 'g')
|
||||
if (value === '') {
|
||||
callback();
|
||||
} else {
|
||||
if (!reg.test(value)) {
|
||||
callback(new Error(this.$t('trials:reading:ruleMessage:validatorNumberInput')));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
},
|
||||
numberOrNEInput(id) {
|
||||
this.formChanged = true
|
||||
// this.questionForm[id] = this.questionForm[id].toUpperCase();
|
||||
if (!this.questionForm[id]) {
|
||||
return
|
||||
}
|
||||
let value = this.questionForm[id].toUpperCase().replace(/[^0-9.NE]/g, '')
|
||||
if (value.startsWith('N')) {
|
||||
if (value.length > 2) {
|
||||
value = value.substring(0, 2)
|
||||
}
|
||||
if (value.length === 2 && value[1] !== 'E') {
|
||||
value = 'N'
|
||||
}
|
||||
} else {
|
||||
value = value.replace(/[NE]/g, '')
|
||||
}
|
||||
this.questionForm[id] = value
|
||||
this.$set(this.questionForm, id, value)
|
||||
},
|
||||
numberInput(id) {
|
||||
this.formChanged = true
|
||||
let value = this.questionForm[id].replace(/[^\d.]/g, '')
|
||||
this.$set(this.questionForm, id, value)
|
||||
},
|
||||
handleNumberOrNEBlur(item) {
|
||||
if (this.questionForm[item.Id] && !this.questionForm[item.Id].startsWith('N')) {
|
||||
if (item.ValueType === 3) {
|
||||
// 文本
|
||||
this.$set(this.questionForm, item.Id, value)
|
||||
} else {
|
||||
const num = parseFloat(this.questionForm[item.Id])
|
||||
if (!isNaN(num)) {
|
||||
this.$set(this.questionForm, item.Id, parseFloat(num).toFixed(this.digitPlaces))
|
||||
if (item.ValueType === 0) {
|
||||
// 整型
|
||||
this.$set(this.questionForm, item.Id, parseInt(num))
|
||||
} else if (item.ValueType === 1 || item.ValueType === 2){
|
||||
// 小数、百分比
|
||||
this.$set(this.questionForm, item.Id, parseFloat(num).toFixed(this.digitPlaces))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
handleBlur(value, a, item) {
|
||||
if (!value || item.ValueType === 3) return false
|
||||
const num = parseFloat(this.questionForm[item.Id])
|
||||
if (!isNaN(num)) {
|
||||
this.$set(this.questionForm, item.Id, parseFloat(num).toFixed(this.digitPlaces))
|
||||
if (item.ValueType === 0) {
|
||||
// 整型
|
||||
this.$set(this.questionForm, item.Id, parseInt(num))
|
||||
} else if (item.ValueType === 1 || item.ValueType === 2){
|
||||
// 小数、百分比
|
||||
this.$set(this.questionForm, item.Id, parseFloat(num).toFixed(this.digitPlaces))
|
||||
}
|
||||
}
|
||||
},
|
||||
numberToFixed(v) {
|
||||
if (this.digitPlaces > -1) {
|
||||
return isNaN(parseFloat(v)) ? null : `${parseFloat(v).toFixed(this.digitPlaces)}`
|
||||
@@ -559,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)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -631,11 +775,11 @@ export default {
|
||||
}
|
||||
const res = await submitTableQuestion(params)
|
||||
if (res.IsSuccess) {
|
||||
// this.refreshQuestions()
|
||||
await this.getReadingQuestionAndAnswer(this.visitTaskId)
|
||||
this.$refs['ecrf'].getQuestions(this.visitTaskId, true)
|
||||
this.$refs['ecrf2'].getQuestions(this.visitTaskId, true)
|
||||
this.$refs['ecrf3'].getQuestions(this.visitTaskId, true)
|
||||
this.refreshQuestions()
|
||||
// await this.getReadingQuestionAndAnswer(this.visitTaskId)
|
||||
// this.$refs['ecrf'].getQuestions(this.visitTaskId, true)
|
||||
// this.$refs['ecrf2'].getQuestions(this.visitTaskId, true)
|
||||
// this.$refs['ecrf3'].getQuestions(this.visitTaskId, true)
|
||||
DicomEvent.$emit('getReportInfo', true)
|
||||
// 保存成功!
|
||||
this.addOrEdit.visible = false
|
||||
@@ -666,7 +810,8 @@ export default {
|
||||
}
|
||||
const res = await deleteReadingRowAnswer(param)
|
||||
if (res.IsSuccess) {
|
||||
await this.getReadingQuestionAndAnswer(this.visitTaskId)
|
||||
this.refreshQuestions()
|
||||
// await this.getReadingQuestionAndAnswer(this.visitTaskId)
|
||||
DicomEvent.$emit('getReportInfo', true)
|
||||
// '删除成功!'
|
||||
this.$message.success(this.$t('common:message:deletedSuccessfully'))
|
||||
@@ -681,7 +826,7 @@ export default {
|
||||
uploadTpl(lesionType, TableName) {
|
||||
this.upload.lesionType = lesionType
|
||||
this.upload.TableName = TableName
|
||||
this.upload.title = `导入( ${this.$fd('LesionType', lesionType)} )`
|
||||
this.upload.title = `${this.$fd('LesionType', lesionType)}`
|
||||
this.upload.visible = true
|
||||
},
|
||||
async downloadTpl(lesionType) {
|
||||
@@ -689,7 +834,7 @@ export default {
|
||||
const params = {
|
||||
visitTaskId: this.visitTaskId
|
||||
}
|
||||
if (lesionType === 112) {
|
||||
if (lesionType === 104) {
|
||||
await getOCTFCTTemplate(params)
|
||||
} else {
|
||||
await getOCTLipidAngleTemplate(params)
|
||||
@@ -699,7 +844,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async uploadDlgClose() {
|
||||
await this.getReadingQuestionAndAnswer(this.visitTaskId)
|
||||
this.refreshQuestions()
|
||||
DicomEvent.$emit('getReportInfo', true)
|
||||
this.upload.visible = false
|
||||
},
|
||||
@@ -717,7 +862,7 @@ export default {
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.measurement-wrapper {
|
||||
height: 100%;
|
||||
height: calc(100% - 50px);
|
||||
overflow-y: auto;
|
||||
// overflow: hidden;
|
||||
|
||||
@@ -870,18 +1015,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 {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -72,15 +72,17 @@ export default {
|
||||
data.append('file', param.file)
|
||||
data.append('visitTaskId', this.visitTaskId)
|
||||
data.append('TableName', this.TableName)
|
||||
if (this.lesionType === 112) {
|
||||
data.append('readingImportType', 1)
|
||||
await readingImport(data)
|
||||
} else {
|
||||
data.append('readingImportType', 2)
|
||||
await readingImport(data)
|
||||
}
|
||||
data.append('readingImportType', 3)
|
||||
await readingImport(data)
|
||||
// if (this.lesionType === 112) {
|
||||
// data.append('readingImportType', 1)
|
||||
// await readingImport(data)
|
||||
// } else {
|
||||
// data.append('readingImportType', 2)
|
||||
// await readingImport(data)
|
||||
// }
|
||||
this.$emit('close')
|
||||
this.$message.success('导入成功!')
|
||||
this.$message.success(this.$t('common:message:savedSuccessfully'))
|
||||
loading.close()
|
||||
} catch (e) {
|
||||
loading.close()
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
@change="((val) => { formItemChange(val, question) })" />
|
||||
<!-- 下拉框 -->
|
||||
<el-select v-else-if="question.Type === 'select'" v-model="questionForm[question.Id]"
|
||||
:disabled="readingTaskState >= 2 || ((question.TableQuestionType === 2 || question.QuestionGenre === 2) && !!question.DictionaryCode) || isFirstChangeTask || question.QuestionType === 50 || question.QuestionType === 55"
|
||||
:disabled="readingTaskState >= 2 || ((question.TableQuestionType === 2 || question.QuestionGenre === 2) && !!question.DictionaryCode) || isFirstChangeTask || question.QuestionType === 50 || question.QuestionType === 55 || question.QuestionType === 1026"
|
||||
clearable :multiple="question.OptionTypeEnum === 1" @change="((val) => { formItemChange(val, question) })">
|
||||
<template v-if="question.TableQuestionType === 1">
|
||||
<el-option v-for="item in organList" :key="item.Id" :label="item[question.DataTableColumn]"
|
||||
@@ -169,7 +169,16 @@
|
||||
<div style="display: flex;justify-content: space-between;"
|
||||
v-else-if="question.Type === 'number' || question.Type === 'calculation'">
|
||||
<template v-if="question.Type === 'number'">
|
||||
<el-input-number v-if="question.ValueType === 0" v-model="questionForm[question.Id]" :precision="0"
|
||||
<el-input
|
||||
v-if="question.Unit"
|
||||
type="number"
|
||||
@input="(val) => questionForm[question.Id] = val.replace(/[^\d.]/g, '')"
|
||||
@blur="handleBlur(questionForm[question.Id], questionForm, question)"
|
||||
v-model.trim="questionForm[question.Id]" :disabled="readingTaskState === 2">
|
||||
<template slot="append" v-if="question.Unit !== 0">{{ question.Unit !== 4 ? $fd('ValueUnit', question.Unit) : question.CustomUnit }}</template>
|
||||
<template slot="append" v-else-if="question.ValueType === 2">%</template>
|
||||
</el-input>
|
||||
<el-input-number v-else-if="question.ValueType === 0" v-model="questionForm[question.Id]" :precision="0"
|
||||
:disabled="readingTaskState >= 2 || isFirstChangeTask" />
|
||||
<el-input-number v-else-if="question.ValueType === 3" v-model="questionForm[question.Id]"
|
||||
:disabled="readingTaskState >= 2 || isFirstChangeTask" />
|
||||
@@ -387,7 +396,21 @@ export default {
|
||||
this.fileList.splice(this.fileList.findIndex(f => f.url === file.url), 1)
|
||||
this.urls.splice(this.fileList.findIndex(f => f === file.url), 1)
|
||||
this.$emit('setFormItemData', { key: this.question.Id, val: this.urls.length > 0 ? this.urls.join('|') : '' })
|
||||
}
|
||||
},
|
||||
handleBlur(value, a, item) {
|
||||
if (!value || item.ValueType === 3) return false
|
||||
const num = parseFloat(this.questionForm[item.Id])
|
||||
if (!isNaN(num)) {
|
||||
this.$set(this.questionForm, item.Id, parseFloat(num).toFixed(this.digitPlaces))
|
||||
if (item.ValueType === 0) {
|
||||
// 整型
|
||||
this.$set(this.questionForm, item.Id, parseInt(num))
|
||||
} else if (item.ValueType === 1 || item.ValueType === 2){
|
||||
// 小数、百分比
|
||||
this.$set(this.questionForm, item.Id, parseFloat(num).toFixed(this.digitPlaces))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -179,7 +179,9 @@ export default {
|
||||
v.IsBaseLineTask = this.isBaseLineTask
|
||||
if (v.Type === 'group' && v.GroupClassify !== this.groupClassify) continue
|
||||
if (!v.IsPage && v.Type !== 'group' && v.Type !== 'summary' && i.Id) {
|
||||
this.$set(this.questionForm, v.Id, v.Answer ? v.Answer : null)
|
||||
const answer = v.Type === 'select' && v.OptionTypeEnum === 1 && v.Answer ? JSON.parse(v.Answer) : v.Answer
|
||||
this.$set(this.questionForm, v.Id, answer ? answer : null)
|
||||
// this.$set(this.questionForm, v.Id, v.Answer ? v.Answer : null)
|
||||
if (v.QuestionType === 44) {
|
||||
// 影像质量评估
|
||||
this.imageQualityId = v.Id
|
||||
@@ -210,6 +212,7 @@ export default {
|
||||
// 脾底位置
|
||||
this.spleenBottomId = v.Id
|
||||
}
|
||||
|
||||
}
|
||||
if (v.Childrens.length > 0) {
|
||||
this.setChild(v.Childrens)
|
||||
@@ -231,7 +234,11 @@ export default {
|
||||
obj.forEach(i => {
|
||||
i.IsBaseLineTask = this.isBaseLineTask
|
||||
if (i.Type !== 'group' && i.Type !== 'summary' && i.Id) {
|
||||
this.$set(this.questionForm, i.Id, i.Answer ? i.Answer : null)
|
||||
let answer = i.Type === 'select' && i.OptionTypeEnum === 1 && i.Answer ? JSON.parse(i.Answer) : i.Answer
|
||||
if (i.DictionaryCode && i.Type === 'calculation') {
|
||||
answer = this.$fd(i.DictionaryCode, parseInt(i.Answer))
|
||||
}
|
||||
this.$set(this.questionForm, i.Id, answer ? answer : null)
|
||||
if (i.QuestionType === 44) {
|
||||
// 影响质量评估
|
||||
this.imageQualityId = i.Id
|
||||
@@ -342,7 +349,7 @@ export default {
|
||||
var answers = []
|
||||
var imageQuality = null
|
||||
for (const k in this.questionForm) {
|
||||
answers.push({ id: k, answer: this.questionForm[k] })
|
||||
answers.push({ id: k, answer: Array.isArray(this.questionForm[k]) ? JSON.stringify(this.questionForm[k]) : this.questionForm[k] })
|
||||
if (k === this.imageQualityId) {
|
||||
imageQuality = this.questionForm[k]
|
||||
}
|
||||
@@ -685,7 +692,6 @@ export default {
|
||||
this.setQuestionStatus(this.questions, this.measurements[idx].QuestionType)
|
||||
},
|
||||
setQuestionStatus(obj, questionType) {
|
||||
console.log(obj, questionType)
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
if (obj[i].QuestionType === questionType) {
|
||||
this.$set(obj[i], 'SaveEnum', 1)
|
||||
@@ -785,6 +791,7 @@ export default {
|
||||
if (obj.key === this.spleenStatusId) {
|
||||
this.setSpleenCommentDisplay()
|
||||
}
|
||||
|
||||
this.formChanged = true
|
||||
},
|
||||
setQuestions() {
|
||||
|
||||
@@ -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 }
|
||||
|
||||