|
@@ -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;
|