From 33f5e7b71a607ed84ba6696ef34c6dd8d8f438ba Mon Sep 17 00:00:00 2001 From: Sakurasan <26715255+Sakurasan@users.noreply.github.com> Date: Sun, 30 Aug 2026 01:54:57 +0800 Subject: [PATCH] rewrite: toast stacking with daisyui toast container - module-level reactive toast list in composables/toast (setToast signature unchanged, 13 call sites untouched) - multiple toasts stack simultaneously in daisyUI toast container, each auto-dismisses (3s) with close button - TransitionGroup enter/leave animation (transform/opacity, honors reduced-motion) - drop provide/inject queue that blocked consecutive toasts --- REFACTOR_PLAN.md | 8 +++ frontend/src/App.vue | 19 +---- frontend/src/components/common/Toast.vue | 92 +++++++++++------------- frontend/src/composables/toast.ts | 41 +++++++---- 4 files changed, 78 insertions(+), 82 deletions(-) diff --git a/REFACTOR_PLAN.md b/REFACTOR_PLAN.md index f32229d..81f4dfe 100644 --- a/REFACTOR_PLAN.md +++ b/REFACTOR_PLAN.md @@ -284,3 +284,11 @@ src/ - 背景:daisyUI dark 主题的 primary 为紫色,普通主按钮(btn-primary)在深色模式下显示为紫底。 - 方案:`main.css` 覆写 `.btn-primary` 的 daisyUI 颜色变量(`--btn-color` / `--btn-fg`)——浅色主题黑底白字(#171717/#fff,hover 纯黑)、深色主题白底黑字(#fff/#171717,hover 浅灰)。仅影响 btn-primary;success/error/warning/outline/ghost 等特殊按钮与链接、开关、焦点环均保持原样。 - 验证:`pnpm build` 通过;浏览器实测深色(白底黑字 New Token)与浅色(黑底白字 Log In)两种主题,特殊按钮未受影响;已恢复维护者的 auto 主题偏好与 admin 会话。 + +### 增量 7 — Toast 重写:多实例堆叠(维护者反馈)✅ + +- 背景:原实现为串行队列(processQueue 一次展示一条),连续操作时提示互相阻塞。 +- daisyUI 的 `toast` 组件本身只负责定位与堆叠(容器内多个 `alert` 自动纵向排列),队列/自动消失/动画需应用层实现——已按此重写: + - `composables/toast.ts`:模块级响应式 `toasts` 列表,`setToast(message, type?, duration?)` 推入带唯一 id 的条目并定时自动移除(默认 3s);`useToast()` 签名不变,13 个调用视图零改动;移除原 provide/inject 方案。 + - `Toast.vue`:daisyUI `toast toast-top toast-end` 容器 + `TransitionGroup` 进出场动画(仅 transform/opacity,配合全局 reduced-motion 降级)、每条带关闭按钮(aria-label)、容器 `aria-live="polite"`。 +- 验证:`pnpm build` 通过;浏览器实测连续触发两条 toast 同时堆叠展示、3s 后全部自动消失。 diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 0ffd764..fe20c03 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,25 +1,8 @@ diff --git a/frontend/src/components/common/Toast.vue b/frontend/src/components/common/Toast.vue index b3b39e6..5f9fd63 100644 --- a/frontend/src/components/common/Toast.vue +++ b/frontend/src/components/common/Toast.vue @@ -1,56 +1,50 @@ - + + + diff --git a/frontend/src/composables/toast.ts b/frontend/src/composables/toast.ts index b773487..4daa4ea 100644 --- a/frontend/src/composables/toast.ts +++ b/frontend/src/composables/toast.ts @@ -1,24 +1,35 @@ -import { inject } from 'vue' -import type { InjectionKey } from 'vue' +import { ref } from 'vue' export type ToastType = 'info' | 'success' | 'error' -export type ToastMessage = { - message: string - type?: ToastType - duration?: number +export type ToastItem = { + id: number + message: string + type: ToastType + duration: number } -export type ToastContext = { - setToast: (message: string, type?: ToastType, duration?: number) => void +// 模块级共享状态:所有活动 toast 堆叠展示,互不阻塞 +const toasts = ref([]) +let seed = 0 + +function dismiss(id: number) { + toasts.value = toasts.value.filter(t => t.id !== id) } -export const ToastKey: InjectionKey = Symbol('toast') +export function setToast(message: string, type: ToastType = 'info', duration = 3000) { + const id = ++seed + toasts.value.push({ id, message, type, duration }) + if (duration > 0) { + setTimeout(() => dismiss(id), duration) + } +} -export function useToast(): ToastContext { - const ctx = inject(ToastKey) - if (!ctx) { - throw new Error('ToastContext 未提供:请在 App 根组件 provide(ToastKey, ...)') - } - return ctx +export function useToast() { + return { setToast } +} + +// 供 Toast 组件渲染使用 +export function useToasts() { + return { toasts, dismiss } }