diff --git a/src/components/Dicom/DicomCanvas.vue b/src/components/Dicom/DicomCanvas.vue index 1d9f207a..45721380 100644 --- a/src/components/Dicom/DicomCanvas.vue +++ b/src/components/Dicom/DicomCanvas.vue @@ -79,16 +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 @@ -219,6 +221,11 @@ 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 @@ -388,7 +395,7 @@ export default { 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') diff --git a/src/components/Dicom/DicomViewer.vue b/src/components/Dicom/DicomViewer.vue index c446a914..d6485891 100644 --- a/src/components/Dicom/DicomViewer.vue +++ b/src/components/Dicom/DicomViewer.vue @@ -296,14 +296,14 @@
+ v-if="type === 'Study' && modality && ['PT、CT', 'CT、PT', 'PET-CT'].includes(modality)">
{{ $t('trials:tab:patientData') }}
- + + style="width: 100%" :disabled="!isEdit"> @@ -311,32 +311,32 @@ + style="width: 100%" :disabled="!isEdit"> + style="width: 100%" :disabled="!isEdit"> + style="width: 100%" :disabled="!isEdit"> + style="width: 100%" @input="computeTimeRelation" :disabled="!isEdit"> + style="width: 100%" @input="computeTimeRelation" :disabled="!isEdit"> - - - + @@ -374,6 +374,7 @@ import { getPatientInfo, editPatientInfo } from '@/api/trials' +import { setPTClinicalDataForInstance, clearPTClinicalDataCache } from '@/utils/ptClinicalDataCache' export default { name: 'DicomsViewer', components: { @@ -394,6 +395,20 @@ export default { default: '' } }, + watch: { + modality: { + immediate: true, + handler(v) { + if (v) { + if (this.type === 'Study' && ['PT、CT', 'CT、PT', 'PET-CT'].includes(v)) { + this.$nextTick(()=>{ + this.getPatientInfo() + }) + } + } + } + } + }, data() { return { hasAnonymous: false, @@ -460,7 +475,6 @@ export default { ] }, formLoading: false, - isHaveStudyClinicalData: false, type: '', isEdit: 0 } @@ -477,12 +491,11 @@ export default { this.wwwcList[0] = '-1' this.colormapsList = cornerstone.colors.getColormapsList() this.currentDicomCanvas = this.$refs['dicomCanvas0'] - this.isHaveStudyClinicalData = this.$route.query.isHaveStudyClinicalData === 'true' this.type = this.$route.query.type this.isEdit = parseInt(this.$route.query.showDelete) - if (this.isHaveStudyClinicalData && this.type === 'Study' && ['PT、CT', 'CT、PT', 'PET-CT'].includes(this.modality)) { - this.getPatientInfo() - } + }, + beforeDestroy() { + clearPTClinicalDataCache() }, methods: { @@ -712,7 +725,17 @@ export default { this.currentDicomCanvas.toolState.clipPlaying = false this.$nextTick(() => { this.series = Object.assign({}, dicomSeries) + this.seriesList = [this.series] this.currentDicomCanvas.loadImageStack(this.series) + if ( + this.formData.PatientWeight != null || + this.formData.RadionuclideTotalDose != null || + this.formData.RadionuclideHalfLife != null || + this.formData.RadiopharmaceuticalStartTime != null || + this.formData.AcquisitionTime != null + ) { + this.cachePtClinicalDataToInstances() + } }) }, loadOtherImageStack(seriesList) { @@ -726,6 +749,15 @@ export default { this.$refs[`dicomCanvas${canvasIndex}`].loadImageStack(series) } }) + if ( + this.formData.PatientWeight != null || + this.formData.RadionuclideTotalDose != null || + this.formData.RadionuclideHalfLife != null || + this.formData.RadiopharmaceuticalStartTime != null || + this.formData.AcquisitionTime != null + ) { + this.cachePtClinicalDataToInstances() + } }) }, activateDicomCanvas(index) { @@ -988,7 +1020,7 @@ export default { try { this.formLoading = true let studyId = this.$route.query.studyId - let res = await getPatientInfo({ studyId: studyId }) + let res = await getPatientInfo({studyId: studyId}) this.formData = { Id: res.Result.Id || '', PatientSex: res.Result.PatientSex || '', @@ -1000,12 +1032,88 @@ export default { TimeCheck: '' } this.computeTimeRelation() + // 缓存 PT 临床数据:用于 2D SUV 计算时优先使用接口/人工录入值 + this.cachePtClinicalDataToInstances() this.formLoading = false - } catch (e) { + } catch(e) { this.formLoading = false console.log(e) } }, + cachePtClinicalDataToInstances() { + const clinicalData = { + PatientSex: this.formData.PatientSex, + PatientWeight: this.formData.PatientWeight, + RadionuclideTotalDose: this.formData.RadionuclideTotalDose, + RadionuclideHalfLife: this.formData.RadionuclideHalfLife, + RadiopharmaceuticalStartTime: this.formData.RadiopharmaceuticalStartTime, + AcquisitionTime: this.formData.AcquisitionTime + } + const seriesList = Array.isArray(this.seriesList) ? this.seriesList : [] + seriesList.forEach(series => { + const instanceInfoList = series?.instanceInfoList + if (Array.isArray(instanceInfoList) && instanceInfoList.length > 0) { + instanceInfoList.forEach(instance => { + if (instance && instance.Id != null) { + setPTClinicalDataForInstance(instance.Id, clinicalData) + } + }) + return + } + const imageIds = series?.imageIds + if (Array.isArray(imageIds) && imageIds.length > 0) { + imageIds.forEach(imageId => { + const instanceId = this.getInstanceIdFromImageId(imageId) + if (instanceId) { + setPTClinicalDataForInstance(instanceId, clinicalData) + } + }) + } + }) + }, + getInstanceIdFromImageId(imageId) { + try { + const qIndex = String(imageId).indexOf('?') + if (qIndex === -1) return null + const params = new URLSearchParams(String(imageId).slice(qIndex + 1)) + const instanceId = params.get('instanceId') + return instanceId ? String(instanceId).trim() : null + } catch (e) { + return null + } + }, + refreshDicomAfterClinicalDataChanged() { + // 患者信息保存后,强制刷新画布与标注统计,确保 SUV 等数据显示使用最新的 PT 临床数据口径 + const toolTypes = [ + 'Probe', + 'RectangleRoi', + 'EllipticalRoi', + 'FreehandRoi', + 'Bidirectional', + 'Length', + 'ArrowAnnotate', + 'Angle', + 'CobbAngle' + ] + const elements = document.querySelectorAll('.dicom-item') + Array.from(elements).forEach((wrapper) => { + const index = wrapper.getAttribute('data-index') + const canvasComp = index !== null ? this.$refs[`dicomCanvas${index}`] : null + const element = canvasComp?.$refs?.canvas + if (!element) return + toolTypes.forEach((toolType) => { + const toolState = cornerstoneTools.getToolState(element, toolType) + if (toolState && Array.isArray(toolState.data)) { + toolState.data.forEach((d) => { + if (d && typeof d === 'object') { + d.invalidated = true + } + }) + } + }) + cornerstone.updateImage(element, true) + }) + }, async submitForm() { try { let valid = await this.$refs.patientForm.validate() @@ -1015,6 +1123,8 @@ export default { this.formLoading = false if (res.IsSuccess) { this.$message.success(this.$t('common:message:savedSuccessfully')) + this.cachePtClinicalDataToInstances() + this.refreshDicomAfterClinicalDataChanged() } } catch (e) { this.formLoading = false @@ -1104,6 +1214,26 @@ export default { } } +.Anonymous .btn { + width: 15%; + text-align: center; + height: 40px; + line-height: 30px; + border-radius: 15px; + background-color: rgba(255, 255, 255, .3); + cursor: pointer; + padding: 5px 10px; + border: 1px solid rgba(255, 255, 255, .7); +} +.Anonymous .btn:hover { + background-color: rgba(255, 255, 255, .5); +} + +.Anonymous .activeBtn { + background-color: rgba(255, 255, 255, .5); +} + + .btnBox { display: inline-block; width: 80px; @@ -1372,11 +1502,20 @@ export default { } .patient-form .el-form-item { + display: flex; + align-items: center; margin-bottom: 15px; } .patient-form .el-form-item__label { color: #d0d0d0; + flex: 0 0 150px; + text-align: left; +} + +.patient-form .el-form-item__content { + flex: 1; + margin-left: 0; } .patient-form .el-input.is-disabled .el-input__inner { diff --git a/src/store/modules/reading.js b/src/store/modules/reading.js index b61c175b..37dd90f0 100644 --- a/src/store/modules/reading.js +++ b/src/store/modules/reading.js @@ -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 = [] diff --git a/src/utils/metaDataProvider.js b/src/utils/metaDataProvider.js index d9c38b06..c5c59ae8 100644 --- a/src/utils/metaDataProvider.js +++ b/src/utils/metaDataProvider.js @@ -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"); + + // 场景1:trials/dicoms,imageId 带 visitTaskId + idx,优先使用列表中的最新 study 值 + if (visitTaskId && idx) { + const parts = idx.split("|"); + const studyIndex = toNumber(parts[0]); + if (Number.isInteger(studyIndex) && studyIndex >= 0) { + const visitTaskList = store.state.reading.visitTaskList; + if (Array.isArray(visitTaskList)) { + const visitTaskInfo = visitTaskList.find( + (v) => v && String(v.VisitTaskId) === visitTaskId + ); + const study = visitTaskInfo?.StudyList?.[studyIndex]; + if ( + study && + !( + study.PatientWeight == null && + study.RadionuclideTotalDose == null && + study.RadionuclideHalfLife == null && + study.RadiopharmaceuticalStartTime == null && + study.AcquisitionTime == null && + study.PatientSex == null + ) + ) { + return study; + } + } + } + } + + // 场景2:src/components/Dicom,imageId 仅带 instanceId,回退到实例缓存 + const instanceId = params.get("instanceId"); + if (instanceId) { + const cached = getPTClinicalDataForInstance(instanceId); + if (cached) return cached; + } + + return null; + } catch (e) { + return null; + } +} function parseImageId(imageId) { - // 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); diff --git a/src/utils/ptClinicalDataCache.js b/src/utils/ptClinicalDataCache.js new file mode 100644 index 00000000..55d76550 --- /dev/null +++ b/src/utils/ptClinicalDataCache.js @@ -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() +} diff --git a/src/views/trials/trials-panel/reading/ad-review/index.vue b/src/views/trials/trials-panel/reading/ad-review/index.vue index 085140c4..0a85c255 100644 --- a/src/views/trials/trials-panel/reading/ad-review/index.vue +++ b/src/views/trials/trials-panel/reading/ad-review/index.vue @@ -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') diff --git a/src/views/trials/trials-panel/reading/dicoms/components/DicomCanvas.vue b/src/views/trials/trials-panel/reading/dicoms/components/DicomCanvas.vue index 3cba61c9..e4ac3899 100644 --- a/src/views/trials/trials-panel/reading/dicoms/components/DicomCanvas.vue +++ b/src/views/trials/trials-panel/reading/dicoms/components/DicomCanvas.vue @@ -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) { diff --git a/src/views/trials/trials-panel/reading/dicoms/components/DicomViewer.vue b/src/views/trials/trials-panel/reading/dicoms/components/DicomViewer.vue index 72c31829..cbd3f60f 100644 --- a/src/views/trials/trials-panel/reading/dicoms/components/DicomViewer.vue +++ b/src/views/trials/trials-panel/reading/dicoms/components/DicomViewer.vue @@ -539,7 +539,7 @@ - + diff --git a/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/getPTImageIdInstanceMetadata.js b/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/getPTImageIdInstanceMetadata.js index 143770d9..f0e1c75b 100644 --- a/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/getPTImageIdInstanceMetadata.js +++ b/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/getPTImageIdInstanceMetadata.js @@ -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 +} diff --git a/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/ptScalingMetaDataProvider.js b/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/ptScalingMetaDataProvider.js index 33e55fd4..f91066a4 100644 --- a/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/ptScalingMetaDataProvider.js +++ b/src/views/trials/trials-panel/reading/dicoms/components/Fusion/js/ptScalingMetaDataProvider.js @@ -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 } diff --git a/src/views/trials/trials-panel/reading/dicoms/components/StudyList.vue b/src/views/trials/trials-panel/reading/dicoms/components/StudyList.vue index 67e70262..ba349335 100644 --- a/src/views/trials/trials-panel/reading/dicoms/components/StudyList.vue +++ b/src/views/trials/trials-panel/reading/dicoms/components/StudyList.vue @@ -99,6 +99,37 @@
{{ series.description }}
+
+ +

{{ $t('trials:ptData:title') }}

+
+ + {{ study.PatientSex }} +
+
+ + {{ study.PatientWeight }} +
+
+ + {{ study.RadionuclideTotalDose }} +
+
+ + {{ study.RadionuclideHalfLife }} +
+
+ + {{ study.RadiopharmaceuticalStartTime }} +
+
+ + {{ study.AcquisitionTime }} +
+ +
+

@@ -1108,3 +1139,46 @@ export default { background-color: #213a54; } + diff --git a/src/views/trials/trials-panel/reading/dicoms/customize/CustomizeDicomCanvas.vue b/src/views/trials/trials-panel/reading/dicoms/customize/CustomizeDicomCanvas.vue index eea7bdf4..92e832e6 100644 --- a/src/views/trials/trials-panel/reading/dicoms/customize/CustomizeDicomCanvas.vue +++ b/src/views/trials/trials-panel/reading/dicoms/customize/CustomizeDicomCanvas.vue @@ -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) { diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue b/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue index 335e5b22..70f042bc 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue @@ -577,6 +577,7 @@ import colorMap from './colorMap.vue' import RectangleROITool from './tools/RectangleROITool' import ScaleOverlayTool from './tools/ScaleOverlayTool' import SegmentBidirectionalTool from './tools/SegmentBidirectionalTool' +import { setPTClinicalDataForInstance, clearPTClinicalDataCache } from '@/utils/ptClinicalDataCache' import FixedRadiusCircleROITool from './tools/FixedRadiusCircleROITool' import uploadDicomAndNonedicom from '@/components/uploadDicomAndNonedicom' import downloadDicomAndNonedicom from '@/components/downloadDicomAndNonedicom' @@ -1155,10 +1156,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 @@ -4404,6 +4427,7 @@ export default { }, }, beforeDestroy() { + clearPTClinicalDataCache() DicomEvent.$off('isCanActiveNoneDicomTool') DicomEvent.$off('removeNoneDicomMeasureData') DicomEvent.$off('addNoneDicomMeasureData') diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/StudyList.vue b/src/views/trials/trials-panel/reading/dicoms3D/components/StudyList.vue index 779e9e45..b5595516 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/StudyList.vue +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/StudyList.vue @@ -76,7 +76,7 @@

{{ series.Description }} -
+

{{ $t('trials:ptData:title') }}

@@ -104,7 +104,7 @@ {{ study.AcquisitionTime }}
+ style="font-size: 15px;cursor: pointer;color: #f5f7fa;" />
diff --git a/src/views/trials/trials-panel/reading/visit-review/components/FileViewer.vue b/src/views/trials/trials-panel/reading/visit-review/components/FileViewer.vue index d867d4ee..73605664 100644 --- a/src/views/trials/trials-panel/reading/visit-review/components/FileViewer.vue +++ b/src/views/trials/trials-panel/reading/visit-review/components/FileViewer.vue @@ -245,7 +245,7 @@ - + diff --git a/src/views/trials/trials-panel/visit/crc-upload/components/uploadPetClinicalData.vue b/src/views/trials/trials-panel/visit/crc-upload/components/uploadPetClinicalData.vue index fda8ce2d..bf5f8fb3 100644 --- a/src/views/trials/trials-panel/visit/crc-upload/components/uploadPetClinicalData.vue +++ b/src/views/trials/trials-panel/visit/crc-upload/components/uploadPetClinicalData.vue @@ -240,6 +240,7 @@ v-model="formData.PatientSex" :placeholder="$t('common:ruleMessage:select')" style="width: 100%" + :disabled="!allowAddOrEdit" > @@ -251,6 +252,7 @@ v-model.number="formData.PatientWeight" :placeholder="$t('trials:patientWeight:eg')" style="width: 100%" + :disabled="!allowAddOrEdit" >
@@ -261,6 +263,7 @@ v-model.number="formData.RadionuclideTotalDose" :placeholder="$t('trials:totalDose:eg')" style="width: 100%" + :disabled="!allowAddOrEdit" > @@ -269,6 +272,7 @@ v-model.number="formData.RadionuclideHalfLife" :placeholder="$t('trials:halfLife:eg')" style="width: 100%" + :disabled="!allowAddOrEdit" >
@@ -280,6 +284,7 @@ :placeholder="$t('trials:injectTime:eg')" style="width: 100%" @input="computeTimeRelation" + :disabled="!allowAddOrEdit" > @@ -289,6 +294,7 @@ :placeholder="$t('trials:injectTime:eg')" style="width: 100%" @input="computeTimeRelation" + :disabled="!allowAddOrEdit" > @@ -303,7 +309,7 @@ - + {{ $t('trials:ptData:button:submit') }} diff --git a/src/views/trials/trials-panel/visit/qc-check/components/qualityAssurance.vue b/src/views/trials/trials-panel/visit/qc-check/components/qualityAssurance.vue index 8e5a25f2..d9b21e6e 100644 --- a/src/views/trials/trials-panel/visit/qc-check/components/qualityAssurance.vue +++ b/src/views/trials/trials-panel/visit/qc-check/components/qualityAssurance.vue @@ -2452,7 +2452,7 @@ export default { const routeData = this.$router.resolve({ path: `/showdicom?trialId=${this.trialId}&subjectVisitId=${this.data.Id }&studyId=${row.StudyId}&showDelete=${this.isAudit ? 0 : 1 - }&TokenKey=${token}&type=Study&isHaveStudyClinicalData=${this.IsHaveStudyClinicalData}`, + }&TokenKey=${token}&type=Study`, }) this.open = window.open(routeData.href, '_blank') },