今天没时间了,不做多解释; 问题现象
扫描枪写入文本肉眼可见卡顿; 了解扫描枪是模拟键盘快速输入,会触发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>