# 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 = ({ id, title, description, className, onClick, onSubmit, }) => { // Hooks (按顺序) const [state, setState] = useState(initialState); const { data, loading, error } = useCustomHook(); // 事件处理函数 const handleClick = useCallback(() => { onClick?.(); }, [onClick]); // 副作用 useEffect(() => { // 副作用逻辑 }, [dependencies]); // 条件渲染 if (loading) { return ; } if (error) { return ; } // 主渲染 return (

{title}

{description && (

{description}

)}
); }; // 默认导出 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; // 样式相关 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 { data: T; message: string; success: boolean; } // 工具类型 export type CreateMaterialRequest = Omit; export type UpdateMaterialRequest = Partial>; ``` ### 类型导入导出规范 ```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; createMaterial: (request: CreateMaterialRequest) => Promise; updateMaterial: (id: string, request: UpdateMaterialRequest) => Promise; deleteMaterial: (id: string) => Promise; } export const useMaterialStore = create((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 { try { const materials = await invoke('get_all_materials'); return materials; } catch (error) { console.error('获取素材列表失败:', error); throw new Error(`获取素材列表失败: ${error}`); } } /** * 根据ID获取素材 */ static async getMaterialById(id: string): Promise { try { const material = await invoke('get_material_by_id', { id }); return material; } catch (error) { console.error('获取素材详情失败:', error); throw new Error(`获取素材详情失败: ${error}`); } } /** * 创建素材 */ static async createMaterial(request: CreateMaterialRequest): Promise { try { const material = await invoke('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; clearResults: () => void; } /** * 素材搜索Hook * 提供素材搜索功能的封装 */ export const useMaterialSearch = ( options: UseMaterialSearchOptions = {} ): UseMaterialSearchReturn => { const { initialQuery = '', autoSearch = false } = options; const [materials, setMaterials] = useState([]); const [query, setQuery] = useState(initialQuery); const [loading, setLoading] = useState(false); const [error, setError] = useState(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(); expect(screen.getByText('Test Material')).toBeInTheDocument(); expect(screen.getByText('video')).toBeInTheDocument(); }); it('应该处理点击事件', async () => { const onClickMock = vi.fn(); render(); fireEvent.click(screen.getByRole('button')); await waitFor(() => { expect(onClickMock).toHaveBeenCalledWith(mockMaterial); }); }); it('应该显示加载状态', () => { render(); expect(screen.getByTestId('loading-spinner')).toBeInTheDocument(); }); }); ``` ### 测试原则 1. **测试行为**: 测试组件的行为而不是实现细节 2. **用户视角**: 从用户的角度编写测试 3. **覆盖率**: 关键功能100%覆盖 4. **可维护性**: 测试代码也要易于维护 ## 性能优化规范 ### React性能优化 ```typescript // 使用memo优化组件重渲染 export const MaterialCard = memo(({ 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 }) => ( {({ index, style, data }) => (
)}
); ``` ## 错误处理规范 ### 错误边界 ```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 (

出现了错误

{this.state.error?.message}

); } return this.props.children; } } ``` ### 异步错误处理 ```typescript // 在组件中处理异步错误 const [error, setError] = useState(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类型是否完整准确 - [ ] 是否有适当的错误处理 - [ ] 是否有性能优化考虑 - [ ] 是否有相应的测试覆盖 - [ ] 代码是否符合团队规范