张涛 il y a 1 an
révision
673174d651
100 fichiers modifiés avec 11877 ajouts et 0 suppressions
  1. 2
    0
      .env
  2. 2
    0
      .env.production
  3. 24
    0
      .gitignore
  4. 19
    0
      index.html
  5. 8
    0
      jsconfig.json
  6. 7182
    0
      package-lock.json
  7. 41
    0
      package.json
  8. 7
    0
      public/config.js
  9. BIN
      public/logo.png
  10. 110
    0
      public/particles/particles.json
  11. 9
    0
      public/particles/particles.min.js
  12. 1
    0
      public/qrcode.min.js
  13. 1
    0
      public/vite.svg
  14. 42
    0
      src/App.css
  15. 10
    0
      src/App.jsx
  16. 1
    0
      src/assets/react.svg
  17. 15
    0
      src/components/EditableTag/Tag.jsx
  18. 21
    0
      src/components/EditableTag/index.jsx
  19. 27
    0
      src/components/EditableTag/style.less
  20. 4
    0
      src/components/Money/float.js
  21. 30
    0
      src/components/Money/index.jsx
  22. 83
    0
      src/components/Page/Curd.jsx
  23. 93
    0
      src/components/Page/Edit.jsx
  24. 90
    0
      src/components/Page/List.jsx
  25. 23
    0
      src/components/Page/index.jsx
  26. 10
    0
      src/components/Upload/Upload.jsx
  27. 60
    0
      src/components/Upload/UploadImage.jsx
  28. 115
    0
      src/components/Upload/UploadImageList.jsx
  29. 94
    0
      src/components/Upload/UploadVideo.jsx
  30. 11
    0
      src/components/Upload/index.jsx
  31. 32
    0
      src/components/Upload/request.js
  32. 7
    0
      src/components/Upload/style.less
  33. 87
    0
      src/components/Wangeditor/index.jsx
  34. 81
    0
      src/components/chart/index.jsx
  35. 79
    0
      src/index.less
  36. 31
    0
      src/layouts/AuthLayout/Main.jsx
  37. 3
    0
      src/layouts/AuthLayout/RedirectLogin.jsx
  38. 31
    0
      src/layouts/AuthLayout/RequireLogin.jsx
  39. 72
    0
      src/layouts/AuthLayout/components/Background.jsx
  40. 31
    0
      src/layouts/AuthLayout/components/Container.jsx
  41. 15
    0
      src/layouts/AuthLayout/components/Footer.jsx
  42. 36
    0
      src/layouts/AuthLayout/components/Header/Exit.jsx
  43. 14
    0
      src/layouts/AuthLayout/components/Header/Language.jsx
  44. 24
    0
      src/layouts/AuthLayout/components/Header/Title.jsx
  45. 120
    0
      src/layouts/AuthLayout/components/Header/User.jsx
  46. 30
    0
      src/layouts/AuthLayout/components/Header/index.jsx
  47. 19
    0
      src/layouts/AuthLayout/components/HtmlTitle.jsx
  48. 17
    0
      src/layouts/AuthLayout/components/Logo.jsx
  49. 31
    0
      src/layouts/AuthLayout/components/Menus.jsx
  50. 21
    0
      src/layouts/AuthLayout/components/PageTransition/index.jsx
  51. 21
    0
      src/layouts/AuthLayout/components/PageTransition/style.less
  52. 21
    0
      src/layouts/AuthLayout/components/SiderBar.jsx
  53. 55
    0
      src/layouts/AuthLayout/index.jsx
  54. 81
    0
      src/layouts/AuthLayout/style.less
  55. 27
    0
      src/layouts/AuthLayout/withLogin.jsx
  56. 28
    0
      src/locale/config.js
  57. 4
    0
      src/locale/en.json
  58. 4
    0
      src/locale/zh.json
  59. 5
    0
      src/locale/zht.json
  60. 21
    0
      src/main.jsx
  61. 29
    0
      src/pages/404/index.jsx
  62. 24
    0
      src/pages/firstPage/components/Brand.jsx
  63. 66
    0
      src/pages/firstPage/components/Content.jsx
  64. 238
    0
      src/pages/firstPage/components/DetailedPlan.jsx
  65. 29
    0
      src/pages/firstPage/components/Header.jsx
  66. 17
    0
      src/pages/firstPage/components/HeaderCenter.jsx
  67. 15
    0
      src/pages/firstPage/components/InWebsite.jsx
  68. 71
    0
      src/pages/firstPage/components/InitialHeart.jsx
  69. 30
    0
      src/pages/firstPage/components/Introduce.jsx
  70. 11
    0
      src/pages/firstPage/components/SelectRegister.jsx
  71. 17
    0
      src/pages/firstPage/components/ToInitialHeart.jsx
  72. 16
    0
      src/pages/firstPage/components/ToIntroduce.jsx
  73. 18
    0
      src/pages/firstPage/components/ToSelectRegister.jsx
  74. 42
    0
      src/pages/firstPage/index.jsx
  75. 606
    0
      src/pages/firstPage/index.less
  76. 111
    0
      src/pages/fxuser/Edit.jsx
  77. 61
    0
      src/pages/fxuser/index.jsx
  78. 11
    0
      src/pages/login/Effect.jsx
  79. 187
    0
      src/pages/login/LoginForm.bak.jsx
  80. 67
    0
      src/pages/login/LoginForm.jsx
  81. 67
    0
      src/pages/login/LoginQrcode.bak.jsx
  82. 45
    0
      src/pages/login/LoginUse.jsx
  83. 24
    0
      src/pages/login/Particles.jsx
  84. 159
    0
      src/pages/login/components/LoginQrCode.jsx
  85. 28
    0
      src/pages/login/components/QrCode.jsx
  86. 29
    0
      src/pages/login/components/SuccessIcon.jsx
  87. 41
    0
      src/pages/login/components/qrcode.module.less
  88. 34
    0
      src/pages/login/foo.js
  89. 61
    0
      src/pages/login/index.jsx
  90. 77
    0
      src/pages/login/qrcode.less
  91. 99
    0
      src/pages/login/style.less
  92. 6
    0
      src/pages/menu/index.jsx
  93. 120
    0
      src/pages/tenant/Edit.jsx
  94. 109
    0
      src/pages/tenant/index.jsx
  95. 18
    0
      src/routes/Router.jsx
  96. 37
    0
      src/routes/hooks/usePrompt.jsx
  97. 17
    0
      src/routes/hooks/useRoute.jsx
  98. 44
    0
      src/routes/menus.jsx
  99. 13
    0
      src/routes/permissions.js
  100. 0
    0
      src/routes/routes.jsx

+ 2
- 0
.env Voir le fichier

@@ -0,0 +1,2 @@
1
+VITE_SERVER_BASE=/api
2
+# VITE_LOGIN_URL=http://localhost:3009/admin/

+ 2
- 0
.env.production Voir le fichier

@@ -0,0 +1,2 @@
1
+# VITE_SERVER_BASE=https://api.crm2.njyunzhi.com/api/platform
2
+# VITE_LOGIN_URL=https://crm2.njyunzhi.com/admin/index.html

+ 24
- 0
.gitignore Voir le fichier

@@ -0,0 +1,24 @@
1
+# Logs
2
+logs
3
+*.log
4
+npm-debug.log*
5
+yarn-debug.log*
6
+yarn-error.log*
7
+pnpm-debug.log*
8
+lerna-debug.log*
9
+
10
+node_modules
11
+dist
12
+dist-ssr
13
+*.local
14
+
15
+# Editor directories and files
16
+.vscode/*
17
+!.vscode/extensions.json
18
+.idea
19
+.DS_Store
20
+*.suo
21
+*.ntvs*
22
+*.njsproj
23
+*.sln
24
+*.sw?

+ 19
- 0
index.html Voir le fichier

@@ -0,0 +1,19 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+
4
+<head>
5
+  <meta charset="UTF-8" />
6
+  
7
+  <link rel="icon" type="image/svg+xml" href="https://yz-crm.oss-cn-nanjing.aliyuncs.com/images/favicon.ico" />
8
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9
+  <script src="./config.js"></script>
10
+  <script src="./qrcode.min.js"></script>
11
+  <title>商户管理</title>
12
+</head>
13
+
14
+<body>
15
+  <div id="root"></div>
16
+  <script type="module" src="/src/main.jsx"></script>
17
+</body>
18
+
19
+</html>

+ 8
- 0
jsconfig.json Voir le fichier

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

+ 7182
- 0
package-lock.json
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 41
- 0
package.json Voir le fichier

@@ -0,0 +1,41 @@
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": "^3.0.3",
17
+    "antd": "^4.23.4",
18
+    "axios": "^1.2.0",
19
+    "classnames": "^2.3.2",
20
+    "echarts": "^5.4.0",
21
+    "i18next": "^23.9.0",
22
+    "md5": "^2.3.0",
23
+    "moment": "^2.29.4",
24
+    "react": "18.2.0",
25
+    "react-dom": "18.2.0",
26
+    "react-helmet": "^6.1.0",
27
+    "react-i18next": "^14.0.5",
28
+    "react-intl": "^6.6.2",
29
+    "react-intl-universal": "^2.10.1",
30
+    "react-router-dom": "^6.4.2",
31
+    "react-transition-group": "^4.4.5"
32
+  },
33
+  "devDependencies": {
34
+    "@types/react": "18.0.27",
35
+    "@types/react-dom": "18.0.10",
36
+    "@vitejs/plugin-react": "^2.0.0",
37
+    "less": "^4.1.3",
38
+    "vite": "^3.0.0",
39
+    "vite-plugin-imp": "^2.2.0"
40
+  }
41
+}

+ 7
- 0
public/config.js Voir le fichier

@@ -0,0 +1,7 @@
1
+
2
+var SERVER_BASE = 'http://api.crm.njyunzhi.com';
3
+
4
+// 获取真实的上传文件地址
5
+function getRealPath(path) {
6
+  return path;
7
+}

BIN
public/logo.png Voir le fichier


+ 110
- 0
public/particles/particles.json Voir le fichier

@@ -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
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 1
- 0
public/qrcode.min.js
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 1
- 0
public/vite.svg Voir le fichier

@@ -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 Voir le fichier

@@ -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 Voir le fichier

@@ -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 Voir le fichier

@@ -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 Voir le fichier

@@ -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 Voir le fichier

@@ -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 Voir le fichier

@@ -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 Voir le fichier

@@ -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 Voir le fichier

@@ -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
+}

+ 83
- 0
src/components/Page/Curd.jsx Voir le fichier

@@ -0,0 +1,83 @@
1
+import React from 'react';
2
+import { DrawerForm } from '@ant-design/pro-components';
3
+import { Form } from 'antd';
4
+import List from './List';
5
+
6
+const drawerProps = {
7
+  destroyOnClose: true
8
+}
9
+export default (props) => {
10
+  const { rowKey, columns, request, formProps, renderFormItems, ...leftProps } = props;
11
+
12
+  const [form] = Form.useForm();
13
+  const listRef = React.useRef();
14
+  const rowRef = React.useRef();
15
+  const [open, setOpen] = React.useState(false);
16
+  
17
+  const onAdd = () => {
18
+    rowRef.current = undefined;
19
+    form.resetFields();
20
+    setOpen(true);
21
+  }
22
+
23
+  const onEdit = (row) => {
24
+    rowRef.current = row;
25
+    form.setFieldsValue(row);
26
+    setOpen(true);
27
+  }
28
+
29
+  const onDelete = (row) => {
30
+    rowRef.current = row;
31
+
32
+    listRef.current.showLoading('正在操作, 请稍候...');
33
+    request.del(row[rowKey]).then(() => {
34
+      listRef.current.hideLoading();
35
+      listRef.current.reload();
36
+    }).catch(() => {
37
+      listRef.current.hideLoading();
38
+    });
39
+  }
40
+
41
+  const onFinish = async (values) => {
42
+    if (rowRef.current) {
43
+      await request.update(rowRef.current[rowKey], {
44
+        ...rowRef.current,
45
+        ...values,
46
+      });
47
+    } else {
48
+      await request.save(values);
49
+    }
50
+    listRef.current.reload();
51
+
52
+    return true;
53
+  }
54
+
55
+  return (
56
+    <>
57
+      <List
58
+        ref={listRef}
59
+        rowKey={rowKey}
60
+        columns={columns}
61
+        request={request.list}
62
+        onAdd={onAdd}
63
+        onDelete={onDelete}
64
+        onEdit={onEdit}
65
+        {...leftProps}
66
+      />
67
+
68
+      <DrawerForm
69
+        submitTimeout={3000}
70
+        open={open}
71
+        form={form}
72
+        drawerProps={drawerProps}
73
+        onOpenChange={setOpen}
74
+        onFinish={onFinish}
75
+        {...formProps}
76
+      >
77
+        {
78
+          renderFormItems && renderFormItems()
79
+        }
80
+      </DrawerForm>
81
+    </>
82
+  )
83
+}

+ 93
- 0
src/components/Page/Edit.jsx Voir le fichier

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

+ 90
- 0
src/components/Page/List.jsx Voir le fichier

@@ -0,0 +1,90 @@
1
+import React from 'react';
2
+import { Button, Popconfirm, message } from 'antd';
3
+import { ProTable } from '@ant-design/pro-components';
4
+import { queryTable } from '@/utils/request';
5
+import Page from './index';
6
+
7
+export default React.forwardRef((props, ref) => {
8
+  /**
9
+   * request 与 ProTable 定义不同,这个只是普通的接口即可
10
+   */
11
+  const { request, rowKey, columns, onAdd, onDelete, onEdit, toolBarRender, columnOptionRender, ...leftProps } = props;
12
+  const actionRef = React.useRef();
13
+  const hideRef = React.useRef();
14
+
15
+  const api = React.useMemo(() => queryTable(request), [request]);
16
+// console.log(request)
17
+  // 统一实现操作列
18
+  const cols = React.useMemo(() => [
19
+    ...columns,
20
+    {
21
+      title: '操作',
22
+      valueType: 'option',
23
+      key: 'option',
24
+      ellipsis: true,
25
+      fixed: 'right',
26
+      render: (_, record) => [
27
+        (
28
+          onEdit &&
29
+          <Button style={{ padding: 0 }} type="link" key={1} onClick={() => onEdit(record)}>
30
+            编辑
31
+          </Button>
32
+        ),
33
+        (
34
+          onDelete &&
35
+          <Popconfirm
36
+            key={3}
37
+            title="您是否确认删除 ?"
38
+            onConfirm={() => onDelete(record)}
39
+            okText="确定"
40
+            cancelText="取消"
41
+          >
42
+            <Button style={{ padding: 0 }} type="link">
43
+              删除
44
+            </Button>
45
+          </Popconfirm>
46
+        ),
47
+        ...(columnOptionRender ? columnOptionRender(_, record) : []),
48
+      ].filter(Boolean),
49
+    },
50
+  ], [columns, onEdit, onDelete]);
51
+
52
+  React.useImperativeHandle(ref, () => {
53
+    const showLoading = msg => (hideRef.current = message.loading(msg || '操作中, 请稍候...'));
54
+    const hideLoading = () => hideRef.current && hideRef.current();
55
+    const reload = () => actionRef.current?.reload && actionRef.current.reload();
56
+
57
+    return {
58
+      showLoading,
59
+      hideLoading,
60
+      reload,
61
+      actionRef: actionRef,
62
+    }
63
+  }, []);
64
+
65
+  return (
66
+    <Page>
67
+      <ProTable
68
+        rowKey={rowKey}
69
+        columns={cols}
70
+        request={api}
71
+        cardBordered
72
+        actionRef={actionRef}
73
+        toolBarRender={() => [
74
+          (
75
+            onAdd &&
76
+            <Button
77
+              key="1"
78
+              type="primary"
79
+              onClick={onAdd}
80
+            >
81
+              新增
82
+            </Button>
83
+          ),
84
+          ...(toolBarRender ? toolBarRender() : []),
85
+        ].filter(Boolean)}
86
+        {...leftProps}
87
+      />
88
+    </Page>
89
+  )
90
+});

+ 23
- 0
src/components/Page/index.jsx Voir le fichier

@@ -0,0 +1,23 @@
1
+import React from 'react';
2
+import { Typography } from 'antd';
3
+import useRoute from '@/routes/hooks/useRoute';
4
+
5
+const pageStyle = {
6
+  margin: '0 24px 24px 24px',
7
+  // margin: '24px',
8
+  minHeight: 'calc(100% - 24px)',
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={3} style={{ paddingBottom: '12px', fontWeight: 400 }}>{ title }</Title> }
20
+      {props.children}
21
+    </div>
22
+  )
23
+}

+ 10
- 0
src/components/Upload/Upload.jsx Voir le fichier

@@ -0,0 +1,10 @@
1
+import { Upload } from 'antd';
2
+import { uploadFile } from './request'
3
+
4
+export default (props) => {
5
+  return (
6
+    <Upload {...props} customRequest={uploadFile} >
7
+      {props.children}
8
+    </Upload>
9
+  )
10
+}

+ 60
- 0
src/components/Upload/UploadImage.jsx Voir le fichier

@@ -0,0 +1,60 @@
1
+import React, { useState } from 'react';
2
+import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
3
+import Upload from './Upload';
4
+
5
+function beforeUpload(file) {
6
+  const isImage = file.type === 'image/jpeg' || file.type === 'image/png'||file.type==='image/gif';
7
+  if (!isImage) {
8
+    message.error('请上传 JPG , PNG或GIF 图片!');
9
+  }
10
+  const isLt10M = file.size / 1024 / 1024 < 10;
11
+  if (!isLt10M) {
12
+    message.error('图片大小必须小于 10MB!');
13
+  }
14
+
15
+  return isImage && isLt10M;
16
+}
17
+
18
+const UploadButton = (props) => (
19
+  <div>
20
+    {props.loading ? <LoadingOutlined /> : <PlusOutlined />}
21
+    <div style={{ marginTop: 8 }}>上传</div>
22
+  </div>
23
+);
24
+
25
+export default (props) => {
26
+  const { value, onChange } = props;
27
+
28
+  const [loading, setLoading] = useState(false);
29
+
30
+  const handleChange = info => {
31
+    if (info.file.status === 'uploading') {
32
+      setLoading(true);
33
+      return;
34
+    }
35
+    if (info.file.status === 'error') {
36
+      setLoading(false);
37
+      return;
38
+    }
39
+
40
+    if (info.file.status === 'done') {
41
+      setLoading(false);
42
+      const { url, fileType } = info.file.response;
43
+      onChange(url);
44
+    }
45
+  };
46
+
47
+  const previewUrl = getRealPath ? getRealPath(value) : value;
48
+
49
+  return (
50
+    <Upload
51
+      listType="picture-card"
52
+      className="image-uploader"
53
+      showUploadList={false}
54
+      beforeUpload={beforeUpload}
55
+      onChange={handleChange}
56
+    >
57
+      {value ? <img src={previewUrl} alt="avatar" style={{ width: '100%', height: '100%' }} /> : <UploadButton loading={loading} />}
58
+    </Upload>
59
+  );
60
+}

+ 115
- 0
src/components/Upload/UploadImageList.jsx Voir le fichier

@@ -0,0 +1,115 @@
1
+
2
+import React, { useEffect, useState } from 'react';
3
+import { Modal } from 'antd'
4
+import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
5
+import Upload from './Upload';
6
+
7
+function beforeUpload(file) {
8
+  const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'|| file.type === 'image/gif';
9
+  if (!isJpgOrPng) {
10
+    message.error('请上传 JPG,PNG 或 GIF 图片!');
11
+  }
12
+  const isLt10M = file.size / 1024 / 1024 < 10;
13
+  if (!isLt10M) {
14
+    message.error('图片大小必须小于 10MB!');
15
+  }
16
+
17
+  return isJpgOrPng && isLt10M;
18
+}
19
+
20
+const UploadButton = (props) => (
21
+  <div>
22
+    {props.loading ? <LoadingOutlined /> : <PlusOutlined />}
23
+    <div style={{ marginTop: 8 }}>上传</div>
24
+  </div>
25
+);
26
+
27
+export default (props) => {
28
+  const { value, onChange, input, output } = props;
29
+
30
+  const [loading, setLoading] = useState(false);
31
+  const [previewVisible, setPreviewVisible] = useState(false);
32
+  const [previewImage, setPreviewImage] = useState();
33
+  const [fileList, setFileList] = useState([]);
34
+
35
+  const handleChange = info => {
36
+    if (info.file.status === 'uploading') {
37
+      setLoading(true);
38
+      return;
39
+    }
40
+    if (info.file.status === 'error') {
41
+      setLoading(false);
42
+      return;
43
+    }
44
+
45
+    if (info.file.status === 'removed') {
46
+      toggleChange(info.fileList)
47
+    }
48
+  };
49
+
50
+  const handleSuccess = ({url}) => {
51
+    setLoading(false)
52
+    const inx = fileList?.length ? fileList.length + 1 : 1
53
+    const list = fileList.concat({
54
+      url,
55
+      uid: `new-${inx}`,
56
+      status: 'done'
57
+    })
58
+    toggleChange(list)
59
+  }
60
+
61
+  const toggleChange = (list) => {
62
+    if (output) {
63
+      onChange(list.map(x => output(x)))
64
+    } else {
65
+      onChange(list)
66
+    }
67
+  }
68
+
69
+  const handlePreview = (file) => {
70
+    setPreviewImage(file.url)
71
+    setPreviewVisible(true)
72
+  }
73
+
74
+  useEffect(() => {
75
+    if (input) {
76
+      const lst = (value || []).map((it) => {
77
+        const { uid, url } = input(it)
78
+        return {
79
+          uid,
80
+          url,
81
+          status: 'done',
82
+          raw: it,
83
+        }
84
+      })
85
+      setFileList(lst)
86
+    } else {
87
+      setFileList(value || [])
88
+    }
89
+  }, [value])
90
+
91
+  return (
92
+    <>
93
+      <Upload
94
+        listType="picture-card"
95
+        className="image-uploader"
96
+        fileList={fileList}
97
+        beforeUpload={beforeUpload}
98
+        onChange={handleChange}
99
+        onPreview={handlePreview}
100
+        onSuccess={handleSuccess}
101
+      >
102
+        <UploadButton loading={loading} />
103
+      </Upload>
104
+      <Modal
105
+        visible={previewVisible}
106
+        title="图片预览"
107
+        footer={null}
108
+        onCancel={() => setPreviewVisible(false)}
109
+      >
110
+        <img alt="example" style={{ width: '100%' }} src={previewImage} />
111
+      </Modal>
112
+    </>
113
+  );
114
+}
115
+

+ 94
- 0
src/components/Upload/UploadVideo.jsx Voir le fichier

@@ -0,0 +1,94 @@
1
+import React, { useState, useRef, useEffect } from 'react';
2
+import { Button } from 'antd'
3
+import Upload from './Upload';
4
+// import { uplodeNoteImage } from '@/services/note';
5
+import styles from './style.less';
6
+
7
+function beforeUpload(file) {
8
+  const isMp4 = file.type === 'video/mp4';
9
+  if (!isMp4) {
10
+    message.error('请上传 MP4 视频!');
11
+  }
12
+
13
+  return isMp4;
14
+}
15
+
16
+export default (props) => {
17
+  //onPoster 改变主图事件   isScreenshot判断父组件是否需要截屏按钮
18
+  const { value, onChange, onPoster, isScreenshot } = props;
19
+  const [loading, setLoading] = useState(false);//上传按钮loading框
20
+  const [capturing, setCapturing] = useState(false);//截屏按钮loading框
21
+  const [video, setVideo] = useState()//当前上传的视频
22
+  const [isVideoReady, setIsVideoReady] = useState(false)//判断视频是否加载成功
23
+  
24
+  const canvasRef = useRef()
25
+  const videoRef = useRef()
26
+
27
+  useEffect(() => {
28
+    setVideo(value)//父组件的视频获取成功时改变当前页面的视频
29
+  }, [value])
30
+
31
+  const handleChange = info => {
32
+    if (info.file.status === 'uploading') {
33
+      setLoading(true);
34
+      return;
35
+    }
36
+    if (info.file.status === 'error') {
37
+      setLoading(false);
38
+      return;
39
+    }
40
+    if (info.file.status === 'done') {
41
+      onChange(info.file.response);
42
+      setLoading(false);
43
+    }
44
+  };
45
+
46
+  //开始截图
47
+  const handelScreenshot = () => {
48
+    const ctx = canvasRef.current.getContext("2d");  
49
+    ctx.drawImage(videoRef.current, 0, 0);    
50
+    const image = canvasRef.current.toDataURL("image/png", 0.9)
51
+    setCapturing(true)
52
+    // uplodeNoteImage({ base64: image }).then((res) => {
53
+    //   onPoster(res)
54
+    //   setCapturing(false)
55
+    // })
56
+  }
57
+
58
+  //视频加载成功事件
59
+  const handleVideoReady = (e) => {
60
+    setIsVideoReady(true)
61
+    canvasRef.current.width = e.target.videoWidth;
62
+    canvasRef.current.height = e.target.videoHeight;
63
+  }
64
+
65
+  return (
66
+    <div className={styles['video-uploader']}>
67
+      <Upload
68
+        className="image-uploader"
69
+        showUploadList={false}
70
+        beforeUpload={beforeUpload}
71
+        onChange={handleChange}
72
+      >
73
+        <div style={{ maxWidth: '1px', maxHeight: '1px', overflow: 'hidden' }}>
74
+          <canvas ref={canvasRef}></canvas>
75
+        </div>
76
+        <Button loading={loading}>
77
+          点击上传视频 (MP4)
78
+        </Button>
79
+
80
+      </Upload>
81
+      {
82
+        isScreenshot && (
83
+          // disabled={!isVideoReady} 当视频准备成功时截屏按钮可以点击  否则不能点击
84
+          <Button loading={capturing} style={{ marginLeft: '1em' }} onClick={handelScreenshot} disabled={!isVideoReady}>截屏</Button>
85
+        )
86
+      }
87
+      {
88
+        !!video && (
89
+          <video ref={videoRef} width="320" height='500' crossOrigin='anonymous' controls src={video} onCanPlay={handleVideoReady}></video>
90
+        )
91
+      }
92
+    </div>
93
+  );
94
+}

+ 11
- 0
src/components/Upload/index.jsx Voir le fichier

@@ -0,0 +1,11 @@
1
+import Upload from './Upload';
2
+import UploadImage from './UploadImage';
3
+import UploadImageList from './UploadImageList';
4
+import UploadVideo from './UploadVideo';
5
+
6
+export {
7
+  Upload,
8
+  UploadImage,
9
+  UploadImageList,
10
+  UploadVideo,
11
+}

+ 32
- 0
src/components/Upload/request.js Voir le fichier

@@ -0,0 +1,32 @@
1
+import request from '@/utils/request';
2
+
3
+const upload = (file, fileType = 'image') => {
4
+  const formData = new FormData();
5
+  formData.append("file", file);
6
+  formData.append("fileType", fileType);
7
+
8
+  return request('/api/admin/file', {
9
+    method: 'post',
10
+    data: formData,
11
+    headers: {
12
+      'Content-Type': 'multipart/form-data',
13
+    }
14
+  });
15
+}
16
+
17
+/**
18
+ * 上传文件
19
+ * @returns 
20
+ */
21
+export function uploadFile(params) {
22
+  const { file, onSuccess, onError } = params;
23
+  upload(file).then((res) => {
24
+    onSuccess(res, file)
25
+  }).catch((e) => {
26
+    onError(e)
27
+  });
28
+
29
+  return {
30
+    abort: () => {},
31
+  };
32
+}

+ 7
- 0
src/components/Upload/style.less Voir le fichier

@@ -0,0 +1,7 @@
1
+.video-uploader {
2
+  video {
3
+    margin-top: 2em;
4
+    width: 100%;
5
+    max-height: 360px;
6
+  }
7
+}

+ 87
- 0
src/components/Wangeditor/index.jsx Voir le fichier

@@ -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 Voir le fichier

@@ -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
+}

+ 79
- 0
src/index.less Voir le fichier

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

+ 31
- 0
src/layouts/AuthLayout/Main.jsx Voir le fichier

@@ -0,0 +1,31 @@
1
+import React from "react";
2
+import { Layout } from "antd";
3
+import { useLocation, Outlet, useNavigate } from "react-router-dom";
4
+import { useModel } from "@/store";
5
+import useRoute from "@/routes/hooks/useRoute";
6
+import SiderBar from "./components/SiderBar";
7
+import Header from "./components/Header";
8
+import Container from "./components/Container";
9
+import { authRoutes } from "@/routes/routes";
10
+import { getMenuItems } from "@/routes/menus";
11
+
12
+import "./style.less";
13
+
14
+export default (props) => {
15
+  const { theme } = useModel("system");
16
+  const { user } = useModel("user");
17
+  const navigate = useNavigate();
18
+  const location = useLocation();
19
+  const menus = getMenuItems(authRoutes);
20
+
21
+
22
+  return (
23
+    <Layout className={theme} style={{ height: "100vh" }}>
24
+      <Header theme={theme} user={user} />
25
+      <Layout style={{ height: "calc(100vh - var(--header-height))" }}>
26
+        <SiderBar theme={theme} menus={menus} location={location} />
27
+        <Container location={location} />
28
+      </Layout>
29
+    </Layout>
30
+  );
31
+};

+ 3
- 0
src/layouts/AuthLayout/RedirectLogin.jsx Voir le fichier

@@ -0,0 +1,3 @@
1
+import { Navigate } from "react-router-dom";
2
+
3
+export default () => <Navigate to="/login" />;

+ 31
- 0
src/layouts/AuthLayout/RequireLogin.jsx Voir le fichier

@@ -0,0 +1,31 @@
1
+import React, { useState, useEffect } from "react";
2
+import { useNavigate } from "react-router-dom";
3
+import { useModel } from "@/store";
4
+
5
+export default (props) => {
6
+  const { user, getCurrentUser } = useModel("user");
7
+  const navigate = useNavigate();
8
+  const [userStatus, setUserStatus] = useState(user && user.userId ? 1 : 0);
9
+
10
+  useEffect(() => {
11
+    if (!user || !user.userId) {
12
+      getCurrentUser()
13
+        .then((res) => {
14
+          setUserStatus(1);
15
+        })
16
+        .catch((err) => {
17
+          console.error(err);
18
+          setUserStatus(-1);
19
+        });
20
+    }
21
+  }, []);
22
+
23
+  useEffect(() => {
24
+    if (userStatus === -1) {
25
+      navigate("/login");
26
+      return;
27
+    }
28
+  }, [userStatus]);
29
+
30
+  return userStatus < 1 ? null : props.children;
31
+};

+ 72
- 0
src/layouts/AuthLayout/components/Background.jsx Voir le fichier

@@ -0,0 +1,72 @@
1
+import React from 'react'
2
+
3
+const wrapperStyle = {
4
+  width: '100%',
5
+  height: '100%',
6
+  position: 'absolute',
7
+  zIndex: 0,
8
+  top: 0,
9
+  left: 0,
10
+  overflow: 'hidden',
11
+}
12
+
13
+const colors = [
14
+  '#fff1f0',
15
+  '#fff2e8',
16
+  '#fff7e6',
17
+  '#fffbe6',
18
+  '#feffe6',
19
+  '#fcffe6',
20
+  '#f6ffed',
21
+  '#e6fffb',
22
+  '#e6f4ff',
23
+  '#f0f5ff',
24
+  '#f9f0ff',
25
+  '#fff0f6',
26
+]
27
+
28
+export default (props) => {
29
+  const ref = React.useRef();
30
+
31
+  const [color1, color2] = React.useMemo(() => {
32
+    const inx1 = Math.floor(Math.random() * colors.length);
33
+    const inx2 = Math.floor(Math.random() * colors.length);
34
+
35
+    return [colors[inx1], colors[inx2]];
36
+  }, []);
37
+
38
+  React.useEffect(() => {
39
+    const dom = ref.current;
40
+    const canvas = dom.firstElementChild;
41
+    canvas.width = dom.offsetWidth;
42
+    canvas.height = dom.offsetHeight;
43
+
44
+    const centerX = canvas.width / 2;
45
+    const centerY = canvas.height / 2;
46
+    const size = centerX / 3
47
+
48
+    const ctx = canvas.getContext("2d");
49
+    
50
+    ctx.filter = `blur(200px)`;
51
+
52
+    ctx.beginPath()
53
+    ctx.fillStyle = color1; // "#ffd8bf";
54
+    ctx.ellipse(centerX + 100, centerY + 80, size, size + Math.random() * 100, Math.PI / 4, 0, 2 * Math.PI);
55
+    ctx.fill();
56
+    ctx.closePath();
57
+
58
+    ctx.beginPath()
59
+    ctx.fillStyle = color2; // "#e6fffb";
60
+    ctx.ellipse(centerX + 400, centerY + 120, size, size + Math.random() * 100, Math.PI / 2, 0, 2 * Math.PI);
61
+    ctx.fill();
62
+    ctx.closePath();
63
+
64
+  }, []);
65
+
66
+  return (
67
+    <div ref={ref} style={wrapperStyle}>
68
+      <canvas>
69
+      </canvas>
70
+    </div>
71
+  )
72
+}

+ 31
- 0
src/layouts/AuthLayout/components/Container.jsx Voir le fichier

@@ -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: `0 ${marginSpace}px` }}>
24
+        <PageTransition location={props.location}>
25
+          <Outlet />
26
+        </PageTransition>
27
+      </Content>
28
+      <Footer ref={footerRef} />
29
+    </div>
30
+  )
31
+}

+ 15
- 0
src/layouts/AuthLayout/components/Footer.jsx Voir le fichier

@@ -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
+})

+ 36
- 0
src/layouts/AuthLayout/components/Header/Exit.jsx Voir le fichier

@@ -0,0 +1,36 @@
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
+import { logout } from "@/services/login";
7
+
8
+const { confirm } = Modal;
9
+
10
+export default (props) => {
11
+  const navigate = useNavigate();
12
+  // const { setUser } = useModel("user");
13
+
14
+  const onExit = () => {
15
+    confirm({
16
+      title: "确认退出系统?",
17
+      onOk: () => {
18
+        logout(); // 调用接口
19
+        localStorage.removeItem("token");
20
+        // setUser();
21
+        navigate("/login");
22
+      },
23
+    });
24
+  };
25
+
26
+  return (
27
+    <Button
28
+      className="font"
29
+      type="text"
30
+      icon={<LogoutOutlined />}
31
+      onClick={onExit}
32
+    >
33
+      退出
34
+    </Button>
35
+  );
36
+};

+ 14
- 0
src/layouts/AuthLayout/components/Header/Language.jsx Voir le fichier

@@ -0,0 +1,14 @@
1
+import { ProFormSelect } from "@ant-design/pro-components";
2
+import { Select } from "antd";
3
+import React from "react";
4
+import { useModel } from '@/store';
5
+export default (props) => {
6
+
7
+  const {lang}=useModel("language")
8
+  const language = [
9
+    { label: "中文简体", value: "zh-CN" },
10
+    { label: "中文繁体", value: "zh-Tw" },
11
+    { label: "英文", value: "en-Us" },
12
+  ];
13
+  return <Select defaultValue={lang} style={{ width: 120 }} options={language} />;
14
+};

+ 24
- 0
src/layouts/AuthLayout/components/Header/Title.jsx Voir le fichier

@@ -0,0 +1,24 @@
1
+
2
+import useRoute from '@/routes/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
+}

+ 120
- 0
src/layouts/AuthLayout/components/Header/User.jsx Voir le fichier

@@ -0,0 +1,120 @@
1
+import React, {
2
+  useState,
3
+  forwardRef,
4
+  useRef,
5
+  useImperativeHandle,
6
+} from "react";
7
+import { Avatar, Dropdown } from "antd";
8
+import {
9
+  ModalForm,
10
+  ProFormText,
11
+  ProFormDependency,
12
+} from "@ant-design/pro-components";
13
+import md5 from "md5";
14
+import { changePassword } from "@/services/login";
15
+import { useNavigate } from "react-router-dom";
16
+
17
+const DefaultAvatar = ({ color = "#1296db" }) => (
18
+  <svg
19
+    viewBox="0 0 1024 1024"
20
+    version="1.1"
21
+    xmlns="http://www.w3.org/2000/svg"
22
+    width="24"
23
+    height="24"
24
+  >
25
+    <path
26
+      d="M938.666667 883.2c0-8.533333 0-42.666667-21.333334-81.066667-12.8-25.6-34.133333-42.666667-59.733333-59.733333-29.866667-17.066667-68.266667-34.133333-115.2-42.666667-4.266667 0-34.133333-4.266667-59.733333-12.8-29.866667-8.533333-42.666667-12.8-46.933334-17.066666v-25.6c4.266667-55.466667 34.133333-85.333333 59.733334-110.933334 8.533333-8.533333 17.066667-17.066667 25.6-29.866666 29.866667-42.666667 34.133333-85.333333 34.133333-93.866667 0-8.533333 0-12.8-4.266667-21.333333s-8.533333-17.066667-12.8-21.333334c0-12.8 4.266667-25.6 4.266667-34.133333 0-21.333333 4.266667-55.466667-4.266667-89.6 0-4.266667 0-12.8-4.266666-21.333333-8.533333-25.6-17.066667-46.933333-34.133334-64-8.533333 0-68.266667-59.733333-230.4-72.533334H405.333333c-8.533333 0-17.066667 0-25.6 4.266667-29.866667 8.533333-34.133333 29.866667-38.4 38.4v25.6l-4.266666 4.266667c-4.266667 4.266667-46.933333 42.666667-51.2 93.866666-4.266667 34.133333-4.266667 85.333333 0 123.733334-4.266667 8.533333-12.8 17.066667-12.8 38.4 0 8.533333 4.266667 51.2 34.133333 89.6 8.533333 8.533333 12.8 17.066667 25.6 29.866666 25.6 29.866667 55.466667 59.733333 59.733333 110.933334v25.6c-8.533333 4.266667-21.333333 12.8-51.2 21.333333s-59.733333 12.8-64 12.8c-42.666667 8.533333-81.066667 21.333333-110.933333 38.4-25.6 17.066667-46.933333 38.4-59.733333 59.733333-21.333333 38.4-21.333333 72.533333-21.333334 81.066667v21.333333c0 17.066667 17.066667 34.133333 38.4 34.133334h776.533334c21.333333 0 38.4-17.066667 38.4-38.4v-17.066667z"
27
+      fill={color}
28
+    ></path>
29
+  </svg>
30
+);
31
+
32
+const ChangePassword = forwardRef((props, ref) => {
33
+  const [visible, setVisible] = useState(false);
34
+  const navigate = useNavigate();
35
+  const onFinish = async (values) => {
36
+    const data = {
37
+      originPassword: md5(values.password),
38
+      newPassword: md5(values.newPassword),
39
+    };
40
+    await changePassword(data);
41
+    navigate(`/login`);
42
+
43
+    return true;
44
+  };
45
+
46
+  useImperativeHandle(ref, () => {
47
+    return {
48
+      show: () => setVisible(true),
49
+    };
50
+  });
51
+
52
+  return (
53
+    <ModalForm
54
+      title="修改密码"
55
+      width="480px"
56
+      open={visible}
57
+      onOpenChange={setVisible}
58
+      onFinish={onFinish}
59
+    >
60
+      <ProFormText.Password
61
+        label="原始密码"
62
+        name="password"
63
+        rules={[{ required: true, message: "请输入原始密码!" }]}
64
+      />
65
+      <ProFormText.Password
66
+        label="新密码"
67
+        name="newPassword"
68
+        rules={[{ required: true, message: "请输入新密码!" }]}
69
+      />
70
+
71
+      <ProFormDependency name={["newPassword"]}>
72
+        {({ newPassword }) => (
73
+          <ProFormText.Password
74
+            label="确认新密码"
75
+            name="newPassword2"
76
+            rules={[
77
+              { required: true, message: "请输入新密码!" },
78
+              () => ({
79
+                validator(_, value) {
80
+                  if (!value || value == newPassword) {
81
+                    return Promise.resolve();
82
+                  }
83
+
84
+                  return Promise.reject("两次输入密码不一致");
85
+                },
86
+              }),
87
+            ]}
88
+          />
89
+        )}
90
+      </ProFormDependency>
91
+    </ModalForm>
92
+  );
93
+});
94
+
95
+export default (props) => {
96
+  const items = [
97
+    {
98
+      key: "changePassword",
99
+      label: "修改密码",
100
+    },
101
+  ];
102
+
103
+  const passRef = useRef();
104
+  const { user = {} } = props;
105
+
106
+  const onClick = ({ key }) => {
107
+    if (key === "changePassword") {
108
+      passRef.current.show();
109
+    }
110
+  };
111
+  return (
112
+    <Dropdown menu={{ items, onClick }}>
113
+      <div className="user-info">
114
+        <Avatar size={24} src={<DefaultAvatar />} />
115
+        <span className="font">{user?.user?.userName}</span>
116
+        <ChangePassword user={user} ref={passRef} />
117
+      </div>
118
+    </Dropdown>
119
+  );
120
+};

+ 30
- 0
src/layouts/AuthLayout/components/Header/index.jsx Voir le fichier

@@ -0,0 +1,30 @@
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
+import Language from './Language';
9
+
10
+const { Header } = Layout;
11
+
12
+export default (props) => {
13
+  const { user } = props;
14
+
15
+  const className = useMemo(() => classNames({
16
+    'layout-header': true,
17
+    'light': props.theme === 'light',
18
+  }), [props.theme]);
19
+
20
+  return (
21
+    <Header className={className}>
22
+      <Logo />
23
+      <Space>
24
+        <Language />
25
+        <User user={user} />
26
+        <Exit />
27
+      </Space>
28
+    </Header>
29
+  )
30
+}

+ 19
- 0
src/layouts/AuthLayout/components/HtmlTitle.jsx Voir le fichier

@@ -0,0 +1,19 @@
1
+
2
+import { Helmet } from "react-helmet";
3
+import { useModel } from '@/store';
4
+import useRoute from '@/routes/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
+}

+ 17
- 0
src/layouts/AuthLayout/components/Logo.jsx Voir le fichier

@@ -0,0 +1,17 @@
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
+    <div className="logo">
12
+      <img src="./logo.png" alt="" />
13
+      <h3>{app.shorName}</h3>
14
+    </div>
15
+    // </NavLink>
16
+  )
17
+}

+ 31
- 0
src/layouts/AuthLayout/components/Menus.jsx Voir le fichier

@@ -0,0 +1,31 @@
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
+  return (
23
+    <Menu
24
+      mode="inline"
25
+      style={menuStyle}
26
+      theme={theme}
27
+      items={items}
28
+      selectedKeys={selectedKeys}
29
+    />
30
+  )
31
+}

+ 21
- 0
src/layouts/AuthLayout/components/PageTransition/index.jsx Voir le fichier

@@ -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 Voir le fichier

@@ -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: 0; // 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/SiderBar.jsx Voir le fichier

@@ -0,0 +1,21 @@
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
+  return (
17
+    <Sider className='layout-sidebar' theme={theme} collapsible width={width}>
18
+      <Menus theme={theme} items={menus} location={location} />
19
+    </Sider>
20
+  );
21
+}

+ 55
- 0
src/layouts/AuthLayout/index.jsx Voir le fichier

@@ -0,0 +1,55 @@
1
+import React from "react";
2
+import { Spin, Layout } from "antd";
3
+import HtmlTitle from "./components/HtmlTitle";
4
+// import Main from './Main';
5
+import withLogin from "./withLogin";
6
+import { useLocation, useNavigate } from "react-router-dom";
7
+import { useModel } from "@/store";
8
+
9
+import SiderBar from "./components/SiderBar";
10
+import Header from "./components/Header";
11
+import Container from "./components/Container";
12
+import { authRoutes } from "@/routes/routes";
13
+import { getMenuItems } from "@/routes/menus";
14
+import RequireLogin from "./RequireLogin";
15
+import "./style.less";
16
+
17
+const Spinner = () => (
18
+  <div
19
+    style={{
20
+      width: "100%",
21
+      height: "100%",
22
+      display: "grid",
23
+      placeItems: "center",
24
+    }}
25
+  >
26
+    <Spin />
27
+  </div>
28
+);
29
+
30
+export default (props) => {
31
+  const { theme } = useModel("system");
32
+  const { user, menus } = useModel("user");
33
+  const navigate = useNavigate();
34
+  const location = useLocation();
35
+  // const menus = getMenuItems(authRoutes);
36
+  // const RequireLogin = React.lazy(() => {
37
+  //   return withLogin(import("./Main"));
38
+  // });
39
+  return (
40
+    <Spin spinning={!user} size="large">
41
+      <HtmlTitle />
42
+      {/* <React.Suspense fallback={<Spinner />}> */}
43
+      <RequireLogin>
44
+        <Layout className={theme} style={{ height: "100vh" }}>
45
+          <Header theme={theme} user={user} />
46
+          <Layout style={{ height: "calc(100vh - var(--header-height))" }}>
47
+            <SiderBar theme="light" menus={menus} location={location} />
48
+            <Container location={location} />
49
+          </Layout>
50
+        </Layout>
51
+      </RequireLogin>
52
+      {/* </React.Suspense> */}
53
+    </Spin>
54
+  );
55
+};

+ 81
- 0
src/layouts/AuthLayout/style.less Voir le fichier

@@ -0,0 +1,81 @@
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
+    color: #000;
12
+  }
13
+
14
+  .header-content {
15
+    flex: 1;
16
+
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
+}
78
+
79
+.ant-menu-vertical {
80
+  border: none;
81
+}

+ 27
- 0
src/layouts/AuthLayout/withLogin.jsx Voir le fichier

@@ -0,0 +1,27 @@
1
+import React from "react";
2
+import store from "@/store";
3
+
4
+export default function withLogin (factory) {
5
+  return new Promise((resolve) => {
6
+    const { user, getCurrentUser } = store.getState("user");
7
+    const loged = user && (user.userId || user.id);
8
+    if (loged) {
9
+      resolve(factory);
10
+    } else {
11
+      let ticket = undefined;
12
+      const match = window.location?.href?.match(/ticket=([^?&/]*)/);
13
+      if (match?.length > 0) {
14
+        ticket = match[1];
15
+      }
16
+
17
+      getCurrentUser({ ticket: ticket })
18
+        .then(() => {
19
+          resolve(factory);
20
+        })
21
+        .catch((e) => {
22
+          console.error(e);
23
+          resolve(import("./RedirectLogin"));
24
+        });
25
+    }
26
+  });
27
+}

+ 28
- 0
src/locale/config.js Voir le fichier

@@ -0,0 +1,28 @@
1
+import i18n from 'i18next';
2
+import { initReactI18next } from 'react-i18next';
3
+import translation_en from './en.json';
4
+import translation_zh from './zh.json';
5
+import translation_zht from './zht.json';
6
+
7
+const resources = {
8
+    en: {
9
+        translation: translation_en,
10
+    },
11
+    zh: {
12
+        translation: translation_zh,
13
+    },
14
+    zht: {
15
+        translation: translation_zht,
16
+    },
17
+};
18
+
19
+i18n.use(initReactI18next).init({
20
+    resources,
21
+    // 默认语言  zh/en/zht  中文/英文/繁体
22
+    lng: 'en',
23
+    interpolation: {
24
+        escapeValue: false,
25
+    },
26
+});
27
+
28
+export default i18n;

+ 4
- 0
src/locale/en.json Voir le fichier

@@ -0,0 +1,4 @@
1
+{
2
+  "language": "Language",
3
+  "switch": "Switch"
4
+}

+ 4
- 0
src/locale/zh.json Voir le fichier

@@ -0,0 +1,4 @@
1
+{
2
+  "language": "语言",
3
+  "switch": "选择"
4
+}

+ 5
- 0
src/locale/zht.json Voir le fichier

@@ -0,0 +1,5 @@
1
+{
2
+    "language": "語言",
3
+    "switch": "選擇"
4
+  }
5
+  

+ 21
- 0
src/main.jsx Voir le fichier

@@ -0,0 +1,21 @@
1
+import React from "react";
2
+import ReactDOM from "react-dom/client";
3
+import Background from "./layouts/AuthLayout/components/Background";
4
+import Router from "./routes/Router";
5
+import "./index.less";
6
+import { Provider } from "./store";
7
+import { initTokenIfExists } from "./utils/sso";
8
+
9
+import { ConfigProvider } from "antd";
10
+import zhCN from "antd/es/locale/zh_CN";
11
+import enUS from "antd/es/locale/en_US";
12
+import "moment/dist/locale/zh-cn";
13
+
14
+ReactDOM.createRoot(document.getElementById("root")).render(
15
+  <ConfigProvider locale={zhCN} >
16
+    <Provider>
17
+      {/* <Background /> */}
18
+      <Router />
19
+    </Provider>
20
+  </ConfigProvider>
21
+);

+ 29
- 0
src/pages/404/index.jsx Voir le fichier

@@ -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
+}

+ 24
- 0
src/pages/firstPage/components/Brand.jsx Voir le fichier

@@ -0,0 +1,24 @@
1
+import React from "react";
2
+export default (props) => {
3
+  const imgs = [
4
+    "https://qbitnetwork.com/_nuxt/img/002.001.2fc0413.png",
5
+    "https://qbitnetwork.com/_nuxt/img/002.001.2fc0413.png",
6
+    "https://qbitnetwork.com/_nuxt/img/002.001.2fc0413.png",
7
+    "https://qbitnetwork.com/_nuxt/img/002.001.2fc0413.png",
8
+    "https://qbitnetwork.com/_nuxt/img/002.001.2fc0413.png",
9
+    "https://qbitnetwork.com/_nuxt/img/002.001.2fc0413.png",
10
+    "https://qbitnetwork.com/_nuxt/img/002.001.2fc0413.png",
11
+    "https://qbitnetwork.com/_nuxt/img/002.001.2fc0413.png",
12
+  ];
13
+  return (
14
+    <div className="brand">
15
+      <div className="brand_data">服务品牌/机构客户已逾12,000+</div>
16
+      <div style={{ height: "60px" }}></div>
17
+      <div className="brand_img">
18
+        {imgs.map((x) => {
19
+          return <img src={x} alt="" height="54"/>;
20
+        })}
21
+      </div>
22
+    </div>
23
+  );
24
+};

+ 66
- 0
src/pages/firstPage/components/Content.jsx Voir le fichier

@@ -0,0 +1,66 @@
1
+import React, { useEffect, useState } from "react";
2
+export default (props) => {
3
+  const [currentImageIndex, setCurrentImageIndex] = useState(0);
4
+  const [isFadingOut, setIsFadingOut] = useState(false);
5
+
6
+  const imgShow = [
7
+    "https://qbitnetwork.com/_nuxt/img/001.003.9de6ffe.png",
8
+    "https://qbitnetwork.com/_nuxt/img/001.002.c7d53bd.png",
9
+    "https://qbitnetwork.com/_nuxt/img/001.001.97d8def.png",
10
+  ];
11
+
12
+  const updateImageIndex = () => {
13
+    setIsFadingOut(true);
14
+
15
+    setCurrentImageIndex((prevIndex) => (prevIndex + 1) % imgShow.length);
16
+
17
+    setTimeout(() => {
18
+      setIsFadingOut(false);
19
+    }, 1500); 
20
+  };
21
+  useEffect(() => {
22
+    const timer = setInterval(updateImageIndex, 5000);
23
+
24
+    // 清除定时器
25
+    return () => clearInterval(timer);
26
+  }, []);
27
+
28
+  return (
29
+    <div className="page_details">
30
+      <div className="page_black">
31
+        <span>
32
+          一站式全球资金
33
+          <br />
34
+          管理平台
35
+        </span>
36
+        <p>
37
+          通过搭建数字化的跨境支付和金融基础设施,帮助企业实现更高时效、更低成本的全球收付和财务管理,轻松实现全球业务扩展与增长。
38
+        </p>
39
+        <div className="flickerAll">
40
+          <div className="flicker">
41
+            <span>现在注册</span>
42
+          </div>
43
+          <div className="flicker flickers">
44
+            <span>联系我们</span>
45
+          </div>
46
+        </div>
47
+      </div>
48
+      <img
49
+        className="svgImg"
50
+        src="https://qbitnetwork.com/_nuxt/img/001.000.94cac20.svg"
51
+        alt=""
52
+      />
53
+      <div className="imgRound">
54
+        <div className="relatives">
55
+          <img
56
+            src={imgShow[currentImageIndex]}
57
+            className={isFadingOut ? "fade-out" : ""}
58
+            alt=""
59
+          />
60
+        </div>
61
+      </div>
62
+
63
+
64
+    </div>
65
+  );
66
+};

+ 238
- 0
src/pages/firstPage/components/DetailedPlan.jsx Voir le fichier

@@ -0,0 +1,238 @@
1
+import { ArrowRightOutlined, RightOutlined } from "@ant-design/icons";
2
+import React from "react";
3
+export default (props) => {
4
+  return (
5
+    <div style={{ width: "100%", backgroundColor: "#f0f8f8" }}>
6
+      <div className="marginAuto">
7
+        <div style={{ height: "120px" }}></div>
8
+        <div className="detailed_plan">
9
+          {/* 收款 */}
10
+          <div className="detailed_plan_payment">
11
+            <div style={{ width: "590px" }}>
12
+              <div className="detailed_plan_payment_text">收款</div>
13
+              <div style={{ height: "4px" }}></div>
14
+              <div className="detailed_plan_payment_title">
15
+                <div>一个账户</div>
16
+                <div>轻松管理全球资金</div>
17
+              </div>
18
+              <div style={{ height: "48px" }}></div>
19
+              <div className="detailed_plan_payment_gap">
20
+                <div>
21
+                  <div className="title">多币种</div>
22
+                  <div style={{ height: "12px" }}></div>
23
+                  <div className="nicety">
24
+                    <div>
25
+                      快速开设多币种账户,支持14+本地币种,轻松收取本地&跨境资金。
26
+                    </div>
27
+                  </div>
28
+                </div>
29
+                <div>
30
+                  <div className="title">轻松收款</div>
31
+                  <div style={{ height: "12px" }}></div>
32
+                  <div className="nicety">
33
+                    <div>
34
+                      直连Shopify、PayPal等主流平台,订单信息自动同步,统一管理。
35
+                    </div>
36
+                  </div>
37
+                </div>
38
+                <div>
39
+                  <div className="title">高效管理</div>
40
+                  <div style={{ height: "12px" }}></div>
41
+                  <div className="nicety">
42
+                    <div>
43
+                      可根据需求设置多位管理员,定制化角色查看、管理和操作,赋能团队高效协作。
44
+                    </div>
45
+                  </div>
46
+                </div>
47
+              </div>
48
+
49
+              <div style={{ height: "40px" }}></div>
50
+
51
+              <div className="signal">
52
+                <a
53
+                  href="JavaScript:;"
54
+                  style={{
55
+                    fontWeight: "600",
56
+                    fontSize: "18px",
57
+                  }}
58
+                >
59
+                  了解全球收款账户
60
+                </a>
61
+              </div>
62
+            </div>
63
+            <img
64
+              src="https://qbitnetwork.com/_nuxt/img/005.fa1511a.png"
65
+              alt=""
66
+              width="790"
67
+            />
68
+          </div>
69
+          {/* 管理 */}
70
+          <div className="detailed_plan_manger">
71
+            <div className="manger">
72
+              <div className="detailed_plan_manger_text">管理</div>
73
+              <div style={{ height: "4px" }}></div>
74
+              <div className="manger_title">
75
+                <div>一个账户</div>
76
+                <div>轻松管理全球资金</div>
77
+              </div>
78
+              <div style={{ height: "48px" }}></div>
79
+              <div className="detailed_plan_manger_gap">
80
+                <div>
81
+                  <div className="title">高效操作</div>
82
+                  <div style={{ height: "12px" }}></div>
83
+                  <div className="nicety">
84
+                    <div>全流程线上操作,一键快捷开启收款服务。</div>
85
+                    <div>轻松运营,便捷对账,助力企业实现高效管理。</div>
86
+                  </div>
87
+                </div>
88
+                <div>
89
+                  <div className="title">实时互转</div>
90
+                  <div style={{ height: "12px" }}></div>
91
+                  <div className="nicety">
92
+                    <div>无额外成本,0费用完成Qbit账户间互转。</div>
93
+                    <div>不必提现,实时收付境内外供应商费用。</div>
94
+                  </div>
95
+                </div>
96
+              </div>
97
+            </div>
98
+            <img
99
+              src="https://qbitnetwork.com/_nuxt/img/006.dc6c4bc.png"
100
+              width="878"
101
+              alt=""
102
+            />
103
+          </div>
104
+          {/* 付款 */}
105
+          <div className="detailed_plan_pay">
106
+            <div style={{ width: "590px" }}>
107
+              <div className="detailed_plan_pay_text">付款</div>
108
+              <div style={{ height: "4px" }}></div>
109
+              <div className="detailed_plan_pay_title">
110
+                <div>笔笔畅达</div>
111
+                <div>解锁全球新机遇</div>
112
+              </div>
113
+              <div style={{ height: "48px" }}></div>
114
+              <div className="detailed_plan_pay_gap">
115
+                <div>
116
+                  <div className="title">国际付款</div>
117
+                  <div style={{ height: "12px" }}></div>
118
+                  <div className="nicety">
119
+                    <div>
120
+                      覆盖180+个国家和地区的国际支付能力,连接本地清算网络,快至T+0到账,实现资金快速流转。{" "}
121
+                    </div>
122
+                  </div>
123
+                  <div style={{ height: "16px" }}></div>
124
+                  <div className="signal">
125
+                    <a
126
+                      href="JavaScript:;"
127
+                      style={{
128
+                        fontWeight: "600",
129
+                        fontSize: "18px",
130
+                      }}
131
+                    >
132
+                      了解国际付款
133
+                    </a>
134
+                  </div>
135
+                </div>
136
+                <div>
137
+                  <div className="title">国际付款</div>
138
+                  <div style={{ height: "12px" }}></div>
139
+                  <div className="nicety">
140
+                    <div>
141
+                      覆盖180+个国家和地区的国际支付能力,连接本地清算网络,快至T+0到账,实现资金快速流转。{" "}
142
+                    </div>
143
+                  </div>
144
+                  <div style={{ height: "16px" }}></div>
145
+                  <div className="signal">
146
+                    <a
147
+                      href="JavaScript:;"
148
+                      style={{
149
+                        fontWeight: "600",
150
+                        fontSize: "18px",
151
+                      }}
152
+                    >
153
+                      了解国际付款
154
+                    </a>
155
+                  </div>
156
+                </div>
157
+                <div>
158
+                  <div className="title">国际付款</div>
159
+                  <div style={{ height: "12px" }}></div>
160
+                  <div className="nicety">
161
+                    <div>
162
+                      覆盖180+个国家和地区的国际支付能力,连接本地清算网络,快至T+0到账,实现资金快速流转。{" "}
163
+                    </div>
164
+                  </div>
165
+                </div>
166
+              </div>
167
+
168
+              <div style={{ height: "40px" }}></div>
169
+            </div>
170
+            <img
171
+              src="https://qbitnetwork.com/_nuxt/img/007.c4273a0.png"
172
+              alt=""
173
+              width="896"
174
+            />
175
+          </div>
176
+          {/* 金融 */}
177
+          <div className="detailed_plan_finance">
178
+            <div style={{ width: "590px" }}>
179
+              <div className="detailed_plan_finance_text">供应链金融</div>
180
+              <div style={{ height: "4px" }}></div>
181
+              <div className="detailed_plan_finance_title">
182
+                <div>开拓高速增长新蓝海</div>
183
+              </div>
184
+              <div style={{ height: "48px" }}></div>
185
+              <div className="detailed_plan_finance_gap">
186
+                <div>
187
+                  <div className="title">线上申请,便捷省心</div>
188
+                  <div style={{ height: "12px" }}></div>
189
+                  <div className="nicety">
190
+                    <div>线上申请流程,快至24小时内完成风控,48小时放款。</div>
191
+                  </div>
192
+                </div>
193
+                <div>
194
+                  <div className="title">实时授权,无抵押</div>
195
+                  <div style={{ height: "12px" }}></div>
196
+                  <div className="nicety">
197
+                    <div>
198
+                      纯信用贷款一次授信、循环使用,流程操作简单,省心省事。
199
+                    </div>
200
+                  </div>
201
+                </div>
202
+                <div>
203
+                  <div className="title">更低利率,灵活还款</div>
204
+                  <div style={{ height: "12px" }}></div>
205
+                  <div className="nicety">
206
+                    <div>
207
+                      动态额度年化利率,日&周扣款模型,账户收款余额直扣。
208
+                    </div>
209
+                  </div>
210
+                </div>
211
+              </div>
212
+
213
+              <div style={{ height: "40px" }}></div>
214
+
215
+              <div className="signal">
216
+                <a
217
+                  href="JavaScript:;"
218
+                  style={{
219
+                    fontWeight: "600",
220
+                    fontSize: "18px",
221
+                  }}
222
+                >
223
+                  了解供应链金融
224
+                </a>
225
+              </div>
226
+            </div>
227
+
228
+            <img
229
+              src="https://qbitnetwork.com/_nuxt/img/008.4078b74.png"
230
+              alt=""
231
+              width="820"
232
+            />
233
+          </div>
234
+        </div>
235
+      </div>
236
+    </div>
237
+  );
238
+};

+ 29
- 0
src/pages/firstPage/components/Header.jsx Voir le fichier

@@ -0,0 +1,29 @@
1
+import Logo from "@/layouts/AuthLayout/components/Logo";
2
+import { Button, Layout, Popover, Space } from "antd";
3
+import classNames from "classnames";
4
+import React, { useMemo } from "react";
5
+import HeaderCenter from "./HeaderCenter";
6
+import InWebsite from "./InWebsite";
7
+import Content from "./Content";
8
+
9
+export default (props) => {
10
+  const className = useMemo(
11
+    () =>
12
+      classNames({
13
+        "layout-header": true,
14
+        light: props.theme === "light",
15
+      }),
16
+    [props.theme]
17
+  );
18
+  return (
19
+    <Layout.Header className={className}>
20
+      {/* <div style={{ width: "1200px", display: "flex", margin: "0 auto" }}> */}
21
+        <Logo />
22
+        <Space className="space">
23
+          <HeaderCenter />
24
+          <InWebsite />
25
+        </Space>
26
+      {/* </div> */}
27
+    </Layout.Header>
28
+  );
29
+};

+ 17
- 0
src/pages/firstPage/components/HeaderCenter.jsx Voir le fichier

@@ -0,0 +1,17 @@
1
+import { Button, Popconfirm, Popover } from "antd";
2
+import React from "react";
3
+export default (props) => {
4
+  return (
5
+    <div style={{marginLeft:"80px"}}>
6
+      <Popover title="产品" trigger="hover">
7
+        <Button className="btn">产品</Button>
8
+      </Popover>
9
+      <Popover title="关于" trigger="hover">
10
+        <Button className="btn">关于</Button>
11
+      </Popover>
12
+      <Popover title="资源" trigger="hover">
13
+        <Button className="btn">资源</Button>
14
+      </Popover>
15
+    </div>
16
+  );
17
+};

+ 15
- 0
src/pages/firstPage/components/InWebsite.jsx Voir le fichier

@@ -0,0 +1,15 @@
1
+import { ContainerOutlined } from "@ant-design/icons";
2
+import React from "react";
3
+export default (props) => {
4
+  const onLogin=()=>{}
5
+  const onRegister=()=>{}
6
+  return (
7
+    <div className="InWebsite">
8
+      <ContainerOutlined />
9
+      <div onClick={onLogin}>登录</div>
10
+      <div className="flicker"  onClick={onRegister}>
11
+        <span >注册</span>
12
+      </div>
13
+    </div>
14
+  );
15
+};

+ 71
- 0
src/pages/firstPage/components/InitialHeart.jsx Voir le fichier

@@ -0,0 +1,71 @@
1
+import React from "react";
2
+export default (props) => {
3
+  const listBox = [
4
+    {
5
+      id: 1,
6
+      img: "https://qbitnetwork.com/_nuxt/img/005.001.e3cfeda.svg",
7
+      label: "牌照监管",
8
+      remake:
9
+        "合规即服务,持有美国、香港等国家及地区金融牌照,严格遵守监管机构相关法律法规要求。",
10
+    },
11
+    {
12
+      id: 2,
13
+      img: "https://qbitnetwork.com/_nuxt/img/005.002.b81a265.svg",
14
+      label: "资金安全",
15
+      remake:
16
+        "采取了严格的资金安全保障措施,金融解决方案获得合作银行与机构的合规检验与认可。",
17
+    },
18
+    {
19
+      id: 3,
20
+      img: "https://qbitnetwork.com/_nuxt/img/005.003.55a8f50.svg",
21
+      label: "风控安全",
22
+      remake:
23
+        "实时监控交易,结合复杂的反洗钱监控算法及各类金融行业黑名单,为您的交易保驾护航。",
24
+    },
25
+    {
26
+      id: 4,
27
+      img: "https://qbitnetwork.com/_nuxt/img/005.004.576a014.svg",
28
+      label: "隐私保护",
29
+      remake:
30
+        "构建完善的信息管理体系,所有数据均存放在有密码控制的服务器中,未经授权,绝不透露。",
31
+    },
32
+  ];
33
+  return (
34
+    <div className="initial_heart">
35
+      <div className="initial_heart_important">您的每一笔钱,都很重要</div>
36
+      <div style={{ height: "40px" }}></div>
37
+      <div className="initial_heart_detail">
38
+        自成立之初,Qbit趣比汇便以积极态度拥抱全球行业监管,保障您的资金安全。
39
+      </div>
40
+      <div style={{ height: "40px" }}></div>
41
+
42
+      <div className="signal">
43
+        <a
44
+          href="JavaScript:;"
45
+          style={{
46
+            fontWeight: "600",
47
+            fontSize: "18px",
48
+          }}
49
+        >
50
+          Qbit趣比汇的安全性
51
+        </a>
52
+      </div>
53
+      <div style={{ height: "40px" }}></div>
54
+      <div className="box4">
55
+        {listBox.map((x) => {
56
+          return (
57
+            <div key={x.id} className="box4Background">
58
+              <div className="box4Detail">
59
+                <img src={x.img} alt="" />
60
+                <div style={{ height: "16px" }}></div>
61
+                <div style={{fontWeight:"600",fontSize:"22px",color:"#FFF"}}>{x.label}</div>
62
+                <div style={{ height: "12px" }}></div>
63
+                <div style={{opacity:".8",fontSize:"22px",color:"#FFF"}}>{x.remake}</div>
64
+              </div>
65
+            </div>
66
+          );
67
+        })}
68
+      </div>
69
+    </div>
70
+  );
71
+};

+ 30
- 0
src/pages/firstPage/components/Introduce.jsx Voir le fichier

@@ -0,0 +1,30 @@
1
+import React from "react";
2
+export default (props) => {
3
+  return (
4
+    <div className="introduce">
5
+      <div className="introduce_plan">一站式打通海外资金「收、管、付、融」</div>
6
+      <div style={{ height: "36px" }}></div>
7
+      <div className="introduce_title">
8
+        为跨境贸易构建高效金融生态,解决企业全链路财务需求。
9
+      </div>
10
+      <div style={{ height: "60px" }}></div>
11
+      <img
12
+        style={{ position: "relative" }}
13
+        src="https://qbitnetwork.com/_nuxt/img/003.85fc294.png"
14
+        height="436"
15
+        alt=""
16
+      />
17
+      <img
18
+        src="https://qbitnetwork.com/_nuxt/img/004.f3f7b95.png"
19
+        alt=""
20
+        height="822"
21
+        style={{
22
+          position: "absolute",
23
+          top: "62px",
24
+          right: "-360px",
25
+        }}
26
+      />
27
+      <div style={{ height: "120px" }}></div>
28
+    </div>
29
+  );
30
+};

+ 11
- 0
src/pages/firstPage/components/SelectRegister.jsx Voir le fichier

@@ -0,0 +1,11 @@
1
+import React from "react";
2
+export default (props) => {
3
+  return (
4
+    <div className="select_register">
5
+      <div>选择Qbit趣比汇,加速您的全球业务</div>
6
+      <div className="flicker" >
7
+        <span>现在注册</span>
8
+      </div>
9
+    </div>
10
+  );
11
+};

+ 17
- 0
src/pages/firstPage/components/ToInitialHeart.jsx Voir le fichier

@@ -0,0 +1,17 @@
1
+import React from 'react'
2
+import InitialHeart from './InitialHeart'
3
+export default (props) => {
4
+return (
5
+ <div
6
+      style={{
7
+        backgroundColor: "#313131",
8
+      }}
9
+    >
10
+      <div className="marginAuto">
11
+        <div style={{ height: "120px" }}></div>
12
+        <InitialHeart />
13
+      <div style={{ height: "120px" }}></div>
14
+      </div>
15
+    </div>
16
+)
17
+}

+ 16
- 0
src/pages/firstPage/components/ToIntroduce.jsx Voir le fichier

@@ -0,0 +1,16 @@
1
+import React from "react";
2
+import Introduce from "./Introduce";
3
+export default (props) => {
4
+  return (
5
+    <div
6
+      style={{
7
+        backgroundColor: "#313131",
8
+      }}
9
+    >
10
+      <div className="marginAuto">
11
+        <div style={{ height: "120px" }}></div>
12
+        <Introduce />
13
+      </div>
14
+    </div>
15
+  );
16
+};

+ 18
- 0
src/pages/firstPage/components/ToSelectRegister.jsx Voir le fichier

@@ -0,0 +1,18 @@
1
+import React from "react";
2
+import SelectRegister from "./SelectRegister";
3
+export default (props) => {
4
+  return (
5
+    <div
6
+      style={{
7
+        background:
8
+          "linear-gradient(135deg, rgb(153, 250, 255) 0%, rgb(195, 255, 166) 100%)",
9
+      }}
10
+    >
11
+      <div className="marginAuto">
12
+        <div style={{ height: "120px" }}></div>
13
+        <SelectRegister />
14
+        <div style={{ height: "120px" }}></div>
15
+      </div>
16
+    </div>
17
+  );
18
+};

+ 42
- 0
src/pages/firstPage/index.jsx Voir le fichier

@@ -0,0 +1,42 @@
1
+import Logo from "@/layouts/AuthLayout/components/Logo";
2
+import { Layout, Space } from "antd";
3
+import classNames from "classnames";
4
+import React, { useMemo } from "react";
5
+import Header from "./components/Header";
6
+
7
+import "./index.less";
8
+import Content from "./components/Content";
9
+import Brand from "./components/Brand";
10
+import Introduce from "./components/Introduce";
11
+import ToIntroduce from "./components/ToIntroduce";
12
+import DetailedPlan from "./components/DetailedPlan";
13
+import InitialHeart from "./components/ToInitialHeart";
14
+import ToInitialHeart from "./components/ToInitialHeart";
15
+import ToSelect from "./components/ToSelectRegister";
16
+import ToSelectRegister from "./components/ToSelectRegister";
17
+export default (props) => {
18
+  const theme = "light";
19
+  return (
20
+    <Layout
21
+      className={theme}
22
+      style={{
23
+        backgroundColor: "#f0f8f8",
24
+        fontFamily: "CN",
25
+        overflow: "hidden",
26
+      }}
27
+    >
28
+      <div className="marginAuto">
29
+        <Header theme={theme} />
30
+        <Content />
31
+        <div style={{ height: "284px" }}> </div>
32
+        <Brand />
33
+        <div style={{ height: "200px" }}></div>
34
+      </div>
35
+      <ToIntroduce />
36
+      <DetailedPlan />
37
+      <div style={{ height: "200px" }}></div>
38
+      <ToInitialHeart />
39
+      <ToSelectRegister />
40
+    </Layout>
41
+  );
42
+};

+ 606
- 0
src/pages/firstPage/index.less Voir le fichier

@@ -0,0 +1,606 @@
1
+.space {
2
+    margin-left: auto !important;
3
+    margin-right: auto !important;
4
+}
5
+
6
+.marginAuto {
7
+    width: 1200px;
8
+    margin: 0 auto;
9
+    position: relative
10
+}
11
+
12
+.btn {
13
+    background-color: rgba(0, 0, 0, 0);
14
+    border: none;
15
+
16
+}
17
+
18
+.btn:hover {
19
+    background-color: rgba(0, 0, 0, 0);
20
+}
21
+
22
+
23
+
24
+.InWebsite {
25
+    display: flex;
26
+    align-items: center;
27
+    margin-left: 50%;
28
+    width: 100%;
29
+    position: relative;
30
+
31
+    * {
32
+        margin: 0 20px;
33
+        cursor: pointer;
34
+
35
+    }
36
+
37
+
38
+    .flicker {
39
+        background: black;
40
+        color: white;
41
+        width: 72px;
42
+        height: 42px !important;
43
+        border-radius: 12px;
44
+        text-align: center;
45
+
46
+        span {
47
+            display: block;
48
+            text-align: center;
49
+            position: absolute;
50
+            top: 0;
51
+        }
52
+
53
+    }
54
+
55
+    .flicker:hover {
56
+        animation-duration: 1s;
57
+        animation-fill-mode: both;
58
+        animation-name: box-button-background-black-unset;
59
+        transition-timing-function: linear;
60
+    }
61
+
62
+
63
+
64
+
65
+
66
+
67
+}
68
+
69
+
70
+
71
+.page_details {
72
+    display: flex;
73
+    margin-top: 180px;
74
+
75
+    .page_black {
76
+        width: 616px;
77
+
78
+        &>span {
79
+            display: block;
80
+            box-sizing: border-box;
81
+            color: #000;
82
+            font-weight: 700;
83
+            font-size: 70px;
84
+            line-height: 94px;
85
+
86
+        }
87
+
88
+        &>p {
89
+            padding-top: 28px;
90
+            padding-bottom: 60px;
91
+            color: #3b4d59;
92
+            width: 550px;
93
+            font-size: 20px;
94
+            line-height: 32px;
95
+        }
96
+
97
+        .flickerAll {
98
+            display: flex;
99
+
100
+            .flicker {
101
+                width: 136px;
102
+                height: 50px;
103
+                margin: 0 25px;
104
+                background: black;
105
+                color: white;
106
+                border-radius: 12px;
107
+                padding: 12px 36px;
108
+                position: relative;
109
+                line-height: 50px;
110
+
111
+                span {
112
+                    display: block;
113
+                    text-align: center;
114
+                    position: absolute;
115
+                    top: 0;
116
+                    font-size: 16px;
117
+                    font-weight: 600;
118
+                }
119
+
120
+            }
121
+
122
+            .flicker:hover {
123
+                animation-duration: 1s;
124
+                animation-fill-mode: both;
125
+                animation-name: box-button-background-black-unset;
126
+                transition-timing-function: linear;
127
+            }
128
+
129
+            .flickers:hover {
130
+                animation-duration: 1s;
131
+                animation-fill-mode: both;
132
+                animation-name: box-button-background-transparent-black;
133
+                transition-timing-function: linear;
134
+
135
+                span {
136
+                    color: #FFF;
137
+                }
138
+
139
+            }
140
+
141
+
142
+            .flickers {
143
+                border: 1 solid #000;
144
+                background: rgba(0, 0, 0, 0);
145
+                border: 1px solid #000;
146
+
147
+                span {
148
+                    color: #000;
149
+                }
150
+
151
+            }
152
+        }
153
+    }
154
+
155
+    .svgImg {
156
+        position: absolute;
157
+        top: 68px;
158
+        right: -260px;
159
+    }
160
+
161
+    .imgRound {
162
+        position: absolute;
163
+        top: 210px;
164
+        right: -260px;
165
+
166
+        .relatives {
167
+            position: relative;
168
+
169
+            img {
170
+                height: 686px;
171
+            }
172
+
173
+            .fade-out {
174
+                animation: fadeOut 1.5s forwards;
175
+            }
176
+        }
177
+    }
178
+}
179
+
180
+.brand {
181
+    .brand_data {
182
+        font-size: 22px;
183
+        font-weight: 600;
184
+        color: #698f8f;
185
+    }
186
+
187
+    .brand_img {
188
+        display: flex;
189
+        flex-wrap: wrap;
190
+        row-gap: 60px;
191
+        column-gap: 120px;
192
+    }
193
+}
194
+
195
+.introduce {
196
+
197
+    .introduce_plan {
198
+        color: #fff;
199
+        font-size: 60px;
200
+        font-weight: 700;
201
+    }
202
+
203
+    .introduce_title {
204
+        color: #fff;
205
+        font-size: 20px;
206
+    }
207
+}
208
+
209
+.detailed_plan {
210
+    display: grid;
211
+    gap: 200px;
212
+
213
+    .detailed_plan_payment {
214
+        display: flex;
215
+        align-items: center;
216
+        position: relative;
217
+        left: 0px;
218
+
219
+        .detailed_plan_payment_text {
220
+            color: #698f8f;
221
+            font-weight: 600;
222
+            font-size: 18px;
223
+        }
224
+
225
+        .detailed_plan_payment_title {
226
+            color: #000;
227
+            font-weight: 700;
228
+            font-size: 60px;
229
+        }
230
+
231
+
232
+
233
+        .detailed_plan_payment_gap {
234
+            display: grid;
235
+            gap: 40px;
236
+
237
+            .title {
238
+                color: #031520;
239
+                font-weight: 600;
240
+                font-size: 22px;
241
+            }
242
+
243
+            .nicety {
244
+                color: #5e6970;
245
+                font-size: 18px;
246
+                font-weight: 100;
247
+            }
248
+
249
+        }
250
+
251
+        .signal {
252
+            align-items: center;
253
+            display: flex
254
+        }
255
+
256
+        .signal:after {
257
+            content: "\003E";
258
+            color: #1890FF;
259
+            margin-left: 0.3em;
260
+            font-weight: 900;
261
+            width: 18px;
262
+            height: 28px;
263
+            line-height: 28px;
264
+        }
265
+
266
+        .signal:hover::after {
267
+            content: "\2794";
268
+            color: #1890FF;
269
+            margin-left: 0.3em;
270
+            font-weight: 900;
271
+            width: 18px;
272
+            height: 28px;
273
+            line-height: 28px;
274
+        }
275
+
276
+
277
+    }
278
+
279
+    .detailed_plan_manger {
280
+        display: flex;
281
+        align-items: center;
282
+        flex-direction: row-reverse;
283
+        position: relative;
284
+        left: -268px;
285
+
286
+        .manger {
287
+            width: 590px;
288
+        }
289
+
290
+        .detailed_plan_manger_text {
291
+            color: #698f8f;
292
+            font-weight: 600;
293
+            font-size: 18px;
294
+        }
295
+
296
+        .manger_title {
297
+            color: #000;
298
+            font-weight: 700;
299
+            font-size: 60px;
300
+        }
301
+
302
+        .detailed_plan_manger_gap {
303
+            display: grid;
304
+            gap: 40px;
305
+
306
+            .title {
307
+                color: #031520;
308
+                font-weight: 600;
309
+                font-size: 22px;
310
+            }
311
+
312
+            .nicety {
313
+                color: #5e6970;
314
+                font-size: 18px;
315
+                font-weight: 100;
316
+            }
317
+        }
318
+    }
319
+
320
+
321
+    .detailed_plan_pay {
322
+        display: flex;
323
+        align-items: center;
324
+        position: relative;
325
+        left: 0px;
326
+
327
+        .detailed_plan_pay_text {
328
+            color: #698f8f;
329
+            font-weight: 600;
330
+            font-size: 18px;
331
+        }
332
+
333
+        .detailed_plan_pay_title {
334
+            color: #000;
335
+            font-weight: 700;
336
+            font-size: 60px;
337
+        }
338
+
339
+
340
+
341
+        .detailed_plan_pay_gap {
342
+            display: grid;
343
+            gap: 40px;
344
+
345
+            .title {
346
+                color: #031520;
347
+                font-weight: 600;
348
+                font-size: 22px;
349
+            }
350
+
351
+            .nicety {
352
+                color: #5e6970;
353
+                font-size: 18px;
354
+                font-weight: 100;
355
+            }
356
+
357
+        }
358
+
359
+        .signal {
360
+            align-items: center;
361
+            display: flex
362
+        }
363
+
364
+        .signal:after {
365
+            content: "\003E";
366
+            color: #1890FF;
367
+            margin-left: 0.3em;
368
+            font-weight: 900;
369
+            width: 18px;
370
+            height: 28px;
371
+            line-height: 28px;
372
+        }
373
+
374
+        .signal:hover::after {
375
+            content: "\2794";
376
+            color: #1890FF;
377
+            margin-left: 0.3em;
378
+            font-weight: 900;
379
+            width: 18px;
380
+            height: 28px;
381
+            line-height: 28px;
382
+        }
383
+
384
+
385
+    }
386
+
387
+
388
+    .detailed_plan_finance {
389
+        display: flex;
390
+        align-items: center;
391
+        position: relative;
392
+        left: -210px;
393
+        flex-direction: row-reverse !important;
394
+
395
+        .detailed_plan_finance_text {
396
+            color: #698f8f;
397
+            font-weight: 600;
398
+            font-size: 18px;
399
+        }
400
+
401
+        .detailed_plan_finance_title {
402
+            color: #000;
403
+            font-weight: 700;
404
+            font-size: 60px;
405
+        }
406
+
407
+
408
+
409
+        .detailed_plan_finance_gap {
410
+            display: grid;
411
+            gap: 40px;
412
+
413
+            .title {
414
+                color: #031520;
415
+                font-weight: 600;
416
+                font-size: 22px;
417
+            }
418
+
419
+            .nicety {
420
+                color: #5e6970;
421
+                font-size: 18px;
422
+                font-weight: 100;
423
+            }
424
+
425
+        }
426
+
427
+        .signal {
428
+            align-items: center;
429
+            display: flex
430
+        }
431
+
432
+        .signal:after {
433
+            content: "\003E";
434
+            color: #1890FF;
435
+            margin-left: 0.3em;
436
+            font-weight: 900;
437
+            width: 18px;
438
+            height: 28px;
439
+            line-height: 28px;
440
+        }
441
+
442
+        .signal:hover::after {
443
+            content: "\2794";
444
+            color: #1890FF;
445
+            margin-left: 0.3em;
446
+            font-weight: 900;
447
+            width: 18px;
448
+            height: 28px;
449
+            line-height: 28px;
450
+        }
451
+
452
+
453
+    }
454
+}
455
+
456
+
457
+.initial_heart {
458
+    .initial_heart_important {
459
+        color: #fff;
460
+        font-size: 60px;
461
+        font-weight: 700;
462
+    }
463
+
464
+    .initial_heart_detail {
465
+        color: #FFF;
466
+        opacity: .8;
467
+        font-size: 18;
468
+    }
469
+
470
+    .signal {
471
+        align-items: center;
472
+        display: flex;
473
+        color: #99faff;
474
+
475
+        a {
476
+
477
+            color: #99faff;
478
+        }
479
+
480
+    }
481
+
482
+    .signal:after {
483
+        content: "\003E";
484
+        // color: #1890FF;
485
+        margin-left: 0.3em;
486
+        font-weight: 900;
487
+        width: 18px;
488
+        height: 28px;
489
+        line-height: 28px;
490
+    }
491
+
492
+    .signal:hover::after {
493
+        content: "\2794";
494
+        // color: #1890FF;
495
+        margin-left: 0.3em;
496
+        font-weight: 900;
497
+        width: 18px;
498
+        height: 28px;
499
+        line-height: 28px;
500
+    }
501
+
502
+
503
+    .box4 {
504
+        display: flex;
505
+        gap: 12px;
506
+        flex-wrap: wrap;
507
+
508
+        .box4Background {
509
+            background: #f0f5f50d;
510
+            border-radius: 6px;
511
+            height: 220px;
512
+            width: 594px;
513
+
514
+            .box4Detail {
515
+                padding: 28px 36px 40px 36px;
516
+            }
517
+        }
518
+    }
519
+}
520
+
521
+.select_register {
522
+    display: flex;
523
+    align-items: center;
524
+    position: relative;
525
+
526
+    div:nth-child(1) {
527
+        color: #000;
528
+        font-weight: 700;
529
+        font-size: 48px;
530
+    }
531
+
532
+    .flicker {
533
+        width: 136px;
534
+        height: 50px;
535
+        margin: 0 25px;
536
+        background: black;
537
+        color: white;
538
+        border-radius: 12px;
539
+        padding: 12px 36px;
540
+        position: relative;
541
+        line-height: 50px;
542
+
543
+        span {
544
+            display: block;
545
+            text-align: center;
546
+            position: absolute;
547
+            top: 0;
548
+            font-size: 16px;
549
+            font-weight: 600;
550
+        }
551
+
552
+    }
553
+
554
+    .flicker:hover {
555
+        animation-duration: 1s;
556
+        animation-fill-mode: both;
557
+        animation-name: box-button-background-black-unset;
558
+        transition-timing-function: linear;
559
+    }
560
+}
561
+
562
+@keyframes fadeOut {
563
+    from {
564
+        opacity: 0;
565
+    }
566
+
567
+    to {
568
+        opacity: 1;
569
+    }
570
+}
571
+
572
+
573
+
574
+@keyframes box-button-background-transparent-black {
575
+    0% {
576
+        background: linear-gradient(45deg, transparent, transparent 20%, #99faff 40%, #c3ffa6 60%, #000 80%, #000);
577
+        background-position: 0 0;
578
+        background-size: 500% 100%;
579
+        border-color: #000;
580
+    }
581
+
582
+    100% {
583
+        background: linear-gradient(45deg, transparent, transparent 20%, #99faff 40%, #c3ffa6 60%, #000 80%, #000);
584
+        background-position: 100% 100%;
585
+        background-size: 500% 100%;
586
+        border-color: #000;
587
+    }
588
+}
589
+
590
+
591
+@keyframes box-button-background-black-unset {
592
+    0% {
593
+
594
+        background: linear-gradient(45deg, #000, #000 20%, #99faff 40%, #c3ffa6 60%, #000 80%, #000);
595
+        background-position: 0 0;
596
+        background-size: 500% 100%;
597
+        border-color: initial;
598
+    }
599
+
600
+    100% {
601
+        background: linear-gradient(45deg, #000, #000 20%, #99faff 40%, #c3ffa6 60%, #000 80%, #000);
602
+        background-position: 100% 100%;
603
+        background-size: 500% 100%;
604
+        border-color: #000;
605
+    }
606
+}

+ 111
- 0
src/pages/fxuser/Edit.jsx Voir le fichier

@@ -0,0 +1,111 @@
1
+import React, { useMemo } from "react";
2
+import { Button, Card, Form, Input, Select } from "antd";
3
+import useBool from "@/utils/hooks/useBool";
4
+import { useSearchParams, useNavigate } from "react-router-dom";
5
+import Page from "@/components/Page";
6
+import { formItemLayout, tailFormItemLayout } from "@/utils/form";
7
+import {
8
+  getFxUserId,
9
+  postFxUser,
10
+  putFxUser,
11
+  postFxUserAndLogin,
12
+  putFxUserAndLogin,
13
+} from "@/services/fxuser";
14
+const { Option } = Select;
15
+
16
+export default (props) => {
17
+  const [loading, startLoading, cancelLoading] = useBool();
18
+  const [submiting, startSubmit, cancelSubmit] = useBool();
19
+
20
+  const [searchParams] = useSearchParams();
21
+  const [form] = Form.useForm();
22
+  const navigate = useNavigate();
23
+
24
+  const id = searchParams.get("id");
25
+
26
+  React.useEffect(() => {
27
+    if (id) {
28
+      getFxUserId(id).then((res) => {
29
+        form.setFieldsValue(res);
30
+      });
31
+    }
32
+  });
33
+
34
+  const onFinish = (values) => {
35
+    console.log(values);
36
+    startSubmit();
37
+    if (id) {
38
+      //修改
39
+      putFxUserAndLogin(id, values)
40
+        .then((res) => {
41
+          cancelSubmit();
42
+          navigate(-1);
43
+        })
44
+        .catch(() => {
45
+          cancelSubmit();
46
+        });
47
+    } else {
48
+      //新增
49
+      postFxUserAndLogin(values)
50
+        .then((res) => {
51
+          cancelSubmit();
52
+          navigate(-1);
53
+        })
54
+        .catch(() => {
55
+          cancelSubmit();
56
+        });
57
+    }
58
+  };
59
+
60
+  return (
61
+    <Page>
62
+      <Card loading={loading}>
63
+        <Form
64
+          onFinish={onFinish}
65
+          form={form}
66
+          {...formItemLayout}
67
+          scrollToFirstError
68
+          style={{ maxWidth: "1000px" }}
69
+        >
70
+          <Form.Item
71
+            name="userName"
72
+            label="用户名称"
73
+            rules={[
74
+              {
75
+                required: true,
76
+                message: "请填写用户名称",
77
+              },
78
+            ]}
79
+          >
80
+            <Input placeholder="请输入用户名称" />
81
+          </Form.Item>
82
+          <Form.Item
83
+            name="phone"
84
+            label="手机号"
85
+            rules={[
86
+              {
87
+                required: true,
88
+                message: "请填写手机号",
89
+              },
90
+            ]}
91
+          >
92
+            <Input placeholder="请输入手机号" />
93
+          </Form.Item>
94
+          {id ? null : (
95
+            <Form.Item label="账 户">
96
+              <div style={{ color: "#333" }}>手机号;默认密码:123456</div>
97
+            </Form.Item>
98
+          )}
99
+          <Form.Item {...tailFormItemLayout}>
100
+            <Button loading={submiting} type="primary" htmlType="submit">
101
+              保存
102
+            </Button>
103
+            <Button style={{ marginLeft: "2em" }} onClick={() => navigate(-1)}>
104
+              返回
105
+            </Button>
106
+          </Form.Item>
107
+        </Form>
108
+      </Card>
109
+    </Page>
110
+  );
111
+};

+ 61
- 0
src/pages/fxuser/index.jsx Voir le fichier

@@ -0,0 +1,61 @@
1
+import List from "@/components/Page/List";
2
+import React from "react";
3
+
4
+import { getFxUser, deleteFxUserAndLogin } from "@/services/fxuser";
5
+
6
+import { useNavigate } from "react-router-dom";
7
+import { Button } from "antd";
8
+import moment from "moment";
9
+export default (props) => {
10
+  const navigate = useNavigate();
11
+  const actionRef = React.useRef();
12
+
13
+  const columns = [
14
+    {
15
+      title: "用户名",
16
+      dataIndex: "userName",
17
+      key: "userName",
18
+    },
19
+    {
20
+      title: "手机号",
21
+      dataIndex: "phone",
22
+      key: "phone",
23
+    },
24
+   
25
+    {
26
+      title: "创建时间",
27
+      dataIndex: "createdAt",
28
+      key: "createdAy",
29
+      search: false,
30
+      render: (t) => moment(t).format("YYYY-MM-DD HH:mm:ss"),
31
+    },
32
+   
33
+  ];
34
+
35
+  const DeleteFxUser = (record) => {
36
+    deleteFxUserAndLogin(record.userId).then((res) => {
37
+      actionRef.current.reload();
38
+    });
39
+  };
40
+  return (
41
+    <List
42
+      rowKey="userId"
43
+      actionRef={actionRef}
44
+      request={()=>{}}
45
+      onEdit={(record) => navigate(`/fxuser/edit?id=${record.userId}`)}
46
+      onDelete={DeleteFxUser}
47
+      toolBarRender={() => [
48
+        <Button
49
+          key="1"
50
+          type="primary"
51
+          onClick={() => {
52
+            navigate("edit");
53
+          }}
54
+        >
55
+          新增
56
+        </Button>,
57
+      ]}
58
+      columns={columns}
59
+    />
60
+  );
61
+};

+ 11
- 0
src/pages/login/Effect.jsx Voir le fichier

@@ -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
+};

+ 187
- 0
src/pages/login/LoginForm.bak.jsx Voir le fichier

@@ -0,0 +1,187 @@
1
+import React, { useEffect, useRef, useState } from "react";
2
+import md5 from "md5";
3
+import { Form } from "antd";
4
+import { login, qrCode, postQrCode } from "@/services/login";
5
+import { json, useSearchParams } from "react-router-dom";
6
+import LoginUse from "./LoginUse";
7
+import LoginQrcode from "./LoginQrcode.bak";
8
+export default (props) => {
9
+  const [searchParams] = useSearchParams();
10
+  const appid = searchParams.get("appid");
11
+  const { onSuccess, mode } = props;
12
+  const [form] = Form.useForm();
13
+  const [loading, setLoading] = useState(false);
14
+  const [showLogin, setShowLogin] = useState(true);
15
+  const [codeLapse, setCodeLapse] = useState(0); //0 未扫码,1 扫码, 2确认扫码 3取消扫码 4已删除
16
+
17
+  const qrcodeRef = useRef();
18
+  const refData = useRef();
19
+  useEffect(() => {
20
+    getQrCode();
21
+    console.log(refData);
22
+  }, [refData.id]);
23
+
24
+  const onFinish = (values) => {
25
+    const formData = new FormData();
26
+    formData.append("name", values.name);
27
+    formData.append("pwd", md5(values.pwd));
28
+    if (mode) {
29
+      formData.append("mode", mode);
30
+    }
31
+    formData.append("client", appid);
32
+
33
+    setLoading(true);
34
+    login(formData)
35
+      .then((res) => {
36
+        onSuccess(res);
37
+        setLoading(false);
38
+      })
39
+      .catch((err) => {
40
+        console.log("----err--", err);
41
+        setLoading(false);
42
+      });
43
+  };
44
+
45
+  const getQrCode = () => {
46
+    qrCode({ client: appid || "authcenter" })
47
+      .then((res) => {
48
+        refData.current = res;
49
+        createdCode(res);
50
+        checkTimeUp();
51
+
52
+        console.log(res);
53
+      })
54
+      .catch((err) => {
55
+        setCodeLapse(4);
56
+      });
57
+  };
58
+
59
+  const createdCode = (res) => {
60
+    new QRCode(qrcodeRef.current, {
61
+      width: 150,
62
+      height: 150,
63
+      text: res.qrCode,
64
+      correctLevel: 3,
65
+    });
66
+  };
67
+  /**
68
+   * @param {*轮询方法}
69
+   */
70
+  //  检测二维码状态 业务代码
71
+  const checkStatus = () => {
72
+    const { current } = refData;
73
+    return new Promise((resolve, reject) => {
74
+      postQrCode(current.id, mode)
75
+        .then((it) => {
76
+          if (current.expire >= 0) {
77
+            // 二维码还没有失效
78
+            current.expire--;
79
+            if (it.state == 0) {
80
+              // console.log("未扫码");
81
+              setCodeLapse(0);
82
+              resolve();
83
+            } else if (it.state == 1) {
84
+              // console.log("已扫码");
85
+              setCodeLapse(1);
86
+              resolve();
87
+            } else if (it.state == 3 || it.state == 4) {
88
+              setCodeLapse(4);
89
+              reject();
90
+            } else if (it.token) {
91
+              onSuccess(it);
92
+            } else if (it == null) {
93
+              console.log("null");
94
+              setCodeLapse(4);
95
+            }
96
+          } else {
97
+            // 二维码失效
98
+            console.log("二维码失效");
99
+            qrStop();
100
+            setCodeLapse(4);
101
+            reject();
102
+          }
103
+        })
104
+        .catch((err) => {
105
+          console.log(err);
106
+          qrStop();
107
+          setCodeLapse(4);
108
+          reject();
109
+        });
110
+    });
111
+  };
112
+
113
+  // 检测二维码状态 逻辑代码
114
+  let time = useRef();
115
+  const checkTimeUp = () => {
116
+    checkStatus()
117
+      .then(() => {
118
+        // console.log("===================");
119
+        // console.log("最上面的", time.current);
120
+        time.current = setTimeout(() => {
121
+          // console.log("上面的", time.current);
122
+          if (time.current) {
123
+            // console.log("下面的", time.current);
124
+            console.log("我先清除一下定时器");
125
+            clearTimeout(time.current);
126
+            checkTimeUp();
127
+          }
128
+        }, 1000);
129
+      })
130
+      .catch(() => {
131
+        if (time.current) {
132
+          clearTimeout(time.current);
133
+        }
134
+      });
135
+  };
136
+
137
+  // 删除二维码
138
+  const qrStop = () => {
139
+    if (qrcodeRef.current) {
140
+      const idv = qrcodeRef.current.children;
141
+      for (var i = idv.length - 1; i >= 0; i--) {
142
+        qrcodeRef.current.removeChild(idv[i]);
143
+      }
144
+    } else {
145
+      return;
146
+    }
147
+  };
148
+  // 重新刷新二维码
149
+  const onRenovate4 = () => {
150
+    // 重新查询
151
+    getQrCode();
152
+    setCodeLapse(0);
153
+  };
154
+  // 重新刷新二维码
155
+  const onRenovate1 = () => {
156
+    // 重新查询
157
+    clearTimeout(time.current);
158
+    setCodeLapse(0);
159
+    getQrCode();
160
+  };
161
+  // 切换账号登录
162
+  const checkLoginUse = () => {
163
+    setShowLogin(false);
164
+    clearTimeout(time.current);
165
+  };
166
+  // 切换二维码登录
167
+  const checkLoginwx = () => {
168
+    setShowLogin(true);
169
+    setCodeLapse(0);
170
+    getQrCode();
171
+  };
172
+  return (
173
+    <Form form={form} layout="vertical" onFinish={onFinish}>
174
+      {showLogin ? (
175
+        <LoginQrcode
176
+          qrcodeRef={qrcodeRef}
177
+          codeLapse={codeLapse}
178
+          onRenovate4={onRenovate4}
179
+          onRenovate1={onRenovate1}
180
+          checkLoginUse={checkLoginUse}
181
+        />
182
+      ) : (
183
+        <LoginUse loading={loading} checkLoginwx={checkLoginwx} />
184
+      )}
185
+    </Form>
186
+  );
187
+};

+ 67
- 0
src/pages/login/LoginForm.jsx Voir le fichier

@@ -0,0 +1,67 @@
1
+import React from "react";
2
+import md5 from "md5";
3
+import { Button, Form, Input, Radio } from "antd";
4
+import { login } from "@/services/login";
5
+
6
+export default (props) => {
7
+  const { onSuccess, mode, appid } = props;
8
+  const [form] = Form.useForm();
9
+  const [loading, setLoading] = React.useState(false);
10
+  console.log(process.env.NODE_ENV);
11
+  const onFinish = (values) => {
12
+    setLoading(true);
13
+    login({
14
+      loginName: values.loginName,
15
+      loginType: "platform.pc",
16
+      password: md5(values.password),
17
+    })
18
+      .then((res) => {
19
+        setLoading(true);
20
+        onSuccess(res.user);
21
+        setLoading(false);
22
+      })
23
+      .catch((err) => {
24
+        // console.log('----err--', err);
25
+        setLoading(false);
26
+      });
27
+
28
+    setLoading(false);
29
+  };
30
+  return (
31
+    <Form
32
+      form={form}
33
+      layout="vertical"
34
+      onFinish={onFinish}
35
+    // style={{ width: "260px", marginTop: "24px", paddingBottom: "24px" }}
36
+    >
37
+      <Form.Item
38
+        label="账户"
39
+        name="loginName"
40
+        rules={[{ required: true, message: "请输入账户" }]}
41
+      >
42
+        <Input placeholder="请输入账户" style={{ borderRadius: "4px" }} />
43
+      </Form.Item>
44
+      <Form.Item
45
+        label="密 码"
46
+        name="password"
47
+        rules={[{ required: true, message: "请输入密码" }]}
48
+      >
49
+        <Input.Password
50
+          placeholder="请输入密码"
51
+          style={{ borderRadius: "4px" }}
52
+        />
53
+      </Form.Item>
54
+      <Form.Item>
55
+        <Button
56
+          type="primary"
57
+          size="large"
58
+          style={{ width: "100%", marginTop: "24px", borderRadius: "4px" }}
59
+          loading={loading}
60
+          htmlType="submit"
61
+        >
62
+          登录
63
+        </Button>
64
+      </Form.Item>
65
+    </Form>
66
+  );
67
+};

+ 67
- 0
src/pages/login/LoginQrcode.bak.jsx Voir le fichier

@@ -0,0 +1,67 @@
1
+import React from "react";
2
+import "./qrcode.less";
3
+export default (props) => {
4
+  const { qrcodeRef, codeLapse, onRenovate4, onRenovate1, checkLoginUse } =
5
+    props;
6
+  console.log(codeLapse);
7
+
8
+  return (
9
+    <>
10
+      <span className="pageUse" onClick={checkLoginUse}>
11
+        使用账号登录
12
+      </span>
13
+      <div className="pageQr">
14
+        {codeLapse == 0 ? (
15
+          <div className="qrcode">
16
+            <div className="qrBoder">
17
+              <div ref={qrcodeRef}></div>
18
+            </div>
19
+            <span>微信扫一扫登录</span>
20
+          </div>
21
+        ) : codeLapse == 1 ? (
22
+          <div className="loginSvg">
23
+            <svg
24
+              className="svg"
25
+              width="74"
26
+              height="74"
27
+              viewBox="0 0 18 18"
28
+              xmlns="http://www.w3.org/2000/svg"
29
+            >
30
+              <path
31
+                d="M18 9.111C18 13.971 13.97 18 8.889 18 4.029 18 0 13.97 0 9.111 0 4.03 4.03 0 8.889 0 13.97 0 18 4.03 18 9.111zM4.788 9.79l2.876 2.976c.041.043.109.047.15.007l6.327-6.227a.196.196 0 00-.006-.275l-.236-.233a.221.221 0 00-.291-.017l-5.792 4.825c-.037.032-.114.034-.161-.002L5.329 9.06c-.09-.068-.207-.043-.274.048l-.28.38a.234.234 0 00.013.3z"
32
+                fill="#09BB07"
33
+                fillRule="evenodd"
34
+              ></path>
35
+              <path
36
+                d="M18 9.111C18 13.971 13.97 18 8.889 18 4.029 18 0 13.97 0 9.111 0 4.03 4.03 0 8.889 0 13.97 0 18 4.03 18 9.111zM4.788 9.79l2.876 2.976c.041.043.109.047.15.007l6.327-6.227a.196.196 0 00-.006-.275l-.236-.233a.221.221 0 00-.291-.017l-5.792 4.825c-.037.032-.114.034-.161-.002L5.329 9.06c-.09-.068-.207-.043-.274.048l-.28.38a.234.234 0 00.013.3z"
37
+                fill="#09BB07"
38
+                fillRule="evenodd"
39
+              ></path>
40
+              <path
41
+                d="M18 9.111C18 13.971 13.97 18 8.889 18 4.029 18 0 13.97 0 9.111 0 4.03 4.03 0 8.889 0 13.97 0 18 4.03 18 9.111zM4.788 9.79l2.876 2.976c.041.043.109.047.15.007l6.327-6.227a.196.196 0 00-.006-.275l-.236-.233a.221.221 0 00-.291-.017l-5.792 4.825c-.037.032-.114.034-.161-.002L5.329 9.06c-.09-.068-.207-.043-.274.048l-.28.38a.234.234 0 00.013.3z"
42
+                fill="#09BB07"
43
+                fillRule="evenodd"
44
+              ></path>
45
+            </svg>
46
+            <span className="ScanningCode">已扫码成功</span>
47
+
48
+            <a className="newCode" onClick={onRenovate1}>
49
+              重新扫码
50
+            </a>
51
+          </div>
52
+        ) : codeLapse == 4 ? (
53
+          <div className="loginPage">
54
+            <div className="loginLap">
55
+              <p>二维码已过期</p>
56
+              <p>
57
+                <a onClick={onRenovate4}>点击刷新</a>
58
+              </p>
59
+            </div>
60
+          </div>
61
+        ) : (
62
+          <div></div>
63
+        )}
64
+      </div>
65
+    </>
66
+  );
67
+};

+ 45
- 0
src/pages/login/LoginUse.jsx Voir le fichier

@@ -0,0 +1,45 @@
1
+import React from "react";
2
+import { Button, Form, Input } from "antd";
3
+export default (props) => {
4
+  const { loading, checkLoginwx } = props;
5
+  console.log(loading);
6
+  const style = {
7
+    display: "flex",
8
+    justifyContent: "flex-end",
9
+    cursor: "pointer",
10
+    color: "#576b95",
11
+    position: "absolute",
12
+    top: "0",
13
+    right: "0",
14
+    height: "33px",
15
+    lineHeight: "33px",
16
+  };
17
+  return (
18
+    <>
19
+      <span style={style} onClick={checkLoginwx}>
20
+        使用二维码登录
21
+      </span>
22
+
23
+      <Form.Item label="用户名" name="name" required>
24
+        <Input placeholder="请输入用户名" style={{ borderRadius: "4px" }} />
25
+      </Form.Item>
26
+      <Form.Item label="密 码" name="pwd" required>
27
+        <Input.Password
28
+          placeholder="请输入密码"
29
+          style={{ borderRadius: "4px" }}
30
+        />
31
+      </Form.Item>
32
+      <Form.Item>
33
+        <Button
34
+          type="primary"
35
+          size="large"
36
+          style={{ width: "100%", marginTop: "24px", borderRadius: "4px" }}
37
+          loading={loading}
38
+          htmlType="submit"
39
+        >
40
+          登录
41
+        </Button>
42
+      </Form.Item>
43
+    </>
44
+  );
45
+};

+ 24
- 0
src/pages/login/Particles.jsx Voir le fichier

@@ -0,0 +1,24 @@
1
+import React from 'react'
2
+
3
+export default (props) => {
4
+  React.useEffect(() => {
5
+    const baseUrl = import.meta.env.BASE_URL;
6
+    const oScript = document.createElement("script");
7
+    oScript.type = "text\/javascript";
8
+    oScript.onload = () => {
9
+      particlesJS.load('particles-js', `${baseUrl}particles/particles.json`, function() {
10
+        console.log('callback - particles.js config loaded');
11
+      });
12
+    }
13
+    document.querySelector('head').appendChild(oScript);
14
+    oScript.src = `${baseUrl}particles/particles.min.js`;
15
+
16
+    return () => {
17
+      document.querySelector('head').removeChild(oScript);
18
+    }
19
+  }, [])
20
+
21
+  return (
22
+    <div id="particles-js"></div>
23
+  )
24
+}

+ 159
- 0
src/pages/login/components/LoginQrCode.jsx Voir le fichier

@@ -0,0 +1,159 @@
1
+import React from 'react';
2
+import { Button } from 'antd';
3
+// import { qrCode, postQrCode } from "@/services/login";
4
+import QrCode from './QrCode';
5
+import SuccessIcon from './SuccessIcon';
6
+import styles from './qrcode.module.less';
7
+
8
+// 轮询
9
+const polling = (p, delay) => {
10
+  let t = null;
11
+  let cancelled = false;
12
+
13
+  const clearTicker = () => {
14
+    if (t) {
15
+      clearTimeout(t);
16
+      t = null;
17
+    }
18
+  }
19
+
20
+  const cancel = () => {
21
+    cancelled = true;
22
+    clearTimeout(t);
23
+  };
24
+
25
+  const fn = () => {
26
+    p().then((done) => {
27
+      clearTicker();
28
+
29
+      if (!done && !cancelled) {
30
+        t = setTimeout(fn, delay);
31
+      }
32
+    }).catch(() => {
33
+      clearTicker();
34
+    });
35
+  }
36
+
37
+  fn();
38
+
39
+  return cancel;
40
+};
41
+
42
+export default (props) => {
43
+  const { onSuccess, mode, appid } = props;
44
+
45
+  // 二维码是否初始化
46
+  const [qrInited, setQrInited] = React.useState(false);
47
+  // 是否扫码
48
+  const [scaned, setScaned] = React.useState(false);
49
+  // 二维码数据
50
+  const [qrData, setQrData] = React.useState();
51
+  // 是否发生错误
52
+  const [hasError, setHasError] = React.useState(false);
53
+  // 提示信息
54
+  const [tips, setTips] = React.useState();
55
+  // 停止轮询控制器
56
+  const pollingCancel = React.useRef();
57
+
58
+  // 重新开始
59
+  const onReset = () => {
60
+    // 停止轮询
61
+    if (pollingCancel.current) {
62
+      pollingCancel.current();
63
+    }
64
+
65
+    setQrInited(false);
66
+    setScaned(false);
67
+    setHasError(false);
68
+  }
69
+
70
+  // 校验二维码各种状态
71
+  // const checkQrCode = React.useCallback((qrId, params) => {
72
+  //   return new Promise((resolve, reject) => {
73
+  //     postQrCode(qrId, params).then(res => {
74
+  //       // 带有 token 说明扫码登录成功
75
+  //       if (res.token) {
76
+  //         onSuccess(res);
77
+  //         resolve(true);
78
+  //         return ;
79
+  //       } else {
80
+  //         // state 与 stateLabels 是对应关系
81
+  //         const { state } = res;
82
+  //         const stateLabels = [
83
+  //           '未扫码',
84
+  //           '扫码成功, 请确认登录',
85
+  //           '扫码确认',
86
+  //           '已取消',
87
+  //           '已删除',
88
+  //         ]
89
+
90
+  //         const isError = state > 2;
91
+  //         setScaned(state == 1);
92
+  //         setHasError(isError);
93
+  //         setTips(stateLabels[state]);
94
+
95
+  //         if (isError) {
96
+  //           reject();
97
+  //         } else {
98
+  //           resolve();
99
+  //         }
100
+  //       }
101
+  //     }).catch((err) => {
102
+  //       console.error(err);
103
+  //       setTips(err.data.msg || err.message || err);
104
+
105
+  //       setScaned(false);
106
+  //       setHasError(true);
107
+  //       reject();
108
+  //     });
109
+  //   });
110
+  // }, []);
111
+
112
+  // 请求二维码
113
+  // React.useEffect(() => {
114
+  //   if (!qrInited) {
115
+  //     qrCode({ client: appid || "authcenter" }).then(res => {
116
+  //       setQrData(res);
117
+  //       setQrInited(true);
118
+
119
+  //       // 开启轮询
120
+  //       const fn = () => checkQrCode(res.id, { mode });
121
+  //       pollingCancel.current = polling(fn, 1000);
122
+  //     })
123
+  //   }
124
+  // }, [qrInited, mode]);
125
+
126
+  return (
127
+    <div className={styles['qr-form']}>
128
+      <div className={styles['qr-form-wrapper']}>
129
+
130
+        <div className={styles['qr-box']}>
131
+          <QrCode text={qrData?.qrCode} />
132
+        </div>
133
+
134
+        {
135
+          (scaned || hasError) && (
136
+            <div className={styles['status-box']}>
137
+              <div>
138
+                {
139
+                  scaned && !hasError && <SuccessIcon />
140
+                }
141
+                <div className={styles['status-tip']}>{tips}</div>
142
+                <Button type='link' onClick={onReset}>点击刷新</Button>
143
+              </div>
144
+            </div>
145
+          )
146
+        }
147
+
148
+      </div>
149
+
150
+      <div className={styles['tip-box']}>
151
+        {
152
+          qrInited && !hasError && (
153
+            <div>请使用行信扫码登录</div>
154
+          )
155
+        }
156
+      </div>
157
+    </div>
158
+  )
159
+}

+ 28
- 0
src/pages/login/components/QrCode.jsx Voir le fichier

@@ -0,0 +1,28 @@
1
+import React from 'react';
2
+
3
+export default (props) => {
4
+  const { text, width = 200, height = 200 } = props;
5
+
6
+  const ref = React.useRef();
7
+  const qrRef = React.useRef();
8
+
9
+  React.useEffect(() => {
10
+    if (text) {
11
+      if (qrRef.current) {
12
+        qrRef.current.clear();
13
+        qrRef.current.makeCode(text);
14
+      } else {
15
+        qrRef.current = new QRCode(ref.current, {
16
+          text,
17
+          width,
18
+          height,
19
+          correctLevel: QRCode.CorrectLevel.Q
20
+        });
21
+      }
22
+    }
23
+  }, [text, width, height]);
24
+
25
+  return (
26
+    <div ref={ref}></div>
27
+  )
28
+}

+ 29
- 0
src/pages/login/components/SuccessIcon.jsx Voir le fichier

@@ -0,0 +1,29 @@
1
+import React from 'react'
2
+
3
+export default (props) => {
4
+  return (
5
+    <svg
6
+      className="svg"
7
+      width="74"
8
+      height="74"
9
+      viewBox="0 0 18 18"
10
+      xmlns="http://www.w3.org/2000/svg"
11
+    >
12
+      <path
13
+        d="M18 9.111C18 13.971 13.97 18 8.889 18 4.029 18 0 13.97 0 9.111 0 4.03 4.03 0 8.889 0 13.97 0 18 4.03 18 9.111zM4.788 9.79l2.876 2.976c.041.043.109.047.15.007l6.327-6.227a.196.196 0 00-.006-.275l-.236-.233a.221.221 0 00-.291-.017l-5.792 4.825c-.037.032-.114.034-.161-.002L5.329 9.06c-.09-.068-.207-.043-.274.048l-.28.38a.234.234 0 00.013.3z"
14
+        fill="#09BB07"
15
+        fillRule="evenodd"
16
+      ></path>
17
+      <path
18
+        d="M18 9.111C18 13.971 13.97 18 8.889 18 4.029 18 0 13.97 0 9.111 0 4.03 4.03 0 8.889 0 13.97 0 18 4.03 18 9.111zM4.788 9.79l2.876 2.976c.041.043.109.047.15.007l6.327-6.227a.196.196 0 00-.006-.275l-.236-.233a.221.221 0 00-.291-.017l-5.792 4.825c-.037.032-.114.034-.161-.002L5.329 9.06c-.09-.068-.207-.043-.274.048l-.28.38a.234.234 0 00.013.3z"
19
+        fill="#09BB07"
20
+        fillRule="evenodd"
21
+      ></path>
22
+      <path
23
+        d="M18 9.111C18 13.971 13.97 18 8.889 18 4.029 18 0 13.97 0 9.111 0 4.03 4.03 0 8.889 0 13.97 0 18 4.03 18 9.111zM4.788 9.79l2.876 2.976c.041.043.109.047.15.007l6.327-6.227a.196.196 0 00-.006-.275l-.236-.233a.221.221 0 00-.291-.017l-5.792 4.825c-.037.032-.114.034-.161-.002L5.329 9.06c-.09-.068-.207-.043-.274.048l-.28.38a.234.234 0 00.013.3z"
24
+        fill="#09BB07"
25
+        fillRule="evenodd"
26
+      ></path>
27
+    </svg>
28
+  )
29
+}

+ 41
- 0
src/pages/login/components/qrcode.module.less Voir le fichier

@@ -0,0 +1,41 @@
1
+
2
+.qr-form {
3
+  position: relative;
4
+  line-height: 2em;
5
+  text-align: center;
6
+  margin-top: 24px;
7
+
8
+  .qr-form-wrapper {
9
+    margin: 0 auto;
10
+    width: 240px;
11
+    height: 240px;
12
+    display: grid;
13
+    place-items: center;
14
+  }
15
+
16
+  .qr-box {
17
+    position: absolute;
18
+    padding: 15px;
19
+    box-sizing: border-box;
20
+    border: 1px solid #e2e2e2;
21
+    z-index: 1;
22
+  }
23
+
24
+  .status-box {
25
+    background: rgba(255, 255, 255, 0.98);
26
+    position: relative;
27
+    width: 100%;
28
+    height: 100%;
29
+    z-index: 10;
30
+    display: grid;
31
+    place-items: center;
32
+  
33
+    // .status-tip {
34
+    // }
35
+  }
36
+
37
+  .tip-box {
38
+    margin-top: 1em;
39
+    letter-spacing: 1.4px;
40
+  }
41
+}

+ 34
- 0
src/pages/login/foo.js Voir le fichier

@@ -0,0 +1,34 @@
1
+const { postQrCode } = require("@/services/login");
2
+
3
+const foo = (fp, delay) => {
4
+
5
+    let t = null;
6
+
7
+    const fn = () => {
8
+        fp().then(() => {
9
+            if (t) {
10
+                clearTimeout(t);
11
+            }
12
+    
13
+            t = setTimeout(fn, delay);
14
+        }).catch(() => {
15
+            if (t) {
16
+                clearTimeout(t);
17
+            }
18
+        });
19
+    }
20
+}
21
+
22
+const fp = () => {
23
+    return new Promise((resovle, reject) => {
24
+        postQrCode().then(() => {
25
+            if (succes) {
26
+                resovle()
27
+            } else {
28
+                reject()
29
+            }
30
+        }).catch(() => {
31
+            reject()
32
+        });
33
+    });
34
+}

+ 61
- 0
src/pages/login/index.jsx Voir le fichier

@@ -0,0 +1,61 @@
1
+import React, { useEffect } from "react";
2
+import { Button, message } from "antd";
3
+import { Helmet } from "react-helmet";
4
+import { useNavigate, useSearchParams } from "react-router-dom";
5
+import { useModel } from "@/store";
6
+import LoginForm from "./LoginForm";
7
+import LoginQrCode from "./components/LoginQrCode";
8
+import LoginEffect from "./Effect";
9
+import Particles from './Particles';
10
+import "./style.less";
11
+
12
+const year = new Date().getFullYear();
13
+
14
+export default (props) => {
15
+  const { app } = useModel("system");
16
+  const navigate = useNavigate();
17
+  const [searchParams] = useSearchParams();
18
+
19
+  const title = `欢迎使用${app.fullName}`;
20
+  const copyright = `${app.company} @ ${year}`;
21
+
22
+  const onChangeType = () => {
23
+    setLoginType(loginType == 0 ? 1 : 0);
24
+  };
25
+
26
+  const onSuccess = (row) => {
27
+    try {
28
+      navigate("/fxuser");
29
+    } catch (e) {
30
+      message.error(e);
31
+    }
32
+  };
33
+
34
+  return (
35
+    <div className="login-page">
36
+      <Helmet>
37
+        <title>{app.fullName}</title>
38
+      </Helmet>
39
+      <Particles />
40
+      <div className="login-page-container">
41
+        <div className="login-card">
42
+          <div className="login-card-left">
43
+            <div className="login-form-box">
44
+              <div className="login-form">
45
+                <h2 className="login-text">登录系统</h2>
46
+                <div className="login-subtitle">{title}</div>
47
+                <LoginForm onSuccess={onSuccess} />
48
+              </div>
49
+            </div>
50
+            <div className="login-copyright">
51
+              {copyright}
52
+            </div>
53
+          </div>
54
+          <div className="login-card-right">
55
+            <LoginEffect />
56
+          </div>
57
+        </div>
58
+      </div>
59
+    </div>
60
+  );
61
+};

+ 77
- 0
src/pages/login/qrcode.less Voir le fichier

@@ -0,0 +1,77 @@
1
+ .pageQr{
2
+  position: relative;
3
+  width: 180px;
4
+  height: 180px;
5
+  margin: 0 auto;
6
+  // top: 50%;
7
+  .loginPage,.loginSvg{
8
+    display: table;
9
+      position: absolute;
10
+      top: 100%;
11
+      left: 40%;
12
+      text-align: center;
13
+      width: 180px;
14
+      height: 180px;
15
+      margin-left: -61px;
16
+      margin-top: -132px;
17
+      background-color: #fffffff5;
18
+      .loginLap{
19
+        display: table-cell;
20
+    vertical-align: middle;
21
+      }
22
+
23
+  }
24
+
25
+  .loginSvg{
26
+.ScanningCode{
27
+  display: block;
28
+  // cursor: pointer;
29
+}
30
+
31
+  }
32
+  .newCode{
33
+    position: absolute;
34
+    top: 100%;
35
+    right: 37%;
36
+    cursor: pointer;
37
+    color: #576b95;
38
+    text-decoration: none;
39
+  }
40
+  .qrcode{
41
+    width: 178px;
42
+    height: 150px;
43
+    margin: auto;
44
+    position: absolute;
45
+    top: 30%;
46
+    span{
47
+      display: block;
48
+      text-align: center; 
49
+      padding-top:10px ;
50
+    }
51
+    .qrBoder{
52
+      box-sizing: border-box;
53
+    border: 1px solid #e2e2e2;
54
+    width: 180px;
55
+    height: 180px;
56
+    text-align: -webkit-center;
57
+    img{
58
+      position: absolute;
59
+    top: 10%;
60
+    right: 6%;
61
+    }
62
+    }
63
+  }
64
+ }
65
+
66
+ .pageUse{
67
+  display: flex;
68
+  justify-content: flex-end;
69
+  cursor: pointer;
70
+  color: #576b95;
71
+  
72
+  position: absolute;
73
+  top: 0;
74
+  right: 0;
75
+  height: 33px;
76
+  line-height: 33px;
77
+}

+ 99
- 0
src/pages/login/style.less Voir le fichier

@@ -0,0 +1,99 @@
1
+.login-page {
2
+  height: 100%;
3
+  background: linear-gradient(#fff, #e6f7ff);
4
+
5
+  #particles-js {
6
+    position: absolute;
7
+    top: 0;
8
+    left: 0;
9
+    width: 100%;
10
+    height: 100%;
11
+  }
12
+
13
+  .login-page-container {
14
+    height: 100%;
15
+    display: grid;
16
+    place-items: center;
17
+    position: relative;
18
+    z-index: 10;
19
+  }
20
+
21
+  .login-card {
22
+    width: 900px;
23
+    height: 600px;
24
+    border-radius: 8px;
25
+    overflow: hidden;
26
+    backdrop-filter: blur(6px);
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.45);
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
+      .login-text {
48
+        font-weight: 700;
49
+      }
50
+    }
51
+
52
+    .login-subtitle {
53
+      font-size: 0.9em;
54
+      color: #666;
55
+      margin-bottom: 24px;
56
+      letter-spacing: 2px;
57
+    }
58
+  }
59
+
60
+  .login-effect-box {
61
+    height: 100%;
62
+    display: grid;
63
+    place-items: center;
64
+
65
+    .login-effect {
66
+      width: 180px;
67
+      height: 180px;
68
+      position: relative;
69
+    }
70
+
71
+    .login-effect-arc {
72
+      width: 100%;
73
+      height: 100%;
74
+      border-radius: 50%;
75
+      background-color: #fa8c16;
76
+
77
+      &::after {
78
+        content: '';
79
+        width: 200%;
80
+        height: 100%;
81
+        top: 50%;
82
+        left: -50%;
83
+        position: absolute;
84
+        z-index: 10;
85
+        background-color: rgba(255, 255, 255, 0.001);
86
+        backdrop-filter: blur(16px);
87
+        border-top: 1px solid rgba(255, 255, 255, 0.2);
88
+      }
89
+    }
90
+  }
91
+
92
+  .login-copyright {
93
+    font-size: 12px;
94
+    color: #666;
95
+    line-height: 40px;
96
+    padding-left: 20px;
97
+    box-sizing: border-box;
98
+  }
99
+}

+ 6
- 0
src/pages/menu/index.jsx Voir le fichier

@@ -0,0 +1,6 @@
1
+import React from 'react'
2
+export default (props) => {
3
+return (
4
+<div>11111</div>
5
+)
6
+}

+ 120
- 0
src/pages/tenant/Edit.jsx Voir le fichier

@@ -0,0 +1,120 @@
1
+import React, { useEffect, useState } from "react";
2
+import { postFxTenant, putFxTenant, getFxTenantId } from "@/services/fxtenant";
3
+import { useNavigate, useSearchParams } from "react-router-dom";
4
+import {
5
+  ProForm,
6
+  ProFormText,
7
+  ProFormDateRangePicker,
8
+  ProFormSelect,
9
+} from "@ant-design/pro-components";
10
+import { Card, Form, Row, Col, Button, DatePicker, Select } from "antd";
11
+import Page from "@/components/Page";
12
+import { formItemLayout, tailFormItemLayout } from "@/utils/form";
13
+import moment from "moment";
14
+
15
+const dateFormat = "YYYY-MM-DD";
16
+
17
+export default (props) => {
18
+  const formRef = React.useRef();
19
+  const navigate = useNavigate();
20
+  const [searchParams] = useSearchParams();
21
+  const id = searchParams.get("id");
22
+  useEffect(() => {
23
+    formRef.current?.resetFields();
24
+    if (id) {
25
+      getFxTenantId(id).then((res) => {
26
+        formRef.current?.setFieldsValue({
27
+          ...res,
28
+          time: [
29
+            moment(res.startTime, dateFormat),
30
+            moment(res.endTime, dateFormat),
31
+          ],
32
+
33
+        });
34
+      });
35
+    }
36
+  }, []);
37
+
38
+  const onFinish = (x) => {
39
+    formRef.current?.validateFieldsReturnFormatValue?.().then((values) => {
40
+      console.log("values", values);
41
+      const data = {
42
+        ...values,
43
+        startTime: values?.time[0],
44
+        endTime: values?.time[1],
45
+      };
46
+      const tenantId = id;
47
+      if (!tenantId) {
48
+        postFxTenant(data).then((res) => {
49
+          navigate(-1);
50
+        });
51
+      } else {
52
+        putFxTenant(tenantId, { tenantId, ...data }).then((res) => {
53
+          navigate(-1);
54
+        });
55
+      }
56
+    });
57
+  };
58
+
59
+  return (
60
+    <Page>
61
+      <Card>
62
+        <ProForm
63
+          formRef={formRef}
64
+          onFinish={onFinish}
65
+          layout="horizontal"
66
+          {...formItemLayout}
67
+          submitter={{
68
+            render: (_, doms) => [
69
+              <Row>
70
+                <Col offset={8}>
71
+                  <Button type="primary" htmlType="submit">
72
+                    提交
73
+                  </Button>
74
+                </Col>
75
+                <Col offset={1}>
76
+                  <Button onClick={() => navigate(-1)}>返回</Button>
77
+                </Col>
78
+              </Row>,
79
+            ],
80
+          }}
81
+        >
82
+          <ProFormText
83
+            name="tenantName"
84
+            label="租户名称"
85
+            placeholder="请输入租户名称"
86
+            rules={[{ required: true, message: "租户名称不能为空" }]}
87
+          />
88
+          <Form.Item
89
+            name={["time"]}
90
+            label="合同生效时间"
91
+            rules={[{ required: true, message: "合同生效时间不能为空" }]}
92
+          >
93
+            <DatePicker.RangePicker />
94
+          </Form.Item>
95
+
96
+          <ProFormText
97
+            name="manager"
98
+            label="管理员名称"
99
+            placeholder="请输入管理员名称"
100
+          />
101
+          <ProFormText
102
+            name="phone"
103
+            label="手机号(租户账号)"
104
+            placeholder="请输入手机号"
105
+            rules={[{ required: true, message: "手机号不能为空" }]}
106
+          />
107
+          <ProFormSelect
108
+            label="状态"
109
+            name="status"
110
+            options={[
111
+              { label: '未激活', value: 0 },
112
+              { label: '已激活', value: 1 },
113
+              { label: '停用', value: 2 }
114
+            ]}
115
+          />
116
+        </ProForm>
117
+      </Card>
118
+    </Page>
119
+  );
120
+};

+ 109
- 0
src/pages/tenant/index.jsx Voir le fichier

@@ -0,0 +1,109 @@
1
+import List from "@/components/Page/List";
2
+import React from "react";
3
+
4
+import { getFxTenant, deleteFxTenant } from "@/services/fxtenant";
5
+
6
+import { useNavigate } from "react-router-dom";
7
+import { Button, Typography } from "antd";
8
+import moment from "moment";
9
+export default (props) => {
10
+  const { Text } = Typography;
11
+  const navigate = useNavigate();
12
+  const actionRef = React.useRef();
13
+
14
+  const columns = [
15
+    {
16
+      title: "时间",
17
+      dataIndex: "time",
18
+      valueType: "dateRange",
19
+      hideInTable: true,
20
+      search: {
21
+        transform: (value) => {
22
+          return {
23
+            startTime: value[0],
24
+            endTime: value[1],
25
+          };
26
+        },
27
+      },
28
+    },
29
+    {
30
+      title: "租户名称",
31
+      dataIndex: "tenantName",
32
+    },
33
+    {
34
+      title: "开始时间",
35
+      dataIndex: "startTime",
36
+      render: (t) => moment(t).format("YYYY-MM-DD"),
37
+      search: false,
38
+    },
39
+    {
40
+      title: "结束时间",
41
+      dataIndex: "endTime",
42
+      render: (t) => moment(t).format("YYYY-MM-DD"),
43
+      search: false,
44
+    },
45
+    {
46
+      title: "管理人名称",
47
+      dataIndex: "manager",
48
+      search: false,
49
+    },
50
+    {
51
+      title: "手机号",
52
+      dataIndex: "phone",
53
+    },
54
+    {
55
+      title: "登录地址",
56
+      dataIndex: "tenantId",
57
+      render: (_, record) => {
58
+        const url = `${import.meta.env.VITE_LOGIN_URL}#/${record.tenantId}/login`
59
+        console.log(url)
60
+        return <Text copyable={{ text: url }} ellipsis={true}>{url}</Text>;
61
+      },
62
+      search: false,
63
+    },
64
+    {
65
+      title: "状态",
66
+      dataIndex: "status",
67
+      valueType: "select",
68
+      render: (_, record) =>
69
+        record.status == 0 ? "未激活" : record.status == 1 ? "已激活" : "停用",
70
+      valueEnum: {
71
+        0: "未激活",
72
+        1: "已激活",
73
+        2: "停用",
74
+      },
75
+    }
76
+  ];
77
+
78
+  const DeleteFxUser = (record) => {
79
+    deleteFxTenant(record.tenantId).then((res) => {
80
+      actionRef.current.reload();
81
+    });
82
+  };
83
+
84
+
85
+  return (
86
+    <List
87
+      rowKey="tenantId"
88
+      actionRef={actionRef}
89
+      request={getFxTenant}
90
+      onEdit={(record) => navigate(`/tenant/edit?id=${record.tenantId}`)}
91
+      onDelete={DeleteFxUser}
92
+      toolBarRender={() => [
93
+        <Button
94
+          key="1"
95
+          type="primary"
96
+          onClick={() => {
97
+            navigate("edit");
98
+          }}
99
+        >
100
+          新增
101
+        </Button>,
102
+      ]}
103
+      columnOptionRender={(_, record) => [
104
+        <a href={`${import.meta.env.VITE_LOGIN_URL}#/${record.tenantId}/login`} target='_blank'>登录</a>
105
+      ]}
106
+      columns={columns}
107
+    />
108
+  );
109
+};

+ 18
- 0
src/routes/Router.jsx Voir le fichier

@@ -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, mergeAuthRoutes } from "./routes";
5
+
6
+export default (props) => {
7
+  const { routes } = useModel("user");
8
+  console.log(routes)
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
+};

+ 37
- 0
src/routes/hooks/usePrompt.jsx Voir le fichier

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

+ 17
- 0
src/routes/hooks/useRoute.jsx Voir le fichier

@@ -0,0 +1,17 @@
1
+import { useLocation } from "react-router-dom";
2
+import { routes } from '../routes';
3
+import { flatten } from "../utils";
4
+
5
+let routeMap = null;
6
+
7
+// 获取当前的 route 信息
8
+export default function useRoute() {
9
+  if (!routeMap) {
10
+    routeMap = flatten(routes);
11
+  }
12
+
13
+  const location = useLocation();
14
+  return routeMap[location.pathname];
15
+  // const currentRoute = routeArr.filter(x => x.path === location.pathname)[0];
16
+  // return currentRoute;
17
+}

+ 44
- 0
src/routes/menus.jsx Voir le fichier

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

+ 13
- 0
src/routes/permissions.js Voir le fichier

@@ -0,0 +1,13 @@
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
+    return route;
12
+  }).filter(Boolean);
13
+}

+ 0
- 0
src/routes/routes.jsx Voir le fichier


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff