Procházet zdrojové kódy

更新接口描述文档

dengdx před 1 měsícem
rodič
revize
f8f2b2d11e

+ 67 - 0
bug-analysis-dcm-cell-cross-film-issue.md

@@ -0,0 +1,67 @@
+# DcmCell 胶片间图像显示错误 Bug 分析
+
+## 问题描述
+用户在胶片1上拖拽一个图像,切换到胶片2时,这个图像也会显示在胶片2上。
+
+## 根本原因分析
+
+### 1. 共享渲染引擎问题
+```typescript
+// 所有 DcmCell 实例共享同一个渲染引擎
+const engineId = 'myRenderingEngineForPrint';
+let engine: RenderingEngine | null = cornerstone.getRenderingEngines()?.find(en => en.id === engineId) || null;
+```
+
+### 2. viewport ID 生成不唯一
+```typescript
+// 当前的 viewport ID 生成方式
+const viewportId = useMemo(() => `stackViewport-${imageId}-${uuidv4()}`, []);
+```
+
+**问题**:
+- 在组件初始化时,`imageId` 可能为 `null` 或相同值
+- 这导致不同胶片的不同格子可能生成相同的 viewport ID 前缀
+- `uuidv4()` 只在组件挂载时生成一次,但由于依赖数组为空 `[]`,无法保证真正的唯一性
+
+### 3. 渲染引擎状态污染
+- 共享的渲染引擎在不同胶片间切换时保持了之前的状态
+- 当新的 DcmCell 组件挂载时,可能会复用已有的 viewport 或产生冲突
+
+## 解决方案
+
+### 1. 改进 viewport ID 生成策略
+```typescript
+// 使用胶片ID + 格子索引 + UUID 确保唯一性
+const viewportId = useMemo(() => 
+  `stackViewport-${currentFilm.id}-${indexOfCell}-${uuidv4()}`, 
+  [currentFilm.id, indexOfCell]
+);
+```
+
+### 2. 分离渲染引擎或改进清理机制
+```typescript
+// 方案A: 每个胶片使用独立的渲染引擎
+const engineId = `renderingEngine-${currentFilm.id}`;
+
+// 方案B: 改进现有引擎的 viewport 清理机制
+// 在组件卸载时确保完全清理 viewport
+```
+
+### 3. 加强状态隔离
+- 确保每个 DcmCell 的状态完全独立
+- 在 imageId 变化时正确清理之前的图像状态
+- 避免在不同组件间共享 viewport 实例
+
+### 4. 优化 useEffect 依赖
+```typescript
+// 移除 currentImageId 从依赖数组,避免无限循环
+useEffect(() => {
+  // ...
+}, [imageId, stackViewport]);
+```
+
+## 修复步骤
+1. 修改 viewport ID 生成逻辑,确保唯一性
+2. 改进组件卸载时的清理机制
+3. 优化 useEffect 依赖数组
+4. 测试胶片间切换的图像隔离性

+ 343 - 71
docs/DR.md

@@ -2948,17 +2948,19 @@ zh_CN
 {
 	"code": "0x000000",
 	"data": {
-		"contrast": 2.1,
-		"detail": 2.2,
-		"latitude": 2.3,
-		"noise": 2.4
+		"contrast": 8.0,
+		"detail": 8.0,
+		"latitude": 8.0,
+		"noise": 8.0,
+		"ww_coef": 8.0,
+		"wl_coef": 8.0
 	},
-	"description": "Success",
+	"description": "成功",
 	"solution": ""
 }
 ```
-contrast:增益detail: 细节latitude: 动态范围noise: 噪声模式
-![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/22/2MKX8qDZmEa/b6ea7f763c554e0f92529c72a69952b2)
+contrast:增益detail: 细节latitude: 动态范围noise: 噪声模式ww_coef: 对比度wl_coef: 亮度
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/25/2MPERPLWn9U/f2b624ab68664385bc54aeae217b75f9)
 
 ## 18.2	获取处理后dcm
 
@@ -2983,12 +2985,44 @@ contrast:增益detail: 细节latitude: 动态范围noise: 噪声模式
 |detail||细节 范围0.0~9.0 步长0.1|
 |latitude||动态范围 范围0.0~9.0 步长0.1|
 |noise||噪声模式 范围0.0~9.0 步长0.1|
+|ww_coef||对比度 范围-9.0~9.0|
+|wl_coef||亮度 范围-9.0~9.0|
+### 响应体
+● 200: OK 响应数据格式:File
+直接返回dcm文件报错时:返500
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/25/2MPEVR3QTaK/332e311b96db41ea9ca686e19f01fe33)
+
+## 18.3	获取处理后dcm(pub)
+
+> GET  /dr/api/v1/pub/image/{id}/proc
+### 接口说明
+> 获取应用增益、细节、动态范围、噪声模式后的dcm
+### 地址参数(Path Variable)
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|id||sop_instance_uid|
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYxOTU0MDIsImlkIjozLCJuYW1lIjoiYWRtaW4ifQ.jK86cKyAMwfgxPsE6mpvSE1PF99jHrppGJZdB8G9uEw||
+|Language||en_US 或 zh_CN|
+|Product|DROS|DROS 或 VETDROS|
+|Source|Electron|Electron 或 Browser 或 Android|
+### 请求参数(Query Param)
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|contrast||增益 范围0.0~9.0 步长0.1|
+|detail||细节 范围0.0~9.0 步长0.1|
+|latitude||动态范围 范围0.0~9.0 步长0.1|
+|noise||噪声模式 范围0.0~9.0 步长0.1|
+|ww_coef||对比度 范围-9.0~9.0|
+|wl_coef||亮度 范围-9.0~9.0|
 ### 响应体
 ● 200: OK 响应数据格式:File
 直接返回dcm文件报错时:返500
-![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/22/2MKX7EtxRZ2/d6c692ba28044670955f7579185ff16f)
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/25/2MPEVR3QTaK/332e311b96db41ea9ca686e19f01fe33)
 
-## 18.3	保存图像处理参数
+## 18.4	保存图像处理参数
 
 > POST  /dr/api/v1/auth/image/{id}/proc
 ### 接口说明
@@ -3008,9 +3042,11 @@ contrast:增益detail: 细节latitude: 动态范围noise: 噪声模式
 | 参数名称 | 数据类型 | 默认值 | 不为空 | 描述 |
 | ------ | ------ | ------ | ------ | ------ |
 | contrast|number||true|增益 范围0.0~9.0 步长0.1|
-| detail|string||true|细节 范围0.0~9.0 步长0.1|
-| latitude|string||true|动态范围 范围0.0~9.0 步长0.1|
-| noise|string||true|噪声模式 范围0.0~9.0 步长0.1|
+| detail|number||true|细节 范围0.0~9.0 步长0.1|
+| latitude|number||true|动态范围 范围0.0~9.0 步长0.1|
+| noise|number||true|噪声模式 范围0.0~9.0 步长0.1|
+| ww_coef|number||true|对比度 范围-9.0~9.0|
+| wl_coef|number||true|亮度 范围-9.0~9.0|
 ### 响应体
 ● 200: OK 响应数据格式:JSON
 ```json
@@ -3021,9 +3057,9 @@ contrast:增益detail: 细节latitude: 动态范围noise: 噪声模式
 	"solution": ""
 }
 ```
-![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/22/2MKWwC47kkS/6f786bee6b8c4489b37c04c9793e8843)
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/25/2MPEP8N8Jwu/aa1644b8b0884e37882de48b9500f539)
 
-## 18.4	保存窗宽窗位到dcm
+## 18.5	保存窗宽窗位到dcm
 
 > POST  /dr/api/v1/auth/image/{id}/wcww
 ### 接口说明
@@ -3484,11 +3520,11 @@ lock可选项
 Locked
 Unlocked
 
-## 22	获取检查信息列表
+## 22	获取检查信息列表(含RIS条目如果有)
 
 > GET  /dr/api/v1/auth/study/
 ### 接口说明
-> 获取检查信息列表
+> 获取检查信息列表(包含RIS条目如果有),study_id为空字符串的为RIS条目需保存到本地方可使用,study_id不为空字符串时为本地条目
 ### 请求头
 | 参数名称 | 默认值 | 描述 |
 | ------ | ------ | ------ |
@@ -3527,48 +3563,59 @@ Completed|
 		"count": 2,
 		"studies": [
 			{
-				"study_instance_uid": "2.25.156.999999.0000.1.2.8323328.20101.1760359944.254055",
-				"study_id": "251013205224825",
+				"study_instance_uid": "1.3.46.670589.16.12.2.1.176.53460.62132.20070126.102459.1",
+				"study_id": "",
 				"public_study_id": "",
 				"specific_character_set": "ISO_IR 192",
-				"accession_number": "accessnumber444",
+				"accession_number": "00000187",
 				"ref_physician": "",
-				"patient_id": "1113333",
-				"patient_name": "124",
+				"patient_id": "pidP645",
+				"patient_name": "One^Secondary Capture Image",
 				"patient_english_name": "",
 				"patient_former_name": "",
-				"patient_size": "Medium",
+				"patient_size": "",
 				"other_patient_ids": "",
 				"other_patient_names": "",
-				"patient_age": "000Y",
-				"patient_dob": "2025-10-13T12:52:15.210Z",
-				"patient_sex": "",
+				"patient_age": "",
+				"patient_dob": "1980-07-16T00:00:00Z",
+				"patient_sex": "M",
 				"sex_neutered": "",
 				"pregnancy_status": "",
 				"patient_state": "",
 				"admitting_time": null,
 				"priority": "",
-				"reg_source": "",
-				"study_description": "",
-				"study_start_datetime": "2025-10-13T12:52:24.255622Z",
+				"reg_source": "Ris",
+				"study_description": "CSPINE",
+				"study_start_datetime": null,
 				"study_end_datetime": null,
-				"scheduled_procedure_step_start_date": null,
+				"entry_id": "6ca82204811d435bb037807ebc1d53ad",
+				"mwl": "",
+				"scheduled": {
+					"scheduled_ae_title": "FILMDIGITIZE",
+					"scheduled_performing_physician_name": "",
+					"scheduled_procedure_step_id": "0000018706"
+				},
+				"protocol_code": [
+					{
+						"code_value": "CSPINE L BOTH",
+						"code_meaning": "CSPINE"
+					}
+				],
 				"performed_physician": "",
-				"study_lock": "Unlocked",
+				"study_lock": "",
 				"folder_path": "",
 				"operator_name": "",
-				"modality": "DX",
+				"modality": "",
 				"weight": 0,
 				"thickness": 0,
 				"length": 0,
-				"patient_type": "Dog",
+				"patient_type": "E1",
 				"study_type": "Normal",
-				"owner_name": "主人孙某",
+				"owner_name": "",
 				"chip_number": "",
 				"variety": "",
 				"is_anaesthesia": false,
 				"is_sedation": false,
-				"mwl": "",
 				"is_exported": false,
 				"is_edited": false,
 				"is_appended": false,
@@ -3576,57 +3623,63 @@ Completed|
 				"mapped_status": false,
 				"qc_result": false,
 				"comment": "",
-				"study_status": "InProgress",
-				"portrait_status": "NotSaved",
-				"portrait_file": ".",
+				"study_status": "",
+				"portrait_status": "",
+				"portrait_file": "",
 				"sort": 0,
-				"product": "VETDROS",
-				"create_time": "2025-10-13T12:52:24.313859Z",
+				"product": "",
+				"create_time": null,
 				"series": []
 			},
 			{
-				"study_instance_uid": "2.25.156.999999.0000.1.2.8323328.4730.1760342536.600785",
-				"study_id": "251013160216342",
+				"study_instance_uid": "100.118.116.2005.2.1.1132055956.125.3",
+				"study_id": "251028191159945",
 				"public_study_id": "",
 				"specific_character_set": "ISO_IR 192",
-				"accession_number": "accessnumber444",
+				"accession_number": "",
 				"ref_physician": "",
-				"patient_id": "1113333",
-				"patient_name": "124",
+				"patient_id": "SC-I3",
+				"patient_name": "Three^Secondary Capture Image",
 				"patient_english_name": "",
 				"patient_former_name": "",
-				"patient_size": "Medium",
+				"patient_size": "",
 				"other_patient_ids": "",
 				"other_patient_names": "",
-				"patient_age": "000Y",
-				"patient_dob": "2025-10-13T08:02:11.982Z",
-				"patient_sex": "",
+				"patient_age": "",
+				"patient_dob": "1970-01-01T00:00:00Z",
+				"patient_sex": "O",
 				"sex_neutered": "",
 				"pregnancy_status": "",
 				"patient_state": "",
-				"admitting_time": null,
+				"admitting_time": "1970-01-01T00:00:00Z",
 				"priority": "",
-				"reg_source": "",
+				"reg_source": "Ris",
 				"study_description": "",
-				"study_start_datetime": "2025-10-13T08:02:16.602164Z",
+				"study_start_datetime": "1970-01-01T00:00:00Z",
 				"study_end_datetime": null,
-				"scheduled_procedure_step_start_date": null,
+				"entry_id": "",
+				"mwl": "",
+				"scheduled": {
+					"scheduled_ae_title": "",
+					"scheduled_performing_physician_name": "",
+					"scheduled_procedure_step_id": ""
+				},
+				"protocol_code": [],
 				"performed_physician": "",
 				"study_lock": "Unlocked",
 				"folder_path": "",
 				"operator_name": "",
-				"modality": "DX",
+				"modality": "",
 				"weight": 0,
 				"thickness": 0,
 				"length": 0,
-				"patient_type": "Dog",
+				"patient_type": "Cat",
 				"study_type": "Normal",
-				"owner_name": "主人孙某",
+				"owner_name": "",
 				"chip_number": "",
 				"variety": "",
 				"is_anaesthesia": false,
 				"is_sedation": false,
-				"mwl": "",
 				"is_exported": false,
 				"is_edited": false,
 				"is_appended": false,
@@ -3635,18 +3688,21 @@ Completed|
 				"qc_result": false,
 				"comment": "",
 				"study_status": "Arrived",
-				"portrait_status": "",
-				"portrait_file": ".",
+				"portrait_status": "NotSaved",
+				"portrait_file": "",
 				"sort": 0,
 				"product": "VETDROS",
-				"create_time": "2025-10-13T08:02:16.638166Z",
+				"create_time": "2025-10-28T11:11:59.118514Z",
 				"series": []
 			}
 		]
 	}
 }
 ```
-
+返回值变更:
+study_id为空字符串的为RIS条目需保存到本地方可使用
+study_id不为空字符串时为本地条目
+protocol_code为从RIS拉取的检查信息,方便技师选择对应体位
 
 ## 23	获取检查信息
 
@@ -3715,6 +3771,12 @@ Completed|
 		"is_anaesthesia": false,
 		"is_sedation": false,
 		"mwl": "",
+		"scheduled": {
+			"scheduled_ae_title": "",
+			"scheduled_performing_physician_name": "",
+			"scheduled_procedure_step_id": ""
+		},
+		"protocol_code": [],
 		"is_exported": false,
 		"is_edited": false,
 		"is_appended": false,
@@ -4307,7 +4369,217 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 33	报告预览
+## 33	[RIS]获取RIS配置
+
+> GET  /dr/api/v1/auth/study/ris
+### 接口说明
+> [RIS]同步RIS
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYzOTExNjUsImlkIjozLCJuYW1lIjoiYWRtaW4ifQ.ivwjTzeMXLwJry1iHN8a9hV90ftSgDZF6n8lEfOtTOQ||
+|Language|en|en_US 或 zh_CN|
+|Product|DROS|DROS 或 VETDROS|
+|Source|Electron|Electron 或 Browser 或 Android|
+### 响应体
+● 200: OK 响应数据格式:JSON
+```json
+{
+	"code": "0x000000",
+	"description": "Success",
+	"solution": "",
+	"data": {
+		"@type": "type.googleapis.com/dr.task.RisConfig",
+		"mwl_enable": true,
+		"mwl_refresh_enable": true,
+		"mwl_refresh_interval": 5
+	}
+}
+```
+mwl_enable: RIS是否启用
+mwl_refresh_enable: RIS自动刷新是否启用
+mwl_refresh_interval: RIS自动刷新间隔
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/28/2MUkNuyg1z6/fcbb988d30914a3ab86b3a1b57ad9ea9)
+
+## 34	[RIS]同步RIS
+
+> POST  /dr/api/v1/auth/study/ris
+### 接口说明
+> [RIS]同步RIS
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYzOTExNjUsImlkIjozLCJuYW1lIjoiYWRtaW4ifQ.ivwjTzeMXLwJry1iHN8a9hV90ftSgDZF6n8lEfOtTOQ||
+|Language|en|en_US 或 zh_CN|
+|Product|DROS|DROS 或 VETDROS|
+|Source|Electron|Electron 或 Browser 或 Android|
+### 请求体(Request Body)
+| 参数名称 | 数据类型 | 默认值 | 不为空 | 描述 |
+| ------ | ------ | ------ | ------ | ------ |
+| start_time|string||true|RFC3339Nano格式|
+| end_time|string||true|RFC3339Nano格式|
+| id|string||false|患者ID|
+| name|string||false|患者姓名|
+| acc_no|string||false|登记号|
+### 响应体
+● 200: OK 响应数据格式:JSON
+```json
+{
+	"code": "0x000000",
+	"description": "Success",
+	"solution": "",
+	"data": {
+		"@type": "type.googleapis.com/dr.task.RisSyncReply",
+		"count": 8
+	}
+}
+```
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/28/2MUk8S1j4Jk/6422ab1394b7491dbdb404b77a740e25)
+
+## 35	[RIS]保存本地
+
+> POST  /dr/api/v1/auth/study/ris/single
+### 接口说明
+> [RIS]保存本地,可填写体位
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYzOTExNjUsImlkIjozLCJuYW1lIjoiYWRtaW4ifQ.ivwjTzeMXLwJry1iHN8a9hV90ftSgDZF6n8lEfOtTOQ||
+|Language|en|en_US 或 zh_CN|
+|Product|DROS|DROS 或 VETDROS|
+|Source|Electron|Electron 或 Browser 或 Android|
+### 请求体(Request Body)
+| 参数名称 | 数据类型 | 默认值 | 不为空 | 描述 |
+| ------ | ------ | ------ | ------ | ------ |
+| entry_id|string||true|RIS条目中的entry_id|
+| views|object||true|体位列表|
+|⇥ view_id|string||true|体位id|
+|⇥ procedure_id|string||true|协议id|
+### 响应体
+● 200: OK 响应数据格式:JSON
+```json
+{
+	"code": "0x000000",
+	"description": "Success",
+	"solution": "",
+	"data": {
+		"@type": "type.googleapis.com/dr.study.Study",
+		"study_instance_uid": "100.118.116.2005.2.1.1132055956.125.3",
+		"study_id": "251028170820252",
+		"public_study_id": "",
+		"specific_character_set": "ISO_IR 192",
+		"accession_number": "",
+		"ref_physician": "",
+		"patient_id": "SC-I3",
+		"patient_name": "Three^Secondary Capture Image",
+		"patient_english_name": "",
+		"patient_former_name": "",
+		"patient_size": "",
+		"other_patient_ids": "",
+		"other_patient_names": "",
+		"patient_age": "",
+		"patient_dob": "1970-01-01T00:00:00Z",
+		"patient_sex": "O",
+		"sex_neutered": "",
+		"pregnancy_status": "",
+		"patient_state": "",
+		"admitting_time": "1970-01-01T00:00:00Z",
+		"priority": "",
+		"reg_source": "Ris",
+		"study_description": "testb",
+		"study_start_datetime": "1970-01-01T00:00:00Z",
+		"study_end_datetime": null,
+		"entry_id": "",
+		"scheduled": {
+			"scheduled_ae_title": "",
+			"scheduled_performing_physician_name": "",
+			"scheduled_procedure_step_id": ""
+		},
+		"protocol_code": [
+			{
+				"code_value": "testa",
+				"code_meaning": "testb"
+			},
+			{
+				"code_value": "testc",
+				"code_meaning": "testd"
+			}
+		],
+		"performed_physician": "",
+		"study_lock": "Unlocked",
+		"folder_path": "",
+		"operator_name": "",
+		"modality": "",
+		"weight": 0,
+		"thickness": 0,
+		"length": 0,
+		"patient_type": "Cat",
+		"study_type": "Normal",
+		"owner_name": "",
+		"chip_number": "",
+		"variety": "",
+		"is_anaesthesia": false,
+		"is_sedation": false,
+		"is_exported": false,
+		"is_edited": false,
+		"is_appended": false,
+		"department": "",
+		"mapped_status": false,
+		"qc_result": false,
+		"comment": "",
+		"study_status": "Arrived",
+		"portrait_status": "NotSaved",
+		"portrait_file": "",
+		"sort": 0,
+		"product": "VETDROS",
+		"create_time": "2025-10-28T09:08:20.657317Z",
+		"series": []
+	}
+}
+```
+返回study信息
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/28/2MUknt60Zua/e87b5e3324f74d7ea043190cea4d14d6)
+
+## 36	[RIS]保存本地(批量)
+
+> POST  /dr/api/v1/auth/study/ris/batch
+### 接口说明
+> [RIS]保存本地(批量),保存后没有体位,曝光前需添加体位
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYzOTExNjUsImlkIjozLCJuYW1lIjoiYWRtaW4ifQ.ivwjTzeMXLwJry1iHN8a9hV90ftSgDZF6n8lEfOtTOQ||
+|Language|en|en_US 或 zh_CN|
+|Product|DROS|DROS 或 VETDROS|
+|Source|Electron|Electron 或 Browser 或 Android|
+### 请求体(Request Body)
+| 参数名称 | 数据类型 | 默认值 | 不为空 | 描述 |
+| ------ | ------ | ------ | ------ | ------ |
+| 请求体|array[string]||true|entry_id列表|
+### 响应体
+● 200: OK 响应数据格式:JSON
+```json
+{
+	"code": "0x000000",
+	"description": "Success",
+	"solution": "",
+	"data": {
+		"@type": "type.googleapis.com/google.protobuf.Empty",
+		"value": {}
+	}
+}
+```
+请求示例:
+```
+[
+	"90d2ee28ef43f742c8c65ee26b8674bd",
+	"4aa9c78d484536f75247f522dc6dfacf"
+]
+```
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/28/2MUmJUYuUfw/7c8ed68c02f64d70aaaad8be670696bb)
+
+
+## 37	报告预览
 
 > POST  /dr/api/v1/auth/report/preview
 ### 接口说明
@@ -4365,7 +4637,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 宠物示例:
 ![  ](http://f1-xyj.fangdeco.cn/attachment/2025/9/23/2LYIk58dZYG/74547ae49660497f90c3db8806c87a72)
 
-## 34	保存报告
+## 38	保存报告
 
 > POST  /dr/api/v1/auth/study/{id}/report
 ### 接口说明
@@ -4431,7 +4703,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 示例
 ![  ](http://f1-xyj.fangdeco.cn/attachment/2025/9/19/2LRW3rk4TRY/74ef9c252fcb4627bafd24e0e0752204)
 
-## 35	获取报告
+## 39	获取报告
 
 > GET  /dr/api/v1/auth/study/{id}/report
 ### 接口说明
@@ -4452,7 +4724,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 请求示例
 /api/v1/auth/study/20250912063732016/report
 
-## 36	[Device]打开设备
+## 40	[Device]打开设备
 
 > POST  /api/v1/auth/device/open
 ### 接口说明
@@ -4483,7 +4755,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 37	[Device]执行Get
+## 41	[Device]执行Get
 
 > POST  /api/v1/auth/device/get
 ### 接口说明
@@ -4515,7 +4787,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 38	[Device]执行Action
+## 42	[Device]执行Action
 
 > POST  /api/v1/auth/device/action
 ### 接口说明
@@ -4550,7 +4822,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 39	[Task]开始检查
+## 43	[Task]开始检查
 
 > POST  /api/v1/auth/task/inspection/start
 ### 接口说明
@@ -4585,7 +4857,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 40	[Task]获取全局状态
+## 44	[Task]获取全局状态
 
 > GET  /api/v1/auth/task/inspection/status
 ### 接口说明
@@ -4632,7 +4904,7 @@ Topic: MODULE/TASK/STATUS/GLOBAL
 ```
 
 
-## 41	[Task]软曝光(包含状态推送说明)
+## 45	[Task]软曝光(包含状态推送说明)
 
 > POST  /api/v1/auth/task/inspection/trigger
 ### 接口说明
@@ -4692,7 +4964,7 @@ Topic: MODULE/TASK/STATUS/GLOBAL
 ```
 
 
-## 42	[Task]接受拒绝图像
+## 46	[Task]接受拒绝图像
 
 > POST  /api/v1/auth/task/inspection/judge
 ### 接口说明
@@ -4728,7 +5000,7 @@ Topic: MODULE/TASK/STATUS/GLOBAL
 ```
 
 
-## 43	[Task]挂起或完成study
+## 47	[Task]挂起或完成study
 
 > POST  /api/v1/auth/task/inspection/leave
 ### 接口说明

+ 190 - 0
elegant-webgl-stability-solution.md

@@ -0,0 +1,190 @@
+# 优雅的 WebGL 上下文稳定性检测方案
+
+## 问题:固定延迟的缺陷
+- 100ms可能不够,某些设备可能需要更长时间
+- 浪费时间,某些设备可能很快就稳定了
+- 不够精确,无法确保真正的稳定性
+
+## 优雅解决方案
+
+### 方案1:WebGL 上下文状态检测 + 轮询
+```typescript
+const waitForWebGLStable = async (canvas: HTMLCanvasElement, timeout = 5000): Promise<boolean> => {
+  const startTime = Date.now();
+  
+  return new Promise((resolve) => {
+    const checkStability = () => {
+      const gl = canvas.getContext('webgl') || canvas.getContext('webgl2');
+      
+      if (!gl) {
+        if (Date.now() - startTime > timeout) {
+          resolve(false);
+          return;
+        }
+        setTimeout(checkStability, 16); // ~60fps
+        return;
+      }
+      
+      // 检查 WebGL 状态
+      const isStable = 
+        gl.getError() === gl.NO_ERROR && 
+        gl.getParameter(gl.RENDERER) && 
+        gl.getParameter(gl.VERSION);
+      
+      if (isStable) {
+        resolve(true);
+      } else if (Date.now() - startTime > timeout) {
+        resolve(false);
+      } else {
+        setTimeout(checkStability, 16);
+      }
+    };
+    
+    checkStability();
+  });
+};
+```
+
+### 方案2:Cornerstone 渲染引擎就绪检测
+```typescript
+const waitForViewportReady = async (viewport: any, timeout = 5000): Promise<boolean> => {
+  const startTime = Date.now();
+  
+  return new Promise((resolve) => {
+    const checkReady = () => {
+      try {
+        // 检查 viewport 是否完全初始化
+        const canvas = viewport.canvas;
+        const element = viewport.element;
+        
+        if (!canvas || !element) {
+          if (Date.now() - startTime > timeout) {
+            resolve(false);
+            return;
+          }
+          setTimeout(checkReady, 16);
+          return;
+        }
+        
+        // 检查渲染状态
+        const renderingState = viewport.getRenderingEngine?.()?.hasBeenDestroyed;
+        if (renderingState === false) {
+          resolve(true);
+        } else if (Date.now() - startTime > timeout) {
+          resolve(false);
+        } else {
+          setTimeout(checkReady, 16);
+        }
+      } catch (error) {
+        if (Date.now() - startTime > timeout) {
+          resolve(false);
+        } else {
+          setTimeout(checkReady, 16);
+        }
+      }
+    };
+    
+    checkReady();
+  });
+};
+```
+
+### 方案3:组合方案 - 事件驱动 + 状态检测
+```typescript
+const [isViewportReady, setIsViewportReady] = useState(false);
+
+// 在创建 viewport 后检测就绪状态
+useEffect(() => {
+  if (!stackViewport) return;
+  
+  const checkViewportReady = async () => {
+    try {
+      // 方法1:等待canvas元素存在
+      const canvas = viewportRef.current?.querySelector('canvas');
+      if (!canvas) {
+        setTimeout(checkViewportReady, 16);
+        return;
+      }
+      
+      // 方法2:等待WebGL上下文稳定
+      const isWebGLStable = await waitForWebGLStable(canvas);
+      if (!isWebGLStable) {
+        console.warn('[DcmCell] WebGL上下文未稳定');
+        return;
+      }
+      
+      // 方法3:等待viewport就绪
+      const isViewportStable = await waitForViewportReady(stackViewport);
+      if (!isViewportStable) {
+        console.warn('[DcmCell] Viewport未就绪');
+        return;
+      }
+      
+      console.log('[DcmCell] Viewport完全就绪');
+      setIsViewportReady(true);
+      
+    } catch (error) {
+      console.error('[DcmCell] 检测viewport就绪状态失败:', error);
+    }
+  };
+  
+  checkViewportReady();
+}, [stackViewport]);
+
+// 只有当viewport就绪后才加载图像
+useEffect(() => {
+  if (!isViewportReady || !stackViewport) return;
+  
+  const loadImageFromProps = async () => {
+    // 避免重复加载相同图像
+    if (imageId === currentImageId) return;
+    
+    if (imageId && imageId.trim() !== '') {
+      console.log(`[DcmCell] Viewport就绪,开始加载图像: ${imageId}`);
+      const success = await loadImage(imageId, 'props');
+      if (success) {
+        setCurrentImageId(imageId);
+      }
+    } else {
+      // 清空逻辑...
+    }
+  };
+
+  loadImageFromProps();
+}, [imageId, isViewportReady, stackViewport]);
+```
+
+### 方案4:简化版 - requestAnimationFrame
+```typescript
+const waitForStableFrame = (): Promise<void> => {
+  return new Promise(resolve => {
+    // 等待2-3帧确保渲染稳定
+    requestAnimationFrame(() => {
+      requestAnimationFrame(() => {
+        requestAnimationFrame(resolve);
+      });
+    });
+  });
+};
+
+useEffect(() => {
+  const loadImageFromProps = async () => {
+    if (!stackViewport) return;
+    
+    // 等待几个渲染帧确保稳定
+    await waitForStableFrame();
+    
+    // 避免重复加载相同图像
+    if (imageId === currentImageId) return;
+    
+    // 继续正常的加载逻辑...
+  };
+
+  loadImageFromProps();
+}, [imageId, stackViewport]);
+```
+
+## 推荐实施顺序
+1. 首先尝试方案4(最简单,通常有效)
+2. 如果仍有问题,升级到方案3(完整的状态检测)
+3. 方案1和2作为底层工具函数支持

+ 54 - 0
film-image-residue-analysis.md

@@ -0,0 +1,54 @@
+# 胶片切换图像残留问题深度分析
+
+## 问题现象
+切换胶片后,仍然会有图像残留在新胶片的viewport中。
+
+## 根本原因分析
+
+### 1. React组件复用问题
+```typescript
+// Film.tsx 中的问题代码
+const generateCell = (imageId, indexOfCell) => {
+  return (<ViewportContainer imageId={imageId} currentFilm={currentFilm} indexOfCell={indexOfCell} />);
+};
+```
+
+**问题**:没有为ViewportContainer提供`key`属性,导致React在切换胶片时复用组件实例,只更新props。
+
+### 2. 渲染引擎状态残留
+虽然按胶片ID创建独立的渲染引擎,但切换回之前访问过的胶片时,该渲染引擎可能仍保留之前的viewport状态。
+
+### 3. 组件卸载时序问题
+当切换胶片时,旧的DcmCell组件可能没有在正确的时机卸载和清理viewport。
+
+## 解决方案
+
+### 1. 为ViewportContainer添加唯一key
+```typescript
+// Film.tsx 修复
+const generateCell = (imageId, indexOfCell) => {
+  const uniqueKey = `${currentFilm.id}-${indexOfCell}`;
+  return (
+    <ViewportContainer 
+      key={uniqueKey}
+      imageId={imageId} 
+      currentFilm={currentFilm} 
+      indexOfCell={indexOfCell} 
+    />
+  );
+};
+```
+
+### 2. 改进渲染引擎清理策略
+```typescript
+// 选择一:胶片切换时强制清理所有viewport
+// 选择二:改进viewport的清理逻辑,确保完全清理
+```
+
+### 3. 添加胶片切换时的强制重置
+在切换胶片时,主动清理当前显示的所有viewport,确保状态重置。
+
+## 推荐修复步骤
+1. 首先修复Film.tsx中的key问题
+2. 改进DcmCell.tsx的清理逻辑
+3. 考虑在printSlice中添加胶片切换时的清理action

+ 100 - 0
image-loading-failure-analysis.md

@@ -0,0 +1,100 @@
+# 图像加载失败问题分析
+
+## 问题现象
+- WebGL 警告出现但不崩溃
+- 图像没有显示出来
+- 错误发生在切换胶片时的自动加载 (`loadImageFromProps`)
+
+## 根本原因
+
+### 时序竞争问题
+```typescript
+// 当组件重新创建时(由于 React key 修复)
+1. useEffect() 开始执行 - 初始化渲染引擎和 viewport
+2. useEffect([imageId, stackViewport]) 同时触发 - 尝试加载图像
+3. 但是 WebGL 上下文可能还没有完全稳定
+4. 导致图像加载在不稳定的上下文中失败
+```
+
+### WebGL 上下文初始化不完整
+- 新创建的 WebGL 上下文需要时间来检查和加载扩展
+- `OES_texture_float_linear` 检查失败可能影响后续的纹理操作
+- 虽然 Cornerstone 会尝试降级,但可能导致初次加载失败
+
+## 解决方案
+
+### 方案1:延迟图像加载
+```typescript
+useEffect(() => {
+  const loadImageFromProps = async () => {
+    if (!stackViewport) return;
+    
+    // 给 WebGL 上下文一些时间稳定
+    await new Promise(resolve => setTimeout(resolve, 100));
+    
+    // 避免重复加载相同图像
+    if (imageId === currentImageId) return;
+    
+    if (imageId && imageId.trim() !== '') {
+      console.log(`[DcmCell] 开始从 props 加载图像: ${imageId}`);
+      const success = await loadImage(imageId, 'props');
+      if (success) {
+        setCurrentImageId(imageId);
+      }
+    } else {
+      // 清空逻辑...
+    }
+  };
+
+  loadImageFromProps();
+}, [imageId, stackViewport]);
+```
+
+### 方案2:检查 WebGL 状态
+```typescript
+const isWebGLContextStable = () => {
+  const canvas = viewportRef.current?.querySelector('canvas');
+  if (!canvas) return false;
+  
+  const gl = canvas.getContext('webgl') || canvas.getContext('webgl2');
+  return gl && gl.getError() === gl.NO_ERROR;
+};
+```
+
+### 方案3:改进错误处理和重试
+```typescript
+const loadImage = async (targetImageId: string, source: 'props' | 'drag' = 'props', retryCount = 0): Promise<boolean> => {
+  if (!stackViewport || !targetImageId) return false;
+  
+  try {
+    const fullImageUrl = targetImageId.startsWith('dicomweb:')
+      ? targetImageId
+      : getDcmImageUrl(targetImageId);
+    
+    await stackViewport.setStack([fullImageUrl], 0);
+    stackViewport.render();
+    setHasImage(true);
+    
+    console.log(`[DcmCell] 图像加载成功 (${source}): ${targetImageId}`);
+    return true;
+  } catch (error) {
+    console.error(`[DcmCell] 图像加载失败 (${source}):`, error);
+    
+    // 如果是 WebGL 相关错误且还没重试过,进行重试
+    if (retryCount === 0 && error.message && 
+        (error.message.includes('WebGL') || error.message.includes('texture'))) {
+      console.log(`[DcmCell] WebGL 错误,1秒后重试...`);
+      await new Promise(resolve => setTimeout(resolve, 1000));
+      return loadImage(targetImageId, source, 1);
+    }
+    
+    setHasImage(false);
+    return false;
+  }
+};
+```
+
+## 推荐解决步骤
+1. 首先尝试延迟加载方案(最简单)
+2. 如果仍有问题,添加错误重试机制
+3. 最后考虑 WebGL 状态检查

+ 97 - 0
react-key-viewport-causality-analysis.md

@@ -0,0 +1,97 @@
+# React Key 缺失导致 Viewport 图像残留的因果关系分析
+
+## 问题:为什么没有 key 会导致图像残留?
+
+### React 组件重用机制
+```typescript
+// Film.tsx 当前代码(有问题)
+const generateCell = (imageId, indexOfCell) => {
+  return (<ViewportContainer imageId={imageId} currentFilm={currentFilm} indexOfCell={indexOfCell} />);
+};
+```
+
+### 因果关系链
+
+#### 1. 没有 key → React 重用组件实例
+- **没有 key 时**:React 认为相同位置的相同类型组件是同一个组件
+- **切换胶片时**:
+  - 胶片1的第一个格子:`<ViewportContainer imageId="image-A" .../>`
+  - 切换到胶片2的第一个格子:`<ViewportContainer imageId="image-B" .../>`
+  - React 认为这是**同一个组件**,只需要更新 props
+
+#### 2. 组件重用 → 内部状态不重置
+```typescript
+// ViewportContainer 内部状态在组件重用时不会重置
+const [hasImage, setHasImage] = useState<boolean>(false);
+const [currentImageId, setCurrentImageId] = useState<string | null>(null);
+const [stackViewport, setStackViewport] = useState<any>(null);
+const viewportRef = useRef(null);
+
+// 🚨 问题:切换胶片时这些状态保持原值!
+```
+
+#### 3. 状态不重置 → Viewport 实例保持原有状态
+```typescript
+// useEffect 只在组件首次挂载时执行
+useEffect(() => {
+  // 创建渲染引擎和 viewport...
+  setStackViewport(viewport);
+}, []); // 空依赖数组意味着只执行一次
+
+// 🚨 关键问题:viewport 实例仍然绑定着之前的图像数据
+```
+
+#### 4. Viewport 状态保持 → 图像残留显示
+- Cornerstone 的 viewport 实例内部缓存了图像数据
+- 即使 props 中的 `imageId` 改变,viewport 可能仍显示缓存的图像
+- `useEffect([imageId, stackViewport])` 虽然会触发,但 `stackViewport` 实例本身没变
+
+## 具体场景演示
+
+### 场景:胶片1 → 胶片2 切换
+```
+1. 用户在胶片1拖拽图像A到第一个格子
+   - ViewportContainer 创建 viewport-1
+   - viewport-1 显示图像A
+   - hasImage = true, currentImageId = "imageA"
+
+2. 用户切换到胶片2(第一个格子为空)
+   - React 重用 ViewportContainer 组件实例
+   - props 更新:imageId 从 "imageA" 变为 null
+   - 但是 viewport-1 实例仍然存在,仍然显示图像A
+   - 虽然触发 useEffect,但清空逻辑可能执行不彻底
+
+3. 结果:胶片2显示了胶片1的图像
+```
+
+## 解决方案:为什么 key 能解决问题?
+
+### 有 key 的情况
+```typescript
+// 修复后的代码
+const generateCell = (imageId, indexOfCell) => {
+  const uniqueKey = `${currentFilm.id}-${indexOfCell}`;
+  return (
+    <ViewportContainer 
+      key={uniqueKey}  // 🎯 关键修复
+      imageId={imageId} 
+      currentFilm={currentFilm} 
+      indexOfCell={indexOfCell} 
+    />
+  );
+};
+```
+
+### 修复后的因果关系
+```
+1. 切换胶片时,key 从 "1-0" 变为 "2-0"
+2. React 识别这是不同的组件,卸载旧组件,创建新组件
+3. 旧组件卸载 → 触发 cleanup 函数 → 清理 viewport
+4. 新组件挂载 → 重新初始化所有状态 → 创建新的 viewport
+5. 结果:完全隔离,没有状态残留
+```
+
+## 总结
+**核心问题**:React 组件重用 + Cornerstone Viewport 状态持久化
+**根本原因**:没有 key 导致 React 错误地重用了应该重新创建的组件
+**解决方案**:通过 key 强制 React 在胶片切换时重新创建组件实例

+ 109 - 0
webgl-error-timing-analysis.md

@@ -0,0 +1,109 @@
+# WebGL 错误仅在切换胶片时出现的原因分析
+
+## 现象
+- **拖拽图像时**:没有 WebGL 错误
+- **切换胶片时**:出现 `OES_texture_float_linear` 错误
+
+## 根本原因分析
+
+### 1. React Key 修复的副作用
+我们刚刚添加了 `key={currentFilm.id}-${indexOfCell}`,这导致:
+```typescript
+// 切换胶片时的组件生命周期变化
+胶片1 → 胶片2:
+- 旧组件卸载: ViewportContainer key="1-0" → unmount
+- 新组件创建: ViewportContainer key="2-0" → mount
+```
+
+### 2. 渲染引擎初始化时机差异
+
+#### 拖拽时的执行路径:
+```
+1. 组件已完全初始化 (useEffect 已执行)
+2. 渲染引擎稳定运行
+3. viewport 已创建并稳定
+4. handleDrop → loadImage('drag') 
+5. 在已有的、稳定的 WebGL 上下文中操作
+```
+
+#### 切换胶片时的执行路径:
+```
+1. React 创建新组件实例 (key 不同)
+2. useEffect 开始执行:
+   - 创建新的渲染引擎: `renderingEngine-${currentFilm.id}`
+   - 初始化 WebGL 上下文
+   - 创建新的 viewport
+3. 同时 useEffect([imageId, stackViewport]) 也触发:
+   - loadImage('props') 立即执行
+   - 在新创建的、可能不稳定的 WebGL 上下文中操作
+```
+
+### 3. WebGL 上下文状态差异
+
+#### 稳定的 WebGL 上下文 (拖拽时)
+- 扩展已加载和验证
+- 资源已分配
+- 状态机稳定
+
+#### 新创建的 WebGL 上下文 (切换胶片时)
+- 扩展正在检查和加载
+- 某些扩展可能检查失败但不影响基本功能
+- 状态机可能不稳定
+
+### 4. Cornerstone.js 的扩展检查机制
+```typescript
+// Cornerstone 在新的 WebGL 上下文中会重新检查所有扩展
+// 包括 OES_texture_float_linear
+// 如果检查失败,会记录警告但继续执行
+```
+
+## 证据支持
+
+### 错误堆栈证实了这个分析:
+```
+loadImage @ DcmCell.tsx:56          // 我们的 loadImage 函数
+loadImageFromProps @ DcmCell.tsx:183 // useEffect 中的调用,不是拖拽
+```
+
+这确认错误来自切换胶片时的自动加载,而不是拖拽。
+
+## 结论
+
+1. **错误是良性的**:这是 Cornerstone.js 在新 WebGL 上下文中检查扩展时的警告
+2. **功能正常**:图像仍然可以正常加载和显示
+3. **原因是我们的修复**:React Key 强制重新创建组件暴露了这个检查过程
+
+## 建议处理方式
+
+### 方案1:忽略这个警告(推荐)
+```typescript
+// 这是一个良性警告,不影响功能
+// Cornerstone.js 会自动降级到兼容的渲染模式
+```
+
+### 方案2:抑制特定的 console 输出
+```typescript
+// 如果确实需要减少日志噪音
+const originalWarn = console.warn;
+console.warn = (...args) => {
+  if (args[0] && args[0].includes('OES_texture_float_linear')) {
+    return; // 静默这个特定警告
+  }
+  originalWarn.apply(console, args);
+};
+```
+
+### 方案3:延迟图像加载
+```typescript
+// 确保 WebGL 上下文完全初始化后再加载图像
+useEffect(() => {
+  const loadImageFromProps = async () => {
+    if (!stackViewport) return;
+    
+    // 给 WebGL 上下文一点初始化时间
+    await new Promise(resolve => setTimeout(resolve, 100));
+    
+    // 然后正常加载
+    // ...
+  };
+}, [imageId, stackViewport]);

+ 102 - 0
webgl-texture-error-analysis.md

@@ -0,0 +1,102 @@
+# WebGL 纹理扩展错误分析
+
+## 错误信息
+```
+Failed to load OES_texture_float_linear. Texture filtering is not available for *32F internal formats.
+```
+
+## 错误原因分析
+
+### 1. WebGL 扩展支持问题
+- `OES_texture_float_linear` 是一个 WebGL 扩展
+- 用于支持 32 位浮点纹理的线性过滤
+- 不是所有 GPU/浏览器都支持这个扩展
+
+### 2. 医学图像格式问题
+- DICOM 图像通常使用高精度格式(16位或32位)
+- Cornerstone.js 尝试使用浮点纹理来处理这些高精度图像
+- 当 GPU 不支持浮点纹理线性过滤时就会报错
+
+### 3. 发生位置
+```typescript
+// DcmCell.tsx:56 - stackViewport.setStack() 调用时
+await stackViewport.setStack([fullImageUrl], 0);
+stackViewport.render(); // 可能在这里触发WebGL错误
+```
+
+## 解决方案
+
+### 方案1:添加 WebGL 兼容性检测
+```typescript
+// 检测 WebGL 扩展支持
+const checkWebGLSupport = () => {
+  const canvas = document.createElement('canvas');
+  const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+  if (!gl) return false;
+  
+  const floatLinearExt = gl.getExtension('OES_texture_float_linear');
+  return !!floatLinearExt;
+};
+```
+
+### 方案2:配置 Cornerstone 渲染选项
+```typescript
+// 在创建 viewport 时配置渲染选项
+const viewportInput: cornerstone.Types.PublicViewportInput = {
+  viewportId: viewportId,
+  type: Enums.ViewportType.STACK,
+  element: viewportRef.current,
+  defaultOptions: {
+    background: [0.1, 0.1, 0.1],
+    // 添加渲染配置
+    renderer: {
+      preferLowerPrecision: true, // 优先使用低精度纹理
+      enableFloatTextures: false, // 禁用浮点纹理
+    }
+  },
+};
+```
+
+### 方案3:改进错误处理
+```typescript
+const loadImage = async (targetImageId: string, source: 'props' | 'drag' = 'props'): Promise<boolean> => {
+  if (!stackViewport || !targetImageId) return false;
+  
+  try {
+    const fullImageUrl = targetImageId.startsWith('dicomweb:')
+      ? targetImageId
+      : getDcmImageUrl(targetImageId);
+    
+    await stackViewport.setStack([fullImageUrl], 0);
+    stackViewport.render();
+    setHasImage(true);
+    
+    console.log(`[DcmCell] 图像加载成功 (${source}): ${targetImageId}`);
+    return true;
+  } catch (error) {
+    // 特别处理 WebGL 相关错误
+    if (error.message && error.message.includes('OES_texture_float_linear')) {
+      console.warn(`[DcmCell] WebGL 浮点纹理不支持,尝试降级渲染: ${error.message}`);
+      // 尝试降级处理
+      try {
+        // 可以尝试不同的渲染配置
+        await stackViewport.setStack([fullImageUrl], 0);
+        stackViewport.render();
+        setHasImage(true);
+        return true;
+      } catch (fallbackError) {
+        console.error(`[DcmCell] 降级渲染也失败: ${fallbackError}`);
+      }
+    }
+    
+    console.error(`[DcmCell] 图像加载失败 (${source}):`, error);
+    setHasImage(false);
+    return false;
+  }
+};
+```
+
+## 推荐实施步骤
+1. 首先添加更详细的错误处理和日志
+2. 检查是否可以配置 Cornerstone 使用更兼容的渲染模式
+3. 如果问题持续,考虑添加 WebGL 兼容性检测和降级方案