GrCodeScanner
Берут, когда вход по QR.
Когда брать
- вход по QR — на экране логина другого устройства, вместо ручного набора кода;
- товар на складе — приёмка сканирует штрихкоды подряд,
continuousдля этого и есть; - оплата или ссылка из афиши — код ведёт на страницу, приложение открывает её;
- привязка устройства — код показывает второй экран, а телефон подтверждает.
Когда взять другое
| Нужно | Берите |
|---|---|
| Сфотографировать, а не прочитать код | GrCameraCapture |
| Выбрать кадр из готовой картинки | GrImageCrop |
| Принять файл со снимком кода | GrFileUpload |
Пакет не несёт декодер
Нативный BarcodeDetector есть в Chrome и Edge, но его нет ни в Safari, ни в
Firefox — то есть на iPhone его нет вовсе, а сканируют чаще всего именно
телефоном. Встроить декодер в пакет нельзя: у него нет ни одной зависимости, и
ради одного компонента появилась бы самая тяжёлая — притом у всех, включая
тех, кто взял только кроп.
Поэтому нативный путь компонент умеет сам, а остальные браузеры закрывает детектор, который передаёт приложение:
import { BrowserMultiFormatReader } from '@zxing/browser'
const reader = new BrowserMultiFormatReader()
async function detector(source) {
const canvas = Object.assign(document.createElement('canvas'), {
width: source.videoWidth,
height: source.videoHeight,
})
canvas.getContext('2d')?.drawImage(source, 0, 0)
const result = reader.decodeFromCanvas(canvas)
return result ? [{ value: result.getText(), format: String(result.getBarcodeFormat()) }] : []
}
<GrCodeScanner :detector="detector" />
Проп сильнее нативного пути намеренно: приложение, подключившее свою библиотеку, обычно делает это ради формата, которого нативный детектор не знает.
Читать нечем — и это отдельное состояние: компонент говорит прямо, что браузер не
умеет и что нужен detector. Предлагать «включите камеру» в этом случае значило
бы отправить пользователя решать не ту задачу.
Один код в кадре — одно событие
Камера отдаёт десятки кадров в секунду, и код распознаётся в каждом. Без фильтра приложение получило бы поток одинаковых событий и оформило бы двадцать заказов вместо одного, поэтому сообщается только то, чего не было в предыдущем кадре.
Символика — часть тождества: одно и то же значение в qr_code и в ean_13
считается разными кодами, потому что и означает разное.
continuous снимает фильтр: на приёмке сканируют одинаковые упаковки подряд, и
там повтор — законное второе событие.
Разбор идёт по таймеру, а не по каждому кадру
interval (250 мс по умолчанию) — размен между отзывчивостью и нагревом:
разбирать каждый кадр значит держать процессор занятым непрерывно, а телефон в
руке греется заметно.
Кадр, который не разобрался, — норма, а не сбой: в объектив попала рука,
отражение, размытие. Такой заход молча пропускается, следующий придёт через
interval.
Границы
- нет разбора картинки из файла. Компонент работает с живым потоком; для снимка кода приложение вызывает свой детектор напрямую;
- нет подсветки и зума.
torchиzoom— расширения трека, поддержка у них разная даже внутри Chrome; - нет разметки найденного кода на превью. Нативный API отдаёт координаты, но чужой детектор — не обязательно, и рисовать рамку по половине источников значило бы обещать то, чего нет.
Установка
npm i @feugene/granularity-mediaИмпорт
import { GrCodeScanner } from '@feugene/granularity-media/components/GrCodeScanner'API
API этого компонента ещё не посчитан: генератор витрины пока обходит только ядро. Пока его нет, справочник — в документации пакета.
Примеры 2
Basic
<script setup lang="ts">
import { ref } from 'vue'
import { GrBadge, GrSwitch } from '@feugene/granularity'
import type { GrCodeResult } from '@feugene/granularity-media'
const found = ref<GrCodeResult[]>([])
const continuous = ref(false)
function onDetect(codes: GrCodeResult[]) {
// Свежие коды приходят пачкой: в кадр попадает и наклейка, и ценник рядом.
found.value = [...codes, ...found.value].slice(0, 8)
}
</script>
<template>
<div class="grid gap-4 lg:grid-cols-2">
<GrCodeScanner :continuous="continuous" @detect="onDetect" />
<div class="showcase-demo-panel grid content-start gap-3 rounded-[var(--gr-radius-lg)] border p-4">
<GrSwitch v-model="continuous" size="sm">
Сообщать повторы
</GrSwitch>
<p class="showcase-demo-text text-sm">
Без этого один код в кадре даёт одно событие: камера отдаёт десятки кадров в секунду,
и приложение оформило бы двадцать заказов вместо одного. На приёмке, где сканируют
одинаковые упаковки подряд, повтор — законное второе событие.
</p>
<template v-if="found.length > 0">
<p class="showcase-demo-text text-sm">
Найдено:
</p>
<ul class="grid gap-2">
<li v-for="code in found" :key="`${code.format}:${code.value}`" class="flex items-center gap-2">
<GrBadge size="sm" tone="neutral">{{ code.format }}</GrBadge>
<code class="showcase-demo-text text-sm">{{ code.value }}</code>
</li>
</ul>
</template>
<p v-else class="showcase-demo-text text-sm">
Наведите камеру на QR или штрихкод — содержимое появится здесь.
</p>
</div>
</div>
</template>Receiving
<script setup lang="ts">
import { computed, ref } from 'vue'
import { GrBadge, GrButton, GrEmptyState, GrTable } from '@feugene/granularity'
import type { GrCodeResult } from '@feugene/granularity-media'
/**
* Приёмка: коробки сканируют подряд, и одинаковых среди них большинство.
*
* Здесь видно, зачем `continuous`: без него второй такой же штрихкод не дал бы
* события вовсе, и кладовщик решил бы, что сканер не сработал. С ним повтор —
* законная вторая единица товара.
*/
/**
* Справочник — `Map`, а не объект: ключ штрихкода это строка, и у объекта она
* превратилась бы в число. Код с ведущим нулём (UPC-A, записанный как EAN-13)
* потерял бы его молча — и позиция перестала бы находиться.
*/
const CATALOG = new Map([
['4600051000057', 'Кофе зерновой, 1 кг'],
['5901234123457', 'Бумага А4, 500 л'],
['4008400402222', 'Батарейки AA, 4 шт'],
])
interface Position {
code: string
title: string
count: number
}
const positions = ref<Position[]>([])
const total = computed(() => positions.value.reduce((sum, item) => sum + item.count, 0))
function onDetect(codes: GrCodeResult[]) {
for (const code of codes) {
const found = positions.value.find(item => item.code === code.value)
if (found) {
found.count += 1
continue
}
positions.value = [
{ code: code.value, title: CATALOG.get(code.value) ?? 'Неизвестная позиция', count: 1 },
...positions.value,
]
}
}
</script>
<template>
<div class="grid gap-4 lg:grid-cols-2">
<div class="grid gap-3">
<GrCodeScanner continuous :formats="['ean_13', 'code_128', 'qr_code']" @detect="onDetect" />
<p class="showcase-demo-text text-sm">
Наводите на штрихкоды подряд — повтор увеличивает количество, новый код добавляет строку.
</p>
</div>
<div class="grid content-start gap-3">
<div class="flex items-center justify-between gap-2">
<span class="text-[length:var(--gr-text-sm)] leading-[var(--gr-leading-sm)] font-600">
Принято: {{ total }} шт
</span>
<GrButton
size="xs"
variant="outline"
:disabled="positions.length === 0"
@click="positions = []"
>
Очистить
</GrButton>
</div>
<GrTable v-if="positions.length > 0" :column-count="3">
<template #header>
<tr>
<th class="text-left">
Позиция
</th>
<th class="text-left">
Код
</th>
<th class="text-right">
Кол-во
</th>
</tr>
</template>
<tr v-for="item in positions" :key="item.code">
<td>{{ item.title }}</td>
<td><code class="showcase-demo-text text-xs">{{ item.code }}</code></td>
<td class="text-right">
<GrBadge size="sm" :tone="item.count > 1 ? 'success' : 'neutral'">
{{ item.count }}
</GrBadge>
</td>
</tr>
</GrTable>
<GrEmptyState
v-else
title="Пока ничего не отсканировано"
description="Три кода из справочника опознаются по названию, остальные попадут как «Неизвестная позиция» — на складе это отдельная задача приёмщика."
/>
</div>
</div>
</template>