Feat(mac): prefix queries and example pills in MAC Lookup

The form validates with the shared common/mac-input.js parser, so vendor
prefixes go through. A collapsed "See examples" fold under the input, as in
IP Calculator, holds one pill per kind of answer: full address, vendor
prefix, MA-M block, private registration, multicast and randomized. Browsers
expose no MAC address, so there is no "my address" pill. Changelog entry in
every full locale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jason5ng32andClaude Opus 5.5 committed 2026-10-05 11:21:12 +08:00
1 parent 7a52e8acea
commit e57b9e331d
8 files changed
+116 -15

No files matched your search

@@ -1,3 +1,8 @@
<!-- MAC Lookup (advanced tool, slug `macchecker`): a full address or a
vendor prefix (common/mac-input.js), answered by /api/macchecker from the
local IEEE registries. A collapsed fold under the input holds example
pills, one per kind of answer. Browsers expose no MAC address, so there
is no "my address" pill. -->
<template>
<div class="mac-checker-section my-4 space-y-4">
<!-- Top note -->
@@ -22,6 +27,27 @@
</Button>
</div>
<p v-if="errorMsg" class="text-sm text-destructive">{{ errorMsg }}</p>
<!-- Example inputs, tap to run. Collapsed by default; the chevron
turns from › to ⌄ via data-state. -->
<Collapsible>
<CollapsibleTrigger>
<button type="button" :class="triggerClass">
<ChevronRight class="size-4 transition-transform duration-200" />{{ t('macchecker.Examples') }}
</button>
</CollapsibleTrigger>
<CollapsibleContent>
<ToggleGroup :model-value="picked" type="single" variant="outline" :spacing="2"
:disabled="macCheckStatus === 'running'"
class="mt-2 flex-wrap justify-start" @update:model-value="(v) => v && runPreset(v)">
<ToggleGroupItem v-for="example in EXAMPLES" :key="example.input" :value="example.input"
:class="tagClass" :aria-label="`${t(example.labelKey)}: ${example.input}`">
<span class="text-muted-foreground">{{ t(example.labelKey) }}</span>
<span class="font-mono">{{ example.input }}</span>
</ToggleGroupItem>
</ToggleGroup>
</CollapsibleContent>
</Collapsible>
</div>
<!-- Result area -->
@@ -91,13 +117,16 @@ import { useMainStore } from '@/store';
import { useI18n } from 'vue-i18n';
import { trackEvent } from '@/utils/analytics';
import getCountryName from '@/data/country-name.js';
import { CircleCheck, CircleX, Factory, ListChecks, Search } from '@lucide/vue';
import { normalizeMacQuery } from '@/utils/mac-input.js';
import { ChevronRight, CircleCheck, CircleX, Factory, ListChecks, Search } from '@lucide/vue';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Spinner } from '@/components/ui/spinner';
import { Icon } from '@iconify/vue';
import { Label } from '@/components/ui/label';
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
const { t } = useI18n();
@@ -108,6 +137,22 @@ const macCheckResult = ref({});
const macCheckStatus = ref('idle');
const queryMAC = ref('');
const errorMsg = ref('');
// Which example pill the current result came from, if any.
const picked = ref('');
// One example per kind of answer; the pills double as syntax documentation.
const EXAMPLES = [
{ labelKey: 'macchecker.exampleFull', input: 'F0:2F:4B:01:0A:AA' },
{ labelKey: 'macchecker.examplePrefix', input: '3C:5A:B4' },
{ labelKey: 'macchecker.exampleSubBlock', input: 'F0:40:AF:91:23:45' },
{ labelKey: 'macchecker.examplePrivate', input: 'AC:DE:48:00:11:22' },
{ labelKey: 'macchecker.exampleMulticast', input: '01:00:5E:00:00:FB' },
{ labelKey: 'macchecker.exampleRandom', input: '42:9F:C3:15:7A:E0' },
];
// Pills and fold trigger match IpCalculator's.
const tagClass = 'group h-7 rounded-full px-2.5 text-xs cursor-pointer';
const triggerClass = 'flex items-center gap-1 text-xs text-muted-foreground cursor-pointer hover:text-foreground [&[data-state=open]>svg]:rotate-90';
const leftItems = computed(() => [
{ key: 'macPrefix' },
@@ -127,23 +172,26 @@ const tableItems = computed(() => [
]);
const validateInput = (input) => {
if (!input) return null;
const normalizedInput = input.replace(/[:-]/g, '').replace(/\s+/g, '');
if (normalizedInput.length !== 12 || !/^[0-9A-Fa-f]+$/.test(normalizedInput)) {
errorMsg.value = t('macchecker.invalidMAC');
return null;
}
return normalizedInput;
const hex = normalizeMacQuery(input);
if (!hex) errorMsg.value = t('macchecker.invalidMAC');
return hex;
};
const onSubmit = () => {
trackEvent('Section', 'StartClick', 'MACChecker');
errorMsg.value = '';
macCheckResult.value = {};
const query = validateInput(queryMAC.value);
const raw = queryMAC.value.trim();
picked.value = EXAMPLES.some((e) => e.input === raw) ? raw : '';
const query = validateInput(raw);
if (query) getMacInfo(query);
};
const runPreset = (input) => {
queryMAC.value = input;
onSubmit();
};
const getMacInfo = async (query) => {
macCheckStatus.value = 'running';
try {
+11
View File
@@ -2185,6 +2185,17 @@
"pt-BR": "Melhorada a experiência durante a inicialização do servidor"
}
},
{
"type": "improve",
"change": {
"en": "MAC Lookup now reads the official IEEE offline data",
"zh": "MAC 地址查询改用 IEEE 官方离线数据",
"zh-TW": "MAC 位址查詢改用 IEEE 官方離線資料",
"fr": "La recherche MAC s'appuie désormais sur les données offline de l'IEEE",
"ru": "Поиск MAC теперь использует официальные данные offline IEEE",
"pt-BR": "A consulta MAC agora usa os dados offline do IEEE"
}
},
{
"type": "fix",
"change": {
+8 -1
View File
@@ -902,7 +902,14 @@
"isLocal": "Local address",
"property": "Address property",
"value": "Value",
"manufacturer": "Manufacturer details"
"manufacturer": "Manufacturer details",
"Examples": "See examples",
"exampleFull": "Full address",
"examplePrefix": "Vendor prefix",
"exampleSubBlock": "Small block",
"examplePrivate": "Private registration",
"exampleMulticast": "Multicast",
"exampleRandom": "Randomized"
},
"ipcalculator": {
"Title": "IP Calculator",
+8 -1
View File
@@ -902,7 +902,14 @@
"isLocal": "Adresse locale",
"property": "Propriété de l'adresse",
"value": "Valeur",
"manufacturer": "Détails du fabricant"
"manufacturer": "Détails du fabricant",
"Examples": "Voir des exemples",
"exampleFull": "Adresse complète",
"examplePrefix": "Préfixe constructeur",
"exampleSubBlock": "Petit bloc",
"examplePrivate": "Enregistrement privé",
"exampleMulticast": "Multicast",
"exampleRandom": "Aléatoire"
},
"ipcalculator": {
"Title": "Calculateur IP",
+8 -1
View File
@@ -902,7 +902,14 @@
"isLocal": "Endereço local",
"property": "Propriedade do endereço",
"value": "Valor",
"manufacturer": "Detalhes do fabricante"
"manufacturer": "Detalhes do fabricante",
"Examples": "Ver exemplos",
"exampleFull": "Endereço completo",
"examplePrefix": "Prefixo do fabricante",
"exampleSubBlock": "Bloco pequeno",
"examplePrivate": "Registro privado",
"exampleMulticast": "Multicast",
"exampleRandom": "Aleatório"
},
"ipcalculator": {
"Title": "Calculadora de IP",
+8 -1
View File
@@ -902,7 +902,14 @@
"isLocal": "Локальный адрес",
"property": "Свойство адреса",
"value": "Значение",
"manufacturer": "Сведения о производителе"
"manufacturer": "Сведения о производителе",
"Examples": "Примеры",
"exampleFull": "Полный адрес",
"examplePrefix": "Префикс производителя",
"exampleSubBlock": "Малый блок",
"examplePrivate": "Скрытая регистрация",
"exampleMulticast": "Многоадресный",
"exampleRandom": "Случайный"
},
"ipcalculator": {
"Title": "IP-калькулятор",
+8 -1
View File
@@ -902,7 +902,14 @@
"isLocal": "本地位址",
"property": "位址屬性",
"value": "值",
"manufacturer": "製造商資訊"
"manufacturer": "製造商資訊",
"Examples": "查看用例",
"exampleFull": "完整位址",
"examplePrefix": "廠商字首",
"exampleSubBlock": "小區塊",
"examplePrivate": "隱私註冊",
"exampleMulticast": "多播",
"exampleRandom": "隨機位址"
},
"ipcalculator": {
"Title": "IP 計算器",
+8 -1
View File
@@ -902,7 +902,14 @@
"isLocal": "本地地址",
"property": "地址属性",
"value": "值",
"manufacturer": "制造商信息"
"manufacturer": "制造商信息",
"Examples": "查看用例",
"exampleFull": "完整地址",
"examplePrefix": "厂商前缀",
"exampleSubBlock": "小区块",
"examplePrivate": "隐私注册",
"exampleMulticast": "多播",
"exampleRandom": "随机地址"
},
"ipcalculator": {
"Title": "IP 计算器",