Yansen пре 2 година
комит
617bb7e702
69 измењених фајлова са 3202 додато и 0 уклоњено
  1. 25
    0
      .gitignore
  2. 13
    0
      index.html
  3. 8
    0
      jsconfig.json
  4. 36
    0
      package.json
  5. BIN
      public/logo.png
  6. 110
    0
      public/particles/particles.json
  7. 9
    0
      public/particles/particles.min.js
  8. 1
    0
      public/vite.svg
  9. 42
    0
      src/App.css
  10. 10
    0
      src/App.jsx
  11. 1
    0
      src/assets/react.svg
  12. 15
    0
      src/components/EditableTag/Tag.jsx
  13. 21
    0
      src/components/EditableTag/index.jsx
  14. 27
    0
      src/components/EditableTag/style.less
  15. 4
    0
      src/components/Money/float.js
  16. 30
    0
      src/components/Money/index.jsx
  17. 103
    0
      src/components/Page/Edit.jsx
  18. 90
    0
      src/components/Page/List.jsx
  19. 23
    0
      src/components/Page/index.jsx
  20. 87
    0
      src/components/Wangeditor/index.jsx
  21. 81
    0
      src/components/chart/index.jsx
  22. 76
    0
      src/index.less
  23. 31
    0
      src/layouts/AuthLayout/components/Container.jsx
  24. 15
    0
      src/layouts/AuthLayout/components/Footer.jsx
  25. 28
    0
      src/layouts/AuthLayout/components/Header/Exit.jsx
  26. 24
    0
      src/layouts/AuthLayout/components/Header/Title.jsx
  27. 88
    0
      src/layouts/AuthLayout/components/Header/User.jsx
  28. 28
    0
      src/layouts/AuthLayout/components/Header/index.jsx
  29. 19
    0
      src/layouts/AuthLayout/components/HtmlTitle.jsx
  30. 15
    0
      src/layouts/AuthLayout/components/Logo.jsx
  31. 26
    0
      src/layouts/AuthLayout/components/Menus.jsx
  32. 21
    0
      src/layouts/AuthLayout/components/PageTransition/index.jsx
  33. 21
    0
      src/layouts/AuthLayout/components/PageTransition/style.less
  34. 21
    0
      src/layouts/AuthLayout/components/RequireLogin.jsx
  35. 22
    0
      src/layouts/AuthLayout/components/SiderBar.jsx
  36. 41
    0
      src/layouts/AuthLayout/index.jsx
  37. 77
    0
      src/layouts/AuthLayout/style.less
  38. 23
    0
      src/layouts/PageContainer.jsx
  39. 11
    0
      src/main.jsx
  40. 29
    0
      src/pages/404/index.jsx
  41. 11
    0
      src/pages/login/Effect.jsx
  42. 20
    0
      src/pages/login/LoginForm.jsx
  43. 60
    0
      src/pages/login/index.jsx
  44. 96
    0
      src/pages/login/style.less
  45. 352
    0
      src/pages/sample/form/index.jsx
  46. 191
    0
      src/pages/sample/home/components/AreaChart.jsx
  47. 44
    0
      src/pages/sample/home/components/Banner.jsx
  48. 37
    0
      src/pages/sample/home/components/BarChart.jsx
  49. 112
    0
      src/pages/sample/home/components/PictorialBar.jsx
  50. 48
    0
      src/pages/sample/home/components/PieChart.jsx
  51. 48
    0
      src/pages/sample/home/components/RadarChart.jsx
  52. 40
    0
      src/pages/sample/home/index.jsx
  53. 170
    0
      src/pages/sample/table/index.jsx
  54. 18
    0
      src/routes/Router.jsx
  55. 48
    0
      src/routes/menus.jsx
  56. 14
    0
      src/routes/permissions.js
  57. 112
    0
      src/routes/routes.jsx
  58. 14
    0
      src/store/index.js
  59. 20
    0
      src/store/models/system.js
  60. 37
    0
      src/store/models/user.js
  61. 61
    0
      src/utils/array.js
  62. 8
    0
      src/utils/css.js
  63. 5
    0
      src/utils/float.js
  64. 13
    0
      src/utils/hooks/useBool.js
  65. 36
    0
      src/utils/hooks/usePrompt.jsx
  66. 9
    0
      src/utils/hooks/useRoute.jsx
  67. 19
    0
      src/utils/observe.js
  68. 172
    0
      src/utils/request.js
  69. 35
    0
      vite.config.js

+ 25
- 0
.gitignore Прегледај датотеку

@@ -0,0 +1,25 @@
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
+pnpm-lock.yaml
10
+
11
+node_modules
12
+dist
13
+dist-ssr
14
+*.local
15
+
16
+# Editor directories and files
17
+.vscode/*
18
+!.vscode/extensions.json
19
+.idea
20
+.DS_Store
21
+*.suo
22
+*.ntvs*
23
+*.njsproj
24
+*.sln
25
+*.sw?

+ 13
- 0
index.html Прегледај датотеку

@@ -0,0 +1,13 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+  <head>
4
+    <meta charset="UTF-8" />
5
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+    <title>Vite + React</title>
8
+  </head>
9
+  <body>
10
+    <div id="root"></div>
11
+    <script type="module" src="/src/main.jsx"></script>
12
+  </body>
13
+</html>

+ 8
- 0
jsconfig.json Прегледај датотеку

@@ -0,0 +1,8 @@
1
+{
2
+  "compilerOptions": {
3
+    "baseUrl": ".",
4
+    "paths": {
5
+      "@/*": ["./src/*"]
6
+    }
7
+  }
8
+}

+ 36
- 0
package.json Прегледај датотеку

@@ -0,0 +1,36 @@
1
+{
2
+  "name": "vite-project",
3
+  "private": true,
4
+  "version": "0.0.0",
5
+  "type": "module",
6
+  "scripts": {
7
+    "dev": "vite",
8
+    "build": "vite build",
9
+    "preview": "vite preview"
10
+  },
11
+  "dependencies": {
12
+    "@ant-design/icons": "^4.7.0",
13
+    "@ant-design/pro-components": "^2.3.13",
14
+    "@wangeditor/editor": "^5.1.23",
15
+    "@wangeditor/editor-for-react": "^1.0.6",
16
+    "@zjxpcyc/react-tiny-store": "^2.0.1",
17
+    "antd": "^4.23.4",
18
+    "axios": "^1.2.0",
19
+    "classnames": "^2.3.2",
20
+    "echarts": "^5.4.0",
21
+    "md5": "^2.3.0",
22
+    "react": "18.1.0",
23
+    "react-dom": "18.1.0",
24
+    "react-helmet": "^6.1.0",
25
+    "react-router-dom": "^6.4.2",
26
+    "react-transition-group": "^4.4.5"
27
+  },
28
+  "devDependencies": {
29
+    "@types/react": "^18.0.15",
30
+    "@types/react-dom": "^18.0.6",
31
+    "@vitejs/plugin-react": "^2.0.0",
32
+    "less": "^4.1.3",
33
+    "vite": "^3.0.0",
34
+    "vite-plugin-imp": "^2.2.0"
35
+  }
36
+}


+ 110
- 0
public/particles/particles.json Прегледај датотеку

@@ -0,0 +1,110 @@
1
+{
2
+  "particles": {
3
+    "number": {
4
+      "value": 80,
5
+      "density": {
6
+        "enable": true,
7
+        "value_area": 800
8
+      }
9
+    },
10
+    "color": {
11
+      "value": "#1890ff"
12
+    },
13
+    "shape": {
14
+      "type": "circle",
15
+      "stroke": {
16
+        "width": 0,
17
+        "color": "#000000"
18
+      },
19
+      "polygon": {
20
+        "nb_sides": 5
21
+      },
22
+      "image": {
23
+        "src": "img/github.svg",
24
+        "width": 100,
25
+        "height": 100
26
+      }
27
+    },
28
+    "opacity": {
29
+      "value": 0.5,
30
+      "random": false,
31
+      "anim": {
32
+        "enable": false,
33
+        "speed": 1,
34
+        "opacity_min": 0.1,
35
+        "sync": false
36
+      }
37
+    },
38
+    "size": {
39
+      "value": 3,
40
+      "random": true,
41
+      "anim": {
42
+        "enable": false,
43
+        "speed": 40,
44
+        "size_min": 0.1,
45
+        "sync": false
46
+      }
47
+    },
48
+    "line_linked": {
49
+      "enable": true,
50
+      "distance": 150,
51
+      "color": "#1890ff",
52
+      "opacity": 0.4,
53
+      "width": 1
54
+    },
55
+    "move": {
56
+      "enable": true,
57
+      "speed": 2,
58
+      "direction": "none",
59
+      "random": false,
60
+      "straight": false,
61
+      "out_mode": "out",
62
+      "bounce": false,
63
+      "attract": {
64
+        "enable": false,
65
+        "rotateX": 600,
66
+        "rotateY": 1200
67
+      }
68
+    }
69
+  },
70
+  "interactivity": {
71
+    "detect_on": "canvas",
72
+    "events": {
73
+      "onhover": {
74
+        "enable": false,
75
+        "mode": "repulse"
76
+      },
77
+      "onclick": {
78
+        "enable": false,
79
+        "mode": "push"
80
+      },
81
+      "resize": true
82
+    },
83
+    "modes": {
84
+      "grab": {
85
+        "distance": 400,
86
+        "line_linked": {
87
+          "opacity": 1
88
+        }
89
+      },
90
+      "bubble": {
91
+        "distance": 400,
92
+        "size": 40,
93
+        "duration": 2,
94
+        "opacity": 8,
95
+        "speed": 3
96
+      },
97
+      "repulse": {
98
+        "distance": 200,
99
+        "duration": 0.4
100
+      },
101
+      "push": {
102
+        "particles_nb": 4
103
+      },
104
+      "remove": {
105
+        "particles_nb": 2
106
+      }
107
+    }
108
+  },
109
+  "retina_detect": true
110
+}

+ 9
- 0
public/particles/particles.min.js
Разлика између датотеке није приказан због своје велике величине
Прегледај датотеку


+ 1
- 0
public/vite.svg Прегледај датотеку

@@ -0,0 +1 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

+ 42
- 0
src/App.css Прегледај датотеку

@@ -0,0 +1,42 @@
1
+#root {
2
+  max-width: 1280px;
3
+  margin: 0 auto;
4
+  padding: 2rem;
5
+  text-align: center;
6
+}
7
+
8
+.logo {
9
+  height: 6em;
10
+  padding: 1.5em;
11
+  will-change: filter;
12
+}
13
+.logo:hover {
14
+  filter: drop-shadow(0 0 2em #646cffaa);
15
+}
16
+.logo.react:hover {
17
+  filter: drop-shadow(0 0 2em #61dafbaa);
18
+}
19
+
20
+
21
+@keyframes logo-spin {
22
+  from {
23
+    transform: rotate(0deg);
24
+  }
25
+  to {
26
+    transform: rotate(360deg);
27
+  }
28
+}
29
+
30
+@media (prefers-reduced-motion: no-preference) {
31
+  a:nth-of-type(2) .logo {
32
+    animation: logo-spin infinite 20s linear;
33
+  }
34
+}
35
+
36
+.card {
37
+  padding: 2em;
38
+}
39
+
40
+.read-the-docs {
41
+  color: #888;
42
+}

+ 10
- 0
src/App.jsx Прегледај датотеку

@@ -0,0 +1,10 @@
1
+import { useState } from 'react'
2
+
3
+
4
+function App() {
5
+  const [count, setCount] = useState(0)
6
+
7
+  return null;
8
+}
9
+
10
+export default App

+ 1
- 0
src/assets/react.svg Прегледај датотеку

@@ -0,0 +1 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

+ 15
- 0
src/components/EditableTag/Tag.jsx Прегледај датотеку

@@ -0,0 +1,15 @@
1
+import React from 'react';
2
+import { Button } from 'antd';
3
+import { CloseOutlined } from '@ant-design/icons';
4
+import './style.less'
5
+
6
+export default (props) => {
7
+  const { size, type, onDelete, children } = props;
8
+
9
+  return (
10
+    <div className='tag-btn-group'>
11
+      <Button size={size} type={type}>{children}</Button>
12
+      <Button size={size} type="primary" icon={<CloseOutlined />} onClick={onDelete} />
13
+    </div>
14
+  )
15
+}

+ 21
- 0
src/components/EditableTag/index.jsx Прегледај датотеку

@@ -0,0 +1,21 @@
1
+import React from 'react';
2
+import { Button } from 'antd';
3
+import Tag from './Tag';
4
+import './style.less'
5
+
6
+export default (props) => {
7
+  const { size, type, list = [], onDelete, keyFuc, labelFunc } = props;
8
+
9
+  return (
10
+    <div className='editable-tag-box'>
11
+      {
12
+        list.map((item, index) => {
13
+          const key = keyFuc(item, index);
14
+          const label = labelFunc(item, index);
15
+
16
+          return <Tag key={key} size={size} type={type} onDelete={() => onDelete(item)}>{label}</Tag>
17
+        })
18
+      }
19
+    </div>
20
+  )
21
+}

+ 27
- 0
src/components/EditableTag/style.less Прегледај датотеку

@@ -0,0 +1,27 @@
1
+.tag-btn-group {
2
+  .ant-btn {
3
+    vertical-align: middle;
4
+  }
5
+
6
+  .ant-btn:first-child {
7
+    border-right: none;
8
+  }
9
+
10
+  .ant-btn:last-child {
11
+    // border-left: none;
12
+    border-top-left-radius: 0;
13
+    border-bottom-left-radius: 0;
14
+    margin-left: -1px;
15
+  }
16
+}
17
+
18
+.editable-tag-box {
19
+  display: flex;
20
+  flex-wrap: wrap;
21
+
22
+  .tag-btn-group {
23
+    flex: none;
24
+    box-sizing: border-box;
25
+    padding: 1em;
26
+  }
27
+}

+ 4
- 0
src/components/Money/float.js Прегледај датотеку

@@ -0,0 +1,4 @@
1
+const epsilonN = N => num => Math.round( num * N + Number.EPSILON ) / N;
2
+const epsilon2 = epsilonN(1e2);
3
+
4
+export default epsilon2;

+ 30
- 0
src/components/Money/index.jsx Прегледај датотеку

@@ -0,0 +1,30 @@
1
+import { InputNumber } from "antd"
2
+import { useEffect, useState } from "react"
3
+import epsilon2 from './float'
4
+
5
+export default (props) => {
6
+
7
+  const { value, onChange, ...leftProps } = props
8
+
9
+  const [money, setMoney] = useState(0)
10
+
11
+  useEffect(() => {
12
+    setMoney(epsilon2(value / 100))
13
+  }, [value])
14
+
15
+  const handleChange = (val) => {
16
+    onChange(epsilon2(val * 100))
17
+  }
18
+
19
+  return (
20
+    <InputNumber
21
+      min='0'
22
+      {...leftProps}
23
+      value={money}
24
+      onChange={handleChange}
25
+      precision={2}
26
+      formatter={value => `¥ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
27
+      parser={value => value.replace(/\¥\s?|(,*)/g, '')}
28
+    />
29
+  )
30
+}

+ 103
- 0
src/components/Page/Edit.jsx Прегледај датотеку

@@ -0,0 +1,103 @@
1
+import React from 'react';
2
+import { Button, Card, Form } from 'antd';
3
+import { useSearchParams, useNavigate } from "react-router-dom";
4
+import useBool from '@/utils/hooks/useBool';
5
+import { restful } from '@/utils/request';
6
+import Page from './index';
7
+
8
+const FormItem = Form.Item;
9
+const formItemLayout = { labelCol: { span: 6 }, wrapperCol: { span: 14 } };
10
+
11
+export default (props) => {
12
+  /**
13
+   * resource 用来确定应该调用哪个接口
14
+   * dataFunc 将 form 表单数据转换为接口需要格式, 一般情况下不需要这个操作
15
+   * formDataFunc 将接口返回的值转换为 form 表单数据格式, 一般情况下不需要这个操作
16
+   * width form 表单宽度, 默认 800px
17
+   */
18
+  const { resource, dataFunc, formDataFunc, width = '800px' } = props;
19
+  const [loading, startLoading, stopLoading] = useBool();
20
+  const [form] = Form.useForm();
21
+  const navigate = useNavigate();
22
+
23
+  // 获取 id query 参数, 如果存在说明是编辑,而不是新增
24
+  const [params] = useSearchParams();
25
+  const id = params.get("id");
26
+
27
+  // 接口汇总
28
+  const api = React.useMemo(() => {
29
+    // 通过 resource 知道是哪个接口
30
+    // 通过 restfule 生成 增删改查的接口
31
+    const apis = restful(resource);
32
+
33
+    // 返回三个接口
34
+    // 1. 获取详情
35
+    // 2. 新增
36
+    // 3. 更新
37
+    return {
38
+      get: apis[1],
39
+      save: apis[2],
40
+      update: apis[3],
41
+    }
42
+  }, [resource]);
43
+  
44
+  const navBack = () => {
45
+    const t = setTimeout(() => {
46
+      navigate(-1);
47
+      clearTimeout(t);
48
+    }, 600);
49
+  }
50
+
51
+  // 表单提交
52
+  const onFinish = (values) => {    
53
+    startLoading();
54
+    // 可能需要转换下格式
55
+    const data = dataFunc ? dataFunc(id, values) : values;
56
+    if (id) {
57
+      api.update(id, data).then(() => {
58
+        stopLoading();
59
+        navBack();
60
+      }).catch(stopLoading);
61
+    } else {
62
+      api.save(data)
63
+        .then(() => {
64
+          stopLoading();
65
+          navBack();
66
+        }).catch(stopLoading);
67
+    }
68
+  }
69
+
70
+  // 查询详情
71
+  React.useEffect(() => {
72
+    if (id) {
73
+      api.get(id).then(res => {
74
+        // 此处可能需要转换下数据格式
75
+        const formData = formDataFunc ? formDataFunc(res) : res;
76
+        form.setFieldsValue(formData);
77
+      })
78
+    }
79
+  }, [id]);
80
+
81
+  return (
82
+    <Page>
83
+      <Card>
84
+        <Form {...formItemLayout} form={form} onFinish={onFinish} style={{width}}>
85
+          {props.children}
86
+          <FormItem label=" " colon={false}>
87
+            <Button type="default" onClick={() => navigate(-1)}>
88
+              取消
89
+            </Button>
90
+            <Button
91
+              type="primary"
92
+              htmlType="submit"
93
+              style={{ marginLeft: '4em' }}
94
+              loading={loading}
95
+            >
96
+              确认
97
+            </Button>
98
+          </FormItem>
99
+        </Form>
100
+      </Card>
101
+    </Page>
102
+  )
103
+}

+ 90
- 0
src/components/Page/List.jsx Прегледај датотеку

@@ -0,0 +1,90 @@
1
+import React, { useRef, useMemo } from 'react';
2
+import { Button, Popconfirm, message } from 'antd';
3
+import { ProTable } from '@ant-design/pro-components';
4
+import { Link, useNavigate } from "react-router-dom";
5
+import { queryTable, restful } from '@/utils/request';
6
+import Page from './index';
7
+
8
+export default (props) => {
9
+  /**
10
+   * resource 用来确定应该调用哪个接口
11
+   * rowKey 标识数据每一行的唯一键, 只能是字符串
12
+   * columns 定义表格每一列, 符合 ProTable 定义
13
+   * editURL 编辑页路由
14
+   */
15
+  const { resource, rowKey, columns, editURL } = props;
16
+  const actionRef = useRef();
17
+  const navigate = useNavigate();
18
+
19
+  // 接口汇总
20
+  const api = useMemo(() => {
21
+    // 通过 resource 知道是哪个接口
22
+    // 通过 restfule 生成 增删改查的接口
23
+    const apis = restful(resource);
24
+
25
+    // 返回两个,一个是列表查询, 一个是删除
26
+    return {
27
+      list: apis[0],
28
+      delete: apis[4],
29
+    }
30
+  }, [resource]);
31
+
32
+  // 响应 删除 操作
33
+  const onDelete = (row) => {
34
+    const hide = message.loading('操作中, 请稍候...');
35
+    api.delete(row[rowKey]).then(() => {
36
+      hide();
37
+      actionRef.current.reload();
38
+    }).catch(hide);
39
+  }
40
+
41
+  // 统一实现操作列
42
+  const cols = [
43
+    ...columns,
44
+    {
45
+      title: '操作',
46
+      valueType: 'option',
47
+      key: 'option',
48
+      ellipsis: true,
49
+      render: (_, record) => [
50
+        <Button style={{ padding: 0 }} type="link" key={1} onClick={() => { navigate(`${editURL}?id=${record[rowKey]}`) }}>
51
+          编辑
52
+        </Button>,
53
+        <Popconfirm
54
+          key={3}
55
+          title="您是否确认删除 ?"
56
+          onConfirm={() => onDelete(record)}
57
+          okText="确定"
58
+          cancelText="取消"
59
+        >
60
+          <Button style={{ padding: 0 }} type="link">
61
+            删除
62
+          </Button>
63
+        </Popconfirm>,
64
+      ],
65
+    },
66
+  ]
67
+
68
+  return (
69
+    <Page>
70
+      <ProTable
71
+        rowKey={rowKey}
72
+        columns={cols}
73
+        request={queryTable(api.list)}
74
+        cardBordered
75
+        actionRef={actionRef}
76
+        toolBarRender={() => [
77
+          <Button
78
+            key="1"
79
+            type="primary"
80
+            onClick={() => {
81
+              navigate(editURL);
82
+            }}
83
+          >
84
+            新增
85
+          </Button>,
86
+        ]}
87
+      />
88
+    </Page>
89
+  )
90
+}

+ 23
- 0
src/components/Page/index.jsx Прегледај датотеку

@@ -0,0 +1,23 @@
1
+import React from 'react';
2
+import { Typography } from 'antd';
3
+import useRoute from '@/utils/hooks/useRoute';
4
+
5
+const pageStyle = {
6
+  // margin: '24px 24px 0 24px',
7
+  margin: '24px',
8
+  minHeight: 'calc(100% - 48px)',
9
+}
10
+const { Title } = Typography;
11
+
12
+export default (props) => {
13
+  const { meta = {} } = useRoute() || {};
14
+  const style = meta.noLayout ? { height: '100%' } : pageStyle;
15
+  const title = props.title || meta.title;
16
+
17
+  return (
18
+    <div style={style}>
19
+      { title && !meta.noLayout && <Title level={4} style={{ paddingBottom: '12px' }}>{ title }</Title> }
20
+      {props.children}
21
+    </div>
22
+  )
23
+}

+ 87
- 0
src/components/Wangeditor/index.jsx Прегледај датотеку

@@ -0,0 +1,87 @@
1
+import React, { useState, useEffect } from "react";
2
+import "@wangeditor/editor/dist/css/style.css";
3
+import { Editor, Toolbar } from "@wangeditor/editor-for-react";
4
+
5
+// 工具栏配置参考
6
+// https://www.cnblogs.com/-roc/p/16400965.html
7
+
8
+const defaultStyle = {
9
+  border: "1px solid #ccc",
10
+  zIndex: 100,
11
+  marginTop: "15px"
12
+}
13
+
14
+function MyEditor(props) {
15
+
16
+  const style = React.useMemo(() => ({ ...defaultStyle, ...(props.style || {}) }), [props.style])
17
+
18
+  const {
19
+    value = "",
20
+    onChange = (e) => {
21
+      setHtml(e);
22
+    },
23
+    toolbarConfig = {
24
+      excludeKeys: ["group-image", "group-video"],
25
+    },
26
+    editorConfig = {
27
+      placeholder: "请输入内容...",
28
+    },
29
+    readonly = false,
30
+  } = props;
31
+  const [editor, setEditor] = useState(null); // 存储 editor 实例
32
+  const [html, setHtml] = useState("");
33
+ 
34
+  // 模拟 ajax 请求,异步设置 html
35
+  useEffect(() => {
36
+    setHtml(value || "");
37
+  }, [value]);
38
+
39
+  // 及时销毁 editor
40
+  useEffect(() => {
41
+    return () => {
42
+      if (editor == null) return;
43
+      editor.destroy();
44
+      setEditor(null);
45
+    };
46
+  }, [editor]);
47
+
48
+  function insertText() {
49
+    if (editor == null) return;
50
+    editor.insertText(" hello ");
51
+  }
52
+
53
+  function printHtml() {
54
+    if (editor == null) return;
55
+    console.log(editor.getHtml());
56
+  }
57
+
58
+  const handleChange = (editor) => {
59
+    const raw = editor.getHtml();
60
+    if (raw !== '<p><br></p>') {
61
+      onChange(raw)
62
+    }
63
+  }
64
+
65
+  return !readonly ? (
66
+    <div className={props.className} style={style}>
67
+      <Toolbar
68
+        editor={editor}
69
+        defaultConfig={toolbarConfig}
70
+        mode="default"
71
+        style={{ borderBottom: "1px solid #ccc" }}
72
+      />
73
+      <Editor
74
+        defaultConfig={editorConfig}
75
+        value={html}
76
+        onCreated={setEditor}
77
+        onChange={handleChange}
78
+        mode="default"
79
+        style={{ height: "500px" }}
80
+      />
81
+    </div>
82
+  ) : (
83
+    <div dangerouslySetInnerHTML={{ __html: value }}></div>
84
+  );
85
+}
86
+
87
+export default MyEditor;

+ 81
- 0
src/components/chart/index.jsx Прегледај датотеку

@@ -0,0 +1,81 @@
1
+import React, { useRef, useEffect } from 'react';
2
+
3
+// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
4
+import * as echarts from 'echarts/core';
5
+// 引入图表,图表后缀都为 Chart
6
+import {
7
+  BarChart,
8
+  PieChart,
9
+  LineChart,
10
+  GaugeChart,
11
+  RadarChart,
12
+  PictorialBarChart,
13
+} from 'echarts/charts';
14
+// 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component
15
+import {
16
+  TitleComponent,
17
+  TooltipComponent,
18
+  ToolboxComponent,
19
+  GridComponent,
20
+  DatasetComponent,
21
+  LegendComponent,
22
+  DataZoomComponent,
23
+  TransformComponent,
24
+  VisualMapComponent,
25
+} from 'echarts/components';
26
+// 标签自动布局,全局过渡动画等特性
27
+import { LabelLayout, UniversalTransition } from 'echarts/features';
28
+// 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
29
+import { CanvasRenderer } from 'echarts/renderers';
30
+
31
+// 注册必须的组件
32
+echarts.use([
33
+  TitleComponent,
34
+  TooltipComponent,
35
+  ToolboxComponent,
36
+  GridComponent,
37
+  DatasetComponent,
38
+  LegendComponent,
39
+  DataZoomComponent,
40
+  TransformComponent,
41
+  VisualMapComponent,
42
+  BarChart,
43
+  PieChart,
44
+  LineChart,
45
+  GaugeChart,
46
+  RadarChart,
47
+  PictorialBarChart,
48
+  LabelLayout,
49
+  UniversalTransition,
50
+  CanvasRenderer
51
+]);
52
+
53
+export default (props) => {
54
+  const { className, style, option } = props;
55
+
56
+  const domRef = useRef();
57
+  const chartRef = useRef();
58
+
59
+  useEffect(() => {
60
+    if (!chartRef.current) {
61
+      chartRef.current = echarts.init(domRef.current);
62
+    }
63
+
64
+    const resize = () => {
65
+      const t = setTimeout(() => {
66
+        clearTimeout(t);
67
+        chartRef.current.resize();
68
+      }, 100);
69
+    };
70
+
71
+    chartRef.current.setOption(option);
72
+    resize();
73
+
74
+    window.addEventListener('resize', resize);    
75
+    return () => window.removeEventListener('resize', resize);
76
+  }, [option]);
77
+
78
+  return (
79
+    <div className={className} style={style} ref={domRef}></div>
80
+  )
81
+}

+ 76
- 0
src/index.less Прегледај датотеку

@@ -0,0 +1,76 @@
1
+html, body, #root {
2
+  width: 100%;
3
+  height: 100%;
4
+  margin: 0;
5
+}
6
+
7
+:root {
8
+  --theme-color: #fff;
9
+  --theme-front: #000;
10
+  --header-height: 48px;
11
+  --siderbar-width: 240px;
12
+}
13
+
14
+.main-layout {
15
+  height: 100vh;
16
+  display: flex;
17
+  flex-direction: column;
18
+}
19
+
20
+.fixd-header {
21
+  width: 100%;
22
+  position: fixed;
23
+  top: 0;
24
+  height: var(--header-height);
25
+  line-height: var(--header-height);
26
+  z-index: 100;
27
+}
28
+
29
+.ant-layout {
30
+  background-color: #f0f2f5;
31
+}
32
+
33
+.ant-layout-header {
34
+  line-height: var(--header-height);
35
+  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
36
+  // border-bottom: 1px solid gainsboro;
37
+  z-index: 10;
38
+}
39
+
40
+.layout-sidebar {
41
+  width: var(--siderbar-width);
42
+  background-color: var(--theme-color);
43
+  color: var(--theme-front);
44
+  box-shadow: 1px 0 4px 0 rgba(0, 21, 41, 0.08);
45
+
46
+  .ant-menu {
47
+    background: transparent;
48
+  }
49
+}
50
+
51
+
52
+// 兼容 360
53
+.ant-pro-sider-collapsed-button {
54
+  top: 18px;
55
+  right: -13px;
56
+}
57
+
58
+.ant-pro .ant-pro-layout .ant-pro-sider-logo {
59
+  padding: 16px;
60
+}
61
+
62
+.ant-pro .ant-pro-query-filter .ant-form-item {
63
+  margin: 0;
64
+}
65
+
66
+.ant-pro-page-container .ant-pro-page-container-warp-page-header ~ .ant-pro-grid-content .ant-pro-page-container-children-content {
67
+  margin: 8px 40px 40px 40px;
68
+}
69
+
70
+.ant-pro-table-list-toolbar-container {
71
+  padding: 16px 0;
72
+}
73
+
74
+.ant-pro-card .ant-pro-card-body {
75
+  padding: 16px 24px;
76
+}

+ 31
- 0
src/layouts/AuthLayout/components/Container.jsx Прегледај датотеку

@@ -0,0 +1,31 @@
1
+import React from 'react';
2
+import { Layout } from 'antd';
3
+import { Outlet } from "react-router-dom";
4
+import PageTransition from './PageTransition';
5
+import Footer from './Footer';
6
+
7
+const { Content } = Layout;
8
+const marginSpace = 24;
9
+
10
+export default (props) => {
11
+  const { noFooter } = props;
12
+  const footerRef = React.useRef();
13
+  const [height, setHeight] = React.useState(`calc(100% - ${marginSpace * 2}px`);
14
+
15
+  React.useEffect(() => {
16
+    if (footerRef.current) {
17
+      setHeight(`calc(100% - ${footerRef.current.offsetHeight + marginSpace * 2}px)`);
18
+    }
19
+  }, [noFooter])
20
+
21
+  return (
22
+    <div className='layout-container'>
23
+      <Content style={{ minHeight: height, margin: `${marginSpace}px` }}>
24
+        <PageTransition location={props.location}>
25
+          <Outlet />
26
+        </PageTransition>
27
+      </Content>
28
+      {!noFooter && <Footer ref={footerRef} />}
29
+    </div>
30
+  )
31
+}

+ 15
- 0
src/layouts/AuthLayout/components/Footer.jsx Прегледај датотеку

@@ -0,0 +1,15 @@
1
+import React from 'react';
2
+import { Layout } from 'antd';
3
+import { useModel } from '@/store';
4
+
5
+const { Footer } = Layout;
6
+
7
+const year = new Date().getFullYear();
8
+export default React.forwardRef((props, ref) => {
9
+  const { app } = useModel('system');
10
+  const copyRight = `${app.company || '南京云致'} @ ${year}`;
11
+
12
+  return (
13
+    <Footer ref={ref} style={{ textAlign: 'center', color: 'rgba(0,0,0, 0.3)' }}>{copyRight}</Footer>
14
+  )
15
+})

+ 28
- 0
src/layouts/AuthLayout/components/Header/Exit.jsx Прегледај датотеку

@@ -0,0 +1,28 @@
1
+import React from 'react';
2
+import { useNavigate } from 'react-router-dom';
3
+import { LogoutOutlined } from '@ant-design/icons';
4
+import { Button, Modal } from 'antd';
5
+import { useModel } from '@/store';
6
+
7
+const { confirm } = Modal;
8
+
9
+export default (props) => {
10
+  const navigate = useNavigate();
11
+  const { setUser } = useModel('user');
12
+  
13
+  const onExit = () => {
14
+    confirm({
15
+      title: '确认退出系统?',
16
+      onOk: () => {
17
+        // logout(); // 调用接口
18
+        localStorage.removeItem('token');
19
+        setUser();
20
+        navigate('/login?back=true');
21
+      }
22
+    });
23
+  }
24
+
25
+  return (
26
+    <Button className='font' type="text" icon={<LogoutOutlined />} onClick={onExit}>退出</Button>
27
+  )
28
+}

+ 24
- 0
src/layouts/AuthLayout/components/Header/Title.jsx Прегледај датотеку

@@ -0,0 +1,24 @@
1
+
2
+import useRoute from '@/utils/hooks/useRoute';
3
+
4
+const titleStyle = {
5
+  margin: 0,
6
+}
7
+
8
+const spanStyle = {
9
+  display: 'inline-block',
10
+  width: '24px',
11
+  textAlign: 'center'
12
+}
13
+
14
+export default (props) => {
15
+  const route = useRoute();
16
+  const { title } = route && route.meta || {}
17
+
18
+  return (
19
+    <h5 style={titleStyle}>
20
+      { title && <span style={spanStyle}>&raquo;</span> }
21
+      { title && <span>{title}</span> }
22
+    </h5>
23
+  );
24
+}

+ 88
- 0
src/layouts/AuthLayout/components/Header/User.jsx Прегледај датотеку

@@ -0,0 +1,88 @@
1
+import React, { useState, forwardRef, useRef, useImperativeHandle } from 'react';
2
+import { Avatar, Button, Dropdown, Menu, Form, Input, Modal } from 'antd';
3
+import md5 from 'md5';
4
+
5
+const ChangePassword = forwardRef((props, ref) => {
6
+  const [visible, setVisible] = useState(false);
7
+
8
+  const onFinish = (values) => {
9
+    console.log('Success:', values);
10
+  };
11
+
12
+  useImperativeHandle(ref, () => {
13
+    return {
14
+      show: () => setVisible(true),
15
+    }
16
+  });
17
+
18
+  return (
19
+    <Modal title="修改密码" visible={visible} onCancel={() => setVisible(false)}>
20
+      <Form
21
+        labelCol={{ span: 8 }}
22
+        wrapperCol={{ span: 16 }}
23
+        onFinish={onFinish}
24
+        autoComplete="off"
25
+      >
26
+        <Form.Item
27
+          label="原始密码"
28
+          name="password"
29
+          rules={[{ required: true, message: '请输入原始密码!' }]}
30
+        >
31
+          <Input.Password />
32
+        </Form.Item>
33
+
34
+        <Form.Item
35
+          label="新密码"
36
+          name="newPassword"
37
+          rules={[{ required: true, message: '请输入新密码!' }]}
38
+        >
39
+          <Input.Password />
40
+        </Form.Item>
41
+
42
+        <Form.Item
43
+          label="确认新密码"
44
+          name="newPassword2"
45
+          rules={[{ required: true, message: '请输入新密码!' }]}
46
+        >
47
+          <Input.Password />
48
+        </Form.Item>
49
+
50
+        <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
51
+          <Button type="primary" htmlType="submit">
52
+            提交
53
+          </Button>
54
+        </Form.Item>
55
+      </Form>
56
+    </Modal>
57
+  )
58
+});
59
+
60
+export default (props) => {
61
+  const menuItems = [
62
+    {
63
+      key: 'changePassword',
64
+      label: '修改密码',
65
+    }
66
+  ];
67
+
68
+  const passRef = useRef();
69
+  const { user = {} } = props;
70
+
71
+  const onClick = ({ key }) => {
72
+    if (key === 'changePassword') {
73
+      passRef.current.show();
74
+    }
75
+  };
76
+
77
+  const menu = <Menu items={menuItems} onClick={onClick} />;
78
+
79
+  return (
80
+    <Dropdown overlay={menu}>
81
+      <div className="user-info">
82
+        <Avatar size={24} src="https://joeschmoe.io/api/v1/random" />
83
+        <span className='font'>{user.name}</span>
84
+        <ChangePassword ref={passRef} />
85
+      </div>
86
+    </Dropdown>
87
+  )
88
+}

+ 28
- 0
src/layouts/AuthLayout/components/Header/index.jsx Прегледај датотеку

@@ -0,0 +1,28 @@
1
+import React, { useMemo } from 'react';
2
+import { Layout, Space } from 'antd';
3
+import classNames from 'classnames';
4
+import Title from './Title';
5
+import User from './User';
6
+import Exit from './Exit';
7
+import Logo from '../Logo';
8
+
9
+const { Header } = Layout;
10
+
11
+export default (props) => {
12
+  const { user } = props;
13
+
14
+  const className = useMemo(() => classNames({
15
+    'layout-header': true,
16
+    'light': props.theme === 'light',
17
+  }), [props.theme]);
18
+
19
+  return (
20
+    <Header className={className}>
21
+      <Logo />
22
+      <Space>
23
+        <User user={user} />
24
+        <Exit />
25
+      </Space>
26
+    </Header>
27
+  )
28
+}

+ 19
- 0
src/layouts/AuthLayout/components/HtmlTitle.jsx Прегледај датотеку

@@ -0,0 +1,19 @@
1
+
2
+import { Helmet } from "react-helmet";
3
+import { useModel } from '@/store';
4
+import useRoute from '@/utils/hooks/useRoute';
5
+
6
+export default (props) => {
7
+  const { app } = useModel('system');
8
+  const route = useRoute();
9
+  const { title } = route && route.meta || {}
10
+  
11
+  const titleTemplate = `%s - ${app.fullName}`;
12
+  const defaultTitle = app.fullName;
13
+
14
+  return (
15
+    <Helmet titleTemplate={titleTemplate} defaultTitle={defaultTitle}>
16
+      <title>{title}</title>
17
+    </Helmet>
18
+  );
19
+}

+ 15
- 0
src/layouts/AuthLayout/components/Logo.jsx Прегледај датотеку

@@ -0,0 +1,15 @@
1
+import React from 'react';
2
+import { Typography } from 'antd';
3
+import { NavLink } from "react-router-dom";
4
+import { useModel } from '@/store';
5
+
6
+export default (props) => {
7
+  const { app } = useModel('system');
8
+
9
+  return (
10
+    <NavLink className='logo'  to="/">
11
+      <img src="./logo.png" alt="" />
12
+      <h3>{app.shorName}</h3>
13
+    </NavLink>
14
+  )
15
+}

+ 26
- 0
src/layouts/AuthLayout/components/Menus.jsx Прегледај датотеку

@@ -0,0 +1,26 @@
1
+import React from 'react';
2
+import { Menu } from 'antd';
3
+
4
+const menuStyle = { height: '100%' };
5
+
6
+export default (props) => {
7
+  const { theme, items, location } = props;
8
+
9
+  // const selectedKeys = React.useMemo(() => {
10
+  //   const parts = location.pathname.split('/').filter(Boolean);
11
+  //   const keys = parts.reduce((acc, it) => {
12
+  //     const parent = acc.pop();
13
+  //     const path = !parent ? `/${it}` : `${parent}/${it}`
14
+
15
+  //     return acc.concat([parent, path].filter(Boolean));
16
+  //   }, []);
17
+
18
+  //   return keys;
19
+  // }, [location.pathname]);
20
+
21
+  const selectedKeys = [location.pathname];
22
+
23
+  return (
24
+    <Menu style={menuStyle} theme={theme} items={items} selectedKeys={selectedKeys} />
25
+  )
26
+}

+ 21
- 0
src/layouts/AuthLayout/components/PageTransition/index.jsx Прегледај датотеку

@@ -0,0 +1,21 @@
1
+import { TransitionGroup, CSSTransition } from 'react-transition-group';
2
+import './style.less';
3
+
4
+export default (props) => {
5
+  const { location } = props;
6
+  const currentURL = location.pathname + location.search;
7
+
8
+  return (
9
+    <TransitionGroup component={null}>
10
+      <CSSTransition
11
+        key={currentURL}
12
+        addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
13
+        classNames="page-fade"
14
+      >
15
+        <div style={{ height: '100%' }}>
16
+          {props.children}
17
+        </div>
18
+      </CSSTransition>
19
+    </TransitionGroup>
20
+  );
21
+}

+ 21
- 0
src/layouts/AuthLayout/components/PageTransition/style.less Прегледај датотеку

@@ -0,0 +1,21 @@
1
+
2
+.page-fade-enter {
3
+  opacity: 0;
4
+  transform: translateY(10%);
5
+}
6
+.page-fade-enter-active {
7
+  opacity: 1;
8
+  transform: translateY(0%);
9
+}
10
+.page-fade-exit {
11
+  opacity: 1;
12
+  transform: translateY(0%);
13
+}
14
+.page-fade-exit-active {
15
+  opacity: 0;
16
+  transform: translateY(10%);
17
+}
18
+.page-fade-enter-active,
19
+.page-fade-exit-active {
20
+  transition: opacity 220ms, transform 220ms;
21
+}

+ 21
- 0
src/layouts/AuthLayout/components/RequireLogin.jsx Прегледај датотеку

@@ -0,0 +1,21 @@
1
+import React, { useState, useEffect } from 'react';
2
+import { useLocation, Navigate } from "react-router-dom";
3
+import { useModel } from '@/store';
4
+
5
+export default (props) => {
6
+  const { user, getCurrentUser } = useModel('user');
7
+  const [userStatus, setUserStatus] = useState(user && user.id ? 1 : 0);
8
+
9
+  useEffect(() => {
10
+    if (!user || !user.id) {
11
+      getCurrentUser().then(() => {
12
+        setUserStatus(1);
13
+      }).catch(() => {
14
+        setUserStatus(-1);
15
+      });
16
+    }
17
+  }, []);
18
+
19
+  return userStatus === 0 ? null :
20
+    userStatus === -1 ? <Navigate to="/login?back=true" /> : props.children;
21
+}

+ 22
- 0
src/layouts/AuthLayout/components/SiderBar.jsx Прегледај датотеку

@@ -0,0 +1,22 @@
1
+import React, { useMemo } from 'react';
2
+import { Layout, Spin } from 'antd';
3
+import { getPropertyValue } from '@/utils/css';
4
+import Menus from './Menus';
5
+
6
+const { Sider } = Layout;
7
+
8
+
9
+export default (props) => {
10
+  const { theme, location, menus } = props;
11
+
12
+  const width = useMemo(() => {
13
+    return /\d+/.exec(getPropertyValue('--siderbar-width'))[0] - 0;
14
+  }, []);
15
+
16
+
17
+  return (
18
+    <Sider className='layout-sidebar' theme={theme} collapsible width={width}>
19
+      <Menus theme={theme} items={menus} location={location} />
20
+    </Sider>
21
+  );
22
+}

+ 41
- 0
src/layouts/AuthLayout/index.jsx Прегледај датотеку

@@ -0,0 +1,41 @@
1
+import React, { useEffect } from 'react';
2
+import { Layout, Spin } from 'antd';
3
+import { useLocation, Outlet } from "react-router-dom";
4
+import { useModel } from '@/store';
5
+import useRoute from '@/utils/hooks/useRoute';
6
+import RequireLogin from './components/RequireLogin';
7
+import SiderBar from './components/SiderBar';
8
+import Header from './components/Header';
9
+import Container from './components/Container';
10
+import HtmlTitle from './components/HtmlTitle';
11
+
12
+import './style.less';
13
+
14
+export default (props) => {
15
+  const { theme } = useModel('system');
16
+  const { user, menus } = useModel('user');
17
+  const location = useLocation();
18
+  const { meta } = useRoute() || {};
19
+  const { noLayout = false, noSiderBar = false, noFooter = false } = meta || {};
20
+
21
+  return (
22
+    <Spin spinning={!user} size="large">
23
+      <HtmlTitle />
24
+      <RequireLogin>
25
+        {
26
+          noLayout
27
+            ? <Outlet />
28
+            : (
29
+              <Layout style={{ height: '100vh' }}>
30
+                <Header theme={theme} user={user} />
31
+                <Layout style={{ height: 'calc(100vh - var(--header-height))' }}>
32
+                  { !noSiderBar && <SiderBar theme={theme} menus={menus} location={location} /> }
33
+                  <Container location={location} noFooter={noFooter} />
34
+                </Layout>
35
+              </Layout>
36
+            )
37
+        }
38
+      </RequireLogin>
39
+    </Spin>
40
+  );
41
+}

+ 77
- 0
src/layouts/AuthLayout/style.less Прегледај датотеку

@@ -0,0 +1,77 @@
1
+.layout-header {
2
+  height: var(--header-height);
3
+  box-sizing: border-box;
4
+  padding: 0;
5
+  display: flex;
6
+  align-items: center;
7
+  justify-content: space-between;
8
+  color: #fff;
9
+
10
+  // &.light {
11
+  //   background-color: #fff;
12
+  //   color: #000;
13
+  // }
14
+
15
+  .header-content {
16
+    flex: 1;
17
+  }
18
+
19
+  .font {
20
+    color: inherit;
21
+    display: inline-block;
22
+  }
23
+
24
+  .user-info {
25
+    cursor: pointer;
26
+
27
+    .ant-avatar {
28
+      border: 1px solid rgba(255, 255, 255, 0.4);
29
+    }
30
+
31
+    span {
32
+      margin-left: 1em;
33
+    }
34
+  }
35
+
36
+  .sys-exit {
37
+    margin-right: 1em;
38
+  }
39
+}
40
+
41
+.logo {
42
+  height: var(--header-height);
43
+  display: flex;
44
+  align-items: center;
45
+  box-sizing: border-box;
46
+  padding-left: 1em;
47
+  color: inherit;
48
+
49
+  & > * {
50
+    color: inherit;
51
+    margin: 0;
52
+  }
53
+
54
+  h5 {
55
+    margin: 0;
56
+  }
57
+
58
+  img {
59
+    width: 28px;
60
+    vertical-align: middle;
61
+    margin-right: 1em;
62
+  }
63
+}
64
+
65
+.layout-container {
66
+  flex: 1;
67
+
68
+  overflow-y: auto;
69
+  scrollbar-width: none;
70
+  -ms-overflow-style: none;
71
+
72
+  &::--webkit-scrollbar {
73
+    display: none;
74
+  }
75
+
76
+  padding-top: 0; // 避免子元素的 margin 影响
77
+}

+ 23
- 0
src/layouts/PageContainer.jsx Прегледај датотеку

@@ -0,0 +1,23 @@
1
+import React from 'react';
2
+import { Typography } from 'antd';
3
+import { Outlet } from 'react-router-dom';
4
+import useRoute from '@/utils/hooks/useRoute';
5
+
6
+const containerStyle = {
7
+  // margin: '24px 24px 0 24px',
8
+  margin: '24px',
9
+  minHeight: 'calc(100% - 48px)',
10
+}
11
+const { Title } = Typography;
12
+
13
+export default (props) => {
14
+  const { meta = {} } = useRoute() || {};
15
+  const style = meta.noLayout ? { height: '100%' } : containerStyle;
16
+
17
+  return (
18
+    <div style={style}>
19
+      { meta.title && !meta.noLayout && <Title level={4} style={{ paddingBottom: '12px' }}>{ meta.title }</Title> }
20
+      <Outlet />
21
+    </div>
22
+  )
23
+}

+ 11
- 0
src/main.jsx Прегледај датотеку

@@ -0,0 +1,11 @@
1
+import React from 'react'
2
+import ReactDOM from 'react-dom/client'
3
+import Router from './routes/Router'
4
+import './index.less'
5
+import { Provider } from './store'
6
+
7
+ReactDOM.createRoot(document.getElementById('root')).render(
8
+  <Provider>
9
+    <Router />
10
+  </Provider>
11
+)

+ 29
- 0
src/pages/404/index.jsx Прегледај датотеку

@@ -0,0 +1,29 @@
1
+import React from 'react';
2
+import { Button, Result } from 'antd';
3
+import { Helmet } from "react-helmet";
4
+import { NavLink } from "react-router-dom";
5
+import { useModel } from '@/store';
6
+
7
+const style = {
8
+  display: 'grid',
9
+  placeItems: 'center',
10
+  height: '100%'
11
+}
12
+
13
+export default (props) => {
14
+  const { app } = useModel('system');
15
+
16
+  return (
17
+    <div style={style}>
18
+      <Helmet>
19
+        <title>{app.fullName}</title>
20
+      </Helmet>
21
+      <Result
22
+        status="404"
23
+        title="404"
24
+        subTitle="页面不存在"
25
+        extra={<NavLink to="/" replace><Button type="primary">返回首页</Button></NavLink>}
26
+      />
27
+    </div>
28
+  )
29
+}

+ 11
- 0
src/pages/login/Effect.jsx Прегледај датотеку

@@ -0,0 +1,11 @@
1
+import React from 'react'
2
+
3
+export default (props) => {
4
+  return (
5
+    <div className='login-effect-box'>
6
+      <div className="login-effect">
7
+        <div className="login-effect-arc"></div>
8
+      </div>
9
+    </div>
10
+  )
11
+}

+ 20
- 0
src/pages/login/LoginForm.jsx Прегледај датотеку

@@ -0,0 +1,20 @@
1
+import React from 'react';
2
+import { Button, Form, Input, Radio } from 'antd';
3
+
4
+export default (props) => {
5
+  const [form] = Form.useForm();
6
+
7
+  return (
8
+    <Form form={form} layout="vertical">
9
+      <Form.Item label="用户名" required>
10
+        <Input placeholder="请输入用户名" style={{ borderRadius: '4px' }} />
11
+      </Form.Item>
12
+      <Form.Item label="密 码" required>
13
+        <Input.Password placeholder="请输入密码" style={{ borderRadius: '4px' }} />
14
+      </Form.Item>
15
+      <Form.Item>
16
+        <Button type="primary" size='large' style={{ width: '100%', marginTop: '24px', borderRadius: '4px' }}>登录</Button>
17
+      </Form.Item>
18
+    </Form>
19
+  )
20
+}

+ 60
- 0
src/pages/login/index.jsx Прегледај датотеку

@@ -0,0 +1,60 @@
1
+import React, { useEffect } from 'react';
2
+import { Helmet } from "react-helmet";
3
+import { useModel } from '@/store';
4
+import LoginForm from './LoginForm';
5
+import LoginEffect from './Effect';
6
+import './style.less';
7
+
8
+const year = new Date().getFullYear();
9
+
10
+export default (props) => {
11
+  const { app } = useModel('system');
12
+
13
+  const baseUrl = import.meta.env.BASE_URL;
14
+  const title = `欢迎使用${app.fullName}`;
15
+  const copyright = `${app.company} @ ${year}`;
16
+
17
+  useEffect(() => {
18
+    const oScript = document.createElement("script");
19
+    oScript.type = "text\/javascript";
20
+    oScript.onload = () => {
21
+      particlesJS.load('particles-js', `${baseUrl}particles/particles.json`, function() {
22
+        console.log('callback - particles.js config loaded');
23
+      });
24
+    }
25
+    document.querySelector('head').appendChild(oScript);
26
+    oScript.src = `${baseUrl}particles/particles.min.js`;
27
+
28
+    return () => {
29
+      document.querySelector('head').removeChild(oScript);
30
+    }
31
+  }, [])
32
+
33
+  return (
34
+    <div className='login-page'>
35
+      <Helmet>
36
+        <title>{app.fullName}</title>
37
+      </Helmet>
38
+      <div id="particles-js"></div>
39
+      <div className="login-page-container">
40
+        <div className="login-card">
41
+          <div className="login-card-left">
42
+            <div className="login-form-box">
43
+              <div className="login-form">
44
+                <h2>登录系统</h2>
45
+                <div className="login-subtitle">{title}</div>
46
+                <LoginForm />
47
+              </div>
48
+            </div>
49
+            <div className="login-copyright">
50
+              {copyright}
51
+            </div>
52
+          </div>
53
+          <div className="login-card-right">
54
+            <LoginEffect />
55
+          </div>
56
+        </div>
57
+      </div>
58
+    </div>
59
+  )
60
+}

+ 96
- 0
src/pages/login/style.less Прегледај датотеку

@@ -0,0 +1,96 @@
1
+
2
+.login-page {
3
+  height: 100%;
4
+  background: linear-gradient(#fff, #e6f7ff);
5
+
6
+  #particles-js {
7
+    position: absolute;
8
+    top: 0;
9
+    left: 0;
10
+    width: 100%;
11
+    height: 100%;
12
+  }
13
+
14
+  .login-page-container {
15
+    height: 100%;
16
+    display: grid;
17
+    place-items: center;
18
+    position: relative;
19
+    z-index: 10;
20
+  }
21
+
22
+  .login-card {
23
+    width: 900px;
24
+    height: 600px;
25
+    border-radius: 4px;
26
+    overflow: hidden;
27
+    box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.2);
28
+    
29
+    display: grid;
30
+    grid-template-columns: 1fr 1fr;
31
+  }
32
+
33
+  .login-card-left {
34
+    background: rgba(255, 255, 255, 0.9);
35
+  }
36
+  .login-card-right {
37
+    background: #EFF6FA;
38
+  }
39
+
40
+  .login-form-box {
41
+    height: 560px;
42
+    display: grid;
43
+    place-items: center;
44
+
45
+    .login-form {
46
+      min-width: 280px;
47
+    }
48
+
49
+    .login-subtitle {
50
+      font-size: .9em;
51
+      color: #666;
52
+      margin-bottom: 24px;
53
+      letter-spacing: 2px;
54
+    }
55
+  }
56
+
57
+  .login-effect-box {
58
+    height: 100%;
59
+    display: grid;
60
+    place-items: center;
61
+
62
+    .login-effect {
63
+      width: 180px;
64
+      height: 180px;
65
+      position: relative;
66
+    }
67
+
68
+    .login-effect-arc {
69
+      width: 100%;
70
+      height: 100%;
71
+      border-radius: 50%;
72
+      background-color: #fa8c16;
73
+
74
+      &::after {
75
+        content: '';
76
+        width: 200%;
77
+        height: 100%;
78
+        top: 50%;
79
+        left: -50%;
80
+        position: absolute;
81
+        z-index: 10;
82
+        background-color: rgba(255, 255, 255, 0.001);
83
+        backdrop-filter: blur(16px);
84
+        border-top: 1px solid rgba(255, 255, 255, 0.2);
85
+      }
86
+    }
87
+  }
88
+
89
+  .login-copyright {
90
+    font-size: 12px;
91
+    color: #666;
92
+    line-height: 40px;
93
+    padding-left: 20px;
94
+    box-sizing: border-box;
95
+  }
96
+}

+ 352
- 0
src/pages/sample/form/index.jsx Прегледај датотеку

@@ -0,0 +1,352 @@
1
+import {
2
+  AutoComplete,
3
+  Button,
4
+  Cascader,
5
+  Checkbox,
6
+  Col,
7
+  Form,
8
+  Input,
9
+  InputNumber,
10
+  Row,
11
+  Select,
12
+  Card,
13
+} from 'antd';
14
+import React, { useState } from 'react';
15
+
16
+const { Option } = Select;
17
+const residences = [
18
+  {
19
+    value: 'zhejiang',
20
+    label: 'Zhejiang',
21
+    children: [
22
+      {
23
+        value: 'hangzhou',
24
+        label: 'Hangzhou',
25
+        children: [
26
+          {
27
+            value: 'xihu',
28
+            label: 'West Lake',
29
+          },
30
+        ],
31
+      },
32
+    ],
33
+  },
34
+  {
35
+    value: 'jiangsu',
36
+    label: 'Jiangsu',
37
+    children: [
38
+      {
39
+        value: 'nanjing',
40
+        label: 'Nanjing',
41
+        children: [
42
+          {
43
+            value: 'zhonghuamen',
44
+            label: 'Zhong Hua Men',
45
+          },
46
+        ],
47
+      },
48
+    ],
49
+  },
50
+];
51
+const formItemLayout = {
52
+  labelCol: {
53
+    xs: {
54
+      span: 24,
55
+    },
56
+    sm: {
57
+      span: 8,
58
+    },
59
+  },
60
+  wrapperCol: {
61
+    xs: {
62
+      span: 24,
63
+    },
64
+    sm: {
65
+      span: 16,
66
+    },
67
+  },
68
+};
69
+const tailFormItemLayout = {
70
+  wrapperCol: {
71
+    xs: {
72
+      span: 24,
73
+      offset: 0,
74
+    },
75
+    sm: {
76
+      span: 16,
77
+      offset: 8,
78
+    },
79
+  },
80
+};
81
+
82
+
83
+const BasicForm = () => {
84
+  const [form] = Form.useForm();
85
+
86
+  const onFinish = (values) => {
87
+    console.log('Received values of form: ', values);
88
+  };
89
+
90
+  const prefixSelector = (
91
+    <Form.Item name="prefix" noStyle>
92
+      <Select
93
+        style={{
94
+          width: 70,
95
+        }}
96
+      >
97
+        <Option value="86">+86</Option>
98
+        <Option value="87">+87</Option>
99
+      </Select>
100
+    </Form.Item>
101
+  );
102
+  const suffixSelector = (
103
+    <Form.Item name="suffix" noStyle>
104
+      <Select
105
+        style={{
106
+          width: 70,
107
+        }}
108
+      >
109
+        <Option value="USD">$</Option>
110
+        <Option value="CNY">¥</Option>
111
+      </Select>
112
+    </Form.Item>
113
+  );
114
+  const [autoCompleteResult, setAutoCompleteResult] = useState([]);
115
+
116
+  const onWebsiteChange = (value) => {
117
+    if (!value) {
118
+      setAutoCompleteResult([]);
119
+    } else {
120
+      setAutoCompleteResult(['.com', '.org', '.net'].map((domain) => `${value}${domain}`));
121
+    }
122
+  };
123
+
124
+  const websiteOptions = autoCompleteResult.map((website) => ({
125
+    label: website,
126
+    value: website,
127
+  }));
128
+  return (
129
+    <Form
130
+      {...formItemLayout}
131
+      form={form}
132
+      name="register"
133
+      onFinish={onFinish}
134
+      initialValues={{
135
+        residence: ['zhejiang', 'hangzhou', 'xihu'],
136
+        prefix: '86',
137
+      }}
138
+      scrollToFirstError
139
+      style={{ background: '#fff', boxSizing: 'border-box', padding: '24px' }}
140
+    >
141
+      <Form.Item
142
+        name="email"
143
+        label="E-mail"
144
+        rules={[
145
+          {
146
+            type: 'email',
147
+            message: 'The input is not valid E-mail!',
148
+          },
149
+          {
150
+            required: true,
151
+            message: 'Please input your E-mail!',
152
+          },
153
+        ]}
154
+      >
155
+        <Input />
156
+      </Form.Item>
157
+
158
+      <Form.Item
159
+        name="password"
160
+        label="Password"
161
+        rules={[
162
+          {
163
+            required: true,
164
+            message: 'Please input your password!',
165
+          },
166
+        ]}
167
+        hasFeedback
168
+      >
169
+        <Input.Password />
170
+      </Form.Item>
171
+
172
+      <Form.Item
173
+        name="confirm"
174
+        label="Confirm Password"
175
+        dependencies={['password']}
176
+        hasFeedback
177
+        rules={[
178
+          {
179
+            required: true,
180
+            message: 'Please confirm your password!',
181
+          },
182
+          ({ getFieldValue }) => ({
183
+            validator(_, value) {
184
+              if (!value || getFieldValue('password') === value) {
185
+                return Promise.resolve();
186
+              }
187
+
188
+              return Promise.reject(new Error('The two passwords that you entered do not match!'));
189
+            },
190
+          }),
191
+        ]}
192
+      >
193
+        <Input.Password />
194
+      </Form.Item>
195
+
196
+      <Form.Item
197
+        name="nickname"
198
+        label="Nickname"
199
+        tooltip="What do you want others to call you?"
200
+        rules={[
201
+          {
202
+            required: true,
203
+            message: 'Please input your nickname!',
204
+            whitespace: true,
205
+          },
206
+        ]}
207
+      >
208
+        <Input />
209
+      </Form.Item>
210
+
211
+      <Form.Item
212
+        name="residence"
213
+        label="Habitual Residence"
214
+        rules={[
215
+          {
216
+            type: 'array',
217
+            required: true,
218
+            message: 'Please select your habitual residence!',
219
+          },
220
+        ]}
221
+      >
222
+        <Cascader options={residences} />
223
+      </Form.Item>
224
+
225
+      <Form.Item
226
+        name="phone"
227
+        label="Phone Number"
228
+        rules={[
229
+          {
230
+            required: true,
231
+            message: 'Please input your phone number!',
232
+          },
233
+        ]}
234
+      >
235
+        <Input
236
+          addonBefore={prefixSelector}
237
+          style={{
238
+            width: '100%',
239
+          }}
240
+        />
241
+      </Form.Item>
242
+
243
+      <Form.Item
244
+        name="donation"
245
+        label="Donation"
246
+        rules={[
247
+          {
248
+            required: true,
249
+            message: 'Please input donation amount!',
250
+          },
251
+        ]}
252
+      >
253
+        <InputNumber
254
+          addonAfter={suffixSelector}
255
+          style={{
256
+            width: '100%',
257
+          }}
258
+        />
259
+      </Form.Item>
260
+
261
+      <Form.Item
262
+        name="website"
263
+        label="Website"
264
+        rules={[
265
+          {
266
+            required: true,
267
+            message: 'Please input website!',
268
+          },
269
+        ]}
270
+      >
271
+        <AutoComplete options={websiteOptions} onChange={onWebsiteChange} placeholder="website">
272
+          <Input />
273
+        </AutoComplete>
274
+      </Form.Item>
275
+
276
+      <Form.Item
277
+        name="intro"
278
+        label="Intro"
279
+        rules={[
280
+          {
281
+            required: true,
282
+            message: 'Please input Intro',
283
+          },
284
+        ]}
285
+      >
286
+        <Input.TextArea showCount maxLength={100} />
287
+      </Form.Item>
288
+
289
+      <Form.Item
290
+        name="gender"
291
+        label="Gender"
292
+        rules={[
293
+          {
294
+            required: true,
295
+            message: 'Please select gender!',
296
+          },
297
+        ]}
298
+      >
299
+        <Select placeholder="select your gender">
300
+          <Option value="male">Male</Option>
301
+          <Option value="female">Female</Option>
302
+          <Option value="other">Other</Option>
303
+        </Select>
304
+      </Form.Item>
305
+
306
+      <Form.Item label="Captcha" extra="We must make sure that your are a human.">
307
+        <Row gutter={8}>
308
+          <Col span={12}>
309
+            <Form.Item
310
+              name="captcha"
311
+              noStyle
312
+              rules={[
313
+                {
314
+                  required: true,
315
+                  message: 'Please input the captcha you got!',
316
+                },
317
+              ]}
318
+            >
319
+              <Input />
320
+            </Form.Item>
321
+          </Col>
322
+          <Col span={12}>
323
+            <Button>Get captcha</Button>
324
+          </Col>
325
+        </Row>
326
+      </Form.Item>
327
+
328
+      <Form.Item
329
+        name="agreement"
330
+        valuePropName="checked"
331
+        rules={[
332
+          {
333
+            validator: (_, value) =>
334
+              value ? Promise.resolve() : Promise.reject(new Error('Should accept agreement')),
335
+          },
336
+        ]}
337
+        {...tailFormItemLayout}
338
+      >
339
+        <Checkbox>
340
+          I have read the <a href="">agreement</a>
341
+        </Checkbox>
342
+      </Form.Item>
343
+      <Form.Item {...tailFormItemLayout}>
344
+        <Button type="primary" htmlType="submit">
345
+          Register
346
+        </Button>
347
+      </Form.Item>
348
+    </Form>
349
+  );
350
+};
351
+
352
+export default () => <Card><BasicForm /></Card>;

+ 191
- 0
src/pages/sample/home/components/AreaChart.jsx Прегледај датотеку

@@ -0,0 +1,191 @@
1
+import React from 'react';
2
+import { Card } from 'antd';
3
+import * as echarts from 'echarts/core';
4
+import Chart from '@/components/chart';
5
+
6
+export default (props) => {
7
+  const option = {
8
+    color: ['#80FFA5', '#00DDFF', '#37A2FF', '#FF0087', '#FFBF00'],
9
+    // title: {
10
+    //   text: 'Gradient Stacked Area Chart'
11
+    // },
12
+    tooltip: {
13
+      trigger: 'axis',
14
+      axisPointer: {
15
+        type: 'cross',
16
+        label: {
17
+          backgroundColor: '#6a7985'
18
+        }
19
+      }
20
+    },
21
+    legend: {
22
+      data: ['Line 1', 'Line 2', 'Line 3', 'Line 4', 'Line 5']
23
+    },
24
+    grid: {
25
+      left: '3%',
26
+      right: '4%',
27
+      bottom: '3%',
28
+      containLabel: true
29
+    },
30
+    xAxis: [
31
+      {
32
+        type: 'category',
33
+        boundaryGap: false,
34
+        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
35
+      }
36
+    ],
37
+    yAxis: [
38
+      {
39
+        type: 'value'
40
+      }
41
+    ],
42
+    series: [
43
+      {
44
+        name: 'Line 1',
45
+        type: 'line',
46
+        stack: 'Total',
47
+        smooth: true,
48
+        lineStyle: {
49
+          width: 0
50
+        },
51
+        showSymbol: false,
52
+        areaStyle: {
53
+          opacity: 0.8,
54
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
55
+            {
56
+              offset: 0,
57
+              color: 'rgb(128, 255, 165)'
58
+            },
59
+            {
60
+              offset: 1,
61
+              color: 'rgb(1, 191, 236)'
62
+            }
63
+          ])
64
+        },
65
+        emphasis: {
66
+          focus: 'series'
67
+        },
68
+        data: [140, 232, 101, 264, 90, 340, 250]
69
+      },
70
+      {
71
+        name: 'Line 2',
72
+        type: 'line',
73
+        stack: 'Total',
74
+        smooth: true,
75
+        lineStyle: {
76
+          width: 0
77
+        },
78
+        showSymbol: false,
79
+        areaStyle: {
80
+          opacity: 0.8,
81
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
82
+            {
83
+              offset: 0,
84
+              color: 'rgb(0, 221, 255)'
85
+            },
86
+            {
87
+              offset: 1,
88
+              color: 'rgb(77, 119, 255)'
89
+            }
90
+          ])
91
+        },
92
+        emphasis: {
93
+          focus: 'series'
94
+        },
95
+        data: [120, 282, 111, 234, 220, 340, 310]
96
+      },
97
+      {
98
+        name: 'Line 3',
99
+        type: 'line',
100
+        stack: 'Total',
101
+        smooth: true,
102
+        lineStyle: {
103
+          width: 0
104
+        },
105
+        showSymbol: false,
106
+        areaStyle: {
107
+          opacity: 0.8,
108
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
109
+            {
110
+              offset: 0,
111
+              color: 'rgb(55, 162, 255)'
112
+            },
113
+            {
114
+              offset: 1,
115
+              color: 'rgb(116, 21, 219)'
116
+            }
117
+          ])
118
+        },
119
+        emphasis: {
120
+          focus: 'series'
121
+        },
122
+        data: [320, 132, 201, 334, 190, 130, 220]
123
+      },
124
+      {
125
+        name: 'Line 4',
126
+        type: 'line',
127
+        stack: 'Total',
128
+        smooth: true,
129
+        lineStyle: {
130
+          width: 0
131
+        },
132
+        showSymbol: false,
133
+        areaStyle: {
134
+          opacity: 0.8,
135
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
136
+            {
137
+              offset: 0,
138
+              color: 'rgb(255, 0, 135)'
139
+            },
140
+            {
141
+              offset: 1,
142
+              color: 'rgb(135, 0, 157)'
143
+            }
144
+          ])
145
+        },
146
+        emphasis: {
147
+          focus: 'series'
148
+        },
149
+        data: [220, 402, 231, 134, 190, 230, 120]
150
+      },
151
+      {
152
+        name: 'Line 5',
153
+        type: 'line',
154
+        stack: 'Total',
155
+        smooth: true,
156
+        lineStyle: {
157
+          width: 0
158
+        },
159
+        showSymbol: false,
160
+        label: {
161
+          show: true,
162
+          position: 'top'
163
+        },
164
+        areaStyle: {
165
+          opacity: 0.8,
166
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
167
+            {
168
+              offset: 0,
169
+              color: 'rgb(255, 191, 0)'
170
+            },
171
+            {
172
+              offset: 1,
173
+              color: 'rgb(224, 62, 76)'
174
+            }
175
+          ])
176
+        },
177
+        emphasis: {
178
+          focus: 'series'
179
+        },
180
+        data: [220, 302, 181, 234, 210, 290, 150]
181
+      }
182
+    ]
183
+  };
184
+  
185
+
186
+  return (
187
+    <Card title='Gradient Stacked Area Chart'>
188
+      <Chart option={option} style={props.style}></Chart>
189
+    </Card>
190
+  )
191
+}

+ 44
- 0
src/pages/sample/home/components/Banner.jsx Прегледај датотеку

@@ -0,0 +1,44 @@
1
+import React from 'react';
2
+import { Row, Col, Card, Statistic } from 'antd';
3
+import { ArrowDownOutlined, ArrowUpOutlined, LikeOutlined } from '@ant-design/icons';
4
+
5
+export default (props) => {
6
+  return (
7
+    <Row gutter={24}>
8
+      <Col span={6}>
9
+        <Card>
10
+          <Statistic title="Feedback" value={1128} prefix={<LikeOutlined />} />
11
+        </Card>
12
+      </Col>
13
+      <Col span={6}>
14
+        <Card>
15
+          <Statistic title="Unmerged" value={93} suffix="/ 100" />
16
+        </Card>
17
+      </Col>
18
+      <Col span={6}>
19
+        <Card>
20
+          <Statistic
21
+            title="Active"
22
+            value={11.28}
23
+            precision={2}
24
+            valueStyle={{ color: '#3f8600' }}
25
+            prefix={<ArrowUpOutlined />}
26
+            suffix="%"
27
+          />
28
+        </Card>
29
+      </Col>
30
+      <Col span={6}>
31
+        <Card>
32
+          <Statistic
33
+            title="Idle"
34
+            value={9.3}
35
+            precision={2}
36
+            valueStyle={{ color: '#cf1322' }}
37
+            prefix={<ArrowDownOutlined />}
38
+            suffix="%"
39
+          />
40
+        </Card>
41
+      </Col>
42
+    </Row>
43
+  )
44
+}

+ 37
- 0
src/pages/sample/home/components/BarChart.jsx Прегледај датотеку

@@ -0,0 +1,37 @@
1
+import React from 'react';
2
+import { Card } from 'antd';
3
+import Chart from '@/components/chart';
4
+
5
+export default (props) => {
6
+
7
+  const option = {
8
+    legend: {},
9
+    tooltip: {},
10
+    dataset: {
11
+      source: [
12
+        ['product', '2015', '2016', '2017'],
13
+        ['Matcha Latte', 43.3, 85.8, 93.7],
14
+        ['Milk Tea', 83.1, 73.4, 55.1],
15
+        ['Cheese Cocoa', 86.4, 65.2, 82.5],
16
+        ['Walnut Brownie', 72.4, 53.9, 39.1]
17
+      ]
18
+    },
19
+    xAxis: { type: 'category' },
20
+    yAxis: {},
21
+    grid: {
22
+      left: '3%',
23
+      right: '4%',
24
+      bottom: '3%',
25
+      containLabel: true
26
+    },
27
+    // Declare several bar series, each will be mapped
28
+    // to a column of dataset.source by default.
29
+    series: [{ type: 'bar' }, { type: 'bar' }, { type: 'bar' }]
30
+  };
31
+
32
+  return (
33
+    <Card title='Bar Chart'>
34
+      <Chart option={option} style={props.style}></Chart>
35
+    </Card>
36
+  )
37
+}

+ 112
- 0
src/pages/sample/home/components/PictorialBar.jsx Прегледај датотеку

@@ -0,0 +1,112 @@
1
+import React from 'react';
2
+import { Card } from 'antd';
3
+import * as echarts from 'echarts/core';
4
+import Chart from '@/components/chart';
5
+
6
+
7
+export default (props) => {
8
+  let category = [];
9
+  let dottedBase = +new Date();
10
+  let lineData = [];
11
+  let barData = [];
12
+  for (let i = 0; i < 20; i++) {
13
+    let date = new Date((dottedBase += 3600 * 24 * 1000));
14
+    category.push(
15
+      [date.getFullYear(), date.getMonth() + 1, date.getDate()].join('-')
16
+    );
17
+    let b = Math.random() * 200;
18
+    let d = Math.random() * 200;
19
+    barData.push(b);
20
+    lineData.push(d + b);
21
+  }
22
+
23
+  const option = {
24
+    backgroundColor: '#0f375f',
25
+    tooltip: {
26
+      trigger: 'axis',
27
+      axisPointer: {
28
+        type: 'shadow'
29
+      }
30
+    },
31
+    legend: {
32
+      data: ['line', 'bar'],
33
+      textStyle: {
34
+        color: '#ccc'
35
+      }
36
+    },
37
+    xAxis: {
38
+      data: category,
39
+      axisLine: {
40
+        lineStyle: {
41
+          color: '#ccc'
42
+        }
43
+      }
44
+    },
45
+    yAxis: {
46
+      splitLine: { show: false },
47
+      axisLine: {
48
+        lineStyle: {
49
+          color: '#ccc'
50
+        }
51
+      }
52
+    },
53
+    series: [
54
+      {
55
+        name: 'line',
56
+        type: 'line',
57
+        smooth: true,
58
+        showAllSymbol: true,
59
+        symbol: 'emptyCircle',
60
+        symbolSize: 15,
61
+        data: lineData
62
+      },
63
+      {
64
+        name: 'bar',
65
+        type: 'bar',
66
+        barWidth: 10,
67
+        itemStyle: {
68
+          borderRadius: 5,
69
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
70
+            { offset: 0, color: '#14c8d4' },
71
+            { offset: 1, color: '#43eec6' }
72
+          ])
73
+        },
74
+        data: barData
75
+      },
76
+      {
77
+        name: 'line',
78
+        type: 'bar',
79
+        barGap: '-100%',
80
+        barWidth: 10,
81
+        itemStyle: {
82
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
83
+            { offset: 0, color: 'rgba(20,200,212,0.5)' },
84
+            { offset: 0.2, color: 'rgba(20,200,212,0.2)' },
85
+            { offset: 1, color: 'rgba(20,200,212,0)' }
86
+          ])
87
+        },
88
+        z: -12,
89
+        data: lineData
90
+      },
91
+      {
92
+        name: 'dotted',
93
+        type: 'pictorialBar',
94
+        symbol: 'rect',
95
+        itemStyle: {
96
+          color: '#0f375f'
97
+        },
98
+        symbolRepeat: true,
99
+        symbolSize: [12, 4],
100
+        symbolMargin: 1,
101
+        z: -10,
102
+        data: lineData
103
+      }
104
+    ]
105
+  };
106
+
107
+  return (
108
+    <Card title='Pictorial Chart'>
109
+      <Chart option={option} style={props.style}></Chart>
110
+    </Card>
111
+  )
112
+}

+ 48
- 0
src/pages/sample/home/components/PieChart.jsx Прегледај датотеку

@@ -0,0 +1,48 @@
1
+import React from 'react';
2
+import { Card } from 'antd';
3
+import Chart from '@/components/chart';
4
+
5
+export default (props) => {
6
+
7
+  const option = {
8
+    // title: {
9
+    //   text: 'Referer of a Website',
10
+    //   subtext: 'Fake Data',
11
+    //   left: 'center'
12
+    // },
13
+    tooltip: {
14
+      trigger: 'item'
15
+    },
16
+    legend: {
17
+      orient: 'vertical',
18
+      left: 'left'
19
+    },
20
+    series: [
21
+      {
22
+        name: 'Access From',
23
+        type: 'pie',
24
+        radius: '50%',
25
+        data: [
26
+          { value: 1048, name: 'Search Engine' },
27
+          { value: 735, name: 'Direct' },
28
+          { value: 580, name: 'Email' },
29
+          { value: 484, name: 'Union Ads' },
30
+          { value: 300, name: 'Video Ads' }
31
+        ],
32
+        emphasis: {
33
+          itemStyle: {
34
+            shadowBlur: 10,
35
+            shadowOffsetX: 0,
36
+            shadowColor: 'rgba(0, 0, 0, 0.5)'
37
+          }
38
+        }
39
+      }
40
+    ]
41
+  };
42
+
43
+  return (
44
+    <Card title='Basic Pie Chart'>
45
+      <Chart option={option} style={props.style}></Chart>
46
+    </Card>
47
+  )
48
+}

+ 48
- 0
src/pages/sample/home/components/RadarChart.jsx Прегледај датотеку

@@ -0,0 +1,48 @@
1
+import React from 'react';
2
+import { Card } from 'antd';
3
+import Chart from '@/components/chart';
4
+
5
+export default (props) => {
6
+
7
+  const option = {
8
+    // title: {
9
+    //   text: 'Basic Radar Chart'
10
+    // },
11
+    legend: {
12
+      data: ['Allocated Budget', 'Actual Spending']
13
+    },
14
+    radar: {
15
+      // shape: 'circle',
16
+      indicator: [
17
+        { name: 'Sales', max: 6500 },
18
+        { name: 'Administration', max: 16000 },
19
+        { name: 'Information Technology', max: 30000 },
20
+        { name: 'Customer Support', max: 38000 },
21
+        { name: 'Development', max: 52000 },
22
+        { name: 'Marketing', max: 25000 }
23
+      ]
24
+    },
25
+    series: [
26
+      {
27
+        name: 'Budget vs spending',
28
+        type: 'radar',
29
+        data: [
30
+          {
31
+            value: [4200, 3000, 20000, 35000, 50000, 18000],
32
+            name: 'Allocated Budget'
33
+          },
34
+          {
35
+            value: [5000, 14000, 28000, 26000, 42000, 21000],
36
+            name: 'Actual Spending'
37
+          }
38
+        ]
39
+      }
40
+    ]
41
+  };
42
+
43
+  return (
44
+    <Card title='Basic Radar Chart'>
45
+      <Chart option={option} style={props.style}></Chart>
46
+    </Card>
47
+  )
48
+}

+ 40
- 0
src/pages/sample/home/index.jsx Прегледај датотеку

@@ -0,0 +1,40 @@
1
+import React from 'react';
2
+import { Row, Col, Card, Space, Statistic } from 'antd';
3
+import Banner from './components/Banner';
4
+import AreaChart from './components/AreaChart';
5
+import BarChart from './components/BarChart';
6
+import PictorialBar from './components/PictorialBar';
7
+import RadarChart from './components/RadarChart';
8
+import PieChart from './components/PieChart';
9
+
10
+export default (props) => {
11
+
12
+  const chartStyle = {
13
+    height: '360px',
14
+    width: '100%',
15
+  }
16
+
17
+  return (
18
+    <div>
19
+      <Banner />
20
+
21
+      <Row gutter={24} style={{ marginTop: '24px' }}>
22
+        <Col span={16}>
23
+          <AreaChart style={chartStyle}/>
24
+        </Col>
25
+        <Col span={8}>
26
+          <RadarChart style={chartStyle}/>
27
+        </Col>
28
+      </Row>
29
+
30
+      <Row gutter={24} style={{ marginTop: '24px' }}>
31
+        <Col span={8}>
32
+          <PieChart style={chartStyle}/>
33
+        </Col>
34
+        <Col span={16}>
35
+          <BarChart style={chartStyle}/>
36
+        </Col>
37
+      </Row>
38
+    </div>
39
+  )
40
+}

+ 170
- 0
src/pages/sample/table/index.jsx Прегледај датотеку

@@ -0,0 +1,170 @@
1
+import { EllipsisOutlined, PlusOutlined } from '@ant-design/icons';
2
+import { ProTable, TableDropdown } from '@ant-design/pro-components';
3
+import { Button, Dropdown, Menu, Space, Tag } from 'antd';
4
+import { useRef, useEffect } from 'react';
5
+import { useModel } from '@/store';
6
+
7
+// import request from 'umi-request';
8
+const columns = [
9
+    {
10
+        dataIndex: 'index',
11
+        valueType: 'indexBorder',
12
+        width: 48,
13
+    },
14
+    {
15
+        title: '标题',
16
+        dataIndex: 'title',
17
+        copyable: true,
18
+        ellipsis: true,
19
+        tip: '标题过长会自动收缩',
20
+        formItemProps: {
21
+            rules: [
22
+                {
23
+                    required: true,
24
+                    message: '此项为必填项',
25
+                },
26
+            ],
27
+        },
28
+    },
29
+    {
30
+        disable: true,
31
+        title: '状态',
32
+        dataIndex: 'state',
33
+        filters: true,
34
+        onFilter: true,
35
+        ellipsis: true,
36
+        valueType: 'select',
37
+        valueEnum: {
38
+            all: { text: '超长'.repeat(50) },
39
+            open: {
40
+                text: '未解决',
41
+                status: 'Error',
42
+            },
43
+            closed: {
44
+                text: '已解决',
45
+                status: 'Success',
46
+                disabled: true,
47
+            },
48
+            processing: {
49
+                text: '解决中',
50
+                status: 'Processing',
51
+            },
52
+        },
53
+    },
54
+    {
55
+        disable: true,
56
+        title: '标签',
57
+        dataIndex: 'labels',
58
+        search: false,
59
+        renderFormItem: (_, { defaultRender }) => {
60
+            return defaultRender(_);
61
+        },
62
+        render: (_, record) => (<Space>
63
+        {record.labels.map(({ name, color }) => (<Tag color={color} key={name}>
64
+            {name}
65
+          </Tag>))}
66
+      </Space>),
67
+    },
68
+    {
69
+        title: '创建时间',
70
+        key: 'showTime',
71
+        dataIndex: 'created_at',
72
+        valueType: 'date',
73
+        sorter: true,
74
+        hideInSearch: true,
75
+    },
76
+    {
77
+        title: '创建时间',
78
+        dataIndex: 'created_at',
79
+        valueType: 'dateRange',
80
+        hideInTable: true,
81
+        search: {
82
+            transform: (value) => {
83
+                return {
84
+                    startTime: value[0],
85
+                    endTime: value[1],
86
+                };
87
+            },
88
+        },
89
+    },
90
+    {
91
+        title: '操作',
92
+        valueType: 'option',
93
+        key: 'option',
94
+        render: (text, record, _, action) => [
95
+            <a key="editable" onClick={() => {
96
+                    var _a;
97
+                    (_a = action === null || action === void 0 ? void 0 : action.startEditable) === null || _a === void 0 ? void 0 : _a.call(action, record.id);
98
+                }}>
99
+        编辑
100
+      </a>,
101
+            <a href={record.url} target="_blank" rel="noopener noreferrer" key="view">
102
+        查看
103
+      </a>,
104
+            <TableDropdown key="actionGroup" onSelect={() => action === null || action === void 0 ? void 0 : action.reload()} menus={[
105
+                    { key: 'copy', name: '复制' },
106
+                    { key: 'delete', name: '删除' },
107
+                ]}/>,
108
+        ],
109
+    },
110
+];
111
+const menu = (<Menu items={[
112
+        {
113
+            label: '1st item',
114
+            key: '1',
115
+        },
116
+        {
117
+            label: '2nd item',
118
+            key: '1',
119
+        },
120
+        {
121
+            label: '3rd item',
122
+            key: '1',
123
+        },
124
+    ]}/>);
125
+export default () => {
126
+
127
+    const actionRef = useRef();
128
+    return (
129
+        <ProTable columns={columns} actionRef={actionRef} cardBordered request={async (params = {}, sort, filter) => {
130
+            console.log(sort, filter);
131
+            // return request('https://proapi.azurewebsites.net/github/issues', {
132
+            //     params,
133
+            // });
134
+        }} editable={{
135
+            type: 'multiple',
136
+        }} columnsState={{
137
+            persistenceKey: 'pro-table-singe-demos',
138
+            persistenceType: 'localStorage',
139
+            onChange(value) {
140
+                console.log('value: ', value);
141
+            },
142
+        }} rowKey="id" search={{
143
+            labelWidth: 'auto',
144
+        }} options={{
145
+            setting: {
146
+                listsHeight: 400,
147
+            },
148
+        }} form={{
149
+            // 由于配置了 transform,提交的参与与定义的不同这里需要转化一下
150
+            syncToUrl: (values, type) => {
151
+                if (type === 'get') {
152
+                    return Object.assign(Object.assign({}, values), { created_at: [values.startTime, values.endTime] });
153
+                }
154
+                return values;
155
+            },
156
+        }} pagination={{
157
+            pageSize: 5,
158
+            onChange: (page) => console.log(page),
159
+        }} dateFormatter="string" headerTitle="高级表格" toolBarRender={() => [
160
+            <Button key="button" icon={<PlusOutlined />} type="primary">
161
+          新建
162
+        </Button>,
163
+            <Dropdown key="menu" overlay={menu}>
164
+          <Button>
165
+            <EllipsisOutlined />
166
+          </Button>
167
+        </Dropdown>,
168
+        ]}/>
169
+    );
170
+};

+ 18
- 0
src/routes/Router.jsx Прегледај датотеку

@@ -0,0 +1,18 @@
1
+import React from "react";
2
+import { createHashRouter, RouterProvider } from "react-router-dom";
3
+import { useModel } from "@/store";
4
+import { defaultRoutes } from './routes';
5
+
6
+export default (props) => {
7
+  const { routes } = useModel('user');
8
+
9
+  const router = React.useMemo(() => {
10
+    if (!routes || routes.length < 1) {
11
+      return createHashRouter(defaultRoutes);
12
+    } else {
13
+      return createHashRouter(routes);
14
+    }
15
+  }, [routes]);
16
+  
17
+  return <RouterProvider router={router} />
18
+}

+ 48
- 0
src/routes/menus.jsx Прегледај датотеку

@@ -0,0 +1,48 @@
1
+import { Link } from 'react-router-dom';
2
+
3
+// 菜单是否显示
4
+// 没有 meta 或者 meta.title 为空, 或者 meta.hideInMenu = true 的 都不显示
5
+const isShow = item => item.meta && item.meta.title && !item.meta.hideInMenu;
6
+
7
+const hasChildren = (list) => {
8
+  if (!list || list.length < 1) return false;
9
+
10
+  // 如果子元素全部都是不显示的, 说明子菜单不需要显示
11
+  return list.filter(it => !isShow(it)).length !== list.length;
12
+}
13
+
14
+const getPath = (parent, current = '') => {
15
+  if (current.indexOf('/') === 0 || current.indexOf('http') === 0) return current;
16
+  return `${parent}/${current}`.replace(/\/\//g, '/');
17
+}
18
+
19
+export const getMenuItems = (routes = [], fullPath = '/') => {
20
+  return routes.map(route => {
21
+    const path = getPath(fullPath, route.path);
22
+
23
+    //
24
+    if (!isShow(route)) return false;
25
+    
26
+    const children = hasChildren(route.children) ? getMenuItems(route.children, path) : false;
27
+
28
+    const { target, title, icon } = route.meta || {}
29
+
30
+    // 坑爹 react-router v6 不支持 hash 路由的 target 跳转
31
+    const label = target === '_blank' ?
32
+      <a href={`${window.location.pathname}#${path}`} target={target}>{title}</a>
33
+      : (
34
+        path.indexOf('http') === 0  ? <a href={path} target="_blank">{title}</a>
35
+        : <Link to={path} target={target}>{title}</Link>
36
+      );
37
+
38
+    return Object.assign(
39
+      {
40
+        key: path,
41
+        label,
42
+        title,
43
+        icon,
44
+      },
45
+      children && { children },
46
+    )
47
+  }).filter(Boolean);
48
+}

+ 14
- 0
src/routes/permissions.js Прегледај датотеку

@@ -0,0 +1,14 @@
1
+
2
+export const getAuthedRoutes = (routes, permissions) => {
3
+  if (!routes || routes.length < 1) return [];
4
+
5
+  return routes.map(route => {
6
+    if (route.meta && route.meta.permission && permissions.indexOf(route.meta.permission) < 0) return false;
7
+
8
+    if (route.children) {
9
+      route.children = getAuthedRoutes(route.children, permissions);
10
+    }
11
+
12
+    return route;
13
+  }).filter(Boolean);
14
+}

+ 112
- 0
src/routes/routes.jsx Прегледај датотеку

@@ -0,0 +1,112 @@
1
+import {
2
+  AppstoreOutlined,
3
+  ContainerOutlined,
4
+  DesktopOutlined,
5
+  MailOutlined,
6
+  MenuFoldOutlined,
7
+  MenuUnfoldOutlined,
8
+  PieChartOutlined,
9
+} from '@ant-design/icons';
10
+import AuthLayout from "@/layouts/AuthLayout";
11
+import PageContainer from "@/layouts/PageContainer";
12
+import Login from '@/pages/login';
13
+import Page404 from '@/pages/404';
14
+import Home from "@/pages/sample/home";
15
+import BasicForm from '@/pages/sample/form';
16
+import BasicTable from '@/pages/sample/table';
17
+
18
+/**
19
+ * meta 用来扩展自定义数据数据
20
+ * {
21
+ *    title: 用于页面或者菜单的标题, 没有此字段, 菜单不会显示
22
+ *    hideInMenu: 布尔值, 如果为 false, 菜单不会显示
23
+ *    noLayout: 布尔值, 如果为 true, 将不会使用默认布局
24
+ *    noSiderBar: 布尔值, 如果为 true, 将没有左侧菜单栏
25
+ *    noFooter: 布尔值, 如果为 true, 将没有底部 footer
26
+ *    target: 字符串, 如果为 _blank, 将在新窗口打开
27
+ *    permission: 对应服务器端权限名称
28
+ * }
29
+ */
30
+
31
+export const authRoutes = [
32
+  {
33
+    path: "form",
34
+    element: <BasicForm />,
35
+    meta: {
36
+      title: '表单',
37
+      icon: <AppstoreOutlined />,
38
+      permission: 'form',
39
+    },
40
+  },
41
+  {
42
+    path: "table",
43
+    element: <BasicTable />,
44
+    meta: {
45
+      title: '表格',
46
+      icon: <ContainerOutlined />,
47
+      permission: 'table',
48
+    },
49
+  },
50
+];
51
+
52
+export const defaultRoutes = [
53
+  {
54
+    path: "/",
55
+    element: <AuthLayout />,
56
+    children: [
57
+      {
58
+        index: true,
59
+        element: <Home />,
60
+      },
61
+      {
62
+        path: "home",
63
+        element: <Home />,
64
+        meta: {
65
+          title: '首页',
66
+          icon: <DesktopOutlined />,
67
+        },
68
+      },
69
+      {
70
+        path: '*',
71
+        element: <Page404 />
72
+      }
73
+    ],
74
+  },
75
+  {
76
+    path: '/login',
77
+    element: <Login />,
78
+  },
79
+  {
80
+    path: '*',
81
+    element: <Page404 />
82
+  }
83
+]
84
+
85
+export function mergeAuthRoutes (r1, r2) {
86
+  const r = r1.slice();
87
+  const children = r1[0].children.slice();
88
+  r[0].children = children.concat(r2);
89
+  return r;
90
+}
91
+
92
+// 全部路由
93
+export const routes = mergeAuthRoutes(defaultRoutes, authRoutes);
94
+function getPath (parent = "/", current = "") {
95
+  if (current.indexOf("/") === 0 || current.indexOf("http") === 0)
96
+    return current;
97
+  return `${parent}/${current}`.replace(/\/\//g, "/");
98
+}
99
+
100
+// 路由数组, 一维数组
101
+export const routeArr = (() => {
102
+  const flatten = (routes, parentPath = "/") => {
103
+    return routes.reduce((acc, route) => {
104
+      const path = getPath(parentPath, route.path);
105
+      const children = route.children ? flatten(route.children, path) : [];
106
+
107
+      return acc.concat([{ ...route, path }].concat(children));
108
+    }, []);
109
+  };
110
+
111
+  return flatten(routes);
112
+})();

+ 14
- 0
src/store/index.js Прегледај датотеку

@@ -0,0 +1,14 @@
1
+import { createStore } from "@zjxpcyc/react-tiny-store";
2
+import useSystem from "./models/system";
3
+import useUser from "./models/user";
4
+
5
+const store = createStore({
6
+  system: useSystem,
7
+  user: useUser,
8
+});
9
+
10
+export default store
11
+export const useModel = store.useModel
12
+export const addModel = store.addModel
13
+export const removeModel = store.removeModel
14
+export const Provider = store.Provider

+ 20
- 0
src/store/models/system.js Прегледај датотеку

@@ -0,0 +1,20 @@
1
+import { useState, useCallback } from "react";
2
+
3
+export default function useSystem() {
4
+  // 主题
5
+  const [theme, updateTheme] = useState('light');
6
+
7
+  // 其他配置
8
+  const [app, setApp] = useState({
9
+    fullName: '云致科技信息管理系统',
10
+    shorName: '云致科技',
11
+    company: '云致科技'
12
+  });
13
+
14
+  return {
15
+    theme,
16
+    updateTheme,
17
+    app,
18
+    hashRoute: true,
19
+  }
20
+}

+ 37
- 0
src/store/models/user.js Прегледај датотеку

@@ -0,0 +1,37 @@
1
+import { useState, useRef } from "react";
2
+import { defaultRoutes, authRoutes, mergeAuthRoutes } from '@/routes/routes';
3
+import { getMenuItems } from "@/routes/menus";
4
+import { getAuthedRoutes } from "@/routes/permissions";
5
+
6
+const queryCurrentUser = () => Promise.resolve({ id: 1, name: '管理员', resourcesList: [{ code: 'form' }, { code: 'table' }] });
7
+
8
+export default function useUser() {
9
+  const [user, setUser] = useState();
10
+  const menusRef = useRef();
11
+  const routesRef = useRef();
12
+
13
+  const getCurrentUser = () => new Promise((resolve, reject) => {
14
+    queryCurrentUser().then(res => {
15
+      const permissions = (res.resourcesList || []).map(x => x.code);
16
+
17
+      // authRoutes 是所有待验证授权的路由
18
+      // authedRoutes 是已经被授权的路由
19
+      const authedRoutes = getAuthedRoutes(authRoutes, permissions);
20
+
21
+      menusRef.current = getMenuItems(authedRoutes);
22
+      routesRef.current = mergeAuthRoutes(defaultRoutes, authedRoutes);
23
+
24
+      setUser(res);
25
+      resolve(res);
26
+    }).catch(reject);
27
+  });
28
+
29
+  return {
30
+    user,
31
+    setUser,
32
+    getCurrentUser,
33
+    menus: menusRef.current || [],
34
+    routes: routesRef.current || [],
35
+  }
36
+
37
+}

+ 61
- 0
src/utils/array.js Прегледај датотеку

@@ -0,0 +1,61 @@
1
+
2
+/**
3
+ * 数组 转 Tree
4
+ * @param {*} arr 
5
+ * @param {*} parent 
6
+ * @param {*} key 
7
+ * @returns 
8
+ */
9
+export function arr2Tree(arr = [], parent = 'parentId', key = 'key') {
10
+  // 转换为字典
11
+  const dict = arr.reduce((acc, item) => {
12
+    return {
13
+      ...acc,
14
+      [item[key]]: {
15
+        ...item,
16
+        children: []
17
+      }
18
+    }
19
+  }, {});
20
+
21
+  // 挂载父子节点
22
+  const rootId = -1;
23
+  const tree = [];
24
+  for (let item of arr) {
25
+    const parentNodeId = item[parent];
26
+
27
+    if (parentNodeId === rootId) {
28
+      tree.push(dict[item[key]]);
29
+    } else {
30
+      const parentNode = dict[parentNodeId];
31
+      if (parentNode) {
32
+        parentNode.children.push(item);
33
+      }
34
+    }
35
+  }
36
+
37
+  return [tree, dict];
38
+}
39
+
40
+/**
41
+ * 深度展平数组
42
+ * @param {*} arr 
43
+ * @returns 
44
+ */
45
+export function flatten(arr = []) {
46
+  return arr.reduce((acc, it) => {
47
+    const list = Array.isArray(it) ? flatten(it) : it
48
+    return acc.concat(list);
49
+  }, []);
50
+}
51
+
52
+/**
53
+ * 深度数组去重
54
+ * @param {*} arr 
55
+ */
56
+export function uniq(arr = []) {
57
+  const list = flatten(arr);
58
+  return list.reduce((acc, it) => {
59
+    return acc.indexOf(it) > -1 ? acc : acc.concat(it)
60
+  }, []);
61
+}

+ 8
- 0
src/utils/css.js Прегледај датотеку

@@ -0,0 +1,8 @@
1
+
2
+export function setProperty(...args) {
3
+  document.documentElement.style.setProperty(...args);
4
+}
5
+
6
+export function getPropertyValue(...args) {
7
+  return getComputedStyle(document.documentElement).getPropertyValue(...args);
8
+}

+ 5
- 0
src/utils/float.js Прегледај датотеку

@@ -0,0 +1,5 @@
1
+const epsilonN = N => num => Math.round( num * N + Number.EPSILON ) / N;
2
+
3
+// 可以处理丢失精度的问题
4
+// 比如 epsilon2(0.29 * 100)
5
+export const epsilon2 = epsilonN(1e2);

+ 13
- 0
src/utils/hooks/useBool.js Прегледај датотеку

@@ -0,0 +1,13 @@
1
+import React from "react";
2
+
3
+export default function useLoading(initial = false) {
4
+  const [loading, setLoading] = React.useState(initial);
5
+  const loadingRef = React.useRef();
6
+  loadingRef.current = loading;
7
+
8
+  const setTrue = React.useCallback(() => setLoading(true), []);
9
+  const setFalse = React.useCallback(() => setLoading(false), []);
10
+  const toggle = React.useCallback(() => setLoading(!loadingRef.current), []);
11
+
12
+  return [loading, setTrue, setFalse, setLoading, toggle];
13
+}

+ 36
- 0
src/utils/hooks/usePrompt.jsx Прегледај датотеку

@@ -0,0 +1,36 @@
1
+import React from "react";
2
+import { UNSAFE_NavigationContext } from "react-router-dom";
3
+
4
+// 估计在 react-router v6 的后续某个版本 usePrompt 会回归
5
+export function usePrompt(message, when = true) {
6
+  let blocker = React.useCallback(
7
+    tx => {
8
+      if (window.confirm(message)) tx.retry();
9
+    },
10
+    [message]
11
+  );
12
+
13
+  useBlocker(blocker, when);
14
+}
15
+
16
+function useBlocker(blocker, when = true) {
17
+  let { navigator } = React.useContext(UNSAFE_NavigationContext);
18
+
19
+  React.useEffect(() => {
20
+    if (!when) return;
21
+
22
+    let unblock = navigator.block((tx) => {
23
+      let autoUnblockingTx = {
24
+        ...tx,
25
+        retry() {
26
+          unblock();
27
+          tx.retry();
28
+        }
29
+      };
30
+
31
+      blocker(autoUnblockingTx);
32
+    });
33
+
34
+    return unblock;
35
+  }, [navigator, blocker, when]);
36
+}

+ 9
- 0
src/utils/hooks/useRoute.jsx Прегледај датотеку

@@ -0,0 +1,9 @@
1
+import { useLocation } from "react-router-dom";
2
+import { routeArr } from '@/routes/routes';
3
+
4
+// 获取当前的 route 信息
5
+export default function useRoute() {
6
+  const location = useLocation();
7
+  const currentRoute = routeArr.filter(x => x.path === location.pathname)[0];
8
+  return currentRoute;
9
+}

+ 19
- 0
src/utils/observe.js Прегледај датотеку

@@ -0,0 +1,19 @@
1
+
2
+export default function observe() {
3
+  const listeners = [];
4
+
5
+  const subscribe = (fn) => {
6
+    listeners.push(fn);
7
+
8
+    return () => listeners.splice(listeners.indexOf(fn), 1)
9
+  }
10
+
11
+  const notify = (...args) => {
12
+    listeners.forEach(fn => fn(...args))
13
+  }
14
+
15
+  return {
16
+    subscribe,
17
+    notify,
18
+  }
19
+}

+ 172
- 0
src/utils/request.js Прегледај датотеку

@@ -0,0 +1,172 @@
1
+import axios from "axios";
2
+import React from 'react';
3
+import { message } from 'antd';
4
+
5
+const instance = axios.create({
6
+  baseURL: import.meta.env.VITE_SERVER_BASE,
7
+  timeout: 10000,
8
+  withCredentials: true, // 跨域
9
+});
10
+
11
+
12
+// 添加请求拦截器
13
+instance.interceptors.request.use(function (config) {
14
+  const { headers = {}, method = 'get', responseType = 'json', download = false, silent } = config;
15
+  const token = localStorage.getItem('token') || '';
16
+
17
+  let noTip = silent;
18
+  if (noTip === undefined) {
19
+    noTip = method.toLocaleLowerCase() === 'get' ? true : false;
20
+  }
21
+
22
+  // 在发送请求之前做些什么
23
+  return {
24
+    ...config,
25
+    silent: noTip,
26
+    headers: {
27
+      ...headers,
28
+      Authorization: token,
29
+    },
30
+    responseType: download ? 'blob' : responseType,
31
+  };
32
+}, function (error) {
33
+  // 对请求错误做些什么
34
+  return Promise.reject(error);
35
+});
36
+
37
+// 添加响应拦截器
38
+instance.interceptors.response.use(function (response) {
39
+  // 2xx 范围内的状态码都会触发该函数。
40
+  // 对响应数据做点什么
41
+
42
+  const { data, config } = response;
43
+
44
+  if (config.download && !data.code) {
45
+    return downloadBlob(response, '下载文件');
46
+  }
47
+
48
+  if (data.code === 1000) {
49
+    if (data.data.token) {
50
+      localStorage.setItem('token', data.data.token);
51
+    }
52
+
53
+    if (!config.silent) {
54
+      message.success('操作成功');
55
+    }
56
+
57
+    return data.data;
58
+  } else if (data.code === 1001) {
59
+    if (!config.silent) {
60
+      message.error('未登录或者超时, 请重新登录');
61
+    }
62
+  } else {
63
+    console.log(config)
64
+    if (!config.silent) {
65
+      const errMsg = data.message || '系统错误';
66
+      message.error(errMsg.indexOf('exception') > -1 ? '服务异常' : errMsg);
67
+    }
68
+  }
69
+
70
+  return Promise.reject(response);
71
+}, function (error) {
72
+  // 超出 2xx 范围的状态码都会触发该函数。
73
+  // 对响应错误做点什么
74
+
75
+  console.error(error);
76
+
77
+  return Promise.reject("网络异常, 请重试...");
78
+});
79
+
80
+export default instance;
81
+
82
+export function queryTable (apiRequest) {
83
+  return function (params) {
84
+    const { pageSize } = params;
85
+    return apiRequest({
86
+      ...params,
87
+      pageSize,
88
+      pageNum: params.current,
89
+    })
90
+      .then((res) => {
91
+        return {
92
+          data: res.records,
93
+          success: true,
94
+          total: res.total,
95
+        };
96
+      })
97
+      .catch((err) => {
98
+        return {
99
+          success: false,
100
+        };
101
+      });
102
+  };
103
+}
104
+
105
+export function queryDict (apiRequest) {
106
+  return function (params, labelKey = 'name', valueKey = 'id') {
107
+    const { current, pageSize, ...leftParams } = params || {};
108
+    return apiRequest({
109
+      pageSize: 9999,
110
+      pageNum: 1,
111
+      ...(leftParams || {}),
112
+    })
113
+      .then((res) => {
114
+        return res?.records?.map((x) => ({
115
+          label: x[labelKey],
116
+          value: x[valueKey],
117
+          ...x,
118
+        }));
119
+      })
120
+      .catch((err) => {
121
+        return {
122
+          success: false,
123
+        };
124
+      });
125
+  };
126
+}
127
+
128
+export function restful (url) {
129
+  const list = params => instance.get(url, { params });
130
+  const get = id => instance.get(`${url}/${id}`);
131
+  const add = data => instance.post(url, data);
132
+  const update = (id, data) => instance.put(`${url}/${id}`, data);
133
+  const del = id => instance.delete(`${url}/${id}`);
134
+
135
+  return [list, get, add, update, del];
136
+}
137
+
138
+export function useRequest (fn) {
139
+  const [loading, setLoading] = React.useState(false);
140
+
141
+  const p = (...args) => new Promise((resolve, reject) => {
142
+    setLoading(true);
143
+    fn(...args).then(res => {
144
+      setLoading(false);
145
+      resolve(res);
146
+    }).catch(e => {
147
+      setLoading(false);
148
+      reject(e);
149
+    });
150
+  });
151
+
152
+  return [loading, p]
153
+}
154
+
155
+function downloadBlob (response) {
156
+  let fileName = '未知文件';
157
+  const contentType = response.headers['content-type'];
158
+  const contentDisposition = response.headers['content-disposition'];
159
+  if (contentDisposition) {
160
+    const parts = contentDisposition.split(';filename=');
161
+    if (parts[1]) {
162
+      fileName = decodeURIComponent(parts[1]);
163
+    }
164
+  }
165
+
166
+  const url = window.URL.createObjectURL(new Blob([response.data], { type: contentType }));
167
+  const link = document.createElement('a');
168
+  link.href = url;
169
+  link.setAttribute('download', fileName);
170
+  link.click();
171
+  window.URL.revokeObjectURL(url);
172
+}

+ 35
- 0
vite.config.js Прегледај датотеку

@@ -0,0 +1,35 @@
1
+import { defineConfig } from 'vite'
2
+import path from "path"
3
+import react from '@vitejs/plugin-react'
4
+import vitePluginImp from 'vite-plugin-imp'
5
+
6
+// https://vitejs.dev/config/
7
+export default defineConfig({
8
+  server: {
9
+    port: 3000
10
+  },
11
+  plugins: [
12
+    react(),
13
+    vitePluginImp({
14
+      libList: [
15
+        {
16
+          libName: 'antd',
17
+          style: (name) => `antd/es/${name}/style`,
18
+        }
19
+      ]
20
+    })
21
+  ],
22
+  resolve: {
23
+    alias: [
24
+      { find: '@', replacement: path.resolve(__dirname, 'src') },
25
+    ],
26
+  },
27
+  css: {
28
+    preprocessorOptions: {
29
+      less: {
30
+        // modifyVars: { 'primary-color': '#13c2c2' },
31
+        javascriptEnabled: true,
32
+      },
33
+    },
34
+  },
35
+})