├── .eslintrc.cjs
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── README.zh_CN.md
├── package.json
├── pnpm-lock.yaml
├── public
├── favicon.ico
└── index.html
├── src
├── api
│ ├── axios-interceptors.ts
│ ├── caravan
│ │ ├── Articles.ts
│ │ ├── Labels.ts
│ │ ├── Login.ts
│ │ ├── MenuApi.ts
│ │ ├── Placard.ts
│ │ ├── Rbac.ts
│ │ └── User.ts
│ ├── microservice
│ │ ├── log.ts
│ │ ├── menu.ts
│ │ ├── role.ts
│ │ └── user.ts
│ └── request.ts
├── assets
│ ├── github
│ │ └── fork.png
│ ├── header
│ │ └── user-poster.png
│ ├── music
│ │ └── play.png
│ ├── prewview.png
│ ├── qrcode-mini.png
│ ├── qrcode.png
│ ├── react.svg
│ ├── sakura.png
│ ├── snowflake.png
│ └── user
│ │ ├── avatarbg.png
│ │ └── defaultAvatar.jpg
├── components
│ ├── AvatarData
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── D3
│ │ └── Collision
│ │ │ └── index.tsx
│ ├── Echarts
│ │ ├── AreaStackChart.tsx
│ │ ├── BarChart.tsx
│ │ ├── ChinaChart.tsx
│ │ ├── DataFlowChart.tsx
│ │ ├── HomeSenseChart.tsx
│ │ ├── LineChart.tsx
│ │ ├── LoginFrequencyChart.tsx
│ │ ├── PictorialBarChart.tsx
│ │ ├── PieBorderRadius.tsx
│ │ ├── PieChart.tsx
│ │ ├── Round.tsx
│ │ ├── ScatterChart.tsx
│ │ ├── ServerStatusChart.tsx
│ │ └── index.ts
│ ├── ErrorBoundary
│ │ └── index.tsx
│ ├── FineDay
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── Footer
│ │ └── Copyright
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ ├── Forms
│ │ ├── LoginForm
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ │ └── RegisterForm
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ ├── Header
│ │ ├── DefaultLayoutHeader
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ │ ├── HeaderNotice
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ │ └── ThemeColor
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ ├── IconFont
│ │ └── index.tsx
│ ├── LoginLog
│ │ └── index.tsx
│ ├── Menu
│ │ └── WebMenu
│ │ │ ├── hack.ts
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ ├── Modals
│ │ ├── MenuEditModal.tsx
│ │ ├── MessageModal.tsx
│ │ ├── MetaDescModal
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ │ ├── ModalMessage.tsx
│ │ ├── OperationLabelModal
│ │ │ └── index.tsx
│ │ ├── OperationMenuModal
│ │ │ └── index.tsx
│ │ ├── OperationRoleModal.tsx
│ │ ├── PlacardEditModal
│ │ │ └── index.tsx
│ │ ├── RoleAllocationMenuModal
│ │ │ └── index.tsx
│ │ ├── RoleAllocationUserModal
│ │ │ └── index.tsx
│ │ ├── UserEditModal
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── OAuth2
│ │ └── GithubAuth.tsx
│ ├── Owl
│ │ ├── close-eyes.png
│ │ ├── face.png
│ │ ├── hand-down-left.png
│ │ ├── hand-down-right.png
│ │ ├── hand-up-left.png
│ │ ├── hand-up-right.png
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── PageIntroduction
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── SocketCard
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── StarrySky
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── SuspendFallback
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── SystemInfo
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── TagsView
│ │ ├── Contextmenu
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ │ ├── MenuTag
│ │ │ ├── index.module.scss
│ │ │ └── index.tsx
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── Translatex
│ │ └── index.tsx
│ ├── TypingCard
│ │ ├── Typing.tsx
│ │ └── index.tsx
│ ├── UserCard
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── UserInfoCard
│ │ ├── index.module.scss
│ │ └── index.tsx
│ └── Weaterfall
│ │ └── index.tsx
├── contexts
│ ├── MenuTagContext.tsx
│ └── ProjectContext.tsx
├── hooks
│ └── useCalculativeWidth.tsx
├── layout
│ ├── DefaultLayout
│ │ ├── index.module.scss
│ │ └── index.tsx
│ └── ProjectLayout
│ │ └── index.tsx
├── main.tsx
├── pages
│ ├── About
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── Account
│ │ ├── AccountBind
│ │ │ ├── AccountBind.tsx
│ │ │ └── index.module.scss
│ │ ├── AccountPage
│ │ │ └── accountpage.tsx
│ │ ├── AccountSetting
│ │ │ ├── AccountSetting.tsx
│ │ │ └── index.module.scss
│ │ ├── Base
│ │ │ ├── BaseSetting.tsx
│ │ │ ├── MetaDesc.tsx
│ │ │ ├── basefrom.tsx
│ │ │ └── style.scss
│ │ ├── Messagecenter
│ │ │ ├── MessageCenter.tsx
│ │ │ └── style.scss
│ │ ├── Notification
│ │ │ ├── notification.tsx
│ │ │ └── style.scss
│ │ ├── SecuritySetting
│ │ │ └── SecuritySetting.tsx
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── AnimateCard
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── BMap
│ │ └── index.tsx
│ ├── Chart
│ │ ├── AreaChart
│ │ │ └── index.tsx
│ │ └── EchartsPie
│ │ │ └── index.tsx
│ ├── D3Example
│ │ └── index.tsx
│ ├── FailPage
│ │ ├── FailContainer.tsx
│ │ ├── NeedLogin.tsx
│ │ ├── NotAuthPage.tsx
│ │ └── NotFoundPage.tsx
│ ├── LabelsAdmin
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── Login
│ │ ├── index.module.scss
│ │ ├── index.tsx
│ │ └── snowflake.tsx
│ ├── MenuAdmin
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── MusicChart
│ │ ├── index.module.scss
│ │ ├── index.tsx
│ │ └── musicItem.tsx
│ ├── PlacardAdmin
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── RoleAdmin
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── Root
│ │ ├── index.module.scss
│ │ ├── index.scss
│ │ └── index.tsx
│ ├── UserAdmin
│ │ ├── components
│ │ │ └── Table.tsx
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── UserProfile
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── WebsocketAdmin
│ │ ├── index.module.scss
│ │ └── index.tsx
│ ├── Workbench
│ │ ├── index.module.scss
│ │ └── index.tsx
│ └── Workplace
│ │ ├── index.module.scss
│ │ └── index.tsx
├── router
│ ├── index.tsx
│ └── router.tsx
├── styles
│ ├── _themeMixin.scss
│ ├── _themeVariable.scss
│ └── reset.css
├── utils
│ ├── Sakura.ts
│ ├── core.ts
│ ├── file.ts
│ ├── menuUtils.ts
│ ├── socketDispatch.ts
│ └── ws.ts
└── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── vite.config.ts.timestamp-1734841201844-9b7e5a462699e.mjs
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"],
5 | ignorePatterns: ["dist", ".eslintrc.cjs"],
6 | parser: "@typescript-eslint/parser",
7 | plugins: ["react-refresh", "unused-imports"],
8 | rules: {
9 | "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
10 | "no-unused-vars": "off",
11 | "unused-imports/no-unused-imports": "error",
12 | "unused-imports/no-unused-vars": [
13 | "error",
14 | {
15 | vars: "all",
16 | varsIgnorePattern: "^_",
17 | args: "after-used",
18 | argsIgnorePattern: "^_"
19 | }
20 | ],
21 | "@typescript-eslint/no-unused-vars": [1],
22 | "@typescript-eslint/no-explicit-any": "off",
23 | "max-len": [0, 200]
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/.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 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | analysis-chart.html
26 | package-lock.json
27 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 170,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "trailingComma": "none",
7 | "bracketSpacing": true,
8 | "bracketSameLine": true,
9 | "arrowParens": "avoid",
10 | "endOfLine": "lf",
11 | "singleAttributePerLine": false
12 | }
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 韦钦可
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | English | [简体中文](./README.zh_CN.md)
2 |
3 |
Nest Admin
4 |
5 | - 预览: https://nest-admin.com
6 | - Github: https://github.com/weiqinke/react-admin-nest
7 | - 欢迎 fork star
8 |
9 | ## 📦 Install
10 |
11 | ```bash
12 | $ git clone https://gitee.com/weiqinke/react-admin-nest.git
13 | $ cd react-admin-nest
14 | $ npm install
15 | $ npm dev
16 | ```
17 |
18 | ## 🔨 Build
19 |
20 | ```bash
21 | npm install
22 | npm run build
23 | ```
24 |
25 | - :white_check_mark:**通用登录**:完成
26 | - :white_check_mark:browserHistory
27 | - :white_check_mark:国际化组件 I18N
28 | - :white_check_mark:完善 axios 拦截器
29 | - :white_check_mark:菜单支持 layoutfix 布局
30 | - :white_check_mark:没有登录时,默认跳转到首页
31 | - :white_check_mark:展开菜单仅支持展开两个大类或者仅展开一类菜单
32 | - :white_check_mark:通用切换菜单,右键菜单在点击别的元素时,能自动隐藏:完成
33 | - [x] 持续开发
34 | - [ ] 在 nginx 上适配 browserHistory
35 |
--------------------------------------------------------------------------------
/README.zh_CN.md:
--------------------------------------------------------------------------------
1 | [English](./README.md) | 简体中文
2 |
3 | Nest Admin
4 |
5 | - 预览: https://nest-admin.com
6 | - Github: https://github.com/weiqinke/react-admin-nest
7 | - 欢迎 fork star
8 |
9 | ## 📦 Install
10 |
11 | ```bash
12 | $ git clone https://gitee.com/weiqinke/react-admin-nest.git
13 | $ cd react-admin-nest
14 | $ npm install
15 | $ npm dev
16 | ```
17 |
18 | ## 🔨 Build
19 |
20 | ```bash
21 | npm install
22 | npm run build
23 | ```
24 |
25 | - :white_check_mark:**通用登录**:完成
26 | - :white_check_mark:browserHistory
27 | - :white_check_mark:国际化组件 I18N
28 | - :white_check_mark:完善 axios 拦截器
29 | - :white_check_mark:菜单支持 layoutfix 布局
30 | - :white_check_mark:没有登录时,默认跳转到首页
31 | - :white_check_mark:展开菜单仅支持展开两个大类或者仅展开一类菜单
32 | - :white_check_mark:通用切换菜单,右键菜单在点击别的元素时,能自动隐藏:完成
33 | - [x] 持续开发
34 | - [ ] 在 nginx 上适配 browserHistory
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nest-admin-web",
3 | "version": "2.0.0",
4 | "description": "",
5 | "author": "qkstart@foxmail.com",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vite --host",
9 | "build": "vite build",
10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
11 | "preview": "vite preview",
12 | "lint:fix": "eslint . --fix",
13 | "prettier": "prettier --write .",
14 | "test": "vitest"
15 | },
16 | "dependencies": {
17 | "@ant-design/icons": "^5.2.6",
18 | "@react-spring/web": "^9.7.3",
19 | "animate.css": "^4.1.1",
20 | "antd": "^5.13.1",
21 | "axios": "^1.6.5",
22 | "bytes": "^3.1.2",
23 | "captcha-mini": "^1.1.0",
24 | "d3": "^7.8.5",
25 | "dayjs": "^1.11.10",
26 | "echarts": "^5.4.3",
27 | "echarts-wordcloud": "^2.1.0",
28 | "html2canvas": "^1.4.1",
29 | "lodash-es": "^4.17.21",
30 | "qs": "^6.11.2",
31 | "react": "^18.2.0",
32 | "react-bmap": "^1.0.131",
33 | "react-card-flip": "^1.2.2",
34 | "react-countup": "^6.5.0",
35 | "react-dom": "^18.2.0",
36 | "react-router-dom": "^6.21.2",
37 | "react-transition-group": "^4.4.5",
38 | "socket.io-client": "^4.7.4"
39 | },
40 | "devDependencies": {
41 | "@types/bytes": "^3.1.4",
42 | "@types/d3": "^7.4.3",
43 | "@types/node": "^20.11.2",
44 | "@types/qs": "^6.9.11",
45 | "@types/react": "^18.2.43",
46 | "@types/react-dom": "^18.2.17",
47 | "@types/react-transition-group": "^4.4.10",
48 | "@typescript-eslint/eslint-plugin": "^6.19.0",
49 | "@typescript-eslint/parser": "^6.14.0",
50 | "@vitejs/plugin-react": "^4.2.1",
51 | "eslint": "^8.56.0",
52 | "eslint-plugin-react-hooks": "^4.6.0",
53 | "eslint-plugin-react-refresh": "^0.4.5",
54 | "eslint-plugin-unused-imports": "^3.0.0",
55 | "prettier": "3.2.4",
56 | "rollup-plugin-visualizer": "^5.12.0",
57 | "sass": "^1.69.7",
58 | "terser": "^5.37.0",
59 | "typescript": "^5.7.2",
60 | "vite": "^5.4.11",
61 | "vite-plugin-compression": "^0.5.1",
62 | "vite-plugin-html": "^3.2.2"
63 | },
64 | "license": "MIT"
65 | }
66 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%- title %>
8 |
9 |
10 |
11 | <%- injectScript %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/api/axios-interceptors.ts:
--------------------------------------------------------------------------------
1 | import { message } from "antd";
2 |
3 | // 401拦截
4 | const resp401 = {
5 | /**
6 | * 响应数据之前做点什么
7 | * @param response 响应对象
8 | * @param options 应用配置 包含: {router, i18n, store, message}
9 | * @returns {*}
10 | */
11 | onFulfilled(response) {
12 | if (response.status === 400) {
13 | message.error({
14 | content: "无此接口权限",
15 | duration: 1
16 | });
17 | }
18 | if (response.status === 401) {
19 | message.error({
20 | content: "无此接口权限",
21 | duration: 1
22 | });
23 | }
24 | return response;
25 | }
26 | };
27 |
28 | const resp403 = {
29 | onFulfilled(response) {
30 | if (response.status === 403) {
31 | message.error({
32 | content: "请求被拒绝",
33 | duration: 1
34 | });
35 | }
36 | return response;
37 | }
38 | };
39 |
40 | const resp404 = {
41 | onFulfilled(response) {
42 | if (response.status === 404) {
43 | message.error({
44 | content: "请求找不到",
45 | duration: 1
46 | });
47 | }
48 | return response;
49 | }
50 | };
51 |
52 | const resp415 = {
53 | onFulfilled(response) {
54 | if (response.status === 415) {
55 | message.error({
56 | content: "请求方式不受支持",
57 | duration: 1
58 | });
59 | }
60 | return response;
61 | }
62 | };
63 |
64 | const resp20401 = {
65 | onFulfilled(response) {
66 | if (response.status === 200 && response.data.code === 20401) {
67 | // 为了处理返回数据为空,hash
68 | // $message.error({
69 | // content: '获取到0条数据',
70 | // duration: 1
71 | // });
72 | }
73 | if (response.status === 200 && response.data.code === 41701) {
74 | message.error({
75 | content: "查询参数错误",
76 | duration: 1
77 | });
78 | }
79 | if (response.data.code !== 200 && response.data.message) {
80 | message.error({
81 | content: `${response.data.message}`,
82 | duration: 1
83 | });
84 | }
85 | return response;
86 | }
87 | };
88 |
89 | const resperror = {
90 | onRejected(error) {
91 | message.error({
92 | content: "请求错误",
93 | duration: 1
94 | });
95 | return Promise.reject(error);
96 | }
97 | };
98 |
99 | const reqCommon = {
100 | /**
101 | * 发送请求之前做些什么
102 | * @param config axios config
103 | * @returns {*}
104 | */
105 | onFulfilled(config) {
106 | // if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) {
107 | // message.warning('认证 token 已过期,请重新登录')
108 | // }
109 | const token = window.localStorage.getItem("token") || "";
110 | if (token) {
111 | //将token放到请求头发送给服务器,将tokenkey放在请求头中
112 | // 后端用请求头 Authorization 来获取token 所以必须放在请求头里面,这种需要前后台协商
113 | config.headers["Authorization"] = "Bearer " + token;
114 | return config;
115 | }
116 | return config;
117 | },
118 | /**
119 | * 请求出错时做点什么
120 | * @param error 错误对象
121 | * @returns {Promise}
122 | */
123 | onRejected(error) {
124 | message.error({
125 | content: error.message,
126 | duration: 1
127 | });
128 | return Promise.reject(error);
129 | }
130 | };
131 |
132 | export const requestInterce = [reqCommon]; // 请求拦截
133 |
134 | export const responseInterce = [resp401, resp403, resp404, resp415, resp20401, resperror]; // 响应拦截
135 |
--------------------------------------------------------------------------------
/src/api/caravan/Articles.ts:
--------------------------------------------------------------------------------
1 | import { request } from "@/api/request";
2 | const BASE_URL = REACT_APP_API_URL;
3 |
4 | /**
5 | * 获取当前用户所有文章
6 | * @param payload
7 | * @returns
8 | */
9 |
10 | export const articles = (payload: any) => {
11 | return request(`${BASE_URL}articles`, "get", payload);
12 | };
13 |
14 | export const findallarticle = (payload: any) => {
15 | return request(`${BASE_URL}articles/findallarticle`, "get", payload);
16 | };
17 | /**获取一个文章主体 */
18 | export const onearticlebody = (payload: any) => {
19 | return request(`${BASE_URL}articles/onearticlebody`, "get", payload);
20 | };
21 | export const onearticle = (payload: any) => {
22 | return request(`${BASE_URL}articles/onearticle`, "post", payload);
23 | };
24 |
--------------------------------------------------------------------------------
/src/api/caravan/Labels.ts:
--------------------------------------------------------------------------------
1 | import { request } from "@/api/request";
2 | const BASE_URL = REACT_APP_API_URL;
3 | /**
4 | * 获取当前用户所有标签
5 | * @param payload
6 | * @returns
7 | */
8 | export const getalllabelsbyuser = (payload: any) => {
9 | return request(`${BASE_URL}labels/getalllabelsbyuser`, "GET", payload);
10 | };
11 |
12 | //添加
13 | export const addnewlabelbyuser = (payload: any) => {
14 | return request(`${BASE_URL}labels/addnewlabelbyuser`, "POST", payload);
15 | };
16 |
17 | //更新
18 | export const putlabelbyuser = (payload: any) => {
19 | return request(`${BASE_URL}labels/putlabelbyuser`, "PUT", payload);
20 | };
21 |
22 | //删除
23 | export const deletelabelbyuser = (payload: any) => {
24 | return request(`${BASE_URL}labels/addnewlabelbyuser`, "DELETE", payload);
25 | };
26 |
--------------------------------------------------------------------------------
/src/api/caravan/Login.ts:
--------------------------------------------------------------------------------
1 | import { request } from "@/api/request";
2 |
3 | const BASE_URL: string = REACT_APP_API_URL;
4 |
5 | /** 获取菜单列表接口 */
6 | export const getMenuList = payload => {
7 | return request(`${BASE_URL}user/menu`, "GET", payload);
8 | };
9 | /** 获取通知列表接口 */
10 | export const getNoticeList = payload => {
11 | return request(`${BASE_URL}`, "GET", payload);
12 | };
13 | /** 获取通知列表接口 */
14 | export const getNoticeList2 = payload => {
15 | return request(`${BASE_URL}`, "GET", payload);
16 | };
17 |
18 | /** OAuth2 获取code */
19 | export const userTokenByOAuth2Code = payload => {
20 | return request(`${BASE_URL}user/userTokenByOAuth2Code`, "GET", payload);
21 | };
22 |
23 | /** 登录接口 */
24 | export const apiLogin = payload => {
25 | return request(`${BASE_URL}user/login`, "POST", payload);
26 | };
27 | /** nest登录接口 */
28 | export const accountlogin = payload => {
29 | return request(`${BASE_URL}user/accountlogin`, "POST", payload);
30 | };
31 |
32 | /** 获取菜单接口 */
33 | export const getUserMenus = payload => {
34 | return request(`${BASE_URL}menus/getUserMenus`, "get", payload);
35 | };
36 |
37 | /** nest获取菜单接口 */
38 | export const GetMenuByToken = payload => {
39 | return request(`${BASE_URL}user/getmenubytoken`, "GET", payload);
40 | };
41 |
42 | /** 登出接口 */
43 | export const apiLogout = payload => {
44 | return request(`${BASE_URL}user/logout`, "POST", payload);
45 | };
46 |
47 | /** 登录接口 */
48 | export const GetPlanPaged = payload => {
49 | return request(`${BASE_URL}PlanManage/GetPlanPaged`, "POST", payload);
50 | };
51 |
52 | /** 注册接口 */
53 | export const Zhuce = payload => {
54 | return request(`${BASE_URL}user/zhuce`, "POST", payload);
55 | };
56 |
57 | /** 获取所有人员 */
58 | export const findalluser = payload => {
59 | return request(`${BASE_URL}user/findalluser`, "POST", payload);
60 | };
61 |
62 | /** 添加一个人员 */
63 | export const addOneUser = payload => {
64 | return request(`${BASE_URL}user/addOneUser`, "POST", payload);
65 | };
66 |
67 | /** 编辑一个人员 */
68 | export const editOneUser = payload => {
69 | return request(`${BASE_URL}user/editOneUser`, "POST", payload);
70 | };
71 |
72 | /** 切换一个人员状态 */
73 | export const changeUserStatus = payload => {
74 | return request(`${BASE_URL}user/changeUserStatus`, "POST", payload);
75 | };
76 |
77 | /**
78 | * 获取登录日志
79 | * */
80 | export const getAccountLog = payload => {
81 | return request(`${BASE_URL}user/getAccountLog`, "POST", payload);
82 | };
83 | /**
84 | * 获取我自己的信息
85 | * */
86 | export const getmyuserinfo = () => {
87 | return request(`${BASE_URL}user/getmyuserinfo`, "POST", {});
88 | };
89 |
90 | /**
91 | * 更新我自己的信息
92 | * */
93 | export const updateMyinfo = payload => {
94 | return request(`${BASE_URL}user/updateMyinfo`, "POST", payload);
95 | };
96 |
97 | /**
98 | * 更新我自己的信息
99 | * */
100 | export const updateUserAvatarUrl = payload => {
101 | return request(`${BASE_URL}user/updateUserAvatarUrl`, "POST", payload, {
102 | processData: false
103 | });
104 | };
105 |
106 | /***
107 | * 获取指定时间段的登录日志
108 | * **/
109 | export const findAccountLogs = (payload: any) => {
110 | return request(`${BASE_URL}Accountlog/findAccountLogs`, "GET", payload);
111 | };
112 |
--------------------------------------------------------------------------------
/src/api/caravan/MenuApi.ts:
--------------------------------------------------------------------------------
1 | import { request } from "@/api/request";
2 |
3 | const BASE_URL = REACT_APP_API_URL;
4 |
5 | /** 获取菜单接口 */
6 | export const getUserMenus = () => {
7 | return request(`${BASE_URL}menus/getUserMenus`, "GET", {});
8 | };
9 |
10 | /** 添加菜单接口 */
11 | export const addMenuItem = (payload: any) => {
12 | return request(`${BASE_URL}menus/addMenuItem`, "POST", payload);
13 | };
14 |
15 | /** 编辑菜单接口 */
16 | export const editMenuItem = (payload: any) => {
17 | return request(`${BASE_URL}menus/editMenuItem`, "POST", payload);
18 | };
19 |
20 | /** 删除菜单接口 */
21 | export const delMenuItem = (payload: any) => {
22 | return request(`${BASE_URL}menus`, "DELETE", payload);
23 | };
24 |
25 | /** 删除菜单接口 */
26 | export const getAllMenus = payload => {
27 | return request(`${BASE_URL}menus/getAllMenus`, "GET", payload);
28 | };
29 |
--------------------------------------------------------------------------------
/src/api/caravan/Placard.ts:
--------------------------------------------------------------------------------
1 | import { request } from "@/api/request";
2 |
3 | const BASE_URL = REACT_APP_API_URL;
4 |
5 | /** 获取系统通知接口 */
6 | export const findLastTypePlacard = (payload: any) => {
7 | return request(`${BASE_URL}placard/findLastTypePlacard`, "GET", payload);
8 | };
9 |
10 | /** 获取系统通知列表接口 */
11 | export const getPlacardListByType = (payload: any) => {
12 | return request(`${BASE_URL}placard/getPlacardListByType`, "GET", payload);
13 | };
14 |
15 | /** 获取所有公告和通知 */
16 | export const getAllPlacard = (payload: any) => {
17 | return request(`${BASE_URL}placard/getAllPlacard`, "GET", payload);
18 | };
19 |
20 | /** 添加一个公告 */
21 | export const addPlacard = (payload: any) => {
22 | return request(`${BASE_URL}placard/addPlacard`, "POST", payload);
23 | };
24 | /** 发布一个公告 */
25 | export const broadcastPlacard = (payload: any) => {
26 | return request(`${BASE_URL}placard/broadcastPlacard`, "POST", payload);
27 | };
28 |
29 | /** 已读一个公告 */
30 | export const readOnePlacard = (payload: any) => {
31 | return request(`${BASE_URL}placard/readOnePlacard`, "PUT", payload);
32 | };
33 |
34 | /** 删除一个公告 */
35 | export const deleteOnePlacard = (payload: any) => {
36 | return request(`${BASE_URL}placard`, "DELETE", payload);
37 | };
38 |
--------------------------------------------------------------------------------
/src/api/caravan/Rbac.ts:
--------------------------------------------------------------------------------
1 | import { request } from "@/api/request";
2 |
3 | const BASE_URL = REACT_APP_API_URL;
4 |
5 | /** nest获取所有角色接口 */
6 | export const getallrole = (payload: any) => {
7 | return request(`${BASE_URL}role/getallrole`, "GET", payload);
8 | };
9 |
10 | /** nest添加角色接口 */
11 | export const addrole = (payload: any) => {
12 | return request(`${BASE_URL}role/addrole`, "POST", payload);
13 | };
14 |
15 | /** nest更新角色接口 */
16 | export const updaterole = (payload: any) => {
17 | return request(`${BASE_URL}role/updaterole`, "POST", payload);
18 | };
19 |
20 | /** nest禁用角色接口 */
21 | export const deleterole = (payload: any) => {
22 | return request(`${BASE_URL}role/deleterole`, "POST", payload);
23 | };
24 |
25 | /** nest角色接口 */
26 | export const getUsersByRoleCode = (payload: any) => {
27 | return request(`${BASE_URL}role/getUsersByRoleCode`, "POST", payload);
28 | };
29 |
30 | /** nest角色接口 */
31 | export const getMenusByRoleCode = (payload: any) => {
32 | return request(`${BASE_URL}role/getMenusByRoleCode`, "POST", payload);
33 | };
34 |
35 | /** nest角色分配人接口 */
36 | export const giveUser = (payload: any) => {
37 | return request(`${BASE_URL}role/giveUser`, "POST", payload);
38 | };
39 |
40 | /** nest角色分配人接口 */
41 | export const giveRoleMenus = (payload: any) => {
42 | return request(`${BASE_URL}role/giveRoleMenus`, "POST", payload);
43 | };
44 |
45 | /** nest角色分配人接口 */
46 | export const findAllMenu = (payload: any) => {
47 | return request(`${BASE_URL}role/findAllMenu`, "POST", payload);
48 | };
49 |
50 | /** nest角色分配人接口 */
51 | export const roleMenu = (payload: any) => {
52 | return request(`${BASE_URL}role/menu`, "GET", payload);
53 | };
54 |
--------------------------------------------------------------------------------
/src/api/caravan/User.ts:
--------------------------------------------------------------------------------
1 | import { request } from "@/api/request";
2 |
3 | const BASE_URL = REACT_APP_API_URL;
4 |
5 | /** 获取菜单列表接口 */
6 | export const getMenuList = (payload: any) => {
7 | return request(`${BASE_URL}user/menu`, "GET", payload);
8 | };
9 | /** 获取通知列表接口 */
10 | export const getNoticeList = (payload: any) => {
11 | return request(`${BASE_URL}`, "GET", payload);
12 | };
13 | /** 获取通知列表接口 */
14 | export const getNoticeList2 = (payload: any) => {
15 | return request(`${BASE_URL}`, "GET", payload);
16 | };
17 |
18 | /** 登录接口 */
19 | export const apiLogin = (payload: any) => {
20 | return request(`${BASE_URL}user/login`, "POST", payload);
21 | };
22 | /** nest登录接口 */
23 | export const accountlogin = (payload: any) => {
24 | return request(`${BASE_URL}user/accountlogin`, "POST", payload);
25 | };
26 |
27 | /** nest注册接口 */
28 | export const userRegister = (payload: any) => {
29 | return request(`${BASE_URL}user/register`, "POST", payload);
30 | };
31 |
32 | /** nest用户菜单接口 */
33 | export const userMenus = (payload: any) => {
34 | return request(`${BASE_URL}user/menus`, "GET", payload);
35 | };
36 |
37 | /** nest获取菜单接口 */
38 | export const GetMenuByToken = (payload: any) => {
39 | return request(`${BASE_URL}user/getmenubytoken`, "GET", payload);
40 | };
41 |
42 | /** 登出接口 */
43 | export const apiLogout = (payload: any) => {
44 | return request(`${BASE_URL}user/logout`, "POST", payload);
45 | };
46 |
47 | /** 登录接口 */
48 | export const GetPlanPaged = (payload: any) => {
49 | return request(`${BASE_URL}PlanManage/GetPlanPaged`, "POST", payload);
50 | };
51 |
52 | /** 注册接口 */
53 | export const Zhuce = (payload: any) => {
54 | return request(`${BASE_URL}user/zhuce`, "POST", payload);
55 | };
56 |
57 | /** 获取所有人员 */
58 | export const findalluser = (payload: any) => {
59 | return request(`${BASE_URL}user/findalluser`, "POST", payload);
60 | };
61 |
62 | /** 添加一个人员 */
63 | export const addOneUser = (payload: any) => {
64 | return request(`${BASE_URL}user/addOneUser`, "POST", payload);
65 | };
66 |
67 | /** 编辑一个人员 */
68 | export const editOneUser = (payload: any) => {
69 | return request(`${BASE_URL}user/editOneUser`, "POST", payload);
70 | };
71 |
72 | /** 切换一个人员状态 */
73 | export const changeUserStatus = (payload: any) => {
74 | return request(`${BASE_URL}user/changeUserStatus`, "POST", payload);
75 | };
76 |
77 | /** 获取登录日志 */
78 | export const getAccountLog = (payload: any) => {
79 | return request(`${BASE_URL}user/getAccountLog`, "POST", payload);
80 | };
81 | /** 获取我自己的信息 */
82 | export const getmyuserinfo = () => {
83 | return request(`${BASE_URL}user/getmyuserinfo`, "GET", {});
84 | };
85 |
86 | /** 获取一个用户的信息 */
87 | export const getUserInfoByUid = uid => {
88 | return request(`${BASE_URL}user`, "GET", uid);
89 | };
90 |
91 | /** 更新我自己的信息 */
92 | export const updateMyinfo = (payload: any) => {
93 | return request(`${BASE_URL}user/updateMyinfo`, "POST", payload);
94 | };
95 |
96 | /** 更新我自己的信息 */
97 | export const updateUserAvatarUrl = (payload: any) => {
98 | return request(`${BASE_URL}user/updateUserAvatarUrl`, "POST", payload, {
99 | processData: false
100 | });
101 | };
102 |
103 | /** 获取消息接口 */
104 | export const getallnotices = () => {
105 | return request(`${BASE_URL}notices/getallnotices`, "get", {});
106 | };
107 | /** 发送消息接口 */
108 | export const sendToOne = (payload: any) => {
109 | return request(`${BASE_URL}notices/sendToOne`, "post", payload);
110 | };
111 | /** 获取我的消息 */
112 | export const getUidNotices = () => {
113 | return request(`${BASE_URL}notices/getUidNotices`, "get", {});
114 | };
115 |
--------------------------------------------------------------------------------
/src/api/microservice/log.ts:
--------------------------------------------------------------------------------
1 | import { request } from "@/api/request";
2 |
3 | const BASE_URL = REACT_APP_API_URL;
4 |
5 | /** 获取用户登录日志 */
6 | export const loginlog = (payload: any) => {
7 | return request(`${BASE_URL}logs/loginlog`, "GET", payload);
8 | };
9 |
10 | /** 获取用户消息 */
11 | export const userNotice = (payload: any) => {
12 | return request(`${BASE_URL}logs/userNotice`, "GET", payload);
13 | };
14 |
15 | /** 获取用户未读通知 */
16 | export const userPlacard = (payload: any) => {
17 | return request(`${BASE_URL}logs/placard/user`, "GET", payload);
18 | };
19 |
20 | /** 获取所有通知 */
21 | export const getPlacard = (payload: any) => {
22 | return request(`${BASE_URL}logs/placard`, "GET", payload);
23 | };
24 |
25 | /** 创建通知 */
26 | export const createPlacard = (payload: any) => {
27 | return request(`${BASE_URL}logs/placard/create`, "POST", payload);
28 | };
29 |
30 | /** 更新通知 */
31 | export const updatePlacard = (payload: any) => {
32 | return request(`${BASE_URL}logs/placard/update`, "POST", payload);
33 | };
34 |
35 | /** 推送通知 */
36 | export const pushPlacard = (payload: any) => {
37 | return request(`${BASE_URL}logs/placard/push`, "POST", payload);
38 | };
39 |
40 | /** 推送通知 */
41 | export const submitPlacard = (payload: any) => {
42 | return request(`${BASE_URL}logs/placard/submit`, "POST", payload);
43 | };
44 |
45 | /** 接口调用数量 */
46 | export const apiCount = (payload: any) => {
47 | return request(`${BASE_URL}logs/apicount`, "GET", payload);
48 | };
49 |
--------------------------------------------------------------------------------
/src/api/microservice/menu.ts:
--------------------------------------------------------------------------------
1 | import { request } from "@/api/request";
2 |
3 | const BASE_URL = REACT_APP_API_URL;
4 |
5 | /** 获取所有菜单 */
6 | export const menuFind = (payload: any) => {
7 | return request(`${BASE_URL}menu/find`, "GET", payload);
8 | };
9 |
10 | /** 添加菜单 */
11 | export const createMenuItem = (payload: any) => {
12 | return request(`${BASE_URL}menu/create`, "POST", payload);
13 | };
14 |
15 | /**修改菜单属性 */
16 | export const updateMenuItem = (payload: any) => {
17 | return request(`${BASE_URL}menu/update`, "POST", payload);
18 | };
19 |
--------------------------------------------------------------------------------
/src/api/microservice/role.ts:
--------------------------------------------------------------------------------
1 | import { request } from "@/api/request";
2 |
3 | const BASE_URL = REACT_APP_API_URL;
4 |
5 | /** 获取所有角色 */
6 | export const findRoles = (payload: any) => {
7 | return request(`${BASE_URL}role/findRoles`, "GET", payload);
8 | };
9 |
10 | /** 创建角色 */
11 | export const createRole = payload => {
12 | return request(`${BASE_URL}role/create`, "POST", payload);
13 | };
14 |
15 | /** 更新角色 */
16 | export const updateRole = payload => {
17 | return request(`${BASE_URL}role/updateRole`, "POST", payload);
18 | };
19 |
20 | /** 获取角色下的人员 */
21 | export const findRoleUsers = payload => {
22 | return request(`${BASE_URL}role/findRoleUsers`, "GET", payload);
23 | };
24 |
25 | /** 更新角色下的人员 */
26 | export const updateRoleUsers = payload => {
27 | return request(`${BASE_URL}role/user/update`, "POST", payload);
28 | };
29 |
30 | /** 获取角色下的菜单 */
31 | export const findRoleMenus = payload => {
32 | return request(`${BASE_URL}role/menu`, "GET", payload);
33 | };
34 |
35 | /** 更新角色下的菜单 */
36 | export const updateRoleMenus = payload => {
37 | return request(`${BASE_URL}role/menu/update`, "POST", payload);
38 | };
39 |
--------------------------------------------------------------------------------
/src/api/microservice/user.ts:
--------------------------------------------------------------------------------
1 | import { request } from "@/api/request";
2 |
3 | const BASE_URL = REACT_APP_API_URL;
4 |
5 | /** 用户登录 */
6 | export const login = (payload: any) => {
7 | return request(`${BASE_URL}user/login`, "POST", payload);
8 | };
9 |
10 | /** 用户注册 */
11 | export const register = (payload: any) => {
12 | return request(`${BASE_URL}user/register`, "POST", payload);
13 | };
14 |
15 | /** nest用户菜单接口 */
16 | export const userMenus = (payload: any) => {
17 | return request(`${BASE_URL}user/menus`, "GET", payload);
18 | };
19 |
20 | /** 创建一个用户 */
21 | export const createUser = payload => {
22 | return request(`${BASE_URL}user/create`, "POST", payload);
23 | };
24 |
25 | /** 获取所有用户 */
26 | export const findUsers = payload => {
27 | return request(`${BASE_URL}user/findall`, "GET", payload);
28 | };
29 |
30 | /** 更新用户 */
31 | export const updateUser = (payload: any) => {
32 | return request(`${BASE_URL}user/update`, "POST", payload);
33 | };
34 |
35 | /** 获取用户信息 */
36 | export const userinfo = (payload: any) => {
37 | return request(`${BASE_URL}user/userinfo`, "GET", payload);
38 | };
39 |
40 | /** OAuth2 获取code */
41 | export const userOAuth2 = payload => {
42 | return request(`${BASE_URL}user/oauth2`, "POST", payload);
43 | };
44 |
--------------------------------------------------------------------------------
/src/api/request.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { requestInterce, responseInterce } from "./axios-interceptors";
3 | // 跨域认证信息 header 名
4 | const xsrfHeaderName = "Authorization";
5 | const service = axios.create();
6 | /**接口最大响应时间** */
7 | service.defaults.timeout = 20000;
8 | // axios.defaults.withCredentials= true
9 | service.defaults.withCredentials = false;
10 | service.defaults.xsrfHeaderName = xsrfHeaderName;
11 | service.defaults.xsrfCookieName = xsrfHeaderName;
12 | // 加载请求拦截器
13 | requestInterce.forEach(item => {
14 | let { onFulfilled, onRejected } = item;
15 | if (!onFulfilled || typeof onFulfilled !== "function") {
16 | onFulfilled = config => config;
17 | }
18 | if (!onRejected || typeof onRejected !== "function") {
19 | onRejected = error => Promise.reject(error);
20 | }
21 | service.interceptors.request.use(
22 | config => onFulfilled(config),
23 | error => onRejected(error)
24 | );
25 | });
26 | // 加载响应拦截器
27 | responseInterce.forEach((item: any) => {
28 | let { onFulfilled, onRejected } = item;
29 | if (!onFulfilled || typeof onFulfilled !== "function") {
30 | onFulfilled = response => response;
31 | }
32 | if (!onRejected || typeof onRejected !== "function") {
33 | onRejected = error => Promise.reject(error);
34 | }
35 | service.interceptors.response.use(
36 | response => onFulfilled(response),
37 | error => onRejected(error)
38 | );
39 | });
40 |
41 | /**
42 | * axios请求
43 | * @param url 请求地址
44 | * @param method {string} http method
45 | * @param params 请求参数
46 | * @returns {Promise>}
47 | */
48 | export const request = async (url, method, params = {}, options = {}) => {
49 | switch (method) {
50 | case "GET":
51 | return service.get(url, { params, ...options });
52 | case "POST":
53 | return service.post(url, params, { ...options });
54 | case "DELETE":
55 | return service.delete(url, { params });
56 | case "PUT":
57 | return service.put(url, params, options);
58 | case "PATCH":
59 | return service.patch(url, params, options);
60 | default:
61 | return service.get(url, { params, ...options });
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/src/assets/github/fork.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/assets/github/fork.png
--------------------------------------------------------------------------------
/src/assets/header/user-poster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/assets/header/user-poster.png
--------------------------------------------------------------------------------
/src/assets/music/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/assets/music/play.png
--------------------------------------------------------------------------------
/src/assets/prewview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/assets/prewview.png
--------------------------------------------------------------------------------
/src/assets/qrcode-mini.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/assets/qrcode-mini.png
--------------------------------------------------------------------------------
/src/assets/qrcode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/assets/qrcode.png
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/sakura.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/assets/sakura.png
--------------------------------------------------------------------------------
/src/assets/snowflake.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/assets/snowflake.png
--------------------------------------------------------------------------------
/src/assets/user/avatarbg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/assets/user/avatarbg.png
--------------------------------------------------------------------------------
/src/assets/user/defaultAvatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/assets/user/defaultAvatar.jpg
--------------------------------------------------------------------------------
/src/components/AvatarData/index.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | position: absolute;
3 | bottom: -20px;
4 | left: 24px;
5 | z-index: 9;
6 | height: 130px;
7 | user-select: none;
8 | cursor: pointer;
9 | }
10 |
11 | .userAvatar {
12 | float: left;
13 | transition: 0s linear;
14 | transform:rotate(0deg);
15 | &:hover{
16 | transition: 30s linear;
17 | transform:rotate(3600deg);
18 | }
19 | }
20 |
21 | .info {
22 | float: left;
23 | margin: 42px 0 0 20px;
24 | color: #fff;
25 | }
26 |
27 | .title {
28 | font-size: 22px;
29 | }
30 | .desc {
31 | margin-top: 6px;
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/AvatarData/index.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar } from "antd";
2 | import styles from "./index.module.scss";
3 |
4 | const AvatarData = ({ info }) => {
5 | return (
6 |
7 |
10 |
11 |
12 | {info?.nick}
13 |
14 |
{info?.signature}
15 |
16 |
17 | );
18 | };
19 |
20 | export default AvatarData;
21 |
--------------------------------------------------------------------------------
/src/components/D3/Collision/index.tsx:
--------------------------------------------------------------------------------
1 | import * as d3 from "d3";
2 | import { useEffect, useRef } from "react";
3 |
4 | const width = 600;
5 | const height = 300;
6 |
7 | const createData = () => {
8 | const k = width / 200;
9 | const r = d3.randomUniform(k, k * 4);
10 | const n = 4;
11 | return Array.from({ length: 200 }, (_, i) => ({
12 | r: r(),
13 | group: i && (i % n) + 1
14 | }));
15 | };
16 | const data = createData();
17 | const nodes = data.map(Object.create);
18 |
19 | const Collision = () => {
20 | const svgRef = useRef(null);
21 |
22 | const pointermoved = event => {
23 | const [x, y] = d3.pointer(event);
24 | nodes[0].fx = x - width / 2;
25 | nodes[0].fy = y - height / 2;
26 | };
27 |
28 | useEffect(() => {
29 | const color = d3.scaleOrdinal(d3.schemeTableau10);
30 | const context = svgRef.current.getContext("2d");
31 | d3.forceSimulation(nodes)
32 | .alphaTarget(0.3) // stay hot
33 | .velocityDecay(0.1) // low friction
34 | .force("x", d3.forceX().strength(0.01))
35 | .force("y", d3.forceY().strength(0.01))
36 | .force(
37 | "collide",
38 | d3
39 | .forceCollide()
40 | .radius(d => d.r + 1)
41 | .iterations(3)
42 | )
43 | .force(
44 | "charge",
45 | d3.forceManyBody().strength((d, i) => (i ? 0 : (-width * 2) / 3))
46 | )
47 | .on("tick", () => {
48 | context.clearRect(0, 0, width, height);
49 | context.save();
50 | context.translate(width / 2, height / 2);
51 | for (let i = 1; i < nodes.length; ++i) {
52 | const d = nodes[i];
53 | context.beginPath();
54 | context.moveTo(d.x + d.r, d.y);
55 | context.arc(d.x, d.y, d.r, 0, 2 * Math.PI);
56 | context.fillStyle = color(d.group);
57 | context.fill();
58 | }
59 | context.restore();
60 | });
61 |
62 | d3.select(context.canvas)
63 | .on("touchmove", event => event.preventDefault())
64 | .on("pointermove", pointermoved);
65 | }, []);
66 |
67 | return ;
68 | };
69 |
70 | export default Collision;
71 |
--------------------------------------------------------------------------------
/src/components/Echarts/BarChart.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect, useRef } from "react";
2 | import * as echarts from "echarts";
3 | const BarChart: FC = () => {
4 | const ref = useRef();
5 |
6 | /* { name:标题 , radius:[内圆直径,外圆直径] ,
7 | startAngle:圆心角 value:数据 ,
8 | color1: 颜色, color2: 颜色 } */
9 | function getItem(data) {
10 | return {
11 | name: "景区资源",
12 | type: "pie",
13 | center: ["50%", "60%"],
14 | radius: data.radius,
15 | startAngle: data.startAngle,
16 | avoidLabelOverlap: false,
17 | label: {
18 | show: true,
19 | color: "#2196b0"
20 | // position: 'center'
21 | },
22 | // 鼠标移入时文本状态
23 | emphasis: {
24 | label: {
25 | show: true,
26 | fontSize: 40,
27 | fontWeight: "bold"
28 | }
29 | },
30 | labelLine: {
31 | show: true,
32 | length: 60,
33 | length2: 20
34 | },
35 | data: [
36 | {
37 | value: data.value,
38 | name: data.name,
39 | itemStyle: {
40 | // 渐变颜色
41 | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
42 | {
43 | offset: 0,
44 | color: data.color1
45 | },
46 | {
47 | offset: 1,
48 | color: data.color2
49 | }
50 | ])
51 | }
52 | },
53 | {
54 | value: 100 - data.value,
55 | name: data.name, //设置name防止legend错位
56 | itemStyle: {
57 | // 颜色设置为none,则该片段不渲染
58 | color: "none"
59 | },
60 | label: { show: false }
61 | }
62 | ]
63 | };
64 | }
65 |
66 | useEffect(() => {
67 | const myChart: any = echarts.init(ref.current);
68 | myChart.setOption({
69 | backgroundColor: "rgba(124,193,173,0.5)",
70 | // 1.鼠标移入提示
71 | tooltip: {
72 | trigger: "item"
73 | },
74 | // 2.图例组件
75 | legend: {
76 | top: "5%",
77 | left: "center",
78 | // 文本样式
79 | textStyle: {
80 | color: "#1c91f5"
81 | }
82 | },
83 | // 3.图表内容
84 | series: [
85 | getItem({
86 | name: "AAAA景区",
87 | radius: ["30%", "35%"],
88 | startAngle: -10,
89 | value: 70,
90 | color1: "#8915f9",
91 | color2: "#3d13fd"
92 | }),
93 | getItem({
94 | name: "免费景区",
95 | radius: ["40%", "45%"],
96 | startAngle: 50,
97 | value: 50,
98 | color1: "#f72f48",
99 | color2: "#f44179"
100 | }),
101 | getItem({
102 | name: "度假村",
103 | radius: ["50%", "55%"],
104 | startAngle: 260,
105 | value: 50,
106 | color1: "#1686f3",
107 | color2: "#32b8fc"
108 | }),
109 | getItem({
110 | name: "文化古城",
111 | radius: ["60%", "65%"],
112 | startAngle: 150,
113 | value: 60,
114 | color1: "#2648f7",
115 | color2: "#2d8af9"
116 | })
117 | ]
118 | });
119 | window.addEventListener("resize", function () {
120 | myChart.resize();
121 | });
122 | return () => {
123 | myChart.dispose && myChart.dispose();
124 | };
125 | }, []);
126 |
127 | return ;
128 | };
129 |
130 | export default BarChart;
131 |
--------------------------------------------------------------------------------
/src/components/Echarts/ChinaChart.tsx:
--------------------------------------------------------------------------------
1 | import * as echarts from "echarts";
2 | import React, { useEffect, useRef } from "react";
3 | import axios from "axios";
4 |
5 | const ChinaChart = () => {
6 | const ref = useRef();
7 |
8 | useEffect(() => {
9 | const myChart = echarts.init(ref.current);
10 | axios.get("https://nest-admin.com/china.json").then(res => {
11 | echarts.registerMap("china", res.data);
12 | myChart.setOption({
13 | backgroundColor: "rgba(124,193,173,0.5)",
14 | // 进行相关配置
15 | tooltip: {
16 | borderColor: "#7cc1ad", //边框颜色
17 | borderWidth: 1
18 | }, // 鼠标移到图里面的浮动提示框
19 | dataRange: {
20 | show: false,
21 | min: 10000000,
22 | max: 200000000,
23 | text: ["High", "Low"],
24 | realtime: true,
25 | calculable: true
26 | // color: ["#52dbdf", "#FF9B52", "#FFD068"],
27 | },
28 | geo: {
29 | // 这个是重点配置区
30 | map: "china", // 表示中国地图
31 | roam: false, // 是否允许缩放
32 | zoom: 1,
33 | label: {
34 | show: true, // 是否显示对应地名
35 | color: "#000",
36 | fontSize: 11
37 | },
38 | itemStyle: {
39 | areaColor: null,
40 | shadowOffsetX: 0,
41 | shadowOffsetY: 0,
42 | shadowBlur: 1,
43 | borderWidth: 0,
44 | shadowColor: "rgba(0, 0, 0, 0.5)"
45 | }
46 | },
47 | series: [
48 | {
49 | type: "scatter",
50 | coordinateSystem: "geo" // 对应上方配置
51 | },
52 | {
53 | name: "访问次数", // 浮动框的标题
54 | type: "map",
55 | geoIndex: 0,
56 | data: [
57 | { name: "北京", value: 21893095 },
58 | { name: "天津", value: 13866009 },
59 | { name: "河北", value: 74610235 },
60 | { name: "山西", value: 34915616 },
61 | { name: "内蒙古", value: 24049155 },
62 | { name: "辽宁", value: 42591407 },
63 | { name: "吉林", value: 24073453 },
64 | { name: "黑龙江", value: 31850088 },
65 | { name: "上海", value: 24870895 },
66 | { name: "江苏", value: 84748016 },
67 | { name: "浙江", value: 64567588 },
68 | { name: "安徽", value: 61027171 },
69 | { name: "福建", value: 41540086 },
70 | { name: "江西", value: 45188635 },
71 | { name: "山东", value: 101527453 },
72 | { name: "河南", value: 99365519 },
73 | { name: "湖北", value: 57752557 },
74 | { name: "湖南", value: 66444864 },
75 | { name: "广东", value: 126012510 },
76 | { name: "广西", value: 50126804 },
77 | { name: "海南", value: 10081232 },
78 | { name: "重庆", value: 32054159 },
79 | { name: "四川", value: 83674866 },
80 | { name: "贵州", value: 38562148 },
81 | { name: "云南", value: 47209277 },
82 | { name: "西藏", value: 3648100 },
83 | { name: "陕西", value: 39528999 },
84 | { name: "甘肃", value: 25019831 },
85 | { name: "青海", value: 5923957 },
86 | { name: "宁夏", value: 7202654 },
87 | { name: "新疆", value: 25852345 },
88 | { name: "香港", value: 7508112 },
89 | { name: "澳门", value: 683567 },
90 | { name: "台湾", value: 23420442 }
91 | ]
92 | }
93 | ]
94 | });
95 | });
96 | window.addEventListener("resize", function () {
97 | myChart.resize();
98 | });
99 | return () => {
100 | myChart.dispose && myChart.dispose();
101 | };
102 | }, []);
103 |
104 | return ;
105 | };
106 |
107 | export default ChinaChart;
108 |
--------------------------------------------------------------------------------
/src/components/Echarts/LoginFrequencyChart.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import * as echarts from "echarts";
3 | import React, { useEffect, useRef } from "react";
4 |
5 | const LoginFrequencyChart = ({ dataSource = [] }) => {
6 | const ref = useRef();
7 |
8 | useEffect(() => {
9 | const payload = {};
10 | const userData = {};
11 | const newData = [...dataSource].reverse();
12 | newData.map(v => {
13 | const time = dayjs(v.created).format("MM-DD");
14 | if (!payload[time]) payload[time] = {};
15 | if (!payload[time][v.name]) payload[time][v.name] = 1;
16 | else payload[time][v.name] += 1;
17 |
18 | if (!userData[v.name]) userData[v.name] = {};
19 | if (!userData[v.name][time]) userData[v.name][time] = 1;
20 | else userData[v.name][time] += 1;
21 | });
22 | const xAxisData = Object.keys(payload);
23 | const nameData = Object.keys(userData);
24 | const seriesData = [];
25 | nameData.map(name => {
26 | const data = [];
27 | xAxisData.filter(time => data.push(userData[name][time] || 0));
28 | seriesData.push({
29 | name,
30 | type: "bar",
31 | stack: "login",
32 | emphasis: { focus: "series" },
33 | data: data
34 | });
35 | });
36 |
37 | const option = {
38 | title: {
39 | text: "最近15天登录趋势图"
40 | },
41 | tooltip: {
42 | trigger: "axis",
43 | axisPointer: { type: "shadow" }
44 | },
45 | legend: {
46 | data: nameData
47 | },
48 | grid: {
49 | left: "3%",
50 | right: "4%",
51 | bottom: "3%",
52 | containLabel: true
53 | },
54 | xAxis: {
55 | type: "category",
56 | data: xAxisData
57 | },
58 | yAxis: { type: "value" },
59 | series: seriesData
60 | };
61 |
62 | const myChart = echarts.init(ref.current);
63 | myChart.setOption(option);
64 | window.addEventListener("resize", function () {
65 | myChart.resize();
66 | });
67 | return () => {
68 | myChart.dispose && myChart.dispose();
69 | };
70 | }, [dataSource]);
71 |
72 | return ;
73 | };
74 |
75 | export default LoginFrequencyChart;
76 |
--------------------------------------------------------------------------------
/src/components/Echarts/PieBorderRadius.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect, useRef } from "react";
2 | import * as echarts from "echarts";
3 |
4 | const PieBorderRadius: FC = () => {
5 | const ref = useRef();
6 | useEffect(() => {
7 | const myChart: any = echarts.init(ref.current);
8 | myChart.setOption({
9 | title: {
10 | text: "UserAgent",
11 | left: 12,
12 | top: 12
13 | },
14 | tooltip: {
15 | trigger: "item"
16 | },
17 | series: [
18 | {
19 | name: "Access From",
20 | type: "pie",
21 | radius: ["0%", "70%"],
22 | avoidLabelOverlap: false,
23 | itemStyle: {
24 | borderRadius: 100,
25 | borderColor: "#fff",
26 | borderWidth: 2
27 | },
28 | data: [
29 | { value: 200, name: "Edge" },
30 | { value: 200, name: "Safari" },
31 | { value: 200, name: "Firefox" },
32 | { value: 200, name: "Other" },
33 | { value: 200, name: "Chrome" }
34 | ],
35 | label: {
36 | show: true,
37 | formatter: function (params) {
38 | const name = params.name;
39 | const percent = params.percent;
40 | return name + "\n" + percent;
41 | }
42 | }
43 | }
44 | ]
45 | });
46 | window.addEventListener("resize", function () {
47 | myChart.resize();
48 | });
49 | return () => {
50 | myChart.dispose && myChart.dispose();
51 | };
52 | }, []);
53 |
54 | return ;
55 | };
56 |
57 | export default PieBorderRadius;
58 |
--------------------------------------------------------------------------------
/src/components/Echarts/Round.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect, useRef } from "react";
2 | import * as echarts from "echarts";
3 |
4 | const Round: FC = () => {
5 | const ref = useRef();
6 |
7 | const bgPatternImg = new Image();
8 | bgPatternImg.src = "https://dr.cdnux.com/18510675/i/2025/01/04/676423a20a391.webp";
9 | const chartData = [
10 | {
11 | value: 520
12 | },
13 | {
14 | value: 280
15 | },
16 | {
17 | value: 100
18 | },
19 | {
20 | value: 100
21 | }
22 | ];
23 | const colorList = [
24 | new echarts.graphic.LinearGradient(0, 0, 0, 1, [
25 | {
26 | offset: 0,
27 | color: "#CA8CCA"
28 | },
29 | {
30 | offset: 1,
31 | color: "#EFA5BB"
32 | }
33 | ]),
34 | new echarts.graphic.LinearGradient(0, 0, 0, 1, [
35 | {
36 | offset: 0,
37 | color: "#BFA4E4"
38 | },
39 | {
40 | offset: 1,
41 | color: "#E29CE2"
42 | }
43 | ]),
44 | new echarts.graphic.LinearGradient(0, 0, 0, 1, [
45 | {
46 | offset: 0,
47 | color: "#A8AAE5"
48 | },
49 | {
50 | offset: 1,
51 | color: "#BEA3E3"
52 | }
53 | ]),
54 | new echarts.graphic.LinearGradient(0, 0, 0, 1, [
55 | {
56 | offset: 0,
57 | color: "#A4D37D"
58 | },
59 | {
60 | offset: 1,
61 | color: "#E5F2A7"
62 | }
63 | ])
64 | ];
65 | const sum = chartData.reduce((per, cur) => per + cur.value, 0);
66 | const gap = (1 * sum) / 100;
67 | const pieData1 = [];
68 | const pieData2 = [];
69 | const gapData = {
70 | name: "",
71 | value: gap,
72 | itemStyle: {
73 | color: "transparent"
74 | }
75 | };
76 | for (let i = 0; i < chartData.length; i++) {
77 | pieData1.push({
78 | ...chartData[i],
79 | itemStyle: {
80 | borderRadius: 100,
81 | shadowColor: "#2a2a34",
82 | shadowBlur: 5,
83 | shadowOffsetY: 0,
84 | shadowOffsetX: 0,
85 | borderColor: "#2a2a34",
86 | borderWidth: 2
87 | }
88 | });
89 | pieData1.push(gapData);
90 |
91 | pieData2.push({
92 | ...chartData[i],
93 | itemStyle: {
94 | borderRadius: 10,
95 | color: colorList[i],
96 | opacity: 0.1,
97 | shadowColor: "#000",
98 | shadowBlur: 1,
99 | shadowOffsetY: 5,
100 | shadowOffsetX: 0
101 | }
102 | });
103 | pieData2.push(gapData);
104 | }
105 |
106 | const option = {
107 | backgroundColor: {
108 | image: bgPatternImg,
109 | repeat: "repeat"
110 | },
111 |
112 | color: colorList,
113 |
114 | series: [
115 | {
116 | type: "pie",
117 | z: 3,
118 | radius: ["44%", "51%"],
119 | center: ["50%", "50%"],
120 | label: {
121 | show: false
122 | },
123 | labelLine: {
124 | show: false
125 | },
126 | data: pieData1
127 | }
128 | ]
129 | };
130 |
131 | useEffect(() => {
132 | const myChart: any = echarts.init(ref.current);
133 | myChart.setOption(option);
134 | window.addEventListener("resize", function () {
135 | myChart.resize();
136 | });
137 | return () => {
138 | myChart.dispose && myChart.dispose();
139 | };
140 | }, []);
141 |
142 | return ;
143 | };
144 |
145 | export default Round;
146 |
--------------------------------------------------------------------------------
/src/components/Echarts/index.ts:
--------------------------------------------------------------------------------
1 | // export const AreaStackChart = lazy(() => import(/* webpackChunkName: "AreaStackChart'"*/ "./AreaStackChart"));
2 |
3 | // export const BarChart = lazy(() => import(/* webpackChunkName: "BarChart'"*/ "./BarChart"));
4 |
5 | // export const LineChart = lazy(() => import(/* webpackChunkName: "LineChart'"*/ "./LineChart"));
6 |
7 | // export const PictorialBarChart = lazy(() => import(/* webpackChunkName: "PictorialBarChart'"*/ "./PictorialBarChart"));
8 |
9 | // export const PieChart = lazy(() => import(/* webpackChunkName: "PieChart'"*/ "./PieChart"));
10 |
11 | // export const ScatterChart = lazy(() => import(/* webpackChunkName: "ScatterChart'"*/ "./ScatterChart"));
12 |
13 | // export const LoginFrequencyChart = lazy(() => import(/* webpackChunkName: "ScatterChart'"*/ "./LoginFrequencyChart"));
14 |
15 | import AreaStackChart from "./AreaStackChart";
16 | import BarChart from "./BarChart";
17 | import LineChart from "./LineChart";
18 | import PictorialBarChart from "./PictorialBarChart";
19 | import PieChart from "./PieChart";
20 | import ScatterChart from "./ScatterChart";
21 | import LoginFrequencyChart from "./LoginFrequencyChart";
22 | import ChinaChart from "./ChinaChart";
23 | import PieBorderRadius from "./PieBorderRadius";
24 | import DataFlowChart from "./DataFlowChart";
25 | import HomeSenseChart from "./HomeSenseChart";
26 | import ServerStatusChart from "./ServerStatusChart";
27 |
28 | export {
29 | AreaStackChart,
30 | BarChart,
31 | LineChart,
32 | PictorialBarChart,
33 | PieChart,
34 | ScatterChart,
35 | LoginFrequencyChart,
36 | ChinaChart,
37 | PieBorderRadius,
38 | DataFlowChart,
39 | HomeSenseChart,
40 | ServerStatusChart
41 | };
42 |
--------------------------------------------------------------------------------
/src/components/FineDay/index.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./index.module.scss";
2 |
3 | const FineDay = () => {
4 | return (
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
35 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default FineDay;
48 |
--------------------------------------------------------------------------------
/src/components/Footer/Copyright/index.module.scss:
--------------------------------------------------------------------------------
1 | .copyright {
2 | height: 20px;
3 | padding-right: 12px;
4 | text-align: right;
5 | }
6 |
7 | .link {
8 | color: #1677ff;
9 | &:active {
10 | color: #1677ff;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Footer/Copyright/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./index.module.scss";
3 | import { Link } from "react-router-dom";
4 |
5 | const Copyright = () => {
6 | return (
7 |
8 |
9 | 京ICP备17023235号
10 |
11 |
12 | );
13 | };
14 |
15 | export default Copyright;
16 |
--------------------------------------------------------------------------------
/src/components/Forms/LoginForm/index.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | user-select: none;
3 | width: 324px;
4 | min-height: 366px;
5 | padding: 40px;
6 | position: relative;
7 | background: rgba(164, 203, 200, 0.3);
8 | border-radius: 8px;
9 | &:hover {
10 | box-shadow: -4px 4px 16px 1px rgba(0, 0, 0, 0.15);
11 | background: rgba(164, 203, 200, 1);
12 | transition:
13 | box-shadow 1s,
14 | background 1s,
15 | transform 2s;
16 | }
17 | }
18 |
19 | .title {
20 | text-align: center;
21 | padding: 0px;
22 | margin: 0 0 20px 0;
23 | }
24 |
25 | .forgetPassword {
26 | float: right;
27 | padding-right: 12px;
28 | cursor: pointer;
29 | color: #1890ff;
30 | }
31 |
32 | .other {
33 | padding-bottom: 24px;
34 | }
35 |
36 | .submit {
37 | text-align: center;
38 | }
39 |
40 | .captchaLogin {
41 | margin-top: -11px;
42 | }
43 |
44 | .captchaContainer {
45 | position: absolute;
46 | top: 50%;
47 | right: 2px;
48 | width: calc(50% - 10px);
49 | transform: translateY(-50%);
50 | height: 32px;
51 | border-radius: 6px;
52 | }
53 |
54 | .captchaInput {
55 | width: calc(50%);
56 | }
57 |
58 | .ohter {
59 | display: flex;
60 | justify-content: center;
61 | gap: 15px;
62 | // justify-content: center;
63 |
64 | &:first-child {
65 | margin-left: auto;
66 | }
67 |
68 | &:last-child {
69 | margin-right: auto;
70 | }
71 | }
72 |
73 | .type {
74 | display: block;
75 | }
76 |
77 | .changeQR {
78 | cursor: pointer;
79 | position: absolute;
80 | width: 0;
81 | height: 0;
82 | top: 0px;
83 | right: 0px;
84 | border-top-left-radius: 8px;
85 | border-bottom: 30px solid transparent;
86 | border-right: 30px solid transparent;
87 | border-top: 30px solid rgba(7, 67, 36, 0.8);
88 | border-left: 30px solid rgba(7, 67, 36, 0.8);
89 | transform: rotate(90deg);
90 | }
91 |
92 | .code {
93 | float: right;
94 | margin-top: -22px;
95 | margin-right: -10px;
96 | width: 30px;
97 | height: 30px;
98 | background: url("/src/assets/qrcode-mini.png") no-repeat;
99 | background-size: 100%;
100 | }
101 |
102 | .card {
103 | text-align: center;
104 | margin-top: 45px;
105 | }
106 |
107 | .register {
108 | cursor: pointer;
109 | color: #1677ff;
110 | }
111 |
--------------------------------------------------------------------------------
/src/components/Forms/RegisterForm/index.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | user-select: none;
3 | padding: 40px;
4 | min-width: 324px;
5 | position: relative;
6 | background: rgba(164, 203, 200, 1);
7 |
8 | }
9 |
10 | .title {
11 | text-align: center;
12 | padding: 0px;
13 | margin: 0px 0px 12px 0px;
14 | }
15 |
16 | .forgetPassword {
17 | float: right;
18 | padding-right: 12px;
19 | cursor: pointer;
20 | color: #1890ff;
21 | }
22 |
23 | .other {
24 | padding-bottom: 24px;
25 | }
26 |
27 | .submit {
28 | text-align: center;
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Header/DefaultLayoutHeader/index.module.scss:
--------------------------------------------------------------------------------
1 | @import "@/styles/themeMixin";
2 |
3 | .layoutHeader {
4 | height: 50px;
5 | line-height: 50px;
6 | overflow: hidden;
7 | @include get-class-from-key("background-color");
8 | @include get-class-from-key("color", "color-head");
9 | .user {
10 | float: right;
11 | cursor: pointer;
12 | user-select: none;
13 | @include get-class-from-key("background-color");
14 | @include get-class-from-key("color", "head-icon-color");
15 | &:hover {
16 | @include get-class-from-key("background-color", "head-icon-bg");
17 | @include get-class-from-key("color", "head-icon-bg-hover");
18 | }
19 | }
20 |
21 | .nameContainer {
22 | padding: 0 12px;
23 | }
24 |
25 | .name {
26 | margin-left: 6px;
27 | }
28 |
29 | .logo {
30 | float: left;
31 | cursor: pointer;
32 | user-select: none;
33 | width: 200px;
34 | text-align: center;
35 | font-size: 30px;
36 | font-weight: bold;
37 | }
38 |
39 | .title {
40 | /*渐变背景*/
41 | background-image: -webkit-linear-gradient(
42 | left,
43 | #3498db,
44 | #f47920 10%,
45 | #d71345 20%,
46 | #f7acbc 30%,
47 | #ffd400 40%,
48 | #3498db 50%,
49 | #f47920 60%,
50 | #d71345 70%,
51 | #f7acbc 80%,
52 | #ffd400 90%,
53 | #3498db
54 | );
55 | color: transparent; /*文字填充色为透明*/
56 | -webkit-text-fill-color: transparent;
57 | -webkit-background-clip: text;
58 | background-clip: text;
59 | background-size: 200% 100%;
60 | /* 动画 */
61 | animation: masked-animation 4s infinite linear;
62 | }
63 | }
64 |
65 | @keyframes masked-animation {
66 | 0% {
67 | background-position: 0 0;
68 | }
69 | 100% {
70 | background-position: -100% 0;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/Header/HeaderNotice/index.module.scss:
--------------------------------------------------------------------------------
1 | @import "@/styles/themeMixin";
2 | .notice {
3 | float: right;
4 | user-select: none;
5 | cursor: pointer;
6 | margin-right: 20px;
7 |
8 | .icon {
9 | font-size: 22px;
10 | @include get-class-from-key("color", "head-icon-color");
11 | &:hover {
12 | @include get-class-from-key("background-color", "head-icon-bg");
13 | @include get-class-from-key("color", "head-icon-bg-hover");
14 | }
15 | }
16 | }
17 |
18 | .container {
19 | :global {
20 | .ant-tabs-tab {
21 | width: 130px;
22 | .ant-tabs-tab-btn {
23 | margin: 0 auto;
24 | }
25 | }
26 | }
27 | }
28 |
29 | .bottomBar {
30 | width: 100%;
31 | border-top: 1px solid #f0f0f0;
32 | border-radius: 0 0 2px 2px;
33 | transition: all 0.3s;
34 | }
35 |
36 | .dobtn {
37 | width: 49%;
38 | padding: 12px 0;
39 | float: left;
40 | cursor: pointer;
41 | text-align: center;
42 | }
43 |
44 | .doLeftBtn {
45 | @extend .dobtn;
46 | border-right: 1px solid #f0f0f0;
47 | }
48 |
49 | .tab {
50 | padding: 0 12px;
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/Header/ThemeColor/index.module.scss:
--------------------------------------------------------------------------------
1 | @import "@/styles/themeMixin";
2 |
3 | .ThemeColor {
4 | float: right;
5 | cursor: pointer;
6 | user-select: none;
7 | margin-right: 10px;
8 | font-size: 22px;
9 | padding: 0 12px;
10 | @include get-class-from-key("color", "head-icon-color");
11 | &:hover {
12 | @include get-class-from-key("background-color", "head-icon-bg");
13 | @include get-class-from-key("color", "head-icon-bg-hover");
14 | }
15 | }
16 |
17 | .themetooltip {
18 | @include get-class-from-key("color", "head-icon-color");
19 | &:hover {
20 | @include get-class-from-key("background-color", "head-icon-bg");
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/Header/ThemeColor/index.tsx:
--------------------------------------------------------------------------------
1 | import { SunOutlined } from "@ant-design/icons";
2 | import styles from "./index.module.scss";
3 | import { useContext, useState } from "react";
4 | import { Tooltip } from "antd";
5 | import ProjectContext from "@/contexts/ProjectContext";
6 |
7 | const ThemeColor = () => {
8 | const { theme, setTheme } = useContext(ProjectContext);
9 | const themeDict = {
10 | light: "dark",
11 | dark: "light"
12 | };
13 |
14 | function handleChangeTheme() {
15 | const data = themeDict[theme];
16 | setTheme(data);
17 | }
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default ThemeColor;
28 |
--------------------------------------------------------------------------------
/src/components/IconFont/index.tsx:
--------------------------------------------------------------------------------
1 | import { createFromIconfontCN } from "@ant-design/icons";
2 |
3 | const IconFont = createFromIconfontCN({
4 | scriptUrl: [
5 | "//at.alicdn.com/t/c/font_4414978_4e7y8rpfw8r.js" // icon-javascript, icon-java, icon-shoppingcart (overridden)
6 | ]
7 | });
8 |
9 | export default IconFont;
10 |
--------------------------------------------------------------------------------
/src/components/LoginLog/index.tsx:
--------------------------------------------------------------------------------
1 | import { loginlog } from "@/api/microservice/log";
2 | import { LoginFrequencyChart } from "@/components/Echarts";
3 | import { getDateTimeFormat } from "@/utils/core";
4 | import { Table } from "antd";
5 | import dayjs from "dayjs";
6 | import { useEffect, useState } from "react";
7 |
8 | const LoginLog = () => {
9 | const columns: any = [
10 | {
11 | title: "昵称",
12 | dataIndex: "name"
13 | },
14 | {
15 | title: "浏览器版本",
16 | dataIndex: "bowser",
17 | responsive: ["lg", "xl", "xxl"]
18 | },
19 | {
20 | title: "来源",
21 | dataIndex: "host",
22 | responsive: ["lg", "xl", "xxl"]
23 | },
24 | {
25 | title: "登录设备",
26 | dataIndex: "os",
27 | responsive: ["lg", "xl", "xxl"]
28 | },
29 | {
30 | title: "国家",
31 | dataIndex: "country",
32 | responsive: ["md", "lg", "xl", "xxl"]
33 | },
34 | {
35 | title: "省份",
36 | dataIndex: "province",
37 | responsive: ["lg", "xl", "xxl"]
38 | },
39 | {
40 | title: "市区",
41 | dataIndex: "city",
42 | responsive: ["lg", "xl", "xxl"]
43 | },
44 | {
45 | title: "登录时间",
46 | dataIndex: "created",
47 | render: item => {
48 | return {getDateTimeFormat(new Date(item).valueOf())};
49 | }
50 | }
51 | ];
52 |
53 | const [dataSource, setDataSource] = useState();
54 |
55 | const getLog = () => {
56 | loginlog({
57 | st: dayjs().subtract(15, "days").format("YYYY-MM-DD"),
58 | et: dayjs().add(1, "days").format("YYYY-MM-DD")
59 | })
60 | .then(r => {
61 | if (r.data.code === 200) setDataSource(r?.data?.data?.reverse() || []);
62 | })
63 | .catch(() => setDataSource([]));
64 | };
65 | useEffect(getLog, []);
66 |
67 | return (
68 |
85 | );
86 | };
87 |
88 | export default LoginLog;
89 |
--------------------------------------------------------------------------------
/src/components/Menu/WebMenu/hack.ts:
--------------------------------------------------------------------------------
1 | const menuItems: any = [
2 | {
3 | alwaysShow: true,
4 | component: "Layout",
5 | hidden: false,
6 | meta: { icon: "system", noCache: true, title: "统计面板" },
7 | name: "dashboard",
8 | path: "/dashboard"
9 | },
10 | {
11 | alwaysShow: true,
12 | children: [
13 | {
14 | component: "system/user/index",
15 | hidden: false,
16 | meta: { icon: "peoples", noCache: true, title: "后台用户管理" },
17 | name: "User",
18 | path: "user"
19 | }
20 | ],
21 | component: "Layout",
22 | hidden: false,
23 | meta: { icon: "system", noCache: true, title: "系统管理" },
24 | name: "system",
25 | path: "/system",
26 | redirect: "noredirect"
27 | },
28 | {
29 | alwaysShow: true,
30 | children: [
31 | {
32 | component: "weapp/periodical/index",
33 | hidden: false,
34 | meta: { icon: "peoples", noCache: true, title: "期刊管理" },
35 | name: "Periodical",
36 | path: "periodical"
37 | },
38 | {
39 | component: "weapp/weappuser/index",
40 | hidden: false,
41 | meta: { icon: "peoples", noCache: true, title: "用户管理" },
42 | name: "weappuser",
43 | path: "weappuser"
44 | },
45 | {
46 | component: "weapp/banner/index",
47 | hidden: false,
48 | meta: { icon: "peoples", noCache: true, title: "轮播图管理" },
49 | name: "banner",
50 | path: "banner"
51 | },
52 | {
53 | component: "weapp/article/index",
54 | hidden: false,
55 | meta: { icon: "peoples", noCache: true, title: "文章管理" },
56 | name: "article",
57 | path: "article"
58 | },
59 | {
60 | component: "weapp/redeemcode/index",
61 | hidden: false,
62 | meta: { icon: "peoples", noCache: true, title: "兑换码管理" },
63 | name: "redeemcode",
64 | path: "redeemcode"
65 | },
66 | {
67 | component: "weapp/articleinfo/index",
68 | hidden: true,
69 | meta: { icon: "peoples", noCache: true, title: "文章详情" },
70 | name: "articleinfo",
71 | path: "articleinfo"
72 | }
73 | ],
74 | component: "Layout",
75 | hidden: false,
76 | meta: { icon: "monitor", noCache: true, title: "小程序内容管理" },
77 | name: "weapp",
78 | path: "/weapp",
79 | redirect: "noredirect"
80 | }
81 | ];
82 |
83 | const creteMenu = (items: any, list: any[]) => {
84 | const parentPath: any = list?.length > 0 ? `${list.join("/")}/` : "";
85 | for (let i = 0; i < items.length; i++) {
86 | const element = items[i];
87 | element.label = element?.meta?.title;
88 | element.key = `${parentPath}${element.path}`;
89 | delete element.alwaysShow;
90 | if (element.children) {
91 | // 需要过滤掉不该显示的菜单
92 | const showChildrens = element?.children.filter(v => !v?.hidden);
93 | element.children = creteMenu(showChildrens, [...list, element.path]);
94 | }
95 | }
96 |
97 | return items;
98 | };
99 |
100 | export const menus = creteMenu(menuItems, []);
101 |
--------------------------------------------------------------------------------
/src/components/Menu/WebMenu/index.module.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/components/Menu/WebMenu/index.module.scss
--------------------------------------------------------------------------------
/src/components/Menu/WebMenu/index.tsx:
--------------------------------------------------------------------------------
1 | import IconFont from "@/components/IconFont";
2 | import MenuTagContext from "@/contexts/MenuTagContext";
3 | import { formatURL, getLocalStorageMenus, getMenus, getOpenKeysByUrls, setLocalStorage } from "@/utils/menuUtils";
4 | import { AppstoreOutlined } from "@ant-design/icons";
5 | import { Menu } from "antd";
6 | import { toNumber } from "lodash-es";
7 | import { useContext, useLayoutEffect, useState } from "react";
8 | import { useLocation, useNavigate } from "react-router-dom";
9 |
10 | const createMenuIcon = item => {
11 | return item.map(v => {
12 | v.icon = v.icon ? : ;
13 | if (v.children) createMenuIcon(v.children);
14 | return v;
15 | });
16 | };
17 |
18 | const WebMenu = () => {
19 | const { addTag } = useContext(MenuTagContext);
20 | const navigate = useNavigate();
21 | const { pathname } = useLocation();
22 | const menusList = JSON.parse(getLocalStorageMenus() || "");
23 | const [menuItems, setMenuItems] = useState([]);
24 | const [selectedKeys, setSelectedKeys] = useState([]);
25 | const [openKeys, setOpenkeys] = useState([]);
26 |
27 | // 点击 menuItem 触发的事件
28 | const onClick = props => {
29 | const { keyPath, key } = props;
30 | // key 是指 嵌套到最底层的那一层菜单 key
31 | // keyPath 是指 由深到浅组成的数组
32 | // keyPath 是当前所有的菜单,我们需要把菜单拼接起来,记住,从后往前
33 | const menuPath: string[] = keyPath.map(v => toNumber(v)).reverse();
34 | let changeMenus = menusList.concat();
35 | const urls = [];
36 | for (let index = 0; index < menuPath.length; index++) {
37 | const item = menuPath[index];
38 | const result = changeMenus.find(v => v.id === parseInt(item));
39 | urls.push(result?.url || "");
40 | if (result.children) changeMenus = result.children;
41 | }
42 | const url = formatURL("/" + urls.join("/"));
43 | navigate(url);
44 | setOpenkeys(() => keyPath); // 修改当前组件的state
45 | setSelectedKeys(() => [key]);
46 | addTag(urls, key);
47 | };
48 |
49 | useLayoutEffect(() => {
50 | // 说明:
51 | // (1)
52 | // 这里要考虑 ( 登陆第一次跳转的加载 ) 和 ( 刷新浏览器的加载 )
53 | // 不管哪种情况,都获取当前的 pathname,当前pathname是 ( path和menu的keys要一致的原因 )
54 | // 如果第一次刷新,是不是可以直接从地址栏解析出 id ?
55 | const menusList = JSON.parse(getLocalStorageMenus() || "");
56 | const result = getMenus(menusList);
57 | const items = createMenuIcon(result);
58 | const data = pathname.split("/").filter(v => v);
59 | const [openKeys, currentKey] = getOpenKeysByUrls(data, items);
60 | setMenuItems(items);
61 | setOpenkeys(openKeys);
62 | setSelectedKeys(currentKey);
63 | }, [pathname]);
64 |
65 | // 展开/关闭的回调
66 | const onOpenChange = (openKeys: any) => {
67 | setOpenkeys(() => openKeys);
68 | setLocalStorage("cacheOpenKeys", openKeys); // 记住展开关闭的组,刷新持久化
69 | };
70 |
71 | return (
72 |
73 |
83 |
84 | );
85 | };
86 |
87 | export default WebMenu;
88 |
--------------------------------------------------------------------------------
/src/components/Modals/MenuEditModal.tsx:
--------------------------------------------------------------------------------
1 | import { createMenuItem, updateMenuItem } from "@/api/microservice/menu";
2 | import { Col, Form, Input, Modal, Row, Select, message } from "antd";
3 | import { FC, useEffect } from "react";
4 |
5 | const layout = {
6 | labelCol: { span: 8 },
7 | wrapperCol: { span: 16 }
8 | };
9 |
10 | const { Option } = Select;
11 | const { TextArea } = Input;
12 |
13 | const MenuEditModal: FC = ({ onOk, initMenuItem, parentUid, onCancel }) => {
14 | const [form] = Form.useForm();
15 |
16 | const handleSubmit = () => {};
17 |
18 | const creatRoleSubmit = async () => {
19 | const data = await form.validateFields();
20 | let result: any = null;
21 | const payload = {
22 | parentUid,
23 | ...data,
24 | version: 1,
25 | delete:0,
26 | };
27 | if (payload.id) {
28 | result = await updateMenuItem(payload);
29 | } else {
30 | result = await createMenuItem(payload);
31 | }
32 | if (result.data.code !== 200) {
33 | message.info("操作失败");
34 | return;
35 | }
36 | message.info("操作成功");
37 | onOk(true);
38 | };
39 |
40 | const onGenderChange = (value: string) => {
41 | form.setFieldsValue({ type: value });
42 | };
43 |
44 | const hiddMenuChange = (value: string) => {
45 | form.setFieldsValue({ show: value });
46 | };
47 |
48 | useEffect(() => {
49 | form.resetFields();
50 | form.getFieldsValue();
51 | }, [form]);
52 |
53 | return (
54 |
55 |
106 |
107 | );
108 | };
109 |
110 | export default MenuEditModal;
111 |
--------------------------------------------------------------------------------
/src/components/Modals/MessageModal.tsx:
--------------------------------------------------------------------------------
1 | import { Modal } from "antd";
2 | import React, { FC, useEffect } from "react";
3 |
4 | const MessageModal: FC = ({ title = "提示", pendingCallback, content, okText = "确认", cancelText = "取消" }) => {
5 | const onOkSubmit = async () => {
6 | pendingCallback(true);
7 | };
8 | const CancelSubmit = () => {
9 | pendingCallback(false);
10 | };
11 |
12 | useEffect(() => {}, []);
13 |
14 | return (
15 |
16 | {content}
17 |
18 | );
19 | };
20 |
21 | export default MessageModal;
22 |
--------------------------------------------------------------------------------
/src/components/Modals/MetaDescModal/index.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 40px;
3 | width: 300px;
4 | margin: 0 auto;
5 | user-select: none;
6 | cursor: pointer;
7 | }
8 |
9 | .avatar {
10 | text-align: center;
11 | width: 100px;
12 | height: 100px;
13 | margin-left: 50%;
14 | transform: translateX(-50%);
15 | }
16 |
17 | .name {
18 | text-align: center;
19 | font-size: 22px;
20 | margin-top: 15px;
21 | font-size: 24px;
22 | font-weight: 500;
23 | }
24 |
25 | .description {
26 | margin-top: 8px;
27 | text-align: center;
28 | }
29 |
30 | .area {
31 | height: 30px;
32 | line-height: 30px;
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Modals/MetaDescModal/index.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import React, { FC } from "react";
3 |
4 | import { Avatar, Button, Modal } from "antd";
5 |
6 | import styles from "./index.module.scss";
7 | import { BankOutlined, HomeOutlined, MailOutlined, WomanOutlined } from "@ant-design/icons";
8 | import IconFont from "@/components/IconFont";
9 |
10 | const MetaDescModal: FC = ({ userInfo, onOk }) => {
11 | return (
12 |
20 | 确定
21 |
22 | }>
23 |
24 |
25 |
{userInfo.name}
26 |
{userInfo.signature}
27 |
28 |
29 |
30 | {userInfo.email}
31 |
32 |
33 | 女
34 |
35 |
36 |
37 |
38 | 1992/8/11
39 |
40 |
41 |
42 |
43 | {dayjs(userInfo.created).format("YYYY-MM-DD HH:mm")}
44 |
45 |
46 |
47 | {userInfo.address}
48 |
49 |
50 |
51 | 集团 - 事业群 - 技术部
52 |
53 |
54 |
55 | 中国 • 广东省 • 深圳市
56 |
57 |
58 |
59 | JavaScript、HTML、CSS、Vue、Node
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default MetaDescModal;
67 |
--------------------------------------------------------------------------------
/src/components/Modals/ModalMessage.tsx:
--------------------------------------------------------------------------------
1 | import { AppstoreOutlined, MailOutlined, SettingOutlined } from "@ant-design/icons";
2 | import { Menu, Modal, PageHeader } from "antd";
3 | import React, { FC, useEffect } from "react";
4 |
5 | const { SubMenu } = Menu;
6 |
7 | interface ModalMessageProps {
8 | title: string;
9 | onOk: any;
10 | onCancel: any;
11 | content: any;
12 | okText: string;
13 | cancelText: string;
14 | }
15 |
16 | const ModalMessage: FC = ({ title, onOk, onCancel, content, okText, cancelText }) => {
17 | useEffect(() => {}, []);
18 |
19 | return (
20 |
21 | null} title="Title" subTitle="This is a subtitle" />
22 |
23 |
46 | {content}
47 |
48 | );
49 | };
50 |
51 | export default ModalMessage;
52 |
--------------------------------------------------------------------------------
/src/components/Modals/OperationLabelModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, FC } from "react";
2 | import { Form, Row, Col, Input, Modal, message, Select } from "antd";
3 | import { addnewlabelbyuser, putlabelbyuser } from "@/api/caravan/Labels";
4 |
5 | const layout = {
6 | labelCol: { span: 8 },
7 | wrapperCol: { span: 16 }
8 | };
9 |
10 | const OperationLabelModal: FC = ({ label, onCancel, onOk }) => {
11 | const [form] = Form.useForm();
12 |
13 | const onFinish = async data => {
14 | const payload = { ...label, ...data };
15 | const result = label?.id ? await putlabelbyuser(payload) : await addnewlabelbyuser(payload);
16 | if (result.data.code !== 200) {
17 | message.info("操作失败");
18 | return;
19 | }
20 | message.info("操作成功");
21 | onOk();
22 | };
23 |
24 | const hiddMenuChange = (dontshow: string) => form.setFieldsValue({ dontshow });
25 |
26 | useEffect(() => {
27 | form.resetFields();
28 | form.getFieldsValue();
29 | }, [form]);
30 |
31 | return (
32 | form.submit()} onCancel={onCancel} width={800} getContainer={false}>
33 |
66 |
67 | );
68 | };
69 |
70 | export default OperationLabelModal;
71 |
--------------------------------------------------------------------------------
/src/components/Modals/OperationMenuModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { message, Modal, Tree } from "antd";
2 | import React, { FC, useEffect, useState } from "react";
3 | import { giveRoleMenus } from "@/api/caravan/Rbac";
4 |
5 | const OperationMenuModal: FC = (props: any) => {
6 | const { title, pendingCallback, changeMenu, allMenu, okText, cancelText, roleCode } = props;
7 | const [expandedKeys, setExpandedKeys] = useState([]);
8 | const [checkedKeys, setCheckedKeys] = useState([]);
9 | const [selectedKeys, setSelectedKeys] = useState([]);
10 | const [autoExpandParent, setAutoExpandParent] = useState(true);
11 | const [, setexpandedKeysValue] = useState(true);
12 | const onOkSubmit = async () => {
13 | //分配人员
14 | const payload = { roleCode, menus: checkedKeys };
15 | const result: any = await giveRoleMenus(payload);
16 | if (result.data.code === 200) {
17 | message.info("操作成功");
18 | pendingCallback(true);
19 | return;
20 | }
21 | message.error("操作失败");
22 | };
23 | const CancelSubmit = () => {
24 | pendingCallback(false);
25 | };
26 | useEffect(() => {
27 | setCheckedKeys(changeMenu);
28 | }, [changeMenu]);
29 | const onExpand = (expandedKeysValue: React.Key[]) => {
30 | setexpandedKeysValue(expandedKeysValue);
31 | setExpandedKeys(expandedKeysValue);
32 | setAutoExpandParent(false);
33 | };
34 | const [, setinfo] = useState(null);
35 |
36 | const onSelect = (selectedKeysValue: React.Key[]) => {
37 | setSelectedKeys(selectedKeysValue);
38 | };
39 | const onCheck = (checkedKeys: any, info: any) => {
40 | setinfo(info);
41 | setCheckedKeys(checkedKeys);
42 | };
43 |
44 | return (
45 |
46 |
47 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default OperationMenuModal;
65 |
--------------------------------------------------------------------------------
/src/components/Modals/OperationRoleModal.tsx:
--------------------------------------------------------------------------------
1 | import { createRole, updateRole } from "@/api/microservice/role";
2 | import { Col, Form, Input, Modal, Row, message } from "antd";
3 | import { FC, useEffect } from "react";
4 |
5 | const layout = {
6 | labelCol: { span: 8 },
7 | wrapperCol: { span: 16 }
8 | };
9 |
10 | const RoleEditModal: FC = ({ role, onCancel, onOk }: any) => {
11 | const [form] = Form.useForm();
12 |
13 | const onFinish = async data => {
14 | const result: any = role?.id ? await updateRole({ ...role, ...data }) : await createRole({ ...role, ...data });
15 | if (result.data.code !== 200) {
16 | message.info("操作失败");
17 | return;
18 | }
19 | message.info("操作成功");
20 | onOk();
21 | };
22 |
23 | useEffect(() => {
24 | form.resetFields();
25 | form.getFieldsValue();
26 | }, [form]);
27 |
28 | useEffect(() => {
29 | if (role?.id) form.setFieldsValue({ ...role });
30 | }, [role, form]);
31 |
32 | return (
33 | form.submit()} onCancel={onCancel} width={800}>
34 |
58 |
59 | );
60 | };
61 |
62 | export default RoleEditModal;
63 |
--------------------------------------------------------------------------------
/src/components/Modals/PlacardEditModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { createPlacard, updatePlacard } from "@/api/microservice/log";
2 | import { Col, Form, Input, Modal, Row, Select, message } from "antd";
3 | import { FC, useEffect } from "react";
4 |
5 | const layout = {
6 | labelCol: { span: 8 },
7 | wrapperCol: { span: 16 }
8 | };
9 |
10 | const PlacardEditModal: FC = ({ placard, onOk, onCancel }: any) => {
11 | const [form] = Form.useForm();
12 |
13 | const onGenderChange = (type: string) => form.setFieldsValue({ type });
14 |
15 | const onFinish = async payload => {
16 | const result = placard?.id ? await updatePlacard({ ...placard, ...payload }) : await createPlacard(payload);
17 | if (result.data.code !== 200) {
18 | message.info("操作失败");
19 | return;
20 | }
21 | message.success("操作成功");
22 | onOk();
23 | };
24 |
25 | useEffect(() => {
26 | form.resetFields();
27 | form.getFieldsValue();
28 | }, [form]);
29 |
30 | useEffect(() => {
31 | if (placard?.id) {
32 | form.setFieldsValue({ ...placard });
33 | }
34 | }, [placard, form]);
35 |
36 | return (
37 | form.submit()} onCancel={onCancel} width={800} getContainer={false}>
38 |
65 |
66 | );
67 | };
68 |
69 | export default PlacardEditModal;
70 |
--------------------------------------------------------------------------------
/src/components/Modals/RoleAllocationMenuModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { updateRoleMenus } from "@/api/microservice/role";
2 | import { message, Modal, Tree } from "antd";
3 | import React, { FC, useState } from "react";
4 |
5 | const RoleAllocationMenuModal: FC = ({ treeData, onOk, onCancel, initMenus, okText, cancelText, id, title }: any) => {
6 | const [expandedKeys, setExpandedKeys] = useState([]);
7 | const [checkedKeys, setCheckedKeys] = useState([...initMenus]);
8 | const [selectedKeys, setSelectedKeys] = useState([]);
9 | const [autoExpandParent, setAutoExpandParent] = useState(true);
10 |
11 | const onOkSubmit = async () => {
12 | //分配菜单
13 | const result: any = await updateRoleMenus({ id, mids: checkedKeys });
14 | if (result.data.code === 200) {
15 | message.info("操作成功");
16 | return onOk();
17 | }
18 | message.error("操作失败");
19 | };
20 |
21 | const onExpand = (datas: React.Key[]) => {
22 | setExpandedKeys(datas);
23 | setAutoExpandParent(false);
24 | };
25 |
26 | const onCheck = (checkedKeysValue: React.Key[]) => {
27 | setCheckedKeys(checkedKeysValue);
28 | };
29 |
30 | return (
31 |
32 |
33 | setSelectedKeys(v)}
41 | onCheck={onCheck}
42 | selectedKeys={selectedKeys}
43 | treeData={treeData}
44 | />
45 |
46 |
47 | );
48 | };
49 |
50 | export default RoleAllocationMenuModal;
51 |
--------------------------------------------------------------------------------
/src/components/Modals/RoleAllocationUserModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { updateRoleUsers } from "@/api/microservice/role";
2 | import { message, Modal, Table } from "antd";
3 | import React, { FC, useState } from "react";
4 |
5 | const RoleAllocationUserModal: FC = ({ dataSource, onOk, onCancel, initUIDs, id, okText, cancelText, title }: any) => {
6 | const [selectedRowKeys, setSelectedRowKeys] = useState([...initUIDs]);
7 |
8 | const rowSelection: any = {
9 | type: "checkbox",
10 | selectedRowKeys,
11 | onChange: (keys: React.Key[]) => setSelectedRowKeys(keys)
12 | };
13 |
14 | const onSubmit = async () => {
15 | updateRoleUsers({ id, uids: selectedRowKeys })
16 | .then(res => {
17 | if (res.data.code === 200) {
18 | onOk();
19 | message.info("操作成功");
20 | return;
21 | }
22 | })
23 | .catch(() => message.error("操作失败"));
24 | };
25 |
26 | const columns = [
27 | {
28 | title: "账号",
29 | dataIndex: "username"
30 | },
31 | {
32 | title: "电话",
33 | dataIndex: "mobile"
34 | },
35 | {
36 | title: "email",
37 | dataIndex: "email"
38 | }
39 | ];
40 |
41 | return (
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default RoleAllocationUserModal;
49 |
--------------------------------------------------------------------------------
/src/components/Modals/UserEditModal/index.module.scss:
--------------------------------------------------------------------------------
1 | .avatar {
2 | margin: 0 auto;
3 | border-radius: 50%;
4 | text-align: center;
5 | padding: 10px 0px;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Modals/index.tsx:
--------------------------------------------------------------------------------
1 | import { lazy } from "react";
2 | export const MenuEditModal = lazy(() => import(/* webpackChunkName: "MenuEditModal'"*/ "./MenuEditModal"));
3 | export const MessageModal = lazy(() => import(/* webpackChunkName: "MessageModal'"*/ "./MessageModal"));
4 | export const UserEditModal = lazy(() => import(/* webpackChunkName: "UserEditModal'"*/ "./UserEditModal"));
5 | export const OperationRoleModal = lazy(() => import(/* webpackChunkName: "OperationRoleModal'"*/ "./OperationRoleModal"));
6 | export const PlacardEditModal = lazy(() => import(/* webpackChunkName: "PlacardEditModal'"*/ "./PlacardEditModal"));
7 | export const OperationMenuModal = lazy(() => import(/* webpackChunkName: "OperationMenuModal'"*/ "./OperationMenuModal"));
8 | export const OperationLabelModal = lazy(() => import(/* webpackChunkName: "OperationLabelModal'"*/ "./OperationLabelModal"));
9 | export const RoleAllocationUserModal = lazy(() => import(/* webpackChunkName: "RoleAllocationUserModal'"*/ "./RoleAllocationUserModal"));
10 | export const RoleAllocationMenuModal = lazy(() => import(/* webpackChunkName: "RoleAllocationMenuModal'"*/ "./RoleAllocationMenuModal"));
11 | export const MetaDescModal = lazy(() => import(/* webpackChunkName: "MetaDescModal'"*/ "./MetaDescModal"));
12 |
--------------------------------------------------------------------------------
/src/components/OAuth2/GithubAuth.tsx:
--------------------------------------------------------------------------------
1 | import { GithubOutlined } from "@ant-design/icons";
2 | import { Button } from "antd";
3 | import { useEffect, useState } from "react";
4 |
5 | const GithubAuth = () => {
6 | const [link, setLink] = useState("");
7 | useEffect(() => {
8 | const client_id = "Ov23liFaKrYrVi01nfLN";
9 | const authorize_uri = "https://github.com/login/oauth/authorize";
10 | const redirect_uri = "https://nest-admin.com/login";
11 | setLink(`${authorize_uri}?client_id=${client_id}&scope=user:email&redirect_uri=${redirect_uri}&state=github`);
12 | }, []);
13 | return (
14 |
18 | );
19 | };
20 |
21 | export default GithubAuth;
22 |
--------------------------------------------------------------------------------
/src/components/Owl/close-eyes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/components/Owl/close-eyes.png
--------------------------------------------------------------------------------
/src/components/Owl/face.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/components/Owl/face.png
--------------------------------------------------------------------------------
/src/components/Owl/hand-down-left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/components/Owl/hand-down-left.png
--------------------------------------------------------------------------------
/src/components/Owl/hand-down-right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/components/Owl/hand-down-right.png
--------------------------------------------------------------------------------
/src/components/Owl/hand-up-left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/components/Owl/hand-up-left.png
--------------------------------------------------------------------------------
/src/components/Owl/hand-up-right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weiqinke/react-admin-nest/36c1cf592208959d9c3a771797b706018bcf06b9/src/components/Owl/hand-up-right.png
--------------------------------------------------------------------------------
/src/components/Owl/index.module.scss:
--------------------------------------------------------------------------------
1 | @mixin backgroundImage($url) {
2 | background-image: url($url);
3 | background-repeat: no-repeat;
4 | background-size: 100%;
5 | }
6 | .owlRect {
7 | position: relative;
8 | height: 95px;
9 | background-color: transparent;
10 | z-index: 3;
11 | .position {
12 | position: absolute;
13 | left: 50%;
14 | transform: translateX(-50%);
15 | }
16 | .owlContainer {
17 | position: relative;
18 | width: 120px;
19 | height: 95px;
20 | transform: translateY(12%);
21 | @include backgroundImage("./face.png");
22 | .leftHand,
23 | .rightHand {
24 | z-index: 2;
25 | position: absolute;
26 | width: 45px;
27 | height: 25px;
28 | transition: transform 0.2s linear;
29 | }
30 | .leftHand {
31 | bottom: 3px;
32 | left: -35px;
33 | @include backgroundImage("./hand-down-left.png");
34 | }
35 | .rightHand {
36 | bottom: 3px;
37 | right: -40px;
38 | @include backgroundImage("./hand-down-right.png");
39 | }
40 | .leftHideHand,
41 | .rightHideHand {
42 | z-index: 3;
43 | position: absolute;
44 | width: 50px;
45 | height: 40px;
46 | opacity: 0;
47 | transition: opacity 0.1s linear 0.1s;
48 | }
49 | .leftHideHand {
50 | bottom: 11px;
51 | left: -5px;
52 | @include backgroundImage("./hand-up-left.png");
53 | }
54 | .rightHideHand {
55 | bottom: 11px;
56 | right: 5px;
57 | @include backgroundImage("./hand-up-right.png");
58 | }
59 | .closeEyes {
60 | z-index: 1;
61 | width: 100%;
62 | height: 100%;
63 | opacity: 0;
64 | transition: opacity 0.1s linear 0.1s;
65 | @include backgroundImage("./close-eyes.png");
66 | }
67 | }
68 |
69 | .owlpassword {
70 | .leftHand {
71 | transform: translateX(30px) scale(0) translateY(-10px);
72 | }
73 | .rightHand {
74 | transform: translateX(-40px) scale(0) translateY(-10px);
75 | }
76 | .leftHideHand,
77 | .rightHideHand,
78 | .closeEyes {
79 | opacity: 1;
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/Owl/index.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./index.module.scss";
2 |
3 | const Owl = ({ closeEyes }) => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default Owl;
20 |
--------------------------------------------------------------------------------
/src/components/PageIntroduction/index.module.scss:
--------------------------------------------------------------------------------
1 | .container{
2 | background-color: #fff;
3 | padding: 12px;
4 | user-select: none;
5 | }
6 | .introduction{
7 | margin-top: 12px;
8 | }
--------------------------------------------------------------------------------
/src/components/PageIntroduction/index.tsx:
--------------------------------------------------------------------------------
1 | import { Breadcrumb } from "antd";
2 | import TypingCard from "../TypingCard";
3 |
4 | import styles from "./index.module.scss";
5 | const PageIntroduction = ({ infos, introduction }) => {
6 | return (
7 |
13 | );
14 | };
15 |
16 | export default PageIntroduction;
17 |
--------------------------------------------------------------------------------
/src/components/SocketCard/index.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | margin-top: 54px;
3 | position: relative;
4 | width: 270px;
5 | height: 100px;
6 | background-color: #ccc;
7 | border-radius: 20px;
8 | box-shadow: 0 35px 80px rgba(0, 0, 0, 0.15);
9 | transition: 0.5s linear;
10 | user-select: none;
11 | cursor: pointer;
12 | &:hover {
13 | height: 150px;
14 | .imgbox {
15 | width: 100px;
16 | height: 100px;
17 | }
18 | .content {
19 | .details {
20 | transform: translateY(0px);
21 | }
22 | }
23 | }
24 | }
25 |
26 | .imgbox {
27 | width: 80px;
28 | height: 80px;
29 | position: absolute;
30 | top: -50px;
31 | left: 50%;
32 | transform: translateX(-50%);
33 | border-radius: 20px;
34 | box-shadow: 0 35px 80px rgba(0, 0, 0, 0.35);
35 | background-color: #000;
36 | overflow: hidden;
37 | transition: 0.5s linear;
38 | &:hover {
39 | width: 130px;
40 | height: 130px;
41 | }
42 | }
43 |
44 | .img {
45 | position: absolute;
46 | top: 0;
47 | left: 0;
48 | width: 100%;
49 | height: 100%;
50 | object-fit: cover;
51 | background: url("/src/assets/header/user-poster.png") no-repeat;
52 | background-size: 100% 100%;
53 | }
54 |
55 | .content {
56 | position: absolute;
57 | width: 100%;
58 | height: 100%;
59 | display: flex;
60 | justify-content: center;
61 | align-items: flex-end;
62 | overflow: hidden;
63 | }
64 |
65 | .details {
66 | padding: 0px 40px 10px 40px;
67 | text-align: center;
68 | width: 100%;
69 | transition: 0.5s linear;
70 | transform: translateY(35px);
71 | & {
72 | h2 {
73 | font-size: 1.25em;
74 | font-weight: 600;
75 | color: #555;
76 | line-height: 1.2em;
77 | span {
78 | font-size: 0.75em;
79 | font-size: 500;
80 | opacity: 0.5;
81 | }
82 | }
83 | }
84 | }
85 |
86 | .data {
87 | display: flex;
88 | justify-content: space-between;
89 | margin: 20px 0;
90 |
91 | & {
92 | h3 {
93 | font-size: 1em;
94 | font-weight: 600;
95 | color: #555;
96 | line-height: 1.2em;
97 | span {
98 | font-size: 0.85em;
99 | font-size: 400;
100 | opacity: 0.5;
101 | }
102 | }
103 | }
104 | }
105 |
106 | .actionBtn {
107 | display: flex;
108 | justify-content: center;
109 | gap: 6px;
110 | }
111 |
112 | .btn {
113 | align-self: center; /* 子元素自身在交叉轴上居中对齐 */
114 | }
115 |
--------------------------------------------------------------------------------
/src/components/SocketCard/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "antd";
2 |
3 | import styles from "./index.module.scss";
4 | import { webSocketManager } from "@/utils/ws";
5 |
6 | const SocketCard = ({ userInfo, loadUserInfo, getUserView, ForcedOffline }) => {
7 | const isMe = webSocketManager.MySocketID === userInfo.id;
8 |
9 | return (
10 |
11 |
14 |
15 |
16 |
17 | {userInfo?.nick || userInfo?.name}
18 | {isMe ? "(自己)" : ""}
19 |
20 | Web Front-end developer
21 |
22 |
23 |
26 |
29 | {!isMe && (
30 |
33 | )}
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default SocketCard;
42 |
--------------------------------------------------------------------------------
/src/components/StarrySky/index.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./index.module.scss";
2 | const StarrySky = () => {
3 | return (
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default StarrySky;
29 |
--------------------------------------------------------------------------------
/src/components/SuspendFallback/index.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | height: 100%;
3 | .maxheight {
4 | height: 100vh;
5 | }
6 | .loadingText {
7 | width: 100%;
8 | height: calc(100vh - 288px);
9 | background-color: transparent;
10 | position: relative;
11 | .title {
12 | font-size: 32px;
13 | text-align: center;
14 | position: absolute;
15 | left: 50%;
16 | top: 50%;
17 | transform: translate(-50%, -50%);
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/SuspendFallback/index.tsx:
--------------------------------------------------------------------------------
1 | import { Spin } from "antd";
2 | import styles from "./index.module.scss";
3 |
4 | const SuspendFallback = () => {
5 | return (
6 |
13 | );
14 | };
15 |
16 | export default SuspendFallback;
17 |
--------------------------------------------------------------------------------
/src/components/SystemInfo/index.module.scss:
--------------------------------------------------------------------------------
1 | .itemText {
2 | white-space: nowrap;
3 | text-overflow: ellipsis;
4 | overflow: hidden;
5 | font-size: 14px;
6 | color: #000;
7 | height: 30px;
8 | line-height: 30px;
9 |
10 | em {
11 | color: #000;
12 | font-weight: 500;
13 | font-style: normal;
14 | }
15 | }
16 |
17 | .mem {
18 | text-align: center;
19 | }
20 |
21 | .surplus {
22 | margin-top: 8px;
23 | }
24 |
25 | .cardContainer {
26 | border: 12px;
27 | background-color: #41b3a3;
28 | border-radius: 8px;
29 | border: 2px solid #41b3a3;
30 | &.fense {
31 | background-color: #f8c0cb;
32 | border: 2px solid #f8c0cb;
33 | }
34 |
35 | &.zise {
36 | background-color: #ccbce1;
37 | border: 2px solid #ccbce1;
38 | }
39 |
40 | .header {
41 | width: 70%;
42 | overflow: hidden;
43 | border-radius: 8px 0px 0px 0px;
44 | position: relative;
45 | .tab {
46 | height: 20px;
47 | background-color: #fff;
48 | border-radius: 0px 10px 0px 0px;
49 | transform: perspective(18px) rotateX(12deg) rotateY(0deg);
50 | position: relative;
51 | top: -1px;
52 | left: -15px;
53 | }
54 | .roundNode {
55 | position: absolute;
56 | bottom: 0px;
57 | right: 0px;
58 | width: 10px;
59 | height: 10px;
60 | background: radial-gradient(circle at 100% 0, transparent, transparent 9.5px, #000 10px, #000);
61 | }
62 | }
63 | .body {
64 | min-height: 120px;
65 | border-radius: 0px 8px 8px 8px;
66 | background-color: #fff;
67 | padding: 0px 20px 20px 20px;
68 | .title {
69 | padding: 0px 0px 12px 0px;
70 | font-size: 14px;
71 | font-weight: bold;
72 | }
73 | }
74 | }
75 |
76 | .Gcontainer {
77 | position: relative;
78 | height: 48px;
79 | overflow: hidden;
80 | border-radius: 8px 0px 0 0px;
81 | }
82 | .Ginner {
83 | position: absolute;
84 | user-select: none;
85 | width: 50%;
86 | height: 50px;
87 | background: #fff;
88 | bottom: 0;
89 | border-radius: 0 20px 0 20px;
90 | transform: perspective(40px) scaleX(1.4) scaleY(1.5) rotateX(20deg) translate(-10px, 0);
91 | transform-origin: 50% 100%;
92 | }
93 | .Ginner::before {
94 | content: "";
95 | position: absolute;
96 | right: -10px;
97 | width: 10px;
98 | height: 10px;
99 | top: 40px;
100 | background: radial-gradient(circle at 100% 0, transparent, transparent 9.5px, #fff 10px, #fff);
101 | }
102 | .Gafter {
103 | position: absolute;
104 | width: 150px;
105 | height: 50px;
106 | background: transparent;
107 | bottom: 49px;
108 | right: 0;
109 | border-radius: 20px 0 20px 0;
110 | transform: perspective(40px) scaleX(1.4) scaleY(-1.5) rotateX(20deg) translate(14px, 0);
111 | transform-origin: 53% 100%;
112 | }
113 | .Gafter::before {
114 | content: "";
115 | position: absolute;
116 | left: -10px;
117 | top: 40px;
118 | width: 10px;
119 | height: 10px;
120 | background: radial-gradient(circle at 0 0, transparent, transparent 9.5px, transparent 10px, transparent);
121 | }
122 | .GinnerText,
123 | .GafterText {
124 | user-select: none;
125 | cursor: pointer;
126 | position: absolute;
127 | height: 50px;
128 | line-height: 50px;
129 | text-align: center;
130 | }
131 | .GinnerText {
132 | top: 50%;
133 | transform: translateY(-50%);
134 | }
135 | .GafterText {
136 | top: 50%;
137 | }
138 |
--------------------------------------------------------------------------------
/src/components/TagsView/Contextmenu/index.module.scss:
--------------------------------------------------------------------------------
1 | .contextmenuDom {
2 | user-select: none;
3 | position: fixed;
4 | // 用了wangeditor 需要比编辑器更高层级
5 | z-index: 20000;
6 | list-style: none;
7 | padding: 0px;
8 | margin: 0px;
9 | width: 112px;
10 | background-color: #fff;
11 | border-radius: 8px;
12 | box-shadow: -4px 4px 16px 1px rgba(0, 0, 0, 0.15);
13 | cursor: pointer;
14 |
15 | li:first-child {
16 | border-radius: 8px 8px 0px 0px;
17 | }
18 |
19 | li {
20 | &:first-of-type {
21 | border-radius: 8px 8px 0px 0px;
22 | }
23 | width: 100%;
24 | text-align: center;
25 | font-size: 14px;
26 | height: 40px;
27 | line-height: 40px;
28 | &:hover {
29 | background-color: #41b3a3;
30 | color: #313653;
31 | transition:
32 | opacity 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
33 | width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
34 | }
35 | }
36 | li:last-child {
37 | border-radius: 0px 0px 8px 8px;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/TagsView/Contextmenu/index.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./index.module.scss";
2 |
3 | const Contextmenu = ({ pageclientX, pageclientY, handleCloseLeftTags, handleCloseRightTags, handleCloseOtherTags, contextMenuTag, refreshPage }) => {
4 | return (
5 |
6 | - 关闭左侧
7 | - 关闭右侧
8 | - 关闭其他
9 | {contextMenuTag?.active ? - 刷新页面
: ""}
10 |
11 | );
12 | };
13 |
14 | export default Contextmenu;
15 |
--------------------------------------------------------------------------------
/src/components/TagsView/MenuTag/index.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 2px 20px 2px 16px;
3 | margin: 0px;
4 | border-bottom: 2px solid #000;
5 | border-radius: 4px 4px 0 0;
6 | background: #fafafa;
7 | white-space: nowrap;
8 | user-select: none;
9 | position: relative;
10 | }
11 |
12 | .activeContainer {
13 | @extend .container;
14 | border-bottom: 2px solid #41b3a3;
15 | }
16 |
17 | .round {
18 | position: absolute;
19 | top: 9px;
20 | left: 7px;
21 | width: 9px;
22 | height: 9px;
23 | background: #ccc;
24 | border-radius: 50%;
25 | }
26 |
27 | .activeRound {
28 | @extend .round;
29 | background: #41b3a3;
30 | }
31 |
32 | .name {
33 | color: #575757;
34 | margin: 0 5px;
35 | &:hover {
36 | color: #313653;
37 | opacity: 0.85;
38 | }
39 | }
40 |
41 | .close {
42 | background: transparent no-repeat 100% 100%;
43 | background-size: cover;
44 | border-radius: 50%;
45 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAHhlWElmTU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAIdpAAQAAAABAAAATgAAAAAAAACQAAAAAQAAAJAAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAACCgAwAEAAAAAQAAACAAAAAAfgvaUgAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAAalJREFUWAntlr9OwzAQxm2XAV4DIfEQTFHE2p2B5+hSiQ5d+jh0BKlZUJ6i8BxUUBt/up5kOU7iM38W4sWxc3ffz2dfYqWm9t8zoOME7HYv1859Lo3Re6Xspqqq99hGMm6a5lwps7DWXSl1XNV1/Rr6n4UDerYPWus755TS2tz4APNSiLZtLw6Hj0cf69bHVM7NIHEfappwgGfn3BvPwdHTb2kVPJvXw4fF2YOyyiPqOwDG2LWHfWazEohT2rfkS5Eopt1wXO47ZwAvBgKMbofUNwlQCiEVh04vgBSiRHwUIBeiVDwLYAwC71EpiQM3el7gObgFMODWv0qULsqV2um0Z4nDIxsAxikIzHOTisNPBACHPogSccTrfIgw+ZdNlIG+1TNwSRayM9Aj/kSihFDy2c7KQEqcV0vSv1iGQ+L8m86x4W2K+8EMSAJLbEOIXoCSgCU+SYCSQLwqqW8HQBqAhcNeEiNRhmbxnW87QOhw2nlcotaaZQiK5w4A3V7JjEuNT3vsPDROQfiL6WXsk7gVH1e4vf7EtRwQfjt8JuharvVsHQNM4ykDX94FYhBKOJraAAAAAElFTkSuQmCC);
46 | width: 16px;
47 | height: 16px;
48 |
49 | margin-top: 3px;
50 | position: absolute;
51 | top: 3px;
52 | &:hover {
53 | background-color: #1772f6;
54 | }
55 | }
56 |
57 | .bottomline {
58 | width: 100%;
59 | height: 4px;
60 | background: #fafafa;
61 | border-radius: 0px 0px 100px 100px;
62 | }
63 |
64 | .contextb {
65 | z-index: -1;
66 | position: absolute;
67 | width: calc(100% + 5px);
68 | height: 100%;
69 | top: 0px;
70 | left: -5px;
71 | }
72 |
--------------------------------------------------------------------------------
/src/components/TagsView/MenuTag/index.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./index.module.scss";
2 |
3 | const MenuTag = ({ tag, onClose }) => {
4 | const className = tag?.active ? styles.activeContainer : styles.container;
5 | return (
6 |
7 |
8 | {tag.name}
9 | onClose(e, tag)}>
10 |
11 | );
12 | };
13 |
14 | export default MenuTag;
15 |
--------------------------------------------------------------------------------
/src/components/TagsView/index.module.scss:
--------------------------------------------------------------------------------
1 | .contextb {
2 | z-index: -1;
3 | position: absolute;
4 | width: calc(100% + 5px);
5 | height: 100%;
6 | top: 0px;
7 | left: -5px;
8 | }
9 |
10 | :global {
11 | .anticon.anticon-close {
12 | color: #000;
13 | }
14 | }
15 |
16 | .contextmenuDom {
17 | user-select: none;
18 | position: fixed;
19 | // 用了wangeditor 需要比编辑器更高层级
20 | z-index: 20000;
21 | list-style: none;
22 | padding: 0px;
23 | margin: 0px;
24 | width: 112px;
25 | background-color: #fff;
26 | border-radius: 8px;
27 | box-shadow: -4px 4px 16px 1px rgba(0, 0, 0, 0.15);
28 | cursor: pointer;
29 |
30 | li:first-child {
31 | border-radius: 8px 8px 0px 0px;
32 | }
33 |
34 | li {
35 | &:first-of-type {
36 | border-radius: 8px 8px 0px 0px;
37 | }
38 | width: 100%;
39 | text-align: center;
40 | font-size: 14px;
41 | height: 40px;
42 | line-height: 40px;
43 | &:hover {
44 | background-color: #41b3a3;
45 | color: #313653;
46 | transition:
47 | opacity 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
48 | width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
49 | }
50 | }
51 | li:last-child {
52 | border-radius: 0px 0px 8px 8px;
53 | }
54 | }
55 |
56 | .tagsdiv {
57 | background: #fff;
58 | padding: 6px 4px 0px 4px;
59 | :global {
60 | .ant-tabs-nav-list {
61 | padding: 0;
62 |
63 | &:last-child{
64 | margin-right: auto;
65 | }
66 | .ant-tabs-tab {
67 | background: #fafafa;
68 | border: 1px solid #e8e8e8;
69 | padding: 0px;
70 | margin: 0px;
71 | margin-right: 2px;
72 | }
73 | .ant-tabs-tab-active.ant-tabs-tab {
74 | .anticon.anticon-close {
75 | &:hover {
76 | color: #1890ff;
77 | }
78 | }
79 | }
80 | .ant-tabs-ink-bar {
81 | background: transparent;
82 | }
83 | }
84 | }
85 | }
86 |
87 | .DropChild {
88 | white-space: nowrap;
89 | overflow-x: scroll;
90 | &::-webkit-scrollbar {
91 | /*滚动条整体样式*/
92 | height: 5px;
93 | }
94 | &::-webkit-scrollbar-thumb {
95 | /*滚动条里面小方块*/
96 | border-radius: 10px;
97 | box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
98 | background: #aeaeae;
99 | }
100 | &::-webkit-scrollbar-track {
101 | /*滚动条里面轨道*/
102 | box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
103 | border-radius: 10px;
104 | background: #ededed;
105 | }
106 |
107 | &::after {
108 | content: "";
109 | display: table;
110 | clear: both;
111 | }
112 | }
113 | .tagitem {
114 | margin: 0px;
115 | padding: 0px;
116 | background: transparent;
117 | .tagChildDiv {
118 | padding: 10px 0px 10px 16px;
119 | margin: 0px;
120 | margin-right: 2px;
121 | border: 1px solid #000;
122 | border-bottom: 2px solid #000;
123 | border-radius: 2px;
124 | background: #fafafa;
125 | line-height: 22px;
126 | white-space: nowrap;
127 | b.round {
128 | float: left;
129 | width: 9px;
130 | height: 9px;
131 | background: #ccc;
132 | border-radius: 50%;
133 | margin-top: 7px;
134 | margin-left: 0px;
135 | margin-right: 5px;
136 | }
137 | b.name {
138 | color: #575757;
139 | &:hover {
140 | color: #313653;
141 | opacity: 0.85;
142 | }
143 | }
144 | .bottomline {
145 | width: 100%;
146 | height: 4px;
147 | background: #fafafa;
148 | border-radius: 0px 0px 100px 100px;
149 | }
150 | .close {
151 | color: #898989;
152 | margin-left: 12px;
153 | margin-top: 5px;
154 | margin-right: 12px;
155 | &:hover {
156 | color: #313653;
157 | }
158 | }
159 | }
160 | }
161 |
162 | .ant-tabs-nav-list {
163 | background-color: #000;
164 | }
165 |
--------------------------------------------------------------------------------
/src/components/Translatex/index.tsx:
--------------------------------------------------------------------------------
1 | import { animated, useSpring } from "@react-spring/web";
2 | import React, { useEffect } from "react";
3 |
4 | const Translatex = props => {
5 | const [animate, setAnimate] = React.useState(false);
6 | const {
7 | children,
8 | direction = "left",
9 | delay = 3000,
10 | run = false,
11 | config = {
12 | tension: 100,
13 | friction: 26
14 | }
15 | } = props;
16 |
17 | const animateStyles = useSpring({
18 | opacity: animate ? 1 : 0,
19 | transform: animate
20 | ? `${direction === "left" ? "translateX(0px) scale(1) rotateY(0deg)" : "translateY(0px) scale(1) rotateY(0deg)"}`
21 | : `${direction === "left" ? "translateX(80px) scale(1) rotateY(10deg)" : "translateY(80px) scale(0.9) rotateY(10deg)"}`,
22 | delay,
23 | config: { ...config },
24 | onRest: () => {}
25 | });
26 |
27 | useEffect(() => {
28 | setAnimate(run);
29 | }, [run]);
30 |
31 | return (
32 |
33 | {run && children}
34 |
35 | );
36 | };
37 |
38 | export default Translatex;
39 |
--------------------------------------------------------------------------------
/src/components/TypingCard/Typing.tsx:
--------------------------------------------------------------------------------
1 | class Typing {
2 | public opts;
3 | public source;
4 | public output;
5 | public delay: number;
6 | public chain;
7 | public element;
8 | public doneState: boolean;
9 | public timer;
10 |
11 | constructor(opts) {
12 | this.opts = opts || {};
13 | const element = document.createElement("div");
14 | element.innerHTML = opts.data;
15 | this.element = element;
16 | this.output = opts.output;
17 | this.delay = opts.delay || 120;
18 | this.chain = {
19 | parent: null,
20 | dom: this.output,
21 | val: []
22 | };
23 | this.doneState = false;
24 | }
25 |
26 | convert(dom, arr) {
27 | //将dom节点的子节点转换成数组,
28 | const children: HTMLElement[] = Array.from(dom.childNodes);
29 | for (let i = 0; i < children.length; i++) {
30 | const node = children[i];
31 | if (node.nodeType === 3) {
32 | arr = arr.concat(node.nodeValue.split("")); //将字符串转换成字符串数组,后面打印时才会一个一个的打印
33 | } else if (node.nodeType === 1) {
34 | let val = [];
35 | val = this.convert(node, val);
36 | arr.push({
37 | dom: node,
38 | val: val
39 | });
40 | }
41 | }
42 | return arr;
43 | }
44 | print(dom, val, callback) {
45 | clearTimeout(this.timer);
46 | this.timer = null;
47 | this.timer = setTimeout(function () {
48 | if (dom.current) {
49 | dom.current.appendChild(document.createTextNode(val));
50 | } else {
51 | if (!dom.appendChild) {
52 | return;
53 | }
54 | dom.appendChild(document.createTextNode(val));
55 | }
56 | callback();
57 | }, this.delay);
58 | }
59 | play(ele) {
60 | if (this.doneState) return;
61 | //当打印最后一个字符时,动画完毕,执行done
62 | if (!ele.val.length) {
63 | if (ele.parent) this.play(ele.parent);
64 | else this.done();
65 | return;
66 | }
67 | const current = ele.val.shift(); //获取第一个元素,同时删除数组中的第一个元素
68 | if (typeof current === "string") {
69 | this.print(ele.dom, current, () => {
70 | this.play(ele); //继续打印下一个字符
71 | });
72 | } else {
73 | const dom = current.dom.cloneNode(); //克隆节点,不克隆节点的子节点,所以不用加参数true
74 | if (ele.dom.current) {
75 | ele.dom.current.appendChild(dom);
76 | } else {
77 | ele.dom.appendChild(dom);
78 | }
79 | this.play({
80 | parent: ele,
81 | dom,
82 | val: current.val
83 | });
84 | }
85 | }
86 | start() {
87 | //初始化函数
88 | this.chain.val = this.convert(this.element, this.chain.val);
89 | clearTimeout(this.timer);
90 | this.timer = null;
91 | this.output.current.innerHTML = "";
92 | this.play(this.chain);
93 | }
94 | done() {
95 | this.doneState = true;
96 | clearTimeout(this.timer);
97 | this.timer = null;
98 | }
99 | }
100 |
101 | export default Typing;
102 |
--------------------------------------------------------------------------------
/src/components/TypingCard/index.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from "react";
2 | import Typing from "./Typing";
3 |
4 | const TypingCard = (props: any) => {
5 | const outputEl: any = useRef();
6 |
7 | useEffect(() => {
8 | const typing: any = new Typing({
9 | data: props.source,
10 | output: outputEl,
11 | delay: 50
12 | });
13 | typing.start();
14 | return () => {
15 | typing.done();
16 | };
17 | }, [props]);
18 |
19 | return (
20 |
23 | );
24 | };
25 |
26 | export default TypingCard;
27 |
--------------------------------------------------------------------------------
/src/components/UserCard/index.module.scss:
--------------------------------------------------------------------------------
1 | $base-url: "@/assets/header/user-poster.png";
2 | $avatarbg: "@/assets/user/avatarbg.png";
3 |
4 | .usercard {
5 | width: 100%;
6 | .antskeleton {
7 | width: 100% !important;
8 | }
9 | .antskeletonimage {
10 | width: 100% !important;
11 | }
12 | .antcardcover {
13 | max-height: 200px;
14 | overflow: hidden;
15 | }
16 | }
17 |
18 | .userCardBg {
19 | width: 100%;
20 | min-height: 100px;
21 | position: relative;
22 | // background-image: url($base-url);
23 | background: linear-gradient(135deg, #292a3a, #536976);
24 | }
25 |
26 | .avatarbg {
27 | width: 150px;
28 | height: 150px;
29 | position: absolute;
30 | top: -50px;
31 | transform: translateX(-50%);
32 | left: 50%;
33 | background: url($avatarbg) no-repeat;
34 | background-size: 100% 100%;
35 | }
36 |
37 | .avatar {
38 | position: absolute;
39 | top: -50px;
40 | transform: translateX(-50%);
41 | left: 50%;
42 | }
43 |
44 | .img {
45 | width: 84px;
46 | height: 84px;
47 | border-radius: 50%;
48 | overflow: hidden;
49 | position: absolute;
50 | transform: translate(-50%, -9%);
51 | left: 50%;
52 | }
53 |
54 | .cover {
55 | position: relative;
56 | background: #fff;
57 | &:before {
58 | content: "";
59 | top: 0;
60 | left: 0;
61 | width: 100%;
62 | height: 100%;
63 | z-index: 1;
64 | position: absolute;
65 | }
66 | }
67 |
68 | .userinfo {
69 | position: relative;
70 | padding-top: 38px;
71 | &::after {
72 | content: "";
73 | display: table;
74 | clear: both;
75 | }
76 | .info {
77 | display: flex;
78 | flex-direction: column;
79 | align-items: center;
80 | justify-content: center;
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/UserCard/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Card, Flex, Typography } from "antd";
2 | import React, { useEffect, useState } from "react";
3 | import type { CardProps } from "antd/es/card";
4 | import CountUp from "react-countup";
5 | import { GithubOutlined, LikeOutlined, MailOutlined } from "@ant-design/icons";
6 | import Translatex from "../Translatex";
7 | import avatar from "@/assets/user/defaultAvatar.jpg";
8 |
9 | import styles from "./index.module.scss";
10 |
11 | const { Text, Title } = Typography;
12 |
13 | export interface UserCardProp extends CardProps {
14 | children?: React.ReactNode;
15 | info?: any;
16 | index?: number;
17 | }
18 |
19 | export const UserFooter: React.FC<{ name: string; count: number }> = ({ name, count }) => (
20 |
21 | {name}
22 |
23 |
24 |
25 |
26 | );
27 |
28 | const UserCard: React.FC = ({
29 | info = {
30 | id: 1,
31 | name: "User",
32 | description: "description",
33 | follwer: 0,
34 | mits: 0,
35 | total: 0,
36 | forbid: false
37 | },
38 | index = 1
39 | }) => {
40 | const [loading, setLoading] = useState(true);
41 | useEffect(() => {
42 | const timer = setTimeout(() => {
43 | setLoading(false);
44 | }, 1000);
45 | return () => {
46 | clearTimeout(timer);
47 | };
48 | }, []);
49 |
50 | return (
51 |
52 | }
55 | actions={[
56 | ,
57 | ,
58 |
59 | ]}>
60 |
61 |
62 |

63 |
64 |
65 |
66 |
67 | {info.name}
68 |
69 | {info.description}
70 |
71 | } />
72 | } />
73 | } />
74 |
75 |
76 |
77 |
78 |
79 | );
80 | };
81 |
82 | export default UserCard;
83 |
--------------------------------------------------------------------------------
/src/components/UserInfoCard/index.module.scss:
--------------------------------------------------------------------------------
1 | .page {
2 | padding: 0;
3 | }
4 |
5 | .cover {
6 | max-height: 300px;
7 | position: relative;
8 | }
9 |
10 | .segm {
11 | width: 100%;
12 | vertical-align: right;
13 | padding: 6px;
14 | background-color: rgba(163, 163, 163, 0.54);
15 | &::after {
16 | content: "";
17 | display: table;
18 | clear: both;
19 | }
20 | }
21 |
22 | .right {
23 | float: right;
24 | }
25 |
--------------------------------------------------------------------------------
/src/contexts/MenuTagContext.tsx:
--------------------------------------------------------------------------------
1 | import { getMenuItemByID, getMenuItemByUrl } from "@/utils/menuUtils";
2 | import React, { useEffect, useState } from "react";
3 | import { useLocation, useNavigate } from "react-router-dom";
4 |
5 | const MenuTagContext = React.createContext({
6 | tags: [],
7 | setTags: () => null,
8 | // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
9 | addTag: (urls: any, key: any) => null,
10 | activeUrl: "",
11 | setActiveUrl: () => null,
12 | tagPlanVisible: "",
13 | setTagPlanVisible: () => null,
14 | refresh: true,
15 | setRefresh: () => null
16 | });
17 |
18 | type MenuTagProps = {
19 | url: string;
20 | closable: boolean;
21 | name: string;
22 | active: boolean;
23 | };
24 |
25 | export const MenuTagContextProvider = ({ children }) => {
26 | const location = useLocation();
27 | const navigate = useNavigate();
28 |
29 | const [tags, setTags] = useState([]);
30 | const [activeUrl, setActiveUrl] = useState("");
31 | const [tagPlanVisible, setTagPlanVisible] = useState("");
32 | const [refresh, setRefresh] = useState(false);
33 |
34 | const addTag = (tag, id) => {
35 | const url = tag.join("/");
36 | const path = `/${url}`;
37 | const isRepeat = tags.find(v => v.url === path);
38 | if (isRepeat) {
39 | setTags(v => {
40 | return v.map(d => {
41 | d.active = d.url === path;
42 | return d;
43 | });
44 | });
45 | } else {
46 | const item = getMenuItemByID(id);
47 | // 如果没有设置该菜单,就不添加标签了
48 | if (!item) return;
49 | const res = tags.map(v => {
50 | v.active = false;
51 | return v;
52 | });
53 | setTags([
54 | ...res,
55 | {
56 | url: path,
57 | closable: false,
58 | name: item.name,
59 | active: true
60 | }
61 | ]);
62 | }
63 | };
64 |
65 | useEffect(() => {
66 | if (!tags.length) {
67 | const url = location?.pathname;
68 | const initItem = getMenuItemByUrl(url.split("/").pop());
69 | setTags([
70 | {
71 | url,
72 | closable: false,
73 | name: initItem?.name,
74 | active: true
75 | }
76 | ]);
77 | }
78 | // eslint-disable-next-line react-hooks/exhaustive-deps
79 | }, [location?.pathname]);
80 |
81 | useEffect(() => {
82 | if (activeUrl) navigate(activeUrl);
83 | // eslint-disable-next-line react-hooks/exhaustive-deps
84 | }, [activeUrl]);
85 |
86 | return (
87 |
99 | {children}
100 |
101 | );
102 | };
103 |
104 | export default MenuTagContext;
105 |
--------------------------------------------------------------------------------
/src/contexts/ProjectContext.tsx:
--------------------------------------------------------------------------------
1 | import { userinfo } from "@/api/microservice/user";
2 | import { getUserState } from "@/utils/core";
3 | import axios from "axios";
4 | import React, { useEffect, useState } from "react";
5 |
6 | const Cancel = axios.CancelToken;
7 | const source = Cancel.source();
8 |
9 | const ProjectContext: any = React.createContext({
10 | profile: {},
11 | setProfile: () => null,
12 | value: {},
13 | setValue: () => null,
14 | theme: {},
15 | setTheme: () => null
16 | });
17 |
18 | export const ProjectContextProvider = ({ theme, setTheme, children }) => {
19 | const [value, setValue] = useState(getUserState());
20 | const [profile, setProfile] = useState({});
21 |
22 | useEffect(() => {
23 | if (value.uid) {
24 | userinfo({ uid: value.uid, cancelToken: source.token }).then(res => {
25 | setProfile(res.data.data.profile);
26 | });
27 | }
28 | return () => {
29 | source.cancel();
30 | };
31 | }, [value]);
32 |
33 | return {children};
34 | };
35 |
36 | export default ProjectContext;
37 |
--------------------------------------------------------------------------------
/src/hooks/useCalculativeWidth.tsx:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect, useState } from "react";
2 |
3 | /**根据父节点的ref和子元素的列数等数据,计算出子元素的宽度。用于响应式布局
4 | * @param fatherRef 父节点的ref
5 | * @param marginX 子元素的水平间距
6 | * @param cols 一行个数 (一行有几列)
7 | * @param callback 根据浏览器宽度自动计算大小后的回调函数,参数是计算好的子元素宽度
8 | * @returns 返回子元素宽度的响应式数据
9 | */
10 | const useCalculativeWidth = (fatherRef: RefObject, marginX: number, cols: number, callback?: (nowWidth: number) => void) => {
11 | const [itemWidth, setItemWidth] = useState(50);
12 | useEffect(() => {
13 | /**计算单个子元素宽度,根据list的宽度计算 */
14 | const countWidth = () => {
15 | const width = fatherRef.current?.offsetWidth;
16 | if (width) {
17 | const _width = (width - marginX * (cols + 1)) / cols;
18 | setItemWidth(_width);
19 | callback && callback(_width);
20 | }
21 | };
22 | countWidth(); //先执行一次,后续再监听绑定
23 | window.addEventListener("resize", countWidth);
24 | return () => window.removeEventListener("resize", countWidth);
25 | }, []);
26 | return itemWidth;
27 | };
28 |
29 | export default useCalculativeWidth;
30 |
--------------------------------------------------------------------------------
/src/layout/DefaultLayout/index.module.scss:
--------------------------------------------------------------------------------
1 | .layout {
2 | width: 100vw;
3 | background-color: #f0f2f5;
4 | }
5 |
6 | .content {
7 | background-color: #f0f2f5;
8 | }
9 |
10 | .sider {
11 | height: calc(100vh - 52px);
12 | overflow: auto;
13 | user-select: none;
14 | }
15 |
16 | .pageContent {
17 | overflow: auto;
18 | height: 100%;
19 | }
20 |
21 | .outletContainer {
22 | height: calc(100vh - 88px);
23 | overflow: auto;
24 | }
25 |
26 | .pageOutlet {
27 | min-height: calc(100vh - 110px);
28 | border-left: 4px solid #fff;
29 | :global {
30 | .transformOutletitem-enter {
31 | opacity: 0;
32 | transform: translateX(100%);
33 | }
34 | .transformOutletitem-enter-done {
35 | opacity: 1;
36 | transform: translateX(0%);
37 | transition: transform 300ms ease;
38 | }
39 | .transformOutletitem-exit {
40 | opacity: 0;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/layout/DefaultLayout/index.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorBoundary } from "@/components/ErrorBoundary";
2 | import Copyright from "@/components/Footer/Copyright";
3 | import DefaultLayoutHeader from "@/components/Header/DefaultLayoutHeader";
4 | import WebMenu from "@/components/Menu/WebMenu";
5 | import TagsView from "@/components/TagsView";
6 | import MenuTagContext from "@/contexts/MenuTagContext";
7 | import FailContainer from "@/pages/FailPage/FailContainer";
8 | import { Layout } from "antd";
9 | import { throttle } from "lodash-es";
10 | import { Suspense, useContext, useEffect, useState } from "react";
11 | import { CSSTransition, SwitchTransition } from "react-transition-group";
12 | import { Outlet, useLocation } from "react-router-dom";
13 | import styles from "./index.module.scss";
14 | import SuspendFallback from "@/components/SuspendFallback";
15 |
16 | const { Content, Sider } = Layout;
17 |
18 | const DefaultLayout = () => {
19 | const location = useLocation();
20 | const { setTagPlanVisible, refresh } = useContext(MenuTagContext);
21 | const [collapsed, setCollapsed] = useState(false);
22 |
23 | useEffect(() => {
24 | const resize = throttle(
25 | () => {
26 | const winW = document.body.clientWidth;
27 | setCollapsed(winW < 600);
28 | },
29 | 500,
30 | { trailing: true, leading: false }
31 | );
32 | resize();
33 | const clickRemove = () => setTagPlanVisible();
34 | window.addEventListener("click", clickRemove);
35 |
36 | window.addEventListener("resize", resize);
37 | return () => {
38 | window && window.removeEventListener("resize", resize);
39 | window.removeEventListener("click", clickRemove);
40 | };
41 | }, [setTagPlanVisible]);
42 |
43 | return (
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | }>
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | );
80 | };
81 |
82 | export default DefaultLayout;
83 |
--------------------------------------------------------------------------------
/src/layout/ProjectLayout/index.tsx:
--------------------------------------------------------------------------------
1 | import { MenuTagContextProvider } from "@/contexts/MenuTagContext";
2 | import { ProjectContextProvider } from "@/contexts/ProjectContext";
3 | import { webSocketManager } from "@/utils/ws";
4 | import { ConfigProvider } from "antd";
5 | import { useEffect, useLayoutEffect, useState } from "react";
6 | import { Outlet } from "react-router-dom";
7 |
8 | const ProjectLayout = () => {
9 | const [theme, setTheme] = useState(localStorage.getItem("data-theme") || "dark");
10 | useEffect(() => {
11 | webSocketManager.create();
12 | return () => {
13 | webSocketManager.postMessage({
14 | name: "qkstartCar",
15 | type: "123",
16 | message: "",
17 | data: []
18 | });
19 | webSocketManager.socket.disconnect();
20 | webSocketManager.close();
21 | };
22 | }, []);
23 |
24 | useLayoutEffect(() => {
25 | localStorage.setItem("data-theme", theme);
26 | document.body.setAttribute("data-theme", theme);
27 | if (theme === "dark") {
28 | setMenusConfig({
29 | darkItemSelectedBg: "#41b3a3",
30 | // 选中时菜单颜色 和父级选中颜色
31 | darkItemSelectedColor: "#fafafc",
32 | // 未选中的菜单项颜色
33 | darkItemColor: "#fafafc",
34 | darkGroupTitleColor: "#000"
35 | });
36 | setButtonConfig({
37 | defaultBg: "#41b3a3",
38 | primaryColor: "#fff"
39 | });
40 | } else {
41 | // 粉色背景
42 | setMenusConfig({
43 | darkItemSelectedBg: "#fedce0",
44 | darkPopupBg: "#fedce0",
45 | darkSubMenuItemBg: "#000",
46 | darkItemBg: "#fedce0",
47 | // 选中时菜单颜色 和父级选中颜色
48 | darkItemSelectedColor: "#fafafc",
49 | darkItemBg: "#001529",
50 | // 未选中的菜单项颜色
51 | darkItemColor: "#fafafc",
52 | darkGroupTitleColor: "#000"
53 | });
54 |
55 | setButtonConfig({
56 | defaultBg: "#fedce0",
57 | primaryColor: "#fff"
58 | });
59 | }
60 | }, [theme]);
61 |
62 | const [menusConfig, setMenusConfig] = useState({
63 | darkItemSelectedBg: "#41b3a3"
64 | });
65 |
66 | const [ButtonConfig, setButtonConfig] = useState({
67 | defaultBg: "#41b3a3",
68 | primaryColor: "#fff"
69 | });
70 |
71 | return (
72 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | );
88 | };
89 |
90 | export default ProjectLayout;
91 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import ProjectRouter from "./router/index.tsx";
4 | import { RouterProvider } from "react-router-dom";
5 |
6 | import "./styles/reset.css";
7 |
8 | const router = ProjectRouter();
9 |
10 | ReactDOM.createRoot(document.getElementById("caravan-root")!).render(
11 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/src/pages/About/index.module.scss:
--------------------------------------------------------------------------------
1 | .card {
2 | background-color: #fff;
3 | padding: 12px;
4 | margin: 12px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/pages/About/index.tsx:
--------------------------------------------------------------------------------
1 | import TypingCard from "@/components/TypingCard";
2 | import { Descriptions } from "antd";
3 | import { Link } from "react-router-dom";
4 | import styles from "./index.module.scss";
5 |
6 | const About = () => {
7 | const { dependencies, devDependencies, version, lastBuildTime } = REACT_PACKAGE;
8 |
9 | const depResult = [];
10 | for (const key in dependencies) {
11 | depResult.push({
12 | key,
13 | label: key,
14 | children: dependencies[key]
15 | });
16 | }
17 |
18 | const devResult = [];
19 | for (const key in devDependencies) {
20 | devResult.push({
21 | key,
22 | label: key,
23 | children: devDependencies[key]
24 | });
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 |
36 |
37 |
38 |
39 |
56 | 仓库地址
57 |
58 | )
59 | },
60 | {
61 | label: "预览地址",
62 | children: (
63 |
64 | 预览地址
65 |
66 | )
67 | }
68 | ]}
69 | />
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | );
79 | };
80 |
81 | export default About;
82 |
--------------------------------------------------------------------------------
/src/pages/Account/AccountBind/AccountBind.tsx:
--------------------------------------------------------------------------------
1 | import { AlipayCircleOutlined, DingdingOutlined, TaobaoCircleOutlined } from "@ant-design/icons";
2 | import { Divider, List } from "antd";
3 | import React, { useEffect } from "react";
4 |
5 | const AccountBind: React.FC = function () {
6 | useEffect(() => {}, []);
7 | return (
8 |
9 |
10 | 账号设置
11 |
12 |
13 | 绑定]}>
14 | }
16 | title={绑定淘宝}
17 | description={当前未绑定淘宝账号}
18 | />
19 |
20 | 绑定]}>
21 | }
23 | title={绑定支付宝}
24 | description={当前未绑定支付宝账号}
25 | />
26 |
27 | 绑定]}>
28 | }
30 | title={绑定钉钉}
31 | description={当前未绑定钉钉账号}
32 | />
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default AccountBind;
40 |
--------------------------------------------------------------------------------
/src/pages/Account/AccountBind/index.module.scss:
--------------------------------------------------------------------------------
1 | .AccountBind {
2 | .bangdinghao {
3 | padding-right: 12px;
4 | font-size: 17px;
5 | .font17 {
6 | font-size: 17px;
7 | }
8 | .playicon {
9 | font-size: 40px;
10 | margin-top: 12px;
11 | }
12 | .ant-list-item-action {
13 | li {
14 | a {
15 | font-size: 17px;
16 | }
17 | }
18 | }
19 | }
20 | .list {
21 | margin-bottom: 10px;
22 | display: flex;
23 | border-bottom: 1px solid #e6e6e6;
24 | font-size: 17px;
25 | .left {
26 | flex: 1;
27 | }
28 | .xiugaitype {
29 | font-size: 17px;
30 | line-height: 70px;
31 | margin-right: 12px;
32 | }
33 | .offstate {
34 | color: #999;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/pages/Account/AccountPage/accountpage.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect } from "react";
2 | import { Form, Input, Button, Divider } from "antd";
3 | const AccountPage: FC = () => {
4 | const [form] = Form.useForm();
5 | const handleUpdateUser = () => {};
6 | const userInfo: any = {
7 | loginName: ""
8 | };
9 | useEffect(() => {}, []);
10 |
11 | return (
12 |
13 |
14 | 修改密码
15 |
16 |
26 |
27 |
28 |
41 |
42 |
43 |
44 |
45 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default AccountPage;
55 |
--------------------------------------------------------------------------------
/src/pages/Account/AccountSetting/AccountSetting.tsx:
--------------------------------------------------------------------------------
1 | import { Divider, Tag } from "antd";
2 | import React, { useEffect } from "react";
3 |
4 | const AccountSetting: React.FC = function () {
5 | useEffect(() => {}, []);
6 | return (
7 |
8 |
9 | 账号设置
10 |
11 |
12 |
13 |
账户密码
14 |
15 | 当前强度:强
16 |
17 |
18 |
19 | 修改
20 |
21 |
22 |
23 |
24 |
密保手机
25 |
26 | 已绑定手机:
27 | 138****8293
28 |
29 |
30 |
31 | 验证
32 |
33 |
34 | 换绑
35 |
36 |
37 |
38 |
39 |
密保问题
40 |
41 | 未设置密保问题,密保问题可有效保护账户安全
42 |
43 |
44 |
45 | 修改
46 |
47 |
48 |
49 |
50 |
备用邮箱
51 |
52 | 已绑定邮箱:
53 | ant***sign.com
54 |
55 |
56 |
57 | 修改
58 |
59 |
60 |
61 |
62 |
备用邮箱
63 |
64 | MFA 设备:
65 | 未绑定 MFA 设备,绑定后,可以进行二次确认
66 |
67 |
68 |
69 | 绑定
70 |
71 |
72 | 启用
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | export default AccountSetting;
80 |
--------------------------------------------------------------------------------
/src/pages/Account/AccountSetting/index.module.scss:
--------------------------------------------------------------------------------
1 | .AccountSetting {
2 | .list {
3 | margin-bottom: 10px;
4 | display: flex;
5 | border-bottom: 1px solid #e6e6e6;
6 | font-size: 17px;
7 | .left {
8 | flex: 1;
9 | }
10 | .xiugaitype {
11 | color: #0095ff;
12 | font-size: 17px;
13 | line-height: 70px;
14 | margin-right: 12px;
15 | }
16 | .offstate {
17 | color: #999;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/pages/Account/Base/BaseSetting.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 个人中心
3 | */
4 | import React, { FC, useEffect, useState } from "react";
5 | import { Avatar, Card, Col, Divider, Row } from "antd";
6 | import { getmyuserinfo, updateMyinfo } from "@/api/caravan/User";
7 | import userPoster from "@/assets/header/user-poster.png";
8 | import MetaDesc from "./MetaDesc";
9 | import BaseFrom from "./basefrom";
10 |
11 | import "./style.scss";
12 |
13 | const { Meta } = Card;
14 |
15 | const BaseSetting: FC = function () {
16 | const [initFormItem, setInitFormItem] = useState({});
17 | const [needaxios, setNeedAxios] = useState(1);
18 | const pendingCallback = async (formdata: any) => {
19 | const result = await updateMyinfo(formdata);
20 | if (result.data.code === 200) {
21 | setNeedAxios(needaxios + 1);
22 | }
23 | };
24 | useEffect(() => {
25 | getmyuserinfo().then(result => {
26 | if (result.data.code === 200) {
27 | setInitFormItem(result.data.data);
28 | }
29 | });
30 | }, [needaxios]);
31 | return (
32 |
33 |
34 |
35 | 个人中心
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | }>
45 | }
47 | description={}
48 | />
49 |
50 |
51 |
52 |
53 | );
54 | };
55 | export default BaseSetting;
56 |
--------------------------------------------------------------------------------
/src/pages/Account/Base/MetaDesc.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from "dayjs";
2 | import React, { FC } from "react";
3 | import "./style.scss";
4 |
5 | const MetaDesc: FC = (props: any) => {
6 | const { userInfo } = props;
7 | return (
8 |
9 |
{userInfo.loginName}
10 |
11 | 账号:
12 | {userInfo.name}
13 |
14 |
简介:{userInfo.signature}
15 |
邮箱:{userInfo.email}
16 |
地区:{userInfo.address}
17 |
注册时间:{dayjs(userInfo.created).format("YYYY-MM-DD HH:mm")}
18 |
19 | );
20 | };
21 |
22 | export default MetaDesc;
23 |
--------------------------------------------------------------------------------
/src/pages/Account/Base/basefrom.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect, useState } from "react";
2 | import { Button, Col, Form, Input } from "antd";
3 | const { TextArea } = Input;
4 | const layout = {
5 | labelCol: { span: 6 },
6 | wrapperCol: { span: 18 }
7 | };
8 | const tailLayout = {
9 | wrapperCol: { offset: 6, span: 16 }
10 | };
11 | const BaseFrom: FC = (props: any) => {
12 | const [form] = Form.useForm();
13 | const { initFormItem, pendingCallback } = props;
14 | const [editFlag, setEditFlag] = useState(false);
15 | const onRequiredTypeChange = () => {
16 | setEditFlag(true);
17 | };
18 | const onFinish = async () => {
19 | if (!editFlag) {
20 | return;
21 | }
22 | setEditFlag(false);
23 | const result = await form.validateFields();
24 | pendingCallback(result);
25 | };
26 | useEffect(() => {
27 | form.setFieldsValue(initFormItem);
28 | }, [form, initFormItem]);
29 |
30 | return (
31 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
64 |
65 |
66 | );
67 | };
68 |
69 | export default BaseFrom;
70 |
--------------------------------------------------------------------------------
/src/pages/Account/Base/style.scss:
--------------------------------------------------------------------------------
1 | .setting-base {
2 | .ant-card-meta-title {
3 | margin-bottom: 0 !important;
4 | }
5 |
6 | .meta-desc {
7 | line-height: 1.7;
8 | .loginname {
9 | margin-bottom: 8px;
10 | }
11 | }
12 |
13 | .poster {
14 | display: block;
15 | background: #f7f7f7;
16 | object-fit: cover;
17 | }
18 | }
19 |
20 | .formdom {
21 | .formitem {
22 | margin-bottom: 15px;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/pages/Account/Messagecenter/MessageCenter.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState } from "react";
2 | import { Button, Col, Divider, Table } from "antd";
3 |
4 | import "./style.scss";
5 |
6 | const MessageCenter: FC = () => {
7 | const [selectedRowKeys, setSelectedRowKeys] = useState([]);
8 | const [tableColumns] = useState([
9 | {
10 | title: "",
11 | dataIndex: "hasRead",
12 | width: 12,
13 | className: "unread-row",
14 | render: (hasRead: boolean) => !hasRead && ●
15 | },
16 | { title: "标题内容", dataIndex: "content" },
17 | { title: "提交时间", dataIndex: "createdAt", width: 150 },
18 | { title: "类型", dataIndex: "title", width: 130 }
19 | ]);
20 |
21 | const handleAction = () => {};
22 | return (
23 |
24 |
25 |
26 | 消息中心
27 |
28 |
29 |
setSelectedRowKeys(selectedKeys)
34 | }}
35 | rowKey={(record: any) => record.uid}
36 | />
37 |
38 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default MessageCenter;
48 |
--------------------------------------------------------------------------------
/src/pages/Account/Messagecenter/style.scss:
--------------------------------------------------------------------------------
1 | .inner-message {
2 | position: relative;
3 | height: 100%;
4 | font-size: 12px;
5 |
6 | .ant-table {
7 | font-size: 12px;
8 | }
9 |
10 | .unread-dot {
11 | display: block;
12 | color: #f50;
13 | transform: scale(0.9);
14 | }
15 |
16 | .unread-row {
17 | padding-left: 0 !important;
18 | padding-right: 0 !important;
19 | }
20 |
21 | .action-group {
22 | position: absolute;
23 | bottom: 18px;
24 | left: 20px;
25 |
26 | .ant-btn {
27 | margin-right: 15px;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/pages/Account/Notification/notification.tsx:
--------------------------------------------------------------------------------
1 | import { Divider, Switch } from "antd";
2 | import React, { useEffect, useState } from "react";
3 |
4 | import "./style.scss";
5 |
6 | const NotificationPage: React.FC = function () {
7 | const [userConfig, setUserConfig] = useState({
8 | isMatterNotify: true,
9 | isTaskNotify: true
10 | });
11 |
12 | useEffect(() => {}, []);
13 |
14 | function handleUpdateUserConfig(type: number, checked: boolean) {
15 | const fields: any = {
16 | 0: "isTaskNotify",
17 | 1: "isMatterNotify"
18 | };
19 | setUserConfig({
20 | ...userConfig,
21 | [fields[type]]: checked
22 | });
23 | }
24 |
25 | return (
26 |
27 |
28 | 消息通知
29 |
30 |
31 |
32 |
待办任务
33 |
开通后将以站内信的形式通知并且通知到邮箱, 否则只会站内信通知
34 |
35 |
36 |
37 |
38 |
39 |
提醒事项
40 |
开通后将以站内信的形式通知并且通知到邮箱, 否则只会站内信通知
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default NotificationPage;
49 |
--------------------------------------------------------------------------------
/src/pages/Account/Notification/style.scss:
--------------------------------------------------------------------------------
1 | .notification {
2 | .list {
3 | margin-bottom: 10px;
4 | display: flex;
5 | border-bottom: 1px solid #e6e6e6;
6 |
7 | .left {
8 | flex: 1;
9 | }
10 |
11 | .title {
12 | color: rgba(0, 0, 0, 0.65);
13 | }
14 |
15 | .description {
16 | color: rgba(0, 0, 0, 0.45);
17 | }
18 |
19 | .ant-switch {
20 | align-self: center;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/pages/Account/SecuritySetting/SecuritySetting.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 安全设置
3 | */
4 | import React, { FC } from "react";
5 | import { Col, Divider, Row } from "antd";
6 |
7 | const SecuritySetting: FC = function () {
8 | return (
9 |
10 |
11 |
12 | 安全设置
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default SecuritySetting;
21 |
--------------------------------------------------------------------------------
/src/pages/Account/index.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 12px;
3 | }
4 |
5 | .title {
6 | width: 160px;
7 | height: 30px;
8 | line-height: 30px;
9 | }
10 |
11 | .tabplane {
12 | overflow: auto;
13 | :global {
14 | .ant-tabs-tab-active {
15 | background-color: #e6f7ff;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/pages/Account/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { Layout, Tabs } from "antd";
3 | import AccountBind from "./AccountBind/AccountBind";
4 | import AccountSetting from "./AccountSetting/AccountSetting";
5 | import BaseSetting from "./Base/BaseSetting";
6 | import MessageCenter from "./Messagecenter/MessageCenter";
7 | import NotificationPage from "./Notification/notification";
8 | import SecuritySetting from "./SecuritySetting/SecuritySetting";
9 |
10 | import styles from "./index.module.scss";
11 |
12 | const { TabPane } = Tabs;
13 |
14 | const Account: FC = () => {
15 | return (
16 |
17 |
18 | 个人中心} key="BaseSetting">
19 |
20 |
21 | 消息中心} key="MessageCenter">
22 |
23 |
24 | 消息通知} key="notification">
25 |
26 |
27 | 账号设置} key="AccountSetting">
28 |
29 |
30 | 安全设置} key="SecuritySetting">
31 |
32 |
33 | 账号绑定} key="AccountBind">
34 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default Account;
42 |
--------------------------------------------------------------------------------
/src/pages/AnimateCard/index.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 12px;
3 | user-select: none;
4 | :global {
5 | .animateContainer {
6 | width: 100%;
7 | height: 100px;
8 | background: #fce5cd;
9 | border: 1px solid #000;
10 | text-align: center;
11 | line-height: 100px;
12 | font-size: 16px;
13 | font-weight: 800;
14 | color: #351c75;
15 | animation-iteration-count: 1;
16 | cursor: pointer;
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/BMap/index.tsx:
--------------------------------------------------------------------------------
1 | import { Map } from "react-bmap";
2 |
3 | const BMapPage = () => {
4 | return (
5 |
6 |
13 |
14 | );
15 | };
16 |
17 | export default BMapPage;
18 |
--------------------------------------------------------------------------------
/src/pages/Chart/AreaChart/index.tsx:
--------------------------------------------------------------------------------
1 | import { BarChart, HomeSenseChart, LineChart, PictorialBarChart, PieChart, ScatterChart, ServerStatusChart } from "@/components/Echarts";
2 | import { Col, Row } from "antd";
3 |
4 | const AreaChart = () => {
5 | return (
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 | export default AreaChart;
34 |
--------------------------------------------------------------------------------
/src/pages/Chart/EchartsPie/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect } from "react";
2 | import * as echarts from "echarts";
3 |
4 | const EchartsPie: FC = () => {
5 | useEffect(() => {
6 | const dom: any = document.getElementById("pie");
7 | const myChart: any = echarts.init(dom);
8 | myChart.setOption({
9 | tooltip: {
10 | trigger: "item",
11 | formatter: "{a}
{b} : {c} ({d}%)"
12 | },
13 | legend: {
14 | orient: "vertical",
15 | left: "left",
16 | data: ["直接访问", "邮件营销", "联盟广告", "视频广告", "搜索引擎"]
17 | },
18 | series: [
19 | {
20 | type: "pie",
21 | radius: "55%",
22 | center: ["50%", "60%"],
23 | data: [
24 | { value: 335, name: "直接访问" },
25 | { value: 310, name: "邮件营销" },
26 | { value: 234, name: "联盟广告" },
27 | { value: 135, name: "视频广告" },
28 | { value: 1548, name: "搜索引擎" }
29 | ],
30 | itemStyle: {}
31 | }
32 | ]
33 | });
34 | window.addEventListener("resize", function () {
35 | myChart.resize();
36 | });
37 | return () => {
38 | myChart.dispose && myChart.dispose();
39 | };
40 | }, []);
41 |
42 | return ;
43 | };
44 |
45 | export default EchartsPie;
46 |
--------------------------------------------------------------------------------
/src/pages/D3Example/index.tsx:
--------------------------------------------------------------------------------
1 | import Collision from "@/components/D3/Collision";
2 | import { withErrorBoundary } from "@/components/ErrorBoundary";
3 | import { Row, Col, Card } from "antd";
4 | import { useLoaderData } from "react-router-dom";
5 | import FailContainer from "../FailPage/FailContainer";
6 |
7 | // eslint-disable-next-line react-refresh/only-export-components
8 | const D3Example = () => {
9 | useLoaderData();
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | // eslint-disable-next-line react-refresh/only-export-components
28 | export default withErrorBoundary(D3Example, {
29 | fallbackRender: FailContainer
30 | });
31 |
--------------------------------------------------------------------------------
/src/pages/FailPage/FailContainer.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Result } from "antd";
2 |
3 | const FailContainer = ({ resetErrorBoundary }) => {
4 | return (
5 |
10 | 重新渲染
11 |
12 | }
13 | />
14 | );
15 | };
16 |
17 | export default FailContainer;
18 |
--------------------------------------------------------------------------------
/src/pages/FailPage/NeedLogin.tsx:
--------------------------------------------------------------------------------
1 | import { Spin, Alert } from "antd";
2 | import { useEffect } from "react";
3 | import { useNavigate } from "react-router-dom";
4 |
5 | const NeedLogin = () => {
6 | const navigate = useNavigate();
7 | useEffect(() => {
8 | navigate("/login");
9 | // eslint-disable-next-line react-hooks/exhaustive-deps
10 | }, []);
11 |
12 | return (
13 |
18 | );
19 | };
20 |
21 | export default NeedLogin;
22 |
--------------------------------------------------------------------------------
/src/pages/FailPage/NotAuthPage.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const NotAuthPage = () => {
4 | return 没有权限查看此页面
;
5 | };
6 |
7 | export default NotAuthPage;
8 |
--------------------------------------------------------------------------------
/src/pages/FailPage/NotFoundPage.tsx:
--------------------------------------------------------------------------------
1 | import { getIndexUrl } from "@/utils/menuUtils";
2 | import { Button, Result } from "antd";
3 | import { useNavigate } from "react-router-dom";
4 |
5 | const NotFoundPage = () => {
6 | const navigator = useNavigate();
7 | const onClick = () => {
8 | navigator(getIndexUrl());
9 | };
10 | return (
11 |
17 | 返回主页
18 |
19 | }
20 | />
21 | );
22 | };
23 |
24 | export default NotFoundPage;
25 |
--------------------------------------------------------------------------------
/src/pages/LabelsAdmin/index.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 12px;
3 | }
4 |
5 | .header {
6 | margin-bottom: 12px;
7 | }
8 |
--------------------------------------------------------------------------------
/src/pages/LabelsAdmin/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useEffect, useState } from "react";
2 | import { Button, Space, Table, Tag } from "antd";
3 | import { getalllabelsbyuser } from "@/api/caravan/Labels";
4 | import { isTagVisible } from "@/utils/core";
5 | import { OperationLabelModal } from "@/components/Modals";
6 |
7 | import styles from "./index.module.scss";
8 |
9 | const LabelsAdmin: FC = () => {
10 | const [dataSource, setDataSource] = useState([]);
11 | const [label, setLabel] = useState();
12 |
13 | const findAll = () => {
14 | setLabel(null);
15 | getalllabelsbyuser({})
16 | .then(res => setDataSource(res.data.data || []))
17 | .catch(() => setDataSource([]));
18 | };
19 | useEffect(findAll, []);
20 |
21 | const columns: any = [
22 | {
23 | title: "标签名称",
24 | dataIndex: "title"
25 | },
26 | {
27 | title: "备注",
28 | dataIndex: "description",
29 | responsive: ["lg"]
30 | },
31 | {
32 | title: "排序",
33 | dataIndex: "sort",
34 | responsive: ["lg"]
35 | },
36 | {
37 | title: "是否可见",
38 | dataIndex: "visible",
39 | render: (item: any) => (isTagVisible(item) ? 隐藏 : 显示)
40 | },
41 | {
42 | title: "操作",
43 | render: (item: any) => (
44 |
47 | )
48 | }
49 | ];
50 |
51 | return (
52 |
53 |
54 |
57 |
60 |
61 |
62 | {label &&
setLabel(null)} onOk={findAll} />}
63 |
64 | );
65 | };
66 |
67 | export default LabelsAdmin;
68 |
--------------------------------------------------------------------------------
/src/pages/Login/index.tsx:
--------------------------------------------------------------------------------
1 | import Copyright from "@/components/Footer/Copyright";
2 | import LoginForm from "@/components/Forms/LoginForm";
3 | import RegisterForm from "@/components/Forms/RegisterForm";
4 | import { useState } from "react";
5 | import { CSSTransition, TransitionGroup } from "react-transition-group";
6 | import Snowflake from "./snowflake";
7 | import styles from "./index.module.scss";
8 | import Owl from "@/components/Owl";
9 |
10 | const Login = () => {
11 | const [register, setRegister] = useState(0);
12 | const onClick = () => setRegister(v => v + 1);
13 | const [random] = useState(Math.random());
14 | const [closeEyes, setCloseEyes] = useState(false);
15 | const bg = random < 0.45 ? "https://dr.cdnux.com/18510675/i/2025/01/04/65a28bad6be9e.jpg" : "https://dr.cdnux.com/18510675/i/2024/12/31/7xt4r.webp";
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {register % 2 ? setRegister(0)} setCloseEyes={setCloseEyes} /> : }
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default Login;
39 |
--------------------------------------------------------------------------------
/src/pages/Login/snowflake.tsx:
--------------------------------------------------------------------------------
1 | import snowflake_png from "@/assets/snowflake.png";
2 | import sakura_png from "@/assets/sakura.png";
3 | import { useEffect, useRef } from "react";
4 | import { getRandom } from "@/utils/core";
5 | import { Sakura, SakuraList } from "@/utils/Sakura";
6 | let stopFun: any;
7 | const Snowflake = ({ rdm }) => {
8 | const ref: any = useRef();
9 |
10 | useEffect(() => {
11 | const onresize = () => {
12 | const dom = ref.current;
13 | if (dom) {
14 | const clientHeight = document.body.clientHeight;
15 | const clientWidth = document.body.clientWidth;
16 | const canvasSnow: any = ref.current?.firstChild;
17 | canvasSnow.width = clientWidth;
18 | canvasSnow.height = clientHeight;
19 | }
20 | };
21 | //樱花
22 | const img = new Image();
23 | img.src = rdm < 0.45 ? sakura_png : snowflake_png;
24 |
25 | function startSakura() {
26 | if (ref.current?.firstChild) {
27 | ref.current?.removeChild(ref.current?.firstChild);
28 | }
29 | const canvas = document.createElement("canvas");
30 | canvas.height = document.body.clientHeight;
31 | canvas.width = document.body.clientWidth;
32 | canvas.setAttribute("style", "position: absolute;left: 0;top: 0;pointer-events: none;object-fit:cover;");
33 | ref.current!.appendChild(canvas);
34 | const cxt = canvas.getContext("2d");
35 | const sakuraList = new SakuraList();
36 | for (let i = 0; i < 100; i++) {
37 | const randomX = getRandom("x");
38 | const randomY = getRandom("y");
39 | const randomR = getRandom("r");
40 | const randomS = getRandom("s");
41 | const randomFnx = getRandom("fnx");
42 | const randomFny = getRandom("fny");
43 | const randomFnR = getRandom("fnr");
44 | const sakura = new Sakura(img, randomX, randomY, randomS, randomR, {
45 | x: randomFnx,
46 | y: randomFny,
47 | r: randomFnR
48 | });
49 | sakura.draw(cxt);
50 | sakuraList.push(sakura);
51 | }
52 |
53 | stopFun = function () {
54 | cxt.clearRect(0, 0, canvas.width, canvas.height);
55 | sakuraList.update();
56 | sakuraList.draw(cxt);
57 | requestAnimationFrame(stopFun);
58 | };
59 |
60 | requestAnimationFrame(stopFun);
61 | }
62 |
63 | img.onload = function () {
64 | startSakura();
65 | };
66 |
67 | window.addEventListener("resize", onresize);
68 | return () => {
69 | window.removeEventListener("resize", onresize);
70 | window.cancelAnimationFrame(stopFun);
71 | const child = ref.current?.firstChild;
72 | child?.parentNode?.removeChild(child);
73 | };
74 | }, [rdm]);
75 |
76 | return ;
77 | };
78 |
79 | export default Snowflake;
80 |
--------------------------------------------------------------------------------
/src/pages/MenuAdmin/index.module.scss:
--------------------------------------------------------------------------------
1 | .menusAdminContainer {
2 | padding: 12px;
3 | }
4 |
5 | .header {
6 | margin: 12px 0;
7 | }
8 |
9 | .tableContainer{
10 | background-color: #fff;;
11 | padding: 0px 12px;
12 | margin-top: 12px;
13 | }
--------------------------------------------------------------------------------
/src/pages/MusicChart/index.module.scss:
--------------------------------------------------------------------------------
1 | .MusicChart{
2 | margin: 12px;
3 | padding: 12px;
4 | background-color: #fff;
5 | .musicContainer{
6 | display: inline-block;
7 | .musicItem{
8 | font-size: 16px;
9 | padding: 12px;
10 | cursor: pointer;
11 | user-select: none;
12 | &:hover{
13 | background: #DDD;
14 | }
15 | .title{
16 | text-align: center;
17 | }
18 | .user{
19 | font-size: 12px;
20 | color: #333;
21 | }
22 | .playIconNode{
23 | margin: 0 auto;
24 | text-align: center;
25 | display: inline-block;
26 | margin: 6px 0px;
27 | .playIcon{
28 | border: 12px solid #ccc;
29 | border-radius: 50%;
30 | background-color: #ccc;
31 | transform: scale(0.5);
32 | }
33 | }
34 | .share{
35 | margin:0px 12px;
36 | }
37 | .btns{
38 | margin:0px 6px;
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/pages/MusicChart/musicItem.tsx:
--------------------------------------------------------------------------------
1 | import { DownloadOutlined, LikeFilled, LikeOutlined, PlayCircleFilled, ShareAltOutlined } from "@ant-design/icons";
2 | import { Tooltip } from "antd";
3 | import { useState } from "react";
4 | import styles from "./index.module.scss";
5 | const MusicItem = ({ data }) => {
6 | const [music, setMusic] = useState(data);
7 |
8 | const like = () => {
9 | setMusic({ ...music, action: 1 });
10 | };
11 |
12 | return (
13 |
14 |
{music.name}
15 |
演唱者: 未知
16 |
21 |
22 |
23 |
24 | {music?.action ? : }
25 | {music?.action ? "1" : "0"}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default MusicItem;
45 |
--------------------------------------------------------------------------------
/src/pages/PlacardAdmin/index.module.scss:
--------------------------------------------------------------------------------
1 | .placard {
2 | padding: 12px;
3 | }
4 |
5 | .header {
6 | margin: 12px 0;
7 | }
8 |
9 | .tableContainer {
10 | background-color: #fff;
11 | padding: 0px 12px;
12 | margin-top: 12px;
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/RoleAdmin/index.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 12px;
3 | }
4 |
5 | .tipContainer{
6 | margin-bottom: 12px;
7 | }
8 |
9 | .tip {
10 | font-size: 1em;
11 | }
12 |
13 | .weightFont {
14 | font-size: 15px;
15 | color: #f00;
16 | padding:0 6px;
17 | }
18 |
19 | .header {
20 | margin: 12px 0;
21 | }
22 |
23 | .tableContainer{
24 | background-color: #fff;;
25 | padding: 0px 12px;
26 | margin-top: 12px;
27 | }
--------------------------------------------------------------------------------
/src/pages/Root/index.module.scss:
--------------------------------------------------------------------------------
1 | .root {
2 | border-radius: 0;
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/Root/index.scss:
--------------------------------------------------------------------------------
1 | .root {
2 | border-radius: 0;
3 | }
4 |
5 | .fade-enter {
6 | opacity: 1;
7 | transform: translateX(100%);
8 | }
9 |
10 | .fade-enter-active {
11 | opacity: 1;
12 | transition: transform 1s ease-in-out;
13 | transform: translateX(0px);
14 | }
15 |
16 | .fade-exit {
17 | opacity: 0;
18 | }
19 |
20 | .fade-exit-active {
21 | opacity: 0;
22 | }
23 |
--------------------------------------------------------------------------------
/src/pages/Root/index.tsx:
--------------------------------------------------------------------------------
1 | import ProjectRouter from "@/router";
2 | import { RouterProvider } from "react-router-dom";
3 | import { ProjectContextProvider } from "@/contexts/ProjectContext";
4 | import { MenuTagContextProvider } from "@/contexts/MenuTagContext";
5 |
6 | import "./index.scss";
7 | import { ConfigProvider } from "antd";
8 | import { webSocketManager } from "@/utils/ws";
9 | import { useEffect } from "react";
10 |
11 | const Root = () => {
12 | const router = ProjectRouter();
13 |
14 | useEffect(() => {
15 | webSocketManager.create();
16 | return () => {
17 | webSocketManager.postMessage({
18 | name: "qkstartCar",
19 | type: "123",
20 | message: "",
21 | data: []
22 | });
23 | webSocketManager.socket.disconnect();
24 | webSocketManager.close();
25 | };
26 | }, []);
27 |
28 | return (
29 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default Root;
50 |
--------------------------------------------------------------------------------
/src/pages/UserAdmin/components/Table.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Table, Tag } from "antd";
2 |
3 | const UserTable = ({ dataSource, setUser, showDeleteConfirm }) => {
4 | const columns: any = [
5 | {
6 | title: "账号",
7 | dataIndex: "username"
8 | },
9 | {
10 | title: "昵称",
11 | dataIndex: ["profile", "nick"]
12 | },
13 | {
14 | title: "邮箱",
15 | dataIndex: "email",
16 | responsive: ["xxl", "xl", "lg", "md"]
17 | },
18 | {
19 | title: "电话",
20 | dataIndex: "mobile",
21 | responsive: ["xxl", "xl", "lg", "md"]
22 | },
23 | {
24 | title: "状态",
25 | dataIndex: "banned",
26 | responsive: ["xxl", "xl", "lg", "md"],
27 | render: (item: any) => (
28 |
29 | {item ? "禁用" : "启用"}
30 |
31 | )
32 | },
33 | {
34 | title: "操作",
35 | render: (item: any) => {
36 | return (
37 |
38 |
41 |
44 |
45 | );
46 | }
47 | }
48 | ];
49 | return ;
50 | };
51 |
52 | export default UserTable;
53 |
--------------------------------------------------------------------------------
/src/pages/UserAdmin/index.module.scss:
--------------------------------------------------------------------------------
1 | .userAdminContainer {
2 | padding: 12px;
3 | }
4 |
5 | .searchContainer {
6 | margin: 12px 0px;
7 | padding: 12px;
8 | background-color: #fff;
9 | border-radius: 6px;
10 | }
11 |
12 | .tableContainer {
13 | border-radius: 6px;
14 | background-color: #fff;
15 | margin-top: 12px;
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/UserAdmin/index.tsx:
--------------------------------------------------------------------------------
1 | import { UserEditModal } from "@/components/Modals";
2 | import { ExclamationCircleOutlined } from "@ant-design/icons";
3 | import { Button, Form, Input, Modal, Space, message } from "antd";
4 | import { FC, useEffect, useState } from "react";
5 | import { findUsers, updateUser } from "@/api/microservice/user";
6 | import PageIntroduction from "@/components/PageIntroduction";
7 | import UserTable from "./components/Table";
8 |
9 | import styles from "./index.module.scss";
10 |
11 | const infos = [
12 | {
13 | title: "系统设置"
14 | },
15 | {
16 | title: "用户管理"
17 | }
18 | ];
19 |
20 | const UserAdmin: FC = () => {
21 | const [form] = Form.useForm();
22 | const [dataSource, setDataSource] = useState([]);
23 | const [user, setUser] = useState(null);
24 |
25 | const showDeleteConfirm = (item: any) => {
26 | Modal.confirm({
27 | title: "提示",
28 | icon: ,
29 | content: {`是否${item.banned ? "启用" : "禁用"}该用户?`},
30 | okText: "确定",
31 | okType: "danger",
32 | cancelText: "取消",
33 | onOk: async () => {
34 | const result = await updateUser({ banned: !item.banned });
35 | if (result.data.code === 200) {
36 | message.info("操作成功");
37 | findAll();
38 | return;
39 | }
40 | message.info("操作失败");
41 | },
42 | onCancel() {}
43 | });
44 | };
45 |
46 | const findAll = () => {
47 | setUser(null);
48 | const { username, email, mobile } = form.getFieldsValue();
49 | const payload = { banned: false, recycle: false };
50 | if (username) payload.username = username;
51 | if (email) payload.email = email;
52 | if (mobile) payload.mobile = mobile;
53 |
54 | findUsers(payload)
55 | .then(result => {
56 | if (result.data.code === 200) {
57 | setDataSource(result.data.data || []);
58 | } else {
59 | setDataSource([]);
60 | }
61 | })
62 | .catch(() => {
63 | setDataSource([]);
64 | });
65 | };
66 | useEffect(findAll, []);
67 | return (
68 |
96 | );
97 | };
98 |
99 | export default UserAdmin;
100 |
--------------------------------------------------------------------------------
/src/pages/UserProfile/index.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 12px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/UserProfile/index.tsx:
--------------------------------------------------------------------------------
1 | import UserInfoCard from "@/components/UserInfoCard";
2 | import styles from "./index.module.scss";
3 |
4 | const UserProfile = () => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
12 | export default UserProfile;
13 |
--------------------------------------------------------------------------------
/src/pages/WebsocketAdmin/index.module.scss:
--------------------------------------------------------------------------------
1 | .websocketAdmin {
2 | padding: 12px;
3 | }
4 |
5 | .imgbg {
6 | width: 300px;
7 | height: 140px;
8 | user-select: none;
9 | margin-bottom: 12px;
10 | }
11 |
--------------------------------------------------------------------------------
/src/pages/Workbench/index.module.scss:
--------------------------------------------------------------------------------
1 | .workbench {
2 | padding: 12px;
3 | }
4 |
5 | .info {
6 | margin-bottom: 12px;
7 | }
8 |
--------------------------------------------------------------------------------
/src/pages/Workbench/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 |
3 | import styles from "./index.module.scss";
4 | import SystemInfo from "@/components/SystemInfo";
5 | import LoginLog from "@/components/LoginLog";
6 |
7 | const Workbench: FC = () => {
8 | return (
9 |
15 | );
16 | };
17 |
18 | export default Workbench;
19 |
--------------------------------------------------------------------------------
/src/pages/Workplace/index.module.scss:
--------------------------------------------------------------------------------
1 | $fork-me: "@/assets/github/fork.png";
2 | .baseStyle {
3 | display: flex;
4 | padding: 1rem;
5 | border-radius: 5px;
6 | font-size: 2em;
7 | }
8 |
9 | .wechat {
10 | @extend .baseStyle;
11 | background-color: #70ca63;
12 | }
13 |
14 | .qq {
15 | @extend .baseStyle;
16 | background-color: #12b7f5;
17 | }
18 |
19 | .dingding {
20 | @extend .baseStyle;
21 | background-color: #0089ff;
22 | }
23 |
24 | .weibo {
25 | @extend .baseStyle;
26 | background-color: #fb452d;
27 | }
28 |
29 | .iconStyle {
30 | color: #fbfbf4b3;
31 | font-size: 5rem;
32 | padding-right: 1rem;
33 | }
34 |
35 | .baseTitle {
36 | font-size: 2rem;
37 | }
38 |
39 | .githubCorner {
40 | position: absolute;
41 | top: 0;
42 | right: 6px;
43 | background-image: url($fork-me);
44 | background-size: 100% 100%;
45 | width: 120px;
46 | height: 120px;
47 | z-index: 2;
48 | cursor: pointer;
49 | }
50 |
51 | .header {
52 | color: #fff;
53 | }
54 |
55 | .container {
56 | padding: 1rem;
57 | }
58 |
59 | .baseChart {
60 | margin-top: 12px;
61 | border-radius: 5px;
62 | overflow: hidden;
63 | background-color: #fff;
64 | .imgbg{
65 | background-repeat: no-repeat;
66 | background-size: cover;
67 | background-position: center;
68 | height: 400px;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/pages/Workplace/index.tsx:
--------------------------------------------------------------------------------
1 | import { PieBorderRadius } from "@/components/Echarts";
2 | import { DingdingOutlined, QqOutlined, WechatOutlined, WeiboOutlined } from "@ant-design/icons";
3 | import { Col, Row } from "antd";
4 | import CountUp from "react-countup";
5 |
6 | import styles from "./index.module.scss";
7 | import Round from "@/components/Echarts/Round";
8 |
9 | const Workplace = () => {
10 | const toMygit = () => window.open("https://github.com/weiqinke/react-admin-nest", "topFrame");
11 | const src = "https://dr.cdnux.com/18510675/i/2024/12/31/7xt4r.webp";
12 | return (
13 |
14 |
15 |
16 |
23 |
24 |
25 |
32 |
33 |
34 |
41 |
42 |
43 |
51 |
52 |
53 |
54 |
55 |
58 |
59 |
60 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
75 |
76 |
77 |
78 | );
79 | };
80 |
81 | export default Workplace;
82 |
--------------------------------------------------------------------------------
/src/router/index.tsx:
--------------------------------------------------------------------------------
1 | import ProjectContext from "@/contexts/ProjectContext";
2 | import NeedLogin from "@/pages/FailPage/NeedLogin";
3 | import RouterPages from "@/router/router";
4 | import React, { useContext } from "react";
5 | import { createBrowserRouter } from "react-router-dom";
6 |
7 | const WrapperRoute = (props: any) => {
8 | const { component, title = "welcome", auth } = props;
9 | document.title = title;
10 | const { value: data }: any = useContext(ProjectContext);
11 | const { uid, exp } = data;
12 | const value = new Date().valueOf();
13 | const authEndTime = new Date(exp * 1000).valueOf();
14 | // 权限到期了,就跳转到 login
15 | if (authEndTime <= value) return ;
16 | if (auth) {
17 | // 如果该菜单需要权限检查
18 | if (!uid) return ;
19 | }
20 | // 权限到期了,就跳转到 login
21 | return ;
22 | };
23 |
24 | const ProjectRouter = () => {
25 | const crateROuter = (routerItem: string | any[]) => {
26 | const routers = [];
27 | for (let i = 0; i < routerItem.length; i++) {
28 | const item = routerItem[i];
29 | item.element = ;
30 | if (item.children && item.children.length > 0) {
31 | item.children = crateROuter(item.children);
32 | }
33 | routers.push(item);
34 | }
35 | return routers;
36 | };
37 |
38 | return createBrowserRouter(crateROuter(RouterPages));
39 | };
40 |
41 | export default ProjectRouter;
42 |
--------------------------------------------------------------------------------
/src/styles/_themeMixin.scss:
--------------------------------------------------------------------------------
1 | @import "./themeVariable.scss"; //导入主题颜色变量
2 |
3 | @mixin themeMixin {
4 | @each $theme-name, $theme-map in $themes {
5 | //!global 把局部变量强升为全局变量
6 | $theme-map: $theme-map !global;
7 | [data-theme="#{$theme-name}"] & {
8 | // #{} 插值表达式
9 | @content;
10 | }
11 | }
12 | }
13 |
14 | //声明一个根据Key获取颜色的function
15 | @function themed($key) {
16 | @return map-get($theme-map, $key);
17 | }
18 |
19 | @mixin get-class-from-key($key, $colorName: $key) {
20 | @include themeMixin {
21 | // 因为插值表达式会将变量转变成无符号字符串,所以可以直接当变量名
22 | #{$key}: themed($colorName);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/styles/_themeVariable.scss:
--------------------------------------------------------------------------------
1 | /**
2 | _themeVariable.scss文件
3 | 下边是sass语法的map数据结构
4 | */
5 | $themes: (
6 | "light": (
7 | background-color: #fedce0,
8 | background-slider: #fff,
9 | head-color: #fff,
10 | head-icon-bg: #158670,
11 | // header icon 正常的颜色
12 | head-icon-color: #158670,
13 | head-icon-bg-hover: #fff,
14 | font-basis-color: #b6bbc9,
15 | box-border: #b6bbc9,
16 | transition: 0.2s,
17 | color-font-normal: #a6acbd,
18 | color-font-light: #fff,
19 | color-font-dark: #003049,
20 | color-pink: #f72585,
21 | color-green: #22b07d,
22 | color-logo: #1081e8,
23 | color-head: #fff
24 | ),
25 | "dark": (
26 | background-color: #001529,
27 | background-slider: #171717,
28 | head-color: #fff,
29 | head-icon-bg: #41b3a3,
30 | // header icon 正常的颜色
31 | head-icon-color: #fff,
32 | head-icon-bg-hover: #fff,
33 | font-basis-color: #f0f0f0,
34 | box-border: none,
35 | transition: 0.2s,
36 | color-normal: #a6acbd,
37 | color-font-normal: #fff,
38 | color-font-light: #fff,
39 | color-font-dark: #fff,
40 | color-pink: #f72585,
41 | color-green: #22b07d,
42 | color-logo: #fff,
43 | color-head: #fff
44 | )
45 | );
46 |
--------------------------------------------------------------------------------
/src/styles/reset.css:
--------------------------------------------------------------------------------
1 | :root {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | body {
7 | margin: 0;
8 | display: flex;
9 | place-items: center;
10 | min-height: 100vh;
11 | min-width: 100vw;
12 | overflow: hidden;
13 | }
14 |
15 | ::-webkit-scrollbar {
16 | /*滚动条整体样式*/
17 | width: 8px;
18 | height: 8px;
19 | }
20 |
21 | ::-webkit-scrollbar-track {
22 | /*滚动条里面轨道*/
23 | background-color: rgba(255, 255, 255, 0);
24 | border-radius: 0px;
25 | }
26 |
27 | ::-webkit-scrollbar-thumb {
28 | /*滚动条里面小方块*/
29 | border-radius: 4px;
30 | background-color: #c0c4cc;
31 |
32 | &:hover {
33 | background-color: #81858c;
34 | }
35 | }
36 |
37 | ::-webkit-resizer {
38 | display: none;
39 | }
40 |
41 | ::-webkit-scrollbar-corner {
42 | display: none;
43 | }
44 |
45 | * {
46 | /*火狐的样式*/
47 | scrollbar-color: #999 #ccc;
48 | scrollbar-width: thin;
49 | }
50 |
--------------------------------------------------------------------------------
/src/utils/Sakura.ts:
--------------------------------------------------------------------------------
1 | import { getRandom } from "./core";
2 |
3 | export const Sakura: any = function (img: any, x: any, y: any, s: any, r: any, fn: any) {
4 | this.img = img;
5 | this.x = x;
6 | this.y = y;
7 | this.s = s;
8 | this.r = r;
9 | this.fn = fn;
10 | };
11 |
12 | Sakura.prototype.draw = function (cxt: any) {
13 | cxt.save();
14 | cxt.translate(this.x, this.y);
15 | cxt.rotate(this.r);
16 | cxt.drawImage(this.img, 0, 0, 40 * this.s, 40 * this.s);
17 | cxt.restore();
18 | };
19 |
20 | Sakura.prototype.update = function () {
21 | this.x = this.fn.x(this.x, this.y);
22 | this.y = this.fn.y(this.y, this.y);
23 | this.r = this.fn.r(this.r);
24 |
25 | const clientHeight = document.body.clientHeight;
26 | const clientWidth = document.body.clientWidth;
27 | if (this.x > clientWidth || this.x < 0 || this.y > clientHeight || this.y < 0) {
28 | this.r = getRandom("fnr");
29 | if (Math.random() > 0.4) {
30 | this.x = getRandom("x");
31 | this.y = 0;
32 | this.s = getRandom("s");
33 | this.r = getRandom("r");
34 | } else {
35 | this.x = clientWidth;
36 | this.y = getRandom("y");
37 | this.s = getRandom("s");
38 | this.r = getRandom("r");
39 | }
40 | }
41 | };
42 |
43 | export const SakuraList: any = function () {
44 | this.list = [];
45 | };
46 | SakuraList.prototype.push = function (sakura: any) {
47 | this.list.push(sakura);
48 | };
49 | SakuraList.prototype.update = function () {
50 | for (let i = 0, len = this.list.length; i < len; i++) {
51 | this.list[i].update();
52 | }
53 | };
54 | SakuraList.prototype.draw = function (cxt: any) {
55 | for (let i = 0, len = this.list.length; i < len; i++) {
56 | this.list[i].draw(cxt);
57 | }
58 | };
59 | SakuraList.prototype.get = function (i: any) {
60 | return this.list[i];
61 | };
62 | SakuraList.prototype.size = function () {
63 | return this.list.length;
64 | };
65 |
--------------------------------------------------------------------------------
/src/utils/file.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-duplicate-enum-values */
2 | import html2canvas from "html2canvas";
3 |
4 | const createBodyImg = async () => {
5 | const body: any = document.querySelector("body");
6 | const canvas = await html2canvas(body);
7 | const context: any = canvas.getContext("2d");
8 | // 关闭抗锯齿形
9 | context.mozImageSmoothingEnabled = false;
10 | context.webkitImageSmoothingEnabled = false;
11 | context.msImageSmoothingEnabled = false;
12 | context.imageSmoothingEnabled = false;
13 | const data = canvas2Base64(canvas, canvas.width, canvas.height);
14 | return data || "";
15 | };
16 |
17 | function canvas2Base64(canvas: any, width: any, height: any) {
18 | const retCanvas = document.createElement("canvas");
19 | const retCtx: any = retCanvas.getContext("2d");
20 | retCanvas.width = width;
21 | retCanvas.height = height;
22 | retCtx.drawImage(canvas, 0, 0, width, height, 0, 0, width, height);
23 | const Base64 = retCanvas.toDataURL("image/jpeg"); // 可以根据需要更改格式
24 | return Base64;
25 | }
26 | // 文件大小
27 | enum FileSizes {
28 | "K" = 1024,
29 | "M" = 1048576,
30 | "G" = 1073741824,
31 | "T" = 1099511627776
32 | }
33 |
34 | // 文件图标
35 | enum Icons {
36 | "doc" = "iconword",
37 | "docx" = "iconword",
38 | "xls" = "iconexcel",
39 | "xlsx" = "iconexcel",
40 | "ppt" = "iconppt",
41 | "pptx" = "iconppt",
42 | "pdf" = "iconpdf",
43 | "zip" = "iconzip",
44 | "rar" = "iconzip",
45 | "jpg" = "iconimage",
46 | "jpeg" = "iconimage",
47 | "png" = "iconimage",
48 | "gif" = "iconimage",
49 | "bmp" = "iconimage",
50 | "file" = "iconcolorfile"
51 | }
52 |
53 | // 计算文件大小
54 | function calcFileSize(fileByte: number): string {
55 | const KB = FileSizes.K;
56 | const MB = FileSizes.M;
57 | const GB = FileSizes.G;
58 | const TB = FileSizes.T;
59 | const FIXED_TWO_POINT = 2;
60 | let fileSizeMsg = "";
61 | if (fileByte < KB) {
62 | fileSizeMsg = "文件小于1K";
63 | } else if (fileByte > KB && fileByte < MB) {
64 | fileSizeMsg = (fileByte / KB).toFixed(FIXED_TWO_POINT) + "K";
65 | } else if (fileByte === MB) {
66 | fileSizeMsg = "1M";
67 | } else if (fileByte > MB && fileByte < GB) {
68 | fileSizeMsg = (fileByte / (KB * KB)).toFixed(FIXED_TWO_POINT) + "M";
69 | } else if (fileByte > MB && fileByte === GB) {
70 | fileSizeMsg = "1G";
71 | } else if (fileByte > GB && fileByte < TB) {
72 | fileSizeMsg = (fileByte / (KB * KB * KB)).toFixed(FIXED_TWO_POINT) + "G";
73 | } else {
74 | fileSizeMsg = "文件超过1T";
75 | }
76 | return fileSizeMsg;
77 | }
78 |
79 | // 获取文件后缀
80 | function getFileSuffix(fileName: string): string {
81 | const pointIndex: number = fileName.lastIndexOf(".");
82 | let suffix: string;
83 | if (pointIndex > -1) {
84 | suffix = fileName.slice(pointIndex + 1);
85 | } else {
86 | suffix = "file";
87 | }
88 | return suffix;
89 | }
90 |
91 | // 获取文件图标
92 | function getFileIcon(fileName: string): string {
93 | const suffix: string = getFileSuffix(fileName);
94 | return Icons["doc"] || suffix;
95 | }
96 |
97 | // 下载文件
98 | function downloadByURI(data: string, fileName: string, header: string = "") {
99 | const link = document.createElement("a");
100 | link.style.display = "none";
101 | link.href = header + data;
102 | link.download = fileName;
103 | document.body.appendChild(link);
104 | link.click();
105 | document.body.removeChild(link);
106 | }
107 |
108 | function downloadByBlob() {}
109 |
110 | export { calcFileSize, getFileIcon, downloadByURI, downloadByBlob, createBodyImg };
111 |
--------------------------------------------------------------------------------
/src/utils/socketDispatch.ts:
--------------------------------------------------------------------------------
1 | import { notification } from "antd";
2 | const SocketDispatch = (payload: any, MySocketID: string) => {
3 | const { name } = payload;
4 | switch (name) {
5 | case "STSTEM":
6 | SOCKET_SYSTE(payload, MySocketID);
7 | return;
8 | case "MESSAGE":
9 | getOnlineUsers(payload, MySocketID);
10 | return;
11 | default:
12 | SOCKET_lOGIN(payload, MySocketID);
13 | }
14 | };
15 |
16 | const SOCKET_lOGIN = async (payload: any, MySocketID: string) => {
17 | const { message, name, id } = payload;
18 | if (name === "loginMessage") {
19 | if (id === MySocketID) {
20 | // TODO 我自己上线,不用通知我,其实此时需要判断有没有登录,没登录,就不要提示了。
21 | return;
22 | }
23 | notification.open({
24 | message: "通知",
25 | description: message
26 | });
27 | }
28 | };
29 | const SOCKET_SYSTE = async (payload: any) => {
30 | const { name } = payload;
31 | switch (name) {
32 | case "html2canvas":
33 | return;
34 | }
35 | };
36 |
37 | const getOnlineUsers = async (payload: any) => {
38 | const { func_call, data } = payload;
39 | const { OnlineUser } = data;
40 | switch (func_call) {
41 | case "getOnlineUsers":
42 | return OnlineUser;
43 | }
44 | };
45 |
46 | export default SocketDispatch;
47 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare const __API_URL__: string;
3 | declare const NODE_ENV: string;
4 | declare const REACT_PACKAGE: {
5 | dependencies;
6 | devDependencies;
7 | version: string;
8 | lastBuildTime: string;
9 | };
10 | declare const REACT_APP_API_URL: string;
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": false,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": false,
21 | "noImplicitAny": false,
22 | "noFallthroughCasesInSwitch": true,
23 | "paths": {
24 | "@/*": ["./src/*"]
25 | }
26 | },
27 | "include": ["src"],
28 | "references": [{ "path": "./tsconfig.node.json" }]
29 | }
30 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { defineConfig } from "vite";
3 | // https://vitejs.dev/config/
4 | import react from "@vitejs/plugin-react";
5 | import dayjs from "dayjs";
6 | import { visualizer } from "rollup-plugin-visualizer";
7 | import viteCompression from "vite-plugin-compression";
8 | import { dependencies, devDependencies, name, version } from "./package.json";
9 |
10 | import { createHtmlPlugin } from "vite-plugin-html";
11 |
12 | const isDev = process.env.NODE_ENV === "development";
13 | const httpUrl = isDev ? "https://nest-admin.com/micro/api/" : "https://nest-admin.com/micro/api/";
14 |
15 | export default defineConfig({
16 | plugins: [
17 | react(),
18 | createHtmlPlugin({
19 | minify: true,
20 | /**
21 | * 在这里写entry后,你将不需要在`index.html`内添加 script 标签,原有标签需要删除
22 | * @default src/main.ts
23 | */
24 | entry: "/src/main.tsx",
25 | /**
26 | * 需要注入 index.html ejs 模版的数据
27 | */
28 | template: "public/index.html",
29 | inject: {
30 | data: {
31 | // 查找.env.test文件里面的VITE_PROJECT_TITLE,请以VITE_标识开头
32 | title: "编程的奇思妙想" || name,
33 | injectScript: ``
34 | },
35 | tags: [
36 | {
37 | injectTo: "body-prepend",
38 | tag: "div",
39 | attrs: {
40 | id: "tag"
41 | }
42 | }
43 | ]
44 | }
45 | }),
46 | // 构建压缩文件
47 | viteCompression({
48 | // 记录压缩文件及其压缩率。默认true
49 | verbose: true,
50 | // 是否启用压缩,默认false
51 | disable: false,
52 | // 需要使用压缩前的最小文件大小,单位字节(byte) b,1b(字节)=8bit(比特), 1KB=1024B
53 | threshold: 10240, // 即10kb以上即会压缩
54 | // 压缩算法 可选 'gzip' | 'brotliCompress' | 'deflate' | 'deflateRaw'
55 | algorithm: "gzip",
56 | // 压缩后的文件格式
57 | ext: ".gz"
58 | }),
59 | visualizer({
60 | emitFile: false,
61 | filename: "analysis-chart.html", // 分析图生成的文件名
62 | open: true // 如果存在本地服务端口,将在打包后自动展示
63 | })
64 | ],
65 | define: {
66 | REACT_APP_API_URL: JSON.stringify(httpUrl),
67 | REACT_PACKAGE: JSON.stringify({
68 | dependencies,
69 | devDependencies,
70 | name,
71 | version,
72 | lastBuildTime: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss")
73 | }),
74 | NODE_ENV: JSON.stringify(process.env.NODE_ENV)
75 | },
76 | resolve: {
77 | alias: {
78 | "@": path.resolve(__dirname, "./src")
79 | }
80 | },
81 | preview: {
82 | proxy: {
83 | // 使用 proxy 实例
84 | // "/nest3011": {
85 | // target: "https://nest-admin.com/nest3011/",
86 | // changeOrigin: true,
87 | // secure: false,
88 | // configure: (proxy, options) => {
89 | // return proxy;
90 | // },
91 | // },
92 | }
93 | },
94 | server: {
95 | hmr: {}
96 | },
97 | build: {
98 | minify: "terser", // 必须开启:使用 terserOptions 才有效果
99 | terserOptions: {
100 | compress: {
101 | drop_console: process.env.NODE_ENV === "production" ? true : false,
102 | drop_debugger: process.env.NODE_ENV === "production" ? true : false
103 | }
104 | },
105 | rollupOptions: {
106 | // 静态资源分类打包
107 | output: {
108 | // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
109 | globals: {},
110 | chunkFileNames: "js/[name]-[hash].js",
111 | entryFileNames: "js/[name]-[hash].js",
112 | assetFileNames: "[ext]/[name]-[hash].[ext]",
113 | manualChunks(id) {
114 | // 静态资源分拆打包
115 | if (id.includes("node_modules")) {
116 | return id.toString().split("node_modules/")[1].split("/")[0].toString();
117 | }
118 | }
119 | }
120 | }
121 | }
122 | });
123 |
--------------------------------------------------------------------------------