BarcodeViewer komponent eklendi.
This commit is contained in:
parent
b31a651313
commit
660f6db268
9 changed files with 212 additions and 5 deletions
|
|
@ -1018,6 +1018,7 @@ alır):
|
||||||
| `Rate` | `count` (`5`), `value`/`defaultValue`, `allowHalf` (`false`), `allowClear` (`true`), `color` (`amber`), `size` (`20`), `gap` (`4`), `tooltips[]`, `disabled`, `readOnly` |
|
| `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 |
|
| `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 |
|
| `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:**
|
**Gösterim/geri bildirim bileşenleri:**
|
||||||
|
|
||||||
|
|
|
||||||
13
README.md
13
README.md
|
|
@ -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,
|
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
|
ü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`
|
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`
|
uygun klasör) → `runtimeNodes.ts` sözlüğü → `catalog.ts` tanımı (`kind: 'custom'`) → `LanguagesData.json`
|
||||||
açıklama anahtarı.
|
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
|
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
|
ürettiği kodu düğüm ağacına çevirir (`isDesignerGeneratedCode`), aksi hâlde tanımadığı JSX'i
|
||||||
düşürüp kaydederken kodu yok ederdi.
|
düşürüp kaydederken kodu yok ederdi.
|
||||||
|
|
|
||||||
|
|
@ -28986,6 +28986,18 @@
|
||||||
"en": "Text field with a camera button that reads barcodes and QR codes.",
|
"en": "Text field with a camera button that reads barcodes and QR codes.",
|
||||||
"tr": "Barkod ve QR okuyan kamera butonlu metin alanı."
|
"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",
|
"resourceName": "Platform",
|
||||||
"key": "App.Geolocation.Unsupported",
|
"key": "App.Geolocation.Unsupported",
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import PlatformIcon from './PlatformIcon'
|
import PlatformIcon from './PlatformIcon'
|
||||||
import { BarcodeScanField } from '@/components/shared'
|
import { BarcodeScanField, BarcodeViewer } from '@/components/shared'
|
||||||
import {
|
import {
|
||||||
PublicDemoButton,
|
PublicDemoButton,
|
||||||
PublicHeaderBar,
|
PublicHeaderBar,
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
export const RUNTIME_NODE_COMPONENTS = {
|
export const RUNTIME_NODE_COMPONENTS = {
|
||||||
PlatformIcon,
|
PlatformIcon,
|
||||||
BarcodeScanField,
|
BarcodeScanField,
|
||||||
|
BarcodeViewer,
|
||||||
PublicHeaderBar,
|
PublicHeaderBar,
|
||||||
PublicLogo,
|
PublicLogo,
|
||||||
PublicLanguageSelector,
|
PublicLanguageSelector,
|
||||||
|
|
|
||||||
76
ui/src/components/shared/BarcodeViewer.tsx
Normal file
76
ui/src/components/shared/BarcodeViewer.tsx
Normal 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
|
||||||
|
|
@ -3,6 +3,7 @@ export { default as AdaptableCard } from './AdaptableCard'
|
||||||
export { default as Affix } from './Affix'
|
export { default as Affix } from './Affix'
|
||||||
export { default as AuthorityCheck } from './AuthorityCheck'
|
export { default as AuthorityCheck } from './AuthorityCheck'
|
||||||
export { BarcodeScanField, BarcodeScannerDialog, BarcodeScannerHost } from './BarcodeScanner'
|
export { BarcodeScanField, BarcodeScannerDialog, BarcodeScannerHost } from './BarcodeScanner'
|
||||||
|
export { default as BarcodeViewer } from './BarcodeViewer'
|
||||||
export { default as PermissionCheck } from './PermissionCheck'
|
export { default as PermissionCheck } from './PermissionCheck'
|
||||||
export { default as ConfirmDialog } from './ConfirmDialog'
|
export { default as ConfirmDialog } from './ConfirmDialog'
|
||||||
export { default as Container } from './Container'
|
export { default as Container } from './Container'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { CUSTOM_COMPONENTS, HTML_ELEMENTS } from '@/components/codeLayout/data/componentDefinitions'
|
import { CUSTOM_COMPONENTS, HTML_ELEMENTS } from '@/components/codeLayout/data/componentDefinitions'
|
||||||
import generatedComponentProps from './generated/componentProps.json'
|
import generatedComponentProps from './generated/componentProps.json'
|
||||||
|
import { BARCODE_WRITE_FORMATS } from '@/utils/barcode'
|
||||||
import {
|
import {
|
||||||
DESIGNER_DATA_COMPONENT_NAMES,
|
DESIGNER_DATA_COMPONENT_NAMES,
|
||||||
FORM_EVENTS,
|
FORM_EVENTS,
|
||||||
|
|
@ -1252,6 +1253,32 @@ export const DESIGNER_EXTRAS: DesignerComponentDefinition[] = [
|
||||||
],
|
],
|
||||||
hooks: [],
|
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',
|
name: 'Spacer',
|
||||||
icon: 'Space',
|
icon: 'Space',
|
||||||
|
|
|
||||||
|
|
@ -117,3 +117,81 @@ export const stopCameraStream = (stream?: MediaStream | null) =>
|
||||||
|
|
||||||
/** Okuma geri bildirimi. Desteklemeyen cihazda sessizce atlanır. */
|
/** Okuma geri bildirimi. Desteklemeyen cihazda sessizce atlanır. */
|
||||||
export const vibrateOnScan = () => navigator.vibrate?.(60)
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue