├── .editorconfig ├── .env ├── .env.development ├── .env.production ├── .env.test ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .stylelintrc.js ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── index.html ├── lint-staged.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── favicon.ico ├── src ├── App.tsx ├── api │ ├── config │ │ └── servicePort.ts │ ├── helper │ │ ├── axiosCancel.ts │ │ └── checkStatus.ts │ ├── index.ts │ ├── interface │ │ └── index.ts │ └── modules │ │ └── login.ts ├── assets │ ├── fonts │ │ ├── DIN.otf │ │ ├── MetroDF.ttf │ │ ├── YouSheBiaoTiHei.ttf │ │ └── font.less │ ├── iconfont │ │ ├── iconfont.less │ │ └── iconfont.ttf │ ├── icons │ │ ├── xianxingdaoyu.svg │ │ ├── xianxingdiqiu.svg │ │ ├── xianxingditu.svg │ │ ├── xianxingfanchuan.svg │ │ ├── xianxingfeiji.svg │ │ ├── xianxinglvhangriji.svg │ │ ├── xianxingtianqiyubao.svg │ │ ├── xianxingxiangjipaizhao.svg │ │ ├── xianxingxiarilengyin.svg │ │ ├── xianxingyoulun.svg │ │ └── xianxingzijiayou.svg │ └── images │ │ ├── avatar.png │ │ ├── favicon.svg │ │ ├── login_bg.svg │ │ ├── login_left.png │ │ ├── login_left1.png │ │ ├── login_left2.png │ │ ├── login_left3.png │ │ ├── login_left4.png │ │ ├── logo.png │ │ ├── welcome.png │ │ └── welcome01.png ├── components │ ├── ErrorMessage │ │ ├── 403.tsx │ │ ├── 404.tsx │ │ ├── 500.tsx │ │ └── index.less │ ├── Loading │ │ ├── index.less │ │ └── index.tsx │ ├── SwitchDark │ │ └── index.tsx │ └── svgIcon │ │ └── index.tsx ├── config │ ├── config.ts │ ├── nprogress.ts │ └── serviceLoading.tsx ├── enums │ └── httpEnum.ts ├── hooks │ ├── useAuthButtons.ts │ ├── useEcharts.ts │ ├── useTable.ts │ ├── useTheme.ts │ └── useTime.ts ├── language │ ├── index.ts │ └── modules │ │ ├── en.ts │ │ └── zh.ts ├── layouts │ ├── components │ │ ├── Footer │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── Header │ │ │ ├── components │ │ │ │ ├── AssemblySize.tsx │ │ │ │ ├── AvatarIcon.tsx │ │ │ │ ├── BreadcrumbNav.tsx │ │ │ │ ├── CollapseIcon.tsx │ │ │ │ ├── Fullscreen.tsx │ │ │ │ ├── InfoModal.tsx │ │ │ │ ├── Language.tsx │ │ │ │ ├── PasswordModal.tsx │ │ │ │ └── Theme.tsx │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── Menu │ │ │ ├── components │ │ │ │ └── Logo.tsx │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── Tabs │ │ │ ├── components │ │ │ └── MoreButton.tsx │ │ │ ├── index.less │ │ │ └── index.tsx │ ├── index.less │ └── index.tsx ├── main.tsx ├── redux │ ├── index.ts │ ├── interface │ │ └── index.ts │ ├── modules │ │ ├── auth │ │ │ ├── action.ts │ │ │ └── reducer.ts │ │ ├── breadcrumb │ │ │ ├── action.ts │ │ │ └── reducer.ts │ │ ├── global │ │ │ ├── action.ts │ │ │ └── reducer.ts │ │ ├── menu │ │ │ ├── action.ts │ │ │ └── reducer.ts │ │ └── tabs │ │ │ ├── action.ts │ │ │ └── reducer.ts │ └── mutation-types.ts ├── routers │ ├── constant.tsx │ ├── index.tsx │ ├── interface │ │ └── index.ts │ ├── modules │ │ ├── assembly.tsx │ │ ├── dashboard.tsx │ │ ├── dataScreen.tsx │ │ ├── echarts.tsx │ │ ├── error.tsx │ │ ├── form.tsx │ │ ├── home.tsx │ │ ├── link.tsx │ │ ├── menu.tsx │ │ └── proTable.tsx │ └── utils │ │ ├── authRouter.tsx │ │ └── lazyLoad.tsx ├── styles │ ├── common.less │ ├── reset.less │ ├── theme │ │ ├── theme-dark.less │ │ └── theme-default.less │ └── var.less ├── typings │ ├── global.d.ts │ ├── plugins.d.ts │ └── window.d.ts ├── utils │ ├── echarts │ │ └── index.ts │ ├── getEnv.ts │ ├── is │ │ └── index.ts │ └── util.ts ├── views │ ├── assembly │ │ ├── batchImport │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── guide │ │ │ ├── index.tsx │ │ │ └── steps.ts │ │ ├── selectIcon │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── svgIcon │ │ │ ├── index.less │ │ │ └── index.tsx │ ├── dashboard │ │ ├── dataVisualize │ │ │ ├── components │ │ │ │ ├── curve.tsx │ │ │ │ └── pie.tsx │ │ │ ├── images │ │ │ │ ├── 1-bg.png │ │ │ │ ├── 2-bg.png │ │ │ │ ├── 3-bg.png │ │ │ │ ├── 4-bg.png │ │ │ │ ├── add_person.png │ │ │ │ ├── add_team.png │ │ │ │ ├── book-bg.png │ │ │ │ ├── book-sum.png │ │ │ │ ├── book_sum.png │ │ │ │ └── today.png │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── embedded │ │ │ ├── index.less │ │ │ └── index.tsx │ ├── dataScreen │ │ ├── assets │ │ │ ├── alarmList.Json │ │ │ ├── china.json │ │ │ └── ranking-icon.ts │ │ ├── components │ │ │ ├── AgeRatioChart.tsx │ │ │ ├── AnnualUseChart.less │ │ │ ├── AnnualUseChart.tsx │ │ │ ├── ChinaMapChart.less │ │ │ ├── ChinaMapChart.tsx │ │ │ ├── DataHeaderTime.tsx │ │ │ ├── HotPlateChart.less │ │ │ ├── HotPlateChart.tsx │ │ │ ├── MaleFemaleRatioChart.less │ │ │ ├── MaleFemaleRatioChart.tsx │ │ │ ├── OverNext30Chart.less │ │ │ ├── OverNext30Chart.tsx │ │ │ ├── PlatformSourceChart.tsx │ │ │ ├── RealTimeAccessChart.less │ │ │ └── RealTimeAccessChart.tsx │ │ ├── images │ │ │ ├── bg.png │ │ │ ├── contrast-bg.png │ │ │ ├── dataScreen-alarm.png │ │ │ ├── dataScreen-header-btn-bg-l.png │ │ │ ├── dataScreen-header-btn-bg-r.png │ │ │ ├── dataScreen-header-center-bg.png │ │ │ ├── dataScreen-header-left-bg.png │ │ │ ├── dataScreen-header-right-bg.png │ │ │ ├── dataScreen-header-warn-bg.png │ │ │ ├── dataScreen-main-cb.png │ │ │ ├── dataScreen-main-lb.png │ │ │ ├── dataScreen-main-lc.png │ │ │ ├── dataScreen-main-lt.png │ │ │ ├── dataScreen-main-rb.png │ │ │ ├── dataScreen-main-rc.png │ │ │ ├── dataScreen-main-rt.png │ │ │ ├── dataScreen-title.png │ │ │ ├── dataScreen-warn-bg.png │ │ │ ├── line-bg.png │ │ │ ├── man-bg.png │ │ │ ├── man.png │ │ │ ├── map-title-bg.png │ │ │ ├── rankingChart-bg.png │ │ │ ├── total.png │ │ │ ├── woman-bg.png │ │ │ └── woman.png │ │ ├── index.less │ │ └── index.tsx │ ├── echarts │ │ ├── columnChart │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── lineChart │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── nestedChart │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── pieChart │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── radarChart │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── waterChart │ │ │ ├── index.less │ │ │ └── index.tsx │ ├── form │ │ ├── basicForm │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── dynamicForm │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── validateForm │ │ │ ├── index.less │ │ │ └── index.tsx │ ├── home │ │ ├── index.less │ │ └── index.tsx │ ├── link │ │ ├── gitee │ │ │ └── index.tsx │ │ ├── github │ │ │ └── index.tsx │ │ ├── juejin │ │ │ └── index.tsx │ │ └── myBlog │ │ │ └── index.tsx │ ├── login │ │ ├── components │ │ │ └── LoginForm.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── menu │ │ ├── menu1 │ │ │ └── index.tsx │ │ ├── menu2 │ │ │ ├── menu21 │ │ │ │ └── index.tsx │ │ │ ├── menu22 │ │ │ │ ├── menu221 │ │ │ │ │ └── index.tsx │ │ │ │ └── menu222 │ │ │ │ │ └── index.tsx │ │ │ └── menu23 │ │ │ │ └── index.tsx │ │ └── menu3 │ │ │ └── index.tsx │ └── proTable │ │ ├── useComponent │ │ ├── index.less │ │ └── index.tsx │ │ └── useHooks │ │ ├── index.less │ │ └── index.tsx └── vite-env.d.ts ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # @see: http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] # 表示所有文件适用 6 | charset = utf-8 # 设置文件字符集为 utf-8 7 | end_of_line = lf # 控制换行类型(lf | cr | crlf) 8 | insert_final_newline = true # 始终在文件末尾插入一个新行 9 | indent_style = tab # 缩进风格(tab | space) 10 | indent_size = 2 # 缩进大小 11 | max_line_length = 130 # 最大行长度 12 | 13 | [*.md] # 表示仅 md 文件适用以下规则 14 | max_line_length = off # 关闭最大行长度限制 15 | trim_trailing_whitespace = false # 关闭末尾空格修剪 16 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # title 2 | VITE_GLOB_APP_TITLE = 'Hooks-Admin' 3 | 4 | # port 5 | VITE_PORT = 3301 6 | 7 | # open 运行 npm run dev 时自动打开浏览器 8 | VITE_OPEN = true 9 | 10 | # 是否生成包预览文件 11 | VITE_REPORT = false 12 | 13 | # 是否开启gzip压缩 14 | VITE_BUILD_GZIP = false 15 | 16 | # 是否删除生产环境 console 17 | VITE_DROP_CONSOLE = true 18 | 19 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 本地环境 2 | NODE_ENV = 'development' 3 | 4 | # 本地环境接口地址 5 | VITE_API_URL = '/api' -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 线上环境 2 | NODE_ENV = "production" 3 | 4 | # 线上环境接口地址 5 | VITE_API_URL = "https://mock.mengxuegu.com/mock/62abda3212c1416424630a45" 6 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # 测试环境 2 | NODE_ENV = "test" 3 | 4 | # 测试环境接口地址 5 | VITE_API_URL = "https://mock.mengxuegu.com/mock/62abda3212c1416424630a45" 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.sh 2 | node_modules 3 | *.md 4 | *.woff 5 | *.ttf 6 | .vscode 7 | .idea 8 | dist 9 | /public 10 | /docs 11 | .husky 12 | .local 13 | /bin 14 | .eslintrc.js 15 | .prettierrc.js 16 | /src/mock/* 17 | 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @see: http://eslint.cn 2 | 3 | module.exports = { 4 | settings: { 5 | react: { 6 | version: "detect" 7 | } 8 | }, 9 | root: true, 10 | env: { 11 | browser: true, 12 | node: true, 13 | es6: true 14 | }, 15 | /* 指定如何解析语法 */ 16 | parser: "@typescript-eslint/parser", 17 | /* 优先级低于 parse 的语法解析配置 */ 18 | parserOptions: { 19 | ecmaVersion: 2020, 20 | sourceType: "module", 21 | jsxPragma: "React", 22 | ecmaFeatures: { 23 | jsx: true 24 | } 25 | }, 26 | plugins: ["react", "@typescript-eslint", "react-hooks", "prettier"], 27 | /* 继承某些已有的规则 */ 28 | extends: [ 29 | "eslint:recommended", 30 | "plugin:react/recommended", 31 | "plugin:@typescript-eslint/recommended", 32 | "plugin:react/jsx-runtime", 33 | "plugin:react-hooks/recommended", 34 | "prettier", 35 | "plugin:prettier/recommended" 36 | ], 37 | /* 38 | * "off" 或 0 ==> 关闭规则 39 | * "warn" 或 1 ==> 打开的规则作为警告(不影响代码执行) 40 | * "error" 或 2 ==> 规则作为一个错误(代码不能执行,界面报错) 41 | */ 42 | rules: { 43 | // eslint (http://eslint.cn/docs/rules) 44 | "no-var": "error", // 要求使用 let 或 const 而不是 var 45 | "no-multiple-empty-lines": ["error", { max: 1 }], // 不允许多个空行 46 | "no-use-before-define": "off", // 禁止在 函数/类/变量 定义之前使用它们 47 | "prefer-const": "off", // 此规则旨在标记使用 let 关键字声明但在初始分配后从未重新分配的变量,要求使用 const 48 | "no-irregular-whitespace": "off", // 禁止不规则的空白 49 | 50 | // typeScript (https://typescript-eslint.io/rules) 51 | "@typescript-eslint/no-unused-vars": "error", // 禁止定义未使用的变量 52 | "@typescript-eslint/no-inferrable-types": "off", // 可以轻松推断的显式类型可能会增加不必要的冗长 53 | "@typescript-eslint/no-namespace": "off", // 禁止使用自定义 TypeScript 模块和命名空间。 54 | "@typescript-eslint/no-explicit-any": "off", // 禁止使用 any 类型 55 | "@typescript-eslint/ban-ts-ignore": "off", // 禁止使用 @ts-ignore 56 | "@typescript-eslint/ban-types": "off", // 禁止使用特定类型 57 | "@typescript-eslint/explicit-function-return-type": "off", // 不允许对初始化为数字、字符串或布尔值的变量或参数进行显式类型声明 58 | "@typescript-eslint/no-var-requires": "off", // 不允许在 import 语句中使用 require 语句 59 | "@typescript-eslint/no-empty-function": "off", // 禁止空函数 60 | "@typescript-eslint/no-use-before-define": "off", // 禁止在变量定义之前使用它们 61 | "@typescript-eslint/ban-ts-comment": "off", // 禁止 @ts- 使用注释或要求在指令后进行描述 62 | "@typescript-eslint/no-non-null-assertion": "off", // 不允许使用后缀运算符的非空断言(!) 63 | "@typescript-eslint/explicit-module-boundary-types": "off", // 要求导出函数和类的公共类方法的显式返回和参数类型 64 | 65 | // react (https://github.com/jsx-eslint/eslint-plugin-react) 66 | "react-hooks/rules-of-hooks": "off", 67 | "react-hooks/exhaustive-deps": "off" 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | stats.html 15 | 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | !.vscode/settings.json 21 | .idea 22 | .DS_Store 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint:lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | .local 3 | /node_modules/** 4 | 5 | **/*.svg 6 | **/*.sh 7 | 8 | /public/* 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // @see: https://www.prettier.cn 2 | 3 | module.exports = { 4 | // 超过最大值换行 5 | printWidth: 130, 6 | // 缩进字节数 7 | tabWidth: 2, 8 | // 使用制表符而不是空格缩进行 9 | useTabs: true, 10 | // 结尾不用分号(true有,false没有) 11 | semi: true, 12 | // 使用单引号(true单双引号,false双引号) 13 | singleQuote: false, 14 | // 更改引用对象属性的时间 可选值"" 15 | quoteProps: "as-needed", 16 | // 在对象,数组括号与文字之间加空格 "{ foo: bar }" 17 | bracketSpacing: true, 18 | // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"",默认none 19 | trailingComma: "none", 20 | // 在JSX中使用单引号而不是双引号 21 | jsxSingleQuote: false, 22 | // (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号 ,always:不省略括号 23 | arrowParens: "avoid", 24 | // 如果文件顶部已经有一个 doclock,这个选项将新建一行注释,并打上@format标记。 25 | insertPragma: false, 26 | // 指定要使用的解析器,不需要写文件开头的 @prettier 27 | requirePragma: false, 28 | // 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行 29 | proseWrap: "preserve", 30 | // 在html中空格是否是敏感的 "css" - 遵守CSS显示属性的默认值, "strict" - 空格被认为是敏感的 ,"ignore" - 空格被认为是不敏感的 31 | htmlWhitespaceSensitivity: "css", 32 | // 换行符使用 lf 结尾是 可选值"" 33 | endOfLine: "auto", 34 | // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码 35 | rangeStart: 0, 36 | rangeEnd: Infinity, 37 | // Vue文件脚本和样式标签缩进 38 | vueIndentScriptAndStyle: false 39 | }; 40 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | /public/* 3 | public/* 4 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | // @see: https://stylelint.io 2 | 3 | module.exports = { 4 | extends: [ 5 | "stylelint-config-standard", // 配置stylelint拓展插件 6 | "stylelint-config-prettier", // 配置stylelint和prettier兼容 7 | "stylelint-config-recess-order" // 配置stylelint css属性书写顺序插件, 8 | ], 9 | plugins: ["stylelint-less"], // 配置stylelint less拓展插件 10 | rules: { 11 | indentation: null, // 指定缩进空格 12 | "no-descending-specificity": null, // 禁止在具有较高优先级的选择器后出现被其覆盖的较低优先级的选择器 13 | "function-url-quotes": "always", // 要求或禁止 URL 的引号 "always(必须加上引号)"|"never(没有引号)" 14 | "string-quotes": "double", // 指定字符串使用单引号或双引号 15 | "unit-case": null, // 指定单位的大小写 "lower(全小写)"|"upper(全大写)" 16 | "color-hex-case": "lower", // 指定 16 进制颜色的大小写 "lower(全小写)"|"upper(全大写)" 17 | "color-hex-length": "long", // 指定 16 进制颜色的简写或扩写 "short(16进制简写)"|"long(16进制扩写)" 18 | "rule-empty-line-before": "never", // 要求或禁止在规则之前的空行 "always(规则之前必须始终有一个空行)"|"never(规则前绝不能有空行)"|"always-multi-line(多行规则之前必须始终有一个空行)"|"never-multi-line(多行规则之前绝不能有空行。)" 19 | "font-family-no-missing-generic-family-keyword": null, // 禁止在字体族名称列表中缺少通用字体族关键字 20 | "block-opening-brace-space-before": "always", // 要求在块的开大括号之前必须有一个空格或不能有空白符 "always(大括号前必须始终有一个空格)"|"never(左大括号之前绝不能有空格)"|"always-single-line(在单行块中的左大括号之前必须始终有一个空格)"|"never-single-line(在单行块中的左大括号之前绝不能有空格)"|"always-multi-line(在多行块中,左大括号之前必须始终有一个空格)"|"never-multi-line(多行块中的左大括号之前绝不能有空格)" 21 | "property-no-unknown": null, // 禁止未知的属性(true 为不允许) 22 | "no-empty-source": null, // 禁止空源码 23 | "declaration-block-trailing-semicolon": null, // 要求或不允许在声明块中使用尾随分号 string:"always(必须始终有一个尾随分号)"|"never(不得有尾随分号)" 24 | "selector-class-pattern": null, // 强制选择器类名的格式 25 | "value-no-vendor-prefix": null, // 关闭 vendor-prefix(为了解决多行省略 -webkit-box) 26 | "at-rule-no-unknown": null, 27 | "selector-pseudo-class-no-unknown": [ 28 | true, 29 | { 30 | ignorePseudoClasses: ["global", "v-deep", "deep"] 31 | } 32 | ] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dsznajder.es7-react-js-snippets", 4 | "stylelint.vscode-stylelint", 5 | "dbaeumer.vscode-eslint", 6 | "editorconfig.editorconfig", 7 | "esbenp.prettier-vscode" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // 保存自动格式化代码 3 | "editor.formatOnSave": true, 4 | "stylelint.enable": true, 5 | // 开启stylelint自动修复 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.stylelint": true 8 | }, 9 | // 配置stylelint检查的文件类型范围 10 | "stylelint.validate": ["css", "less", "postcss", "scss", "sass"], 11 | "files.eol": "\n", 12 | "cSpell.words": [ 13 | "antd", 14 | "anticon", 15 | "Appstore", 16 | "Biao", 17 | "bqddxxwqmfncffacvbpkuxvwvqrhln", 18 | "breakline", 19 | "browserslist", 20 | "cnpm", 21 | "commitlint", 22 | "contentright", 23 | "doclock", 24 | "easymock", 25 | "esbuild", 26 | "fangda", 27 | "fastmock", 28 | "Geeker", 29 | "Gitee", 30 | "iconfont", 31 | "immer", 32 | "juejin", 33 | "loglevel", 34 | "malefemale", 35 | "nprogress", 36 | "persistor", 37 | "Prefixs", 38 | "screenfull", 39 | "Sider", 40 | "styl", 41 | "stylelint", 42 | "stylelintignore", 43 | "stylelintrc", 44 | "suoxiao", 45 | "truetype", 46 | "zhongyingwen", 47 | "zhuti" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### 0.0.1 (2022-06-24) 6 | 7 | ### Features 8 | 9 | - 🚀 拆分路由 ([a5b7aaf](https://gitee.com/laramie/Hooks-Admin/commit/a5b7aafde0a562d9ea446b7ea5476167809102e3)) 10 | - 🚀 初始化仓库 ([6386e68](https://gitee.com/laramie/Hooks-Admin/commit/6386e685b84afa96699fc0202b4ee406b154e8af)) 11 | - 🚀 初始化项目配置 ([960a80e](https://gitee.com/laramie/Hooks-Admin/commit/960a80e1168671e0ad59acd266f3e3d105eb4dd0)) 12 | - 🚀 初始化项目配置 ([8368f1d](https://gitee.com/laramie/Hooks-Admin/commit/8368f1d689406697214530b06c29a28d43b6ae87)) 13 | - 🚀 更新配置文件 ([dc8ccbc](https://gitee.com/laramie/Hooks-Admin/commit/dc8ccbc4a7455bf634a72be40dfbd893e6b8ab45)) 14 | - 🚀 获取按钮权限列表 ([f18d916](https://gitee.com/laramie/Hooks-Admin/commit/f18d9169ef35ac6f82394c04cc05a800d915916a)) 15 | - 🚀 集成 redux + react-redux + immer ([a9f3a1a](https://gitee.com/laramie/Hooks-Admin/commit/a9f3a1ab04e006f5e2eb5020425adc55def984fe)) 16 | - 🚀 配置 menu ([dbd39f1](https://gitee.com/laramie/Hooks-Admin/commit/dbd39f1052d370e85f8aa43291fe51b618ce2394)) 17 | - 🚀 配置路由 ([5a7f12f](https://gitee.com/laramie/Hooks-Admin/commit/5a7f12ff72891bc51410e150d5f4b7db4df96397)) 18 | - 🚀 完成 layouts 布局 ([7ad6777](https://gitee.com/laramie/Hooks-Admin/commit/7ad6777972978ae2bd70d9f88246a31c214f162f)) 19 | - 🚀 完成 menu 自适应 ([5aed727](https://gitee.com/laramie/Hooks-Admin/commit/5aed727ed040a6ca93305d1f2f24d21b03ab7729)) 20 | - 🚀 完成 tabs 和 menu 联动 ([f90de14](https://gitee.com/laramie/Hooks-Admin/commit/f90de142b824c64a3c46d64dbf28102a6bb2ec71)) 21 | - 🚀 完成 tabs 联动 && 并存储到 redux ([94d39ff](https://gitee.com/laramie/Hooks-Admin/commit/94d39ff9fe15098e1b99be919dc1fdffb8d4bb42)) 22 | - 🚀 完成 tabs 新增 ([59ca7f3](https://gitee.com/laramie/Hooks-Admin/commit/59ca7f36702b3d0c8e7084ea0bf2aabd48b18831)) 23 | - 🚀 完成菜单动态渲染 ([b91a8f9](https://gitee.com/laramie/Hooks-Admin/commit/b91a8f915c9df949a71afba22bc61968a29bc898)) 24 | - 🚀 完成菜单权限 && 路由拦截 ([6514eb0](https://gitee.com/laramie/Hooks-Admin/commit/6514eb0b3760f287e4ee15f4b3fa0c90f2503d8e)) 25 | - 🚀 完成登录页逻辑 ([4ea6ff9](https://gitee.com/laramie/Hooks-Admin/commit/4ea6ff9f76d9dfda31be530e758fecb2c4a4cb45)) 26 | - 🚀 新增 表单、Echarts、常用组件 模块 ([55eee9d](https://gitee.com/laramie/Hooks-Admin/commit/55eee9d2d57e9d2dea2b7b7b1ca4f51304550db2)) 27 | - 🚀 新增 api 请求 ([50505c7](https://gitee.com/laramie/Hooks-Admin/commit/50505c7bd95e235bbef4c501ba8afb6eb6e942f8)) 28 | - 🚀 新增 nprogress 插件 ([87c3185](https://gitee.com/laramie/Hooks-Admin/commit/87c318580e5f039430a113600393dc7a8c698cc0)) 29 | - 🚀 新增错误页 ([4ed22d4](https://gitee.com/laramie/Hooks-Admin/commit/4ed22d40bafe75a516be3bb85527ac059aa67aaf)) 30 | - 🚀 新增登录页 ([55da905](https://gitee.com/laramie/Hooks-Admin/commit/55da9053626679b014a97ea314eeeee74ec50867)) 31 | - 🚀 新增路由懒加载 ([644b6ae](https://gitee.com/laramie/Hooks-Admin/commit/644b6ae6931fc634e676f038ba85cda69e461ba8)) 32 | - 🚀 修改路由配置 ([a6ba805](https://gitee.com/laramie/Hooks-Admin/commit/a6ba8052410d66353aeb55010e845d393c71e6b2)) 33 | - 🚀 优化路由拦截方法 ([94ea881](https://gitee.com/laramie/Hooks-Admin/commit/94ea881ad350ac4b6d2dc7d338801d5dfddbd1fe)) 34 | - 🚀 增加 Header 工具图标 ([9ecbed3](https://gitee.com/laramie/Hooks-Admin/commit/9ecbed38729dabbea5114d3031740dee0a993760)) 35 | - 🚀 增加修改密码 && 个人信息 modal ([317f763](https://gitee.com/laramie/Hooks-Admin/commit/317f763ee02210b59888de11b0d89037e2377371)) 36 | 37 | ### Bug Fixes 38 | 39 | - 🧩 解决 menu 超出不能滚动 bug ([fae1509](https://gitee.com/laramie/Hooks-Admin/commit/fae15093ae8a3ca46e6958f08e8cafd47be1ad69)) 40 | - 🧩 解决 sider 折叠闪烁 bug ([78099ab](https://gitee.com/laramie/Hooks-Admin/commit/78099ab246aec5e44136f1cd354e333e882e77e8)) 41 | - 🧩 修改头部图标样式 ([b3dac8f](https://gitee.com/laramie/Hooks-Admin/commit/b3dac8feadf7fa074ba0c1b6cfdd534634b8e08c)) 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SpicyBoy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%- title %> 8 | 9 | 10 |
11 | 90 |
91 |
92 | 93 |
94 |
95 |
96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], 3 | "{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": ["prettier --write--parser json"], 4 | "package.json": ["prettier --write"], 5 | "*.{scss,less,styl}": ["stylelint --fix", "prettier --write"], 6 | "*.md": ["prettier --write"] 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "vite", 7 | "serve": "vite", 8 | "build:dev": "tsc && vite build --mode development", 9 | "build:test": "tsc && vite build --mode test", 10 | "build:pro": "tsc && vite build --mode production", 11 | "preview": "vite preview", 12 | "lint:eslint": "eslint --fix --ext .js,.ts,.tsx ./src", 13 | "lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,scss,html,md}\"", 14 | "lint:stylelint": "stylelint --cache --fix \"**/*.{less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", 15 | "lint:lint-staged": "lint-staged", 16 | "prepare": "husky install", 17 | "release": "standard-version", 18 | "commit": "git pull && git add -A && git-cz && git push" 19 | }, 20 | "dependencies": { 21 | "antd": "^4.22.2", 22 | "axios": "^0.27.2", 23 | "driver.js": "^0.9.8", 24 | "echarts": "^5.3.0", 25 | "echarts-liquidfill": "^3.1.0", 26 | "i18next": "^21.8.10", 27 | "immer": "^9.0.15", 28 | "js-md5": "^0.7.3", 29 | "moment": "^2.29.3", 30 | "nprogress": "^0.2.0", 31 | "qs": "^6.10.5", 32 | "react": "^18.2.0", 33 | "react-activation": "^0.11.2", 34 | "react-dom": "^18.2.0", 35 | "react-i18next": "^11.17.3", 36 | "react-redux": "^8.0.2", 37 | "react-router-dom": "^6.3.0", 38 | "react-transition-group": "^4.4.2", 39 | "redux": "^4.2.0", 40 | "redux-persist": "^6.0.0", 41 | "redux-promise": "^0.6.0", 42 | "redux-thunk": "^2.4.1", 43 | "screenfull": "^6.0.2" 44 | }, 45 | "devDependencies": { 46 | "@commitlint/cli": "^17.0.2", 47 | "@commitlint/config-conventional": "^17.0.2", 48 | "@types/node": "^17.0.41", 49 | "@types/react": "^18.0.0", 50 | "@types/react-dom": "^18.0.0", 51 | "@types/react-router-dom": "^5.3.3", 52 | "@types/redux-promise": "^0.5.29", 53 | "@typescript-eslint/eslint-plugin": "^5.27.1", 54 | "@typescript-eslint/parser": "^5.27.1", 55 | "@vitejs/plugin-react": "^1.3.0", 56 | "autoprefixer": "^10.4.7", 57 | "commitizen": "^4.2.4", 58 | "cz-git": "^1.3.4", 59 | "eslint": "^8.17.0", 60 | "eslint-config-prettier": "^8.5.0", 61 | "eslint-plugin-prettier": "^4.0.0", 62 | "eslint-plugin-react": "^7.30.0", 63 | "eslint-plugin-react-hooks": "^4.5.0", 64 | "husky": "^8.0.1", 65 | "less": "^4.1.3", 66 | "lint-staged": "^13.0.2", 67 | "postcss": "^8.4.14", 68 | "prettier": "^2.6.2", 69 | "rollup-plugin-visualizer": "^5.6.0", 70 | "standard-version": "^9.5.0", 71 | "stylelint": "^14.9.1", 72 | "stylelint-config-prettier": "^9.0.3", 73 | "stylelint-config-recess-order": "^3.0.0", 74 | "stylelint-config-standard": "^26.0.0", 75 | "stylelint-less": "^1.0.6", 76 | "typescript": "^4.6.3", 77 | "vite": "^2.9.9", 78 | "vite-plugin-compression": "^0.5.1", 79 | "vite-plugin-eslint": "^1.6.1", 80 | "vite-plugin-html": "^3.2.0", 81 | "vite-plugin-svg-icons": "^2.0.1" 82 | }, 83 | "config": { 84 | "commitizen": { 85 | "path": "node_modules/cz-git" 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/public/favicon.ico -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { getBrowserLang } from "@/utils/util"; 3 | import { ConfigProvider } from "antd"; 4 | import { connect } from "react-redux"; 5 | import { setLanguage } from "@/redux/modules/global/action"; 6 | import { HashRouter } from "react-router-dom"; 7 | import AuthRouter from "@/routers/utils/authRouter"; 8 | import Router from "@/routers/index"; 9 | import useTheme from "@/hooks/useTheme"; 10 | import zhCN from "antd/lib/locale/zh_CN"; 11 | import enUS from "antd/lib/locale/en_US"; 12 | import i18n from "i18next"; 13 | import "moment/dist/locale/zh-cn"; 14 | 15 | const App = (props: any) => { 16 | const { language, assemblySize, themeConfig, setLanguage } = props; 17 | const [i18nLocale, setI18nLocale] = useState(zhCN); 18 | 19 | // 全局使用主题 20 | useTheme(themeConfig); 21 | 22 | // 设置 antd 语言国际化 23 | const setAntdLanguage = () => { 24 | // 如果 redux 中有默认语言就设置成 redux 的默认语言,没有默认语言就设置成浏览器默认语言 25 | if (language && language == "zh") return setI18nLocale(zhCN); 26 | if (language && language == "en") return setI18nLocale(enUS); 27 | if (getBrowserLang() == "zh") return setI18nLocale(zhCN); 28 | if (getBrowserLang() == "en") return setI18nLocale(enUS); 29 | }; 30 | 31 | useEffect(() => { 32 | // 全局使用国际化 33 | i18n.changeLanguage(language || getBrowserLang()); 34 | setLanguage(language || getBrowserLang()); 35 | setAntdLanguage(); 36 | }, [language]); 37 | 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | const mapStateToProps = (state: any) => state.global; 50 | const mapDispatchToProps = { setLanguage }; 51 | export default connect(mapStateToProps, mapDispatchToProps)(App); 52 | -------------------------------------------------------------------------------- /src/api/config/servicePort.ts: -------------------------------------------------------------------------------- 1 | // 后端微服务端口名 2 | export const PORT1 = "/hooks"; 3 | export const PORT2 = "/geeker"; 4 | -------------------------------------------------------------------------------- /src/api/helper/axiosCancel.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, Canceler } from "axios"; 2 | import { isFunction } from "@/utils/is/index"; 3 | import qs from "qs"; 4 | 5 | // * 声明一个 Map 用于存储每个请求的标识 和 取消函数 6 | let pendingMap = new Map(); 7 | 8 | // * 序列化参数 9 | export const getPendingUrl = (config: AxiosRequestConfig) => 10 | [config.method, config.url, qs.stringify(config.data), qs.stringify(config.params)].join("&"); 11 | 12 | export class AxiosCanceler { 13 | /** 14 | * @description: 添加请求 15 | * @param {Object} config 16 | */ 17 | addPending(config: AxiosRequestConfig) { 18 | // * 在请求开始前,对之前的请求做检查取消操作 19 | this.removePending(config); 20 | const url = getPendingUrl(config); 21 | config.cancelToken = 22 | config.cancelToken || 23 | new axios.CancelToken(cancel => { 24 | if (!pendingMap.has(url)) { 25 | // 如果 pending 中不存在当前请求,则添加进去 26 | pendingMap.set(url, cancel); 27 | } 28 | }); 29 | } 30 | 31 | /** 32 | * @description: 移除请求 33 | * @param {Object} config 34 | */ 35 | removePending(config: AxiosRequestConfig) { 36 | const url = getPendingUrl(config); 37 | 38 | if (pendingMap.has(url)) { 39 | // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除 40 | const cancel = pendingMap.get(url); 41 | cancel && cancel(); 42 | pendingMap.delete(url); 43 | } 44 | } 45 | 46 | /** 47 | * @description: 清空所有pending 48 | */ 49 | removeAllPending() { 50 | pendingMap.forEach(cancel => { 51 | cancel && isFunction(cancel) && cancel(); 52 | }); 53 | pendingMap.clear(); 54 | } 55 | 56 | /** 57 | * @description: 重置 58 | */ 59 | reset(): void { 60 | pendingMap = new Map(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/api/helper/checkStatus.ts: -------------------------------------------------------------------------------- 1 | import { message } from "antd"; 2 | 3 | /** 4 | * @description: 校验网络请求状态码 5 | * @param {Number} status 6 | * @return void 7 | */ 8 | export const checkStatus = (status: number): void => { 9 | switch (status) { 10 | case 400: 11 | message.error("请求失败!请您稍后重试"); 12 | break; 13 | case 401: 14 | message.error("登录失效!请您重新登录"); 15 | break; 16 | case 403: 17 | message.error("当前账号无权限访问!"); 18 | break; 19 | case 404: 20 | message.error("你所访问的资源不存在!"); 21 | break; 22 | case 405: 23 | message.error("请求方式错误!请您稍后重试"); 24 | break; 25 | case 408: 26 | message.error("请求超时!请您稍后重试"); 27 | break; 28 | case 500: 29 | message.error("服务异常!"); 30 | break; 31 | case 502: 32 | message.error("网关错误!"); 33 | break; 34 | case 503: 35 | message.error("服务不可用!"); 36 | break; 37 | case 504: 38 | message.error("网关超时!"); 39 | break; 40 | default: 41 | message.error("请求失败!"); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import NProgress from "@/config/nprogress"; 2 | import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, AxiosResponse } from "axios"; 3 | import { showFullScreenLoading, tryHideFullScreenLoading } from "@/config/serviceLoading"; 4 | import { ResultData } from "@/api/interface"; 5 | import { ResultEnum } from "@/enums/httpEnum"; 6 | import { checkStatus } from "./helper/checkStatus"; 7 | import { AxiosCanceler } from "./helper/axiosCancel"; 8 | import { setToken } from "@/redux/modules/global/action"; 9 | import { message } from "antd"; 10 | import { store } from "@/redux"; 11 | 12 | const axiosCanceler = new AxiosCanceler(); 13 | 14 | const config = { 15 | // 默认地址请求地址,可在 .env 开头文件中修改 16 | baseURL: import.meta.env.VITE_API_URL as string, 17 | // 设置超时时间(10s) 18 | timeout: 10000, 19 | // 跨域时候允许携带凭证 20 | withCredentials: true 21 | }; 22 | 23 | class RequestHttp { 24 | service: AxiosInstance; 25 | public constructor(config: AxiosRequestConfig) { 26 | // 实例化axios 27 | this.service = axios.create(config); 28 | 29 | /** 30 | * @description 请求拦截器 31 | * 客户端发送请求 -> [请求拦截器] -> 服务器 32 | * token校验(JWT) : 接受服务器返回的token,存储到redux/本地储存当中 33 | */ 34 | this.service.interceptors.request.use( 35 | (config: AxiosRequestConfig) => { 36 | NProgress.start(); 37 | // * 将当前请求添加到 pending 中 38 | axiosCanceler.addPending(config); 39 | // * 如果当前请求不需要显示 loading,在api服务中通过指定的第三个参数: { headers: { noLoading: true } }来控制不显示loading,参见loginApi 40 | config.headers!.noLoading || showFullScreenLoading(); 41 | const token: string = store.getState().global.token; 42 | return { ...config, headers: { ...config.headers, "x-access-token": token } }; 43 | }, 44 | (error: AxiosError) => { 45 | return Promise.reject(error); 46 | } 47 | ); 48 | 49 | /** 50 | * @description 响应拦截器 51 | * 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息 52 | */ 53 | this.service.interceptors.response.use( 54 | (response: AxiosResponse) => { 55 | const { data, config } = response; 56 | NProgress.done(); 57 | // * 在请求结束后,移除本次请求(关闭loading) 58 | axiosCanceler.removePending(config); 59 | tryHideFullScreenLoading(); 60 | // * 登录失效(code == 599) 61 | if (data.code == ResultEnum.OVERDUE) { 62 | store.dispatch(setToken("")); 63 | message.error(data.msg); 64 | window.location.hash = "/login"; 65 | return Promise.reject(data); 66 | } 67 | // * 全局错误信息拦截(防止下载文件得时候返回数据流,没有code,直接报错) 68 | if (data.code && data.code !== ResultEnum.SUCCESS) { 69 | message.error(data.msg); 70 | return Promise.reject(data); 71 | } 72 | // * 成功请求(在页面上除非特殊情况,否则不用处理失败逻辑) 73 | return data; 74 | }, 75 | async (error: AxiosError) => { 76 | const { response } = error; 77 | NProgress.done(); 78 | tryHideFullScreenLoading(); 79 | // 请求超时单独判断,请求超时没有 response 80 | if (error.message.indexOf("timeout") !== -1) message.error("请求超时,请稍后再试"); 81 | // 根据响应的错误状态码,做不同的处理 82 | if (response) checkStatus(response.status); 83 | // 服务器结果都没有返回(可能服务器错误可能客户端断网) 断网处理:可以跳转到断网页面 84 | if (!window.navigator.onLine) window.location.hash = "/500"; 85 | return Promise.reject(error); 86 | } 87 | ); 88 | } 89 | 90 | // * 常用请求方法封装 91 | get(url: string, params?: object, _object = {}): Promise> { 92 | return this.service.get(url, { params, ..._object }); 93 | } 94 | post(url: string, params?: object, _object = {}): Promise> { 95 | return this.service.post(url, params, _object); 96 | } 97 | put(url: string, params?: object, _object = {}): Promise> { 98 | return this.service.put(url, params, _object); 99 | } 100 | delete(url: string, params?: any, _object = {}): Promise> { 101 | return this.service.delete(url, { params, ..._object }); 102 | } 103 | } 104 | 105 | export default new RequestHttp(config); 106 | -------------------------------------------------------------------------------- /src/api/interface/index.ts: -------------------------------------------------------------------------------- 1 | // * 请求响应参数(不包含data) 2 | export interface Result { 3 | code: string; 4 | msg: string; 5 | } 6 | 7 | // * 请求响应参数(包含data) 8 | export interface ResultData extends Result { 9 | data?: T; 10 | } 11 | 12 | // * 分页响应参数 13 | export interface ResPage { 14 | datalist: T[]; 15 | pageNum: number; 16 | pageSize: number; 17 | total: number; 18 | } 19 | 20 | // * 分页请求参数 21 | export interface ReqPage { 22 | pageNum: number; 23 | pageSize: number; 24 | } 25 | 26 | // * 登录 27 | export namespace Login { 28 | export interface ReqLoginForm { 29 | username: string; 30 | password: string; 31 | } 32 | export interface ResLogin { 33 | access_token: string; 34 | } 35 | export interface ResAuthButtons { 36 | [propName: string]: any; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/api/modules/login.ts: -------------------------------------------------------------------------------- 1 | import { Login } from "@/api/interface/index"; 2 | import { PORT1 } from "@/api/config/servicePort"; 3 | import qs from "qs"; 4 | 5 | import http from "@/api"; 6 | 7 | /** 8 | * @name 登录模块 9 | */ 10 | // * 用户登录接口 11 | export const loginApi = (params: Login.ReqLoginForm) => { 12 | return http.post(PORT1 + `/login`, params); 13 | return http.post(PORT1 + `/login`, {}, { params }); // post 请求携带 query 参数 ==> ?username=admin&password=123456 14 | return http.post(PORT1 + `/login`, qs.stringify(params)); // post 请求携带 表单 参数 ==> application/x-www-form-urlencoded 15 | return http.post(PORT1 + `/login`, params, { headers: { noLoading: true } }); // 控制当前请求不显示 loading 16 | }; 17 | 18 | // * 获取按钮权限 19 | export const getAuthorButtons = () => { 20 | return http.get(PORT1 + `/auth/buttons`); 21 | }; 22 | 23 | // * 获取菜单列表 24 | export const getMenuList = () => { 25 | return http.get(PORT1 + `/menu/list`); 26 | }; 27 | -------------------------------------------------------------------------------- /src/assets/fonts/DIN.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/assets/fonts/DIN.otf -------------------------------------------------------------------------------- /src/assets/fonts/MetroDF.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/assets/fonts/MetroDF.ttf -------------------------------------------------------------------------------- /src/assets/fonts/YouSheBiaoTiHei.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/assets/fonts/YouSheBiaoTiHei.ttf -------------------------------------------------------------------------------- /src/assets/fonts/font.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: YouSheBiaoTiHei; 3 | src: url("./YouSheBiaoTiHei.ttf"); 4 | } 5 | @font-face { 6 | font-family: MetroDF; 7 | src: url("./MetroDF.ttf"); 8 | } 9 | @font-face { 10 | font-family: DIN; 11 | src: url("./DIN.Otf"); 12 | } 13 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: iconfont; 3 | src: url("iconfont.ttf?t=1648886414212") format("truetype"); 4 | } 5 | .iconfont { 6 | font-family: iconfont !important; 7 | font-size: 16px; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | font-style: normal; 11 | } 12 | .icon-zhongyingwen::before { 13 | content: "\e605"; 14 | } 15 | .icon-suoxiao::before { 16 | content: "\e641"; 17 | } 18 | .icon-fangda::before { 19 | content: "\e826"; 20 | } 21 | .icon-contentright::before { 22 | content: "\e8c9"; 23 | } 24 | .icon-zhuti::before { 25 | content: "\e62b"; 26 | } 27 | -------------------------------------------------------------------------------- /src/assets/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/assets/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/icons/xianxingdaoyu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/xianxingfanchuan.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/assets/images/avatar.png -------------------------------------------------------------------------------- /src/assets/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/images/login_bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/assets/images/login_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/assets/images/login_left.png -------------------------------------------------------------------------------- /src/assets/images/login_left1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/assets/images/login_left1.png -------------------------------------------------------------------------------- /src/assets/images/login_left2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/assets/images/login_left2.png -------------------------------------------------------------------------------- /src/assets/images/login_left3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/assets/images/login_left3.png -------------------------------------------------------------------------------- /src/assets/images/login_left4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/assets/images/login_left4.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/assets/images/welcome.png -------------------------------------------------------------------------------- /src/assets/images/welcome01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/assets/images/welcome01.png -------------------------------------------------------------------------------- /src/components/ErrorMessage/403.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from "antd"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { HOME_URL } from "@/config/config"; 4 | import "./index.less"; 5 | 6 | const NotAuth = () => { 7 | const navigate = useNavigate(); 8 | const goHome = () => { 9 | navigate(HOME_URL); 10 | }; 11 | return ( 12 | 18 | Back Home 19 | 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default NotAuth; 26 | -------------------------------------------------------------------------------- /src/components/ErrorMessage/404.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from "antd"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { HOME_URL } from "@/config/config"; 4 | import "./index.less"; 5 | 6 | const NotFound = () => { 7 | const navigate = useNavigate(); 8 | const goHome = () => { 9 | navigate(HOME_URL); 10 | }; 11 | return ( 12 | 18 | Back Home 19 | 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default NotFound; 26 | -------------------------------------------------------------------------------- /src/components/ErrorMessage/500.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from "antd"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { HOME_URL } from "@/config/config"; 4 | import "./index.less"; 5 | 6 | const NotNetwork = () => { 7 | const navigate = useNavigate(); 8 | const goHome = () => { 9 | navigate(HOME_URL); 10 | }; 11 | return ( 12 | 18 | Back Home 19 | 20 | } 21 | /> 22 | ); 23 | }; 24 | 25 | export default NotNetwork; 26 | -------------------------------------------------------------------------------- /src/components/ErrorMessage/index.less: -------------------------------------------------------------------------------- 1 | .ant-result { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: 100%; 7 | .ant-result-image { 8 | margin: 0; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Loading/index.less: -------------------------------------------------------------------------------- 1 | /* 请求 Loading 样式 */ 2 | .request-loading { 3 | .ant-spin-text { 4 | margin-top: 5px; 5 | font-size: 18px; 6 | color: #509ff1; 7 | } 8 | .ant-spin-dot-item { 9 | background-color: #509ff1; 10 | } 11 | } 12 | 13 | /* 请求 Loading 遮罩层样式 */ 14 | #loading { 15 | position: fixed; 16 | top: 0; 17 | right: 0; 18 | bottom: 0; 19 | left: 0; 20 | z-index: 9998; 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | font-size: 20px; 25 | background: rgb(0 0 0 / 50%); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | import { Spin } from "antd"; 2 | import "./index.less"; 3 | 4 | const Loading = ({ tip = "Loading" }: { tip?: string }) => { 5 | return ; 6 | }; 7 | 8 | export default Loading; 9 | -------------------------------------------------------------------------------- /src/components/SwitchDark/index.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from "antd"; 2 | import { connect } from "react-redux"; 3 | import { setThemeConfig } from "@/redux/modules/global/action"; 4 | 5 | const SwitchDark = (props: any) => { 6 | const { setThemeConfig, themeConfig } = props; 7 | const onChange = (checked: boolean) => { 8 | setThemeConfig({ ...themeConfig, isDark: checked }); 9 | }; 10 | 11 | return ( 12 | 🌞} 16 | unCheckedChildren={<>🌜} 17 | onChange={onChange} 18 | /> 19 | ); 20 | }; 21 | 22 | const mapStateToProps = (state: any) => state.global; 23 | const mapDispatchToProps = { setThemeConfig }; 24 | export default connect(mapStateToProps, mapDispatchToProps)(SwitchDark); 25 | -------------------------------------------------------------------------------- /src/components/svgIcon/index.tsx: -------------------------------------------------------------------------------- 1 | interface SvgProps { 2 | name: string; // 图标的名称 ==> 必传 3 | color?: string; //图标的颜色 ==> 非必传 4 | prefix?: string; // 图标的前缀 ==> 非必传(默认为"icon") 5 | iconStyle?: { [key: string]: any }; // 图标的样式 ==> 非必传 6 | } 7 | 8 | export default function SvgIcon(props: SvgProps) { 9 | const { name, prefix = "icon", iconStyle = { width: "100px", height: "100px" } } = props; 10 | const symbolId = `#${prefix}-${name}`; 11 | return ( 12 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | // ? 全局不动配置项 只做导出不做修改 2 | 3 | // * 首页地址(默认) 4 | export const HOME_URL: string = "/home/index"; 5 | 6 | // * Tabs(黑名单地址,不需要添加到 tabs 的路由地址,暂时没用) 7 | export const TABS_BLACK_LIST: string[] = ["/403", "/404", "/500", "/layout", "/login", "/dataScreen"]; 8 | 9 | // * 高德地图key 10 | export const MAP_KEY: string = ""; 11 | -------------------------------------------------------------------------------- /src/config/nprogress.ts: -------------------------------------------------------------------------------- 1 | import NProgress from "nprogress"; 2 | import "nprogress/nprogress.css"; 3 | 4 | NProgress.configure({ 5 | easing: "ease", // 动画方式 6 | speed: 500, // 递增进度条的速度 7 | showSpinner: true, // 是否显示加载ico 8 | trickleSpeed: 200, // 自动递增间隔 9 | minimum: 0.3 // 初始化时的最小百分比 10 | }); 11 | 12 | export default NProgress; 13 | -------------------------------------------------------------------------------- /src/config/serviceLoading.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import Loading from "@/components/Loading"; 3 | 4 | let needLoadingRequestCount = 0; 5 | 6 | // * 显示loading 7 | export const showFullScreenLoading = () => { 8 | if (needLoadingRequestCount === 0) { 9 | let dom = document.createElement("div"); 10 | dom.setAttribute("id", "loading"); 11 | document.body.appendChild(dom); 12 | ReactDOM.createRoot(dom).render(); 13 | } 14 | needLoadingRequestCount++; 15 | }; 16 | 17 | // * 隐藏loading 18 | export const tryHideFullScreenLoading = () => { 19 | if (needLoadingRequestCount <= 0) return; 20 | needLoadingRequestCount--; 21 | if (needLoadingRequestCount === 0) { 22 | document.body.removeChild(document.getElementById("loading") as HTMLElement); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/enums/httpEnum.ts: -------------------------------------------------------------------------------- 1 | // * 请求枚举配置 2 | /** 3 | * @description:请求配置 4 | */ 5 | export enum ResultEnum { 6 | SUCCESS = 200, 7 | ERROR = 500, 8 | OVERDUE = 599, 9 | TIMEOUT = 10000, 10 | TYPE = "success" 11 | } 12 | 13 | /** 14 | * @description:请求方法 15 | */ 16 | export enum RequestEnum { 17 | GET = "GET", 18 | POST = "POST", 19 | PATCH = "PATCH", 20 | PUT = "PUT", 21 | DELETE = "DELETE" 22 | } 23 | 24 | /** 25 | * @description:常用的contentTyp类型 26 | */ 27 | export enum ContentTypeEnum { 28 | // json 29 | JSON = "application/json;charset=UTF-8", 30 | // text 31 | TEXT = "text/plain;charset=UTF-8", 32 | // form-data 一般配合qs 33 | FORM_URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8", 34 | // form-data 上传 35 | FORM_DATA = "multipart/form-data;charset=UTF-8" 36 | } 37 | -------------------------------------------------------------------------------- /src/hooks/useAuthButtons.ts: -------------------------------------------------------------------------------- 1 | import { searchRoute } from "@/utils/util"; 2 | import { useLocation } from "react-router-dom"; 3 | import { routerArray } from "@/routers"; 4 | import { store } from "@/redux"; 5 | 6 | /** 7 | * @description 页面按钮权限 hooks 8 | * */ 9 | const useAuthButtons = () => { 10 | const { pathname } = useLocation(); 11 | const route = searchRoute(pathname, routerArray); 12 | 13 | return { 14 | BUTTONS: store.getState().auth.authButtons[route.meta!.key!] || {} 15 | }; 16 | }; 17 | 18 | export default useAuthButtons; 19 | -------------------------------------------------------------------------------- /src/hooks/useEcharts.ts: -------------------------------------------------------------------------------- 1 | import * as echarts from "echarts"; 2 | import { useEffect, useRef } from "react"; 3 | /** 4 | * @description 使用Echarts(只是为了添加图表响应式) 5 | * @param {Element} data 数据 目前只针对于次Hooks-admin里一些data都是写死在options 所以data为可选 根据项目自行修改即可 6 | * @param {Object} options 绘制Echarts的参数(必传) 7 | * @return chart 8 | * */ 9 | export const useEcharts = (options: echarts.EChartsCoreOption, data?: any) => { 10 | const myChart = useRef(); 11 | const echartsRef = useRef(null); 12 | 13 | const echartsResize = () => { 14 | echartsRef && myChart?.current?.resize(); 15 | }; 16 | 17 | useEffect(() => { 18 | if (data?.length !== 0) { 19 | myChart?.current?.setOption(options); 20 | } 21 | }, [data]); 22 | 23 | useEffect(() => { 24 | if (echartsRef?.current) { 25 | myChart.current = echarts.init(echartsRef.current as HTMLDivElement); 26 | } 27 | myChart?.current?.setOption(options); 28 | window.addEventListener("resize", echartsResize, false); 29 | return () => { 30 | window.removeEventListener("resize", echartsResize); 31 | myChart?.current?.dispose(); 32 | }; 33 | }, []); 34 | 35 | return [echartsRef]; 36 | }; 37 | -------------------------------------------------------------------------------- /src/hooks/useTable.ts: -------------------------------------------------------------------------------- 1 | const useTable = () => {}; 2 | 3 | export default useTable; 4 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import defaultTheme from "@/styles/theme/theme-default.less"; 2 | import darkTheme from "@/styles/theme/theme-dark.less"; 3 | import { ThemeConfigProp } from "@/redux/interface"; 4 | 5 | /** 6 | * @description 全局主题设置 7 | * */ 8 | const useTheme = (themeConfig: ThemeConfigProp) => { 9 | const { weakOrGray, isDark } = themeConfig; 10 | const initTheme = () => { 11 | // 灰色和弱色切换 12 | const body = document.documentElement as HTMLElement; 13 | if (!weakOrGray) body.setAttribute("style", ""); 14 | if (weakOrGray === "weak") body.setAttribute("style", "filter: invert(80%)"); 15 | if (weakOrGray === "gray") body.setAttribute("style", "filter: grayscale(1)"); 16 | 17 | // 切换暗黑模式 18 | let head = document.getElementsByTagName("head")[0]; 19 | const getStyle = head.getElementsByTagName("style"); 20 | if (getStyle.length > 0) { 21 | for (let i = 0, l = getStyle.length; i < l; i++) { 22 | if (getStyle[i]?.getAttribute("data-type") === "dark") getStyle[i].remove(); 23 | } 24 | } 25 | let styleDom = document.createElement("style"); 26 | styleDom.dataset.type = "dark"; 27 | styleDom.innerHTML = isDark ? darkTheme : defaultTheme; 28 | head.appendChild(styleDom); 29 | }; 30 | initTheme(); 31 | 32 | return { 33 | initTheme 34 | }; 35 | }; 36 | 37 | export default useTheme; 38 | -------------------------------------------------------------------------------- /src/hooks/useTime.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { useEffect, useState, useRef } from "react"; 3 | 4 | /** 5 | * @description 获取本地时间 6 | */ 7 | export const useTimes = () => { 8 | const timer: any = useRef(null); 9 | const [time, setTime] = useState(moment().format("YYYY年MM月DD日 HH:mm:ss")); 10 | useEffect(() => { 11 | timer.current = setInterval(() => { 12 | setTime(moment().format("YYYY年MM月DD日 HH:mm:ss")); 13 | }, 1000); 14 | return () => { 15 | clearInterval(timer.current); 16 | }; 17 | }, [time]); 18 | 19 | return { 20 | time 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/language/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import enUsTrans from "./modules/en"; 3 | import zhCnTrans from "./modules/zh"; 4 | import { initReactI18next } from "react-i18next"; 5 | 6 | i18n.use(initReactI18next).init({ 7 | resources: { 8 | en: { 9 | translation: enUsTrans 10 | }, 11 | zh: { 12 | translation: zhCnTrans 13 | } 14 | }, 15 | // 选择默认语言,选择内容为上述配置中的 key,即 en/zh 16 | fallbackLng: "zh", 17 | debug: false, 18 | interpolation: { 19 | escapeValue: false // not needed for react as it escapes by default 20 | } 21 | }); 22 | 23 | export default i18n; 24 | -------------------------------------------------------------------------------- /src/language/modules/en.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | login: { 3 | confirm: "Login", 4 | reset: "Reset" 5 | }, 6 | home: { 7 | welcome: "Welcome" 8 | }, 9 | tabs: { 10 | more: "More", 11 | closeCurrent: "Current", 12 | closeOther: "Other", 13 | closeAll: "All" 14 | }, 15 | header: { 16 | componentSize: "Component Size", 17 | language: "Language", 18 | theme: "theme", 19 | themeSetting: "Theme setting", 20 | darkMode: "Dark Mode", 21 | lightMode: "Light Mode", 22 | fullScreen: "Full Screen", 23 | exitFullScreen: "Exit Full Screen", 24 | personalData: "Personal Data", 25 | changePassword: "Change Password", 26 | logout: "Logout" 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/language/modules/zh.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | login: { 3 | confirm: "登录", 4 | reset: "重置" 5 | }, 6 | home: { 7 | welcome: "欢迎使用" 8 | }, 9 | tabs: { 10 | more: "更多", 11 | closeCurrent: "关闭当前", 12 | closeOther: "关闭其它", 13 | closeAll: "关闭所有" 14 | }, 15 | header: { 16 | componentSize: "组件大小", 17 | language: "语言", 18 | theme: "主题", 19 | themeSetting: "主题设置", 20 | darkMode: "暗黑模式", 21 | lightMode: "浅色模式", 22 | fullScreen: "全屏", 23 | exitFullScreen: "退出全屏", 24 | personalData: "个人资料", 25 | changePassword: "修改密码", 26 | logout: "退出登录" 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/layouts/components/Footer/index.less: -------------------------------------------------------------------------------- 1 | .footer { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | height: 30px; 6 | border-top: 1px solid #e4e7ed; 7 | a { 8 | font-size: 14px; 9 | color: #858585; 10 | text-decoration: none; 11 | letter-spacing: 0.5px; 12 | white-space: nowrap; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/layouts/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import "./index.less"; 3 | 4 | const LayoutFooter = (props: any) => { 5 | const { themeConfig } = props; 6 | return ( 7 | <> 8 | {!themeConfig.footer && ( 9 | 14 | )} 15 | 16 | ); 17 | }; 18 | 19 | const mapStateToProps = (state: any) => state.global; 20 | export default connect(mapStateToProps)(LayoutFooter); 21 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/AssemblySize.tsx: -------------------------------------------------------------------------------- 1 | import { Dropdown, Menu } from "antd"; 2 | import { setAssemblySize } from "@/redux/modules/global/action"; 3 | import { connect } from "react-redux"; 4 | 5 | const AssemblySize = (props: any) => { 6 | const { assemblySize, setAssemblySize } = props; 7 | 8 | // 切换组件大小 9 | const onClick = (e: MenuInfo) => { 10 | setAssemblySize(e.key); 11 | }; 12 | 13 | const menu = ( 14 | 默认, 20 | onClick 21 | }, 22 | { 23 | disabled: assemblySize == "large", 24 | key: "large", 25 | label: 大型, 26 | onClick 27 | }, 28 | { 29 | disabled: assemblySize == "small", 30 | key: "small", 31 | label: 小型, 32 | onClick 33 | } 34 | ]} 35 | /> 36 | ); 37 | return ( 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | const mapStateToProps = (state: any) => state.global; 45 | const mapDispatchToProps = { setAssemblySize }; 46 | export default connect(mapStateToProps, mapDispatchToProps)(AssemblySize); 47 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/AvatarIcon.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import { Avatar, Modal, Menu, Dropdown, message } from "antd"; 3 | import { ExclamationCircleOutlined } from "@ant-design/icons"; 4 | import { useNavigate } from "react-router-dom"; 5 | import { HOME_URL } from "@/config/config"; 6 | import { connect } from "react-redux"; 7 | import { setToken } from "@/redux/modules/global/action"; 8 | import PasswordModal from "./PasswordModal"; 9 | import InfoModal from "./InfoModal"; 10 | import avatar from "@/assets/images/avatar.png"; 11 | 12 | const AvatarIcon = (props: any) => { 13 | const { setToken } = props; 14 | const navigate = useNavigate(); 15 | 16 | interface ModalProps { 17 | showModal: (params: { name: number }) => void; 18 | } 19 | const passRef = useRef(null); 20 | const infoRef = useRef(null); 21 | 22 | // 退出登录 23 | const logout = () => { 24 | Modal.confirm({ 25 | title: "温馨提示 🧡", 26 | icon: , 27 | content: "是否确认退出登录?", 28 | okText: "确认", 29 | cancelText: "取消", 30 | onOk: () => { 31 | setToken(""); 32 | message.success("退出登录成功!"); 33 | navigate("/login"); 34 | } 35 | }); 36 | }; 37 | 38 | // Dropdown Menu 39 | const menu = ( 40 | 首页, 45 | onClick: () => navigate(HOME_URL) 46 | }, 47 | { 48 | key: "2", 49 | label: 个人信息, 50 | onClick: () => infoRef.current!.showModal({ name: 11 }) 51 | }, 52 | { 53 | key: "3", 54 | label: 修改密码, 55 | onClick: () => passRef.current!.showModal({ name: 11 }) 56 | }, 57 | { 58 | type: "divider" 59 | }, 60 | { 61 | key: "4", 62 | label: 退出登录, 63 | onClick: logout 64 | } 65 | ]} 66 | > 67 | ); 68 | return ( 69 | <> 70 | 71 | 72 | 73 | 74 | 75 | 76 | ); 77 | }; 78 | 79 | const mapDispatchToProps = { setToken }; 80 | export default connect(null, mapDispatchToProps)(AvatarIcon); 81 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/BreadcrumbNav.tsx: -------------------------------------------------------------------------------- 1 | import { Breadcrumb } from "antd"; 2 | import { useLocation } from "react-router-dom"; 3 | import { HOME_URL } from "@/config/config"; 4 | import { connect } from "react-redux"; 5 | 6 | const BreadcrumbNav = (props: any) => { 7 | const { pathname } = useLocation(); 8 | const { themeConfig } = props.global; 9 | const breadcrumbList = props.breadcrumb.breadcrumbList[pathname] || []; 10 | 11 | return ( 12 | <> 13 | {!themeConfig.breadcrumb && ( 14 | 15 | 首页 16 | {breadcrumbList.map((item: string) => { 17 | return {item !== "首页" ? item : null}; 18 | })} 19 | 20 | )} 21 | 22 | ); 23 | }; 24 | 25 | const mapStateToProps = (state: any) => state; 26 | export default connect(mapStateToProps)(BreadcrumbNav); 27 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/CollapseIcon.tsx: -------------------------------------------------------------------------------- 1 | import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons"; 2 | import { connect } from "react-redux"; 3 | import { updateCollapse } from "@/redux/modules/menu/action"; 4 | 5 | const CollapseIcon = (props: any) => { 6 | const { isCollapse, updateCollapse } = props; 7 | return ( 8 |
{ 11 | updateCollapse(!isCollapse); 12 | }} 13 | > 14 | {isCollapse ? : } 15 |
16 | ); 17 | }; 18 | 19 | const mapStateToProps = (state: any) => state.menu; 20 | const mapDispatchToProps = { updateCollapse }; 21 | export default connect(mapStateToProps, mapDispatchToProps)(CollapseIcon); 22 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/Fullscreen.tsx: -------------------------------------------------------------------------------- 1 | import screenfull from "screenfull"; 2 | import { message } from "antd"; 3 | import { useEffect, useState } from "react"; 4 | 5 | const Fullscreen = () => { 6 | const [fullScreen, setFullScreen] = useState(screenfull.isFullscreen); 7 | 8 | useEffect(() => { 9 | screenfull.on("change", () => { 10 | if (screenfull.isFullscreen) setFullScreen(true); 11 | else setFullScreen(false); 12 | return () => screenfull.off("change", () => {}); 13 | }); 14 | }, []); 15 | 16 | const handleFullScreen = () => { 17 | if (!screenfull.isEnabled) message.warning("当前您的浏览器不支持全屏 ❌"); 18 | screenfull.toggle(); 19 | }; 20 | return ( 21 | 22 | ); 23 | }; 24 | export default Fullscreen; 25 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/InfoModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useImperativeHandle, Ref } from "react"; 2 | import { Modal, message } from "antd"; 3 | 4 | interface Props { 5 | innerRef: Ref<{ showModal: (params: any) => void } | undefined>; 6 | } 7 | 8 | const InfoModal = (props: Props) => { 9 | const [modalVisible, setModalVisible] = useState(false); 10 | 11 | useImperativeHandle(props.innerRef, () => ({ 12 | showModal 13 | })); 14 | 15 | const showModal = (params: { name: number }) => { 16 | console.log(params); 17 | setModalVisible(true); 18 | }; 19 | 20 | const handleOk = () => { 21 | setModalVisible(false); 22 | message.success("修改用户信息成功 🎉🎉🎉"); 23 | }; 24 | 25 | const handleCancel = () => { 26 | setModalVisible(false); 27 | }; 28 | return ( 29 | 30 |

User Info...

31 |

User Info...

32 |

User Info...

33 |
34 | ); 35 | }; 36 | export default InfoModal; 37 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/Language.tsx: -------------------------------------------------------------------------------- 1 | import { Dropdown, Menu } from "antd"; 2 | import { connect } from "react-redux"; 3 | import { setLanguage } from "@/redux/modules/global/action"; 4 | 5 | const Language = (props: any) => { 6 | const { language, setLanguage } = props; 7 | 8 | const menu = ( 9 | 简体中文, 14 | onClick: () => setLanguage("zh"), 15 | disabled: language === "zh" 16 | }, 17 | { 18 | key: "2", 19 | label: English, 20 | onClick: () => setLanguage("en"), 21 | disabled: language === "en" 22 | } 23 | ]} 24 | /> 25 | ); 26 | return ( 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | const mapStateToProps = (state: any) => state.global; 34 | const mapDispatchToProps = { setLanguage }; 35 | export default connect(mapStateToProps, mapDispatchToProps)(Language); 36 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/PasswordModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useImperativeHandle, Ref } from "react"; 2 | import { Modal, message } from "antd"; 3 | 4 | interface Props { 5 | innerRef: Ref<{ showModal: (params: any) => void }>; 6 | } 7 | 8 | const PasswordModal = (props: Props) => { 9 | const [isModalVisible, setIsModalVisible] = useState(false); 10 | 11 | useImperativeHandle(props.innerRef, () => ({ 12 | showModal 13 | })); 14 | 15 | const showModal = (params: { name: number }) => { 16 | console.log(params); 17 | setIsModalVisible(true); 18 | }; 19 | 20 | const handleOk = () => { 21 | setIsModalVisible(false); 22 | message.success("修改密码成功 🎉🎉🎉"); 23 | }; 24 | 25 | const handleCancel = () => { 26 | setIsModalVisible(false); 27 | }; 28 | return ( 29 | 30 |

Some Password...

31 |

Some Password...

32 |

Some Password...

33 |
34 | ); 35 | }; 36 | export default PasswordModal; 37 | -------------------------------------------------------------------------------- /src/layouts/components/Header/components/Theme.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer, Divider, Switch } from "antd"; 2 | import { useState } from "react"; 3 | import { connect } from "react-redux"; 4 | import { FireOutlined, SettingOutlined } from "@ant-design/icons"; 5 | import { setThemeConfig } from "@/redux/modules/global/action"; 6 | import { updateCollapse } from "@/redux/modules/menu/action"; 7 | import SwitchDark from "@/components/SwitchDark"; 8 | 9 | const Theme = (props: any) => { 10 | const [visible, setVisible] = useState(false); 11 | const { setThemeConfig, updateCollapse } = props; 12 | const { isCollapse } = props.menu; 13 | const { themeConfig } = props.global; 14 | const { weakOrGray, breadcrumb, tabs, footer } = themeConfig; 15 | 16 | const setWeakOrGray = (checked: boolean, theme: string) => { 17 | if (checked) return setThemeConfig({ ...themeConfig, weakOrGray: theme }); 18 | setThemeConfig({ ...themeConfig, weakOrGray: "" }); 19 | }; 20 | 21 | const onChange = (checked: boolean, keyName: string) => { 22 | return setThemeConfig({ ...themeConfig, [keyName]: !checked }); 23 | }; 24 | 25 | return ( 26 | <> 27 | { 30 | setVisible(true); 31 | }} 32 | > 33 | { 37 | setVisible(false); 38 | }} 39 | visible={visible} 40 | width={320} 41 | > 42 | {/* 全局主题 */} 43 | 44 | 45 | 全局主题 46 | 47 |
48 | 暗黑模式 49 | 50 |
51 |
52 | 灰色模式 53 | { 56 | setWeakOrGray(e, "gray"); 57 | }} 58 | /> 59 |
60 |
61 | 色弱模式 62 | { 65 | setWeakOrGray(e, "weak"); 66 | }} 67 | /> 68 |
69 |
70 | {/* 界面设置 */} 71 | 72 | 73 | 界面设置 74 | 75 |
76 | 折叠菜单 77 | { 80 | updateCollapse(e); 81 | }} 82 | /> 83 |
84 |
85 | 面包屑导航 86 | { 89 | onChange(e, "breadcrumb"); 90 | }} 91 | /> 92 |
93 |
94 | 标签栏 95 | { 98 | onChange(e, "tabs"); 99 | }} 100 | /> 101 |
102 |
103 | 页脚 104 | { 107 | onChange(e, "footer"); 108 | }} 109 | /> 110 |
111 |
112 | 113 | ); 114 | }; 115 | 116 | const mapStateToProps = (state: any) => state; 117 | const mapDispatchToProps = { setThemeConfig, updateCollapse }; 118 | export default connect(mapStateToProps, mapDispatchToProps)(Theme); 119 | -------------------------------------------------------------------------------- /src/layouts/components/Header/index.less: -------------------------------------------------------------------------------- 1 | .ant-layout-header { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | border-bottom: 1px solid #f6f6f6; 6 | .header-lf { 7 | display: flex; 8 | align-items: center; 9 | .collapsed { 10 | margin-right: 20px; 11 | font-size: 18px; 12 | cursor: pointer; 13 | transition: color 0.3s; 14 | } 15 | } 16 | .header-ri { 17 | display: flex; 18 | align-items: center; 19 | .icon-style { 20 | margin-right: 22px; 21 | font-size: 19px; 22 | line-height: 19px; 23 | cursor: pointer; 24 | } 25 | .username { 26 | margin: 0 20px 0 0; 27 | font-size: 15px; 28 | } 29 | .ant-avatar { 30 | cursor: pointer; 31 | } 32 | } 33 | } 34 | .theme-item { 35 | display: flex; 36 | align-items: center; 37 | justify-content: space-between; 38 | margin: 25px 0; 39 | span { 40 | font-size: 14px; 41 | } 42 | .ant-switch { 43 | width: 46px; 44 | } 45 | } 46 | .divider { 47 | margin: 0 0 22px !important; 48 | font-size: 15px !important; 49 | .anticon { 50 | margin-right: 10px; 51 | } 52 | } 53 | .ant-divider-with-text::before, 54 | .ant-divider-with-text::after { 55 | border-top: 1px solid #dcdfe6 !important; 56 | } 57 | -------------------------------------------------------------------------------- /src/layouts/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from "antd"; 2 | import AvatarIcon from "./components/AvatarIcon"; 3 | import CollapseIcon from "./components/CollapseIcon"; 4 | import BreadcrumbNav from "./components/BreadcrumbNav"; 5 | import AssemblySize from "./components/AssemblySize"; 6 | import Language from "./components/Language"; 7 | import Theme from "./components/Theme"; 8 | import Fullscreen from "./components/Fullscreen"; 9 | import "./index.less"; 10 | 11 | const LayoutHeader = () => { 12 | const { Header } = Layout; 13 | 14 | return ( 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | 25 | Hooks 26 | 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default LayoutHeader; 33 | -------------------------------------------------------------------------------- /src/layouts/components/Menu/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import logo from "@/assets/images/logo.png"; 2 | import { connect } from "react-redux"; 3 | 4 | const Logo = (props: any) => { 5 | const { isCollapse } = props; 6 | return ( 7 |
8 | logo 9 | {!isCollapse ?

Hooks Admin

: null} 10 |
11 | ); 12 | }; 13 | 14 | const mapStateToProps = (state: any) => state.menu; 15 | export default connect(mapStateToProps)(Logo); 16 | -------------------------------------------------------------------------------- /src/layouts/components/Menu/index.less: -------------------------------------------------------------------------------- 1 | .menu { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | height: 100%; 6 | .logo-box { 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | height: 55px; 11 | .logo-img { 12 | width: 30px; 13 | margin: 0; 14 | } 15 | .logo-text { 16 | margin: 0 0 0 10px; 17 | font-size: 24px; 18 | font-weight: bold; 19 | color: #dadada; 20 | white-space: nowrap; 21 | } 22 | } 23 | .ant-menu-root { 24 | flex: 1; 25 | overflow-x: hidden; 26 | overflow-y: auto; 27 | } 28 | 29 | /* 去除菜单 Loading 遮罩层 */ 30 | .ant-spin-nested-loading, 31 | .ant-spin-container { 32 | display: flex; 33 | flex-direction: column; 34 | height: 100%; 35 | .ant-spin { 36 | max-height: 100% !important; 37 | } 38 | .ant-spin-container::after { 39 | background: transparent !important; 40 | } 41 | .ant-spin-blur { 42 | overflow: auto !important; 43 | clear: none !important; 44 | opacity: 1 !important; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/layouts/components/Tabs/components/MoreButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Dropdown, Menu } from "antd"; 2 | import { DownOutlined } from "@ant-design/icons"; 3 | import { useLocation, useNavigate } from "react-router-dom"; 4 | import { useTranslation } from "react-i18next"; 5 | import { HOME_URL } from "@/config/config"; 6 | 7 | const MoreButton = (props: any) => { 8 | const { t } = useTranslation(); 9 | const { pathname } = useLocation(); 10 | const navigate = useNavigate(); 11 | 12 | // close multipleTab 13 | const closeMultipleTab = (tabPath?: string) => { 14 | const handleTabsList = props.tabsList.filter((item: Menu.MenuOptions) => { 15 | return item.path === tabPath || item.path === HOME_URL; 16 | }); 17 | props.setTabsList(handleTabsList); 18 | tabPath ?? navigate(HOME_URL); 19 | }; 20 | 21 | const menu = ( 22 | {t("tabs.closeCurrent")}, 27 | onClick: () => props.delTabs(pathname) 28 | }, 29 | { 30 | key: "2", 31 | label: {t("tabs.closeOther")}, 32 | onClick: () => closeMultipleTab(pathname) 33 | }, 34 | { 35 | key: "3", 36 | label: {t("tabs.closeAll")}, 37 | onClick: () => closeMultipleTab() 38 | } 39 | ]} 40 | /> 41 | ); 42 | return ( 43 | 44 | 47 | 48 | ); 49 | }; 50 | export default MoreButton; 51 | -------------------------------------------------------------------------------- /src/layouts/components/Tabs/index.less: -------------------------------------------------------------------------------- 1 | .tabs { 2 | position: relative; 3 | border-bottom: 1px solid #e4e7ed; 4 | .ant-tabs { 5 | padding: 0 90px 0 13px; 6 | .ant-tabs-nav { 7 | margin: 0; 8 | &::before { 9 | border: none; 10 | } 11 | .ant-tabs-ink-bar { 12 | visibility: visible; 13 | } 14 | .ant-tabs-tab-with-remove.ant-tabs-tab-active { 15 | .ant-tabs-tab-remove { 16 | top: 1px; 17 | margin: 7px; 18 | color: @primary-color !important; 19 | opacity: 1 !important; 20 | } 21 | .ant-tabs-tab-btn { 22 | transform: translateX(-9px); 23 | } 24 | } 25 | .ant-tabs-tab { 26 | padding: 8px 22px; 27 | color: #cccccc; 28 | background: none; 29 | border: none; 30 | transition: none; 31 | .anticon-home { 32 | margin-right: 7px; 33 | } 34 | .ant-tabs-tab-remove { 35 | position: absolute; 36 | right: 0; 37 | color: #cccccc; 38 | opacity: 0; 39 | transition: 0.1s ease-in-out; 40 | &:hover { 41 | color: @primary-color; 42 | } 43 | } 44 | } 45 | .ant-tabs-tab.ant-tabs-tab-with-remove { 46 | &:hover { 47 | .ant-tabs-tab-remove { 48 | top: 1px; 49 | margin: 7px; 50 | opacity: 1; 51 | transition: 0.1s ease-in-out; 52 | } 53 | .ant-tabs-tab-btn { 54 | transform: translateX(-9px); 55 | } 56 | } 57 | } 58 | } 59 | } 60 | .more-button { 61 | position: absolute; 62 | top: 8px; 63 | right: 13px; 64 | padding-left: 10px; 65 | font-size: 12px; 66 | } 67 | } 68 | 69 | /* tabs 超出显示的样式 */ 70 | .ant-tabs-dropdown { 71 | .ant-tabs-dropdown-menu-item { 72 | .anticon-home { 73 | margin-right: 7px; 74 | } 75 | } 76 | } 77 | 78 | /* tabs 不受全局组件大小影响 */ 79 | .ant-tabs-small > .ant-tabs-nav .ant-tabs-tab, 80 | .ant-tabs-large > .ant-tabs-nav .ant-tabs-tab { 81 | padding: 8px 22px !important; 82 | font-size: 14px !important; 83 | } 84 | -------------------------------------------------------------------------------- /src/layouts/components/Tabs/index.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs, message } from "antd"; 2 | import { HomeFilled } from "@ant-design/icons"; 3 | import { useEffect, useState } from "react"; 4 | import { useLocation, useNavigate } from "react-router-dom"; 5 | import { HOME_URL } from "@/config/config"; 6 | import { connect } from "react-redux"; 7 | import { setTabsList } from "@/redux/modules/tabs/action"; 8 | import { routerArray } from "@/routers"; 9 | import { searchRoute } from "@/utils/util"; 10 | import MoreButton from "./components/MoreButton"; 11 | import "./index.less"; 12 | 13 | const LayoutTabs = (props: any) => { 14 | const { tabsList } = props.tabs; 15 | const { themeConfig } = props.global; 16 | const { setTabsList } = props; 17 | const { TabPane } = Tabs; 18 | const { pathname } = useLocation(); 19 | const navigate = useNavigate(); 20 | const [activeValue, setActiveValue] = useState(pathname); 21 | 22 | useEffect(() => { 23 | addTabs(); 24 | }, [pathname]); 25 | 26 | // click tabs 27 | const clickTabs = (path: string) => { 28 | navigate(path); 29 | }; 30 | 31 | // add tabs 32 | const addTabs = () => { 33 | const route = searchRoute(pathname, routerArray); 34 | let newTabsList = JSON.parse(JSON.stringify(tabsList)); 35 | if (tabsList.every((item: any) => item.path !== route.path)) { 36 | newTabsList.push({ title: route.meta!.title, path: route.path }); 37 | } 38 | setTabsList(newTabsList); 39 | setActiveValue(pathname); 40 | }; 41 | 42 | // delete tabs 43 | const delTabs = (tabPath?: string) => { 44 | if (tabPath === HOME_URL) return; 45 | if (pathname === tabPath) { 46 | tabsList.forEach((item: Menu.MenuOptions, index: number) => { 47 | if (item.path !== pathname) return; 48 | const nextTab = tabsList[index + 1] || tabsList[index - 1]; 49 | if (!nextTab) return; 50 | navigate(nextTab.path); 51 | }); 52 | } 53 | message.success("你删除了Tabs标签 😆😆😆"); 54 | setTabsList(tabsList.filter((item: Menu.MenuOptions) => item.path !== tabPath)); 55 | }; 56 | 57 | return ( 58 | <> 59 | {!themeConfig.tabs && ( 60 |
61 | { 68 | delTabs(path as string); 69 | }} 70 | > 71 | {tabsList.map((item: Menu.MenuOptions) => { 72 | return ( 73 | 77 | {item.path == HOME_URL ? : ""} 78 | {item.title} 79 | 80 | } 81 | closable={item.path !== HOME_URL} 82 | > 83 | ); 84 | })} 85 | 86 | 87 |
88 | )} 89 | 90 | ); 91 | }; 92 | 93 | const mapStateToProps = (state: any) => state; 94 | const mapDispatchToProps = { setTabsList }; 95 | export default connect(mapStateToProps, mapDispatchToProps)(LayoutTabs); 96 | -------------------------------------------------------------------------------- /src/layouts/index.less: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | min-width: 950px; 4 | height: 100%; 5 | .ant-layout-sider { 6 | box-sizing: border-box; 7 | } 8 | .ant-layout { 9 | /* 防止 tabs 超出不收缩 */ 10 | overflow-x: hidden; 11 | .ant-layout-content { 12 | box-sizing: border-box; 13 | flex: 1; 14 | padding: 10px 12px; 15 | overflow-x: hidden; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/layouts/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Outlet } from "react-router-dom"; 3 | import { Layout } from "antd"; 4 | import { setAuthButtons } from "@/redux/modules/auth/action"; 5 | import { updateCollapse } from "@/redux/modules/menu/action"; 6 | import { getAuthorButtons } from "@/api/modules/login"; 7 | import { connect } from "react-redux"; 8 | import LayoutMenu from "./components/Menu"; 9 | import LayoutHeader from "./components/Header"; 10 | import LayoutTabs from "./components/Tabs"; 11 | import LayoutFooter from "./components/Footer"; 12 | import "./index.less"; 13 | 14 | const LayoutIndex = (props: any) => { 15 | const { Sider, Content } = Layout; 16 | const { isCollapse, updateCollapse, setAuthButtons } = props; 17 | 18 | // 获取按钮权限列表 19 | const getAuthButtonsList = async () => { 20 | const { data } = await getAuthorButtons(); 21 | setAuthButtons(data); 22 | }; 23 | 24 | // 监听窗口大小变化 25 | const listeningWindow = () => { 26 | window.onresize = () => { 27 | return (() => { 28 | let screenWidth = document.body.clientWidth; 29 | if (!isCollapse && screenWidth < 1200) updateCollapse(true); 30 | if (!isCollapse && screenWidth > 1200) updateCollapse(false); 31 | })(); 32 | }; 33 | }; 34 | 35 | useEffect(() => { 36 | listeningWindow(); 37 | getAuthButtonsList(); 38 | }, []); 39 | 40 | return ( 41 | // 这里不用 Layout 组件原因是切换页面时样式会先错乱然后在正常显示,造成页面闪屏效果 42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | ); 56 | }; 57 | 58 | const mapStateToProps = (state: any) => state.menu; 59 | const mapDispatchToProps = { setAuthButtons, updateCollapse }; 60 | export default connect(mapStateToProps, mapDispatchToProps)(LayoutIndex); 61 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import "@/styles/reset.less"; 3 | import "@/assets/iconfont/iconfont.less"; 4 | import "@/assets/fonts/font.less"; 5 | // import "antd/dist/antd.less"; 6 | import "@/styles/common.less"; 7 | import "@/language/index"; 8 | import "virtual:svg-icons-register"; 9 | import { PersistGate } from "redux-persist/integration/react"; 10 | import { Provider } from "react-redux"; 11 | import { store, persistor } from "@/redux"; 12 | import App from "@/App"; 13 | 14 | // react 17 创建,控制台会报错,暂时不影响使用(菜单折叠时不会出现闪烁) 15 | ReactDOM.render( 16 | // * react严格模式 17 | // 18 | 19 | 20 | 21 | 22 | , 23 | // , 24 | document.getElementById("root") 25 | ); 26 | 27 | // import ReactDOM from "react-dom/client"; 28 | // react 18 创建(会导致 antd 菜单折叠时闪烁,等待官方修复) 29 | // ReactDOM.createRoot(document.getElementById("root")!).render( 30 | // // * react严格模式 31 | // // 32 | // 33 | // 34 | // 35 | // 36 | // 37 | // // 38 | // ); 39 | -------------------------------------------------------------------------------- /src/redux/index.ts: -------------------------------------------------------------------------------- 1 | import { legacy_createStore as createStore, combineReducers, Store, compose } from "redux"; 2 | import { persistStore, persistReducer } from "redux-persist"; 3 | import { applyMiddleware } from "redux"; 4 | import storage from "redux-persist/lib/storage"; 5 | import reduxThunk from "redux-thunk"; 6 | import reduxPromise from "redux-promise"; 7 | import global from "./modules/global/reducer"; 8 | import menu from "./modules/menu/reducer"; 9 | import tabs from "./modules/tabs/reducer"; 10 | import auth from "./modules/auth/reducer"; 11 | import breadcrumb from "./modules/breadcrumb/reducer"; 12 | 13 | // 创建reducer(拆分reducer) 14 | const reducer = combineReducers({ 15 | global, 16 | menu, 17 | tabs, 18 | auth, 19 | breadcrumb 20 | }); 21 | 22 | // redux 持久化配置 23 | const persistConfig = { 24 | key: "redux-state", 25 | storage: storage 26 | }; 27 | const persistReducerConfig = persistReducer(persistConfig, reducer); 28 | 29 | // 开启 redux-devtools 30 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 31 | 32 | // 使用 redux 中间件 33 | const middleWares = applyMiddleware(reduxThunk, reduxPromise); 34 | 35 | // 创建 store 36 | const store: Store = createStore(persistReducerConfig, composeEnhancers(middleWares)); 37 | 38 | // 创建持久化 store 39 | const persistor = persistStore(store); 40 | 41 | export { store, persistor }; 42 | -------------------------------------------------------------------------------- /src/redux/interface/index.ts: -------------------------------------------------------------------------------- 1 | import type { SizeType } from "antd/lib/config-provider/SizeContext"; 2 | 3 | /* themeConfigProp */ 4 | export interface ThemeConfigProp { 5 | primary: string; 6 | isDark: boolean; 7 | weakOrGray: string; 8 | breadcrumb: boolean; 9 | tabs: boolean; 10 | footer: boolean; 11 | } 12 | 13 | /* GlobalState */ 14 | export interface GlobalState { 15 | token: string; 16 | userInfo: any; 17 | assemblySize: SizeType; 18 | language: string; 19 | themeConfig: ThemeConfigProp; 20 | } 21 | 22 | /* MenuState */ 23 | export interface MenuState { 24 | isCollapse: boolean; 25 | menuList: Menu.MenuOptions[]; 26 | } 27 | 28 | /* TabsState */ 29 | export interface TabsState { 30 | tabsActive: string; 31 | tabsList: Menu.MenuOptions[]; 32 | } 33 | 34 | /* BreadcrumbState */ 35 | export interface BreadcrumbState { 36 | breadcrumbList: { 37 | [propName: string]: any; 38 | }; 39 | } 40 | 41 | /* AuthState */ 42 | export interface AuthState { 43 | authButtons: { 44 | [propName: string]: any; 45 | }; 46 | authRouter: string[]; 47 | } 48 | -------------------------------------------------------------------------------- /src/redux/modules/auth/action.ts: -------------------------------------------------------------------------------- 1 | import * as types from "@/redux/mutation-types"; 2 | 3 | // * setAuthButtons 4 | export const setAuthButtons = (authButtons: { [propName: string]: any }) => ({ 5 | type: types.SET_AUTH_BUTTONS, 6 | authButtons 7 | }); 8 | 9 | // * setAuthRouter 10 | export const setAuthRouter = (authRouter: string[]) => ({ 11 | type: types.SET_AUTH_ROUTER, 12 | authRouter 13 | }); 14 | -------------------------------------------------------------------------------- /src/redux/modules/auth/reducer.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from "redux"; 2 | import { AuthState } from "@/redux/interface"; 3 | import produce from "immer"; 4 | import * as types from "@/redux/mutation-types"; 5 | 6 | const authState: AuthState = { 7 | authButtons: {}, 8 | authRouter: [] 9 | }; 10 | 11 | // auth reducer 12 | const auth = (state: AuthState = authState, action: AnyAction) => 13 | produce(state, draftState => { 14 | switch (action.type) { 15 | case types.SET_AUTH_BUTTONS: 16 | draftState.authButtons = action.authButtons; 17 | break; 18 | case types.SET_AUTH_ROUTER: 19 | draftState.authRouter = action.authRouter; 20 | break; 21 | default: 22 | return draftState; 23 | } 24 | }); 25 | 26 | export default auth; 27 | -------------------------------------------------------------------------------- /src/redux/modules/breadcrumb/action.ts: -------------------------------------------------------------------------------- 1 | import * as types from "@/redux/mutation-types"; 2 | 3 | // * setBreadcrumbList 4 | export const setBreadcrumbList = (breadcrumbList: { [propName: string]: any }) => ({ 5 | type: types.SET_BREADCRUMB_LIST, 6 | breadcrumbList 7 | }); 8 | -------------------------------------------------------------------------------- /src/redux/modules/breadcrumb/reducer.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from "redux"; 2 | import { BreadcrumbState } from "@/redux/interface"; 3 | import produce from "immer"; 4 | import * as types from "@/redux/mutation-types"; 5 | 6 | const breadcrumbState: BreadcrumbState = { 7 | breadcrumbList: {} 8 | }; 9 | 10 | // breadcrumb reducer 11 | const breadcrumb = (state: BreadcrumbState = breadcrumbState, action: AnyAction) => 12 | produce(state, draftState => { 13 | switch (action.type) { 14 | case types.SET_BREADCRUMB_LIST: 15 | draftState.breadcrumbList = action.breadcrumbList; 16 | break; 17 | default: 18 | return draftState; 19 | } 20 | }); 21 | 22 | export default breadcrumb; 23 | -------------------------------------------------------------------------------- /src/redux/modules/global/action.ts: -------------------------------------------------------------------------------- 1 | import * as types from "@/redux/mutation-types"; 2 | import { ThemeConfigProp } from "@/redux/interface/index"; 3 | 4 | // * setToken 5 | export const setToken = (token: string) => ({ 6 | type: types.SET_TOKEN, 7 | token 8 | }); 9 | 10 | // * setAssemblySize 11 | export const setAssemblySize = (assemblySize: string) => ({ 12 | type: types.SET_ASSEMBLY_SIZE, 13 | assemblySize 14 | }); 15 | 16 | // * setLanguage 17 | export const setLanguage = (language: string) => ({ 18 | type: types.SET_LANGUAGE, 19 | language 20 | }); 21 | 22 | // * setThemeConfig 23 | export const setThemeConfig = (themeConfig: ThemeConfigProp) => ({ 24 | type: types.SET_THEME_CONFIG, 25 | themeConfig 26 | }); 27 | -------------------------------------------------------------------------------- /src/redux/modules/global/reducer.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from "redux"; 2 | import { GlobalState } from "@/redux/interface"; 3 | import produce from "immer"; 4 | import * as types from "@/redux/mutation-types"; 5 | 6 | const globalState: GlobalState = { 7 | token: "", 8 | userInfo: "", 9 | assemblySize: "middle", 10 | language: "", 11 | themeConfig: { 12 | // 默认 primary 主题颜色 13 | primary: "#1890ff", 14 | // 深色模式 15 | isDark: false, 16 | // 色弱模式(weak) || 灰色模式(gray) 17 | weakOrGray: "", 18 | // 面包屑导航 19 | breadcrumb: true, 20 | // 标签页 21 | tabs: true, 22 | // 页脚 23 | footer: true 24 | } 25 | }; 26 | 27 | // global reducer 28 | const global = (state: GlobalState = globalState, action: AnyAction) => 29 | produce(state, draftState => { 30 | switch (action.type) { 31 | case types.SET_TOKEN: 32 | draftState.token = action.token; 33 | break; 34 | case types.SET_ASSEMBLY_SIZE: 35 | draftState.assemblySize = action.assemblySize; 36 | break; 37 | case types.SET_LANGUAGE: 38 | draftState.language = action.language; 39 | break; 40 | case types.SET_THEME_CONFIG: 41 | draftState.themeConfig = action.themeConfig; 42 | break; 43 | default: 44 | return draftState; 45 | } 46 | }); 47 | 48 | export default global; 49 | -------------------------------------------------------------------------------- /src/redux/modules/menu/action.ts: -------------------------------------------------------------------------------- 1 | import * as types from "@/redux/mutation-types"; 2 | import { getMenuList } from "@/api/modules/login"; 3 | import { Dispatch } from "react"; 4 | 5 | // * updateCollapse 6 | export const updateCollapse = (isCollapse: boolean) => ({ 7 | type: types.UPDATE_COLLAPSE, 8 | isCollapse 9 | }); 10 | 11 | // * setMenuList 12 | export const setMenuList = (menuList: Menu.MenuOptions[]) => ({ 13 | type: types.SET_MENU_LIST, 14 | menuList 15 | }); 16 | 17 | // ? 下面方法仅为测试使用,不参与任何功能开发 18 | interface MenuProps { 19 | type: string; 20 | menuList: Menu.MenuOptions[]; 21 | } 22 | // * redux-thunk 23 | export const getMenuListActionThunk = () => { 24 | return async (dispatch: Dispatch) => { 25 | const res = await getMenuList(); 26 | dispatch({ 27 | type: types.SET_MENU_LIST, 28 | menuList: (res.data as Menu.MenuOptions[]) ?? [] 29 | }); 30 | }; 31 | }; 32 | 33 | // * redux-promise《async/await》 34 | export const getMenuListAction = async (): Promise => { 35 | const res = await getMenuList(); 36 | return { 37 | type: types.SET_MENU_LIST, 38 | menuList: res.data ? res.data : [] 39 | }; 40 | }; 41 | 42 | // * redux-promise《.then/.catch》 43 | export const getMenuListActionPromise = (): Promise => { 44 | return getMenuList().then(res => { 45 | return { 46 | type: types.SET_MENU_LIST, 47 | menuList: res.data ? res.data : [] 48 | }; 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /src/redux/modules/menu/reducer.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from "redux"; 2 | import { MenuState } from "@/redux/interface"; 3 | import produce from "immer"; 4 | import * as types from "@/redux/mutation-types"; 5 | 6 | const menuState: MenuState = { 7 | isCollapse: false, 8 | menuList: [] 9 | }; 10 | 11 | // menu reducer 12 | const menu = (state: MenuState = menuState, action: AnyAction) => 13 | produce(state, draftState => { 14 | switch (action.type) { 15 | case types.UPDATE_COLLAPSE: 16 | draftState.isCollapse = action.isCollapse; 17 | break; 18 | case types.SET_MENU_LIST: 19 | draftState.menuList = action.menuList; 20 | break; 21 | default: 22 | return draftState; 23 | } 24 | }); 25 | 26 | export default menu; 27 | -------------------------------------------------------------------------------- /src/redux/modules/tabs/action.ts: -------------------------------------------------------------------------------- 1 | import * as types from "@/redux/mutation-types"; 2 | 3 | // * setTabsList 4 | export const setTabsList = (tabsList: Menu.MenuOptions[]) => ({ 5 | type: types.SET_TABS_LIST, 6 | tabsList 7 | }); 8 | 9 | // * setTabsActive 10 | export const setTabsActive = (tabsActive: string) => ({ 11 | type: types.SET_TABS_ACTIVE, 12 | tabsActive 13 | }); 14 | -------------------------------------------------------------------------------- /src/redux/modules/tabs/reducer.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from "redux"; 2 | import { TabsState } from "@/redux/interface"; 3 | import { HOME_URL } from "@/config/config"; 4 | import produce from "immer"; 5 | import * as types from "@/redux/mutation-types"; 6 | 7 | const tabsState: TabsState = { 8 | // tabsActive 其实没啥用,使用 pathname 就可以了😂 9 | tabsActive: HOME_URL, 10 | tabsList: [{ title: "首页", path: HOME_URL }] 11 | }; 12 | 13 | // tabs reducer 14 | const tabs = (state: TabsState = tabsState, action: AnyAction) => 15 | produce(state, draftState => { 16 | switch (action.type) { 17 | case types.SET_TABS_LIST: 18 | draftState.tabsList = action.tabsList; 19 | break; 20 | case types.SET_TABS_ACTIVE: 21 | draftState.tabsActive = action.tabsActive; 22 | break; 23 | default: 24 | return draftState; 25 | } 26 | }); 27 | 28 | export default tabs; 29 | -------------------------------------------------------------------------------- /src/redux/mutation-types.ts: -------------------------------------------------------------------------------- 1 | // 更新 menu 折叠状态 2 | export const UPDATE_COLLAPSE = "UPDATE_ASIDE_COLLAPSE"; 3 | // 设置 menuList 4 | export const SET_MENU_LIST = "SET_MENU_LIST"; 5 | // 设置 tabsList 6 | export const SET_TABS_LIST = "SET_TABS_LIST"; 7 | // 设置 tabsActive 8 | export const SET_TABS_ACTIVE = "SET_TABS_ACTIVE"; 9 | // 设置 breadcrumb 10 | export const SET_BREADCRUMB_LIST = "SET_BREADCRUMB_LIST"; 11 | // 设置 authButtons 12 | export const SET_AUTH_BUTTONS = "SET_AUTH_BUTTONS"; 13 | // 设置 authRouter 14 | export const SET_AUTH_ROUTER = "SET_AUTH_ROUTER"; 15 | // 设置 token 16 | export const SET_TOKEN = "SET_TOKEN"; 17 | // 设置 assemblySize 18 | export const SET_ASSEMBLY_SIZE = "SET_ASSEMBLY_SIZE"; 19 | // 设置 setLanguage 20 | export const SET_LANGUAGE = "SET_LANGUAGE"; 21 | // 设置 setThemeConfig 22 | export const SET_THEME_CONFIG = "SET_THEME_CONFIG"; 23 | -------------------------------------------------------------------------------- /src/routers/constant.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "@/layouts/index"; 2 | // 懒加载 Layout 3 | // import React from "react"; 4 | // import lazyLoad from "@/routers/utils/lazyLoad"; 5 | // const Layout = lazyLoad(React.lazy(() => import("@/layouts/index"))); 6 | 7 | /** 8 | * @description: default layout 9 | */ 10 | export const LayoutIndex = () => ; 11 | -------------------------------------------------------------------------------- /src/routers/index.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, useRoutes } from "react-router-dom"; 2 | import { RouteObject } from "@/routers/interface"; 3 | import Login from "@/views/login/index"; 4 | 5 | // * 导入所有router 6 | const metaRouters = import.meta.globEager("./modules/*.tsx"); 7 | 8 | // * 处理路由 9 | export const routerArray: RouteObject[] = []; 10 | Object.keys(metaRouters).forEach(item => { 11 | Object.keys(metaRouters[item]).forEach((key: any) => { 12 | routerArray.push(...metaRouters[item][key]); 13 | }); 14 | }); 15 | 16 | export const rootRouter: RouteObject[] = [ 17 | { 18 | path: "/", 19 | element: 20 | }, 21 | { 22 | path: "/login", 23 | element: , 24 | meta: { 25 | requiresAuth: false, 26 | title: "登录页", 27 | key: "login" 28 | } 29 | }, 30 | ...routerArray, 31 | { 32 | path: "*", 33 | element: 34 | } 35 | ]; 36 | 37 | const Router = () => { 38 | const routes = useRoutes(rootRouter); 39 | return routes; 40 | }; 41 | 42 | export default Router; 43 | -------------------------------------------------------------------------------- /src/routers/interface/index.ts: -------------------------------------------------------------------------------- 1 | export interface MetaProps { 2 | keepAlive?: boolean; 3 | requiresAuth?: boolean; 4 | title: string; 5 | key?: string; 6 | } 7 | 8 | export interface RouteObject { 9 | caseSensitive?: boolean; 10 | children?: RouteObject[]; 11 | element?: React.ReactNode; 12 | index?: boolean; 13 | path?: string; 14 | meta?: MetaProps; 15 | isLink?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/routers/modules/assembly.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import lazyLoad from "@/routers/utils/lazyLoad"; 3 | import { LayoutIndex } from "@/routers/constant"; 4 | import { RouteObject } from "@/routers/interface"; 5 | 6 | // 常用组件模块 7 | const assemblyRouter: Array = [ 8 | { 9 | element: , 10 | meta: { 11 | title: "常用组件" 12 | }, 13 | children: [ 14 | { 15 | path: "/assembly/guide", 16 | element: lazyLoad(React.lazy(() => import("@/views/assembly/guide/index"))), 17 | meta: { 18 | requiresAuth: true, 19 | title: "引导页", 20 | key: "guide" 21 | } 22 | }, 23 | { 24 | path: "/assembly/svgIcon", 25 | element: lazyLoad(React.lazy(() => import("@/views/assembly/svgIcon/index"))), 26 | meta: { 27 | requiresAuth: true, 28 | title: "SVG 图标", 29 | key: "svgIcon" 30 | } 31 | }, 32 | { 33 | path: "/assembly/selectIcon", 34 | element: lazyLoad(React.lazy(() => import("@/views/assembly/selectIcon/index"))), 35 | meta: { 36 | requiresAuth: true, 37 | title: "Icon 选择", 38 | key: "selectIcon" 39 | } 40 | }, 41 | { 42 | path: "/assembly/batchImport", 43 | element: lazyLoad(React.lazy(() => import("@/views/assembly/batchImport/index"))), 44 | meta: { 45 | requiresAuth: true, 46 | title: "批量导入数据", 47 | key: "selectIcon" 48 | } 49 | } 50 | ] 51 | } 52 | ]; 53 | 54 | export default assemblyRouter; 55 | -------------------------------------------------------------------------------- /src/routers/modules/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import lazyLoad from "@/routers/utils/lazyLoad"; 3 | import { LayoutIndex } from "@/routers/constant"; 4 | import { RouteObject } from "@/routers/interface"; 5 | 6 | // dashboard 模块 7 | const dashboardRouter: Array = [ 8 | { 9 | element: , 10 | meta: { 11 | title: "Dashboard" 12 | }, 13 | children: [ 14 | { 15 | path: "/dashboard/dataVisualize", 16 | element: lazyLoad(React.lazy(() => import("@/views/dashboard/dataVisualize/index"))), 17 | meta: { 18 | requiresAuth: true, 19 | title: "数据可视化", 20 | key: "dataVisualize" 21 | } 22 | }, 23 | { 24 | path: "/dashboard/embedded", 25 | element: lazyLoad(React.lazy(() => import("@/views/dashboard/embedded/index"))), 26 | meta: { 27 | requiresAuth: true, 28 | title: "内嵌页面", 29 | key: "embedded" 30 | } 31 | } 32 | ] 33 | } 34 | ]; 35 | 36 | export default dashboardRouter; 37 | -------------------------------------------------------------------------------- /src/routers/modules/dataScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import lazyLoad from "@/routers/utils/lazyLoad"; 3 | import { RouteObject } from "@/routers/interface"; 4 | 5 | // 数据大屏模块 6 | const dataScreenRouter: Array = [ 7 | { 8 | path: "/dataScreen/index", 9 | element: lazyLoad(React.lazy(() => import("@/views/dataScreen/index"))), 10 | meta: { 11 | requiresAuth: true, 12 | title: "数据大屏", 13 | key: "dataScreen" 14 | } 15 | } 16 | ]; 17 | 18 | export default dataScreenRouter; 19 | -------------------------------------------------------------------------------- /src/routers/modules/echarts.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import lazyLoad from "@/routers/utils/lazyLoad"; 3 | import { LayoutIndex } from "@/routers/constant"; 4 | import { RouteObject } from "@/routers/interface"; 5 | 6 | // echarts 模块 7 | const formRouter: Array = [ 8 | { 9 | element: , 10 | meta: { 11 | title: "Echarts" 12 | }, 13 | children: [ 14 | { 15 | path: "/echarts/waterChart", 16 | element: lazyLoad(React.lazy(() => import("@/views/echarts/waterChart/index"))), 17 | meta: { 18 | requiresAuth: true, 19 | title: "水型图", 20 | key: "waterChart" 21 | } 22 | }, 23 | { 24 | path: "/echarts/columnChart", 25 | element: lazyLoad(React.lazy(() => import("@/views/echarts/columnChart/index"))), 26 | meta: { 27 | requiresAuth: true, 28 | title: "柱状图", 29 | key: "columnChart" 30 | } 31 | }, 32 | { 33 | path: "/echarts/lineChart", 34 | element: lazyLoad(React.lazy(() => import("@/views/echarts/lineChart/index"))), 35 | meta: { 36 | requiresAuth: true, 37 | title: "折线图", 38 | key: "lineChart" 39 | } 40 | }, 41 | { 42 | path: "/echarts/pieChart", 43 | element: lazyLoad(React.lazy(() => import("@/views/echarts/pieChart/index"))), 44 | meta: { 45 | requiresAuth: true, 46 | title: "饼图", 47 | key: "pieChart" 48 | } 49 | }, 50 | { 51 | path: "/echarts/radarChart", 52 | element: lazyLoad(React.lazy(() => import("@/views/echarts/radarChart/index"))), 53 | meta: { 54 | requiresAuth: true, 55 | title: "雷达图", 56 | key: "radarChart" 57 | } 58 | }, 59 | { 60 | path: "/echarts/nestedChart", 61 | element: lazyLoad(React.lazy(() => import("@/views/echarts/nestedChart/index"))), 62 | meta: { 63 | requiresAuth: true, 64 | title: "嵌套环形图", 65 | key: "nestedChart" 66 | } 67 | } 68 | ] 69 | } 70 | ]; 71 | 72 | export default formRouter; 73 | -------------------------------------------------------------------------------- /src/routers/modules/error.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import lazyLoad from "@/routers/utils/lazyLoad"; 3 | import { RouteObject } from "@/routers/interface"; 4 | 5 | // 错误页面模块 6 | const errorRouter: Array = [ 7 | { 8 | path: "/403", 9 | element: lazyLoad(React.lazy(() => import("@/components/ErrorMessage/403"))), 10 | meta: { 11 | requiresAuth: true, 12 | title: "403页面", 13 | key: "403" 14 | } 15 | }, 16 | { 17 | path: "/404", 18 | element: lazyLoad(React.lazy(() => import("@/components/ErrorMessage/404"))), 19 | meta: { 20 | requiresAuth: false, 21 | title: "404页面", 22 | key: "404" 23 | } 24 | }, 25 | { 26 | path: "/500", 27 | element: lazyLoad(React.lazy(() => import("@/components/ErrorMessage/500"))), 28 | meta: { 29 | requiresAuth: false, 30 | title: "500页面", 31 | key: "500" 32 | } 33 | } 34 | ]; 35 | 36 | export default errorRouter; 37 | -------------------------------------------------------------------------------- /src/routers/modules/form.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import lazyLoad from "@/routers/utils/lazyLoad"; 3 | import { LayoutIndex } from "@/routers/constant"; 4 | import { RouteObject } from "@/routers/interface"; 5 | 6 | // 表单 Form 模块 7 | const formRouter: Array = [ 8 | { 9 | element: , 10 | meta: { 11 | title: "表单 Form" 12 | }, 13 | children: [ 14 | { 15 | path: "/form/basicForm", 16 | element: lazyLoad(React.lazy(() => import("@/views/form/basicForm/index"))), 17 | meta: { 18 | requiresAuth: true, 19 | title: "基础 Form", 20 | key: "basicForm" 21 | } 22 | }, 23 | { 24 | path: "/form/validateForm", 25 | element: lazyLoad(React.lazy(() => import("@/views/form/validateForm/index"))), 26 | meta: { 27 | requiresAuth: true, 28 | title: "校验 Form", 29 | key: "validateForm" 30 | } 31 | }, 32 | { 33 | path: "/form/dynamicForm", 34 | element: lazyLoad(React.lazy(() => import("@/views/form/dynamicForm/index"))), 35 | meta: { 36 | requiresAuth: true, 37 | title: "动态 Form", 38 | key: "dynamicForm" 39 | } 40 | } 41 | ] 42 | } 43 | ]; 44 | 45 | export default formRouter; 46 | -------------------------------------------------------------------------------- /src/routers/modules/home.tsx: -------------------------------------------------------------------------------- 1 | // import React from "react"; 2 | // import lazyLoad from "@/routers/util/lazyLoad"; 3 | import { LayoutIndex } from "@/routers/constant"; 4 | import { RouteObject } from "@/routers/interface"; 5 | import Home from "@/views/home/index"; 6 | 7 | // 首页模块 8 | const homeRouter: Array = [ 9 | { 10 | element: , 11 | children: [ 12 | { 13 | path: "/home/index", 14 | // element: lazyLoad(React.lazy(() => import("@/views/home/index"))), 15 | element: , 16 | meta: { 17 | requiresAuth: true, 18 | title: "首页", 19 | key: "home" 20 | } 21 | } 22 | ] 23 | } 24 | ]; 25 | 26 | export default homeRouter; 27 | -------------------------------------------------------------------------------- /src/routers/modules/link.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import lazyLoad from "@/routers/utils/lazyLoad"; 3 | import { LayoutIndex } from "@/routers/constant"; 4 | import { RouteObject } from "@/routers/interface"; 5 | 6 | // 外部链接模块 7 | const linkRouter: Array = [ 8 | { 9 | element: , 10 | meta: { 11 | title: "外部链接" 12 | }, 13 | children: [ 14 | { 15 | path: "/link/gitee", 16 | element: lazyLoad(React.lazy(() => import("@/views/link/gitee/index"))), 17 | meta: { 18 | requiresAuth: true, 19 | title: "Gitee 仓库", 20 | key: "gitee" 21 | } 22 | }, 23 | { 24 | path: "/link/github", 25 | element: lazyLoad(React.lazy(() => import("@/views/link/github/index"))), 26 | meta: { 27 | requiresAuth: true, 28 | title: "GitHub 仓库", 29 | key: "github" 30 | } 31 | }, 32 | { 33 | path: "/link/juejin", 34 | element: lazyLoad(React.lazy(() => import("@/views/link/juejin/index"))), 35 | meta: { 36 | requiresAuth: true, 37 | title: "掘金文档", 38 | key: "juejin" 39 | } 40 | }, 41 | { 42 | path: "/link/myBlog", 43 | element: lazyLoad(React.lazy(() => import("@/views/link/myBlog/index"))), 44 | meta: { 45 | requiresAuth: true, 46 | title: "个人博客", 47 | key: "myBlog" 48 | } 49 | } 50 | ] 51 | } 52 | ]; 53 | 54 | export default linkRouter; 55 | -------------------------------------------------------------------------------- /src/routers/modules/menu.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import lazyLoad from "@/routers/utils/lazyLoad"; 3 | import { LayoutIndex } from "@/routers/constant"; 4 | import { RouteObject } from "@/routers/interface"; 5 | 6 | // menu 模块 7 | const menuRouter: Array = [ 8 | { 9 | element: , 10 | meta: { 11 | title: "嵌套菜单" 12 | }, 13 | children: [ 14 | { 15 | path: "/menu/menu1", 16 | element: lazyLoad(React.lazy(() => import("@/views/menu/menu1/index"))), 17 | meta: { 18 | requiresAuth: true, 19 | title: "菜单1", 20 | key: "menu1" 21 | } 22 | }, 23 | { 24 | path: "/menu/menu2/menu21", 25 | element: lazyLoad(React.lazy(() => import("@/views/menu/menu2/menu21/index"))), 26 | meta: { 27 | requiresAuth: true, 28 | title: "菜单2-1", 29 | key: "menu21" 30 | } 31 | }, 32 | { 33 | path: "/menu/menu2/menu22/menu221", 34 | element: lazyLoad(React.lazy(() => import("@/views/menu/menu2/menu22/menu221/index"))), 35 | meta: { 36 | requiresAuth: true, 37 | title: "菜单2-2-1", 38 | key: "menu221" 39 | } 40 | }, 41 | { 42 | path: "/menu/menu2/menu22/menu222", 43 | element: lazyLoad(React.lazy(() => import("@/views/menu/menu2/menu22/menu222/index"))), 44 | meta: { 45 | requiresAuth: true, 46 | title: "菜单2-2-2", 47 | key: "menu222" 48 | } 49 | }, 50 | { 51 | path: "/menu/menu2/menu23", 52 | element: lazyLoad(React.lazy(() => import("@/views/menu/menu2/menu23/index"))), 53 | meta: { 54 | requiresAuth: true, 55 | title: "菜单2-3", 56 | key: "menu23" 57 | } 58 | }, 59 | { 60 | path: "/menu/menu3", 61 | element: lazyLoad(React.lazy(() => import("@/views/menu/menu3/index"))), 62 | meta: { 63 | requiresAuth: true, 64 | title: "菜单3", 65 | key: "menu3" 66 | } 67 | } 68 | ] 69 | } 70 | ]; 71 | 72 | export default menuRouter; 73 | -------------------------------------------------------------------------------- /src/routers/modules/proTable.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import lazyLoad from "@/routers/utils/lazyLoad"; 3 | import { LayoutIndex } from "@/routers/constant"; 4 | import { RouteObject } from "@/routers/interface"; 5 | 6 | // 超级表格模块 7 | const proTableRouter: Array = [ 8 | { 9 | element: , 10 | meta: { 11 | title: "超级表格" 12 | }, 13 | children: [ 14 | { 15 | path: "/proTable/useHooks", 16 | element: lazyLoad(React.lazy(() => import("@/views/proTable/useHooks/index"))), 17 | meta: { 18 | requiresAuth: true, 19 | title: "使用 Hooks", 20 | key: "useHooks" 21 | } 22 | }, 23 | { 24 | path: "/proTable/useComponent", 25 | element: lazyLoad(React.lazy(() => import("@/views/proTable/useComponent/index"))), 26 | meta: { 27 | requiresAuth: true, 28 | title: "使用 Component", 29 | key: "useComponent" 30 | } 31 | } 32 | ] 33 | } 34 | ]; 35 | 36 | export default proTableRouter; 37 | -------------------------------------------------------------------------------- /src/routers/utils/authRouter.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation, Navigate } from "react-router-dom"; 2 | import { AxiosCanceler } from "@/api/helper/axiosCancel"; 3 | import { searchRoute } from "@/utils/util"; 4 | import { rootRouter } from "@/routers/index"; 5 | import { HOME_URL } from "@/config/config"; 6 | import { store } from "@/redux/index"; 7 | 8 | const axiosCanceler = new AxiosCanceler(); 9 | 10 | /** 11 | * @description 路由守卫组件 12 | * */ 13 | const AuthRouter = (props: { children: JSX.Element }) => { 14 | const { pathname } = useLocation(); 15 | const route = searchRoute(pathname, rootRouter); 16 | // * 在跳转路由之前,清除所有的请求 17 | axiosCanceler.removeAllPending(); 18 | 19 | // * 判断当前路由是否需要访问权限(不需要权限直接放行) 20 | if (!route.meta?.requiresAuth) return props.children; 21 | 22 | // * 判断是否有Token 23 | const token = store.getState().global.token; 24 | if (!token) return ; 25 | 26 | // * Dynamic Router(动态路由,根据后端返回的菜单数据生成的一维数组) 27 | const dynamicRouter = store.getState().auth.authRouter; 28 | // * Static Router(静态路由,必须配置首页地址,否则不能进首页获取菜单、按钮权限等数据),获取数据的时候会loading,所有配置首页地址也没问题 29 | const staticRouter = [HOME_URL, "/403"]; 30 | const routerList = dynamicRouter.concat(staticRouter); 31 | // * 如果访问的地址没有在路由表中重定向到403页面 32 | if (routerList.indexOf(pathname) == -1) return ; 33 | 34 | // * 当前账号有权限返回 Router,正常访问页面 35 | return props.children; 36 | }; 37 | 38 | export default AuthRouter; 39 | -------------------------------------------------------------------------------- /src/routers/utils/lazyLoad.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import { Spin } from "antd"; 3 | 4 | /** 5 | * @description 路由懒加载 6 | * @param {Element} Comp 需要访问的组件 7 | * @returns element 8 | */ 9 | const lazyLoad = (Comp: React.LazyExoticComponent): React.ReactNode => { 10 | return ( 11 | 22 | } 23 | > 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default lazyLoad; 30 | -------------------------------------------------------------------------------- /src/styles/common.less: -------------------------------------------------------------------------------- 1 | /* 常用 flex */ 2 | .flx-center { 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | } 7 | .flx-justify-between { 8 | display: flex; 9 | align-items: center; 10 | justify-content: space-between; 11 | } 12 | .flx-align-center { 13 | display: flex; 14 | align-items: center; 15 | } 16 | 17 | /* 清除浮动 */ 18 | .clearfix::after { 19 | display: block; 20 | height: 0; 21 | overflow: hidden; 22 | clear: both; 23 | content: ""; 24 | } 25 | 26 | /* 文字单行省略号 */ 27 | .sle { 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | white-space: nowrap; 31 | } 32 | 33 | /* 文字多行省略号 */ 34 | .mle { 35 | display: -webkit-box; 36 | overflow: hidden; 37 | -webkit-box-orient: vertical; 38 | -webkit-line-clamp: 2; 39 | } 40 | 41 | /* 文字多了自動換行 */ 42 | .break-word { 43 | word-break: break-all; 44 | word-wrap: break-word; 45 | } 46 | 47 | /* page switch animation */ 48 | .fade-enter { 49 | opacity: 0; 50 | transform: translateX(-30px); 51 | } 52 | .fade-enter-active, 53 | .fade-exit-active { 54 | opacity: 1; 55 | transition: all 0.2s ease-out; 56 | transform: translateX(0); 57 | } 58 | .fade-exit { 59 | opacity: 0; 60 | transform: translateX(30px); 61 | } 62 | 63 | /* scroll bar */ 64 | ::-webkit-scrollbar { 65 | width: 8px; 66 | height: 8px; 67 | background-color: #ffffff; 68 | } 69 | ::-webkit-scrollbar-thumb { 70 | background-color: #dddee0; 71 | border-radius: 20px; 72 | box-shadow: inset 0 0 0 #ffffff; 73 | } 74 | 75 | /* card 卡片样式 */ 76 | .card { 77 | box-sizing: border-box; 78 | padding: 20px; 79 | overflow-x: hidden; 80 | border: 1px solid #e4e7ed; 81 | border-radius: 4px; 82 | } 83 | 84 | /* content-box */ 85 | .content-box { 86 | display: flex; 87 | flex-direction: column; 88 | width: 100%; 89 | height: 100%; 90 | .text { 91 | margin: 30px 0; 92 | font-size: 23px; 93 | font-weight: 700; 94 | text-align: center; 95 | a { 96 | text-decoration: underline !important; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/styles/reset.less: -------------------------------------------------------------------------------- 1 | /* Reset style sheet */ 2 | html, 3 | body, 4 | div, 5 | span, 6 | applet, 7 | object, 8 | iframe, 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5, 14 | h6, 15 | p, 16 | blockquote, 17 | pre, 18 | a, 19 | abbr, 20 | acronym, 21 | address, 22 | big, 23 | cite, 24 | code, 25 | del, 26 | dfn, 27 | em, 28 | img, 29 | ins, 30 | kbd, 31 | q, 32 | s, 33 | samp, 34 | small, 35 | strike, 36 | strong, 37 | sub, 38 | sup, 39 | tt, 40 | var, 41 | b, 42 | u, 43 | i, 44 | center, 45 | dl, 46 | dt, 47 | dd, 48 | ol, 49 | ul, 50 | li, 51 | fieldset, 52 | form, 53 | label, 54 | legend, 55 | table, 56 | caption, 57 | tbody, 58 | tfoot, 59 | thead, 60 | tr, 61 | th, 62 | td, 63 | article, 64 | aside, 65 | canvas, 66 | details, 67 | embed, 68 | figure, 69 | figcaption, 70 | footer, 71 | header, 72 | hgroup, 73 | menu, 74 | nav, 75 | output, 76 | ruby, 77 | section, 78 | summary, 79 | time, 80 | mark, 81 | audio, 82 | video { 83 | padding: 0; 84 | margin: 0; 85 | font: inherit; 86 | font-size: 100%; 87 | vertical-align: baseline; 88 | border: 0; 89 | } 90 | 91 | /* HTML5 display-role reset for older browsers */ 92 | article, 93 | aside, 94 | details, 95 | figcaption, 96 | figure, 97 | footer, 98 | header, 99 | hgroup, 100 | menu, 101 | nav, 102 | section { 103 | display: block; 104 | } 105 | body { 106 | padding: 0; 107 | margin: 0; 108 | } 109 | ol, 110 | ul { 111 | list-style: none; 112 | } 113 | blockquote, 114 | q { 115 | quotes: none; 116 | } 117 | blockquote::before, 118 | blockquote::after, 119 | q::before, 120 | q::after { 121 | content: ""; 122 | content: none; 123 | } 124 | table { 125 | border-spacing: 0; 126 | border-collapse: collapse; 127 | } 128 | html, 129 | body, 130 | #root { 131 | width: 100%; 132 | height: 100%; 133 | } 134 | -------------------------------------------------------------------------------- /src/styles/theme/theme-dark.less: -------------------------------------------------------------------------------- 1 | @import "antd/dist/antd.dark.less"; 2 | 3 | /* 自定义 antd 暗黑模式样式 */ 4 | @dark-main-bg-color: #141414; 5 | @dark-bg-color: #1f1f1f; 6 | @dark-border-color: #414243; 7 | @dark-text-color: #d9d9d9; 8 | @dark-shadow-color: 5px 5px 15px rgb(255 255 255 / 20%); 9 | @dark-scrollbar-bg-color: #686868; 10 | 11 | /* 需要自定义覆盖的样式 */ 12 | body { 13 | background-color: @dark-main-bg-color !important; 14 | // guide 15 | #driver-highlighted-element-stage { 16 | background-color: #525457 !important; 17 | } 18 | } 19 | 20 | /* login container(先固定样式) */ 21 | .login-container { 22 | background-color: @dark-main-bg-color !important; 23 | .login-box { 24 | background-color: rgb(0 0 0 / 80%) !important; 25 | .login-form { 26 | background-color: @dark-main-bg-color !important; 27 | box-shadow: @dark-shadow-color !important; 28 | .login-logo { 29 | .logo-text { 30 | color: @dark-text-color !important; 31 | } 32 | } 33 | .login-btn { 34 | .ant-btn-default { 35 | color: @dark-text-color !important; 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | /* container */ 43 | .container { 44 | /* sider */ 45 | .ant-layout-sider { 46 | border-right: 1px solid @dark-border-color !important; 47 | .ant-menu { 48 | &::-webkit-scrollbar { 49 | background-color: @dark-bg-color !important; 50 | } 51 | &::-webkit-scrollbar-thumb { 52 | background-color: @dark-scrollbar-bg-color !important; 53 | } 54 | } 55 | .logo-box { 56 | border-bottom: 1px solid @dark-border-color !important; 57 | } 58 | } 59 | 60 | /* layout */ 61 | .ant-layout { 62 | background-color: @dark-main-bg-color !important; 63 | .ant-layout-header, 64 | .tabs, 65 | .footer, 66 | .card { 67 | background-color: @dark-bg-color !important; 68 | border-color: @dark-border-color !important; 69 | .text { 70 | color: @dark-text-color !important; 71 | } 72 | } 73 | .ant-layout-header { 74 | height: 55px; 75 | padding: 0 40px 0 20px; 76 | .icon-style, 77 | .username { 78 | color: @dark-text-color !important; 79 | } 80 | } 81 | .footer { 82 | a { 83 | color: @dark-text-color !important; 84 | } 85 | } 86 | .ant-layout-content { 87 | &::-webkit-scrollbar { 88 | background-color: @dark-main-bg-color !important; 89 | } 90 | &::-webkit-scrollbar-thumb { 91 | background-color: @dark-scrollbar-bg-color !important; 92 | } 93 | .card { 94 | &::-webkit-scrollbar { 95 | background-color: @dark-bg-color !important; 96 | } 97 | &::-webkit-scrollbar-thumb { 98 | background-color: @dark-scrollbar-bg-color !important; 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | 106 | -------------------------------------------------------------------------------- /src/styles/theme/theme-default.less: -------------------------------------------------------------------------------- 1 | @import "antd/dist/antd.less"; 2 | 3 | /* 自定义 antd 默认样式 */ 4 | @light-bg-color: #ffffff; 5 | @light-main-bg-color: #f0f2f5; 6 | @light-border-color: #e4e7ed; 7 | @light-border-header-color: #f6f6f6; 8 | @light-text-color: rgba(0, 0, 0, 0.85); 9 | @light-shadow-color: 0 0 12px #0000000d; 10 | @light-scrollbar-bg-color: #dddee0; 11 | 12 | /* 需要自定义覆盖的样式 */ 13 | body { 14 | background-color: @light-bg-color !important; 15 | #driver-highlighted-element-stage { 16 | background-color: #fff !important; 17 | } 18 | } 19 | 20 | /* login container(先固定样式) */ 21 | .login-container { 22 | background-color: #eeeeee !important; 23 | .login-box { 24 | background-color: hsl(0deg 0% 100% / 80%) !important; 25 | .login-form { 26 | background-color: transparent !important; 27 | box-shadow: 2px 3px 7px rgb(0 0 0 / 20%) !important; 28 | .login-logo { 29 | .logo-text { 30 | color: #475768 !important; 31 | } 32 | } 33 | .login-btn { 34 | .ant-btn-default { 35 | color: #606266 !important; 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | /* container */ 43 | .container { 44 | /* sider */ 45 | .ant-layout-sider { 46 | border-right: 1px solid @light-border-color !important; 47 | .ant-menu { 48 | &::-webkit-scrollbar { 49 | background-color: #001529 !important; 50 | } 51 | &::-webkit-scrollbar-thumb { 52 | background-color: #41444b !important; 53 | } 54 | } 55 | .logo-box { 56 | border-bottom: 1px solid #010b14 !important; 57 | } 58 | } 59 | 60 | /* layout */ 61 | .ant-layout { 62 | background-color: @light-main-bg-color !important; 63 | .tabs, 64 | .footer, 65 | .card { 66 | background-color: @light-bg-color !important; 67 | border-color: @light-border-color !important; 68 | } 69 | .ant-layout-header { 70 | height: 55px; 71 | padding: 0 40px 0 20px; 72 | background-color: @light-bg-color !important; 73 | border-color: @light-border-header-color !important; 74 | .icon-style, 75 | .username { 76 | color: @light-text-color !important; 77 | } 78 | } 79 | .footer { 80 | a { 81 | color: @light-text-color !important; 82 | } 83 | } 84 | .card { 85 | box-shadow: @light-shadow-color !important; 86 | .text { 87 | color: #585858 !important; 88 | } 89 | } 90 | .ant-layout-content { 91 | &::-webkit-scrollbar { 92 | background-color: @light-main-bg-color !important; 93 | } 94 | &::-webkit-scrollbar-thumb { 95 | background-color: @light-scrollbar-bg-color !important; 96 | } 97 | .card { 98 | &::-webkit-scrollbar { 99 | background-color: @light-bg-color !important; 100 | } 101 | &::-webkit-scrollbar-thumb { 102 | background-color: @light-scrollbar-bg-color !important; 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/styles/var.less: -------------------------------------------------------------------------------- 1 | /* Global definition less */ 2 | @primary-color: #1890ff; 3 | -------------------------------------------------------------------------------- /src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | // * Menu 2 | declare namespace Menu { 3 | interface MenuOptions { 4 | path: string; 5 | title: string; 6 | icon?: string; 7 | isLink?: string; 8 | close?: boolean; 9 | children?: MenuOptions[]; 10 | } 11 | } 12 | 13 | // * Vite 14 | declare type Recordable = Record; 15 | 16 | declare interface ViteEnv { 17 | VITE_API_URL: string; 18 | VITE_PORT: number; 19 | VITE_OPEN: boolean; 20 | VITE_GLOB_APP_TITLE: string; 21 | VITE_DROP_CONSOLE: boolean; 22 | VITE_PROXY_URL: string; 23 | VITE_BUILD_GZIP: boolean; 24 | VITE_REPORT: boolean; 25 | } 26 | 27 | // * Dropdown MenuInfo 28 | declare interface MenuInfo { 29 | key: string; 30 | keyPath: string[]; 31 | /** @deprecated This will not support in future. You should avoid to use this */ 32 | item: React.ReactInstance; 33 | domEvent: React.MouseEvent | React.KeyboardEvent; 34 | } 35 | -------------------------------------------------------------------------------- /src/typings/plugins.d.ts: -------------------------------------------------------------------------------- 1 | declare module "qs"; 2 | declare module "nprogress"; 3 | declare module "js-md5"; 4 | declare module "react-transition-group"; 5 | -------------------------------------------------------------------------------- /src/typings/window.d.ts: -------------------------------------------------------------------------------- 1 | // * global 2 | declare global { 3 | interface Window { 4 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any; 5 | } 6 | interface Navigator { 7 | msSaveOrOpenBlob: (blob: Blob, fileName: string) => void; 8 | browserLanguage: string; 9 | } 10 | } 11 | export {}; 12 | -------------------------------------------------------------------------------- /src/utils/echarts/index.ts: -------------------------------------------------------------------------------- 1 | // * Echarts 按需引入 2 | import * as echarts from "echarts/core"; 3 | import { 4 | BarChart, 5 | // 系列类型的定义后缀都为 SeriesOption 6 | BarSeriesOption, 7 | LineChart, 8 | LineSeriesOption 9 | } from "echarts/charts"; 10 | import { LegendComponent } from "echarts/components"; 11 | import { 12 | TitleComponent, 13 | // 组件类型的定义后缀都为 ComponentOption 14 | TitleComponentOption, 15 | TooltipComponent, 16 | TooltipComponentOption, 17 | GridComponent, 18 | GridComponentOption, 19 | // 数据集组件 20 | DatasetComponent, 21 | DatasetComponentOption, 22 | // 内置数据转换器组件 (filter, sort) 23 | TransformComponent 24 | } from "echarts/components"; 25 | import { LabelLayout, UniversalTransition } from "echarts/features"; 26 | import { CanvasRenderer } from "echarts/renderers"; 27 | 28 | // 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型 29 | export type ECOption = echarts.ComposeOption< 30 | | BarSeriesOption 31 | | LineSeriesOption 32 | | TitleComponentOption 33 | | TooltipComponentOption 34 | | GridComponentOption 35 | | DatasetComponentOption 36 | >; 37 | 38 | // 注册必须的组件 39 | echarts.use([ 40 | LegendComponent, 41 | TitleComponent, 42 | TooltipComponent, 43 | GridComponent, 44 | DatasetComponent, 45 | TransformComponent, 46 | BarChart, 47 | LineChart, 48 | LabelLayout, 49 | UniversalTransition, 50 | CanvasRenderer 51 | ]); 52 | 53 | export default echarts; 54 | -------------------------------------------------------------------------------- /src/utils/getEnv.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import dotenv from "dotenv"; 4 | 5 | export function isDevFn(mode: string): boolean { 6 | return mode === "development"; 7 | } 8 | 9 | export function isProdFn(mode: string): boolean { 10 | return mode === "production"; 11 | } 12 | 13 | /** 14 | * Whether to generate package preview 15 | */ 16 | export function isReportMode(): boolean { 17 | return process.env.VITE_REPORT === "true"; 18 | } 19 | 20 | // Read all environment variable configuration files to process.env 21 | export function wrapperEnv(envConf: Recordable): ViteEnv { 22 | const ret: any = {}; 23 | 24 | for (const envName of Object.keys(envConf)) { 25 | let realName = envConf[envName].replace(/\\n/g, "\n"); 26 | realName = realName === "true" ? true : realName === "false" ? false : realName; 27 | 28 | if (envName === "VITE_PORT") { 29 | realName = Number(realName); 30 | } 31 | if (envName === "VITE_PROXY") { 32 | try { 33 | realName = JSON.parse(realName); 34 | } catch (error) { 35 | console.log(error); 36 | } 37 | } 38 | ret[envName] = realName; 39 | process.env[envName] = realName; 40 | } 41 | return ret; 42 | } 43 | 44 | /** 45 | * Get the environment variables starting with the specified prefix 46 | * @param match prefix 47 | * @param confFiles ext 48 | */ 49 | export function getEnvConfig(match = "VITE_GLOB_", confFiles = [".env", ".env.production"]) { 50 | let envConfig = {}; 51 | confFiles.forEach(item => { 52 | try { 53 | const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item))); 54 | envConfig = { ...envConfig, ...env }; 55 | } catch (error) { 56 | console.error(`Error in parsing ${item}`, error); 57 | } 58 | }); 59 | 60 | Object.keys(envConfig).forEach(key => { 61 | const reg = new RegExp(`^(${match})`); 62 | if (!reg.test(key)) { 63 | Reflect.deleteProperty(envConfig, key); 64 | } 65 | }); 66 | return envConfig; 67 | } 68 | 69 | /** 70 | * Get user root directory 71 | * @param dir file path 72 | */ 73 | export function getRootPath(...dir: string[]) { 74 | return path.resolve(process.cwd(), ...dir); 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/is/index.ts: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString; 2 | 3 | /** 4 | * @description: 判断值是否未某个类型 5 | */ 6 | export function is(val: unknown, type: string) { 7 | return toString.call(val) === `[object ${type}]`; 8 | } 9 | 10 | /** 11 | * @description: 是否为函数 12 | */ 13 | export function isFunction(val: unknown): val is T { 14 | return is(val, "Function"); 15 | } 16 | 17 | /** 18 | * @description: 是否已定义 19 | */ 20 | export const isDef = (val?: T): val is T => { 21 | return typeof val !== "undefined"; 22 | }; 23 | 24 | export const isUnDef = (val?: T): val is T => { 25 | return !isDef(val); 26 | }; 27 | /** 28 | * @description: 是否为对象 29 | */ 30 | export const isObject = (val: any): val is Record => { 31 | return val !== null && is(val, "Object"); 32 | }; 33 | 34 | /** 35 | * @description: 是否为时间 36 | */ 37 | export function isDate(val: unknown): val is Date { 38 | return is(val, "Date"); 39 | } 40 | 41 | /** 42 | * @description: 是否为数值 43 | */ 44 | export function isNumber(val: unknown): val is number { 45 | return is(val, "Number"); 46 | } 47 | 48 | /** 49 | * @description: 是否为AsyncFunction 50 | */ 51 | export function isAsyncFunction(val: unknown): val is Promise { 52 | return is(val, "AsyncFunction"); 53 | } 54 | 55 | /** 56 | * @description: 是否为promise 57 | */ 58 | export function isPromise(val: unknown): val is Promise { 59 | return is(val, "Promise") && isObject(val) && isFunction(val.then) && isFunction(val.catch); 60 | } 61 | 62 | /** 63 | * @description: 是否为字符串 64 | */ 65 | export function isString(val: unknown): val is string { 66 | return is(val, "String"); 67 | } 68 | 69 | /** 70 | * @description: 是否为boolean类型 71 | */ 72 | export function isBoolean(val: unknown): val is boolean { 73 | return is(val, "Boolean"); 74 | } 75 | 76 | /** 77 | * @description: 是否为数组 78 | */ 79 | export function isArray(val: any): val is Array { 80 | return val && Array.isArray(val); 81 | } 82 | 83 | /** 84 | * @description: 是否客户端 85 | */ 86 | export const isClient = () => { 87 | return typeof window !== "undefined"; 88 | }; 89 | 90 | /** 91 | * @description: 是否为浏览器 92 | */ 93 | export const isWindow = (val: any): val is Window => { 94 | return typeof window !== "undefined" && is(val, "Window"); 95 | }; 96 | 97 | export const isElement = (val: unknown): val is Element => { 98 | return isObject(val) && !!val.tagName; 99 | }; 100 | 101 | export const isServer = typeof window === "undefined"; 102 | 103 | // 是否为图片节点 104 | export function isImageDom(o: Element) { 105 | return o && ["IMAGE", "IMG"].includes(o.tagName); 106 | } 107 | 108 | export function isNull(val: unknown): val is null { 109 | return val === null; 110 | } 111 | 112 | export function isNullAndUnDef(val: unknown): val is null | undefined { 113 | return isUnDef(val) && isNull(val); 114 | } 115 | 116 | export function isNullOrUnDef(val: unknown): val is null | undefined { 117 | return isUnDef(val) || isNull(val); 118 | } 119 | -------------------------------------------------------------------------------- /src/views/assembly/batchImport/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/assembly/batchImport/index.less -------------------------------------------------------------------------------- /src/views/assembly/batchImport/index.tsx: -------------------------------------------------------------------------------- 1 | import "./index.less"; 2 | 3 | const BatchImport = () => { 4 | return ( 5 |
6 | BatchImport 🍓🍇🍈🍉 7 |
8 | ); 9 | }; 10 | 11 | export default BatchImport; 12 | -------------------------------------------------------------------------------- /src/views/assembly/guide/index.tsx: -------------------------------------------------------------------------------- 1 | import Driver from "driver.js"; // import driver.js 2 | import "driver.js/dist/driver.min.css"; // import driver.js css 3 | import { Button, Alert } from "antd"; 4 | import steps from "./steps"; 5 | 6 | const Guide = () => { 7 | const driver = new Driver({ 8 | animate: true, // 在更改突出显示的元素时是否设置动画, 9 | opacity: 0.75, // 背景不透明度(0表示只有弹出窗口,没有覆盖) 10 | doneBtnText: "结束", // 最后一个按钮上的文本 11 | closeBtnText: "关闭", // 此步骤的“关闭”按钮上的文本 12 | nextBtnText: "下一步", // 此步骤的下一步按钮文本 13 | prevBtnText: "上一步" // 此步骤的上一个按钮文本 14 | }); 15 | 16 | const guide = () => { 17 | driver.defineSteps(steps); 18 | driver.start(); 19 | console.log(driver, "driver"); 20 | }; 21 | return ( 22 |
23 | 28 |
29 | 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default Guide; 38 | -------------------------------------------------------------------------------- /src/views/assembly/guide/steps.ts: -------------------------------------------------------------------------------- 1 | const steps = [ 2 | { 3 | element: "#antd-button", 4 | popover: { 5 | title: "Guide Button", 6 | description: "Open && Close Guide", 7 | position: "bottom" 8 | } 9 | }, 10 | { 11 | element: "#isCollapse", 12 | popover: { 13 | title: "Collapse Icon", 14 | description: "Open && Close sidebar", 15 | position: "bottom" 16 | } 17 | }, 18 | { 19 | element: ".ant-breadcrumb", 20 | popover: { 21 | title: "Breadcrumb", 22 | description: "Indicate the current page location", 23 | position: "right" 24 | } 25 | }, 26 | { 27 | element: ".icon-contentright", 28 | popover: { 29 | title: "Switch Assembly Size", 30 | description: "Switch the system size", 31 | position: "left" 32 | } 33 | }, 34 | { 35 | element: ".icon-zhongyingwen", 36 | popover: { 37 | title: "Switch Language", 38 | description: "Switch the system language", 39 | position: "left" 40 | } 41 | }, 42 | { 43 | element: ".icon-zhuti", 44 | popover: { 45 | title: "Setting Layout", 46 | description: "Customize settings layout", 47 | position: "left" 48 | } 49 | }, 50 | { 51 | element: ".icon-fangda", 52 | popover: { 53 | title: "Full Screen", 54 | description: "Full Screen, Exit The Full Screen Page", 55 | position: "left" 56 | } 57 | } 58 | ]; 59 | 60 | export default steps; 61 | -------------------------------------------------------------------------------- /src/views/assembly/selectIcon/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/assembly/selectIcon/index.less -------------------------------------------------------------------------------- /src/views/assembly/selectIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import "./index.less"; 2 | 3 | const SelectIcon = () => { 4 | return ( 5 |
6 | SelectIcon 🍓🍇🍈🍉 7 |
8 | ); 9 | }; 10 | 11 | export default SelectIcon; 12 | -------------------------------------------------------------------------------- /src/views/assembly/svgIcon/index.less: -------------------------------------------------------------------------------- 1 | .card { 2 | box-sizing: border-box; 3 | padding: 20px; 4 | overflow-x: hidden; 5 | border: 1px solid #e4e7ed; 6 | border-radius: 4px; 7 | box-shadow: 0 0 12px #0000000d; 8 | .icon-list { 9 | box-sizing: border-box; 10 | display: flex; 11 | justify-content: space-between; 12 | width: 100%; 13 | padding: 40px 100px 0; 14 | } 15 | .antd-descriptions { 16 | width: 100%; 17 | padding: 40px 0 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/views/assembly/svgIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, Descriptions } from "antd"; 2 | import SvgIcon from "@/components/svgIcon"; 3 | import "./index.less"; 4 | 5 | const svgIcon = () => { 6 | return ( 7 |
8 | 13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 图标的名称,svg 图标必须存储在 src/assets/icons 目录下 29 | 图标的前缀,默认为icon 30 | 图标的样式,默认样式为 {"{ width: 100px, height: 100px}"} 31 | 32 |
33 | ); 34 | }; 35 | 36 | export default svgIcon; 37 | -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/components/curve.tsx: -------------------------------------------------------------------------------- 1 | import { useEcharts } from "@/hooks/useEcharts"; 2 | 3 | const Curve = () => { 4 | const data = [ 5 | { value: 30, spotName: "掘金" }, 6 | { value: 90, spotName: "CSDN" }, 7 | { value: 10, spotName: "Gitee" }, 8 | { value: 70, spotName: "GitHub" }, 9 | { value: 20, spotName: "知乎" }, 10 | { value: 60, spotName: "MyBlog" }, 11 | { value: 55, spotName: "简书" }, 12 | { value: 80, spotName: "StackOverFlow" }, 13 | { value: 50, spotName: "博客园" } 14 | ]; 15 | const option: any = { 16 | tooltip: { 17 | trigger: "axis", 18 | backgroundColor: "transparent", 19 | axisPointer: { 20 | type: "none" 21 | }, 22 | padding: 0, 23 | formatter: (p: any) => { 24 | let dom = `
26 |
平台 : ${p[0].name}
27 |
数据量 : ${p[0].value}
28 |
`; 29 | return dom; 30 | } 31 | }, 32 | toolbox: { 33 | show: true, 34 | orient: "horizontal" 35 | }, 36 | grid: { 37 | left: "5%", 38 | right: "6%" 39 | }, 40 | dataZoom: [ 41 | { 42 | show: false, 43 | height: 10, 44 | xAxisIndex: [0], 45 | bottom: 0, 46 | startValue: 0, //数据窗口范围的起始数值 47 | endValue: 9, //数据窗口范围的结束数值 48 | handleStyle: { 49 | color: "#6b9dfe" 50 | }, 51 | textStyle: { 52 | color: "transparent" 53 | } 54 | }, 55 | { 56 | type: "inside", 57 | show: true, 58 | height: 0, 59 | zoomLock: true //控制伸缩 60 | } 61 | ], 62 | xAxis: [ 63 | { 64 | type: "category", 65 | data: data.map((val: any) => { 66 | return { 67 | value: val.spotName 68 | }; 69 | }), 70 | axisTick: { 71 | show: false 72 | }, 73 | axisLabel: { 74 | // interval: time > 4 ? 27 : 0, 75 | margin: 20, 76 | interval: 0, 77 | color: "#a1a1a1", 78 | fontSize: 14, 79 | formatter: function (name: string) { 80 | undefined; 81 | return name.length > 8 ? name.slice(0, 8) + "..." : name; 82 | } 83 | }, 84 | axisLine: { 85 | lineStyle: { 86 | color: "#F6F6F7", 87 | width: 2 88 | } 89 | } 90 | } 91 | ], 92 | yAxis: [ 93 | { 94 | min: 0, 95 | axisLine: { 96 | show: false 97 | }, 98 | axisTick: { 99 | show: false 100 | }, 101 | splitLine: { 102 | show: true, 103 | lineStyle: { 104 | type: "dashed", 105 | color: "#edeff5", 106 | width: 2 107 | } 108 | }, 109 | axisLabel: { 110 | color: "#a1a1a1", 111 | fontSize: 16, 112 | fontWeight: 400, 113 | formatter: function (value: number) { 114 | if (value === 0) { 115 | return value; 116 | } else if (value >= 10000) { 117 | return value / 10000 + "w"; 118 | } 119 | return value; 120 | } 121 | } 122 | } 123 | ], 124 | series: [ 125 | { 126 | name: "Direct", 127 | type: "bar", 128 | data: data.map((val: any) => { 129 | return { 130 | value: val.value 131 | }; 132 | }), 133 | barWidth: "45px", 134 | itemStyle: { 135 | color: "#C5D8FF", 136 | borderRadius: [12, 12, 0, 0] 137 | }, 138 | emphasis: { 139 | itemStyle: { 140 | color: "#6B9DFE" 141 | } 142 | } 143 | } 144 | ] 145 | }; 146 | const [echartsRef] = useEcharts(option, data); 147 | return
; 148 | }; 149 | 150 | export default Curve; 151 | -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/components/pie.tsx: -------------------------------------------------------------------------------- 1 | import { useEcharts } from "@/hooks/useEcharts"; 2 | 3 | const Curve = () => { 4 | const pieData: any = [ 5 | { value: 5000, name: "Gitee 访问量" }, 6 | { value: 5000, name: "GitHub 访问量" } 7 | ]; 8 | const option: any = { 9 | title: { 10 | text: "Gitee / GitHub", 11 | subtext: "访问占比", 12 | left: "56%", 13 | top: "45%", 14 | textAlign: "center", 15 | textStyle: { 16 | fontSize: 18, 17 | color: "#767676" 18 | }, 19 | subtextStyle: { 20 | fontSize: 15, 21 | color: "#a1a1a1" 22 | } 23 | }, 24 | tooltip: { 25 | trigger: "item" 26 | }, 27 | legend: { 28 | top: "4%", 29 | left: "2%", 30 | orient: "vertical", 31 | icon: "circle", //图例形状 32 | align: "left", 33 | itemGap: 20, 34 | textStyle: { 35 | fontSize: 13, 36 | color: "#a1a1a1", 37 | fontWeight: 500 38 | }, 39 | formatter: function (name: string) { 40 | let dataCopy = ""; 41 | for (let i = 0; i < pieData.length; i++) { 42 | if (pieData[i].name == name && pieData[i].value >= 10000) { 43 | dataCopy = (pieData[i].value / 10000).toFixed(2); 44 | return name + " " + dataCopy + "w"; 45 | } else if (pieData[i].name == name) { 46 | dataCopy = pieData[i].value; 47 | return name + " " + dataCopy; 48 | } 49 | } 50 | } 51 | }, 52 | series: [ 53 | { 54 | type: "pie", 55 | radius: ["70%", "40%"], 56 | center: ["57%", "52%"], 57 | silent: true, 58 | clockwise: true, 59 | startAngle: 150, 60 | data: pieData, 61 | labelLine: { 62 | length: 80, 63 | length2: 30, 64 | lineStyle: { 65 | width: 1 66 | } 67 | }, 68 | label: { 69 | position: "outside", 70 | show: true, 71 | formatter: "{d}%", 72 | fontWeight: 400, 73 | fontSize: 19, 74 | color: "#a1a1a1" 75 | }, 76 | color: [ 77 | { 78 | type: "linear", 79 | x: 0, 80 | y: 0, 81 | x2: 0.5, 82 | y2: 1, 83 | colorStops: [ 84 | { 85 | offset: 0, 86 | color: "#feb791" // 0% 处的颜色 87 | }, 88 | { 89 | offset: 1, 90 | color: "#fe8b4c" // 100% 处的颜色 91 | } 92 | ] 93 | }, 94 | { 95 | type: "linear", 96 | x: 0, 97 | y: 0, 98 | x2: 1, 99 | y2: 0.5, 100 | colorStops: [ 101 | { 102 | offset: 0, 103 | color: "#b898fd" // 0% 处的颜色 104 | }, 105 | { 106 | offset: 1, 107 | color: "#8347fd" // 100% 处的颜色 108 | } 109 | ] 110 | } 111 | ] 112 | } 113 | ] 114 | }; 115 | 116 | const [echartsRef] = useEcharts(option, pieData); 117 | return
; 118 | }; 119 | 120 | export default Curve; 121 | -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/images/1-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dashboard/dataVisualize/images/1-bg.png -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/images/2-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dashboard/dataVisualize/images/2-bg.png -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/images/3-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dashboard/dataVisualize/images/3-bg.png -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/images/4-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dashboard/dataVisualize/images/4-bg.png -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/images/add_person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dashboard/dataVisualize/images/add_person.png -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/images/add_team.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dashboard/dataVisualize/images/add_team.png -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/images/book-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dashboard/dataVisualize/images/book-bg.png -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/images/book-sum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dashboard/dataVisualize/images/book-sum.png -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/images/book_sum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dashboard/dataVisualize/images/book_sum.png -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/images/today.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dashboard/dataVisualize/images/today.png -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/index.less: -------------------------------------------------------------------------------- 1 | .dataVisualize-box { 2 | min-width: 1035px; 3 | .top-box { 4 | box-sizing: border-box; 5 | padding: 20px 40px 35px; 6 | margin-bottom: 10px; 7 | .top-title { 8 | font-family: "PingFang SC"; 9 | font-size: 18px; 10 | font-weight: bold; 11 | } 12 | .top-content { 13 | display: flex; 14 | flex: 1; 15 | justify-content: space-between; 16 | margin-top: 10px; 17 | .item-left { 18 | box-sizing: border-box; 19 | width: 22%; 20 | padding: 40px 0 120px 30px; 21 | overflow: hidden; 22 | color: #ffffff; 23 | background: url("./images/book-bg.png"); 24 | background-size: 100% 100%; 25 | border-radius: 20px; 26 | .left-title { 27 | font-family: "PingFang SC"; 28 | font-size: 20px; 29 | font-weight: 500; 30 | } 31 | .img-box { 32 | display: flex; 33 | align-items: center; 34 | justify-content: center; 35 | width: 90px; 36 | height: 90px; 37 | margin: 40px 0 20px; 38 | background: #ffffff; 39 | background-color: #ffffff; 40 | border-radius: 20px; 41 | box-shadow: 0 10px 20px rgb(0 0 0 / 14%); 42 | img { 43 | width: 60px; 44 | height: 65px; 45 | } 46 | } 47 | .left-number { 48 | overflow: hidden; 49 | font-family: DIN; 50 | font-size: 62px; 51 | font-weight: 500; 52 | } 53 | } 54 | .item-center { 55 | display: flex; 56 | flex: 1; 57 | flex-wrap: wrap; 58 | align-content: space-between; 59 | justify-content: space-between; 60 | min-width: 250px; 61 | padding: 0 50px; 62 | .traffic-box { 63 | box-sizing: border-box; 64 | display: flex; 65 | flex-direction: column; 66 | width: 47%; 67 | height: 48%; 68 | padding: 25px; 69 | border-radius: 30px; 70 | .traffic-img { 71 | display: flex; 72 | align-items: center; 73 | justify-content: center; 74 | width: 70px; 75 | height: 70px; 76 | margin-bottom: 10px; 77 | background-color: #ffffff; 78 | border-radius: 19px; 79 | } 80 | } 81 | img { 82 | width: 33px; 83 | height: 33px; 84 | } 85 | .item-value { 86 | margin-bottom: 4px; 87 | font-family: DIN; 88 | font-size: 28px; 89 | font-weight: bold; 90 | color: #1a1a37; 91 | } 92 | .traffic-name { 93 | overflow: hidden; 94 | font-family: DIN; 95 | font-size: 15px; 96 | font-weight: 400; 97 | color: #1a1a37; 98 | white-space: nowrap; 99 | } 100 | .gitee-traffic { 101 | background: url("./images/1-bg.png"); 102 | background-color: #e8faea; 103 | background-size: 100% 100%; 104 | } 105 | .gitHub-traffic { 106 | background: url("./images/2-bg.png"); 107 | background-color: #e7e1fb; 108 | background-size: 100% 100%; 109 | } 110 | .today-traffic { 111 | background: url("./images/3-bg.png"); 112 | background-color: #fdf3e9; 113 | background-size: 100% 100%; 114 | } 115 | .yesterday-traffic { 116 | background: url("./images/4-bg.png"); 117 | background-color: #f0f5fb; 118 | background-size: 100% 100%; 119 | } 120 | } 121 | .item-right { 122 | box-sizing: border-box; 123 | display: flex; 124 | flex-direction: column; 125 | width: 40%; 126 | height: 430px; 127 | border: 1px solid #e5e7eb; 128 | border-radius: 25px; 129 | .echarts-title { 130 | padding: 15px 20px; 131 | font-family: "PingFang SC"; 132 | font-size: 18px; 133 | font-weight: 600; 134 | border-bottom: 1px solid #e5e7eb; 135 | } 136 | .book-echarts { 137 | flex: 1; 138 | width: 100%; 139 | } 140 | } 141 | } 142 | } 143 | .bottom-box { 144 | position: relative; 145 | padding: 20px 0 0; 146 | .bottom-title { 147 | position: absolute; 148 | top: 16%; 149 | left: 3%; 150 | font-family: "PingFang SC"; 151 | font-size: 18px; 152 | font-weight: 600; 153 | } 154 | .bottom-tabs { 155 | padding: 0 50px; 156 | } 157 | .curve-echarts { 158 | width: 100%; 159 | height: 400px; 160 | padding-left: 10px; 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/views/dashboard/dataVisualize/index.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from "antd"; 2 | import Pie from "./components/pie"; 3 | import Curve from "./components/curve"; 4 | import "./index.less"; 5 | import BookSum from "./images/book-sum.png"; 6 | import AddPerson from "./images/add_person.png"; 7 | import AddTeam from "./images/add_team.png"; 8 | import Today from "./images/today.png"; 9 | import BookSum1 from "./images/book_sum.png"; 10 | 11 | const { TabPane } = Tabs; 12 | 13 | const DataVisualize = () => { 14 | const onChange = (key: string) => { 15 | console.log(key); 16 | }; 17 | 18 | const tabsList = [ 19 | { label: "未来7日", name: 1 }, 20 | { label: "近七日", name: 2 }, 21 | { label: "近一月", name: 3 }, 22 | { label: "近三月", name: 4 }, 23 | { label: "近半年", name: 5 }, 24 | { label: "近一年", name: 6 } 25 | ]; 26 | 27 | return ( 28 |
29 |
30 |
数据可视化
31 | 32 | {tabsList.map(item => { 33 | return ; 34 | })} 35 | 36 |
37 |
38 | 访问总数 39 |
40 | 41 |
42 | 848.132w 43 |
44 |
45 |
46 |
47 | 48 |
49 | 2222 50 | Gitee 访问量 51 |
52 |
53 |
54 | 55 |
56 | 2222 57 | GitHub 访问量 58 |
59 |
60 |
61 | 62 |
63 | 4567 64 | 今日访问量 65 |
66 |
67 |
68 | 69 |
70 | 1234 71 | 昨日访问量 72 |
73 |
74 |
75 |
Gitee / GitHub 访问量占比
76 |
77 | 78 |
79 |
80 |
81 |
82 |
83 |
数据来源
84 |
85 | 86 | {tabsList.map(item => { 87 | return ; 88 | })} 89 | 90 |
91 |
92 | 93 |
94 |
95 |
96 | ); 97 | }; 98 | 99 | export default DataVisualize; 100 | -------------------------------------------------------------------------------- /src/views/dashboard/embedded/index.less: -------------------------------------------------------------------------------- 1 | .full-iframe { 2 | width: 100%; 3 | height: 99%; 4 | } 5 | -------------------------------------------------------------------------------- /src/views/dashboard/embedded/index.tsx: -------------------------------------------------------------------------------- 1 | import "./index.less"; 2 | 3 | const Embedded = () => { 4 | return ; 5 | }; 6 | 7 | export default Embedded; 8 | -------------------------------------------------------------------------------- /src/views/dataScreen/assets/alarmList.Json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "warnMsg": "2022-04-25 14:09-23:09 预约人数 1,006 人次,已达到最大承载量的 99 %", 5 | "label": "峨眉山" 6 | }, 7 | { 8 | "id": 2, 9 | "warnMsg": "2022-04-12 19:30-20:30 预约人数 66,666 人次,已达到最大承载量的 25 %", 10 | "label": "稻城亚丁" 11 | }, 12 | { 13 | "id": 3, 14 | "warnMsg": "2022-04-09 14:09-23:09 预约人数 5,813 人次,已达到最大承载量的 3 %", 15 | "label": "九寨沟" 16 | }, 17 | { 18 | "id": 4, 19 | "warnMsg": "2022-04-07 22:39-23:39 预约人数 999 人次,已达到最大承载量的 13 %", 20 | "label": "万里长城" 21 | }, 22 | { 23 | "id": 5, 24 | "warnMsg": "2022-03-29 09:00-12:00 预约人数 123,368 人次,已达到最大承载量的 46 %", 25 | "label": "北京故宫" 26 | }, 27 | { 28 | "id": 6, 29 | "warnMsg": "2022-03-29 09:00-12:00 预约人数 869 人次,已达到最大承载量的 95 %", 30 | "label": "阆中古城" 31 | }, 32 | { 33 | "id": 7, 34 | "warnMsg": "2022-03-29 09:00-12:00 预约人数 6,985 人次,已达到最大承载量的 80 %", 35 | "label": "乐山大佛" 36 | }, 37 | { 38 | "id": 8, 39 | "warnMsg": "2022-03-29 09:00-12:00 预约人数 25,696 人次,已达到最大承载量的 70 %", 40 | "label": "阿坝州黄龙" 41 | }, 42 | { 43 | "id": 9, 44 | "warnMsg": "2022-03-29 09:00-12:00 预约人数 45,987 人次,已达到最大承载量的 55 %", 45 | "label": "青城山" 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /src/views/dataScreen/components/AgeRatioChart.tsx: -------------------------------------------------------------------------------- 1 | import { useEcharts } from "@/hooks/useEcharts"; 2 | import { EChartsOption } from "echarts"; 3 | 4 | interface ChartProp { 5 | value: string; 6 | name: string; 7 | percentage: string; 8 | } 9 | const AgeRatioChart = () => { 10 | let data: any = [ 11 | { 12 | value: 200, 13 | name: "10岁以下", 14 | percentage: "16%" 15 | }, 16 | { 17 | value: 110, 18 | name: "10 - 18岁", 19 | percentage: "8%" 20 | }, 21 | { 22 | value: 150, 23 | name: "18 - 30岁", 24 | percentage: "12%" 25 | }, 26 | { 27 | value: 310, 28 | name: "30 - 40岁", 29 | percentage: "24%" 30 | }, 31 | { 32 | value: 250, 33 | name: "40 - 60岁", 34 | percentage: "20%" 35 | }, 36 | { 37 | value: 260, 38 | name: "60岁以上", 39 | percentage: "20%" 40 | } 41 | ]; 42 | const colors = ["#F6C95C", "#EF7D33", "#1F9393", "#184EA1", "#81C8EF", "#9270CA"]; 43 | const option: EChartsOption = { 44 | color: colors, 45 | tooltip: { 46 | show: true, 47 | trigger: "item", 48 | formatter: "{b}
占比:{d}%" 49 | }, 50 | legend: { 51 | orient: "vertical", 52 | right: "20px", 53 | top: "15px", 54 | itemGap: 15, 55 | itemWidth: 14, 56 | formatter: function (name) { 57 | let text = ""; 58 | data.forEach((val: ChartProp) => { 59 | if (val.name === name) { 60 | text = " " + name + "  " + val.percentage; 61 | } 62 | }); 63 | return text; 64 | }, 65 | textStyle: { 66 | color: "#fff" 67 | } 68 | }, 69 | grid: { 70 | top: "bottom", 71 | left: 10, 72 | bottom: 10 73 | }, 74 | series: [ 75 | { 76 | zlevel: 1, 77 | name: "年龄比例", 78 | type: "pie", 79 | selectedMode: "single", 80 | radius: [50, 90], 81 | center: ["35%", "50%"], 82 | startAngle: 60, 83 | // hoverAnimation: false, 84 | label: { 85 | position: "inside", 86 | show: true, 87 | color: "#fff", 88 | formatter: function (params: any) { 89 | return params.data.percentage; 90 | }, 91 | rich: { 92 | b: { 93 | fontSize: 16, 94 | lineHeight: 30, 95 | color: "#fff" 96 | } 97 | } 98 | }, 99 | itemStyle: { 100 | shadowColor: "rgba(0, 0, 0, 0.2)", 101 | shadowBlur: 10 102 | }, 103 | data: data.map((val: ChartProp, index: number) => { 104 | return { 105 | value: val.value, 106 | name: val.name, 107 | percentage: val.percentage, 108 | itemStyle: { 109 | borderWidth: 10, 110 | shadowBlur: 20, 111 | borderColor: colors[index], 112 | borderRadius: 10 113 | } 114 | }; 115 | }) 116 | }, 117 | { 118 | name: "", 119 | type: "pie", 120 | selectedMode: "single", 121 | radius: [50, 90], 122 | center: ["35%", "50%"], 123 | startAngle: 60, 124 | data: [ 125 | { 126 | value: 1000, 127 | name: "", 128 | label: { 129 | show: true, 130 | formatter: "{a|本日总数}", 131 | rich: { 132 | a: { 133 | align: "center", 134 | color: "rgb(98,137,169)", 135 | fontSize: 14 136 | } 137 | }, 138 | position: "center" 139 | } 140 | } 141 | ] 142 | } 143 | ] 144 | }; 145 | const [echartsRef] = useEcharts(option, data); 146 | return
; 147 | }; 148 | 149 | export default AgeRatioChart; 150 | -------------------------------------------------------------------------------- /src/views/dataScreen/components/AnnualUseChart.less: -------------------------------------------------------------------------------- 1 | .annual-tooTip { 2 | box-sizing: border-box; 3 | width: 206px; 4 | height: 103px; 5 | padding: 5px 20px; 6 | background: url("../images/contrast-bg.png") no-repeat; 7 | background-size: 100% 100%; 8 | .annual-month { 9 | display: inline-block; 10 | margin-bottom: 2px; 11 | font-size: 10px; 12 | color: #03b8e2; 13 | transform: scale(0.9); 14 | } 15 | .annual-list { 16 | display: flex; 17 | flex-direction: column; 18 | width: 100%; 19 | .year-item { 20 | display: flex; 21 | align-items: center; 22 | width: 100%; 23 | height: 22px; 24 | .year-dot { 25 | width: 5px; 26 | height: 5px; 27 | margin: 0 2px; 28 | border-radius: 50%; 29 | } 30 | .year-name, 31 | .year-value { 32 | font-size: 10px; 33 | color: #03b8e2; 34 | transform: scale(0.8); 35 | } 36 | .year-name { 37 | margin: 0 2px; 38 | } 39 | .year-value { 40 | display: inline-block; 41 | width: 25%; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/views/dataScreen/components/ChinaMapChart.less: -------------------------------------------------------------------------------- 1 | .map-ball { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | width: 900px; 6 | height: 900px; 7 | transform: translate(-50%, -50%); 8 | img { 9 | width: 500px; 10 | height: 500px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/views/dataScreen/components/DataHeaderTime.tsx: -------------------------------------------------------------------------------- 1 | import { useTimes } from "@/hooks/useTime"; 2 | const DataHeaderTime = () => { 3 | const { time } = useTimes(); 4 | 5 | return 当前时间:{time}; 6 | }; 7 | 8 | export default DataHeaderTime; 9 | -------------------------------------------------------------------------------- /src/views/dataScreen/components/HotPlateChart.less: -------------------------------------------------------------------------------- 1 | .echarts-header { 2 | box-sizing: border-box; 3 | display: flex; 4 | height: 36px; 5 | margin: 10px 10px 0; 6 | line-height: 36px; 7 | background: url("../images/rankingChart-bg.png") no-repeat; 8 | background-size: 100% 100%; 9 | span { 10 | width: 18%; 11 | margin-left: 4px; 12 | font-size: 14px; 13 | font-weight: bold; 14 | color: #fdbc52; 15 | text-align: center; 16 | &:nth-child(2) { 17 | margin-left: 4px; 18 | } 19 | &:last-child { 20 | width: 20%; 21 | margin-left: 60px; 22 | } 23 | } 24 | } 25 | .hot-echarts { 26 | width: 100%; 27 | height: calc(100% - 56px); 28 | } 29 | -------------------------------------------------------------------------------- /src/views/dataScreen/components/MaleFemaleRatioChart.less: -------------------------------------------------------------------------------- 1 | .malefemaleRatio-main { 2 | box-sizing: border-box; 3 | width: 100%; 4 | height: 100%; 5 | padding: 40px 65px; 6 | .malefemaleRatio-header { 7 | display: flex; 8 | justify-content: space-between; 9 | width: 100%; 10 | height: 115px; 11 | .man, 12 | .woman { 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | width: 110px; 17 | height: 115px; 18 | background: url("../images/man-bg.png") no-repeat; 19 | background-size: 100% 100%; 20 | img { 21 | width: 60px; 22 | height: 60px; 23 | margin-top: 17px; 24 | } 25 | span { 26 | margin-top: 2px; 27 | font-size: 13px; 28 | color: #ffffff; 29 | } 30 | } 31 | .woman { 32 | background: url("../images/woman-bg.png") no-repeat; 33 | } 34 | } 35 | .echarts { 36 | width: 100%; 37 | height: calc(100% - 115px); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/views/dataScreen/components/MaleFemaleRatioChart.tsx: -------------------------------------------------------------------------------- 1 | import { useEcharts } from "@/hooks/useEcharts"; 2 | import { EChartsOption } from "echarts"; 3 | import man from "../images/man.png"; 4 | import woman from "../images/woman.png"; 5 | import "./MaleFemaleRatioChart.less"; 6 | 7 | interface ChartProp { 8 | man: number; 9 | woman: number; 10 | } 11 | const MaleFemaleRatioChart = () => { 12 | let data: ChartProp = { 13 | man: 0.6, 14 | woman: 0.4 15 | }; 16 | const option: EChartsOption = { 17 | xAxis: { 18 | type: "value", 19 | show: false 20 | }, 21 | grid: { 22 | left: 0, 23 | top: "30px", 24 | bottom: 0, 25 | right: 0 26 | }, 27 | yAxis: [ 28 | { 29 | type: "category", 30 | position: "left", 31 | data: ["男生"], 32 | axisTick: { 33 | show: false 34 | }, 35 | axisLine: { 36 | show: false 37 | }, 38 | axisLabel: { 39 | show: false 40 | } 41 | }, 42 | { 43 | type: "category", 44 | position: "right", 45 | data: ["女士"], 46 | axisTick: { 47 | show: false 48 | }, 49 | axisLine: { 50 | show: false 51 | }, 52 | axisLabel: { 53 | show: false, 54 | padding: [0, 0, 40, -60], 55 | fontSize: 12, 56 | lineHeight: 60, 57 | color: "rgba(255, 255, 255, 0.9)", 58 | formatter: "{value}" + data.woman * 100 + "%", 59 | rich: { 60 | a: { 61 | color: "transparent", 62 | lineHeight: 30, 63 | fontFamily: "digital", 64 | fontSize: 12 65 | } 66 | } 67 | } 68 | } 69 | ], 70 | series: [ 71 | { 72 | type: "bar", 73 | barWidth: 20, 74 | data: [data.man], 75 | z: 20, 76 | itemStyle: { 77 | borderRadius: 10, 78 | color: "#007AFE" 79 | }, 80 | label: { 81 | show: true, 82 | color: "#E7E8ED", 83 | position: "insideLeft", 84 | offset: [0, -20], 85 | fontSize: 12, 86 | formatter: () => { 87 | return `男士 ${data.man * 100}%`; 88 | } 89 | } 90 | }, 91 | { 92 | type: "bar", 93 | barWidth: 20, 94 | data: [1], 95 | barGap: "-100%", 96 | itemStyle: { 97 | borderRadius: 10, 98 | color: "#FF4B7A" 99 | }, 100 | label: { 101 | show: true, 102 | color: "#E7E8ED", 103 | position: "insideRight", 104 | offset: [0, -20], 105 | fontSize: 12, 106 | formatter: () => { 107 | return `女士 ${data.woman * 100}%`; 108 | } 109 | } 110 | } 111 | ] 112 | }; 113 | const [echartsRef] = useEcharts(option, data); 114 | return ( 115 |
116 |
117 |
118 | 男士 119 | 120 |
121 |
122 | 女士 123 | 124 |
125 |
126 |
127 |
128 | ); 129 | }; 130 | 131 | export default MaleFemaleRatioChart; 132 | -------------------------------------------------------------------------------- /src/views/dataScreen/components/OverNext30Chart.less: -------------------------------------------------------------------------------- 1 | .lineChart-bg { 2 | box-sizing: border-box; 3 | display: flex; 4 | align-items: center; 5 | width: 180px; 6 | height: 60px; 7 | padding-left: 20px; 8 | background: url("../images/line-bg.png") no-repeat; 9 | background-size: 100% 100%; 10 | span { 11 | font-size: 12px; 12 | color: rgb(255 255 255 / 80%); 13 | i { 14 | font-style: normal; 15 | color: #f5b348; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/views/dataScreen/components/RealTimeAccessChart.less: -------------------------------------------------------------------------------- 1 | .actual-total { 2 | position: relative; 3 | display: flex; 4 | align-items: center; 5 | justify-content: flex-end; 6 | height: 50px; 7 | margin-top: 10px; 8 | margin-right: 4px; 9 | .actual-item { 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | width: 52px; 14 | height: 50px; 15 | margin-right: 1px; 16 | font-family: MetroDF; 17 | font-size: 32px; 18 | color: #66ffff; 19 | background: url("../images/total.png") no-repeat; 20 | background-size: 100% 100%; 21 | &:last-child { 22 | margin-right: 0; 23 | font-size: 22px; 24 | } 25 | } 26 | .expect-total { 27 | position: absolute; 28 | top: -30px; 29 | right: 5px; 30 | font-family: "PingFang SC"; 31 | font-size: 14px; 32 | color: #ffffff; 33 | i { 34 | font-style: normal; 35 | font-style: oblique; 36 | color: #ff8100; 37 | } 38 | } 39 | } 40 | .actual-echarts { 41 | width: 100%; 42 | height: calc(100% - 50px); 43 | } 44 | -------------------------------------------------------------------------------- /src/views/dataScreen/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/bg.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/contrast-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/contrast-bg.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-alarm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-alarm.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-header-btn-bg-l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-header-btn-bg-l.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-header-btn-bg-r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-header-btn-bg-r.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-header-center-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-header-center-bg.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-header-left-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-header-left-bg.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-header-right-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-header-right-bg.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-header-warn-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-header-warn-bg.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-main-cb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-main-cb.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-main-lb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-main-lb.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-main-lc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-main-lc.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-main-lt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-main-lt.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-main-rb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-main-rb.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-main-rc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-main-rc.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-main-rt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-main-rt.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-title.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/dataScreen-warn-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/dataScreen-warn-bg.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/line-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/line-bg.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/man-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/man-bg.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/man.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/map-title-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/map-title-bg.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/rankingChart-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/rankingChart-bg.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/total.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/total.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/woman-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/woman-bg.png -------------------------------------------------------------------------------- /src/views/dataScreen/images/woman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/dataScreen/images/woman.png -------------------------------------------------------------------------------- /src/views/echarts/columnChart/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/echarts/columnChart/index.less -------------------------------------------------------------------------------- /src/views/echarts/columnChart/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEcharts } from "@/hooks/useEcharts"; 2 | import * as echarts from "echarts"; 3 | 4 | const ColumnChart = () => { 5 | let option: echarts.EChartsOption = { 6 | tooltip: { 7 | trigger: "axis", 8 | axisPointer: { 9 | type: "shadow" 10 | } 11 | }, 12 | legend: { 13 | textStyle: { 14 | color: "#a1a1a1" 15 | } 16 | }, 17 | grid: { 18 | left: "3%", 19 | right: "4%", 20 | bottom: "3%", 21 | containLabel: true 22 | }, 23 | xAxis: [ 24 | { 25 | type: "category", 26 | data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], 27 | axisLabel: { 28 | color: "#a1a1a1" 29 | } 30 | } 31 | ], 32 | yAxis: [ 33 | { 34 | type: "value", 35 | axisLabel: { 36 | color: "#a1a1a1" 37 | } 38 | } 39 | ], 40 | series: [ 41 | { 42 | name: "Direct", 43 | type: "bar", 44 | emphasis: { 45 | focus: "series" 46 | }, 47 | data: [320, 332, 301, 334, 390, 330, 320] 48 | }, 49 | { 50 | name: "Email", 51 | type: "bar", 52 | stack: "Ad", 53 | emphasis: { 54 | focus: "series" 55 | }, 56 | data: [120, 132, 101, 134, 90, 230, 210] 57 | }, 58 | { 59 | name: "Union Ads", 60 | type: "bar", 61 | stack: "Ad", 62 | emphasis: { 63 | focus: "series" 64 | }, 65 | data: [220, 182, 191, 234, 290, 330, 310] 66 | }, 67 | { 68 | name: "Video Ads", 69 | type: "bar", 70 | stack: "Ad", 71 | emphasis: { 72 | focus: "series" 73 | }, 74 | data: [150, 232, 201, 154, 190, 330, 410] 75 | }, 76 | { 77 | name: "Search Engine", 78 | type: "bar", 79 | data: [862, 1018, 964, 1026, 1679, 1600, 1570], 80 | emphasis: { 81 | focus: "series" 82 | }, 83 | markLine: { 84 | lineStyle: { 85 | type: "dashed" 86 | }, 87 | data: [[{ type: "min" }, { type: "max" }]] 88 | } 89 | }, 90 | { 91 | name: "Baidu", 92 | type: "bar", 93 | barWidth: 5, 94 | stack: "Search Engine", 95 | emphasis: { 96 | focus: "series" 97 | }, 98 | data: [620, 732, 701, 734, 1090, 1130, 1120] 99 | }, 100 | { 101 | name: "Google", 102 | type: "bar", 103 | stack: "Search Engine", 104 | emphasis: { 105 | focus: "series" 106 | }, 107 | data: [120, 132, 101, 134, 290, 230, 220] 108 | }, 109 | { 110 | name: "Bing", 111 | type: "bar", 112 | stack: "Search Engine", 113 | emphasis: { 114 | focus: "series" 115 | }, 116 | data: [60, 72, 71, 74, 190, 130, 110] 117 | }, 118 | { 119 | name: "Others", 120 | type: "bar", 121 | stack: "Search Engine", 122 | emphasis: { 123 | focus: "series" 124 | }, 125 | data: [62, 82, 91, 84, 109, 110, 120] 126 | } 127 | ] 128 | }; 129 | 130 | const [echartsRef] = useEcharts(option); 131 | return
; 132 | }; 133 | 134 | export default ColumnChart; 135 | -------------------------------------------------------------------------------- /src/views/echarts/lineChart/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/echarts/lineChart/index.less -------------------------------------------------------------------------------- /src/views/echarts/lineChart/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEcharts } from "@/hooks/useEcharts"; 2 | import * as echarts from "echarts"; 3 | 4 | const LineChart = () => { 5 | let option: echarts.EChartsOption = { 6 | title: { 7 | text: "Stacked Area Chart", 8 | textStyle: { 9 | color: "#a1a1a1" 10 | } 11 | }, 12 | tooltip: { 13 | trigger: "axis", 14 | axisPointer: { 15 | type: "cross", 16 | label: { 17 | backgroundColor: "#6a7985" 18 | } 19 | } 20 | }, 21 | legend: { 22 | data: ["Email", "Union Ads", "Video Ads", "Direct", "Search Engine"], 23 | textStyle: { 24 | color: "#a1a1a1" 25 | } 26 | }, 27 | toolbox: { 28 | feature: { 29 | saveAsImage: {} 30 | } 31 | }, 32 | grid: { 33 | left: "3%", 34 | right: "4%", 35 | bottom: "3%", 36 | containLabel: true 37 | }, 38 | xAxis: [ 39 | { 40 | type: "category", 41 | boundaryGap: false, 42 | data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], 43 | axisLabel: { 44 | color: "#a1a1a1" 45 | } 46 | } 47 | ], 48 | yAxis: [ 49 | { 50 | type: "value", 51 | axisLabel: { 52 | color: "#a1a1a1" 53 | } 54 | } 55 | ], 56 | series: [ 57 | { 58 | name: "Email", 59 | type: "line", 60 | stack: "Total", 61 | areaStyle: {}, 62 | emphasis: { 63 | focus: "series" 64 | }, 65 | data: [120, 132, 101, 134, 90, 230, 210] 66 | }, 67 | { 68 | name: "Union Ads", 69 | type: "line", 70 | stack: "Total", 71 | areaStyle: {}, 72 | emphasis: { 73 | focus: "series" 74 | }, 75 | data: [220, 182, 191, 234, 290, 330, 310] 76 | }, 77 | { 78 | name: "Video Ads", 79 | type: "line", 80 | stack: "Total", 81 | areaStyle: {}, 82 | emphasis: { 83 | focus: "series" 84 | }, 85 | data: [150, 232, 201, 154, 190, 330, 410] 86 | }, 87 | { 88 | name: "Direct", 89 | type: "line", 90 | stack: "Total", 91 | areaStyle: {}, 92 | emphasis: { 93 | focus: "series" 94 | }, 95 | data: [320, 332, 301, 334, 390, 330, 320] 96 | }, 97 | { 98 | name: "Search Engine", 99 | type: "line", 100 | stack: "Total", 101 | label: { 102 | show: true, 103 | position: "top" 104 | }, 105 | areaStyle: {}, 106 | emphasis: { 107 | focus: "series" 108 | }, 109 | data: [820, 932, 901, 934, 1290, 1330, 1320] 110 | } 111 | ] 112 | }; 113 | 114 | const [echartsRef] = useEcharts(option); 115 | return
; 116 | }; 117 | 118 | export default LineChart; 119 | -------------------------------------------------------------------------------- /src/views/echarts/nestedChart/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/echarts/nestedChart/index.less -------------------------------------------------------------------------------- /src/views/echarts/nestedChart/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEcharts } from "@/hooks/useEcharts"; 2 | import * as echarts from "echarts"; 3 | 4 | const NestedChart = () => { 5 | let option: echarts.EChartsOption = { 6 | tooltip: { 7 | trigger: "item", 8 | formatter: "{a}
{b}: {c} ({d}%)" 9 | }, 10 | legend: { 11 | data: ["Direct", "Marketing", "Search Engine", "Email", "Union Ads", "Video Ads", "Baidu", "Google", "Bing", "Others"], 12 | textStyle: { 13 | color: "#a1a1a1" 14 | } 15 | }, 16 | series: [ 17 | { 18 | name: "Access From", 19 | type: "pie", 20 | selectedMode: "single", 21 | radius: [0, "30%"], 22 | label: { 23 | position: "inner", 24 | fontSize: 14 25 | }, 26 | labelLine: { 27 | show: false 28 | }, 29 | data: [ 30 | { value: 1548, name: "Search Engine" }, 31 | { value: 775, name: "Direct" }, 32 | { value: 679, name: "Marketing", selected: true } 33 | ] 34 | }, 35 | { 36 | name: "Access From", 37 | type: "pie", 38 | radius: ["45%", "60%"], 39 | labelLine: { 40 | length: 30 41 | }, 42 | label: { 43 | formatter: "{a|{a}}{abg|}\n{hr|}\n {b|{b}:}{c} {per|{d}%} ", 44 | backgroundColor: "#F6F8FC", 45 | borderColor: "#8C8D8E", 46 | borderWidth: 1, 47 | borderRadius: 4, 48 | rich: { 49 | a: { 50 | color: "#6E7079", 51 | lineHeight: 22, 52 | align: "center" 53 | }, 54 | hr: { 55 | borderColor: "#8C8D8E", 56 | width: "100%", 57 | borderWidth: 1, 58 | height: 0 59 | }, 60 | b: { 61 | color: "#4C5058", 62 | fontSize: 14, 63 | fontWeight: "bold", 64 | lineHeight: 33 65 | }, 66 | per: { 67 | color: "#fff", 68 | backgroundColor: "#4C5058", 69 | padding: [3, 4], 70 | borderRadius: 4 71 | } 72 | } 73 | }, 74 | data: [ 75 | { value: 1048, name: "Baidu" }, 76 | { value: 335, name: "Direct" }, 77 | { value: 310, name: "Email" }, 78 | { value: 251, name: "Google" }, 79 | { value: 234, name: "Union Ads" }, 80 | { value: 147, name: "Bing" }, 81 | { value: 135, name: "Video Ads" }, 82 | { value: 102, name: "Others" } 83 | ] 84 | } 85 | ] 86 | }; 87 | 88 | const [echartsRef] = useEcharts(option); 89 | return
; 90 | }; 91 | 92 | export default NestedChart; 93 | -------------------------------------------------------------------------------- /src/views/echarts/pieChart/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/echarts/pieChart/index.less -------------------------------------------------------------------------------- /src/views/echarts/pieChart/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEcharts } from "@/hooks/useEcharts"; 2 | import * as echarts from "echarts"; 3 | 4 | const PieChart = () => { 5 | let option: echarts.EChartsOption = { 6 | tooltip: { 7 | trigger: "item", 8 | formatter: "{a}
{b} : {c} ({d}%)" 9 | }, 10 | legend: { 11 | left: "center", 12 | top: "bottom", 13 | data: ["rose 1", "rose 2", "rose 3", "rose 4", "rose 5", "rose 6", "rose 7", "rose 8"], 14 | textStyle: { 15 | color: "#a1a1a1" 16 | } 17 | }, 18 | toolbox: { 19 | show: true, 20 | feature: { 21 | mark: { show: true }, 22 | dataView: { show: true, readOnly: false }, 23 | restore: { show: true }, 24 | saveAsImage: { show: true } 25 | } 26 | }, 27 | series: [ 28 | { 29 | name: "Radius Mode", 30 | type: "pie", 31 | radius: [60, 280], 32 | center: ["50%", "50%"], 33 | roseType: "radius", 34 | itemStyle: { 35 | borderRadius: 5 36 | }, 37 | label: { 38 | show: true 39 | }, 40 | emphasis: { 41 | label: { 42 | show: true 43 | } 44 | }, 45 | data: [ 46 | { value: 40, name: "rose 1" }, 47 | { value: 33, name: "rose 2" }, 48 | { value: 28, name: "rose 3" }, 49 | { value: 22, name: "rose 4" }, 50 | { value: 20, name: "rose 5" }, 51 | { value: 15, name: "rose 6" }, 52 | { value: 12, name: "rose 7" }, 53 | { value: 10, name: "rose 8" } 54 | ] 55 | } 56 | ] 57 | }; 58 | 59 | const [echartsRef] = useEcharts(option); 60 | return
; 61 | }; 62 | 63 | export default PieChart; 64 | -------------------------------------------------------------------------------- /src/views/echarts/radarChart/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/echarts/radarChart/index.less -------------------------------------------------------------------------------- /src/views/echarts/radarChart/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEcharts } from "@/hooks/useEcharts"; 2 | import * as echarts from "echarts"; 3 | 4 | const RadarChart = () => { 5 | let option: echarts.EChartsOption = { 6 | title: { 7 | text: "Basic Radar Chart", 8 | textStyle: { 9 | color: "#a1a1a1" 10 | } 11 | }, 12 | legend: { 13 | data: ["Allocated Budget", "Actual Spending"], 14 | textStyle: { 15 | color: "#a1a1a1" 16 | } 17 | }, 18 | radar: { 19 | // shape: 'circle', 20 | indicator: [ 21 | { name: "Sales", max: 6500 }, 22 | { name: "Administration", max: 16000 }, 23 | { name: "Information Technology", max: 30000 }, 24 | { name: "Customer Support", max: 38000 }, 25 | { name: "Development", max: 52000 }, 26 | { name: "Marketing", max: 25000 } 27 | ] 28 | }, 29 | series: [ 30 | { 31 | name: "Budget vs spending", 32 | type: "radar", 33 | data: [ 34 | { 35 | value: [4200, 3000, 20000, 35000, 50000, 18000], 36 | name: "Allocated Budget" 37 | }, 38 | { 39 | value: [5000, 14000, 28000, 26000, 42000, 21000], 40 | name: "Actual Spending" 41 | } 42 | ] 43 | } 44 | ] 45 | }; 46 | 47 | const [echartsRef] = useEcharts(option); 48 | return
; 49 | }; 50 | 51 | export default RadarChart; 52 | -------------------------------------------------------------------------------- /src/views/echarts/waterChart/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/echarts/waterChart/index.less -------------------------------------------------------------------------------- /src/views/form/basicForm/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/form/basicForm/index.less -------------------------------------------------------------------------------- /src/views/form/basicForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form, Input, Select, Space, message } from "antd"; 2 | import "./index.less"; 3 | 4 | const BasicForm = () => { 5 | const { Option } = Select; 6 | const [form] = Form.useForm(); 7 | 8 | const onGenderChange = (value: string) => { 9 | switch (value) { 10 | case "male": 11 | form.setFieldsValue({ note: "Hi, man!" }); 12 | return; 13 | case "female": 14 | form.setFieldsValue({ note: "Hi, lady!" }); 15 | return; 16 | case "other": 17 | form.setFieldsValue({ note: "Hi there!" }); 18 | } 19 | }; 20 | 21 | const onFinish = (values: any) => { 22 | message.success("提交的数据为 : " + JSON.stringify(values)); 23 | console.log(JSON.stringify(values)); 24 | }; 25 | 26 | const onReset = () => { 27 | form.resetFields(); 28 | }; 29 | 30 | const onFill = () => { 31 | form.setFieldsValue({ 32 | user: "mark", 33 | note: "Hello world!", 34 | gender: "male" 35 | }); 36 | }; 37 | 38 | return ( 39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 53 | 54 | 55 | 56 | 59 | 62 | {" "} 65 | 66 | 67 |
68 |
69 | ); 70 | }; 71 | 72 | export default BasicForm; 73 | -------------------------------------------------------------------------------- /src/views/form/dynamicForm/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/form/dynamicForm/index.less -------------------------------------------------------------------------------- /src/views/form/dynamicForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; 2 | import { Button, Form, Input, Space } from "antd"; 3 | import "./index.less"; 4 | 5 | const DynamicForm = () => { 6 | const onFinish = (values: any) => { 7 | console.log("Received values of form:", values); 8 | }; 9 | 10 | return ( 11 |
12 |
13 | 14 | {(fields, { add, remove }) => ( 15 | <> 16 | {fields.map(({ key, name, ...restField }) => ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | remove(name)} /> 25 | 26 | ))} 27 | 28 | 31 | 32 | 33 | )} 34 | 35 | 36 | 39 | 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default DynamicForm; 46 | -------------------------------------------------------------------------------- /src/views/form/validateForm/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/form/validateForm/index.less -------------------------------------------------------------------------------- /src/views/form/validateForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form, Input, Select, Space, message } from "antd"; 2 | import "./index.less"; 3 | 4 | const ValidateForm = () => { 5 | const { Option } = Select; 6 | const [form] = Form.useForm(); 7 | 8 | const onGenderChange = (value: string) => { 9 | switch (value) { 10 | case "male": 11 | form.setFieldsValue({ note: "Hi, man!" }); 12 | return; 13 | case "female": 14 | form.setFieldsValue({ note: "Hi, lady!" }); 15 | return; 16 | case "other": 17 | form.setFieldsValue({ note: "Hi there!" }); 18 | } 19 | }; 20 | 21 | const onFinish = (values: any) => { 22 | message.success("提交的数据为 : " + JSON.stringify(values)); 23 | console.log(JSON.stringify(values)); 24 | }; 25 | 26 | const onReset = () => { 27 | form.resetFields(); 28 | }; 29 | 30 | const onFill = () => { 31 | form.setFieldsValue({ 32 | user: "mark", 33 | note: "Hello world!", 34 | gender: "male" 35 | }); 36 | }; 37 | 38 | return ( 39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 53 | 54 | 55 | 56 | 59 | 62 | 65 | 66 | 67 |
68 |
69 | ); 70 | }; 71 | 72 | export default ValidateForm; 73 | -------------------------------------------------------------------------------- /src/views/home/index.less: -------------------------------------------------------------------------------- 1 | .home { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | height: 100%; 6 | img { 7 | width: 70%; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/views/home/index.tsx: -------------------------------------------------------------------------------- 1 | import welcome from "@/assets/images/welcome01.png"; 2 | import "./index.less"; 3 | 4 | const Home = () => { 5 | return ( 6 |
7 | welcome 8 |
9 | ); 10 | }; 11 | 12 | export default Home; 13 | -------------------------------------------------------------------------------- /src/views/link/gitee/index.tsx: -------------------------------------------------------------------------------- 1 | const Gitee = () => { 2 | return ( 3 |
4 | 5 | Gitee 仓库: 6 | 7 | https://gitee.com/laramie/Hooks-Admin 8 | {" "} 9 | 🍒🍉🍊 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default Gitee; 16 | -------------------------------------------------------------------------------- /src/views/link/github/index.tsx: -------------------------------------------------------------------------------- 1 | const Github = () => { 2 | return ( 3 |
4 | 5 | Github 仓库: 6 | 7 | https://github.com/HalseySpicy/Hooks-Admin 8 | {" "} 9 | 🍒🍉🍊 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default Github; 16 | -------------------------------------------------------------------------------- /src/views/link/juejin/index.tsx: -------------------------------------------------------------------------------- 1 | const Juejin = () => { 2 | return ( 3 |
4 | 5 | 掘金文档: 6 | 7 | https://juejin.cn/user/3263814531551816/posts 8 | {" "} 9 | 🍒🍉🍊 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default Juejin; 16 | -------------------------------------------------------------------------------- /src/views/link/myBlog/index.tsx: -------------------------------------------------------------------------------- 1 | const MyBlog = () => { 2 | return ( 3 |
4 | 5 | MyBlog : 6 | 7 | http://www.spicyboy.cn 8 | {" "} 9 | 🍒🍉🍊 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default MyBlog; 16 | -------------------------------------------------------------------------------- /src/views/login/components/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import md5 from "js-md5"; 2 | import { useState } from "react"; 3 | import { Button, Form, Input, message } from "antd"; 4 | import { useNavigate } from "react-router-dom"; 5 | import { Login } from "@/api/interface"; 6 | import { loginApi } from "@/api/modules/login"; 7 | import { HOME_URL } from "@/config/config"; 8 | import { connect } from "react-redux"; 9 | import { setToken } from "@/redux/modules/global/action"; 10 | import { useTranslation } from "react-i18next"; 11 | import { setTabsList } from "@/redux/modules/tabs/action"; 12 | import { UserOutlined, LockOutlined, CloseCircleOutlined } from "@ant-design/icons"; 13 | 14 | const LoginForm = (props: any) => { 15 | const { t } = useTranslation(); 16 | const { setToken, setTabsList } = props; 17 | const navigate = useNavigate(); 18 | const [form] = Form.useForm(); 19 | const [loading, setLoading] = useState(false); 20 | 21 | // 登录 22 | const onFinish = async (loginForm: Login.ReqLoginForm) => { 23 | try { 24 | setLoading(true); 25 | loginForm.password = md5(loginForm.password); 26 | const { data } = await loginApi(loginForm); 27 | setToken(data?.access_token); 28 | setTabsList([]); 29 | message.success("登录成功!"); 30 | navigate(HOME_URL); 31 | } finally { 32 | setLoading(false); 33 | } 34 | }; 35 | 36 | const onFinishFailed = (errorInfo: any) => { 37 | console.log("Failed:", errorInfo); 38 | }; 39 | 40 | return ( 41 |
51 | 52 | } /> 53 | 54 | 55 | } /> 56 | 57 | 58 | 66 | 69 | 70 |
71 | ); 72 | }; 73 | 74 | const mapDispatchToProps = { setToken, setTabsList }; 75 | export default connect(null, mapDispatchToProps)(LoginForm); 76 | -------------------------------------------------------------------------------- /src/views/login/index.less: -------------------------------------------------------------------------------- 1 | .login-container { 2 | position: relative; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | min-width: 550px; 7 | height: 100%; 8 | min-height: 500px; 9 | background-image: url("@/assets/images/login_bg.svg"); 10 | background-position: 50%; 11 | background-size: 100% 100%; 12 | background-size: cover; 13 | .dark { 14 | position: absolute; 15 | top: 5%; 16 | right: 3.2%; 17 | } 18 | .login-box { 19 | box-sizing: border-box; 20 | display: flex; 21 | align-items: center; 22 | justify-content: space-around; 23 | width: 96%; 24 | height: 94%; 25 | padding: 0 4% 0 20px; 26 | overflow: hidden; 27 | border-radius: 10px; 28 | .login-left { 29 | width: 750px; 30 | img { 31 | width: 100%; 32 | height: 100%; 33 | } 34 | } 35 | .login-form { 36 | padding: 40px 45px 25px; 37 | border-radius: 10px; 38 | .login-logo { 39 | display: flex; 40 | align-items: center; 41 | justify-content: center; 42 | margin-bottom: 40px; 43 | .login-icon { 44 | width: 70px; 45 | } 46 | .logo-text { 47 | padding-left: 25px; 48 | font-size: 48px; 49 | font-weight: bold; 50 | white-space: nowrap; 51 | } 52 | } 53 | .ant-form-item { 54 | height: 75px; 55 | margin-bottom: 0; 56 | .ant-input-prefix { 57 | margin-right: 10px; 58 | } 59 | .ant-input-affix-wrapper-lg { 60 | padding: 8.3px 11px; 61 | } 62 | .ant-input-affix-wrapper, 63 | .ant-input-lg { 64 | font-size: 14px; 65 | } 66 | .ant-input-affix-wrapper { 67 | color: #bfbfbf; 68 | } 69 | } 70 | .login-btn { 71 | width: 100%; 72 | margin-top: 10px; 73 | white-space: nowrap; 74 | .ant-form-item-control-input-content { 75 | display: flex; 76 | justify-content: space-between; 77 | .ant-btn { 78 | width: 180px; 79 | span { 80 | font-size: 14px; 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/views/login/index.tsx: -------------------------------------------------------------------------------- 1 | import LoginForm from "./components/LoginForm"; 2 | import SwitchDark from "@/components/SwitchDark"; 3 | import loginLeft from "@/assets/images/login_left.png"; 4 | import logo from "@/assets/images/logo.png"; 5 | import "./index.less"; 6 | 7 | const Login = () => { 8 | return ( 9 |
10 | 11 |
12 |
13 | login 14 |
15 |
16 |
17 | logo 18 | Hooks-Admin 19 |
20 | 21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Login; 28 | -------------------------------------------------------------------------------- /src/views/menu/menu1/index.tsx: -------------------------------------------------------------------------------- 1 | const Menu1 = () => { 2 | return ( 3 |
4 | Menu1 🍓🍇🍈🍉 5 |
6 | ); 7 | }; 8 | 9 | export default Menu1; 10 | -------------------------------------------------------------------------------- /src/views/menu/menu2/menu21/index.tsx: -------------------------------------------------------------------------------- 1 | const Menu21 = () => { 2 | return ( 3 |
4 | Menu21 🍓🍇🍈🍉 5 |
6 | ); 7 | }; 8 | 9 | export default Menu21; 10 | -------------------------------------------------------------------------------- /src/views/menu/menu2/menu22/menu221/index.tsx: -------------------------------------------------------------------------------- 1 | const Menu221 = () => { 2 | return ( 3 |
4 | Menu221 🍓🍇🍈🍉 5 |
6 | ); 7 | }; 8 | 9 | export default Menu221; 10 | -------------------------------------------------------------------------------- /src/views/menu/menu2/menu22/menu222/index.tsx: -------------------------------------------------------------------------------- 1 | const Menu222 = () => { 2 | return ( 3 |
4 | Menu222 🍓🍇🍈🍉 5 |
6 | ); 7 | }; 8 | 9 | export default Menu222; 10 | -------------------------------------------------------------------------------- /src/views/menu/menu2/menu23/index.tsx: -------------------------------------------------------------------------------- 1 | const Menu23 = () => { 2 | return ( 3 |
4 | Menu23 🍓🍇🍈🍉 5 |
6 | ); 7 | }; 8 | 9 | export default Menu23; 10 | -------------------------------------------------------------------------------- /src/views/menu/menu3/index.tsx: -------------------------------------------------------------------------------- 1 | const Menu3 = () => { 2 | return ( 3 |
4 | Menu3 🍓🍇🍈🍉 5 |
6 | ); 7 | }; 8 | 9 | export default Menu3; 10 | -------------------------------------------------------------------------------- /src/views/proTable/useComponent/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HalseySpicy/Hooks-Admin/0f0735266b71ca70d619d6ccb47e9a988d992034/src/views/proTable/useComponent/index.less -------------------------------------------------------------------------------- /src/views/proTable/useComponent/index.tsx: -------------------------------------------------------------------------------- 1 | import "./index.less"; 2 | 3 | const UseComponent = () => { 4 | return ( 5 |
6 | UseComponent 🍓🍇🍈🍉 7 |
8 | ); 9 | }; 10 | 11 | export default UseComponent; 12 | -------------------------------------------------------------------------------- /src/views/proTable/useHooks/index.less: -------------------------------------------------------------------------------- 1 | .date, 2 | .auth { 3 | margin-bottom: 20px; 4 | } 5 | -------------------------------------------------------------------------------- /src/views/proTable/useHooks/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { Table, DatePicker, Button, Space } from "antd"; 3 | import useAuthButtons from "@/hooks/useAuthButtons"; 4 | 5 | import "./index.less"; 6 | 7 | const UseHooks = () => { 8 | // 按钮权限 9 | const { BUTTONS } = useAuthButtons(); 10 | const { RangePicker } = DatePicker; 11 | 12 | useEffect(() => { 13 | console.log(BUTTONS); 14 | }, []); 15 | 16 | const dataSource = [ 17 | { 18 | key: "1", 19 | name: "胡彦斌", 20 | age: 32, 21 | address: "西湖区湖底公园1号" 22 | }, 23 | { 24 | key: "2", 25 | name: "胡彦祖", 26 | age: 42, 27 | address: "西湖区湖底公园1号" 28 | }, 29 | { 30 | key: "3", 31 | name: "刘彦祖", 32 | age: 18, 33 | address: "西湖区湖底公园1号" 34 | }, 35 | { 36 | key: "4", 37 | name: "刘彦祖", 38 | age: 18, 39 | address: "翻斗大街翻斗花园二号楼1001室" 40 | }, 41 | { 42 | key: "5", 43 | name: "刘彦祖", 44 | age: 18, 45 | address: "翻斗大街翻斗花园二号楼1001室" 46 | } 47 | ]; 48 | 49 | const columns: any[] = [ 50 | { 51 | title: "姓名", 52 | dataIndex: "name", 53 | key: "name", 54 | align: "center" 55 | }, 56 | { 57 | title: "年龄", 58 | dataIndex: "age", 59 | key: "age", 60 | align: "center" 61 | }, 62 | { 63 | title: "住址", 64 | dataIndex: "address", 65 | key: "address", 66 | align: "center", 67 | width: "50%" 68 | } 69 | ]; 70 | return ( 71 |
72 |
73 | 切换国际化的时候看我 😎 : 74 | 75 |
76 |
77 | 78 | {BUTTONS.add && } 79 | {BUTTONS.delete && } 80 | {BUTTONS.edit && } 81 | 82 |
83 | 84 | 85 | ); 86 | }; 87 | 88 | export default UseHooks; 89 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | 11 | /* Strict Type-Checking Options */ 12 | "strict": true /* Enable all strict type-checking options. */, 13 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 14 | // "strictNullChecks": true, /* Enable strict null checks. */ 15 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 16 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 17 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 18 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 19 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 20 | 21 | "forceConsistentCasingInFileNames": true, 22 | "module": "ESNext", 23 | "moduleResolution": "Node", 24 | "resolveJsonModule": true, 25 | "isolatedModules": true, 26 | "noEmit": true, 27 | "jsx": "react-jsx", 28 | // 解析非相对模块名的基准目录 29 | "baseUrl": "./", 30 | // 模块名到基于 baseUrl的路径映射的列表。 31 | "paths": { 32 | "@": ["src"], 33 | "@/*": ["src/*"] 34 | } 35 | }, 36 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "vite.config.ts"], 37 | "exclude": ["node_modules", "dist", "**/*.js"] 38 | } 39 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv, ConfigEnv, UserConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import { resolve } from "path"; 4 | import { wrapperEnv } from "./src/utils/getEnv"; 5 | import { visualizer } from "rollup-plugin-visualizer"; 6 | import { createHtmlPlugin } from "vite-plugin-html"; 7 | import viteCompression from "vite-plugin-compression"; 8 | import eslintPlugin from "vite-plugin-eslint"; 9 | import { createSvgIconsPlugin } from "vite-plugin-svg-icons"; 10 | 11 | // @see: https://vitejs.dev/config/ 12 | export default defineConfig((mode: ConfigEnv): UserConfig => { 13 | const env = loadEnv(mode.mode, process.cwd()); 14 | const viteEnv = wrapperEnv(env); 15 | 16 | return { 17 | // base: "/", 18 | // alias config 19 | resolve: { 20 | alias: { 21 | "@": resolve(__dirname, "./src") 22 | } 23 | }, 24 | // global css 25 | css: { 26 | preprocessorOptions: { 27 | less: { 28 | // modifyVars: { 29 | // "primary-color": "#1DA57A", 30 | // }, 31 | javascriptEnabled: true, 32 | additionalData: `@import "@/styles/var.less";` 33 | } 34 | } 35 | }, 36 | // server config 37 | server: { 38 | host: "0.0.0.0", // 服务器主机名,如果允许外部访问,可设置为"0.0.0.0" 39 | port: viteEnv.VITE_PORT, 40 | open: viteEnv.VITE_OPEN, 41 | cors: true, 42 | // https: false, 43 | // 代理跨域(mock 不需要配置,这里只是个事列) 44 | proxy: { 45 | "/api": { 46 | target: "https://mock.mengxuegu.com/mock/62abda3212c1416424630a45", // easymock 47 | changeOrigin: true, 48 | rewrite: path => path.replace(/^\/api/, "") 49 | } 50 | } 51 | }, 52 | // plugins 53 | plugins: [ 54 | react(), 55 | createHtmlPlugin({ 56 | inject: { 57 | data: { 58 | title: viteEnv.VITE_GLOB_APP_TITLE 59 | } 60 | } 61 | }), 62 | // * 使用 svg 图标 63 | createSvgIconsPlugin({ 64 | iconDirs: [resolve(process.cwd(), "src/assets/icons")], 65 | symbolId: "icon-[dir]-[name]" 66 | }), 67 | // * EsLint 报错信息显示在浏览器界面上 68 | eslintPlugin(), 69 | // * 是否生成包预览 70 | viteEnv.VITE_REPORT && visualizer(), 71 | // * gzip compress 72 | viteEnv.VITE_BUILD_GZIP && 73 | viteCompression({ 74 | verbose: true, 75 | disable: false, 76 | threshold: 10240, 77 | algorithm: "gzip", 78 | ext: ".gz" 79 | }) 80 | ], 81 | esbuild: { 82 | pure: viteEnv.VITE_DROP_CONSOLE ? ["console.log", "debugger"] : [] 83 | }, 84 | // build configure 85 | build: { 86 | outDir: "dist", 87 | // esbuild 打包更快,但是不能去除 console.log,去除 console 使用 terser 模式 88 | minify: "esbuild", 89 | // minify: "terser", 90 | // terserOptions: { 91 | // compress: { 92 | // drop_console: viteEnv.VITE_DROP_CONSOLE, 93 | // drop_debugger: true 94 | // } 95 | // }, 96 | rollupOptions: { 97 | output: { 98 | // Static resource classification and packaging 99 | chunkFileNames: "assets/js/[name]-[hash].js", 100 | entryFileNames: "assets/js/[name]-[hash].js", 101 | assetFileNames: "assets/[ext]/[name]-[hash].[ext]" 102 | } 103 | } 104 | } 105 | }; 106 | }); 107 | --------------------------------------------------------------------------------