# 模拟状态的呈现效果
## 1. 需求描述
### 1.1 业务场景
系统支持模拟器和真实设备两种工作模式。当系统运行在模拟器模式下时,某些硬件操作(如曝光操作)无法真实执行,需要在UI上明确标识这种状态。
### 1.2 具体需求
从 API `dr/api/v1/pub/software_info` 可以获取设备类型信息:
- `FPD: "Simulator"` - 表示平板探测器为模拟设备
- `FPD: "Physics"` - 表示平板探测器为真实物理设备
**需求**:
- 当 `FPD === "Simulator"` 时(模拟环境),`DeviceArea.tsx` 中的"曝光指示器"按钮应:
1. **可以点击**(用于模拟测试)
2. **显示"模拟"标识**(使用橙色 Badge 角标)
3. **保持状态颜色显示**(如绿色表示就绪)
- 当 `FPD === "Physics"` 时(真实环境),"曝光指示器"按钮应:
1. **不可点击**(真实曝光需通过硬件触发,防止误操作)
2. **不显示"模拟"角标**
3. **保持状态颜色显示**(如绿色表示就绪)
### 1.3 用户体验目标
- 用户能够清晰识别当前是否在模拟环境下工作
- 模拟环境:允许点击按钮进行模拟测试,显示橙色"模拟"角标
- 真实环境:禁止点击按钮(硬件触发),无角标显示
- 两种环境下都保持设备状态颜色指示(绿色/黄色/红色等)
---
## 2. 技术方案对比
### 方案 1:扩展现有的 productSlice(推荐)✅
#### 优点
- ✅ 改动最小,softwareInfo 已经在 productSlice 中获取
- ✅ 逻辑集中,所有软件配置信息在一个地方管理
- ✅ 无需新建文件和额外的初始化逻辑
#### 缺点
- ⚠️ productSlice 会包含设备相关信息,职责略微混合
#### 实现步骤
1. 修改 `src/states/productSlice.ts`
- 在 ProductState 接口中添加 `fpd` 和 `gen` 字段
- 更新 `initializeProductState` thunk,将 FPD 和 GEN 信息存入 Redux
2. 修改 `src/pages/exam/DeviceArea.tsx`
- 从 Redux 读取 FPD 状态
- 在 onClick 中判断环境类型:真实环境阻止执行,模拟环境允许执行
- 使用 Ant Design Badge 组件在模拟环境下显示"模拟"标识
- 不使用 disabled 属性,保持状态颜色显示
---
### 方案 2:创建独立的 deviceInfoSlice
#### 优点
- ✅ 职责分离更清晰,设备信息独立管理
- ✅ 便于未来扩展更多设备相关状态
#### 缺点
- ❌ 需要新建文件 `src/states/device/deviceInfoSlice.ts`
- ❌ 需要在 store 中注册新的 reducer
- ❌ 需要额外的初始化逻辑,可能重复调用 API
---
### 方案 3:在 DeviceArea 组件内部管理状态
#### 优点
- ✅ 组件自包含,不依赖全局状态
#### 缺点
- ❌ 如果其他组件也需要 FPD 状态,会导致重复请求
- ❌ 组件职责过重
- ❌ 难以测试和维护
---
## 3. UI 展示方案对比
### 方案 A:onClick 判断 + Badge 角标(最终采用)✅
```tsx
import { Badge } from 'antd';
}
onClick={() => {
if (!isSimulator) {
// 真实环境下禁止点击
console.warn('真实环境下,曝光操作需要通过硬件触发');
return;
}
// 模拟环境下允许点击
triggerInspection();
}}
title={`曝光指示器: ${exposureStatus}${isSimulator ? ' (模拟模式)' : ' (真实模式)'}`}
/>
;
```
**效果**:
- 模拟环境:显示橙色"模拟"角标,按钮可点击
- 真实环境:无角标,按钮点击无效
- 两种环境都保持状态颜色(如 text-green-500)
**优点**:
- ✅ 保留状态颜色指示
- ✅ 清晰的视觉区分(模拟有角标,真实无角标)
- ✅ 不使用 disabled 属性,避免灰色覆盖
- ✅ 符合实际使用场景(模拟可测试,真实靠硬件)
---
### 方案 B:使用 disabled 属性(已弃用)
**缺点**:
- ❌ disabled 会应用灰色样式,覆盖状态颜色
- ❌ 无法同时显示禁用状态和设备状态颜色
- ❌ 不符合需求(模拟环境需要可点击)
---
## 4. 推荐方案总结
### 最佳组合
**状态管理**:方案 1(扩展 productSlice)
**UI 展示**:方案 A(onClick 判断 + Badge 角标)
### 理由
1. **开发效率高**:复用现有的 softwareInfo 获取逻辑
2. **用户体验好**:
- 模拟环境:显示角标 + 可点击(便于测试)
- 真实环境:无角标 + 不可点击(防止误操作)
- 两种环境都保持状态颜色指示
3. **代码改动小**:只需修改两个文件
4. **易于维护**:逻辑集中,便于未来扩展
5. **符合实际场景**:真实设备曝光由硬件触发,软件按钮仅作状态显示
---
## 5. 涉及的核心概念
### 5.1 软件信息(SoftwareInfo)
**接口定义**:
```typescript
interface SoftwareInfo {
FPD: string; // 平板探测器类型
GEN: string; // 发生器类型
language: string[]; // 支持的语言列表
product: string; // 产品名称
guest: string; // 急诊访问 token
sn: string; // 序列号
server: Record; // 服务器信息
}
```
**设备类型**:
- `"Simulator"` - 模拟设备
- `"Physics"` - 物理设备
### 5.2 Redux 状态管理
**productSlice 的作用**:
- 存储产品相关的全局配置信息
- 在应用初始化时自动获取软件信息
- 提供给各个组件使用
**数据流**:
```
应用启动
↓
dispatch(initializeProductState())
↓
调用 fetchSoftwareInfo() API
↓
Redux store 更新(包含 fpd, gen)
↓
组件通过 useSelector 读取状态
↓
根据状态渲染 UI
```
### 5.3 Ant Design Badge 组件
**用途**:在按钮或图标上显示小标识
**关键属性**:
- `count`: 显示的内容(数字或文字)
- `offset`: 位置偏移 `[x, y]`
- `color`: 角标颜色
- `size`: 角标大小
---
## 6. 详细实现方案
### 6.1 修改 productSlice.ts
**文件路径**:`src/states/productSlice.ts`
#### 步骤 1:扩展 State 接口
```typescript
interface ProductState {
productName: 'DROS' | 'VETDROS';
language: string;
source: 'Electron' | 'Browser' | 'Android';
guest: string;
fpd: string; // ⭐ 新增:平板探测器类型
gen: string; // ⭐ 新增:发生器类型
}
const initialState: ProductState = {
productName: 'DROS',
language: 'en',
source: 'Browser',
guest: '',
fpd: '', // ⭐ 新增
gen: '', // ⭐ 新增
};
```
#### 步骤 2:更新 initializeProductState thunk
```typescript
export const initializeProductState = createAsyncThunk(
'product/initializeProductState',
async () => {
const softwareInfo = await fetchSoftwareInfo();
console.log(`加载软件系统信息:${JSON.stringify(softwareInfo)}`);
return {
productName: softwareInfo.product as 'DROS' | 'VETDROS',
language: softwareInfo.language[0],
source: 'Browser' as const,
guest: softwareInfo.guest,
fpd: softwareInfo.FPD, // ⭐ 新增
gen: softwareInfo.GEN, // ⭐ 新增
};
}
);
```
#### 步骤 3:更新 Reducers
```typescript
const productSlice = createSlice({
name: 'product',
initialState,
reducers: {
setProduct: (state, action: PayloadAction) => {
state.productName = action.payload.productName;
state.language = action.payload.language;
state.source = action.payload.source;
state.guest = action.payload.guest;
state.fpd = action.payload.fpd; // ⭐ 新增
state.gen = action.payload.gen; // ⭐ 新增
},
},
extraReducers: (builder) => {
builder
.addCase(initializeProductState.fulfilled, (state, action) => {
state.productName = action.payload.productName;
state.language = action.payload.language;
state.source = action.payload.source;
state.guest = action.payload.guest;
state.fpd = action.payload.fpd; // ⭐ 新增
state.gen = action.payload.gen; // ⭐ 新增
})
.addCase(initializeProductState.rejected, (state, action) => {
console.error('Failed to initialize product state:', action.error);
});
},
});
```
---
### 6.2 修改 DeviceArea.tsx
**文件路径**:`src/pages/exam/DeviceArea.tsx`
#### 完整代码
```tsx
import { Flex, Button, Badge } from 'antd';
import {
ToolOutlined,
CameraOutlined,
TabletOutlined,
} from '@ant-design/icons';
import { useSelector } from 'react-redux';
import { RootState } from '@/states/store';
import {
GENERATOR_STATUS,
GeneratorStatus,
} from '@/states/exam/deviceAreaSlice';
import triggerInspection from '../../API/exam/triggerInspection';
const DeviceArea = ({ className }: { className?: string }) => {
// 现有的状态选择器
const generatorStatus = useSelector(
(state: RootState) => state.deviceArea.generatorStatus
);
const generatorStatus_2 = useSelector(
(state: RootState) => state.deviceArea.generatorStatus_2
);
const exposureStatus = useSelector(
(state: RootState) => state.deviceArea.exposureStatus
);
const tabletStatus = useSelector(
(state: RootState) => state.deviceArea.tabletStatus
);
// ⭐ 新增:读取 FPD 状态
const fpd = useSelector((state: RootState) => state.product.fpd);
const isSimulator = fpd === 'Simulator';
const btnStyle = { width: '1.5rem', height: '1.5rem' };
const classValue = 'mr-1';
return (
{/* 手闸状态指示器 - 保持不变 */}
}
title={`手闸状态指示器: ${generatorStatus}`}
/>
{/* ⭐ 曝光指示器 - 添加模拟状态标识 */}
}
onClick={() => {
if (!isSimulator) {
// 真实环境下禁止点击
console.warn('真实环境下,曝光操作需要通过硬件触发');
return;
}
// 模拟环境下允许点击
triggerInspection();
}}
title={`曝光指示器: ${exposureStatus}${isSimulator ? ' (模拟模式)' : ' (真实模式)'}`}
/>
{/* 平板指示器 - 保持不变 */}
}
title={`平板指示器: ${tabletStatus}`}
/>
);
};
export default DeviceArea;
```
#### 关键修改点
1. **导入 Badge 组件**:
```tsx
import { Flex, Button, Badge } from 'antd';
```
2. **读取 FPD 状态**:
```tsx
const fpd = useSelector((state: RootState) => state.product.fpd);
const isSimulator = fpd === 'Simulator';
```
3. **使用 Badge 包裹按钮**:
```tsx
```
4. **在 onClick 中判断环境**:
```tsx
onClick={() => {
if (!isSimulator) {
console.warn('真实环境下,曝光操作需要通过硬件触发');
return;
}
triggerInspection();
}}
```
5. **更新 title 提示**:
```tsx
title={`曝光指示器: ${exposureStatus}${isSimulator ? ' (模拟模式)' : ' (真实模式)'}`}
```
---
## 7. 相关文件清单
### 需要修改的文件
| 文件路径 | 修改内容 |
| ------------------------------- | -------------------------------- |
| `src/states/productSlice.ts` | 扩展 State 接口,添加 fpd 和 gen |
| `src/pages/exam/DeviceArea.tsx` | 添加 Badge 组件和禁用逻辑 |
### 相关参考文件
| 文件路径 | 作用 |
| ----------------------------------- | --------------------------------- |
| `src/API/softwareInfo.ts` | 定义 SoftwareInfo 接口和 API 调用 |
| `src/states/store.ts` | Redux store 配置 |
| `src/API/exam/triggerInspection.ts` | 曝光触发函数 |
---
## 8. 注意事项
### 8.1 初始化时序
- `initializeProductState` 需要在应用启动时尽早调用
- 确保在 DeviceArea 渲染前,fpd 状态已经加载完成
- 可以在 `app.tsx` 或根组件中调用
### 8.2 默认值处理
- 在 API 未返回或加载失败时,fpd 默认为空字符串
- 空字符串不等于 "Simulator",因此默认按钮是启用的
- 如果需要更安全的默认行为,可以考虑:
```typescript
const isSimulator = fpd === 'Simulator' || fpd === '';
```
### 8.3 样式兼容性
- Badge 组件的 offset 可能需要根据实际按钮大小调整
- 在不同屏幕尺寸下测试 Badge 的显示效果
- 确保 Badge 不会遮挡按钮图标
### 8.4 国际化
- "模拟" 文字应该支持国际化
- 可以使用 i18n 替换硬编码的文字:
```tsx
import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
;
```
---
## 9. 未来扩展建议
### 9.1 更多设备状态
除了 FPD 和 GEN,未来可能需要显示更多设备状态:
- 手闸(Hand Switch)状态
- 脚踏板(Foot Switch)状态
- 准直器(Collimator)状态
建议在 productSlice 中统一管理这些设备信息。
### 9.2 设备状态实时更新
当前方案中,FPD 状态只在初始化时获取一次。如果设备可能在运行时切换模式,需要:
1. 实现 WebSocket 或轮询机制监听设备状态变化
2. 更新 Redux store 中的 fpd 状态
3. UI 自动响应状态变化
### 9.3 更丰富的视觉反馈
- 在模拟模式下,可以为整个设备区域添加背景色
- 在页面顶部显示全局的"模拟模式"横幅
- 使用动画效果吸引用户注意
### 9.4 用户偏好设置
允许用户配置模拟模式下的行为:
- 是否完全隐藏曝光按钮
- 是否显示模拟曝光动画
- 是否允许"假装曝光"用于培训目的
---
## 10. 参考资料
- [Ant Design Badge 组件文档](https://ant.design/components/badge-cn)
- [Redux Toolkit 官方文档](https://redux-toolkit.js.org/)
- [React useSelector Hook](https://react-redux.js.org/api/hooks#useselector)
---
## 更新记录
| 日期 | 修改人 | 修改内容 |
| --------- | ------ | ---------------------------- |
| 2025/10/7 | - | 创建文档,定义需求和实现方案 |