|
@@ -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 (简化版本)
|
|
|
|
|
+- 👥 维护人员: 开发团队
|
|
|
|
|
+- 🎯 实现状态: 设计完成,待开发实施
|