Ver código fonte

添加RIS同步的基本实现,能发出ris同步请求

dengdx 1 mês atrás
pai
commit
66040b085c

+ 657 - 0
docs/实现/RIS同步功能实现方案.md

@@ -0,0 +1,657 @@
+# RIS同步功能实现方案
+
+## 📖 概述
+
+本文档详细描述了RIS(Radiology Information System)同步功能的简化实现方案。该功能允许用户通过ActionPanel中的RIS按钮,基于当前搜索条件触发后端RIS同步,然后显示同步结果并自动刷新列表。
+
+## 🔄 核心流程
+
+**简化的RIS同步流程**:
+1. 用户点击RIS按钮
+2. 获取当前搜索条件
+3. 调用syncRis API触发后端同步
+4. 显示同步结果弹框(显示同步数据量)
+5. 用户确认后自动重新搜索(等同于点击search按钮)
+
+## 🏗️ 架构设计
+
+### 参与者层级分析
+
+#### 架构层级参与者
+- **UI层**: `ActionPanel.tsx` - RIS按钮交互入口
+- **状态管理层**: Redux slices - 管理同步状态和数据流
+- **业务逻辑层**: Thunk异步操作 - 处理同步逻辑
+- **API层**: `risActions.ts` - RIS接口调用
+- **数据层**: `work.ts` Task实体和数据结构
+
+#### 组件级参与者
+**主要组件**:
+- `ActionPanel.tsx` - 包含RIS按钮的操作面板
+- `SearchPanel.tsx` - 提供同步条件的搜索面板
+- `WorklistTable` - 显示同步结果的表格
+
+**辅助组件**:
+- `WorklistPage` - 工作列表页面容器
+- 消息通知组件 - 显示同步结果反馈
+
+#### 方法级参与者
+**API方法**:
+- `syncRis()` - 执行RIS同步(核心方法)
+- `fetchWorkThunk()` - 重新搜索刷新列表
+
+**UI方法**:
+- `handleRisSync()` - RIS按钮点击处理
+- `triggerSearch()` - 触发自动搜索
+- `showSyncSuccessModal()` - 显示成功弹框
+- `showSyncErrorModal()` - 显示错误弹框
+
+## 📊 系统架构图
+
+```mermaid
+graph TB
+    subgraph "🎨 UI层"
+        A[ActionPanel RIS按钮]
+        B[SearchPanel 搜索条件]
+        C[WorklistTable 数据显示]
+        D[Modal 结果弹框]
+    end
+    
+    subgraph "🏪 状态管理层"
+        E[searchSlice 搜索状态]
+        F[workSlice 列表数据]
+    end
+    
+    subgraph "🌐 API层"
+        G[syncRis API]
+        H[fetchTaskList API]
+    end
+    
+    subgraph "💾 数据层"
+        I[RIS服务器]
+        J[本地数据库]
+    end
+    
+    A --> G
+    A --> D
+    B --> E
+    D --> H
+    G --> I
+    H --> J
+    F --> C
+    E --> B
+    
+    style A fill:#e1f5fe
+    style D fill:#f1f8e9
+    style G fill:#e8f5e8
+    style I fill:#fff3e0
+```
+
+## 🔄 交互流程
+
+### 序列图
+
+```mermaid
+sequenceDiagram
+    participant User as 👤 用户
+    participant ActionPanel as 🎛️ ActionPanel
+    participant SearchState as 📋 搜索状态
+    participant RisAPI as 🌐 RIS API
+    participant Backend as 🏢 后端RIS同步
+    participant Modal as 📋 结果弹框
+    participant WorkList as 📊 工作列表
+
+    User->>ActionPanel: 点击RIS按钮
+    ActionPanel->>ActionPanel: setRisSyncing(true)
+    ActionPanel->>SearchState: 读取搜索条件
+    SearchState-->>ActionPanel: 返回条件参数
+    
+    ActionPanel->>RisAPI: syncRis(参数)
+    RisAPI->>Backend: 执行RIS数据同步
+    Backend-->>RisAPI: 返回同步结果
+    RisAPI-->>ActionPanel: 同步完成 + 数据量
+    
+    alt 同步成功
+        ActionPanel->>Modal: showSyncSuccessModal
+        Modal-->>User: 显示"同步X条数据"
+        User->>Modal: 点击确定
+        Modal->>ActionPanel: onOk回调
+        ActionPanel->>WorkList: triggerSearch()
+        WorkList-->>User: 显示更新列表
+    else 同步失败
+        ActionPanel->>Modal: showSyncErrorModal
+        Modal-->>User: 显示错误信息
+    end
+    
+    ActionPanel->>ActionPanel: setRisSyncing(false)
+```
+
+### 数据流向图
+
+```mermaid
+flowchart TD
+    A[🔍 当前搜索条件] --> B[🎛️ RIS按钮点击]
+    B --> C[🌐 调用syncRis API]
+    C --> D[🏢 后端执行RIS同步]
+    D --> E[📊 返回同步结果]
+    E --> F[📋 显示结果弹框]
+    F --> G[🔍 自动触发搜索]
+    G --> H[📋 刷新列表显示]
+    
+    style B fill:#e3f2fd
+    style F fill:#f1f8e9
+    style H fill:#fce4ec
+```
+
+## 💾 数据结构
+
+### 核心数据类型
+
+```typescript
+// RIS同步请求参数
+interface RisSyncRequest {
+  start_time: string;    // RFC3339格式时间
+  end_time: string;      // RFC3339格式时间
+  id?: string;           // 患者ID(可选)
+  name?: string;         // 患者姓名(可选)
+  acc_no?: string;       // 登记号(可选)
+}
+
+// RIS同步响应
+interface RisSyncResponse {
+  code: string;          // 响应码
+  description: string;   // 描述信息
+  solution: string;      // 解决方案
+  data: {
+    '@type': string;     // 类型标识
+    count: number;       // 同步的条目数量
+  };
+}
+
+// 搜索状态(复用现有结构)
+interface SearchState {
+  id: string;
+  name: string; 
+  acc_no: string;
+  start_time: string;
+  end_time: string;
+  timeRangeType: 'today' | '7days' | 'all';
+}
+
+// RIS同步状态
+interface RisSyncState {
+  loading: boolean;           // 同步进行中
+  lastSyncTime: string;      // 最后同步时间
+  syncCount: number;         // 本次同步数量
+  error: string | null;      // 错误信息
+}
+```
+
+### 类图
+
+```mermaid
+classDiagram
+    class ActionPanel {
+        +handleRisSync()
+        +triggerSearch()
+        +showSyncSuccessModal()
+        +showSyncErrorModal()
+        +risSyncing: boolean
+    }
+    
+    class SearchSlice {
+        +id: string
+        +name: string
+        +acc_no: string
+        +start_time: string
+        +end_time: string
+        +setId()
+        +setName()
+        +setTimeRange()
+    }
+    
+    class RisAPI {
+        +syncRis(params: RisSyncRequest)
+        +getRisConfig()
+        +saveRisSingle()
+        +saveRisBatch()
+    }
+    
+    class WorkSlice {
+        +fetchWorkThunk()
+        +deleteWorkThunk()
+        +lockWorkThunk()
+    }
+    
+    class Modal {
+        +success()
+        +error()
+    }
+    
+    ActionPanel --> SearchSlice : 读取搜索条件
+    ActionPanel --> RisAPI : 直接调用API
+    ActionPanel --> Modal : 显示结果弹框
+    ActionPanel --> WorkSlice : 触发搜索刷新
+```
+
+## 🔧 实现细节
+
+### 1. 简化的状态管理
+
+#### 无需复杂Redux状态
+基于新的需求,我们采用更简单的方案:
+- **本地状态**: 只在ActionPanel中使用`useState`管理loading状态
+- **复用现有逻辑**: 直接复用SearchPanel中的搜索逻辑
+- **简单弹框**: 使用antd的Modal.success/error进行结果提示
+
+```typescript
+// 在ActionPanel.tsx中
+const [risSyncing, setRisSyncing] = useState(false);
+```
+
+### 2. RIS同步结果弹框
+
+#### 简单的Modal实现
+```typescript
+// 成功弹框
+const showSyncSuccessModal = (count: number, onOk: () => void) => {
+  Modal.success({
+    title: '🎉 RIS同步成功',
+    content: (
+      <div style={{ padding: '16px 0' }}>
+        <p style={{ fontSize: '16px', margin: '8px 0' }}>
+          共同步 <strong style={{ color: '#52c41a' }}>{count}</strong> 条数据
+        </p>
+        <p style={{ color: '#666', margin: '8px 0' }}>
+          系统将自动刷新列表显示最新数据
+        </p>
+      </div>
+    ),
+    okText: '确定',
+    centered: true,
+    onOk: onOk
+  });
+};
+
+// 失败弹框
+const showSyncErrorModal = (errorMessage: string) => {
+  Modal.error({
+    title: '❌ RIS同步失败',
+    content: (
+      <div style={{ padding: '16px 0' }}>
+        <p style={{ margin: '8px 0' }}>{errorMessage}</p>
+        <p style={{ color: '#666', fontSize: '14px' }}>
+          请检查网络连接或联系技术支持
+        </p>
+      </div>
+    ),
+    okText: '确定',
+    centered: true
+  });
+};
+```
+
+### 3. 自动搜索刷新逻辑
+
+#### 复用SearchPanel的搜索逻辑
+```typescript
+// 在ActionPanel.tsx中添加搜索触发函数
+const triggerSearch = useCallback(() => {
+  // 重置分页
+  dispatch(setPage(1));
+  dispatch(setPageSize(10));
+
+  // 获取当前搜索条件
+  const searchState = useSelector((state: RootState) => state.search);
+  const commonFilters = {
+    patient_id: searchState.id,
+    patient_name: searchState.name,
+    access_number: searchState.acc_no,
+    start_time: searchState.start_time,
+    end_time: searchState.end_time,
+  };
+
+  // 根据当前页面类型调用相应的搜索
+  if (currentKey === 'bin') {
+    dispatch(fetchBinThunk({
+      page: 1,
+      pageSize: 10,
+      filters: commonFilters as BinFilter,
+    }));
+  } else {
+    const status = currentKey === 'worklist' ? 'Arrived,InProgress' : 'Completed';
+    dispatch(fetchWorkThunk({
+      page: 1,
+      pageSize: 10,
+      filters: { ...commonFilters, status } as WorkFilter,
+    }));
+  }
+}, [dispatch, currentKey]);
+```
+
+### 4. ActionPanel完整实现
+
+#### RIS按钮处理函数
+```typescript
+const handleRisSync = useCallback(async () => {
+  try {
+    setRisSyncing(true);
+    
+    // 获取搜索条件
+    const searchState = useSelector((state: RootState) => state.search);
+    
+    // 构建同步参数
+    const syncParams: RisSyncRequest = {
+      start_time: searchState.start_time,
+      end_time: searchState.end_time,
+      id: searchState.id || undefined,
+      name: searchState.name || undefined,
+      acc_no: searchState.acc_no || undefined
+    };
+
+    // 调用RIS同步API
+    const result = await syncRis(syncParams);
+    
+    // 显示成功弹框
+    showSyncSuccessModal(result.data.count, () => {
+      // 自动触发搜索
+      triggerSearch();
+    });
+    
+  } catch (error) {
+    console.error('RIS同步失败:', error);
+    showSyncErrorModal(error.message || '同步过程中发生未知错误');
+  } finally {
+    setRisSyncing(false);
+  }
+}, [triggerSearch]);
+
+// 按钮组件
+<ActionButton
+  icon={<Icon module="module-patient" name="RIS" />}
+  tooltip={<FormattedMessage id="actionPanel.risSync" />}
+  onClick={handleRisSync}
+  loading={risSyncing}
+  disabled={risSyncing}
+/>
+```
+
+### 5. 实现优势
+
+#### 简化架构的优势
+1. **更简单**: 无需复杂的Redux状态管理
+2. **更直接**: API调用 → 结果弹框 → 自动搜索
+3. **更易维护**: 复用现有的搜索逻辑,减少代码重复
+4. **用户体验好**: 清晰的反馈流程和自动刷新
+
+#### 数据流特点
+- **后端处理**: RIS同步在后端完成,前端只触发和展示结果
+- **自动刷新**: 同步成功后自动重新搜索,无需手动刷新
+- **状态简单**: 只需要管理按钮的loading状态
+
+## 🧪 测试方案
+
+### 🔍 **功能测试场景**
+
+| 测试场景 | 前置条件 | 操作步骤 | 预期结果 | 验收标准 |
+|---------|---------|---------|---------|---------|
+| **正常同步流程** | 设置有效搜索条件 | 1. 在搜索面板设置患者姓名"张三"<br>2. 设置时间范围为今天<br>3. 点击RIS按钮<br>4. 在弹框中点击确定 | 1. 按钮显示loading状态<br>2. 弹框显示"共同步X条数据"<br>3. 点击确定后列表自动刷新<br>4. 新同步的数据出现在列表中 | ✅ 完整流程无错误<br>✅ 数据正确同步和显示 |
+| **同步无数据** | 设置不匹配的条件 | 1. 设置未来时间范围<br>2. 点击RIS按钮 | 弹框显示"共同步0条数据" | ✅ 正确显示零条记录<br>✅ 不影响现有列表 |
+| **网络异常处理** | 模拟网络断开 | 1. 断开网络<br>2. 设置搜索条件<br>3. 点击RIS按钮 | 显示错误弹框,提示网络错误 | ✅ 错误信息清晰<br>✅ 不执行列表刷新 |
+| **服务器错误处理** | 模拟API返回错误 | 1. 模拟syncRis返回错误<br>2. 点击RIS按钮 | 显示具体的错误信息弹框 | ✅ 显示服务器错误详情<br>✅ 提供解决建议 |
+| **防重复点击** | 同步进行中 | 1. 点击RIS按钮<br>2. 在同步完成前再次点击 | 按钮禁用,无法重复点击 | ✅ 按钮disabled状态<br>✅ 防止重复请求 |
+| **弹框交互** | 同步成功 | 1. 同步成功后显示弹框<br>2. 不点击确定,直接关闭弹框 | 弹框关闭,不执行搜索 | ✅ 用户可以取消后续操作 |
+
+### 边界条件测试
+
+| 边界条件 | 测试场景 | 预期行为 |
+|---------|---------|---------|
+| **时间格式** | 测试各种时间格式输入 | 正确转换为RFC3339格式 |
+| **特殊字符** | 患者姓名包含特殊字符 | 正确编码和传输 |
+| **超长字符串** | 输入超长的患者姓名 | 适当截断或验证 |
+| **并发操作** | 同时进行多个操作 | 操作互不干扰 |
+
+### 性能测试
+
+| 测试项目 | 测试条件 | 性能要求 | 验收标准 |
+|---------|---------|---------|---------|
+| **同步响应时间** | 同步100条数据 | < 3秒 | 用户体验良好 |
+| **内存使用** | 同步大量数据后 | < 100MB增长 | 内存使用合理 |
+| **UI响应性** | 同步进行中 | UI保持响应 | 不阻塞用户操作 |
+
+### 兼容性测试
+
+| 测试环境 | 测试内容 | 预期结果 |
+|---------|---------|---------|
+| **不同浏览器** | Chrome、Firefox、Edge | 功能正常 |
+| **不同屏幕尺寸** | 手机、平板、桌面 | 布局适配 |
+| **不同网络条件** | 快速、慢速、不稳定 | 优雅降级 |
+
+## 🐛 潜在问题分析
+
+### 边界情况处理
+
+#### 1. 搜索条件验证
+**问题**: 用户可能在没有设置任何搜索条件时点击RIS同步
+```typescript
+// 解决方案:条件验证
+const validateSearchConditions = (searchState: SearchState): boolean => {
+  const hasTimeRange = searchState.start_time && searchState.end_time;
+  const hasPatientInfo = searchState.id || searchState.name || searchState.acc_no;
+  
+  if (!hasTimeRange && !hasPatientInfo) {
+    message.warning('请至少设置时间范围或患者信息');
+    return false;
+  }
+  
+  return true;
+};
+```
+
+#### 2. 大量数据同步
+**问题**: 同步大量数据可能导致内存和性能问题
+```typescript
+// 解决方案:分批处理和进度提示
+const handleLargeDataSync = async (params: RisSyncRequest) => {
+  // 1. 预估数据量
+  const estimatedCount = await estimateSyncCount(params);
+  
+  if (estimatedCount > 1000) {
+    // 2. 显示确认对话框
+    const confirmed = await showConfirmDialog(
+      `预计同步 ${estimatedCount} 条数据,可能需要较长时间,是否继续?`
+    );
+    
+    if (!confirmed) return;
+    
+    // 3. 显示进度条
+    showProgressModal();
+  }
+  
+  // 4. 执行同步
+  return await syncRis(params);
+};
+```
+
+#### 3. 网络超时处理
+**问题**: 网络不稳定可能导致同步超时
+```typescript
+// 解决方案:超时重试机制
+const risSyncWithRetry = async (params: RisSyncRequest, retries = 3) => {
+  for (let i = 0; i < retries; i++) {
+    try {
+      return await syncRis(params);
+    } catch (error) {
+      if (i === retries - 1) throw error;
+      
+      // 等待后重试
+      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
+    }
+  }
+};
+```
+
+### 异常处理机制
+
+#### 错误处理层级
+```mermaid
+flowchart TD
+    A[用户操作] --> B[UI层验证]
+    B --> C[Redux Thunk]
+    C --> D[API调用]
+    D --> E[服务器响应]
+    
+    B --> F[显示验证错误]
+    C --> G[显示业务错误]
+    D --> H[显示网络错误]
+    E --> I[显示服务器错误]
+    
+    F --> J[用户反馈]
+    G --> J
+    H --> J
+    I --> J
+    
+    style F fill:#ffebee
+    style G fill:#fff3e0
+    style H fill:#e8f5e8
+    style I fill:#f3e5f5
+```
+
+#### 具体错误处理策略
+
+| 错误类型 | 处理策略 | 用户反馈 |
+|---------|---------|---------|
+| **验证错误** | 前端验证,阻止请求 | 友好的提示信息 |
+| **网络错误** | 自动重试,超时处理 | 网络状态提示 |
+| **权限错误** | 引导用户登录 | 权限说明 |
+| **服务器错误** | 记录日志,显示错误 | 技术支持联系方式 |
+| **数据错误** | 数据回滚,状态恢复 | 操作失败说明 |
+
+### 性能优化考虑
+
+#### 1. 内存管理
+- **数据分页**: 避免一次性加载大量数据
+- **虚拟滚动**: 大列表使用虚拟滚动
+- **内存清理**: 及时清理无用数据
+
+#### 2. 网络优化
+- **请求去重**: 防止重复请求
+- **请求缓存**: 缓存频繁请求的结果
+- **压缩传输**: 启用gzip压缩
+
+#### 3. UI性能
+- **异步加载**: 非阻塞UI更新
+- **防抖处理**: 防止频繁操作
+- **加载状态**: 提供清晰的加载反馈
+
+## 🔒 安全考虑
+
+### 数据安全
+- **参数验证**: 严格验证所有输入参数
+- **SQL注入防护**: 后端参数化查询
+- **XSS防护**: 前端数据转义
+
+### 权限控制
+- **接口权限**: 验证用户RIS同步权限
+- **数据隔离**: 确保用户只能访问授权数据
+- **操作日志**: 记录所有同步操作
+
+## 📈 监控和日志
+
+### 操作监控
+```typescript
+// 监控关键指标
+const monitorRisSync = {
+  syncCount: 0,           // 同步次数
+  totalSyncedItems: 0,    // 总同步条目数
+  averageResponseTime: 0, // 平均响应时间
+  errorRate: 0,          // 错误率
+  
+  // 记录同步操作
+  logSyncOperation: (params: RisSyncRequest, result: RisSyncResponse) => {
+    console.log('RIS同步操作', {
+      timestamp: new Date().toISOString(),
+      params,
+      result,
+      responseTime: performance.now()
+    });
+  }
+};
+```
+
+### 错误日志
+```typescript
+// 错误追踪
+const logRisSyncError = (error: Error, context: any) => {
+  console.error('RIS同步错误', {
+    error: error.message,
+    stack: error.stack,
+    context,
+    timestamp: new Date().toISOString(),
+    userAgent: navigator.userAgent
+  });
+  
+  // 发送到监控服务
+  sendErrorToMonitoring(error, context);
+};
+```
+
+## 🚀 部署和维护
+
+### 部署检查清单
+- [ ] API接口测试通过
+- [ ] 前端功能测试通过
+- [ ] 性能测试满足要求
+- [ ] 安全审计通过
+- [ ] 文档更新完成
+
+### 维护指南
+- **定期监控**: 关注同步成功率和响应时间
+- **日志分析**: 定期分析错误日志,优化问题点
+- **用户反馈**: 收集用户使用反馈,持续改进
+- **版本更新**: 跟进RIS服务器接口变更
+
+## 📚 相关文档
+
+- [RIS API 接口文档](src/API/patient/risActions.ts)
+- [工作列表状态管理](src/states/patient/worklist/slices/workSlice.ts)
+- [搜索面板组件](src/pages/patient/components/SearchPanel.tsx)
+- [操作面板组件](src/pages/patient/components/ActionPanel.tsx)
+
+## 🚀 实施步骤
+
+### **开发顺序建议**
+1. **第一步**: 修改ActionPanel.tsx,添加handleRisSync函数
+2. **第二步**: 实现Modal弹框显示逻辑
+3. **第三步**: 添加triggerSearch自动搜索功能
+4. **第四步**: 完善错误处理和loading状态
+5. **第五步**: 进行完整的功能测试
+
+### **代码变更清单**
+- **修改文件**: `src/pages/patient/components/ActionPanel.tsx`
+- **新增依赖**: 无(使用现有的antd Modal和RIS API)
+- **测试文件**: 需要更新ActionPanel的单元测试
+
+## 📋 总结
+
+### **核心优势**
+✅ **简单高效**: 无复杂状态管理,直接API调用
+✅ **用户友好**: 清晰的反馈流程和自动刷新
+✅ **易于维护**: 复用现有逻辑,代码变更最小
+✅ **稳定可靠**: 完整的错误处理和边界情况考虑
+
+### **技术亮点**
+- 复用SearchPanel的搜索逻辑实现自动刷新
+- 使用antd Modal提供一致的用户体验
+- 本地状态管理避免复杂的Redux配置
+- 完整的错误处理和用户反馈机制
+
+### **实现效果**
+用户点击RIS按钮 → 后端同步数据 → 弹框显示结果 → 自动刷新列表,整个流程简洁流畅,符合用户期望。
+
+---
+
+**文档信息**
+- 📅 最后更新时间: 2025-01-30
+- 📋 文档版本: v2.0 (简化版本)
+- 👥 维护人员: 开发团队
+- 🎯 实现状态: 设计完成,待开发实施

+ 138 - 5
src/pages/patient/components/ActionPanel.tsx

@@ -1,9 +1,10 @@
-import React from 'react';
+import React, { useState, useCallback } from 'react';
 import { Button, Tooltip, Modal, message } from 'antd';
 import { useDispatch, useSelector } from 'react-redux';
 import {
   deleteWorkThunk,
   lockWorkInWorklistThunk,
+  fetchWorkThunk,
 } from '@/states/patient/worklist/slices/workSlice';
 import { deleteWorkThunk as deleteWorkThunkFromHistory, lockWorkInhistorylistThunk } from '@/states/patient/worklist/slices/history';
 import { switchToSendPanel } from '@/states/patient/worklist/slices/historyPanelSwitchSlice';
@@ -19,12 +20,18 @@ import { setBusinessFlow } from '@/states/BusinessFlowSlice';
 import { setSourceTask, setRegisterInfo } from '@/states/patient/reregister/reregisterSlice';
 import { mapTaskToRegisterInfo } from '@/domain/patient/taskToRegister';
 import { showNotImplemented } from '@/utils/notificationHelper';
-import { string } from 'zod';
+import { syncRis, RisSyncRequest } from '@/API/patient/risActions';
+import { fetchBinThunk } from '@/states/patient/bin/slices/binSlice';
+import { setPage, setPageSize } from '@/states/patient/worklist/slices/searchSlice';
+import { WorkFilter } from '@/states/patient/worklist/types/workfilter';
+import { BinFilter } from '@/states/patient/bin/types/binFilter';
 
 interface ActionButtonProps {
   icon: React.ReactNode;
   tooltip: React.ReactNode;
   onClick?: () => void;
+  loading?: boolean;
+  disabled?: boolean;
   'data-testid'?: string;
 }
 
@@ -32,15 +39,27 @@ const ActionButton: React.FC<ActionButtonProps> = ({
   icon,
   tooltip,
   onClick,
+  loading,
+  disabled,
   'data-testid': dataTestId,
 }) => (
   <Tooltip title={tooltip}>
-    <Button icon={icon} onClick={onClick} style={{ width: '2.5rem' }} data-testid={dataTestId} />
+    <Button
+      icon={icon}
+      onClick={onClick}
+      loading={loading}
+      disabled={disabled}
+      style={{ width: '2.5rem' }}
+      data-testid={dataTestId}
+    />
   </Tooltip>
 );
 
 const ActionPanel: React.FC = () => {
   const dispatch = useDispatch<AppDispatch>();
+  
+  // RIS同步loading状态
+  const [risSyncing, setRisSyncing] = useState(false);
 
   const workSelectedIds = useSelector(
     (state: RootState) => state.workSelection.selectedIds
@@ -61,6 +80,7 @@ const ActionPanel: React.FC = () => {
   const workEntitiesFromHistory = useSelector(
     (state: RootState) => state.historyEntities.data
   );
+  const searchState = useSelector((state: RootState) => state.search);
 
   const getSelectedWorkIds = () => {
     const selectedIds =
@@ -238,6 +258,116 @@ const ActionPanel: React.FC = () => {
     });
   };
 
+  // RIS同步成功弹框
+  const showSyncSuccessModal = useCallback((count: number, onOk: () => void) => {
+    Modal.success({
+      title: '🎉 RIS同步成功',
+      content: (
+        <div style={{ padding: '16px 0' }}>
+          <p style={{ fontSize: '16px', margin: '8px 0' }}>
+            共同步 <strong style={{ color: '#52c41a' }}>{count}</strong> 条数据
+          </p>
+          <p style={{ color: '#666', margin: '8px 0' }}>
+            系统将自动刷新列表显示最新数据
+          </p>
+        </div>
+      ),
+      okText: '确定',
+      centered: true,
+      onOk: onOk
+    });
+  }, []);
+
+  // RIS同步失败弹框
+  const showSyncErrorModal = useCallback((errorMessage: string) => {
+    Modal.error({
+      title: '❌ RIS同步失败',
+      content: (
+        <div style={{ padding: '16px 0' }}>
+          <p style={{ margin: '8px 0' }}>{errorMessage}</p>
+          <p style={{ color: '#666', fontSize: '14px' }}>
+            请检查网络连接或联系技术支持
+          </p>
+        </div>
+      ),
+      okText: '确定',
+      centered: true
+    });
+  }, []);
+
+  // 触发搜索刷新列表(复用SearchPanel的逻辑)
+  const triggerSearch = useCallback(() => {
+    // 重置分页
+    dispatch(setPage(1));
+    dispatch(setPageSize(10));
+
+    // 构建通用的过滤条件
+    const commonFilters = {
+      patient_id: searchState.id,
+      patient_name: searchState.name,
+      access_number: searchState.acc_no,
+      start_time: searchState.start_time,
+      end_time: searchState.end_time,
+    };
+
+    // 根据当前页面类型调用相应的搜索
+    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,
+      }));
+    }
+  }, [dispatch, currentKey, searchState]);
+
+  // RIS同步处理函数
+  const handleRisSync = useCallback(async () => {
+    try {
+      setRisSyncing(true);
+      
+      // 构建同步参数
+      const syncParams: RisSyncRequest = {
+        start_time: searchState.start_time,
+        end_time: searchState.end_time,
+        id: searchState.id || undefined,
+        name: searchState.name || undefined,
+        acc_no: searchState.acc_no || undefined
+      };
+
+      console.log('RIS同步参数:', syncParams);
+
+      // 调用RIS同步API
+      const result = await syncRis(syncParams);
+      
+      console.log('RIS同步结果:', result);
+      
+      // 显示成功弹框
+      showSyncSuccessModal(result.data.count, () => {
+        // 自动触发搜索
+        triggerSearch();
+      });
+      
+    } catch (error) {
+      console.error('RIS同步失败:', error);
+      const errorMessage = error instanceof Error ? error.message : '同步过程中发生未知错误';
+      showSyncErrorModal(errorMessage);
+    } finally {
+      setRisSyncing(false);
+    }
+  }, [searchState, showSyncSuccessModal, showSyncErrorModal, triggerSearch]);
+
   return (
     <div className="flex flex-wrap gap-2 w-full">
       <ActionButton
@@ -312,10 +442,13 @@ const ActionPanel: React.FC = () => {
         tooltip={
           <FormattedMessage
             id="actionPanel.risSync"
-            defaultMessage="actionPanel.risSync"
+            defaultMessage="RIS同步"
           />
         }
-        onClick={() => showNotImplemented('')}
+        onClick={handleRisSync}
+        loading={risSyncing}
+        disabled={risSyncing}
+        data-testid="ris-sync-button"
       />
       {currentKey === 'historylist' && (
       <ActionButton