diff --git a/app/pages/about/index.vue b/app/pages/about/index.vue index 15de4bb..4c21410 100644 --- a/app/pages/about/index.vue +++ b/app/pages/about/index.vue @@ -1,5 +1,70 @@ + + \ No newline at end of file diff --git a/app/pages/admin.vue b/app/pages/admin.vue index 61e22c2..cb34991 100644 --- a/app/pages/admin.vue +++ b/app/pages/admin.vue @@ -15,11 +15,6 @@ const adminNav = [ to: '/admin/scheduler', icon: 'schedule' }, - { - label: '个人资料', - to: '/admin/profile', - icon: 'user' - }, ] const iconPaths: Record = { diff --git a/app/pages/auth/login.vue b/app/pages/auth/login.vue index f24e460..cacba61 100644 --- a/app/pages/auth/login.vue +++ b/app/pages/auth/login.vue @@ -4,6 +4,7 @@ definePageMeta({ }) const route = useRoute() +const { $toast } = useNuxtApp() const redirect = computed(() => route.query.redirect as string || '/') const loginForm = reactive({ @@ -19,26 +20,27 @@ const captcha = reactive({ loading: false, }) -const loginError = ref('') const loginLoading = ref(false) const { refresh } = useAuthSession() async function fetchCaptcha() { captcha.loading = true + const loadingToast = $toast.loading('加载验证码...', { toastId: 'captcha-loading' }) try { const res = await $fetch<{ code: number; data: { captchaId: string; imageSvg: string } }>('/api/auth/captcha') captcha.id = res.data.captchaId captcha.svg = res.data.imageSvg captcha.answer = '' + $toast.update(loadingToast, { render: '验证码加载成功', type: 'success', isLoading: false, autoClose: 1500 }) } catch (e: any) { console.error('获取验证码失败', e) + $toast.update(loadingToast, { render: '验证码加载失败', type: 'error', isLoading: false, autoClose: 3000 }) } finally { captcha.loading = false } } async function handleLogin() { - loginError.value = '' loginLoading.value = true try { await $fetch('/api/auth/login', { @@ -50,10 +52,11 @@ async function handleLogin() { captchaAnswer: captcha.answer, }, }) + $toast.success('登录成功!') await refresh(true) await navigateTo(redirect.value) } catch (e: any) { - loginError.value = e?.data?.statusMessage || e?.message || '登录失败' + $toast.error(e?.data?.statusMessage || e?.message || '登录失败') await fetchCaptcha() } finally { loginLoading.value = false @@ -87,8 +90,6 @@ onMounted(fetchCaptcha)
-
{{ loginError }}
-
@@ -404,15 +405,6 @@ onMounted(fetchCaptcha) opacity: 0.5; } -.form-error { - padding: 12px 16px; - background: rgba(198, 69, 69, 0.08); - border: 1px solid rgba(198, 69, 69, 0.2); - border-radius: 6px; - color: var(--color-error); - font-size: 13px; -} - .form-options { display: flex; align-items: center; diff --git a/app/plugins/vue3-toastify.client.ts b/app/plugins/vue3-toastify.client.ts new file mode 100644 index 0000000..ebc2070 --- /dev/null +++ b/app/plugins/vue3-toastify.client.ts @@ -0,0 +1,18 @@ +import Vue3Toastify, { toast } from 'vue3-toastify'; +import 'vue3-toastify/dist/index.css'; +import './vue3-toastify.css' + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.use(Vue3Toastify, { + autoClose: 3000, + newestOnTop: true, + clearOnUrlChange: false, + theme: "colored", + transition: "slide", + position: "top-right", + }); + + return { + provide: { toast }, + }; +}); \ No newline at end of file diff --git a/app/plugins/vue3-toastify.css b/app/plugins/vue3-toastify.css new file mode 100644 index 0000000..92cccb3 --- /dev/null +++ b/app/plugins/vue3-toastify.css @@ -0,0 +1,127 @@ + +/* ── Toast Customization (vue3-toastify) ── */ +:root { + --toastify-color-light: #efe9de; + --toastify-color-dark: #181715; + --toastify-color-info: #5db8a6; + --toastify-color-success: #5db872; + --toastify-color-warning: #e8a55a; + --toastify-color-error: #c64545; + --toastify-text-color-light: #141413; + --toastify-text-color-dark: #faf9f5; + --toastify-text-color-info: #ffffff; + --toastify-text-color-success: #ffffff; + --toastify-text-color-warning: #ffffff; + --toastify-text-color-error: #ffffff; + --toastify-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +.Toastify__toast-container { + font-family: var(--font-body); + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; +} + +.Toastify__toast { + border-radius: 10px; + box-shadow: 0 1px 3px rgba(20, 20, 19, 0.06), 0 4px 12px rgba(20, 20, 19, 0.04); + min-height: 48px; + padding: 12px 14px; + border: 1px solid #e6dfd8; + margin: 0; +} + +.Toastify__toast--default { + background: #efe9de; + color: #141413; + border-color: #e6dfd8; +} + +.Toastify__toast--info { + background: #5db8a6; + color: #ffffff; + border-color: #4da396; +} + +.Toastify__toast--success { + background: #5db872; + color: #ffffff; + border-color: #4da362; +} + +.Toastify__toast--warning { + background: #e8a55a; + color: #ffffff; + border-color: #d8944b; +} + +.Toastify__toast--error { + background: #c64545; + color: #ffffff; + border-color: #b53d3d; +} + +.Toastify__toast-body { + font-family: var(--font-body); + font-size: 14px; + color: inherit; + padding: 0 4px; +} + +.Toastify__toast-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 10px; +} + +.Toastify__close-button { + color: inherit; + opacity: 0.4; + border-radius: 4px; + padding: 2px 6px; + margin-left: 10px; + transition: opacity 0.2s ease; + align-self: center; + font-size: 18px; + line-height: 1; +} + +.Toastify__close-button:hover { + opacity: 0.8; +} + +.Toastify__progress-bar { + height: 2px; + border-radius: 0 0 8px 8px; + opacity: 0.5; +} + +.Toastify__progress-bar--info { background: rgba(255, 255, 255, 0.5); } +.Toastify__progress-bar--success { background: rgba(255, 255, 255, 0.5); } +.Toastify__progress-bar--warning { background: rgba(255, 255, 255, 0.5); } +.Toastify__progress-bar--error { background: rgba(255, 255, 255, 0.5); } +.Toastify__progress-bar--default { background: rgba(20, 20, 19, 0.12); } + +/* Slide animation - gentler */ +.Toastify__slide-enter--top-right, +.Toastify__slide-enter--bottom-right { + animation-name: Toastify__slideInRight; +} + +@keyframes Toastify__slideInRight { + from { transform: translateX(110%); } + to { transform: translateX(0); } +} + +/* Loading toast - dark style */ +.Toastify__toast--loading { + background: #181715; + color: #faf9f5; + border-color: #252320; + min-width: 260px; +} + + diff --git a/bun.lock b/bun.lock index e4443d2..e000a57 100644 --- a/bun.lock +++ b/bun.lock @@ -34,6 +34,7 @@ "tsconfig": "workspace:*", "tsx": "4.21.0", "typescript": "6.0.2", + "vue3-toastify": "^0.2.9", }, }, "packages/cache": { @@ -1587,6 +1588,8 @@ "vue-router": ["vue-router@5.0.4", "https://registry.npmmirror.com/vue-router/-/vue-router-5.0.4.tgz", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg=="], + "vue3-toastify": ["vue3-toastify@0.2.9", "", { "peerDependencies": { "vue": ">=3.2.0" }, "optionalPeers": ["vue"] }, "sha512-LN2GJGKgjt+C6IIBANp9hCM2A4yc5jC2Kj4YGl1J7ptj4rhThOJOGGlkCg+IxDt7hntNw9n3MG9ifv/AymyxKQ=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], diff --git a/docs/superpowers/plans/2026-05-26-login-register-toast-design.md b/docs/superpowers/plans/2026-05-26-login-register-toast-design.md new file mode 100644 index 0000000..89f7cbf --- /dev/null +++ b/docs/superpowers/plans/2026-05-26-login-register-toast-design.md @@ -0,0 +1,261 @@ +# Login/Register Toast 体验优化 + +**Goal:** 用 vue3-toastify 替换登录/注册页面的静态错误提示,提升用户体验。 + +**Architecture:** 在 login.vue 和 register.vue 中注入 toast 服务,移除静态 `form-error` div,改为 toast 弹出提示。成功/加载状态同样使用 toast 反馈。 + +**Tech Stack:** vue3-toastify (已安装), Nuxt 3, Vue 3 Composition API + +--- + +## 文件变更 + +- Modify: `app/pages/auth/login.vue` +- Modify: `app/pages/auth/register.vue` + +--- + +## Task 1: 优化 login.vue + +**Files:** +- Modify: `app/pages/auth/login.vue` + +- [ ] **Step 1: 注入 toast 服务** + +在 `