diff --git a/package.json b/package.json index 8641f081..79ed4a88 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,10 @@ "dependencies": { "@aws-sdk/client-s3": "3.726.1", "@cornerstonejs/adapters": "^4.19.2", - "@cornerstonejs/polymorphic-segmentation": "4.19.2", "@cornerstonejs/calculate-suv": "^1.1.0", "@cornerstonejs/core": "^4.19.2", "@cornerstonejs/dicom-image-loader": "^4.19.2", + "@cornerstonejs/polymorphic-segmentation": "^4.19.2", "@cornerstonejs/tools": "^4.19.2", "@fingerprintjs/fingerprintjs": "^4.6.2", "@icr/polyseg-wasm": "^0.4.0", @@ -125,4 +125,4 @@ "not dead", "not op_mini all" ] -} \ No newline at end of file +} 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 860762d7..473284a3 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/ReadPage.vue @@ -1605,9 +1605,10 @@ export default { toolGroup.addTool(EllipticalROITool.toolName, { getTextLines: this.getEllipticalROIToolTextLines }) - toolGroup.addTool(FixedRadiusCircleROITool.toolName), { - getTextLines: this.getCircleROIToolTextLines - } + toolGroup.addTool(FixedRadiusCircleROITool.toolName, { + radius: Number.isFinite(this.taskInfo.CircleRadius) ? this.taskInfo.CircleRadius : 1, + getTextLines: this.getCircleROIToolTextLines + }) toolGroup.addTool(AngleTool.toolName, { getTextLines: this.getAngleToolTextLines }) diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/helpers/setNmFusionColorMapTransferFunctionForVolumeActor.js b/src/views/trials/trials-panel/reading/dicoms3D/components/helpers/setNmFusionColorMapTransferFunctionForVolumeActor.js index 757c3ee0..e7fb2773 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/helpers/setNmFusionColorMapTransferFunctionForVolumeActor.js +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/helpers/setNmFusionColorMapTransferFunctionForVolumeActor.js @@ -1,5 +1,6 @@ import vtkColorTransferFunction from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction"; import vtkPiecewiseFunction from "@kitware/vtk.js/Common/DataModel/PiecewiseFunction"; +import { Scale } from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction/Constants"; import { cache, metaData, utilities } from "@cornerstonejs/core"; const { getColormap } = utilities.colormap; @@ -14,7 +15,7 @@ function getWindowCenterFromVolumeId(volumeId) { ? voiLutModule.windowCenter[0] : voiLutModule?.windowCenter; const center = Number(rawCenter); - return Number.isFinite(center) ? center : null; + return center; } export default function setPetColorMapTransferFunctionForVolumeActor({ @@ -33,7 +34,25 @@ export default function setPetColorMapTransferFunctionForVolumeActor({ const center = getWindowCenterFromVolumeId(volumeId); const upper = center > 1 ? center : 5; - cfun.setMappingRange(1, upper); + const safeUpper = Number.isFinite(upper) && upper > 0 ? upper : 5; + const rangeMin = 0; + const rangeMax = safeUpper; + cfun.setScale(Scale.LOG10); + cfun.setMappingRange(rangeMin, rangeMax); volumeActor.getProperty().setRGBTransferFunction(0, cfun); -} + //低信号更明显,系数可以更小 + const thresholdValue0 = Math.max(rangeMin, rangeMax * 0.002); + const thresholdValue1 = Math.max(thresholdValue0, rangeMax * 0.02); + const delta = Math.abs(rangeMax - rangeMin) * 0.001; + const threshold0MinusDelta = Math.max(rangeMin, thresholdValue0 - delta); + + const ofun = vtkPiecewiseFunction.newInstance(); + ofun.addPoint(rangeMin, 0.0); + ofun.addPoint(threshold0MinusDelta, 0.0); + //低信号更明显,系数可以更小 + ofun.addPoint(thresholdValue0, 0.08); + ofun.addPoint(thresholdValue1, 0.9); + ofun.addPoint(rangeMax, 1.0); + volumeActor.getProperty().setScalarOpacity(0, ofun); +} diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/toolConfig.js b/src/views/trials/trials-panel/reading/dicoms3D/components/toolConfig.js index 9531a991..f93549c7 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/toolConfig.js +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/toolConfig.js @@ -311,6 +311,15 @@ const config = { 'isDisabled': false, 'disabledReason': '' }, + { + 'name': '定圆工具', + 'icon': 'oval', + 'toolName': 'FixedRadiusCircleROI', + 'props': ['radius', 'area', 'mean', 'max', 'stdDev'], + 'i18nKey': 'trials:reading:button:fixedCircle', + 'isDisabled': false, + 'disabledReason': '' + } ], 'customizeStandardsNoneDicom': [ { diff --git a/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FixedRadiusCircleROITool.js b/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FixedRadiusCircleROITool.js index da073e7f..2aacc73d 100644 --- a/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FixedRadiusCircleROITool.js +++ b/src/views/trials/trials-panel/reading/dicoms3D/components/tools/FixedRadiusCircleROITool.js @@ -28,10 +28,12 @@ class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool { preventHandleOutsideImage: false, storePointData: false, centerPointRadius: 0, + calculateStats: true, radius: 10, // Default radius in mm radiusUnit: 'mm', statsCalculator: BasicStatsCalculator, getTextLines: defaultGetTextLines, + simplified: true, }, } ) { @@ -67,9 +69,12 @@ class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool { ); const FrameOfReferenceUID = viewport.getFrameOfReferenceUID(); + const targetId = this.getTargetId(viewport); // Calculate end point based on fixed radius - const radius = this.configuration.radius || 10; + const radius = Number.isFinite(this.configuration.radius) + ? this.configuration.radius + : 10; // viewUp is a normalized vector. // We want a point 'radius' distance away from center. // We can use viewUp or any vector in the plane. @@ -106,7 +111,20 @@ class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool { points: [[...worldPos], [...endPos]], activeHandleIndex: null, }, - cachedStats: {}, + cachedStats: { + [targetId]: { + Modality: null, + radius: null, + radiusUnit: null, + area: null, + mean: null, + stdDev: null, + max: null, + isEmptyArea: null, + areaUnit: null, + modalityUnit: null, + }, + }, }, }; @@ -180,9 +198,11 @@ class FixedRadiusCircleROITool extends cornerstoneTools.CircleROITool { }; } -export default FixedRadiusCircleROITool; function defaultGetTextLines(data, targetId) { - const cachedVolumeStats = data.cachedStats[targetId]; + const cachedVolumeStats = data?.cachedStats?.[targetId]; + if (!cachedVolumeStats) { + return []; + } const { radius, radiusUnit, @@ -195,32 +215,33 @@ function defaultGetTextLines(data, targetId) { modalityUnit, } = cachedVolumeStats; const textLines = []; - - if (radius) { + if (csUtils.isNumber(radius)) { const radiusLine = isEmptyArea ? `Radius: Oblique not supported` : `Radius: ${csUtils.roundNumber(radius)} ${radiusUnit}`; textLines.push(radiusLine); } - if (area) { + if (csUtils.isNumber(area)) { const areaLine = isEmptyArea ? `Area: Oblique not supported` : `Area: ${csUtils.roundNumber(area)} ${areaUnit}`; textLines.push(areaLine); } - if (mean) { + if (csUtils.isNumber(mean)) { textLines.push(`Mean: ${csUtils.roundNumber(mean)} ${modalityUnit}`); } - if (max) { + if (csUtils.isNumber(max)) { textLines.push(`Max: ${csUtils.roundNumber(max)} ${modalityUnit}`); } - if (stdDev) { + if (csUtils.isNumber(stdDev)) { textLines.push(`Std Dev: ${csUtils.roundNumber(stdDev)} ${modalityUnit}`); } return textLines; -} \ No newline at end of file +} + +export default FixedRadiusCircleROITool; diff --git a/src/views/trials/trials-panel/setting/reading-unit/components/ReadingRules.vue b/src/views/trials/trials-panel/setting/reading-unit/components/ReadingRules.vue index 302b224e..bb5be704 100644 --- a/src/views/trials/trials-panel/setting/reading-unit/components/ReadingRules.vue +++ b/src/views/trials/trials-panel/setting/reading-unit/components/ReadingRules.vue @@ -34,13 +34,30 @@ + !hasPermi(['trials:trials-panel:setting:reading-unit:edit'])" + @change="handleReadingToolListChange" + > {{ $t(`${tool.i18nKey}`) }} + + + + + + @@ -510,7 +527,8 @@ export default { ImageUploadEnum: null, IsImageFilter: false, KeyFileListStr: '', - KeyFileList: [] + KeyFileList: [], + CircleRadius: null }, rules: { IsAutoCreate: [ @@ -678,6 +696,10 @@ export default { trigger: ['blur', 'change'], }, ], + CircleRadius: [ + { required: true, message: this.$t('common:ruleMessage:specify'), trigger: ['blur', 'change'], }, + { pattern: /^(0|[1-9]\d*)(\.\d{1,2})?$/, message: '请输入正确的数值格式,最多两位小数', trigger: 'blur' } + ] // IsReadingTaskViewInOrder: [ // { required: true, message: this.$t('common:ruleMessage:select'), trigger: ['blur', 'change'] } // ] @@ -1044,6 +1066,37 @@ export default { }) }) }, + handleReadingToolListChange(v) { + if(!v.includes('FixedRadiusCircleROI')) { + this.form.CircleRadius = null + } + }, + handleNumberInput(value, field) { + if (!value) { + this.form[field] = '' + return + } + + let val = value.toString() + + // 1. 清除“数字”和“.”以外的字符 + val = val.replace(/[^\d.]/g, '') + // 2. 保证第一个字符不能是“.” + val = val.replace(/^\./g, '') + // 3. 保证不能连续输入多个“.”,只保留第一个 + val = val.replace(/\.{2,}/g, '.') + val = val.replace('.', '$#$').replace(/\./g, '').replace('$#$', '.') + // 4. 限制只能输入两位小数 + val = val.replace(/^(\-)*(\d+)\.(\d{2}).*$/, '$1$2.$3') + // 5. 处理前导零(例如输入 01 自动变成 1) + if (val.length > 1 && val[0] === '0' && val[1] !== '.') { + val = val.replace(/^0+/, '') + if (val === '') val = '0' + } + this.$nextTick(() => { + this.ruleForm[field] = val + }); + }, }, } diff --git a/src/views/trials/trials-panel/trial-summary/data-sync/components/FileList.vue b/src/views/trials/trials-panel/trial-summary/data-sync/components/FileList.vue index 8543fb31..e4ffbf13 100644 --- a/src/views/trials/trials-panel/trial-summary/data-sync/components/FileList.vue +++ b/src/views/trials/trials-panel/trial-summary/data-sync/components/FileList.vue @@ -21,6 +21,17 @@ + + + + + @@ -32,6 +43,21 @@ + + + + + + + + + @@ -201,6 +227,11 @@ const searchDataDefault = () => { UploadRegion: null, TargetRegion: null, IsSync: null, + UploadStartTime: null, + UploadEndTime: null, + SyncFinishedStartTime: null, + SyncFinishedEndTime: null, + Priority: null, Asc: false, SortField: 'UpdateTime', PageIndex: 1, @@ -227,7 +258,8 @@ export default { list: [], total: 0, loading: false, - datetimerange: [], + uploadTimeRange: null, + SyncTimeRange: null, regionOptions: [ { value: 'CN', @@ -363,13 +395,22 @@ export default { if (!size) return return (size / Math.pow(1024, 2)).toFixed(3) + 'MB' }, - handleDatetimeChange(val) { + handleUploadtimeChange(val) { if (val) { - this.searchData.BeginDate = val[0] - this.searchData.EndDate = val[1] + this.searchData.UploadStartTime = val[0] + this.searchData.UploadEndTime = val[1] } else { - this.searchData.BeginDate = '' - this.searchData.EndDate = '' + this.searchData.UploadStartTime = '' + this.searchData.UploadEndTime = '' + } + }, + handleSynctimeChange(val) { + if (val) { + this.searchData.SyncFinishedStartTime = val[0] + this.searchData.SyncFinishedEndTime = val[1] + } else { + this.searchData.SyncFinishedStartTime = '' + this.searchData.SyncFinishedEndTime = '' } }, handleSearch() { @@ -378,7 +419,10 @@ export default { }, // 重置列表查询 handleReset() { - this.datetimerange = null + this.uploadTimeRange = null + this.handleUploadtimeChange() + this.SyncTimeRange = null + this.handleSynctimeChange() this.searchData = searchDataDefault() this.getList() }, diff --git a/src/views/trials/trials-panel/trial-summary/data-sync/components/StudyList.vue b/src/views/trials/trials-panel/trial-summary/data-sync/components/StudyList.vue index 45635c5e..b8277cf1 100644 --- a/src/views/trials/trials-panel/trial-summary/data-sync/components/StudyList.vue +++ b/src/views/trials/trials-panel/trial-summary/data-sync/components/StudyList.vue @@ -31,6 +31,17 @@ + + @@ -42,6 +53,17 @@ + + @@ -146,6 +168,10 @@ const searchDataDefault = () => { UploadRegion: '', TargetRegion: '', IsSync: null, + // UploadStartTime: null, + // UploadEndTime: null, + // SyncFinishedStartTime: null, + // SyncFinishedEndTime: null, PageIndex: 1, PageSize: 20, Asc: true, @@ -179,7 +205,9 @@ export default { } ], fileUploadRecordId: '', - path: '' + path: '', + uploadTimeRange: null, + SyncTimeRange: null, } }, mounted() { @@ -231,13 +259,34 @@ export default { console.log(e) } }, + handleUploadtimeChange(val) { + if (val) { + this.searchData.UploadStartTime = val[0] + this.searchData.UploadEndTime = val[1] + } else { + this.searchData.UploadStartTime = '' + this.searchData.UploadEndTime = '' + } + }, + handleSynctimeChange(val) { + if (val) { + this.searchData.SyncFinishedStartTime = val[0] + this.searchData.SyncFinishedEndTime = val[1] + } else { + this.searchData.SyncFinishedStartTime = '' + this.searchData.SyncFinishedEndTime = '' + } + }, handleSearch() { this.searchData.PageIndex = 1 this.getList() }, // 重置列表查询 handleReset() { - this.datetimerange = null + // this.uploadTimeRange = null + // this.handleUploadtimeChange() + // this.SyncTimeRange = null + // this.handleSynctimeChange() this.searchData = searchDataDefault() this.getList() }, diff --git a/src/views/trials/trials-panel/trial-summary/data-sync/components/TaskList.vue b/src/views/trials/trials-panel/trial-summary/data-sync/components/TaskList.vue index 7c3d079a..d4837f0a 100644 --- a/src/views/trials/trials-panel/trial-summary/data-sync/components/TaskList.vue +++ b/src/views/trials/trials-panel/trial-summary/data-sync/components/TaskList.vue @@ -21,6 +21,28 @@ /> + + + + + + + + {{ $t('common:button:search') }} @@ -91,6 +113,8 @@ const searchDataDefault = () => { JobState: null, FileName: '', Path: '', + StartTime: '', + EndTime: '', Asc: false, SortField: '', PageIndex: 1, @@ -122,6 +146,24 @@ export default { total: 0, loading: false, datetimerange: [], + beginPickerOption: { + disabledDate: time => { + if (this.searchData.EndTime) { + return time.getTime() >= new Date(this.searchData.EndTime).getTime() + } else { + return time.getTime() > Date.now() + } + } + }, + endpickerOption: { + disabledDate: time => { + if (this.searchData.StartTime) { + return time.getTime() > Date.now() || time.getTime() <= new Date(this.searchData.StartTime).getTime() - 86400000 + } else { + return time.getTime() > Date.now() + } + } + } } }, mounted() {