
OHIF Viewer 报告保存对话框深度解析目标序列选择、序列命名与版本化机制【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers本文以 OHIF Viewer 行为文档 report-dialog-save-destinations.md 为主线结合extensions/default与extensions/cornerstone-dicom-seg的实际源码与测试完整剖析保存对话框Report Dialog的工作原理。你将掌握Save to current/Save as new/Replace existing三种保存目标的判定逻辑、predecessorImageId与PredecessorSequence的底层机制、新序列的四种命名候选及其记忆历史、新序列编号的计算规则以及如何通过ohif.createReportDialog自定义替换整个对话框。一、概述保存对话框解决什么问题当用户需要把一次编辑的成果写回 DICOM 存储时OHIF Viewer 会弹出一个报告保存对话框Report Dialog。它可以存储三类对象分割SegmentationSEG轮廓集Contour Set / RTSTRUCT测量报告Measurement Report / SR对话框向用户提出两个核心问题对象写入哪个序列Series以及这个序列叫什么名字。两者看似简单背后却牵涉到版本化语义、前驱引用链与记忆历史三个设计维度。该功能横跨多个模块实现位置如下对话框本体与ohif.createReportDialog定制点reportDialogCustomization.tsx对话框的 Prompt 封装输入输出契约createReportDialogPrompt.tsx序列描述记忆历史seriesDescriptionHistory.tsSEG / RTSTRUCT 的保存调用方storeSegmentationcommandsModule.ts测量报告的保存调用方promptSaveReport.tsx前驱实例读取PredecessorSequenceprovider位于文档所引用的 adapters 包libs/cornerstonejs/packages/adapters/src/utilities/referencedMetadataProvider.ts此外配套的迁移指南 report-dialog.md 记录了 3.13 → 3.14 版本中该对话框从单一下拉框演进为三种明确目标的完整过程可作为行为文档的延伸阅读。二、核心语义一次保存只存一个对象在深入三个目标之前必须先理解保存对话框的基础语义每次保存都把当前的全部数据作为一个对象写入目标序列对话框不做任何合并。这意味着保存进一个已有数据的序列时不会先读取该序列的旧数据——旧对象不会与新对象融合当前数据也不会被遗漏任何一部分——保存的是服务中选定的全量数据测量服务中的全部测量、分割服务中的全部分段目标序列决定了新对象取代哪一个旧实例——predecessorImageId指向的实例被继承supersede旧实例仍然保留在序列中但新实例成为该序列被默认加载的对象。从源码看reportDialogCustomization.tsx 中的Destination类型注释明确写道三种目标存储的是同一个对象对话框不合并任何内容All three store the same object, and the dialog merges nothing。三、三种保存目标Save to current / Save as new / Replace existing3.1 三种目标一览目标对象去向对话框何时提供该目标Save to current视口加载数据来源的那个序列数据的predecessorImageId指向一个已加载的序列Save as new新建一个序列编号与描述均可编辑始终可用Replace existing用户选择的、同模态modality的其他已加载序列视口还持有至少一个此类序列默认选择规则当Save to current可用时它是默认选择否则默认选择Save as newSave to current与Replace existing都保留目标序列原有的编号与描述因此对话框对这两项只读展示、不允许用户编辑。在 UI 上这三个目标以分段控件Segmented Control的形式呈现在Series行上控件下方有一行帮助文字保存按钮上也重复了所选目标的措辞。源码中这三个目标的文案定义在 reportDialogCustomization.tsx 的DESTINATIONS数组中Save to current帮助文字为Adds a new version to this series as the new default. Earlier versions are kept.向该序列添加一个新版本作为新的默认对象旧版本保留不可用时提示This data has not been saved to a series yet这些数据尚未保存到任何序列。Save as new帮助文字为Creates a separate series.创建独立序列。Replace existing帮助文字为Choose a series to replace. The current data becomes the default and earlier versions are kept.选择一个要替换的序列当前数据成为默认旧版本保留不可用时提示No other series of this type is loaded没有加载其他同类序列。3.2 三个目标的源码实现reportDialogCustomization.tsx 中existingSeries从displaySetService.getDisplaySetCache()中筛选出同模态的显示集映射为ExistingSeries结构value即predecessorImageId作为写入目标seriesNumber用于计算下一个可用编号并过滤掉没有predecessorImageId的条目与重复值currentSeriesexistingSeries中value predecessorImageId的那一个即Save to current的写入目标数据从未被存储过时它为nullreplaceableSeriesexistingSeries中排除currentSeries的其余部分即Replace existing的可选项。对话框打开时useStateDestination(currentSeries ? current : new)决定默认选中的标签页。只有当Replace existing目标被选中但尚未选择具体序列时保存与下载按钮才会被禁用isIncomplete判断。四、哪些序列会被对话框提供为目标4.1 两个硬性条件对话框把某个已加载序列作为目标必须同时满足两个条件序列持有同类型的对象——即与本次保存的模态modality一致保存 SEG 时只考虑 SEG 序列序列具有predecessorImageId值。predecessorImageId命名的是上次有人保存进该序列的那个直接前驱实例immediate prior object。本次保存将取代supersede这一个实例。adapter 通过PredecessorSequenceprovider借助这个值解析出对应的序列与实例编号。4.2 为什么不回退到 SeriesInstanceUID对话框不会回退到显示集的SeriesInstanceUID值。原因在于类型语义UID 命名的是序列而不是实例因此它不指向任何前驱对象UID 也不是 image idPredecessorSequenceprovider 无法为 UID 找到任何实例结果就是若以 UID 作为前驱provider 找不到实例保存会新建一个序列而不是扩展所选的序列。迁移文档 report-dialog.md 补充说明旧版本的行为正是把SeriesInstanceUID当作序列值塞给 provider导致 provider 抛出TypeError——这正是行为文档中provider 对 UID 找不到实例这一规则的由来。4.3 本地 id 是合法值一个本地 idlocal id例如dicomfile:3是完全合法的predecessorImageId。机制如下用户上传的实例uploaded instance在注册时被赋予一个本地 idprovider 能够解析本地 id因此用户可以多次针对同一个上传实例执行保存例如先存Save as new之后再次保存时通过本地 id 定位到它。4.4 下载但未存储的序列一个被下载download却从未写回存储的显示集没有predecessorImageId值因此对话框不会提供该显示集作为保存目标。但要注意新建序列的编号计算仍然会计入这类序列。因为编号来自该模态的全部已加载序列而不仅仅是被提供的那些。源码见 reportDialogCustomization.tsxdefaultNewSeriesNumber遍历modalityDisplaySets同模态全部显示集取最大SeriesNumber再加一而非基于existingSeries或replaceableSeries。五、新序列叫什么四种命名候选5.1 四名候选的优先级Save as new提供四种候选名称描述字段从第一个非空名称开始填充优先级名称来源说明1itemName用户为该项目选择的名称。保存前的重命名会反映到该字段2加载来源序列的描述即该数据上一次保存时使用的序列名3该类型项目以前用过的描述最近使用的最靠前详见记住的描述4defaultSeriesDescription没有任何其他名称时的兜底名称仓库内三个内置调用方传入的defaultSeriesDescription分别为SegmentationSEG、ContoursRTSTRUCT与Measurements测量报告。5.2 itemName 与 labelIsGenerated 的微妙关系itemName必须是用户选择过的名称。以storeSegmentation为例commandsModule.ts 中const { label, predecessorImageId, labelIsGenerated } segmentation; // 只有用户选择的名称才进入 itemName对话框将其放在第一位 // 生成的名称则进入 defaultSeriesDescription放在最后。 const chosenLabel labelIsGenerated ? : label || ; const defaultSeriesDescription (labelIsGenerated label) || (modality RTSTRUCT ? Contours : Segmentation);这里的关键在于labelIsGenerated标记分割服务Segmentation Service为新分割自动生成一个名称例如Segmentation 3并给分割打上labelIsGenerated标记一个仍带着该标记的标签会被归入defaultSeriesDescription而非itemName——因为生成的名称不能压过outrank用户记住的描述重命名会清除该标记。此后即使用户手动输入一个与自动生成名一模一样的字符串例如真的输入了Segmentation 3保存时也不会做字符串比对该名称仍会以itemName的身份进入候选第一位。迁移文档还补充了labelIsGenerated的完整生命周期Segmentation.labelIsGenerated是cornerstonejs/tools中的新字段由SegmentationPublicInput.config携带创建者发明标签时传入labelIsGenerated: true不带标签的创建者同样会得到该标记而携带label但未带labelIsGenerated的更新操作会清除标记即重命名。5.3 下拉列表的去重与回退规则四种名称按上述顺序放入下拉列表pull-down并遵循以下规则空白名称与纯空格名称会被剔除防止其藏住后面的名称对话框忽略大小写比较名称列表中每个名称只出现一次字段被清空时回退到列表的第一个名称。源码实现见 reportDialogCustomization.tsxdescriptionOptions先对[itemName, currentSeries?.description, ...remembered, defaultSeriesDescription]逐一trim()并过滤空值再用toLowerCase()去重。5.4 没有 itemName 的调用方没有可编辑名称的调用方不传itemName。例如测量报告measurement report没有此类名称因此其描述字段直接从加载来源序列的描述或最近使用的名称开始。5.5 键盘交互用户可以通过三种方式使用该列表源码见 reportDialogCustomization.tsx点击下拉按钮展示全部候选名称键入文字列表被过滤为当前输入可以补全complete的名称子集Tab补全为过滤后列表的第一个名称若与当前输入不同则阻止默认 Tab 行为方向键 Enter在列表中上下移动并选中一项Escape关闭列表。六、记住的描述Remembered Descriptions6.1 什么时候被记住用户创建新序列并使用该描述时此描述被记住下载download也算一次保存同样触发记忆保存进一个已存在的序列时不记住任何东西——因为该序列保留自己的描述使用defaultSeriesDescription的保存不进入历史调用方每次保存都会提供该名称对话框无论如何都会提供它记住它只会污染历史。由此产生一个重要推论自动生成的分割标签如Segmentation 1落在defaultSeriesDescription上因此不会进入历史对话框也就不会把Segmentation 1当作后来某个无关分割的名称来推荐。6.2 两个配置项配置项含义默认值itemType描述被记住时使用的键key模态modality即SEG/RTSTRUCT/SR各自独立rememberedDescriptionCount每种类型记住多少条描述5特别地rememberedDescriptionCount: 0会完全关闭该功能不记忆、不提供列表只剩一个名称对话框也不渲染下拉按钮退化为普通文本输入框。对于不允许向 localStorage 持久化任何内容的部署应显式传入rememberedDescriptionCount: 0。6.3 存储实现历史数据存放在浏览器localStorage中键名为ohif.seriesDescriptionHistory结构为Recordstring, string[]按itemType分组、最近使用在前。核心实现在 seriesDescriptionHistory.tsgetSeriesDescriptionHistory(itemType, maxCount)读取并返回该类型最近使用的描述最多maxCount条第 39-52 行rememberSeriesDescription(itemType, description, maxCount)把描述记为最近使用移除更早的同名条目大小写不敏感isSameDescription用toLowerCase()比较并截断到maxCount条第 58-82 行readHistory()由于 localStorage 在某些浏览器配置下不可用、内容也可能被污染所有读取都是防御性的失败仅意味着没有历史第 20-29 行。6.4 测试用例佐证reportDialogCustomization.test.ts 中对记忆历史的断言非常直观例如remembers the description that was used, per type of item输入Right kidney保存后storedHistory()等于{ SEG: [Right kidney] }remembers nothing for the name that the caller provides直接用生成的Segmentation 1保存后storedHistory()等于{}remembers nothing when the typed name is the provided one即使输入带空格变体的contours也不会被记住remembers nothing when an existing series is stored into存入已有序列不触发记忆。七、新序列的序列号Series Number7.1 计算规则Save as new提供的序列号 该模态全部已加载序列的最高序列号 1且至少为minSeriesNumber。用户可以直接编辑该数字若数字为空或非法则回退到对话框最初提供的数字。源码见 reportDialogCustomization.tsx 与parsedSeriesNumber的解析逻辑第 279-282 行。7.2 minSeriesNumber 的模态默认值minSeriesNumber由 createReportDialogPrompt.tsx 按模态计算仅当调用方未显式传入时生效minSeriesNumber || (modality SR 3000) || (modality SEG 3100) || (modality RTSTRUCT 3200) || 4000;即SR 为 3000、SEG 为 3100、RTSTRUCT 为 3200其他模态一律 4000。同时在 reportDialogCustomization.tsx 中对话框组件自身的minSeriesNumber默认值为3000。7.3 返回值seriesNumber 与 priorSeriesNumbercreateReportDialogPrompt返回seriesNumber同时返回priorSeriesNumber比seriesNumber小 1。这是为了兼容旧调用方凡是按1 priorSeriesNumber计算编号的调用方都能得到对话框当前显示可能被用户编辑过的数字。迁移文档给出了明确的版本差异3.13旧调用方自己算SeriesNumber: 1 priorSeriesNumber无法感知用户的编辑3.14新直接使用SeriesNumber: seriesNumber对话框已返回最终值priorSeriesNumber从该模态最高现有编号变为seriesNumber - 1旧式1 priorSeriesNumber依然可用。7.4 存储端的优先级一个容易被忽略的细节当设置了predecessorImageId时SeriesDescription和SeriesNumber会被忽略因为生成的实例会应用前驱实例的序列数据——这正是对话框在扩展已有序列时把编号与描述显示为只读的原因。这在 commandsModule.ts 的storeSegmentation中有直接体现const args { segmentationId, options: { dataSource: dataSourceName, SeriesDescription: series ? undefined : reportName || label || defaultSeriesDescription, SeriesNumber: series ? undefined : seriesNumber, predecessorImageId: series, }, };当series即predecessorImageId存在时SeriesDescription与SeriesNumber均为undefined。八、保存之后被存储的对象成为新前驱要让第二次保存默认 Save to current成立保存动作本身必须把本次写入的实例登记为新的前驱。核心机制是registerStoredInstanceImageId从 viewer 存储的实例会像从数据源加载的实例一样被识别——它获得一个重新加载该实例时会用到的 imageId并在加入元数据存储前把这个 imageId 映射到它的 UIDs。因此由存储实例生成的显示集自带predecessorImageId。两类对象的登记路径不同分割与轮廓SEG / RTSTRUCT保存会移除内存中的分割并展示存储下来的那个因此重新加载的分割通过正常加载路径从显示集取得前驱测量Measurements测量不会从存储的报告重新加载因此新增的recordMeasurementsPredecessor命令在CORNERSTONE命令模块中直接把前驱记录在测量及其派生的标注上确保之后编辑测量不会丢失该引用。在 promptSaveReport.tsx 中可以看到测量路径的完整闭环storeMeasurements执行后从新显示集取predecessorImageId再调用recordMeasurementsPredecessor写回测量数据。实际效果同一份数据第二次保存时默认选中Save to current并指向第一次保存所创建的序列。九、如何自定义或替换对话框ohif.createReportDialog是正式的定制点customization。任何自定义组件只要注册到该键名下即可整体替换内置对话框。替换时需遵循的输入输出契约如下9.1 输入 props内置ReportDialog组件接收见 reportDialogCustomization.tsxdataSources可写入的数据源列表多数据源导出开启时显示选择器modality本次保存的模态用于筛选可写序列predecessorImageId数据加载来源的 image id决定Save to current是否可用minSeriesNumber新序列编号的下限itemName用户为该对象选择的名称如分割名新序列首选它生成的名称应放入defaultSeriesDescriptiondefaultSeriesDescription无其他名称时的兜底名itemType记忆历史的键默认取模态rememberedDescriptionCount记忆条数0 关闭enableDownload是否显示 Download 按钮。9.2 输出 onSaveonSave负载包含reportName序列描述、dataSource、series以predecessorImageId值引用目标序列新建序列时为null、seriesNumber对话框显示并可能被编辑过的最终编号以及priorSeriesNumber。3.14 起新增seriesNumber字段一个只创建新序列的自定义对话框可以直接传series: null与自定义的seriesNumber。9.3 复用记忆历史描述记忆是对话框自身的职责。自定义对话框若想复用该行为应直接调用 seriesDescriptionHistory.ts 导出的getSeriesDescriptionHistory与rememberSeriesDescription不想使用的对话框忽略itemType与rememberedDescriptionCount即可。9.4 配套的样式与行为细节对话框标题为Save Segmentation、Save Contours、Save Measurements3.14 前为 Store/Create Report 措辞调用方可通过title或模式的defaultSaveTitle覆盖底部操作区为FooterAction左侧Download、右侧Cancel与主保存按钮对话框打开时默认焦点会落在系列目标分段控件上但当目标是Save as new时代码通过useEffect把光标移到描述输入框便于用户直接键入命名第 436-442 行。十、测试验证与行为契约reportDialogCustomization.test.ts 用 701 行测试完整锁定了本文描述的全部行为是理解行为契约的最佳入口。测试通过模拟多个显示集验证目标可用性无前驱时current禁用并提示无其他同模态序列时replace禁用默认选中标签页随数据是否被保存过而切换第 186-215 行Save to current展示目标序列只读的编号与描述onSave携带该序列的predecessorImageId与序列号第 217-235 行Save as new编号为最高现有编号 1没有同模态序列时用minSeriesNumber不提供的序列无前驱的下载序列仍然计入编号计算空字段回退到提供的值第 237-301 行Replace existing选择前禁用保存按钮不重复提供当前序列无描述的序列以Series 3103形式命名带本地 iddicomfile:3的上传序列可被选中并可重复保存第 303-384 行。十一、相关阅读行为文档本体platform/docs/docs/behaviours/report-dialog-save-destinations.md3.13 → 3.14 迁移指南含输入输出契约与版本差异详解platform/docs/docs/migration-guide/3p13-to-3p14/report-dialog.md测量保存的调用方与recordMeasurementsPredecessor流程extensions/default/src/utils/promptSaveReport.tsx分割保存的调用方extensions/cornerstone-dicom-seg/src/commandsModule.ts掌握这套目标判定 命名候选 前驱链的机制后无论是排查保存行为、扩展新的保存对象类型还是整体替换保存对话框都能准确对齐 OHIF Viewer 既有的数据版本化语义避免在自定义过程中破坏旧版本保留、新版本成为默认的核心契约。【免费下载链接】ViewersOHIF zero-footprint DICOM viewer and oncology specific Lesion Tracker, plus shared extension packages项目地址: https://gitcode.com/GitHub_Trending/vi/Viewers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考