Procházet zdrojové kódy

Merge branch 'dev' of HuangKaiSheng/seecoder-devcloud-frontend into master

HuangKaiSheng před 5 roky
rodič
revize
ab7926a243
61 změnil soubory, kde provedl 2512 přidání a 1401 odebrání
  1. 1 0
      .eslintrc.js
  2. 1 0
      Dockerfile
  3. 2 0
      mock/index.js
  4. 1 10
      mock/routers/buglist.js
  5. 45 0
      mock/routers/commits.js
  6. 14 0
      mock/routers/deployment.js
  7. 59 26
      mock/routers/pipeline.js
  8. 17 17
      mock/routers/project.js
  9. 89 0
      mock/routers/stage.js
  10. 254 210
      mock/routers/tree.js
  11. 8 2
      mock/routers/user.js
  12. 267 238
      package-lock.json
  13. 1 0
      package.json
  14. 11 1
      src/App.vue
  15. 20 0
      src/api/devcloud/apitest.ts
  16. 5 0
      src/api/devcloud/autotest.ts
  17. 8 2
      src/api/devcloud/buglist.ts
  18. 33 0
      src/api/devcloud/functest.ts
  19. 26 8
      src/api/devcloud/pipeline.ts
  20. 4 0
      src/api/devcloud/prefix.ts
  21. 8 3
      src/api/devcloud/project.ts
  22. 7 1
      src/api/devcloud/tree.ts
  23. 4 0
      src/api/index.ts
  24. 0 0
      src/assets/icon/pipeline.svg
  25. 1 0
      src/assets/icon/test-hover.svg
  26. 1 0
      src/assets/icon/test-using.svg
  27. 1 0
      src/assets/icon/test.svg
  28. 0 120
      src/components/APITestInfoForm.vue
  29. 6 2
      src/components/BugAddDrawer.vue
  30. 11 6
      src/components/BugEditDrawer.vue
  31. 2 2
      src/components/CommonDrawer.vue
  32. 11 4
      src/components/GeneralTable.vue
  33. 0 187
      src/components/List.vue
  34. 0 121
      src/components/ListFilter.vue
  35. 9 3
      src/components/Menu.vue
  36. 0 83
      src/components/MissionDetailsForm.vue
  37. 7 3
      src/components/PriorityTag.vue
  38. 32 11
      src/components/RequirementDrawer.vue
  39. 0 68
      src/components/RightForm.vue
  40. 11 11
      src/components/SideBar.vue
  41. 3 0
      src/components/StateTag.vue
  42. 25 14
      src/components/Tree.vue
  43. 20 4
      src/components/TreeNode.vue
  44. 5 1
      src/composable/useProject.ts
  45. 1 1
      src/composable/useUser.ts
  46. 50 42
      src/element-plus.ts
  47. 0 1
      src/main.ts
  48. 1 0
      src/store/dispatchers/user.ts
  49. 8 7
      src/store/modules/user.ts
  50. 34 7
      src/store/modules/workspace.ts
  51. 3 3
      src/typings/project.d.ts
  52. 15 2
      src/utils/axios.ts
  53. 1 1
      src/utils/constants.ts
  54. 0 1
      src/utils/jsonlint.js
  55. 11 0
      src/utils/time.ts
  56. 965 95
      src/views/Project/APITest.vue
  57. 8 2
      src/views/Project/BugList.vue
  58. 219 72
      src/views/Project/Pipeline.vue
  59. 138 7
      src/views/Project/Schedule.vue
  60. 8 2
      src/views/Project/TaskList.vue
  61. 20 0
      yarn.lock

+ 1 - 0
.eslintrc.js

@@ -19,5 +19,6 @@ module.exports = {
     'no-param-reassign': [2, { props: false }],
     'linebreak-style': [0, 'error', 'windows'],
     'import/no-cycle': 0,
+    '@typescript-eslint/no-this-alias': ['off'],
   },
 };

+ 1 - 0
Dockerfile

@@ -2,6 +2,7 @@ FROM node:10-alpine as builder
 WORKDIR /code
 RUN yarn config set registry https://registry.npm.taobao.org/
 ADD package.json /code
+ADD yarn.lock /code
 RUN yarn install
 ADD . /code
 RUN yarn build:prod

+ 2 - 0
mock/index.js

@@ -28,6 +28,8 @@ app.use(`${prefix}/pipelines`, require('./routers/pipeline'));
 app.use(`${prefix}/deployments`, require('./routers/deployment'));
 app.use(`${prefix}/tree`, require('./routers/tree'));
 app.use(`${prefix}/bug_list`, require('./routers/buglist'));
+app.use(`${prefix}/stages`, require('./routers/stage'));
+app.use(`${prefix}/commits`, require('./routers/commits'));
 
 app.listen(4000);
 

+ 1 - 10
mock/routers/buglist.js

@@ -23,15 +23,6 @@ router.get('/list', (req, res) => {
   res.json(wrapper(Mock.mock(
     {
       'data|5-10': [{
-        'commits|0-10': [
-          {
-            'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
-            'id|+1': 101,
-            message: 'git commit message',
-            timestamp: Random.datetime(),
-            title: 'titleeee',
-          },
-        ],
         description: 'bug description',
         endTime: Random.datetime(),
         timeToTake: 140,
@@ -40,7 +31,7 @@ router.get('/list', (req, res) => {
         processorId: 1001,
         processorName: 'hkshu12',
         startTime: Random.datetime(),
-        'state|1': ['已创建', '进行中', '已完成'],
+        'state|1': ['已创建', '进行中', '已完成', '已结束'],
         title: 'bug titleeee',
       }],
     },

+ 45 - 0
mock/routers/commits.js

@@ -0,0 +1,45 @@
+/* eslint-disable import/no-extraneous-dependencies */
+/* eslint-disable @typescript-eslint/no-var-requires */
+const router = require('express').Router();
+const Mock = require('mockjs');
+const wrapper = require('../wrapper');
+
+const { Random } = Mock;
+
+router.get('/bug_list', (req, res) => {
+  res.json(wrapper(
+    Mock.mock(
+      {
+        'commits|0-10': [
+          {
+            'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
+            'id|+1': 101,
+            message: 'git commit message',
+            timestamp: Random.datetime(),
+            title: 'titleeee',
+          },
+        ],
+      },
+    ).commits,
+  ));
+});
+
+router.get('/tree', (req, res) => {
+  res.json(wrapper(
+    Mock.mock(
+      {
+        'commits|0-10': [
+          {
+            'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
+            'id|+1': 101,
+            message: 'git commit message',
+            timestamp: Random.datetime(),
+            title: 'titleeee',
+          },
+        ],
+      },
+    ).commits,
+  ));
+});
+
+module.exports = router;

+ 14 - 0
mock/routers/deployment.js

@@ -1,3 +1,4 @@
+/* eslint-disable max-len */
 /* eslint-disable import/no-extraneous-dependencies */
 /* eslint-disable @typescript-eslint/no-var-requires */
 const router = require('express').Router();
@@ -31,4 +32,17 @@ router.get('/list', (req, res) => {
   ));
 });
 
+router.get('/log', (req, res) => {
+  res.json(wrapper(`2021-03-16 00:01:58.621  INFO 20584 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
+2021-03-16 00:01:58.622  INFO 20584 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Context refreshed
+2021-03-16 00:01:58.634  INFO 20584 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Found 1 custom documentation plugin(s)
+2021-03-16 00:01:58.641  INFO 20584 --- [           main] s.d.s.w.s.ApiListingReferenceScanner     : Scanning for api listing references
+2021-03-16 00:01:58.974  INFO 20584 --- [           main] DeferredRepositoryInitializationListener : Triggering deferred initialization of Spring Data repositories…
+2021-03-16 00:01:58.975  INFO 20584 --- [           main] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
+2021-03-16 00:01:58.993  INFO 20584 --- [           main] s.devcloud.web.DevcloudWebApplication    : Started DevcloudWebApplication in 6.659 seconds (JVM running for 8.913)
+2021-03-16 00:12:39.211  INFO 20584 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
+2021-03-16 00:12:39.211  INFO 20584 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
+2021-03-16 00:12:39.222  INFO 20584 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 11 ms`));
+});
+
 module.exports = router;

+ 59 - 26
mock/routers/pipeline.js

@@ -13,14 +13,42 @@ router.delete('', (req, res) => {
 
 router.get('', (req, res) => {
   res.json(wrapper({
-    configJson: {
-      code: 0,
-      data: {
-        configJson: 'string',
-        id: 0,
+    configJson: JSON.stringify([
+      {
+        name: 'java-image-build',
+        active: true,
+        configs: {
+          repoUrl: '#todo',
+          branchName: '#todo',
+          jarName: '#todo',
+        },
+      },
+      {
+        name: 'k8s-namespace',
+        active: true,
+        configs: {
+        },
+      },
+      {
+        name: 'k8s-deployment',
+        active: true,
+        configs: {
+          port: '#todo',
+        },
+      },
+      {
+        name: 'k8s-service',
+        active: true,
+        configs: {
+        },
+      },
+      {
+        name: 'k8s-ingress',
+        active: true,
+        configs: {
+        },
       },
-      msg: 'string',
-    },
+    ]),
     id: req.params.pipelineId,
   }));
 });
@@ -37,24 +65,14 @@ router.put('/config', (req, res) => {
   res.json(wrapper('success'));
 });
 
-router.get('/history/list', (req, res) => {
+router.get('/record/list', (req, res) => {
   res.json(wrapper(
     Mock.mock(
       {
         'data|10': [{
-          details: `2021-03-16 00:01:58.621  INFO 20584 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
-2021-03-16 00:01:58.622  INFO 20584 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Context refreshed
-2021-03-16 00:01:58.634  INFO 20584 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Found 1 custom documentation plugin(s)
-2021-03-16 00:01:58.641  INFO 20584 --- [           main] s.d.s.w.s.ApiListingReferenceScanner     : Scanning for api listing references
-2021-03-16 00:01:58.974  INFO 20584 --- [           main] DeferredRepositoryInitializationListener : Triggering deferred initialization of Spring Data repositories…
-2021-03-16 00:01:58.975  INFO 20584 --- [           main] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
-2021-03-16 00:01:58.993  INFO 20584 --- [           main] s.devcloud.web.DevcloudWebApplication    : Started DevcloudWebApplication in 6.659 seconds (JVM running for 8.913)
-2021-03-16 00:12:39.211  INFO 20584 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
-2021-03-16 00:12:39.211  INFO 20584 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
-2021-03-16 00:12:39.222  INFO 20584 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 11 ms`,
           'id|+5': 101,
           pipelineId: 100,
-          'result|1': ['成功', '失败', '执行中'],
+          'result|1': ['成功', '失败', '执行中', '无执行记录'],
           startTime: Random.datetime(),
           username: 'hkshu12',
         }],
@@ -63,12 +81,20 @@ router.get('/history/list', (req, res) => {
   ));
 });
 
-router.get('/list', (req, res) => {
+router.get('/record', (req, res) => {
   res.json(wrapper(
-    Mock.mock(
-      {
-        'data|10': [{
-          details: `2021-03-16 00:01:58.621  INFO 20584 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
+    Mock.mock({
+      'id|+5': 101,
+      pipelineId: 100,
+      'result|1': ['成功', '失败', '执行中'],
+      startTime: Random.datetime(),
+      username: 'hkshu12',
+    }).data,
+  ));
+});
+
+router.get('/record/details', (req, res) => {
+  res.json(wrapper(`2021-03-16 00:01:58.621  INFO 20584 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
 2021-03-16 00:01:58.622  INFO 20584 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Context refreshed
 2021-03-16 00:01:58.634  INFO 20584 --- [           main] d.s.w.p.DocumentationPluginsBootstrapper : Found 1 custom documentation plugin(s)
 2021-03-16 00:01:58.641  INFO 20584 --- [           main] s.d.s.w.s.ApiListingReferenceScanner     : Scanning for api listing references
@@ -77,10 +103,17 @@ router.get('/list', (req, res) => {
 2021-03-16 00:01:58.993  INFO 20584 --- [           main] s.devcloud.web.DevcloudWebApplication    : Started DevcloudWebApplication in 6.659 seconds (JVM running for 8.913)
 2021-03-16 00:12:39.211  INFO 20584 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
 2021-03-16 00:12:39.211  INFO 20584 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
-2021-03-16 00:12:39.222  INFO 20584 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 11 ms`,
+2021-03-16 00:12:39.222  INFO 20584 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 11 ms`));
+});
+
+router.get('/list', (req, res) => {
+  res.json(wrapper(
+    Mock.mock(
+      {
+        'data|10': [{
           'id|+1': 100,
           name: Random.cname(),
-          'result|1': ['成功', '失败', '执行中'],
+          'result|1': ['成功', '失败', '执行中', '无执行记录'],
           startTime: Random.datetime(),
           username: 'hkshu12',
         }],

+ 17 - 17
mock/routers/project.js

@@ -23,6 +23,23 @@ router.get('/listByUser',
     ));
   });
 
+router.get('/members', (req, res) => {
+  res.json(wrapper(
+    Mock.mock({
+      'data|4': [
+        {
+          email: Random.email('smail.nju.edu.cn'),
+          'id|+1': 1110,
+          phone: 'string',
+          'role|1': ['学生', '教师', '管理员'],
+          'username|1': ['hkshu12', 'cyt123', 'Jinyao'],
+          withGitlab: Random.boolean(),
+        },
+      ],
+    }).data,
+  ));
+});
+
 router.get('/:projectId',
   (req, res) => {
     const { projectId: id } = req.params;
@@ -50,23 +67,6 @@ router.post('/members', (req, res) => {
   res.json(wrapper('success'));
 });
 
-router.get('/:projectId/members', (req, res) => {
-  res.json(wrapper(
-    Mock.mock({
-      'data|4': [
-        {
-          email: Random.email('smail.nju.edu.cn'),
-          'id|+1': 1110,
-          phone: 'string',
-          'role|1': ['学生', '教师', '管理员'],
-          'username|1': ['hkshu12', 'cyt123', 'Jinyao'],
-          withGitlab: Random.boolean(),
-        },
-      ],
-    }).data,
-  ));
-});
-
 router.get('/refresh', (req, res) => {
   res.json(wrapper('success'));
 });

+ 89 - 0
mock/routers/stage.js

@@ -0,0 +1,89 @@
+/* eslint-disable import/no-extraneous-dependencies */
+/* eslint-disable @typescript-eslint/no-var-requires */
+const router = require('express').Router();
+const Mock = require('mockjs');
+const wrapper = require('../wrapper');
+
+const { Random } = Mock;
+
+router.get('',
+  (req, res) => {
+    res.json(wrapper(
+      [
+        {
+          name: 'java-image-build',
+          title: '1',
+          description: 'desc',
+          skippable: true,
+          configs: [
+            {
+              name: 'repoUrl',
+              value: '#todo',
+              editable: true,
+            },
+            {
+              name: 'branchName',
+              value: '#todo',
+              editable: true,
+            },
+            {
+              name: 'jarName',
+              value: '#todo',
+              editable: true,
+            },
+          ],
+        },
+        {
+          name: 'k8s-namespace',
+          title: '2',
+          description: 'desc',
+          skippable: true,
+          configs: [
+            {
+              name: 'repoUrl',
+              value: '#todo',
+              editable: false,
+            },
+          ],
+        },
+        {
+          name: 'k8s-deployment',
+          title: '3',
+          skippable: false,
+          description: 'desc',
+          configs: [{
+            name: 'port',
+            value: '#todo',
+            editable: true,
+          }],
+        },
+        {
+          name: 'k8s-service',
+          title: '4',
+          description: 'desc',
+          skippable: true,
+          configs: [
+            {
+              name: 'repoUrl',
+              value: '#todo',
+              editable: false,
+            },
+          ],
+        },
+        {
+          name: 'k8s-ingress',
+          title: '5',
+          description: 'desc',
+          skippable: false,
+          configs: [
+            {
+              name: 'repoUrl',
+              value: '#todo',
+              editable: false,
+            },
+          ],
+        }],
+    ));
+  });
+
+module.exports = router;

+ 254 - 210
mock/routers/tree.js

@@ -32,13 +32,13 @@ router.get('/node/type/task/:projectId', (req, res) => {
         endTime: Random.datetime(),
         'id|+1': 1,
         leaf: false,
-        parent: null,
+        parentId: null,
         'priority|1': ['紧急', '一般', '宽松'],
         processorId: 101,
         processorName: 'Jinyao',
         projectId: 101,
         startTime: Random.datetime(),
-        'state|1': ['已创建', '进行中', '已完成'],
+        'state|1': ['已创建', '进行中', '已完成', '已结束'],
         timeToTake: 10,
         title: Random.cname(),
         'type|1': ['需求', '任务'],
@@ -57,215 +57,259 @@ router.post('/subNode/:parentId', (req, res) => {
 
 router.get('/:projectId', (req, res) => {
   res.json(wrapper(
-    Mock.mock([{
-      'commits|0-10': [
-        {
-          'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
-          'id|+1': 101,
-          message: 'git commit message',
-          timestamp: Random.datetime(),
-          title: 'titleeee',
-        },
-      ],
-      deep: 1,
-      description: Random.csentence(10, 40),
-      endTime: Random.datetime(),
-      id: 1,
-      leaf: false,
-      parent: null,
-      'priority|1': ['紧急', '一般', '宽松'],
-      processorId: 101,
-      processorName: 'Jinyao',
-      projectId: 101,
-      startTime: Random.datetime(),
-      'state|1': ['已创建', '进行中', '已完成'],
-      timeToTake: 10,
-      title: Random.cname(),
-      'type|1': ['需求', '任务'],
-    },
-    {
-      'commits|0-10': [
-        {
-          'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
-          'id|+1': 101,
-          message: 'git commit message',
-          timestamp: Random.datetime(),
-          title: 'titleeee',
-        },
-      ],
-      deep: 2,
-      description: Random.csentence(10, 40),
-      endTime: Random.datetime(),
-      id: 2,
-      leaf: false,
-      parent: 1,
-      'priority|1': ['紧急', '一般', '宽松'],
-      processorId: 101,
-      processorName: 'Jinyao',
-      projectId: 101,
-      startTime: Random.datetime(),
-      'state|1': ['已创建', '进行中', '已完成'],
-      timeToTake: 10,
-      title: Random.cname(),
-      'type|1': ['需求', '任务'],
-    },
-    {
-      'commits|0-10': [
-        {
-          'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
-          'id|+1': 101,
-          message: 'git commit message',
-          timestamp: Random.datetime(),
-          title: 'titleeee',
-        },
-      ],
-      deep: 2,
-      description: Random.csentence(10, 40),
-      endTime: Random.datetime(),
-      id: 3,
-      leaf: false,
-      parent: 1,
-      'priority|1': ['紧急', '一般', '宽松'],
-      processorId: 101,
-      processorName: 'Jinyao',
-      projectId: 101,
-      startTime: Random.datetime(),
-      'state|1': ['已创建', '进行中', '已完成'],
-      timeToTake: 10,
-      title: Random.cname(),
-      'type|1': ['需求', '任务'],
-    },
-    {
-      'commits|0-10': [
-        {
-          'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
-          'id|+1': 101,
-          message: 'git commit message',
-          timestamp: Random.datetime(),
-          title: 'titleeee',
-        },
-      ],
-      deep: 3,
-      description: Random.csentence(10, 40),
-      endTime: Random.datetime(),
-      id: 4,
-      leaf: false,
-      parent: 3,
-      'priority|1': ['紧急', '一般', '宽松'],
-      processorId: 101,
-      processorName: 'Jinyao',
-      projectId: 101,
-      startTime: Random.datetime(),
-      'state|1': ['已创建', '进行中', '已完成'],
-      timeToTake: 10,
-      title: Random.cname(),
-      'type|1': ['需求', '任务'],
-    },
-    {
-      'commits|0-10': [
-        {
-          'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
-          'id|+1': 101,
-          message: 'git commit message',
-          timestamp: Random.datetime(),
-          title: 'titleeee',
-        },
-      ],
-      deep: 3,
-      description: Random.csentence(10, 40),
-      endTime: Random.datetime(),
-      id: 5,
-      leaf: false,
-      parent: 3,
-      'priority|1': ['紧急', '一般', '宽松'],
-      processorId: 101,
-      processorName: 'Jinyao',
-      projectId: 101,
-      startTime: Random.datetime(),
-      'state|1': ['已创建', '进行中', '已完成'],
-      timeToTake: 10,
-      title: Random.cname(),
-      'type|1': ['需求', '任务'],
-    },
-    {
-      'commits|0-10': [
-        {
-          'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
-          'id|+1': 101,
-          message: 'git commit message',
-          timestamp: Random.datetime(),
-          title: 'titleeee',
-        },
-      ],
-      deep: 4,
-      description: Random.csentence(10, 40),
-      endTime: Random.datetime(),
-      id: 6,
-      leaf: false,
-      parent: 5,
-      'priority|1': ['紧急', '一般', '宽松'],
-      processorId: 101,
-      processorName: 'Jinyao',
-      projectId: 101,
-      startTime: Random.datetime(),
-      'state|1': ['已创建', '进行中', '已完成'],
-      timeToTake: 10,
-      title: Random.cname(),
-      'type|1': ['需求', '任务'],
-    },
-    {
-      'commits|0-10': [
-        {
-          'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
-          'id|+1': 101,
-          message: 'git commit message',
+    Mock.mock([
+      {
+        'commits|0-10': [
+          {
+            'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
+            'id|+1': 101,
+            message: 'git commit message',
+            timestamp: Random.datetime(),
+            title: 'titleeee',
+          },
+        ],
+        deep: 2,
+        description: Random.csentence(10, 40),
+        endTime: Random.datetime(),
+        id: 3,
+        leaf: false,
+        parentId: 1,
+        'priority|1': ['紧急', '一般', '宽松'],
+        processorId: 101,
+        processorName: 'Jinyao',
+        projectId: 101,
+        startTime: Random.datetime(),
+        'state|1': ['已创建', '进行中', '已完成'],
+        timeToTake: 10,
+        title: Random.cname(),
+        'type|1': ['需求', '任务'],
+      },
+      {
+        'commits|0-10': [
+          {
+            'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
+            'id|+1': 101,
+            message: 'git commit message',
+            timestamp: Random.datetime(),
+            title: 'titleeee',
+          },
+        ],
+        deep: 3,
+        description: Random.csentence(10, 40),
+        endTime: Random.datetime(),
+        id: 9,
+        leaf: false,
+        parentId: 2,
+        'priority|1': ['紧急', '一般', '宽松'],
+        processorId: 101,
+        processorName: 'Jinyao',
+        projectId: 101,
+        startTime: Random.datetime(),
+        'state|1': ['已创建', '进行中', '已完成'],
+        timeToTake: 10,
+        title: Random.cname(),
+        'type|1': ['需求', '任务'],
+      },
+      {
+        'commits|0-10': [
+          {
+            'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
+            'id|+1': 101,
+            message: 'git commit message',
+            timestamp: Random.datetime(),
+            title: 'titleeee',
+          },
+        ],
+        deep: 3,
+        description: Random.csentence(10, 40),
+        endTime: Random.datetime(),
+        id: 10,
+        leaf: false,
+        parentId: 2,
+        'priority|1': ['紧急', '一般', '宽松'],
+        processorId: 101,
+        processorName: 'Jinyao',
+        projectId: 101,
+        startTime: Random.datetime(),
+        'state|1': ['已创建', '进行中', '已完成'],
+        timeToTake: 10,
+        title: Random.cname(),
+        'type|1': ['需求', '任务'],
+      },
+      {
+        'commits|0-10': [
+          {
+            'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
+            'id|+1': 101,
+            message: 'git commit message',
+            timestamp: Random.datetime(),
+            title: 'titleeee',
+          },
+        ],
+        deep: 4,
+        description: Random.csentence(10, 40),
+        endTime: Random.datetime(),
+        id: 6,
+        leaf: false,
+        parentId: 5,
+        'priority|1': ['紧急', '一般', '宽松'],
+        processorId: 101,
+        processorName: 'Jinyao',
+        projectId: 101,
+        startTime: Random.datetime(),
+        'state|1': ['已创建', '进行中', '已完成'],
+        timeToTake: 10,
+        title: Random.cname(),
+        'type|1': ['需求', '任务'],
+      },
+      {
+        'commits|0-10': [
+          {
+            'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
+            'id|+1': 101,
+            message: 'git commit message',
+            timestamp: Random.datetime(),
+            title: 'titleeee',
+          },
+        ],
+        deep: 3,
+        description: Random.csentence(10, 40),
+        endTime: Random.datetime(),
+        id: 4,
+        leaf: false,
+        parentId: 3,
+        'priority|1': ['紧急', '一般', '宽松'],
+        processorId: 101,
+        processorName: 'Jinyao',
+        projectId: 101,
+        startTime: Random.datetime(),
+        'state|1': ['已创建', '进行中', '已完成'],
+        timeToTake: 10,
+        title: Random.cname(),
+        'type|1': ['需求', '任务'],
+      },
+      {
+        'commits|0-10': [
+          {
+            'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
+            'id|+1': 101,
+            message: 'git commit message',
+            timestamp: Random.datetime(),
+            title: 'titleeee',
+          },
+        ],
+        deep: 1,
+        description: Random.csentence(10, 40),
+        endTime: Random.datetime(),
+        id: 1,
+        leaf: false,
+        parentId: null,
+        'priority|1': ['紧急', '一般', '宽松'],
+        processorId: 101,
+        processorName: 'Jinyao',
+        projectId: 101,
+        startTime: Random.datetime(),
+        'state|1': ['已创建', '进行中', '已完成'],
+        timeToTake: 10,
+        title: Random.cname(),
+        'type|1': ['需求', '任务'],
+      },
+      {
+        'commits|0-10': [
+          {
+            'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
+            'id|+1': 101,
+            message: 'git commit message',
+            timestamp: Random.datetime(),
+            title: 'titleeee',
+          },
+        ],
+        deep: 3,
+        description: Random.csentence(10, 40),
+        endTime: Random.datetime(),
+        id: 5,
+        leaf: false,
+        parentId: 3,
+        'priority|1': ['紧急', '一般', '宽松'],
+        processorId: 101,
+        processorName: 'Jinyao',
+        projectId: 101,
+        startTime: Random.datetime(),
+        'state|1': ['已创建', '进行中', '已完成'],
+        timeToTake: 10,
+        title: Random.cname(),
+        'type|1': ['需求', '任务'],
+      },
+      {
+        'commits|0-10': [
+          {
+            'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
+            'id|+1': 101,
+            message: 'git commit message',
+            timestamp: Random.datetime(),
+            title: 'titleeee',
+          },
+        ],
+        deep: 2,
+        description: Random.csentence(10, 40),
+        endTime: Random.datetime(),
+        id: 2,
+        leaf: false,
+        parentId: 1,
+        'priority|1': ['紧急', '一般', '宽松'],
+        processorId: 101,
+        processorName: 'Jinyao',
+        projectId: 101,
+        startTime: Random.datetime(),
+        'state|1': ['已创建', '进行中', '已完成'],
+        timeToTake: 10,
+        title: Random.cname(),
+        'type|1': ['需求', '任务'],
+      },
+      {
+        'commits|0-10': [
+          {
+            'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
+            'id|+1': 101,
+            message: 'git commit message',
 
-          timestamp: Random.datetime(),
-          title: 'titleeee',
-        },
-      ],
-      deep: 4,
-      description: Random.csentence(10, 40),
-      endTime: Random.datetime(),
-      id: 7,
-      leaf: false,
-      parent: 5,
-      'priority|1': ['紧急', '一般', '宽松'],
-      processorId: 101,
-      processorName: 'Jinyao',
-      projectId: 101,
-      startTime: Random.datetime(),
-      'state|1': ['已创建', '进行中', '已完成'],
-      timeToTake: 10,
-      title: Random.cname(),
-      'type|1': ['需求', '任务'],
-    },
-    {
-      'commits|0-10': [
-        {
-          'gitlabUsername|1': ['hks', 'wph', 'zxr', 'cyt'],
-          'id|+1': 101,
-          message: 'git commit message',
-          timestamp: Random.datetime(),
-          title: 'titleeee',
-        },
-      ],
-      deep: 4,
-      description: Random.csentence(10, 40),
-      endTime: Random.datetime(),
-      id: 8,
-      leaf: false,
-      parent: 5,
-      'priority|1': ['紧急', '一般', '宽松'],
-      processorId: 101,
-      processorName: 'Jinyao',
-      projectId: 101,
-      startTime: Random.datetime(),
-      'state|1': ['已创建', '进行中', '已完成'],
-      timeToTake: 10,
-      title: Random.cname(),
-      'type|1': ['需求', '任务'],
-    },
+            timestamp: Random.datetime(),
+            title: 'titleeee',
+          },
+        ],
+        deep: 4,
+        description: Random.csentence(10, 40),
+        endTime: Random.datetime(),
+        id: 7,
+        leaf: false,
+        parentId: 5,
+        'priority|1': ['紧急', '一般', '宽松'],
+        processorId: 101,
+        processorName: 'Jinyao',
+        projectId: 101,
+        startTime: Random.datetime(),
+        'state|1': ['已创建', '进行中', '已完成'],
+        timeToTake: 10,
+        title: Random.cname(),
+        'type|1': ['需求', '任务'],
+      },
+      {
+        deep: 4,
+        description: Random.csentence(10, 40),
+        endTime: Random.datetime(),
+        id: 8,
+        leaf: false,
+        parentId: 5,
+        'priority|1': ['紧急', '一般', '宽松'],
+        processorId: 101,
+        processorName: 'Jinyao',
+        projectId: 101,
+        startTime: Random.datetime(),
+        'state|1': ['已创建', '进行中', '已完成'],
+        timeToTake: 10,
+        title: Random.cname(),
+        'type|1': ['需求', '任务'],
+      },
     ]),
   ));
 });

+ 8 - 2
mock/routers/user.js

@@ -8,8 +8,14 @@ const SECRET = 'SEEC-1919810-OIDC-114514';
 
 router.get('/self',
   (req, res) => {
-    const userinfo = jwt.decode(req.headers.authorization, SECRET);
-    res.json(wrapper(userinfo));
+    const userinfo = (jwt.decode(req.headers.authorization, SECRET)).user_info;
+    res.json(wrapper({
+      email: userinfo.email,
+      id: userinfo.id,
+      phone: userinfo.phone,
+      username: userinfo.name,
+      role: userinfo.role,
+    }));
   });
 module.exports = router;
 

+ 267 - 238
package-lock.json

@@ -2881,6 +2881,160 @@
         "tslint": "^5.20.1",
         "webpack": "^4.0.0",
         "yorkie": "^2.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+          "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "cosmiconfig": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
+          "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "@types/parse-json": "^4.0.0",
+            "import-fresh": "^3.1.0",
+            "parse-json": "^5.0.0",
+            "path-type": "^4.0.0",
+            "yaml": "^1.7.2"
+          }
+        },
+        "deepmerge": {
+          "version": "4.2.2",
+          "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
+          "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
+          "dev": true,
+          "optional": true
+        },
+        "fork-ts-checker-webpack-plugin-v5": {
+          "version": "npm:fork-ts-checker-webpack-plugin@5.2.1",
+          "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz",
+          "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "@babel/code-frame": "^7.8.3",
+            "@types/json-schema": "^7.0.5",
+            "chalk": "^4.1.0",
+            "cosmiconfig": "^6.0.0",
+            "deepmerge": "^4.2.2",
+            "fs-extra": "^9.0.0",
+            "memfs": "^3.1.2",
+            "minimatch": "^3.0.4",
+            "schema-utils": "2.7.0",
+            "semver": "^7.3.2",
+            "tapable": "^1.0.0"
+          }
+        },
+        "fs-extra": {
+          "version": "9.1.0",
+          "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+          "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "at-least-node": "^1.0.0",
+            "graceful-fs": "^4.2.0",
+            "jsonfile": "^6.0.1",
+            "universalify": "^2.0.0"
+          }
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true,
+          "optional": true
+        },
+        "jsonfile": {
+          "version": "6.1.0",
+          "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+          "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "graceful-fs": "^4.1.6",
+            "universalify": "^2.0.0"
+          }
+        },
+        "lru-cache": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "yallist": "^4.0.0"
+          }
+        },
+        "schema-utils": {
+          "version": "2.7.0",
+          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
+          "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "@types/json-schema": "^7.0.4",
+            "ajv": "^6.12.2",
+            "ajv-keywords": "^3.4.1"
+          }
+        },
+        "semver": {
+          "version": "7.3.5",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+          "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "universalify": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+          "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+          "dev": true,
+          "optional": true
+        }
       }
     },
     "@vue/cli-plugin-vuex": {
@@ -2953,6 +3107,37 @@
         "webpack-merge": "^4.2.2"
       },
       "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+          "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
         "debug": {
           "version": "4.3.1",
           "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
@@ -2962,11 +3147,62 @@
             "ms": "2.1.2"
           }
         },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true,
+          "optional": true
+        },
+        "json5": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
+          "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "minimist": "^1.2.5"
+          }
+        },
+        "loader-utils": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
+          "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "big.js": "^5.2.2",
+            "emojis-list": "^3.0.0",
+            "json5": "^2.1.2"
+          }
+        },
         "ms": {
           "version": "2.1.2",
           "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
           "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
           "dev": true
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "vue-loader-v16": {
+          "version": "npm:vue-loader@16.2.0",
+          "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.2.0.tgz",
+          "integrity": "sha512-TitGhqSQ61RJljMmhIGvfWzJ2zk9m1Qug049Ugml6QP3t0e95o0XJjk29roNEiPKJQBEi8Ord5hFuSuELzSp8Q==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "chalk": "^4.1.0",
+            "hash-sum": "^2.0.0",
+            "loader-utils": "^2.0.0"
+          }
         }
       }
     },
@@ -6143,6 +6379,22 @@
         "safer-buffer": "^2.1.0"
       }
     },
+    "echarts": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.0.2.tgz",
+      "integrity": "sha512-En0VYpc96nw2/2AZoBWPHsGi471zMublttj50kfFpYAeR4geup0Tj9iVgEXh7QYZFPnRiruDJEjcB5PXZ+BYzQ==",
+      "requires": {
+        "tslib": "2.0.3",
+        "zrender": "5.0.4"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
+          "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
+        }
+      }
+    },
     "ee-first": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -7260,160 +7512,6 @@
         "worker-rpc": "^0.1.0"
       }
     },
-    "fork-ts-checker-webpack-plugin-v5": {
-      "version": "npm:fork-ts-checker-webpack-plugin@5.2.1",
-      "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz",
-      "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==",
-      "dev": true,
-      "optional": true,
-      "requires": {
-        "@babel/code-frame": "^7.8.3",
-        "@types/json-schema": "^7.0.5",
-        "chalk": "^4.1.0",
-        "cosmiconfig": "^6.0.0",
-        "deepmerge": "^4.2.2",
-        "fs-extra": "^9.0.0",
-        "memfs": "^3.1.2",
-        "minimatch": "^3.0.4",
-        "schema-utils": "2.7.0",
-        "semver": "^7.3.2",
-        "tapable": "^1.0.0"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "4.3.0",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-convert": "^2.0.1"
-          }
-        },
-        "chalk": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
-          "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
-        "color-convert": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-name": "~1.1.4"
-          }
-        },
-        "cosmiconfig": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
-          "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "@types/parse-json": "^4.0.0",
-            "import-fresh": "^3.1.0",
-            "parse-json": "^5.0.0",
-            "path-type": "^4.0.0",
-            "yaml": "^1.7.2"
-          }
-        },
-        "deepmerge": {
-          "version": "4.2.2",
-          "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
-          "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
-          "dev": true,
-          "optional": true
-        },
-        "fs-extra": {
-          "version": "9.1.0",
-          "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
-          "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "at-least-node": "^1.0.0",
-            "graceful-fs": "^4.2.0",
-            "jsonfile": "^6.0.1",
-            "universalify": "^2.0.0"
-          }
-        },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-          "dev": true,
-          "optional": true
-        },
-        "jsonfile": {
-          "version": "6.1.0",
-          "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
-          "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "graceful-fs": "^4.1.6",
-            "universalify": "^2.0.0"
-          }
-        },
-        "lru-cache": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-          "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "yallist": "^4.0.0"
-          }
-        },
-        "schema-utils": {
-          "version": "2.7.0",
-          "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
-          "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "@types/json-schema": "^7.0.4",
-            "ajv": "^6.12.2",
-            "ajv-keywords": "^3.4.1"
-          }
-        },
-        "semver": {
-          "version": "7.3.4",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
-          "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "lru-cache": "^6.0.0"
-          }
-        },
-        "supports-color": {
-          "version": "7.2.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
-        },
-        "universalify": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
-          "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
-          "dev": true,
-          "optional": true
-        }
-      }
-    },
     "form-data": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
@@ -13708,90 +13806,6 @@
         }
       }
     },
-    "vue-loader-v16": {
-      "version": "npm:vue-loader@16.1.2",
-      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
-      "integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
-      "dev": true,
-      "optional": true,
-      "requires": {
-        "chalk": "^4.1.0",
-        "hash-sum": "^2.0.0",
-        "loader-utils": "^2.0.0"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "4.3.0",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-convert": "^2.0.1"
-          }
-        },
-        "chalk": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
-          "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
-        "color-convert": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-name": "~1.1.4"
-          }
-        },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-          "dev": true,
-          "optional": true
-        },
-        "json5": {
-          "version": "2.2.0",
-          "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
-          "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "minimist": "^1.2.5"
-          }
-        },
-        "loader-utils": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
-          "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "big.js": "^5.2.2",
-            "emojis-list": "^3.0.0",
-            "json5": "^2.1.2"
-          }
-        },
-        "supports-color": {
-          "version": "7.2.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
-        }
-      }
-    },
     "vue-router": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.3.tgz",
@@ -14728,6 +14742,21 @@
           "dev": true
         }
       }
+    },
+    "zrender": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.0.4.tgz",
+      "integrity": "sha512-DJpy0yrHYY5CuH6vhb9IINWbjvBUe/56J8aH86Jb7O8rRPAYZ3M2E469Qf5B3EOIfM3o3aUrO5edRQfLJ+l1Qw==",
+      "requires": {
+        "tslib": "2.0.3"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
+          "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
+        }
+      }
     }
   }
 }

+ 1 - 0
package.json

@@ -13,6 +13,7 @@
   "dependencies": {
     "axios": "^0.21.1",
     "core-js": "^3.6.5",
+    "echarts": "^5.1.1",
     "element-plus": "^1.0.2-beta.31",
     "jsonwebtoken": "^8.5.1",
     "vue": "^3.0.0",

+ 11 - 1
src/App.vue

@@ -8,7 +8,7 @@
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue';
+import { defineComponent, onMounted } from 'vue';
 import Menu from '@/components/Menu.vue';
 
 import { getVariableFromQueryString } from '@/utils/qs';
@@ -27,6 +27,12 @@ export default defineComponent({
 
     UserDispatcher.updateUserInfo();
     WorkspaceDispatcher.refreshProjectList();
+
+    onMounted(() => {
+      window.onbeforeunload = () => {
+        localStorage.setItem('workspace', JSON.stringify(store.state.workspace));
+      };
+    });
   },
 });
 </script>
@@ -40,6 +46,10 @@ html, body {
   width: 100%;
 }
 
+div[role=tooltip] {
+  max-width: 30vw;
+}
+
 #app {
   font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB",
   "Microsoft YaHei","微软雅黑",Arial,sans-serif;

+ 20 - 0
src/api/devcloud/apitest.ts

@@ -2,4 +2,24 @@ import axios from '@/utils/axios';
 
 import { APITEST_PREFIX } from './prefix';
 
+type CreateAPITest = {
+  projectId: number;
+  createTime: string;
+  username: string;
+  testName: string;
+  url: string;
+  params: JSON;
+  method: string;
+  qps: number;
+}
 export const getAPITestInfosByProjectId = (projectId: number) => axios.get(`${APITEST_PREFIX}/list/${projectId}`);
+
+export const createAPITest = (APITest: CreateAPITest) => axios.post(`${APITEST_PREFIX}/create`, APITest);
+
+export const deleteAPITestByTestId = (testId: number) => axios.get(`${APITEST_PREFIX}/delete/${testId}`);
+
+export const executeAPITestByTestId = (testId: number) => axios.get(`${APITEST_PREFIX}/execute/${testId}`);
+
+export const getAPITestResultByTestId = (testId: number) => axios.get(`${APITEST_PREFIX}/result/${testId}`);
+
+export const getLatestResultByTestId = (testId: number) => axios.get(`${APITEST_PREFIX}/latest/${testId}`);

+ 5 - 0
src/api/devcloud/autotest.ts

@@ -0,0 +1,5 @@
+import axios from '@/utils/axios';
+
+import { AUTOTEST_PREFIX } from './prefix';
+
+export const getAutoTestResultByProjectId = (projectId: number) => axios.get(`${AUTOTEST_PREFIX}/list/${projectId}`);

+ 8 - 2
src/api/devcloud/buglist.ts

@@ -1,6 +1,6 @@
 import axios from '@/utils/axios';
 
-import { BUGLIST_PREFIX } from './prefix';
+import { BUGLIST_PREFIX, COMMIT_PREFIX } from './prefix';
 
 interface BugRecordPayload {
   description: string;
@@ -20,7 +20,7 @@ type UpdateBugRecordPayload = BugRecordPayload & {
 
 export const createBugRecord = (createBugRecordPayload: CreateBugRecordPayload) => axios.post(`${BUGLIST_PREFIX}`, createBugRecordPayload);
 
-export const updateBugRecord = (updateBugRecordPayload: UpdateBugRecordPayload) => axios.post(`${BUGLIST_PREFIX}`, updateBugRecordPayload);
+export const updateBugRecord = (updateBugRecordPayload: UpdateBugRecordPayload) => axios.put(`${BUGLIST_PREFIX}`, updateBugRecordPayload);
 
 export const finishBugRecord = (id: number) => axios.post(`${BUGLIST_PREFIX}/finish`, null, {
   params: {
@@ -33,3 +33,9 @@ export const getBugListByProjectId = (projectId: number) => axios.get(`${BUGLIST
     projectId,
   },
 });
+
+export const getBugRecordCommitHistory = (bugId: number) => axios.get(`${COMMIT_PREFIX}/bug_list`, {
+  params: {
+    bugId,
+  },
+});

+ 33 - 0
src/api/devcloud/functest.ts

@@ -0,0 +1,33 @@
+import axios from '@/utils/axios';
+
+import { FUNCTEST_PREFIX } from './prefix';
+
+export const getTestCases = (projectId: number) => axios.get(`${FUNCTEST_PREFIX}?projectId=${projectId}`);
+
+export const createTestCase = (projectId: number, title: string) => axios.post(`${FUNCTEST_PREFIX}?projectId=${projectId}&title=${title}`);
+
+export const deleteTestCase = (testCaseId: number) => axios.delete(`${FUNCTEST_PREFIX}?testCaseId=${testCaseId}`);
+
+export const updateTestCase = (testCaseId: number, title: string) => axios.put(
+  `${FUNCTEST_PREFIX}?testCaseId=${testCaseId}&title=${title}`,
+);
+
+export const finishTestCase = (testCaseId: number) => axios.put(`${FUNCTEST_PREFIX}/finish?testCaseId=${testCaseId}`);
+
+export const reopenTestCase = (testCaseId: number) => axios.put(`${FUNCTEST_PREFIX}/reopen?testCaseId=${testCaseId}`);
+
+export const getTestCaseSteps = (testCaseId: number) => axios.get(`${FUNCTEST_PREFIX}/steps?testCaseId=${testCaseId}`);
+
+export const createTestCaseStep = (testCaseId: number) => axios.post(`${FUNCTEST_PREFIX}/steps?testCaseId=${testCaseId}`);
+
+export const updateTestCaseStep = (testStepId: number, description: string, expectation: string) => axios.put(
+  `${FUNCTEST_PREFIX}/steps?testStepId=${testStepId}&description=${description}&expectation=${expectation}`,
+);
+
+export const updateTestCaseStepState = (testStepId: number, state: string) => axios.put(
+  `${FUNCTEST_PREFIX}/steps/state?testStepId=${testStepId}&state=${state}`,
+);
+
+export const deleteTestCaseStep = (testStepId: number) => axios.delete(`${FUNCTEST_PREFIX}/steps?testStepId=${testStepId}`);
+
+export const getLatestRecord = (projectId: number) => axios.get(`${FUNCTEST_PREFIX}/record/latest?projectId=${projectId}`);

+ 26 - 8
src/api/devcloud/pipeline.ts

@@ -1,13 +1,14 @@
 import axios from '@/utils/axios';
 
-import { PIPELINE_PREFIX, DEPLOYMENT_PREFIX } from './prefix';
+import { PIPELINE_PREFIX, DEPLOYMENT_PREFIX, STAGE_PREFIX } from './prefix';
 
 interface PipelinePayload {
   projectId: number;
   pipelineId: number;
 }
 
-type CreatePipelinePayload = PipelinePayload & {
+type CreatePipelinePayload = {
+  projectId: number;
   name: string;
   templateName: string;
 }
@@ -32,7 +33,19 @@ export const updatePipeline = (updatePipelinePayload: UpdatePipelinePayload) =>
 
 export const getPipelineTemplates = () => axios.get(`${PIPELINE_PREFIX}/templates`);
 
-export const getPipelineHistory = (pipeline: PipelinePayload) => axios.get(`${PIPELINE_PREFIX}/history/list`, {
+export const getPipelineRecord = (recordId: number) => axios.get(`${PIPELINE_PREFIX}/record`, {
+  params: {
+    recordId,
+  },
+});
+
+export const getPipelineRecordDetail = (recordId: number) => axios.get(`${PIPELINE_PREFIX}/record/details`, {
+  params: {
+    recordId,
+  },
+});
+
+export const getPipelineRecordList = (pipeline: PipelinePayload) => axios.get(`${PIPELINE_PREFIX}/record/list`, {
   params: pipeline,
 });
 
@@ -43,13 +56,11 @@ export const getPipelinesByProjectId = (projectId: number) => axios.get(`${PIPEL
 });
 
 // DEPLOYMENT API
-export const deleteInstance = (deploymentId: number) => axios.delete(`${DEPLOYMENT_PREFIX}`, {
-  params: {
-    deploymentId,
-  },
+export const deleteInstance = (pipeline: PipelinePayload) => axios.delete(`${DEPLOYMENT_PREFIX}`, {
+  params: pipeline,
 });
 
-export const triggerDeployment = (pipeline: PipelinePayload) => axios.post(`${DEPLOYMENT_PREFIX}`, {
+export const triggerDeployment = (pipeline: PipelinePayload) => axios.post(`${DEPLOYMENT_PREFIX}`, null, {
   params: pipeline,
 });
 
@@ -58,3 +69,10 @@ export const getDeploymentsByProjectId = (projectId: number) => axios.get(`${DEP
     projectId,
   },
 });
+
+export const getDeploymentLog = (pipeline: PipelinePayload) => axios.get(`${DEPLOYMENT_PREFIX}/log`, {
+  params: pipeline,
+});
+
+// STAGE API
+export const getStages = () => axios.get(`${STAGE_PREFIX}`);

+ 4 - 0
src/api/devcloud/prefix.ts

@@ -6,3 +6,7 @@ export const PROJECT_PREFIX = '/project';
 export const GROUP_PREFIX = '/groups';
 export const BUGLIST_PREFIX = '/bug_list';
 export const APITEST_PREFIX = '/test';
+export const STAGE_PREFIX = '/stages';
+export const COMMIT_PREFIX = '/commits';
+export const FUNCTEST_PREFIX = '/func_test';
+export const AUTOTEST_PREFIX = '/auto_test';

+ 8 - 3
src/api/devcloud/project.ts

@@ -16,7 +16,12 @@ export const createProject = (name: string, description: string) => axios.post(`
 
 export const getProjectListByUserId = () => axios.get(`${PROJECT_PREFIX}/listByUser`);
 
-export const addProjectMember = (addProjectMemberPayload: AddProjectMemberPayload) => axios.post(`${PROJECT_PREFIX}/members`,
-  addProjectMemberPayload);
+export const addProjectMember = (addProjectMemberPayload: AddProjectMemberPayload) => axios.post(`${PROJECT_PREFIX}/members`, null, {
+  params: addProjectMemberPayload,
+});
 
-export const getProjectMembersByProjectId = (projectId: number) => axios.get(`${PROJECT_PREFIX}/${projectId}/members`);
+export const getProjectMembersByProjectId = (projectId: number) => axios.get(`${PROJECT_PREFIX}/members`, {
+  params: {
+    projectId,
+  },
+});

+ 7 - 1
src/api/devcloud/tree.ts

@@ -1,6 +1,6 @@
 import axios from '@/utils/axios';
 
-import { TREE_PREFIX } from './prefix';
+import { COMMIT_PREFIX, TREE_PREFIX } from './prefix';
 
 export const insertTreeNode = (treenode: TreeNode) => axios.post(`${TREE_PREFIX}/node`, treenode);
 
@@ -13,3 +13,9 @@ export const deleteTreeNode = (nodeId: number) => axios.delete(`${TREE_PREFIX}/n
 export const addChildNode = (parentId: number) => axios.post(`${TREE_PREFIX}/subNode/${parentId}`);
 
 export const getTreeNodesByProjectId = (projectId: number) => axios.get(`${TREE_PREFIX}/${projectId}`);
+
+export const getTaskRecordCommitHistory = (treeId: number) => axios.get(`${COMMIT_PREFIX}/tree`, {
+  params: {
+    treeId,
+  },
+});

+ 4 - 0
src/api/index.ts

@@ -4,6 +4,8 @@ import * as RequirementAPI from './devcloud/tree';
 import * as BugListAPI from './devcloud/buglist';
 import * as PipelineAPI from './devcloud/pipeline';
 import * as APITestAPI from './devcloud/apitest';
+import * as FuncTestAPI from './devcloud/functest';
+import * as AutoTestAPI from './devcloud/autotest';
 import * as PortalAPI from './portal';
 
 export {
@@ -14,4 +16,6 @@ export {
   PipelineAPI,
   PortalAPI,
   APITestAPI,
+  FuncTestAPI,
+  AutoTestAPI,
 };

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
src/assets/icon/pipeline.svg


+ 1 - 0
src/assets/icon/test-hover.svg

@@ -0,0 +1 @@
+<svg t="1619542812387" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2966" width="200" height="200"><path d="M820.736 807.488l96.288 96.288a32 32 0 1 1-45.248 45.248l-105.696-105.696a224 224 0 1 1 54.656-35.84z m-143.04-455.424a288.352 288.352 0 0 0-81.184 9.92c-25.28-19.84-53.888-33.44-84.512-39.04v77.568a289.6 289.6 0 0 0-64 58.464V322.88c-108.544 19.968-192 140-192 285.088 0 145.088 83.456 265.12 192 285.088v-72.064a289.6 289.6 0 0 0 64 58.464v13.6c5.344-0.96 10.656-2.24 15.872-3.68a286.144 286.144 0 0 0 83.168 32.128C571.744 946.112 527.2 960 480 960c-100.416 0-188.864-62.816-240.384-158.08A64 64 0 0 0 160 864a32 32 0 0 1-64 0 128 128 0 0 1 115.84-127.424A415.904 415.904 0 0 1 193.152 640H96a32 32 0 0 1 0-64h97.184c2.496-33.792 8.896-66.24 18.624-96.576A128 128 0 0 1 96 352a32 32 0 0 1 64 0 64 64 0 0 0 79.616 62.08c17.28-31.936 38.688-60.224 63.328-83.712a192 192 0 1 1 354.144 0c7.136 6.848 14.048 14.08 20.64 21.696z m116.32 26.976c3.84-8.192 5.984-17.376 5.984-27.04a32 32 0 0 1 64 0c0 21.92-5.504 42.56-15.232 60.608a288 288 0 0 0-54.72-33.568zM356.544 289.92A244.448 244.448 0 0 1 480 256c44.16 0 86.048 12.16 123.456 33.92A128.128 128.128 0 0 0 480 128a128 128 0 0 0-123.456 161.92zM672 800a160 160 0 1 0 0-320 160 160 0 0 0 0 320z" fill="#8a8a8a" p-id="2967"></path></svg>

+ 1 - 0
src/assets/icon/test-using.svg

@@ -0,0 +1 @@
+<svg t="1619542812387" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2966" width="200" height="200"><path d="M820.736 807.488l96.288 96.288a32 32 0 1 1-45.248 45.248l-105.696-105.696a224 224 0 1 1 54.656-35.84z m-143.04-455.424a288.352 288.352 0 0 0-81.184 9.92c-25.28-19.84-53.888-33.44-84.512-39.04v77.568a289.6 289.6 0 0 0-64 58.464V322.88c-108.544 19.968-192 140-192 285.088 0 145.088 83.456 265.12 192 285.088v-72.064a289.6 289.6 0 0 0 64 58.464v13.6c5.344-0.96 10.656-2.24 15.872-3.68a286.144 286.144 0 0 0 83.168 32.128C571.744 946.112 527.2 960 480 960c-100.416 0-188.864-62.816-240.384-158.08A64 64 0 0 0 160 864a32 32 0 0 1-64 0 128 128 0 0 1 115.84-127.424A415.904 415.904 0 0 1 193.152 640H96a32 32 0 0 1 0-64h97.184c2.496-33.792 8.896-66.24 18.624-96.576A128 128 0 0 1 96 352a32 32 0 0 1 64 0 64 64 0 0 0 79.616 62.08c17.28-31.936 38.688-60.224 63.328-83.712a192 192 0 1 1 354.144 0c7.136 6.848 14.048 14.08 20.64 21.696z m116.32 26.976c3.84-8.192 5.984-17.376 5.984-27.04a32 32 0 0 1 64 0c0 21.92-5.504 42.56-15.232 60.608a288 288 0 0 0-54.72-33.568zM356.544 289.92A244.448 244.448 0 0 1 480 256c44.16 0 86.048 12.16 123.456 33.92A128.128 128.128 0 0 0 480 128a128 128 0 0 0-123.456 161.92zM672 800a160 160 0 1 0 0-320 160 160 0 0 0 0 320z" fill="#1296db" p-id="2967"></path></svg>

+ 1 - 0
src/assets/icon/test.svg

@@ -0,0 +1 @@
+<svg t="1619542812387" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2966" width="200" height="200"><path d="M820.736 807.488l96.288 96.288a32 32 0 1 1-45.248 45.248l-105.696-105.696a224 224 0 1 1 54.656-35.84z m-143.04-455.424a288.352 288.352 0 0 0-81.184 9.92c-25.28-19.84-53.888-33.44-84.512-39.04v77.568a289.6 289.6 0 0 0-64 58.464V322.88c-108.544 19.968-192 140-192 285.088 0 145.088 83.456 265.12 192 285.088v-72.064a289.6 289.6 0 0 0 64 58.464v13.6c5.344-0.96 10.656-2.24 15.872-3.68a286.144 286.144 0 0 0 83.168 32.128C571.744 946.112 527.2 960 480 960c-100.416 0-188.864-62.816-240.384-158.08A64 64 0 0 0 160 864a32 32 0 0 1-64 0 128 128 0 0 1 115.84-127.424A415.904 415.904 0 0 1 193.152 640H96a32 32 0 0 1 0-64h97.184c2.496-33.792 8.896-66.24 18.624-96.576A128 128 0 0 1 96 352a32 32 0 0 1 64 0 64 64 0 0 0 79.616 62.08c17.28-31.936 38.688-60.224 63.328-83.712a192 192 0 1 1 354.144 0c7.136 6.848 14.048 14.08 20.64 21.696z m116.32 26.976c3.84-8.192 5.984-17.376 5.984-27.04a32 32 0 0 1 64 0c0 21.92-5.504 42.56-15.232 60.608a288 288 0 0 0-54.72-33.568zM356.544 289.92A244.448 244.448 0 0 1 480 256c44.16 0 86.048 12.16 123.456 33.92A128.128 128.128 0 0 0 480 128a128 128 0 0 0-123.456 161.92zM672 800a160 160 0 1 0 0-320 160 160 0 0 0 0 320z" fill="#cdcdcd" p-id="2967"></path></svg>

+ 0 - 120
src/components/APITestInfoForm.vue

@@ -1,120 +0,0 @@
-<template>
-  <right-form ref="rightForm">
-    <div class="APITestInfoForm">
-      <div class="title">测试任务详情</div>
-      <div class="marginTop">
-        <ElRow>
-          <ElCol :span="3">
-            <div class="key">url:</div>
-          </ElCol>
-          <ElCol :span="12" :offset="2">
-            <div class="value">{{info.url}}</div>
-          </ElCol>
-        </ElRow>
-      </div>
-      <div class="marginTop">
-        <ElRow>
-          <ElCol :span="3">
-            <div class="key">params:</div>
-          </ElCol>
-          <ElCol :span="12" :offset="2">
-            <div>
-              <vue-json-editor
-                :height="'400px'"
-                :value="info.params"
-                :show-btns="false"
-                :mode="'code'"
-                :modes="['code']"
-                lang="zh"
-              ></vue-json-editor>
-            </div>
-          </ElCol>
-        </ElRow>
-      </div>
-      <div class="marginTop">
-        <ElRow>
-          <ElCol :span="3">
-            <div class="key">并发数:</div>
-          </ElCol>
-          <ElCol :span="12" :offset="2">
-            <div class="value">{{info.qps}}</div>
-          </ElCol>
-        </ElRow>
-      </div>
-      <div class="title">测试结果</div>
-      <div v-if="info.success === 0 && info.failure === 0">
-        还未运行!
-      </div>
-      <div v-else>
-        <div class="marginTop">
-          <ElRow>
-            <ElCol :span="3">
-              <div class="key">成功:</div>
-            </ElCol>
-            <ElCol :span="12" :offset="2">
-              <div class="value">{{info.success}}</div>
-            </ElCol>
-          </ElRow>
-        </div>
-        <div class="marginTop">
-          <ElRow>
-            <ElCol :span="3">
-              <div class="key">失败:</div>
-            </ElCol>
-            <ElCol :span="12" :offset="2">
-              <div class="value">{{info.failure}}</div>
-            </ElCol>
-          </ElRow>
-        </div>
-      </div>
-    </div>
-  </right-form>
-</template>
-
-<script>
-import vueJsonEditor from 'vue-json-editor';
-import RightForm from './RightForm.vue';
-
-export default {
-  components: {
-    vueJsonEditor,
-    RightForm,
-  },
-  name: 'APITestInfoForm',
-  data() {
-    return {
-    };
-  },
-  props: {
-    info: {
-      type: Object,
-    },
-  },
-  methods: {
-    showForm() {
-      this.$refs.rightForm.showForm();
-    },
-  },
-};
-</script>
-
-<style>
-.APITestInfoForm {
-  margin-left: 30px;
-}
-.APITestInfoForm .title {
-  font-size: 24px;
-  font-weight: bold;
-  margin-top: 20px;
-}
-.APITestInfoForm .marginTop {
-  margin-top: 20px;
-}
-.APITestInfoForm .marginTop .key {
-  font-size: 20px;
-  font-weight: bold;
-}
-.APITestInfoForm .marginTop .value {
-  font-size: 18px;
-}
-</style>

+ 6 - 2
src/components/BugAddDrawer.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-drawer v-model="showDrawer" :size="650" @before-close="(done) => {done()}">
+  <el-drawer v-model="showDrawer" :size="650">
     <template #title>
       <h3>新建缺陷记录</h3>
     </template>
@@ -42,7 +42,7 @@
         </el-select>
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" @click="submitAdd">创建</el-button>
+        <el-button type="primary" v-loading="confirmLoading" @click="submitAdd">创建</el-button>
         <el-button @click="showDrawer = false">取消</el-button>
       </el-form-item>
     </el-form>
@@ -57,6 +57,7 @@ import { BugListAPI } from '../api';
 
 export default {
   name: 'BugAddDrawer',
+  emits: ['finish-add'],
   data() {
     return {
       showDrawer: false,
@@ -76,6 +77,7 @@ export default {
           required: true, massage: '请选择类型', trigger: 'change',
         }],
       },
+      confirmLoading: false,
     };
   },
   computed: {
@@ -88,6 +90,7 @@ export default {
       this.showDrawer = true;
     },
     submitAdd() {
+      this.confirmLoading = true;
       const param = { projectId: this.$store.state.workspace.currentProjectId, ...this.param };
       BugListAPI.createBugRecord(param).then(
         (res) => {
@@ -95,6 +98,7 @@ export default {
           this.showDrawer = false;
         },
       );
+      this.confirmLoading = false;
     },
   },
 };

+ 11 - 6
src/components/BugEditDrawer.vue

@@ -2,7 +2,7 @@
   <el-drawer
     v-model="showDrawer"
     :size="650"
-    @before-close="(done) => {
+    :before-close="(done) => {
       isEditing = false;
       done();
     }">
@@ -115,12 +115,12 @@
                       </el-input-number>
                     </div>
                   </RowItem>
-                  <RowItem :label="'开始时间:'"><span>{{startTime}}</span></RowItem>
-                  <RowItem :label="'完成时间:'"><span>{{endTime}}</span></RowItem>
+                  <RowItem :label="'开始时间:'"><span>{{formatter(startTime)}}</span></RowItem>
+                  <RowItem :label="'完成时间:'"><span>{{formatter(endTime)}}</span></RowItem>
                 </el-col>
               </el-row>
               <el-row v-if="isEditing" class="entry-row" style="margin-top: 15px">
-                <el-button @click="submitEdit" size="mini" type="primary">提交</el-button>
+                <el-button @click="submitEdit" size="mini" v-loading="confirmLoading" type="primary">提交</el-button>
                 <el-button @click="cancelEdit" size="mini">取消</el-button>
               </el-row>
             </el-tab-pane>
@@ -130,8 +130,8 @@
                 <el-table :data="commits">
                   <el-table-column type="expand">
                     <template #default="scope">
-                      <RowItem :label="'message:'" label-span="5" value-span="19">{{scope.row.message}}</RowItem>
-                      <RowItem :label="'timestamp:'" label-span="5" value-span="19">{{scope.row.timestamp}}</RowItem>
+                      <RowItem :label="'message:'" :label-span="5" :value-span="19">{{scope.row.message}}</RowItem>
+                      <RowItem :label="'timestamp:'" :label-span="5" :value-span="19">{{scope.row.timestamp}}</RowItem>
                     </template>
                   </el-table-column>
                   <el-table-column
@@ -167,6 +167,7 @@ import { BugListAPI } from '@/api';
 import PriorityTag from '@/components/PriorityTag.vue';
 import StateTag from '@/components/StateTag.vue';
 import { priorityOptions, stateOptions } from '@/utils/constants';
+import { formatter } from '@/utils/time';
 import RowItem from './RowItem.vue';
 
 export default {
@@ -200,6 +201,7 @@ export default {
         processorId: null,
         processorName: null,
       },
+      confirmLoading: false,
       stateOptions,
       priorityOptions,
     };
@@ -210,6 +212,7 @@ export default {
     },
   },
   methods: {
+    formatter,
     show() {
       this.showDrawer = true;
     },
@@ -223,11 +226,13 @@ export default {
       this.editedBugRecord.processorName = this.processorName;
     },
     submitEdit() {
+      this.confirmLoading = true;
       const param = { id: this.id, ...this.editedBugRecord };
       BugListAPI.updateBugRecord(param).then((res) => {
         this.isEditing = false;
         this.$emit('bug-record-update');
       });
+      this.confirmLoading = false;
     },
     cancelEdit() {
       this.isEditing = false;

+ 2 - 2
src/components/CommonDrawer.vue

@@ -2,7 +2,7 @@
   <el-drawer
     v-model="showDrawer"
     :size="650"
-    @before-close="(done) => {
+    @="(done) => {
       isEditing = false;
       done();
     }">
@@ -208,7 +208,7 @@ export default {
     startTime: String,
     endTime: String,
     childs: Array,
-    parent: Number,
+    parentId: Number,
     commits: Array,
     showSons: {
       type: Boolean,

+ 11 - 4
src/components/GeneralTable.vue

@@ -8,7 +8,7 @@
   <ElTable
     ref="tableRef"
     :data="data"
-    :default-sort="{prop: 'id', order: 'descending'}"
+    v-loading="loading"
     @row-click="rowClick"
   >
     <ElTableColumn
@@ -54,18 +54,18 @@
       width="100"
     ></ElTableColumn>
     <ElTableColumn
-      prop="startTime"
       label="开始时间"
+      :formatter="(row) => formatter(row.startTime)"
       align="center"
     ></ElTableColumn>
     <ElTableColumn
-      prop="endTime"
       label="结束时间"
+      :formatter="(row) => formatter(row.endTime)"
       align="center"
     ></ElTableColumn>
     <ElTableColumn
       prop="state"
-      label="执行状态"
+      label="状态"
       align="center"
       width="120"
       :filters="[{ text: '已创建', value: '已创建' }, { text: '进行中', value: '进行中' }, { text: '已完成', value: '已完成' }]"
@@ -88,6 +88,8 @@ import StateTag from '@/components/StateTag.vue';
 import PriorityTag from '@/components/PriorityTag.vue';
 import { ElTable } from 'element-plus';
 
+import { formatter } from '@/utils/time';
+
 interface Record {
   commits: Array<CommitRecord>;
   description: string;
@@ -111,6 +113,10 @@ export default defineComponent({
       type: Boolean,
       default: false,
     },
+    loading: {
+      type: Boolean,
+      default: false,
+    },
   },
   emits: ['row-click', 'add'],
   setup(props, { emit }) {
@@ -136,6 +142,7 @@ export default defineComponent({
       tableRef,
       clearFilter,
       add,
+      formatter,
     };
   },
 });

+ 0 - 187
src/components/List.vue

@@ -1,187 +0,0 @@
-<template>
-  <div style="width: 100%">
-    <div>
-      <el-button type="primary"  v-if="!isShowCheckbox" size="small" @click="showCheckbox">批量操作</el-button>
-      <span  v-if="isShowCheckbox">
-      <el-button type="info" size="small" @click="isShowCheckbox=false">取消操作</el-button>
-      <el-button type="danger"  size="small" @click="handleMultipleDelete">批量删除</el-button>
-      <el-select
-        @change="handleMultipleStatusChange"
-        v-model="status"
-        size='medium'
-        placeholder="批量修改状态"
-        style="margin-left: 30px">
-        <el-option
-          v-for="item in statusOptions"
-          :key="item.value"
-          :label="item.label"
-          :value="item.value">
-        </el-option>
-      </el-select>
-      <el-select
-        @change="handleMultipleAssignmentChange"
-        v-model="assignment"
-        size='medium'
-        placeholder="批量修改负责人"
-        style="margin-left: 30px">
-        <el-option
-          v-for="item in assignmentOptions"
-          :key="item.value"
-          :label="item.label"
-          :value="item.value">
-        </el-option>
-      </el-select>
-      </span>
-    </div>
-    <el-table
-      :data="listData"
-      @selection-change="handleSelectionChange"
-      style="width: 100%">
-      <el-table-column
-        v-if="isShowCheckbox"
-        type="selection"
-        width="60">
-      </el-table-column>
-      <el-table-column
-        prop="projectCode"
-        label="编号"
-        width="180">
-      </el-table-column>
-      <el-table-column
-        prop="title"
-        min-width="180"
-        label="标题">
-      </el-table-column>
-      <el-table-column
-        prop="assignment"
-        label="优先级"
-        width="180">
-      </el-table-column>
-      <el-table-column
-        prop="status"
-        label="状态"
-        width="180">
-      </el-table-column>
-      <el-table-column
-        prop="assignment"
-        label="计划时间"
-        width="180">
-      </el-table-column>
-      <el-table-column
-        prop="assignment"
-        label="创建时间"
-        width="180">
-      </el-table-column>
-      <el-table-column
-        prop="assignment"
-        label="经办人"
-        width="180">
-      </el-table-column>
-    </el-table>
-    <el-pagination
-      style="display: flex;flex-direction: row;justify-content: center;margin-top: 20px"
-      background
-      @current-change="handleCurrentChange"
-      :page-size="pageSize"
-      layout="prev, pager, next"
-      :total="total">
-    </el-pagination>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'List',
-  props: {
-    listData: Array, // 展示的列表数据
-    total: Number, // 总页数
-    statusOptions: {
-      type: Array,
-      default() {
-        return [
-          {
-            label: '未分配',
-            value: 1,
-          },
-          {
-            label: '未完成',
-            value: 2,
-          },
-          {
-            label: '已完成',
-            value: 3,
-          },
-          {
-            label: '已关闭',
-            value: 4,
-          }];
-      },
-    }, // 状态选项
-    assignmentOptions: {
-      type: Array,
-      default() {
-        return [
-          {
-            label: '组员A',
-            value: 1,
-          },
-          {
-            label: '组员B',
-            value: 2,
-          },
-          {
-            label: '组员C',
-            value: 3,
-          },
-          {
-            label: '组员D',
-            value: 4,
-          }];
-      },
-    }, // 分配人选项
-  },
-  data() {
-    return {
-      isShowCheckbox: false,
-      pageSize: 10,
-      selectedId: [],
-      status: null,
-      assignment: null,
-    };
-  },
-  methods: {
-    showCheckbox() {
-      this.isShowCheckbox = true;
-    },
-    handleSelectionChange(val) {
-      console.warn(val);
-      const tempArray = [];
-      val.forEach((item) => {
-        tempArray.push(item.projectCode);
-      });
-      this.selectedId = tempArray;
-    },
-    // 分页切换事件
-    handleCurrentChange(currentPage) {
-      console.log(currentPage);
-      this.$emit('current-change', currentPage);
-    },
-    // 批量删除事件
-    handleMultipleDelete() {
-      console.warn('deleteMultiply');
-      this.$emit('multiple-delete', this.selectedId);
-    },
-    // 批量修改状态事件
-    handleMultipleStatusChange() {
-      this.$emit('multiple-status-change', this.selectedId);
-    },
-    // 批量修改分配人事件
-    handleMultipleAssignmentChange() {
-      this.$emit('multiple-assignment-change', this.selectedId);
-    },
-  },
-};
-</script>
-
-<style scoped>
-</style>

+ 0 - 121
src/components/ListFilter.vue

@@ -1,121 +0,0 @@
-<template>
-  <div class="list-filter-container">
-    <el-select
-      class="select"
-      size="small"
-      v-model="assignment"
-      @change="handleFilterChange">
-      <el-option
-        v-for="item in assignmentOptions"
-        :key="item.value"
-        :label="item.label"
-        :value="item.value">
-      </el-option>
-    </el-select>
-    <el-select
-      class="select"
-      size="small"
-      v-model="status"
-      @change="handleFilterChange"
-      style="margin-left: 10px">
-      <el-option
-        v-for="item in statusOptions"
-        :key="item.value"
-        :label="item.label"
-        :value="item.value">
-      </el-option>
-    </el-select>
-    <el-select
-      class="select"
-      size="small"
-      v-model="sort"
-      style="margin-left: auto"
-      @change="handleFilterChange">
-      <el-option
-        v-for="item in sortOptions"
-        :key="item.value"
-        :label="item.label"
-        :value="item.value">
-      </el-option>
-    </el-select>
-  </div>
-
-</template>
-
-<script>
-export default {
-  name: 'ListFilter',
-  props: {
-    assignmentOptions: {
-      type: Array,
-      default() {
-        return [{
-          value: 0,
-          label: '全部',
-        }, {
-          value: 1,
-          label: '分配到我',
-        }];
-      },
-    },
-    statusOptions: {
-      type: Array,
-      default() {
-        return [{
-          value: 0,
-          label: '全部状态',
-        }, {
-          value: 1,
-          label: '已完成',
-        }, {
-          value: 2,
-          label: '进行中',
-        }];
-      },
-    },
-    sortOptions: {
-      type: Array,
-      default() {
-        return [{
-          value: 0,
-          label: '默认排序',
-        }, {
-          value: 1,
-          label: '创建时间',
-        }, {
-          value: 2,
-          label: '优先级',
-        }];
-      },
-    },
-  },
-  data() {
-    return {
-      assignment: 0,
-      status: 0,
-      sort: 0,
-    };
-  },
-  methods: {
-    handleFilterChange() {
-      this.$emit('filter-change', {
-        assignment: this.assignment,
-        status: this.status,
-        sort: this.sort,
-      });
-    },
-  },
-};
-</script>
-
-<style scoped>
-.list-filter-container{
-  width: 100%;
-  display: flex;
-  justify-content: start;
-  align-items: center;
-}
-.select{
-  width: 130px;
-}
-</style>

+ 9 - 3
src/components/Menu.vue

@@ -77,6 +77,8 @@ import { useUser } from '@/composable/useUser';
 import { defineComponent, ref, reactive } from 'vue';
 import { PortalAPI, ProjectAPI } from '@/api';
 
+import { WorkspaceDispatcher } from '@/store/dispatchers';
+
 export default defineComponent({
   name: 'Menu',
   setup() {
@@ -96,8 +98,10 @@ export default defineComponent({
       name: '',
       description: '',
     });
-    const createProject = () => {
-      ProjectAPI.createProject(createProjectForm.name, createProjectForm.description);
+    const createProject = async () => {
+      await ProjectAPI.createProject(createProjectForm.name, createProjectForm.description);
+      showProjectDrawer.value = false;
+      WorkspaceDispatcher.refreshProjectList();
     };
 
     interface InviteProjectForm {
@@ -132,10 +136,12 @@ export default defineComponent({
         return;
       }
 
-      ProjectAPI.addProjectMember({
+      await ProjectAPI.addProjectMember({
         projectId: inviteProjectForm.projectId,
         invitedUserId,
       });
+      showInviteDrawer.value = false;
+      if (selectedProject.value.id !== -1) { WorkspaceDispatcher.changeProject(selectedProject.value.id); }
     };
 
     const handleSelect = (index: string) => {

+ 0 - 83
src/components/MissionDetailsForm.vue

@@ -1,83 +0,0 @@
-<template>
-<right-form ref="rightForm">
-  <div class="missionDetialscontainer">
-    <div class="missionBaseInfo">
-      <div class="missionBaseLine1">
-        {{taskInfo.creator+" 创建于"+taskInfo.createTime+" "+taskInfo.project}}
-      </div>
-      <div class="missionBaseLine2">
-        {{"#"+taskInfo.taskId+"  "+taskInfo.taskName}}
-      </div>
-    </div>
-    <el-tabs class="tablePane" v-model="activeName" @tab-click="handleClick">
-      <el-tab-pane label="描述信息" name="taskdDescriptionInfo">
-        任务描述信息
-      </el-tab-pane>
-      <el-tab-pane label="代码提交记录" name="commitRecord">代码提交记录</el-tab-pane>
-    </el-tabs>
-  </div>
-</right-form>
-</template>
-
-<script>
-import RightForm from './RightForm.vue';
-
-export default {
-  components: { RightForm },
-  name: 'MissionDetailsForm',
-  data() {
-    return {
-      activeName: 'second',
-      taskInfo: {
-        taskId: 9973461,
-        creator: 'wkx',
-        createTime: '2020/01/23 15:15:37 GMT+8:00',
-        project: 'testPro',
-        taskName: '软工三需求迭代一',
-        taskType: '任务类型一',
-        taskPriority: '一般',
-        taskStatus: '新建',
-        taskExecutor: 'wkx',
-        expectedToTakeTime: '5小时',
-        startTime: '2020/01/24 15:15:37 GMT+8:00',
-        endTime: '',
-        taskDescription: '这个是任务的描述,喜欢写就写好了',
-        GitRecord: {
-          record1: '生成好的git信息1',
-          record2: '生成好的git信息2',
-          record3: '生成好的git信息3',
-        },
-        LeafTaskNode: {
-          leafTask1Id: 9973461,
-          leafTask2Id: 9973461,
-          leafTask3Id: 9973461,
-        },
-      },
-    };
-  },
-  methods: {
-    handleClick(tab, event) {
-      console.log(tab, event);
-    },
-    showForm() {
-      this.$refs.rightForm.showForm();
-    },
-  },
-};
-</script>
-
-<style scoped>
-.missionDetialscontainer{
-  margin: 0 40px 0 40px;
-  .missionBaseInfo{
-    .missionBaseLine1{
-      color: #ecf0f1;
-      font-size: 14px;
-    }
-    .missionBaseLine1{
-      color: #2C3E50;
-      font-size: 24px;
-    }
-  }
-}
-</style>

+ 7 - 3
src/components/PriorityTag.vue

@@ -1,12 +1,12 @@
 <template>
   <template v-if="priority === '紧急'">
-    <ElTag type="warning">{{priority}}</ElTag>
+    <ElTag :size="size" type="warning">{{priority}}</ElTag>
   </template>
   <template v-else-if="priority === '一般'">
-    <ElTag>{{priority}}</ElTag>
+    <ElTag :size="size">{{priority}}</ElTag>
   </template>
   <template v-else-if="priority === '宽松'">
-    <ElTag type="success">{{priority}}</ElTag>
+    <ElTag :size="size" type="success">{{priority}}</ElTag>
   </template>
 </template>
 
@@ -16,6 +16,10 @@ import { defineComponent } from 'vue';
 export default defineComponent({
   props: {
     priority: String,
+    size: {
+      type: String,
+      default: '',
+    },
   },
 });
 </script>

+ 32 - 11
src/components/RequirementDrawer.vue

@@ -2,7 +2,7 @@
   <el-drawer
     v-model="showDrawer"
     :size="650"
-    @before-close="(done) => {
+    :before-close="(done) => {
       isEditing = false;
       done();
     }">
@@ -11,7 +11,12 @@
         <el-col :span="2">
           <div class="node-type">{{type}}</div>
         </el-col>
-        <el-col :span="12" :offset="2">
+        <el-col :span="4">
+          <template class="entry-row">
+            <span>#{{id}}</span>
+          </template>
+        </el-col>
+        <el-col :span="8">
           <template class="entry-row" v-if="isEditing">
             <el-input placeholder="请输入标题" size="mini" style="width: 200px;display: inline;" v-model="editedRequirement.title"/>
           </template>
@@ -95,7 +100,7 @@
                       <el-tag>{{processorName}}</el-tag>
                     </div>
                     <div v-else>
-                      <el-select style="width: 160px" size="mini" v-model="editedRequirement.userId" >
+                      <el-select style="width: 160px" size="mini" v-model="editedRequirement.processorId" >
                         <el-option
                           v-for="item in  userOptions"
                           :key="item.value"
@@ -115,8 +120,8 @@
                       </el-input-number>
                     </div>
                   </RowItem>
-                  <RowItem :label="'开始时间:'"><span>{{startTime}}</span></RowItem>
-                  <RowItem :label="'完成时间:'"><span>{{endTime}}</span></RowItem>
+                  <RowItem :label="'开始时间:'"><span>{{formatter(startTime)}}</span></RowItem>
+                  <RowItem :label="'完成时间:'"><span>{{formatter(endTime)}}</span></RowItem>
                 </el-col>
               </el-row>
               <!-- 相关需求 -->
@@ -136,18 +141,19 @@
               </template>
               </template>
               <el-row v-if="isEditing" class="entry-row" style="margin-top: 15px">
-                <el-button @click="submitEdit" size="mini" type="primary">提交</el-button>
+                <el-button @click="submitEdit" v-loading="confirmLoading" size="mini" type="primary">提交</el-button>
                 <el-button @click="cancelEdit" size="mini">取消</el-button>
               </el-row>
             </el-tab-pane>
-            <el-tab-pane label="历史提交" name="commit_history">
+            <template v-if="type!=='任务'">
+              <el-tab-pane label="历史提交" name="commit_history">
               <!-- 相关提交 -->
               <el-row class="entry-row" style="margin-top: 15px">
                 <el-table :data="commits">
                   <el-table-column type="expand">
                     <template #default="scope">
-                      <RowItem :label="'message:'" label-span="5" value-span="19">{{scope.row.message}}</RowItem>
-                      <RowItem :label="'timestamp:'" label-span="5" value-span="19">{{scope.row.timestamp}}</RowItem>
+                      <RowItem :label="'message:'" :label-span="5" :value-span="19">{{scope.row.message}}</RowItem>
+                      <RowItem :label="'timestamp:'" :label-span="5" :value-span="19">{{scope.row.timestamp}}</RowItem>
                     </template>
                   </el-table-column>
                   <el-table-column
@@ -171,6 +177,7 @@
                 </el-table>
               </el-row>
             </el-tab-pane>
+            </template>
           </el-tabs>
         </el-col>
       </el-row>
@@ -183,6 +190,7 @@ import { RequirementAPI } from '@/api';
 import PriorityTag from '@/components/PriorityTag.vue';
 import StateTag from '@/components/StateTag.vue';
 import { priorityOptions, stateOptions } from '@/utils/constants';
+import { formatter } from '@/utils/time';
 import RowItem from './RowItem.vue';
 
 export default {
@@ -202,7 +210,7 @@ export default {
     startTime: String,
     endTime: String,
     childs: Array,
-    parent: Number,
+    parentId: Number,
     commits: Array,
     showSons: {
       type: Boolean,
@@ -223,6 +231,7 @@ export default {
         processorId: null,
         processorName: null,
       },
+      confirmLoading: false,
       stateOptions,
       priorityOptions,
     };
@@ -233,6 +242,7 @@ export default {
     },
   },
   methods: {
+    formatter,
     show() {
       this.showDrawer = true;
     },
@@ -246,11 +256,22 @@ export default {
       this.editedRequirement.processorName = this.processorName;
     },
     submitEdit() {
-      const param = { id: this.id, ...this.editedRequirement };
+      this.confirmLoading = true;
+      const param = {
+        id: this.id,
+        title: this.editedRequirement.title,
+        description: this.editedRequirement.description,
+        state: this.editedRequirement.state,
+        timeToTake: this.editedRequirement.timeToTake,
+        priority: this.editedRequirement.priority,
+        processorId: this.editedRequirement.processorId,
+      };
       RequirementAPI.updateTreeNode(param).then((res) => {
         this.isEditing = false;
         this.$emit('requirement-update');
+        this.showDrawer = false;
       });
+      this.confirmLoading = false;
     },
     cancelEdit() {
       this.isEditing = false;

+ 0 - 68
src/components/RightForm.vue

@@ -1,68 +0,0 @@
-<template>
-  <transition name="translation">
-  <div class="right-bar-container" v-if="isFormShow">
-    <div class="form-util">
-      <div class="close" @click="isFormShow=false"></div>
-    </div>
-    <div>
-      <slot></slot>
-    </div>
-  </div>
-  </transition>
-</template>
-
-<script>
-export default {
-  name: 'RightForm',
-  data() {
-    return {
-      isFormShow: false,
-    };
-  },
-  methods: {
-    // 父组件通过ref的方式调用该方法显示表单
-    showForm() {
-      this.isFormShow = true;
-    },
-  },
-};
-</script>
-
-<style scoped>
-.right-bar-container{
-  position: fixed;
-  right: 0px;
-  top: 0px;
-  height: 100vh;
-  width: 800px;
-  box-shadow: 0 0 5px #BDC3C7;
-  background: #FFFFFF;
-  z-index: 101;
-}
-.translation-enter-active,.translation-leave-active {
-  transition: all 0.3s ease-out;
-}
-.translation-enter-from,
-.translation-leave-to {
-  transform: translateX(800px);
-}
-.form-util{
-  width: 80px;
-  margin-left: auto;
-  padding: 10px;
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-  align-items: center;
-}
-.form-util .close{
-  width: 20px;
-  height: 20px;
-  cursor: pointer;
-  background-position: center;
-  background-repeat: no-repeat;
-  background-size: 100% 100%;
-  background-image: url("../assets/icon/close.svg");
-}
-
-</style>

+ 11 - 11
src/components/SideBar.vue

@@ -9,9 +9,9 @@
     <el-menu-item index="bugList">
       <i class="icon" target="bugList"></i>
     </el-menu-item>
-    <el-menu-item index="statistics">
+    <!-- <el-menu-item index="statistics">
       <i class="icon" target="statistics"></i>
-    </el-menu-item>
+    </el-menu-item> -->
     <el-menu-item index="pipeline">
       <i class="icon" target="pipeline"></i>
     </el-menu-item>
@@ -52,7 +52,7 @@ export default {
   .el-menu-item:hover & {
     background-image: url("../assets/icon/navigation-hover.svg");
   }
-  .el-menu-item.is-active:hover & {
+  .el-menu-item.is-active & {
     background-image: url("../assets/icon/navigation-using.svg");
   }
 }
@@ -61,7 +61,7 @@ export default {
   .el-menu-item:hover & {
     background-image: url("../assets/icon/task-hover.svg");
   }
-  .el-menu-item.is-active:hover & {
+  .el-menu-item.is-active & {
     background-image: url("../assets/icon/task-using.svg");
   }
 }
@@ -70,7 +70,7 @@ export default {
   .el-menu-item:hover & {
     background-image: url("../assets/icon/bug-hover.svg");
   }
-  .el-menu-item.is-active:hover & {
+  .el-menu-item.is-active & {
     background-image: url("../assets/icon/bug-using.svg");
   }
 }
@@ -79,7 +79,7 @@ export default {
   .el-menu-item:hover & {
     background-image: url("../assets/icon/training-hover.svg");
   }
-  .el-menu-item.is-active:hover & {
+  .el-menu-item.is-active & {
     background-image: url("../assets/icon/training-using.svg");
   }
 }
@@ -88,17 +88,17 @@ export default {
   .el-menu-item:hover & {
     background-image: url("../assets/icon/training-hover.svg");
   }
-  .el-menu-item.is-active:hover & {
+  .el-menu-item.is-active & {
     background-image: url("../assets/icon/training-using.svg");
   }
 }
 .icon[target=apitest]{
-   background-image: url("../assets/icon/training.svg");
+   background-image: url("../assets/icon/test.svg");
   .el-menu-item:hover & {
-    background-image: url("../assets/icon/training-hover.svg");
+    background-image: url("../assets/icon/test-hover.svg");
   }
-  .el-menu-item.is-active:hover & {
-    background-image: url("../assets/icon/training-using.svg");
+  .el-menu-item.is-active & {
+    background-image: url("../assets/icon/test-using.svg");
   }
 }
 </style>

+ 3 - 0
src/components/StateTag.vue

@@ -8,6 +8,9 @@
   <template v-if="state === '已完成'">
     <ElTag type="info">{{state}}</ElTag>
   </template>
+  <template v-if="state === '已结束'">
+    <ElTag type="info">{{state}}</ElTag>
+  </template>
 </template>
 
 <script lang="ts">

+ 25 - 14
src/components/Tree.vue

@@ -5,9 +5,12 @@
       <TreeNode
         v-for="item in col"
         v-bind="item"
+        :processor="item.processorName"
+        :priority="item.priority"
         :key="item.id"
         :position-x="index"
         :node-style="NODE_STYLE"
+        :depth="item.deep"
         @addChild="addChild"
         @deleteNode="deleteNode"
         @nodeClick="nodeClick"
@@ -24,7 +27,7 @@
 <!--      @nodeClick="nodeClick"-->
 <!--    />-->
   </div>
-  <canvas id="line-drawer" :height="HEIGHT" :width="WIDTH"></canvas>
+  <canvas id="line-drawer" :height="fitHeight" :width="WIDTH"></canvas>
 </div>
 </template>
 
@@ -34,12 +37,10 @@ import TreeNode from './TreeNode.vue';
 // 容器宽高
 const HEIGHT = 700;
 const WIDTH = 1000;
-const MARGIN_TOP = 80;
-const MARGIN_LEFT = 100;
 const TREE_MAX_HEIGHT = 4;
 const NODE_STYLE = {
   width: 250,
-  height: 100,
+  height: 140,
   padding: 25,
 };
 
@@ -74,7 +75,7 @@ export default {
         } else {
           console.warn('需求树数据有误,树高度错误');
         }
-        if (!item.parent) rootNode = item;
+        if (!item.parentId) rootNode = item;
         // if (item.parent) {
         //   const tempParent = nodeMap.get(item.parent);
         //   if (!tempParent.childs) {
@@ -99,8 +100,13 @@ export default {
       rootNode.positionY = 0;
       for (let i = 1; i < TREE_MAX_HEIGHT; i += 1) {
         let sum = 0;
+        let tempParent = colList[0][0].id;
         colList[i].forEach((item) => {
-          item.positionY = nodeMap.get(item.parent).positionY + sum;
+          if (item.parentId !== tempParent) {
+            sum = 0;
+            tempParent = item.parentId;
+          }
+          item.positionY = nodeMap.get(item.parentId).positionY + sum;
           sum += item.contentHeight;
         });
       }
@@ -113,8 +119,14 @@ export default {
           }
         });
       }
+      // 计算所需容器高度
+      let maxPositionY = 0;
+      this.data.forEach((item) => {
+        if (item.positionY && item.positionY > maxPositionY) maxPositionY = item.positionY;
+      });
+      this.drawLine((maxPositionY * 140 + 100).toFixed(0));
       // 返回col
-      this.drawLine();
+      console.warn(colList);
       return colList;
     },
   },
@@ -124,9 +136,7 @@ export default {
       WIDTH,
       TREE_MAX_HEIGHT,
       NODE_STYLE,
-      parent2children: {},
-      depth2node: {},
-      layout: {},
+      fitHeight: 0,
       ctx: null,
     };
   },
@@ -138,9 +148,10 @@ export default {
     initCanvasCtx() {
       this.ctx = document.getElementById('line-drawer').getContext('2d');
     },
-    drawLine() {
+    drawLine(height) {
+      this.fitHeight = height > HEIGHT ? height : HEIGHT;
       setTimeout(() => {
-        this.ctx.clearRect(0, 0, WIDTH, HEIGHT);
+        this.ctx.clearRect(0, 0, WIDTH, height > HEIGHT ? height : HEIGHT);
         this.ctx.lineWidth = 1;
         this.ctx.strokeStyle = '#bbbbbb';
         this.data.forEach((item) => {
@@ -166,7 +177,7 @@ export default {
     },
     addChild(id) {
       this.$emit('change', {
-        type: 'addChild',
+        type: 'addChildNode',
         id,
       });
     },
@@ -177,7 +188,7 @@ export default {
         type: 'warning',
       }).then(() => {
         this.$emit('change', {
-          type: 'deleteNode',
+          type: 'deleteTreeNode',
           id,
         });
       }).catch(() => {

+ 20 - 4
src/components/TreeNode.vue

@@ -8,22 +8,30 @@
             <ElTag class="node-state" :type="getTagTypeFromState(state)">{{state}}</ElTag>
           </div>
         </template>
+          <div class="card-body">
+            <PriorityTag :size="'mini'" :priority="priority"></PriorityTag>
+            <ElTag size="mini" >{{processor}}</ElTag>
+          </div>
       </ElCard>
     </div>
     <div class="node-postfix">
       <div class="icon" target="delete" @click="deleteNode(id)"></div>
-      <div class="icon" target="add" @click="addChild(id)"></div>
+      <template v-if="depth!==4">
+        <div class="icon" target="add" @click="addChild(id)"></div>
+      </template>
     </div>
   </div>
 </template>
 
 <script>
+import PriorityTag from '@/components/PriorityTag.vue';
+
 const getTagTypeFromState = (state) => {
   if (state === '已创建') {
     return '';
   } if (state === '进行中') {
     return 'success';
-  } if (state === '已完成') {
+  } if (state === '已完成' || state === '已结束') {
     return 'info';
   }
   return 'danger';
@@ -31,6 +39,7 @@ const getTagTypeFromState = (state) => {
 
 export default {
   name: 'TreeNode',
+  components: { PriorityTag },
   props: {
     id: Number,
     state: String,
@@ -38,12 +47,15 @@ export default {
     positionY: Number,
     positionX: Number,
     nodeStyle: Object,
+    depth: Number,
+    processor: String,
+    priority: String,
   },
   computed: {
     containerStyle() {
       return {
-        top: `${this.positionY * 130}px`,
-        left: `${280 * this.positionX}px`,
+        top: `${this.positionY * 140}px`,
+        left: `${280 * this.positionX - 100}px`,
         width: `${this.nodeStyle.width + this.nodeStyle.padding * 2}px`,
         height: `${this.nodeStyle.height + this.nodeStyle.padding * 2}px`,
       };
@@ -106,6 +118,10 @@ export default {
   display: flex;
   justify-content: space-between;
 }
+.card-body{
+  display: flex;
+  justify-content: space-between;
+}
 .icon {
   width: 14px;
   height: 14px;

+ 5 - 1
src/composable/useProject.ts

@@ -12,7 +12,11 @@ export const useProjectList = () => {
 
   const handleProjectChange = (projectId: number) => {
     WorkspaceDispatcher.changeProject(projectId);
-    router.push(`/project/${projectId}/schedule`);
+    if (router.currentRoute.value.path.includes('schedule')) {
+      router.go(0);
+    } else {
+      router.push(`/project/${projectId}/schedule`);
+    }
   };
   return {
     projectList,

+ 1 - 1
src/composable/useUser.ts

@@ -6,7 +6,7 @@ export const useUser = () => {
   const store = useStore();
 
   const userInfo = computed(() => store.state.user);
-  const userLoggedIn = computed(() => store.state.user.role !== 'GUEST');
+  const userLoggedIn = computed(() => store.state.user.role !== '游客');
 
   const userLogOut = () => {
     UserDispatcher.userLogOut();

+ 50 - 42
src/element-plus.ts

@@ -1,41 +1,45 @@
 import {
   ElAside,
+  ElButton,
+  ElButtonGroup,
   ElCard,
   ElCarousel,
   ElCarouselItem,
-  ElContainer,
+  ElCheckbox,
   ElCol,
+  ElContainer,
+  ElDialog,
   ElDivider,
   ElDrawer,
+  ElForm,
+  ElFormItem,
   ElHeader,
+  ElIcon,
+  ElInput,
+  ElInputNumber,
   ElLink,
+  ElLoading,
   ElMain,
   ElMenu,
-  ElDialog,
-  ElIcon,
-  ElInput,
-  ElSubmenu,
-  ElSwitch,
   ElMenuItem,
-  ElForm,
-  ElFormItem,
+  ElMessage,
+  ElMessageBox,
+  ElOption,
+  ElPagination,
+  ElRadio,
+  ElRadioGroup,
   ElRow,
+  ElSelect,
+  ElStep,
+  ElSteps,
+  ElSubmenu,
+  ElSwitch,
+  ElTabPane,
   ElTable,
   ElTableColumn,
-  ElTag,
-  ElPagination,
-  ElButton,
-  ElSelect,
-  ElOption,
   ElTabs,
-  ElTabPane,
-  ElButtonGroup,
-  ElInputNumber,
-  ElRadio,
-  ElRadioGroup,
+  ElTag,
   ElTooltip,
-  ElMessage,
-  ElMessageBox,
 } from 'element-plus';
 
 import ZH_CN from 'element-plus/lib/locale/lang/zh-cn';
@@ -45,43 +49,47 @@ locale.use(ZH_CN);
 
 export const Components = [
   ElAside,
-  ElRow,
-  ElContainer,
+  ElButton,
+  ElButtonGroup,
+  ElCard,
+  ElCarousel,
+  ElCarouselItem,
+  ElCheckbox,
   ElCol,
-  ElHeader,
-  ElMain,
-  ElMenu,
+  ElContainer,
   ElDialog,
+  ElDivider,
   ElDrawer,
-  ElLink,
-  ElMenuItem,
-  ElCarousel,
   ElForm,
   ElFormItem,
+  ElHeader,
   ElIcon,
   ElInput,
-  ElCarouselItem,
-  ElDivider,
-  ElCard,
-  ElTable,
-  ElTableColumn,
-  ElTag,
+  ElInputNumber,
+  ElLink,
+  ElMain,
+  ElMenu,
+  ElMenuItem,
+  ElOption,
   ElPagination,
-  ElButton,
+  ElRadio,
+  ElRadioGroup,
+  ElRow,
   ElSelect,
-  ElSwitch,
-  ElOption,
+  ElStep,
+  ElSteps,
   ElSubmenu,
-  ElTabs,
+  ElSwitch,
   ElTabPane,
-  ElButtonGroup,
-  ElInputNumber,
-  ElRadio,
-  ElRadioGroup,
+  ElTable,
+  ElTableColumn,
+  ElTabs,
+  ElTag,
   ElTooltip,
 ];
 
 export const Plugins = [
   ElMessage,
   ElMessageBox,
+  ElLoading,
 ];

+ 0 - 1
src/main.ts

@@ -8,7 +8,6 @@ const app = createApp(App);
 
 app.use(router);
 app.use(store, key);
-
 Components.forEach((component) => {
   app.component(component.name, component);
 });

+ 1 - 0
src/store/dispatchers/user.ts

@@ -6,4 +6,5 @@ export const updateUserInfo = () => {
 
 export const userLogOut = () => {
   store.dispatch('userLogOut');
+  store.dispatch('clearWorkspace');
 };

+ 8 - 7
src/store/modules/user.ts

@@ -7,16 +7,17 @@ const DEFAULT: UserState = {
   email: '',
   id: -1,
   phone: '',
-  role: 'GUEST',
+  role: '游客',
   withGitlab: false,
 };
 
 const actions: ActionTree<UserState, RootState> = {
   async updateUserInfo({ commit }) {
-    const res = (await UserAPI.getUserInfo()) as unknown as {
-      user_info: UserState;
-    };
-    commit('updateUserInfo', res.user_info);
+    const userinfo = await UserAPI.getUserInfo();
+    if (!userinfo) {
+      return;
+    }
+    commit('updateUserInfo', userinfo);
   },
   userLogOut({ commit }) {
     commit('updateUserInfo', DEFAULT);
@@ -25,9 +26,9 @@ const actions: ActionTree<UserState, RootState> = {
 
 const mutations: MutationTree<UserState> = {
   updateUserInfo(state, {
-    name, email, id, phone, role,
+    username, email, id, phone, role,
   }) {
-    state.username = name;
+    state.username = username;
     state.email = email;
     state.id = id;
     state.phone = phone;

+ 34 - 7
src/store/modules/workspace.ts

@@ -6,24 +6,39 @@ import {
   WorkspaceState, RootState, ChangeProjectPayload, UserState,
 } from '../typings';
 
+const NOT_SELECT_PROJECT_HINT = '未选择项目';
+
+const EMPTY_PROJECT_LIST_HINT = '暂无项目';
+
+const NO_SUCH_PROJECT_HINT = '未知项目';
+
 const DEFAULT: WorkspaceState = {
   currentProjectId: -1,
   currentProjectMembers: [],
   projectList: [],
 };
 
+const FROM_LOCAL_STORAGE = JSON.parse(localStorage.getItem('workspace') || '{}');
+
+const INIT = Object.keys(FROM_LOCAL_STORAGE).length > 0 ? FROM_LOCAL_STORAGE : DEFAULT;
+
 const actions: ActionTree<WorkspaceState, RootState> = {
+  clearWorkspace({ commit }) {
+    commit('updateProjectList', []);
+    commit('changeProject', { projectId: -1 });
+    commit('updateMembers', []);
+  },
   async refreshProjectList({ commit }) {
-    const projectList = await ProjectAPI.getProjectListByUserId();
+    const projectList = (await ProjectAPI.getProjectListByUserId()) || [];
     commit('updateProjectList', projectList);
   },
   async changeProject({ commit }, payload: ChangeProjectPayload) {
-    const members = await ProjectAPI.getProjectMembersByProjectId(payload.projectId);
+    const members = (await ProjectAPI.getProjectMembersByProjectId(payload.projectId)) || [];
     commit('updateMembers', members);
     commit('changeProject', payload);
   },
   resetCurrentProject({ commit }) {
-    commit('changeProject', -1);
+    commit('changeProject', { projectId: -1 });
   },
 };
 
@@ -41,11 +56,23 @@ const mutations: MutationTree<WorkspaceState> = {
 
 const getters: GetterTree<WorkspaceState, RootState> = {
   currentProjectInfo(state) {
-    return Array.prototype.find.call(state.projectList, (project) => project.id === state.currentProjectId)
-      || {
+    if (state.projectList.length === 0) {
+      return {
+        id: -1,
+        name: EMPTY_PROJECT_LIST_HINT,
+      };
+    }
+    if (state.currentProjectId === -1) {
+      return {
         id: -1,
-        name: '未选择项目',
+        name: NOT_SELECT_PROJECT_HINT,
       };
+    }
+    const currentProjectInfo = Array.prototype.find.call(state.projectList, (project) => project.id === state.currentProjectId);
+    return currentProjectInfo || {
+      id: -1,
+      name: NO_SUCH_PROJECT_HINT,
+    };
   },
   userOptions(state) {
     return state.currentProjectMembers.map((user) => ({
@@ -56,7 +83,7 @@ const getters: GetterTree<WorkspaceState, RootState> = {
 };
 
 export const workspace: Module<WorkspaceState, RootState> = {
-  state: () => DEFAULT,
+  state: () => INIT,
   actions,
   mutations,
   getters,

+ 3 - 3
src/typings/project.d.ts

@@ -18,10 +18,10 @@ declare interface TreeNode {
   id: number;
   leaf?: boolean;
   parentId?: number;
-  priority?: number;
+  priority?: RecordPriority;
   projectId?: number;
   startTime?: string;
-  state?: TreeNodeState;
+  state?: RecordState;
   timeToTake?: number;
   title?: string;
   type?: TreeNodeType;
@@ -36,4 +36,4 @@ declare interface CommitRecord {
   title: string;
 }
 
-declare type Role = 'TEACHER' | 'STUDENT' | 'ADMIN' | 'GUEST';
+declare type Role = '教师' | '学生' | '管理员' | '游客';

+ 15 - 2
src/utils/axios.ts

@@ -1,6 +1,7 @@
 /* eslint-disable */
 import axios from 'axios';
 import { TokenDispatcher } from '@/store/dispatchers';
+import {ElMessage} from "element-plus";
 
 const { destroyToken } = TokenDispatcher;
 
@@ -19,6 +20,10 @@ const _axios = axios.create(config);
 
 _axios.interceptors.request.use(
   (config) => {
+    const token = localStorage.getItem('token');
+    if (!token) {
+      return Promise.reject({message:'未登录', hidden: true});
+    }
     config.headers.Authorization = localStorage.getItem('token');
     return config;
   },
@@ -37,9 +42,17 @@ _axios.interceptors.response.use(
     }
     return response.data.data;
   },
-  (error) =>
+  (error) => {
     // Do something with response error
-    Promise.reject(error),
+    if (!error.hidden) {
+      ElMessage({
+        type: 'error',
+        showClose: true,
+        message: '接口出错,错误信息:' + error.message
+      })
+    }
+    Promise.reject(error)
+  }
 );
 
 export default _axios;

+ 1 - 1
src/utils/constants.ts

@@ -1,2 +1,2 @@
-export const stateOptions = [{ value: '已完成' }, { value: '进行中' }, { value: '已创建' }];
+export const stateOptions = [{ value: '已完成' }, { value: '进行中' }, { value: '已创建' }, { value: '已结束' }];
 export const priorityOptions = [{ value: 0, label: '紧急' }, { value: 1, label: '宽松' }, { value: 2, label: '一般' }];

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 1
src/utils/jsonlint.js


+ 11 - 0
src/utils/time.ts

@@ -0,0 +1,11 @@
+export const formatter = (timestamp: string | null) => {
+  if (!timestamp) return '';
+  const date = new Date(timestamp);
+  const year = date.getFullYear();
+  const month = `0${date.getMonth() + 1}`.substr(-2, 2);
+  const day = `0${date.getDate()}`.substr(-2, 2);
+  const hour = `0${date.getHours()}`.substr(-2, 2);
+  const minute = `0${date.getMinutes()}`.substr(-2, 2);
+  const second = `0${date.getSeconds()}`.substr(-2, 2);
+  return `${year}/${month}/${day} ${hour}:${minute}:${second}`;
+};

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 965 - 95
src/views/Project/APITest.vue


+ 8 - 2
src/views/Project/BugList.vue

@@ -3,6 +3,7 @@
   <GeneralTable
     :data="bugList"
     :showAdd="true"
+    :loading="loading"
     @add="addBugRecord"
     @row-click="openDrawer"
   />
@@ -10,7 +11,7 @@
   <BugAddDrawer
     ref="addDrawer"
     @finish-add="fetch"></BugAddDrawer>
-  <BugEditDrawer ref="editDrawer" v-bind="selectedBug" @bug-record-update="fetch">
+  <BugEditDrawer ref="editDrawer" v-bind="selectedBug" :commits="commits" @bug-record-update="fetch">
   </BugEditDrawer>
 </div>
 </template>
@@ -26,9 +27,11 @@ export default {
   components: { BugEditDrawer, BugAddDrawer, GeneralTable },
   data() {
     return {
+      loading: false,
       bugList: [],
       showAddBugDrawer: false,
       selectedBug: null,
+      commits: [],
     };
   },
   mounted() {
@@ -36,10 +39,13 @@ export default {
   },
   methods: {
     async fetch() {
+      this.loading = true;
       this.bugList = await BugListAPI.getBugListByProjectId(this.$store.state.workspace.currentProjectId);
+      this.loading = false;
     },
-    openDrawer(row) {
+    async openDrawer(row) {
       this.selectedBug = Array.prototype.find.call(this.bugList, (bug) => bug.id === row.id);
+      this.commits = await BugListAPI.getBugRecordCommitHistory(row.id);
       this.$refs.editDrawer.show();
       // 打开详情抽屉
       console.log(row);

+ 219 - 72
src/views/Project/Pipeline.vue

@@ -1,7 +1,7 @@
 <template>
-  <ElTabs class="tabs" v-model="activeTab">
+  <ElTabs class="tabs" v-model="activeTab"  v-loading.lock="loading">
     <ElTabPane label="流水线" name="pipeline">
-      <ElTable :data="pipelines" width="100%">
+      <ElTable :data="pipelines" width="100%" :default-sort="{prop: 'id', order: 'descending'}">
         <ElTableColumn
           prop="id"
           label="ID"
@@ -23,13 +23,13 @@
         <ElTableColumn
           label="最近执行时间"
           align="center"
-          :formatter="(row) => timeFormatter(row, 'startTime')"
+          :formatter="(row) => formatter(row.startTime)"
         ></ElTableColumn>
         <ElTableColumn
           prop="result"
           label="执行状态"
           align="center"
-          width="100"
+          width="110"
           :filters="[{ text: '成功', value: '成功' }, { text: '失败', value: '失败' }]"
           :filter-method="(value, row) => {
             return row.result === value;
@@ -46,6 +46,9 @@
             <template v-else-if="scope.row.result === '执行中'">
               <ElTag>{{scope.row.result}}<i class="el-icon-loading"></i></ElTag>
             </template>
+            <template v-else-if="scope.row.result === '无执行记录'">
+              <ElTag>{{scope.row.result}}</ElTag>
+            </template>
             <template v-else>
               <ElTag type="warning">未知状态</ElTag>
             </template>
@@ -57,7 +60,7 @@
         >
           <template #default="scope">
             <ElButton
-              @click="handleClick('detail', scope.row.id)"
+              @click="handleClick('detail', scope.row.recordId)"
               type="text"
               size="small"
               :disabled="scope.row.result==='执行中'"
@@ -80,23 +83,16 @@
         </ElTableColumn>
       </ElTable>
       <ElDialog v-model="showPipelineDetailModal" destroy-on-close>
-        <template #title>最近一次流水线构建的执行结果详情</template>
-        <pre class="pipeline-detail">{{detail}}</pre>
+        <template #title>历史执行结果详情</template>
+        <pre class="log" v-loading="dialogLoading" >{{detail}}</pre>
       </ElDialog>
       <ElDialog v-model="showPipelineHistoryModal" destroy-on-close>
         <template #title>流水线构建历史</template>
         <ElTable
           :data="pipelineHistory"
-          :default-sort="{
-            props: 'id',
-          }"
+          v-loading="dialogLoading"
           height="400"
         >
-          <ElTableColumn type="expand">
-            <template #default="scope">
-              <pre class="pipeline-detail">{{scope.row.details}}</pre>
-            </template>
-          </ElTableColumn>
           <ElTableColumn
             prop="id"
             label="构建ID"
@@ -118,36 +114,92 @@
           <ElTableColumn
             label="执行时间"
             align="center"
-            :formatter="(row) => timeFormatter(row, 'startTime')"
+            :formatter="(row) => formatter(row.startTime)"
           ></ElTableColumn>
+          <ElTableColumn
+            prop="result"
+            label="执行状态"
+            align="center"
+            width="100"
+            :filters="[{ text: '成功', value: '成功' }, { text: '失败', value: '失败' }]"
+            :filter-method="(value, row) => {
+              return row.result === value;
+            }"
+            filter-placement="bottom-end"
+          >
+            <template #default="scope">
+              <template v-if="scope.row.result === '成功'">
+                <ElTag type="success">{{scope.row.result}}</ElTag>
+              </template>
+              <template v-else-if="scope.row.result === '失败'">
+                <ElTag type="danger">{{scope.row.result}}</ElTag>
+              </template>
+              <template v-else-if="scope.row.result === '执行中'">
+                <ElTag>{{scope.row.result}}<i class="el-icon-loading"></i></ElTag>
+              </template>
+              <template v-else-if="scope.row.result === '无执行记录'">
+                <ElTag>{{scope.row.result}}</ElTag>
+              </template>
+              <template v-else>
+                <ElTag type="warning">未知状态</ElTag>
+              </template>
+            </template>
+          </ElTableColumn>
+          <ElTableColumn>
+            <template #default="scope">
+              <ElButton @click="handleClick('detail', scope.row.id)" type="text" size="small">查看详情</ElButton>
+            </template>
+          </ElTableColumn>
         </ElTable>
       </ElDialog>
       <ElDialog
         v-model="showPipelineEditModal"
-        :before-close="(done) => {
-          jsonValid=true;
-          done();
-        }"
+        width="80%"
         destroy-on-close
       >
         <template #title>
           <span>编辑流水线配置</span>
-          <span class="button-group">
-            <ElButton type="text">脚本教程</ElButton>
-          </span>
         </template>
-        <ElInput
-          type="textarea"
-          :rows="16"
-          placeholder="请输入流水线配置"
-          @input="lintJson"
-          v-model="pipelineConfig">
-        ></ElInput>
-        <pre v-if="!jsonValid" :innerText="hint"></pre>
+        <ElSteps :active="activeStep" v-loading="dialogLoading">
+          <ElStep
+            v-for="(config, index) in pipelineConfig"
+            :key="index"
+            class="step"
+            @click="() => activeStep = index"
+          >
+          <template #title>
+            <span>{{getStage(config.name).title}}</span>
+            <ElTooltip
+            class="tooltip"
+              effect="dark"
+              :content="getStage(config.name).description"
+              placement="right-end"
+            >
+              <i class="el-icon-question"></i>
+            </ElTooltip>
+          </template>
+          </ElStep>
+        </ElSteps>
+        <div class="edit-pipeline-form" v-for="(config, index) in pipelineConfig" :key="index" v-show="activeStep === index">
+          <ElForm label-width="160px">
+            <div style="margin: 20px 0 20px 160px;">
+              <ElCheckbox
+                :disabled="!getStage(config.name).skippable"
+                v-model="config.active"
+              >启用该阶段</ElCheckbox>
+            </div>
+            <ElFormItem v-for="(val, key) in config.configs" :key="key" :label="key">
+              <ElInput
+                :disabled="!getStage(config.name).configs.find((config) => config.name === key).editable"
+                v-model="config.configs[key]"
+              ></ElInput>
+            </ElFormItem>
+          </ElForm>
+        </div>
         <template #footer>
-          <span class="dialog-footer">
-            <el-button type="primary" @click="handlePipelineUpdateConfirm" :disabled="!jsonValid">确 定</el-button>
-          </span>
+          <ElButton :disabled="activeStep == 0" @click="() => activeStep -= 1">上一步</ElButton>
+          <ElButton :disabled="isLastStep"  @click="() => activeStep += 1">下一步</ElButton>
+          <ElButton type="primary" @click="handlePipelineUpdateConfirm">确定</ElButton>
         </template>
       </ElDialog>
       <ElDialog
@@ -170,6 +222,7 @@
           label-position="top"
           label-suffix=":"
           class="create-pipeline-form"
+          v-loading="dialogLoading"
         >
           <ElFormItem label="名称" prop="name" required>
             <ElInput v-model="createPipelineForm.name" placeholder="请输入流水线名称"></ElInput>
@@ -186,13 +239,13 @@
         </ElForm>
         <template #footer>
           <span class="dialog-footer">
-            <el-button type="primary" @click="handlePipelineCreateConfirm">确 定</el-button>
+            <el-button type="primary" v-loading="confirmLoading" @click="handlePipelineCreateConfirm">确 定</el-button>
           </span>
         </template>
       </ElDialog>
     </ElTabPane>
     <ElTabPane label="部署产物" name="production">
-      <ElTable :data="productions" width="100%">
+      <ElTable :data="productions" width="100%" :default-sort="{prop: 'id', order: 'descending'}">
         <ElTableColumn
           prop="id"
           label="ID"
@@ -203,62 +256,82 @@
           prop="name"
           label="名称"
           align="center"
+          width="100"
         ></ElTableColumn>
         <ElTableColumn
           prop="pipelineId"
           label="流水线ID"
           align="center"
-          width="100"
+          width="60"
         ></ElTableColumn>
         <ElTableColumn
           label="部署时间"
           align="center"
-          :formatter="(row) => timeFormatter(row, 'deployedTime')"
+          width="320"
+          :formatter="(row) => formatter(row.deployedTime)"
         ></ElTableColumn>
         <ElTableColumn
           label="URL"
           align="center"
         >
           <template #default="scope">
+            <ElTooltip effect="dark" :content="scope.row.accessUrl" placement="top-start">
               <ElButton type="text" @click="open(scope.row.accessUrl, '_blank')">
                 {{scope.row.accessUrl}}
               </ElButton>
+            </ElTooltip>
           </template>
         </ElTableColumn>
         <ElTableColumn
           label="运行状态"
           align="center"
+          width="100"
         >
           <template #default="scope">
-            <template v-if="scope.row.status === '正常'">
-              <ElTag type="success">{{scope.row.status}}</ElTag>
-            </template>
-            <template v-else>
-              <ElTag type="danger">{{scope.row.status}}</ElTag>
-            </template>
+            <ElTag :type="scope.row.status.indexOf('正常') > -1 ? 'success' : 'danger'">{{scope.row.status}}</ElTag>
+          </template>
+        </ElTableColumn>
+        <ElTableColumn
+          label="运行状态"
+          align="center"
+          width="100"
+        >
+          <template #default="scope">
+            <ElButton type="text" @click="showLog(scope.row.pipelineId)">查看日志</ElButton>
           </template>
         </ElTableColumn>
-        <ElTableColumn>
+        <ElTableColumn width="60">
           <template #default="scope">
-            <ElButton type="text" @click="deleteProduction(scope.row.id)">
+            <ElButton type="text" @click="deleteProduction(scope.row.pipelineId)">
               <i class="el-icon-delete"></i>
             </ElButton>
           </template>
         </ElTableColumn>
       </ElTable>
+      <ElDialog v-model="showProductionLogModal" destroy-on-close>
+        <template #title>
+          产物日志
+        </template>
+        <pre class="log" v-loading="dialogLoading" >{{currentLog}}</pre>
+        <template #footer>
+          <ElButton @click="refreshLog">刷新</ElButton>
+        </template>
+      </ElDialog>
     </ElTabPane>
   </ElTabs>
 </template>
 
 <script>
-import jsonlint from '@/utils/jsonlint';
-
 import { PipelineAPI } from '@/api';
-import debounce from '@/utils/debounce';
+
+import { formatter } from '@/utils/time';
 
 export default {
   data() {
     return {
+      loading: false,
+      dialogLoading: false,
+      confirmLoading: false,
       activeTab: 'pipeline',
       pipelines: [],
       productions: [],
@@ -276,16 +349,41 @@ export default {
         name: '',
         template: '',
       },
+      stages: [],
+      activeStep: 0,
+      showProductionLogModal: false,
+      currentLogId: -1,
+      currentLog: '',
     };
   },
+  computed: {
+    isLastStep() {
+      return this.activeStep === this.stages.length - 1;
+    },
+    stageConfigsBase() {
+      return this.stages.map((stage) => ({
+        name: stage.name,
+        active: true,
+        configs: stage.configs.map(({ name, value, editable }) => ({
+          [name]: value,
+          editable,
+        })).reduce((prev, curr) => Object.assign(prev, curr), {}),
+      }));
+    },
+    stageConfigs() {
+      return this.stageConfigsBase;
+    },
+  },
   async mounted() {
+    this.loading = true;
     this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
     this.productions = await PipelineAPI.getDeploymentsByProjectId(this.$store.state.workspace.currentProjectId);
     this.templates = await PipelineAPI.getPipelineTemplates();
+    this.stages = await PipelineAPI.getStages();
+    this.loading = false;
   },
   methods: {
-    timeFormatter: (row, property) => `${row[property].year}/${row[property].month}/${row[property].day} `
-            + `${row[property].hours}:${row[property].minutes}:${row[property].seconds}`,
+    formatter,
     async handleClick(action, id) {
       switch (action) {
         case 'create': {
@@ -293,40 +391,59 @@ export default {
           break;
         }
         case 'trigger': {
+          this.loading = true;
           await PipelineAPI.triggerDeployment({
             projectId: this.$store.state.workspace.currentProjectId,
             pipelineId: id,
           });
-          this.productions = await PipelineAPI.getDeploymentsByProjectId(this.$store.state.workspace.currentProjectId);
+
+          PipelineAPI.getDeploymentsByProjectId(this.$store.state.workspace.currentProjectId)
+            .then(
+              (res) => {
+                this.productions = res;
+              },
+            );
+          this.loading = true;
+          this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
+          this.loading = false;
           break;
         }
         case 'detail': {
           this.showPipelineDetailModal = true;
-          this.detail = Array.prototype.find.call(this.pipelines, (pipeline) => pipeline.id === id).details;
+          this.dialogLoading = true;
+          this.detail = await PipelineAPI.getPipelineRecordDetail(id);
+          this.dialogLoading = false;
           break;
         }
         case 'history': {
           this.showPipelineHistoryModal = true;
-          this.pipelineHistory = await PipelineAPI.getPipelineHistory({
+          this.dialogLoading = true;
+          this.pipelineHistory = await PipelineAPI.getPipelineRecordList({
             projectId: this.$store.state.workspace.currentProjectId,
             pipelineId: id,
           });
+          this.dialogLoading = false;
           break;
         }
         case 'edit': {
           this.showPipelineEditModal = true;
+          this.dialogLoading = true;
           this.editingPipelineId = id;
-          this.pipelineConfig = await PipelineAPI.getPipeline({
+          const config = (await PipelineAPI.getPipeline({
             projectId: this.$store.state.workspace.currentProjectId,
             pipelineId: id,
-          }).configJson || '{}';
+          })).configJson;
+          this.pipelineConfig = JSON.parse(config || '{}');
+          this.dialogLoading = false;
           break;
         }
         case 'delete': {
+          this.loading = true;
           PipelineAPI.deletePipeline({
             projectId: this.$store.state.workspace.currentProjectId,
             pipelineId: id,
           });
+          this.loading = false;
           break;
         }
         default: {
@@ -334,43 +451,67 @@ export default {
         }
       }
     },
-    // eslint-disable-next-line func-names
-    lintJson: debounce(function (value) {
-      this.jsonValid = true;
-      try {
-        const result = jsonlint.parse(value);
-        this.pipelineConfig = JSON.stringify(result, null, '  ');
-      } catch (e) {
-        this.jsonValid = false;
-        this.hint = `${value}\n${e}`;
-      }
-    }, 500),
     async handlePipelineUpdateConfirm() {
+      this.confirmLoading = true;
       await PipelineAPI.updatePipeline({
         projectId: this.$store.state.workspace.currentProjectId,
         pipelineId: this.editingPipelineId,
-        configJson: JSON.stringify(JSON.parse(this.pipelineConfig)),
+        configJson: JSON.stringify(this.pipelineConfig),
       });
+      this.confirmLoading = false;
       this.showPipelineEditModal = false;
+      this.loading = true;
+      this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
+      this.loading = false;
     },
     async handlePipelineCreateConfirm() {
+      this.confirmLoading = true;
       this.$refs.createPipelineForm.validate(async (valid) => {
         if (valid) {
           await PipelineAPI.createPipeline({
             projectId: this.$store.state.workspace.currentProjectId,
-            pipelineId: this.editingPipelineId,
             name: this.createPipelineForm.name,
             templateName: this.createPipelineForm.template,
           });
           this.showPipelineCreateModal = false;
         }
       });
+      this.confirmLoading = false;
+      this.loading = true;
+      this.pipelines = await PipelineAPI.getPipelinesByProjectId(this.$store.state.workspace.currentProjectId);
+      this.loading = false;
     },
     open(url, target) {
       window.open(url, target);
     },
-    async deleteProduction(id) {
-      await PipelineAPI.deleteInstance(id);
+    async deleteProduction(pipelineId) {
+      this.loading = true;
+      await PipelineAPI.deleteInstance({
+        projectId: this.$store.state.workspace.currentProjectId,
+        pipelineId,
+      });
+      this.loading = false;
+    },
+    getStage(stageName) {
+      return this.stages.find((stage) => stage.name === stageName);
+    },
+    async showLog(pipelineId) {
+      this.currentLogId = pipelineId;
+      this.showProductionLogModal = true;
+      this.dialogLoading = true;
+      this.currentLog = await PipelineAPI.getDeploymentLog({
+        projectId: this.$store.state.workspace.currentProjectId,
+        pipelineId,
+      });
+      this.dialogLoading = false;
+    },
+    async refreshLog() {
+      this.dialogLoading = true;
+      this.currentLog = await PipelineAPI.getDeploymentLog({
+        projectId: this.$store.state.workspace.currentProjectId,
+        pipelineId: this.currentLogId,
+      });
+      this.dialogLoading = false;
     },
   },
 };
@@ -384,7 +525,7 @@ export default {
 .add-button {
   margin-left: 14px;
 }
-.pipeline-detail {
+.log {
   width: 100%;
   word-break: break-word;
   white-space: pre-wrap;
@@ -393,7 +534,13 @@ export default {
   width: 60%;
   margin: 0 auto;
 }
+.edit-pipeline-form {
+  margin-top: 20px;
+}
 .button-group {
   margin-left: 40px;
 }
+.step {
+  cursor: pointer;
+}
 </style>

+ 138 - 7
src/views/Project/Schedule.vue

@@ -1,11 +1,40 @@
 <template>
-  <el-row style="display: flex;flex-direction: row;align-items: center;margin-bottom: 10px;color: #333333">
+  <el-row  style="display: flex;flex-direction: row;align-items: center;margin-bottom: 10px;color: #333333">
+    <h4>项目概览</h4>
+  </el-row>
+  <el-card shadow="never" class='description-card'>
+    <el-row>
+      <el-col :span="12">
+        <div class="description-card-text" style="font-size: 18px;font-weight: bold">{{selectedProject.name}}</div>
+        <div class="description-card-text">项目地址:
+          <span><a target="_blank" :href="selectedProject.gitRemoteUrl">{{selectedProject.gitRemoteUrl}}</a></span>
+        </div>
+        <div class="description-card-text">项目介绍:
+          <span>{{selectedProject.description}}</span>
+        </div>
+        <div class="description-card-text">项目成员:
+          <span>
+            <el-tag v-for="item in projectMembers" :key="item.id" style="margin: 0 5px;">
+              {{item.username}}
+            </el-tag>
+          </span>
+        </div>
+      </el-col>
+      <el-col :span="6">
+        <div style="height: 140px;width: 140px" id="requirement_pie"></div>
+      </el-col>
+      <el-col :span="6">
+        <div style="height: 140px;width: 140px" id="bug_pie"></div>
+      </el-col>
+    </el-row>
+  </el-card>
+  <el-row  style="display: flex;flex-direction: row;align-items: center;margin-bottom: 10px;color: #333333">
     <h4>项目规划</h4>
     <el-tooltip class="item" effect="dark" :content="scheduleGuide" placement="right-end">
       <i class="el-icon-question"></i>
     </el-tooltip>
   </el-row>
-  <Tree :data="requirements" @change="handleTreeChange" @nodeClick="handleNodeClick" ref="tree"></Tree>
+  <Tree :data="requirements" v-loading="loading" @change="handleTreeChange" @nodeClick="handleNodeClick" ref="tree"></Tree>
   <RequirementDrawer
     ref="drawer"
     v-bind="requirementInDrawer"
@@ -16,44 +45,72 @@
 
 <script>
 import Tree from '@/components/Tree.vue';
-
+import * as echarts from 'echarts';
 import { RequirementAPI } from '@/api';
 import RequirementDrawer from '@/components/RequirementDrawer.vue';
+import { useProjectList } from '../../composable/useProject';
 
 const scheduleGuide = '项目规划用于asdsadsadda';
 export default {
   name: 'Schedule',
   components: { RequirementDrawer, Tree },
+  setup() {
+    const { selectedProject } = useProjectList();
+    return {
+      selectedProject,
+    };
+  },
   data() {
     return {
       scheduleGuide,
+      loading: false,
       requirements: [],
       showRequirementDrawer: false,
       addRequirementDrawer: false,
       parentOfAddingRequirement: null,
       requirementInDrawer: {},
+      projectMembers: [],
     };
   },
   async mounted() {
+    this.projectMembers = this.$store.state.workspace.currentProjectMembers;
     await this.getTreeData();
+    this.drawChart();
   },
   methods: {
     async getTreeData() {
-      this.requirements = this.addChild2Node(await RequirementAPI.getTreeNodesByProjectId(this.$store.state.workspace.currentProjectId));
+      this.loading = true;
+      try {
+        this.requirements = this.addChild2Node(await RequirementAPI.getTreeNodesByProjectId(this.$store.state.workspace.currentProjectId));
+      } catch (e) {
+        console.error(e);
+      }
+      this.loading = false;
     },
     addChild2Node(array) {
       const nodeMap = new Map();
       array.forEach((item) => nodeMap.set(item.id, item));
       array.forEach((item) => {
-        if (item.parent) {
-          const tempParent = nodeMap.get(item.parent);
+        if (item.parentId) {
+          const tempParent = nodeMap.get(item.parentId);
           if (!tempParent.childs) {
             tempParent.childs = [];
           }
           tempParent.childs.push(item);
         }
       });
-      return array;
+      // 返回前序遍历的列表
+      const newArr = [];
+      const preOrder = (node) => {
+        newArr.push(node);
+        if (!node.childs || node.childs.length === 0) return;
+        node.childs.sort((node1, node2) => node1.id - node2.id);
+        newArr.concat(node.childs);
+        node.childs.forEach((_node) => preOrder(_node));
+      };
+      const root = array.find((item) => !item.parentId);
+      preOrder(root);
+      return newArr;
     },
     async handleTreeChange(msg) {
       const { type, id } = msg;
@@ -67,10 +124,84 @@ export default {
     handleDrawerContentChange(id) {
       this.requirementInDrawer = Array.prototype.find.call(this.requirements, (requirement) => requirement.id === id);
     },
+    drawChart() {
+      const requirementPie = echarts.init(document.getElementById('requirement_pie'));
+      requirementPie.setOption({
+        tooltip: {
+          trigger: 'item',
+        },
+        series: [
+          {
+            name: '任务情况',
+            type: 'pie',
+            radius: ['40%', '70%'],
+            avoidLabelOverlap: false,
+            label: {
+              show: false,
+              position: 'center',
+            },
+            emphasis: {
+              label: {
+                show: true,
+                fontSize: '10',
+                fontWeight: 'bold',
+              },
+            },
+            labelLine: {
+              show: false,
+            },
+            data: [
+              { value: 1048, name: '已完成' },
+              { value: 735, name: '进行中' },
+              { value: 580, name: '已结束' },
+            ],
+          },
+        ],
+      });
+      const bugPie = echarts.init(document.getElementById('bug_pie'));
+      bugPie.setOption({
+        tooltip: {
+          trigger: 'item',
+        },
+        series: [
+          {
+            name: '任务情况',
+            type: 'pie',
+            radius: ['40%', '70%'],
+            avoidLabelOverlap: false,
+            label: {
+              show: false,
+              position: 'center',
+            },
+            emphasis: {
+              label: {
+                show: true,
+                fontSize: '10',
+                fontWeight: 'bold',
+              },
+            },
+            labelLine: {
+              show: false,
+            },
+            data: [
+              { value: 1048, name: '已完成' },
+              { value: 735, name: '进行中' },
+              { value: 580, name: '已结束' },
+            ],
+          },
+        ],
+      });
+    },
   },
 };
 </script>
 
 <style>
+.description-card{
+  margin: 10px 0;
+}
+.description-card-text{
+  margin: 5px 0;
+}
 
 </style>

+ 8 - 2
src/views/Project/TaskList.vue

@@ -2,13 +2,14 @@
   <div class="table-container">
     <GeneralTable
       :data="taskList"
+      :loading="loading"
       @row-click="openDrawer"
     />
   </div>
   <RequriementDrawer
     ref="drawer"
     v-bind="requirementInDrawer"
-    @change="handleDrawerContentChange"
+    :commits="commits"
     @requirement-update="fetch"
     :showSons="false"
   ></RequriementDrawer>
@@ -24,10 +25,12 @@ export default {
   components: { GeneralTable, RequriementDrawer },
   data() {
     return {
+      loading: false,
       taskList: [],
       requirementInDrawer: {},
       stateOptions: [{ value: '已完成' }, { value: '进行中' }, { value: '已创建' }],
       priorityOptions: [{ value: 0, label: '紧急' }, { value: 1, label: '宽松' }, { value: 2, label: '一般' }],
+      commits: [],
     };
   },
   mounted() {
@@ -35,10 +38,13 @@ export default {
   },
   methods: {
     async fetch() {
+      this.loading = true;
       this.taskList = await RequirementAPI.getTasksByProjectId(this.$store.state.workspace.currentProjectId);
+      this.loading = false;
     },
-    openDrawer(row) {
+    async openDrawer(row) {
       this.requirementInDrawer = row;
+      this.commits = await RequirementAPI.getTaskRecordCommitHistory(row.id);
       this.$refs.drawer.show();
     },
   },

+ 20 - 0
yarn.lock

@@ -3646,6 +3646,14 @@ ecdsa-sig-formatter@1.0.11:
   dependencies:
     safe-buffer "^5.0.1"
 
+echarts@^5.0.2:
+  version "5.1.1"
+  resolved "https://registry.nlark.com/echarts/download/echarts-5.1.1.tgz?cache=0&sync_timestamp=1619495447964&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fecharts%2Fdownload%2Fecharts-5.1.1.tgz#b186f162f017c555cfd67b12ede6762bdc3ddfda"
+  integrity sha1-sYbxYvAXxVXP1nsS7eZ2K9w939o=
+  dependencies:
+    tslib "2.0.3"
+    zrender "5.1.0"
+
 ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.npm.taobao.org/ee-first/download/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -9009,6 +9017,11 @@ tsconfig-paths@^3.9.0:
     minimist "^1.2.0"
     strip-bom "^3.0.0"
 
+tslib@2.0.3:
+  version "2.0.3"
+  resolved "https://registry.npm.taobao.org/tslib/download/tslib-2.0.3.tgz?cache=0&sync_timestamp=1617647331485&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftslib%2Fdownload%2Ftslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c"
+  integrity sha1-jgdBrEX8DCJuWKF7/D5kubxsphw=
+
 tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
   version "1.14.1"
   resolved "https://registry.npm.taobao.org/tslib/download/tslib-1.14.1.tgz?cache=0&sync_timestamp=1609887512458&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftslib%2Fdownload%2Ftslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@@ -9782,3 +9795,10 @@ yorkie@^2.0.0:
     is-ci "^1.0.10"
     normalize-path "^1.0.0"
     strip-indent "^2.0.0"
+
+zrender@5.1.0:
+  version "5.1.0"
+  resolved "https://registry.nlark.com/zrender/download/zrender-5.1.0.tgz#b6a84c3aa7ccc6642ee0519901ca4c0835c4d85e"
+  integrity sha1-tqhMOqfMxmQu4FGZAcpMCDXE2F4=
+  dependencies:
+    tslib "2.0.3"

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů