@ramathibodi/nuxt-commons 0.1.73 → 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 -96
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -0
- package/dist/runtime/components/Alert.vue +58 -54
- package/dist/runtime/components/BarcodeReader.vue +130 -122
- package/dist/runtime/components/ExportCSV.vue +110 -102
- package/dist/runtime/components/FileBtn.vue +79 -67
- package/dist/runtime/components/ImportCSV.vue +151 -139
- package/dist/runtime/components/MrzReader.vue +168 -0
- package/dist/runtime/components/SplitterPanel.vue +67 -59
- package/dist/runtime/components/TabsGroup.vue +39 -31
- package/dist/runtime/components/TextBarcode.vue +66 -54
- package/dist/runtime/components/device/IdCardButton.vue +95 -83
- package/dist/runtime/components/device/IdCardWebSocket.vue +207 -195
- package/dist/runtime/components/device/Scanner.vue +350 -338
- package/dist/runtime/components/dialog/Confirm.vue +112 -100
- package/dist/runtime/components/dialog/Host.vue +88 -84
- package/dist/runtime/components/dialog/Index.vue +84 -72
- package/dist/runtime/components/dialog/Loading.vue +51 -39
- package/dist/runtime/components/dialog/default/Confirm.vue +112 -100
- package/dist/runtime/components/dialog/default/Loading.vue +60 -48
- package/dist/runtime/components/dialog/default/Notify.vue +82 -70
- package/dist/runtime/components/dialog/default/Printing.vue +46 -34
- package/dist/runtime/components/dialog/default/VerifyUser.vue +144 -132
- package/dist/runtime/components/document/Form.vue +50 -42
- package/dist/runtime/components/document/TemplateBuilder.vue +536 -524
- package/dist/runtime/components/form/ActionPad.vue +156 -144
- package/dist/runtime/components/form/Birthdate.vue +116 -104
- package/dist/runtime/components/form/CheckboxGroup.vue +99 -87
- package/dist/runtime/components/form/CodeEditor.vue +45 -37
- package/dist/runtime/components/form/Date.vue +270 -258
- package/dist/runtime/components/form/DateTime.vue +220 -208
- package/dist/runtime/components/form/Dialog.vue +178 -166
- package/dist/runtime/components/form/EditPad.vue +157 -145
- package/dist/runtime/components/form/File.vue +295 -283
- package/dist/runtime/components/form/Hidden.vue +44 -32
- package/dist/runtime/components/form/Iterator.vue +538 -526
- package/dist/runtime/components/form/Login.vue +143 -131
- package/dist/runtime/components/form/Pad.vue +399 -387
- package/dist/runtime/components/form/SignPad.vue +226 -218
- package/dist/runtime/components/form/System.vue +34 -26
- package/dist/runtime/components/form/Table.vue +391 -379
- package/dist/runtime/components/form/TableData.vue +236 -224
- package/dist/runtime/components/form/Time.vue +177 -165
- package/dist/runtime/components/form/images/Capture.vue +245 -237
- package/dist/runtime/components/form/images/Edit.vue +133 -121
- package/dist/runtime/components/form/images/Field.vue +331 -320
- package/dist/runtime/components/form/images/Pad.vue +54 -42
- package/dist/runtime/components/label/Date.vue +37 -29
- package/dist/runtime/components/label/DateAgo.vue +102 -94
- package/dist/runtime/components/label/DateCount.vue +152 -144
- package/dist/runtime/components/label/Field.vue +111 -103
- package/dist/runtime/components/label/FormatMoney.vue +37 -29
- package/dist/runtime/components/label/Mask.vue +46 -38
- package/dist/runtime/components/label/Object.vue +21 -13
- package/dist/runtime/components/master/Autocomplete.vue +89 -81
- package/dist/runtime/components/master/Combobox.vue +88 -80
- package/dist/runtime/components/master/RadioGroup.vue +90 -78
- package/dist/runtime/components/master/Select.vue +70 -62
- package/dist/runtime/components/master/label.vue +55 -47
- package/dist/runtime/components/model/Autocomplete.vue +91 -79
- package/dist/runtime/components/model/Combobox.vue +90 -78
- package/dist/runtime/components/model/Pad.vue +114 -102
- package/dist/runtime/components/model/Select.vue +78 -72
- package/dist/runtime/components/model/Table.vue +370 -358
- package/dist/runtime/components/model/iterator.vue +497 -489
- package/dist/runtime/components/model/label.vue +58 -50
- package/dist/runtime/components/pdf/Print.vue +75 -63
- package/dist/runtime/components/pdf/View.vue +146 -134
- package/dist/runtime/composables/alert.d.ts +4 -0
- package/dist/runtime/composables/api.d.ts +4 -0
- package/dist/runtime/composables/dialog.d.ts +1 -1
- package/dist/runtime/composables/document/templateFormHidden.d.ts +4 -0
- 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/localStorageModel.d.ts +4 -0
- package/dist/runtime/composables/lookupList.d.ts +4 -0
- package/dist/runtime/composables/menu.d.ts +4 -0
- package/dist/runtime/composables/useMrzReader.d.ts +48 -0
- package/dist/runtime/composables/useMrzReader.js +423 -0
- package/dist/runtime/composables/useTesseract.d.ts +16 -0
- package/dist/runtime/composables/useTesseract.js +45 -0
- 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/dist/runtime/utils/asset.d.ts +2 -0
- package/dist/runtime/utils/asset.js +49 -0
- package/package.json +131 -122
- package/scripts/enrich-vue-docs-from-ai.mjs +197 -0
- package/scripts/generate-ai-summary.mjs +321 -0
- package/scripts/generate-composables-md.mjs +129 -0
- package/scripts/postInstall.cjs +70 -70
- package/templates/.codegen/codegen.ts +32 -32
- package/templates/.codegen/plugin-schema-object.js +161 -161
- package/templates/public/tesseract/mrz.traineddata.gz +0 -0
- package/templates/public/tesseract/ocrb.traineddata.gz +0 -0
|
@@ -1,320 +1,331 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
modelValue
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
alert?.addAlert({ message:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
},
|
|
212
|
-
{ immediate:
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
<
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
<
|
|
315
|
-
<v-
|
|
316
|
-
</
|
|
317
|
-
</
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
/**
|
|
3
|
+
* FormImagesField 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 { ref, watch,computed } from 'vue'
|
|
7
|
+
import { isEqual } from 'lodash-es'
|
|
8
|
+
import { VInput } from 'vuetify/components/VInput'
|
|
9
|
+
import { assetUrl } from '#imports'
|
|
10
|
+
import { useAlert } from '../../../composables/alert'
|
|
11
|
+
import { useAssetFile, type Base64Image, type Base64Asset, type Base64File} from '../../../composables/assetFile'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Custom events emitted by FormImagesField.
|
|
15
|
+
* Parents can listen to these events to react to user actions and internal state changes.
|
|
16
|
+
*/
|
|
17
|
+
const emit = defineEmits<{
|
|
18
|
+
(e: 'update:modelValue', value: Base64Image[]): void
|
|
19
|
+
}>()
|
|
20
|
+
|
|
21
|
+
const alert = useAlert()
|
|
22
|
+
const { fileToBase64, hydrateAssetFile } = useAssetFile()
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
modelValue?: Base64Image[] // Bound value for v-model synchronization with the parent component.
|
|
26
|
+
readonly?: boolean // renders as read-only while keeping value visible
|
|
27
|
+
label?: string // UI label displayed to end users
|
|
28
|
+
accept?: string // Accepted file MIME types or extensions for file selection.
|
|
29
|
+
autoHydrate?: boolean // Converts incoming serialized values into component runtime format on mount/watch.
|
|
30
|
+
maxFileSize?: number // Maximum allowed image file size in MB before conversion.
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Public props accepted by FormImagesField.
|
|
34
|
+
* Document each prop field with intent, defaults, and side effects for clear generated docs.
|
|
35
|
+
*/
|
|
36
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
37
|
+
modelValue: () => [] as Base64Image[],
|
|
38
|
+
accept: '.jpg,.jpeg,.png,.webp,.gif,.bmp,.tiff,.tif',
|
|
39
|
+
autoHydrate: false,
|
|
40
|
+
maxFileSize: 10,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/** Internal state (always Base64Image[]) */
|
|
44
|
+
const images = ref<Base64Image[]>([])
|
|
45
|
+
const uploadImages = ref<File[]>([])
|
|
46
|
+
|
|
47
|
+
/** Dialogs */
|
|
48
|
+
const dialog = ref(false) // capture dialog
|
|
49
|
+
const dialogUpdate = ref(false) // edit dialog
|
|
50
|
+
const dataUpdate = ref<Base64Image>({ imageData: {}, imageTitle: '', imageProps: {} })
|
|
51
|
+
|
|
52
|
+
/** Fullscreen preview */
|
|
53
|
+
const dialogImageFullScreen = ref(false)
|
|
54
|
+
const imageFullScreen = ref<{title: string,image: string|undefined}>({ title: '', image: '' })
|
|
55
|
+
|
|
56
|
+
/** ---------- Stable keys + guards ---------- */
|
|
57
|
+
let internalSync = false
|
|
58
|
+
let lastEmittedSig = '' // signature of last emitted state
|
|
59
|
+
|
|
60
|
+
function imageKey(im: Base64Image): string {
|
|
61
|
+
const id = im.imageData?.id
|
|
62
|
+
if (id != null) return `id:${id}`
|
|
63
|
+
const title = im.imageTitle ?? ''
|
|
64
|
+
const len = im.imageData?.base64String?.length ?? 0
|
|
65
|
+
return `t:${title}|l:${len}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function signature(arr: Base64Image[]): string {
|
|
69
|
+
return arr.map(imageKey).join('|')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Emit helper (guarded + updates last signature) */
|
|
73
|
+
function emitNow(next: Base64Image[]) {
|
|
74
|
+
const sig = signature(next)
|
|
75
|
+
if (sig === lastEmittedSig) return
|
|
76
|
+
internalSync = true
|
|
77
|
+
try {
|
|
78
|
+
emit('update:modelValue', next)
|
|
79
|
+
lastEmittedSig = sig
|
|
80
|
+
} finally {
|
|
81
|
+
queueMicrotask(() => { internalSync = false })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** ---------- Helpers ---------- */
|
|
86
|
+
|
|
87
|
+
const addImage = (img: Base64Image) => {
|
|
88
|
+
images.value.push({
|
|
89
|
+
imageData: img.imageData ?? {},
|
|
90
|
+
imageTitle: img.imageTitle ?? '',
|
|
91
|
+
imageProps: img.imageProps ?? {},
|
|
92
|
+
})
|
|
93
|
+
dialog.value = false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const remove = (index: number) => {
|
|
97
|
+
images.value.splice(index, 1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const setDataUpdate = (img: Base64Image) => {
|
|
101
|
+
dataUpdate.value = {
|
|
102
|
+
imageData: { ...(img.imageData ?? {}) },
|
|
103
|
+
imageTitle: img.imageTitle ?? '',
|
|
104
|
+
imageProps: { ...(img.imageProps ?? {}) },
|
|
105
|
+
}
|
|
106
|
+
dialogUpdate.value = true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const checkDuplicationName = (name: string) => images.value.some(({ imageTitle }) => isEqual(imageTitle, name))
|
|
110
|
+
|
|
111
|
+
const isImageDataUrl = (dataUrl: string) => /^data:image\//i.test(dataUrl)
|
|
112
|
+
|
|
113
|
+
const imageSrcFromImageData = (imageData: Base64Image) => {
|
|
114
|
+
if (imageData?.imageData?.base64String) return useAssetFile().ensureDataUrl(imageData?.imageData?.base64String.trim(),(imageData?.imageData as Base64File).fileType || "image/png")
|
|
115
|
+
if (imageData?.imageData?.id != null) return assetUrl(imageData.imageData.id)
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** File → Base64Image using composable */
|
|
120
|
+
const fileToBase64Image = async (file: File): Promise<Base64Image | null> => {
|
|
121
|
+
try {
|
|
122
|
+
const base64 = await fileToBase64(file,props.maxFileSize)
|
|
123
|
+
const dataUrl = base64.base64String || ''
|
|
124
|
+
if (!isImageDataUrl(dataUrl)) {
|
|
125
|
+
alert?.addAlert({ message: `File "${base64.fileName}" is not supported image type.`, alertType: 'error' })
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
imageData: { base64String: dataUrl } as Base64Asset,
|
|
130
|
+
imageTitle: base64.fileName,
|
|
131
|
+
imageProps: {},
|
|
132
|
+
}
|
|
133
|
+
} catch (e: any) {
|
|
134
|
+
alert?.addAlert({ message: String(e), alertType: 'error' })
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Handle upload button update */
|
|
140
|
+
const uploadImageFile = async () => {
|
|
141
|
+
const duplicated: string[] = []
|
|
142
|
+
for (const file of uploadImages.value) {
|
|
143
|
+
if (checkDuplicationName(file.name)) {
|
|
144
|
+
duplicated.push(file.name)
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
const base64Image = await fileToBase64Image(file)
|
|
148
|
+
if (base64Image) addImage(base64Image)
|
|
149
|
+
}
|
|
150
|
+
uploadImages.value = []
|
|
151
|
+
if (duplicated.length) {
|
|
152
|
+
alert?.addAlert({ message: `File(s) are duplicated. ${duplicated.join(', ')}`, alertType: 'error' })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Capture flow (FormDialog) */
|
|
157
|
+
type FormDialogCallback = { done: () => void }
|
|
158
|
+
const modelData = ref()
|
|
159
|
+
|
|
160
|
+
const captureImage = (payload: any, cb: FormDialogCallback) => {
|
|
161
|
+
const dataUrl: string = payload?.imageCapture ?? ''
|
|
162
|
+
if (!dataUrl || !isImageDataUrl(dataUrl)) {
|
|
163
|
+
alert?.addAlert({ message: 'Invalid image.', alertType: 'error' })
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
addImage({
|
|
167
|
+
imageData: { base64String: dataUrl },
|
|
168
|
+
imageTitle: Math.random().toString(36).slice(2, 11),
|
|
169
|
+
imageProps: {},
|
|
170
|
+
})
|
|
171
|
+
cb?.done?.()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Fullscreen preview */
|
|
175
|
+
const openImageFullScreen = (img: Base64Image) => {
|
|
176
|
+
dialogImageFullScreen.value = true
|
|
177
|
+
imageFullScreen.value.title = img.imageTitle ?? ''
|
|
178
|
+
imageFullScreen.value.image = imageSrcFromImageData(img)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** ---------- Watchers (signature-based) ---------- */
|
|
182
|
+
|
|
183
|
+
/* Parent → Internal */
|
|
184
|
+
watch(
|
|
185
|
+
() => props.modelValue,
|
|
186
|
+
async (val) => {
|
|
187
|
+
if (internalSync) return
|
|
188
|
+
|
|
189
|
+
const next = Array.isArray(val) ? [...val] : []
|
|
190
|
+
const nextSig = signature(next)
|
|
191
|
+
|
|
192
|
+
// Only reassign when truly different
|
|
193
|
+
if (nextSig !== signature(images.value)) {
|
|
194
|
+
images.value = next
|
|
195
|
+
|
|
196
|
+
// optional hydration
|
|
197
|
+
if (props.autoHydrate && images.value.length) {
|
|
198
|
+
const targets = images.value.filter(
|
|
199
|
+
(im) => im.imageData?.id != null && !im.imageData?.base64String
|
|
200
|
+
)
|
|
201
|
+
if (targets.length) {
|
|
202
|
+
await Promise.allSettled(targets.map((im) => hydrateAssetFile(im.imageData!)))
|
|
203
|
+
// After hydration, emit once (guarded) to update parent
|
|
204
|
+
emitNow(images.value)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// sync lastEmittedSig to current internal state so next local change emits
|
|
209
|
+
lastEmittedSig = signature(images.value)
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
{ deep: true, immediate: true }
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
/* Internal → Parent: watch the signature instead of deep structure */
|
|
216
|
+
watch(
|
|
217
|
+
() => signature(images.value),
|
|
218
|
+
() => {
|
|
219
|
+
if (internalSync) return
|
|
220
|
+
// Only emit when signature actually changes
|
|
221
|
+
emitNow(images.value)
|
|
222
|
+
},
|
|
223
|
+
{ immediate: false }
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
// validation passthrough
|
|
227
|
+
const inputRef = ref<InstanceType<typeof VInput> | null>(null)
|
|
228
|
+
|
|
229
|
+
const isValid = computed(() => inputRef.value?.isValid)
|
|
230
|
+
const errorMessages = computed(() => inputRef.value?.errorMessages)
|
|
231
|
+
|
|
232
|
+
defineExpose({
|
|
233
|
+
errorMessages,
|
|
234
|
+
isValid,
|
|
235
|
+
reset: () => inputRef.value?.reset(),
|
|
236
|
+
resetValidation: () => inputRef.value?.resetValidation(),
|
|
237
|
+
validate: () => inputRef.value?.validate(),
|
|
238
|
+
})
|
|
239
|
+
</script>
|
|
240
|
+
|
|
241
|
+
<template>
|
|
242
|
+
<v-input v-model="images" v-bind="$attrs" ref="inputRef">
|
|
243
|
+
<template #default="{ isReadonly, isDisabled }">
|
|
244
|
+
<v-container fluid>
|
|
245
|
+
<v-row>
|
|
246
|
+
<v-col class="pa-0">
|
|
247
|
+
<VCard>
|
|
248
|
+
<VToolbar density="compact">
|
|
249
|
+
<VToolbarTitle>{{ label }}</VToolbarTitle>
|
|
250
|
+
<v-spacer />
|
|
251
|
+
<VToolbarItems v-if="!readonly">
|
|
252
|
+
<FileBtn
|
|
253
|
+
v-model="uploadImages"
|
|
254
|
+
:accept="accept"
|
|
255
|
+
color="primary"
|
|
256
|
+
icon="mdi:mdi-image-plus"
|
|
257
|
+
icon-only
|
|
258
|
+
multiple
|
|
259
|
+
variant="text"
|
|
260
|
+
@update:model-value="uploadImageFile"
|
|
261
|
+
:disabled="isDisabled?.value" :readonly="isReadonly?.value"
|
|
262
|
+
/>
|
|
263
|
+
<v-btn color="primary" icon @click="dialog = true" :disabled="isDisabled?.value" :readonly="isReadonly?.value">
|
|
264
|
+
<v-icon>mdi mdi-camera-plus</v-icon>
|
|
265
|
+
</v-btn>
|
|
266
|
+
</VToolbarItems>
|
|
267
|
+
</VToolbar>
|
|
268
|
+
|
|
269
|
+
<VCardText>
|
|
270
|
+
<VRow dense justify="start">
|
|
271
|
+
<VCol v-for="(image, index) in images" :key="`${imageKey(image)}-${index}`" cols="4">
|
|
272
|
+
<VCard>
|
|
273
|
+
<VToolbar density="compact">
|
|
274
|
+
<VToolbarTitle>
|
|
275
|
+
{{ image.imageTitle }}
|
|
276
|
+
</VToolbarTitle>
|
|
277
|
+
<VSpacer />
|
|
278
|
+
<VToolbarItems v-if="!readonly">
|
|
279
|
+
<v-btn icon @click="remove(index)" :disabled="isDisabled?.value" :readonly="isReadonly?.value">
|
|
280
|
+
<v-icon>mdi mdi-delete-outline</v-icon>
|
|
281
|
+
</v-btn>
|
|
282
|
+
<v-btn color="primary" icon @click="setDataUpdate(image)" v-if="!image.imageData?.id" :disabled="isDisabled?.value" :readonly="isReadonly?.value">
|
|
283
|
+
<v-icon>mdi mdi-image-edit-outline</v-icon>
|
|
284
|
+
</v-btn>
|
|
285
|
+
</VToolbarItems>
|
|
286
|
+
</VToolbar>
|
|
287
|
+
|
|
288
|
+
<v-img
|
|
289
|
+
:src="imageSrcFromImageData(image)"
|
|
290
|
+
height="250"
|
|
291
|
+
@click="() => { (props.readonly || image.imageData?.id || isReadonly?.value) ? openImageFullScreen(image) : setDataUpdate(image) }"
|
|
292
|
+
:disabled="isDisabled?.value"
|
|
293
|
+
/>
|
|
294
|
+
</VCard>
|
|
295
|
+
</VCol>
|
|
296
|
+
</VRow>
|
|
297
|
+
</VCardText>
|
|
298
|
+
</VCard>
|
|
299
|
+
</v-col>
|
|
300
|
+
</v-row>
|
|
301
|
+
</v-container>
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
<!-- Edit dialog -->
|
|
305
|
+
<VDialog v-model="dialogUpdate" fullscreen transition="dialog-bottom-transition">
|
|
306
|
+
<FormImagesPad
|
|
307
|
+
v-model="dataUpdate.imageData.base64String"
|
|
308
|
+
@closedDialog="dialogUpdate = false"
|
|
309
|
+
/>
|
|
310
|
+
</VDialog>
|
|
311
|
+
|
|
312
|
+
<!-- Capture dialog -->
|
|
313
|
+
<FormDialog v-model="dialog" :form-data="modelData" @create="captureImage">
|
|
314
|
+
<template #default="{ data }">
|
|
315
|
+
<FormImagesCapture v-model="data.imageCapture" />
|
|
316
|
+
</template>
|
|
317
|
+
</FormDialog>
|
|
318
|
+
|
|
319
|
+
<!-- Fullscreen preview -->
|
|
320
|
+
<v-dialog v-model="dialogImageFullScreen">
|
|
321
|
+
<v-toolbar :title="imageFullScreen.title">
|
|
322
|
+
<v-spacer />
|
|
323
|
+
<v-btn icon="mdi mdi-close" @click="dialogImageFullScreen = false" />
|
|
324
|
+
</v-toolbar>
|
|
325
|
+
<v-card height="80vh">
|
|
326
|
+
<v-img :src="imageFullScreen.image" />
|
|
327
|
+
</v-card>
|
|
328
|
+
</v-dialog>
|
|
329
|
+
</template>
|
|
330
|
+
</v-input>
|
|
331
|
+
</template>
|