✨ Realtime Notify
This commit is contained in:
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<v-menu eager :close-on-content-click="false">
|
||||
<template #activator="{ props }">
|
||||
<v-btn v-bind="props" stacked rounded="circle" size="small" variant="text" :loading="loading">
|
||||
<v-badge v-if="pagination.total > 0" color="error" :content="pagination.total">
|
||||
<v-btn v-bind="props" icon size="small" variant="text" :loading="loading">
|
||||
<v-badge v-if="notify.total > 0" color="error" :content="notify.total">
|
||||
<v-icon icon="mdi-bell" />
|
||||
</v-badge>
|
||||
|
||||
@ -10,19 +10,19 @@
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-list v-if="notifications.length <= 0" class="w-[380px]" density="compact">
|
||||
<v-list v-if="notify.notifications.length <= 0" class="w-[380px]" density="compact">
|
||||
<v-list-item>
|
||||
<v-alert class="text-sm" variant="tonal" type="info">You are done! There is no unread notifications for you.</v-alert>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-list v-else class="w-[380px]" density="compact" lines="three">
|
||||
<v-list-item v-for="item in notifications">
|
||||
<v-list-item v-for="(item, idx) in notify.notifications">
|
||||
<template #title>{{ item.subject }}</template>
|
||||
<template #subtitle>{{ item.content }}</template>
|
||||
|
||||
<template #append>
|
||||
<v-btn icon="mdi-check" size="x-small" variant="text" :disabled="loading" @click="markAsRead(item)" />
|
||||
<v-btn icon="mdi-check" size="x-small" variant="text" :disabled="loading" @click="markAsRead(item, idx)" />
|
||||
</template>
|
||||
|
||||
<div class="flex text-xs gap-1">
|
||||
@ -39,39 +39,17 @@
|
||||
<script setup lang="ts">
|
||||
import { request } from "@/scripts/request"
|
||||
import { getAtk } from "@/stores/userinfo"
|
||||
import { reactive, ref } from "vue"
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
import { useNotifications } from "@/stores/notifications";
|
||||
|
||||
const loading = ref(false)
|
||||
const notify = useNotifications()
|
||||
|
||||
const error = ref<string | null>(null)
|
||||
const submitting = ref(false)
|
||||
const loading = computed(() => notify.loading || submitting.value)
|
||||
|
||||
const notifications = ref<any[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 25, total: 0 })
|
||||
|
||||
async function readNotifications() {
|
||||
loading.value = true
|
||||
const res = await request(
|
||||
"/api/notifications?" +
|
||||
new URLSearchParams({
|
||||
take: pagination.pageSize.toString(),
|
||||
offset: ((pagination.page - 1) * pagination.pageSize).toString(),
|
||||
}),
|
||||
{
|
||||
headers: { Authorization: `Bearer ${getAtk()}` },
|
||||
},
|
||||
)
|
||||
if (res.status === 200) {
|
||||
const data = await res.json()
|
||||
notifications.value = data["data"]
|
||||
pagination.total = data["count"]
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
readNotifications()
|
||||
|
||||
async function markAsRead(item: any) {
|
||||
loading.value = true
|
||||
async function markAsRead(item: any, idx: number) {
|
||||
submitting.value = true
|
||||
const res = await request(`/api/notifications/${item.id}/read`, {
|
||||
method: "PUT",
|
||||
headers: { Authorization: `Bearer ${getAtk()}` },
|
||||
@ -79,9 +57,14 @@ async function markAsRead(item: any) {
|
||||
if (res.status !== 200) {
|
||||
error.value = await res.text()
|
||||
} else {
|
||||
await readNotifications()
|
||||
notify.remove(idx)
|
||||
error.value = null
|
||||
}
|
||||
loading.value = false
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
notify.list()
|
||||
|
||||
onMounted(() => notify.connect())
|
||||
onUnmounted(() => notify.disconnect())
|
||||
</script>
|
||||
|
64
pkg/views/src/stores/notifications.ts
Normal file
64
pkg/views/src/stores/notifications.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { checkLoggedIn, getAtk } from "@/stores/userinfo";
|
||||
import { request } from "@/scripts/request";
|
||||
|
||||
export const useNotifications = defineStore("notifications", () => {
|
||||
let socket: WebSocket;
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const notifications = ref<any[]>([]);
|
||||
const total = ref(0)
|
||||
|
||||
async function list() {
|
||||
loading.value = true;
|
||||
const res = await request(
|
||||
"/api/notifications?" +
|
||||
new URLSearchParams({
|
||||
take: (25).toString(),
|
||||
offset: (0).toString()
|
||||
}),
|
||||
{
|
||||
headers: { Authorization: `Bearer ${getAtk()}` }
|
||||
}
|
||||
);
|
||||
if (res.status === 200) {
|
||||
const data = await res.json();
|
||||
notifications.value = data["data"];
|
||||
total.value = data["count"];
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function remove(idx: number) {
|
||||
notifications.value.splice(idx, 1)
|
||||
total.value--;
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
if (!(checkLoggedIn())) return;
|
||||
|
||||
const uri = `ws://${window.location.host}/api/notifications/listen`;
|
||||
|
||||
socket = new WebSocket(uri + `?tk=${getAtk() as string}`);
|
||||
|
||||
socket.addEventListener("open", (event) => {
|
||||
console.log("[NOTIFICATIONS] The listen websocket has been established... ", event.type);
|
||||
});
|
||||
socket.addEventListener("close", (event) => {
|
||||
console.warn("[NOTIFICATIONS] The listen websocket is disconnected... ", event.reason, event.code);
|
||||
});
|
||||
socket.addEventListener("message", (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
notifications.value.push(data);
|
||||
total.value++;
|
||||
});
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
socket.close();
|
||||
}
|
||||
|
||||
return { loading, notifications, total, list, remove, connect, disconnect };
|
||||
});
|
@ -1,22 +1,27 @@
|
||||
import { fileURLToPath, URL } from "node:url"
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
|
||||
import { defineConfig } from "vite"
|
||||
import vue from "@vitejs/plugin-vue"
|
||||
import vueJsx from "@vitejs/plugin-vue-jsx"
|
||||
import unocss from "unocss/vite"
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import vueJsx from "@vitejs/plugin-vue-jsx";
|
||||
import unocss from "unocss/vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), vueJsx(), unocss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
"/api/notifications/listen": {
|
||||
target: "ws://localhost:8444",
|
||||
ws: true
|
||||
},
|
||||
|
||||
"/api": "http://localhost:8444",
|
||||
"/.well-known": "http://localhost:8444"
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
Reference in New Issue
Block a user