diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..6713236 --- /dev/null +++ b/.env.development @@ -0,0 +1,5 @@ +# 本地环境 +NODE_ENV = 'development' + +# 本地环境接口地址 +VITE_API_URL = '/api' \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..d6c9537 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml new file mode 100644 index 0000000..ddf78bc --- /dev/null +++ b/.github/workflows/release-please.yaml @@ -0,0 +1,17 @@ +name: Release Please + +on: + push: + branches: + - main # 或你使用的默认分支名 + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: GoogleCloudPlatform/release-please-action@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + release-type: node + package-name: 'nirvana' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2aa77d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +pnpm-lock.yaml + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +PROBLEMS.md +mock/*.mjs diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..7241764 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no-install commitlint --edit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..d24fdfc --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 0000000..46e88b0 --- /dev/null +++ b/.stylelintignore @@ -0,0 +1,2 @@ +/dist/* +/public/* diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000..40db42c --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "stylelint-config-standard" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..89778ae --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# Nirvara🚀 + +## 项目简介 + +🚀🚀🚀 Nirvara 是一个使用 React 18 构建的后台管理系统框架,整合了 React-Router v6、React Hooks、Redux & Redux-Toolkit、TypeScript、Vite 和 Ant-Design。它提供了一个现代化、高效的开发环境,适用于快速构建专业的后台管理界面。 + +## Git 地址 + +[GitHub - Nirvara 项目地址](https://github.com/hesoso/toy) + +## 项目功能 + +- ✨ React 18 & React-Router v6:最新的 React 版本和路由管理 +- ✨ React Hooks & Redux-Toolkit:简化的状态管理 +- ✨ TypeScript:增强代码的稳定性和可维护性 +- ✨ Ant-Design v5:自由度极高的主题定制功能 +- ✨ Vite:快速的构建和热重载,vite-plugin-mock 提供 mock 数据 +- ✨ 使用 Prettier 统一格式化代码,集成 Eslint、Stylelint 代码校验规范 +- ✨ 使用 husky、lint-staged、commitlint、commitizen、cz-git 规范提交信息(项目规范配置) + +### 其他 mock 数据解决方案 + +- [fastmock](https://www.fastmock.site/) +- [mengxuegu](https://mock.mengxuegu.com/) + +## 安装与使用 📑 + +下面是如何在本地环境安装和运行你的项目的步骤: + +```bash +# 克隆仓库 +git clone https://github.com/hesoso/toy.git + +# 进入项目目录 +cd nirvana + +# 安装依赖 +yarn install + +# 启动项目 +yarn start +``` + +## 其它命令 + +```bash +#构建生产版本。 +yarn build + +# 校验代码规范 +yarn lint + +# 校验css规范 +yarn lint:css +``` + +## 提交规范 + +为了确保代码质量和提交的一致性,提交代码需遵循以下规范: + +- 功能提交:feat: 新功能描述 +- 修复提交:fix: 修复问题描述 +- 文档提交:docs: 更新文档描述 +- 样式提交:style: 样式更改(不影响代码运行的变动) +- 详见🔗 [提交规范官方配置文件](https://github.com/conventional-changelog/commitlint/blob/master/%40commitlint/config-conventional/index.js) + +## 文件目录说明 📚 + +```text +Nirvara +├─ .github # 用于指定 Release-Please Action 配置 +├─ .husky # 用于配置Git钩子(如pre-commit)的目录 +├─ .vscode # vscode推荐配置 +├─ mock # 模拟接口响应及动态随机数据 +├─ public +├─ src +│ ├─ api # API 接口管理 +│ ├─ assets # 静态资源文件 +│ ├─ components # 全局组件 +│ ├─ config # 基础配置项 +│ ├─ constant # 项目中用到的一些常量 +│ ├─ hooks # 自定义钩子 +│ ├─ http # axios二次封装 +│ ├─ routes # 路由管理 +│ ├─ store # redux store +│ ├─ typings # ts 声明文件 +│ ├─ views # 所有页面 +│ ├─ App.tsx # 入口页面 +│ ├─ main.tsx # 入口文件 +│ └─ env.d.ts # vite 声明文件 +├─ .env.development # 开发环境配置 +├─ .eslintrc.js # eslint 校验代码配置 +├─ .gitignore # git 忽略配置 +├─ .stylelintignore # stylelint 样式校验忽略配置 +├─ .stylelintrc.js # stylelint 样式校验配置 +├─ CHANGELOG.md # 项目更新日志 +├─ commitlint.config.js # git 提交规范配置 +├─ index.html # 入口 html +├─ package.json # 依赖包管理 +├─ README.md # 描述和解释项目的文档 +├─ SCRIPTS.md # 对于一些脚本命令的总结 +├─ TODOS.md # 用于记录一些需要在接下来去完善的事情 +├─ tsconfig.json # typeScript 项目标准配置文件 +├─ tsconfig.node.json # 专门为 Node.js 环境定制的 typeScript 配置文件 +└─ vite.config.ts # vite 配置 +``` + +## 浏览器支持 + +- 本地开发推荐使用 Chrome 最新版浏览器 [Download](https://www.google.com/intl/zh-CN/chrome/)。 +- 生产环境支持现代浏览器,不在支持 IE 浏览器,更多浏览器可以查看 [Can I Use Es Module](https://caniuse.com/?search=ESModule)。 + +| ![IE](https://i.imgtg.com/2023/04/11/8z7ot.png) | ![Edge](https://i.imgtg.com/2023/04/11/8zr3p.png) | ![Firefox](https://i.imgtg.com/2023/04/11/8zKiU.png) | ![Chrome](https://i.imgtg.com/2023/04/11/8zNrx.png) | ![Safari](https://i.imgtg.com/2023/04/11/8zeGj.png) | +| :---------------------------------------------: | :-----------------------------------------------: | :--------------------------------------------------: | :-------------------------------------------------: | :-------------------------------------------------: | +| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | + +## 联系方式 + +如果你有任何问题或建议,可以直接提issues,或者通过邮箱联系我: + +- Github: https://github.com/hesoso/toy/issues +- Email: 240502633@qq.com diff --git a/SCRIPTS.md b/SCRIPTS.md new file mode 100644 index 0000000..d3b4fa5 --- /dev/null +++ b/SCRIPTS.md @@ -0,0 +1,85 @@ +# 常用命令说明 + +## ESLint 相关命令 + +- **`--cache`** + + - **描述**:启用缓存机制,只检查更改的文件。 + - **示例**: + ```bash + eslint --cache + ``` + +- **`--fix`** + + - **描述**:自动修复代码中的错误和风格问题。 + - **示例**: + ```bash + eslint --fix + ``` + +- **`--ext`** + + - **描述**:指定需要检查的文件扩展名。 + - **示例**: + ```bash + eslint --ext .js,.jsx,.ts,.tsx + ``` + +- **`--no-cache`** + + - **描述**:运行检查时不使用缓存。 + - **示例**: + ```bash + eslint --no-cache + ``` + +- **`--quiet`** + + - **描述**:只报告错误,忽略警告。 + - **示例**: + ```bash + eslint --quiet + ``` + +- **`--max-warnings`** + + - **描述**:设置警告的最大数量,超过则退出非零状态。 + - **示例**: + ```bash + eslint --max-warnings 10 + ``` + +- **`--format`** + - **描述**:指定输出报告的格式。 + - **示例**: + ```bash + eslint --format stylish + ``` + +## Prettier 相关命令 + +- **`--write`** + - **描述**:格式化并覆盖指定文件。 + - **示例**: + ```bash + prettier --write + ``` + +## 配置相关命令 + +- **`--config`** + + - **描述**:指定使用的配置文件。 + - **示例**: + ```bash + eslint --config .eslintrc.js + prettier --config .prettierrc + ``` + +- **`--ignore-path`** + - **描述**:指定忽略文件的路径。 + - **示例**: + ```bash + eslint --ignore-path .eslintignore + ``` diff --git a/TODOS.md b/TODOS.md new file mode 100644 index 0000000..7d81db4 --- /dev/null +++ b/TODOS.md @@ -0,0 +1,39 @@ +# Todo List + +此文件用于记录前端项目中待完成的功能和任务。每个任务应当明确、具体,并且尽可能分配给特定的团队成员。 + +## 待办事项 📚 + +### 立即执行 🚀 + +- 🔨 关于 keep-alive 的实现 + +### 短期目标 🚀 + +- 🔨 优化页面加载速度。 +- 🔨 添加用户个人信息页面。 +- 🔨 实现表单验证。 + +### 长期目标 🚀 + +- 🔨 实现国际化,支持多语言。 +- 🔨 开发移动端友好的用户界面。 +- 🔨 增加无障碍功能支持。 + +## 正在进行中的任务 🚀 + +> 这里记录正在进行中的任务。 + +## 完成的任务 🚀 + +> 这里记录已经完成的任务。 + +## 备注 🌈 + +- 请在开始任何新任务前更新此文件。 +- 确保任务的描述尽可能清晰和具体。 +- 每周会进行一次任务审查会议。 + +--- + +更新日期:2023 年 12 月 19 日 diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..5b82653 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,2 @@ +/* eslint-disable no-undef */ +module.exports = { extends: ["@commitlint/config-conventional"] }; diff --git a/index.html b/index.html new file mode 100644 index 0000000..6f48b1a --- /dev/null +++ b/index.html @@ -0,0 +1,22 @@ + + + + + + + + Nirvana + + + + +
+ + + + \ No newline at end of file diff --git a/mock/data.ts b/mock/data.ts new file mode 100644 index 0000000..2b524a1 --- /dev/null +++ b/mock/data.ts @@ -0,0 +1,53 @@ +export const users = [ + { + key: "1008611", + username: "Admin", + password: "Aa111111!", + gender: "未知", + age: 99, + address: "蓬莱仙岛", + phone: 13066668888, + tags: ["master", "loser"], + }, + { + key: "1001011", + username: "Trump", + password: "Aa111111!", + gender: "Male", + age: 70, + address: "Hawail", + phone: 13066668888, + tags: ["matainer"], + }, +]; + +export const roles = [ + { + key: "1", + name: "master", + status: "正常", + auth: "未知", + desc: "主人", + }, + { + key: "12", + name: "guest", + status: "正常", + auth: "未知", + desc: "访客", + }, + { + key: "3", + name: "developer", + status: "正常", + auth: "未知", + desc: "开发者", + }, + { + key: "4", + name: "reporter", + status: "禁用", + auth: "未知", + desc: "报告者", + }, +]; diff --git a/mock/error.ts b/mock/error.ts new file mode 100644 index 0000000..3c61e1d --- /dev/null +++ b/mock/error.ts @@ -0,0 +1,84 @@ +// 模拟错误接口 + +export default [ + { + url: "/api/getError400", + method: "get", + statusCode: 400, + response: () => { + return { msg: "这是一个模拟400错误接口" }; + }, + }, + { + url: "/api/getError401", + method: "get", + statusCode: 401, + response: () => { + return { msg: "这是一个模拟401错误接口" }; + }, + }, + { + url: "/api/getError403", + method: "get", + statusCode: 403, + response: () => { + return { msg: "这是一个模拟403错误接口" }; + }, + }, + { + url: "/api/getError404", + method: "get", + statusCode: 404, + response: () => { + return { msg: "这是一个模拟404错误接口" }; + }, + }, + { + url: "/api/getError405", + method: "get", + statusCode: 405, + response: () => { + return { msg: "这是一个模拟405错误接口" }; + }, + }, + { + url: "/api/getError408", + method: "get", + statusCode: 408, + response: () => { + return { msg: "这是一个模拟408错误接口" }; + }, + }, + { + url: "/api/getError500", + method: "get", + statusCode: 500, + response: () => { + return { msg: "这是一个模拟500错误接口" }; + }, + }, + { + url: "/api/getError502", + method: "get", + statusCode: 502, + response: () => { + return { msg: "这是一个模拟502错误接口" }; + }, + }, + { + url: "/api/getError503", + method: "get", + statusCode: 503, + response: () => { + return { msg: "这是一个模拟503错误接口" }; + }, + }, + { + url: "/api/getError504", + method: "get", + statusCode: 504, + response: () => { + return { msg: "这是一个模拟504错误接口" }; + }, + }, +]; diff --git a/mock/system.ts b/mock/system.ts new file mode 100644 index 0000000..5a1e5dc --- /dev/null +++ b/mock/system.ts @@ -0,0 +1,20 @@ +import { roles } from "./data"; + +export default [ + // 查询角色 + { + url: "/api/getRoles", + method: "get", + response: () => { + return { code: 0, data: roles }; + }, + }, + // 查询菜单 + { + url: "/api/getMenus", + method: "get", + response: () => { + return { code: 0, data: [] }; + }, + }, +]; diff --git a/mock/users.ts b/mock/users.ts new file mode 100644 index 0000000..7c02a38 --- /dev/null +++ b/mock/users.ts @@ -0,0 +1,70 @@ +import faker from "faker"; +import { signals } from "../src/constant/signals"; +import { users } from "./data"; + +export default [ + // 登录接口 + { + url: "/api/login", + method: "post", + response: ({ body }) => { + const { upper, lower } = body; + const validate = signals.some((signal) => { + if (typeof signal.lower === "string") { + return signal.upper === upper && signal.lower === lower; + } else { + return signal.upper === upper && signal.lower.includes(lower!); + } + }); + if (validate) { + return { + code: 0, + msg: "登录成功", + data: { + userId: Date.now(), + userName: upper.slice(-3), + }, + }; + } else { + return { + code: 403, + msg: "用户名或密码错误", + data: null, + }; + } + }, + }, + // 查询所有用户 + { + url: "/api/getUsers", + method: "get", + response: () => { + const fakeUsers = Array.from({ length: 35 }).map((item, index) => ({ + key: index, + username: faker.name.firstName, + password: "Aa111111!", + gender: faker.name.gender, + age: 15 + Math.round(Math.random() * 20), + address: faker.address.cityName, + phone: faker.phone.phoneNumber, + tags: ["developer"], + })); + const allUsers = [...users, ...fakeUsers]; + return { code: 0, data: allUsers, total: allUsers.length }; + }, + }, + // 动态参数 + { + url: "/api/user/:id", + method: "get", + response: ({ params }) => { + return { + code: 0, + data: { + id: params.id, + // name: faker.name.findName(), + }, + }; + }, + }, +]; diff --git a/package.json b/package.json new file mode 100644 index 0000000..d21d0a8 --- /dev/null +++ b/package.json @@ -0,0 +1,55 @@ +{ + "name": "nirvana", + "private": true, + "version": "0.0.0", + "scripts": { + "start": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "lint:css": "stylelint '**/*.scss'" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.{css,scss}": "stylelint --fix" + }, + "dependencies": { + "@ant-design/icons": "^5.2.6", + "@reduxjs/toolkit": "^2.0.1", + "antd": "^5.12.2", + "axios": "^1.6.2", + "nprogress": "^0.2.0", + "pinyin-pro": "^3.18.5", + "qs": "^6.11.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-redux": "^9.0.4", + "react-router-dom": "^6.19.0", + "redux": "^5.0.0" + }, + "devDependencies": { + "@commitlint/cli": "^18.4.3", + "@commitlint/config-conventional": "^18.4.3", + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", + "@vitejs/plugin-react-swc": "^3.5.0", + "eslint": "^8.53.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.4", + "faker": "^6.6.6", + "husky": "^8.0.3", + "lint-staged": "^15.2.0", + "prettier": "^3.1.1", + "sass": "^1.69.5", + "stylelint": "^16.0.2", + "stylelint-config-standard": "^35.0.0", + "typescript": "^5.2.2", + "vite": "^5.0.0", + "vite-plugin-mock": "^3.0.0" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e1fc5d6 Binary files /dev/null and b/public/favicon.ico differ diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..773dc40 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,17 @@ +import { useMemo, createContext } from 'react' +import WrapperRoutes from './routes' + +const Context = createContext({ name: '' }) +console.log(Context) + +export default function App() { + const contextValue = useMemo(() => ({ name: '' }), []) + + return ( + <> + + + + + ) +} diff --git a/src/api/system.ts b/src/api/system.ts new file mode 100644 index 0000000..3eaa3d6 --- /dev/null +++ b/src/api/system.ts @@ -0,0 +1,13 @@ +import http from "@/http"; + +// 获取角色列表 +export const getRoles = () => { + return http.get("/getRoles"); +}; + +// 获取菜单列表 +export const getMenus = () => { + return http.get("/getMenus"); +}; + +export default { getRoles, getMenus }; diff --git a/src/api/user.ts b/src/api/user.ts new file mode 100644 index 0000000..bb7b28f --- /dev/null +++ b/src/api/user.ts @@ -0,0 +1,18 @@ +import http from "@/http"; + +// 用户登录 +export const login = (params) => { + return http.post("/login", params); +}; + +// 用户登出 +export const loginOut = (params) => { + return http.post("/loginout", params); +}; + +// 获取用户列表 +export const getUsers = () => { + return http.get("/getUsers"); +}; + +export default { login, loginOut, getUsers }; diff --git a/src/assets/bg.jpg b/src/assets/bg.jpg new file mode 100755 index 0000000..f3f2d43 Binary files /dev/null and b/src/assets/bg.jpg differ diff --git a/src/assets/bg2.jpg b/src/assets/bg2.jpg new file mode 100755 index 0000000..73f6360 Binary files /dev/null and b/src/assets/bg2.jpg differ diff --git a/src/assets/logo.jpeg b/src/assets/logo.jpeg new file mode 100755 index 0000000..1ac1b6f Binary files /dev/null and b/src/assets/logo.jpeg differ diff --git a/src/components/Fallback.tsx b/src/components/Fallback.tsx new file mode 100644 index 0000000..f63dda8 --- /dev/null +++ b/src/components/Fallback.tsx @@ -0,0 +1,11 @@ +import { Flex, Spin } from "antd"; + +function Fallback() { + return ( + + + + ); +} + +export default Fallback; diff --git a/src/components/NotFound.tsx b/src/components/NotFound.tsx new file mode 100644 index 0000000..06014f0 --- /dev/null +++ b/src/components/NotFound.tsx @@ -0,0 +1,5 @@ +function NotFound() { + return
NotFound
+} + +export default NotFound diff --git a/src/components/Poem/index.tsx b/src/components/Poem/index.tsx new file mode 100644 index 0000000..c4ccb6c --- /dev/null +++ b/src/components/Poem/index.tsx @@ -0,0 +1,5 @@ +import React from 'react' + +export default function Poem() { + return
index
+} diff --git a/src/config/nprogress.ts b/src/config/nprogress.ts new file mode 100644 index 0000000..111664f --- /dev/null +++ b/src/config/nprogress.ts @@ -0,0 +1,13 @@ +import NProgress from "nprogress"; + +import "nprogress/nprogress.css"; + +NProgress.configure({ + easing: "ease", // 动画方式 + speed: 500, // 递增进度条的速度 + showSpinner: true, // 是否显示加载ico + trickleSpeed: 200, // 自动递增间隔 + minimum: 0.3, // 初始化时的最小百分比 +}); + +export default NProgress; diff --git a/src/constant/menus.tsx b/src/constant/menus.tsx new file mode 100644 index 0000000..bae6416 --- /dev/null +++ b/src/constant/menus.tsx @@ -0,0 +1,93 @@ +import type { MenuProps } from "antd"; +import { + HomeOutlined, + AppstoreOutlined, + SettingOutlined, + FormOutlined, + BugOutlined, + SmileOutlined, + TeamOutlined, +} from "@ant-design/icons"; + +const siderMenus = [ + { + key: "home", + label: "首页", + icon: , + }, + { + key: "comp", + label: "组件", + icon: , + children: [ + { + key: "comp/pinyin", + label: "拼音", + }, + ], + }, + { + key: "editor", + label: "编辑器", + icon: , + }, + { + key: "exception", + label: "异常页面", + icon: , + }, + { + key: "tips", + label: "结果页面", + icon: , + disabled: true, + }, + { + key: "auth", + label: "权限测试", + icon: , + }, + { + key: "setting", + label: "系统设置", + icon: , + children: [ + { + key: "setting/users", + label: "用户管理", + }, + { + key: "setting/roles", + label: "角色管理", + }, + { + key: "setting/menus", + label: "菜单管理", + }, + ], + }, +]; + +const userMenus: MenuProps["items"] = [ + { + label: "江南无所有", + key: "0", + }, + { + label: "聊赠一枝春", + key: "1", + }, + { + label: "自定义主题", + key: "2", + }, + { + type: "divider", + }, + { + label: "退出登录", + key: "3", + }, +]; + +export { siderMenus, userMenus }; diff --git a/src/constant/signals.ts b/src/constant/signals.ts new file mode 100644 index 0000000..7f9b6f9 --- /dev/null +++ b/src/constant/signals.ts @@ -0,0 +1,18 @@ +export const signals = [ + { + upper: "天王盖地虎", + lower: ["宝塔镇河妖", "提莫一米五"], + }, + { + upper: "力拔山兮气盖世", + lower: "时不利兮骓不逝", + }, + { + upper: "地振高岗,一派溪山千古秀", + lower: "门朝大海,三河合水万里流", + }, + { + upper: "山有木兮木有枝", + lower: "心悦君兮君不知", + }, +]; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..00df4da --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,6 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' +import type { RootState, AppDispatch } from '../store' + +export const useAppDispatch: () => AppDispatch = useDispatch + +export const useAppSelector: TypedUseSelectorHook = useSelector diff --git a/src/http/helper/canceler.ts b/src/http/helper/canceler.ts new file mode 100644 index 0000000..b3cb4dc --- /dev/null +++ b/src/http/helper/canceler.ts @@ -0,0 +1,35 @@ +import axios, { AxiosRequestConfig, Canceler } from "axios"; +import qs from "qs"; + +// 声明一个 Map 用于存储每个请求的标识 和 取消函数 +const pengdingMap = new Map(); + +const getPendingKey = ({ method, url, data, params }: AxiosRequestConfig) => { + return `${method}&${url}${qs.stringify(data)}${qs.stringify(params)}`; +}; + +const add = (config: AxiosRequestConfig) => { + const key = getPendingKey(config); + config.cancelToken = + config.cancelToken || + new axios.CancelToken((cancel) => { + if (pengdingMap.has(key)) return; + pengdingMap.set(key, cancel); + }); +}; + +const remove = (config: AxiosRequestConfig) => { + const key = getPendingKey(config); + if (pengdingMap.has(key)) { + const cancel = pengdingMap.get(key); + cancel?.(); + pengdingMap.delete(key); + } +}; + +const removeAll = () => { + pengdingMap.forEach((cancel) => cancel?.()); + pengdingMap.clear(); +}; + +export default { add, remove, removeAll }; diff --git a/src/http/helper/checkStatus.ts b/src/http/helper/checkStatus.ts new file mode 100644 index 0000000..6fcf177 --- /dev/null +++ b/src/http/helper/checkStatus.ts @@ -0,0 +1,44 @@ +import { message } from "antd"; +/** + * @description: 校验网络请求状态码 + * @param {Number} status + * @return void + */ +const checkStatus = (status: number) => { + switch (status) { + case 400: + message.error("请求失败!请您稍后重试"); + break; + case 401: + message.error("登录失效!请您重新登录"); + break; + case 403: + message.error("当前账号无权限访问!"); + break; + case 404: + message.error("你所访问的资源不存在!"); + break; + case 405: + message.error("请求方式错误!请您稍后重试"); + break; + case 408: + message.error("请求超时!请您稍后重试"); + break; + case 500: + message.error("服务异常!"); + break; + case 502: + message.error("网关错误!"); + break; + case 503: + message.error("服务不可用!"); + break; + case 504: + message.error("网关超时!"); + break; + default: + message.error("请求失败!"); + } +}; + +export default checkStatus; diff --git a/src/http/helper/retry.ts b/src/http/helper/retry.ts new file mode 100644 index 0000000..c0f51a2 --- /dev/null +++ b/src/http/helper/retry.ts @@ -0,0 +1,46 @@ +import type { AxiosInstance, AxiosError, AxiosRequestConfig } from "axios"; + +type WarpConfig = AxiosRequestConfig & { retryCount: number }; +type WarpAxiosError = AxiosError & { config: WarpConfig }; + +const MAX_RETRIES = 3; +const RETRY_DELAY = 3 * 1000; + +/** + * @description: 错误重试机制 + * @param axiosInstance + * @param error + * @param callback + * @return void + */ +const useRetry = ( + axiosInstance: AxiosInstance, + error: WarpAxiosError, + callback: () => void, +) => { + const config = error.config; + // todoo 这里不同的请求是否会触发重试次数累计 + // 若config不存在或重试次数已达最大值,抛出错误 + if (!config || config.retryCount >= MAX_RETRIES) { + callback(); + return Promise.reject(error); + } + + // 设置重试次数 + config.retryCount = config.retryCount || 0; + + // 根据状态码判断是否重试 + if (error.response?.status === 500) { + config.retryCount += 1; + const backoff = new Promise((resolve) => { + setTimeout(() => { + resolve(void 0); + }, RETRY_DELAY); + }); + return backoff.then(() => axiosInstance(config)); + } else { + callback(); + } +}; + +export default useRetry; diff --git a/src/http/index.ts b/src/http/index.ts new file mode 100644 index 0000000..df1fb98 --- /dev/null +++ b/src/http/index.ts @@ -0,0 +1,68 @@ +import axios, { + AxiosInstance, + AxiosResponse, + InternalAxiosRequestConfig, +} from "axios"; +import nprogress from "@/config/nprogress"; +import useRetry from "./helper/retry"; +import canceler from "./helper/canceler"; +import checkStatus from "./helper/checkStatus"; + +const axiosInstance: AxiosInstance = axios.create({ + baseURL: import.meta.env.VITE_API_URL, + timeout: 10000, + withCredentials: true, +}); + +axiosInstance.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + nprogress.start(); + canceler.add(config); + return config; + }, + (error) => Promise.reject(error), +); + +axiosInstance.interceptors.response.use( + (response: AxiosResponse) => { + nprogress.done(); + canceler.remove(response.config); + return response.data; + }, + (error) => { + useRetry(axiosInstance, error, () => { + nprogress.done(); + canceler.remove(error.config); + error.response && checkStatus(error.response.status); + }); + }, +); + +interface ResponseBase { + code: number; + msg: string; + total?: number; +} + +interface ReponseResult extends ResponseBase { + data: T; +} + +type ReponsePromise = Promise>; + +const http = { + get(url: string, params: object = {}): ReponsePromise { + return axiosInstance.get(url, { ...params }); + }, + post(url: string, params: object = {}): ReponsePromise { + return axiosInstance.post(url, { ...params }); + }, + put(url: string, params: object = {}): ReponsePromise { + return axiosInstance.put(url, { ...params }); + }, + delete(url: string, params: object = {}): ReponsePromise { + return axiosInstance.delete(url, { ...params }); + }, +}; + +export default http; diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..28b1f24 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { Provider } from "react-redux"; +import { BrowserRouter } from "react-router-dom"; + +import store from "./store"; +import App from "./App.tsx"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + + , +); diff --git a/src/routes/index.tsx b/src/routes/index.tsx new file mode 100644 index 0000000..e2e9017 --- /dev/null +++ b/src/routes/index.tsx @@ -0,0 +1,86 @@ +import { lazy, Suspense, ReactNode } from "react"; +import { Navigate, useRoutes } from "react-router-dom"; +import type { RouteObject } from "react-router-dom"; +import { Result } from "antd"; + +import Layout from "../views/layout"; +import Login from "@/views/login"; +import Home from "@/views/home"; + +import Fallback from "@/components/Fallback"; + +// 编辑器 +const LazyEditor = lazy(() => import("../views/editor")); +// 异常页面 +const LazyException = lazy(() => import("../views/exception")); +// 权限测试页面 +const LazyAuth = lazy(() => import("../views/auth")); +// 组件 - 拼音 +const LazyPinyin = lazy(() => import("../views/comp/pinyin")); +// 设置 - 用户管理 +const LazyUsers = lazy(() => import("../views/setting/users")); +// 设置 - 角色管理 +const LazyRoles = lazy(() => import("../views/setting/roles")); +// 设置 - 菜单管理 +const LazyMenus = lazy(() => import("../views/setting/menus")); + +const lazyLood = (element: ReactNode) => { + return }>{element}; +}; + +const routes: RouteObject[] = [ + { + path: "/", + element: , + }, + { + path: "/login", + element: , + }, + { + path: "/", + element: , + children: [ + { + path: "/home", + element: , + }, + { + path: "/comp/pinyin", + element: lazyLood(), + }, + { + path: "/editor", + element: lazyLood(), + }, + { + path: "/exception", + element: lazyLood(), + }, + { + path: "/auth", + element: lazyLood(), + }, + { + path: "/setting/users", + element: lazyLood(), + }, + { + path: "/setting/roles", + element: lazyLood(), + }, + { + path: "/setting/menus", + element: lazyLood(), + }, + ], + }, + { + path: "*", + element: , + }, +]; + +const WrapperRoutes = () => useRoutes(routes); + +export default WrapperRoutes; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..c14733b --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,14 @@ +import { configureStore } from '@reduxjs/toolkit' +import menu from './slice/menu' + +const store = configureStore({ + reducer: { menu } +}) + +export default store + +// 从 store 本身推断出 `RootState` 和 `AppDispatch` types +export type RootState = ReturnType + +// 类型推断: { menu: MenuState } +export type AppDispatch = typeof store.dispatch diff --git a/src/store/slice/menu.ts b/src/store/slice/menu.ts new file mode 100644 index 0000000..67a6e9f --- /dev/null +++ b/src/store/slice/menu.ts @@ -0,0 +1,30 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import type { RootState } from "../index"; + +type MenuState = { + breadcrumbs: Array<{ title: string; key: string }>; +}; + +export const menuSlice = createSlice({ + name: "menu", + initialState: { + value: { + breadcrumbs: [], + }, + }, + reducers: { + setMenu: ( + state: { value: MenuState }, + action: PayloadAction, + ) => { + state.value.breadcrumbs = action.payload.breadcrumbs; + console.log(state.value.breadcrumbs, action.payload); + }, + }, +}); + +export const { setMenu } = menuSlice.actions; + +export const selectMenu = (state: RootState) => state.menu.value; + +export default menuSlice.reducer; diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts new file mode 100644 index 0000000..9185c0e --- /dev/null +++ b/src/typings/index.d.ts @@ -0,0 +1 @@ +type BaseTheme = "dark" | "light"; diff --git a/src/typings/localstorage.d.ts b/src/typings/localstorage.d.ts new file mode 100644 index 0000000..f1552fb --- /dev/null +++ b/src/typings/localstorage.d.ts @@ -0,0 +1,6 @@ +type LocalKey = "LOCAL_BASE_THEME" | "LOCAL_USER_NAME"; + +interface Storage { + getItem(key: LocalKey): string | null; + setItem(key: LocalKey, value: string): void; +} diff --git a/src/utils/index.tsx b/src/utils/index.tsx new file mode 100644 index 0000000..d89021c --- /dev/null +++ b/src/utils/index.tsx @@ -0,0 +1,13 @@ +/** + * 数组转为select组件所需要的options + */ +function obj2options(arr, labelField, valueField) { + return arr.map((item) => { + return { + label: item[labelField], + value: item[valueField] + } + }) +} + +export { obj2options } diff --git a/src/views/auth/index.tsx b/src/views/auth/index.tsx new file mode 100644 index 0000000..7bbb46c --- /dev/null +++ b/src/views/auth/index.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const Auth = () => { + return
Auth
; +}; + +export default Auth; diff --git a/src/views/comp/pinyin/index.tsx b/src/views/comp/pinyin/index.tsx new file mode 100644 index 0000000..65ad07f --- /dev/null +++ b/src/views/comp/pinyin/index.tsx @@ -0,0 +1,54 @@ +import { Card, Flex } from "antd"; +import { pinyin } from "pinyin-pro"; + +const poem1 = `折花逢驿使,寄与陇头人。江南无所有,聊赠一枝春。`; +const poem2 = `君问归期未有期,巴山夜雨涨秋池。何当共剪西窗烛,却话巴山夜雨时。`; + +type TextChar = { + hz: string; + py: string; +}; + +const convert2pinyin = (texts) => { + const results: Array = []; + texts.forEach((text) => { + results.push({ hz: text, py: pinyin(text.charAt(0)) }); + }); + return results; +}; + +const chars1: Array = convert2pinyin(poem1.split("")); +const chars2: Array = convert2pinyin(poem2.split("")); + +const Home = () => { + return ( + + + + {chars1.map((char) => { + return ( + + {char.py} + {char.hz} + + ); + })} + + + + + {chars2.map((char) => { + return ( + + {char.py} + {char.hz} + + ); + })} + + + + ); +}; + +export default Home; diff --git a/src/views/editor/index.tsx b/src/views/editor/index.tsx new file mode 100644 index 0000000..89c2422 --- /dev/null +++ b/src/views/editor/index.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const Editor = () => { + return
Editor
; +}; + +export default Editor; diff --git a/src/views/exception/index.tsx b/src/views/exception/index.tsx new file mode 100644 index 0000000..03bdf9d --- /dev/null +++ b/src/views/exception/index.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const Exception = () => { + return
Exception
; +}; + +export default Exception; diff --git a/src/views/home/index.tsx b/src/views/home/index.tsx new file mode 100644 index 0000000..4224823 --- /dev/null +++ b/src/views/home/index.tsx @@ -0,0 +1,9 @@ +const Home = () => { + return ( + <> +
home
+ + ); +}; + +export default Home; diff --git a/src/views/layout/Footer.tsx b/src/views/layout/Footer.tsx new file mode 100644 index 0000000..bfeee88 --- /dev/null +++ b/src/views/layout/Footer.tsx @@ -0,0 +1,9 @@ +import { Layout } from 'antd' + +const Footer = () => { + return ( + Nirvana ©2023 Created by Sosohe + ) +} + +export default Footer diff --git a/src/views/layout/Header.tsx b/src/views/layout/Header.tsx new file mode 100644 index 0000000..4e138ce --- /dev/null +++ b/src/views/layout/Header.tsx @@ -0,0 +1,44 @@ +import { Layout, Flex, Breadcrumb, Switch, Dropdown, Button } from "antd"; + +import { userMenus } from "../../constant/menus"; +import { useAppSelector } from "../../hooks"; + +type Props = { + theme: BaseTheme; + onThemeChange: (theme: BaseTheme) => void; +}; + +const Header: React.FC = ({ theme, onThemeChange }) => { + const changeTheme = (checked) => { + const theme = checked ? "dark" : "light"; + onThemeChange(theme); + localStorage.setItem("LOCAL_BASE_THEME", theme); + }; + + const breadcrumbsItems = useAppSelector( + (state) => state.menu.value.breadcrumbs, + ); + + return ( + + + + + + +
+ +
+
+
+
+
+ ); +}; + +export default Header; diff --git a/src/views/layout/Sider.tsx b/src/views/layout/Sider.tsx new file mode 100644 index 0000000..bbb2f39 --- /dev/null +++ b/src/views/layout/Sider.tsx @@ -0,0 +1,60 @@ +import { useState } from "react"; +import { Layout, Menu } from "antd"; +import { useNavigate, useLocation } from "react-router-dom"; + +import { useAppDispatch } from "@/hooks"; +import { setMenu } from "@/store/slice/menu"; +import { siderMenus } from "@/constant/menus"; + +const Sider: React.FC<{ theme: BaseTheme }> = ({ theme }) => { + const navigate = useNavigate(); + + const [collapsed, setCollapsed] = useState(false); + + const dispatch = useAppDispatch(); + + const menuClick = ({ key, keyPath }) => { + navigate(`/${key}`); + + const breadcrumbs: Array<{ title: string; key: string }> = []; + + const setBreadcrumbs = (menus) => { + const menuPath = keyPath.pop(); + menus.forEach(({ key, label, children }) => { + if (key !== menuPath) return; + breadcrumbs.push({ title: label, key }); + keyPath.length && setBreadcrumbs(children); + }); + }; + + setBreadcrumbs(siderMenus); + + dispatch(setMenu({ breadcrumbs })); + }; + + const currentKey = useLocation().pathname.slice(1); + const defaultOpenKeys = [currentKey.split("/")[0]]; + const defaultSelectedKeys = [currentKey]; + + return ( + + + + ); +}; + +export default Sider; diff --git a/src/views/layout/index.scss b/src/views/layout/index.scss new file mode 100644 index 0000000..83e2730 --- /dev/null +++ b/src/views/layout/index.scss @@ -0,0 +1,9 @@ +.layout-wapper { + height: 100vh; +} + +.content-wrapper { + padding: 20px; + height: 100%; + overflow: auto; +} diff --git a/src/views/layout/index.tsx b/src/views/layout/index.tsx new file mode 100644 index 0000000..9cb30b6 --- /dev/null +++ b/src/views/layout/index.tsx @@ -0,0 +1,72 @@ +import { useState, useEffect } from "react"; +import { Outlet } from "react-router-dom"; +import { ConfigProvider, Layout, theme } from "antd"; +import type { MappingAlgorithm, ThemeConfig } from "antd"; + +import Header from "./Header"; +import Footer from "./Footer"; +import Sider from "./Sider"; +import "./index.scss"; + +const Layouts = () => { + const color = localStorage.getItem("LOCAL_BASE_THEME") || "light"; + const [baseTheme, setBaseTheme] = useState(color as BaseTheme); + + // const themeConfig = { algorithm: themeAlgorithm, cssVar: true, hashed: false } + const [themeConfig, setThemeConfig] = useState(); + + function getThemeAlgorithm(baseTheme: BaseTheme): MappingAlgorithm { + const isDark = baseTheme === "dark"; + return (seedToken, mapToken) => { + const baseToken = isDark + ? theme.darkAlgorithm(seedToken, mapToken) + : theme.defaultAlgorithm(seedToken); + return { + ...baseToken, + // colorBgLayout: '#fafafc', // Layout 背景色 + // colorBgContainer: '#fafafc', // 组件容器背景色 + // colorBgElevated: '#32363e', // 悬浮容器背景色 + // fontSize: 14, + colorPrimary: "green", + }; + }; + } + + useEffect(() => { + let compsConfig: ThemeConfig["components"] = { + Layout: { + headerHeight: 50, + }, + }; + if (baseTheme === "light") { + compsConfig = { + Layout: { headerBg: "#fafafc", headerHeight: 50 }, + Menu: { itemBg: "#fafafc" }, + }; + } + const config: ThemeConfig = { + algorithm: getThemeAlgorithm(baseTheme), + components: { ...compsConfig }, + }; + setThemeConfig(config); + }, [baseTheme]); + + return ( + + + + +
+ + + + +