FastGPT-03-Web组件与主应用


第一部分:packages/web - Web组件层

模块概览

职责:提供可复用的 UI 组件、React Hooks、国际化工具,供主应用(projects/app)使用。

目录结构

packages/web/
├── common/           # 通用工具(fetch、文件、Zustand)
├── components/       # UI组件(按钮、表单、图标等)
├── context/          # React Context(系统状态)
├── hooks/            # 自定义 Hooks
├── i18n/             # 国际化配置和翻译
├── store/            # 全局状态管理(Zustand)
└── styles/           # 主题和样式

核心组件

1. 通用 UI 组件

MyIcon(图标组件):

// components/common/Icon/index.tsx

import { Icon as ChakraIcon } from '@chakra-ui/react';
import type { IconProps } from '@chakra-ui/react';
import { iconPaths } from './constants';

export const MyIcon = ({
  name,
  width = '1em',
  height = '1em',
  ...props
}: {
  name: string;
} & IconProps) => {
  const path = iconPaths[name];
  
  if (!path) {
    console.warn(`Icon "${name}" not found`);
    return null;
  }

  return (
    <ChakraIcon viewBox="0 0 1024 1024" {...props} w={width} h={height}>
      <path d={path} fill="currentColor" />
    </ChakraIcon>
  );
};

MyTooltip(提示组件):

// components/common/MyTooltip/index.tsx

import { Tooltip } from '@chakra-ui/react';

export const MyTooltip = ({ label, children, ...props }) => {
  return (
    <Tooltip
      label={label}
      bg="gray.700"
      color="white"
      fontSize="sm"
      px={3}
      py={2}
      borderRadius="md"
      hasArrow
      {...props}
    >
      {children}
    </Tooltip>
  );
};

2. 自定义 Hooks

useRequest(封装异步请求):

// hooks/useRequest.tsx

import { useState, useCallback } from 'react';
import { useToast } from './useToast';

export const useRequest = <T = any, P extends any[] = any[]>({
  fetcher,
  onSuccess,
  onError,
  successToast = '',
  errorToast = ''
}: {
  fetcher: (...args: P) => Promise<T>;
  onSuccess?: (data: T) => void;
  onError?: (error: any) => void;
  successToast?: string;
  errorToast?: string;
}) => {
  const [loading, setLoading] = useState(false);
  const { toast } = useToast();

  const run = useCallback(
    async (...args: P) => {
      setLoading(true);
      try {
        const data = await fetcher(...args);
        onSuccess?.(data);
        if (successToast) {
          toast({ title: successToast, status: 'success' });
        }
        return data;
      } catch (error) {
        onError?.(error);
        if (errorToast) {
          toast({ title: errorToast, status: 'error' });
        }
        throw error;
      } finally {
        setLoading(false);
      }
    },
    [fetcher, onSuccess, onError]
  );

  return { loading, run };
};

useConfirm(确认对话框):

// hooks/useConfirm.tsx

import { useDisclosure } from '@chakra-ui/react';
import { useCallback, useRef } from 'react';

export const useConfirm = ({
  title = '提示',
  content,
  onConfirm
}: {
  title?: string;
  content: string;
  onConfirm: () => void | Promise<void>;
}) => {
  const { isOpen, onOpen, onClose } = useDisclosure();
  const callbackRef = useRef(onConfirm);
  callbackRef.current = onConfirm;

  const openConfirm = useCallback(() => {
    onOpen();
  }, [onOpen]);

  const ConfirmModal = useCallback(() => {
    return (
      <Modal isOpen={isOpen} onClose={onClose}>
        <ModalOverlay />
        <ModalContent>
          <ModalHeader>{title}</ModalHeader>
          <ModalBody>{content}</ModalBody>
          <ModalFooter>
            <Button onClick={onClose}>取消</Button>
            <Button
              ml={3}
              colorScheme="red"
              onClick={async () => {
                await callbackRef.current();
                onClose();
              }}
            >
              确认
            </Button>
          </ModalFooter>
        </ModalContent>
      </Modal>
    );
  }, [isOpen, onClose, title, content]);

  return { openConfirm, ConfirmModal };
};

3. 国际化(i18n)

配置

// i18n/utils.ts

import { useTranslation } from 'react-i18next';

export const useI18n = () => {
  const { t, i18n } = useTranslation();
  
  return {
    t,
    currentLang: i18n.language,
    changeLanguage: (lang: string) => i18n.changeLanguage(lang)
  };
};

// 翻译key生成
export const i18nT = (key: string, options?: any) => {
  return key; // 在运行时由 i18next 处理
};

翻译文件结构

i18n/
├── en/
│   ├── common.json
│   ├── app.json
│   ├── chat.json
│   └── ...
└── zh-CN/
    ├── common.json
    ├── app.json
    ├── chat.json
    └── ...

第二部分:projects/app - 主应用

模块概览

职责:FastGPT 的主应用,基于 Next.js 构建,包含前端页面和后端 API Routes。

技术栈

  • Next.js 14(SSR + API Routes)
  • React 18
  • Chakra UI(UI框架)
  • React Query(数据获取)
  • Zustand(状态管理)

目录结构

projects/app/
├── public/          # 静态资源
├── src/
│   ├── components/  # 应用组件
│   ├── global/      # 全局配置
│   ├── pageComponents/ # 页面组件
│   ├── pages/       # Next.js页面和API路由
│   ├── service/     # 服务层(调用@fastgpt/service)
│   ├── types/       # 类型定义
│   └── web/         # Web工具
└── data/            # 配置文件(config.json、model.json)

核心 API Routes

1. 对话 API

v2/chat/completions(OpenAI 兼容接口):

// pages/api/v2/chat/completions.ts

import type { NextApiRequest, NextApiResponse } from 'next';
import { authChatCert } from '@fastgpt/service/support/permission/auth/chat';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { saveChat } from '@fastgpt/service/core/chat/saveChat';

type AuthResponseType = {
  teamId: string;
  tmbId: string;
  appId: string;
  app: AppSchema;
  canWrite: boolean;
};

async function handler(req: NextApiRequest, res: NextApiResponse) {
  const {
    chatId,
    appId,
    stream = true,
    detail = false,
    messages,
    variables = {}
  } = req.body;

  // 1. 鉴权
  const { teamId, tmbId, app, canWrite }: AuthResponseType = await authChatCert({
    req,
    authToken: true,
    appId
  });

  // 2. 提取用户输入
  const userChatInput = messages[messages.length - 1].content;

  // 3. 初始化对话(获取历史)
  const { histories } = await initChat({
    appId,
    chatId,
    teamId,
    tmbId
  });

  // 4. 执行工作流
  const { flowResponses, flowUsages, assistantResponses, newVariables } = await dispatchWorkFlow({
    res: stream ? res : undefined,
    stream,
    detail,
    runningUserInfo: { teamId, tmbId },
    runningAppInfo: { id: appId, name: app.name },
    runtimeNodes: app.nodes,
    runtimeEdges: app.edges,
    histories,
    variables: {
      ...variables,
      cTime: new Date().toISOString(),
      appId
    },
    chatConfig: app.chatConfig,
    mode: 'chat',
    usageSource: 'fastgpt'
  });

  // 5. 保存对话
  await saveChat({
    chatId,
    appId,
    teamId,
    tmbId,
    nodes: app.nodes,
    appChatConfig: app.chatConfig,
    variables: newVariables,
    userContent: {
      obj: 'Human',
      value: [{ type: 'text', text: { content: userChatInput } }]
    },
    aiContent: {
      obj: 'AI',
      value: assistantResponses
    },
    durationSeconds: flowUsages.reduce((sum, u) => sum + (u.duration || 0), 0)
  });

  // 6. 返回响应
  if (stream) {
    res.end();
  } else {
    res.json({
      id: chatId,
      model: app.chatConfig.model,
      usage: {
        prompt_tokens: flowUsages.reduce((sum, u) => sum + u.inputTokens, 0),
        completion_tokens: flowUsages.reduce((sum, u) => sum + u.outputTokens, 0),
        total_tokens: flowUsages.reduce((sum, u) => sum + u.totalTokens, 0)
      },
      choices: [
        {
          message: {
            role: 'assistant',
            content: assistantResponses
              .filter(r => r.type === 'text')
              .map(r => r.text?.content)
              .join('\n')
          },
          finish_reason: 'stop'
        }
      ]
    });
  }
}

export default handler;

关键逻辑

  1. 鉴权authChatCert):验证 API Key 或 Token,获取用户信息
  2. 初始化对话:加载历史记录和上下文
  3. 工作流执行:调用 Service 层的 dispatchWorkFlow
  4. 保存对话:存储用户消息和 AI 回复
  5. 流式响应:通过 SSE 实时推送(stream=true)
  6. 非流式响应:返回完整的 OpenAI 格式 JSON(stream=false)

2. 知识库 API

dataset/collection/create(上传文件创建集合):

// pages/api/core/dataset/collection/create.ts

import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { uploadFile } from '@fastgpt/service/common/file/gridfs/controller';
import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/schema';
import { fileParseQueue } from '@fastgpt/service/worker/controller';

async function handler(req: ApiRequestProps) {
  const { datasetId, name, mode = 'auto' } = req.body;
  const file = req.file; // Multer 上传的文件

  // 1. 验证权限
  const { dataset } = await authDataset({
    req,
    authToken: true,
    authApiKey: true,
    datasetId,
    per: 'w'
  });

  // 2. 上传文件到 GridFS/S3
  const { fileId } = await uploadFile({
    teamId: dataset.teamId,
    file: {
      originalname: file.originalname,
      buffer: file.buffer
    }
  });

  // 3. 创建 Collection
  const collection = await MongoDatasetCollection.create({
    teamId: dataset.teamId,
    datasetId: dataset._id,
    name: name || file.originalname,
    type: DatasetCollectionTypeEnum.file,
    fileId,
    trainingType: mode,
    chunkSettings: dataset.chunkSettings
  });

  // 4. 发送文件解析任务到队列
  await fileParseQueue.add('parse', {
    collectionId: String(collection._id),
    datasetId: String(dataset._id),
    teamId: dataset.teamId,
    fileId,
    filePath: file.originalname
  });

  return {
    collectionId: collection._id
  };
}

export default withNextCors(
  NextAPI(handler, {
    upload: {
      maxFileSize: 100 * 1024 * 1024 // 100MB
    }
  })
);

3. 应用管理 API

app/create(创建应用):

// pages/api/core/app/create.ts

import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { MongoApp } from '@fastgpt/service/core/app/schema';

async function handler(req: ApiRequestProps) {
  const { name, avatar, type = 'simple' } = req.body;

  // 1. 鉴权
  const { teamId, tmbId } = await authUserPer({
    req,
    authToken: true,
    authApiKey: true,
    per: 'w'
  });

  // 2. 创建应用(包含默认工作流节点)
  const defaultNodes = getDefaultNodes(type);
  const defaultEdges = getDefaultEdges(type);

  const app = await MongoApp.create({
    teamId,
    tmbId,
    name,
    avatar: avatar || '/icon/logo.svg',
    type,
    nodes: defaultNodes,
    edges: defaultEdges,
    chatConfig: getDefaultChatConfig()
  });

  return {
    appId: app._id
  };
}

// 获取默认节点(根据应用类型)
const getDefaultNodes = (type: AppTypeEnum) => {
  if (type === 'simple') {
    return [
      {
        nodeId: 'workflowStartNodeId',
        flowNodeType: FlowNodeTypeEnum.workflowStart,
        name: '流程开始',
        position: { x: 300, y: 300 },
        inputs: [
          {
            key: NodeInputKeyEnum.userChatInput,
            renderTypeList: [FlowNodeInputTypeEnum.reference],
            valueType: WorkflowIOValueTypeEnum.string,
            label: '用户问题'
          }
        ],
        outputs: [
          {
            id: 'userChatInput',
            key: NodeInputKeyEnum.userChatInput,
            label: '用户问题',
            valueType: WorkflowIOValueTypeEnum.string
          }
        ]
      },
      {
        nodeId: 'aiChatNodeId',
        flowNodeType: FlowNodeTypeEnum.chatNode,
        name: 'AI 对话',
        position: { x: 800, y: 300 },
        inputs: [
          {
            key: NodeInputKeyEnum.aiModel,
            renderTypeList: [FlowNodeInputTypeEnum.selectLLMModel],
            value: 'gpt-3.5-turbo'
          },
          {
            key: NodeInputKeyEnum.aiSystemPrompt,
            renderTypeList: [FlowNodeInputTypeEnum.textarea],
            value: '你是一个友好的AI助手。'
          },
          {
            key: NodeInputKeyEnum.userChatInput,
            renderTypeList: [FlowNodeInputTypeEnum.reference],
            value: ['workflowStartNodeId', NodeInputKeyEnum.userChatInput]
          }
        ]
      }
    ];
  }
  // 其他类型...
};

核心页面组件

1. 工作流编辑器

位置src/pageComponents/app/EditNodes/index.tsx

基于 ReactFlow 实现的可视化工作流编辑器:

import ReactFlow, {
  Node,
  Edge,
  useNodesState,
  useEdgesState,
  addEdge,
  Background,
  Controls
} from 'reactflow';
import 'reactflow/dist/style.css';

export const WorkflowEditor = ({ appId }: { appId: string }) => {
  const [nodes, setNodes, onNodesChange] = useNodesState([]);
  const [edges, setEdges, onEdgesChange] = useEdgesState([]);

  // 加载应用数据
  useEffect(() => {
    fetchAppDetail(appId).then(app => {
      setNodes(app.nodes.map(nodeToFlowNode));
      setEdges(app.edges.map(edgeToFlowEdge));
    });
  }, [appId]);

  // 连接节点
  const onConnect = useCallback((params) => {
    setEdges((eds) => addEdge(params, eds));
  }, []);

  // 保存工作流
  const handleSave = async () => {
    await updateApp({
      appId,
      nodes: nodes.map(flowNodeToNode),
      edges: edges.map(flowEdgeToEdge)
    });
  };

  return (
    <Box h="100vh">
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
        nodeTypes={customNodeTypes}
        fitView
      >
        <Background />
        <Controls />
      </ReactFlow>
      <Button onClick={handleSave}>保存</Button>
    </Box>
  );
};

// 自定义节点组件
const customNodeTypes = {
  [FlowNodeTypeEnum.chatNode]: ChatNodeCard,
  [FlowNodeTypeEnum.datasetSearchNode]: DatasetSearchNodeCard,
  [FlowNodeTypeEnum.agent]: AgentNodeCard,
  // ... 其他节点类型
};

节点卡片组件示例

const ChatNodeCard = ({ data, id }: NodeProps) => {
  const { updateNodeInputs } = useWorkflowStore();

  return (
    <Box
      border="1px solid"
      borderColor="gray.300"
      borderRadius="md"
      p={4}
      bg="white"
      minW="300px"
    >
      <Flex align="center" mb={3}>
        <MyIcon name="chat" mr={2} />
        <Text fontWeight="bold">AI 对话</Text>
      </Flex>

      {/* 输入配置 */}
      {data.inputs.map((input) => (
        <FormControl key={input.key} mb={3}>
          <FormLabel>{input.label}</FormLabel>
          {input.renderTypeList.includes('select') ? (
            <Select
              value={input.value}
              onChange={(e) =>
                updateNodeInputs(id, input.key, e.target.value)
              }
            >
              {/* 选项 */}
            </Select>
          ) : (
            <Input
              value={input.value}
              onChange={(e) =>
                updateNodeInputs(id, input.key, e.target.value)
              }
            />
          )}
        </FormControl>
      ))}

      {/* 连接点(Handles) */}
      <Handle type="target" position="left" id="target" />
      <Handle type="source" position="right" id="source" />
    </Box>
  );
};

2. 对话界面

位置src/pageComponents/chat/index.tsx

export const ChatPage = ({ appId, chatId }: { appId: string; chatId?: string }) => {
  const [messages, setMessages] = useState<ChatItemType[]>([]);
  const [inputValue, setInputValue] = useState('');
  const [isStreaming, setIsStreaming] = useState(false);

  // 加载历史消息
  useEffect(() => {
    if (chatId) {
      fetchChatHistory({ appId, chatId }).then(setMessages);
    }
  }, [appId, chatId]);

  // 发送消息
  const handleSend = async () => {
    if (!inputValue.trim()) return;

    const newMessage: ChatItemType = {
      obj: 'Human',
      value: [{ type: 'text', text: { content: inputValue } }]
    };

    setMessages([...messages, newMessage]);
    setInputValue('');
    setIsStreaming(true);

    try {
      // SSE 流式请求
      const response = await fetch('/api/v2/chat/completions', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${apiKey}`
        },
        body: JSON.stringify({
          chatId: chatId || nanoid(),
          appId,
          stream: true,
          messages: [
            ...messages.map(msgToOpenAI),
            { role: 'user', content: inputValue }
          ]
        })
      });

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let aiMessage = '';

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value);
        const lines = chunk.split('\n').filter((line) => line.trim());

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const data = JSON.parse(line.slice(6));
            const content = data.choices[0]?.delta?.content || '';
            aiMessage += content;

            // 实时更新 AI 消息
            setMessages((msgs) => {
              const lastMsg = msgs[msgs.length - 1];
              if (lastMsg.obj === 'AI') {
                return [
                  ...msgs.slice(0, -1),
                  {
                    ...lastMsg,
                    value: [
                      {
                        type: 'text',
                        text: { content: aiMessage }
                      }
                    ]
                  }
                ];
              } else {
                return [
                  ...msgs,
                  {
                    obj: 'AI',
                    value: [
                      {
                        type: 'text',
                        text: { content: aiMessage }
                      }
                    ]
                  }
                ];
              }
            });
          }
        }
      }
    } catch (error) {
      console.error('发送消息失败', error);
    } finally {
      setIsStreaming(false);
    }
  };

  return (
    <Flex direction="column" h="100vh">
      {/* 消息列表 */}
      <Box flex={1} overflowY="auto" p={4}>
        {messages.map((msg, index) => (
          <ChatMessage key={index} message={msg} />
        ))}
      </Box>

      {/* 输入框 */}
      <Flex p={4} borderTop="1px solid" borderColor="gray.200">
        <Textarea
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="输入消息..."
          resize="none"
          mr={2}
        />
        <Button
          onClick={handleSend}
          isLoading={isStreaming}
          colorScheme="blue"
        >
          发送
        </Button>
      </Flex>
    </Flex>
  );
};

状态管理

使用 Zustand 管理全局状态:

// web/store/useCommonStore.ts

import create from 'zustand';
import { persist } from 'zustand/middleware';

interface CommonState {
  // 用户信息
  userInfo: UserType | null;
  setUserInfo: (user: UserType | null) => void;

  // 团队信息
  teamInfo: TeamType | null;
  setTeamInfo: (team: TeamType | null) => void;

  // 模型配置
  modelConfig: ModelConfigType[];
  setModelConfig: (config: ModelConfigType[]) => void;

  // 系统配置
  systemConfig: SystemConfigType | null;
  setSystemConfig: (config: SystemConfigType) => void;
}

export const useCommonStore = create<CommonState>()(
  persist(
    (set) => ({
      userInfo: null,
      setUserInfo: (user) => set({ userInfo: user }),

      teamInfo: null,
      setTeamInfo: (team) => set({ teamInfo: team }),

      modelConfig: [],
      setModelConfig: (config) => set({ modelConfig: config }),

      systemConfig: null,
      setSystemConfig: (config) => set({ systemConfig: config })
    }),
    {
      name: 'fastgpt-common-storage',
      partialize: (state) => ({
        userInfo: state.userInfo,
        teamInfo: state.teamInfo
      })
    }
  )
);

关键时序图

用户对话流程(前端到后端)

sequenceDiagram
    autonumber
    participant User as 用户
    participant UI as 前端页面
    participant API as API Routes
    participant Service as Service层
    participant DB as 数据库
    
    User->>UI: 输入消息并点击发送
    UI->>UI: 更新消息列表(本地)
    UI->>API: POST /api/v2/chat/completions<br/>(SSE stream)
    API->>API: 鉴权验证
    API->>Service: initChat(获取历史)
    Service->>DB: 查询历史消息
    DB-->>Service: 返回历史
    Service-->>API: 历史消息
    
    API->>Service: dispatchWorkflow(执行工作流)
    Service->>Service: 执行节点(AI、检索、Agent等)
    
    loop 流式推送
        Service-->>API: SSE chunk
        API-->>UI: data: {content}
        UI->>UI: 实时渲染AI回复
    end
    
    Service-->>API: 工作流执行完成
    API->>Service: saveChat(保存对话)
    Service->>DB: 存储对话记录
    DB-->>Service: 保存成功
    Service-->>API: 保存完成
    API-->>UI: 流结束
    UI->>UI: 标记消息完成

工作流编辑与保存流程

sequenceDiagram
    autonumber
    participant User as 用户
    participant Editor as 工作流编辑器
    participant State as React State
    participant API as API Routes
    participant DB as MongoDB
    
    User->>Editor: 打开应用编辑页面
    Editor->>API: GET /api/core/app/detail?appId=xxx
    API->>DB: 查询应用数据
    DB-->>API: 返回应用(nodes, edges)
    API-->>Editor: 应用数据
    Editor->>State: setNodes, setEdges
    
    User->>Editor: 拖拽添加新节点
    Editor->>State: addNode(newNode)
    State-->>Editor: 更新节点列表
    
    User->>Editor: 连接节点
    Editor->>State: addEdge(newEdge)
    State-->>Editor: 更新边列表
    
    User->>Editor: 点击保存按钮
    Editor->>API: POST /api/core/app/update<br/>{nodes, edges}
    API->>DB: 更新应用文档
    DB-->>API: 更新成功
    API-->>Editor: 保存成功
    Editor->>Editor: 显示成功提示

配置文件

data/config.json

系统配置文件(运行时配置):

{
  "FeConfig": {
    "show_emptyChat": true,
    "show_register": false,
    "show_appStore": true,
    "show_userDetail": true,
    "show_git": true,
    "systemTitle": "FastGPT",
    "systemDescription": "AI Agent 构建平台",
    "concatMd": "",
    "limit": {
      "exportDatasetLimitMinutes": 0,
      "websiteSyncLimitMinuted": 0
    },
    "scripts": []
  },
  "SystemParams": {
    "pluginBaseUrl": "",
    "openapiPrefix": "/api"
  }
}

data/model.json

模型配置文件:

[
  {
    "provider": "OpenAI",
    "model": "gpt-4",
    "name": "GPT-4",
    "type": "llm",
    "maxContext": 8000,
    "maxResponse": 4000,
    "quoteMaxToken": 3000,
    "vision": false,
    "functionCall": true,
    "toolChoice": true,
    "inputPrice": 30,
    "outputPrice": 60
  },
  {
    "provider": "OpenAI",
    "model": "text-embedding-ada-002",
    "name": "Embedding-2",
    "type": "embedding",
    "defaultToken": 512,
    "maxToken": 8191,
    "weight": 100,
    "inputPrice": 0.1
  }
]

部署与构建

构建命令

# 开发环境
pnpm dev

# 构建生产版本
pnpm build

# 启动生产服务
pnpm start

环境变量

# MongoDB
MONGODB_URI=mongodb://localhost:27017/fastgpt

# PostgreSQL(向量数据库)
PG_URL=postgresql://user:pass@localhost:5432/postgres

# Redis
REDIS_URL=redis://localhost:6379

# S3
S3_ENDPOINT=http://localhost:9000
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_BUCKET_NAME=fastgpt

# OneAPI
ONEAPI_URL=http://localhost:3000/v1
ONEAPI_KEY=sk-xxx

# NextAuth
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key

# Root Key(管理员密钥)
ROOT_KEY=your-root-key

Docker 部署

# Dockerfile
FROM node:20-alpine AS base
RUN npm install -g pnpm

FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
CMD ["node", "server.js"]

总结与最佳实践

前端开发规范

  1. 组件复用:通用组件放在 packages/web,页面特定组件放在 pageComponents
  2. 状态管理:全局状态用 Zustand,局部状态用 React State
  3. 数据获取:使用 React Query 管理服务端状态
  4. 类型安全:所有 API 请求和响应定义 TypeScript 类型
  5. 国际化:所有文案使用 i18n 键,不硬编码

API 开发规范

  1. 鉴权:所有 API 必须先调用鉴权函数(authXXX
  2. 错误处理:使用 try-catch 捕获错误,返回标准错误格式
  3. 响应格式:使用 jsonRes 统一响应格式
  4. 日志记录:关键操作记录日志(创建、更新、删除)
  5. 性能优化:数据库查询使用索引,避免 N+1 查询

性能优化

  1. 代码分割:使用 Next.js 的动态导入
  2. 图片优化:使用 Next.js Image 组件
  3. 缓存策略:静态资源使用 CDN,API 响应使用 SWR
  4. SSR优化:首屏使用 SSR,后续使用 CSR