From 6223eac81a0c5461003b1df786c4ff50755d7c87 Mon Sep 17 00:00:00 2001 From: caiyiling <1321909229@qq.com> Date: Fri, 21 Feb 2025 16:13:17 +0800 Subject: [PATCH] =?UTF-8?q?=E9=98=85=E7=89=87=E9=A1=B5=E9=9D=A2=E5=9B=BE?= =?UTF-8?q?=E5=83=8F=E9=A2=84=E8=A7=88=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/store/getters.js | 3 +- src/store/index.js | 5 +- src/store/modules/noneDicomReview.js | 24 + .../components/Fusion/js/initVolumeLoader.js | 7 +- .../reading/none-dicoms/index-.vue | 76 +++ .../visit-review/components/EcrfList.vue | 302 +++++++++ .../visit-review/components/FormItem.vue | 588 ++++++++++++++++++ .../visit-review/components/ImageViewer.vue | 509 +++++++++++++++ .../visit-review/components/ReadPage.vue | 299 +++++++++ .../visit-review/components/ReportPage.vue | 3 + .../visit-review/components/StudyList.vue | 184 ++++++ .../reading/visit-review/index.vue | 79 +++ .../js/hardcodedMetaDataProvider.js | 68 ++ .../visit-review/js/registerWebImageLoader.js | 244 ++++++++ 14 files changed, 2387 insertions(+), 4 deletions(-) create mode 100644 src/store/modules/noneDicomReview.js create mode 100644 src/views/trials/trials-panel/reading/none-dicoms/index-.vue create mode 100644 src/views/trials/trials-panel/reading/visit-review/components/EcrfList.vue create mode 100644 src/views/trials/trials-panel/reading/visit-review/components/FormItem.vue create mode 100644 src/views/trials/trials-panel/reading/visit-review/components/ImageViewer.vue create mode 100644 src/views/trials/trials-panel/reading/visit-review/components/ReadPage.vue create mode 100644 src/views/trials/trials-panel/reading/visit-review/components/ReportPage.vue create mode 100644 src/views/trials/trials-panel/reading/visit-review/components/StudyList.vue create mode 100644 src/views/trials/trials-panel/reading/visit-review/index.vue create mode 100644 src/views/trials/trials-panel/reading/visit-review/js/hardcodedMetaDataProvider.js create mode 100644 src/views/trials/trials-panel/reading/visit-review/js/registerWebImageLoader.js diff --git a/src/store/getters.js b/src/store/getters.js index e843f66d..3f0bc257 100644 --- a/src/store/getters.js +++ b/src/store/getters.js @@ -48,6 +48,7 @@ const getters = { TotalNeedSignSystemDocCount: state => state.user.TotalNeedSignSystemDocCount, TotalNeedSignTrialDocCount: state => state.user.TotalNeedSignTrialDocCount, IsFirstSysDocNeedSign: state => state.user.IsFirstSysDocNeedSign, - TrialStatusStr: state => state.user.TrialStatusStr + TrialStatusStr: state => state.user.TrialStatusStr, + lastViewportTaskId: state => state.noneDicomReview.lastViewportTaskId } export default getters diff --git a/src/store/index.js b/src/store/index.js index c0e04369..9a91ddcd 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -12,7 +12,7 @@ import trials from './modules/trials' import financials from './modules/financials' import reading from './modules/reading' import lang from './modules/lang' - +import noneDicomReview from './modules/noneDicomReview' Vue.use(Vuex) const store = new Vuex.Store({ @@ -27,7 +27,8 @@ const store = new Vuex.Store({ trials, financials, reading, - lang + lang, + noneDicomReview }, getters }) diff --git a/src/store/modules/noneDicomReview.js b/src/store/modules/noneDicomReview.js new file mode 100644 index 00000000..990f2fe0 --- /dev/null +++ b/src/store/modules/noneDicomReview.js @@ -0,0 +1,24 @@ +const getDefaultState = () => { + return { + lastViewportTaskId: null + } +} +const state = getDefaultState + +const mutations = { + +} + +const actions = { + setLastViewportTaskId({ state }, id) { + state.lastViewportTaskId = id + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} + diff --git a/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/initVolumeLoader.js b/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/initVolumeLoader.js index 607048bc..33e817d8 100644 --- a/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/initVolumeLoader.js +++ b/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/initVolumeLoader.js @@ -1,6 +1,7 @@ import { volumeLoader, - cornerstoneStreamingImageVolumeLoader + cornerstoneStreamingImageVolumeLoader, + cornerstoneStreamingDynamicImageVolumeLoader } from '@cornerstonejs/core'; export default function initVolumeLoader() { @@ -11,4 +12,8 @@ export default function initVolumeLoader() { 'cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader ); + volumeLoader.registerVolumeLoader( + 'cornerstoneStreamingDynamicImageVolume', + cornerstoneStreamingDynamicImageVolumeLoader + ); } diff --git a/src/views/trials/trials-panel/reading/none-dicoms/index-.vue b/src/views/trials/trials-panel/reading/none-dicoms/index-.vue new file mode 100644 index 00000000..f1c9db2e --- /dev/null +++ b/src/views/trials/trials-panel/reading/none-dicoms/index-.vue @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + diff --git a/src/views/trials/trials-panel/reading/visit-review/components/EcrfList.vue b/src/views/trials/trials-panel/reading/visit-review/components/EcrfList.vue new file mode 100644 index 00000000..25c1f1a1 --- /dev/null +++ b/src/views/trials/trials-panel/reading/visit-review/components/EcrfList.vue @@ -0,0 +1,302 @@ + + + + + + + + + {{ $t('trials:readingReport:button:skip') }} + + + {{ $t('common:button:save') }} + + + {{ $t('common:button:submit') }} + + + + + + + + + {{ $t('common:dialogTitle:sign') }} + {{ `(${$t('common:label:sign')}${ currentUser })` }} + + + + + + + + + diff --git a/src/views/trials/trials-panel/reading/visit-review/components/FormItem.vue b/src/views/trials/trials-panel/reading/visit-review/components/FormItem.vue new file mode 100644 index 00000000..5bb253ba --- /dev/null +++ b/src/views/trials/trials-panel/reading/visit-review/components/FormItem.vue @@ -0,0 +1,588 @@ + + + + + {{ question.GroupName }} + + + + + + + + + + + + + + + + + + + + + + + + {formItemChange(val, question)})" + > + + + + + + + + + {formItemChange(val, question)})" + > + + + {{ item.label }} + + + + + {{ val.trim() }} + + + + + + + {{ val.trim() }} + + + + + + + { formItemNumberChange(val, question) }" + > + + + { formItemNumberChange(val, question) }" + @input="limitInput($event, questionForm, question.Id)" + > + {{ question.Unit !== 4 ? $fd('ValueUnit', question.Unit) : question.CustomUnit }} + % + + + {{ question.Unit !== 4 ? $fd('ValueUnit', question.Unit) : question.CustomUnit }} + % + + + + + { formItemChange(val, question) }" + > + + + { formItemChange(val, question) }" + > + + {{ item.trim() }} + + + { formItemNumberChange(val, question) }" + /> + + + + + + + + + + + + + + + + + + + + {{ $t('trials:readingUnit:qsList:message:loading') }}... + + + + + + + + + + + diff --git a/src/views/trials/trials-panel/reading/visit-review/components/ImageViewer.vue b/src/views/trials/trials-panel/reading/visit-review/components/ImageViewer.vue new file mode 100644 index 00000000..1103c401 --- /dev/null +++ b/src/views/trials/trials-panel/reading/visit-review/components/ImageViewer.vue @@ -0,0 +1,509 @@ + + + + + + + + + + 1*1 + 1*2 + 2*2 + + + + + + + + + + + + + + + {{ `${taskInfo.SubjectCode} ${v.taskInfo.TaskBlindName} ` }} + + {{ v.currentFileName }} + + + + + + + + {{ v.taskInfo.TaskBlindName }} + + + + + + + + + + + + + + diff --git a/src/views/trials/trials-panel/reading/visit-review/components/ReadPage.vue b/src/views/trials/trials-panel/reading/visit-review/components/ReadPage.vue new file mode 100644 index 00000000..e5fb379c --- /dev/null +++ b/src/views/trials/trials-panel/reading/visit-review/components/ReadPage.vue @@ -0,0 +1,299 @@ + + + + + + + {{ s.TaskBlindName }} + + + + + + + + + + + + + + + + + + + + {{ currentVisitInfo.SubjectCode }} + + + {{ currentVisitInfo.TaskBlindName }} + + + + + + + + + + + diff --git a/src/views/trials/trials-panel/reading/visit-review/components/ReportPage.vue b/src/views/trials/trials-panel/reading/visit-review/components/ReportPage.vue new file mode 100644 index 00000000..e3e2fb95 --- /dev/null +++ b/src/views/trials/trials-panel/reading/visit-review/components/ReportPage.vue @@ -0,0 +1,3 @@ + + 报告页 + diff --git a/src/views/trials/trials-panel/reading/visit-review/components/StudyList.vue b/src/views/trials/trials-panel/reading/visit-review/components/StudyList.vue new file mode 100644 index 00000000..ba63d16b --- /dev/null +++ b/src/views/trials/trials-panel/reading/visit-review/components/StudyList.vue @@ -0,0 +1,184 @@ + + + + + {{ taskInfo.SubjectCode }} + + + {{ visitTaskInfo.TaskBlindName }} + + + + + + + + {{ study.CodeView }} + + {{ study.BodyPart }} + {{ study.Modality }} + + + + + + + {{ k.FileName }} + + + + + + + + + + diff --git a/src/views/trials/trials-panel/reading/visit-review/index.vue b/src/views/trials/trials-panel/reading/visit-review/index.vue new file mode 100644 index 00000000..ef0d6b4a --- /dev/null +++ b/src/views/trials/trials-panel/reading/visit-review/index.vue @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + diff --git a/src/views/trials/trials-panel/reading/visit-review/js/hardcodedMetaDataProvider.js b/src/views/trials/trials-panel/reading/visit-review/js/hardcodedMetaDataProvider.js new file mode 100644 index 00000000..d12f5196 --- /dev/null +++ b/src/views/trials/trials-panel/reading/visit-review/js/hardcodedMetaDataProvider.js @@ -0,0 +1,68 @@ +// Add hardcoded meta data provider for color images +export default function hardcodedMetaDataProvider(type, imageId, imageIds) { + const colonIndex = imageId.indexOf(':') + const scheme = imageId.substring(0, colonIndex) + if (scheme !== 'web') { + return + } + + if (type === 'imagePixelModule') { + const imagePixelModule = { + pixelRepresentation: 0, + bitsAllocated: 24, + bitsStored: 24, + highBit: 24, + photometricInterpretation: 'RGB', + samplesPerPixel: 3 + } + + return imagePixelModule + } else if (type === 'generalSeriesModule') { + const generalSeriesModule = { + modality: 'SC', + seriesNumber: 1, + seriesDescription: 'Color', + seriesDate: '20190201', + seriesTime: '120000', + seriesInstanceUID: '1.2.276.0.7230010.3.1.4.83233.20190201120000.1' + } + + return generalSeriesModule + } else if (type === 'imagePlaneModule') { + const index = imageIds.indexOf(imageId) + // console.warn(index); + const imagePlaneModule = { + imageOrientationPatient: [1, 0, 0, 0, 1, 0], + imagePositionPatient: [0, 0, index * 5], + pixelSpacing: [1, 1], + columnPixelSpacing: 1, + rowPixelSpacing: 1, + frameOfReferenceUID: 'FORUID', + columns: 2048, + rows: 1216, + rowCosines: [1, 0, 0], + columnCosines: [0, 1, 0], + // setting useDefaultValues to true signals the calibration values above cannot be trusted + // and units should be displayed in pixels + usingDefaultValues: true + } + + return imagePlaneModule + } else if (type === 'voiLutModule') { + return { + // According to the DICOM standard, the width is the number of samples + // in the input, so 256 samples. + windowWidth: [256], + // The center is offset by 0.5 to allow for an integer value for even + // sample counts + windowCenter: [128] + } + } else if (type === 'modalityLutModule') { + return { + rescaleSlope: 1, + rescaleIntercept: 0 + } + } else { + return undefined + } +} diff --git a/src/views/trials/trials-panel/reading/visit-review/js/registerWebImageLoader.js b/src/views/trials/trials-panel/reading/visit-review/js/registerWebImageLoader.js new file mode 100644 index 00000000..46f66cad --- /dev/null +++ b/src/views/trials/trials-panel/reading/visit-review/js/registerWebImageLoader.js @@ -0,0 +1,244 @@ +import * as cornerstone from '@cornerstonejs/core' +const canvas = document.createElement('canvas') +let lastImageIdDrawn + +// Todo: this loader should exist in a separate package in the same monorepo + +/** + * creates a cornerstone Image object for the specified Image and imageId + * + * @param image - An Image + * @param imageId - the imageId for this image + * @returns Cornerstone Image Object + */ +function createImage(image, imageId) { + // extract the attributes we need + const rows = image.naturalHeight + const columns = image.naturalWidth + + function getPixelData(targetBuffer) { + const imageData = getImageData() + + let targetArray + + // Check if targetBuffer is provided for volume viewports + if (targetBuffer) { + targetArray = new Uint8Array( + targetBuffer.arrayBuffer, + targetBuffer.offset, + targetBuffer.length + ) + } else { + targetArray = new Uint8Array(imageData.width * imageData.height * 3) + } + + // modify original image data and remove alpha channel (RGBA to RGB) + convertImageDataToRGB(imageData, targetArray) + + return targetArray + } + + function convertImageDataToRGB(imageData, targetArray) { + for (let i = 0, j = 0; i < imageData.data.length; i += 4, j += 3) { + targetArray[j] = imageData.data[i] + targetArray[j + 1] = imageData.data[i + 1] + targetArray[j + 2] = imageData.data[i + 2] + } + } + + function getImageData() { + let context + + if (lastImageIdDrawn === imageId) { + context = canvas.getContext('2d') + } else { + canvas.height = image.naturalHeight + canvas.width = image.naturalWidth + context = canvas.getContext('2d') + context.drawImage(image, 0, 0) + lastImageIdDrawn = imageId + } + + return context.getImageData(0, 0, image.naturalWidth, image.naturalHeight) + } + + function getCanvas() { + if (lastImageIdDrawn === imageId) { + return canvas + } + + canvas.height = image.naturalHeight + canvas.width = image.naturalWidth + const context = canvas.getContext('2d') + + context.drawImage(image, 0, 0) + lastImageIdDrawn = imageId + + return canvas + } + + // Extract the various attributes we need + return { + imageId, + minPixelValue: 0, + maxPixelValue: 255, + slope: 1, + intercept: 0, + windowCenter: 128, + windowWidth: 255, + getPixelData, + getCanvas, + getImage: () => image, + rows, + columns, + height: rows, + width: columns, + color: true, + // we converted the canvas rgba already to rgb above + rgba: false, + columnPixelSpacing: 1, // for web it's always 1 + rowPixelSpacing: 1, // for web it's always 1 + invert: false, + sizeInBytes: rows * columns * 3, + numberOfComponents: 3 + } +} + +function arrayBufferToImage(arrayBuffer) { + return new Promise((resolve, reject) => { + const image = new Image() + const arrayBufferView = new Uint8Array(arrayBuffer) + const blob = new Blob([arrayBufferView]) + const urlCreator = window.URL || window.webkitURL + const imageUrl = urlCreator.createObjectURL(blob) + + image.src = imageUrl + image.onload = () => { + resolve(image) + urlCreator.revokeObjectURL(imageUrl) + } + + image.onerror = (error) => { + urlCreator.revokeObjectURL(imageUrl) + reject(error) + } + }) +} + +// +// This is a cornerstone image loader for web images such as PNG and JPEG +// +const options = { + // callback allowing customization of the xhr (e.g. adding custom auth headers, cors, etc) + beforeSend: (xhr) => { + // xhr + } +} + +// Loads an image given a url to an image +function loadImage(uri, imageId) { + const xhr = new XMLHttpRequest() + + xhr.open('GET', uri, true) + xhr.responseType = 'arraybuffer' + options.beforeSend(xhr) + + xhr.onprogress = function(oProgress) { + if (oProgress.lengthComputable) { + // evt.loaded the bytes browser receive + // evt.total the total bytes set by the header + const loaded = oProgress.loaded + const total = oProgress.total + const percentComplete = Math.round((loaded / total) * 100) + + const eventDetail = { + imageId, + loaded, + total, + percentComplete + } + + cornerstone.triggerEvent( + cornerstone.eventTarget, + 'cornerstoneimageloadprogress', + eventDetail + ) + } + } + + const promise = new Promise((resolve, reject) => { + xhr.onload = function() { + const imagePromise = arrayBufferToImage(this.response) + + imagePromise + .then((image) => { + const imageObject = createImage(image, imageId) + + resolve(imageObject) + }, reject) + .catch((error) => { + console.error(error) + }) + } + xhr.onerror = function(error) { + reject(error) + } + + xhr.send() + }) + + const cancelFn = () => { + xhr.abort() + } + + return { + promise, + cancelFn + } +} + +function registerWebImageLoader(imageLoader) { + imageLoader.registerImageLoader('web', _loadImageIntoBuffer) +} + +/** + * Small stripped down loader from cornerstoneDICOMImageLoader + * Which doesn't create cornerstone images that we don't need + */ +function _loadImageIntoBuffer(imageId, options) { + const uri = imageId.replace('web:', '') + + const promise = new Promise((resolve, reject) => { + // get the pixel data from the server + loadImage(uri, imageId) + .promise.then( + (image) => { + if ( + !options?.targetBuffer?.length || + !options?.targetBuffer?.offset + ) { + resolve(image) + return + } + + // @ts-ignore + image.getPixelData(options.targetBuffer) + + resolve(true) + }, + (error) => { + reject(error) + } + ) + .catch((error) => { + reject(error) + }) + }) + + return { + promise, + cancelFn: undefined + } +} + +export default registerWebImageLoader