pukaad-ui-lib 1.419.0 → 1.421.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-content/card-content-menu.vue +23 -0
- package/dist/runtime/components/drawer/drawer-post-content.d.vue.ts +4 -1
- package/dist/runtime/components/drawer/drawer-post-content.vue +19 -5
- package/dist/runtime/components/drawer/drawer-post-content.vue.d.ts +4 -1
- 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-tag-modal.d.vue.ts +52 -0
- package/dist/runtime/components/input/input-tag-modal.vue +307 -0
- package/dist/runtime/components/input/input-tag-modal.vue.d.ts +52 -0
- package/dist/runtime/components/input/input-text-field.d.vue.ts +1 -1
- package/dist/runtime/components/input/input-text-field.vue.d.ts +1 -1
- package/dist/runtime/components/input/input-textarea.d.vue.ts +1 -1
- package/dist/runtime/components/input/input-textarea.vue.d.ts +1 -1
- package/dist/runtime/components/picker/picker-option-menu/picker-option-menu-user.vue +49 -1
- package/dist/runtime/composables/useApi.js +4 -0
- package/package.json +1 -1
- package/dist/runtime/components/input/input-select-tag.d.vue.ts +0 -3
- package/dist/runtime/components/input/input-select-tag.vue +0 -57
- package/dist/runtime/components/input/input-select-tag.vue.d.ts +0 -3
package/dist/module.json
CHANGED
|
@@ -108,6 +108,29 @@ const actions = computed(() => {
|
|
|
108
108
|
}
|
|
109
109
|
return result;
|
|
110
110
|
}
|
|
111
|
+
if (props.state === "expired") {
|
|
112
|
+
return [
|
|
113
|
+
...props.hidePreview ? [] : [{ name: "preview", icon: "lucide:tv-minimal-play" }],
|
|
114
|
+
{ name: "renew", icon: "lucide:history" },
|
|
115
|
+
{
|
|
116
|
+
name: "delete",
|
|
117
|
+
icon: "lucide:trash-2",
|
|
118
|
+
variant: "default",
|
|
119
|
+
color: "destructive"
|
|
120
|
+
}
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
if (props.state === "trash") {
|
|
124
|
+
return [
|
|
125
|
+
{ name: "recover", icon: "lucide:rotate-ccw" },
|
|
126
|
+
{
|
|
127
|
+
name: "delete",
|
|
128
|
+
icon: "lucide:trash-2",
|
|
129
|
+
variant: "default",
|
|
130
|
+
color: "destructive"
|
|
131
|
+
}
|
|
132
|
+
];
|
|
133
|
+
}
|
|
111
134
|
if (props.state === "draft") {
|
|
112
135
|
return [
|
|
113
136
|
{
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { SelectPlaceItem } from "../input/input-select-place.vue.js";
|
|
2
2
|
import type { MentionItem } from "../input/input-mention.vue.js";
|
|
3
|
-
interface IFileItem {
|
|
3
|
+
export interface IFileItem {
|
|
4
4
|
file?: File;
|
|
5
5
|
url: string;
|
|
6
6
|
description?: string;
|
|
7
7
|
duration?: string;
|
|
8
|
+
mediaType?: "image" | "video";
|
|
8
9
|
}
|
|
9
10
|
interface SelectedTag {
|
|
10
11
|
id: string;
|
|
@@ -24,6 +25,7 @@ export interface DrawerPostContentItem {
|
|
|
24
25
|
}
|
|
25
26
|
export interface DrawerPostContentProps {
|
|
26
27
|
item?: DrawerPostContentItem;
|
|
28
|
+
state?: "personal" | "business";
|
|
27
29
|
}
|
|
28
30
|
type __VLS_Props = DrawerPostContentProps;
|
|
29
31
|
declare const modelValue: import("vue").ModelRef<boolean | undefined, string, boolean | undefined, boolean | undefined>;
|
|
@@ -41,6 +43,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
41
43
|
onSaveDraft?: ((data: unknown) => any) | undefined;
|
|
42
44
|
}>, {
|
|
43
45
|
item: DrawerPostContentItem;
|
|
46
|
+
state: "personal" | "business";
|
|
44
47
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
45
48
|
declare const _default: typeof __VLS_export;
|
|
46
49
|
export default _default;
|
|
@@ -171,7 +171,8 @@ const props = defineProps({
|
|
|
171
171
|
tags: [],
|
|
172
172
|
mentions: [],
|
|
173
173
|
places: []
|
|
174
|
-
}) }
|
|
174
|
+
}) },
|
|
175
|
+
state: { type: String, required: false, default: "personal" }
|
|
175
176
|
});
|
|
176
177
|
const modelValue = defineModel({ type: Boolean });
|
|
177
178
|
const currentId = ref(void 0);
|
|
@@ -197,6 +198,7 @@ const hasCoverFile = computed(() => coverFiles.value.length > 0);
|
|
|
197
198
|
const coverFileType = computed(() => {
|
|
198
199
|
const first = coverFiles.value[0];
|
|
199
200
|
if (!first) return null;
|
|
201
|
+
if (first.mediaType) return first.mediaType;
|
|
200
202
|
return first.file?.type?.startsWith("video") ? "video" : "image";
|
|
201
203
|
});
|
|
202
204
|
const coverAccept = computed(() => {
|
|
@@ -319,6 +321,10 @@ const onClose = async () => {
|
|
|
319
321
|
modelValue.value = false;
|
|
320
322
|
};
|
|
321
323
|
const onSaveDraft = async () => {
|
|
324
|
+
if (props.state === "business") {
|
|
325
|
+
await submitContent("DRAFT");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
322
328
|
if (isSubmitting.value) return;
|
|
323
329
|
isSubmitting.value = true;
|
|
324
330
|
try {
|
|
@@ -331,13 +337,15 @@ const onSaveDraft = async () => {
|
|
|
331
337
|
file: f.file,
|
|
332
338
|
url: f.url,
|
|
333
339
|
description: f.description,
|
|
334
|
-
duration: f.duration
|
|
340
|
+
duration: f.duration,
|
|
341
|
+
mediaType: f.mediaType
|
|
335
342
|
})),
|
|
336
343
|
posterFiles: posterFiles.value.map((f) => ({
|
|
337
344
|
file: f.file,
|
|
338
345
|
url: f.url,
|
|
339
346
|
description: f.description,
|
|
340
|
-
duration: f.duration
|
|
347
|
+
duration: f.duration,
|
|
348
|
+
mediaType: f.mediaType
|
|
341
349
|
})),
|
|
342
350
|
description: description.value,
|
|
343
351
|
tags: JSON.parse(JSON.stringify(selectedTags.value)),
|
|
@@ -373,6 +381,10 @@ const parseDurationToSeconds = (duration) => {
|
|
|
373
381
|
if (Number.isNaN(m) || Number.isNaN(s)) return 0;
|
|
374
382
|
return m * 60 + s;
|
|
375
383
|
};
|
|
384
|
+
const endpointMap = {
|
|
385
|
+
personal: "/personal/contents",
|
|
386
|
+
business: "/business/contents"
|
|
387
|
+
};
|
|
376
388
|
const submitContent = async (status) => {
|
|
377
389
|
if (isSubmitting.value) return;
|
|
378
390
|
if (!hasCoverText.value && !hasCoverFile.value) {
|
|
@@ -414,7 +426,8 @@ const submitContent = async (status) => {
|
|
|
414
426
|
place_ids: selectedPlaces.value.map((p) => p.id),
|
|
415
427
|
status
|
|
416
428
|
};
|
|
417
|
-
const
|
|
429
|
+
const baseEndpoint = endpointMap[props.state];
|
|
430
|
+
const endpoint = isEditMode.value ? `${baseEndpoint}/${currentId.value}` : baseEndpoint;
|
|
418
431
|
const method = isEditMode.value ? "PATCH" : "POST";
|
|
419
432
|
const res = await api(
|
|
420
433
|
endpoint,
|
|
@@ -426,7 +439,8 @@ const submitContent = async (status) => {
|
|
|
426
439
|
if (currentId.value?.startsWith("draft_")) {
|
|
427
440
|
await deleteDraft(currentId.value);
|
|
428
441
|
}
|
|
429
|
-
emit("
|
|
442
|
+
if (status === "DRAFT") emit("saveDraft", res.data);
|
|
443
|
+
else emit("success", res.data);
|
|
430
444
|
modelValue.value = false;
|
|
431
445
|
} catch (e) {
|
|
432
446
|
const apiErr = e;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { SelectPlaceItem } from "../input/input-select-place.vue.js";
|
|
2
2
|
import type { MentionItem } from "../input/input-mention.vue.js";
|
|
3
|
-
interface IFileItem {
|
|
3
|
+
export interface IFileItem {
|
|
4
4
|
file?: File;
|
|
5
5
|
url: string;
|
|
6
6
|
description?: string;
|
|
7
7
|
duration?: string;
|
|
8
|
+
mediaType?: "image" | "video";
|
|
8
9
|
}
|
|
9
10
|
interface SelectedTag {
|
|
10
11
|
id: string;
|
|
@@ -24,6 +25,7 @@ export interface DrawerPostContentItem {
|
|
|
24
25
|
}
|
|
25
26
|
export interface DrawerPostContentProps {
|
|
26
27
|
item?: DrawerPostContentItem;
|
|
28
|
+
state?: "personal" | "business";
|
|
27
29
|
}
|
|
28
30
|
type __VLS_Props = DrawerPostContentProps;
|
|
29
31
|
declare const modelValue: import("vue").ModelRef<boolean | undefined, string, boolean | undefined, boolean | undefined>;
|
|
@@ -41,6 +43,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
|
|
|
41
43
|
onSaveDraft?: ((data: unknown) => any) | undefined;
|
|
42
44
|
}>, {
|
|
43
45
|
item: DrawerPostContentItem;
|
|
46
|
+
state: "personal" | "business";
|
|
44
47
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
45
48
|
declare const _default: typeof __VLS_export;
|
|
46
49
|
export default _default;
|
|
@@ -64,11 +64,11 @@ 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
|
+
crossorigin: "" | "anonymous" | "use-credentials";
|
|
67
68
|
responsive: boolean;
|
|
68
69
|
restore: boolean;
|
|
69
70
|
checkCrossOrigin: boolean;
|
|
70
71
|
checkOrientation: boolean;
|
|
71
|
-
crossorigin: "" | "anonymous" | "use-credentials";
|
|
72
72
|
modal: boolean;
|
|
73
73
|
guides: boolean;
|
|
74
74
|
highlight: boolean;
|
|
@@ -64,11 +64,11 @@ 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
|
+
crossorigin: "" | "anonymous" | "use-credentials";
|
|
67
68
|
responsive: boolean;
|
|
68
69
|
restore: boolean;
|
|
69
70
|
checkCrossOrigin: boolean;
|
|
70
71
|
checkOrientation: boolean;
|
|
71
|
-
crossorigin: "" | "anonymous" | "use-credentials";
|
|
72
72
|
modal: boolean;
|
|
73
73
|
guides: boolean;
|
|
74
74
|
highlight: boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { InputTagModalOption, InputTagModalProps } from "../../../types/components/input/input-tag-modal.js";
|
|
2
|
+
interface InputTextFieldExpose {
|
|
3
|
+
setErrors?: (errors: string[]) => void;
|
|
4
|
+
validate?: () => Promise<unknown>;
|
|
5
|
+
}
|
|
6
|
+
type __VLS_Props = InputTagModalProps;
|
|
7
|
+
type __VLS_ModelProps = {
|
|
8
|
+
modelValue?: string | number | null;
|
|
9
|
+
};
|
|
10
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
12
|
+
setErrors: (errors: string[]) => void;
|
|
13
|
+
validate: () => Promise<unknown>;
|
|
14
|
+
reload: () => Promise<void>;
|
|
15
|
+
fieldRef: import("vue").Ref<{
|
|
16
|
+
setErrors?: ((errors: string[]) => void) | undefined;
|
|
17
|
+
validate?: (() => Promise<unknown>) | undefined;
|
|
18
|
+
} | null, InputTextFieldExpose | {
|
|
19
|
+
setErrors?: ((errors: string[]) => void) | undefined;
|
|
20
|
+
validate?: (() => Promise<unknown>) | undefined;
|
|
21
|
+
} | null>;
|
|
22
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
23
|
+
"update:modelValue": (value: string | number | null) => any;
|
|
24
|
+
} & {
|
|
25
|
+
select: (option: InputTagModalOption) => any;
|
|
26
|
+
clear: () => any;
|
|
27
|
+
open: () => any;
|
|
28
|
+
"fetch-error": (error: unknown) => any;
|
|
29
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
30
|
+
onSelect?: ((option: InputTagModalOption) => any) | undefined;
|
|
31
|
+
onClear?: (() => any) | undefined;
|
|
32
|
+
"onUpdate:modelValue"?: ((value: string | number | null) => any) | undefined;
|
|
33
|
+
onOpen?: (() => any) | undefined;
|
|
34
|
+
"onFetch-error"?: ((error: unknown) => any) | undefined;
|
|
35
|
+
}>, {
|
|
36
|
+
title: string;
|
|
37
|
+
loading: boolean;
|
|
38
|
+
disabled: boolean;
|
|
39
|
+
pageSize: number;
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
required: boolean;
|
|
43
|
+
options: InputTagModalOption[];
|
|
44
|
+
labelKey: string;
|
|
45
|
+
placeholder: string;
|
|
46
|
+
disabledErrorMessage: boolean;
|
|
47
|
+
valueKey: string;
|
|
48
|
+
searchPlaceholder: string;
|
|
49
|
+
clearable: boolean;
|
|
50
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
51
|
+
declare const _default: typeof __VLS_export;
|
|
52
|
+
export default _default;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="cn('w-full', props.class)">
|
|
3
|
+
<InputTextField
|
|
4
|
+
ref="fieldRef"
|
|
5
|
+
v-model="displayValue"
|
|
6
|
+
:id="props.id"
|
|
7
|
+
:name="props.name"
|
|
8
|
+
:label="props.label"
|
|
9
|
+
:placeholder="props.placeholder"
|
|
10
|
+
:description="props.description"
|
|
11
|
+
:rules="props.rules"
|
|
12
|
+
:required="props.required"
|
|
13
|
+
:disabled="props.disabled"
|
|
14
|
+
:disabled-error-message="props.disabledErrorMessage"
|
|
15
|
+
:icon-append="appendIcon"
|
|
16
|
+
class="cursor-pointer"
|
|
17
|
+
readonly
|
|
18
|
+
@click="onOpenModal"
|
|
19
|
+
@click-icon-append="onAppendClick"
|
|
20
|
+
/>
|
|
21
|
+
|
|
22
|
+
<Modal v-model="isOpenModal" :title="props.title">
|
|
23
|
+
<template #title>
|
|
24
|
+
<span class="font-title-medium-prominent text-black">
|
|
25
|
+
{{ props.title }}
|
|
26
|
+
</span>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<div class="flex w-full flex-col gap-[16px]">
|
|
30
|
+
<InputSearch
|
|
31
|
+
v-model="searchKeyword"
|
|
32
|
+
name="tag-modal-search"
|
|
33
|
+
:placeholder="props.searchPlaceholder"
|
|
34
|
+
icon="lucide:search"
|
|
35
|
+
icon-position="right"
|
|
36
|
+
disabled-error-message
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<div
|
|
40
|
+
v-if="isInitialLoading"
|
|
41
|
+
class="flex h-[392px] items-center justify-center"
|
|
42
|
+
>
|
|
43
|
+
<Icon
|
|
44
|
+
name="lucide:loader-circle"
|
|
45
|
+
:size="24"
|
|
46
|
+
class="animate-spin text-primary"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div
|
|
51
|
+
v-else-if="filteredOptions.length > 0"
|
|
52
|
+
class="flex max-h-[592px] flex-wrap content-start gap-[8px] overflow-y-auto pr-[4px]"
|
|
53
|
+
@scroll.passive="onListScroll"
|
|
54
|
+
>
|
|
55
|
+
<Chip
|
|
56
|
+
v-for="option in filteredOptions"
|
|
57
|
+
:key="optionKey(option)"
|
|
58
|
+
:color="isSelected(option) ? 'primary' : 'bright'"
|
|
59
|
+
class="cursor-pointer transition-colors"
|
|
60
|
+
@click="onSelectOption(option)"
|
|
61
|
+
>
|
|
62
|
+
{{ option.label }}
|
|
63
|
+
</Chip>
|
|
64
|
+
|
|
65
|
+
<div
|
|
66
|
+
v-if="isLoadingMore"
|
|
67
|
+
class="flex w-full items-center justify-center py-[8px]"
|
|
68
|
+
>
|
|
69
|
+
<Icon
|
|
70
|
+
name="lucide:loader-circle"
|
|
71
|
+
:size="20"
|
|
72
|
+
class="animate-spin text-primary"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div
|
|
78
|
+
v-else
|
|
79
|
+
class="flex h-[392px] flex-col items-center justify-center gap-[8px]"
|
|
80
|
+
>
|
|
81
|
+
<Icon name="pukaad:page-not-found" class="h-[100px] w-[121px]" />
|
|
82
|
+
<div class="font-body-large text-gray">ไม่พบข้อมูล</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</Modal>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<script setup>
|
|
90
|
+
import { computed, nextTick, reactive, ref, watch } from "vue";
|
|
91
|
+
import { useDebounceFn } from "@vueuse/core";
|
|
92
|
+
import { cn } from "#pukaad-ui/runtime/plugins/shadcn";
|
|
93
|
+
defineOptions({ inheritAttrs: false });
|
|
94
|
+
const props = defineProps({
|
|
95
|
+
class: { type: null, required: false },
|
|
96
|
+
id: { type: String, required: false, default: "input-tag-modal" },
|
|
97
|
+
name: { type: String, required: false, default: "input-tag-modal" },
|
|
98
|
+
label: { type: String, required: false },
|
|
99
|
+
placeholder: { type: String, required: false, default: "\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E41\u0E17\u0E47\u0E01" },
|
|
100
|
+
searchPlaceholder: { type: String, required: false, default: "\u0E04\u0E49\u0E19\u0E2B\u0E32\u0E41\u0E17\u0E47\u0E01" },
|
|
101
|
+
title: { type: String, required: false, default: "\u0E41\u0E17\u0E47\u0E01\u0E17\u0E35\u0E48\u0E40\u0E01\u0E35\u0E48\u0E22\u0E27\u0E02\u0E49\u0E2D\u0E07" },
|
|
102
|
+
description: { type: String, required: false },
|
|
103
|
+
rules: { type: [Object, String, Function], required: false },
|
|
104
|
+
options: { type: Array, required: false, default: () => [] },
|
|
105
|
+
fetchFn: { type: Function, required: false },
|
|
106
|
+
valueKey: { type: String, required: false, default: "value" },
|
|
107
|
+
labelKey: { type: String, required: false, default: "label" },
|
|
108
|
+
pageSize: { type: Number, required: false, default: 30 },
|
|
109
|
+
required: { type: Boolean, required: false, default: false },
|
|
110
|
+
disabled: { type: Boolean, required: false, default: false },
|
|
111
|
+
disabledErrorMessage: { type: Boolean, required: false, default: false },
|
|
112
|
+
clearable: { type: Boolean, required: false, default: true },
|
|
113
|
+
loading: { type: Boolean, required: false, default: false }
|
|
114
|
+
});
|
|
115
|
+
const emit = defineEmits(["open", "select", "clear", "fetch-error"]);
|
|
116
|
+
const modelValue = defineModel({ type: [String, Number, null], ...{ default: null } });
|
|
117
|
+
const fieldRef = ref(null);
|
|
118
|
+
const isOpenModal = ref(false);
|
|
119
|
+
const searchKeyword = ref("");
|
|
120
|
+
const selectedOptionCache = ref(null);
|
|
121
|
+
const suppressSearchWatch = ref(false);
|
|
122
|
+
const remoteState = reactive({
|
|
123
|
+
data: [],
|
|
124
|
+
page: 1,
|
|
125
|
+
hasMore: true,
|
|
126
|
+
loading: false,
|
|
127
|
+
lastSearch: "",
|
|
128
|
+
generation: 0
|
|
129
|
+
});
|
|
130
|
+
const hasValue = computed(
|
|
131
|
+
() => modelValue.value !== null && modelValue.value !== void 0 && modelValue.value !== ""
|
|
132
|
+
);
|
|
133
|
+
const optionKey = (option) => `${typeof option.value}:${String(option.value)}`;
|
|
134
|
+
const mergeOptions = (current, incoming) => {
|
|
135
|
+
const map = new Map(current.map((option) => [optionKey(option), option]));
|
|
136
|
+
incoming.forEach((option) => map.set(optionKey(option), option));
|
|
137
|
+
return Array.from(map.values());
|
|
138
|
+
};
|
|
139
|
+
const mapRemoteOption = (item) => {
|
|
140
|
+
if (typeof item !== "object" || item === null) return null;
|
|
141
|
+
const record = item;
|
|
142
|
+
const rawValue = record[props.valueKey];
|
|
143
|
+
const rawLabel = record[props.labelKey];
|
|
144
|
+
if (rawValue === void 0 || rawValue === null || rawLabel === void 0) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
value: typeof rawValue === "number" ? rawValue : String(rawValue),
|
|
149
|
+
label: String(rawLabel)
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
const resolvedOptions = computed(
|
|
153
|
+
() => props.fetchFn ? remoteState.data : props.options
|
|
154
|
+
);
|
|
155
|
+
const filteredOptions = computed(() => {
|
|
156
|
+
if (props.fetchFn) return resolvedOptions.value;
|
|
157
|
+
const query = searchKeyword.value.trim().toLocaleLowerCase("th");
|
|
158
|
+
if (!query) return resolvedOptions.value;
|
|
159
|
+
return resolvedOptions.value.filter(
|
|
160
|
+
(option) => option.label.toLocaleLowerCase("th").includes(query)
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
const knownOptions = computed(
|
|
164
|
+
() => mergeOptions(props.options, remoteState.data)
|
|
165
|
+
);
|
|
166
|
+
watch(
|
|
167
|
+
[modelValue, knownOptions],
|
|
168
|
+
([value, options]) => {
|
|
169
|
+
if (value === null || value === void 0 || value === "") {
|
|
170
|
+
selectedOptionCache.value = null;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const found = options.find((option) => option.value === value);
|
|
174
|
+
if (found) {
|
|
175
|
+
selectedOptionCache.value = found;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (selectedOptionCache.value?.value !== value) {
|
|
179
|
+
selectedOptionCache.value = null;
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{ immediate: true, deep: true }
|
|
183
|
+
);
|
|
184
|
+
const displayValue = computed({
|
|
185
|
+
get: () => selectedOptionCache.value?.label ?? "",
|
|
186
|
+
set: () => void 0
|
|
187
|
+
});
|
|
188
|
+
const appendIcon = computed(
|
|
189
|
+
() => hasValue.value && props.clearable ? "lucide:x" : "lucide:chevron-down"
|
|
190
|
+
);
|
|
191
|
+
const isInitialLoading = computed(
|
|
192
|
+
() => (props.loading || remoteState.loading) && filteredOptions.value.length === 0
|
|
193
|
+
);
|
|
194
|
+
const isLoadingMore = computed(
|
|
195
|
+
() => !!props.fetchFn && remoteState.loading && remoteState.data.length > 0
|
|
196
|
+
);
|
|
197
|
+
const fetchRemote = async (reset = false, search = searchKeyword.value.trim()) => {
|
|
198
|
+
if (!props.fetchFn) return;
|
|
199
|
+
if (!reset && (remoteState.loading || !remoteState.hasMore)) return;
|
|
200
|
+
let generation = remoteState.generation;
|
|
201
|
+
if (reset) {
|
|
202
|
+
generation = remoteState.generation + 1;
|
|
203
|
+
remoteState.generation = generation;
|
|
204
|
+
remoteState.page = 1;
|
|
205
|
+
remoteState.data = [];
|
|
206
|
+
remoteState.hasMore = true;
|
|
207
|
+
remoteState.loading = false;
|
|
208
|
+
remoteState.lastSearch = search;
|
|
209
|
+
}
|
|
210
|
+
if (remoteState.loading) return;
|
|
211
|
+
const page = remoteState.page;
|
|
212
|
+
remoteState.loading = true;
|
|
213
|
+
try {
|
|
214
|
+
const response = await props.fetchFn(
|
|
215
|
+
page,
|
|
216
|
+
props.pageSize,
|
|
217
|
+
search || void 0
|
|
218
|
+
);
|
|
219
|
+
if (generation !== remoteState.generation) return;
|
|
220
|
+
const mapped = (response.data ?? []).map(mapRemoteOption).filter((option) => option !== null);
|
|
221
|
+
remoteState.data = reset ? mergeOptions([], mapped) : mergeOptions(remoteState.data, mapped);
|
|
222
|
+
const totalPages = Number(response.meta?.total_pages ?? 1);
|
|
223
|
+
remoteState.page = page + 1;
|
|
224
|
+
remoteState.hasMore = page < totalPages;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (generation === remoteState.generation) {
|
|
227
|
+
remoteState.hasMore = false;
|
|
228
|
+
emit("fetch-error", error);
|
|
229
|
+
console.error("InputTagModal: Failed to fetch options:", error);
|
|
230
|
+
}
|
|
231
|
+
} finally {
|
|
232
|
+
if (generation === remoteState.generation) {
|
|
233
|
+
remoteState.loading = false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const debouncedRemoteSearch = useDebounceFn(() => {
|
|
238
|
+
void fetchRemote(true);
|
|
239
|
+
}, 300);
|
|
240
|
+
watch(searchKeyword, () => {
|
|
241
|
+
if (!props.fetchFn || !isOpenModal.value || suppressSearchWatch.value) return;
|
|
242
|
+
debouncedRemoteSearch();
|
|
243
|
+
});
|
|
244
|
+
watch(
|
|
245
|
+
() => props.fetchFn,
|
|
246
|
+
() => {
|
|
247
|
+
remoteState.generation += 1;
|
|
248
|
+
remoteState.data = [];
|
|
249
|
+
remoteState.page = 1;
|
|
250
|
+
remoteState.hasMore = true;
|
|
251
|
+
remoteState.loading = false;
|
|
252
|
+
remoteState.lastSearch = "";
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
const onOpenModal = async () => {
|
|
256
|
+
if (props.disabled) return;
|
|
257
|
+
suppressSearchWatch.value = true;
|
|
258
|
+
searchKeyword.value = "";
|
|
259
|
+
isOpenModal.value = true;
|
|
260
|
+
emit("open");
|
|
261
|
+
await nextTick();
|
|
262
|
+
suppressSearchWatch.value = false;
|
|
263
|
+
if (props.fetchFn && (remoteState.data.length === 0 || remoteState.lastSearch !== "")) {
|
|
264
|
+
await fetchRemote(true, "");
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const onAppendClick = () => {
|
|
268
|
+
if (props.disabled) return;
|
|
269
|
+
if (hasValue.value && props.clearable) {
|
|
270
|
+
modelValue.value = null;
|
|
271
|
+
selectedOptionCache.value = null;
|
|
272
|
+
emit("clear");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
void onOpenModal();
|
|
276
|
+
};
|
|
277
|
+
const onSelectOption = (option) => {
|
|
278
|
+
modelValue.value = option.value;
|
|
279
|
+
selectedOptionCache.value = option;
|
|
280
|
+
emit("select", option);
|
|
281
|
+
isOpenModal.value = false;
|
|
282
|
+
};
|
|
283
|
+
const isSelected = (option) => option.value === modelValue.value;
|
|
284
|
+
const onListScroll = (event) => {
|
|
285
|
+
if (!props.fetchFn || remoteState.loading || !remoteState.hasMore) return;
|
|
286
|
+
const target = event.currentTarget;
|
|
287
|
+
if (!target) return;
|
|
288
|
+
const distanceFromBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
|
|
289
|
+
if (distanceFromBottom < 48) {
|
|
290
|
+
void fetchRemote();
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const reload = async () => {
|
|
294
|
+
if (!props.fetchFn) return;
|
|
295
|
+
await fetchRemote(true);
|
|
296
|
+
};
|
|
297
|
+
const setErrors = (errors) => {
|
|
298
|
+
fieldRef.value?.setErrors?.(errors);
|
|
299
|
+
};
|
|
300
|
+
const validate = async () => await fieldRef.value?.validate?.();
|
|
301
|
+
defineExpose({
|
|
302
|
+
setErrors,
|
|
303
|
+
validate,
|
|
304
|
+
reload,
|
|
305
|
+
fieldRef
|
|
306
|
+
});
|
|
307
|
+
</script>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { InputTagModalOption, InputTagModalProps } from "../../../types/components/input/input-tag-modal.js";
|
|
2
|
+
interface InputTextFieldExpose {
|
|
3
|
+
setErrors?: (errors: string[]) => void;
|
|
4
|
+
validate?: () => Promise<unknown>;
|
|
5
|
+
}
|
|
6
|
+
type __VLS_Props = InputTagModalProps;
|
|
7
|
+
type __VLS_ModelProps = {
|
|
8
|
+
modelValue?: string | number | null;
|
|
9
|
+
};
|
|
10
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
12
|
+
setErrors: (errors: string[]) => void;
|
|
13
|
+
validate: () => Promise<unknown>;
|
|
14
|
+
reload: () => Promise<void>;
|
|
15
|
+
fieldRef: import("vue").Ref<{
|
|
16
|
+
setErrors?: ((errors: string[]) => void) | undefined;
|
|
17
|
+
validate?: (() => Promise<unknown>) | undefined;
|
|
18
|
+
} | null, InputTextFieldExpose | {
|
|
19
|
+
setErrors?: ((errors: string[]) => void) | undefined;
|
|
20
|
+
validate?: (() => Promise<unknown>) | undefined;
|
|
21
|
+
} | null>;
|
|
22
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
23
|
+
"update:modelValue": (value: string | number | null) => any;
|
|
24
|
+
} & {
|
|
25
|
+
select: (option: InputTagModalOption) => any;
|
|
26
|
+
clear: () => any;
|
|
27
|
+
open: () => any;
|
|
28
|
+
"fetch-error": (error: unknown) => any;
|
|
29
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
30
|
+
onSelect?: ((option: InputTagModalOption) => any) | undefined;
|
|
31
|
+
onClear?: (() => any) | undefined;
|
|
32
|
+
"onUpdate:modelValue"?: ((value: string | number | null) => any) | undefined;
|
|
33
|
+
onOpen?: (() => any) | undefined;
|
|
34
|
+
"onFetch-error"?: ((error: unknown) => any) | undefined;
|
|
35
|
+
}>, {
|
|
36
|
+
title: string;
|
|
37
|
+
loading: boolean;
|
|
38
|
+
disabled: boolean;
|
|
39
|
+
pageSize: number;
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
required: boolean;
|
|
43
|
+
options: InputTagModalOption[];
|
|
44
|
+
labelKey: string;
|
|
45
|
+
placeholder: string;
|
|
46
|
+
disabledErrorMessage: boolean;
|
|
47
|
+
valueKey: string;
|
|
48
|
+
searchPlaceholder: string;
|
|
49
|
+
clearable: boolean;
|
|
50
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
51
|
+
declare const _default: typeof __VLS_export;
|
|
52
|
+
export default _default;
|
|
@@ -59,8 +59,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
59
59
|
id: string;
|
|
60
60
|
name: string;
|
|
61
61
|
required: boolean;
|
|
62
|
-
readonly: boolean;
|
|
63
62
|
limit: number;
|
|
63
|
+
readonly: boolean;
|
|
64
64
|
disabledErrorMessage: boolean;
|
|
65
65
|
disabledBorder: boolean;
|
|
66
66
|
showCounter: boolean;
|
|
@@ -59,8 +59,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
59
59
|
id: string;
|
|
60
60
|
name: string;
|
|
61
61
|
required: boolean;
|
|
62
|
-
readonly: boolean;
|
|
63
62
|
limit: number;
|
|
63
|
+
readonly: boolean;
|
|
64
64
|
disabledErrorMessage: boolean;
|
|
65
65
|
disabledBorder: boolean;
|
|
66
66
|
showCounter: boolean;
|
|
@@ -47,8 +47,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
47
47
|
fullHeight: boolean;
|
|
48
48
|
fullWidth: boolean;
|
|
49
49
|
rows: number;
|
|
50
|
-
readonly: boolean;
|
|
51
50
|
limit: number;
|
|
51
|
+
readonly: boolean;
|
|
52
52
|
disabledErrorMessage: boolean;
|
|
53
53
|
disabledBorder: boolean;
|
|
54
54
|
showCounter: boolean;
|
|
@@ -47,8 +47,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
47
47
|
fullHeight: boolean;
|
|
48
48
|
fullWidth: boolean;
|
|
49
49
|
rows: number;
|
|
50
|
-
readonly: boolean;
|
|
51
50
|
limit: number;
|
|
51
|
+
readonly: boolean;
|
|
52
52
|
disabledErrorMessage: boolean;
|
|
53
53
|
disabledBorder: boolean;
|
|
54
54
|
showCounter: boolean;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
<script setup>
|
|
27
27
|
import { computed, ref } from "vue";
|
|
28
|
-
const emit = defineEmits(["profile-edit", "profile-share", "profile-follower-delete", "profile-block", "blog-pin", "blog-unpin", "blog-edit", "blog-delete", "report-blog", "report-place", "report-profile", "report-content", "delete-content", "content-delete", "delete", "report", "report-review", "place-close-duplicate", "review-edit", "review-delete", "review-reply", "reply-edit", "reply-delete", "archive", "profile-name-updated", "report-submit"]);
|
|
28
|
+
const emit = defineEmits(["profile-edit", "profile-share", "profile-follower-delete", "profile-block", "blog-pin", "blog-unpin", "blog-edit", "blog-delete", "report-blog", "report-place", "report-profile", "report-content", "report-spotlight", "report-tag", "report-label", "delete-content", "content-delete", "delete", "report", "report-review", "place-close-duplicate", "review-edit", "review-delete", "review-reply", "reply-edit", "reply-delete", "archive", "profile-name-updated", "report-submit"]);
|
|
29
29
|
const props = defineProps({
|
|
30
30
|
state: { type: String, required: false },
|
|
31
31
|
profileDetail: { type: Object, required: false },
|
|
@@ -173,6 +173,39 @@ const menu = [
|
|
|
173
173
|
emit("report");
|
|
174
174
|
}
|
|
175
175
|
},
|
|
176
|
+
{
|
|
177
|
+
label: "\u0E23\u0E32\u0E22\u0E07\u0E32\u0E19\u0E2B\u0E31\u0E27\u0E02\u0E49\u0E2D\u0E41\u0E19\u0E30\u0E19\u0E33\u0E19\u0E35\u0E49",
|
|
178
|
+
name: "report-spotlight",
|
|
179
|
+
icon: "lucide:flag",
|
|
180
|
+
action: () => {
|
|
181
|
+
reportState.value = "spotlight";
|
|
182
|
+
isOpenModalReport.value = true;
|
|
183
|
+
emit("report-spotlight");
|
|
184
|
+
emit("report");
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
label: "\u0E23\u0E32\u0E22\u0E07\u0E32\u0E19\u0E41\u0E17\u0E47\u0E01\u0E19\u0E35\u0E49",
|
|
189
|
+
name: "report-tag",
|
|
190
|
+
icon: "lucide:flag",
|
|
191
|
+
action: () => {
|
|
192
|
+
reportState.value = "tag";
|
|
193
|
+
isOpenModalReport.value = true;
|
|
194
|
+
emit("report-tag");
|
|
195
|
+
emit("report");
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
label: "\u0E23\u0E32\u0E22\u0E07\u0E32\u0E19\u0E2B\u0E21\u0E27\u0E14\u0E2B\u0E21\u0E39\u0E48\u0E19\u0E35\u0E49",
|
|
200
|
+
name: "report-label",
|
|
201
|
+
icon: "lucide:flag",
|
|
202
|
+
action: () => {
|
|
203
|
+
reportState.value = "label";
|
|
204
|
+
isOpenModalReport.value = true;
|
|
205
|
+
emit("report-label");
|
|
206
|
+
emit("report");
|
|
207
|
+
}
|
|
208
|
+
},
|
|
176
209
|
{
|
|
177
210
|
label: "\u0E25\u0E1A\u0E40\u0E19\u0E37\u0E49\u0E2D\u0E2B\u0E32\u0E19\u0E35\u0E49",
|
|
178
211
|
name: "delete-content",
|
|
@@ -302,6 +335,21 @@ const items = computed(() => {
|
|
|
302
335
|
(item) => ["report-content"].includes(item.name)
|
|
303
336
|
);
|
|
304
337
|
}
|
|
338
|
+
if (props.state === "spotlight") {
|
|
339
|
+
return menu.filter(
|
|
340
|
+
(item) => ["report-spotlight"].includes(item.name)
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
if (props.state === "tag") {
|
|
344
|
+
return menu.filter(
|
|
345
|
+
(item) => ["report-tag"].includes(item.name)
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
if (props.state === "label") {
|
|
349
|
+
return menu.filter(
|
|
350
|
+
(item) => ["report-label"].includes(item.name)
|
|
351
|
+
);
|
|
352
|
+
}
|
|
305
353
|
if (props.state === "delete-content") {
|
|
306
354
|
return menu.filter(
|
|
307
355
|
(item) => ["delete-content", "content-delete"].includes(item.name)
|
|
@@ -3,6 +3,7 @@ export const useApi = () => {
|
|
|
3
3
|
const config = useRuntimeConfig();
|
|
4
4
|
const baseURL = config.public.BASE_URL_API;
|
|
5
5
|
const provinceIdState = useState("pukaad:province-id", () => null);
|
|
6
|
+
const businessProfileId = useCookie("PUKAAD_BUSINESS_PROFILE_ID");
|
|
6
7
|
const configProvinceId = config.public.APP_PROVINCE_ID;
|
|
7
8
|
const { APP_TYPE } = config.public;
|
|
8
9
|
const secId = useCookie(
|
|
@@ -22,6 +23,9 @@ export const useApi = () => {
|
|
|
22
23
|
if (pid) {
|
|
23
24
|
headers.set("X-Province-ID", String(pid));
|
|
24
25
|
}
|
|
26
|
+
if (businessProfileId.value) {
|
|
27
|
+
headers.set("X-Business-Profile-ID", businessProfileId.value);
|
|
28
|
+
}
|
|
25
29
|
options.headers = headers;
|
|
26
30
|
},
|
|
27
31
|
onResponseError({ response }) {
|
package/package.json
CHANGED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
-
declare const _default: typeof __VLS_export;
|
|
3
|
-
export default _default;
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!-- <InputTextField
|
|
3
|
-
:name="props.name"
|
|
4
|
-
:label="props.label"
|
|
5
|
-
:placeholder="props.placeholder"
|
|
6
|
-
:full-width="props.fullWidth"
|
|
7
|
-
:width="props.width"
|
|
8
|
-
:height="props.height"
|
|
9
|
-
:required="props.required"
|
|
10
|
-
:disabled="props.disabled"
|
|
11
|
-
:readonly="props.readonly"
|
|
12
|
-
:append-icon="props.appendIcon"
|
|
13
|
-
@click="isOpenModal = true"
|
|
14
|
-
@clickAppendIcon="isOpenModal = true"
|
|
15
|
-
@reset="isOpenModal = true"
|
|
16
|
-
reset
|
|
17
|
-
v-model="selectedTag"
|
|
18
|
-
/>
|
|
19
|
-
<Modal
|
|
20
|
-
v-model="isOpenModal"
|
|
21
|
-
title="แท็กที่เกี่ยวข้อง"
|
|
22
|
-
:gap="16"
|
|
23
|
-
:width="604"
|
|
24
|
-
:height="670"
|
|
25
|
-
>
|
|
26
|
-
<div class="space-y-[16px]">
|
|
27
|
-
<InputSearch v-model="search" placeholder="ค้นหาแท็ก" full-width />
|
|
28
|
-
<Divider :height="0" />
|
|
29
|
-
<div
|
|
30
|
-
v-if="filteredTags.length > 0"
|
|
31
|
-
class="flex flex-wrap gap-[8px] h-[526px] overflow-y-auto"
|
|
32
|
-
>
|
|
33
|
-
<Chip
|
|
34
|
-
v-for="tag in filteredTags"
|
|
35
|
-
:key="tag"
|
|
36
|
-
size="small"
|
|
37
|
-
:color="selectedTag === tag ? 'primary' : 'default'"
|
|
38
|
-
class="cursor-pointer"
|
|
39
|
-
@click="onSelectTag(tag)"
|
|
40
|
-
>
|
|
41
|
-
{{ tag }}
|
|
42
|
-
</Chip>
|
|
43
|
-
</div>
|
|
44
|
-
<div
|
|
45
|
-
v-else
|
|
46
|
-
class="flex flex-col gap-[8px] items-center justify-center h-[526px]"
|
|
47
|
-
>
|
|
48
|
-
<Icon name="pukaad:page-not-found" size="100" />
|
|
49
|
-
<div class="text-gray font-body-large">ไม่พบข้อมูล</div>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
</Modal> -->
|
|
53
|
-
</template>
|
|
54
|
-
|
|
55
|
-
<script setup>
|
|
56
|
-
|
|
57
|
-
</script>
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
-
declare const _default: typeof __VLS_export;
|
|
3
|
-
export default _default;
|