style(frontend): 统一 Views 模块代码风格
- 移除语句末尾分号,规范代码格式 - 优化组件结构和类型定义 - 改进视图文档和示例 - 提升代码一致性
This commit is contained in:
@@ -75,43 +75,43 @@ This document provides practical examples of how to use the authentication views
|
||||
|
||||
```typescript
|
||||
// Method 1: Direct import
|
||||
import LoginView from '@/views/auth/LoginView.vue';
|
||||
import RegisterView from '@/views/auth/RegisterView.vue';
|
||||
import LoginView from '@/views/auth/LoginView.vue'
|
||||
import RegisterView from '@/views/auth/RegisterView.vue'
|
||||
|
||||
// Method 2: Named exports from index
|
||||
import { LoginView, RegisterView } from '@/views/auth';
|
||||
import { LoginView, RegisterView } from '@/views/auth'
|
||||
|
||||
// Method 3: Lazy loading (recommended for routes)
|
||||
const LoginView = () => import('@/views/auth/LoginView.vue');
|
||||
const RegisterView = () => import('@/views/auth/RegisterView.vue');
|
||||
const LoginView = () => import('@/views/auth/LoginView.vue')
|
||||
const RegisterView = () => import('@/views/auth/RegisterView.vue')
|
||||
```
|
||||
|
||||
### Using in Router
|
||||
|
||||
```typescript
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/auth/LoginView.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: () => import('@/views/auth/RegisterView.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
},
|
||||
];
|
||||
meta: { requiresAuth: false }
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
routes
|
||||
})
|
||||
|
||||
export default router;
|
||||
export default router
|
||||
```
|
||||
|
||||
### Navigation to Auth Views
|
||||
@@ -142,13 +142,13 @@ router.push({
|
||||
### Programmatic Auth Flow
|
||||
|
||||
```typescript
|
||||
import { useAuthStore } from '@/stores';
|
||||
import { useAppStore } from '@/stores';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores'
|
||||
import { useAppStore } from '@/stores'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const appStore = useAppStore();
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore()
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
// Login
|
||||
async function login() {
|
||||
@@ -156,12 +156,12 @@ async function login() {
|
||||
await authStore.login({
|
||||
username: 'john_doe',
|
||||
password: 'MySecurePass123'
|
||||
});
|
||||
})
|
||||
|
||||
appStore.showSuccess('Login successful!');
|
||||
router.push('/dashboard');
|
||||
appStore.showSuccess('Login successful!')
|
||||
router.push('/dashboard')
|
||||
} catch (error) {
|
||||
appStore.showError('Login failed. Please check your credentials.');
|
||||
appStore.showError('Login failed. Please check your credentials.')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,12 +172,12 @@ async function register() {
|
||||
username: 'jane_smith',
|
||||
email: 'jane@example.com',
|
||||
password: 'SecurePass123'
|
||||
});
|
||||
})
|
||||
|
||||
appStore.showSuccess('Account created successfully!');
|
||||
router.push('/dashboard');
|
||||
appStore.showSuccess('Account created successfully!')
|
||||
router.push('/dashboard')
|
||||
} catch (error) {
|
||||
appStore.showError('Registration failed. Please try again.');
|
||||
appStore.showError('Registration failed. Please try again.')
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -236,7 +236,7 @@ async function register() {
|
||||
{
|
||||
response: {
|
||||
data: {
|
||||
detail: "Invalid username or password"
|
||||
detail: 'Invalid username or password'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,12 +244,13 @@ async function register() {
|
||||
|
||||
// Example 3: Network error
|
||||
{
|
||||
message: "Network Error"
|
||||
message: 'Network Error'
|
||||
}
|
||||
// Displayed: "Network Error" + Error toast
|
||||
|
||||
// Example 4: Unknown error
|
||||
{}
|
||||
{
|
||||
}
|
||||
// Displayed: "Login failed. Please check your credentials and try again." (default)
|
||||
```
|
||||
|
||||
@@ -258,10 +259,10 @@ async function register() {
|
||||
```typescript
|
||||
// Multiple validation errors displayed simultaneously
|
||||
errors = {
|
||||
username: "Username must be at least 3 characters",
|
||||
email: "Please enter a valid email address",
|
||||
password: "Password must be at least 8 characters with letters and numbers",
|
||||
confirmPassword: "Passwords do not match"
|
||||
username: 'Username must be at least 3 characters',
|
||||
email: 'Please enter a valid email address',
|
||||
password: 'Password must be at least 8 characters with letters and numbers',
|
||||
confirmPassword: 'Passwords do not match'
|
||||
}
|
||||
|
||||
// Each error appears below its respective input field with red styling
|
||||
@@ -272,86 +273,86 @@ errors = {
|
||||
### Unit Test: Login View
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createPinia } from 'pinia';
|
||||
import LoginView from '@/views/auth/LoginView.vue';
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import LoginView from '@/views/auth/LoginView.vue'
|
||||
|
||||
describe('LoginView', () => {
|
||||
it('validates required fields', async () => {
|
||||
const wrapper = mount(LoginView, {
|
||||
global: {
|
||||
plugins: [createPinia()],
|
||||
},
|
||||
});
|
||||
plugins: [createPinia()]
|
||||
}
|
||||
})
|
||||
|
||||
// Submit empty form
|
||||
await wrapper.find('form').trigger('submit');
|
||||
await wrapper.find('form').trigger('submit')
|
||||
|
||||
// Check for validation errors
|
||||
expect(wrapper.text()).toContain('Username is required');
|
||||
expect(wrapper.text()).toContain('Password is required');
|
||||
});
|
||||
expect(wrapper.text()).toContain('Username is required')
|
||||
expect(wrapper.text()).toContain('Password is required')
|
||||
})
|
||||
|
||||
it('calls authStore.login on valid submission', async () => {
|
||||
const wrapper = mount(LoginView, {
|
||||
global: {
|
||||
plugins: [createPinia()],
|
||||
},
|
||||
});
|
||||
plugins: [createPinia()]
|
||||
}
|
||||
})
|
||||
|
||||
// Fill in form
|
||||
await wrapper.find('#username').setValue('john_doe');
|
||||
await wrapper.find('#password').setValue('SecurePass123');
|
||||
await wrapper.find('#username').setValue('john_doe')
|
||||
await wrapper.find('#password').setValue('SecurePass123')
|
||||
|
||||
// Submit form
|
||||
await wrapper.find('form').trigger('submit');
|
||||
await wrapper.find('form').trigger('submit')
|
||||
|
||||
// Verify authStore.login was called
|
||||
// (mock implementation needed)
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### E2E Test: Registration Flow
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('user can register successfully', async ({ page }) => {
|
||||
// Navigate to register page
|
||||
await page.goto('/register');
|
||||
await page.goto('/register')
|
||||
|
||||
// Fill in registration form
|
||||
await page.fill('#username', 'new_user');
|
||||
await page.fill('#email', 'new_user@example.com');
|
||||
await page.fill('#password', 'SecurePass123');
|
||||
await page.fill('#confirmPassword', 'SecurePass123');
|
||||
await page.fill('#username', 'new_user')
|
||||
await page.fill('#email', 'new_user@example.com')
|
||||
await page.fill('#password', 'SecurePass123')
|
||||
await page.fill('#confirmPassword', 'SecurePass123')
|
||||
|
||||
// Submit form
|
||||
await page.click('button[type="submit"]');
|
||||
await page.click('button[type="submit"]')
|
||||
|
||||
// Wait for redirect to dashboard
|
||||
await page.waitForURL('/dashboard');
|
||||
await page.waitForURL('/dashboard')
|
||||
|
||||
// Verify success toast appears
|
||||
await expect(page.locator('.toast-success')).toBeVisible();
|
||||
await expect(page.locator('.toast-success')).toContainText('Account created successfully');
|
||||
});
|
||||
await expect(page.locator('.toast-success')).toBeVisible()
|
||||
await expect(page.locator('.toast-success')).toContainText('Account created successfully')
|
||||
})
|
||||
|
||||
test('shows validation errors for invalid inputs', async ({ page }) => {
|
||||
await page.goto('/register');
|
||||
await page.goto('/register')
|
||||
|
||||
// Enter mismatched passwords
|
||||
await page.fill('#password', 'SecurePass123');
|
||||
await page.fill('#confirmPassword', 'DifferentPass');
|
||||
await page.fill('#password', 'SecurePass123')
|
||||
await page.fill('#confirmPassword', 'DifferentPass')
|
||||
|
||||
// Submit form
|
||||
await page.click('button[type="submit"]');
|
||||
await page.click('button[type="submit"]')
|
||||
|
||||
// Verify error message
|
||||
await expect(page.locator('text=Passwords do not match')).toBeVisible();
|
||||
});
|
||||
await expect(page.locator('text=Passwords do not match')).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
## Integration with Navigation Guards
|
||||
@@ -359,15 +360,15 @@ test('shows validation errors for invalid inputs', async ({ page }) => {
|
||||
### Router Guard Example
|
||||
|
||||
```typescript
|
||||
import { useAuthStore } from '@/stores';
|
||||
import { useAuthStore } from '@/stores'
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const authStore = useAuthStore();
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Redirect authenticated users away from auth pages
|
||||
if (authStore.isAuthenticated && (to.path === '/login' || to.path === '/register')) {
|
||||
next('/dashboard');
|
||||
return;
|
||||
next('/dashboard')
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect unauthenticated users to login
|
||||
@@ -375,12 +376,12 @@ router.beforeEach((to, from, next) => {
|
||||
next({
|
||||
path: '/login',
|
||||
query: { redirect: to.fullPath }
|
||||
});
|
||||
return;
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
next()
|
||||
})
|
||||
```
|
||||
|
||||
## Customization Examples
|
||||
@@ -393,16 +394,16 @@ async function handleLogin(): Promise<void> {
|
||||
try {
|
||||
await authStore.login({
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
});
|
||||
password: formData.password
|
||||
})
|
||||
|
||||
appStore.showSuccess('Login successful!');
|
||||
appStore.showSuccess('Login successful!')
|
||||
|
||||
// Custom redirect logic
|
||||
const isAdmin = authStore.isAdmin;
|
||||
const redirectTo = isAdmin ? '/admin/dashboard' : '/dashboard';
|
||||
const isAdmin = authStore.isAdmin
|
||||
const redirectTo = isAdmin ? '/admin/dashboard' : '/dashboard'
|
||||
|
||||
await router.push(redirectTo);
|
||||
await router.push(redirectTo)
|
||||
} catch (error) {
|
||||
// Error handling...
|
||||
}
|
||||
@@ -414,19 +415,20 @@ async function handleLogin(): Promise<void> {
|
||||
```typescript
|
||||
// Custom password strength validation
|
||||
function validatePasswordStrength(password: string): boolean {
|
||||
const hasMinLength = password.length >= 12;
|
||||
const hasUpperCase = /[A-Z]/.test(password);
|
||||
const hasLowerCase = /[a-z]/.test(password);
|
||||
const hasNumber = /[0-9]/.test(password);
|
||||
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
|
||||
const hasMinLength = password.length >= 12
|
||||
const hasUpperCase = /[A-Z]/.test(password)
|
||||
const hasLowerCase = /[a-z]/.test(password)
|
||||
const hasNumber = /[0-9]/.test(password)
|
||||
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password)
|
||||
|
||||
return hasMinLength && hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar;
|
||||
return hasMinLength && hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar
|
||||
}
|
||||
|
||||
// Use in validation
|
||||
if (!validatePasswordStrength(formData.password)) {
|
||||
errors.password = 'Password must be at least 12 characters with uppercase, lowercase, numbers, and special characters';
|
||||
isValid = false;
|
||||
errors.password =
|
||||
'Password must be at least 12 characters with uppercase, lowercase, numbers, and special characters'
|
||||
isValid = false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -439,26 +441,27 @@ async function handleRegister(): Promise<void> {
|
||||
await authStore.register({
|
||||
username: formData.username,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
});
|
||||
password: formData.password
|
||||
})
|
||||
|
||||
appStore.showSuccess('Account created successfully!');
|
||||
await router.push('/dashboard');
|
||||
appStore.showSuccess('Account created successfully!')
|
||||
await router.push('/dashboard')
|
||||
} catch (error: unknown) {
|
||||
const err = error as { response?: { status?: number; data?: { detail?: string } } };
|
||||
const err = error as { response?: { status?: number; data?: { detail?: string } } }
|
||||
|
||||
// Custom error handling based on status code
|
||||
if (err.response?.status === 409) {
|
||||
errorMessage.value = 'This username or email is already registered. Please use a different one.';
|
||||
errorMessage.value =
|
||||
'This username or email is already registered. Please use a different one.'
|
||||
} else if (err.response?.status === 422) {
|
||||
errorMessage.value = 'Invalid input. Please check your information and try again.';
|
||||
errorMessage.value = 'Invalid input. Please check your information and try again.'
|
||||
} else if (err.response?.status === 500) {
|
||||
errorMessage.value = 'Server error. Please try again later.';
|
||||
errorMessage.value = 'Server error. Please try again later.'
|
||||
} else {
|
||||
errorMessage.value = err.response?.data?.detail || 'Registration failed. Please try again.';
|
||||
errorMessage.value = err.response?.data?.detail || 'Registration failed. Please try again.'
|
||||
}
|
||||
|
||||
appStore.showError(errorMessage.value);
|
||||
appStore.showError(errorMessage.value)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -483,9 +486,7 @@ async function handleRegister(): Promise<void> {
|
||||
|
||||
```html
|
||||
<!-- Proper labels for screen readers -->
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Username
|
||||
</label>
|
||||
<label for="username" class="mb-1 block text-sm font-medium text-gray-700"> Username </label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
@@ -548,10 +549,10 @@ import LoginView from '@/views/auth/LoginView.vue'; // ❌ Eager loaded
|
||||
```typescript
|
||||
// Solution: Initialize auth state on app mount
|
||||
// In main.ts or App.vue
|
||||
import { useAuthStore } from '@/stores';
|
||||
import { useAuthStore } from '@/stores'
|
||||
|
||||
const authStore = useAuthStore();
|
||||
authStore.checkAuth(); // Restore auth from localStorage
|
||||
const authStore = useAuthStore()
|
||||
authStore.checkAuth() // Restore auth from localStorage
|
||||
```
|
||||
|
||||
### Issue: Redirect loop after login
|
||||
@@ -559,12 +560,12 @@ authStore.checkAuth(); // Restore auth from localStorage
|
||||
```typescript
|
||||
// Solution: Check router guard logic
|
||||
router.beforeEach((to, from, next) => {
|
||||
const authStore = useAuthStore();
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// ✅ Correct: Check specific routes
|
||||
if (authStore.isAuthenticated && (to.path === '/login' || to.path === '/register')) {
|
||||
next('/dashboard');
|
||||
return;
|
||||
next('/dashboard')
|
||||
return
|
||||
}
|
||||
|
||||
// ❌ Wrong: Blanket redirect
|
||||
@@ -572,8 +573,8 @@ router.beforeEach((to, from, next) => {
|
||||
// next('/dashboard'); // This causes loops!
|
||||
// }
|
||||
|
||||
next();
|
||||
});
|
||||
next()
|
||||
})
|
||||
```
|
||||
|
||||
### Issue: Form not clearing after successful submission
|
||||
|
||||
Reference in New Issue
Block a user