@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,246 +1,246 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
/**
|
|
3
|
-
* FormImagesCapture handles image capture, editing, preview, and value synchronization for schema-driven form image fields.
|
|
4
|
-
* This doc block is consumed by vue-docgen for generated API documentation.
|
|
5
|
-
*/
|
|
6
|
-
import {computed, defineExpose, onBeforeUnmount, onMounted, ref, useTemplateRef, watchEffect} from "vue"
|
|
7
|
-
import {useDevicesList, useUserMedia} from "@vueuse/core"
|
|
8
|
-
import {getBase64Strings, type ImageFormat} from 'exif-rotate-js'
|
|
9
|
-
import {useAlert} from '../../../composables/alert'
|
|
10
|
-
import {VInput} from 'vuetify/components/VInput'
|
|
11
|
-
|
|
12
|
-
interface Props {
|
|
13
|
-
imageFormat?: ImageFormat // Output image MIME/encoding format for capture or editing.
|
|
14
|
-
autoStart?: boolean // Starts camera stream automatically when dialog opens.
|
|
15
|
-
fileOnly?: boolean // Emits file object only, without base64 payload conversion.
|
|
16
|
-
readonly?: boolean // renders as read-only while keeping value visible
|
|
17
|
-
disabled?: boolean // disables user interaction for this field
|
|
18
|
-
buttonText?: string // Label text displayed on the primary action button.
|
|
19
|
-
maxHeight?: string | number // Maximum height constraint for previews, dialogs, or editors.
|
|
20
|
-
maxWidth?: string | number // Maximum width constraint for previews, dialogs, or editors.
|
|
21
|
-
aspectRatio?: number // Target aspect ratio used while cropping or capturing images.
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Public props accepted by FormImagesCapture.
|
|
26
|
-
* Document each prop field with intent, defaults, and side effects for clear generated docs.
|
|
27
|
-
*/
|
|
28
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
29
|
-
imageFormat: "image/jpeg",
|
|
30
|
-
autoStart: true,
|
|
31
|
-
fileOnly: false,
|
|
32
|
-
readonly: false,
|
|
33
|
-
disabled: false,
|
|
34
|
-
buttonText: "Add Photo",
|
|
35
|
-
maxWidth: 1024
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
const alert = useAlert()
|
|
39
|
-
|
|
40
|
-
const imageData = defineModel<string>()
|
|
41
|
-
const inputRef = useTemplateRef<VInput>("inputRef")
|
|
42
|
-
|
|
43
|
-
const isLoading = ref<boolean>(false)
|
|
44
|
-
const isCaptured = computed(()=>!!imageData.value)
|
|
45
|
-
const isEditing = ref<boolean>(false)
|
|
46
|
-
|
|
47
|
-
const videoScreen = ref<HTMLVideoElement>()
|
|
48
|
-
|
|
49
|
-
const currentCameraId = ref<ConstrainDOMString | undefined>()
|
|
50
|
-
const { videoInputs: cameras } = useDevicesList({
|
|
51
|
-
requestPermissions: true,
|
|
52
|
-
constraints: { audio: false, video: true },
|
|
53
|
-
onUpdated() {
|
|
54
|
-
if (!cameras.value.find(camera => camera.deviceId === currentCameraId.value))
|
|
55
|
-
currentCameraId.value = cameras.value[0]?.deviceId
|
|
56
|
-
},
|
|
57
|
-
})
|
|
58
|
-
const hasCamera = computed(()=>{ return !!currentCameraId.value })
|
|
59
|
-
|
|
60
|
-
const { stream, start: cameraStart, stop: cameraStop, enabled: cameraEnabled } = useUserMedia({
|
|
61
|
-
constraints: { video: { deviceId: currentCameraId.value, width: {min: 1280}, aspectRatio: props.aspectRatio}},
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
watchEffect(() => {
|
|
65
|
-
if (videoScreen.value) videoScreen.value.srcObject = (stream.value) ? stream.value! : null
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
function startCamera() {
|
|
69
|
-
imageData.value = undefined
|
|
70
|
-
if (!cameraEnabled.value) {
|
|
71
|
-
isLoading.value = true
|
|
72
|
-
cameraStart().finally(()=>{
|
|
73
|
-
isLoading.value = false
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function stopCamera() {
|
|
79
|
-
if (cameraEnabled.value) cameraStop()
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function captureImage() {
|
|
83
|
-
if (videoScreen.value) {
|
|
84
|
-
const canvas = document.createElement('canvas')
|
|
85
|
-
canvas.width = videoScreen.value.videoWidth
|
|
86
|
-
canvas.height = videoScreen.value.videoHeight
|
|
87
|
-
const context = canvas.getContext('2d')
|
|
88
|
-
if (context) {
|
|
89
|
-
context.drawImage(videoScreen.value, 0, 0, canvas.width, canvas.height)
|
|
90
|
-
isEditing.value = false
|
|
91
|
-
imageData.value = canvas.toDataURL(props.imageFormat)
|
|
92
|
-
stopCamera()
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function captureImageFile(selectedFile: File | File[] | undefined) {
|
|
98
|
-
if (!selectedFile) {
|
|
99
|
-
alert?.addAlert({ message: 'No file selected.', alertType: 'error' })
|
|
100
|
-
return
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const scanImageSingleFile: File = Array.isArray(selectedFile) ? selectedFile[0] : selectedFile
|
|
104
|
-
|
|
105
|
-
getBase64Strings([scanImageSingleFile], { maxSize: computedMaxSize.value,type: props.imageFormat,quality: 1 }).then((returnData) => {
|
|
106
|
-
isEditing.value = false
|
|
107
|
-
imageData.value = returnData[0]
|
|
108
|
-
stopCamera()
|
|
109
|
-
}).catch((e) => void e)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
onMounted(() => {
|
|
113
|
-
if (!isCaptured.value && props.autoStart && !props.fileOnly) startCamera()
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
onBeforeUnmount(() => {
|
|
117
|
-
stopCamera()
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
function reset() {
|
|
121
|
-
inputRef.value?.reset()
|
|
122
|
-
stopCamera()
|
|
123
|
-
if (props.autoStart && !props.fileOnly) startCamera()
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const computedMaxWidth = computed(() => {
|
|
127
|
-
if (typeof props.maxWidth === 'number') {
|
|
128
|
-
return `${props.maxWidth}px`
|
|
129
|
-
} else if (!isNaN(Number(props.maxWidth))) {
|
|
130
|
-
return `${props.maxWidth}px`
|
|
131
|
-
}
|
|
132
|
-
return props.maxWidth
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
const computedMaxHeight = computed(() => {
|
|
136
|
-
if (typeof props.maxHeight === 'number') {
|
|
137
|
-
return `${props.maxHeight}px`
|
|
138
|
-
} else if (!isNaN(Number(props.maxHeight))) {
|
|
139
|
-
return `${props.maxHeight}px`
|
|
140
|
-
}
|
|
141
|
-
return props.maxHeight
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
const computedMaxSize = computed(() => {
|
|
145
|
-
let tmpMaxHeight : number = 1024
|
|
146
|
-
let tmpMaxWidth : number = 1024
|
|
147
|
-
|
|
148
|
-
if (typeof props.maxWidth === 'number') {
|
|
149
|
-
tmpMaxWidth = <number>props.maxWidth
|
|
150
|
-
} else if (!isNaN(Number(props.maxWidth))) {
|
|
151
|
-
tmpMaxWidth = Number(props.maxWidth)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (typeof props.maxHeight === 'number') {
|
|
155
|
-
tmpMaxHeight = <number>props.maxHeight
|
|
156
|
-
} else if (!isNaN(Number(props.maxHeight))) {
|
|
157
|
-
tmpMaxHeight = Number(props.maxHeight)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return (tmpMaxWidth>tmpMaxHeight) ? tmpMaxWidth : tmpMaxHeight
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
const operation = ref({startCamera,stopCamera,reset,captureImage,captureImageFile,isLoading,isCaptured,hasCamera})
|
|
164
|
-
|
|
165
|
-
const isValid = computed(()=>{
|
|
166
|
-
return inputRef.value?.isValid
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
const errorMessages = computed(()=>{
|
|
170
|
-
return inputRef.value?.errorMessages
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
defineExpose({
|
|
174
|
-
errorMessages,
|
|
175
|
-
isValid,
|
|
176
|
-
reset,
|
|
177
|
-
resetValidation : ()=>inputRef.value?.resetValidation(),
|
|
178
|
-
validate : ()=>inputRef.value?.validate(),
|
|
179
|
-
operation
|
|
180
|
-
})
|
|
181
|
-
</script>
|
|
182
|
-
|
|
183
|
-
<template>
|
|
184
|
-
<v-input v-model="imageData" v-bind="$attrs" ref="inputRef">
|
|
185
|
-
<template #default="{isReadonly,isDisabled}">
|
|
186
|
-
<v-container fluid class="ma-0 pa-0">
|
|
187
|
-
<v-card>
|
|
188
|
-
<v-card-text class="d-flex justify-center text-center" v-if="!isLoading && !isCaptured">
|
|
189
|
-
<template v-if="!hasCamera || fileOnly">
|
|
190
|
-
<FileBtn
|
|
191
|
-
color="primary"
|
|
192
|
-
variant="flat"
|
|
193
|
-
accept="image/*"
|
|
194
|
-
@update:model-value="captureImageFile"
|
|
195
|
-
:disabled="disabled || readonly || (isReadonly.value || isDisabled.value)"
|
|
196
|
-
>
|
|
197
|
-
<v-icon>mdi mdi-image-plus</v-icon>
|
|
198
|
-
{{ buttonText }}
|
|
199
|
-
</FileBtn>
|
|
200
|
-
</template>
|
|
201
|
-
<template v-else>
|
|
202
|
-
<div style="position: relative; display: inline-block; width: 100%;" :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}">
|
|
203
|
-
<video autoplay ref="videoScreen" width="100%" :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}"></video>
|
|
204
|
-
<div style="position: absolute; bottom: 10px; right: 10px; z-index: 2000;">
|
|
205
|
-
<FileBtn
|
|
206
|
-
accept="image/*"
|
|
207
|
-
icon="mdi mdi-image-plus"
|
|
208
|
-
icon-only
|
|
209
|
-
@update:model-value="captureImageFile"
|
|
210
|
-
:disabled="disabled || readonly || (isReadonly.value || isDisabled.value)"
|
|
211
|
-
/>
|
|
212
|
-
</div>
|
|
213
|
-
</div>
|
|
214
|
-
</template>
|
|
215
|
-
</v-card-text>
|
|
216
|
-
<v-card-text class="d-flex justify-center" v-if="isCaptured">
|
|
217
|
-
<div style="position: relative; display: inline-block; width: 100%;" :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}" v-if="!isEditing">
|
|
218
|
-
<v-img :src="imageData" :max-height="maxHeight" :max-width="maxWidth" contain></v-img>
|
|
219
|
-
<div style="position: absolute; bottom: 10px; right: 10px; z-index: 2000;">
|
|
220
|
-
<v-btn
|
|
221
|
-
icon="mdi mdi-image-edit"
|
|
222
|
-
icon-only
|
|
223
|
-
@click="isEditing=true"
|
|
224
|
-
:disabled="disabled || readonly || (isReadonly.value || isDisabled.value)"
|
|
225
|
-
/>
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
<form-images-edit v-model="imageData" :aspect-ratio="aspectRatio" :image-format="imageFormat" @update:model-value="isEditing=false" @close="isEditing=false" v-else></form-images-edit>
|
|
229
|
-
</v-card-text>
|
|
230
|
-
<v-card-text class="d-flex justify-center" v-if="isLoading">
|
|
231
|
-
<v-progress-circular indeterminate></v-progress-circular>
|
|
232
|
-
</v-card-text>
|
|
233
|
-
<v-card-actions v-if="!readonly && (!fileOnly || isCaptured) && !isEditing && !(isReadonly.value || isDisabled.value)">
|
|
234
|
-
<slot name="actions" :operation="operation">
|
|
235
|
-
<v-spacer></v-spacer>
|
|
236
|
-
<v-btn color="primary" variant="flat" @click="startCamera" v-if="!cameraEnabled && hasCamera && !fileOnly" :disabled="disabled">{{ (isCaptured) ? "Retake" : "Start" }}</v-btn>
|
|
237
|
-
<v-btn color="primary" variant="flat" @click="captureImage" v-if="cameraEnabled" :disabled="disabled">Capture</v-btn>
|
|
238
|
-
<v-btn color="primary" variant="flat" @click="reset" :disabled="disabled">Reset</v-btn>
|
|
239
|
-
<v-spacer></v-spacer>
|
|
240
|
-
</slot>
|
|
241
|
-
</v-card-actions>
|
|
242
|
-
</v-card>
|
|
243
|
-
</v-container>
|
|
244
|
-
</template>
|
|
245
|
-
</v-input>
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
/**
|
|
3
|
+
* FormImagesCapture handles image capture, editing, preview, and value synchronization for schema-driven form image fields.
|
|
4
|
+
* This doc block is consumed by vue-docgen for generated API documentation.
|
|
5
|
+
*/
|
|
6
|
+
import {computed, defineExpose, onBeforeUnmount, onMounted, ref, useTemplateRef, watchEffect} from "vue"
|
|
7
|
+
import {useDevicesList, useUserMedia} from "@vueuse/core"
|
|
8
|
+
import {getBase64Strings, type ImageFormat} from 'exif-rotate-js'
|
|
9
|
+
import {useAlert} from '../../../composables/alert'
|
|
10
|
+
import {VInput} from 'vuetify/components/VInput'
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
imageFormat?: ImageFormat // Output image MIME/encoding format for capture or editing.
|
|
14
|
+
autoStart?: boolean // Starts camera stream automatically when dialog opens.
|
|
15
|
+
fileOnly?: boolean // Emits file object only, without base64 payload conversion.
|
|
16
|
+
readonly?: boolean // renders as read-only while keeping value visible
|
|
17
|
+
disabled?: boolean // disables user interaction for this field
|
|
18
|
+
buttonText?: string // Label text displayed on the primary action button.
|
|
19
|
+
maxHeight?: string | number // Maximum height constraint for previews, dialogs, or editors.
|
|
20
|
+
maxWidth?: string | number // Maximum width constraint for previews, dialogs, or editors.
|
|
21
|
+
aspectRatio?: number // Target aspect ratio used while cropping or capturing images.
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Public props accepted by FormImagesCapture.
|
|
26
|
+
* Document each prop field with intent, defaults, and side effects for clear generated docs.
|
|
27
|
+
*/
|
|
28
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
29
|
+
imageFormat: "image/jpeg",
|
|
30
|
+
autoStart: true,
|
|
31
|
+
fileOnly: false,
|
|
32
|
+
readonly: false,
|
|
33
|
+
disabled: false,
|
|
34
|
+
buttonText: "Add Photo",
|
|
35
|
+
maxWidth: 1024
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const alert = useAlert()
|
|
39
|
+
|
|
40
|
+
const imageData = defineModel<string>()
|
|
41
|
+
const inputRef = useTemplateRef<VInput>("inputRef")
|
|
42
|
+
|
|
43
|
+
const isLoading = ref<boolean>(false)
|
|
44
|
+
const isCaptured = computed(()=>!!imageData.value)
|
|
45
|
+
const isEditing = ref<boolean>(false)
|
|
46
|
+
|
|
47
|
+
const videoScreen = ref<HTMLVideoElement>()
|
|
48
|
+
|
|
49
|
+
const currentCameraId = ref<ConstrainDOMString | undefined>()
|
|
50
|
+
const { videoInputs: cameras } = useDevicesList({
|
|
51
|
+
requestPermissions: true,
|
|
52
|
+
constraints: { audio: false, video: true },
|
|
53
|
+
onUpdated() {
|
|
54
|
+
if (!cameras.value.find(camera => camera.deviceId === currentCameraId.value))
|
|
55
|
+
currentCameraId.value = cameras.value[0]?.deviceId
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
const hasCamera = computed(()=>{ return !!currentCameraId.value })
|
|
59
|
+
|
|
60
|
+
const { stream, start: cameraStart, stop: cameraStop, enabled: cameraEnabled } = useUserMedia({
|
|
61
|
+
constraints: { video: { deviceId: currentCameraId.value, width: {min: 1280}, aspectRatio: props.aspectRatio}},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
watchEffect(() => {
|
|
65
|
+
if (videoScreen.value) videoScreen.value.srcObject = (stream.value) ? stream.value! : null
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
function startCamera() {
|
|
69
|
+
imageData.value = undefined
|
|
70
|
+
if (!cameraEnabled.value) {
|
|
71
|
+
isLoading.value = true
|
|
72
|
+
cameraStart().finally(()=>{
|
|
73
|
+
isLoading.value = false
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function stopCamera() {
|
|
79
|
+
if (cameraEnabled.value) cameraStop()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function captureImage() {
|
|
83
|
+
if (videoScreen.value) {
|
|
84
|
+
const canvas = document.createElement('canvas')
|
|
85
|
+
canvas.width = videoScreen.value.videoWidth
|
|
86
|
+
canvas.height = videoScreen.value.videoHeight
|
|
87
|
+
const context = canvas.getContext('2d')
|
|
88
|
+
if (context) {
|
|
89
|
+
context.drawImage(videoScreen.value, 0, 0, canvas.width, canvas.height)
|
|
90
|
+
isEditing.value = false
|
|
91
|
+
imageData.value = canvas.toDataURL(props.imageFormat)
|
|
92
|
+
stopCamera()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function captureImageFile(selectedFile: File | File[] | undefined) {
|
|
98
|
+
if (!selectedFile) {
|
|
99
|
+
alert?.addAlert({ message: 'No file selected.', alertType: 'error' })
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const scanImageSingleFile: File = Array.isArray(selectedFile) ? selectedFile[0] : selectedFile
|
|
104
|
+
|
|
105
|
+
getBase64Strings([scanImageSingleFile], { maxSize: computedMaxSize.value,type: props.imageFormat,quality: 1 }).then((returnData) => {
|
|
106
|
+
isEditing.value = false
|
|
107
|
+
imageData.value = returnData[0]
|
|
108
|
+
stopCamera()
|
|
109
|
+
}).catch((e) => void e)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onMounted(() => {
|
|
113
|
+
if (!isCaptured.value && props.autoStart && !props.fileOnly) startCamera()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
onBeforeUnmount(() => {
|
|
117
|
+
stopCamera()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
function reset() {
|
|
121
|
+
inputRef.value?.reset()
|
|
122
|
+
stopCamera()
|
|
123
|
+
if (props.autoStart && !props.fileOnly) startCamera()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const computedMaxWidth = computed(() => {
|
|
127
|
+
if (typeof props.maxWidth === 'number') {
|
|
128
|
+
return `${props.maxWidth}px`
|
|
129
|
+
} else if (!isNaN(Number(props.maxWidth))) {
|
|
130
|
+
return `${props.maxWidth}px`
|
|
131
|
+
}
|
|
132
|
+
return props.maxWidth
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const computedMaxHeight = computed(() => {
|
|
136
|
+
if (typeof props.maxHeight === 'number') {
|
|
137
|
+
return `${props.maxHeight}px`
|
|
138
|
+
} else if (!isNaN(Number(props.maxHeight))) {
|
|
139
|
+
return `${props.maxHeight}px`
|
|
140
|
+
}
|
|
141
|
+
return props.maxHeight
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const computedMaxSize = computed(() => {
|
|
145
|
+
let tmpMaxHeight : number = 1024
|
|
146
|
+
let tmpMaxWidth : number = 1024
|
|
147
|
+
|
|
148
|
+
if (typeof props.maxWidth === 'number') {
|
|
149
|
+
tmpMaxWidth = <number>props.maxWidth
|
|
150
|
+
} else if (!isNaN(Number(props.maxWidth))) {
|
|
151
|
+
tmpMaxWidth = Number(props.maxWidth)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (typeof props.maxHeight === 'number') {
|
|
155
|
+
tmpMaxHeight = <number>props.maxHeight
|
|
156
|
+
} else if (!isNaN(Number(props.maxHeight))) {
|
|
157
|
+
tmpMaxHeight = Number(props.maxHeight)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (tmpMaxWidth>tmpMaxHeight) ? tmpMaxWidth : tmpMaxHeight
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const operation = ref({startCamera,stopCamera,reset,captureImage,captureImageFile,isLoading,isCaptured,hasCamera})
|
|
164
|
+
|
|
165
|
+
const isValid = computed(()=>{
|
|
166
|
+
return inputRef.value?.isValid
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const errorMessages = computed(()=>{
|
|
170
|
+
return inputRef.value?.errorMessages
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
defineExpose({
|
|
174
|
+
errorMessages,
|
|
175
|
+
isValid,
|
|
176
|
+
reset,
|
|
177
|
+
resetValidation : ()=>inputRef.value?.resetValidation(),
|
|
178
|
+
validate : ()=>inputRef.value?.validate(),
|
|
179
|
+
operation
|
|
180
|
+
})
|
|
181
|
+
</script>
|
|
182
|
+
|
|
183
|
+
<template>
|
|
184
|
+
<v-input v-model="imageData" v-bind="$attrs" ref="inputRef">
|
|
185
|
+
<template #default="{isReadonly,isDisabled}">
|
|
186
|
+
<v-container fluid class="ma-0 pa-0">
|
|
187
|
+
<v-card>
|
|
188
|
+
<v-card-text class="d-flex justify-center text-center" v-if="!isLoading && !isCaptured">
|
|
189
|
+
<template v-if="!hasCamera || fileOnly">
|
|
190
|
+
<FileBtn
|
|
191
|
+
color="primary"
|
|
192
|
+
variant="flat"
|
|
193
|
+
accept="image/*"
|
|
194
|
+
@update:model-value="captureImageFile"
|
|
195
|
+
:disabled="disabled || readonly || (isReadonly.value || isDisabled.value)"
|
|
196
|
+
>
|
|
197
|
+
<v-icon>mdi mdi-image-plus</v-icon>
|
|
198
|
+
{{ buttonText }}
|
|
199
|
+
</FileBtn>
|
|
200
|
+
</template>
|
|
201
|
+
<template v-else>
|
|
202
|
+
<div style="position: relative; display: inline-block; width: 100%;" :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}">
|
|
203
|
+
<video autoplay ref="videoScreen" width="100%" :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}"></video>
|
|
204
|
+
<div style="position: absolute; bottom: 10px; right: 10px; z-index: 2000;">
|
|
205
|
+
<FileBtn
|
|
206
|
+
accept="image/*"
|
|
207
|
+
icon="mdi mdi-image-plus"
|
|
208
|
+
icon-only
|
|
209
|
+
@update:model-value="captureImageFile"
|
|
210
|
+
:disabled="disabled || readonly || (isReadonly.value || isDisabled.value)"
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</template>
|
|
215
|
+
</v-card-text>
|
|
216
|
+
<v-card-text class="d-flex justify-center" v-if="isCaptured">
|
|
217
|
+
<div style="position: relative; display: inline-block; width: 100%;" :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}" v-if="!isEditing">
|
|
218
|
+
<v-img :src="imageData" :max-height="maxHeight" :max-width="maxWidth" contain></v-img>
|
|
219
|
+
<div style="position: absolute; bottom: 10px; right: 10px; z-index: 2000;">
|
|
220
|
+
<v-btn
|
|
221
|
+
icon="mdi mdi-image-edit"
|
|
222
|
+
icon-only
|
|
223
|
+
@click="isEditing=true"
|
|
224
|
+
:disabled="disabled || readonly || (isReadonly.value || isDisabled.value)"
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
<form-images-edit v-model="imageData" :aspect-ratio="aspectRatio" :image-format="imageFormat" @update:model-value="isEditing=false" @close="isEditing=false" v-else></form-images-edit>
|
|
229
|
+
</v-card-text>
|
|
230
|
+
<v-card-text class="d-flex justify-center" v-if="isLoading">
|
|
231
|
+
<v-progress-circular indeterminate></v-progress-circular>
|
|
232
|
+
</v-card-text>
|
|
233
|
+
<v-card-actions v-if="!readonly && (!fileOnly || isCaptured) && !isEditing && !(isReadonly.value || isDisabled.value)">
|
|
234
|
+
<slot name="actions" :operation="operation">
|
|
235
|
+
<v-spacer></v-spacer>
|
|
236
|
+
<v-btn color="primary" variant="flat" @click="startCamera" v-if="!cameraEnabled && hasCamera && !fileOnly" :disabled="disabled">{{ (isCaptured) ? "Retake" : "Start" }}</v-btn>
|
|
237
|
+
<v-btn color="primary" variant="flat" @click="captureImage" v-if="cameraEnabled" :disabled="disabled">Capture</v-btn>
|
|
238
|
+
<v-btn color="primary" variant="flat" @click="reset" :disabled="disabled">Reset</v-btn>
|
|
239
|
+
<v-spacer></v-spacer>
|
|
240
|
+
</slot>
|
|
241
|
+
</v-card-actions>
|
|
242
|
+
</v-card>
|
|
243
|
+
</v-container>
|
|
244
|
+
</template>
|
|
245
|
+
</v-input>
|
|
246
246
|
</template>
|