GrCodeScanner
Берут, когда вход по QR.
Обзор
Когда брать
- вход по QR — на экране логина другого устройства, вместо ручного набора кода;
- товар на складе — приёмка сканирует штрихкоды подряд,
continuousдля этого и есть; - оплата или ссылка из афиши — код ведёт на страницу, приложение открывает её;
- привязка устройства — код показывает второй экран, а телефон подтверждает.
Когда взять другое
| Нужно | Берите |
|---|---|
| Сфотографировать, а не прочитать код | GrCameraCapture |
| Выбрать кадр из готовой картинки | GrImageCrop |
| Принять файл со снимком кода | GrFileUpload |
Примеры 3 у каждого — живое демо и его код
Приёмка коробок по заказу
continuousне останавливает сканер и сообщает тот же код повторно: одинаковые коробки подряд — законные повторы, а остановка после каждой стоила бы дороже самого скана.formatsсужает поиск до EAN-13 и Code 128,intervalзадаёт частоту разбора кадров, а код не из заказа попадает в отдельную строку.Receiving · PO-77120Northwind Supplies → Hamburg, dock 3In progressКамера выключена.
Desk lamp, black400638133393124 / 24Desk lamp, white400638133394817 / 24Monitor arm, single59012341234570 / 12Cable tray, 80 cm761234567890440 / 40Scan cartons in any order; repeats count.Привязка телефона по QR-коду
Один скан — один ответ, поэтому сканер останавливается на первой находке.
detectприносит значение вместе с символикой: ссылка и номер заказа как строки похожи, и страница проверяет оба, прежде чем что-то привязать. Камера включается по нажатию: запрос разрешения без действия пользователя отклоняют не глядя.Link this phoneOpen Settings → Devices on your computer and scan the QR code shown there The camera starts when you press the button.Камера выключена.
Проверка билетов на входе со своей панелью
Слот
controlsзаменяет панель и оставляет поток на месте: разрешение, цикл кадров и уборка остаются за компонентом. Страница решает, что написано на одной большой кнопке, и называет каждое состояние словами — «Камера заблокирована, разрешите её в адресной строке» поможет волонтёру больше, чем «denied».Hall B · check-in1 of 320 admitted Камера выключена.
Camera off
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
facing | "user" | "environment" | undefined | "environment" | Какая камера: `environment` — тыловая, ей и сканируют. |
deviceId | string | undefined | undefined | Конкретное устройство, если приложение его уже выбрало. |
autoStart | boolean | undefined | false | Включать камеру сразу. По умолчанию `false`: запрос разрешения без действия пользователя отклоняют не глядя, а второй раз браузер уже не спросит. |
size | GrCodeScannerSize | undefined | undefined | Ступень размера: элементы управления и кегль подписей. По умолчанию `md`; берётся из `GrConfigProvider`. |
disabled | boolean | undefined | false | Сканер недоступен: камера не включается, кнопки выключены. |
ariaLabel | string | undefined | undefined | Имя группы с превью и кнопками. Не задано — из локали («Code scanner»). |
formats | readonly string[] | undefined | undefined | Символики: `qr_code`, `ean_13`, `code_128`, … Без списка — все, что умеет браузер. |
detector | GrCodeDetector | undefined | undefined | Детектор для браузеров без `BarcodeDetector` — Safari и Firefox. Пакет своего декодера не несёт: у него нет ни одной зависимости, и ради одного компонента появилась бы самая тяжёлая. Рецепт подключения готовой библиотеки — на странице компонента. |
interval | number | undefined | 250 | Как часто разбирать кадр, мс. Чаще — горячее устройство, реже — вялый отклик. |
continuous | boolean | undefined | false | Сообщать один и тот же код повторно: приёмка сканирует подряд. |
Слоты
| Slot | Type | Описание |
|---|---|---|
controls | { status: GrCameraStatus; start: () => void; stop: () => void; } | Своя панель управления вместо встроенной. |
События
| Event | Сигнатура | Описание |
|---|---|---|
start | (event: "start"): void | — |
stop | (event: "stop"): void | — |
statusChange | (event: "statusChange", status: GrCameraStatus): void | — |
detect | (event: "detect", codes: GrCodeResult[]): void | — |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
start | () => Promise<void> | Запросить разрешение, включить камеру и начать разбор кадров. При `disabled`, во время запроса и на живом потоке ничего не делает; отказ оседает в `status`. |
stop | () => void | Остановить разбор кадров и выключить камеру; состояние возвращается в `idle`. |
status | GrCameraStatus | Текущее состояние камеры (`GrCameraStatus`): `idle`, `starting`, `live` или причина отказа. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrCameraStatus, GrCodeResult, GrCodeDetector } from '@feugene/granularity-media/components/GrCodeScanner'Файлdist/types/components/shared/cameraState.d.ts
Состояния камеры и разбор отказов.
Вынесено из компонента, потому что именно здесь живёт вся неочевидность:
getUserMedia отвечает DOMException с полудюжиной имён, и от того, в какое
из них попал отказ, зависит текст, который увидит пользователь. Проверить это
рендером нельзя — камеры у тестового окружения нет.
Лежит в components/shared/, а не у GrCameraCapture: разбор читает и
сканер — через общий useCameraStream. Импорт из директории другого
компонента granum считает ребром графа, и объявить его значило бы заставить
потребителя, выбравшего один сканер, оплатить CSS и safelist камеры целиком.
type GrCameraStatus = 'idle'Файлdist/types/components/GrCodeScanner/codeDetection.d.ts
Один распознанный код.
interface GrCodeResult {
/** Содержимое: адрес, номер товара, текст. */
value: string;
/** Символика: `qr_code`, `ean_13`, `code_128`, … */
format: string;
}Детектор кадра. Ровно та форма, в которую заворачивается любая библиотека: на вход — то, что умеет рисовать canvas, на выход — список кодов.
type GrCodeDetector = (source: CanvasImageSource) => Promise<GrCodeResult[]>;
/** Есть ли нативный `BarcodeDetector`. */
export declare function nativeDetectorSupported(scope?: typeof globalThis): boolean;
/**
* Нативный детектор в общей форме.
*
* Формат приводится к нашему типу здесь, а не в компоненте: спецификация зовёт
* содержимое `rawValue`, и это единственное место, где чужое имя видно.
*/
export declare function createNativeDetector(formats: readonly string[] | undefined, scope?: typeof globalThis): GrCodeDetector | null;
/**
* Что сообщать наружу из очередного кадра.
*
* Камера отдаёт десятки кадров в секунду, и один и тот же код распознаётся в
* каждом. Без фильтра приложение получило бы поток одинаковых событий и
* оформило бы двадцать заказов вместо одного. Поэтому по умолчанию сообщается
* только то, чего не было в предыдущем кадре.
*
* `continuous` нужен там, где сканируют подряд (приёмка на складе): там повтор
* того же кода — законное второе событие.
*/
export declare function freshCodes(previous: readonly GrCodeResult[], next: readonly GrCodeResult[], continuous: boolean): GrCodeResult[];Есть ли нативный BarcodeDetector.
function nativeDetectorSupported(scope?: typeof globalThis): boolean;Нативный детектор в общей форме.
Формат приводится к нашему типу здесь, а не в компоненте: спецификация зовёт
содержимое rawValue, и это единственное место, где чужое имя видно.
function createNativeDetector(formats: readonly string[] | undefined, scope?: typeof globalThis): GrCodeDetector | null;Что сообщать наружу из очередного кадра.
Камера отдаёт десятки кадров в секунду, и один и тот же код распознаётся в
каждом. Без фильтра приложение получило бы поток одинаковых событий и
оформило бы двадцать заказов вместо одного. Поэтому по умолчанию сообщается
только то, чего не было в предыдущем кадре.
continuous нужен там, где сканируют подряд (приёмка на складе): там повтор
того же кода — законное второе событие.
function freshCodes(previous: readonly GrCodeResult[], next: readonly GrCodeResult[], continuous: boolean): GrCodeResult[];Файлdist/types/components/GrCodeScanner/config.d.ts
Кнопки состояния и управления — GrButton ядра, и ребро объявлено: granum
подмешивает safelist и CSS только тем компонентам, что попали в селекцию.
const grCodeScannerConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCodeScanner">;Файлdist/types/components/GrCodeScanner/defaults.d.ts
Настраиваемые через GrConfigProvider пропы.
Аугментация реестра идёт по месту объявления — через
@feugene/granularity/composables/useGrComponentConfig, а не через реэкспорт:
иначе слияние отваливается молча.
interface GrCodeScannerConfigurableProps {
size?: GrCodeScannerSize;
}Файлdist/types/components/GrCodeScanner/GrCodeScanner.vue.d.ts
interface GrCodeScannerProps {
/** Какая камера: `environment` — тыловая, ей и сканируют. */
facing?: 'user' | 'environment';
/** Конкретное устройство, если приложение его уже выбрало. */
deviceId?: string;
/**
* Включать камеру сразу.
*
* По умолчанию `false`: запрос разрешения без действия пользователя
* отклоняют не глядя, а второй раз браузер уже не спросит.
*/
autoStart?: boolean;
/** Символики: `qr_code`, `ean_13`, `code_128`, … Без списка — все, что умеет браузер. */
formats?: readonly string[];
/**
* Детектор для браузеров без `BarcodeDetector` — Safari и Firefox.
*
* Пакет своего декодера не несёт: у него нет ни одной зависимости, и ради
* одного компонента появилась бы самая тяжёлая. Рецепт подключения готовой
* библиотеки — на странице компонента.
*/
detector?: GrCodeDetector;
/** Как часто разбирать кадр, мс. Чаще — горячее устройство, реже — вялый отклик. */
interval?: number;
/** Сообщать один и тот же код повторно: приёмка сканирует подряд. */
continuous?: boolean;
/**
* Ступень размера: элементы управления и кегль подписей. По умолчанию `md`;
* берётся из `GrConfigProvider`.
*/
size?: GrCodeScannerSize;
/** Сканер недоступен: камера не включается, кнопки выключены. */
disabled?: boolean;
/** Имя группы с превью и кнопками. Не задано — из локали («Code scanner»). */
ariaLabel?: string;
}interface GrCodeScannerEmits {
/** Распознаны коды. Без `continuous` — только те, которых не было в прошлом кадре. */
(e: 'detect', codes: GrCodeResult[]): void;
/** Поток камеры пошёл — сканирование началось. */
(e: 'start'): void;
/** Камера выключена. */
(e: 'stop'): void;
/** Состояние камеры сменилось — значения `GrCameraStatus`. */
(e: 'statusChange', status: GrCameraStatus): void;
}Файлdist/types/components/GrCodeScanner/grCodeScannerStyles.d.ts
type GrCodeScannerSize = 'xs' | 'sm' | 'md' | 'lg';Safelist пуст: всё, что рисует сканер, лежит целыми литералами здесь и в
шаблоне, и granum извлекает это сам — модуль уезжает в общий dist/chunks/,
но сборка доходит до него по графу бандла компонента.
const grCodeScannerSafelist: string[];Использование из прозы библиотеки
Пакет не несёт декодер
Нативный 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. Предлагать «включите камеру» в этом случае значило
бы отправить пользователя решать не ту задачу.
Проверяется это после монтирования: на сервере BarcodeDetector нет
никогда, и решение в рендере расходилось бы с браузером при гидрации. Первый
рендер везде одинаков — состояние покоя, — и сообщение «браузер не умеет читать
коды» появляется только там, где это правда.
Свой слот #controls владеет действиями
Слот получает status, start и stop — тип GrCameraStatus экспортируется и
из @feugene/granularity-media/components/GrCodeScanner. Когда слот передан,
в рамке остаётся только текст состояния: своя кнопка «Начать» там стала бы
второй под панелью приложения.
Один код в кадре — одно событие
Камера отдаёт десятки кадров в секунду, и код распознаётся в каждом. Без фильтра приложение получило бы поток одинаковых событий и оформило бы двадцать заказов вместо одного, поэтому сообщается только то, чего не было в предыдущем кадре.
Символика — часть тождества: одно и то же значение в qr_code и в ean_13
считается разными кодами, потому что и означает разное.
continuous снимает фильтр: на приёмке сканируют одинаковые упаковки подряд, и
там повтор — законное второе событие.
Разбор идёт по таймеру, а не по каждому кадру
interval (250 мс по умолчанию) — размен между отзывчивостью и нагревом:
разбирать каждый кадр значит держать процессор занятым непрерывно, а телефон в
руке греется заметно.
Кадр, который не разобрался, — норма, а не сбой: в объектив попала рука,
отражение, размытие. Такой заход молча пропускается, следующий придёт через
interval.
Границы
- нет разбора картинки из файла. Компонент работает с живым потоком; для снимка кода приложение вызывает свой детектор напрямую;
- нет подсветки и зума.
torchиzoom— расширения трека, поддержка у них разная даже внутри Chrome; - нет разметки найденного кода на превью. Нативный API отдаёт координаты, но чужой детектор — не обязательно, и рисовать рамку по половине источников значило бы обещать то, чего нет.
Пропы этого примера