@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,284 +1,296 @@
|
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
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
|
-
|
|
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
|
-
</v-chip>
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
/**
|
|
3
|
+
* FormFile is a schema-driven form field component that binds model data, renders field UI, and emits normalized updates.
|
|
4
|
+
* This doc block is consumed by vue-docgen for generated API documentation.
|
|
5
|
+
*/
|
|
6
|
+
import { castArray } from 'lodash-es'
|
|
7
|
+
import { ref, shallowRef, watch, computed } from 'vue'
|
|
8
|
+
import { VTextField } from 'vuetify/components/VTextField'
|
|
9
|
+
import { useAlert } from '../../composables/alert'
|
|
10
|
+
import { type Base64File } from '../../composables/assetFile'
|
|
11
|
+
import { useAssetFile } from '../../composables/assetFile'
|
|
12
|
+
|
|
13
|
+
const alert = useAlert()
|
|
14
|
+
const { hydrateAssetFile, base64ToFile, fileToBase64, downloadBase64File } = useAssetFile()
|
|
15
|
+
|
|
16
|
+
const fileToBase64WithMaxSize = (file: File) => fileToBase64(file, props.maxSize)
|
|
17
|
+
|
|
18
|
+
interface Props extends /* @vue-ignore */ InstanceType<typeof VTextField['$props']> {
|
|
19
|
+
accept?: string // Accepted file MIME types or extensions for file selection.
|
|
20
|
+
multiple?: boolean // Allows selecting or uploading more than one file.
|
|
21
|
+
maxSize?: number // Maximum allowed output size (MB) before upload is blocked.
|
|
22
|
+
modelValue?: Base64File | Base64File[] | null // Bound value for v-model synchronization with the parent component.
|
|
23
|
+
downloadable?: boolean // Shows download actions for selected/loaded files.
|
|
24
|
+
autoHydrate?: boolean // Converts incoming serialized values into component runtime format on mount/watch.
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Public props accepted by FormFile.
|
|
29
|
+
* Document each prop field with intent, defaults, and side effects for clear generated docs.
|
|
30
|
+
*/
|
|
31
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
32
|
+
accept: '*',
|
|
33
|
+
multiple: false,
|
|
34
|
+
maxSize: 5,
|
|
35
|
+
downloadable: false,
|
|
36
|
+
autoHydrate: false,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Custom events emitted by FormFile.
|
|
41
|
+
* Parents can listen to these events to react to user actions and internal state changes.
|
|
42
|
+
*/
|
|
43
|
+
const emit = defineEmits<{
|
|
44
|
+
(e: 'update:modelValue', value: Base64File | Base64File[] | null): void
|
|
45
|
+
}>()
|
|
46
|
+
|
|
47
|
+
/** UI ref */
|
|
48
|
+
const fileInput = ref<HTMLInputElement | null>(null)
|
|
49
|
+
|
|
50
|
+
/** Internal sources (always arrays) */
|
|
51
|
+
const assets = ref<Base64File[]>([]) // items with server id (or ones we keep as “assets”)
|
|
52
|
+
const files = shallowRef<File[]>([]) // native File objects picked by the user
|
|
53
|
+
|
|
54
|
+
/** Cache to avoid re-reading the same File repeatedly */
|
|
55
|
+
const fileCache = new WeakMap<File, Promise<Base64File>>()
|
|
56
|
+
|
|
57
|
+
/** Re-entrancy guard to break the emit -> props watcher -> syncFromModel loop */
|
|
58
|
+
let internalSync = false
|
|
59
|
+
|
|
60
|
+
/** Build a stable dedupe key */
|
|
61
|
+
function base64FileKey(x: Base64File): string {
|
|
62
|
+
if (x.id != null) return `id:${x.id}`
|
|
63
|
+
const name = x.fileName ?? ''
|
|
64
|
+
const len = x.base64String?.length ?? 0
|
|
65
|
+
return `n:${name}|l:${len}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Dedupe by key (preserves order) */
|
|
69
|
+
function uniqByKey(arr: Base64File[]): Base64File[] {
|
|
70
|
+
const seen = new Set<string>()
|
|
71
|
+
const out: Base64File[] = []
|
|
72
|
+
for (const it of arr) {
|
|
73
|
+
const k = base64FileKey(it)
|
|
74
|
+
if (!seen.has(k)) {
|
|
75
|
+
seen.add(k)
|
|
76
|
+
out.push(it)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Shallow equal by key sequence */
|
|
83
|
+
function arraysEqualByKey(a: Base64File[], b: Base64File[]): boolean {
|
|
84
|
+
if (a === b) return true
|
|
85
|
+
if (a.length !== b.length) return false
|
|
86
|
+
for (let i = 0; i < a.length; i++) {
|
|
87
|
+
if (base64FileKey(a[i]) !== base64FileKey(b[i])) return false
|
|
88
|
+
}
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Normalize incoming modelValue into arrays for internal use */
|
|
93
|
+
async function syncFromModel() {
|
|
94
|
+
const mv = props.modelValue
|
|
95
|
+
if (!mv) {
|
|
96
|
+
assets.value = []
|
|
97
|
+
files.value = []
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const asArray = castArray(mv) as Base64File[]
|
|
102
|
+
|
|
103
|
+
// Split into “asset with id” vs “inline base64” (no id)
|
|
104
|
+
const incomingAssets = asArray.filter(a => a.id != null)
|
|
105
|
+
const inlineBase64 = asArray.filter(a => a.id == null && a.base64String)
|
|
106
|
+
|
|
107
|
+
// Convert inline base64 to File for consistent UX
|
|
108
|
+
const inlineFiles: File[] = []
|
|
109
|
+
for (const item of inlineBase64) {
|
|
110
|
+
const f = base64ToFile(item.base64String as string, item.fileName)
|
|
111
|
+
if (f) inlineFiles.push(f)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
assets.value = incomingAssets
|
|
115
|
+
files.value = inlineFiles
|
|
116
|
+
|
|
117
|
+
// Optionally hydrate assets that need it
|
|
118
|
+
if (props.autoHydrate) {
|
|
119
|
+
const needsHydration = assets.value.filter(a => a.id != null && !a.base64String)
|
|
120
|
+
if (needsHydration.length) {
|
|
121
|
+
await Promise.allSettled(needsHydration.map(hydrateAssetFile))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Open chooser */
|
|
127
|
+
function openWindowUpload() {
|
|
128
|
+
if (props.multiple || (assets.value.length === 0 && files.value.length === 0)) {
|
|
129
|
+
fileInput.value?.click()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Add files from input (single or multiple) */
|
|
134
|
+
function addFiles(payload: File | File[]) {
|
|
135
|
+
if (Array.isArray(payload)) files.value = [...files.value, ...payload]
|
|
136
|
+
else files.value = [...files.value, payload]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Remove chips */
|
|
140
|
+
function removeFileByIndex(i: number | string) {
|
|
141
|
+
const idx = Number(i)
|
|
142
|
+
if (idx >= 0 && idx < files.value.length) {
|
|
143
|
+
const copy = files.value.slice()
|
|
144
|
+
copy.splice(idx, 1)
|
|
145
|
+
files.value = copy
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function removeAssetByIndex(i: number | string) {
|
|
149
|
+
const idx = Number(i)
|
|
150
|
+
if (idx >= 0 && idx < assets.value.length) {
|
|
151
|
+
const copy = assets.value.slice()
|
|
152
|
+
copy.splice(idx, 1)
|
|
153
|
+
assets.value = copy
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Convert current files → Base64File[] (cached) */
|
|
158
|
+
async function filesToBase64Files(list: File[]): Promise<Base64File[]> {
|
|
159
|
+
const tasks = list.map(f => {
|
|
160
|
+
let p = fileCache.get(f)
|
|
161
|
+
if (!p) {
|
|
162
|
+
p = fileToBase64WithMaxSize(f)
|
|
163
|
+
fileCache.set(f, p)
|
|
164
|
+
}
|
|
165
|
+
return p
|
|
166
|
+
})
|
|
167
|
+
return Promise.all(tasks)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Combined result (array form) */
|
|
171
|
+
const combinedArray = ref<Base64File[]>([])
|
|
172
|
+
|
|
173
|
+
/** Dirty flag for <v-text-field> */
|
|
174
|
+
const isDirty = computed(() =>
|
|
175
|
+
props.multiple ? combinedArray.value.length > 0 : combinedArray.value[0] != null
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
/** Sync from props.modelValue (guarded) */
|
|
179
|
+
watch(
|
|
180
|
+
() => props.modelValue,
|
|
181
|
+
() => {
|
|
182
|
+
if (internalSync) return
|
|
183
|
+
void syncFromModel()
|
|
184
|
+
},
|
|
185
|
+
{ deep: true, immediate: true }
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
/** Rebuild combined and emit (guarded, deduped, only-on-change) */
|
|
189
|
+
watch([assets, files], async () => {
|
|
190
|
+
try {
|
|
191
|
+
const base64FromFiles = await filesToBase64Files(files.value)
|
|
192
|
+
|
|
193
|
+
const mergedArray = props.multiple
|
|
194
|
+
? [...assets.value, ...base64FromFiles]
|
|
195
|
+
: (assets.value[0] ?? base64FromFiles[0] ?? null)
|
|
196
|
+
? [assets.value[0] ?? base64FromFiles[0]]
|
|
197
|
+
: []
|
|
198
|
+
|
|
199
|
+
const nextCombined = props.multiple ? uniqByKey(mergedArray) : mergedArray
|
|
200
|
+
|
|
201
|
+
if (!arraysEqualByKey(combinedArray.value, nextCombined)) {
|
|
202
|
+
combinedArray.value = nextCombined
|
|
203
|
+
|
|
204
|
+
internalSync = true
|
|
205
|
+
try {
|
|
206
|
+
if (props.multiple) {
|
|
207
|
+
emit('update:modelValue', combinedArray.value)
|
|
208
|
+
} else {
|
|
209
|
+
emit('update:modelValue', combinedArray.value[0] ?? null)
|
|
210
|
+
}
|
|
211
|
+
} finally {
|
|
212
|
+
// let Vue flush the emit before re-enabling the external sync
|
|
213
|
+
queueMicrotask(() => { internalSync = false })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch (error: any) {
|
|
217
|
+
alert?.addAlert({ message: String(error), alertType: 'error' })
|
|
218
|
+
files.value = []
|
|
219
|
+
const fallback = props.multiple
|
|
220
|
+
? [...assets.value]
|
|
221
|
+
: (assets.value[0] ? [assets.value[0]] : [])
|
|
222
|
+
|
|
223
|
+
if (!arraysEqualByKey(combinedArray.value, fallback)) {
|
|
224
|
+
combinedArray.value = fallback
|
|
225
|
+
internalSync = true
|
|
226
|
+
try {
|
|
227
|
+
if (props.multiple) emit('update:modelValue', combinedArray.value)
|
|
228
|
+
else emit('update:modelValue', combinedArray.value[0] ?? null)
|
|
229
|
+
} finally {
|
|
230
|
+
queueMicrotask(() => { internalSync = false })
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}, { deep: true, immediate: true })
|
|
235
|
+
</script>
|
|
236
|
+
|
|
237
|
+
<template>
|
|
238
|
+
<v-text-field
|
|
239
|
+
v-bind="$attrs"
|
|
240
|
+
label="Upload files"
|
|
241
|
+
readonly
|
|
242
|
+
:dirty="isDirty"
|
|
243
|
+
v-on="isDirty && !props.multiple ? {} : { click: openWindowUpload }"
|
|
244
|
+
>
|
|
245
|
+
<template #default>
|
|
246
|
+
<!-- Server/asset items -->
|
|
247
|
+
<v-chip
|
|
248
|
+
v-for="(asset, index) in assets"
|
|
249
|
+
:key="`${asset.id ?? asset.fileName}-${index}`"
|
|
250
|
+
color="green"
|
|
251
|
+
variant="flat"
|
|
252
|
+
closable
|
|
253
|
+
@click:close="removeAssetByIndex(index)"
|
|
254
|
+
>
|
|
255
|
+
{{ asset.originalFileName || asset.fileName }}
|
|
256
|
+
<template #append v-if="props.downloadable">
|
|
257
|
+
<slot name="download" :item="asset">
|
|
258
|
+
<v-icon
|
|
259
|
+
v-if="asset.base64String"
|
|
260
|
+
@click.stop="downloadBase64File(asset.base64String, asset.originalFileName || asset.fileName)"
|
|
261
|
+
>
|
|
262
|
+
mdi mdi-download
|
|
263
|
+
</v-icon>
|
|
264
|
+
</slot>
|
|
265
|
+
</template>
|
|
266
|
+
</v-chip>
|
|
267
|
+
|
|
268
|
+
<!-- Local uploaded files -->
|
|
269
|
+
<v-chip
|
|
270
|
+
v-for="(file, index) in files"
|
|
271
|
+
:key="`${file.name}-${file.size}-${index}`"
|
|
272
|
+
color="primary"
|
|
273
|
+
variant="flat"
|
|
274
|
+
closable
|
|
275
|
+
@click:close="removeFileByIndex(index)"
|
|
276
|
+
>
|
|
277
|
+
{{ file.name }}
|
|
278
|
+
</v-chip>
|
|
279
|
+
</template>
|
|
280
|
+
|
|
281
|
+
<!-- Add more button for multi mode -->
|
|
282
|
+
<template v-if="props.multiple && combinedArray.length > 0" #append-inner>
|
|
283
|
+
<VBtn variant="text" :icon="true" @click="openWindowUpload">
|
|
284
|
+
<v-icon>mdi mdi-plus</v-icon>
|
|
285
|
+
</VBtn>
|
|
286
|
+
</template>
|
|
287
|
+
</v-text-field>
|
|
288
|
+
|
|
289
|
+
<v-file-input
|
|
290
|
+
ref="fileInput"
|
|
291
|
+
:accept="props.accept"
|
|
292
|
+
:multiple="props.multiple"
|
|
293
|
+
style="display: none"
|
|
294
|
+
@update:model-value="addFiles"
|
|
295
|
+
/>
|
|
284
296
|
</template>
|