Skip to Content

前端开发

本项目使用 Next.js 15React 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>; }

路由和导航

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 会显示详细的错误信息和堆栈跟踪。


下一步:查看表单处理状态管理深入了解。