1443 lines
34 KiB
Markdown
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*
|