QC质控查看非dicom文件页面更改

rj
caiyiling 2026-06-29 16:28:33 +08:00
parent f03db6f995
commit 92da42f010
6 changed files with 1478 additions and 7 deletions

View File

@ -128,6 +128,11 @@ export const constantRoutes = [
hidden: true,
component: () => import('@/views/none-dicom-show/index')
},
{
path: '/noneDicoms',
hidden: true,
component: () => import('@/views/none-dicoms/index')
},
{
path: '/noneDicomReading',
name: 'noneDicomReading',

View File

@ -0,0 +1,868 @@
<template>
<div class="none-dicom-viewer">
<!-- viewports -->
<div class="viewports-wrapper" v-loading="loading" ref="viewports-wrapper">
<div ref="container" class="grid-container" :style="gridStyle">
<div v-for="(v, index) in viewportInfos" :ref="`grid-cell-${index}`" :key="index" :style="getCellStyle(index)"
:class="['grid-cell', index === activeCanvasIndex ? 'cell_active' : '', index === fullScreenIndex ? 'cell-full-screen' : '']"
@dblclick="toggleFullScreen($event, index)" @click="activeCanvas(index)"
@mouseup="sliderMouseup($event, index)" @mousemove="sliderMousemove($event, index)"
@mouseleave="sliderMouseleave($event, index)">
<div v-show="imageType.includes(v.fileType)" :ref="`canvas-${index}`" class="content">
<div class="left-top-text">
<div v-if="v.currentFileName">{{ v.currentFileName }}</div>
</div>
<div :ref="`sliderBox-${index}`" class="right-slider-box" @click.stop="clickSlider($event, index)">
<div :style="{ top: v.height + '%' }" class="slider" @click.stop.prevent="() => { return }"
@mousedown.stop="sliderMousedown($event, index)" />
</div>
</div>
<div v-if="v.fileType === 'application/pdf' && fullScreenIndex === null" class="content flex_col">
<div class="content-top" style="height: 50px;">
<div class="left-top-text">
<div v-if="v.currentFileName">{{ v.currentFileName }}</div>
</div>
</div>
<div class="content-main" style="flex: 1;">
<iframe v-if="v.currentFilePath" :ref="`iframe-${index}`"
:src="`/static/pdfjs/web/viewer.html?file=${OSSclientConfig.basePath}${v.currentFilePath}?index=${index}`"
width="100%" height="100%" frameborder="0" crossorigin="anonymous" />
</div>
</div>
<div v-if="v.fileType.includes('mp4')" class="content flex_col">
<div class="content-top" style="height: 50px;">
<div class="left-top-text">
<div v-if="v.currentFileName">{{ v.currentFileName }}</div>
</div>
</div>
<div class="content-main" :ref="`video-content-main-${index}`" style="flex: 1;">
<video v-if="v.currentFilePath" :ref="`videovideo-${index}`"
:src="`${OSSclientConfig.basePath}${v.currentFilePath}`" crossorigin="anonymous"
width="100%" height="100%" autoplay controls controlsList="nodownload"></video>
</div>
</div>
</div>
</div>
</div>
<!-- tools -->
<div class="dicom-tools">
<div class="measureTool-wrapper">
<div class="sideTool-title">{{ $t('trials:reading:button:layout') }}</div>
<div class="sideTool-wrapper">
<label>{{ $t('trials:reading:button:layout') }}:</label>
<select class="sidetool-select" style="width:90px" :value="layout.replace('*', 'x')" @change="handleLayoutChange">
<option value="1x1">1x1</option>
<option value="1x2">1x2</option>
<option value="2x1">2x1</option>
<option value="2x2">2x2</option>
</select>
</div>
</div>
<!-- 图像变换 -->
<div class="measureTool-wrapper">
<div class="sideTool-title">{{ $t('trials:dicom-show:transform') }}</div>
<div class="sideTool-wrapper">
<button :title="$t('trials:reading:button:zoom')" :class="{ 'btn-link': true, activeTool: activeTool === 'Zoom' }"
data-tool="Zoom" @click="setToolActive('Zoom')">
<svg-icon icon-class="magnifier" style="font-size:20px;" />
</button>
<button :title="$t('trials:reading:button:move')" :class="{ 'btn-link': true, activeTool: activeTool === 'Pan' }"
data-tool="Pan" @click="setToolActive('Pan')">
<svg-icon icon-class="move" style="font-size:20px;" />
</button>
<button :title="$t('trials:reading:button:rotate')" :class="{ 'btn-link': true, activeTool: activeTool === 'PlanarRotate' }"
data-tool="PlanarRotate" @click="setToolActive('PlanarRotate')">
<svg-icon icon-class="rotate" style="font-size:20px;" />
</button>
<button :title="$t('trials:reading:button:reset')" class="btn-link" @click="resetViewport">
<svg-icon icon-class="refresh" style="font-size:20px;" />
</button>
</div>
</div>
<!-- 测量工具 -->
<!-- <div class="measureTool-wrapper">
<div class="sideTool-title">{{ $t('trials:dicom-show:measurementLabeling') }}</div>
<div class="sideTool-wrapper">
<button :title="$t('trials:reading:button:screenShot')" class="btn-link" @click="saveImage">
<svg-icon icon-class="image" style="font-size:20px;" />
</button>
<button :title="$t('trials:reading:button:reset')" class="btn-link" @click="resetViewport">
<svg-icon icon-class="refresh" style="font-size:20px;" />
</button>
</div>
</div> -->
</div>
</div>
</template>
<script>
import {
RenderingEngine,
Enums,
imageLoader,
metaData,
getRenderingEngine,
eventTarget,
utilities as csUtils
// getEnabledElementByIds
} from '@cornerstonejs/core'
import * as cornerstoneTools from '@cornerstonejs/tools'
import LengthscaleTool from "@/views/trials/trials-panel/reading/visit-review/tools/LengthscaleTool"
import initLibraries from '@/views/trials/trials-panel/reading/dicoms/components/Fusion/js/initLibraries'
import hardcodedMetaDataProvider from '@/views/trials/trials-panel/reading/visit-review/js/hardcodedMetaDataProvider'
import registerWebImageLoader from '@/views/trials/trials-panel/reading/visit-review/js/registerWebImageLoader'
import store from '@/store'
const { ViewportType } = Enums
const renderingEngineId = 'myRenderingEngine'
const {
ToolGroupManager,
Enums: csToolsEnums,
StackScrollTool,
PanTool,
ZoomTool,
PlanarRotateTool,
ArrowAnnotateTool,
RectangleROITool,
PlanarFreehandROITool,
SplineROITool,
EraserTool,
LengthTool,
EllipticalROITool,
CircleROITool,
AngleTool,
BidirectionalTool,
// cursors
} = cornerstoneTools
const newStyles = {
global: {
color: 'rgb(255, 0, 0)',
colorHighlighted: 'rgb(0, 255, 0)',
colorSelected: 'rgb(255, 0, 0)',
colorLocked: 'rgb(255, 0, 0)',
lineWidth: '1',
lineDash: '',
shadow: true,
textBoxVisibility: true,
textBoxFontFamily: 'Helvetica Neue, Helvetica, Arial, sans-serif',
textBoxFontSize: '14px',
textBoxColor: 'rgb(255, 0, 0)',
textBoxColorHighlighted: 'rgb(0, 255, 0)',
textBoxColorSelected: 'rgb(255, 0, 0)',
textBoxColorLocked: 'rgb(255, 0, 0)',
textBoxBackground: '',
textBoxLinkLineWidth: '1',
textBoxLinkLineDash: '2,3',
textBoxShadow: true,
markerSize: '10'
}
}
cornerstoneTools.annotation.config.style.setDefaultToolStyles(newStyles)
const { MouseBindings, Events: toolsEvents } = csToolsEnums
export default {
name: 'ImageViewer',
data() {
return {
rows: 1,
cols: 1,
fullScreenIndex: null,
imageIds: [],
layout: '1*1',
cellsMax: 4,
activeTool: '',
imageType: ['image/jpeg', 'image/jpg', 'image/bmp', 'image/png'],
loading: false,
trialId: null,
activeName: '1',
tools: [],
viewportInfos: [],
activeCanvasIndex: 0,
}
},
computed: {
gridStyle() {
return {
display: 'grid',
gridTemplateRows: `repeat(${this.rows}, 1fr)`,
gridTemplateColumns: `repeat(${this.cols}, 1fr)`,
height: '100%',
width: '100%'
}
},
cellStyle() {
return {
border: '1px dashed #ccc',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}
},
cells() {
return Array(this.rows * this.cols).fill(0)
}
},
mounted() {
this.tools = config.customizeStandardsNoneDicom
this.trialId = this.$route.query.trialId
this.viewportInfos = Array.from({ length: this.cellsMax }, (_, index) => ({
index: index,
studyId: '',
currentImageIdIndex: 0,
viewportId: `canvas-${index}`,
fileType: '',
currentFileName: '',
currentFilePath: '',
imageIds: [],
oldB: null,
oldM: null,
isMove: false,
height: 0
}))
this.initLoader()
this.$refs['viewports-wrapper'].addEventListener('wheel', (e) => {
this.setToolsPassive()
});
},
methods: {
showPanel(e) {
e.currentTarget.firstChild.lastChild.style.display = 'block'
},
toolMouseout(e) {
e.currentTarget.firstChild.lastChild.style.display = 'none'
},
//
async initLoader() {
registerWebImageLoader(imageLoader)
await initLibraries()
let renderingEngine = getRenderingEngine(renderingEngineId)
if (!renderingEngine) {
renderingEngine = new RenderingEngine(renderingEngineId)
}
const resizeObserver = new ResizeObserver(() => {
const renderingEngine = getRenderingEngine(renderingEngineId)
if (renderingEngine) {
renderingEngine.resize(true, false)
}
})
const element1 = this.$refs['canvas-0'][0]
const element2 = this.$refs['canvas-1'][0]
const element3 = this.$refs['canvas-2'][0]
const element4 = this.$refs['canvas-3'][0]
const elements = [
element1,
element2,
element3,
element4
]
elements.forEach((element, i) => {
element.oncontextmenu = (e) => e.preventDefault()
resizeObserver.observe(element)
element.addEventListener('CORNERSTONE_STACK_NEW_IMAGE', this.stackNewImage)
})
const viewportInputArray = [
{
viewportId: 'canvas-0',
type: ViewportType.STACK,
element: element1
},
{
viewportId: 'canvas-1',
type: ViewportType.STACK,
element: element2
},
{
viewportId: 'canvas-2',
type: ViewportType.STACK,
element: element3
},
{
viewportId: 'canvas-3',
type: ViewportType.STACK,
element: element4
}
]
const viewportIds = ['canvas-0', 'canvas-1', 'canvas-2', 'canvas-3']
renderingEngine.setViewports(viewportInputArray)
cornerstoneTools.addTool(StackScrollTool)
cornerstoneTools.addTool(PanTool)
cornerstoneTools.addTool(ZoomTool)
cornerstoneTools.addTool(PlanarRotateTool)
cornerstoneTools.addTool(ArrowAnnotateTool)
cornerstoneTools.addTool(RectangleROITool)
cornerstoneTools.addTool(EllipticalROITool)
cornerstoneTools.addTool(CircleROITool)
cornerstoneTools.addTool(AngleTool)
cornerstoneTools.addTool(PlanarFreehandROITool)
cornerstoneTools.addTool(SplineROITool)
cornerstoneTools.addTool(EraserTool)
cornerstoneTools.addTool(LengthTool)
cornerstoneTools.addTool(LengthscaleTool)
cornerstoneTools.addTool(BidirectionalTool)
viewportIds.forEach((viewportId, i) => {
const toolGroupId = `canvas-${i}`
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId)
toolGroup.addViewport(viewportId, renderingEngineId)
toolGroup.addTool(StackScrollTool.toolName)
toolGroup.addTool(PanTool.toolName)
toolGroup.addTool(ZoomTool.toolName)
toolGroup.addTool(PlanarRotateTool.toolName)
toolGroup.addTool(ArrowAnnotateTool.toolName)
toolGroup.addTool(RectangleROITool.toolName)
toolGroup.addTool(BidirectionalTool.toolName)
toolGroup.addTool(EllipticalROITool.toolName)
toolGroup.addTool(CircleROITool.toolName)
toolGroup.addTool(AngleTool.toolName)
toolGroup.addTool(PlanarFreehandROITool.toolName)
toolGroup.addTool(SplineROITool.toolName)
toolGroup.addTool(EraserTool.toolName)
toolGroup.addTool(LengthTool.toolName)
toolGroup.addTool(LengthscaleTool.toolName)
toolGroup.setToolActive(StackScrollTool.toolName, {
bindings: [{ mouseButton: MouseBindings.Wheel }]
})
toolGroup.setToolPassive(PanTool.toolName)
toolGroup.setToolPassive(ZoomTool.toolName)
toolGroup.setToolPassive(PlanarRotateTool.toolName)
toolGroup.setToolPassive(EraserTool.toolName)
})
},
lengthscaleToolDisabled(saved) {
const viewportIds = ['canvas-0', 'canvas-1', 'canvas-2', 'canvas-3']
viewportIds.forEach(toolGroupId => {
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
if (saved) {
toolGroup.setToolPassive(LengthscaleTool.toolName)
} else {
toolGroup.setToolEnabled(LengthscaleTool.toolName)
}
})
},
//
stackNewImage(e) {
const { detail } = e
const i = this.viewportInfos.findIndex(i => i.viewportId === detail.viewportId)
if (i === -1) return
this.viewportInfos[i].currentImageIdIndex = detail.imageIdIndex
const obj = this.viewportInfos[i].fileList[detail.imageIdIndex]
if (!obj) return
this.viewportInfos[i].currentFileName = obj.FileName
const total = this.viewportInfos[i].imageIds.length
this.viewportInfos[i].height = total > 1 ? (this.viewportInfos[i].currentImageIdIndex) * 100 / (total - 1) : 0
this.$emit('toggleImage', {
studyId: this.viewportInfos[i].studyId,
path: obj.Path
})
},
//
async renderImage(imageIds, canvasIndex, sliceIndex) {
metaData.addProvider((type, imageId) => hardcodedMetaDataProvider(type, imageId, imageIds), 10000)
const renderingEngine = getRenderingEngine(renderingEngineId)
const viewport = renderingEngine.getViewport(`canvas-${canvasIndex}`)
await viewport.setStack(imageIds)
this.setToolsPassive()
viewport.setImageIdIndex(sliceIndex)
viewport.render()
// this.updateViewportInfos()
},
setActiveCanvasImages(obj) {
if (!obj || Object.keys(obj).length === 0) return
const i = this.viewportInfos.findIndex(i => i.index === this.activeCanvasIndex)
if (i === -1) return
if (obj.studyId === this.viewportInfos[i].studyId && this.imageType.includes(this.viewportInfos[i].fileType) && this.imageType.includes(obj.fileInfo.FileType)) {
const fileIndex = this.viewportInfos[i].fileList.findIndex(i => i.Path === obj.fileInfo.Path)
this.sliceIndex(fileIndex)
} else {
if (!this.imageType.includes(obj.fileInfo.FileType)) {
this.fullScreenIndex = null
}
this.updateViewportInfos(this.activeCanvasIndex, obj)
}
this.setToolsPassive()
},
//
activeCanvas(index) {
if (this.activeCanvasIndex === index) return
const i = this.viewportInfos.findIndex(i => i.index === index)
if (i === -1) return
this.activeCanvasIndex = index
this.setToolsPassive()
},
//
updateViewportInfos(index, obj) {
const i = this.viewportInfos.findIndex(i => i.index === index)
if (i === -1) return
this.viewportInfos[i].taskInfo = obj.visitTaskInfo
this.viewportInfos[i].currentFileName = obj.fileInfo.FileName
this.viewportInfos[i].studyId = obj.studyId
this.viewportInfos[i].fileType = obj.fileInfo.FileType
if (this.imageType.includes(obj.fileInfo.FileType)) {
const arr = obj.fileList.filter(i => this.imageType.includes(i.FileType))
this.viewportInfos[i].fileList = arr
const fileIndex = this.viewportInfos[i].fileList.findIndex(i => i.Path === obj.fileInfo.Path)
this.viewportInfos[i].currentImageIdIndex = fileIndex
const imageIds = []
for (let i = 0; i < arr.length; i++) {
const path = arr[i].Path
imageIds.push(`web:${this.OSSclientConfig.basePath}${path}`)
}
this.viewportInfos[i].imageIds = imageIds
if (imageIds.length > 0) {
this.renderImage(imageIds, index, fileIndex)
}
this.$nextTick(() => {
const renderingEngine = getRenderingEngine(renderingEngineId)
if (renderingEngine) {
renderingEngine.resize(true, false)
}
})
} else {
this.viewportInfos[i].currentFilePath = obj.fileInfo.Path
}
},
//
sliceIndex(index) {
const i = this.viewportInfos.findIndex(i => i.index === this.activeCanvasIndex)
if (i === -1) return
if (index < 0 || index >= this.viewportInfos[i].imageIds.length) return
const renderingEngine = getRenderingEngine(renderingEngineId)
const viewport = renderingEngine.getViewport(
this.viewportInfos[i].viewportId
)
this.setToolsPassive()
viewport.setImageIdIndex(index)
viewport.render()
},
//
handleCommand(command) {
this.fullScreenIndex = null
this.layout = command
this.rows = parseInt(command.split('*')[0])
this.cols = parseInt(command.split('*')[1])
if (this.rows === 1 && this.cols === 1) {
this.$nextTick(() => {
this.activeCanvas(0)
})
}
},
handleLayoutChange(event) {
this.handleCommand(event.target.value.replace('x', '*'))
},
getCellStyle(index) {
return {
...this.cellStyle,
display: index < this.cells.length ? this.cellStyle.display : 'none'
}
},
//
toggleFullScreen(e, index) {
const i = this.viewportInfos.findIndex(i => i.index === index)
if (i === -1) return
if (!this.viewportInfos[i].currentFileName) return
if (this.imageType.includes(this.viewportInfos[i].fileType)) {
this.fullScreenIndex = this.fullScreenIndex === index ? null : index
this.activeCanvasIndex = index
}
},
//
setToolActive(toolName) {
const i = this.viewportInfos.findIndex(i => i.index === this.activeCanvasIndex)
if (i === -1) return
const toolGroupId = `canvas-${this.viewportInfos[i].index}`
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
if (this.activeTool === toolName) {
toolGroup.setToolPassive(this.activeTool)
this.activeTool = ''
} else {
if (this.activeTool) {
toolGroup.setToolPassive(this.activeTool)
}
toolGroup.setToolActive(toolName, {
bindings: [{ mouseButton: MouseBindings.Primary }]
})
this.activeTool = toolName
}
},
setToolsPassive() {
if (!this.activeTool) return
const toolGroupIds = ['canvas-0', 'canvas-1', 'canvas-2', 'canvas-3']
toolGroupIds.forEach(toolGroupId => {
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId)
toolGroup.setToolPassive(this.activeTool)
})
this.activeTool = ''
},
//
resetViewport() {
this.setToolsPassive()
const renderingEngine = getRenderingEngine(renderingEngineId)
const viewportId = `canvas-${this.activeCanvasIndex}`
const viewport = (renderingEngine.getViewport(viewportId))
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()
},
//
clickSlider(e, index) {
const i = this.viewportInfos.findIndex(i => i.index === index)
if (i === -1) return
const sliderBox = this.$refs[`sliderBox-${index}`] && this.$refs[`sliderBox-${index}`][0]
if (!sliderBox || !this.imageType.includes(this.viewportInfos[i].fileType)) return
const boxHeight = sliderBox.clientHeight
if (!boxHeight || !this.viewportInfos[i].imageIds.length) return
const height = e.offsetY * 100 / boxHeight
this.viewportInfos[i].height = height
let sliceIdx = Math.trunc(this.viewportInfos[i].imageIds.length * height / 100)
sliceIdx = sliceIdx >= this.viewportInfos[i].imageIds.length ? this.viewportInfos[i].imageIds.length - 1 : sliceIdx < 0 ? 0 : sliceIdx
const renderingEngine = getRenderingEngine(renderingEngineId)
const viewport = renderingEngine.getViewport(
this.viewportInfos[i].viewportId
)
this.setToolsPassive()
viewport.setImageIdIndex(sliceIdx)
viewport.render()
},
sliderMouseup(e, index) {
const i = this.viewportInfos.findIndex(i => i.index === index)
if (i === -1 || !this.imageType.includes(this.viewportInfos[i].fileType)) return
this.viewportInfos[i].isMove = false
},
sliderMousemove(e, index) {
const i = this.viewportInfos.findIndex(i => i.index === index)
if (i === -1 || !this.imageType.includes(this.viewportInfos[i].fileType)) return
if (!this.viewportInfos[i].isMove) return
const sliderBox = this.$refs[`sliderBox-${index}`] && this.$refs[`sliderBox-${index}`][0]
if (!sliderBox || !this.viewportInfos[i].imageIds.length) return
const delta = this.viewportInfos[i].oldB - (this.viewportInfos[i].oldM - e.clientY)
const boxHeight = sliderBox.clientHeight
if (delta < 0) return
if (delta > boxHeight) return
const height = delta * 100 / boxHeight
let sliceIdx = Math.trunc(this.viewportInfos[i].imageIds.length * height / 100)
sliceIdx = sliceIdx >= this.viewportInfos[i].imageIds.length ? this.viewportInfos[i].imageIds.length - 1 : sliceIdx < 0 ? 0 : sliceIdx
this.viewportInfos[i].height = height
// if (this.viewportInfos[i].currentImageIdIndex !== i) {
const renderingEngine = getRenderingEngine(renderingEngineId)
const viewport = renderingEngine.getViewport(
this.viewportInfos[i].viewportId
)
this.setToolsPassive()
viewport.setImageIdIndex(sliceIdx)
viewport.render()
// }
// this.$emit('toggleImage', { taskId: this.viewportInfos[i].taskInfo.VisitTaskId, studyId: this.viewportInfos[i].studyId, imageIndex: sliceIdx })
},
sliderMouseleave(e, index) {
const i = this.viewportInfos.findIndex(i => i.index === index)
if (i === -1 || !this.imageType.includes(this.viewportInfos[i].fileType)) return
if (!this.viewportInfos[i].isMove) return
this.viewportInfos[i].isMove = false
},
sliderMousedown(e, index) {
const i = this.viewportInfos.findIndex(i => i.index === index)
if (i === -1 || !this.imageType.includes(this.viewportInfos[i].fileType)) return
const sliderBox = this.$refs[`sliderBox-${index}`] && this.$refs[`sliderBox-${index}`][0]
if (!sliderBox || !this.viewportInfos[i].imageIds.length) return
const boxHeight = sliderBox.clientHeight
const sliderTop = parseFloat(e.currentTarget.style.top) || 0
this.viewportInfos[i].oldB = sliderTop * boxHeight / 100
this.viewportInfos[i].oldM = e.clientY
this.viewportInfos[i].isMove = true
e.stopImmediatePropagation()
e.stopPropagation()
e.preventDefault()
}
}
}
</script>
<style lang="scss" scoped>
.content {
cursor: default !important;
}
.dropdown {
position: relative;
display: inline-block;
.text {
text-align: center;
}
}
.dropdown-content {
display: none;
position: absolute;
background-color: #383838;
color: #fff;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 9999;
font-size: 12px;
ul {
list-style: none;
margin: 0;
padding: 0;
text-align: center;
li {
a {
display: block;
padding: 5px;
}
}
}
ul li a:hover {
background-color: #727272;
}
.customize-standards-list {
width: max-content;
white-space: nowrap;
li a {
display: flex;
align-items: center;
padding: 8px !important;
box-sizing: border-box;
}
}
}
.none-dicom-viewer {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
user-select: none;
background: #000;
.dicom-tools {
width: 300px;
height: 100%;
background-color: #323232;
margin: 0 0 0 2px;
padding: 0;
color: #d0d0d0;
font-size: 13px;
overflow-y: auto;
}
.sideTool-title {
padding: 5px 8px;
background-color: #525252;
color: #bebdbd;
font-size: 14px;
margin: 4px;
}
.sideTool-wrapper {
border-bottom: 1px solid gray;
margin: 0 4px;
padding: 4px;
font-size: 12px;
}
.sidetool-select {
height: 30px;
background-color: rgba(0, 0, 0, 0);
color: #d0d0d0;
border-radius: 4px;
border: 1px solid #5b5b5b;
}
.sidetool-select > option {
background-color: #323232;
}
.sideTool-wrapper button.btn-link {
height: 40px;
padding: 8px !important;
border: 1px solid rgba(37, 37, 37, 1);
margin: 1px 1px;
}
.sideTool-wrapper .btn-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 40px;
color: #cccccc;
background-color: transparent;
cursor: pointer;
}
.activeTool {
background: #16477b90 !important;
border: none !important;
}
.sideTool-wrapper label {
display: inline-block;
min-width: 44px;
margin-right: 6px;
}
.viewports-wrapper {
flex: 1;
min-width: 0;
.grid-container {
display: grid;
height: 100%;
width: 100%;
position: relative;
}
.grid-cell {
border: 1px dashed #ccc;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
min-width: 0;
min-height: 0;
z-index: 1;
}
.cell_active {
border-color: #fafa00 !important;
}
.cell-full-screen {
grid-column: 1 / -1;
grid-row: 1 / -1;
}
.flex_col {
display: flex;
flex-direction: column;
}
.content {
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-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
}
.content-main {
position: relative;
overflow: hidden;
background: #1e1e1e;
}
video {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
object-position: center center;
background: transparent;
}
}
}
}
</style>

View File

@ -0,0 +1,456 @@
<template>
<div v-loading="loading" class="none-dicom-study-wrapper">
<div class="study-info">
{{ $t('trials:none-dicom-show:fileList') }}
</div>
<div class="ps" ref="studyBox_ps">
<el-collapse v-model="activeNames">
<el-collapse-item v-for="(study, index) in studyList" :key="`${study.Id}`" :name="`${study.Id}`">
<template slot="title">
<div class="dicom-desc">
<div style="text-overflow: ellipsis;overflow: hidden;">
<span :title="study.CodeView">{{ study.CodeView }}</span>
<span
v-if="otherInfo && otherInfo.IsShowStudyName"
:title="study.StudyName"
style="margin-left: 5px;"
>
{{study.StudyName }}
</span>
</div>
<div style="text-overflow: ellipsis;overflow: hidden;">
<span :title="study.BodyPart">{{ getBodyPart(study.BodyPart, study.BodyPartForEditOther) }}</span>
<span style="margin-left: 5px;" :title="study.Modality">{{ study.Modality }}</span>
</div>
</div>
</template>
<div class="file-list-container">
<div v-for="(k, i) in study.NoneDicomStudyFileList" :key="i" style="position:relative;margin-top:1px;"
@click="selectFile(study, index, i, k)" :ref="`noneDicomRef_${index}_${i}`">
<div :class="{ 'file-active': index === activeStudyIndex && i === activeFileIndex }" class="file-wrapper">
<div class="file-main">
<div class="file-image">
<el-image
v-if="k.FileType === 'image/jpeg' || k.FileType === 'image/jpg' || k.FileType === 'image/bmp' || k.FileType === 'image/png'"
style="width: 100%;height: 100%;"
:src="`${OSSclientConfig.basePath}${k.Path}?x-oss-process=image/resize,w_50,h_50/format,png`"
fit="contain" crossorigin="anonymous" />
<el-image v-else-if="k.FileType === 'application/pdf'" style="width: 100%;height: 100%;" :src="pdf"
fit="contain" crossorigin="anonymous" />
<el-image v-else-if="
!!~k.FileType.indexOf('mp4')
" style="width: 100%; height: 100%" :src="mp4" fit="contain" crossorigin="anonymous" />
</div>
<div class="file-text" :title="k.FileName">
{{ k.FileName }}
</div>
</div>
<div v-if="isQcCheck" class="switchBox">
<div class="item">
<span>{{ $t('trials:audit:table:isReading') }}</span>
<el-switch
v-model="k.IsReading"
:disabled="k.IsDeleted || isAudit"
:active-text="$fd('YesOrNo', true)"
:inactive-text="$fd('YesOrNo', false)"
@change="changeReadingStatus($event, study, k)"
/>
</div>
<div class="item">
<span>{{ $t('trials:audit:table:isDelete') }}</span>
<el-switch
v-model="k.IsDeleted"
:disabled="isAudit"
:active-text="$fd('YesOrNo', true)"
:inactive-text="$fd('YesOrNo', false)"
@change="changeDeleteStatus($event, study, k)"
/>
</div>
</div>
</div>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script>
import { getNoneDicomStudyList, setNodicomStudyState } from '@/api/trials'
import pdf from '@/assets/pdf.png'
import mp4 from '@/assets/mp4.png'
export default {
name: 'StudyList',
props: {
visitTaskInfo: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loading: false,
activeNames: [],
activeStudyIndex: -1,
activeFileIndex: -1,
studyList: [],
pdf,
mp4,
BodyPart: {},
subjectVisitId: '',
studyId: '',
otherInfo: null,
criterionType: null,
isQcCheck: false,
isAudit: false
}
},
async mounted() {
this.subjectVisitId = this.$route.query.subjectVisitId
this.studyId = this.$route.query.studyId
this.isQcCheck = !!this.$route.query.isQcCheck
this.getNoneDicoms()
// this.$nextTick(() => {
// this.activeStudy(this.studyList[0].Id)
// })
this.BodyPart.Bodypart = await this.$getBodyPart(this.$route.query.trialId)
},
methods: {
scrollActiveFileIntoView() {
this.$nextTick(() => {
const refKey = `noneDicomRef_${this.activeStudyIndex}_${this.activeFileIndex}`
const targetRef = this.$refs[refKey]
const element = Array.isArray(targetRef) ? targetRef[0] : targetRef
if (!element) return
this.scrollElementToVisible(element)
})
},
scrollElementToVisible(element, options = {}) {
const container = this.$refs['studyBox_ps']
if (!container || !element) return
const elementTop = element.offsetTop
const elementBottom = elementTop + element.offsetHeight
const visibleTop = container.scrollTop
const visibleBottom = visibleTop + container.clientHeight
if (elementTop >= visibleTop && elementBottom <= visibleBottom) return
const offset = options.offset != null
? options.offset
: Math.max((container.clientHeight - element.offsetHeight) / 2, 0)
const targetPosition = Math.max(elementTop - offset, 0)
container.scrollTo({
top: targetPosition,
behavior: options.behavior || 'smooth'
})
},
async getNoneDicoms() {
const loading = this.$loading({ fullscreen: true })
try {
let res = await getNoneDicomStudyList(
this.subjectVisitId,
this.studyId,
false,
this.$route.query.visitTaskId,
!!this.$route.query.isReading,
this.$route.query.isImageSegmentLabel,
)
this.studyList = res.Result
this.otherInfo = res.OtherInfo
this.isAudit = res.OtherInfo.AuditState * 1 > 6
const studyIndex = this.studyList.findIndex((item) => {
return item.NoneDicomStudyFileList.length > 0
})
if (studyIndex > -1) {
this.selectFile(this.studyList[studyIndex], studyIndex, 0)
let studyId = this.studyList[studyIndex].Id
this.activeNames = [studyId]
}
if (this.criterionType === 19 || this.criterionType === 20) {
this.sortFile()
}
loading.close()
} catch(e) {
console.log(e)
loading.close()
}
},
selected() {
},
sortFile() {
},
changeReadingStatus(callback, row, file) {
let statusStr = ''
if (callback) {
statusStr = this.$t('trials:audit:label:setSeriesReading')
file.IsReading = false
} else {
statusStr = this.$t('trials:audit:label:setSeriesNotReading')
file.IsReading = true
}
let message = this.$t('trials:audit:message:changeSeriesStatus').replace('xxx', statusStr)
message = message.replace('yyy', this.$fd('YesOrNo', !file.IsReading))
this.$confirm(message, {
distinguishCancelAndClose: true,
type: 'warning',
})
.then(() => {
const state = file.IsReading ? 1 : 2
this.loading = true
const params = {
SubjectVisitId: row.SubjectVisitId,
NoneDicomStudyId: row.Id,
State: state,
NoneDicomStudyFileId: file.Id,
}
return setNodicomStudyState(params)
.then((res) => {
this.loading = false
if (res.IsSuccess) {
this.$message.success(this.$t('common:message:savedSuccessfully'))
file.IsReading = !file.IsReading
}
})
.catch(() => {
this.loading = false
})
})
.catch(() => { })
},
changeDeleteStatus(callback, row, file) {
let statusStr = ''
if (callback) {
statusStr = this.$t('trials:audit:label:setSeriesDeleted')
file.IsDeleted = false
} else {
statusStr = this.$t('trials:audit:label:setSeriesNotDelete')
file.IsDeleted = true
}
let message = this.$t('trials:audit:message:changeSeriesStatus').replace('xxx', statusStr)
message = message.replace('yyy', this.$fd('YesOrNo', !file.IsDeleted))
this.$confirm(message, {
distinguishCancelAndClose: true,
type: 'warning',
})
.then(() => {
const state = file.IsDeleted ? 5 : 4
this.loading = true
const params = {
SubjectVisitId: row.SubjectVisitId,
NoneDicomStudyId: row.Id,
State: state,
NoneDicomStudyFileId: file.Id,
}
return setNodicomStudyState(params)
.then((res) => {
this.loading = false
if (res.IsSuccess) {
this.$message.success(this.$t('common:message:savedSuccessfully'))
file.IsDeleted = !file.IsDeleted
}
})
.catch(() => {
this.loading = false
})
})
.catch(() => { })
},
getBodyPart(bodyPart, other) {
if (!bodyPart && !other) return ''
var separator = ','
if (bodyPart.indexOf('|') > -1) {
separator = '|'
} else if (bodyPart.indexOf(',') > -1) {
separator = ','
} else if (bodyPart.indexOf('') > -1) {
separator = ''
}
var arr = bodyPart.split(separator)
var newArr = arr.map((i) => {
return this.$fd('Bodypart', i.trim(), 'Code', this.BodyPart, 'Name')
})
if (other) {
newArr.push(other)
}
newArr = newArr.filter(Boolean)
return newArr.join(' | ')
},
//
selectFile(study, studyIndex, fileIndex) {
this.activeStudyIndex = studyIndex
this.activeFileIndex = fileIndex
const fileList = study.NoneDicomStudyFileList
this.$emit('selectFile', { fileInfo: fileList[fileIndex], fileList, visitTaskInfo: this.visitTaskInfo, studyId: study.Id })
this.scrollActiveFileIntoView()
},
activeImage(obj) {
if (!obj || this.studyList.length === 0) return
const studyIndex = this.studyList.findIndex(i => i.Id === obj.studyId)
if (studyIndex === -1) return
this.activeStudy(`${obj.studyId}`)
this.activeStudyIndex = studyIndex
const fileList = this.studyList[studyIndex].NoneDicomStudyFileList || []
const fileIndex = fileList.findIndex(f => f.Path === obj.path)
if (fileIndex === -1) return
this.activeFileIndex = fileIndex
this.scrollActiveFileIntoView()
},
activeStudy(id) {
const studyId = `${id}`
if (this.activeNames.indexOf(studyId) > -1) return
this.activeNames.push(studyId)
}
}
}
</script>
<style lang="scss" scoped>
.none-dicom-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;
line-height: 40px;
}
.dicom-desc {
font-weight: bold;
font-size: 13px;
text-align: left;
color: #d0d0d0;
padding: 2px 8px 2px 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ps {
flex: 1;
overflow-anchor: none;
touch-action: auto;
overflow-y: auto;
}
.file-active {
// background: #16477b;
background-color: #183553 !important;
border: 1px solid #23527b !important;
}
::v-deep .el-progress__text {
color: #ccc;
font-size: 12px;
}
.file-list-container {
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
.file-wrapper {
width: 100%;
padding: 5px;
display: flex;
flex-direction: column;
align-items: stretch;
cursor: pointer;
background-color: #3a3a3a;
// color: #ddd;
// white-space: nowrap;
// overflow: hidden;
// text-overflow: ellipsis;
.file-main {
display: flex;
flex-direction: row;
align-items: center;
min-width: 0;
}
.el-progress__text {
display: none;
}
.el-progress-bar {
padding-right: 0px;
}
.file-image {
width: 50px;
height: 50px;
}
.file-text {
flex: 1;
padding-left: 5px;
color: #ddd;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.switchBox {
width: 100%;
margin-top: 6px;
color: #ddd;
.item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 5px;
}
.item:last-child {
margin-bottom: 0;
}
}
::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: #353535 !important;
color: #ddd;
border-bottom-color: #5a5a5a;
padding-left: 5px;
height: 60px;
line-height: 20px;
}
}
::v-deep .el-progress-bar__inner {
transition: width 0s ease;
}
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<div class="read-page-container">
<!-- 检查列表 -->
<div class="left-panel">
<div class="study-info">
<study-list ref="studyList" @selectFile="selectFile"/>
</div>
</div>
<!-- 图像 -->
<div class="middle-panel">
<file-viewer ref="fileViewer"
@toggleImage="toggleImage"
@selectFile="selectFile" />
</div>
</div>
</template>
<script>
import store from '@/store'
import { changeURLStatic } from '@/utils/history.js'
import StudyList from './components/study-list'
import FileViewer from './components/file-viewer'
export default {
name: 'NoneDicoms',
components: {
StudyList,
FileViewer
},
data() {
return {
}
},
mounted() {
if (this.$route.query.TokenKey) {
store.dispatch('user/setToken', this.$route.query.TokenKey)
changeURLStatic('TokenKey', '')
}
},
methods: {
toggleImage(obj) {
this.$refs['studyList'].activeImage(obj)
},
selectFile(obj) {
if (obj.fileInfo && obj.fileList && obj.studyId) {
this.$refs['fileViewer'].setActiveCanvasImages(obj)
return
}
}
}
}
</script>
<style lang="scss" scoped>
.read-page-container {
box-sizing: border-box;
height: 100%;
display: flex;
background-color: #444;
padding: 5px;
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-thumb {
border-radius: 10px;
background: #d0d0d0;
}
.left-panel {
display: flex;
width: 230px;
border: 1px solid #727272;
color: #fff;
::-webkit-scrollbar {
width: 3px;
height: 3px;
}
::-webkit-scrollbar-thumb {
border-radius: 10px;
background: #d0d0d0;
}
.study-info {
width: 100%;
border-left: 1px solid #727272;
}
}
.middle-panel {
flex: 1;
border: 1px solid #727272;
margin: 0 5px;
}
// .right-panel {
// width: 400px;
// border: 1px solid #727272;
// padding: 0 10px;
// overflow-y: auto;
// .text-info {
// display: flex;
// align-items: center;
// justify-content: space-between;
// color: #ddd;
// }
// }
::v-deep .dialog-container {
margin-top: 50px !important;
width: 75%;
height: 80%;
}
::v-deep .el-dialog__body {
padding: 20px 20px 0 20px;
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

@ -107,12 +107,18 @@ export default {
},
methods: {
scrollElementToTop(element, options = {}) {
const absoluteTop = element.offsetTop;
const targetPosition = absoluteTop - (options.offset || 0);
this.$refs['studyBox_ps'].scrollTo({
const container = this.$refs['studyBox_ps']
if (!container || !element) return
const elementTop = element.offsetTop
const elementBottom = elementTop + element.offsetHeight
const visibleTop = container.scrollTop
const visibleBottom = visibleTop + container.clientHeight
if (elementTop >= visibleTop && elementBottom <= visibleBottom) return
const targetPosition = Math.max(elementTop - (options.offset || 0), 0)
container.scrollTo({
top: targetPosition,
behavior: options.behavior || 'smooth'
});
})
},
getBodyPart(bodyPart, other) {
if (!bodyPart && !other) return ''

View File

@ -2504,10 +2504,10 @@ export default {
var token = getToken()
const routeData = isReading
? this.$router.resolve({
path: `/showNoneDicoms?trialId=${trialId}&subjectVisitId=${this.data.Id}&TokenKey=${token}&isReading=1`,
path: `/noneDicoms?trialId=${trialId}&subjectVisitId=${this.data.Id}&TokenKey=${token}&isReading=1`,
})
: this.$router.resolve({
path: `/showNoneDicoms?trialId=${trialId}&subjectVisitId=${this.data.Id}&TokenKey=${token}&isQcCheck=${this.SecondReviewState > 0 ? 0 : 1}`,
path: `/noneDicoms?trialId=${trialId}&subjectVisitId=${this.data.Id}&TokenKey=${token}&isQcCheck=${this.SecondReviewState > 0 ? 0 : 1}`,
})
this.open = window.open(routeData.href, '_blank')
},
@ -2520,7 +2520,7 @@ export default {
let trialId = this.$route.query.trialId
var token = getToken()
const routeData = this.$router.resolve({
path: `/showNoneDicoms?trialId=${trialId}&subjectVisitId=${this.data.Id}&studyId=${row.Id}&TokenKey=${token}&isQcCheck=${this.SecondReviewState > 0 ? 0 : 1}`,
path: `/noneDicoms?trialId=${trialId}&subjectVisitId=${this.data.Id}&studyId=${row.Id}&TokenKey=${token}&isQcCheck=${this.SecondReviewState > 0 ? 0 : 1}`,
})
this.open = window.open(routeData.href, '_blank')
},