From f7dd68f7f33b668f1c0baf6113c879e8bf8ee119 Mon Sep 17 00:00:00 2001 From: caiyiling <1321909229@qq.com> Date: Thu, 23 Apr 2026 16:32:25 +0800 Subject: [PATCH] =?UTF-8?q?pt=E4=B8=B4=E5=BA=8A=E6=95=B0=E6=8D=AE=E6=9B=B4?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Dicom/DicomViewer.vue | 173 ++++++++++++++---- src/store/modules/reading.js | 6 + src/utils/metaDataProvider.js | 142 ++++++++++++++ src/utils/ptClinicalDataCache.js | 26 +++ .../reading/dicoms/components/DicomViewer.vue | 2 +- .../Fusion/js/getPTImageIdInstanceMetadata.js | 73 +++++++- .../reading/dicoms3D/components/ReadPage.vue | 19 ++ .../reading/dicoms3D/components/StudyList.vue | 2 +- .../visit-review/components/FileViewer.vue | 2 +- .../qc-check/components/qualityAssurance.vue | 2 +- 10 files changed, 398 insertions(+), 49 deletions(-) create mode 100644 src/utils/ptClinicalDataCache.js diff --git a/src/components/Dicom/DicomViewer.vue b/src/components/Dicom/DicomViewer.vue index 58ebfa96..9ce5a84e 100644 --- a/src/components/Dicom/DicomViewer.vue +++ b/src/components/Dicom/DicomViewer.vue @@ -285,14 +285,14 @@
+ v-if="type === 'Study' && modality && ['PT、CT', 'CT、PT', 'PET-CT'].includes(modality)">
{{ $t('trials:tab:patientData') }}
- + + style="width: 100%" :disabled="!isEdit"> @@ -300,32 +300,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"> - - - + @@ -363,6 +363,7 @@ import { getPatientInfo, editPatientInfo } from '@/api/trials' +import { setPTClinicalDataForInstance } from '@/utils/ptClinicalDataCache' export default { name: 'DicomsViewer', components: { @@ -379,6 +380,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 { isAnonymous: false, @@ -444,7 +459,6 @@ export default { ] }, formLoading: false, - isHaveStudyClinicalData: false, type: '', isEdit: 0 } @@ -459,12 +473,8 @@ 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() - } }, methods: { @@ -820,7 +830,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 || '', @@ -832,12 +842,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() @@ -847,6 +933,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 @@ -910,28 +998,28 @@ export default { align-items: center; justify-content: space-between; z-index: 9999; - - .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); - - &:hover { - background-color: rgba(255, 255, 255, .5); - } - } - - .activeBtn { - background-color: rgba(255, 255, 255, .5); - } } +.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; @@ -1200,11 +1288,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..85466ac2 100644 --- a/src/utils/metaDataProvider.js +++ b/src/utils/metaDataProvider.js @@ -1,4 +1,6 @@ import * as cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader' +import store from '@/store' +import { getPTClinicalDataForInstance } from '@/utils/ptClinicalDataCache' import { getImageTypeSubItemFromDataset, extractOrientationFromDataset, @@ -6,6 +8,87 @@ import { extractSpacingFromDataset, extractSliceThicknessFromDataset, } 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 parseDicomTimeToObject(value) { + if (value === undefined || value === null || value === '') return null; + const raw = String(value).trim(); + if (!raw) return null; + const cleaned = raw.replace(/[^\d.]/g, ''); + if (!cleaned) return null; + const [baseRaw, fracRaw] = cleaned.split('.'); + const base = `${baseRaw || ''}`.padStart(6, '0').slice(-6); + const hours = toNumber(base.slice(0, 2)) ?? 0; + const minutes = toNumber(base.slice(2, 4)) ?? 0; + const seconds = toNumber(base.slice(4, 6)) ?? 0; + const fractionalSeconds = `${fracRaw || ''}`.padEnd(6, '0').slice(0, 6); + if (!Number.isFinite(hours) || !Number.isFinite(minutes) || !Number.isFinite(seconds)) return null; + return { hours, minutes, seconds, fractionalSeconds }; +} + +function getFirstSequenceItemDataSet(dataSet, tag) { + const el = dataSet?.elements?.[tag]; + const item = el?.items?.[0]; + return item?.dataSet; +} + +function getIntString(dataSet, tag) { + if (!dataSet) return undefined; + if (typeof dataSet.intString === 'function') { + return dataSet.intString(tag); + } + const raw = dataSet.string(tag); + const n = raw != null ? parseInt(raw, 10) : NaN; + return Number.isFinite(n) ? n : undefined; +} + +function getPTClinicalOverrideFromImageId(imageId) { + try { + const qIndex0 = imageId.indexOf('?'); + if (qIndex0 !== -1) { + const params0 = new URLSearchParams(imageId.slice(qIndex0 + 1)); + const instanceId0 = params0.get('instanceId'); + if (instanceId0) { + const cached = getPTClinicalDataForInstance(instanceId0); + if (cached) return cached; + } + } + 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'); + if (!visitTaskId || !idx) return null; + const parts = idx.split('|'); + const studyIndex = toNumber(parts[0]); + if (!Number.isInteger(studyIndex) || studyIndex < 0) return null; + + const visitTaskList = store?.state?.reading?.visitTaskList; + if (!Array.isArray(visitTaskList)) return null; + const visitTaskInfo = visitTaskList.find(v => v && v.VisitTaskId === visitTaskId); + const study = visitTaskInfo?.StudyList?.[studyIndex]; + if (!study) return null; + + if ( + study.PatientWeight == null && + study.RadionuclideTotalDose == null && + study.RadionuclideHalfLife == null && + study.RadiopharmaceuticalStartTime == null && + study.AcquisitionTime == null && + study.PatientSex == null + ) { + return null; + } + return study; + } 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(':'); @@ -168,6 +251,65 @@ function metaDataProvider(type, imageId) { if (!dataSet) { return; } + const ptOverride = getPTClinicalOverrideFromImageId(imageId); + if (type === 'generalSeriesModule') { + // 参照 cornerstoneWADOImageLoader 的 module 结构返回,避免调用方拿不到预期字段 + const dicomParser = cornerstoneWADOImageLoader?.external?.dicomParser; + const modality = dataSet.string('x00080060'); + const seriesDateRaw = dataSet.string('x00080021') || ''; + const seriesTimeRaw = dataSet.string('x00080031') || ''; + const acquisitionDateRaw = dataSet.string('x00080022') || ''; + const acquisitionTimeRaw = dataSet.string('x00080032') || ''; + const seriesTimeValue = ptOverride?.AcquisitionTime ?? seriesTimeRaw; + return { + modality, + seriesInstanceUID: dataSet.string('x0020000e'), + seriesNumber: getIntString(dataSet, 'x00200011'), + studyInstanceUID: dataSet.string('x0020000d'), + seriesDate: dicomParser?.parseDA ? dicomParser.parseDA(seriesDateRaw) : seriesDateRaw, + seriesTime: dicomParser?.parseTM ? dicomParser.parseTM(seriesTimeValue || '') : parseDicomTimeToObject(seriesTimeValue), + acquisitionDate: dicomParser?.parseDA ? dicomParser.parseDA(acquisitionDateRaw) : acquisitionDateRaw, + acquisitionTime: dicomParser?.parseTM ? dicomParser.parseTM(acquisitionTimeRaw || '') : parseDicomTimeToObject(acquisitionTimeRaw) + }; + } + if (type === 'patientStudyModule') { + // 参照 cornerstoneWADOImageLoader 的 module 结构返回 + const patientWeightRaw = dataSet.floatString('x00101030') || dataSet.string('x00101030'); + // const patientSexRaw = dataSet.string('x00100040'); + const patientSizeRaw = dataSet.floatString('x00101020') || dataSet.string('x00101020'); + const patientAge = getIntString(dataSet, 'x00101010'); + const patientWeight = toNumber(ptOverride?.PatientWeight ?? patientWeightRaw); + // const patientSex = ptOverride?.PatientSex ?? patientSexRaw; + const patientSize = toNumber(patientSizeRaw); + return { + patientAge, + patientWeight, + // patientSex, + patientSize + }; + } + if (type === 'petIsotopeModule') { + // 统一 SUV 口径:优先使用接口/人工录入的 PT 临床数据,缺失时回退读取 DICOM Tag + // 同时保持与 cornerstoneWADOImageLoader 返回结构一致(radiopharmaceuticalInfo.*) + const dicomParser = cornerstoneWADOImageLoader?.external?.dicomParser; + const radioPharmItem = getFirstSequenceItemDataSet(dataSet, 'x00540016'); + const startTimeRaw = radioPharmItem?.string?.('x00181072') || dataSet.string('x00181072'); + const totalDoseRaw = radioPharmItem?.floatString?.('x00181074') || radioPharmItem?.string?.('x00181074') || dataSet.floatString('x00181074') || dataSet.string('x00181074'); + const halfLifeRaw = radioPharmItem?.floatString?.('x00181075') || radioPharmItem?.string?.('x00181075') || dataSet.floatString('x00181075') || dataSet.string('x00181075'); + const startTimeValue = ptOverride?.RadiopharmaceuticalStartTime ?? startTimeRaw; + const radiopharmaceuticalStartTime = dicomParser?.parseTM + ? dicomParser.parseTM(startTimeValue || '') + : parseDicomTimeToObject(startTimeValue); + const radionuclideTotalDose = toNumber(ptOverride?.RadionuclideTotalDose ?? totalDoseRaw); + const radionuclideHalfLife = toNumber(ptOverride?.RadionuclideHalfLife ?? halfLifeRaw); + return { + radiopharmaceuticalInfo: { + radiopharmaceuticalStartTime, + radionuclideTotalDose, + radionuclideHalfLife + } + }; + } if (type === 'imagePlaneModule') { // const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6); // const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3); diff --git a/src/utils/ptClinicalDataCache.js b/src/utils/ptClinicalDataCache.js new file mode 100644 index 00000000..887fef3c --- /dev/null +++ b/src/utils/ptClinicalDataCache.js @@ -0,0 +1,26 @@ +// 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 clearPTClinicalDataCache() { + instanceIdToClinicalData.clear() +} + 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..1decd99c 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,6 +1,7 @@ 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(':') @@ -42,6 +43,9 @@ function getMetaData(type, imageId) { } } export default function getPTImageIdInstanceMetadata(imageId) { + const instanceId = getInstanceIdFromImageId(imageId) + // 统一 SUV 口径:若接口存在 PT 临床数据,则优先覆盖 DICOM 元数据 + const ptClinicalData = instanceId ? getPTClinicalDataForInstance(instanceId) : null const petSequenceModule = metaData.get('petIsotopeModule', imageId) const generalSeriesModule = metaData.get('generalSeriesModule', imageId) @@ -60,25 +64,36 @@ export default function getPTImageIdInstanceMetadata(imageId) { const { seriesDate, seriesTime, acquisitionDate, acquisitionTime } = generalSeriesModule var { patientWeight } = patientStudyModule + if (ptClinicalData && ptClinicalData.PatientWeight != null && ptClinicalData.PatientWeight !== '') { + patientWeight = parseFloat(ptClinicalData.PatientWeight) + } // console.log('更改前:', patientWeight) // patientWeight = patientWeight * 10 // console.log('更改后:', patientWeight) const { correctedImage, units, decayCorrection } = ptSeriesModule + const totalDose = ptClinicalData && ptClinicalData.RadionuclideTotalDose != null && ptClinicalData.RadionuclideTotalDose !== '' + ? parseFloat(ptClinicalData.RadionuclideTotalDose) + : radiopharmaceuticalInfo.radionuclideTotalDose + const halfLife = ptClinicalData && ptClinicalData.RadionuclideHalfLife != null && ptClinicalData.RadionuclideHalfLife !== '' + ? parseFloat(ptClinicalData.RadionuclideHalfLife) + : radiopharmaceuticalInfo.radionuclideHalfLife + const startTimeOverride = ptClinicalData ? normalizeDicomTime(ptClinicalData.RadiopharmaceuticalStartTime) : null + const acquisitionTimeOverride = ptClinicalData ? normalizeDicomTime(ptClinicalData.AcquisitionTime) : null if ( seriesDate === undefined || seriesTime === undefined || patientWeight === undefined || acquisitionDate === undefined || - acquisitionTime === undefined || + (acquisitionTimeOverride || acquisitionTime) === undefined || correctedImage === undefined || units === undefined || decayCorrection === undefined || - radiopharmaceuticalInfo.radionuclideTotalDose === undefined || - radiopharmaceuticalInfo.radionuclideHalfLife === undefined || + totalDose === undefined || + halfLife === undefined || (radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime === undefined && seriesDate === undefined && - radiopharmaceuticalInfo.radiopharmaceuticalStartTime === undefined) + (startTimeOverride || radiopharmaceuticalInfo.radiopharmaceuticalStartTime) === undefined) // ) { throw new Error('required metadata are missing') @@ -87,14 +102,14 @@ export default function getPTImageIdInstanceMetadata(imageId) { const instanceMetadata = { CorrectedImage: correctedImage, Units: units, - RadionuclideHalfLife: radiopharmaceuticalInfo.radionuclideHalfLife, - RadionuclideTotalDose: radiopharmaceuticalInfo.radionuclideTotalDose, + RadionuclideHalfLife: halfLife, + RadionuclideTotalDose: totalDose, DecayCorrection: decayCorrection, PatientWeight: patientWeight, SeriesDate: seriesDate, SeriesTime: seriesTime, AcquisitionDate: acquisitionDate, - AcquisitionTime: acquisitionTime + AcquisitionTime: acquisitionTimeOverride || acquisitionTime } if ( @@ -159,6 +174,10 @@ export default function getPTImageIdInstanceMetadata(imageId) { instanceMetadata.RadiopharmaceuticalStartTime = timeString } + if (startTimeOverride) { + instanceMetadata.RadiopharmaceuticalStartTime = startTimeOverride + } + if ( instanceMetadata.AcquisitionTime && instanceMetadata.AcquisitionTime !== undefined && @@ -201,6 +220,9 @@ export default function getPTImageIdInstanceMetadata(imageId) { ) { instanceMetadata.PatientSex = patientStudyModule.patientSex } + if (ptClinicalData && ptClinicalData.PatientSex != null && ptClinicalData.PatientSex !== '') { + instanceMetadata.PatientSex = ptClinicalData.PatientSex + } if ( patientStudyModule.patientSize && @@ -234,3 +256,40 @@ function convertInterfaceDateToString(date) { } 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 (value === undefined || value === null || 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}` +} 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..09adf380 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 } from '@/utils/ptClinicalDataCache' import FixedRadiusCircleROITool from './tools/FixedRadiusCircleROITool' import uploadDicomAndNonedicom from '@/components/uploadDicomAndNonedicom' import downloadDicomAndNonedicom from '@/components/downloadDicomAndNonedicom' @@ -1155,10 +1156,28 @@ export default { let keySeriesIndex = -1 const arr = res1.Result arr.forEach((study, studyIndex) => { + // 缓存接口返回的 PT 临床数据:用于后续统一 SUV 计算(3D suvFactor 计算优先用接口值) + const ptClinicalData = { + PatientSex: study.PatientSex, + PatientWeight: study.PatientWeight, + RadionuclideTotalDose: study.RadionuclideTotalDose, + RadionuclideHalfLife: study.RadionuclideHalfLife, + RadiopharmaceuticalStartTime: study.RadiopharmaceuticalStartTime, + AcquisitionTime: study.AcquisitionTime + } + const hasPtClinicalData = + 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) { + setPTClinicalDataForInstance(instance.Id, ptClinicalData) + } if (study.IsCriticalSequence) { keyStudyIndex = studyIndex keySeriesIndex = seriesIndex 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..f58686e8 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/StudyList.vue +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/StudyList.vue @@ -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/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') },