Files
smart-crop-ui/docs/Pages目录结构与开发规范.md

1474 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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目录的开发提供了完整的指导确保代码的可维护性、可扩展性和团队协作效率。