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 @@