|
|
@@ -1,753 +1,131 @@
|
|
|
# Visa Card H5
|
|
|
|
|
|
-这是一个基于 React + TypeScript + Vite 的现代化 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 dev` | 本地开发(使用 `.env.localdev` + `.env`) |
|
|
|
+| `pnpm build` | 生产构建(等价于 `build:prod`) |
|
|
|
+| `pnpm build:dev` | 开发模式构建(`.env.development`) |
|
|
|
+| `pnpm build:prod` | 生产模式构建(`.env.production`) |
|
|
|
+| `pnpm build:test` | 测试环境构建(`.env.test`) |
|
|
|
+| `pnpm preview` | 预览构建产物(使用 localdev 模式) |
|
|
|
+| `pnpm lint` / `pnpm lint:fix` | ESLint 检查 / 自动修复 |
|
|
|
+| `pnpm stylelint` / `pnpm stylelint:fix` | 样式检查 / 自动修复 |
|
|
|
|
|
|
-# 代码检查
|
|
|
-pnpm lint # 检查 TypeScript/JavaScript 代码
|
|
|
-pnpm lint:fix # 自动修复 TypeScript/JavaScript 代码问题
|
|
|
-pnpm stylelint # 检查样式文件
|
|
|
-pnpm stylelint:fix # 自动修复样式文件问题
|
|
|
-```
|
|
|
+## 环境配置
|
|
|
|
|
|
-## 开发规范
|
|
|
+通过不同 `.env` 文件区分环境,Vite 按 **mode** 加载:
|
|
|
|
|
|
-- 遵循 TypeScript 严格模式
|
|
|
-- 使用 ESLint 和 Prettier 进行代码格式化
|
|
|
-- 使用 Conventional Commits 规范提交信息
|
|
|
-- 组件采用函数式组件和 Hooks
|
|
|
-- 样式优先使用 Tailwind CSS,必要时使用 SCSS 或 Less
|
|
|
+| 文件 | 何时加载 |
|
|
|
+|------|----------|
|
|
|
+| `.env` | 始终加载(公共默认) |
|
|
|
+| `.env.localdev` | `pnpm dev` / `pnpm preview`(`--mode localdev`) |
|
|
|
+| `.env.development` | `pnpm build:dev` |
|
|
|
+| `.env.production` | `pnpm build` / `pnpm build:prod` |
|
|
|
+| `.env.test` | `pnpm build:test` |
|
|
|
+| `.env.local` | 本地覆盖,除 test 外都会加载(一般不提交) |
|
|
|
|
|
|
-## 部署
|
|
|
+需在环境文件中配置的变量见 `.env` 内注释或项目文档。
|
|
|
|
|
|
-项目支持多环境部署,通过 `.env` 文件配置不同环境的变量:
|
|
|
+## 技术栈
|
|
|
|
|
|
-- `.env.development` - 开发环境
|
|
|
-- `.env.test` - 测试环境
|
|
|
-- `.env.production` - 生产环境
|
|
|
+- **框架**:React 18、TypeScript
|
|
|
+- **构建**:Vite 6、pnpm
|
|
|
+- **UI**:Ant Design 5、Tailwind CSS、SCSS/Less
|
|
|
+- **路由**:React Router 7
|
|
|
+- **状态**:基于 Context + Hooks 的 Model 方案(类 Umi max)
|
|
|
+- **国际化**:i18next(zh-CN / en-US / fa-IR)
|
|
|
+- **请求**:Axios 封装(`src/utils/request`)
|
|
|
+- **图标**:@iconify/react + 自定义 SVG 转 Iconify 插件
|
|
|
+- **规范**:ESLint、Prettier、Stylelint、Commitlint、Husky、lint-staged
|
|
|
|
|
|
## 项目结构
|
|
|
|
|
|
```
|
|
|
-├── build/ # 构建相关配置和脚本
|
|
|
-├── public/ # 静态资源目录
|
|
|
-│ ├── favicon.ico # 网站图标
|
|
|
-│ └── index.html # HTML 模板
|
|
|
+├── build/ # Vite 构建相关(插件、工具)
|
|
|
+├── public/ # 静态资源
|
|
|
├── 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/ # 构建输出目录
|
|
|
+│ ├── assets/ # 资源
|
|
|
+│ │ └── iconify/ # 单色 single-color、多色 multi-color 图标
|
|
|
+│ ├── components/ # 公共组件
|
|
|
+│ ├── config/ # 应用与请求配置(含 request 拦截器、错误处理)
|
|
|
+│ ├── defines/ # 枚举与常量
|
|
|
+│ ├── hooks/ # 公共 Hooks
|
|
|
+│ ├── i18n/ # 国际化配置
|
|
|
+│ ├── layouts/ # 布局
|
|
|
+│ ├── locales/ # 多语言文案(zh-CN、en-US、fa-IR)
|
|
|
+│ ├── models/ # 全局状态 Model
|
|
|
+│ ├── pages/ # 页面(按页面分目录)
|
|
|
+│ ├── router/ # 路由配置
|
|
|
+│ ├── services/ # API 接口(按模块分目录)
|
|
|
+│ ├── styles/ # 全局样式
|
|
|
+│ ├── utils/ # 工具与 request 封装
|
|
|
+│ ├── App.tsx
|
|
|
+│ └── main.tsx
|
|
|
+├── types/ # 全局类型与 env 类型
|
|
|
+├── index.html
|
|
|
+├── vite.config.ts
|
|
|
+└── package.json
|
|
|
```
|
|
|
|
|
|
-### 目录说明
|
|
|
-
|
|
|
-- **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 类型定义目录
|
|
|
- - 存放全局类型声明文件
|
|
|
- - 第三方库的类型定义
|
|
|
+- 严格 TypeScript,遵循项目 ESLint/Prettier 配置
|
|
|
+- 提交信息使用 Conventional Commits
|
|
|
+- 组件:函数组件 + Hooks;样式优先 Tailwind,必要时 SCSS/Less
|
|
|
+- 用户可见文案全部走 `locales`,禁止在代码中硬编码
|
|
|
+- 常量与枚举放在 `src/defines/`,请求在 `src/services/` 按模块组织
|
|
|
+- 新页面:在 `src/pages/` 下建目录、在 `routes` 与 `locales` 中注册
|
|
|
|
|
|
-- **dist/** - 构建输出目录
|
|
|
- - 存放构建后的生产环境代码
|
|
|
- - 包含优化后的静态资源
|
|
|
- - 部署时使用此目录的内容
|
|
|
+## 状态管理(Model)
|
|
|
|
|
|
-## 状态管理
|
|
|
-
|
|
|
-项目使用基于 React Context 和 Hooks 的 Model 方案进行状态管理,类似 UmiJS Max 的 model 方案。
|
|
|
-
|
|
|
-### 目录结构
|
|
|
-
|
|
|
-```
|
|
|
-src/
|
|
|
- └── models/ # 模型目录
|
|
|
- ├── userModel.ts # 用户模型
|
|
|
- └── ... # 其他模型
|
|
|
-```
|
|
|
+- Model 定义在 `src/models/`,通过 `createModel(hook, name)` 创建。
|
|
|
+- 在组件中:`const user = userModel.useModel()`,使用返回的状态与方法。
|
|
|
+- 根组件已通过 `autoImportModels` 注入所有 Model 的 Provider。
|
|
|
|
|
|
-#### 使用示例
|
|
|
+详见 `src/utils/model/` 与现有 `userModel`、`dialogModel`、`userConfigModel` 实现。
|
|
|
|
|
|
-1. 创建模型:
|
|
|
+## 图标
|
|
|
|
|
|
-```typescript
|
|
|
-// src/models/userModel.ts
|
|
|
-import { useState } from 'react';
|
|
|
-import { createModel } from '@/utils/model/createModel';
|
|
|
+- 单色图标:放入 `src/assets/iconify/single-color/`,会转为 `currentColor`,可用 `className` 控制颜色。
|
|
|
+- 多色图标:放入 `src/assets/iconify/multi-color/`,保留原色。
|
|
|
+- 使用:`import icon from '@/assets/iconify/single-color/xxx.svg'`,再 `<Icon icon={icon} />`(@iconify/react)。
|
|
|
|
|
|
-// 定义状态类型
|
|
|
-interface UserState {
|
|
|
- name: string;
|
|
|
- age: number;
|
|
|
-}
|
|
|
+## 多语言
|
|
|
|
|
|
-// 创建自定义 Hook
|
|
|
-const useUserModel = () => {
|
|
|
- const [state, setState] = useState<UserState>({
|
|
|
- name: 'John Doe',
|
|
|
- age: 30
|
|
|
- });
|
|
|
+- 文案在 `src/locales/` 下按语言分目录(zh-CN、en-US、fa-IR),每语言含 common、components、menus、pages 等模块。
|
|
|
+- 组件内:`const { t } = useTranslation();`,`t('key')` 或带命名空间的 key。
|
|
|
+- 禁止在页面/组件中写死中文或英文字符串。
|
|
|
|
|
|
- const updateName = (name: string) => {
|
|
|
- setState(prev => ({ ...prev, name }));
|
|
|
- };
|
|
|
+## 请求与接口
|
|
|
|
|
|
- const updateAge = (age: number) => {
|
|
|
- setState(prev => ({ ...prev, age }));
|
|
|
- };
|
|
|
+- 请求封装:`src/utils/request`,基础配置与拦截器在 `src/config/request/`(认证、加解密、错误处理)。
|
|
|
+- 接口按模块放在 `src/services/`,每模块可有 `typings.d.ts` + `index.ts`。
|
|
|
+- 调用:`import { fetchXxx } from '@/services/xxx'`,错误由统一错误处理兜底,可选 `skipErrorHandler` 自行处理。
|
|
|
|
|
|
- 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 (
|
|
|
- <div>
|
|
|
- <h1>Hello, {user.name}!</h1>
|
|
|
- <p>Age: {user.age}</p>
|
|
|
- <button onClick={() => user.updateName('张三')}>修改名字</button>
|
|
|
- <button onClick={() => user.incrementAge()}>年龄+1</button>
|
|
|
- </div>
|
|
|
- );
|
|
|
-};
|
|
|
-```
|
|
|
-
|
|
|
-## 图标使用说明
|
|
|
-
|
|
|
-项目使用了自定义的 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';
|
|
|
-
|
|
|
-// 单色图标
|
|
|
-<Icon icon={singleColorIcon} className="text-red-500" />
|
|
|
-
|
|
|
-// 多色图标
|
|
|
-<Icon icon={multiColorIcon} />
|
|
|
-```
|
|
|
-
|
|
|
-### 图标特性
|
|
|
-
|
|
|
-- 单色图标可以通过 `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 (
|
|
|
- <div>
|
|
|
- <h1>{t('common.title')}</h1>
|
|
|
- <p>{t('common.description')}</p>
|
|
|
- </div>
|
|
|
- );
|
|
|
-};
|
|
|
-```
|
|
|
-
|
|
|
-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`)
|
|
|
+- `src/utils/localUtils`、`src/utils/sessionUtils` 提供 `createLocalTools` / `createSessionTools`,支持按 key/value 加密与命名空间。
|
|
|
+- 通过环境变量配置加密密钥与存储命名空间,敏感数据建议开启加密选项。
|
|
|
|
|
|
-用于处理请求和响应的数据加密解密。可以通过请求配置选项控制单个请求是否启用加密:
|
|
|
-
|
|
|
-```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<string>;
|
|
|
- permissions?: Array<string>;
|
|
|
- };
|
|
|
-
|
|
|
- // 登录参数类型
|
|
|
- type LoginParams = {
|
|
|
- username: string;
|
|
|
- password: string;
|
|
|
- captchaId: string;
|
|
|
- captchaCode: string;
|
|
|
- };
|
|
|
-
|
|
|
- // 登录结果类型
|
|
|
- type LoginResult = Result<UserInfo>;
|
|
|
-}
|
|
|
-```
|
|
|
-
|
|
|
-2. **接口实现** (`services/[module]/index.ts`)
|
|
|
-
|
|
|
-```typescript
|
|
|
-import { request } from '@/utils/request';
|
|
|
-
|
|
|
-// 登录接口
|
|
|
-export async function fetchLogin(body: API.LoginParams, options?: { [key: string]: any }) {
|
|
|
- return request<API.LoginResult>('/user/login', {
|
|
|
- method: 'POST',
|
|
|
- data: body,
|
|
|
- ...(options || {}),
|
|
|
- requireToken: false,
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-// 获取验证码
|
|
|
-export async function fetchCaptcha(options?: { [key: string]: any }) {
|
|
|
- return request<API.CaptchaResult>('/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<string, string>;
|
|
|
- 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: <T = any>(key: string, value: T, opts?: StorageOptions) => void;
|
|
|
- getLocal: <T = any>(key: string, opts?: StorageOptions) => T | null;
|
|
|
- removeLocal: (key: string, opts?: StorageOptions) => void;
|
|
|
- clearLocal: () => void;
|
|
|
-}
|
|
|
+## 部署
|
|
|
|
|
|
-interface SessionStorageInstance {
|
|
|
- setSession: <T = any>(key: string, value: T, opts?: StorageOptions) => void;
|
|
|
- getSession: <T = any>(key: string, opts?: StorageOptions) => T | null;
|
|
|
- removeSession: (key: string, opts?: StorageOptions) => void;
|
|
|
- clearSession: () => void;
|
|
|
-}
|
|
|
-```
|
|
|
+- 构建产物在 `dist/`,将对应环境(如 test/production)的构建结果部署到静态服务器或 CDN 即可。
|
|
|
+- 多环境通过上述 `build:dev` / `build:test` / `build:prod` 与对应 `.env.*` 区分。
|