扫码枪卡顿有效解决方案

今天没时间了,不做多解释; 问题现象

扫描枪写入文本肉眼可见卡顿; 了解扫描枪是模拟键盘快速输入,会触发key/Down和Input事件,双向绑定和input事件,没输入一个字母,设计双向绑定和渲染,输入太快,来不及渲染,所以卡顿;

核心思路,降低input事件触发频次,降低渲染,在keydown中获取判断是扫描还是手工输入,如果是扫描,则拼接字符串,然后后再更新文本,否额常规输入;

卡的原因还包括,输入法的,拼写校验、自动完成等等,实际上再扫码过程,这些辅助性内容都是干扰项;

拿去绝对好使用,钱前后端花费好几天时间,我尝试原生html input,能好一点点,cpu低配工控机,仍然卡顿;

ScanInput.vue

xml 复制代码
<template>
  <span class="scan-search-input" :class="getSizeClass">
    <!-- Prefix Slot -->
    <span v-if="$slots.prefix" class="scan-search-input__prefix">
      <slot name="prefix"></slot>
    </span>

    <!-- Input -->
    <input
      ref="inputRef"
      v-model="localValue"
      v-bind="$attrs"
      class="scan-search-input__input"
      autocomplete="off"
      autocorrect="off"
      autocapitalize="off"
      spellcheck="false"
      inputmode="text"
      @focus="onFocus"
      @blur="onBlur"
      @keydown="onKeydown"
      @input="onManualInput"
    />

    <!-- Suffix Area -->
    <span v-if="showSuffix" class="scan-search-input__suffix">
      <!-- Clear Button -->
      <span
        v-if="props.allowClear && localValue"
        class="scan-search-input__clear"
        @click.stop="handleClear"
      >
        <svg
          focusable="false"
          data-icon="close-circle"
          width="1em"
          height="1em"
          fill="currentColor"
          aria-hidden="true"
          fill-rule="evenodd"
          viewBox="64 64 896 896"
        >
          <path
            d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm127.98 274.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z"
          />
        </svg>
      </span>

      <!-- Enter Button (only if no custom suffix) -->
      <button
        v-if="hasEnterButton && !$slots.suffix"
        type="button"
        class="scan-search-input__btn"
        :disabled="props.loading"
        @click="handlePressEnter"
      >
        {{ props.enterButton === true ? '搜索' : props.enterButton }}
      </button>

      <!-- Custom Suffix Slot -->
      <slot v-else-if="$slots.suffix" name="suffix"></slot>
    </span>
  </span>
</template>

<script setup>
  import { ref, watch, nextTick, computed, useSlots, onMounted } from 'vue';

  const props = defineProps({
    value: { type: String, default: '' },
    scanSeparator: { type: String, default: '' },
    allowClear: { type: Boolean, default: false },
    enterButton: { type: [Boolean, String], default: false },
    loading: { type: Boolean, default: false },
    size: { type: String, default: 'middle' }, // 'small' | 'middle' | 'large'
    autoFocus: { type: Boolean, default: false },
  });

  const emit = defineEmits(['update:value', 'pressEnter']);

  const localValue = ref(props.value);
  watch(
    () => props.value,
    (val) => {
      localValue.value = val;
    },
  );

  const slots = useSlots();

  // 判断是否显示右侧区域
  const hasEnterButton = computed(() => !!props.enterButton);
  const showSuffix = computed(() => props.allowClear || hasEnterButton.value || !!slots.suffix);

  const inputRef = ref(null);

  // ========== 扫码逻辑 ==========
  let scanBuffer = '';
  let lastKeyTime = 0;
  let scanTimeout = null;
  let isScanning = false;
  // 扫码结束后延迟 150ms 提交(更快响应)
  const SCAN_END_DELAY = 200;
  // 启动扫码:前两个字符间隔需 <60ms
  const QUICK_INPUT_THRESHOLD = 60;

  function onManualInput(e) {
    if (isScanning) return;
    emit('update:value', e.target.value);
  }

  function onKeydown(e) {
    const now = Date.now();
    const timeDiff = now - lastKeyTime;
    lastKeyTime = now;

    if (e.ctrlKey || e.altKey || e.metaKey) return;

    const key = e.key;
    console.info('timeDiff', timeDiff, key);

    if (key === 'Enter') {
      if (isScanning && scanBuffer) {
        // 扫码中按回车:先应用扫码结果,再触发 pressEnter
        applyScanResult();
        e.preventDefault();
        // 触发 pressEnter,event 为 null 表示非用户交互触发
        nextTick(() => {
          emit('pressEnter', e);
        });
      } else {
        // 手动输入按回车
        handlePressEnter(e);
      }
      return;
    }

    // key !== 1 显示排除 非打印字符
    if (key.length !== 1 || key < ' ' || key > '~') {
      clearScanState();
      return;
    }

    if (timeDiff < QUICK_INPUT_THRESHOLD || isScanning) {
      isScanning = true;
      scanBuffer += key;
      e.preventDefault();

      clearTimeout(scanTimeout);
      scanTimeout = setTimeout(() => {
        if (isScanning && scanBuffer) {
          applyScanResult();
          // 注意:自动扫码完成(无回车)通常不触发 pressEnter
          // 如果你也希望自动触发,请在这里加 emit
        }
      }, SCAN_END_DELAY);
    } else {
      clearScanState();
    }
  }

  function applyScanResult() {
    const scannedPart = scanBuffer;
    clearScanState();

    const currentValue = localValue.value || '';
    const newValue = currentValue ? currentValue + props.scanSeparator + scannedPart : scannedPart;

    localValue.value = newValue;

    nextTick(() => {
      emit('update:value', newValue);
    });

    inputRef.value?.focus();
  }

  function clearScanState() {
    isScanning = false;
    scanBuffer = '';
    clearTimeout(scanTimeout);
  }

  function onBlur() {
    if (isScanning) {
      clearScanState();
    }
  }

  function onFocus() {
    // 可扩展
  }

  function handlePressEnter(e) {
    if (props.loading) return;
    emit('pressEnter', e);
  }

  function handleClear() {
    localValue.value = '';
    emit('update:value', '');
    nextTick(() => {
      inputRef.value?.focus();
    });
  }

  // 尺寸类
  const getSizeClass = computed(() => {
    return `scan-search-input--${props.size}`;
  });

  // 自动聚焦(仅当 props.autoFocus 为 true)
  onMounted(() => {
    if (props.autoFocus) {
      // 使用 nextTick 确保 DOM 已渲染
      nextTick(() => {
        inputRef.value?.focus();
      });
    }
  });

  // 暴露 focus 方法供父组件调用
  defineExpose({
    focus: () => inputRef.value?.focus(),
  });
</script>

<style lang="less" scoped>
  .scan-search-input {
    display: flex;
    align-items: center;
    width: 100%;
    padding: 4px 11px;
    transition: all 0.3s;
    border: 1px solid #d9d9d9;
    border-radius: 2px;
    background-color: #fff;
    font-size: 14px;
  }

  .scan-search-input:hover,
  .scan-search-input:focus-within {
    border-color: @primary-color;
    box-shadow: 0 0 0 2px fade(@primary-color, 20%);
  }

  .scan-search-input--large {
    padding: 6.5px 11px;
    font-size: 14px;
  }

  .scan-search-input--small {
    padding: 0 7px;
    font-size: 14px;
  }

  .scan-search-input__prefix {
    margin-right: 2px;
    margin-left: 0;
    color: rgb(0 0 0 / 65%);
    line-height: 1;
  }

  .scan-search-input__input {
    flex: 1;
    min-width: 0;
    padding: 4px;
    border: none;
    outline: none;
    background: transparent;
    color: rgb(0 0 0 / 88%);
  }

  .scan-search-input__input::placeholder {
    color: #bfbfbf;
  }

  .scan-search-input__suffix {
    display: flex;
    align-items: center;
    margin-left: 8px;
  }

  .scan-search-input__clear {
    margin-right: 10px;
    color: rgb(0 0 0 / 25%);
    font-size: 10px;
    line-height: 1;
    cursor: pointer;
  }

  .scan-search-input__clear:hover {
    color: rgb(0 0 0 / 45%);
  }

  .scan-search-input__btn {
    display: none;
    height: 32px;
    margin-left: 8px;
    padding: 0 15px;
    transition: all 0.3s;
    border: none;
    border-radius: 0 2px 2px 0;
    background-color: @primary-color;
    color: #fff;
    cursor: pointer;
  }

  .scan-search-input--large .scan-search-input__btn {
    height: 40px;
  }

  .scan-search-input--small .scan-search-input__btn {
    height: 24px;
  }

  .scan-search-input__btn:hover:not(:disabled) {
    background-color: fade(@primary-color, 90%);
  }

  .scan-search-input__btn:disabled {
    background-color: #d9d9d9;
    cursor: not-allowed;
  }
</style>

业务使用ScanInput,之前是antd Input组件

ini 复制代码
  <ScanInput
            class="h-10 py-0"
            ref="barCodeRef"
            placeholder="请扫描或输入产品条码号,按回车键提交"
            size="large"
            allowClear
            autofocus
            :enterButton="true"
            v-model:value="barCode"
            @press-enter="debounceHandleSubmit"
          >
            <template #prefix>
              <Icon icon="ant-design:scan-outlined" />
            </template>
          </ScanInput>
相关推荐
VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vue校园社团管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·课程设计
ChangYan.2 小时前
直接下载源码但是执行npm run compile后报错
前端·npm·node.js
skywalk81632 小时前
在 FreeBSD 上可以使用的虚拟主机(Web‑Hosting)面板
前端·主机·webmin
ohyeah3 小时前
深入理解 React 中的 useRef:不只是获取 DOM 元素
前端·react.js
MoXinXueWEB3 小时前
前端页面获取不到url上参数值
前端
低保和光头哪个先来3 小时前
场景6:对浏览器内核的理解
开发语言·前端·javascript·vue.js·前端框架
想要一只奶牛猫4 小时前
Spring Web MVC(三)
前端·spring·mvc
奋飛4 小时前
微前端系列:核心概念、价值与应用场景
前端·微前端·micro·mfe·什么是微前端
进击的野人5 小时前
Vue Router 深度解析:从基础概念到高级应用实践
前端·vue.js·前端框架
北慕阳5 小时前
健康管理前端记录
前端