Reconstruct notification center for client side

This commit is contained in:
Donald Zou 2025-06-01 11:19:50 +08:00
parent 4c8ba6b0a8
commit 173cc57490
9 changed files with 288 additions and 22 deletions

View File

@ -10,7 +10,9 @@
"dependencies": {
"bootstrap": "^5.3.6",
"bootstrap-icons": "^1.13.1",
"dayjs": "^1.11.13",
"pinia": "^3.0.2",
"uuid": "^11.1.0",
"vue": "^3.5.13",
"vue-router": "^4.5.1"
},
@ -1708,6 +1710,12 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
@ -2645,6 +2653,19 @@
"browserslist": ">= 4.21.0"
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmmirror.com/vite/-/vite-6.3.5.tgz",

View File

@ -11,7 +11,9 @@
"dependencies": {
"bootstrap": "^5.3.6",
"bootstrap-icons": "^1.13.1",
"dayjs": "^1.11.13",
"pinia": "^3.0.2",
"uuid": "^11.1.0",
"vue": "^3.5.13",
"vue-router": "^4.5.1"
},

View File

@ -1,13 +1,30 @@
<script setup>
import './assets/main.css'
import NotificationList from "@/components/notification/notificationList.vue";
</script>
<template>
<div data-bs-theme="dark" class="text-body bg-body">
<RouterView></RouterView>
<div data-bs-theme="dark" class="text-body bg-body w-100 h-100">
<Suspense>
<RouterView v-slot="{ Component }">
<Transition name="app" mode="out-in" type="transition" appear>
<Component :is="Component"></Component>
</Transition>
</RouterView>
</Suspense>
<NotificationList></NotificationList>
</div>
</template>
<style scoped>
.app-enter-active,
.app-leave-active {
transition: all 0.5s cubic-bezier(0.82, 0.58, 0.17, 1);
}
.app-enter-from,
.app-leave-to{
opacity: 0;
filter: blur(5px);
transform: scale(0.95);
}
</style>

View File

@ -52,4 +52,8 @@
.btn-brand:hover{
--brandColor1: rgb(0, 142, 216);
--brandColor2: rgba(249, 70, 71)
}
::-webkit-scrollbar {
display: none;
}

View File

@ -0,0 +1,68 @@
<script setup>
import {onMounted} from "vue";
const props = defineProps({
notificationData: {
id: "",
show: true,
content: "",
time: "",
status: ""
}
})
let timeout = undefined;
const show = () => {
props.notificationData.show = true;
timeout = setTimeout(() => {
dismiss()
}, 50000)
}
const clearTime = () => clearTimeout(timeout)
const dismiss = () => props.notificationData.show = false;
onMounted(() => {
show()
})
</script>
<template>
<div
@mouseenter="clearTime()"
@mouseleave="notificationData.show ? show():undefined"
:class="{
'text-bg-success': notificationData.status === 'success',
'text-bg-warning': notificationData.status === 'warning',
'text-bg-danger': notificationData.status === 'danger'
}"
class="card shadow rounded-3 position-relative message ms-auto notification">
<div class="card-body">
<div class="d-flex align-items-center mb-2">
<small>
{{ notificationData.time.format("hh:mm A") }}
</small>
<small class="ms-auto">
<a role="button" @click="dismiss()">
Dismiss<i class="bi bi-x-lg ms-2"></i>
</a>
</small>
</div>
<span class="fw-medium">{{ notificationData.content }}</span>
</div>
</div>
</template>
<style scoped>
.notification{
width: 100%;
word-break: break-all;
}
@media screen and (min-width: 576px) {
.notification{
width: 400px;
}
}
</style>

View File

@ -0,0 +1,57 @@
<script setup>
import {clientStore} from "@/stores/clientStore.js";
import Notification from "@/components/notification/notification.vue";
import {computed, onMounted} from "vue";
const store = clientStore()
const notifications = computed(() => {
return store.notifications.filter(x => x.show).slice().reverse()
})
onMounted(() => {
store.newNotification("Hi!!lskadjlkasjdlkasjkldjaslkdjklasjdlkjaslkdjlkasjdlkjsalkdjlkasjdlk", "warning")
})
</script>
<template>
<div class="messageCentre text-body position-absolute d-flex">
<TransitionGroup name="message" tag="div"
class="position-relative flex-sm-grow-0 flex-grow-1 d-flex align-items-end ms-sm-auto flex-column gap-2">
<Notification v-for="n in notifications"
:notificationData="n" :key="n.id"></Notification>
</TransitionGroup>
</div>
</template>
<style scoped>
.message-move, /* apply transition to moving elements */
.message-enter-active,
.message-leave-active {
transition: all 0.5s cubic-bezier(0.82, 0.58, 0.17, 1);
}
.message-enter-from,
.message-leave-to {
filter: blur(2px);
opacity: 0;
}
.message-enter-from{
transform: translateY(-30px);
}
.message-leave-to{
transform: translateY(30px);
}
.messageCentre{
z-index: 9999;
top: 1rem;
right: 1rem;
}
@media screen and (max-width: 768px) {
.messageCentre{
width: calc(100% - 2rem);
}
}
</style>

View File

@ -0,0 +1,22 @@
import {defineStore} from "pinia";
import {ref} from "vue";
import {v4} from "uuid"
import dayjs from "dayjs";
export const clientStore = defineStore('clientStore', () => {
const notifications = ref([])
function newNotification(content, status) {
notifications.value.push({
id: v4().toString(),
status: status,
content: content,
time: dayjs(),
show: true
})
}
return {
notifications, newNotification
}
})

View File

@ -2,38 +2,47 @@
import {reactive} from "vue";
const formData = reactive({
username: "",
email: "",
password: ""
})
});
const submit = (e) => {
e.preventDefault();
for (let key in formData){
if (formData[key].length === 0){
break
}
}
}
</script>
<template>
<div class="d-flex vh-100 vw-100 p-4 overflow-y-scroll">
<div class="m-auto" style="width: 600px">
<h4 class="mb-0">Welcome to</h4>
<h1 class="fw-bold display-4">WGDashboard Client</h1>
<div class="mt-4 d-flex flex-column gap-3">
<h1>Sign In</h1>
<p>to your WGDashboard Client account</p>
<form class="mt-4 d-flex flex-column gap-3" @submit="e => submit(e)">
<div class="form-floating">
<input type="text"
required
v-model="formData.username"
name="username"
autocomplete="username"
v-model="formData.email"
name="email"
autocomplete="email"
autofocus
class="form-control rounded-3" id="username" placeholder="Username">
class="form-control rounded-3" id="email" placeholder="email">
<label for="floatingInput" class="d-flex">
<i class="bi bi-person-circle me-2"></i>
Username
Email
</label>
</div>
<div class="form-floating">
<input type="password"
required
v-model="formData.password"
name="username"
autocomplete="username"
autofocus
class="form-control rounded-3" id="username" placeholder="Username">
name="password"
autocomplete="password"
class="form-control rounded-3" id="password" placeholder="Password">
<label for="floatingInput" class="d-flex">
<i class="bi bi-key me-2"></i>
Password
@ -42,8 +51,13 @@ const formData = reactive({
<a href="#" class="text-body text-decoration-none">
Forgot Password?
</a>
<button class="btn btn-primary rounded-3 btn-lg btn-brand w-100 fw-bold">Sign In</button>
</div>
<div class="d-flex">
<button class="ms-auto btn btn-primary rounded-3 btn-brand px-3 py-2">
Continue
<i class="ms-2 bi bi-arrow-right"></i>
</button>
</div>
</form>
<div>
<hr class="my-4">

View File

@ -2,16 +2,77 @@
import {reactive} from "vue";
const formData = reactive({
username: "",
email: "",
password: ""
password: "",
confirmPassword: ""
})
</script>
<template>
<div class="d-flex vh-100 vw-100 p-4 overflow-y-scroll">
<div class="m-auto" style="width: 600px">
<h1 class="fw-bold display-4">Sign Up</h1>
<h1>Sign Up</h1>
<p>to use WGDashboard Client</p>
<div class="mt-4 d-flex flex-column gap-3">
<div class="form-floating">
<input type="text"
required
v-model="formData.email"
name="email"
autocomplete="email"
autofocus
class="form-control rounded-3" id="email" placeholder="email">
<label for="floatingInput" class="d-flex">
<i class="bi bi-person-circle me-2"></i>
Email
</label>
</div>
<div class="form-floating">
<input type="password"
required
v-model="formData.password"
name="password"
autocomplete="password"
autofocus
class="form-control rounded-3" id="password" placeholder="password">
<label for="floatingInput" class="d-flex">
<i class="bi bi-key me-2"></i>
Password
</label>
</div>
<div class="form-floating">
<input type="password"
required
v-model="formData.password"
name="confirm_password"
autocomplete="confirm_password"
autofocus
class="form-control rounded-3" id="confirm_password" placeholder="confirm_password">
<label for="floatingInput" class="d-flex">
<i class="bi bi-key me-2"></i>
Confirm Password
</label>
</div>
<div class="d-flex">
<button class="ms-auto btn btn-primary rounded-3 btn-brand px-3 py-2">
Continue
<i class="ms-2 bi bi-arrow-right"></i>
</button>
</div>
</div>
<div>
<hr class="my-4">
<div class="d-flex align-items-center">
<span class="text-muted">
Already have an account?
</span>
<RouterLink to="/signin" class="text-body text-decoration-none ms-auto fw-bold">
Sign In
</RouterLink>
</div>
</div>
</div>
</div>
</template>