38 KiB
38 KiB
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 组件拆分原则
拆分条件:
- 组件代码超过100行
- 组件承担多个职责
- 组件需要复用
- 组件逻辑复杂(包含多个useState或useEffect)
拆分命名:
- 主组件:保持原名称(如:MachineryEntry)
- 子组件:基于功能命名(如:MachineryList、MachineryForm、MachineryFilter)
- 通用组件:基于用途命名(如:TableHeader、FormActions)
2. Next.js 开发规范
2.1 页面组件开发规范
2.1.1 服务端组件规范(推荐)
// 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 客户端组件规范
// 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 布局组件规范
// 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 加载和错误状态
// 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
// 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路由
// 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 认证中间件
// 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 数据获取优化
// 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 静态生成和增量静态生成
// 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 路由类型
// 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 响应类型
// 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 页面类型定义
// 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 组件类型定义
// 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
// 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
// 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 页面主样式
/* 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 组件样式
/* 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 页面工具函数
// 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 格式化工具
// 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 常量定义规范
// 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 新页面开发流程
- 创建页面目录结构
- 定义类型接口 (index.types.ts)
- 开发主组件 (index.jsx)
- 拆分子组件 (components/)
- 创建Hooks (hooks/)
- 编写工具函数 (utils/)
- 定义常量 (constants.js)
- 编写样式 (index.css)
- 单元测试
5.2 组件拆分流程
- 识别拆分点: 组件代码复杂度、复用性、职责单一性
- 创建组件目录: 创建
components/ComponentName/目录 - 提取组件逻辑: 将相关代码移动到新组件
- 定义Props接口: 创建类型定义文件
- 处理状态管理: 使用Hooks或状态提升
- 更新主组件: 修改主组件使用新组件
- 样式分离: 创建独立的样式文件
- 测试验证: 确保功能正常
6. 质量保证
6.1 代码检查
- 使用ESLint检查代码规范
- 使用Prettier格式化代码
- 使用TypeScript进行类型检查
- 组件必须有PropTypes或TypeScript接口
6.2 性能优化
- 使用React.memo包装组件避免不必要的重渲染
- 使用useCallback和useMemo优化性能
- 实现虚拟滚动处理大数据量
- 使用懒加载和代码分割
6.3 可维护性
- 保持组件单一职责
- 提取可复用的通用组件
- 编写清晰的注释和文档
- 保持一致的代码风格
这个规范为pages目录的开发提供了完整的指导,确保代码的可维护性、可扩展性和团队协作效率。