@ramathibodi/nuxt-commons 0.1.74 → 0.1.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +115 -115
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Alert.vue +58 -58
  4. package/dist/runtime/components/BarcodeReader.vue +130 -130
  5. package/dist/runtime/components/ExportCSV.vue +110 -110
  6. package/dist/runtime/components/FileBtn.vue +79 -79
  7. package/dist/runtime/components/ImportCSV.vue +151 -151
  8. package/dist/runtime/components/MrzReader.vue +168 -168
  9. package/dist/runtime/components/SplitterPanel.vue +67 -67
  10. package/dist/runtime/components/TabsGroup.vue +39 -39
  11. package/dist/runtime/components/TextBarcode.vue +66 -66
  12. package/dist/runtime/components/device/IdCardButton.vue +95 -95
  13. package/dist/runtime/components/device/IdCardWebSocket.vue +207 -207
  14. package/dist/runtime/components/device/Scanner.vue +350 -350
  15. package/dist/runtime/components/dialog/Confirm.vue +112 -112
  16. package/dist/runtime/components/dialog/Host.vue +88 -88
  17. package/dist/runtime/components/dialog/Index.vue +84 -84
  18. package/dist/runtime/components/dialog/Loading.vue +51 -51
  19. package/dist/runtime/components/dialog/default/Confirm.vue +112 -112
  20. package/dist/runtime/components/dialog/default/Loading.vue +60 -60
  21. package/dist/runtime/components/dialog/default/Notify.vue +82 -82
  22. package/dist/runtime/components/dialog/default/Printing.vue +46 -46
  23. package/dist/runtime/components/dialog/default/VerifyUser.vue +144 -144
  24. package/dist/runtime/components/document/Form.vue +50 -50
  25. package/dist/runtime/components/document/TemplateBuilder.vue +536 -536
  26. package/dist/runtime/components/form/ActionPad.vue +156 -156
  27. package/dist/runtime/components/form/Birthdate.vue +116 -116
  28. package/dist/runtime/components/form/CheckboxGroup.vue +99 -99
  29. package/dist/runtime/components/form/CodeEditor.vue +45 -45
  30. package/dist/runtime/components/form/Date.vue +270 -270
  31. package/dist/runtime/components/form/DateTime.vue +220 -220
  32. package/dist/runtime/components/form/Dialog.vue +178 -178
  33. package/dist/runtime/components/form/EditPad.vue +157 -157
  34. package/dist/runtime/components/form/File.vue +295 -295
  35. package/dist/runtime/components/form/Hidden.vue +44 -44
  36. package/dist/runtime/components/form/Iterator.vue +538 -538
  37. package/dist/runtime/components/form/Login.vue +143 -143
  38. package/dist/runtime/components/form/Pad.vue +399 -399
  39. package/dist/runtime/components/form/SignPad.vue +226 -226
  40. package/dist/runtime/components/form/System.vue +34 -34
  41. package/dist/runtime/components/form/Table.vue +391 -391
  42. package/dist/runtime/components/form/TableData.vue +236 -236
  43. package/dist/runtime/components/form/Time.vue +177 -177
  44. package/dist/runtime/components/form/images/Capture.vue +245 -245
  45. package/dist/runtime/components/form/images/Edit.vue +133 -133
  46. package/dist/runtime/components/form/images/Field.vue +331 -331
  47. package/dist/runtime/components/form/images/Pad.vue +54 -54
  48. package/dist/runtime/components/label/Date.vue +37 -37
  49. package/dist/runtime/components/label/DateAgo.vue +102 -102
  50. package/dist/runtime/components/label/DateCount.vue +152 -152
  51. package/dist/runtime/components/label/Field.vue +111 -111
  52. package/dist/runtime/components/label/FormatMoney.vue +37 -37
  53. package/dist/runtime/components/label/Mask.vue +46 -46
  54. package/dist/runtime/components/label/Object.vue +21 -21
  55. package/dist/runtime/components/master/Autocomplete.vue +89 -89
  56. package/dist/runtime/components/master/Combobox.vue +88 -88
  57. package/dist/runtime/components/master/RadioGroup.vue +90 -90
  58. package/dist/runtime/components/master/Select.vue +70 -70
  59. package/dist/runtime/components/master/label.vue +55 -55
  60. package/dist/runtime/components/model/Autocomplete.vue +91 -91
  61. package/dist/runtime/components/model/Combobox.vue +90 -90
  62. package/dist/runtime/components/model/Pad.vue +114 -114
  63. package/dist/runtime/components/model/Select.vue +78 -84
  64. package/dist/runtime/components/model/Table.vue +370 -370
  65. package/dist/runtime/components/model/iterator.vue +497 -497
  66. package/dist/runtime/components/model/label.vue +58 -58
  67. package/dist/runtime/components/pdf/Print.vue +75 -75
  68. package/dist/runtime/components/pdf/View.vue +146 -146
  69. package/dist/runtime/composables/dialog.d.ts +1 -1
  70. package/dist/runtime/composables/graphql.d.ts +1 -1
  71. package/dist/runtime/composables/graphqlModel.d.ts +9 -9
  72. package/dist/runtime/composables/graphqlModelItem.d.ts +7 -7
  73. package/dist/runtime/composables/graphqlModelOperation.d.ts +6 -6
  74. package/dist/runtime/composables/userPermission.d.ts +1 -1
  75. package/dist/runtime/labs/Calendar.vue +99 -99
  76. package/dist/runtime/labs/form/EditMobile.vue +152 -152
  77. package/dist/runtime/labs/form/TextFieldMask.vue +43 -43
  78. package/dist/runtime/plugins/clientConfig.d.ts +1 -1
  79. package/dist/runtime/plugins/default.d.ts +1 -1
  80. package/dist/runtime/plugins/dialogManager.d.ts +1 -1
  81. package/dist/runtime/plugins/permission.d.ts +1 -1
  82. package/dist/runtime/types/alert.d.ts +11 -11
  83. package/dist/runtime/types/clientConfig.d.ts +13 -13
  84. package/dist/runtime/types/dialogManager.d.ts +35 -35
  85. package/dist/runtime/types/formDialog.d.ts +5 -5
  86. package/dist/runtime/types/graphqlOperation.d.ts +23 -23
  87. package/dist/runtime/types/menu.d.ts +31 -31
  88. package/dist/runtime/types/modules.d.ts +7 -7
  89. package/dist/runtime/types/permission.d.ts +13 -13
  90. package/package.json +131 -131
  91. package/scripts/enrich-vue-docs-from-ai.mjs +197 -197
  92. package/scripts/generate-ai-summary.mjs +321 -321
  93. package/scripts/generate-composables-md.mjs +129 -129
  94. package/scripts/postInstall.cjs +70 -70
  95. package/templates/.codegen/codegen.ts +32 -32
  96. package/templates/.codegen/plugin-schema-object.js +161 -161
@@ -1,151 +1,151 @@
1
- <script lang="ts" setup>
2
- /**
3
- * ImportCSV imports spreadsheet rows, maps nested object keys, and emits normalized records for downstream processing.
4
- * This doc block is consumed by vue-docgen for generated API documentation.
5
- */
6
- import * as XLSX from 'xlsx'
7
- import { ref } from 'vue'
8
- import { useAlert } from '../composables/alert'
9
- import { VBtn } from 'vuetify/components/VBtn'
10
-
11
- interface ImportButtonProps extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
12
- stringFields?: Array<string> // Field paths that must stay as string values (no nested object conversion).
13
- tooltip?: string | Record<string,any> | undefined // Tooltip text or config object shown for the action control.
14
- }
15
-
16
- /**
17
- * Public props accepted by ImportCSV.
18
- * Document each prop field with intent, defaults, and side effects for clear generated docs.
19
- */
20
- const props = withDefaults(defineProps<ImportButtonProps>(), {
21
- stringFields: ()=>[],
22
- tooltip: ()=>({text: 'Import', location: 'bottom'}),
23
- })
24
-
25
- const alert = useAlert()
26
- /**
27
- * Custom events emitted by ImportCSV.
28
- * Parents can listen to these events to react to user actions and internal state changes.
29
- */
30
- const emit = defineEmits<{
31
- (e: 'import', value: object[]): void
32
- }>()
33
-
34
- const loading = ref(false)
35
- const fileBtnRef = ref()
36
-
37
- function uploadedFile(files: File[] | File | undefined) {
38
- if (!files) return
39
-
40
- if (Array.isArray(files) && files.length != 1) {
41
- alert?.addAlert({ message: 'Please select a single file for import', alertType: 'error' })
42
- return
43
- }
44
-
45
- if (Array.isArray(files)) files = files[0]
46
-
47
- const fileExtension = files.name.slice(files.name.lastIndexOf('.')).toLowerCase()
48
- if (!['.xlsx', '.csv'].includes(fileExtension)) {
49
- alert?.addAlert({ message: `Please upload a file with .csv or .xlsx extension only (${files.name})`, alertType: 'error' })
50
- return
51
- }
52
-
53
- const reader = new FileReader()
54
- reader.onload = (e: ProgressEvent<FileReader>) => {
55
- const workbook = XLSX.read(e.target?.result)
56
- const parsedData = parseAndAggregateColumns(
57
- XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]])
58
- )
59
- emit('import', parsedData)
60
- loading.value = false
61
- fileBtnRef.value.reset()
62
- }
63
- loading.value = true
64
- reader.readAsArrayBuffer(files)
65
- }
66
-
67
- /**
68
- * Recursively aggregates nested columns and properties.
69
- * @param items - Array of data from the Excel sheet
70
- */
71
- const parseAndAggregateColumns = (items: any[]) => {
72
- return items.map((item: any) => {
73
- const aggregatedItem: any = {}
74
-
75
- for (const key in item) {
76
- if (key.includes('.')) {
77
- // Extract root key and subKey
78
- const [rootKey, ...subKeys] = key.split('.')
79
-
80
- // Recursively aggregate subKeys
81
- aggregatedItem[rootKey] = aggregatedItem[rootKey] || {}
82
- assignNestedValue(aggregatedItem[rootKey], subKeys, parseIfJson(item[key]))
83
- } else {
84
- // Directly assign root-level properties
85
- if (props.stringFields.includes(key)) aggregatedItem[key] = item[key]
86
- else aggregatedItem[key] = parseIfJson(item[key])
87
- }
88
- }
89
-
90
- return aggregatedItem
91
- })
92
- }
93
-
94
- /**
95
- * Recursively assigns a value to a nested key structure.
96
- * @param obj - The object to assign to
97
- * @param keys - Array of keys leading to the final property
98
- * @param value - The value to assign
99
- */
100
- const assignNestedValue = (obj: any, keys: string[], value: any) => {
101
- const [currentKey, ...remainingKeys] = keys
102
- if (remainingKeys.length === 0) {
103
- obj[currentKey] = value
104
- } else {
105
- obj[currentKey] = obj[currentKey] || {}
106
- assignNestedValue(obj[currentKey], remainingKeys, value)
107
- }
108
- }
109
-
110
- /**
111
- * Attempt to parse a value as JSON or array.
112
- * @param value - The value to parse
113
- */
114
- const parseIfJson = (value: any) => {
115
- if (typeof value === 'string') {
116
- try {
117
- let parsedValue = JSON.parse(value)
118
- return (parsedValue==value) ? value : parsedValue
119
- } catch {
120
- // If parsing fails, return the original value
121
- return value
122
- }
123
- }
124
- return value
125
- }
126
- </script>
127
-
128
- <template>
129
- <FileBtn
130
- ref="fileBtnRef"
131
- v-bind="$attrs"
132
- color="primary"
133
- :loading="loading"
134
- text="Import CSV"
135
- accept=".csv, .xlsx"
136
- :multiple="false"
137
- @update:model-value="uploadedFile"
138
- :tooltip="props.tooltip"
139
- >
140
- <template
141
- v-for="(_, name, index) in ($slots as {})"
142
- :key="index"
143
- #[name]="slotData"
144
- >
145
- <slot
146
- :name="name"
147
- v-bind="((slotData || {}) as object)"
148
- />
149
- </template>
150
- </FileBtn>
151
- </template>
1
+ <script lang="ts" setup>
2
+ /**
3
+ * ImportCSV imports spreadsheet rows, maps nested object keys, and emits normalized records for downstream processing.
4
+ * This doc block is consumed by vue-docgen for generated API documentation.
5
+ */
6
+ import * as XLSX from 'xlsx'
7
+ import { ref } from 'vue'
8
+ import { useAlert } from '../composables/alert'
9
+ import { VBtn } from 'vuetify/components/VBtn'
10
+
11
+ interface ImportButtonProps extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
12
+ stringFields?: Array<string> // Field paths that must stay as string values (no nested object conversion).
13
+ tooltip?: string | Record<string,any> | undefined // Tooltip text or config object shown for the action control.
14
+ }
15
+
16
+ /**
17
+ * Public props accepted by ImportCSV.
18
+ * Document each prop field with intent, defaults, and side effects for clear generated docs.
19
+ */
20
+ const props = withDefaults(defineProps<ImportButtonProps>(), {
21
+ stringFields: ()=>[],
22
+ tooltip: ()=>({text: 'Import', location: 'bottom'}),
23
+ })
24
+
25
+ const alert = useAlert()
26
+ /**
27
+ * Custom events emitted by ImportCSV.
28
+ * Parents can listen to these events to react to user actions and internal state changes.
29
+ */
30
+ const emit = defineEmits<{
31
+ (e: 'import', value: object[]): void
32
+ }>()
33
+
34
+ const loading = ref(false)
35
+ const fileBtnRef = ref()
36
+
37
+ function uploadedFile(files: File[] | File | undefined) {
38
+ if (!files) return
39
+
40
+ if (Array.isArray(files) && files.length != 1) {
41
+ alert?.addAlert({ message: 'Please select a single file for import', alertType: 'error' })
42
+ return
43
+ }
44
+
45
+ if (Array.isArray(files)) files = files[0]
46
+
47
+ const fileExtension = files.name.slice(files.name.lastIndexOf('.')).toLowerCase()
48
+ if (!['.xlsx', '.csv'].includes(fileExtension)) {
49
+ alert?.addAlert({ message: `Please upload a file with .csv or .xlsx extension only (${files.name})`, alertType: 'error' })
50
+ return
51
+ }
52
+
53
+ const reader = new FileReader()
54
+ reader.onload = (e: ProgressEvent<FileReader>) => {
55
+ const workbook = XLSX.read(e.target?.result)
56
+ const parsedData = parseAndAggregateColumns(
57
+ XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]])
58
+ )
59
+ emit('import', parsedData)
60
+ loading.value = false
61
+ fileBtnRef.value.reset()
62
+ }
63
+ loading.value = true
64
+ reader.readAsArrayBuffer(files)
65
+ }
66
+
67
+ /**
68
+ * Recursively aggregates nested columns and properties.
69
+ * @param items - Array of data from the Excel sheet
70
+ */
71
+ const parseAndAggregateColumns = (items: any[]) => {
72
+ return items.map((item: any) => {
73
+ const aggregatedItem: any = {}
74
+
75
+ for (const key in item) {
76
+ if (key.includes('.')) {
77
+ // Extract root key and subKey
78
+ const [rootKey, ...subKeys] = key.split('.')
79
+
80
+ // Recursively aggregate subKeys
81
+ aggregatedItem[rootKey] = aggregatedItem[rootKey] || {}
82
+ assignNestedValue(aggregatedItem[rootKey], subKeys, parseIfJson(item[key]))
83
+ } else {
84
+ // Directly assign root-level properties
85
+ if (props.stringFields.includes(key)) aggregatedItem[key] = item[key]
86
+ else aggregatedItem[key] = parseIfJson(item[key])
87
+ }
88
+ }
89
+
90
+ return aggregatedItem
91
+ })
92
+ }
93
+
94
+ /**
95
+ * Recursively assigns a value to a nested key structure.
96
+ * @param obj - The object to assign to
97
+ * @param keys - Array of keys leading to the final property
98
+ * @param value - The value to assign
99
+ */
100
+ const assignNestedValue = (obj: any, keys: string[], value: any) => {
101
+ const [currentKey, ...remainingKeys] = keys
102
+ if (remainingKeys.length === 0) {
103
+ obj[currentKey] = value
104
+ } else {
105
+ obj[currentKey] = obj[currentKey] || {}
106
+ assignNestedValue(obj[currentKey], remainingKeys, value)
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Attempt to parse a value as JSON or array.
112
+ * @param value - The value to parse
113
+ */
114
+ const parseIfJson = (value: any) => {
115
+ if (typeof value === 'string') {
116
+ try {
117
+ let parsedValue = JSON.parse(value)
118
+ return (parsedValue==value) ? value : parsedValue
119
+ } catch {
120
+ // If parsing fails, return the original value
121
+ return value
122
+ }
123
+ }
124
+ return value
125
+ }
126
+ </script>
127
+
128
+ <template>
129
+ <FileBtn
130
+ ref="fileBtnRef"
131
+ v-bind="$attrs"
132
+ color="primary"
133
+ :loading="loading"
134
+ text="Import CSV"
135
+ accept=".csv, .xlsx"
136
+ :multiple="false"
137
+ @update:model-value="uploadedFile"
138
+ :tooltip="props.tooltip"
139
+ >
140
+ <template
141
+ v-for="(_, name, index) in ($slots as {})"
142
+ :key="index"
143
+ #[name]="slotData"
144
+ >
145
+ <slot
146
+ :name="name"
147
+ v-bind="((slotData || {}) as object)"
148
+ />
149
+ </template>
150
+ </FileBtn>
151
+ </template>
@@ -1,168 +1,168 @@
1
- <script lang="ts" setup>
2
- /**
3
- * MrzReader captures passport/ID images from camera or upload, runs OCR + MRZ parsing,
4
- * and emits normalized MRZ data once checksum validation passes.
5
- */
6
- import { computed, onBeforeUnmount, onMounted, ref, watchEffect } from 'vue'
7
- import { useDevicesList, useUserMedia } from '@vueuse/core'
8
- import { useAlert } from '../composables/alert'
9
- import { useMrzReader, type MrzResult } from '../composables/useMrzReader'
10
-
11
- const mrzReaderControl = ref<number | null>(null)
12
-
13
- const alert = useAlert()
14
- const isLoading = ref(false)
15
-
16
- interface Props {
17
- intervalMs?: number // Scan interval (ms) for periodic camera-frame OCR attempts.
18
- scaleFactor?: number // Upscale factor applied before OCR to improve small-text readability.
19
- useOpenCv?: boolean // Enables OpenCV-assisted region detection before OCR.
20
- }
21
-
22
- const props = withDefaults(defineProps<Props>(), {
23
- intervalMs: 900,
24
- scaleFactor: 2,
25
- useOpenCv: true,
26
- })
27
-
28
- const emit = defineEmits<{
29
- (event: 'decode', value: MrzResult): void // Emitted when a valid MRZ payload is parsed.
30
- (event: 'error', error: string | unknown): void // Emitted when camera/OCR/parsing fails.
31
- }>()
32
-
33
- const videoScreen = ref<HTMLVideoElement>()
34
-
35
- const currentCameraId = ref<ConstrainDOMString | undefined>()
36
- const { videoInputs: cameras } = useDevicesList({
37
- requestPermissions: true,
38
- constraints: { audio: false, video: true },
39
- onUpdated() {
40
- if (!cameras.value.find(camera => camera.deviceId === currentCameraId.value))
41
- currentCameraId.value = cameras.value[0]?.deviceId
42
- },
43
- })
44
-
45
- const hasCamera = computed(() => !!currentCameraId.value)
46
-
47
- const { stream, start: cameraStart, stop: cameraStop, enabled: cameraEnabled } = useUserMedia({
48
- constraints: { video: { deviceId: currentCameraId.value } },
49
- })
50
-
51
- watchEffect(() => {
52
- if (videoScreen.value)
53
- videoScreen.value.srcObject = stream.value ?? null
54
- })
55
-
56
- const mrzReader = useMrzReader({
57
- scaleFactor: props.scaleFactor,
58
- useOpenCv: props.useOpenCv,
59
- lang: 'ocrb',
60
- langPath: '/tesseract/',
61
- })
62
-
63
- async function scanOnce() {
64
- if (!videoScreen.value || !cameraEnabled.value) return
65
-
66
- try {
67
- const parsed = await mrzReader.decodeFromVideoElement(videoScreen.value)
68
- if (parsed) {
69
- emit('decode', parsed)
70
- stopCamera()
71
- }
72
- }
73
- catch (err) {
74
- emit('error', err)
75
- }
76
- }
77
-
78
- function startCamera() {
79
- if (cameraEnabled.value) return
80
-
81
- isLoading.value = true
82
- cameraStart()
83
- .then(() => {
84
- if (mrzReaderControl.value) {
85
- window.clearInterval(mrzReaderControl.value)
86
- mrzReaderControl.value = null
87
- }
88
-
89
- mrzReaderControl.value = window.setInterval(() => {
90
- void scanOnce()
91
- }, props.intervalMs)
92
- })
93
- .catch((err) => {
94
- emit('error', err)
95
- })
96
- .finally(() => {
97
- isLoading.value = false
98
- })
99
- }
100
-
101
- function stopCamera() {
102
- if (mrzReaderControl.value) {
103
- window.clearInterval(mrzReaderControl.value)
104
- mrzReaderControl.value = null
105
- }
106
- if (cameraEnabled.value)
107
- cameraStop()
108
- }
109
-
110
- function scanImageFile(selectedFile: File | File[] | undefined) {
111
- if (!selectedFile) {
112
- alert?.addAlert({ message: 'No file selected.', alertType: 'error' })
113
- return
114
- }
115
-
116
- const file = Array.isArray(selectedFile) ? selectedFile[0] : selectedFile
117
-
118
- void mrzReader.decodeFromImageFile(file)
119
- .then((parsed) => {
120
- if (parsed)
121
- emit('decode', parsed)
122
- else
123
- alert?.addAlert({ message: 'MRZ not found or checksum invalid.', alertType: 'warning' })
124
- })
125
- .catch((err) => {
126
- emit('error', err)
127
- })
128
- }
129
-
130
- onMounted(() => {
131
- void mrzReader.ensureOpenCvReady()
132
- startCamera()
133
- })
134
-
135
- onBeforeUnmount(() => {
136
- stopCamera()
137
- })
138
- </script>
139
-
140
- <template>
141
- <v-card flat>
142
- <v-card-text class="d-flex justify-center" v-if="isLoading">
143
- <v-progress-circular indeterminate />
144
- </v-card-text>
145
- <v-card-text v-else>
146
- <v-col v-if="hasCamera">
147
- <div style="position: relative; display: inline-block; width: 100%;" :style="{ maxWidth: '1024px' }">
148
- <video autoplay ref="videoScreen" width="100%" :style="{ maxWidth: '1024px' }"></video>
149
- <div style="position: absolute; bottom: 10px; right: 10px; z-index: 2000;">
150
- <FileBtn
151
- accept="image/*"
152
- icon="mdi mdi-image-plus"
153
- icon-only
154
- @update:model-value="scanImageFile"
155
- />
156
- </div>
157
- </div>
158
- </v-col>
159
- <v-col v-else>
160
- <FileBtn
161
- accept="image/*"
162
- text="Upload Image"
163
- @update:model-value="scanImageFile"
164
- />
165
- </v-col>
166
- </v-card-text>
167
- </v-card>
168
- </template>
1
+ <script lang="ts" setup>
2
+ /**
3
+ * MrzReader captures passport/ID images from camera or upload, runs OCR + MRZ parsing,
4
+ * and emits normalized MRZ data once checksum validation passes.
5
+ */
6
+ import { computed, onBeforeUnmount, onMounted, ref, watchEffect } from 'vue'
7
+ import { useDevicesList, useUserMedia } from '@vueuse/core'
8
+ import { useAlert } from '../composables/alert'
9
+ import { useMrzReader, type MrzResult } from '../composables/useMrzReader'
10
+
11
+ const mrzReaderControl = ref<number | null>(null)
12
+
13
+ const alert = useAlert()
14
+ const isLoading = ref(false)
15
+
16
+ interface Props {
17
+ intervalMs?: number // Scan interval (ms) for periodic camera-frame OCR attempts.
18
+ scaleFactor?: number // Upscale factor applied before OCR to improve small-text readability.
19
+ useOpenCv?: boolean // Enables OpenCV-assisted region detection before OCR.
20
+ }
21
+
22
+ const props = withDefaults(defineProps<Props>(), {
23
+ intervalMs: 900,
24
+ scaleFactor: 2,
25
+ useOpenCv: true,
26
+ })
27
+
28
+ const emit = defineEmits<{
29
+ (event: 'decode', value: MrzResult): void // Emitted when a valid MRZ payload is parsed.
30
+ (event: 'error', error: string | unknown): void // Emitted when camera/OCR/parsing fails.
31
+ }>()
32
+
33
+ const videoScreen = ref<HTMLVideoElement>()
34
+
35
+ const currentCameraId = ref<ConstrainDOMString | undefined>()
36
+ const { videoInputs: cameras } = useDevicesList({
37
+ requestPermissions: true,
38
+ constraints: { audio: false, video: true },
39
+ onUpdated() {
40
+ if (!cameras.value.find(camera => camera.deviceId === currentCameraId.value))
41
+ currentCameraId.value = cameras.value[0]?.deviceId
42
+ },
43
+ })
44
+
45
+ const hasCamera = computed(() => !!currentCameraId.value)
46
+
47
+ const { stream, start: cameraStart, stop: cameraStop, enabled: cameraEnabled } = useUserMedia({
48
+ constraints: { video: { deviceId: currentCameraId.value } },
49
+ })
50
+
51
+ watchEffect(() => {
52
+ if (videoScreen.value)
53
+ videoScreen.value.srcObject = stream.value ?? null
54
+ })
55
+
56
+ const mrzReader = useMrzReader({
57
+ scaleFactor: props.scaleFactor,
58
+ useOpenCv: props.useOpenCv,
59
+ lang: 'ocrb',
60
+ langPath: '/tesseract/',
61
+ })
62
+
63
+ async function scanOnce() {
64
+ if (!videoScreen.value || !cameraEnabled.value) return
65
+
66
+ try {
67
+ const parsed = await mrzReader.decodeFromVideoElement(videoScreen.value)
68
+ if (parsed) {
69
+ emit('decode', parsed)
70
+ stopCamera()
71
+ }
72
+ }
73
+ catch (err) {
74
+ emit('error', err)
75
+ }
76
+ }
77
+
78
+ function startCamera() {
79
+ if (cameraEnabled.value) return
80
+
81
+ isLoading.value = true
82
+ cameraStart()
83
+ .then(() => {
84
+ if (mrzReaderControl.value) {
85
+ window.clearInterval(mrzReaderControl.value)
86
+ mrzReaderControl.value = null
87
+ }
88
+
89
+ mrzReaderControl.value = window.setInterval(() => {
90
+ void scanOnce()
91
+ }, props.intervalMs)
92
+ })
93
+ .catch((err) => {
94
+ emit('error', err)
95
+ })
96
+ .finally(() => {
97
+ isLoading.value = false
98
+ })
99
+ }
100
+
101
+ function stopCamera() {
102
+ if (mrzReaderControl.value) {
103
+ window.clearInterval(mrzReaderControl.value)
104
+ mrzReaderControl.value = null
105
+ }
106
+ if (cameraEnabled.value)
107
+ cameraStop()
108
+ }
109
+
110
+ function scanImageFile(selectedFile: File | File[] | undefined) {
111
+ if (!selectedFile) {
112
+ alert?.addAlert({ message: 'No file selected.', alertType: 'error' })
113
+ return
114
+ }
115
+
116
+ const file = Array.isArray(selectedFile) ? selectedFile[0] : selectedFile
117
+
118
+ void mrzReader.decodeFromImageFile(file)
119
+ .then((parsed) => {
120
+ if (parsed)
121
+ emit('decode', parsed)
122
+ else
123
+ alert?.addAlert({ message: 'MRZ not found or checksum invalid.', alertType: 'warning' })
124
+ })
125
+ .catch((err) => {
126
+ emit('error', err)
127
+ })
128
+ }
129
+
130
+ onMounted(() => {
131
+ void mrzReader.ensureOpenCvReady()
132
+ startCamera()
133
+ })
134
+
135
+ onBeforeUnmount(() => {
136
+ stopCamera()
137
+ })
138
+ </script>
139
+
140
+ <template>
141
+ <v-card flat>
142
+ <v-card-text class="d-flex justify-center" v-if="isLoading">
143
+ <v-progress-circular indeterminate />
144
+ </v-card-text>
145
+ <v-card-text v-else>
146
+ <v-col v-if="hasCamera">
147
+ <div style="position: relative; display: inline-block; width: 100%;" :style="{ maxWidth: '1024px' }">
148
+ <video autoplay ref="videoScreen" width="100%" :style="{ maxWidth: '1024px' }"></video>
149
+ <div style="position: absolute; bottom: 10px; right: 10px; z-index: 2000;">
150
+ <FileBtn
151
+ accept="image/*"
152
+ icon="mdi mdi-image-plus"
153
+ icon-only
154
+ @update:model-value="scanImageFile"
155
+ />
156
+ </div>
157
+ </div>
158
+ </v-col>
159
+ <v-col v-else>
160
+ <FileBtn
161
+ accept="image/*"
162
+ text="Upload Image"
163
+ @update:model-value="scanImageFile"
164
+ />
165
+ </v-col>
166
+ </v-card-text>
167
+ </v-card>
168
+ </template>