- 更新 CLAUDE.md 中 HTTP 客户端说明(apiFetch -> http) - 更新 docs/web/DESIGN.md 技术栈说明 - 添加任务工作流文档 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
30 KiB
30 KiB
Seclusion Web 前端设计文档
本文档定义了 Seclusion Web 前端的整体架构、技术选型、目录结构和开发规范。
一、技术栈
| 领域 | 技术选型 | 版本 | 说明 |
|---|---|---|---|
| 框架 | Next.js | 16.x | App Router, React Server Components |
| UI 框架 | React | 19.x | 最新版本 |
| UI 组件库 | shadcn/ui | latest | 基于 Radix UI + Tailwind CSS |
| 状态管理 | Zustand | ^4.x | 轻量级、类型安全 |
| 服务端状态 | TanStack Query | ^5.x | 数据获取、缓存、同步 |
| 样式方案 | Tailwind CSS | ^3.x | 工具优先 CSS 框架 |
| 表单处理 | React Hook Form | ^7.x | 高性能表单库 |
| 表单验证 | Zod | ^3.x | TypeScript 优先的 Schema 验证 |
| 图标 | Lucide Icons | latest | shadcn/ui 默认搭配 |
| HTTP 客户端 | Axios | ^1.x | 封装为 http 模块 |
二、目录结构
apps/web/src/
├── app/ # Next.js App Router
│ ├── (auth)/ # 认证相关页面组
│ │ ├── login/
│ │ │ └── page.tsx
│ │ ├── register/
│ │ │ └── page.tsx
│ │ ├── forgot-password/
│ │ │ └── page.tsx
│ │ └── layout.tsx # 认证页面布局(居中卡片样式)
│ │
│ ├── (dashboard)/ # 后台管理页面组
│ │ ├── dashboard/
│ │ │ └── page.tsx # 仪表盘首页
│ │ ├── users/
│ │ │ ├── page.tsx # 用户列表
│ │ │ └── [id]/
│ │ │ └── page.tsx # 用户详情/编辑
│ │ ├── settings/
│ │ │ └── page.tsx # 系统设置
│ │ ├── profile/
│ │ │ └── page.tsx # 个人中心
│ │ └── layout.tsx # 后台布局(侧边栏 + 顶栏)
│ │
│ ├── api/ # API Routes (如需要)
│ ├── layout.tsx # 根布局
│ ├── page.tsx # 首页/落地页
│ ├── globals.css # 全局样式
│ ├── providers.tsx # 全局 Providers 组合
│ └── not-found.tsx # 404 页面
│
├── components/
│ ├── ui/ # shadcn/ui 组件(CLI 自动生成)
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ ├── card.tsx
│ │ ├── dialog.tsx
│ │ ├── form.tsx
│ │ ├── table.tsx
│ │ ├── toast.tsx
│ │ └── ...
│ │
│ ├── layout/ # 布局组件
│ │ ├── Header.tsx # 顶部导航栏
│ │ ├── Sidebar.tsx # 侧边栏
│ │ ├── Footer.tsx # 页脚
│ │ ├── MobileNav.tsx # 移动端导航
│ │ ├── UserNav.tsx # 用户下拉菜单
│ │ └── ThemeToggle.tsx # 主题切换
│ │
│ ├── forms/ # 表单组件
│ │ ├── LoginForm.tsx # 登录表单
│ │ ├── RegisterForm.tsx # 注册表单
│ │ ├── UserForm.tsx # 用户编辑表单
│ │ └── SettingsForm.tsx # 设置表单
│ │
│ └── shared/ # 通用业务组件
│ ├── Captcha.tsx # 验证码组件
│ ├── DataTable.tsx # 通用数据表格
│ ├── DataTablePagination.tsx # 表格分页
│ ├── DataTableToolbar.tsx # 表格工具栏
│ ├── ConfirmDialog.tsx # 确认弹窗
│ ├── LoadingSpinner.tsx # 加载动画
│ ├── EmptyState.tsx # 空状态
│ ├── ErrorBoundary.tsx # 错误边界
│ └── PageHeader.tsx # 页面标题头
│
├── hooks/ # 自定义 Hooks
│ ├── useAuth.ts # 认证相关 Hook
│ ├── useUsers.ts # 用户 CRUD Hooks
│ ├── useCaptcha.ts # 验证码 Hook
│ ├── useMediaQuery.ts # 响应式 Hook
│ └── useMounted.ts # 挂载状态 Hook
│
├── stores/ # Zustand Store
│ ├── index.ts # 统一导出
│ ├── authStore.ts # 认证状态
│ ├── uiStore.ts # UI 状态
│ └── types.ts # Store 类型定义
│
├── services/ # API 服务层
│ ├── auth.service.ts # 认证 API
│ ├── user.service.ts # 用户 API
│ └── captcha.service.ts # 验证码 API
│
├── lib/ # 工具库
│ ├── http.ts # Axios HTTP 客户端封装
│ ├── crypto.ts # 加密模块 (已有)
│ ├── auth.ts # 认证相关工具函数
│ ├── query-client.ts # TanStack Query 配置
│ ├── validations.ts # Zod Schema 定义
│ └── utils.ts # 通用工具函数
│
├── types/ # 类型定义
│ └── index.ts # 类型导出 (已有)
│
└── config/ # 配置文件
├── site.ts # 站点配置
├── nav.ts # 导航菜单配置
└── constants.ts # 常量定义
三、状态管理
3.1 状态分类
| 状态类型 | 管理方式 | 示例 |
|---|---|---|
| 服务端状态 | TanStack Query | 用户列表、用户详情 |
| 客户端全局状态 | Zustand | 认证信息、主题设置 |
| 组件本地状态 | useState | 表单输入、弹窗开关 |
| URL 状态 | Next.js Router | 分页参数、筛选条件 |
3.2 Auth Store
// stores/authStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import type { AuthUser } from '@seclusion/shared';
interface AuthState {
// State
token: string | null;
refreshToken: string | null;
user: AuthUser | null;
// Computed
isAuthenticated: boolean;
// Actions
setAuth: (token: string, refreshToken: string, user: AuthUser) => void;
setUser: (user: AuthUser) => void;
logout: () => void;
clearAuth: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
token: null,
refreshToken: null,
user: null,
get isAuthenticated() {
return !!get().token && !!get().user;
},
setAuth: (token, refreshToken, user) =>
set({ token, refreshToken, user }),
setUser: (user) =>
set({ user }),
logout: () =>
set({ token: null, refreshToken: null, user: null }),
clearAuth: () =>
set({ token: null, refreshToken: null, user: null }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
token: state.token,
refreshToken: state.refreshToken,
user: state.user,
}),
}
)
);
3.3 UI Store
// stores/uiStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
type Theme = 'light' | 'dark' | 'system';
interface UIState {
// State
theme: Theme;
sidebarOpen: boolean;
sidebarCollapsed: boolean;
// Actions
setTheme: (theme: Theme) => void;
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
setSidebarCollapsed: (collapsed: boolean) => void;
}
export const useUIStore = create<UIState>()(
persist(
(set) => ({
theme: 'system',
sidebarOpen: true,
sidebarCollapsed: false,
setTheme: (theme) => set({ theme }),
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setSidebarOpen: (open) => set({ sidebarOpen: open }),
setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }),
}),
{
name: 'ui-storage',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
theme: state.theme,
sidebarCollapsed: state.sidebarCollapsed,
}),
}
)
);
四、数据请求层
4.1 架构分层
┌─────────────┐
│ Component │ React 组件
└──────┬──────┘
│ 调用
┌──────▼──────┐
│ Hook │ useUsers, useAuth (TanStack Query)
└──────┬──────┘
│ 调用
┌──────▼──────┐
│ Service │ userService, authService
└──────┬──────┘
│ 调用
┌──────▼──────┐
│ http │ Axios HTTP 客户端 + 加密处理
└─────────────┘
4.2 Service 层示例
// services/user.service.ts
import { http } from '@/lib/http';
import type {
UserResponse,
PaginatedResponse,
UpdateUserDto
} from '@seclusion/shared';
import { API_ENDPOINTS } from '@/config/constants';
export interface GetUsersParams {
page?: number;
pageSize?: number;
search?: string;
}
export const userService = {
// 获取用户列表
getUsers: (params: GetUsersParams = {}): Promise<PaginatedResponse<UserResponse>> => {
return http.get<PaginatedResponse<UserResponse>>(API_ENDPOINTS.USERS, { params });
},
// 获取单个用户
getUser: (id: string): Promise<UserResponse> => {
return http.get<UserResponse>(`${API_ENDPOINTS.USERS}/${id}`);
},
// 更新用户
updateUser: (id: string, data: UpdateUserDto): Promise<UserResponse> => {
return http.patch<UserResponse>(`${API_ENDPOINTS.USERS}/${id}`, data);
},
// 删除用户
deleteUser: (id: string): Promise<void> => {
return http.delete<void>(`${API_ENDPOINTS.USERS}/${id}`);
},
// 恢复用户
restoreUser: (id: string): Promise<UserResponse> => {
return http.patch<UserResponse>(`${API_ENDPOINTS.USERS}/${id}/restore`);
},
};
4.3 Hook 层示例
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { userService, type GetUsersParams } from '@/services/user.service';
import { useIsAuthenticated } from '@/stores';
// Query Keys
export const userKeys = {
all: ['users'] as const,
lists: () => [...userKeys.all, 'list'] as const,
list: (params: GetUsersParams) => [...userKeys.lists(), params] as const,
details: () => [...userKeys.all, 'detail'] as const,
detail: (id: string) => [...userKeys.details(), id] as const,
};
// 获取用户列表
export function useUsers(params: GetUsersParams = {}) {
const isAuthenticated = useIsAuthenticated();
return useQuery({
queryKey: userKeys.list(params),
queryFn: () => userService.getUsers(params),
enabled: isAuthenticated,
});
}
// 获取单个用户
export function useUser(id: string) {
const isAuthenticated = useIsAuthenticated();
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => userService.getUser(id),
enabled: isAuthenticated && !!id,
});
}
// 更新用户
export function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateUserDto }) =>
userService.updateUser(id, data),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
queryClient.invalidateQueries({ queryKey: userKeys.detail(id) });
},
});
}
// 删除用户
export function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => userService.deleteUser(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: userKeys.lists() });
},
});
}
五、路由设计
5.1 路由表
| 路由 | 页面 | 布局 | 权限 | 说明 |
|---|---|---|---|---|
/ |
首页 | Root | 公开 | 落地页/欢迎页 |
/login |
登录 | Auth | 公开 | 登录表单 |
/register |
注册 | Auth | 公开 | 注册表单 |
/forgot-password |
忘记密码 | Auth | 公开 | 密码重置 |
/dashboard |
仪表盘 | Dashboard | 需认证 | 数据概览 |
/users |
用户列表 | Dashboard | 需认证 | 用户管理 |
/users/[id] |
用户详情 | Dashboard | 需认证 | 查看/编辑用户 |
/settings |
系统设置 | Dashboard | 需认证 | 系统配置 |
/profile |
个人中心 | Dashboard | 需认证 | 个人信息 |
5.2 路由守卫
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// 需要认证的路<E79A84><E8B7AF><EFBFBD>
const protectedRoutes = ['/dashboard', '/users', '/settings', '/profile'];
// 认证页面(已登录用户应该跳转)
const authRoutes = ['/login', '/register', '/forgot-password'];
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
const { pathname } = request.nextUrl;
// 检查是否访问受保护路由
const isProtectedRoute = protectedRoutes.some(route =>
pathname.startsWith(route)
);
// 检查是否访问认证页面
const isAuthRoute = authRoutes.some(route =>
pathname.startsWith(route)
);
// 未登录访问受保护路由 -> 跳转登录
if (isProtectedRoute && !token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
// 已登录访问认证页面 -> 跳转仪表盘
if (isAuthRoute && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
六、布局系统
6.1 Auth 布局
用于登录、注册、忘记密码等认证页面。
┌─────────────────────────────────────────────────┐
│ │
│ ┌─────────────────┐ │
│ │ Logo │ │
│ │ │ │
│ │ ┌───────────┐ │ │
│ │ │ │ │ │
│ │ │ 表单 │ │ │
│ │ │ │ │ │
│ │ └───────────┘ │ │
│ │ │ │
│ │ 其他链接 │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────┘
6.2 Dashboard 布局
用于后台管理页面。
┌─────────────────────────────────────────────────┐
│ Logo │ Header (面包屑 / 搜索 / 通知 / 头像) │
├────────┼────────────────────────────────────────┤
│ │ │
│ S │ ┌────────────────────────────────┐ │
│ i │ │ Page Header │ │
│ d │ ├────────────────────────────────┤ │
│ e │ │ │ │
│ b │ │ │ │
│ a │ │ Page Content │ │
│ r │ │ │ │
│ │ │ │ │
│ 导航 │ │ │ │
│ 菜单 │ └────────────────────────────────┘ │
│ │ │
└────────┴────────────────────────────────────────┘
6.3 响应式设计
| 断点 | 宽度 | 侧边栏 | 说明 |
|---|---|---|---|
| sm | < 640px | 隐藏,抽屉式 | 移动端 |
| md | 640-1024px | 折叠图标模式 | 平板 |
| lg | > 1024px | 完整展开 | 桌面 |
七、表单处理
7.1 Zod Schema 定义
// lib/validations.ts
import { z } from 'zod';
// 登录表单
export const loginSchema = z.object({
email: z
.string()
.min(1, '请输入邮箱')
.email('邮箱格式不正确'),
password: z
.string()
.min(1, '请输入密码')
.min(6, '密码至少 6 位'),
captchaId: z.string().min(1, '验证码 ID 不能为空'),
captchaCode: z
.string()
.min(1, '请输入验证码')
.length(4, '验证码为 4 位'),
});
export type LoginFormValues = z.infer<typeof loginSchema>;
// 注册表单
export const registerSchema = z.object({
email: z
.string()
.min(1, '请输入邮箱')
.email('邮箱格式不正确'),
password: z
.string()
.min(1, '请输入密码')
.min(6, '密码至少 6 位')
.max(32, '密码最多 32 位'),
confirmPassword: z.string().min(1, '请确认密码'),
name: z
.string()
.min(1, '请输入用户名')
.min(2, '用户名至少 2 位')
.max(20, '用户名最多 20 位'),
captchaId: z.string().min(1, '验证码 ID 不能为空'),
captchaCode: z
.string()
.min(1, '请输入验证码')
.length(4, '验证码为 4 位'),
}).refine((data) => data.password === data.confirmPassword, {
message: '两次密码输入不一致',
path: ['confirmPassword'],
});
export type RegisterFormValues = z.infer<typeof registerSchema>;
// 用户更新表单
export const updateUserSchema = z.object({
name: z
.string()
.min(2, '用户名至少 2 位')
.max(20, '用户名最多 20 位')
.optional(),
email: z
.string()
.email('邮箱格式不正确')
.optional(),
});
export type UpdateUserFormValues = z.infer<typeof updateUserSchema>;
7.2 表单组件示例
// components/forms/LoginForm.tsx
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { loginSchema, type LoginFormValues } from '@/lib/validations';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Captcha } from '@/components/shared/Captcha';
import { useLogin } from '@/hooks/useAuth';
import { CaptchaScene } from '@seclusion/shared';
export function LoginForm() {
const { mutate: login, isPending } = useLogin();
const form = useForm<LoginFormValues>({
resolver: zodResolver(loginSchema),
defaultValues: {
email: '',
password: '',
captchaId: '',
captchaCode: '',
},
});
const onSubmit = (values: LoginFormValues) => {
login(values);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormControl>
<Input placeholder="请输入邮箱" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>密码</FormLabel>
<FormControl>
<Input type="password" placeholder="请输入密码" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Captcha
scene={CaptchaScene.LOGIN}
value={form.watch('captchaCode')}
onChange={(id, code) => {
form.setValue('captchaId', id);
form.setValue('captchaCode', code);
}}
/>
<Button type="submit" className="w-full" disabled={isPending}>
{isPending ? '登录中...' : '登录'}
</Button>
</form>
</Form>
);
}
八、组件规范
8.1 shadcn/ui 组件清单
基础组件
button- 按钮input- 输入框label- 标签textarea- 多行输入checkbox- 复选框radio-group- 单选组select- 下拉选择switch- 开关
数据展示
card- 卡片table- 表格badge- 徽章avatar- 头像skeleton- 骨架屏
反馈
toast/sonner- 消息提示dialog- 对话框alert-dialog- 确认对话框tooltip- 工具提示popover- 气泡卡片
导航
tabs- 标签页dropdown-menu- 下拉菜单navigation-menu- 导航菜单breadcrumb- 面包屑pagination- 分页
布局
separator- 分隔线scroll-area- 滚动区域sheet- 抽屉(移动端侧边栏)collapsible- 折叠面板
8.2 组件命名规范
| 类型 | 命名规则 | 示例 |
|---|---|---|
| UI 组件 | kebab-case 文件名 | button.tsx, data-table.tsx |
| 业务组件 | PascalCase 文件名 | LoginForm.tsx, UserTable.tsx |
| 布局组件 | PascalCase 文件名 | Header.tsx, Sidebar.tsx |
| Hook | camelCase | useAuth.ts, useUsers.ts |
| Service | kebab-case + .service | user.service.ts |
| Store | camelCase + Store | authStore.ts |
8.3 组件结构模板
// components/shared/ExampleComponent.tsx
'use client';
import { useState } from 'react';
import { cn } from '@/lib/utils';
// 类型定义
interface ExampleComponentProps {
title: string;
description?: string;
className?: string;
onAction?: () => void;
}
// 组件实现
export function ExampleComponent({
title,
description,
className,
onAction,
}: ExampleComponentProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className={cn('rounded-lg border p-4', className)}>
<h3 className="font-semibold">{title}</h3>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</div>
);
}
九、主题系统
9.1 颜色变量
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
9.2 主题切换实现
// components/layout/ThemeToggle.tsx
'use client';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
export function ThemeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">切换主题</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>
浅色
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
深色
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
系统
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
十、实施计划
Phase 1: 基础设施 (1-2 天)
- 安装配置 shadcn/ui
npx shadcn@latest init - 安装 Zustand
pnpm add zustand - 安装 TanStack Query
pnpm add @tanstack/react-query - 安装 React Hook Form + Zod
pnpm add react-hook-form @hookform/resolvers zod - 安装 next-themes (主题支持)
pnpm add next-themes - 创建目录结构
- 配置全局 Providers
Phase 2: 布局与基础组件 (2-3 天)
- 安装必要的 shadcn/ui 组件
- 实现 Auth 布局
- 实现 Dashboard 布局
- Header 组件
- Sidebar 组件
- UserNav 组件
- 实现 ThemeToggle 组件
- 配置响应式断点
Phase 3: 状态管理 (1 天)
- 实现 authStore
- 实现 uiStore
- 配置 TanStack Query Client
- 实现 API Services 层
Phase 4: 认证模块 (2 天)
- 实现 Zod 验证 Schema
- 实现登录页面
- 实现注册页面
- 实现路由中间件
- 重构 Captcha 组件
Phase 5: 业务模块 (3-5 天)
- 实现仪表盘页面
- 实现用户列表页面
- DataTable 组件
- 搜索筛选
- 分页
- 实现用户详情/编辑页面
- 实现设置页面
- 实现个人中心页面
Phase 6: 优化与完善 (1-2 天)
- 错误处理与边界
- 加载状态与骨架屏
- 性能优化
- 响应式适配测试
十一、开发规范
11.1 文件命名
| 类型 | 命名方式 | 示例 |
|---|---|---|
| 页面文件 | page.tsx |
app/users/page.tsx |
| 布局文件 | layout.tsx |
app/(dashboard)/layout.tsx |
| 组件 | PascalCase | UserTable.tsx |
| Hook | camelCase + use | useAuth.ts |
| Service | kebab-case.service | user.service.ts |
| Store | camelCase + Store | authStore.ts |
| 工具函数 | camelCase | formatDate.ts |
| 类型 | PascalCase | UserResponse |
| 常量 | UPPER_SNAKE_CASE | API_BASE_URL |
11.2 导入顺序
// 1. React/Next 相关
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
// 2. 第三方库
import { useQuery } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
// 3. 共享包
import type { UserResponse } from '@seclusion/shared';
// 4. 内部 UI 组件
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
// 5. 内部业务组件
import { UserTable } from '@/components/shared/UserTable';
// 6. Hooks
import { useUsers } from '@/hooks/useUsers';
// 7. Stores
import { useAuthStore } from '@/stores/authStore';
// 8. Services
import { userService } from '@/services/user.service';
// 9. 工具函数 & 配置
import { cn } from '@/lib/utils';
// 10. 类型
import type { PageProps } from '@/types';
// 11. 样式 (如果有)
import './styles.css';
11.3 组件编写原则
- 单一职责: 每个组件只做一件事
- 组合优于继承: 使用组合模式构建复杂组件
- Props 向下,Events 向上: 数据流向清晰
- 优先使用服务端组件: 只在需要交互时使用 'use client'
- 类型安全: 所有 Props 都要定义 TypeScript 类型
11.4 状态管理原则
- URL 优先: 可共享的状态放 URL (分页、筛选)
- 服务端状态用 Query: 从 API 获取的数据用 TanStack Query
- 客户端全局状态用 Zustand: 认证、主题等跨组件状态
- 本地状态用 useState: 组件内部状态
附录
A. 常用命令
# 添加 shadcn/ui 组件
npx shadcn@latest add button
# 添加多个组件
npx shadcn@latest add button input card table
# 开发
pnpm dev
# 构建
pnpm build
# 类型检查
pnpm typecheck
# 代码检查
pnpm lint