浏览代码

Merge branch 'demand' into http_mock

reedliuqing_i 5 年之前
父节点
当前提交
d4c26065fa

+ 2 - 1
package.json

@@ -15,10 +15,11 @@
     "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
   },
   "dependencies": {
+    "animate.css": "^3.7.2",
     "axios": "0.18.0",
     "dayjs": "^1.8.17",
     "echarts": "^4.2.1",
-    "element-ui": "^2.9.1",
+    "element-ui": "^2.13.0",
     "file-saver": "^2.0.2",
     "html2canvas": "^1.0.0-rc.3",
     "jodit": "^3.2.58",

+ 107 - 0
src/api/requirement.js

@@ -0,0 +1,107 @@
+import request from '@/utils/request'
+
+const requestIp = 'http://10.179.24.176:8990'
+
+export function getRequirement(data) {
+  return request({
+    url: requestIp + '/requirement/queryRequirementInfoList',
+    method: 'post',
+    data
+  })
+}
+
+export function showRequirementEnum() {
+  return request({
+    url: requestIp + '/config/showRequirementEnum',
+    method: 'get'
+  })
+}
+
+export function getMemberInfo(data) {
+  return request({
+    url: requestIp + '/member/queryMemberInfoByIDAPorName',
+    method: 'post',
+    data
+  })
+}
+
+export function createRequirement(data) {
+  return request({
+    url: requestIp + '/requirement/createRequirement',
+    method: 'post',
+    data
+  })
+}
+
+export function updateRequirement(data) {
+  return request({
+    url: requestIp + '/requirement/updateRequirement',
+    method: 'post',
+    data
+  })
+}
+
+export function updateRequirementStatus(data) {
+  return request({
+    url: requestIp + '/requirement/updateRequirementStatus',
+    method: 'post',
+    data
+  })
+}
+
+export function deleteRequirement(data) {
+  return request({
+    url: requestIp + '/requirement/deleteRequirementById',
+    method: 'post',
+    data
+  })
+}
+
+export function getRequirementById(data) {
+  return request({
+    url: requestIp + '/requirement/queryOneRequirementDetail',
+    method: 'post',
+    data
+  })
+}
+
+export function getTaskStatusMapInfo(data) {
+  return request({
+    url: requestIp + '/task/getTaskStatusMapInfo',
+    method: 'post',
+    data
+  })
+}
+
+export function getBugStatusMapInfo(data) {
+  return request({
+    url: requestIp + '/bug/getBugStatusMapInfo',
+    method: 'post',
+    data
+  })
+}
+
+export function getTaskByRequireId(data) {
+  return request({
+    url: requestIp + '/task/listByTaskId',
+    method: 'get',
+    params: data
+  })
+}
+
+export function getCommentList(data) {
+  return request({
+    url: requestIp + '/comment/list',
+    method: 'post',
+    data
+  })
+}
+
+export function addComment(data) {
+  return request({
+    url: requestIp + '/comment/create',
+    method: 'post',
+    data
+  })
+}
+

+ 27 - 0
src/api/version.js

@@ -0,0 +1,27 @@
+import request from '@/utils/request'
+
+const requestIp = 'http://10.179.24.176:8990'
+
+export function getVersionHomePageList(data) {
+  return request({
+    url: requestIp + '/Version/versionHomePageList',
+    method: 'post',
+    data
+  })
+}
+
+export function showAppClientEnum(data) {
+  return request({
+    url: requestIp + '/config/showAppClientEnum',
+    method: 'get',
+    data
+  })
+}
+
+export function showVersionEnum(data) {
+  return request({
+    url: requestIp + '/config/showVersionEnum',
+    method: 'get',
+    data
+  })
+}

+ 48 - 0
src/components/chart/index.vue

@@ -0,0 +1,48 @@
+<template>
+  <div :id="chartId" style="width: 100%;height: 160px" />
+</template>
+
+<script>
+import echarts from 'echarts'
+
+export default {
+  name: 'Hello',
+  props: {
+    chartId: {
+      type: String,
+      default: 'myChart'
+    },
+    option: {
+      type: Object,
+      default() {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      msg: 'Welcome to Your Vue.js App'
+    }
+  },
+  watch: {
+    option(newValue, oldValue) {
+      this.drawLine()
+    }
+  },
+  mounted() {
+    this.drawLine()
+  },
+  methods: {
+    drawLine() {
+      // 基于准备好的dom,初始化echarts实例
+      const myChart = echarts.init(document.getElementById(this.chartId))
+      // 绘制图表
+      myChart.setOption(this.option)
+    }
+  }
+}
+</script>
+
+<style>
+
+</style>

+ 1 - 0
src/layout/components/AppMain.vue

@@ -26,6 +26,7 @@ export default {
   width: 100%;
   position: relative;
   overflow: hidden;
+  background-color: #F2F3F6;
 }
 .fixed-header+.app-main {
   padding-top: 80px;

+ 2 - 0
src/main.js

@@ -17,6 +17,8 @@ import '@/icons' // icon
 import htmlToPdf from '@/utils/htmlToPdf'
 Vue.use(htmlToPdf)
 
+import animated from 'animate.css'
+Vue.use(animated)
 /**
  * If you don't want to use mock-server
  * you want to use MockJs for mock api

+ 20 - 0
src/router/index.js

@@ -158,6 +158,26 @@ export const constantRoutes = [
             name: '任务更新',
             component: () => import('@/views/projectManage/taskList/taskUpdateCreate'),
             meta: { title: '任务更新' }
+          },
+          {
+            path: 'requirement',
+            name: '需求',
+            component: () => import('@/views/projectManage/requirement/list/index.vue'),
+            meta: { title: '需求' }
+          },
+          {
+            path: 'requirementDetails/:id',
+            name: '需求详情',
+            hidden: true,
+            props: true,
+            component: () => import('@/views/projectManage/requirement/details/index.vue'),
+            meta: { title: '需求详情' }
+          },
+          {
+            path: 'version',
+            name: '版本',
+            component: () => import('@/views/projectManage/version/list/index.vue'),
+            meta: { title: '版本' }
           }
         ]
       },

+ 2 - 1
src/store/getters.js

@@ -5,6 +5,7 @@ const getters = {
   avatar: state => state.user.avatar,
   name: state => state.user.name,
   menu: state => state.data.menu,
-  subMenu: state => state.data.subMenu
+  subMenu: state => state.data.subMenu,
+  project: state => state.project.project
 }
 export default getters

+ 3 - 1
src/store/index.js

@@ -5,6 +5,7 @@ import app from './modules/app'
 import settings from './modules/settings'
 import user from './modules/user'
 import data from './modules/data'
+import project from './modules/project'
 
 Vue.use(Vuex)
 
@@ -13,7 +14,8 @@ const store = new Vuex.Store({
     app,
     settings,
     user,
-    data
+    data,
+    project
   },
   getters
 })

+ 23 - 0
src/store/modules/project.js

@@ -0,0 +1,23 @@
+const state = {
+  project: {}
+}
+
+const mutations = {
+  SETPROJECT(state, value) {
+    state.project = value
+  }
+}
+
+const actions = {
+  setProject({ commit }, value) {
+    commit('SETPROJECT', value)
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  mutations,
+  actions
+}
+

+ 646 - 0
src/views/projectManage/requirement/details/index.vue

@@ -0,0 +1,646 @@
+<template>
+  <el-container>
+    <el-header style="margin: 2%;" class="layout_header">
+      <div>
+        <span style="font-size: 16px;color:#333333">{{ '需求:' + requirement.name }}</span>
+        <el-dropdown
+          size="mini"
+          split-button
+          style="margin-left: 10px"
+          @command="updateRequirementStatus"
+        >
+          <span class="el-dropdown-link">{{ getStatusName() }}</span>
+          <el-dropdown-menu slot="dropdown">
+            <el-dropdown-item
+              v-for="(item,index) in searchInfo.requirementStatus"
+              :key="index"
+              :command="item"
+            >{{ item.msg }}</el-dropdown-item>
+          </el-dropdown-menu>
+        </el-dropdown>
+        <div style="display: inline-block;float: right">
+          <el-button size="mini" style="margin-left: 10px" @click="deleteRequirement">删除需求</el-button>
+          <el-button type="primary" size="mini" @click="createTask">新建</el-button>
+        </div>
+      </div>
+    </el-header>
+    <el-container>
+      <el-aside width="52%" style="margin: 0% 2% 2% 2%;" class="layout_aside">
+        <div style="font-size: 18px">
+          <b style="color: #409EFF">I</b>数据统计
+        </div>
+        <div v-loading="loading.task || loading.bug" class="div_statistics" style="margin: 2%">
+          <el-container style="border-bottom: 1px solid #D8D8D8">
+            <el-aside width="30%" style="border-right: 2px solid #BBBBBB">
+              <div style="width:100%;text-align: center;">
+                <div style="font-size: 16px;margin-top: 26px">任务数量</div>
+                <div style="font-size: 72px">{{ statistics.task.totalCount }}</div>
+                <div
+                  style="font-size: 14px;color: rgba(245,108,108,1);margin-bottom: 16px"
+                >{{ '已延期'+statistics.task.delayCount+'个' }}</div>
+              </div>
+            </el-aside>
+            <el-aside width="70%">
+              <requirement-chart
+                :chart-id="'taskCount'"
+                :option="taskOption"
+                style="position: relative;bottom: 30px"
+              />
+            </el-aside>
+          </el-container>
+          <el-container>
+            <el-aside width="30%" style="border-right: 2px solid #BBBBBB">
+              <div style="width:100%;text-align: center;">
+                <div style="font-size: 16px;margin-top: 26px">任务数量</div>
+                <div style="font-size: 72px">{{ statistics.bug.totalCount }}</div>
+                <div
+                  style="font-size: 14px;color: rgba(245,108,108,1);margin-bottom: 16px"
+                >{{ '以后修复'+statistics.bug.fixInFutureCount+'个' }}</div>
+              </div>
+            </el-aside>
+            <el-aside width="70%">
+              <requirement-chart
+                :chart-id="'bugCount'"
+                :option="bugOption"
+                style="position: relative;bottom: 30px"
+              />
+            </el-aside>
+          </el-container>
+        </div>
+      </el-aside>
+      <el-aside width="42%" style="margin: 0% 2% 2% 0;" class="layout_aside">
+        <div style="font-size: 18px">
+          <b style="color: #409EFF;">I</b>基础信息
+        </div>
+        <div
+          v-loading="loading.info"
+          style="font-size: 14px;color: #666666;margin-top: 4%"
+          class="div_requirment_info"
+        >
+          <el-row>
+            <el-col :span="6">归属的项目:</el-col>
+            <el-col :span="18">
+              <div
+                style="display: inline-block;color: #409EFF"
+              >{{ requirement.belongingProjectName }}</div>
+              <el-button
+                style="position: absolute;top: 5px;right: 0"
+                type="primary"
+                size="mini"
+                @click="updateDialogVisible = true"
+              >修改</el-button>
+            </el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="6">业务线:</el-col>
+            <el-col :span="18">{{ requirement.bizName }}</el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="6">优先级:</el-col>
+            <el-col :span="18">{{ requirement.priorityName }}</el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="6">需求来源:</el-col>
+            <el-col :span="18">{{ requirement.sourceTypeName }}</el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="6">PM:</el-col>
+            <el-col :span="18">{{ getPmName() }}</el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="6">PRD链接:</el-col>
+            <el-col :span="18">
+              <el-link type="primary" :href="requirement.mrdUrl">{{ requirement.mrdUrl }}</el-link>
+            </el-col>
+          </el-row>
+          <el-row>
+            <el-col :span="6">是否跟版:</el-col>
+            <el-col :span="18">{{ requirement.dependOnRelease? '是':'否' }}</el-col>
+          </el-row>
+          <el-row v-if="requirement.dependOnRelease">
+            <el-col :span="6">涉及的客户端:</el-col>
+            <el-col :span="18">{{ getAppClientName() }}</el-col>
+          </el-row>
+        </div>
+      </el-aside>
+    </el-container>
+    <el-main id="requirement_details" style="margin: 0 2% 0 2%;padding: 0" class="layout_main">
+      <div style="font-size: 18px;margin: 2%">
+        <b style="color: #409EFF;">I</b>任务
+      </div>
+      <el-radio-group v-model="radio" size="mini" style="float: right;margin: 0 1% 1% 0">
+        <el-radio-button label="列表" />
+        <el-radio-button label="甘特图" />
+      </el-radio-group>
+      <br>
+      <el-table
+        v-if="radio === '列表'"
+        v-loading="loading.table"
+        :data="tableData"
+        style="width: 100%;"
+        highlight-current-row
+        :header-cell-style="{ background: '#6AB4FF', color: '#FFFFFF',textAlign: 'center'}"
+        :cell-style="{textAlign: 'center'}"
+      >
+        <el-table-column prop="name" label="任务名" min-width="20%">
+          <template v-slot="scope">
+            <div style="cursor: pointer;" @click="getToTaskDetails(scope.row.id)">{{ scope.row.name }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="beginTime" label="排期" min-width="15%">
+          <template
+            v-slot="scope"
+          >{{ getSchedule(scope.row) }}</template>
+        </el-table-column>
+        <el-table-column prop="statusString" label="状态" min-width="10%">
+          <template v-slot="scope">
+            <div style="color: #FF9500">{{ scope.row.statusString }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="stageString" label="任务健康状态" min-width="12%">
+          <template v-slot="scope">{{ scope.row.stageString }}</template>
+        </el-table-column>
+        <el-table-column prop="rate" label="任务进展" min-width="15%">
+          <template v-slot="scope">
+            <el-progress :percentage="Number(scope.row.rate?scope.row.rate.replace(/%/g,''):0)" />
+          </template>
+        </el-table-column>
+        <el-table-column prop="rdObject" label="开发负责人" min-width="10%">
+          <template v-slot="scope">{{ scope.row.rdObject?scope.row.rdObject.name:'空' }}</template>
+        </el-table-column>
+        <el-table-column prop="qaObject" label="测试负责人" min-width="10%">
+          <template v-slot="scope">{{ scope.row.qaObject?scope.row.qaObject.name:'空' }}</template>
+        </el-table-column>
+        <el-table-column prop="rdList" label="开发" min-width="10%">
+          <template v-slot="scope">{{ getQaOrRdNameList(scope.row.rdList) }}</template>
+        </el-table-column>
+        <el-table-column prop="qaList" label="测试" min-width="10%">
+          <template v-slot="scope">{{ getQaOrRdNameList(scope.row.qaList) }}</template>
+        </el-table-column>
+      </el-table>
+      <div style="margin:0 2%;padding: 2% 0;border-bottom: 1px solid #D8D8D8;font-size: 14px;color: #333333">排期汇总:{{ getScheduleCollect() }}</div>
+      <el-row style="margin: 2%;font-size: 14px;color: #333333">
+        <el-col :span="8">
+          <div v-for="(item,index) in task.preOnlineVersion" :key="index" style="margin-bottom:10px"> <span :style="{visibility: index===0?'visible ':'hidden'}">预期上线版本:</span>{{ item }}</div>
+        </el-col>
+        <el-col :span="16">
+          <div v-for="(item,index) in task.preOnlineVersion" :key="index" style="margin-bottom:10px"> <span :style="{visibility: index===0?'visible ':'hidden'}">实际上线版本:</span>{{ item }}</div>
+        </el-col>
+      </el-row>
+    </el-main>
+    <el-main style="margin: 2%;" class="layout_main requirement_details_layout_main">
+      <div style="font-size: 18px;margin-bottom: 2%">
+        <b style="color: #409EFF;">I</b>评论
+      </div>
+      <div>
+        <div v-for="(item,index) in comments" :key="index" class="animated bounceInRight">
+          <div
+            style="font-size:14px;color:#333B4A;display: inline-block;"
+          >{{ item.commentInfo.name }}</div>
+          <div
+            style="margin-left:20px;display: inline-block;color: #9B9B9B;font-size:12px"
+          >{{ item.commentInfo.gmtCreater }}</div>
+          <p
+            style="font-size:14px;color:#333B4A;margin-top: 10px;white-space: pre-line;"
+          >{{ item.commentInfo.content }}</p>
+          <br>
+        </div>
+        <el-input
+          v-model="commentContent"
+          type="textarea"
+          placeholder="请输入评论内容"
+          maxlength="300"
+          show-word-limit
+          :autosize="{ minRows: 3, maxRows: 5}"
+          style="margin: 2% 0;"
+        />
+        <el-button type="primary" size="small" style="float: right" @click="addComment">发表评论</el-button>
+      </div>
+    </el-main>
+    <requirement-update
+      title="编辑需求"
+      :data="requirement"
+      :visible="updateDialogVisible"
+      :info="searchInfo"
+      @cancel="updateDialogVisible=false"
+      @confirm="getRequirementById();updateDialogVisible=false"
+    />
+  </el-container>
+</template>
+
+<script>
+import {
+  getRequirementById,
+  showRequirementEnum,
+  getTaskStatusMapInfo,
+  getBugStatusMapInfo,
+  getTaskByRequireId,
+  deleteRequirement,
+  updateRequirementStatus,
+  getCommentList,
+  addComment
+} from '@/api/requirement.js'
+import RequirementChart from '@/components/chart/index.vue'
+import RequirementUpdate from '@/views/projectManage/requirement/list/create.vue'
+
+export default {
+  components: {
+    RequirementChart,
+    RequirementUpdate
+  },
+  props: {
+    id: {
+      type: String,
+      default: '0'
+    }
+  },
+  data() {
+    return {
+      loading: {
+        table: true,
+        info: true,
+        task: true,
+        bug: true
+      },
+      radio: '列表',
+      updateDialogVisible: false,
+      tableData: null,
+      statistics: {
+        task: {
+          totalCount: 0,
+          delayCount: 0
+        },
+        bug: {
+          totalCount: 0,
+          fixInFutureCount: 0
+        }
+      },
+      belongingProject: { msg: '不存在' },
+      status: {},
+      requirement: {
+        name: ''
+      },
+      projectOb: {},
+      bizTypeOb: {},
+      appClientOb: {},
+      requirementStatusOb: {},
+      searchInfo: {
+        belongingProject: [],
+        sourceType: [],
+        bizType: [],
+        priority: [],
+        requirementStatus: []
+      },
+      taskOption: {
+        color: ['#69B3FF'],
+        title: {
+          show: false
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            // 坐标轴指示器,坐标轴触发有效
+            type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
+          }
+        },
+        grid: {
+          left: '3%',
+          right: '4%',
+          bottom: '0%',
+          containLabel: true
+        },
+        xAxis: [
+          {
+            type: 'category',
+            data: ['未开始', '开发中', '测试中', '已上线'],
+            axisTick: {
+              alignWithLabel: true,
+              show: false
+            },
+            axisLine: {
+              show: true,
+              lineStyle: {
+                type: 'dashed'
+              }
+            }
+          }
+        ],
+        yAxis: [
+          {
+            type: 'value',
+            axisTick: {
+              show: false
+            },
+            axisLine: {
+              show: false
+            },
+            axisLabel: {
+              show: false
+            },
+            splitLine: {
+              show: false
+            }
+          }
+        ],
+        series: [
+          {
+            type: 'bar',
+            barWidth: '60%',
+            label: {
+              show: true,
+              position: 'inside'
+            },
+            data: []
+          }
+        ]
+      },
+      bugOption: {
+        color: ['#69B3FF'],
+        title: {
+          show: false
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            // 坐标轴指示器,坐标轴触发有效
+            type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
+          }
+        },
+        grid: {
+          left: '3%',
+          right: '4%',
+          bottom: '0%',
+          containLabel: true
+        },
+        xAxis: [
+          {
+            type: 'category',
+            data: ['待开发', '开发中', '待测试', '已完成', 'Reopen'],
+            axisTick: {
+              alignWithLabel: true,
+              show: false
+            },
+            axisLine: {
+              show: true,
+              lineStyle: {
+                type: 'dashed'
+              }
+            }
+          }
+        ],
+        yAxis: [
+          {
+            type: 'value',
+            axisTick: {
+              show: false
+            },
+            axisLine: {
+              show: false
+            },
+            axisLabel: {
+              show: false
+            },
+            splitLine: {
+              show: false
+            }
+          }
+        ],
+        series: [
+          {
+            type: 'bar',
+            barWidth: '60%',
+            label: {
+              show: true,
+              position: 'inside'
+            },
+            data: []
+          }
+        ]
+      },
+      comments: [],
+      commentContent: null,
+      task: {
+        reqStartTime: null,
+        reqEndTime: null,
+        preOnlineVersion: []
+      }
+    }
+  },
+  mounted() {
+    this.showRequirementEnum().then(res => {
+      this.getRequirementById()
+    })
+    this.getTaskStatusMapInfo()
+    this.getBugStatusMapInfo()
+    this.getTaskByRequireId()
+    this.getCommentList()
+  },
+  methods: {
+    getScheduleCollect() {
+      if (this.task.reqStartTime && this.task.reqEndTime) {
+        return this.task.reqStartTime.substring(0, 10).replace(/-/, '年').replace(/-/, '月') + '日' + ' ~ ' + this.task.reqEndTime.substring(0, 10).replace(/-/, '年').replace(/-/, '月') + '日'
+      }
+      return ''
+    },
+    getSchedule(row) {
+      if (row.beginTime && row.endTime) {
+        return row.beginTime.substring(2, 10).replace(/-/g, '/') + ' ~ ' + row.endTime.substring(2, 10).replace(/-/g, '/')
+      }
+      return '无'
+    },
+    getCommentList() {
+      getCommentList({ type: 4, joinId: this.id }).then(res => {
+        this.comments = res.data
+        this.commentContent = ''
+      })
+    },
+    addComment() {
+      if (!this.commentContent) {
+        this.$message.warning('评论不能为空')
+        return
+      }
+      const user = localStorage.getItem('username')
+      addComment({
+        commentInfo: { joinId: this.id, type: 4, content: this.commentContent },
+        user: { ename: user }
+      }).then(res => {
+        if (res.code === 200) {
+          this.getCommentList()
+        } else {
+          this.$message.warning(res.msg)
+        }
+      })
+    },
+    getStatusName() {
+      return this.requirementStatusOb[this.requirement.status]
+    },
+    getRequirementById() {
+      this.loading.info = true
+      getRequirementById({
+        id: this.id
+      }).then(res => {
+        this.requirement = res.data
+        this.loading.info = false
+      })
+    },
+    getTaskStatusMapInfo() {
+      getTaskStatusMapInfo({
+        requireId: this.id
+      }).then(res => {
+        const name = []
+        const data = []
+        for (const i in res.data.statusInfoList) {
+          name.push(res.data.statusInfoList[i].statusString)
+          data.push(res.data.statusInfoList[i].count)
+        }
+        this.statistics.task = res.data
+        this.taskOption.xAxis[0].data = name
+        this.taskOption.series[0].data = data
+        this.taskOption = JSON.parse(JSON.stringify(this.taskOption))
+        this.loading.task = false
+      })
+    },
+    getBugStatusMapInfo() {
+      getBugStatusMapInfo({
+        requireId: this.id
+      }).then(res => {
+        const name = []
+        const data = []
+        for (const i in res.data.statusInfoList) {
+          name.push(res.data.statusInfoList[i].statusString)
+          data.push(res.data.statusInfoList[i].count)
+        }
+        this.statistics.bug = res.data
+        this.bugOption.xAxis[0].data = name
+        this.bugOption.series[0].data = data
+        this.bugOption = JSON.parse(JSON.stringify(this.bugOption))
+        this.loading.bug = false
+      })
+    },
+    getTaskByRequireId() {
+      getTaskByRequireId({
+        id: this.id
+      }).then(res => {
+        this.task = res.data
+        this.tableData = res.data.taskDetails
+        this.loading.table = false
+      })
+    },
+    getPmName() {
+      const names = []
+      for (const i in this.requirement.pm) {
+        names.push(this.requirement.pm[i].name)
+      }
+      return names.join(',')
+    },
+    getAppClientName() {
+      const names = []
+      for (const i in this.requirement.referredClientType) {
+        names.push(this.appClientOb[this.requirement.referredClientType[i]])
+      }
+      return names.join('\n')
+    },
+    showRequirementEnum() {
+      return showRequirementEnum().then(res => {
+        this.searchInfo = res.data
+        for (const i in this.searchInfo.belongingProject) {
+          this.projectOb[
+            this.searchInfo.belongingProject[i].code
+          ] = this.searchInfo.belongingProject[i].msg
+        }
+        for (const i in this.searchInfo.bizType) {
+          this.bizTypeOb[
+            this.searchInfo.bizType[i].code
+          ] = this.searchInfo.bizType[i].msg
+        }
+        for (const i in this.searchInfo.appClient) {
+          this.appClientOb[
+            this.searchInfo.appClient[i].code
+          ] = this.searchInfo.appClient[i].msg
+        }
+        for (const i in this.searchInfo.requirementStatus) {
+          this.requirementStatusOb[
+            this.searchInfo.requirementStatus[i].code
+          ] = this.searchInfo.requirementStatus[i].msg
+        }
+      })
+    },
+    deleteRequirement() {
+      const user = localStorage.getItem('username')
+      this.$confirm('此操作将永久删除该需求, 是否继续?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(() => {
+          deleteRequirement({
+            id: this.id,
+            modifier: user
+          }).then(res => {
+            if (res.code === 200) {
+              this.$router.push({ name: '需求' })
+            } else {
+              this.$message.warning(res.msg)
+            }
+          })
+        })
+        .catch(() => {})
+    },
+    createTask() {},
+    updateRequirementStatus(status) {
+      const modifier = localStorage.getItem('username')
+      updateRequirementStatus({ id: this.id, status: status.code, modifier: modifier }).then(
+        res => {
+          if (res.code === 200) {
+            this.getRequirementById()
+          } else {
+            this.$message.warning(res.msg)
+          }
+        }
+      )
+    },
+    getQaOrRdNameList(list) {
+      const arr = []
+      for (const i in list) {
+        arr.push(list[i].name)
+      }
+      return arr.join(',')
+    },
+    getToTaskDetails(id) {
+      // this.$router.push({ name: '任务', params: { id: id + '' }})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.div_requirment_info .el-row .el-col {
+  margin: 10px 0;
+}
+.layout_header,
+.layout_aside,
+.layout_main {
+  border-radius: 8px;
+  background-color: #ffffff;
+}
+.div_statistics > .el-container {
+  margin: 0;
+}
+.layout_aside,
+.layout_main {
+  padding: 2%;
+}
+.layout_header {
+  line-height: 60px;
+}
+.requirement_info::before {
+  height: 0px;
+}
+</style>
+
+<style>
+.requirement_details_layout_main .el-table__body tr:hover td {
+  color: #409eff;
+  background: #eef0f5;
+} /*hover时字体, 背景颜色*/
+</style>

+ 283 - 0
src/views/projectManage/requirement/list/create.vue

@@ -0,0 +1,283 @@
+<template>
+  <el-dialog :title="title" :visible.sync="isVisible" width="60%">
+    <el-form :model="form" :rules="rules" label-position="right" label-width="120px">
+      <el-form-item label="需求名称" prop="name">
+        <el-input v-model="form.name" placeholder="请输入需求名称" />
+      </el-form-item>
+      <div style="display: flex;">
+        <div style="flex; 1;">
+          <el-form-item label="归属项目" prop="belongingProject">
+            <el-select v-model="form.belongingProject" placeholder="请选择" style="width:20vw">
+              <el-option
+                v-for="(item,index) in info.belongingProject"
+                :key="index"
+                :label="item.msg"
+                :value="item.code"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="需求来源" prop="sourceType">
+            <el-select v-model="form.sourceType" placeholder="请选择" style="width:20vw">
+              <el-option
+                v-for="(item,index) in info.sourceType"
+                :key="index"
+                :label="item.msg"
+                :value="item.code"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="PM" prop="pm">
+            <el-select
+              v-model="form.pm"
+              filterable
+              remote
+              placeholder="请输入需求产出的姓名或邮箱前缀"
+              :remote-method="searchUser"
+              :loading="userLoading"
+              style="width:20vw"
+            >
+              <el-option
+                v-for="(item,index) in options"
+                :key="index"
+                :label="item.name"
+                :value="item.idap"
+              >
+                <div style="text-align: center">
+                  <span style="float: left;color: #8492a6; font-size: 13px">{{ item.idap }}</span>
+                  <span>{{ item.name }}</span>
+                  <span style="float: right; color: #8492a6; font-size: 13px">{{ item.deptName }}</span>
+                </div>
+              </el-option>
+            </el-select>
+          </el-form-item>
+          <el-form-item label="是否依赖发版" prop="dependOnRelease">
+            <el-radio-group v-model="form.dependOnRelease">
+              <el-radio :label="1">是</el-radio>
+              <el-radio :label="0">否</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </div>
+        <div style="flex; 1; margin: 0 0 0 auto;">
+          <el-form-item label="优先级" prop="priority">
+            <el-select v-model="form.priority" placeholder="请选择" style="width:20vw">
+              <el-option
+                v-for="(item,index) in info.priority"
+                :key="index"
+                :label="item.msg"
+                :value="item.code"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="业务线" prop="bizId">
+            <el-select v-model="form.bizId" placeholder="请选择" style="width:20vw">
+              <el-option
+                v-for="(item,index) in info.bizType"
+                :key="index"
+                :label="item.msg"
+                :value="item.code"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="PRD链接" prop="mrdUrl">
+            <el-input v-model="form.mrdUrl" placeholder="请粘贴PRD链接" />
+          </el-form-item>
+          <el-form-item v-show="form.dependOnRelease" label="涉及的客户端" prop="referredClientType">
+            <el-select v-model="form.referredClientType" multiple placeholder="请选择,多选">
+              <el-option
+                v-for="item in info.appClient"
+                :key="item.code"
+                :label="item.msg"
+                :value="item.code"
+              />
+            </el-select>
+          </el-form-item>
+        </div>
+      </div>
+    </el-form>
+    <template v-slot:footer>
+      <el-button @click="cancel">取 消</el-button>
+      <el-button type="primary" @click="confirm">确 认</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script>
+import { getMemberInfo, createRequirement, updateRequirement } from '@/api/requirement.js'
+
+export default {
+  props: {
+    title: {
+      type: String,
+      default: '标题'
+    },
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    info: {
+      type: Object,
+      default() {
+        return {
+          belongingProject: [],
+          sourceType: [],
+          bizType: [],
+          priority: []
+        }
+      }
+    },
+    data: {
+      type: Object,
+      default() {
+        return null
+      }
+    }
+  },
+  data() {
+    return {
+      isVisible: false,
+      form: {
+        name: null,
+        belongingProject: null,
+        priority: null,
+        sourceType: null,
+        bizId: null,
+        pm: null,
+        mrdUrl: null,
+        dependOnRelease: 0,
+        referredClientType: null,
+        creator: null
+      },
+      rules: {
+        name: [{ required: true, message: '请输入需求名称', trigger: 'blur' }],
+        belongingProject: [
+          { required: true, message: '请选择归属项目', trigger: 'change' }
+        ],
+        priority: [
+          { required: true, message: '请选择优先级', trigger: 'change' }
+        ],
+        sourceType: [
+          { required: true, message: '请选择需求来源', trigger: 'change' }
+        ],
+        bizId: [{ required: true, message: '请选择业务线', trigger: 'change' }],
+        pm: [{ required: true, message: '请输入PM', trigger: 'change' }],
+        mrdUrl: [{ required: true, message: '请粘贴PRD连接', trigger: 'blur' }],
+        dependOnRelease: [
+          { required: true, message: '请选择业务线', trigger: 'blur' }
+        ],
+        referredClientType: [
+          { required: true, message: '请选择涉及的客户端', trigger: 'change' }
+        ]
+      },
+      options: [],
+      userLoading: false
+    }
+  },
+  watch: {
+    visible: function(newVisible, oldVisible) {
+      this.isVisible = newVisible
+      if (newVisible && this.title === '编辑需求') {
+        this.form = JSON.parse(JSON.stringify(this.data))
+        this.form.pm = this.form.pm[0].idap
+        this.init()
+      }
+    },
+    isVisible: function(newIsVisible, oldIsVisible) {
+      if (!newIsVisible) {
+        if (this.title === '新建需求') {
+          for (const i in this.form) {
+            this.form[i] = null
+          }
+          this.form.dependOnRelease = 0
+        }
+        this.$emit('cancel', false)
+      }
+    }
+  },
+  methods: {
+    cancel: function() {
+      this.$emit('cancel', false)
+    },
+    confirm: function() {
+      for (const i in this.form) {
+        if (!this.form[i]) {
+          if (i === 'name') {
+            this.$message.warning('需求名称为空,请输入需求名称')
+            return
+          } else if (i === 'belongingProject') {
+            this.$message.warning('请选择归属项目')
+            return
+          } else if (i === 'priority') {
+            if (this.form[i] === 0) {
+              break
+            }
+            this.$message.warning('请选择优先级')
+            return
+          } else if (i === 'sourceType') {
+            this.$message.warning('请选择需求来源')
+            return
+          } else if (i === 'bizId') {
+            this.$message.warning('请选择业务线')
+            return
+          } else if (i === 'pm') {
+            this.$message.warning('请填写PM名称')
+            return
+          } else if (i === 'mrdUrl') {
+            this.$message.warning('请输入Prd链接')
+            return
+          } else if (i === 'dependOnRelease') {
+            if (this.form[i] === 0) {
+              break
+            }
+            return
+          } else if (i === 'referredClientType' && this.form.dependOnRelease === 1) {
+            this.$message.warning('请选择客户端')
+            return
+          }
+        }
+      }
+      if (this.form.referredClientType && this.form.dependOnRelease === 1 && this.form.referredClientType.length === 0) {
+        this.$message.warning('请选择客户端')
+        return
+      }
+      this.form.creator = localStorage.getItem('username')
+      if (this.title === '新建需求') {
+        createRequirement(this.form)
+          .then(res => {
+            if (res.code === 200) {
+              this.$emit('confirm', this.form)
+            } else {
+              this.$message.warning(res.msg)
+            }
+          })
+      } else {
+        updateRequirement(this.form)
+          .then(res => {
+            if (res.code === 200) {
+              this.$emit('confirm', this.form)
+            } else {
+              this.$message.warning(res.msg)
+            }
+          })
+      }
+    },
+    searchUser(query) {
+      this.userLoading = true
+      getMemberInfo({ memberIDAP: query }).then(res => {
+        this.options = res.data
+        this.userLoading = false
+      })
+    },
+    init() {
+      if (this.form.pm) {
+        this.searchUser(this.form.pm)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.el-select {
+  width: 20vw;
+}
+</style>

+ 417 - 0
src/views/projectManage/requirement/list/index.vue

@@ -0,0 +1,417 @@
+<template>
+  <div class="container">
+    <el-header class="requirement-header">
+      <el-row>
+        <el-col :span="3">
+          <el-input id="requirement_basicName1" v-model="searchForm.name" placeholder="需求名称" />
+        </el-col>
+        <el-col :span="3">
+          <el-dropdown @command="handleCommand">
+            <span class="el-dropdown-link">
+              {{ getTitle(searchTitle.project) }}
+              <i class="el-icon-arrow-down el-icon--right" />
+            </span>
+            <el-dropdown-menu slot="dropdown" class="requirement_el-dropdown-menu">
+              <el-dropdown-item
+                v-for="(item,index) in searchInfo.belongingProject"
+                :key="index"
+                :command="{value: item,flag: 1}"
+              >{{ item.msg }}</el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+        </el-col>
+        <el-col :span="3">
+          <el-dropdown @command="handleCommand">
+            <span class="el-dropdown-link">
+              {{ getTitle(searchTitle.businessline) }}
+              <i class="el-icon-arrow-down el-icon--right" />
+            </span>
+            <el-dropdown-menu slot="dropdown" class="requirement_el-dropdown-menu">
+              <el-dropdown-item
+                v-for="(item,index) in searchInfo.bizType"
+                :key="index"
+                :command="{value: item,flag: 2}"
+              >{{ item.msg }}</el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+        </el-col>
+        <el-col :span="3">
+          <el-dropdown @command="handleCommand">
+            <span class="el-dropdown-link">
+              {{ getTitle(searchTitle.source) }}
+              <i class="el-icon-arrow-down el-icon--right" />
+            </span>
+            <el-dropdown-menu slot="dropdown" class="requirement_el-dropdown-menu">
+              <el-dropdown-item
+                v-for="(item,index) in searchInfo.sourceType"
+                :key="index"
+                :command="{value: item,flag: 3}"
+              >{{ item.msg }}</el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+        </el-col>
+        <el-col :span="3">
+          <el-dropdown @command="handleCommand">
+            <span class="el-dropdown-link">
+              {{ getTitle(searchTitle.priority) }}
+              <i class="el-icon-arrow-down el-icon--right" />
+            </span>
+            <el-dropdown-menu slot="dropdown" class="requirement_el-dropdown-menu">
+              <el-dropdown-item
+                v-for="(item,index) in searchInfo.priority"
+                :key="index"
+                :command="{value: item,flag: 4}"
+              >{{ item.msg }}</el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+        </el-col>
+        <el-col :span="3">
+          <el-select
+            id="requirement_basicName2"
+            v-model="searchForm.pm"
+            filterable
+            remote
+            placeholder="PM"
+            :remote-method="searchUser"
+            :loading="userLoading"
+          >
+            <el-option v-for="item in options" :key="item.idap" :label="item.name" :value="item.idap">
+              <div style="text-align: center">
+                <span style="float: left;color: #8492a6; font-size: 13px">{{ item.idap }}</span>
+                <span>{{ item.name }}</span>
+                <span style="float: right; color: #8492a6; font-size: 13px">{{ item.deptName }}</span>
+              </div>
+            </el-option>
+          </el-select>
+        </el-col>
+        <el-col :span="3">
+          <el-select
+            id="requirement_basicName3"
+            v-model="searchForm.creator"
+            filterable
+            remote
+            placeholder="创建人"
+            :remote-method="searchUser"
+            :loading="userLoading"
+          >
+            <el-option v-for="item in options" :key="item.idap" :label="item.name" :value="item.idap">
+              <div style="text-align: center">
+                <span style="float: left;color: #8492a6; font-size: 13px">{{ item.idap }}</span>
+                <span>{{ item.name }}</span>
+                <span style="float: right; color: #8492a6; font-size: 13px">{{ item.deptName }}</span>
+              </div>
+            </el-option>
+          </el-select>
+        </el-col>
+        <el-col :span="3">
+          <div style="float: right">
+            <el-button type="primary" size="mini" @click="getTableData">查询</el-button>
+            <el-button size="mini" @click="reset">重置</el-button>
+          </div>
+        </el-col>
+      </el-row>
+    </el-header>
+    <el-main class="requirement-main">
+      <div style="font-size: 18px;margin: 2%">
+        <b style="color: #409EFF;">I</b>版本列表
+        <el-button
+          type="primary"
+          size="mini"
+          icon="el-icon-plus"
+          style="float: right;"
+          @click="createDialogVisible = true"
+        >新建</el-button>
+      </div>
+      <el-table
+        v-loading="loading"
+        :data="tableData"
+        style="width: 100%"
+        highlight-current-row
+        :header-cell-style="{ background: '#6AB4FF', color: '#FFFFFF',textAlign: 'center'}"
+        :cell-style="{textAlign: 'center'}"
+      >
+        <el-table-column prop="priority" label="优先级" sortable min-width="8%">
+          <template v-slot="scope">
+            <div class="div_priority" :style="{background: priorityColors[scope.row.priority % priorityColors.length]}">{{ 'P'+scope.row.priority }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="name" label="需求名称" min-width="18%">
+          <template v-slot="scope">
+            <div style="cursor: pointer;" @click="getToRequirementDetails(scope.row.id)">{{ scope.row.name }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="bizId" label="业务线" min-width="10%">
+          <template v-slot="scope">
+            {{ getBizName(scope.row.bizId) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="sourceType" label="需求来源" min-width="10%">
+          <template v-slot="scope">
+            {{ sourceTypeOb[scope.row.sourceType] }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="belongingProjectName" label="归属的项目" min-width="10%">
+          <template v-slot="scope">
+            {{ getProjectName(scope.row.belongingProject) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="pm" label="PM" min-width="10%">
+          <template v-slot="scope">
+            {{ scope.row.pm[0]?scope.row.pm[0].name:'无' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="taskCount" label="任务总数" min-width="8%" />
+        <el-table-column prop="bugCount" label="bug总数" min-width="8%" />
+        <el-table-column prop="creator" label="创建人" min-width="8%" />
+        <el-table-column prop="createTime" label="创建时间" min-width="8%">
+          <template v-slot="scope">{{ getcreateTime(scope.row.createTime) }}</template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        layout="->,total, sizes, prev, pager, next, jumper"
+        :current-page="searchForm.curIndex"
+        :page-size="searchForm.pageSize"
+        :page-sizes="[15,30,45,total]"
+        :total="total"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </el-main>
+    <requirement-create
+      title="新建需求"
+      :visible="createDialogVisible"
+      :info="searchInfo"
+      @cancel="createDialogVisible=false"
+      @confirm="getTableData();createDialogVisible=false"
+    />
+  </div>
+</template>
+
+<script>
+import {
+  getRequirement,
+  showRequirementEnum,
+  getMemberInfo
+} from '@/api/requirement.js'
+import RequirementCreate from './create'
+
+export default {
+  components: {
+    RequirementCreate
+  },
+  data() {
+    return {
+      priorityColors: ['#F56C6C', '#FF8952', '#7ED321', '#61D3B8', '#F7AD3F', '#69B3FF', '#C889FF'],
+      currentRow: null,
+      searchInfo: {
+        belongingProject: [],
+        sourceType: [],
+        bizType: [],
+        priority: []
+      },
+      projectOb: {},
+      bizTypeOb: {},
+      sourceTypeOb: {},
+      searchForm: {
+        name: null,
+        belongingProject: null,
+        bizId: null,
+        sourceType: null,
+        priority: null,
+        pm: null,
+        creator: null,
+        pageSize: 15,
+        curIndex: 1
+      },
+      searchTitle: {
+        project: '归属的项目',
+        businessline: '业务线',
+        source: '需求来源',
+        priority: '优先级'
+      },
+      userLoading: false,
+      loading: false,
+      tableData: null,
+      createDialogVisible: false,
+      options: [],
+      total: 0,
+      items: [
+        { type: '' },
+        { type: 'success' },
+        { type: 'info' },
+        { type: 'danger' },
+        { type: 'warning' }
+      ]
+    }
+  },
+  mounted() {
+    this.showRequirementEnum().then(res => {
+      this.getTableData()
+    })
+  },
+  methods: {
+    getTitle(str) {
+      if (str.length <= 6) {
+        return str
+      } else {
+        return str.substring(0, 6) + '...'
+      }
+    },
+    getTableData() {
+      this.loading = true
+      getRequirement(this.searchForm).then(res => {
+        this.tableData = res.data.list
+        this.total = res.data.total
+        this.loading = false
+      })
+    },
+    showRequirementEnum() {
+      return showRequirementEnum().then(res => {
+        this.searchInfo = res.data
+        for (const i in this.searchInfo.belongingProject) {
+          this.projectOb[this.searchInfo.belongingProject[i].code] = this.searchInfo.belongingProject[i].msg
+        }
+        for (const i in this.searchInfo.bizType) {
+          this.bizTypeOb[this.searchInfo.bizType[i].code] = this.searchInfo.bizType[i].msg
+        }
+        for (const i in this.searchInfo.sourceType) {
+          this.sourceTypeOb[this.searchInfo.sourceType[i].code] = this.searchInfo.sourceType[i].msg
+        }
+      })
+    },
+    handleCommand(command) {
+      switch (command.flag) {
+        case 1:
+          this.searchForm.belongingProject = command.value.code
+          this.searchTitle.project = command.value.msg
+          break
+        case 2:
+          this.searchForm.bizId = command.value.code
+          this.searchTitle.businessline = command.value.msg
+          break
+        case 3:
+          this.searchForm.sourceType = command.value.code
+          this.searchTitle.source = command.value.msg
+          break
+        case 4:
+          this.searchForm.priority = command.value.code
+          this.searchTitle.priority = command.value.msg
+          break
+      }
+    },
+    getcreateTime(time) {
+      return time.substring(2, 10).replace(/-/g, '/')
+    },
+    // 分页pageSize选择
+    handleSizeChange: function(pageSize) {
+      this.searchForm.pageSize = pageSize
+      this.getTableData()
+    },
+    // 当前页选择
+    handleCurrentChange: function(currentPage) {
+      this.searchForm.curIndex = currentPage
+      this.getTableData()
+    },
+    searchUser(query) {
+      this.userLoading = true
+      getMemberInfo({ memberIDAP: query }).then(res => {
+        this.options = res.data
+        this.userLoading = false
+      })
+    },
+    reset() {
+      this.searchForm = {
+        name: null,
+        belongingProject: null,
+        bizId: null,
+        sourceType: null,
+        priority: null,
+        pm: null,
+        creator: null,
+        pageSize: 15,
+        curIndex: 1
+      }
+      this.searchTitle = {
+        project: '归属的项目',
+        businessline: '业务线',
+        source: '需求来源',
+        priority: '优先级'
+      }
+      // this.getTableData()
+    },
+    getProjectName(id) {
+      const ret = this.projectOb[id]
+      if (typeof ret === 'undefined') {
+        return '无归属项目'
+      }
+      return ret
+    },
+    getBizName(id) {
+      const ret = this.bizTypeOb[id]
+      if (typeof ret === 'undefined') {
+        return '无'
+      }
+      return ret
+    },
+    getToRequirementDetails(id) {
+      this.$router.push({ name: '需求详情', params: { id: id + '' }})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.el-dropdown-link {
+  white-space: nowrap;
+  cursor: pointer;
+  color: #333333;
+}
+.el-icon-arrow-down {
+  font-size: 12px;
+}
+.requirement-header {
+  margin: 1%;
+  background-color: #ffffff;
+  border-radius: 8px;
+  line-height: 60px;
+}
+.requirement-main {
+  margin: 0 1%;
+  background-color: #ffffff;
+  border-radius: 8px;
+  padding: 0%;
+}
+.priority_div {
+  width: fit-content;
+  width: -webkit-fit-content;
+  width: -moz-fit-content;
+  color: #ffffff;
+}
+.div_priority {
+  color: #ffffff;
+  width:fit-content;
+  padding: 0 12px;
+  border-radius: 4px;
+  margin: auto;
+}
+</style>
+
+<style>
+.requirement-header .el-input__inner {
+  border: none;
+}
+#requirement_basicName1::-webkit-input-placeholder {
+  color: #333333;
+}
+#requirement_basicName2::-webkit-input-placeholder {
+  color: #333333;
+}
+#requirement_basicName3::-webkit-input-placeholder {
+  color: #333333;
+}
+.requirement-main .el-table .el-table__body tr:hover td { color: #409EFF; background: #EEF0F5; } /*hover时字体, 背景颜色*/
+.requirement_el-dropdown-menu {
+  max-height: 300px !important;
+  overflow: auto !important;}
+</style>

+ 455 - 0
src/views/projectManage/version/list/index.vue

@@ -0,0 +1,455 @@
+<template>
+  <el-container>
+    <el-header class="layout_header">
+      <el-row>
+        <el-col :span="3">
+          <el-dropdown @command="handleCommand">
+            <span class="el-dropdown-link">
+              {{ getTitle(searchTitle.client) }}
+              <i class="el-icon-arrow-down el-icon--right" />
+            </span>
+            <el-dropdown-menu slot="dropdown" class="version_el-dropdown-menu">
+              <el-dropdown-item
+                v-for="(item,index) in searchInfo.clients"
+                :key="index"
+                :command="{value: item,flag: 1}"
+              >{{ item.msg }}</el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+        </el-col>
+        <el-col :span="5">
+          <el-dropdown @command="handleCommand">
+            <span class="el-dropdown-link">
+              {{ getTitle(searchTitle.version) }}
+              <i class="el-icon-arrow-down el-icon--right" />
+            </span>
+            <el-dropdown-menu slot="dropdown" class="version_el-dropdown-menu">
+              <el-dropdown-item
+                v-for="(item,index) in searchInfo.versions"
+                :key="index"
+                :command="{value: item,flag: 2}"
+              >{{ item.msg }}</el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+        </el-col>
+      </el-row>
+    </el-header>
+    <el-main class="layout_main version_list_layout_main">
+      <div style="font-size: 18px;margin: 2%">
+        <b style="color: #409EFF;">I</b>版本列表
+      </div>
+      <el-table
+        v-loading="loading"
+        :data="tableData"
+        style="width: 100%;"
+        highlight-current-row
+        :header-cell-style="{ background: '#6AB4FF', color: '#FFFFFF',textAlign: 'center'}"
+        :cell-style="{textAlign: 'center'}"
+      >
+        <el-table-column prop="priority" label="优先级" min-width="6%">
+          <template v-slot="scope">
+            <div class="div_priority" :style="{background: priorityColors[scope.row.priority%priorityColors.length]}">{{ 'P'+scope.row.priority }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="requirementName" label="需求名称" min-width="20%">
+          <template v-slot="scope">
+            <div style="cursor: pointer;" @click="getToRequirementDetails(scope.row.id)">
+              {{ scope.row.requirementName }}
+              <div v-if="scope.row.delay" class="div_requirement_name">{{ searchTitle.version +'版本已延期' }}</div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="bizId" label="业务线" min-width="8%">
+          <template v-slot="scope">
+            {{ versionEnum.bizTypeMap[scope.row.bizId] }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="pmName" label="PM" min-width="10%" />
+        <el-table-column prop="status" label="状态" min-width="8%">
+          <template v-slot="scope">
+            {{ versionEnum.requirementStatusMap[scope.row.status] }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="statusInfoCountList" label="任务" min-width="35%">
+          <template v-slot="scope">
+            <version-chart
+              :chart-id="'taskCount' + scope.$index"
+              :option="taskOptionList[scope.$index]"
+              style="position: relative;bottom: 30px"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column prop="statusInfoHealthyList" label="任务健康分布" min-width="30%">
+          <template v-slot="scope">
+            <version-chart
+              :chart-id="'taskHealthCount' + scope.$index"
+              :option="taskHealthOptionList[scope.$index]"
+              style="position: relative;bottom: 30px"
+            />
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+        background
+        layout="->,total, sizes, prev, pager, next, jumper"
+        :current-page="searchForm.curIndex"
+        :page-size="searchForm.pageSize"
+        :page-sizes="[15,30,45,total]"
+        :total="total"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </el-main>
+  </el-container>
+</template>
+
+<script>
+import { getVersionHomePageList, showAppClientEnum, showVersionEnum } from '@/api/version.js'
+import VersionChart from '@/components/chart/index.vue'
+
+export default {
+  components: {
+    VersionChart
+  },
+  data() {
+    return {
+      priorityColors: ['#F56C6C', '#FF8952', '#7ED321', '#61D3B8', '#F7AD3F', '#69B3FF', '#C889FF'],
+      tableData: [],
+      loading: false,
+      searchInfo: {
+        clients: [],
+        versions: []
+      },
+      searchTitle: {
+        client: '选择客户端',
+        version: '版本'
+      },
+      total: 0,
+      searchForm: {
+        clientType: null,
+        versionType: null,
+        curIndex: 1,
+        pageSize: 15
+      },
+      versionEnum: {
+        requirementStatusMap: {},
+        bizTypeMap: {},
+        taskStatusMap: {},
+        taskStageMap: {}
+      },
+      taskOptionTemplate: {
+        color: ['#69B3FF'],
+        title: {
+          show: false
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            // 坐标轴指示器,坐标轴触发有效
+            type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
+          }
+        },
+        grid: {
+          left: '3%',
+          right: '4%',
+          bottom: '0%',
+          containLabel: true
+        },
+        xAxis: [
+          {
+            type: 'category',
+            data: [],
+            axisTick: {
+              alignWithLabel: true,
+              show: false
+            },
+            axisLine: {
+              show: true,
+              lineStyle: {
+                type: 'dashed'
+              }
+            }
+          }
+        ],
+        yAxis: [
+          {
+            type: 'value',
+            axisTick: {
+              show: false
+            },
+            axisLine: {
+              show: false
+            },
+            axisLabel: {
+              show: false
+            },
+            splitLine: {
+              show: false
+            }
+          }
+        ],
+        series: [
+          {
+            type: 'bar',
+            barWidth: '60%',
+            label: {
+              show: true,
+              position: 'inside'
+            },
+            data: []
+          }
+        ]
+      },
+      taskOptionList: [],
+      taskHealthOptionTemplate: {
+        color: ['#909399', '#61D3B8', '#F7AD3F', '#F56C6C'],
+        title: {
+          show: false
+        },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: {
+            // 坐标轴指示器,坐标轴触发有效
+            type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
+          }
+        },
+        grid: {
+          left: '3%',
+          right: '4%',
+          bottom: '0%',
+          containLabel: true
+        },
+        xAxis: [
+          {
+            type: 'category',
+            data: [],
+            axisTick: {
+              alignWithLabel: true,
+              show: false
+            },
+            axisLine: {
+              show: true,
+              lineStyle: {
+                type: 'dashed'
+              }
+            }
+          }
+        ],
+        yAxis: [
+          {
+            type: 'value',
+            axisTick: {
+              show: false
+            },
+            axisLine: {
+              show: false
+            },
+            axisLabel: {
+              show: false
+            },
+            splitLine: {
+              show: false
+            }
+          }
+        ],
+        series: [
+          {
+            type: 'bar',
+            barWidth: '60%',
+            label: {
+              show: true,
+              position: 'inside'
+            },
+            data: []
+          }
+        ]
+      },
+      taskHealthOptionList: []
+    }
+  },
+  mounted() {
+    this.showAppClientEnum()
+    this.showVersionEnum()
+  },
+  methods: {
+    getTitle(str) {
+      if (str.length <= 6) {
+        return str
+      } else {
+        return str.substring(0, 6) + '...'
+      }
+    },
+    handleCommand(command) {
+      switch (command.flag) {
+        case 1:
+          this.searchTitle.client = command.value.msg
+          this.searchInfo.versions = command.value.childEnumInfos
+          if (command.value.code !== this.searchForm.clientType) {
+            if (this.searchInfo.versions && this.searchInfo.versions.length > 0) {
+              this.searchForm.versionType = this.searchInfo.versions[0].code
+              this.searchTitle.version = this.searchInfo.versions[0].msg
+            } else {
+              this.searchForm.versionType = null
+              this.searchTitle.version = '无版本'
+            }
+          }
+          this.searchForm.clientType = command.value.code
+          break
+        case 2:
+          this.searchForm.versionType = command.value.code
+          this.searchTitle.version = command.value.msg
+          break
+      }
+      this.getVersionHomePageList()
+    },
+    getToRequirementDetails(val) {
+      // this.$router.push({ name: '版本详情', params: { id: val.id + '' }})
+    },
+    // 分页pageSize选择
+    handleSizeChange(pageSize) {
+      this.searchForm.pageSize = pageSize
+      this.getVersionHomePageList()
+    },
+    // 当前页选择
+    handleCurrentChange(currentPage) {
+      this.searchForm.curIndex = currentPage
+      this.getVersionHomePageList()
+    },
+    getVersionHomePageList() {
+      this.loading = true
+      getVersionHomePageList(this.searchForm)
+        .then(res => {
+          if (res.code === 200) {
+            this.createTaskOption(res.data.list)
+            this.createTaskHealthOption(res.data.list)
+            this.tableData = res.data.list
+            this.total = res.data.total
+          } else {
+            this.tableData = null
+            this.$message.warning(res.msg)
+          }
+          this.loading = false
+        })
+    },
+    createTaskOption(data) {
+      this.taskOptionList = []
+      for (const i in data) {
+        const option = JSON.parse(JSON.stringify(this.taskOptionTemplate))
+        for (const j in data[i].statusInfoCountList) {
+          option.xAxis[0].data.push(data[i].statusInfoCountList[j].statusString)
+          option.series[0].data.push(data[i].statusInfoCountList[j].count)
+        }
+        this.taskOptionList.push(option)
+      }
+    },
+    createTaskHealthOption(data) {
+      this.taskHealthOptionList = []
+      for (const i in data) {
+        const option = JSON.parse(JSON.stringify(this.taskHealthOptionTemplate))
+        for (const j in data[i].statusInfoHealthyList) {
+          option.xAxis[0].data.push(data[i].statusInfoHealthyList[j].statusString)
+          const item = {
+            value: data[i].statusInfoHealthyList[j].count,
+            itemStyle: {
+              color: this.taskHealthOptionTemplate.color[j % this.taskHealthOptionTemplate.color.length]
+            }
+          }
+          option.series[0].data.push(item)
+        }
+        this.taskHealthOptionList.push(option)
+      }
+    },
+    showAppClientEnum() {
+      showAppClientEnum()
+        .then(res => {
+          if (res.code === 200) {
+            this.searchInfo.clients = res.data.appClient
+            if (this.searchInfo.clients.length > 0) {
+              this.searchForm.clientType = this.searchInfo.clients[0].code
+              this.searchTitle.client = this.searchInfo.clients[0].msg
+              if (this.searchInfo.clients[0].childEnumInfos && this.searchInfo.clients[0].childEnumInfos.length > 0) {
+                this.searchInfo.versions = this.searchInfo.clients[0].childEnumInfos
+                this.searchForm.versionType = this.searchInfo.versions[0].code
+                this.searchTitle.version = this.searchInfo.versions[0].msg
+              }
+            }
+            this.getVersionHomePageList()
+          } else {
+            this.$message.warning(res.msg)
+          }
+        })
+    },
+    showVersionEnum() {
+      showVersionEnum()
+        .then(res => {
+          if (res.code === 200) {
+            const enums = res.data
+            for (const i in enums.bizType) {
+              this.versionEnum.bizTypeMap[enums.bizType[i].code] = enums.bizType[i].msg
+            }
+            for (const i in enums.requirementStatus) {
+              this.versionEnum.requirementStatusMap[enums.requirementStatus[i].code] = enums.requirementStatus[i].msg
+            }
+            for (const i in enums.taskStatus) {
+              this.versionEnum.taskStatusMap[enums.taskStatus[i].code] = enums.taskStatus[i].msg
+            }
+            for (const i in enums.taskStage) {
+              this.versionEnum.taskStageMap[enums.taskStage[i].code] = enums.taskStage[i].msg
+            }
+          } else {
+            this.$message.warning(res.msg)
+          }
+        })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.el-dropdown-link {
+  cursor: pointer;
+  color: #333333;
+}
+.layout_header,
+.layout_main {
+  border-radius: 8px;
+  background-color: #ffffff;
+}
+.layout_main {
+  padding: 0
+}
+.layout_header {
+  margin: 2%;
+  line-height: 60px;
+}
+.layout_main {
+  margin: 0 2% 2% 2%
+}
+.div_priority {
+  color: #ffffff;
+  width:fit-content;
+  padding: 0 12px;
+  border-radius: 4px;
+  margin: auto;
+}
+.div_requirement_name {
+  width:fit-content;
+  margin: auto;
+  padding: 0px 5px 0 10px;
+  background: #F56C6C;
+  color: #ffffff;
+  -webkit-clip-path: polygon(15% 0, 100% 0, 100% 100%, 15% 100%, 0 50%);
+  clip-path: polygon(15% 0, 100% 0, 100% 100%, 15% 100%, 0 50%);
+  border-radius: 8px;
+}
+</style>
+
+<style>
+.version_list_layout_main .el-table__body tr:hover td {
+  color: #409eff;
+  background: #eef0f5;
+} /*hover时字体, 背景颜色*/
+.version_el-dropdown-menu {
+  max-height: 300px !important;
+  overflow: auto !important;}
+</style>