# 开发指南 本文档为开发者提供详细的项目架构说明、开发流程和最佳实践。 ## 目录 - [项目架构](#项目架构) - [技术栈详解](#技术栈详解) - [开发环境设置](#开发环境设置) - [代码规范](#代码规范) - [组件开发](#组件开发) - [状态管理](#状态管理) - [API 集成](#api-集成) - [测试策略](#测试策略) - [性能优化](#性能优化) - [调试技巧](#调试技巧) - [贡献流程](#贡献流程) ## 项目架构 ### 整体架构 ``` 三算命平台 ├── 前端应用 (React + TypeScript) │ ├── 用户界面层 │ ├── 业务逻辑层 │ ├── 数据访问层 │ └── 工具函数层 ├── 后端服务 (Supabase) │ ├── 数据库 (PostgreSQL) │ ├── 认证服务 │ ├── Edge Functions │ └── 实时订阅 └── 部署平台 (Vercel/Netlify) ├── CDN 加速 ├── 自动部署 └── 环境管理 ``` ### 前端架构 ``` src/ ├── components/ # 组件层 │ ├── ui/ # 基础UI组件 │ │ ├── Button.tsx │ │ ├── Card.tsx │ │ ├── Input.tsx │ │ └── Select.tsx │ ├── Layout.tsx # 布局组件 │ ├── AnalysisResultDisplay.tsx # 业务组件 │ └── ErrorBoundary.tsx # 错误边界 ├── pages/ # 页面层 │ ├── HomePage.tsx # 首页 │ ├── AnalysisPage.tsx # 分析页面 │ ├── HistoryPage.tsx # 历史记录 │ └── ProfilePage.tsx # 用户资料 ├── contexts/ # 状态管理层 │ └── AuthContext.tsx # 认证上下文 ├── hooks/ # 自定义Hook层 │ └── use-mobile.tsx # 移动端检测 ├── lib/ # 工具函数层 │ ├── supabase.ts # Supabase客户端 │ └── utils.ts # 通用工具 ├── types/ # 类型定义层 │ └── index.ts # TypeScript类型 └── data/ # 静态数据层 ``` ### 数据流架构 ``` 用户交互 → 组件状态 → Context/Hook → API调用 → Supabase → 数据库 ↓ ↓ ↓ ↓ ↓ ↓ UI更新 ← 状态更新 ← 数据处理 ← 响应处理 ← Edge Function ← 查询结果 ``` ## 技术栈详解 ### 前端核心技术 #### React 18.3.1 - **并发特性**: 使用 Suspense 和 lazy loading - **Hooks**: 优先使用函数组件和 Hooks - **错误边界**: 实现全局错误处理 ```typescript // 示例:使用 Suspense 进行代码分割 import { Suspense, lazy } from 'react' const AnalysisPage = lazy(() => import('./pages/AnalysisPage')) function App() { return ( 加载中...}> ) } ``` #### TypeScript - **严格模式**: 启用所有严格类型检查 - **类型定义**: 为所有 API 响应定义类型 - **泛型使用**: 提高代码复用性 ```typescript // 示例:API 响应类型定义 interface BaziAnalysisResult { bazi: { year: string month: string day: string hour: string } wuxing: { wood: number fire: number earth: number metal: number water: number } analysis: { character: string career: string wealth: string health: string relationships: string } } ``` #### Tailwind CSS - **实用优先**: 使用原子化 CSS 类 - **响应式设计**: 移动端优先的设计方法 - **自定义主题**: 中国风配色和字体 ```typescript // tailwind.config.js 自定义配置 module.exports = { theme: { extend: { colors: { 'chinese-red': '#DC143C', 'chinese-gold': '#FFD700', 'chinese-black': '#2C2C2C' }, fontFamily: { 'chinese': ['Noto Sans SC', 'sans-serif'] } } } } ``` ### 后端服务架构 #### Supabase - **数据库**: PostgreSQL 关系型数据库 - **认证**: JWT 基础的用户认证 - **实时**: WebSocket 实时数据同步 - **Edge Functions**: Deno 运行时的服务端函数 ```typescript // Supabase 客户端配置 import { createClient } from '@supabase/supabase-js' const supabaseUrl = import.meta.env.VITE_SUPABASE_URL const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY export const supabase = createClient(supabaseUrl, supabaseKey, { auth: { autoRefreshToken: true, persistSession: true, detectSessionInUrl: true } }) ``` ## 开发环境设置 ### 1. 环境要求 ```bash # 检查 Node.js 版本 node --version # >= 18.0.0 # 检查 pnpm 版本 pnpm --version # >= 8.0.0 # 检查 Git 版本 git --version # >= 2.0.0 ``` ### 2. 项目初始化 ```bash # 克隆项目 git clone https://github.com/patdelphi/suanming.git cd suanming # 安装依赖 pnpm install # 复制环境变量模板 cp .env.example .env.local # 编辑环境变量 vim .env.local ``` ### 3. 开发服务器 ```bash # 启动开发服务器 pnpm dev # 启动并打开浏览器 pnpm dev --open # 指定端口 pnpm dev --port 3000 ``` ### 4. 开发工具配置 #### VS Code 推荐扩展 ```json // .vscode/extensions.json { "recommendations": [ "bradlc.vscode-tailwindcss", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "ms-vscode.vscode-typescript-next", "formulahendry.auto-rename-tag", "christian-kohler.path-intellisense" ] } ``` #### VS Code 设置 ```json // .vscode/settings.json { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, "typescript.preferences.importModuleSpecifier": "relative", "tailwindCSS.experimental.classRegex": [ ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"], ["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] ] } ``` ## 代码规范 ### ESLint 配置 ```javascript // eslint.config.js import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' export default tseslint.config( { ignores: ['dist'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], '@typescript-eslint/no-unused-vars': 'warn', '@typescript-eslint/no-explicit-any': 'warn', }, }, ) ``` ### 命名规范 ```typescript // 组件命名:PascalCase const AnalysisResultDisplay: React.FC = () => {} // Hook 命名:camelCase,以 use 开头 const useAuth = () => {} // 常量命名:SCREAMING_SNAKE_CASE const API_BASE_URL = 'https://api.example.com' // 类型命名:PascalCase,接口以 I 开头(可选) interface UserProfile { id: string name: string } // 枚举命名:PascalCase enum AnalysisType { BAZI = 'bazi', ZIWEI = 'ziwei', YIJING = 'yijing' } ``` ### 文件组织规范 ```typescript // 导入顺序 // 1. React 相关 import React, { useState, useEffect } from 'react' // 2. 第三方库 import { useNavigate } from 'react-router-dom' import { toast } from 'sonner' // 3. 内部组件 import { Button } from '../ui/Button' import { Card } from '../ui/Card' // 4. 内部工具 import { supabase } from '../../lib/supabase' import { cn } from '../../lib/utils' // 5. 类型定义 import type { AnalysisResult } from '../../types' ``` ## 组件开发 ### 组件结构模板 ```typescript // components/ExampleComponent.tsx import React from 'react' import { cn } from '../../lib/utils' // 组件属性接口 interface ExampleComponentProps { title: string description?: string variant?: 'default' | 'primary' | 'secondary' className?: string children?: React.ReactNode onClick?: () => void } // 组件实现 const ExampleComponent: React.FC = ({ title, description, variant = 'default', className, children, onClick }) => { return ( {title} {description && {description}} {children} ) } export default ExampleComponent ``` ### UI 组件开发 使用 `class-variance-authority` 创建可变样式组件: ```typescript // components/ui/Button.tsx import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '../../lib/utils' const buttonVariants = cva( 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background', { variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: 'border border-input hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'underline-offset-4 hover:underline text-primary' }, size: { default: 'h-10 py-2 px-4', sm: 'h-9 px-3 rounded-md', lg: 'h-11 px-8 rounded-md' } }, defaultVariants: { variant: 'default', size: 'default' } } ) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { return ( ) } ) Button.displayName = 'Button' export { Button, buttonVariants } ``` ## 状态管理 ### Context 模式 ```typescript // contexts/AuthContext.tsx import React, { createContext, useContext, useEffect, useState } from 'react' import { User, Session } from '@supabase/supabase-js' import { supabase } from '../lib/supabase' interface AuthContextType { user: User | null session: Session | null loading: boolean signIn: (email: string, password: string) => Promise signUp: (email: string, password: string) => Promise signOut: () => Promise } const AuthContext = createContext(undefined) export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [user, setUser] = useState(null) const [session, setSession] = useState(null) const [loading, setLoading] = useState(true) useEffect(() => { // 获取初始会话 supabase.auth.getSession().then(({ data: { session } }) => { setSession(session) setUser(session?.user ?? null) setLoading(false) }) // 监听认证状态变化 const { data: { subscription } } = supabase.auth.onAuthStateChange( async (event, session) => { setSession(session) setUser(session?.user ?? null) setLoading(false) } ) return () => subscription.unsubscribe() }, []) const signIn = async (email: string, password: string) => { const { error } = await supabase.auth.signInWithPassword({ email, password }) if (error) throw error } const signUp = async (email: string, password: string) => { const { error } = await supabase.auth.signUp({ email, password }) if (error) throw error } const signOut = async () => { const { error } = await supabase.auth.signOut() if (error) throw error } const value = { user, session, loading, signIn, signUp, signOut } return ( {children} ) } export const useAuth = () => { const context = useContext(AuthContext) if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider') } return context } ``` ### 自定义 Hooks ```typescript // hooks/useAnalysis.ts import { useState, useCallback } from 'react' import { supabase } from '../lib/supabase' import { toast } from 'sonner' interface AnalysisParams { type: 'bazi' | 'ziwei' | 'yijing' data: any } export const useAnalysis = () => { const [loading, setLoading] = useState(false) const [result, setResult] = useState(null) const [error, setError] = useState(null) const analyze = useCallback(async ({ type, data }: AnalysisParams) => { setLoading(true) setError(null) try { const { data: result, error } = await supabase.functions.invoke( `${type}-analyzer`, { body: data } ) if (error) throw error setResult(result) toast.success('分析完成') } catch (err) { const message = err instanceof Error ? err.message : '分析失败' setError(message) toast.error(message) } finally { setLoading(false) } }, []) return { analyze, loading, result, error } } ``` ## API 集成 ### Supabase 客户端封装 ```typescript // lib/api.ts import { supabase } from './supabase' import type { AnalysisResult, UserProfile } from '../types' export class ApiClient { // 用户相关 static async getUserProfile(userId: string): Promise { const { data, error } = await supabase .from('user_profiles') .select('*') .eq('id', userId) .single() if (error) throw error return data } static async updateUserProfile(userId: string, profile: Partial) { const { data, error } = await supabase .from('user_profiles') .update(profile) .eq('id', userId) .select() .single() if (error) throw error return data } // 分析相关 static async getAnalysisHistory(userId: string, type?: string) { let query = supabase .from('numerology_readings') .select('*') .eq('user_id', userId) .order('created_at', { ascending: false }) if (type) { query = query.eq('reading_type', type) } const { data, error } = await query if (error) throw error return data } static async performAnalysis(type: string, data: any): Promise { const { data: result, error } = await supabase.functions.invoke( `${type}-analyzer`, { body: data } ) if (error) throw error return result } // 实时订阅 static subscribeToAnalysisUpdates(userId: string, callback: (payload: any) => void) { return supabase .channel('analysis-updates') .on( 'postgres_changes', { event: '*', schema: 'public', table: 'numerology_readings', filter: `user_id=eq.${userId}` }, callback ) .subscribe() } } ``` ### 错误处理 ```typescript // lib/error-handler.ts import { toast } from 'sonner' export class ApiError extends Error { constructor( message: string, public code?: string, public status?: number ) { super(message) this.name = 'ApiError' } } export const handleApiError = (error: unknown) => { console.error('API Error:', error) if (error instanceof ApiError) { toast.error(error.message) return error } if (error instanceof Error) { toast.error(error.message) return new ApiError(error.message) } const fallbackError = new ApiError('未知错误') toast.error(fallbackError.message) return fallbackError } // 使用示例 try { await ApiClient.performAnalysis('bazi', data) } catch (error) { handleApiError(error) } ``` ## 测试策略 ### 单元测试 ```typescript // __tests__/utils.test.ts import { describe, it, expect } from 'vitest' import { cn, formatDate } from '../lib/utils' describe('Utils', () => { describe('cn', () => { it('should merge class names correctly', () => { expect(cn('base', 'additional')).toBe('base additional') expect(cn('base', { 'conditional': true })).toBe('base conditional') expect(cn('base', { 'conditional': false })).toBe('base') }) }) describe('formatDate', () => { it('should format date correctly', () => { const date = new Date('2024-01-01') expect(formatDate(date)).toBe('2024年1月1日') }) }) }) ``` ### 组件测试 ```typescript // __tests__/Button.test.tsx import { render, screen, fireEvent } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' import { Button } from '../components/ui/Button' describe('Button', () => { it('should render correctly', () => { render(Click me) expect(screen.getByRole('button')).toBeInTheDocument() expect(screen.getByText('Click me')).toBeInTheDocument() }) it('should handle click events', () => { const handleClick = vi.fn() render(Click me) fireEvent.click(screen.getByRole('button')) expect(handleClick).toHaveBeenCalledTimes(1) }) it('should apply variant styles', () => { render(Delete) const button = screen.getByRole('button') expect(button).toHaveClass('bg-destructive') }) }) ``` ### E2E 测试 ```typescript // e2e/auth.spec.ts import { test, expect } from '@playwright/test' test.describe('Authentication', () => { test('should allow user to sign in', async ({ page }) => { await page.goto('/login') await page.fill('[data-testid="email"]', 'test@example.com') await page.fill('[data-testid="password"]', 'password123') await page.click('[data-testid="sign-in-button"]') await expect(page).toHaveURL('/dashboard') await expect(page.locator('[data-testid="user-menu"]')).toBeVisible() }) test('should show error for invalid credentials', async ({ page }) => { await page.goto('/login') await page.fill('[data-testid="email"]', 'invalid@example.com') await page.fill('[data-testid="password"]', 'wrongpassword') await page.click('[data-testid="sign-in-button"]') await expect(page.locator('[data-testid="error-message"]')).toBeVisible() }) }) ``` ## 性能优化 ### 代码分割 ```typescript // 路由级别的代码分割 import { lazy, Suspense } from 'react' import { Routes, Route } from 'react-router-dom' const HomePage = lazy(() => import('./pages/HomePage')) const AnalysisPage = lazy(() => import('./pages/AnalysisPage')) const HistoryPage = lazy(() => import('./pages/HistoryPage')) function App() { return ( 加载中...}> } /> } /> } /> ) } ``` ### 组件优化 ```typescript // 使用 React.memo 优化组件渲染 import React, { memo, useMemo, useCallback } from 'react' interface ExpensiveComponentProps { data: any[] onItemClick: (id: string) => void } const ExpensiveComponent = memo(({ data, onItemClick }) => { // 使用 useMemo 缓存计算结果 const processedData = useMemo(() => { return data.map(item => ({ ...item, processed: expensiveCalculation(item) })) }, [data]) // 使用 useCallback 缓存事件处理函数 const handleClick = useCallback((id: string) => { onItemClick(id) }, [onItemClick]) return ( {processedData.map(item => ( handleClick(item.id)}> {item.processed} ))} ) }) function expensiveCalculation(item: any) { // 模拟昂贵的计算 return item.value * Math.random() } ``` ### 图片优化 ```typescript // 图片懒加载组件 import React, { useState, useRef, useEffect } from 'react' interface LazyImageProps { src: string alt: string className?: string placeholder?: string } const LazyImage: React.FC = ({ src, alt, className, placeholder = '/placeholder.jpg' }) => { const [isLoaded, setIsLoaded] = useState(false) const [isInView, setIsInView] = useState(false) const imgRef = useRef(null) useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsInView(true) observer.disconnect() } }, { threshold: 0.1 } ) if (imgRef.current) { observer.observe(imgRef.current) } return () => observer.disconnect() }, []) return ( setIsLoaded(true)} style={{ opacity: isLoaded ? 1 : 0.5, transition: 'opacity 0.3s ease' }} /> ) } ``` ## 调试技巧 ### React DevTools ```typescript // 在开发环境中启用 React DevTools if (import.meta.env.DEV) { // 为组件添加显示名称 Component.displayName = 'ComponentName' // 添加调试信息 console.log('Component rendered with props:', props) } ``` ### 错误边界 ```typescript // components/ErrorBoundary.tsx import React, { Component, ErrorInfo, ReactNode } from 'react' interface Props { children: ReactNode fallback?: ReactNode } interface State { hasError: boolean error?: Error } export class ErrorBoundary extends Component { public state: State = { hasError: false } public static getDerivedStateFromError(error: Error): State { return { hasError: true, error } } public componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error('ErrorBoundary caught an error:', error, errorInfo) // 发送错误报告到监控服务 if (import.meta.env.PROD) { // Sentry.captureException(error, { extra: errorInfo }) } } public render() { if (this.state.hasError) { return this.props.fallback || ( 出现了一些问题 请刷新页面重试 {import.meta.env.DEV && ( 错误详情 {this.state.error?.stack} )} ) } return this.props.children } } ``` ### 性能监控 ```typescript // lib/performance.ts export const measurePerformance = (name: string, fn: () => void) => { if (import.meta.env.DEV) { const start = performance.now() fn() const end = performance.now() console.log(`${name} took ${end - start} milliseconds`) } else { fn() } } // 使用示例 measurePerformance('Data Processing', () => { // 执行数据处理逻辑 processLargeDataSet(data) }) ``` ## 贡献流程 ### 1. 开发流程 ```bash # 1. 创建功能分支 git checkout -b feature/new-feature # 2. 开发和测试 npm run dev npm run test npm run lint # 3. 提交代码 git add . git commit -m "feat: add new feature" # 4. 推送分支 git push origin feature/new-feature # 5. 创建 Pull Request ``` ### 2. 提交信息规范 ``` type(scope): description [optional body] [optional footer] ``` 类型说明: - `feat`: 新功能 - `fix`: 修复bug - `docs`: 文档更新 - `style`: 代码格式调整 - `refactor`: 代码重构 - `test`: 测试相关 - `chore`: 构建过程或辅助工具的变动 ### 3. Code Review 检查清单 - [ ] 代码符合项目规范 - [ ] 包含适当的测试 - [ ] 文档已更新 - [ ] 性能影响已评估 - [ ] 安全性已考虑 - [ ] 向后兼容性已确认 - [ ] UI/UX 符合设计规范 --- 更多开发相关问题,请参考 [FAQ](FAQ.md) 或在 GitHub Issues 中讨论。
{description}
请刷新页面重试
{this.state.error?.stack}