阅片页面更改

uat
caiyiling 2025-04-01 16:48:04 +08:00
parent 4188c0110f
commit 0960b480a6
10 changed files with 3006 additions and 5 deletions

View File

@ -277,7 +277,7 @@ export default {
}
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.preview-wrapper{
display: flex;
flex-direction: row !important;

View File

@ -1,7 +1,10 @@
<template>
<div ref="container" style="width:100%;height:100%" class="dicom-container">
<!-- 访视阅片 -->
<div v-if="(isReadingTaskViewInOrder === 1 || ((isReadingTaskViewInOrder !== 1) && isShow)) && readingCategory=== 1 && CriterionType !== 0" class="reading-wrapper">
<div v-if="readingCategory=== 1 && (CriterionType === 7)" class="reading-wrapper">
<VisitReview />
</div>
<div v-else-if="(isReadingTaskViewInOrder === 1 || ((isReadingTaskViewInOrder !== 1) && isShow)) && readingCategory=== 1 && CriterionType !== 0" class="reading-wrapper">
<el-tabs v-model="activeName" v-loading="loading" :before-leave="beforeLeave">
<!-- 阅片 -->
<el-tab-pane :label="$t('trials:reading:tabTitle:review')" name="read">
@ -29,7 +32,7 @@
</el-tab-pane>
</el-tabs>
</div>
<div v-if="(isReadingTaskViewInOrder === 1 || ((isReadingTaskViewInOrder !== 1) && isShow)) && readingCategory=== 1 && CriterionType === 0" class="reading-wrapper">
<div v-else-if="(isReadingTaskViewInOrder === 1 || ((isReadingTaskViewInOrder !== 1) && isShow)) && readingCategory=== 1 && CriterionType === 0" class="reading-wrapper">
<el-tabs v-model="activeName" v-loading="loading" :before-leave="beforeLeaveCustomize">
<!-- 阅片 -->
<el-tab-pane :label="$t('trials:reading:tabTitle:review')" name="read">
@ -171,6 +174,7 @@
</template>
<script>
import { getNextTask, readClinicalData, verifyDefaultQuestionBeAnswer } from '@/api/trials'
import VisitReview from './../dicoms3D/components/VisitReview'
import ReadPage from './components/ReadPage'
import CustomizeReadPage from './customize/CustomizeReadPage'
import ReportPage from './components/ReportPage'
@ -187,6 +191,7 @@ import requestPoolManager from '@/utils/request-pool'
export default {
name: 'Reading',
components: {
VisitReview,
ReadPage,
ReportPage,
GlobalReview,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,273 @@
<template>
<div v-loading="loading" class="study-wrapper">
<div class="study-info">
<div
v-if="taskInfo && taskInfo.IsReadingShowSubjectInfo"
:title="taskInfo.SubjectCode"
>
{{ taskInfo.SubjectCode }}
</div>
<div
v-if="taskInfo && taskInfo.IsReadingShowSubjectInfo"
:title="visitTaskInfo.TaskBlindName"
>
{{ visitTaskInfo.TaskBlindName }}
</div>
</div>
<div class="ps">
<el-collapse v-model="activeNames">
<el-collapse-item v-for="(study, index) in studyList" :key="`${study.StudyId}`" :name="`${study.StudyId}`">
<template slot="title">
<div
v-if="!study.IsCriticalSequence"
class="dicom-desc"
>
<template v-if="taskInfo && taskInfo.IsShowStudyName">
<div style="text-overflow: ellipsis;overflow: hidden;">
<span :title="study.StudyCode">{{ study.StudyCode }}</span>
<span v-if="study.StudyName" :title="study.StudyName" style="margin-left: 5px;">{{ study.StudyName }}</span>
<span v-else :title="study.Modalities" style="margin-left: 5px;">{{ `${study.Modalities} (${study.SeriesCount})` }}</span>
</div>
<div style="text-overflow: ellipsis;overflow: hidden;" v-if="study.StudyName" >
<span :title="study.Modalities">{{ `${study.Modalities} (${study.SeriesCount})` }}</span>
</div>
</template>
<template v-else-if="taskInfo && !taskInfo.IsShowStudyName">
<div style="text-overflow: ellipsis;overflow: hidden;">
<span :title="study.StudyCode">{{ study.StudyCode }}</span>
<span :title="study.Modalities">{{ `${study.Modalities} (${study.SeriesCount})` }}</span>
</div>
</template>
</div>
<div v-else>
<!-- 关键序列 -->
{{ $t('trials:reading:title:keySeries') }}
</div>
</template>
<div class="dicom-list-container">
<div
v-for="(series, i) in study.SeriesList"
:key="i"
style="position:relative;margin-top:1px;"
@click="activeSeries(series, i, index)"
>
<div
:class="{'series-active': index === activeStudyIndex && i === activeSeriesIndex}"
class="series-wrapper"
>
<div class="series-image">
<el-image
style="width: 100%;height: 100%;"
:src="`${OSSclientConfig.basePath}${series.ImageResizePath}`"
fit="fill"
crossorigin="anonymous"
/>
</div>
<div class="series-text" >
<div class="text-desc" v-if="!study.IsCriticalSequence" :title="series.SeriesNumber">
#{{ series.SeriesNumber }}
</div>
<div class="text-desc" v-if="series.Description" :title="series.Description">
{{ series.Description }}
</div>
<div class="text-desc" v-if="series.SliceThickness && !study.IsCriticalSequence">
T: {{ parseFloat(series.SliceThickness).toFixed(digitPlaces) }}
</div>
<div class="text-desc">
<span v-show="series.LoadedImageCount < series.InstanceCount">
{{ series.Modality }}: {{ series.LoadedImageCount }}/{{ series.InstanceCount }} image
</span>
<span v-show="series.LoadedImageCount >= series.InstanceCount">{{ series.Modality }}: {{ series.InstanceCount }} image</span>
</div>
<div v-show="series.IsBeMark">
<i class="el-icon-star-on" style="font-size: 16px;color: #ff5722;" />
</div>
</div>
</div>
<div v-if="series.IsDicom && series.LoadedImageProgress>0 && series.LoadedImageProgress<100" style="width: 100%;">
<el-progress
:percentage="parseInt((series.LoadedImageProgress).toFixed(2))"
/>
</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script>
export default {
name: 'StudyList',
props: {
visitTaskInfo: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loading: false,
activeNames: [],
activeStudyIndex: -1,
activeSeriesIndex: -1,
taskInfo: null,
studyList: [],
digitPlaces: 2
}
},
mounted() {
this.taskInfo = JSON.parse(localStorage.getItem('taskInfo'))
let digitPlaces = Number(localStorage.getItem('digitPlaces'))
this.digitPlaces = digitPlaces === -1 ? this.digitPlaces : digitPlaces
this.studyList = this.visitTaskInfo.StudyList
if (this.studyList.length === 0) return
this.$nextTick(() => {
this.activeStudy(this.studyList[0].StudyId)
})
},
methods: {
activeSeries(series, seriesIndex, studyIndex) {
this.activeStudyIndex = studyIndex
this.activeSeriesIndex = seriesIndex
this.$emit('activeSeries', series)
},
activeStudy(id) {
if (this.activeNames.indexOf(id) > -1) return
this.activeNames.push(id)
},
setSeriesActive(studyIndex, seriesIndex) {
this.activeStudyIndex = studyIndex
this.activeSeriesIndex = seriesIndex
let studyId = this.studyList[studyIndex].StudyId
if (!studyId) return
this.activeStudy(studyId)
},
getPreviousOrNextSeries(type, series) {
let seriseList = this.studyList.map(s => s.SeriesList).flat()
let i = seriseList.findIndex(i => i.Id === series.Id && i.StudyId === series.StudyId)
if (i === -1) return
let newIndex = null
if (type === -1) {
newIndex = i === 0 ? i : i - 1
} else {
newIndex = i >= seriseList.length - 1 ? i : i +1
}
let studyIndex = seriseList[newIndex].StudyIndex
let seriesIndex = seriseList[newIndex].SeriesIndex
this.setSeriesActive(studyIndex, seriesIndex)
this.activeSeries(seriseList[newIndex], seriesIndex, studyIndex)
}
}
}
</script>
<style lang="scss" scoped>
.study-wrapper{
width:100%;
height: 100%;
overflow-y: hidden;
overflow-x: hidden;
display: flex;
flex-direction: column;
.study-info {
font-size: 16px;
font-weight: bold;
color: #ddd;
padding: 5px 0px;
margin: 0;
text-align: center;
background-color: #4c4c4c;
height: 50px;
}
.dicom-desc{
font-weight: bold;
font-size: 13px;
text-align: left;
color: #d0d0d0;
padding: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ps {
flex: 1;
overflow-anchor: none;
touch-action: auto;
overflow-y: auto;
}
.series-active {
background-color: #607d8b!important;
border: 1px solid #607d8b!important;
}
::v-deep.el-progress__text{
color: #ccc;
font-size: 12px;
}
.dicom-list-container{
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
.series-wrapper {
width: 100%;
padding: 5px;
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
background-color: #3a3a3a;
.el-progress__text{
display: none;
}
.el-progress-bar{
padding-right:0px;
}
.series-image {
width: 50px;
height: 50px;
}
.series-text {
flex: 1;
padding-left: 5px;
color: #ddd;
.text-desc {
width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
line-height: 16px;
}
}
}
}
::v-deep.el-collapse{
border: none;
.el-collapse-item{
background-color: #000!important;
color: #ddd;
}
.el-collapse-item__content{
padding-bottom:0px;
background-color: #000!important;
}
.el-collapse-item__header{
background-color: #000!important;
color: #ddd;
border-bottom-color:#5a5a5a;
padding-left: 5px;
// height: 50px;
line-height: 20px;
}
}
::v-deep .el-progress-bar__inner{
transition: width 0s ease;
}
}
</style>

View File

@ -0,0 +1,551 @@
<template>
<div
ref="viewport"
class="viewport-wrapper"
@mouseup="sliderMouseup"
@mousemove="sliderMousemove"
@mouseleave="sliderMouseleave"
>
<div class="left-top-text" v-if="series && taskInfo">
<div
v-if="taskInfo.IsExistsClinicalData"
class="cd-info"
:title="$t('trials:reading:button:clinicalData')"
>
<svg-icon
style="cursor: pointer;"
icon-class="documentation"
class="svg-icon"
@click.stop="viewCD(series.TaskInfo.VisitTaskId)" />
</div>
<h2
v-if="taskInfo.IsReadingShowSubjectInfo"
class="subject-info"
>
{{ `${series.TaskInfo.SubjectCode} ${series.TaskInfo.TaskBlindName} ` }}
</h2>
<div>Series: #{{ series.SeriesNumber }}</div>
<div>Image: #{{ `${series.SliceIndex + 1}/${series.ImageIds.length}` }}</div>
<div>{{series.Modality}}</div>
</div>
<div
v-if="series && taskInfo && taskInfo.IsReadingTaskViewInOrder === 1"
class="top-center-tool"
>
<div class="toggle-visit-container">
<div
class="arrw_icon"
:style="{ cursor: series.TaskInfo.VisitTaskNum !== 0 ? 'pointer' : 'not-allowed', color: series.TaskInfo.VisitTaskNum !== 0 ? '#fff': '#6b6b6b' }"
@click.stop.prevent="toggleTask($event, series.TaskInfo.VisitTaskNum, -1)"
@dblclick.stop="preventDefault($event)"
>
<i class="el-icon-caret-left" />
</div>
<div class="arrow_text">
{{ series.TaskInfo.TaskBlindName }}
</div>
<div
class="arrw_icon"
:style="{ cursor: series.TaskInfo.VisitTaskNum < taskInfo.VisitNum ? 'pointer' : 'not-allowed', color: series.TaskInfo.VisitTaskNum < taskInfo.VisitNum ? '#fff': '#6b6b6b' }"
@click.stop.prevent="toggleTask($event, series.TaskInfo.VisitTaskNum, 1)"
@dblclick.stop="preventDefault($event)"
>
<i class="el-icon-caret-right" />
</div>
</div>
</div>
<div class="right-top-text" v-if="series">
<div>{{ series.Description }}</div>
</div>
<div class="left-bottom-text" v-if="series">
<div v-show="mousePosition.index.length > 0">
Pos: {{ mousePosition.index[0] }}, {{ mousePosition.index[1] }}, {{ mousePosition.index[2] }}
</div>
<div v-if="(series.Modality === 'CT' || series.Modality === 'DR' || series.Modality === 'CR') && mousePosition.value">
HU: {{ mousePosition.value }}
</div>
<div v-else-if="(series.Modality === 'PT' && mousePosition.value)">
SUVbw(g/ml): {{ digitPlaces === -1 ?mousePosition.value.toFixed(3) :mousePosition.value.toFixed(digitPlaces) }}
</div>
<div v-else-if="mousePosition.value">
Density: {{ mousePosition.value }}
</div>
<div v-show="imageInfo.size">
W*H: {{ imageInfo.size }}
</div>
</div>
<div class="right-bottom-text" v-if="series">
<div v-show="imageInfo.location">Location: {{ `${Number(imageInfo.location).toFixed(digitPlaces)} mm` }}</div>
<div v-show="series.SliceThickness">Slice Thickness: {{ `${Number(series.SliceThickness).toFixed(digitPlaces)} mm` }}</div>
<div v-show="imageInfo.wwwc ">WW/WL: {{ imageInfo.wwwc }}</div>
</div>
<div class="orientation-top">
{{ markers.top }}
</div>
<div class="orientation-right">
{{ markers.right }}
</div>
<div class="orientation-bottom">
{{ markers.bottom }}
</div>
<div class="orientation-left">
{{ markers.left }}
</div>
<div ref="sliderBox" class="right-slider-box" @click.stop="clickSlider($event)">
<div :style="{top: sliderInfo.height + '%'}" class="slider" @click.stop.prevent="() => {return}" @mousedown.stop="sliderMousedown($event)" />
</div>
</div>
</template>
<script>
import * as cornerstonejs from '@cornerstonejs/core'
import {
RenderingEngine,
Enums,
imageLoader,
metaData,
getRenderingEngine,
eventTarget,
utilities as csUtils
} from '@cornerstonejs/core'
import * as cornerstoneTools from '@cornerstonejs/tools'
import { vec3, mat4 } from 'gl-matrix'
export default {
name: 'Viewport',
props: {
renderingEngineId: {
type: String,
required: true
},
viewportId: {
type: String,
required: true
},
viewportIndex: {
type: Number,
required: true
}
},
data(){
return {
element: '',
series: null,
taskInfo: null,
sliderInfo: {
oldB: null,
oldM: null,
isMove: false,
height: 0
},
mousePosition: {
index: [],
value: null,
modalityUnit: '',
world: []
},
imageInfo: {
zoom: null,
size: null,
location: null,
sliceThickness: null,
wwwc: null
},
digitPlaces: 2,
orientationMarkers: [],
originalMarkers: [],
markers: { top: '', right: '', bottom: '', left: '' },
playClipState: false,
wwwcIdx: 2
}
},
mounted() {
this.taskInfo = JSON.parse(localStorage.getItem('taskInfo'))
let digitPlaces = Number(localStorage.getItem('digitPlaces'))
this.digitPlaces = digitPlaces === -1 ? this.digitPlaces : digitPlaces
this.$nextTick(()=>{
this.initViewport()
})
console.log(cornerstonejs)
},
methods: {
initViewport() {
this.element = this.$refs['viewport']
const resizeObserver = new ResizeObserver(() => {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
if (renderingEngine) {
renderingEngine.resize(true, false)
}
})
this.element.oncontextmenu = (e) => e.preventDefault()
resizeObserver.observe(this.element)
this.element.addEventListener('CORNERSTONE_STACK_NEW_IMAGE', this.stackNewImage)
this.element.addEventListener('CORNERSTONE_VOI_MODIFIED', this.voiModified)
this.element.addEventListener('CORNERSTONE_TOOLS_MOUSE_MOVE', this.cornerstoneToolsMouseMove)
this.element.addEventListener('mouseleave', () => {
this.mousePosition.index = []
})
// console.log(cornerstonejs,cornerstoneTools)
// element.addEventListener('CORNERSTONE_STACK_NEW_IMAGE', this.stackNewImage)
},
stackNewImage(e) {
const { detail } = e
this.series.SliceIndex = detail.imageIdIndex
this.sliderInfo.height = detail.imageIdIndex * 100 / (this.series.ImageIds.length - 1)
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
let zoom = viewport.getZoom()
this.imageInfo.zoom = zoom.toFixed(4)
this.imageInfo.size = `${detail.image.columns}*${detail.image.rows}`
const imagePlaneModule = metaData.get('imagePlaneModule', detail.imageId)
this.imageInfo.location = imagePlaneModule.sliceLocation
// this.imageInfo.wwwc = `${Math.round(detail.image.windowWidth)}/${Math.round(detail.image.windowCenter)}`
this.getOrientationMarker()
},
voiModified(e) {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
const properties = viewport.getProperties()
if (properties && properties.voiRange) {
var { lower, upper } = properties.voiRange
const { windowWidth, windowCenter } = csUtils.windowLevel.toWindowLevel(
lower,
upper
)
this.imageInfo.wwwc = `${Math.round(windowWidth)}/${Math.round(windowCenter)}`
}
},
getOrientationMarker() {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
const { viewUp, viewPlaneNormal } = viewport.getCamera()
const viewRight = vec3.create()
vec3.cross(viewRight, viewUp, viewPlaneNormal)
const columnCosines = [-viewUp[0], -viewUp[1], -viewUp[2]]
const rowCosines = viewRight
const rowString = cornerstoneTools.utilities.orientation.getOrientationStringLPS(rowCosines)
const columnString = cornerstoneTools.utilities.orientation.getOrientationStringLPS(columnCosines)
const oppositeRowString = cornerstoneTools.utilities.orientation.invertOrientationStringLPS(rowString)
const oppositeColumnString = cornerstoneTools.utilities.orientation.invertOrientationStringLPS(columnString)
this.markers.top = oppositeColumnString
this.markers.right = rowString
this.markers.bottom = columnString
this.markers.left = oppositeRowString
this.orientationMarkers = [oppositeColumnString, rowString, columnString, oppositeRowString]
if (this.originalMarkers.length === 0) {
this.originalMarkers = [...this.orientationMarkers]
}
},
setMarkers() {
let markers = [...this.orientationMarkers]
for (const key in this.markers) {
let v = markers.shift(0)
this.markers[key] = v
}
},
resetOrientationMarkers() {
if (this.originalMarkers.length > 0) {
console.log(this.originalMarkers)
this.orientationMarkers = [...this.originalMarkers]
this.setMarkers()
}
},
rotateOrientationMarkers(type) {
if (this.orientationMarkers.length > 0) {
if (type === 1) {
this.resetOrientationMarkers()
return
}
let markers = [...this.orientationMarkers]
if (type === 2) {
//
this.orientationMarkers[0] = markers[2]
this.orientationMarkers[2] = markers[0]
} else if (type === 3) {
//
this.orientationMarkers[1] = markers[3]
this.orientationMarkers[3] = markers[1]
} else if (type === 4) {
// 90
this.orientationMarkers = markers.slice(1, 4).concat(markers[0])
} else if (type === 5) {
// 90
this.orientationMarkers = [markers[3]].concat(markers.slice(0, 3))
}
this.setMarkers()
}
},
toggleClipPlay(isPlay, framesPerSecond) {
this.playClipState = isPlay
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
if (isPlay) {
cornerstoneTools.utilities.cine.playClip(viewport.element, { framesPerSecond })
} else {
cornerstoneTools.utilities.cine.stopClip(viewport.element)
}
},
scrollPage(type) {
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
const currentImageIdIndex = viewport.getCurrentImageIdIndex();
const numImages = viewport.getImageIds().length;
let newImageIdIndex = null;
if (type === 0) {
newImageIdIndex = 0
} else if (type === -1) {
newImageIdIndex = currentImageIdIndex === 0 ? currentImageIdIndex : currentImageIdIndex - 1
} else if (type === 1) {
newImageIdIndex = currentImageIdIndex === numImages - 1 ? currentImageIdIndex : currentImageIdIndex + 1
} else if (type === 99999) {
newImageIdIndex = numImages -1
}
viewport.setImageIdIndex(newImageIdIndex);
},
setZoom(ratio) {
const renderingEngine = getRenderingEngine(this.renderingEngineId);
const viewport = renderingEngine.getViewport(this.viewportId);
const zoom = viewport.getZoom();
if (ratio > 0) {
viewport.setZoom(zoom * 1.05);
} else {
viewport.setZoom(zoom / 1.05);
}
viewport.render();
},
resize(forceFitToWindow) {
console.log('resize: ', forceFitToWindow)
const renderingEngine = getRenderingEngine(this.renderingEngineId);
renderingEngine.resize(true, forceFitToWindow)
},
async setSeriesInfo(obj){
if (this.series && obj.Id === this.series.Id && obj.Description === this.series.Description) return
this.series = {...obj}
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
await viewport.setStack(obj.ImageIds, obj.SliceIndex)
cornerstoneTools.utilities.stackPrefetch.enable(viewport.element);
viewport.render()
},
cornerstoneToolsMouseMove(e) {
const { currentPoints } = e.detail
const worldPoint = currentPoints.world
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(this.viewportId)
const imageData = viewport.getImageData()
if (!imageData) return
const index = imageData.imageData.worldToIndex(worldPoint)
index[0] = Math.floor(index[0])
index[1] = Math.floor(index[1])
index[2] = Math.floor(index[2])
this.mousePosition.index = index
},
toggleTask(evt, visitTaskNum, i) {
this.$emit('activeViewport', this.viewportIndex)
const num = visitTaskNum + i
if (num >= 0 && num <= this.taskInfo.VisitNum) {
this.$emit('toggleTaskByViewport', {series: this.series, visitTaskNum: num})
}
evt.stopImmediatePropagation()
evt.stopPropagation()
evt.preventDefault()
},
viewCD(taskId) {
this.$emit('previewCD', taskId)
},
setWwwcIdx(idx) {
this.wwwcIdx = idx
},
clickSlider(e) {
const height = e.offsetY * 100 / this.$refs['sliderBox'].clientHeight
this.sliderInfo.height = height
let sliceIdx = Math.trunc(this.series.ImageIds.length * height / 100)
sliceIdx = sliceIdx >= this.series.ImageIds.length ? this.series.ImageIds.length - 1 : sliceIdx < 0 ? 0 : sliceIdx
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(
this.viewportId
)
viewport.setImageIdIndex(sliceIdx)
viewport.render()
},
sliderMouseup(e) {
this.sliderInfo.isMove = false
},
sliderMousedown(e) {
const boxHeight = this.$refs['sliderBox'].clientHeight
this.sliderInfo.oldB = parseInt(e.srcElement.style.top) * boxHeight / 100
this.sliderInfo.oldM = e.clientY
this.sliderInfo.isMove = true
e.stopImmediatePropagation()
e.stopPropagation()
e.preventDefault()
},
sliderMousemove(e) {
if (!this.sliderInfo.isMove) return
const delta = this.sliderInfo.oldB - (this.sliderInfo.oldM - e.clientY)
const boxHeight = this.$refs['sliderBox'].clientHeight
if (delta < 0) return
if (delta > boxHeight) return
const height = delta * 100 / boxHeight
let sliceIdx = Math.trunc(this.series.ImageIds.length * height / 100)
sliceIdx = sliceIdx >= this.series.ImageIds.length ? this.series.ImageIds.length - 1 : sliceIdx < 0 ? 0 : sliceIdx
this.sliderInfo.height = height
const renderingEngine = getRenderingEngine(this.renderingEngineId)
const viewport = renderingEngine.getViewport(
this.viewportId
)
viewport.setImageIdIndex(sliceIdx)
viewport.render()
},
sliderMouseleave(e) {
if (!this.sliderInfo.isMove) return
this.sliderInfo.isMove = false
},
preventDefault(e) {
e.stopImmediatePropagation()
e.stopPropagation()
e.preventDefault()
},
}
}
</script>
<style lang="scss" scoped>
.viewport-wrapper {
width:100%;
height:100%;
position: relative;
.left-top-text {
position: absolute;
left: 5px;
top: 5px;
color: #ddd;
z-index: 1;
font-size: 12px;
.cd-info {
color: #ddd;
font-size: 18px;
}
.subject-info {
color:#f44336;
padding: 5px 0px;
margin: 0;
}
}
.top-center-tool {
position: absolute;
left:50%;
top: 5px;
transform: translateX(-50%);
z-index: 1;
.toggle-visit-container {
display: flex;
}
.arrw_icon{
width: 20px;
height: 20px;
background-color: #3f3f3f;
text-align: center;
line-height: 20px;
border-radius: 10%;
}
.arrow_text{
height: 20px;
line-height: 20px;
background-color: #00000057;
color: #fff;
padding:0 10px;
font-size: 14px;
}
}
.right-top-text {
position: absolute;
right: 5px;
top: 5px;
color: #ddd;
z-index: 1;
font-size: 12px;
}
.left-bottom-text {
position: absolute;
left: 5px;
bottom: 5px;
color: #ddd;
z-index: 1;
font-size: 12px;
}
.right-bottom-text {
position: absolute;
right: 5px;
bottom: 5px;
color: #ddd;
z-index: 1;
font-size: 12px;
}
.right-slider-box {
position: absolute;
right: 1px;
height: calc(100% - 140px);
transform: translateY(-50%);
top: calc(50% - 30px);
width: 10px;
background: #333;
z-index: 1;
cursor: pointer;
}
.right-slider-box:after{
content: '';
position: absolute;
bottom: -20px;
left: 0;
height: 20px;
width: 100%;
background: #333;
}
.slider {
height: 20px;
width: 100%;
position: absolute;
top: 0;
z-index:10;
background: #9e9e9e;
cursor: move
}
.orientation-top {
position: absolute;
left: 50%;
top: 30px;
color: #f44336;
transform: translateX(-50%);
z-index: 1;
}
.orientation-bottom {
position: absolute;
left: 50%;
bottom: 15px;
color: #f44336;
transform: translateX(-50%);
z-index: 1;
}
.orientation-left {
position: absolute;
top: 50%;
left: 15px;
color: #f44336;
transform: translateY(-50%);
z-index: 1;
}
.orientation-right {
position: absolute;
top: 50%;
right: 15px;
color: #f44336;
transform: translateY(-50%);
z-index: 1;
}
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<div class="visit-review-container">
<el-tabs
v-model="activeName"
>
<!-- 阅片 -->
<el-tab-pane
v-if="taskInfo"
:label="$t('trials:reading:tabTitle:review')"
name="read"
>
<read-page />
</el-tab-pane>
<!-- 报告 -->
<el-tab-pane
v-if="taskInfo && !taskInfo.IseCRFShowInDicomReading"
:label="$t('trials:reading:tabTitle:report')"
name="report"
>
<report-page v-if="activeName === 'report'" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import ReadPage from './ReadPage'
import ReportPage from './ReportPage'
export default {
name: 'VisitReview',
components: {
ReadPage,
ReportPage
},
data() {
return {
activeName: 'read',
taskInfo: null
}
},
mounted() {
this.taskInfo = JSON.parse(localStorage.getItem('taskInfo'))
}
}
</script>
<style lang="scss" scoped>
.visit-review-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #000;
padding: 5px;
::v-deep .el-tabs {
height: 100%;
display: flex;
flex-direction: column;
.el-tabs__item {
color: #fff;
}
.el-tabs__item.is-active {
color: #428bca;
}
.el-tabs__item:hover {
color: #428bca;
}
.el-tabs__header {
height: 50px;
margin:0px;
}
.el-tabs__content {
flex: 1;
margin:0px;
}
.el-tab-pane {
height: 100%;
}
}
}
</style>

View File

@ -0,0 +1,193 @@
const config = {
"standards": [
{
"type": 1,
"name": "RECIST 1.1",
"tools": [
{
"name": "直径测量工具",
"icon": "length",
"toolName": "Length",
"i18nKey": "trials:reading:button:length"
},
{
"name": "长短径测量工具",
"icon": "bidirection",
"toolName": "Bidirectional",
"i18nKey": "trials:reading:button:bidirectional"
},
{
"name": "矩形工具",
"icon": "rectangle",
"toolName": "RectangleRoi",
"i18nKey": "trials:reading:button:rectangle"
},
{
"name": "箭头工具",
"icon": "arrow",
"toolName": "ArrowAnnotate",
"i18nKey": "trials:reading:button:arrowAnnotate"
}
]
},
{
"type": 2,
"name": "Lugano 2014",
"tools": [
{
"name": "直径测量工具",
"icon": "length",
"toolName": "Length",
"i18nKey": "trials:reading:button:length"
},
{
"name": "长短径测量工具",
"icon": "bidirection",
"toolName": "Bidirectional",
"i18nKey": "trials:reading:button:bidirectional"
},
{
"name": "矩形工具",
"icon": "rectangle",
"toolName": "RectangleRoi",
"i18nKey": "trials:reading:button:rectangle"
},
{
"name": "箭头工具",
"icon": "arrow",
"toolName": "ArrowAnnotate",
"i18nKey": "trials:reading:button:arrowAnnotate"
}
]
},
{
"type": 3,
"name": "iRECIST",
"tools": [
{
"name": "直径测量工具",
"icon": "length",
"toolName": "Length",
"i18nKey": "trials:reading:button:length"
},
{
"name": "长短径测量工具",
"icon": "bidirection",
"toolName": "Bidirectional",
"i18nKey": "trials:reading:button:bidirectional"
},
{
"name": "矩形工具",
"icon": "rectangle",
"toolName": "RectangleRoi",
"i18nKey": "trials:reading:button:rectangle"
},
{
"name": "箭头工具",
"icon": "arrow",
"toolName": "ArrowAnnotate",
"i18nKey": "trials:reading:button:arrowAnnotate"
}
]
},
{
"type": 7,
"name": "mRECIST HCC",
"tools": [
{
"name": "直径测量工具",
"icon": "length",
"toolName": "Length",
"i18nKey": "trials:reading:button:length"
},
{
"name": "长短径测量工具",
"icon": "bidirection",
"toolName": "Bidirectional",
"i18nKey": "trials:reading:button:bidirectional"
},
{
"name": "矩形工具",
"icon": "rectangle",
"toolName": "RectangleRoi",
"i18nKey": "trials:reading:button:rectangle"
},
{
"name": "箭头工具",
"icon": "arrow",
"toolName": "ArrowAnnotate",
"i18nKey": "trials:reading:button:arrowAnnotate"
}
]
},
{
"type": 10,
"name": "PCWG3",
"tools": [
{
"name": "矩形工具",
"icon": "rectangle",
"toolName": "RectangleRoi",
"i18nKey": "trials:reading:button:rectangle"
},
{
"name": "箭头工具",
"icon": "arrow",
"toolName": "ArrowAnnotate",
"i18nKey": "trials:reading:button:arrowAnnotate"
}
]
},
{
"type": 17,
"name": "PCWG3",
"tools": [
{
"name": "直径测量工具",
"icon": "length",
"toolName": "Length",
"i18nKey": "trials:reading:button:length"
},
{
"name": "矩形工具",
"icon": "rectangle",
"toolName": "RectangleRoi",
"i18nKey": "trials:reading:button:rectangle"
},
{
"name": "箭头工具",
"icon": "arrow",
"toolName": "ArrowAnnotate",
"i18nKey": "trials:reading:button:arrowAnnotate"
}
]
},
{
"type": 19,
"name": "IVUS定量评估",
"tools": []
},
{
"type": 20,
"name": "OCT定量评估",
"tools": []
},
{
"type": 21,
"name": "MRI-PDFF",
"tools": [
{
"name": "圆形测量",
"icon": "oval",
"toolName": "Probe",
"i18nKey": "trials:reading:button:circle"
}
]
}
]
}
const getTools = (criterionType) => {
const standard = config.standards.find(s => s.type === criterionType);
return standard?.tools || [];
};
export {config, getTools}

View File

@ -0,0 +1,113 @@
<template>
<div v-loading="loading" class="reading-viewer-container">
<!-- 访视阅片 -->
<visit-review
v-if="taskInfo && taskInfo.ReadingCategory=== 1"
/>
<!-- 临床数据 -->
<el-dialog
:visible.sync="clinicalDataVisible"
:custom-class="isClinicalDataFullscreen?'full-dialog-container':'dialog-container'"
:show-close="false"
:close-on-click-modal="false"
:fullscreen="isClinicalDataFullscreen"
>
<span slot="title" class="dialog-footer">
<!-- 当前阅片任务存在临床数据请查看若已查看请点击确认 -->
<span v-if="!closeCDVisible">{{ $t('trials:reading:dagTitle:msg1') }}</span>
<div style="position: absolute;right: 20px;top: 10px;">
<svg-icon :icon-class="isClinicalDataFullscreen?'exit-fullscreen':'fullscreen'" style="cursor: pointer;font-size: 20px;" @click="isClinicalDataFullscreen=!isClinicalDataFullscreen" />
<svg-icon v-if="closeCDVisible" icon-class="dClose" style="cursor: pointer;font-size: 25px;margin-left: 10px;" @click="clinicalDataVisible = false" />
</div>
</span>
<div style="height: 100%;margin:0;display: flex;flex-direction: column;">
<clinical-data
v-if="clinicalDataVisible"
style="flex: 1"
:trial-id="trialId"
:subject-id="taskInfo.SubjectId"
:visit-task-id="cdVisitTaskId"
:is-reading-show-subject-info="taskInfo.IsReadingShowSubjectInfo"
/>
<div v-if="!closeCDVisible" style="text-align:right">
<el-button type="primary" @click="handleConfirmCD">{{ $t('trials:reading:button:confirm') }}</el-button>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import VisitReview from './components/VisitReview'
import ClinicalData from '@/views/trials/trials-panel/reading/clinical-data'
export default {
name:'Dicoms3d',
components: { VisitReview,ClinicalData },
data() {
return {
taskInfo: null,
trialId: '',
loading: false,
clinicalDataVisible: false,
isClinicalDataFullscreen: false,
closeCDVisible: false,
cdVisitTaskId: ''
}
},
mounted() {
this.trialId = this.$route.query.trialId
this.getTaskInfo()
},
methods: {
async getTaskInfo() {
this.loading = true
try {
const params = {
subjectId: this.$route.query.subjectId,
trialId: this.$route.query.trialId,
subjectCode: this.$route.query.subjectCode,
visitTaskId: this.$route.query.visitTaskId,
trialReadingCriterionId: this.$route.query.TrialReadingCriterionId
}
const res = await getNextTask(params)
this.taskInfo = res.Result
localStorage.setItem('taskInfo', JSON.stringify(res.Result))
localStorage.setItem('digitPlaces', JSON.stringify(res.Result.DigitPlaces))
this.loading = false
this.$nextTick(() => {
if (this.taskInfo.IsExistsClinicalData && this.taskInfo.IsNeedReadClinicalData && !this.taskInfo.IsReadClinicalData) {
this.isClinicalDataFullscreen = false
this.clinicalDataVisible = true
this.cdVisitTaskId = this.taskInfo.VisitTaskId
}
})
} catch (e) {
console.log(e)
this.loading = false
}
},
}
}
</script>
<style lang="scss" scoped>
.reading-viewer-container {
width: 100%;
height: 100%;
// ::v-deep .dialog-container{
// margin-top: 50px !important;
// width:75%;
// height:80%;
// }
// ::v-deep .el-dialog__body{
// padding: 10px;
// height: calc(100% - 70px);
// }
// .el-dialog__header{
// position: relative;
// }
// .full-dialog-container{
// ::v-deep .is-fullscreen .el-dialog__body{
// height: calc(100% - 70px);
// }
// }
}
</style>

View File

@ -295,8 +295,8 @@ export default {
async toggleTaskByViewer(visitTaskNum) {
const i = this.visitTaskList.findIndex(v => v.VisitTaskNum === visitTaskNum)
if (i === -1) return
const visistTaskId = this.visitTaskList[i].VisitTaskId
this.setActiveTaskVisitId(visistTaskId, true)
const visitTaskId = this.visitTaskList[i].VisitTaskId
this.setActiveTaskVisitId(visitTaskId, true)
},
// 访
async setActiveTaskVisitId(id, isInitActiveFile = false) {