Procházet zdrojové kódy

feat (1.68.0 -> 1.69.0): 新增扫码枪状态指示器组件并集成到患者注册表单

- 新增 BarcodeScannerIndicator 组件,提供扫码枪状态可视化指示
- 在患者注册表单右上角集成扫码枪状态指示器,浮动显示
- 添加扫码枪状态相关国际化文本,支持中英文提示
- 优化表单代码格式,修复空格问题

改动文件:
- src/components/BarcodeScannerIndicator/index.tsx (新增)
- src/pages/patient/components/register.form.tsx
- src/assets/i18n/messages/en.js
- src/assets/i18n/messages/zh.js
dengdx před 10 hodinami
rodič
revize
3a46c52279

+ 16 - 1
CHANGELOG.md

@@ -2,6 +2,21 @@
 
 本项目的所有重要变更都将记录在此文件中.
 
+## [1.69.0] - 2026-01-19 10:36
+
+feat (1.68.0 -> 1.69.0): 新增扫码枪状态指示器组件并集成到患者注册表单
+
+- 新增 BarcodeScannerIndicator 组件,提供扫码枪状态可视化指示
+- 在患者注册表单右上角集成扫码枪状态指示器,浮动显示
+- 添加扫码枪状态相关国际化文本,支持中英文提示
+- 优化表单代码格式,修复空格问题
+
+改动文件:
+- src/components/BarcodeScannerIndicator/index.tsx (新增)
+- src/pages/patient/components/register.form.tsx
+- src/assets/i18n/messages/en.js
+- src/assets/i18n/messages/zh.js
+
 ## [1.68.0] - 2026-01-16 18:43
 
 feat (1.67.0 -> 1.68.0): 实现扫码枪自动填充患者注册表单功能
@@ -154,7 +169,7 @@ fix (1.66.0 -> 1.66.1): 修复服务器配置连接检测和实时更新功能
 ### 修复 (Fixed)
 
 - **修复患者登记号格式** - 修复患者登记号(accession_number)生成格式,从毫秒改为秒数,确保格式正确
-  - 在register.form.tsx中将dayjs格式从'YYYYMMDDHHmmSS'改为'YYYYMMDDHHmmss'
+  - 在register.form.tsx中将dayjs格式从'YYYMMDDHHmmSS'改为'YYYYMMDDHHmmss'
 
 **改动文件:**
 

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "zsis",
-  "version": "1.68.0",
+  "version": "1.69.0",
   "private": true,
   "description": "医学成像系统",
   "main": "main.js",

+ 3 - 0
src/assets/i18n/messages/en.js

@@ -407,4 +407,7 @@ export default {
   'systemMode.R.desc': 'Both generator and FPD are virtual',
   'systemMode.S.desc': 'Both generator and FPD are real hardware',
   'systemMode.R-S.desc': 'FPD is real hardware, generator is virtual',
+  'scanner.indicator.tooltip.ready': 'Scanner is ready, you can scan QR code',
+  'scanner.indicator.tooltip.notReady': 'Scanner unavailable, click to activate',
+  'scanner.indicator.activated': 'Scanner activated, please scan QR code',
 };

+ 3 - 0
src/assets/i18n/messages/zh.js

@@ -407,4 +407,7 @@ export default {
   "systemMode.R.desc": "发生器、FPD均为虚拟",
   "systemMode.S.desc": "发生器、FPD均为真实硬件",
   "systemMode.R-S.desc": "FPD真实、发生器虚拟",
+  "scanner.indicator.tooltip.ready": "扫码枪已就绪,可以扫描二维码",
+  "scanner.indicator.tooltip.notReady": "扫码枪不可用,点击此处激活",
+  "scanner.indicator.activated": "扫码枪已就绪,请扫描二维码",
 };

+ 177 - 0
src/components/BarcodeScannerIndicator/index.tsx

@@ -0,0 +1,177 @@
+import React, { useState, useEffect } from 'react';
+import { Tooltip, message } from 'antd';
+import { ScanOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
+import { useIntl } from 'react-intl';
+
+interface BarcodeScannerIndicatorProps {
+  style?: React.CSSProperties;
+  className?: string;
+  inline?: boolean; // 是否为内联模式(非浮动)
+}
+
+const BarcodeScannerIndicator: React.FC<BarcodeScannerIndicatorProps> = ({
+  style,
+  className,
+  inline = false
+}) => {
+  const intl = useIntl();
+  const [hasFocusedInput, setHasFocusedInput] = useState(false);
+
+  // 检测元素是否为可编辑元素(排除隐藏的扫码枪input)
+  const isEditableElement = (element: HTMLElement | null): boolean => {
+    if (!element) return false;
+
+    // 排除隐藏的扫码枪input(通过样式判断)
+    if (element.tagName === 'INPUT') {
+      const input = element as HTMLInputElement;
+      const style = window.getComputedStyle(input);
+      // 如果是隐藏的input(opacity为0或position为fixed且left为负值),则不算作可编辑元素
+      if (
+        style.opacity === '0' ||
+        (style.position === 'fixed' && parseInt(style.left) < -1000)
+      ) {
+        return false;
+      }
+    }
+
+    return (
+      element.tagName === 'INPUT' ||
+      element.tagName === 'TEXTAREA' ||
+      element.isContentEditable ||
+      element.closest('.ant-select') !== null ||
+      element.closest('.ant-picker') !== null ||
+      element.closest('.ant-input-number') !== null
+    );
+  };
+
+  // 监听焦点变化
+  useEffect(() => {
+    const handleFocusIn = (e: FocusEvent) => {
+      const target = e.target as HTMLElement;
+      setHasFocusedInput(isEditableElement(target));
+    };
+
+    const handleFocusOut = () => {
+      // 延迟检查,确保新焦点已设置
+      setTimeout(() => {
+        const activeElement = document.activeElement as HTMLElement;
+        setHasFocusedInput(isEditableElement(activeElement));
+      }, 10);
+    };
+
+    window.addEventListener('focusin', handleFocusIn);
+    window.addEventListener('focusout', handleFocusOut);
+
+    // 初始化检查
+    const activeElement = document.activeElement as HTMLElement;
+    setHasFocusedInput(isEditableElement(activeElement));
+
+    return () => {
+      window.removeEventListener('focusin', handleFocusIn);
+      window.removeEventListener('focusout', handleFocusOut);
+    };
+  }, []);
+
+  // 点击处理:使所有输入框失焦
+  const handleClick = () => {
+    const activeElement = document.activeElement as HTMLElement;
+    
+    if (activeElement && activeElement.blur) {
+      activeElement.blur();
+    }
+
+    // 确保焦点移到 body
+    document.body.focus();
+
+    // 显示提示
+    message.success(
+      intl.formatMessage({
+        id: 'scanner.indicator.activated',
+        defaultMessage: '扫码枪已就绪,请扫描二维码'
+      })
+    );
+  };
+
+  const isReady = !hasFocusedInput;
+
+  const tooltipText = intl.formatMessage({
+    id: isReady ? 'scanner.indicator.tooltip.ready' : 'scanner.indicator.tooltip.notReady',
+    defaultMessage: isReady 
+      ? '扫码枪已就绪,可以扫描二维码' 
+      : '扫码枪不可用,点击此处激活'
+  });
+
+  return (
+    <Tooltip
+      title={tooltipText}
+      placement="top"
+    >
+      <div
+        role="button"
+        tabIndex={0}
+        onClick={handleClick}
+        onKeyDown={(e) => {
+          if (e.key === 'Enter' || e.key === ' ') {
+            e.preventDefault();
+            handleClick();
+          }
+        }}
+        style={{
+          display: 'inline-flex',
+          alignItems: 'center',
+          justifyContent: 'center',
+          padding: '4px 15px',
+          backgroundColor: isReady ? '#52c41a' : '#d9d9d9',
+          border: `1px solid ${isReady ? '#52c41a' : '#d9d9d9'}`,
+          borderRadius: '6px',
+          color: 'white',
+          cursor: 'pointer',
+          transition: 'all 0.3s ease',
+          height: '100%',
+          minHeight: '32px',
+          outline: 'none',
+          userSelect: 'none',
+          ...style
+        }}
+        className={className}
+        onMouseEnter={(e) => {
+          e.currentTarget.style.opacity = '0.85';
+        }}
+        onMouseLeave={(e) => {
+          e.currentTarget.style.opacity = '1';
+        }}
+      >
+        <div style={{ position: 'relative', display: 'inline-block' }}>
+          <ScanOutlined style={{ fontSize: '18px' }} />
+          {isReady ? (
+            <CheckCircleOutlined
+              style={{
+                position: 'absolute',
+                bottom: -2,
+                right: -2,
+                fontSize: '10px',
+                color: '#52c41a',
+                backgroundColor: 'white',
+                borderRadius: '50%'
+              }}
+            />
+          ) : (
+            <CloseCircleOutlined
+              style={{
+                position: 'absolute',
+                bottom: -2,
+                right: -2,
+                fontSize: '10px',
+                color: '#ff4d4f',
+                backgroundColor: 'white',
+                borderRadius: '50%'
+              }}
+            />
+          )}
+        </div>
+      </div>
+    </Tooltip>
+  );
+};
+
+export default BarcodeScannerIndicator;

+ 13 - 1
src/pages/patient/components/register.form.tsx

@@ -8,6 +8,7 @@ import {
   Radio,
   FormInstance,
   message,
+  Space,
 } from 'antd';
 import { useIntl, FormattedMessage } from 'react-intl';
 import { registerFormFields } from '@/validation/patient/registerSchema';
@@ -22,6 +23,8 @@ import {
 } from '@/domain/patient/pregnancyStatus';
 import { getGenderOptions } from '@/domain/patient/genderOptions';
 import { processQRCodeData, transformToFormData } from '@/domain/qrcode/qrCodeDataProcessor';
+import BarcodeScannerIndicator from '@/components/BarcodeScannerIndicator';
+
 interface BasicInfoFormProps {
   style?: React.CSSProperties;
   form?: FormInstance;
@@ -66,7 +69,7 @@ const BasicInfoForm: React.FC<BasicInfoFormProps> = ({
 
       const allValues = form?.getFieldsValue();
       if (allValues && Object.keys(allValues).length > 0) {
-        const merged = { ...allValues ,...updatedFormData};
+        const merged = { ...allValues, ...updatedFormData };
         dispatch(setFormData(merged));
         form?.setFieldsValue(merged)
       }
@@ -278,6 +281,14 @@ const BasicInfoForm: React.FC<BasicInfoFormProps> = ({
       }
       onValuesChange={onValuesChange}
     >
+      {/* 扫码枪状态指示器 - 浮动在右上角 */}
+      <div style={{
+        position: 'absolute',
+        right: '48px',
+        zIndex: 1000
+      }}>
+        <BarcodeScannerIndicator />
+      </div>
       {/** 宠物专用 */}
       {productName === 'VETDROS' && (
         <Form.Item
@@ -333,6 +344,7 @@ const BasicInfoForm: React.FC<BasicInfoFormProps> = ({
           })}
         />
       </Form.Item>
+
       <Form.Item
         label={
           <FormattedMessage