Explorar o código

Merge branch 'dev' into dev_bugList

Jinyao %!s(int64=5) %!d(string=hai) anos
pai
achega
cfade5ba19
Modificáronse 69 ficheiros con 2265 adicións e 437 borrados
  1. 2 0
      .env
  2. 3 0
      .env.dev
  3. 3 0
      .env.dev-mock
  4. 2 0
      .env.prod
  5. 2 0
      .eslintrc.js
  6. 10 0
      Dockerfile
  7. 15 3
      README.md
  8. 0 2
      babel.config.js
  9. 13 5
      mock/index.js
  10. 70 0
      mock/routers/buglist.js
  11. 45 0
      mock/routers/deployment.js
  12. 114 0
      mock/routers/pipeline.js
  13. 48 12
      mock/routers/project.js
  14. 9 24
      mock/routers/user.js
  15. 5 2
      package.json
  16. 19 15
      src/App.vue
  17. 34 0
      src/api/devcloud/buglist.ts
  18. 60 0
      src/api/devcloud/pipeline.ts
  19. 7 0
      src/api/devcloud/prefix.ts
  20. 22 0
      src/api/devcloud/project.ts
  21. 15 0
      src/api/devcloud/tree.ts
  22. 15 0
      src/api/devcloud/user.ts
  23. 10 3
      src/api/index.ts
  24. 5 0
      src/api/portal.ts
  25. 0 7
      src/api/project.ts
  26. 0 12
      src/api/user.ts
  27. 1 0
      src/assets/icon/add.svg
  28. 1 0
      src/assets/icon/delete.svg
  29. 189 117
      src/components/Menu.vue
  30. 41 0
      src/components/MissionDetailsForm.vue
  31. 11 22
      src/components/ProjectCard.vue
  32. 1 0
      src/components/RightForm.vue
  33. 68 99
      src/components/SideBar.vue
  34. 265 0
      src/components/Tree.vue
  35. 101 0
      src/components/TreeNode.vue
  36. 9 12
      src/composable/useProject.ts
  37. 21 0
      src/composable/useUser.ts
  38. 33 0
      src/element-plus.ts
  39. 6 7
      src/main.ts
  40. 12 19
      src/router/index.ts
  41. 9 0
      src/store/dispatchers/index.ts
  42. 11 0
      src/store/dispatchers/token.ts
  43. 9 0
      src/store/dispatchers/user.ts
  44. 9 0
      src/store/dispatchers/workspace.ts
  45. 14 19
      src/store/index.ts
  46. 9 0
      src/store/modules/index.ts
  47. 32 0
      src/store/modules/token.ts
  48. 42 0
      src/store/modules/user.ts
  49. 51 0
      src/store/modules/workspace.ts
  50. 33 0
      src/store/typings.ts
  51. 24 0
      src/typings/project.d.ts
  52. 0 0
      src/typings/shims-vue.d.ts
  53. 28 0
      src/typings/vuex.d.ts
  54. 15 18
      src/utils/axios.ts
  55. 15 0
      src/utils/debounce.ts
  56. 1 0
      src/utils/jsonlint.js
  57. 10 0
      src/utils/qs.ts
  58. 11 0
      src/utils/token.ts
  59. 4 0
      src/utils/user.ts
  60. 0 4
      src/utils/userStatus.ts
  61. 11 8
      src/views/Home.vue
  62. 3 3
      src/views/Login.vue
  63. 2 1
      src/views/Project/BugList.vue
  64. 390 4
      src/views/Project/Pipeline.vue
  65. 63 2
      src/views/Project/Schedule.vue
  66. 83 4
      src/views/Project/TaskList.vue
  67. 11 13
      src/views/ProjectContainer.vue
  68. 8 0
      vue.config.js
  69. 80 0
      yarn.lock

+ 2 - 0
.env

@@ -0,0 +1,2 @@
+VUE_APP_PORTAL_HOST='p.seecoder.cn'
+VUE_APP_API_URL = '/api'

+ 3 - 0
.env.dev

@@ -0,0 +1,3 @@
+NODE_ENV = development
+VUE_APP_API_URL = 'http://localhost:8080'
+VUE_APP_ENV_NAME = '[开发环境]'

+ 3 - 0
.env.dev-mock

@@ -0,0 +1,3 @@
+NODE_ENV = development-mock
+VUE_APP_API_URL = 'http://localhost:4000/api'
+VUE_APP_ENV_NAME = '[Mock开发环境]'

+ 2 - 0
.env.prod

@@ -0,0 +1,2 @@
+NODE_ENV = production
+VUE_APP_ENV_NAME = ''

+ 2 - 0
.eslintrc.js

@@ -16,6 +16,8 @@ module.exports = {
     'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
     'import/prefer-default-export': 'off',
     'max-len': ['error', { code: 140, ignoreTrailingComments: true }],
+    'no-param-reassign': [2, { props: false }],
     'linebreak-style': [0, 'error', 'windows'],
+    'import/no-cycle': 0,
   },
 };

+ 10 - 0
Dockerfile

@@ -0,0 +1,10 @@
+FROM node:10-alpine as builder
+WORKDIR /code
+RUN yarn config set registry https://registry.npm.taobao.org/
+ADD package.json /code
+RUN yarn install
+ADD . /code
+RUN yarn build:prod
+
+FROM nginx:1.19-alpine
+COPY --from=builder /code/dist/ /var/www/html/dist/

+ 15 - 3
README.md

@@ -1,11 +1,23 @@
 # devcloud-frontend
 
+## 命令介绍
+
 请使用`yarn`作为包管理器
 
 `yarn mock`启动`mock server`。
 
-`yarn serve`启动本地服务.
+`yarn serve`启动本地服务,需搭配本地运行在8080端口的后端服务。
+
+`yarn serve:mock` 启动本地服务,需搭配`mock server`进行使用。
+
+`yarn build:test`执行线上测试环境打包构建。
+
+`yarn build:prod`执行生产环境打包构建。
+
+`yarn lint --fix`进行格式化。
+
+## 环境变量说明
 
-`yarn build`执行构建。
+`.env`文件中,声明单点登录门户网站`host`
 
-`yarn lint`进行格式化。
+`.env.[mode]`文件中,声明当前环境的接口地址`API_URL`与环境名`ENV_NAME`

+ 0 - 2
babel.config.js

@@ -4,8 +4,6 @@ module.exports = {
       'import',
       {
         libraryName: 'element-plus',
-        // 由于 customStyleName 在配置中被声明的原因,`style: true` 会被直接忽略掉,
-        // 如果你需要使用 scss 源文件,把文件结尾的扩展名从 `.css` 替换成 `.scss` 就可以了
         customStyleName: (name) => `element-plus/lib/theme-chalk/${name}.css`
         ,
       },

+ 13 - 5
mock/index.js

@@ -11,15 +11,23 @@ app.use(bodyParser.json());
 // 添加跨域
 app.use((req, res, next) => {
   res.header('Access-Control-Allow-Origin', '*');
-  res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS');
-  res.header('Access-Control-Allow-Headers', 'X-Requested-With');
-  res.header('Access-Control-Allow-Headers', 'Content-Type');
-  next();
+  res.header('Access-Control-Allow-Methods', '*');
+  res.header('Access-Control-Allow-Headers', '*');
+  if (req.method === 'OPTIONS') {
+    res.sendStatus(200);
+  } else {
+    next();
+  }
 });
 
 const prefix = '/api';
 
-app.use(`${prefix}/user`, require('./routers/user'));
+app.use(`${prefix}/users`, require('./routers/user'));
 app.use(`${prefix}/project`, require('./routers/project'));
+app.use(`${prefix}/pipelines`, require('./routers/pipeline'));
+app.use(`${prefix}/deployments`, require('./routers/deployment'));
+app.use(`${prefix}/bug_list`, require('./routers/buglist'));
 
 app.listen(4000);
+
+console.log('Mock Server is now running at port 4000.');

+ 70 - 0
mock/routers/buglist.js

@@ -0,0 +1,70 @@
+/* eslint-disable max-len */
+/* 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.post('', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.put('', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.post('/finish', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+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: 'time',
+            title: 'titleeee',
+          },
+        ],
+        description: 'bug description',
+        endTime: {
+          date: 0,
+          day: 0,
+          hours: 0,
+          minutes: 0,
+          month: 0,
+          nanos: 0,
+          seconds: 0,
+          time: 0,
+          timezoneOffset: 0,
+          year: 0,
+        },
+        expectedTimeCost: 140,
+        'id|+5': 1001,
+        priority: ['紧急', '一般', '宽松'],
+        startTime: {
+          date: 0,
+          day: 0,
+          hours: 0,
+          minutes: 0,
+          month: 0,
+          nanos: 0,
+          seconds: 0,
+          time: 0,
+          timezoneOffset: 0,
+          year: 0,
+        },
+        'state|1': ['CREATED', 'PROCESSING', 'FINISHED'],
+        title: 'bug titleeee',
+      },
+    },
+  ).data));
+});
+
+module.exports = router;

+ 45 - 0
mock/routers/deployment.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.delete('', (_, res) => {
+  res.json(wrapper('success'));
+});
+
+router.post('', (_, res) => {
+  res.json(wrapper('success'));
+});
+
+router.get('/list', (req, res) => {
+  res.json(wrapper(
+    Mock.mock(
+      {
+        'data|10': [{
+          accessUrl: Random.url('http', 'production.seecoder.cn'),
+          'name|1': ['MySQL', 'Java', 'Node'],
+          status: '正常',
+          'id|+5': 101,
+          pipelineId: 100,
+          deployedTime: {
+            date: 0,
+            day: 1,
+            'hours|+1': 12,
+            'minutes|+1': 30,
+            month: 3,
+            nanos: 0,
+            'seconds|+1': 15,
+            time: 0,
+            timezoneOffset: 0,
+            year: 2021,
+          },
+        }],
+      },
+    ).data,
+  ));
+});
+
+module.exports = router;

+ 114 - 0
mock/routers/pipeline.js

@@ -0,0 +1,114 @@
+/* eslint-disable max-len */
+/* 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.delete('', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.get('', (req, res) => {
+  res.json(wrapper({
+    configJson: {
+      code: 0,
+      data: {
+        configJson: 'string',
+        id: 0,
+      },
+      msg: 'string',
+    },
+    id: req.params.pipelineId,
+  }));
+});
+
+router.post('', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.get('/templates', (req, res) => {
+  res.json(wrapper(['Java', 'MySQL', 'Node', 'Go', 'Android']));
+});
+
+router.put('/config', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.get('/history/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': ['成功', '失败', '执行中'],
+          startTime: {
+            date: 0,
+            day: 1,
+            'hours|+1': 12,
+            'minutes|+1': 30,
+            month: 3,
+            nanos: 0,
+            'seconds|+1': 15,
+            time: 0,
+            timezoneOffset: 0,
+            year: 2021,
+          },
+          username: 'hkshu12',
+        }],
+      },
+    ).data,
+  ));
+});
+
+router.get('/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|+1': 100,
+          name: Random.cname(),
+          'result|1': ['成功', '失败', '执行中'],
+          startTime: {
+            date: 0,
+            day: 1,
+            'hours|+1': 12,
+            'minutes|+1': 30,
+            month: 3,
+            nanos: 0,
+            'seconds|+1': 15,
+            time: 0,
+            timezoneOffset: 0,
+            year: 2021,
+          },
+          username: 'hkshu12',
+        }],
+      },
+    ).data,
+  ));
+});
+
+module.exports = router;

+ 48 - 12
mock/routers/project.js

@@ -6,21 +6,57 @@ const wrapper = require('../wrapper');
 
 const { Random } = Mock;
 
-router.get('/all',
+const groups = [101, 102, 103, 104];
+
+router.get('/:projectId',
+  (req, res) => {
+    const { projectId: id } = req.params;
+    res.json(wrapper(
+      Mock.mock(
+        {
+          description: Random.csentence(5, 40),
+          gitRemoteUrl: Random.url('http', 'gitlab.seecoder.cn'),
+          groupId: 101,
+          id,
+          name: 'string',
+        },
+      ),
+    ));
+  });
+
+router.post('/create',
+  (req, res) => {
+    const { body } = req;
+    body.id = 101;
+    res.json(wrapper(body));
+  });
+
+router.get('/listByUser/:userId',
   (req, res) => {
-    const query = req.query || {};
-    const { userId } = query;
     res.json(wrapper(
-      Mock.mock({
-        userId,
-        'projectList|1-10': [
-          {
-            'projectId|+1': 100,
-            name: Random.ctitle(10),
-          },
-        ],
-      }),
+      Mock.mock(Mock.mock({
+        'data|5-10': [{
+          'id|+1': 100,
+          description: Random.csentence(5, 40),
+          gitRemoteUrl: Random.url('http', 'gitlab.seecoder.cn'),
+          'groupId|1': groups,
+          name: Random.cname(),
+        }],
+      }).data),
     ));
   });
 
+router.get('/delete/:projectId',
+  (req, res) => {
+    res.json(wrapper({}));
+  });
+
+router.get('/members', (req, res) => {
+  res.json(wrapper([101, 102, 103, 104]));
+});
+
+router.get('/refresh', (req, res) => {
+  res.json(wrapper('success'));
+});
+
 module.exports = router;

+ 9 - 24
mock/routers/user.js

@@ -1,34 +1,19 @@
 /* eslint-disable import/no-extraneous-dependencies */
 /* eslint-disable @typescript-eslint/no-var-requires */
 const router = require('express').Router();
+const jwt = require('jsonwebtoken');
 const wrapper = require('../wrapper');
 
-router.get('/info/:id',
-  (req, res) => res.json(wrapper({
-    id: req.params.id,
-    username: 'kenny',
-  })));
-// query参数可以通过req.query.name取得
-router.get('/info',
+const SECRET = 'SEEC-1919810-OIDC-114514';
+
+router.get('/self',
   (req, res) => {
-    const query = req.query || {};
-    return res.json(wrapper({
-      limit: query.limit,
-      offset: query.offset,
-      list: [{
-        username: 'admin1',
-        sex: 1,
-      }, {
-        username: 'admin2',
-        sex: 0,
-      }],
-    }));
+    const userinfo = jwt.decode(req.headers.authorization, SECRET);
+    res.json(wrapper(userinfo));
   });
+module.exports = router;
 
-router.post('/post',
+router.post('/gitlab',
   (req, res) => {
-    console.log(req.body);
-    res.end();
+    res.json(wrapper('success'));
   });
-
-module.exports = router;

+ 5 - 2
package.json

@@ -3,8 +3,10 @@
   "version": "0.1.0",
   "private": true,
   "scripts": {
-    "serve": "vue-cli-service serve",
-    "build": "vue-cli-service build",
+    "serve": "vue-cli-service serve --port 3000 --mode dev",
+    "serve:mock": "vue-cli-service serve --port 3000 --mode dev-mock",
+    "build:test": "vue-cli-service build --mode test",
+    "build:prod": "vue-cli-service build --mode prod",
     "lint": "vue-cli-service lint",
     "mock": "node mock/index.js"
   },
@@ -12,6 +14,7 @@
     "axios": "^0.21.1",
     "core-js": "^3.6.5",
     "element-plus": "^1.0.2-beta.31",
+    "jsonwebtoken": "^8.5.1",
     "vue": "^3.0.0",
     "vue-axios": "^3.2.4",
     "vue-class-component": "^8.0.0-0",

+ 19 - 15
src/App.vue

@@ -1,28 +1,32 @@
 <template>
-  <Menu
-    :project-list="projectList"
-    :selected-project="selectedProject"
-    @project-change="handleProjectChange"></Menu>
-  <router-view/>
+  <el-header>
+    <Menu></Menu>
+  </el-header>
+  <el-container>
+    <router-view/>
+  </el-container>
 </template>
 
 <script lang="ts">
-import {
-  defineComponent,
-} from 'vue';
+import { defineComponent } from 'vue';
 import Menu from '@/components/Menu.vue';
 
-import { useProject } from '@/composable/useProject';
+import { getVariableFromQueryString } from '@/utils/qs';
+import { WorkspaceDispatcher, UserDispatcher, TokenDispatcher } from './store/dispatchers';
+import { useStore } from './store';
 
 export default defineComponent({
   components: { Menu },
   setup() {
-    const { projectList, selectedProject, handleProjectChange } = useProject();
-    return {
-      projectList,
-      selectedProject,
-      handleProjectChange,
-    };
+    const store = useStore();
+    console.log(store.state);
+    const token = getVariableFromQueryString('id_token');
+    if (token) {
+      TokenDispatcher.updateToken(token);
+    }
+
+    UserDispatcher.updateUserInfo();
+    WorkspaceDispatcher.refreshProjectList();
   },
 });
 </script>

+ 34 - 0
src/api/devcloud/buglist.ts

@@ -0,0 +1,34 @@
+import axios from '@/utils/axios';
+
+import { BUGLIST_PREFIX } from './prefix';
+
+interface BugRecordPayload {
+  description: string;
+  expectedTimeCost: number;
+  priority: '紧急'|'一般'|'宽松';
+  title: string;
+}
+
+type CreateBugRecordPayload = BugRecordPayload & {
+  projectId: number;
+}
+
+type UpdateBugRecordPayload = BugRecordPayload & {
+  id: number;
+}
+
+export const createBugRecord = (createBugRecordPayload: CreateBugRecordPayload) => axios.post(`${BUGLIST_PREFIX}`, createBugRecordPayload);
+
+export const updateBugRecord = (updateBugRecordPayload: UpdateBugRecordPayload) => axios.post(`${BUGLIST_PREFIX}`, updateBugRecordPayload);
+
+export const finishBugRecord = (id: number) => axios.post(`${BUGLIST_PREFIX}/finish`, null, {
+  params: {
+    id,
+  },
+});
+
+export const getBugListByProjectId = (projectId: number) => axios.get(`${BUGLIST_PREFIX}/list`, {
+  params: {
+    projectId,
+  },
+});

+ 60 - 0
src/api/devcloud/pipeline.ts

@@ -0,0 +1,60 @@
+import axios from '@/utils/axios';
+
+import { PIPELINE_PREFIX, DEPLOYMENT_PREFIX } from './prefix';
+
+interface PipelinePayload {
+  projectId: number;
+  pipelineId: number;
+}
+
+type CreatePipelinePayload = PipelinePayload & {
+  name: string;
+  templateName: string;
+}
+
+type UpdatePipelinePayload = PipelinePayload & {
+  configJson: string;
+}
+
+// PIPELINE API
+export const deletePipeline = (pipeline: PipelinePayload) => axios.delete(`${PIPELINE_PREFIX}`, {
+  params: pipeline,
+});
+
+export const getPipeline = (pipeline: PipelinePayload) => axios.get(`${PIPELINE_PREFIX}`, {
+  params: pipeline,
+});
+
+export const createPipeline = (createPipelinePayload: CreatePipelinePayload) => axios.post(`${PIPELINE_PREFIX}`, createPipelinePayload);
+
+export const updatePipeline = (updatePipelinePayload: UpdatePipelinePayload) => axios.put(`${PIPELINE_PREFIX}/config`,
+  updatePipelinePayload);
+
+export const getPipelineTemplates = () => axios.get(`${PIPELINE_PREFIX}/templates`);
+
+export const getPipelineHistory = (pipeline: PipelinePayload) => axios.get(`${PIPELINE_PREFIX}/history/list`, {
+  params: pipeline,
+});
+
+export const getPipelinesByProjectId = (projectId: number) => axios.get(`${PIPELINE_PREFIX}/list`, {
+  params: {
+    projectId,
+  },
+});
+
+// DEPLOYMENT API
+export const deleteInstance = (deploymentId: number) => axios.delete(`${DEPLOYMENT_PREFIX}`, {
+  params: {
+    deploymentId,
+  },
+});
+
+export const triggerDeployment = (pipeline: PipelinePayload) => axios.post(`${DEPLOYMENT_PREFIX}`, {
+  params: pipeline,
+});
+
+export const getDeploymentsByProjectId = (projectId: number) => axios.get(`${DEPLOYMENT_PREFIX}/list`, {
+  params: {
+    projectId,
+  },
+});

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

@@ -0,0 +1,7 @@
+export const PIPELINE_PREFIX = '/pipelines';
+export const DEPLOYMENT_PREFIX = '/deployments';
+export const USER_PREFIX = '/users';
+export const TREE_PREFIX = '/tree';
+export const PROJECT_PREFIX = '/project';
+export const GROUP_PREFIX = '/groups';
+export const BUGLIST_PREFIX = '/bug_list';

+ 22 - 0
src/api/devcloud/project.ts

@@ -0,0 +1,22 @@
+import axios from '@/utils/axios';
+
+import { PROJECT_PREFIX } from './prefix';
+
+interface AddProjectMemberPayload {
+  invitedUserId: number;
+  projectId: number;
+}
+
+export const getProjectById = (projectId: number) => axios.get(`${PROJECT_PREFIX}/${projectId}`);
+
+export const createProject = (name: string, description: string) => axios.post(`${PROJECT_PREFIX}/create`, {
+  name,
+  description,
+});
+
+export const getProjectListByUserId = (userId: number) => axios.get(`${PROJECT_PREFIX}/listByUser/${userId}`);
+
+export const deleteProject = (projectId: string) => axios.get(`${PROJECT_PREFIX}/delete/${projectId}`);
+
+export const addProjectMember = (addProjectMemberPayload: AddProjectMemberPayload) => axios.post(`${PROJECT_PREFIX}/members`,
+  addProjectMemberPayload);

+ 15 - 0
src/api/devcloud/tree.ts

@@ -0,0 +1,15 @@
+import axios from '@/utils/axios';
+
+import { TREE_PREFIX } from './prefix';
+
+export const getAllTreeNodes = () => axios.get(`${TREE_PREFIX}/all`);
+
+export const insertTreeNode = (treenode: TreeNode) => axios.post(`${TREE_PREFIX}/node`, treenode);
+
+export const updateTreeNode = (treenode: TreeNode) => axios.put(`${TREE_PREFIX}/node`, treenode);
+
+export const deleteTreeNode = (nodeId: number) => axios.delete(`${TREE_PREFIX}/node/${nodeId}`);
+
+export const addChildNode = (parentId: number) => axios.post(`${TREE_PREFIX}/subNode/${parentId}`);
+
+export const getTreeNodesByProjectId = (projectId: number) => axios.get(`${TREE_PREFIX}/${projectId}`);

+ 15 - 0
src/api/devcloud/user.ts

@@ -0,0 +1,15 @@
+import axios from '@/utils/axios';
+
+import { USER_PREFIX } from './prefix';
+
+interface CreateGitlabAccountPayload {
+  email: string;
+  password: string;
+  username: string;
+}
+
+// USER API
+export const getUserInfo = () => axios.get(`${USER_PREFIX}/self`);
+
+export const createGitlabAccount = (createGitlabAccountPayload: CreateGitlabAccountPayload) => axios.post(`${USER_PREFIX}/gitlab`,
+  createGitlabAccountPayload);

+ 10 - 3
src/api/index.ts

@@ -1,6 +1,13 @@
-import * as UserAPI from './user';
-import * as ProjectAPI from './project';
+import * as UserAPI from './devcloud/user';
+import * as ProjectAPI from './devcloud/project';
+import * as RequirementAPI from './devcloud/tree';
+import * as PipelineAPI from './devcloud/pipeline';
+import * as PortalAPI from './portal';
 
 export {
-  UserAPI, ProjectAPI,
+  UserAPI,
+  ProjectAPI,
+  RequirementAPI,
+  PipelineAPI,
+  PortalAPI,
 };

+ 5 - 0
src/api/portal.ts

@@ -0,0 +1,5 @@
+import axios from '@/utils/axios';
+
+export const getUserIdByPhoneNumber = (phone: string) => axios.get(
+  `http://${process.env.VUE_APP_PORTAL_HOST}/api/user/findIdByPhone/${phone}`,
+);

+ 0 - 7
src/api/project.ts

@@ -1,7 +0,0 @@
-import axios from '@/utils/axios';
-
-export const getProjectList = (userId: number) => axios.get('/project/all', {
-  params: {
-    userId,
-  },
-});

+ 0 - 12
src/api/user.ts

@@ -1,12 +0,0 @@
-import axios from '@/utils/axios';
-
-export const getUserInfo = (userId: number) => axios.get(`/user/info/${userId}`);
-
-export const getUserInfo2 = (userId: number) => axios.get('/user/info', {
-  params: { userId },
-});
-
-export const post = () => axios.post('/user/post', {
-  userId: 3,
-  gender: 'male',
-});

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

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1615360924760" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1836" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M896 544H128c-17.6 0-32-14.4-32-32s14.4-32 32-32h768c17.6 0 32 14.4 32 32s-14.4 32-32 32z" p-id="1837"></path><path d="M512 928c-17.6 0-32-14.4-32-32V128c0-17.6 14.4-32 32-32s32 14.4 32 32v768c0 17.6-14.4 32-32 32z" p-id="1838"></path></svg>

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

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1615360913868" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2268" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M768 960H256c-52.8 0-96-43.2-96-96V288c0-17.6 14.4-32 32-32s32 14.4 32 32v576c0 17.6 14.4 32 32 32h512c17.6 0 32-14.4 32-32V288c0-17.6 14.4-32 32-32s32 14.4 32 32v576c0 52.8-43.2 96-96 96zM896 192H672c-8 0-16-3.2-22.4-9.6l-44.8-44.8c-6.4-6.4-14.4-9.6-24-9.6h-139.2c-8 0-16 3.2-22.4 9.6l-44.8 44.8c-6.4 6.4-14.4 9.6-22.4 9.6H128c-17.6 0-32-14.4-32-32s14.4-32 32-32h211.2l35.2-35.2C392 73.6 417.6 64 443.2 64h139.2c25.6 0 49.6 9.6 67.2 28.8L684.8 128H896c17.6 0 32 14.4 32 32s-14.4 32-32 32z" p-id="2269"></path><path d="M400 832c-17.6 0-32-14.4-32-32V288c0-17.6 14.4-32 32-32s32 14.4 32 32v512c0 17.6-14.4 32-32 32zM624 832c-17.6 0-32-14.4-32-32V288c0-17.6 14.4-32 32-32s32 14.4 32 32v512c0 17.6-14.4 32-32 32z" p-id="2270"></path></svg>

+ 189 - 117
src/components/Menu.vue

@@ -1,145 +1,217 @@
 <template>
-  <div id="menu" class="menu-container">
-    <div class="menu-logo">
+  <ElMenu class="menu" mode="horizontal" @select="handleSelect">
+    <ElMenuItem class="menu-logo" index="logo" disabled>
       LOGO
+    </ElMenuItem>
+    <ElSubmenu index="project">
+      <template #title>{{selectedProject.name}}</template>
+      <template v-if="projectList.length > 0">
+        <ElMenuItem v-for="project in projectList" :key="project.id" :index="`project-${project.id}`">{{project.name}}</ElMenuItem>
+      </template>
+      <template v-else>
+        <ElMenuItem disabled>暂无项目</ElMenuItem>
+      </template>
+    </ElSubmenu>
+      <template v-if="userLoggedIn">
+        <ElSubmenu index="user" class="user">
+          <template #title>{{userInfo.username}}</template>
+          <ElMenuItem index="user-project">创建项目</ElMenuItem>
+          <ElMenuItem index="user-invite">邀请成员</ElMenuItem>
+          <ElMenuItem index="user-logout">登出</ElMenuItem>
+        </ElSubmenu>
+      </template>
+      <template v-else>
+        <ElLink type="primary" href="/login" class="user">登录</ElLink>
+      </template>
+  </ElMenu>
+  <ElDrawer v-model="showProjectDrawer">
+    <template #title><span>创建项目</span></template>
+    <div class="drawer-content">
+      <div class="form-container">
+        <ElForm :model="createProjectForm">
+          <ElFormItem label="项目名" prop="name">
+            <ElInput v-model="createProjectForm.name" placeholder="请输入要创建的项目名"></ElInput>
+          </ElFormItem>
+          <ElFormItem label="项目描述" prop="description">
+            <ElInput
+              v-model="createProjectForm.description"
+              placeholder="请输入项目描述"
+              type="textarea"
+              :autosize="{ minRows: 3, maxRows: 3}"
+            ></ElInput>
+          </ElFormItem>
+          <ElFormItem>
+            <ElButton @click="createProject" type="primary">创建</ElButton>
+          </ElFormItem>
+        </ElForm>
+      </div>
     </div>
-    <div class="project-switcher" v-show="projectList.length>0" ref="switcher">
-      <div class="selected-project-name">{{selectedProject.name}}</div>
-      <div class="icon" @click="handleSwitcherClick"></div>
-      <transition name="expand">
-        <div class="dropdown" v-show="isDropDownExpand">
-          <div
-            v-for="item in projectList"
-            :key="item.id"
-            class="item"
-            @click="handleProjectChange(item)">{{item.name}}</div>
-        </div>
-      </transition>
+  </ElDrawer>
+  <ElDrawer v-model="showInviteDrawer">
+    <template #title><span>邀请成员</span></template>
+    <div class="drawer-content">
+      <div class="form-container">
+      <ElForm :model="inviteProjectForm">
+        <ElFormItem label="项目" prop="projectId">
+          <ElSelect v-model="inviteProjectForm.projectId" placeholder="请选择成员将要加入的项目">
+            <ElOption v-for="project in projectList" :key="project.id" :value="project.id" :label="project.name"></ElOption>
+          </ElSelect>
+        </ElFormItem>
+        <ElFormItem label="手机号" prop="phone">
+          <ElInput v-model="inviteProjectForm.phone" placeholder="请输入成员注册所用手机号"></ElInput>
+        </ElFormItem>
+        <ElFormItem>
+          <ElButton @click="inviteMember" type="primary">邀请</ElButton>
+        </ElFormItem>
+      </ElForm>
     </div>
-    <div class="notify"></div>
-    <div class="user"></div>
   </div>
+  </ElDrawer>
 </template>
 
-<script>
-export default {
+<script lang="ts">
+import { ElMessage } from 'element-plus';
+
+import { useProjectList } from '@/composable/useProject';
+import { useUser } from '@/composable/useUser';
+import { defineComponent, ref, reactive } from 'vue';
+import { PortalAPI, ProjectAPI } from '@/api';
+
+export default defineComponent({
   name: 'Menu',
-  props: {
-    projectList: Array,
-    selectedProject: Object,
-  },
-  data() {
-    return {
-      isDropDownExpand: false,
+  setup() {
+    const { projectList, selectedProject, handleProjectChange } = useProjectList();
+    const { userInfo, userLoggedIn, userLogOut } = useUser();
+
+    const showProjectDrawer = ref(false);
+    const showInviteDrawer = ref(false);
+
+    const openProjectDrawer = () => {
+      showProjectDrawer.value = true;
     };
-  },
-  mounted() {
-    document.addEventListener('click', (e) => {
-      if (!this.$refs.switcher.contains(e.target)) {
-        this.isDropDownExpand = false;
-      }
+    const openInviteDrawer = () => {
+      showInviteDrawer.value = true;
+    };
+    const createProjectForm = reactive({
+      name: '',
+      description: '',
     });
+    const createProject = () => {
+      ProjectAPI.createProject(createProjectForm.name, createProjectForm.description);
+    };
+
+    interface InviteProjectForm {
+      projectId: undefined | number;
+      phone: string;
+    }
+
+    const inviteProjectForm = reactive<InviteProjectForm>({
+      projectId: undefined,
+      phone: '',
+    });
+    const inviteMember = async () => {
+      let invitedUserId: number;
+
+      const res = await PortalAPI.getUserIdByPhoneNumber(inviteProjectForm.phone) as unknown as {id: number};
+
+      if (res) {
+        invitedUserId = res.id;
+      } else {
+        ElMessage({
+          type: 'error',
+          message: '成员不存在,请再次确认手机号',
+        });
+        return;
+      }
+
+      if (!inviteProjectForm.projectId) {
+        ElMessage({
+          type: 'error',
+          message: '请选择有效的项目',
+        });
+        return;
+      }
+
+      ProjectAPI.addProjectMember({
+        projectId: inviteProjectForm.projectId,
+        invitedUserId,
+      });
+    };
+
+    const handleSelect = (index: string) => {
+      const [type, id] = index.split('-');
+      switch (type) {
+        case 'logo': {
+          break;
+        }
+        case 'project': {
+          handleProjectChange(parseInt(id, 10));
+          break;
+        }
+        case 'user': {
+          if (id === 'project') {
+            openProjectDrawer();
+          } else if (id === 'invite') {
+            openInviteDrawer();
+          } else if (id === 'logout') {
+            userLogOut();
+          }
+          break;
+        }
+        default:
+          console.warn('unknown type of sub-menu');
+          break;
+      }
+    };
+
+    return {
+      projectList,
+      selectedProject,
+
+      handleSelect,
+
+      userInfo,
+      userLoggedIn,
+      userLogOut,
+
+      showProjectDrawer,
+      showInviteDrawer,
+
+      createProjectForm,
+      createProject,
+      inviteProjectForm,
+      inviteMember,
+    };
   },
-  methods: {
-    handleSwitcherClick() {
-      this.isDropDownExpand = !this.isDropDownExpand;
-    },
-    handleProjectChange(item) {
-      this.$emit('project-change', item.id);
-      this.isDropDownExpand = !this.isDropDownExpand;
-    },
-  },
-};
+});
 </script>
 
 <style scoped>
-.menu-container{
-  height: 58px;
+.menu {
   width: 100%;
-  position: fixed;
-  top:0px;
-  background-color: #34495E;
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: start;
 }
-.menu-logo{
-  color: #FFFFFF;
+.menu-logo {
+  color: #000000;
   font-size: 24px;
   font-weight: bolder;
   line-height: 58px;
   margin-left: 30px;
   transition: all .2s linear;
 }
-.menu-logo:hover{
-  color: #BDC3C7;
-}
-.project-switcher{
-  margin-left: 20px;
-  background-color: #2C3E50;
-  height: 30px;
-  padding: 5px;
-  width: 240px;
-  display: flex;
-  flex-direction: row;
-  justify-content: start;
-  align-items: center;
-  border-radius: 5px;
-  position: relative;
-}
-.project-switcher .icon{
-  width: 24px;
-  height: 24px;
-  cursor: pointer;
-  margin-left: auto;
-  background-position: center;
-  background-repeat: no-repeat;
-  background-size: 100% 100%;
-  transition: all .2s linear;
-  background-image: url("../assets/icon/menu.svg");
+.menu-logo.is-disabled {
+  opacity: 1;
+  cursor: default;
 }
-.project-switcher .icon:hover{
-  background-image: url("../assets/icon/menu-hover.svg");
+.user {
+  float: right;
+  height: 60px;
+  margin: 0 20px;
 }
-.project-switcher .dropdown{
-  width: 240px;
-  position: absolute;
-  background-color: #2C3E50;
-  color: #FFFFFF;
-  top: 39px;
-  left: 0px;
-  padding: 5px;
-  border-radius: 0 0 5px 5px;
-  flex-direction: column;
+.drawer-content {
+  display: flex;
   justify-content: center;
-  align-items: start;
-  transform-origin: 0px 0px;
-}
-.expand-enter-active,.expand-leave-active{
-  transition: all .3s ease-in-out;
-}
-.expand-enter-from,.expand-leave-to{
-  opacity: 0;
-  transform: scaleY(0);
-}
-.project-switcher .selected-project-name{
-  width: 220px;
-  line-height: 30px;
-  color: #FFFFFF;
-  text-align: start;
-  text-overflow: ellipsis;
-  white-space:nowrap; overflow:hidden
-}
-.project-switcher .dropdown .item{
-  line-height: 24px;
-  width: 240px;
-  text-align: start;
-  text-overflow: ellipsis;
-  white-space:nowrap; overflow:hidden;
-  cursor: pointer;
 }
-.project-switcher .dropdown .item:hover{
-  background-color: #34495E;
+.form-container {
+  width: 80%;
 }
 </style>

+ 41 - 0
src/components/MissionDetailsForm.vue

@@ -0,0 +1,41 @@
+<template>
+<right-form ref="rightForm">
+  <div class="missionDetialscontainer">
+    <div class="missionBaseInfo">任务基础信息展示</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',
+    };
+  },
+  methods: {
+    handleClick(tab, event) {
+      console.log(tab, event);
+    },
+    showForm() {
+      this.$refs.rightForm.showForm();
+    },
+  },
+};
+</script>
+
+<style scoped>
+.missionDetialscontainer{
+  .el-tabs__nav-scroll{
+    width: 200px;
+  }
+}
+</style>

+ 11 - 22
src/components/ProjectCard.vue

@@ -1,10 +1,11 @@
 <template>
-  <div class="project-card" @click="goToProject(id)">
-    <div class="project-name">{{name}}</div>
-    <div class="project-description">
-      {{description}}
-    </div>
-  </div>
+  <ElCard class="project-card" shadow="hover">
+    <template #header>
+      <span>{{name}}</span>
+    </template>
+    <div>描述:{{description}}</div>
+    <div>链接:{{gitRemoteUrl}}</div>
+  </ElCard>
 </template>
 
 <script lang="ts">
@@ -15,27 +16,15 @@ export default defineComponent({
     id: Number,
     name: String,
     description: String,
-  },
-  methods: {
-    goToProject(id: number) {
-      this.$router.push(`/project/${id}/schedule`);
-    },
+    gitRemoteUrl: String,
+    groupId: Number,
   },
 });
 </script>
 
 <style lang="scss" scoped>
 .project-card {
-  display: inline-block;
-  box-shadow: 0 0 1px #BDC3C7;
-  border-radius: 5px;
-  padding: 10px;
-  height: 150px;
-  width: 320px;
-  margin: 10px auto;
-  transition: all .3s ease;
-}
-.project-card:hover {
-  box-shadow: 0 0 5px #BDC3C7;
+  width: calc(25% - 24px);
+  margin: 12px;
 }
 </style>

+ 1 - 0
src/components/RightForm.vue

@@ -37,6 +37,7 @@ export default {
   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;

+ 68 - 99
src/components/SideBar.vue

@@ -1,135 +1,104 @@
 <template>
-  <div class="side-bar-container" @click="handleClick">
-      <div :class="{icon:true,selected:selectedIndex === 0}" target="schedule"></div>
-      <div :class="{icon:true,selected:selectedIndex === 1}" target="taskList"></div>
-      <div :class="{icon:true,selected:selectedIndex === 2}" target="bugList"></div>
-      <div :class="{icon:true,selected:selectedIndex === 3}" target="statistics"></div>
-      <div :class="{icon:true,selected:selectedIndex === 4}" target="pipeline"></div>
-      <div :class="{icon:true,selected:selectedIndex === 5}" target="apitest"></div>
-  </div>
+  <el-menu default-active="schedule" class="side-bar" :collapse="true" :router="true">
+    <el-menu-item index="schedule">
+      <i class="icon" target="schedule"></i>
+    </el-menu-item>
+    <el-menu-item index="taskList">
+      <i class="icon" target="taskList"></i>
+    </el-menu-item>
+    <el-menu-item index="bugList">
+      <i class="icon" target="bugList"></i>
+    </el-menu-item>
+    <el-menu-item index="statistics">
+      <i class="icon" target="statistics"></i>
+    </el-menu-item>
+    <el-menu-item index="pipeline">
+      <i class="icon" target="pipeline"></i>
+    </el-menu-item>
+    <el-menu-item index="apitest">
+      <i class="icon" target="apitest"></i>
+    </el-menu-item>
+  </el-menu>
 </template>
 
 <script>
 export default {
   name: 'SideBar',
-  data() {
-    return {
-      selectedIndex: 0,
-    };
-  },
-  methods: {
-    handleClick(e) {
-      const value = e.target.attributes.target.nodeValue ? e.target.attributes.target.nodeValue : '';
-      switch (value) {
-        case 'schedule':
-          this.selectedIndex = 0;
-          break;
-        case 'taskList':
-          this.selectedIndex = 1;
-          break;
-        case 'bugList':
-          this.selectedIndex = 2;
-          break;
-        case 'statistics':
-          this.selectedIndex = 3;
-          break;
-        case 'pipeline':
-          this.selectedIndex = 4;
-          break;
-        case 'apitest':
-          this.selectedIndex = 5;
-          break;
-        default:
-          throw new TypeError('Unknown Tab Value!');
-      }
-
-      this.$emit('change', value);
-    },
-  },
 };
 </script>
 
-<style scoped>
-.side-bar-container{
-  width: 42px;
+<style lang="scss" scoped>
+.side-bar {
   height: 100%;
-  position: fixed;
-  top:58px;
-  left: 0;
-  background-color: #ecf0f1;
-  display: flex;
-  flex-direction: column;
-  justify-content: start;
-  align-items: center;
-  padding-top: 20px;
 }
-.icon{
+.icon {
   width: 28px;
   height: 28px;
+  float: right;
   background-position: center;
   background-repeat: no-repeat;
   background-size: 100% 100%;
-  margin-top:16px;
+  margin-top: 14px;
   transition: all .2s linear;
-}
-.icon:hover{
-  cursor: pointer;
-  transition-duration:0.2s;
-  transform:scale(1.1);
-  transition-timing-function:ease-in-out;
+  .el-menu-item:hover & {
+    cursor: pointer;
+    transition-duration:0.2s;
+    transform:scale(1.1);
+    transition-timing-function:ease-in-out;
+  }
 }
 .icon[target=schedule]{
   background-image: url("../assets/icon/navigation.svg");
-}
-.icon[target=schedule]:hover{
-  background-image: url("../assets/icon/navigation-hover.svg");
-}
-.icon[target=schedule].selected{
-  background-image: url("../assets/icon/navigation-using.svg");
+  .el-menu-item:hover & {
+    background-image: url("../assets/icon/navigation-hover.svg");
+  }
+  .el-menu-item.is-active:hover & {
+    background-image: url("../assets/icon/navigation-using.svg");
+  }
 }
 .icon[target=taskList]{
   background-image: url("../assets/icon/task.svg");
-}
-.icon[target=taskList]:hover{
-  background-image: url("../assets/icon/task-hover.svg");
-}
-.icon[target=taskList].selected{
-  background-image: url("../assets/icon/task-using.svg");
+  .el-menu-item:hover & {
+    background-image: url("../assets/icon/task-hover.svg");
+  }
+  .el-menu-item.is-active:hover & {
+    background-image: url("../assets/icon/task-using.svg");
+  }
 }
 .icon[target=bugList]{
   background-image: url("../assets/icon/bug.svg");
-}
-.icon[target=bugList]:hover{
-  background-image: url("../assets/icon/bug-hover.svg");
-}
-.icon[target=bugList].selected{
-  background-image: url("../assets/icon/bug-using.svg");
+  .el-menu-item:hover & {
+    background-image: url("../assets/icon/bug-hover.svg");
+  }
+  .el-menu-item.is-active:hover & {
+    background-image: url("../assets/icon/bug-using.svg");
+  }
 }
 .icon[target=statistics]{
   background-image: url("../assets/icon/training.svg");
-}
-.icon[target=statistics]:hover{
-  background-image: url("../assets/icon/training-hover.svg");
-}
-.icon[target=statistics].selected{
-  background-image: url("../assets/icon/training-using.svg");
+  .el-menu-item:hover & {
+    background-image: url("../assets/icon/training-hover.svg");
+  }
+  .el-menu-item.is-active:hover & {
+    background-image: url("../assets/icon/training-using.svg");
+  }
 }
 .icon[target=pipeline]{
   background-image: url("../assets/icon/training.svg");
-}
-.icon[target=pipeline]:hover{
-  background-image: url("../assets/icon/training-hover.svg");
-}
-.icon[target=pipeline].selected{
-  background-image: url("../assets/icon/training-using.svg");
+  .el-menu-item:hover & {
+    background-image: url("../assets/icon/training-hover.svg");
+  }
+  .el-menu-item.is-active:hover & {
+    background-image: url("../assets/icon/training-using.svg");
+  }
 }
 .icon[target=apitest]{
-  background-image: url("../assets/icon/training.svg");
-}
-.icon[target=apitest]:hover{
-  background-image: url("../assets/icon/training-hover.svg");
-}
-.icon[target=apitest].selected{
-  background-image: url("../assets/icon/training-using.svg");
+   background-image: url("../assets/icon/training.svg");
+  .el-menu-item:hover & {
+    background-image: url("../assets/icon/training-hover.svg");
+  }
+  .el-menu-item.is-active:hover & {
+    background-image: url("../assets/icon/training-using.svg");
+  }
 }
 </style>

+ 265 - 0
src/components/Tree.vue

@@ -0,0 +1,265 @@
+<template>
+<div id="tree-container" :style="`height:${HEIGHT}px`">
+  <div id="node-container" :style="`height:${HEIGHT}px; width:${WIDTH}px`">
+    <TreeNode
+      v-for="requirement in data"
+      :key="requirement.id"
+      v-bind="requirement"
+      :ref="`treenode-${requirement.id}`"
+      @addChild="addChild"
+      @deleteNode="deleteNode"
+      @nodeClick="nodeClick"
+    />
+  </div>
+  <canvas id="line-drawer" :height="HEIGHT" :width="WIDTH"></canvas>
+</div>
+</template>
+
+<script>
+import TreeNode from './TreeNode.vue';
+
+// 容器宽高
+const HEIGHT = 600;
+const WIDTH = 1100;
+
+export default {
+  name: 'Tree',
+  components: { TreeNode },
+  props: {
+    data: Array,
+  },
+  data() {
+    return {
+      HEIGHT,
+      WIDTH,
+      parent2children: {},
+      depth2node: {},
+      layout: {},
+      ctx: null,
+    };
+  },
+  mounted() {
+    this.initCanvasCtx();
+    this.regenerateTree();
+  },
+  methods: {
+    initCanvasCtx() {
+      this.ctx = document.getElementById('line-drawer').getContext('2d');
+
+      this.ctx.lineWidth = 1;
+      this.ctx.strokeStyle = '#bbbbbb';
+    },
+    regenerateTree() {
+      this.initParent2Children();
+      this.initDepth2Node();
+      this.calculateLayout();
+      this.locateAndDraw();
+    },
+    initParent2Children() {
+      this.parent2children = {};
+      for (let i = 0; i < this.data.length; i += 1) {
+        const parentid = this.data[i].parent;
+        if (!Object.prototype.hasOwnProperty.call(this.parent2children, parentid)) {
+          this.parent2children[parentid] = [];
+        }
+        this.parent2children[parentid].push(this.data[i]);
+      }
+    },
+    initDepth2Node() {
+      this.depth2node = {};
+      let depth = 1;
+      let parentList = [null];
+      for (;depth < 5; depth += 1) {
+        this.depth2node[depth] = [];
+        for (let i = 0; i < this.data.length; i += 1) {
+          if (parentList.includes(this.data[i].parent)) {
+            this.depth2node[depth].push(this.data[i]);
+          }
+        }
+        parentList = this.depth2node[depth].map((node) => node.id);
+      }
+    },
+    calculateLayout() {
+      // clear the layout
+      this.layout = {};
+      const MARGIN_TOP = 40;
+      const MARGIN_LEFT = 100;
+      const COL_WIDTH = (WIDTH - MARGIN_LEFT) / 4;
+
+      for (let depth = 4; depth > 0; depth -= 1) {
+        const offsetLeft = MARGIN_LEFT + (COL_WIDTH * (depth * 2 - 1)) / 2;
+        let offsetTop = MARGIN_TOP;
+        for (let i = 0; i < this.depth2node[depth].length; i += 1) {
+          const treenode = this.$refs[`treenode-${this.depth2node[depth][i].id}`];
+          const { width, height } = treenode.getTreeNodeSize();
+          const offsetX = offsetLeft - width / 2;
+          let offsetY;
+          if (depth === 4) {
+            offsetY = offsetTop;
+            offsetTop += height + MARGIN_TOP;
+          } else if (Object.prototype.hasOwnProperty.call(this.parent2children, this.depth2node[depth][i].id)) {
+            const childrenList = this.parent2children[this.depth2node[depth][i].id].map((node) => node.id);
+            offsetY = childrenList.reduce(
+              (sum, curr) => sum + this.layout[curr].y + this.layout[curr].height / 2,
+              0,
+            ) / childrenList.length - height / 2;
+          } else {
+            offsetY = offsetTop;
+          }
+          offsetTop = offsetY + height + MARGIN_TOP;
+          this.layout[this.depth2node[depth][i].id] = {
+            x: offsetX,
+            y: offsetY,
+            width,
+            height,
+          };
+        }
+      }
+    },
+    locateAndDraw() {
+      // 定位TreeNodeContainer
+      // eslint-disable-next-line no-restricted-syntax
+      for (const id of Object.keys(this.layout)) {
+        const { x, y } = this.layout[id];
+        this.$refs[`treenode-${id}`].setTreeNodeOffset(x, y);
+      }
+      // clear the canvas
+      this.ctx.clearRect(0, 0, WIDTH, HEIGHT);
+
+      // 在canvas上绘制节点间连线
+      const kvs = Object.entries(this.parent2children);
+      // eslint-disable-next-line no-restricted-syntax
+      for (const [parent, children] of kvs) {
+        if (parent === 'null') {
+          // eslint-disable-next-line no-continue
+          continue;
+        }
+        const {
+          x: startX, y: startY, width: startW, height: startH,
+        } = this.layout[parent];
+        const start = {
+          x: startX + startW,
+          y: startY + startH / 2,
+        };
+        // eslint-disable-next-line no-restricted-syntax
+        for (const child of children) {
+          const {
+            x: endX, y: endY, height: endH,
+          } = this.layout[child.id];
+          const end = {
+            x: endX,
+            y: endY + endH / 2,
+          };
+          this.ctx.beginPath();
+          this.ctx.moveTo(start.x, start.y);
+          this.ctx.lineTo(end.x, end.y);
+          this.ctx.stroke();
+        }
+      }
+    },
+    addChild(id) {
+      this.$emit('change', {
+        type: 'addChild',
+        id,
+      });
+    },
+    deleteNode(id) {
+      this.$emit('change', {
+        type: 'deleteNode',
+        id,
+      });
+    },
+    nodeClick(id) {
+      this.$emit('node-click', id);
+    },
+  },
+};
+</script>
+
+<style lang="scss">
+#tree-container {
+  display: flex;
+  justify-content:center;
+}
+#node-container {
+  position:absolute;
+  z-index: 11;
+}
+#line-drawer {
+  position: absolute;
+}
+.jsmind-inner {
+  position:relative;
+  overflow:auto;
+  width:100%;
+  height:100%;
+  user-select:none;
+}
+
+canvas {
+  position:absolute;
+  z-index:1;
+}
+
+jmnodes {
+  position:absolute;
+  z-index:2;
+  background-color:rgba(0,0,0,0);
+}
+jmnode {
+  position:absolute;
+  cursor:default;
+  max-width:400px;
+  white-space:nowrap;
+  overflow:hidden;
+  text-overflow:ellipsis;
+
+  padding:10px;
+  background-color:#fff;
+  color:#333;
+  border-radius:1px;
+  border: 1px solid #333;
+  font:16px/1.125 Verdana,Arial,Helvetica,sans-serif;
+
+  &:hover {
+    box-shadow:2px 2px 4px #333;
+    background-color:#ebebeb;
+    color:#333;
+  }
+
+  &.selected {
+    background-color:#11f;
+    color:#fff;
+    box-shadow:2px 2px 8px #000;
+  }
+
+  &.root {
+    font-size:24px;
+  }
+}
+jmexpander {
+  position:absolute;
+  width:11px;
+  height:11px;
+  display:block;
+  overflow:hidden;
+  line-height:12px;
+  font-size:12px;
+  text-align:center;
+  border-radius:6px;
+  border-width:1px;
+  border-style:solid;
+  cursor:pointer;
+
+  border-color:gray;
+
+  &:hover {
+    border-color:#000;
+  }
+}
+
+@media screen and (max-device-width: 1024px) {
+    jmnode{padding:5px;border-radius:3px;font-size:14px;}
+    jmnode.root{font-size:21px;}
+}
+</style>

+ 101 - 0
src/components/TreeNode.vue

@@ -0,0 +1,101 @@
+<template>
+  <div class="tree-node-container" ref="node">
+    <div class="tree-node" @click="nodeClick(id)">
+      <div class="node-state">{{state}}</div>
+      <div class="node-title">{{title}}</div>
+    </div>
+    <div class="node-postfix">
+      <div class="icon" target="delete" @click="deleteNode(id)"></div>
+      <div class="icon" target="add" @click="addChild(id)"></div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'TreeNode',
+  props: {
+    id: Number,
+    state: String,
+    title: String,
+  },
+  methods: {
+    setTreeNodeOffsetX(x) {
+      this.$refs.node.style.left = `${x}px`;
+    },
+    setTreeNodeOffsetY(y) {
+      this.$refs.node.style.top = `${y}px`;
+    },
+    setTreeNodeOffset(x, y) {
+      this.$refs.node.style.left = `${x}px`;
+      this.$refs.node.style.top = `${y}px`;
+    },
+    getTreeNodeSize() {
+      return {
+        height: this.$refs.node.offsetHeight,
+        width: this.$refs.node.offsetWidth,
+      };
+    },
+    addChild(id) {
+      this.$emit('add-child', id);
+    },
+    deleteNode(id) {
+      this.$emit('delete-node', id);
+    },
+    nodeClick(id) {
+      this.$emit('node-click', id);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.tree-node-container {
+  position: absolute;
+  height: 140px;
+  width: 20%;
+
+  user-select: none;
+  & .node-postfix {
+    position: absolute;
+    top: 120px;
+    width: 100%;
+    height: 20px;
+    display: none;
+  }
+
+  &:hover .node-postfix {
+    display: block;
+  }
+}
+.tree-node {
+  box-sizing: border-box;
+  border-radius: 2px;
+  border: 1px solid #3f3f3f;
+  height: 120px;
+  width: 100%;
+}
+.icon {
+  width: 14px;
+  height: 14px;
+  float: right;
+  background-position: center;
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+  margin-top: 4px;
+  margin-right: 10px;
+  transition: all .2s linear;
+}
+.icon:hover{
+  cursor: pointer;
+  transition-duration: 0.2s;
+  transform: scale(1.1);
+  transition-timing-function:ease-in-out;
+}
+.icon[target=add]{
+  background-image: url("../assets/icon/add.svg");
+}
+.icon[target=delete]{
+  background-image: url("../assets/icon/delete.svg");
+}
+</style>

+ 9 - 12
src/composable/useProject.ts

@@ -1,21 +1,18 @@
-import { computed, reactive } from 'vue';
+import { computed } from 'vue';
+import { useStore } from '@/store';
+import { WorkspaceDispatcher } from '@/store/dispatchers';
 import { useRouter } from 'vue-router';
-import { useStore } from 'vuex';
 
-export const useProject = () => {
+export const useProjectList = () => {
   const store = useStore();
   const router = useRouter();
 
-  const projectList = computed(() => store.state.projectList);
-  let selectedProject = reactive({});
+  const projectList = computed(() => store.state.workspace.projectList);
+  const selectedProject = computed(() => store.getters.currentProjectInfo);
 
-  const handleProjectChange = (id: number) => {
-    projectList.value.forEach((item: Project) => {
-      if (item.id === id) {
-        selectedProject = item;
-        router.push(`/project/${id}/schedule`);
-      }
-    });
+  const handleProjectChange = (projectId: number) => {
+    WorkspaceDispatcher.changeProject(projectId);
+    router.push(`/project/${projectId}/schedule`);
   };
   return {
     projectList,

+ 21 - 0
src/composable/useUser.ts

@@ -0,0 +1,21 @@
+import { computed } from 'vue';
+import { useStore } from '@/store';
+import { UserDispatcher, TokenDispatcher } from '@/store/dispatchers';
+
+export const useUser = () => {
+  const store = useStore();
+
+  const userInfo = computed(() => store.state.user);
+  const userLoggedIn = computed(() => store.state.user.role !== 'GUEST');
+
+  const userLogOut = () => {
+    UserDispatcher.userLogOut();
+    TokenDispatcher.destroyToken();
+    window.location.href = '/';
+  };
+  return {
+    userInfo,
+    userLoggedIn,
+    userLogOut,
+  };
+};

+ 33 - 0
src/element-plus.ts

@@ -1,35 +1,68 @@
 import {
+  ElAside,
   ElCard,
   ElCarousel,
   ElCarouselItem,
   ElContainer,
   ElDivider,
+  ElDrawer,
   ElHeader,
+  ElLink,
   ElMain,
+  ElMenu,
+  ElDialog,
+  ElIcon,
+  ElInput,
+  ElSubmenu,
+  ElMenuItem,
+  ElForm,
+  ElFormItem,
   ElRow,
   ElTable,
   ElTableColumn,
+  ElTag,
   ElPagination,
   ElButton,
   ElSelect,
   ElOption,
+  ElTabs,
+  ElTabPane,
 } from 'element-plus';
 
+import ZH_CN from 'element-plus/lib/locale/lang/zh-cn';
+import locale from 'element-plus/lib/locale';
+
+locale.use(ZH_CN);
+
 export const Components = [
+  ElAside,
   ElRow,
   ElContainer,
   ElHeader,
   ElMain,
+  ElMenu,
+  ElDialog,
+  ElDrawer,
+  ElLink,
+  ElMenuItem,
   ElCarousel,
+  ElForm,
+  ElFormItem,
+  ElIcon,
+  ElInput,
   ElCarouselItem,
   ElDivider,
   ElCard,
   ElTable,
   ElTableColumn,
+  ElTag,
   ElPagination,
   ElButton,
   ElSelect,
   ElOption,
+  ElSubmenu,
+  ElTabs,
+  ElTabPane,
 ];
 
 export const Plugins = [];

+ 6 - 7
src/main.ts

@@ -1,14 +1,13 @@
 import { createApp } from 'vue';
-import axios from '@/utils/axios';
 import { Components, Plugins } from '@/element-plus';
-import 'element-plus/lib/theme-chalk/index.css';
-import App from './App.vue';
-import router from './router';
-import store from './store';
+import { store, key } from '@/store';
+import App from '@/App.vue';
+import router from '@/router';
 
 const app = createApp(App);
-app.use(router).use(store);
-app.config.globalProperties.axios = axios;
+
+app.use(router);
+app.use(store, key);
 
 Components.forEach((component) => {
   app.component(component.name, component);

+ 12 - 19
src/router/index.ts

@@ -1,4 +1,4 @@
-import { isUserLoggedIn } from '@/utils/userStatus';
+import { isUserLoggedIn } from '@/utils/user';
 import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
 import { HOME, LOGIN, PROJECT } from './name';
 
@@ -16,11 +16,11 @@ const routes: Array<RouteRecordRaw> = [
     path: '/',
     redirect: '/home',
   },
-  AuthWrapper({
+  {
     path: '/home',
     name: HOME,
     component: () => import('@/views/Home.vue'),
-  }),
+  },
   {
     path: '/login',
     name: LOGIN,
@@ -36,27 +36,27 @@ const routes: Array<RouteRecordRaw> = [
     children: [
       {
         path: 'schedule',
-        component: import(/* webpackChunkName: "project" */'@/views/Project/Schedule.vue'),
+        component: () => import(/* webpackChunkName: "project" */'@/views/Project/Schedule.vue'),
       },
       {
         path: 'taskList',
-        component: import(/* webpackChunkName: "project" */'@/views/Project/TaskList.vue'),
+        component: () => import(/* webpackChunkName: "project" */'@/views/Project/TaskList.vue'),
       },
       {
         path: 'bugList',
-        component: import(/* webpackChunkName: "project" */'@/views/Project/BugList.vue'),
+        component: () => import(/* webpackChunkName: "project" */'@/views/Project/BugList.vue'),
       },
       {
         path: 'statistics',
-        component: import(/* webpackChunkName: "project" */'@/views/Project/Statistics.vue'),
+        component: () => import(/* webpackChunkName: "project" */'@/views/Project/Statistics.vue'),
       },
       {
         path: 'pipeline',
-        component: import(/* webpackChunkName: "project" */'@/views/Project/Pipeline.vue'),
+        component: () => import(/* webpackChunkName: "project" */'@/views/Project/Pipeline.vue'),
       },
       {
         path: 'apitest',
-        component: import(/* webpackChunkName: "project" */'@/views/Project/APITest.vue'),
+        component: () => import(/* webpackChunkName: "project" */'@/views/Project/APITest.vue'),
       },
     ],
   }),
@@ -85,23 +85,16 @@ router.beforeEach((to, from, next) => {
 });
 
 router.beforeEach((to, from, next) => {
-  if (to.name === LOGIN) {
-    if (isUserLoggedIn()) {
-      // eslint-disable-next-line no-alert
-      alert('you\'ve logged in');
-    } else {
-      next();
-    }
-  } else {
+  if (to.name !== LOGIN || !isUserLoggedIn()) {
     next();
   }
 });
 
 router.afterEach((to, from) => {
   if (to.matched.some((record) => record.meta.hideNavigator)) {
-    document.getElementById('menu')!.style.display = 'none';
+    document.getElementsByTagName('header')[0]!.style.display = 'none';
   } else {
-    document.getElementById('menu')!.style.display = 'flex';
+    document.getElementsByTagName('header')[0]!.style.display = 'flex';
   }
 });
 

+ 9 - 0
src/store/dispatchers/index.ts

@@ -0,0 +1,9 @@
+import * as UserDispatcher from './user';
+import * as WorkspaceDispatcher from './workspace';
+import * as TokenDispatcher from './token';
+
+export {
+  UserDispatcher,
+  WorkspaceDispatcher,
+  TokenDispatcher,
+};

+ 11 - 0
src/store/dispatchers/token.ts

@@ -0,0 +1,11 @@
+import { store } from '../index';
+
+export const updateToken = (token: string) => {
+  store.dispatch('updateToken', {
+    token,
+  });
+};
+
+export const destroyToken = () => {
+  store.dispatch('destoryToken');
+};

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

@@ -0,0 +1,9 @@
+import { store } from '../index';
+
+export const updateUserInfo = () => {
+  store.dispatch('updateUserInfo');
+};
+
+export const userLogOut = () => {
+  store.dispatch('userLogOut');
+};

+ 9 - 0
src/store/dispatchers/workspace.ts

@@ -0,0 +1,9 @@
+import { store } from '../index';
+
+export const refreshProjectList = () => {
+  store.dispatch('refreshProjectList', store.state.user.id);
+};
+
+export const changeProject = (projectId: number) => {
+  store.dispatch('changeProject', { projectId });
+};

+ 14 - 19
src/store/index.ts

@@ -1,21 +1,16 @@
-import { createStore } from 'vuex';
+import { InjectionKey } from 'vue';
+import { createStore, Store, useStore as baseUseStore } from 'vuex';
 
-export default createStore({
-  state: {
-    projectList: [{ name: '项目一', description: 'asd', id: 0 },
-      { name: '项目二', description: 'asd', id: 1 },
-      { name: '项目三', description: 'asd', id: 2 },
-      { name: '长项目名12312312312123123312', description: 'asd', id: 3 },
-      { name: '项目一', description: 'asd', id: 4 },
-      { name: '项目二', description: 'asd', id: 5 },
-      { name: '项目三', description: 'asd', id: 6 },
-      { name: '长项目名12312312312123123312', description: 'asd', id: 7 }],
-  },
-  mutations: {
-    refreshProjectList(state) {
-      state.projectList = [{ name: '项目一', description: 'asd', id: 0 },
-        { name: '项目二', description: 'asd', id: 1 },
-        { name: '项目三', description: 'asd', id: 2 }];
-    },
-  },
+import { RootState } from './typings';
+
+import modules from './modules';
+
+export const key: InjectionKey<Store<RootState>> = Symbol('store');
+
+export const store = createStore<RootState>({
+  modules,
 });
+
+export function useStore() {
+  return baseUseStore(key);
+}

+ 9 - 0
src/store/modules/index.ts

@@ -0,0 +1,9 @@
+import { user } from './user';
+import { workspace } from './workspace';
+import { token } from './token';
+
+export default {
+  user,
+  workspace,
+  token,
+};

+ 32 - 0
src/store/modules/token.ts

@@ -0,0 +1,32 @@
+import { saveToken, destoryToken } from '@/utils/token';
+import { ActionTree, Module, MutationTree } from 'vuex';
+import { RootState, TokenPayload, TokenState } from '../typings';
+
+const DEFAULT: TokenState = {
+  token: localStorage.getItem('token') || '',
+};
+
+const actions: ActionTree<TokenState, RootState> = {
+  updateToken({ commit }, payload: TokenPayload) {
+    commit('updateToken', payload);
+    saveToken(payload.token);
+  },
+  destoryToken({ commit }) {
+    commit('updateToken', {
+      token: '',
+    } as TokenPayload);
+    destoryToken();
+  },
+};
+
+const mutations: MutationTree<TokenState> = {
+  updateToken(state, { token }: TokenPayload) {
+    state.token = token;
+  },
+};
+
+export const token: Module<TokenState, RootState> = {
+  state: () => DEFAULT,
+  mutations,
+  actions,
+};

+ 42 - 0
src/store/modules/user.ts

@@ -0,0 +1,42 @@
+import { UserAPI } from '@/api';
+import { ActionTree, Module, MutationTree } from 'vuex';
+import { RootState, UserState } from '../typings';
+
+const DEFAULT: UserState = {
+  username: 'GUEST',
+  email: '',
+  id: -1,
+  phone: '',
+  role: 'GUEST',
+  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);
+  },
+  userLogOut({ commit }) {
+    commit('updateUserInfo', DEFAULT);
+  },
+};
+
+const mutations: MutationTree<UserState> = {
+  updateUserInfo(state, {
+    name, email, id, phone, role,
+  }) {
+    state.username = name;
+    state.email = email;
+    state.id = id;
+    state.phone = phone;
+    state.role = role;
+  },
+};
+
+export const user: Module<UserState, RootState> = {
+  state: () => DEFAULT,
+  actions,
+  mutations,
+};

+ 51 - 0
src/store/modules/workspace.ts

@@ -0,0 +1,51 @@
+import { ProjectAPI } from '@/api';
+import {
+  ActionTree, GetterTree, Module, MutationTree,
+} from 'vuex';
+import {
+  WorkspaceState, RootState, ChangeProjectPayload,
+} from '../typings';
+
+const DEFAULT: WorkspaceState = {
+  currentProjectId: -1,
+  projectList: [],
+};
+
+const actions: ActionTree<WorkspaceState, RootState> = {
+  async refreshProjectList({ commit }, userId) {
+    const projectList = await ProjectAPI.getProjectListByUserId(userId);
+    commit('updateProjectList', projectList);
+  },
+  changeProject({ commit }, payload: ChangeProjectPayload) {
+    commit('changeProject', payload);
+  },
+  resetCurrentProject({ commit }) {
+    commit('changeProject', -1);
+  },
+};
+
+const mutations: MutationTree<WorkspaceState> = {
+  updateProjectList(state, payload: Array<Project>) {
+    state.projectList = payload;
+  },
+  changeProject(state, { projectId }: ChangeProjectPayload) {
+    state.currentProjectId = projectId;
+  },
+};
+
+const getters: GetterTree<WorkspaceState, RootState> = {
+  currentProjectInfo(state) {
+    return Array.prototype.find.call(state.projectList, (project) => project.id === state.currentProjectId)
+      || {
+        id: -1,
+        name: '未选择项目',
+      };
+  },
+};
+
+export const workspace: Module<WorkspaceState, RootState> = {
+  state: () => DEFAULT,
+  actions,
+  mutations,
+  getters,
+};

+ 33 - 0
src/store/typings.ts

@@ -0,0 +1,33 @@
+type ProjectId = number;
+
+export interface RootState {
+  token: TokenState;
+  user: UserState;
+  workspace: WorkspaceState;
+}
+
+export interface TokenState {
+  token: string;
+}
+
+export interface UserState {
+  email: string;
+  id: number;
+  phone: string;
+  role: Role;
+  username: string;
+  withGitlab: boolean;
+}
+
+export interface WorkspaceState {
+  currentProjectId: ProjectId;
+  projectList: Array<Project>;
+}
+
+export interface ChangeProjectPayload {
+  projectId: ProjectId;
+}
+
+export interface TokenPayload {
+  token: string;
+}

+ 24 - 0
src/typings/project.d.ts

@@ -2,4 +2,28 @@ declare interface Project {
   id: number;
   name: string;
   description: string;
+  gitRemoteUrl: string;
 }
+
+declare type TreeNodeType = '需求' | '任务';
+declare type TreeNodeState = '已创建' | '进行中' | '已结束';
+
+declare interface TreeNode {
+  deep: number;
+  description: string;
+  endTime: string;
+  groupId: number;
+  id: number;
+  leaf: boolean;
+  parentId: number;
+  priority: number;
+  projectId: number;
+  startTime: string;
+  state: TreeNodeState;
+  timeToTake: number;
+  title: string;
+  type: TreeNodeType;
+  userId: number;
+}
+
+declare type Role = 'TEACHER' | 'STUDENT' | 'ADMIN' | 'GUEST';

+ 0 - 0
src/shims-vue.d.ts → src/typings/shims-vue.d.ts


+ 28 - 0
src/typings/vuex.d.ts

@@ -0,0 +1,28 @@
+import { ComponentCustomProperties } from 'vue';
+import { Store } from 'vuex';
+
+declare module '@vue/runtime-core' {
+  // declare your own store states
+  interface State {
+    token: {
+      token: string;
+    };
+    user: {
+      email: string;
+      id: number;
+      phone: string;
+      role: Role;
+      username: string;
+      withGitlab: boolean;
+    };
+    workspace: {
+      currentProjectId: ProjectId;
+      projectList: Array<Project>;
+    };
+  }
+
+  // provide typings for `this.$store`
+  interface ComponentCustomProperties {
+    $store: Store<State>;
+  }
+}

+ 15 - 18
src/utils/axios.ts

@@ -1,24 +1,16 @@
 /* eslint-disable */
 import axios from 'axios';
+import { TokenDispatcher } from '@/store/dispatchers';
+
+const { destroyToken } = TokenDispatcher;
 
 // Full config:  https://github.com/axios/axios#request-config
 // axios.defaults.baseURL = process.env.baseURL || process.env.apiUrl || '';
 // axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
 // axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
 
-const getBaseUrl = () => {
-  switch (process.env.NODE_ENV) {
-    case 'development':
-      return 'http://localhost:4000/api';
-    case 'production':
-      return process.env.baseURL || process.env.apiUrl || '';
-    default:
-      return process.env.baseURL || process.env.apiUrl || '';
-  }
-};
-
 const config = {
-  baseURL: getBaseUrl(),
+  baseURL: process.env.VUE_APP_API_URL,
   // timeout: 60 * 1000, // Timeout
   // withCredentials: true, // Check cross-site Access-Control
 };
@@ -26,9 +18,10 @@ const config = {
 const _axios = axios.create(config);
 
 _axios.interceptors.request.use(
-  (config) =>
-    // Do something before request is sent
-    config,
+  (config) => {
+    config.headers.Authorization = localStorage.getItem('token');
+    return config;
+  },
   (error) =>
     // Do something with request error
     Promise.reject(error),
@@ -36,9 +29,13 @@ _axios.interceptors.request.use(
 
 // Add a response interceptor
 _axios.interceptors.response.use(
-  (response) =>
-    // Do something with response data
-    response.data.message,
+  (response) => {
+    if (response.data.code === 10110) {
+      destroyToken();
+      return;
+    }
+    return response.data.data;
+  },
   (error) =>
     // Do something with response error
     Promise.reject(error),

+ 15 - 0
src/utils/debounce.ts

@@ -0,0 +1,15 @@
+/* eslint-disable */
+const debounce = (fn: Function, delay: number) => {
+  let timer: NodeJS.Timeout;
+  return function () {
+    // @ts-ignore
+    const context = this;
+    const args = arguments;
+    if (timer) {
+      clearTimeout(timer);
+    }
+    timer = setTimeout(() => fn.apply(context, args), delay);
+  };
+};
+
+export default debounce;

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1 - 0
src/utils/jsonlint.js


+ 10 - 0
src/utils/qs.ts

@@ -0,0 +1,10 @@
+export const getVariableFromQueryString = (name: string) => {
+  const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i');
+
+  const res = window.location.search.substr(1).match(reg);
+
+  if (res != null) {
+    return unescape(res[2]);
+  }
+  return null;
+};

+ 11 - 0
src/utils/token.ts

@@ -0,0 +1,11 @@
+export const saveToken = (token: string) => {
+  localStorage.setItem('token', token);
+  localStorage.setItem('user', JSON.stringify({
+    login: true,
+  }));
+};
+
+export const destoryToken = () => {
+  localStorage.removeItem('token');
+  localStorage.removeItem('user');
+};

+ 4 - 0
src/utils/user.ts

@@ -0,0 +1,4 @@
+export const isUserLoggedIn = () => {
+  const user = JSON.parse(localStorage.getItem('user') || '{}');
+  return user.login || false;
+};

+ 0 - 4
src/utils/userStatus.ts

@@ -1,4 +0,0 @@
-export const isUserLoggedIn = () => {
-  const userStatus = JSON.parse(localStorage.getItem('user') || '{}');
-  return userStatus.login;
-};

+ 11 - 8
src/views/Home.vue

@@ -2,39 +2,42 @@
   <div class="home">
     <div class="poster">
     </div>
-    <div style="font-weight: bolder;font-size: 32px;padding: 30px">Hello,username</div>
+    <div style="font-weight: bolder;font-size: 32px;padding: 30px">Hello,{{username}}</div>
     <div class="project-list">
       <ProjectCard
         v-for="project in projectList"
         :key="project.id"
-        :id="project.id"
-        :name="project.name"
-        :description="project.description"
+        v-bind="project"
+        @click="handleProjectChange(project.id)"
       ></ProjectCard>
     </div>
   </div>
 </template>
 
 <script lang="ts">
-import { computed, defineComponent } from 'vue';
+import { defineComponent, computed } from 'vue';
 import ProjectCard from '@/components/ProjectCard.vue';
-import { useStore } from 'vuex';
+import { useProjectList } from '@/composable/useProject';
+import { useStore } from '@/store';
 
 export default defineComponent({
   components: {
     ProjectCard,
   },
   setup() {
+    const { projectList, handleProjectChange } = useProjectList();
     const store = useStore();
     return {
-      projectList: computed(() => store.state.projectList),
+      projectList,
+      username: computed(() => store.state.user.username),
+      handleProjectChange,
     };
   },
 });
 </script>
 <style scoped>
 .home{
-  margin-top: 58px ;
+  width: 100%;
 }
 .project-list {
   display:flex;

+ 3 - 3
src/views/Login.vue

@@ -11,9 +11,9 @@ import { defineComponent } from 'vue';
 export default defineComponent({
   methods: {
     async login() {
-      window.location = `http://p.seecoder.cn/api/interaction?scope=openid&client_id=seec-devcloud&redirect_uri=${encodeURI(
-        `http://${window.location.host}/api/auth/receive`,
-      )}` as unknown as Location;
+      window.location = `http://${process.env.VUE_APP_PORTAL_HOST}/api/interaction?scope=openid&client_id=seec-seec&redirect_uri=${
+        encodeURI(`http://${window.location.host}/`)
+      }` as unknown as Location;
     },
   },
 });

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

@@ -6,7 +6,8 @@
       style="margin-top: 10px"
       :listData="bugData"
       :total="300"
-      :is-show-checkbox="isShowCheckbox"  @current-change="handleCurrentChange"></List>
+      :is-show-checkbox="isShowCheckbox"  @current-change="handleCurrentChange">
+    </List>
     <RightForm ref="rightForm">
     </RightForm>
   </div>

+ 390 - 4
src/views/Project/Pipeline.vue

@@ -1,13 +1,399 @@
 <template>
-  project-pipeline
+  <ElTabs class="tabs" v-model="activeTab">
+    <ElTabPane label="流水线" name="pipeline">
+      <ElTable :data="pipelines" width="100%">
+        <ElTableColumn
+          prop="id"
+          label="ID"
+          align="center"
+          width="60"
+        ></ElTableColumn>
+        <ElTableColumn
+          prop="name"
+          label="名称"
+          align="center"
+          width="180"
+        ></ElTableColumn>
+        <ElTableColumn
+          prop="username"
+          label="最近执行人"
+          align="center"
+          width="180"
+        ></ElTableColumn>
+        <ElTableColumn
+          label="最近执行时间"
+          align="center"
+          :formatter="(row) => timeFormatter(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>
+              <ElTag type="warning">未知状态</ElTag>
+            </template>
+          </template>
+        </ElTableColumn>
+        <ElTableColumn
+          label="详情"
+          align="center"
+        >
+          <template #default="scope">
+            <ElButton
+              @click="handleClick('detail', scope.row.id)"
+              type="text"
+              size="small"
+              :disabled="scope.row.result==='执行中'"
+            >最近执行详情</ElButton>
+            <ElButton @click="handleClick('history', scope.row.id)" type="text" size="small">历史</ElButton>
+          </template>
+        </ElTableColumn>
+        <ElTableColumn
+          align="center"
+        >
+          <template #header>
+            <span>操作</span>
+            <ElButton @click="handleClick('create')" type="text" class="add-button">新建</ElButton>
+          </template>
+          <template #default="scope">
+            <ElButton @click="handleClick('trigger', scope.row.id)" type="text" size="small">触发</ElButton>
+            <ElButton @click="handleClick('edit', scope.row.id)" type="text" size="small">编辑</ElButton>
+            <ElButton @click="handleClick('delete', scope.row.id)" type="text" size="small">删除</ElButton>
+          </template>
+        </ElTableColumn>
+      </ElTable>
+      <ElDialog v-model="showPipelineDetailModal" destroy-on-close>
+        <template #title>最近一次流水线构建的执行结果详情</template>
+        <pre class="pipeline-detail">{{detail}}</pre>
+      </ElDialog>
+      <ElDialog v-model="showPipelineHistoryModal" destroy-on-close>
+        <template #title>流水线构建历史</template>
+        <ElTable
+          :data="pipelineHistory"
+          :default-sort="{
+            props: 'id',
+          }"
+          height="400"
+        >
+          <ElTableColumn type="expand">
+            <template #default="scope">
+              <pre class="pipeline-detail">{{scope.row.details}}</pre>
+            </template>
+          </ElTableColumn>
+          <ElTableColumn
+            prop="id"
+            label="构建ID"
+            align="center"
+            width="100"
+          ></ElTableColumn>
+          <ElTableColumn
+            prop="pipelineId"
+            label="流水线ID"
+            align="center"
+            width="100"
+          ></ElTableColumn>
+          <ElTableColumn
+            prop="username"
+            label="触发人"
+            align="center"
+            width="180"
+          ></ElTableColumn>
+          <ElTableColumn
+            label="执行时间"
+            align="center"
+            :formatter="(row) => timeFormatter(row, 'startTime')"
+          ></ElTableColumn>
+        </ElTable>
+      </ElDialog>
+      <ElDialog
+        v-model="showPipelineEditModal"
+        :before-close="(done) => {
+          jsonValid=true;
+          done();
+        }"
+        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>
+        <template #footer>
+          <span class="dialog-footer">
+            <el-button type="primary" @click="handlePipelineUpdateConfirm" :disabled="!jsonValid">确 定</el-button>
+          </span>
+        </template>
+      </ElDialog>
+      <ElDialog
+        v-model="showPipelineCreateModal"
+        :before-close="(done) => {
+          $refs['createPipelineForm'].resetFields();
+          done();
+        }"
+        destroy-on-close
+      >
+        <template #title>
+          <span>创建流水线</span>
+          <span class="button-group">
+            <ElButton type="text">模板详解</ElButton>
+          </span>
+        </template>
+        <ElForm
+          :model="createPipelineForm"
+          ref="createPipelineForm"
+          label-position="top"
+          label-suffix=":"
+          class="create-pipeline-form"
+        >
+          <ElFormItem label="名称" prop="name" required>
+            <ElInput v-model="createPipelineForm.name" placeholder="请输入流水线名称"></ElInput>
+          </ElFormItem>
+          <ElFormItem label="模板" prop="template" required>
+            <ElSelect v-model="createPipelineForm.template" placeholder="请选择模板">
+              <ElOption
+                v-for="(template, index) in templates"
+                :key="index" :label="template"
+                :value="template"
+              ></ElOption>
+            </ElSelect>
+          </ElFormItem>
+        </ElForm>
+        <template #footer>
+          <span class="dialog-footer">
+            <el-button type="primary" @click="handlePipelineCreateConfirm">确 定</el-button>
+          </span>
+        </template>
+      </ElDialog>
+    </ElTabPane>
+    <ElTabPane label="部署产物" name="production">
+      <ElTable :data="productions" width="100%">
+        <ElTableColumn
+          prop="id"
+          label="ID"
+          align="center"
+          width="60"
+        ></ElTableColumn>
+        <ElTableColumn
+          prop="name"
+          label="名称"
+          align="center"
+        ></ElTableColumn>
+        <ElTableColumn
+          prop="pipelineId"
+          label="流水线ID"
+          align="center"
+          width="100"
+        ></ElTableColumn>
+        <ElTableColumn
+          label="部署时间"
+          align="center"
+          :formatter="(row) => timeFormatter(row, 'deployedTime')"
+        ></ElTableColumn>
+        <ElTableColumn
+          label="URL"
+          align="center"
+        >
+          <template #default="scope">
+              <ElButton type="text" @click="open(scope.row.accessUrl, '_blank')">
+                {{scope.row.accessUrl}}
+              </ElButton>
+          </template>
+        </ElTableColumn>
+        <ElTableColumn
+          label="运行状态"
+          align="center"
+        >
+          <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>
+          </template>
+        </ElTableColumn>
+        <ElTableColumn>
+          <template #default="scope">
+            <ElButton type="text" @click="deleteProduction(scope.row.id)">
+              <i class="el-icon-delete"></i>
+            </ElButton>
+          </template>
+        </ElTableColumn>
+      </ElTable>
+    </ElTabPane>
+  </ElTabs>
 </template>
 
 <script>
-export default {
+import jsonlint from '@/utils/jsonlint';
+
+import { PipelineAPI } from '@/api';
+import debounce from '@/utils/debounce';
 
+export default {
+  data() {
+    return {
+      activeTab: 'pipeline',
+      pipelines: [],
+      productions: [],
+      showPipelineCreateModal: false,
+      showPipelineEditModal: false,
+      showPipelineHistoryModal: false,
+      showPipelineDetailModal: false,
+      detail: '',
+      pipelineHistory: [],
+      pipelineConfig: '',
+      jsonValid: true,
+      editingPipelineId: -1,
+      templates: [],
+      createPipelineForm: {
+        name: '',
+        template: '',
+      },
+    };
+  },
+  async mounted() {
+    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();
+  },
+  methods: {
+    timeFormatter: (row, property) => `${row[property].year}/${row[property].month}/${row[property].day} `
+            + `${row[property].hours}:${row[property].minutes}:${row[property].seconds}`,
+    async handleClick(action, id) {
+      switch (action) {
+        case 'create': {
+          this.showPipelineCreateModal = true;
+          break;
+        }
+        case 'trigger': {
+          await PipelineAPI.triggerDeployment({
+            projectId: this.$store.state.workspace.currentProjectId,
+            pipelineId: id,
+          });
+          this.productions = await PipelineAPI.getDeploymentsByProjectId(this.$store.state.workspace.currentProjectId);
+          break;
+        }
+        case 'detail': {
+          this.showPipelineDetailModal = true;
+          this.detail = Array.prototype.find.call(this.pipelines, (pipeline) => pipeline.id === id).details;
+          break;
+        }
+        case 'history': {
+          this.showPipelineHistoryModal = true;
+          this.pipelineHistory = await PipelineAPI.getPipelineHistory({
+            projectId: this.$store.state.workspace.currentProjectId,
+            pipelineId: id,
+          });
+          break;
+        }
+        case 'edit': {
+          this.showPipelineEditModal = true;
+          this.editingPipelineId = id;
+          this.pipelineConfig = await PipelineAPI.getPipeline({
+            projectId: this.$store.state.workspace.currentProjectId,
+            pipelineId: id,
+          }).configJson || '{}';
+          break;
+        }
+        case 'delete': {
+          PipelineAPI.deletePipeline({
+            projectId: this.$store.state.workspace.currentProjectId,
+            pipelineId: id,
+          });
+          break;
+        }
+        default: {
+          console.warn('Unknown type of action.');
+        }
+      }
+    },
+    // 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() {
+      await PipelineAPI.updatePipeline({
+        projectId: this.$store.state.workspace.currentProjectId,
+        pipelineId: this.editingPipelineId,
+        configJson: JSON.stringify(JSON.parse(this.pipelineConfig)),
+      });
+      this.showPipelineEditModal = false;
+    },
+    async handlePipelineCreateConfirm() {
+      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;
+        }
+      });
+    },
+    open(url, target) {
+      window.open(url, target);
+    },
+    async deleteProduction(id) {
+      await PipelineAPI.deleteInstance(id);
+    },
+  },
 };
 </script>
 
-<style>
-
+<style scoped>
+.tabs {
+  width: 80%;
+  margin: auto auto;
+}
+.add-button {
+  margin-left: 14px;
+}
+.pipeline-detail {
+  width: 100%;
+  word-break: break-word;
+  white-space: pre-wrap;
+}
+.create-pipeline-form {
+  width: 60%;
+  margin: 0 auto;
+}
+.button-group {
+  margin-left: 40px;
+}
 </style>

+ 63 - 2
src/views/Project/Schedule.vue

@@ -1,10 +1,71 @@
 <template>
-  project-schedule
+  <Tree :data="requirements" @change="handleTreeChange" @nodeClick="handleNodeClick" ref="tree"></Tree>
+  <RightForm ref="rightForm"/>
 </template>
 
 <script>
-export default {
+import Tree from '@/components/Tree.vue';
+import RightForm from '@/components/RightForm.vue';
+
+import { RequirementAPI } from '@/api';
 
+export default {
+  name: 'Schedule',
+  components: { Tree, RightForm },
+  data() {
+    return {
+      requirements: [
+        {
+          id: 1,
+          title: 't1',
+          parent: null,
+          state: '未开始',
+        },
+        {
+          id: 2,
+          title: 't2',
+          parent: 1,
+          state: '未开始',
+        },
+        {
+          id: 3,
+          title: 't3',
+          parent: 1,
+          state: '未开始',
+        },
+        {
+          id: 4,
+          title: 't4',
+          parent: 2,
+          state: '未开始',
+        },
+        {
+          id: 5,
+          title: 't5',
+          parent: 2,
+          state: '进行中',
+        },
+        {
+          id: 6,
+          title: 't6',
+          parent: 4,
+          state: '已完成',
+        },
+      ],
+    };
+  },
+  methods: {
+    async handleTreeChange(msg) {
+      // TODO: 补充API定义
+      const { type, id } = msg;
+      await RequirementAPI[type](id);
+      // this.requirements = await RequirementAPI.getTreeNode(projectId);
+      this.$refs.tree.regenerateTree();
+    },
+    handleNodeClick(id) {
+      this.$refs.rightForm.showForm();
+    },
+  },
 };
 </script>
 

+ 83 - 4
src/views/Project/TaskList.vue

@@ -1,6 +1,6 @@
 <template>
-  project-task-list
   <div class="taskListContainer">
+    <button @click="showMissionDetailsForm">from test</button>
     <div class="taskFunctionBar">
 <!--      后期可以考虑吧 控制台也才抽出来作为组件 -->
       <div class="masterFunctionBar">
@@ -37,7 +37,12 @@
     </div>
 
     <div class="taskTableContainer">
-
+      <List
+        :listData="bugData"
+        :total="300"
+        :is-show-checkbox="isShowCheckbox"  @current-change="handleCurrentChange">
+      </List>
+      <MissionDetailsForm ref="missionDetailsForm"> </MissionDetailsForm>
 <!--     组件,然后 当展示的时候,将筛选的选项传入-->
 <!--      buglist和任务list复用-->
 <!--      点击具体项的时候,任务的抽屉也是一个组件-->
@@ -47,7 +52,11 @@
 </template>
 
 <script>
+import MissionDetailsForm from '../../components/MissionDetailsForm.vue';
+import List from '../../components/List.vue';
+
 export default {
+  components: { MissionDetailsForm, List },
   data() {
     return {
       taskScope: [{
@@ -80,15 +89,80 @@ export default {
         label: '优先级',
       }],
       selectSortOptions: 'allStatus',
-
+      bugData: [
+        {
+          projectCode: 'EXAMPLE-123',
+          title: 'bug1',
+          assignment: '经办人1',
+          status: '进行中',
+        },
+        {
+          projectCode: 'EXAMPLE-124',
+          title: 'bug2',
+          assignment: '经办人2',
+          status: '进行中',
+        },
+        {
+          projectCode: 'EXAMPLE-123',
+          title: 'bug1',
+          assignment: '经办人1',
+          status: '进行中',
+        },
+        {
+          projectCode: 'EXAMPLE-124',
+          title: 'bug2',
+          assignment: '经办人2',
+          status: '进行中',
+        },
+        {
+          projectCode: 'EXAMPLE-123',
+          title: 'bug1',
+          assignment: '经办人1',
+          status: '进行中',
+        },
+        {
+          projectCode: 'EXAMPLE-124',
+          title: 'bug2',
+          assignment: '经办人2',
+          status: '进行中',
+        },
+        {
+          projectCode: 'EXAMPLE-123',
+          title: 'bug1',
+          assignment: '经办人1',
+          status: '进行中',
+        },
+        {
+          projectCode: 'EXAMPLE-124',
+          title: 'bug2',
+          assignment: '经办人2',
+          status: '进行中',
+        }],
+      isShowCheckbox: true,
     };
   },
+  methods: {
+    /**
+     * @param filterObject 用于数据获取请求的参数对象
+     * @param currentPage  当前页数
+     */
+    getBugData(filterObject, currentPage) {
+      console.log('getBugData');
+    },
+    showMissionDetailsForm() {
+      console.log(this.$refs.missionDetailsForm);
+      this.$refs.missionDetailsForm.showForm();
+    },
+    handleCurrentChange(currentPage) {
+      this.getBugData({}, currentPage);
+    },
+  },
 };
 </script>
 
 <style scoped>
 .taskListContainer{
-  margin: 30px 30px 0 30px;
+  margin: 70px 30px 0 30px;
 }
 .taskFunctionBar .masterFunctionBar{
   display: flex;
@@ -112,4 +186,9 @@ export default {
 .sortOptionsSelect{
   margin-left: auto;
 }
+
+.taskTableContainer{
+  margin-top: 20px;
+}
+
 </style>

+ 11 - 13
src/views/ProjectContainer.vue

@@ -1,22 +1,22 @@
 <template>
-  <div>
-    <SideBar @change="handleTabChange"></SideBar>
-    <div class="container">
-      <router-view></router-view><!--此处路由路由4个功能页-->
-    </div>
-  </div>
+  <el-aside width="65">
+    <SideBar></SideBar>
+  </el-aside>
+  <el-main class="container">
+    <router-view></router-view>
+  </el-main>
 </template>
 
 <script>
 import SideBar from '@/components/SideBar.vue';
+import { WorkspaceDispatcher } from '@/store/dispatchers';
 
 export default {
   name: 'ProjectContainer',
   components: { SideBar },
-  methods: {
-    handleTabChange(value) {
-      this.$router.push(value);
-    },
+  mounted() {
+    const { projectId } = this.$route.params;
+    WorkspaceDispatcher.changeProject(parseInt(projectId, 10));
   },
 };
 </script>
@@ -25,8 +25,6 @@ export default {
 .container{
   background-color: #FFFFFF;
   width: calc(100% - 42px);
-  margin-top: 58px;
-  margin-left: 42px;
   min-height: 60vh;
 }
 /* 用于功能页组件最外层 */
@@ -36,7 +34,7 @@ export default {
   box-sizing: border-box;
   display: flex;
   flex-direction: column;
-  justify-content: start;
+  justify-content: flex-start;
   align-items: center;
 }
 </style>

+ 8 - 0
vue.config.js

@@ -0,0 +1,8 @@
+module.exports = {
+  chainWebpack: (config) => {
+    config.plugin('html').tap((args) => {
+      args[0].title = `${process.env.VUE_APP_ENV_NAME}Devcloud | SEEC`;
+      return args;
+    });
+  },
+};

+ 80 - 0
yarn.lock

@@ -2365,6 +2365,11 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4
     escalade "^3.1.1"
     node-releases "^1.1.70"
 
+buffer-equal-constant-time@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npm.taobao.org/buffer-equal-constant-time/download/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
+  integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
+
 buffer-from@^1.0.0:
   version "1.1.1"
   resolved "https://registry.npm.taobao.org/buffer-from/download/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@@ -3634,6 +3639,13 @@ ecc-jsbn@~0.1.1:
     jsbn "~0.1.0"
     safer-buffer "^2.1.0"
 
+ecdsa-sig-formatter@1.0.11:
+  version "1.0.11"
+  resolved "https://registry.npm.taobao.org/ecdsa-sig-formatter/download/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
+  integrity sha1-rg8PothQRe8UqBfao86azQSJ5b8=
+  dependencies:
+    safe-buffer "^5.0.1"
+
 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"
@@ -5719,6 +5731,22 @@ jsonfile@^6.0.1:
   optionalDependencies:
     graceful-fs "^4.1.6"
 
+jsonwebtoken@^8.5.1:
+  version "8.5.1"
+  resolved "https://registry.npm.taobao.org/jsonwebtoken/download/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
+  integrity sha1-AOceC431TCEhofJhN98igGc7zA0=
+  dependencies:
+    jws "^3.2.2"
+    lodash.includes "^4.3.0"
+    lodash.isboolean "^3.0.3"
+    lodash.isinteger "^4.0.4"
+    lodash.isnumber "^3.0.3"
+    lodash.isplainobject "^4.0.6"
+    lodash.isstring "^4.0.1"
+    lodash.once "^4.0.0"
+    ms "^2.1.1"
+    semver "^5.6.0"
+
 jsprim@^1.2.2:
   version "1.4.1"
   resolved "https://registry.npm.taobao.org/jsprim/download/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -5729,6 +5757,23 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     verror "1.10.0"
 
+jwa@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.npm.taobao.org/jwa/download/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
+  integrity sha1-dDwymFy56YZVUw1TZBtmyGRbA5o=
+  dependencies:
+    buffer-equal-constant-time "1.0.1"
+    ecdsa-sig-formatter "1.0.11"
+    safe-buffer "^5.0.1"
+
+jws@^3.2.2:
+  version "3.2.2"
+  resolved "https://registry.npm.taobao.org/jws/download/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
+  integrity sha1-ABCZ82OUaMlBQADpmZX6UvtHgwQ=
+  dependencies:
+    jwa "^1.4.1"
+    safe-buffer "^5.0.1"
+
 killable@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npm.taobao.org/killable/download/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
@@ -5934,6 +5979,36 @@ lodash.defaultsdeep@^4.6.1:
   resolved "https://registry.npm.taobao.org/lodash.defaultsdeep/download/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6"
   integrity sha1-US6b1yHSctlOPTpjZT+hdRZ0HKY=
 
+lodash.includes@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.npm.taobao.org/lodash.includes/download/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
+  integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
+
+lodash.isboolean@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.npm.taobao.org/lodash.isboolean/download/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
+  integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
+
+lodash.isinteger@^4.0.4:
+  version "4.0.4"
+  resolved "https://registry.npm.taobao.org/lodash.isinteger/download/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
+  integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=
+
+lodash.isnumber@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.npm.taobao.org/lodash.isnumber/download/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
+  integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
+
+lodash.isplainobject@^4.0.6:
+  version "4.0.6"
+  resolved "https://registry.npm.taobao.org/lodash.isplainobject/download/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+  integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
+
+lodash.isstring@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.npm.taobao.org/lodash.isstring/download/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
+  integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
+
 lodash.kebabcase@^4.1.1:
   version "4.1.1"
   resolved "https://registry.npm.taobao.org/lodash.kebabcase/download/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
@@ -5949,6 +6024,11 @@ lodash.memoize@^4.1.2:
   resolved "https://registry.npm.taobao.org/lodash.memoize/download/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
   integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
 
+lodash.once@^4.0.0:
+  version "4.1.1"
+  resolved "https://registry.npm.taobao.org/lodash.once/download/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
+  integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
+
 lodash.transform@^4.6.0:
   version "4.6.0"
   resolved "https://registry.npm.taobao.org/lodash.transform/download/lodash.transform-4.6.0.tgz#12306422f63324aed8483d3f38332b5f670547a0"

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio