|
|
1 tháng trước cách đây | |
|---|---|---|
| .cursor | 1 tháng trước cách đây | |
| .vscode | 1 tháng trước cách đây | |
| build | 1 tháng trước cách đây | |
| public | 1 tháng trước cách đây | |
| src | 1 tháng trước cách đây | |
| types | 1 tháng trước cách đây | |
| .env | 1 tháng trước cách đây | |
| .env.development | 1 tháng trước cách đây | |
| .env.production | 1 tháng trước cách đây | |
| .env.test | 1 tháng trước cách đây | |
| .gitignore | 1 tháng trước cách đây | |
| .markdownlint.json | 1 tháng trước cách đây | |
| .npmrc | 1 tháng trước cách đây | |
| .nvmrc | 1 tháng trước cách đây | |
| .prettierrc | 1 tháng trước cách đây | |
| .stylelintrc.mjs | 1 tháng trước cách đây | |
| README.md | 1 tháng trước cách đây | |
| commitlint.config.js | 1 tháng trước cách đây | |
| eslint.config.js | 1 tháng trước cách đây | |
| index.html | 1 tháng trước cách đây | |
| package.json | 1 tháng trước cách đây | |
| pnpm-lock.yaml | 1 tháng trước cách đây | |
| postcss.config.js | 1 tháng trước cách đây | |
| tailwind.config.js | 1 tháng trước cách đây | |
| tsconfig.json | 1 tháng trước cách đây | |
| vite.config.ts | 1 tháng trước cách đây |
这是一个基于 React + TypeScript + Vite 的现代化 H5 项目。
# 安装依赖
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 # 自动修复样式文件问题
项目支持多环境部署,通过 .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/ - 静态资源目录
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 # 用户模型
└── ... # 其他模型
// 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<UserState>({
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');
// 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 格式。
src/assets/single-color/ - 单色图标src/assets/multi-color/ - 多色图标import singleColorIcon from '@/assets/single-color/icon.svg';
import multiColorIcon from '@/assets/multi-color/icon.svg';
import { Icon } from '@iconify/react';
// 单色图标
<Icon icon={singleColorIcon} className="text-red-500" />
// 多色图标
<Icon icon={multiColorIcon} />
className 或 color 属性修改颜色width 和 height 属性)项目使用 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 # 页面相关翻译
common.ts - 通用翻译
components.ts - 组件相关翻译
menus.ts - 菜单相关翻译
pages.ts - 页面相关翻译
import { useTranslation } from 'react-i18next';
const MyComponent = () => {
const { t } = useTranslation();
return (
<div>
<h1>{t('common.title')}</h1>
<p>{t('common.description')}</p>
</div>
);
};
// en-US/common.ts
export default {
yes: 'Yes',
no: 'No',
ok: 'OK',
cancel: 'Cancel',
loading: 'Loading...'
};
t('key', { value: dynamicValue })t('key', { count: number })项目使用 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 # 类型定义
config/request/index.ts)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],
};
config/request/authHeaderInterceptor.ts)用于处理请求认证,自动添加 token 等认证信息。
通过环境变量配置加密密钥:
VITE_REQUEST_ENCRYPTION_KEY=your-encryption-key
VITE_ENABLE_REQUEST_ENCRYPTION=true
config/request/encryptionInterceptors.ts)用于处理请求和响应的数据加密解密。可以通过请求配置选项控制单个请求是否启用加密:
// 单个请求启用加密
request('/api/data', {
encryption: {
enabled: true,
key: 'custom-key' // 可选,使用自定义密钥
}
});
// 单个请求禁用加密
request('/api/data', {
encryption: {
enabled: false
}
});
config/request/requestErrorConfig.ts)统一处理请求错误,包括网络错误、业务错误等。
可以通过配置 skipErrorHandler: true 来跳过默认的错误处理,自行处理错误。
在 services 目录下按模块定义 API 服务,每个模块包含类型定义和接口实现:
services/[module]/typings.d.ts)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>;
}
services/[module]/index.ts)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,
});
}
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) {
// 处理错误
}
};
};
// 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,
};
};
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[]; // 响应拦截器
}
项目提供了统一的错误处理机制:
可以通过配置 skipErrorHandler: true 来跳过默认的错误处理,自行处理错误。
默认情况下,请求会直接返回服务器响应的数据。如果需要获取完整的响应信息(包括状态码、响应头等),可以配置 getResponse: true。
示例:
// 获取完整响应信息
const response = await request('/api/data', {
getResponse: true,
});
// response 包含完整的响应信息
console.log(response.status); // HTTP 状态码
console.log(response.headers); // 响应头
console.log(response.data); // 响应数据
services 目录下按模块组织typings.d.ts(类型定义)和 index.ts(接口实现)requireToken: true项目提供了统一的本地存储工具,支持 localStorage 和 sessionStorage,并支持数据加密存储。
src/
└── utils/
├── storage/ # 存储工具
│ ├── index.ts # 存储工具入口
│ └── types.ts # 类型定义
├── localUtils.ts # localStorage 工具
└── sessionUtils.ts # sessionStorage 工具
通过环境变量配置加密密钥:
VITE_STORAGE_ENCRYPTION_KEY=your-encryption-key
通过环境变量配置存储命名空间:
VITE_APP_STORAGE_NAMESPACE=your-app-namespace
import { createLocalTools, createSessionTools } from '@/utils/localUtils';
// 创建 localStorage 工具实例
const ls = createLocalTools({
encryptKey: true, // 是否加密 key
encryptValue: true, // 是否加密 value
});
// 创建 sessionStorage 工具实例
const ss = createSessionTools({
encryptKey: true,
encryptValue: true,
});
// 存储数据
ls.setLocal('userInfo', { name: 'John', age: 30 });
// 获取数据
const userInfo = ls.getLocal<{ name: string; age: number }>('userInfo');
// 删除数据
ls.removeLocal('userInfo');
// 清除所有数据
ls.clearLocal();
// 存储数据
ss.setSession('tempData', { id: 1, status: 'pending' });
// 获取数据
const tempData = ss.getSession<{ id: number; status: string }>('tempData');
// 删除数据
ss.removeSession('tempData');
// 清除所有数据
ss.clearSession();
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;
}