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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Layouts;
diff --git a/src/views/login/index.scss b/src/views/login/index.scss
new file mode 100644
index 0000000..24365ac
--- /dev/null
+++ b/src/views/login/index.scss
@@ -0,0 +1,42 @@
+.login-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100vw;
+ height: 100vh;
+ background-color: #000;
+ background-image: url('../../assets/bg2.jpg');
+ background-repeat: no-repeat;
+ background-size: contain;
+}
+
+.login-box {
+ padding: 0 50px;
+ width: 250px;
+ border-radius: 20px;
+ color: white;
+ background-color: rgba(0 0 0 / 30%);
+ animation: shadow-vary linear 5s infinite;
+}
+
+@keyframes shadow-vary {
+ 0% {
+ box-shadow: 0 0 30px #d14e24;
+ }
+
+ 25% {
+ box-shadow: 0 0 30px black;
+ }
+
+ 50% {
+ box-shadow: 0 0 30px blueviolet;
+ }
+
+ 75% {
+ box-shadow: 0 0 30px black;
+ }
+
+ 100% {
+ box-shadow: 0 0 30px #d14e24;
+ }
+}
diff --git a/src/views/login/index.tsx b/src/views/login/index.tsx
new file mode 100644
index 0000000..f60012b
--- /dev/null
+++ b/src/views/login/index.tsx
@@ -0,0 +1,113 @@
+import React, { useState } from "react";
+import {
+ Button,
+ Form,
+ Select,
+ Input,
+ ConfigProvider,
+ notification,
+} from "antd";
+import { useNavigate } from "react-router-dom";
+import { MehOutlined } from "@ant-design/icons";
+import { signals } from "@/constant/signals";
+import { obj2options } from "@/utils";
+import userApi from "@/api/user";
+import "./index.scss";
+
+type FieldType = {
+ upper?: string;
+ lower?: string;
+};
+
+interface LoginRes {
+ userId: string;
+ username: string;
+}
+
+const Context = React.createContext({ name: "Default" });
+
+export default function Login() {
+ const navigate = useNavigate();
+
+ const [errCount, setErrCount] = useState(0);
+
+ const [api, contextHolder] = notification.useNotification();
+
+ const openNotification = (tips: string, message = "Emmm...") => {
+ api.info({
+ message,
+ description: {() => tips},
+ placement: "topRight",
+ icon: ,
+ });
+ };
+
+ const onFinish = async (values: FieldType) => {
+ const res = await userApi.login(values);
+
+ if (res.code === 0) {
+ navigate("/home", { state: { username: res.data.username } });
+ } else {
+ let errMessage = "摸鱼都不会?";
+ if (errCount === 1) errMessage = "你是真的菜。";
+ if (errCount > 1) errMessage = "去玩扫雷吧!";
+ openNotification(errMessage);
+ setErrCount(errCount + 1);
+ }
+ };
+
+ return (
+
+ {contextHolder}
+
+
暗号
+
+
+ name="upper"
+ rules={[{ required: true, message: "别逼我求你选!" }]}
+ >
+
+
+
+
+ name="lower"
+ rules={[{ required: true, message: "这题不会就换个会的!" }]}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/views/setting/menus/index.tsx b/src/views/setting/menus/index.tsx
new file mode 100644
index 0000000..77dcf84
--- /dev/null
+++ b/src/views/setting/menus/index.tsx
@@ -0,0 +1,98 @@
+import { useEffect, useState } from "react";
+import { Space, Table, Tag, Button } from "antd";
+import type { ColumnsType } from "antd/es/table";
+import { getMenus } from "@/api/system";
+
+interface UserDataType {
+ key: string;
+ username: string;
+ age: number;
+ address: string;
+ phone: string;
+ tags: string[];
+}
+
+const columns: ColumnsType = [
+ {
+ title: "Username",
+ dataIndex: "username",
+ key: "username",
+ },
+ {
+ title: "Password",
+ dataIndex: "password",
+ key: "password",
+ },
+ {
+ title: "Gender",
+ dataIndex: "gender",
+ key: "gender",
+ },
+ {
+ title: "Age",
+ dataIndex: "age",
+ key: "age",
+ },
+ {
+ title: "Address",
+ dataIndex: "address",
+ key: "address",
+ },
+ {
+ title: "Tags",
+ key: "tags",
+ dataIndex: "tags",
+ render: (_, { tags }) => (
+ <>
+ {tags.map((tag) => {
+ let color = tag === "master" ? "green" : "geekblue";
+ if (tag === "matainer") color = "orange";
+ return (
+
+ {tag.toUpperCase()}
+
+ );
+ })}
+ >
+ ),
+ },
+ {
+ title: "Action",
+ key: "action",
+ render: () => (
+
+ Edit
+ Delete
+
+ ),
+ },
+];
+
+const Users = () => {
+ const [menus, setMenus] = useState([]);
+
+ const getMenuList = async () => {
+ const res = await getMenus();
+ if (res.code !== 0) return;
+ setMenus(res.data);
+ };
+
+ useEffect(() => {
+ getMenuList();
+ }, []);
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default Users;
diff --git a/src/views/setting/roles/index.tsx b/src/views/setting/roles/index.tsx
new file mode 100644
index 0000000..9db2eb7
--- /dev/null
+++ b/src/views/setting/roles/index.tsx
@@ -0,0 +1,70 @@
+import { useEffect, useState } from "react";
+import { Space, Table } from "antd";
+import type { ColumnsType } from "antd/es/table";
+import { getRoles } from "@/api/system";
+
+interface RoleDataType {
+ key: string;
+ name: string;
+ status: string;
+ auth: string[];
+ desc: string;
+}
+
+const columns: ColumnsType = [
+ {
+ title: "Name",
+ dataIndex: "name",
+ key: "name",
+ },
+ {
+ title: "Status",
+ dataIndex: "status",
+ key: "status",
+ },
+ {
+ title: "Auth",
+ dataIndex: "auth",
+ key: "auth",
+ },
+ {
+ title: "Desc",
+ dataIndex: "desc",
+ key: "desc",
+ },
+ {
+ title: "Action",
+ key: "action",
+ render: () => (
+
+ Edit
+ Delete
+
+ ),
+ },
+];
+
+const Roles = () => {
+ const [roles, setRoles] = useState([]);
+
+ const getRoleList = async () => {
+ const res = await getRoles();
+ if (res.code !== 0) return;
+ setRoles(res.data);
+ };
+
+ useEffect(() => {
+ getRoleList();
+ }, []);
+
+ return (
+
+ );
+};
+
+export default Roles;
diff --git a/src/views/setting/users/index.tsx b/src/views/setting/users/index.tsx
new file mode 100644
index 0000000..738bafb
--- /dev/null
+++ b/src/views/setting/users/index.tsx
@@ -0,0 +1,95 @@
+import { useEffect, useState } from "react";
+import { Space, Table, Tag } from "antd";
+import type { ColumnsType } from "antd/es/table";
+import { getUsers } from "@/api/user";
+
+interface UserDataType {
+ key: string;
+ username: string;
+ age: number;
+ address: string;
+ phone: string;
+ tags: string[];
+}
+
+const columns: ColumnsType = [
+ {
+ title: "Username",
+ dataIndex: "username",
+ key: "username",
+ },
+ {
+ title: "Password",
+ dataIndex: "password",
+ key: "password",
+ },
+ {
+ title: "Gender",
+ dataIndex: "gender",
+ key: "gender",
+ },
+ {
+ title: "Age",
+ dataIndex: "age",
+ key: "age",
+ },
+ {
+ title: "Address",
+ dataIndex: "address",
+ key: "address",
+ },
+ {
+ title: "Tags",
+ key: "tags",
+ dataIndex: "tags",
+ render: (_, { tags }) => (
+ <>
+ {tags.map((tag) => {
+ let color = tag === "master" ? "green" : "geekblue";
+ if (tag === "matainer") color = "orange";
+ return (
+
+ {tag.toUpperCase()}
+
+ );
+ })}
+ >
+ ),
+ },
+ {
+ title: "Action",
+ key: "action",
+ render: () => (
+
+ Edit
+ Delete
+
+ ),
+ },
+];
+
+const Users = () => {
+ const [users, setUsers] = useState([]);
+
+ const getUserList = async () => {
+ const res = await getUsers();
+ if (res.code !== 0) return;
+ setUsers(res.data);
+ };
+
+ useEffect(() => {
+ getUserList();
+ }, []);
+
+ return (
+
+ );
+};
+
+export default Users;
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..98bcddc
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitAny": false,
+ "baseUrl": "./",
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..42872c5
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..06df1a7
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,20 @@
+import path from "path";
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react-swc";
+import { viteMockServe } from "vite-plugin-mock";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ react(),
+ viteMockServe({
+ mockPath: "mock",
+ enable: true,
+ }),
+ ],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+});