本文档详细描述了RIS(Radiology Information System)同步功能的简化实现方案。该功能允许用户通过ActionPanel中的RIS按钮,基于当前搜索条件触发后端RIS同步,然后显示同步结果并自动刷新列表。
简化的RIS同步流程:
ActionPanel.tsx - RIS按钮交互入口risActions.ts - RIS接口调用work.ts Task实体和数据结构主要组件:
ActionPanel.tsx - 包含RIS按钮的操作面板SearchPanel.tsx - 提供同步条件的搜索面板WorklistTable - 显示同步结果的表格辅助组件:
WorklistPage - 工作列表页面容器API方法:
syncRis() - 执行RIS同步(核心方法)fetchWorkThunk() - 重新搜索刷新列表UI方法:
handleRisSync() - RIS按钮点击处理triggerSearch() - 触发自动搜索showSyncSuccessModal() - 显示成功弹框showSyncErrorModal() - 显示错误弹框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
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)
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
// 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; // 错误信息
}
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 : 触发搜索刷新
基于新的需求,我们采用更简单的方案:
useState管理loading状态// 在ActionPanel.tsx中
const [risSyncing, setRisSyncing] = useState(false);
// 成功弹框
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
});
};
// 在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]);
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}
/>
| 测试场景 | 前置条件 | 操作步骤 | 预期结果 | 验收标准 |
|---|---|---|---|---|
| 正常同步流程 | 设置有效搜索条件 | 1. 在搜索面板设置患者姓名"张三" 2. 设置时间范围为今天 3. 点击RIS按钮 4. 在弹框中点击确定 |
1. 按钮显示loading状态 2. 弹框显示"共同步X条数据" 3. 点击确定后列表自动刷新 4. 新同步的数据出现在列表中 |
✅ 完整流程无错误 ✅ 数据正确同步和显示 |
| 同步无数据 | 设置不匹配的条件 | 1. 设置未来时间范围 2. 点击RIS按钮 |
弹框显示"共同步0条数据" | ✅ 正确显示零条记录 ✅ 不影响现有列表 |
| 网络异常处理 | 模拟网络断开 | 1. 断开网络 2. 设置搜索条件 3. 点击RIS按钮 |
显示错误弹框,提示网络错误 | ✅ 错误信息清晰 ✅ 不执行列表刷新 |
| 服务器错误处理 | 模拟API返回错误 | 1. 模拟syncRis返回错误 2. 点击RIS按钮 |
显示具体的错误信息弹框 | ✅ 显示服务器错误详情 ✅ 提供解决建议 |
| 防重复点击 | 同步进行中 | 1. 点击RIS按钮 2. 在同步完成前再次点击 |
按钮禁用,无法重复点击 | ✅ 按钮disabled状态 ✅ 防止重复请求 |
| 弹框交互 | 同步成功 | 1. 同步成功后显示弹框 2. 不点击确定,直接关闭弹框 |
弹框关闭,不执行搜索 | ✅ 用户可以取消后续操作 |
| 边界条件 | 测试场景 | 预期行为 |
|---|---|---|
| 时间格式 | 测试各种时间格式输入 | 正确转换为RFC3339格式 |
| 特殊字符 | 患者姓名包含特殊字符 | 正确编码和传输 |
| 超长字符串 | 输入超长的患者姓名 | 适当截断或验证 |
| 并发操作 | 同时进行多个操作 | 操作互不干扰 |
| 测试项目 | 测试条件 | 性能要求 | 验收标准 |
|---|---|---|---|
| 同步响应时间 | 同步100条数据 | < 3秒 | 用户体验良好 |
| 内存使用 | 同步大量数据后 | < 100MB增长 | 内存使用合理 |
| UI响应性 | 同步进行中 | UI保持响应 | 不阻塞用户操作 |
| 测试环境 | 测试内容 | 预期结果 |
|---|---|---|
| 不同浏览器 | Chrome、Firefox、Edge | 功能正常 |
| 不同屏幕尺寸 | 手机、平板、桌面 | 布局适配 |
| 不同网络条件 | 快速、慢速、不稳定 | 优雅降级 |
问题: 用户可能在没有设置任何搜索条件时点击RIS同步
// 解决方案:条件验证
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;
};
问题: 同步大量数据可能导致内存和性能问题
// 解决方案:分批处理和进度提示
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);
};
问题: 网络不稳定可能导致同步超时
// 解决方案:超时重试机制
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)));
}
}
};
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
| 错误类型 | 处理策略 | 用户反馈 |
|---|---|---|
| 验证错误 | 前端验证,阻止请求 | 友好的提示信息 |
| 网络错误 | 自动重试,超时处理 | 网络状态提示 |
| 权限错误 | 引导用户登录 | 权限说明 |
| 服务器错误 | 记录日志,显示错误 | 技术支持联系方式 |
| 数据错误 | 数据回滚,状态恢复 | 操作失败说明 |
// 监控关键指标
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()
});
}
};
// 错误追踪
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);
};
src/pages/patient/components/ActionPanel.tsx✅ 简单高效: 无复杂状态管理,直接API调用 ✅ 用户友好: 清晰的反馈流程和自动刷新 ✅ 易于维护: 复用现有逻辑,代码变更最小 ✅ 稳定可靠: 完整的错误处理和边界情况考虑
用户点击RIS按钮 → 后端同步数据 → 弹框显示结果 → 自动刷新列表,整个流程简洁流畅,符合用户期望。
文档信息