Quellcode durchsuchen

feat: 实现回收站功能及修复删除操作bug

- 新增 binActions.ts 实现回收站API接口(获取列表、磁盘信息、恢复、删除、清空)
- 新增 bin 状态管理模块(binSlice、binDiskInfoSlice、binFilter类型定义)
- 完善 Bin.tsx 主页面接入Redux状态管理
- 完善 BinOperationPanel 和 BinActionPanel 实现操作逻辑和磁盘信息显示
- 修改 SearchPanel 支持回收站页面过滤功能(根据currentKey调用不同API)
- 在 store.ts 中注册bin相关reducers
- 修复删除操作bug:在thunk中返回IDs避免action.payload为undefined
- 新增完整的回收站功能实现文档

改动文件:
- docs/DR.md (更新API文档)
- docs/实现/回收站功能.md (新增)
- src/API/patient/binActions.ts (新增)
- src/states/patient/bin/types/binFilter.ts (新增)
- src/states/patient/bin/slices/binSlice.ts (新增)
- src/states/patient/bin/slices/binDiskInfoSlice.ts (新增)
- src/states/store.ts
- src/pages/patient/Bin.tsx
- src/pages/patient/components/BinOperationPanel.tsx
- src/pages/patient/components/BinActionPanel.tsx
- src/pages/patient/components/SearchPanel.tsx
sw vor 1 Tag
Ursprung
Commit
dcc73cdda9

+ 547 - 24
docs/DR.md

@@ -2401,7 +2401,530 @@ zh_CN
 请求示例
 ![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/11/2M2HYvSokoS/13f07b5d144c465daff5ca8b70cc82c2)
 
-## 17	登记检查信息
+## 17	回收站
+
+## 17.1	(回收站)获取磁盘信息
+
+> GET  /dr/api/v1/auth/recycle_bin/disk_info
+### 接口说明
+> (回收站)获取磁盘信息
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYxOTU0MDIsImlkIjozLCJuYW1lIjoiYWRtaW4ifQ.jK86cKyAMwfgxPsE6mpvSE1PF99jHrppGJZdB8G9uEw||
+|Language||en_US 或 zh_CN|
+|Product|DROS|DROS 或 VETDROS|
+|Source|Electron|Electron 或 Browser 或 Android|
+### 响应体
+● 200: OK 响应数据格式:JSON
+```json
+{
+	"code": "0x000000",
+	"data": {
+		"total": 243047182336,
+		"total_str": "226.36 GB",
+		"free": 200334045184,
+		"free_str": "186.58 GB",
+		"recycle_bin": 0,
+		"recycle_bin_str": "0.00 KB"
+	},
+	"description": "Success",
+	"solution": ""
+}
+```
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/15/2M8unmrIn56/954dcd1bc845496f8628d55c32e97774)
+
+## 17.2	(回收站)获取检查列表
+
+> GET  /dr/api/v1/auth/recycle_bin/study/
+### 接口说明
+> 获取检查列表
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYzOTExNjUsImlkIjozLCJuYW1lIjoiYWRtaW4ifQ.ivwjTzeMXLwJry1iHN8a9hV90ftSgDZF6n8lEfOtTOQ||
+|Language|en|en_US 或 zh_CN|
+|Product|DROS|DROS 或 VETDROS|
+|Source|Electron|Electron 或 Browser 或 Android|
+### 请求参数(Query Param)
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|id||模糊搜索|
+|name||模糊搜索|
+|acc_no||模糊搜索|
+|start_time||RFC3339Nano格式
+例:2025-06-19T00:00:00.000+08:00|
+|end_time||RFC3339Nano格式
+例:2025-06-19T23:59:59.999+08:00|
+|page|1|页码
+取值范围:>0|
+|page_size|10|行数
+取值范围:1-10000|
+### 响应体
+● 200: OK 响应数据格式:JSON
+```json
+{
+	"code": "0x000000",
+	"description": "成功",
+	"solution": "",
+	"data": {
+		"@type": "type.googleapis.com/dr.study.StudyList",
+		"count": 2,
+		"studies": [
+			{
+				"study_instance_uid": "2.25.156.999999.0000.1.2.8323328.20101.1760359944.254055",
+				"study_id": "251013205224825",
+				"public_study_id": "",
+				"specific_character_set": "ISO_IR 192",
+				"accession_number": "accessnumber444",
+				"ref_physician": "",
+				"patient_id": "1113333",
+				"patient_name": "124",
+				"patient_english_name": "",
+				"patient_former_name": "",
+				"patient_size": "Medium",
+				"other_patient_ids": "",
+				"other_patient_names": "",
+				"patient_age": "000Y",
+				"patient_dob": "2025-10-13T12:52:15.210Z",
+				"patient_sex": "",
+				"sex_neutered": "",
+				"pregnancy_status": "",
+				"patient_state": "",
+				"admitting_time": null,
+				"priority": "",
+				"reg_source": "",
+				"study_description": "",
+				"study_start_datetime": "2025-10-13T12:52:24.255622Z",
+				"study_end_datetime": null,
+				"scheduled_procedure_step_start_date": null,
+				"performed_physician": "",
+				"study_lock": "Unlocked",
+				"folder_path": "",
+				"operator_name": "",
+				"modality": "DX",
+				"weight": 0,
+				"thickness": 0,
+				"length": 0,
+				"patient_type": "Dog",
+				"study_type": "Normal",
+				"owner_name": "主人孙某",
+				"chip_number": "",
+				"variety": "",
+				"is_anaesthesia": false,
+				"is_sedation": false,
+				"mwl": "",
+				"is_exported": false,
+				"is_edited": false,
+				"is_appended": false,
+				"department": "",
+				"mapped_status": false,
+				"qc_result": false,
+				"comment": "",
+				"study_status": "InProgress",
+				"portrait_status": "NotSaved",
+				"portrait_file": ".",
+				"sort": 0,
+				"product": "VETDROS",
+				"create_time": "2025-10-13T12:52:24.313859Z",
+				"series": []
+			},
+			{
+				"study_instance_uid": "2.25.156.999999.0000.1.2.8323328.4730.1760342536.600785",
+				"study_id": "251013160216342",
+				"public_study_id": "",
+				"specific_character_set": "ISO_IR 192",
+				"accession_number": "accessnumber444",
+				"ref_physician": "",
+				"patient_id": "1113333",
+				"patient_name": "124",
+				"patient_english_name": "",
+				"patient_former_name": "",
+				"patient_size": "Medium",
+				"other_patient_ids": "",
+				"other_patient_names": "",
+				"patient_age": "000Y",
+				"patient_dob": "2025-10-13T08:02:11.982Z",
+				"patient_sex": "",
+				"sex_neutered": "",
+				"pregnancy_status": "",
+				"patient_state": "",
+				"admitting_time": null,
+				"priority": "",
+				"reg_source": "",
+				"study_description": "",
+				"study_start_datetime": "2025-10-13T08:02:16.602164Z",
+				"study_end_datetime": null,
+				"scheduled_procedure_step_start_date": null,
+				"performed_physician": "",
+				"study_lock": "Unlocked",
+				"folder_path": "",
+				"operator_name": "",
+				"modality": "DX",
+				"weight": 0,
+				"thickness": 0,
+				"length": 0,
+				"patient_type": "Dog",
+				"study_type": "Normal",
+				"owner_name": "主人孙某",
+				"chip_number": "",
+				"variety": "",
+				"is_anaesthesia": false,
+				"is_sedation": false,
+				"mwl": "",
+				"is_exported": false,
+				"is_edited": false,
+				"is_appended": false,
+				"department": "",
+				"mapped_status": false,
+				"qc_result": false,
+				"comment": "",
+				"study_status": "Arrived",
+				"portrait_status": "",
+				"portrait_file": ".",
+				"sort": 0,
+				"product": "VETDROS",
+				"create_time": "2025-10-13T08:02:16.638166Z",
+				"series": []
+			}
+		]
+	}
+}
+```
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/15/2M8r0e0RUae/5193822f96b74f41a768ae2168e50993)
+
+## 17.3	(回收站)获取检查信息
+
+> GET  /dr/api/v1/auth/recycle_bin/study/{id}
+### 接口说明
+> :id -- 检查信息ID(study_id)  例:20250619164137632
+### 地址参数(Path Variable)
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|id||study_id|
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTYxOTU0MDIsImlkIjozLCJuYW1lIjoiYWRtaW4ifQ.jK86cKyAMwfgxPsE6mpvSE1PF99jHrppGJZdB8G9uEw||
+|Language||en_US 或 zh_CN|
+|Product|DROS|DROS 或 VETDROS|
+|Source|Electron|Electron 或 Browser 或 Android|
+### 响应体
+● 200: OK 响应数据格式:JSON
+```json
+{
+	"code": "0x000000",
+	"description": "成功",
+	"solution": "",
+	"data": {
+		"@type": "type.googleapis.com/dr.study.Study",
+		"study_instance_uid": "2.25.156.999999.0000.1.2.8323328.289656.1760324762.914274",
+		"study_id": "251013110602828",
+		"public_study_id": "",
+		"specific_character_set": "ISO_IR 192",
+		"accession_number": "accessnumber444",
+		"ref_physician": "",
+		"patient_id": "1113333",
+		"patient_name": "124",
+		"patient_english_name": "",
+		"patient_former_name": "",
+		"patient_size": "Medium",
+		"other_patient_ids": "",
+		"other_patient_names": "",
+		"patient_age": "000Y",
+		"patient_dob": "2025-10-13T03:04:39.003Z",
+		"patient_sex": "",
+		"sex_neutered": "",
+		"pregnancy_status": "",
+		"patient_state": "",
+		"admitting_time": null,
+		"priority": "",
+		"reg_source": "",
+		"study_description": "",
+		"study_start_datetime": "2025-10-13T03:06:02.988664Z",
+		"study_end_datetime": null,
+		"scheduled_procedure_step_start_date": null,
+		"performed_physician": "",
+		"study_lock": "Unlocked",
+		"folder_path": "",
+		"operator_name": "",
+		"modality": "DX",
+		"weight": 0,
+		"thickness": 0,
+		"length": 0,
+		"patient_type": "Dog",
+		"study_type": "Emergency",
+		"owner_name": "主人孙某",
+		"chip_number": "",
+		"variety": "",
+		"is_anaesthesia": false,
+		"is_sedation": false,
+		"mwl": "",
+		"is_exported": false,
+		"is_edited": false,
+		"is_appended": false,
+		"department": "",
+		"mapped_status": false,
+		"qc_result": false,
+		"comment": "",
+		"study_status": "Completed",
+		"portrait_status": "Saved",
+		"portrait_file": "2.25.156.999999.0000.1.5.8323328.289656.1760324762.914274.dcm",
+		"sort": 0,
+		"product": "VETDROS",
+		"create_time": "2025-10-13T03:06:03.016908Z",
+		"series": [
+			{
+				"series_instance_uid": "2.25.156.999999.0000.1.3.8323328.289656.1760324762.914275",
+				"study_instance_uid": "2.25.156.999999.0000.1.2.8323328.289656.1760324762.914274",
+				"study_id": "251013110602828",
+				"procedure_id": "",
+				"patient_type": "Dog",
+				"body_part": "Dog_Hip",
+				"performed_datetime": null,
+				"performed_protocol_code_meaning": "",
+				"performed_protocol_code_value": "",
+				"sort": 1,
+				"product": "VETDROS",
+				"is_pre_install": true,
+				"create_time": "2025-10-13T03:06:03.019906Z",
+				"images": [
+					{
+						"sop_instance_uid": "2.25.156.999999.0000.1.4.8323328.289656.1760324762.914276",
+						"series_instance_uid": "2.25.156.999999.0000.1.3.8323328.289656.1760324762.914275",
+						"study_instance_uid": "2.25.156.999999.0000.1.2.8323328.289656.1760324762.914274",
+						"secondary_sop_uid": "",
+						"study_id": "251013110602828",
+						"view_id": "View_Dog_PV_LAT_02",
+						"view_description": "髋关节侧位",
+						"patient_type": "Dog",
+						"body_part_id": "Dog_Hip",
+						"anatomic_region": "Hip joint",
+						"image_type": "expose",
+						"image_file_path": "",
+						"image_file": "2.25.156.999999.0000.1.4.8323328.289656.1760324762.914276.dcm",
+						"thumbnail_file": "2.25.156.999999.0000.1.4.8323328.289656.1760324762.914276.webp",
+						"acquisition_mode": "RAD",
+						"acquisition_context": null,
+						"img_proc_context": null,
+						"comment": "task_version: 0.5.0-7-g3a9cf08",
+						"expose_status": "Exposed",
+						"expose_time": "2025-10-13T03:07:12.292005Z",
+						"judged_status": "Accept",
+						"send_status": "Unsent",
+						"export_status": "NotExported",
+						"storage_status": "Saved",
+						"raw_exists": true,
+						"ticket": "",
+						"sort": 1,
+						"product": "VETDROS",
+						"is_pre_install": true,
+						"create_time": "2025-10-13T03:06:03.022905Z"
+					}
+				]
+			},
+			{
+				"series_instance_uid": "2.25.156.999999.0000.1.3.8323328.289656.1760324762.914277",
+				"study_instance_uid": "2.25.156.999999.0000.1.2.8323328.289656.1760324762.914274",
+				"study_id": "251013110602828",
+				"procedure_id": "",
+				"patient_type": "Dog",
+				"body_part": "Dog_Hip",
+				"performed_datetime": null,
+				"performed_protocol_code_meaning": "",
+				"performed_protocol_code_value": "",
+				"sort": 2,
+				"product": "VETDROS",
+				"is_pre_install": true,
+				"create_time": "2025-10-13T03:06:03.021321Z",
+				"images": [
+					{
+						"sop_instance_uid": "2.25.156.999999.0000.1.4.8323328.289656.1760324763.914278",
+						"series_instance_uid": "2.25.156.999999.0000.1.3.8323328.289656.1760324762.914277",
+						"study_instance_uid": "2.25.156.999999.0000.1.2.8323328.289656.1760324762.914274",
+						"secondary_sop_uid": "",
+						"study_id": "251013110602828",
+						"view_id": "View_Dog_PV_LAT_01",
+						"view_description": "骨盆侧位",
+						"patient_type": "Dog",
+						"body_part_id": "Dog_Hip",
+						"anatomic_region": "Pelvis",
+						"image_type": "expose",
+						"image_file_path": "",
+						"image_file": "2.25.156.999999.0000.1.4.8323328.289656.1760324763.914278.dcm",
+						"thumbnail_file": "2.25.156.999999.0000.1.4.8323328.289656.1760324763.914278.webp",
+						"acquisition_mode": "RAD",
+						"acquisition_context": null,
+						"img_proc_context": null,
+						"comment": "task_version: 0.5.0-7-g3a9cf08",
+						"expose_status": "Exposed",
+						"expose_time": "2025-10-13T03:30:24.191742Z",
+						"judged_status": "Accept",
+						"send_status": "Unsent",
+						"export_status": "NotExported",
+						"storage_status": "Saved",
+						"raw_exists": true,
+						"ticket": "",
+						"sort": 2,
+						"product": "VETDROS",
+						"is_pre_install": true,
+						"create_time": "2025-10-13T03:06:03.024446Z"
+					}
+				]
+			},
+			{
+				"series_instance_uid": "2.25.156.999999.0000.1.3.8323328.289656.1760324763.914279",
+				"study_instance_uid": "2.25.156.999999.0000.1.2.8323328.289656.1760324762.914274",
+				"study_id": "251013110602828",
+				"procedure_id": "",
+				"patient_type": "Dog",
+				"body_part": "Dog_Hip",
+				"performed_datetime": null,
+				"performed_protocol_code_meaning": "",
+				"performed_protocol_code_value": "",
+				"sort": 3,
+				"product": "VETDROS",
+				"is_pre_install": true,
+				"create_time": "2025-10-13T03:06:03.022034Z",
+				"images": [
+					{
+						"sop_instance_uid": "2.25.156.999999.0000.1.4.8323328.289656.1760324763.914280",
+						"series_instance_uid": "2.25.156.999999.0000.1.3.8323328.289656.1760324763.914279",
+						"study_instance_uid": "2.25.156.999999.0000.1.2.8323328.289656.1760324762.914274",
+						"secondary_sop_uid": "",
+						"study_id": "251013110602828",
+						"view_id": "View_Dog_PV_VD_01",
+						"view_description": "骨盆伸腿腹背位",
+						"patient_type": "Dog",
+						"body_part_id": "Dog_Hip",
+						"anatomic_region": "Pelvis",
+						"image_type": "expose",
+						"image_file_path": "",
+						"image_file": "2.25.156.999999.0000.1.4.8323328.289656.1760324763.914280.dcm",
+						"thumbnail_file": "2.25.156.999999.0000.1.4.8323328.289656.1760324763.914280.webp",
+						"acquisition_mode": "RAD",
+						"acquisition_context": null,
+						"img_proc_context": null,
+						"comment": "task_version: 0.5.0-7-g3a9cf08",
+						"expose_status": "Exposed",
+						"expose_time": "2025-10-13T03:31:34.368028Z",
+						"judged_status": "Accept",
+						"send_status": "Unsent",
+						"export_status": "NotExported",
+						"storage_status": "Saved",
+						"raw_exists": false,
+						"ticket": "",
+						"sort": 3,
+						"product": "VETDROS",
+						"is_pre_install": true,
+						"create_time": "2025-10-13T03:06:03.025481Z"
+					}
+				]
+			}
+		]
+	}
+}
+```
+
+
+## 17.4	(回收站)恢复条目(批量)
+
+> POST  /dr/api/v1/auth/recycle_bin/study
+### 接口说明
+> (回收站)恢复条目(批量)
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTEyNzc5NzAsImlkIjoxLCJuYW1lIjoiYWRtaW4ifQ.ooTGwBXaNhtunbKbpqteWbjDwJLjnRmSIl80r5dp1pY||
+|Language||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/google.protobuf.Empty",
+		"value": {}
+	}
+}
+```
+请求body示例
+```
+[
+	"20250623110433242",
+	"20250623110436888"
+]
+```
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/15/2M8rOxgZH8a/5b7198bb993a403fbff0dabb9ad4419a)
+
+## 17.5	(回收站)删除条目(批量)
+
+> DELETE  /dr/api/v1/auth/recycle_bin/study
+### 接口说明
+> 删除回收站条目(批量)
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTEyNzc5NzAsImlkIjoxLCJuYW1lIjoiYWRtaW4ifQ.ooTGwBXaNhtunbKbpqteWbjDwJLjnRmSIl80r5dp1pY||
+|Language||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/google.protobuf.Empty",
+		"value": {}
+	}
+}
+```
+请求body示例
+```
+[
+	"20250623110433242",
+	"20250623110436888"
+]
+```
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/15/2M8r6DDnFTM/06f2d062e5da4f61beb6b55754e8539e)
+
+## 17.6	清空回收站条目
+
+> DELETE  /dr/api/v1/auth/recycle_bin/clear
+### 接口说明
+> 清空回收站条目
+### 请求头
+| 参数名称 | 默认值 | 描述 |
+| ------ | ------ | ------ |
+|Authorization|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTEyNzc5NzAsImlkIjoxLCJuYW1lIjoiYWRtaW4ifQ.ooTGwBXaNhtunbKbpqteWbjDwJLjnRmSIl80r5dp1pY||
+|Language||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/google.protobuf.Empty",
+		"value": {}
+	}
+}
+```
+请求body示例
+![  ](http://f1-xyj.fangdeco.cn/attachment/2025/10/15/2M8rAXoaHSK/60d74d30133e498c94a386e6d42657f9)
+
+
+## 18	登记检查信息
 
 > POST  /dr/api/v1/auth/study
 ### 接口说明
@@ -2600,7 +3123,7 @@ zh_CN
 ```
 
 
-## 18	变更登记信息
+## 19	变更登记信息
 
 > PUT  /dr/api/v1/auth/study/{id}
 ### 接口说明
@@ -2795,7 +3318,7 @@ zh_CN
 ```
 
 
-## 19	锁定检查信息(不可删除)
+## 20	锁定检查信息(不可删除)
 
 > PUT  /dr/api/v1/auth/study/{id}/lock
 ### 接口说明
@@ -2829,7 +3352,7 @@ lock可选项
 Locked
 Unlocked
 
-## 20	获取检查信息列表
+## 21	获取检查信息列表
 
 > GET  /dr/api/v1/auth/study/
 ### 接口说明
@@ -2993,7 +3516,7 @@ Completed|
 ```
 
 
-## 21	获取检查信息
+## 22	获取检查信息
 
 > GET  /dr/api/v1/auth/study/{id}
 ### 接口说明
@@ -3227,7 +3750,7 @@ Completed|
 ```
 
 
-## 22	获取检查信息状态
+## 23	获取检查信息状态
 
 > GET  /dr/api/v1/auth/study/{id}/stat
 ### 接口说明
@@ -3259,7 +3782,7 @@ Completed|
 ```
 
 
-## 23	删除检查信息(批量)
+## 24	删除检查信息(批量)
 
 > DELETE  /dr/api/v1/auth/study
 ### 接口说明
@@ -3294,7 +3817,7 @@ Completed|
 ```
 
 
-## 24	[Study]存储拍摄的急诊患者影像
+## 25	[Study]存储拍摄的急诊患者影像
 
 > POST  /api/v1/auth/study/portrait
 ### 接口说明
@@ -3330,7 +3853,7 @@ Completed|
 ```
 
 
-## 25	[Study]批量添加体位和协议
+## 26	[Study]批量添加体位和协议
 
 > POST  /dr/api/v1/auth/image
 ### 接口说明
@@ -3445,7 +3968,7 @@ Completed|
 ```
 
 
-## 26	[Study]复制体位
+## 27	[Study]复制体位
 
 > POST  /dr/api/v1/auth/image/copy
 ### 接口说明
@@ -3509,7 +4032,7 @@ Completed|
 ```
 
 
-## 27	[Study]体位重新排序
+## 28	[Study]体位重新排序
 
 > POST  /dr/api/v1/auth/image/sort
 ### 接口说明
@@ -3559,7 +4082,7 @@ Completed|
 ```
 
 
-## 28	[Study]删除体位
+## 29	[Study]删除体位
 
 > DELETE  /dr/api/v1/auth/image/:id
 ### 接口说明
@@ -3587,7 +4110,7 @@ Completed|
 示例请求
 DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.1749539018.668478
 
-## 29	[Study]存储后处理dcm
+## 30	[Study]存储后处理dcm
 
 > POST  /api/v1/auth/image/post_proc
 ### 接口说明
@@ -3621,7 +4144,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 30	报告预览
+## 31	报告预览
 
 > POST  /dr/api/v1/auth/report/preview
 ### 接口说明
@@ -3679,7 +4202,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)
 
-## 31	保存报告
+## 32	保存报告
 
 > POST  /dr/api/v1/auth/study/{id}/report
 ### 接口说明
@@ -3745,7 +4268,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)
 
-## 32	获取报告
+## 33	获取报告
 
 > GET  /dr/api/v1/auth/study/{id}/report
 ### 接口说明
@@ -3766,7 +4289,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 请求示例
 /api/v1/auth/study/20250912063732016/report
 
-## 33	[Device]打开设备
+## 34	[Device]打开设备
 
 > POST  /api/v1/auth/device/open
 ### 接口说明
@@ -3797,7 +4320,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 34	[Device]执行Get
+## 35	[Device]执行Get
 
 > POST  /api/v1/auth/device/get
 ### 接口说明
@@ -3829,7 +4352,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 35	[Device]执行Action
+## 36	[Device]执行Action
 
 > POST  /api/v1/auth/device/action
 ### 接口说明
@@ -3864,7 +4387,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 36	[Task]开始检查
+## 37	[Task]开始检查
 
 > POST  /api/v1/auth/task/inspection/start
 ### 接口说明
@@ -3899,7 +4422,7 @@ DELETE {{drurl}}/api/v1/auth/image/1.2.276.0.1000000.5.1.4.701601461.19649.17495
 ```
 
 
-## 37	[Task]获取全局状态
+## 38	[Task]获取全局状态
 
 > GET  /api/v1/auth/task/inspection/status
 ### 接口说明
@@ -3946,7 +4469,7 @@ Topic: MODULE/TASK/STATUS/GLOBAL
 ```
 
 
-## 38	[Task]软曝光(包含状态推送说明)
+## 39	[Task]软曝光(包含状态推送说明)
 
 > POST  /api/v1/auth/task/inspection/trigger
 ### 接口说明
@@ -4006,7 +4529,7 @@ Topic: MODULE/TASK/STATUS/GLOBAL
 ```
 
 
-## 39	[Task]接受拒绝图像
+## 40	[Task]接受拒绝图像
 
 > POST  /api/v1/auth/task/inspection/judge
 ### 接口说明
@@ -4042,7 +4565,7 @@ Topic: MODULE/TASK/STATUS/GLOBAL
 ```
 
 
-## 40	[Task]挂起或完成study
+## 41	[Task]挂起或完成study
 
 > POST  /api/v1/auth/task/inspection/leave
 ### 接口说明

+ 798 - 0
docs/实现/回收站功能.md

@@ -0,0 +1,798 @@
+# 回收站功能实现文档
+
+## 📋 一、现状分析
+
+### 已实现部分
+通过代码分析,发现回收站功能已有基础框架:
+
+✅ **UI组件**
+- `Bin.tsx` - 回收站主页面
+- `BinOperationPanel.tsx` - 操作面板容器
+- `BinActionPanel.tsx` - 磁盘信息和操作按钮
+
+⚠️ **待完善部分**
+- 缺少专门的状态管理(binSlice)
+- 缺少API接口实现
+- 过滤功能未实现
+- 操作逻辑为空(TODO注释)
+
+## 🎯 二、参与者清单
+
+### 1. 前端组件层
+**页面组件**
+- `Bin.tsx` - 回收站主页面容器
+- `BinOperationPanel.tsx` - 操作面板容器
+
+**UI组件**
+- `WorklistTable` - 表格展示组件(复用)
+- `BinActionPanel` - 操作按钮组件
+- `SearchPanel` - 搜索过滤组件(复用)
+- `GenericPagination` - 分页组件(复用)
+- `PatientPortraitFloat` - 患者照片浮窗(复用)
+
+### 2. 状态管理层
+**Redux Slices(需新建)**
+- `binEntitiesSlice` - 回收站数据实体
+- `binFiltersSlice` - 过滤条件
+- `binPaginationSlice` - 分页状态
+- `binSelectionSlice` - 选择状态
+- `binUISlice` - UI状态
+- `binDiskInfoSlice` - 磁盘信息状态
+
+**共享Slices(复用)**
+- `searchSlice` - 搜索条件管理
+- `thumbnailListSlice` - 缩略图列表
+
+### 3. API服务层(需新建)
+**文件**: `src/API/patient/binActions.ts`
+
+**函数清单**:
+- `fetchBinDiskInfo()` - 获取磁盘信息
+- `fetchBinList()` - 获取回收站列表
+- `fetchBinStudy()` - 获取单个检查详情
+- `restoreBinStudies()` - 恢复条目
+- `deleteBinStudies()` - 删除条目
+- `clearBin()` - 清空回收站
+
+### 4. 数据模型层
+**类型定义**(部分复用,部分新建)
+- `Task/Study` - 检查数据结构(复用)
+- `BinFilter` - 回收站过滤条件(新建)
+- `DiskInfo` - 磁盘信息(新建)
+
+## 🔄 三、交互流程
+
+### 用户操作流程
+```
+用户进入回收站
+    ↓
+加载数据(列表+磁盘信息)
+    ↓
+【可选】应用过滤条件 → 刷新列表
+    ↓
+【可选】选择条目
+    ↓
+执行操作:
+    ├─ 恢复选中 → API调用 → 刷新列表
+    ├─ 删除选中 → 确认对话框 → API调用 → 刷新列表
+    └─ 清空回收站 → 确认对话框 → API调用 → 刷新列表
+```
+
+### 数据流图
+
+```mermaid
+sequenceDiagram
+    participant U as 用户
+    participant C as Bin组件
+    participant S as Redux Store
+    participant A as API Service
+    participant B as 后端服务
+
+    U->>C: 进入回收站
+    C->>S: dispatch(fetchBinThunk)
+    S->>A: fetchBinList()
+    A->>B: GET /recycle_bin/study
+    B-->>A: 返回数据
+    A-->>S: 数据+total
+    S-->>C: 更新state
+    C->>S: dispatch(fetchDiskInfo)
+    S->>A: fetchBinDiskInfo()
+    A->>B: GET /recycle_bin/disk_info
+    B-->>A: 磁盘信息
+    A-->>S: 磁盘数据
+    S-->>C: 更新state
+    C-->>U: 显示列表+磁盘信息
+    
+    U->>C: 选择条目+点击恢复
+    C->>S: dispatch(restoreBinThunk)
+    S->>A: restoreBinStudies([ids])
+    A->>B: POST /recycle_bin/study
+    B-->>A: 成功响应
+    A-->>S: 操作结果
+    S->>S: 刷新列表
+    S-->>C: 更新state
+    C-->>U: 显示成功提示
+```
+
+## 📊 四、数据结构设计
+
+### 回收站状态结构
+```typescript
+// src/states/patient/bin/types/binFilter.ts
+export interface BinFilter {
+  id?: string;
+  name?: string;
+  acc_no?: string;
+  start_time?: string;
+  end_time?: string;
+  page?: number;
+  page_size?: number;
+}
+
+// 磁盘信息
+export interface DiskInfo {
+  total: number;          // 总容量(字节)
+  total_str: string;      // 格式化字符串
+  free: number;           // 剩余空间(字节)
+  free_str: string;       // 格式化字符串
+  recycle_bin: number;    // 回收站占用(字节)
+  recycle_bin_str: string; // 格式化字符串
+}
+
+// 磁盘信息状态
+export interface BinDiskInfoState {
+  data: DiskInfo | null;
+  loading: boolean;
+  error: string | null;
+}
+```
+
+### API数据结构
+```typescript
+// src/API/patient/binActions.ts
+
+// 磁盘信息响应
+export interface DiskInfoResponse {
+  code: string;
+  description: string;
+  solution: string;
+  data: DiskInfo;
+}
+
+// 回收站列表响应
+export interface BinListResponse {
+  code: string;
+  description: string;
+  solution: string;
+  data: {
+    '@type': string;
+    count: number;
+    studies: Study[];
+  };
+}
+```
+
+## 📝 五、实现待办清单
+
+### 阶段一:API层实现 ✅
+- [x] 创建 `src/API/patient/binActions.ts`
+- [x] 实现 `fetchBinDiskInfo()` - 获取磁盘信息
+- [x] 实现 `fetchBinList()` - 获取回收站列表
+- [x] 实现 `fetchBinStudy()` - 获取检查详情
+- [x] 实现 `restoreBinStudies()` - 批量恢复
+- [x] 实现 `deleteBinStudies()` - 批量删除
+- [x] 实现 `clearBin()` - 清空回收站
+
+### 阶段二:状态管理实现 ✅
+- [x] 创建 `src/states/patient/bin/` 目录
+- [x] 创建类型定义 `types/binFilter.ts`
+- [x] 创建 `slices/binSlice.ts` - 回收站主slice
+- [x] 创建 `slices/binDiskInfoSlice.ts` - 磁盘信息slice
+- [x] 实现fetchBinThunk - 数据获取
+- [x] 实现restoreBinThunk - 恢复操作
+- [x] 实现deleteBinThunk - 删除操作
+- [x] 实现clearBinThunk - 清空操作
+- [x] 在store中注册bin相关reducers
+
+### 阶段三:UI组件完善 ✅
+- [x] 修改 `Bin.tsx` - 接入状态管理
+- [x] 修改 `BinOperationPanel.tsx` - 接入磁盘信息
+- [x] 修改 `BinActionPanel.tsx` - 实现操作逻辑
+- [x] 添加确认对话框
+- [x] 添加操作成功/失败提示
+- [x] 实现过滤功能
+- [x] 实现分页功能
+- [x] 实现表格行选择
+- [x] 添加患者照片显示功能
+
+### 阶段四:测试与优化
+- [ ] 编写组件单元测试
+- [ ] 编写集成测试
+- [ ] 测试边界情况
+- [ ] 性能优化
+- [ ] 代码审查
+
+## 🔍 六、执行流程详解
+
+### 1. 初始化流程
+```
+用户点击"回收站"标签
+    ↓
+Bin组件挂载
+    ↓
+useEffect触发
+    ↓
+并行执行:
+├─ dispatch(fetchBinThunk()) → 获取列表数据
+└─ dispatch(fetchDiskInfoThunk()) → 获取磁盘信息
+    ↓
+Redux更新state
+    ↓
+组件重新渲染
+    ↓
+显示表格+磁盘信息+操作按钮
+```
+
+### 2. 过滤流程
+```
+用户输入过滤条件
+    ↓
+SearchPanel组件更新
+    ↓
+dispatch(setFilters())
+    ↓
+useEffect监听filters变化
+    ↓
+dispatch(fetchBinThunk(filters))
+    ↓
+重新获取数据
+    ↓
+更新表格显示
+```
+
+### 3. 恢复流程
+```
+用户选择条目
+    ↓
+dispatch(setSelectedIds())
+    ↓
+用户点击"恢复"按钮
+    ↓
+显示确认对话框
+    ↓
+用户确认
+    ↓
+dispatch(restoreBinThunk(selectedIds))
+    ↓
+调用API: POST /recycle_bin/study
+    ↓
+成功后:
+├─ 显示成功提示
+├─ 清空选择
+└─ 刷新列表和磁盘信息
+```
+
+### 4. 删除流程
+```
+用户选择条目
+    ↓
+用户点击"删除"按钮
+    ↓
+显示确认对话框(警告:永久删除)
+    ↓
+用户确认
+    ↓
+dispatch(deleteBinThunk(selectedIds))
+    ↓
+调用API: DELETE /recycle_bin/study
+    ↓
+成功后:
+├─ 显示成功提示
+├─ 清空选择
+└─ 刷新列表和磁盘信息
+```
+
+### 5. 清空流程
+```
+用户点击"清空"按钮
+    ↓
+显示确认对话框(警告:清空所有)
+    ↓
+用户确认
+    ↓
+dispatch(clearBinThunk())
+    ↓
+调用API: DELETE /recycle_bin/clear
+    ↓
+成功后:
+├─ 显示成功提示
+└─ 刷新列表和磁盘信息(应为空)
+```
+
+## 🧪 七、测试方案
+
+### 功能测试场景
+
+#### 1. 数据加载测试
+- ✅ 进入回收站页面,验证列表数据正确加载
+- ✅ 验证磁盘信息正确显示
+- ✅ 验证空状态时的UI显示
+- ✅ 验证加载失败时的错误提示
+
+#### 2. 过滤功能测试
+- ✅ 按患者ID过滤
+- ✅ 按患者姓名过滤
+- ✅ 按登记号过滤
+- ✅ 按时间范围过滤
+- ✅ 组合条件过滤
+- ✅ 清空过滤条件
+
+#### 3. 选择功能测试
+- ✅ 单行选择
+- ✅ 多行选择
+- ✅ 全选
+- ✅ 取消选择
+- ✅ 未选择时操作按钮禁用
+
+#### 4. 恢复功能测试
+- ✅ 恢复单个条目
+- ✅ 批量恢复多个条目
+- ✅ 恢复成功后列表更新
+- ✅ 恢复失败时错误提示
+- ✅ 取消恢复操作
+
+#### 5. 删除功能测试
+- ✅ 删除单个条目
+- ✅ 批量删除多个条目
+- ✅ 删除成功后列表更新
+- ✅ 删除失败时错误提示
+- ✅ 取消删除操作
+- ✅ 确认对话框正确显示警告信息
+
+#### 6. 清空功能测试
+- ✅ 清空回收站
+- ✅ 清空后列表为空
+- ✅ 清空后磁盘信息更新
+- ✅ 清空失败时错误提示
+- ✅ 取消清空操作
+- ✅ 确认对话框正确显示警告信息
+
+#### 7. 分页测试
+- ✅ 切换页码
+- ✅ 修改每页条数
+- ✅ 分页信息正确显示
+- ✅ 跳转到指定页
+
+#### 8. 响应式测试
+- ✅ 桌面端布局正确
+- ✅ 移动端布局正确
+- ✅ 抽屉菜单功能正常
+
+## 🐛 八、潜在问题分析
+
+### 边界情况
+
+#### 1. 空数据状态
+**问题**:回收站为空时的UI处理
+**解决方案**:
+- 显示空状态提示组件
+- 禁用清空按钮
+- 保持磁盘信息显示
+
+#### 2. 大量数据
+**问题**:回收站包含大量条目时的性能
+**解决方案**:
+- 实现虚拟滚动
+- 优化分页加载
+- 添加加载骨架屏
+
+#### 3. 并发操作
+**问题**:用户快速连续点击操作按钮
+**解决方案**:
+- 操作进行时禁用按钮
+- 使用loading状态管理
+- 防抖处理
+
+#### 4. 网络异常
+**问题**:API调用失败的处理
+**解决方案**:
+- 显示错误提示
+- 提供重试机制
+- 保持页面状态不崩溃
+
+### 异常处理策略
+
+#### 1. API错误处理
+```typescript
+try {
+  const result = await binAPI.restore(ids);
+  if (result.code !== '0x000000') {
+    message.error(result.description);
+    return;
+  }
+  message.success('恢复成功');
+  dispatch(fetchBinThunk());
+} catch (error) {
+  message.error('操作失败,请重试');
+  console.error('Restore error:', error);
+}
+```
+
+#### 2. 状态同步问题
+**问题**:操作后列表与实际状态不一致
+**解决方案**:
+- 操作成功后立即刷新列表
+- 使用乐观更新+回滚机制
+- 添加手动刷新按钮
+
+#### 3. 选择状态管理
+**问题**:删除后选中的ID仍存在
+**解决方案**:
+- 操作成功后清空选择
+- 过滤无效的选中ID
+- 同步更新相关UI状态
+
+## 📐 九、架构图
+
+### 组件关系图
+```mermaid
+graph TD
+    A[PatientManagement] --> B[Bin.tsx]
+    B --> C[WorklistTable]
+    B --> D[BinOperationPanel]
+    B --> E[GenericPagination]
+    B --> F[PatientPortraitFloat]
+    D --> G[SearchPanel]
+    D --> H[BinActionPanel]
+    
+    B -.Redux.-> I[binEntitiesSlice]
+    B -.Redux.-> J[binFiltersSlice]
+    B -.Redux.-> K[binPaginationSlice]
+    B -.Redux.-> L[binSelectionSlice]
+    D -.Redux.-> M[binDiskInfoSlice]
+    
+    I -.API.-> N[binActions.ts]
+    M -.API.-> N
+    N -.HTTP.-> O[Backend API]
+```
+
+### 类图
+```mermaid
+classDiagram
+    class BinPage {
+        +diskInfo: DiskInfo
+        +binData: Study[]
+        +filters: BinFilter
+        +selectedIds: string[]
+        +handleRowClick()
+        +handleRestore()
+        +handleDelete()
+        +handleClear()
+    }
+    
+    class BinActionPanel {
+        +totalCapacity: number
+        +freeSpace: number
+        +binCapacity: number
+        +onDelete()
+        +onRestore()
+        +onEmpty()
+    }
+    
+    class BinSlice {
+        +entities: Study[]
+        +filters: BinFilter
+        +pagination: Pagination
+        +selection: Selection
+        +fetchBinThunk()
+        +restoreBinThunk()
+        +deleteBinThunk()
+        +clearBinThunk()
+    }
+    
+    class BinActions {
+        +fetchBinDiskInfo()
+        +fetchBinList()
+        +restoreBinStudies()
+        +deleteBinStudies()
+        +clearBin()
+    }
+    
+    BinPage --> BinActionPanel
+    BinPage --> BinSlice
+    BinSlice --> BinActions
+```
+
+## 📌 十、关键代码示例
+
+### 1. API实现
+```typescript
+// src/API/patient/binActions.ts
+export const fetchBinDiskInfo = async (): Promise<DiskInfoResponse> => {
+  const response = await axiosInstance.get('/auth/recycle_bin/disk_info');
+  return response.data;
+};
+
+export const fetchBinList = async (
+  page: number,
+  pageSize: number,
+  filter: BinFilter
+): Promise<{ items: Study[]; total: number }> => {
+  const response = await axiosInstance.get('/auth/recycle_bin/study', {
+    params: {
+      page,
+      page_size: pageSize,
+      id: filter.id,
+      name: filter.name,
+      acc_no: filter.acc_no,
+      start_time: filter.start_time,
+      end_time: filter.end_time,
+    },
+  });
+  
+  const { studies, count } = response.data.data;
+  return { items: studies, total: count };
+};
+
+export const restoreBinStudies = async (studyIds: string[]): Promise<void> => {
+  const response = await axiosInstance.post('/auth/recycle_bin/study', studyIds);
+  if (response.data.code !== '0x000000') {
+    throw new Error(response.data.description);
+  }
+};
+
+export const deleteBinStudies = async (studyIds: string[]): Promise<void> => {
+  const response = await axiosInstance.delete('/auth/recycle_bin/study', {
+    data: studyIds,
+  });
+  if (response.data.code !== '0x000000') {
+    throw new Error(response.data.description);
+  }
+};
+
+export const clearBin = async (): Promise<void> => {
+  const response = await axiosInstance.delete('/auth/recycle_bin/clear');
+  if (response.data.code !== '0x000000') {
+    throw new Error(response.data.description);
+  }
+};
+```
+
+### 2. Slice实现
+```typescript
+// src/states/patient/bin/slices/binSlice.ts
+export const fetchBinThunk = createFetchThunk<BinFilter, Study>(
+  'bin',
+  async ({ page, pageSize, filters }) => {
+    const { items, total } = await fetchBinList(page, pageSize, filters);
+    return { data: items, total };
+  }
+);
+
+export const restoreBinThunk = createAsyncThunk(
+  'bin/restore',
+  async (ids: string[], { dispatch }) => {
+    await restoreBinStudies(ids);
+    dispatch(selectionSlice.actions.clearSelection());
+    dispatch(fetchBinThunk());
+    dispatch(fetchDiskInfoThunk());
+  }
+);
+
+export const deleteBinThunk = createAsyncThunk(
+  'bin/delete',
+  async (ids: string[], { dispatch }) => {
+    await deleteBinStudies(ids);
+    dispatch(selectionSlice.actions.clearSelection());
+    dispatch(fetchBinThunk());
+    dispatch(fetchDiskInfoThunk());
+  }
+);
+
+export const clearBinThunk = createAsyncThunk(
+  'bin/clear',
+  async (_, { dispatch }) => {
+    await clearBin();
+    dispatch(selectionSlice.actions.clearSelection());
+    dispatch(fetchBinThunk());
+    dispatch(fetchDiskInfoThunk());
+  }
+);
+```
+
+### 3. 组件实现
+```typescript
+// src/pages/patient/Bin.tsx 关键部分
+const BinPage: React.FC = () => {
+  const dispatch = useAppDispatch();
+  const binData = useSelector((state: RootState) => state.binEntities.data);
+  const selectedIds = useSelector((state: RootState) => state.binSelection.selectedIds);
+  const diskInfo = useSelector((state: RootState) => state.binDiskInfo.data);
+  
+  useEffect(() => {
+    dispatch(fetchBinThunk());
+    dispatch(fetchDiskInfoThunk());
+  }, []);
+  
+  const handleRestore = () => {
+    if (selectedIds.length === 0) {
+      message.warning('请先选择要恢复的条目');
+      return;
+    }
+    
+    Modal.confirm({
+      title: '确认恢复',
+      content: `确定要恢复选中的 ${selectedIds.length} 个条目吗?`,
+      onOk: async () => {
+        try {
+          await dispatch(restoreBinThunk(selectedIds)).unwrap();
+          message.success('恢复成功');
+        } catch (error) {
+          message.error('恢复失败,请重试');
+        }
+      },
+    });
+  };
+  
+  const handleDelete = () => {
+    if (selectedIds.length === 0) {
+      message.warning('请先选择要删除的条目');
+      return;
+    }
+    
+    Modal.confirm({
+      title: '确认删除',
+      content: `确定要永久删除选中的 ${selectedIds.length} 个条目吗?此操作不可恢复!`,
+      okText: '确认删除',
+      okType: 'danger',
+      onOk: async () => {
+        try {
+          await dispatch(deleteBinThunk(selectedIds)).unwrap();
+          message.success('删除成功');
+        } catch (error) {
+          message.error('删除失败,请重试');
+        }
+      },
+    });
+  };
+  
+  const handleClear = () => {
+    Modal.confirm({
+      title: '确认清空回收站',
+      content: '确定要清空回收站吗?此操作将永久删除所有内容,不可恢复!',
+      okText: '确认清空',
+      okType: 'danger',
+      onOk: async () => {
+        try {
+          await dispatch(clearBinThunk()).unwrap();
+          message.success('清空成功');
+        } catch (error) {
+          message.error('清空失败,请重试');
+        }
+      },
+    });
+  };
+  
+  // ...其他代码
+};
+```
+
+## ✅ 十一、总结
+
+回收站功能实现完成了以下内容:
+
+### 新建文件
+1. **API层**:`src/API/patient/binActions.ts` - 完整的API接口实现
+2. **类型定义**:`src/states/patient/bin/types/binFilter.ts` - 过滤器类型
+3. **状态管理**:
+   - `src/states/patient/bin/slices/binSlice.ts` - 主slice
+   - `src/states/patient/bin/slices/binDiskInfoSlice.ts` - 磁盘信息slice
+
+### 修改文件
+1. **页面组件**:`src/pages/patient/Bin.tsx` - 完整接入状态管理
+2. **操作面板**:
+   - `src/pages/patient/components/BinOperationPanel.tsx` - 接入磁盘信息
+   - `src/pages/patient/components/BinActionPanel.tsx` - 实现操作逻辑
+3. **Store配置**:`src/states/store.ts` - 注册bin相关reducers
+
+### 复用组件
+- WorklistTable - 表格显示
+- SearchPanel - 搜索过滤
+- GenericPagination - 分页
+- PatientPortraitFloat - 患者照片
+
+### 核心功能
+✅ 数据加载(列表+磁盘信息)
+✅ 搜索过滤(ID、姓名、登记号、时间范围)
+✅ 分页展示
+✅ 行选择(单选、多选)
+✅ 恢复操作(批量)
+✅ 删除操作(批量,二次确认)
+✅ 清空回收站(二次确认)
+✅ 错误处理和用户提示
+
+### 技术特点
+- 参考worklist和historylist的实现模式
+- 使用统一的列表模板创建slices
+- 完善的错误处理和loading状态
+- 所有危险操作都有二次确认
+- 响应式设计支持移动端
+
+---
+
+## 🎉 十二、实现完成状态
+
+### ✅ 已完成的功能模块
+
+**API层** (100%)
+- ✅ fetchBinDiskInfo - 获取磁盘信息
+- ✅ fetchBinList - 获取回收站列表(支持过滤和分页)
+- ✅ fetchBinStudy - 获取单个检查详情
+- ✅ restoreBinStudies - 批量恢复条目
+- ✅ deleteBinStudies - 批量删除条目
+- ✅ clearBin - 清空回收站
+
+**状态管理层** (100%)
+- ✅ binEntitiesSlice - 实体数据管理
+- ✅ binFiltersSlice - 过滤条件管理
+- ✅ binPaginationSlice - 分页状态管理
+- ✅ binSelectionSlice - 选择状态管理
+- ✅ binUISlice - UI状态管理
+- ✅ binDiskInfoSlice - 磁盘信息管理
+- ✅ 所有thunk函数(fetch、restore、delete、clear)
+- ✅ 在store中注册所有reducers
+
+**UI组件层** (100%)
+- ✅ Bin.tsx - 主页面完整实现
+- ✅ BinOperationPanel.tsx - 操作面板实现
+- ✅ BinActionPanel.tsx - 操作按钮实现
+- ✅ 复用WorklistTable、SearchPanel等组件
+- ✅ 响应式布局(桌面/移动端)
+- ✅ 患者照片浮窗功能
+
+**交互逻辑** (100%)
+- ✅ 数据加载与刷新
+- ✅ 搜索过滤功能
+- ✅ 分页导航
+- ✅ 行选择交互
+- ✅ 恢复操作(含确认对话框)
+- ✅ 删除操作(含警告对话框)
+- ✅ 清空操作(含警告对话框)
+- ✅ 操作成功/失败提示
+- ✅ 错误处理机制
+
+### 📋 待测试项目
+
+**功能测试**
+- [ ] 端到端功能测试
+- [ ] 边界情况测试
+- [ ] 错误处理测试
+- [ ] 性能测试
+
+**集成测试**
+- [ ] 与worklist的交互测试
+- [ ] 与historylist的交互测试
+- [ ] API集成测试
+
+### 🚀 下一步工作
+
+1. **测试阶段**
+   - 编写单元测试
+   - 执行集成测试
+   - 用户验收测试
+
+2. **优化阶段**
+   - 性能优化
+   - 用户体验优化
+   - 代码审查
+
+3. **文档完善**
+   - 更新用户手册
+   - 编写开发者文档
+   - 记录已知问题
+
+---
+
+**实现时间**: 2025年10月15日  
+**实现状态**: ✅ 核心功能已完成,待测试验收  
+**技术栈**: React + Redux Toolkit + Ant Design + TypeScript

+ 282 - 0
src/API/patient/binActions.ts

@@ -0,0 +1,282 @@
+import axiosInstance from '../interceptor';
+import { Task } from '@/domain/work';
+import { FetchTaskListStudy } from './workActions';
+
+/**
+ * 回收站过滤条件
+ */
+export interface BinFilter {
+  id?: string;
+  name?: string;
+  acc_no?: string;
+  start_time?: string;
+  end_time?: string;
+  page?: number;
+  page_size?: number;
+}
+
+/**
+ * 磁盘信息
+ */
+export interface DiskInfo {
+  total: number;          // 总容量(字节)
+  total_str: string;      // 格式化字符串
+  free: number;           // 剩余空间(字节)
+  free_str: string;       // 格式化字符串
+  recycle_bin: number;    // 回收站占用(字节)
+  recycle_bin_str: string; // 格式化字符串
+}
+
+/**
+ * 磁盘信息响应
+ */
+export interface DiskInfoResponse {
+  code: string;
+  description: string;
+  solution: string;
+  data: DiskInfo;
+}
+
+/**
+ * 回收站列表响应
+ */
+export interface BinListResponse {
+  code: string;
+  description: string;
+  solution: string;
+  data: {
+    '@type': string;
+    count: number;
+    studies: FetchTaskListStudy[];
+  };
+}
+
+/**
+ * 通用响应接口
+ */
+interface CommonResponse {
+  code: string;
+  description: string;
+  solution: string;
+  data: Record<string, unknown>;
+}
+
+/**
+ * 将回收站Study映射到Task
+ */
+const mapBinStudyToTask = (study: FetchTaskListStudy): Task => ({
+  StudyInstanceUID: study.study_instance_uid,
+  StudyID: study.study_id,
+  SpecificCharacterSet: study.specific_character_set,
+  AccessionNumber: study.accession_number,
+  PatientID: study.patient_id,
+  PatientName: study.patient_name,
+  DisplayPatientName: study.patient_name,
+  PatientSize: study.patient_size,
+  PatientAge: study.patient_age,
+  PatientSex: study.patient_sex,
+  AdmittingTime: study.admitting_time ?? '',
+  RegSource: study.reg_source,
+  StudyStatus: study.study_status,
+  RequestedProcedureID: '',
+  PerformedProtocolCodeValue: '',
+  PerformedProtocolCodeMeaning: '',
+  PerformedProcedureStepID: '',
+  StudyDescription: study.study_description,
+  StudyStartDatetime: study.study_start_datetime ?? '',
+  ScheduledProcedureStepStartDate:
+    study.scheduled_procedure_step_start_date ?? '',
+  StudyLock: study.study_lock,
+  OperatorID: study.operator_name,
+  Modality: study.modality,
+  Views: [],
+  Thickness: study.thickness,
+  PatientType: study.patient_type,
+  StudyType: study.study_type,
+  QRCode: '',
+  IsExported: study.is_exported,
+  IsEdited: study.is_edited,
+  WorkRef: '',
+  IsAppended: study.is_appended,
+  CreationTime: study.create_time,
+  MappedStatus: study.mapped_status,
+  IsDelete: false,
+  patient_dob: study.patient_dob,
+  ref_physician: study.ref_physician,
+  weight: study.weight,
+  length: study.length,
+  comment: study.comment,
+  owner_name: study.owner_name,
+  chip_number: study.chip_number,
+  variety: study.variety,
+  pregnancy_status: study.pregnancy_status,
+  sex_neutered: study.sex_neutered,
+  is_anaesthesia: study.is_anaesthesia,
+  is_sedation: study.is_sedation,
+  portrait_status: study.portrait_status,
+  portrait_file: study.portrait_file,
+});
+
+/**
+ * 获取磁盘信息
+ */
+export const fetchBinDiskInfo = async (): Promise<DiskInfo> => {
+  try {
+    const response = await axiosInstance.get<DiskInfoResponse>(
+      '/auth/recycle_bin/disk_info'
+    );
+    
+    if (response.data.code !== '0x000000') {
+      console.error('获取磁盘信息失败:', response.data.description);
+      throw new Error(response.data.description);
+    }
+    
+    return response.data.data;
+  } catch (error) {
+    console.error('获取磁盘信息出错:', error);
+    throw error;
+  }
+};
+
+/**
+ * 获取回收站列表
+ */
+export const fetchBinList = async (
+  page: number,
+  pageSize: number,
+  filter: BinFilter
+): Promise<{ items: Task[]; total: number }> => {
+  try {
+    console.log(
+      `获取回收站列表 - page: ${page}, pageSize: ${pageSize}, filter:`,
+      filter
+    );
+    
+    const response = await axiosInstance.get<BinListResponse>(
+      '/auth/recycle_bin/study',
+      {
+        params: {
+          page,
+          page_size: pageSize,
+          id: filter.id || undefined,
+          name: filter.name || undefined,
+          acc_no: filter.acc_no || undefined,
+          start_time: filter.start_time || undefined,
+          end_time: filter.end_time || undefined,
+        },
+      }
+    );
+
+    if (response.data.code !== '0x000000') {
+      console.error('获取回收站列表失败:', response.data.description);
+      throw new Error(response.data.description);
+    }
+
+    const { studies, count } = response.data.data;
+    const tasks = studies.map(mapBinStudyToTask);
+    
+    console.log(`回收站列表获取成功 - 总数: ${count}, 当前页数量: ${tasks.length}`);
+    
+    return { items: tasks, total: count };
+  } catch (error) {
+    console.error('获取回收站列表出错:', error);
+    throw error;
+  }
+};
+
+/**
+ * 获取单个检查详情(从回收站)
+ */
+export const fetchBinStudy = async (studyId: string): Promise<Task> => {
+  try {
+    const response = await axiosInstance.get<{
+      code: string;
+      description: string;
+      solution: string;
+      data: FetchTaskListStudy;
+    }>(`/auth/recycle_bin/study/${studyId}`);
+
+    if (response.data.code !== '0x000000') {
+      console.error('获取回收站检查详情失败:', response.data.description);
+      throw new Error(response.data.description);
+    }
+
+    return mapBinStudyToTask(response.data.data);
+  } catch (error) {
+    console.error('获取回收站检查详情出错:', error);
+    throw error;
+  }
+};
+
+/**
+ * 批量恢复回收站条目
+ */
+export const restoreBinStudies = async (studyIds: string[]): Promise<void> => {
+  try {
+    console.log('恢复回收站条目:', studyIds);
+    
+    const response = await axiosInstance.post<CommonResponse>(
+      '/auth/recycle_bin/study',
+      studyIds
+    );
+
+    if (response.data.code !== '0x000000') {
+      console.error('恢复回收站条目失败:', response.data.description);
+      throw new Error(response.data.description);
+    }
+    
+    console.log('恢复回收站条目成功');
+  } catch (error) {
+    console.error('恢复回收站条目出错:', error);
+    throw error;
+  }
+};
+
+/**
+ * 批量删除回收站条目(永久删除)
+ */
+export const deleteBinStudies = async (studyIds: string[]): Promise<void> => {
+  try {
+    console.log('删除回收站条目:', studyIds);
+    
+    const response = await axiosInstance.delete<CommonResponse>(
+      '/auth/recycle_bin/study',
+      {
+        data: studyIds,
+      }
+    );
+
+    if (response.data.code !== '0x000000') {
+      console.error('删除回收站条目失败:', response.data.description);
+      throw new Error(response.data.description);
+    }
+    
+    console.log('删除回收站条目成功');
+  } catch (error) {
+    console.error('删除回收站条目出错:', error);
+    throw error;
+  }
+};
+
+/**
+ * 清空回收站
+ */
+export const clearBin = async (): Promise<void> => {
+  try {
+    console.log('清空回收站');
+    
+    const response = await axiosInstance.delete<CommonResponse>(
+      '/auth/recycle_bin/clear'
+    );
+
+    if (response.data.code !== '0x000000') {
+      console.error('清空回收站失败:', response.data.description);
+      throw new Error(response.data.description);
+    }
+    
+    console.log('清空回收站成功');
+  } catch (error) {
+    console.error('清空回收站出错:', error);
+    throw error;
+  }
+};

+ 112 - 11
src/pages/patient/Bin.tsx

@@ -1,21 +1,100 @@
-import React, { useState } from 'react';
-import { Row, Col, Button, Drawer, Grid, Pagination } from 'antd';
+import React, { useState, useEffect } from 'react';
+import { Row, Col, Button, Drawer, Grid } from 'antd';
 import { SettingOutlined } from '@ant-design/icons';
 import { FormattedMessage } from 'react-intl';
 import WorklistTable from './components/WorklistTable';
 import BinOperationPanel from './components/BinOperationPanel';
+import GenericPagination from '../../components/GenericPagination';
+import PatientPortraitFloat from './components/PatientPortraitFloat';
+import { useAppDispatch, useAppSelector } from '../../states/store';
+import {
+  fetchBinThunk,
+  binSelectionSlice,
+  binPaginationSlice,
+  binFiltersSlice,
+} from '../../states/patient/bin/slices/binSlice';
+import { fetchDiskInfoThunk } from '../../states/patient/bin/slices/binDiskInfoSlice';
+import { Task } from '@/domain/work';
 
 const { useBreakpoint } = Grid;
 
 const BinPage: React.FC = () => {
   const screens = useBreakpoint();
   const [drawerVisible, setDrawerVisible] = useState(false);
+  const [selectedPatientForPortrait, setSelectedPatientForPortrait] =
+    useState<Task | null>(null);
+  const dispatch = useAppDispatch();
+
+  // 从Redux获取状态
+  const binData = useAppSelector((state) => state.binEntities.data);
+  const page = useAppSelector((state) => state.binPagination.page);
+  const pageSize = useAppSelector((state) => state.binPagination.pageSize);
+  const filters = useAppSelector((state) => state.binFilters);
+  const selectedIds = useAppSelector((state) => state.binSelection.selectedIds);
+
+  // 初始加载数据
+  useEffect(() => {
+    console.log('Bin页面加载,获取回收站数据');
+    dispatch(fetchBinThunk({ page, pageSize, filters }));
+    dispatch(fetchDiskInfoThunk());
+  }, []);
+
+  // 监听filters和分页变化,重新获取数据
+  useEffect(() => {
+    console.log('回收站过滤条件或分页变化,重新获取数据');
+    dispatch(fetchBinThunk({ page, pageSize, filters }));
+  }, [filters, page, pageSize, dispatch]);
+
+  // 同步分页到filters
+  useEffect(() => {
+    dispatch(
+      binFiltersSlice.actions.setFilters({
+        page,
+        page_size: pageSize,
+      })
+    );
+  }, [dispatch, page, pageSize]);
+
+  // 处理行点击事件
+  const handleRowClick = (record: Task) => {
+    const studyId = record.StudyID;
+    const newSelectedIds = [studyId];
+
+    // 设置选中患者用于显示照片
+    setSelectedPatientForPortrait(record);
+
+    // 更新 Redux 状态
+    dispatch(binSelectionSlice.actions.setSelectedIds(newSelectedIds));
+  };
 
   return (
     <div className="h-full">
+      {/* 患者照片浮动组件 */}
+      <PatientPortraitFloat
+        patient={selectedPatientForPortrait}
+        onClose={() => setSelectedPatientForPortrait(null)}
+      />
+
       {screens.xs ? (
         <>
-          <WorklistTable />
+          <div className="flex-1 overflow-auto">
+            <WorklistTable
+              columnConfig={[]}
+              worklistData={binData}
+              filters={filters}
+              page={page}
+              pageSize={pageSize}
+              selectedIds={selectedIds}
+              handleRowClick={handleRowClick}
+              handleRowDoubleClick={() => {}}
+            />
+          </div>
+          <GenericPagination
+            paginationSelector={(state) => state.binPagination}
+            entitiesSelector={(state) => state.binEntities}
+            paginationActions={binPaginationSlice.actions}
+            className="border-t"
+          />
           <Button
             type="primary"
             shape="circle"
@@ -26,8 +105,8 @@ const BinPage: React.FC = () => {
           <Drawer
             title={
               <FormattedMessage
-                id="worklist.operationPanel"
-                defaultMessage="worklist.operationPanel"
+                id="bin.operationPanel"
+                defaultMessage="Operation Panel"
               />
             }
             placement="left"
@@ -39,14 +118,36 @@ const BinPage: React.FC = () => {
           </Drawer>
         </>
       ) : (
-        <Row gutter={16} className="h-full">
-          <Col span={screens.lg ? 18 : screens.md ? 20 : 24}>
-            <WorklistTable />
-            <div className="flex justify-center mt-4">
-              <Pagination defaultCurrent={1} total={50} />
+        <Row className="h-full">
+          <Col
+            span={screens.lg ? 18 : screens.md ? 20 : 24}
+            className="h-full flex flex-col"
+          >
+            <div className="flex-1 flex flex-col">
+              <div className="flex-1 overflow-auto">
+                <WorklistTable
+                  columnConfig={[]}
+                  worklistData={binData}
+                  filters={filters}
+                  page={page}
+                  pageSize={pageSize}
+                  selectedIds={selectedIds}
+                  handleRowClick={handleRowClick}
+                  handleRowDoubleClick={() => {}}
+                />
+              </div>
+              <GenericPagination
+                paginationSelector={(state) => state.binPagination}
+                entitiesSelector={(state) => state.binEntities}
+                paginationActions={binPaginationSlice.actions}
+                className="border-t"
+              />
             </div>
           </Col>
-          <Col span={screens.lg ? 6 : screens.md ? 4 : 0} className="h-full">
+          <Col
+            span={screens.lg ? 6 : screens.md ? 4 : 0}
+            className="h-full overflow-auto"
+          >
             <BinOperationPanel />
           </Col>
         </Row>

+ 19 - 14
src/pages/patient/components/BinActionPanel.tsx

@@ -6,9 +6,12 @@ import Icon from '@/components/Icon';
 const { Text } = Typography;
 
 interface BinActionPanelProps {
-  totalCapacity: number; // 总容量,单位GB
-  freeSpace: number; // 剩余空间,单位GB
-  binCapacity: number; // 回收站容量,单位GB
+  totalCapacity: number; // 总容量,单位字节
+  freeSpace: number; // 剩余空间,单位字节
+  binCapacity: number; // 回收站容量,单位字节
+  totalCapacityStr: string; // 总容量格式化字符串
+  freeSpaceStr: string; // 剩余空间格式化字符串
+  binCapacityStr: string; // 回收站容量格式化字符串
   onDelete: () => void;
   onRestore: () => void;
   onEmpty: () => void;
@@ -18,13 +21,18 @@ const BinActionPanel: React.FC<BinActionPanelProps> = ({
   totalCapacity,
   freeSpace,
   binCapacity,
+  totalCapacityStr,
+  freeSpaceStr,
+  binCapacityStr,
   onDelete,
   onRestore,
   onEmpty,
 }) => {
   const usedSpace = totalCapacity - freeSpace;
-  const usedPercent = Math.round((usedSpace / totalCapacity) * 100);
-  const binPercent = Math.round((binCapacity / totalCapacity) * 100);
+  const usedPercent =
+    totalCapacity > 0 ? Math.round((usedSpace / totalCapacity) * 100) : 0;
+  const binPercent =
+    totalCapacity > 0 ? Math.round((binCapacity / totalCapacity) * 100) : 0;
 
   // 使用Card组件和theme.token适配多主题
   return (
@@ -38,29 +46,26 @@ const BinActionPanel: React.FC<BinActionPanelProps> = ({
           <Text strong>
             <FormattedMessage
               id="bin.diskCapacity"
-              defaultMessage="bin.diskCapacity"
+              defaultMessage="磁盘容量:"
             />
           </Text>
-          <Text type="secondary">{totalCapacity} GB</Text>
+          <Text type="secondary">{totalCapacityStr || '0.00 KB'}</Text>
           <Progress percent={usedPercent} size="small" showInfo={false} />
         </Space>
         <Space direction="vertical" size={4} style={{ width: '100%' }}>
           <Text strong>
-            <FormattedMessage
-              id="bin.freeSpace"
-              defaultMessage="bin.freeSpace"
-            />
+            <FormattedMessage id="bin.freeSpace" defaultMessage="剩余空间:" />
           </Text>
-          <Text type="secondary">{freeSpace} GB</Text>
+          <Text type="secondary">{freeSpaceStr || '0.00 KB'}</Text>
         </Space>
         <Space direction="vertical" size={4} style={{ width: '100%' }}>
           <Text strong>
             <FormattedMessage
               id="bin.binCapacity"
-              defaultMessage="bin.binCapacity"
+              defaultMessage="回收站容量:"
             />
           </Text>
-          <Text type="secondary">{binCapacity} GB</Text>
+          <Text type="secondary">{binCapacityStr || '0.00 KB'}</Text>
           <Progress
             percent={binPercent}
             size="small"

+ 104 - 12
src/pages/patient/components/BinOperationPanel.tsx

@@ -1,25 +1,117 @@
 import React from 'react';
+import { Modal, message } from 'antd';
 import SearchPanel from './SearchPanel';
 import BinActionPanel from './BinActionPanel';
+import { useAppDispatch, useAppSelector } from '../../../states/store';
+import {
+  restoreBinThunk,
+  deleteBinThunk,
+  clearBinThunk,
+} from '../../../states/patient/bin/slices/binSlice';
+import { fetchDiskInfoThunk } from '../../../states/patient/bin/slices/binDiskInfoSlice';
 
 const BinOperationPanel: React.FC = () => {
+  const dispatch = useAppDispatch();
+
+  // 从Redux获取状态
+  const diskInfo = useAppSelector((state) => state.binDiskInfo.data);
+  const selectedIds = useAppSelector((state) => state.binSelection.selectedIds);
+  const binData = useAppSelector((state) => state.binEntities.data);
+
+  // 恢复选中条目
+  const handleRestore = () => {
+    if (selectedIds.length === 0) {
+      message.warning('请先选择要恢复的条目');
+      return;
+    }
+
+    Modal.confirm({
+      title: '确认恢复',
+      content: `确定要恢复选中的 ${selectedIds.length} 个条目吗?`,
+      okText: '确认恢复',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await dispatch(restoreBinThunk(selectedIds)).unwrap();
+          message.success('恢复成功');
+          // 刷新磁盘信息
+          dispatch(fetchDiskInfoThunk());
+        } catch (error) {
+          message.error('恢复失败,请重试');
+          console.error('恢复失败:', error);
+        }
+      },
+    });
+  };
+
+  // 删除选中条目
+  const handleDelete = () => {
+    if (selectedIds.length === 0) {
+      message.warning('请先选择要删除的条目');
+      return;
+    }
+
+    Modal.confirm({
+      title: '确认删除',
+      content: `确定要永久删除选中的 ${selectedIds.length} 个条目吗?此操作不可恢复!`,
+      okText: '确认删除',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await dispatch(deleteBinThunk(selectedIds)).unwrap();
+          message.success('删除成功');
+          // 刷新磁盘信息
+          dispatch(fetchDiskInfoThunk());
+        } catch (error) {
+          message.error('删除失败,请重试');
+          console.error('删除失败:', error);
+        }
+      },
+    });
+  };
+
+  // 清空回收站
+  const handleEmpty = () => {
+    if (!binData || binData.length === 0) {
+      message.warning('回收站已经是空的');
+      return;
+    }
+
+    Modal.confirm({
+      title: '确认清空回收站',
+      content: '确定要清空回收站吗?此操作将永久删除所有内容,不可恢复!',
+      okText: '确认清空',
+      okType: 'danger',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await dispatch(clearBinThunk()).unwrap();
+          message.success('清空成功');
+          // 刷新磁盘信息
+          dispatch(fetchDiskInfoThunk());
+        } catch (error) {
+          message.error('清空失败,请重试');
+          console.error('清空失败:', error);
+        }
+      },
+    });
+  };
+
   return (
     <div className="w-full overflow-auto h-full">
       <SearchPanel />
       <div className="mt-4">
         <BinActionPanel
-          totalCapacity={1000}
-          freeSpace={500}
-          binCapacity={100}
-          onDelete={() => {
-            /* TODO: implement delete logic */
-          }}
-          onRestore={() => {
-            /* TODO: implement restore logic */
-          }}
-          onEmpty={() => {
-            /* TODO: implement empty logic */
-          }}
+          totalCapacity={diskInfo?.total || 0}
+          freeSpace={diskInfo?.free || 0}
+          binCapacity={diskInfo?.recycle_bin || 0}
+          totalCapacityStr={diskInfo?.total_str || ''}
+          freeSpaceStr={diskInfo?.free_str || ''}
+          binCapacityStr={diskInfo?.recycle_bin_str || ''}
+          onDelete={handleDelete}
+          onRestore={handleRestore}
+          onEmpty={handleEmpty}
         />
       </div>
     </div>

+ 37 - 18
src/pages/patient/components/SearchPanel.tsx

@@ -15,8 +15,10 @@ import {
   setPageSize,
 } from '../../../states/patient/worklist/slices/searchSlice';
 import { fetchWorkThunk } from '../../../states/patient/worklist/slices/workSlice';
+import { fetchBinThunk } from '../../../states/patient/bin/slices/binSlice';
 import { AppDispatch, RootState } from '../../../states/store';
 import { WorkFilter } from '@/states/patient/worklist/types/workfilter';
+import { BinFilter } from '@/states/patient/bin/types/binFilter';
 import TimeRangeSelector from './TimeRangeSelector';
 // import { AnyAction } from '@reduxjs/toolkit';
 
@@ -100,26 +102,43 @@ const SearchPanel: React.FC = () => {
         type="primary"
         icon={<SearchOutlined />}
         onClick={() => {
-          // 根据 currentKey 确定 status
-          const status =
-            currentKey === 'worklist' ? 'Arrived,InProgress' : 'Completed';
-
           dispatch(setPage(1));
           dispatch(setPageSize(10));
-          dispatch(
-            fetchWorkThunk({
-              page: 1,
-              pageSize: 10,
-              filters: {
-                patient_id: id,
-                patient_name: name,
-                access_number: accNo,
-                start_time: startTime,
-                end_time: endTime,
-                status: status,
-              } as WorkFilter,
-            })
-          );
+
+          // 通用的过滤条件
+          const commonFilters = {
+            patient_id: id,
+            patient_name: name,
+            access_number: accNo,
+            start_time: startTime,
+            end_time: endTime,
+          };
+
+          // 根据 currentKey 调用不同的 thunk
+          if (currentKey === 'bin') {
+            // 回收站搜索
+            dispatch(
+              fetchBinThunk({
+                page: 1,
+                pageSize: 10,
+                filters: commonFilters as BinFilter,
+              })
+            );
+          } else {
+            // worklist/history 搜索
+            const status =
+              currentKey === 'worklist' ? 'Arrived,InProgress' : 'Completed';
+            dispatch(
+              fetchWorkThunk({
+                page: 1,
+                pageSize: 10,
+                filters: {
+                  ...commonFilters,
+                  status: status,
+                } as WorkFilter,
+              })
+            );
+          }
         }}
       >
         <FormattedMessage

+ 71 - 0
src/states/patient/bin/slices/binDiskInfoSlice.ts

@@ -0,0 +1,71 @@
+import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import { fetchBinDiskInfo, DiskInfo } from '../../../../API/patient/binActions';
+
+/**
+ * 磁盘信息状态接口
+ */
+export interface BinDiskInfoState {
+  data: DiskInfo | null;
+  loading: boolean;
+  error: string | null;
+}
+
+/**
+ * 初始状态
+ */
+const initialState: BinDiskInfoState = {
+  data: null,
+  loading: false,
+  error: null,
+};
+
+/**
+ * 获取磁盘信息的thunk
+ */
+export const fetchDiskInfoThunk = createAsyncThunk(
+  'binDiskInfo/fetch',
+  async () => {
+    const data = await fetchBinDiskInfo();
+    return data;
+  }
+);
+
+/**
+ * 磁盘信息slice
+ */
+const binDiskInfoSlice = createSlice({
+  name: 'binDiskInfo',
+  initialState,
+  reducers: {
+    // 清空磁盘信息
+    clearDiskInfo: (state) => {
+      state.data = null;
+      state.error = null;
+    },
+  },
+  extraReducers: (builder) => {
+    builder
+      // 获取磁盘信息 - pending
+      .addCase(fetchDiskInfoThunk.pending, (state) => {
+        state.loading = true;
+        state.error = null;
+      })
+      // 获取磁盘信息 - fulfilled
+      .addCase(
+        fetchDiskInfoThunk.fulfilled,
+        (state, action: PayloadAction<DiskInfo>) => {
+          state.loading = false;
+          state.data = action.payload;
+          state.error = null;
+        }
+      )
+      // 获取磁盘信息 - rejected
+      .addCase(fetchDiskInfoThunk.rejected, (state, action) => {
+        state.loading = false;
+        state.error = action.error.message || '获取磁盘信息失败';
+      });
+  },
+});
+
+export const { clearDiskInfo } = binDiskInfoSlice.actions;
+export default binDiskInfoSlice;

+ 167 - 0
src/states/patient/bin/slices/binSlice.ts

@@ -0,0 +1,167 @@
+/* eslint-disable */
+import { createEntityListSlices } from '../../../list_template/createListSlices';
+import {
+  createFetchThunk,
+  createDeleteThunk,
+} from '../../../list_template/thunk.factory';
+import { work, workAnimal } from '../../worklist/types/worklist';
+import { BinFilter } from '../types/binFilter';
+import { createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
+import {
+  setId,
+  setName,
+  setAccNo,
+  setStartTime,
+  setEndTime,
+  setPage,
+  setPageSize,
+} from '../../worklist/slices/searchSlice';
+import {
+  fetchBinList,
+  restoreBinStudies,
+  deleteBinStudies,
+  clearBin,
+} from '../../../../API/patient/binActions';
+import store from '@/states/store';
+
+// 获取回收站列表的thunk
+export const fetchBinThunk = createFetchThunk<BinFilter, work | workAnimal>(
+  'bin',
+  async ({ page, pageSize, filters }) => {
+    const { items, total } = await fetchBinList(page, pageSize, {
+      id: filters.patient_id,
+      name: filters.patient_name,
+      acc_no: filters.access_number,
+      start_time: filters.start_time,
+      end_time: filters.end_time,
+    });
+    return { data: items, total };
+  }
+);
+
+// 恢复回收站条目的thunk
+export const restoreBinThunk = createAsyncThunk(
+  'bin/restore',
+  async (ids: string[], { dispatch }) => {
+    await restoreBinStudies(ids);
+    // 清空选择
+    dispatch(selectionSlice.actions.clearSelection());
+    // 刷新列表
+    const state = store.getState();
+    const { page, pageSize } = state.binPagination;
+    const filters = state.binFilters;
+    await dispatch(fetchBinThunk({ page, pageSize, filters }));
+    // 返回恢复的IDs,用于reducer更新
+    return ids;
+  }
+);
+
+// 删除回收站条目的thunk
+export const deleteBinThunk = createAsyncThunk(
+  'bin/delete',
+  async (ids: string[], { dispatch }) => {
+    await deleteBinStudies(ids);
+    // 清空选择
+    dispatch(selectionSlice.actions.clearSelection());
+    // 刷新列表
+    const state = store.getState();
+    const { page, pageSize } = state.binPagination;
+    const filters = state.binFilters;
+    await dispatch(fetchBinThunk({ page, pageSize, filters }));
+    // 返回删除的IDs,用于reducer更新
+    return ids;
+  }
+);
+
+// 清空回收站的thunk
+export const clearBinThunk = createAsyncThunk(
+  'bin/clear',
+  async (_, { dispatch }) => {
+    await clearBin();
+    // 清空选择
+    dispatch(selectionSlice.actions.clearSelection());
+    // 刷新列表
+    const state = store.getState();
+    const { page, pageSize } = state.binPagination;
+    const filters = state.binFilters;
+    await dispatch(fetchBinThunk({ page, pageSize, filters }));
+    // 返回空数组,因为清空了所有条目
+    return [];
+  }
+);
+
+// 额外的reducer - 监听search相关的action
+const extraReducersForFilter = (builder) => {
+  builder.addCase(
+    setId.type,
+    (state: BinFilter, action: PayloadAction<string>) => {
+      state.patient_id = action.payload;
+    }
+  );
+  builder.addCase(
+    setName.type,
+    (state: BinFilter, action: PayloadAction<string>) => {
+      state.patient_name = action.payload;
+    }
+  );
+  builder.addCase(
+    setAccNo.type,
+    (state: BinFilter, action: PayloadAction<string>) => {
+      state.access_number = action.payload;
+    }
+  );
+  builder.addCase(
+    setStartTime.type,
+    (state: BinFilter, action: PayloadAction<string>) => {
+      state.start_time = action.payload;
+    }
+  );
+  builder.addCase(
+    setEndTime.type,
+    (state: BinFilter, action: PayloadAction<string>) => {
+      state.end_time = action.payload;
+    }
+  );
+  builder.addCase(
+    setPage.type,
+    (state: BinFilter, action: PayloadAction<number>) => {
+      state.page = action.payload;
+    }
+  );
+  builder.addCase(
+    setPageSize.type,
+    (state: BinFilter, action: PayloadAction<number>) => {
+      state.page_size = action.payload;
+    }
+  );
+};
+
+// 创建回收站的slices
+const {
+  entitiesSlice,
+  filtersSlice,
+  paginationSlice,
+  selectionSlice,
+  uiSlice,
+} = createEntityListSlices<work | workAnimal, BinFilter>(
+  'bin',
+  fetchBinThunk,
+  deleteBinThunk, // 这里传deleteBinThunk,但实际删除操作会用自定义的
+  'StudyID',
+  extraReducersForFilter,
+  {
+    patient_id: '',
+    patient_name: '',
+    start_time: '',
+    end_time: '',
+    access_number: '',
+    page: 1,
+    page_size: 10,
+  } satisfies BinFilter
+);
+
+export const binEntitiesSlice = entitiesSlice;
+export const binFiltersSlice = filtersSlice;
+export const binPaginationSlice = paginationSlice;
+export const binSelectionSlice = selectionSlice;
+export const binUISlice = uiSlice;

+ 13 - 0
src/states/patient/bin/types/binFilter.ts

@@ -0,0 +1,13 @@
+/**
+ * 回收站过滤条件类型
+ * 与API层的BinFilter保持一致
+ */
+export interface BinFilter {
+  patient_id?: string;
+  patient_name?: string;
+  start_time?: string;
+  end_time?: string;
+  access_number?: string;
+  page?: number;
+  page_size?: number;
+}

+ 14 - 0
src/states/store.ts

@@ -71,6 +71,14 @@ import themeReducer from './themeSlice';
 import cameraReducer from './exam/cameraSlice';
 import pacsNodeReducer from './output/pacsNode/pacsNodeSlice';
 import selectedPatientReducer from './patient/worklist/slices/selectedPatientSlice';
+import {
+  binEntitiesSlice,
+  binFiltersSlice,
+  binPaginationSlice,
+  binSelectionSlice,
+  binUISlice,
+} from './patient/bin/slices/binSlice';
+import binDiskInfoSlice from './patient/bin/slices/binDiskInfoSlice';
 
 const store = configureStore({
   reducer: {
@@ -134,6 +142,12 @@ const store = configureStore({
     camera: cameraReducer,
     pacsNode: pacsNodeReducer,
     selectedPatient: selectedPatientReducer,
+    binEntities: binEntitiesSlice.reducer,
+    binFilters: binFiltersSlice.reducer,
+    binPagination: binPaginationSlice.reducer,
+    binSelection: binSelectionSlice.reducer,
+    binUI: binUISlice.reducer,
+    binDiskInfo: binDiskInfoSlice.reducer,
   },
   middleware: (getDefaultMiddleware) =>
     getDefaultMiddleware().concat(