|
@@ -0,0 +1,566 @@
|
|
|
+# 模拟状态的呈现效果
|
|
|
+
|
|
|
+## 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';
|
|
|
+
|
|
|
+<Badge count={isSimulator ? '模拟' : 0} offset={[-5, 5]} color="orange">
|
|
|
+ <Button
|
|
|
+ style={btnStyle}
|
|
|
+ icon={<CameraOutlined />}
|
|
|
+ onClick={() => {
|
|
|
+ if (!isSimulator) {
|
|
|
+ // 真实环境下禁止点击
|
|
|
+ console.warn('真实环境下,曝光操作需要通过硬件触发');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 模拟环境下允许点击
|
|
|
+ triggerInspection();
|
|
|
+ }}
|
|
|
+ title={`曝光指示器: ${exposureStatus}${isSimulator ? ' (模拟模式)' : ' (真实模式)'}`}
|
|
|
+ />
|
|
|
+</Badge>;
|
|
|
+```
|
|
|
+
|
|
|
+**效果**:
|
|
|
+
|
|
|
+- 模拟环境:显示橙色"模拟"角标,按钮可点击
|
|
|
+- 真实环境:无角标,按钮点击无效
|
|
|
+- 两种环境都保持状态颜色(如 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<string, any>; // 服务器信息
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**设备类型**:
|
|
|
+
|
|
|
+- `"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<ProductState>) => {
|
|
|
+ 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 (
|
|
|
+ <Flex justify="end" align="center" className={`w-full ${className}`}>
|
|
|
+ {/* 手闸状态指示器 - 保持不变 */}
|
|
|
+ <Button
|
|
|
+ style={btnStyle}
|
|
|
+ className={classValue}
|
|
|
+ icon={
|
|
|
+ <ToolOutlined
|
|
|
+ className={
|
|
|
+ generatorStatus_2 === GENERATOR_STATUS.GENERATOR_STATUS_STANDBY
|
|
|
+ ? 'text-green-500'
|
|
|
+ : generatorStatus === GeneratorStatus.GENERATOR_RAD_PREPARE
|
|
|
+ ? 'text-yellow-500'
|
|
|
+ : generatorStatus === GeneratorStatus.GENERATOR_RAD_READY
|
|
|
+ ? 'text-yellow-500'
|
|
|
+ : ''
|
|
|
+ }
|
|
|
+ />
|
|
|
+ }
|
|
|
+ title={`手闸状态指示器: ${generatorStatus}`}
|
|
|
+ />
|
|
|
+
|
|
|
+ {/* ⭐ 曝光指示器 - 添加模拟状态标识 */}
|
|
|
+ <Badge count={isSimulator ? '模拟' : 0} offset={[-5, 5]} color="orange">
|
|
|
+ <Button
|
|
|
+ style={btnStyle}
|
|
|
+ data-testid="device-all-ready"
|
|
|
+ className={`${classValue} ${
|
|
|
+ exposureStatus === 'ready'
|
|
|
+ ? 'text-green-500'
|
|
|
+ : exposureStatus === 'not_ready'
|
|
|
+ ? ''
|
|
|
+ : ''
|
|
|
+ }`}
|
|
|
+ icon={<CameraOutlined />}
|
|
|
+ onClick={() => {
|
|
|
+ if (!isSimulator) {
|
|
|
+ // 真实环境下禁止点击
|
|
|
+ console.warn('真实环境下,曝光操作需要通过硬件触发');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 模拟环境下允许点击
|
|
|
+ triggerInspection();
|
|
|
+ }}
|
|
|
+ title={`曝光指示器: ${exposureStatus}${isSimulator ? ' (模拟模式)' : ' (真实模式)'}`}
|
|
|
+ />
|
|
|
+ </Badge>
|
|
|
+
|
|
|
+ {/* 平板指示器 - 保持不变 */}
|
|
|
+ <Button
|
|
|
+ style={btnStyle}
|
|
|
+ className={`${classValue} ${
|
|
|
+ tabletStatus === 'exposing'
|
|
|
+ ? 'text-yellow-500'
|
|
|
+ : tabletStatus === 'ready'
|
|
|
+ ? 'text-green-500'
|
|
|
+ : tabletStatus === 'error'
|
|
|
+ ? 'text-red-500'
|
|
|
+ : ''
|
|
|
+ }`}
|
|
|
+ icon={<TabletOutlined />}
|
|
|
+ title={`平板指示器: ${tabletStatus}`}
|
|
|
+ />
|
|
|
+ </Flex>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+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
|
|
|
+ <Badge count={isSimulator ? '模拟' : 0} offset={[-5, 5]} color="orange">
|
|
|
+ <Button ... />
|
|
|
+ </Badge>
|
|
|
+ ```
|
|
|
+
|
|
|
+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();
|
|
|
+ <Badge count={isSimulator ? t('simulator') : 0} />;
|
|
|
+ ```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 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 | - | 创建文档,定义需求和实现方案 |
|