前端开发
本项目使用 Next.js 15 和 React 19 构建管理后台,采用 App Router 和 Server Components,使用 shadcn/ui 作为 UI 组件库。
技术栈
- 框架:Next.js 15 (App Router)
- UI 库:React 19
- 样式:TailwindCSS
- 组件库:shadcn/ui
- 表单:React Hook Form + Zod
- 状态管理:React Context + Server Components
- 图标:Lucide Icons
项目结构
apps/admin/
├── app/
│ ├── (app)/ # 主应用页面
│ │ ├── dashboard/ # 仪表板
│ │ ├── intents/ # 采购意图管理
│ │ └── executions/ # 执行任务管理
│ ├── (auth)/ # 认证页面
│ │ ├── login/
│ │ └── register/
│ └── layout.tsx # 根布局
├── components/
│ ├── app-sidebar.tsx # 侧边栏
│ ├── nav-main.tsx # 主导航
│ ├── page-container.tsx # 页面容器
│ └── providers.tsx # Context Providers
├── features/ # 业务功能模块
│ └── intent/
│ ├── components/
│ ├── hooks/
│ └── types/
├── lib/
│ ├── auth.ts # 认证工具
│ ├── format.ts # 格式化工具
│ └── user.ts # 用户工具
└── config/
└── routes.ts # 路由配置UI 组件开发
shadcn/ui 组件库
本项目使用 shadcn/ui 作为 UI 组件库,所有组件存放在 packages/ui/ 中供多个应用共享。
添加新组件
# 从 shadcn/ui 添加组件
pnpm dlx shadcn@latest add button -c apps/admin
pnpm dlx shadcn@latest add card -c apps/admin
pnpm dlx shadcn@latest add dialog -c apps/admin
# 组件会被安装到 packages/ui/src/components/使用组件
// 在任何应用中导入使用
import { Button } from "@repo/ui/components/button";
import { Card, CardContent, CardHeader, CardTitle } from "@repo/ui/components/card";
import { Alert, AlertDescription } from "@repo/ui/components/alert";
export default function Page() {
return (
<Card>
<CardHeader>
<CardTitle>标题</CardTitle>
</CardHeader>
<CardContent>
<Alert>
<AlertDescription>这是一个提示信息</AlertDescription>
</Alert>
<Button>点击按钮</Button>
</CardContent>
</Card>
);
}常用组件
- Button - 按钮(多种变体:default, destructive, outline, ghost)
- Card - 卡片容器
- Dialog - 对话框/模态框
- Form - 表单组件
- Input - 输入框
- Select - 下拉选择
- Table - 数据表格
- Badge - 标签徽章
- Alert - 提示信息
- Tabs - 标签页
查看所有组件:https://ui.shadcn.com/docs/components
自定义组件
在 packages/ui/src/components/ 中创建自定义组件:
// packages/ui/src/components/status-badge.tsx
import { Badge } from "./badge";
import { cn } from "@repo/ui/lib/utils";
export type Status = "pending" | "approved" | "rejected" | "completed";
export interface StatusBadgeProps {
status: Status;
className?: string;
}
const statusConfig = {
pending: { label: "待处理", variant: "secondary" as const },
approved: { label: "已批准", variant: "default" as const },
rejected: { label: "已拒绝", variant: "destructive" as const },
completed: { label: "已完成", variant: "success" as const },
};
export function StatusBadge({ status, className }: StatusBadgeProps) {
const config = statusConfig[status];
return (
<Badge variant={config.variant} className={cn(className)}>
{config.label}
</Badge>
);
}使用自定义组件:
import { StatusBadge } from "@repo/ui/components/status-badge";
<StatusBadge status="approved" />样式工具
使用 cn 函数合并 className:
import { cn } from "@repo/ui/lib/utils";
<div className={cn(
"base-class",
isActive && "active-class",
className
)} />Next.js App Router
页面创建
// app/(app)/dashboard/page.tsx
export default function DashboardPage() {
return (
<div>
<h1>仪表板</h1>
{/* 页面内容 */}
</div>
);
}布局组件
// app/(app)/layout.tsx
import { AppSidebar } from "@/components/app-sidebar";
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen">
<AppSidebar />
<main className="flex-1">{children}</main>
</div>
);
}Server Components
默认情况下,所有组件都是 Server Components:
// app/(app)/intents/page.tsx
import { prisma } from "@repo/db";
export default async function IntentsPage() {
// 直接在组件中查询数据库
const intents = await prisma.intention.findMany();
return (
<div>
<h1>采购意图列表</h1>
<ul>
{intents.map((intent) => (
<li key={intent.id}>{intent.title}</li>
))}
</ul>
</div>
);
}Client Components
需要使用交互、状态或浏览器 API 时使用 Client Components:
"use client";
import { useState } from "react";
import { Button } from "@repo/ui/components/button";
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<Button onClick={() => setCount(count + 1)}>
增加
</Button>
</div>
);
}Server Actions
使用 Server Actions 处理表单提交:
// app/actions/intent.ts
"use server";
import { prisma } from "@repo/db";
import { revalidatePath } from "next/cache";
export async function createIntent(formData: FormData) {
const title = formData.get("title") as string;
const url = formData.get("url") as string;
await prisma.intention.create({
data: { title, url },
});
revalidatePath("/intents");
}// app/(app)/intents/create-form.tsx
import { createIntent } from "@/app/actions/intent";
import { Button } from "@repo/ui/components/button";
export function CreateIntentForm() {
return (
<form action={createIntent}>
<input name="title" placeholder="标题" required />
<input name="url" placeholder="URL" required />
<Button type="submit">创建</Button>
</form>
);
}表单处理
React Hook Form + Zod
使用 React Hook Form 和 Zod 进行表单验证:
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@repo/ui/components/button";
import { Input } from "@repo/ui/components/input";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@repo/ui/components/form";
const formSchema = z.object({
title: z.string().min(1, "标题不能为空"),
url: z.string().url("请输入有效的 URL"),
price: z.number().positive("价格必须大于 0"),
});
type FormValues = z.infer<typeof formSchema>;
export function IntentForm() {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
url: "",
price: 0,
},
});
const onSubmit = async (data: FormValues) => {
console.log(data);
// 提交到 API
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>标题</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>商品链接</FormLabel>
<FormControl>
<Input {...field} type="url" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="price"
render={({ field }) => (
<FormItem>
<FormLabel>价格</FormLabel>
<FormControl>
<Input {...field} type="number" onChange={(e) => field.onChange(+e.target.value)} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">提交</Button>
</form>
</Form>
);
}数据获取
服务端数据获取
// app/(app)/intents/page.tsx
import { prisma } from "@repo/db";
export default async function IntentsPage() {
const intents = await prisma.intention.findMany({
orderBy: { createdAt: "desc" },
take: 10,
});
return (
<div>
{intents.map((intent) => (
<div key={intent.id}>{intent.title}</div>
))}
</div>
);
}客户端数据获取
"use client";
import { useEffect, useState } from "react";
export function IntentsList() {
const [intents, setIntents] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/intents")
.then((res) => res.json())
.then((data) => {
setIntents(data);
setLoading(false);
});
}, []);
if (loading) return <div>加载中...</div>;
return (
<div>
{intents.map((intent) => (
<div key={intent.id}>{intent.title}</div>
))}
</div>
);
}状态管理
Context API
// app/providers.tsx
"use client";
import { createContext, useContext, useState } from "react";
type User = { id: string; name: string } | null;
const UserContext = createContext<{
user: User;
setUser: (user: User) => void;
}>({
user: null,
setUser: () => {},
});
export function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User>(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
const context = useContext(UserContext);
if (!context) {
throw new Error("useUser must be used within UserProvider");
}
return context;
}使用:
// app/layout.tsx
import { UserProvider } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<UserProvider>
{children}
</UserProvider>
</body>
</html>
);
}// 任何组件中
"use client";
import { useUser } from "@/app/providers";
export function UserGreeting() {
const { user } = useUser();
return <div>Hello, {user?.name}</div>;
}路由和导航
Link 组件
import Link from "next/link";
<Link href="/intents">采购意图</Link>
<Link href="/executions">执行任务</Link>编程式导航
"use client";
import { useRouter } from "next/navigation";
export function NavigateButton() {
const router = useRouter();
return (
<button onClick={() => router.push("/dashboard")}>
前往仪表板
</button>
);
}动态路由
app/(app)/intents/[id]/page.tsx → /intents/123// app/(app)/intents/[id]/page.tsx
import { prisma } from "@repo/db";
export default async function IntentDetailPage({ params }: { params: { id: string } }) {
const intent = await prisma.intention.findUnique({
where: { id: params.id },
});
if (!intent) {
return <div>找不到该采购意图</div>;
}
return (
<div>
<h1>{intent.title}</h1>
<p>{intent.url}</p>
</div>
);
}图片优化
使用 Next.js Image 组件:
import Image from "next/image";
<Image
src="/product.jpg"
alt="商品图片"
width={400}
height={300}
priority // 首屏图片使用
/>最佳实践
1. 优先使用 Server Components
- 默认使用 Server Components
- 仅在需要交互时使用 Client Components
- 将 Client Components 推到组件树的叶子节点
2. 合理使用缓存
// 使用 revalidate
export const revalidate = 60; // 60秒重新验证
// 或使用 revalidatePath
import { revalidatePath } from "next/cache";
revalidatePath("/intents");3. 错误处理
// app/(app)/error.tsx
"use client";
export default function Error({ error, reset }: { error: Error, reset: () => void }) {
return (
<div>
<h2>出错了!</h2>
<p>{error.message}</p>
<button onClick={reset}>重试</button>
</div>
);
}4. 加载状态
// app/(app)/loading.tsx
export default function Loading() {
return <div>加载中...</div>;
}5. 响应式设计
使用 TailwindCSS 断点:
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* 响应式网格 */}
</div>
<div className="text-sm md:text-base lg:text-lg">
{/* 响应式文字大小 */}
</div>调试技巧
React DevTools
安装 React Developer Tools 浏览器扩展,查看组件树和 props。
控制台日志
console.log("数据:", data);
console.error("错误:", error);
console.table(array); // 表格形式显示数组Next.js 开发工具
开发模式下,Next.js 会显示详细的错误信息和堆栈跟踪。