@7365admin1/layer-common 1.10.7 → 1.10.9
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/CHANGELOG.md +12 -0
- package/components/AccessCardAddForm.vue +1 -1
- package/components/AccessCardAssignToUnitForm.vue +1 -1
- package/components/AccessManagement.vue +1 -1
- package/components/{AddSupplyForm.vue → AddEqupmentForm.vue} +5 -5
- package/components/BuildingManagement/units.vue +2 -2
- package/components/BuildingUnitFormAdd.vue +4 -4
- package/components/BuildingUnitFormEdit.vue +114 -68
- package/components/Carousel.vue +474 -0
- package/components/DrawImage.vue +172 -0
- package/components/EntryPassInformation.vue +283 -29
- package/components/{CheckoutItemMain.vue → EquipmentItemMain.vue} +95 -87
- package/components/{SupplyManagement.vue → EquipmentManagement.vue} +3 -3
- package/components/Feedback/Form.vue +4 -4
- package/components/FeedbackMain.vue +748 -145
- package/components/FileInput.vue +289 -0
- package/components/Input/DateTimePicker.vue +17 -11
- package/components/ManageChecklistMain.vue +379 -41
- package/components/StockCard.vue +11 -7
- package/components/TableHygiene.vue +42 -452
- package/components/UnitPersonCard.vue +74 -14
- package/components/VisitorForm.vue +193 -52
- package/components/VisitorFormSelection.vue +13 -2
- package/components/VisitorManagement.vue +83 -55
- package/composables/useAccessManagement.ts +41 -18
- package/composables/useCleaningPermission.ts +7 -7
- package/composables/useDashboardData.ts +2 -2
- package/composables/useEquipment.ts +63 -0
- package/composables/{useCheckout.ts → useEquipmentItem.ts} +7 -7
- package/composables/{useCheckoutPermission.ts → useEquipmentItemPermission.ts} +13 -13
- package/composables/{useSupply.ts → useEquipmentManagement.ts} +1 -1
- package/composables/useEquipmentManagementPermission.ts +96 -0
- package/composables/{useSupplyPermission.ts → useEquipmentPermission.ts} +9 -9
- package/composables/useFeedback.ts +53 -21
- package/composables/useLocalAuth.ts +29 -1
- package/composables/useUploadFiles.ts +94 -0
- package/composables/useUtils.ts +152 -53
- package/composables/useVehicle.ts +21 -2
- package/composables/useVisitor.ts +9 -7
- package/composables/useWorkOrder.ts +25 -3
- package/package.json +2 -1
- package/types/building.d.ts +1 -1
- package/types/{checkout-item.d.ts → equipment-item.d.ts} +3 -3
- package/types/{supply.d.ts → equipment.d.ts} +2 -2
- package/types/feedback.d.ts +5 -2
- package/types/people.d.ts +3 -1
- package/types/user.d.ts +1 -0
- package/types/vehicle.d.ts +2 -0
- package/types/visitor.d.ts +2 -1
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-carousel
|
|
3
|
+
eager
|
|
4
|
+
:show-arrows="mediaItems && mediaItems.length > 1"
|
|
5
|
+
v-if="!isConverting"
|
|
6
|
+
:height="height || 400"
|
|
7
|
+
>
|
|
8
|
+
<template v-for="(item, idx) in mediaItems" :key="idx">
|
|
9
|
+
<!-- IMAGE -->
|
|
10
|
+
<v-carousel-item v-if="item.type === 'image'">
|
|
11
|
+
<v-img
|
|
12
|
+
:src="item.url"
|
|
13
|
+
class="cursor-pointer"
|
|
14
|
+
cover
|
|
15
|
+
height="100%"
|
|
16
|
+
@click.stop="openMediaDialog(item.url)"
|
|
17
|
+
>
|
|
18
|
+
<template #placeholder>
|
|
19
|
+
<v-skeleton-loader height="100%" width="100%" />
|
|
20
|
+
</template>
|
|
21
|
+
</v-img>
|
|
22
|
+
|
|
23
|
+
<div class="edit-btn mb-5" v-if="imgEditable">
|
|
24
|
+
<v-btn
|
|
25
|
+
color="primary"
|
|
26
|
+
height="42px"
|
|
27
|
+
width="100px"
|
|
28
|
+
@click.stop="onMediaEdit(item.url, idx)"
|
|
29
|
+
>
|
|
30
|
+
Edit
|
|
31
|
+
</v-btn>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="delete-btn mb-3" v-if="imgDelete">
|
|
35
|
+
<v-btn
|
|
36
|
+
color="red"
|
|
37
|
+
height="42px"
|
|
38
|
+
width="100px"
|
|
39
|
+
@click.stop="onMediaDelete(item.url)"
|
|
40
|
+
>
|
|
41
|
+
Delete
|
|
42
|
+
</v-btn>
|
|
43
|
+
</div>
|
|
44
|
+
</v-carousel-item>
|
|
45
|
+
|
|
46
|
+
<!-- VIDEO -->
|
|
47
|
+
<v-carousel-item v-else-if="item.type === 'video'">
|
|
48
|
+
<div
|
|
49
|
+
class="media-fill cursor-pointer"
|
|
50
|
+
@click.stop="openMediaDialog(item.url)"
|
|
51
|
+
>
|
|
52
|
+
<video class="media-video" muted playsinline preload="metadata">
|
|
53
|
+
<source :src="item.url" />
|
|
54
|
+
</video>
|
|
55
|
+
<div class="play-overlay">
|
|
56
|
+
<v-btn class="play-btn" icon size="55" color="primary">
|
|
57
|
+
<v-icon icon="mdi-play" size="40" />
|
|
58
|
+
</v-btn>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="delete-btn pb-4" v-if="imgDelete">
|
|
62
|
+
<v-btn
|
|
63
|
+
color="red"
|
|
64
|
+
height="42px"
|
|
65
|
+
width="100px"
|
|
66
|
+
@click.stop="onMediaDelete(item.url)"
|
|
67
|
+
>
|
|
68
|
+
Delete
|
|
69
|
+
</v-btn>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</v-carousel-item>
|
|
73
|
+
|
|
74
|
+
<!-- DOCUMENT -->
|
|
75
|
+
<v-carousel-item v-else>
|
|
76
|
+
<div
|
|
77
|
+
class="media-fill d-flex flex-column align-center justify-center cursor-pointer"
|
|
78
|
+
@click.stop="
|
|
79
|
+
downloadFile({ name: item.name, url: item.url, type: item.type })
|
|
80
|
+
"
|
|
81
|
+
>
|
|
82
|
+
<v-icon
|
|
83
|
+
:icon="getFileIcon(item.type)"
|
|
84
|
+
size="150"
|
|
85
|
+
:color="getFileIconColor(getFileIcon(item.type))"
|
|
86
|
+
/>
|
|
87
|
+
<div class="text-h6 text-center text-wrap mt-2">{{ item.name }}</div>
|
|
88
|
+
|
|
89
|
+
<div class="delete-btn pb-4" v-if="imgDelete">
|
|
90
|
+
<v-btn
|
|
91
|
+
color="red"
|
|
92
|
+
height="42px"
|
|
93
|
+
width="100px"
|
|
94
|
+
@click.stop="onMediaDelete(item.url)"
|
|
95
|
+
>
|
|
96
|
+
Delete
|
|
97
|
+
</v-btn>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</v-carousel-item>
|
|
101
|
+
</template>
|
|
102
|
+
</v-carousel>
|
|
103
|
+
|
|
104
|
+
<v-skeleton-loader height="400px" width="100%" v-else />
|
|
105
|
+
|
|
106
|
+
<!-- Dialog for fullscreen view -->
|
|
107
|
+
<v-dialog v-model="isDialogOpen" max-width="700">
|
|
108
|
+
<v-card>
|
|
109
|
+
<v-btn icon class="close-btn" @click="isDialogOpen = false">
|
|
110
|
+
<v-icon icon="mdi-close" size="40" />
|
|
111
|
+
</v-btn>
|
|
112
|
+
|
|
113
|
+
<!-- Image preview -->
|
|
114
|
+
<v-img
|
|
115
|
+
v-if="selectedMediaType === 'image'"
|
|
116
|
+
:src="selectedMediaUrl"
|
|
117
|
+
contain
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
<!-- Video preview with playback controls -->
|
|
121
|
+
<div
|
|
122
|
+
v-else-if="selectedMediaType === 'video'"
|
|
123
|
+
class="dialog-video-container"
|
|
124
|
+
>
|
|
125
|
+
<video autoplay controls class="full-size-video" @click.stop>
|
|
126
|
+
<source :src="selectedMediaUrl" />
|
|
127
|
+
</video>
|
|
128
|
+
</div>
|
|
129
|
+
</v-card>
|
|
130
|
+
</v-dialog>
|
|
131
|
+
|
|
132
|
+
<!-- Image editor component -->
|
|
133
|
+
<DrawImage
|
|
134
|
+
v-if="isShowImageEdit"
|
|
135
|
+
:is-show-dialog="isShowImageEdit"
|
|
136
|
+
:image-url="imageUrl"
|
|
137
|
+
:image-idx="imageIdx"
|
|
138
|
+
@on-submit="onImageSubmitEdit"
|
|
139
|
+
@on-close-dialog="isShowImageEdit = false"
|
|
140
|
+
/>
|
|
141
|
+
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
142
|
+
</template>
|
|
143
|
+
|
|
144
|
+
<script setup lang="ts">
|
|
145
|
+
const { fileToBase64 } = useUtils();
|
|
146
|
+
|
|
147
|
+
const emit = defineEmits([
|
|
148
|
+
"on-click-carousel",
|
|
149
|
+
"on-file-edit",
|
|
150
|
+
"on-file-delete",
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
const props = defineProps<{
|
|
154
|
+
height?: string | number;
|
|
155
|
+
urls?: string[];
|
|
156
|
+
clickable?: boolean;
|
|
157
|
+
imgEditable?: boolean;
|
|
158
|
+
dataFiles?: {
|
|
159
|
+
name: string;
|
|
160
|
+
data: File;
|
|
161
|
+
progress: number;
|
|
162
|
+
id?: string;
|
|
163
|
+
url?: string;
|
|
164
|
+
type?: string;
|
|
165
|
+
}[];
|
|
166
|
+
imgDelete?: boolean;
|
|
167
|
+
}>();
|
|
168
|
+
|
|
169
|
+
const isConverting = ref(false);
|
|
170
|
+
const isShowImageEdit = ref(false);
|
|
171
|
+
const imageUrl = ref();
|
|
172
|
+
const imageIdx = ref();
|
|
173
|
+
const videoRefs = ref([]);
|
|
174
|
+
|
|
175
|
+
// Media items with thumbnail property
|
|
176
|
+
const mediaItems = ref<
|
|
177
|
+
{
|
|
178
|
+
url: string;
|
|
179
|
+
name: string;
|
|
180
|
+
type: string;
|
|
181
|
+
mimeType?: string;
|
|
182
|
+
thumbnail?: string;
|
|
183
|
+
}[]
|
|
184
|
+
>([]);
|
|
185
|
+
|
|
186
|
+
// Dialog state
|
|
187
|
+
const isDialogOpen = ref(false);
|
|
188
|
+
const selectedMediaUrl = ref("");
|
|
189
|
+
const selectedMediaType = ref<string>("");
|
|
190
|
+
const selectedMediaMimeType = ref("");
|
|
191
|
+
|
|
192
|
+
function getFileType(file: any) {
|
|
193
|
+
return file.type.startsWith("video/")
|
|
194
|
+
? "video"
|
|
195
|
+
: file.type.startsWith("image/")
|
|
196
|
+
? "image"
|
|
197
|
+
: file.type;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getMimeType(file: any) {
|
|
201
|
+
return file.type || "application/octet-stream";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Edit functions
|
|
205
|
+
const onMediaEdit = (url: string, idx: number) => {
|
|
206
|
+
isShowImageEdit.value = true;
|
|
207
|
+
imageUrl.value = url;
|
|
208
|
+
imageIdx.value = idx;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const onMediaDelete = (url: string) => {
|
|
212
|
+
mediaItems.value = mediaItems.value.filter((file) => file.url !== url);
|
|
213
|
+
URL.revokeObjectURL(url);
|
|
214
|
+
emit("on-file-delete", url);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const onImageSubmitEdit = (url: string, idx: number) => {
|
|
218
|
+
if (mediaItems.value && mediaItems.value.length > 0) {
|
|
219
|
+
mediaItems.value[idx].url = url;
|
|
220
|
+
}
|
|
221
|
+
isShowImageEdit.value = false;
|
|
222
|
+
emit("on-file-edit", url, idx);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Open dialog for fullscreen view
|
|
226
|
+
const openMediaDialog = (url: string) => {
|
|
227
|
+
const mediaItem = mediaItems.value.find((item) => item.url === url);
|
|
228
|
+
if (mediaItem) {
|
|
229
|
+
selectedMediaUrl.value = url;
|
|
230
|
+
selectedMediaType.value = mediaItem.type;
|
|
231
|
+
selectedMediaMimeType.value = mediaItem.mimeType || "";
|
|
232
|
+
isDialogOpen.value = true;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
async function getMimeFromHead(url: string) {
|
|
237
|
+
const res = await fetch(url);
|
|
238
|
+
if (!res.ok) throw new Error(`HEAD failed: ${res.status}`);
|
|
239
|
+
return res.headers.get("content-type");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
watchEffect(async (onCleanup) => {
|
|
243
|
+
let isCancelled = false;
|
|
244
|
+
|
|
245
|
+
onCleanup(() => {
|
|
246
|
+
isCancelled = true;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Local temporary storage
|
|
250
|
+
const newMediaItems: any = [];
|
|
251
|
+
|
|
252
|
+
if (props.dataFiles && props.dataFiles.length > 0) {
|
|
253
|
+
isConverting.value = true;
|
|
254
|
+
|
|
255
|
+
for (const file of props.dataFiles) {
|
|
256
|
+
try {
|
|
257
|
+
let fileType;
|
|
258
|
+
let mimeType;
|
|
259
|
+
let finalUrl;
|
|
260
|
+
|
|
261
|
+
// 1. Process the file data
|
|
262
|
+
if ("type" in file && "size" in file) {
|
|
263
|
+
mimeType = getMimeType(file);
|
|
264
|
+
fileType = getFileType(file);
|
|
265
|
+
|
|
266
|
+
if (fileType === "video") {
|
|
267
|
+
finalUrl = URL.createObjectURL(file);
|
|
268
|
+
} else {
|
|
269
|
+
// Awaiting async conversion
|
|
270
|
+
finalUrl = await fileToBase64(file);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
finalUrl = file.url;
|
|
274
|
+
fileType = getFileType(file);
|
|
275
|
+
mimeType = getMimeType(file);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 2. Check if the watcher was reset during the 'await'
|
|
279
|
+
if (isCancelled) return;
|
|
280
|
+
|
|
281
|
+
// 3. Push to LOCAL array, not the reactive .value
|
|
282
|
+
newMediaItems.push({
|
|
283
|
+
url: finalUrl,
|
|
284
|
+
name: file?.name,
|
|
285
|
+
type: fileType,
|
|
286
|
+
mimeType: mimeType,
|
|
287
|
+
thumbnail: undefined,
|
|
288
|
+
});
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error("Error processing file:", error);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} else if (props.urls && props.urls.length > 0) {
|
|
294
|
+
isConverting.value = true;
|
|
295
|
+
|
|
296
|
+
for (const url of props.urls) {
|
|
297
|
+
try {
|
|
298
|
+
const mimeType = await getMimeFromHead(url);
|
|
299
|
+
if (isCancelled) return;
|
|
300
|
+
|
|
301
|
+
newMediaItems.push({
|
|
302
|
+
url,
|
|
303
|
+
name: "",
|
|
304
|
+
type: mimeType
|
|
305
|
+
? mimeType.startsWith("image/")
|
|
306
|
+
? "image"
|
|
307
|
+
: mimeType.startsWith("video/")
|
|
308
|
+
? "video"
|
|
309
|
+
: mimeType
|
|
310
|
+
: mimeType,
|
|
311
|
+
});
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error("Error fetching MIME type:", error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 4. Finalizing state
|
|
319
|
+
if (!isCancelled) {
|
|
320
|
+
isConverting.value = false;
|
|
321
|
+
// Replace the entire array at once to avoid "doubling" or partial updates
|
|
322
|
+
mediaItems.value = newMediaItems;
|
|
323
|
+
console.log("watchEffect mediaItems updated:", mediaItems.value);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const getFileIcon = (type: string) => {
|
|
328
|
+
const iconMap: any = {
|
|
329
|
+
"application/pdf": "mdi-file-pdf-box",
|
|
330
|
+
"application/msword": "mdi-file-word-box",
|
|
331
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
332
|
+
"mdi-file-word-box",
|
|
333
|
+
"application/vnd.ms-excel": "mdi-file-excel-box",
|
|
334
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
|
|
335
|
+
"mdi-file-excel-box",
|
|
336
|
+
"application/vnd.ms-powerpoint": "mdi-file-powerpoint-box",
|
|
337
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
|
338
|
+
"mdi-file-powerpoint-box",
|
|
339
|
+
"text/plain": "mdi-file-document-outline",
|
|
340
|
+
"text/csv": "mdi-file-delimited-outline",
|
|
341
|
+
"application/zip": "mdi-zip-box-outline",
|
|
342
|
+
"application/x-rar-compressed": "mdi-zip-box-outline",
|
|
343
|
+
// Add more mappings as needed
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
return iconMap[type] || "mdi-file-outline";
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
function getFileIconColor(icon?: string) {
|
|
350
|
+
if (icon === "mdi-file-pdf-box") return "#F44336"; // Red
|
|
351
|
+
if (icon === "mdi-file-word-box") return "#2196F3"; // Blue
|
|
352
|
+
if (icon === "mdi-file-excel-box" || icon === "mdi-file-delimited-outline")
|
|
353
|
+
return "#4CAF50"; // Green
|
|
354
|
+
if (icon === "mdi-file-powerpoint-box") return "#FF9800"; // Orange
|
|
355
|
+
if (icon === "mdi-zip-box-outline") return "#9C27B0"; // Purple
|
|
356
|
+
return "grey";
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const message = ref("");
|
|
360
|
+
const messageColor = ref("");
|
|
361
|
+
const messageSnackbar = ref(false);
|
|
362
|
+
|
|
363
|
+
function showMessage(msg: string, color: string) {
|
|
364
|
+
message.value = msg;
|
|
365
|
+
messageColor.value = color;
|
|
366
|
+
messageSnackbar.value = true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function downloadFile({
|
|
370
|
+
name,
|
|
371
|
+
url,
|
|
372
|
+
type,
|
|
373
|
+
}: {
|
|
374
|
+
name: string;
|
|
375
|
+
url: string;
|
|
376
|
+
type: string;
|
|
377
|
+
}) {
|
|
378
|
+
try {
|
|
379
|
+
const link = document.createElement("a");
|
|
380
|
+
link.href = url;
|
|
381
|
+
if (type == "application/pdf") {
|
|
382
|
+
link.target = "_blank";
|
|
383
|
+
}
|
|
384
|
+
link.rel = "noopener noreferrer";
|
|
385
|
+
link.download = name;
|
|
386
|
+
|
|
387
|
+
document.body.appendChild(link);
|
|
388
|
+
link.click();
|
|
389
|
+
document.body.removeChild(link);
|
|
390
|
+
|
|
391
|
+
URL.revokeObjectURL(url);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
showMessage(err as string, "error");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// // Clean up object URLs when component is unmounted
|
|
398
|
+
// onBeforeUnmount(() => {
|
|
399
|
+
// // Revoke any object URLs to prevent memory leaks
|
|
400
|
+
// mediaItems.value.forEach((item) => {
|
|
401
|
+
// if (item.type === "video" && item.url && item.url.startsWith("blob:")) {
|
|
402
|
+
// URL.revokeObjectURL(item.url);
|
|
403
|
+
// }
|
|
404
|
+
// });
|
|
405
|
+
// });
|
|
406
|
+
</script>
|
|
407
|
+
|
|
408
|
+
<style scoped>
|
|
409
|
+
.edit-btn {
|
|
410
|
+
position: absolute;
|
|
411
|
+
bottom: 24%;
|
|
412
|
+
right: 5%;
|
|
413
|
+
z-index: 5;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.delete-btn {
|
|
417
|
+
position: absolute;
|
|
418
|
+
bottom: 13%;
|
|
419
|
+
right: 5%;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.close-btn {
|
|
423
|
+
position: absolute;
|
|
424
|
+
top: 10px;
|
|
425
|
+
right: 10px;
|
|
426
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
427
|
+
color: white;
|
|
428
|
+
z-index: 10;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.play-overlay {
|
|
432
|
+
position: absolute;
|
|
433
|
+
inset: 0;
|
|
434
|
+
display: flex;
|
|
435
|
+
align-items: center;
|
|
436
|
+
justify-content: center;
|
|
437
|
+
pointer-events: none;
|
|
438
|
+
}
|
|
439
|
+
.play-overlay .v-btn {
|
|
440
|
+
pointer-events: auto;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.play-btn {
|
|
444
|
+
border-radius: 9999px;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.dialog-video-container {
|
|
448
|
+
display: flex;
|
|
449
|
+
justify-content: center;
|
|
450
|
+
align-items: center;
|
|
451
|
+
width: 100%;
|
|
452
|
+
padding: 20px;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.full-size-video {
|
|
456
|
+
max-width: 100%;
|
|
457
|
+
max-height: 80vh;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.media-fill {
|
|
461
|
+
position: relative;
|
|
462
|
+
width: 100%;
|
|
463
|
+
height: 100%;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.media-video {
|
|
467
|
+
position: absolute;
|
|
468
|
+
inset: 0;
|
|
469
|
+
width: 100%;
|
|
470
|
+
height: 100%;
|
|
471
|
+
object-fit: cover;
|
|
472
|
+
display: block;
|
|
473
|
+
}
|
|
474
|
+
</style>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog
|
|
3
|
+
v-model="isDialogVisible"
|
|
4
|
+
transition="dialog-top-transition"
|
|
5
|
+
max-width="1000"
|
|
6
|
+
>
|
|
7
|
+
<v-card class="pa-1">
|
|
8
|
+
<v-toolbar>
|
|
9
|
+
<v-spacer></v-spacer>
|
|
10
|
+
<v-btn icon="mdi-close" @click="emit('onCloseDialog')" />
|
|
11
|
+
</v-toolbar>
|
|
12
|
+
<canvas id="imageCanvas" />
|
|
13
|
+
<v-card-title class="text-end">
|
|
14
|
+
<v-btn
|
|
15
|
+
color="orange-darken-3"
|
|
16
|
+
size="small"
|
|
17
|
+
variant="flat"
|
|
18
|
+
style="height: 40px; margin-right: 12px"
|
|
19
|
+
@click="onClearDrawing()"
|
|
20
|
+
>
|
|
21
|
+
Clear Drawing
|
|
22
|
+
</v-btn>
|
|
23
|
+
<v-btn
|
|
24
|
+
color="blue-darken-3"
|
|
25
|
+
size="small"
|
|
26
|
+
variant="flat"
|
|
27
|
+
style="height: 40px"
|
|
28
|
+
@click="submit"
|
|
29
|
+
>
|
|
30
|
+
Submit
|
|
31
|
+
</v-btn>
|
|
32
|
+
</v-card-title>
|
|
33
|
+
</v-card>
|
|
34
|
+
</v-dialog>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup lang="ts">
|
|
38
|
+
const { getImage } = useUtils();
|
|
39
|
+
|
|
40
|
+
const props = defineProps({
|
|
41
|
+
message: {
|
|
42
|
+
type: String,
|
|
43
|
+
},
|
|
44
|
+
isShowDialog: {
|
|
45
|
+
type: Boolean,
|
|
46
|
+
default: false,
|
|
47
|
+
},
|
|
48
|
+
imageUrl: {
|
|
49
|
+
type: String,
|
|
50
|
+
},
|
|
51
|
+
imageIdx: {
|
|
52
|
+
type: Number,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const emit = defineEmits<{
|
|
56
|
+
(event: "onSubmit", value: string, idx: number): void;
|
|
57
|
+
(event: "onCloseDialog"): void;
|
|
58
|
+
}>();
|
|
59
|
+
|
|
60
|
+
const isDialogVisible = computed(() => props.isShowDialog);
|
|
61
|
+
|
|
62
|
+
var canvas: any = document.getElementById("imageCanvas");
|
|
63
|
+
var ctx: any = "";
|
|
64
|
+
var img: any = "";
|
|
65
|
+
var pos: any = { x: 0, y: 0 };
|
|
66
|
+
|
|
67
|
+
onMounted(async () => {
|
|
68
|
+
canvas = document.getElementById("imageCanvas");
|
|
69
|
+
ctx = canvas?.getContext("2d");
|
|
70
|
+
|
|
71
|
+
var rect = canvas.getBoundingClientRect();
|
|
72
|
+
canvas.width = rect.width < 712 ? 712 : rect.width;
|
|
73
|
+
canvas.height = rect.height < 400 ? 400 : rect.height;
|
|
74
|
+
|
|
75
|
+
const response = await getImage(props.imageUrl!);
|
|
76
|
+
if (!response) return;
|
|
77
|
+
handleImage(response);
|
|
78
|
+
|
|
79
|
+
canvas.addEventListener("mousedown", setPosition);
|
|
80
|
+
canvas.addEventListener("mousemove", draw);
|
|
81
|
+
canvas.addEventListener(
|
|
82
|
+
"touchstart",
|
|
83
|
+
function (e: any) {
|
|
84
|
+
var touch = e.touches[0];
|
|
85
|
+
var mouseEvent = new MouseEvent("mousedown", {
|
|
86
|
+
buttons: 1,
|
|
87
|
+
clientX: touch.clientX,
|
|
88
|
+
clientY: touch.clientY,
|
|
89
|
+
});
|
|
90
|
+
canvas.dispatchEvent(mouseEvent);
|
|
91
|
+
},
|
|
92
|
+
false,
|
|
93
|
+
);
|
|
94
|
+
canvas.addEventListener(
|
|
95
|
+
"touchmove",
|
|
96
|
+
function (e: any) {
|
|
97
|
+
var touch = e.touches[0];
|
|
98
|
+
var mouseEvent = new MouseEvent("mousemove", {
|
|
99
|
+
buttons: 1,
|
|
100
|
+
clientX: touch.clientX,
|
|
101
|
+
clientY: touch.clientY,
|
|
102
|
+
});
|
|
103
|
+
canvas.dispatchEvent(mouseEvent);
|
|
104
|
+
},
|
|
105
|
+
false,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
var imageCanvas: any = document.getElementById("imageCanvas");
|
|
109
|
+
imageCanvas.style.cursor = "crosshair";
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
function handleImage(image: Blob) {
|
|
113
|
+
var reader = new FileReader();
|
|
114
|
+
reader.onload = function (event: any) {
|
|
115
|
+
img = new Image();
|
|
116
|
+
img.onload = function () {
|
|
117
|
+
var ratio = this.height / this.width;
|
|
118
|
+
canvas.height = canvas.width * ratio;
|
|
119
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
120
|
+
};
|
|
121
|
+
img.src = event.target.result;
|
|
122
|
+
};
|
|
123
|
+
reader.readAsDataURL(image);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function setPosition(e: any) {
|
|
127
|
+
var rect = canvas.getBoundingClientRect();
|
|
128
|
+
|
|
129
|
+
if (rect.width < 712 && rect.height < 400) {
|
|
130
|
+
var scaleX = 712 / rect.width;
|
|
131
|
+
var scaleY = 400 / rect.height;
|
|
132
|
+
|
|
133
|
+
pos.x = (e.clientX - rect.left) * scaleX;
|
|
134
|
+
pos.y = (e.clientY - rect.top) * scaleY;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pos.x = e.clientX - rect.left;
|
|
139
|
+
pos.y = e.clientY - rect.top;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function draw(e: any) {
|
|
143
|
+
canvas.style.cursor = "crosshair";
|
|
144
|
+
|
|
145
|
+
if (e.buttons !== 1) return;
|
|
146
|
+
if (pos.x === 0 && pos.y === 0) setPosition(e);
|
|
147
|
+
|
|
148
|
+
ctx.beginPath();
|
|
149
|
+
ctx.lineWidth = 5;
|
|
150
|
+
ctx.lineCap = "round";
|
|
151
|
+
ctx.strokeStyle = "#FF0000";
|
|
152
|
+
ctx.moveTo(pos.x, pos.y);
|
|
153
|
+
setPosition(e);
|
|
154
|
+
ctx.lineTo(pos.x, pos.y);
|
|
155
|
+
ctx.stroke();
|
|
156
|
+
ctx.closePath();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const submit = () => {
|
|
160
|
+
canvas.toBlob((blob: any) => {
|
|
161
|
+
const url = URL.createObjectURL(blob);
|
|
162
|
+
console.log("URL", url);
|
|
163
|
+
|
|
164
|
+
emit("onSubmit", url, props.imageIdx!);
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
async function onClearDrawing() {
|
|
169
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
170
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
171
|
+
}
|
|
172
|
+
</script>
|