Browse Source

增加了测试用例管理的功能和临时改动了登录的入口

linyk 3 years ago
parent
commit
c89336dd98
38 changed files with 20906 additions and 136 deletions
  1. 1 1
      config/dev.env.js
  2. 1 1
      config/proxyConfig.js
  3. 17589 1
      package-lock.json
  4. 1 0
      package.json
  5. 8 13
      src/App.vue
  6. 65 0
      src/components/DragSelect/index.vue
  7. 101 0
      src/components/Pagination/index.vue
  8. 111 0
      src/components/file/FileUpload.vue
  9. 103 0
      src/components/file/ImgUpload.vue
  10. 12 3
      src/components/task/Task.vue
  11. 63 0
      src/components/text/ExpendText.vue
  12. 6 2
      src/config/index.js
  13. 49 0
      src/directive/clipboard/clipboard.js
  14. 13 0
      src/directive/clipboard/index.js
  15. 77 0
      src/directive/el-drag-dialog/drag.js
  16. 13 0
      src/directive/el-drag-dialog/index.js
  17. 41 0
      src/directive/el-table/adaptive.js
  18. 13 0
      src/directive/el-table/index.js
  19. 13 0
      src/directive/permission/index.js
  20. 31 0
      src/directive/permission/permission.js
  21. 91 0
      src/directive/sticky.js
  22. 13 0
      src/directive/waves/index.js
  23. 26 0
      src/directive/waves/waves.css
  24. 72 0
      src/directive/waves/waves.js
  25. 42 24
      src/js/api.js
  26. 5 0
      src/js/file.js
  27. 4 7
      src/js/http.js
  28. 101 0
      src/pages/TestCase/components/defect_detail.vue
  29. 198 0
      src/pages/TestCase/components/defect_form.vue
  30. 179 0
      src/pages/TestCase/components/testcase_detail.vue
  31. 242 0
      src/pages/TestCase/components/testcase_form.vue
  32. 539 0
      src/pages/TestCase/exam_testcases.vue
  33. 685 0
      src/pages/TestCase/testcases.vue
  34. 73 0
      src/pages/TestCase/utils/index.js
  35. 53 0
      src/pages/login/login.vue
  36. 97 84
      src/router/index.js
  37. 117 0
      src/utils/index.js
  38. 58 0
      src/utils/scroll-to.js

+ 1 - 1
config/dev.env.js

@@ -14,7 +14,7 @@ const prodEnv = require('./prod.env')
 module.exports = {
   NODE_ENV: '"development"',
   ENV_CONFIG: "'dev'",
-  API_ROOT: '"//127.0.0.1"',
+  API_ROOT: '"http://127.0.0.1:5757"',
   LOGIN_URL: '"http://127.0.0.1:8081/page/login?redirect=http%3a%2f%2fcrowd.dev.mooctest.net%2f%23%2fhome"',
   REGISTER_URL: '"http://127.0.0.1:8081/page/register"'
 }

+ 1 - 1
config/proxyConfig.js

@@ -1,6 +1,6 @@
 module.exports = {
   '/api': {
-    target: 'http://localhost:8080/', // 设置你调用的接口域名和端口号
+    target: 'http://localhost:8080/api/', // 设置你调用的接口域名和端口号
     changeOrigin: true,     // 跨域
     pathRewrite: {
       '^/api': '/'

File diff suppressed because it is too large
+ 17589 - 1
package-lock.json


+ 1 - 0
package.json

@@ -21,6 +21,7 @@
     "mockjs": "^1.1.0",
     "moment": "^2.29.1",
     "querystring": "^0.2.0",
+    "sortablejs": "^1.14.0",
     "v-region": "^2.2.2",
     "vue": "^2.6.12",
     "vue-router": "^3.4.9",

+ 8 - 13
src/App.vue

@@ -15,19 +15,16 @@
 </template>
 
 <script>
-import Http from '@/js/http.js'
 import HeaderContainer from '@/components/commons/Header2.0'
 import FooterContainer from '@/components/commons/Footer2.0'
 import HomeSlice from '@/components/commons/HomeSlice'
 import HomeSliceOnline from '@/components/commons/HomeSliceOnline'
-import {getCurrentUser, storageGet, storageSave} from '@/js/index'
-import {setConfig} from '../src/config/index'
-import {slice_type} from "../tool4deploy/slider-type";
+import {slice_type} from '../tool4deploy/slider-type'
 
 export default {
   name: 'App',
-  components: {HeaderContainer, FooterContainer, HomeSlice ,HomeSliceOnline},
-  data(){
+  components: {HeaderContainer, FooterContainer, HomeSlice, HomeSliceOnline},
+  data () {
     return {
       // showSlice:false
       slice_type
@@ -46,7 +43,6 @@ export default {
       //   this.setCurrUserByHttp()
       // })
 
-
       // if (storageGet('user') == null) {
       //   storageSave('rolesPermissions', {
       //     'isRegionManager': false,
@@ -73,14 +69,13 @@ export default {
       //   this.fullScreenLoading = false
       //   this.isLogin = true
       // }
-    },
-  },
-  computed:{
-    showSlice(){
-      if(this.$route.path==='/home' || this.$route.path==='/')
-        return  true;
     }
   },
+  computed: {
+    showSlice () {
+      if (this.$route.path === '/home' || this.$route.path === '/') { return true }
+    }
+  }
 }
 </script>
 

+ 65 - 0
src/components/DragSelect/index.vue

@@ -0,0 +1,65 @@
+<template>
+  <el-select ref="dragSelect" v-model="selectVal" v-bind="$attrs" class="drag-select" multiple v-on="$listeners">
+    <slot />
+  </el-select>
+</template>
+
+<script>
+import Sortable from 'sortablejs'
+
+export default {
+  name: 'DragSelect',
+  props: {
+    value: {
+      type: Array,
+      required: true
+    }
+  },
+  computed: {
+    selectVal: {
+      get() {
+        return [...this.value]
+      },
+      set(val) {
+        this.$emit('input', [...val])
+      }
+    }
+  },
+  mounted() {
+    this.setSort()
+  },
+  methods: {
+    setSort() {
+      const el = this.$refs.dragSelect.$el.querySelectorAll('.el-select__tags > span')[0]
+      this.sortable = Sortable.create(el, {
+        ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
+        setData: function(dataTransfer) {
+          dataTransfer.setData('Text', '')
+          // to avoid Firefox bug
+          // Detail see : https://github.com/RubaXa/Sortable/issues/1012
+        },
+        onEnd: evt => {
+          const targetRow = this.value.splice(evt.oldIndex, 1)[0]
+          this.value.splice(evt.newIndex, 0, targetRow)
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.drag-select {
+  ::v-deep {
+    .sortable-ghost {
+      opacity: .8;
+      color: #fff !important;
+      background: #42b983 !important;
+    }
+
+    .el-tag {
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 101 - 0
src/components/Pagination/index.vue

@@ -0,0 +1,101 @@
+<template>
+  <div :class="{'hidden':hidden}" class="pagination-container">
+    <el-pagination
+      :background="background"
+      :current-page.sync="currentPage"
+      :page-size.sync="pageSize"
+      :layout="layout"
+      :page-sizes="pageSizes"
+      :total="total"
+      v-bind="$attrs"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+    />
+  </div>
+</template>
+
+<script>
+import { scrollTo } from '@/utils/scroll-to'
+
+export default {
+  name: 'Pagination',
+  props: {
+    total: {
+      required: true,
+      type: Number
+    },
+    page: {
+      type: Number,
+      default: 1
+    },
+    limit: {
+      type: Number,
+      default: 20
+    },
+    pageSizes: {
+      type: Array,
+      default() {
+        return [10, 20, 30, 50]
+      }
+    },
+    layout: {
+      type: String,
+      default: 'total, sizes, prev, pager, next, jumper'
+    },
+    background: {
+      type: Boolean,
+      default: true
+    },
+    autoScroll: {
+      type: Boolean,
+      default: true
+    },
+    hidden: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    currentPage: {
+      get() {
+        return this.page
+      },
+      set(val) {
+        this.$emit('update:page', val)
+      }
+    },
+    pageSize: {
+      get() {
+        return this.limit
+      },
+      set(val) {
+        this.$emit('update:limit', val)
+      }
+    }
+  },
+  methods: {
+    handleSizeChange(val) {
+      this.$emit('pagination', { page: this.currentPage, limit: val })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    },
+    handleCurrentChange(val) {
+      this.$emit('pagination', { page: val, limit: this.pageSize })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+  background: #fff;
+  padding: 32px 16px;
+}
+.pagination-container.hidden {
+  display: none;
+}
+</style>

+ 111 - 0
src/components/file/FileUpload.vue

@@ -0,0 +1,111 @@
+<template>
+  <el-upload
+    drag
+    class="upload-file"
+    :action="uploadUrl"
+    :on-success="handleUploadSuccess"
+    multiple
+    :limit="countLimit"
+    :on-exceed="handleExceed"
+    :before-upload="beforeFileUpload"
+    :on-remove="handleFileRemove"
+    :file-list="fileList"
+    :on-error="imgUploadError"
+    :disabled="disabled"
+  >
+    <i class="el-icon-upload"></i>
+    <div class="el-upload__text">
+      将文件拖到此处,或
+      <em>点击上传</em>
+    </div>
+  </el-upload>
+</template>
+
+<script>
+import Apis from '@/js/api.js'
+
+export default {
+  name: 'FileUpload',
+
+  data: function () {
+    return {
+      fileList: [],
+      uploadUrl: process.env.API_ROOT + Apis.FILE.UPLOAD_TEST_CASE_FILE
+    }
+  },
+  props: {
+    countLimit: {
+      type: Number,
+      default: 1
+    },
+    files: Array,
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  watch: {
+    files: {
+      immediate: true,
+      handler: function () {
+        if (this.files.length !== this.fileList.length) {
+          this.fileList = []
+          this.files.map(file => {
+            let fileName = file.substring(file.lastIndexOf('/') + 1)
+            fileName = fileName.substring(0, fileName.indexOf('_')) + fileName.substring(fileName.lastIndexOf('.'))
+            this.fileList.push({ name: fileName, url: file })
+          })
+        }
+      }
+    }
+  },
+  methods: {
+    handleExceed (files, fileList) {
+      this.$message.warning(
+        `当前限制选择 ${this.countLimit} 个文件,本次选择了 ${
+          files.length
+        } 个文件,共选择了 ${files.length + fileList.length} 个文件`
+      )
+    },
+    beforeFileUpload (file) {
+      console.log(file)
+      const isPDF = file.type === 'application/pdf'
+      const isDOC = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
+      const isEXCEL = file.type === 'application/vnd.ms-excel'
+      const isXLS = file.type === 'application/x-xls'
+      const isTXT = file.type === 'text/plain'
+      const isXLSX = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+      if (!(isDOC || isEXCEL || isPDF || isTXT || isXLS || isXLSX)) {
+        this.$message.error('上传文件只能是 PDF 、 DOC 、DOCX 、XLS、TXT、XLSX 格式!')
+      }
+      return isDOC || isEXCEL || isPDF || isTXT || isXLS || isXLSX
+    },
+    handleUploadSuccess (response, file, fileList) {
+      // 这两个的顺序不能换
+      this.fileList.push(file)
+      this.files.push(response)
+    },
+    imgUploadError (err, file, fileList) { // 图片上传失败调用
+      console.log(err)
+      this.$message.error('上传图片失败!')
+    },
+    handleFileRemove (file) {
+      console.log(this.fileList)
+      for (const i in this.fileList) {
+        if (this.fileList[i].uid === file.uid) {
+          // 这两个的顺序不能换
+          this.fileList.splice(i, 1)
+          this.files.splice(i, 1)
+        }
+      }
+      console.log(this.files)
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .upload-file {
+    width: 400px;
+  }
+</style>

+ 103 - 0
src/components/file/ImgUpload.vue

@@ -0,0 +1,103 @@
+<template>
+  <el-upload
+    list-type="picture-card"
+    accept="image/*"
+    :action="uploadUrl"
+    :on-success="handleUploadSuccess"
+    multiple
+    :limit="countLimit"
+    :on-exceed="handleExceed"
+    :before-upload="beforeFileUpload"
+    :on-remove="handleFileRemove"
+    :file-list="fileList"
+    :on-error="imgUploadError"
+    :disabled="disabled"
+  >
+    <i slot="default" class="el-icon-plus"></i>
+  </el-upload>
+</template>
+
+<script>
+import Apis from '@/js/api.js'
+
+export default {
+  name: 'ImgUpload',
+  data: function () {
+    return {
+      fileList: [],
+      uploadUrl: process.env.API_ROOT + Apis.FILE.UPLOAD_TEST_CASE_IMAGE
+    }
+  },
+  props: {
+    countLimit: {
+      type: Number,
+      default: 1
+    },
+    files: Array,
+    disabled: {
+      type: Boolean,
+      default: false
+    }
+  },
+  watch: {
+    files: {
+      immediate: true,
+      handler: function () {
+        if (this.files.length !== this.fileList.length) {
+          this.fileList = []
+          this.files.map(file => {
+            this.fileList.push({ url: file })
+          })
+        }
+      }
+    }
+  },
+  methods: {
+    handleExceed (files, fileList) {
+      this.$message.warning(
+        `当前限制选择 ${this.countLimit} 个文件,本次选择了 ${
+          files.length
+        } 个文件,共选择了 ${files.length + fileList.length} 个文件`
+      )
+    },
+    beforeFileUpload (file) {
+      console.log(file)
+      const isJPG = file.type === 'image/jpeg'
+      const isPNG = file.type === 'image/png'
+      const isLt2M = file.size / 1024 / 1024 < 2
+      if (!isJPG && !isPNG) {
+        this.$message.error('上传头像图片只能是 JPG 和 PNG 格式!')
+      }
+      if (!isLt2M) {
+        this.$message.error('上传图片大小不能超过 2MB!')
+      }
+      return (isJPG || isPNG) && isLt2M
+    },
+    handleUploadSuccess (response, file, fileList) {
+      // 这两个的顺序不能换
+      this.fileList.push(file)
+      this.files.push(response)
+    },
+    // handleAvatarSuccess (res, file) { // 图片上传成功
+    //   console.log(res)
+    //   console.log(file)
+    //   this.imageUrl = URL.createObjectURL(file.raw)
+    // },
+    imgUploadError (err, file, fileList) { // 图片上传失败调用
+      console.log(err)
+      this.$message.error('上传图片失败!')
+    },
+    handleFileRemove (file) {
+      console.log(this.fileList)
+      for (const i in this.fileList) {
+        if (this.fileList[i].uid === file.uid) {
+          // 这两个的顺序不能换
+          this.fileList.splice(i, 1)
+          this.files.splice(i, 1)
+        }
+      }
+      console.log(this.files)
+    }
+  }
+}
+</script>

+ 12 - 3
src/components/task/Task.vue

@@ -210,8 +210,8 @@
               <el-button v-if="taskOperationControl.taskRecommend" type="primary" size="mini" @click="recommendTask()">任务推荐
               </el-button>
 
-              <el-button v-if="taskOperationControl.uploadReport" type="primary" size="mini" @click="toCreateReport()">
-                上传报告
+              <el-button v-if="taskOperationControl.uploadReport" type="primary" size="mini" @click="toTestCases()">
+                用例管理
               </el-button>
               <el-button v-if="taskOperationControl.taskDemonstrate" type="success" size="mini" @click="gotoDataboard()">
                 任务面板
@@ -930,8 +930,17 @@ export default {
     },
     reformDate(date) {
       return getFormalTimeFromDate(date)
+    },
+    // 跳转到测试用例管理页面
+    toTestCases () {
+      this.$router.push({
+        name: 'TestCases',
+        params: {
+          taskCode: this.taskId
+        }
+      })
     }
-  },
+  }
 }
 //回收站
 //

+ 63 - 0
src/components/text/ExpendText.vue

@@ -0,0 +1,63 @@
+<template>
+  <div>
+    <span>{{isExpend ? text : capitalize(text)}}</span>
+    <span @click="expendClick" class="expend" v-if="showExpend">
+      {{isExpend ? '收起' : '展开'}}
+      <i :class="isExpend ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"/>
+    </span>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ExpendText',
+  data: function () {
+    return {
+      isExpend: false
+    }
+  },
+  props: {
+    length: {
+      type: Number,
+      default: 100
+    },
+    text: {
+      type: String,
+      required: true
+    }
+  },
+  computed: {
+    showExpend: function () {
+      return this.text && this.text.length > this.length
+    }
+  },
+  watch: {
+    'text': function () {
+      this.isExpend = false
+    }
+  },
+  methods: {
+    capitalize (value) {
+      if (!value) return ''
+      value = value.toString()
+      if (value.length > 100) {
+        return value.substr(0, 100)
+      } else {
+        return value
+      }
+    },
+    expendClick () {
+      this.isExpend = !this.isExpend
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .expend {
+    color: coral
+  }
+  .expend:hover {
+    cursor: pointer;
+  }
+</style>

+ 6 - 2
src/config/index.js

@@ -134,9 +134,13 @@ export const setConfig = (conf) => {
   CONFIG = conf;
 }
 
+//本地开发使用
+export const login_url = '#/login'
+export const register_url = ''
+
 // 开发使用
-export const login_url = 'http://crowd.dev.mooctest.net/page/login?redirect=http%3a%2f%2fcrowd.dev.mooctest.net%2f%23%2fhome'
-export const register_url = 'http://crowd.dev.mooctest.net/page/register'
+// export const login_url = 'http://crowd.dev.mooctest.net:8281/page/login?redirect=http%3a%2f%2fdev.mooctest.net%3A5757%2f%23%2fhome'
+// export const register_url = 'http://crowd.dev.mooctest.net/page/register'
 
 
 // 线上使用

+ 49 - 0
src/directive/clipboard/clipboard.js

@@ -0,0 +1,49 @@
+// Inspired by https://github.com/Inndy/vue-clipboard2
+const Clipboard = require('clipboard')
+if (!Clipboard) {
+  throw new Error('you should npm install `clipboard` --save at first ')
+}
+
+export default {
+  bind(el, binding) {
+    if (binding.arg === 'success') {
+      el._v_clipboard_success = binding.value
+    } else if (binding.arg === 'error') {
+      el._v_clipboard_error = binding.value
+    } else {
+      const clipboard = new Clipboard(el, {
+        text() { return binding.value },
+        action() { return binding.arg === 'cut' ? 'cut' : 'copy' }
+      })
+      clipboard.on('success', e => {
+        const callback = el._v_clipboard_success
+        callback && callback(e) // eslint-disable-line
+      })
+      clipboard.on('error', e => {
+        const callback = el._v_clipboard_error
+        callback && callback(e) // eslint-disable-line
+      })
+      el._v_clipboard = clipboard
+    }
+  },
+  update(el, binding) {
+    if (binding.arg === 'success') {
+      el._v_clipboard_success = binding.value
+    } else if (binding.arg === 'error') {
+      el._v_clipboard_error = binding.value
+    } else {
+      el._v_clipboard.text = function() { return binding.value }
+      el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' }
+    }
+  },
+  unbind(el, binding) {
+    if (binding.arg === 'success') {
+      delete el._v_clipboard_success
+    } else if (binding.arg === 'error') {
+      delete el._v_clipboard_error
+    } else {
+      el._v_clipboard.destroy()
+      delete el._v_clipboard
+    }
+  }
+}

+ 13 - 0
src/directive/clipboard/index.js

@@ -0,0 +1,13 @@
+import Clipboard from './clipboard'
+
+const install = function(Vue) {
+  Vue.directive('Clipboard', Clipboard)
+}
+
+if (window.Vue) {
+  window.clipboard = Clipboard
+  Vue.use(install); // eslint-disable-line
+}
+
+Clipboard.install = install
+export default Clipboard

+ 77 - 0
src/directive/el-drag-dialog/drag.js

@@ -0,0 +1,77 @@
+export default {
+  bind(el, binding, vnode) {
+    const dialogHeaderEl = el.querySelector('.el-dialog__header')
+    const dragDom = el.querySelector('.el-dialog')
+    dialogHeaderEl.style.cssText += ';cursor:move;'
+    dragDom.style.cssText += ';top:0px;'
+
+    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
+    const getStyle = (function() {
+      if (window.document.currentStyle) {
+        return (dom, attr) => dom.currentStyle[attr]
+      } else {
+        return (dom, attr) => getComputedStyle(dom, false)[attr]
+      }
+    })()
+
+    dialogHeaderEl.onmousedown = (e) => {
+      // 鼠标按下,计算当前元素距离可视区的距离
+      const disX = e.clientX - dialogHeaderEl.offsetLeft
+      const disY = e.clientY - dialogHeaderEl.offsetTop
+
+      const dragDomWidth = dragDom.offsetWidth
+      const dragDomHeight = dragDom.offsetHeight
+
+      const screenWidth = document.body.clientWidth
+      const screenHeight = document.body.clientHeight
+
+      const minDragDomLeft = dragDom.offsetLeft
+      const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth
+
+      const minDragDomTop = dragDom.offsetTop
+      const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight
+
+      // 获取到的值带px 正则匹配替换
+      let styL = getStyle(dragDom, 'left')
+      let styT = getStyle(dragDom, 'top')
+
+      if (styL.includes('%')) {
+        styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100)
+        styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100)
+      } else {
+        styL = +styL.replace(/\px/g, '')
+        styT = +styT.replace(/\px/g, '')
+      }
+
+      document.onmousemove = function(e) {
+        // 通过事件委托,计算移动的距离
+        let left = e.clientX - disX
+        let top = e.clientY - disY
+
+        // 边界处理
+        if (-(left) > minDragDomLeft) {
+          left = -minDragDomLeft
+        } else if (left > maxDragDomLeft) {
+          left = maxDragDomLeft
+        }
+
+        if (-(top) > minDragDomTop) {
+          top = -minDragDomTop
+        } else if (top > maxDragDomTop) {
+          top = maxDragDomTop
+        }
+
+        // 移动当前元素
+        dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`
+
+        // emit onDrag event
+        vnode.child.$emit('dragDialog')
+      }
+
+      document.onmouseup = function(e) {
+        document.onmousemove = null
+        document.onmouseup = null
+      }
+    }
+  }
+}

+ 13 - 0
src/directive/el-drag-dialog/index.js

@@ -0,0 +1,13 @@
+import drag from './drag'
+
+const install = function(Vue) {
+  Vue.directive('el-drag-dialog', drag)
+}
+
+if (window.Vue) {
+  window['el-drag-dialog'] = drag
+  Vue.use(install); // eslint-disable-line
+}
+
+drag.install = install
+export default drag

+ 41 - 0
src/directive/el-table/adaptive.js

@@ -0,0 +1,41 @@
+import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'
+
+/**
+ * How to use
+ * <el-table height="100px" v-el-height-adaptive-table="{bottomOffset: 30}">...</el-table>
+ * el-table height is must be set
+ * bottomOffset: 30(default)   // The height of the table from the bottom of the page.
+ */
+
+const doResize = (el, binding, vnode) => {
+  const { componentInstance: $table } = vnode
+
+  const { value } = binding
+
+  if (!$table.height) {
+    throw new Error(`el-$table must set the height. Such as height='100px'`)
+  }
+  const bottomOffset = (value && value.bottomOffset) || 30
+
+  if (!$table) return
+
+  const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset
+  $table.layout.setHeight(height)
+  $table.doLayout()
+}
+
+export default {
+  bind(el, binding, vnode) {
+    el.resizeListener = () => {
+      doResize(el, binding, vnode)
+    }
+    // parameter 1 is must be "Element" type
+    addResizeListener(window.document.body, el.resizeListener)
+  },
+  inserted(el, binding, vnode) {
+    doResize(el, binding, vnode)
+  },
+  unbind(el) {
+    removeResizeListener(window.document.body, el.resizeListener)
+  }
+}

+ 13 - 0
src/directive/el-table/index.js

@@ -0,0 +1,13 @@
+import adaptive from './adaptive'
+
+const install = function(Vue) {
+  Vue.directive('el-height-adaptive-table', adaptive)
+}
+
+if (window.Vue) {
+  window['el-height-adaptive-table'] = adaptive
+  Vue.use(install); // eslint-disable-line
+}
+
+adaptive.install = install
+export default adaptive

+ 13 - 0
src/directive/permission/index.js

@@ -0,0 +1,13 @@
+import permission from './permission'
+
+const install = function(Vue) {
+  Vue.directive('permission', permission)
+}
+
+if (window.Vue) {
+  window['permission'] = permission
+  Vue.use(install); // eslint-disable-line
+}
+
+permission.install = install
+export default permission

+ 31 - 0
src/directive/permission/permission.js

@@ -0,0 +1,31 @@
+import store from '@/store'
+
+function checkPermission(el, binding) {
+  const { value } = binding
+  const roles = store.getters && store.getters.roles
+
+  if (value && value instanceof Array) {
+    if (value.length > 0) {
+      const permissionRoles = value
+
+      const hasPermission = roles.some(role => {
+        return permissionRoles.includes(role)
+      })
+
+      if (!hasPermission) {
+        el.parentNode && el.parentNode.removeChild(el)
+      }
+    }
+  } else {
+    throw new Error(`need roles! Like v-permission="['admin','editor']"`)
+  }
+}
+
+export default {
+  inserted(el, binding) {
+    checkPermission(el, binding)
+  },
+  update(el, binding) {
+    checkPermission(el, binding)
+  }
+}

+ 91 - 0
src/directive/sticky.js

@@ -0,0 +1,91 @@
+const vueSticky = {}
+let listenAction
+vueSticky.install = Vue => {
+  Vue.directive('sticky', {
+    inserted(el, binding) {
+      const params = binding.value || {}
+      const stickyTop = params.stickyTop || 0
+      const zIndex = params.zIndex || 1000
+      const elStyle = el.style
+
+      elStyle.position = '-webkit-sticky'
+      elStyle.position = 'sticky'
+      // if the browser support css sticky(Currently Safari, Firefox and Chrome Canary)
+      // if (~elStyle.position.indexOf('sticky')) {
+      //     elStyle.top = `${stickyTop}px`;
+      //     elStyle.zIndex = zIndex;
+      //     return
+      // }
+      const elHeight = el.getBoundingClientRect().height
+      const elWidth = el.getBoundingClientRect().width
+      elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}`
+
+      const parentElm = el.parentNode || document.documentElement
+      const placeholder = document.createElement('div')
+      placeholder.style.display = 'none'
+      placeholder.style.width = `${elWidth}px`
+      placeholder.style.height = `${elHeight}px`
+      parentElm.insertBefore(placeholder, el)
+
+      let active = false
+
+      const getScroll = (target, top) => {
+        const prop = top ? 'pageYOffset' : 'pageXOffset'
+        const method = top ? 'scrollTop' : 'scrollLeft'
+        let ret = target[prop]
+        if (typeof ret !== 'number') {
+          ret = window.document.documentElement[method]
+        }
+        return ret
+      }
+
+      const sticky = () => {
+        if (active) {
+          return
+        }
+        if (!elStyle.height) {
+          elStyle.height = `${el.offsetHeight}px`
+        }
+
+        elStyle.position = 'fixed'
+        elStyle.width = `${elWidth}px`
+        placeholder.style.display = 'inline-block'
+        active = true
+      }
+
+      const reset = () => {
+        if (!active) {
+          return
+        }
+
+        elStyle.position = ''
+        placeholder.style.display = 'none'
+        active = false
+      }
+
+      const check = () => {
+        const scrollTop = getScroll(window, true)
+        const offsetTop = el.getBoundingClientRect().top
+        if (offsetTop < stickyTop) {
+          sticky()
+        } else {
+          if (scrollTop < elHeight + stickyTop) {
+            reset()
+          }
+        }
+      }
+      listenAction = () => {
+        check()
+      }
+
+      window.addEventListener('scroll', listenAction)
+    },
+
+    unbind() {
+      window.removeEventListener('scroll', listenAction)
+    }
+  })
+}
+
+export default vueSticky
+

+ 13 - 0
src/directive/waves/index.js

@@ -0,0 +1,13 @@
+import waves from './waves'
+
+const install = function(Vue) {
+  Vue.directive('waves', waves)
+}
+
+if (window.Vue) {
+  window.waves = waves
+  Vue.use(install); // eslint-disable-line
+}
+
+waves.install = install
+export default waves

+ 26 - 0
src/directive/waves/waves.css

@@ -0,0 +1,26 @@
+.waves-ripple {
+    position: absolute;
+    border-radius: 100%;
+    background-color: rgba(0, 0, 0, 0.15);
+    background-clip: padding-box;
+    pointer-events: none;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+    -webkit-transform: scale(0);
+    -ms-transform: scale(0);
+    transform: scale(0);
+    opacity: 1;
+}
+
+.waves-ripple.z-active {
+    opacity: 0;
+    -webkit-transform: scale(2);
+    -ms-transform: scale(2);
+    transform: scale(2);
+    -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, transform 0.6s ease-out;
+    transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
+}

+ 72 - 0
src/directive/waves/waves.js

@@ -0,0 +1,72 @@
+import './waves.css'
+
+const context = '@@wavesContext'
+
+function handleClick(el, binding) {
+  function handle(e) {
+    const customOpts = Object.assign({}, binding.value)
+    const opts = Object.assign({
+      ele: el, // 波纹作用元素
+      type: 'hit', // hit 点击位置扩散 center中心点扩展
+      color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
+    },
+    customOpts
+    )
+    const target = opts.ele
+    if (target) {
+      target.style.position = 'relative'
+      target.style.overflow = 'hidden'
+      const rect = target.getBoundingClientRect()
+      let ripple = target.querySelector('.waves-ripple')
+      if (!ripple) {
+        ripple = document.createElement('span')
+        ripple.className = 'waves-ripple'
+        ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
+        target.appendChild(ripple)
+      } else {
+        ripple.className = 'waves-ripple'
+      }
+      switch (opts.type) {
+        case 'center':
+          ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
+          ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
+          break
+        default:
+          ripple.style.top =
+            (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
+              document.body.scrollTop) + 'px'
+          ripple.style.left =
+            (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
+              document.body.scrollLeft) + 'px'
+      }
+      ripple.style.backgroundColor = opts.color
+      ripple.className = 'waves-ripple z-active'
+      return false
+    }
+  }
+
+  if (!el[context]) {
+    el[context] = {
+      removeHandle: handle
+    }
+  } else {
+    el[context].removeHandle = handle
+  }
+
+  return handle
+}
+
+export default {
+  bind(el, binding) {
+    el.addEventListener('click', handleClick(el, binding), false)
+  },
+  update(el, binding) {
+    el.removeEventListener('click', el[context].removeHandle, false)
+    el.addEventListener('click', handleClick(el, binding), false)
+  },
+  unbind(el) {
+    el.removeEventListener('click', el[context].removeHandle, false)
+    el[context] = null
+    delete el[context]
+  }
+}

+ 42 - 24
src/js/api.js

@@ -13,6 +13,7 @@ export default {
     END_PROJECT: '/api/project/{projectId}/status/finished',
     MORE_HOT_PROJECT: '/api/square/hotProject/list',
     CROWD_PROJECT: '/api/common/index/crowd/project/{code}',
+    GET_SIMPLE_DATAS: '/api/simpleprojectdatas',
   },
   TASK: {
     GET_TASK: '/api/project/{projectId}/task/{taskId}/',
@@ -24,8 +25,12 @@ export default {
     SUBMIT_TASK: '/api/project/{projectId}/task/{taskId}/status/commit',
     END_TASK: '/api/project/{projectId}/task/{taskId}/status/finished',
     MORE_HOT_TASK: '/api/square/hotTasks/list',
-    GET_TASK_CLOUD:'/api/project/{projectId}/task/{taskId}/word',
-    GET_TOKEN:'/api/project/{projectCode}/task/{taskCode}/token'
+    GET_TASK_CLOUD: '/api/project/{projectId}/task/{taskId}/word',
+    GET_TOKEN: '/api/project/{projectCode}/task/{taskCode}/token',
+    GET_SIMPLE_USER_TASK_DATAS: '/api/simpleusertaskdatas',
+    GET_SIMPLE_DATAS_BY_PROJECT: '/api/simpletaskdatas/{projectCode}',
+    GET_TASK_USER_DATAS: '/api/project/{projectCode}/task/{taskCode}/taskusers',
+    COMMIT_TASK: '/api/task/{taskCode}/commit'
   },
   REPORT: {
     GET_TASK_REPORT: '/api/project/{projectId}/task/{taskId}/report/{reportId}/',
@@ -38,11 +43,13 @@ export default {
     DELETE_PROJECT_REPORT: ''
   },
   FILE: {
-    REQUIREMENT_FILE: '/api/files/requirementfile/{userId}/',
-    APK: '/api/files/apk/{userId}/',
-    UPLOAD_REPORT_FILE: '/api/files/report/{userId}/',
+    REQUIREMENT_FILE: '/api/files/requirementfile/0/',
+    APK: '/api/files/apk/0/',
+    UPLOAD_REPORT_FILE: '/api/files/report/0/',
     UPLOAD_EXCEL: '',
-    UPLOAD_IMAGE: '/api/files/image/{userId}/',
+    UPLOAD_IMAGE: '/api/files/image/0/',
+    UPLOAD_TEST_CASE_IMAGE: '/api/files/testcaseimage/',
+    UPLOAD_TEST_CASE_FILE: '/api/files/testcasefile/',
     GET_TEMPLATE_EXCEL_FILE: ''
   },
   USER: {
@@ -57,23 +64,23 @@ export default {
     UPDATE_INDIVIDUAL_AUTHENTICATION_INFO: '/api/user/{userId}/personalAuth',
     UPDATE_ENTERPRISE_AUTHENTICATION_INFO: '/api/user/{userId}/enterpriseAuth',
     UPDATE_AGENCY_AUTHENTICATION_INFO: '/api/user/{userId}/agency/',
-    UPDATE_AGENCY_RESOURCE_AND_ABILITY:'/api/user/{userId}/agency/resource',
+    UPDATE_AGENCY_RESOURCE_AND_ABILITY: '/api/user/{userId}/agency/resource',
     GET_INDIVIDUAL_AUTHENTICATION_INFO: '/api/user/{userId}/personalAuth',
     GET_ENTERPRISE_AUTHENTICATION_INFO: '/api/user/{userId}/enterpriseAuth',
     GET_AGENCY_AUTHENTICATION_INFO: '/api/user/{userId}/agency',
     GET_AGENCY_AUTHENTICATION_INFO_COMMON: '/api/user/{userId}/agency/common',
     GET_ALL_HANDLING_AUTH_INFO: '/api/user/authentication/handling',
     GET_ALL_HANDLED_AUTH_INFO: '/api/user/authentication/handled',
-    PASS_AGENCY_AUTH:'/api/user/{userId}/agency/status/accept',
-    PASS_ENTERPRISE_AUTH:'/api/user/{userId}/enterpriseAuth/status/accept',
-    PASS_INDIVIDUAL_AUTH:'/api/user/{userId}/personalAuth/status/accept',
-    REJECT_AGENCY_AUTH:'/api/user/{userId}/agency/status/reject',
-    REJECT_ENTERPRISE_AUTH:'/api/user/{userId}/enterpriseAuth/status/reject',
-    REJECT_INDIVIDUAL_AUTH:'/api/user/{userId}/personalAuth/status/reject',
-    GET_DETAIL: "/api/user/detail/{userId}",
-    GET_ADDRESS: "/api/index/address",
-    IS_PART: "/api/common/check/create/project/{userId}",
-    IS_AGENCY: "/api/common/check/accept/task/{userId}",
+    PASS_AGENCY_AUTH: '/api/user/{userId}/agency/status/accept',
+    PASS_ENTERPRISE_AUTH: '/api/user/{userId}/enterpriseAuth/status/accept',
+    PASS_INDIVIDUAL_AUTH: '/api/user/{userId}/personalAuth/status/accept',
+    REJECT_AGENCY_AUTH: '/api/user/{userId}/agency/status/reject',
+    REJECT_ENTERPRISE_AUTH: '/api/user/{userId}/enterpriseAuth/status/reject',
+    REJECT_INDIVIDUAL_AUTH: '/api/user/{userId}/personalAuth/status/reject',
+    GET_DETAIL: '/api/user/detail/{userId}',
+    GET_ADDRESS: '/api/index/address',
+    IS_PART: '/api/common/check/create/project/{userId}',
+    IS_AGENCY: '/api/common/check/accept/task/{userId}'
   },
   PAGE: {
     HOME_PAGE: '/api/common/index/',
@@ -81,25 +88,36 @@ export default {
     MY_CROWD_TEST_PAGE: '/api/common/mycrowd/{userId}',
     TASK_DETAIL_PAGE: '/api/page/taskDetail/{taskId}/',
     PROJECT_DETAIL_PAGE: '/api/project/{projectId}/',
-    REPORT_DETAIL_PAGE: '/api/page/reportDetail/{reportId}/',
+    REPORT_DETAIL_PAGE: '/api/page/reportDetail/{reportId}/'
   },
   AGENCY: {
-    GET_DETAIL: '/api/agency/{agencyId}',
+    GET_DETAIL: '/api/agency/{agencyId}'
   },
   RESOURCE: {
-    GET_DETAIL: '/api/common/index/resource/{code}',
+    GET_DETAIL: '/api/common/index/resource/{code}'
   },
   EXPERT: {
-    GET_DETAIL: '/api/common/index/expert/{id}',
+    GET_DETAIL: '/api/common/index/expert/{id}'
   },
   TECHNOLOGY: {
-    GET_MORE: '/api/technical/more',
+    GET_MORE: '/api/technical/more'
   },
   GENERAL: {
     GET_ALL_INSTITUTIONS: '/api/regionalManager',
     GET_ALL_AGENCIES: '/api/agency/list',
     GET_ALL_ApplicationType: '/api/list/application',
     GET_ALL_TestType: '/api/list/type',
-    GET_ALL_Filed: '/api/list/filed',
-  }
+    GET_ALL_Filed: '/api/list/filed'
+  },
+  TESTCASE: {
+    ADD: '/api/testcase/',
+    UPDATE: '/api/testcase/{id}/',
+    USER_TEST_CASES: '/api/testcase/designer/{taskCode}/{designerId}/{pageNo}/{pageSize}/',
+    DELETE: '/api/testcase/{id}/',
+    ADD_DEFECT: '/api/testcase/defect/',
+    UPDATE_DEFECT: '/api/testcase/defect/{id}/',
+    DELETE_DEFECT: '/api/testcase/defect/{id}/',
+    EXAM: '/api/testcase/exam/{id}/'
+  },
+  LOGIN: '/api/login2/'
 }

+ 5 - 0
src/js/file.js

@@ -0,0 +1,5 @@
+export function toFileName (url) {
+  let fileName = url.substring(url.lastIndexOf('/') + 1)
+  fileName = fileName.substring(0, fileName.indexOf('_')) + fileName.substring(fileName.lastIndexOf('.'))
+  return fileName
+}

+ 4 - 7
src/js/http.js

@@ -67,11 +67,9 @@ export default {
         .then(response => {
           resolve(response.data)
         }).catch(error => {
-        reject(error.response)
-      })
-
+          reject(error.response)
+        })
     })
-
   },
   put (url, data) {
     return new Promise((resolve, reject) => {
@@ -90,7 +88,7 @@ export default {
     return new Promise((resolve, reject) => {
       axios.delete(handleUrl(url), {data: handleParams(data)}).then(
         (result) => {
-          resolve(result)
+          resolve(result.data)
         }
       ).catch(
         (error) => {
@@ -111,6 +109,5 @@ export default {
         }
       )
     })
-  },
+  }
 }
-

+ 101 - 0
src/pages/TestCase/components/defect_detail.vue

@@ -0,0 +1,101 @@
+<template>
+  <el-form ref="defectDetail" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+    <el-form-item label="用例编号">
+      <span>{{defectData.testCaseCode}}</span>
+    </el-form-item>
+    <el-form-item label="缺陷编号">
+      <span>{{defectData.code}}</span>
+    </el-form-item>
+    <el-form-item label="缺陷描述">
+      <expend-text :text="defectData.descr"></expend-text>
+    </el-form-item>
+    <el-form-item label="严重等级">
+      <span>{{toSeriousnessCn(defectData.seriousness)}}</span>
+    </el-form-item>
+    <el-form-item label="优先级">
+      <span>{{toPriorityCn(defectData.priority)}}</span>
+    </el-form-item>
+    <el-form-item label="缺陷类型">
+      <span>{{toDefectTypeCn(defectData.defectType)}}</span>
+    </el-form-item>
+    <el-form-item label="前置条件">
+      <expend-text :text="defectData.preconditions"></expend-text>
+    </el-form-item>
+    <el-form-item label="环境配置">
+      <expend-text :text="defectData.envConfig"></expend-text>
+    </el-form-item>
+    <el-form-item label="操作步骤">
+      <expend-text :text="defectData.opeSteps"></expend-text>
+    </el-form-item>
+    <el-form-item label="输入数据">
+      <expend-text :text="defectData.inputDatas"></expend-text>
+    </el-form-item>
+    <el-form-item label="预期结果">
+      <expend-text :text="defectData.expectedResult"></expend-text>
+    </el-form-item>
+    <el-form-item label="测试结果">
+      <expend-text :text="defectData.testResult"></expend-text>
+    </el-form-item>
+    <el-form-item label="其他说明">
+      <expend-text :text="defectData.others"></expend-text>
+    </el-form-item>
+    <el-form-item label="附件" prop="files">
+      <span v-if="defectData.files.length === 0">无</span>
+      <div v-for="file in defectData.files">
+        <el-link :key="file" :href="file" target="_blank">{{toFileName(file)}}</el-link>
+      </div>
+    </el-form-item>
+    <el-form-item label="截图" prop="screenshots">
+      <span v-if="defectData.screenshots.length === 0">无</span>
+      <el-image v-for="screenshot in defectData.screenshots" :key="screenshot"
+        style="width: 130px; height: 130px; margin-left: 10px;"
+        :src="screenshot"
+        :preview-src-list="[screenshot]">
+      </el-image>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script>
+import ExpendText from '@/components/text/ExpendText'
+import TestCaseUtils from '../utils'
+import {toFileName} from '@/js/file'
+
+export default {
+  name: 'DefectDetail',
+  components: {ExpendText},
+  props: {
+    defectData: {
+      type: Object,
+      default: function () {
+        return {
+          id: undefined,
+          testCaseCode: '',
+          taskCode: '',
+          descr: '',
+          preconditions: '',
+          envConfig: '',
+          priority: undefined,
+          seriousness: undefined,
+          defectType: undefined,
+          opeSteps: '',
+          inputDatas: '',
+          expectedResult: '',
+          others: '',
+          testResult: '',
+          files: [],
+          screenshots: []
+        }
+      }
+    }
+  },
+  methods: {
+    ...TestCaseUtils,
+    toFileName
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 198 - 0
src/pages/TestCase/components/defect_form.vue

@@ -0,0 +1,198 @@
+<template>
+  <el-form ref="defectForm" :rules="rules" :model="defectData" v-loading="loading" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+    <el-form-item label="用例编号" prop="testCaseCode">
+      <el-input v-model="defectData.testCaseCode" disabled/>
+    </el-form-item>
+    <el-form-item label="描述" prop="descr">
+      <el-input type="textarea" v-model="defectData.descr"/>
+    </el-form-item>
+    <el-form-item label="严重等级" prop="seriousness">
+      <el-select v-model="defectData.seriousness">
+        <el-option value="VERY_HIGH" label="极高"/>
+        <el-option value="HIGH" label="高"/>
+        <el-option value="MID" label="中"/>
+        <el-option value="LOW" label="低"/>
+        <el-option value="VERY_LOW" label="极低"/>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="优先级" prop="priority">
+      <el-select v-model="defectData.priority">
+        <el-option value="HIGH" label="高"/>
+        <el-option value="MID" label="中"/>
+        <el-option value="LOW" label="低"/>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="缺陷类型" prop="defectType">
+      <el-select v-model="defectData.defectType">
+        <el-option value="FUNCTIONALITY" label="功能性"/>
+        <el-option value="COMPATIBILITY" label="兼容性"/>
+        <el-option value="INFORMATION_SECURITY" label="信息安全性"/>
+        <el-option value="RELIABILITY" label="可靠性"/>
+        <el-option value="USE" label="易用性"/>
+        <el-option value="PERFORMANCE" label="性能效率"/>
+        <el-option value="PORTABILITY" label="可移植性"/>
+        <el-option value="MAINTAINABILITY" label="维护性"/>
+        <el-option value="OTHER" label="其他"/>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="前置条件" prop="preconditions">
+      <el-input type="textarea" v-model="defectData.preconditions"/>
+    </el-form-item>
+    <el-form-item label="环境配置" prop="envConfig">
+      <el-input type="textarea" v-model="defectData.envConfig"/>
+    </el-form-item>
+    <el-form-item label="操作步骤" prop="opeSteps">
+      <el-input type="textarea" v-model="defectData.opeSteps"/>
+    </el-form-item>
+    <el-form-item label="输入数据" prop="inputDatas">
+      <el-input type="textarea" v-model="defectData.inputDatas"/>
+    </el-form-item>
+    <el-form-item label="预期结果" prop="expectedResult">
+      <el-input type="textarea" v-model="defectData.expectedResult"/>
+    </el-form-item>
+    <el-form-item label="测试结果" prop="testResult">
+      <el-input type="textarea" v-model="defectData.testResult"/>
+    </el-form-item>
+    <el-form-item label="其他说明" prop="others">
+      <el-input type="textarea" v-model="defectData.others"/>
+    </el-form-item>
+    <el-form-item label="附件" prop="files">
+      <file-upload :countLimit="countLimit" :files="defectData.files"></file-upload>
+    </el-form-item>
+    <el-form-item label="截图" prop="screenshots">
+      <img-upload :countLimit="countLimit" :files="defectData.screenshots"></img-upload>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script>
+import ElDragSelect from '@/components/DragSelect'
+import Api from '@/js/api'
+import Http from '@/js/http'
+import {notify} from '@/constants'
+import FileUpload from '@/components/file/FileUpload'
+import ImgUpload from '@/components/file/ImgUpload'
+
+export default {
+  name: 'DefectForm',
+  components: { ElDragSelect, FileUpload, ImgUpload },
+  data: function () {
+    return {
+      countLimit: 3,
+      rules: {
+        descr: [
+          {required: true, message: '描述不可为空', trigger: 'blur'}
+        ],
+        seriousness: [
+          {required: true, message: '严重等级不可为空'}
+        ],
+        priority: [
+          {required: true, message: '优先级不可为空'}
+        ],
+        defectType: [
+          {required: true, message: '缺陷类型不可为空'}
+        ],
+        testResult: [
+          {required: true, message: '测试结果不可为空'}
+        ]
+      },
+      loading: false
+    }
+  },
+  props: {
+    defectData: {
+      type: Object,
+      default: function () {
+        return {
+          id: undefined,
+          testCaseCode: '',
+          taskCode: '',
+          descr: '',
+          preconditions: '',
+          envConfig: '',
+          priority: undefined,
+          seriousness: undefined,
+          defectType: undefined,
+          opeSteps: '',
+          inputDatas: '',
+          expectedResult: '',
+          others: '',
+          testResult: '',
+          files: [],
+          screenshots: []
+        }
+      }
+    }
+  },
+  methods: {
+    submitForm (callback) {
+      this.$refs['defectForm'].validate(valid => {
+        if (valid) {
+          this.showLoading()
+          const newDefect = {
+            testCaseCode: this.defectData.testCaseCode,
+            taskCode: this.defectData.taskCode,
+            descr: this.defectData.descr,
+            preconditions: this.defectData.preconditions,
+            envConfig: this.defectData.envConfig,
+            priority: this.defectData.priority,
+            seriousness: this.defectData.seriousness,
+            defectType: this.defectData.defectType,
+            opeSteps: this.defectData.opeSteps,
+            inputDatas: this.defectData.inputDatas,
+            expectedResult: this.defectData.expectedResult,
+            others: this.defectData.others,
+            testResult: this.defectData.testResult,
+            files: this.defectData.files,
+            screenshots: this.defectData.screenshots
+          }
+          if (this.defectData.id && this.defectData.id > 0) {
+            newDefect['id'] = this.defectData.id
+          }
+          console.log(newDefect)
+          let url
+          let submitDataMethod
+          if (newDefect['id']) {
+            url = Api.TESTCASE.UPDATE_DEFECT.replace('{id}', newDefect['id'])
+            submitDataMethod = Http.put
+          } else {
+            url = Api.TESTCASE.ADD_DEFECT
+            submitDataMethod = Http.post
+          }
+          submitDataMethod(url, newDefect).then((res) => {
+            console.log(res)
+            this.hideLoading()
+            if (res.code !== 20000) {
+              notify('error', '提交缺陷失败:' + res.msg)
+            } else {
+              this.defectData.id = res.data
+              notify('success', '提交成功')
+              callback()
+            }
+          }).catch((error) => {
+            this.hideLoading()
+            notify('error', '缺陷创建失败:' + error)
+          })
+          // 提交 report
+        } else {
+          notify('error', '表单填写有误')
+          return false
+        }
+      })
+    },
+    showLoading () {
+      this.loading = true
+    },
+    hideLoading () {
+      this.loading = false
+    },
+    clearValidate (props = []) {
+      this.$refs['defectForm'].clearValidate()
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 179 - 0
src/pages/TestCase/components/testcase_detail.vue

@@ -0,0 +1,179 @@
+<template>
+  <el-form ref="testCaseDetail" :rules="rules" :model="testCaseData" v-loading="loading" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+    <el-form-item label="任务编号" prop="taskCode">
+      <span>{{testCaseData.taskCode}}</span>
+    </el-form-item>
+    <el-form-item label="用例编号" prop="code">
+      <span>{{testCaseData.code}}</span>
+    </el-form-item>
+    <el-form-item label="用例名字" prop="name">
+      <span>{{testCaseData.name}}</span>
+    </el-form-item>
+    <el-form-item label="用例描述" prop="descr">
+      <expend-text :text="testCaseData.descr"></expend-text>
+    </el-form-item>
+    <el-form-item label="关联需求" prop="demand">
+      <expend-text :text="testCaseData.demand"></expend-text>
+    </el-form-item>
+    <el-form-item label="优先级" prop="priority">
+      <span>{{toPriorityCn(testCaseData.priority)}}</span>
+    </el-form-item>
+    <el-form-item label="前置条件" prop="preconditions">
+      <expend-text :text="testCaseData.preconditions"></expend-text>
+    </el-form-item>
+    <el-form-item label="环境配置" prop="envConfig">
+      <expend-text :text="testCaseData.envConfig"></expend-text>
+    </el-form-item>
+    <el-form-item label="操作步骤" prop="opeSteps">
+      <expend-text :text="testCaseData.opeSteps"></expend-text>
+    </el-form-item>
+    <el-form-item label="输入数据" prop="inputDatas">
+      <expend-text :text="testCaseData.inputDatas"></expend-text>
+    </el-form-item>
+    <el-form-item label="预期结果" prop="expectedResult">
+      <expend-text :text="testCaseData.expectedResult"></expend-text>
+    </el-form-item>
+    <el-form-item label="评判标准" prop="evaCriteria">
+      <expend-text :text="testCaseData.evaCriteria"></expend-text>
+    </el-form-item>
+    <el-form-item label="其他说明" prop="others">
+      <expend-text :text="testCaseData.others"></expend-text>
+    </el-form-item>
+    <el-form-item label="测试结果" prop="testResult">
+      <expend-text :text="testCaseData.testResult"></expend-text>
+    </el-form-item>
+    <el-form-item label="测试结论" prop="testStatus">
+      <span>{{toTestStatusCn(testCaseData.testStatus)}}</span>
+    </el-form-item>
+    <el-form-item label="附件" prop="files">
+      <span v-if="testCaseData.files.length === 0">无</span>
+      <div :key="file" v-for="file in testCaseData.files">
+        <el-link :href="file" target="_blank">{{toFileName(file)}}</el-link>
+      </div>
+    </el-form-item>
+    <el-form-item label="截图" prop="screenshots">
+      <span v-if="testCaseData.screenshots.length === 0">无</span>
+      <el-image v-for="screenshot in testCaseData.screenshots" :key="screenshot"
+        style="width: 130px; height: 130px; margin-left: 10px;"
+        :src="screenshot"
+        :preview-src-list="[screenshot]">
+      </el-image>
+    </el-form-item>
+    <el-form-item label="审核结果" v-if="!canAudit">
+      <span>{{toExamStatusCn(testCaseData.examStatus)}}</span>
+    </el-form-item>
+    <el-form-item label="审核结果说明" v-if="!canAudit">
+      <expend-text :text="testCaseData.examDescr"></expend-text>
+    </el-form-item>
+    <el-form-item label="审核结果" prop="examStatus" v-if="canAudit">
+      <el-select v-model="testCaseData.examStatus">
+        <el-option value="WAIT" label="待审核"/>
+        <el-option value="VALID" label="有效"/>
+        <el-option value="INVALID" label="无效"/>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="审核结果说明" prop="examDescr" v-if="canAudit">
+      <el-input type="textarea" v-model="testCaseData.examDescr"/>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script>
+import Api from '@/js/api'
+import Http from '@/js/http'
+import {notify} from '@/constants'
+import ExpendText from '@/components/text/ExpendText'
+import TestCaseUtils from '../utils'
+import {toFileName} from '@/js/file'
+
+export default {
+  name: 'TestcaseDetail',
+  components: {ExpendText},
+  data: function () {
+    return {
+      rules: {
+        examStatus: [
+          {required: true, message: '审核结果不能为空', trigger: 'blur'}
+        ]
+      },
+      loading: false
+    }
+  },
+  props: {
+    canAudit: {
+      type: Boolean,
+      default: false
+    },
+    testCaseData: {
+      type: Object,
+      default: function () {
+        return {
+          id: undefined,
+          taskCode: '',
+          name: '',
+          descr: '',
+          demand: '',
+          preconditions: '',
+          envConfig: '',
+          priority: undefined,
+          opeSteps: '',
+          inputDatas: '',
+          expectedResult: '',
+          evaCriteria: '',
+          others: '',
+          testResult: '',
+          testStatus: undefined,
+          files: [],
+          screenshots: [],
+          examStatus: '',
+          examDescr: ''
+        }
+      }
+    }
+  },
+  methods: {
+    ...TestCaseUtils,
+    toFileName,
+    submitAuditResult (callback) {
+      this.$refs['testCaseDetail'].validate(valid => {
+        if (valid) {
+          this.showLoading()
+          const auditResult = {
+            examStatus: this.testCaseData.examStatus,
+            examDescr: this.testCaseData.examDescr
+          }
+          Http.put(Api.TESTCASE.EXAM.replace('{id}', this.testCaseData.id), auditResult).then((res) => {
+            this.hideLoading()
+            if (res.code !== 20000) {
+              notify('error', '提交审核结果失败:' + res.data)
+            } else {
+              notify('success', '提交成功')
+              callback(this.testCaseData.examStatus, this.testCaseData.examDescr)
+            }
+          }).catch((error) => {
+            this.hideLoading()
+            notify('error', '提交审核结果失败:' + error.data)
+          })
+          // 提交 report
+        } else {
+          notify('error', '表单填写有误')
+          return false
+        }
+      })
+    },
+    showLoading () {
+      this.loading = true
+    },
+    hideLoading () {
+      this.loading = false
+    },
+    clearValidate (props = []) {
+      this.$refs['testCaseDetail'].clearValidate()
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 242 - 0
src/pages/TestCase/components/testcase_form.vue

@@ -0,0 +1,242 @@
+<template>
+  <el-form ref="testCaseForm" :rules="rules" :model="testCaseData" v-loading="loading" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+    <el-form-item label="任务编号" prop="taskCode">
+      <el-input v-model="testCaseData.taskCode" disabled/>
+    </el-form-item>
+    <el-form-item label="用例名字" prop="name">
+      <el-input v-model="testCaseData.name" :disabled="readOnly"/>
+    </el-form-item>
+    <el-form-item label="用例描述" prop="descr">
+      <el-input type="textarea" v-model="testCaseData.descr" :disabled="readOnly"/>
+    </el-form-item>
+    <el-form-item label="关联需求" prop="demand">
+      <el-input type="textarea" v-model="testCaseData.demand" :disabled="readOnly"/>
+    </el-form-item>
+    <el-form-item label="优先级" prop="priority">
+      <el-select v-model="testCaseData.priority" :disabled="readOnly">
+        <el-option value="HIGH" label="高"/>
+        <el-option value="MID" label="中"/>
+        <el-option value="LOW" label="低"/>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="前置条件" prop="preconditions">
+      <el-input type="textarea" v-model="testCaseData.preconditions" :disabled="readOnly"/>
+    </el-form-item>
+    <el-form-item label="环境配置" prop="envConfig">
+      <el-input type="textarea" v-model="testCaseData.envConfig" :disabled="readOnly"/>
+    </el-form-item>
+    <el-form-item label="操作步骤" prop="opeSteps">
+      <el-input type="textarea" v-model="testCaseData.opeSteps" :disabled="readOnly"/>
+    </el-form-item>
+    <el-form-item label="输入数据" prop="inputDatas">
+      <el-input type="textarea" v-model="testCaseData.inputDatas" :disabled="readOnly"/>
+    </el-form-item>
+    <el-form-item label="预期结果" prop="expectedResult">
+      <el-input type="textarea" v-model="testCaseData.expectedResult" :disabled="readOnly"/>
+    </el-form-item>
+    <el-form-item label="评判标准" prop="evaCriteria">
+      <el-input type="textarea" v-model="testCaseData.evaCriteria" :disabled="readOnly"/>
+    </el-form-item>
+    <el-form-item label="其他说明" prop="others">
+      <el-input type="textarea" v-model="testCaseData.others" :disabled="readOnly"/>
+    </el-form-item>
+    <el-form-item label="测试结果" prop="testResult">
+      <el-input type="textarea" v-model="testCaseData.testResult" :disabled="readOnly"/>
+    </el-form-item>
+    <el-form-item label="测试结论" prop="testStatus">
+      <el-select v-model="testCaseData.testStatus" :disabled="readOnly">
+        <el-option value="WAIT" label="待测试"/>
+        <el-option value="PASS" label="通过"/>
+        <el-option value="NO_PASS" label="不通过"/>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="附件" prop="files">
+      <file-upload :countLimit="countLimit" :files="testCaseData.files" :disabled="readOnly"></file-upload>
+    </el-form-item>
+    <el-form-item label="截图" prop="screenshots">
+      <img-upload :countLimit="countLimit" :files="testCaseData.screenshots" :disabled="readOnly"></img-upload>
+    </el-form-item>
+    <el-form-item label="审核结果" prop="examStatus" v-if="isCommitted">
+      <el-select v-model="testCaseData.examStatus" :disabled="canAudit">
+        <el-option value="WAIT" label="待审核"/>
+        <el-option value="PASS" label="有效"/>
+        <el-option value="NO_PASS" label="无效"/>
+      </el-select>
+    </el-form-item>
+    <el-form-item label="审核结果说明" prop="testResult" v-if="isCommitted">
+      <el-input type="textarea" v-model="testCaseData.examDescr" :disabled="canAudit"/>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script>
+import ElDragSelect from '@/components/DragSelect'
+import Api from '@/js/api'
+import Http from '@/js/http'
+import {notify} from '@/constants'
+import FileUpload from '@/components/file/FileUpload'
+import ImgUpload from '@/components/file/ImgUpload'
+
+export default {
+  name: 'TestcaseForm',
+  components: { ElDragSelect, FileUpload, ImgUpload },
+  data: function () {
+    return {
+      countLimit: 3,
+      rules: {
+        name: [
+          {required: true, message: '用例名称不可为空', trigger: 'blur'},
+          {min: 1, max: 50, message: '用例名称长度在 1 到 50 个字符', trigger: 'blur'}
+        ],
+        priority: [
+          {required: true, message: '优先级不可为空'}
+        ],
+        testStatus: [
+          {required: true, message: '测试结论不可为空'}
+        ]
+      },
+      loading: false
+    }
+  },
+  props: {
+    readOnly: {
+      type: Boolean,
+      default: false
+    },
+    isCommitted: {
+      type: Boolean,
+      default: false
+    },
+    canAudit: {
+      type: Boolean,
+      default: false
+    },
+    testCaseData: {
+      type: Object,
+      default: function () {
+        return {
+          id: undefined,
+          taskCode: '',
+          name: '',
+          descr: '',
+          demand: '',
+          preconditions: '',
+          envConfig: '',
+          priority: undefined,
+          opeSteps: '',
+          inputDatas: '',
+          expectedResult: '',
+          evaCriteria: '',
+          others: '',
+          testResult: '',
+          testStatus: undefined,
+          files: [],
+          screenshots: [],
+          examStatus: '',
+          examDescr: ''
+        }
+      }
+    }
+  },
+  methods: {
+    submitForm (callback) {
+      this.$refs['testCaseForm'].validate(valid => {
+        if (valid) {
+          this.showLoading()
+          const newTestCase = {
+            taskCode: this.testCaseData.taskCode,
+            name: this.testCaseData.name,
+            descr: this.testCaseData.descr,
+            demand: this.testCaseData.demand,
+            preconditions: this.testCaseData.preconditions,
+            envConfig: this.testCaseData.envConfig,
+            priority: this.testCaseData.priority,
+            opeSteps: this.testCaseData.opeSteps,
+            inputDatas: this.testCaseData.inputDatas,
+            expectedResult: this.testCaseData.expectedResult,
+            evaCriteria: this.testCaseData.evaCriteria,
+            others: this.testCaseData.others,
+            testResult: this.testCaseData.testResult,
+            testStatus: this.testCaseData.testStatus,
+            files: this.testCaseData.files,
+            screenshots: this.testCaseData.screenshots
+          }
+          if (this.testCaseData.id && this.testCaseData.id > 0) {
+            newTestCase['id'] = this.testCaseData.id
+          }
+          console.log(newTestCase)
+          let url
+          let submitDataMethod
+          if (newTestCase['id']) {
+            url = Api.TESTCASE.UPDATE.replace('{id}', newTestCase['id'])
+            submitDataMethod = Http.put
+          } else {
+            url = Api.TESTCASE.ADD
+            submitDataMethod = Http.post
+          }
+          submitDataMethod(url, newTestCase).then((res) => {
+            console.log(res)
+            this.hideLoading()
+            if (res.code !== 20000) {
+              notify('error', '提交测试用例失败:' + res.data)
+            } else {
+              this.testCaseData.id = res.data
+              notify('success', '提交成功')
+              callback()
+            }
+          }).catch((error) => {
+            this.hideLoading()
+            notify('error', '用例创建失败:' + error)
+          })
+          // 提交 report
+        } else {
+          notify('error', '表单填写有误')
+          return false
+        }
+      })
+    },
+    submitAuditResult (callback) {
+      this.$refs['testCaseForm'].validate(valid => {
+        if (valid) {
+          this.showLoading()
+          const auditResult = {
+            id: this.testCaseData.id,
+            examStatus: this.testCaseData.examStatus,
+            examDescr: this.testCaseData.examDescr
+          }
+          Http.put(url, auditResult).then((res) => {
+            console.log(res)
+            this.hideLoading()
+            if (res.code !== 20000) {
+              notify('error', '提交审核结果失败:' + res.data)
+            } else {
+              notify('success', '提交成功')
+              callback()
+            }
+          }).catch((error) => {
+            this.hideLoading()
+            notify('error', '用例创建失败:' + error)
+          })
+          // 提交 report
+        } else {
+          notify('error', '表单填写有误')
+          return false
+        }
+      })
+    },
+    showLoading () {
+      this.loading = true
+    },
+    hideLoading () {
+      this.loading = false
+    },
+    clearValidate (props = []) {
+      this.$refs['testCaseForm'].clearValidate()
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 539 - 0
src/pages/TestCase/exam_testcases.vue

@@ -0,0 +1,539 @@
+<template>
+  <div class="app-container">
+    <div class="title h1" style="margin-top: 10px;">测试用例审核</div>
+    <div class="filter-container">
+      <el-select v-model="listQueryParam.selectedProjectCode"  filterable :filter-method="projectDataFilter" @click.native="eqNoClick">
+        <el-option
+          v-for="project in searchProjects"
+          :key="project.code"
+          :label="project.name"
+          :value="project.code"
+        />
+      </el-select>
+      <el-select v-model="listQueryParam.selectedTaskCode"  filterable :filter-method="taskDataFilter" @click.native="eqNoClick">
+        <el-option
+          v-for="task in searchTasks"
+          :key="task.code"
+          :label="task.name"
+          :value="task.code"
+        />
+      </el-select>
+      <el-select v-model="listQueryParam.selectedUserId">
+        <el-option
+          v-for="user in users"
+          :key="'user' + user.id"
+          :label="user.name"
+          :value="user.id"
+        />
+      </el-select>
+      <el-select v-model="listQueryParam.testStatus">
+        <el-option value="" label="---测试结论---"/>
+        <el-option value="WAIT" label="待测试"/>
+        <el-option value="PASS" label="通过"/>
+        <el-option value="NO_PASS" label="不通过"/>
+      </el-select>
+      <el-select v-model="listQueryParam.examStatus">
+        <el-option value="" label="---审核结果---"/>
+        <el-option value="WAIT" label="待审核"/>
+        <el-option value="VALID" label="有效"/>
+        <el-option value="INVALID" label="无效"/>
+      </el-select>
+    </div>
+
+    <el-table
+      ref="testCaseTable"
+      :key="tableKey"
+      v-loading="listLoading"
+      :data="testCaseDatas"
+      border
+      fit
+      highlight-current-row
+      style="width: 100%"
+    >
+      <el-table-column type="expand" align="center"  min-width="1%">
+        <template slot-scope="{row}">
+          <el-table
+            :key="'innerTableKey' + row.id"
+            :data="row.defects"
+            border
+            fit
+            style="width: 100%"
+          >
+            <el-table-column label="编号" align="center" min-width="15%">
+              <template slot-scope="{row}">
+                <span>{{ row.code }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="缺陷描述" align="center" :show-overflow-tooltip="true" min-width="35%">
+              <template slot-scope="{row}">
+                <span>{{ row.descr }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="严重等级" align="center" min-width="12%">
+              <template slot-scope="{row}">
+                <span>{{ toSeriousnessCn(row.seriousness) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="优先级" align="center" min-width="10%">
+              <template slot-scope="{row}">
+                <span>{{ toPriorityCn(row.priority) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="缺陷类型" align="center" min-width="12%">
+              <template slot-scope="{row}">
+                <span>{{ toDefectTypeCn(row.defectType) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作步骤" align="center" :show-overflow-tooltip="true" min-width="35%">
+              <template slot-scope="{row}">
+                <span>{{ row.opeSteps }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="输入数据" align="center" :show-overflow-tooltip="true" min-width="35%">
+              <template slot-scope="{row}">
+                <span>{{ row.inputDatas }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="预期结果" align="center" :show-overflow-tooltip="true" min-width="35%">
+              <template slot-scope="{row}">
+                <span>{{ row.expectedResult }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="测试结果" align="center" :show-overflow-tooltip="true" min-width="35%">
+              <template slot-scope="{row}">
+                <span>{{ row.testResult }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" class-name="small-padding fixed-width" min-width="20%">
+              <template slot-scope="{row,$index}">
+                <i class="el-icon-tickets mini-margin" @click="handleDefectDetail(row)" style="cursor: pointer;" title="查看详情"></i>
+              </template>
+            </el-table-column>
+          </el-table>
+        </template>
+      </el-table-column>
+      <el-table-column label="编号" prop="code" sortable="custom" align="center" min-width="3%">
+        <template slot-scope="{row}">
+          <span>{{ row.code }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="用例名称" align="center" :show-overflow-tooltip="true" min-width="6%">
+        <template slot-scope="{row}">
+          <span>{{ row.name }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="优先级" align="center" min-width="3%">
+        <template slot-scope="{row}">
+          <span>{{ toPriorityCn(row.priority) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="描述" align="center" :show-overflow-tooltip="true" min-width="10%">
+        <template slot-scope="{row}">
+          <span>{{ row.descr }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="关联需求" align="center" :show-overflow-tooltip="true" min-width="10%">
+        <template slot-scope="{row}">
+          <span>{{ row.demand }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="预期结果" align="center" :show-overflow-tooltip="true" min-width="8%">
+        <template slot-scope="{row}">
+          <span>{{ row.expectedResult }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="测试结果" align="center" :show-overflow-tooltip="true" min-width="8%">
+        <template slot-scope="{row}">
+          <span>{{ row.testResult }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="测试结论" align="center" min-width="4%">
+        <template slot-scope="{row}">
+          <span>{{ toTestStatusCn(row.testStatus) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="缺陷数量" align="center" min-width="4%">
+        <template slot-scope="{row}">
+          <a href="javascript:void(0)" @click="toogleExpand2(row)" style="color: red;">{{ row.defects.length }}个</a>
+        </template>
+      </el-table-column>
+      <el-table-column label="审核结果" align="center" min-width="4%">
+        <template slot-scope="{row}">
+          <span>{{ toExamStatusCn(row.examStatus) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="审核结果说明" align="center" :show-overflow-tooltip="true" min-width="8%">
+        <template slot-scope="{row}">
+          <span>{{ row.examDescr }}</span>
+        </template>
+      </el-table-column>
+<!--      <el-table-column v-if="showReviewer" label="Reviewer" width="110px" align="center">-->
+<!--        <template slot-scope="{row}">-->
+<!--          <span style="color:red;">{{ row.reviewer }}</span>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+<!--      <el-table-column label="Imp" width="80px">-->
+<!--        <template slot-scope="{row}">-->
+<!--          <svg-icon v-for="n in + row.importance" :key="n" icon-class="star" class="meta-item__icon" />-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+      <el-table-column label="操作" align="center" min-width="5%">
+        <template slot-scope="{row,$index}">
+          <i class="el-icon-tickets mini-margin" @click="handleDetail(row)" style="cursor: pointer;" title="查看详情" v-if="!canAudit"></i>
+          <i class="el-icon-edit mini-margin" @click="handleDetail(row)" style="cursor: pointer;" title="审核"  v-if="canAudit"></i>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="listQueryParam.pageNo" :limit.sync="listQueryParam.pageSize" @pagination="getList" />
+
+    <el-dialog title="用例详情" :visible.sync="dialogFormVisible">
+      <testcase-detail ref="testCaseDetail" style="width: 600px;" :test-case-data="testCaseData" :can-audit="canAudit"></testcase-detail>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="submitData()" v-if="canAudit">
+          提交
+        </el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="缺陷详情" :visible.sync="defectDialogFormVisible">
+      <defect-detail ref="defectDetail" style="width: 600px;" :defect-data="defectData"></defect-detail>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="defectDialogFormVisible = false">
+          取消
+        </el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import waves from '@/directive/waves' // waves directive
+import Pagination from '@/components/Pagination' // secondary package based on el-pagination
+import TestcaseDetail from './components/testcase_detail'
+import DefectDetail from './components/defect_detail'
+import Http from '@/js/http'
+import Api from '@/js/api'
+import {notify} from '@/constants'
+import TestCaseUtils from './utils'
+
+export default {
+  name: 'ComplexTable',
+  components: { Pagination, TestcaseDetail, DefectDetail },
+  directives: { waves },
+  data () {
+    return {
+      testCaseData: {},
+      defectData: {},
+      testCaseDatas: [],
+      tableKey: 0,
+      total: 0,
+      listQueryParam: {
+        pageNo: 1,
+        pageSize: 20,
+        selectedProjectCode: this.$route.params.projectCode,
+        selectedTaskCode: this.$route.params.taskCode,
+        selectedUserId: parseInt(this.$route.params.userId),
+        testStatus: '',
+        examStatus: ''
+      },
+      listLoading: true,
+      showReviewer: false,
+      dialogFormVisible: false,
+      defectDialogFormVisible: false,
+      selectedTaskCode: '',
+      selectedTaskData: {
+        name: '',
+        code: '',
+        status: undefined,
+        id: undefined
+      },
+      projects: [],
+      searchProjects: [],
+      tasks: [],
+      searchTasks: [],
+      users: [],
+      selectedUser: {
+        name: '',
+        username: '',
+        isCommitted: undefined,
+        id: undefined
+      },
+      canAudit: false
+    }
+  },
+  watch: {
+    'listQueryParam.selectedProjectCode': {
+      immediate: false,
+      handler: function () {
+        this.getSimpleTaskDatas()
+      }
+    },
+    'listQueryParam.selectedTaskCode': {
+      immediate: false,
+      handler: function () {
+        this.getTaskUserDatas()
+      }
+    },
+    'listQueryParam.selectedUserId': {
+      immediate: false,
+      handler: function () {
+        this.getList()
+      }
+    },
+    'listQueryParam.examStatus': {
+      immediate: false,
+      handler: function () {
+        this.getList()
+      }
+    },
+    'listQueryParam.testStatus': {
+      immediate: false,
+      handler: function () {
+        this.getList()
+      }
+    }
+  },
+  created () {
+    this.initData()
+    this.initDefectData()
+    this.getSimpleProjectDatas()
+    this.getSimpleTaskDatas()
+    this.getTaskUserDatas()
+  },
+  methods: {
+    ...TestCaseUtils,
+    getList () {
+      this.showListLoading()
+      let url = Api.TESTCASE.USER_TEST_CASES.replace('{taskCode}', this.listQueryParam.selectedTaskCode)
+        .replace('{designerId}', this.listQueryParam.selectedUserId)
+        .replace('{pageNo}', this.listQueryParam.pageNo - 1)
+        .replace('{pageSize}', this.listQueryParam.pageSize)
+      if (this.listQueryParam.testStatus || this.listQueryParam.examStatus) {
+        if (url.indexOf('?') === -1) {
+          url = url + '?'
+        }
+        if (this.listQueryParam.testStatus) {
+          url = url + 'testStatus=' + this.listQueryParam.testStatus + '&'
+        }
+        if (this.listQueryParam.examStatus) {
+          url = url + 'examStatus=' + this.listQueryParam.examStatus + '&'
+        }
+        url = url.substring(0, url.length - 1)
+      }
+      Http.get(url).then((res) => {
+        const testCasePage = res.data
+        this.total = testCasePage.totalCount
+        this.pageNo = testCasePage.pageNo + 1
+        this.pageLimit = testCasePage.pageSize
+        this.testCaseDatas = testCasePage.datas
+        this.selectedUser = this.users.find(user => user.id === this.listQueryParam.selectedUserId)
+        this.canAudit = this.selectedTaskData.status !== 4 && this.selectedUser.isCommitted === 1
+      }).catch((error) => {
+        this.total = 0
+        this.pageNo = 1
+        this.pageLimit = this.pageLimit
+        this.testCaseDatas = []
+        notify('error', '获取测试用例数据失败:' + error.data)
+      })
+      this.hideListLoading()
+    },
+    // handleSearch () {
+    //   this.listQueryParam.pageNo = 1
+    //   this.getList()
+    // },
+    initData () {
+      this.testCaseData = {
+        demand: '',
+        descr: '',
+        envConfig: '',
+        evaCriteria: '',
+        testStatus: '',
+        expectedResult: '',
+        files: [],
+        id: undefined,
+        inputDatas: '',
+        name: '',
+        opeSteps: '',
+        others: '',
+        preconditions: '',
+        priority: '',
+        screenshots: [],
+        taskCode: '',
+        testResult: '',
+        examStatus: '',
+        examDescr: ''
+      }
+    },
+    initDefectData () {
+      this.defectData = {
+        descr: '',
+        envConfig: '',
+        expectedResult: '',
+        files: [],
+        id: undefined,
+        inputDatas: '',
+        opeSteps: '',
+        others: '',
+        preconditions: '',
+        priority: '',
+        seriousness: '',
+        defectType: '',
+        screenshots: [],
+        taskCode: '',
+        testCaseCode: '',
+        testResult: ''
+      }
+    },
+    submitData () {
+      this.$refs.testCaseDetail.submitAuditResult((examStatus, examDescr) => {
+        let testCase = this.testCaseDatas.find(testCase => testCase.id === this.testCaseData.id)
+        testCase.examStatus = examStatus
+        testCase.examDescr = examDescr
+      })
+    },
+    handleDetail (row) {
+      this.dialogFormVisible = true
+      this.deepClone(this.testCaseData, row)
+      this.$nextTick(() => {
+        this.$refs['testCaseDetail'].clearValidate()
+      })
+    },
+    handleDefectDetail (row) {
+      this.defectDialogFormVisible = true
+      this.deepClone(this.defectData, row)
+    },
+    projectDataFilter (val) {
+      if (val) {
+        this.searchProjects = this.projects.filter(project => {
+          return project.name.includes(val)
+        })
+      } else {
+        this.searchProjects = this.projects
+      }
+    },
+    taskDataFilter (val) {
+      if (val) {
+        this.searchTasks = this.tasks.filter(task => {
+          return task.name.includes(val)
+        })
+      } else {
+        this.searchTasks = this.tasks
+      }
+    },
+    eqNoClick () {
+      this.searchProjects = this.projects
+      this.searchTasks = this.tasks
+    },
+    getSimpleProjectDatas () {
+      Http.get(Api.PROJECT.GET_SIMPLE_DATAS).then((res) => {
+        this.projects = res.data
+        this.searchProjects = res.data
+      }).catch((error) => {
+        this.hideLoading()
+        notify('error', '获取项目列表数据失败:' + error.data)
+      })
+    },
+    getSimpleTaskDatas () {
+      Http.get(Api.TASK.GET_SIMPLE_DATAS_BY_PROJECT.replace('{projectCode}', this.listQueryParam.selectedProjectCode)).then((res) => {
+        this.tasks = res.data
+        this.searchTasks = res.data
+        if (!(this.searchTasks.find(task => task.code === this.listQueryParam.selectedTaskCode))) {
+          this.selectedTaskData = this.tasks[0]
+          this.listQueryParam.selectedTaskCode = this.selectedTaskData.code
+        }
+      }).catch((error) => {
+        this.hideLoading()
+        notify('error', '获取任务列表数据失败:' + error.data)
+      })
+    },
+    getTaskUserDatas () {
+      Http.get(Api.TASK.GET_TASK_USER_DATAS.replace('{projectCode}', this.listQueryParam.selectedProjectCode)
+        .replace('{taskCode}', this.listQueryParam.selectedTaskCode)).then((res) => {
+        this.users = res.data
+        this.users.forEach(user => {
+          if (!(user.isCommitted)) {
+            user.name = user.name + '(未提交)'
+          } else {
+            user.name = user.name + '(已提交)'
+          }
+        })
+        if (!(this.users.find(user => user.id === this.listQueryParam.selectedUserId))) {
+          this.listQueryParam.selectedUserId = this.users[0].id
+        } else {
+          this.getList()
+        }
+      }).catch((error) => {
+        notify('error', '获取用户列表数据失败:' + error.data)
+      })
+    },
+    deepClone: function (oldData, newData) {
+      if (newData instanceof Array) {
+        if (oldData.length !== newData.length) {
+          oldData = new Array(newData.length)
+        }
+        for (var i = 0; i < newData.length; i++) {
+          if (newData[i] instanceof Array) {
+            oldData[i] = new Array(newData[i].length)
+            this.deepClone(oldData[i], newData[i])
+          } else if (newData[i] instanceof Object) {
+            oldData[i] = {}
+            this.deepClone(oldData[i], newData[i])
+          } else {
+            oldData[i] = newData[i]
+          }
+        }
+      } else if (newData instanceof Object) {
+        if (oldData.length > 0) {
+          oldData = {}
+        }
+        for (var key in newData) {
+          if (newData[key] instanceof Array) {
+            oldData[key] = new Array(newData[key].length)
+            this.deepClone(oldData[key], newData[key])
+          } else if (newData[key] instanceof Object) {
+            oldData[key] = {}
+            this.deepClone(oldData[key], newData[key])
+          } else {
+            oldData[key] = newData[key]
+          }
+        }
+      }
+      console.log('deepClone')
+      console.log(oldData)
+    },
+    showListLoading () {
+      this.listLoading = true
+    },
+    hideListLoading () {
+      this.listLoading = false
+    },
+    toogleExpand2 (row) {
+      let testCaseTable = this.$refs.testCaseTable
+      testCaseTable.toggleRowExpansion(row)
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .filter-container {
+    margin-bottom: 4px;
+    padding-left: 6px;
+  }
+  .mini-margin {
+    margin-left: 6px;
+    margin-right: 6px;
+  }
+</style>
+<style>
+.el-table__expanded-cell {
+  background-color: #eee!important;
+}
+.el-table__expanded-cell:hover {
+  background-color: #eee!important;
+}
+</style>

+ 685 - 0
src/pages/TestCase/testcases.vue

@@ -0,0 +1,685 @@
+<template>
+  <div class="app-container">
+    <div class="title h1" style="margin-top: 10px;">测试用例管理</div>
+    <div class="filter-container">
+      <el-select v-model="listQueryParam.selectedTaskCode"  filterable :filter-method="dataFilter" @click.native="eqNoClick">
+        <el-option
+          v-for="task in searchTasks"
+          :key="task.code"
+          :label="task.name"
+          :value="task.code"
+        />
+      </el-select>
+      <el-select v-model="listQueryParam.testStatus">
+        <el-option value="" label="---测试结论---"/>
+        <el-option value="WAIT" label="待测试"/>
+        <el-option value="PASS" label="通过"/>
+        <el-option value="NO_PASS" label="不通过"/>
+      </el-select>
+      <el-select v-model="listQueryParam.examStatus" v-if="!testCaseCanEdit">
+        <el-option value="" label="---审核结果---"/>
+        <el-option value="WAIT" label="待审核"/>
+        <el-option value="VALID" label="有效"/>
+        <el-option value="INVALID" label="无效"/>
+      </el-select>
+<!--      <el-input v-model="listQuery.title" placeholder="用例名" style="width: 200px;" class="filter-item" @keyup.enter.native="handleFilter" />-->
+<!--      <el-input v-model="listQuery.title" placeholder="用例编号" style="width: 200px;" class="filter-item" @keyup.enter.native="handleFilter" />-->
+<!--      <el-button v-waves class="filter-item" type="primary" icon="el-icon-search" @click="handleSearch">-->
+<!--        Search-->
+<!--      </el-button>-->
+      <el-button class="filter-item" style="margin-left: 10px;" type="primary" @click="handleCreate" v-if="testCaseCanEdit">
+        新增测试用例
+      </el-button>
+      <el-button class="filter-item" style="margin-left: 10px;" type="success" @click="handleSubmitAudit" v-if="testCaseCanEdit">
+        提交审核
+      </el-button>
+      <span style="display: block; float: right; color: indianred; margin-right: 20px">提交状态:{{isCommitted ? '已提交' : '未提交'}}</span>
+    </div>
+
+    <el-table
+      ref="testCaseTable"
+      :key="tableKey"
+      v-loading="listLoading"
+      :data="testCaseDatas"
+      border
+      fit
+      highlight-current-row
+      style="width: 100%"
+    >
+      <el-table-column type="expand" align="center" min-width="1%">
+        <template slot-scope="{row}">
+          <el-table
+            :key="'innerTableKey' + row.id"
+            :data="row.defects"
+            border
+            fit
+            style="width: 100%"
+          >
+            <el-table-column label="编号" align="center" min-width="15%">
+              <template slot-scope="{row}">
+                <span>{{ row.code }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="缺陷描述" align="center" :show-overflow-tooltip="true" min-width="35%">
+              <template slot-scope="{row}">
+                <span>{{ row.descr }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="严重等级" align="center" min-width="12%">
+              <template slot-scope="{row}">
+                <span>{{ toSeriousnessCn(row.seriousness) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="优先级" align="center" min-width="10%">
+              <template slot-scope="{row}">
+                <span>{{ toPriorityCn(row.priority) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="缺陷类型" align="center" min-width="12%">
+              <template slot-scope="{row}">
+                <span>{{ toDefectTypeCn(row.defectType) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作步骤" align="center" :show-overflow-tooltip="true" min-width="35%">
+              <template slot-scope="{row}">
+                <span>{{ row.opeSteps }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="输入数据" align="center" :show-overflow-tooltip="true" min-width="35%">
+              <template slot-scope="{row}">
+                <span>{{ row.inputDatas }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="预期结果" align="center" :show-overflow-tooltip="true" min-width="35%">
+              <template slot-scope="{row}">
+                <span>{{ row.expectedResult }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="测试结果" align="center" :show-overflow-tooltip="true" min-width="35%">
+              <template slot-scope="{row}">
+                <span>{{ row.testResult }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" class-name="small-padding fixed-width" min-width="20%">
+              <template slot-scope="{row,$index}">
+                <i class="el-icon-tickets mini-margin" @click="handleDefectDetail(row)" v-if="!testCaseCanEdit" style="cursor: pointer;" title="查看详情"></i>
+                <i class="el-icon-edit mini-margin " @click="handleUpdateDefect(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="编辑"></i>
+                <i class="el-icon-delete mini-margin " @click="handleDeleteDefect(row,$index)" v-if="testCaseCanEdit" style="cursor: pointer;" title="删除"></i>
+                <i class="el-icon-document-copy mini-margin " @click="handleCopyDefect(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="复制"></i>
+              </template>
+            </el-table-column>
+          </el-table>
+        </template>
+      </el-table-column>
+      <el-table-column label="编号" prop="code" sortable="custom" align="center" min-width="3%">
+        <template slot-scope="{row}">
+          <span>{{ row.code }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="用例名称" align="center" :show-overflow-tooltip="true" min-width="6%">
+        <template slot-scope="{row}">
+          <span>{{ row.name }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="优先级" align="center" min-width="3%">
+        <template slot-scope="{row}">
+          <span>{{ toPriorityCn(row.priority) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="描述" align="center" :show-overflow-tooltip="true" min-width="10%">
+        <template slot-scope="{row}">
+          <span>{{ row.descr }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="关联需求" align="center" :show-overflow-tooltip="true" min-width="10%">
+        <template slot-scope="{row}">
+          <span>{{ row.demand }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="预期结果" align="center" :show-overflow-tooltip="true" min-width="8%">
+        <template slot-scope="{row}">
+          <span>{{ row.expectedResult }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="测试结果" align="center" :show-overflow-tooltip="true" min-width="8%">
+        <template slot-scope="{row}">
+          <span>{{ row.testResult }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="测试结论" align="center" min-width="4%">
+        <template slot-scope="{row}">
+          <span>{{ toTestStatusCn(row.testStatus) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="缺陷数量(个)" align="center" min-width="4%">
+        <template slot-scope="{row}">
+          <a href="javascript:void(0)" @click="toogleExpand2(row)" style="color: indianred;">{{ row.defects.length }}</a>
+        </template>
+      </el-table-column>
+      <el-table-column label="审核结果" align="center" min-width="4%" v-if="!testCaseCanEdit">
+        <template slot-scope="{row}">
+          <span>{{ toExamStatusCn(row.examStatus) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="审核结果说明" align="center" :show-overflow-tooltip="true" min-width="8%"  v-if="!testCaseCanEdit">
+        <template slot-scope="{row}">
+          <span>{{ row.examDescr }}</span>
+        </template>
+      </el-table-column>
+<!--      <el-table-column v-if="showReviewer" label="Reviewer" width="110px" align="center">-->
+<!--        <template slot-scope="{row}">-->
+<!--          <span style="color:red;">{{ row.reviewer }}</span>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+<!--      <el-table-column label="Imp" width="80px">-->
+<!--        <template slot-scope="{row}">-->
+<!--          <svg-icon v-for="n in + row.importance" :key="n" icon-class="star" class="meta-item__icon" />-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+      <el-table-column label="操作" align="center" min-width="5%">
+        <template slot-scope="{row,$index}">
+          <i class="el-icon-tickets mini-margin" @click="handleDetail(row)" v-if="!testCaseCanEdit" style="cursor: pointer;" title="查看详情"></i>
+          <i class="el-icon-edit mini-margin" @click="handleUpdate(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="编辑"></i>
+          <i class="el-icon-delete mini-margin" @click="handleDelete(row,$index)" v-if="testCaseCanEdit" style="cursor: pointer;" title="删除"></i>
+          <i class="el-icon-document-copy mini-margin" @click="handleCopy(row)" v-if="testCaseCanEdit" style="cursor: pointer;" title="复制"></i>
+          <i class="el-icon-document-add mini-margin" @click="handleCreateDefect(row)" v-if="testCaseCanEdit && row.testStatus === 'NO_PASS'" style="cursor: pointer;" title="新增缺陷"></i>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="total>0" :total="total" :page.sync="listQueryParam.pageNo" :limit.sync="listQueryParam.pageSize" @pagination="getList" />
+
+    <el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
+      <testcase-form ref="testCaseForm" style="width: 600px;" :test-case-data="testCaseData" :read-only="!testCaseCanEdit"></testcase-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="submitData()">
+          提交
+        </el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog :title="textMap[dialogStatus]" :visible.sync="defectDialogFormVisible">
+      <defect-form ref="defectForm" style="width: 600px;" :defect-data="defectData"></defect-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="defectDialogFormVisible = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="submitDefectData()">
+          提交
+        </el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="用例详情" :visible.sync="testCaseDetailDialogFormVisible">
+      <testcase-detail ref="testCaseDetail" style="width: 600px;" :test-case-data="testCaseData" :can-audit="false"></testcase-detail>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="testCaseDetailDialogFormVisible = false">
+          取消
+        </el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="缺陷详情" :visible.sync="defectDetailDialogFormVisible">
+      <defect-detail ref="defectDetail" style="width: 600px;" :defect-data="defectData"></defect-detail>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="defectDetailDialogFormVisible = false">
+          取消
+        </el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import waves from '@/directive/waves' // waves directive
+import Pagination from '@/components/Pagination' // secondary package based on el-pagination
+import TestcaseForm from './components/testcase_form'
+import DefectForm from './components/defect_form'
+import TestcaseDetail from './components/testcase_detail'
+import DefectDetail from './components/defect_detail'
+import Http from '@/js/http'
+import Api from '@/js/api'
+import {notify} from '@/constants'
+import TestCaseUtils from './utils'
+
+export default {
+  name: 'ComplexTable',
+  components: { Pagination, TestcaseForm, DefectForm, TestcaseDetail, DefectDetail },
+  directives: { waves },
+  data () {
+    return {
+      testCaseData: {},
+      defectData: {},
+      testCaseDatas: [],
+      tableKey: 0,
+      total: 0,
+      listQueryParam: {
+        pageNo: 1,
+        pageSize: 20,
+        selectedTaskCode: this.$route.params.taskCode,
+        testStatus: '',
+        examStatus: ''
+      },
+      listLoading: true,
+      showReviewer: false,
+      dialogFormVisible: false,
+      defectDialogFormVisible: false,
+      testCaseDetailDialogFormVisible: false,
+      defectDetailDialogFormVisible: false,
+      dialogStatus: '',
+      textMap: {
+        update: '编辑测试用例',
+        create: '新建测试用例',
+        create_defect: '新建缺陷',
+        update_defect: '编辑缺陷',
+        detail: '测试用例详情',
+        defect_detail: '缺陷详情'
+      },
+      selectedTaskCode: '',
+      tasks: [],
+      searchTasks: [],
+      testCaseCanEdit: false,
+      isCommitted: false
+    }
+  },
+  watch: {
+    'listQueryParam.selectedTaskCode': function (newVal, oldVal) {
+      this.getList()
+      this.handleStatus()
+    },
+    'listQueryParam.testStatus': function (newVal, oldVal) {
+      this.getList()
+    },
+    'listQueryParam.examStatus': function (newVal, oldVal) {
+      this.getList()
+    }
+  },
+  created () {
+    this.initData()
+    this.initDefectData()
+    this.getSimpleTaskDatas()
+    this.getList()
+  },
+  methods: {
+    ...TestCaseUtils,
+    handleStatus () {
+      let task = this.tasks.find(task => {
+        return task.code === this.listQueryParam.selectedTaskCode
+      })
+      this.testCaseCanEdit = !(task.isCommitted) && task.status !== 5
+      this.isCommitted = task.isCommitted && true
+    },
+    getList () {
+      this.showListLoading()
+      let url = Api.TESTCASE.USER_TEST_CASES.replace('{taskCode}', this.listQueryParam.selectedTaskCode)
+        .replace('{designerId}', '0')
+        .replace('{pageNo}', this.listQueryParam.pageNo - 1)
+        .replace('{pageSize}', this.listQueryParam.pageSize)
+      if (this.listQueryParam.testStatus || this.listQueryParam.examStatus) {
+        if (url.indexOf('?') === -1) {
+          url = url + '?'
+        }
+        if (this.listQueryParam.testStatus) {
+          url = url + 'testStatus=' + this.listQueryParam.testStatus + '&'
+        }
+        if (this.listQueryParam.examStatus) {
+          url = url + 'examStatus=' + this.listQueryParam.examStatus + '&'
+        }
+        url = url.substring(0, url.length - 1)
+      }
+      Http.get(url).then((res) => {
+        const testCasePage = res.data
+        this.total = testCasePage.totalCount
+        this.listQueryParam.pageNo = testCasePage.pageNo + 1
+        this.listQueryParam.pageSize = testCasePage.pageSize
+        this.testCaseDatas = testCasePage.datas
+      }).catch((error) => {
+        console.error(error)
+        notify('error', '获取测试用例数据失败:' + error)
+      })
+      this.hideListLoading()
+    },
+    // handleSearch () {
+    //   this.listQueryParam.pageNo = 1
+    //   this.getList()
+    // },
+    initData () {
+      this.testCaseData = {
+        demand: '',
+        descr: '',
+        envConfig: '',
+        evaCriteria: '',
+        testStatus: '',
+        expectedResult: '',
+        files: [],
+        id: undefined,
+        inputDatas: '',
+        name: '',
+        opeSteps: '',
+        others: '',
+        preconditions: '',
+        priority: '',
+        screenshots: [],
+        taskCode: '',
+        testResult: ''
+      }
+    },
+    initDefectData () {
+      this.defectData = {
+        descr: '',
+        envConfig: '',
+        expectedResult: '',
+        files: [],
+        id: undefined,
+        inputDatas: '',
+        opeSteps: '',
+        others: '',
+        preconditions: '',
+        priority: '',
+        seriousness: '',
+        defectType: '',
+        screenshots: [],
+        taskCode: '',
+        testCaseCode: '',
+        testResult: ''
+      }
+    },
+    handleSubmitAudit () {
+      this.$confirm('提交任务之后将不能再编辑测试用例,您确定要提交该任务吗', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+        closeOnClickModal: false
+      }).then(() => {
+        this.showListLoading()
+        Http.put(Api.TASK.COMMIT_TASK.replace('{taskCode}', this.listQueryParam.selectedTaskCode), {}).then((res) => {
+          this.hideListLoading()
+          if (res.code === 20000) {
+            this.$notify({
+              title: 'Success',
+              message: '提交成功',
+              type: 'success',
+              duration: 2000
+            })
+            this.testCaseCanEdit = false
+          } else {
+            this.$notify({
+              title: 'Error',
+              message: '提交失败:' + res.data,
+              type: 'Error',
+              duration: 2000
+            })
+          }
+        }).catch((error) => {
+          this.hideListLoading()
+          notify('error', '提交失败:' + error)
+        })
+      }).catch(() => {
+      })
+    },
+    handleCreate () {
+      this.initData()
+      this.dialogStatus = 'create'
+      this.dialogFormVisible = true
+      this.testCaseData.taskCode = this.listQueryParam.selectedTaskCode
+      this.$nextTick(() => {
+        this.$refs['testCaseForm'].clearValidate()
+      })
+    },
+    handleCreateDefect (testCase) {
+      this.initDefectData()
+      this.dialogStatus = 'create_defect'
+      this.defectDialogFormVisible = true
+      this.defectData.taskCode = this.listQueryParam.selectedTaskCode
+      this.defectData.testCaseCode = testCase.code
+      this.defectData.priority = testCase.priority
+      this.defectData.preconditions = testCase.preconditions
+      this.defectData.envConfig = testCase.envConfig
+      this.defectData.opeSteps = testCase.opeSteps
+      this.defectData.inputDatas = testCase.inputDatas
+      this.defectData.expectedResult = testCase.expectedResult
+      this.defectData.testResult = testCase.testResult
+      this.$nextTick(() => {
+        this.$refs['defectForm'].clearValidate()
+      })
+    },
+    submitData () {
+      let callback = null
+      if (this.testCaseData.id) {
+        callback = this.getList
+      } else {
+        callback = () => {
+          this.listQueryParam.pageNo = Math.ceil((this.total + 1) / this.listQueryParam.pageSize)
+          this.getList()
+        }
+      }
+      this.$refs.testCaseForm.submitForm(callback)
+    },
+    submitDefectData () {
+      this.$refs.defectForm.submitForm(this.getList)
+    },
+    handleDetail (row) {
+      this.testCaseDetailDialogFormVisible = true
+      this.deepClone(this.testCaseData, row)
+    },
+    handleDefectDetail (row) {
+      this.defectDetailDialogFormVisible = true
+      this.deepClone(this.defectData, row)
+    },
+    handleUpdate (row) {
+      this.dialogStatus = 'update'
+      this.dialogFormVisible = true
+      this.deepClone(this.testCaseData, row)
+      this.$nextTick(() => {
+        this.$refs['testCaseForm'].clearValidate()
+      })
+    },
+    handleUpdateDefect (row) {
+      this.dialogStatus = 'update_defect'
+      this.defectDialogFormVisible = true
+      this.deepClone(this.defectData, row)
+      this.$nextTick(() => {
+        this.$refs['defectForm'].clearValidate()
+      })
+      console.log(this.defectData)
+    },
+    handleDelete (row, index) {
+      this.$confirm('您确定要删除该条数据吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+        closeOnClickModal: false
+      }).then(() => {
+        this.showListLoading()
+        Http.delete(Api.TESTCASE.DELETE.replace('{id}', row.id), {}).then((res) => {
+          this.hideListLoading()
+          if (res.code === 20000) {
+            this.$notify({
+              title: 'Success',
+              message: '删除成功',
+              type: 'success',
+              duration: 2000
+            })
+            this.getList()
+          } else {
+            this.$notify({
+              title: 'Error',
+              message: '删除失败' + res.msg,
+              type: 'Error',
+              duration: 2000
+            })
+          }
+        }).catch((error) => {
+          this.hideListLoading()
+          notify('error', '删除失败:' + error)
+        })
+      }).catch(() => {
+      })
+    },
+    handleDeleteDefect (row, index) {
+      this.$confirm('您确定要删除该条数据吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        center: true,
+        closeOnClickModal: false
+      }).then(() => {
+        this.showListLoading()
+        Http.delete(Api.TESTCASE.DELETE_DEFECT.replace('{id}', row.id), {}).then((res) => {
+          this.hideListLoading()
+          if (res.code === 20000) {
+            this.$notify({
+              title: 'Success',
+              message: '删除成功',
+              type: 'success',
+              duration: 2000
+            })
+            for (let i in this.testCaseDatas) {
+              let testCase = this.testCaseDatas[i]
+              if (testCase.defects.length > index) {
+                if (testCase.defects[index].id === row.id) {
+                  testCase.defects.splice(index, 1)
+                  break
+                }
+              }
+            }
+          } else {
+            this.$notify({
+              title: 'Error',
+              message: '删除失败' + res.msg,
+              type: 'Error',
+              duration: 2000
+            })
+          }
+        }).catch((error) => {
+          this.hideListLoading()
+          notify('error', '删除失败:' + error)
+        })
+      }).catch(() => {
+      })
+    },
+    handleCopy (row) {
+      this.dialogStatus = 'create'
+      this.dialogFormVisible = true
+      this.deepClone(this.testCaseData, row)
+      this.testCaseData.id = undefined
+      this.$nextTick(() => {
+        this.$refs['testCaseForm'].clearValidate()
+      })
+      console.log(this.testCaseData)
+    },
+    handleCopyDefect (row) {
+      this.dialogStatus = 'create_defect'
+      this.defectDialogFormVisible = true
+      this.deepClone(this.defectData, row)
+      this.defectData.id = undefined
+      this.$nextTick(() => {
+        this.$refs['defectForm'].clearValidate()
+      })
+      console.log(this.defectData)
+    },
+    getSortClass: function (key) {
+      const sort = this.listQuery.sort
+      return sort === `+${key}` ? 'ascending' : 'descending'
+    },
+    dataFilter (val) {
+      if (val) {
+        this.searchTasks = this.tasks.filter(task => {
+          return task.name.includes(val)
+        })
+      } else {
+        this.searchTasks = this.tasks
+      }
+    },
+    eqNoClick () {
+      this.searchTasks = this.tasks
+    },
+    getSimpleTaskDatas () {
+      Http.get(Api.TASK.GET_SIMPLE_USER_TASK_DATAS).then((res) => {
+        this.tasks = res.data
+        this.tasks.forEach(task => {
+          if (task.status === 4) {
+            task.name = task.name + '(已结束)'
+          } else if (task.status === 5) {
+            task.name = task.name + '(已超时)'
+          }
+        })
+        this.searchTasks = this.tasks
+        this.handleStatus()
+      }).catch((error) => {
+        this.hideLoading()
+        notify('error', '获取任务数据失败:' + error.data)
+      })
+    },
+    deepClone: function (oldData, newData) {
+      if (newData instanceof Array) {
+        if (oldData.length !== newData.length) {
+          oldData = new Array(newData.length)
+        }
+        for (var i = 0; i < newData.length; i++) {
+          if (newData[i] instanceof Array) {
+            oldData[i] = new Array(newData[i].length)
+            this.deepClone(oldData[i], newData[i])
+          } else if (newData[i] instanceof Object) {
+            oldData[i] = {}
+            this.deepClone(oldData[i], newData[i])
+          } else {
+            oldData[i] = newData[i]
+          }
+        }
+      } else if (newData instanceof Object) {
+        if (oldData.length > 0) {
+          oldData = {}
+        }
+        for (var key in newData) {
+          if (newData[key] instanceof Array) {
+            oldData[key] = new Array(newData[key].length)
+            this.deepClone(oldData[key], newData[key])
+          } else if (newData[key] instanceof Object) {
+            oldData[key] = {}
+            this.deepClone(oldData[key], newData[key])
+          } else {
+            oldData[key] = newData[key]
+          }
+        }
+      }
+      console.log('deepClone')
+      console.log(oldData)
+    },
+    showListLoading () {
+      this.listLoading = true
+    },
+    hideListLoading () {
+      this.listLoading = false
+    },
+    toogleExpand2 (row) {
+      let testCaseTable = this.$refs.testCaseTable
+      testCaseTable.toggleRowExpansion(row)
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .filter-container {
+    margin-bottom: 4px;
+    padding-left: 6px;
+  }
+  .mini-margin {
+    margin-left: 6px;
+    margin-right: 6px;
+  }
+</style>
+<style>
+.el-table__expanded-cell {
+  background-color: #eee!important;
+}
+.el-table__expanded-cell:hover {
+  background-color: #eee!important;
+}
+</style>

+ 73 - 0
src/pages/TestCase/utils/index.js

@@ -0,0 +1,73 @@
+export default {
+  toTestStatusCn (testStatus) {
+    var testStatusCn = ''
+    if (testStatus === 'WAIT') {
+      testStatusCn = '待测试'
+    } else if (testStatus === 'PASS') {
+      testStatusCn = '通过'
+    } else if (testStatus === 'NO_PASS') {
+      testStatusCn = '未通过'
+    }
+    return testStatusCn
+  },
+  toExamStatusCn (examStatus) {
+    var examStatusCn = ''
+    if (examStatus === 'WAIT') {
+      examStatusCn = '待审核'
+    } else if (examStatus === 'VALID') {
+      examStatusCn = '有效'
+    } else if (examStatus === 'INVALID') {
+      examStatusCn = '无效'
+    }
+    return examStatusCn
+  },
+  toPriorityCn (priority) {
+    var priorityCn = ''
+    if (priority === 'HIGH') {
+      priorityCn = '高'
+    } else if (priority === 'MID') {
+      priorityCn = '中'
+    } else if (priority === 'LOW') {
+      priorityCn = '低'
+    }
+    return priorityCn
+  },
+  toSeriousnessCn (seriousness) {
+    var seriousnessCn = ''
+    if (seriousness === 'VERY_HIGH') {
+      seriousnessCn = '极高'
+    } else if (seriousness === 'HIGH') {
+      seriousnessCn = '高'
+    } else if (seriousness === 'MID') {
+      seriousnessCn = '中'
+    } else if (seriousness === 'LOW') {
+      seriousnessCn = '低'
+    } else if (seriousness === 'VERY_LOW') {
+      seriousnessCn = '极低'
+    }
+    return seriousnessCn
+  },
+  toDefectTypeCn (defectType) {
+    var defectTypeCn = ''
+    if (defectType === 'FUNCTIONALITY') {
+      defectTypeCn = '功能性'
+    } else if (defectType === 'COMPATIBILITY') {
+      defectTypeCn = '兼容性'
+    } else if (defectType === 'INFORMATION_SECURITY') {
+      defectTypeCn = '信息安全性'
+    } else if (defectType === 'RELIABILITY') {
+      defectTypeCn = '可靠性'
+    } else if (defectType === 'USE') {
+      defectTypeCn = '易用性'
+    } else if (defectType === 'PERFORMANCE') {
+      defectTypeCn = '性能效率'
+    } else if (defectType === 'PORTABILITY') {
+      defectTypeCn = '可移植性'
+    } else if (defectType === 'MAINTAINABILITY') {
+      defectTypeCn = '维护性'
+    } else if (defectType === 'OTHER') {
+      defectTypeCn = '其他'
+    }
+    return defectTypeCn
+  }
+}

+ 53 - 0
src/pages/login/login.vue

@@ -0,0 +1,53 @@
+<template>
+  <el-form ref="loginForm" label-position="left" label-width="100px" style="width: 400px; margin-left:50px;">
+    <el-form-item label="邮箱" prop="email">
+      <el-input v-model="email"/>
+    </el-form-item>
+    <el-form-item label="密码" prop="password">
+      <el-input type="password" v-model="password"/>
+    </el-form-item>
+    <el-form-item>
+      <el-button type="primary" @click="login()">
+        登录
+      </el-button>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script>
+import Http from '@/js/http'
+import Api from '@/js/api'
+import {notify} from '@/constants'
+import router from '@/router'
+
+export default {
+  name: 'login',
+  data: function () {
+    return {
+      email: '',
+      password: ''
+    }
+  },
+  methods: {
+    login () {
+      const loginData = {
+        email: this.email,
+        password: this.password
+      }
+      Http.post(Api.LOGIN, loginData).then((res) => {
+        if (res.code === 20000) {
+          router.back()
+        } else {
+          notify('error', '登陆失败:' + res.msg)
+        }
+      }).catch((error) => {
+        notify('error', '登陆失败:' + error.data)
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+
+</style>

+ 97 - 84
src/router/index.js

@@ -27,7 +27,7 @@ export default new Router({
         title: '',
         requireAuth: false,
         showSlice: true
-      },
+      }
     },
     {
       path: '/home',
@@ -37,7 +37,7 @@ export default new Router({
         title: '',
         requireAuth: false,
         showSlice: true
-      },
+      }
     },
     // {
     //   path: '/home',
@@ -63,8 +63,8 @@ export default new Router({
       component: resolve => require(['@/components/Mine.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/project/create',
@@ -72,8 +72,8 @@ export default new Router({
       component: resolve => require(['@/components/project/ProjectCreate.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/project/:projectId',
@@ -81,8 +81,8 @@ export default new Router({
       component: resolve => require(['@/components/project/Project.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/project/:projectId/task/create',
@@ -90,8 +90,8 @@ export default new Router({
       component: resolve => require(['@/components/task/TaskCreate.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/project/:projectId/task/:taskId',
@@ -99,8 +99,8 @@ export default new Router({
       component: resolve => require(['@/components/task/Task.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/project/:projectId/analyse',
@@ -108,8 +108,8 @@ export default new Router({
       component: resolve => require(['@/components/project/AnalyseDemand.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/project/:projectId/report/create',
@@ -117,8 +117,8 @@ export default new Router({
       component: resolve => require(['@/components/report/ProjectReportCreate.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/project/:projectId/task/:taskId/report/create',
@@ -126,8 +126,8 @@ export default new Router({
       component: resolve => require(['@/components/report/TaskReportCreate.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/project/:projectId/task/:taskId/report/:reportId',
@@ -135,8 +135,8 @@ export default new Router({
       component: resolve => require(['@/components/report/TaskReport.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/project/:projectId/report/:reportId',
@@ -144,8 +144,8 @@ export default new Router({
       component: resolve => require(['@/components/report/ProjectReport.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/greenChannel/addProject',
@@ -153,8 +153,8 @@ export default new Router({
       component: resolve => require(['@/components/cheat/ProjectAdd.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/greenChannel/addAgency',
@@ -162,8 +162,8 @@ export default new Router({
       component: resolve => require(['@/components/cheat/AgencyAdd.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/authentication/individual/create',
@@ -171,8 +171,8 @@ export default new Router({
       component: resolve => require(['@/components/authen/IndividualAuthenticationCreate.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/authentication/enterprise/create',
@@ -180,8 +180,8 @@ export default new Router({
       component: resolve => require(['@/components/authen/EnterpriseAuthenticationCreate.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/authentication/agency/create',
@@ -189,8 +189,8 @@ export default new Router({
       component: resolve => require(['@/components/authen/AgencyAuthenticationCreate.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/authentication/individual/:userId',
@@ -198,8 +198,8 @@ export default new Router({
       component: resolve => require(['@/components/authen/IndividualAuthentication.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/authentication/enterprise/:userId',
@@ -207,8 +207,8 @@ export default new Router({
       component: resolve => require(['@/components/authen/EnterpriseAuthentication.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/authentication/agency/:userId',
@@ -216,8 +216,8 @@ export default new Router({
       component: resolve => require(['@/components/authen/AgencyAuthentication.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/authentication/index',
@@ -225,8 +225,8 @@ export default new Router({
       component: resolve => require(['@/components/authen/AuthenticationIndex.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/authentication/manage',
@@ -234,8 +234,8 @@ export default new Router({
       component: resolve => require(['@/components/authen/AuthenticationManage.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     // {
     //   path: '/agency/:userId',
@@ -252,8 +252,8 @@ export default new Router({
       component: resolve => require(['@/pages/Square/Square2.0.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/popular/list',
@@ -261,8 +261,8 @@ export default new Router({
       component: resolve => require(['@/pages/Square/PopularProjectAndTaskList.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
-      },
+        requireAuth: false
+      }
     },
     {
       path: '/technology',
@@ -270,7 +270,7 @@ export default new Router({
       component: resolve => require(['@/pages/Technology/Technology2.0.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -279,7 +279,7 @@ export default new Router({
       component: resolve => require(['@/pages/Technology/TechnologyMore.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -288,7 +288,7 @@ export default new Router({
       component: resolve => require(['@/pages/HomepageSearch/ExpertList.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -297,7 +297,7 @@ export default new Router({
       component: resolve => require(['@/pages/HomepageSearch/AgencyList.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -306,7 +306,7 @@ export default new Router({
       component: resolve => require(['@/pages/HomepageSearch/AgencyResidentList.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -315,7 +315,7 @@ export default new Router({
       component: resolve => require(['@/pages/HomepageSearch/CompetitionList.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -324,7 +324,7 @@ export default new Router({
       component: resolve => require(['@/pages/HomepageSearch/CrowdList.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -333,7 +333,7 @@ export default new Router({
       component: resolve => require(['@/pages/HomepageSearch/UserList.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -342,7 +342,7 @@ export default new Router({
       component: resolve => require(['@/pages/HomepageSearch/ResourceList.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -351,7 +351,7 @@ export default new Router({
       component: resolve => require(['@/pages/DetailPage/CrowdDetail.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -360,7 +360,7 @@ export default new Router({
       component: resolve => require(['@/pages/DetailPage/NewAgencyDetail.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -369,7 +369,7 @@ export default new Router({
       component: resolve => require(['@/pages/DetailPage/UserDetail.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -378,7 +378,7 @@ export default new Router({
       component: resolve => require(['@/pages/DetailPage/ResourceDetail.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -387,7 +387,7 @@ export default new Router({
       component: resolve => require(['@/pages/DetailPage/ExpertDetail.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -396,7 +396,7 @@ export default new Router({
       component: resolve => require(['@/pages/DetailPage/FieldDetail.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -405,7 +405,7 @@ export default new Router({
       component: resolve => require(['@/pages/DetailPage/ApplicationTypeDetail.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -414,7 +414,7 @@ export default new Router({
       component: resolve => require(['@/pages/DetailPage/TestTypeDetail.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       }
     },
     {
@@ -423,37 +423,37 @@ export default new Router({
       component: resolve => require(['@/pages/UserCenter/UserCenter.vue'], resolve),
       meta: {
         title: '',
-        requireAuth: false,
+        requireAuth: false
       },
       children: [
         {
           path: '/personal/mine',
-          component: resolve => require(['@/pages/UserCenter/Mine.vue'], resolve),
+          component: resolve => require(['@/pages/UserCenter/Mine.vue'], resolve)
         },
         {
           path: '/personal/qualification',
-          component: resolve => require(['@/pages/UserCenter/MyQualification.vue'], resolve),
+          component: resolve => require(['@/pages/UserCenter/MyQualification.vue'], resolve)
         },
         {
           path: '/personal/bankCard',
-          component: resolve => require(['@/pages/UserCenter/MyBankCard.vue'], resolve),
+          component: resolve => require(['@/pages/UserCenter/MyBankCard.vue'], resolve)
         },
         {
           path: '/personal/modifyPsw',
-          component: resolve => require(['@/pages/UserCenter/ModifyPsw.vue'], resolve),
+          component: resolve => require(['@/pages/UserCenter/ModifyPsw.vue'], resolve)
         },
         {
           path: '/personal/phoneBinding',
           component: resolve => require(['@/pages/UserCenter/PhoneBinding.vue'], resolve),
           children: [
             {
-              path:'/personal/phoneBinding/binding',
-              component:resolve => require(['@/pages/UserCenter/BindingMobile.vue'], resolve),
+              path: '/personal/phoneBinding/binding',
+              component: resolve => require(['@/pages/UserCenter/BindingMobile.vue'], resolve)
             },
             {
-              path:'/personal/phoneBinding/rebinding',
-              component:resolve => require(['@/pages/UserCenter/ReBindingMobile.vue'], resolve),
-            },
+              path: '/personal/phoneBinding/rebinding',
+              component: resolve => require(['@/pages/UserCenter/ReBindingMobile.vue'], resolve)
+            }
           ]
         },
         {
@@ -461,26 +461,26 @@ export default new Router({
           component: resolve => require(['@/pages/UserCenter/MailBinding.vue'], resolve),
           children: [
             {
-              path:'/personal/mailBinding/binding',
-              component:resolve => require(['@/pages/UserCenter/BindingMail.vue'], resolve),
+              path: '/personal/mailBinding/binding',
+              component: resolve => require(['@/pages/UserCenter/BindingMail.vue'], resolve)
             },
             {
-              path:'/personal/mailBinding/rebinding',
-              component:resolve => require(['@/pages/UserCenter/ReBindingMail.vue'], resolve),
-            },
+              path: '/personal/mailBinding/rebinding',
+              component: resolve => require(['@/pages/UserCenter/ReBindingMail.vue'], resolve)
+            }
           ]
         },
         {
           path: '/personal/authentication',
-          component: resolve => require(['@/pages/UserCenter/Authentication.vue'], resolve),
+          component: resolve => require(['@/pages/UserCenter/Authentication.vue'], resolve)
         },
         {
           path: '/personal/authentication/enterprise',
-          component: resolve => require(['@/pages/UserCenter/EnterpriseAuth.vue'], resolve),
+          component: resolve => require(['@/pages/UserCenter/EnterpriseAuth.vue'], resolve)
         },
         {
           path: '/personal/authentication/individual',
-          component: resolve => require(['@/pages/UserCenter/IndividualAuth.vue'], resolve),
+          component: resolve => require(['@/pages/UserCenter/IndividualAuth.vue'], resolve)
         },
         {
           path: '',
@@ -489,10 +489,23 @@ export default new Router({
       ]
     },
     {
-      path:'/statistics',
-      component: resolve => require(['@/pages/Statistics/StatisticsReport.vue'], resolve),
+      path: '/statistics',
+      component: resolve => require(['@/pages/Statistics/StatisticsReport.vue'], resolve)
+    },
+    {
+      path: '/testcases/:taskCode',
+      name: 'TestCases',
+      component: resolve => require(['@/pages/TestCase/testcases.vue'], resolve)
+    },
+    {
+      path: '/examtestcases/:projectCode/:taskCode/:userId',
+      name: 'ExamTestCases',
+      component: resolve => require(['@/pages/TestCase/exam_testcases.vue'], resolve)
+    },
+    {
+      path: '/login',
+      component: resolve => require(['@/pages/login/login.vue'], resolve)
     }
-
   ]
 })
 // const originalPush = Router.prototype.push

+ 117 - 0
src/utils/index.js

@@ -0,0 +1,117 @@
+/**
+ * Created by PanJiaChen on 16/11/18.
+ */
+
+/**
+ * Parse the time to string
+ * @param {(Object|string|number)} time
+ * @param {string} cFormat
+ * @returns {string | null}
+ */
+export function parseTime(time, cFormat) {
+  if (arguments.length === 0 || !time) {
+    return null
+  }
+  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
+  let date
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if ((typeof time === 'string')) {
+      if ((/^[0-9]+$/.test(time))) {
+        // support "1548221490638"
+        time = parseInt(time)
+      } else {
+        // support safari
+        // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
+        time = time.replace(new RegExp(/-/gm), '/')
+      }
+    }
+
+    if ((typeof time === 'number') && (time.toString().length === 10)) {
+      time = time * 1000
+    }
+    date = new Date(time)
+  }
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  }
+  const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
+    const value = formatObj[key]
+    // Note: getDay() returns 0 on Sunday
+    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
+    return value.toString().padStart(2, '0')
+  })
+  return time_str
+}
+
+/**
+ * @param {number} time
+ * @param {string} option
+ * @returns {string}
+ */
+export function formatTime(time, option) {
+  if (('' + time).length === 10) {
+    time = parseInt(time) * 1000
+  } else {
+    time = +time
+  }
+  const d = new Date(time)
+  const now = Date.now()
+
+  const diff = (now - d) / 1000
+
+  if (diff < 30) {
+    return '刚刚'
+  } else if (diff < 3600) {
+    // less 1 hour
+    return Math.ceil(diff / 60) + '分钟前'
+  } else if (diff < 3600 * 24) {
+    return Math.ceil(diff / 3600) + '小时前'
+  } else if (diff < 3600 * 24 * 2) {
+    return '1天前'
+  }
+  if (option) {
+    return parseTime(time, option)
+  } else {
+    return (
+      d.getMonth() +
+      1 +
+      '月' +
+      d.getDate() +
+      '日' +
+      d.getHours() +
+      '时' +
+      d.getMinutes() +
+      '分'
+    )
+  }
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function param2Obj(url) {
+  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
+  if (!search) {
+    return {}
+  }
+  const obj = {}
+  const searchArr = search.split('&')
+  searchArr.forEach(v => {
+    const index = v.indexOf('=')
+    if (index !== -1) {
+      const name = v.substring(0, index)
+      const val = v.substring(index + 1, v.length)
+      obj[name] = val
+    }
+  })
+  return obj
+}

+ 58 - 0
src/utils/scroll-to.js

@@ -0,0 +1,58 @@
+Math.easeInOutQuad = function (t, b, c, d) {
+  t /= d / 2
+  if (t < 1) {
+    return c / 2 * t * t + b
+  }
+  t--
+  return -c / 2 * (t * (t - 2) - 1) + b
+}
+
+// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
+var requestAnimFrame = (function () {
+  return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60) }
+})()
+
+/**
+ * Because it's so fucking difficult to detect the scrolling element, just move them all
+ * @param {number} amount
+ */
+function move (amount) {
+  document.documentElement.scrollTop = amount
+  document.body.parentNode.scrollTop = amount
+  document.body.scrollTop = amount
+}
+
+function position () {
+  return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
+}
+
+/**
+ * @param {number} to
+ * @param {number} duration
+ * @param {Function} callback
+ */
+export function scrollTo (to, duration, callback) {
+  const start = position()
+  const change = to - start
+  const increment = 20
+  let currentTime = 0
+  duration = (typeof (duration) === 'undefined') ? 500 : duration
+  var animateScroll = function () {
+    // increment the time
+    currentTime += increment
+    // find the value with the quadratic in-out easing function
+    var val = Math.easeInOutQuad(currentTime, start, change, duration)
+    // move the document.body
+    move(val)
+    // do the animation unless its over
+    if (currentTime < duration) {
+      requestAnimFrame(animateScroll)
+    } else {
+      if (callback && typeof (callback) === 'function') {
+        // the animation is done so lets callback
+        callback()
+      }
+    }
+  }
+  animateScroll()
+}

Some files were not shown because too many files changed in this diff