lisenzhou 2 年 前
コミット
fe316d2107
共有67 個のファイルを変更した4342 個の追加0 個の削除を含む
  1. 16
    0
      .editorconfig
  2. 8
    0
      .eslintignore
  3. 8
    0
      .eslintrc.js
  4. 40
    0
      .gitignore
  5. 23
    0
      .prettierignore
  6. 5
    0
      .prettierrc.js
  7. 57
    0
      README.md
  8. 62
    0
      config/config.js
  9. 21
    0
      config/defaultSettings.ts
  10. 593
    0
      config/oneapi.json
  11. 34
    0
      config/proxy.js
  12. 61
    0
      config/routes.js
  13. 9
    0
      jest.config.js
  14. 11
    0
      jsconfig.json
  15. 174
    0
      mock/listTableList.ts
  16. 107
    0
      mock/notices.ts
  17. 5
    0
      mock/route.ts
  18. 203
    0
      mock/user.ts
  19. 94
    0
      package.json
  20. 22
    0
      playwright.config.ts
  21. 1
    0
      public/CNAME
  22. バイナリ
      public/favicon.ico
  23. バイナリ
      public/icons/icon-128x128.png
  24. バイナリ
      public/icons/icon-192x192.png
  25. バイナリ
      public/icons/icon-512x512.png
  26. 1
    0
      public/logo.svg
  27. 5
    0
      public/pro_icon.svg
  28. 9
    0
      src/access.ts
  29. 97
    0
      src/app.jsx
  30. 38
    0
      src/components/Footer/index.jsx
  31. 16
    0
      src/components/HeaderDropdown/index.jsx
  32. 16
    0
      src/components/HeaderDropdown/index.less
  33. 105
    0
      src/components/HeaderSearch/index.jsx
  34. 25
    0
      src/components/HeaderSearch/index.less
  35. 126
    0
      src/components/NoticeIcon/NoticeIcon.tsx
  36. 103
    0
      src/components/NoticeIcon/NoticeList.less
  37. 112
    0
      src/components/NoticeIcon/NoticeList.tsx
  38. 152
    0
      src/components/NoticeIcon/index.jsx
  39. 35
    0
      src/components/NoticeIcon/index.less
  40. 112
    0
      src/components/RightContent/AvatarDropdown.jsx
  41. 27
    0
      src/components/RightContent/index.jsx
  42. 73
    0
      src/components/RightContent/index.less
  43. 66
    0
      src/components/UploadImage/index.jsx
  44. 53
    0
      src/components/UploadVideo/index.jsx
  45. 267
    0
      src/components/index.md
  46. 45
    0
      src/e2e/baseLayout.e2e.spec.ts
  47. 91
    0
      src/global.jsx
  48. 50
    0
      src/global.less
  49. 22
    0
      src/manifest.json
  50. 18
    0
      src/pages/404.jsx
  51. 45
    0
      src/pages/Admin.jsx
  52. 118
    0
      src/pages/User/Login/index.jsx
  53. 48
    0
      src/pages/User/Login/index.less
  54. 17
    0
      src/pages/Welcome.jsx
  55. 8
    0
      src/pages/Welcome.less
  56. 236
    0
      src/pages/document.ejs
  57. 161
    0
      src/pages/rotationChart/edit/index.jsx
  58. 153
    0
      src/pages/rotationChart/list/index.jsx
  59. 65
    0
      src/service-worker.js
  60. 30
    0
      src/services/api/login.js
  61. 37
    0
      src/services/api/rotationChart.js
  62. 24
    0
      src/typings.d.ts
  63. 16
    0
      src/utils/download.js
  64. 68
    0
      src/utils/request.js
  65. 47
    0
      tests/run-tests.js
  66. 10
    0
      tests/setupTests.js
  67. 41
    0
      tsconfig.json

+ 16
- 0
.editorconfig ファイルの表示

@@ -0,0 +1,16 @@
1
+# http://editorconfig.org
2
+root = true
3
+
4
+[*]
5
+indent_style = space
6
+indent_size = 2
7
+end_of_line = lf
8
+charset = utf-8
9
+trim_trailing_whitespace = true
10
+insert_final_newline = true
11
+
12
+[*.md]
13
+trim_trailing_whitespace = false
14
+
15
+[Makefile]
16
+indent_style = tab

+ 8
- 0
.eslintignore ファイルの表示

@@ -0,0 +1,8 @@
1
+/lambda/
2
+/scripts
3
+/config
4
+.history
5
+public
6
+dist
7
+.umi
8
+mock

+ 8
- 0
.eslintrc.js ファイルの表示

@@ -0,0 +1,8 @@
1
+module.exports = {
2
+  extends: [require.resolve('@umijs/fabric/dist/eslint')],
3
+  globals: {
4
+    ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
5
+    page: true,
6
+    REACT_APP_ENV: true,
7
+  },
8
+};

+ 40
- 0
.gitignore ファイルの表示

@@ -0,0 +1,40 @@
1
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+# dependencies
4
+**/node_modules
5
+# roadhog-api-doc ignore
6
+/src/utils/request-temp.js
7
+_roadhog-api-doc
8
+
9
+# production
10
+/dist
11
+
12
+# misc
13
+.DS_Store
14
+npm-debug.log*
15
+yarn-error.log
16
+
17
+/coverage
18
+.idea
19
+yarn.lock
20
+package-lock.json
21
+pnpm-lock.yaml
22
+*bak
23
+
24
+
25
+# visual studio code
26
+.history
27
+*.log
28
+functions/*
29
+.temp/**
30
+
31
+# umi
32
+.umi
33
+.umi-production
34
+
35
+# screenshot
36
+screenshot
37
+.firebase
38
+.eslintcache
39
+
40
+build

+ 23
- 0
.prettierignore ファイルの表示

@@ -0,0 +1,23 @@
1
+**/*.svg
2
+package.json
3
+.umi
4
+.umi-production
5
+/dist
6
+.dockerignore
7
+.DS_Store
8
+.eslintignore
9
+*.png
10
+*.toml
11
+docker
12
+.editorconfig
13
+Dockerfile*
14
+.gitignore
15
+.prettierignore
16
+LICENSE
17
+.eslintcache
18
+*.lock
19
+yarn-error.log
20
+.history
21
+CNAME
22
+/build
23
+/public

+ 5
- 0
.prettierrc.js ファイルの表示

@@ -0,0 +1,5 @@
1
+const fabric = require('@umijs/fabric');
2
+
3
+module.exports = {
4
+  ...fabric.prettier,
5
+};

+ 57
- 0
README.md ファイルの表示

@@ -0,0 +1,57 @@
1
+# Ant Design Pro
2
+
3
+This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use.
4
+
5
+## Environment Prepare
6
+
7
+Install `node_modules`:
8
+
9
+```bash
10
+npm install
11
+```
12
+
13
+or
14
+
15
+```bash
16
+yarn
17
+```
18
+
19
+## Provided Scripts
20
+
21
+Ant Design Pro provides some useful script to help you quick start and build with web project, code style check and test.
22
+
23
+Scripts provided in `package.json`. It's safe to modify or add additional script:
24
+
25
+### Start project
26
+
27
+```bash
28
+npm start
29
+```
30
+
31
+### Build project
32
+
33
+```bash
34
+npm run build
35
+```
36
+
37
+### Check code style
38
+
39
+```bash
40
+npm run lint
41
+```
42
+
43
+You can also use script to auto fix some lint error:
44
+
45
+```bash
46
+npm run lint:fix
47
+```
48
+
49
+### Test code
50
+
51
+```bash
52
+npm test
53
+```
54
+
55
+## More
56
+
57
+You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro).

+ 62
- 0
config/config.js ファイルの表示

@@ -0,0 +1,62 @@
1
+// https://umijs.org/config/
2
+import { defineConfig } from '@umijs/max';
3
+import { join } from 'path';
4
+import defaultSettings from './defaultSettings';
5
+import proxy from './proxy';
6
+import routes from './routes';
7
+
8
+const { REACT_APP_ENV } = process.env;
9
+
10
+export default defineConfig({
11
+  hash: true,
12
+  antd: {
13
+    dark: true,
14
+  },
15
+  request: {},
16
+  initialState: {},
17
+  model: {},
18
+  layout: {
19
+    // https://umijs.org/zh-CN/plugins/plugin-layout
20
+    locale: false,
21
+    siderWidth: 208,
22
+    ...defaultSettings,
23
+  },
24
+  // https://umijs.org/zh-CN/plugins/plugin-locale
25
+  locale: false,
26
+
27
+  targets: {
28
+    ie: 11,
29
+  },
30
+  // umi routes: https://umijs.org/docs/routing
31
+  routes,
32
+  access: {},
33
+  // Theme for antd: https://ant.design/docs/react/customize-theme-cn
34
+  theme: {
35
+    // 如果不想要 configProvide 动态设置主题需要把这个设置为 default
36
+    // 只有设置为 variable, 才能使用 configProvide 动态设置主色调
37
+    // https://ant.design/docs/react/customize-theme-variable-cn
38
+    'root-entry-name': 'variable',
39
+  },
40
+  ignoreMomentLocale: true,
41
+  proxy: proxy[REACT_APP_ENV || 'dev'],
42
+  manifest: {
43
+    basePath: '/',
44
+  },
45
+  // Fast Refresh 热更新
46
+  fastRefresh: true,
47
+  presets: ['umi-presets-pro'],
48
+  openAPI: [
49
+    {
50
+      requestLibPath: "import { request } from '@umijs/max'",
51
+      // 或者使用在线的版本
52
+      // schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json"
53
+      schemaPath: join(__dirname, 'oneapi.json'),
54
+      mock: false,
55
+    },
56
+    {
57
+      requestLibPath: "import { request } from '@umijs/max'",
58
+      schemaPath: 'https://gw.alipayobjects.com/os/antfincdn/CA1dOm%2631B/openapi.json',
59
+      projectName: 'swagger',
60
+    },
61
+  ],
62
+});

+ 21
- 0
config/defaultSettings.ts ファイルの表示

@@ -0,0 +1,21 @@
1
+import { Settings as LayoutSettings } from '@ant-design/pro-components';
2
+
3
+const Settings: LayoutSettings & {
4
+  pwa?: boolean;
5
+  logo?: string;
6
+} = {
7
+  navTheme: 'dark',
8
+  // 拂晓蓝
9
+  primaryColor: '#1890ff',
10
+  layout: 'side',
11
+  contentWidth: 'Fluid',
12
+  fixedHeader: false,
13
+  fixSiderbar: true,
14
+  colorWeak: false,
15
+  title: '军工站',
16
+  pwa: false,
17
+  logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
18
+  iconfontUrl: '',
19
+};
20
+
21
+export default Settings;

+ 593
- 0
config/oneapi.json ファイルの表示

@@ -0,0 +1,593 @@
1
+{
2
+  "openapi": "3.0.1",
3
+  "info": {
4
+    "title": "Ant Design Pro",
5
+    "version": "1.0.0"
6
+  },
7
+  "servers": [
8
+    {
9
+      "url": "http://localhost:8000/"
10
+    },
11
+    {
12
+      "url": "https://localhost:8000/"
13
+    }
14
+  ],
15
+  "paths": {
16
+    "/api/currentUser": {
17
+      "get": {
18
+        "tags": ["api"],
19
+        "description": "获取当前的用户",
20
+        "operationId": "currentUser",
21
+        "responses": {
22
+          "200": {
23
+            "description": "Success",
24
+            "content": {
25
+              "application/json": {
26
+                "schema": {
27
+                  "$ref": "#/components/schemas/CurrentUser"
28
+                }
29
+              }
30
+            }
31
+          },
32
+          "401": {
33
+            "description": "Error",
34
+            "content": {
35
+              "application/json": {
36
+                "schema": {
37
+                  "$ref": "#/components/schemas/ErrorResponse"
38
+                }
39
+              }
40
+            }
41
+          }
42
+        }
43
+      },
44
+      "x-swagger-router-controller": "api"
45
+    },
46
+    "/api/login/captcha": {
47
+      "post": {
48
+        "description": "发送验证码",
49
+        "operationId": "getFakeCaptcha",
50
+        "tags": ["login"],
51
+        "parameters": [
52
+          {
53
+            "name": "phone",
54
+            "in": "query",
55
+            "description": "手机号",
56
+            "schema": {
57
+              "type": "string"
58
+            }
59
+          }
60
+        ],
61
+        "responses": {
62
+          "200": {
63
+            "description": "Success",
64
+            "content": {
65
+              "application/json": {
66
+                "schema": {
67
+                  "$ref": "#/components/schemas/FakeCaptcha"
68
+                }
69
+              }
70
+            }
71
+          }
72
+        }
73
+      }
74
+    },
75
+    "/api/login/outLogin": {
76
+      "post": {
77
+        "description": "登录接口",
78
+        "operationId": "outLogin",
79
+        "tags": ["login"],
80
+        "responses": {
81
+          "200": {
82
+            "description": "Success",
83
+            "content": {
84
+              "application/json": {
85
+                "schema": {
86
+                  "type": "object"
87
+                }
88
+              }
89
+            }
90
+          },
91
+          "401": {
92
+            "description": "Error",
93
+            "content": {
94
+              "application/json": {
95
+                "schema": {
96
+                  "$ref": "#/components/schemas/ErrorResponse"
97
+                }
98
+              }
99
+            }
100
+          }
101
+        }
102
+      },
103
+      "x-swagger-router-controller": "api"
104
+    },
105
+    "/api/login/account": {
106
+      "post": {
107
+        "tags": ["login"],
108
+        "description": "登录接口",
109
+        "operationId": "login",
110
+        "requestBody": {
111
+          "description": "登录系统",
112
+          "content": {
113
+            "application/json": {
114
+              "schema": {
115
+                "$ref": "#/components/schemas/LoginParams"
116
+              }
117
+            }
118
+          },
119
+          "required": true
120
+        },
121
+        "responses": {
122
+          "200": {
123
+            "description": "Success",
124
+            "content": {
125
+              "application/json": {
126
+                "schema": {
127
+                  "$ref": "#/components/schemas/LoginResult"
128
+                }
129
+              }
130
+            }
131
+          },
132
+          "401": {
133
+            "description": "Error",
134
+            "content": {
135
+              "application/json": {
136
+                "schema": {
137
+                  "$ref": "#/components/schemas/ErrorResponse"
138
+                }
139
+              }
140
+            }
141
+          }
142
+        },
143
+        "x-codegen-request-body-name": "body"
144
+      },
145
+      "x-swagger-router-controller": "api"
146
+    },
147
+    "/api/notices": {
148
+      "summary": "getNotices",
149
+      "description": "NoticeIconItem",
150
+      "get": {
151
+        "tags": ["api"],
152
+        "operationId": "getNotices",
153
+        "responses": {
154
+          "200": {
155
+            "description": "Success",
156
+            "content": {
157
+              "application/json": {
158
+                "schema": {
159
+                  "$ref": "#/components/schemas/NoticeIconList"
160
+                }
161
+              }
162
+            }
163
+          }
164
+        }
165
+      }
166
+    },
167
+    "/api/rule": {
168
+      "get": {
169
+        "tags": ["rule"],
170
+        "description": "获取规则列表",
171
+        "operationId": "rule",
172
+        "parameters": [
173
+          {
174
+            "name": "current",
175
+            "in": "query",
176
+            "description": "当前的页码",
177
+            "schema": {
178
+              "type": "number"
179
+            }
180
+          },
181
+          {
182
+            "name": "pageSize",
183
+            "in": "query",
184
+            "description": "页面的容量",
185
+            "schema": {
186
+              "type": "number"
187
+            }
188
+          }
189
+        ],
190
+        "responses": {
191
+          "200": {
192
+            "description": "Success",
193
+            "content": {
194
+              "application/json": {
195
+                "schema": {
196
+                  "$ref": "#/components/schemas/RuleList"
197
+                }
198
+              }
199
+            }
200
+          },
201
+          "401": {
202
+            "description": "Error",
203
+            "content": {
204
+              "application/json": {
205
+                "schema": {
206
+                  "$ref": "#/components/schemas/ErrorResponse"
207
+                }
208
+              }
209
+            }
210
+          }
211
+        }
212
+      },
213
+      "post": {
214
+        "tags": ["rule"],
215
+        "description": "新建规则",
216
+        "operationId": "addRule",
217
+        "responses": {
218
+          "200": {
219
+            "description": "Success",
220
+            "content": {
221
+              "application/json": {
222
+                "schema": {
223
+                  "$ref": "#/components/schemas/RuleListItem"
224
+                }
225
+              }
226
+            }
227
+          },
228
+          "401": {
229
+            "description": "Error",
230
+            "content": {
231
+              "application/json": {
232
+                "schema": {
233
+                  "$ref": "#/components/schemas/ErrorResponse"
234
+                }
235
+              }
236
+            }
237
+          }
238
+        }
239
+      },
240
+      "put": {
241
+        "tags": ["rule"],
242
+        "description": "新建规则",
243
+        "operationId": "updateRule",
244
+        "responses": {
245
+          "200": {
246
+            "description": "Success",
247
+            "content": {
248
+              "application/json": {
249
+                "schema": {
250
+                  "$ref": "#/components/schemas/RuleListItem"
251
+                }
252
+              }
253
+            }
254
+          },
255
+          "401": {
256
+            "description": "Error",
257
+            "content": {
258
+              "application/json": {
259
+                "schema": {
260
+                  "$ref": "#/components/schemas/ErrorResponse"
261
+                }
262
+              }
263
+            }
264
+          }
265
+        }
266
+      },
267
+      "delete": {
268
+        "tags": ["rule"],
269
+        "description": "删除规则",
270
+        "operationId": "removeRule",
271
+        "responses": {
272
+          "200": {
273
+            "description": "Success",
274
+            "content": {
275
+              "application/json": {
276
+                "schema": {
277
+                  "type": "object"
278
+                }
279
+              }
280
+            }
281
+          },
282
+          "401": {
283
+            "description": "Error",
284
+            "content": {
285
+              "application/json": {
286
+                "schema": {
287
+                  "$ref": "#/components/schemas/ErrorResponse"
288
+                }
289
+              }
290
+            }
291
+          }
292
+        }
293
+      },
294
+      "x-swagger-router-controller": "api"
295
+    },
296
+    "/swagger": {
297
+      "x-swagger-pipe": "swagger_raw"
298
+    }
299
+  },
300
+  "components": {
301
+    "schemas": {
302
+      "CurrentUser": {
303
+        "type": "object",
304
+        "properties": {
305
+          "name": {
306
+            "type": "string"
307
+          },
308
+          "avatar": {
309
+            "type": "string"
310
+          },
311
+          "userid": {
312
+            "type": "string"
313
+          },
314
+          "email": {
315
+            "type": "string"
316
+          },
317
+          "signature": {
318
+            "type": "string"
319
+          },
320
+          "title": {
321
+            "type": "string"
322
+          },
323
+          "group": {
324
+            "type": "string"
325
+          },
326
+          "tags": {
327
+            "type": "array",
328
+            "items": {
329
+              "type": "object",
330
+              "properties": {
331
+                "key": {
332
+                  "type": "string"
333
+                },
334
+                "label": {
335
+                  "type": "string"
336
+                }
337
+              }
338
+            }
339
+          },
340
+          "notifyCount": {
341
+            "type": "integer",
342
+            "format": "int32"
343
+          },
344
+          "unreadCount": {
345
+            "type": "integer",
346
+            "format": "int32"
347
+          },
348
+          "country": {
349
+            "type": "string"
350
+          },
351
+          "access": {
352
+            "type": "string"
353
+          },
354
+          "geographic": {
355
+            "type": "object",
356
+            "properties": {
357
+              "province": {
358
+                "type": "object",
359
+                "properties": {
360
+                  "label": {
361
+                    "type": "string"
362
+                  },
363
+                  "key": {
364
+                    "type": "string"
365
+                  }
366
+                }
367
+              },
368
+              "city": {
369
+                "type": "object",
370
+                "properties": {
371
+                  "label": {
372
+                    "type": "string"
373
+                  },
374
+                  "key": {
375
+                    "type": "string"
376
+                  }
377
+                }
378
+              }
379
+            }
380
+          },
381
+          "address": {
382
+            "type": "string"
383
+          },
384
+          "phone": {
385
+            "type": "string"
386
+          }
387
+        }
388
+      },
389
+      "LoginResult": {
390
+        "type": "object",
391
+        "properties": {
392
+          "status": {
393
+            "type": "string"
394
+          },
395
+          "type": {
396
+            "type": "string"
397
+          },
398
+          "currentAuthority": {
399
+            "type": "string"
400
+          }
401
+        }
402
+      },
403
+      "PageParams": {
404
+        "type": "object",
405
+        "properties": {
406
+          "current": {
407
+            "type": "number"
408
+          },
409
+          "pageSize": {
410
+            "type": "number"
411
+          }
412
+        }
413
+      },
414
+      "RuleListItem": {
415
+        "type": "object",
416
+        "properties": {
417
+          "key": {
418
+            "type": "integer",
419
+            "format": "int32"
420
+          },
421
+          "disabled": {
422
+            "type": "boolean"
423
+          },
424
+          "href": {
425
+            "type": "string"
426
+          },
427
+          "avatar": {
428
+            "type": "string"
429
+          },
430
+          "name": {
431
+            "type": "string"
432
+          },
433
+          "owner": {
434
+            "type": "string"
435
+          },
436
+          "desc": {
437
+            "type": "string"
438
+          },
439
+          "callNo": {
440
+            "type": "integer",
441
+            "format": "int32"
442
+          },
443
+          "status": {
444
+            "type": "integer",
445
+            "format": "int32"
446
+          },
447
+          "updatedAt": {
448
+            "type": "string",
449
+            "format": "datetime"
450
+          },
451
+          "createdAt": {
452
+            "type": "string",
453
+            "format": "datetime"
454
+          },
455
+          "progress": {
456
+            "type": "integer",
457
+            "format": "int32"
458
+          }
459
+        }
460
+      },
461
+      "RuleList": {
462
+        "type": "object",
463
+        "properties": {
464
+          "data": {
465
+            "type": "array",
466
+            "items": {
467
+              "$ref": "#/components/schemas/RuleListItem"
468
+            }
469
+          },
470
+          "total": {
471
+            "type": "integer",
472
+            "description": "列表的内容总数",
473
+            "format": "int32"
474
+          },
475
+          "success": {
476
+            "type": "boolean"
477
+          }
478
+        }
479
+      },
480
+      "FakeCaptcha": {
481
+        "type": "object",
482
+        "properties": {
483
+          "code": {
484
+            "type": "integer",
485
+            "format": "int32"
486
+          },
487
+          "status": {
488
+            "type": "string"
489
+          }
490
+        }
491
+      },
492
+      "LoginParams": {
493
+        "type": "object",
494
+        "properties": {
495
+          "username": {
496
+            "type": "string"
497
+          },
498
+          "password": {
499
+            "type": "string"
500
+          },
501
+          "autoLogin": {
502
+            "type": "boolean"
503
+          },
504
+          "type": {
505
+            "type": "string"
506
+          }
507
+        }
508
+      },
509
+      "ErrorResponse": {
510
+        "required": ["errorCode"],
511
+        "type": "object",
512
+        "properties": {
513
+          "errorCode": {
514
+            "type": "string",
515
+            "description": "业务约定的错误码"
516
+          },
517
+          "errorMessage": {
518
+            "type": "string",
519
+            "description": "业务上的错误信息"
520
+          },
521
+          "success": {
522
+            "type": "boolean",
523
+            "description": "业务上的请求是否成功"
524
+          }
525
+        }
526
+      },
527
+      "NoticeIconList": {
528
+        "type": "object",
529
+        "properties": {
530
+          "data": {
531
+            "type": "array",
532
+            "items": {
533
+              "$ref": "#/components/schemas/NoticeIconItem"
534
+            }
535
+          },
536
+          "total": {
537
+            "type": "integer",
538
+            "description": "列表的内容总数",
539
+            "format": "int32"
540
+          },
541
+          "success": {
542
+            "type": "boolean"
543
+          }
544
+        }
545
+      },
546
+      "NoticeIconItemType": {
547
+        "title": "NoticeIconItemType",
548
+        "description": "已读未读列表的枚举",
549
+        "type": "string",
550
+        "properties": {},
551
+        "enum": ["notification", "message", "event"]
552
+      },
553
+      "NoticeIconItem": {
554
+        "type": "object",
555
+        "properties": {
556
+          "id": {
557
+            "type": "string"
558
+          },
559
+          "extra": {
560
+            "type": "string",
561
+            "format": "any"
562
+          },
563
+          "key": { "type": "string" },
564
+          "read": {
565
+            "type": "boolean"
566
+          },
567
+          "avatar": {
568
+            "type": "string"
569
+          },
570
+          "title": {
571
+            "type": "string"
572
+          },
573
+          "status": {
574
+            "type": "string"
575
+          },
576
+          "datetime": {
577
+            "type": "string",
578
+            "format": "date"
579
+          },
580
+          "description": {
581
+            "type": "string"
582
+          },
583
+          "type": {
584
+            "extensions": {
585
+              "x-is-enum": true
586
+            },
587
+            "$ref": "#/components/schemas/NoticeIconItemType"
588
+          }
589
+        }
590
+      }
591
+    }
592
+  }
593
+}

+ 34
- 0
config/proxy.js ファイルの表示

@@ -0,0 +1,34 @@
1
+/**
2
+ * 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
3
+ * -------------------------------
4
+ * The agent cannot take effect in the production environment
5
+ * so there is no configuration of the production environment
6
+ * For details, please see
7
+ * https://pro.ant.design/docs/deploy
8
+ */
9
+export default {
10
+  dev: {
11
+    // localhost:8000/api/** -> https://preview.pro.ant.design/api/**
12
+    '/api/': {
13
+      // 要代理的地址
14
+      target: 'http://192.168.89.76:8087',
15
+      // 配置了这个可以从 http 代理到 https
16
+      // 依赖 origin 的功能可能需要这个,比如 cookie
17
+      changeOrigin: true,
18
+    },
19
+  },
20
+  test: {
21
+    '/api/': {
22
+      target: 'https://proapi.azurewebsites.net',
23
+      changeOrigin: true,
24
+      pathRewrite: { '^': '' },
25
+    },
26
+  },
27
+  pre: {
28
+    '/api/': {
29
+      target: 'your pre url',
30
+      changeOrigin: true,
31
+      pathRewrite: { '^': '' },
32
+    },
33
+  },
34
+};

+ 61
- 0
config/routes.js ファイルの表示

@@ -0,0 +1,61 @@
1
+export default [
2
+  // {
3
+  //   path: '/user',
4
+  //   layout: false,
5
+  //   routes: [
6
+  //     {
7
+  //       name: 'login',
8
+  //       path: '/user/login',
9
+  //       component: './User/Login',
10
+  //     },
11
+  //     {
12
+  //       component: './404',
13
+  //     },
14
+  //   ],
15
+  // },
16
+  // {
17
+  //   path: '/welcome',
18
+  //   name: 'welcome',
19
+  //   icon: 'smile',
20
+  //   component: './Welcome',
21
+  // },
22
+  {
23
+    path: '/rotationChart',
24
+    name: '轮播图管理',
25
+    icon: 'smile',
26
+    // access: 'canAdmin',
27
+    routes: [
28
+      {
29
+        path: '/rotationChart/list',
30
+        name: '轮播图管理',
31
+        icon: 'smile',
32
+        component: './rotationChart/list',
33
+      },
34
+      {
35
+        path: '/rotationChart/add',
36
+        name: '轮播图新增',
37
+        icon: 'smile',
38
+        hideInMenu: true,
39
+        component: './rotationChart/edit',
40
+      },
41
+      {
42
+        path: '/rotationChart/edit',
43
+        name: '轮播图编辑',
44
+        icon: 'smile',
45
+        hideInMenu: true,
46
+        component: './rotationChart/edit',
47
+      },
48
+      {
49
+        component: './404',
50
+      },
51
+    ],
52
+  },
53
+
54
+  {
55
+    path: '/',
56
+    redirect: '/rotationChart/list',
57
+  },
58
+  {
59
+    component: './404',
60
+  },
61
+];

+ 9
- 0
jest.config.js ファイルの表示

@@ -0,0 +1,9 @@
1
+module.exports = {
2
+  testURL: 'http://localhost:8000',
3
+  verbose: false,
4
+  extraSetupFiles: ['./tests/setupTests.js'],
5
+  globals: {
6
+    ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false,
7
+    localStorage: null,
8
+  },
9
+};

+ 11
- 0
jsconfig.json ファイルの表示

@@ -0,0 +1,11 @@
1
+{
2
+  "compilerOptions": {
3
+    "jsx": "react-jsx",
4
+    "emitDecoratorMetadata": true,
5
+    "experimentalDecorators": true,
6
+    "baseUrl": ".",
7
+    "paths": {
8
+      "@/*": ["./src/*"]
9
+    }
10
+  }
11
+}

+ 174
- 0
mock/listTableList.ts ファイルの表示

@@ -0,0 +1,174 @@
1
+import { Request, Response } from 'express';
2
+import moment from 'moment';
3
+import { parse } from 'url';
4
+
5
+// mock tableListDataSource
6
+const genList = (current: number, pageSize: number) => {
7
+  const tableListDataSource: API.RuleListItem[] = [];
8
+
9
+  for (let i = 0; i < pageSize; i += 1) {
10
+    const index = (current - 1) * 10 + i;
11
+    tableListDataSource.push({
12
+      key: index,
13
+      disabled: i % 6 === 0,
14
+      href: 'https://ant.design',
15
+      avatar: [
16
+        'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
17
+        'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
18
+      ][i % 2],
19
+      name: `TradeCode ${index}`,
20
+      owner: '曲丽丽',
21
+      desc: '这是一段描述',
22
+      callNo: Math.floor(Math.random() * 1000),
23
+      status: Math.floor(Math.random() * 10) % 4,
24
+      updatedAt: moment().format('YYYY-MM-DD'),
25
+      createdAt: moment().format('YYYY-MM-DD'),
26
+      progress: Math.ceil(Math.random() * 100),
27
+    });
28
+  }
29
+  tableListDataSource.reverse();
30
+  return tableListDataSource;
31
+};
32
+
33
+let tableListDataSource = genList(1, 100);
34
+
35
+function getRule(req: Request, res: Response, u: string) {
36
+  let realUrl = u;
37
+  if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
38
+    realUrl = req.url;
39
+  }
40
+  const { current = 1, pageSize = 10 } = req.query;
41
+  const params = parse(realUrl, true).query as unknown as API.PageParams &
42
+    API.RuleListItem & {
43
+      sorter: any;
44
+      filter: any;
45
+    };
46
+
47
+  let dataSource = [...tableListDataSource].slice(
48
+    ((current as number) - 1) * (pageSize as number),
49
+    (current as number) * (pageSize as number),
50
+  );
51
+  if (params.sorter) {
52
+    const sorter = JSON.parse(params.sorter);
53
+    dataSource = dataSource.sort((prev, next) => {
54
+      let sortNumber = 0;
55
+      Object.keys(sorter).forEach((key) => {
56
+        if (sorter[key] === 'descend') {
57
+          if (prev[key] - next[key] > 0) {
58
+            sortNumber += -1;
59
+          } else {
60
+            sortNumber += 1;
61
+          }
62
+          return;
63
+        }
64
+        if (prev[key] - next[key] > 0) {
65
+          sortNumber += 1;
66
+        } else {
67
+          sortNumber += -1;
68
+        }
69
+      });
70
+      return sortNumber;
71
+    });
72
+  }
73
+  if (params.filter) {
74
+    const filter = JSON.parse(params.filter as any) as {
75
+      [key: string]: string[];
76
+    };
77
+    if (Object.keys(filter).length > 0) {
78
+      dataSource = dataSource.filter((item) => {
79
+        return Object.keys(filter).some((key) => {
80
+          if (!filter[key]) {
81
+            return true;
82
+          }
83
+          if (filter[key].includes(`${item[key]}`)) {
84
+            return true;
85
+          }
86
+          return false;
87
+        });
88
+      });
89
+    }
90
+  }
91
+
92
+  if (params.name) {
93
+    dataSource = dataSource.filter((data) => data?.name?.includes(params.name || ''));
94
+  }
95
+  const result = {
96
+    data: dataSource,
97
+    total: tableListDataSource.length,
98
+    success: true,
99
+    pageSize,
100
+    current: parseInt(`${params.current}`, 10) || 1,
101
+  };
102
+
103
+  return res.json(result);
104
+}
105
+
106
+function postRule(req: Request, res: Response, u: string, b: Request) {
107
+  let realUrl = u;
108
+  if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
109
+    realUrl = req.url;
110
+  }
111
+
112
+  const body = (b && b.body) || req.body;
113
+  const { method, name, desc, key } = body;
114
+
115
+  switch (method) {
116
+    /* eslint no-case-declarations:0 */
117
+    case 'delete':
118
+      tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1);
119
+      break;
120
+    case 'post':
121
+      (() => {
122
+        const i = Math.ceil(Math.random() * 10000);
123
+        const newRule: API.RuleListItem = {
124
+          key: tableListDataSource.length,
125
+          href: 'https://ant.design',
126
+          avatar: [
127
+            'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
128
+            'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
129
+          ][i % 2],
130
+          name,
131
+          owner: '曲丽丽',
132
+          desc,
133
+          callNo: Math.floor(Math.random() * 1000),
134
+          status: Math.floor(Math.random() * 10) % 2,
135
+          updatedAt: moment().format('YYYY-MM-DD'),
136
+          createdAt: moment().format('YYYY-MM-DD'),
137
+          progress: Math.ceil(Math.random() * 100),
138
+        };
139
+        tableListDataSource.unshift(newRule);
140
+        return res.json(newRule);
141
+      })();
142
+      return;
143
+
144
+    case 'update':
145
+      (() => {
146
+        let newRule = {};
147
+        tableListDataSource = tableListDataSource.map((item) => {
148
+          if (item.key === key) {
149
+            newRule = { ...item, desc, name };
150
+            return { ...item, desc, name };
151
+          }
152
+          return item;
153
+        });
154
+        return res.json(newRule);
155
+      })();
156
+      return;
157
+    default:
158
+      break;
159
+  }
160
+
161
+  const result = {
162
+    list: tableListDataSource,
163
+    pagination: {
164
+      total: tableListDataSource.length,
165
+    },
166
+  };
167
+
168
+  res.json(result);
169
+}
170
+
171
+export default {
172
+  'GET /api/rule': getRule,
173
+  'POST /api/rule': postRule,
174
+};

+ 107
- 0
mock/notices.ts ファイルの表示

@@ -0,0 +1,107 @@
1
+import { Request, Response } from 'express';
2
+
3
+const getNotices = (req: Request, res: Response) => {
4
+  res.json({
5
+    data: [
6
+      {
7
+        id: '000000001',
8
+        avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/MSbDR4FR2MUAAAAAAAAAAAAAFl94AQBr',
9
+        title: '你收到了 14 份新周报',
10
+        datetime: '2017-08-09',
11
+        type: 'notification',
12
+      },
13
+      {
14
+        id: '000000002',
15
+        avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/hX-PTavYIq4AAAAAAAAAAAAAFl94AQBr',
16
+        title: '你推荐的 曲妮妮 已通过第三轮面试',
17
+        datetime: '2017-08-08',
18
+        type: 'notification',
19
+      },
20
+      {
21
+        id: '000000003',
22
+        avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/jHX5R5l3QjQAAAAAAAAAAAAAFl94AQBr',
23
+        title: '这种模板可以区分多种通知类型',
24
+        datetime: '2017-08-07',
25
+        read: true,
26
+        type: 'notification',
27
+      },
28
+      {
29
+        id: '000000004',
30
+        avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/Wr4mQqx6jfwAAAAAAAAAAAAAFl94AQBr',
31
+        title: '左侧图标用于区分不同的类型',
32
+        datetime: '2017-08-07',
33
+        type: 'notification',
34
+      },
35
+      {
36
+        id: '000000005',
37
+        avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/Mzj_TbcWUj4AAAAAAAAAAAAAFl94AQBr',
38
+        title: '内容不要超过两行字,超出时自动截断',
39
+        datetime: '2017-08-07',
40
+        type: 'notification',
41
+      },
42
+      {
43
+        id: '000000006',
44
+        avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/eXLzRbPqQE4AAAAAAAAAAAAAFl94AQBr',
45
+        title: '曲丽丽 评论了你',
46
+        description: '描述信息描述信息描述信息',
47
+        datetime: '2017-08-07',
48
+        type: 'message',
49
+        clickClose: true,
50
+      },
51
+      {
52
+        id: '000000007',
53
+        avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/w5mRQY2AmEEAAAAAAAAAAAAAFl94AQBr',
54
+        title: '朱偏右 回复了你',
55
+        description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
56
+        datetime: '2017-08-07',
57
+        type: 'message',
58
+        clickClose: true,
59
+      },
60
+      {
61
+        id: '000000008',
62
+        avatar: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/wPadR5M9918AAAAAAAAAAAAAFl94AQBr',
63
+        title: '标题',
64
+        description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
65
+        datetime: '2017-08-07',
66
+        type: 'message',
67
+        clickClose: true,
68
+      },
69
+      {
70
+        id: '000000009',
71
+        title: '任务名称',
72
+        description: '任务需要在 2017-01-12 20:00 前启动',
73
+        extra: '未开始',
74
+        status: 'todo',
75
+        type: 'event',
76
+      },
77
+      {
78
+        id: '000000010',
79
+        title: '第三方紧急代码变更',
80
+        description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
81
+        extra: '马上到期',
82
+        status: 'urgent',
83
+        type: 'event',
84
+      },
85
+      {
86
+        id: '000000011',
87
+        title: '信息安全考试',
88
+        description: '指派竹尔于 2017-01-09 前完成更新并发布',
89
+        extra: '已耗时 8 天',
90
+        status: 'doing',
91
+        type: 'event',
92
+      },
93
+      {
94
+        id: '000000012',
95
+        title: 'ABCD 版本发布',
96
+        description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
97
+        extra: '进行中',
98
+        status: 'processing',
99
+        type: 'event',
100
+      },
101
+    ],
102
+  });
103
+};
104
+
105
+export default {
106
+  'GET /api/notices': getNotices,
107
+};

+ 5
- 0
mock/route.ts ファイルの表示

@@ -0,0 +1,5 @@
1
+export default {
2
+  '/api/auth_routes': {
3
+    '/form/advanced-form': { authority: ['admin', 'user'] },
4
+  },
5
+};

+ 203
- 0
mock/user.ts ファイルの表示

@@ -0,0 +1,203 @@
1
+import { Request, Response } from 'express';
2
+
3
+const waitTime = (time: number = 100) => {
4
+  return new Promise((resolve) => {
5
+    setTimeout(() => {
6
+      resolve(true);
7
+    }, time);
8
+  });
9
+};
10
+
11
+async function getFakeCaptcha(req: Request, res: Response) {
12
+  await waitTime(2000);
13
+  return res.json('captcha-xxx');
14
+}
15
+
16
+const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env;
17
+
18
+/**
19
+ * 当前用户的权限,如果为空代表没登录
20
+ * current user access, if is '', user need login
21
+ * 如果是 pro 的预览,默认是有权限的
22
+ */
23
+let access = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ? 'admin' : '';
24
+
25
+const getAccess = () => {
26
+  return access;
27
+};
28
+
29
+// 代码中会兼容本地 service mock 以及部署站点的静态数据
30
+export default {
31
+  // 支持值为 Object 和 Array
32
+  'GET /api/currentUser': (req: Request, res: Response) => {
33
+    if (!getAccess()) {
34
+      res.status(401).send({
35
+        data: {
36
+          isLogin: false,
37
+        },
38
+        errorCode: '401',
39
+        errorMessage: '请先登录!',
40
+        success: true,
41
+      });
42
+      return;
43
+    }
44
+    res.send({
45
+      success: true,
46
+      data: {
47
+        name: 'Serati Ma',
48
+        avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
49
+        userid: '00000001',
50
+        email: 'antdesign@alipay.com',
51
+        signature: '海纳百川,有容乃大',
52
+        title: '交互专家',
53
+        group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
54
+        tags: [
55
+          {
56
+            key: '0',
57
+            label: '很有想法的',
58
+          },
59
+          {
60
+            key: '1',
61
+            label: '专注设计',
62
+          },
63
+          {
64
+            key: '2',
65
+            label: '辣~',
66
+          },
67
+          {
68
+            key: '3',
69
+            label: '大长腿',
70
+          },
71
+          {
72
+            key: '4',
73
+            label: '川妹子',
74
+          },
75
+          {
76
+            key: '5',
77
+            label: '海纳百川',
78
+          },
79
+        ],
80
+        notifyCount: 12,
81
+        unreadCount: 11,
82
+        country: 'China',
83
+        access: getAccess(),
84
+        geographic: {
85
+          province: {
86
+            label: '浙江省',
87
+            key: '330000',
88
+          },
89
+          city: {
90
+            label: '杭州市',
91
+            key: '330100',
92
+          },
93
+        },
94
+        address: '西湖区工专路 77 号',
95
+        phone: '0752-268888888',
96
+      },
97
+    });
98
+  },
99
+  // GET POST 可省略
100
+  'GET /api/users': [
101
+    {
102
+      key: '1',
103
+      name: 'John Brown',
104
+      age: 32,
105
+      address: 'New York No. 1 Lake Park',
106
+    },
107
+    {
108
+      key: '2',
109
+      name: 'Jim Green',
110
+      age: 42,
111
+      address: 'London No. 1 Lake Park',
112
+    },
113
+    {
114
+      key: '3',
115
+      name: 'Joe Black',
116
+      age: 32,
117
+      address: 'Sidney No. 1 Lake Park',
118
+    },
119
+  ],
120
+  'POST /api/login/account': async (req: Request, res: Response) => {
121
+    const { password, username, type } = req.body;
122
+    await waitTime(2000);
123
+    if (password === '1' && username === '1') {
124
+      res.send({
125
+        status: 'ok',
126
+        type,
127
+        currentAuthority: 'admin',
128
+      });
129
+      access = 'admin';
130
+      return;
131
+    }
132
+    if (password === 'ant.design' && username === 'user') {
133
+      res.send({
134
+        status: 'ok',
135
+        type,
136
+        currentAuthority: 'user',
137
+      });
138
+      access = 'user';
139
+      return;
140
+    }
141
+    if (type === 'mobile') {
142
+      res.send({
143
+        status: 'ok',
144
+        type,
145
+        currentAuthority: 'admin',
146
+      });
147
+      access = 'admin';
148
+      return;
149
+    }
150
+
151
+    res.send({
152
+      status: 'error',
153
+      type,
154
+      currentAuthority: 'guest',
155
+    });
156
+    access = 'guest';
157
+  },
158
+  'POST /api/login/outLogin': (req: Request, res: Response) => {
159
+    access = '';
160
+    res.send({ data: {}, success: true });
161
+  },
162
+  'POST /api/register': (req: Request, res: Response) => {
163
+    res.send({ status: 'ok', currentAuthority: 'user', success: true });
164
+  },
165
+  'GET /api/500': (req: Request, res: Response) => {
166
+    res.status(500).send({
167
+      timestamp: 1513932555104,
168
+      status: 500,
169
+      error: 'error',
170
+      message: 'error',
171
+      path: '/base/category/list',
172
+    });
173
+  },
174
+  'GET /api/404': (req: Request, res: Response) => {
175
+    res.status(404).send({
176
+      timestamp: 1513932643431,
177
+      status: 404,
178
+      error: 'Not Found',
179
+      message: 'No message available',
180
+      path: '/base/category/list/2121212',
181
+    });
182
+  },
183
+  'GET /api/403': (req: Request, res: Response) => {
184
+    res.status(403).send({
185
+      timestamp: 1513932555104,
186
+      status: 403,
187
+      error: 'Forbidden',
188
+      message: 'Forbidden',
189
+      path: '/base/category/list',
190
+    });
191
+  },
192
+  'GET /api/401': (req: Request, res: Response) => {
193
+    res.status(401).send({
194
+      timestamp: 1513932555104,
195
+      status: 401,
196
+      error: 'Unauthorized',
197
+      message: 'Unauthorized',
198
+      path: '/base/category/list',
199
+    });
200
+  },
201
+
202
+  'GET  /api/login/captcha': getFakeCaptcha,
203
+};

+ 94
- 0
package.json ファイルの表示

@@ -0,0 +1,94 @@
1
+{
2
+  "name": "ant-design-pro",
3
+  "version": "6.0.0-beta.1",
4
+  "private": true,
5
+  "description": "An out-of-box UI solution for enterprise applications",
6
+  "scripts": {
7
+    "analyze": "cross-env ANALYZE=1 max build",
8
+    "build": "max build",
9
+    "deploy": "npm run build && npm run gh-pages",
10
+    "dev": "npm run start:dev",
11
+    "gh-pages": "gh-pages -d dist",
12
+    "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
13
+    "postinstall": "max setup",
14
+    "lint": "npm run lint:js && npm run lint:prettier && npm run tsc",
15
+    "lint-staged": "lint-staged",
16
+    "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
17
+    "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src ",
18
+    "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
19
+    "lint:prettier": "prettier -c --write \"src/**/*\" --end-of-line auto",
20
+    "openapi": "max openapi",
21
+    "playwright": "playwright install && playwright test",
22
+    "prepare": "husky install",
23
+    "prettier": "prettier -c --write \"src/**/*\"",
24
+    "serve": "umi-serve",
25
+    "start": "cross-env UMI_ENV=dev max dev",
26
+    "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev",
27
+    "start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev",
28
+    "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
29
+    "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
30
+    "test": "max test",
31
+    "test:component": "max test ./src/components",
32
+    "test:e2e": "node ./tests/run-tests.js",
33
+    "tsc": "tsc --noEmit"
34
+  },
35
+  "lint-staged": {
36
+    "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
37
+    "**/*.{js,jsx,tsx,ts,less,md,json}": [
38
+      "prettier --write"
39
+    ]
40
+  },
41
+  "browserslist": [
42
+    "> 1%",
43
+    "last 2 versions",
44
+    "not ie <= 10"
45
+  ],
46
+  "dependencies": {
47
+    "@ant-design/icons": "^4.7.0",
48
+    "@ant-design/pro-components": "1.1.1",
49
+    "@umijs/route-utils": "^2.0.0",
50
+    "antd": "^4.20.0",
51
+    "classnames": "^2.3.0",
52
+    "lodash": "^4.17.0",
53
+    "moment": "^2.29.0",
54
+    "omit.js": "^2.0.2",
55
+    "rc-menu": "^9.1.0",
56
+    "rc-util": "^5.16.0",
57
+    "react": "^17.0.0",
58
+    "react-dev-inspector": "^1.7.0",
59
+    "react-dom": "^17.0.0",
60
+    "react-helmet-async": "^1.2.0"
61
+  },
62
+  "devDependencies": {
63
+    "@ant-design/pro-cli": "^2.1.0",
64
+    "@playwright/test": "^1.17.0",
65
+    "@types/classnames": "^2.3.1",
66
+    "@types/express": "^4.17.0",
67
+    "@types/history": "^4.7.0",
68
+    "@types/jest": "^26.0.0",
69
+    "@types/lodash": "^4.14.0",
70
+    "@types/react": "^17.0.0",
71
+    "@types/react-dom": "^17.0.0",
72
+    "@types/react-helmet": "^6.1.0",
73
+    "@umijs/fabric": "^2.11.1",
74
+    "@umijs/max": "^4.0.0-rc.22",
75
+    "@umijs/openapi": "^1.3.0",
76
+    "cross-env": "^7.0.0",
77
+    "cross-port-killer": "^1.3.0",
78
+    "detect-installer": "^1.0.0",
79
+    "eslint": "^7.32.0",
80
+    "gh-pages": "^3.2.0",
81
+    "husky": "^7.0.4",
82
+    "jsdom-global": "^3.0.0",
83
+    "lint-staged": "^10.0.0",
84
+    "mockjs": "^1.1.0",
85
+    "prettier": "^2.5.0",
86
+    "swagger-ui-dist": "^4.12.0",
87
+    "typescript": "^4.5.0",
88
+    "umi-presets-pro": "^1.0.1",
89
+    "umi-serve": "^1.9.10"
90
+  },
91
+  "engines": {
92
+    "node": ">=12.0.0"
93
+  }
94
+}

+ 22
- 0
playwright.config.ts ファイルの表示

@@ -0,0 +1,22 @@
1
+// playwright.config.ts
2
+import type { PlaywrightTestConfig } from '@playwright/test';
3
+import { devices } from '@playwright/test';
4
+
5
+const config: PlaywrightTestConfig = {
6
+  forbidOnly: !!process.env.CI,
7
+  retries: process.env.CI ? 2 : 0,
8
+  use: {
9
+    trace: 'on-first-retry',
10
+  },
11
+  projects: [
12
+    {
13
+      name: 'chromium',
14
+      use: { ...devices['Desktop Chrome'] },
15
+    },
16
+    {
17
+      name: 'firefox',
18
+      use: { ...devices['Desktop Firefox'] },
19
+    },
20
+  ],
21
+};
22
+export default config;

+ 1
- 0
public/CNAME ファイルの表示

@@ -0,0 +1 @@
1
+preview.pro.ant.design

バイナリ
public/favicon.ico ファイルの表示


バイナリ
public/icons/icon-128x128.png ファイルの表示


バイナリ
public/icons/icon-192x192.png ファイルの表示


バイナリ
public/icons/icon-512x512.png ファイルの表示


+ 1
- 0
public/logo.svg ファイルの表示

@@ -0,0 +1 @@
1
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" version="1.1" viewBox="0 0 200 200"><title>Group 28 Copy 5</title><desc>Created with Sketch.</desc><defs><linearGradient id="linearGradient-1" x1="62.102%" x2="108.197%" y1="0%" y2="37.864%"><stop offset="0%" stop-color="#4285EB"/><stop offset="100%" stop-color="#2EC7FF"/></linearGradient><linearGradient id="linearGradient-2" x1="69.644%" x2="54.043%" y1="0%" y2="108.457%"><stop offset="0%" stop-color="#29CDFF"/><stop offset="37.86%" stop-color="#148EFF"/><stop offset="100%" stop-color="#0A60FF"/></linearGradient><linearGradient id="linearGradient-3" x1="69.691%" x2="16.723%" y1="-12.974%" y2="117.391%"><stop offset="0%" stop-color="#FA816E"/><stop offset="41.473%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient><linearGradient id="linearGradient-4" x1="68.128%" x2="30.44%" y1="-35.691%" y2="114.943%"><stop offset="0%" stop-color="#FA8E7D"/><stop offset="51.264%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient></defs><g id="Page-1" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><g id="logo" transform="translate(-20.000000, -20.000000)"><g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)"><g id="Group-27-Copy-3"><g id="Group-25" fill-rule="nonzero"><g id="2"><path id="Shape" fill="url(#linearGradient-1)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/><path id="Shape" fill="url(#linearGradient-2)" d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z"/></g><path id="Shape" fill="url(#linearGradient-3)" d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z"/></g><ellipse id="Combined-Shape" cx="100.519" cy="100.437" fill="url(#linearGradient-4)" rx="23.6" ry="23.581"/></g></g></g></g></svg>

+ 5
- 0
public/pro_icon.svg ファイルの表示

@@ -0,0 +1,5 @@
1
+<svg width="42" height="42" xmlns="http://www.w3.org/2000/svg">
2
+ <g>
3
+  <path fill="#070707" d="m6.717392,13.773912l5.6,0c2.8,0 4.7,1.9 4.7,4.7c0,2.8 -2,4.7 -4.9,4.7l-2.5,0l0,4.3l-2.9,0l0,-13.7zm2.9,2.2l0,4.9l1.9,0c1.6,0 2.6,-0.9 2.6,-2.4c0,-1.6 -0.9,-2.4 -2.6,-2.4l-1.9,0l0,-0.1zm8.9,11.5l2.7,0l0,-5.7c0,-1.4 0.8,-2.3 2.2,-2.3c0.4,0 0.8,0.1 1,0.2l0,-2.4c-0.2,-0.1 -0.5,-0.1 -0.8,-0.1c-1.2,0 -2.1,0.7 -2.4,2l-0.1,0l0,-1.9l-2.7,0l0,10.2l0.1,0zm11.7,0.1c-3.1,0 -5,-2 -5,-5.3c0,-3.3 2,-5.3 5,-5.3s5,2 5,5.3c0,3.4 -1.9,5.3 -5,5.3zm0,-2.1c1.4,0 2.2,-1.1 2.2,-3.2c0,-2 -0.8,-3.2 -2.2,-3.2c-1.4,0 -2.2,1.2 -2.2,3.2c0,2.1 0.8,3.2 2.2,3.2z" class="st0" id="Ant-Design-Pro"/>
4
+ </g>
5
+</svg>

+ 9
- 0
src/access.ts ファイルの表示

@@ -0,0 +1,9 @@
1
+/**
2
+ * @see https://umijs.org/zh-CN/plugins/plugin-access
3
+ * */
4
+export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
5
+  const { currentUser } = initialState ?? {};
6
+  return {
7
+    canAdmin: currentUser && currentUser.access === 'admin',
8
+  };
9
+}

+ 97
- 0
src/app.jsx ファイルの表示

@@ -0,0 +1,97 @@
1
+import Footer from '@/components/Footer';
2
+import RightContent from '@/components/RightContent';
3
+import { currentUser as queryCurrentUser } from '@/services/api/login';
4
+import { requestConfig } from '@/utils/request';
5
+import { SettingDrawer } from '@ant-design/pro-components';
6
+import { history } from '@umijs/max';
7
+import defaultSettings from '../config/defaultSettings';
8
+
9
+const isDev = process.env.NODE_ENV === 'development';
10
+// const loginPath = '/user/login';
11
+
12
+/**
13
+ * @see  https://umijs.org/zh-CN/plugins/plugin-initial-state
14
+ * */
15
+
16
+export async function getInitialState() {
17
+  // const fetchUserInfo = async () => {
18
+  //   try {
19
+  //     const msg = await queryCurrentUser();
20
+  //     return msg.data;
21
+  //   } catch (error) {
22
+  //     history.push(loginPath);
23
+  //   }
24
+  //   return undefined;
25
+  // };
26
+  // // 如果不是登录页面,执行
27
+  // if (history.location.pathname !== loginPath) {
28
+  //   const currentUser = await fetchUserInfo();
29
+  //   return {
30
+  //     fetchUserInfo,
31
+  //     currentUser,
32
+  //     settings: defaultSettings,
33
+  //   };
34
+  // }
35
+  return {
36
+    // fetchUserInfo,
37
+    settings: defaultSettings,
38
+  };
39
+}
40
+
41
+// ProLayout 支持的api https://procomponents.ant.design/components/layout
42
+export const layout = ({ initialState, setInitialState }) => {
43
+  return {
44
+    // rightContentRender: () => <RightContent />,
45
+    disableContentMargin: false,
46
+    // waterMarkProps: {
47
+    //   content: initialState?.currentUser?.name,
48
+    // },
49
+    // footerRender: () => <Footer />,
50
+    onPageChange: () => {
51
+      const { location } = history;
52
+
53
+      // 如果没有登录,重定向到 login
54
+      // if (!initialState?.currentUser && location.pathname !== loginPath) {
55
+      //   history.push(loginPath);
56
+      // }
57
+    },
58
+    // links: isDev
59
+    //   ? [
60
+    //       <Link key="openapi" to="/umi/plugin/openapi" target="_blank">
61
+    //         <LinkOutlined />
62
+    //         <span>OpenAPI 文档</span>
63
+    //       </Link>,
64
+    //     ]
65
+    //   : [],
66
+    menuHeaderRender: undefined,
67
+
68
+    // 自定义 403 页面
69
+    // unAccessible: <div>unAccessible</div>,
70
+    // 增加一个 loading 的状态
71
+    childrenRender: (children, props) => {
72
+      // if (initialState?.loading) return <PageLoading />;
73
+      return (
74
+        <>
75
+          {children}
76
+          {/* {!props.location?.pathname?.includes('/login') && ( */}
77
+          <SettingDrawer
78
+            disableUrlParams
79
+            enableDarkTheme
80
+            settings={initialState?.settings}
81
+            onSettingChange={(settings) => {
82
+              setInitialState((preInitialState) => ({
83
+                ...preInitialState,
84
+                settings,
85
+              }));
86
+            }}
87
+          />
88
+          {/* )} */}
89
+        </>
90
+      );
91
+    },
92
+
93
+    ...initialState?.settings,
94
+  };
95
+};
96
+
97
+export const request = requestConfig;

+ 38
- 0
src/components/Footer/index.jsx ファイルの表示

@@ -0,0 +1,38 @@
1
+import { DefaultFooter } from '@ant-design/pro-components';
2
+
3
+const Footer = () => {
4
+  const defaultMessage = '无际科技';
5
+
6
+  const currentYear = new Date().getFullYear();
7
+
8
+  return (
9
+    <DefaultFooter
10
+      style={{
11
+        background: 'none',
12
+      }}
13
+      copyright={`${currentYear} ${defaultMessage}`}
14
+      // links={[
15
+      //   {
16
+      //     key: 'Ant Design Pro',
17
+      //     title: 'Ant Design Pro',
18
+      //     href: 'https://pro.ant.design',
19
+      //     blankTarget: true,
20
+      //   },
21
+      //   {
22
+      //     key: 'github',
23
+      //     title: <GithubOutlined />,
24
+      //     href: 'https://github.com/ant-design/ant-design-pro',
25
+      //     blankTarget: true,
26
+      //   },
27
+      //   {
28
+      //     key: 'Ant Design',
29
+      //     title: 'Ant Design',
30
+      //     href: 'https://ant.design',
31
+      //     blankTarget: true,
32
+      //   },
33
+      // ]}
34
+    />
35
+  );
36
+};
37
+
38
+export default Footer;

+ 16
- 0
src/components/HeaderDropdown/index.jsx ファイルの表示

@@ -0,0 +1,16 @@
1
+import { Dropdown } from 'antd';
2
+// import type { DropDownProps } from 'antd/es/dropdown';
3
+import classNames from 'classnames';
4
+import styles from './index.less';
5
+
6
+// export type HeaderDropdownProps = {
7
+//   overlayClassName?: string;
8
+//   overlay: React.ReactNode | (() => React.ReactNode) | any;
9
+//   placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
10
+// } & Omit<DropDownProps, 'overlay'>;
11
+
12
+const HeaderDropdown = ({ overlayClassName: cls, ...restProps }) => (
13
+  <Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
14
+);
15
+
16
+export default HeaderDropdown;

+ 16
- 0
src/components/HeaderDropdown/index.less ファイルの表示

@@ -0,0 +1,16 @@
1
+@import (reference) '~antd/es/style/themes/index';
2
+
3
+.container > * {
4
+  background-color: @popover-bg;
5
+  border-radius: 4px;
6
+  box-shadow: @shadow-1-down;
7
+}
8
+
9
+@media screen and (max-width: @screen-xs) {
10
+  .container {
11
+    width: 100% !important;
12
+  }
13
+  .container > * {
14
+    border-radius: 0 !important;
15
+  }
16
+}

+ 105
- 0
src/components/HeaderSearch/index.jsx ファイルの表示

@@ -0,0 +1,105 @@
1
+import { SearchOutlined } from '@ant-design/icons';
2
+// import type { InputRef } from 'antd';
3
+import { AutoComplete, Input } from 'antd';
4
+// import type { AutoCompleteProps } from 'antd/es/auto-complete';
5
+import classNames from 'classnames';
6
+import useMergedState from 'rc-util/es/hooks/useMergedState';
7
+import { useRef } from 'react';
8
+import styles from './index.less';
9
+
10
+// export type HeaderSearchProps = {
11
+//   onSearch?: (value?: string) => void;
12
+//   onChange?: (value?: string) => void;
13
+//   onVisibleChange?: (b: boolean) => void;
14
+//   className?: string;
15
+//   placeholder?: string;
16
+//   options: AutoCompleteProps['options'];
17
+//   defaultVisible?: boolean;
18
+//   visible?: boolean;
19
+//   defaultValue?: string;
20
+//   value?: string;
21
+// };
22
+
23
+const HeaderSearch = (props) => {
24
+  const {
25
+    className,
26
+    defaultValue,
27
+    onVisibleChange,
28
+    placeholder,
29
+    visible,
30
+    defaultVisible,
31
+    ...restProps
32
+  } = props;
33
+
34
+  const inputRef = useRef(null);
35
+
36
+  const [value, setValue] =
37
+    (useMergedState < string) |
38
+    (undefined >
39
+      (defaultValue,
40
+      {
41
+        value: props.value,
42
+        onChange: props.onChange,
43
+      }));
44
+
45
+  const [searchMode, setSearchMode] = useMergedState(defaultVisible ?? false, {
46
+    value: props.visible,
47
+    onChange: onVisibleChange,
48
+  });
49
+
50
+  const inputClass = classNames(styles.input, {
51
+    [styles.show]: searchMode,
52
+  });
53
+  return (
54
+    <div
55
+      className={classNames(className, styles.headerSearch)}
56
+      onClick={() => {
57
+        setSearchMode(true);
58
+        if (searchMode && inputRef.current) {
59
+          inputRef.current.focus();
60
+        }
61
+      }}
62
+      onTransitionEnd={({ propertyName }) => {
63
+        if (propertyName === 'width' && !searchMode) {
64
+          if (onVisibleChange) {
65
+            onVisibleChange(searchMode);
66
+          }
67
+        }
68
+      }}
69
+    >
70
+      <SearchOutlined
71
+        key="Icon"
72
+        style={{
73
+          cursor: 'pointer',
74
+        }}
75
+      />
76
+      <AutoComplete
77
+        key="AutoComplete"
78
+        className={inputClass}
79
+        value={value}
80
+        options={restProps.options}
81
+        onChange={(completeValue) => setValue(completeValue)}
82
+      >
83
+        <Input
84
+          size="small"
85
+          ref={inputRef}
86
+          defaultValue={defaultValue}
87
+          aria-label={placeholder}
88
+          placeholder={placeholder}
89
+          onKeyDown={(e) => {
90
+            if (e.key === 'Enter') {
91
+              if (restProps.onSearch) {
92
+                restProps.onSearch(value);
93
+              }
94
+            }
95
+          }}
96
+          onBlur={() => {
97
+            setSearchMode(false);
98
+          }}
99
+        />
100
+      </AutoComplete>
101
+    </div>
102
+  );
103
+};
104
+
105
+export default HeaderSearch;

+ 25
- 0
src/components/HeaderSearch/index.less ファイルの表示

@@ -0,0 +1,25 @@
1
+@import (reference) '~antd/es/style/themes/index';
2
+
3
+.headerSearch {
4
+  display: inline-flex;
5
+  align-items: center;
6
+  .input {
7
+    width: 0;
8
+    min-width: 0;
9
+    overflow: hidden;
10
+    background: transparent;
11
+    border-radius: 0;
12
+    transition: width 0.3s, margin-left 0.3s;
13
+    :global(.ant-select-selection) {
14
+      background: transparent;
15
+    }
16
+    input {
17
+      box-shadow: none !important;
18
+    }
19
+
20
+    &.show {
21
+      width: 210px;
22
+      margin-left: 8px;
23
+    }
24
+  }
25
+}

+ 126
- 0
src/components/NoticeIcon/NoticeIcon.tsx ファイルの表示

@@ -0,0 +1,126 @@
1
+import { BellOutlined } from '@ant-design/icons';
2
+import { Badge, Spin, Tabs } from 'antd';
3
+import classNames from 'classnames';
4
+import useMergedState from 'rc-util/es/hooks/useMergedState';
5
+import React from 'react';
6
+import HeaderDropdown from '../HeaderDropdown';
7
+import styles from './index.less';
8
+import type { NoticeIconTabProps } from './NoticeList';
9
+import NoticeList from './NoticeList';
10
+
11
+const { TabPane } = Tabs;
12
+
13
+export type NoticeIconProps = {
14
+  count?: number;
15
+  bell?: React.ReactNode;
16
+  className?: string;
17
+  loading?: boolean;
18
+  onClear?: (tabName: string, tabKey: string) => void;
19
+  onItemClick?: (item: API.NoticeIconItem, tabProps: NoticeIconTabProps) => void;
20
+  onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void;
21
+  onTabChange?: (tabTile: string) => void;
22
+  style?: React.CSSProperties;
23
+  onPopupVisibleChange?: (visible: boolean) => void;
24
+  popupVisible?: boolean;
25
+  clearText?: string;
26
+  viewMoreText?: string;
27
+  clearClose?: boolean;
28
+  emptyImage?: string;
29
+  children?: React.ReactElement<NoticeIconTabProps>[];
30
+};
31
+
32
+const NoticeIcon: React.FC<NoticeIconProps> & {
33
+  Tab: typeof NoticeList;
34
+} = (props) => {
35
+  const getNotificationBox = (): React.ReactNode => {
36
+    const {
37
+      children,
38
+      loading,
39
+      onClear,
40
+      onTabChange,
41
+      onItemClick,
42
+      onViewMore,
43
+      clearText,
44
+      viewMoreText,
45
+    } = props;
46
+    if (!children) {
47
+      return null;
48
+    }
49
+    const panes: React.ReactNode[] = [];
50
+    React.Children.forEach(children, (child: React.ReactElement<NoticeIconTabProps>): void => {
51
+      if (!child) {
52
+        return;
53
+      }
54
+      const { list, title, count, tabKey, showClear, showViewMore } = child.props;
55
+      const len = list && list.length ? list.length : 0;
56
+      const msgCount = count || count === 0 ? count : len;
57
+      const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title;
58
+      panes.push(
59
+        <TabPane tab={tabTitle} key={tabKey}>
60
+          <NoticeList
61
+            clearText={clearText}
62
+            viewMoreText={viewMoreText}
63
+            list={list}
64
+            tabKey={tabKey}
65
+            onClear={(): void => onClear && onClear(title, tabKey)}
66
+            onClick={(item): void => onItemClick && onItemClick(item, child.props)}
67
+            onViewMore={(event): void => onViewMore && onViewMore(child.props, event)}
68
+            showClear={showClear}
69
+            showViewMore={showViewMore}
70
+            title={title}
71
+          />
72
+        </TabPane>,
73
+      );
74
+    });
75
+    return (
76
+      <>
77
+        <Spin spinning={loading} delay={300}>
78
+          <Tabs className={styles.tabs} onChange={onTabChange}>
79
+            {panes}
80
+          </Tabs>
81
+        </Spin>
82
+      </>
83
+    );
84
+  };
85
+
86
+  const { className, count, bell } = props;
87
+
88
+  const [visible, setVisible] = useMergedState<boolean>(false, {
89
+    value: props.popupVisible,
90
+    onChange: props.onPopupVisibleChange,
91
+  });
92
+  const noticeButtonClass = classNames(className, styles.noticeButton);
93
+  const notificationBox = getNotificationBox();
94
+  const NoticeBellIcon = bell || <BellOutlined className={styles.icon} />;
95
+  const trigger = (
96
+    <span className={classNames(noticeButtonClass, { opened: visible })}>
97
+      <Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}>
98
+        {NoticeBellIcon}
99
+      </Badge>
100
+    </span>
101
+  );
102
+  if (!notificationBox) {
103
+    return trigger;
104
+  }
105
+
106
+  return (
107
+    <HeaderDropdown
108
+      placement="bottomRight"
109
+      overlay={notificationBox}
110
+      overlayClassName={styles.popover}
111
+      trigger={['click']}
112
+      visible={visible}
113
+      onVisibleChange={setVisible}
114
+    >
115
+      {trigger}
116
+    </HeaderDropdown>
117
+  );
118
+};
119
+
120
+NoticeIcon.defaultProps = {
121
+  emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
122
+};
123
+
124
+NoticeIcon.Tab = NoticeList;
125
+
126
+export default NoticeIcon;

+ 103
- 0
src/components/NoticeIcon/NoticeList.less ファイルの表示

@@ -0,0 +1,103 @@
1
+@import (reference) '~antd/es/style/themes/index';
2
+
3
+.list {
4
+  max-height: 400px;
5
+  overflow: auto;
6
+  &::-webkit-scrollbar {
7
+    display: none;
8
+  }
9
+  .item {
10
+    padding-right: 24px;
11
+    padding-left: 24px;
12
+    overflow: hidden;
13
+    cursor: pointer;
14
+    transition: all 0.3s;
15
+
16
+    .meta {
17
+      width: 100%;
18
+    }
19
+
20
+    .avatar {
21
+      margin-top: 4px;
22
+      background: @component-background;
23
+    }
24
+    .iconElement {
25
+      font-size: 32px;
26
+    }
27
+
28
+    &.read {
29
+      opacity: 0.4;
30
+    }
31
+    &:last-child {
32
+      border-bottom: 0;
33
+    }
34
+    &:hover {
35
+      background: @primary-1;
36
+    }
37
+    .title {
38
+      margin-bottom: 8px;
39
+      font-weight: normal;
40
+    }
41
+    .description {
42
+      font-size: 12px;
43
+      line-height: @line-height-base;
44
+    }
45
+    .datetime {
46
+      margin-top: 4px;
47
+      font-size: 12px;
48
+      line-height: @line-height-base;
49
+    }
50
+    .extra {
51
+      float: right;
52
+      margin-top: -1.5px;
53
+      margin-right: 0;
54
+      color: @text-color-secondary;
55
+      font-weight: normal;
56
+    }
57
+  }
58
+  .loadMore {
59
+    padding: 8px 0;
60
+    color: @primary-6;
61
+    text-align: center;
62
+    cursor: pointer;
63
+    &.loadedAll {
64
+      color: rgba(0, 0, 0, 0.25);
65
+      cursor: unset;
66
+    }
67
+  }
68
+}
69
+
70
+.notFound {
71
+  padding: 73px 0 88px;
72
+  color: @text-color-secondary;
73
+  text-align: center;
74
+  img {
75
+    display: inline-block;
76
+    height: 76px;
77
+    margin-bottom: 16px;
78
+  }
79
+}
80
+
81
+.bottomBar {
82
+  height: 46px;
83
+  color: @text-color;
84
+  line-height: 46px;
85
+  text-align: center;
86
+  border-top: 1px solid @border-color-split;
87
+  border-radius: 0 0 @border-radius-base @border-radius-base;
88
+  transition: all 0.3s;
89
+  div {
90
+    display: inline-block;
91
+    width: 50%;
92
+    cursor: pointer;
93
+    transition: all 0.3s;
94
+    user-select: none;
95
+
96
+    &:only-child {
97
+      width: 100%;
98
+    }
99
+    &:not(:only-child):last-child {
100
+      border-left: 1px solid @border-color-split;
101
+    }
102
+  }
103
+}

+ 112
- 0
src/components/NoticeIcon/NoticeList.tsx ファイルの表示

@@ -0,0 +1,112 @@
1
+import { Avatar, List } from 'antd';
2
+import classNames from 'classnames';
3
+import React from 'react';
4
+import styles from './NoticeList.less';
5
+
6
+export type NoticeIconTabProps = {
7
+  count?: number;
8
+  showClear?: boolean;
9
+  showViewMore?: boolean;
10
+  style?: React.CSSProperties;
11
+  title: string;
12
+  tabKey: API.NoticeIconItemType;
13
+  onClick?: (item: API.NoticeIconItem) => void;
14
+  onClear?: () => void;
15
+  emptyText?: string;
16
+  clearText?: string;
17
+  viewMoreText?: string;
18
+  list: API.NoticeIconItem[];
19
+  onViewMore?: (e: any) => void;
20
+};
21
+const NoticeList: React.FC<NoticeIconTabProps> = ({
22
+  list = [],
23
+  onClick,
24
+  onClear,
25
+  title,
26
+  onViewMore,
27
+  emptyText,
28
+  showClear = true,
29
+  clearText,
30
+  viewMoreText,
31
+  showViewMore = false,
32
+}) => {
33
+  if (!list || list.length === 0) {
34
+    return (
35
+      <div className={styles.notFound}>
36
+        <img
37
+          src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
38
+          alt="not found"
39
+        />
40
+        <div>{emptyText}</div>
41
+      </div>
42
+    );
43
+  }
44
+  return (
45
+    <div>
46
+      <List<API.NoticeIconItem>
47
+        className={styles.list}
48
+        dataSource={list}
49
+        renderItem={(item, i) => {
50
+          const itemCls = classNames(styles.item, {
51
+            [styles.read]: item.read,
52
+          });
53
+          // eslint-disable-next-line no-nested-ternary
54
+          const leftIcon = item.avatar ? (
55
+            typeof item.avatar === 'string' ? (
56
+              <Avatar className={styles.avatar} src={item.avatar} />
57
+            ) : (
58
+              <span className={styles.iconElement}>{item.avatar}</span>
59
+            )
60
+          ) : null;
61
+
62
+          return (
63
+            <div
64
+              onClick={() => {
65
+                onClick?.(item);
66
+              }}
67
+            >
68
+              <List.Item className={itemCls} key={item.key || i}>
69
+                <List.Item.Meta
70
+                  className={styles.meta}
71
+                  avatar={leftIcon}
72
+                  title={
73
+                    <div className={styles.title}>
74
+                      {item.title}
75
+                      <div className={styles.extra}>{item.extra}</div>
76
+                    </div>
77
+                  }
78
+                  description={
79
+                    <div>
80
+                      <div className={styles.description}>{item.description}</div>
81
+                      <div className={styles.datetime}>{item.datetime}</div>
82
+                    </div>
83
+                  }
84
+                />
85
+              </List.Item>
86
+            </div>
87
+          );
88
+        }}
89
+      />
90
+      <div className={styles.bottomBar}>
91
+        {showClear ? (
92
+          <div onClick={onClear}>
93
+            {clearText} {title}
94
+          </div>
95
+        ) : null}
96
+        {showViewMore ? (
97
+          <div
98
+            onClick={(e) => {
99
+              if (onViewMore) {
100
+                onViewMore(e);
101
+              }
102
+            }}
103
+          >
104
+            {viewMoreText}
105
+          </div>
106
+        ) : null}
107
+      </div>
108
+    </div>
109
+  );
110
+};
111
+
112
+export default NoticeList;

+ 152
- 0
src/components/NoticeIcon/index.jsx ファイルの表示

@@ -0,0 +1,152 @@
1
+import { getNotices } from '@/services/ant-design-pro/api';
2
+import { useModel, useRequest } from '@umijs/max';
3
+import { message, Tag } from 'antd';
4
+import { groupBy } from 'lodash';
5
+import moment from 'moment';
6
+import { useEffect, useState } from 'react';
7
+import styles from './index.less';
8
+import NoticeIcon from './NoticeIcon';
9
+
10
+// export type GlobalHeaderRightProps = {
11
+//   fetchingNotices?: boolean;
12
+//   onNoticeVisibleChange?: (visible: boolean) => void;
13
+//   onNoticeClear?: (tabName?: string) => void;
14
+// };
15
+
16
+const getNoticeData = (notices) => {
17
+  if (!notices || notices.length === 0 || !Array.isArray(notices)) {
18
+    return {};
19
+  }
20
+
21
+  const newNotices = notices.map((notice) => {
22
+    const newNotice = { ...notice };
23
+
24
+    if (newNotice.datetime) {
25
+      newNotice.datetime = moment(notice.datetime).fromNow();
26
+    }
27
+
28
+    if (newNotice.id) {
29
+      newNotice.key = newNotice.id;
30
+    }
31
+
32
+    if (newNotice.extra && newNotice.status) {
33
+      const color = {
34
+        todo: '',
35
+        processing: 'blue',
36
+        urgent: 'red',
37
+        doing: 'gold',
38
+      }[newNotice.status];
39
+      newNotice.extra = (
40
+        <Tag
41
+          color={color}
42
+          style={{
43
+            marginRight: 0,
44
+          }}
45
+        >
46
+          {newNotice.extra}
47
+        </Tag>
48
+      );
49
+    }
50
+
51
+    return newNotice;
52
+  });
53
+  return groupBy(newNotices, 'type');
54
+};
55
+
56
+const getUnreadData = (noticeData) => {
57
+  const unreadMsg = {};
58
+  Object.keys(noticeData).forEach((key) => {
59
+    const value = noticeData[key];
60
+
61
+    if (!unreadMsg[key]) {
62
+      unreadMsg[key] = 0;
63
+    }
64
+
65
+    if (Array.isArray(value)) {
66
+      unreadMsg[key] = value.filter((item) => !item.read).length;
67
+    }
68
+  });
69
+  return unreadMsg;
70
+};
71
+
72
+const NoticeIconView = () => {
73
+  const { initialState } = useModel('@@initialState');
74
+  const { currentUser } = initialState || {};
75
+  const [notices, setNotices] = useState([]);
76
+  const { data } = useRequest(getNotices);
77
+
78
+  useEffect(() => {
79
+    setNotices(data || []);
80
+  }, [data]);
81
+
82
+  const noticeData = getNoticeData(notices);
83
+  const unreadMsg = getUnreadData(noticeData || {});
84
+
85
+  const changeReadState = (id) => {
86
+    setNotices(
87
+      notices.map((item) => {
88
+        const notice = { ...item };
89
+        if (notice.id === id) {
90
+          notice.read = true;
91
+        }
92
+        return notice;
93
+      }),
94
+    );
95
+  };
96
+
97
+  const clearReadState = (title, key) => {
98
+    setNotices(
99
+      notices.map((item) => {
100
+        const notice = { ...item };
101
+        if (notice.type === key) {
102
+          notice.read = true;
103
+        }
104
+        return notice;
105
+      }),
106
+    );
107
+    message.success(`${'清空了'} ${title}`);
108
+  };
109
+
110
+  return (
111
+    <NoticeIcon
112
+      className={styles.action}
113
+      count={currentUser && currentUser.unreadCount}
114
+      onItemClick={(item) => {
115
+        changeReadState(item.id);
116
+      }}
117
+      onClear={(title, key) => clearReadState(title, key)}
118
+      loading={false}
119
+      clearText="清空"
120
+      viewMoreText="查看更多"
121
+      onViewMore={() => message.info('Click on view more')}
122
+      clearClose
123
+    >
124
+      <NoticeIcon.Tab
125
+        tabKey="notification"
126
+        count={unreadMsg.notification}
127
+        list={noticeData.notification}
128
+        title="通知"
129
+        emptyText="你已查看所有通知"
130
+        showViewMore
131
+      />
132
+      <NoticeIcon.Tab
133
+        tabKey="message"
134
+        count={unreadMsg.message}
135
+        list={noticeData.message}
136
+        title="消息"
137
+        emptyText="您已读完所有消息"
138
+        showViewMore
139
+      />
140
+      <NoticeIcon.Tab
141
+        tabKey="event"
142
+        title="待办"
143
+        emptyText="你已完成所有待办"
144
+        count={unreadMsg.event}
145
+        list={noticeData.event}
146
+        showViewMore
147
+      />
148
+    </NoticeIcon>
149
+  );
150
+};
151
+
152
+export default NoticeIconView;

+ 35
- 0
src/components/NoticeIcon/index.less ファイルの表示

@@ -0,0 +1,35 @@
1
+@import (reference) '~antd/es/style/themes/index';
2
+
3
+.popover {
4
+  position: relative;
5
+  width: 336px;
6
+}
7
+
8
+.noticeButton {
9
+  display: inline-block;
10
+  cursor: pointer;
11
+  transition: all 0.3s;
12
+}
13
+.icon {
14
+  padding: 4px;
15
+  vertical-align: middle;
16
+}
17
+
18
+.badge {
19
+  font-size: 16px;
20
+}
21
+
22
+.tabs {
23
+  :global {
24
+    .ant-tabs-nav-list {
25
+      margin: auto;
26
+    }
27
+
28
+    .ant-tabs-nav-scroll {
29
+      text-align: center;
30
+    }
31
+    .ant-tabs-nav {
32
+      margin-bottom: 0;
33
+    }
34
+  }
35
+}

+ 112
- 0
src/components/RightContent/AvatarDropdown.jsx ファイルの表示

@@ -0,0 +1,112 @@
1
+import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
2
+import { history, useModel } from '@umijs/max';
3
+import { Avatar, Menu, Spin } from 'antd';
4
+import { stringify } from 'querystring';
5
+import { useCallback } from 'react';
6
+import HeaderDropdown from '../HeaderDropdown';
7
+import styles from './index.less';
8
+
9
+/**
10
+ * 退出登录,并且将当前的 url 保存
11
+ */
12
+const loginOut = async () => {
13
+  // await outLogin();
14
+  sessionStorage.removeItem('token');
15
+  const { search, pathname } = history.location;
16
+  const urlParams = new URL(window.location.href).searchParams;
17
+  /** 此方法会跳转到 redirect 参数所在的位置 */
18
+  const redirect = urlParams.get('redirect');
19
+  // Note: There may be security issues, please note
20
+  if (window.location.pathname !== '/user/login' && !redirect) {
21
+    history.replace({
22
+      pathname: '/user/login',
23
+      search: stringify({
24
+        redirect: pathname + search,
25
+      }),
26
+    });
27
+  }
28
+};
29
+
30
+const AvatarDropdown = ({ menu }) => {
31
+  const { initialState, setInitialState } = useModel('@@initialState');
32
+
33
+  const onMenuClick = useCallback(
34
+    (event) => {
35
+      const { key } = event;
36
+      if (key === 'logout') {
37
+        setInitialState((s) => ({ ...s, currentUser: undefined }));
38
+        loginOut();
39
+        return;
40
+      }
41
+      history.push(`/account/${key}`);
42
+    },
43
+    [setInitialState],
44
+  );
45
+
46
+  const loading = (
47
+    <span className={`${styles.action} ${styles.account}`}>
48
+      <Spin
49
+        size="small"
50
+        style={{
51
+          marginLeft: 8,
52
+          marginRight: 8,
53
+        }}
54
+      />
55
+    </span>
56
+  );
57
+
58
+  if (!initialState) {
59
+    return loading;
60
+  }
61
+
62
+  const { currentUser } = initialState;
63
+
64
+  if (!currentUser || !currentUser.name) {
65
+    return loading;
66
+  }
67
+
68
+  const menuItems = [
69
+    ...(menu
70
+      ? [
71
+          {
72
+            key: 'center',
73
+            icon: <UserOutlined />,
74
+            label: '个人中心',
75
+          },
76
+          {
77
+            key: 'settings',
78
+            icon: <SettingOutlined />,
79
+            label: '个人设置',
80
+          },
81
+          {
82
+            type: 'divider',
83
+          },
84
+        ]
85
+      : []),
86
+    {
87
+      key: 'logout',
88
+      icon: <LogoutOutlined />,
89
+      label: '退出登录',
90
+    },
91
+  ];
92
+
93
+  const menuHeaderDropdown = (
94
+    <Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick} items={menuItems} />
95
+  );
96
+
97
+  return (
98
+    <HeaderDropdown overlay={menuHeaderDropdown}>
99
+      <span className={`${styles.action} ${styles.account}`}>
100
+        <Avatar
101
+          className={styles.avatar}
102
+          icon={<UserOutlined />}
103
+          // src={currentUser.avatar}
104
+          alt="avatar"
105
+        />
106
+        <span className={`${styles.name} anticon`}>{currentUser.name}</span>
107
+      </span>
108
+    </HeaderDropdown>
109
+  );
110
+};
111
+
112
+export default AvatarDropdown;

+ 27
- 0
src/components/RightContent/index.jsx ファイルの表示

@@ -0,0 +1,27 @@
1
+import { useModel } from '@umijs/max';
2
+import { Space } from 'antd';
3
+import Avatar from './AvatarDropdown';
4
+import styles from './index.less';
5
+
6
+// export type SiderTheme = 'light' | 'dark';
7
+
8
+const GlobalHeaderRight = () => {
9
+  const { initialState } = useModel('@@initialState');
10
+
11
+  if (!initialState || !initialState.settings) {
12
+    return null;
13
+  }
14
+
15
+  const { navTheme, layout } = initialState.settings;
16
+  let className = styles.right;
17
+
18
+  if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') {
19
+    className = `${styles.right}  ${styles.dark}`;
20
+  }
21
+  return (
22
+    <Space className={className}>
23
+      <Avatar />
24
+    </Space>
25
+  );
26
+};
27
+export default GlobalHeaderRight;

+ 73
- 0
src/components/RightContent/index.less ファイルの表示

@@ -0,0 +1,73 @@
1
+@import (reference) '~antd/es/style/themes/index';
2
+
3
+@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
4
+
5
+.menu {
6
+  :global(.anticon) {
7
+    margin-right: 8px;
8
+  }
9
+  :global(.ant-dropdown-menu-item) {
10
+    min-width: 160px;
11
+  }
12
+}
13
+
14
+.right {
15
+  display: flex;
16
+  float: right;
17
+  height: 48px;
18
+  margin-left: auto;
19
+  overflow: hidden;
20
+  .action {
21
+    display: flex;
22
+    align-items: center;
23
+    height: 48px;
24
+    padding: 0 12px;
25
+    cursor: pointer;
26
+    transition: all 0.3s;
27
+    > span {
28
+      vertical-align: middle;
29
+    }
30
+    &:hover {
31
+      background: @pro-header-hover-bg;
32
+    }
33
+    &:global(.opened) {
34
+      background: @pro-header-hover-bg;
35
+    }
36
+  }
37
+  .search {
38
+    padding: 0 12px;
39
+    &:hover {
40
+      background: transparent;
41
+    }
42
+  }
43
+  .account {
44
+    .avatar {
45
+      margin-right: 8px;
46
+      color: @primary-color;
47
+      vertical-align: top;
48
+      background: rgba(255, 255, 255, 0.85);
49
+    }
50
+  }
51
+}
52
+
53
+@media only screen and (max-width: @screen-md) {
54
+  :global(.ant-divider-vertical) {
55
+    vertical-align: unset;
56
+  }
57
+  .name {
58
+    display: none;
59
+  }
60
+  .right {
61
+    position: absolute;
62
+    top: 0;
63
+    right: 12px;
64
+    .account {
65
+      .avatar {
66
+        margin-right: 0;
67
+      }
68
+    }
69
+    .search {
70
+      display: none;
71
+    }
72
+  }
73
+}

+ 66
- 0
src/components/UploadImage/index.jsx ファイルの表示

@@ -0,0 +1,66 @@
1
+import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
2
+import { Upload } from 'antd';
3
+import { useState } from 'react';
4
+
5
+function beforeUpload(file) {
6
+  const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
7
+  if (!isJpgOrPng) {
8
+    message.error('请上传 JPG 或 PNG 图片!');
9
+  }
10
+  const isLt10M = file.size / 1024 / 1024 < 10;
11
+  if (!isLt10M) {
12
+    message.error('图片大小必须小于 10MB!');
13
+  }
14
+
15
+  return isJpgOrPng && 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
+    console.log(info,'--333-')
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
+
41
+    if (info.file.status === 'done') {
42
+      setLoading(false);
43
+      onChange(info.file.response);
44
+    }
45
+  };
46
+
47
+  const uploadFile = () => {};
48
+
49
+  return (
50
+    <Upload
51
+      // customRequest={uploadFile}
52
+      action={'/api/image/upload'}
53
+      listType="picture-card"
54
+      className="image-uploader"
55
+      showUploadList={false}
56
+      beforeUpload={beforeUpload}
57
+      onChange={handleChange}
58
+    >
59
+      {value ? (
60
+        <img src={value} alt="avatar" style={{ width: '100%', height: '100%' }} />
61
+      ) : (
62
+        <UploadButton loading={loading} />
63
+      )}
64
+    </Upload>
65
+  );
66
+};

+ 53
- 0
src/components/UploadVideo/index.jsx ファイルの表示

@@ -0,0 +1,53 @@
1
+import { Button, Upload } from 'antd';
2
+import { useState } from 'react';
3
+
4
+// import styles from './style.less';
5
+
6
+function beforeUpload(file) {
7
+  const isMp4 = file.type === 'video/mp4';
8
+  if (!isMp4) {
9
+    message.error('请上传 MP4 视频!');
10
+  }
11
+
12
+  return isMp4;
13
+}
14
+
15
+export default (props) => {
16
+  const { value, onChange } = props;
17
+
18
+  const [loading, setLoading] = useState(false);
19
+
20
+  const handleChange = (info) => {
21
+    if (info.file.status === 'uploading') {
22
+      setLoading(true);
23
+      return;
24
+    }
25
+    if (info.file.status === 'error') {
26
+      setLoading(false);
27
+      return;
28
+    }
29
+
30
+    if (info.file.status === 'done') {
31
+      setLoading(false);
32
+      onChange(info.file.response);
33
+    }
34
+  };
35
+
36
+  return (
37
+    <div>
38
+      <Upload
39
+        className="image-uploader"
40
+        showUploadList={false}
41
+        beforeUpload={beforeUpload}
42
+        onChange={handleChange}
43
+      >
44
+        <Button loading={loading}>点击上传视频</Button>
45
+      </Upload>
46
+      {!!value && (
47
+        <video width="320" height="240" controls>
48
+          <source src={value} type="video/mp4" />
49
+        </video>
50
+      )}
51
+    </div>
52
+  );
53
+};

+ 267
- 0
src/components/index.md ファイルの表示

@@ -0,0 +1,267 @@
1
+---
2
+title: 业务组件
3
+sidemenu: false
4
+---
5
+
6
+> 此功能由[dumi](https://d.umijs.org/zh-CN/guide/advanced#umi-%E9%A1%B9%E7%9B%AE%E9%9B%86%E6%88%90%E6%A8%A1%E5%BC%8F)提供,dumi 是一个 📖 为组件开发场景而生的文档工具,用过的都说好。
7
+
8
+# 业务组件
9
+
10
+这里列举了 Pro 中所有用到的组件,这些组件不适合作为组件库,但是在业务中却真实需要。所以我们准备了这个文档,来指导大家是否需要使用这个组件。
11
+
12
+## Footer 页脚组件
13
+
14
+这个组件自带了一些 Pro 的配置,你一般都需要改掉它的信息。
15
+
16
+```tsx
17
+/**
18
+ * background: '#f0f2f5'
19
+ */
20
+import Footer from '@/components/Footer';
21
+
22
+export default () => <Footer />;
23
+```
24
+
25
+## HeaderDropdown 头部下拉列表
26
+
27
+HeaderDropdown 是 antd Dropdown 的封装,但是增加了移动端的特殊处理,用法也是相同的。
28
+
29
+```tsx
30
+/**
31
+ * background: '#f0f2f5'
32
+ */
33
+import HeaderDropdown from '@/components/HeaderDropdown';
34
+import { Button, Menu } from 'antd';
35
+
36
+export default () => {
37
+  const menuHeaderDropdown = (
38
+    <Menu selectedKeys={[]}>
39
+      <Menu.Item key="center">个人中心</Menu.Item>
40
+      <Menu.Item key="settings">个人设置</Menu.Item>
41
+      <Menu.Divider />
42
+      <Menu.Item key="logout">退出登录</Menu.Item>
43
+    </Menu>
44
+  );
45
+  return (
46
+    <HeaderDropdown overlay={menuHeaderDropdown}>
47
+      <Button>hover 展示菜单</Button>
48
+    </HeaderDropdown>
49
+  );
50
+};
51
+```
52
+
53
+## HeaderSearch 头部搜索框
54
+
55
+一个带补全数据的输入框,支持收起和展开 Input
56
+
57
+```tsx
58
+/**
59
+ * background: '#f0f2f5'
60
+ */
61
+import HeaderSearch from '@/components/HeaderSearch';
62
+
63
+export default () => {
64
+  return (
65
+    <HeaderSearch
66
+      placeholder="站内搜索"
67
+      defaultValue="umi ui"
68
+      options={[
69
+        { label: 'Ant Design Pro', value: 'Ant Design Pro' },
70
+        {
71
+          label: 'Ant Design',
72
+          value: 'Ant Design',
73
+        },
74
+        {
75
+          label: 'Pro Table',
76
+          value: 'Pro Table',
77
+        },
78
+        {
79
+          label: 'Pro Layout',
80
+          value: 'Pro Layout',
81
+        },
82
+      ]}
83
+      onSearch={(value) => {
84
+        console.log('input', value);
85
+      }}
86
+    />
87
+  );
88
+};
89
+```
90
+
91
+### API
92
+
93
+| 参数            | 说明                               | 类型                         | 默认值 |
94
+| --------------- | ---------------------------------- | ---------------------------- | ------ |
95
+| value           | 输入框的值                         | `string`                     | -      |
96
+| onChange        | 值修改后触发                       | `(value?: string) => void`   | -      |
97
+| onSearch        | 查询后触发                         | `(value?: string) => void`   | -      |
98
+| options         | 选项菜单的的列表                   | `{label,value}[]`            | -      |
99
+| defaultVisible  | 输入框默认是否显示,只有第一次生效 | `boolean`                    | -      |
100
+| visible         | 输入框是否显示                     | `boolean`                    | -      |
101
+| onVisibleChange | 输入框显示隐藏的回调函数           | `(visible: boolean) => void` | -      |
102
+
103
+## NoticeIcon 通知工具
104
+
105
+通知工具提供一个展示多种通知信息的界面。
106
+
107
+```tsx
108
+/**
109
+ * background: '#f0f2f5'
110
+ */
111
+import NoticeIcon from '@/components/NoticeIcon/NoticeIcon';
112
+import { message } from 'antd';
113
+
114
+export default () => {
115
+  const list = [
116
+    {
117
+      id: '000000001',
118
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
119
+      title: '你收到了 14 份新周报',
120
+      datetime: '2017-08-09',
121
+      type: 'notification',
122
+    },
123
+    {
124
+      id: '000000002',
125
+      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
126
+      title: '你推荐的 曲妮妮 已通过第三轮面试',
127
+      datetime: '2017-08-08',
128
+      type: 'notification',
129
+    },
130
+  ];
131
+  return (
132
+    <NoticeIcon
133
+      count={10}
134
+      onItemClick={(item) => {
135
+        message.info(`${item.title} 被点击了`);
136
+      }}
137
+      onClear={(title: string, key: string) => message.info('点击了清空更多')}
138
+      loading={false}
139
+      clearText="清空"
140
+      viewMoreText="查看更多"
141
+      onViewMore={() => message.info('点击了查看更多')}
142
+      clearClose
143
+    >
144
+      <NoticeIcon.Tab
145
+        tabKey="notification"
146
+        count={2}
147
+        list={list}
148
+        title="通知"
149
+        emptyText="你已查看所有通知"
150
+        showViewMore
151
+      />
152
+      <NoticeIcon.Tab
153
+        tabKey="message"
154
+        count={2}
155
+        list={list}
156
+        title="消息"
157
+        emptyText="您已读完所有消息"
158
+        showViewMore
159
+      />
160
+      <NoticeIcon.Tab
161
+        tabKey="event"
162
+        title="待办"
163
+        emptyText="你已完成所有待办"
164
+        count={2}
165
+        list={list}
166
+        showViewMore
167
+      />
168
+    </NoticeIcon>
169
+  );
170
+};
171
+```
172
+
173
+### NoticeIcon API
174
+
175
+| 参数 | 说明 | 类型 | 默认值 |
176
+| --- | --- | --- | --- |
177
+| count | 有多少未读通知 | `number` | - |
178
+| bell | 铃铛的图表 | `ReactNode` | - |
179
+| onClear | 点击清空数据按钮 | `(tabName: string, tabKey: string) => void` | - |
180
+| onItemClick | 未读消息列被点击 | `(item: API.NoticeIconData, tabProps: NoticeIconTabProps) => void` | - |
181
+| onViewMore | 查看更多的按钮点击 | `(tabProps: NoticeIconTabProps, e: MouseEvent) => void` | - |
182
+| onTabChange | 通知 Tab 的切换 | `(tabTile: string) => void;` | - |
183
+| popupVisible | 通知显示是否展示 | `boolean` | - |
184
+| onPopupVisibleChange | 通知信息显示隐藏的回调函数 | `(visible: boolean) => void` | - |
185
+| clearText | 清空按钮的文字 | `string` | - |
186
+| viewMoreText | 查看更多的按钮文字 | `string` | - |
187
+| clearClose | 展示清空按钮 | `boolean` | - |
188
+| emptyImage | 列表为空时的兜底展示 | `ReactNode` | - |
189
+
190
+### NoticeIcon.Tab API
191
+
192
+| 参数         | 说明               | 类型                                 | 默认值 |
193
+| ------------ | ------------------ | ------------------------------------ | ------ |
194
+| count        | 有多少未读通知     | `number`                             | -      |
195
+| title        | 通知 Tab 的标题    | `ReactNode`                          | -      |
196
+| showClear    | 展示清除按钮       | `boolean`                            | `true` |
197
+| showViewMore | 展示加载更         | `boolean`                            | `true` |
198
+| tabKey       | Tab 的唯一 key     | `string`                             | -      |
199
+| onClick      | 子项的单击事件     | `(item: API.NoticeIconData) => void` | -      |
200
+| onClear      | 清楚按钮的点击     | `()=>void`                           | -      |
201
+| emptyText    | 为空的时候测试     | `()=>void`                           | -      |
202
+| viewMoreText | 查看更多的按钮文字 | `string`                             | -      |
203
+| onViewMore   | 查看更多的按钮点击 | `( e: MouseEvent) => void`           | -      |
204
+| list         | 通知信息的列表     | `API.NoticeIconData`                 | -      |
205
+
206
+### NoticeIconData
207
+
208
+```tsx | pure
209
+export interface NoticeIconData {
210
+  id: string;
211
+  key: string;
212
+  avatar: string;
213
+  title: string;
214
+  datetime: string;
215
+  type: string;
216
+  read?: boolean;
217
+  description: string;
218
+  clickClose?: boolean;
219
+  extra: any;
220
+  status: string;
221
+}
222
+```
223
+
224
+## RightContent
225
+
226
+RightContent 是以上几个组件的组合,同时新增了 plugins 的 `SelectLang` 插件。
227
+
228
+```tsx | pure
229
+<Space>
230
+  <HeaderSearch
231
+    placeholder="站内搜索"
232
+    defaultValue="umi ui"
233
+    options={[
234
+      { label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>, value: 'umi ui' },
235
+      {
236
+        label: <a href="next.ant.design">Ant Design</a>,
237
+        value: 'Ant Design',
238
+      },
239
+      {
240
+        label: <a href="https://protable.ant.design/">Pro Table</a>,
241
+        value: 'Pro Table',
242
+      },
243
+      {
244
+        label: <a href="https://prolayout.ant.design/">Pro Layout</a>,
245
+        value: 'Pro Layout',
246
+      },
247
+    ]}
248
+  />
249
+  <Tooltip title="使用文档">
250
+    <span
251
+      className={styles.action}
252
+      onClick={() => {
253
+        window.location.href = 'https://pro.ant.design/docs/getting-started';
254
+      }}
255
+    >
256
+      <QuestionCircleOutlined />
257
+    </span>
258
+  </Tooltip>
259
+  <Avatar />
260
+  {REACT_APP_ENV && (
261
+    <span>
262
+      <Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
263
+    </span>
264
+  )}
265
+  <SelectLang className={styles.action} />
266
+</Space>
267
+```

+ 45
- 0
src/e2e/baseLayout.e2e.spec.ts ファイルの表示

@@ -0,0 +1,45 @@
1
+import type { Page } from '@playwright/test';
2
+import { expect, test } from '@playwright/test';
3
+const { uniq } = require('lodash');
4
+const RouterConfig = require('../../config/routes').default;
5
+
6
+const BASE_URL = `http://localhost:${process.env.PORT || 8001}`;
7
+
8
+function formatter(routes: any, parentPath = ''): string[] {
9
+  const fixedParentPath = parentPath.replace(/\/{1,}/g, '/');
10
+  let result: string[] = [];
11
+  routes.forEach((item: { path: string; routes: string }) => {
12
+    if (item.path && !item.path.startsWith('/')) {
13
+      result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/'));
14
+    }
15
+    if (item.path && item.path.startsWith('/')) {
16
+      result.push(`${item.path}`.replace(/\/{1,}/g, '/'));
17
+    }
18
+    if (item.routes) {
19
+      result = result.concat(
20
+        formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath),
21
+      );
22
+    }
23
+  });
24
+  return uniq(result.filter((item) => !!item));
25
+}
26
+
27
+const testPage = (path: string, page: Page) => async () => {
28
+  await page.evaluate(() => {
29
+    localStorage.setItem('antd-pro-authority', '["admin"]');
30
+  });
31
+  await page.goto(`${BASE_URL}${path}`);
32
+  await page.waitForSelector('footer', {
33
+    timeout: 2000,
34
+  });
35
+  const haveFooter = await page.evaluate(() => document.getElementsByTagName('footer').length > 0);
36
+  expect(haveFooter).toBeTruthy();
37
+};
38
+
39
+const routers = formatter(RouterConfig);
40
+
41
+routers.forEach((route) => {
42
+  test(`test route page ${route}`, async ({ page }) => {
43
+    await testPage(route, page);
44
+  });
45
+});

+ 91
- 0
src/global.jsx ファイルの表示

@@ -0,0 +1,91 @@
1
+import { useIntl } from '@umijs/max';
2
+import { Button, message, notification } from 'antd';
3
+import defaultSettings from '../config/defaultSettings';
4
+
5
+const { pwa } = defaultSettings;
6
+const isHttps = document.location.protocol === 'https:';
7
+
8
+const clearCache = () => {
9
+  // remove all caches
10
+  if (window.caches) {
11
+    caches
12
+      .keys()
13
+      .then((keys) => {
14
+        keys.forEach((key) => {
15
+          caches.delete(key);
16
+        });
17
+      })
18
+      .catch((e) => console.log(e));
19
+  }
20
+};
21
+
22
+// if pwa is true
23
+if (pwa) {
24
+  // Notify user if offline now
25
+  window.addEventListener('sw.offline', () => {
26
+    message.warning(useIntl().formatMessage({ id: 'app.pwa.offline' }));
27
+  });
28
+
29
+  // Pop up a prompt on the page asking the user if they want to use the latest version
30
+  window.addEventListener('sw.updated', (event) => {
31
+    const e = event ;
32
+    const reloadSW = async () => {
33
+      // Check if there is sw whose state is waiting in ServiceWorkerRegistration
34
+      // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
35
+      const worker = e.detail && e.detail.waiting;
36
+      if (!worker) {
37
+        return true;
38
+      }
39
+      // Send skip-waiting event to waiting SW with MessageChannel
40
+      await new Promise((resolve, reject) => {
41
+        const channel = new MessageChannel();
42
+        channel.port1.onmessage = (msgEvent) => {
43
+          if (msgEvent.data.error) {
44
+            reject(msgEvent.data.error);
45
+          } else {
46
+            resolve(msgEvent.data);
47
+          }
48
+        };
49
+        worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
50
+      });
51
+
52
+      clearCache();
53
+      window.location.reload();
54
+      return true;
55
+    };
56
+    const key = `open${Date.now()}`;
57
+    const btn = (
58
+      <Button
59
+        type="primary"
60
+        onClick={() => {
61
+          notification.close(key);
62
+          reloadSW();
63
+        }}
64
+      >
65
+        {useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
66
+      </Button>
67
+    );
68
+    notification.open({
69
+      message: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }),
70
+      description: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
71
+      btn,
72
+      key,
73
+      onClose: async () => null,
74
+    });
75
+  });
76
+} else if ('serviceWorker' in navigator && isHttps) {
77
+  // unregister service worker
78
+  const { serviceWorker } = navigator;
79
+  if (serviceWorker.getRegistrations) {
80
+    serviceWorker.getRegistrations().then((sws) => {
81
+      sws.forEach((sw) => {
82
+        sw.unregister();
83
+      });
84
+    });
85
+  }
86
+  serviceWorker.getRegistration().then((sw) => {
87
+    if (sw) sw.unregister();
88
+  });
89
+
90
+  clearCache();
91
+}

+ 50
- 0
src/global.less ファイルの表示

@@ -0,0 +1,50 @@
1
+@import '~antd/es/style/variable.less';
2
+
3
+html,
4
+body,
5
+#root {
6
+  height: 100%;
7
+}
8
+
9
+.colorWeak {
10
+  filter: invert(80%);
11
+}
12
+
13
+.ant-layout {
14
+  min-height: 100vh;
15
+}
16
+.ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
17
+  left: unset;
18
+}
19
+
20
+canvas {
21
+  display: block;
22
+}
23
+
24
+body {
25
+  text-rendering: optimizeLegibility;
26
+  -webkit-font-smoothing: antialiased;
27
+  -moz-osx-font-smoothing: grayscale;
28
+}
29
+
30
+ul,
31
+ol {
32
+  list-style: none;
33
+}
34
+
35
+@media (max-width: @screen-xs) {
36
+  .ant-table {
37
+    width: 100%;
38
+    overflow-x: auto;
39
+    &-thead > tr,
40
+    &-tbody > tr {
41
+      > th,
42
+      > td {
43
+        white-space: pre;
44
+        > span {
45
+          display: block;
46
+        }
47
+      }
48
+    }
49
+  }
50
+}

+ 22
- 0
src/manifest.json ファイルの表示

@@ -0,0 +1,22 @@
1
+{
2
+  "name": "Ant Design Pro",
3
+  "short_name": "Ant Design Pro",
4
+  "display": "standalone",
5
+  "start_url": "./?utm_source=homescreen",
6
+  "theme_color": "#002140",
7
+  "background_color": "#001529",
8
+  "icons": [
9
+    {
10
+      "src": "icons/icon-192x192.png",
11
+      "sizes": "192x192"
12
+    },
13
+    {
14
+      "src": "icons/icon-128x128.png",
15
+      "sizes": "128x128"
16
+    },
17
+    {
18
+      "src": "icons/icon-512x512.png",
19
+      "sizes": "512x512"
20
+    }
21
+  ]
22
+}

+ 18
- 0
src/pages/404.jsx ファイルの表示

@@ -0,0 +1,18 @@
1
+import { history } from '@umijs/max';
2
+import { Button, Result } from 'antd';
3
+import React from 'react';
4
+
5
+const NoFoundPage= () => (
6
+  <Result
7
+    status="404"
8
+    title="404"
9
+    subTitle="Sorry, the page you visited does not exist."
10
+    extra={
11
+      <Button type="primary" onClick={() => history.push('/')}>
12
+        Back Home
13
+      </Button>
14
+    }
15
+  />
16
+);
17
+
18
+export default NoFoundPage;

+ 45
- 0
src/pages/Admin.jsx ファイルの表示

@@ -0,0 +1,45 @@
1
+import { HeartTwoTone, SmileTwoTone } from '@ant-design/icons';
2
+import { PageHeaderWrapper } from '@ant-design/pro-components';
3
+import { useIntl } from '@umijs/max';
4
+import { Alert, Card, Typography } from 'antd';
5
+import React from 'react';
6
+
7
+const Admin = () => {
8
+  const intl = useIntl();
9
+  return (
10
+    <PageHeaderWrapper
11
+      content={intl.formatMessage({
12
+        id: 'pages.admin.subPage.title',
13
+        defaultMessage: 'This page can only be viewed by admin',
14
+      })}
15
+    >
16
+      <Card>
17
+        <Alert
18
+          message={intl.formatMessage({
19
+            id: 'pages.welcome.alertMessage',
20
+            defaultMessage: 'Faster and stronger heavy-duty components have been released.',
21
+          })}
22
+          type="success"
23
+          showIcon
24
+          banner
25
+          style={{
26
+            margin: -12,
27
+            marginBottom: 48,
28
+          }}
29
+        />
30
+        <Typography.Title level={2} style={{ textAlign: 'center' }}>
31
+          <SmileTwoTone /> Ant Design Pro <HeartTwoTone twoToneColor="#eb2f96" /> You
32
+        </Typography.Title>
33
+      </Card>
34
+      <p style={{ textAlign: 'center', marginTop: 24 }}>
35
+        Want to add more pages? Please refer to{' '}
36
+        <a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
37
+          use block
38
+        </a>
39
+        。
40
+      </p>
41
+    </PageHeaderWrapper>
42
+  );
43
+};
44
+
45
+export default Admin;

+ 118
- 0
src/pages/User/Login/index.jsx ファイルの表示

@@ -0,0 +1,118 @@
1
+import Footer from '@/components/Footer';
2
+import { login } from '@/services/api/login';
3
+import { LockOutlined, UserOutlined } from '@ant-design/icons';
4
+import { LoginForm, ProFormText } from '@ant-design/pro-components';
5
+import { history, useModel } from '@umijs/max';
6
+import { Alert, message, Tabs } from 'antd';
7
+import { useState } from 'react';
8
+import styles from './index.less';
9
+
10
+const LoginMessage = ({ content }) => {
11
+  return (
12
+    <Alert
13
+      style={{
14
+        marginBottom: 24,
15
+      }}
16
+      message={content}
17
+      type="error"
18
+      showIcon
19
+    />
20
+  );
21
+};
22
+
23
+const Login = () => {
24
+  const [userLoginState, setUserLoginState] = useState({});
25
+  const [type, setType] = useState('account');
26
+  const { initialState, setInitialState } = useModel('@@initialState');
27
+
28
+  const fetchUserInfo = async () => {
29
+    const userInfo = await initialState?.fetchUserInfo?.();
30
+    if (userInfo) {
31
+      await setInitialState((s) => ({
32
+        ...s,
33
+        currentUser: userInfo,
34
+      }));
35
+    }
36
+  };
37
+
38
+  const handleSubmit = async (values) => {
39
+    try {
40
+      // 登录
41
+      const msg = await login({ params: values });
42
+      console.log(msg);
43
+      if (msg.success === 'true') {
44
+        const defaultLoginSuccessMessage = '登录成功!';
45
+        message.success(defaultLoginSuccessMessage);
46
+        await fetchUserInfo();
47
+        const urlParams = new URL(window.location.href).searchParams;
48
+        history.push(urlParams.get('redirect') || '/');
49
+        return;
50
+      }
51
+
52
+      // 如果失败去设置用户错误信息
53
+      setUserLoginState(msg);
54
+    } catch (error) {
55
+      const defaultLoginFailureMessage = '登录失败,请重试!';
56
+      console.log(error);
57
+      message.error(defaultLoginFailureMessage);
58
+    }
59
+  };
60
+
61
+  return (
62
+    <div className={styles.container}>
63
+      <div className={styles.content}>
64
+        <LoginForm
65
+          // logo={<img alt="logo" src="/logo.svg" />}
66
+          title="军工站"
67
+          onFinish={async (values) => {
68
+            await handleSubmit(values);
69
+          }}
70
+        >
71
+          <Tabs activeKey={type} onChange={setType}>
72
+            <Tabs.TabPane key="account" tab="账户密码登录" />
73
+          </Tabs>
74
+
75
+          {userLoginState.success === 'false' && (
76
+            <LoginMessage content={userLoginState.errorMessage} />
77
+          )}
78
+
79
+          <>
80
+            <ProFormText
81
+              name="account"
82
+              fieldProps={{
83
+                size: 'large',
84
+                prefix: <UserOutlined className={styles.prefixIcon} />,
85
+              }}
86
+              placeholder="admin"
87
+              // placeholder="请输入用户名"
88
+              rules={[
89
+                {
90
+                  required: true,
91
+                  message: '请输入用户名!',
92
+                },
93
+              ]}
94
+            />
95
+            <ProFormText.Password
96
+              name="password"
97
+              fieldProps={{
98
+                size: 'large',
99
+                prefix: <LockOutlined className={styles.prefixIcon} />,
100
+              }}
101
+              placeholder="123456"
102
+              // placeholder="请输入密码!"
103
+              rules={[
104
+                {
105
+                  required: true,
106
+                  message: '请输入密码!',
107
+                },
108
+              ]}
109
+            />
110
+          </>
111
+        </LoginForm>
112
+      </div>
113
+      <Footer />
114
+    </div>
115
+  );
116
+};
117
+
118
+export default Login;

+ 48
- 0
src/pages/User/Login/index.less ファイルの表示

@@ -0,0 +1,48 @@
1
+@import (reference) '~antd/es/style/themes/index';
2
+
3
+.container {
4
+  display: flex;
5
+  flex-direction: column;
6
+  height: 100vh;
7
+  overflow: auto;
8
+  background: @layout-body-background;
9
+}
10
+
11
+.lang {
12
+  width: 100%;
13
+  height: 40px;
14
+  line-height: 44px;
15
+  text-align: right;
16
+  :global(.ant-dropdown-trigger) {
17
+    margin-right: 24px;
18
+  }
19
+}
20
+
21
+.content {
22
+  flex: 1;
23
+  padding: 32px 0;
24
+}
25
+
26
+@media (min-width: @screen-md-min) {
27
+  .container {
28
+    background-image: url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr');
29
+    background-size: cover;
30
+  }
31
+
32
+  .content {
33
+    padding: 32px 0 24px;
34
+  }
35
+}
36
+
37
+.icon {
38
+  margin-left: 8px;
39
+  color: rgba(0, 0, 0, 0.2);
40
+  font-size: 24px;
41
+  vertical-align: middle;
42
+  cursor: pointer;
43
+  transition: color 0.3s;
44
+
45
+  &:hover {
46
+    color: @primary-color;
47
+  }
48
+}

+ 17
- 0
src/pages/Welcome.jsx ファイルの表示

@@ -0,0 +1,17 @@
1
+import { PageContainer } from '@ant-design/pro-components';
2
+import { Typography } from 'antd';
3
+import styles from './Welcome.less';
4
+
5
+const CodePreview = ({ children }) => (
6
+  <pre className={styles.pre}>
7
+    <code>
8
+      <Typography.Text copyable>{children}</Typography.Text>
9
+    </code>
10
+  </pre>
11
+);
12
+
13
+const Welcome = () => {
14
+  return <PageContainer>234</PageContainer>;
15
+};
16
+
17
+export default Welcome;

+ 8
- 0
src/pages/Welcome.less ファイルの表示

@@ -0,0 +1,8 @@
1
+@import (reference) '~antd/es/style/themes/index';
2
+
3
+.pre {
4
+  margin: 12px 0;
5
+  padding: 12px 20px;
6
+  background: @input-bg;
7
+  box-shadow: @card-shadow;
8
+}

+ 236
- 0
src/pages/document.ejs ファイルの表示

@@ -0,0 +1,236 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+  <head>
4
+    <meta charset="UTF-8" />
5
+    <meta name="theme-color" content="#1890ff" />
6
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
7
+    <meta
8
+      name="keywords"
9
+      content="antd,umi,umijs,ant design,Scaffolding, layout, Ant Design, project, Pro, admin, console, homepage, out-of-the-box, middle and back office, solution, component library"
10
+    />
11
+    <meta
12
+      name="description"
13
+      content="
14
+    An out-of-box UI solution for enterprise applications as a React boilerplate."
15
+    />
16
+    <meta
17
+      name="description"
18
+      content="
19
+      Out-of-the-box mid-stage front-end/design solution."
20
+    />
21
+    <meta
22
+      name="viewport"
23
+      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
24
+    />
25
+    <title>军工站</title>
26
+    <link rel="icon" href="<%= context.config.publicPath +'favicon.ico'%>" type="image/x-icon" />
27
+  </head>
28
+  <body>
29
+    <noscript>
30
+      <div class="noscript-container">
31
+        Hi there! Please
32
+        <div class="noscript-enableJS">
33
+          <a href="https://www.enablejavascript.io/en" target="_blank" rel="noopener noreferrer">
34
+            <b>enable Javascript</b>
35
+          </a>
36
+        </div>
37
+        in your browser to use Ant Design, Out-of-the-box mid-stage front/design solution!
38
+      </div>
39
+    </noscript>
40
+    <div id="root">
41
+      <style>
42
+        html,
43
+        body,
44
+        #root {
45
+          height: 100%;
46
+          margin: 0;
47
+          padding: 0;
48
+        }
49
+        #root {
50
+          background-repeat: no-repeat;
51
+          background-size: 100% auto;
52
+        }
53
+        .noscript-container {
54
+          display: flex;
55
+          align-content: center;
56
+          justify-content: center;
57
+          margin-top: 90px;
58
+          font-size: 20px;
59
+          font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode',
60
+            Geneva, Verdana, sans-serif;
61
+        }
62
+        .noscript-enableJS {
63
+          padding-right: 3px;
64
+          padding-left: 3px;
65
+        }
66
+        .page-loading-warp {
67
+          display: flex;
68
+          align-items: center;
69
+          justify-content: center;
70
+          padding: 98px;
71
+        }
72
+        .ant-spin {
73
+          position: absolute;
74
+          display: none;
75
+          -webkit-box-sizing: border-box;
76
+          box-sizing: border-box;
77
+          margin: 0;
78
+          padding: 0;
79
+          color: rgba(0, 0, 0, 0.65);
80
+          color: #1890ff;
81
+          font-size: 14px;
82
+          font-variant: tabular-nums;
83
+          line-height: 1.5;
84
+          text-align: center;
85
+          list-style: none;
86
+          opacity: 0;
87
+          -webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
88
+          transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
89
+          transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
90
+          transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
91
+            -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
92
+          -webkit-font-feature-settings: 'tnum';
93
+          font-feature-settings: 'tnum';
94
+        }
95
+
96
+        .ant-spin-spinning {
97
+          position: static;
98
+          display: inline-block;
99
+          opacity: 1;
100
+        }
101
+
102
+        .ant-spin-dot {
103
+          position: relative;
104
+          display: inline-block;
105
+          width: 20px;
106
+          height: 20px;
107
+          font-size: 20px;
108
+        }
109
+
110
+        .ant-spin-dot-item {
111
+          position: absolute;
112
+          display: block;
113
+          width: 9px;
114
+          height: 9px;
115
+          background-color: #1890ff;
116
+          border-radius: 100%;
117
+          -webkit-transform: scale(0.75);
118
+          -ms-transform: scale(0.75);
119
+          transform: scale(0.75);
120
+          -webkit-transform-origin: 50% 50%;
121
+          -ms-transform-origin: 50% 50%;
122
+          transform-origin: 50% 50%;
123
+          opacity: 0.3;
124
+          -webkit-animation: antspinmove 1s infinite linear alternate;
125
+          animation: antSpinMove 1s infinite linear alternate;
126
+        }
127
+
128
+        .ant-spin-dot-item:nth-child(1) {
129
+          top: 0;
130
+          left: 0;
131
+        }
132
+
133
+        .ant-spin-dot-item:nth-child(2) {
134
+          top: 0;
135
+          right: 0;
136
+          -webkit-animation-delay: 0.4s;
137
+          animation-delay: 0.4s;
138
+        }
139
+
140
+        .ant-spin-dot-item:nth-child(3) {
141
+          right: 0;
142
+          bottom: 0;
143
+          -webkit-animation-delay: 0.8s;
144
+          animation-delay: 0.8s;
145
+        }
146
+
147
+        .ant-spin-dot-item:nth-child(4) {
148
+          bottom: 0;
149
+          left: 0;
150
+          -webkit-animation-delay: 1.2s;
151
+          animation-delay: 1.2s;
152
+        }
153
+
154
+        .ant-spin-dot-spin {
155
+          -webkit-transform: rotate(45deg);
156
+          -ms-transform: rotate(45deg);
157
+          transform: rotate(45deg);
158
+          -webkit-animation: antrotate 1.2s infinite linear;
159
+          animation: antRotate 1.2s infinite linear;
160
+        }
161
+
162
+        .ant-spin-lg .ant-spin-dot {
163
+          width: 32px;
164
+          height: 32px;
165
+          font-size: 32px;
166
+        }
167
+
168
+        .ant-spin-lg .ant-spin-dot i {
169
+          width: 14px;
170
+          height: 14px;
171
+        }
172
+
173
+        @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
174
+          .ant-spin-blur {
175
+            background: #fff;
176
+            opacity: 0.5;
177
+          }
178
+        }
179
+
180
+        @-webkit-keyframes antSpinMove {
181
+          to {
182
+            opacity: 1;
183
+          }
184
+        }
185
+
186
+        @keyframes antSpinMove {
187
+          to {
188
+            opacity: 1;
189
+          }
190
+        }
191
+
192
+        @-webkit-keyframes antRotate {
193
+          to {
194
+            -webkit-transform: rotate(405deg);
195
+            transform: rotate(405deg);
196
+          }
197
+        }
198
+
199
+        @keyframes antRotate {
200
+          to {
201
+            -webkit-transform: rotate(405deg);
202
+            transform: rotate(405deg);
203
+          }
204
+        }
205
+      </style>
206
+      <div
207
+        style="
208
+          display: flex;
209
+          flex-direction: column;
210
+          align-items: center;
211
+          justify-content: center;
212
+          height: 100%;
213
+          min-height: 420px;
214
+        "
215
+      >
216
+        <img src="<%= context.config.publicPath +'pro_icon.svg'%>" alt="logo" width="256" />
217
+        <div class="page-loading-warp">
218
+          <div class="ant-spin ant-spin-lg ant-spin-spinning">
219
+            <span class="ant-spin-dot ant-spin-dot-spin"
220
+              ><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
221
+              ><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
222
+            ></span>
223
+          </div>
224
+        </div>
225
+        <div style="display: flex; align-items: center; justify-content: center">
226
+          <img
227
+            src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
228
+            width="32"
229
+            style="margin-right: 8px"
230
+          />
231
+          Ant Design
232
+        </div>
233
+      </div>
234
+    </div>
235
+  </body>
236
+</html>

+ 161
- 0
src/pages/rotationChart/edit/index.jsx ファイルの表示

@@ -0,0 +1,161 @@
1
+import UploadImage from '@/components/UploadImage';
2
+import UploadVideo from '@/components/UploadVideo';
3
+import { addBanner, getBannerdById, updataBanner } from '@/services/api/rotationChart';
4
+import {
5
+  PageContainer,
6
+  ProForm,
7
+  ProFormDigit,
8
+  ProFormRadio,
9
+  ProFormSelect,
10
+  ProFormTextArea,
11
+} from '@ant-design/pro-components';
12
+import { history, useSearchParams } from '@umijs/max';
13
+import { Card, Col, message, Row, Space } from 'antd';
14
+import { useEffect, useRef, useState } from 'react';
15
+
16
+export default (props) => {
17
+  const [searchParams, setSearchParams] = useSearchParams();
18
+  const id = searchParams.get('id');
19
+  const [data, setData] = useState({});
20
+  const formRef = useRef();
21
+  useEffect(() => {
22
+    if (id) {
23
+      getBannerdById(id).then((res) => {
24
+        console.log(res, '====---');
25
+        setData(res);
26
+
27
+        formRef.current.setFieldsValue(res);
28
+      });
29
+    }
30
+  }, [id]);
31
+
32
+  const onFinish = async (values) => {
33
+    if (id) {
34
+      // 修改
35
+      updataBanner(id, { ...values }).then((res) => {
36
+        message.success('修改成功');
37
+        history.back();
38
+      });
39
+    } else {
40
+      // 新增
41
+      addBanner({ ...values }).then((res) => {
42
+        message.success('添加成功');
43
+        history.back();
44
+      });
45
+    }
46
+
47
+    return false;
48
+  };
49
+
50
+  return (
51
+    <PageContainer>
52
+      <Card>
53
+        <ProForm
54
+          formRef={formRef}
55
+          layout={'horizontal'}
56
+          labelCol={{ span: 8 }}
57
+          wrapperCol={{ span: 16 }}
58
+          onFinish={onFinish}
59
+          initialValues={{ type: '1', state: '1' }}
60
+          submitter={{
61
+            searchConfig: {
62
+              resetText: '返回',
63
+            },
64
+            onReset: () => history.back(),
65
+            render: (props, doms) => {
66
+              return (
67
+                <Row>
68
+                  <Col span={8} offset={8}>
69
+                    <Space>{doms}</Space>
70
+                  </Col>
71
+                </Row>
72
+              );
73
+            },
74
+          }}
75
+        >
76
+          <ProFormSelect
77
+            name="type"
78
+            width="md"
79
+            label="轮播图类型"
80
+            valueEnum={{
81
+              1: '图片',
82
+              2: '视频',
83
+            }}
84
+            placeholder="轮播图类型"
85
+            rules={[{ required: true, message: 'Please select your country!' }]}
86
+          />
87
+          <ProForm.Item
88
+            width="md"
89
+            name="image"
90
+            label="图片"
91
+            // extra="longgggggggggggggggggggggggggggggggggg"
92
+            // rules={[{ required: true, message: '请上传图片' }]}
93
+          >
94
+            <UploadImage />
95
+          </ProForm.Item>
96
+          <ProForm.Item noStyle shouldUpdate>
97
+            {(form) => {
98
+              console.log(form, '==');
99
+
100
+              return form.getFieldValue('type') == '1' ? null : (
101
+                <ProForm.Item
102
+                  width="md"
103
+                  name="video"
104
+                  label="视频"
105
+                  // extra="longgggggggggggggggggggggggggggggggggg"
106
+                  // rules={[{ required: true, message: '请上传图片' }]}
107
+                >
108
+                  <UploadVideo />
109
+                </ProForm.Item>
110
+              );
111
+            }}
112
+          </ProForm.Item>
113
+          <ProFormRadio.Group
114
+            name="state"
115
+            label="状态"
116
+            radioType="button"
117
+            options={[
118
+              {
119
+                label: '上架',
120
+                value: '1',
121
+              },
122
+              {
123
+                label: '下架',
124
+                value: '2',
125
+              },
126
+            ]}
127
+          />
128
+          <ProFormDigit name="qz" label="权重" width="md" />
129
+          <ProFormTextArea width="md" name="desc" label="描述" placeholder="请输入描述" />
130
+        </ProForm>
131
+      </Card>
132
+    </PageContainer>
133
+    // <ModalForm
134
+    //   title={`${butttomTitle}`}
135
+    //   trigger={
136
+    //     <Button type="primary" {...butttomProps}>
137
+    //       {butttomTitle}
138
+    //     </Button>
139
+    //   }
140
+    //   autoFocusFirstInput
141
+    //   initialValues={{ type: '1', ...data }}
142
+    //   modalProps={{
143
+    //     onCancel: () => console.log('run'),
144
+    //   }}
145
+    //   submitTimeout={2000}
146
+    //   onFinish={onFinish}
147
+    // >
148
+
149
+    //   <ProFormText width="md" name="platform" label="平台名称" placeholder="请输入平台名称" />
150
+    //   <ProFormText width="md" name="ipAddr" label="IP地址" placeholder="请输入IP地址" />
151
+    //   {/* <ProFormText
152
+    //     width="md"
153
+    //     name="name"
154
+    //     label="推送频率"
155
+    //     tooltip="最长为 24 位"
156
+    //     placeholder="请输入名称"
157
+    //   /> */}
158
+    //   <ProFormTimePicker width="md" name="pushTime" label="推送时间" />
159
+    // </ModalForm>
160
+  );
161
+};

+ 153
- 0
src/pages/rotationChart/list/index.jsx ファイルの表示

@@ -0,0 +1,153 @@
1
+import { deleteBanner, getBannerList, updataBanner } from '@/services/api/rotationChart';
2
+import { queryTable } from '@/utils/request';
3
+import { PageContainer, ProTable } from '@ant-design/pro-components';
4
+import { history } from '@umijs/max';
5
+import { Button, message, Popconfirm } from 'antd';
6
+import { useRef, useState } from 'react';
7
+
8
+const RotationChartList = (props) => {
9
+  console.log(props, '===');
10
+  const [showDetail, setShowDetail] = useState(false);
11
+  const [activeKey, setActiveKey] = useState('');
12
+  const actionRef = useRef();
13
+
14
+  const updata = (row) => {
15
+    if (row.id) {
16
+      updataBanner(row.id, { state: row.state === '1' ? '2' : '1' }).then((res) => {
17
+        message.success('修改成功');
18
+        actionRef.current.reload();
19
+      });
20
+    }
21
+  };
22
+
23
+  const handleDelete = (id) => {
24
+    if (id) {
25
+      deleteBanner(id).then((res) => {
26
+        message.success('删除成功');
27
+        actionRef.current.reload();
28
+      });
29
+    }
30
+  };
31
+
32
+  const columns = [
33
+    {
34
+      title: 'id',
35
+      dataIndex: 'id',
36
+    },
37
+    {
38
+      title: '类型',
39
+      dataIndex: 'type',
40
+      valueEnum: {
41
+        1: '图片',
42
+        2: '视频',
43
+      },
44
+    },
45
+
46
+    {
47
+      title: '封面图',
48
+      dataIndex: 'image',
49
+    },
50
+
51
+    {
52
+      title: '视频',
53
+      dataIndex: 'video',
54
+    },
55
+    {
56
+      title: '状态',
57
+      dataIndex: 'state',
58
+      valueEnum: {
59
+        1: {
60
+          text: '上架',
61
+          status: 'Processing',
62
+        },
63
+        2: {
64
+          text: '下架',
65
+          status: 'Error',
66
+        },
67
+      },
68
+    },
69
+    // {
70
+    //   title: '描述',
71
+    //   dataIndex: 'desc',
72
+    // },
73
+    {
74
+      title: '权重',
75
+      dataIndex: 'qz',
76
+    },
77
+    {
78
+      title: '操作',
79
+      valueType: 'option',
80
+      width: 200,
81
+      render: (_, record) => [
82
+        <Button
83
+          key={1}
84
+          style={{ padding: 0 }}
85
+          type="link"
86
+          onClick={() => {
87
+            updata(record);
88
+          }}
89
+        >
90
+          {record.state === '1' ? '下架' : '上架'}
91
+        </Button>,
92
+        <Button
93
+          key={2}
94
+          style={{ padding: 0 }}
95
+          type="link"
96
+          onClick={() => {
97
+            console.log(record, ']]');
98
+            history.push(`/rotationChart/add?id=${record.id}`);
99
+          }}
100
+        >
101
+          编辑
102
+        </Button>,
103
+
104
+        <Popconfirm
105
+          key={3}
106
+          title="您是否确认删除 ?"
107
+          onConfirm={() => handleDelete(record.id)}
108
+          okText="确定"
109
+          cancelText="取消"
110
+        >
111
+          {/* manualPush */}
112
+          <Button style={{ padding: 0 }} type="link">
113
+            删除
114
+          </Button>
115
+        </Popconfirm>,
116
+      ],
117
+    },
118
+  ];
119
+
120
+  return (
121
+    <PageContainer>
122
+      <ProTable
123
+        // headerTitle={'中高风险地区库'}
124
+        search={false}
125
+        actionRef={actionRef}
126
+        rowKey="id"
127
+        toolBarRender={() => [
128
+          <Button
129
+            key="2"
130
+            type="primary"
131
+            onClick={() => {
132
+              history.push('/rotationChart/add');
133
+            }}
134
+          >
135
+            新增
136
+          </Button>,
137
+          // <EditForm
138
+          //   key="2"
139
+          //   type="add"
140
+          //   butttomTitle="新增轮播图"
141
+          //   onSuccess={() => actionRef.current.reload()}
142
+          //   butttomProps={{}}
143
+          // />,
144
+        ]}
145
+        // search={false}
146
+        request={queryTable(getBannerList)}
147
+        columns={columns}
148
+      />
149
+    </PageContainer>
150
+  );
151
+};
152
+
153
+export default RotationChartList;

+ 65
- 0
src/service-worker.js ファイルの表示

@@ -0,0 +1,65 @@
1
+/* eslint-disable no-restricted-globals */
2
+/* eslint-disable no-underscore-dangle */
3
+/* globals workbox */
4
+workbox.core.setCacheNameDetails({
5
+  prefix: 'antd-pro',
6
+  suffix: 'v5',
7
+});
8
+// Control all opened tabs ASAP
9
+workbox.clientsClaim();
10
+
11
+/**
12
+ * Use precaching list generated by workbox in build process.
13
+ * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching
14
+ */
15
+workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
16
+
17
+/**
18
+ * Register a navigation route.
19
+ * https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route
20
+ */
21
+workbox.routing.registerNavigationRoute('/index.html');
22
+
23
+/**
24
+ * Use runtime cache:
25
+ * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute
26
+ *
27
+ * Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc.
28
+ * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies
29
+ */
30
+
31
+/** Handle API requests */
32
+workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst());
33
+
34
+/** Handle third party requests */
35
+workbox.routing.registerRoute(
36
+  /^https:\/\/gw\.alipayobjects\.com\//,
37
+  workbox.strategies.networkFirst(),
38
+);
39
+workbox.routing.registerRoute(
40
+  /^https:\/\/cdnjs\.cloudflare\.com\//,
41
+  workbox.strategies.networkFirst(),
42
+);
43
+workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst());
44
+
45
+/** Response to client after skipping waiting with MessageChannel */
46
+addEventListener('message', (event) => {
47
+  const replyPort = event.ports[0];
48
+  const message = event.data;
49
+  if (replyPort && message && message.type === 'skip-waiting') {
50
+    event.waitUntil(
51
+      self.skipWaiting().then(
52
+        () => {
53
+          replyPort.postMessage({
54
+            error: null,
55
+          });
56
+        },
57
+        (error) => {
58
+          replyPort.postMessage({
59
+            error,
60
+          });
61
+        },
62
+      ),
63
+    );
64
+  }
65
+});

+ 30
- 0
src/services/api/login.js ファイルの表示

@@ -0,0 +1,30 @@
1
+import { request } from '@umijs/max';
2
+/** 登录接口 POST /v1/admin_api/login */
3
+// export async function login(options) {
4
+//   return request('login', {
5
+//     method: 'GET',
6
+//     // headers: {
7
+//     //   'Content-Type': 'application/json',
8
+//     // },
9
+//     responseInterceptors: [
10
+//       (response) => {
11
+//         const { data = {} } = response;
12
+//         const { jwt = {} } = data;
13
+//         if (jwt.token) {
14
+//           sessionStorage.setItem('token', jwt?.token);
15
+//         }
16
+
17
+//         // do something
18
+//         return response;
19
+//       },
20
+//     ],
21
+//     ...(options || {}),
22
+//   });
23
+// }
24
+
25
+/** 获取当前的用户 GET /currentUser */
26
+export async function currentUser() {
27
+  return request('base/currentUser', {
28
+    method: 'GET',
29
+  });
30
+}

+ 37
- 0
src/services/api/rotationChart.js ファイルの表示

@@ -0,0 +1,37 @@
1
+import { request } from '@umijs/max';
2
+
3
+/**
4
+ * 查询列表
5
+ * @param {*} params
6
+ * @returns
7
+ */
8
+export const getBannerList = (params) => request('/banner', { params });
9
+
10
+/**
11
+ * 详情
12
+ * @param {*} id
13
+ * @returns
14
+ */
15
+export const getBannerdById = (id) => request(`/banner/${id}`);
16
+
17
+/**
18
+ * 新增
19
+ * @param {*} data
20
+ * @returns
21
+ */
22
+export const addBanner = (data) => request('/banner', { method: 'post', data });
23
+
24
+/**
25
+ * 新增
26
+ *  @param {*} id
27
+ * @param {*} data
28
+ * @returns
29
+ */
30
+export const updataBanner = (id, data) => request(`/banner/${id}`, { method: 'put', data });
31
+
32
+/**
33
+ * 删除
34
+ * @param {*} id
35
+ * @returns
36
+ */
37
+export const deleteBanner = (id) => request(`/banner/${id}`, { method: 'delete' });

+ 24
- 0
src/typings.d.ts ファイルの表示

@@ -0,0 +1,24 @@
1
+declare module 'slash2';
2
+declare module '*.css';
3
+declare module '*.less';
4
+declare module '*.scss';
5
+declare module '*.sass';
6
+declare module '*.svg';
7
+declare module '*.png';
8
+declare module '*.jpg';
9
+declare module '*.jpeg';
10
+declare module '*.gif';
11
+declare module '*.bmp';
12
+declare module '*.tiff';
13
+declare module 'omit.js';
14
+declare module 'numeral';
15
+declare module '@antv/data-set';
16
+declare module 'mockjs';
17
+declare module 'react-fittext';
18
+declare module 'bizcharts-plugin-slider';
19
+
20
+// preview.pro.ant.design only do not use in your production ;
21
+// preview.pro.ant.design Dedicated environment variable, please do not use it in your project.
22
+declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined;
23
+
24
+declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;

+ 16
- 0
src/utils/download.js ファイルの表示

@@ -0,0 +1,16 @@
1
+export function fetchBlob(url) {
2
+  return window.fetch(url).then((response) => response.blob());
3
+}
4
+
5
+export function downloadBlob(blob, fileName) {
6
+  const url = window.URL.createObjectURL(blob);
7
+  const link = document.createElement('a');
8
+  link.href = url;
9
+  link.setAttribute('download', fileName);
10
+  link.click();
11
+  window.URL.revokeObjectURL(url);
12
+}
13
+
14
+export function downloadUrl(url, fileName) {
15
+  return fetchBlob(url).then((blob) => downloadBlob(blob, fileName));
16
+}

+ 68
- 0
src/utils/request.js ファイルの表示

@@ -0,0 +1,68 @@
1
+import { message } from 'antd';
2
+
3
+function requestInterceptor(url, options) {
4
+  const headers = options.headers || {};
5
+  // const token = sessionStorage.getItem('token')
6
+  //   ? { Authorization: `Bearer ${sessionStorage.getItem('token')}` }
7
+  //   : {};
8
+
9
+  return {
10
+    // url,
11
+    url: `/api${url}`,
12
+    options: {
13
+      ...options,
14
+      headers: {
15
+        ...headers,
16
+        // ...token,
17
+      },
18
+    },
19
+  };
20
+}
21
+
22
+function responseInterceptor(response) {
23
+  console.log(response, '44114');
24
+  const { data } = response;
25
+
26
+  console.log(data, data.data, '--==666=');
27
+  if (data.code === 1000) {
28
+    return data;
29
+  } else {
30
+    message.error(data.message || '系统错误');
31
+  }
32
+  return Promise.reject(data);
33
+}
34
+
35
+export const requestConfig = {
36
+  requestInterceptors: [requestInterceptor],
37
+  responseInterceptors: [responseInterceptor],
38
+};
39
+
40
+export function queryTable(apiRequest) {
41
+  return function (params) {
42
+    return apiRequest({
43
+      ...params,
44
+      pageNum: params.current,
45
+    })
46
+      .then((res) => {
47
+        console.log(res, '--000');
48
+        return {
49
+          data: res.records,
50
+          success: true,
51
+          total: res.total,
52
+        };
53
+      })
54
+      .catch((err) => {
55
+        return {
56
+          success: false,
57
+        };
58
+      });
59
+  };
60
+}
61
+// return {
62
+//   data: msg.result,
63
+//   // success 请返回 true,
64
+//   // 不然 table 会停止解析数据,即使有数据
65
+//   success: boolean,
66
+//   // 不传会使用 data 的长度,如果是分页一定要传
67
+//   total: number,
68
+// };

+ 47
- 0
tests/run-tests.js ファイルの表示

@@ -0,0 +1,47 @@
1
+/* eslint-disable @typescript-eslint/no-var-requires */
2
+const { spawn } = require('child_process');
3
+const { kill } = require('cross-port-killer');
4
+
5
+const env = Object.create(process.env);
6
+env.BROWSER = 'none';
7
+env.TEST = true;
8
+env.UMI_UI = 'none';
9
+env.PROGRESS = 'none';
10
+// flag to prevent multiple test
11
+let once = false;
12
+
13
+const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['run', 'serve'], {
14
+  env,
15
+});
16
+
17
+startServer.stderr.on('data', (data) => {
18
+  // eslint-disable-next-line
19
+  console.log(data.toString());
20
+});
21
+
22
+startServer.on('exit', () => {
23
+  kill(process.env.PORT || 8000);
24
+});
25
+
26
+console.log('Starting development server for e2e tests...');
27
+startServer.stdout.on('data', (data) => {
28
+  console.log(data.toString());
29
+  // hack code , wait umi
30
+  if (!once && data.toString().indexOf('Serving your umi project!') >= 0) {
31
+    // eslint-disable-next-line
32
+    once = true;
33
+    console.log('Development server is started, ready to run tests.');
34
+    const testCmd = spawn(
35
+      /^win/.test(process.platform) ? 'npm.cmd' : 'npm',
36
+      ['run', 'playwright'],
37
+      {
38
+        stdio: 'inherit',
39
+      },
40
+    );
41
+    testCmd.on('exit', (code) => {
42
+      console.log('服务已经退出,退出码:', code);
43
+      startServer.kill();
44
+      process.exit(code);
45
+    });
46
+  }
47
+});

+ 10
- 0
tests/setupTests.js ファイルの表示

@@ -0,0 +1,10 @@
1
+// do some test init
2
+
3
+const localStorageMock = {
4
+  getItem: jest.fn(),
5
+  setItem: jest.fn(),
6
+  removeItem: jest.fn(),
7
+  clear: jest.fn(),
8
+};
9
+
10
+global.localStorage = localStorageMock;

+ 41
- 0
tsconfig.json ファイルの表示

@@ -0,0 +1,41 @@
1
+{
2
+  "compilerOptions": {
3
+    "outDir": "build/dist",
4
+    "module": "esnext",
5
+    "target": "esnext",
6
+    "lib": ["esnext", "dom"],
7
+    "sourceMap": true,
8
+    "baseUrl": ".",
9
+    "jsx": "react-jsx",
10
+    "resolveJsonModule": true,
11
+    "allowSyntheticDefaultImports": true,
12
+    "moduleResolution": "node",
13
+    "forceConsistentCasingInFileNames": true,
14
+    "noImplicitReturns": true,
15
+    "suppressImplicitAnyIndexErrors": true,
16
+    "noUnusedLocals": true,
17
+    "allowJs": true,
18
+    "skipLibCheck": true,
19
+    "experimentalDecorators": true,
20
+    "strict": true,
21
+    "paths": {
22
+      "@/*": ["./src/*"],
23
+      "@@/*": ["./src/.umi/*"]
24
+    }
25
+  },
26
+  "include": [
27
+    "mock/**/*",
28
+    "src/**/*",
29
+    "playwright.config.ts",
30
+    "tests/**/*",
31
+    "test/**/*",
32
+    "__test__/**/*",
33
+    "typings/**/*",
34
+    "config/**/*",
35
+    ".eslintrc.js",
36
+    ".prettierrc.js",
37
+    "jest.config.js",
38
+    "mock/*"
39
+  ],
40
+  "exclude": ["node_modules", "build", "dist", "scripts", "src/.umi/*", "webpack", "jest"]
41
+}