本模板采用React+Vite+Zustand+antd搭建,并配置了eslint+prettier+commitlint统一代码风格和规范,封装了完整的请求函数,并对权限(路由,菜单,组件)加以控制,提供深色模式和浅色模式两种方案。
//如果你想开箱即用直接执行以下命令
git clone https://github.com/Yohan3416/ManageBase-React.git
//使用pnpm安装依赖
pnpm i
//把我们的项目运行起来
pnpm run dev
首先使用vite搭建一个基础的React项目模板
pnpm create vite
为你的项目起一个名称,选择React+Typscript+SWC
随后根据提示执行
pnpm i
pnpm run dev
安装依赖运行起来我们的初始项目
配置项目的husky@8.0.0 + eslint + prettier + commitlint对代码提供一套规范
如果你自己做项目,不在乎代码格式规范和提交规范,可以跳过这一部分
配置eslint
在根目录安装所需的依赖
pnpm i eslint @typescript-eslint/parser @typescript-eslint/plugin eslint-config-prettier eslint-plugin-prettier -D
安装好之后,在根目录创建.eslintrc.cjs和.eslintignore文件
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:react-hooks/recommended",
"prettier",
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh", "@typescript-eslint", "prettier"],
rules: {
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"no-explicit-any": "off",
"react-refresh/only-export-components": "off",
"prettier/prettier": "error",
"arrow-body-style": "off",
"prefer-arrow-callback": "off",
"prettier/prettier": ["error", { endOfLine: "auto" }],
},
};
node_modules/
pnpm-lock.yaml
dist
public
.husky
.vscode
docs
*.md
.eslintrc
.prettierrc
index.html
commitlint.config.cjs
vite.config.ts
配置好之后,在pakage.json文件中添加执行eslint的脚本
//在scripts中添加
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
配置prettier
//执行以下命令添加依赖
pnpm i prettier -D
在项目根目录下创建.prettierrc和.prettierignore文件,关于详细配置,你可以查阅官方文档,我只是给一个我们团队常用的配置。
{
"extends": ["taro/react","plugin:prettier/recommended"],
"plugins": ["prettier"],
"rules": {
"prefer-const":"warn",
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"quotes":"off",
"no-unused-vars":"off",
"array-bracket-spacing":["error","never"],
"no-var":"warn",
"comma-spacing": [
"error",
{
"before": false,
"after": true
}
],
"no-cjs": "off",
"@typescript-eslint/no-unused-vars": "warn",
"no-commonjs": "off",
"import/no-commonjs": "off",
"@typescript-eslint/no-explicit-any": "off"
}
}
node_modules
dist
public
types
.husky
.vscode
docs
*.md
.eslintrc
.prettierrc
commitlint.config.cjs
index.html
在package.json中增加脚本
//在scripts中添加
"format": "prettier --write src/",
代码保存自动格式化
在根目录下新建.vscode文件夹,再在.vscode文件夹之下新建settings.json和extensions.json
{
"editor.formatOnSave": true, // 开启保存文件自动格式化代码
"editor.defaultFormatter": "esbenp.prettier-vscode", // 设置默认的代码格式化工具为 Prettier
"eslint.enable": true
}
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
]
}
配置husky和lint-staged规范代码提交
husky
pnpm i husky@8.0.0 -D
//执行这行命令在根目录生成.husky文件夹
npx husky install
//在package.json新增脚本
"prepare": "husky install",
在.husky文件夹下
// .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# pnpm run lint
pnpm run lint:lint-staged --allow-empty
lint-staged
pnpm i lint-staged -D
在package.json添加配置项和脚本
//package.json
{
"script": {
//...
"lint:lint-staged": "lint-staged",
}
//...
"lint-staged": {
"*.{js,ts,jsx,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{cjs,json}": [
"prettier --write"
]
}
}
commitlint代码提交规范
// 安装依赖
pnpm i @commitlint/cli @commitlint/config-conventional -D
根目录创建commitlint.config.cjs配置文件
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"subject-case": [0],
"type-enum": [
2,
"always",
[
"feat", //新增功能
"fix", //修复缺陷
"docs", //文档变更
"style", //代码格式修正
"refactor", //代码重构
"perf", //性能优化
"test", //添加疏漏测试或者已有测试改动
"build", //构建流程,外部依赖变更
"cli", //修改 CI 配置,脚本
"revert", //回滚 commit
"chore", //对构建过程或辅助工具和库的更改(不影响源文件和测试用例)
],
],
},
};
执行下面的命令生成commit-msg钩子用于git提交信息校验
npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"
配置路由和Ant Design
// 安装react-router-dom
pnpm i react-router-dom
在src/pages新建页面文件夹
这里只做示例写这么多,你可以随便写两个页面组件
先来实现一个懒加载组件,提高首页加载速度
// src/components/LazyComponent/LazyComponent.tsx
import { Suspense, LazyExoticComponent } from "react";
import LazyLoading from "./LazyLoading";
export default function LazyComponent(props: {
lazyChildren: LazyExoticComponent<() => JSX.Element>;
title: string;
}) {
return (
<Suspense fallback={<LazyLoading title={props.title} />}>
<props.lazyChildren />
</Suspense>
);
}
// src/components/LazyComponent/LazyLoading.tsx
export default function LazyLoading(props: { title: string }) {
return (
<div id="loadingRoot">
<div className="loader-container">
<div className="loader">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<h1>{props.title}</h1>
</div>
</div>
);
}
创建src/router/index.tsx文件,在里面对路由进行配置
// src/router/index.tsx
import { Navigate, createBrowserRouter } from "react-router-dom";
import Login from "../pages/Login/Login.tsx";
import MyLayout from "../Layout/Layout.tsx";
import Error403 from "../pages/403.tsx";
import Error404 from "../pages/404.tsx";
import LazyComponent from "../components/LazyComponent/LazyComponent.tsx";
import { lazy } from "react";
const routes = [
{
path: "/login",
element: <Login />,
},
{
path: "/",
element: <MyLayout />,
children: [
{
index: true,
element: <Navigate to={"/databoard"} />,
},
{
path: "/databoard",
element: (
<LazyComponent
lazyChildren={lazy(
() => import("../pages/LayoutPages/DataBoard/DataBoard.tsx"),
)}
title="数据看板"
/>
),
},
{
path: "/cashier",
element: (
<LazyComponent
lazyChildren={lazy(
() => import("../pages/LayoutPages/Cashier/Cashier.tsx"),
)}
title="收银台"
/>
),
},
{
path: "/bookstorage",
element: (
<LazyComponent
lazyChildren={lazy(
() => import("../pages/LayoutPages/BookStorage/BookStorage.tsx"),
)}
title="书籍入库"
/>
),
},
{
path: "/storemanage",
element: (
<LazyComponent
lazyChildren={lazy(
() => import("../pages/LayoutPages/StoreManage/StoreManage.tsx"),
)}
title="库存管理"
/>
),
},
{
path: "/OrderManage",
element: (
<LazyComponent
lazyChildren={lazy(
() => import("../pages/LayoutPages/OrderManage/OrderMannage.tsx"),
)}
title="订单管理"
/>
),
},
{
path: "/department",
children: [
{
index: true,
element: <Navigate to={"/department/add"} />,
},
{
path: "/department/add",
element: (
<LazyComponent
lazyChildren={lazy(
() =>
import("../pages/LayoutPages/Department/Department.tsx"),
)}
title="部门管理"
/>
),
},
],
},
{
path: "/logrecord",
element: (
<LazyComponent
lazyChildren={lazy(
() => import("../pages/LayoutPages/LogRecord/LogRecord.tsx"),
)}
title="日志记录"
/>
),
},
],
},
{
path: "/403",
element: <Error403 />,
},
{
path: "/404",
element: <Error404 />,
},
{
path: "*",
element: <Navigate to={"/404"} />,
},
];
const router = createBrowserRouter(routes) as ReturnType<
typeof createBrowserRouter
>;
export default router;
修改App.tsx文件
import { RouterProvider } from 'react-router-dom'
import Router from './router';
function App() {
return (
<RouterProvider router={router} />
);
}
export default App;
使用antd优化403和404页面
安装Ant Design
pnpm install antd
// src/pages/403.tsx
import React from 'react';
import { Result, Button } from 'antd';
import { useHistory } from 'react-router-dom';
const Error403 = () => {
const history = useHistory();
const handleBack = () => {
history.push('/'); // 跳转回主页或其他你希望的路径
};
return (
<Result
status="403"
title="403"
subTitle="抱歉,您没有权限访问此页面."
extra={<Button type="primary" onClick={handleBack}>返回首页</Button>}
/>
);
};
export default Error403;
// src/pages/404.tsx
import React from 'react';
import { Result, Button } from 'antd';
import { useHistory } from 'react-router-dom';
const Error404 = () => {
const history = useHistory();
const handleBack = () => {
history.push('/'); // 跳转回主页或其他你希望的路径
};
return (
<Result
status="404"
title="404"
subTitle="抱歉,您访问的页面不存在."
extra={<Button type="primary" onClick={handleBack}>返回首页</Button>}
/>
);
};
export default Error404;
状态管理ZuStand配置(持久化)
npm install zustand
在src下新建store文件夹
// src/store/index.ts 用于整体导出
export { useUserStore } from "./user";
// src/store/user.ts 用于存放用户信息
import { create } from "zustand";
import { persist } from "zustand/middleware"; //用于持久化存储
import { produce } from "immer";
type Action = {
updateToken: (token: string) => void;
updateRole: (role: string) => void;
updateUserName?: (username: string) => void;
};
interface State {
username: string;
role: string;
token: string;
} //用于规定本仓库的数据结构
export const useUserStore = create<State & Action>()(
persist( //使用持久化组件包裹
(set) => ({
username: "默认名称",
role: "admin",
token: "",
updateToken: (token) =>
set(
produce((state) => {
localStorage.setItem("token", token);
state.token = token;
}),
),
updateRole: (role) =>
set(
produce((state) => {
state.role = role;
}),
),
updateUsername: (username: string) =>
set(
produce((state) => {
state.username = username;
}),
),
}),
{
name: "user-storage", //在localStorage中对应的key
},
),
);
登录页面搭建
在后期配置Ant自动导入过后,message等组件不可用,我们先封装一下
// src/utils/AntGlobal.tsx
import { App } from "antd";
import type { MessageInstance } from "antd/es/message/interface";
import type { ModalStaticFunctions } from "antd/es/modal/confirm";
import type { NotificationInstance } from "antd/es/notification/interface";
let message: MessageInstance;
let notification: NotificationInstance;
let modal: Omit<ModalStaticFunctions, "warn">;
const AntdGlobal = () => {
const staticFunction = App.useApp();
message = staticFunction.message;
modal = staticFunction.modal;
notification = staticFunction.notification;
return null;
};
export default AntdGlobal;
export { message, notification, modal };
使用Mock模拟登录接口
安装mock
pnpm i mockjs@1.1.0 @types/mockjs@1.0.10 -D
src下新建mock文件夹
import Mock from "mockjs";
Mock.setup({
timeout: "300-800",
});
const checkLogin = (config: { body: string }) => {
const Config = JSON.parse(config.body);
if (Config.username === "superadmin" && Config.password === "123456") {
return {
code: 200,
data: {
username: "SuperAdmin",
token: Mock.Random.guid(),
role: "SuperAdmin",
},
};
} else if (Config.username === "admin" && Config.password === "123456") {
return {
code: 200,
data: {
username: "Admin",
token: Mock.Random.guid(),
role: "Admin",
},
};
} else {
return {
code: 500,
msg: "账号或者密码错误",
};
}
};
Mock.mock(/\/user\/login/, "post", checkLogin);
在main.tsx文件中导入
import "./mock/index.ts";
封装axios和接口
axios可以自行安装
在utils文件夹下新建 request.ts
import axios, {
AxiosError,
AxiosResponse,
InternalAxiosRequestConfig,
} from "axios";
// 创建一个axios实例
const request = axios.create({
baseURL: "http://localhost:3000", //请求的基地址,包含协议,ip,端口
timeout: 5000, //请求超时的时间
});
// 请求拦截器,用于在发出请求前一些操作,一般会携带上token
request.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
return config;
},
(error: AxiosError) => {
return Promise.reject(error);
},
);
// 响应拦截器,用于请求成功后,数据返回后的操作,一般可以在这里判断一下状态码
request.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
(error: AxiosError) => {
return Promise.reject(error);
},
);
export default request;
封装我们的登录接口
// src/apis/login.ts
import request from "../utils/request";
const login = (data: object) => {
return new Promise((resolve, reject) => {
request
.post("/user/login", data)
.then((res: any) => {
resolve(res.data);
})
.catch((err) => {
reject(err);
});
});
};
export default login;
正式搭建
// pages/Login/Login.tsx
import { useState } from "react";
import type { FormProps } from "antd";
import { Button, Checkbox, Form, Input } from "antd";
import { message } from "../../utils/AntdGlobal";
import login from "../../api/login";
import "./Login.scss";
import { useUserStore } from "../../store";
import { useNavigate } from "react-router-dom";
type FieldType = {
username?: string;
password?: string;
remember?: string;
};
function Login() {
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const handleLogin = (values: FieldType) => {
return login(values)
.then((res: any) => {
if (res.code === 200) {
useUserStore.getState().updateToken(res.data.token);
useUserStore.getState().updateRole(res.data.role);
message.success("登录成功");
setTimeout(() => {
navigate("/", { replace: true });
}, 1000);
if (values.remember) {
if (values.username && values.password) {
localStorage.setItem("username", values.username);
localStorage.setItem("password", values.password);
}
} else {
if (localStorage.getItem("username"))
localStorage.removeItem("username");
}
} else {
message.error(res.msg);
}
})
.catch(() => {
message.error("登录失败");
});
};
const onFinish: FormProps<FieldType>["onFinish"] = (values = {}) => {
setIsLoading(true);
handleLogin(values).finally(() => {
setTimeout(() => {
setIsLoading(false);
}, 1000);
});
};
const onFinishFailed: FormProps<FieldType>["onFinishFailed"] = (
errorInfo,
) => {
console.log("Failed:", errorInfo);
};
return (
<div className="loginContainer">
<Form
name="basic"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600, minWidth: 350 }}
initialValues={{ remember: false }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item<FieldType>
label="用户名"
name="username"
rules={[{ required: true, message: "请输入用户名!" }]}
>
<Input />
</Form.Item>
<Form.Item<FieldType>
label="密码"
name="password"
rules={[{ required: true, message: "请输入密码!" }]}
>
<Input.Password />
</Form.Item>
<Form.Item<FieldType>
name="remember"
valuePropName="checked"
wrapperCol={{ offset: 8, span: 16 }}
>
<Checkbox>Remember me</Checkbox>
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit" loading={isLoading}>
登录
</Button>
</Form.Item>
</Form>
</div>
);
}
export default Login;
导航守卫的实现
我们发现,即使在没有登录的情况下,直接访问首页地址,依然能够进入到后台,这不是我们想要的结果
新建文件如下
//router/loader.ts
import { redirect } from "react-router-dom";
export function Loader() {
const tk = localStorage.getItem("token");
if (tk) {
return null;
} else {
return redirect("/login");
}
}
修改router/index.tsx,配置loader
//引入
import { Loader } from "./loader.ts";
//只需要在path = `/`路由中配置loader属性,会在到达该页面和他的子页面时,先执行loader
loader: Loader,
尝试清空本地存储的数据,刷新页面,是不是就重定向到登录页面了
下一期将搭建Layout页面,并根据角色不同获取动态路由进行菜单级权限控制和登出功能