pukaad-ui-lib 1.345.0 → 1.347.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/data-table.d.vue.ts +9 -3
- package/dist/runtime/components/data-table.vue +46 -34
- package/dist/runtime/components/data-table.vue.d.ts +9 -3
- package/dist/runtime/components/drawer/drawer-notification.d.vue.ts +33 -0
- package/dist/runtime/components/drawer/drawer-notification.vue +260 -0
- package/dist/runtime/components/drawer/drawer-notification.vue.d.ts +33 -0
- package/dist/runtime/components/image/image-cropper.d.vue.ts +2 -2
- package/dist/runtime/components/image/image-cropper.vue.d.ts +2 -2
- package/dist/runtime/components/ui/native-select/NativeSelectOptGroup.d.vue.ts +2 -2
- package/dist/runtime/components/ui/native-select/NativeSelectOptGroup.vue.d.ts +2 -2
- package/dist/runtime/components/ui/native-select/NativeSelectOption.d.vue.ts +2 -2
- package/dist/runtime/components/ui/native-select/NativeSelectOption.vue.d.ts +2 -2
- package/dist/runtime/composables/store-notification.d.ts +31 -0
- package/dist/runtime/composables/store-notification.js +61 -0
- package/dist/runtime/composables/useRealtime.d.ts +10 -0
- package/dist/runtime/composables/useRealtime.js +127 -0
- package/dist/runtime/utils/websocket.d.ts +1 -0
- package/dist/runtime/utils/websocket.js +15 -0
- package/package.json +1 -1
- /package/dist/runtime/assets/svg/socials/{WhatsApp.svg → Whatsapp.svg} +0 -0
package/dist/module.json
CHANGED
|
@@ -34,7 +34,11 @@ declare var __VLS_13: {
|
|
|
34
34
|
index: number;
|
|
35
35
|
}, __VLS_106: `item-${string}`, __VLS_107: {
|
|
36
36
|
item: Record<string, any>;
|
|
37
|
-
}, __VLS_109: {
|
|
37
|
+
}, __VLS_109: {
|
|
38
|
+
item: Record<string, any>;
|
|
39
|
+
index: number;
|
|
40
|
+
colspan: number;
|
|
41
|
+
}, __VLS_111: {}, __VLS_119: {};
|
|
38
42
|
type __VLS_Slots = {} & {
|
|
39
43
|
[K in NonNullable<typeof __VLS_41>]?: (props: typeof __VLS_42) => any;
|
|
40
44
|
} & {
|
|
@@ -46,9 +50,11 @@ type __VLS_Slots = {} & {
|
|
|
46
50
|
} & {
|
|
47
51
|
item?: (props: typeof __VLS_97) => any;
|
|
48
52
|
} & {
|
|
49
|
-
|
|
53
|
+
'row-banner'?: (props: typeof __VLS_109) => any;
|
|
54
|
+
} & {
|
|
55
|
+
empty?: (props: typeof __VLS_111) => any;
|
|
50
56
|
} & {
|
|
51
|
-
'empty-content'?: (props: typeof
|
|
57
|
+
'empty-content'?: (props: typeof __VLS_119) => any;
|
|
52
58
|
};
|
|
53
59
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
54
60
|
"update:page": (value: number) => any;
|
|
@@ -76,6 +76,9 @@ const itemHeaders = computed(() => {
|
|
|
76
76
|
);
|
|
77
77
|
return header;
|
|
78
78
|
});
|
|
79
|
+
const totalColumns = computed(
|
|
80
|
+
() => itemHeaders.value.length + (props.selectable ? 1 : 0)
|
|
81
|
+
);
|
|
79
82
|
const sortedItems = computed(() => {
|
|
80
83
|
return props.items.slice().sort((a, b) => {
|
|
81
84
|
if (!sortkey.value) return 0;
|
|
@@ -197,49 +200,58 @@ const getJustifyClass = (alignment = "start") => {
|
|
|
197
200
|
|
|
198
201
|
<!-- Data Rows -->
|
|
199
202
|
<template v-else-if="sortedItems.length > 0">
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
:data-state="
|
|
203
|
+
<template v-for="(item, i_body) in sortedItems" :key="i_body">
|
|
204
|
+
<ShadTableRow
|
|
205
|
+
class="cursor-pointer"
|
|
206
|
+
:data-state="
|
|
205
207
|
props.selectable && isSelected(item) ? 'selected' : void 0
|
|
206
208
|
"
|
|
207
|
-
|
|
208
|
-
>
|
|
209
|
-
<!-- Checkbox per row -->
|
|
210
|
-
<ShadTableCell v-if="props.selectable" @click.stop>
|
|
211
|
-
<div class="flex items-center justify-center">
|
|
212
|
-
<InputCheckbox
|
|
213
|
-
:model-value="isSelected(item)"
|
|
214
|
-
@update:model-value="() => toggleRow(item)"
|
|
215
|
-
/>
|
|
216
|
-
</div>
|
|
217
|
-
</ShadTableCell>
|
|
218
|
-
|
|
219
|
-
<slot
|
|
220
|
-
name="item"
|
|
221
|
-
:item="item"
|
|
222
|
-
:header="itemHeaders"
|
|
223
|
-
:index="i_body"
|
|
209
|
+
@click="emit('click-row', item, i_body)"
|
|
224
210
|
>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
211
|
+
<!-- Checkbox per row -->
|
|
212
|
+
<ShadTableCell v-if="props.selectable" @click.stop>
|
|
213
|
+
<div class="flex items-center justify-center">
|
|
214
|
+
<InputCheckbox
|
|
215
|
+
:model-value="isSelected(item)"
|
|
216
|
+
@update:model-value="() => toggleRow(item)"
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
</ShadTableCell>
|
|
220
|
+
|
|
221
|
+
<slot
|
|
222
|
+
name="item"
|
|
223
|
+
:item="item"
|
|
224
|
+
:header="itemHeaders"
|
|
225
|
+
:index="i_body"
|
|
226
|
+
>
|
|
227
|
+
<ShadTableCell
|
|
228
|
+
v-for="(header, i_header) in itemHeaders"
|
|
229
|
+
:key="i_header"
|
|
230
|
+
:class="
|
|
229
231
|
getAlignmentClass(header.alignment || props.cellAlignment)
|
|
230
232
|
"
|
|
231
|
-
|
|
233
|
+
:style="{
|
|
232
234
|
width: header.width ? typeof header.width === 'number' ? `${header.width}px` : header.width : void 0,
|
|
233
235
|
minWidth: header.minWidth ? typeof header.minWidth === 'number' ? `${header.minWidth}px` : header.minWidth : void 0,
|
|
234
236
|
maxWidth: header.maxWidth ? typeof header.maxWidth === 'number' ? `${header.maxWidth}px` : header.maxWidth : header.width ? typeof header.width === 'number' ? `${header.width}px` : header.width : header.minWidth ? typeof header.minWidth === 'number' ? `${header.minWidth}px` : header.minWidth : void 0
|
|
235
237
|
}"
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
>
|
|
239
|
+
<slot :name="`item-${header.key}`" :item="item">
|
|
240
|
+
{{ item[header.key] }}
|
|
241
|
+
</slot>
|
|
242
|
+
</ShadTableCell>
|
|
243
|
+
</slot>
|
|
244
|
+
</ShadTableRow>
|
|
245
|
+
|
|
246
|
+
<!-- แถวเสริมเต็มความกว้าง แทรกใต้แถวนี้ — ใช้กับกรณีเช่น banner "กำลังโอนสิทธิ์" (Figma 2779-278256) -->
|
|
247
|
+
<!-- ไม่ระบุ slot นี้ = ไม่มีอะไร render เพิ่ม, consumer เป็นคนคุม v-if ว่าจะโชว์กับ item ไหน -->
|
|
248
|
+
<slot
|
|
249
|
+
name="row-banner"
|
|
250
|
+
:item="item"
|
|
251
|
+
:index="i_body"
|
|
252
|
+
:colspan="totalColumns"
|
|
253
|
+
/>
|
|
254
|
+
</template>
|
|
243
255
|
</template>
|
|
244
256
|
|
|
245
257
|
<!-- Empty State -->
|
|
@@ -34,7 +34,11 @@ declare var __VLS_13: {
|
|
|
34
34
|
index: number;
|
|
35
35
|
}, __VLS_106: `item-${string}`, __VLS_107: {
|
|
36
36
|
item: Record<string, any>;
|
|
37
|
-
}, __VLS_109: {
|
|
37
|
+
}, __VLS_109: {
|
|
38
|
+
item: Record<string, any>;
|
|
39
|
+
index: number;
|
|
40
|
+
colspan: number;
|
|
41
|
+
}, __VLS_111: {}, __VLS_119: {};
|
|
38
42
|
type __VLS_Slots = {} & {
|
|
39
43
|
[K in NonNullable<typeof __VLS_41>]?: (props: typeof __VLS_42) => any;
|
|
40
44
|
} & {
|
|
@@ -46,9 +50,11 @@ type __VLS_Slots = {} & {
|
|
|
46
50
|
} & {
|
|
47
51
|
item?: (props: typeof __VLS_97) => any;
|
|
48
52
|
} & {
|
|
49
|
-
|
|
53
|
+
'row-banner'?: (props: typeof __VLS_109) => any;
|
|
54
|
+
} & {
|
|
55
|
+
empty?: (props: typeof __VLS_111) => any;
|
|
50
56
|
} & {
|
|
51
|
-
'empty-content'?: (props: typeof
|
|
57
|
+
'empty-content'?: (props: typeof __VLS_119) => any;
|
|
52
58
|
};
|
|
53
59
|
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
54
60
|
"update:page": (value: number) => any;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface NotificationActor {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
avatar: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface NotificationRef {
|
|
7
|
+
type: string;
|
|
8
|
+
id: string;
|
|
9
|
+
}
|
|
10
|
+
export interface NotificationItem {
|
|
11
|
+
id: string;
|
|
12
|
+
category: string;
|
|
13
|
+
action: string;
|
|
14
|
+
message: string;
|
|
15
|
+
ref?: NotificationRef;
|
|
16
|
+
is_read: boolean;
|
|
17
|
+
actor?: NotificationActor;
|
|
18
|
+
grouped_count?: number;
|
|
19
|
+
created_at: string;
|
|
20
|
+
}
|
|
21
|
+
declare const modelValue: import("vue").ModelRef<boolean | undefined, string, boolean | undefined, boolean | undefined>;
|
|
22
|
+
type __VLS_ModelProps = {
|
|
23
|
+
modelValue?: typeof modelValue['value'];
|
|
24
|
+
};
|
|
25
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
26
|
+
select: (item: NotificationItem) => any;
|
|
27
|
+
"update:modelValue": (value: boolean | undefined) => any;
|
|
28
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
29
|
+
onSelect?: ((item: NotificationItem) => any) | undefined;
|
|
30
|
+
"onUpdate:modelValue"?: ((value: boolean | undefined) => any) | undefined;
|
|
31
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
32
|
+
declare const _default: typeof __VLS_export;
|
|
33
|
+
export default _default;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Drawer v-model="modelValue" title="การแจ้งเตือน" class="w-[748px]">
|
|
3
|
+
<div class="flex justify-between items-center mb-[16px]">
|
|
4
|
+
<ShadTabs v-model="selectedTab" class="w-full">
|
|
5
|
+
<div class="flex justify-between items-center w-full">
|
|
6
|
+
<ShadTabsList>
|
|
7
|
+
<ShadTabsTrigger
|
|
8
|
+
v-for="tab in tabs"
|
|
9
|
+
:key="tab.value"
|
|
10
|
+
:value="tab.value"
|
|
11
|
+
class="px-4 py-2"
|
|
12
|
+
>
|
|
13
|
+
<Icon name="lucide:layers" />
|
|
14
|
+
<div class="flex">
|
|
15
|
+
{{ tab.label }}
|
|
16
|
+
<div
|
|
17
|
+
v-if="
|
|
18
|
+
tab.value === 'general' ? hasUnreadGeneral : hasUnreadSystem
|
|
19
|
+
"
|
|
20
|
+
class="bg-destructive rounded-full w-[10px] h-[10px] m-[2px] shrink-0"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
</ShadTabsTrigger>
|
|
24
|
+
</ShadTabsList>
|
|
25
|
+
|
|
26
|
+
<div
|
|
27
|
+
v-if="notifications.length > 0"
|
|
28
|
+
class="text-primary font-body-medium cursor-pointer"
|
|
29
|
+
@click="markAllRead"
|
|
30
|
+
>
|
|
31
|
+
อ่านทั้งหมด
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<ShadTabsContent value="general" class="flex flex-col gap-[8px]">
|
|
36
|
+
<template v-if="isLoading && notifications.length === 0">
|
|
37
|
+
<div class="flex justify-center py-[40px]">
|
|
38
|
+
<Icon
|
|
39
|
+
name="lucide:loader-2"
|
|
40
|
+
size="24"
|
|
41
|
+
class="animate-spin text-gray"
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<template v-else-if="notifications.length === 0">
|
|
47
|
+
<div
|
|
48
|
+
class="flex flex-col items-center justify-center py-[40px] gap-[8px]"
|
|
49
|
+
>
|
|
50
|
+
<Icon name="lucide:bell-off" size="40" class="text-gray" />
|
|
51
|
+
<div class="font-body-medium text-gray">ยังไม่มีการแจ้งเตือน</div>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<template v-else>
|
|
56
|
+
<Card
|
|
57
|
+
v-for="item in notifications"
|
|
58
|
+
:key="item.id"
|
|
59
|
+
variant="outline"
|
|
60
|
+
class="cursor-pointer transition-colors"
|
|
61
|
+
:class="{ 'bg-bright': !item.is_read }"
|
|
62
|
+
@click="onClickNotification(item)"
|
|
63
|
+
>
|
|
64
|
+
<div class="flex gap-[8px] items-center">
|
|
65
|
+
<div class="flex gap-[8px] items-center w-full">
|
|
66
|
+
<Avatar :size="30" :src="item.actor?.avatar ?? void 0" />
|
|
67
|
+
<div class="flex flex-col gap-[4px]">
|
|
68
|
+
<div class="font-body-medium">
|
|
69
|
+
<span
|
|
70
|
+
v-if="item.actor"
|
|
71
|
+
class="font-body-medium-prominent"
|
|
72
|
+
>
|
|
73
|
+
{{ item.actor.name }}
|
|
74
|
+
</span>
|
|
75
|
+
{{ item.message }}
|
|
76
|
+
<span
|
|
77
|
+
v-if="item.grouped_count && item.grouped_count > 1"
|
|
78
|
+
class="text-gray font-body-small"
|
|
79
|
+
>
|
|
80
|
+
({{ item.grouped_count }} คน)
|
|
81
|
+
</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="text-gray font-body-small">
|
|
84
|
+
{{ formatRelativeTime(item.created_at) }}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div
|
|
89
|
+
v-if="!item.is_read"
|
|
90
|
+
class="bg-primary rounded-full w-[10px] h-[10px] shrink-0"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</Card>
|
|
94
|
+
</template>
|
|
95
|
+
</ShadTabsContent>
|
|
96
|
+
|
|
97
|
+
<ShadTabsContent value="system" class="flex flex-col gap-[8px]">
|
|
98
|
+
<template v-if="isLoading && notifications.length === 0">
|
|
99
|
+
<div class="flex justify-center py-[40px]">
|
|
100
|
+
<Icon
|
|
101
|
+
name="lucide:loader-2"
|
|
102
|
+
size="24"
|
|
103
|
+
class="animate-spin text-gray"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
</template>
|
|
107
|
+
|
|
108
|
+
<template v-else-if="notifications.length === 0">
|
|
109
|
+
<div
|
|
110
|
+
class="flex flex-col items-center justify-center py-[40px] gap-[8px]"
|
|
111
|
+
>
|
|
112
|
+
<Icon name="lucide:bell-off" size="40" class="text-gray" />
|
|
113
|
+
<div class="font-body-medium text-gray">ยังไม่มีการแจ้งเตือนระบบ</div>
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<template v-else>
|
|
118
|
+
<Card
|
|
119
|
+
v-for="item in notifications"
|
|
120
|
+
:key="item.id"
|
|
121
|
+
variant="outline"
|
|
122
|
+
class="cursor-pointer transition-colors"
|
|
123
|
+
:class="{ 'bg-bright': !item.is_read }"
|
|
124
|
+
@click="onClickNotification(item)"
|
|
125
|
+
>
|
|
126
|
+
<div class="flex gap-[8px] items-center">
|
|
127
|
+
<div class="flex gap-[8px] items-center w-full">
|
|
128
|
+
<div class="flex items-center justify-center w-[30px] h-[30px] rounded-full bg-muted shrink-0">
|
|
129
|
+
<Icon name="lucide:bell" size="16" class="text-primary" />
|
|
130
|
+
</div>
|
|
131
|
+
<div class="flex flex-col gap-[4px]">
|
|
132
|
+
<div class="font-body-medium">{{ item.message }}</div>
|
|
133
|
+
<div class="text-gray font-body-small">
|
|
134
|
+
{{ formatRelativeTime(item.created_at) }}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div
|
|
139
|
+
v-if="!item.is_read"
|
|
140
|
+
class="bg-primary rounded-full w-[10px] h-[10px] shrink-0"
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
</Card>
|
|
144
|
+
</template>
|
|
145
|
+
</ShadTabsContent>
|
|
146
|
+
</ShadTabs>
|
|
147
|
+
</div>
|
|
148
|
+
</Drawer>
|
|
149
|
+
</template>
|
|
150
|
+
|
|
151
|
+
<script setup>
|
|
152
|
+
import { ref, computed, watch } from "vue";
|
|
153
|
+
import { useApi } from "../../composables/useApi";
|
|
154
|
+
import { useNotificationStore } from "../../composables/store-notification";
|
|
155
|
+
const api = useApi();
|
|
156
|
+
const modelValue = defineModel({ type: Boolean });
|
|
157
|
+
const notificationStore = useNotificationStore();
|
|
158
|
+
const emit = defineEmits(["select"]);
|
|
159
|
+
const selectedTab = ref("general");
|
|
160
|
+
const tabs = [
|
|
161
|
+
{ value: "general", label: "\u0E17\u0E31\u0E48\u0E27\u0E44\u0E1B" },
|
|
162
|
+
{ value: "system", label: "\u0E23\u0E30\u0E1A\u0E1A" }
|
|
163
|
+
];
|
|
164
|
+
const generalNotifications = ref([]);
|
|
165
|
+
const systemNotifications = ref([]);
|
|
166
|
+
const isLoading = ref(false);
|
|
167
|
+
const notifications = computed(
|
|
168
|
+
() => selectedTab.value === "system" ? systemNotifications.value : generalNotifications.value
|
|
169
|
+
);
|
|
170
|
+
const hasUnreadGeneral = computed(
|
|
171
|
+
() => generalNotifications.value.some((n) => !n.is_read)
|
|
172
|
+
);
|
|
173
|
+
const hasUnreadSystem = computed(
|
|
174
|
+
() => systemNotifications.value.some((n) => !n.is_read)
|
|
175
|
+
);
|
|
176
|
+
const fetchNotifications = async () => {
|
|
177
|
+
try {
|
|
178
|
+
isLoading.value = true;
|
|
179
|
+
if (selectedTab.value === "system") {
|
|
180
|
+
const response = await api(`/notifications`, {
|
|
181
|
+
params: { page: 1, page_size: 50, category: "system" }
|
|
182
|
+
});
|
|
183
|
+
systemNotifications.value = response.data;
|
|
184
|
+
} else {
|
|
185
|
+
const response = await api(`/notifications`, {
|
|
186
|
+
params: { page: 1, page_size: 50 }
|
|
187
|
+
});
|
|
188
|
+
generalNotifications.value = response.data.filter(
|
|
189
|
+
(n) => n.category !== "system"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error("Fetch notifications error:", err);
|
|
194
|
+
} finally {
|
|
195
|
+
isLoading.value = false;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const markAsRead = async (item) => {
|
|
199
|
+
if (item.is_read) return;
|
|
200
|
+
try {
|
|
201
|
+
await api(`/notifications/${item.id}/read`, { method: "PATCH" });
|
|
202
|
+
item.is_read = true;
|
|
203
|
+
const totalUnread = generalNotifications.value.filter((n) => !n.is_read).length + systemNotifications.value.filter((n) => !n.is_read).length;
|
|
204
|
+
notificationStore.setUnreadCount(totalUnread);
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const markAllRead = async () => {
|
|
209
|
+
try {
|
|
210
|
+
await api(`/notifications/read-all`, { method: "PATCH" });
|
|
211
|
+
generalNotifications.value.forEach((n) => n.is_read = true);
|
|
212
|
+
systemNotifications.value.forEach((n) => n.is_read = true);
|
|
213
|
+
notificationStore.setUnreadCount(0);
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const onClickNotification = (item) => {
|
|
218
|
+
markAsRead(item);
|
|
219
|
+
emit("select", item);
|
|
220
|
+
};
|
|
221
|
+
const formatRelativeTime = (dateStr) => {
|
|
222
|
+
const now = /* @__PURE__ */ new Date();
|
|
223
|
+
const date = new Date(dateStr);
|
|
224
|
+
const diffMs = now.getTime() - date.getTime();
|
|
225
|
+
const diffSec = Math.floor(diffMs / 1e3);
|
|
226
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
227
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
228
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
229
|
+
if (diffSec < 60) return "\u0E40\u0E21\u0E37\u0E48\u0E2D\u0E0B\u0E31\u0E01\u0E04\u0E23\u0E39\u0E48";
|
|
230
|
+
if (diffMin < 60) return `${diffMin} \u0E19\u0E32\u0E17\u0E35\u0E17\u0E35\u0E48\u0E41\u0E25\u0E49\u0E27`;
|
|
231
|
+
if (diffHour < 24) return `${diffHour} \u0E0A\u0E31\u0E48\u0E27\u0E42\u0E21\u0E07\u0E17\u0E35\u0E48\u0E41\u0E25\u0E49\u0E27`;
|
|
232
|
+
if (diffDay < 7) return `${diffDay} \u0E27\u0E31\u0E19\u0E17\u0E35\u0E48\u0E41\u0E25\u0E49\u0E27`;
|
|
233
|
+
return date.toLocaleDateString("th-TH", {
|
|
234
|
+
day: "numeric",
|
|
235
|
+
month: "short",
|
|
236
|
+
year: "numeric"
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
watch(
|
|
240
|
+
() => notificationStore.isOpenDrawer,
|
|
241
|
+
(val) => {
|
|
242
|
+
if (val) {
|
|
243
|
+
fetchNotifications();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
watch(selectedTab, () => {
|
|
248
|
+
if (notificationStore.isOpenDrawer) {
|
|
249
|
+
fetchNotifications();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
watch(
|
|
253
|
+
() => notificationStore.unreadCount,
|
|
254
|
+
() => {
|
|
255
|
+
if (notificationStore.isOpenDrawer) {
|
|
256
|
+
fetchNotifications();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface NotificationActor {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
avatar: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface NotificationRef {
|
|
7
|
+
type: string;
|
|
8
|
+
id: string;
|
|
9
|
+
}
|
|
10
|
+
export interface NotificationItem {
|
|
11
|
+
id: string;
|
|
12
|
+
category: string;
|
|
13
|
+
action: string;
|
|
14
|
+
message: string;
|
|
15
|
+
ref?: NotificationRef;
|
|
16
|
+
is_read: boolean;
|
|
17
|
+
actor?: NotificationActor;
|
|
18
|
+
grouped_count?: number;
|
|
19
|
+
created_at: string;
|
|
20
|
+
}
|
|
21
|
+
declare const modelValue: import("vue").ModelRef<boolean | undefined, string, boolean | undefined, boolean | undefined>;
|
|
22
|
+
type __VLS_ModelProps = {
|
|
23
|
+
modelValue?: typeof modelValue['value'];
|
|
24
|
+
};
|
|
25
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
26
|
+
select: (item: NotificationItem) => any;
|
|
27
|
+
"update:modelValue": (value: boolean | undefined) => any;
|
|
28
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
29
|
+
onSelect?: ((item: NotificationItem) => any) | undefined;
|
|
30
|
+
"onUpdate:modelValue"?: ((value: boolean | undefined) => any) | undefined;
|
|
31
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
32
|
+
declare const _default: typeof __VLS_export;
|
|
33
|
+
export default _default;
|
|
@@ -64,15 +64,15 @@ 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
|
+
background: boolean;
|
|
68
|
+
modal: boolean;
|
|
67
69
|
responsive: boolean;
|
|
68
70
|
restore: boolean;
|
|
69
71
|
checkCrossOrigin: boolean;
|
|
70
72
|
checkOrientation: boolean;
|
|
71
73
|
crossorigin: "" | "anonymous" | "use-credentials";
|
|
72
|
-
modal: boolean;
|
|
73
74
|
guides: boolean;
|
|
74
75
|
highlight: boolean;
|
|
75
|
-
background: boolean;
|
|
76
76
|
autoCrop: boolean;
|
|
77
77
|
movable: boolean;
|
|
78
78
|
rotatable: boolean;
|
|
@@ -64,15 +64,15 @@ 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
|
+
background: boolean;
|
|
68
|
+
modal: boolean;
|
|
67
69
|
responsive: boolean;
|
|
68
70
|
restore: boolean;
|
|
69
71
|
checkCrossOrigin: boolean;
|
|
70
72
|
checkOrientation: boolean;
|
|
71
73
|
crossorigin: "" | "anonymous" | "use-credentials";
|
|
72
|
-
modal: boolean;
|
|
73
74
|
guides: boolean;
|
|
74
75
|
highlight: boolean;
|
|
75
|
-
background: boolean;
|
|
76
76
|
autoCrop: boolean;
|
|
77
77
|
movable: boolean;
|
|
78
78
|
rotatable: boolean;
|
|
@@ -14,7 +14,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
|
|
|
14
14
|
dir?: string | undefined | undefined;
|
|
15
15
|
draggable?: (boolean | "true" | "false") | undefined;
|
|
16
16
|
enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
|
|
17
|
-
enterKeyHint?: "search" | "done" | "next" | "
|
|
17
|
+
enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
|
|
18
18
|
hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
|
|
19
19
|
id?: string | undefined | undefined;
|
|
20
20
|
inert?: (boolean | "true" | "false") | undefined;
|
|
@@ -208,7 +208,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
|
|
|
208
208
|
dir?: string | undefined | undefined;
|
|
209
209
|
draggable?: (boolean | "true" | "false") | undefined;
|
|
210
210
|
enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
|
|
211
|
-
enterKeyHint?: "search" | "done" | "next" | "
|
|
211
|
+
enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
|
|
212
212
|
hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
|
|
213
213
|
id?: string | undefined | undefined;
|
|
214
214
|
inert?: (boolean | "true" | "false") | undefined;
|
|
@@ -14,7 +14,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
|
|
|
14
14
|
dir?: string | undefined | undefined;
|
|
15
15
|
draggable?: (boolean | "true" | "false") | undefined;
|
|
16
16
|
enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
|
|
17
|
-
enterKeyHint?: "search" | "done" | "next" | "
|
|
17
|
+
enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
|
|
18
18
|
hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
|
|
19
19
|
id?: string | undefined | undefined;
|
|
20
20
|
inert?: (boolean | "true" | "false") | undefined;
|
|
@@ -208,7 +208,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
|
|
|
208
208
|
dir?: string | undefined | undefined;
|
|
209
209
|
draggable?: (boolean | "true" | "false") | undefined;
|
|
210
210
|
enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
|
|
211
|
-
enterKeyHint?: "search" | "done" | "next" | "
|
|
211
|
+
enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
|
|
212
212
|
hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
|
|
213
213
|
id?: string | undefined | undefined;
|
|
214
214
|
inert?: (boolean | "true" | "false") | undefined;
|
|
@@ -16,7 +16,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
|
|
|
16
16
|
dir?: string | undefined | undefined;
|
|
17
17
|
draggable?: (boolean | "true" | "false") | undefined;
|
|
18
18
|
enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
|
|
19
|
-
enterKeyHint?: "search" | "done" | "next" | "
|
|
19
|
+
enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
|
|
20
20
|
hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
|
|
21
21
|
id?: string | undefined | undefined;
|
|
22
22
|
inert?: (boolean | "true" | "false") | undefined;
|
|
@@ -212,7 +212,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
|
|
|
212
212
|
dir?: string | undefined | undefined;
|
|
213
213
|
draggable?: (boolean | "true" | "false") | undefined;
|
|
214
214
|
enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
|
|
215
|
-
enterKeyHint?: "search" | "done" | "next" | "
|
|
215
|
+
enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
|
|
216
216
|
hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
|
|
217
217
|
id?: string | undefined | undefined;
|
|
218
218
|
inert?: (boolean | "true" | "false") | undefined;
|
|
@@ -16,7 +16,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
|
|
|
16
16
|
dir?: string | undefined | undefined;
|
|
17
17
|
draggable?: (boolean | "true" | "false") | undefined;
|
|
18
18
|
enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
|
|
19
|
-
enterKeyHint?: "search" | "done" | "next" | "
|
|
19
|
+
enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
|
|
20
20
|
hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
|
|
21
21
|
id?: string | undefined | undefined;
|
|
22
22
|
inert?: (boolean | "true" | "false") | undefined;
|
|
@@ -212,7 +212,7 @@ declare const __VLS_base: import("vue").DefineComponent<{
|
|
|
212
212
|
dir?: string | undefined | undefined;
|
|
213
213
|
draggable?: (boolean | "true" | "false") | undefined;
|
|
214
214
|
enterkeyhint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
|
|
215
|
-
enterKeyHint?: "search" | "done" | "next" | "
|
|
215
|
+
enterKeyHint?: "search" | "done" | "next" | "send" | "enter" | "go" | "previous" | undefined;
|
|
216
216
|
hidden?: "" | (boolean | "true" | "false") | "hidden" | "until-found" | undefined;
|
|
217
217
|
id?: string | undefined | undefined;
|
|
218
218
|
inert?: (boolean | "true" | "false") | undefined;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const useNotificationStore: import("pinia").StoreDefinition<"notification", Pick<{
|
|
2
|
+
unreadCount: import("vue").Ref<number, number>;
|
|
3
|
+
isOpenDrawer: import("vue").Ref<boolean, boolean>;
|
|
4
|
+
fetchUnreadCount: () => Promise<void>;
|
|
5
|
+
initRealtime: () => void;
|
|
6
|
+
closeRealtime: () => void;
|
|
7
|
+
setUnreadCount: (count: number) => void;
|
|
8
|
+
openDrawer: () => void;
|
|
9
|
+
closeDrawer: () => void;
|
|
10
|
+
toggleDrawer: () => void;
|
|
11
|
+
}, "unreadCount" | "isOpenDrawer">, Pick<{
|
|
12
|
+
unreadCount: import("vue").Ref<number, number>;
|
|
13
|
+
isOpenDrawer: import("vue").Ref<boolean, boolean>;
|
|
14
|
+
fetchUnreadCount: () => Promise<void>;
|
|
15
|
+
initRealtime: () => void;
|
|
16
|
+
closeRealtime: () => void;
|
|
17
|
+
setUnreadCount: (count: number) => void;
|
|
18
|
+
openDrawer: () => void;
|
|
19
|
+
closeDrawer: () => void;
|
|
20
|
+
toggleDrawer: () => void;
|
|
21
|
+
}, never>, Pick<{
|
|
22
|
+
unreadCount: import("vue").Ref<number, number>;
|
|
23
|
+
isOpenDrawer: import("vue").Ref<boolean, boolean>;
|
|
24
|
+
fetchUnreadCount: () => Promise<void>;
|
|
25
|
+
initRealtime: () => void;
|
|
26
|
+
closeRealtime: () => void;
|
|
27
|
+
setUnreadCount: (count: number) => void;
|
|
28
|
+
openDrawer: () => void;
|
|
29
|
+
closeDrawer: () => void;
|
|
30
|
+
toggleDrawer: () => void;
|
|
31
|
+
}, "fetchUnreadCount" | "initRealtime" | "closeRealtime" | "setUnreadCount" | "openDrawer" | "closeDrawer" | "toggleDrawer">>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { defineStore } from "pinia";
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
import { useApi } from "./useApi.js";
|
|
4
|
+
import { useRealtime } from "./useRealtime.js";
|
|
5
|
+
export const useNotificationStore = defineStore("notification", () => {
|
|
6
|
+
const api = useApi();
|
|
7
|
+
const { onEvent, offEvent } = useRealtime();
|
|
8
|
+
const unreadCount = ref(0);
|
|
9
|
+
const isOpenDrawer = ref(false);
|
|
10
|
+
const fetchUnreadCount = async () => {
|
|
11
|
+
try {
|
|
12
|
+
const response = await api(
|
|
13
|
+
`/notifications/unread-count`
|
|
14
|
+
);
|
|
15
|
+
unreadCount.value = response.data.count;
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.error("Failed to fetch unread count", err);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const onNotificationEvent = (_payload) => {
|
|
21
|
+
const payload = _payload;
|
|
22
|
+
if (typeof payload.unread_count === "number") {
|
|
23
|
+
unreadCount.value = payload.unread_count;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (payload?.action === "delete") {
|
|
27
|
+
unreadCount.value = Math.max(0, unreadCount.value - 1);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
unreadCount.value++;
|
|
31
|
+
};
|
|
32
|
+
const initRealtime = () => {
|
|
33
|
+
onEvent("notification", onNotificationEvent);
|
|
34
|
+
};
|
|
35
|
+
const closeRealtime = () => {
|
|
36
|
+
offEvent("notification", onNotificationEvent);
|
|
37
|
+
};
|
|
38
|
+
const setUnreadCount = (count) => {
|
|
39
|
+
unreadCount.value = count;
|
|
40
|
+
};
|
|
41
|
+
const openDrawer = () => {
|
|
42
|
+
isOpenDrawer.value = true;
|
|
43
|
+
};
|
|
44
|
+
const closeDrawer = () => {
|
|
45
|
+
isOpenDrawer.value = false;
|
|
46
|
+
};
|
|
47
|
+
const toggleDrawer = () => {
|
|
48
|
+
isOpenDrawer.value = !isOpenDrawer.value;
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
unreadCount,
|
|
52
|
+
isOpenDrawer,
|
|
53
|
+
fetchUnreadCount,
|
|
54
|
+
initRealtime,
|
|
55
|
+
closeRealtime,
|
|
56
|
+
setUnreadCount,
|
|
57
|
+
openDrawer,
|
|
58
|
+
closeDrawer,
|
|
59
|
+
toggleDrawer
|
|
60
|
+
};
|
|
61
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type EventCallback = (payload: unknown) => void;
|
|
2
|
+
export declare const useRealtime: () => {
|
|
3
|
+
connect: () => void;
|
|
4
|
+
disconnect: () => void;
|
|
5
|
+
send: (event: string, payload: unknown) => void;
|
|
6
|
+
onEvent: (event: string, callback: EventCallback) => void;
|
|
7
|
+
offEvent: (event: string, callback: EventCallback) => void;
|
|
8
|
+
isConnected: import("vue").Ref<boolean, boolean>;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { ref, getCurrentInstance, onBeforeUnmount } from "vue";
|
|
2
|
+
import { useRuntimeConfig } from "nuxt/app";
|
|
3
|
+
import { createAuthenticatedWebSocket } from "../utils/websocket.js";
|
|
4
|
+
const socket = ref(null);
|
|
5
|
+
const isConnected = ref(false);
|
|
6
|
+
const reconnectAttempts = ref(0);
|
|
7
|
+
const maxReconnectDelay = 3e4;
|
|
8
|
+
const baseReconnectDelay = 1e3;
|
|
9
|
+
let reconnectTimeout = null;
|
|
10
|
+
let isManuallyClosed = false;
|
|
11
|
+
const eventListeners = /* @__PURE__ */ new Map();
|
|
12
|
+
export const useRealtime = () => {
|
|
13
|
+
const { BASE_URL_API } = useRuntimeConfig().public;
|
|
14
|
+
const onEvent = (event, callback) => {
|
|
15
|
+
if (!eventListeners.has(event)) {
|
|
16
|
+
eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
17
|
+
}
|
|
18
|
+
eventListeners.get(event).add(callback);
|
|
19
|
+
};
|
|
20
|
+
const offEvent = (event, callback) => {
|
|
21
|
+
eventListeners.get(event)?.delete(callback);
|
|
22
|
+
};
|
|
23
|
+
const getWsUrl = () => {
|
|
24
|
+
let url = BASE_URL_API;
|
|
25
|
+
if (typeof window !== "undefined" && window.location.hostname !== "localhost") {
|
|
26
|
+
url = url.replace("localhost", window.location.hostname).replace("127.0.0.1", window.location.hostname);
|
|
27
|
+
}
|
|
28
|
+
if (url.startsWith("http://")) {
|
|
29
|
+
url = url.replace("http://", "ws://");
|
|
30
|
+
} else if (url.startsWith("https://")) {
|
|
31
|
+
url = url.replace("https://", "wss://");
|
|
32
|
+
}
|
|
33
|
+
return `${url}/ws`;
|
|
34
|
+
};
|
|
35
|
+
const connect = () => {
|
|
36
|
+
if (socket.value?.readyState === WebSocket.OPEN || socket.value?.readyState === WebSocket.CONNECTING) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
isManuallyClosed = false;
|
|
40
|
+
const wsUrl = getWsUrl();
|
|
41
|
+
console.log("Connecting to WS:", wsUrl);
|
|
42
|
+
const ws = createAuthenticatedWebSocket(wsUrl);
|
|
43
|
+
if (!ws) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
socket.value = ws;
|
|
47
|
+
socket.value.onopen = () => {
|
|
48
|
+
console.log("WS Connected");
|
|
49
|
+
isConnected.value = true;
|
|
50
|
+
reconnectAttempts.value = 0;
|
|
51
|
+
};
|
|
52
|
+
socket.value.onmessage = (event) => {
|
|
53
|
+
try {
|
|
54
|
+
const data = JSON.parse(event.data);
|
|
55
|
+
console.log("WS Event:", data);
|
|
56
|
+
handleEvent(data);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error("WS Parse Error:", e);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
socket.value.onclose = () => {
|
|
62
|
+
console.log("WS Closed");
|
|
63
|
+
isConnected.value = false;
|
|
64
|
+
socket.value = null;
|
|
65
|
+
if (!isManuallyClosed) {
|
|
66
|
+
attemptReconnect();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
socket.value.onerror = (error) => {
|
|
70
|
+
console.error("WS Error:", error);
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const attemptReconnect = () => {
|
|
74
|
+
if (reconnectTimeout) clearTimeout(reconnectTimeout);
|
|
75
|
+
const delay = Math.min(
|
|
76
|
+
baseReconnectDelay * Math.pow(2, reconnectAttempts.value),
|
|
77
|
+
maxReconnectDelay
|
|
78
|
+
);
|
|
79
|
+
const jitter = Math.random() * 1e3;
|
|
80
|
+
console.log(
|
|
81
|
+
`Reconnecting in ${delay + jitter}ms... (Attempt ${reconnectAttempts.value + 1})`
|
|
82
|
+
);
|
|
83
|
+
reconnectTimeout = setTimeout(() => {
|
|
84
|
+
reconnectAttempts.value++;
|
|
85
|
+
connect();
|
|
86
|
+
}, delay + jitter);
|
|
87
|
+
};
|
|
88
|
+
const disconnect = () => {
|
|
89
|
+
isManuallyClosed = true;
|
|
90
|
+
if (socket.value) {
|
|
91
|
+
socket.value.close();
|
|
92
|
+
socket.value = null;
|
|
93
|
+
}
|
|
94
|
+
if (reconnectTimeout) {
|
|
95
|
+
clearTimeout(reconnectTimeout);
|
|
96
|
+
reconnectTimeout = null;
|
|
97
|
+
}
|
|
98
|
+
isConnected.value = false;
|
|
99
|
+
};
|
|
100
|
+
const send = (event, payload) => {
|
|
101
|
+
if (socket.value?.readyState === WebSocket.OPEN) {
|
|
102
|
+
socket.value.send(JSON.stringify({ event, payload }));
|
|
103
|
+
} else {
|
|
104
|
+
console.warn("WS: Cannot send, socket is not connected.");
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const handleEvent = (data) => {
|
|
108
|
+
const { event, payload } = data;
|
|
109
|
+
const listeners = eventListeners.get(event);
|
|
110
|
+
if (listeners) {
|
|
111
|
+
listeners.forEach((cb) => cb(payload));
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
if (getCurrentInstance()) {
|
|
115
|
+
onBeforeUnmount(() => {
|
|
116
|
+
disconnect();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
connect,
|
|
121
|
+
disconnect,
|
|
122
|
+
send,
|
|
123
|
+
onEvent,
|
|
124
|
+
offEvent,
|
|
125
|
+
isConnected
|
|
126
|
+
};
|
|
127
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const createAuthenticatedWebSocket: (url: string) => WebSocket | null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useRuntimeConfig, useCookie } from "nuxt/app";
|
|
2
|
+
export const createAuthenticatedWebSocket = (url) => {
|
|
3
|
+
const config = useRuntimeConfig();
|
|
4
|
+
const { APP_TYPE } = config.public;
|
|
5
|
+
const secId = useCookie(APP_TYPE === "OFFICE" ? "OFFICE_SEC_ID" : "SEC_ID");
|
|
6
|
+
if (!secId.value) {
|
|
7
|
+
console.warn(
|
|
8
|
+
"WS: No auth token found, cannot create authenticated connection."
|
|
9
|
+
);
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const wsUrl = new URL(url);
|
|
13
|
+
wsUrl.searchParams.append("token", secId.value);
|
|
14
|
+
return new WebSocket(wsUrl.toString());
|
|
15
|
+
};
|
package/package.json
CHANGED
|
File without changes
|