irc_web/src/components/Dicom/DicomViewer.vue

1338 lines
47 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div ref="dicom-wrapper" class="dicom-wrapper">
<div ref="dicomViewer" class="dicom-viewer">
<!-- <div v-for="i in layoutRow" :key="i" class="dicom-row" :style="{height: rowHeight}">
<div v-for="j in layoutCol" :key="j" class="dicom-item" oncontextmenu="return false">
<dicom-canvas ref="dicomCanvas" style="width:100%;height:100%" />
</div>
</div>-->
<div class="Anonymous" v-if="isAnonymous">
<div :class="{ btn: true, activeBtn: activeTool === 'Note_RectangleRoi' }"
@click="setToolActive($event, 'Note_RectangleRoi')">矩形</div>
<div :class="{ btn: true, activeBtn: activeTool === 'Eraser' }" @click="setToolActive($event, 'Eraser')">清除
</div>
<div class="btn" @click="anonymousImage">应用</div>
<div class="btn">应用整个序列</div>
<!-- <div class="btn">刷新图像</div> -->
<div class="btn" v-if="!isComparison">对比</div>
<div class="btn" v-else>退出</div>
<div class="btn">恢复</div>
</div>
<div v-show="layoutRow >= 1" class="dicom-row" :style="{ height: rowHeight }">
<div v-show="layoutRow >= 1 && layoutCol >= 1" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas0' }" data-index="0" @click="activateDicomCanvas(0)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas0" style="width:100%;height:100%" />
</div>
<div v-show="layoutRow >= 1 && layoutCol >= 2" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas1' }" data-index="1" @click="activateDicomCanvas(1)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas1" style="width:100%;height:100%" />
</div>
<div v-show="layoutRow >= 1 && layoutCol >= 3" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas2' }" data-index="2" @click="activateDicomCanvas(2)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas2" style="width:100%;height:100%" />
</div>
</div>
<div v-show="layoutRow >= 2" class="dicom-row" :style="{ height: rowHeight }">
<div v-show="layoutRow >= 2 && layoutCol >= 1" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas3' }" data-index="3" @click="activateDicomCanvas(3)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas3" style="width:100%;height:100%" />
</div>
<div v-show="layoutRow >= 2 && layoutCol >= 2" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas4' }" data-index="4" @click="activateDicomCanvas(4)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas4" style="width:100%;height:100%" />
</div>
<div v-show="layoutRow >= 2 && layoutCol >= 3" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas5' }" data-index="5" @click="activateDicomCanvas(5)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas5" style="width:100%;height:100%" />
</div>
</div>
<div v-show="layoutRow == 3" class="dicom-row" :style="{ height: rowHeight }">
<div v-show="layoutRow == 3 && layoutCol >= 1" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas6' }" data-index="6" @click="activateDicomCanvas(6)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas6" style="width:100%;height:100%" />
</div>
<div v-show="layoutRow == 3 && layoutCol >= 2" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas7' }" data-index="7" @click="activateDicomCanvas(7)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas7" style="width:100%;height:100%" />
</div>
<div v-show="layoutRow == 3 && layoutCol >= 3" class="dicom-item"
:class="{ 'activeItem': activeItem == 'dicomCanvas8' }" data-index="8" @click="activateDicomCanvas(8)"
@dblclick="setFullScreen($event)">
<dicom-canvas ref="dicomCanvas8" style="width:100%;height:100%" />
</div>
</div>
</div>
<div ref="dicomTools" class="dicom-tools">
<!-- 布局 -->
<div class="measureTool-wrapper">
<div class="sideTool-title">{{ $t('trials:reading:button:layout') }}</div>
<div class="sideTool-wrapper">
<label>{{ $t('trials:reading:button:layout') }}:</label>
<select class="sidetool-select" style="width:90px" :disabled="isAnonymous" @change="changeLayout($event)">
<option value="1x1" selected>1x1</option>
<option value="1x2">1x2</option>
<option value="2x1">2x1</option>
<option value="2x2">2x2</option>
<option value="1x3">1x3</option>
<option value="3x1">3x1</option>
<option value="2x3">2x3</option>
<option value="3x2">3x2</option>
<option value="3x3">3x3</option>
</select>
<div class="btnBox" @click="openAnonymous">像素匿名</div>
</div>
</div>
<!-- 图像变换 -->
<div class="measureTool-wrapper">
<div class="sideTool-title">{{ $t('trials:dicom-show:transform') }}</div>
<div class="sideTool-wrapper">
<button :title="$t('trials:reading:button:wwwc')" class="btn-link" data-tool="Wwwc"
@click="setToolActive($event, 'Wwwc')">
<svg-icon icon-class="reverse" style="font-size:20px;" />
</button>
<!-- <button
title="区域调窗"
class="btn-link"
data-tool="WwwcRegion"
@click="setToolActive($event,'WwwcRegion')"
>
<svg-icon icon-class="wwwcRegion" style="font-size:20px;" />
</button>-->
<button :title="$t('trials:reading:button:reverseColor')" class="btn-link" @click="toggleInvert">
<svg-icon icon-class="reversecolor" style="font-size:20px;" />
</button>
<button :title="$t('trials:reading:button:zoom')" class="btn-link" data-tool="Zoom"
@click="setToolActive($event, 'Zoom')">
<svg-icon icon-class="magnifier" style="font-size:20px;" />
</button>
<button :title="$t('trials:dicom-show:lens')" class="btn-link" data-tool="Magnify"
@click="setToolActive($event, 'Magnify')">
<svg-icon icon-class="zoom" style="font-size:20px;" />
</button>
<button :title="$t('trials:reading:button:rotate')" class="btn-link dropdown" data-tool="Rotate">
<svg-icon icon-class="rotate" style="font-size:20px;" />
<div class="dropdown-content">
<div @click.stop="setDicomCanvasRotate(1)">{{ $t('trials:reading:button:rotateDefault') }}</div>
<div @click.stop="setDicomCanvasRotate(2)">{{ $t('trials:reading:button:rotateHorizontal') }}</div>
<div @click.stop="setDicomCanvasRotate(3)">{{ $t('trials:reading:button:rotateVertical') }}</div>
<div @click.stop="setDicomCanvasRotate(4)">{{ $t('trials:reading:button:rotateTurnLeft') }}</div>
<div @click.stop="setDicomCanvasRotate(5)">{{ $t('trials:reading:button:rotateTurnRight') }}</div>
</div>
</button>
<button :title="$t('trials:reading:button:move')" class="btn-link" data-tool="Pan"
@click="setToolActive($event, 'Pan')">
<svg-icon icon-class="move" style="font-size:20px;" />
</button>
<button :title="$t('trials:reading:button:fitWindow')" class="btn-link" data-tool="fitToWindow"
@click="fitToType($event, 'fitToWindow')">
<svg-icon icon-class="fitToWindow" style="font-size:20px;" />
</button>
<button :title="$t('trials:reading:button:fitImage')" class="btn-link" data-tool="fitToImage"
@click="fitToType($event, 'fitToImage')">
<svg-icon icon-class="fitToImage" style="font-size:20px;" />
</button>
<!-- <button title="旋转" class="btn-link dropdown" data-tool="Rotate" @click="setToolActive($event,'Rotate')"> -->
</div>
</div>
<!-- 测量标注 -->
<div class="measureTool-wrapper">
<div class="sideTool-title">{{ $t('trials:dicom-show:measurementLabeling') }}</div>
<div class="sideTool-wrapper">
<!-- 探针 -->
<button :title="$t('trials:dicom-show:Probe')" class="btn-link" data-tool="Probe"
@click="setToolActive($event, 'Probe')">
<svg-icon icon-class="pixel" style="font-size:20px;" />
</button>
<!-- 长度测量 -->
<button :title="$t('trials:dicom-show:Length')" class="btn-link" data-tool="Length"
@click="setToolActive($event, 'Length')">
<svg-icon icon-class="length" style="font-size:20px;" />
</button>
<!-- 角度测量 -->
<button :title="$t('trials:dicom-show:Angle')" class="btn-link" data-tool="Angle"
@click="setToolActive($event, 'Angle')">
<svg-icon icon-class="angle" style="font-size:20px;" />
</button>
<!-- Cobb测量 -->
<button :title="$t('trials:dicom-show:CobbAngle')" class="btn-link" data-tool="CobbAngle"
@click="setToolActive($event, 'CobbAngle')">
<svg-icon icon-class="cobb" style="font-size:20px;" />
</button>
<!-- 椭圆测量 -->
<button :title="$t('trials:dicom-show:EllipticalRoi')" class="btn-link" data-tool="EllipticalRoi"
@click="setToolActive($event, 'EllipticalRoi')">
<svg-icon icon-class="oval" style="font-size:20px;" />
</button>
<!-- 矩形测量 -->
<button :title="$t('trials:dicom-show:RectangleRoi')" class="btn-link" data-tool="RectangleRoi"
@click="setToolActive($event, 'RectangleRoi')">
<svg-icon icon-class="rectangle" style="font-size:20px;" />
</button>
<!-- 多边形标记 -->
<button :title="$t('trials:dicom-show:FreehandRoi')" class="btn-link" data-tool="FreehandRoi"
@click="setToolActive($event, 'FreehandRoi')">
<svg-icon icon-class="polygon" style="font-size:20px;" />
</button>
<!-- 十字线 -->
<button :title="$t('trials:dicom-show:Bidirectional')" class="btn-link" data-tool="Bidirectional"
@click="setToolActive($event, 'Bidirectional')">
<svg-icon icon-class="bidirection" style="font-size:20px;" />
</button>
<!-- 文字标注 -->
<button :title="$t('trials:dicom-show:ArrowAnnotate')" class="btn-link" data-tool="ArrowAnnotate"
@click="setToolActive($event, 'ArrowAnnotate')">
<svg-icon icon-class="label" style="font-size:20px;" />
</button>
<!-- 清除测量和标记 -->
<button :title="$t('trials:dicom-show:Eraser')" class="btn-link" data-tool="Eraser"
@click="setToolActive($event, 'Eraser')">
<svg-icon icon-class="clear" style="font-size:20px;" />
</button>
<!-- 截屏 -->
<button :title="$t('trials:dicom-show:image')" class="btn-link" @click="currentDicomCanvas.saveImage()">
<svg-icon icon-class="image" style="font-size:20px;" />
</button>
<!-- 标签 -->
<button :title="$t('trials:dicom-show:tags')" class="btn-link" @click="currentDicomCanvas.showTags()">
<svg-icon icon-class="dictionary" style="font-size:20px;" />
</button>
</div>
</div>
<div class="measureTool-wrapper">
<!-- 播放 -->
<div class="sideTool-title">{{ $t('trials:dicom-show:play') }}</div>
<div class="sideTool-wrapper">
<!-- 第一帧 -->
<button class="btn-link" :title="$t('trials:dicom-show:firstframe')"
@click="currentDicomCanvas.scrollPage(-9999)">
<svg-icon icon-class="firstframe" style="font-size:20px;" />
</button>
<!-- 显示上一张影像 -->
<button class="btn-link" :title="$t('trials:dicom-show:previousframe')"
@click="currentDicomCanvas.scrollPage(-1)">
<svg-icon icon-class="previousframe" style="font-size:20px;" />
</button>
<!-- 播放 -->
<button class="btn-link" :title="$t('trials:dicom-show:play')" @click="clipPlay">
<svg-icon :icon-class="currentDicomCanvas.toolState.clipPlaying ? 'stop' : 'play'"
style="font-size:20px;" />
</button>
<!-- 下一帧 -->
<button class="btn-link" :title="$t('trials:dicom-show:nextframe')" @click="currentDicomCanvas.scrollPage(1)">
<svg-icon icon-class="nextframe" style="font-size:20px;" />
</button>
<!-- 最后一帧 -->
<button class="btn-link" :title="$t('trials:dicom-show:lastframe')"
@click="currentDicomCanvas.scrollPage(9999)">
<svg-icon icon-class="lastframe" style="font-size:20px;" />
</button>
<select v-model="fps" class="sidetool-select" style="width:60px" @change="setDicomCanvasfps($event)">
<!-- 默认值 -->
<option :value="5">5</option>
<option :value="10">10</option>
<option :value="15">15</option>
<option :value="20">20</option>
<option :value="30">30</option>
</select>
</div>
</div>
<div class="measureTool-wrapper">
<!-- 颜色 -->
<div class="sideTool-title">{{ $t('trials:dicom-show:color') }}</div>
<div class="sideTool-wrapper">
<!-- 预设窗位值 -->
<label>{{ $t('trials:dicom-show:dicomCanvasWwwc') }}:</label>
<select v-model="wwwcList[currentDicomCanvasIndex]" class="sidetool-select" style="width:100px"
@change="setDicomCanvasWwwc($event)">
<!-- 默认值 -->
<option :value="-1">{{ $t('trials:dicom-show:default') }}</option>
<!-- 自定义 -->
<option :value="0">{{ $t('trials:dicom-show:custom') }}</option>
<!-- 区域窗宽 -->
<option :value="1" style="border-bottom:1px solid #fff;">{{ $t('trials:reading:button:wwwcRegion') }}
</option>
<option :value="2">CT Abdomen</option>
<option :value="3">CT Angio</option>
<option :value="4">CT Bone</option>
<option :value="5">CT Brain</option>
<option :value="6">CT Chest</option>
<option :value="7">CT Lungs</option>
</select>
</div>
<div class="sideTool-wrapper">
<!-- 伪彩色 -->
<label>{{ $t('trials:dicom-show:pseudocolor') }}:</label>
<select v-model="colorList[currentDicomCanvasIndex]" class="sidetool-select" style="width:90px"
@change="setColormap($event)">
<!-- 默认值 -->
<option value>{{ $t('trials:dicom-show:default') }}</option>
<option v-for="(item, index) in colormapsList" :key="index" :value="item.id">{{ item.name }}</option>
</select>
</div>
</div>
<!-- 患者信息 -->
<div class="measureTool-wrapper patient-form"
v-if="type === 'Study' && modality && ['PT、CT', 'CT、PT', 'PET-CT'].includes(modality)">
<div class="sideTool-title">{{ $t('trials:tab:patientData') }}</div>
<div class="sideTool-wrapper">
<el-form ref="patientForm" size="mini" :model="formData" :rules="rules" label-width="150" v-loading="formLoading">
<!-- 性别 -->
<el-form-item :label="$t('trials:ptData:label:patientSex')" prop="PatientSex">
<el-select v-model="formData.PatientSex" :placeholder="$t('common:ruleMessage:select')"
style="width: 100%" :disabled="!isEdit">
<el-option :label="$t('trials:patientSex:male')" value="M"></el-option>
<el-option :label="$t('trials:patientSex:female')" value="F"></el-option>
</el-select>
</el-form-item>
<!-- 体重kg 例如 70.5-->
<el-form-item :label="$t('trials:ptData:label:patientWeight')" prop="PatientWeight">
<el-input v-model.number="formData.PatientWeight" :placeholder="$t('trials:patientWeight:eg')"
style="width: 100%" :disabled="!isEdit"></el-input>
</el-form-item>
<!-- 总剂量Bq 例如 740000000-->
<el-form-item :label="$t('trials:ptData:label:totalDose')" prop="RadionuclideTotalDose">
<el-input v-model.number="formData.RadionuclideTotalDose" :placeholder="$t('trials:totalDose:eg')"
style="width: 100%" :disabled="!isEdit"></el-input>
</el-form-item>
<!-- 半衰期s 例如 21600-->
<el-form-item :label="$t('trials:ptData:label:halfLife')" prop="RadionuclideHalfLife">
<el-input v-model.number="formData.RadionuclideHalfLife" :placeholder="$t('trials:halfLife:eg')"
style="width: 100%" :disabled="!isEdit"></el-input>
</el-form-item>
<!-- 注射时间s Unix 秒 或 相对秒-->
<el-form-item :label="$t('trials:ptData:label:injectTime')" prop="RadiopharmaceuticalStartTime">
<el-input v-model.number="formData.RadiopharmaceuticalStartTime" :placeholder="$t('trials:injectTime:eg')"
style="width: 100%" @input="computeTimeRelation" :disabled="!isEdit"></el-input>
</el-form-item>
<!-- 成像时间s Unix 秒 或 相对秒-->
<el-form-item :label="$t('trials:ptData:label:acquisitionTime')" prop="AcquisitionTime">
<el-input v-model.number="formData.AcquisitionTime" :placeholder="$t('trials:injectTime:eg')"
style="width: 100%" @input="computeTimeRelation" :disabled="!isEdit"></el-input>
</el-form-item>
<!-- 时间一致性检查 -->
<!-- <el-form-item :label="$t('trials:ptData:label:timeCheck')">
<el-input v-model="formData.TimeCheck" disabled style="width: 100%"></el-input>
</el-form-item> -->
<!-- 提交 -->
<el-form-item style="margin-top: 20px;text-align: right;" v-if="isEdit">
<el-button type="primary" @click="submitForm" size="small">{{ $t('trials:ptData:button:submit')
}}</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
<el-dialog v-if="customWwc.visible" :visible.sync="customWwc.visible" :close-on-click-modal="false"
:title="customWwc.title" width="400px" custom-class="base-dialog-wrapper" append-to-body>
<CustomWwwcForm @close="customWwc.visible = false" @setWwwc="setWwwc" />
</el-dialog>
</div>
</template>
<script>
import DicomCanvas from './DicomCanvas'
import CustomWwwcForm from './CustomWwwcForm'
import * as cornerstone from 'cornerstone-core'
import * as cornerstoneMath from 'cornerstone-math'
import * as cornerstoneTools from 'cornerstone-tools'
import Hammer from 'hammerjs'
// cornerstone.imageCache.setMaximumSizeBytes(0)
cornerstoneTools.external.cornerstone = cornerstone
cornerstoneTools.external.Hammer = Hammer
cornerstoneTools.external.cornerstoneMath = cornerstoneMath
console.log(cornerstoneTools, 'cornerstoneTools')
console.log(cornerstone, 'cornerstone')
import '@/utils/dialog'
import { studyMaskImage, studyUndoMaskImage } from "@/api/reading"
import {
getPatientInfo,
editPatientInfo
} from '@/api/trials'
import { setPTClinicalDataForInstance, clearPTClinicalDataCache } from '@/utils/ptClinicalDataCache'
export default {
name: 'DicomsViewer',
components: {
DicomCanvas,
CustomWwwcForm,
},
props: {
loading: {
type: Boolean,
default: false
},
modality: {
type: String,
default: ''
}
},
watch: {
modality: {
immediate: true,
handler(v) {
if (v) {
if (this.type === 'Study' && ['PT、CT', 'CT、PT', 'PET-CT'].includes(v)) {
this.$nextTick(()=>{
this.getPatientInfo()
})
}
}
}
}
},
data() {
return {
isAnonymous: false,
isComparison: false,
activeTool: '',
activeItem: 'dicomCanvas0',
layoutRow: 1,
layoutCol: 1,
currentDicomCanvasIndex: 0,
currentDicomCanvas: {
toolState: {
clipPlaying: false,
},
},
rowHeight: '100%',
sync: {
Wwwc: null,
},
colormapsList: [],
rotateList: [],
colorList: [],
wwwcList: [],
layout: null,
seriesList: [],
series: {},
customWwc: { visible: false, title: null },
fps: 15,
formData: {
Id: '',
PatientSex: '',
PatientWeight: null,
RadionuclideTotalDose: null,
RadionuclideHalfLife: null,
RadiopharmaceuticalStartTime: null,
AcquisitionTime: null,
TimeCheck: ''
},
rules: {
PatientSex: [
{ required: true, message: this.$t('common:ruleMessage:select'), trigger: 'change' }
],
PatientWeight: [
{ required: true, message: this.$t('common:ruleMessage:specify'), trigger: 'blur' },
{ type: 'number', min: 0, message: this.$t('trials:ptData:ruleMessage:number1'), trigger: 'blur' }//数值必须大于0
],
RadionuclideTotalDose: [
{ required: true, message: this.$t('common:ruleMessage:specify'), trigger: 'blur' },
{ type: 'number', min: 0, message: this.$t('trials:ptData:ruleMessage:number1'), trigger: 'blur' }//数值必须大于0
],
RadionuclideHalfLife: [
{ required: true, message: this.$t('common:ruleMessage:specify'), trigger: 'blur' },
{ type: 'number', min: 0, message: this.$t('trials:ptData:ruleMessage:number1'), trigger: 'blur' }
],
RadiopharmaceuticalStartTime: [
{ required: true, message: this.$t('common:ruleMessage:specify'), trigger: 'blur' },
{ type: 'number', message: this.$t('trials:ptData:ruleMessage:number2'), trigger: 'blur' }//请输入数字
],
AcquisitionTime: [
{ required: true, message: this.$t('common:ruleMessage:specify'), trigger: 'blur' },
{ type: 'number', message: this.$t('trials:ptData:ruleMessage:number2'), trigger: 'blur' },//请输入数字
// 自定义校验:确保成像时间不早于注射时间
{ validator: this.validateTime, trigger: 'blur' }
]
},
formLoading: false,
type: '',
isEdit: 0
}
},
mounted() {
this.customWwc = {
visible: false,
title: this.$t('DicomViewer:data:customWwc'),
}
this.rotateList[0] = '1'
this.colorList[0] = ''
this.wwwcList[0] = '-1'
this.colormapsList = cornerstone.colors.getColormapsList()
this.currentDicomCanvas = this.$refs['dicomCanvas0']
this.type = this.$route.query.type
this.isEdit = parseInt(this.$route.query.showDelete)
},
beforeDestroy() {
clearPTClinicalDataCache()
},
methods: {
anonymousImage() {
console.log(this.series, 'this.series')
this.$refs[`dicomCanvas0`].getNote_RectangleRoi().then(async obj => {
let { toolInfo, image } = obj
console.log(image, 'image')
if (!toolInfo || toolInfo.data.length <= 0) return this.$confirm(this.$t("DicomViewer:anonymous:notMark"))
let instanceInfo = this.series.instanceInfoList.find(item => item.ImageId === image.imageId)
let data = {
// SeriesId: this.series.seriesId,
instanceIdList: [instanceInfo.Id],
MaskRegionList: []
}
toolInfo.data.forEach(item => {
let currentStart = item.handles.start
let currentEnd = item.handles.end
let start = {
x: currentStart.x > currentEnd.x ? Math.round(currentEnd.x) : Math.round(currentStart.x),
y: currentStart.y > currentEnd.y ? Math.round(currentEnd.y) : Math.round(currentStart.y),
}
let end = {
x: currentStart.x > currentEnd.x ? Math.round(currentStart.x) : Math.round(currentEnd.x),
y: currentStart.y > currentEnd.y ? Math.round(currentStart.y) : Math.round(currentEnd.y),
}
let width = end.x - start.x
let height = end.y - start.y
data.MaskRegionList.push({
X: start.x,
Y: start.y,
Width: width,
Height: height
})
})
let res = await this.studyMaskImage(data)
if (!res) return false
// this.$emit("update:loading", true)
// let strs = image.imageId.split("?")
// let newImageId = `${strs[0]}-MaskImage?${strs[1]}`
// console.log(newImageId, 'newImageId')
// this.$refs[`dicomCanvas0`].reloadImage(newImageId)
})
},
openAnonymous() {
this.isAnonymous = !this.isAnonymous
if (!this.isAnonymous) {
const elements = document.querySelectorAll('.dicom-item')
const scope = this
scope.activeTool = null
Array.from(elements).forEach((element, index) => {
if (element.style.display !== 'none') {
scope.$refs[`dicomCanvas${index}`].setToolPassive(toolName)
}
})
} else {
this.activateDicomCanvas(0)
this.changeLayout('1x1')
}
},
async studyMaskImage(data) {
try {
this.$emit("update:loading", true)
let res = await studyMaskImage(data)
this.$emit("update:loading", false)
if (res.IsSuccess) {
return true
}
return false
} catch (err) {
console.log(err)
this.$emit("update:loading", false)
return false
}
},
loadImageStack(dicomSeries) {
this.currentDicomCanvas.toolState.clipPlaying = false
this.$nextTick(() => {
this.series = Object.assign({}, dicomSeries)
this.seriesList = [this.series]
this.currentDicomCanvas.loadImageStack(this.series)
if (
this.formData.PatientWeight != null ||
this.formData.RadionuclideTotalDose != null ||
this.formData.RadionuclideHalfLife != null ||
this.formData.RadiopharmaceuticalStartTime != null ||
this.formData.AcquisitionTime != null
) {
this.cachePtClinicalDataToInstances()
}
})
},
loadOtherImageStack(seriesList) {
this.$nextTick(() => {
this.seriesList = seriesList
const elements = document.querySelectorAll('.dicom-item')
Array.from(elements).forEach((element, index) => {
const canvasIndex = element.getAttribute('data-index')
if (index < seriesList.length && element.style.display !== 'none') {
const series = Object.assign({}, seriesList[index])
this.$refs[`dicomCanvas${canvasIndex}`].loadImageStack(series)
}
})
if (
this.formData.PatientWeight != null ||
this.formData.RadionuclideTotalDose != null ||
this.formData.RadionuclideHalfLife != null ||
this.formData.RadiopharmaceuticalStartTime != null ||
this.formData.AcquisitionTime != null
) {
this.cachePtClinicalDataToInstances()
}
})
},
activateDicomCanvas(index) {
if (index !== this.currentDicomCanvasIndex) {
this.currentDicomCanvasIndex = index
this.currentDicomCanvas = this.$refs[`dicomCanvas${index}`]
this.currentDicomCanvas.tabIndex = 0
this.activeItem = `dicomCanvas${index}`
if (this.sync.ReferenceLines) {
if (this.sync['ReferenceLines']) {
const elements = this.sync['ReferenceLines'].getSourceElements()
if (elements.length > 0) {
this.$refs[`dicomCanvas${index}`].removeTarget(
this.sync['ReferenceLines']
)
this.sync['ReferenceLines'].addTarget(elements[0])
this.sync['ReferenceLines'].removeSource(elements[0])
this.$refs[`dicomCanvas${index}`].addSourceElement(
this.sync['ReferenceLines']
)
}
}
}
if (!this.rotateList[this.currentDicomCanvasIndex]) {
this.rotateList[this.currentDicomCanvasIndex] = '1'
}
if (!this.colorList[this.currentDicomCanvasIndex]) {
this.colorList[this.currentDicomCanvasIndex] = ''
}
if (!this.wwwcList[this.currentDicomCanvasIndex]) {
this.wwwcList[this.currentDicomCanvasIndex] = '-1'
}
}
},
changeLayout(event) {
const arr = event.target ? event.target.value.split('x') : event.split('x')
this.layoutRow = parseInt(arr[0])
this.layoutCol = parseInt(arr[1])
this.rowHeight = 100 / this.layoutRow + '%'
this.$forceUpdate()
this.$nextTick(() => {
const elements = document.querySelectorAll('.cornerstone-element')
Array.from(elements).forEach((element) => {
cornerstone.enable(element)
cornerstone.resize(element)
})
})
},
setFullScreen(e) {
if (this.layoutRow === 1 && this.layoutCol === 1) return
if (this.layout) {
e.currentTarget.classList.remove('dicom-item-fullscreen')
const arr = this.layout.split('x')
this.layoutRow = parseInt(arr[0])
this.layoutCol = parseInt(arr[1])
this.rowHeight = 100 / this.layoutRow + '%'
this.$forceUpdate()
this.$nextTick(() => {
const elements = document.querySelectorAll('.cornerstone-element')
Array.from(elements).forEach((element) => {
cornerstone.enable(element)
cornerstone.resize(element)
})
})
this.layout = null
} else {
this.layout = `${this.layoutRow}x${this.layoutCol}`
e.currentTarget.classList.add('dicom-item-fullscreen')
cornerstone.enable(e.currentTarget.children[0])
cornerstone.resize(e.currentTarget.children[0])
}
},
fillColor() {
const elements = document.querySelectorAll('.dicom-item')
const scope = this
Array.from(elements).forEach((element, index) => {
if (element.style.display !== 'none') {
scope.$refs[`dicomCanvas${index}`].fillColor()
}
})
},
setToolActive(e, toolName) {
const elements = document.querySelectorAll('.dicom-item')
if (e.currentTarget.classList.contains('activeTool')) {
e.currentTarget.classList.remove('activeTool')
const scope = this
scope.activeTool = null
Array.from(elements).forEach((element, index) => {
if (element.style.display !== 'none') {
scope.$refs[`dicomCanvas${index}`].setToolPassive(toolName)
}
})
} else {
const toolButtons = document.querySelectorAll('[data-tool]')
Array.from(toolButtons).forEach((toolBtn) => {
toolBtn.classList.remove('activeTool')
})
e.currentTarget.classList.add('activeTool')
const scope = this
scope.activeTool = toolName
Array.from(elements).forEach((element, index) => {
if (element.style.display !== 'none') {
scope.$refs[`dicomCanvas${index}`].setToolActive(toolName)
}
})
}
},
setDicomCanvasWwwc(event) {
this.wwwcList[this.currentDicomCanvasIndex] = event.target.value
const type = parseInt(event.target.value)
if (type === -1) {
// 默认值
this.currentDicomCanvas.resetWwwc()
} else if (type === 0) {
// 自定义
this.customWwc.visible = true
} else if (type === 1) {
// 区域窗宽
this.currentDicomCanvas.setToolActive('WwwcRegion')
} else if (type === 2) {
this.currentDicomCanvas.setWwwc(400, 60)
} else if (type === 3) {
this.currentDicomCanvas.setWwwc(600, 300)
} else if (type === 4) {
this.currentDicomCanvas.setWwwc(1500, 300)
} else if (type === 5) {
this.currentDicomCanvas.setWwwc(80, 40)
} else if (type === 6) {
this.currentDicomCanvas.setWwwc(400, 40)
} else if (type === 7) {
this.currentDicomCanvas.setWwwc(1500, -400)
}
},
setWwwc(v) {
this.currentDicomCanvas.setWwwc(v.ww, v.wc)
this.customWwc.visible = false
},
toggleInvert() {
this.currentDicomCanvas.toggleInvert()
},
setDicomCanvasRotate(value) {
const type = parseInt(value)
if (type === 1) {
this.currentDicomCanvas.resetRotate()
} else if (type === 2) {
this.currentDicomCanvas.setRotate(true, false, 0, type)
} else if (type === 3) {
this.currentDicomCanvas.setRotate(false, true, 0, type)
} else if (type === 4) {
this.currentDicomCanvas.setRotate(false, false, -90, type)
} else if (type === 5) {
this.currentDicomCanvas.setRotate(false, false, 90, type)
}
},
setColormap(event) {
this.colorList[this.currentDicomCanvasIndex] = event.target.value
this.currentDicomCanvas.setColormap(event.target.value)
},
setDicomCanvasfps(event) {
this.currentDicomCanvas.setFps(event.target.value)
},
clipPlay() {
this.currentDicomCanvas.setFps(this.fps)
this.currentDicomCanvas.toggleClipPlay()
},
fitToType(e, type) {
const toolButtons = document.querySelectorAll('[data-tool]')
Array.from(toolButtons).forEach((toolBtn) => {
toolBtn.classList.remove('activeTool')
})
e.currentTarget.classList.add('activeTool')
if (type === 'fitToWindow') {
this.$refs[`dicomCanvas${this.currentDicomCanvasIndex}`].fitToWindow()
} else {
this.$refs[`dicomCanvas${this.currentDicomCanvasIndex}`].fitToImage()
}
},
setAnnotationSync(e) {
if (this.layoutRow * this.layoutCol === 1) {
return
}
this.resetSync('annotationSync')
let isActive = true
const toolBtn = e.currentTarget
if (toolBtn.classList.contains('activeTool')) {
toolBtn.classList.remove('activeTool')
isActive = false
} else {
toolBtn.classList.add('activeTool')
}
if (!this.sync.annotationSync) {
this.sync.annotationSync = new cornerstoneTools.Synchronizer(
'cornerstoneimagerendered',
cornerstoneTools.updateImageSynchronizer
)
}
const scope = this
const elements = document.querySelectorAll('.dicom-item')
Array.from(elements).forEach((element, index) => {
if (element.style.display !== 'none') {
if (isActive) {
if (index !== scope.currentDicomCanvasIndex) {
scope.$refs[`dicomCanvas${index}`].addTargetElement(
scope.sync['annotationSync']
)
} else {
scope.$refs[`dicomCanvas${index}`].addSourceElement(
scope.sync['annotationSync']
)
}
scope.$refs[`dicomCanvas${index}`].activeAnnotationSync(
scope.sync.annotationSync
)
} else {
scope.$refs[`dicomCanvas${index}`].disabledAnnotationSync(
scope.sync.annotationSync
)
}
}
})
if (!isActive) {
scope.sync.annotationSync = null
}
},
resetSync(prop) {
for (const key in this.sync) {
if (key !== prop) {
if (this.sync[prop]) {
this.sync[prop] = null
}
}
}
},
// 时间一致性校验
validateTime(rule, value, callback) {
const { RadiopharmaceuticalStartTime } = this.formData
if (value && RadiopharmaceuticalStartTime !== null && value < RadiopharmaceuticalStartTime) {
callback(new Error(this.$t('trials:ptData:ruleMessage:number3')))//成像时间不能早于注射时间
} else {
callback()
}
},
computeTimeRelation() {
const startTime = this.formData.RadiopharmaceuticalStartTime
const acquireTime = this.formData.AcquisitionTime
if (!startTime || !acquireTime) {
this.formData.TimeCheck = ''
return
}
if (startTime <= acquireTime) {
this.formData.TimeCheck = this.$t('trials:ptData:timeCheck:val1') //注射时间 ≤ 成像时间
} else {
this.formData.TimeCheck = this.$t('trials:ptData:timeCheck:val2') //注射时间 > 成像时间
}
},
async getPatientInfo() {
try {
this.formLoading = true
let studyId = this.$route.query.studyId
let res = await getPatientInfo({studyId: studyId})
this.formData = {
Id: res.Result.Id || '',
PatientSex: res.Result.PatientSex || '',
PatientWeight: parseFloat(res.Result.PatientWeight) || null,
RadionuclideTotalDose: parseFloat(res.Result.RadionuclideTotalDose) || null,
RadionuclideHalfLife: parseFloat(res.Result.RadionuclideHalfLife) || null,
RadiopharmaceuticalStartTime: parseFloat(res.Result.RadiopharmaceuticalStartTime) || '',
AcquisitionTime: parseFloat(res.Result.AcquisitionTime) || '',
TimeCheck: ''
}
this.computeTimeRelation()
// 缓存 PT 临床数据:用于 2D SUV 计算时优先使用接口/人工录入值
this.cachePtClinicalDataToInstances()
this.formLoading = false
} catch(e) {
this.formLoading = false
console.log(e)
}
},
cachePtClinicalDataToInstances() {
const clinicalData = {
PatientSex: this.formData.PatientSex,
PatientWeight: this.formData.PatientWeight,
RadionuclideTotalDose: this.formData.RadionuclideTotalDose,
RadionuclideHalfLife: this.formData.RadionuclideHalfLife,
RadiopharmaceuticalStartTime: this.formData.RadiopharmaceuticalStartTime,
AcquisitionTime: this.formData.AcquisitionTime
}
const seriesList = Array.isArray(this.seriesList) ? this.seriesList : []
seriesList.forEach(series => {
const instanceInfoList = series?.instanceInfoList
if (Array.isArray(instanceInfoList) && instanceInfoList.length > 0) {
instanceInfoList.forEach(instance => {
if (instance && instance.Id != null) {
setPTClinicalDataForInstance(instance.Id, clinicalData)
}
})
return
}
const imageIds = series?.imageIds
if (Array.isArray(imageIds) && imageIds.length > 0) {
imageIds.forEach(imageId => {
const instanceId = this.getInstanceIdFromImageId(imageId)
if (instanceId) {
setPTClinicalDataForInstance(instanceId, clinicalData)
}
})
}
})
},
getInstanceIdFromImageId(imageId) {
try {
const qIndex = String(imageId).indexOf('?')
if (qIndex === -1) return null
const params = new URLSearchParams(String(imageId).slice(qIndex + 1))
const instanceId = params.get('instanceId')
return instanceId ? String(instanceId).trim() : null
} catch (e) {
return null
}
},
refreshDicomAfterClinicalDataChanged() {
// 患者信息保存后,强制刷新画布与标注统计,确保 SUV 等数据显示使用最新的 PT 临床数据口径
const toolTypes = [
'Probe',
'RectangleRoi',
'EllipticalRoi',
'FreehandRoi',
'Bidirectional',
'Length',
'ArrowAnnotate',
'Angle',
'CobbAngle'
]
const elements = document.querySelectorAll('.dicom-item')
Array.from(elements).forEach((wrapper) => {
const index = wrapper.getAttribute('data-index')
const canvasComp = index !== null ? this.$refs[`dicomCanvas${index}`] : null
const element = canvasComp?.$refs?.canvas
if (!element) return
toolTypes.forEach((toolType) => {
const toolState = cornerstoneTools.getToolState(element, toolType)
if (toolState && Array.isArray(toolState.data)) {
toolState.data.forEach((d) => {
if (d && typeof d === 'object') {
d.invalidated = true
}
})
}
})
cornerstone.updateImage(element, true)
})
},
async submitForm() {
try {
let valid = await this.$refs.patientForm.validate()
if (!valid) return
this.formLoading = true
let res = await editPatientInfo(this.formData)
this.formLoading = false
if (res.IsSuccess) {
this.$message.success(this.$t('common:message:savedSuccessfully'))
this.cachePtClinicalDataToInstances()
this.refreshDicomAfterClinicalDataChanged()
}
} catch (e) {
this.formLoading = false
console.log(e)
}
}
}
}
</script>
<style>
.dicom-wrapper {
display: flex;
height: 100%;
}
.dicom-wrapper .case-dialog-class {
position: fixed;
left: 25%;
pointer-events: auto;
display: block;
overflow: auto;
}
.dicom-wrapper .case-dialog-div {
pointer-events: none;
}
.dicom-wrapper .case-dialog-class .el-dialog__body {
max-height: 300px;
overflow-y: auto;
}
.dicom-wrapper .el-dialog__header {
padding: 15px;
}
.dicom-wrapper .el-dialog__body {
padding: 10px 20px;
}
.dicom-viewer {
display: flex;
flex-direction: column;
flex: 1;
position: relative;
}
.Anonymous {
position: absolute;
border-radius: 5px;
bottom: 30px;
left: 5%;
right: 5%;
width: 90%;
height: 60px;
padding: 0 10px;
background-color: rgba(255, 255, 255, .2);
display: flex;
align-items: center;
justify-content: space-between;
z-index: 9999;
}
.Anonymous .btn {
width: 15%;
text-align: center;
height: 40px;
line-height: 30px;
border-radius: 15px;
background-color: rgba(255, 255, 255, .3);
cursor: pointer;
padding: 5px 10px;
border: 1px solid rgba(255, 255, 255, .7);
}
.Anonymous .btn:hover {
background-color: rgba(255, 255, 255, .5);
}
.Anonymous .activeBtn {
background-color: rgba(255, 255, 255, .5);
}
.btnBox {
display: inline-block;
width: 80px;
text-align: center;
height: 30px;
line-height: 20px;
border-radius: 15px;
background-color: rgba(255, 255, 255, .3);
cursor: pointer;
padding: 5px 10px;
border: 1px solid rgba(255, 255, 255, .7);
margin: 5px;
}
.dicom-wrapper .dicom-row {
display: flex;
flex-direction: row;
flex: 1;
width: 100%;
}
.dicom-wrapper .dicom-item {
position: relative;
width: 0;
flex: 1;
flex-shrink: 1;
box-sizing: border-box;
padding: 5px;
border: 1px solid #c8c8c8;
}
.dicom-wrapper .activeItem {
border: 2px solid chocolate;
}
.dicom-item-fullscreen {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.dicom-wrapper ::-webkit-scrollbar {
width: 7px;
height: 7px;
}
.dicom-wrapper ::-webkit-scrollbar-thumb {
border-radius: 10px;
background: gray;
}
.dicom-wrapper .dicom-tools {
/* display: flex;
flex-direction: column; */
width: 300px;
height: 100%;
background-color: #323232;
margin: 0;
padding: 0;
margin-left: 2px;
/* border: 1px solid blueviolet; */
color: #d0d0d0;
font-size: 13px;
overflow-y: auto;
}
.dicom-wrapper .measure-wrapper {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
}
.measure-wrapper .measure-list {
flex: 1;
overflow-y: auto;
}
.measure-list-header {
padding: 2px 0px;
border-bottom: 1px solid gray;
}
.measure-wrapper ul {
margin: 0;
padding: 0 5px;
list-style: none;
}
.measure-wrapper ul li {
height: 20px;
display: flex;
justify-content: space-between;
margin-bottom: 5px;
text-align: center;
}
.measure-wrapper select {
height: 20px;
background-color: rgba(0, 0, 0, 0);
/* color: #323232; */
border-radius: 4px;
font-size: 13px;
color: #606626;
}
.measure-wrapper select>option {
color: #323232;
}
/* .measure-wrapper select {
height: 20px;
background-color: rgba(0, 0, 0, 0);
color: #d0d0d0;
border-radius: 4px;
}
.measure-wrapper select > option {
background-color: #323232;
} */
.dicom-canvas {
position: relative;
width: 100%;
height: 100%;
border: 1px solid #333333;
}
.dicom-canvas.active {
border: 1px solid #337ab7;
}
.sideTool-title {
padding: 5px 8px;
background-color: #525252;
color: #bebdbd;
font-size: 14px;
margin: 4px;
}
.sideTool-wrapper {
border-bottom: 1px solid gray;
margin: 0px 4px;
padding: 4px;
font-size: 12px;
}
.sideTool-wrapper .btn {
font-size: 12px;
}
.sidetool-select {
height: 30px;
background-color: rgba(0, 0, 0, 0);
color: #d0d0d0;
border-radius: 4px;
}
.sidetool-select>option {
background-color: #323232;
}
.sideTool-wrapper button.btn-link {
height: 40px;
padding: 8px !important;
border: 1px solid rgba(37, 37, 37, 1);
margin: 1px 1px;
}
.sideTool-wrapper .btn-link {
display: inline-block;
height: 40px;
padding: 8px !important;
border: 1px solid rgba(37, 37, 37, 1);
margin: 1px 1px;
}
.dicom-wrapper .btn-group {
position: relative;
display: inline-block;
height: 30px;
padding: 6px 3px;
vertical-align: middle;
margin: 1px 1px;
border-radius: 3px;
font-size: 12px;
font-weight: 500;
color: silver;
line-height: 20px !important;
background: rgba(50, 50, 50, 1);
border: 1px solid rgba(37, 37, 37, 1);
min-height: 30px;
}
.dicom-wrapper .btn-group .btn-left {
position: relative;
float: left;
padding: 1px !important;
margin-left: 0;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.dicom-wrapper .activeTool {
background: #16477b90 !important;
border: none;
}
.dicom-wrapper .btn-link {
color: #cccccc;
background-color: #ffffff00;
cursor: pointer;
}
.dicom-wrapper .btn-submit {
cursor: pointer;
/* background-color: #eeeeee; */
background: #eeeeee;
border: 1px solid #dcdfe6;
border-radius: 4px;
color: #606266;
font-size: 13px;
}
/* .dicom-wrapper .btn-link:hover,
.dicom-wrapper .btn-link:focus,
.dicom-wrapper .btn-link:active {
border-color: transparent;
background-color: transparent;
} */
.dicom-wrapper .iconHover:hover {
color: red;
}
.dicom-wrapper .dropdown {
position: relative;
display: inline-block;
}
.dicom-wrapper .dropdown-content {
display: none;
position: absolute;
left: -20px;
top: 40px;
color: #d0d0d0;
background-color: #323232;
min-width: 100px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
border: 1px solid #4e4e4e;
padding: 5px;
}
.dicom-wrapper .dropdown:hover .dropdown-content {
display: block;
}
.dicom-wrapper .dropdown-content div {
height: 25px;
line-height: 25px;
cursor: point;
}
.dicom-wrapper .dropdown-content div:hover {
background-color: #16477b90;
}
.patient-form .el-form-item {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.patient-form .el-form-item__label {
color: #d0d0d0;
flex: 0 0 150px;
text-align: left;
}
.patient-form .el-form-item__content {
flex: 1;
margin-left: 0;
}
.patient-form .el-input.is-disabled .el-input__inner {
background-color: #424244;
}
.patient-form .el-input .el-input__inner {
background-color: #323232;
color: #d0d0d0;
}
</style>