|
|
@@ -24,6 +24,7 @@ import {
|
|
|
import { getGenderOptions } from '@/domain/patient/genderOptions';
|
|
|
import { processQRCodeData, transformToFormData } from '@/domain/qrcode/qrCodeDataProcessor';
|
|
|
import BarcodeScannerIndicator from '@/components/BarcodeScannerIndicator';
|
|
|
+import * as iconv from 'iconv-lite';
|
|
|
|
|
|
interface BasicInfoFormProps {
|
|
|
style?: React.CSSProperties;
|
|
|
@@ -55,6 +56,8 @@ const BasicInfoForm: React.FC<BasicInfoFormProps> = ({
|
|
|
const scanInputRef = useRef<HTMLInputElement>(null);
|
|
|
const [isComposing, setIsComposing] = useState(false);
|
|
|
const bufferRef = useRef('');
|
|
|
+ const altCodeBufferRef = useRef(''); // 用于累积Alt+数字序列
|
|
|
+ const isAltPressedRef = useRef(false); // 跟踪Alt键状态
|
|
|
const timerRef = useRef<number | null>(null);
|
|
|
|
|
|
// 同步初始表单值到 Redux store,并生成登记号
|
|
|
@@ -166,8 +169,10 @@ const BasicInfoForm: React.FC<BasicInfoFormProps> = ({
|
|
|
console.log('[扫码枪调试] 得到的扫码数据是:', JSON.stringify(scannedText));
|
|
|
console.log('[扫码枪调试] 扫码数据长度:', scannedText.length);
|
|
|
|
|
|
+ // 解析Alt码序列
|
|
|
+ const parsedData = parseAltCodeSequence(bufferRef.current);
|
|
|
// 使用二维码数据处理器解析扫码文本(复用现有逻辑)
|
|
|
- const result = processQRCodeData(scannedText);
|
|
|
+ const result = processQRCodeData(parsedData);
|
|
|
if (!result.success) {
|
|
|
message.error(result.error || '扫码枪数据处理失败');
|
|
|
return;
|
|
|
@@ -190,6 +195,91 @@ const BasicInfoForm: React.FC<BasicInfoFormProps> = ({
|
|
|
}
|
|
|
}, [form, dispatch]);
|
|
|
|
|
|
+ // 处理Alt键释放,将GBK码转换为中文字符
|
|
|
+ const convertAltCodeToChar = (altCode: string): string => {
|
|
|
+ if (!altCode || altCode.length === 0) {
|
|
|
+ console.log('[扫码枪调试] convertAltCodeToChar: 空字符串');
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ const code = parseInt(altCode);
|
|
|
+ if (isNaN(code) || code < 0 || code > 65535) {
|
|
|
+ console.warn('[扫码枪调试] convertAltCodeToChar: 无效的数字:', altCode);
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 对于GBK双字节字符(>255),使用iconv-lite进行解码
|
|
|
+ if (code > 255) {
|
|
|
+ // 将十进制GBK码转换为字节数组
|
|
|
+ const highByte = (code >> 8) & 0xFF;
|
|
|
+ const lowByte = code & 0xFF;
|
|
|
+
|
|
|
+ console.log('[扫码枪调试] 尝试GBK解码:', {
|
|
|
+ altCode,
|
|
|
+ code,
|
|
|
+ highByte: highByte.toString(16),
|
|
|
+ lowByte: lowByte.toString(16)
|
|
|
+ });
|
|
|
+
|
|
|
+ // 确保是有效的GBK编码范围
|
|
|
+ if (highByte >= 0x81 && highByte <= 0xFE && lowByte >= 0x40 && lowByte <= 0xFE) {
|
|
|
+ const gbkBytes = new Uint8Array([highByte, lowByte]);
|
|
|
+ const decodedString = iconv.decode(gbkBytes, 'gbk');
|
|
|
+
|
|
|
+ console.log('[扫码枪调试] GBK解码成功:', altCode, `-> "${decodedString}" (charCode: ${decodedString.charCodeAt(0)})`);
|
|
|
+ return decodedString;
|
|
|
+ } else {
|
|
|
+ console.warn('[扫码枪调试] 无效的GBK编码范围:', altCode, `(0x${highByte.toString(16)}, 0x${lowByte.toString(16)})`);
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 对于单字节字符(ASCII),直接返回
|
|
|
+ const char = String.fromCharCode(code);
|
|
|
+ console.log('[扫码枪调试] ASCII转换:', altCode, `-> "${char}" (可见: ${char === '\t' ? '\\t' : char === ' ' ? '<space>' : char})`);
|
|
|
+ return char;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('[扫码枪调试] GBK解码失败:', altCode, error);
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 解析Alt码序列:将 <ALT>52962<ALT>54257... 转换为 吴玉...
|
|
|
+ const parseAltCodeSequence = (rawText: string): string => {
|
|
|
+ console.log('[扫码枪调试] 开始解析Alt码序列:', rawText);
|
|
|
+
|
|
|
+ // 使用正则表达式匹配 <ALT>后跟5位数字的模式
|
|
|
+ const altCodePattern = /<ALT>(\d{5})/g;
|
|
|
+
|
|
|
+ let result = rawText;
|
|
|
+ let match;
|
|
|
+
|
|
|
+ while ((match = altCodePattern.exec(rawText)) !== null) {
|
|
|
+ const altMarker = match[0]; // <ALT>52962
|
|
|
+ const altCode = match[1]; // 52962
|
|
|
+
|
|
|
+ // 尝试转换Alt码为字符
|
|
|
+ const convertedChar = convertAltCodeToChar(altCode);
|
|
|
+
|
|
|
+ if (convertedChar) {
|
|
|
+ // 替换 <ALT>数字 为转换后的字符
|
|
|
+ result = result.replace(altMarker, convertedChar);
|
|
|
+ console.log('[扫码枪调试] 替换Alt码:', altMarker, '->', convertedChar);
|
|
|
+ } else {
|
|
|
+ // 如果转换失败,可能Windows已经转换过了,删除<ALT>标记
|
|
|
+ result = result.replace(altMarker, '');
|
|
|
+ console.log('[扫码枪调试] 删除Alt标记:', altMarker);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理剩余的 <ALT> 标记(没有跟5位数字的)
|
|
|
+ result = result.replace(/<ALT>/g, '');
|
|
|
+
|
|
|
+ console.log('[扫码枪调试] 解析完成,结果:', result);
|
|
|
+ return result;
|
|
|
+ };
|
|
|
+
|
|
|
// 扫码枪扫码监听器:页面级 keydown 监听,自动切换焦点到隐藏input
|
|
|
useEffect(() => {
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
@@ -206,11 +296,41 @@ const BasicInfoForm: React.FC<BasicInfoFormProps> = ({
|
|
|
scanInputRef.current?.focus();
|
|
|
}
|
|
|
|
|
|
+ // 添加调试日志,记录每个按键事件
|
|
|
+ console.log('[扫码枪调试] KeyDown事件:', {
|
|
|
+ key: e.key,
|
|
|
+ code: e.code,
|
|
|
+ ctrlKey: e.ctrlKey,
|
|
|
+ altKey: e.altKey,
|
|
|
+ shiftKey: e.shiftKey,
|
|
|
+ metaKey: e.metaKey,
|
|
|
+ isEditable,
|
|
|
+ currentBuffer: bufferRef.current,
|
|
|
+ altCodeBuffer: altCodeBufferRef.current,
|
|
|
+ isAltPressed: isAltPressedRef.current
|
|
|
+ });
|
|
|
+
|
|
|
+ // 处理Alt键按下 - 将Alt作为标记累积到缓冲区
|
|
|
+ if (e.key === 'Alt') {
|
|
|
+ // 只在Alt键首次按下时添加标记,避免重复
|
|
|
+ if (!isAltPressedRef.current) {
|
|
|
+ isAltPressedRef.current = true;
|
|
|
+ bufferRef.current += '<ALT>';
|
|
|
+ console.log('[扫码枪调试] Alt键首次按下,添加标记');
|
|
|
+ } else {
|
|
|
+ console.log('[扫码枪调试] Alt键已按下,忽略重复事件');
|
|
|
+ }
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
// 扫码枪通常以 Enter 结束
|
|
|
if (e.key === 'Enter') {
|
|
|
if (bufferRef.current) {
|
|
|
processScannedData(bufferRef.current);
|
|
|
bufferRef.current = '';
|
|
|
+ altCodeBufferRef.current = '';
|
|
|
if (scanInputRef.current) {
|
|
|
scanInputRef.current.value = '';
|
|
|
}
|
|
|
@@ -223,29 +343,48 @@ const BasicInfoForm: React.FC<BasicInfoFormProps> = ({
|
|
|
e.preventDefault();
|
|
|
e.stopPropagation();
|
|
|
|
|
|
- // 手动将Tab字符插入到隐藏input中
|
|
|
+ // 直接添加到缓冲区,不依赖DOM输入
|
|
|
+ bufferRef.current += '\t';
|
|
|
+
|
|
|
+ // 同时更新隐藏input的值(用于中文输入兼容性)
|
|
|
const input = scanInputRef.current;
|
|
|
if (input) {
|
|
|
- const start = input.selectionStart || 0;
|
|
|
- const end = input.selectionEnd || 0;
|
|
|
- const value = input.value;
|
|
|
-
|
|
|
- // 在光标位置插入Tab字符
|
|
|
- input.value = value.substring(0, start) + '\t' + value.substring(end);
|
|
|
-
|
|
|
- // 更新光标位置
|
|
|
- input.selectionStart = input.selectionEnd = start + 1;
|
|
|
-
|
|
|
- // 手动触发input事件,确保React能检测到变化
|
|
|
+ input.value = bufferRef.current;
|
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
}
|
|
|
+ } else if (/^\d$/.test(e.key) || /^[a-zA-Z]$/.test(e.key)) {
|
|
|
+ // 捕获所有数字和字母键(无论是Numpad还是Digit,无论Alt键状态)
|
|
|
+ bufferRef.current += e.key;
|
|
|
+ console.log('[扫码枪调试] 累积字符:', e.key, '当前缓冲区:', bufferRef.current);
|
|
|
+
|
|
|
+ // 阻止默认行为,避免Windows自动转换Alt码
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ } else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
|
|
|
+ // 其他可打印字符(如空格、标点等)
|
|
|
+ bufferRef.current += e.key;
|
|
|
+ console.log('[扫码枪调试] 累积字符:', e.key, '当前缓冲区:', bufferRef.current);
|
|
|
+
|
|
|
+ // 阻止默认行为
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ }
|
|
|
+ // 对于其他控制键(如Shift、Ctrl等),不阻止,让它们正常工作
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleKeyUp = (e: KeyboardEvent) => {
|
|
|
+ if (e.key === 'Alt') {
|
|
|
+ console.log('[扫码枪调试] Alt键释放');
|
|
|
+ isAltPressedRef.current = false;
|
|
|
}
|
|
|
- // 其他字符不阻止,让它们自然输入到隐藏input中
|
|
|
- // input会自动过滤掉无效字符,只保留有效的文本
|
|
|
};
|
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
- return () => window.removeEventListener('keydown', handleKeyDown);
|
|
|
+ window.addEventListener('keyup', handleKeyUp);
|
|
|
+ return () => {
|
|
|
+ window.removeEventListener('keydown', handleKeyDown);
|
|
|
+ window.removeEventListener('keyup', handleKeyUp);
|
|
|
+ };
|
|
|
}, [processScannedData]);
|
|
|
|
|
|
/** input 事件:真正可靠地拿中文 */
|
|
|
@@ -745,6 +884,7 @@ const BasicInfoForm: React.FC<BasicInfoFormProps> = ({
|
|
|
{/* 扫码枪兜底 input(隐藏) */}
|
|
|
<input
|
|
|
ref={scanInputRef}
|
|
|
+ data-testid="scancode-input"
|
|
|
type="text"
|
|
|
style={{
|
|
|
position: 'fixed',
|