1474 lines
38 KiB
Markdown
1474 lines
38 KiB
Markdown
# Next.js App Router 开发规范
|
||
|
||
## 1. 目录结构规范
|
||
|
||
### 1.1 Next.js App Router 标准结构
|
||
|
||
基于 Next.js 14 App Router 的文件系统路由,每个页面目录采用以下标准结构:
|
||
|
||
```
|
||
src/app/ModuleName/
|
||
├── 📄 page.tsx # 页面主组件(服务端组件)
|
||
├── 📄 layout.tsx # 模块专属布局
|
||
├── 📄 loading.tsx # 加载状态组件
|
||
├── 📄 error.tsx # 错误边界组件
|
||
├── 📄 not-found.tsx # 404页面组件
|
||
├── 📂 SubModuleName/ # 子模块目录
|
||
│ ├── 📄 page.tsx # 子模块页面
|
||
│ └── 📂 [id]/ # 动态路由
|
||
│ └── 📄 page.tsx # 动态参数页面
|
||
├── 📂 components/ # 页面专用组件
|
||
│ ├── 📂 Component1/ # 子组件1
|
||
│ │ ├── 📄 index.tsx # 子组件实现
|
||
│ │ ├── 📄 index.module.css # 组件样式(CSS Modules)
|
||
│ │ └── 📄 types.ts # 类型定义
|
||
│ └── 📂 common/ # 通用组件
|
||
│ ├── 📄 PageHeader.tsx
|
||
│ └── 📄 LoadingSpinner.tsx
|
||
├── 📂 lib/ # 页面专用工具和逻辑
|
||
│ ├── 📄 actions.ts # 服务端操作
|
||
│ ├── 📄 hooks/ # 客户端Hooks
|
||
│ │ ├── 📄 usePageData.tsx
|
||
│ │ └── 📄 usePageActions.tsx
|
||
│ ├── 📄 utils.tsx # 工具函数
|
||
│ └── 📄 constants.tsx # 常量定义
|
||
└── 📄 types.ts # 页面类型定义
|
||
```
|
||
|
||
### 1.2 动态路由结构
|
||
|
||
#### 1.2.1 基础动态路由
|
||
```
|
||
src/app/machinery/archive/[id]/
|
||
├── 📄 page.tsx # 详情页面 /machinery/archive/[id]
|
||
├── 📄 loading.tsx # 加载状态
|
||
├── 📄 error.tsx # 错误处理
|
||
└── 📂 components/
|
||
├── 📄 MachineryDetail.tsx
|
||
└── 📄 RelatedInfo.tsx
|
||
```
|
||
|
||
#### 1.2.2 嵌套动态路由
|
||
```
|
||
src/app/config/tenant/[enterpriseId]/
|
||
├── 📄 page.tsx # 企业详情页
|
||
├── 📂 edit/ # 编辑功能
|
||
│ └── 📄 page.tsx # /config/tenant/[enterpriseId]/edit
|
||
├── 📂 users/ # 用户管理
|
||
│ ├── 📄 page.tsx # 用户列表
|
||
│ └── 📂 [userId]/ # 用户详情
|
||
│ └── 📄 page.tsx # /config/tenant/[enterpriseId]/users/[userId]
|
||
```
|
||
|
||
#### 1.2.3 路由组结构
|
||
```
|
||
src/app/
|
||
├── 📂 (auth)/ # 认证路由组(不显示在URL中)
|
||
│ ├── 📄 layout.tsx # 认证布局
|
||
│ ├── 📂 login/
|
||
│ │ └── 📄 page.tsx # /login
|
||
│ └── 📂 register/
|
||
│ └── 📄 page.tsx # /register
|
||
├── 📂 (dashboard)/ # 仪表板路由组
|
||
│ ├── 📄 layout.tsx # 仪表板布局
|
||
│ ├── 📂 machinery/ # 农机管理
|
||
│ ├── 📂 field/ # 地块管理
|
||
│ └── 📂 config/ # 配置管理
|
||
└── 📂 api/ # API路由
|
||
└── 📂 machinery/
|
||
└── 📄 route.ts # /api/machinery
|
||
```
|
||
|
||
### 1.2 组件拆分原则
|
||
|
||
**拆分条件:**
|
||
1. 组件代码超过100行
|
||
2. 组件承担多个职责
|
||
3. 组件需要复用
|
||
4. 组件逻辑复杂(包含多个useState或useEffect)
|
||
|
||
**拆分命名:**
|
||
- 主组件:保持原名称(如:MachineryEntry)
|
||
- 子组件:基于功能命名(如:MachineryList、MachineryForm、MachineryFilter)
|
||
- 通用组件:基于用途命名(如:TableHeader、FormActions)
|
||
|
||
## 2. Next.js 开发规范
|
||
|
||
### 2.1 页面组件开发规范
|
||
|
||
#### 2.1.1 服务端组件规范(推荐)
|
||
|
||
```typescript
|
||
// src/app/machinery/archive/entry/page.tsx
|
||
import { Suspense } from 'react';
|
||
import { getMachineryList } from '@/lib/services/api/machineryApi';
|
||
import { MachineryListClient } from './components/MachineryListClient';
|
||
import { MachineryPageHeader } from '@/components/layout/MachineryPageHeader';
|
||
import { MachineryFilters } from './components/MachineryFilters';
|
||
import { LoadingSkeleton } from '@/components/ui/loading-skeleton';
|
||
|
||
export default async function MachineryArchivePage({
|
||
searchParams,
|
||
}: {
|
||
searchParams?: { [key: string]: string | string[] | undefined };
|
||
}) {
|
||
// 服务端数据获取
|
||
const initialData = await getMachineryList(searchParams);
|
||
|
||
return (
|
||
<div className="container mx-auto p-6">
|
||
<MachineryPageHeader title="农机档案管理" />
|
||
|
||
<MachineryFilters initialFilters={searchParams} />
|
||
|
||
<Suspense fallback={<LoadingSkeleton />}>
|
||
<MachineryListClient initialData={initialData} />
|
||
</Suspense>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 元数据配置
|
||
export async function generateMetadata() {
|
||
return {
|
||
title: '农机档案管理 - 智慧农业系统',
|
||
description: '管理农机档案信息,包括录入、编辑、删除等功能',
|
||
};
|
||
}
|
||
|
||
// 静态生成参数(可选)
|
||
export async function generateStaticParams() {
|
||
return [
|
||
{ category: 'tractor' },
|
||
{ category: 'harvester' },
|
||
{ category: 'seeder' },
|
||
];
|
||
}
|
||
```
|
||
|
||
#### 2.1.2 客户端组件规范
|
||
|
||
```typescript
|
||
// src/app/machinery/archive/entry/components/MachineryListClient.tsx
|
||
'use client';
|
||
|
||
import { useState, useCallback } from 'react';
|
||
import { useMachineryStore } from '@/lib/stores/machineryStore';
|
||
import { MachineryGrid } from '@/components/business/machinery/MachineryGrid';
|
||
import { MachineryTable } from '@/components/business/machinery/MachineryTable';
|
||
import { CreateMachineryModal } from '@/components/business/machinery/CreateMachineryModal';
|
||
|
||
interface Props {
|
||
initialData: MachineryResponse;
|
||
}
|
||
|
||
export function MachineryListClient({ initialData }: Props) {
|
||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||
const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid');
|
||
|
||
const {
|
||
machinery,
|
||
loading,
|
||
filters,
|
||
updateMachinery,
|
||
deleteMachinery,
|
||
refetch,
|
||
} = useMachineryStore();
|
||
|
||
const handleCreate = useCallback(async (data: CreateMachineryDto) => {
|
||
await updateMachinery(data);
|
||
setIsCreateModalOpen(false);
|
||
refetch();
|
||
}, [updateMachinery, refetch]);
|
||
|
||
const handleEdit = useCallback(async (id: string, data: UpdateMachineryDto) => {
|
||
await updateMachinery({ ...data, id });
|
||
refetch();
|
||
}, [updateMachinery, refetch]);
|
||
|
||
const handleDelete = useCallback(async (id: string) => {
|
||
await deleteMachinery(id);
|
||
refetch();
|
||
}, [deleteMachinery, refetch]);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="flex justify-between items-center">
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={() => setViewMode('grid')}
|
||
className={`px-4 py-2 rounded ${
|
||
viewMode === 'grid' ? 'bg-green-600 text-white' : 'bg-gray-200'
|
||
}`}
|
||
>
|
||
网格视图
|
||
</button>
|
||
<button
|
||
onClick={() => setViewMode('table')}
|
||
className={`px-4 py-2 rounded ${
|
||
viewMode === 'table' ? 'bg-green-600 text-white' : 'bg-gray-200'
|
||
}`}
|
||
>
|
||
表格视图
|
||
</button>
|
||
</div>
|
||
|
||
<button
|
||
onClick={() => setIsCreateModalOpen(true)}
|
||
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
|
||
>
|
||
新增农机
|
||
</button>
|
||
</div>
|
||
|
||
{loading ? (
|
||
<div className="flex justify-center py-8">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600"></div>
|
||
</div>
|
||
) : viewMode === 'grid' ? (
|
||
<MachineryGrid
|
||
machinery={machinery}
|
||
onEdit={handleEdit}
|
||
onDelete={handleDelete}
|
||
/>
|
||
) : (
|
||
<MachineryTable
|
||
machinery={machinery}
|
||
onEdit={handleEdit}
|
||
onDelete={handleDelete}
|
||
/>
|
||
)}
|
||
|
||
<CreateMachineryModal
|
||
open={isCreateModalOpen}
|
||
onClose={() => setIsCreateModalOpen(false)}
|
||
onSave={handleCreate}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
### 2.2 动态路由开发规范
|
||
|
||
#### 2.2.1 动态参数处理
|
||
|
||
```typescript
|
||
// src/app/machinery/archive/[id]/page.tsx
|
||
import { notFound } from 'next/navigation';
|
||
import { Metadata } from 'next';
|
||
import { getMachineryById } from '@/lib/services/api/machineryApi';
|
||
import { MachineryDetailPage } from '@/components/pages/machinery/MachineryDetailPage';
|
||
|
||
interface Props {
|
||
params: { id: string };
|
||
searchParams: { [key: string]: string | string[] | undefined };
|
||
}
|
||
|
||
// 生成页面元数据
|
||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||
try {
|
||
const machinery = await getMachineryById(params.id);
|
||
return {
|
||
title: `${machinery.name} - 农机详情`,
|
||
description: `查看农机 ${machinery.name} 的详细信息`,
|
||
};
|
||
} catch {
|
||
return {
|
||
title: '农机详情 - 未找到',
|
||
};
|
||
}
|
||
}
|
||
|
||
// 服务端组件 - 获取数据并渲染
|
||
export default async function MachineryDetail({ params }: Props) {
|
||
const machinery = await getMachineryById(params.id);
|
||
|
||
if (!machinery) {
|
||
notFound();
|
||
}
|
||
|
||
return (
|
||
<div className="container mx-auto p-6">
|
||
<MachineryDetailPage machinery={machinery} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 静态生成常用农机详情页(提升性能)
|
||
export async function generateStaticParams() {
|
||
// 这里可以预生成一些常用的农机详情页
|
||
// 实际项目中可以从数据库获取
|
||
return [
|
||
{ id: 'machinery-001' },
|
||
{ id: 'machinery-002' },
|
||
{ id: 'machinery-003' },
|
||
];
|
||
}
|
||
```
|
||
|
||
#### 2.2.2 布局组件规范
|
||
|
||
```typescript
|
||
// src/app/machinery/layout.tsx
|
||
import { ReactNode } from 'react';
|
||
import { MachinerySidebar } from '@/components/layout/MachinerySidebar';
|
||
import { MachineryBreadcrumb } from '@/components/navigation/MachineryBreadcrumb';
|
||
|
||
export default function MachineryLayout({
|
||
children,
|
||
}: {
|
||
children: ReactNode;
|
||
}) {
|
||
return (
|
||
<div className="flex h-full">
|
||
{/* 农机模块侧边栏 */}
|
||
<div className="w-64 bg-gray-50 border-r">
|
||
<MachinerySidebar />
|
||
</div>
|
||
|
||
{/* 主内容区域 */}
|
||
<div className="flex-1 flex flex-col">
|
||
{/* 面包屑导航 */}
|
||
<div className="bg-white border-b px-6 py-4">
|
||
<MachineryBreadcrumb />
|
||
</div>
|
||
|
||
{/* 页面内容 */}
|
||
<main className="flex-1 overflow-auto">
|
||
{children}
|
||
</main>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
#### 2.2.3 加载和错误状态
|
||
|
||
```typescript
|
||
// src/app/machinery/loading.tsx
|
||
import { LoadingSkeleton } from '@/components/ui/loading-skeleton';
|
||
|
||
export default function MachineryLoading() {
|
||
return (
|
||
<div className="container mx-auto p-6">
|
||
<div className="animate-pulse">
|
||
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
{[...Array(6)].map((_, i) => (
|
||
<div key={i} className="bg-white rounded-lg shadow-sm p-6">
|
||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div>
|
||
<div className="space-y-2">
|
||
<div className="h-3 bg-gray-200 rounded"></div>
|
||
<div className="h-3 bg-gray-200 rounded w-5/6"></div>
|
||
<div className="h-3 bg-gray-200 rounded w-4/6"></div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// src/app/machinery/error.tsx
|
||
'use client';
|
||
|
||
import { useEffect } from 'react';
|
||
import { Button } from '@/components/ui/button';
|
||
import { AlertTriangle } from 'lucide-react';
|
||
|
||
export default function Error({
|
||
error,
|
||
reset,
|
||
}: {
|
||
error: Error;
|
||
reset: () => void;
|
||
}) {
|
||
useEffect(() => {
|
||
console.error('农机模块错误:', error);
|
||
}, [error]);
|
||
|
||
return (
|
||
<div className="container mx-auto p-6">
|
||
<div className="flex flex-col items-center justify-center min-h-96">
|
||
<AlertTriangle className="h-12 w-12 text-red-500 mb-4" />
|
||
<h2 className="text-2xl font-semibold mb-2">页面加载失败</h2>
|
||
<p className="text-gray-600 mb-6 text-center max-w-md">
|
||
{error.message || '加载农机模块时发生了错误,请稍后重试。'}
|
||
</p>
|
||
<Button onClick={reset} variant="outline">
|
||
重新加载
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 2.3 服务端操作规范
|
||
|
||
#### 2.3.1 Server Actions
|
||
|
||
```typescript
|
||
// src/lib/actions/machinery.ts
|
||
'use server';
|
||
|
||
import { revalidatePath } from 'next/cache';
|
||
import { redirect } from 'next/navigation';
|
||
import { z } from 'zod';
|
||
import { createMachinery, updateMachinery, deleteMachinery } from '@/lib/services/api/machineryApi';
|
||
|
||
// 表单验证Schema
|
||
const createMachinerySchema = z.object({
|
||
name: z.string().min(1, '农机名称不能为空'),
|
||
model: z.string().min(1, '型号不能为空'),
|
||
category: z.enum(['拖拉机', '收割机', '播种机', '植保机械']),
|
||
manufacturer: z.string().min(1, '制造商不能为空'),
|
||
});
|
||
|
||
export async function createMachineryAction(formData: FormData) {
|
||
try {
|
||
// 验证表单数据
|
||
const validatedData = createMachinerySchema.parse({
|
||
name: formData.get('name'),
|
||
model: formData.get('model'),
|
||
category: formData.get('category'),
|
||
manufacturer: formData.get('manufacturer'),
|
||
});
|
||
|
||
// 创建农机
|
||
await createMachinery(validatedData);
|
||
|
||
// 重新验证缓存
|
||
revalidatePath('/machinery/archive');
|
||
revalidatePath('/machinery');
|
||
|
||
return { success: true, message: '农机创建成功' };
|
||
} catch (error) {
|
||
if (error instanceof z.ZodError) {
|
||
return { success: false, message: error.errors[0].message };
|
||
}
|
||
return { success: false, message: '创建失败,请重试' };
|
||
}
|
||
}
|
||
|
||
export async function updateMachineryAction(id: string, formData: FormData) {
|
||
try {
|
||
const validatedData = createMachinerySchema.parse({
|
||
name: formData.get('name'),
|
||
model: formData.get('model'),
|
||
category: formData.get('category'),
|
||
manufacturer: formData.get('manufacturer'),
|
||
});
|
||
|
||
await updateMachinery(id, validatedData);
|
||
revalidatePath('/machinery/archive');
|
||
revalidatePath(`/machinery/archive/${id}`);
|
||
|
||
return { success: true, message: '农机更新成功' };
|
||
} catch (error) {
|
||
if (error instanceof z.ZodError) {
|
||
return { success: false, message: error.errors[0].message };
|
||
}
|
||
return { success: false, message: '更新失败,请重试' };
|
||
}
|
||
}
|
||
|
||
export async function deleteMachineryAction(id: string) {
|
||
try {
|
||
await deleteMachinery(id);
|
||
revalidatePath('/machinery/archive');
|
||
redirect('/machinery/archive');
|
||
} catch (error) {
|
||
throw new Error('删除农机失败');
|
||
}
|
||
}
|
||
### 2.4 Next.js API 路由规范
|
||
|
||
#### 2.4.1 API 路由结构
|
||
|
||
```typescript
|
||
// src/app/api/machinery/route.ts - 农机API路由
|
||
import { NextRequest, NextResponse } from 'next/server';
|
||
import { getMachineryList, createMachinery } from '@/lib/services/machineryService';
|
||
import { machinerySchema } from '@/lib/validations/machinery';
|
||
|
||
// GET - 获取农机列表
|
||
export async function GET(request: NextRequest) {
|
||
try {
|
||
const { searchParams } = new URL(request.url);
|
||
|
||
// 解析查询参数
|
||
const filters = {
|
||
page: parseInt(searchParams.get('page') || '1'),
|
||
limit: parseInt(searchParams.get('limit') || '10'),
|
||
category: searchParams.get('category'),
|
||
status: searchParams.get('status'),
|
||
};
|
||
|
||
const data = await getMachineryList(filters);
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
data,
|
||
message: '获取农机列表成功'
|
||
});
|
||
} catch (error) {
|
||
console.error('获取农机列表失败:', error);
|
||
return NextResponse.json(
|
||
{
|
||
success: false,
|
||
message: '获取农机列表失败'
|
||
},
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|
||
|
||
// POST - 创建农机
|
||
export async function POST(request: NextRequest) {
|
||
try {
|
||
const body = await request.json();
|
||
|
||
// 验证数据
|
||
const validatedData = machinerySchema.parse(body);
|
||
|
||
const machinery = await createMachinery(validatedData);
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
data: machinery,
|
||
message: '创建农机成功'
|
||
}, { status: 201 });
|
||
} catch (error) {
|
||
if (error.name === 'ZodError') {
|
||
return NextResponse.json({
|
||
success: false,
|
||
message: '数据验证失败',
|
||
errors: error.errors
|
||
}, { status: 400 });
|
||
}
|
||
|
||
console.error('创建农机失败:', error);
|
||
return NextResponse.json({
|
||
success: false,
|
||
message: '创建农机失败'
|
||
}, { status: 500 });
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2.4.2 动态API路由
|
||
|
||
```typescript
|
||
// src/app/api/machinery/[id]/route.ts - 农机详情API
|
||
import { NextRequest, NextResponse } from 'next/server';
|
||
import { getMachineryById, updateMachinery, deleteMachinery } from '@/lib/services/machineryService';
|
||
|
||
interface Context {
|
||
params: { id: string };
|
||
}
|
||
|
||
// GET - 获取农机详情
|
||
export async function GET(request: NextRequest, context: Context) {
|
||
try {
|
||
const { id } = context.params;
|
||
const machinery = await getMachineryById(id);
|
||
|
||
if (!machinery) {
|
||
return NextResponse.json({
|
||
success: false,
|
||
message: '农机不存在'
|
||
}, { status: 404 });
|
||
}
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
data: machinery
|
||
});
|
||
} catch (error) {
|
||
console.error('获取农机详情失败:', error);
|
||
return NextResponse.json({
|
||
success: false,
|
||
message: '获取农机详情失败'
|
||
}, { status: 500 });
|
||
}
|
||
}
|
||
|
||
// PUT - 更新农机
|
||
export async function PUT(request: NextRequest, context: Context) {
|
||
try {
|
||
const { id } = context.params;
|
||
const body = await request.json();
|
||
|
||
const validatedData = machinerySchema.partial().parse(body);
|
||
const machinery = await updateMachinery(id, validatedData);
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
data: machinery,
|
||
message: '更新农机成功'
|
||
});
|
||
} catch (error) {
|
||
if (error.name === 'ZodError') {
|
||
return NextResponse.json({
|
||
success: false,
|
||
message: '数据验证失败',
|
||
errors: error.errors
|
||
}, { status: 400 });
|
||
}
|
||
|
||
console.error('更新农机失败:', error);
|
||
return NextResponse.json({
|
||
success: false,
|
||
message: '更新农机失败'
|
||
}, { status: 500 });
|
||
}
|
||
}
|
||
|
||
// DELETE - 删除农机
|
||
export async function DELETE(request: NextRequest, context: Context) {
|
||
try {
|
||
const { id } = context.params;
|
||
await deleteMachinery(id);
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
message: '删除农机成功'
|
||
});
|
||
} catch (error) {
|
||
console.error('删除农机失败:', error);
|
||
return NextResponse.json({
|
||
success: false,
|
||
message: '删除农机失败'
|
||
}, { status: 500 });
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.5 路由中间件规范
|
||
|
||
#### 2.5.1 认证中间件
|
||
|
||
```typescript
|
||
// middleware.ts
|
||
import { NextResponse } from 'next/server';
|
||
import type { NextRequest } from 'next/server';
|
||
import { getToken } from 'next-auth/jwt';
|
||
|
||
// 需要认证的路由
|
||
const protectedRoutes = [
|
||
'/machinery',
|
||
'/field',
|
||
'/operation',
|
||
'/asset',
|
||
'/ai-model',
|
||
'/irrigation',
|
||
'/config'
|
||
];
|
||
|
||
// 公开路由
|
||
const publicRoutes = [
|
||
'/',
|
||
'/login',
|
||
'/register',
|
||
'/api/auth'
|
||
];
|
||
|
||
export async function middleware(request: NextRequest) {
|
||
const { pathname } = request.nextUrl;
|
||
const token = await getToken({
|
||
req: request,
|
||
secret: process.env.NEXTAUTH_SECRET
|
||
});
|
||
|
||
// 检查是否访问需要认证的路由
|
||
const isProtectedRoute = protectedRoutes.some(route =>
|
||
pathname.startsWith(route)
|
||
);
|
||
|
||
// 检查是否访问公开路由
|
||
const isPublicRoute = publicRoutes.some(route =>
|
||
pathname.startsWith(route)
|
||
);
|
||
|
||
// API路由处理
|
||
if (pathname.startsWith('/api/')) {
|
||
// 认证相关API允许访问
|
||
if (pathname.startsWith('/api/auth/')) {
|
||
return NextResponse.next();
|
||
}
|
||
|
||
// 其他API需要认证
|
||
if (!token) {
|
||
return NextResponse.json(
|
||
{ error: '未授权访问' },
|
||
{ status: 401 }
|
||
);
|
||
}
|
||
|
||
// 添加用户信息到请求头
|
||
const requestHeaders = new Headers(request.headers);
|
||
requestHeaders.set('user-id', token.sub as string);
|
||
|
||
return NextResponse.next({
|
||
request: {
|
||
headers: requestHeaders,
|
||
},
|
||
});
|
||
}
|
||
|
||
// 未登录用户访问受保护路由
|
||
if (isProtectedRoute && !token) {
|
||
const loginUrl = new URL('/login', request.url);
|
||
loginUrl.searchParams.set('callbackUrl', pathname);
|
||
return NextResponse.redirect(loginUrl);
|
||
}
|
||
|
||
// 已登录用户访问登录页
|
||
if (token && (pathname === '/login' || pathname === '/register')) {
|
||
return NextResponse.redirect(new URL('/machinery', request.url));
|
||
}
|
||
|
||
return NextResponse.next();
|
||
}
|
||
|
||
// 中间件匹配配置
|
||
export const config = {
|
||
matcher: [
|
||
/*
|
||
* 匹配所有路径除了:
|
||
* - _next/static (静态文件)
|
||
* - _next/image (图片优化)
|
||
* - favicon.ico (favicon文件)
|
||
* - public文件夹中的文件
|
||
*/
|
||
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
|
||
],
|
||
};
|
||
```
|
||
|
||
### 2.6 路由性能优化规范
|
||
|
||
#### 2.6.1 数据获取优化
|
||
|
||
```typescript
|
||
// src/lib/services/api/machineryApi.ts
|
||
import { cache } from 'react';
|
||
|
||
// 使用cache缓存数据获取
|
||
export const getMachineryById = cache(async (id: string) => {
|
||
const res = await fetch(`${process.env.API_BASE_URL}/machinery/${id}`, {
|
||
next: {
|
||
tags: [`machinery-${id}`], // 缓存标签
|
||
revalidate: 300, // 5分钟重新验证
|
||
},
|
||
});
|
||
|
||
if (!res.ok) {
|
||
throw new Error('Failed to fetch machinery');
|
||
}
|
||
|
||
return res.json();
|
||
});
|
||
|
||
export const getMachineryList = cache(async (params?: any) => {
|
||
const searchParams = new URLSearchParams(params);
|
||
const res = await fetch(`${process.env.API_BASE_URL}/machinery?${searchParams}`, {
|
||
next: {
|
||
tags: ['machinery-list'], // 缓存标签
|
||
revalidate: 60, // 1分钟重新验证
|
||
},
|
||
});
|
||
|
||
if (!res.ok) {
|
||
throw new Error('Failed to fetch machinery list');
|
||
}
|
||
|
||
return res.json();
|
||
});
|
||
|
||
// 数据重新验证
|
||
export async function revalidateMachineryCache(id?: string) {
|
||
const { revalidateTag } = await import('next/cache');
|
||
|
||
if (id) {
|
||
revalidateTag(`machinery-${id}`);
|
||
} else {
|
||
revalidateTag('machinery-list');
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2.6.2 静态生成和增量静态生成
|
||
|
||
```typescript
|
||
// src/app/machinery/archive/page.tsx
|
||
import { getMachineryCategories } from '@/lib/services/api/machineryApi';
|
||
|
||
// 增量静态生成(ISR)
|
||
export async function generateStaticParams() {
|
||
try {
|
||
const categories = await getMachineryCategories();
|
||
|
||
return categories.map((category) => ({
|
||
category: category.name,
|
||
}));
|
||
} catch {
|
||
// 如果获取失败,返回空数组
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// 设置页面为静态生成
|
||
export const dynamic = 'force-static';
|
||
export const revalidate = 3600; // 1小时重新生成
|
||
```
|
||
|
||
### 2.7 类型定义规范
|
||
|
||
#### 2.7.1 Next.js 路由类型
|
||
|
||
```typescript
|
||
// src/types/nextjs.ts
|
||
import { Metadata, ResolvingMetadata } from 'next';
|
||
|
||
// 页面Props类型
|
||
export interface PageProps {
|
||
params: Record<string, string>;
|
||
searchParams: Record<string, string | string[] | undefined>;
|
||
}
|
||
|
||
// 动态路由Props类型
|
||
export interface DynamicPageProps<T extends Record<string, string> = Record<string, string>> {
|
||
params: T;
|
||
searchParams: Record<string, string | string[] | undefined>;
|
||
}
|
||
|
||
// 布局Props类型
|
||
export interface LayoutProps {
|
||
children: React.ReactNode;
|
||
params?: Record<string, string>;
|
||
}
|
||
|
||
// 元数据生成函数类型
|
||
export type MetadataGenerator = (props: PageProps) => Promise<Metadata>;
|
||
|
||
// 服务端组件类型
|
||
export type ServerComponent<P = {}> = (props: P) => Promise<React.ReactElement>;
|
||
|
||
// 客户端组件类型
|
||
export type ClientComponent<P = {}> = (props: P) => React.ReactElement;
|
||
```
|
||
|
||
#### 2.7.2 API 响应类型
|
||
|
||
```typescript
|
||
// src/types/api.ts
|
||
export interface ApiResponse<T = any> {
|
||
success: boolean;
|
||
data?: T;
|
||
message: string;
|
||
errors?: any[];
|
||
}
|
||
|
||
export interface PaginatedResponse<T> {
|
||
items: T[];
|
||
total: number;
|
||
page: number;
|
||
pageSize: number;
|
||
totalPages: number;
|
||
}
|
||
|
||
export interface ApiError {
|
||
success: false;
|
||
message: string;
|
||
code?: string;
|
||
details?: any;
|
||
}
|
||
```
|
||
|
||
### 2.2 类型定义规范
|
||
|
||
#### 2.2.1 页面类型定义
|
||
|
||
```typescript
|
||
// pages/AgriculturalMachinery/Archive/MachineryEntry/index.types.ts
|
||
export interface MachineryRecord {
|
||
id: string;
|
||
name: string;
|
||
model: string;
|
||
category: MachineryCategory;
|
||
status: MachineryStatus;
|
||
manufacturer: string;
|
||
purchaseDate: string;
|
||
price: number;
|
||
// ... 其他字段
|
||
}
|
||
|
||
export type MachineryCategory =
|
||
| '耕地机械'
|
||
| '播种机械'
|
||
| '收获机械'
|
||
| '植保机械';
|
||
|
||
export type MachineryStatus =
|
||
| '运行中'
|
||
| '空闲中'
|
||
| '待维护'
|
||
| '已报废';
|
||
|
||
export interface MachineryFilters {
|
||
category?: MachineryCategory;
|
||
status?: MachineryStatus;
|
||
manufacturer?: string;
|
||
dateRange?: [string, string];
|
||
}
|
||
|
||
export interface PaginationState {
|
||
current: number;
|
||
pageSize: number;
|
||
total: number;
|
||
}
|
||
```
|
||
|
||
#### 2.2.2 组件类型定义
|
||
|
||
```typescript
|
||
// pages/AgriculturalMachinery/Archive/MachineryEntry/components/MachineryList/types.ts
|
||
import { MachineryRecord, MachineryFilters, PaginationState } from '../../index.types';
|
||
|
||
export interface MachineryListProps {
|
||
machinery: MachineryRecord[];
|
||
loading: boolean;
|
||
error: string | null;
|
||
filters: MachineryFilters;
|
||
pagination: PaginationState;
|
||
onPageChange: (page: number) => void;
|
||
onEdit: (record: MachineryRecord) => void;
|
||
onDelete: (id: string) => void;
|
||
onBatchDelete: (ids: string[]) => void;
|
||
}
|
||
|
||
export interface SelectedRow {
|
||
id: string;
|
||
name: string;
|
||
// ... 其他需要显示在选中行的字段
|
||
}
|
||
```
|
||
|
||
### 2.3 Hooks规范
|
||
|
||
#### 2.3.1 页面数据Hook
|
||
|
||
```javascript
|
||
// pages/AgriculturalMachinery/Archive/MachineryEntry/hooks/usePageData.js
|
||
import { useState, useEffect, useCallback } from 'react';
|
||
import { getMachineryList, deleteMachinery } from '@/apis/subModules/agriculturalMachinery';
|
||
import { showMessage } from '@/utils/message';
|
||
|
||
export function useMachineryData() {
|
||
const [machinery, setMachinery] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState(null);
|
||
const [filters, setFilters] = useState({});
|
||
const [pagination, setPagination] = useState({
|
||
current: 1,
|
||
pageSize: 10,
|
||
total: 0
|
||
});
|
||
|
||
// 获取数据
|
||
const fetchMachinery = useCallback(async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
|
||
try {
|
||
const params = {
|
||
...filters,
|
||
page: pagination.current,
|
||
pageSize: pagination.pageSize
|
||
};
|
||
|
||
const response = await getMachineryList(params);
|
||
setMachinery(response.data.list);
|
||
setPagination(prev => ({
|
||
...prev,
|
||
total: response.data.total
|
||
}));
|
||
} catch (err) {
|
||
setError(err.message);
|
||
showMessage('error', '获取农机数据失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [filters, pagination.current, pagination.pageSize]);
|
||
|
||
// 初始加载
|
||
useEffect(() => {
|
||
fetchMachinery();
|
||
}, [fetchMachinery]);
|
||
|
||
// 处理筛选变化
|
||
const handleFilterChange = useCallback((newFilters) => {
|
||
setFilters(newFilters);
|
||
setPagination(prev => ({ ...prev, current: 1 }));
|
||
}, []);
|
||
|
||
// 处理分页变化
|
||
const handlePageChange = useCallback((page) => {
|
||
setPagination(prev => ({ ...prev, current: page }));
|
||
}, []);
|
||
|
||
return {
|
||
machinery,
|
||
loading,
|
||
error,
|
||
filters,
|
||
pagination,
|
||
handleFilterChange,
|
||
handlePageChange,
|
||
refreshData: fetchMachinery
|
||
};
|
||
}
|
||
```
|
||
|
||
#### 2.3.2 页面操作Hook
|
||
|
||
```javascript
|
||
// pages/AgriculturalMachinery/Archive/MachineryEntry/hooks/usePageActions.js
|
||
import { useCallback } from 'react';
|
||
import { deleteMachinery, updateMachinery } from '@/apis/subModules/agriculturalMachinery';
|
||
import { showMessage } from '@/utils/message';
|
||
|
||
export function useMachineryActions(refreshData) {
|
||
// 删除农机
|
||
const handleDelete = useCallback(async (id) => {
|
||
try {
|
||
await deleteMachinery(id);
|
||
showMessage('success', '删除成功');
|
||
refreshData();
|
||
} catch (error) {
|
||
showMessage('error', '删除失败');
|
||
}
|
||
}, [refreshData]);
|
||
|
||
// 编辑农机
|
||
const handleEdit = useCallback(async (data) => {
|
||
try {
|
||
await updateMachinery(data.id, data);
|
||
showMessage('success', '更新成功');
|
||
refreshData();
|
||
} catch (error) {
|
||
showMessage('error', '更新失败');
|
||
}
|
||
}, [refreshData]);
|
||
|
||
// 批量删除
|
||
const handleBatchDelete = useCallback(async (ids) => {
|
||
try {
|
||
await Promise.all(ids.map(id => deleteMachinery(id)));
|
||
showMessage('success', '批量删除成功');
|
||
refreshData();
|
||
} catch (error) {
|
||
showMessage('error', '批量删除失败');
|
||
}
|
||
}, [refreshData]);
|
||
|
||
// 导出数据
|
||
const handleExport = useCallback(() => {
|
||
// 实现导出逻辑
|
||
console.log('导出数据');
|
||
}, []);
|
||
|
||
return {
|
||
handleDelete,
|
||
handleEdit,
|
||
handleBatchDelete,
|
||
handleExport
|
||
};
|
||
}
|
||
```
|
||
|
||
### 2.4 样式规范
|
||
|
||
#### 2.4.1 页面主样式
|
||
|
||
```css
|
||
/* pages/AgriculturalMachinery/Archive/MachineryEntry/index.css */
|
||
.machinery-entry {
|
||
padding: 24px;
|
||
background: #f5f5f5;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
padding: 16px;
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.page-header h1 {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin: 0;
|
||
}
|
||
|
||
.page-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.page-actions button {
|
||
padding: 8px 16px;
|
||
border-radius: 4px;
|
||
border: 1px solid #d9d9d9;
|
||
background: white;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.page-actions button:hover {
|
||
background: #f0f0f0;
|
||
border-color: #40a9ff;
|
||
}
|
||
|
||
.page-actions button.primary {
|
||
background: #40a9ff;
|
||
color: white;
|
||
border-color: #40a9ff;
|
||
}
|
||
|
||
.page-actions button.primary:hover {
|
||
background: #1890ff;
|
||
border-color: #1890ff;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.page-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
}
|
||
|
||
.page-actions {
|
||
width: 100%;
|
||
justify-content: flex-end;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2.4.2 组件样式
|
||
|
||
```css
|
||
/* pages/AgriculturalMachinery/Archive/MachineryEntry/components/MachineryList/index.css */
|
||
.machinery-list {
|
||
background: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.error-state {
|
||
padding: 48px;
|
||
text-align: center;
|
||
color: #666;
|
||
}
|
||
|
||
.error-state p {
|
||
margin-bottom: 16px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.error-state button {
|
||
padding: 8px 16px;
|
||
background: #ff4d4f;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.error-state button:hover {
|
||
background: #ff7875;
|
||
}
|
||
|
||
.table-actions {
|
||
padding: 16px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.selected-info {
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.batch-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.batch-actions button {
|
||
padding: 4px 8px;
|
||
font-size: 12px;
|
||
border-radius: 4px;
|
||
border: 1px solid #d9d9d9;
|
||
background: white;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.batch-actions button:hover {
|
||
background: #f0f0f0;
|
||
}
|
||
|
||
.batch-actions button.danger {
|
||
color: #ff4d4f;
|
||
border-color: #ff4d4f;
|
||
}
|
||
|
||
.batch-actions button.danger:hover {
|
||
background: #ff4d4f;
|
||
color: white;
|
||
}
|
||
```
|
||
|
||
### 2.5 工具函数规范
|
||
|
||
#### 2.5.1 页面工具函数
|
||
|
||
```javascript
|
||
// pages/AgriculturalMachinery/Archive/MachineryEntry/utils/pageHelpers.js
|
||
import dayjs from 'dayjs';
|
||
import { formatCurrency } from '@/utils/formatters';
|
||
|
||
/**
|
||
* 格式化农机状态
|
||
*/
|
||
export function formatMachineryStatus(status) {
|
||
const statusMap = {
|
||
'运行中': { text: '运行中', color: '#52c41a' },
|
||
'空闲中': { text: '空闲中', color: '#d9d9d9' },
|
||
'待维护': { text: '待维护', color: '#faad14' },
|
||
'已报废': { text: '已报废', color: '#ff4d4f' }
|
||
};
|
||
|
||
return statusMap[status] || { text: '未知', color: '#d9d9d9' };
|
||
}
|
||
|
||
/**
|
||
* 格式化日期
|
||
*/
|
||
export function formatDate(date) {
|
||
return dayjs(date).format('YYYY-MM-DD HH:mm');
|
||
}
|
||
|
||
/**
|
||
* 格式化价格
|
||
*/
|
||
export function formatPrice(price) {
|
||
return formatCurrency(price);
|
||
}
|
||
|
||
/**
|
||
* 生成农机二维码内容
|
||
*/
|
||
export function generateQRCode(machinery) {
|
||
return JSON.stringify({
|
||
id: machinery.id,
|
||
name: machinery.name,
|
||
model: machinery.model,
|
||
category: machinery.category,
|
||
manufacturer: machinery.manufacturer
|
||
});
|
||
}
|
||
```
|
||
|
||
#### 2.5.2 格式化工具
|
||
|
||
```javascript
|
||
// pages/AgriculturalMachinery/Archive/MachineryEntry/utils/formatters.js
|
||
/**
|
||
* 格式化货币
|
||
*/
|
||
export function formatCurrency(amount) {
|
||
return new Intl.NumberFormat('zh-CN', {
|
||
style: 'currency',
|
||
currency: 'CNY'
|
||
}).format(amount);
|
||
}
|
||
|
||
/**
|
||
* 格式化数字
|
||
*/
|
||
export function formatNumber(num, precision = 2) {
|
||
return new Intl.NumberFormat('zh-CN', {
|
||
minimumFractionDigits: precision,
|
||
maximumFractionDigits: precision
|
||
}).format(num);
|
||
}
|
||
|
||
/**
|
||
* 格式化百分比
|
||
*/
|
||
export function formatPercent(value) {
|
||
return `${(value * 100).toFixed(1)}%`;
|
||
}
|
||
```
|
||
|
||
### 2.6 常量定义规范
|
||
|
||
```javascript
|
||
// pages/AgriculturalMachinery/Archive/MachineryEntry/constants.js
|
||
export const MACHINERY_CATEGORIES = [
|
||
{ value: '耕地机械', label: '耕地机械' },
|
||
{ value: '播种机械', label: '播种机械' },
|
||
{ value: '收获机械', label: '收获机械' },
|
||
{ value: '植保机械', label: '植保机械' }
|
||
];
|
||
|
||
export const MACHINERY_STATUS = [
|
||
{ value: '运行中', label: '运行中', color: '#52c41a' },
|
||
{ value: '空闲中', label: '空闲中', color: '#d9d9d9' },
|
||
{ value: '待维护', label: '待维护', color: '#faad14' },
|
||
{ value: '已报废', label: '已报废', color: '#ff4d4f' }
|
||
];
|
||
|
||
export const MANUFACTURERS = [
|
||
'约翰迪尔',
|
||
'久保田',
|
||
'福田雷沃',
|
||
'中国一拖',
|
||
'时风集团'
|
||
];
|
||
|
||
export const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
||
export const DEFAULT_PAGE_SIZE = 10;
|
||
```
|
||
|
||
## 3. 组件拆分示例
|
||
|
||
### 3.1 MachineryEntry 组件拆分
|
||
|
||
```
|
||
MachineryEntry/
|
||
├── index.jsx # 主组件
|
||
├── index.css # 主样式
|
||
├── index.types.ts # 主类型定义
|
||
├── hooks/ # 页面Hooks
|
||
│ ├── usePageData.js # 数据管理Hook
|
||
│ └── usePageActions.js # 操作Hook
|
||
├── utils/ # 工具函数
|
||
│ ├── pageHelpers.js # 页面工具函数
|
||
│ └── formatters.js # 格式化函数
|
||
├── constants.js # 常量定义
|
||
└── components/ # 子组件
|
||
├── MachineryFilter/ # 筛选组件
|
||
│ ├── index.jsx
|
||
│ ├── index.css
|
||
│ └── types.ts
|
||
├── MachineryList/ # 列表组件
|
||
│ ├── index.jsx
|
||
│ ├── index.css
|
||
│ ├── types.ts
|
||
│ └── components/
|
||
│ ├── MachineryTable/ # 表格组件
|
||
│ │ ├── index.jsx
|
||
│ │ ├── index.css
|
||
│ │ └── types.ts
|
||
│ └── TableActions/ # 表格操作组件
|
||
│ ├── index.jsx
|
||
│ ├── index.css
|
||
│ └── types.ts
|
||
├── MachineryForm/ # 表单组件
|
||
│ ├── index.jsx
|
||
│ ├── index.css
|
||
│ ├── types.ts
|
||
│ └── components/
|
||
│ ├── BasicInfo/ # 基本信息表单
|
||
│ ├── TechnicalInfo/ # 技术参数表单
|
||
│ ├── PurchaseInfo/ # 购买信息表单
|
||
│ └── InsuranceInfo/ # 保险信息表单
|
||
└── common/ # 通用组件
|
||
├── PageHeader/ # 页面头部
|
||
├── LoadingSpinner/ # 加载动画
|
||
└── EmptyState/ # 空状态
|
||
```
|
||
|
||
## 4. 文件命名规范
|
||
|
||
### 4.1 组件文件命名
|
||
|
||
- **主组件**: 使用功能名称,如 `MachineryEntry.jsx`
|
||
- **子组件**: 使用功能描述,如 `MachineryTable.jsx`
|
||
- **通用组件**: 使用通用描述,如 `TableHeader.jsx`
|
||
- **Hooks**: 以 `use` 开头,如 `usePageData.js`
|
||
- **工具函数**: 使用动词或名词,如 `pageHelpers.js`, `formatters.js`
|
||
- **类型文件**: 以 `.types.ts` 结尾,如 `index.types.ts`
|
||
- **样式文件**: 与组件同名,如 `index.css`
|
||
|
||
### 4.2 目录命名
|
||
|
||
- **页面目录**: 使用业务模块名,如 `AgriculturalMachinery`
|
||
- **组件目录**: 使用功能描述,如 `MachineryList`
|
||
- **通用目录**: 使用通用描述,如 `common`
|
||
|
||
## 5. 开发流程
|
||
|
||
### 5.1 新页面开发流程
|
||
|
||
1. **创建页面目录结构**
|
||
2. **定义类型接口** (index.types.ts)
|
||
3. **开发主组件** (index.jsx)
|
||
4. **拆分子组件** (components/)
|
||
5. **创建Hooks** (hooks/)
|
||
6. **编写工具函数** (utils/)
|
||
7. **定义常量** (constants.js)
|
||
8. **编写样式** (index.css)
|
||
9. **单元测试**
|
||
|
||
### 5.2 组件拆分流程
|
||
|
||
1. **识别拆分点**: 组件代码复杂度、复用性、职责单一性
|
||
2. **创建组件目录**: 创建 `components/ComponentName/` 目录
|
||
3. **提取组件逻辑**: 将相关代码移动到新组件
|
||
4. **定义Props接口**: 创建类型定义文件
|
||
5. **处理状态管理**: 使用Hooks或状态提升
|
||
6. **更新主组件**: 修改主组件使用新组件
|
||
7. **样式分离**: 创建独立的样式文件
|
||
8. **测试验证**: 确保功能正常
|
||
|
||
## 6. 质量保证
|
||
|
||
### 6.1 代码检查
|
||
|
||
- 使用ESLint检查代码规范
|
||
- 使用Prettier格式化代码
|
||
- 使用TypeScript进行类型检查
|
||
- 组件必须有PropTypes或TypeScript接口
|
||
|
||
### 6.2 性能优化
|
||
|
||
- 使用React.memo包装组件避免不必要的重渲染
|
||
- 使用useCallback和useMemo优化性能
|
||
- 实现虚拟滚动处理大数据量
|
||
- 使用懒加载和代码分割
|
||
|
||
### 6.3 可维护性
|
||
|
||
- 保持组件单一职责
|
||
- 提取可复用的通用组件
|
||
- 编写清晰的注释和文档
|
||
- 保持一致的代码风格
|
||
|
||
这个规范为pages目录的开发提供了完整的指导,确保代码的可维护性、可扩展性和团队协作效率。 |