@iservice365/layer-common 1.1.0 → 1.2.0
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 +6 -0
- package/components/CameraForm.vue +264 -0
- package/components/CameraMain.vue +352 -0
- package/components/Card/DeleteConfirmation.vue +51 -0
- package/components/Card/MemberInfoSummary.vue +44 -0
- package/components/Chat/Information.vue +28 -11
- package/components/Dialog/DeleteConfirmation.vue +51 -0
- package/components/Dialog/UpdateMoreAction.vue +99 -0
- package/components/Feedback/Form.vue +17 -3
- package/components/FeedbackDetail.vue +0 -11
- package/components/FeedbackMain.vue +21 -11
- package/components/Input/DateTimePicker.vue +5 -1
- package/components/Input/File.vue +1 -1
- package/components/Input/FileV2.vue +111 -63
- package/components/Input/InputPhoneNumberV2.vue +115 -0
- package/components/Input/NRICNumber.vue +41 -0
- package/components/Input/PhoneNumber.vue +1 -0
- package/components/Input/VehicleNumber.vue +41 -0
- package/components/NumberSettingField.vue +107 -0
- package/components/PeopleForm.vue +420 -0
- package/components/TableMain.vue +2 -1
- package/components/VehicleUpdateMoreAction.vue +84 -0
- package/components/VisitorForm.vue +712 -0
- package/components/VisitorFormSelection.vue +53 -0
- package/components/VisitorManagement.vue +568 -0
- package/components/WorkOrder/Create.vue +70 -46
- package/components/WorkOrder/Main.vue +11 -10
- package/composables/useBuilding.ts +250 -0
- package/composables/useBuildingUnit.ts +116 -0
- package/composables/useFeedback.ts +3 -3
- package/composables/useFile.ts +7 -9
- package/composables/useLocal.ts +67 -0
- package/composables/usePeople.ts +48 -0
- package/composables/useSecurityUtils.ts +18 -0
- package/composables/useSiteSettings.ts +111 -0
- package/composables/useUtils.ts +30 -1
- package/composables/useVisitor.ts +79 -0
- package/package.json +1 -1
- package/plugins/vuetify.ts +6 -1
- package/types/building.d.ts +19 -0
- package/types/camera.d.ts +31 -0
- package/types/people.d.ts +22 -0
- package/types/select.d.ts +4 -0
- package/types/site.d.ts +10 -7
- package/types/visitor.d.ts +42 -0
- package/utils/phoneMasks.ts +1703 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card flat border="sm grey-lighten-2" color="grey-lighten-3" class="w-100">
|
|
3
|
+
<v-card-text>
|
|
4
|
+
<v-row no-gutters>
|
|
5
|
+
<v-col cols="10" no-gutters>
|
|
6
|
+
<v-row no-gutters class="ga-1">
|
|
7
|
+
<template v-for="(label, key) in displayFields" :key="key">
|
|
8
|
+
<v-col v-if="member[key]" cols="12">
|
|
9
|
+
<span class="d-flex ga-3 align-center"><strong>{{ label }}:</strong> {{ member[key]
|
|
10
|
+
}}</span>
|
|
11
|
+
</v-col>
|
|
12
|
+
</template>
|
|
13
|
+
</v-row>
|
|
14
|
+
</v-col>
|
|
15
|
+
<v-col cols="2" class="d-flex align-center justify-center">
|
|
16
|
+
<v-btn icon="mdi-trash-outline" size="small" color="red" @click="emit('remove')" />
|
|
17
|
+
</v-col>
|
|
18
|
+
</v-row>
|
|
19
|
+
</v-card-text>
|
|
20
|
+
</v-card>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
|
|
25
|
+
const prop = defineProps({
|
|
26
|
+
member: {
|
|
27
|
+
type: Object as PropType<TMemberInfo>,
|
|
28
|
+
required: true
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits(['remove'])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
const displayFields: Record<keyof TMemberInfo, string> = {
|
|
36
|
+
name: "Name",
|
|
37
|
+
nric: "NRIC",
|
|
38
|
+
visitorPass: "Pass",
|
|
39
|
+
contact: "Contact"
|
|
40
|
+
} as const
|
|
41
|
+
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<style scoped></style>
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
<v-row dense class="my-1 pr-4">
|
|
63
63
|
<v-col cols="6" class="py-1"><strong>Category:</strong></v-col>
|
|
64
64
|
<v-col cols="6" class="py-1 text-right text-capitalize">{{
|
|
65
|
-
item.
|
|
65
|
+
item.category
|
|
66
66
|
}}</v-col>
|
|
67
67
|
</v-row>
|
|
68
68
|
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
|
|
187
187
|
<WorkOrderCreate
|
|
188
188
|
v-model="showCreateDialog"
|
|
189
|
-
|
|
189
|
+
created-from="feedback"
|
|
190
190
|
:work-order="_workOrder"
|
|
191
191
|
@update:work-order="(val: TWorkOrderCreate) => (_workOrder = val)"
|
|
192
192
|
:is-edit-mode="isEditMode"
|
|
@@ -269,20 +269,37 @@ const _workOrder = ref<TWorkOrderCreate>({
|
|
|
269
269
|
const serviceProviders = ref<
|
|
270
270
|
Array<{ title: string; value: string; subtitle: string }>
|
|
271
271
|
>([]);
|
|
272
|
-
const { getAll: getAllServiceProvider } = useServiceProvider();
|
|
273
|
-
|
|
274
|
-
const { data: getAllReq } = useLazyAsyncData("get-all-service-providers", () =>
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
// const { getAll: getAllServiceProvider } = useServiceProvider();
|
|
273
|
+
|
|
274
|
+
// const { data: getAllReq } = useLazyAsyncData("get-all-service-providers", () =>
|
|
275
|
+
// getAllServiceProvider({
|
|
276
|
+
// siteId: useRoute().params.site as string,
|
|
277
|
+
// })
|
|
278
|
+
// );
|
|
279
|
+
|
|
280
|
+
// watchEffect(() => {
|
|
281
|
+
// if (getAllReq.value) {
|
|
282
|
+
// serviceProviders.value = getAllReq.value.items.map((i: any) => ({
|
|
283
|
+
// title: i.nature.replace(/_/g, " "),
|
|
284
|
+
// value: i.serviceProviderOrgId,
|
|
285
|
+
// subtitle: i.name,
|
|
286
|
+
// }));
|
|
287
|
+
// }
|
|
288
|
+
// });
|
|
289
|
+
|
|
290
|
+
const { getBySiteAsServiceProvider } = useCustomerSite();
|
|
291
|
+
|
|
292
|
+
const { data: getAllReq } = useLazyAsyncData(
|
|
293
|
+
"get-by-site-as-service-provider",
|
|
294
|
+
() => getBySiteAsServiceProvider(useRoute().params.site as string)
|
|
278
295
|
);
|
|
279
296
|
|
|
280
297
|
watchEffect(() => {
|
|
281
298
|
if (getAllReq.value) {
|
|
282
|
-
serviceProviders.value = getAllReq.value.
|
|
299
|
+
serviceProviders.value = getAllReq.value.map((i: any) => ({
|
|
283
300
|
title: i.nature.replace(/_/g, " "),
|
|
284
|
-
|
|
285
|
-
|
|
301
|
+
subtitle: i.title,
|
|
302
|
+
value: i._id.org,
|
|
286
303
|
}));
|
|
287
304
|
}
|
|
288
305
|
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card width="100%" :disabled="loading" :loading="loading">
|
|
3
|
+
<v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-5 px-7 text-center">
|
|
4
|
+
<span> {{ promptTitle }}</span>
|
|
5
|
+
|
|
6
|
+
<span v-if="message" class="text-error mt-2">
|
|
7
|
+
{{ message }} Do you want to delete anyway?
|
|
8
|
+
</span>
|
|
9
|
+
</v-card-text>
|
|
10
|
+
|
|
11
|
+
<v-toolbar class="pa-0" density="compact">
|
|
12
|
+
<v-row no-gutters>
|
|
13
|
+
<v-col cols="6" class="pa-0">
|
|
14
|
+
<v-btn block variant="text" class="text-none" size="large" tile @click="emit('close')"
|
|
15
|
+
height="48">
|
|
16
|
+
Cancel
|
|
17
|
+
</v-btn>
|
|
18
|
+
</v-col>
|
|
19
|
+
|
|
20
|
+
<v-col cols="6" class="pa-0">
|
|
21
|
+
<v-btn block tile variant="flat" class="text-none" size="large" height="48" color="black"
|
|
22
|
+
@click="emit('delete')">
|
|
23
|
+
Delete
|
|
24
|
+
</v-btn>
|
|
25
|
+
</v-col>
|
|
26
|
+
</v-row>
|
|
27
|
+
</v-toolbar>
|
|
28
|
+
</v-card>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script setup lang="ts">
|
|
32
|
+
const props = defineProps({
|
|
33
|
+
message: {
|
|
34
|
+
type: String,
|
|
35
|
+
default: ""
|
|
36
|
+
},
|
|
37
|
+
promptTitle: {
|
|
38
|
+
type: String,
|
|
39
|
+
default: "Are you sure want to delete this? "
|
|
40
|
+
},
|
|
41
|
+
loading: {
|
|
42
|
+
type: Boolean,
|
|
43
|
+
default: false
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const emit = defineEmits(["close", "delete"])
|
|
48
|
+
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<style scoped></style>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card width="100%">
|
|
3
|
+
<v-toolbar>
|
|
4
|
+
<template v-if="$slots.header">
|
|
5
|
+
<slot name="header" :title="title" :onClose="onClose" />
|
|
6
|
+
</template>
|
|
7
|
+
<template v-else>
|
|
8
|
+
<v-row no-gutters class="fill-height px-6" align="center">
|
|
9
|
+
<span class="font-weight-bold text-h5 text-capitalize">{{
|
|
10
|
+
title
|
|
11
|
+
}}</span>
|
|
12
|
+
</v-row>
|
|
13
|
+
</template>
|
|
14
|
+
</v-toolbar>
|
|
15
|
+
|
|
16
|
+
<v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0">
|
|
17
|
+
<slot name="content" />
|
|
18
|
+
</v-card-text>
|
|
19
|
+
|
|
20
|
+
<v-toolbar class="pa-0" density="compact">
|
|
21
|
+
<v-row no-gutters>
|
|
22
|
+
<v-col cols="6" class="pa-0">
|
|
23
|
+
<v-btn
|
|
24
|
+
block
|
|
25
|
+
variant="text"
|
|
26
|
+
class="text-none"
|
|
27
|
+
size="large"
|
|
28
|
+
@click="emit('close')"
|
|
29
|
+
height="48"
|
|
30
|
+
>
|
|
31
|
+
Close
|
|
32
|
+
</v-btn>
|
|
33
|
+
</v-col>
|
|
34
|
+
|
|
35
|
+
<v-col cols="6" class="pa-0" >
|
|
36
|
+
<v-menu contained>
|
|
37
|
+
<template #activator="{ props }">
|
|
38
|
+
<v-btn
|
|
39
|
+
block
|
|
40
|
+
variant="flat"
|
|
41
|
+
color="black"
|
|
42
|
+
class="text-none"
|
|
43
|
+
height="48"
|
|
44
|
+
v-bind="props"
|
|
45
|
+
tile
|
|
46
|
+
>
|
|
47
|
+
More actions
|
|
48
|
+
</v-btn>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<v-list class="pa-0">
|
|
52
|
+
<v-list-item v-if="canUpdate" @click="emit('edit')">
|
|
53
|
+
<v-list-item-title class="text-subtitle-2">
|
|
54
|
+
{{ editButtonLabel }}
|
|
55
|
+
</v-list-item-title>
|
|
56
|
+
</v-list-item>
|
|
57
|
+
|
|
58
|
+
<v-list-item v-if="canDelete" @click="emit('delete')" class="text-red">
|
|
59
|
+
<v-list-item-title class="text-subtitle-2">
|
|
60
|
+
{{ deleteButtonLabel }}
|
|
61
|
+
</v-list-item-title>
|
|
62
|
+
</v-list-item>
|
|
63
|
+
</v-list>
|
|
64
|
+
</v-menu>
|
|
65
|
+
</v-col>
|
|
66
|
+
</v-row>
|
|
67
|
+
</v-toolbar>
|
|
68
|
+
</v-card>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script setup lang="ts">
|
|
72
|
+
const prop = defineProps({
|
|
73
|
+
canUpdate: {
|
|
74
|
+
type: Boolean,
|
|
75
|
+
default: true,
|
|
76
|
+
},
|
|
77
|
+
canDelete: {
|
|
78
|
+
type: Boolean,
|
|
79
|
+
default: true,
|
|
80
|
+
},
|
|
81
|
+
editButtonLabel: {
|
|
82
|
+
type: String,
|
|
83
|
+
default: "Edit",
|
|
84
|
+
},
|
|
85
|
+
deleteButtonLabel: {
|
|
86
|
+
type: String,
|
|
87
|
+
default: "Delete",
|
|
88
|
+
},
|
|
89
|
+
title: {
|
|
90
|
+
type: String,
|
|
91
|
+
default: "Details",
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const emit = defineEmits(["close", "edit", "delete"]);
|
|
96
|
+
const { canUpdate, editButtonLabel, deleteButtonLabel, title } = prop;
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<style scoped></style>
|
|
@@ -15,13 +15,18 @@
|
|
|
15
15
|
@errored="onImageError"
|
|
16
16
|
/>
|
|
17
17
|
|
|
18
|
-
<v-
|
|
18
|
+
<v-autocomplete
|
|
19
|
+
v-model="localFeedback.subject"
|
|
20
|
+
:items="subjects"
|
|
21
|
+
item-title="title"
|
|
22
|
+
item-value="value"
|
|
23
|
+
item-props
|
|
19
24
|
label="Subject"
|
|
20
25
|
variant="outlined"
|
|
21
26
|
density="compact"
|
|
22
|
-
v-model="localFeedback.subject"
|
|
23
27
|
class="mb-2"
|
|
24
|
-
|
|
28
|
+
clearable
|
|
29
|
+
:custom-filter="customFilter"
|
|
25
30
|
/>
|
|
26
31
|
|
|
27
32
|
<v-autocomplete
|
|
@@ -133,4 +138,13 @@ function closeDialog() {
|
|
|
133
138
|
function submitFeedback() {
|
|
134
139
|
emit("submit", { ...localFeedback.value });
|
|
135
140
|
}
|
|
141
|
+
|
|
142
|
+
const { subjects } = useLocal();
|
|
143
|
+
|
|
144
|
+
const customFilter = (value: string, query: string, item: any) => {
|
|
145
|
+
const title = item?.raw?.title?.toLowerCase() || "";
|
|
146
|
+
const subtitle = item?.raw?.subtitle?.toLowerCase() || "";
|
|
147
|
+
const search = query.toLowerCase();
|
|
148
|
+
return title.includes(search) || subtitle.includes(search);
|
|
149
|
+
};
|
|
136
150
|
</script>
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="feedback-detail-wrapper">
|
|
3
3
|
<v-row no-gutters class="fill-height">
|
|
4
|
-
<!-- <v-col cols="12" xl="3" lg="4" md="4" class="fill-height">
|
|
5
|
-
<div class="panel-container border-e">
|
|
6
|
-
<ChatNavigation
|
|
7
|
-
:title="'Feedbacks'"
|
|
8
|
-
:items="items"
|
|
9
|
-
@select="handleSelectFeedback"
|
|
10
|
-
@search="_getFeedbacks"
|
|
11
|
-
/>
|
|
12
|
-
</div>
|
|
13
|
-
</v-col> -->
|
|
14
4
|
|
|
15
5
|
<v-col cols="12" xl="7" lg="7" md="7" class="fill-height">
|
|
16
6
|
<div class="panel-container border-e">
|
|
@@ -179,7 +169,6 @@ const {
|
|
|
179
169
|
} = useLazyAsyncData("get-all-feedbacks", () =>
|
|
180
170
|
_getFeedbacks({
|
|
181
171
|
page: page.value,
|
|
182
|
-
organization: route.params.org as string,
|
|
183
172
|
site: route.params.site as string,
|
|
184
173
|
})
|
|
185
174
|
);
|
|
@@ -164,6 +164,10 @@ const props = defineProps({
|
|
|
164
164
|
type: Boolean,
|
|
165
165
|
default: false,
|
|
166
166
|
},
|
|
167
|
+
category: {
|
|
168
|
+
type: String,
|
|
169
|
+
default: "",
|
|
170
|
+
},
|
|
167
171
|
});
|
|
168
172
|
|
|
169
173
|
const isEditMode = ref(false);
|
|
@@ -179,20 +183,27 @@ const { getUserFromCookie } = useLocal();
|
|
|
179
183
|
const serviceProviders = ref<
|
|
180
184
|
Array<{ title: string; value: string; subtitle: string }>
|
|
181
185
|
>([]);
|
|
182
|
-
const { getAll: getAllServiceProvider } = useServiceProvider();
|
|
186
|
+
// const { getAll: getAllServiceProvider } = useServiceProvider();
|
|
183
187
|
|
|
184
|
-
const { data: getAllReq } = useLazyAsyncData("get-all-service-providers", () =>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
// const { data: getAllReq } = useLazyAsyncData("get-all-service-providers", () =>
|
|
189
|
+
// getAllServiceProvider({
|
|
190
|
+
// siteId: useRoute().params.site as string,
|
|
191
|
+
// })
|
|
192
|
+
// );
|
|
193
|
+
|
|
194
|
+
const { getBySiteAsServiceProvider } = useCustomerSite();
|
|
195
|
+
|
|
196
|
+
const { data: getAllReq } = useLazyAsyncData(
|
|
197
|
+
"get-by-site-as-service-provider",
|
|
198
|
+
() => getBySiteAsServiceProvider(useRoute().params.site as string)
|
|
188
199
|
);
|
|
189
200
|
|
|
190
201
|
watchEffect(() => {
|
|
191
202
|
if (getAllReq.value) {
|
|
192
|
-
serviceProviders.value = getAllReq.value.
|
|
203
|
+
serviceProviders.value = getAllReq.value.map((i: any) => ({
|
|
193
204
|
title: i.nature.replace(/_/g, " "),
|
|
194
|
-
|
|
195
|
-
|
|
205
|
+
subtitle: i.title,
|
|
206
|
+
value: i.nature,
|
|
196
207
|
}));
|
|
197
208
|
}
|
|
198
209
|
});
|
|
@@ -244,11 +255,11 @@ const {
|
|
|
244
255
|
data: getAllFeedbackReq,
|
|
245
256
|
status: getAllReqStatus,
|
|
246
257
|
refresh: getAllReqRefresh,
|
|
247
|
-
} = useLazyAsyncData("get-all-feedbacks", () =>
|
|
258
|
+
} = useLazyAsyncData("get-all-feedbacks-" + props.category, () =>
|
|
248
259
|
_getFeedbacks({
|
|
249
260
|
page: page.value,
|
|
250
|
-
organization: route.params.org as string,
|
|
251
261
|
site: route.params.site as string,
|
|
262
|
+
category: props.category,
|
|
252
263
|
})
|
|
253
264
|
);
|
|
254
265
|
|
|
@@ -268,7 +279,6 @@ async function updatePage(pageVal: any) {
|
|
|
268
279
|
page.value = pageVal;
|
|
269
280
|
const response = await _getFeedbacks({
|
|
270
281
|
page: page.value,
|
|
271
|
-
organization: route.params.org as string,
|
|
272
282
|
site: route.params.site as string,
|
|
273
283
|
});
|
|
274
284
|
if (response) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="d-flex flex-column">
|
|
3
3
|
<v-text-field v-bind="$attrs" ref="dateTimePickerRef" :model-value="dateTimeFormattedReadOnly"
|
|
4
|
-
placeholder="
|
|
4
|
+
:placeholder="placeholder" :rules="rules" style="z-index: 10" @click="openDatePicker">
|
|
5
5
|
<template #append-inner>
|
|
6
6
|
<v-icon icon="mdi-calendar" @click.stop="openDatePicker" />
|
|
7
7
|
</template>
|
|
@@ -20,6 +20,10 @@ const prop = defineProps({
|
|
|
20
20
|
type: Array as PropType<Array<any>>,
|
|
21
21
|
default: () => []
|
|
22
22
|
},
|
|
23
|
+
placeholder: {
|
|
24
|
+
type: String,
|
|
25
|
+
default: 'MM/DD/YYYY, HH:MM AM/PM'
|
|
26
|
+
}
|
|
23
27
|
})
|
|
24
28
|
|
|
25
29
|
const { formatDateISO8601 } = useUtils()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-row no-gutters class="w-100 pb-5" @click="resetErrorMessage">
|
|
3
3
|
<v-file-upload v-model="uploadFiles" density="compact" @update:model-value="handleUpdateValue"
|
|
4
|
-
:loading="processing" :disabled="processing" :height="height" title="
|
|
4
|
+
:loading="processing" :disabled="processing" :height="height" :title="title" :accept="accept"
|
|
5
5
|
name="upload_images" class="text-caption w-100" clearable :multiple="multiple">
|
|
6
6
|
<template v-slot:item="{ props: itemProps, file }">
|
|
7
7
|
<v-file-upload-item v-bind="itemProps" lines="one" nav>
|
|
@@ -22,93 +22,141 @@
|
|
|
22
22
|
</template>
|
|
23
23
|
|
|
24
24
|
<script setup lang="ts">
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
import { nextTick, ref, onMounted, watch } from 'vue'
|
|
26
|
+
|
|
27
|
+
const props = defineProps({
|
|
28
|
+
height: {
|
|
29
|
+
type: [Number, String],
|
|
30
|
+
default: 68,
|
|
31
|
+
},
|
|
32
|
+
multiple: {
|
|
33
|
+
type: Boolean,
|
|
34
|
+
default: false,
|
|
35
|
+
},
|
|
36
|
+
maxLength: {
|
|
37
|
+
type: Number,
|
|
38
|
+
default: 10,
|
|
39
|
+
},
|
|
40
|
+
title: {
|
|
41
|
+
type: String,
|
|
42
|
+
default: 'Upload Images'
|
|
43
|
+
},
|
|
44
|
+
accept: {
|
|
45
|
+
type: String,
|
|
46
|
+
default: "image/*"
|
|
47
|
+
}
|
|
41
48
|
})
|
|
42
49
|
|
|
43
50
|
const { addFile, deleteFile, getFileUrl, urlToFile } = useFile()
|
|
44
51
|
|
|
52
|
+
// The parent v-model binding
|
|
53
|
+
const idsArray = defineModel<string[]>({ default: [] })
|
|
54
|
+
|
|
45
55
|
const uploadFiles = ref<File[]>([])
|
|
46
|
-
const filesCollection =
|
|
47
|
-
const
|
|
48
|
-
const errorMessage = ref('');
|
|
56
|
+
const filesCollection = ref<{ file: File; id: string }[]>([])
|
|
57
|
+
const errorMessage = ref('')
|
|
49
58
|
const processing = ref(false)
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
function fileKey(f: File) {
|
|
61
|
+
return `${f.name}_${f.size}_${f.lastModified}`
|
|
62
|
+
}
|
|
55
63
|
|
|
56
64
|
async function handleRemove(removedFile: File) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const fileId = arr.find(item => fileKey(item.file) === fileKey(removedFile))?.id
|
|
65
|
+
const key = fileKey(removedFile)
|
|
66
|
+
const arr = [...filesCollection.value]
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
const removedItem = arr.find((item) => fileKey(item.file) === key)
|
|
69
|
+
if (!removedItem) return
|
|
70
|
+
|
|
71
|
+
filesCollection.value = arr.filter((item) => item.id !== removedItem.id)
|
|
72
|
+
uploadFiles.value = uploadFiles.value.filter((f) => fileKey(f) !== key)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
idsArray.value = filesCollection.value.map((x) => x.id)
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await deleteFile(removedItem.id)
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.warn('Failed to delete file', err)
|
|
81
|
+
}
|
|
63
82
|
}
|
|
64
83
|
|
|
65
84
|
async function handleUpdateValue(value: File[]) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if ((existingLength + value.length) > max) {
|
|
71
|
-
value = value.slice(0, (max - existingLength))
|
|
72
|
-
errorMessage.value = `Max value of allowed image is ${max}`
|
|
73
|
-
}
|
|
74
|
-
uploadFiles.value = []
|
|
75
|
-
processing.value = true
|
|
76
|
-
// Determine which files are newly added or removed
|
|
77
|
-
const fileKey = (f: File) => `${f.name}_${f.size}_${f.lastModified}`
|
|
85
|
+
await nextTick()
|
|
86
|
+
const max = props.maxLength
|
|
87
|
+
const existingLength = filesCollection.value.length
|
|
88
|
+
errorMessage.value = ''
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
if (existingLength + value.length > max) {
|
|
91
|
+
value = value.slice(0, max - existingLength)
|
|
92
|
+
errorMessage.value = `Max allowed images is ${max}`
|
|
93
|
+
}
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
const collectionKeys = filesCollection.value.map((x) => fileKey(x.file))
|
|
96
|
+
const addedFiles = value.filter((f) => !collectionKeys.includes(fileKey(f)))
|
|
97
|
+
|
|
98
|
+
processing.value = true
|
|
99
|
+
try {
|
|
85
100
|
for (const file of addedFiles) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
} catch (err) {
|
|
92
|
-
console.error("Upload failed", err)
|
|
93
|
-
errorMessage.value = `Failed to upload ${file.name}`
|
|
94
|
-
} finally {
|
|
95
|
-
processing.value = false
|
|
96
|
-
uploadFiles.value = filesCollection.value.map(x => x.file)
|
|
97
|
-
}
|
|
101
|
+
const res = await addFile(file) // should return { id, url }
|
|
102
|
+
if (res?.id) {
|
|
103
|
+
filesCollection.value.push({ file, id: res.id })
|
|
104
|
+
}
|
|
98
105
|
}
|
|
99
106
|
|
|
107
|
+
uploadFiles.value = filesCollection.value.map((x) => x.file)
|
|
108
|
+
idsArray.value = filesCollection.value.map((x) => x.id)
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error('Upload failed', err)
|
|
111
|
+
errorMessage.value = 'Failed to upload some files.'
|
|
112
|
+
} finally {
|
|
113
|
+
processing.value = false
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async function loadFilesFromIds(ids: string[]) {
|
|
119
|
+
const result: { file: File; id: string }[] = []
|
|
120
|
+
for (const id of ids) {
|
|
121
|
+
try {
|
|
122
|
+
const url = await getFileUrl(id)
|
|
123
|
+
const name = decodeURIComponent(url.split('/').pop() || `file_${id}`)
|
|
124
|
+
const file = await urlToFile(url, name)
|
|
125
|
+
result.push({ file, id })
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.warn('Failed to load file from ID:', id, err)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return result
|
|
100
131
|
}
|
|
101
132
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
133
|
+
|
|
134
|
+
function resetErrorMessage() {
|
|
135
|
+
errorMessage.value = ''
|
|
105
136
|
}
|
|
106
137
|
|
|
107
|
-
|
|
108
|
-
|
|
138
|
+
|
|
139
|
+
onMounted(async () => {
|
|
140
|
+
if (idsArray.value.length > 0) {
|
|
141
|
+
processing.value = true
|
|
142
|
+
const loaded = await loadFilesFromIds(idsArray.value)
|
|
143
|
+
filesCollection.value = loaded
|
|
144
|
+
uploadFiles.value = loaded.map((x) => x.file)
|
|
145
|
+
processing.value = false
|
|
146
|
+
}
|
|
109
147
|
})
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
watch(
|
|
151
|
+
filesCollection,
|
|
152
|
+
(newVal) => {
|
|
153
|
+
idsArray.value = newVal.map((x) => x.id)
|
|
154
|
+
},
|
|
155
|
+
{ deep: true }
|
|
156
|
+
)
|
|
110
157
|
</script>
|
|
111
158
|
|
|
159
|
+
|
|
112
160
|
<style scoped>
|
|
113
161
|
* :deep(.v-file-upload-title) {
|
|
114
162
|
font-size: 1rem;
|