docs: 添加项目开发规范文档
- 添加项目概览文档 - 添加前端开发规范 - 添加后端开发规范 - 添加数据库设计规范 - 添加UI/UX设计规范 - 添加开发指南 - 为项目提供完整的开发标准和最佳实践
This commit is contained in:
644
.promptx/frontend-coding-standards.md
Normal file
644
.promptx/frontend-coding-standards.md
Normal file
@@ -0,0 +1,644 @@
|
||||
# MixVideo 前端开发规范
|
||||
|
||||
## 技术栈规范
|
||||
|
||||
### 核心技术
|
||||
- **React 18**: 函数组件 + Hooks
|
||||
- **TypeScript 5.8**: 严格类型检查
|
||||
- **Vite 6.0**: 构建工具
|
||||
- **TailwindCSS 3.4**: 样式框架
|
||||
- **Zustand 4.4**: 状态管理
|
||||
- **React Router 6.20**: 路由管理
|
||||
|
||||
### 开发工具
|
||||
- **ESLint**: 代码检查
|
||||
- **Prettier**: 代码格式化
|
||||
- **Vitest**: 单元测试
|
||||
- **Testing Library**: 组件测试
|
||||
|
||||
## 项目结构规范
|
||||
|
||||
### 目录结构
|
||||
```
|
||||
src/
|
||||
├── components/ # 可复用组件
|
||||
│ ├── ui/ # 基础UI组件
|
||||
│ ├── business/ # 业务组件
|
||||
│ └── __tests__/ # 组件测试
|
||||
├── pages/ # 页面组件
|
||||
├── hooks/ # 自定义Hooks
|
||||
├── services/ # 业务服务层
|
||||
├── store/ # Zustand状态管理
|
||||
├── types/ # TypeScript类型定义
|
||||
├── utils/ # 工具函数
|
||||
├── styles/ # 样式文件
|
||||
│ ├── design-system.css
|
||||
│ └── animations.css
|
||||
└── tests/ # 测试文件
|
||||
├── setup.ts
|
||||
├── components/
|
||||
├── integration/
|
||||
└── e2e/
|
||||
```
|
||||
|
||||
### 文件命名规范
|
||||
- **组件文件**: PascalCase (如 `MaterialCard.tsx`)
|
||||
- **Hook文件**: camelCase (如 `useMaterialSearch.ts`)
|
||||
- **服务文件**: camelCase (如 `materialService.ts`)
|
||||
- **类型文件**: camelCase (如 `material.ts`)
|
||||
- **工具文件**: camelCase (如 `imagePathUtils.ts`)
|
||||
|
||||
## 组件开发规范
|
||||
|
||||
### 组件结构模板
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
// 类型定义
|
||||
interface ComponentProps {
|
||||
// 必需属性
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
// 可选属性
|
||||
description?: string;
|
||||
className?: string;
|
||||
|
||||
// 事件处理
|
||||
onClick?: () => void;
|
||||
onSubmit?: (data: FormData) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件描述
|
||||
* 遵循 Tauri 开发规范的组件设计原则
|
||||
*/
|
||||
export const Component: React.FC<ComponentProps> = ({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
onClick,
|
||||
onSubmit,
|
||||
}) => {
|
||||
// Hooks (按顺序)
|
||||
const [state, setState] = useState<StateType>(initialState);
|
||||
const { data, loading, error } = useCustomHook();
|
||||
|
||||
// 事件处理函数
|
||||
const handleClick = useCallback(() => {
|
||||
onClick?.();
|
||||
}, [onClick]);
|
||||
|
||||
// 副作用
|
||||
useEffect(() => {
|
||||
// 副作用逻辑
|
||||
}, [dependencies]);
|
||||
|
||||
// 条件渲染
|
||||
if (loading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage error={error} />;
|
||||
}
|
||||
|
||||
// 主渲染
|
||||
return (
|
||||
<div className={clsx('base-classes', className)}>
|
||||
<h2 className="text-lg font-semibold">{title}</h2>
|
||||
{description && (
|
||||
<p className="text-gray-600">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 默认导出
|
||||
export default Component;
|
||||
```
|
||||
|
||||
### 组件设计原则
|
||||
1. **单一职责**: 每个组件只负责一个功能
|
||||
2. **可复用性**: 通过props实现组件的灵活配置
|
||||
3. **可测试性**: 组件逻辑清晰,易于测试
|
||||
4. **性能优化**: 合理使用memo、useMemo、useCallback
|
||||
5. **无障碍性**: 支持键盘导航和屏幕阅读器
|
||||
|
||||
### Props设计规范
|
||||
```typescript
|
||||
// ✅ 好的Props设计
|
||||
interface GoodProps {
|
||||
// 必需属性在前
|
||||
id: string;
|
||||
title: string;
|
||||
|
||||
// 可选属性在后
|
||||
description?: string;
|
||||
variant?: 'primary' | 'secondary' | 'danger';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
disabled?: boolean;
|
||||
|
||||
// 事件处理器
|
||||
onClick?: (event: MouseEvent) => void;
|
||||
onSubmit?: (data: FormData) => Promise<void>;
|
||||
|
||||
// 样式相关
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
// ❌ 避免的Props设计
|
||||
interface BadProps {
|
||||
data: any; // 避免使用any
|
||||
config: object; // 避免使用object
|
||||
callback: Function; // 避免使用Function
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript 规范
|
||||
|
||||
### 类型定义规范
|
||||
```typescript
|
||||
// 基础类型定义
|
||||
export interface Material {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
type: MaterialType;
|
||||
size: number;
|
||||
duration?: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// 枚举定义
|
||||
export enum MaterialType {
|
||||
VIDEO = 'video',
|
||||
AUDIO = 'audio',
|
||||
IMAGE = 'image',
|
||||
}
|
||||
|
||||
// 联合类型
|
||||
export type Status = 'idle' | 'loading' | 'success' | 'error';
|
||||
|
||||
// 泛型接口
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
message: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
// 工具类型
|
||||
export type CreateMaterialRequest = Omit<Material, 'id' | 'createdAt' | 'updatedAt'>;
|
||||
export type UpdateMaterialRequest = Partial<Pick<Material, 'name' | 'type'>>;
|
||||
```
|
||||
|
||||
### 类型导入导出规范
|
||||
```typescript
|
||||
// 类型导入
|
||||
import type { Material, MaterialType } from '../types/material';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
// 值导入
|
||||
import { MaterialService } from '../services/materialService';
|
||||
import { useMaterialStore } from '../store/materialStore';
|
||||
|
||||
// 混合导入
|
||||
import React, { type FC, type ReactNode } from 'react';
|
||||
```
|
||||
|
||||
## 状态管理规范
|
||||
|
||||
### Zustand Store 设计
|
||||
```typescript
|
||||
import { create } from 'zustand';
|
||||
import type { Material } from '../types/material';
|
||||
|
||||
interface MaterialState {
|
||||
// 状态数据
|
||||
materials: Material[];
|
||||
selectedMaterial: Material | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// 同步操作
|
||||
setMaterials: (materials: Material[]) => void;
|
||||
setSelectedMaterial: (material: Material | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
|
||||
// 异步操作
|
||||
fetchMaterials: () => Promise<void>;
|
||||
createMaterial: (request: CreateMaterialRequest) => Promise<void>;
|
||||
updateMaterial: (id: string, request: UpdateMaterialRequest) => Promise<void>;
|
||||
deleteMaterial: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useMaterialStore = create<MaterialState>((set, get) => ({
|
||||
// 初始状态
|
||||
materials: [],
|
||||
selectedMaterial: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
// 同步操作
|
||||
setMaterials: (materials) => set({ materials }),
|
||||
setSelectedMaterial: (selectedMaterial) => set({ selectedMaterial }),
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setError: (error) => set({ error }),
|
||||
|
||||
// 异步操作
|
||||
fetchMaterials: async () => {
|
||||
try {
|
||||
set({ loading: true, error: null });
|
||||
const materials = await MaterialService.getAllMaterials();
|
||||
set({ materials, loading: false });
|
||||
} catch (error) {
|
||||
set({ error: error.message, loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
createMaterial: async (request) => {
|
||||
try {
|
||||
set({ loading: true, error: null });
|
||||
const material = await MaterialService.createMaterial(request);
|
||||
set((state) => ({
|
||||
materials: [...state.materials, material],
|
||||
loading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
set({ error: error.message, loading: false });
|
||||
}
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
### 状态管理原则
|
||||
1. **单一数据源**: 每个状态只有一个来源
|
||||
2. **不可变更新**: 使用不可变的方式更新状态
|
||||
3. **异步处理**: 统一的异步操作模式
|
||||
4. **错误处理**: 完善的错误状态管理
|
||||
|
||||
## 服务层规范
|
||||
|
||||
### 服务类设计
|
||||
```typescript
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type { Material, CreateMaterialRequest, UpdateMaterialRequest } from '../types/material';
|
||||
|
||||
/**
|
||||
* 素材管理服务
|
||||
* 遵循 Tauri 开发规范的前端服务层设计
|
||||
*/
|
||||
export class MaterialService {
|
||||
/**
|
||||
* 获取所有素材
|
||||
*/
|
||||
static async getAllMaterials(): Promise<Material[]> {
|
||||
try {
|
||||
const materials = await invoke<Material[]>('get_all_materials');
|
||||
return materials;
|
||||
} catch (error) {
|
||||
console.error('获取素材列表失败:', error);
|
||||
throw new Error(`获取素材列表失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取素材
|
||||
*/
|
||||
static async getMaterialById(id: string): Promise<Material | null> {
|
||||
try {
|
||||
const material = await invoke<Material | null>('get_material_by_id', { id });
|
||||
return material;
|
||||
} catch (error) {
|
||||
console.error('获取素材详情失败:', error);
|
||||
throw new Error(`获取素材详情失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建素材
|
||||
*/
|
||||
static async createMaterial(request: CreateMaterialRequest): Promise<Material> {
|
||||
try {
|
||||
const material = await invoke<Material>('create_material', { request });
|
||||
return material;
|
||||
} catch (error) {
|
||||
console.error('创建素材失败:', error);
|
||||
throw new Error(`创建素材失败: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 服务层原则
|
||||
1. **静态方法**: 使用静态方法避免实例化
|
||||
2. **错误处理**: 统一的错误处理和日志记录
|
||||
3. **类型安全**: 严格的类型定义和检查
|
||||
4. **文档注释**: 详细的JSDoc注释
|
||||
|
||||
## 样式规范
|
||||
|
||||
### TailwindCSS 使用规范
|
||||
```typescript
|
||||
// ✅ 推荐的样式写法
|
||||
const buttonStyles = {
|
||||
base: 'inline-flex items-center justify-center rounded-md font-medium transition-colors',
|
||||
variants: {
|
||||
primary: 'bg-primary-600 text-white hover:bg-primary-700',
|
||||
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
|
||||
danger: 'bg-red-600 text-white hover:bg-red-700',
|
||||
},
|
||||
sizes: {
|
||||
sm: 'px-3 py-1.5 text-sm',
|
||||
md: 'px-4 py-2 text-base',
|
||||
lg: 'px-6 py-3 text-lg',
|
||||
},
|
||||
};
|
||||
|
||||
// 使用clsx进行条件样式
|
||||
const className = clsx(
|
||||
buttonStyles.base,
|
||||
buttonStyles.variants[variant],
|
||||
buttonStyles.sizes[size],
|
||||
disabled && 'opacity-50 cursor-not-allowed',
|
||||
className
|
||||
);
|
||||
```
|
||||
|
||||
### CSS变量使用
|
||||
```css
|
||||
/* 使用设计系统中定义的CSS变量 */
|
||||
.custom-component {
|
||||
background-color: var(--primary-500);
|
||||
color: var(--gray-50);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义Hooks规范
|
||||
|
||||
### Hook设计模板
|
||||
```typescript
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import type { Material } from '../types/material';
|
||||
import { MaterialService } from '../services/materialService';
|
||||
|
||||
interface UseMaterialSearchOptions {
|
||||
initialQuery?: string;
|
||||
autoSearch?: boolean;
|
||||
}
|
||||
|
||||
interface UseMaterialSearchReturn {
|
||||
materials: Material[];
|
||||
query: string;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
search: (query: string) => Promise<void>;
|
||||
clearResults: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 素材搜索Hook
|
||||
* 提供素材搜索功能的封装
|
||||
*/
|
||||
export const useMaterialSearch = (
|
||||
options: UseMaterialSearchOptions = {}
|
||||
): UseMaterialSearchReturn => {
|
||||
const { initialQuery = '', autoSearch = false } = options;
|
||||
|
||||
const [materials, setMaterials] = useState<Material[]>([]);
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const search = useCallback(async (searchQuery: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setQuery(searchQuery);
|
||||
|
||||
const results = await MaterialService.searchMaterials(searchQuery);
|
||||
setMaterials(results);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '搜索失败');
|
||||
setMaterials([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearResults = useCallback(() => {
|
||||
setMaterials([]);
|
||||
setQuery('');
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoSearch && initialQuery) {
|
||||
search(initialQuery);
|
||||
}
|
||||
}, [autoSearch, initialQuery, search]);
|
||||
|
||||
return {
|
||||
materials,
|
||||
query,
|
||||
loading,
|
||||
error,
|
||||
search,
|
||||
clearResults,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## 测试规范
|
||||
|
||||
### 组件测试模板
|
||||
```typescript
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { MaterialCard } from '../MaterialCard';
|
||||
import type { Material } from '../../types/material';
|
||||
|
||||
const mockMaterial: Material = {
|
||||
id: '1',
|
||||
name: 'Test Material',
|
||||
path: '/test/path',
|
||||
type: 'video',
|
||||
size: 1024,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
describe('MaterialCard', () => {
|
||||
it('应该正确渲染素材信息', () => {
|
||||
render(<MaterialCard material={mockMaterial} />);
|
||||
|
||||
expect(screen.getByText('Test Material')).toBeInTheDocument();
|
||||
expect(screen.getByText('video')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('应该处理点击事件', async () => {
|
||||
const onClickMock = vi.fn();
|
||||
render(<MaterialCard material={mockMaterial} onClick={onClickMock} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onClickMock).toHaveBeenCalledWith(mockMaterial);
|
||||
});
|
||||
});
|
||||
|
||||
it('应该显示加载状态', () => {
|
||||
render(<MaterialCard material={mockMaterial} loading />);
|
||||
|
||||
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 测试原则
|
||||
1. **测试行为**: 测试组件的行为而不是实现细节
|
||||
2. **用户视角**: 从用户的角度编写测试
|
||||
3. **覆盖率**: 关键功能100%覆盖
|
||||
4. **可维护性**: 测试代码也要易于维护
|
||||
|
||||
## 性能优化规范
|
||||
|
||||
### React性能优化
|
||||
```typescript
|
||||
// 使用memo优化组件重渲染
|
||||
export const MaterialCard = memo<MaterialCardProps>(({ material, onClick }) => {
|
||||
// 组件实现
|
||||
});
|
||||
|
||||
// 使用useMemo优化计算
|
||||
const expensiveValue = useMemo(() => {
|
||||
return computeExpensiveValue(data);
|
||||
}, [data]);
|
||||
|
||||
// 使用useCallback优化函数引用
|
||||
const handleClick = useCallback((id: string) => {
|
||||
onClick?.(id);
|
||||
}, [onClick]);
|
||||
|
||||
// 使用lazy加载组件
|
||||
const LazyComponent = lazy(() => import('./LazyComponent'));
|
||||
```
|
||||
|
||||
### 列表优化
|
||||
```typescript
|
||||
// 使用react-window进行虚拟化
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
|
||||
const VirtualizedList: FC<{ items: Material[] }> = ({ items }) => (
|
||||
<List
|
||||
height={600}
|
||||
itemCount={items.length}
|
||||
itemSize={80}
|
||||
itemData={items}
|
||||
>
|
||||
{({ index, style, data }) => (
|
||||
<div style={style}>
|
||||
<MaterialCard material={data[index]} />
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
);
|
||||
```
|
||||
|
||||
## 错误处理规范
|
||||
|
||||
### 错误边界
|
||||
```typescript
|
||||
import React, { Component, type ReactNode } from 'react';
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<
|
||||
{ children: ReactNode },
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
constructor(props: { children: ReactNode }) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="error-fallback">
|
||||
<h2>出现了错误</h2>
|
||||
<p>{this.state.error?.message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 异步错误处理
|
||||
```typescript
|
||||
// 在组件中处理异步错误
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleAsyncOperation = async () => {
|
||||
try {
|
||||
setError(null);
|
||||
await someAsyncOperation();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 在服务层统一错误处理
|
||||
export const handleApiError = (error: unknown): never => {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`API错误: ${error.message}`);
|
||||
}
|
||||
throw new Error('未知错误');
|
||||
};
|
||||
```
|
||||
|
||||
## 代码质量规范
|
||||
|
||||
### ESLint配置
|
||||
```json
|
||||
{
|
||||
"extends": [
|
||||
"@vitejs/eslint-config-react",
|
||||
"@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": "warn"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 代码审查清单
|
||||
- [ ] 组件是否遵循单一职责原则
|
||||
- [ ] TypeScript类型是否完整准确
|
||||
- [ ] 是否有适当的错误处理
|
||||
- [ ] 是否有性能优化考虑
|
||||
- [ ] 是否有相应的测试覆盖
|
||||
- [ ] 代码是否符合团队规范
|
||||
Reference in New Issue
Block a user