BarcodeViewer komponent eklendi.

This commit is contained in:
Sedat ÖZTÜRK 2026-09-21 22:53:48 +03:00
parent b31a651313
commit 660f6db268
9 changed files with 212 additions and 5 deletions

View file

@ -1018,6 +1018,7 @@ alır):
| `Rate` | `count` (`5`), `value`/`defaultValue`, `allowHalf` (`false`), `allowClear` (`true`), `color` (`amber`), `size` (`20`), `gap` (`4`), `tooltips[]`, `disabled`, `readOnly` |
| `Upload` | `accept`, `multiple`, `draggable` (`false`), `uploadLimit`, `showList` (`true`), `tip`, `fileList[]`, `disabled` — dosyanın **gerçek** yüklenmesi `beforeUpload`/`onChange` script'inde `api.post` ile yapılır |
| `BarcodeScanField` | `label`, `name` (`'barcode'`), `placeholder`, `buttonText`, `continuous` (`false` — açıkken diyalog okumadan sonra kapanmaz), `readOnly` (`true`), `className`; **olay**: `onScan(event)` — `event` okunan kodun metin hâli (Event workspace'te Input'un `onChange`'i gibi düzenlenir). Kamerayı `BarcodeDetector` (Android/Chromium) ya da lazy `zxing-wasm` (iOS/Firefox) ile okur — 1D + QR. **HTTPS şart**; iOS'ta yüklü PWA içinde yalnız 16.4+ çalışır, bileşen aksini kullanıcıya söyler |
| `BarcodeViewer` | `value` (barkoda çevrilecek veri), `format` (`QRCode` \| `Code128` \| `Code39` \| `Code93` \| `Codabar` \| `EAN13` \| `EAN8` \| `UPCA` \| `UPCE` \| `ITF` \| `DataMatrix` \| `PDF417` \| `Aztec`), `label`, `showText` (`false` — kodun altına okunabilir metni de basar), `scale` (`4`, modül kalınlığı), `fitWidth` (`false` — açıkken kapsayıcının genişliğine yayılır), `className`. Veriyi **SVG** olarak çizer (lazy `zxing-wasm/writer`); karanlık temada da okunabilsin diye beyaz zemin üstünde. `Form` düğümünün içinde `value` bir kolona bağlanırsa kayıt değiştikçe kod yeniden çizilir — tıpkı bir `Input` gibi. Veri formata uymuyorsa (EAN13'e harf gibi) kod yerine zxing'in hata metni görünür |
**Gösterim/geri bildirim bileşenleri:**

View file

@ -1021,10 +1021,21 @@ dosyası yoktur; liste her `npm run build` / `npm start` ile tazelenir.
bileşenler tek bir sözlükte durur (`RUNTIME_NODE_COMPONENTS`): kanvas onu `VisualCanvas` içinde,
üretilen kod ise runtime kapsamı üzerinden kullanır — iki taraf aynı kaynaktan beslendiği için
tasarım ve çalışma zamanı ayrışamaz. Sözlükte `PlatformIcon`, `BarcodeScanField`
(`components/shared/BarcodeScanner`) ve public site parçaları (`components/publicSite`) bulunur. Yeni bir düğüm eklerken: bileşen `components/publicSite` (ya da
(`components/shared/BarcodeScanner`), `BarcodeViewer` (`components/shared/BarcodeViewer.tsx`) ve
public site parçaları (`components/publicSite`) bulunur. Yeni bir düğüm eklerken: bileşen `components/publicSite` (ya da
uygun klasör) → `runtimeNodes.ts` sözlüğü → `catalog.ts` tanımı (`kind: 'custom'`) → `LanguagesData.json`
açıklama anahtarı.
**Barkod.** Okuma ve çizme ayrı yollardır, ikisi de `utils/barcode.ts` içindedir ve `zxing-wasm`
yalnızca gerektiğinde lazy iner. Okuma: Chromium'da yerleşik `BarcodeDetector`, olmayan tarayıcıda
`zxing-wasm/reader` (`BarcodeScanField`, `BarcodeScannerDialog`). Çizme: `zxing-wasm/writer`
(`writeBarcodeSvg` → `BarcodeViewer`); SVG üretilir, böylece büyütmede ve baskıda kırılmaz.
Desteklenen çizim formatları `BARCODE_WRITE_FORMATS` listesindedir (QRCode, Code39, Code93,
Code128, EAN13/8, UPCA/E, ITF, Codabar, DataMatrix, PDF417, Aztec) ve katalogdaki `format`
seçenekleri de bu listeden üretilir — liste tek yerde değişir. `BarcodeViewer` bir `value` propu
taşıdığı için tasarımcıdaki `Form` düğümünün içinde tıpkı bir `Input` gibi bir kolona bağlanabilir;
kayıt değiştikçe kod yeniden çizilir.
Elle yazılmış bir bileşen Visual Designer'da **kod modunda** açılır: tasarımcı yalnızca kendi
ürettiği kodu düğüm ağacına çevirir (`isDesignerGeneratedCode`), aksi hâlde tanımadığı JSX'i
düşürüp kaydederken kodu yok ederdi.

View file

@ -28986,6 +28986,18 @@
"en": "Text field with a camera button that reads barcodes and QR codes.",
"tr": "Barkod ve QR okuyan kamera butonlu metin alanı."
},
{
"resourceName": "Platform",
"key": "App.CatalogBarcodeViewer.BarcodeViewerDescription",
"en": "Draws any value as a barcode or QR code; inside a Form the bound column is rendered.",
"tr": "Herhangi bir değeri barkod ya da QR olarak çizer; Form içinde bağlı kolonu gösterir."
},
{
"resourceName": "Platform",
"key": "App.BarcodeViewer.NoValue",
"en": "No value to draw as a barcode.",
"tr": "Barkoda çevrilecek bir değer yok."
},
{
"resourceName": "Platform",
"key": "App.Geolocation.Unsupported",

View file

@ -1,6 +1,6 @@
import type React from 'react'
import PlatformIcon from './PlatformIcon'
import { BarcodeScanField } from '@/components/shared'
import { BarcodeScanField, BarcodeViewer } from '@/components/shared'
import {
PublicDemoButton,
PublicHeaderBar,
@ -19,6 +19,7 @@ import {
export const RUNTIME_NODE_COMPONENTS = {
PlatformIcon,
BarcodeScanField,
BarcodeViewer,
PublicHeaderBar,
PublicLogo,
PublicLanguageSelector,

View file

@ -0,0 +1,76 @@
import { useEffect, useState } from 'react'
import classNames from 'classnames'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { writeBarcodeSvg, type BarcodeWriteFormat } from '@/utils/barcode'
export type BarcodeViewerProps = {
/** Barkoda çevrilecek veri. Form içinde kolona bağlanan alan budur. */
value?: string
format?: BarcodeWriteFormat
label?: string
/** Kodun altına okunabilir metni de basar. */
showText?: boolean
/** Modül (en ince çubuk) kalınlığı; büyüdükçe kod büyür. */
scale?: number
/** Kodu kapsayıcının genişliğine yayar; kapalıyken `scale` boyutu korunur. */
fitWidth?: boolean
className?: string
}
/**
* Herhangi bir veriyi barkod/QR olarak çizer. Tasarımcıdan sürüklenip bırakılan
* düğüm ve üretilen kod aynı bileşeni kullanır; `value` bir Form'un kaydına
* bağlandığında alan değiştikçe kod da yeniden çizilir.
*
* Kod SVG olarak üretilir: baskıda ve büyütmede kırılmaz, karanlık temada da
* okunabilir kalsın diye beyaz zemin üstünde çizilir.
*/
const BarcodeViewer = ({
value,
format = 'QRCode',
label,
showText = false,
scale = 4,
fitWidth = false,
className,
}: BarcodeViewerProps) => {
const { translate } = useLocalization()
const [svg, setSvg] = useState('')
const [error, setError] = useState('')
const text = value === undefined || value === null ? '' : String(value)
useEffect(() => {
let cancelled = false
void writeBarcodeSvg(text, { format, scale, showText }).then((result) => {
if (cancelled) return
setSvg(result.svg)
setError(result.error)
})
return () => {
cancelled = true
}
}, [text, format, scale, showText])
return (
<div className={className}>
{label && <label className="mb-1 block text-sm font-semibold">{label}</label>}
{!text ? (
<span className="text-sm opacity-70">{translate('::App.BarcodeViewer.NoValue')}</span>
) : error ? (
<span className="text-sm text-red-500">{error}</span>
) : (
<div
// zxing'in ürettiği SVG'dir, kullanıcı verisi değil: metin HRI içine
// kaçışlanarak yazılır, bu yüzden gömmek güvenlidir.
dangerouslySetInnerHTML={{ __html: svg }}
className={classNames(
'inline-block rounded bg-white p-2',
fitWidth && 'block w-full [&>svg]:h-auto [&>svg]:w-full',
)}
/>
)}
</div>
)
}
export default BarcodeViewer

View file

@ -3,6 +3,7 @@ export { default as AdaptableCard } from './AdaptableCard'
export { default as Affix } from './Affix'
export { default as AuthorityCheck } from './AuthorityCheck'
export { BarcodeScanField, BarcodeScannerDialog, BarcodeScannerHost } from './BarcodeScanner'
export { default as BarcodeViewer } from './BarcodeViewer'
export { default as PermissionCheck } from './PermissionCheck'
export { default as ConfirmDialog } from './ConfirmDialog'
export { default as Container } from './Container'

View file

@ -1,5 +1,6 @@
import { CUSTOM_COMPONENTS, HTML_ELEMENTS } from '@/components/codeLayout/data/componentDefinitions'
import generatedComponentProps from './generated/componentProps.json'
import { BARCODE_WRITE_FORMATS } from '@/utils/barcode'
import {
DESIGNER_DATA_COMPONENT_NAMES,
FORM_EVENTS,
@ -1252,6 +1253,32 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
],
hooks: [],
},
{
// Veriyi barkod/QR olarak cizer. Bir Form'un icinde `value` bir kolona
// baglanirsa alan degistikce kod da yeniden cizilir.
name: 'BarcodeViewer',
icon: 'QrCode',
category: 'basic',
kind: 'custom',
toolboxGroup: 'ui',
description: 'App.CatalogBarcodeViewer.BarcodeViewerDescription',
properties: [
{ name: 'label', type: 'string', value: '', category: 'properties' },
{ name: 'value', type: 'string', value: '1234567890', category: 'properties' },
{
name: 'format',
type: 'select',
value: 'QRCode',
options: [...BARCODE_WRITE_FORMATS],
category: 'properties',
},
{ name: 'showText', type: 'boolean', value: false, category: 'properties' },
{ name: 'scale', type: 'number', value: 4, category: 'styling' },
{ name: 'fitWidth', type: 'boolean', value: false, category: 'styling' },
{ name: 'className', type: 'string', value: '', category: 'styling' },
],
hooks: [],
},
{
name: 'Spacer',
icon: 'Space',

View file

@ -117,3 +117,81 @@ export const stopCameraStream = (stream?: MediaStream | null) =>
/** Okuma geri bildirimi. Desteklemeyen cihazda sessizce atlanır. */
export const vibrateOnScan = () => navigator.vibrate?.(60)
/**
* Yazma (çizme) tarafı. Okuma yolundan bağımsızdır: `zxing-wasm/writer` yalnızca
* ekranda bir barkod çizildiğinde iner, okuma yapan ekranlara yük bindirmez.
*/
export const BARCODE_WRITE_FORMATS = [
'QRCode',
'Code128',
'Code39',
'Code93',
'Codabar',
'EAN13',
'EAN8',
'UPCA',
'UPCE',
'ITF',
'DataMatrix',
'PDF417',
'Aztec',
] as const
export type BarcodeWriteFormat = (typeof BARCODE_WRITE_FORMATS)[number]
export interface BarcodeWriteOptions {
format?: BarcodeWriteFormat
/** Modül (en ince çubuk) kalınlığı. */
scale?: number
/** Kodun altına okunabilir metni de basar. */
showText?: boolean
}
type ZxingWriter = (
value: string,
options: BarcodeWriteOptions,
) => Promise<{ svg: string; error: string }>
let zxingWriter: Promise<ZxingWriter> | undefined
const getZxingWriter = () => {
zxingWriter ??= (async () => {
const [{ prepareZXingModule, writeBarcode }, { default: wasmUrl }] = await Promise.all([
import('zxing-wasm/writer'),
import('zxing-wasm/writer/zxing_writer.wasm?url'),
])
prepareZXingModule({ overrides: { locateFile: () => wasmUrl } })
return async (value: string, options: BarcodeWriteOptions) => {
const result = await writeBarcode(value, {
format: options.format ?? 'QRCode',
scale: options.scale && options.scale > 0 ? options.scale : 4,
addHRT: options.showText ?? false,
})
return { svg: result.svg, error: result.error }
}
})()
return zxingWriter
}
/**
* Metni barkod SVG'sine çevirir. Veri formata uymuyorsa (EAN13'e harf gibi)
* zxing boş SVG ve bir hata metni döner; çağıran taraf bunu kullanıcıya gösterir.
* SVG'nin başındaki XML/DOCTYPE bildirimi ayıklanır — gömülü kullanımda geçersizdir.
*/
export const writeBarcodeSvg = async (
value: string,
options: BarcodeWriteOptions = {},
): Promise<{ svg: string; error: string }> => {
if (!value) return { svg: '', error: '' }
try {
const write = await getZxingWriter()
const { svg, error } = await write(value, options)
const start = svg.indexOf('<svg')
return { svg: start < 0 ? '' : svg.slice(start), error }
} catch (error) {
return { svg: '', error: error instanceof Error ? error.message : String(error) }
}
}