250 lines
6.0 KiB
Vue
250 lines
6.0 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="mac-address-input" :class="{ disabled: disabled }">
|
|||
|
|
<template v-for="n in 6" :key="n">
|
|||
|
|
<el-input
|
|||
|
|
ref="inputRefs"
|
|||
|
|
v-model="segments[n - 1]"
|
|||
|
|
type="text"
|
|||
|
|
maxlength="2"
|
|||
|
|
:disabled="disabled"
|
|||
|
|
@input="handleInput(n - 1)"
|
|||
|
|
@keydown="handleKeydown(n - 1,$event)"
|
|||
|
|
@focus="handleFocus(n - 1)"
|
|||
|
|
@blur="handleBlur"
|
|||
|
|
@paste="handlePaste(n - 1, $event)"
|
|||
|
|
/>
|
|||
|
|
<span v-if="n < 6" class="separator">:</span>
|
|||
|
|
</template>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, watch, nextTick } from 'vue'
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
modelValue?: string
|
|||
|
|
disabled?: boolean
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface Emits {
|
|||
|
|
(e: 'update:modelValue', value: string): void
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = withDefaults(defineProps<Props>(), {
|
|||
|
|
modelValue: '',
|
|||
|
|
disabled: false
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits<Emits>()
|
|||
|
|
|
|||
|
|
// 创建6个输入框的引用
|
|||
|
|
const inputRefs = ref<InstanceType<typeof import('element-plus').ElInput>[]>([])
|
|||
|
|
|
|||
|
|
// MAC地址的6个段
|
|||
|
|
const segments = ref<string[]>(Array(6).fill(''))
|
|||
|
|
|
|||
|
|
// 解析传入的MAC地址
|
|||
|
|
const parseMacAddress = (mac: string): string[] => {
|
|||
|
|
if (!mac) return Array(6).fill('')
|
|||
|
|
|
|||
|
|
// 移除非十六进制字符并转为大写
|
|||
|
|
const cleanMac = mac.replace(/[^0-9a-fA-F]/g, '').toUpperCase()
|
|||
|
|
|
|||
|
|
// 分割成6段,每段2个字符
|
|||
|
|
const result: string[] = []
|
|||
|
|
for (let i = 0; i < 6; i++) {
|
|||
|
|
result.push(cleanMac.substr(i * 2, 2))
|
|||
|
|
}
|
|||
|
|
return result
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化MAC地址段
|
|||
|
|
const formatSegment = (value: string): string => {
|
|||
|
|
// 只保留前两个十六进制字符并转为大写
|
|||
|
|
return value.replace(/[^0-9a-fA-F]/g, '').toUpperCase().substr(0, 2)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 当前聚焦的输入框索引
|
|||
|
|
const focusedIndex = ref<number | null>(null)
|
|||
|
|
|
|||
|
|
// 处理输入事件
|
|||
|
|
const handleInput = (index: number) => {
|
|||
|
|
// 格式化输入值
|
|||
|
|
const formatted = formatSegment(segments.value[index])
|
|||
|
|
segments.value[index] = formatted
|
|||
|
|
|
|||
|
|
// 如果当前段已满2个字符且不是最后一段,自动跳转到下一个输入框
|
|||
|
|
if (formatted.length === 2 && index < 5) {
|
|||
|
|
// 获取下一个输入框的DOM元素
|
|||
|
|
setTimeout(() => {
|
|||
|
|
const nextInput = inputRefs.value[index + 1]
|
|||
|
|
if (nextInput) {
|
|||
|
|
const inputEl = nextInput.$el.querySelector('input')
|
|||
|
|
if (inputEl) {
|
|||
|
|
inputEl.focus()
|
|||
|
|
inputEl.select()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}, 0)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新v-model值
|
|||
|
|
updateModelValue()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理键盘事件
|
|||
|
|
const handleKeydown = (index: number, event: KeyboardEvent) => {
|
|||
|
|
const target = event.target as HTMLInputElement
|
|||
|
|
|
|||
|
|
// 处理退格键
|
|||
|
|
if (event.key === 'Backspace' && target.selectionStart === 0 && target.selectionEnd === 0) {
|
|||
|
|
if (segments.value[index] === '' && index > 0) {
|
|||
|
|
// 如果当前段为空,跳转到前一个输入框
|
|||
|
|
event.preventDefault()
|
|||
|
|
const prevInput = inputRefs.value[index - 1]
|
|||
|
|
if (prevInput) {
|
|||
|
|
const inputEl = prevInput.$el.querySelector('input')
|
|||
|
|
if (inputEl) {
|
|||
|
|
inputEl.focus()
|
|||
|
|
// 将光标移到末尾
|
|||
|
|
setTimeout(() => {
|
|||
|
|
inputEl.selectionStart = inputEl.value.length
|
|||
|
|
inputEl.selectionEnd = inputEl.value.length
|
|||
|
|
}, 0)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理左箭头键
|
|||
|
|
if (event.key === 'ArrowLeft' && target.selectionStart === 0) {
|
|||
|
|
if (index > 0) {
|
|||
|
|
event.preventDefault()
|
|||
|
|
const prevInput = inputRefs.value[index - 1]
|
|||
|
|
if (prevInput) {
|
|||
|
|
const inputEl = prevInput.$el.querySelector('input')
|
|||
|
|
if (inputEl) {
|
|||
|
|
inputEl.focus()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理右箭头键和制表符
|
|||
|
|
if ((event.key === 'ArrowRight' && target.selectionStart === target.value.length) || event.key === 'Tab') {
|
|||
|
|
if (index < 5) {
|
|||
|
|
event.preventDefault()
|
|||
|
|
const nextInput = inputRefs.value[index + 1]
|
|||
|
|
if (nextInput) {
|
|||
|
|
const inputEl = nextInput.$el.querySelector('input')
|
|||
|
|
if (inputEl) {
|
|||
|
|
inputEl.focus()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理焦点事件
|
|||
|
|
const handleFocus = (index: number) => {
|
|||
|
|
focusedIndex.value = index
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理失焦事件
|
|||
|
|
const handleBlur = () => {
|
|||
|
|
focusedIndex.value = null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理粘贴事件
|
|||
|
|
const handlePaste = (index: number, event: ClipboardEvent) => {
|
|||
|
|
event.preventDefault()
|
|||
|
|
const pastedText = event.clipboardData?.getData('text') || ''
|
|||
|
|
|
|||
|
|
// 清理粘贴的文本
|
|||
|
|
const cleanText = parseMacAddress(pastedText)
|
|||
|
|
|
|||
|
|
// 填充各段
|
|||
|
|
for (let i = 0; i < 6; i++) {
|
|||
|
|
segments.value[i] = cleanText[i] || ''
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新v-model值
|
|||
|
|
updateModelValue()
|
|||
|
|
|
|||
|
|
// 聚焦到最后一个非空段或最后一个段
|
|||
|
|
setTimeout(() => {
|
|||
|
|
let focusIndex = 5
|
|||
|
|
for (let i = 5; i >= 0; i--) {
|
|||
|
|
if (segments.value[i] !== '') {
|
|||
|
|
focusIndex = i
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const inputToFocus = inputRefs.value[focusIndex]
|
|||
|
|
if (inputToFocus) {
|
|||
|
|
const inputEl = inputToFocus.$el.querySelector('input')
|
|||
|
|
if (inputEl) {
|
|||
|
|
inputEl.focus()
|
|||
|
|
inputEl.select()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}, 0)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新v-model值
|
|||
|
|
const updateModelValue = () => {
|
|||
|
|
// 过滤掉空段,然后用冒号连接
|
|||
|
|
const nonEmptySegments = segments.value.filter(s => s !== '')
|
|||
|
|
const mac = nonEmptySegments.join(':')
|
|||
|
|
emit('update:modelValue', mac)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监听modelValue变化
|
|||
|
|
watch(
|
|||
|
|
() => props.modelValue,
|
|||
|
|
(newVal) => {
|
|||
|
|
if (newVal !== segments.value.filter(s => s !== '').join(':')) {
|
|||
|
|
segments.value = parseMacAddress(newVal || '')
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{ immediate: true }
|
|||
|
|
)
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
.mac-address-input {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 4px;
|
|||
|
|
|
|||
|
|
&.disabled {
|
|||
|
|
opacity: 0.7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
input {
|
|||
|
|
width: 40px;
|
|||
|
|
height: 32px;
|
|||
|
|
text-align: center;
|
|||
|
|
border: 1px solid #dcdfe6;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
outline: none;
|
|||
|
|
font-size: 14px;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
|
|||
|
|
&:focus {
|
|||
|
|
border-color: #409eff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&:disabled {
|
|||
|
|
background-color: #f5f7fa;
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.separator {
|
|||
|
|
user-select: none;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|