index.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import React, { useState, useEffect } from 'react';
  2. import { Tooltip, message } from 'antd';
  3. import { ScanOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
  4. import { useIntl } from 'react-intl';
  5. interface BarcodeScannerIndicatorProps {
  6. style?: React.CSSProperties;
  7. className?: string;
  8. inline?: boolean; // 是否为内联模式(非浮动)
  9. }
  10. const BarcodeScannerIndicator: React.FC<BarcodeScannerIndicatorProps> = ({
  11. style,
  12. className,
  13. inline = false
  14. }) => {
  15. const intl = useIntl();
  16. const [hasFocusedInput, setHasFocusedInput] = useState(false);
  17. // 检测元素是否为可编辑元素(排除隐藏的扫码枪input)
  18. const isEditableElement = (element: HTMLElement | null): boolean => {
  19. if (!element) return false;
  20. // 排除隐藏的扫码枪input(通过样式判断)
  21. if (element.tagName === 'INPUT') {
  22. const input = element as HTMLInputElement;
  23. const style = window.getComputedStyle(input);
  24. // 如果是隐藏的input(opacity为0或position为fixed且left为负值),则不算作可编辑元素
  25. if (
  26. style.opacity === '0' ||
  27. (style.position === 'fixed' && parseInt(style.left) < -1000)
  28. ) {
  29. return false;
  30. }
  31. }
  32. return (
  33. element.tagName === 'INPUT' ||
  34. element.tagName === 'TEXTAREA' ||
  35. element.isContentEditable ||
  36. element.closest('.ant-select') !== null ||
  37. element.closest('.ant-picker') !== null ||
  38. element.closest('.ant-input-number') !== null
  39. );
  40. };
  41. // 监听焦点变化
  42. useEffect(() => {
  43. const handleFocusIn = (e: FocusEvent) => {
  44. const target = e.target as HTMLElement;
  45. setHasFocusedInput(isEditableElement(target));
  46. };
  47. const handleFocusOut = () => {
  48. // 延迟检查,确保新焦点已设置
  49. setTimeout(() => {
  50. const activeElement = document.activeElement as HTMLElement;
  51. setHasFocusedInput(isEditableElement(activeElement));
  52. }, 10);
  53. };
  54. window.addEventListener('focusin', handleFocusIn);
  55. window.addEventListener('focusout', handleFocusOut);
  56. // 初始化检查
  57. const activeElement = document.activeElement as HTMLElement;
  58. setHasFocusedInput(isEditableElement(activeElement));
  59. return () => {
  60. window.removeEventListener('focusin', handleFocusIn);
  61. window.removeEventListener('focusout', handleFocusOut);
  62. };
  63. }, []);
  64. // 点击处理:使所有输入框失焦
  65. const handleClick = () => {
  66. const activeElement = document.activeElement as HTMLElement;
  67. if (activeElement && activeElement.blur) {
  68. activeElement.blur();
  69. }
  70. // 确保焦点移到 body
  71. document.body.focus();
  72. // 显示提示
  73. message.success(
  74. intl.formatMessage({
  75. id: 'scanner.indicator.activated',
  76. defaultMessage: '扫码枪已就绪,请扫描二维码'
  77. })
  78. );
  79. };
  80. const isReady = !hasFocusedInput;
  81. const tooltipText = intl.formatMessage({
  82. id: isReady ? 'scanner.indicator.tooltip.ready' : 'scanner.indicator.tooltip.notReady',
  83. defaultMessage: isReady
  84. ? '扫码枪已就绪,可以扫描二维码'
  85. : '扫码枪不可用,点击此处激活'
  86. });
  87. return (
  88. <Tooltip
  89. title={tooltipText}
  90. placement="top"
  91. >
  92. <div
  93. role="button"
  94. tabIndex={0}
  95. onClick={handleClick}
  96. onKeyDown={(e) => {
  97. if (e.key === 'Enter' || e.key === ' ') {
  98. e.preventDefault();
  99. handleClick();
  100. }
  101. }}
  102. style={{
  103. display: 'inline-flex',
  104. alignItems: 'center',
  105. justifyContent: 'center',
  106. padding: '4px 15px',
  107. backgroundColor: isReady ? '#52c41a' : '#d9d9d9',
  108. border: `1px solid ${isReady ? '#52c41a' : '#d9d9d9'}`,
  109. borderRadius: '6px',
  110. color: 'white',
  111. cursor: 'pointer',
  112. transition: 'all 0.3s ease',
  113. height: '100%',
  114. minHeight: '32px',
  115. outline: 'none',
  116. userSelect: 'none',
  117. ...style
  118. }}
  119. className={className}
  120. onMouseEnter={(e) => {
  121. e.currentTarget.style.opacity = '0.85';
  122. }}
  123. onMouseLeave={(e) => {
  124. e.currentTarget.style.opacity = '1';
  125. }}
  126. >
  127. <div style={{ position: 'relative', display: 'inline-block' }}>
  128. <ScanOutlined style={{ fontSize: '18px' }} />
  129. {isReady ? (
  130. <CheckCircleOutlined
  131. style={{
  132. position: 'absolute',
  133. bottom: -2,
  134. right: -2,
  135. fontSize: '10px',
  136. color: '#52c41a',
  137. backgroundColor: 'white',
  138. borderRadius: '50%'
  139. }}
  140. />
  141. ) : (
  142. <CloseCircleOutlined
  143. style={{
  144. position: 'absolute',
  145. bottom: -2,
  146. right: -2,
  147. fontSize: '10px',
  148. color: '#ff4d4f',
  149. backgroundColor: 'white',
  150. borderRadius: '50%'
  151. }}
  152. />
  153. )}
  154. </div>
  155. </div>
  156. </Tooltip>
  157. );
  158. };
  159. export default BarcodeScannerIndicator;