|
|
<template> |
|
|
<Teleport to="body"> |
|
|
<Transition name="toast" appear> |
|
|
<div |
|
|
v-if="visible && message" |
|
|
class="toast" |
|
|
:class="[type, position]" |
|
|
@click="handleClick" |
|
|
> |
|
|
<div class="toast-content"> |
|
|
|
|
|
<div class="toast-icon" v-if="showIcon"> |
|
|
<i :class="iconClass"></i> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="toast-message">{{ message }}</div> |
|
|
</div> |
|
|
</div> |
|
|
</Transition> |
|
|
</Teleport> |
|
|
</template> |
|
|
|
|
|
<script setup> |
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue' |
|
|
|
|
|
const props = defineProps({ |
|
|
|
|
|
message: { |
|
|
type: String, |
|
|
default: '' |
|
|
}, |
|
|
|
|
|
|
|
|
type: { |
|
|
type: String, |
|
|
default: 'info', |
|
|
validator: (value) => ['success', 'error', 'warning', 'info'].includes(value) |
|
|
}, |
|
|
|
|
|
|
|
|
duration: { |
|
|
type: Number, |
|
|
default: 3000 |
|
|
}, |
|
|
|
|
|
|
|
|
position: { |
|
|
type: String, |
|
|
default: 'top', |
|
|
validator: (value) => ['top', 'center', 'bottom'].includes(value) |
|
|
}, |
|
|
|
|
|
|
|
|
showIcon: { |
|
|
type: Boolean, |
|
|
default: true |
|
|
}, |
|
|
|
|
|
|
|
|
closable: { |
|
|
type: Boolean, |
|
|
default: true |
|
|
} |
|
|
}) |
|
|
|
|
|
const emit = defineEmits(['close']) |
|
|
|
|
|
const visible = ref(false) |
|
|
let timer = null |
|
|
|
|
|
|
|
|
const iconClass = computed(() => { |
|
|
const iconMap = { |
|
|
success: 'fas fa-check-circle', |
|
|
error: 'fas fa-times-circle', |
|
|
warning: 'fas fa-exclamation-triangle', |
|
|
info: 'fas fa-info-circle' |
|
|
} |
|
|
|
|
|
return iconMap[props.type] || iconMap.info |
|
|
}) |
|
|
|
|
|
|
|
|
const handleClick = () => { |
|
|
if (props.closable) { |
|
|
close() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const show = () => { |
|
|
visible.value = true |
|
|
|
|
|
if (props.duration > 0) { |
|
|
timer = setTimeout(() => { |
|
|
close() |
|
|
}, props.duration) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const close = () => { |
|
|
visible.value = false |
|
|
if (timer) { |
|
|
clearTimeout(timer) |
|
|
timer = null |
|
|
} |
|
|
emit('close') |
|
|
} |
|
|
|
|
|
|
|
|
onMounted(() => { |
|
|
show() |
|
|
}) |
|
|
|
|
|
onUnmounted(() => { |
|
|
if (timer) { |
|
|
clearTimeout(timer) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
defineExpose({ |
|
|
close |
|
|
}) |
|
|
</script> |
|
|
|
|
|
<style scoped> |
|
|
.toast { |
|
|
position: fixed; |
|
|
left: 50%; |
|
|
transform: translateX(-50%); |
|
|
z-index: 10000; |
|
|
max-width: calc(100vw - 32px); |
|
|
min-width: 200px; |
|
|
} |
|
|
|
|
|
.toast.top { |
|
|
top: 20px; |
|
|
} |
|
|
|
|
|
.toast.center { |
|
|
top: 50%; |
|
|
transform: translate(-50%, -50%); |
|
|
} |
|
|
|
|
|
.toast.bottom { |
|
|
bottom: 20px; |
|
|
} |
|
|
|
|
|
.toast-content { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 12px; |
|
|
padding: 12px 16px; |
|
|
background: var(--bg-card); |
|
|
backdrop-filter: blur(20px); |
|
|
border-radius: 8px; |
|
|
border: 1px solid rgba(255, 255, 255, 0.1); |
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); |
|
|
} |
|
|
|
|
|
.toast-icon { |
|
|
flex-shrink: 0; |
|
|
} |
|
|
|
|
|
.toast-icon i { |
|
|
font-size: 16px; |
|
|
} |
|
|
|
|
|
.toast-message { |
|
|
font-size: 14px; |
|
|
font-weight: 500; |
|
|
color: var(--text-primary); |
|
|
line-height: 1.4; |
|
|
} |
|
|
|
|
|
|
|
|
.toast.success .toast-icon i { |
|
|
color: #48bb78; |
|
|
} |
|
|
|
|
|
.toast.success .toast-content { |
|
|
border-color: rgba(72, 187, 120, 0.3); |
|
|
} |
|
|
|
|
|
.toast.error .toast-icon i { |
|
|
color: #e53e3e; |
|
|
} |
|
|
|
|
|
.toast.error .toast-content { |
|
|
border-color: rgba(229, 62, 62, 0.3); |
|
|
} |
|
|
|
|
|
.toast.warning .toast-icon i { |
|
|
color: #ed8936; |
|
|
} |
|
|
|
|
|
.toast.warning .toast-content { |
|
|
border-color: rgba(237, 137, 54, 0.3); |
|
|
} |
|
|
|
|
|
.toast.info .toast-icon i { |
|
|
color: #3182ce; |
|
|
} |
|
|
|
|
|
.toast.info .toast-content { |
|
|
border-color: rgba(49, 130, 206, 0.3); |
|
|
} |
|
|
|
|
|
|
|
|
.toast-enter-active, |
|
|
.toast-leave-active { |
|
|
transition: all 0.3s ease-out; |
|
|
} |
|
|
|
|
|
.toast-enter-from { |
|
|
opacity: 0; |
|
|
transform: translateX(-50%) translateY(-20px); |
|
|
} |
|
|
|
|
|
.toast.center.toast-enter-from { |
|
|
transform: translate(-50%, -50%) scale(0.9); |
|
|
} |
|
|
|
|
|
.toast.bottom.toast-enter-from { |
|
|
transform: translateX(-50%) translateY(20px); |
|
|
} |
|
|
|
|
|
.toast-leave-to { |
|
|
opacity: 0; |
|
|
transform: translateX(-50%) translateY(-20px); |
|
|
} |
|
|
|
|
|
.toast.center.toast-leave-to { |
|
|
transform: translate(-50%, -50%) scale(0.9); |
|
|
} |
|
|
|
|
|
.toast.bottom.toast-leave-to { |
|
|
transform: translateX(-50%) translateY(20px); |
|
|
} |
|
|
|
|
|
|
|
|
@media (max-width: 375px) { |
|
|
.toast { |
|
|
max-width: calc(100vw - 24px); |
|
|
min-width: auto; |
|
|
} |
|
|
|
|
|
.toast-content { |
|
|
padding: 10px 14px; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.toast-icon i { |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.toast-message { |
|
|
font-size: 13px; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@media (hover: none) { |
|
|
.toast-content:active { |
|
|
transform: scale(0.98); |
|
|
} |
|
|
} |
|
|
</style> |