Browse Source

feat: 修改文件评审相关功能

山山 4 năm trước cách đây
mục cha
commit
132e027bbd

+ 0 - 11
Dockerfile

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

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 94 - 321
package-lock.json


+ 7 - 2
package.json

@@ -11,14 +11,19 @@
     "mock": "node mock/index.js"
   },
   "dependencies": {
+    "@highlightjs/vue-plugin": "^2.0.1",
     "axios": "^0.21.1",
+    "codemirror": "^5.65.2",
+    "codemirror-editor-vue3": "^2.0.6",
     "core-js": "^3.6.5",
-    "echarts": "^5.1.1",
+    "echarts": "^5.3.2",
     "element-plus": "^1.0.2-beta.31",
+    "highlight.js": "^11.5.1",
     "jsonwebtoken": "^8.5.1",
     "vue": "^3.0.0",
     "vue-axios": "^3.2.4",
     "vue-class-component": "^8.0.0-0",
+    "vue-codemirror": "^4.0.6",
     "vue-json-editor": "^1.4.3",
     "vue-router": "^4.0.0-0",
     "vuex": "^4.0.0"
@@ -32,7 +37,7 @@
     "@vue/cli-plugin-typescript": "~4.5.0",
     "@vue/cli-plugin-vuex": "~4.5.0",
     "@vue/cli-service": "~4.5.0",
-    "@vue/compiler-sfc": "^3.0.0",
+    "@vue/compiler-sfc": "^3.0.4",
     "@vue/eslint-config-airbnb": "^5.0.2",
     "@vue/eslint-config-typescript": "^5.0.2",
     "babel-plugin-component": "^1.1.1",

+ 29 - 7
src/api/devcloud/codereview.ts

@@ -32,24 +32,46 @@ interface CommentPayload {
 
 export const getProjectBranches = (projectId: number) => axios.get(`${CODEREVIEW_PREFIX}/branches/${projectId}`);
 
-export const createCodeReview = (codeReview: CodeReviewPayload) => axios.post(`${CODEREVIEW_PREFIX}/create`, codeReview);
+export const createCodeReview = (codeReview: CodeReviewPayload) => axios.post(`${CODEREVIEW_PREFIX}/branch_review`, codeReview);
 
 export const getCodeReviewList = (projectId: number) => axios.get(`${CODEREVIEW_PREFIX}/${projectId}`);
 
 export const compareBranch = (projectId: number, fromBash: string, toBash: string) => axios.get(`${CODEREVIEW_PREFIX}/compare?projectId=${projectId}&fromBash=${fromBash}&toBash=${toBash}`);
 
-export const getCodeReviewDetail = (codeReviewId: number) => axios.get(`${CODEREVIEW_PREFIX}/get/${codeReviewId}`);
+export const getCodeReviewDetail = (codeReviewId: number) => axios.get(`${CODEREVIEW_PREFIX}/branch_review/${codeReviewId}`);
 
 export const createComment = (comment: CommentPayload) => axios.post(`${COMMENT_PREFIX}/create`, comment);
 
 export const getComment = (codeReviewId: number) => axios.get(`${COMMENT_PREFIX}/${codeReviewId}`);
 
-export const getRepositoryTree = (projectId: number, branch: string) => axios.get(`${FILEREVIEW_PREFIX}/repository_tree?projectId=${projectId}&branch=${branch}`);
+export const closeCodeReview = (codeReviewId: number) => axios.put(`${CODEREVIEW_PREFIX}/close/${codeReviewId}`);
 
-export const getSubTree = (projectId: number, branch: string, path: string) => axios.get(`${FILEREVIEW_PREFIX}/repository_tree?projectId=${projectId}&branch=${branch}&path=${path}`);
+export const reopenCodeReview = (codeReviewId: number) => axios.put(`${CODEREVIEW_PREFIX}/reopen/${codeReviewId}`);
 
-export const inviteUser = (phoneNumber: string) => axios.get(`${FILEREVIEW_PREFIX}/invite?phoneNumber=${phoneNumber}`);
+export const mergeCodeReview = (codeReviewId: number) => axios.put(`${CODEREVIEW_PREFIX}/merge/${codeReviewId}`);
 
-export const createFileReview = (fileReview: FileReviewPayload) => axios.post(`${FILEREVIEW_PREFIX}/create`, fileReview);
+export const getRepositoryTree = (projectId: number, branch: string) => axios.get(`${CODEREVIEW_PREFIX}/repository_tree?projectId=${projectId}&branch=${branch}`);
 
-export const getFileReviewDetail = (fileReviewId: number) => axios.get(`${FILEREVIEW_PREFIX}/${fileReviewId}`);
+export const getSubTree = (projectId: number, branch: string, path: string) => axios.get(`${CODEREVIEW_PREFIX}/repository_tree?projectId=${projectId}&branch=${branch}&path=${path}`);
+
+export const inviteUser = (phoneNumber: string) => axios.get(`${CODEREVIEW_PREFIX}/invite?phoneNumber=${phoneNumber}`);
+
+export const createFileReview = (fileReview: FileReviewPayload) => axios.post(`${CODEREVIEW_PREFIX}/file_review`, fileReview);
+
+export const getFileReviewDetail = (fileReviewId: number) => axios.get(`${CODEREVIEW_PREFIX}/file_review/${fileReviewId}`);
+
+export const updateFileList = (fileReviewId: number, fileList: Array<string>) => axios.put(`${CODEREVIEW_PREFIX}/file_review?fileReviewId=${fileReviewId}&fileList=${fileList}`);
+
+export const getFileContent = (projectId: number, branch: string, path: string) => axios.get(`${CODEREVIEW_PREFIX}/file_content?projectId=${projectId}&branch=${branch}&path=${path}`);
+
+export const submitToInspection = (fileReviewId: number) => axios.put(`${CODEREVIEW_PREFIX}/inspect?fileReviewId=${fileReviewId}`);
+
+export const createCommentForFileReview = (comment: CommentPayload) => axios.post(`${COMMENT_PREFIX}/createForFileReview`, comment);
+
+export const getFileReviewComment = (fileReviewId: number) => axios.get(`${COMMENT_PREFIX}/listForFileReview/${fileReviewId}`);
+
+export const submitToRework = (fileReviewId: number, reviewerId: number) => axios.put(`${CODEREVIEW_PREFIX}/rework?fileReviewId=${fileReviewId}&reviewerId=${reviewerId}`);
+
+export const submitToComplete = (fileReviewId: number, reviewerId: number) => axios.put(`${CODEREVIEW_PREFIX}/complete?fileReviewId=${fileReviewId}&reviewerId=${reviewerId}`);
+
+export const checkInspected = (fileReviewId: number, reviewerId: number) => axios.get(`${CODEREVIEW_PREFIX}/hasInspected?fileReviewId=${fileReviewId}&reviewerId=${reviewerId}`);

+ 9 - 8
src/components/CodeReviewAddDrawer.vue

@@ -23,7 +23,7 @@
               </div>
             </div>
             <div class="short-id-block">
-              <div class="short-id-line">{{branches[branchOptions.indexOf(sourceBranch)].commit.short_id}}</div>
+              <div class="short-id-line">{{branches[branchOptions.indexOf(sourceBranch)].commit.shortId}}</div>
               <div class="copy-button"><i class="el-icon-document-copy"/></div>
             </div>
         </div>
@@ -46,7 +46,7 @@
               </div>
             </div>
             <div class="short-id-block">
-              <div class="short-id-line">{{branches[branchOptions.indexOf(targetBranch)].commit.short_id}}</div>
+              <div class="short-id-line">{{branches[branchOptions.indexOf(targetBranch)].commit.shortId}}</div>
               <div class="copy-button"><i class="el-icon-document-copy"/></div>
             </div>
         </div>
@@ -66,17 +66,17 @@
             <el-option v-for=" member in projectMemberList" :label="member.label" :key="member.id" :value="member.value"/>
           </el-select>
         </el-form-item>
-        <el-form-item prop="deleteSourceBranch">
+        <!-- <el-form-item prop="deleteSourceBranch">
           <el-switch v-model="codeReviewCreateForm.deleteSourceBranch"
             active-text="合并后删除源分支"
             active-color="#13ce66"/>
-        </el-form-item>
+        </el-form-item> -->
         <el-form-item>
           <el-button type="success" v-loading="submitLoading" size="medium" @click="submitForm">立即创建</el-button>
           <el-button type="text" size="medium" @click="backToSelectBranch">重新选择分支</el-button>
         </el-form-item>
       </el-form>
-      <CodeDifferencePlate :data="diffMsg.res"/>
+      <CodeDifferencePlate :data="diffMsg"/>
     </div>
   </el-drawer>
 </template>
@@ -133,10 +133,10 @@ export default {
       this.showDrawer = true;
     },
     getAuthorName(branchName) {
-      return this.branches[this.branchOptions.indexOf(branchName)].commit.author_name;
+      return this.branches[this.branchOptions.indexOf(branchName)].commit.authorName;
     },
     getCreateTime(branchName) {
-      return this.branches[this.branchOptions.indexOf(branchName)].commit.created_at.split('T')[0];
+      return this.branches[this.branchOptions.indexOf(branchName)].commit.createdAt.split('T')[0];
     },
     getBranchBash(branchName) {
       return this.branches[this.branchOptions.indexOf(branchName)].commit.id;
@@ -179,6 +179,7 @@ export default {
           CodeReviewAPI.createCodeReview(this.codeReviewCreateForm).then(
             (res) => {
               this.showDrawer = false;
+              this.$parent.refreshCodeReviewList();
               this.handleDrawerClose(() => {});
             }
           );
@@ -201,7 +202,7 @@ export default {
   },
   async mounted(){
       //更新项目名称,项目Id,创建者Id
-      this.projectOptions[0] = this.sourceProject = this.targetProject = this.getProjectNameFromUrl(this.currentProjectInfo.gitRemoteUrl);
+      this.projectOptions[0] = this.sourceProject = this.targetProject = this.currentProjectInfo.name;
       this.codeReviewCreateForm.projectId = this.currentProjectInfo.id;
       this.codeReviewCreateForm.creatorId = this.currentUser.id;
       //获取项目的分支信息

+ 22 - 11
src/components/FileReviewAddDrawer.vue

@@ -12,11 +12,15 @@
       <el-select v-model="fileReviewForm.reviewerList" multiple placeholder="请选择评审人,可以通过手机号邀请">
         <el-option v-for="item in reviewerOption" :key="item.id" :label="item.label" :value="item.value"/>
       </el-select>
-      <el-popover width="230" trigger="click">
-        <div class="popoverContent">
-          <el-input v-model="phoneNumber" placeholder="请输入手机号码"></el-input>
-          <el-button icon="el-icon-search" circle size="mini" @click="inviteUser"></el-button>
-        </div>
+      <el-popover width="300" trigger="click">
+        <el-row>
+          <el-input v-model="phoneNumber" placeholder="请输入手机号码">
+            <template #append>
+              <el-button icon="el-icon-search" circle size="mini" @click="inviteUser"></el-button>
+            </template>
+          </el-input>
+          <!-- <el-button icon="el-icon-search" circle size="mini" @click="inviteUser"></el-button> -->
+        </el-row>
         <template #reference><el-button type="primary" style="margin-left: 20px">邀请用户</el-button></template>
       </el-popover>
       <el-input v-model="fileReviewForm.title" placeholder="请输入标题" minlength="1" maxlength="20" show-word-limit></el-input>
@@ -95,13 +99,25 @@ export default {
     },
     createFileReview() {
       this.fileReviewForm.fileList = this.$refs.fileTree.getCheckedKeys();
-      console.log(this.fileReviewForm);
+      if (this.fileReviewForm.fileList.length === 0) {
+        this.$message({ message: '文件列表不能为空', type: 'error' });
+        return;
+      }
+      if (this.fileReviewForm.reviewerList.length === 0) {
+        this.$message({ message: '评审不能为空', type: 'error' });
+        return;
+      }
+      if (this.fileReviewForm.title === '') {
+        this.$message({ message: '标题不能为空', type: 'error' });
+        return;
+      }
       CodeReviewAPI.createFileReview(this.fileReviewForm).then(
         (res) => {
           this.showDrawer = false;
           this.handleDrawerClose(
             () => {
               console.log('finishAdd');
+              this.$parent.refreshCodeReviewList();
             },
           );
         },
@@ -158,11 +174,6 @@ export default {
 .el-select {
   width: 60%;
 }
-.popoverContent {
-  > * {
-    display: inline-block;
-  }
-}
 ::v-deep {
   .el-tree {
     .el-tree-node__content {

+ 4 - 1
src/components/Menu.vue

@@ -205,7 +205,7 @@ export default defineComponent({
 });
 </script>
 
-<style scoped>
+<style lang="scss" scoped>
 .menu {
   width: 100%;
 }
@@ -236,5 +236,8 @@ export default defineComponent({
 }
 .message {
   float: right;
+  .el-badge {
+    height: 20px;
+  }
 }
 </style>

+ 2 - 0
src/element-plus.ts

@@ -47,6 +47,7 @@ import {
   ElCollapse,
   ElCollapseItem,
   ElPopover,
+  ElBadge,
 } from 'element-plus';
 
 import ZH_CN from 'element-plus/lib/locale/lang/zh-cn';
@@ -100,6 +101,7 @@ export const Components = [
   ElCollapse,
   ElCollapseItem,
   ElPopover,
+  ElBadge,
 ];
 
 export const Plugins = [

+ 3 - 1
src/views/Project/CodeReview.vue

@@ -63,7 +63,6 @@
 <script>
 import CodeReviewAddDrawer from '@/components/CodeReviewAddDrawer.vue';
 import FileReviewAddDrawer from '@/components/FileReviewAddDrawer.vue';
-import { useRouter } from 'vue-router';
 import { CodeReviewAPI } from '../../api';
 
 export default {
@@ -120,6 +119,9 @@ export default {
     addFileReview() {
       this.$refs.fileReviewAddDrawer.show();
     },
+    async refreshCodeReviewList() {
+      this.codeReviewList = await CodeReviewAPI.getCodeReviewList(this.currentProjectInfo.id);
+    },
     directToDetail(id) {
       this.$router.push(`codereviewdetail?codeReviewId=${id}`);
     },

+ 70 - 18
src/views/Project/CodeReviewDetail.vue

@@ -7,24 +7,42 @@
       <el-tag type="danger" effect="dark" v-if="codeReviewDetail.status === 'Closed'">Closed</el-tag>
       <el-tag type="info" effect="dark" v-if="codeReviewDetail.status === 'Merged'">Merged</el-tag>
       <div>opened on {{codeReviewDetail.createTime.split('T')[0]}} by {{getCreatorName(codeReviewDetail.creatorId)}}</div>
-      <el-button type="warning" plain size="medium" v-if="codeReviewDetail.status === 'Opened'">Close merge request</el-button>
+      <el-button type="warning" plain size="medium" v-if="codeReviewDetail.status === 'Opened'" @click="closeCodeReview()">Close merge request</el-button>
+      <el-button type="success" plain size="medium" v-if="codeReviewDetail.status === 'Closed'" @click="reopenCodeReview()">Reopen merge request</el-button>
     </div>
     <el-divider/>
     <div>Request to merge <span class="branch-name">{{codeReviewDetail.sourceBranch}}</span> into <span class="branch-name">{{codeReviewDetail.targetBranch}}</span></div>
     <div class="operation-line">
-      <el-button type="success" size="medium" plain :disabled="!codeReviewDetail.status === 'Opened'">Merge</el-button>
-      <el-switch v-model="codeReviewDetail.deleteSourceBranch" active-text="合并后删除源分支"></el-switch>
+      <el-button type="success" size="medium" plain
+      :disabled="disableMergeButton()"
+      @click="mergeCodeReview()">
+        Merge
+      </el-button>
+      <!-- <el-switch v-model="codeReviewDetail.deleteSourceBranch" active-text="合并后删除源分支"></el-switch> -->
     </div>
     <el-divider/>
     <ElTabs v-model="activeTab">
       <ElTabPane :label="'评审意见'" name="comments">
-        <div v-for="comment in commentList" :key="comment.id" class="comment-line">
+        <!-- <div v-for="comment in commentList" :key="comment.id" class="comment-line">
           <div>
             <i class="el-icon-plus"/>{{comment.creatorName}}
             <span class="vice-comment"> @ {{comment.creatorName}} {{comment.commentType}}ed at {{comment.time}}</span>
           </div>
           <div class="comment-content">{{comment.content}}</div>
-        </div>
+        </div> -->
+        <el-timeline reverse = "true">
+          <el-timeline-item v-for="(item, index) in commentList" :key="index" :timestamp="formatTime(item.time)" class="comment-item">
+            <el-row>
+              <el-tag size="small" type="primary" v-if="item.commentType==='Commit'">Commited</el-tag>
+              <el-tag size="small" type="info" v-if="item.commentType==='Comment'">Commented</el-tag>
+              <el-tag size="small" type="success" v-if="item.commentType==='Reopen'">Reopened</el-tag>
+              <el-tag size="small" type="danger" v-if="item.commentType==='Close'">Closed</el-tag>
+              <el-tag size="small" type="success" v-if="item.commentType==='Merge'">Merged</el-tag>
+              <span class="creator-name-plate">by {{item.creatorName}}</span>
+            </el-row>
+            <el-row>{{item.content}}</el-row>
+          </el-timeline-item>
+        </el-timeline>
         <div class="text-container">
           <div>创建新的评审意见</div>
           <el-input type="textarea" :rows="5" v-model="newComment.content"/>
@@ -32,7 +50,7 @@
         </div>
       </ElTabPane>
       <ElTabPane :label="'代码差异'" name="codeDiff">
-        <CodeDifferencePlate :data="codeDiffMsg.res"/>
+        <CodeDifferencePlate :data="codeDiffMsg"/>
       </ElTabPane>
     </ElTabs>
   </div>
@@ -51,6 +69,7 @@ export default {
       codeReviewDetail: {
         creatorId: 0,
         createTime: '',
+        reviewerId: 0,
       },
       commentList: [
         {
@@ -104,12 +123,49 @@ export default {
     },
     submitComment() {
       CodeReviewAPI.createComment(this.newComment).then(
-        (res) => {
-          this.commentList.push(JSON.parse(JSON.stringify(this.newComment)));
+        async (res) => {
+          this.commentList = await CodeReviewAPI.getComment(this.codeReviewId);
           this.newComment.content = '';
         },
       );
     },
+    formatTime(timestamp) {
+      return new Date(timestamp).toLocaleString();
+    },
+    closeCodeReview() {
+      CodeReviewAPI.closeCodeReview(this.codeReviewId).then(
+        async (res) => {
+          this.codeReviewDetail.status = 'Closed';
+          this.$message('代码评审任务已关闭');
+          this.commentList = await CodeReviewAPI.getComment(this.codeReviewId);
+        },
+      );
+    },
+    reopenCodeReview() {
+      CodeReviewAPI.reopenCodeReview(this.codeReviewId).then(
+        async (res) => {
+          this.codeReviewDetail.status = 'Opened';
+          this.$message('代码评审任务已重新开启');
+          this.commentList = await CodeReviewAPI.getComment(this.codeReviewId);
+        },
+      );
+    },
+    disableMergeButton() {
+      if (this.codeReviewDetail.status !== 'Opened') return true;
+      if (this.currentUser.id !== this.codeReviewDetail.reviewerId) return true;
+      return false;
+    },
+    mergeCodeReview() {
+      CodeReviewAPI.mergeCodeReview(this.codeReviewId).then(
+        async (res) => {
+          if (res === null) {
+            this.codeReviewDetail.status = 'Merged';
+            this.$message('代码评审任务已合并分支');
+            this.commentList = await CodeReviewAPI.getComment(this.codeReviewId);
+          }
+        },
+      );
+    },
   },
   computed: {
     projectMemberList() {
@@ -172,17 +228,13 @@ export default {
   color: #1b69b6;
   font-weight: 600;
 }
-.comment-line {
-  border-bottom: gainsboro solid 1px;
-  font-size: 16px;
-  padding: 7px 10px;
-}
-.vice-comment {
-  color: #707070;
+.comment-item {
+  .el-row {
+    padding: 3px;
+  }
 }
-.comment-content {
-  margin-top: 7px;
-  margin-left: 50px;
+.creator-name-plate {
+  margin-left: 6px;
 }
 .text-container {
   font-size: 16px;

+ 303 - 5
src/views/Project/FileReviewDetail.vue

@@ -1,16 +1,97 @@
 <template>
   <div class="container">
-    <h2>{{fileReview.title}}</h2>
-    <el-tree :load="loadNode" :props="fileListProps" lazy show-checkbox ref="fileTree" node-key="path"></el-tree>
+    <el-row>
+      <h2>{{fileReview.title}}</h2>
+      <el-tag class="status-tag" size="medium" v-if="fileReview.status === 'Planning'">计划中</el-tag>
+      <el-tag class="status-tag" size="medium" v-if="fileReview.status === 'Inspecting'">审查中</el-tag>
+      <el-tag class="status-tag" size="medium" v-if="fileReview.status === 'Rework'">修改中</el-tag>
+      <el-tag class="status-tag" size="medium" v-if="fileReview.status === 'Completed'">已结束</el-tag>
+    </el-row>
+    <el-row class="status-information">
+      <i class="el-icon-info"></i>
+      <div v-if="fileReview.status === 'Planning' && userRole === 'WORKER'">目前正处于计划阶段,您可以更改递交审查的文件列表</div>
+      <div v-if="fileReview.status === 'Planning' && userRole === 'CREATOR'">目前正处于计划阶段,您可以更改递交审查的文件列表,确认无误后请点击递交审查进入下一阶段</div>
+      <div v-if="fileReview.status === 'Planning' && userRole === 'REVIEWER'">目前正处于计划阶段,您可以预览需评审文件,但暂时无法发表评论</div>
+      <div v-if="fileReview.status === 'Inspecting' && userRole === 'WORKER'">目前正处于评审阶段,请等待所有评审人评审完成</div>
+      <div v-if="fileReview.status === 'Inspecting' && userRole === 'CREATOR'">目前正处于评审阶段,请等待所有评审人评审完成</div>
+      <div v-if="fileReview.status === 'Inspecting' && userRole === 'REVIEWER' && !hasInspected">目前正处于评审阶段,请提出您的评审意见,并根据结果选择返工或是结束</div>
+      <div v-if="fileReview.status === 'Inspecting' && userRole === 'REVIEWER' && hasInspected">目前正处于评审阶段,您已提交了您的评审结果,请等待其他人评审结束</div>
+      <div v-if="fileReview.status === 'Rework' && userRole === 'WORKER'">目前正处于返工阶段,请根据评审意见完成修改</div>
+      <div v-if="fileReview.status === 'Rework' && userRole === 'CREATOR'">目前正处于返工阶段,请根据评审意见完成修改,修改完毕后请点击递交审查进入下一阶段</div>
+      <div v-if="fileReview.status === 'Rework' && userRole === 'REVIEWER'">目前正处于返工阶段,请等待修改完毕</div>
+      <div v-if="fileReview.status === 'Completed'">代码已通过评审</div>
+    </el-row>
+    <el-row>
+      <h3>需评审文件列表</h3>
+      <div class="button-plate">
+        <el-button type="primary" size="small" v-if="showUpdateFileList()" @click="updateFileList">更新文件列表</el-button>
+        <el-button type="success" size="small" v-if="showMoveToInspect()" @click="submitToInspect()">递交审查</el-button>
+        <el-button type="primary" size="small" v-if="showSubmitToRework()" @click="submitToRework()" :disabled="hasInspected">递交返工</el-button>
+        <el-button type="success" size="small" v-if="showSubmitToRework()" @click="submitToComplete()" :disabled="hasInspected">结束评审</el-button>
+      </div>
+    </el-row>
+    <el-tree :load="loadNode" :props="fileListProps" lazy show-checkbox ref="fileTree" node-key="path" @node-click="clickTreeNode"></el-tree>
+    <ElTabs v-model="activeTab" style="width: 90%">
+      <ElTabPane :label="'代码展示'" name="codePlate">
+        <div class="file-headline">{{fileName}}</div>
+        <div class="code-plate">
+          <el-row v-for="(line, index) in code.split('\n')" :key="index">
+            <el-col :span="1" class="line-number">
+              <button @click="clickLine(index+1)">{{index+1}}</button>
+            </el-col>
+            <el-col :span="23">
+              <highlightjs language="java" :code="line" v-if="line !== ''" />
+              <highlightjs language="java" :code="'&emsp;'" v-if="line === ''"/>
+            </el-col>
+          </el-row>
+          <el-drawer v-model="showAddCommentDrawer" >
+            <template #title><h4>添加评论</h4></template>
+            <el-form ref="addComment" :model="addCommentForm" :rules="addCommentRules">
+              <el-form-item label="选择类型">
+                <el-select v-model="addCommentForm.commentType">
+                  <el-option label="普通" value="Comment"/>
+                  <el-option label="警告" value="Warning"/>
+                  <el-option label="严重错误" value="Error"/>
+                </el-select>
+              </el-form-item>
+              <el-form-item label="填写意见" prop="content">
+                <el-input type="textarea" :autosize="{minRows:3, maxRows: 5}" v-model="addCommentForm.content"></el-input>
+              </el-form-item>
+              <el-form-item @click="addComment()">
+                <el-button type="success">创建</el-button>
+              </el-form-item>
+            </el-form>
+          </el-drawer>
+        </div>
+      </ElTabPane>
+      <ElTabPane :label="'评审意见'" name="commentList">
+        <div v-if="commentList.length!==0">
+          <div class="comment-line" v-for="item in commentList" :key="item.id">
+            <el-row>
+              <el-tag size="mini" effect="plain" type="warning" v-if="item.commentType==='Warning'">Warning</el-tag>
+              <el-tag size="mini" effect="plain" type="danger" v-if="item.commentType==='Error'">Error</el-tag>
+              at &nbsp;<span class="path">{{item.content.split('\t')[0]}}</span>, Line&nbsp;<span class="path"> {{item.content.split('\t')[1]}}</span>
+            </el-row>
+            <el-row>{{item.content.split('\t')[2]}}</el-row>
+          </div>
+        </div>
+      </ElTabPane>
+    </ElTabs>
   </div>
 </template>
 <script>
+import 'highlight.js/lib/common';
+import 'highlight.js/styles/base16/dracula.css';
+import hljsVuePlugin from '@highlightjs/vue-plugin';
 import { CodeReviewAPI } from '../../api';
 
 export default {
   name: 'FileReviewDetail.vue',
+  components: { highlightjs: hljsVuePlugin.component },
   data() {
     return {
+      activeTab: 'codePlate',
+      showAddCommentDrawer: false,
       fileReviewId: -1,
       fileReview: {
         id: -1,
@@ -22,6 +103,7 @@ export default {
         createTime: '',
         status: '',
       },
+      userRole: '',
       fileListProps: {
         label: 'name',
         children: 'children',
@@ -31,11 +113,44 @@ export default {
         name: 'file1',
         path: '',
       }],
+      nodeTreeRootConfig: {
+        nodeLevel0: {},
+        resolveLevel0: {},
+      },
+      fileName: '',
+      code: 'const i = 100;\nvar adc = true;\n',
+      commentList: [{
+        id: -1,
+        creatorId: -1,
+        creatorName: 'ShanShan',
+        content: 'main.java\t13\tthe veriable i should have an another name',
+        commentType: 'Error',
+      }],
+      addCommentForm: {
+        codeReviewId: -1,
+        creatorId: -1,
+        creatorName: '',
+        commentType: 'Comment',
+        content: '',
+        replyId: -1,
+      },
+      selectedLine: {
+        path: '',
+        line: -1,
+      },
+      addCommentRules: {
+        content: [{ required: true, message: '请输入评审意见', trigger: 'blur' }],
+      },
+      hasInspected: false,
     };
   },
   methods: {
     async loadNode(node, resolve) {
       if (node.level === 0) {
+        this.nodeTreeRootConfig = {
+          nodeLevel0: node,
+          resolveLevel0: resolve,
+        };
         return resolve(this.fileList);
       }
       if (node.level > 0) {
@@ -44,14 +159,113 @@ export default {
       }
       return resolve([]);
     },
+    setUserRole() {
+      if (this.currentUserId === this.fileReview.creatorId) this.userRole = 'CREATOR';
+      else if (this.fileReview.reviewerList.includes(this.currentUserId)) this.userRole = 'REVIEWER';
+      else this.userRole = 'WORKER';
+    },
+    showUpdateFileList() {
+      return this.fileReview.status === 'Planning' && (this.userRole === 'WORKER' || this.userRole === 'CREATOR');
+    },
+    updateFileList() {
+      this.fileReview.fileList = this.$refs.fileTree.getCheckedKeys();
+      CodeReviewAPI.updateFileList(this.fileReview.id, this.fileReview.fileList).then(
+        (res) => {
+          this.$message({
+            message: '文件列表更新成功',
+            type: 'success',
+          });
+        },
+      );
+    },
+    showMoveToInspect() {
+      return (this.fileReview.status === 'Planning' || this.fileReview.status === 'Rework') && this.userRole === 'CREATOR';
+    },
+    submitToInspect() {
+      CodeReviewAPI.submitToInspection(this.fileReview.id).then(
+        (res) => {
+          this.$message({ message: '递交审查成功', type: 'success' });
+          this.fileReview.status = 'Inspecting';
+        },
+      );
+    },
+    showSubmitToRework() {
+      return this.fileReview.status === 'Inspecting' && this.userRole === 'REVIEWER';
+    },
+    async submitToRework() {
+      this.fileReview.status = await CodeReviewAPI.submitToRework(this.fileReview.id, this.currentUserId);
+      this.$message({ message: '递交结果成功', type: 'success' });
+      this.hasInspected = true;
+    },
+    async submitToComplete() {
+      this.fileReview.status = await CodeReviewAPI.submitToComplete(this.fileReview.id, this.currentUserId);
+      this.$message({ message: '递交结果成功', type: 'success' });
+      this.hasInspected = true;
+    },
+    async clickTreeNode(data, node, obj) {
+      if (node.isLeaf) {
+        if (this.fileReview.fileList.includes(data.path)) {
+          this.fileName = data.path;
+          this.code = await CodeReviewAPI.getFileContent(this.fileReview.projectId, this.fileReview.branch, this.fileName);
+        } else {
+          this.$message('选中的文件不在评审列表中');
+        }
+      }
+    },
+    clickLine(lineNum) {
+      if (this.userRole === 'REVIEWER' && this.fileReview.status === 'Inspecting' && !this.hasInspected) {
+        this.selectedLine.path = this.fileName;
+        this.selectedLine.line = lineNum;
+        this.showAddCommentDrawer = true;
+      }
+      // this.selectedLine.path = this.fileName;
+      // this.selectedLine.line = lineNum;
+      // this.showAddCommentDrawer = true;
+    },
+    addComment() {
+      this.$refs.addComment.validate(
+        (valid) => {
+          if (valid) {
+            this.addCommentForm.content = `${this.selectedLine.path}\t${this.selectedLine.line}\t${this.addCommentForm.content}`;
+            CodeReviewAPI.createCommentForFileReview(this.addCommentForm).then(
+              async (res) => {
+                this.$message({ message: '创建评审意见成功', type: 'success' });
+                this.$refs.addComment.resetFields();
+                this.commentList = await CodeReviewAPI.getFileReviewComment(this.fileReview.id);
+                this.showAddCommentDrawer = false;
+              },
+            );
+          }
+        },
+      );
+    },
+    getFirstFile() {
+      let ret = '';
+      this.fileReview.fileList.forEach((file) => {
+        if (file.includes('.')) ret = file;
+      });
+      return ret;
+    },
+  },
+  computed: {
+    currentUserId() {
+      return this.$store.state.user.id;
+    },
   },
-  async created() {
+  async beforeCreate() {
     this.fileReviewId = this.$route.query.fileReviewId;
     this.fileReview = await CodeReviewAPI.getFileReviewDetail(this.fileReviewId);
     this.fileList = await CodeReviewAPI.getRepositoryTree(this.fileReview.projectId, this.fileReview.branch);
-    console.log(this.fileList);
+    this.nodeTreeRootConfig.nodeLevel0.childNodes = [];
+    this.loadNode(this.nodeTreeRootConfig.nodeLevel0, this.nodeTreeRootConfig.resolveLevel0);
     this.$refs.fileTree.setCheckedKeys(this.fileReview.fileList);
-    console.log(this.fileReview);
+    this.setUserRole();
+    this.fileName = this.getFirstFile();
+    this.code = await CodeReviewAPI.getFileContent(this.fileReview.projectId, this.fileReview.branch, this.fileName);
+    this.commentList = await CodeReviewAPI.getFileReviewComment(this.fileReview.id);
+    this.addCommentForm.codeReviewId = this.fileReview.id;
+    this.addCommentForm.creatorId = this.currentUserId;
+    this.hasInspected = await CodeReviewAPI.checkInspected(this.fileReview.id, this.currentUserId);
   },
 };
 </script>
@@ -62,4 +276,88 @@ export default {
     margin-top: 10px;
   }
 }
+.status-tag {
+  margin-left:25px;
+}
+.status-information {
+  color: #909399;
+  font-size: 14px;
+}
+.button-plate {
+  position: absolute;
+  right: 10%;
+  z-index: 10;
+}
+.el-tree {
+  border-style: solid;
+  border-width: 1px;
+  border-color: gainsboro;
+  border-radius: 5px;
+  width: 90%;
+}
+.file-headline {
+  width: 90%;
+  height: 24px;
+  padding: 5px;
+  font-size: 19px;
+}
+.code-plate {
+  width: 100%;
+  max-height: 450px;
+  margin-top: 0px;
+  overflow-y: auto;
+  .el-row {
+    height: 30px;
+    .line-number {
+      border-right: solid 1px #909399;
+      button {
+        width: 100%;
+        height: 100%;
+        padding: 5px;
+        border-width: 0px;
+        text-align: right;
+        background-color: #282936;
+        color: #909399;
+        font-family: Consolas, Monaco, monospace;
+        font-size: 14px;
+        cursor: pointer;
+      }
+      :focus-within {
+        background-color: #535353;
+      }
+    }
+  }
+}
+.comment-line {
+  padding: 5px 10px;
+  border-bottom: solid 1px gainsboro;
+  .el-row {
+    padding: 2px;
+    .el-tag {
+      margin-right: 20px;
+    }
+    .path {
+      color: #409EFF;
+    }
+  }
+}
+.el-form {
+  padding: 12px;
+}
+::v-deep {
+  .el-tree {
+    .el-tree-node__content {
+      height: 35px;
+    }
+  }
+  .code-plate {
+    code {
+      padding-top: 5px;
+      padding-bottom: 5px;
+      height: 20px;
+      font-family: Consolas, Monaco, monospace;
+      font-size: 14px;
+    }
+  }
+}
 </style>

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác