Compare commits

..

46 Commits

Author SHA1 Message Date
wangxiaoshuang c339adb73f Merge branch 'main' into uat_us
# Conflicts:
#	src/views/trials/trials-panel/reading/dicoms3D/components/customize/ReportPage.vue
2026-04-27 16:47:23 +08:00
wangxiaoshuang 5cbd13cc3d 1
continuous-integration/drone/push Build is passing Details
2026-04-27 16:14:43 +08:00
wangxiaoshuang 7dcc524bd3 切换分割与ecrf表时显示全部分割标记
continuous-integration/drone/push Build is passing Details
2026-04-27 15:37:59 +08:00
wangxiaoshuang bc4639a238 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is passing Details
2026-04-27 13:12:39 +08:00
wangxiaoshuang 0f25958d9b 分割部分问题解决 2026-04-27 13:12:35 +08:00
caiyiling ff0d204612 融合视口新增定位工具
continuous-integration/drone/push Build is passing Details
2026-04-27 11:49:07 +08:00
wangxiaoshuang 91386589ae 删除分组后,会出现分段的气泡窗口
continuous-integration/drone/push Build is passing Details
2026-04-27 11:16:03 +08:00
wangxiaoshuang b2a9f279a1 窗口联动关闭 2026-04-27 11:15:49 +08:00
wangxiaoshuang b4fc5d970f 项目列表查询条件
continuous-integration/drone/push Build is passing Details
2026-04-24 17:06:36 +08:00
wangxiaoshuang e046bffdbd EICS关于页面 2026-04-24 16:55:14 +08:00
wangxiaoshuang 38ea9ddb0b Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is passing Details
2026-04-24 16:49:46 +08:00
wangxiaoshuang 1194e56139 对于问题反馈列表增加的状态增加:无法解决。 2026-04-24 16:49:35 +08:00
caiyiling 741c723bab Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is passing Details
2026-04-24 16:10:51 +08:00
caiyiling fea0047f58 数据同步更改 2026-04-24 16:10:35 +08:00
wangxiaoshuang 5a943fbefe 分割问题解决
continuous-integration/drone/push Build is passing Details
2026-04-24 16:03:23 +08:00
caiyiling 6c88af0a03 质控更改
continuous-integration/drone/push Build is passing Details
2026-04-24 15:52:07 +08:00
caiyiling 26412931a9 样式冲突更改
continuous-integration/drone/push Build is passing Details
2026-04-24 14:49:32 +08:00
wangxiaoshuang 5a1b64a023 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is passing Details
# Conflicts:
#	src/components/Dicom/DicomViewer.vue
2026-04-24 14:41:39 +08:00
wangxiaoshuang 64d8c853b3 质控像素匿名 2026-04-24 14:37:36 +08:00
caiyiling 0261fb04c6 1
continuous-integration/drone/push Build is passing Details
2026-04-24 14:33:16 +08:00
caiyiling ab372a9c29 PT临床数据更改
continuous-integration/drone/push Build is running Details
2026-04-24 14:30:36 +08:00
caiyiling f7dd68f7f3 pt临床数据更改
continuous-integration/drone/push Build is passing Details
2026-04-23 16:32:25 +08:00
wangxiaoshuang 946203f124 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web
continuous-integration/drone/push Build is passing Details
# Conflicts:
#	src/components/Dicom/DicomViewer.vue
#	src/views/dicom-show/dicom-study.vue
2026-04-23 15:53:04 +08:00
wangxiaoshuang f0eb814492 质控数据匿名 2026-04-23 15:50:15 +08:00
caiyiling 6c36c48b82 自动切换任务更改;PT序列支持修改检查的患者数据
continuous-integration/drone/push Build is passing Details
2026-04-23 11:39:07 +08:00
wangxiaoshuang 47b05526a2 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web 2026-04-22 15:06:47 +08:00
wangxiaoshuang f7afca6e73 注释(未完成) 2026-04-22 15:06:39 +08:00
caiyiling 04aa61658c Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is passing Details
2026-04-22 13:41:44 +08:00
caiyiling 86c506bc1d 非dicom阅片页面快捷键功能补充 2026-04-22 13:41:03 +08:00
wangxiaoshuang 06959b4465 双屏问题修复
continuous-integration/drone/push Build is running Details
2026-04-22 13:37:57 +08:00
caiyiling 838ab975cc Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is passing Details
2026-04-22 11:09:56 +08:00
caiyiling 7aa33672a9 质控更改 2026-04-22 11:08:48 +08:00
wangxiaoshuang 81297954d3 查看历史访视黑屏问题
continuous-integration/drone/push Build is passing Details
2026-04-22 11:01:12 +08:00
wangxiaoshuang 25e348f428 上传问题解决
continuous-integration/drone/push Build is passing Details
2026-04-22 10:15:08 +08:00
caiyiling c101302cca 指控添加跳过功能
continuous-integration/drone/push Build is passing Details
2026-04-21 17:04:57 +08:00
caiyiling c4fd8c3ea3 融合视口crosshairsTool更改;数据同步页面更改;项目中培训文档路径更改
continuous-integration/drone/push Build is passing Details
2026-04-21 16:49:40 +08:00
caiyiling a7e7419a20 Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is failing Details
2026-04-21 13:16:14 +08:00
caiyiling 55da1d9219 数据同步 2026-04-21 13:15:23 +08:00
wangxiaoshuang 2a842fd2f8 上传影像添加字段
continuous-integration/drone/push Build is running Details
2026-04-21 13:15:18 +08:00
caiyiling ca2e9e0253 数据同步列表更改
continuous-integration/drone/push Build is passing Details
2026-04-21 11:14:00 +08:00
caiyiling b4c06990f6 审批信息补充
continuous-integration/drone/push Build is passing Details
2026-04-21 10:17:49 +08:00
caiyiling 599fad48ba Merge branch 'main' of https://gitea.frp.extimaging.com/XCKJ/irc_web into main
continuous-integration/drone/push Build is passing Details
2026-04-21 09:33:26 +08:00
caiyiling 72b5d515ba 融合页面crosshairsTool更改 2026-04-21 09:32:41 +08:00
wangxiaoshuang 608bd42836 查看历史访视在新窗口打开
continuous-integration/drone/push Build is passing Details
2026-04-20 17:59:45 +08:00
caiyiling 040f66f182 定圆工具更改及国际化全局替换IRC逻辑更改
continuous-integration/drone/push Build is passing Details
2026-04-20 15:20:46 +08:00
wangxiaoshuang 7afbc360e5 新增的分段后,点击计算长短径保存,刷新页面后计算值全部变为0
continuous-integration/drone/push Build is passing Details
2026-04-17 15:52:55 +08:00
69 changed files with 5094 additions and 2444 deletions

1496
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -407,4 +407,20 @@ export function changeSegmentationSavedStatus(data) {
method: 'post',
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
})
}

View File

@ -4430,4 +4430,22 @@ export function updateImageResizePath(data) {
method: 'post',
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
})
}

View File

@ -15,7 +15,7 @@
<div v-show="dicomInfo.thick">Slice Thickness {{ dicomInfo.thick }}mm</div>
<div>WW/WC {{ dicomInfo.wwwc }}</div>
<div>Zoom {{ dicomInfo.zoom }}</div>
<div v-show="dicomInfo.location">Location {{ dicomInfo.location }}mm</div> -->
<div v-show="dicomInfo.location">Location {{ dicomInfo.location }}mm</div>-->
<!-- <div v-show="toolState.clipPlaying">FPS {{ dicomInfo.fps }}</div> -->
<div v-show="mousePosition.mo">
Pos: {{ mousePosition.x ? mousePosition.x.toFixed(0) : '' }}, {{ mousePosition.y ? mousePosition.y.toFixed(0) :
@ -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,24 +50,17 @@
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> -->
</div>-->
<div v-show="dicomInfo.location">Location: {{ dicomInfo.location }}</div>
<div v-show="dicomInfo.thick">Slice Thickness: {{ dicomInfo.thick }}mm</div>
<div v-show="dicomInfo.wwwc">WW/WL: {{ dicomInfo.wwwc }}</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. imageIdimageId
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

View File

@ -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;
}

View File

@ -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-else style="color: #f44336">N/A, </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,152 +225,204 @@
<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>
<span :class="[scope.row.dicomInfo.patientName ? '' : 'colorOfRed']">
{{
scope.row.dicomInfo.patientName
? scope.row.dicomInfo.patientName
: 'N/A'
scope.row.dicomInfo.patientName
? scope.row.dicomInfo.patientName
: 'N/A'
}}
</span>
</div>
<div>
<span :class="[scope.row.dicomInfo.patientSex ? '' : 'colorOfRed']">
{{
scope.row.dicomInfo.patientSex
? scope.row.dicomInfo.patientSex
: 'N/A'
scope.row.dicomInfo.patientSex
? scope.row.dicomInfo.patientSex
: 'N/A'
}},
</span>
<span :class="[scope.row.dicomInfo.patientAge ? '' : 'colorOfRed']">
{{
scope.row.dicomInfo.patientAge
? scope.row.dicomInfo.patientAge
: 'N/A'
scope.row.dicomInfo.patientAge
? scope.row.dicomInfo.patientAge
: 'N/A'
}},
</span>
<span :class="[
<span
:class="[
scope.row.dicomInfo.patientBirthDate ? '' : 'colorOfRed',
]">
]"
>
{{
scope.row.dicomInfo.patientBirthDate
? scope.row.dicomInfo.patientBirthDate
: 'N/A'
scope.row.dicomInfo.patientBirthDate
? scope.row.dicomInfo.patientBirthDate
: 'N/A'
}}
</span>
</div>
</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 }}/{{
scope.row.dicomInfo.fileCount
scope.row.dicomInfo.fileCount
}}
({{
(scope.row.dicomInfo.uploadFileSize / 1024 / 1024).toFixed(3)
(scope.row.dicomInfo.uploadFileSize / 1024 / 1024).toFixed(3)
}}MB/{{
(scope.row.dicomInfo.fileSize / 1024 / 1024).toFixed(3)
(scope.row.dicomInfo.fileSize / 1024 / 1024).toFixed(3)
}}MB)
</span>
</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>&nbsp;</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>&nbsp;</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>&nbsp;</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--
@ -673,7 +801,7 @@ export default {
var validFilesCount = 0
scope.uploadQueues = []
for (var i = 0; i < checkFiles.length; ++i) {
; (function (index) {
;(function (index) {
p = p.then(function () {
if (
checkFiles[index].name.toUpperCase().indexOf('DICOMDIR') === -1
@ -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,23 +1409,27 @@ export default {
dicomInfo.failedFileCount++
Record.FileCount++
} else {
let path = `/${params.trialId}/Image/${params.subjectId
}/${params.subjectVisitId}/${dicomInfo.visitTaskId
}/${scope.getGuid(
dicomInfo.studyUid +
let path = `/${params.trialId}/Image/${
params.subjectId
}/${params.subjectVisitId}/${
dicomInfo.visitTaskId
}/${scope.getGuid(
dicomInfo.studyUid +
v.seriesUid +
o.instanceUid +
params.trialId
)}`
)}`
if (scope.IsImageSegment) {
path = `/${params.trialId}/Image/${params.subjectId
}/${params.subjectVisitId}/AnnotationImage/${dicomInfo.visitTaskId
}/${scope.getGuid(
dicomInfo.studyUid +
path = `/${params.trialId}/Image/${
params.subjectId
}/${params.subjectVisitId}/AnnotationImage/${
dicomInfo.visitTaskId
}/${scope.getGuid(
dicomInfo.studyUid +
v.seriesUid +
o.instanceUid +
params.trialId
)}`
)}`
}
if (scope.isClose) return
let res = await dcmUpload(
@ -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,20 +1637,16 @@ export default {
}
let OSSclient = scope.OSSclient
try {
let seriesRes = await OSSclient.put(
thumbnailPath,
blob,
{
fileName: `${v.seriesUid}.jpg`,
fileSize: blob.size,
fileType: 'image/jpeg',
uploadBatchId: uploadBatchId,
batchDataType: 6,
trialId: params.trialId,
subjectId: params.subjectId,
subjectVisitId: params.subjectVisitId
}
)
let seriesRes = await OSSclient.put(thumbnailPath, blob, {
fileName: `${v.seriesUid}.jpg`,
fileSize: blob.size,
fileType: 'image/jpeg',
uploadBatchId: uploadBatchId,
batchDataType: 6,
trialId: params.trialId,
subjectId: params.subjectId,
subjectVisitId: params.subjectVisitId,
})
if (seriesRes && seriesRes.url) {
o.ImageResizePath = scope.$getObjectName(seriesRes.url)
}
@ -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')
},

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="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

View File

@ -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)

View File

@ -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 = []

View File

@ -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 {
@ -378,4 +376,4 @@ body .el-table th.gutter {
height: 20px !important;
vertical-align: -0.4em !important;
cursor: pointer;
}
}

View File

@ -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';
} from "./extractPositioningFromDataset";
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;
}
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");
// 场景1trials/dicomsimageId 带 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;
}
}
}
}
// 场景2src/components/DicomimageId 仅带 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) {
// 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=');
// 兼容 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 = [];
if (frameIndex !== -1) {
const frameStr = url.substr(frameIndex + 6);
parts.forEach((part) => {
const eqIndex = part.indexOf("=");
const key = eqIndex === -1 ? part : part.slice(0, eqIndex);
const value = eqIndex === -1 ? "" : part.slice(eqIndex + 1);
frame = parseInt(frameStr, 10);
url = url.substr(0, frameIndex - 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: imageId.substr(0, firstColonIndex),
url,
frame,
};
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);

View File

@ -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

View File

@ -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()
}

View File

@ -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},`
}

View File

@ -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,8 +447,11 @@ export default {
this.seriesList = seriesList
if (this.seriesList.length > 0) {
this.loadAllImages()
this.$refs.dicomViewer.loadImageStack(this.seriesList[0], this.labels[this.tpCode])
this.firstInstanceId = this.seriesList[0].imageIds[0]
if (isJump) {
this.$refs.dicomViewer.loadImageStack(this.seriesList[0], this.labels[this.tpCode])
this.firstInstanceId = this.seriesList[0].imageIds[0]
}
}
}
} catch (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) {

View File

@ -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>
@ -434,7 +273,7 @@ export default {
}
},
//
report() {},
report() { },
//
handleSelectChange(selection) {
// console.log(selection, "handleSelectChange");
@ -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;
}

View File

@ -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">

View File

@ -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')

View File

@ -313,6 +313,10 @@ export default {
isExistsClinicalData: {
type: Boolean,
required: true
},
imageToolType: {
type: Number,
required: true
}
},
data() {
@ -549,9 +553,9 @@ export default {
this.signVisible = false
// window.location.reload()
// 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')

View File

@ -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) {

View File

@ -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()

View File

@ -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
)
instanceMetadata.RadiopharmaceuticalStartDateTime = dateString
}
if (
instanceMetadata.AcquisitionDate &&
instanceMetadata.AcquisitionDate !== undefined &&
typeof instanceMetadata.AcquisitionDate !== 'string'
) {
const dateString = convertInterfaceDateToString(
instanceMetadata.AcquisitionDate
)
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,
'RadiopharmaceuticalStartDateTime',
toDicomDateString(radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime)
)
assignIfPresent(instanceMetadata, 'RadiopharmaceuticalStartTime', startTime)
assignIfPresent(instanceMetadata, 'FrameReferenceTime', ptImageModule.frameReferenceTime)
assignIfPresent(instanceMetadata, 'ActualFrameDuration', ptImageModule.actualFrameDuration)
assignIfPresent(
instanceMetadata,
'PatientSex',
firstValue(ptClinicalData?.PatientSex, patientStudyModule.patientSex)
)
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
}

View File

@ -1,17 +1,31 @@
import { utilities as csUtils } from '@cornerstonejs/core'
const scalingPerImageId = {}
function addInstance(imageId, scalingMetaData) {
const imageURI = csUtils.imageIdToURI(imageId)
scalingPerImageId[imageURI] = scalingMetaData
}
function get(type, imageId) {
if (type === 'scalingModule') {
const imageURI = csUtils.imageIdToURI(imageId)
return scalingPerImageId[imageURI]
}
}
export default { addInstance, get }
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) {
// 统一缩放元数据的 key忽略 frame 参数,避免 getCurrentImageId() 带 frame 导致查不到 scalingModule
const imageURI = normalizeImageURI(csUtils.imageIdToURI(imageId))
scalingPerImageId[imageURI] = scalingMetaData
}
function get(type, imageId) {
if (type === 'scalingModule') {
const imageURI = normalizeImageURI(csUtils.imageIdToURI(imageId))
return scalingPerImageId[imageURI]
}
}
export default { addInstance, get }

View File

@ -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 })

View File

@ -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'))
}

View File

@ -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

View File

@ -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>

View File

@ -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) {

View File

@ -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,

View File

@ -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

View File

@ -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 })
})

View File

@ -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){

View File

@ -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')

View File

@ -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();
}
});
}
}

View File

@ -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) {

View File

@ -211,21 +211,23 @@ export default {
this.mousePosition.index = []
this.mousePosition.value = null
})
document.addEventListener('mouseup', () => {
this.sliderMouseup()
if (this.isMip) {
this.rotateBarMouseup()
}
})
document.addEventListener('mousemove', (e) => {
this.sliderMousemove(e)
if (this.isMip) {
this.rotateBarMousemove(e)
}
})
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(e)
}
},
handleDocumentMouseMove(e) {
this.sliderMousemove(e)
if (this.isMip) {
this.rotateBarMousemove(e)
}
},
stackNewImage(e) {
const { detail } = e
if (this.series) {
@ -670,30 +672,32 @@ 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([{
volumeId: this.volumeId,
callback: (r) => {
if (this.series.Modality === 'NM') {
setMipTransferFunctionForVolumeActor({ ...r, volumeId: this.volumeId })
} else {
setPetTransferFunctionForVolumeActor(r)
}
// setPetColorMapTransferFunctionForVolumeActor(r)
console.log("mip渲染成功")
},
slabThickness,
blendMode: BlendModes.MAXIMUM_INTENSITY_BLEND,
defaultOptions: {
orientation: OrientationAxis.CORONAL
)
await viewport.setVolumes([{
volumeId: this.volumeId,
callback: (r) => {
if (this.series.Modality === 'NM') {
setMipTransferFunctionForVolumeActor({ ...r, volumeId: this.volumeId })
} else {
setPetTransferFunctionForVolumeActor(r)
}
}])
console.log("mip渲染成功")
},
slabThickness,
blendMode: BlendModes.MAXIMUM_INTENSITY_BLEND,
defaultOptions: {
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>

View File

@ -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) {
toolGroup.setToolDisabled(this.activeTool)
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) {
toolGroup.setToolDisabled(this.activeTool)
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) {
toolGroup.setToolDisabled(this.activeTool)
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) {
toolGroup.setToolDisabled(this.activeTool)
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) {
toolGroup.setToolDisabled(this.activeTool)
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) {
toolGroup.setToolDisabled(this.activeTool)
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) {
toolGroup.setToolDisabled(this.activeTool)
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)
// 123490590
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')

View File

@ -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

View File

@ -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') {

View File

@ -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);

View File

@ -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%;

View File

@ -59,10 +59,10 @@
<div v-if="series" class="right-bottom-text">
<div v-show="imageInfo.location">Location: {{
`${Number(imageInfo.location).toFixed(digitPlaces)} mm`
}}</div>
}}</div>
<div v-show="imageInfo.sliceThickness">Slice Thickness: {{
`${Number(imageInfo.sliceThickness).toFixed(digitPlaces)} mm`
}}</div>
}}</div>
<div v-show="imageInfo.wwwc">WW/WL: {{ imageInfo.wwwc }}</div>
</div>
<div class="orientation-top">
@ -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)

View File

@ -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 = {

View File

@ -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]"
@ -130,10 +145,10 @@
v-else-if="questionForm[scope.row.QuestionId] instanceof Array && questionForm[scope.row.QuestionId][scope.row.xfIndex] && (scope.row.Type === 'select' || scope.row.Type === 'radio')"
>
{{
Array.isArray(questionForm[scope.row.QuestionId][scope.row.xfIndex][scope.row.TableQuestionId])
?
questionForm[scope.row.QuestionId][scope.row.xfIndex][scope.row.TableQuestionId].join(',') :
questionForm[scope.row.QuestionId][scope.row.xfIndex][scope.row.TableQuestionId]
Array.isArray(questionForm[scope.row.QuestionId][scope.row.xfIndex][scope.row.TableQuestionId])
?
questionForm[scope.row.QuestionId][scope.row.xfIndex][scope.row.TableQuestionId].join(',') :
questionForm[scope.row.QuestionId][scope.row.xfIndex][scope.row.TableQuestionId]
}}
</span>
<el-select
@ -165,11 +180,10 @@
: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 }}
scope.row.Unit) : scope.row.CustomUnit }}
</template>
<template v-else-if="scope.row.ValueType === 2" slot="append">%</template>
</el-input>
@ -209,11 +223,10 @@
: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 }}
scope.row.Unit) : scope.row.CustomUnit }}
</template>
<template v-else-if="scope.row.ValueType === 2" slot="append">%</template>
</el-input>
@ -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">
@ -277,11 +282,10 @@
</template>
<template v-else-if="scope.row.ValueType === 2">
{{ isNaN(parseInt(scope.row.Answers[task.VisitTaskId])) ?
scope.row.Answers[task.VisitTaskId] : `${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" />
@ -385,7 +387,7 @@ export default {
handler(v, oldv) {
try {
if (!v[this.questionId] || !oldv[this.questionId]) return
} catch (e) {}
} catch (e) { }
this.formItemNumberChange(this.questionId, false)
},
},
@ -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) => {

View File

@ -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')
}
})

View File

@ -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: {

View File

@ -136,7 +136,9 @@ class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool {
);
triggerAnnotationRenderForViewportIds(viewportIdsToRender);
triggerAnnotationCompleted(annotation);
setTimeout(() => {
triggerAnnotationCompleted(annotation);
}, 0);
return annotation;
};

View File

@ -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

View File

@ -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')

View File

@ -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"

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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') }}

View File

@ -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>

View File

@ -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

View File

@ -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({

View File

@ -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

View File

@ -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
this.searchData.SubjectCode = this.rowInfo.SubjectCode
this.searchData.VisitName = this.rowInfo.VisitName
this.searchData.StudyCode = this.rowInfo.StudyCode
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

View File

@ -148,7 +148,7 @@
/>
</el-tab-pane>
</el-tabs>
</el-dialog>
</el-dialog>
</BaseContainer>
</template>
<script>
@ -177,7 +177,7 @@ const searchDataDefault = () => {
Asc: true,
SortField: 'StudyCode'
}
}
}
export default {
components: { BaseContainer, Pagination, FileList, TaskList },
data() {

View File

@ -178,9 +178,11 @@ export default {
try {
this.loading = true
this.searchData.TrialId = this.$route.query.trialId
this.searchData.StudyCode = this.rowInfo.StudyCode
this.searchData.SubjectCode = this.rowInfo.SubjectCode
this.searchData.VisitName = this.rowInfo.VisitName
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

View File

@ -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,15 +86,62 @@ 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-tab-pane {
> .el-tabs__content > .el-tab-pane {
height: 100%;
}
}
</style>
.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%;
}
}
</style>

View File

@ -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>

View File

@ -1022,8 +1022,9 @@ export default {
methods: {
// 退
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"), {
try {
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,

View File

@ -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>
@ -149,7 +160,7 @@
<!-- 预览PET-CT数据 -->
<el-button type="primary" icon="el-icon-document tip-i" :title="$t('trials:audit:tab:clinicalData')"
v-if="
['PT、CT', 'CT、PT', 'PET-CT'].includes(
['PT、CT', 'CT、PT', 'PET-CT'].includes(
scope.row.Modalities
) && IsHaveStudyClinicalData
" circle :disabled="scope.row.IsDeleted" @click="handlePreviewClinicalData(scope.row)" />
@ -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')

View File

@ -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,