pukaad-ui-lib 1.296.0 → 1.297.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/card/card-review.d.vue.ts +4 -2
- package/dist/runtime/components/card/card-review.vue +115 -68
- package/dist/runtime/components/card/card-review.vue.d.ts +4 -2
- package/dist/runtime/components/display/display-rating-summary.d.vue.ts +5 -1
- package/dist/runtime/components/display/display-rating-summary.vue +33 -13
- package/dist/runtime/components/display/display-rating-summary.vue.d.ts +5 -1
- package/dist/runtime/components/drawer/drawer-post-review.d.vue.ts +4 -6
- package/dist/runtime/components/drawer/drawer-post-review.vue +98 -100
- package/dist/runtime/components/drawer/drawer-post-review.vue.d.ts +4 -6
- package/dist/runtime/components/drawer/drawer-suggest-place/drawer-suggest-place.vue +2 -1
- package/dist/runtime/components/drawer/drawer-suggest-place/suggest-place-form.d.vue.ts +18 -6
- package/dist/runtime/components/drawer/drawer-suggest-place/suggest-place-form.vue +133 -88
- package/dist/runtime/components/drawer/drawer-suggest-place/suggest-place-form.vue.d.ts +18 -6
- package/dist/runtime/components/image/image-cropper.d.vue.ts +1 -1
- package/dist/runtime/components/image/image-cropper.vue.d.ts +1 -1
- package/dist/runtime/components/input/input-address.d.vue.ts +1 -1
- package/dist/runtime/components/input/input-address.vue.d.ts +1 -1
- package/dist/runtime/components/input/input-file.d.vue.ts +1 -1
- package/dist/runtime/components/input/input-file.vue.d.ts +1 -1
- package/dist/runtime/components/input/input-password.d.vue.ts +1 -1
- package/dist/runtime/components/input/input-password.vue.d.ts +1 -1
- package/dist/runtime/components/modal/modal-password-confirmed.d.vue.ts +1 -1
- package/dist/runtime/components/modal/modal-password-confirmed.vue.d.ts +1 -1
- package/dist/runtime/components/modal/modal-password-verify.d.vue.ts +1 -1
- package/dist/runtime/components/modal/modal-password-verify.vue.d.ts +1 -1
- package/dist/runtime/components/modal/modal-phone-OTP.d.vue.ts +1 -1
- package/dist/runtime/components/modal/modal-phone-OTP.vue.d.ts +1 -1
- package/package.json +1 -1
- /package/dist/runtime/assets/svg/socials/{WhatsApp.svg → Whatsapp.svg} +0 -0
package/dist/module.json
CHANGED
|
@@ -6,11 +6,13 @@ type __VLS_Props = {
|
|
|
6
6
|
disabledMenu?: boolean;
|
|
7
7
|
};
|
|
8
8
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
edit: (item: CardReviewProps) => any;
|
|
9
10
|
"toggle-like": (item: CardReviewProps, liked: boolean) => any;
|
|
10
|
-
"select-image": (item: CardReviewProps, index: number
|
|
11
|
+
"select-image": (item: CardReviewProps, index: number) => any;
|
|
11
12
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
13
|
+
onEdit?: ((item: CardReviewProps) => any) | undefined;
|
|
12
14
|
"onToggle-like"?: ((item: CardReviewProps, liked: boolean) => any) | undefined;
|
|
13
|
-
"onSelect-image"?: ((item: CardReviewProps, index: number
|
|
15
|
+
"onSelect-image"?: ((item: CardReviewProps, index: number) => any) | undefined;
|
|
14
16
|
}>, {
|
|
15
17
|
item: CardReviewProps;
|
|
16
18
|
disabledMenu: boolean;
|
|
@@ -1,70 +1,103 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="py-[16px] border-b-[1px] border-mercury flex gap-[16px] w-full">
|
|
3
|
-
<div v-if="props.item.user.avatar">
|
|
4
|
-
<Avatar
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
</div>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
2
|
+
<div class="py-[16px] border-b-[1px] border-mercury flex gap-[16px] w-full">
|
|
3
|
+
<div v-if="props.item.user.avatar">
|
|
4
|
+
<Avatar
|
|
5
|
+
:src="props.item.user.avatar"
|
|
6
|
+
alt="profile_myProfile"
|
|
7
|
+
:size="30"
|
|
8
|
+
class="cursor-pointer"
|
|
9
|
+
@click="navigateToProfile(props.item.user.path_name)"
|
|
10
|
+
/>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="flex flex-col gap-[24px] w-full">
|
|
13
|
+
<div class="flex flex-col gap-[8px]">
|
|
14
|
+
<div class="flex flex-col gap-[6px]">
|
|
15
|
+
<div class="flex flex-col gap-[4px]">
|
|
16
|
+
<div class="flex justify-between items-center">
|
|
17
|
+
<div
|
|
18
|
+
class="font-body-large cursor-pointer"
|
|
19
|
+
@click="navigateToProfile(props.item.user.path_name)"
|
|
20
|
+
>
|
|
21
|
+
{{ props.item.user?.name }}
|
|
22
|
+
</div>
|
|
23
|
+
<div class="flex gap-[8px] items-center">
|
|
24
|
+
<Button
|
|
25
|
+
v-if="props.item.user?.is_my_review"
|
|
26
|
+
variant="text"
|
|
27
|
+
color="default"
|
|
28
|
+
disabled-padding
|
|
29
|
+
@click="emit('edit', props.item)"
|
|
30
|
+
>
|
|
31
|
+
<Icon name="lucide:pencil" :size="16" />
|
|
32
|
+
แก้ไข
|
|
33
|
+
</Button>
|
|
34
|
+
<Button
|
|
35
|
+
variant="text"
|
|
36
|
+
:color="liked ? 'primary' : 'default'"
|
|
37
|
+
:disabled="props.disabledLike"
|
|
38
|
+
:aria-pressed="liked"
|
|
39
|
+
@click="toggleLike"
|
|
40
|
+
>
|
|
41
|
+
<Icon
|
|
42
|
+
:name="liked ? 'pukaad:thumbs-up-solid' : 'pukaad:thumbs-up-regular'"
|
|
43
|
+
:size="20"
|
|
44
|
+
/>
|
|
45
|
+
{{ convertNumber(likeCount) }}
|
|
46
|
+
</Button>
|
|
47
|
+
<PickerOptionMenuUser
|
|
48
|
+
v-if="!props.disabledMenu"
|
|
49
|
+
:state="menuType"
|
|
50
|
+
disabled-padding
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="text-gray font-body-small">
|
|
55
|
+
{{ convertNumber(props.item.user?.review_count ?? 0) }} รีวิว •
|
|
56
|
+
{{ convertNumber(props.item.user?.like_count ?? 0) }} ชื่นชอบรีวิว
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="flex gap-[8px] items-center">
|
|
60
|
+
<InputRating :size="11" readonly :model-value="props.item.review?.rating" />
|
|
61
|
+
<div class="text-gray font-body-small">
|
|
62
|
+
{{ convertDate(props.item.review?.created_at ?? "") }}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div v-if="props.item.review?.description" class="font-body-large">
|
|
68
|
+
{{ props.item.review?.description }}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div v-if="props.item.review?.images?.length" class="flex gap-[8px]">
|
|
72
|
+
<DisplayImageReview :items="props.item.review?.images" @select="selectImage" />
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div v-if="props.item.replies">
|
|
77
|
+
<div class="p-[16px] rounded-sm bg-bright">
|
|
78
|
+
<div class="text-gray font-body-large">
|
|
79
|
+
การตอบกลับจาก {{ props.item.replies?.author }}
|
|
80
|
+
</div>
|
|
81
|
+
<div class="font-body-large">{{ props.item.replies?.description }}</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<ModalMediaView
|
|
88
|
+
v-model="isOpen"
|
|
89
|
+
:items="props.item.review?.images"
|
|
90
|
+
:start-index="startIndex"
|
|
91
|
+
title="รูปภาพรีวิว"
|
|
92
|
+
/>
|
|
59
93
|
</template>
|
|
60
94
|
|
|
61
95
|
<script setup>
|
|
62
|
-
import { computed, ref } from "vue";
|
|
96
|
+
import { computed, ref, watch } from "vue";
|
|
63
97
|
import { useRouter } from "vue-router";
|
|
64
98
|
import { useConvert } from "../../composables/useConvert";
|
|
65
99
|
const router = useRouter();
|
|
66
|
-
const { convertNumber } = useConvert();
|
|
67
|
-
const isOpen = ref(false);
|
|
100
|
+
const { convertNumber, convertDate } = useConvert();
|
|
68
101
|
const props = defineProps({
|
|
69
102
|
item: { type: Object, required: true, default: () => ({
|
|
70
103
|
review_id: "0",
|
|
@@ -74,7 +107,8 @@ const props = defineProps({
|
|
|
74
107
|
avatar: "",
|
|
75
108
|
path_name: "",
|
|
76
109
|
review_count: 0,
|
|
77
|
-
like_count: 0
|
|
110
|
+
like_count: 0,
|
|
111
|
+
is_my_review: false
|
|
78
112
|
},
|
|
79
113
|
review: {
|
|
80
114
|
rating: 0,
|
|
@@ -94,15 +128,29 @@ const props = defineProps({
|
|
|
94
128
|
disabledLike: { type: Boolean, required: false, default: false },
|
|
95
129
|
disabledMenu: { type: Boolean, required: false, default: false }
|
|
96
130
|
});
|
|
97
|
-
const emit = defineEmits(["toggle-like", "select-image"]);
|
|
131
|
+
const emit = defineEmits(["toggle-like", "select-image", "edit"]);
|
|
132
|
+
const isOpen = ref(false);
|
|
133
|
+
const startIndex = ref(0);
|
|
98
134
|
const liked = ref(props.item.review?.liked ?? false);
|
|
99
135
|
const likeCount = ref(props.item.review?.like_count ?? 0);
|
|
136
|
+
watch(
|
|
137
|
+
() => props.item.review?.liked,
|
|
138
|
+
(val) => {
|
|
139
|
+
liked.value = val ?? false;
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
watch(
|
|
143
|
+
() => props.item.review?.like_count,
|
|
144
|
+
(val) => {
|
|
145
|
+
likeCount.value = val ?? 0;
|
|
146
|
+
}
|
|
147
|
+
);
|
|
100
148
|
const menuType = computed(() => props.isProfile ? "my-profile" : "profile");
|
|
101
|
-
const
|
|
149
|
+
const navigateToProfile = (pathName) => {
|
|
102
150
|
if (!pathName) return;
|
|
103
|
-
router.push(
|
|
151
|
+
router.push(`/@${pathName}`);
|
|
104
152
|
};
|
|
105
|
-
|
|
153
|
+
const toggleLike = () => {
|
|
106
154
|
if (liked.value) {
|
|
107
155
|
likeCount.value -= 1;
|
|
108
156
|
liked.value = false;
|
|
@@ -111,11 +159,10 @@ function toggleLike() {
|
|
|
111
159
|
liked.value = true;
|
|
112
160
|
}
|
|
113
161
|
emit("toggle-like", props.item, liked.value);
|
|
114
|
-
}
|
|
115
|
-
const startIndex = ref(0);
|
|
162
|
+
};
|
|
116
163
|
const selectImage = (index) => {
|
|
117
164
|
startIndex.value = index;
|
|
118
165
|
isOpen.value = true;
|
|
119
|
-
emit("select-image", props.item, index
|
|
166
|
+
emit("select-image", props.item, index);
|
|
120
167
|
};
|
|
121
168
|
</script>
|
|
@@ -6,11 +6,13 @@ type __VLS_Props = {
|
|
|
6
6
|
disabledMenu?: boolean;
|
|
7
7
|
};
|
|
8
8
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
9
|
+
edit: (item: CardReviewProps) => any;
|
|
9
10
|
"toggle-like": (item: CardReviewProps, liked: boolean) => any;
|
|
10
|
-
"select-image": (item: CardReviewProps, index: number
|
|
11
|
+
"select-image": (item: CardReviewProps, index: number) => any;
|
|
11
12
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
13
|
+
onEdit?: ((item: CardReviewProps) => any) | undefined;
|
|
12
14
|
"onToggle-like"?: ((item: CardReviewProps, liked: boolean) => any) | undefined;
|
|
13
|
-
"onSelect-image"?: ((item: CardReviewProps, index: number
|
|
15
|
+
"onSelect-image"?: ((item: CardReviewProps, index: number) => any) | undefined;
|
|
14
16
|
}>, {
|
|
15
17
|
item: CardReviewProps;
|
|
16
18
|
disabledMenu: boolean;
|
|
@@ -8,7 +8,11 @@ export interface DisplayRatingSummaryProps {
|
|
|
8
8
|
distribution?: RatingDistribution[];
|
|
9
9
|
badge?: number;
|
|
10
10
|
}
|
|
11
|
-
declare const __VLS_export: import("vue").DefineComponent<DisplayRatingSummaryProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<DisplayRatingSummaryProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"write-review": () => any;
|
|
13
|
+
}, string, import("vue").PublicProps, Readonly<DisplayRatingSummaryProps> & Readonly<{
|
|
14
|
+
"onWrite-review"?: (() => any) | undefined;
|
|
15
|
+
}>, {
|
|
12
16
|
rating: number | string;
|
|
13
17
|
totalReviews: number | string;
|
|
14
18
|
distribution: RatingDistribution[];
|
|
@@ -1,30 +1,49 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="flex gap-[24px] items-center">
|
|
3
3
|
<!-- Left: คะแนนรวม -->
|
|
4
|
-
<div class="flex flex-col
|
|
5
|
-
<div
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
<div class="flex flex-col gap-[8px] w-[214px] justify-center items-center">
|
|
5
|
+
<div
|
|
6
|
+
class="flex flex-col items-center text-primary flex-shrink-0 w-[100px]"
|
|
7
|
+
>
|
|
8
|
+
<div class="text-[56px] font-bold leading-none">
|
|
9
|
+
{{ Number(props.rating).toFixed(1) }}
|
|
10
|
+
</div>
|
|
11
|
+
<div class="font-body-large">
|
|
12
|
+
{{ convertNumber(Number(props.totalReviews)) }} รีวิว
|
|
13
|
+
</div>
|
|
10
14
|
</div>
|
|
15
|
+
<Button color="primary" @click="emit('write-review')">
|
|
16
|
+
<Icon name="pukaad:write-review" /> เขียนรีวิว
|
|
17
|
+
</Button>
|
|
11
18
|
</div>
|
|
12
|
-
|
|
13
19
|
<!-- Right: แถบการกระจายคะแนน -->
|
|
14
20
|
<div class="flex flex-col gap-[8px] flex-1">
|
|
15
|
-
<div
|
|
21
|
+
<div
|
|
22
|
+
v-for="(item, idx) in sortedDistribution"
|
|
23
|
+
:key="item.stars"
|
|
24
|
+
class="flex items-center gap-[12px]"
|
|
25
|
+
>
|
|
16
26
|
<!-- badge แสดงบน row แรก (5 ดาว) -->
|
|
17
27
|
<div class="relative flex-shrink-0">
|
|
18
28
|
<InputRating :size="16" readonly :model-value="item.stars" />
|
|
19
|
-
<div
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
<div
|
|
30
|
+
v-if="idx === 0 && props.badge != null"
|
|
31
|
+
class="absolute -top-[10px] -left-[10px] min-w-[22px] h-[22px] px-[4px] rounded-full bg-error flex items-center justify-center"
|
|
32
|
+
>
|
|
33
|
+
<span class="text-white text-[11px] font-bold leading-none">{{
|
|
34
|
+
props.badge
|
|
35
|
+
}}</span>
|
|
22
36
|
</div>
|
|
23
37
|
</div>
|
|
24
38
|
|
|
25
39
|
<div class="flex-1 max-w-[260px]">
|
|
26
|
-
<ProgressBar
|
|
27
|
-
|
|
40
|
+
<ProgressBar
|
|
41
|
+
:value="maxCount > 0 ? item.count / maxCount * 100 : 0"
|
|
42
|
+
full-width
|
|
43
|
+
:height="8"
|
|
44
|
+
color-active="#FBC02D"
|
|
45
|
+
color-background="#E0E0E0"
|
|
46
|
+
/>
|
|
28
47
|
</div>
|
|
29
48
|
|
|
30
49
|
<div class="text-gray font-body-large w-[72px]">
|
|
@@ -39,6 +58,7 @@
|
|
|
39
58
|
import { computed } from "vue";
|
|
40
59
|
import { useConvert } from "../../composables/useConvert";
|
|
41
60
|
const { convertNumber } = useConvert();
|
|
61
|
+
const emit = defineEmits(["write-review"]);
|
|
42
62
|
const props = defineProps({
|
|
43
63
|
rating: { type: [Number, String], required: false, default: 0 },
|
|
44
64
|
totalReviews: { type: [Number, String], required: false, default: 0 },
|
|
@@ -8,7 +8,11 @@ export interface DisplayRatingSummaryProps {
|
|
|
8
8
|
distribution?: RatingDistribution[];
|
|
9
9
|
badge?: number;
|
|
10
10
|
}
|
|
11
|
-
declare const __VLS_export: import("vue").DefineComponent<DisplayRatingSummaryProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<DisplayRatingSummaryProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"write-review": () => any;
|
|
13
|
+
}, string, import("vue").PublicProps, Readonly<DisplayRatingSummaryProps> & Readonly<{
|
|
14
|
+
"onWrite-review"?: (() => any) | undefined;
|
|
15
|
+
}>, {
|
|
12
16
|
rating: number | string;
|
|
13
17
|
totalReviews: number | string;
|
|
14
18
|
distribution: RatingDistribution[];
|
|
@@ -12,20 +12,18 @@ export interface DrawerPostReviewItem {
|
|
|
12
12
|
description: string;
|
|
13
13
|
photos: IFileItem[];
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
type __VLS_Props = {
|
|
16
16
|
item?: DrawerPostReviewItem;
|
|
17
|
-
}
|
|
18
|
-
type __VLS_Props = DrawerPostReviewProps;
|
|
17
|
+
};
|
|
19
18
|
type __VLS_ModelProps = {
|
|
20
19
|
modelValue?: boolean;
|
|
21
20
|
};
|
|
22
21
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
23
22
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
23
|
+
success: (data: unknown) => any;
|
|
24
24
|
"update:modelValue": (value: boolean) => any;
|
|
25
|
-
} & {
|
|
26
|
-
success: (data: any) => any;
|
|
27
25
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
28
|
-
onSuccess?: ((data:
|
|
26
|
+
onSuccess?: ((data: unknown) => any) | undefined;
|
|
29
27
|
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
30
28
|
}>, {
|
|
31
29
|
item: DrawerPostReviewItem;
|
|
@@ -1,87 +1,82 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<Drawer
|
|
3
|
-
class="w-[748px]"
|
|
4
|
-
:title="drawerTitle"
|
|
5
|
-
@close="onClose"
|
|
6
|
-
disabled-auto-close
|
|
7
|
-
@submit="onSubmit"
|
|
8
|
-
v-model="isOpen"
|
|
9
|
-
>
|
|
10
|
-
<div class="flex flex-col gap-[16px]">
|
|
11
|
-
<div class="flex gap-[16px]">
|
|
12
|
-
<div
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class="
|
|
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
|
-
</Button>
|
|
73
|
-
</div>
|
|
74
|
-
</template>
|
|
75
|
-
</Drawer>
|
|
2
|
+
<Drawer
|
|
3
|
+
class="w-[748px]"
|
|
4
|
+
:title="drawerTitle"
|
|
5
|
+
@close="onClose"
|
|
6
|
+
disabled-auto-close
|
|
7
|
+
@submit="onSubmit"
|
|
8
|
+
v-model="isOpen"
|
|
9
|
+
>
|
|
10
|
+
<div class="flex flex-col gap-[16px]">
|
|
11
|
+
<div class="flex gap-[16px]">
|
|
12
|
+
<div class="w-[178px] h-[100px] rounded-[8px] overflow-hidden flex-shrink-0">
|
|
13
|
+
<Image
|
|
14
|
+
v-if="form.coverImage"
|
|
15
|
+
:src="form.coverImage"
|
|
16
|
+
width="auto"
|
|
17
|
+
height="auto"
|
|
18
|
+
fit="cover"
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="flex flex-col gap-[4px]">
|
|
22
|
+
<div class="font-body-large-prominent">{{ form.placeName }}</div>
|
|
23
|
+
<div class="font-body-small text-gray">{{ form.address }}</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="flex items-center justify-center py-[16px]">
|
|
28
|
+
<InputRating v-model="form.rating" :size="28" />
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<InputTextarea
|
|
32
|
+
name="description"
|
|
33
|
+
label="คำอธิบาย"
|
|
34
|
+
placeholder="เขียนรีวิวของคุณ"
|
|
35
|
+
showCounter
|
|
36
|
+
v-model="form.description"
|
|
37
|
+
class="min-h-[120px]"
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
<div class="flex flex-col gap-[8px]">
|
|
41
|
+
<div class="flex flex-col gap-[4px]">
|
|
42
|
+
<div class="text-gray font-body-large">เพิ่มภาพถ่าย</div>
|
|
43
|
+
<div class="text-gray font-body-small">อัปโหลด 9 รายการ</div>
|
|
44
|
+
</div>
|
|
45
|
+
<InputFile
|
|
46
|
+
:limit="9"
|
|
47
|
+
name="photos"
|
|
48
|
+
accept="image/jpeg,image/png,image/webp,image/bmp,image/gif"
|
|
49
|
+
v-model="form.photos"
|
|
50
|
+
/>
|
|
51
|
+
<div class="flex flex-col text-gray font-body-small">
|
|
52
|
+
<div>รองรับไฟล์ *.jpg *.jpeg *.png *.webp *.bmp *.gif</div>
|
|
53
|
+
<div>ขนาดไฟล์สูงสุด 30 mb</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<template #footer>
|
|
59
|
+
<div class="flex justify-end gap-[16px] items-center">
|
|
60
|
+
<Button variant="outline" @click="onClose" :disabled="isSubmitting">ยกเลิก</Button>
|
|
61
|
+
<Button
|
|
62
|
+
type="submit"
|
|
63
|
+
color="primary"
|
|
64
|
+
:disabled="form.rating === 0 || isSubmitting"
|
|
65
|
+
:loading="isSubmitting"
|
|
66
|
+
>
|
|
67
|
+
ยืนยัน
|
|
68
|
+
</Button>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
71
|
+
</Drawer>
|
|
76
72
|
</template>
|
|
77
73
|
|
|
78
74
|
<script setup>
|
|
79
|
-
import { useApi } from "../../composables/useApi";
|
|
80
75
|
import { ref, watch, computed } from "vue";
|
|
81
76
|
import { useNuxtApp } from "nuxt/app";
|
|
77
|
+
import { useApi } from "../../composables/useApi";
|
|
82
78
|
const { $alert } = useNuxtApp();
|
|
83
79
|
const api = useApi();
|
|
84
|
-
const emit = defineEmits(["success"]);
|
|
85
80
|
const props = defineProps({
|
|
86
81
|
item: { type: Object, required: false, default: () => ({
|
|
87
82
|
placeId: "",
|
|
@@ -93,6 +88,7 @@ const props = defineProps({
|
|
|
93
88
|
photos: []
|
|
94
89
|
}) }
|
|
95
90
|
});
|
|
91
|
+
const emit = defineEmits(["success"]);
|
|
96
92
|
const isOpen = defineModel({ type: Boolean, ...{ default: false } });
|
|
97
93
|
const isSubmitting = ref(false);
|
|
98
94
|
const form = ref({
|
|
@@ -104,10 +100,6 @@ const form = ref({
|
|
|
104
100
|
description: props.item.description,
|
|
105
101
|
photos: props.item.photos ? [...props.item.photos] : []
|
|
106
102
|
});
|
|
107
|
-
const isEdit = computed(() => !!(props.item?.rating && props.item.rating > 0));
|
|
108
|
-
const drawerTitle = computed(
|
|
109
|
-
() => isEdit.value ? "\u0E41\u0E01\u0E49\u0E44\u0E02\u0E23\u0E35\u0E27\u0E34\u0E27" : "\u0E40\u0E02\u0E35\u0E22\u0E19\u0E23\u0E35\u0E27\u0E34\u0E27"
|
|
110
|
-
);
|
|
111
103
|
watch(isOpen, (newVal) => {
|
|
112
104
|
if (newVal) {
|
|
113
105
|
form.value = {
|
|
@@ -121,6 +113,8 @@ watch(isOpen, (newVal) => {
|
|
|
121
113
|
};
|
|
122
114
|
}
|
|
123
115
|
});
|
|
116
|
+
const isEdit = computed(() => (props.item?.rating ?? 0) > 0);
|
|
117
|
+
const drawerTitle = computed(() => isEdit.value ? "\u0E41\u0E01\u0E49\u0E44\u0E02\u0E23\u0E35\u0E27\u0E34\u0E27" : "\u0E40\u0E02\u0E35\u0E22\u0E19\u0E23\u0E35\u0E27\u0E34\u0E27");
|
|
124
118
|
const hasChanges = () => {
|
|
125
119
|
const orig = props.item;
|
|
126
120
|
if (form.value.rating !== orig.rating) return true;
|
|
@@ -128,29 +122,17 @@ const hasChanges = () => {
|
|
|
128
122
|
if (form.value.photos.length !== (orig.photos?.length ?? 0)) return true;
|
|
129
123
|
return false;
|
|
130
124
|
};
|
|
131
|
-
const onClose = async () => {
|
|
132
|
-
if (hasChanges()) {
|
|
133
|
-
const { isConfirmed } = await $alert.show({
|
|
134
|
-
type: "warning",
|
|
135
|
-
title: "\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E2B\u0E19\u0E49\u0E32\u0E19\u0E35\u0E49\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48 ?",
|
|
136
|
-
description: "\u0E01\u0E32\u0E23\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E41\u0E1B\u0E25\u0E07\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E22\u0E31\u0E07\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E23\u0E31\u0E1A\u0E01\u0E32\u0E23\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01 \u0E04\u0E38\u0E13\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E25\u0E30\u0E17\u0E34\u0E49\u0E07\u0E01\u0E32\u0E23\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E41\u0E1B\u0E25\u0E07\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48 ?",
|
|
137
|
-
confirmText: "\u0E25\u0E30\u0E17\u0E34\u0E49\u0E07",
|
|
138
|
-
cancelText: "\u0E41\u0E01\u0E49\u0E44\u0E02\u0E15\u0E48\u0E2D",
|
|
139
|
-
showCancelBtn: true
|
|
140
|
-
});
|
|
141
|
-
if (isConfirmed) isOpen.value = false;
|
|
142
|
-
} else {
|
|
143
|
-
isOpen.value = false;
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
125
|
const uploadNewPhotos = async (items) => {
|
|
147
126
|
const newItems = items.filter((it) => it.file);
|
|
148
127
|
if (newItems.length === 0) return [];
|
|
149
128
|
const fileNames = newItems.map((it) => it.file.name);
|
|
150
|
-
const presignedRes = await api(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
129
|
+
const presignedRes = await api(
|
|
130
|
+
"/storage/presigned-urls",
|
|
131
|
+
{
|
|
132
|
+
method: "post",
|
|
133
|
+
body: { state: "suggest-place-review", file_name: fileNames }
|
|
134
|
+
}
|
|
135
|
+
);
|
|
154
136
|
const uploadItems = presignedRes?.data?.items ?? [];
|
|
155
137
|
if (uploadItems.length !== newItems.length) {
|
|
156
138
|
throw new Error("\u0E44\u0E21\u0E48\u0E2A\u0E32\u0E21\u0E32\u0E23\u0E16\u0E2A\u0E23\u0E49\u0E32\u0E07 upload URL \u0E44\u0E14\u0E49 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E25\u0E2D\u0E07\u0E43\u0E2B\u0E21\u0E48\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07");
|
|
@@ -168,6 +150,21 @@ const uploadNewPhotos = async (items) => {
|
|
|
168
150
|
);
|
|
169
151
|
return uploadItems.map((u) => u.public_url);
|
|
170
152
|
};
|
|
153
|
+
const onClose = async () => {
|
|
154
|
+
if (hasChanges()) {
|
|
155
|
+
const { isConfirmed } = await $alert.show({
|
|
156
|
+
type: "warning",
|
|
157
|
+
title: "\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E2B\u0E19\u0E49\u0E32\u0E19\u0E35\u0E49\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48 ?",
|
|
158
|
+
description: "\u0E01\u0E32\u0E23\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E41\u0E1B\u0E25\u0E07\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13\u0E22\u0E31\u0E07\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E23\u0E31\u0E1A\u0E01\u0E32\u0E23\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01 \u0E04\u0E38\u0E13\u0E15\u0E49\u0E2D\u0E07\u0E01\u0E32\u0E23\u0E25\u0E30\u0E17\u0E34\u0E49\u0E07\u0E01\u0E32\u0E23\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E41\u0E1B\u0E25\u0E07\u0E2B\u0E23\u0E37\u0E2D\u0E44\u0E21\u0E48 ?",
|
|
159
|
+
confirmText: "\u0E25\u0E30\u0E17\u0E34\u0E49\u0E07",
|
|
160
|
+
cancelText: "\u0E41\u0E01\u0E49\u0E44\u0E02\u0E15\u0E48\u0E2D",
|
|
161
|
+
showCancelBtn: true
|
|
162
|
+
});
|
|
163
|
+
if (isConfirmed) isOpen.value = false;
|
|
164
|
+
} else {
|
|
165
|
+
isOpen.value = false;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
171
168
|
const onSubmit = async () => {
|
|
172
169
|
if (isSubmitting.value) return;
|
|
173
170
|
isSubmitting.value = true;
|
|
@@ -189,7 +186,8 @@ const onSubmit = async () => {
|
|
|
189
186
|
emit("success", response?.data);
|
|
190
187
|
isOpen.value = false;
|
|
191
188
|
} catch (err) {
|
|
192
|
-
const
|
|
189
|
+
const apiErr = err;
|
|
190
|
+
const msg = apiErr?.data?.message || apiErr?.message || "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14 \u0E01\u0E23\u0E38\u0E13\u0E32\u0E25\u0E2D\u0E07\u0E43\u0E2B\u0E21\u0E48\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07";
|
|
193
191
|
await $alert.show({
|
|
194
192
|
type: "error",
|
|
195
193
|
title: "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14",
|
|
@@ -12,20 +12,18 @@ export interface DrawerPostReviewItem {
|
|
|
12
12
|
description: string;
|
|
13
13
|
photos: IFileItem[];
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
type __VLS_Props = {
|
|
16
16
|
item?: DrawerPostReviewItem;
|
|
17
|
-
}
|
|
18
|
-
type __VLS_Props = DrawerPostReviewProps;
|
|
17
|
+
};
|
|
19
18
|
type __VLS_ModelProps = {
|
|
20
19
|
modelValue?: boolean;
|
|
21
20
|
};
|
|
22
21
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
23
22
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
23
|
+
success: (data: unknown) => any;
|
|
24
24
|
"update:modelValue": (value: boolean) => any;
|
|
25
|
-
} & {
|
|
26
|
-
success: (data: any) => any;
|
|
27
25
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
28
|
-
onSuccess?: ((data:
|
|
26
|
+
onSuccess?: ((data: unknown) => any) | undefined;
|
|
29
27
|
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
30
28
|
}>, {
|
|
31
29
|
item: DrawerPostReviewItem;
|
|
@@ -201,8 +201,9 @@ const onSubmit = async () => {
|
|
|
201
201
|
emit("submit", formData.value);
|
|
202
202
|
isOpen.value = false;
|
|
203
203
|
} catch (e) {
|
|
204
|
+
const apiErr = e;
|
|
204
205
|
$toast?.error?.(
|
|
205
|
-
|
|
206
|
+
apiErr?.data?.error || apiErr?.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"
|
|
206
207
|
);
|
|
207
208
|
} finally {
|
|
208
209
|
loading.value = false;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { InputAddressValue } from "#pukaad-ui/runtime/components/input/input-address.vue";
|
|
2
|
+
interface FileItem {
|
|
3
|
+
file?: File;
|
|
4
|
+
url: string;
|
|
5
|
+
}
|
|
6
|
+
interface ContactChannel {
|
|
7
|
+
type: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
2
10
|
export interface SuggestPlaceData {
|
|
3
11
|
address?: InputAddressValue;
|
|
4
12
|
latLng?: {
|
|
@@ -8,18 +16,22 @@ export interface SuggestPlaceData {
|
|
|
8
16
|
businessName?: string;
|
|
9
17
|
nameTh?: string;
|
|
10
18
|
nameEn?: string;
|
|
11
|
-
categories?:
|
|
19
|
+
categories?: {
|
|
20
|
+
id?: string;
|
|
21
|
+
value?: string;
|
|
22
|
+
label?: string;
|
|
23
|
+
}[];
|
|
12
24
|
description?: string;
|
|
13
|
-
openingHours?:
|
|
25
|
+
openingHours?: Record<string, unknown>;
|
|
14
26
|
phone?: string;
|
|
15
27
|
extraPhones?: string[];
|
|
16
28
|
isReview?: boolean;
|
|
17
29
|
rating?: number;
|
|
18
30
|
reviewDescription?: string;
|
|
19
|
-
reviewPhotos?:
|
|
20
|
-
photos?:
|
|
21
|
-
videos?:
|
|
22
|
-
contactChannels?:
|
|
31
|
+
reviewPhotos?: FileItem[];
|
|
32
|
+
photos?: FileItem[];
|
|
33
|
+
videos?: FileItem[];
|
|
34
|
+
contactChannels?: ContactChannel[];
|
|
23
35
|
}
|
|
24
36
|
type __VLS_Props = {
|
|
25
37
|
state?: "personal" | "business" | "backoffice";
|
|
@@ -1,76 +1,130 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="flex gap-[16px] w-full">
|
|
3
|
-
<!-- กรอกข้อมูล -->
|
|
4
|
-
<div class="flex flex-col gap-[16px] w-[490px]">
|
|
5
|
-
<InputAddress name="address" v-model="modelValue.address" :fixed-province-id="props.fixedProvinceId" />
|
|
6
|
-
<template v-if="isAddressCompleted">
|
|
7
|
-
<div class="font-body-large-prominent">รายละเอียด</div>
|
|
8
|
-
<div class="flex flex-col gap-[4px]">
|
|
9
|
-
<InputAutocomplete
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
placeholder="
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
2
|
+
<div class="flex gap-[16px] w-full">
|
|
3
|
+
<!-- กรอกข้อมูล -->
|
|
4
|
+
<div class="flex flex-col gap-[16px] w-[490px]">
|
|
5
|
+
<InputAddress name="address" v-model="modelValue.address" :fixed-province-id="props.fixedProvinceId" />
|
|
6
|
+
<template v-if="isAddressCompleted">
|
|
7
|
+
<div class="font-body-large-prominent">รายละเอียด</div>
|
|
8
|
+
<div class="flex flex-col gap-[4px]">
|
|
9
|
+
<InputAutocomplete
|
|
10
|
+
name="businessName"
|
|
11
|
+
v-model="modelValue.businessName"
|
|
12
|
+
label="ชื่อสถานที่ธุรกิจ"
|
|
13
|
+
placeholder="กรอกชื่อสถานที่"
|
|
14
|
+
required
|
|
15
|
+
show-counter
|
|
16
|
+
:limit="180"
|
|
17
|
+
:free-text="true"
|
|
18
|
+
:fetch-fn="fetchApprovedPlaces"
|
|
19
|
+
value-key="business_name"
|
|
20
|
+
label-key="business_name"
|
|
21
|
+
/>
|
|
22
|
+
<InputTextField
|
|
23
|
+
v-if="extraNameCount >= 1"
|
|
24
|
+
name="nameTh"
|
|
25
|
+
v-model="modelValue.nameTh"
|
|
26
|
+
label="ชื่อภาษาไทย"
|
|
27
|
+
placeholder="ใส่ในกรณีที่ต่างจากชื่อหลัก"
|
|
28
|
+
show-counter
|
|
29
|
+
:limit="180"
|
|
30
|
+
/>
|
|
31
|
+
<InputTextField
|
|
32
|
+
v-if="extraNameCount >= 2"
|
|
33
|
+
name="nameEn"
|
|
34
|
+
v-model="modelValue.nameEn"
|
|
35
|
+
label="ชื่อภาษาอังกฤษ"
|
|
36
|
+
placeholder="ใส่ในกรณีที่ต่างจากชื่อหลัก"
|
|
37
|
+
show-counter
|
|
38
|
+
:limit="180"
|
|
39
|
+
/>
|
|
40
|
+
<Button v-if="extraNameCount < 2" variant="text" color="primary" class="w-[145px]" @click="extraNameCount++">
|
|
41
|
+
<Icon name="lucide:plus" />
|
|
42
|
+
เพิ่มชื่อสถานที่
|
|
43
|
+
</Button>
|
|
44
|
+
</div>
|
|
45
|
+
<InputCombobox
|
|
46
|
+
name="categories"
|
|
47
|
+
v-model="modelValue.categories"
|
|
48
|
+
label="หมวดหมู่"
|
|
49
|
+
placeholder="เพิ่มหมวดหมู่ที่เกี่ยวข้องกับสถานที่"
|
|
50
|
+
:limit="3"
|
|
51
|
+
show-counter
|
|
52
|
+
:options="categoryOptions"
|
|
53
|
+
required
|
|
54
|
+
multiple
|
|
55
|
+
/>
|
|
56
|
+
<InputTextarea
|
|
57
|
+
name="description"
|
|
58
|
+
v-model="modelValue.description"
|
|
59
|
+
label="คำอธิบาย"
|
|
60
|
+
placeholder="คำอธิบายเกี่ยวกับสถานที่ (สูงสุด 220 ตัวอักษร)"
|
|
61
|
+
:limit="220"
|
|
62
|
+
show-counter
|
|
63
|
+
/>
|
|
64
|
+
<InputDateOpening name="openingHours" v-model="modelValue.openingHours" />
|
|
65
|
+
<div class="flex flex-col gap-[8px]">
|
|
66
|
+
<InputTextField name="phone" v-model="modelValue.phone" label="เบอร์โทรศัพท์" placeholder="กรอกเบอร์โทรศัพท์" />
|
|
67
|
+
<InputTextField
|
|
68
|
+
v-for="(_, index) in modelValue.extraPhones"
|
|
69
|
+
:key="index"
|
|
70
|
+
:name="`extraPhone-${index}`"
|
|
71
|
+
label="เบอร์โทรศัพท์"
|
|
72
|
+
placeholder="กรอกเบอร์โทรศัพท์"
|
|
73
|
+
v-model="modelValue.extraPhones[index]"
|
|
74
|
+
/>
|
|
75
|
+
<Button variant="text" color="primary" class="w-[145px]" @click="modelValue.extraPhones.push('')">
|
|
76
|
+
<Icon name="lucide:plus" />
|
|
77
|
+
เพิ่มเบอร์โทรศัพท์
|
|
78
|
+
</Button>
|
|
79
|
+
<InputLink name="contactChannels" format="contact_channel" default-first v-model="modelValue.contactChannels" />
|
|
80
|
+
</div>
|
|
81
|
+
<template v-if="props.state === 'personal'">
|
|
82
|
+
<div class="flex flex-col gap-[16px]">
|
|
83
|
+
<InputCheckbox name="isReview" v-model="modelValue.isReview" label="คุณต้องการรีวิวสถานที่นี้" />
|
|
84
|
+
<template v-if="modelValue.isReview">
|
|
85
|
+
<InputRating name="rating" v-model="modelValue.rating" class="flex py-4 justify-center" />
|
|
86
|
+
<InputTextarea
|
|
87
|
+
name="reviewDescription"
|
|
88
|
+
v-model="modelValue.reviewDescription"
|
|
89
|
+
label="คำอธิบาย"
|
|
90
|
+
placeholder="คำอธิบายเกี่ยวกับสถานที่"
|
|
91
|
+
show-counter
|
|
92
|
+
/>
|
|
93
|
+
<div class="flex flex-col gap-[8px]">
|
|
94
|
+
<div class="flex flex-col gap-[4px]">
|
|
95
|
+
<div class="font-body-large-prominent text-gray">เพิ่มภาพถ่าย</div>
|
|
96
|
+
<div class="font-body-small text-gray">สูงสุด 9 รายการ</div>
|
|
97
|
+
</div>
|
|
98
|
+
<InputFile name="reviewPhotos" v-model="modelValue.reviewPhotos" accept="image/*" :limit="9" />
|
|
99
|
+
<div class="font-body-small text-gray w-[250px]">
|
|
100
|
+
รองรับไฟล์ *.jpg *.jpeg *.png *.webp *.bmp *.gif ขนาดไฟล์ไม่เกิน 30 mb
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
104
|
+
</div>
|
|
105
|
+
</template>
|
|
106
|
+
</template>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- แสดง polygon -->
|
|
110
|
+
<div class="flex flex-col gap-[16px] w-[334px] sticky top-0 self-start">
|
|
111
|
+
<SuggestPlaceMap
|
|
112
|
+
:province-id="modelValue.address?.province_id"
|
|
113
|
+
:amphur-id="modelValue.address?.amphur_id"
|
|
114
|
+
:tambon-id="modelValue.address?.tambon_id"
|
|
115
|
+
@update:lat-lng="modelValue.latLng = $event"
|
|
116
|
+
/>
|
|
117
|
+
<InputTextField
|
|
118
|
+
name="latitude-longitude"
|
|
119
|
+
icon-prepend="lucide:locate-fixed"
|
|
120
|
+
readonly
|
|
121
|
+
placeholder="ละติจูด, ลองจิจูด"
|
|
122
|
+
:model-value="
|
|
70
123
|
modelValue.latLng ? `${modelValue.latLng.lat.toFixed(6)}, ${modelValue.latLng.lng.toFixed(6)}` : ''
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
124
|
+
"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
74
128
|
</template>
|
|
75
129
|
|
|
76
130
|
<script setup>
|
|
@@ -98,9 +152,7 @@ watch(_model, (val) => {
|
|
|
98
152
|
watch(modelValue, (val) => {
|
|
99
153
|
_model.value = { ...val };
|
|
100
154
|
}, { deep: true });
|
|
101
|
-
const extraNameCount = ref(
|
|
102
|
-
modelValue.nameEn ? 2 : modelValue.nameTh ? 1 : 0
|
|
103
|
-
);
|
|
155
|
+
const extraNameCount = ref(modelValue.nameEn ? 2 : modelValue.nameTh ? 1 : 0);
|
|
104
156
|
const isAddressCompleted = computed(() => {
|
|
105
157
|
const v = modelValue.address || {};
|
|
106
158
|
return !!(v.province_id && v.amphur_id && v.tambon_id && v.zipcode);
|
|
@@ -120,15 +172,15 @@ const listEndpointMap = {
|
|
|
120
172
|
business: "/business/suggest-places",
|
|
121
173
|
backoffice: "/office/suggest-places"
|
|
122
174
|
};
|
|
175
|
+
const categoryEndpointMap = {
|
|
176
|
+
personal: "/personal/suggest-places/categories",
|
|
177
|
+
business: "/business/suggest-places/categories",
|
|
178
|
+
backoffice: "/office/suggest-places/categories"
|
|
179
|
+
};
|
|
123
180
|
const fetchApprovedPlaces = async (page, pageSize, search) => {
|
|
124
181
|
const base = listEndpointMap[props.state] ?? "/personal/suggest-places";
|
|
125
182
|
const res = await api(base, {
|
|
126
|
-
query: {
|
|
127
|
-
q: search ?? "",
|
|
128
|
-
status: "PENDING,APPROVED",
|
|
129
|
-
page,
|
|
130
|
-
page_size: pageSize
|
|
131
|
-
}
|
|
183
|
+
query: { q: search ?? "", status: "PENDING,APPROVED", page, page_size: pageSize }
|
|
132
184
|
});
|
|
133
185
|
const data = (res.data ?? []).map((item) => ({
|
|
134
186
|
...item,
|
|
@@ -140,17 +192,10 @@ const fetchApprovedPlaces = async (page, pageSize, search) => {
|
|
|
140
192
|
};
|
|
141
193
|
};
|
|
142
194
|
const categoryOptions = ref([]);
|
|
143
|
-
const categoryEndpointMap = {
|
|
144
|
-
personal: "/personal/suggest-places/categories",
|
|
145
|
-
business: "/business/suggest-places/categories",
|
|
146
|
-
backoffice: "/office/suggest-places/categories"
|
|
147
|
-
};
|
|
148
195
|
onMounted(async () => {
|
|
149
196
|
try {
|
|
150
197
|
const endpoint = categoryEndpointMap[props.state] ?? "/personal/suggest-places/categories";
|
|
151
|
-
const res = await api(
|
|
152
|
-
endpoint
|
|
153
|
-
);
|
|
198
|
+
const res = await api(endpoint);
|
|
154
199
|
categoryOptions.value = (res.data ?? []).map((c) => ({
|
|
155
200
|
label: c.category_name,
|
|
156
201
|
value: c.id
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { InputAddressValue } from "#pukaad-ui/runtime/components/input/input-address.vue";
|
|
2
|
+
interface FileItem {
|
|
3
|
+
file?: File;
|
|
4
|
+
url: string;
|
|
5
|
+
}
|
|
6
|
+
interface ContactChannel {
|
|
7
|
+
type: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
2
10
|
export interface SuggestPlaceData {
|
|
3
11
|
address?: InputAddressValue;
|
|
4
12
|
latLng?: {
|
|
@@ -8,18 +16,22 @@ export interface SuggestPlaceData {
|
|
|
8
16
|
businessName?: string;
|
|
9
17
|
nameTh?: string;
|
|
10
18
|
nameEn?: string;
|
|
11
|
-
categories?:
|
|
19
|
+
categories?: {
|
|
20
|
+
id?: string;
|
|
21
|
+
value?: string;
|
|
22
|
+
label?: string;
|
|
23
|
+
}[];
|
|
12
24
|
description?: string;
|
|
13
|
-
openingHours?:
|
|
25
|
+
openingHours?: Record<string, unknown>;
|
|
14
26
|
phone?: string;
|
|
15
27
|
extraPhones?: string[];
|
|
16
28
|
isReview?: boolean;
|
|
17
29
|
rating?: number;
|
|
18
30
|
reviewDescription?: string;
|
|
19
|
-
reviewPhotos?:
|
|
20
|
-
photos?:
|
|
21
|
-
videos?:
|
|
22
|
-
contactChannels?:
|
|
31
|
+
reviewPhotos?: FileItem[];
|
|
32
|
+
photos?: FileItem[];
|
|
33
|
+
videos?: FileItem[];
|
|
34
|
+
contactChannels?: ContactChannel[];
|
|
23
35
|
}
|
|
24
36
|
type __VLS_Props = {
|
|
25
37
|
state?: "personal" | "business" | "backoffice";
|
|
@@ -64,12 +64,12 @@ declare const __VLS_export: import("vue").DefineComponent<ImageCropperProps, {
|
|
|
64
64
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<ImageCropperProps> & Readonly<{}>, {
|
|
65
65
|
src: string;
|
|
66
66
|
center: boolean;
|
|
67
|
+
modal: boolean;
|
|
67
68
|
responsive: boolean;
|
|
68
69
|
restore: boolean;
|
|
69
70
|
checkCrossOrigin: boolean;
|
|
70
71
|
checkOrientation: boolean;
|
|
71
72
|
crossorigin: "" | "anonymous" | "use-credentials";
|
|
72
|
-
modal: boolean;
|
|
73
73
|
guides: boolean;
|
|
74
74
|
highlight: boolean;
|
|
75
75
|
background: boolean;
|
|
@@ -64,12 +64,12 @@ declare const __VLS_export: import("vue").DefineComponent<ImageCropperProps, {
|
|
|
64
64
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<ImageCropperProps> & Readonly<{}>, {
|
|
65
65
|
src: string;
|
|
66
66
|
center: boolean;
|
|
67
|
+
modal: boolean;
|
|
67
68
|
responsive: boolean;
|
|
68
69
|
restore: boolean;
|
|
69
70
|
checkCrossOrigin: boolean;
|
|
70
71
|
checkOrientation: boolean;
|
|
71
72
|
crossorigin: "" | "anonymous" | "use-credentials";
|
|
72
|
-
modal: boolean;
|
|
73
73
|
guides: boolean;
|
|
74
74
|
highlight: boolean;
|
|
75
75
|
background: boolean;
|
|
@@ -49,11 +49,11 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
49
49
|
label: string;
|
|
50
50
|
required: boolean;
|
|
51
51
|
name: string;
|
|
52
|
+
gap: string;
|
|
52
53
|
placeholder: string;
|
|
53
54
|
labelDetail: string;
|
|
54
55
|
placeholderDetail: string;
|
|
55
56
|
requiredDetail: boolean;
|
|
56
|
-
gap: string;
|
|
57
57
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
58
58
|
declare const _default: typeof __VLS_export;
|
|
59
59
|
export default _default;
|
|
@@ -49,11 +49,11 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
49
49
|
label: string;
|
|
50
50
|
required: boolean;
|
|
51
51
|
name: string;
|
|
52
|
+
gap: string;
|
|
52
53
|
placeholder: string;
|
|
53
54
|
labelDetail: string;
|
|
54
55
|
placeholderDetail: string;
|
|
55
56
|
requiredDetail: boolean;
|
|
56
|
-
gap: string;
|
|
57
57
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
58
58
|
declare const _default: typeof __VLS_export;
|
|
59
59
|
export default _default;
|
|
@@ -35,8 +35,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
35
35
|
fullHeight: boolean;
|
|
36
36
|
name: string;
|
|
37
37
|
limit: number;
|
|
38
|
-
disabledErrorMessage: boolean;
|
|
39
38
|
accept: string;
|
|
39
|
+
disabledErrorMessage: boolean;
|
|
40
40
|
labelIcon: string;
|
|
41
41
|
disabledDrop: boolean;
|
|
42
42
|
column: boolean;
|
|
@@ -35,8 +35,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
35
35
|
fullHeight: boolean;
|
|
36
36
|
name: string;
|
|
37
37
|
limit: number;
|
|
38
|
-
disabledErrorMessage: boolean;
|
|
39
38
|
accept: string;
|
|
39
|
+
disabledErrorMessage: boolean;
|
|
40
40
|
labelIcon: string;
|
|
41
41
|
disabledDrop: boolean;
|
|
42
42
|
column: boolean;
|
|
@@ -22,8 +22,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
22
22
|
}>, {
|
|
23
23
|
id: string;
|
|
24
24
|
name: string;
|
|
25
|
-
new: boolean;
|
|
26
25
|
disabledForgotPassword: boolean;
|
|
26
|
+
new: boolean;
|
|
27
27
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
28
|
declare const _default: typeof __VLS_export;
|
|
29
29
|
export default _default;
|
|
@@ -22,8 +22,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
22
22
|
}>, {
|
|
23
23
|
id: string;
|
|
24
24
|
name: string;
|
|
25
|
-
new: boolean;
|
|
26
25
|
disabledForgotPassword: boolean;
|
|
26
|
+
new: boolean;
|
|
27
27
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
28
28
|
declare const _default: typeof __VLS_export;
|
|
29
29
|
export default _default;
|
|
@@ -24,8 +24,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
24
24
|
}) => any) | undefined;
|
|
25
25
|
}>, {
|
|
26
26
|
title: string;
|
|
27
|
-
disabledForgotPassword: boolean;
|
|
28
27
|
confirmText: string;
|
|
28
|
+
disabledForgotPassword: boolean;
|
|
29
29
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
30
30
|
declare const _default: typeof __VLS_export;
|
|
31
31
|
export default _default;
|
|
@@ -24,8 +24,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
24
24
|
}) => any) | undefined;
|
|
25
25
|
}>, {
|
|
26
26
|
title: string;
|
|
27
|
-
disabledForgotPassword: boolean;
|
|
28
27
|
confirmText: string;
|
|
28
|
+
disabledForgotPassword: boolean;
|
|
29
29
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
30
30
|
declare const _default: typeof __VLS_export;
|
|
31
31
|
export default _default;
|
|
@@ -32,8 +32,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
32
32
|
}>, {
|
|
33
33
|
title: string;
|
|
34
34
|
mode: "login" | "secure";
|
|
35
|
-
disabledForgotPassword: boolean;
|
|
36
35
|
confirmText: string;
|
|
36
|
+
disabledForgotPassword: boolean;
|
|
37
37
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
38
38
|
declare const _default: typeof __VLS_export;
|
|
39
39
|
export default _default;
|
|
@@ -32,8 +32,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
32
32
|
}>, {
|
|
33
33
|
title: string;
|
|
34
34
|
mode: "login" | "secure";
|
|
35
|
-
disabledForgotPassword: boolean;
|
|
36
35
|
confirmText: string;
|
|
36
|
+
disabledForgotPassword: boolean;
|
|
37
37
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
38
38
|
declare const _default: typeof __VLS_export;
|
|
39
39
|
export default _default;
|
|
@@ -14,8 +14,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
14
14
|
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
15
15
|
onComplete?: ((data?: any) => any) | undefined;
|
|
16
16
|
}>, {
|
|
17
|
-
phone: string;
|
|
18
17
|
confirmedText: string;
|
|
18
|
+
phone: string;
|
|
19
19
|
phoneLabel: string;
|
|
20
20
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
21
|
declare const _default: typeof __VLS_export;
|
|
@@ -14,8 +14,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
14
14
|
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
15
15
|
onComplete?: ((data?: any) => any) | undefined;
|
|
16
16
|
}>, {
|
|
17
|
-
phone: string;
|
|
18
17
|
confirmedText: string;
|
|
18
|
+
phone: string;
|
|
19
19
|
phoneLabel: string;
|
|
20
20
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
21
|
declare const _default: typeof __VLS_export;
|
package/package.json
CHANGED
|
File without changes
|