9 changed files with 484 additions and 22 deletions
@ -1,5 +1,70 @@ |
|||
<script setup> |
|||
const { $toast } = useNuxtApp(); |
|||
|
|||
const notifyAll = () => { |
|||
$toast('这是一条默认消息', { type: 'default' }); |
|||
$toast.info('这是一条 info 消息'); |
|||
$toast.success('这是一条 success 消息'); |
|||
$toast.warning('这是一条 warning 消息'); |
|||
$toast.error('这是一条 error 消息'); |
|||
$toast.loading('加载中...'); |
|||
}; |
|||
|
|||
const notifyDefault = () => $toast('默认消息 default'); |
|||
const notifyInfo = () => $toast.info('Info 消息'); |
|||
const notifySuccess = () => $toast.success('Success 消息'); |
|||
const notifyWarning = () => $toast.warning('Warning 消息'); |
|||
const notifyError = () => $toast.error('Error 消息'); |
|||
</script> |
|||
|
|||
<template> |
|||
<div> |
|||
dsa |
|||
<div class="p-8 flex flex-col gap-4 max-w-md"> |
|||
<h1 class="text-2xl font-semibold mb-4">Toast 测试</h1> |
|||
|
|||
<button |
|||
class="px-4 py-2 bg-[#cc785c] text-white rounded-lg hover:bg-[#a9583e] transition-colors" |
|||
@click="notifyAll" |
|||
> |
|||
全部类型 |
|||
</button> |
|||
|
|||
<div class="flex gap-2 flex-wrap"> |
|||
<button |
|||
class="px-4 py-2 bg-[#efe9de] border border-[#e6dfd8] rounded-lg hover:bg-[#e8e0d2] transition-colors" |
|||
@click="notifyDefault" |
|||
> |
|||
Default |
|||
</button> |
|||
<button |
|||
class="px-4 py-2 bg-[#5db8a6] text-white rounded-lg hover:opacity-90 transition-opacity" |
|||
@click="notifyInfo" |
|||
> |
|||
Info |
|||
</button> |
|||
<button |
|||
class="px-4 py-2 bg-[#5db872] text-white rounded-lg hover:opacity-90 transition-opacity" |
|||
@click="notifySuccess" |
|||
> |
|||
Success |
|||
</button> |
|||
<button |
|||
class="px-4 py-2 bg-[#e8a55a] text-white rounded-lg hover:opacity-90 transition-opacity" |
|||
@click="notifyWarning" |
|||
> |
|||
Warning |
|||
</button> |
|||
<button |
|||
class="px-4 py-2 bg-[#c64545] text-white rounded-lg hover:opacity-90 transition-opacity" |
|||
@click="notifyError" |
|||
> |
|||
Error |
|||
</button> |
|||
<button |
|||
class="px-4 py-2 bg-[#181715] text-white rounded-lg hover:opacity-90 transition-opacity" |
|||
@click="$toast.loading('加载中...')" |
|||
> |
|||
Loading |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
@ -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 }, |
|||
}; |
|||
}); |
|||
@ -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; |
|||
} |
|||
|
|||
|
|||
@ -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 服务** |
|||
|
|||
在 `<script setup>` 开头添加: |
|||
```typescript |
|||
const { $toast } = useNuxtApp() |
|||
``` |
|||
|
|||
- [ ] **Step 2: 移除 form-error div 并添加 toast 注入** |
|||
|
|||
删除模板中的: |
|||
```html |
|||
<div v-if="loginError" class="form-error">{{ loginError }}</div> |
|||
``` |
|||
|
|||
- [ ] **Step 3: 修改 handleLogin 错误处理** |
|||
|
|||
将: |
|||
```typescript |
|||
loginError.value = e?.data?.statusMessage || e?.message || '登录失败' |
|||
``` |
|||
|
|||
改为: |
|||
```typescript |
|||
$toast.error(e?.data?.statusMessage || e?.message || '登录失败') |
|||
await fetchCaptcha() |
|||
``` |
|||
|
|||
- [ ] **Step 4: 修改 handleLogin 成功处理** |
|||
|
|||
将: |
|||
```typescript |
|||
await refresh(true) |
|||
await navigateTo(redirect.value) |
|||
``` |
|||
|
|||
改为: |
|||
```typescript |
|||
$toast.success('登录成功!') |
|||
await refresh(true) |
|||
await navigateTo(redirect.value) |
|||
``` |
|||
|
|||
- [ ] **Step 5: 修改 fetchCaptcha 加载状态** |
|||
|
|||
将: |
|||
```typescript |
|||
async function fetchCaptcha() { |
|||
captcha.loading = true |
|||
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 = '' |
|||
} catch (e: any) { |
|||
console.error('获取验证码失败', e) |
|||
} finally { |
|||
captcha.loading = false |
|||
} |
|||
} |
|||
``` |
|||
|
|||
改为: |
|||
```typescript |
|||
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 |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- [ ] **Step 6: 删除 form-error 相关样式** |
|||
|
|||
删除 CSS 中的 `.form-error { ... }` 样式块 |
|||
|
|||
- [ ] **Step 7: 提交代码** |
|||
|
|||
```bash |
|||
git add app/pages/auth/login.vue |
|||
git commit -m "feat(auth): add toast notifications to login page" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Task 2: 优化 register.vue |
|||
|
|||
**Files:** |
|||
- Modify: `app/pages/auth/register.vue` |
|||
|
|||
- [ ] **Step 1: 注入 toast 服务** |
|||
|
|||
在 `<script setup>` 开头添加: |
|||
```typescript |
|||
const { $toast } = useNuxtApp() |
|||
``` |
|||
|
|||
- [ ] **Step 2: 移除 form-error div** |
|||
|
|||
删除模板中的: |
|||
```html |
|||
<div v-if="registerError" class="form-error">{{ registerError }}</div> |
|||
``` |
|||
|
|||
- [ ] **Step 3: 移除 registerError ref** |
|||
|
|||
删除: |
|||
```typescript |
|||
const registerError = ref('') |
|||
``` |
|||
|
|||
- [ ] **Step 4: 修改 handleRegister 表单验证错误** |
|||
|
|||
将: |
|||
```typescript |
|||
if (registerForm.password !== registerForm.confirmPassword) { |
|||
registerError.value = '两次密码输入不一致' |
|||
return |
|||
} |
|||
``` |
|||
|
|||
改为: |
|||
```typescript |
|||
if (registerForm.password !== registerForm.confirmPassword) { |
|||
$toast.error('两次密码输入不一致') |
|||
return |
|||
} |
|||
``` |
|||
|
|||
- [ ] **Step 5: 修改 handleRegister 请求错误处理** |
|||
|
|||
将: |
|||
```typescript |
|||
} catch (e: any) { |
|||
registerError.value = e?.data?.statusMessage || e?.message || '注册失败' |
|||
await fetchCaptcha() |
|||
``` |
|||
|
|||
改为: |
|||
```typescript |
|||
} catch (e: any) { |
|||
$toast.error(e?.data?.statusMessage || e?.message || '注册失败') |
|||
await fetchCaptcha() |
|||
``` |
|||
|
|||
- [ ] **Step 6: 修改 handleRegister 成功处理** |
|||
|
|||
将: |
|||
```typescript |
|||
await navigateTo('/auth/login?tab=login') |
|||
``` |
|||
|
|||
改为: |
|||
```typescript |
|||
$toast.success('注册成功!正在跳转...') |
|||
setTimeout(() => navigateTo('/auth/login?tab=login'), 1500) |
|||
``` |
|||
|
|||
- [ ] **Step 7: 修改 fetchCaptcha 加载状态** (同 login.vue) |
|||
|
|||
将: |
|||
```typescript |
|||
async function fetchCaptcha() { |
|||
captcha.loading = true |
|||
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 = '' |
|||
} catch (e: any) { |
|||
console.error('获取验证码失败', e) |
|||
} finally { |
|||
captcha.loading = false |
|||
} |
|||
} |
|||
``` |
|||
|
|||
改为: |
|||
```typescript |
|||
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 |
|||
} |
|||
} |
|||
``` |
|||
|
|||
- [ ] **Step 8: 删除 form-error 和 registerError 相关样式/逻辑** |
|||
|
|||
删除 CSS 中的 `.form-error { ... }` 样式块 |
|||
|
|||
- [ ] **Step 9: 提交代码** |
|||
|
|||
```bash |
|||
git add app/pages/auth/register.vue |
|||
git commit -m "feat(auth): add toast notifications to register page" |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## 验证步骤 |
|||
|
|||
1. 启动开发服务器: `npm run dev` |
|||
2. 访问 `/auth/login`,测试以下场景: |
|||
- 输入错误信息点击登录 → 应看到 toast 错误提示 |
|||
- 登录成功 → 应看到 toast 成功提示并跳转 |
|||
- 刷新页面 → 验证码加载中应有 toast 提示 |
|||
3. 访问 `/auth/register`,测试以下场景: |
|||
- 两次密码不一致 → 应看到 toast 错误提示 |
|||
- 注册成功 → 应看到 toast 成功提示并延迟跳转 |
|||
- 验证码加载 → 应看到 toast 加载状态 |
|||
|
|||
--- |
|||
|
|||
**Plan complete.** 可选执行方式: |
|||
|
|||
**1. Subagent-Driven (推荐)** - 我派发独立 subagent 执行每个任务 |
|||
**2. Inline Execution** - 本会话内顺序执行 |
|||
|
|||
选择哪种方式? |
|||
Binary file not shown.
Loading…
Reference in new issue