Files
leaudit-platform-frontend/docs/开发规范手册.md
T

1443 lines
34 KiB
Markdown

# 中国烟草AI合同及卷宗审核系统 - 开发规范手册
## 目录
1. [项目概述](#1-项目概述)
2. [技术栈规范](#2-技术栈规范)
3. [项目结构规范](#3-项目结构规范)
4. [TypeScript代码规范](#4-typescript代码规范)
5. [React组件规范](#5-react组件规范)
6. [样式规范](#6-样式规范)
7. [API调用规范](#7-api调用规范)
8. [路由开发规范](#8-路由开发规范)
9. [安全规范](#9-安全规范)
10. [Git提交规范](#10-git提交规范)
11. [环境变量规范](#11-环境变量规范)
12. [注释规范](#12-注释规范)
13. [错误处理规范](#13-错误处理规范)
14. [命名规范速查表](#14-命名规范速查表)
---
## 1. 项目概述
### 1.1 项目简介
本项目是中国烟草AI合同及卷宗审核系统,采用 Remix (React) + TypeScript 构建,提供智能文档审查、风险评估和合规检查功能。
### 1.2 核心命令
```bash
# 开发
npm run dev # 启动开发服务器 (端口 5173)
npm run typecheck # TypeScript 类型检查
npm run lint # ESLint 检查
# 构建
npm run build # 生产构建
npm run build:production:multi # 多实例生产构建
# 部署
npm start # 单实例生产启动
npm run start:pm2:multi # PM2 多实例启动
```
---
## 2. 技术栈规范
### 2.1 核心技术
| 技术 | 版本 | 用途 |
|------|------|------|
| Remix | ^2.16.2 | React 全栈框架 |
| React | ^18.2.0 | UI 库 |
| TypeScript | ^5.x | 类型系统 |
| Vite | ^5.x | 构建工具 |
| Tailwind CSS | ^3.4 | 样式框架 |
| Ant Design | ^6.0 | UI 组件库 |
| Axios | ^1.9 | HTTP 客户端 |
| Remixicon | 本地化 | 图标库 |
### 2.2 路径别名
```json
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
}
}
}
```
**导入示例**:
```typescript
import { Button } from '~/components/ui/Button'; // ✅ 正确
import { Button } from '../components/ui/Button'; // ❌ 不推荐
```
---
## 3. 项目结构规范
### 3.1 目录结构
```
app/
├── api/ # API 层 (服务端调用封装)
│ ├── axios-client.ts # Axios 核心客户端
│ ├── postgrest-client.ts # PostgREST API 封装
│ ├── login/ # 登录认证 API
│ │ ├── auth.server.ts # 会话管理
│ │ ├── oauth-client.ts # OAuth2.0 客户端
│ │ └── token-manager.server.ts # Token 管理
│ ├── contracts/ # 合同相关 API
│ ├── cross-checking/ # 交叉评查 API
│ └── [feature]/ # 其他功能 API
├── components/ # 组件目录
│ ├── ui/ # 通用 UI 组件 (自包含设计)
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.css
│ │ │ └── index.ts
│ │ ├── Card/
│ │ └── index.ts # 统一导出
│ ├── layout/ # 布局组件
│ │ ├── Layout.tsx
│ │ ├── Sidebar.tsx
│ │ └── Header.tsx
│ ├── reviews/ # 评查功能组件
│ └── [feature]/ # 功能特定组件
├── routes/ # Remix 路由 (76个)
│ ├── _index.tsx # 首页
│ ├── login.tsx # 登录页
│ ├── callback.tsx # OAuth 回调
│ ├── documents.tsx # 文档管理
│ ├── cross-checking.tsx # 交叉评查
│ └── api.*.tsx # API 路由
├── config/ # 配置文件
│ └── api-config.ts # API 端口配置
├── styles/ # 样式文件
│ ├── main.css # 主样式
│ └── components/ # 组件样式
│ ├── card.css
│ ├── sidebar.css
│ └── button.css
├── types/ # 类型定义
│ ├── document.ts # 文档相关类型
│ ├── user.ts # 用户相关类型
│ └── api.ts # API 相关类型
├── hooks/ # 自定义 Hooks
├── contexts/ # React Context
├── utils/ # 工具函数
└── root.tsx # 应用根组件
```
### 3.2 文件组织原则
| 目录 | 组织方式 | 说明 |
|------|----------|------|
| `api/` | 按功能模块 | 每个功能模块独立目录 |
| `components/` | 按组件类型 | UI 组件自包含 (tsx + css + index.ts) |
| `routes/` | 按路由 | 一个文件一个路由 |
| `types/` | 按领域 | 按业务领域分类 |
| `styles/` | 按组件 | 与组件对应 |
---
## 4. TypeScript代码规范
### 4.1 类型定义规范
```typescript
// ✅ 1. 接口命名 - PascalCase
interface DocumentInfo {
id: string;
name: string;
path: string;
status: ProcessingStatus;
createdAt: string;
}
// ✅ 2. Props 接口命名 - ComponentNameProps
interface ButtonProps {
children: React.ReactNode;
type?: 'primary' | 'default' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
className?: string;
onClick?: (e: React.MouseEvent) => void;
}
// ✅ 3. 状态类型 - 字符串字面量联合
type ProcessingStatus =
| 'Waiting'
| 'Cutting'
| 'Extractioning'
| 'Evaluationing'
| 'Processed';
type UserRole = 'common' | 'developer' | 'admin';
// ✅ 4. 函数类型
type ApiResponse<T> = {
data: T;
status: number;
message?: string;
};
type ErrorResponse = {
error: string;
status: number;
};
// ✅ 5. 枚举定义 (仅在需要组合值时使用)
enum FileType {
CONTRACT = 'contract',
LICENSE = 'license',
OTHER = 'other'
}
```
### 4.2 类型使用原则
```typescript
// ✅ 优先使用 interface
interface UserInfo {
name: string;
email: string;
}
// ✅ 需要合并时使用 type
type UserWithRole = UserInfo & { role: UserRole };
// ✅ 避免使用 any,使用 unknown
function handleData(data: unknown): void {
if (typeof data === 'string') {
console.log(data.toUpperCase());
}
}
// ✅ 使用 as const 冻结对象
const STATUS_CONFIG = {
WAITING: { label: '等待中', color: 'gray' },
SUCCESS: { label: '成功', color: 'green' },
} as const;
// ✅ 泛型约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
```
### 4.3 导入类型
```typescript
// ✅ 使用 type 导入仅类型的依赖
import type { DocumentInfo } from '~/types/document';
import type { UserRole } from '~/types/user';
// ✅ 混合导入
import { Button } from '~/components/ui/Button'; // 值导入
import type { ButtonProps } from '~/components/ui/Button'; // 类型导入
// ✅ 从同一模块导入值和类型
import { useState, useEffect } from 'react'; // 运行时
import type { Dispatch, SetStateAction } from 'react'; // 仅类型
```
---
## 5. React组件规范
### 5.1 组件定义
```typescript
// ✅ 正确 - 使用函数声明
export function Card({
children,
title,
icon,
extra,
className = ''
}: CardProps) {
return (
<div className={`card ${className}`}>
{title && (
<div className="card-header">
<div className="card-title">
{icon && <i className={`${icon} mr-2`}></i>}
<span>{title}</span>
</div>
{extra && <div className="card-extra">{extra}</div>}
</div>
)}
<div className="card-body">{children}</div>
</div>
);
}
// ❌ 错误 - 使用箭头函数
export const Card = ({ children, title }: CardProps) => { ... };
```
### 5.2 Hooks 使用顺序
```typescript
export function DocumentList({ documents }: DocumentListProps) {
// 1. State hooks (按依赖关系排序)
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [filter, setFilter] = useState<FilterType>('all');
const [isLoading, setIsLoading] = useState(false);
// 2. Ref hooks
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
// 3. Effect hooks (按依赖关系分组)
useEffect(() => {
// 数据获取
fetchDocuments();
}, []);
useEffect(() => {
// 订阅/事件监听
const handler = () => { ... };
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
// 4. 计算属性 (useMemo/useCallback)
const filteredDocuments = useMemo(() => {
return documents.filter(doc => {
if (filter === 'all') return true;
return doc.status === filter;
});
}, [documents, filter]);
const handleSelectAll = useCallback(() => {
setSelectedIds(documents.map(d => d.id));
}, [documents]);
// 5. 事件处理函数
const handleSelect = (id: string) => {
setSelectedIds(prev =>
prev.includes(id)
? prev.filter(i => i !== id)
: [...prev, id]
);
};
const handleDelete = async (id: string) => {
setIsLoading(true);
try {
await deleteDocument(id);
} finally {
setIsLoading(false);
}
};
// 6. Render
return (
<div ref={containerRef} className="document-list">
{/* ... */}
</div>
);
}
```
### 5.3 组件 Props 规范
```typescript
// ✅ Props 接口定义
interface ButtonProps {
children: React.ReactNode;
type?: ButtonType;
size?: ButtonSize;
disabled?: boolean;
loading?: boolean;
icon?: string;
className?: string;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}
// ✅ 带默认值的 props
interface CardProps {
title?: React.ReactNode;
icon?: string;
className?: string;
bodyClassName?: string;
children: React.ReactNode;
}
// ✅ 提取子组件 Props
interface TableColumn<T> {
key: keyof T | string;
title: string;
render?: (value: T[keyof T], record: T) => React.ReactNode;
width?: number | string;
}
```
### 5.4 自包含组件设计
每个 UI 组件应包含:
```
components/
└── Button/
├── Button.tsx # 组件实现
├── Button.css # 组件样式
└── index.ts # 导出
```
```typescript
// Button/index.ts
export { Button } from './Button';
export type { ButtonProps } from './Button';
```
---
## 6. 样式规范
### 6.1 样式架构
项目采用 **Tailwind CSS + 自定义 CSS** 混合模式:
```css
/* 1. Tailwind 工具类 (优先使用) */
<div className="flex items-center justify-between p-4 mb-6">
/* 2. 设计系统变量 */
<div className="text-[--color-primary]">
/* 3. 自定义 BEM 类 */
<div className="card">
<div className="card-header"></div>
<div className="card-body"></div>
</div>
```
### 6.2 设计系统变量
```css
/* app/root.tsx 或 main.css */
:root {
/* 主色调 */
--color-primary: #00684a;
--color-primary-hover: #005a3f;
--color-primary-light: rgba(0, 104, 74, 0.1);
/* 状态色 */
--color-success: #52c41a;
--color-warning: #faad14;
--color-error: #f5222d;
/* 中性色 */
--color-gray-50: #f8f9fa;
--color-gray-100: #f1f3f5;
--color-gray-200: #e9ecef;
--color-gray-300: #dee2e6;
--color-gray-400: #ced4da;
--color-gray-500: #adb5bd;
--color-gray-600: #868e96;
--color-gray-700: #495057;
--color-gray-800: #343a40;
--color-gray-900: #212529;
}
```
### 6.3 BEM 命名规范
```css
/* 组件块 */
.card { }
.sidebar { }
.modal { }
/* 元素 */
.card-header { }
.card-title { }
.card-body { }
.card-footer { }
.sidebar-menu-item { }
.sidebar-menu-item.active { }
/* 修饰符 */
.card--compact { }
.button--primary { }
.button--disabled { }
```
### 6.4 Tailwind 常用配置
```css
/* 间距 */
p-4 = 16px, p-5 = 20px, p-6 = 24px
mb-4 = 16px, mb-6 = 24px
/* 圆角 */
rounded = 4px, rounded-md = 6px, rounded-lg = 8px
/* 阴影 */
shadow-sm, shadow-md, shadow-lg
/* 过渡 */
transition-all duration-200 ease-in-out
```
### 6.5 RemixIcon 图标使用
```tsx
// 基本使用
<i className="ri-home-line"></i>
<i className="ri-file-list-3-line"></i>
<i className="ri-user-line"></i>
// 尺寸控制
<i className="ri-home-line ri-lg"></i>
<i className="ri-home-line ri-xl"></i>
// 结合样式
<i className="ri-error-warning-line text-red-500"></i>
<i className="ri-check-line text-green-600"></i>
// 在按钮中使用
<Button icon="ri-add-line"></Button>
```
**⚠️ 重要 - CSS 隔离时的图标兼容**:
```css
/* 当使用 CSS 隔离时,必须添加图标例外规则 */
.my-isolated-container * {
font-family: inherit !important;
}
.my-isolated-container [class^="ri-"],
.my-isolated-container [class*=" ri-"],
.my-isolated-container i[class^="ri-"],
.my-isolated-container i[class*=" ri-"] {
font-family: 'remixicon' !important;
font-style: normal !important;
font-weight: normal !important;
line-height: 1 !important;
}
```
---
## 7. API调用规范
### 7.1 API 分层架构
```
┌─────────────────────────────────────────────┐
│ 路由层 (routes/*.tsx) │
│ loader / action 函数 │
└────────────────────┬────────────────────────┘
┌────────────────────▼────────────────────────┐
│ 业务 API 层 (app/api/[feature]/) │
│ 封装业务逻辑的 API 函数 │
└────────────────────┬────────────────────────┘
┌────────────────────▼────────────────────────┐
│ PostgREST 客户端 (postgrest-client.ts) │
│ 处理 PostgREST 特定参数 │
└────────────────────┬────────────────────────┘
┌────────────────────▼────────────────────────┐
│ Axios 核心 (axios-client.ts) │
│ 请求拦截、响应拦截、JWT 处理 │
└─────────────────────────────────────────────┘
```
### 7.2 Axios 客户端使用
```typescript
// app/api/axios-client.ts - 已配置拦截器
import { get, post, put, del } from '~/api/axios-client';
// GET 请求
const data = await get<UserInfo>('/admin/users/1');
// POST 请求
const result = await post<ApiResponse>('/admin/documents', {
name: '合同.pdf',
type: 'contract'
});
// PUT 请求
const updated = await put<Document>('/admin/documents/123', {
name: '新名称.pdf'
});
// DELETE 请求
await del('/admin/documents/123');
```
### 7.3 PostgREST 客户端使用
```typescript
// app/api/postgrest-client.ts
import {
postgrestGet,
postgrestPost,
postgrestPatch,
postgrestDelete
} from '~/api/postgrest-client';
// 查询参数
const result = await postgrestGet<Document[]>('/documents', {
select: 'id,name,status',
eq: { status: 'Processed' },
order: 'created_at.desc',
limit: 10
});
// 插入数据
await postgrestPost('/documents', {
name: '新文档',
status: 'Waiting'
});
// 更新数据
await postgrestPatch('/documents', {
id: 123,
name: '更新后的名称'
});
// 删除数据
await postgrestDelete('/documents', 123);
```
### 7.4 业务 API 封装模式
```typescript
// app/api/contracts/documents.ts
import { postgrestGet, postgrestPost } from '~/api/postgrest-client';
import type { Document, DocumentFilters } from '~/types/document';
/**
* 获取文档列表
*/
export async function getDocuments(
filters?: DocumentFilters
): Promise<Document[]> {
const params = {
select: 'id,name,path,status,created_at',
order: 'created_at.desc',
limit: filters?.limit ?? 20,
offset: filters?.offset ?? 0,
...(filters?.status && { eq: { status: filters.status } })
};
const result = await postgrestGet<Document[]>('/documents', params);
if ('error' in result) {
throw new Error(result.error);
}
return result.data;
}
/**
* 创建文档
*/
export async function createDocument(
data: Omit<Document, 'id' | 'created_at'>
): Promise<Document> {
const result = await postgrestPost<Document>('/documents', data);
if ('error' in result) {
throw new Error(result.error);
}
return result.data;
}
```
### 7.5 Loader 中使用 API
```typescript
// routes/documents.tsx
import { type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getDocuments } from "~/api/contracts/documents";
import { getUserSession } from "~/api/login/auth.server";
export async function loader({ request }: LoaderFunctionArgs) {
// 1. 获取用户会话
const { userInfo, frontendJWT } = await getUserSession(request);
// 2. 解析查询参数
const url = new URL(request.url);
const status = url.searchParams.get('status') ?? undefined;
const page = parseInt(url.searchParams.get('page') ?? '1', 10);
// 3. 获取数据
const documents = await getDocuments({
status,
limit: 20,
offset: (page - 1) * 20
});
// 4. 返回数据
return Response.json({
documents,
userInfo,
pagination: {
page,
total: documents.length
}
});
}
export default function DocumentsPage() {
const { documents, userInfo } = useLoaderData<typeof loader>();
return (
<Layout userInfo={userInfo}>
<div className="documents-page">
{/* ... */}
</div>
</Layout>
);
}
```
---
## 8. 路由开发规范
### 8.1 路由文件命名
| 类型 | 命名格式 | 示例 |
|------|----------|------|
| 页面路由 | kebab-case | `documents.tsx`, `user-profile.tsx` |
| 嵌套路由 | `_` 前缀 | `documents_.list.tsx`, `documents_.detail.$id.tsx` |
| 动态路由 | `$param` | `reviews.$id.tsx`, `contract-template.detail.$id.tsx` |
| API 路由 | `api.` 前缀 | `api.documents.tsx`, `api.users.$id.tsx` |
### 8.2 标准路由结构
```typescript
// routes/documents.tsx
import {
type MetaFunction,
type LoaderFunctionArgs,
type ActionFunctionArgs
} from "@remix-run/node";
import {
useLoaderData,
useFetcher,
useNavigation
} from "@remix-run/react";
// ============ 1. Meta 配置 ============
export const meta: MetaFunction = () => [
{ title: "文档管理 - 合同审核系统" },
{ name: "description", content: "文档管理和评查功能" }
];
// ============ 2. Links 配置 ============
import styles from '~/styles/pages/documents.css?url';
export function links() {
return [
{ rel: "stylesheet", href: styles }
];
}
// ============ 3. Handle 配置 ============
export const handle = {
hideBreadcrumb: false, // 显示面包屑
title: "文档管理"
};
// ============ 4. Loader 函数 ============
export async function loader({ request }: LoaderFunctionArgs) {
// 获取用户会话
const { getUserSession } = await import("~/api/login/auth.server");
const { userInfo, frontendJWT } = await getUserSession(request);
// 获取数据
const documents = await getDocuments(request);
return Response.json({
documents,
userInfo,
frontendJWT
});
}
// ============ 5. Action 函数 ============
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
switch (intent) {
case "delete":
const id = formData.get("id") as string;
await deleteDocument(id);
return Response.json({ success: true, action: "delete" });
case "batch-delete":
const ids = formData.getAll("ids") as string[];
await batchDeleteDocuments(ids);
return Response.json({ success: true, action: "batch-delete" });
default:
return Response.json({ error: "未知操作" }, { status: 400 });
}
}
// ============ 6. 组件实现 ============
export default function DocumentsPage() {
const { documents, userInfo } = useLoaderData<typeof loader>();
const navigation = useNavigation();
const fetcher = useFetcher();
const isSubmitting = navigation.state === "submitting";
return (
<Layout userInfo={userInfo}>
<div className="page-container">
<PageHeader
title="文档管理"
actions={<Button icon="ri-add-line"></Button>}
/>
<DocumentList documents={documents} />
</div>
</Layout>
);
}
// ============ 7. ErrorBoundary ============
import { useRouteError, isRouteErrorResponse } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div className="error-page">
<h1>{error.status} - {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
return (
<div className="error-page">
<h1></h1>
<p>{error instanceof Error ? error.message : "未知错误"}</p>
</div>
);
}
```
### 8.3 嵌套路由布局
```
routes/
├── documents.tsx # 父路由 - 布局
├── documents.list.tsx # 列表页面 (使用 documents.tsx 布局)
├── documents.create.tsx # 创建页面 (使用 documents.tsx 布局)
└── documents_.detail.$id.tsx # 详情页面 (不使用父布局,下划线跳过)
```
```typescript
// routes/documents.tsx - 布局组件
export default function DocumentsLayout() {
const { userInfo } = useLoaderData<typeof loader>();
return (
<Layout userInfo={userInfo}>
<div className="documents-layout">
<aside className="documents-sidebar">
{/* 侧边栏导航 */}
</aside>
<main className="documents-main">
<Outlet /> {/* 子路由内容 */}
</main>
</div>
</Layout>
);
}
```
### 8.4 API 路由
```typescript
// routes/api.documents.tsx
import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
import { getDocuments, createDocument } from "~/api/contracts/documents";
// GET - 获取列表
export async function loader({ request }: LoaderFunctionArgs) {
const documents = await getDocuments();
return Response.json({ data: documents });
}
// POST - 创建
export async function action({ request }: ActionFunctionArgs) {
if (request.method !== "POST") {
return Response.json({ error: "方法不允许" }, { status: 405 });
}
const data = await request.json();
const document = await createDocument(data);
return Response.json({ data: document }, { status: 201 });
}
```
---
## 9. 安全规范
### 9.1 敏感信息处理
| 信息类型 | 处理方式 | 错误做法 |
|----------|----------|----------|
| `JWT_SECRET` | 环境变量,绝对不提交 | 硬编码在代码中 |
| `OAUTH_CLIENT_SECRET` | 服务端环境变量 | 使用 `NEXT_PUBLIC_` 前缀 |
| API 密钥 | 服务端配置 | 暴露到客户端 |
| 用户密码 | 不存储,MD5 传输 | 明文传输或存储 |
### 9.2 环境变量命名
```bash
# ✅ 客户端安全变量 (可被客户端代码访问)
NEXT_PUBLIC_API_BASE_URL=http://10.79.97.17:8000
NEXT_PUBLIC_DOCUMENT_URL=http://10.76.244.156:9000/docauditai/
# ❌ 服务端专用变量 (不可被客户端访问)
JWT_SECRET=your-secret-key-here # ❌ NEXT_PUBLIC_JWT_SECRET
OAUTH_CLIENT_SECRET=your-client-secret # ❌ NEXT_PUBLIC_OAUTH_CLIENT_SECRET
```
### 9.3 认证白名单
```typescript
// app/api/login/auth.server.ts
// 不需要认证的路径
const PUBLIC_PATHS = [
'/login',
'/callback',
'/oauth/authorize'
];
// 401 错误容忍的路径 (不触发登出)
const ERROR_TOLERANT_PATHS = [
'/admin/statistics/top-error-points',
'/admin/statistics/top-risk-users'
];
```
### 9.4 Cookie 安全配置
```typescript
export const sessionStorage = createCookieSessionStorage({
cookie: {
name: "__lgsession",
httpOnly: true, // 防止 XSS 攻击
path: "/",
sameSite: "lax", // CSRF 保护
secrets: [process.env.JWT_SECRET ?? "default-secret"],
maxAge: 60 * 60 * 8, // 8 小时
secure: process.env.NODE_ENV === "production"
}
});
```
### 9.5 文件路径安全
```typescript
// 防止路径遍历攻击
import path from "path";
function safeFilePath(userPath: string): string {
const normalized = path.normalize(userPath);
const baseDir = "/safe/uploads/directory";
// 确保路径在安全目录内
if (!normalized.startsWith(baseDir)) {
throw new Error("非法文件路径");
}
return normalized;
}
```
---
## 10. Git提交规范
### 10.1 提交信息格式
```
<type>(<scope>): <subject>
<body>
<footer>
```
### 10.2 Type 类型
| Type | 说明 |
|------|------|
| `feat` | 新功能 |
| `fix` | Bug 修复 |
| `docs` | 文档更新 |
| `style` | 代码格式(不影响功能) |
| `refactor` | 重构(不是新功能或修复) |
| `perf` | 性能优化 |
| `test` | 测试相关 |
| `chore` | 构建/工具变更 |
### 10.3 Scope 范围
| Scope | 说明 |
|-------|------|
| `api` | API 层 |
| `ui` | UI 组件 |
| `auth` | 认证相关 |
| `router` | 路由 |
| `config` | 配置 |
| `docs` | 文档 |
| `deps` | 依赖更新 |
### 10.4 提交示例
```bash
# 新功能
git commit -m "feat(api): 添加文档批量删除接口"
# Bug 修复
git commit -m "fix(ui): 修复 Button 组件在 Safari 的样式问题"
# 重构
git commit -m "refactor(auth): 重构 Token 刷新逻辑"
# 文档更新
git commit -m "docs: 更新 API 文档中的接口说明"
# 多项更改
git commit -m "feat(documents): 添加文档筛选和排序功能
- 添加状态筛选功能
- 添加日期范围筛选
- 优化排序逻辑
- 更新单元测试"
# 关闭 Issue
git commit -m "fix(auth): 修复 Token 过期后无法自动登出的问题
Closes #123"
```
### 10.5 分支命名
```
main # 主分支
develop # 开发分支
feature/doc-viewer # 功能分支
fix/token-refresh # 修复分支
hotfix/critical-bug # 紧急修复分支
release/v1.2.0 # 发布分支
```
---
## 11. 环境变量规范
### 11.1 必需的环境变量
```bash
# JWT 配置
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
# OAuth2.0 配置
OAUTH_CLIENT_SECRET=your-oauth-client-secret
# API 配置 (可选,优先使用端口配置)
NEXT_PUBLIC_API_BASE_URL=http://10.79.97.17:8000
NEXT_PUBLIC_DOCUMENT_URL=http://10.76.244.156:9000/docauditai/
NEXT_PUBLIC_UPLOAD_URL=http://10.79.97.17:8000/admin/documents
```
### 11.2 PM2 多实例配置
```javascript
// ecosystem.config.cjs
module.exports = {
apps: [
{
name: 'docreview-meizhou',
script: 'npm',
args: 'start',
env: {
NODE_ENV: 'production',
PORT: 51703,
CLIENT_ID: 'meizhou'
}
},
{
name: 'docreview-yunfu',
script: 'npm',
args: 'start',
env: {
NODE_ENV: 'production',
PORT: 51704,
CLIENT_ID: 'yunfu'
}
}
]
};
```
### 11.3 端口配置映射
```typescript
// app/config/api-config.ts
const PORT_CONFIGS = {
'51703': { // 梅州
apiBaseUrl: 'http://10.79.97.17:8000',
documentUrl: 'http://10.76.244.156:9000/docauditai/'
},
'51704': { // 云浮
apiBaseUrl: 'http://10.79.97.18:8000',
documentUrl: 'http://10.76.244.157:9000/docauditai/'
}
// ...
};
```
---
## 12. 注释规范
### 12.1 JSDoc 注释
```typescript
/**
* 文件描述注释
* @description 该模块提供文档相关的 API 接口封装
* @author 开发团队
*/
/**
* 获取文档详情
* @param id - 文档 ID
* @param request - 请求对象
* @returns 文档详情对象
* @throws {ApiError} 当文档不存在时抛出错误
*/
async function getDocumentById(id: string, request: Request): Promise<Document> {
// ...
}
/**
* 文档状态类型
* @typedef {'Waiting' | 'Cutting' | 'Extractioning' | 'Evaluationing' | 'Processed'} ProcessingStatus
*/
/**
* 用户角色类型
* @enum {string}
*/
enum UserRole {
COMMON = 'common',
DEVELOPER = 'developer',
ADMIN = 'admin'
}
```
### 12.2 代码内注释
```typescript
// ✅ 使用单行注释说明 WHY,不说明 WHAT
// 因为 API 需要 1-based 页码
const page = parseInt(url.searchParams.get('page') ?? '1', 10);
// 等待 DOM 完全加载后再初始化编辑器
useEffect(() => {
initEditor();
}, []);
// 防止重复提交
if (isSubmitting) return;
// ❌ 避免无意义的注释
// 设置状态为 true
setIsLoading(true);
// 获取用户信息
const user = await getUser();
```
### 12.3 TODO 注释
```typescript
// TODO: 优化性能 - 考虑使用虚拟滚动
// TODO(FEATURE): 添加批量导出功能
// TODO(BUG): 修复 Safari 上的日期选择器问题 (#123)
// FIXME: 临时解决方案,需要重构
// HACK: 处理旧版 API 兼容性
```
---
## 13. 错误处理规范
### 13.1 API 错误响应格式
```typescript
// 成功响应
{
data: T,
status: 200
}
// 错误响应
{
error: string,
status: number
}
```
### 13.2 错误处理模式
```typescript
// 1. Loader 中的错误处理
export async function loader({ request }: LoaderFunctionArgs) {
try {
const data = await fetchData();
return Response.json({ data });
} catch (error) {
console.error('数据获取失败:', error);
return Response.json(
{ error: '获取数据失败,请稍后重试' },
{ status: 500 }
);
}
}
// 2. Action 中的错误处理
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
try {
const result = await submitData(formData);
return Response.json({ success: true, data: result });
} catch (error) {
if (error instanceof ValidationError) {
return Response.json(
{ error: error.message, fields: error.fields },
{ status: 400 }
);
}
return Response.json(
{ error: '提交失败,请稍后重试' },
{ status: 500 }
);
}
}
// 3. 前端错误处理
const fetcher = useFetcher();
useEffect(() => {
if (fetcher.data?.error) {
toast.error(fetcher.data.error);
}
}, [fetcher.data]);
```
### 13.3 错误边界组件
```typescript
// routes/reviews.tsx
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div className="error-container">
<i className="ri-error-warning-line text-4xl text-red-500"></i>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
<Button onClick={() => window.location.href = '/'}>
</Button>
</div>
);
}
return (
<div className="error-container">
<h1></h1>
<p>{error instanceof Error ? error.message : '未知错误'}</p>
<Button onClick={() => window.location.href = '/'}>
</Button>
</div>
);
}
```
### 13.4 自定义错误类
```typescript
// app/utils/errors.ts
export class ApiError extends Error {
status: number;
code?: string;
constructor(message: string, status: number = 500, code?: string) {
super(message);
this.name = 'ApiError';
this.status = status;
this.code = code;
}
}
export class ValidationError extends ApiError {
fields: Record<string, string>;
constructor(message: string, fields: Record<string, string> = {}) {
super(message, 400, 'VALIDATION_ERROR');
this.name = 'ValidationError';
this.fields = fields;
}
}
export class NotFoundError extends ApiError {
constructor(resource: string) {
super(`${resource} 不存在`, 404, 'NOT_FOUND');
this.name = 'NotFoundError';
}
}
```
---
## 14. 命名规范速查表
### 14.1 文件命名
| 类型 | 规范 | 示例 |
|------|------|------|
| 组件文件 | PascalCase.tsx | `Button.tsx`, `Card.tsx` |
| 样式文件 | kebab-case.css | `button.css`, `card-header.css` |
| 路由文件 | kebab-case.tsx | `documents.tsx`, `user-profile.tsx` |
| 工具文件 | camelCase.ts | `utils.ts`, `dateHelper.ts` |
| 类型文件 | kebab-case.ts | `document-types.ts` |
| 测试文件 | ComponentName.test.tsx | `Button.test.tsx` |
### 14.2 变量命名
| 类型 | 规范 | 示例 |
|------|------|------|
| 普通变量 | camelCase | `userName`, `isLoading` |
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRIES`, `API_BASE_URL` |
| 枚举值 | UPPER_SNAKE_CASE | `FileType.CONTRACT` |
| 组件状态 | camelCase + 前缀 | `isLoading`, `hasError`, `isVisible` |
| 数组 | 复数名词或加 List/Suffix | `users`, `documentList`, `items` |
### 14.3 React 命名
| 类型 | 规范 | 示例 |
|------|------|------|
| 组件名 | PascalCase | `function Button()` |
| Props 接口 | ComponentNameProps | `ButtonProps` |
| 事件处理 | handle + 动作 | `handleClick`, `handleSubmit` |
| 布尔属性 | is/has/should + 描述 | `isDisabled`, `hasChildren` |
### 14.4 CSS 类命名
| 类型 | 规范 | 示例 |
|------|------|------|
| 组件块 | BEM | `.card`, `.sidebar` |
| 元素 | BEM | `.card-header`, `.card-title` |
| 状态 | BEM 修饰符 | `.card--compact`, `.button--disabled` |
| 工具类 | Tailwind | `.flex`, `.text-center` |
| 图标类 | RemixIcon | `.ri-home-line` |
---
## 附录
### A. 开发检查清单
**新组件开发**:
- [ ] TypeScript 接口定义完整
- [ ] Props 定义包含必要的可选属性
- [ ] 使用函数声明而非箭头函数
- [ ] Hooks 顺序正确
- [ ] 样式符合设计系统
- [ ] 组件自包含 (tsx + css + index.ts)
- [ ] 导出类型定义
**新页面开发**:
- [ ] Loader 函数完整
- [ ] Action 函数处理所有意图
- [ ] Meta 配置完整
- [ ] Links 加载必要样式
- [ ] ErrorBoundary 处理错误
- [ ] 响应式设计适配
- [ ] 权限检查
**API 调用**:
- [ ] 使用 axios-client 或 postgrest-client
- [ ] 错误处理完整
- [ ] Loading 状态处理
- [ ] 类型定义正确
**代码提交前**:
- [ ] ESLint 检查通过
- [ ] TypeScript 编译无错误
- [ ] 代码格式规范
- [ ] 注释完整
- [ ] 无敏感信息泄露
- [ ] Git 提交信息规范
### B. 参考资源
- [Remix 官方文档](https://remix.run/docs)
- [TypeScript 官方文档](https://www.typescriptlang.org/docs)
- [Tailwind CSS 文档](https://tailwindcss.com/docs)
- [Ant Design 组件库](https://ant.design/components/overview)
- [RemixIcon 图标库](https://remixicon.com/)
---
*本文档最后更新: 2026-03-18*