系统支持模拟器和真实设备两种工作模式。当系统运行在模拟器模式下时,某些硬件操作(如曝光操作)无法真实执行,需要在UI上明确标识这种状态。
从 API dr/api/v1/pub/software_info
可以获取设备类型信息:
FPD: "Simulator"
- 表示平板探测器为模拟设备FPD: "Physics"
- 表示平板探测器为真实物理设备需求:
当 FPD === "Simulator"
时(模拟环境),DeviceArea.tsx
中的"曝光指示器"按钮应:
当 FPD === "Physics"
时(真实环境),"曝光指示器"按钮应:
src/states/productSlice.ts
fpd
和 gen
字段initializeProductState
thunk,将 FPD 和 GEN 信息存入 Reduxsrc/pages/exam/DeviceArea.tsx
src/states/device/deviceInfoSlice.ts
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>;
效果:
优点:
缺点:
状态管理:方案 1(扩展 productSlice)
UI 展示:方案 A(onClick 判断 + Badge 角标)
接口定义:
interface SoftwareInfo {
FPD: string; // 平板探测器类型
GEN: string; // 发生器类型
language: string[]; // 支持的语言列表
product: string; // 产品名称
guest: string; // 急诊访问 token
sn: string; // 序列号
server: Record<string, any>; // 服务器信息
}
设备类型:
"Simulator"
- 模拟设备"Physics"
- 物理设备productSlice 的作用:
数据流:
应用启动
↓
dispatch(initializeProductState())
↓
调用 fetchSoftwareInfo() API
↓
Redux store 更新(包含 fpd, gen)
↓
组件通过 useSelector 读取状态
↓
根据状态渲染 UI
用途:在按钮或图标上显示小标识
关键属性:
count
: 显示的内容(数字或文字)offset
: 位置偏移 [x, y]
color
: 角标颜色size
: 角标大小文件路径:src/states/productSlice.ts
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: '', // ⭐ 新增
};
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, // ⭐ 新增
};
}
);
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);
});
},
});
文件路径:src/pages/exam/DeviceArea.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;
import { Flex, Button, Badge } from 'antd';
const fpd = useSelector((state: RootState) => state.product.fpd);
const isSimulator = fpd === 'Simulator';
<Badge count={isSimulator ? '模拟' : 0} offset={[-5, 5]} color="orange">
<Button ... />
</Badge>
onClick={() => {
if (!isSimulator) {
console.warn('真实环境下,曝光操作需要通过硬件触发');
return;
}
triggerInspection();
}}
更新 title 提示:
title={`曝光指示器: ${exposureStatus}${isSimulator ? ' (模拟模式)' : ' (真实模式)'}`}
文件路径 | 修改内容 |
---|---|
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 |
曝光触发函数 |
initializeProductState
需要在应用启动时尽早调用app.tsx
或根组件中调用typescript
const isSimulator = fpd === 'Simulator' || fpd === '';
import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
<Badge count={isSimulator ? t('simulator') : 0} />;
除了 FPD 和 GEN,未来可能需要显示更多设备状态:
建议在 productSlice 中统一管理这些设备信息。
当前方案中,FPD 状态只在初始化时获取一次。如果设备可能在运行时切换模式,需要:
允许用户配置模拟模式下的行为:
日期 | 修改人 | 修改内容 |
---|---|---|
2025/10/7 | - | 创建文档,定义需求和实现方案 |