pukaad-ui-lib 1.212.2 → 1.213.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/dist/module.json +1 -1
- package/dist/runtime/components/drawer/drawer-suggest-place/drawer-suggest-place.d.vue.ts +5 -4
- package/dist/runtime/components/drawer/drawer-suggest-place/drawer-suggest-place.vue +65 -3
- package/dist/runtime/components/drawer/drawer-suggest-place/drawer-suggest-place.vue.d.ts +5 -4
- package/dist/runtime/components/drawer/drawer-suggest-place/suggest-place-form.d.vue.ts +1 -0
- package/dist/runtime/components/drawer/drawer-suggest-place/suggest-place-form.vue +83 -7
- package/dist/runtime/components/drawer/drawer-suggest-place/suggest-place-form.vue.d.ts +1 -0
- package/dist/runtime/components/input/input-link.d.vue.ts +2 -0
- package/dist/runtime/components/input/input-link.vue +8 -2
- package/dist/runtime/components/input/input-link.vue.d.ts +2 -0
- package/dist/runtime/composables/usePresignedUpload.d.ts +9 -0
- package/dist/runtime/composables/usePresignedUpload.js +45 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -8,11 +8,12 @@ type __VLS_ModelProps = {
|
|
|
8
8
|
};
|
|
9
9
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
10
10
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
-
|
|
12
|
-
"update:
|
|
13
|
-
|
|
11
|
+
"update:modelValue": (value: boolean) => any;
|
|
12
|
+
"update:data": (value: SuggestPlaceData) => any;
|
|
13
|
+
} & {
|
|
14
|
+
submit: (data: SuggestPlaceData) => any;
|
|
14
15
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
15
|
-
onSubmit?: ((
|
|
16
|
+
onSubmit?: ((data: SuggestPlaceData) => any) | undefined;
|
|
16
17
|
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
17
18
|
"onUpdate:data"?: ((value: SuggestPlaceData) => any) | undefined;
|
|
18
19
|
}>, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<Drawer class="w-[888px]" :title="drawerTitle" v-model="isOpen">
|
|
2
|
+
<Drawer class="w-[888px]" :title="drawerTitle" :loading="loading" v-model="isOpen">
|
|
3
3
|
<!-- backoffice: แสดงแบบมี tabs -->
|
|
4
4
|
<ShadTabs v-if="props.state === 'backoffice'">
|
|
5
5
|
<ShadTabsList>
|
|
@@ -97,12 +97,19 @@
|
|
|
97
97
|
|
|
98
98
|
<script setup>
|
|
99
99
|
import { computed, ref } from "vue";
|
|
100
|
+
import { useNuxtApp } from "nuxt/app";
|
|
101
|
+
import { useApi } from "@/runtime/composables/useApi";
|
|
102
|
+
import { usePresignedUpload } from "@/runtime/composables/usePresignedUpload";
|
|
100
103
|
const props = defineProps({
|
|
101
104
|
state: { type: String, required: false, default: "personal" }
|
|
102
105
|
});
|
|
103
106
|
const emit = defineEmits(["submit"]);
|
|
107
|
+
const { $toast } = useNuxtApp();
|
|
108
|
+
const api = useApi();
|
|
109
|
+
const { uploadFiles } = usePresignedUpload();
|
|
104
110
|
const isOpen = defineModel({ type: Boolean, ...{ default: false } });
|
|
105
111
|
const formData = defineModel("data", { type: Object, ...{ default: () => ({}) } });
|
|
112
|
+
const loading = ref(false);
|
|
106
113
|
const photoColumns = ref(false);
|
|
107
114
|
const videoColumns = ref(false);
|
|
108
115
|
if (!formData.value.photos) formData.value.photos = [];
|
|
@@ -115,8 +122,63 @@ const isFormValid = computed(() => {
|
|
|
115
122
|
const hasCategories = !!(d.categories && d.categories.length > 0);
|
|
116
123
|
return addressComplete && hasLatLng && hasBusinessName && hasCategories;
|
|
117
124
|
});
|
|
118
|
-
const
|
|
119
|
-
|
|
125
|
+
const endpointMap = {
|
|
126
|
+
personal: "/personal/suggest-places",
|
|
127
|
+
business: "/business/suggest-places",
|
|
128
|
+
backoffice: "/office/suggest-places"
|
|
129
|
+
};
|
|
130
|
+
const onSubmit = async () => {
|
|
131
|
+
loading.value = true;
|
|
132
|
+
try {
|
|
133
|
+
const d = formData.value;
|
|
134
|
+
const [photoUrls, videoUrls, reviewPhotoUrls] = await Promise.all([
|
|
135
|
+
uploadFiles(d.photos ?? [], "suggest-place-media"),
|
|
136
|
+
uploadFiles(d.videos ?? [], "suggest-place-media"),
|
|
137
|
+
uploadFiles(d.reviewPhotos ?? [], "suggest-place-review")
|
|
138
|
+
]);
|
|
139
|
+
const categoryIds = (d.categories ?? []).map(
|
|
140
|
+
(c) => typeof c === "object" ? String(c.id ?? c.value) : String(c)
|
|
141
|
+
);
|
|
142
|
+
const endpoint = endpointMap[props.state] ?? "/personal/suggest-places";
|
|
143
|
+
const res = await api(endpoint, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
body: {
|
|
146
|
+
business_name: d.businessName ?? "",
|
|
147
|
+
name_th: d.nameTh ?? "",
|
|
148
|
+
name_en: d.nameEn ?? "",
|
|
149
|
+
description: d.description ?? "",
|
|
150
|
+
address: {
|
|
151
|
+
province_id: d.address?.province_id,
|
|
152
|
+
amphur_id: d.address?.amphur_id,
|
|
153
|
+
tambon_id: d.address?.tambon_id,
|
|
154
|
+
zipcode: d.address?.zipcode,
|
|
155
|
+
detail_address: d.address?.detail_address ?? "",
|
|
156
|
+
full_address: d.address?.full_address ?? ""
|
|
157
|
+
},
|
|
158
|
+
latLng: d.latLng ?? null,
|
|
159
|
+
phone: d.phone ?? "",
|
|
160
|
+
extra_phones: d.extraPhones ?? [],
|
|
161
|
+
contact_channels: d.contactChannels ?? [],
|
|
162
|
+
opening_hours: d.openingHours ?? null,
|
|
163
|
+
categories: categoryIds,
|
|
164
|
+
is_review: d.isReview ?? false,
|
|
165
|
+
rating: d.rating ?? 0,
|
|
166
|
+
review_description: d.reviewDescription ?? "",
|
|
167
|
+
review_photos: reviewPhotoUrls,
|
|
168
|
+
photos: photoUrls,
|
|
169
|
+
videos: videoUrls
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
$toast?.success?.(res?.message || "\u0E41\u0E19\u0E30\u0E19\u0E33\u0E2A\u0E16\u0E32\u0E19\u0E17\u0E35\u0E48\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08");
|
|
173
|
+
emit("submit", formData.value);
|
|
174
|
+
isOpen.value = false;
|
|
175
|
+
} catch (e) {
|
|
176
|
+
$toast?.error?.(
|
|
177
|
+
e?.data?.error || e?.data?.message || "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14\u0E43\u0E19\u0E01\u0E32\u0E23\u0E40\u0E0A\u0E37\u0E48\u0E2D\u0E21\u0E15\u0E48\u0E2D"
|
|
178
|
+
);
|
|
179
|
+
} finally {
|
|
180
|
+
loading.value = false;
|
|
181
|
+
}
|
|
120
182
|
};
|
|
121
183
|
const drawerTitle = computed(() => {
|
|
122
184
|
if (props.state === "personal" || props.state === "business") {
|
|
@@ -8,11 +8,12 @@ type __VLS_ModelProps = {
|
|
|
8
8
|
};
|
|
9
9
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
10
10
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
-
|
|
12
|
-
"update:
|
|
13
|
-
|
|
11
|
+
"update:modelValue": (value: boolean) => any;
|
|
12
|
+
"update:data": (value: SuggestPlaceData) => any;
|
|
13
|
+
} & {
|
|
14
|
+
submit: (data: SuggestPlaceData) => any;
|
|
14
15
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
15
|
-
onSubmit?: ((
|
|
16
|
+
onSubmit?: ((data: SuggestPlaceData) => any) | undefined;
|
|
16
17
|
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
17
18
|
"onUpdate:data"?: ((value: SuggestPlaceData) => any) | undefined;
|
|
18
19
|
}>, {
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
show-counter
|
|
16
16
|
:limit="180"
|
|
17
17
|
free-text
|
|
18
|
+
:fetch-fn="fetchApprovedPlaces"
|
|
19
|
+
value-key="business_name"
|
|
20
|
+
label-key="business_name"
|
|
18
21
|
/>
|
|
19
22
|
<InputTextField
|
|
20
23
|
v-if="extraNameCount >= 1"
|
|
@@ -52,7 +55,7 @@
|
|
|
52
55
|
placeholder="เพิ่มหมวดหมู่ที่เกี่ยวข้องกับสถานที่"
|
|
53
56
|
:limit="3"
|
|
54
57
|
show-counter
|
|
55
|
-
:
|
|
58
|
+
:options="categoryOptions"
|
|
56
59
|
required
|
|
57
60
|
multiple
|
|
58
61
|
/>
|
|
@@ -64,9 +67,17 @@
|
|
|
64
67
|
:limit="220"
|
|
65
68
|
show-counter
|
|
66
69
|
/>
|
|
67
|
-
<InputDateOpening
|
|
70
|
+
<InputDateOpening
|
|
71
|
+
name="openingHours"
|
|
72
|
+
v-model="modelValue.openingHours"
|
|
73
|
+
/>
|
|
68
74
|
<div class="flex flex-col gap-[8px]">
|
|
69
|
-
<InputPhone
|
|
75
|
+
<InputPhone
|
|
76
|
+
name="phone"
|
|
77
|
+
v-model="modelValue.phone"
|
|
78
|
+
label="เบอร์โทรศัพท์"
|
|
79
|
+
placeholder="กรอกเบอร์โทรศัพท์"
|
|
80
|
+
/>
|
|
70
81
|
<InputPhone
|
|
71
82
|
v-for="(item, index) in modelValue.extraPhones"
|
|
72
83
|
:key="index"
|
|
@@ -84,12 +95,26 @@
|
|
|
84
95
|
<Icon name="lucide:plus" />
|
|
85
96
|
เพิ่มเบอร์โทรศัพท์
|
|
86
97
|
</Button>
|
|
98
|
+
<InputLink
|
|
99
|
+
name="contactChannels"
|
|
100
|
+
format="contact_channel"
|
|
101
|
+
default-first
|
|
102
|
+
v-model="modelValue.contactChannels"
|
|
103
|
+
/>
|
|
87
104
|
</div>
|
|
88
105
|
<template v-if="props.state === 'personal'">
|
|
89
106
|
<div class="flex flex-col gap-[16px]">
|
|
90
|
-
<InputCheckbox
|
|
107
|
+
<InputCheckbox
|
|
108
|
+
name="isReview"
|
|
109
|
+
v-model="modelValue.isReview"
|
|
110
|
+
label="คุณต้องการรีวิวสถานที่นี้"
|
|
111
|
+
/>
|
|
91
112
|
<template v-if="modelValue.isReview">
|
|
92
|
-
<InputRating
|
|
113
|
+
<InputRating
|
|
114
|
+
name="rating"
|
|
115
|
+
v-model="modelValue.rating"
|
|
116
|
+
class="flex py-4 justify-center"
|
|
117
|
+
/>
|
|
93
118
|
<InputTextarea
|
|
94
119
|
name="reviewDescription"
|
|
95
120
|
v-model="modelValue.reviewDescription"
|
|
@@ -104,7 +129,12 @@
|
|
|
104
129
|
</div>
|
|
105
130
|
<div class="font-body-small text-gray">สูงสุด 9 รายการ</div>
|
|
106
131
|
</div>
|
|
107
|
-
<InputFile
|
|
132
|
+
<InputFile
|
|
133
|
+
name="reviewPhotos"
|
|
134
|
+
v-model="modelValue.reviewPhotos"
|
|
135
|
+
accept="image/*"
|
|
136
|
+
:limit="9"
|
|
137
|
+
/>
|
|
108
138
|
<div class="font-body-small text-gray w-[250px]">
|
|
109
139
|
รองรับไฟล์ *.jpg *.jpeg *.png *.webp *.bmp *.gif
|
|
110
140
|
ขนาดไฟล์ไม่เกิน 30 mb
|
|
@@ -138,11 +168,13 @@
|
|
|
138
168
|
</template>
|
|
139
169
|
|
|
140
170
|
<script setup>
|
|
141
|
-
import { ref, computed } from "vue";
|
|
171
|
+
import { ref, computed, onMounted } from "vue";
|
|
172
|
+
import { useApi } from "@/runtime/composables/useApi";
|
|
142
173
|
import SuggestPlaceMap from "./suggest-place-map.vue";
|
|
143
174
|
const props = defineProps({
|
|
144
175
|
state: { type: String, required: false, default: "personal" }
|
|
145
176
|
});
|
|
177
|
+
const api = useApi();
|
|
146
178
|
const modelValue = defineModel({ type: Object, ...{
|
|
147
179
|
default: () => ({})
|
|
148
180
|
} });
|
|
@@ -157,4 +189,48 @@ const isAddressCompleted = computed(() => {
|
|
|
157
189
|
const v = modelValue.value.address || {};
|
|
158
190
|
return !!(v.province_id && v.amphur_id && v.tambon_id && v.zipcode);
|
|
159
191
|
});
|
|
192
|
+
const listEndpointMap = {
|
|
193
|
+
personal: "/personal/suggest-places",
|
|
194
|
+
business: "/business/suggest-places",
|
|
195
|
+
backoffice: "/office/suggest-places"
|
|
196
|
+
};
|
|
197
|
+
const fetchApprovedPlaces = async (page, pageSize, search) => {
|
|
198
|
+
const base = listEndpointMap[props.state] ?? "/personal/suggest-places";
|
|
199
|
+
const res = await api(base, {
|
|
200
|
+
query: {
|
|
201
|
+
q: search ?? "",
|
|
202
|
+
status: "PENDING,APPROVED",
|
|
203
|
+
page,
|
|
204
|
+
page_size: pageSize
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
const data = (res.data ?? []).map((item) => ({
|
|
208
|
+
...item,
|
|
209
|
+
description: item.address?.full_address ?? ""
|
|
210
|
+
}));
|
|
211
|
+
return {
|
|
212
|
+
data,
|
|
213
|
+
meta: { total_pages: res.meta?.total_pages ?? 1 }
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
const categoryOptions = ref([]);
|
|
217
|
+
const categoryEndpointMap = {
|
|
218
|
+
personal: "/personal/suggest-places/categories",
|
|
219
|
+
business: "/business/suggest-places/categories",
|
|
220
|
+
backoffice: "/office/suggest-places/categories"
|
|
221
|
+
};
|
|
222
|
+
onMounted(async () => {
|
|
223
|
+
try {
|
|
224
|
+
const endpoint = categoryEndpointMap[props.state] ?? "/personal/suggest-places/categories";
|
|
225
|
+
const res = await api(
|
|
226
|
+
endpoint
|
|
227
|
+
);
|
|
228
|
+
categoryOptions.value = (res.data ?? []).map((c) => ({
|
|
229
|
+
label: c.category_name,
|
|
230
|
+
value: c.id
|
|
231
|
+
}));
|
|
232
|
+
} catch {
|
|
233
|
+
categoryOptions.value = [];
|
|
234
|
+
}
|
|
235
|
+
});
|
|
160
236
|
</script>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { InputLinkItem, ContactChannel, InputLinkProps } from "@/types/components/input/input-link";
|
|
2
2
|
type __VLS_Props = InputLinkProps & {
|
|
3
3
|
modelValue?: InputLinkItem[] | ContactChannel[];
|
|
4
|
+
defaultFirst?: boolean;
|
|
4
5
|
};
|
|
5
6
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
6
7
|
"update:modelValue": (value: InputLinkItem[] | ContactChannel[]) => any;
|
|
@@ -10,6 +11,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
|
|
|
10
11
|
name: string;
|
|
11
12
|
modelValue: InputLinkItem[] | ContactChannel[];
|
|
12
13
|
format: "legacy" | "contact_channel";
|
|
14
|
+
defaultFirst: boolean;
|
|
13
15
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
14
16
|
declare const _default: typeof __VLS_export;
|
|
15
17
|
export default _default;
|
|
@@ -37,11 +37,12 @@
|
|
|
37
37
|
</template>
|
|
38
38
|
|
|
39
39
|
<script setup>
|
|
40
|
-
import { computed, ref, nextTick, watch } from "vue";
|
|
40
|
+
import { computed, ref, nextTick, watch, onMounted } from "vue";
|
|
41
41
|
const props = defineProps({
|
|
42
42
|
name: { type: String, required: false, default: "link" },
|
|
43
43
|
format: { type: String, required: false, default: "legacy" },
|
|
44
|
-
modelValue: { type: Array, required: false, default: () => [] }
|
|
44
|
+
modelValue: { type: Array, required: false, default: () => [] },
|
|
45
|
+
defaultFirst: { type: Boolean, required: false, default: false }
|
|
45
46
|
});
|
|
46
47
|
const emit = defineEmits(["update:modelValue"]);
|
|
47
48
|
const listSocial = [
|
|
@@ -208,6 +209,11 @@ const listOption = computed(() => {
|
|
|
208
209
|
});
|
|
209
210
|
return arr;
|
|
210
211
|
});
|
|
212
|
+
onMounted(() => {
|
|
213
|
+
if (props.defaultFirst && linkValue.value.length === 0) {
|
|
214
|
+
addLink();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
211
217
|
const addLink = () => {
|
|
212
218
|
const link = {
|
|
213
219
|
name: "Email",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { InputLinkItem, ContactChannel, InputLinkProps } from "@/types/components/input/input-link";
|
|
2
2
|
type __VLS_Props = InputLinkProps & {
|
|
3
3
|
modelValue?: InputLinkItem[] | ContactChannel[];
|
|
4
|
+
defaultFirst?: boolean;
|
|
4
5
|
};
|
|
5
6
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
6
7
|
"update:modelValue": (value: InputLinkItem[] | ContactChannel[]) => any;
|
|
@@ -10,6 +11,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
|
|
|
10
11
|
name: string;
|
|
11
12
|
modelValue: InputLinkItem[] | ContactChannel[];
|
|
12
13
|
format: "legacy" | "contact_channel";
|
|
14
|
+
defaultFirst: boolean;
|
|
13
15
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
14
16
|
declare const _default: typeof __VLS_export;
|
|
15
17
|
export default _default;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { $fetch } from "ofetch";
|
|
2
|
+
import { useApi } from "./useApi.js";
|
|
3
|
+
export const usePresignedUpload = () => {
|
|
4
|
+
const api = useApi();
|
|
5
|
+
const uploadFiles = async (files, state) => {
|
|
6
|
+
if (!files || files.length === 0) return [];
|
|
7
|
+
const newFiles = files.filter((item) => !!item.file);
|
|
8
|
+
const existingUrls = files.filter((item) => !item.file && item.url && !item.url.startsWith("blob:")).map((item) => item.url);
|
|
9
|
+
if (newFiles.length === 0) return existingUrls;
|
|
10
|
+
const res = await api("/storage/presigned-urls", {
|
|
11
|
+
method: "POST",
|
|
12
|
+
body: {
|
|
13
|
+
state,
|
|
14
|
+
file_name: newFiles.map((item) => item.file.name)
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
let presignedItems;
|
|
18
|
+
const d = res.data;
|
|
19
|
+
if (d.items) {
|
|
20
|
+
presignedItems = d.items;
|
|
21
|
+
} else {
|
|
22
|
+
presignedItems = [
|
|
23
|
+
{
|
|
24
|
+
original_name: d.original_name,
|
|
25
|
+
upload_url: d.upload_url,
|
|
26
|
+
public_url: d.public_url,
|
|
27
|
+
filename: d.filename
|
|
28
|
+
}
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
await Promise.all(
|
|
32
|
+
newFiles.map(
|
|
33
|
+
(item, index) => $fetch(presignedItems[index].upload_url, {
|
|
34
|
+
method: "PUT",
|
|
35
|
+
body: item.file,
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": item.file.type
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
return [...existingUrls, ...presignedItems.map((item) => item.public_url)];
|
|
43
|
+
};
|
|
44
|
+
return { uploadFiles };
|
|
45
|
+
};
|