Browse Source

Merge branch 'dev_schedule_tree' into dev_taskList

Jinyao 5 years ago
parent
commit
718487e99d

+ 1 - 1
.env

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

+ 2 - 0
mock/index.js

@@ -26,6 +26,8 @@ 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}/tree`, require('./routers/tree'));
+app.use(`${prefix}/bug_list`, require('./routers/buglist'));
 
 app.listen(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;

+ 8 - 16
mock/routers/project.js

@@ -46,25 +46,17 @@ router.get('/listByUser/:userId',
     ));
   });
 
-router.get('/listByGroup/:groupId',
-  (req, res) => {
-    const { groupId } = req.params;
-    res.json(wrapper(
-      Mock.mock(Mock.mock({
-        'data|3-5': [{
-          'id|+1': 100,
-          description: Random.csentence(5, 40),
-          gitRemoteUrl: Random.url('http', 'gitlab.seecoder.cn'),
-          groupId,
-          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;

+ 167 - 0
mock/routers/tree.js

@@ -0,0 +1,167 @@
+/* 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('/node', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.put('/node', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.delete('/node/:nodeId', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.post('/subNode/:parentId', (req, res) => {
+  res.json(wrapper('success'));
+});
+
+router.get('/:projectId', (req, res) => {
+  res.json(wrapper(
+    Mock.mock([{
+      deep: 1,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 1,
+      leaf: false,
+      parent: null,
+      priority: 0,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 2,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 2,
+      leaf: false,
+      parent: 1,
+      priority: 0,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 2,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 3,
+      leaf: false,
+      parent: 1,
+      priority: 0,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 3,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 4,
+      leaf: false,
+      parent: 3,
+      priority: 1,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 3,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 5,
+      leaf: false,
+      parent: 3,
+      priority: 2,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 4,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 6,
+      leaf: false,
+      parent: 5,
+      priority: 0,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 4,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 7,
+      leaf: false,
+      parent: 5,
+      priority: 0,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    {
+      deep: 4,
+      description: Random.csentence(10, 40),
+      endTime: Random.datetime(),
+      groupId: 101,
+      id: 8,
+      leaf: false,
+      parent: 5,
+      priority: 0,
+      projectId: 101,
+      startTime: Random.datetime(),
+      'state|1': ['已创建', '进行中', '已完成'],
+      timeToTake: 10,
+      title: Random.cname(),
+      'type|1': ['需求', '任务'],
+      userId: 101,
+    },
+    ]),
+  ));
+});
+
+module.exports = router;

+ 5 - 0
mock/routers/user.js

@@ -12,3 +12,8 @@ router.get('/self',
     res.json(wrapper(userinfo));
   });
 module.exports = router;
+
+router.post('/gitlab',
+  (req, res) => {
+    res.json(wrapper('success'));
+  });

+ 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,
+  },
+});

+ 2 - 0
src/api/pipeline.ts → src/api/devcloud/pipeline.ts

@@ -16,6 +16,7 @@ type UpdatePipelinePayload = PipelinePayload & {
   configJson: string;
 }
 
+// PIPELINE API
 export const deletePipeline = (pipeline: PipelinePayload) => axios.delete(`${PIPELINE_PREFIX}`, {
   params: pipeline,
 });
@@ -41,6 +42,7 @@ export const getPipelinesByProjectId = (projectId: number) => axios.get(`${PIPEL
   },
 });
 
+// DEPLOYMENT API
 export const deleteInstance = (deploymentId: number) => axios.delete(`${DEPLOYMENT_PREFIX}`, {
   params: {
     deploymentId,

+ 2 - 0
src/api/prefix.ts → src/api/devcloud/prefix.ts

@@ -3,3 +3,5 @@ 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';

+ 12 - 3
src/api/project.ts → src/api/devcloud/project.ts

@@ -2,12 +2,21 @@ 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 = (project: Project) => axios.post(`${PROJECT_PREFIX}/create`, project);
+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 getProjectListByGroupId = (groupId: number) => axios.get(`${PROJECT_PREFIX}/listByGroup/${groupId}`);
-
 export const deleteProject = (projectId: string) => axios.get(`${PROJECT_PREFIX}/delete/${projectId}`);
+
+export const addProjectMember = (addProjectMemberPayload: AddProjectMemberPayload) => axios.post(`${PROJECT_PREFIX}/members`,
+  addProjectMemberPayload);

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


+ 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);

+ 6 - 4
src/api/index.ts

@@ -1,11 +1,13 @@
-import * as UserAPI from './user';
-import * as ProjectAPI from './project';
-import * as RequirementAPI from './tree';
-import * as PipelineAPI from './pipeline';
+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,
   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 - 5
src/api/user.ts

@@ -1,5 +0,0 @@
-import axios from '@/utils/axios';
-
-import { USER_PREFIX } from './prefix';
-
-export const getUserInfo = () => axios.get(`${USER_PREFIX}/self`);

+ 156 - 0
src/ccccyt/Tree.vue

@@ -0,0 +1,156 @@
+<template>
+  <div class="tree-container">
+    <el-row :gutter="20" >
+        <TreeNode v-for="item in colList[0]" :title="item.title" :position-y="item.positionY" position-x="0" :key="item.id"></TreeNode>
+        <TreeNode v-for="item in colList[1]" :title="item.title" :position-y="item.positionY" :position-x="1" :key="item.id"></TreeNode>
+        <TreeNode v-for="item in colList[2]" :title="item.title" :position-y="item.positionY" :position-x="2" :key="item.id"></TreeNode>
+        <TreeNode v-for="item in colList[3]" :title="item.title" :position-y="item.positionY" :position-x="3" :key="item.id"></TreeNode>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import TreeNode from "@/components/TreeNode";
+export default {
+name: "Tree",
+  components: {TreeNode},
+  data() {
+  return {
+    treeHeight: 4,
+    requirements: [{
+      deep: 1,
+      id: 1,
+      parent: null,
+      title: '这是标题',
+    },
+      {
+        deep: 2,
+        id: 2,
+        parent: 1,
+        title: '这是标题2',
+      },
+      {
+        deep: 2,
+        id: 3,
+        parent: 1,
+        priority: 0,
+        title: '这是标题3',
+      },
+      {
+        deep: 3,
+        id: 4,
+        parent: 3,
+        title: '这是标题4',
+      },
+      {
+        deep:4,
+        id:9,
+        parent: 4,
+        title: '这是标题9'
+      },
+      {
+        deep: 3,
+        id: 5,
+        parent: 3,
+        title: '这是标题5',
+      },
+      {
+        deep: 4,
+        id: 6,
+        parent: 5,
+        title: '这是标题6',
+      },
+      {
+        deep: 4,
+        id: 7,
+        parent: 5,
+        title: '这是标题7',
+      },
+      {
+        deep: 4,
+        id: 8,
+        parent: 5,
+        title: '这是标题8',
+      },
+    ],
+    tree: {},
+    nodeMap: {},
+    colList: [[],[],[],[]]
+  }
+  },
+  mounted() {
+    this.array2tree()
+    this.getList()
+    this.tree.positionY = 0
+    this.calculateContentHeight(this.tree)
+    this.calculateNodePositionY()
+    this.offsetPositionY()
+    console.warn(this.tree)
+  },
+  methods: {
+  array2tree(){
+    let treeMap = new Map()
+    this.requirements.forEach((item)=>{
+      treeMap.set(item.id,item)
+    })
+    this.nodeMap = treeMap
+    this.requirements.forEach((item)=>{
+      if(item.parent){
+        let tempParent = treeMap.get(item.parent)
+        if(!tempParent.childs){
+          tempParent.childs = []
+        }
+        tempParent.childs.push(item)
+      }else{
+        this.tree = item
+      }
+    })
+  },
+  getList(){
+    this.requirements.forEach((item)=>{
+      this.colList[item.deep-1].push(item)
+    })
+  },
+  calculateContentHeight(node){
+    if(!node.childs){
+      node.contentHeight = 1
+      return 1
+    }
+    node.contentHeight = node.childs.reduce((total,current)=>{
+      return total + this.calculateContentHeight(current)
+    },0)
+    return node.contentHeight
+  },
+  calculateNodePositionY() {
+    for(let i=1;i<4;i++){
+      let sum = 0
+      this.colList[i].forEach((item)=>{
+        item.positionY = this.nodeMap.get(item.parent).positionY + sum
+        sum = sum + item.contentHeight
+      })
+    }
+  },
+  offsetPositionY(){
+    for(let i=2;i>=0;i--){
+      this.colList[i].forEach((item)=>{
+        if(item.childs){
+          let length = item.childs.length
+          item.positionY = item.childs.reduce((total,current)=>{
+            return total+current.positionY/length
+          },0)
+        }
+      })
+
+    }
+  }
+  }
+}
+</script>
+
+<style scoped>
+.tree-container{
+  min-height: 300px;
+  width: 100%;
+  background-color: #ffffff;
+}
+</style>

+ 44 - 0
src/ccccyt/TreeNode.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="node-container" :style="nodeStyle">
+  <el-card class="node-card">
+    {{title}}
+  </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+name: "TreeNode",
+props:{
+  title: String,
+  positionY: Number,
+  positionX: Number
+},
+computed:{
+  nodeStyle(){
+    return {
+      top: (this.positionY*120)+'px',
+      left: (300*this.positionX)+'px'
+    }
+  }
+}
+}
+</script>
+
+<style scoped>
+.node-container{
+  width: 300px;
+  height: 120px;
+  box-sizing: border-box;
+  position: absolute;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.node-card{
+  width: 260px;
+  height: 100px;
+  background-color: #42b983;
+}
+
+</style>

+ 128 - 30
src/components/Menu.vue

@@ -3,32 +3,20 @@
     <ElMenuItem class="menu-logo" index="logo" disabled>
       LOGO
     </ElMenuItem>
-    <ElSubmenu index="group">
-      <template #title>{{selectedGroup.name}}</template>
-      <template  v-if="groupList.length > 0">
-      <ElMenuItem v-for="group in groupList" :key="group.id" :index="`group-${group.id}`">{{group.name}}</ElMenuItem>
-      </template>
-      <template v-else>
-        <ElMenuItem disabled>暂未加入任何小组</ElMenuItem>
-      </template>
-    </ElSubmenu>
     <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-if="selectedGroup.id === -1">
-        <ElMenuItem disabled>请先选择小组</ElMenuItem>
-      </template>
       <template v-else>
-        <ElMenuItem disabled>该小组暂无项目</ElMenuItem>
+        <ElMenuItem disabled>暂无项目</ElMenuItem>
       </template>
     </ElSubmenu>
       <template v-if="userLoggedIn">
         <ElSubmenu index="user" class="user">
           <template #title>{{userInfo.username}}</template>
-          <ElMenuItem index="user-create">创建小组</ElMenuItem>
-          <ElMenuItem index="user-join">加入小组</ElMenuItem>
+          <ElMenuItem index="user-project">创建项目</ElMenuItem>
+          <ElMenuItem index="user-invite">邀请成员</ElMenuItem>
           <ElMenuItem index="user-logout">登出</ElMenuItem>
         </ElSubmenu>
       </template>
@@ -36,21 +24,120 @@
         <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>
+  </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>
+  </ElDrawer>
 </template>
 
 <script lang="ts">
+import { ElMessage } from 'element-plus';
+
 import { useProjectList } from '@/composable/useProject';
-import { useGroupList } from '@/composable/useGroup';
 import { useUser } from '@/composable/useUser';
-import { defineComponent } from 'vue';
+import { defineComponent, ref, reactive } from 'vue';
+import { PortalAPI, ProjectAPI } from '@/api';
 
 export default defineComponent({
   name: 'Menu',
-  setup(_, { emit }) {
-    const { projectListOfCurrentGroup: projectList, selectedProject, handleProjectChange } = useProjectList();
-    const { groupList, selectedGroup, handleGroupChange } = useGroupList();
+  setup() {
+    const { projectList, selectedProject, handleProjectChange } = useProjectList();
     const { userInfo, userLoggedIn, userLogOut } = useUser();
 
+    const showProjectDrawer = ref(false);
+    const showInviteDrawer = ref(false);
+
+    const openProjectDrawer = () => {
+      showProjectDrawer.value = true;
+    };
+    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) {
@@ -61,15 +148,11 @@ export default defineComponent({
           handleProjectChange(parseInt(id, 10));
           break;
         }
-        case 'group': {
-          handleGroupChange(parseInt(id, 10));
-          break;
-        }
         case 'user': {
-          if (id === 'create') {
-            emit('show-create-group-drawer');
-          } else if (id === 'join') {
-            emit('show-join-group-drawer');
+          if (id === 'project') {
+            openProjectDrawer();
+          } else if (id === 'invite') {
+            openInviteDrawer();
           } else if (id === 'logout') {
             userLogOut();
           }
@@ -84,12 +167,20 @@ export default defineComponent({
     return {
       projectList,
       selectedProject,
-      groupList,
-      selectedGroup,
+
       handleSelect,
+
       userInfo,
       userLoggedIn,
       userLogOut,
+
+      showProjectDrawer,
+      showInviteDrawer,
+
+      createProjectForm,
+      createProject,
+      inviteProjectForm,
+      inviteMember,
     };
   },
 });
@@ -116,4 +207,11 @@ export default defineComponent({
   height: 60px;
   margin: 0 20px;
 }
+.drawer-content {
+  display: flex;
+  justify-content: center;
+}
+.form-container {
+  width: 80%;
+}
 </style>

+ 124 - 119
src/components/Tree.vue

@@ -1,15 +1,28 @@
 <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"
-    />
+    <template v-for="(col, index) in treeNodeColList" :key="index">
+      <TreeNode
+        v-for="item in col"
+        v-bind="item"
+        :key="item.id"
+        :position-x="index"
+        :node-style="NODE_STYLE"
+        @addChild="addChild"
+        @deleteNode="deleteNode"
+        @nodeClick="nodeClick"
+        :ref="`treenode-${item.id}`"
+      />
+    </template>
+<!--    <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>
@@ -21,6 +34,14 @@ import TreeNode from './TreeNode.vue';
 // 容器宽高
 const HEIGHT = 600;
 const WIDTH = 1100;
+const MARGIN_TOP = 80;
+const MARGIN_LEFT = 100;
+const TREE_MAX_HEIGHT = 4;
+const NODE_STYLE = {
+  width: 250,
+  height: 100,
+  padding: 25,
+};
 
 export default {
   name: 'Tree',
@@ -28,10 +49,77 @@ export default {
   props: {
     data: Array,
   },
+  computed: {
+    /**
+     * @returns {*[][]}
+     */
+    treeNodeColList() {
+      // 将点存入map便于调用
+      const nodeMap = new Map();
+      this.data.forEach((item) => {
+        nodeMap.set(item.id, item);
+      });
+      // 将array转为树状结构;顺便拆成col
+      let rootNode;
+      const colList = [];
+      for (let i = 0; i < TREE_MAX_HEIGHT; i += 1) {
+        colList.push([]);
+      }
+      this.data.forEach((item) => {
+        if (item.deep > 0 && item.deep <= TREE_MAX_HEIGHT) {
+          colList[item.deep - 1].push(item);
+        } else {
+          console.warn('需求树数据有误,树高度错误');
+        }
+        if (item.parent) {
+          const tempParent = nodeMap.get(item.parent);
+          if (!tempParent.childs) {
+            tempParent.childs = [];
+          }
+          tempParent.childs.push(item);
+        } else {
+          rootNode = item;
+        }
+      });
+      // 计算一个节点包含他的后代所需要的空间
+      const calculateNodeContentHeight = (node) => {
+        if (!node.childs) {
+          node.contentHeight = 1;
+          return 1;
+        }
+        node.contentHeight = node.childs.reduce((total, current) => total + calculateNodeContentHeight(current), 0);
+        return node.contentHeight;
+      };
+      calculateNodeContentHeight(rootNode);
+      // 计算节点基准Y轴位置
+      rootNode.positionY = 0;
+      for (let i = 1; i < TREE_MAX_HEIGHT; i += 1) {
+        let sum = 0;
+        colList[i].forEach((item) => {
+          item.positionY = nodeMap.get(item.parent).positionY + sum;
+          sum += item.contentHeight;
+        });
+      }
+      // 计算节点Y轴偏移位置
+      for (let i = TREE_MAX_HEIGHT - 2; i >= 0; i -= 1) {
+        colList[i].forEach((item) => {
+          if (item.childs) {
+            const { length } = item.childs;
+            item.positionY = item.childs.reduce((total, current) => total + current.positionY / length, 0);
+          }
+        });
+      }
+      // 返回col
+      this.drawLine();
+      return colList;
+    },
+  },
   data() {
     return {
       HEIGHT,
       WIDTH,
+      TREE_MAX_HEIGHT,
+      NODE_STYLE,
       parent2children: {},
       depth2node: {},
       layout: {},
@@ -40,122 +128,38 @@ export default {
   },
   mounted() {
     this.initCanvasCtx();
-    this.regenerateTree();
+    // 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,
+    drawLine() {
+      setTimeout(() => {
+        this.ctx.clearRect(0, 0, WIDTH, HEIGHT);
+        this.ctx.lineWidth = 1;
+        this.ctx.strokeStyle = '#bbbbbb';
+        this.data.forEach((item) => {
+          if (!item.childs) return;
+          const parentCard = this.$refs[`treenode-${item.id}`].getTreeNodeSizeAndPosition();
+          console.warn(parentCard);
+          const start = {
+            x: parentCard.left + parentCard.width,
+            y: parentCard.top + parentCard.height / 2,
           };
-        }
-      }
-    },
-    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();
-        }
-      }
+          item.childs.forEach((child) => {
+            const childCard = this.$refs[`treenode-${child.id}`].getTreeNodeSizeAndPosition();
+            const end = {
+              x: childCard.left,
+              y: childCard.top + childCard.height / 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', {
@@ -178,6 +182,7 @@ export default {
 
 <style lang="scss">
 #tree-container {
+  position: relative;
   display: flex;
   justify-content:center;
 }

+ 56 - 25
src/components/TreeNode.vue

@@ -1,8 +1,14 @@
 <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 class="tree-node-container" :style="containerStyle" ref="node">
+    <div class="tree-node" :style="cardStyle"  @click="nodeClick(id)">
+      <ElCard class="tree-node-card" shadow="hover">
+        <template #header>
+          <div class="card-header">
+            <span class="node-title">{{title}}</span>
+            <ElTag class="node-state" :type="getTagTypeFromState(state)">{{state}}</ElTag>
+          </div>
+        </template>
+      </ElCard>
     </div>
     <div class="node-postfix">
       <div class="icon" target="delete" @click="deleteNode(id)"></div>
@@ -12,28 +18,51 @@
 </template>
 
 <script>
+const getTagTypeFromState = (state) => {
+  if (state === '已创建') {
+    return '';
+  } if (state === '进行中') {
+    return 'success';
+  } if (state === '已完成') {
+    return 'info';
+  }
+  return 'danger';
+};
+
 export default {
   name: 'TreeNode',
   props: {
     id: Number,
     state: String,
     title: String,
+    positionY: Number,
+    positionX: Number,
+    nodeStyle: Object,
   },
-  methods: {
-    setTreeNodeOffsetX(x) {
-      this.$refs.node.style.left = `${x}px`;
-    },
-    setTreeNodeOffsetY(y) {
-      this.$refs.node.style.top = `${y}px`;
+  computed: {
+    containerStyle() {
+      return {
+        top: `${this.positionY * 130}px`,
+        left: `${280 * this.positionX}px`,
+        width: `${this.nodeStyle.width + this.nodeStyle.padding * 2}px`,
+        height: `${this.nodeStyle.height + this.nodeStyle.padding * 2}px`,
+      };
     },
-    setTreeNodeOffset(x, y) {
-      this.$refs.node.style.left = `${x}px`;
-      this.$refs.node.style.top = `${y}px`;
+    cardStyle() {
+      return {
+        width: `${this.nodeStyle.width}px`,
+        height: `${this.nodeStyle.height}px`,
+      };
     },
-    getTreeNodeSize() {
+  },
+  methods: {
+    getTagTypeFromState,
+    getTreeNodeSizeAndPosition() {
       return {
-        height: this.$refs.node.offsetHeight,
-        width: this.$refs.node.offsetWidth,
+        top: this.$refs.node.offsetTop + this.nodeStyle.padding,
+        left: this.$refs.node.offsetLeft + this.nodeStyle.padding,
+        height: this.nodeStyle.height,
+        width: this.nodeStyle.width,
       };
     },
     addChild(id) {
@@ -52,15 +81,17 @@ export default {
 <style lang="scss" scoped>
 .tree-node-container {
   position: absolute;
-  height: 140px;
-  width: 20%;
+  box-sizing: border-box;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 
   user-select: none;
   & .node-postfix {
     position: absolute;
-    top: 120px;
+    bottom: 0px;
+    right: 25px;
     width: 100%;
-    height: 20px;
     display: none;
   }
 
@@ -70,10 +101,10 @@ export default {
 }
 .tree-node {
   box-sizing: border-box;
-  border-radius: 2px;
-  border: 1px solid #3f3f3f;
-  height: 120px;
-  width: 100%;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
 }
 .icon {
   width: 14px;
@@ -82,7 +113,7 @@ export default {
   background-position: center;
   background-repeat: no-repeat;
   background-size: 100% 100%;
-  margin-top: 4px;
+  margin-top: 10px;
   margin-right: 10px;
   transition: all .2s linear;
 }

+ 0 - 19
src/composable/useGroup.ts

@@ -1,19 +0,0 @@
-import { computed } from 'vue';
-import { useStore } from '@/store';
-import { WorkspaceDispatcher } from '@/store/dispatchers';
-
-export const useGroupList = () => {
-  const store = useStore();
-
-  const groupList = computed(() => store.state.workspace.groupList);
-  const selectedGroup = computed(() => store.getters.currentGroupInfo);
-
-  const handleGroupChange = (groupId: number) => {
-    WorkspaceDispatcher.changeGroup(groupId);
-  };
-  return {
-    groupList,
-    selectedGroup,
-    handleGroupChange,
-  };
-};

+ 0 - 2
src/composable/useProject.ts

@@ -8,7 +8,6 @@ export const useProjectList = () => {
   const router = useRouter();
 
   const projectList = computed(() => store.state.workspace.projectList);
-  const projectListOfCurrentGroup = computed(() => store.getters.projectListOfCurrentGroup);
   const selectedProject = computed(() => store.getters.currentProjectInfo);
 
   const handleProjectChange = (projectId: number) => {
@@ -17,7 +16,6 @@ export const useProjectList = () => {
   };
   return {
     projectList,
-    projectListOfCurrentGroup,
     selectedProject,
     handleProjectChange,
   };

+ 1 - 3
src/composable/useUser.ts

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

+ 2 - 0
src/element-plus.ts

@@ -5,6 +5,7 @@ import {
   ElCarouselItem,
   ElContainer,
   ElDivider,
+  ElDrawer,
   ElHeader,
   ElLink,
   ElMain,
@@ -41,6 +42,7 @@ export const Components = [
   ElMain,
   ElMenu,
   ElDialog,
+  ElDrawer,
   ElLink,
   ElMenuItem,
   ElCarousel,

+ 16 - 16
src/router/index.ts

@@ -67,22 +67,22 @@ const router = createRouter({
   routes,
 });
 
-// router.beforeEach((to, from, next) => {
-//   if (to.matched.some((record) => record.meta.requiresAuth)) {
-//     // this route requires auth, check if logged in
-//     // if not, redirect to login page.
-//     if (!isUserLoggedIn()) {
-//       next({
-//         name: LOGIN,
-//         query: { redirect: to.fullPath },
-//       });
-//     } else {
-//       next();
-//     }
-//   } else {
-//     next();
-//   }
-// });
+router.beforeEach((to, from, next) => {
+  if (to.matched.some((record) => record.meta.requiresAuth)) {
+    // this route requires auth, check if logged in
+    // if not, redirect to login page.
+    if (!isUserLoggedIn()) {
+      next({
+        name: LOGIN,
+        query: { redirect: to.fullPath },
+      });
+    } else {
+      next();
+    }
+  } else {
+    next();
+  }
+});
 
 router.beforeEach((to, from, next) => {
   if (to.name !== LOGIN || !isUserLoggedIn()) {

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

@@ -4,12 +4,6 @@ export const refreshProjectList = () => {
   store.dispatch('refreshProjectList', store.state.user.id);
 };
 
-export const changeGroup = (groupId: number) => {
-  store.dispatch('changeGroup', { groupId });
-  store.dispatch('resetCurrentProject');
-};
-
 export const changeProject = (projectId: number) => {
-  changeGroup(store.getters.getGroupIdFromProjectId(projectId));
   store.dispatch('changeProject', { projectId });
 };

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

@@ -1,5 +1,4 @@
 import { saveToken, destoryToken } from '@/utils/token';
-import { useRouter } from 'vue-router';
 import { ActionTree, Module, MutationTree } from 'vuex';
 import { RootState, TokenPayload, TokenState } from '../typings';
 
@@ -17,7 +16,6 @@ const actions: ActionTree<TokenState, RootState> = {
       token: '',
     } as TokenPayload);
     destoryToken();
-    useRouter().push('/home');
   },
 };
 

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

@@ -8,6 +8,7 @@ const DEFAULT: UserState = {
   id: -1,
   phone: '',
   role: 'GUEST',
+  withGitlab: false,
 };
 
 const actions: ActionTree<UserState, RootState> = {

+ 1 - 46
src/store/modules/workspace.ts

@@ -3,34 +3,11 @@ import {
   ActionTree, GetterTree, Module, MutationTree,
 } from 'vuex';
 import {
-  WorkspaceState, RootState, ChangeGroupPayload, ChangeProjectPayload,
+  WorkspaceState, RootState, ChangeProjectPayload,
 } from '../typings';
 
 const DEFAULT: WorkspaceState = {
   currentProjectId: -1,
-  currentGroupId: -1,
-  groupList: [
-    {
-      id: 101,
-      name: 'Group-1',
-    },
-    {
-      id: 102,
-      name: 'Group-2',
-    },
-    {
-      id: 13,
-      name: 'Group-3',
-    },
-    {
-      id: 104,
-      name: 'Group-4',
-    },
-    {
-      id: 105,
-      name: 'Group-5',
-    },
-  ],
   projectList: [],
 };
 
@@ -39,9 +16,6 @@ const actions: ActionTree<WorkspaceState, RootState> = {
     const projectList = await ProjectAPI.getProjectListByUserId(userId);
     commit('updateProjectList', projectList);
   },
-  changeGroup({ commit }, payload: ChangeGroupPayload) {
-    commit('changeGroup', payload);
-  },
   changeProject({ commit }, payload: ChangeProjectPayload) {
     commit('changeProject', payload);
   },
@@ -54,24 +28,12 @@ const mutations: MutationTree<WorkspaceState> = {
   updateProjectList(state, payload: Array<Project>) {
     state.projectList = payload;
   },
-  changeGroup(state, { groupId }: ChangeGroupPayload) {
-    state.currentGroupId = groupId;
-  },
   changeProject(state, { projectId }: ChangeProjectPayload) {
     state.currentProjectId = projectId;
   },
 };
 
 const getters: GetterTree<WorkspaceState, RootState> = {
-  projectListOfCurrentGroup(state) {
-    return state.projectList.filter((project) => project.groupId === state.currentGroupId);
-  },
-  getGroupIdFromProjectId(state) {
-    return (projectId: number) => {
-      const p = Array.prototype.find.call(state.projectList, (project) => project.id === projectId) as Project;
-      return p.groupId;
-    };
-  },
   currentProjectInfo(state) {
     return Array.prototype.find.call(state.projectList, (project) => project.id === state.currentProjectId)
       || {
@@ -79,13 +41,6 @@ const getters: GetterTree<WorkspaceState, RootState> = {
         name: '未选择项目',
       };
   },
-  currentGroupInfo(state) {
-    return Array.prototype.find.call(state.groupList, (group) => group.id === state.currentGroupId)
-      || {
-        id: -1,
-        name: '未选择小组',
-      };
-  },
 };
 
 export const workspace: Module<WorkspaceState, RootState> = {

+ 1 - 8
src/store/typings.ts

@@ -1,7 +1,5 @@
-type GroupId = number;
 type ProjectId = number;
 
-// eslint-disable-next-line @typescript-eslint/no-empty-interface
 export interface RootState {
   token: TokenState;
   user: UserState;
@@ -18,19 +16,14 @@ export interface UserState {
   phone: string;
   role: Role;
   username: string;
+  withGitlab: boolean;
 }
 
 export interface WorkspaceState {
-  currentGroupId: GroupId;
   currentProjectId: ProjectId;
-  groupList: Array<Group>;
   projectList: Array<Project>;
 }
 
-export interface ChangeGroupPayload {
-  groupId: GroupId;
-}
-
 export interface ChangeProjectPayload {
   projectId: ProjectId;
 }

+ 2 - 8
src/typings/project.d.ts

@@ -2,17 +2,11 @@ declare interface Project {
   id: number;
   name: string;
   description: string;
-  groupId: number;
   gitRemoteUrl: string;
 }
 
-declare interface Group {
-  id: number;
-  name: string;
-}
-
-declare type TreeNodeType = '0:需求' | '1:任务';
-declare type TreeNodeState = '0:已创建' | '1:进行中' | '2:已结束';
+declare type TreeNodeType = '需求' | '任务';
+declare type TreeNodeState = '已创建' | '进行中' | '已结束';
 
 declare interface TreeNode {
   deep: number;

+ 1 - 2
src/typings/vuex.d.ts

@@ -13,11 +13,10 @@ declare module '@vue/runtime-core' {
       phone: string;
       role: Role;
       username: string;
+      withGitlab: boolean;
     };
     workspace: {
-      currentGroupId: GroupId;
       currentProjectId: ProjectId;
-      groupList: Array<Group>;
       projectList: Array<Project>;
     };
   }

+ 3 - 3
src/views/Login.vue

@@ -11,9 +11,9 @@ import { defineComponent } from 'vue';
 export default defineComponent({
   methods: {
     async login() {
-      window.location = `${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;
+      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;
     },
   },
 });

+ 148 - 43
src/views/Project/Schedule.vue

@@ -1,69 +1,174 @@
 <template>
   <Tree :data="requirements" @change="handleTreeChange" @nodeClick="handleNodeClick" ref="tree"></Tree>
-  <RightForm ref="rightForm"/>
+  <ElDrawer v-model="showRequirementDrawer">
+    <!-- TODO: 供展示和编辑节点详情,可在taskList中复用  -->
+  </ElDrawer>
 </template>
 
 <script>
 import Tree from '@/components/Tree.vue';
-import RightForm from '@/components/RightForm.vue';
 
 import { RequirementAPI } from '@/api';
 
 export default {
   name: 'Schedule',
-  components: { Tree, RightForm },
+  components: { Tree },
   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: '已完成',
-        },
+      requirements: [{
+        deep: 1,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 1,
+        leaf: false,
+        parent: null,
+        priority: 0,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '已创建',
+        timeToTake: 10,
+        title: '这是标题1',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 2,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 2,
+        leaf: false,
+        parent: 1,
+        priority: 0,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '已创建',
+        timeToTake: 10,
+        title: '这是标题2',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 2,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 3,
+        leaf: false,
+        parent: 1,
+        priority: 0,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '进行中',
+        timeToTake: 10,
+        title: '这是标题3',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 3,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 4,
+        leaf: false,
+        parent: 3,
+        priority: 1,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '已完成',
+        timeToTake: 10,
+        title: '这是标题4',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 3,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 5,
+        leaf: false,
+        parent: 3,
+        priority: 2,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '已完成',
+        timeToTake: 10,
+        title: '这是标题5',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 4,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 6,
+        leaf: false,
+        parent: 5,
+        priority: 0,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '进行中',
+        timeToTake: 10,
+        title: '这是标题6',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 4,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 7,
+        leaf: false,
+        parent: 5,
+        priority: 0,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '已完成',
+        timeToTake: 10,
+        title: '这是标题7',
+        type: '需求',
+        userId: 101,
+      },
+      {
+        deep: 4,
+        description: '这里是节点描述啊啊啊。',
+        endTime: '2021/3/17 12:00:01',
+        groupId: 101,
+        id: 8,
+        leaf: false,
+        parent: 5,
+        priority: 0,
+        projectId: 101,
+        startTime: '2021/3/17 12:00:01',
+        state: '已完成',
+        timeToTake: 10,
+        title: '这是标题8',
+        type: '需求',
+        userId: 101,
+      },
       ],
+      showRequirementDrawer: false,
     };
   },
+  async mounted() {
+    // this.requirements = await RequirementAPI.getTreeNodesByProjectId(this.$store.state.workspace.currentProjectId);
+    // this.$refs.tree.regenerateTree();
+  },
   methods: {
     async handleTreeChange(msg) {
-      // TODO: 补充API定义
       const { type, id } = msg;
       await RequirementAPI[type](id);
-      // this.requirements = await RequirementAPI.getTreeNode(projectId);
+      // this.requirements = await RequirementAPI.getTreeNodesByProjectId(this.$store.state.workspace.currentProjectId);
       this.$refs.tree.regenerateTree();
     },
     handleNodeClick(id) {
-      this.$refs.rightForm.showForm();
+      this.showRequirementDrawer = true;
+      this.requirementInDrawer = Array.prototype.find.call(this.requirements, (requirement) => requirement.id === id);
     },
   },
 };