├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── components.json ├── docs └── image-optimization.md ├── eslint.config.js ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public └── vite.svg ├── src ├── App.css ├── App.tsx ├── assets │ └── react.svg ├── components │ ├── common │ │ ├── ErrorBoundary.tsx │ │ ├── LazyImage.tsx │ │ └── VirtualList.tsx │ ├── examples │ │ └── image-gallery.tsx │ ├── layout │ │ ├── Layout.tsx │ │ ├── PageHeader.tsx │ │ └── Sidebar.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── optimized-image.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ └── tooltip.tsx ├── config │ ├── api.config.ts │ └── routes.config.ts ├── hooks │ ├── use-mobile.ts │ ├── useApi.ts │ ├── useAuth.ts │ ├── useCodeSplitting.ts │ ├── useDebounceThrottle.ts │ ├── useIntersectionObserver.ts │ ├── useLocalStorage.ts │ ├── useNetworkStatus.ts │ ├── useTheme.ts │ ├── useTranslation.ts │ └── useUserBehavior.ts ├── index.css ├── lib │ └── utils.ts ├── main.tsx ├── pages │ ├── Dashboard.tsx │ ├── NotFound.tsx │ ├── Profile.tsx │ ├── Settings.tsx │ └── auth │ │ ├── ForgotPassword.tsx │ │ ├── Login.tsx │ │ └── Register.tsx ├── providers │ ├── AppProvider.tsx │ ├── AuthProvider.tsx │ ├── I18nProvider.tsx │ ├── RouterProvider.tsx │ ├── StoreProvider.tsx │ └── ThemeProvider.tsx ├── services │ ├── ApiService.ts │ ├── LoggerService.ts │ └── StorageService.ts ├── stores │ ├── authStore.ts │ ├── i18nStore.ts │ ├── notificationStore.ts │ └── themeStore.ts ├── types │ ├── auth.types.ts │ ├── common.types.ts │ ├── i18n.types.ts │ ├── theme.types.ts │ └── vite-plugins.d.ts ├── utils │ ├── cacheManager.ts │ ├── image-optimization.ts │ └── performance.ts └── vite-env.d.ts ├── tailwind.config.js ├── template_config.json ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 / Contributing Guide 2 | 3 | [English](#english) | [中文](#中文) 4 | 5 | 6 | ## Contributing to ReactUltra 7 | 8 | Thank you for your interest in contributing to ReactUltra! This document provides guidelines and instructions to help you contribute effectively to this project. 9 | 10 | ### Code of Conduct 11 | 12 | By participating in this project, you agree to abide by our code of conduct, which expects all participants to be respectful, inclusive, and considerate. 13 | 14 | ### Getting Started 15 | 16 | 1. **Fork the Repository** 17 | - Fork the ReactUltra repository to your GitHub account 18 | 19 | 2. **Clone Your Fork** 20 | ```bash 21 | git clone https://github.com/YOUR-USERNAME/reactultra.git 22 | cd reactultra 23 | ``` 24 | 25 | 3. **Set Up Development Environment** 26 | ```bash 27 | pnpm install 28 | ``` 29 | 30 | 4. **Create a Branch** 31 | ```bash 32 | git checkout -b feature/your-feature-name 33 | ``` 34 | - Name your branch in a descriptive way: `feature/...`, `bugfix/...`, `docs/...`, etc. 35 | 36 | ### Development Workflow 37 | 38 | 1. **Make Your Changes** 39 | - Write code following the style guides and best practices 40 | - Keep commits small and focused 41 | 42 | 2. **Write Tests** 43 | - Add tests for any new features 44 | - Ensure all tests pass with `pnpm test` 45 | 46 | 3. **Documentation** 47 | - Update documentation for any changed functionality 48 | - Include JSDoc comments for functions and components 49 | 50 | 4. **Commit Your Changes** 51 | - Follow the commit message conventions: 52 | ``` 53 | type(scope): subject 54 | 55 | body (optional) 56 | ``` 57 | - Types include: feat, fix, docs, style, refactor, test, chore 58 | - Example: `feat(auth): add two-factor authentication` 59 | 60 | 5. **Push to Your Fork** 61 | ```bash 62 | git push origin feature/your-feature-name 63 | ``` 64 | 65 | 6. **Create a Pull Request** 66 | - Go to the ReactUltra repository 67 | - Click "Compare & pull request" 68 | - Fill out the PR template with details about your changes 69 | 70 | ### Pull Request Process 71 | 72 | 1. **Code Review**: At least one maintainer will review your changes 73 | 2. **CI Checks**: All automated tests must pass 74 | 3. **Approval**: Changes require approval from at least one maintainer 75 | 4. **Merge**: A maintainer will merge your PR once approved 76 | 77 | ### Style Guide 78 | 79 | #### JavaScript/TypeScript 80 | - Follow the ESLint and Prettier configurations in the project 81 | - Use TypeScript for all new code 82 | - Prefer functional components with hooks 83 | - Use named exports for components and utilities 84 | 85 | #### Components 86 | - One component per file 87 | - Use function declarations for components, not arrow functions 88 | - Place types/interfaces at the top of the file 89 | - Follow the component folder structure 90 | 91 | #### CSS 92 | - Use the project's preferred styling approach consistently 93 | - Avoid overly specific selectors 94 | - Use design tokens for colors, spacing, etc. 95 | 96 | ### Testing Guidelines 97 | 98 | - Write unit tests for utility functions 99 | - Write component tests for UI components 100 | - Focus on behavior, not implementation details 101 | - Use React Testing Library for component testing 102 | 103 | ### Documentation 104 | 105 | - Update the README.md if you add or change functionality 106 | - Update Storybook stories for component changes 107 | - Consider adding examples for complex features 108 | 109 | ### Additional Resources 110 | 111 | - [React Documentation](https://reactjs.org/docs/getting-started.html) 112 | - [TypeScript Documentation](https://www.typescriptlang.org/docs) 113 | - [Ant Design Guidelines](https://ant.design/docs/spec/introduce) 114 | 115 | ### Getting Help 116 | 117 | If you have questions or need help, you can: 118 | - Open an issue for discussion 119 | - Reach out to maintainers via the provided contact methods 120 | 121 | Thank you for contributing to ReactUltra! 122 | 123 | --- 124 | 125 | 126 | ## 参与贡献 ReactUltra 127 | 128 | 感谢您有兴趣为 ReactUltra 做出贡献!本文档提供了指南和说明,帮助您有效地为该项目做出贡献。 129 | 130 | ### 行为准则 131 | 132 | 通过参与此项目,您同意遵守我们的行为准则,该准则期望所有参与者尊重、包容和体贴他人。 133 | 134 | ### 开始 135 | 136 | 1. **复刻仓库** 137 | - 将 ReactUltra 仓库复刻到您的 GitHub 账户 138 | 139 | 2. **克隆您的复刻** 140 | ```bash 141 | git clone https://github.com/您的用户名/reactultra.git 142 | cd reactultra 143 | ``` 144 | 145 | 3. **设置开发环境** 146 | ```bash 147 | pnpm install 148 | ``` 149 | 150 | 4. **创建分支** 151 | ```bash 152 | git checkout -b feature/您的功能名称 153 | ``` 154 | - 以描述性方式命名您的分支:`feature/...`、`bugfix/...`、`docs/...` 等。 155 | 156 | ### 开发工作流程 157 | 158 | 1. **进行更改** 159 | - 按照样式指南和最佳实践编写代码 160 | - 保持提交小而集中 161 | 162 | 2. **编写测试** 163 | - 为任何新功能添加测试 164 | - 确保所有测试通过 `pnpm test` 165 | 166 | 3. **文档** 167 | - 更新任何更改功能的文档 168 | - 为函数和组件包含 JSDoc 注释 169 | 170 | 4. **提交您的更改** 171 | - 遵循提交信息约定: 172 | ``` 173 | type(scope): subject 174 | 175 | body (可选) 176 | ``` 177 | - 类型包括:feat、fix、docs、style、refactor、test、chore 178 | - 示例:`feat(auth): 添加双因素认证` 179 | 180 | 5. **推送到您的复刻** 181 | ```bash 182 | git push origin feature/您的功能名称 183 | ``` 184 | 185 | 6. **创建拉取请求** 186 | - 前往 ReactUltra 仓库 187 | - 点击"Compare & pull request" 188 | - 填写 PR 模板,详细说明您的更改 189 | 190 | ### 拉取请求流程 191 | 192 | 1. **代码审查**:至少一名维护者将审查您的更改 193 | 2. **CI 检查**:所有自动测试必须通过 194 | 3. **批准**:更改需要至少一名维护者的批准 195 | 4. **合并**:经批准后,维护者将合并您的 PR 196 | 197 | ### 样式指南 198 | 199 | #### JavaScript/TypeScript 200 | - 遵循项目中的 ESLint 和 Prettier 配置 201 | - 所有新代码使用 TypeScript 202 | - 优先使用带有钩子的函数组件 203 | - 对组件和工具使用命名导出 204 | 205 | #### 组件 206 | - 每个文件一个组件 207 | - 使用函数声明定义组件,而不是箭头函数 208 | - 将类型/接口放在文件顶部 209 | - 遵循组件文件夹结构 210 | 211 | #### CSS 212 | - 一致地使用项目首选的样式方法 213 | - 避免过于特定的选择器 214 | - 使用设计令牌表示颜色、间距等 215 | 216 | ### 测试指南 217 | 218 | - 为工具函数编写单元测试 219 | - 为 UI 组件编写组件测试 220 | - 关注行为,而非实现细节 221 | - 使用 React Testing Library 进行组件测试 222 | 223 | ### 文档 224 | 225 | - 如果添加或更改功能,请更新 README.md 226 | - 为组件更改更新 Storybook 故事 227 | - 考虑为复杂功能添加示例 228 | 229 | ### 其他资源 230 | 231 | - [React 文档](https://reactjs.org/docs/getting-started.html) 232 | - [TypeScript 文档](https://www.typescriptlang.org/docs) 233 | - [Ant Design 指南](https://ant.design/docs/spec/introduce) 234 | 235 | ### 获取帮助 236 | 237 | 如果您有问题或需要帮助,您可以: 238 | - 打开一个 issue 进行讨论 239 | - 通过提供的联系方式联系维护者 240 | 241 | 感谢您为 ReactUltra 做出贡献! -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ReactUltra - 企业级 React 应用框架 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 45 | 46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-ultra", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview", 11 | "test": "jest", 12 | "test:watch": "jest --watch", 13 | "storybook": "storybook dev -p 6006", 14 | "build-storybook": "storybook build", 15 | "prepare": "husky install", 16 | "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx}\"" 17 | }, 18 | "dependencies": { 19 | "@hookform/resolvers": "^4.1.2", 20 | "@radix-ui/react-accordion": "^1.2.3", 21 | "@radix-ui/react-alert-dialog": "^1.1.6", 22 | "@radix-ui/react-aspect-ratio": "^1.1.2", 23 | "@radix-ui/react-avatar": "^1.1.3", 24 | "@radix-ui/react-checkbox": "^1.1.4", 25 | "@radix-ui/react-collapsible": "^1.1.3", 26 | "@radix-ui/react-context-menu": "^2.2.6", 27 | "@radix-ui/react-dialog": "^1.1.6", 28 | "@radix-ui/react-dropdown-menu": "^2.1.6", 29 | "@radix-ui/react-hover-card": "^1.1.6", 30 | "@radix-ui/react-label": "^2.1.2", 31 | "@radix-ui/react-menubar": "^1.1.6", 32 | "@radix-ui/react-navigation-menu": "^1.2.5", 33 | "@radix-ui/react-popover": "^1.1.6", 34 | "@radix-ui/react-progress": "^1.1.2", 35 | "@radix-ui/react-radio-group": "^1.2.3", 36 | "@radix-ui/react-scroll-area": "^1.2.3", 37 | "@radix-ui/react-select": "^2.1.6", 38 | "@radix-ui/react-separator": "^1.1.2", 39 | "@radix-ui/react-slider": "^1.2.3", 40 | "@radix-ui/react-slot": "^1.1.2", 41 | "@radix-ui/react-switch": "^1.1.3", 42 | "@radix-ui/react-tabs": "^1.1.3", 43 | "@radix-ui/react-toggle": "^1.1.2", 44 | "@radix-ui/react-toggle-group": "^1.1.2", 45 | "@radix-ui/react-tooltip": "^1.1.8", 46 | "@tailwindcss/vite": "^4.0.8", 47 | "antd": "^5.15.0", 48 | "axios": "^1.6.7", 49 | "class-variance-authority": "^0.7.1", 50 | "clsx": "^2.1.1", 51 | "cmdk": "1.0.0", 52 | "date-fns": "^4.1.0", 53 | "echarts": "^5.5.0", 54 | "echarts-for-react": "^3.0.2", 55 | "embla-carousel-react": "^8.5.2", 56 | "framer-motion": "^11.0.8", 57 | "i18next": "^23.16.8", 58 | "i18next-browser-languagedetector": "^7.2.2", 59 | "i18next-http-backend": "^2.7.3", 60 | "input-otp": "^1.4.2", 61 | "jwt-decode": "^4.0.0", 62 | "lucide-react": "^0.475.0", 63 | "next-themes": "^0.4.4", 64 | "react": "^19.0.0", 65 | "react-day-picker": "8.10.1", 66 | "react-dom": "^19.0.0", 67 | "react-dropzone": "^14.2.3", 68 | "react-error-boundary": "^4.1.2", 69 | "react-hook-form": "^7.54.2", 70 | "react-i18next": "^14.0.5", 71 | "react-intersection-observer": "^9.8.0", 72 | "react-query": "^3.39.3", 73 | "react-resizable-panels": "^2.1.7", 74 | "react-router-dom": "^6.30.0", 75 | "recharts": "^2.15.1", 76 | "slate": "^0.101.5", 77 | "slate-history": "^0.100.0", 78 | "slate-react": "^0.101.5", 79 | "sonner": "^2.0.1", 80 | "tailwind-merge": "^3.0.2", 81 | "tailwindcss-animate": "^1.0.7", 82 | "uuid": "^11.1.0", 83 | "vaul": "^1.1.2", 84 | "webfontloader": "^1.6.28", 85 | "zod": "^3.24.2", 86 | "zustand": "^4.5.6" 87 | }, 88 | "devDependencies": { 89 | "@babel/plugin-transform-react-jsx": "^7.27.1", 90 | "@commitlint/cli": "^18.6.0", 91 | "@commitlint/config-conventional": "^18.6.0", 92 | "@eslint/js": "^9.21.0", 93 | "@storybook/addon-essentials": "^7.6.15", 94 | "@storybook/addon-interactions": "^7.6.15", 95 | "@storybook/addon-links": "^7.6.15", 96 | "@storybook/blocks": "^7.6.15", 97 | "@storybook/react": "^7.6.15", 98 | "@storybook/react-vite": "^7.6.15", 99 | "@storybook/testing-library": "^0.2.2", 100 | "@testing-library/jest-dom": "^6.4.2", 101 | "@testing-library/react": "^14.2.1", 102 | "@testing-library/user-event": "^14.5.2", 103 | "@types/jest": "^29.5.12", 104 | "@types/node": "^22.13.5", 105 | "@types/react": "^19.0.10", 106 | "@types/react-dom": "^19.0.4", 107 | "@types/webfontloader": "^1.6.38", 108 | "@typescript-eslint/eslint-plugin": "^7.0.2", 109 | "@typescript-eslint/parser": "^7.0.2", 110 | "@vitejs/plugin-react": "^4.3.4", 111 | "autoprefixer": "^10.4.20", 112 | "eslint": "^9.21.0", 113 | "eslint-config-prettier": "^9.1.0", 114 | "eslint-plugin-jsx-a11y": "^6.8.0", 115 | "eslint-plugin-prettier": "^5.1.3", 116 | "eslint-plugin-react": "^7.33.2", 117 | "eslint-plugin-react-hooks": "^5.0.0", 118 | "eslint-plugin-react-refresh": "^0.4.19", 119 | "eslint-plugin-storybook": "^0.6.15", 120 | "globals": "^15.15.0", 121 | "husky": "^9.0.11", 122 | "jest": "^29.7.0", 123 | "jest-environment-jsdom": "^29.7.0", 124 | "lint-staged": "^15.2.2", 125 | "postcss": "^8.5.3", 126 | "prettier": "^3.2.5", 127 | "storybook": "^7.6.15", 128 | "tailwindcss": "^4.0.8", 129 | "ts-jest": "^29.1.2", 130 | "typescript": "~5.7.2", 131 | "typescript-eslint": "^8.24.1", 132 | "vite": "^6.2.0", 133 | "vite-imagetools": "^6.2.9", 134 | "vite-plugin-html": "^3.2.2", 135 | "vite-plugin-prerender": "^1.0.8", 136 | "vite-plugin-pwa": "^0.17.5", 137 | "vite-tsconfig-paths": "^4.3.1", 138 | "workbox-window": "^7.0.0" 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | width: 100%; 3 | height: 100%; 4 | position: absolute; 5 | overflow: hidden; 6 | } 7 | 8 | 9 | @keyframes logo-spin { 10 | from { 11 | transform: rotate(0deg); 12 | } 13 | 14 | to { 15 | transform: rotate(360deg); 16 | } 17 | } 18 | 19 | @media (prefers-reduced-motion: no-preference) { 20 | a:nth-of-type(2) .logo { 21 | animation: logo-spin infinite 20s linear; 22 | } 23 | } 24 | 25 | .card { 26 | padding: 2em; 27 | } 28 | 29 | .read-the-docs { 30 | color: #888; 31 | } -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorBoundary } from "./components/common/ErrorBoundary"; 2 | import { AppProvider } from "./providers/AppProvider"; 3 | import { Toaster } from "./components/ui/sonner"; 4 | 5 | function App() { 6 | return ( 7 | 8 | 9 | 10 | {/* Children will be rendered by RouterProvider */} 11 | 12 | 13 | ); 14 | } 15 | 16 | export default App; -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/common/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ReactNode } from 'react'; 2 | 3 | interface ErrorBoundaryProps { 4 | children: ReactNode; 5 | fallback?: ReactNode; 6 | } 7 | 8 | interface ErrorBoundaryState { 9 | hasError: boolean; 10 | error: Error | null; 11 | } 12 | 13 | export class ErrorBoundary extends Component { 14 | constructor(props: ErrorBoundaryProps) { 15 | super(props); 16 | this.state = { 17 | hasError: false, 18 | error: null, 19 | }; 20 | } 21 | 22 | static getDerivedStateFromError(error: Error): ErrorBoundaryState { 23 | // Update state so the next render shows the fallback UI 24 | return { hasError: true, error }; 25 | } 26 | 27 | componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { 28 | // You can log the error to an error reporting service 29 | console.error('Error caught by boundary:', error, errorInfo); 30 | } 31 | 32 | resetError = (): void => { 33 | this.setState({ 34 | hasError: false, 35 | error: null, 36 | }); 37 | }; 38 | 39 | render(): ReactNode { 40 | if (this.state.hasError) { 41 | // You can render any custom fallback UI 42 | return this.props.fallback || ( 43 |
44 |
45 |

Something went wrong

46 |
47 |

48 | {this.state.error?.message || 'An unexpected error occurred'} 49 |

50 |
51 |

52 | Please try reloading the page or contact support if the issue persists. 53 |

54 |
55 | 61 | 67 |
68 |
69 |
70 | ); 71 | } 72 | 73 | return this.props.children; 74 | } 75 | } -------------------------------------------------------------------------------- /src/components/common/LazyImage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useEffect } from 'react'; 2 | import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; 3 | import { cn } from '../../lib/utils'; 4 | 5 | interface LazyImageProps extends React.ImgHTMLAttributes { 6 | src: string; 7 | alt: string; 8 | placeholderSrc?: string; 9 | blurEffect?: boolean; 10 | aspectRatio?: string; 11 | wrapperClassName?: string; 12 | } 13 | 14 | export function LazyImage({ 15 | src, 16 | alt, 17 | className, 18 | placeholderSrc = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9IiNlZWVlZWUiLz48L3N2Zz4=', 19 | blurEffect = true, 20 | aspectRatio = '16/9', 21 | wrapperClassName, 22 | ...rest 23 | }: LazyImageProps) { 24 | const [isLoaded, setIsLoaded] = useState(false); 25 | const [currentSrc, setCurrentSrc] = useState(placeholderSrc); 26 | const imageRef = useRef(null); 27 | 28 | // 使用 IntersectionObserver 检测图片可见性 29 | const entry = useIntersectionObserver(imageRef as React.RefObject, { 30 | freezeOnceVisible: true, 31 | rootMargin: '100px' // 提前100px开始加载 32 | }); 33 | 34 | const isVisible = !!entry?.isIntersecting; 35 | 36 | useEffect(() => { 37 | // 当图片可见时加载实际图片 38 | if (!isVisible || isLoaded) return; 39 | 40 | const img = new Image(); 41 | img.src = src; 42 | img.onload = () => { 43 | setCurrentSrc(src); 44 | setIsLoaded(true); 45 | }; 46 | img.onerror = () => { 47 | console.error(`Failed to load image: ${src}`); 48 | // 保持使用占位图 49 | }; 50 | }, [src, isVisible, isLoaded]); 51 | 52 | // 计算图片宽度和高度,以避免布局移动 53 | const [width, height] = aspectRatio.split('/').map(Number); 54 | const paddingBottom = `${(height / width) * 100}%`; 55 | 56 | return ( 57 |
64 | {alt} 77 |
78 | ); 79 | } -------------------------------------------------------------------------------- /src/components/common/VirtualList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { useIntersectionObserver } from '../../hooks/useIntersectionObserver'; 3 | 4 | interface VirtualListProps { 5 | data: T[]; 6 | renderItem: (item: T, index: number) => React.ReactNode; 7 | itemHeight: number; 8 | className?: string; 9 | overscan?: number; 10 | onEndReached?: () => void; 11 | endReachedThreshold?: number; 12 | getItemKey?: (item: T, index: number) => string | number; 13 | } 14 | 15 | export function VirtualList({ 16 | data, 17 | renderItem, 18 | itemHeight, 19 | className = '', 20 | overscan = 5, 21 | onEndReached, 22 | endReachedThreshold = 0.8, 23 | getItemKey = (_, index) => index, 24 | }: VirtualListProps) { 25 | const containerRef = useRef(null); 26 | const [visibleRange, setVisibleRange] = useState({ start: 0, end: overscan * 2 }); 27 | const totalHeight = data.length * itemHeight; 28 | 29 | // 添加底部观察元素,用于触发加载更多 30 | const endRef = useRef(null); 31 | const entry = useIntersectionObserver(endRef, {}); 32 | const isVisible = !!entry?.isIntersecting; 33 | 34 | useEffect(() => { 35 | if (isVisible && onEndReached) { 36 | onEndReached(); 37 | } 38 | }, [isVisible, onEndReached]); 39 | 40 | useEffect(() => { 41 | const handleScroll = () => { 42 | if (!containerRef.current) return; 43 | 44 | const { scrollTop, clientHeight } = containerRef.current; 45 | const scrollBottom = scrollTop + clientHeight; 46 | 47 | // 计算可见范围 48 | const firstVisibleIndex = Math.floor(scrollTop / itemHeight); 49 | const lastVisibleIndex = Math.ceil(scrollBottom / itemHeight); 50 | 51 | // 添加缓冲区域(overscan) 52 | const start = Math.max(0, firstVisibleIndex - overscan); 53 | const end = Math.min(data.length, lastVisibleIndex + overscan); 54 | 55 | setVisibleRange({ start, end }); 56 | }; 57 | 58 | const currentRef = containerRef.current; 59 | if (currentRef) { 60 | currentRef.addEventListener('scroll', handleScroll); 61 | // 初始计算 62 | handleScroll(); 63 | } 64 | 65 | return () => { 66 | if (currentRef) { 67 | currentRef.removeEventListener('scroll', handleScroll); 68 | } 69 | }; 70 | }, [data.length, itemHeight, overscan]); 71 | 72 | // 获取当前可见数据 73 | const visibleData = data.slice(visibleRange.start, visibleRange.end); 74 | 75 | return ( 76 |
81 |
82 | {visibleData.map((item, index) => { 83 | const actualIndex = visibleRange.start + index; 84 | const key = getItemKey(item, actualIndex); 85 | return ( 86 |
95 | {renderItem(item, actualIndex)} 96 |
97 | ); 98 | })} 99 | {/* 底部监听元素 */} 100 |
109 |
110 |
111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /src/components/examples/image-gallery.tsx: -------------------------------------------------------------------------------- 1 | import { OptimizedImage } from '@/components/ui/optimized-image'; 2 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; 3 | import { Badge } from '@/components/ui/badge'; 4 | 5 | const sampleImages = [ 6 | { 7 | src: '/images/hero-banner.jpg', 8 | alt: '主横幅图片', 9 | title: '优先加载示例', 10 | description: '首屏重要图片,设置priority=true', 11 | priority: true, 12 | width: 800, 13 | height: 400, 14 | }, 15 | { 16 | src: '/images/product-1.jpg', 17 | alt: '产品图片1', 18 | title: '懒加载示例', 19 | description: '滚动到视口时才加载', 20 | width: 300, 21 | height: 300, 22 | }, 23 | { 24 | src: '/images/product-2.jpg', 25 | alt: '产品图片2', 26 | title: '响应式图片', 27 | description: '根据屏幕尺寸自动选择最佳图片', 28 | width: 300, 29 | height: 300, 30 | }, 31 | { 32 | src: '/images/invalid-image.jpg', 33 | alt: '无效图片', 34 | title: '错误处理示例', 35 | description: '展示图片加载失败时的处理', 36 | fallback: '/images/placeholder.jpg', 37 | width: 300, 38 | height: 300, 39 | }, 40 | ]; 41 | 42 | export function ImageGallery() { 43 | const handleLoadComplete = (result: { naturalWidth: number; naturalHeight: number }) => { 44 | console.log('图片加载完成:', result); 45 | }; 46 | 47 | const handleLoadError = (error: string) => { 48 | console.error('图片加载错误:', error); 49 | }; 50 | 51 | return ( 52 |
53 |
54 |

优化图片组件示例

55 |

56 | 展示 OptimizedImage 组件的各种功能:懒加载、WebP支持、响应式图片、错误处理等 57 |

58 |
59 | 60 | {/* 主横幅 - 优先加载 */} 61 | 62 | 63 | 64 | 优先加载图片 65 | priority=true 66 | 67 | 68 | 首屏重要图片,立即加载不等待滚动 69 | 70 | 71 | 72 | 82 | 83 | 84 | 85 | {/* 图片网格 - 懒加载 */} 86 |
87 | {sampleImages.slice(1).map((image, index) => ( 88 | 89 | 90 | {image.title} 91 | {image.description} 92 | 93 | 94 | 105 | 106 | 107 | ))} 108 |
109 | 110 | {/* 性能提示 */} 111 | 112 | 113 | 性能优化特性 114 | 115 | 116 |
117 |
118 |

🚀 懒加载

119 |

120 | 图片进入视口前50px时才开始加载,减少初始页面加载时间 121 |

122 |
123 |
124 |

🖼️ WebP支持

125 |

126 | 自动检测浏览器支持,优先使用WebP格式减少文件大小 127 |

128 |
129 |
130 |

📱 响应式

131 |

132 | 根据屏幕尺寸自动选择最适合的图片分辨率 133 |

134 |
135 |
136 |

⚡ 性能监控

137 |

138 | 开发环境下自动记录图片加载时间,便于性能优化 139 |

140 |
141 |
142 |
143 |
144 |
145 | ); 146 | } -------------------------------------------------------------------------------- /src/components/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Outlet } from 'react-router-dom'; 3 | import { Sidebar } from './Sidebar'; 4 | import { useTheme } from '../../hooks/useTheme'; 5 | 6 | interface LayoutProps { 7 | children?: React.ReactNode; 8 | } 9 | 10 | const Layout: React.FC = ({ children }) => { 11 | const { theme } = useTheme(); 12 | 13 | return ( 14 |
15 | {/* Sidebar */} 16 | 17 | 18 | {/* Main content area */} 19 |
20 | {/* Main content */} 21 |
22 | {children || } 23 |
24 |
25 |
26 | ); 27 | }; 28 | 29 | export default Layout; -------------------------------------------------------------------------------- /src/components/layout/PageHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { cn } from '../../lib/utils'; 3 | 4 | interface PageHeaderProps { 5 | title: string; 6 | description?: string; 7 | actions?: ReactNode; 8 | className?: string; 9 | } 10 | 11 | export const PageHeader: React.FC = ({ 12 | title, 13 | description, 14 | actions, 15 | className 16 | }) => { 17 | return ( 18 |
19 |
20 |

{title}

21 | {description && ( 22 |

{description}

23 | )} 24 |
25 | 26 | {actions && ( 27 |
{actions}
28 | )} 29 |
30 | ); 31 | }; -------------------------------------------------------------------------------- /src/components/layout/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, useLocation } from 'react-router-dom'; 3 | import { cn } from '../../lib/utils'; 4 | import { Button } from '../ui/button'; 5 | import { ScrollArea } from '../ui/scroll-area'; 6 | import { useAuth } from '../../hooks/useAuth'; 7 | import { 8 | LayoutDashboard, 9 | Settings, 10 | User, 11 | HelpCircle, 12 | LogOut, 13 | ChevronRight, 14 | Menu, 15 | X 16 | } from 'lucide-react'; 17 | 18 | export const Sidebar: React.FC = () => { 19 | const { logout } = useAuth(); 20 | const [expanded, setExpanded] = useState(true); 21 | const [mobileOpen, setMobileOpen] = useState(false); 22 | const location = useLocation(); 23 | 24 | const navigationItems = [ 25 | { 26 | name: 'Dashboard', 27 | href: '/', 28 | icon: 29 | }, 30 | { 31 | name: 'Profile', 32 | href: '/profile', 33 | icon: 34 | }, 35 | { 36 | name: 'Settings', 37 | href: '/settings', 38 | icon: 39 | } 40 | ]; 41 | 42 | const toggleExpanded = () => setExpanded(prev => !prev); 43 | const toggleMobile = () => setMobileOpen(prev => !prev); 44 | 45 | return ( 46 | <> 47 | {/* Mobile sidebar toggle */} 48 | 56 | 57 | {/* Sidebar background overlay for mobile */} 58 | {mobileOpen && ( 59 |
63 | )} 64 | 65 | {/* Sidebar container */} 66 | 151 | 152 | ); 153 | }; -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 3 | import { ChevronDownIcon } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | function Accordion({ 8 | ...props 9 | }: React.ComponentProps) { 10 | return 11 | } 12 | 13 | function AccordionItem({ 14 | className, 15 | ...props 16 | }: React.ComponentProps) { 17 | return ( 18 | 23 | ) 24 | } 25 | 26 | function AccordionTrigger({ 27 | className, 28 | children, 29 | ...props 30 | }: React.ComponentProps) { 31 | return ( 32 | 33 | svg]:rotate-180", 37 | className 38 | )} 39 | {...props} 40 | > 41 | {children} 42 | 43 | 44 | 45 | ) 46 | } 47 | 48 | function AccordionContent({ 49 | className, 50 | children, 51 | ...props 52 | }: React.ComponentProps) { 53 | return ( 54 | 59 |
{children}
60 |
61 | ) 62 | } 63 | 64 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 65 | -------------------------------------------------------------------------------- /src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { buttonVariants } from "@/components/ui/button" 8 | 9 | function AlertDialog({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function AlertDialogTrigger({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return ( 19 | 20 | ) 21 | } 22 | 23 | function AlertDialogPortal({ 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 28 | ) 29 | } 30 | 31 | function AlertDialogOverlay({ 32 | className, 33 | ...props 34 | }: React.ComponentProps) { 35 | return ( 36 | 44 | ) 45 | } 46 | 47 | function AlertDialogContent({ 48 | className, 49 | ...props 50 | }: React.ComponentProps) { 51 | return ( 52 | 53 | 54 | 62 | 63 | ) 64 | } 65 | 66 | function AlertDialogHeader({ 67 | className, 68 | ...props 69 | }: React.ComponentProps<"div">) { 70 | return ( 71 |
76 | ) 77 | } 78 | 79 | function AlertDialogFooter({ 80 | className, 81 | ...props 82 | }: React.ComponentProps<"div">) { 83 | return ( 84 |
92 | ) 93 | } 94 | 95 | function AlertDialogTitle({ 96 | className, 97 | ...props 98 | }: React.ComponentProps) { 99 | return ( 100 | 105 | ) 106 | } 107 | 108 | function AlertDialogDescription({ 109 | className, 110 | ...props 111 | }: React.ComponentProps) { 112 | return ( 113 | 118 | ) 119 | } 120 | 121 | function AlertDialogAction({ 122 | className, 123 | ...props 124 | }: React.ComponentProps) { 125 | return ( 126 | 130 | ) 131 | } 132 | 133 | function AlertDialogCancel({ 134 | className, 135 | ...props 136 | }: React.ComponentProps) { 137 | return ( 138 | 142 | ) 143 | } 144 | 145 | export { 146 | AlertDialog, 147 | AlertDialogPortal, 148 | AlertDialogOverlay, 149 | AlertDialogTrigger, 150 | AlertDialogContent, 151 | AlertDialogHeader, 152 | AlertDialogFooter, 153 | AlertDialogTitle, 154 | AlertDialogDescription, 155 | AlertDialogAction, 156 | AlertDialogCancel, 157 | } 158 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | function Alert({ 23 | className, 24 | variant, 25 | ...props 26 | }: React.ComponentProps<"div"> & VariantProps) { 27 | return ( 28 |
34 | ) 35 | } 36 | 37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { 38 | return ( 39 |
47 | ) 48 | } 49 | 50 | function AlertDescription({ 51 | className, 52 | ...props 53 | }: React.ComponentProps<"div">) { 54 | return ( 55 |
63 | ) 64 | } 65 | 66 | export { Alert, AlertTitle, AlertDescription } 67 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 2 | 3 | function AspectRatio({ 4 | ...props 5 | }: React.ComponentProps) { 6 | return 7 | } 8 | 9 | export { AspectRatio } 10 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 34 | ) 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ) 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback } 54 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const badgeVariants = cva( 8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-auto", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 14 | secondary: 15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 16 | destructive: 17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", 18 | outline: 19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | } 26 | ) 27 | 28 | function Badge({ 29 | className, 30 | variant, 31 | asChild = false, 32 | ...props 33 | }: React.ComponentProps<"span"> & 34 | VariantProps & { asChild?: boolean }) { 35 | const Comp = asChild ? Slot : "span" 36 | 37 | return ( 38 | 43 | ) 44 | } 45 | 46 | export { Badge, badgeVariants } 47 | -------------------------------------------------------------------------------- /src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { ChevronRight, MoreHorizontal } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { 8 | return