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

38 KiB
Raw Permalink Blame History

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 服务端组件规范(推荐)

// 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 新页面开发流程

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