pt临床数据更改
continuous-integration/drone/push Build is passing Details

uat_us
caiyiling 2026-04-23 16:32:25 +08:00
parent 946203f124
commit f7dd68f7f3
10 changed files with 398 additions and 49 deletions

View File

@ -285,14 +285,14 @@
<!-- 患者信息 --> <!-- 患者信息 -->
<div class="measureTool-wrapper patient-form" <div class="measureTool-wrapper patient-form"
v-if="isHaveStudyClinicalData && type === 'Study' && modality && ['PT、CT', 'CT、PT', 'PET-CT'].includes(modality)"> v-if="type === 'Study' && modality && ['PT、CT', 'CT、PT', 'PET-CT'].includes(modality)">
<div class="sideTool-title">{{ $t('trials:tab:patientData') }}</div> <div class="sideTool-title">{{ $t('trials:tab:patientData') }}</div>
<div class="sideTool-wrapper"> <div class="sideTool-wrapper">
<el-form ref="patientForm" :model="formData" :rules="rules" label-width="150" v-loading="formLoading"> <el-form ref="patientForm" size="mini" :model="formData" :rules="rules" label-width="150" v-loading="formLoading">
<!-- 性别 --> <!-- 性别 -->
<el-form-item :label="$t('trials:ptData:label:patientSex')" prop="PatientSex"> <el-form-item :label="$t('trials:ptData:label:patientSex')" prop="PatientSex">
<el-select v-model="formData.PatientSex" :placeholder="$t('common:ruleMessage:select')" <el-select v-model="formData.PatientSex" :placeholder="$t('common:ruleMessage:select')"
style="width: 100%" size="small" :disabled="!isEdit"> style="width: 100%" :disabled="!isEdit">
<el-option :label="$t('trials:patientSex:male')" value="M"></el-option> <el-option :label="$t('trials:patientSex:male')" value="M"></el-option>
<el-option :label="$t('trials:patientSex:female')" value="F"></el-option> <el-option :label="$t('trials:patientSex:female')" value="F"></el-option>
</el-select> </el-select>
@ -300,32 +300,32 @@
<!-- 体重kg 例如 70.5--> <!-- 体重kg 例如 70.5-->
<el-form-item :label="$t('trials:ptData:label:patientWeight')" prop="PatientWeight"> <el-form-item :label="$t('trials:ptData:label:patientWeight')" prop="PatientWeight">
<el-input v-model.number="formData.PatientWeight" :placeholder="$t('trials:patientWeight:eg')" <el-input v-model.number="formData.PatientWeight" :placeholder="$t('trials:patientWeight:eg')"
style="width: 100%" size="small" :disabled="!isEdit"></el-input> style="width: 100%" :disabled="!isEdit"></el-input>
</el-form-item> </el-form-item>
<!-- 总剂量Bq 例如 740000000--> <!-- 总剂量Bq 例如 740000000-->
<el-form-item :label="$t('trials:ptData:label:totalDose')" prop="RadionuclideTotalDose"> <el-form-item :label="$t('trials:ptData:label:totalDose')" prop="RadionuclideTotalDose">
<el-input v-model.number="formData.RadionuclideTotalDose" :placeholder="$t('trials:totalDose:eg')" <el-input v-model.number="formData.RadionuclideTotalDose" :placeholder="$t('trials:totalDose:eg')"
style="width: 100%" size="small" :disabled="!isEdit"></el-input> style="width: 100%" :disabled="!isEdit"></el-input>
</el-form-item> </el-form-item>
<!-- 半衰期s 例如 21600--> <!-- 半衰期s 例如 21600-->
<el-form-item :label="$t('trials:ptData:label:halfLife')" prop="RadionuclideHalfLife"> <el-form-item :label="$t('trials:ptData:label:halfLife')" prop="RadionuclideHalfLife">
<el-input v-model.number="formData.RadionuclideHalfLife" :placeholder="$t('trials:halfLife:eg')" <el-input v-model.number="formData.RadionuclideHalfLife" :placeholder="$t('trials:halfLife:eg')"
style="width: 100%" size="small" :disabled="!isEdit"></el-input> style="width: 100%" :disabled="!isEdit"></el-input>
</el-form-item> </el-form-item>
<!-- 注射时间s Unix 相对秒--> <!-- 注射时间s Unix 相对秒-->
<el-form-item :label="$t('trials:ptData:label:injectTime')" prop="RadiopharmaceuticalStartTime"> <el-form-item :label="$t('trials:ptData:label:injectTime')" prop="RadiopharmaceuticalStartTime">
<el-input v-model.number="formData.RadiopharmaceuticalStartTime" :placeholder="$t('trials:injectTime:eg')" <el-input v-model.number="formData.RadiopharmaceuticalStartTime" :placeholder="$t('trials:injectTime:eg')"
style="width: 100%" @input="computeTimeRelation" size="small" :disabled="!isEdit"></el-input> style="width: 100%" @input="computeTimeRelation" :disabled="!isEdit"></el-input>
</el-form-item> </el-form-item>
<!-- 成像时间s Unix 相对秒--> <!-- 成像时间s Unix 相对秒-->
<el-form-item :label="$t('trials:ptData:label:acquisitionTime')" prop="AcquisitionTime"> <el-form-item :label="$t('trials:ptData:label:acquisitionTime')" prop="AcquisitionTime">
<el-input v-model.number="formData.AcquisitionTime" :placeholder="$t('trials:injectTime:eg')" <el-input v-model.number="formData.AcquisitionTime" :placeholder="$t('trials:injectTime:eg')"
style="width: 100%" @input="computeTimeRelation" size="small" :disabled="!isEdit"></el-input> style="width: 100%" @input="computeTimeRelation" :disabled="!isEdit"></el-input>
</el-form-item> </el-form-item>
<!-- 时间一致性检查 --> <!-- 时间一致性检查 -->
<el-form-item :label="$t('trials:ptData:label:timeCheck')"> <!-- <el-form-item :label="$t('trials:ptData:label:timeCheck')">
<el-input v-model="formData.TimeCheck" disabled style="width: 100%" size="small"></el-input> <el-input v-model="formData.TimeCheck" disabled style="width: 100%"></el-input>
</el-form-item> </el-form-item> -->
<!-- 提交 --> <!-- 提交 -->
<el-form-item style="margin-top: 20px;text-align: right;" v-if="isEdit"> <el-form-item style="margin-top: 20px;text-align: right;" v-if="isEdit">
@ -363,6 +363,7 @@ import {
getPatientInfo, getPatientInfo,
editPatientInfo editPatientInfo
} from '@/api/trials' } from '@/api/trials'
import { setPTClinicalDataForInstance } from '@/utils/ptClinicalDataCache'
export default { export default {
name: 'DicomsViewer', name: 'DicomsViewer',
components: { components: {
@ -379,6 +380,20 @@ export default {
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() { data() {
return { return {
isAnonymous: false, isAnonymous: false,
@ -444,7 +459,6 @@ export default {
] ]
}, },
formLoading: false, formLoading: false,
isHaveStudyClinicalData: false,
type: '', type: '',
isEdit: 0 isEdit: 0
} }
@ -459,12 +473,8 @@ export default {
this.wwwcList[0] = '-1' this.wwwcList[0] = '-1'
this.colormapsList = cornerstone.colors.getColormapsList() this.colormapsList = cornerstone.colors.getColormapsList()
this.currentDicomCanvas = this.$refs['dicomCanvas0'] this.currentDicomCanvas = this.$refs['dicomCanvas0']
this.isHaveStudyClinicalData = this.$route.query.isHaveStudyClinicalData === 'true'
this.type = this.$route.query.type this.type = this.$route.query.type
this.isEdit = parseInt(this.$route.query.showDelete) 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: { methods: {
@ -820,7 +830,7 @@ export default {
try { try {
this.formLoading = true this.formLoading = true
let studyId = this.$route.query.studyId let studyId = this.$route.query.studyId
let res = await getPatientInfo({ studyId: studyId }) let res = await getPatientInfo({studyId: studyId})
this.formData = { this.formData = {
Id: res.Result.Id || '', Id: res.Result.Id || '',
PatientSex: res.Result.PatientSex || '', PatientSex: res.Result.PatientSex || '',
@ -832,12 +842,88 @@ export default {
TimeCheck: '' TimeCheck: ''
} }
this.computeTimeRelation() this.computeTimeRelation()
// PT 2D SUV 使/
this.cachePtClinicalDataToInstances()
this.formLoading = false this.formLoading = false
} catch (e) { } catch(e) {
this.formLoading = false this.formLoading = false
console.log(e) 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() { async submitForm() {
try { try {
let valid = await this.$refs.patientForm.validate() let valid = await this.$refs.patientForm.validate()
@ -847,6 +933,8 @@ export default {
this.formLoading = false this.formLoading = false
if (res.IsSuccess) { if (res.IsSuccess) {
this.$message.success(this.$t('common:message:savedSuccessfully')) this.$message.success(this.$t('common:message:savedSuccessfully'))
this.cachePtClinicalDataToInstances()
this.refreshDicomAfterClinicalDataChanged()
} }
} catch (e) { } catch (e) {
this.formLoading = false this.formLoading = false
@ -910,28 +998,28 @@ export default {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
z-index: 9999; 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 { .btnBox {
display: inline-block; display: inline-block;
width: 80px; width: 80px;
@ -1200,11 +1288,20 @@ export default {
} }
.patient-form .el-form-item { .patient-form .el-form-item {
display: flex;
align-items: center;
margin-bottom: 15px; margin-bottom: 15px;
} }
.patient-form .el-form-item__label { .patient-form .el-form-item__label {
color: #d0d0d0; 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 { .patient-form .el-input.is-disabled .el-input__inner {

View File

@ -932,6 +932,12 @@ const actions = {
data.IsDicom = study.IsDicom data.IsDicom = study.IsDicom
data.PreviewImageCount = 0 data.PreviewImageCount = 0
data.IsCriticalSequence = study.IsCriticalSequence 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 = [] var seriesList = []
study.SeriesList.forEach((series, seriesIndex) => { study.SeriesList.forEach((series, seriesIndex) => {
const imageIds = [] const imageIds = []

View File

@ -1,4 +1,6 @@
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 { import {
getImageTypeSubItemFromDataset, getImageTypeSubItemFromDataset,
extractOrientationFromDataset, extractOrientationFromDataset,
@ -6,6 +8,87 @@ import {
extractSpacingFromDataset, extractSpacingFromDataset,
extractSliceThicknessFromDataset, 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 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) { function parseImageId(imageId) {
// build a url by parsing out the url scheme and frame index from the imageId // build a url by parsing out the url scheme and frame index from the imageId
const firstColonIndex = imageId.indexOf(':'); const firstColonIndex = imageId.indexOf(':');
@ -168,6 +251,65 @@ function metaDataProvider(type, imageId) {
if (!dataSet) { if (!dataSet) {
return; 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') { if (type === 'imagePlaneModule') {
// const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6); // const imageOrientationPatient = getNumberValues(dataSet, 'x00200037', 6);
// const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3); // const imagePositionPatient = getNumberValues(dataSet, 'x00200032', 3);

View File

@ -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()
}

View File

@ -539,7 +539,7 @@
</el-tab-pane> </el-tab-pane>
<!-- 其他 --> <!-- 其他 -->
<el-tab-pane :label="$t('trials:reading:tab:others')" name="3"> <el-tab-pane :label="$t('trials:reading:tab:others')" name="3">
<Others v-if="activeName === '3'" /> <Others v-if="activeName === '3'" :imageToolType="1"/>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>

View File

@ -1,6 +1,7 @@
import { metaData } from '@cornerstonejs/core' import { metaData } from '@cornerstonejs/core'
// import { InstanceMetadata } from '@cornerstonejs/calculate-suv' // import { InstanceMetadata } from '@cornerstonejs/calculate-suv'
import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader' import cornerstoneDICOMImageLoader from '@cornerstonejs/dicom-image-loader'
import { getPTClinicalDataForInstance } from '@/utils/ptClinicalDataCache'
function parseImageId(imageId) { function parseImageId(imageId) {
// build a url by parsing out the url scheme and frame index from the imageId // build a url by parsing out the url scheme and frame index from the imageId
const firstColonIndex = imageId.indexOf(':') const firstColonIndex = imageId.indexOf(':')
@ -42,6 +43,9 @@ function getMetaData(type, imageId) {
} }
} }
export default function getPTImageIdInstanceMetadata(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 petSequenceModule = metaData.get('petIsotopeModule', imageId)
const generalSeriesModule = metaData.get('generalSeriesModule', imageId) const generalSeriesModule = metaData.get('generalSeriesModule', imageId)
@ -60,25 +64,36 @@ export default function getPTImageIdInstanceMetadata(imageId) {
const { seriesDate, seriesTime, acquisitionDate, acquisitionTime } = const { seriesDate, seriesTime, acquisitionDate, acquisitionTime } =
generalSeriesModule generalSeriesModule
var { patientWeight } = patientStudyModule var { patientWeight } = patientStudyModule
if (ptClinicalData && ptClinicalData.PatientWeight != null && ptClinicalData.PatientWeight !== '') {
patientWeight = parseFloat(ptClinicalData.PatientWeight)
}
// console.log('更改前:', patientWeight) // console.log('更改前:', patientWeight)
// patientWeight = patientWeight * 10 // patientWeight = patientWeight * 10
// console.log('更改后:', patientWeight) // console.log('更改后:', patientWeight)
const { correctedImage, units, decayCorrection } = ptSeriesModule 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 ( if (
seriesDate === undefined || seriesDate === undefined ||
seriesTime === undefined || seriesTime === undefined ||
patientWeight === undefined || patientWeight === undefined ||
acquisitionDate === undefined || acquisitionDate === undefined ||
acquisitionTime === undefined || (acquisitionTimeOverride || acquisitionTime) === undefined ||
correctedImage === undefined || correctedImage === undefined ||
units === undefined || units === undefined ||
decayCorrection === undefined || decayCorrection === undefined ||
radiopharmaceuticalInfo.radionuclideTotalDose === undefined || totalDose === undefined ||
radiopharmaceuticalInfo.radionuclideHalfLife === undefined || halfLife === undefined ||
(radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime === undefined && (radiopharmaceuticalInfo.radiopharmaceuticalStartDateTime === undefined &&
seriesDate === undefined && seriesDate === undefined &&
radiopharmaceuticalInfo.radiopharmaceuticalStartTime === undefined) (startTimeOverride || radiopharmaceuticalInfo.radiopharmaceuticalStartTime) === undefined)
// //
) { ) {
throw new Error('required metadata are missing') throw new Error('required metadata are missing')
@ -87,14 +102,14 @@ export default function getPTImageIdInstanceMetadata(imageId) {
const instanceMetadata = { const instanceMetadata = {
CorrectedImage: correctedImage, CorrectedImage: correctedImage,
Units: units, Units: units,
RadionuclideHalfLife: radiopharmaceuticalInfo.radionuclideHalfLife, RadionuclideHalfLife: halfLife,
RadionuclideTotalDose: radiopharmaceuticalInfo.radionuclideTotalDose, RadionuclideTotalDose: totalDose,
DecayCorrection: decayCorrection, DecayCorrection: decayCorrection,
PatientWeight: patientWeight, PatientWeight: patientWeight,
SeriesDate: seriesDate, SeriesDate: seriesDate,
SeriesTime: seriesTime, SeriesTime: seriesTime,
AcquisitionDate: acquisitionDate, AcquisitionDate: acquisitionDate,
AcquisitionTime: acquisitionTime AcquisitionTime: acquisitionTimeOverride || acquisitionTime
} }
if ( if (
@ -159,6 +174,10 @@ export default function getPTImageIdInstanceMetadata(imageId) {
instanceMetadata.RadiopharmaceuticalStartTime = timeString instanceMetadata.RadiopharmaceuticalStartTime = timeString
} }
if (startTimeOverride) {
instanceMetadata.RadiopharmaceuticalStartTime = startTimeOverride
}
if ( if (
instanceMetadata.AcquisitionTime && instanceMetadata.AcquisitionTime &&
instanceMetadata.AcquisitionTime !== undefined && instanceMetadata.AcquisitionTime !== undefined &&
@ -201,6 +220,9 @@ export default function getPTImageIdInstanceMetadata(imageId) {
) { ) {
instanceMetadata.PatientSex = patientStudyModule.patientSex instanceMetadata.PatientSex = patientStudyModule.patientSex
} }
if (ptClinicalData && ptClinicalData.PatientSex != null && ptClinicalData.PatientSex !== '') {
instanceMetadata.PatientSex = ptClinicalData.PatientSex
}
if ( if (
patientStudyModule.patientSize && patientStudyModule.patientSize &&
@ -234,3 +256,40 @@ function convertInterfaceDateToString(date) {
} }
export { getPTImageIdInstanceMetadata } 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}`
}

View File

@ -577,6 +577,7 @@ import colorMap from './colorMap.vue'
import RectangleROITool from './tools/RectangleROITool' import RectangleROITool from './tools/RectangleROITool'
import ScaleOverlayTool from './tools/ScaleOverlayTool' import ScaleOverlayTool from './tools/ScaleOverlayTool'
import SegmentBidirectionalTool from './tools/SegmentBidirectionalTool' import SegmentBidirectionalTool from './tools/SegmentBidirectionalTool'
import { setPTClinicalDataForInstance } from '@/utils/ptClinicalDataCache'
import FixedRadiusCircleROITool from './tools/FixedRadiusCircleROITool' import FixedRadiusCircleROITool from './tools/FixedRadiusCircleROITool'
import uploadDicomAndNonedicom from '@/components/uploadDicomAndNonedicom' import uploadDicomAndNonedicom from '@/components/uploadDicomAndNonedicom'
import downloadDicomAndNonedicom from '@/components/downloadDicomAndNonedicom' import downloadDicomAndNonedicom from '@/components/downloadDicomAndNonedicom'
@ -1155,10 +1156,28 @@ export default {
let keySeriesIndex = -1 let keySeriesIndex = -1
const arr = res1.Result const arr = res1.Result
arr.forEach((study, studyIndex) => { 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) => { study.SeriesList.forEach((series, seriesIndex) => {
const imageIds = [] const imageIds = []
const stack = [] const stack = []
series.InstanceInfoList.forEach((instance, instanceIndex) => { series.InstanceInfoList.forEach((instance, instanceIndex) => {
if (hasPtClinicalData) {
setPTClinicalDataForInstance(instance.Id, ptClinicalData)
}
if (study.IsCriticalSequence) { if (study.IsCriticalSequence) {
keyStudyIndex = studyIndex keyStudyIndex = studyIndex
keySeriesIndex = seriesIndex keySeriesIndex = seriesIndex

View File

@ -104,7 +104,7 @@
<span>{{ study.AcquisitionTime }}</span> <span>{{ study.AcquisitionTime }}</span>
</div> </div>
<i slot="reference" class="el-icon-document" <i slot="reference" class="el-icon-document"
style="font-size: 15px;cursor: pointer;color: #428bca;" /> style="font-size: 15px;cursor: pointer;color: #f5f7fa;" />
</el-popover> </el-popover>
</div> </div>
</div> </div>

View File

@ -245,7 +245,7 @@
</el-tab-pane> </el-tab-pane>
<!-- 其他 --> <!-- 其他 -->
<el-tab-pane :label="$t('trials:reading:tab:others')" name="2"> <el-tab-pane :label="$t('trials:reading:tab:others')" name="2">
<Others v-if="activeName === '2'" /> <Others v-if="activeName === '2'" :imageToolType="2"/>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</el-dialog> </el-dialog>

View File

@ -2452,7 +2452,7 @@ export default {
const routeData = this.$router.resolve({ const routeData = this.$router.resolve({
path: `/showdicom?trialId=${this.trialId}&subjectVisitId=${this.data.Id path: `/showdicom?trialId=${this.trialId}&subjectVisitId=${this.data.Id
}&studyId=${row.StudyId}&showDelete=${this.isAudit ? 0 : 1 }&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') this.open = window.open(routeData.href, '_blank')
}, },