Notification

在页面放置一次,任意 Alpine 组件可 $dispatch("notify", { type, content }) 发出 Toast。position / appearance / size / duration / event-name 通过 prop 控制。

bordered-left / md / bottom-right (event: notify)

<x-button color="info" appearance="outline"
    x-on:click="$dispatch('notify', { type: 'info', content: 'Checking for updates…' })">info</x-button>
<x-button color="success" appearance="outline"
    x-on:click="$dispatch('notify', { type: 'success', content: 'Saved' })">success</x-button>
<x-button color="warning" appearance="outline"
    x-on:click="$dispatch('notify', { type: 'warning', content: 'Your plan expires soon' })">warning</x-button>
<x-button color="error" appearance="outline"
    x-on:click="$dispatch('notify', { type: 'error', content: 'Connection failed' })">error</x-button>
<x-button color="neutral" appearance="base-100"
    x-on:click="
        $dispatch('notify', { type: 'success', content: 'First of three' });
        setTimeout(() => $dispatch('notify', { type: 'info', content: 'Second of three' }), 200);
        setTimeout(() => $dispatch('notify', { type: 'warning', content: 'Third of three' }), 400);
    ">stack 3</x-button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] gap-2 bg-transparent text-info border-info hover:bg-info hover:text-info-content focus-visible:ring-info cursor-pointer" x-on:click="$dispatch('notify', { type: 'info', content: 'Checking for updates…' })">info</button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] gap-2 bg-transparent text-success border-success hover:bg-success hover:text-success-content focus-visible:ring-success cursor-pointer" x-on:click="$dispatch('notify', { type: 'success', content: 'Saved' })">success</button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] gap-2 bg-transparent text-warning border-warning hover:bg-warning hover:text-warning-content focus-visible:ring-warning cursor-pointer" x-on:click="$dispatch('notify', { type: 'warning', content: 'Your plan expires soon' })">warning</button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] gap-2 bg-transparent text-error border-error hover:bg-error hover:text-error-content focus-visible:ring-error cursor-pointer" x-on:click="$dispatch('notify', { type: 'error', content: 'Connection failed' })">error</button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-md)] px-[var(--px-field-md)] text-[length:var(--text-field-md)] gap-2 bg-base-100 text-base-content border-base-content/10 hover:bg-base-200 focus-visible:ring-base-content cursor-pointer" x-on:click="$dispatch('notify', { type: 'success', content: 'First of three' }); setTimeout(() =&gt; $dispatch('notify', { type: 'info', content: 'Second of three' }), 200); setTimeout(() =&gt; $dispatch('notify', { type: 'warning', content: 'Third of three' }), 400);">stack 3</button>

solid / top-right / sm (event: notify-solid)

<x-button color="info" appearance="solid" size="sm"
    x-on:click="$dispatch('notify-solid', { type: 'info', content: 'Synced' })">info</x-button>
<x-button color="success" appearance="solid" size="sm"
    x-on:click="$dispatch('notify-solid', { type: 'success', content: 'Deploy succeeded' })">success</x-button>
<x-button color="warning" appearance="solid" size="sm"
    x-on:click="$dispatch('notify-solid', { type: 'warning', content: 'CPU usage is high' })">warning</x-button>
<x-button color="error" appearance="solid" size="sm"
    x-on:click="$dispatch('notify-solid', { type: 'error', content: 'Request timed out' })">error</x-button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] gap-1.5 bg-info text-info-content border-info hover:bg-info/90 focus-visible:ring-info cursor-pointer" x-on:click="$dispatch('notify-solid', { type: 'info', content: 'Synced' })">info</button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] gap-1.5 bg-success text-success-content border-success hover:bg-success/90 focus-visible:ring-success cursor-pointer" x-on:click="$dispatch('notify-solid', { type: 'success', content: 'Deploy succeeded' })">success</button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] gap-1.5 bg-warning text-warning-content border-warning hover:bg-warning/90 focus-visible:ring-warning cursor-pointer" x-on:click="$dispatch('notify-solid', { type: 'warning', content: 'CPU usage is high' })">warning</button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] gap-1.5 bg-error text-error-content border-error hover:bg-error/90 focus-visible:ring-error cursor-pointer" x-on:click="$dispatch('notify-solid', { type: 'error', content: 'Request timed out' })">error</button>

soft / bottom-left / lg (event: notify-left)

<x-button color="info" appearance="soft" size="sm"
    x-on:click="$dispatch('notify-left', { type: 'info', content: 'Backup started' })">info</x-button>
<x-button color="success" appearance="soft" size="sm"
    x-on:click="$dispatch('notify-left', { type: 'success', content: 'Archive complete' })">success</x-button>
<x-button color="warning" appearance="soft" size="sm"
    x-on:click="$dispatch('notify-left', { type: 'warning', content: 'Storage is running low' })">warning</x-button>
<x-button color="error" appearance="soft" size="sm"
    x-on:click="$dispatch('notify-left', { type: 'error', content: 'Backup failed' })">error</x-button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] gap-1.5 bg-info/10 text-info border-transparent hover:bg-info/20 focus-visible:ring-info cursor-pointer" x-on:click="$dispatch('notify-left', { type: 'info', content: 'Backup started' })">info</button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] gap-1.5 bg-success/10 text-success border-transparent hover:bg-success/20 focus-visible:ring-success cursor-pointer" x-on:click="$dispatch('notify-left', { type: 'success', content: 'Archive complete' })">success</button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] gap-1.5 bg-warning/10 text-warning border-transparent hover:bg-warning/20 focus-visible:ring-warning cursor-pointer" x-on:click="$dispatch('notify-left', { type: 'warning', content: 'Storage is running low' })">warning</button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] gap-1.5 bg-error/10 text-error border-transparent hover:bg-error/20 focus-visible:ring-error cursor-pointer" x-on:click="$dispatch('notify-left', { type: 'error', content: 'Backup failed' })">error</button>

outline / top-center / sm (event: notify-top)

<x-button color="info" appearance="outline" size="sm"
    x-on:click="$dispatch('notify-top', { type: 'info', content: 'Shown at the top center' })">info</x-button>
<x-button color="success" appearance="outline" size="sm"
    x-on:click="$dispatch('notify-top', { type: 'success', content: 'Copied' })">success</x-button>
<x-button color="warning" appearance="outline" size="sm"
    x-on:click="$dispatch('notify-top', { type: 'warning', content: 'Your session is expiring' })">warning</x-button>
<x-button color="error" appearance="outline" size="sm"
    x-on:click="$dispatch('notify-top', { type: 'error', content: 'You do not have permission' })">error</x-button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] gap-1.5 bg-transparent text-info border-info hover:bg-info hover:text-info-content focus-visible:ring-info cursor-pointer" x-on:click="$dispatch('notify-top', { type: 'info', content: 'Shown at the top center' })">info</button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] gap-1.5 bg-transparent text-success border-success hover:bg-success hover:text-success-content focus-visible:ring-success cursor-pointer" x-on:click="$dispatch('notify-top', { type: 'success', content: 'Copied' })">success</button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] gap-1.5 bg-transparent text-warning border-warning hover:bg-warning hover:text-warning-content focus-visible:ring-warning cursor-pointer" x-on:click="$dispatch('notify-top', { type: 'warning', content: 'Your session is expiring' })">warning</button>
<button class="inline-flex items-center justify-center whitespace-nowrap font-medium tracking-wide transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 rounded-[var(--radius-field)] border-[length:var(--border)] h-[var(--h-field-sm)] px-[var(--px-field-sm)] text-[length:var(--text-field-sm)] gap-1.5 bg-transparent text-error border-error hover:bg-error hover:text-error-content focus-visible:ring-error cursor-pointer" x-on:click="$dispatch('notify-top', { type: 'error', content: 'You do not have permission' })">error</button>

用法

1. 只需在 layout 中放置一次

<x-notification-system />
<div x-data="{ notifications: [], variants: JSON.parse('{\u0022info\u0022:\u0022bg-base-100 text-base-content border-base-content\\\/10 border-l-4 border-l-info\u0022,\u0022success\u0022:\u0022bg-base-100 text-base-content border-base-content\\\/10 border-l-4 border-l-success\u0022,\u0022warning\u0022:\u0022bg-base-100 text-base-content border-base-content\\\/10 border-l-4 border-l-warning\u0022,\u0022error\u0022:\u0022bg-base-100 text-base-content border-base-content\\\/10 border-l-4 border-l-error\u0022}'), iconColors: JSON.parse('{\u0022info\u0022:\u0022text-info\u0022,\u0022success\u0022:\u0022text-success\u0022,\u0022warning\u0022:\u0022text-warning\u0022,\u0022error\u0022:\u0022text-error\u0022}'), defaultVariant: 'bg-base-100 text-base-content border-base-content\/10 border-l-4 border-l-info', defaultIconColor: 'text-info', init() { }, add(e) { const id = e.timeStamp ?? Date.now() + Math.random(); this.notifications.push({ id, type: e.detail.type, content: e.detail.content, show: false, }); this.$nextTick(() =&gt; { const n = this.notifications.find(x =&gt; x.id === id); if (n) n.show = true; setTimeout(() =&gt; this.dismiss(id), 3000); }); }, dismiss(id) { const n = this.notifications.find(x =&gt; x.id === id); if (!n) return; n.show = false; setTimeout(() =&gt; { this.notifications = this.notifications.filter(x =&gt; x.id !== id); }, 300); }, variantFor(type) { return this.variants[type] || this.defaultVariant; }, iconColorFor(type) { return this.iconColors[type] || this.defaultIconColor; }, }" @notify.window="add($event)" class="fixed z-50 flex flex-col gap-3 max-w-sm w-full px-4 py-4 pointer-events-none bottom-0 right-0 items-end" role="status" aria-live="polite">
<template x-for="n in notifications" :key="n.id">
<div x-show="n.show" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0" x-transition:leave="transition ease-in duration-300" x-transition:leave-start="opacity-100 translate-y-0" x-transition:leave-end="opacity-0 translate-y-2" :class="variantFor(n.type)" class="pointer-events-auto flex items-start rounded-[var(--radius-box)] tune-border shadow-[var(--shadow-box)] min-h-[var(--h-field-md)] px-[var(--px-field-md)] py-2.5 text-[length:var(--text-field-md)] gap-3">
<div x-show="n.type === 'info'" class="shrink-0">
<svg class="w-5 h-5" :class="iconColorFor('info')" viewbox="0 0 24 24">
<g fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5"></circle>
<path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M12 17v-6"></path>
<circle cx="1" cy="1" r="1" fill="currentColor" transform="matrix(1 0 0 -1 11 9)"></circle>
</g></svg>
</div>
<div x-show="n.type === 'success'" class="shrink-0">
<svg class="w-5 h-5" :class="iconColorFor('success')" viewbox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="10"></circle>
<path stroke-linecap="round" stroke-linejoin="round" d="m8.5 12.5l2 2l5-5"></path>
</g></svg>
</div>
<div x-show="n.type === 'warning'" class="shrink-0">
<svg class="w-5 h-5" :class="iconColorFor('warning')" viewbox="0 0 24 24">
<g fill="none">
<path stroke="currentColor" stroke-width="1.5" d="M5.312 10.762C8.23 5.587 9.689 3 12 3s3.77 2.587 6.688 7.762l.364.644c2.425 4.3 3.638 6.45 2.542 8.022S17.786 21 12.364 21h-.728c-5.422 0-8.134 0-9.23-1.572s.117-3.722 2.542-8.022z"></path>
<path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M12 8v5"></path>
<circle cx="12" cy="16" r="1" fill="currentColor"></circle>
</g></svg>
</div>
<div x-show="n.type === 'error'" class="shrink-0">
<svg class="w-5 h-5" :class="iconColorFor('error')" viewbox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="10"></circle>
<path stroke-linecap="round" d="m14.5 9.5l-5 5m0-5l5 5"></path>
</g></svg>
</div>
<div class="flex-1 min-w-0">
<span x-text="n.content"></span>
</div><button type="button" @click="dismiss(n.id)" class="shrink-0 inline-flex items-center justify-center rounded-[var(--radius-field)] opacity-60 hover:opacity-100 transition-opacity cursor-pointer leading-none w-6 h-6 text-xl" aria-label="关闭">×</button>
</div>
</template>
</div>

2. 从任意组件中 dispatch

<button x-on:click="$dispatch('notify', { type: 'success', content: 'Saved' })">
    Save
</button>
<button x-on:click="$dispatch('notify', { type: 'success', content: 'Saved' })">Save</button>

3. 也可通过 Laravel 的 session flash 触发

return redirect()->back()->with('notify', ['type' => 'success', 'content' => 'Saved']);
return redirect()-&gt;back()-&gt;with('notify', ['type' =&gt; 'success', 'content' =&gt; 'Saved']);

4. 想更改位置和外观时

<x-notification-system position="top-right" appearance="solid" size="sm" :duration="5000" />
<div x-data="{ notifications: [], variants: JSON.parse('{\u0022info\u0022:\u0022bg-info text-info-content border-info\u0022,\u0022success\u0022:\u0022bg-success text-success-content border-success\u0022,\u0022warning\u0022:\u0022bg-warning text-warning-content border-warning\u0022,\u0022error\u0022:\u0022bg-error text-error-content border-error\u0022}'), iconColors: JSON.parse('{\u0022info\u0022:\u0022text-info-content\u0022,\u0022success\u0022:\u0022text-success-content\u0022,\u0022warning\u0022:\u0022text-warning-content\u0022,\u0022error\u0022:\u0022text-error-content\u0022}'), defaultVariant: 'bg-info text-info-content border-info', defaultIconColor: 'text-info-content', init() { }, add(e) { const id = e.timeStamp ?? Date.now() + Math.random(); this.notifications.push({ id, type: e.detail.type, content: e.detail.content, show: false, }); this.$nextTick(() =&gt; { const n = this.notifications.find(x =&gt; x.id === id); if (n) n.show = true; setTimeout(() =&gt; this.dismiss(id), 5000); }); }, dismiss(id) { const n = this.notifications.find(x =&gt; x.id === id); if (!n) return; n.show = false; setTimeout(() =&gt; { this.notifications = this.notifications.filter(x =&gt; x.id !== id); }, 300); }, variantFor(type) { return this.variants[type] || this.defaultVariant; }, iconColorFor(type) { return this.iconColors[type] || this.defaultIconColor; }, }" @notify.window="add($event)" class="fixed z-50 flex flex-col gap-3 max-w-sm w-full px-4 py-4 pointer-events-none top-0 right-0 items-end" role="status" aria-live="polite">
<template x-for="n in notifications" :key="n.id">
<div x-show="n.show" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0" x-transition:leave="transition ease-in duration-300" x-transition:leave-start="opacity-100 translate-y-0" x-transition:leave-end="opacity-0 translate-y-2" :class="variantFor(n.type)" class="pointer-events-auto flex items-start rounded-[var(--radius-box)] tune-border shadow-[var(--shadow-box)] min-h-[var(--h-field-sm)] px-[var(--px-field-sm)] py-2 text-[length:var(--text-field-sm)] gap-2">
<div x-show="n.type === 'info'" class="shrink-0">
<svg class="w-4 h-4" :class="iconColorFor('info')" viewbox="0 0 24 24">
<g fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5"></circle>
<path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M12 17v-6"></path>
<circle cx="1" cy="1" r="1" fill="currentColor" transform="matrix(1 0 0 -1 11 9)"></circle>
</g></svg>
</div>
<div x-show="n.type === 'success'" class="shrink-0">
<svg class="w-4 h-4" :class="iconColorFor('success')" viewbox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="10"></circle>
<path stroke-linecap="round" stroke-linejoin="round" d="m8.5 12.5l2 2l5-5"></path>
</g></svg>
</div>
<div x-show="n.type === 'warning'" class="shrink-0">
<svg class="w-4 h-4" :class="iconColorFor('warning')" viewbox="0 0 24 24">
<g fill="none">
<path stroke="currentColor" stroke-width="1.5" d="M5.312 10.762C8.23 5.587 9.689 3 12 3s3.77 2.587 6.688 7.762l.364.644c2.425 4.3 3.638 6.45 2.542 8.022S17.786 21 12.364 21h-.728c-5.422 0-8.134 0-9.23-1.572s.117-3.722 2.542-8.022z"></path>
<path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M12 8v5"></path>
<circle cx="12" cy="16" r="1" fill="currentColor"></circle>
</g></svg>
</div>
<div x-show="n.type === 'error'" class="shrink-0">
<svg class="w-4 h-4" :class="iconColorFor('error')" viewbox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="10"></circle>
<path stroke-linecap="round" d="m14.5 9.5l-5 5m0-5l5 5"></path>
</g></svg>
</div>
<div class="flex-1 min-w-0">
<span x-text="n.content"></span>
</div><button type="button" @click="dismiss(n.id)" class="shrink-0 inline-flex items-center justify-center rounded-[var(--radius-field)] opacity-60 hover:opacity-100 transition-opacity cursor-pointer leading-none w-5 h-5 text-base" aria-label="关闭">×</button>
</div>
</template>
</div>

5. 如需在同一页面共存多个,请用 event-name 区分

<x-notification-system position="top-center" event-name="notify-toast" />
<button x-on:click="$dispatch('notify-toast', { type: 'info', content: '…' })"></button>
<div x-data="{ notifications: [], variants: JSON.parse('{\u0022info\u0022:\u0022bg-base-100 text-base-content border-base-content\\\/10 border-l-4 border-l-info\u0022,\u0022success\u0022:\u0022bg-base-100 text-base-content border-base-content\\\/10 border-l-4 border-l-success\u0022,\u0022warning\u0022:\u0022bg-base-100 text-base-content border-base-content\\\/10 border-l-4 border-l-warning\u0022,\u0022error\u0022:\u0022bg-base-100 text-base-content border-base-content\\\/10 border-l-4 border-l-error\u0022}'), iconColors: JSON.parse('{\u0022info\u0022:\u0022text-info\u0022,\u0022success\u0022:\u0022text-success\u0022,\u0022warning\u0022:\u0022text-warning\u0022,\u0022error\u0022:\u0022text-error\u0022}'), defaultVariant: 'bg-base-100 text-base-content border-base-content\/10 border-l-4 border-l-info', defaultIconColor: 'text-info', init() { }, add(e) { const id = e.timeStamp ?? Date.now() + Math.random(); this.notifications.push({ id, type: e.detail.type, content: e.detail.content, show: false, }); this.$nextTick(() =&gt; { const n = this.notifications.find(x =&gt; x.id === id); if (n) n.show = true; setTimeout(() =&gt; this.dismiss(id), 3000); }); }, dismiss(id) { const n = this.notifications.find(x =&gt; x.id === id); if (!n) return; n.show = false; setTimeout(() =&gt; { this.notifications = this.notifications.filter(x =&gt; x.id !== id); }, 300); }, variantFor(type) { return this.variants[type] || this.defaultVariant; }, iconColorFor(type) { return this.iconColors[type] || this.defaultIconColor; }, }" @notify-toast.window="add($event)" class="fixed z-50 flex flex-col gap-3 max-w-sm w-full px-4 py-4 pointer-events-none top-0 left-1/2 -translate-x-1/2 items-center" role="status" aria-live="polite">
<template x-for="n in notifications" :key="n.id">
<div x-show="n.show" x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0" x-transition:leave="transition ease-in duration-300" x-transition:leave-start="opacity-100 translate-y-0" x-transition:leave-end="opacity-0 translate-y-2" :class="variantFor(n.type)" class="pointer-events-auto flex items-start rounded-[var(--radius-box)] tune-border shadow-[var(--shadow-box)] min-h-[var(--h-field-md)] px-[var(--px-field-md)] py-2.5 text-[length:var(--text-field-md)] gap-3">
<div x-show="n.type === 'info'" class="shrink-0">
<svg class="w-5 h-5" :class="iconColorFor('info')" viewbox="0 0 24 24">
<g fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5"></circle>
<path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M12 17v-6"></path>
<circle cx="1" cy="1" r="1" fill="currentColor" transform="matrix(1 0 0 -1 11 9)"></circle>
</g></svg>
</div>
<div x-show="n.type === 'success'" class="shrink-0">
<svg class="w-5 h-5" :class="iconColorFor('success')" viewbox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="10"></circle>
<path stroke-linecap="round" stroke-linejoin="round" d="m8.5 12.5l2 2l5-5"></path>
</g></svg>
</div>
<div x-show="n.type === 'warning'" class="shrink-0">
<svg class="w-5 h-5" :class="iconColorFor('warning')" viewbox="0 0 24 24">
<g fill="none">
<path stroke="currentColor" stroke-width="1.5" d="M5.312 10.762C8.23 5.587 9.689 3 12 3s3.77 2.587 6.688 7.762l.364.644c2.425 4.3 3.638 6.45 2.542 8.022S17.786 21 12.364 21h-.728c-5.422 0-8.134 0-9.23-1.572s.117-3.722 2.542-8.022z"></path>
<path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M12 8v5"></path>
<circle cx="12" cy="16" r="1" fill="currentColor"></circle>
</g></svg>
</div>
<div x-show="n.type === 'error'" class="shrink-0">
<svg class="w-5 h-5" :class="iconColorFor('error')" viewbox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="10"></circle>
<path stroke-linecap="round" d="m14.5 9.5l-5 5m0-5l5 5"></path>
</g></svg>
</div>
<div class="flex-1 min-w-0">
<span x-text="n.content"></span>
</div><button type="button" @click="dismiss(n.id)" class="shrink-0 inline-flex items-center justify-center rounded-[var(--radius-field)] opacity-60 hover:opacity-100 transition-opacity cursor-pointer leading-none w-6 h-6 text-xl" aria-label="关闭">×</button>
</div>
</template>
</div><button x-on:click="$dispatch('notify-toast', { type: 'info', content: '…' })"></button>