@ramathibodi/nuxt-commons 0.1.13 → 0.1.14
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/dist/module.json +1 -1
- package/dist/runtime/components/ExportCSV.vue +4 -4
- package/dist/runtime/components/FileBtn.vue +3 -3
- package/dist/runtime/components/ImportCSV.vue +1 -1
- package/dist/runtime/components/form/Birthdate.vue +21 -4
- package/dist/runtime/components/form/Table.vue +1 -1
- package/dist/runtime/components/form/images/Capture.vue +231 -0
- package/dist/runtime/components/form/images/Edit.vue +95 -121
- package/dist/runtime/components/label/Field.vue +21 -8
- package/dist/runtime/components/master/Autocomplete.vue +7 -7
- package/dist/runtime/components/master/Combobox.vue +1 -1
- package/dist/runtime/components/master/RadioGroup.vue +1 -1
- package/dist/runtime/components/master/Select.vue +1 -1
- package/dist/runtime/components/model/Table.vue +2 -2
- package/dist/runtime/components/model/iterator.vue +3 -3
- package/dist/runtime/composables/graphqlModel.d.ts +4 -1
- package/dist/runtime/composables/graphqlModel.mjs +4 -4
- package/dist/runtime/composables/graphqlModelOperation.mjs +7 -4
- package/dist/runtime/labs/form/TextFieldMask.vue +4 -4
- package/package.json +5 -4
- package/dist/runtime/components/Camera.vue +0 -129
- package/dist/runtime/components/form/images/CameraCrop.vue +0 -58
- package/dist/runtime/components/form/images/Preview.vue +0 -48
package/dist/module.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import {
|
|
2
|
+
import {ref, withDefaults} from 'vue'
|
|
3
3
|
import * as XLSX from 'xlsx'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import {VBtn} from 'vuetify/components/VBtn'
|
|
5
|
+
import {useAlert} from '../composables/alert'
|
|
6
6
|
|
|
7
7
|
interface ExportButtonProps extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
|
|
8
8
|
fileName?: string
|
|
@@ -48,7 +48,7 @@ function exportFile() {
|
|
|
48
48
|
>
|
|
49
49
|
<slot
|
|
50
50
|
:name="name"
|
|
51
|
-
v-bind="(slotData as object)"
|
|
51
|
+
v-bind="((slotData || {}) as object)"
|
|
52
52
|
/>
|
|
53
53
|
</template>
|
|
54
54
|
</VBtn>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import {ref, watch} from 'vue'
|
|
3
|
+
import {VBtn} from 'vuetify/components/VBtn'
|
|
4
4
|
|
|
5
5
|
interface Props extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
|
|
6
6
|
accept?: string
|
|
@@ -48,7 +48,7 @@ defineExpose({ reset })
|
|
|
48
48
|
>
|
|
49
49
|
<slot
|
|
50
50
|
:name="name"
|
|
51
|
-
v-bind="(slotData as object)"
|
|
51
|
+
v-bind="((slotData || {}) as object)"
|
|
52
52
|
/>
|
|
53
53
|
</template>
|
|
54
54
|
</v-btn>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import Datepicker from '@vuepic/vue-datepicker'
|
|
3
3
|
import '@vuepic/vue-datepicker/dist/main.css'
|
|
4
|
-
import {nextTick, ref, watch, watchEffect} from 'vue'
|
|
4
|
+
import {nextTick, ref, watch, watchEffect,computed} from 'vue'
|
|
5
5
|
import {type dateFormat, Datetime} from '../../utils/datetime'
|
|
6
6
|
|
|
7
7
|
interface IDobPrecision {
|
|
@@ -101,6 +101,12 @@ function updateDate(dateString: string | null) {
|
|
|
101
101
|
selectedDate.value = null
|
|
102
102
|
}
|
|
103
103
|
else {
|
|
104
|
+
if (dobPrecisionSelected.value=="yearMonth") {
|
|
105
|
+
dateTime.luxonDateTime = dateTime.luxonDateTime.startOf('month')
|
|
106
|
+
}
|
|
107
|
+
if (dobPrecisionSelected.value=="year") {
|
|
108
|
+
dateTime.luxonDateTime = dateTime.luxonDateTime.startOf('year')
|
|
109
|
+
}
|
|
104
110
|
selectedDate.value = dateTime.toFormat('yyyy-MM-dd', 'EN')
|
|
105
111
|
displayedDate.value = selectedDate.value
|
|
106
112
|
}
|
|
@@ -111,8 +117,16 @@ function updateDate(dateString: string | null) {
|
|
|
111
117
|
function formatDate(dateString: string | null) {
|
|
112
118
|
if (!dateString) return null
|
|
113
119
|
|
|
120
|
+
let displayFormat = props.format
|
|
121
|
+
if (dobPrecisionSelected.value=="yearMonth") {
|
|
122
|
+
displayFormat = "MMM yyyy"
|
|
123
|
+
}
|
|
124
|
+
if (dobPrecisionSelected.value=="year") {
|
|
125
|
+
displayFormat = "yyyy"
|
|
126
|
+
}
|
|
127
|
+
|
|
114
128
|
const dateTime = Datetime().fromString(dateString, undefined, props.locale)
|
|
115
|
-
return dateTime.toFormat(
|
|
129
|
+
return dateTime.toFormat(displayFormat, props.locale)
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
function handleTextFieldClear() {
|
|
@@ -126,8 +140,7 @@ function resetDatePicker() {
|
|
|
126
140
|
|
|
127
141
|
watchEffect(() => {
|
|
128
142
|
if (!isTextFieldFocused.value && selectedDate.value) {
|
|
129
|
-
|
|
130
|
-
displayedDate.value = dateTime.toFormat(props.format, props.locale)
|
|
143
|
+
displayedDate.value = formatDate(selectedDate.value)
|
|
131
144
|
}
|
|
132
145
|
else {
|
|
133
146
|
displayedDate.value = selectedDate.value
|
|
@@ -142,6 +155,10 @@ watch(() => props.modelValue, () => {
|
|
|
142
155
|
updateDate(props.modelValue || null)
|
|
143
156
|
}, { immediate: true })
|
|
144
157
|
|
|
158
|
+
watch(dobPrecisionSelected,()=>{
|
|
159
|
+
updateDate(selectedDate.value)
|
|
160
|
+
})
|
|
161
|
+
|
|
145
162
|
function toggleMenuOpen(trigger: string) {
|
|
146
163
|
if ((trigger === 'textField' && props.pickerOnly) || (trigger === 'icon' && !props.pickerOnly)) {
|
|
147
164
|
isMenuOpen.value = true
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import {computed, onBeforeUnmount, onMounted, ref, watchEffect} from "vue"
|
|
3
|
+
import {useDevicesList, useUserMedia} from "@vueuse/core"
|
|
4
|
+
import {getBase64Strings, type ImageFormat} from 'exif-rotate-js'
|
|
5
|
+
import {useAlert} from '../../../composables/alert'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
imageFormat?: ImageFormat
|
|
9
|
+
autoStart?: boolean
|
|
10
|
+
fileOnly?: boolean
|
|
11
|
+
required?: boolean
|
|
12
|
+
readonly?: boolean
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
requiredMessage?: string
|
|
15
|
+
buttonText?: string
|
|
16
|
+
maxHeight?: string | number
|
|
17
|
+
maxWidth?: string | number
|
|
18
|
+
aspectRatio?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
22
|
+
imageFormat: "image/jpeg",
|
|
23
|
+
autoStart: true,
|
|
24
|
+
fileOnly: false,
|
|
25
|
+
required: false,
|
|
26
|
+
readonly: false,
|
|
27
|
+
disabled: false,
|
|
28
|
+
requiredMessage: "This field is required",
|
|
29
|
+
buttonText: "Add Photo",
|
|
30
|
+
maxWidth: 1024
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const alert = useAlert()
|
|
34
|
+
|
|
35
|
+
const imageData = defineModel<string>()
|
|
36
|
+
|
|
37
|
+
const isLoading = ref<boolean>(false)
|
|
38
|
+
const isCaptured = computed(()=>!!imageData.value)
|
|
39
|
+
const isEditing = ref<boolean>(false)
|
|
40
|
+
const showRequiredMessage = ref<boolean>(false)
|
|
41
|
+
|
|
42
|
+
const videoScreen = ref<HTMLVideoElement>()
|
|
43
|
+
|
|
44
|
+
const currentCameraId = ref<ConstrainDOMString | undefined>()
|
|
45
|
+
const { videoInputs: cameras } = useDevicesList({
|
|
46
|
+
requestPermissions: true,
|
|
47
|
+
constraints: { audio: false, video: true },
|
|
48
|
+
onUpdated() {
|
|
49
|
+
if (!cameras.value.find(camera => camera.deviceId === currentCameraId.value))
|
|
50
|
+
currentCameraId.value = cameras.value[0]?.deviceId
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
const hasCamera = computed(()=>{ return !!currentCameraId.value })
|
|
54
|
+
|
|
55
|
+
const { stream, start: cameraStart, stop: cameraStop, enabled: cameraEnabled } = useUserMedia({
|
|
56
|
+
constraints: { video: { deviceId: currentCameraId.value, width: {min: 1280}, aspectRatio: props.aspectRatio}},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
watchEffect(() => {
|
|
60
|
+
if (videoScreen.value) videoScreen.value.srcObject = (stream.value) ? stream.value! : null
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
function startCamera() {
|
|
64
|
+
imageData.value = undefined
|
|
65
|
+
if (!cameraEnabled.value && hasCamera) {
|
|
66
|
+
isLoading.value = true
|
|
67
|
+
cameraStart().finally(()=>{
|
|
68
|
+
isLoading.value = false
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stopCamera() {
|
|
74
|
+
if (cameraEnabled.value) cameraStop()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function captureImage() {
|
|
78
|
+
if (videoScreen.value) {
|
|
79
|
+
const canvas = document.createElement('canvas')
|
|
80
|
+
canvas.width = videoScreen.value.videoWidth
|
|
81
|
+
canvas.height = videoScreen.value.videoHeight
|
|
82
|
+
const context = canvas.getContext('2d')
|
|
83
|
+
if (context) {
|
|
84
|
+
context.drawImage(videoScreen.value, 0, 0, canvas.width, canvas.height)
|
|
85
|
+
isEditing.value = false
|
|
86
|
+
imageData.value = canvas.toDataURL(props.imageFormat)
|
|
87
|
+
stopCamera()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function captureImageFile(selectedFile: File | File[] | undefined) {
|
|
93
|
+
if (!selectedFile) {
|
|
94
|
+
alert?.addAlert({ message: 'No file selected.', alertType: 'error' })
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const scanImageSingleFile: File = Array.isArray(selectedFile) ? selectedFile[0] : selectedFile
|
|
99
|
+
|
|
100
|
+
getBase64Strings([scanImageSingleFile], { maxSize: computedMaxSize.value,type: props.imageFormat,quality: 1 }).then((returnData) => {
|
|
101
|
+
isEditing.value = false
|
|
102
|
+
imageData.value = returnData[0]
|
|
103
|
+
stopCamera()
|
|
104
|
+
}).catch((e) => void e)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
onMounted(() => {
|
|
108
|
+
if (!isCaptured.value && props.autoStart && !props.fileOnly) startCamera()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
onBeforeUnmount(() => {
|
|
112
|
+
stopCamera()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
function validate() {
|
|
116
|
+
if (!props.required || isCaptured.value) {
|
|
117
|
+
showRequiredMessage.value = false
|
|
118
|
+
return true
|
|
119
|
+
} else {
|
|
120
|
+
showRequiredMessage.value = true
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function reset() {
|
|
126
|
+
imageData.value = undefined
|
|
127
|
+
stopCamera()
|
|
128
|
+
if (props.autoStart && !props.fileOnly) startCamera()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const computedMaxWidth = computed(() => {
|
|
132
|
+
if (typeof props.maxWidth === 'number') {
|
|
133
|
+
return `${props.maxWidth}px`
|
|
134
|
+
} else if (!isNaN(Number(props.maxWidth))) {
|
|
135
|
+
return `${props.maxWidth}px`
|
|
136
|
+
}
|
|
137
|
+
return props.maxWidth
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const computedMaxHeight = computed(() => {
|
|
141
|
+
if (typeof props.maxHeight === 'number') {
|
|
142
|
+
return `${props.maxHeight}px`
|
|
143
|
+
} else if (!isNaN(Number(props.maxHeight))) {
|
|
144
|
+
return `${props.maxHeight}px`
|
|
145
|
+
}
|
|
146
|
+
return props.maxHeight
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const computedMaxSize = computed(() => {
|
|
150
|
+
let tmpMaxHeight : number = 1024
|
|
151
|
+
let tmpMaxWidth : number = 1024
|
|
152
|
+
|
|
153
|
+
if (typeof props.maxWidth === 'number') {
|
|
154
|
+
tmpMaxWidth = <number>props.maxWidth
|
|
155
|
+
} else if (!isNaN(Number(props.maxWidth))) {
|
|
156
|
+
tmpMaxWidth = Number(props.maxWidth)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (typeof props.maxHeight === 'number') {
|
|
160
|
+
tmpMaxHeight = <number>props.maxHeight
|
|
161
|
+
} else if (!isNaN(Number(props.maxHeight))) {
|
|
162
|
+
tmpMaxHeight = Number(props.maxHeight)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (tmpMaxWidth>tmpMaxHeight) ? tmpMaxWidth : tmpMaxHeight
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const operation = ref({startCamera,stopCamera,reset,captureImage,captureImageFile,isLoading,isCaptured,hasCamera})
|
|
169
|
+
</script>
|
|
170
|
+
|
|
171
|
+
<template>
|
|
172
|
+
<v-card>
|
|
173
|
+
<v-card-text class="d-flex justify-center text-center" v-if="!isLoading && !isCaptured">
|
|
174
|
+
<template v-if="!hasCamera || fileOnly">
|
|
175
|
+
<FileBtn
|
|
176
|
+
color="primary"
|
|
177
|
+
variant="flat"
|
|
178
|
+
accept="image/*"
|
|
179
|
+
@update:model-value="captureImageFile"
|
|
180
|
+
:disabled="disabled || readonly"
|
|
181
|
+
>
|
|
182
|
+
<v-icon>mdi mdi-image-plus</v-icon>
|
|
183
|
+
{{ buttonText }}
|
|
184
|
+
</FileBtn>
|
|
185
|
+
</template>
|
|
186
|
+
<template v-else>
|
|
187
|
+
<div style="position: relative; display: inline-block; width: 100%;" :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}">
|
|
188
|
+
<video autoplay ref="videoScreen" width="100%" :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}"></video>
|
|
189
|
+
<div style="position: absolute; bottom: 10px; right: 10px; z-index: 2000;">
|
|
190
|
+
<FileBtn
|
|
191
|
+
accept="image/*"
|
|
192
|
+
icon="mdi mdi-image-plus"
|
|
193
|
+
icon-only
|
|
194
|
+
@update:model-value="captureImageFile"
|
|
195
|
+
:disabled="disabled || readonly"
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</template>
|
|
200
|
+
</v-card-text>
|
|
201
|
+
<v-card-text class="d-flex justify-center" v-if="isCaptured">
|
|
202
|
+
<div style="position: relative; display: inline-block; width: 100%;" :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}" v-if="!isEditing">
|
|
203
|
+
<v-img :src="imageData" :max-height="maxHeight" :max-width="maxWidth" contain></v-img>
|
|
204
|
+
<div style="position: absolute; bottom: 10px; right: 10px; z-index: 2000;">
|
|
205
|
+
<v-btn
|
|
206
|
+
icon="mdi mdi-image-edit"
|
|
207
|
+
icon-only
|
|
208
|
+
@click="isEditing=true"
|
|
209
|
+
:disabled="disabled || readonly"
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
<form-images-edit v-model="imageData" :aspect-ratio="aspectRatio" :image-format="imageFormat" @update:model-value="isEditing=false" v-else></form-images-edit>
|
|
214
|
+
</v-card-text>
|
|
215
|
+
<v-card-text class="d-flex justify-center" v-if="isLoading">
|
|
216
|
+
<v-progress-circular indeterminate></v-progress-circular>
|
|
217
|
+
</v-card-text>
|
|
218
|
+
<v-card-text v-if="showRequiredMessage" class="text-center">
|
|
219
|
+
<span class="red--text">{{ requiredMessage }}</span>
|
|
220
|
+
</v-card-text>
|
|
221
|
+
<v-card-actions v-if="!readonly && (!fileOnly || isCaptured) && !isEditing">
|
|
222
|
+
<slot name="actions" :operation="operation">
|
|
223
|
+
<v-spacer></v-spacer>
|
|
224
|
+
<v-btn color="primary" variant="flat" @click="startCamera" v-if="!cameraEnabled && hasCamera && !fileOnly" :disabled="disabled">{{ (isCaptured) ? "Retake" : "Start" }}</v-btn>
|
|
225
|
+
<v-btn color="primary" variant="flat" @click="captureImage" v-if="cameraEnabled" :disabled="disabled">Capture</v-btn>
|
|
226
|
+
<v-btn color="primary" variant="flat" @click="reset" :disabled="disabled">Reset</v-btn>
|
|
227
|
+
<v-spacer></v-spacer>
|
|
228
|
+
</slot>
|
|
229
|
+
</v-card-actions>
|
|
230
|
+
</v-card>
|
|
231
|
+
</template>
|
|
@@ -1,143 +1,117 @@
|
|
|
1
|
-
<script
|
|
2
|
-
import {
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import {ref,computed} from 'vue'
|
|
3
3
|
import Cropper from 'cropperjs'
|
|
4
4
|
import 'cropperjs/dist/cropper.css'
|
|
5
|
+
import type {ImageFormat} from "exif-rotate-js"
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
imageFormat?: ImageFormat
|
|
9
|
+
maxHeight?: string | number
|
|
10
|
+
maxWidth?: string | number
|
|
11
|
+
aspectRatio?: number
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
const props = withDefaults(defineProps<Props>(), {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
imageFormat: "image/jpeg",
|
|
16
|
+
maxWidth: 1024
|
|
15
17
|
})
|
|
16
18
|
|
|
17
|
-
const
|
|
19
|
+
const imageData = defineModel<string>()
|
|
18
20
|
|
|
19
|
-
const cropper = ref<Cropper
|
|
20
|
-
const
|
|
21
|
+
const cropper = ref<Cropper>()
|
|
22
|
+
const imageRef = ref<HTMLImageElement>()
|
|
23
|
+
const dragMode = ref<string>("crop")
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
image.onload = () => {
|
|
29
|
-
canvas.width = image.width
|
|
30
|
-
canvas.height = image.height
|
|
31
|
-
context?.clearRect(0, 0, canvas.width, canvas.height)
|
|
32
|
-
context?.drawImage(image, 0, 0)
|
|
33
|
-
if (!props.readonly) {
|
|
34
|
-
cropper.value?.destroy()
|
|
35
|
-
cropper.value = new Cropper(canvas, {
|
|
36
|
-
aspectRatio: 1,
|
|
37
|
-
rotatable: true,
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
}
|
|
25
|
+
function loadCropper() {
|
|
26
|
+
if (cropper.value) cropper.value?.destroy()
|
|
27
|
+
if (imageRef.value) {
|
|
28
|
+
cropper.value = new Cropper(<HTMLImageElement>imageRef.value,{
|
|
29
|
+
aspectRatio: props.aspectRatio
|
|
30
|
+
})
|
|
41
31
|
}
|
|
42
32
|
}
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
34
|
+
const dragMove = () => {
|
|
35
|
+
cropper.value?.setDragMode('move')
|
|
36
|
+
}
|
|
37
|
+
const dragCrop = () => {
|
|
38
|
+
cropper.value?.setDragMode('crop')
|
|
39
|
+
}
|
|
40
|
+
const rotateLeft90 = () => cropper.value?.rotate(-90)
|
|
41
|
+
const rotateLeft = () => cropper.value?.rotate(-5)
|
|
42
|
+
const rotateRight = () => cropper.value?.rotate(5)
|
|
43
|
+
const rotateRight90 = () => cropper.value?.rotate(90)
|
|
44
|
+
const flipHorizontal = () => cropper.value?.scaleX(cropper.value.getData().scaleX * -1)
|
|
45
|
+
const flipVertical = () => cropper.value?.scaleY(cropper.value.getData().scaleY * -1)
|
|
46
|
+
const zoomIn = () => cropper.value?.zoom(0.1)
|
|
47
|
+
const zoomOut = () => cropper.value?.zoom(-0.1)
|
|
48
|
+
const moveUp = () => cropper.value?.move(0, -10)
|
|
49
|
+
const moveDown = () => cropper.value?.move(0, 10)
|
|
50
|
+
const moveLeft = () => cropper.value?.move(-10, 0)
|
|
51
|
+
const moveRight = () => cropper.value?.move(10, 0)
|
|
52
|
+
const accept = () => {
|
|
53
|
+
const croppedCanvas = cropper.value?.getCroppedCanvas()
|
|
54
|
+
console.log(props.imageFormat)
|
|
55
|
+
imageData.value = croppedCanvas?.toDataURL(props.imageFormat,1)
|
|
56
|
+
}
|
|
57
|
+
const reset = () => cropper.value?.reset()
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
59
|
+
const computedMaxWidth = computed(() => {
|
|
60
|
+
if (typeof props.maxWidth === 'number') {
|
|
61
|
+
return `${props.maxWidth}px`
|
|
62
|
+
} else if (!isNaN(Number(props.maxWidth))) {
|
|
63
|
+
return `${props.maxWidth}px`
|
|
51
64
|
}
|
|
65
|
+
return props.maxWidth
|
|
52
66
|
})
|
|
53
67
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
emit('closeDialog', false)
|
|
68
|
+
const computedMaxHeight = computed(() => {
|
|
69
|
+
if (typeof props.maxHeight === 'number') {
|
|
70
|
+
return `${props.maxHeight}px`
|
|
71
|
+
} else if (!isNaN(Number(props.maxHeight))) {
|
|
72
|
+
return `${props.maxHeight}px`
|
|
60
73
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const resetCrop = () => {
|
|
64
|
-
emit('closeDialog', false)
|
|
65
|
-
emit('openCamera', true)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const close = () => {
|
|
69
|
-
emit('closeDialog', false)
|
|
70
|
-
}
|
|
74
|
+
return props.maxHeight
|
|
75
|
+
})
|
|
71
76
|
</script>
|
|
72
|
-
|
|
73
77
|
<template>
|
|
74
|
-
<
|
|
75
|
-
<v-card
|
|
76
|
-
<v-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<v-btn
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
v-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
v-
|
|
104
|
-
|
|
105
|
-
>
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
style="max-height: 70vh; max-width: 40vh"
|
|
111
|
-
/>
|
|
112
|
-
<v-row
|
|
113
|
-
justify="center"
|
|
114
|
-
class="mt-2"
|
|
115
|
-
>
|
|
116
|
-
<slot
|
|
117
|
-
name="actionButtons"
|
|
118
|
-
:reset-crop="resetCrop"
|
|
119
|
-
:handle-crop="handleCrop"
|
|
120
|
-
>
|
|
121
|
-
<v-btn
|
|
122
|
-
class="ma-1"
|
|
123
|
-
prepend-icon="mdi mdi-camera"
|
|
124
|
-
color="primary"
|
|
125
|
-
@click="resetCrop"
|
|
126
|
-
>
|
|
127
|
-
Retake
|
|
128
|
-
</v-btn>
|
|
129
|
-
<v-btn
|
|
130
|
-
class="ma-1"
|
|
131
|
-
prepend-icon="fa-solid fa-check"
|
|
132
|
-
color="success"
|
|
133
|
-
@click="handleCrop"
|
|
134
|
-
>
|
|
135
|
-
Done
|
|
136
|
-
</v-btn>
|
|
137
|
-
</slot>
|
|
138
|
-
</v-row>
|
|
139
|
-
</v-col>
|
|
140
|
-
</v-row>
|
|
141
|
-
</v-card-text>
|
|
142
|
-
</v-card>
|
|
78
|
+
<div :style="{maxWidth:computedMaxWidth,maxHeight:computedMaxHeight}">
|
|
79
|
+
<v-card>
|
|
80
|
+
<v-toolbar color="primary" dark>
|
|
81
|
+
<v-btn-toggle v-model="dragMode" class="ml-1">
|
|
82
|
+
<v-btn value="move" icon="mdi mdi-cursor-move" @click="dragMove"></v-btn>
|
|
83
|
+
<v-btn value="crop" icon="mdi mdi-crop" @click="dragCrop"></v-btn>
|
|
84
|
+
</v-btn-toggle>
|
|
85
|
+
<v-btn-group class="ml-1">
|
|
86
|
+
<v-btn value="rotateLeft" icon="mdi mdi-rotate-left-variant" @click="rotateLeft90"></v-btn>
|
|
87
|
+
<v-btn value="rotateRight" icon="mdi mdi-rotate-left" @click="rotateLeft"></v-btn>
|
|
88
|
+
<v-btn value="rotateRight" icon="mdi mdi-rotate-right" @click="rotateRight"></v-btn>
|
|
89
|
+
<v-btn value="rotateRight" icon="mdi mdi-rotate-right-variant" @click="rotateRight90"></v-btn>
|
|
90
|
+
</v-btn-group>
|
|
91
|
+
<v-btn-group class="ml-1">
|
|
92
|
+
<v-btn value="flipHorizontal" icon="mdi mdi-flip-horizontal" @click="flipHorizontal"></v-btn>
|
|
93
|
+
<v-btn value="flipVertical" icon="mdi mdi-flip-vertical" @click="flipVertical"></v-btn>
|
|
94
|
+
</v-btn-group>
|
|
95
|
+
<v-btn-group class="ml-1">
|
|
96
|
+
<v-btn value="flipHorizontal" icon="mdi mdi-magnify-plus-outline" @click="zoomIn"></v-btn>
|
|
97
|
+
<v-btn value="flipVertical" icon="mdi mdi-magnify-minus-outline" @click="zoomOut"></v-btn>
|
|
98
|
+
</v-btn-group>
|
|
99
|
+
<v-btn-group class="ml-1">
|
|
100
|
+
<v-btn value="moveUp" icon="mdi mdi-arrow-up" @click="moveUp"></v-btn>
|
|
101
|
+
<v-btn value="moveDown" icon="mdi mdi-arrow-down" @click="moveDown"></v-btn>
|
|
102
|
+
<v-btn value="moveLeft" icon="mdi mdi-arrow-left" @click="moveLeft"></v-btn>
|
|
103
|
+
<v-btn value="moveRight" icon="mdi mdi-arrow-right" @click="moveRight"></v-btn>
|
|
104
|
+
</v-btn-group>
|
|
105
|
+
<v-spacer></v-spacer>
|
|
106
|
+
<v-btn icon="mdi mdi-check" @click="accept"></v-btn>
|
|
107
|
+
<v-btn icon="mdi mdi-refresh" @click="reset"></v-btn>
|
|
108
|
+
</v-toolbar>
|
|
109
|
+
<v-card-text class="ma-0 pa-0">
|
|
110
|
+
<img ref="imageRef" :src="imageData" alt="" @load="loadCropper" style="display: block; max-width: 100%">
|
|
111
|
+
</v-card-text>
|
|
112
|
+
</v-card>
|
|
113
|
+
</div>
|
|
143
114
|
</template>
|
|
115
|
+
<style>
|
|
116
|
+
.cropper-bg{background-repeat:repeat!important}
|
|
117
|
+
</style>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
interface Props {
|
|
3
3
|
label: string
|
|
4
|
-
value?: string
|
|
4
|
+
value?: string | null | undefined
|
|
5
|
+
horizontal?: boolean
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -10,18 +11,30 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
10
11
|
</script>
|
|
11
12
|
|
|
12
13
|
<template>
|
|
13
|
-
<v-
|
|
14
|
-
<
|
|
14
|
+
<div v-if="horizontal" class="d-flex align-end" :="$attrs">
|
|
15
|
+
<div class="text-medium-emphasis">
|
|
15
16
|
<slot name="label">
|
|
16
|
-
{{ label }}
|
|
17
|
+
{{ label }}:
|
|
17
18
|
</slot>
|
|
18
|
-
</
|
|
19
|
-
<
|
|
19
|
+
</div>
|
|
20
|
+
<div class="ml-1">
|
|
20
21
|
<slot name="value">
|
|
21
22
|
{{ value }}
|
|
22
23
|
</slot>
|
|
23
|
-
</
|
|
24
|
-
</
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<v-card v-else variant="flat" :="$attrs">
|
|
27
|
+
<VCardSubtitle class="ma-0 pa-0 text-black">
|
|
28
|
+
<slot name="label">
|
|
29
|
+
{{ label }}
|
|
30
|
+
</slot>
|
|
31
|
+
</VCardSubtitle>
|
|
32
|
+
<VCardText class="text-h6 pa-0 mb-2">
|
|
33
|
+
<slot name="value">
|
|
34
|
+
{{ value }}
|
|
35
|
+
</slot>
|
|
36
|
+
</VCardText>
|
|
37
|
+
</v-card>
|
|
25
38
|
</template>
|
|
26
39
|
|
|
27
40
|
<style lang="">
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
2
|
+
import {VAutocomplete} from 'vuetify/components/VAutocomplete'
|
|
3
|
+
import {concat, isEmpty} from 'lodash-es'
|
|
4
|
+
import {computed, ref, watch} from 'vue'
|
|
5
|
+
import {watchDebounced} from '@vueuse/core'
|
|
6
|
+
import {useFuzzy} from '../../composables/utils/fuzzy'
|
|
7
|
+
import {useGraphQl} from '../../composables/graphql'
|
|
8
8
|
|
|
9
9
|
interface Props extends /* @vue-ignore */ InstanceType<typeof VAutocomplete['$props']> {
|
|
10
10
|
fuzzy?: boolean
|
|
@@ -134,7 +134,7 @@ const computedNoDataText = computed(() => {
|
|
|
134
134
|
>
|
|
135
135
|
<slot
|
|
136
136
|
:name="name"
|
|
137
|
-
v-bind="(slotData as object)"
|
|
137
|
+
v-bind="((slotData || {}) as object)"
|
|
138
138
|
:operation="operation"
|
|
139
139
|
/>
|
|
140
140
|
</template>
|
|
@@ -155,7 +155,7 @@ defineExpose({ reload })
|
|
|
155
155
|
>
|
|
156
156
|
<slot
|
|
157
157
|
:name="name"
|
|
158
|
-
v-bind="(slotData as object)"
|
|
158
|
+
v-bind="((slotData || {}) as object)"
|
|
159
159
|
:operation="operation"
|
|
160
160
|
/>
|
|
161
161
|
</template>
|
|
@@ -196,7 +196,7 @@ defineExpose({ reload })
|
|
|
196
196
|
>
|
|
197
197
|
<slot
|
|
198
198
|
:name="name"
|
|
199
|
-
v-bind="(slotData as object)"
|
|
199
|
+
v-bind="((slotData || {}) as object)"
|
|
200
200
|
:operation="operation"
|
|
201
201
|
/>
|
|
202
202
|
</template>
|
|
@@ -77,7 +77,7 @@ type SortItem = { key: string, order?: boolean | 'asc' | 'desc' }
|
|
|
77
77
|
const sortBy = ref<SortItem[]>()
|
|
78
78
|
|
|
79
79
|
const pageCount = computed(() => {
|
|
80
|
-
if (itemsPerPageInternal.value == 'All' || itemsPerPageInternal.value == '-1' || itemsPerPageInternal.value <= 0
|
|
80
|
+
if (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || itemsPerPageInternal.value == '-1' || Number(itemsPerPageInternal.value) <= 0) return 1
|
|
81
81
|
else return Math.ceil(itemsLength.value / Number(itemsPerPageInternal.value))
|
|
82
82
|
})
|
|
83
83
|
const currentPage = ref<number>(1)
|
|
@@ -86,7 +86,7 @@ watch([currentPage, itemsPerPageInternal, sortBy], () => {
|
|
|
86
86
|
if (canServerPageable.value) {
|
|
87
87
|
loadItems({
|
|
88
88
|
page: currentPage.value,
|
|
89
|
-
itemsPerPage: (itemsPerPageInternal.value == 'All' || itemsPerPageInternal.value <= 0) ? '-1' : itemsPerPageInternal.value,
|
|
89
|
+
itemsPerPage: (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || Number(itemsPerPageInternal.value) <= 0) ? '-1' : itemsPerPageInternal.value,
|
|
90
90
|
sortBy: sortBy.value,
|
|
91
91
|
})
|
|
92
92
|
}
|
|
@@ -99,7 +99,7 @@ const computedInitialData = computed(() => {
|
|
|
99
99
|
})
|
|
100
100
|
|
|
101
101
|
const computedSkeletonPerPage = computed(() => {
|
|
102
|
-
if (itemsPerPageInternal.value == 'All' || itemsPerPageInternal.value <= 0) return 1
|
|
102
|
+
if (!itemsPerPageInternal.value || itemsPerPageInternal.value == 'All' || Number(itemsPerPageInternal.value) <= 0) return 1
|
|
103
103
|
else return Number(itemsPerPageInternal.value)
|
|
104
104
|
})
|
|
105
105
|
|
|
@@ -3,7 +3,10 @@ import { type GraphqlModelConfigProps } from './graphqlModelOperation';
|
|
|
3
3
|
export interface HeaderProps {
|
|
4
4
|
headers?: any[];
|
|
5
5
|
}
|
|
6
|
-
export
|
|
6
|
+
export interface InitialDataProps {
|
|
7
|
+
initialData?: Record<string, any>;
|
|
8
|
+
}
|
|
9
|
+
export type GraphqlModelProps = GraphqlModelConfigProps & Partial<HeaderProps> & Partial<InitialDataProps>;
|
|
7
10
|
export declare function useGraphqlModel<T extends GraphqlModelProps>(props: T): {
|
|
8
11
|
items: import("vue").Ref<Record<string, any>[]>;
|
|
9
12
|
itemsLength: import("vue").Ref<number>;
|
|
@@ -63,7 +63,7 @@ export function useGraphqlModel(props) {
|
|
|
63
63
|
function createItem(item, callback, importing = false) {
|
|
64
64
|
isLoading.value = true;
|
|
65
65
|
return operationCreate.value?.call(fields.value, { input: item }).then((result) => {
|
|
66
|
-
if (canServerPageable) {
|
|
66
|
+
if (canServerPageable.value) {
|
|
67
67
|
if (!importing)
|
|
68
68
|
loadItems(currentOptions.value);
|
|
69
69
|
} else
|
|
@@ -81,7 +81,7 @@ export function useGraphqlModel(props) {
|
|
|
81
81
|
isLoading.value = true;
|
|
82
82
|
const importPromise = [];
|
|
83
83
|
importItems2.forEach((item) => {
|
|
84
|
-
const eachPromise = createItem(item, void 0, true);
|
|
84
|
+
const eachPromise = createItem(Object.assign({}, props.initialData, item), void 0, true);
|
|
85
85
|
if (eachPromise)
|
|
86
86
|
importPromise.push(eachPromise);
|
|
87
87
|
});
|
|
@@ -95,7 +95,7 @@ export function useGraphqlModel(props) {
|
|
|
95
95
|
function updateItem(item, callback) {
|
|
96
96
|
isLoading.value = true;
|
|
97
97
|
return operationUpdate.value?.call(fields.value, { input: item }).then((result) => {
|
|
98
|
-
if (canServerPageable)
|
|
98
|
+
if (canServerPageable.value)
|
|
99
99
|
loadItems(currentOptions.value);
|
|
100
100
|
else {
|
|
101
101
|
const index = items.value.findIndex((item2) => item2[props.modelKey || "id"] === result[props.modelKey || "id"]);
|
|
@@ -124,7 +124,7 @@ export function useGraphqlModel(props) {
|
|
|
124
124
|
}
|
|
125
125
|
function loadItems(options) {
|
|
126
126
|
currentOptions.value = options;
|
|
127
|
-
if (canServerPageable) {
|
|
127
|
+
if (canServerPageable.value) {
|
|
128
128
|
const pageableVariable = {
|
|
129
129
|
page: options.page,
|
|
130
130
|
perPage: options.itemsPerPage,
|
|
@@ -7,6 +7,9 @@ export function useGraphqlModelOperation(props) {
|
|
|
7
7
|
function lowercaseFirstLetter(string) {
|
|
8
8
|
return string?.charAt(0).toLowerCase() + string?.slice(1);
|
|
9
9
|
}
|
|
10
|
+
const computedModelName = computed(() => {
|
|
11
|
+
return props.modelName.split("By")[0].trim();
|
|
12
|
+
});
|
|
10
13
|
const operationCreate = computed(() => {
|
|
11
14
|
if (props.operationCreate) {
|
|
12
15
|
if (typeof props.operationCreate === "string") {
|
|
@@ -16,7 +19,7 @@ export function useGraphqlModelOperation(props) {
|
|
|
16
19
|
return props.operationCreate;
|
|
17
20
|
}
|
|
18
21
|
}
|
|
19
|
-
return graphqlOperation["create" + capitalizeFirstLetter(
|
|
22
|
+
return graphqlOperation["create" + capitalizeFirstLetter(computedModelName.value)];
|
|
20
23
|
});
|
|
21
24
|
const operationUpdate = computed(() => {
|
|
22
25
|
if (props.operationUpdate) {
|
|
@@ -27,7 +30,7 @@ export function useGraphqlModelOperation(props) {
|
|
|
27
30
|
return props.operationUpdate;
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
|
-
return graphqlOperation["update" + capitalizeFirstLetter(
|
|
33
|
+
return graphqlOperation["update" + capitalizeFirstLetter(computedModelName.value)];
|
|
31
34
|
});
|
|
32
35
|
const operationDelete = computed(() => {
|
|
33
36
|
if (props.operationDelete) {
|
|
@@ -38,7 +41,7 @@ export function useGraphqlModelOperation(props) {
|
|
|
38
41
|
return props.operationDelete;
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
|
-
return graphqlOperation["delete" + capitalizeFirstLetter(
|
|
44
|
+
return graphqlOperation["delete" + capitalizeFirstLetter(computedModelName.value)];
|
|
42
45
|
});
|
|
43
46
|
const operationRead = computed(() => {
|
|
44
47
|
if (props.operationRead) {
|
|
@@ -71,7 +74,7 @@ export function useGraphqlModelOperation(props) {
|
|
|
71
74
|
return props.operationSearch;
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
|
-
return graphqlOperation["search" + capitalizeFirstLetter(
|
|
77
|
+
return graphqlOperation["search" + capitalizeFirstLetter(computedModelName.value)];
|
|
75
78
|
});
|
|
76
79
|
return { operationCreate, operationUpdate, operationDelete, operationRead, operationReadPageable, operationSearch };
|
|
77
80
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import {computed} from 'vue'
|
|
3
|
+
import {VTextField} from 'vuetify/components/VTextField'
|
|
4
|
+
import {type MaskaDetail, type MaskOptions, vMaska} from 'maska'
|
|
5
5
|
|
|
6
6
|
export interface Props extends /* @vue-ignore */ InstanceType<typeof VTextField['$props']> {
|
|
7
7
|
type: 'eReceipt' | 'telephone'
|
|
@@ -36,7 +36,7 @@ function onMaska(event: CustomEvent<MaskaDetail>) {
|
|
|
36
36
|
>
|
|
37
37
|
<slot
|
|
38
38
|
:name="name"
|
|
39
|
-
v-bind="(slotData as object)"
|
|
39
|
+
v-bind="((slotData || {}) as object)"
|
|
40
40
|
/>
|
|
41
41
|
</template>
|
|
42
42
|
</v-text-field>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramathibodi/nuxt-commons",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Ramathibodi Nuxt modules for common components",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
"@fullcalendar/multimonth": "^6.1.11",
|
|
67
67
|
"@fullcalendar/timegrid": "^6.1.11",
|
|
68
68
|
"@fullcalendar/vue3": "^6.1.11",
|
|
69
|
-
"@graphql-codegen/cli": "^5.0.2",
|
|
70
69
|
"@graphql-codegen/add": "^5.0.2",
|
|
70
|
+
"@graphql-codegen/cli": "^5.0.2",
|
|
71
71
|
"@graphql-codegen/plugin-helpers": "^5.0.4",
|
|
72
72
|
"@graphql-codegen/typescript": "^4.0.6",
|
|
73
73
|
"@mdi/font": "^7.4.47",
|
|
@@ -79,20 +79,21 @@
|
|
|
79
79
|
"@zxing/browser": "^0.1.4",
|
|
80
80
|
"cropperjs": "^1.6.2",
|
|
81
81
|
"currency.js": "^2.0.4",
|
|
82
|
+
"exif-rotate-js": "^1.5.0",
|
|
82
83
|
"fuse.js": "^7.0.0",
|
|
83
|
-
"graphql": "^16.9.0",
|
|
84
84
|
"gql-query-builder": "^3.8.0",
|
|
85
|
+
"graphql": "^16.9.0",
|
|
85
86
|
"lodash": "^4.17.21",
|
|
86
87
|
"luxon": "^3.4.4",
|
|
87
88
|
"maska": "^2.1.11",
|
|
88
89
|
"pdf-vue3": "^1.0.12",
|
|
90
|
+
"prettier": "3.3.2",
|
|
89
91
|
"print-js": "^1.6.0",
|
|
90
92
|
"uid": "^2.0.2",
|
|
91
93
|
"vue": "^3.4.25",
|
|
92
94
|
"vue-codemirror": "^6.1.1",
|
|
93
95
|
"vue-signature-pad": "^3.0.2",
|
|
94
96
|
"vuetify": "^3.6.8",
|
|
95
|
-
"prettier": "3.3.2",
|
|
96
97
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz"
|
|
97
98
|
},
|
|
98
99
|
"devDependencies": {
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
import { ref, onMounted, watchEffect } from 'vue'
|
|
3
|
-
import { useDevicesList, useUserMedia } from '@vueuse/core'
|
|
4
|
-
import { useAlert } from '../composables/alert'
|
|
5
|
-
|
|
6
|
-
interface Props {
|
|
7
|
-
modelValue?: string
|
|
8
|
-
}
|
|
9
|
-
const props = defineProps<Props>()
|
|
10
|
-
const emit = defineEmits(['update:modelValue', 'closeDialog'])
|
|
11
|
-
|
|
12
|
-
const alert = useAlert()
|
|
13
|
-
const videoRef = ref<HTMLVideoElement>()
|
|
14
|
-
const capturedPhoto = ref<string | null>(null)
|
|
15
|
-
const currentCameraId = ref<ConstrainDOMString | undefined>()
|
|
16
|
-
|
|
17
|
-
const { videoInputs: cameras } = useDevicesList({
|
|
18
|
-
requestPermissions: true,
|
|
19
|
-
constraints: { audio: false, video: true },
|
|
20
|
-
onUpdated() {
|
|
21
|
-
if (!cameras.value.find(camera => camera.deviceId === currentCameraId.value))
|
|
22
|
-
currentCameraId.value = cameras.value[0]?.deviceId
|
|
23
|
-
},
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
const { stream, enabled } = useUserMedia({
|
|
27
|
-
constraints: { video: { deviceId: currentCameraId.value } },
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
watchEffect(() => {
|
|
31
|
-
if (videoRef.value) (videoRef.value as HTMLVideoElement).srcObject = stream.value!
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const captureImage = () => {
|
|
35
|
-
if (videoRef.value) {
|
|
36
|
-
const canvas = document.createElement('canvas')
|
|
37
|
-
canvas.width = (videoRef.value as HTMLVideoElement).videoWidth
|
|
38
|
-
canvas.height = (videoRef.value as HTMLVideoElement).videoHeight
|
|
39
|
-
const context = canvas.getContext('2d')
|
|
40
|
-
if (context) {
|
|
41
|
-
context.drawImage(videoRef.value as HTMLVideoElement, 0, 0, canvas.width, canvas.height)
|
|
42
|
-
capturedPhoto.value = canvas.toDataURL('image/jpeg')
|
|
43
|
-
|
|
44
|
-
enabled.value = false
|
|
45
|
-
emit('update:modelValue', capturedPhoto.value)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function loadImageFile(selectedFile: File | File[] | undefined) {
|
|
51
|
-
if (!selectedFile) {
|
|
52
|
-
alert?.addAlert({ message: 'No file selected.', alertType: 'error' })
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const reader = new FileReader()
|
|
57
|
-
reader.onload = (event) => {
|
|
58
|
-
capturedPhoto.value = event.target?.result as string
|
|
59
|
-
|
|
60
|
-
enabled.value = false
|
|
61
|
-
emit('update:modelValue', capturedPhoto.value)
|
|
62
|
-
}
|
|
63
|
-
const scanImageSingleFile: File = Array.isArray(selectedFile) ? selectedFile[0] : selectedFile
|
|
64
|
-
reader.readAsDataURL(scanImageSingleFile)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const openCamera = () => {
|
|
68
|
-
enabled.value = true
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const closeDialog = () => {
|
|
72
|
-
enabled.value = false
|
|
73
|
-
emit('closeDialog')
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
defineExpose({
|
|
77
|
-
openCamera,
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
onMounted(() => {
|
|
81
|
-
enabled.value = true
|
|
82
|
-
})
|
|
83
|
-
</script>
|
|
84
|
-
|
|
85
|
-
<template>
|
|
86
|
-
<v-card>
|
|
87
|
-
<v-card-title class="d-flex justify-end">
|
|
88
|
-
<v-btn
|
|
89
|
-
icon="fa:fa-solid fa-xmark"
|
|
90
|
-
variant="text"
|
|
91
|
-
@click="closeDialog"
|
|
92
|
-
/>
|
|
93
|
-
</v-card-title>
|
|
94
|
-
<v-card-text class="d-flex justify-center">
|
|
95
|
-
<video
|
|
96
|
-
ref="videoRef"
|
|
97
|
-
autoplay
|
|
98
|
-
style="max-width: 1024px"
|
|
99
|
-
/>
|
|
100
|
-
<div style="z-index: 2000; position: relative; bottom: -410px; left: -80px; height: 50px">
|
|
101
|
-
<FileBtn
|
|
102
|
-
accept="image/*"
|
|
103
|
-
icon="mdi mdi-image-plus"
|
|
104
|
-
icon-only
|
|
105
|
-
@update:model-value="loadImageFile"
|
|
106
|
-
/>
|
|
107
|
-
</div>
|
|
108
|
-
</v-card-text>
|
|
109
|
-
<v-card-actions class="d-flex justify-center">
|
|
110
|
-
<v-btn
|
|
111
|
-
icon
|
|
112
|
-
size="x-large"
|
|
113
|
-
variant="tonal"
|
|
114
|
-
@click="captureImage()"
|
|
115
|
-
>
|
|
116
|
-
<v-icon
|
|
117
|
-
icon="fa:fa-solid fa-circle"
|
|
118
|
-
size="x-large"
|
|
119
|
-
/>
|
|
120
|
-
<v-tooltip
|
|
121
|
-
activator="parent"
|
|
122
|
-
location="top"
|
|
123
|
-
>
|
|
124
|
-
ถ่ายภาพ
|
|
125
|
-
</v-tooltip>
|
|
126
|
-
</v-btn>
|
|
127
|
-
</v-card-actions>
|
|
128
|
-
</v-card>
|
|
129
|
-
</template>
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
import { ref } from 'vue'
|
|
3
|
-
import { isUndefined } from 'lodash'
|
|
4
|
-
|
|
5
|
-
interface Props {
|
|
6
|
-
modelValue?: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const props = defineProps<Props>()
|
|
10
|
-
const emit = defineEmits(['update:modelValue', 'closeDialog', 'openCamera'])
|
|
11
|
-
|
|
12
|
-
const selectedImage = ref<string>()
|
|
13
|
-
const isDialogOpen = ref<boolean>(false)
|
|
14
|
-
const cameraRef = ref()
|
|
15
|
-
|
|
16
|
-
const handleAddImage = (image: string) => {
|
|
17
|
-
selectedImage.value = image
|
|
18
|
-
isDialogOpen.value = true
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const handleCloseDialog = () => {
|
|
22
|
-
isDialogOpen.value = false
|
|
23
|
-
emit('openCamera', true)
|
|
24
|
-
emit('closeDialog', false)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const handleCameraClose = (shouldClose: boolean, editStatus: string) => {
|
|
28
|
-
if (isUndefined(editStatus)) {
|
|
29
|
-
emit('closeDialog', shouldClose)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const handleImageEdit = (imageData: any) => {
|
|
34
|
-
emit('update:modelValue', imageData)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const handleOpenCamera = () => {
|
|
38
|
-
emit('openCamera', true)
|
|
39
|
-
isDialogOpen.value = false
|
|
40
|
-
cameraRef.value.openCamera()
|
|
41
|
-
}
|
|
42
|
-
</script>
|
|
43
|
-
|
|
44
|
-
<template>
|
|
45
|
-
<Camera
|
|
46
|
-
ref="cameraRef"
|
|
47
|
-
@update:model-value="handleAddImage"
|
|
48
|
-
@close-dialog="handleCameraClose"
|
|
49
|
-
/>
|
|
50
|
-
<v-dialog v-model="isDialogOpen">
|
|
51
|
-
<FormImagesEdit
|
|
52
|
-
v-model="selectedImage"
|
|
53
|
-
@update:model-value="handleImageEdit"
|
|
54
|
-
@close-dialog="handleCloseDialog"
|
|
55
|
-
@open-camera="handleOpenCamera"
|
|
56
|
-
/>
|
|
57
|
-
</v-dialog>
|
|
58
|
-
</template>
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
interface Props {
|
|
3
|
-
modelValue?: any
|
|
4
|
-
readonly?: boolean
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
8
|
-
readonly: false,
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
const emit = defineEmits([
|
|
12
|
-
'update:modelValue',
|
|
13
|
-
'closeDialogPreview',
|
|
14
|
-
'toPaint',
|
|
15
|
-
])
|
|
16
|
-
|
|
17
|
-
const editImage = () => {
|
|
18
|
-
emit('toPaint')
|
|
19
|
-
}
|
|
20
|
-
</script>
|
|
21
|
-
|
|
22
|
-
<template>
|
|
23
|
-
<v-card>
|
|
24
|
-
<v-toolbar>
|
|
25
|
-
<v-toolbar-title>Preview</v-toolbar-title>
|
|
26
|
-
<v-spacer />
|
|
27
|
-
<v-btn
|
|
28
|
-
v-if="!props.readonly"
|
|
29
|
-
icon="fa:fa-solid fa-pen"
|
|
30
|
-
variant="text"
|
|
31
|
-
@click="editImage"
|
|
32
|
-
/>
|
|
33
|
-
<v-btn
|
|
34
|
-
icon="fa:fa-solid fa-xmark"
|
|
35
|
-
variant="text"
|
|
36
|
-
@click="$emit('closeDialogPreview', false)"
|
|
37
|
-
/>
|
|
38
|
-
</v-toolbar>
|
|
39
|
-
<v-card-text>
|
|
40
|
-
<v-row>
|
|
41
|
-
<v-img
|
|
42
|
-
:src="modelValue.data"
|
|
43
|
-
height="70dvh"
|
|
44
|
-
/>
|
|
45
|
-
</v-row>
|
|
46
|
-
</v-card-text>
|
|
47
|
-
</v-card>
|
|
48
|
-
</template>
|