# Visa Card H5 这是一个基于 React + TypeScript + Vite 的现代化 H5 项目。 ## 环境要求 - Node.js >= 18.20.7 - pnpm >= 9 - 现代浏览器支持 ## 浏览器支持 - Chrome >= 87 - Firefox >= 78 - Safari >= 14 - Edge >= 88 - Android >= 4.4 - iOS >= 9 ## 技术栈 - **核心框架**:React 18 + TypeScript - **构建工具**:Vite 6 - **包管理器**:pnpm - **UI 框架**:Ant Design 5 - **路由**:React Router 7 - **状态管理**: - 基于 React Context + Hooks 的自定义方案,类似 UmiJS max 中的 model - **样式解决方案**: - Tailwind CSS - SCSS - Less - **国际化**:i18next - **HTTP 客户端**:Axios - **工具库**: - lodash-es - ramda - dayjs - **图标解决方案**: - @iconify/react - 自定义 Vite 插件(SVG 转 Iconify) - Ant Design Icon - **代码规范**: - ESLint - Prettier - Stylelint - Commitlint - **Git 工作流**: - Husky - lint-staged ## 开发命令 ```bash # 安装依赖 pnpm install --frozen-lockfile # 启动开发服务器 pnpm dev # 构建生产环境 pnpm build # 构建测试环境 pnpm build:test # 预览构建结果 pnpm preview # 代码检查 pnpm lint # 检查 TypeScript/JavaScript 代码 pnpm lint:fix # 自动修复 TypeScript/JavaScript 代码问题 pnpm stylelint # 检查样式文件 pnpm stylelint:fix # 自动修复样式文件问题 ``` ## 开发规范 - 遵循 TypeScript 严格模式 - 使用 ESLint 和 Prettier 进行代码格式化 - 使用 Conventional Commits 规范提交信息 - 组件采用函数式组件和 Hooks - 样式优先使用 Tailwind CSS,必要时使用 SCSS 或 Less ## 部署 项目支持多环境部署,通过 `.env` 文件配置不同环境的变量: - `.env.development` - 开发环境 - `.env.test` - 测试环境 - `.env.production` - 生产环境 ## 项目结构 ``` ├── build/ # 构建相关配置和脚本 ├── public/ # 静态资源目录 │ ├── favicon.ico # 网站图标 │ └── index.html # HTML 模板 ├── src/ │ ├── assets/ # 项目资源文件 │ │ ├── single-color/ # 单色图标 │ │ └── multi-color/ # 多色图标 │ ├── components/ # 公共组件 │ ├── config/ # 配置文件 │ │ └── request/ # 请求相关配置 │ │ ├── index.ts # 请求基础配置 │ │ ├── authHeaderInterceptor.ts # 认证拦截器 │ │ ├── encryptionInterceptors.ts # 加密解密拦截器 │ │ └── errorHandler.ts # 错误处理 │ ├── defines/ # 枚举和常量定义 │ │ ├── index.ts # 导出入口 │ │ ├── errorShowType.ts # 错误展示类型枚举 │ │ └── ... # 其他常量/枚举定义文件 │ ├── firebase.tsx # Firebase 配置 │ ├── i18n/ # 国际化配置 │ ├── layouts/ # 布局组件 │ ├── locales/ # 国际化语言包 │ ├── main.tsx # 应用入口 │ ├── models/ # 数据模型 │ ├── pages/ # 页面组件 │ ├── router/ # 路由配置 │ ├── services/ # API 服务 │ ├── styles/ # 全局样式 │ ├── utils/ # 工具函数 │ └── App.tsx # 根组件 ├── types/ # TypeScript 类型定义 └── dist/ # 构建输出目录 ``` ### 目录说明 - **build/** - 包含构建相关的配置文件和脚本 - 构建脚本 - 环境配置 - 部署配置 - **public/** - 静态资源目录 - 存放不需要通过构建工具处理的静态文件 - 包含网站图标、HTML 模板等 - 构建时会被直接复制到输出目录 - **src/** - 源代码目录 - 包含所有需要编译的源代码 - 按功能模块组织代码结构 - **src/pages/** - 页面组件目录 - 遵循"一个页面一个目录"的原则 - 每个页面的目录结构应该类似下面的结构: ``` pages/ ├── home/ # 首页 │ ├── index.tsx # 页面主组件 │ ├── components/ # 页面级组件 │ ├── hooks/ # 页面级 Hooks │ ├── styles/ # 页面级样式 │ └── types.ts # 页面级类型定义 ├── about/ # 关于页面 │ ├── index.tsx │ ├── components/ │ ├── hooks/ │ ├── styles/ │ └── types.ts └── ... ``` - 页面级组件、Hooks、样式等资源应放在对应页面目录下 - 页面间共享的组件应放在 `src/components` 目录下 - **types/** - TypeScript 类型定义目录 - 存放全局类型声明文件 - 第三方库的类型定义 - **dist/** - 构建输出目录 - 存放构建后的生产环境代码 - 包含优化后的静态资源 - 部署时使用此目录的内容 ## 状态管理 项目使用基于 React Context 和 Hooks 的 Model 方案进行状态管理,类似 UmiJS Max 的 model 方案。 ### 目录结构 ``` src/ └── models/ # 模型目录 ├── userModel.ts # 用户模型 └── ... # 其他模型 ``` #### 使用示例 1. 创建模型: ```typescript // src/models/userModel.ts import { useState } from 'react'; import { createModel } from '@/utils/model/createModel'; // 定义状态类型 interface UserState { name: string; age: number; } // 创建自定义 Hook const useUserModel = () => { const [state, setState] = useState({ name: 'John Doe', age: 30 }); const updateName = (name: string) => { setState(prev => ({ ...prev, name })); }; const updateAge = (age: number) => { setState(prev => ({ ...prev, age })); }; const incrementAge = () => { setState(prev => ({ ...prev, age: prev.age + 1 })); }; return { ...state, updateName, updateAge, incrementAge }; }; // 创建 model export const userModel = createModel(useUserModel, 'user'); ``` 2. 在组件中使用: ```typescript // src/pages/home/index.tsx import { userModel } from '@/models/userModel'; const Home = () => { const user = userModel.useModel(); return (

Hello, {user.name}!

Age: {user.age}

); }; ``` ## 图标使用说明 项目使用了自定义的 Vite 插件,可以将指定目录的 SVG 图标转换为 Iconify 格式。 ### 使用方法 1. 将需要转换的图标文件存放在以下目录: - `src/assets/single-color/` - 单色图标 - `src/assets/multi-color/` - 多色图标 2. 直接导入图标: ```tsx import singleColorIcon from '@/assets/single-color/icon.svg'; import multiColorIcon from '@/assets/multi-color/icon.svg'; ``` 3. 在组件中使用: ```tsx import { Icon } from '@iconify/react'; // 单色图标 // 多色图标 ``` ### 图标特性 - 单色图标可以通过 `className` 或 `color` 属性修改颜色 - 多色图标保持原始颜色 - 支持自定义大小(通过 `width` 和 `height` 属性) - 支持所有 Iconify 组件的属性 ## 多语言使用说明 项目使用 i18next 实现多语言支持,目前支持英文和波斯语。 ### 目录结构 ``` src/ ├── i18n/ # i18n 配置 │ └── index.ts # i18n 初始化配置 └── locales/ # 语言包 ├── en-US.ts # 英文语言包入口 ├── fa-IR.ts # 波斯语语言包入口 ├── en-US/ # 英文语言包模块 │ ├── common.ts # 通用翻译 │ ├── components.ts # 组件相关翻译 │ ├── menus.ts # 菜单相关翻译 │ └── pages.ts # 页面相关翻译 └── fa-IR/ # 波斯语语言包模块 ├── common.ts # 通用翻译 ├── components.ts # 组件相关翻译 ├── menus.ts # 菜单相关翻译 └── pages.ts # 页面相关翻译 ``` ### 翻译文件说明 1. **common.ts** - 通用翻译 - 包含按钮文本、提示信息、错误信息等通用文案 - 适用于整个应用的共享文本 2. **components.ts** - 组件相关翻译 - 包含各个组件的标签、提示、占位符等文本 - 按组件名称组织翻译 key 3. **menus.ts** - 菜单相关翻译 - 包含导航菜单、侧边栏等菜单项的文本 - 按菜单层级组织翻译 key 4. **pages.ts** - 页面相关翻译 - 包含各个页面的标题、描述、提示等文本 - 按页面名称组织翻译 key ### 使用方法 1. 在组件中使用: ```tsx import { useTranslation } from 'react-i18next'; const MyComponent = () => { const { t } = useTranslation(); return (

{t('common.title')}

{t('common.description')}

); }; ``` 2. 语言包格式示例: ```typescript // en-US/common.ts export default { yes: 'Yes', no: 'No', ok: 'OK', cancel: 'Cancel', loading: 'Loading...' }; ``` ### 注意事项 - 所有用户可见的文本都应该使用翻译函数 - 动态内容使用插值语法:`t('key', { value: dynamicValue })` - 复数形式使用:`t('key', { count: number })` - 默认语言为英文 - 波斯语文本需要从右到左(RTL)显示,注意布局适配 ## 网络请求使用说明 项目使用 Axios 作为 HTTP 客户端,并封装了一套完整的请求工具。 ### 目录结构 ``` src/ ├── config/ │ └── request/ # 请求配置 │ ├── index.ts # 基础配置 │ ├── authHeaderInterceptor.ts # 认证拦截器 │ ├── encryptionInterceptors.ts # 加密解密拦截器 │ └── requestErrorConfig.ts # 错误处理配置 ├── services/ # API 服务 │ ├── login/ # 登录相关接口 │ │ ├── index.ts # 接口实现 │ │ └── typings.d.ts # 类型定义 │ └── ... # 其他模块接口 └── utils/ └── request/ # 请求工具 ├── index.ts # 请求实例 └── types.ts # 类型定义 ``` ### 配置说明 1. **基础配置** (`config/request/index.ts`) ```typescript import { stringify } from 'qs'; import { RequestConfig } from '@/utils/request/types'; import { authHeaderInterceptor } from './authHeaderInterceptor'; import { requestEncryptionInterceptor, responseDecryptionInterceptor } from './encryptionInterceptors'; import { errorConfig } from './requestErrorConfig'; const config: RequestConfig = { baseURL: import.meta.env.VITE_API_BASE_URL!, timeout: 15000, headers: { Accept: 'application/json, text/plain, */*', 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', }, paramsSerializer: (params) => stringify(params), ...errorConfig, requestInterceptors: [authHeaderInterceptor, requestEncryptionInterceptor], responseInterceptors: [responseDecryptionInterceptor], }; ``` 2. **认证拦截器** (`config/request/authHeaderInterceptor.ts`) 用于处理请求认证,自动添加 token 等认证信息。 3. **加密解密配置** 通过环境变量配置加密密钥: ```env VITE_REQUEST_ENCRYPTION_KEY=your-encryption-key VITE_ENABLE_REQUEST_ENCRYPTION=true ``` 4. **加密解密拦截器** (`config/request/encryptionInterceptors.ts`) 用于处理请求和响应的数据加密解密。可以通过请求配置选项控制单个请求是否启用加密: ```typescript // 单个请求启用加密 request('/api/data', { encryption: { enabled: true, key: 'custom-key' // 可选,使用自定义密钥 } }); // 单个请求禁用加密 request('/api/data', { encryption: { enabled: false } }); ``` 5. **错误处理配置** (`config/request/requestErrorConfig.ts`) 统一处理请求错误,包括网络错误、业务错误等。 可以通过配置 `skipErrorHandler: true` 来跳过默认的错误处理,自行处理错误。 ### API 服务定义 在 `services` 目录下按模块定义 API 服务,每个模块包含类型定义和接口实现: 1. **类型定义** (`services/[module]/typings.d.ts`) ```typescript declare namespace API { // 用户信息类型 type UserInfo = { accessToken?: string; refreshToken?: string; expires?: number; username?: string; nickname?: string; avatar?: string; roles?: Array; permissions?: Array; }; // 登录参数类型 type LoginParams = { username: string; password: string; captchaId: string; captchaCode: string; }; // 登录结果类型 type LoginResult = Result; } ``` 2. **接口实现** (`services/[module]/index.ts`) ```typescript import { request } from '@/utils/request'; // 登录接口 export async function fetchLogin(body: API.LoginParams, options?: { [key: string]: any }) { return request('/user/login', { method: 'POST', data: body, ...(options || {}), requireToken: false, }); } // 获取验证码 export async function fetchCaptcha(options?: { [key: string]: any }) { return request('/captcha/get', { method: 'POST', ...(options || {}), requireToken: false, }); } ``` ### 使用示例 1. **在组件中使用** ```typescript import { fetchLogin, fetchCaptcha } from '@/services/login'; const LoginPage = () => { const handleLogin = async () => { try { // 获取验证码 const captchaResult = await fetchCaptcha(); // 登录 const result = await fetchLogin({ username: 'test', password: '123456', captchaId: captchaResult.id, captchaCode: '1234', }); // 处理登录成功 // 注意:request 已经处理了 success 判断,这里直接使用返回的数据 console.log('登录成功:', result); } catch (error) { // 处理错误 } }; }; ``` 2. **在 Model 中使用** ```typescript // models/userModel.ts import { fetchLogin, fetchCaptcha } from '@/services/login'; const useUserModel = () => { const login = async (params: API.LoginParams) => { try { const result = await fetchLogin(params); // 直接使用返回的数据,不需要判断 success return result; } catch (error) { // 处理错误 throw error; } }; const getCaptcha = async () => { return fetchCaptcha(); }; return { login, getCaptcha, }; }; ``` ### 请求配置选项 ```typescript interface IRequestOptions { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; data?: any; params?: any; headers?: Record; timeout?: number; getResponse?: boolean; // 是否返回完整响应 requireToken?: boolean; // 是否需要 token skipErrorHandler?: boolean; // 是否跳过默认的错误处理 requestInterceptors?: IRequestInterceptorTuple[]; // 请求拦截器 responseInterceptors?: IResponseInterceptorTuple[]; // 响应拦截器 } ``` ### 错误处理 项目提供了统一的错误处理机制: 1. 网络错误(如超时、断网等) 2. HTTP 错误(如 404、500 等) 3. 业务错误(后端返回的错误信息) 可以通过配置 `skipErrorHandler: true` 来跳过默认的错误处理,自行处理错误。 ### 响应数据 默认情况下,请求会直接返回服务器响应的数据。如果需要获取完整的响应信息(包括状态码、响应头等),可以配置 `getResponse: true`。 示例: ```typescript // 获取完整响应信息 const response = await request('/api/data', { getResponse: true, }); // response 包含完整的响应信息 console.log(response.status); // HTTP 状态码 console.log(response.headers); // 响应头 console.log(response.data); // 响应数据 ``` ### 注意事项 1. 所有 API 请求都应该在 `services` 目录下按模块组织 2. 每个模块包含 `typings.d.ts`(类型定义)和 `index.ts`(接口实现) 3. 使用 TypeScript 类型定义请求参数和响应数据 4. 需要认证的接口设置 `requireToken: true` 5. 支持请求和响应拦截器,可以针对特定请求添加自定义拦截器 ## 本地存储使用说明 项目提供了统一的本地存储工具,支持 localStorage 和 sessionStorage,并支持数据加密存储。 ### 目录结构 ``` src/ └── utils/ ├── storage/ # 存储工具 │ ├── index.ts # 存储工具入口 │ └── types.ts # 类型定义 ├── localUtils.ts # localStorage 工具 └── sessionUtils.ts # sessionStorage 工具 ``` ### 配置说明 1. **加密配置** 通过环境变量配置加密密钥: ```env VITE_STORAGE_ENCRYPTION_KEY=your-encryption-key ``` 2. **命名空间配置** 通过环境变量配置存储命名空间: ```env VITE_APP_STORAGE_NAMESPACE=your-app-namespace ``` ### 使用示例 1. **创建存储工具实例** ```typescript import { createLocalTools, createSessionTools } from '@/utils/localUtils'; // 创建 localStorage 工具实例 const ls = createLocalTools({ encryptKey: true, // 是否加密 key encryptValue: true, // 是否加密 value }); // 创建 sessionStorage 工具实例 const ss = createSessionTools({ encryptKey: true, encryptValue: true, }); ``` 2. **使用 localStorage** ```typescript // 存储数据 ls.setLocal('userInfo', { name: 'John', age: 30 }); // 获取数据 const userInfo = ls.getLocal<{ name: string; age: number }>('userInfo'); // 删除数据 ls.removeLocal('userInfo'); // 清除所有数据 ls.clearLocal(); ``` 3. **使用 sessionStorage** ```typescript // 存储数据 ss.setSession('tempData', { id: 1, status: 'pending' }); // 获取数据 const tempData = ss.getSession<{ id: number; status: string }>('tempData'); // 删除数据 ss.removeSession('tempData'); // 清除所有数据 ss.clearSession(); ``` ### 注意事项 1. 所有存储的 key 都会自动添加应用命名空间前缀 2. 加密存储的配置(key 和 value 是否加密)由创建存储工具实例时的参数决定 3. 清除操作会清除所有属于当前应用的数据(包括加密和未加密的数据) 4. 对于敏感数据,建议在创建存储工具实例时启用加密选项 5. 每个存储操作都可以通过 opts 参数覆盖全局配置 6. 建议在应用初始化时创建存储工具实例,并在整个应用中复用 ### 类型定义 ```typescript interface StorageOptions { expire?: number; // 过期时间,单位秒 encryptKey?: boolean; // 是否加密 key encryptValue?: boolean; // 是否加密 value } interface LocalStorageInstance { setLocal: (key: string, value: T, opts?: StorageOptions) => void; getLocal: (key: string, opts?: StorageOptions) => T | null; removeLocal: (key: string, opts?: StorageOptions) => void; clearLocal: () => void; } interface SessionStorageInstance { setSession: (key: string, value: T, opts?: StorageOptions) => void; getSession: (key: string, opts?: StorageOptions) => T | null; removeSession: (key: string, opts?: StorageOptions) => void; clearSession: () => void; } ```