@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.
- package/README.md +115 -115
- package/dist/module.json +1 -1
- package/dist/runtime/components/Alert.vue +58 -58
- package/dist/runtime/components/BarcodeReader.vue +130 -130
- package/dist/runtime/components/ExportCSV.vue +110 -110
- package/dist/runtime/components/FileBtn.vue +79 -79
- package/dist/runtime/components/ImportCSV.vue +151 -151
- package/dist/runtime/components/MrzReader.vue +168 -168
- package/dist/runtime/components/SplitterPanel.vue +67 -67
- package/dist/runtime/components/TabsGroup.vue +39 -39
- package/dist/runtime/components/TextBarcode.vue +66 -66
- package/dist/runtime/components/device/IdCardButton.vue +95 -95
- package/dist/runtime/components/device/IdCardWebSocket.vue +207 -207
- package/dist/runtime/components/device/Scanner.vue +350 -350
- package/dist/runtime/components/dialog/Confirm.vue +112 -112
- package/dist/runtime/components/dialog/Host.vue +88 -88
- package/dist/runtime/components/dialog/Index.vue +84 -84
- package/dist/runtime/components/dialog/Loading.vue +51 -51
- package/dist/runtime/components/dialog/default/Confirm.vue +112 -112
- package/dist/runtime/components/dialog/default/Loading.vue +60 -60
- package/dist/runtime/components/dialog/default/Notify.vue +82 -82
- package/dist/runtime/components/dialog/default/Printing.vue +46 -46
- package/dist/runtime/components/dialog/default/VerifyUser.vue +144 -144
- package/dist/runtime/components/document/Form.vue +50 -50
- package/dist/runtime/components/document/TemplateBuilder.vue +536 -536
- package/dist/runtime/components/form/ActionPad.vue +156 -156
- package/dist/runtime/components/form/Birthdate.vue +116 -116
- package/dist/runtime/components/form/CheckboxGroup.vue +99 -99
- package/dist/runtime/components/form/CodeEditor.vue +45 -45
- package/dist/runtime/components/form/Date.vue +270 -270
- package/dist/runtime/components/form/DateTime.vue +220 -220
- package/dist/runtime/components/form/Dialog.vue +178 -178
- package/dist/runtime/components/form/EditPad.vue +157 -157
- package/dist/runtime/components/form/File.vue +295 -295
- package/dist/runtime/components/form/Hidden.vue +44 -44
- package/dist/runtime/components/form/Iterator.vue +538 -538
- package/dist/runtime/components/form/Login.vue +143 -143
- package/dist/runtime/components/form/Pad.vue +399 -399
- package/dist/runtime/components/form/SignPad.vue +226 -226
- package/dist/runtime/components/form/System.vue +34 -34
- package/dist/runtime/components/form/Table.vue +391 -391
- package/dist/runtime/components/form/TableData.vue +236 -236
- package/dist/runtime/components/form/Time.vue +177 -177
- package/dist/runtime/components/form/images/Capture.vue +245 -245
- package/dist/runtime/components/form/images/Edit.vue +133 -133
- package/dist/runtime/components/form/images/Field.vue +331 -331
- package/dist/runtime/components/form/images/Pad.vue +54 -54
- package/dist/runtime/components/label/Date.vue +37 -37
- package/dist/runtime/components/label/DateAgo.vue +102 -102
- package/dist/runtime/components/label/DateCount.vue +152 -152
- package/dist/runtime/components/label/Field.vue +111 -111
- package/dist/runtime/components/label/FormatMoney.vue +37 -37
- package/dist/runtime/components/label/Mask.vue +46 -46
- package/dist/runtime/components/label/Object.vue +21 -21
- package/dist/runtime/components/master/Autocomplete.vue +89 -89
- package/dist/runtime/components/master/Combobox.vue +88 -88
- package/dist/runtime/components/master/RadioGroup.vue +90 -90
- package/dist/runtime/components/master/Select.vue +70 -70
- package/dist/runtime/components/master/label.vue +55 -55
- package/dist/runtime/components/model/Autocomplete.vue +91 -91
- package/dist/runtime/components/model/Combobox.vue +90 -90
- package/dist/runtime/components/model/Pad.vue +114 -114
- package/dist/runtime/components/model/Select.vue +78 -84
- package/dist/runtime/components/model/Table.vue +370 -370
- package/dist/runtime/components/model/iterator.vue +497 -497
- package/dist/runtime/components/model/label.vue +58 -58
- package/dist/runtime/components/pdf/Print.vue +75 -75
- package/dist/runtime/components/pdf/View.vue +146 -146
- package/dist/runtime/composables/dialog.d.ts +1 -1
- package/dist/runtime/composables/graphql.d.ts +1 -1
- package/dist/runtime/composables/graphqlModel.d.ts +9 -9
- package/dist/runtime/composables/graphqlModelItem.d.ts +7 -7
- package/dist/runtime/composables/graphqlModelOperation.d.ts +6 -6
- package/dist/runtime/composables/userPermission.d.ts +1 -1
- package/dist/runtime/labs/Calendar.vue +99 -99
- package/dist/runtime/labs/form/EditMobile.vue +152 -152
- package/dist/runtime/labs/form/TextFieldMask.vue +43 -43
- package/dist/runtime/plugins/clientConfig.d.ts +1 -1
- package/dist/runtime/plugins/default.d.ts +1 -1
- package/dist/runtime/plugins/dialogManager.d.ts +1 -1
- package/dist/runtime/plugins/permission.d.ts +1 -1
- package/dist/runtime/types/alert.d.ts +11 -11
- package/dist/runtime/types/clientConfig.d.ts +13 -13
- package/dist/runtime/types/dialogManager.d.ts +35 -35
- package/dist/runtime/types/formDialog.d.ts +5 -5
- package/dist/runtime/types/graphqlOperation.d.ts +23 -23
- package/dist/runtime/types/menu.d.ts +31 -31
- package/dist/runtime/types/modules.d.ts +7 -7
- package/dist/runtime/types/permission.d.ts +13 -13
- package/package.json +131 -131
- package/scripts/enrich-vue-docs-from-ai.mjs +197 -197
- package/scripts/generate-ai-summary.mjs +321 -321
- package/scripts/generate-composables-md.mjs +129 -129
- package/scripts/postInstall.cjs +70 -70
- package/templates/.codegen/codegen.ts +32 -32
- 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>
|