| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706 |
- import {
- Row,
- Col,
- Select,
- InputNumber,
- Button,
- Switch,
- Divider,
- Tooltip,
- message,
- Flex,
- Modal,
- } from 'antd';
- import ErrorMessage from '@/components/ErrorMessage';
- import { patientSizes, PatientSize } from '../../states/patientSize';
- import { WorkstationTypeLabels, WorkstationType } from '../../states/workstation';
- import { FormattedMessage, useIntl } from 'react-intl';
- import { useSelector, useDispatch, useStore } from 'react-redux';
- import { deleteBodyPosition } from '../../API/patient/viewActions';
- import { copyPositionThunk } from '../../states/exam/examWorksCacheSlice';
- import {
- removeBodyPositionBySopInstanceUid,
- setByIndex,
- judgeImageThunk,
- } from '../../states/exam/bodyPositionListSlice';
- import { RootState } from '../../states/store';
- import {
- setAprConfig,
- setBodysize,
- setWorkstation,
- setThickness,
- setIsAECEnabled,
- updateAprParamsThunk,
- } from '../../states/exam/aprSlice';
- import { workstationIdFromWorkstation } from '../../states/workstation';
- import { AprParamsUpdateRequest } from '../../API/exam/APRActions';
- import BodyPositionList from './components/BodyPositionList';
- import BodyPositionDetail from './components/BodyPositionDetail';
- import { AppDispatch } from '@/states/store';
- import { useRef } from 'react';
- import Icon from '@/components/Icon';
- import ParaSettingCoordinator from '@/domain/exam/paraSettingCoordinator';
- import { resetDevices } from '@/states/device/deviceSlice';
- import { openCamera, closeCamera } from '@/states/exam/cameraSlice';
- import CameraModal from '@/components/CameraModal';
- const ContentAreaLarge = () => {
- const intl = useIntl();
- const dispatch = useDispatch<AppDispatch>();
- const isResetting = useSelector(
- (state: RootState) => state.device.status === 'loading'
- );
- const store = useStore<RootState>();
- const aprConfig = useSelector((state: RootState) => state.apr.aprConfig);
- const bodysize = useSelector((state: RootState) => state.apr.bodysize);
- const workstation = useSelector((state: RootState) => state.apr.workstation);
- const thickness = useSelector((state: RootState) => state.apr.thickness);
- const isAECEnabled = useSelector(
- (state: RootState) => state.apr.isAECEnabled
- );
- const currentExposureMode = useSelector(
- (state: RootState) => state.apr.currentExposureMode
- );
- const productName = useSelector(
- (state: RootState) => state.product.productName
- );
- const handleBodysizeChange = (key: string) => {
- const value = patientSizes[key as PatientSize]; // 获取对应的显示文本
- console.log('体型 key:', key); // 例如: 'small'
- console.log('体型 value:', value); // 例如: 'Small'
- dispatch(setBodysize(value));
- };
- const handleWorkstationChange = (value: string) => {
- dispatch(setWorkstation(value));
- };
- const handleThicknessChange = (value: number | null) => {
- if (value !== null) {
- dispatch(setThickness(value));
- }
- };
- const handleAECChange = (checked: boolean) => {
- dispatch(setIsAECEnabled(checked));
- };
- const handleExposureModeChange = (value: string) => {
- ParaSettingCoordinator.setExposureMode(value);
- };
- const handleResetParameters = async () => {
- try {
- await dispatch(resetDevices());
- message.success(`重置高压发生器成功`);
- } catch (error) {
- console.error('Error resetting devices:', error);
- message.error(`重置高压发生器失败 ${error}`);
- }
- };
- /**
- * 处理打开摄像头按钮点击事件
- */
- const handleOpenCamera = () => {
- // 获取当前选中体位的 study_id
- const currentStudyId = selectedBodyPosition?.study_id;
- if (!currentStudyId) {
- message.warning('请先选择一个体位');
- return;
- }
- console.log('[handleOpenCamera] 打开摄像头,Study ID:', currentStudyId);
- // dispatch openCamera action,打开摄像头
- dispatch(openCamera(currentStudyId));
- };
- /**
- * 处理拒绝图像按钮点击事件
- */
- const handleReject = async () => {
- if (!selectedBodyPosition) {
- message.warning('请先选择一个体位');
- return;
- }
- Modal.confirm({
- title: '确认拒绝',
- content: `确定要拒绝选中的图像吗?`,
- okText: '确认拒绝',
- cancelText: '取消',
- okButtonProps: {
- danger: true,
- 'data-testid': 'modal-confirm-delete'
- },
- cancelButtonProps: {
- 'data-testid': 'modal-cancel-delete'
- },
- centered: true,
- onOk: async () => {
- try {
- await dispatch(
- judgeImageThunk({
- sopInstanceUid: selectedBodyPosition.sop_instance_uid,
- accept: false,
- })
- ).unwrap();
- message.success('图像已拒绝');
- } catch (err) {
- console.error('拒绝图像失败:', err);
- message.error('拒绝图像失败');
- }
- },
- });
- };
- /**
- * 处理恢复图像按钮点击事件
- */
- const handleRestore = async () => {
- if (!selectedBodyPosition) {
- message.warning('请先选择一个体位');
- return;
- }
- try {
- await dispatch(
- judgeImageThunk({
- sopInstanceUid: selectedBodyPosition.sop_instance_uid,
- accept: true,
- })
- ).unwrap();
- message.success('图像已恢复');
- } catch (err) {
- console.error('恢复图像失败:', err);
- message.error('恢复图像失败');
- }
- };
- /**
- * 处理保存参数按钮点击事件
- */
- const handleSaveParams = async () => {
- if (!selectedBodyPosition) {
- message.warning('请先选择一个体位');
- return;
- }
- try {
- // 获取工作站ID - 将字符串转换为WorkstationType枚举
- const workstationType = workstation as WorkstationType;
- const workStationId = workstationIdFromWorkstation(
- workstationType,
- productName
- );
- // 构建请求参数
- const request: AprParamsUpdateRequest = {
- work_station_id: workStationId,
- patient_size: bodysize,
- kV: aprConfig.kV,
- mA: aprConfig.mA,
- ms: aprConfig.ms,
- mAs: aprConfig.mAs,
- };
- // 调用 updateAprParamsThunk
- await dispatch(
- updateAprParamsThunk({
- id: selectedBodyPosition.view_id,
- request,
- })
- ).unwrap();
- message.success('参数保存成功');
- } catch (err) {
- console.error('保存参数失败:', err);
- message.error('保存参数失败');
- }
- };
- // 1. 正常在顶层用 useSelector 订阅
- const selectedBodyPosition = useSelector(
- (state: RootState) => state.bodyPositionList.selectedBodyPosition
- );
- // 2. 用 ref 保存最新值(每次渲染都会更新)
- const positionRef = useRef(selectedBodyPosition);
- positionRef.current = selectedBodyPosition;
- // 3. 订阅 camera 状态
- const cameraIsOpen = useSelector((state: RootState) => state.camera.isOpen);
- const cameraStudyId = useSelector(
- (state: RootState) => state.camera.currentStudyId
- );
- // 4. 计算按钮可见性
- const loading = useSelector(
- (state: RootState) => state.bodyPositionList.loading
- );
- const exposeStatus = selectedBodyPosition?.dview.expose_status;
- const judgedStatus = selectedBodyPosition?.dview.judged_status || '';
- const isExposed = exposeStatus === 'Exposed';
- const isUnexposed = exposeStatus === 'Unexposed';
- // 按钮可见性逻辑
- const showRejectButton = isExposed && judgedStatus !== 'Reject';
- const showRestoreButton = isExposed && judgedStatus === 'Reject';
- const showSaveParamsButton = isUnexposed;
- const showDeleteButton = isUnexposed;
- return (
- <Row className="w-full p-1" style={{ height: '100%', display: 'flex' }}>
- <Col span={20} style={{ display: 'flex', flexDirection: 'column' }} className='h-full'>
- <Row gutter={16} style={{ flex: 1, minHeight: 0, display: 'flex' }} className='h-full'>
- <Col span={4} className="flex flex-col h-full" >
- <BodyPositionList layout="vertical"></BodyPositionList>
- </Col>
- <Col span={20}>
- <BodyPositionDetail />
- </Col>
- </Row>
- </Col>
- <Col
- span={4}
- style={{ height: '100%', overflowY: 'auto', flexShrink: 0 }}
- >
- <Row gutter={16} align="middle">
- <Col span={productName === 'DROS' ? 9 : 12}>
- <Select
- placeholder={<FormattedMessage id="exam.bodysize.placeholder" />}
- style={{ width: '100%', marginBottom: 8 }}
- value={!!bodysize ? intl.formatMessage({ id: bodysize }) : undefined}
- onChange={handleBodysizeChange}
- >
- {Object.entries(patientSizes).map(
- ([key, value]: [string, string]) => (
- <Select.Option key={key} value={key}>
- <FormattedMessage id={value} />
- </Select.Option>
- )
- )}
- </Select>
- </Col>
- <Col span={productName === 'DROS' ? 9 : 12}>
- <label
- style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
- >
- <FormattedMessage
- id="exam.thickness.label"
- defaultMessage="厚度 (cm)"
- />
- </label>
- <InputNumber
- placeholder={intl.formatMessage({
- id: 'exam.thickness.placeholder',
- })}
- style={{ width: '100%', marginBottom: 8 }}
- value={thickness || undefined}
- min={1}
- max={50}
- step={1}
- onChange={handleThicknessChange}
- onStep={(value, info) => {
- if (info.type === 'up') {
- ParaSettingCoordinator.increaseThickness();
- } else {
- ParaSettingCoordinator.decreaseThickness();
- }
- }}
- />
- </Col>
- {productName === 'DROS' && (
- <Col span={15}>
- <Select
- placeholder="选择工作位"
- style={{ width: '100%', marginBottom: 8 }}
- value={workstation}
- onChange={handleWorkstationChange}
- >
- {Object.entries(WorkstationTypeLabels).map(
- ([key, value]: [string, string]) => (
- <Select.Option key={key} value={value}>
- <FormattedMessage
- id={`workstation.${key.toLowerCase()}`}
- />
- </Select.Option>
- )
- )}
- </Select>
- </Col>
- )}
- </Row>
- <div>
- <div>
- <label
- style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
- >
- mA
- </label>
- <Tooltip
- title={
- currentExposureMode === 'mAs'
- ? '当前为 mAs 模式,mA 和 ms 不可调整'
- : ''
- }
- >
- <InputNumber
- disabled={currentExposureMode === 'mAs'}
- placeholder={currentExposureMode === 'mAs' ? '--' : 'mA'}
- style={{ width: '100%', marginBottom: 8 }}
- value={
- currentExposureMode === 'mAs'
- ? null
- : (aprConfig.mA ?? undefined)
- }
- onStep={(value, info) => {
- if (info.type === 'up') {
- ParaSettingCoordinator.increaseMA();
- } else {
- ParaSettingCoordinator.decreaseMA();
- }
- return false;
- }}
- />
- </Tooltip>
- </div>
- <div>
- <label
- style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
- >
- ms
- </label>
- <Tooltip
- title={
- currentExposureMode === 'mAs'
- ? '当前为 mAs 模式,mA 和 ms 不可调整'
- : ''
- }
- >
- <InputNumber
- disabled={currentExposureMode === 'mAs'}
- placeholder={currentExposureMode === 'mAs' ? '--' : 'ms'}
- style={{ width: '100%', marginBottom: 8 }}
- value={
- currentExposureMode === 'mAs'
- ? null
- : (aprConfig.ms ?? undefined)
- }
- onStep={(value, info) => {
- if (info.type === 'up') {
- ParaSettingCoordinator.increaseMS();
- } else {
- ParaSettingCoordinator.decreaseMS();
- }
- }}
- />
- </Tooltip>
- </div>
- <div>
- <label
- style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
- >
- mAs
- </label>
- <Tooltip
- title={
- currentExposureMode === 'time'
- ? '当前为 time 模式,mAs 不可调整'
- : ''
- }
- >
- <InputNumber
- disabled={currentExposureMode === 'time'}
- placeholder={currentExposureMode === 'time' ? '--' : 'mAs'}
- style={{ width: '100%', marginBottom: 8 }}
- value={
- currentExposureMode === 'time'
- ? null
- : (aprConfig.mAs ?? undefined)
- }
- onStep={(value, info) => {
- if (info.type === 'up') {
- ParaSettingCoordinator.increaseMAS();
- } else {
- ParaSettingCoordinator.decreaseMAS();
- }
- }}
- />
- </Tooltip>
- </div>
- <div>
- <label
- style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
- >
- KV
- </label>
- <InputNumber
- placeholder="KV"
- style={{ width: '100%', marginBottom: 8 }}
- value={aprConfig.kV ?? undefined}
- // onChange={(value) =>
- // dispatch(setAprConfig({ ...aprConfig, kV: value ?? 0 }))
- // }
- onStep={(value, info) => {
- if (info.type === 'up') {
- ParaSettingCoordinator.increaseKV();
- } else {
- ParaSettingCoordinator.decreaseKV();
- }
- }}
- />
- </div>
- {/* <div>
- <label
- style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
- >
- density
- </label>
- <InputNumber
- placeholder="density"
- style={{ width: '100%', marginBottom: 8 }}
- value={aprConfig.AECDensity ?? undefined}
- onChange={(value) =>
- dispatch(setAprConfig({ ...aprConfig, AECDensity: value ?? 0 }))
- }
- onStep={(value, info) => {
- if (info.type === 'up') {
- ParaSettingCoordinator.increaseDensity();
- } else {
- ParaSettingCoordinator.decreaseDensity();
- }
- }}
- />
- </div> */}
- </div>
- <div>
- <label
- style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
- >
- <FormattedMessage id="exam.exposureMode.label" />
- </label>
- <Select
- placeholder={<FormattedMessage id="exam.exposureMode.placeholder" />}
- value={currentExposureMode}
- onChange={handleExposureModeChange}
- style={{ width: '100%', marginBottom: 8 }}
- >
- <Select.Option value="mAs">mAs</Select.Option>
- <Select.Option value="time">time</Select.Option>
- </Select>
- </div>
- <div>
- <label
- style={{ display: 'block', marginBottom: 4, fontSize: '12px' }}
- >
- AEC
- </label>
- <Switch
- checkedChildren={<FormattedMessage id="exam.aec.enabled" />}
- unCheckedChildren={<FormattedMessage id="exam.aec.disabled" />}
- checked={isAECEnabled}
- onChange={handleAECChange}
- style={{ marginBottom: 8 }}
- />
- </div>
- <Flex align="center" justify="start" gap="middle" wrap>
- <Button
- data-testid="reset-generator-btn"
- style={{ width: '1.5rem', height: '1.5rem' }}
- icon={
- <Icon
- module="module-exam"
- name="btn_ResetGenerator"
- userId="base"
- theme="default"
- size="2x"
- state="normal"
- />
- }
- title={intl.formatMessage({ id: 'exam.action.resetParams' })}
- onClick={handleResetParameters}
- disabled={isResetting}
- />
- </Flex>
- <Divider />
- <Flex wrap gap="small">
- {showDeleteButton && (
- <Tooltip title={intl.formatMessage({ id: 'exam.action.deletePosition' })}>
- <Button
- style={{ width: '1.5rem', height: '1.5rem' }}
- icon={
- <Icon
- module="module-exam"
- name="btn_DeleteView"
- userId="base"
- theme="default"
- size="2x"
- state="normal"
- />
- }
- onClick={async () => {
- const state = store.getState().bodyPositionList;
- const selectedBodyPosition = state.selectedBodyPosition;
- const bodyPositions = state.bodyPositions;
- // 检查体位数量,至少保留一个
- if (bodyPositions.length <= 1) {
- message.warning('至少需要保留一个体位,无法删除');
- return;
- }
- console.log(
- `选中的体位:${JSON.stringify(selectedBodyPosition)}`
- );
- if (
- selectedBodyPosition &&
- selectedBodyPosition.sop_instance_uid
- ) {
- try {
- await deleteBodyPosition(
- selectedBodyPosition.sop_instance_uid
- );
- dispatch(
- removeBodyPositionBySopInstanceUid(
- selectedBodyPosition.sop_instance_uid
- )
- );
- dispatch(setByIndex(0));
- } catch (error) {
- console.error('Error deleting body position:', error);
- message.error('Failed to delete body position');
- }
- }
- }}
- />
- </Tooltip>
- )}
- <Tooltip title={intl.formatMessage({ id: 'exam.action.copyPosition' })}>
- <Button
- style={{ width: '1.5rem', height: '1.5rem' }}
- icon={
- <Icon
- module="module-exam"
- name="btn_Copy"
- userId="base"
- theme="default"
- size="2x"
- state="normal"
- />
- }
- onClick={() => {
- const instanceUid =
- store.getState().bodyPositionList.selectedBodyPosition
- ?.study_instance_uid ?? '';
- console.log('Copying position for instance UID:', instanceUid);
- console.log(
- `${store.getState().bodyPositionList.selectedBodyPosition}`
- );
- dispatch(copyPositionThunk({ instanceUid }));
- }}
- />
- </Tooltip>
- {showSaveParamsButton && (
- <Tooltip title={intl.formatMessage({ id: 'exam.action.saveParams' })}>
- <Button
- style={{ width: '1.5rem', height: '1.5rem' }}
- icon={
- <Icon
- module="module-exam"
- name="btn_Save"
- userId="base"
- theme="default"
- size="2x"
- state="normal"
- />
- }
- onClick={handleSaveParams}
- />
- </Tooltip>
- )}
- <Tooltip title={intl.formatMessage({ id: 'exam.action.toggleCamera' })}>
- <Button
- style={{ width: '1.5rem', height: '1.5rem' }}
- icon={
- <Icon
- module="module-exam"
- name="btn_OpenCamera"
- userId="base"
- theme="default"
- size="2x"
- state="normal"
- />
- }
- onClick={handleOpenCamera}
- />
- </Tooltip>
- {showRejectButton && (
- <Tooltip title={intl.formatMessage({ id: 'exam.action.reject' })}>
- <Button
- style={{ width: '1.5rem', height: '1.5rem' }}
- loading={loading}
- icon={
- <Icon
- module="module-exam"
- name="btn_RejectImage"
- userId="base"
- theme="default"
- size="2x"
- state="normal"
- />
- }
- onClick={handleReject}
- />
- </Tooltip>
- )}
- {showRestoreButton && (
- <Tooltip title={intl.formatMessage({ id: 'exam.action.restore' })}>
- <Button
- style={{ width: '1.5rem', height: '1.5rem' }}
- loading={loading}
- icon={
- <Icon
- module="module-exam"
- name="btn_RestoreImage"
- userId="base"
- theme="default"
- size="2x"
- state="normal"
- />
- }
- onClick={handleRestore}
- />
- </Tooltip>
- )}
- </Flex>
- <ErrorMessage />
- </Col>
- {/* 摄像头 Modal */}
- {cameraIsOpen && cameraStudyId && (
- <CameraModal
- visible={cameraIsOpen}
- studyId={cameraStudyId}
- onClose={() => dispatch(closeCamera())}
- />
- )}
- </Row>
- );
- };
- export default ContentAreaLarge;
|