mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 19:45:08 +08:00
129 lines
3.5 KiB
TypeScript
129 lines
3.5 KiB
TypeScript
import { createApp, h, reactive, TransitionGroup } from 'vue'
|
|
import McToast from '@/components/common/McToast.vue'
|
|
|
|
// Project-native toast — frosted glass, spring slide-in from the top,
|
|
// status-colored indicator (check / x / triangle / circle), auto
|
|
// dismiss with longer hold for failures so the user has time to read.
|
|
//
|
|
// Drop-in replacement for ElMessage's success/error/warning/info calls.
|
|
// The host element + Vue subtree are mounted lazily on first use so
|
|
// nothing renders until a toast is actually fired.
|
|
|
|
export type McToastType = 'success' | 'error' | 'warning' | 'info'
|
|
|
|
export interface McToastOptions {
|
|
/** Override auto-dismiss in ms. Default: 3000 (success/info), 4000 (warning), 4500 (error). */
|
|
duration?: number
|
|
}
|
|
|
|
interface ToastEntry {
|
|
id: number
|
|
type: McToastType
|
|
message: string
|
|
}
|
|
|
|
const toasts = reactive<ToastEntry[]>([])
|
|
let nextId = 1
|
|
let mounted = false
|
|
|
|
function defaultDuration(type: McToastType): number {
|
|
if (type === 'error') return 4500
|
|
if (type === 'warning') return 4000
|
|
return 3000
|
|
}
|
|
|
|
function ensureHost(): void {
|
|
if (mounted || typeof document === 'undefined') return
|
|
// HMR safety: if a prior module instance left a host behind, drop it
|
|
// so this module's reactive array is what renders.
|
|
document.querySelector('.mc-toast-host')?.remove()
|
|
mounted = true
|
|
|
|
const host = document.createElement('div')
|
|
host.className = 'mc-toast-host'
|
|
document.body.appendChild(host)
|
|
|
|
// Inject host CSS once — kept here (not scoped to McToast) because the
|
|
// stack itself is a global overlay sibling of the app root, and the
|
|
// enter/leave transition names need to match TransitionGroup's class
|
|
// contract.
|
|
if (!document.querySelector('#mc-toast-host-style')) {
|
|
const style = document.createElement('style')
|
|
style.id = 'mc-toast-host-style'
|
|
style.textContent = `
|
|
.mc-toast-host {
|
|
position: fixed;
|
|
top: 18px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 3000;
|
|
pointer-events: none;
|
|
}
|
|
.mc-toast-stack {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.mc-toast-enter-active,
|
|
.mc-toast-leave-active {
|
|
transition:
|
|
transform 0.32s cubic-bezier(0.32, 0.72, 0, 1),
|
|
opacity 0.22s ease;
|
|
}
|
|
.mc-toast-enter-from {
|
|
opacity: 0;
|
|
transform: translateY(-12px);
|
|
}
|
|
.mc-toast-leave-to {
|
|
opacity: 0;
|
|
transform: translateY(-6px);
|
|
}
|
|
.mc-toast-move {
|
|
transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1);
|
|
}
|
|
`
|
|
document.head.appendChild(style)
|
|
}
|
|
|
|
createApp({
|
|
render: () =>
|
|
h(
|
|
TransitionGroup as unknown as any,
|
|
{ name: 'mc-toast', tag: 'div', class: 'mc-toast-stack' },
|
|
() =>
|
|
toasts.map((t) =>
|
|
h(McToast, { key: t.id, type: t.type, message: t.message }),
|
|
),
|
|
),
|
|
}).mount(host)
|
|
}
|
|
|
|
function remove(id: number): void {
|
|
const idx = toasts.findIndex((t) => t.id === id)
|
|
if (idx >= 0) toasts.splice(idx, 1)
|
|
}
|
|
|
|
function show(type: McToastType, message: string, opts?: McToastOptions): void {
|
|
ensureHost()
|
|
const id = nextId++
|
|
toasts.push({ id, type, message })
|
|
const duration = opts?.duration ?? defaultDuration(type)
|
|
setTimeout(() => remove(id), duration)
|
|
}
|
|
|
|
export const mcToast = {
|
|
success(message: string, opts?: McToastOptions): void {
|
|
show('success', message, opts)
|
|
},
|
|
error(message: string, opts?: McToastOptions): void {
|
|
show('error', message, opts)
|
|
},
|
|
warning(message: string, opts?: McToastOptions): void {
|
|
show('warning', message, opts)
|
|
},
|
|
info(message: string, opts?: McToastOptions): void {
|
|
show('info', message, opts)
|
|
},
|
|
}
|