Files
DataBuddy-vue/src/components/layout/Tip.vue

220 lines
5.0 KiB
Vue

<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
const notifications = ref([])
const timeouts = ref([])
const visibleNotifications = computed(() => {
return notifications.value
.filter(n => n && n.show)
.sort((a, b) => a.createdAt - b.createdAt)
})
const clearAllTimeouts = () => {
timeouts.value.forEach(timer => clearTimeout(timer))
timeouts.value = []
}
const addNotification = (content, theme = 'success', duration = 3000) => {
if (!content) return
const id = Date.now() + Math.random().toString(36).slice(2, 9)
const createdAt = Date.now()
const notification = {
id,
content,
theme,
show: true,
height: 0,
createdAt
}
notifications.value.push(notification)
nextTick(() => {
const stillExists = notifications.value.some(n => n.id === id)
if (!stillExists) return
const el = document.getElementById(`notification-${id}`)
if (el) {
const styles = window.getComputedStyle(el)
const margin = parseFloat(styles.marginBottom) || 15
notification.height = el.offsetHeight + margin
}
})
const timer = setTimeout(() => {
removeNotification(id)
}, duration)
timeouts.value.push(timer)
if (notifications.value.length > 10) {
removeNotification(notifications.value[0].id)
}
return id
}
const removeNotification = (id) => {
const index = notifications.value.findIndex(n => n.id === id)
if (index === -1) return
const notification = notifications.value[index]
notification.show = false
const visibleNotifs = notifications.value.filter(n => n.show).sort((a, b) => a.position - b.position);
visibleNotifs.forEach((notif, i) => {
notif.position = i;
});
const timer = setTimeout(() => {
const currentIndex = notifications.value.findIndex(n => n.id === id)
if (currentIndex === -1) return
notifications.value.splice(currentIndex, 1)
for (let i = currentIndex; i < notifications.value.length; i++) {
notifications.value[i].position -= 1
}
}, 350)
timeouts.value.push(timer)
}
defineExpose({
addNotification,
success: (content, duration) => addNotification(content, 'success', duration),
error: (content, duration) => addNotification(content, 'error', duration),
warning: (content, duration) => addNotification(content, 'warning', duration),
info: (content, duration) => addNotification(content, 'info', duration)
})
const getTopPosition = (index) => {
let top = 35;
for (let i = 0; i < index; i++) {
const notification = visibleNotifications.value[i];
if (notification && notification.height) {
top += notification.height;
} else {
top += 70;
}
}
return `${top}px`;
}
const props = defineProps({
content: String,
theme: {
type: String,
default: 'success',
validator: (value) => ['success', 'error', 'warning', 'info'].includes(value)
},
duration: {
type: Number,
default: 3000
}
})
watch(() => props.content, (newVal) => {
if (newVal) {
addNotification(newVal, props.theme, props.duration)
}
})
onMounted(() => {
if (props.content) {
addNotification(props.content, props.theme, props.duration)
}
})
onUnmounted(() => {
clearAllTimeouts()
})
</script>
<template>
<div class="notifications-container">
<transition-group name="slide-fade" tag="div">
<div v-for="(notif, index) in visibleNotifications" :key="notif.id" :id="`notification-${notif.id}`" class="tip-container" :class="`theme-${notif.theme}`" :style="{ top: getTopPosition(index) }">
{{ notif.content }}
</div>
</transition-group>
</div>
</template>
<style scoped>
.notifications-container {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9999;
pointer-events: none;
}
.tip-container {
position: absolute;
right: 16px;
z-index: 9999;
padding: 20px 12px;
border-radius: 15px;
min-width: 280px;
max-width: 80%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
color: white;
text-align: center;
pointer-events: auto;
transition: all 0.3s ease;
}
.theme-success {
background: linear-gradient(135deg, #4caf50, #43a047);
}
.theme-error {
background: linear-gradient(135deg, #f44336, #e53935);
}
.theme-warning {
background: linear-gradient(135deg, #ff9800, #fb8c00);
}
.theme-info {
background: linear-gradient(135deg, #2196f3, #1e88e5);
}
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 0.35s cubic-bezier(0.23, 1, 0.32, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
opacity: 0;
transform: translate(20px);
}
.slide-fade-enter-to,
.slide-fade-leave-from {
opacity: 1;
transform: translate(0);
}
@media (max-width: 480px) {
.tip-container {
min-width: 70%;
max-width: 80%;
padding: 10px 15px;
left: 50%;
transform: translateX(-50%);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translate(-50%, -20px);
}
.slide-fade-enter-to,
.slide-fade-leave-from {
transform: translate(-50%, 0);
}
}
</style>