yangg 2 долоо хоног өмнө
parent
commit
6508de7b14

+ 386 - 65
src/views/system/borrow/component/CollectEquipment/index.vue

@@ -215,7 +215,20 @@
                 <el-row :gutter="16">
                 <el-row :gutter="16">
                     <el-col :span="24">
                     <el-col :span="24">
                         <div style="margin: 8px 0">
                         <div style="margin: 8px 0">
-                            <el-input ref="searchInputRef" v-model="searchText" placeholder="请扫描设备条码添加" style="width: 300px" @keyup.enter.prevent="handleScanSearch" />
+                            <textarea
+                                ref="searchInputRef"
+                                v-model="searchText"
+                                class="device-code-input"
+                                placeholder="设备编号(支持扫码枪/RFID批量录入)"
+                                :disabled="loading"
+                                autocomplete="off"
+                                rows="1"
+                                style="width: 300px; vertical-align: top;"
+                                @input="handleDeviceInput"
+                                @keydown="handleRFIDKeydown"
+                                @blur="handleDeviceBlur"
+                                @keydown.enter.prevent="handleDeviceEnter"
+                            ></textarea>
                             <el-button type="primary" style="margin-left: 16px" @click="handleAdd">手动借用</el-button>
                             <el-button type="primary" style="margin-left: 16px" @click="handleAdd">手动借用</el-button>
                         </div>
                         </div>
                         <el-table :data="deviceList" border style="width: 100%">
                         <el-table :data="deviceList" border style="width: 100%">
@@ -412,7 +425,7 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { ref, watch, defineProps, defineEmits, onMounted, computed, nextTick } from 'vue';
+import { ref, watch, defineProps, defineEmits, onMounted, onBeforeUnmount, computed, nextTick } from 'vue';
 import SelectDeviceDialog from './SelectDeviceDialog/index.vue';
 import SelectDeviceDialog from './SelectDeviceDialog/index.vue';
 import SettlementDialog from './SettlementDialog.vue';
 import SettlementDialog from './SettlementDialog.vue';
 import AbnormalDialog from './AbnormalDialog/index.vue';
 import AbnormalDialog from './AbnormalDialog/index.vue';
@@ -679,8 +692,23 @@ const searchText = ref('');
 const deviceList = ref<DeviceListItem[]>([]);
 const deviceList = ref<DeviceListItem[]>([]);
 const accessoryInfo = ref('');
 const accessoryInfo = ref('');
 const remark = ref('');
 const remark = ref('');
-// 领取模式下的“实借配件信息”本地输入,不回显历史
+// 领取模式下的"实借配件信息"本地输入,不回显历史
 const collectAccessories = ref('');
 const collectAccessories = ref('');
+// RFID高速扫码缓冲相关
+const rfidInputBuffer = ref('');
+let rfidInputTimer: ReturnType<typeof setTimeout> | null = null;
+const loading = ref(false);
+// 记录最后一次输入时间,用于检测是否是高速扫码输入
+let lastInputTime = 0;
+// 记录连续输入次数,用于判断是否是扫码输入
+let consecutiveInputCount = 0;
+// RFID读取规则配置(简化版,可根据需要扩展)
+const readRuleConfig = ref({
+  enableRFIDBuffer: true, // 是否启用RFID缓冲
+  rfidInputTimeout: 120, // RFID缓冲判定时间(ms)
+  separator: ' ', // 分隔符
+  minInputSpeed: 30, // 最小输入速度(ms),低于此值认为是扫码输入
+});
 
 
 // 归还设备相关数据
 // 归还设备相关数据
 const returnSearchText = ref('');
 const returnSearchText = ref('');
@@ -910,65 +938,327 @@ const handleViewAbnormal = (index: number) => {
   }
   }
 };
 };
 
 
-// 处理扫码搜索
+// 清理RFID输入定时器
+const clearRFIDInputTimer = () => {
+  if (rfidInputTimer) {
+    clearTimeout(rfidInputTimer);
+    rfidInputTimer = null;
+  }
+};
+
+// 提交RFID缓冲区内容
+const submitRFIDBuffer = () => {
+  if (!readRuleConfig.value.enableRFIDBuffer) return;
+  if (!rfidInputBuffer.value) return;
+  const bufferedValue = rfidInputBuffer.value;
+  rfidInputBuffer.value = '';
+  clearRFIDInputTimer();
+  consecutiveInputCount = 0; // 重置计数
+  lastInputTime = 0; // 重置时间
+  searchText.value = bufferedValue;
+  handleScanSearch();
+};
+
+// 调度RFID缓冲区提交(延迟提交)
+const scheduleRFIDBufferSubmit = () => {
+  if (!readRuleConfig.value.enableRFIDBuffer) return;
+  clearRFIDInputTimer();
+  const delay = Math.max(readRuleConfig.value.rfidInputTimeout || 120, 30);
+  rfidInputTimer = setTimeout(() => {
+    submitRFIDBuffer();
+  }, delay);
+};
+
+// 处理设备输入(用于同步 textarea 内容到缓冲区,作为备用方案防止第一个字符丢失)
+const handleDeviceInput = () => {
+  // 如果 textarea 有内容但缓冲区为空,可能是第一个字符通过默认行为输入了
+  // 这种情况发生在:第一个字符没有被拦截(因为判断逻辑),但后续字符是高速输入
+  if (rfidInputBuffer.value === '' && searchText.value && searchText.value.length > 0) {
+    // 同步到缓冲区,确保内容不丢失
+    rfidInputBuffer.value = searchText.value;
+    // 如果内容长度大于1,说明已经有多个字符,可能是高速输入
+    if (searchText.value.length > 1) {
+      lastInputTime = Date.now();
+      consecutiveInputCount = searchText.value.length;
+    } else {
+      // 单个字符,初始化时间戳
+      lastInputTime = Date.now();
+      consecutiveInputCount = 1;
+    }
+  } else if (rfidInputBuffer.value && searchText.value !== rfidInputBuffer.value) {
+    // 如果缓冲区有内容但 textarea 内容不同,同步缓冲区内容到 textarea
+    // 这确保显示的内容与缓冲区一致
+    searchText.value = rfidInputBuffer.value;
+  }
+};
+
+// 检测是否是高速扫码输入(通过输入速度判断)
+const isHighSpeedInput = (): boolean => {
+  const now = Date.now();
+  const timeSinceLastInput = lastInputTime > 0 ? now - lastInputTime : 0;
+  lastInputTime = now;
+  
+  // 如果是第一次输入(lastInputTime 之前为 0),先假设可能是高速输入
+  // 等待第二个字符来判断
+  if (timeSinceLastInput === 0) {
+    return false; // 第一次输入,暂时返回 false,但会在 handleRFIDKeydown 中特殊处理
+  }
+  
+  // 如果输入间隔很短(小于配置的最小输入速度),认为是高速扫码
+  if (timeSinceLastInput < readRuleConfig.value.minInputSpeed && timeSinceLastInput > 0) {
+    consecutiveInputCount++;
+    return true;
+  }
+  
+  // 如果输入间隔较长,重置计数
+  if (timeSinceLastInput > 200) {
+    consecutiveInputCount = 0;
+    // 如果之前有缓冲内容,先提交
+    if (rfidInputBuffer.value) {
+      submitRFIDBuffer();
+    }
+  }
+  
+  return false;
+};
+
+// 处理RFID键盘输入(高速扫码)
+const handleRFIDKeydown = (event: KeyboardEvent) => {
+  // 如果未启用RFID缓冲,直接返回,不拦截
+  if (!readRuleConfig.value.enableRFIDBuffer) {
+    return;
+  }
+  
+  // 不拦截修饰键组合(如 Ctrl+C, Ctrl+V 等)
+  if (event.ctrlKey || event.metaKey || event.altKey) {
+    return;
+  }
+  
+  // 处理回车键
+  if (event.key === 'Enter') {
+    // 如果有缓冲内容,提交缓冲
+    if (rfidInputBuffer.value) {
+      event.preventDefault();
+      submitRFIDBuffer();
+    }
+    // 否则让默认行为处理(触发 handleDeviceEnter)
+    return;
+  }
+  
+  // 处理退格键
+  if (event.key === 'Backspace') {
+    // 如果有缓冲内容,处理缓冲
+    if (rfidInputBuffer.value) {
+      event.preventDefault();
+      rfidInputBuffer.value = rfidInputBuffer.value.slice(0, -1);
+      searchText.value = rfidInputBuffer.value;
+    }
+    // 否则让默认行为处理
+    return;
+  }
+  
+  // 处理普通字符输入
+  if (event.key.length === 1) {
+    // 检测是否是高速扫码输入
+    const isHighSpeed = isHighSpeedInput();
+    
+    // 判断是否是第一次输入(缓冲区为空且计数为0且时间戳为0)
+    const isFirstInput = rfidInputBuffer.value === '' && consecutiveInputCount === 0;
+    
+    // 如果是高速输入、已经有缓冲内容、或者第一次输入,使用缓冲模式
+    // 第一次输入时,先捕获到缓冲区,等待第二个字符来判断是否是高速输入
+    if (isHighSpeed || rfidInputBuffer.value || consecutiveInputCount > 0 || isFirstInput) {
+      event.preventDefault();
+      event.stopPropagation(); // 阻止事件冒泡,避免影响其他元素
+      rfidInputBuffer.value += event.key;
+      searchText.value = rfidInputBuffer.value;
+      
+      // 如果是第一次输入,初始化时间戳
+      if (isFirstInput) {
+        lastInputTime = Date.now();
+        consecutiveInputCount = 1;
+      } else {
+        consecutiveInputCount++;
+      }
+      
+      scheduleRFIDBufferSubmit();
+    } else {
+      // 如果是正常输入且之前有缓冲内容,先提交缓冲内容
+      if (rfidInputBuffer.value) {
+        submitRFIDBuffer();
+        // 提交后,当前字符走默认行为(正常输入)
+        // 注意:这里不阻止默认行为,让字符正常输入到 textarea
+      }
+      // 否则让默认行为处理(正常输入)
+    }
+  }
+};
+
+// 处理输入框失焦
+const handleDeviceBlur = (event: FocusEvent) => {
+  // 如果焦点转移到其他输入元素(如表单输入框),不自动聚焦
+  const relatedTarget = event.relatedTarget as HTMLElement;
+  if (relatedTarget && (
+    relatedTarget.tagName === 'INPUT' || 
+    relatedTarget.tagName === 'TEXTAREA' ||
+    relatedTarget.closest('.el-input') ||
+    relatedTarget.closest('.el-textarea') ||
+    relatedTarget.closest('form')
+  )) {
+    // 如果有缓冲内容,先提交
+    if (rfidInputBuffer.value) {
+      submitRFIDBuffer();
+    }
+    return;
+  }
+  
+  // 其他情况(如点击按钮等),延迟后自动聚焦
+  setTimeout(() => {
+    if (searchInputRef.value && !loading.value) {
+      searchInputRef.value.focus();
+    }
+  }, 10);
+};
+
+// 处理回车键
+const handleDeviceEnter = (event: KeyboardEvent) => {
+  if (event.shiftKey) return;
+  if (readRuleConfig.value.enableRFIDBuffer) {
+    event.preventDefault();
+    submitRFIDBuffer();
+    return;
+  }
+  event.preventDefault();
+  handleScanSearch();
+};
+
+// 分割多个设备编号(支持分隔符)
+const splitDeviceCodes = (input: string): string[] => {
+  const trimmed = input.trim();
+  if (!trimmed) return [];
+  
+  const { separator } = readRuleConfig.value;
+  
+  // 优先处理换行符
+  if (/[\r\n]/.test(trimmed)) {
+    return trimmed.split(/[\r\n]+/).filter(code => code.trim());
+  }
+  
+  // 处理配置的分隔符
+  if (separator && separator.trim() !== '') {
+    const escapedSeparator = separator.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+    const separatorPattern = new RegExp(escapedSeparator + '+', 'g');
+    return trimmed.split(separatorPattern).filter(code => code.trim());
+  }
+  
+  // 处理空格分隔
+  if (/\s/.test(trimmed)) {
+    return trimmed.split(/\s+/).filter(code => code.trim());
+  }
+  
+  // 单个设备编号
+  return [trimmed];
+};
+
+// 处理扫码搜索(支持批量)
 const handleScanSearch = async (event?: Event) => {
 const handleScanSearch = async (event?: Event) => {
     // 阻止默认行为,防止表单提交导致页面刷新
     // 阻止默认行为,防止表单提交导致页面刷新
     if (event) {
     if (event) {
         event.preventDefault();
         event.preventDefault();
         event.stopPropagation();
         event.stopPropagation();
     }
     }
+    
+    // 如果启用了RFID缓冲,先提交缓冲区
+    if (readRuleConfig.value.enableRFIDBuffer && rfidInputBuffer.value) {
+      const bufferedValue = rfidInputBuffer.value;
+      rfidInputBuffer.value = '';
+      clearRFIDInputTimer();
+      searchText.value = bufferedValue;
+    }
+    
     if (!searchText.value.trim()) return;
     if (!searchText.value.trim()) return;
     
     
+    // 分割多个设备编号
+    const codes = splitDeviceCodes(searchText.value);
+    if (codes.length === 0) return;
+    
+    loading.value = true;
+    let successCount = 0;
+    let failCount = 0;
+    
     try {
     try {
-        const res = await deviceApi.GetList({ 
-            page: 1, 
-            limit: 1, 
-            code: searchText.value.trim(),
-            status: 1 
-        });
-        
-        if (res.data && res.data.length > 0) {
-            const device = res.data[0];
-            if (device.available_quantity <= 0) {
-                ElMessage.warning('该设备当前无可用数量');
-                // 重新聚焦输入框
-                nextTick(() => {
-                    searchInputRef.value?.focus?.();
-                });
-                return;
-            }
-            
-            // 检查是否已存在
-            const existingDevice = deviceList.value.find(d => d.device_no === device.id);
-            if (existingDevice) {
-                existingDevice.borrow_count++;
-                ElMessage.success('设备数量已更新');
-            } else {
-                // 添加新设备到列表
-                deviceList.value.push({
-                    device_no: device.id,
-                    device_code: device.code,
-                    device_type: device.category_name,
-                    device_name: device.name || '',
-                    borrow_count: 1,
-                    brand: device.brand ||device.device_brand || '',
-                    device_specification: device.specification,
-                    model: device.model || '',
-                    warehouse: device.warehouse,
-                    available_quantity:device.available_quantity
-                });
-                ElMessage.success('设备已添加到列表');
-            }
-        } else {
-            ElMessage.warning('未找到对应的设备');
+      // 批量处理设备编号
+      for (const code of codes) {
+        try {
+          const res = await deviceApi.GetList({ 
+              page: 1, 
+              limit: 1, 
+              code: code.trim(),
+              status: 1 
+          });
+          
+          if (res.data && res.data.length > 0) {
+              const device = res.data[0];
+              if (device.available_quantity <= 0) {
+                  ElMessage.warning(`设备 ${code} 当前无可用数量`);
+                  failCount++;
+                  continue;
+              }
+              
+              // 检查是否已存在
+              const existingDevice = deviceList.value.find(d => d.device_no === device.id);
+              if (existingDevice) {
+                  existingDevice.borrow_count++;
+                  successCount++;
+              } else {
+                  // 添加新设备到列表
+                  deviceList.value.push({
+                      device_no: device.id,
+                      device_code: device.code,
+                      device_type: device.category_name,
+                      device_name: device.name || '',
+                      borrow_count: 1,
+                      brand: device.brand ||device.device_brand || '',
+                      device_specification: device.specification,
+                      model: device.model || '',
+                      warehouse: device.warehouse,
+                      available_quantity:device.available_quantity
+                  });
+                  successCount++;
+              }
+          } else {
+              ElMessage.warning(`未找到设备编号: ${code}`);
+              failCount++;
+          }
+        } catch (error) {
+          console.error(`处理设备编号 ${code} 失败:`, error);
+          failCount++;
+        }
+      }
+      
+      // 显示批量处理结果
+      if (codes.length > 1) {
+        if (successCount > 0 && failCount === 0) {
+          ElMessage.success(`成功添加 ${successCount} 个设备`);
+        } else if (successCount > 0 && failCount > 0) {
+          ElMessage.warning(`成功添加 ${successCount} 个设备,失败 ${failCount} 个`);
+        } else if (failCount > 0) {
+          ElMessage.error(`添加失败,共 ${failCount} 个设备编号`);
         }
         }
+      } else if (successCount > 0) {
+        ElMessage.success('设备已添加到列表');
+      }
     } catch (error) {
     } catch (error) {
         ElMessage.error('搜索设备失败');
         ElMessage.error('搜索设备失败');
     } finally {
     } finally {
         searchText.value = ''; // 清空搜索框
         searchText.value = ''; // 清空搜索框
+        loading.value = false;
         // 重新聚焦输入框,保持选中状态
         // 重新聚焦输入框,保持选中状态
         nextTick(() => {
         nextTick(() => {
-            searchInputRef.value?.focus?.();
+            if (searchInputRef.value) {
+              searchInputRef.value.focus();
+              searchInputRef.value.select();
+            }
         });
         });
     }
     }
 };
 };
@@ -1010,28 +1300,14 @@ const handleReturnScanSearch = async (event?: Event) => {
             // 检查是否已存在
             // 检查是否已存在
             const existingDeviceIndex = returnDeviceList.value.findIndex(d => d.device_no === device.id);
             const existingDeviceIndex = returnDeviceList.value.findIndex(d => d.device_no === device.id);
             if (existingDeviceIndex !== -1) {
             if (existingDeviceIndex !== -1) {
-                // 如果设备已存在,更新其状态return_status.label = '已归还';
+                // 如果设备已存在,更新其状态
                 returnDeviceList.value[existingDeviceIndex].return_status = normalizedStatus;
                 returnDeviceList.value[existingDeviceIndex].return_status = normalizedStatus;
                 returnDeviceList.value[existingDeviceIndex].is_return = true;
                 returnDeviceList.value[existingDeviceIndex].is_return = true;
                 returnDeviceList.value[existingDeviceIndex].return_time = dayjs().format('YYYY-MM-DD HH:mm:ss');
                 returnDeviceList.value[existingDeviceIndex].return_time = dayjs().format('YYYY-MM-DD HH:mm:ss');
                 ElMessage.success(`设备 ${device.category_name} 状态已更新为已归还`);
                 ElMessage.success(`设备 ${device.category_name} 状态已更新为已归还`);
             } else {
             } else {
-                // 添加新设备到列表
-                returnDeviceList.value.push({
-                    device_no: device.id,
-                    device_code: device.code,
-                    device_type: device.category_name,
-                    device_name: device.name || '',
-                    borrow_count: 1,
-                    brand: device.brand||device.device_brand || '',
-                    model: device.model || '',
-                    warehouse: device.warehouse,
-                    return_status: normalizedStatus,
-                    is_return: true,
-                    return_time: device.return_times || dayjs().format('YYYY-MM-DD HH:mm:ss')
-                });
-                console.log("returnDeviceList.value:::",returnDeviceList.value);
-                ElMessage.success('设备已添加到列表');
+                // 设备不存在于归还列表中,提示用户
+                ElMessage.warning(`设备 ${device.code || device.category_name} 不在当前归还列表中,请先通过手动归还添加该设备`);
             }
             }
         } else {
         } else {
             ElMessage.warning('未找到对应的设备');
             ElMessage.warning('未找到对应的设备');
@@ -1234,7 +1510,10 @@ function handleClick(tab: TabsPaneContext) {
   // 当切换到设备清单标签页时,自动聚焦输入框
   // 当切换到设备清单标签页时,自动聚焦输入框
   if (tab.props.name === 'second') {
   if (tab.props.name === 'second') {
     nextTick(() => {
     nextTick(() => {
-      searchInputRef.value?.focus?.();
+      if (searchInputRef.value) {
+        searchInputRef.value.focus();
+        searchInputRef.value.select();
+      }
     });
     });
   } else if (tab.props.name === 'third') {
   } else if (tab.props.name === 'third') {
     // 当切换到归还设备清单标签页时,自动聚焦输入框
     // 当切换到归还设备清单标签页时,自动聚焦输入框
@@ -1352,7 +1631,10 @@ watch(
 		// 当对话框打开且当前标签页是设备清单时,自动聚焦输入框
 		// 当对话框打开且当前标签页是设备清单时,自动聚焦输入框
 		if (v && activeName.value === 'second' && isCollect.value) {
 		if (v && activeName.value === 'second' && isCollect.value) {
 			nextTick(() => {
 			nextTick(() => {
-				searchInputRef.value?.focus?.();
+				if (searchInputRef.value) {
+					searchInputRef.value.focus();
+					searchInputRef.value.select();
+				}
 			});
 			});
 		} else if (v && activeName.value === 'third' && !isView.value) {
 		} else if (v && activeName.value === 'third' && !isView.value) {
 			// 当对话框打开且当前标签页是归还设备清单时,自动聚焦输入框
 			// 当对话框打开且当前标签页是归还设备清单时,自动聚焦输入框
@@ -1360,6 +1642,13 @@ watch(
 				returnSearchInputRef.value?.focus?.();
 				returnSearchInputRef.value?.focus?.();
 			});
 			});
 		}
 		}
+		// 清理RFID缓冲状态
+		if (v) {
+			rfidInputBuffer.value = '';
+			clearRFIDInputTimer();
+			consecutiveInputCount = 0;
+			lastInputTime = 0;
+		}
 	}
 	}
 );
 );
 
 
@@ -1585,6 +1874,11 @@ function onCancel() {
     selectedDevices.value = [];
     selectedDevices.value = [];
     // 清理附件文件列表
     // 清理附件文件列表
     attachmentFileList.value = [];
     attachmentFileList.value = [];
+    // 清理RFID缓冲
+    rfidInputBuffer.value = '';
+    clearRFIDInputTimer();
+    consecutiveInputCount = 0;
+    lastInputTime = 0;
     // 关闭所有对话框
     // 关闭所有对话框
     showSelectDeviceDialog.value = false;
     showSelectDeviceDialog.value = false;
     showSettlementDialog.value = false;
     showSettlementDialog.value = false;
@@ -1733,6 +2027,14 @@ function clickReturn(){
   }
   }
 }
 }
 
 
+// 组件卸载时清理定时器和状态
+onBeforeUnmount(() => {
+  clearRFIDInputTimer();
+  rfidInputBuffer.value = '';
+  consecutiveInputCount = 0;
+  lastInputTime = 0;
+});
+
 	/* formRef.value.validate((valid: boolean) => {
 	/* formRef.value.validate((valid: boolean) => {
 		if (valid) {
 		if (valid) {
 			form.value.items = form.value.items.map(item => ({
 			form.value.items = form.value.items.map(item => ({
@@ -1776,4 +2078,23 @@ function clickReturn(){
     height: 178px;
     height: 178px;
     display: block;
     display: block;
   }
   }
+  .device-code-input {
+    padding: 8px 12px;
+    font-size: 14px;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    outline: none;
+    resize: vertical;
+    transition: all 0.3s;
+    font-family: 'Microsoft YaHei', sans-serif;
+    line-height: 1.5;
+  }
+  .device-code-input:focus {
+    border-color: #409EFF;
+    box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+  }
+  .device-code-input:disabled {
+    background-color: #f5f7fa;
+    cursor: not-allowed;
+  }
   </style>
   </style>