Merge branch 'main' into uat_us
# Conflicts: # src/views/trials/trials-panel/reading/dicoms3D/components/customize/ReportPage.vueuat_us
commit
c339adb73f
File diff suppressed because it is too large
Load Diff
|
|
@ -408,3 +408,19 @@ export function changeSegmentationSavedStatus(data) {
|
|||
data
|
||||
})
|
||||
}
|
||||
// 图像数据匿名
|
||||
export function studyMaskImage(data) {
|
||||
return request({
|
||||
url: `/Study/studyMaskImage`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
// 撤销匿名
|
||||
export function studyUndoMaskImage(data) {
|
||||
return request({
|
||||
url: `/Study/studyUndoMaskImage`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
@ -4431,3 +4431,21 @@ export function updateImageResizePath(data) {
|
|||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取PET图像上患者信息
|
||||
export function getPatientInfo(data) {
|
||||
return request({
|
||||
url: `/Study/getPatientInfo`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
//编辑患者基本信息
|
||||
export function editPatientInfo(data) {
|
||||
return request({
|
||||
url: `/Study/editPatientInfo`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
@ -23,17 +23,11 @@
|
|||
</div>
|
||||
<div
|
||||
v-if="(dicomInfo.modality === 'CT' || dicomInfo.modality === 'DR' || dicomInfo.modality === 'CR') && mousePosition.mo">
|
||||
HU: {{ mousePosition.mo }}
|
||||
</div>
|
||||
<div v-else-if="(dicomInfo.modality === 'PT' && mousePosition.suv)">
|
||||
SUVbw(g/ml): {{ mousePosition.suv.toFixed(3) }}
|
||||
</div>
|
||||
<div v-else-if="mousePosition.mo">
|
||||
Density: {{ mousePosition.mo }}
|
||||
</div>
|
||||
<div>
|
||||
W*H: {{ dicomInfo.size }}
|
||||
</div>
|
||||
HU: {{ mousePosition.mo }}</div>
|
||||
<div v-else-if="(dicomInfo.modality === 'PT' && mousePosition.suv)">SUVbw(g/ml): {{ mousePosition.suv.toFixed(3)
|
||||
}}</div>
|
||||
<div v-else-if="mousePosition.mo">Density: {{ mousePosition.mo }}</div>
|
||||
<div>W*H: {{ dicomInfo.size }}</div>
|
||||
|
||||
<div>Zoom: {{ dicomInfo.zoom }}</div>
|
||||
</div>
|
||||
|
|
@ -56,21 +50,14 @@
|
|||
style="z-index:10;background: #9e9e9e;height: 20px;width: 100%;position: absolute;top: 0;cursor: move"
|
||||
@mousedown="sliderMousedown($event)" />
|
||||
</div>
|
||||
<div style="position: absolute;left: 50%;top: 15px;color: #f44336;">
|
||||
{{ markers.top }}
|
||||
</div>
|
||||
<div style="position: absolute;top: 50%;right: 15px;color: #f44336;">
|
||||
{{ markers.right }}
|
||||
</div>
|
||||
<div style="position: absolute;left: 50%;top: 15px;color: #f44336;">{{ markers.top }}</div>
|
||||
<div style="position: absolute;top: 50%;right: 15px;color: #f44336;">{{ markers.right }}</div>
|
||||
|
||||
<div style="position: absolute;left: 50%;bottom: 15px;color: #f44336;">
|
||||
{{ markers.bottom }}
|
||||
</div>
|
||||
<div style="position: absolute;top: 50%;left: 15px;color: #f44336;">
|
||||
{{ markers.left }}
|
||||
</div>
|
||||
<div style="position: absolute;left: 50%;bottom: 15px;color: #f44336;">{{ markers.bottom }}</div>
|
||||
<div style="position: absolute;top: 50%;left: 15px;color: #f44336;">{{ markers.left }}</div>
|
||||
|
||||
<div class="info-instance">
|
||||
<svg-icon icon-class="IsMasked" style="font-size:20px;" v-show="dicomInfo.IsMasked" />
|
||||
<!-- <div v-show="dicomInfo.pixel">
|
||||
Pixel: {{ dicomInfo.pixel }}mm
|
||||
</div>-->
|
||||
|
|
@ -92,15 +79,18 @@
|
|||
import * as cornerstone from 'cornerstone-core'
|
||||
import * as cornerstoneMath from 'cornerstone-math'
|
||||
import * as cornerstoneTools from 'cornerstone-tools'
|
||||
import metaDataProvider from '@/utils/metaDataProvider'
|
||||
|
||||
const scroll = cornerstoneTools.import('util/scrollToIndex')
|
||||
import Hammer from 'hammerjs'
|
||||
import getOrientationString from '@/views/trials/trials-panel/reading/dicoms/tools/OrientationMarkers/getOrientationString'
|
||||
import invertOrientationString from '@/views/trials/trials-panel/reading/dicoms/tools/OrientationMarkers/invertOrientationString'
|
||||
|
||||
import calculateSUV from '@/views/trials/trials-panel/reading/dicoms/tools/calculateSUV'
|
||||
const calculateSUV = cornerstoneTools.import('util/calculateSUV')
|
||||
// import requestPoolManager from '@/utils/request-pool'
|
||||
import ScaleOverlayTool from '@/views/trials/trials-panel/reading/dicoms/tools/ScaleOverlay/ScaleOverlayTool'
|
||||
import Note_RectangleRoiTool from '@/views/trials/trials-panel/reading/dicoms/tools/RectangleRoi/Note_RectangleRoiTool'
|
||||
let isMetaDataProviderAdded = false
|
||||
cornerstoneTools.external.cornerstone = cornerstone
|
||||
cornerstoneTools.external.Hammer = Hammer
|
||||
cornerstoneTools.external.cornerstoneMath = cornerstoneMath
|
||||
|
|
@ -117,7 +107,7 @@ export default {
|
|||
computed: {
|
||||
NSTip() {
|
||||
return `${this.$store.state.trials.downloadSize}, NS: ${this.$store.state.trials.downloadTip}`
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -130,7 +120,7 @@ export default {
|
|||
seriesNumber: '',
|
||||
imageIds: [],
|
||||
currentImageIdIndex: 0,
|
||||
firstImageLoading: false
|
||||
firstImageLoading: false,
|
||||
// preventCache: true
|
||||
},
|
||||
dicomInfo: {
|
||||
|
|
@ -150,14 +140,15 @@ export default {
|
|||
wwwc: '',
|
||||
zoom: 0,
|
||||
location: '',
|
||||
fps: 5
|
||||
fps: 5,
|
||||
IsMasked: false
|
||||
},
|
||||
toolState: {
|
||||
initialized: false,
|
||||
activeTool: 'none',
|
||||
dicomInfoVisible: false,
|
||||
clipPlaying: false,
|
||||
viewportInvert: false
|
||||
viewportInvert: false,
|
||||
},
|
||||
loadImagePromise: null,
|
||||
AnnotationSync: null,
|
||||
|
|
@ -168,18 +159,20 @@ export default {
|
|||
sliderInfo: {
|
||||
oldB: null,
|
||||
oldM: null,
|
||||
isMove: false
|
||||
isMove: false,
|
||||
},
|
||||
mousePosition: { x: '', y: '', mo: '' },
|
||||
markers: { top: '', right: '', bottom: '', left: '' },
|
||||
orientationMarkers: [],
|
||||
originalMarkers: [],
|
||||
dcmTag: { visible: false, title: this.$t('trials:dicom-tag:title') }
|
||||
dcmTag: { visible: false, title: this.$t('trials:dicom-tag:title') },
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.type = this.$router.currentRoute.query.type ? this.$router.currentRoute.query.type : ''
|
||||
this.type = this.$router.currentRoute.query.type
|
||||
? this.$router.currentRoute.query.type
|
||||
: ''
|
||||
this.canvas = this.$refs.canvas
|
||||
this.canvas.addEventListener('cornerstonenewimage', this.onNewImage)
|
||||
this.canvas.addEventListener(
|
||||
|
|
@ -200,7 +193,10 @@ export default {
|
|||
document.addEventListener('mousemove', (e) => {
|
||||
this.sliderMousemove(e)
|
||||
})
|
||||
this.canvas.addEventListener('cornerstonetoolsstackscroll', this.stackScrollCallback)
|
||||
this.canvas.addEventListener(
|
||||
'cornerstonetoolsstackscroll',
|
||||
this.stackScrollCallback
|
||||
)
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -210,7 +206,11 @@ export default {
|
|||
this.stack.seriesId = dicomSeries.seriesId
|
||||
this.stack.seriesNumber = dicomSeries.seriesNumber
|
||||
this.stack.imageIds = dicomSeries.imageIds
|
||||
this.stack.currentImageIdIndex = dicomSeries.imageIdIndex && dicomSeries.imageIdIndex < dicomSeries.imageIds.length ? dicomSeries.imageIdIndex : 0
|
||||
this.stack.currentImageIdIndex =
|
||||
dicomSeries.imageIdIndex &&
|
||||
dicomSeries.imageIdIndex < dicomSeries.imageIds.length
|
||||
? dicomSeries.imageIdIndex
|
||||
: 0
|
||||
this.stack.firstImageLoading = true
|
||||
this.stack.description = dicomSeries.description
|
||||
this.toolState.viewportInvert = false
|
||||
|
|
@ -221,18 +221,27 @@ export default {
|
|||
// this.stack.instanceId = instanceId
|
||||
this.toolState.clipPlaying = false
|
||||
const element = this.$refs.canvas
|
||||
if (!isMetaDataProviderAdded) {
|
||||
// 注册自定义 metaDataProvider:统一 SUV 口径(优先接口,缺失回退 DICOM)
|
||||
cornerstone.metaData.addProvider(metaDataProvider, 100000)
|
||||
isMetaDataProviderAdded = true
|
||||
}
|
||||
cornerstone.enable(element)
|
||||
cornerstoneTools.stopClip(this.canvas)
|
||||
this.toolState.clipPlaying = false
|
||||
this.loading = true
|
||||
|
||||
cornerstone.loadAndCacheImage(this.stack.imageIds[this.stack.currentImageIdIndex])
|
||||
.then(image => {
|
||||
cornerstone
|
||||
.loadAndCacheImage(
|
||||
this.stack.imageIds[this.stack.currentImageIdIndex]
|
||||
)
|
||||
.then((image) => {
|
||||
this.loading = false
|
||||
if (this.stack.imageIds.indexOf(image.imageId) !== -1) {
|
||||
this.onFirstImageLoaded(image)
|
||||
}
|
||||
}).catch((error) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
this.loading = false
|
||||
if (error.error && error.error.message) {
|
||||
this.$alert(error.error.message)
|
||||
|
|
@ -255,15 +264,17 @@ export default {
|
|||
const apiTool = cornerstoneTools[`${toolName}Tool`]
|
||||
|
||||
if (apiTool) {
|
||||
const toolAlreadyAddedToElement = cornerstoneTools.getToolForElement(
|
||||
element,
|
||||
apiTool
|
||||
)
|
||||
const toolAlreadyAddedToElement =
|
||||
cornerstoneTools.getToolForElement(element, apiTool)
|
||||
if (!toolAlreadyAddedToElement) {
|
||||
if (toolName === 'RectangleRoi') {
|
||||
cornerstoneTools.addToolForElement(element, apiTool, { configuration: { showMinMax: true, showStatsText: true } })
|
||||
cornerstoneTools.addToolForElement(element, apiTool, {
|
||||
configuration: { showMinMax: true, showStatsText: true },
|
||||
})
|
||||
} else if (toolName === 'EllipticalRoi') {
|
||||
cornerstoneTools.addToolForElement(element, apiTool, { configuration: { showMinMax: true } })
|
||||
cornerstoneTools.addToolForElement(element, apiTool, {
|
||||
configuration: { showMinMax: true },
|
||||
})
|
||||
} else {
|
||||
cornerstoneTools.addToolForElement(element, apiTool)
|
||||
}
|
||||
|
|
@ -287,8 +298,28 @@ export default {
|
|||
false
|
||||
)
|
||||
})
|
||||
if (!cornerstoneTools.getToolForElement(element, cornerstoneTools.WwwcRegionTool)) {
|
||||
cornerstoneTools.addToolForElement(element, cornerstoneTools.WwwcRegionTool)
|
||||
if (
|
||||
!cornerstoneTools.getToolForElement(element, Note_RectangleRoiTool)
|
||||
) {
|
||||
cornerstoneTools.addToolForElement(element, Note_RectangleRoiTool, {
|
||||
configuration: {
|
||||
color: '#f00',
|
||||
lineWidth: 0.5,
|
||||
drawHandles: false,
|
||||
fillColor: 'rgba(0, 0, 0, 1)',
|
||||
},
|
||||
})
|
||||
}
|
||||
if (
|
||||
!cornerstoneTools.getToolForElement(
|
||||
element,
|
||||
cornerstoneTools.WwwcRegionTool
|
||||
)
|
||||
) {
|
||||
cornerstoneTools.addToolForElement(
|
||||
element,
|
||||
cornerstoneTools.WwwcRegionTool
|
||||
)
|
||||
}
|
||||
if (
|
||||
!cornerstoneTools.getToolForElement(
|
||||
|
|
@ -347,20 +378,24 @@ export default {
|
|||
// var instanceId = image.imageId.split('/')[image.imageId.split('/').length - 1]
|
||||
// instanceId = instanceId.split('.')[0]
|
||||
// this.stack.instanceId = instanceId
|
||||
this.height = (this.stack.currentImageIdIndex) * 100 / (this.stack.imageIds.length - 1)
|
||||
this.height =
|
||||
(this.stack.currentImageIdIndex * 100) /
|
||||
(this.stack.imageIds.length - 1)
|
||||
this.resetWwwc()
|
||||
},
|
||||
onNewImage(e) {
|
||||
e.detail.enabledElement.options = {}
|
||||
var data = e.detail.image.data
|
||||
this.dicomInfo.hospital = data.string('x00080080')
|
||||
let instanceInfo = this.series.instanceInfoList.find(item => item.ImageId === e.detail.image.imageId)
|
||||
this.dicomInfo.IsMasked = instanceInfo.IsMasked
|
||||
// this.dicomInfo.pid = data.string('x00100020')
|
||||
this.dicomInfo.pid = data.string('x00120040')
|
||||
this.dicomInfo.name = data.string('x00100010')
|
||||
this.dicomInfo.age = data.string('x00101010')
|
||||
this.dicomInfo.sex = data.string('x00100040')
|
||||
this.dicomInfo.acc = data.string('x00080050') // 登记号
|
||||
this.dicomInfo.modality = data.string('x00080060')
|
||||
this.dicomInfo.modality = (data.string('x00080060') || '').trim()
|
||||
this.dicomInfo.time = this.formatDicomDateTime(
|
||||
data.string('x00080020'),
|
||||
data.string('x00080030')
|
||||
|
|
@ -379,21 +414,30 @@ export default {
|
|||
if (this.dicomInfo.thick) {
|
||||
this.dicomInfo.thick = this.dicomInfo.thick.toFixed(2)
|
||||
}
|
||||
const newImageIdIndex = this.stack.imageIds.findIndex(i => i === e.detail.image.imageId)
|
||||
const newImageIdIndex = this.stack.imageIds.findIndex(
|
||||
(i) => i === e.detail.image.imageId
|
||||
)
|
||||
if (newImageIdIndex === -1) return
|
||||
this.stack.currentImageIdIndex = newImageIdIndex
|
||||
this.stack.imageIdIndex = newImageIdIndex
|
||||
this.series.imageIdIndex = newImageIdIndex
|
||||
this.height = (this.stack.currentImageIdIndex) * 100 / (this.stack.imageIds.length - 1)
|
||||
this.height =
|
||||
(this.stack.currentImageIdIndex * 100) /
|
||||
(this.stack.imageIds.length - 1)
|
||||
this.resetWwwc()
|
||||
},
|
||||
stackScrollCallback(e) {
|
||||
const { detail } = e
|
||||
if (this.isScrollSync) {
|
||||
this.$emit('scrollSync', { canvasIndex: this.canvasIndex, direction: detail.direction })
|
||||
this.$emit('scrollSync', {
|
||||
canvasIndex: this.canvasIndex,
|
||||
direction: detail.direction,
|
||||
})
|
||||
}
|
||||
this.stack.currentImageIdIndex = e.detail.newImageIdIndex
|
||||
this.height = (this.stack.currentImageIdIndex) * 100 / (this.stack.imageIds.length - 1)
|
||||
this.height =
|
||||
(this.stack.currentImageIdIndex * 100) /
|
||||
(this.stack.imageIds.length - 1)
|
||||
// var priority = new Date(new Date().setHours(23, 59, 59, 999)).getTime()
|
||||
|
||||
// requestPoolManager.loadAndCacheImagePlus(this.stack.imageIds[this.stack.currentImageIdIndex], this.stack.seriesId, priority)
|
||||
|
|
@ -408,7 +452,9 @@ export default {
|
|||
if (date) {
|
||||
date = `${date.substr(0, 4)}-${date.substr(4, 2)}-${date.substr(6, 2)}`
|
||||
}
|
||||
if (time) { time = `${time.substr(0, 2)}:${time.substr(2, 2)}:${time.substr(4, 2)}` }
|
||||
if (time) {
|
||||
time = `${time.substr(0, 2)}:${time.substr(2, 2)}:${time.substr(4, 2)}`
|
||||
}
|
||||
return time ? `${date} ${time}` : `${date} 00:00:00`
|
||||
},
|
||||
|
||||
|
|
@ -453,7 +499,7 @@ export default {
|
|||
top: oppositeColumn,
|
||||
bottom: column,
|
||||
left: oppositeRow,
|
||||
right: row
|
||||
right: row,
|
||||
}
|
||||
if (!markers) {
|
||||
return
|
||||
|
|
@ -500,13 +546,7 @@ export default {
|
|||
if (image.color) {
|
||||
stats.storedPixels = this.getRGBPixels(element, x, y, 1, 1)
|
||||
} else {
|
||||
stats.storedPixels = cornerstone.getStoredPixels(
|
||||
element,
|
||||
x,
|
||||
y,
|
||||
1,
|
||||
1
|
||||
)
|
||||
stats.storedPixels = cornerstone.getStoredPixels(element, x, y, 1, 1)
|
||||
stats.sp = stats.storedPixels[0]
|
||||
stats.mo = stats.sp * image.slope + image.intercept
|
||||
stats.suv = calculateSUV(image, stats.sp)
|
||||
|
|
@ -533,7 +573,8 @@ export default {
|
|||
if (enabledElement.image.color) {
|
||||
for (row = 0; row < height; row++) {
|
||||
for (column = 0; column < width; column++) {
|
||||
spIndex = ((row + y) * enabledElement.image.columns + (column + x)) * 4
|
||||
spIndex =
|
||||
((row + y) * enabledElement.image.columns + (column + x)) * 4
|
||||
const red = pixelData[spIndex]
|
||||
const green = pixelData[spIndex + 1]
|
||||
const blue = pixelData[spIndex + 2]
|
||||
|
|
@ -551,7 +592,8 @@ export default {
|
|||
},
|
||||
sliderMousedown(e) {
|
||||
var boxHeight = this.$refs['sliderBox'].clientHeight
|
||||
this.sliderInfo.oldB = parseInt(e.srcElement.style.top) * boxHeight / 100
|
||||
this.sliderInfo.oldB =
|
||||
(parseInt(e.srcElement.style.top) * boxHeight) / 100
|
||||
this.sliderInfo.oldM = e.clientY
|
||||
this.sliderInfo.isMove = true
|
||||
e.stopImmediatePropagation()
|
||||
|
|
@ -564,9 +606,14 @@ export default {
|
|||
var boxHeight = this.$refs['sliderBox'].clientHeight
|
||||
if (PX < 0) return
|
||||
if (PX > boxHeight) return
|
||||
var height = PX * 100 / boxHeight
|
||||
var index = Math.trunc(this.stack.imageIds.length * this.height / 100)
|
||||
index = index > this.stack.imageIds.length ? this.stack.imageIds.length : index < 0 ? 0 : index
|
||||
var height = (PX * 100) / boxHeight
|
||||
var index = Math.trunc((this.stack.imageIds.length * this.height) / 100)
|
||||
index =
|
||||
index > this.stack.imageIds.length
|
||||
? this.stack.imageIds.length
|
||||
: index < 0
|
||||
? 0
|
||||
: index
|
||||
// if (!cornerstone.imageCache.getImageLoadObject(this.stack.imageIds[index])) return
|
||||
this.height = height
|
||||
if (this.stack.currentImageIdIndex !== index) {
|
||||
|
|
@ -578,9 +625,9 @@ export default {
|
|||
},
|
||||
goViewer(e) {
|
||||
// console.log(this.$refs['sliderBox'].clientHeight)
|
||||
var height = e.offsetY * 100 / this.$refs['sliderBox'].clientHeight
|
||||
var height = (e.offsetY * 100) / this.$refs['sliderBox'].clientHeight
|
||||
this.height = height
|
||||
var index = Math.trunc(this.stack.imageIds.length * this.height / 100)
|
||||
var index = Math.trunc((this.stack.imageIds.length * this.height) / 100)
|
||||
scroll(this.canvas, index)
|
||||
},
|
||||
onClipStopped() {
|
||||
|
|
@ -673,10 +720,7 @@ export default {
|
|||
}
|
||||
this.toolState.clipPlaying = true
|
||||
cornerstoneTools.playClip(this.canvas, this.dicomInfo.fps)
|
||||
cornerstoneTools.getToolState(
|
||||
this.canvas,
|
||||
'playClip'
|
||||
).data[0].loop = true
|
||||
cornerstoneTools.getToolState(this.canvas, 'playClip').data[0].loop = true
|
||||
},
|
||||
setFps(fps) {
|
||||
this.dicomInfo.fps = fps
|
||||
|
|
@ -712,8 +756,8 @@ export default {
|
|||
invert: false,
|
||||
preventZoomOutsideImage: false,
|
||||
minScale: 0.1,
|
||||
maxScale: 20.0
|
||||
}
|
||||
maxScale: 20.0,
|
||||
},
|
||||
})
|
||||
cornerstoneTools.setToolActive('Zoom', { mouseButtonMask: 1 })
|
||||
this.toolState.activeTool = 'zoom'
|
||||
|
|
@ -797,9 +841,110 @@ export default {
|
|||
setToolPassive(toolName) {
|
||||
cornerstoneTools.setToolPassiveForElement(this.canvas, toolName)
|
||||
},
|
||||
async reloadImage(newImageId = null) {
|
||||
// 1. 获取当前imageId(如果未指定新imageId)
|
||||
let element = this.canvas
|
||||
this.stack.imageIds.splice(this.stack.currentImageIdIndex, 1, newImageId)
|
||||
const currentImageId =
|
||||
newImageId || cornerstone.getImage(element)?.imageId
|
||||
|
||||
if (!currentImageId) {
|
||||
console.error('没有找到可用的imageId')
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 清除该影像关联的工具状态(标注数据)
|
||||
this.clearToolStateForImage(element, currentImageId)
|
||||
|
||||
// 3. 从缓存中移除该影像
|
||||
this.removeImageFromCache(currentImageId)
|
||||
|
||||
// 4. 重新加载并渲染
|
||||
await this.loadAndRenderImage(element, currentImageId)
|
||||
},
|
||||
clearToolStateForImage(element, imageId) {
|
||||
// 获取全局状态管理器
|
||||
const globalToolStateManager =
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager
|
||||
|
||||
if (globalToolStateManager) {
|
||||
// 方式1:清空该影像的全部工具数据
|
||||
globalToolStateManager.clearImageIdToolState(imageId)
|
||||
console.log(`已清除影像 ${imageId} 的所有标注数据`)
|
||||
}
|
||||
|
||||
// 方式2:如果使用元素级别的状态管理器
|
||||
const elementToolStateManager =
|
||||
cornerstoneTools.getElementToolStateManager(element)
|
||||
if (elementToolStateManager && elementToolStateManager.get(element)) {
|
||||
// 清空该元素上当前显示影像的数据
|
||||
elementToolStateManager.clear(element)
|
||||
}
|
||||
},
|
||||
removeImageFromCache(imageId) {
|
||||
const imageCache = cornerstone.imageCache
|
||||
|
||||
if (imageCache && imageCache[imageId]) {
|
||||
// 从缓存中移除
|
||||
delete imageCache[imageId]
|
||||
console.log(`已从缓存中移除影像: ${imageId}`)
|
||||
}
|
||||
|
||||
// 也可以使用官方API(如果可用)
|
||||
if (typeof cornerstone.imageCache.removeImage === 'function') {
|
||||
cornerstone.imageCache.removeImageLoadObject(imageId)
|
||||
}
|
||||
},
|
||||
async loadAndRenderImage(element, imageId) {
|
||||
try {
|
||||
// 1. 清除当前画布内容(可选)
|
||||
const canvas = element.querySelector('canvas')
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
// 2. 重新加载图像(强制从源头重新获取)
|
||||
const image = await cornerstone.loadAndCacheImage(imageId)
|
||||
|
||||
// 3. 获取或创建视口设置
|
||||
let viewport = cornerstone.getViewport(element)
|
||||
if (!viewport) {
|
||||
viewport = cornerstone.getDefaultViewport(element, image)
|
||||
}
|
||||
|
||||
// 4. 显示图像
|
||||
cornerstone.displayImage(element, image, viewport)
|
||||
|
||||
// 5. 触发重绘
|
||||
cornerstone.updateImage(element)
|
||||
|
||||
console.log(`影像 ${imageId} 重新加载并渲染完成`)
|
||||
|
||||
return image
|
||||
} catch (error) {
|
||||
console.error('加载图像失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
getNote_RectangleRoi() {
|
||||
return new Promise(resolve => {
|
||||
let toolInfo = cornerstoneTools.getToolState(this.canvas, 'Note_RectangleRoi')
|
||||
let image = cornerstone.getImage(this.canvas)
|
||||
resolve({ toolInfo, image })
|
||||
})
|
||||
// console.log(
|
||||
// cornerstoneTools.getToolState(this.canvas, 'Note_RectangleRoi')
|
||||
// )
|
||||
// console.log(cornerstone.getImage(this.canvas))
|
||||
// let image = cornerstone.getImage(this.canvas)
|
||||
// // cornerstone.imageCache.removeImageLoadObject(image.imageId)
|
||||
|
||||
// this.reloadImage(this.canvas, image.imageId)
|
||||
},
|
||||
setToolActive(toolName) {
|
||||
cornerstoneTools.setToolActiveForElement(this.canvas, toolName, {
|
||||
mouseButtonMask: 1
|
||||
mouseButtonMask: 1,
|
||||
})
|
||||
},
|
||||
setAllToolsPassive() {
|
||||
|
|
@ -833,7 +978,7 @@ export default {
|
|||
)
|
||||
}
|
||||
cornerstoneTools.setToolEnabledForElement(this.canvas, 'ReferenceLines', {
|
||||
synchronizationContext: synchronizer
|
||||
synchronizationContext: synchronizer,
|
||||
})
|
||||
|
||||
// cornerstoneTools.addTool(cornerstoneTools.CrosshairsTool)
|
||||
|
|
@ -864,7 +1009,7 @@ export default {
|
|||
}
|
||||
cornerstoneTools.setToolActiveForElement(this.canvas, toolName, {
|
||||
mouseButtonMask: 1,
|
||||
synchronizationContext: synchronizer
|
||||
synchronizationContext: synchronizer,
|
||||
})
|
||||
},
|
||||
disabledViewPortToolSync(synchronizer, toolName) {
|
||||
|
|
@ -1095,7 +1240,7 @@ export default {
|
|||
'CobbAngle',
|
||||
'Angle',
|
||||
'Bidirectional',
|
||||
'FreehandRoi'
|
||||
'FreehandRoi',
|
||||
]
|
||||
for (let i = 0; i < toolROITypes.length; i++) {
|
||||
const toolROIType = toolROITypes[i]
|
||||
|
|
@ -1115,12 +1260,12 @@ export default {
|
|||
removeLabel(item) {
|
||||
const promise = scroll(this.canvas, item.data.imageIdIndex)
|
||||
const scope = this
|
||||
Promise.all([promise]).then(res => {
|
||||
Promise.all([promise]).then((res) => {
|
||||
cornerstoneTools.removeToolState(scope.canvas, item.type, item.data)
|
||||
cornerstone.updateImage(scope.canvas)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,36 +2,21 @@
|
|||
<!--FEEDBACK-->
|
||||
<div v-if="visible" @click.stop="() => false" class="feedBack-box">
|
||||
<div class="feedBack-box-modal"></div>
|
||||
<el-dialog
|
||||
:visible.sync="visible"
|
||||
v-dialogDrag
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
@close="cancel"
|
||||
:modal="false"
|
||||
>
|
||||
<el-dialog :visible.sync="visible" v-dialogDrag width="800px" :close-on-click-modal="false" @close="cancel"
|
||||
:modal="false">
|
||||
<div slot="title">
|
||||
{{ title }}
|
||||
</div>
|
||||
<!-- 项目详情 -->
|
||||
<div class="trialsBox" v-if="visitTaskId || SubjectVisitId">
|
||||
<el-form
|
||||
label-position="right"
|
||||
:model="form"
|
||||
:inline="true"
|
||||
class="trialsForm"
|
||||
v-if="type === 'detail'"
|
||||
>
|
||||
<el-form label-position="right" :model="form" :inline="true" class="trialsForm" v-if="type === 'detail'">
|
||||
<el-form-item :label="$t('feedBack:trials:code')" style="width: 40%">
|
||||
<span>{{ form.TrialCode }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('feedBack:trials:name')" style="width: 40%">
|
||||
<span>{{ form.ExperimentName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('feedBack:trials:siteCode')"
|
||||
style="width: 40%"
|
||||
>
|
||||
<el-form-item :label="$t('feedBack:trials:siteCode')" style="width: 40%">
|
||||
<span>{{ form.TrialSiteCode }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('feedBack:trials:visit')" style="width: 40%">
|
||||
|
|
@ -39,47 +24,21 @@
|
|||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-form
|
||||
ref="feedBackForm"
|
||||
label-position="right"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form ref="feedBackForm" label-position="right" :model="form" :rules="rules" label-width="100px">
|
||||
<!-- 影像异常tip -->
|
||||
<p class="tip" v-if="type === 'imgfail'">
|
||||
<i
|
||||
class="el-icon-warning-outline"
|
||||
style="color: #f56c6c; font-size: 24px"
|
||||
></i>
|
||||
<i class="el-icon-warning-outline" style="color: #f56c6c; font-size: 24px"></i>
|
||||
<span>{{ $t('feedBack:imgfail:tip') }}</span>
|
||||
</p>
|
||||
<!-- 问题反馈 -->
|
||||
<el-form-item
|
||||
:label="$t('feedBack:form:feedBack')"
|
||||
prop="QuestionType"
|
||||
v-if="type === 'feedback' && trialId"
|
||||
>
|
||||
<el-select
|
||||
v-model="form.QuestionType"
|
||||
style="width: 100%"
|
||||
popper-class="feedBack-select-box"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in QuestionTypeOptions"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<el-form-item :label="$t('feedBack:form:feedBack')" prop="QuestionType" v-if="type === 'feedback' && trialId">
|
||||
<el-select v-model="form.QuestionType" style="width: 100%" popper-class="feedBack-select-box">
|
||||
<el-option v-for="item in QuestionTypeOptions" :key="item.id" :label="item.label" :value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 问题反馈 -->
|
||||
<el-form-item
|
||||
:label="$t('feedBack:form:feedBack')"
|
||||
prop="QuestionType"
|
||||
v-if="type === 'detail'"
|
||||
>
|
||||
<el-form-item :label="$t('feedBack:form:feedBack')" prop="QuestionType" v-if="type === 'detail'">
|
||||
<span>{{
|
||||
QuestionTypeOptions.filter(
|
||||
(item) => item.value === form.QuestionType
|
||||
|
|
@ -87,43 +46,22 @@
|
|||
}}</span>
|
||||
</el-form-item>
|
||||
<!-- 问题描述 -->
|
||||
<el-form-item
|
||||
:label="$t('feedBack:form:description')"
|
||||
prop="QuestionDescription"
|
||||
>
|
||||
<el-input
|
||||
v-model="form.QuestionDescription"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:maxlength="500"
|
||||
:disabled="type === 'detail'"
|
||||
/>
|
||||
<el-form-item :label="$t('feedBack:form:description')" prop="QuestionDescription">
|
||||
<el-input v-model="form.QuestionDescription" type="textarea" :rows="4" :maxlength="500"
|
||||
:disabled="type === 'detail'" />
|
||||
</el-form-item>
|
||||
<!-- 截图 -->
|
||||
<el-form-item :label="$t('feedBack:form:screenshot')" prop="screenshot">
|
||||
<uploadImage
|
||||
:path.sync="form.ScreenshotList"
|
||||
:isUpload.sync="loading"
|
||||
:trialId="trialId"
|
||||
:disabled="type === 'detail'"
|
||||
ref="uploadImage"
|
||||
/>
|
||||
<uploadImage :path.sync="form.ScreenshotList" :isUpload.sync="loading" :trialId="trialId"
|
||||
:disabled="type === 'detail'" ref="uploadImage" />
|
||||
</el-form-item>
|
||||
<!-- 反馈时间 -->
|
||||
<el-form-item
|
||||
:label="$t('feedBack:form:time')"
|
||||
prop="screenshot"
|
||||
v-if="type === 'detail'"
|
||||
>
|
||||
<el-form-item :label="$t('feedBack:form:time')" prop="screenshot" v-if="type === 'detail'">
|
||||
<span>{{ form.CreateTime }}</span>
|
||||
</el-form-item>
|
||||
<!-- 状态 -->
|
||||
<el-form-item
|
||||
:label="$t('feedBack:form:status')"
|
||||
prop="screenshot"
|
||||
v-if="type === 'detail' && level > 7"
|
||||
>
|
||||
<el-switch
|
||||
<el-form-item :label="$t('feedBack:form:status')" prop="screenshot" v-if="type === 'detail' && level >= 7">
|
||||
<!-- <el-switch
|
||||
v-model="form.State"
|
||||
active-color="#13ce66"
|
||||
inactive-color="#ff4949"
|
||||
|
|
@ -134,21 +72,26 @@
|
|||
:disabled="level < 8 || !isStateChange"
|
||||
@change="changeState"
|
||||
>
|
||||
</el-switch>
|
||||
</el-switch> -->
|
||||
<el-select v-model="form.State" :popper-append-to-body="false" :disabled="level < 8">
|
||||
<el-option v-for="item in $d.FeedBackStatus" :key="item.id" :label="item.label"
|
||||
:value="item.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 无法解决原因 -->
|
||||
<el-form-item :label="$t('feedBack:form:Reason')" prop="Reason"
|
||||
v-if="type === 'detail' && level >= 7 && form.State === 2">
|
||||
<el-input v-model="form.Reason" type="textarea" :rows="4" :maxlength="500" :disabled="level < 8" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" v-if="type !== 'detail' || isImgfail">
|
||||
<!--type !== 'detail'-->
|
||||
<div slot="footer" v-if="level >= 8 || isImgfail">
|
||||
<!-- 取消 -->
|
||||
<el-button size="small" @click.stop="cancel">
|
||||
{{ $t('feedBack:button:cancel') }}
|
||||
</el-button>
|
||||
<!-- 保存 -->
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click.stop="save"
|
||||
:loading="loading"
|
||||
>
|
||||
<el-button type="primary" size="small" @click.stop="save" :loading="loading">
|
||||
{{ $t('feedBack:button:save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
|
@ -190,6 +133,7 @@ export default {
|
|||
SubjectVisitId: null,
|
||||
ScreenshotList: [],
|
||||
ScreenshotListStr: null,
|
||||
Reason: null
|
||||
},
|
||||
rules: {
|
||||
QuestionType: [
|
||||
|
|
@ -307,6 +251,7 @@ export default {
|
|||
// 修改状态
|
||||
async changeState() {
|
||||
if (this.isImgfail) return
|
||||
if (this.form.State !== 1 && this.form.State !== 0) return false
|
||||
try {
|
||||
let data = {
|
||||
IdList: [this.Id],
|
||||
|
|
@ -381,6 +326,10 @@ export default {
|
|||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
::v-deep.el-popper {
|
||||
z-index: 4000 !important;
|
||||
}
|
||||
|
||||
.tip {
|
||||
width: 86%;
|
||||
margin: auto;
|
||||
|
|
@ -389,12 +338,14 @@ export default {
|
|||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
// border-radius: 5px;
|
||||
// background-color: #eee;
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.trialsBox {
|
||||
margin: auto;
|
||||
margin-bottom: 20px;
|
||||
|
|
@ -405,11 +356,13 @@ export default {
|
|||
border-radius: 5px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.trialsForm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,16 +12,33 @@
|
|||
<!--受试者-->
|
||||
<el-table-column prop="SubjectCode" :label="$t('upload:dicom:table:subjectCode')" sortable />
|
||||
<!--访视名称-->
|
||||
<el-table-column prop="VisitName" :label="$t('download:table:VisitName')" v-if="IsImageSegment" sortable />
|
||||
<el-table-column
|
||||
prop="VisitName"
|
||||
:label="$t('download:table:VisitName')"
|
||||
v-if="IsImageSegment"
|
||||
sortable
|
||||
/>
|
||||
<!--任务名称-->
|
||||
<el-table-column prop="TaskBlindName" :label="$t('upload:dicom:table:taskBlindName')" v-else sortable />
|
||||
<el-table-column
|
||||
prop="TaskBlindName"
|
||||
:label="$t('upload:dicom:table:taskBlindName')"
|
||||
v-else
|
||||
sortable
|
||||
/>
|
||||
<!--原始检查数-->
|
||||
<el-table-column prop="OrginalStudyList" :label="$t('upload:dicom:table:orginalStudyListNum')">
|
||||
<el-table-column
|
||||
prop="OrginalStudyList"
|
||||
:label="$t('upload:dicom:table:orginalStudyListNum')"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-button v-if="
|
||||
<el-button
|
||||
v-if="
|
||||
scope.row.OrginalStudyList &&
|
||||
scope.row.OrginalStudyList.length >= 1
|
||||
" type="text" @click="handleOpenDialog(scope.row, 'OrginalStudyList')">
|
||||
"
|
||||
type="text"
|
||||
@click="handleOpenDialog(scope.row, 'OrginalStudyList')"
|
||||
>
|
||||
<span>{{ scope.row.OrginalStudyList.length }}</span>
|
||||
</el-button>
|
||||
<span v-else>0</span>
|
||||
|
|
@ -30,9 +47,13 @@
|
|||
<!--后处理检查数-->
|
||||
<el-table-column prop="UploadStudyList" :label="$t('upload:dicom:table:uploadStudyListNum')">
|
||||
<template slot-scope="scope">
|
||||
<el-button v-if="
|
||||
<el-button
|
||||
v-if="
|
||||
scope.row.UploadStudyList && scope.row.UploadStudyList.length >= 1
|
||||
" type="text" @click="handleOpenDialog(scope.row, 'UploadStudyList', true)">
|
||||
"
|
||||
type="text"
|
||||
@click="handleOpenDialog(scope.row, 'UploadStudyList', true)"
|
||||
>
|
||||
<span>{{ scope.row.UploadStudyList.length }}</span>
|
||||
</el-button>
|
||||
<span v-else>0</span>
|
||||
|
|
@ -42,27 +63,57 @@
|
|||
<template slot-scope="scope">
|
||||
<div class="btnBox">
|
||||
<!--上传--->
|
||||
<form id="inputForm" :ref="`uploadForm_${scope.row.Id}`" enctype="multipart/form-data" v-if="!forbid">
|
||||
<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"
|
||||
:title="$t('upload:dicom:button:upload')" />
|
||||
<input :title="$t('upload:dicom:button:upload')" type="file" :name="`file_${scope.row.VisitTaskId}`"
|
||||
:ref="`pathClear_${scope.row.VisitTaskId}`" :disabled="btnLoading" webkitdirectory multiple @change="
|
||||
<el-button
|
||||
circle
|
||||
icon="el-icon-upload2"
|
||||
:disabled="btnLoading"
|
||||
:loading="btnLoading"
|
||||
:title="$t('upload:dicom:button:upload')"
|
||||
/>
|
||||
<input
|
||||
:title="$t('upload:dicom:button:upload')"
|
||||
type="file"
|
||||
:name="`file_${scope.row.VisitTaskId}`"
|
||||
:ref="`pathClear_${scope.row.VisitTaskId}`"
|
||||
:disabled="btnLoading"
|
||||
webkitdirectory
|
||||
multiple
|
||||
@change="
|
||||
($event) => beginScanFiles($event, scope.row.VisitTaskId)
|
||||
" />
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<!--预览--->
|
||||
<el-button circle icon="el-icon-view" :disabled="!scope.row.UploadStudyList ||
|
||||
<el-button
|
||||
circle
|
||||
icon="el-icon-view"
|
||||
:disabled="!scope.row.UploadStudyList ||
|
||||
scope.row.UploadStudyList.length <= 0
|
||||
" @click.stop="handleViewReadingImages(scope.row)" :title="$t('upload:dicom:button:preview')" />
|
||||
"
|
||||
@click.stop="handleViewReadingImages(scope.row)"
|
||||
:title="$t('upload:dicom:button:preview')"
|
||||
/>
|
||||
<!--删除--->
|
||||
<el-button circle :disabled="!scope.row.UploadStudyList ||
|
||||
<el-button
|
||||
circle
|
||||
:disabled="!scope.row.UploadStudyList ||
|
||||
scope.row.UploadStudyList.length <= 0 ||
|
||||
scope.row.ReadingTaskState === 2
|
||||
" icon="el-icon-delete" :title="$t('upload:dicom:button:delete')" @click.stop="remove(scope.row)" />
|
||||
"
|
||||
icon="el-icon-delete"
|
||||
:title="$t('upload:dicom:button:delete')"
|
||||
@click.stop="remove(scope.row)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
@ -70,32 +121,49 @@
|
|||
<div style="margin: 10px 0" class="top">
|
||||
<span>{{ $t('upload:dicom:uploadTitle') }}</span>
|
||||
<div class="btnBox" v-if="!forbid">
|
||||
<span style="margin-right: 10px">
|
||||
{{ $store.state.trials.uploadTip }}
|
||||
</span>
|
||||
<span style="margin-right: 10px">{{ $store.state.trials.uploadTip }}</span>
|
||||
<form id="inputForm" ref="uploadForm" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<div id="directoryInputWrapper" class="btn btn-link file-input">
|
||||
<el-button type="primary" :disabled="btnLoading" :loading="btnLoading" size="mini">
|
||||
{{ $t('upload:dicom:button:batchUpload') }}
|
||||
</el-button>
|
||||
<input type="file" name="file" ref="pathClear" :disabled="btnLoading" webkitdirectory multiple title=""
|
||||
@change="beginScanFiles($event)" />
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="btnLoading"
|
||||
:loading="btnLoading"
|
||||
size="mini"
|
||||
>{{ $t('upload:dicom:button:batchUpload') }}</el-button>
|
||||
<input
|
||||
type="file"
|
||||
name="file"
|
||||
ref="pathClear"
|
||||
:disabled="btnLoading"
|
||||
webkitdirectory
|
||||
multiple
|
||||
title
|
||||
@change="beginScanFiles($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!--上传列表-->
|
||||
<el-table ref="dicomFilesTable" v-adaptive="{ bottomOffset: 80 }" height="100" :data="uploadQueues"
|
||||
class="dicomFiles-table" @selection-change="handleSelectionChange">
|
||||
<el-table
|
||||
ref="dicomFilesTable"
|
||||
v-adaptive="{ bottomOffset: 80 }"
|
||||
height="100"
|
||||
:data="uploadQueues"
|
||||
class="dicomFiles-table"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="index" width="40" />
|
||||
<el-table-column min-width="200" show-overflow-tooltip>
|
||||
<template slot="header">
|
||||
<el-tooltip placement="top">
|
||||
<div slot="content">
|
||||
{{ $t('trials:uploadDicomList:table:studyDetail1') }}<br />
|
||||
{{ $t('trials:uploadDicomList:table:studyDetail2') }}<br />
|
||||
{{ $t('trials:uploadDicomList:table:studyDetail1') }}
|
||||
<br />
|
||||
{{ $t('trials:uploadDicomList:table:studyDetail2') }}
|
||||
<br />
|
||||
{{ $t('trials:uploadedDicoms:table:studyDate') }}
|
||||
</div>
|
||||
<span>{{ $t('trials:uploadDicomList:table:studyInfo') }}</span>
|
||||
|
|
@ -105,13 +173,16 @@
|
|||
<div style="line-height: 15px">
|
||||
<div>
|
||||
<div>
|
||||
<span v-if="scope.row.dicomInfo.accNumber"><span style="font-weight: 500">Acc:</span>
|
||||
{{ scope.row.dicomInfo.accNumber }}</span>
|
||||
<span v-if="scope.row.dicomInfo.accNumber">
|
||||
<span style="font-weight: 500">Acc:</span>
|
||||
{{ scope.row.dicomInfo.accNumber }}
|
||||
</span>
|
||||
<span v-else style="color: #f44336">N/A</span>
|
||||
</div>
|
||||
<div style="display: inline-block; margin-right: 2px">
|
||||
<span v-if="scope.row.dicomInfo.modality.length > 0">
|
||||
{{ scope.row.dicomInfo.modality.join('、') }},</span>
|
||||
<span
|
||||
v-if="scope.row.dicomInfo.modality.length > 0"
|
||||
>{{ scope.row.dicomInfo.modality.join('、') }},</span>
|
||||
<span v-else style="color: #f44336">N/A,</span>
|
||||
</div>
|
||||
<div style="display: inline-block; margin-right: 2px">
|
||||
|
|
@ -126,20 +197,15 @@
|
|||
|
||||
<div>
|
||||
<div style="display: inline-block; margin-right: 2px">
|
||||
<span v-if="scope.row.dicomInfo.bodyPart">
|
||||
{{ scope.row.dicomInfo.bodyPart }},
|
||||
</span>
|
||||
<span v-if="scope.row.dicomInfo.bodyPart">{{ scope.row.dicomInfo.bodyPart }},</span>
|
||||
<span v-else style="color: #f44336">N/A,</span>
|
||||
</div>
|
||||
<div style="display: inline-block">
|
||||
<span v-if="scope.row.dicomInfo.description">
|
||||
{{ scope.row.dicomInfo.description }}</span>
|
||||
<span v-if="scope.row.dicomInfo.description">{{ scope.row.dicomInfo.description }}</span>
|
||||
<span v-else style="color: #f44336">N/A</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ scope.row.dicomInfo.studyTime }}
|
||||
</div>
|
||||
<div>{{ scope.row.dicomInfo.studyTime }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
@ -147,8 +213,10 @@
|
|||
<template slot="header">
|
||||
<el-tooltip placement="top">
|
||||
<div slot="content">
|
||||
{{ $t('trials:uploadDicomList:table:pId') }}<br />
|
||||
{{ $t('trials:uploadDicomList:table:patientName') }}<br />
|
||||
{{ $t('trials:uploadDicomList:table:pId') }}
|
||||
<br />
|
||||
{{ $t('trials:uploadDicomList:table:patientName') }}
|
||||
<br />
|
||||
{{ $t('trials:uploadDicomList:table:pInfo') }}
|
||||
</div>
|
||||
<span>{{ $t('trials:uploadDicomList:table:patientInfo') }}</span>
|
||||
|
|
@ -157,8 +225,11 @@
|
|||
<template slot-scope="scope">
|
||||
<div style="line-height: 15px">
|
||||
<div>
|
||||
<span v-if="scope.row.dicomInfo.patientId"><span style="font-weight: 500">PID: </span>{{
|
||||
scope.row.dicomInfo.patientId }}</span>
|
||||
<span v-if="scope.row.dicomInfo.patientId">
|
||||
<span style="font-weight: 500">PID:</span>
|
||||
{{
|
||||
scope.row.dicomInfo.patientId }}
|
||||
</span>
|
||||
<span v-else style="color: #f44336">N/A</span>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -187,9 +258,11 @@
|
|||
}},
|
||||
</span>
|
||||
|
||||
<span :class="[
|
||||
<span
|
||||
:class="[
|
||||
scope.row.dicomInfo.patientBirthDate ? '' : 'colorOfRed',
|
||||
]">
|
||||
]"
|
||||
>
|
||||
{{
|
||||
scope.row.dicomInfo.patientBirthDate
|
||||
? scope.row.dicomInfo.patientBirthDate
|
||||
|
|
@ -200,14 +273,20 @@
|
|||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('trials:uploadDicomList:table:failedFileCount')" min-width="150"
|
||||
show-overflow-tooltip>
|
||||
<el-table-column
|
||||
:label="$t('trials:uploadDicomList:table:failedFileCount')"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-progress color="#409eff" :percentage="(
|
||||
<el-progress
|
||||
color="#409eff"
|
||||
:percentage="(
|
||||
(scope.row.dicomInfo.uploadFileSize * 100) /
|
||||
(scope.row.dicomInfo.fileSize ? scope.row.dicomInfo.fileSize : 1)
|
||||
).toFixed(2) * 1
|
||||
" />
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
{{ $t('trials:uploadDicomList:table:uploadNow')
|
||||
}}{{ scope.row.dicomInfo.failedFileCount }}/{{
|
||||
|
|
@ -222,87 +301,128 @@
|
|||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('trials:uploadDicomList:table:status')" min-width="140" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
:label="$t('trials:uploadDicomList:table:status')"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span v-if="
|
||||
<span
|
||||
v-if="
|
||||
!scope.row.dicomInfo.failedFileCount &&
|
||||
!scope.row.dicomInfo.isInit
|
||||
">
|
||||
{{ $t('trials:uploadDicomList:table:status1') }}</span>
|
||||
<span style="color: #409eff" v-else-if="
|
||||
"
|
||||
>{{ $t('trials:uploadDicomList:table:status1') }}</span>
|
||||
<span
|
||||
style="color: #409eff"
|
||||
v-else-if="
|
||||
!scope.row.dicomInfo.failedFileCount &&
|
||||
scope.row.dicomInfo.isInit &&
|
||||
btnLoading
|
||||
">{{ $t('trials:uploadDicomList:table:status2') }}</span>
|
||||
<span style="color: #409eff" v-else-if="
|
||||
"
|
||||
>{{ $t('trials:uploadDicomList:table:status2') }}</span>
|
||||
<span
|
||||
style="color: #409eff"
|
||||
v-else-if="
|
||||
scope.row.dicomInfo.failedFileCount <
|
||||
scope.row.dicomInfo.fileCount && !scope.row.uploadState.record
|
||||
">{{ $t('trials:uploadDicomList:table:status2') }}</span>
|
||||
<span style="color: #2cc368" v-else-if="
|
||||
"
|
||||
>{{ $t('trials:uploadDicomList:table:status2') }}</span>
|
||||
<span
|
||||
style="color: #2cc368"
|
||||
v-else-if="
|
||||
scope.row.dicomInfo.failedFileCount ===
|
||||
scope.row.dicomInfo.fileCount
|
||||
">{{ $t('trials:uploadDicomList:table:status3') }}</span>
|
||||
<span style="color: #f66" v-else-if="
|
||||
"
|
||||
>{{ $t('trials:uploadDicomList:table:status3') }}</span>
|
||||
<span
|
||||
style="color: #f66"
|
||||
v-else-if="
|
||||
scope.row.uploadState.record &&
|
||||
scope.row.uploadState.record.fileCount === 0
|
||||
">{{ $t('trials:uploadDicomList:table:status5') }}</span>
|
||||
<span style="color: #f66" v-else>{{
|
||||
"
|
||||
>{{ $t('trials:uploadDicomList:table:status5') }}</span>
|
||||
<span style="color: #f66" v-else>
|
||||
{{
|
||||
$t('trials:uploadDicomList:table:Failed')
|
||||
}}</span>
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('trials:uploadDicomList:table:record')" min-width="140" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
:label="$t('trials:uploadDicomList:table:record')"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip placement="top" v-if="scope.row.uploadState.record">
|
||||
<div slot="content">
|
||||
<div style="max-height: 500px; overflow-y: auto">
|
||||
{{ $t('trials:uploadDicomList:table:Existed') }}:
|
||||
<div v-if="scope.row.uploadState.record.Existed.length">
|
||||
<div v-for="item of scope.row.uploadState.record.Existed" :key="item"
|
||||
style="font-size: 12px; color: #baa72a">
|
||||
{{ item }}
|
||||
</div>
|
||||
<div
|
||||
v-for="item of scope.row.uploadState.record.Existed"
|
||||
:key="item"
|
||||
style="font-size: 12px; color: #baa72a"
|
||||
>{{ item }}</div>
|
||||
</div>
|
||||
<div v-else> </div>
|
||||
{{ $t('trials:uploadDicomList:table:Uploaded') }}:
|
||||
<div v-if="scope.row.uploadState.record.Uploaded.length">
|
||||
<div v-for="item of scope.row.uploadState.record.Uploaded" :key="item"
|
||||
style="font-size: 12px; color: #24b837">
|
||||
{{ item }}
|
||||
</div>
|
||||
<div
|
||||
v-for="item of scope.row.uploadState.record.Uploaded"
|
||||
:key="item"
|
||||
style="font-size: 12px; color: #24b837"
|
||||
>{{ item }}</div>
|
||||
</div>
|
||||
<div v-else> </div>
|
||||
<br />
|
||||
{{ $t('trials:uploadDicomList:table:Failed') }}:
|
||||
<div v-if="scope.row.uploadState.record.Failed.length">
|
||||
<div v-for="item of scope.row.uploadState.record.Failed" :key="item"
|
||||
style="font-size: 12px; color: #f66">
|
||||
{{ item }}
|
||||
</div>
|
||||
<div
|
||||
v-for="item of scope.row.uploadState.record.Failed"
|
||||
:key="item"
|
||||
style="font-size: 12px; color: #f66"
|
||||
>{{ item }}</div>
|
||||
</div>
|
||||
<div v-else> </div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button size="mini" style="cursor: pointer">
|
||||
<span style="font-size: 12px; color: #baa72a">{{
|
||||
<span style="font-size: 12px; color: #baa72a">
|
||||
{{
|
||||
scope.row.uploadState.record.Existed.length
|
||||
}}</span>
|
||||
}}
|
||||
</span>
|
||||
/
|
||||
<span style="font-size: 12px; color: #24b837">{{
|
||||
<span style="font-size: 12px; color: #24b837">
|
||||
{{
|
||||
scope.row.uploadState.record.Uploaded.length
|
||||
}}</span>
|
||||
}}
|
||||
</span>
|
||||
/
|
||||
<span style="font-size: 12px; color: #f66">{{
|
||||
<span style="font-size: 12px; color: #f66">
|
||||
{{
|
||||
scope.row.uploadState.record.Failed.length
|
||||
}}</span>
|
||||
}}
|
||||
</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<study-view v-if="model_cfg.visible" :model_cfg="model_cfg" :IsDicom="true" :bodyPart="bodyPart"
|
||||
:subjectVisitId="openSubjectVisitId" :modelList="modelList" :isUpload="openIsUpload"
|
||||
:visitTaskId="openVisitTaskId" :TrialModality="TrialModality" @getList="getList" />
|
||||
<study-view
|
||||
v-if="model_cfg.visible"
|
||||
:model_cfg="model_cfg"
|
||||
:IsDicom="true"
|
||||
:bodyPart="bodyPart"
|
||||
:subjectVisitId="openSubjectVisitId"
|
||||
:modelList="modelList"
|
||||
:isUpload="openIsUpload"
|
||||
:visitTaskId="openVisitTaskId"
|
||||
:TrialModality="TrialModality"
|
||||
@getList="getList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
|
@ -335,6 +455,7 @@ cornerstoneWADOImageLoader.external.cornerstone = cornerstone
|
|||
import { convertBytes } from '@/utils/dicom-character-set'
|
||||
import { parseDicom } from '@/utils/parseDicom.js'
|
||||
import { dcmUpload } from '@/utils/dcmUpload/dcmUpload'
|
||||
import dcmjs from '@/utils/dcmUpload/dcmjs'
|
||||
import store from '@/store'
|
||||
import { getToken } from '@/utils/auth'
|
||||
export default {
|
||||
|
|
@ -373,7 +494,7 @@ export default {
|
|||
IsImageSegment: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
'study-view': studyView,
|
||||
|
|
@ -455,8 +576,10 @@ export default {
|
|||
}
|
||||
if (this.IsImageSegment) {
|
||||
params.IsImageSegmentLabel = true
|
||||
}
|
||||
else if (this.Criterion.CriterionType == 19 || this.Criterion.CriterionType == 20) {
|
||||
} else if (
|
||||
this.Criterion.CriterionType == 19 ||
|
||||
this.Criterion.CriterionType == 20
|
||||
) {
|
||||
params.IsImageSegmentLabel = false
|
||||
}
|
||||
this.loading = true
|
||||
|
|
@ -543,7 +666,9 @@ export default {
|
|||
this.openIsUpload = isUpload
|
||||
this.openSubjectVisitId = item.SubjectVisitId || item.SourceSubjectVisitId
|
||||
this.openVisitTaskId = item.VisitTaskId
|
||||
this.model_cfg.title = `${item.SubjectCode || ''} > ${this.IsImageSegment ? item.VisitName : item.TaskBlindName}`
|
||||
this.model_cfg.title = `${item.SubjectCode || ''} > ${
|
||||
this.IsImageSegment ? item.VisitName : item.TaskBlindName
|
||||
}`
|
||||
this.modelList = item[list]
|
||||
this.model_cfg.visible = true
|
||||
},
|
||||
|
|
@ -645,7 +770,10 @@ export default {
|
|||
for (let i = 0; i < checkFiles.length; i++) {
|
||||
let item = checkFiles[i]
|
||||
var dicom = await parseDicom(item, ['StudyInstanceUid', 'Modality'])
|
||||
if (!!~this.errStudyUidList.indexOf(dicom.StudyInstanceUid) || (this.IsImageSegment && dicom.Modality !== 'IVUS')) {
|
||||
if (
|
||||
!!~this.errStudyUidList.indexOf(dicom.StudyInstanceUid) ||
|
||||
(this.IsImageSegment && dicom.Modality !== 'IVUS')
|
||||
) {
|
||||
this.hasOtherStudy = true
|
||||
checkFiles.splice(i, 1)
|
||||
i--
|
||||
|
|
@ -712,7 +840,8 @@ export default {
|
|||
var studyUid = data.string('x0020000d')
|
||||
if (!studyUid) return resolve()
|
||||
var pixelDataElement = data.elements.x7fe00010
|
||||
if (!pixelDataElement && modality !== 'SR' && modality !== 'ECG') return resolve()
|
||||
if (!pixelDataElement && modality !== 'SR' && modality !== 'ECG')
|
||||
return resolve()
|
||||
var studyIndex = 0
|
||||
while (
|
||||
studyIndex < scope.uploadQueues.length &&
|
||||
|
|
@ -809,6 +938,39 @@ export default {
|
|||
},
|
||||
})
|
||||
}
|
||||
if (
|
||||
!scope.uploadQueues[studyIndex].dicomInfo.RadionuclideTotalDose
|
||||
) {
|
||||
let dataset = dcmjs.data.DicomMessage.readFile(e.target.result)
|
||||
if (
|
||||
dataset.dict['00540016'] &&
|
||||
dataset.dict['00540016'].Value &&
|
||||
dataset.dict['00540016'].Value[0]
|
||||
) {
|
||||
let RadionuclideTotalDose = dataset.dict['00540016'].Value[0][
|
||||
'00181074'
|
||||
]
|
||||
? dataset.dict['00540016'].Value[0]['00181074'].Value[0]
|
||||
: null
|
||||
let RadionuclideHalfLife = dataset.dict['00540016'].Value[0][
|
||||
'00181075'
|
||||
]
|
||||
? dataset.dict['00540016'].Value[0]['00181075'].Value[0]
|
||||
: null
|
||||
let RadiopharmaceuticalStartTime = dataset.dict['00540016']
|
||||
.Value[0]['00181072']
|
||||
? dataset.dict['00540016'].Value[0]['00181072'].Value[0]
|
||||
: null
|
||||
scope.uploadQueues[studyIndex].dicomInfo.RadionuclideTotalDose =
|
||||
RadionuclideTotalDose
|
||||
scope.uploadQueues[studyIndex].dicomInfo.RadionuclideHalfLife =
|
||||
RadionuclideHalfLife
|
||||
scope.uploadQueues[
|
||||
studyIndex
|
||||
].dicomInfo.RadiopharmaceuticalStartTime =
|
||||
RadiopharmaceuticalStartTime
|
||||
}
|
||||
}
|
||||
var modality = scope.uploadQueues[studyIndex].dicomInfo.modality
|
||||
var currentModality = data.string('x00080060')
|
||||
if (!(modality.indexOf(currentModality) > -1)) {
|
||||
|
|
@ -876,8 +1038,9 @@ export default {
|
|||
)
|
||||
seriesItem = {
|
||||
seriesUid: seriesUid,
|
||||
RadiopharmaceuticalInformationSequence: data.string('x00540016') || "",
|
||||
AcquisitionDate: data.string('x00080022') || "",
|
||||
RadiopharmaceuticalInformationSequence:
|
||||
data.string('x00540016') || '',
|
||||
AcquisitionDate: data.string('x00080022') || '',
|
||||
|
||||
DicomSeriesDate: data.string('x00080021'),
|
||||
DicomSeriesTime: data.string('x00080031'),
|
||||
|
|
@ -1085,16 +1248,19 @@ export default {
|
|||
var scope = this
|
||||
return new Promise(function (resolve, reject) {
|
||||
try {
|
||||
let subjectVisitId = null;
|
||||
let subjectVisitId = null
|
||||
if (scope.VisitTaskId) {
|
||||
scope.StudyInstanceUidList.forEach(item => {
|
||||
scope.StudyInstanceUidList.forEach((item) => {
|
||||
if (item.VisitTaskId === scope.VisitTaskId) {
|
||||
subjectVisitId = item.SourceSubjectVisitId
|
||||
}
|
||||
})
|
||||
} else {
|
||||
scope.StudyInstanceUidList.forEach(item => {
|
||||
if (item.StudyInstanceUid === scope.uploadQueues[index].dicomInfo.studyUid) {
|
||||
scope.StudyInstanceUidList.forEach((item) => {
|
||||
if (
|
||||
item.StudyInstanceUid ===
|
||||
scope.uploadQueues[index].dicomInfo.studyUid
|
||||
) {
|
||||
subjectVisitId = item.SourceSubjectVisitId
|
||||
}
|
||||
})
|
||||
|
|
@ -1144,6 +1310,11 @@ export default {
|
|||
failedFileCount: 0,
|
||||
RecordPath: null,
|
||||
study: {
|
||||
RadionuclideTotalDose: dicomInfo.RadionuclideTotalDose,
|
||||
RadionuclideHalfLife: dicomInfo.RadionuclideHalfLife,
|
||||
RadiopharmaceuticalStartTime:
|
||||
dicomInfo.RadiopharmaceuticalStartTime,
|
||||
|
||||
studyId: dicomInfo.studyId,
|
||||
studyInstanceUid: dicomInfo.studyUid,
|
||||
studyTime: dicomInfo.studyTime,
|
||||
|
|
@ -1197,8 +1368,10 @@ export default {
|
|||
seriesInstanceUid: v.seriesUid,
|
||||
SOPClassUID: o.SOPClassUID,
|
||||
TransferSytaxUID: o.TransferSytaxUID,
|
||||
MediaStorageSOPInstanceUID: o.MediaStorageSOPInstanceUID,
|
||||
MediaStorageSOPClassUID: o.MediaStorageSOPClassUID,
|
||||
MediaStorageSOPInstanceUID:
|
||||
o.MediaStorageSOPInstanceUID,
|
||||
MediaStorageSOPClassUID:
|
||||
o.MediaStorageSOPClassUID,
|
||||
sopInstanceUid: o.instanceUid,
|
||||
instanceNumber: o.instanceNumber,
|
||||
instanceTime: o.instanceTime,
|
||||
|
|
@ -1215,14 +1388,17 @@ export default {
|
|||
path: o.myPath,
|
||||
FileSize: o.FileSize,
|
||||
|
||||
PhotometricInterpretation: o.PhotometricInterpretation,
|
||||
PhotometricInterpretation:
|
||||
o.PhotometricInterpretation,
|
||||
BitsAllocated: o.BitsAllocated,
|
||||
PixelRepresentation: o.PixelRepresentation,
|
||||
RescaleIntercept: o.RescaleIntercept,
|
||||
RescaleSlope: o.RescaleSlope,
|
||||
ImagePositionPatient: o.ImagePositionPatient,
|
||||
ImageOrientationPatient: o.ImageOrientationPatient,
|
||||
SequenceOfUltrasoundRegions: o.SequenceOfUltrasoundRegions,
|
||||
ImageOrientationPatient:
|
||||
o.ImageOrientationPatient,
|
||||
SequenceOfUltrasoundRegions:
|
||||
o.SequenceOfUltrasoundRegions,
|
||||
FrameTime: o.FrameTime,
|
||||
CorrectedImage: o.CorrectedImage,
|
||||
Units: o.Units,
|
||||
|
|
@ -1233,8 +1409,10 @@ export default {
|
|||
dicomInfo.failedFileCount++
|
||||
Record.FileCount++
|
||||
} else {
|
||||
let path = `/${params.trialId}/Image/${params.subjectId
|
||||
}/${params.subjectVisitId}/${dicomInfo.visitTaskId
|
||||
let path = `/${params.trialId}/Image/${
|
||||
params.subjectId
|
||||
}/${params.subjectVisitId}/${
|
||||
dicomInfo.visitTaskId
|
||||
}/${scope.getGuid(
|
||||
dicomInfo.studyUid +
|
||||
v.seriesUid +
|
||||
|
|
@ -1242,8 +1420,10 @@ export default {
|
|||
params.trialId
|
||||
)}`
|
||||
if (scope.IsImageSegment) {
|
||||
path = `/${params.trialId}/Image/${params.subjectId
|
||||
}/${params.subjectVisitId}/AnnotationImage/${dicomInfo.visitTaskId
|
||||
path = `/${params.trialId}/Image/${
|
||||
params.subjectId
|
||||
}/${params.subjectVisitId}/AnnotationImage/${
|
||||
dicomInfo.visitTaskId
|
||||
}/${scope.getGuid(
|
||||
dicomInfo.studyUid +
|
||||
v.seriesUid +
|
||||
|
|
@ -1276,7 +1456,7 @@ export default {
|
|||
batchDataType: 5,
|
||||
trialId: params.trialId,
|
||||
subjectId: params.subjectId,
|
||||
subjectVisitId: params.subjectVisitId
|
||||
subjectVisitId: params.subjectVisitId,
|
||||
}
|
||||
)
|
||||
if (!res || !res.url) {
|
||||
|
|
@ -1309,7 +1489,7 @@ export default {
|
|||
batchDataType: 6,
|
||||
trialId: params.trialId,
|
||||
subjectId: params.subjectId,
|
||||
subjectVisitId: params.subjectVisitId
|
||||
subjectVisitId: params.subjectVisitId,
|
||||
}
|
||||
)
|
||||
if (seriesRes && seriesRes.url) {
|
||||
|
|
@ -1329,8 +1509,10 @@ export default {
|
|||
sopInstanceUid: o.instanceUid,
|
||||
SOPClassUID: o.SOPClassUID,
|
||||
TransferSytaxUID: o.TransferSytaxUID,
|
||||
MediaStorageSOPInstanceUID: o.MediaStorageSOPInstanceUID,
|
||||
MediaStorageSOPClassUID: o.MediaStorageSOPClassUID,
|
||||
MediaStorageSOPInstanceUID:
|
||||
o.MediaStorageSOPInstanceUID,
|
||||
MediaStorageSOPClassUID:
|
||||
o.MediaStorageSOPClassUID,
|
||||
instanceNumber: o.instanceNumber,
|
||||
instanceTime: o.instanceTime,
|
||||
imageRows: o.imageRows,
|
||||
|
|
@ -1346,14 +1528,17 @@ export default {
|
|||
path: scope.$getObjectName(res.url),
|
||||
FileSize: o.FileSize,
|
||||
|
||||
PhotometricInterpretation: o.PhotometricInterpretation,
|
||||
PhotometricInterpretation:
|
||||
o.PhotometricInterpretation,
|
||||
BitsAllocated: o.BitsAllocated,
|
||||
PixelRepresentation: o.PixelRepresentation,
|
||||
RescaleIntercept: o.RescaleIntercept,
|
||||
RescaleSlope: o.RescaleSlope,
|
||||
ImagePositionPatient: o.ImagePositionPatient,
|
||||
ImageOrientationPatient: o.ImageOrientationPatient,
|
||||
SequenceOfUltrasoundRegions: o.SequenceOfUltrasoundRegions,
|
||||
ImageOrientationPatient:
|
||||
o.ImageOrientationPatient,
|
||||
SequenceOfUltrasoundRegions:
|
||||
o.SequenceOfUltrasoundRegions,
|
||||
FrameTime: o.FrameTime,
|
||||
CorrectedImage: o.CorrectedImage,
|
||||
Units: o.Units,
|
||||
|
|
@ -1408,7 +1593,8 @@ export default {
|
|||
instanceList: instanceList,
|
||||
ImageResizePath: ImageResizePath,
|
||||
|
||||
RadiopharmaceuticalInformationSequence: v.RadiopharmaceuticalInformationSequence,
|
||||
RadiopharmaceuticalInformationSequence:
|
||||
v.RadiopharmaceuticalInformationSequence,
|
||||
AcquisitionDate: v.AcquisitionDate,
|
||||
})
|
||||
}
|
||||
|
|
@ -1451,10 +1637,7 @@ 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',
|
||||
|
|
@ -1462,9 +1645,8 @@ export default {
|
|||
batchDataType: 6,
|
||||
trialId: params.trialId,
|
||||
subjectId: params.subjectId,
|
||||
subjectVisitId: params.subjectVisitId
|
||||
}
|
||||
)
|
||||
subjectVisitId: params.subjectVisitId,
|
||||
})
|
||||
if (seriesRes && seriesRes.url) {
|
||||
o.ImageResizePath = scope.$getObjectName(seriesRes.url)
|
||||
}
|
||||
|
|
@ -1577,7 +1759,11 @@ export default {
|
|||
var token = getToken()
|
||||
let trialId = this.$route.query.trialId
|
||||
const routeData = this.$router.resolve({
|
||||
path: `/showvisitdicoms?page=upload&trialId=${trialId}&visitTaskId=${this.IsImageSegment ? 'undefined' : row.VisitTaskId}&subjectVisitId=${row.SourceSubjectVisitId}&isReading=1&TokenKey=${token}`,
|
||||
path: `/showvisitdicoms?page=upload&trialId=${trialId}&visitTaskId=${
|
||||
this.IsImageSegment ? 'undefined' : row.VisitTaskId
|
||||
}&subjectVisitId=${
|
||||
row.SourceSubjectVisitId
|
||||
}&isReading=1&TokenKey=${token}`,
|
||||
})
|
||||
this.open = window.open(routeData.href, '_blank')
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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="1777010831812" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1646" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 896c-230.058667 0-421.461333-165.546667-461.610667-384a468.565333 468.565333 0 0 1 142.506667-258.816L89.6 149.973333A42.666667 42.666667 0 1 1 149.930667 89.6L934.4 874.112a42.666667 42.666667 0 0 1-60.373333 60.330667l-111.061334-111.061334A466.901333 466.901333 0 0 1 512 896zM253.226667 313.6A382.506667 382.506667 0 0 0 137.514667 512a384.213333 384.213333 0 0 0 563.242666 249.088l-86.528-86.528A192 192 0 0 1 349.44 409.770667L253.226667 313.6z m297.770666 297.728l-138.325333-138.325333a106.666667 106.666667 0 0 0 138.282667 138.282666l0.042666 0.042667z m285.312 22.357333l10.24-18.218666A381.226667 381.226667 0 0 0 886.485333 512a384.213333 384.213333 0 0 0-440.661333-292.949333c-2.944 0.512-9.173333 1.877333-18.730667 4.096a42.88 42.88 0 1 1-19.2-83.541334A470.912 470.912 0 0 1 512 128c230.058667 0 421.461333 165.546667 461.610667 384a467.072 467.072 0 0 1-57.685334 153.856 94.933333 94.933333 0 0 1-8.021333 11.178667 44.373333 44.373333 0 0 1-58.794667 9.088 39.552 39.552 0 0 1-12.8-52.437334z m-188.501333-257.493333a192 192 0 0 1 55.850667 147.626667l-203.52-203.477334a192 192 0 0 1 147.626666 55.893334z" fill="#e6e6e6" p-id="1647"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 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="1777014169186" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4736" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1024 739.560727V1024H739.560727v-84.898909h199.540364v-199.540364H1024z m-939.101091 0v199.540364h199.540364V1024H0V739.560727h84.898909zM824.878545 199.121455v625.75709H199.121455V199.121455h625.75709z m-85.317818 85.317818H284.439273v455.121454h455.121454V284.439273zM1024 0v284.439273h-84.898909V84.898909h-199.540364V0H1024zM284.439273 0v84.898909H84.898909v199.540364H0V0h284.439273z" fill="#e6e6e6" p-id="4737"></path></svg>
|
||||
|
After Width: | Height: | Size: 764 B |
|
|
@ -337,8 +337,10 @@ async function VueInit() {
|
|||
})
|
||||
}
|
||||
let CompanyInfo = JSON.parse(localStorage.getItem('CompanyInfo'))
|
||||
if (CompanyInfo && CompanyInfo.SystemShortName && text.toUpperCase() !== "CIRCLE") {
|
||||
let test = new RegExp('IRC', 'ig')
|
||||
if (CompanyInfo && CompanyInfo.SystemShortName && (text.toUpperCase() !== "CIRCLE")) {
|
||||
// 精准匹配独立单词 IRC
|
||||
// let test = new RegExp('IRC', 'ig')
|
||||
let test = new RegExp('\\bIRC\\b', 'ig')
|
||||
text = text.replace(test, CompanyInfo.SystemShortName)
|
||||
}
|
||||
// return i18n.t(key)
|
||||
|
|
|
|||
|
|
@ -932,6 +932,12 @@ const actions = {
|
|||
data.IsDicom = study.IsDicom
|
||||
data.PreviewImageCount = 0
|
||||
data.IsCriticalSequence = study.IsCriticalSequence
|
||||
data.PatientSex = study.PatientSex
|
||||
data.PatientWeight = study.PatientWeight
|
||||
data.RadionuclideHalfLife = study.RadionuclideHalfLife
|
||||
data.RadionuclideTotalDose = study.RadionuclideTotalDose
|
||||
data.RadiopharmaceuticalStartTime = study.RadiopharmaceuticalStartTime
|
||||
data.AcquisitionTime = study.AcquisitionTime
|
||||
var seriesList = []
|
||||
study.SeriesList.forEach((series, seriesIndex) => {
|
||||
const imageIds = []
|
||||
|
|
|
|||
|
|
@ -239,21 +239,15 @@ body .el-table th.gutter {
|
|||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.el-descriptions__body
|
||||
.el-descriptions__table
|
||||
.el-descriptions-item__cell.is-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 {
|
||||
.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 {
|
||||
.el-descriptions__body .el-descriptions__table .el-descriptions-item__cell.is-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
|
@ -345,6 +339,10 @@ body .el-table th.gutter {
|
|||
.el-dialog__wrapper {
|
||||
z-index: 3999 !important;
|
||||
}
|
||||
|
||||
.el-popper {
|
||||
z-index: 4000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.feedBack-select-box {
|
||||
|
|
|
|||
|
|
@ -1,32 +1,123 @@
|
|||
import * as cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'
|
||||
import * as cornerstoneWADOImageLoader from "cornerstone-wado-image-loader";
|
||||
import store from "@/store";
|
||||
import { getPTClinicalDataForInstance } from "@/utils/ptClinicalDataCache";
|
||||
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(':');
|
||||
} from "./extractPositioningFromDataset";
|
||||
|
||||
let url = imageId.substring(firstColonIndex + 1);
|
||||
const frameIndex = url.indexOf('frame=');
|
||||
|
||||
let frame;
|
||||
|
||||
if (frameIndex !== -1) {
|
||||
const frameStr = url.substr(frameIndex + 6);
|
||||
|
||||
frame = parseInt(frameStr, 10);
|
||||
url = url.substr(0, frameIndex - 1);
|
||||
function toNumber(val) {
|
||||
if (val === undefined || val === null || val === "") return null;
|
||||
const n = typeof val === "number" ? val : parseFloat(val);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
return {
|
||||
scheme: imageId.substr(0, firstColonIndex),
|
||||
url,
|
||||
frame,
|
||||
};
|
||||
function normalizeDicomTime(value) {
|
||||
if (value === undefined || value === null || value === "") return null;
|
||||
const num = toNumber(value);
|
||||
if (num != null) {
|
||||
return String(Math.floor(num)).padStart(6, "0");
|
||||
}
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
const plain = trimmed.replace(/:/g, "");
|
||||
const parts = plain.split(".");
|
||||
const main = parts[0];
|
||||
const fractional = parts[1];
|
||||
if (!/^\d+$/.test(main)) {
|
||||
return trimmed;
|
||||
}
|
||||
const normalizedMain = main.padStart(6, "0");
|
||||
return fractional ? `${normalizedMain}.${fractional}` : normalizedMain;
|
||||
}
|
||||
|
||||
function getPTClinicalOverrideFromImageId(imageId) {
|
||||
try {
|
||||
const qIndex = imageId.indexOf("?");
|
||||
if (qIndex === -1) return null;
|
||||
const params = new URLSearchParams(imageId.slice(qIndex + 1));
|
||||
const visitTaskId = params.get("visitTaskId");
|
||||
const idx = params.get("idx");
|
||||
|
||||
// 场景1:trials/dicoms,imageId 带 visitTaskId + idx,优先使用列表中的最新 study 值
|
||||
if (visitTaskId && idx) {
|
||||
const parts = idx.split("|");
|
||||
const studyIndex = toNumber(parts[0]);
|
||||
if (Number.isInteger(studyIndex) && studyIndex >= 0) {
|
||||
const visitTaskList = store.state.reading.visitTaskList;
|
||||
if (Array.isArray(visitTaskList)) {
|
||||
const visitTaskInfo = visitTaskList.find(
|
||||
(v) => v && String(v.VisitTaskId) === visitTaskId
|
||||
);
|
||||
const study = visitTaskInfo?.StudyList?.[studyIndex];
|
||||
if (
|
||||
study &&
|
||||
!(
|
||||
study.PatientWeight == null &&
|
||||
study.RadionuclideTotalDose == null &&
|
||||
study.RadionuclideHalfLife == null &&
|
||||
study.RadiopharmaceuticalStartTime == null &&
|
||||
study.AcquisitionTime == null &&
|
||||
study.PatientSex == null
|
||||
)
|
||||
) {
|
||||
return study;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 场景2:src/components/Dicom,imageId 仅带 instanceId,回退到实例缓存
|
||||
const instanceId = params.get("instanceId");
|
||||
if (instanceId) {
|
||||
const cached = getPTClinicalDataForInstance(instanceId);
|
||||
if (cached) return cached;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function parseImageId(imageId) {
|
||||
// 兼容 frame 参数不在最后的情况:只移除 frame 参数本身,保留 instanceId/visitTaskId/idx 等其余 query
|
||||
const firstColonIndex = imageId.indexOf(":");
|
||||
const scheme = imageId.substr(0, firstColonIndex);
|
||||
const urlPart = imageId.substring(firstColonIndex + 1);
|
||||
let url = urlPart;
|
||||
let frame;
|
||||
const qIndex = urlPart.indexOf("?");
|
||||
if (qIndex !== -1) {
|
||||
const base = urlPart.slice(0, qIndex);
|
||||
const query = urlPart.slice(qIndex + 1);
|
||||
const parts = query.split("&").filter(Boolean);
|
||||
const preservedParts = [];
|
||||
|
||||
parts.forEach((part) => {
|
||||
const eqIndex = part.indexOf("=");
|
||||
const key = eqIndex === -1 ? part : part.slice(0, eqIndex);
|
||||
const value = eqIndex === -1 ? "" : part.slice(eqIndex + 1);
|
||||
|
||||
if (key === "frame") {
|
||||
const n = value !== "" ? parseInt(value, 10) : NaN;
|
||||
if (Number.isFinite(n)) {
|
||||
frame = n;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 保留原始 query 片段,避免 URLSearchParams 把 | 编码成 %7C 导致 dataSetCache key 对不上
|
||||
preservedParts.push(part);
|
||||
});
|
||||
|
||||
url = preservedParts.length ? `${base}?${preservedParts.join("&")}` : base;
|
||||
}
|
||||
|
||||
return { scheme, url, frame };
|
||||
}
|
||||
function getNumberValues(dataSet, tag, minimumLength) {
|
||||
const values = [];
|
||||
|
|
@ -35,7 +126,7 @@ function getNumberValues(dataSet, tag, minimumLength) {
|
|||
if (!valueAsString) {
|
||||
return;
|
||||
}
|
||||
const split = valueAsString.split('\\');
|
||||
const split = valueAsString.split("\\");
|
||||
|
||||
if (minimumLength && split.length < minimumLength) {
|
||||
return;
|
||||
|
|
@ -74,28 +165,31 @@ function getLutData(lutDataSet, tag, lutDescriptor) {
|
|||
return lut;
|
||||
}
|
||||
function populateSmallestLargestPixelValues(dataSet, imagePixelModule) {
|
||||
const pixelRepresentation = dataSet.uint16('x00280103');
|
||||
const pixelRepresentation = dataSet.uint16("x00280103");
|
||||
if (pixelRepresentation === 0) {
|
||||
imagePixelModule.smallestPixelValue = dataSet.uint16('x00280106');
|
||||
imagePixelModule.largestPixelValue = dataSet.uint16('x00280107');
|
||||
imagePixelModule.smallestPixelValue = dataSet.uint16("x00280106");
|
||||
imagePixelModule.largestPixelValue = dataSet.uint16("x00280107");
|
||||
} else {
|
||||
imagePixelModule.smallestPixelValue = dataSet.int16('x00280106');
|
||||
imagePixelModule.largestPixelValue = dataSet.int16('x00280107');
|
||||
imagePixelModule.smallestPixelValue = dataSet.int16("x00280106");
|
||||
imagePixelModule.largestPixelValue = dataSet.int16("x00280107");
|
||||
}
|
||||
imagePixelModule.largestPixelValue = imagePixelModule.largestPixelValue === 0 ? undefined : imagePixelModule.largestPixelValue;
|
||||
imagePixelModule.largestPixelValue =
|
||||
imagePixelModule.largestPixelValue === 0
|
||||
? undefined
|
||||
: imagePixelModule.largestPixelValue;
|
||||
}
|
||||
function populatePaletteColorLut(dataSet, imagePixelModule) {
|
||||
imagePixelModule.redPaletteColorLookupTableDescriptor = getLutDescriptor(
|
||||
dataSet,
|
||||
'x00281101'
|
||||
"x00281101"
|
||||
);
|
||||
imagePixelModule.greenPaletteColorLookupTableDescriptor = getLutDescriptor(
|
||||
dataSet,
|
||||
'x00281102'
|
||||
"x00281102"
|
||||
);
|
||||
imagePixelModule.bluePaletteColorLookupTableDescriptor = getLutDescriptor(
|
||||
dataSet,
|
||||
'x00281103'
|
||||
"x00281103"
|
||||
);
|
||||
|
||||
// The first Palette Color Lookup Table Descriptor value is the number of entries in the lookup table.
|
||||
|
|
@ -134,56 +228,165 @@ function populatePaletteColorLut(dataSet, imagePixelModule) {
|
|||
|
||||
imagePixelModule.redPaletteColorLookupTableData = getLutData(
|
||||
dataSet,
|
||||
'x00281201',
|
||||
"x00281201",
|
||||
imagePixelModule.redPaletteColorLookupTableDescriptor
|
||||
);
|
||||
imagePixelModule.greenPaletteColorLookupTableData = getLutData(
|
||||
dataSet,
|
||||
'x00281202',
|
||||
"x00281202",
|
||||
imagePixelModule.greenPaletteColorLookupTableDescriptor
|
||||
);
|
||||
imagePixelModule.bluePaletteColorLookupTableData = getLutData(
|
||||
dataSet,
|
||||
'x00281203',
|
||||
"x00281203",
|
||||
imagePixelModule.bluePaletteColorLookupTableDescriptor
|
||||
);
|
||||
}
|
||||
|
||||
function getSpacingBetweenSlices(dataSet) {
|
||||
if (dataSet?.elements?.x00180088) {
|
||||
return dataSet.floatString('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');
|
||||
return pixelMeasuresSequence.items[0].dataSet.floatString("x00180088");
|
||||
}
|
||||
}
|
||||
function metaDataProvider(type, imageId) {
|
||||
const parsedImageId = parseImageId(imageId);
|
||||
const dataSet = cornerstoneWADOImageLoader.wadouri.dataSetCacheManager.get(parsedImageId.url);
|
||||
const dataSet = cornerstoneWADOImageLoader.wadouri.dataSetCacheManager.get(
|
||||
parsedImageId.url
|
||||
);
|
||||
|
||||
if (!dataSet) {
|
||||
return;
|
||||
}
|
||||
if (type === 'imagePlaneModule') {
|
||||
const ptOverride = getPTClinicalOverrideFromImageId(imageId);
|
||||
if (type === "generalSeriesModule") {
|
||||
const { dicomParser } = cornerstoneWADOImageLoader.external || {};
|
||||
const overrideTime = normalizeDicomTime(ptOverride?.AcquisitionTime);
|
||||
const seriesTimeRaw =
|
||||
overrideTime ||
|
||||
dataSet.string("x00080031") ||
|
||||
dataSet.string("x00080032") ||
|
||||
"";
|
||||
const acquisitionTimeRaw =
|
||||
overrideTime ||
|
||||
dataSet.string("x00080032") ||
|
||||
dataSet.string("x00080031") ||
|
||||
"";
|
||||
return {
|
||||
modality: dataSet.string("x00080060"),
|
||||
seriesInstanceUID: dataSet.string("x0020000e"),
|
||||
seriesNumber: dataSet.intString("x00200011"),
|
||||
studyInstanceUID: dataSet.string("x0020000d"),
|
||||
seriesDate: dicomParser.parseDA(dataSet.string("x00080021")),
|
||||
// 2D SUV 计算读取的是 seriesTime,这里同步使用用户录入采集时间
|
||||
// seriesTime: dicomParser.parseTM(String(seriesTimeRaw)),
|
||||
seriesTime: dicomParser.parseTM(dataSet.string('x00080031') || ''),
|
||||
acquisitionDate: dicomParser.parseDA(dataSet.string("x00080022") || ""),
|
||||
acquisitionTime: dicomParser.parseTM(String(acquisitionTimeRaw)),
|
||||
};
|
||||
}
|
||||
if (type === "patientStudyModule") {
|
||||
const weightOverride = ptOverride?.PatientWeight;
|
||||
const weight =
|
||||
weightOverride
|
||||
? toNumber(weightOverride)
|
||||
: dataSet.floatString("x00101030");
|
||||
return {
|
||||
patientAge: dataSet.intString("x00101010"),
|
||||
patientSize: dataSet.floatString("x00101020"),
|
||||
patientWeight: weight,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "petIsotopeModule") {
|
||||
const { dicomParser } = cornerstoneWADOImageLoader.external || {};
|
||||
const radiopharmaceuticalInfo = dataSet.elements.x00540016;
|
||||
|
||||
if (radiopharmaceuticalInfo === undefined) {
|
||||
return;
|
||||
}
|
||||
const firstRadiopharmaceuticalInfoDataSet =
|
||||
radiopharmaceuticalInfo.items[0].dataSet;
|
||||
|
||||
const startTimeRaw =
|
||||
firstRadiopharmaceuticalInfoDataSet.string("x00181072") || "";
|
||||
const totalDoseRaw =
|
||||
firstRadiopharmaceuticalInfoDataSet.floatString("x00181074");
|
||||
const halfLifeRaw =
|
||||
firstRadiopharmaceuticalInfoDataSet.floatString("x00181075");
|
||||
let startTimeValue = normalizeDicomTime(
|
||||
ptOverride?.RadiopharmaceuticalStartTime
|
||||
);
|
||||
if (!startTimeValue && startTimeRaw) {
|
||||
startTimeValue = normalizeDicomTime(startTimeRaw) || startTimeRaw;
|
||||
}
|
||||
const radiopharmaceuticalStartTime = dicomParser.parseTM(
|
||||
String(startTimeValue)
|
||||
);
|
||||
const overrideTotalDose =
|
||||
ptOverride?.RadionuclideTotalDose != null &&
|
||||
ptOverride.RadionuclideTotalDose !== ""
|
||||
? toNumber(ptOverride.RadionuclideTotalDose)
|
||||
: null;
|
||||
const overrideHalfLife =
|
||||
ptOverride?.RadionuclideHalfLife != null &&
|
||||
ptOverride.RadionuclideHalfLife !== ""
|
||||
? toNumber(ptOverride.RadionuclideHalfLife)
|
||||
: null;
|
||||
const radionuclideTotalDose =
|
||||
overrideTotalDose != null ? overrideTotalDose : toNumber(totalDoseRaw);
|
||||
const radionuclideHalfLife =
|
||||
overrideHalfLife != null ? overrideHalfLife : toNumber(halfLifeRaw);
|
||||
return {
|
||||
radiopharmaceuticalInfo: {
|
||||
radiopharmaceuticalStartTime,
|
||||
radionuclideTotalDose,
|
||||
radionuclideHalfLife,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (type === "imagePlaneModule") {
|
||||
// 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);
|
||||
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 sliceThickness = extractSliceThicknessFromDataset(
|
||||
dataSet,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
const modality = dataSet.string("x00080060");
|
||||
if (modality && modality.includes('NM') && parsedImageId.frame !== undefined && parsedImageId.frame > 1) {
|
||||
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 step =
|
||||
spacingBetweenSlices !== undefined
|
||||
? spacingBetweenSlices
|
||||
: sliceThickness;
|
||||
if (
|
||||
imageOrientationPatient &&
|
||||
imagePositionPatient &&
|
||||
step !== undefined &&
|
||||
frameIndex !== undefined
|
||||
) {
|
||||
const rowCosines = [
|
||||
parseFloat(imageOrientationPatient[0]),
|
||||
parseFloat(imageOrientationPatient[1]),
|
||||
|
|
@ -210,7 +413,11 @@ function metaDataProvider(type, imageId) {
|
|||
}
|
||||
}
|
||||
|
||||
const estimatedRadiographicMagnificationFactor = getNumberValues(dataSet, 'x00181114', 2);
|
||||
const estimatedRadiographicMagnificationFactor = getNumberValues(
|
||||
dataSet,
|
||||
"x00181114",
|
||||
2
|
||||
);
|
||||
let columnPixelSpacing = null;
|
||||
|
||||
let rowPixelSpacing = null;
|
||||
|
|
@ -219,8 +426,10 @@ function metaDataProvider(type, imageId) {
|
|||
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];
|
||||
|
|
@ -244,37 +453,37 @@ 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,
|
||||
sliceLocation: dataSet.floatString('x00201041'),
|
||||
sliceLocation: dataSet.floatString("x00201041"),
|
||||
pixelSpacing,
|
||||
rowPixelSpacing,
|
||||
columnPixelSpacing,
|
||||
};
|
||||
}
|
||||
if (type === 'imagePixelModule') {
|
||||
if (type === "imagePixelModule") {
|
||||
const imagePixelModule = {
|
||||
samplesPerPixel: dataSet.uint16('x00280002'),
|
||||
photometricInterpretation: dataSet.string('x00280004'),
|
||||
rows: dataSet.uint16('x00280010'),
|
||||
columns: dataSet.uint16('x00280011'),
|
||||
bitsAllocated: dataSet.uint16('x00280100'),
|
||||
bitsStored: dataSet.uint16('x00280101'),
|
||||
highBit: dataSet.uint16('x00280102'),
|
||||
pixelRepresentation: dataSet.uint16('x00280103'),
|
||||
planarConfiguration: dataSet.uint16('x00280006'),
|
||||
pixelAspectRatio: dataSet.string('x00280034'),
|
||||
samplesPerPixel: dataSet.uint16("x00280002"),
|
||||
photometricInterpretation: dataSet.string("x00280004"),
|
||||
rows: dataSet.uint16("x00280010"),
|
||||
columns: dataSet.uint16("x00280011"),
|
||||
bitsAllocated: dataSet.uint16("x00280100"),
|
||||
bitsStored: dataSet.uint16("x00280101"),
|
||||
highBit: dataSet.uint16("x00280102"),
|
||||
pixelRepresentation: dataSet.uint16("x00280103"),
|
||||
planarConfiguration: dataSet.uint16("x00280006"),
|
||||
pixelAspectRatio: dataSet.string("x00280034"),
|
||||
};
|
||||
populateSmallestLargestPixelValues(dataSet, imagePixelModule);
|
||||
|
||||
if (
|
||||
imagePixelModule.photometricInterpretation === 'PALETTE COLOR' &&
|
||||
imagePixelModule.photometricInterpretation === "PALETTE COLOR" &&
|
||||
dataSet.elements.x00281101
|
||||
) {
|
||||
populatePaletteColorLut(dataSet, imagePixelModule);
|
||||
|
|
|
|||
|
|
@ -56,10 +56,15 @@ async function ossGenerateSTS() {
|
|||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const trialId = urlParams.get('trialId')
|
||||
if (Object.keys(fileInfo).length !== 0) {
|
||||
fileInfo.fileType = mimeTypeToExt(fileInfo.fileType)
|
||||
let params = Object.assign({path: objectName}, fileInfo)
|
||||
addOrUpdateFileUploadRecord(params)
|
||||
} else if (trialId) {
|
||||
let params = { trialId }
|
||||
const fileName = objectName.split('/').pop()
|
||||
const fileType = fileName.includes('.')
|
||||
? fileName.split('.').pop().toLowerCase()
|
||||
: ''
|
||||
let params = { trialId, path: objectName, fileName, fileType }
|
||||
addOrUpdateFileUploadRecord(params)
|
||||
}
|
||||
resolve({
|
||||
|
|
@ -108,10 +113,15 @@ async function ossGenerateSTS() {
|
|||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const trialId = urlParams.get('trialId')
|
||||
if (Object.keys(fileInfo).length !== 0) {
|
||||
fileInfo.fileType = mimeTypeToExt(fileInfo.fileType)
|
||||
let params = Object.assign({path: data.path}, fileInfo)
|
||||
addOrUpdateFileUploadRecord(params)
|
||||
} else if (trialId) {
|
||||
let params = { trialId }
|
||||
const fileName = data.path.split('/').pop()
|
||||
const fileType = fileName.includes('.')
|
||||
? fileName.split('.').pop().toLowerCase()
|
||||
: ''
|
||||
let params = { trialId, path: data.path, fileName, fileType }
|
||||
addOrUpdateFileUploadRecord(params)
|
||||
}
|
||||
|
||||
|
|
@ -235,10 +245,15 @@ function uploadAWS(aws, data, progress, fileInfo) {
|
|||
|
||||
const trialId = urlParams.get('trialId')
|
||||
if (Object.keys(fileInfo).length !== 0) {
|
||||
fileInfo.fileType = mimeTypeToExt(fileInfo.fileType)
|
||||
let params = Object.assign({path: decodeUtf8(curPath)}, fileInfo)
|
||||
addOrUpdateFileUploadRecord(params)
|
||||
} else if (trialId) {
|
||||
let params = { trialId }
|
||||
const fileName = decodeUtf8(curPath).split('/').pop()
|
||||
const fileType = fileName.includes('.')
|
||||
? fileName.split('.').pop().toLowerCase()
|
||||
: ''
|
||||
let params = { trialId, path: decodeUtf8(curPath), fileName, fileType }
|
||||
addOrUpdateFileUploadRecord(params)
|
||||
}
|
||||
resolve({
|
||||
|
|
@ -350,5 +365,37 @@ function isCredentialsExpired(credentials) {
|
|||
return expireDate.getTime() - now.getTime() <= 300000;
|
||||
|
||||
}
|
||||
function mimeTypeToExt(mimeType) {
|
||||
const map = {
|
||||
// 图片
|
||||
'image/jpeg': 'jpg',
|
||||
'image/jpg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/gif': 'gif',
|
||||
'image/webp': 'webp',
|
||||
'image/svg+xml': 'svg',
|
||||
'image/bmp': 'bmp',
|
||||
|
||||
// 文档
|
||||
'application/dicom': 'dcm',
|
||||
'application/pdf': 'pdf',
|
||||
'application/msword': 'doc',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
|
||||
'application/vnd.ms-excel': 'xls',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
|
||||
'application/vnd.ms-powerpoint': '.ppt',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
|
||||
'text/plain': 'txt',
|
||||
// 音频/视频
|
||||
'audio/mpeg': 'mp3',
|
||||
'audio/wav': 'wav',
|
||||
'video/mp4': 'mp4',
|
||||
'video/mpeg': 'mpeg',
|
||||
// 压缩包
|
||||
'application/zip': 'zip',
|
||||
'application/x-rar-compressed': 'rar',
|
||||
'application/x-7z-compressed': '7z',
|
||||
};
|
||||
return map[mimeType] || ''; // 找不到返回空字符串
|
||||
}
|
||||
export const OSSclient = ossGenerateSTS
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
// PT 临床数据缓存:用于统一 SUV 口径(优先接口,缺失则回退 DICOM 元数据)
|
||||
const instanceIdToClinicalData = new Map()
|
||||
|
||||
function normalizeId(id) {
|
||||
if (id === undefined || id === null) return null
|
||||
const s = String(id).trim()
|
||||
return s ? s : null
|
||||
}
|
||||
|
||||
export function setPTClinicalDataForInstance(instanceId, clinicalData) {
|
||||
const key = normalizeId(instanceId)
|
||||
if (!key) return
|
||||
if (!clinicalData || typeof clinicalData !== 'object') return
|
||||
instanceIdToClinicalData.set(key, { ...clinicalData })
|
||||
}
|
||||
|
||||
export function getPTClinicalDataForInstance(instanceId) {
|
||||
const key = normalizeId(instanceId)
|
||||
if (!key) return null
|
||||
return instanceIdToClinicalData.get(key) || null
|
||||
}
|
||||
|
||||
export function deletePTClinicalDataForInstance(instanceId) {
|
||||
const key = normalizeId(instanceId)
|
||||
if (!key) return
|
||||
instanceIdToClinicalData.delete(key)
|
||||
}
|
||||
|
||||
export function clearPTClinicalDataCache() {
|
||||
instanceIdToClinicalData.clear()
|
||||
}
|
||||
|
|
@ -52,18 +52,22 @@ function getScreen() {
|
|||
})
|
||||
|
||||
}
|
||||
export async function openWindow(url, name, Skip = false) {
|
||||
export async function openWindow(url, name, Skip = false, noreferrer = false) {
|
||||
// 判断浏览器是否兼容
|
||||
// 高版本的谷歌,edge不支持跨屏,需要降低浏览器版本86.0版
|
||||
let fulls = ''
|
||||
if (noreferrer) {
|
||||
fulls = 'noopener,noreferrer'
|
||||
}
|
||||
if (!window.getScreens && !window.getScreenDetails) {
|
||||
console.log('你的浏览器版本不支持多屏展示功能!');
|
||||
return window.open(url, name);
|
||||
return window.open(url, name, fulls);
|
||||
}
|
||||
let permission = await getPermission()
|
||||
if (!permission) {
|
||||
// alert('使用多屏功能请先进行授权')
|
||||
if (Skip) {
|
||||
return window.open(url, name);
|
||||
return window.open(url, name, fulls);
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -72,14 +76,13 @@ export async function openWindow(url, name, Skip = false) {
|
|||
// 判断是否2个屏幕
|
||||
if (multiScreen.length < 2) {
|
||||
console.log('请接入多个显示屏!');
|
||||
return window.open(url, name);
|
||||
return window.open(url, name, fulls);
|
||||
}
|
||||
console.log(screen, 'screen')
|
||||
// 获取当前屏幕availLeft信息和所有信息比对,取另一个屏幕数据
|
||||
let currentAvailLeft = screen.availLeft ? screen.availLeft : '0'
|
||||
let newCurr = multiScreen.find((t) => t.availLeft != currentAvailLeft)
|
||||
console.log(newCurr, 'newCurr')
|
||||
let fulls = ''
|
||||
for (let key in newCurr) {
|
||||
fulls += `${key}=${(newCurr[key] || newCurr[key] === 0) ? newCurr[key] : 0},`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,12 +123,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="viewerContent">
|
||||
<dicom-viewer id="dicomViewer" ref="dicomViewer" style="height:100%" />
|
||||
<dicom-viewer id="dicomViewer" ref="dicomViewer" style="height:100%" :loading.sync="loading"
|
||||
:modality="modality" :Comparison.sync="isComparison" @loadStudy="loadStudy" />
|
||||
</div>
|
||||
<!-- <div class="viewerRightSidePanel">
|
||||
<dicom-tools />
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -202,7 +202,8 @@ export default {
|
|||
isFromCRCUpload: false,
|
||||
isReading: null,
|
||||
activeSeriesId: null,
|
||||
isPacs: false
|
||||
isPacs: false,
|
||||
isComparison: false
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
|
|
@ -307,7 +308,7 @@ export default {
|
|||
})
|
||||
})
|
||||
},
|
||||
async loadStudy() {
|
||||
async loadStudy(isJump = true) {
|
||||
let params = {}
|
||||
if (this.isPacs) {
|
||||
params.IsPacs = true
|
||||
|
|
@ -327,7 +328,7 @@ export default {
|
|||
isReading = `?IsPacs=true`
|
||||
}
|
||||
const url = `/series/list/${this.studyId}${isReading}`
|
||||
this.getSeriesList(url)
|
||||
this.getSeriesList(url, isJump)
|
||||
}
|
||||
},
|
||||
async loadPatientStudy() {
|
||||
|
|
@ -392,7 +393,7 @@ export default {
|
|||
console.log(err)
|
||||
}
|
||||
},
|
||||
async getSeriesList(url) {
|
||||
async getSeriesList(url, isJump = true) {
|
||||
try {
|
||||
const data = await getSeriesList(url)
|
||||
if (data.IsSuccess) {
|
||||
|
|
@ -431,7 +432,7 @@ export default {
|
|||
isDeleted: item.IsDeleted,
|
||||
previewImageUrl: item.ImageResizePath ? this.OSSclientConfig.basePath + item.ImageResizePath : `/api/series/preview/${item.Id}`,
|
||||
instanceCount: item.InstanceCount,
|
||||
prefetchInstanceCount: 0,
|
||||
prefetchInstanceCount: !isJump ? item.InstanceInfoList.length * 100 : 0,
|
||||
hasLabel: item.HasLabel,
|
||||
keySeries: item.KeySeries,
|
||||
tpCode: this.tpCode,
|
||||
|
|
@ -446,9 +447,12 @@ export default {
|
|||
this.seriesList = seriesList
|
||||
if (this.seriesList.length > 0) {
|
||||
this.loadAllImages()
|
||||
if (isJump) {
|
||||
this.$refs.dicomViewer.loadImageStack(this.seriesList[0], this.labels[this.tpCode])
|
||||
this.firstInstanceId = this.seriesList[0].imageIds[0]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
|
|
@ -527,6 +531,7 @@ export default {
|
|||
}
|
||||
},
|
||||
showSeriesImage(e, seriesIndex, series) {
|
||||
if (!isComparison) return false
|
||||
this.activeSeriesId = series.seriesId
|
||||
workSpeedclose(true)
|
||||
// if (seriesIndex === this.currentSeriesIndex) return
|
||||
|
|
@ -920,6 +925,7 @@ export default {
|
|||
}
|
||||
if (this.visitTaskId === params.visitTaskId) {
|
||||
const seriesIndex = params.seriesIndex
|
||||
if (!this.seriesList[seriesIndex]) return false
|
||||
var prefetchInstanceCount = this.seriesList[seriesIndex].prefetchInstanceCount
|
||||
var instanceCount = this.seriesList[seriesIndex].instanceCount
|
||||
if (!this.activeSeriesId) {
|
||||
|
|
|
|||
|
|
@ -4,122 +4,57 @@
|
|||
<el-form :inline="true">
|
||||
<!--项目编号/实验名称-->
|
||||
<el-form-item :label="$t('feedBack:search:trials')" v-if="level > 8">
|
||||
<el-input
|
||||
v-model="searchData.TrialKeyInfo"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
></el-input>
|
||||
<el-input v-model="searchData.TrialKeyInfo" clearable style="width: 150px"></el-input>
|
||||
</el-form-item>
|
||||
<!--中心-->
|
||||
<el-form-item :label="$t('feedBack:search:site')" v-if="level > 7">
|
||||
<el-input
|
||||
v-model="searchData.TrialSiteCode"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
></el-input>
|
||||
<el-input v-model="searchData.TrialSiteCode" clearable style="width: 150px"></el-input>
|
||||
</el-form-item>
|
||||
<!--受试者访视-->
|
||||
<el-form-item
|
||||
:label="$t('feedBack:search:subjectVisit')"
|
||||
v-if="level > 7"
|
||||
>
|
||||
<el-input
|
||||
v-model="searchData.SubejctAndVisitKeyInfo"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
></el-input>
|
||||
<el-form-item :label="$t('feedBack:search:subjectVisit')" v-if="level > 7">
|
||||
<el-input v-model="searchData.SubejctAndVisitKeyInfo" clearable style="width: 150px"></el-input>
|
||||
</el-form-item>
|
||||
<!--角色-->
|
||||
<el-form-item :label="$t('feedBack:search:role')" v-if="level > 7">
|
||||
<el-select
|
||||
v-model="searchData.UserTypeEnum"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
popper-class="feedBack-select-box"
|
||||
>
|
||||
<el-option
|
||||
v-for="item of UserTypeOptins"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
<el-select v-model="searchData.UserTypeEnum" clearable style="width: 150px"
|
||||
popper-class="feedBack-select-box">
|
||||
<el-option v-for="item of UserTypeOptins" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!--反馈人-->
|
||||
<el-form-item :label="$t('feedBack:search:user')" v-if="level > 7">
|
||||
<el-input
|
||||
v-model="searchData.FeedBackUserKeyInfo"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
></el-input>
|
||||
<el-input v-model="searchData.FeedBackUserKeyInfo" clearable style="width: 150px"></el-input>
|
||||
</el-form-item>
|
||||
<!--问题类型-->
|
||||
<el-form-item :label="$t('feedBack:search:questionType')">
|
||||
<el-select
|
||||
v-model="searchData.QuestionType"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
popper-class="feedBack-select-box"
|
||||
>
|
||||
<el-option
|
||||
v-for="item of QuestionTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
<el-select v-model="searchData.QuestionType" clearable style="width: 150px"
|
||||
popper-class="feedBack-select-box">
|
||||
<el-option v-for="item of QuestionTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!--问题描述-->
|
||||
<el-form-item :label="$t('feedBack:search:description')">
|
||||
<el-input
|
||||
v-model="searchData.QuestionDescription"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
></el-input>
|
||||
<el-input v-model="searchData.QuestionDescription" clearable style="width: 150px"></el-input>
|
||||
</el-form-item>
|
||||
<!--状态-->
|
||||
<el-form-item :label="$t('feedBack:search:status')" v-if="level > 7">
|
||||
<el-select
|
||||
v-model="searchData.State"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
popper-class="feedBack-select-box"
|
||||
>
|
||||
<el-option
|
||||
v-for="item of $d.FeedBackStatus"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
<el-select v-model="searchData.State" clearable style="width: 150px" popper-class="feedBack-select-box">
|
||||
<el-option v-for="item of $d.FeedBackStatus" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!--反馈日期-->
|
||||
<el-form-item :label="$t('feedBack:search:time')">
|
||||
<el-date-picker
|
||||
v-model="datetimerange"
|
||||
type="datetimerange"
|
||||
:default-time="['00:00:00', '23:59:59']"
|
||||
:start-placeholder="$t('feedBack:search:beginTime')"
|
||||
:end-placeholder="$t('feedBack:search:endTime')"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
@change="handleDatetimeChange"
|
||||
style="width: 250px"
|
||||
popper-class="feedBack-select-box"
|
||||
/>
|
||||
<el-date-picker v-model="datetimerange" type="datetimerange" :default-time="['00:00:00', '23:59:59']"
|
||||
:start-placeholder="$t('feedBack:search:beginTime')" :end-placeholder="$t('feedBack:search:endTime')"
|
||||
value-format="yyyy-MM-dd HH:mm:ss" @change="handleDatetimeChange" style="width: 250px"
|
||||
popper-class="feedBack-select-box" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-search"
|
||||
@click="getList"
|
||||
>
|
||||
<el-button type="primary" icon="el-icon-search" @click="getList">
|
||||
{{ $t('common:button:search') }}
|
||||
</el-button>
|
||||
<!-- 重置 -->
|
||||
<el-button
|
||||
icon="el-icon-refresh-left"
|
||||
@click="handleReset"
|
||||
>
|
||||
<el-button icon="el-icon-refresh-left" @click="handleReset">
|
||||
{{ $t('common:button:reset') }}
|
||||
</el-button>
|
||||
<!-- <el-button
|
||||
|
|
@ -131,97 +66,32 @@
|
|||
{{ $t("common:button:export") }}
|
||||
</el-button> -->
|
||||
</el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="resolve"
|
||||
style="float: right"
|
||||
v-if="level > 7"
|
||||
:disabled="tableSelectData.length <= 0"
|
||||
>
|
||||
<el-button type="primary" @click="resolve" style="float: right" v-if="level > 7"
|
||||
:disabled="tableSelectData.length <= 0">
|
||||
{{ $t('feedBack:button:resolve') }}
|
||||
</el-button>
|
||||
</el-form>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
v-adaptive="{ bottomOffset: 45 }"
|
||||
height="100"
|
||||
:data="list"
|
||||
class="table"
|
||||
@selection-change="handleSelectChange"
|
||||
@sort-change="handleSortByColumn"
|
||||
:default-sort="{ prop: 'CreateTime', order: 'descending' }"
|
||||
>
|
||||
<el-table-column
|
||||
type="selection"
|
||||
align="center"
|
||||
width="45"
|
||||
v-if="level > 7"
|
||||
:selectable="handleSelectTable"
|
||||
/>
|
||||
<el-table v-loading="loading" v-adaptive="{ bottomOffset: 45 }" height="100" :data="list" class="table"
|
||||
@selection-change="handleSelectChange" @sort-change="handleSortByColumn"
|
||||
:default-sort="{ prop: 'CreateTime', order: 'descending' }">
|
||||
<el-table-column type="selection" align="center" width="45" v-if="level > 7" :selectable="handleSelectTable" />
|
||||
<el-table-column type="index" width="50" />
|
||||
<el-table-column
|
||||
:label="$t('feedBack:form:trialCode')"
|
||||
prop="TrialCode"
|
||||
min-width="100"
|
||||
show-overflow-tooltip
|
||||
sortable="custom"
|
||||
v-if="level > 8"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('feedBack:form:study')"
|
||||
prop="ExperimentName"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
sortable="custom"
|
||||
v-if="level > 8"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('feedBack:form:siteCode')"
|
||||
prop="TrialSiteCode"
|
||||
min-width="100"
|
||||
show-overflow-tooltip
|
||||
sortable="custom"
|
||||
v-if="level > 7"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('feedBack:form:SubjectCode')"
|
||||
prop="SubjectCode"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
sortable="custom"
|
||||
v-if="level > 7"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('feedBack:form:subjectVisit')"
|
||||
prop="SubjectVisitName"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
sortable="custom"
|
||||
v-if="level > 7"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('feedBack:form:role')"
|
||||
prop="FeedBackUserName"
|
||||
min-width="100"
|
||||
show-overflow-tooltip
|
||||
sortable="custom"
|
||||
v-if="level > 7"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('feedBack:form:user')"
|
||||
prop="FeedBackFullName"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
sortable="custom"
|
||||
v-if="level > 7"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('feedBack:form:questionType')"
|
||||
prop="QuestionType"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
sortable="custom"
|
||||
>
|
||||
<el-table-column :label="$t('feedBack:form:trialCode')" prop="TrialCode" min-width="100" show-overflow-tooltip
|
||||
sortable="custom" v-if="level > 8" />
|
||||
<el-table-column :label="$t('feedBack:form:study')" prop="ExperimentName" min-width="140" show-overflow-tooltip
|
||||
sortable="custom" v-if="level > 8" />
|
||||
<el-table-column :label="$t('feedBack:form:siteCode')" prop="TrialSiteCode" min-width="100"
|
||||
show-overflow-tooltip sortable="custom" v-if="level > 7" />
|
||||
<el-table-column :label="$t('feedBack:form:SubjectCode')" prop="SubjectCode" min-width="120"
|
||||
show-overflow-tooltip sortable="custom" v-if="level > 7" />
|
||||
<el-table-column :label="$t('feedBack:form:subjectVisit')" prop="SubjectVisitName" min-width="120"
|
||||
show-overflow-tooltip sortable="custom" v-if="level > 7" />
|
||||
<el-table-column :label="$t('feedBack:form:role')" prop="FeedBackUserName" min-width="100" show-overflow-tooltip
|
||||
sortable="custom" v-if="level > 7" />
|
||||
<el-table-column :label="$t('feedBack:form:user')" prop="FeedBackFullName" min-width="140" show-overflow-tooltip
|
||||
sortable="custom" v-if="level > 7" />
|
||||
<el-table-column :label="$t('feedBack:form:questionType')" prop="QuestionType" min-width="180"
|
||||
show-overflow-tooltip sortable="custom">
|
||||
<template slot-scope="scope">
|
||||
<span>{{
|
||||
QuestionTypeOptions.filter(
|
||||
|
|
@ -230,49 +100,22 @@
|
|||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('feedBack:form:description')"
|
||||
prop="QuestionDescription"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('feedBack:form:time')"
|
||||
prop="CreateTime"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('feedBack:form:status')"
|
||||
prop="State"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
sortable="custom"
|
||||
v-if="level > 7"
|
||||
>
|
||||
<el-table-column :label="$t('feedBack:form:description')" prop="QuestionDescription" min-width="200"
|
||||
show-overflow-tooltip sortable="custom" />
|
||||
<el-table-column :label="$t('feedBack:form:time')" prop="CreateTime" min-width="180" show-overflow-tooltip
|
||||
sortable="custom" />
|
||||
<el-table-column :label="$t('feedBack:form:status')" prop="State" min-width="120" show-overflow-tooltip
|
||||
sortable="custom" v-if="level > 7">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="['danger', 'success'][scope.row.State]">{{
|
||||
<el-tag :type="['danger', 'success', 'danger'][scope.row.State]">{{
|
||||
$fd('FeedBackStatus', scope.row.State)
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('feedBack:form:uploadTime')"
|
||||
prop="UpdateTime"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
sortable="custom"
|
||||
v-if="level > 7"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('common:action:action')"
|
||||
fixed="right"
|
||||
prop="UserTypeShortName"
|
||||
width="100"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<el-table-column :label="$t('feedBack:form:uploadTime')" prop="UpdateTime" min-width="180" show-overflow-tooltip
|
||||
sortable="custom" v-if="level > 7" />
|
||||
<el-table-column :label="$t('common:action:action')" fixed="right" prop="UserTypeShortName" width="100"
|
||||
show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" @click="getDetail(scope.row)">
|
||||
{{ $t('common:button:view') }}
|
||||
|
|
@ -281,12 +124,8 @@
|
|||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination" style="text-align: right; margin-top: 5px">
|
||||
<pagination
|
||||
:total="total"
|
||||
:page.sync="searchData.PageIndex"
|
||||
:limit.sync="searchData.PageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
<pagination :total="total" :page.sync="searchData.PageIndex" :limit.sync="searchData.PageSize"
|
||||
@pagination="getList" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -472,31 +311,37 @@ export default {
|
|||
display: flex;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 0;
|
||||
flex-grow: 4;
|
||||
|
||||
// border-right: 1px solid #ccc;
|
||||
.filter-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
flex: 1;
|
||||
padding: 5px 0px;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 0;
|
||||
flex-grow: 6;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.selected-row {
|
||||
background-color: cadetblue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,12 @@
|
|||
<!-- {{ $t('login:title:system_title_about') }} -->
|
||||
<svg-icon icon-class="login-logo" style="width: 250px; height: 71px" />
|
||||
</p>
|
||||
<p style="margin-bottom: 0px" v-else>{{ $t('login:title:system') }}</p>
|
||||
<p style="margin-bottom: 0px" v-else>
|
||||
<!-- {{ $t('login:title:system_title_about') }} -->
|
||||
<img src="@/assets/system.png" alt=""
|
||||
:style="{ width: isEN ? '180px' : '200px', height: isEN ? '60px' : '65px' }">
|
||||
<p style="margin-bottom: 0px">{{ $t('login:title:system') }}</p>
|
||||
</p>
|
||||
<p style="margin-bottom: 20px; margin-top: 0">
|
||||
V{{ $version.IsEnv_US ? $version.Version_US : $version.Version }}
|
||||
</p>
|
||||
|
|
@ -165,12 +170,12 @@
|
|||
Copyright © {{ new Date().getFullYear() }} Shanghai Extensive Imaging
|
||||
Inc.
|
||||
</p>
|
||||
<div style="margin-bottom: 20px" v-if="NODE_ENV === 'usa'">
|
||||
<!-- <div style="margin-bottom: 20px" v-if="NODE_ENV === 'usa'">
|
||||
<img style="width: 180px" src="@/assets/zzlogo-usa.png" alt="" />
|
||||
</div>
|
||||
<div style="margin-bottom: 20px" v-else>
|
||||
<img style="width: 180px" src="@/assets/zzlogo2.png" alt="" />
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" size="mini" @click="aboutVisible = false">
|
||||
|
|
|
|||
|
|
@ -17,11 +17,17 @@
|
|||
</el-form-item>
|
||||
|
||||
<!-- Sponsor -->
|
||||
<el-form-item :label="$t('trials:trials-list:table:sponsor')">
|
||||
<el-select v-model="searchData.SponsorId" style="width: 150px" clearable>
|
||||
<el-form-item :label="$t('trials:trials-list:table:sponsor')" v-if="IsZhiZhun">
|
||||
<el-select v-model="searchData.SponsorId" style="width: 150px" clearable filterable>
|
||||
<el-option v-for="item in sponsorList" :key="item.Id" :label="item.SponsorName" :value="item.Id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 状态 -->
|
||||
<el-form-item :label="$t('trials:trials-list:table:TrialStatusStr')" v-if="IsZhiZhun">
|
||||
<el-select v-model="searchData.TrialStatusStr" style="width: 150px" clearable>
|
||||
<el-option v-for="item in $d.TrialStatusEnum" :key="item.id" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 阅片标准 -->
|
||||
<el-form-item v-if="hasPermi(['role:ir'])" :label="$t('trials:trials-list:table:IR_ReadingCriterionList')">
|
||||
<el-select v-model="searchData.CriterionType" style="width: 150px" clearable>
|
||||
|
|
@ -347,6 +353,7 @@ import {
|
|||
import { getTrialList_Export } from '@/api/export'
|
||||
import store from '@/store'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { getUser } from '@/api/admin'
|
||||
import BaseContainer from '@/components/BaseContainer'
|
||||
import Pagination from '@/components/Pagination'
|
||||
import TrialForm from './components/TrialForm'
|
||||
|
|
@ -372,6 +379,7 @@ const searchDataDefault = () => {
|
|||
PageSize: 20,
|
||||
Asc: false,
|
||||
SortField: '',
|
||||
TrialStatusStr: null,
|
||||
CriterionType: null,
|
||||
PM_EMail: null,
|
||||
}
|
||||
|
|
@ -410,6 +418,7 @@ export default {
|
|||
{ value: 'III' },
|
||||
{ value: 'IV' },
|
||||
],
|
||||
IsZhiZhun: false,
|
||||
expeditedOption: this.$d.TrialExpeditedState,
|
||||
beginPickerOption: {
|
||||
disabledDate: (time) => {
|
||||
|
|
@ -439,6 +448,7 @@ export default {
|
|||
},
|
||||
created() {
|
||||
this.initPage()
|
||||
this.getUser()
|
||||
},
|
||||
mounted() {
|
||||
this.$EventBus.$on('reload', (data) => {
|
||||
|
|
@ -446,6 +456,11 @@ export default {
|
|||
})
|
||||
},
|
||||
methods: {
|
||||
getUser() {
|
||||
getUser().then(res => {
|
||||
this.IsZhiZhun = res.Result.IsZhiZhun
|
||||
})
|
||||
},
|
||||
initPage() {
|
||||
this.getList()
|
||||
store.dispatch('global/getSponsorList')
|
||||
|
|
|
|||
|
|
@ -313,6 +313,10 @@ export default {
|
|||
isExistsClinicalData: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
imageToolType: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
|
@ -551,7 +555,7 @@ export default {
|
|||
// window.opener.postMessage('refreshTaskList', window.location)
|
||||
// 设置当前任务阅片状态为已读
|
||||
this.adInfo.ReadingTaskState = 2
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({imageToolType: this.imageToolType})
|
||||
var isAutoTask = res.Result.AutoCutNextTask
|
||||
if (isAutoTask) {
|
||||
// store.dispatch('reading/resetVisitTasks')
|
||||
|
|
|
|||
|
|
@ -1134,7 +1134,8 @@ export default {
|
|||
// resolve()
|
||||
// })
|
||||
this.loading = true
|
||||
cornerstone.metaData.addProvider(metaDataProvider, 1);
|
||||
cornerstone.metaData.removeProvider(metaDataProvider)
|
||||
cornerstone.metaData.addProvider(metaDataProvider, 100000)
|
||||
cornerstone.loadAndCacheImage(this.stack.imageIds[this.stack.currentImageIdIndex])
|
||||
.then(async image => {
|
||||
if (this.stack.imageIds.indexOf(image.imageId) !== -1) {
|
||||
|
|
|
|||
|
|
@ -539,7 +539,7 @@
|
|||
</el-tab-pane>
|
||||
<!-- 其他 -->
|
||||
<el-tab-pane :label="$t('trials:reading:tab:others')" name="3">
|
||||
<Others v-if="activeName === '3'" />
|
||||
<Others v-if="activeName === '3'" :imageToolType="1"/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
|
|
@ -1230,7 +1230,7 @@ export default {
|
|||
async getHotKeys() {
|
||||
// const loading = this.$loading({ fullscreen: true })
|
||||
try {
|
||||
const res = await getDoctorShortcutKey({ imageToolType: 0 })
|
||||
const res = await getDoctorShortcutKey({ imageToolType: 1 })
|
||||
res.Result.map(item => {
|
||||
this.hotKeyList.push({ id: item.Id, altKey: item.AltKey, ctrlKey: item.CtrlKey, shiftKey: item.ShiftKey, metaKey: item.MetaKey, key: item.Keyboardkey, code: item.Code, text: item.Text, shortcutKeyEnum: item.ShortcutKeyEnum })
|
||||
})
|
||||
|
|
@ -2341,7 +2341,7 @@ export default {
|
|||
this.readingTaskState = 2
|
||||
await store.dispatch('reading/setVisitTaskReadingTaskState', { visitTaskId: this.visitTaskId, readingTaskState: 2 })
|
||||
await store.dispatch('reading/setCurrentReadingTaskState', 2)
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({imageToolType: 1})
|
||||
var isAutoTask = res.Result.AutoCutNextTask
|
||||
if (isAutoTask) {
|
||||
window.location.reload()
|
||||
|
|
|
|||
|
|
@ -1,20 +1,38 @@
|
|||
import { metaData } from '@cornerstonejs/core'
|
||||
// import { InstanceMetadata } from '@cornerstonejs/calculate-suv'
|
||||
import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader'
|
||||
import { getPTClinicalDataForInstance } from '@/utils/ptClinicalDataCache'
|
||||
|
||||
function parseImageId(imageId) {
|
||||
// build a url by parsing out the url scheme and frame index from the imageId
|
||||
const firstColonIndex = imageId.indexOf(':')
|
||||
|
||||
let url = imageId.substring(firstColonIndex + 1)
|
||||
const frameIndex = url.indexOf('frame=')
|
||||
|
||||
const urlPart = imageId.substring(firstColonIndex + 1)
|
||||
let url = urlPart
|
||||
let frame
|
||||
const qIndex = urlPart.indexOf('?')
|
||||
|
||||
if (frameIndex !== -1) {
|
||||
const frameStr = url.substr(frameIndex + 6)
|
||||
if (qIndex !== -1) {
|
||||
const base = urlPart.slice(0, qIndex)
|
||||
const query = urlPart.slice(qIndex + 1)
|
||||
const parts = query.split('&').filter(Boolean)
|
||||
const preservedParts = []
|
||||
|
||||
frame = parseInt(frameStr, 10)
|
||||
url = url.substr(0, frameIndex - 1)
|
||||
parts.forEach((part) => {
|
||||
const eqIndex = part.indexOf('=')
|
||||
const key = eqIndex === -1 ? part : part.slice(0, eqIndex)
|
||||
const value = eqIndex === -1 ? '' : part.slice(eqIndex + 1)
|
||||
|
||||
if (key === 'frame') {
|
||||
const n = value !== '' ? parseInt(value, 10) : NaN
|
||||
if (Number.isFinite(n)) {
|
||||
frame = n
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
preservedParts.push(part)
|
||||
})
|
||||
|
||||
url = preservedParts.length ? `${base}?${preservedParts.join('&')}` : base
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -23,6 +41,7 @@ function parseImageId(imageId) {
|
|||
frame
|
||||
}
|
||||
}
|
||||
|
||||
function getMetaData(type, imageId) {
|
||||
// const { dicomParser } = cornerstoneDICOMImageLoader.external
|
||||
const parsedImageId = parseImageId(imageId)
|
||||
|
|
@ -32,6 +51,7 @@ function getMetaData(type, imageId) {
|
|||
if (!dataSet) {
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'petImageModule') {
|
||||
// 1340137.4196974 240000
|
||||
// console.log(dataSet.string('x00541300'), dataSet.string('x00181242'))
|
||||
|
|
@ -41,173 +61,69 @@ function getMetaData(type, imageId) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function getPTImageIdInstanceMetadata(imageId) {
|
||||
const instanceId = getInstanceIdFromImageId(imageId)
|
||||
const ptClinicalData = instanceId ? getPTClinicalDataForInstance(instanceId) : null
|
||||
const petSequenceModule = metaData.get('petIsotopeModule', imageId)
|
||||
const generalSeriesModule = metaData.get('generalSeriesModule', imageId) || {}
|
||||
const patientStudyModule = metaData.get('patientStudyModule', imageId) || {}
|
||||
const ptSeriesModule = metaData.get('petSeriesModule', imageId) || {}
|
||||
const ptImageModule = getMetaData('petImageModule', imageId) || {}
|
||||
const radiopharmaceuticalInfo = petSequenceModule?.radiopharmaceuticalInfo
|
||||
|
||||
const generalSeriesModule = metaData.get('generalSeriesModule', imageId)
|
||||
const patientStudyModule = metaData.get('patientStudyModule', imageId)
|
||||
|
||||
const ptSeriesModule = metaData.get('petSeriesModule', imageId)
|
||||
// const ptImageModule = metaData.get('petImageModule', imageId)
|
||||
|
||||
const ptImageModule = getMetaData('petImageModule', imageId)
|
||||
if (!petSequenceModule) {
|
||||
if (!radiopharmaceuticalInfo) {
|
||||
throw new Error('petSequenceModule metadata is required')
|
||||
}
|
||||
|
||||
const radiopharmaceuticalInfo = petSequenceModule.radiopharmaceuticalInfo
|
||||
|
||||
const { seriesDate, seriesTime, acquisitionDate, acquisitionTime } =
|
||||
generalSeriesModule
|
||||
var { patientWeight } = patientStudyModule
|
||||
// console.log('更改前:', patientWeight)
|
||||
// patientWeight = patientWeight * 10
|
||||
// console.log('更改后:', patientWeight)
|
||||
const { correctedImage, units, decayCorrection } = ptSeriesModule
|
||||
|
||||
if (
|
||||
seriesDate === undefined ||
|
||||
seriesTime === undefined ||
|
||||
patientWeight === undefined ||
|
||||
acquisitionDate === undefined ||
|
||||
acquisitionTime === undefined ||
|
||||
correctedImage === undefined ||
|
||||
units === undefined ||
|
||||
decayCorrection === undefined ||
|
||||
radiopharmaceuticalInfo.radionuclideTotalDose === undefined ||
|
||||
radiopharmaceuticalInfo.radionuclideHalfLife === undefined ||
|
||||
(radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime === undefined &&
|
||||
seriesDate === undefined &&
|
||||
radiopharmaceuticalInfo.radiopharmaceuticalStartTime === undefined)
|
||||
//
|
||||
) {
|
||||
throw new Error('required metadata are missing')
|
||||
}
|
||||
const patientWeight = toNumber(
|
||||
ptClinicalData?.PatientWeight,
|
||||
patientStudyModule.patientWeight
|
||||
)
|
||||
const totalDose = toNumber(
|
||||
ptClinicalData?.RadionuclideTotalDose,
|
||||
radiopharmaceuticalInfo.radionuclideTotalDose
|
||||
)
|
||||
const halfLife = toNumber(
|
||||
ptClinicalData?.RadionuclideHalfLife,
|
||||
radiopharmaceuticalInfo.radionuclideHalfLife
|
||||
)
|
||||
const startTime = firstValue(
|
||||
normalizeDicomTime(ptClinicalData?.RadiopharmaceuticalStartTime),
|
||||
toDicomTimeString(radiopharmaceuticalInfo.radiopharmaceuticalStartTime)
|
||||
)
|
||||
const acquisitionTime = firstValue(
|
||||
normalizeDicomTime(ptClinicalData?.AcquisitionTime),
|
||||
toDicomTimeString(generalSeriesModule.acquisitionTime)
|
||||
)
|
||||
|
||||
const instanceMetadata = {
|
||||
CorrectedImage: correctedImage,
|
||||
Units: units,
|
||||
RadionuclideHalfLife: radiopharmaceuticalInfo.radionuclideHalfLife,
|
||||
RadionuclideTotalDose: radiopharmaceuticalInfo.radionuclideTotalDose,
|
||||
DecayCorrection: decayCorrection,
|
||||
CorrectedImage: ptSeriesModule.correctedImage,
|
||||
Units: ptSeriesModule.units,
|
||||
RadionuclideHalfLife: halfLife,
|
||||
RadionuclideTotalDose: totalDose,
|
||||
DecayCorrection: ptSeriesModule.decayCorrection,
|
||||
PatientWeight: patientWeight,
|
||||
SeriesDate: seriesDate,
|
||||
SeriesTime: seriesTime,
|
||||
AcquisitionDate: acquisitionDate,
|
||||
SeriesDate: toDicomDateString(generalSeriesModule.seriesDate),
|
||||
SeriesTime: toDicomTimeString(generalSeriesModule.seriesTime),
|
||||
AcquisitionDate: toDicomDateString(generalSeriesModule.acquisitionDate),
|
||||
AcquisitionTime: acquisitionTime
|
||||
}
|
||||
|
||||
if (
|
||||
radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime &&
|
||||
radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime !== undefined &&
|
||||
typeof radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime === 'string'
|
||||
) {
|
||||
instanceMetadata.RadiopharmaceuticalStartDateTime =
|
||||
radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime
|
||||
}
|
||||
|
||||
if (
|
||||
radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime &&
|
||||
radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime !== undefined &&
|
||||
typeof radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime !== 'string'
|
||||
) {
|
||||
const dateString = convertInterfaceDateToString(
|
||||
radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime
|
||||
assignIfPresent(
|
||||
instanceMetadata,
|
||||
'RadiopharmaceuticalStartDateTime',
|
||||
toDicomDateString(radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime)
|
||||
)
|
||||
instanceMetadata.RadiopharmaceuticalStartDateTime = dateString
|
||||
}
|
||||
|
||||
if (
|
||||
instanceMetadata.AcquisitionDate &&
|
||||
instanceMetadata.AcquisitionDate !== undefined &&
|
||||
typeof instanceMetadata.AcquisitionDate !== 'string'
|
||||
) {
|
||||
const dateString = convertInterfaceDateToString(
|
||||
instanceMetadata.AcquisitionDate
|
||||
assignIfPresent(instanceMetadata, 'RadiopharmaceuticalStartTime', startTime)
|
||||
assignIfPresent(instanceMetadata, 'FrameReferenceTime', ptImageModule.frameReferenceTime)
|
||||
assignIfPresent(instanceMetadata, 'ActualFrameDuration', ptImageModule.actualFrameDuration)
|
||||
assignIfPresent(
|
||||
instanceMetadata,
|
||||
'PatientSex',
|
||||
firstValue(ptClinicalData?.PatientSex, patientStudyModule.patientSex)
|
||||
)
|
||||
instanceMetadata.AcquisitionDate = dateString
|
||||
}
|
||||
|
||||
if (
|
||||
instanceMetadata.SeriesDate &&
|
||||
instanceMetadata.SeriesDate !== undefined &&
|
||||
typeof instanceMetadata.SeriesDate !== 'string'
|
||||
) {
|
||||
const dateString = convertInterfaceDateToString(
|
||||
instanceMetadata.SeriesDate
|
||||
)
|
||||
instanceMetadata.SeriesDate = dateString
|
||||
}
|
||||
|
||||
if (
|
||||
radiopharmaceuticalInfo.radiopharmaceuticalStartTime &&
|
||||
radiopharmaceuticalInfo.radiopharmaceuticalStartTime !== undefined &&
|
||||
typeof radiopharmaceuticalInfo.radiopharmaceuticalStartTime === 'string'
|
||||
) {
|
||||
instanceMetadata.RadiopharmaceuticalStartTime =
|
||||
radiopharmaceuticalInfo.radiopharmaceuticalStartTime
|
||||
}
|
||||
|
||||
if (
|
||||
radiopharmaceuticalInfo.radiopharmaceuticalStartTime &&
|
||||
radiopharmaceuticalInfo.radiopharmaceuticalStartTime !== undefined &&
|
||||
typeof radiopharmaceuticalInfo.radiopharmaceuticalStartTime !== 'string'
|
||||
) {
|
||||
const timeString = convertInterfaceTimeToString(
|
||||
radiopharmaceuticalInfo.radiopharmaceuticalStartTime
|
||||
)
|
||||
instanceMetadata.RadiopharmaceuticalStartTime = timeString
|
||||
}
|
||||
|
||||
if (
|
||||
instanceMetadata.AcquisitionTime &&
|
||||
instanceMetadata.AcquisitionTime !== undefined &&
|
||||
typeof instanceMetadata.AcquisitionTime !== 'string'
|
||||
) {
|
||||
const timeString = convertInterfaceTimeToString(
|
||||
instanceMetadata.AcquisitionTime
|
||||
)
|
||||
instanceMetadata.AcquisitionTime = timeString
|
||||
}
|
||||
|
||||
if (
|
||||
instanceMetadata.SeriesTime &&
|
||||
instanceMetadata.SeriesTime !== undefined &&
|
||||
typeof instanceMetadata.SeriesTime !== 'string'
|
||||
) {
|
||||
const timeString = convertInterfaceTimeToString(
|
||||
instanceMetadata.SeriesTime
|
||||
)
|
||||
instanceMetadata.SeriesTime = timeString
|
||||
}
|
||||
|
||||
if (
|
||||
ptImageModule.frameReferenceTime &&
|
||||
ptImageModule.frameReferenceTime !== undefined
|
||||
) {
|
||||
instanceMetadata.FrameReferenceTime = ptImageModule.frameReferenceTime
|
||||
}
|
||||
|
||||
if (
|
||||
ptImageModule.actualFrameDuration &&
|
||||
ptImageModule.actualFrameDuration !== undefined
|
||||
) {
|
||||
instanceMetadata.ActualFrameDuration = ptImageModule.actualFrameDuration
|
||||
}
|
||||
|
||||
if (
|
||||
patientStudyModule.patientSex &&
|
||||
patientStudyModule.patientSex !== undefined
|
||||
) {
|
||||
instanceMetadata.PatientSex = patientStudyModule.patientSex
|
||||
}
|
||||
|
||||
if (
|
||||
patientStudyModule.patientSize &&
|
||||
patientStudyModule.patientSize !== undefined
|
||||
) {
|
||||
instanceMetadata.PatientSize = patientStudyModule.patientSize
|
||||
}
|
||||
assignIfPresent(instanceMetadata, 'PatientSize', patientStudyModule.patientSize)
|
||||
|
||||
return instanceMetadata
|
||||
}
|
||||
|
|
@ -222,15 +138,81 @@ function convertInterfaceTimeToString(time) {
|
|||
'0'
|
||||
)
|
||||
|
||||
const timeString = `${hours}${minutes}${seconds}.${fractionalSeconds}`
|
||||
return timeString
|
||||
return `${hours}${minutes}${seconds}.${fractionalSeconds}`
|
||||
}
|
||||
|
||||
function convertInterfaceDateToString(date) {
|
||||
const month = `${date.month}`.padStart(2, '0')
|
||||
const day = `${date.day}`.padStart(2, '0')
|
||||
const dateString = `${date.year}${month}${day}`
|
||||
return dateString
|
||||
return `${date.year}${month}${day}`
|
||||
}
|
||||
|
||||
export { getPTImageIdInstanceMetadata }
|
||||
|
||||
function getInstanceIdFromImageId(imageId) {
|
||||
try {
|
||||
const qIndex = imageId.indexOf('?')
|
||||
if (qIndex === -1) return null
|
||||
const params = new URLSearchParams(imageId.slice(qIndex + 1))
|
||||
const instanceId = params.get('instanceId')
|
||||
if (!instanceId) return null
|
||||
return String(instanceId).trim() || null
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDicomTime(value) {
|
||||
if (!hasValue(value)) return null
|
||||
if (typeof value === 'object') {
|
||||
return convertInterfaceTimeToString(value)
|
||||
}
|
||||
const raw = String(value).trim()
|
||||
if (!raw) return null
|
||||
|
||||
if (raw.includes(':')) {
|
||||
const parts = raw.split(':')
|
||||
const hh = `${parts[0] || '00'}`.padStart(2, '0')
|
||||
const mm = `${parts[1] || '00'}`.padStart(2, '0')
|
||||
const ss = `${parts[2] || '00'}`.padStart(2, '0')
|
||||
return `${hh}${mm}${ss}.000000`
|
||||
}
|
||||
|
||||
const cleaned = raw.replace(/[^\d.]/g, '')
|
||||
if (!cleaned) return null
|
||||
const [baseRaw, fracRaw] = cleaned.split('.')
|
||||
const base = `${baseRaw || ''}`.padStart(6, '0').slice(-6)
|
||||
const frac = `${fracRaw || ''}`.padEnd(6, '0').slice(0, 6)
|
||||
return `${base}.${frac}`
|
||||
}
|
||||
|
||||
function hasValue(value) {
|
||||
return value !== undefined && value !== null && value !== ''
|
||||
}
|
||||
|
||||
function firstValue(...values) {
|
||||
return values.find(hasValue)
|
||||
}
|
||||
|
||||
function toNumber(...values) {
|
||||
const value = firstValue(...values)
|
||||
if (!hasValue(value)) return value
|
||||
|
||||
const numberValue = Number(value)
|
||||
return Number.isFinite(numberValue) ? numberValue : value
|
||||
}
|
||||
|
||||
function toDicomTimeString(value) {
|
||||
if (!hasValue(value)) return undefined
|
||||
return typeof value === 'string' ? value : convertInterfaceTimeToString(value)
|
||||
}
|
||||
|
||||
function toDicomDateString(value) {
|
||||
if (!hasValue(value)) return undefined
|
||||
return typeof value === 'string' ? value : convertInterfaceDateToString(value)
|
||||
}
|
||||
|
||||
function assignIfPresent(target, key, value) {
|
||||
if (!hasValue(value)) return
|
||||
target[key] = value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,28 @@ import { utilities as csUtils } from '@cornerstonejs/core'
|
|||
|
||||
const scalingPerImageId = {}
|
||||
|
||||
function normalizeImageURI(imageURI) {
|
||||
if (!imageURI) return imageURI
|
||||
const qIndex = imageURI.indexOf('?')
|
||||
if (qIndex === -1) return imageURI
|
||||
const base = imageURI.slice(0, qIndex)
|
||||
const query = imageURI.slice(qIndex + 1)
|
||||
const params = new URLSearchParams(query)
|
||||
if (!params.has('frame')) return imageURI
|
||||
params.delete('frame')
|
||||
const rest = params.toString()
|
||||
return rest ? `${base}?${rest}` : base
|
||||
}
|
||||
|
||||
function addInstance(imageId, scalingMetaData) {
|
||||
const imageURI = csUtils.imageIdToURI(imageId)
|
||||
// 统一缩放元数据的 key:忽略 frame 参数,避免 getCurrentImageId() 带 frame 导致查不到 scalingModule
|
||||
const imageURI = normalizeImageURI(csUtils.imageIdToURI(imageId))
|
||||
scalingPerImageId[imageURI] = scalingMetaData
|
||||
}
|
||||
|
||||
function get(type, imageId) {
|
||||
if (type === 'scalingModule') {
|
||||
const imageURI = csUtils.imageIdToURI(imageId)
|
||||
const imageURI = normalizeImageURI(csUtils.imageIdToURI(imageId))
|
||||
return scalingPerImageId[imageURI]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export default {
|
|||
this.loading = true
|
||||
this.hotKeyList = []
|
||||
try {
|
||||
const res = await getDoctorShortcutKey({ imageToolType: this.readingTool })
|
||||
const res = await getDoctorShortcutKey({ imageToolType: this.readingTool === 1 ? 2 : 1 })
|
||||
if (res.IsSuccess) {
|
||||
res.Result.map(item => {
|
||||
this.hotKeyList.push({ id: item.Id, keys: { controlKey: { altKey: item.AltKey, ctrlKey: item.CtrlKey, shiftKey: item.ShiftKey, metaKey: item.MetaKey, key: item.Keyboardkey, code: item.Code }, text: item.Text }, label: item.ShortcutKeyEnum })
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@
|
|||
import { setAutoCutNextTask, getAutoCutNextTask } from '@/api/user'
|
||||
export default {
|
||||
name: 'Others',
|
||||
props: {
|
||||
imageToolType: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
|
|
@ -47,7 +53,7 @@ export default {
|
|||
async initForm() {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({imageToolType: this.imageToolType})
|
||||
if (res.IsSuccess) {
|
||||
this.form.AutoCutNextTask = res.Result.AutoCutNextTask
|
||||
this.form.IsDoubleScreen = res.Result.IsDoubleScreen
|
||||
|
|
@ -62,7 +68,8 @@ export default {
|
|||
if (!valid) return
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await setAutoCutNextTask(this.form)
|
||||
let params = Object.assign(this.form, {imageToolType: this.imageToolType})
|
||||
const res = await setAutoCutNextTask(params)
|
||||
if (res.IsSuccess) {
|
||||
this.$message.success(this.$t('common:message:savedSuccessfully'))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -733,7 +733,7 @@ export default {
|
|||
await store.dispatch('reading/setVisitTaskReadingTaskState', { visitTaskId: this.visitTaskId, readingTaskState: 2 })
|
||||
// DicomEvent.$emit('setReadingState', 2)
|
||||
await store.dispatch('reading/setCurrentReadingTaskState', 2)
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({imageToolType: 1})
|
||||
var isAutoTask = res.Result.AutoCutNextTask
|
||||
if (isAutoTask) {
|
||||
// DicomEvent.$emit('reload')
|
||||
|
|
@ -780,7 +780,7 @@ export default {
|
|||
var readingTool = this.$router.currentRoute.query.readingTool
|
||||
var path = `/readingDicoms?TrialReadingCriterionId=${trialReadingCriterionId}&trialId=${trialId}&subjectCode=${subjectCode}&subjectId=${subjectId}&visitTaskId=${task.VisitTaskId}&isReadingTaskViewInOrder=${isReadingTaskViewInOrder}&criterionType=${criterionType}&readingTool=${readingTool}&TokenKey=${token}`
|
||||
const routeData = this.$router.resolve({ path })
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({imageToolType: 1})
|
||||
let IsDoubleScreen = false
|
||||
if (res.IsSuccess) {
|
||||
IsDoubleScreen = res.Result.IsDoubleScreen
|
||||
|
|
|
|||
|
|
@ -99,6 +99,37 @@
|
|||
<el-tooltip class="item" effect="dark" :content="series.description" placement="right">
|
||||
<div style="">{{ series.description }}</div>
|
||||
</el-tooltip>
|
||||
<div class="patient-info" style="position: absolute;right: 0;top: 30px;" v-if="['PT','PET'].includes(series.modality)">
|
||||
<el-popover placement="right" trigger="hover" popper-class="patient-info-popper">
|
||||
<h4>{{ $t('trials:ptData:title') }}</h4>
|
||||
<div class="patient-info-row">
|
||||
<label>{{ $t('trials:ptData:label:patientSex') }}</label>
|
||||
<span>{{ study.PatientSex }}</span>
|
||||
</div>
|
||||
<div class="patient-info-row">
|
||||
<label>{{ $t('trials:ptData:label:patientWeight') }}</label>
|
||||
<span>{{ study.PatientWeight }}</span>
|
||||
</div>
|
||||
<div class="patient-info-row">
|
||||
<label>{{ $t('trials:ptData:label:totalDose') }}</label>
|
||||
<span>{{ study.RadionuclideTotalDose }}</span>
|
||||
</div>
|
||||
<div class="patient-info-row">
|
||||
<label>{{ $t('trials:ptData:label:halfLife') }}</label>
|
||||
<span>{{ study.RadionuclideHalfLife }}</span>
|
||||
</div>
|
||||
<div class="patient-info-row">
|
||||
<label>{{ $t('trials:ptData:label:injectTime') }}</label>
|
||||
<span>{{ study.RadiopharmaceuticalStartTime }}</span>
|
||||
</div>
|
||||
<div class="patient-info-row">
|
||||
<label>{{ $t('trials:ptData:label:acquisitionTime') }}</label>
|
||||
<span>{{ study.AcquisitionTime }}</span>
|
||||
</div>
|
||||
<i slot="reference" class="el-icon-document"
|
||||
style="font-size: 15px;cursor: pointer;color: #f5f7fa;" />
|
||||
</el-popover>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p v-show="series.sliceThickness && !study.IsCriticalSequence">
|
||||
|
|
@ -1108,3 +1139,46 @@ export default {
|
|||
background-color: #213a54;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.patient-info-popper {
|
||||
font-size: 12px;
|
||||
color: #ddd;
|
||||
background-color: #2f2f2f;
|
||||
border-color: #5a5a5a;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.patient-info-popper[x-placement^='right'] .popper__arrow {
|
||||
border-right-color: #5a5a5a;
|
||||
}
|
||||
|
||||
.patient-info-popper[x-placement^='right'] .popper__arrow::after {
|
||||
border-right-color: #2f2f2f;
|
||||
}
|
||||
|
||||
.patient-info-popper .patient-info-row {
|
||||
display: grid;
|
||||
grid-template-columns: 100px minmax(0, 1fr);
|
||||
column-gap: 12px;
|
||||
align-items: center;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.patient-info-popper .patient-info-row + .patient-info-row {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.patient-info-popper label {
|
||||
color: #bbb;
|
||||
// font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.patient-info-popper span {
|
||||
text-align: left;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1081,7 +1081,8 @@ export default {
|
|||
// resolve()
|
||||
// })
|
||||
this.loading = true
|
||||
cornerstone.metaData.addProvider(metaDataProvider, 1);
|
||||
cornerstone.metaData.removeProvider(metaDataProvider)
|
||||
cornerstone.metaData.addProvider(metaDataProvider, 100000)
|
||||
cornerstone.loadAndCacheImage(this.stack.imageIds[this.stack.currentImageIdIndex])
|
||||
.then(async image => {
|
||||
if (this.stack.imageIds.indexOf(image.imageId) !== -1) {
|
||||
|
|
|
|||
|
|
@ -411,7 +411,7 @@
|
|||
<WL v-if="activeName === '2'" @getWwcTpl="getWwcTpl" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('trials:reading:tab:others')" name="3">
|
||||
<Others v-if="activeName === '3'" />
|
||||
<Others v-if="activeName === '3'" :imageToolType="1"/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
|
|
@ -924,7 +924,7 @@ export default {
|
|||
async getHotKeys() {
|
||||
// const loading = this.$loading({ fullscreen: true })
|
||||
try {
|
||||
let res = await getDoctorShortcutKey({ imageToolType: 0 })
|
||||
let res = await getDoctorShortcutKey({ imageToolType: 1 })
|
||||
res.Result.map((item) => {
|
||||
this.hotKeyList.push({
|
||||
id: item.Id,
|
||||
|
|
|
|||
|
|
@ -835,7 +835,7 @@ export default {
|
|||
store.dispatch('reading/setVisitTaskReadingTaskState', { visitTaskId: this.visitTaskId, readingTaskState: 2 })
|
||||
DicomEvent.$emit('setReadingState', 2)
|
||||
window.opener.postMessage('refreshTaskList', window.location)
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({imageToolType: 1})
|
||||
let isAutoTask = res.Result.AutoCutNextTask
|
||||
if (isAutoTask) {
|
||||
window.location.reload()
|
||||
|
|
@ -875,7 +875,7 @@ export default {
|
|||
var trialReadingCriterionId = this.$router.currentRoute.query.TrialReadingCriterionId
|
||||
var path = `/readingDicoms?TrialReadingCriterionId=${trialReadingCriterionId}&trialId=${trialId}&subjectCode=${subjectCode}&subjectId=${subjectId}&visitTaskId=${task.VisitTaskId}&isReadingTaskViewInOrder=${isReadingTaskViewInOrder}&criterionType=${criterionType}&readingTool=${readingTool}&TokenKey=${token}`
|
||||
const routeData = this.$router.resolve({ path })
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({imageToolType: 1})
|
||||
let IsDoubleScreen = false
|
||||
if (res.IsSuccess) {
|
||||
IsDoubleScreen = res.Result.IsDoubleScreen
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default {
|
|||
this.loading = true
|
||||
this.hotKeyList = []
|
||||
try{
|
||||
let res = await getDoctorShortcutKey({ imageToolType: this.readingTool })
|
||||
let res = await getDoctorShortcutKey({ imageToolType: this.readingTool === 1 ? 2 : 1 })
|
||||
res.Result.map(item => {
|
||||
this.hotKeyList.push({ id: item.Id, keys: { controlKey: { altKey: item.AltKey, ctrlKey: item.CtrlKey, shiftKey: item.ShiftKey, metaKey: item.MetaKey, key: item.Keyboardkey, code: item.Code }, text: item.Text }, label: item.ShortcutKeyEnum })
|
||||
})
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@
|
|||
import { setAutoCutNextTask, getAutoCutNextTask } from '@/api/user'
|
||||
export default {
|
||||
name: 'Others',
|
||||
props: {
|
||||
imageToolType: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
|
|
@ -38,7 +44,7 @@ export default {
|
|||
async initForm() {
|
||||
this.loading = true
|
||||
try{
|
||||
await getAutoCutNextTask()
|
||||
await getAutoCutNextTask({imageToolType: this.imageToolType})
|
||||
this.form.AutoCutNextTask = res.Result.AutoCutNextTask
|
||||
this.loading = false
|
||||
}catch(e){
|
||||
|
|
@ -50,7 +56,8 @@ export default {
|
|||
if (!valid) return
|
||||
this.loading = true
|
||||
try{
|
||||
await setAutoCutNextTask(this.form)
|
||||
let params = Object.assign(this.form, {imageToolType: this.imageToolType})
|
||||
await setAutoCutNextTask(params)
|
||||
this.loading = false
|
||||
this.$message.success(this.$t('common:message:savedSuccessfully'))
|
||||
}catch(e){
|
||||
|
|
|
|||
|
|
@ -52,21 +52,21 @@
|
|||
<GlobalReview v-else-if="isShow && readingCategory === 2" :trial-id="trialId" :subject-id="subjectId"
|
||||
:visit-task-id="visitTaskId" :reading-category="readingCategory" :subject-code="subjectCode"
|
||||
:task-blind-name="taskBlindName" :is-reading-show-subject-info="isReadingShowSubjectInfo"
|
||||
:is-reading-show-previous-results="isReadingShowPreviousResults"
|
||||
:is-exists-clinical-data="isExistsClinicalData" />
|
||||
:is-reading-show-previous-results="isReadingShowPreviousResults" :is-exists-clinical-data="isExistsClinicalData"
|
||||
:imageToolType="1" />
|
||||
<!-- 裁判阅片 -->
|
||||
<AdReview v-else-if="isShow && readingCategory === 4" :trial-id="trialId" :subject-id="subjectId"
|
||||
:visit-task-id="visitTaskId" :reading-category="readingCategory" :subject-code="subjectCode"
|
||||
:task-blind-name="taskBlindName" :is-reading-show-subject-info="isReadingShowSubjectInfo"
|
||||
:is-reading-show-previous-results="isReadingShowPreviousResults"
|
||||
:is-exists-clinical-data="isExistsClinicalData" />
|
||||
:is-reading-show-previous-results="isReadingShowPreviousResults" :is-exists-clinical-data="isExistsClinicalData"
|
||||
:imageToolType="1" />
|
||||
|
||||
<!-- 肿瘤学阅片 -->
|
||||
<OncologyReview v-else-if="isShow && readingCategory === 5" :trial-id="trialId" :subject-id="subjectId"
|
||||
:visit-task-id="visitTaskId" :reading-category="readingCategory" :subject-code="subjectCode"
|
||||
:task-blind-name="taskBlindName" :is-reading-show-subject-info="isReadingShowSubjectInfo"
|
||||
:is-reading-show-previous-results="isReadingShowPreviousResults"
|
||||
:is-exists-clinical-data="isExistsClinicalData" />
|
||||
:is-reading-show-previous-results="isReadingShowPreviousResults" :is-exists-clinical-data="isExistsClinicalData"
|
||||
:imageToolType="1" />
|
||||
|
||||
<el-dialog :visible.sync="dialogVisible" :custom-class="isFullscreen ? 'full-dialog-container' : 'dialog-container'"
|
||||
:show-close="false" :close-on-click-modal="false" :fullscreen="isFullscreen">
|
||||
|
|
@ -285,6 +285,13 @@ export default {
|
|||
store.dispatch('reading/resetVisitTasks')
|
||||
this.getTaskInfo()
|
||||
window.addEventListener('beforeunload', this.handleWindowClose)
|
||||
window.addEventListener('storage', (event) => {
|
||||
if (event.key === 'closePage') {
|
||||
if (this.$router.currentRoute.query.pageType && this.$router.currentRoute.query.pageType === 'History') {
|
||||
window.close()
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
DicomEvent.$off('getNextTask')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
// Note_RectangleRoiTool.js
|
||||
import * as cornerstoneTools from 'cornerstone-tools';
|
||||
|
||||
export default class Note_RectangleRoiTool extends cornerstoneTools.RectangleRoiTool {
|
||||
constructor(props = {}) {
|
||||
const defaultProps = {
|
||||
name: 'Note_RectangleRoi',
|
||||
configuration: {
|
||||
fillColor: 'rgba(255, 255, 0, 0.3)', // 默认填充颜色:半透明黄色
|
||||
strokeColor: 'yellow', // 边框颜色
|
||||
lineWidth: 2, // 边框宽度
|
||||
drawHandles: true, // 是否绘制控制点
|
||||
handleColor: 'white', // 控制点颜色
|
||||
}
|
||||
};
|
||||
super({ ...defaultProps, ...props });
|
||||
}
|
||||
|
||||
renderToolData(evt) {
|
||||
const eventData = evt.detail;
|
||||
const { element } = eventData;
|
||||
|
||||
// 获取工具状态
|
||||
const toolData = cornerstoneTools.getToolState(element, this.name);
|
||||
|
||||
if (!toolData || !toolData.data || !toolData.data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = eventData.canvasContext.canvas;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
context.save();
|
||||
|
||||
// 获取配置
|
||||
const fillColor = this.configuration.fillColor || 'rgba(255, 255, 0, 0.3)';
|
||||
const strokeColor = this.configuration.strokeColor || 'yellow';
|
||||
const lineWidth = this.configuration.lineWidth || 2;
|
||||
const drawHandles = this.configuration.drawHandles !== false;
|
||||
|
||||
// 遍历所有矩形标注
|
||||
toolData.data.forEach((measurement) => {
|
||||
if (!measurement.handles?.start || !measurement.handles?.end) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = measurement.handles.start;
|
||||
const end = measurement.handles.end;
|
||||
|
||||
// 计算矩形坐标和尺寸
|
||||
const x = Math.min(start.x, end.x);
|
||||
const y = Math.min(start.y, end.y);
|
||||
const width = Math.abs(end.x - start.x);
|
||||
const height = Math.abs(end.y - start.y);
|
||||
|
||||
// 1. 绘制填充
|
||||
if (fillColor) {
|
||||
context.fillStyle = fillColor;
|
||||
context.fillRect(x, y, width, height);
|
||||
}
|
||||
|
||||
// 2. 绘制边框
|
||||
context.strokeStyle = strokeColor;
|
||||
context.lineWidth = lineWidth;
|
||||
context.strokeRect(x, y, width, height);
|
||||
|
||||
// 3. 绘制控制点(可选)
|
||||
if (drawHandles) {
|
||||
this.drawHandles(context, measurement, eventData);
|
||||
}
|
||||
});
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制控制点
|
||||
*/
|
||||
drawHandles(context, measurement, eventData) {
|
||||
const handles = measurement.handles;
|
||||
const handleColor = this.configuration.handleColor || 'white';
|
||||
|
||||
// 绘制所有控制点
|
||||
Object.keys(handles).forEach(key => {
|
||||
const handle = handles[key];
|
||||
if (handle && typeof handle.x === 'number' && typeof handle.y === 'number') {
|
||||
context.beginPath();
|
||||
context.arc(handle.x, handle.y, 5, 0, 2 * Math.PI);
|
||||
context.fillStyle = handleColor;
|
||||
context.fill();
|
||||
context.strokeStyle = 'black';
|
||||
context.lineWidth = 1;
|
||||
context.stroke();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -185,7 +185,8 @@ export default {
|
|||
rotateAngle: 0,
|
||||
rotateBarLeft: 0,
|
||||
loading: false,
|
||||
toggleClipPlayTimer: null
|
||||
toggleClipPlayTimer: null,
|
||||
isFlip: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -363,7 +364,11 @@ export default {
|
|||
|
||||
return 'unknown';
|
||||
},
|
||||
setFilp(f = false) {
|
||||
this.isFlip = f
|
||||
},
|
||||
stackNewImage(e) {
|
||||
if (this.isFlip) return this.isFlip = false
|
||||
const { detail } = e
|
||||
this.series.SliceIndex = detail.imageIndex
|
||||
this.sliderInfo.height = detail.imageIndex * 100 / detail.numberOfSlices
|
||||
|
|
@ -512,7 +517,7 @@ export default {
|
|||
const renderingEngine = getRenderingEngine(this.renderingEngineId)
|
||||
const viewport = renderingEngine.getViewport(this.viewportId)
|
||||
const currentImageIdIndex = viewport.getCurrentImageIdIndex()
|
||||
const numImages = viewport.getImageIds().length
|
||||
const numImages = this.imageInfo.total
|
||||
let newImageIdIndex = null
|
||||
if (type === 0) {
|
||||
newImageIdIndex = 0
|
||||
|
|
@ -628,6 +633,7 @@ export default {
|
|||
let volume = cache.getVolume(this.volumeId)
|
||||
// console.log(volume, 'volume')
|
||||
if (this.series.orientation === 'AXIAL' && this.series.curIndex) return this.setFullScreen(this.series.curIndex)
|
||||
console.log(this.series.orientation, this.series.curIndex)
|
||||
let index = this.series.orientation === 'AXIAL' ? Math.ceil((volume._imageIds.length - 1) / 2) - 1 : Math.ceil((volume.dimensions[0]) / 2) - 1
|
||||
this.setFullScreen(index)
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -211,20 +211,22 @@ export default {
|
|||
this.mousePosition.index = []
|
||||
this.mousePosition.value = null
|
||||
})
|
||||
document.addEventListener('mouseup', () => {
|
||||
this.sliderMouseup()
|
||||
document.addEventListener('mouseup', this.handleDocumentMouseUp)
|
||||
document.addEventListener('mousemove', this.handleDocumentMouseMove)
|
||||
// console.log(cornerstoneTools)
|
||||
// element.addEventListener('CORNERSTONE_STACK_NEW_IMAGE', this.stackNewImage)
|
||||
},
|
||||
handleDocumentMouseUp(e) {
|
||||
this.sliderMouseup(e)
|
||||
if (this.isMip) {
|
||||
this.rotateBarMouseup()
|
||||
this.rotateBarMouseup(e)
|
||||
}
|
||||
})
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
},
|
||||
handleDocumentMouseMove(e) {
|
||||
this.sliderMousemove(e)
|
||||
if (this.isMip) {
|
||||
this.rotateBarMousemove(e)
|
||||
}
|
||||
})
|
||||
// console.log(cornerstoneTools)
|
||||
// element.addEventListener('CORNERSTONE_STACK_NEW_IMAGE', this.stackNewImage)
|
||||
},
|
||||
stackNewImage(e) {
|
||||
const { detail } = e
|
||||
|
|
@ -670,14 +672,14 @@ export default {
|
|||
this.series = { ...data }
|
||||
if (this.isMip) {
|
||||
let volume = cache.getVolume(this.volumeId)
|
||||
const ptVolumeDimensions = volume.dimensions;
|
||||
const ptVolumeDimensions = volume.dimensions
|
||||
const slabThickness = Math.sqrt(
|
||||
ptVolumeDimensions[0] * ptVolumeDimensions[0] +
|
||||
ptVolumeDimensions[1] * ptVolumeDimensions[1] +
|
||||
ptVolumeDimensions[2] * ptVolumeDimensions[2]
|
||||
);
|
||||
viewport
|
||||
.setVolumes([{
|
||||
)
|
||||
|
||||
await viewport.setVolumes([{
|
||||
volumeId: this.volumeId,
|
||||
callback: (r) => {
|
||||
if (this.series.Modality === 'NM') {
|
||||
|
|
@ -685,7 +687,6 @@ export default {
|
|||
} else {
|
||||
setPetTransferFunctionForVolumeActor(r)
|
||||
}
|
||||
// setPetColorMapTransferFunctionForVolumeActor(r)
|
||||
console.log("mip渲染成功")
|
||||
},
|
||||
slabThickness,
|
||||
|
|
@ -694,6 +695,9 @@ export default {
|
|||
orientation: OrientationAxis.CORONAL
|
||||
}
|
||||
}])
|
||||
|
||||
// viewport.setBlendMode(BlendModes.MAXIMUM_INTENSITY_BLEND)
|
||||
// viewport.setSlabThickness(slabThickness)
|
||||
} else {
|
||||
viewport
|
||||
.setVolumes([{
|
||||
|
|
@ -838,7 +842,6 @@ export default {
|
|||
this.sliderInfo.isMove = false
|
||||
},
|
||||
handletoolsMouseWheel(e) {
|
||||
if (this.activeTool === 'Crosshairs') return
|
||||
const { viewportId, wheel } = e.detail
|
||||
if (this.isMip) {
|
||||
const container = document.getElementById('rotateBar')
|
||||
|
|
@ -862,7 +865,6 @@ export default {
|
|||
this.rotateBarInfo.isMove = false
|
||||
},
|
||||
rotateBarMousemove(e) {
|
||||
if (this.activeTool === 'Crosshairs') return
|
||||
// 滚动旋转
|
||||
if (!this.rotateBarInfo.isMove) return
|
||||
const container = document.getElementById('rotateBar')
|
||||
|
|
@ -874,11 +876,11 @@ export default {
|
|||
if (x > containerWidth - sliderWidth) x = containerWidth - sliderWidth
|
||||
const deltaX = x - this.rotateBarLeft
|
||||
const angle = Math.sin((deltaX * (360 / (containerWidth - sliderWidth))) * Math.PI / 180)
|
||||
// const angle = (deltaX / (containerWidth - sliderWidth)) * (2 * Math.PI)
|
||||
this.rotate(angle)
|
||||
this.rotateBarLeft = x
|
||||
},
|
||||
rotateBarMousedown(e) {
|
||||
if (this.activeTool === 'Crosshairs') return
|
||||
this.rotateBarInfo.initLeft = e.srcElement.offsetLeft
|
||||
this.rotateBarInfo.initX = e.clientX
|
||||
this.rotateBarInfo.isMove = true
|
||||
|
|
@ -917,7 +919,6 @@ export default {
|
|||
viewport.render()
|
||||
},
|
||||
clickRotate(e) {
|
||||
if (this.activeTool === 'Crosshairs') return
|
||||
// console.log('clickRotate')
|
||||
const container = document.getElementById('rotateBar')
|
||||
const containerWidth = container.offsetWidth
|
||||
|
|
@ -926,6 +927,7 @@ export default {
|
|||
const x = Math.trunc(e.offsetX)
|
||||
const deltaX = x - this.rotateBarLeft
|
||||
const angle = Math.sin((deltaX * (360 / (containerWidth - sliderWidth))) * Math.PI / 180)
|
||||
// const angle = (deltaX / (containerWidth - sliderWidth)) * (2 * Math.PI)
|
||||
this.rotate(angle)
|
||||
this.rotateBarLeft = x
|
||||
},
|
||||
|
|
@ -936,6 +938,8 @@ export default {
|
|||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('mouseup', this.handleDocumentMouseUp)
|
||||
document.removeEventListener('mousemove', this.handleDocumentMouseMove)
|
||||
this.series = null
|
||||
this.topFusionVolumeActor = null
|
||||
},
|
||||
|
|
@ -1180,4 +1184,5 @@ export default {
|
|||
cursor: move
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -162,9 +162,14 @@
|
|||
</div>
|
||||
<!-- MPR -->
|
||||
<div class="tool-item" :title="`${$t('trials:reading:button:mpr')}`" @click.prevent="openMPRViewport()"
|
||||
v-if="(criterionType === 0 && readingTool === 0) || this.readingTool === 3">
|
||||
v-if="((criterionType === 0 && readingTool === 0) || this.readingTool === 3) && !isMPR">
|
||||
<svg-icon icon-class="mpr" class="svg-icon" style="transform: rotate(180deg);" />
|
||||
</div>
|
||||
<!-- 退出MPR -->
|
||||
<div class="tool-item" :title="`${$t('trials:reading:button:exit_mpr')}`" @click.prevent="openMPRViewport()"
|
||||
v-if="((criterionType === 0 && readingTool === 0) || this.readingTool === 3) && isMPR">
|
||||
<svg-icon icon-class="exit_mpr" class="svg-icon" style="transform: rotate(180deg);" />
|
||||
</div>
|
||||
<!-- 直方图 -->
|
||||
<div class="tool-item" :title="`${$t('trials:reading:button:histogram')}`" @click.prevent="openHistogram"
|
||||
v-if="this.readingTool === 3">
|
||||
|
|
@ -180,11 +185,6 @@
|
|||
@click.prevent="openFusion">
|
||||
<svg-icon icon-class="fusion" class="svg-icon" />
|
||||
</div>
|
||||
<div :class="['tool-item', activeTool === 'Crosshairs' ? 'tool-item-active' : '']"
|
||||
v-if="readingTool === 2 && isFusion" :title="$t('trials:reading:button:crosshairs')"
|
||||
@click.prevent="setToolActive('Crosshairs')">
|
||||
<svg-icon icon-class="crosshairs" class="svg-icon" />
|
||||
</div>
|
||||
<div v-for="tool in tools" :key="tool.toolName"
|
||||
:class="['tool-item', readingTaskState === 2 ? 'tool-disabled' : '', activeTool === tool.toolName ? 'tool-item-active' : '']"
|
||||
:style="{ cursor: tool.isDisabled ? 'not-allowed' : 'pointer' }"
|
||||
|
|
@ -477,7 +477,7 @@
|
|||
</el-tab-pane>
|
||||
<!-- 其他 -->
|
||||
<el-tab-pane :label="$t('trials:reading:tab:others')" name="3">
|
||||
<Others v-if="personalConfigDialog.activeName === '3'" />
|
||||
<Others v-if="personalConfigDialog.activeName === '3'" :imageToolType="1" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
|
|
@ -577,6 +577,8 @@ import colorMap from './colorMap.vue'
|
|||
import RectangleROITool from './tools/RectangleROITool'
|
||||
import ScaleOverlayTool from './tools/ScaleOverlayTool'
|
||||
import SegmentBidirectionalTool from './tools/SegmentBidirectionalTool'
|
||||
import FusionJumpToPointTool from './tools/FusionJumpToPointTool'
|
||||
import { setPTClinicalDataForInstance, clearPTClinicalDataCache } from '@/utils/ptClinicalDataCache'
|
||||
import FixedRadiusCircleROITool from './tools/FixedRadiusCircleROITool'
|
||||
import uploadDicomAndNonedicom from '@/components/uploadDicomAndNonedicom'
|
||||
import downloadDicomAndNonedicom from '@/components/downloadDicomAndNonedicom'
|
||||
|
|
@ -609,7 +611,6 @@ const {
|
|||
AngleTool,
|
||||
CobbAngleTool,
|
||||
EraserTool,
|
||||
MIPJumpToClickTool,
|
||||
VolumeRotateTool,
|
||||
CrosshairsTool,
|
||||
EllipticalROITool,
|
||||
|
|
@ -816,6 +817,13 @@ export default {
|
|||
segIndex: null,
|
||||
curSegSeries: {},
|
||||
fusionOverlayModality: null,
|
||||
fusionOverlayDefaultUpper: null,
|
||||
fusionOverlayDefaultRange: null,
|
||||
fusionCrosshairStyle: {
|
||||
lineWidth: 2,
|
||||
lineLength: 20,
|
||||
centerHoleSize: 20,
|
||||
},
|
||||
lastUpper: null,
|
||||
hasFusionUpperInitialized: false,
|
||||
timer: {},
|
||||
|
|
@ -1016,6 +1024,15 @@ export default {
|
|||
// this.$refs.surfaceViewport.setSeriesInfo(obj)
|
||||
},
|
||||
async openHistogram() {
|
||||
const renderingEngine = getRenderingEngine(this.renderingEngineId)
|
||||
let viewportId = `${this.viewportKey}-${this.activeViewportIndex}`
|
||||
const viewport = renderingEngine.getViewport(viewportId)
|
||||
let imageIds = viewport.getImageIds(this.$refs[viewportId][0].volumeId)
|
||||
let imageId = imageIds[0]
|
||||
const imagePixelModule = metaData.get('imagePixelModule', imageId);
|
||||
const photometricInterpretation = imagePixelModule?.photometricInterpretation;
|
||||
console.log(photometricInterpretation, 'photometricInterpretation')
|
||||
if (photometricInterpretation && photometricInterpretation !== 'MONOCHROME1' && photometricInterpretation !== 'MONOCHROME2') return this.$confirm(this.$t('trials:histogram:confirm:photometricInterpretationNotSupported'))
|
||||
this.histogramVisible = true
|
||||
this.setToolsPassive()
|
||||
this.$refs.histogram.init()
|
||||
|
|
@ -1043,6 +1060,7 @@ export default {
|
|||
},
|
||||
handleClick(tab, event) {
|
||||
this.formWrapperActiveName = tab.name
|
||||
this.SegmentConfig.InactiveSegmentations.show = true
|
||||
},
|
||||
setMPRInfo(obj) {
|
||||
let { type, key, value } = obj
|
||||
|
|
@ -1153,10 +1171,32 @@ export default {
|
|||
let keySeriesIndex = -1
|
||||
const arr = res1.Result
|
||||
arr.forEach((study, studyIndex) => {
|
||||
// 仅对 PT/PET study 缓存临床参数,供 3D SUV 计算链路覆盖原始 DICOM 元数据
|
||||
const ptClinicalData = {
|
||||
PatientSex: study.PatientSex,
|
||||
PatientWeight: study.PatientWeight,
|
||||
RadionuclideTotalDose: study.RadionuclideTotalDose,
|
||||
RadionuclideHalfLife: study.RadionuclideHalfLife,
|
||||
RadiopharmaceuticalStartTime: study.RadiopharmaceuticalStartTime,
|
||||
AcquisitionTime: study.AcquisitionTime
|
||||
}
|
||||
const isPtStudy = ['PT、CT', 'CT、PT', 'PET-CT'].includes(study.Modalities)
|
||||
const hasPtClinicalData =
|
||||
isPtStudy &&
|
||||
(
|
||||
ptClinicalData.PatientWeight !== null ||
|
||||
ptClinicalData.RadionuclideTotalDose !== null ||
|
||||
ptClinicalData.RadionuclideHalfLife !== null ||
|
||||
ptClinicalData.RadiopharmaceuticalStartTime !== null ||
|
||||
ptClinicalData.AcquisitionTime !== null
|
||||
)
|
||||
study.SeriesList.forEach((series, seriesIndex) => {
|
||||
const imageIds = []
|
||||
const stack = []
|
||||
series.InstanceInfoList.forEach((instance, instanceIndex) => {
|
||||
if (hasPtClinicalData && ['PT', 'PET'].includes(String(series.Modality).toUpperCase())) {
|
||||
setPTClinicalDataForInstance(instance.Id, ptClinicalData)
|
||||
}
|
||||
if (study.IsCriticalSequence) {
|
||||
keyStudyIndex = studyIndex
|
||||
keySeriesIndex = seriesIndex
|
||||
|
|
@ -1491,7 +1531,7 @@ export default {
|
|||
cornerstoneTools.addTool(EllipticalROITool)
|
||||
cornerstoneTools.addTool(AngleTool)
|
||||
cornerstoneTools.addTool(CobbAngleTool)
|
||||
cornerstoneTools.addTool(MIPJumpToClickTool)
|
||||
cornerstoneTools.addTool(FusionJumpToPointTool)
|
||||
cornerstoneTools.addTool(VolumeRotateTool)
|
||||
cornerstoneTools.addTool(CrosshairsTool)
|
||||
cornerstoneTools.addTool(LabelMapEditWithContourTool)
|
||||
|
|
@ -1602,10 +1642,11 @@ export default {
|
|||
getReferenceLineColor: this.setCrosshairsToolLineColor
|
||||
});
|
||||
} else if (toolGroupId === this.fusionToolGroupId) {
|
||||
toolGroup.addTool(CrosshairsTool.toolName, {
|
||||
getReferenceLineColor: this.setCrosshairsToolLineColor,
|
||||
getReferenceLineSlabThicknessControlsOn: (otherViewportId) => otherViewportId !== 'viewport-fusion-3'
|
||||
});
|
||||
// toolGroup.addTool(CrosshairsTool.toolName, {
|
||||
// getReferenceLineColor: this.setFusionCrosshairsToolLineColor,
|
||||
// getReferenceLineSlabThicknessControlsOn: () => false,
|
||||
// minimal: { enabled: true, lineLengthInPx: 10000 }
|
||||
// });
|
||||
} else {
|
||||
toolGroup.addTool(WindowLevelTool.toolName)
|
||||
}
|
||||
|
|
@ -1659,17 +1700,25 @@ export default {
|
|||
})
|
||||
if (viewportId === 'viewport-fusion-3') {
|
||||
toolGroup.addTool(VolumeRotateTool.toolName)
|
||||
toolGroup.addTool(MIPJumpToClickTool.toolName, {
|
||||
targetViewportIds: fusionViewportIds.filter((id) => id !== viewportId)
|
||||
toolGroup.addTool(FusionJumpToPointTool.toolName, {
|
||||
targetViewportIds: ['viewport-fusion-0', 'viewport-fusion-1', 'viewport-fusion-2', 'viewport-fusion-3', 'viewport-fusion-hidden-sag'],
|
||||
useBrightestPoint: true,
|
||||
jumpToTargetViewports: true,
|
||||
dispatchEventName: 'fusion-mip-point-selected',
|
||||
getReferenceLineColor: this.setFusionCrosshairsToolLineColor,
|
||||
style: this.fusionCrosshairStyle,
|
||||
referenceLinesCenterGapRadius: this.fusionCrosshairStyle.centerHoleSize,
|
||||
minimal: {
|
||||
enabled: true,
|
||||
lineLengthInPx: this.fusionCrosshairStyle.lineLength,
|
||||
},
|
||||
})
|
||||
|
||||
// Set the initial state of the tools, here we set one tool active on left click.
|
||||
// This means left click will draw that tool.
|
||||
toolGroup.setToolActive(MIPJumpToClickTool.toolName, {
|
||||
toolGroup.setToolActive(VolumeRotateTool.toolName, {
|
||||
bindings: [
|
||||
{
|
||||
mouseButton: MouseBindings.Primary // Left Click
|
||||
}
|
||||
mouseButton: MouseBindings.Wheel,
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
|
@ -1918,6 +1967,7 @@ export default {
|
|||
const { annotation } = e.detail
|
||||
try {
|
||||
if (!annotation) return
|
||||
if (annotation.metadata.toolName === CrosshairsTool.toolName) return
|
||||
if (this.readingTaskState === 2 && !annotation.data.label) return false
|
||||
if (this.readingTaskState === 2) {
|
||||
const errorMsg = { message: 'annotation Not allowed to operate' }
|
||||
|
|
@ -2063,6 +2113,7 @@ export default {
|
|||
try {
|
||||
// if ( this.resetAnnotation && this.isFusion ) return false
|
||||
if (!annotation) return false
|
||||
if (annotation.metadata.toolName === CrosshairsTool.toolName) return false
|
||||
if (this.readingTaskState === 2 && !annotation.data.label) return false
|
||||
if (this.readingTaskState === 2) {
|
||||
const errorMsg = { message: 'annotation Not allowed to operate' }
|
||||
|
|
@ -2263,6 +2314,22 @@ export default {
|
|||
let index = viewportId.split("-").pop()
|
||||
return colors[colors.length - 1 - Number(index)] || colors[0]
|
||||
},
|
||||
setFusionCrosshairsToolLineColor(viewportId) {
|
||||
let colors = [
|
||||
'#fb628b',
|
||||
'#fb628b',
|
||||
'#fb628b',
|
||||
'#ffd10a',
|
||||
'#b6d634',
|
||||
]
|
||||
|
||||
if (viewportId === 'viewport-fusion-hidden-sag') {
|
||||
return colors[colors.length - 1]
|
||||
} else {
|
||||
let index = viewportId.split("-").pop()
|
||||
return colors[Number(index)] || colors[0]
|
||||
}
|
||||
},
|
||||
getLengthToolTextLines(data, targetId) {
|
||||
const cachedVolumeStats = data.cachedStats[targetId]
|
||||
const { length, unit } = cachedVolumeStats
|
||||
|
|
@ -2538,13 +2605,41 @@ export default {
|
|||
setFusionMipJumpEnabled(enabled) {
|
||||
if (!this.isFusion) return
|
||||
const toolGroup = ToolGroupManager.getToolGroup(this.fusionToolGroupId)
|
||||
if (!toolGroup || !toolGroup.hasTool(MIPJumpToClickTool.toolName)) return
|
||||
if (!toolGroup || !toolGroup.hasTool(FusionJumpToPointTool.toolName)) return
|
||||
if (enabled) {
|
||||
toolGroup.setToolActive(MIPJumpToClickTool.toolName, {
|
||||
toolGroup.setToolActive(FusionJumpToPointTool.toolName, {
|
||||
bindings: [{ mouseButton: MouseBindings.Primary }]
|
||||
})
|
||||
this.dispatchFusionCenterPoint()
|
||||
} else {
|
||||
toolGroup.setToolDisabled(MIPJumpToClickTool.toolName)
|
||||
toolGroup.setToolDisabled(FusionJumpToPointTool.toolName)
|
||||
}
|
||||
},
|
||||
dispatchFusionCenterPoint() {
|
||||
const renderingEngine = getRenderingEngine(renderingEngineId)
|
||||
if (!renderingEngine) return
|
||||
const toolGroup = ToolGroupManager.getToolGroup(this.fusionToolGroupId)
|
||||
const instance = toolGroup?.getToolInstance?.(FusionJumpToPointTool.toolName)
|
||||
if (!instance?.setPoint) return
|
||||
const candidates = ['viewport-fusion-2', 'viewport-fusion-1', 'viewport-fusion-0']
|
||||
for (const viewportId of candidates) {
|
||||
const viewport = renderingEngine.getViewport(viewportId)
|
||||
if (!viewport?.canvasToWorld || !viewport?.element) continue
|
||||
const width = viewport.element.clientWidth
|
||||
const height = viewport.element.clientHeight
|
||||
let worldPoint = null
|
||||
if (width && height) {
|
||||
worldPoint = viewport.canvasToWorld([width / 2, height / 2])
|
||||
}
|
||||
if ((!worldPoint || worldPoint.length < 3) && viewport.getCamera) {
|
||||
worldPoint = viewport.getCamera()?.focalPoint
|
||||
}
|
||||
if (!worldPoint || worldPoint.length < 3) continue
|
||||
instance.setPoint(worldPoint, viewportId, renderingEngine.id, {
|
||||
jumpToTargetViewports: true,
|
||||
dispatchEvent: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
},
|
||||
setFusionMipRotateEnabled(enabled) {
|
||||
|
|
@ -2562,14 +2657,17 @@ export default {
|
|||
// 激活工具
|
||||
setToolActive(toolName) {
|
||||
if (this.histogramVisible) return false
|
||||
if (this.isFusion && toolName === CrosshairsTool.toolName) return false
|
||||
const toolGroupId = this.getActiveToolGroupId()
|
||||
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
|
||||
if (!toolGroup) return
|
||||
if (this.activeTool === toolName) {
|
||||
if (toolName === CrosshairsTool.toolName) {
|
||||
if (toolGroup.hasTool(this.activeTool)) {
|
||||
toolGroup.setToolDisabled(this.activeTool)
|
||||
}
|
||||
this.setFusionMipJumpEnabled(true)
|
||||
this.setFusionMipRotateEnabled(true)
|
||||
// this.setFusionMipRotateEnabled(true)
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
|
|
@ -2577,9 +2675,11 @@ export default {
|
|||
} else {
|
||||
if (this.activeTool) {
|
||||
if (this.activeTool === CrosshairsTool.toolName) {
|
||||
if (toolGroup.hasTool(this.activeTool)) {
|
||||
toolGroup.setToolDisabled(this.activeTool)
|
||||
}
|
||||
this.setFusionMipJumpEnabled(true)
|
||||
this.setFusionMipRotateEnabled(true)
|
||||
// this.setFusionMipRotateEnabled(true)
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
|
|
@ -2589,34 +2689,32 @@ export default {
|
|||
})
|
||||
if (toolName === CrosshairsTool.toolName) {
|
||||
if (this.isFusion) {
|
||||
const instance = toolGroup.getToolInstance?.(CrosshairsTool.toolName)
|
||||
if (instance && !instance.__fusionSameForPatched) {
|
||||
instance.__fusionSameForPatched = true
|
||||
const original = instance._checkIfViewportsRenderingSameScene?.bind(instance)
|
||||
instance._checkIfViewportsRenderingSameScene = (viewport, otherViewport) => {
|
||||
try {
|
||||
const a = viewport?.getFrameOfReferenceUID?.()
|
||||
const b = otherViewport?.getFrameOfReferenceUID?.()
|
||||
if (a && b && a === b) return true
|
||||
} catch (e) { }
|
||||
return original ? original(viewport, otherViewport) : true
|
||||
}
|
||||
}
|
||||
// const instance = toolGroup.getToolInstance?.(CrosshairsTool.toolName)
|
||||
// if (instance && !instance.__fusionSameForPatched) {
|
||||
// instance.__fusionSameForPatched = true
|
||||
// const original = instance._checkIfViewportsRenderingSameScene?.bind(instance)
|
||||
// instance._checkIfViewportsRenderingSameScene = (viewport, otherViewport) => {
|
||||
// try {
|
||||
// const a = viewport?.getFrameOfReferenceUID?.()
|
||||
// const b = otherViewport?.getFrameOfReferenceUID?.()
|
||||
// if (a && b && a === b) return true
|
||||
// } catch (e) { }
|
||||
// return original ? original(viewport, otherViewport) : true
|
||||
// }
|
||||
// }
|
||||
}
|
||||
this.setFusionMipJumpEnabled(false)
|
||||
this.setFusionMipRotateEnabled(false)
|
||||
// this.setFusionMipRotateEnabled(false)
|
||||
}
|
||||
this.activeTool = toolName
|
||||
}
|
||||
},
|
||||
hoverFusionViewport(index) {
|
||||
if (!this.isFusion) return
|
||||
if (this.activeTool === CrosshairsTool.toolName) return
|
||||
const toolGroup = ToolGroupManager.getToolGroup(this.fusionToolGroupId)
|
||||
if (!toolGroup) return
|
||||
|
||||
const isMip = index === 3
|
||||
this.setFusionMipJumpEnabled(isMip)
|
||||
if (isMip) {
|
||||
if (toolGroup.hasTool(StackScrollTool.toolName)) {
|
||||
toolGroup.setToolDisabled(StackScrollTool.toolName)
|
||||
|
|
@ -2650,7 +2748,9 @@ export default {
|
|||
if (!toolGroup) return
|
||||
if (this.activeTool === toolName) {
|
||||
if (toolName === CrosshairsTool.toolName) {
|
||||
if (toolGroup.hasTool(this.activeTool)) {
|
||||
toolGroup.setToolDisabled(this.activeTool)
|
||||
}
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
|
|
@ -2658,7 +2758,9 @@ export default {
|
|||
} else {
|
||||
if (this.activeTool) {
|
||||
if (this.activeTool === CrosshairsTool.toolName) {
|
||||
if (toolGroup.hasTool(this.activeTool)) {
|
||||
toolGroup.setToolDisabled(this.activeTool)
|
||||
}
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
|
|
@ -2683,7 +2785,9 @@ export default {
|
|||
if (!toolGroup) return
|
||||
if (this.activeTool) {
|
||||
if (this.activeTool === CrosshairsTool.toolName) {
|
||||
if (toolGroup.hasTool(this.activeTool)) {
|
||||
toolGroup.setToolDisabled(this.activeTool)
|
||||
}
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
|
|
@ -2703,7 +2807,9 @@ export default {
|
|||
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
|
||||
if (!toolGroup) return
|
||||
if (this.activeTool === CrosshairsTool.toolName) {
|
||||
if (toolGroup.hasTool(this.activeTool)) {
|
||||
toolGroup.setToolDisabled(this.activeTool)
|
||||
}
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
|
|
@ -2733,7 +2839,9 @@ export default {
|
|||
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
|
||||
if (!toolGroup) return
|
||||
if (this.activeTool === CrosshairsTool.toolName) {
|
||||
if (toolGroup.hasTool(this.activeTool)) {
|
||||
toolGroup.setToolDisabled(this.activeTool)
|
||||
}
|
||||
} else {
|
||||
toolGroup.setToolPassive(this.activeTool)
|
||||
}
|
||||
|
|
@ -2795,6 +2903,7 @@ export default {
|
|||
const viewportId = `${this.viewportKey}-${this.activeViewportIndex}`
|
||||
const viewport = renderingEngine.getViewport(viewportId)
|
||||
const type = parseInt(value)
|
||||
if (this.readingTool === 3 || this.isMPR) this.$refs[`${this.viewportKey}-${this.activeViewportIndex}`][0].setFilp(true)
|
||||
// 1:默认值;2:垂直翻转;3:水平翻转;4:左转90度;5:右转90度;
|
||||
if (type === 1) {
|
||||
// viewport.resetCamera()
|
||||
|
|
@ -2821,16 +2930,71 @@ export default {
|
|||
}
|
||||
this.$refs[`${this.viewportKey}-${this.activeViewportIndex}`][0].rotateOrientationMarkers(type)
|
||||
},
|
||||
resetCrosshairsAnnotationsForViewports(viewportIds = []) {
|
||||
if (!viewportIds || viewportIds.length === 0) return
|
||||
const viewportIdSet = new Set(viewportIds)
|
||||
const annotations = cornerstoneTools.annotation.state.getAllAnnotations()
|
||||
annotations.forEach((a) => {
|
||||
if (!a) return
|
||||
if (a.metadata.toolName !== CrosshairsTool.toolName) return
|
||||
const vpId = a.data.viewportId
|
||||
if (!vpId || !viewportIdSet.has(vpId)) return
|
||||
cornerstoneTools.annotation.state.removeAnnotation(a.annotationUID)
|
||||
})
|
||||
},
|
||||
// 重置视口
|
||||
resetViewport() {
|
||||
async resetViewport() {
|
||||
this.setToolsPassive()
|
||||
const renderingEngine = getRenderingEngine(renderingEngineId)
|
||||
if (this.isFusion) {
|
||||
const fusionViewportIds = ['viewport-fusion-0', 'viewport-fusion-1', 'viewport-fusion-2', 'viewport-fusion-3']
|
||||
const fusionAllViewportIds = [...fusionViewportIds, 'viewport-fusion-hidden-sag']
|
||||
for (const id of fusionAllViewportIds) {
|
||||
const viewport = renderingEngine.getViewport(id)
|
||||
if (!viewport) continue
|
||||
const ref = this.$refs[id]?.[0]
|
||||
const index = ref?.series?.SliceIndex
|
||||
if (ref.resetOrientationMarkers) ref.resetOrientationMarkers()
|
||||
viewport.resetCamera({ resetPan: true, resetZoom: true, resetToCenter: true, resetRotation: true })
|
||||
if (viewport.resetSlabThickness) viewport.resetSlabThickness()
|
||||
viewport.resetProperties()
|
||||
if (id === 'viewport-fusion-3') {
|
||||
if (ref.rotateBarLeft || ref.rotateBarLeft === 0) ref.rotateBarLeft = 0
|
||||
if (ref.rotateAngle || ref.rotateAngle === 0) ref.rotateAngle = 0
|
||||
const ptSeries = ref?.series
|
||||
if (ptSeries && ptSeries.SeriesInstanceUid) {
|
||||
await ref.setSeriesInfo({ data: ptSeries }, false, { isMip: true, colorMap: false })
|
||||
}
|
||||
} else {
|
||||
viewport.render()
|
||||
}
|
||||
if ((index || index === 0) && ref.setFullScreen) {
|
||||
ref.setFullScreen(index)
|
||||
}
|
||||
}
|
||||
this.resetCrosshairsAnnotationsForViewports(fusionAllViewportIds)
|
||||
renderingEngine.render()
|
||||
this.dispatchFusionCenterPoint()
|
||||
if (this.fusionOverlayModality === 'NM' && Number.isFinite(this.fusionOverlayDefaultUpper) && Number.isFinite(this.fusionOverlayDefaultRange)) {
|
||||
this.lastUpper = null
|
||||
this.hasFusionUpperInitialized = false
|
||||
if (this.$refs.colorMap) {
|
||||
this.$refs.colorMap.range = this.fusionOverlayDefaultRange
|
||||
this.$refs.colorMap.upper = this.fusionOverlayDefaultUpper
|
||||
this.$refs.colorMap.upperRangeChange(this.fusionOverlayDefaultRange)
|
||||
this.$refs.colorMap.changeVoi(this.fusionOverlayDefaultUpper)
|
||||
}
|
||||
this.voiChange(this.fusionOverlayDefaultUpper)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const viewportId = `${this.viewportKey}-${this.activeViewportIndex}`
|
||||
const viewport = renderingEngine.getViewport(viewportId)
|
||||
this.$refs[`${this.viewportKey}-${this.activeViewportIndex}`][0].resetOrientationMarkers()
|
||||
let index = this.$refs[`${this.viewportKey}-${this.activeViewportIndex}`][0].series.SliceIndex
|
||||
if (this.readingTool !== 3) {
|
||||
viewport.resetCamera({ resetPan: true, resetZoom: true, resetToCenter: true })
|
||||
viewport.resetCamera({ resetPan: true, resetZoom: true, resetToCenter: true, resetRotation: true })
|
||||
}
|
||||
viewport.resetProperties()
|
||||
if (this.isMPR) {
|
||||
|
|
@ -2844,7 +3008,7 @@ export default {
|
|||
viewport.setProperties({ voiRange: { upper: 5, lower: 0 } })
|
||||
}
|
||||
viewport.render()
|
||||
renderingEngine.render()
|
||||
// renderingEngine.render()
|
||||
if (this.readingTool === 3) {
|
||||
DicomEvent.$emit('isloaded', { isChange: false, viewportId })
|
||||
}
|
||||
|
|
@ -3020,7 +3184,7 @@ export default {
|
|||
// 获取热键配置信息
|
||||
async getHotKeys() {
|
||||
try {
|
||||
const res = await getDoctorShortcutKey({ imageToolType: 0 })
|
||||
const res = await getDoctorShortcutKey({ imageToolType: 1 })
|
||||
res.Result.map(item => {
|
||||
this.hotKeyList.push({ id: item.Id, altKey: item.AltKey, ctrlKey: item.CtrlKey, shiftKey: item.ShiftKey, metaKey: item.MetaKey, key: item.Keyboardkey, code: item.Code, text: item.Text, shortcutKeyEnum: item.ShortcutKeyEnum })
|
||||
})
|
||||
|
|
@ -3973,7 +4137,10 @@ export default {
|
|||
},
|
||||
async openMPRViewport(data = null) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
this.setToolsPassive()
|
||||
if (this.isMPR) {
|
||||
this.activeSeries(this.$refs[`viewport-MPR-0`][0].series)
|
||||
resolve(false)
|
||||
if (!data) return resolve(false)
|
||||
let viewportSeries = this.$refs[`viewport-MPR-0`][0].series
|
||||
if (data && viewportSeries.SeriesInstanceUid === data.SeriesInstanceUid) return resolve(true)
|
||||
|
|
@ -4005,9 +4172,12 @@ export default {
|
|||
await this.getVolume(series)
|
||||
this.loading = false
|
||||
this.loadingText = null
|
||||
delete series.orientation
|
||||
delete series.isLocation
|
||||
this.$refs[`viewport-MPR-0`][0].setSeriesInfo(Object.assign({ orientation: 'AXIAL', isLocation: data && this.activeViewportIndex === 0 }, series))
|
||||
this.$refs[`viewport-MPR-1`][0].setSeriesInfo(Object.assign({ orientation: 'SAGITTAL', isLocation: data && this.activeViewportIndex === 1 }, series))
|
||||
this.$refs[`viewport-MPR-2`][0].setSeriesInfo(Object.assign({ orientation: 'CORONAL', isLocation: data && this.activeViewportIndex === 2 }, series))
|
||||
this.setToolActive('Crosshairs')
|
||||
resolve(false)
|
||||
})
|
||||
|
||||
|
|
@ -4041,6 +4211,9 @@ export default {
|
|||
this.$refs[`viewport-1`][0].setSeriesInfo(pt)
|
||||
this.$refs[`viewport-2`][0].setSeriesInfo(pt)
|
||||
this.$refs[`viewport-3`][0].setSeriesInfo(pt)
|
||||
this.$nextTick(() => {
|
||||
this.setFusionMipJumpEnabled(true)
|
||||
})
|
||||
// this.resetAnnotation = false
|
||||
return true
|
||||
}
|
||||
|
|
@ -4086,12 +4259,20 @@ export default {
|
|||
const nmMax = Number(rawWidth)
|
||||
if (Number.isFinite(nmMax) && nmMax > 0) {
|
||||
const halfMax = Math.round(nmMax * 0.5)
|
||||
this.fusionOverlayDefaultRange = Math.round(nmMax)
|
||||
this.fusionOverlayDefaultUpper = halfMax
|
||||
this.lastUpper = null
|
||||
this.hasFusionUpperInitialized = false
|
||||
this.$refs.colorMap.range = Math.round(nmMax)
|
||||
this.$refs.colorMap.upper = halfMax
|
||||
this.$refs.colorMap.upperRangeChange(Math.round(nmMax))
|
||||
this.voiChange(halfMax)
|
||||
}
|
||||
} else {
|
||||
this.fusionOverlayDefaultRange = null
|
||||
this.fusionOverlayDefaultUpper = null
|
||||
}
|
||||
this.setFusionMipJumpEnabled(true)
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
|
|
@ -4293,6 +4474,7 @@ export default {
|
|||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearPTClinicalDataCache()
|
||||
DicomEvent.$off('isCanActiveNoneDicomTool')
|
||||
DicomEvent.$off('removeNoneDicomMeasureData')
|
||||
DicomEvent.$off('addNoneDicomMeasureData')
|
||||
|
|
|
|||
|
|
@ -918,7 +918,7 @@ export default {
|
|||
// 设置当前任务阅片状态为已读
|
||||
this.readingTaskState = 2
|
||||
this.$emit('setReadingTaskState', 2)
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({imageToolType: 1})
|
||||
var isAutoTask = res.Result.AutoCutNextTask
|
||||
if (isAutoTask) {
|
||||
window.location.reload()
|
||||
|
|
@ -961,9 +961,9 @@ export default {
|
|||
this.$router.currentRoute.query.isReadingTaskViewInOrder
|
||||
var criterionType = this.$router.currentRoute.query.criterionType
|
||||
var readingTool = this.$router.currentRoute.query.readingTool
|
||||
var path = `/readingDicoms?TrialReadingCriterionId=${trialReadingCriterionId}&trialId=${trialId}&subjectCode=${subjectCode}&subjectId=${subjectId}&visitTaskId=${task.VisitTaskId}&isReadingTaskViewInOrder=${isReadingTaskViewInOrder}&criterionType=${criterionType}&readingTool=${readingTool}&TokenKey=${token}`
|
||||
var path = `/readingDicoms?TrialReadingCriterionId=${trialReadingCriterionId}&trialId=${trialId}&subjectCode=${subjectCode}&subjectId=${subjectId}&visitTaskId=${task.VisitTaskId}&isReadingTaskViewInOrder=${isReadingTaskViewInOrder}&criterionType=${criterionType}&readingTool=${readingTool}&pageType=History&TokenKey=${token}`
|
||||
const routeData = this.$router.resolve({ path })
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({imageToolType: 1})
|
||||
let IsDoubleScreen = false
|
||||
if (res.IsSuccess) {
|
||||
IsDoubleScreen = res.Result.IsDoubleScreen
|
||||
|
|
|
|||
|
|
@ -146,7 +146,6 @@ export default {
|
|||
},
|
||||
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') {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<div class="Segmentations" v-loading="loading">
|
||||
<h3 style="color: #fff;margin: 0;padding: 15px 10px;">
|
||||
<span>{{ series.TaskInfo.SubjectCode }} </span>
|
||||
<span style="margin-left:5px;">{{ series.TaskInfo.TaskBlindName }}</span>
|
||||
</h3>
|
||||
<el-collapse v-model="activeNames">
|
||||
<el-collapse-item name="tools">
|
||||
<template slot="title">
|
||||
|
|
@ -8,24 +12,24 @@
|
|||
<div class="tool-frame">
|
||||
<div :title="$t('trials:Segmentations:tools:contour')"
|
||||
:class="['tool-item', activeTool === 'LabelMapEditWithContour' && segmentList.length > 0 ? 'tool-item-active' : '']"
|
||||
:style="{ cursor: segmentList.length <= 0 || (curSegment && curSegment.lock) || ['viewport-MPR-1', 'viewport-MPR-2'].includes(`${viewportKey}-${activeViewportIndex}`) ? 'not-allowed' : 'pointer' }"
|
||||
:style="{ cursor: isMPR || segmentList.length <= 0 || (curSegment && curSegment.lock) || ['viewport-MPR-1', 'viewport-MPR-2'].includes(`${viewportKey}-${activeViewportIndex}`) ? 'not-allowed' : 'pointer' }"
|
||||
@click.prevent="setToolActive('LabelMapEditWithContour')">
|
||||
<svg-icon icon-class="contour" class="svg-icon" />
|
||||
</div>
|
||||
<div :title="$t('trials:Segmentations:tools:thresholecircle')"
|
||||
:class="['tool-item', ThresholdTools.includes(activeTool) && segmentList.length > 0 ? 'tool-item-active' : '']"
|
||||
:style="{ cursor: segmentList.length <= 0 || (curSegment && curSegment.lock) || ['viewport-MPR-1', 'viewport-MPR-2'].includes(`${viewportKey}-${activeViewportIndex}`) ? 'not-allowed' : 'pointer' }"
|
||||
:style="{ cursor: isMPR || segmentList.length <= 0 || (curSegment && curSegment.lock) || ['viewport-MPR-1', 'viewport-MPR-2'].includes(`${viewportKey}-${activeViewportIndex}`) ? 'not-allowed' : 'pointer' }"
|
||||
@click.prevent="initThreshold">
|
||||
<svg-icon icon-class="thresholecircle" class="svg-icon" />
|
||||
</div>
|
||||
<div :title="$t('trials:Segmentations:tools:circularbrush')"
|
||||
:class="['tool-item', activeTool === 'CircularBrush' && segmentList.length > 0 ? 'tool-item-active' : '']"
|
||||
:style="{ cursor: segmentList.length <= 0 || (curSegment && curSegment.lock) || ['viewport-MPR-1', 'viewport-MPR-2'].includes(`${viewportKey}-${activeViewportIndex}`) ? 'not-allowed' : 'pointer' }"
|
||||
:style="{ cursor: isMPR || segmentList.length <= 0 || (curSegment && curSegment.lock) || ['viewport-MPR-1', 'viewport-MPR-2'].includes(`${viewportKey}-${activeViewportIndex}`) ? 'not-allowed' : 'pointer' }"
|
||||
@click.prevent="setToolActive('CircularBrush')">
|
||||
<svg-icon icon-class="circularbrush" class="svg-icon" />
|
||||
</div>
|
||||
<div :class="['tool-item', activeTool === 'CircularEraser' && segmentList.length > 0 ? 'tool-item-active' : '']"
|
||||
:style="{ cursor: segmentList.length <= 0 || (curSegment && curSegment.lock) || ['viewport-MPR-1', 'viewport-MPR-2'].includes(`${viewportKey}-${activeViewportIndex}`) ? 'not-allowed' : 'pointer' }"
|
||||
:style="{ cursor: isMPR || segmentList.length <= 0 || (curSegment && curSegment.lock) || ['viewport-MPR-1', 'viewport-MPR-2'].includes(`${viewportKey}-${activeViewportIndex}`) ? 'not-allowed' : 'pointer' }"
|
||||
:title="$t('trials:Segmentations:tools:Eraser')"
|
||||
@click.prevent="setToolActive('CircularEraser')">
|
||||
<svg-icon icon-class="clear" class="svg-icon" />
|
||||
|
|
@ -202,7 +206,9 @@
|
|||
<template v-if="item.stats">
|
||||
<div v-for="k in statsKey" :key="k" class="statsBox">
|
||||
<span>{{ k }}</span>
|
||||
<span v-if="item.stats[k]">{{ Number(item.stats[k].value).toFixed(2)
|
||||
<span v-if="item.stats[k]">{{ JSON.stringify(item.stats[k].value) !== 'null'
|
||||
?
|
||||
Number(item.stats[k].value).toFixed(2) : null
|
||||
}}<i>{{ item.stats[k].unit }}</i></span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -402,13 +408,12 @@ export default {
|
|||
this.statsKey = getCustomizeStandardsSegmentDicomTools('Labelmap')[0].props.filter(item => item !== 'width' && item !== 'length')
|
||||
// console.log(segmentation, 'segmentation')
|
||||
// console.log(annotation, 'annotation')
|
||||
console.log(cache, 'cache')
|
||||
// console.log(cache, 'cache')
|
||||
eventTarget.addEventListener(
|
||||
'CORNERSTONE_TOOLS_SEGMENTATION_DATA_MODIFIED',
|
||||
this.segmentationModifiedCallback
|
||||
);
|
||||
DicomEvent.$on('activeSeries', (series) => {
|
||||
console.log(series, 'series')
|
||||
let { TaskInfo = {}, Id } = series
|
||||
if (this.isMPR) return false
|
||||
if (Id === this.series.Id && TaskInfo.VisitTaskId === this.visitInfo.VisitTaskId) return false
|
||||
|
|
@ -494,11 +499,11 @@ export default {
|
|||
this.popoverId = `popover-${item.segmentationId}_${item.segmentIndex}`
|
||||
},
|
||||
initThreshold() {
|
||||
if (this.isMPR) return false
|
||||
if (!this.ThresholdTools.includes(this.activeTool)) {
|
||||
this.setToolActive(this.ThresholdTools[0])
|
||||
this.thresholdType = this.ThresholdTools[0]
|
||||
}
|
||||
|
||||
},
|
||||
createSegmentConfiguration(segmentIndex, segmentationId, otherSegments) {
|
||||
const containedSegmentIndices = otherSegments
|
||||
|
|
@ -618,6 +623,7 @@ export default {
|
|||
// if (!this.series.TaskInfo || this.series.TaskInfo.VisitTaskId !== this.visitInfo.VisitTaskId) return false
|
||||
if (this.segmentList.length <= 0) return false
|
||||
if (this.curSegment.lock) return false
|
||||
if (this.isMPR) return false
|
||||
if (this.histogramVisible && !this.ThresholdTools.includes(toolName)) return false
|
||||
if (['viewport-MPR-1', 'viewport-MPR-2'].includes(`${this.viewportKey}-${this.activeViewportIndex}`)) return false
|
||||
const toolGroupId = this.isMPR ? this.volumeToolGroupId : `${this.viewportKey}-${this.activeViewportIndex}`
|
||||
|
|
@ -880,6 +886,7 @@ export default {
|
|||
},
|
||||
// 删除分割分组
|
||||
async delSegmentGroup() {
|
||||
this.popoverId = null
|
||||
let confirm = await this.$confirm(this.$t('trials:reading:Segmentations:confirm:delSegmentions'))
|
||||
if (!confirm) return false
|
||||
let res = await this.deleteSegmentation(this.segmentationId)
|
||||
|
|
@ -897,6 +904,8 @@ export default {
|
|||
annotations.forEach(item => {
|
||||
annotation.state.removeAnnotation(item.annotationUID)
|
||||
})
|
||||
let f = this.segmentList.some(item => item.segUrl)
|
||||
DicomEvent.$emit("IsBeSegment", { StudyId: this.series.StudyId, Id: this.series.Id, IsBeSegment: f })
|
||||
if (this.segmentList.length > 0) {
|
||||
this.segmentationId = this.segmentList[0].segmentationId;
|
||||
this.selectSegmentGroup()
|
||||
|
|
@ -1027,6 +1036,7 @@ export default {
|
|||
},
|
||||
// 导出SEG
|
||||
exportSegmentGroup() {
|
||||
this.popoverVisible = false
|
||||
let group = this.segmentList.find(item => item.segmentationId === this.segmentationId)
|
||||
this.exportSegmentation(this.segmentationId, group, true)
|
||||
},
|
||||
|
|
@ -1471,6 +1481,7 @@ export default {
|
|||
}
|
||||
this.$emit("update:globalLoading", true)
|
||||
this.$emit("update:loadingText", this.$t("segment:loadingText:saveSegmentation"))
|
||||
let IsBeSegment = false
|
||||
for (let i = 0; i < segmentList.length; i++) {
|
||||
let segmentGroup = segmentList[i]
|
||||
// 将所有分割进行锁定
|
||||
|
|
@ -1487,6 +1498,7 @@ export default {
|
|||
}/${this.series.Id}/${segmentGroup.name}.dcm`
|
||||
const result = await this.OSSclient.put(path, blob)
|
||||
segmentGroup.segUrl = this.$getObjectName(result.url)
|
||||
DicomEvent.$emit("IsBeSegment", { StudyId: this.series.StudyId, Id: this.series.Id, IsBeSegment: true })
|
||||
} else {
|
||||
segmentGroup.segUrl = null
|
||||
}
|
||||
|
|
@ -1498,6 +1510,10 @@ export default {
|
|||
this.syncBindingAnswer(segmentList)
|
||||
}
|
||||
}
|
||||
if (!IsBeSegment) {
|
||||
let f = this.segmentList.some(item => item.segUrl)
|
||||
DicomEvent.$emit("IsBeSegment", { StudyId: this.series.StudyId, Id: this.series.Id, IsBeSegment: f })
|
||||
}
|
||||
this.$emit("update:globalLoading", false)
|
||||
} catch (err) {
|
||||
this.loading = false
|
||||
|
|
@ -1535,7 +1551,7 @@ export default {
|
|||
segmentationId: list[0].segmentationId,
|
||||
segmentIndices: list.map(item => item.segmentIndex),
|
||||
});
|
||||
console.log(bidirectionalData)
|
||||
// console.log(bidirectionalData)
|
||||
if (bidirectionalData.length <= 0) {
|
||||
list.forEach(item => {
|
||||
let annotations = annotation.state.getAllAnnotations().filter(i => i.metadata.segmentationId === item.segmentationId && i.metadata.segmentIndex === item.segmentIndex);
|
||||
|
|
|
|||
|
|
@ -74,8 +74,39 @@
|
|||
#{{ series.SeriesNumber }}
|
||||
|
||||
</div>
|
||||
<div v-if="series.Description" class="text-desc" :title="series.Description">
|
||||
<div v-if="series.Description" class="text-desc" :title="series.Description" style="position: relative;">
|
||||
{{ series.Description }}
|
||||
<div class="patient-info" style="position: absolute;right: 0;top: 0;" v-if="['PT','PET'].includes(series.Modality)">
|
||||
<el-popover placement="right" trigger="hover" popper-class="patient-info-popper">
|
||||
<h4>{{ $t('trials:ptData:title') }}</h4>
|
||||
<div class="patient-info-row">
|
||||
<label>{{ $t('trials:ptData:label:patientSex') }}</label>
|
||||
<span>{{ study.PatientSex }}</span>
|
||||
</div>
|
||||
<div class="patient-info-row">
|
||||
<label>{{ $t('trials:ptData:label:patientWeight') }}</label>
|
||||
<span>{{ study.PatientWeight }}</span>
|
||||
</div>
|
||||
<div class="patient-info-row">
|
||||
<label>{{ $t('trials:ptData:label:totalDose') }}</label>
|
||||
<span>{{ study.RadionuclideTotalDose }}</span>
|
||||
</div>
|
||||
<div class="patient-info-row">
|
||||
<label>{{ $t('trials:ptData:label:halfLife') }}</label>
|
||||
<span>{{ study.RadionuclideHalfLife }}</span>
|
||||
</div>
|
||||
<div class="patient-info-row">
|
||||
<label>{{ $t('trials:ptData:label:injectTime') }}</label>
|
||||
<span>{{ study.RadiopharmaceuticalStartTime }}</span>
|
||||
</div>
|
||||
<div class="patient-info-row">
|
||||
<label>{{ $t('trials:ptData:label:acquisitionTime') }}</label>
|
||||
<span>{{ study.AcquisitionTime }}</span>
|
||||
</div>
|
||||
<i slot="reference" class="el-icon-document"
|
||||
style="font-size: 15px;cursor: pointer;color: #f5f7fa;" />
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="series.SliceThickness && !study.IsCriticalSequence" class="text-desc">
|
||||
T: {{ parseFloat(series.SliceThickness).toFixed(digitPlaces) }}
|
||||
|
|
@ -89,8 +120,8 @@
|
|||
}} 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.IsBeSegment || series.IsBeMark || markedSeriesIds.includes(series.Id)"
|
||||
class="el-icon-star-on" style="font-size: 12px;color: #ff5722;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -151,6 +182,19 @@ export default {
|
|||
this.$nextTick(() => {
|
||||
this.activeStudy(this.studyList[0].StudyId)
|
||||
})
|
||||
DicomEvent.$on('IsBeSegment', (obj) => {
|
||||
this.studyList.some(study => {
|
||||
if (study.StudyId === obj.StudyId) {
|
||||
study.SeriesList.some(series => {
|
||||
if (series.Id === obj.Id) {
|
||||
series.IsBeSegment = obj.IsBeSegment
|
||||
}
|
||||
return series.Id === obj.Id
|
||||
})
|
||||
}
|
||||
return study.StudyId === obj.StudyId
|
||||
})
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
activeSeries(series, seriesIndex, studyIndex) {
|
||||
|
|
@ -210,10 +254,56 @@ export default {
|
|||
const seriesIndex = seriseList[newIndex].SeriesIndex
|
||||
this.setSeriesActive(studyIndex, seriesIndex)
|
||||
this.activeSeries(seriseList[newIndex], seriesIndex, studyIndex)
|
||||
},
|
||||
showPatientInfo(study) {
|
||||
console.log(study)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.patient-info-popper {
|
||||
font-size: 12px;
|
||||
color: #ddd;
|
||||
background-color: #2f2f2f;
|
||||
border-color: #5a5a5a;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.patient-info-popper[x-placement^='right'] .popper__arrow {
|
||||
border-right-color: #5a5a5a;
|
||||
}
|
||||
|
||||
.patient-info-popper[x-placement^='right'] .popper__arrow::after {
|
||||
border-right-color: #2f2f2f;
|
||||
}
|
||||
|
||||
.patient-info-popper .patient-info-row {
|
||||
display: grid;
|
||||
grid-template-columns: 100px minmax(0, 1fr);
|
||||
column-gap: 12px;
|
||||
align-items: center;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.patient-info-popper .patient-info-row + .patient-info-row {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.patient-info-popper label {
|
||||
color: #bbb;
|
||||
// font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.patient-info-popper span {
|
||||
text-align: left;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
.study-wrapper {
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -203,7 +203,8 @@ export default {
|
|||
rotateAngle: 0,
|
||||
rotateBarLeft: 0,
|
||||
loading: false,
|
||||
toggleClipPlayTimer: null
|
||||
toggleClipPlayTimer: null,
|
||||
isFlip: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -382,10 +383,15 @@ export default {
|
|||
|
||||
return 'unknown';
|
||||
},
|
||||
setFilp(f = false) {
|
||||
this.isFlip = f
|
||||
},
|
||||
stackNewImage(e) {
|
||||
if (this.isFlip) return this.isFlip = false
|
||||
const { detail } = e
|
||||
delete this.series.segment
|
||||
this.series.SliceIndex = detail.imageIndex
|
||||
console.log(detail.imageIndex, 'idenx')
|
||||
this.sliderInfo.height = detail.imageIndex * 100 / detail.numberOfSlices
|
||||
const renderingEngine = getRenderingEngine(this.renderingEngineId)
|
||||
const viewport = renderingEngine.getViewport(this.viewportId)
|
||||
|
|
@ -441,7 +447,7 @@ export default {
|
|||
)
|
||||
csUtils.jumpToSlice(viewport.element, { imageIndex: index })
|
||||
viewport.render()
|
||||
})
|
||||
}, 100)
|
||||
},
|
||||
voiModified(e) {
|
||||
const renderingEngine = getRenderingEngine(this.renderingEngineId)
|
||||
|
|
@ -674,7 +680,6 @@ export default {
|
|||
setTimeout(() => { csUtils.jumpToSlice(viewport.element, { imageIndex: data.SliceIndex }); })
|
||||
}
|
||||
})
|
||||
res.volume.dimensionGroupNumber = 2;
|
||||
viewport.render()
|
||||
if (this.series.Modality === 'PT' || this.series.Modality === 'NM') {
|
||||
setTimeout(() => {
|
||||
|
|
@ -687,7 +692,6 @@ export default {
|
|||
}
|
||||
await renderSegmentation(this.series, this.series.TaskInfo, this.viewportId, this.SegmentConfig, this.renderingEngineId, data.segment, this.actionConfiguration)
|
||||
DicomEvent.$emit('SegmentationLoading', this.viewportId)
|
||||
console.log(data.segment, 'data.segment')
|
||||
if (data.segment) return false
|
||||
if (this.series.hasOwnProperty('curIndex')) return this.setFullScreen(this.series.curIndex)
|
||||
this.setFullScreen(Math.ceil((res.volume._imageIds.length - 1) / 2) - 1)
|
||||
|
|
|
|||
|
|
@ -519,7 +519,7 @@ export default {
|
|||
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"))
|
||||
if (!obj.stats || !obj.stats[imageToolAttribute] || obj.stats[imageToolAttribute].value === null) return this.$confirm(this.$t("segment:error:notValue"))
|
||||
answer = obj.stats[imageToolAttribute] ? Number((obj.stats[imageToolAttribute]).value).toFixed(this.digitPlaces) : ''
|
||||
}
|
||||
let o = {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
</el-button>
|
||||
<el-button v-if="readingTaskState < 2" type="primary" size="small" @click="getReportInfo">
|
||||
{{
|
||||
<<<<<<< HEAD
|
||||
$t('trials:readingReport:button:refresh') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
|
|
@ -24,10 +25,22 @@
|
|||
<el-button v-if="readingTaskState < 2" type="primary" size="small" @click="handleConfirm">
|
||||
{{
|
||||
$t('common:button:submit') }}
|
||||
=======
|
||||
$t('trials:readingReport:button:refresh') }}
|
||||
</el-button>
|
||||
<el-button v-if="readingTaskState < 2" type="primary" size="small" @click="handleSave(true)">
|
||||
{{
|
||||
$t('common:button:save') }}
|
||||
</el-button>
|
||||
<el-button v-if="readingTaskState < 2" type="primary" size="small" @click="handleConfirm">
|
||||
{{
|
||||
$t('common:button:submit') }}
|
||||
>>>>>>> main
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex: 1">
|
||||
<<<<<<< HEAD
|
||||
<el-table
|
||||
v-if="taskQuestions.length > 0"
|
||||
ref="reportList"
|
||||
|
|
@ -40,11 +53,20 @@
|
|||
:tree-props="{ children: 'Childrens', hasChildren: 'hasChildren' }"
|
||||
size="mini"
|
||||
>
|
||||
=======
|
||||
<el-table v-if="taskQuestions.length > 0" ref="reportList" v-adaptive="{ bottomOffset: 0 }"
|
||||
:data="taskQuestions" row-key="Id" border default-expand-all height="100"
|
||||
:tree-props="{ children: 'Childrens', hasChildren: 'hasChildren' }" size="mini">
|
||||
>>>>>>> main
|
||||
<el-table-column prop label show-overflow-tooltip width="350px">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.QuestionName">
|
||||
{{ scope.row.BlindName ? scope.row.QuestionName :
|
||||
<<<<<<< HEAD
|
||||
scope.row.QuestionName }}
|
||||
=======
|
||||
scope.row.QuestionName }}
|
||||
>>>>>>> main
|
||||
<svg-icon
|
||||
v-if="scope.row.ShowChartTypeEnum > 0 || (scope.row.LesionType === 0 && scope.row.ReportLayType === 1)"
|
||||
icon-class="readingChart"
|
||||
|
|
@ -58,13 +80,9 @@
|
|||
ReportChartTypeEnum: scope.row.LesionType === 0 && scope.row.ReportLayType === 1 ? 0 : null,
|
||||
QuestionName: scope.row.QuestionName
|
||||
}
|
||||
})"
|
||||
/>
|
||||
})" />
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
style="font-weight: bold;font-size: 16px;color: #f44336;"
|
||||
>{{ scope.row.GroupName }}</span>
|
||||
<span v-else style="font-weight: bold;font-size: 16px;color: #f44336;">{{ scope.row.GroupName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
|
|
@ -100,16 +118,13 @@
|
|||
size="mini"
|
||||
/>
|
||||
<span
|
||||
v-else-if="questionForm[scope.row.QuestionId] instanceof Array && (scope.row.Type === 'input' || scope.row.Type === 'textarea')"
|
||||
>{{ questionForm[scope.row.QuestionId][scope.row.xfIndex][scope.row.TableQuestionId] }}</span>
|
||||
v-else-if="questionForm[scope.row.QuestionId] instanceof Array && (scope.row.Type === 'input' || scope.row.Type === 'textarea')">{{
|
||||
questionForm[scope.row.QuestionId][scope.row.xfIndex][scope.row.TableQuestionId] }}</span>
|
||||
<el-input
|
||||
v-else-if="(scope.row.Type === 'input' || scope.row.Type === 'textarea') && !scope.row.IsShowInDicom && ((task.IsBaseLine && scope.row.LimitEdit === 1) || (!task.IsBaseLine && scope.row.LimitEdit === 2) || scope.row.LimitEdit === 0)"
|
||||
v-model="questionForm[scope.row.QuestionId]"
|
||||
size="mini"
|
||||
/>
|
||||
<span
|
||||
v-else-if="scope.row.Type === 'input' || scope.row.Type === 'textarea'"
|
||||
>{{ questionForm[scope.row.QuestionId] }}</span>
|
||||
v-model="questionForm[scope.row.QuestionId]" size="mini" />
|
||||
<span v-else-if="scope.row.Type === 'input' || scope.row.Type === 'textarea'">{{
|
||||
questionForm[scope.row.QuestionId] }}</span>
|
||||
<el-select
|
||||
v-else-if="questionForm[scope.row.QuestionId] instanceof Array && (scope.row.Type === 'select' || scope.row.Type === 'radio') && !scope.row.IsShowInDicom && ((task.IsBaseLine && scope.row.LimitEdit === 1) || (!task.IsBaseLine && scope.row.LimitEdit === 2) || scope.row.LimitEdit === 0)"
|
||||
v-model="questionForm[scope.row.QuestionId][scope.row.xfIndex][scope.row.TableQuestionId]"
|
||||
|
|
@ -165,8 +180,7 @@
|
|||
:disabled="scope.row.DataSource === 1"
|
||||
size="mini"
|
||||
@blur="limitBlur(questionForm[scope.row.QuestionId][scope.row.xfIndex], scope.row.TableQuestionId, scope.row.ValueType)"
|
||||
@focus="() => { questionId = scope.row.QuestionId }"
|
||||
>
|
||||
@focus="() => { questionId = scope.row.QuestionId }">
|
||||
<template v-if="scope.row.Unit !== 0" slot="append">
|
||||
{{ scope.row.Unit !== 4 ? $fd('ValueUnit',
|
||||
scope.row.Unit) : scope.row.CustomUnit }}
|
||||
|
|
@ -209,8 +223,7 @@
|
|||
:disabled="scope.row.DataSource === 1"
|
||||
size="mini"
|
||||
@blur="limitBlur(questionForm, scope.row.QuestionId, scope.row.ValueType)"
|
||||
@focus="() => { questionId = scope.row.QuestionId }"
|
||||
>
|
||||
@focus="() => { questionId = scope.row.QuestionId }">
|
||||
<template v-if="scope.row.Unit !== 0" slot="append">
|
||||
{{ scope.row.Unit !== 4 ? $fd('ValueUnit',
|
||||
scope.row.Unit) : scope.row.CustomUnit }}
|
||||
|
|
@ -237,19 +250,11 @@
|
|||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
v-else-if="scope.row.Type === 'upload' && scope.row.Answers[task.VisitTaskId]"
|
||||
>
|
||||
<span
|
||||
v-for="(url, index) in scope.row.Answers[task.VisitTaskId].split('|')"
|
||||
:key="url"
|
||||
style="margin-left: 5px;"
|
||||
>
|
||||
<el-button
|
||||
v-if="scope.row.Answers[task.VisitTaskId]"
|
||||
type="text"
|
||||
@click="preview(url)"
|
||||
>{{ `${$t('trials:noneDicom:title:attachment')}${index + 1}` }}</el-button>
|
||||
<template v-else-if="scope.row.Type === 'upload' && scope.row.Answers[task.VisitTaskId]">
|
||||
<span v-for="(url, index) in scope.row.Answers[task.VisitTaskId].split('|')" :key="url"
|
||||
style="margin-left: 5px;">
|
||||
<el-button v-if="scope.row.Answers[task.VisitTaskId]" type="text" @click="preview(url)">{{
|
||||
`${$t('trials:noneDicom:title:attachment')}${index + 1}` }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="scope.row.DictionaryCode">
|
||||
|
|
@ -279,9 +284,8 @@
|
|||
{{ isNaN(parseInt(scope.row.Answers[task.VisitTaskId])) ?
|
||||
scope.row.Answers[task.VisitTaskId] : `${scope.row.Answers[task.VisitTaskId]} %` }}
|
||||
</template>
|
||||
<template
|
||||
v-else-if="scope.row.Answers && scope.row.Answers.hasOwnProperty(task.VisitTaskId)"
|
||||
>{{ scope.row.Answers[task.VisitTaskId] }}</template>
|
||||
<template v-else-if="scope.row.Answers && scope.row.Answers.hasOwnProperty(task.VisitTaskId)">{{
|
||||
scope.row.Answers[task.VisitTaskId] }}</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
|
@ -310,9 +314,7 @@
|
|||
ref="picture_perview_customizeReportPage"
|
||||
style="margin: 0 10px"
|
||||
v-if="currentType && ['png', 'jpg', 'jpeg'].includes(currentType.toLowerCase())"
|
||||
:images="[`${OSSclientConfig.basePath}${currentPath}`]"
|
||||
:options="viewerOptions"
|
||||
>
|
||||
:images="[`${OSSclientConfig.basePath}${currentPath}`]" :options="viewerOptions">
|
||||
<img v-show="false" :src="`${OSSclientConfig.basePath}${currentPath}`" alt="Image" />
|
||||
</viewer>
|
||||
<readingChart ref="readingChart_report" />
|
||||
|
|
@ -987,7 +989,7 @@ export default {
|
|||
this.readingTaskState = 2
|
||||
this.$emit('setReadingTaskState', 2)
|
||||
window.opener.postMessage('refreshTaskList', window.location)
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({ imageToolType: 1 })
|
||||
const isAutoTask = res.Result.AutoCutNextTask
|
||||
if (isAutoTask) {
|
||||
window.location.reload()
|
||||
|
|
@ -1029,9 +1031,9 @@ export default {
|
|||
this.$router.currentRoute.query.isReadingTaskViewInOrder
|
||||
var criterionType = this.$router.currentRoute.query.criterionType
|
||||
var readingTool = this.$router.currentRoute.query.readingTool
|
||||
var path = `/readingDicoms?TrialReadingCriterionId=${trialReadingCriterionId}&trialId=${trialId}&subjectCode=${subjectCode}&subjectId=${subjectId}&visitTaskId=${task.VisitTaskId}&isReadingTaskViewInOrder=${isReadingTaskViewInOrder}&criterionType=${criterionType}&readingTool=${readingTool}&TokenKey=${token}`
|
||||
var path = `/readingDicoms?TrialReadingCriterionId=${trialReadingCriterionId}&trialId=${trialId}&subjectCode=${subjectCode}&subjectId=${subjectId}&visitTaskId=${task.VisitTaskId}&isReadingTaskViewInOrder=${isReadingTaskViewInOrder}&criterionType=${criterionType}&readingTool=${readingTool}&pageType=History&TokenKey=${token}`
|
||||
const routeData = this.$router.resolve({ path })
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({ imageToolType: 1 })
|
||||
let IsDoubleScreen = false
|
||||
if (res.IsSuccess) {
|
||||
IsDoubleScreen = res.Result.IsDoubleScreen
|
||||
|
|
@ -1059,6 +1061,7 @@ export default {
|
|||
'noopener,noreferrer'
|
||||
)
|
||||
}
|
||||
localStorage.setItem('closePage', Date.now());
|
||||
},
|
||||
handleSave(isPrompt) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
|
|||
|
|
@ -295,7 +295,6 @@ 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
|
||||
if (['viewport-MPR-1', 'viewport-MPR-2'].includes(viewportId)) return false
|
||||
const renderingEngine = getRenderingEngine(renderingEngineId)
|
||||
|
|
@ -400,7 +399,6 @@ async function renderSegmentation(series, visitInfo, viewportId, SegmentConfig,
|
|||
annotation.locking.setAnnotationLocked(an.annotationUID, true)
|
||||
annotation.visibility.setAnnotationVisibility(an.annotationUID, true)
|
||||
}
|
||||
console.log(an, 'an')
|
||||
}
|
||||
|
||||
})
|
||||
|
|
|
|||
|
|
@ -18,6 +18,14 @@
|
|||
@click.prevent="setToolActive('histogram_PlanarFreehandROI')">
|
||||
<svg-icon icon-class="polygon" class="svg-icon" />
|
||||
</div>
|
||||
<div :class="['tool-item']" :title="$t('trials:histogram:button:bgopen')"
|
||||
@click.prevent="showDefaultData(false)" v-if="isNeedDefault">
|
||||
<svg-icon icon-class="eye-open" class="svg-icon" />
|
||||
</div>
|
||||
<div :class="['tool-item']" :title="$t('trials:histogram:button:bgclose')"
|
||||
@click.prevent="showDefaultData(true)" v-else>
|
||||
<svg-icon icon-class="eye" class="svg-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">{{ $t("trials:histogram:title:histogram") }}</div>
|
||||
<i class="el-icon-circle-close closeBtn" @click.stop="close"></i>
|
||||
|
|
@ -115,13 +123,18 @@ export default {
|
|||
'#fb628b',
|
||||
],
|
||||
colors: [],
|
||||
seriesData: {}
|
||||
seriesData: {},
|
||||
isNeedDefault: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// this.initChart()
|
||||
},
|
||||
methods: {
|
||||
async showDefaultData(f) {
|
||||
this.isNeedDefault = f
|
||||
this.initChart()
|
||||
},
|
||||
setToolActive(toolName) {
|
||||
const toolGroupId = `${this.viewportKey}-${this.activeViewportIndex}`
|
||||
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
|
||||
|
|
@ -206,8 +219,9 @@ export default {
|
|||
}
|
||||
let seriesData = []
|
||||
Object.keys(this.seriesData).forEach(key => {
|
||||
seriesData.push(this.seriesData[key])
|
||||
if (key !== 'default' || this.isNeedDefault) seriesData.push(this.seriesData[key])
|
||||
})
|
||||
this.chart.clear();
|
||||
const option = {
|
||||
useUTC: true,
|
||||
title: {
|
||||
|
|
|
|||
|
|
@ -136,7 +136,9 @@ class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool {
|
|||
);
|
||||
|
||||
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
|
||||
setTimeout(() => {
|
||||
triggerAnnotationCompleted(annotation);
|
||||
}, 0);
|
||||
|
||||
return annotation;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,522 @@
|
|||
import { getEnabledElement, getRenderingEngine, VolumeViewport } from '@cornerstonejs/core'
|
||||
import * as cornerstoneTools from '@cornerstonejs/tools'
|
||||
|
||||
const { AnnotationDisplayTool, ToolGroupManager, utilities, annotation, drawing } = cornerstoneTools
|
||||
const { getAnnotations, addAnnotation } = annotation.state
|
||||
const { drawLine } = drawing
|
||||
|
||||
class FusionJumpToPointTool extends AnnotationDisplayTool {
|
||||
constructor(toolProps = {}, defaultToolProps = {
|
||||
supportedInteractionTypes: ['Mouse', 'Touch'],
|
||||
configuration: {
|
||||
targetViewportIds: [],//要联动跳转的视口
|
||||
toolGroupId: '',
|
||||
useBrightestPoint: true,//是否走 MIP 最亮点
|
||||
jumpToTargetViewports: true,
|
||||
dispatchEventName: 'fusion-mip-point-selected',//兼容外部事件监听
|
||||
getReferenceLineColor: null,
|
||||
style: {
|
||||
lineWidth: 2,
|
||||
lineLength: 20,
|
||||
centerHoleSize: 20,
|
||||
},
|
||||
referenceLinesCenterGapRadius: 20,
|
||||
minimal: {
|
||||
enabled: true,
|
||||
lineLengthInPx: 20,
|
||||
},
|
||||
},
|
||||
}) {
|
||||
super(toolProps, defaultToolProps)
|
||||
this.isHandleDragging = false
|
||||
this.suppressNextClick = false
|
||||
this.dragSourceViewportId = null
|
||||
}
|
||||
|
||||
mouseClickCallback(evt) {
|
||||
if (this.suppressNextClick) {
|
||||
this.suppressNextClick = false
|
||||
return
|
||||
}
|
||||
const { element, currentPoints } = evt.detail || {}
|
||||
const worldPoint = currentPoints?.world
|
||||
if (!element || !worldPoint || worldPoint.length < 3) return
|
||||
|
||||
const enabledElement = getEnabledElement(element)
|
||||
const { viewport, renderingEngine } = enabledElement || {}
|
||||
if (!viewport || !renderingEngine) return
|
||||
|
||||
const selectedPoint = this._resolveSelectedPoint(viewport, worldPoint)
|
||||
if (!selectedPoint || selectedPoint.length < 3) return
|
||||
|
||||
this.setPoint(selectedPoint, viewport.id, renderingEngine.id)
|
||||
}
|
||||
|
||||
mouseDownCallback(evt) {
|
||||
this._tryStartDrag(evt)
|
||||
}
|
||||
|
||||
preMouseDownCallback(evt) {
|
||||
return this._tryStartDrag(evt)
|
||||
}
|
||||
|
||||
mouseDragCallback(evt) {
|
||||
if (!this.isHandleDragging) return
|
||||
const { element, currentPoints } = evt.detail || {}
|
||||
if (!element) return
|
||||
|
||||
const enabledElement = getEnabledElement(element)
|
||||
const { viewport, renderingEngine } = enabledElement || {}
|
||||
if (!viewport || !renderingEngine) return
|
||||
|
||||
let worldPoint = currentPoints?.world
|
||||
if ((!worldPoint || worldPoint.length < 3) && currentPoints?.canvas && viewport.canvasToWorld) {
|
||||
worldPoint = viewport.canvasToWorld(currentPoints.canvas)
|
||||
}
|
||||
if (!worldPoint || worldPoint.length < 3) return
|
||||
|
||||
const annotation = this._getViewportCrosshairAnnotation(viewport)
|
||||
const sourceViewportId = this.dragSourceViewportId || annotation?.data?.sourceViewportId || viewport.id
|
||||
this.setPoint(worldPoint, sourceViewportId, renderingEngine.id)
|
||||
evt.preventDefault?.()
|
||||
}
|
||||
|
||||
mouseUpCallback(evt) {
|
||||
if (!this.isHandleDragging) return
|
||||
this.isHandleDragging = false
|
||||
this.dragSourceViewportId = null
|
||||
this.suppressNextClick = true
|
||||
evt.preventDefault?.()
|
||||
}
|
||||
|
||||
getHandleNearImagePoint(element, annotation, canvasCoords, proximity) {
|
||||
const enabledElement = getEnabledElement(element)
|
||||
const viewport = enabledElement?.viewport
|
||||
if (!viewport || !annotation?.data) return null
|
||||
if (annotation.data.type !== 'fusion-jump-crosshair') return null
|
||||
if (annotation.data.viewportId !== viewport.id) return null
|
||||
|
||||
const worldPoint = annotation.data?.handles?.points?.[0]
|
||||
if (!worldPoint) return null
|
||||
|
||||
const pointCanvas = viewport.worldToCanvas(worldPoint)
|
||||
if (!pointCanvas || pointCanvas.length < 2) return null
|
||||
|
||||
// Keep center handle hit-test generous so dragging still works after MIP rotation.
|
||||
const threshold = Math.max(12, proximity * 2)
|
||||
const near = Math.hypot(canvasCoords[0] - pointCanvas[0], canvasCoords[1] - pointCanvas[1]) < threshold
|
||||
return near ? worldPoint : null
|
||||
}
|
||||
|
||||
handleSelectedCallback(evt, annotation) {
|
||||
if (!annotation?.data || annotation.data.type !== 'fusion-jump-crosshair') return
|
||||
this.isHandleDragging = true
|
||||
evt.preventDefault?.()
|
||||
}
|
||||
|
||||
toolSelectedCallback(evt, annotation) {
|
||||
if (!annotation?.data || annotation.data.type !== 'fusion-jump-crosshair') return
|
||||
this.isHandleDragging = true
|
||||
evt.preventDefault?.()
|
||||
}
|
||||
|
||||
isPointNearTool(element, annotation, canvasCoords, proximity) {
|
||||
const enabledElement = getEnabledElement(element)
|
||||
const viewport = enabledElement?.viewport
|
||||
if (!viewport || !annotation?.data) return false
|
||||
if (annotation.data.type !== 'fusion-jump-crosshair') return false
|
||||
if (annotation.data.viewportId !== viewport.id) return false
|
||||
|
||||
const worldPoint = annotation.data?.handles?.points?.[0]
|
||||
if (!worldPoint) return false
|
||||
const pointCanvas = viewport.worldToCanvas(worldPoint)
|
||||
if (!pointCanvas || pointCanvas.length < 2) return false
|
||||
|
||||
const appearance = this._normalizeAppearance(annotation.data?.crosshairAppearance || {}, annotation.data?.sourceViewportId)
|
||||
const [cx, cy] = pointCanvas
|
||||
const halfHole = appearance.centerHoleSize / 2
|
||||
const len = appearance.lineLength
|
||||
const hitPad = Math.max(8, proximity)
|
||||
|
||||
const segments = [
|
||||
[[cx, cy - halfHole], [cx, cy - halfHole - len]],
|
||||
[[cx + halfHole, cy], [cx + halfHole + len, cy]],
|
||||
[[cx, cy + halfHole], [cx, cy + halfHole + len]],
|
||||
[[cx - halfHole, cy], [cx - halfHole - len, cy]],
|
||||
]
|
||||
|
||||
return segments.some(([a, b]) => this._distancePointToSegment(canvasCoords, a, b) <= hitPad)
|
||||
}
|
||||
|
||||
_distancePointToSegment(point, start, end) {
|
||||
const [px, py] = point
|
||||
const [x1, y1] = start
|
||||
const [x2, y2] = end
|
||||
const dx = x2 - x1
|
||||
const dy = y2 - y1
|
||||
const lenSq = dx * dx + dy * dy
|
||||
if (lenSq === 0) return Math.hypot(px - x1, py - y1)
|
||||
|
||||
let t = ((px - x1) * dx + (py - y1) * dy) / lenSq
|
||||
t = Math.max(0, Math.min(1, t))
|
||||
const projX = x1 + t * dx
|
||||
const projY = y1 + t * dy
|
||||
return Math.hypot(px - projX, py - projY)
|
||||
}
|
||||
|
||||
setPoint(worldPoint, sourceViewportId, renderingEngineId, options = {}) {
|
||||
if (!Array.isArray(worldPoint) || worldPoint.length < 3) return
|
||||
const renderingEngine = typeof renderingEngineId === 'string'
|
||||
? getRenderingEngine(renderingEngineId)
|
||||
: renderingEngineId
|
||||
if (!renderingEngine) return
|
||||
|
||||
this._applyPoint({
|
||||
renderingEngine,
|
||||
worldPoint,
|
||||
sourceViewportId,
|
||||
jumpToTargetViewports: options.jumpToTargetViewports !== false,
|
||||
dispatchEvent: options.dispatchEvent !== false,
|
||||
})
|
||||
}
|
||||
|
||||
renderAnnotation(enabledElement, svgDrawingHelper) {
|
||||
const { viewport } = enabledElement
|
||||
if (!viewport?.element) return false
|
||||
|
||||
const annotations = getAnnotations(this.getToolName(), viewport.element) || []
|
||||
const crosshairAnnotation = annotations.find((item) =>
|
||||
item?.data?.type === 'fusion-jump-crosshair' && item?.data?.viewportId === viewport.id
|
||||
)
|
||||
if (!crosshairAnnotation) return false
|
||||
|
||||
const worldPoint = crosshairAnnotation.data?.handles?.points?.[0]
|
||||
if (!worldPoint) return false
|
||||
|
||||
const canvasPoint = viewport.worldToCanvas(worldPoint)
|
||||
const [cx, cy] = canvasPoint || []
|
||||
const canvasWidth = viewport.canvas?.width / (window.devicePixelRatio || 1) || 0
|
||||
const canvasHeight = viewport.canvas?.height / (window.devicePixelRatio || 1) || 0
|
||||
if (!Number.isFinite(cx) || !Number.isFinite(cy) || cx < 0 || cy < 0 || cx > canvasWidth || cy > canvasHeight) {
|
||||
return false
|
||||
}
|
||||
|
||||
const appearance = this._normalizeAppearance(crosshairAnnotation.data?.crosshairAppearance || {}, crosshairAnnotation.data?.sourceViewportId)
|
||||
const halfHole = appearance.centerHoleSize / 2
|
||||
const len = appearance.lineLength
|
||||
const lineOptions = {
|
||||
color: appearance.color,
|
||||
width: appearance.lineWidth,
|
||||
}
|
||||
const horizontalLineOptions = {
|
||||
color: appearance.horizontalColor || appearance.color,
|
||||
width: appearance.lineWidth,
|
||||
}
|
||||
const verticalLineOptions = {
|
||||
color: appearance.verticalColor || appearance.color,
|
||||
width: appearance.lineWidth,
|
||||
}
|
||||
const uid = crosshairAnnotation.annotationUID
|
||||
|
||||
drawLine(svgDrawingHelper, uid, 'seg-top', [cx, cy - halfHole], [cx, cy - halfHole - len], verticalLineOptions, `${uid}-top`)
|
||||
drawLine(svgDrawingHelper, uid, 'seg-right', [cx + halfHole, cy], [cx + halfHole + len, cy], horizontalLineOptions, `${uid}-right`)
|
||||
drawLine(svgDrawingHelper, uid, 'seg-bottom', [cx, cy + halfHole], [cx, cy + halfHole + len], verticalLineOptions, `${uid}-bottom`)
|
||||
drawLine(svgDrawingHelper, uid, 'seg-left', [cx - halfHole, cy], [cx - halfHole - len, cy], horizontalLineOptions, `${uid}-left`)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
_applyPoint({ renderingEngine, worldPoint, sourceViewportId, jumpToTargetViewports, dispatchEvent }) {
|
||||
const targetViewports = this._getTargetViewports(renderingEngine)
|
||||
const sourceViewport = sourceViewportId ? renderingEngine.getViewport(sourceViewportId) : null
|
||||
const viewportMap = new Map()
|
||||
targetViewports.forEach((vp) => viewportMap.set(vp.id, vp))
|
||||
if (sourceViewport) {
|
||||
viewportMap.set(sourceViewport.id, sourceViewport)
|
||||
}
|
||||
const targetViewportIds = Array.from(viewportMap.keys())
|
||||
const sourceAppearance = this._resolveCrosshairAppearance(sourceViewportId || targetViewportIds[0])
|
||||
|
||||
targetViewportIds.forEach((viewportId) => {
|
||||
const viewport = viewportMap.get(viewportId)
|
||||
if (!viewport?.element) return
|
||||
|
||||
if (
|
||||
jumpToTargetViewports &&
|
||||
this.configuration.jumpToTargetViewports &&
|
||||
sourceViewportId &&
|
||||
viewport.id !== sourceViewportId &&
|
||||
viewport instanceof VolumeViewport
|
||||
) {
|
||||
try {
|
||||
viewport.jumpToWorld(worldPoint)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Align with Crosshairs color semantics: resolve per target viewport id.
|
||||
const appearance = this._resolveCrosshairAppearance(viewport.id)
|
||||
const axisColors = this._resolveAxisColorsForViewport(viewport, renderingEngine, appearance.color)
|
||||
this._upsertCrosshairAnnotation(viewport, worldPoint, sourceViewportId || viewport.id, appearance)
|
||||
if (axisColors) {
|
||||
const annotation = this._getViewportCrosshairAnnotation(viewport)
|
||||
if (annotation?.data?.crosshairAppearance) {
|
||||
annotation.data.crosshairAppearance.horizontalColor = axisColors.horizontalColor
|
||||
annotation.data.crosshairAppearance.verticalColor = axisColors.verticalColor
|
||||
}
|
||||
}
|
||||
viewport.render?.()
|
||||
})
|
||||
|
||||
if (utilities?.triggerAnnotationRenderForViewportIds && targetViewportIds.length) {
|
||||
utilities.triggerAnnotationRenderForViewportIds(targetViewportIds)
|
||||
}
|
||||
|
||||
if (dispatchEvent) {
|
||||
this._dispatchPointEvent(worldPoint, sourceViewportId, sourceAppearance)
|
||||
}
|
||||
}
|
||||
|
||||
_upsertCrosshairAnnotation(viewport, worldPoint, sourceViewportId, crosshairAppearance) {
|
||||
let crosshairAnnotation = this._getViewportCrosshairAnnotation(viewport)
|
||||
|
||||
if (!crosshairAnnotation) {
|
||||
const camera = viewport.getCamera?.() || {}
|
||||
crosshairAnnotation = {
|
||||
metadata: {
|
||||
toolName: this.getToolName(),
|
||||
viewPlaneNormal: camera.viewPlaneNormal ? [...camera.viewPlaneNormal] : undefined,
|
||||
viewUp: camera.viewUp ? [...camera.viewUp] : undefined,
|
||||
FrameOfReferenceUID: viewport.getFrameOfReferenceUID?.(),
|
||||
referencedImageId: null,
|
||||
},
|
||||
data: {
|
||||
type: 'fusion-jump-crosshair',
|
||||
viewportId: viewport.id,
|
||||
sourceViewportId,
|
||||
crosshairAppearance,
|
||||
handles: {
|
||||
points: [[...worldPoint]],
|
||||
},
|
||||
},
|
||||
}
|
||||
addAnnotation(crosshairAnnotation, viewport.element)
|
||||
return
|
||||
}
|
||||
|
||||
crosshairAnnotation.data.sourceViewportId = sourceViewportId
|
||||
crosshairAnnotation.data.crosshairAppearance = crosshairAppearance
|
||||
crosshairAnnotation.data.handles.points[0] = [...worldPoint]
|
||||
crosshairAnnotation.invalidated = true
|
||||
}
|
||||
|
||||
_getViewportCrosshairAnnotation(viewport) {
|
||||
if (!viewport?.element) return null
|
||||
const annotations = getAnnotations(this.getToolName(), viewport.element) || []
|
||||
return annotations.find((item) =>
|
||||
item?.data?.type === 'fusion-jump-crosshair' && item?.data?.viewportId === viewport.id
|
||||
) || null
|
||||
}
|
||||
|
||||
_tryStartDrag(evt) {
|
||||
const { element, currentPoints } = evt.detail || {}
|
||||
const canvasPoint = currentPoints?.canvas
|
||||
if (!element || !canvasPoint) return false
|
||||
|
||||
const enabledElement = getEnabledElement(element)
|
||||
const viewport = enabledElement?.viewport
|
||||
if (!viewport?.element) return false
|
||||
|
||||
const annotation = this._getViewportCrosshairAnnotation(viewport)
|
||||
if (!annotation) return false
|
||||
|
||||
const nearHandle = this.getHandleNearImagePoint(element, annotation, canvasPoint, 10)
|
||||
const nearLine = this.isPointNearTool(element, annotation, canvasPoint, 10)
|
||||
this.isHandleDragging = !!(nearHandle || nearLine)
|
||||
if (!this.isHandleDragging) return false
|
||||
|
||||
this.dragSourceViewportId = annotation?.data?.sourceViewportId || viewport.id
|
||||
evt.preventDefault?.()
|
||||
return true
|
||||
}
|
||||
|
||||
_dispatchPointEvent(worldPoint, sourceViewportId, crosshairAppearance) {
|
||||
const { dispatchEventName } = this.configuration
|
||||
if (!dispatchEventName || typeof window === 'undefined') return
|
||||
window.dispatchEvent(new CustomEvent(dispatchEventName, {
|
||||
detail: {
|
||||
world: [...worldPoint],
|
||||
sourceViewportId,
|
||||
crosshairAppearance,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
_resolveSelectedPoint(viewport, worldPoint) {
|
||||
if (!this.configuration.useBrightestPoint) {
|
||||
return worldPoint
|
||||
}
|
||||
|
||||
const volumeId = viewport.getVolumeId?.()
|
||||
if (!volumeId) {
|
||||
return worldPoint
|
||||
}
|
||||
|
||||
let maxIntensity = -Infinity
|
||||
const maxFn = (intensity, point) => {
|
||||
if (intensity > maxIntensity) {
|
||||
maxIntensity = intensity
|
||||
return point
|
||||
}
|
||||
}
|
||||
|
||||
const brightestPoint = utilities.planar.getPointInLineOfSightWithCriteria(
|
||||
viewport,
|
||||
worldPoint,
|
||||
volumeId,
|
||||
maxFn
|
||||
)
|
||||
|
||||
return brightestPoint && brightestPoint.length >= 3 ? brightestPoint : worldPoint
|
||||
}
|
||||
|
||||
_getTargetViewports(renderingEngine) {
|
||||
const { targetViewportIds, toolGroupId } = this.configuration
|
||||
const hasTargetIds = Array.isArray(targetViewportIds) && targetViewportIds.length > 0
|
||||
|
||||
return renderingEngine.getViewports().filter((vp) => {
|
||||
if (hasTargetIds && targetViewportIds.includes(vp.id)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (toolGroupId) {
|
||||
const toolGroup = ToolGroupManager.getToolGroupForViewport(vp.id, renderingEngine.id)
|
||||
if (toolGroup?.id === toolGroupId) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return !hasTargetIds && !toolGroupId
|
||||
})
|
||||
}
|
||||
|
||||
_resolveCrosshairAppearance(sourceViewportId) {
|
||||
const {
|
||||
getReferenceLineColor,
|
||||
style = {},
|
||||
referenceLinesCenterGapRadius,
|
||||
minimal = {},
|
||||
} = this.configuration
|
||||
|
||||
const color = typeof getReferenceLineColor === 'function'
|
||||
? getReferenceLineColor(sourceViewportId)
|
||||
: null
|
||||
|
||||
const minimalLineLength = minimal?.enabled && Number.isFinite(minimal?.lineLengthInPx)
|
||||
? minimal.lineLengthInPx
|
||||
: undefined
|
||||
const lineLength = Number.isFinite(style.lineLength)
|
||||
? style.lineLength
|
||||
: (Number.isFinite(minimalLineLength) ? minimalLineLength : 40)
|
||||
const centerHoleSize = Number.isFinite(style.centerHoleSize)
|
||||
? style.centerHoleSize
|
||||
: (Number.isFinite(referenceLinesCenterGapRadius) ? referenceLinesCenterGapRadius : 20)
|
||||
|
||||
return this._normalizeAppearance({
|
||||
color: color || '#6fb9ff',
|
||||
lineWidth: Number.isFinite(style.lineWidth) ? style.lineWidth : 2,
|
||||
lineLength,
|
||||
centerHoleSize,
|
||||
}, sourceViewportId)
|
||||
}
|
||||
|
||||
_resolveAxisColorsForViewport(viewport, renderingEngine, fallbackColor) {
|
||||
if (!viewport || !renderingEngine) return null
|
||||
|
||||
const orientation = this._getViewportOrientation(viewport)
|
||||
const orientationColorMap = this._getOrientationColorMap(renderingEngine, fallbackColor)
|
||||
const defaultColor = fallbackColor || '#6fb9ff'
|
||||
|
||||
if (orientation === 'axial') {
|
||||
return {
|
||||
horizontalColor: orientationColorMap.sagittal || defaultColor,
|
||||
verticalColor: orientationColorMap.coronal || defaultColor,
|
||||
}
|
||||
}
|
||||
if (orientation === 'sagittal') {
|
||||
return {
|
||||
horizontalColor: orientationColorMap.coronal || defaultColor,
|
||||
verticalColor: orientationColorMap.axial || defaultColor,
|
||||
}
|
||||
}
|
||||
if (orientation === 'coronal') {
|
||||
return {
|
||||
horizontalColor: orientationColorMap.sagittal || defaultColor,
|
||||
verticalColor: orientationColorMap.axial || defaultColor,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
horizontalColor: defaultColor,
|
||||
verticalColor: defaultColor,
|
||||
}
|
||||
}
|
||||
|
||||
_getOrientationColorMap(renderingEngine, fallbackColor) {
|
||||
const map = {
|
||||
axial: null,
|
||||
sagittal: null,
|
||||
coronal: null,
|
||||
}
|
||||
const targetViewports = this._getTargetViewports(renderingEngine)
|
||||
targetViewports.forEach((vp) => {
|
||||
const orientation = this._getViewportOrientation(vp)
|
||||
if (!orientation || map[orientation]) return
|
||||
map[orientation] = this._getReferenceLineColor(vp.id, fallbackColor)
|
||||
})
|
||||
return map
|
||||
}
|
||||
|
||||
_getViewportOrientation(viewport) {
|
||||
const normal = viewport?.getCamera?.()?.viewPlaneNormal
|
||||
if (!normal || normal.length < 3) return null
|
||||
const abs = normal.map((n) => Math.abs(n))
|
||||
const max = Math.max(abs[0], abs[1], abs[2])
|
||||
if (max === abs[2]) return 'axial'
|
||||
if (max === abs[0]) return 'sagittal'
|
||||
if (max === abs[1]) return 'coronal'
|
||||
return null
|
||||
}
|
||||
|
||||
_getReferenceLineColor(viewportId, fallbackColor) {
|
||||
if (typeof this.configuration.getReferenceLineColor === 'function') {
|
||||
const color = this.configuration.getReferenceLineColor(viewportId)
|
||||
if (color) return color
|
||||
}
|
||||
return fallbackColor || '#6fb9ff'
|
||||
}
|
||||
|
||||
_normalizeAppearance(appearance = {}, sourceViewportId) {
|
||||
const lineWidth = Number.isFinite(appearance.lineWidth) ? appearance.lineWidth : 2
|
||||
const lineLength = Number.isFinite(appearance.lineLength) ? appearance.lineLength : 9
|
||||
const centerHoleSize = Number.isFinite(appearance.centerHoleSize) ? appearance.centerHoleSize : 8
|
||||
let color = appearance.color
|
||||
if (!color && typeof this.configuration.getReferenceLineColor === 'function') {
|
||||
color = this.configuration.getReferenceLineColor(sourceViewportId)
|
||||
}
|
||||
|
||||
return {
|
||||
color: color || '#6fb9ff',
|
||||
lineWidth: Math.max(1, lineWidth),
|
||||
lineLength: Math.max(4, lineLength),
|
||||
centerHoleSize: Math.max(2, centerHoleSize),
|
||||
horizontalColor: appearance.horizontalColor || null,
|
||||
verticalColor: appearance.verticalColor || null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FusionJumpToPointTool.toolName = 'FusionJumpToPointTool'
|
||||
|
||||
export default FusionJumpToPointTool
|
||||
|
|
@ -153,6 +153,10 @@ export default {
|
|||
isExistsClinicalData: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
imageToolType: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
|
@ -361,7 +365,7 @@ export default {
|
|||
this.signVisible = false
|
||||
// 设置当前任务阅片状态为已读
|
||||
this.readingTaskState = 2
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({imageToolType: this.imageToolType})
|
||||
var isAutoTask = res.Result.AutoCutNextTask
|
||||
if (isAutoTask) {
|
||||
// DicomEvent.$emit('getNextTask')
|
||||
|
|
|
|||
|
|
@ -8,14 +8,18 @@
|
|||
:reading-category="taskInfo.ReadingCategory" :subject-code="taskInfo.SubjectCode"
|
||||
:task-blind-name="taskInfo.TaskBlindName" :is-reading-show-subject-info="taskInfo.IsReadingShowSubjectInfo"
|
||||
:is-reading-show-previous-results="taskInfo.IsReadingShowPreviousResults"
|
||||
:is-exists-clinical-data="taskInfo.IsExistsClinicalData" />
|
||||
:is-exists-clinical-data="taskInfo.IsExistsClinicalData"
|
||||
:imageToolType="2"
|
||||
/>
|
||||
<!-- 裁判阅片 -->
|
||||
<ad-review v-else-if="taskInfo && taskInfo.ReadingCategory === 4" :trial-id="trialId"
|
||||
:subject-id="taskInfo.SubjectId" :visit-task-id="taskInfo.VisitTaskId"
|
||||
:reading-category="taskInfo.ReadingCategory" :subject-code="taskInfo.SubjectCode"
|
||||
:task-blind-name="taskInfo.TaskBlindName" :is-reading-show-subject-info="taskInfo.IsReadingShowSubjectInfo"
|
||||
:is-reading-show-previous-results="taskInfo.IsReadingShowPreviousResults"
|
||||
:is-exists-clinical-data="taskInfo.IsExistsClinicalData" />
|
||||
:is-exists-clinical-data="taskInfo.IsExistsClinicalData"
|
||||
:imageToolType="2"
|
||||
/>
|
||||
<!-- 肿瘤学阅片 -->
|
||||
<!-- <oncology-review v-else-if="taskInfo && taskInfo.ReadingCategory=== 5" /> -->
|
||||
<el-dialog :visible.sync="clinicalDataVisible"
|
||||
|
|
|
|||
|
|
@ -205,6 +205,10 @@ export default {
|
|||
isExistsClinicalData: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
imageToolType: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
|
@ -397,7 +401,7 @@ export default {
|
|||
// 设置当前任务阅片状态为已读
|
||||
this.oncologyInfo.ReadingTaskState = 2
|
||||
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({imageToolType: this.imageToolType})
|
||||
var isAutoTask = res.Result.AutoCutNextTask
|
||||
if (isAutoTask) {
|
||||
// store.dispatch('reading/resetVisitTasks')
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@
|
|||
custom-class="base-dialog-wrapper">
|
||||
<el-form ref="reasonForm" :rules="rules" :model="ApplyforReasonForm" class="demo-ruleForm" size="small"
|
||||
label-width="380px">
|
||||
<p>{{ $t('trials:readTask:applyReread:title').replace('xxx', rowData.SubjectCode).replace('yyy', rowData.TaskName) }}</p>
|
||||
<!-- 申请原因 -->
|
||||
<el-divider content-position="left">{{ $t('trials:readTask:title:applyReason') }}</el-divider>
|
||||
<!-- 申请原因 -->
|
||||
|
|
@ -441,7 +442,8 @@ export default {
|
|||
path = `/noneDicomReading?TrialReadingCriterionId=${row.TrialReadingCriterionId}&trialId=${this.trialId}&subjectCode=${row.SubjectCode}&subjectId=${row.SubjectId}&visitTaskId=${row.Id}&isReadingTaskViewInOrder=${row.IsReadingTaskViewInOrder}&criterionType=${row.CriterionType}&readingTool=${row.ReadingTool}&TokenKey=${token}`
|
||||
}
|
||||
var routeData = this.$router.resolve({ path })
|
||||
const res = await getAutoCutNextTask()
|
||||
let imageToolType = row.ReadingTool === 1 ? 2 : 1
|
||||
const res = await getAutoCutNextTask({imageToolType})
|
||||
let IsDoubleScreen = false
|
||||
if (res.IsSuccess) {
|
||||
IsDoubleScreen = res.Result.IsDoubleScreen
|
||||
|
|
|
|||
|
|
@ -329,7 +329,8 @@ export default {
|
|||
path = `/noneDicomReading?TrialReadingCriterionId=${this.TrialReadingCriterionId}&trialId=${this.trialId}&subjectCode=${row.SubjectCode}&subjectId=${row.SubjectId}&isReadingTaskViewInOrder=${this.isReadingTaskViewInOrder}&criterionType=${this.criterionType}&readingTool=${this.readingTool}&TokenKey=${token}`
|
||||
}
|
||||
var routeData = this.$router.resolve({ path })
|
||||
const res = await getAutoCutNextTask()
|
||||
let imageToolType = this.readingTool === 1 ? 2 : 1
|
||||
const res = await getAutoCutNextTask({imageToolType})
|
||||
let IsDoubleScreen = false
|
||||
if (res.IsSuccess) {
|
||||
IsDoubleScreen = res.Result.IsDoubleScreen
|
||||
|
|
|
|||
|
|
@ -460,6 +460,7 @@
|
|||
custom-class="base-dialog-wrapper">
|
||||
<el-form ref="reasonForm" :rules="rules" :model="ApplyforReasonForm" class="demo-ruleForm" size="small"
|
||||
label-width="180px">
|
||||
<p>{{ $t('trials:reviewTrack:applyReread:title2').replace('xxx', rowData.SubjectCode).replace('yyy', rowData.TaskName) }}</p>
|
||||
<!-- 申请原因 -->
|
||||
<el-divider content-position="left">
|
||||
{{ $t('trials:reviewTrack:applyReread:title:applyReason') }}
|
||||
|
|
@ -624,6 +625,7 @@
|
|||
custom-class="base-dialog-wrapper">
|
||||
<el-form ref="backReasonForm" :rules="rules" :model="backforReasonForm" class="demo-ruleForm" size="small"
|
||||
label-width="180px">
|
||||
<p>{{ $t('trials:reviewTrack:applyReread:title1').replace('xxx', rowData.SubjectCode).replace('yyy', rowData.TaskName) }}</p>
|
||||
<!-- 申请原因 -->
|
||||
<el-divider content-position="left">
|
||||
{{ $t('trials:reviewTrack:applyReread:title:backReason') }}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@
|
|||
</div>
|
||||
<!-- viewports -->
|
||||
<div class="viewports-wrapper" v-loading="loading" ref="viewports-wrapper">
|
||||
<div class="grid-container" :style="gridStyle">
|
||||
<div ref="container" class="grid-container" :style="gridStyle">
|
||||
<div v-for="(v, index) in viewportInfos" v-show="index < cells.length" :key="index" :style="cellStyle"
|
||||
:class="['grid-cell', index === activeCanvasIndex ? 'cell_active' : '', index === fullScreenIndex ? 'cell-full-screen' : '']"
|
||||
@dblclick="toggleFullScreen($event, index)" @click="activeCanvas(index)"
|
||||
|
|
@ -237,7 +237,17 @@
|
|||
|
||||
<el-dialog v-if="personalConfigDialog.visible" :visible.sync="personalConfigDialog.visible"
|
||||
:close-on-click-modal="false" :title="personalConfigDialog.title" width="600px">
|
||||
<Others />
|
||||
<!-- <Others /> -->
|
||||
<el-tabs v-model="activeName" class="personal_config">
|
||||
<!-- 热键 -->
|
||||
<el-tab-pane :label="$t('trials:reading:tab:hotkeys')" name="1">
|
||||
<Hotkeys v-if="activeName === '1'" :reading-tool="1" @reset="resetHotkeyList" />
|
||||
</el-tab-pane>
|
||||
<!-- 其他 -->
|
||||
<el-tab-pane :label="$t('trials:reading:tab:others')" name="2">
|
||||
<Others v-if="activeName === '2'" :imageToolType="2"/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
<upload-dicom-and-nonedicom v-if="uploadImageVisible" :subject-id="uploadSubjectId"
|
||||
:subject-code="uploadSubjectCode" :criterion="uploadTrialCriterion" :visible.sync="uploadImageVisible"
|
||||
|
|
@ -272,6 +282,7 @@
|
|||
</template>
|
||||
<script>
|
||||
import { addNoneDicomMark, deleteTrialFileType, getCriterionReadingInfo, setReadKeyFile } from '@/api/trials'
|
||||
import { getDoctorShortcutKey } from '@/api/user'
|
||||
import html2canvas from 'html2canvas'
|
||||
import {
|
||||
RenderingEngine,
|
||||
|
|
@ -290,6 +301,7 @@ import hardcodedMetaDataProvider from './../js/hardcodedMetaDataProvider'
|
|||
import registerWebImageLoader from './../js/registerWebImageLoader'
|
||||
import { mapGetters } from 'vuex'
|
||||
import store from '@/store'
|
||||
import Hotkeys from '@/views/trials/trials-panel/reading/dicoms/components/Hotkeys'
|
||||
import Others from '@/views/trials/trials-panel/reading/dicoms/components/Others'
|
||||
import Manuals from '@/views/trials/trials-panel/reading/dicoms/components/Manuals'
|
||||
const { ViewportType } = Enums
|
||||
|
|
@ -321,6 +333,7 @@ const { MouseBindings, Events: toolsEvents } = csToolsEnums
|
|||
export default {
|
||||
name: 'ImageViewer',
|
||||
components: {
|
||||
Hotkeys,
|
||||
Others,
|
||||
downloadDicomAndNonedicom,
|
||||
uploadDicomAndNonedicom,
|
||||
|
|
@ -397,10 +410,13 @@ export default {
|
|||
annotation: null
|
||||
},
|
||||
loading: false,
|
||||
manualsDialog: { visible: false, isFullscreen: false },
|
||||
manualsDialog: { visible: false, isFullscreen: false, justKeyDoc: false },
|
||||
trialId: null,
|
||||
|
||||
ManualsClose: false
|
||||
ManualsClose: false,
|
||||
activeName: '1',
|
||||
hotKeyList: [],
|
||||
isShowAnnotations: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -498,6 +514,7 @@ export default {
|
|||
this.$refs['viewports-wrapper'].addEventListener('wheel', (e) => {
|
||||
this.setToolsPassive()
|
||||
});
|
||||
this.getHotKeys()
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('message', this.handleIframeMessage)
|
||||
|
|
@ -1730,6 +1747,39 @@ export default {
|
|||
viewport.resetCamera({ resetPan: true, resetZoom: true, resetToCenter: true })
|
||||
renderingEngine.render()
|
||||
},
|
||||
setZoomScale(isZoomIn) {
|
||||
this.setToolsPassive()
|
||||
const renderingEngine = getRenderingEngine(renderingEngineId)
|
||||
const viewportId = `canvas-${this.activeCanvasIndex}`
|
||||
const viewport = renderingEngine?.getViewport(viewportId)
|
||||
if (!viewport) return
|
||||
const camera = viewport.getCamera()
|
||||
const factor = 1.1
|
||||
const current = Number(camera.parallelScale)
|
||||
if (!Number.isFinite(current) || current <= 0) return
|
||||
const next = isZoomIn ? current / factor : current * factor
|
||||
const parallelScale = Math.max(0.0001, Math.min(1000000, next))
|
||||
viewport.setCamera({ parallelScale })
|
||||
viewport.render()
|
||||
},
|
||||
async saveImage() {
|
||||
const divForDownloadViewport = document.querySelector(
|
||||
`div[data-viewport-uid="canvas-${this.activeCanvasIndex}"]`
|
||||
)
|
||||
if (!divForDownloadViewport) return
|
||||
const canvas = await html2canvas(divForDownloadViewport)
|
||||
const base64Str = canvas.toDataURL('image/png', 1)
|
||||
const i = this.viewportInfos.findIndex(v => v.index === this.activeCanvasIndex)
|
||||
const name = (i > -1 ? this.viewportInfos[i].currentFileName : '') || `screenshot-${Date.now()}`
|
||||
const safeName = String(name).replace(/[\\/:*?"<>|]/g, '_')
|
||||
const downloadName = safeName.toLowerCase().endsWith('.png') ? safeName : `${safeName}.png`
|
||||
const a = document.createElement('a')
|
||||
a.href = base64Str
|
||||
a.download = downloadName
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
},
|
||||
resetAnnotations({ annotations, visitTaskId }) {
|
||||
// 移除病灶
|
||||
const arr = cornerstoneTools.annotation.state.getAllAnnotations()
|
||||
|
|
@ -1920,8 +1970,101 @@ export default {
|
|||
this.$emit('previewCD', id)
|
||||
},
|
||||
previewConfig() {
|
||||
this.activeName = '1'
|
||||
this.personalConfigDialog.visible = true
|
||||
},
|
||||
// 获取热键配置信息
|
||||
async getHotKeys() {
|
||||
try {
|
||||
const res = await getDoctorShortcutKey({ imageToolType: 2 })
|
||||
res.Result.map(item => {
|
||||
this.hotKeyList.push({ id: item.Id, altKey: item.AltKey, ctrlKey: item.CtrlKey, shiftKey: item.ShiftKey, metaKey: item.MetaKey, key: item.Keyboardkey, code: item.Code, text: item.Text, shortcutKeyEnum: item.ShortcutKeyEnum })
|
||||
})
|
||||
this.bindHotKey()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
// 绑定热键事件
|
||||
bindHotKey() {
|
||||
const container = this.$refs['container']
|
||||
if (!container) return
|
||||
container.tabIndex = 0
|
||||
container.focus()
|
||||
container.addEventListener('keydown', event => {
|
||||
const idx = this.hotKeyList.findIndex(i => i.code === event.code && i.ctrlKey === event.ctrlKey && i.shiftKey === event.shiftKey && i.altKey === event.altKey)
|
||||
if (idx === -1) return
|
||||
const shortcutKeyEnum = this.hotKeyList[idx].shortcutKeyEnum
|
||||
|
||||
if (shortcutKeyEnum === 1) {
|
||||
// 前一图像视口
|
||||
const canvasIndex = this.activeCanvasIndex === 0 ? this.activeCanvasIndex : this.activeCanvasIndex - 1
|
||||
this.activeCanvas(canvasIndex)
|
||||
} else if (shortcutKeyEnum === 2) {
|
||||
// 后一图像视口
|
||||
const canvasIndex = this.activeCanvasIndex === this.cells.length - 1 ? this.activeCanvasIndex : this.activeCanvasIndex + 1
|
||||
this.activeCanvas(canvasIndex)
|
||||
} else if (shortcutKeyEnum === 5) {
|
||||
// 上一张图像
|
||||
const i = this.viewportInfos.findIndex(v => v.index === this.activeCanvasIndex)
|
||||
if (i > -1 && this.imageType.includes(this.viewportInfos[i].fileType)) {
|
||||
this.sliceIndex(this.viewportInfos[i].currentImageIdIndex - 1)
|
||||
}
|
||||
} else if (shortcutKeyEnum === 6) {
|
||||
// 下一张图像
|
||||
const i = this.viewportInfos.findIndex(v => v.index === this.activeCanvasIndex)
|
||||
if (i > -1 && this.imageType.includes(this.viewportInfos[i].fileType)) {
|
||||
this.sliceIndex(this.viewportInfos[i].currentImageIdIndex + 1)
|
||||
}
|
||||
} else if (shortcutKeyEnum === 11) {
|
||||
// 放大
|
||||
const i = this.viewportInfos.findIndex(v => v.index === this.activeCanvasIndex)
|
||||
if (i > -1 && this.imageType.includes(this.viewportInfos[i].fileType)) {
|
||||
this.setZoomScale(true)
|
||||
}
|
||||
} else if (shortcutKeyEnum === 12) {
|
||||
// 缩小
|
||||
const i = this.viewportInfos.findIndex(v => v.index === this.activeCanvasIndex)
|
||||
if (i > -1 && this.imageType.includes(this.viewportInfos[i].fileType)) {
|
||||
this.setZoomScale(false)
|
||||
}
|
||||
} else if (shortcutKeyEnum === 15) {
|
||||
// 截图
|
||||
const i = this.viewportInfos.findIndex(v => v.index === this.activeCanvasIndex)
|
||||
if (i > -1 && this.imageType.includes(this.viewportInfos[i].fileType)) {
|
||||
this.saveImage()
|
||||
}
|
||||
} else if (shortcutKeyEnum === 18) {
|
||||
// 重置
|
||||
this.resetViewport()
|
||||
} else if (shortcutKeyEnum === 19) {
|
||||
// 显示或隐藏标记
|
||||
this.isShowAnnotations = !this.isShowAnnotations
|
||||
const { visibility } = cornerstoneTools.annotation
|
||||
if (this.isShowAnnotations) {
|
||||
visibility.showAllAnnotations()
|
||||
} else {
|
||||
const annotationUIDs = cornerstoneTools.annotation.state.getAllAnnotations().map((a) => a.annotationUID)
|
||||
annotationUIDs.forEach((annotationUID) => {
|
||||
visibility.setAnnotationVisibility(annotationUID, false)
|
||||
})
|
||||
}
|
||||
const renderingEngine = getRenderingEngine(renderingEngineId)
|
||||
let viewportIds = [`canvas-0`, `canvas-1`, `canvas-2`, `canvas-3`]
|
||||
renderingEngine.renderViewports(viewportIds)
|
||||
}
|
||||
event.stopImmediatePropagation()
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
})
|
||||
},
|
||||
// 重置热键信息
|
||||
resetHotkeyList(arr) {
|
||||
this.hotKeyList = []
|
||||
arr.map(item => {
|
||||
this.hotKeyList.push({ id: item.id, altKey: item.keys.controlKey.altKey, ctrlKey: item.keys.controlKey.ctrlKey, shiftKey: item.keys.controlKey.shiftKey, metaKey: item.keys.controlKey.metaKey, key: item.keys.controlKey.key, code: item.keys.controlKey.code, text: item.keys.text, shortcutKeyEnum: item.label })
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -2178,4 +2321,17 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
.personal_config {
|
||||
::v-deep .el-tabs__content {
|
||||
height: 450px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
::v-deep .hot-keys-label {
|
||||
color: #dfdfdf !important;
|
||||
}
|
||||
|
||||
::v-deep .shortcut-key-input span {
|
||||
color: #dfdfdf !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -775,7 +775,7 @@ export default {
|
|||
localStorage.setItem('taskInfo', JSON.stringify(this.taskInfo))
|
||||
store.dispatch('noneDicomReview/setCurrentTaskState', 2)
|
||||
window.opener.postMessage('refreshTaskList', window.location)
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({ imageToolType: 2 })
|
||||
const isAutoTask = res.Result.AutoCutNextTask
|
||||
if (isAutoTask) {
|
||||
window.location.reload()
|
||||
|
|
@ -814,7 +814,7 @@ export default {
|
|||
var trialReadingCriterionId = this.$router.currentRoute.query.TrialReadingCriterionId
|
||||
var path = `/noneDicomReading?TrialReadingCriterionId=${trialReadingCriterionId}&trialId=${trialId}&subjectCode=${subjectCode}&subjectId=${subjectId}&visitTaskId=${task.VisitTaskId}&isReadingTaskViewInOrder=${isReadingTaskViewInOrder}&criterionType=${criterionType}&readingTool=${readingTool}&TokenKey=${token}`
|
||||
const routeData = this.$router.resolve({ path })
|
||||
const res = await getAutoCutNextTask()
|
||||
const res = await getAutoCutNextTask({ imageToolType: 2 })
|
||||
let IsDoubleScreen = false
|
||||
if (res.IsSuccess) {
|
||||
IsDoubleScreen = res.Result.IsDoubleScreen
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export default {
|
|||
this.loading = true
|
||||
var file = await this.fileToBlob(param.file)
|
||||
const res = await this.OSSclient.put(
|
||||
`/${this.$route.query.trialId}/DocumentToSign/${param.file.name}${new Date().getTime()}`,
|
||||
`/${this.$route.query.trialId}/DocumentToSign/${param.file.name}`,
|
||||
file
|
||||
)
|
||||
this.fileList.push({
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ export default {
|
|||
this.loading = true
|
||||
var file = await this.fileToBlob(param.file)
|
||||
const trialId = this.$route.query.trialId
|
||||
const res = await this.OSSclient.put(`/${trialId}/DocumentToSign/${param.file.name}${new Date().getTime()}`, file)
|
||||
const res = await this.OSSclient.put(`/${trialId}/DocumentToSign/${param.file.name}`, file)
|
||||
this.fileList.push({ name: param.file.name, path: this.$getObjectName(res.url), url: this.$getObjectName(res.url) })
|
||||
this.form.Path = this.$getObjectName(res.url)
|
||||
this.form.Name = param.file.name
|
||||
|
|
|
|||
|
|
@ -99,11 +99,11 @@
|
|||
<el-table-column label="文件类型" prop="FileType" min-width="90" show-overflow-tooltip sortable="custom">
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="源区域" prop="UploadRegion" min-width="60" show-overflow-tooltip sortable="custom" />
|
||||
<el-table-column label="源区域" prop="UploadRegion" min-width="90" show-overflow-tooltip sortable="custom" />
|
||||
|
||||
<el-table-column label="源可用时间" prop="CreateTime" min-width="90" show-overflow-tooltip sortable="custom" />
|
||||
<el-table-column label="源可用时间" prop="CreateTime" min-width="120" show-overflow-tooltip sortable="custom" />
|
||||
<el-table-column label="路径" prop="Path" min-width="90" show-overflow-tooltip sortable="custom" />
|
||||
<el-table-column label="是否需要同步" prop="IsNeedSync" min-width="90" show-overflow-tooltip sortable="custom">
|
||||
<el-table-column label="是否需要同步" prop="IsNeedSync" min-width="120" show-overflow-tooltip sortable="custom">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.IsNeedSync" type="success">
|
||||
{{ $fd('YesOrNo', scope.row.IsNeedSync) }}
|
||||
|
|
@ -113,10 +113,10 @@
|
|||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="目标区域" prop="TargetRegion" min-width="80" show-overflow-tooltip sortable="custom" />
|
||||
<el-table-column label="目标可用时间" prop="SyncFinishedTime" min-width="90" show-overflow-tooltip sortable="custom" />
|
||||
<el-table-column label="优先级" prop="Priority" min-width="60" show-overflow-tooltip sortable="custom" />
|
||||
<el-table-column label="是否同步完成" prop="IsSync" min-width="90" show-overflow-tooltip sortable="custom">
|
||||
<el-table-column label="目标区域" prop="TargetRegion" min-width="100" show-overflow-tooltip sortable="custom" />
|
||||
<el-table-column label="目标可用时间" prop="SyncFinishedTime" min-width="120" show-overflow-tooltip sortable="custom" />
|
||||
<el-table-column label="优先级" prop="Priority" min-width="80" show-overflow-tooltip sortable="custom" />
|
||||
<el-table-column label="是否同步完成" prop="IsSync" min-width="120" show-overflow-tooltip sortable="custom">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.IsSync" type="success">
|
||||
{{ $fd('YesOrNo', scope.row.IsSync) }}
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="更新时间" prop="UpdateTime" min-width="90" show-overflow-tooltip sortable="custom" />
|
||||
<el-table-column label="更新时间" prop="UpdateTime" min-width="120" show-overflow-tooltip sortable="custom" />
|
||||
<el-table-column label="操作" width="240" show-overflow-tooltip fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="primary" size="mini" @click="handleOpenTaskTable(scope.row)">
|
||||
|
|
@ -244,7 +244,9 @@ export default {
|
|||
props: {
|
||||
rowInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
dataFileType: {
|
||||
type: Number,
|
||||
|
|
@ -298,9 +300,12 @@ export default {
|
|||
try {
|
||||
this.loading = true
|
||||
this.searchData.TrialId = this.$route.query.trialId
|
||||
if (Object.keys(this.rowInfo).length !== 0) {
|
||||
this.searchData.SubjectCode = this.rowInfo.SubjectCode
|
||||
this.searchData.VisitName = this.rowInfo.VisitName
|
||||
this.searchData.StudyCode = this.rowInfo.StudyCode
|
||||
}
|
||||
|
||||
this.searchData.DataFileType = this.dataFileType
|
||||
let res = await getFileUploadRecordList(this.searchData)
|
||||
this.loading = false
|
||||
|
|
|
|||
|
|
@ -178,9 +178,11 @@ export default {
|
|||
try {
|
||||
this.loading = true
|
||||
this.searchData.TrialId = this.$route.query.trialId
|
||||
if (Object.keys(this.rowInfo).length !== 0) {
|
||||
this.searchData.StudyCode = this.rowInfo.StudyCode
|
||||
this.searchData.SubjectCode = this.rowInfo.SubjectCode
|
||||
this.searchData.VisitName = this.rowInfo.VisitName
|
||||
}
|
||||
|
||||
let res = await getUploadFileSyncRecordList(this.searchData)
|
||||
this.loading = false
|
||||
|
|
|
|||
|
|
@ -1,22 +1,84 @@
|
|||
<template>
|
||||
<el-tabs class="data-sync-tabs" type="border-card" tab-position="left" v-model="activeTab" >
|
||||
<el-tab-pane label="检查列表" name="study">
|
||||
<StudyList />
|
||||
<StudyList v-if="activeTab === 'study'"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="其他" name="other">其他</el-tab-pane>
|
||||
<el-tab-pane label="其他" name="other">
|
||||
<!-- <FileList v-if="activeTab === 'other'" :dataFileType="2" @openTaskTable="openTaskTable"/> -->
|
||||
<el-tabs v-if="activeTab === 'other'" class="detail-tabs" v-model="tabInfo.activeTab" @tab-click="handleDetailTabClick">
|
||||
<el-tab-pane label="文件级别" name="file">
|
||||
<!-- v-if="tabInfo.activeTab === 'file'" -->
|
||||
<FileList
|
||||
v-if="tabInfo.activeTab === 'file'"
|
||||
:dataFileType="2"
|
||||
@openTaskTable="openTaskTable"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="任务级别" name="task">
|
||||
<TaskList
|
||||
v-if="tabInfo.activeTab === 'task'"
|
||||
:rowInfo="tabInfo.currentRow"
|
||||
:fileUploadRecordId="fileUploadRecordId"
|
||||
:path="path"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-tab-pane>
|
||||
<!-- <el-dialog
|
||||
v-if="detailDialog.visible"
|
||||
:visible.sync="detailDialog.visible"
|
||||
fullscreen
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
class="detail-dialog"
|
||||
>
|
||||
<span slot="title">{{ detailDialog.title }}</span>
|
||||
<span v-if="detailDialog.currentRow">{{`${detailDialog.currentRow.SubjectCode} / ${detailDialog.currentRow.VisitName} ${detailDialog.currentRow.StudyCode ? ' / ' + detailDialog.currentRow.StudyCode : ''} (源:${detailDialog.currentRow.UploadRegion} -> 目标:${detailDialog.currentRow.TargetRegion})`}}</span>
|
||||
<TaskList
|
||||
:rowInfo="detailDialog.currentRow"
|
||||
:fileUploadRecordId="detailDialog.currentRow.Id"
|
||||
:path="detailDialog.currentRow.Path"
|
||||
/>
|
||||
</el-dialog> -->
|
||||
</el-tabs>
|
||||
</template>
|
||||
<script>
|
||||
import StudyList from './components/StudyList'
|
||||
import FileList from './components/FileList'
|
||||
import TaskList from './components/TaskList'
|
||||
export default {
|
||||
name: 'DataSync',
|
||||
components: {
|
||||
StudyList
|
||||
StudyList,
|
||||
FileList,
|
||||
TaskList
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
activeTab: 'study'
|
||||
activeTab: 'study',
|
||||
tabInfo: {
|
||||
title: '详情',
|
||||
activeTab: 'file',
|
||||
currentRow: null
|
||||
},
|
||||
fileUploadRecordId: '',
|
||||
path: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openTaskTable(obj) {
|
||||
this.tabInfo.currentRow = obj || null
|
||||
this.fileUploadRecordId = obj.Id
|
||||
this.path = obj.Path
|
||||
this.tabInfo.activeTab = 'task'
|
||||
},
|
||||
handleDetailTabClick(tab) {
|
||||
const name = tab.name
|
||||
if (name === 'file') {
|
||||
this.fileUploadRecordId = ''
|
||||
this.path = ''
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -24,14 +86,61 @@ export default {
|
|||
.data-sync-tabs{
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
.el-tabs__header {
|
||||
> .el-tabs__header {
|
||||
height: 100%;
|
||||
}
|
||||
.el-tabs__content {
|
||||
> .el-tabs__content {
|
||||
height: 100%;
|
||||
padding: 0px;
|
||||
overflow: auto;
|
||||
}
|
||||
> .el-tabs__content > .el-tab-pane {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.detail-dialog {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-dialog__header {
|
||||
flex: 0 0 auto;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding-top: 10px;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.is-fullscreen {
|
||||
.el-dialog__body {
|
||||
margin-top: 10px;
|
||||
height: calc(100% - 80px);
|
||||
}
|
||||
}
|
||||
}
|
||||
.detail-tabs {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
.el-tabs__header {
|
||||
flex: 0 0 auto;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.el-tabs__content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.el-tab-pane {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,10 +1,12 @@
|
|||
<template>
|
||||
<div v-loading="loading" class="clinical-data-wrapper">
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane v-for="cd in clinicalDatas" :key="cd.ClinicalDataTrialSetId" :label="$i18n.locale === 'zh'
|
||||
? cd.ClinicalDataSetName
|
||||
: cd.ClinicalDataSetEnName
|
||||
">
|
||||
<el-tabs type="border-card" v-model="activeName" @tab-click="tabClick">
|
||||
<el-tab-pane
|
||||
v-for="cd in clinicalDatas"
|
||||
:key="cd.ClinicalDataTrialSetId"
|
||||
:label="$i18n.locale === 'zh' ? cd.ClinicalDataSetName: cd.ClinicalDataSetEnName"
|
||||
:name="cd.ClinicalDataTrialSetId"
|
||||
>
|
||||
<!-- 格式化录入 -->
|
||||
<div v-if="cd.ClinicalUploadType === 0">
|
||||
<!-- 既往放疗史 -->
|
||||
|
|
@ -222,6 +224,96 @@
|
|||
:open-type="'add'" @close="getClinicalData" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<!-- 患者数据 -->
|
||||
<el-tab-pane :label="$t('trials:tab:patientData')" name="patientForm">
|
||||
<el-form
|
||||
ref="patientForm"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="100"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<div class="form-row">
|
||||
<!-- 性别 -->
|
||||
<el-form-item class="form-item-half" :label="$t('trials:ptData:label:patientSex')" prop="PatientSex">
|
||||
<el-select
|
||||
v-model="formData.PatientSex"
|
||||
:placeholder="$t('common:ruleMessage:select')"
|
||||
style="width: 100%"
|
||||
:disabled="!isPatientFormCanEdit"
|
||||
>
|
||||
<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 class="form-item-half" :label="$t('trials:ptData:label:patientWeight')" prop="PatientWeight">
|
||||
<el-input
|
||||
v-model.number="formData.PatientWeight"
|
||||
:placeholder="$t('trials:patientWeight:eg')"
|
||||
style="width: 100%"
|
||||
:disabled="!isPatientFormCanEdit"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<!-- 总剂量(Bq) 例如 740000000-->
|
||||
<el-form-item class="form-item-half" :label="$t('trials:ptData:label:totalDose')" prop="RadionuclideTotalDose">
|
||||
<el-input
|
||||
v-model.number="formData.RadionuclideTotalDose"
|
||||
:placeholder="$t('trials:totalDose:eg')"
|
||||
style="width: 100%"
|
||||
:disabled="!isPatientFormCanEdit"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<!-- 半衰期(s) 例如 21600-->
|
||||
<el-form-item class="form-item-half" :label="$t('trials:ptData:label:halfLife')" prop="RadionuclideHalfLife">
|
||||
<el-input
|
||||
v-model.number="formData.RadionuclideHalfLife"
|
||||
:placeholder="$t('trials:halfLife:eg')"
|
||||
style="width: 100%"
|
||||
:disabled="!isPatientFormCanEdit"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<!-- 注射时间(s) Unix 秒 或 相对秒-->
|
||||
<el-form-item class="form-item-half" :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="!isPatientFormCanEdit"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<!-- 成像时间(s) Unix 秒 或 相对秒-->
|
||||
<el-form-item class="form-item-half" :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="!isPatientFormCanEdit"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 时间一致性检查 -->
|
||||
<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="isPatientFormCanEdit">
|
||||
<el-button type="primary" @click="submitForm">{{ $t('trials:ptData:button:submit') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- 既往放疗史 -->
|
||||
|
|
@ -269,6 +361,8 @@ import {
|
|||
deletePreviousSurgery,
|
||||
getCRCClinicalData,
|
||||
addOrUpdateReadingClinicalData,
|
||||
getPatientInfo,
|
||||
editPatientInfo
|
||||
} from '@/api/trials'
|
||||
import PreviousRadiotherapy from './previousRadiotherapy'
|
||||
import PreviousSurgery from './previousSurgery'
|
||||
|
|
@ -313,6 +407,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isPatientFormAllowEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -336,9 +434,50 @@ export default {
|
|||
downloadLoading: false,
|
||||
clinicalDatas: [],
|
||||
previewObj: { visible: false, filePath: '', fileType: '' },
|
||||
activeName: '',
|
||||
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,
|
||||
isPatientFormCanEdit: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.isPatientFormCanEdit = this.allowAddOrEdit || this.isPatientFormAllowEdit
|
||||
this.getClinicalData()
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -452,6 +591,7 @@ export default {
|
|||
getCRCClinicalData(param)
|
||||
.then((res) => {
|
||||
this.clinicalDatas = res.Result
|
||||
this.activeName = res.Result.length > 0 ? res.Result[0].ClinicalDataTrialSetId : ''
|
||||
this.loading = false
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
@ -636,6 +776,70 @@ export default {
|
|||
handleDownloadTpl(cd) {
|
||||
window.open(this.OSSclientConfig.basePath + cd.Path)
|
||||
},
|
||||
// 时间一致性校验
|
||||
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 tabClick(tab) {
|
||||
try {
|
||||
const name = tab.name
|
||||
if (name === 'patientForm' && !this.formData.Id) {
|
||||
this.formLoading = true
|
||||
let res = await getPatientInfo({studyId: this.studyData.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()
|
||||
this.formLoading = false
|
||||
}
|
||||
} catch(e) {
|
||||
this.formLoading = false
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
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'))
|
||||
}
|
||||
} catch(e) {
|
||||
this.formLoading = false
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -646,5 +850,16 @@ export default {
|
|||
margin-bottom: 10px;
|
||||
background-color: #dcdfe6;
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 0px;
|
||||
.el-form-item {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
.form-item-half {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1023,7 +1023,8 @@ export default {
|
|||
// 申请影像退回
|
||||
async imageBack(row) {
|
||||
try {
|
||||
this.$prompt(`<span style="color:#F56C6C">*</span><span>${this.$t("trials:crcUpload:confirmMessage:ApplyReason")}</span>`, this.$t("trials:crcUpload:confirmMessage:imageBack"), {
|
||||
let title = `${this.$t('trials:crcUpload:confirmMessage:imageBackTitle').replace('xxx', row.SubjectCode).replace('yyy', row.VisitName)}`
|
||||
this.$prompt(`<p style="margin-left: 5px;">${title}</p><span style="color:#F56C6C">*</span><span>${this.$t("trials:crcUpload:confirmMessage:ApplyReason")}</span>`, this.$t("trials:crcUpload:confirmMessage:imageBack"), {
|
||||
confirmButtonText: this.$t("common:button:save"),
|
||||
cancelButtonText: this.$t("common:button:cancel"),
|
||||
dangerouslyUseHTMLString: true,
|
||||
|
|
|
|||
|
|
@ -76,7 +76,18 @@
|
|||
<el-table-column type="selection" width="55" v-if='$store.state.trials.config.IsSupportQCDownloadImage'>
|
||||
</el-table-column>
|
||||
<!-- 检查编号 -->
|
||||
<el-table-column prop="StudyCode" :label="$t('trials:audit:table:studyId')" sortable />
|
||||
<el-table-column prop="StudyCode" :label="$t('trials:audit:table:studyId')" sortable>
|
||||
<template slot-scope="scope">
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
v-if="['PT、CT', 'CT、PT', 'PET-CT'].includes(scope.row.Modalities) && IsHaveStudyClinicalData && scope.row.IsHasEmptyPatientInfo"
|
||||
>
|
||||
<div slot="content">{{ $t('trials:audit:message:ptDataValid') }}</div>
|
||||
<span class="el-icon-warning" style="color: red; cursor: pointer"></span>
|
||||
</el-tooltip>
|
||||
{{ scope.row.StudyCode }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 检查名称 -->
|
||||
<el-table-column prop="StudyName" v-if="relationInfo.IsShowStudyName"
|
||||
:label="$t('trials:audit:table:StudyName')" sortable>
|
||||
|
|
@ -898,6 +909,11 @@
|
|||
@click="handleQCState(8)">
|
||||
{{ $t('trials:audit:button:auditPassed') }}
|
||||
</el-button>
|
||||
<!-- 跳过 -->
|
||||
<el-button size="small" type="primary" round
|
||||
@click="skipTask">
|
||||
{{ $t('trials:audit:button:skipTask') }}
|
||||
</el-button>
|
||||
<!-- 审核终止 -->
|
||||
<!-- <el-button :disabled="isAudit" size="small" type="primary" round @click="handleQCState(7)">-->
|
||||
<!-- {{ $t('trials:audit:button:auditFailed') }}-->
|
||||
|
|
@ -905,7 +921,7 @@
|
|||
</div>
|
||||
<!--petct临床数据预览-->
|
||||
<el-dialog v-if="petVisible" :show-close="true" :visible.sync="petVisible" append-to-body>
|
||||
<uploadPetClinicalData :subject-visit-id="data.Id" :data="data" :studyData="rowData" :allow-add-or-edit="false" />
|
||||
<uploadPetClinicalData :subject-visit-id="data.Id" :data="data" :studyData="rowData" :allow-add-or-edit="false" :isPatientFormAllowEdit="!isAudit && SecondReviewState == 0"/>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -2129,6 +2145,43 @@ export default {
|
|||
this.$refs['signForm'].btnLoading = false
|
||||
})
|
||||
},
|
||||
async skipTask() {
|
||||
try {
|
||||
let res = await getNextIQCQuality({
|
||||
trialId: this.trialId,
|
||||
SubjectId: this.data.SubjectId,
|
||||
SubjectVisitId: this.data.Id,
|
||||
IsSkipCurrentVisit: true
|
||||
})
|
||||
|
||||
if (res.Result && res.Result.VisitId) {
|
||||
let confirm = await this.$confirm(
|
||||
this.$t('trials:qcQuality:title:title2', '', {
|
||||
showCancelButton: false,
|
||||
})
|
||||
)
|
||||
if (confirm !== 'confirm') return
|
||||
await collectNextIQCQuality({
|
||||
trialId: this.trialId,
|
||||
SubjectId: this.data.SubjectId,
|
||||
SubjectVisitId: this.data.Id,
|
||||
IsSkipCurrentVisit: true
|
||||
})
|
||||
this.$emit('getList')
|
||||
this.$emit('nextTask', res.Result.VisitId)
|
||||
} else {
|
||||
// 没有后续质控任务
|
||||
this.$emit('getList')
|
||||
let confirm = await this.$confirm(this.$t('trials:qcQuality:title:closeQCDialog'))
|
||||
if (confirm !== 'confirm') return
|
||||
this.$emit('close')
|
||||
}
|
||||
} catch(e) {
|
||||
console.log(e)
|
||||
this.$emit('getList')
|
||||
}
|
||||
|
||||
},
|
||||
getNextQCInfo() {
|
||||
// '是否确认进入下一个质控任务?'
|
||||
var message = this.$t('trials:qcQuality:title:title2')
|
||||
|
|
|
|||
|
|
@ -732,7 +732,8 @@ export default {
|
|||
// 申请影像退回
|
||||
async imageBack(row) {
|
||||
try {
|
||||
this.$prompt(`<span style="color:#F56C6C">*</span><span>${this.$t("trials:crcUpload:confirmMessage:ApplyReason")}</span>`, this.$t("trials:crcUpload:confirmMessage:imageBack"), {
|
||||
let title = `${this.$t('trials:crcUpload:confirmMessage:imageBackTitle').replace('xxx', row.SubjectCode).replace('yyy', row.VisitName)}`
|
||||
this.$prompt(`<p style="margin-left: 5px;">${title}</p><span style="color:#F56C6C">*</span><span>${this.$t("trials:crcUpload:confirmMessage:ApplyReason")}</span>`, this.$t("trials:crcUpload:confirmMessage:imageBack"), {
|
||||
confirmButtonText: this.$t("common:button:save"),
|
||||
cancelButtonText: this.$t("common:button:cancel"),
|
||||
dangerouslyUseHTMLString: true,
|
||||
|
|
|
|||
Loading…
Reference in New Issue