功能更新:新增Bug管理模块,完善用户角色分配,优化项目设置
This commit is contained in:
@@ -1,67 +1,293 @@
|
||||
<template>
|
||||
<div class="page-wrap">
|
||||
<page-section title="计划进度">
|
||||
<el-form :inline="true" size="small">
|
||||
<el-form-item label="项目ID">
|
||||
<el-input v-model="projectId" style="width: 120px;"></el-input>
|
||||
<el-form :inline="true" size="small" @submit.native.prevent>
|
||||
<el-form-item label="产品名称">
|
||||
<el-input :value="productName" disabled style="width: 220px;"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划ID">
|
||||
<el-input v-model="planId" style="width: 120px;"></el-input>
|
||||
<el-form-item label="项目名称">
|
||||
<el-input :value="projectName" disabled style="width: 220px;"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划名称">
|
||||
<el-input :value="planNameDisplay" disabled style="width: 240px;"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="fetchProgress">刷新</el-button>
|
||||
<el-button type="primary" @click="fetchProgressBoard">刷新看板</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<page-section title="轮次汇总">
|
||||
<json-viewer :value="progress.round_summary || []"></json-viewer>
|
||||
</page-section>
|
||||
|
||||
<el-row :gutter="16" class="metric-row">
|
||||
<el-col :span="6">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">总用例数</div>
|
||||
<div class="metric-value">{{ summary.total }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<page-section title="人员负载">
|
||||
<json-viewer :value="progress.assignee_load || []"></json-viewer>
|
||||
</page-section>
|
||||
<el-col :span="6">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">待执行</div>
|
||||
<div class="metric-value warning">{{ summary.pending }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<page-section title="每日趋势">
|
||||
<json-viewer :value="progress.daily_trend || []"></json-viewer>
|
||||
</page-section>
|
||||
<el-col :span="6">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">已执行</div>
|
||||
<div class="metric-value success">{{ summary.executed }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">完成率</div>
|
||||
<div class="metric-value primary">{{ summary.progressPercent }}%</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<div class="board-card">
|
||||
<div class="board-title">执行完成度</div>
|
||||
<div class="dashboard-wrap">
|
||||
<el-progress
|
||||
type="dashboard"
|
||||
:percentage="summary.progressPercent"
|
||||
:color="progressColor">
|
||||
</el-progress>
|
||||
<div class="dashboard-desc">已执行 {{ summary.executed }} / {{ summary.total }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<div class="board-card">
|
||||
<div class="board-title">状态分布</div>
|
||||
<div v-for="item in statusBarData" :key="item.key" class="status-bar-row">
|
||||
<div class="status-label">
|
||||
<el-tag size="mini" :type="item.tag">{{ item.label }}</el-tag>
|
||||
</div>
|
||||
<div class="status-bar-track">
|
||||
<div class="status-bar-fill" :style="{ width: item.percent + '%', background: item.color }"></div>
|
||||
</div>
|
||||
<div class="status-value">{{ item.count }}({{ item.percent }}%)</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="board-card" style="margin-top: 16px;">
|
||||
<div class="board-title">用例状态看板明细</div>
|
||||
<el-table v-loading="loading" :data="displayRows" border style="margin-top: 10px;">
|
||||
<el-table-column label="模块" min-width="160">
|
||||
<template slot-scope="scope">{{ formatModuleName(scope.row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="caseKey" label="用例编号" min-width="120"></el-table-column>
|
||||
<el-table-column prop="caseTitle" label="用例名称" min-width="220"></el-table-column>
|
||||
<el-table-column label="状态" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" :type="statusTagType(scope.row.statusCode)">{{ statusLabel(scope.row.statusCode) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="actualResult" label="执行结果" min-width="180"></el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
style="margin-top: 12px; text-align: right;"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:current-page="pageNo"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="pageSize"
|
||||
:total="caseListTotal"
|
||||
@size-change="handleCaseListSizeChange"
|
||||
@current-change="handleCaseListPageChange">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</page-section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PageSection from '@/components/TestPlatform/common/PageSection'
|
||||
import JsonViewer from '@/components/TestPlatform/common/JsonViewer'
|
||||
import { getPlanProgress } from '@/api/planApi'
|
||||
import { getPlanCaseList, getPlanProgress } from '@/api/planApi'
|
||||
|
||||
export default {
|
||||
name: 'PlanProgress',
|
||||
components: { PageSection, JsonViewer },
|
||||
components: { PageSection },
|
||||
data() {
|
||||
return {
|
||||
projectId: this.$route.query.projectId || 1,
|
||||
loading: false,
|
||||
projectId: this.$route.query.projectId || '',
|
||||
planId: this.$route.query.planId || '',
|
||||
progress: {}
|
||||
progress: {},
|
||||
planCaseTableData: [],
|
||||
caseListTotal: 0,
|
||||
pageNo: 1,
|
||||
pageSize: 10
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
productName() {
|
||||
return this.$route.query.productName || ''
|
||||
},
|
||||
projectName() {
|
||||
return this.$route.query.projectName || ''
|
||||
},
|
||||
planNameDisplay() {
|
||||
return this.$route.query.planName || ''
|
||||
},
|
||||
summary() {
|
||||
const statusCount = this.statusCountMap
|
||||
const progressTotal = this.toNumber(this.progress.total_cases || this.progress.total || 0)
|
||||
const total = progressTotal || this.caseListTotal || 0
|
||||
const pending = this.toNumber(statusCount[0])
|
||||
const executed = Math.max(0, total - pending)
|
||||
const progressPercent = total > 0 ? Math.round((executed / total) * 100) : 0
|
||||
return { total, pending, executed, progressPercent }
|
||||
},
|
||||
/** 顶部看板统计以 getPlanProgress 为准,不能按当前页用例行数聚合 */
|
||||
statusCountMap() {
|
||||
const map = {}
|
||||
const fromProgress = this.progress.status_count || this.progress.statusCount || {}
|
||||
if (fromProgress && typeof fromProgress === 'object' && Object.keys(fromProgress).length > 0) {
|
||||
Object.keys(fromProgress).forEach(key => {
|
||||
map[this.toNumber(key)] = this.toNumber(fromProgress[key])
|
||||
})
|
||||
return map
|
||||
}
|
||||
this.planCaseTableData.forEach(item => {
|
||||
const key = this.toNumber(item.statusCode)
|
||||
map[key] = (map[key] || 0) + 1
|
||||
})
|
||||
return map
|
||||
},
|
||||
statusBarData() {
|
||||
const total = this.summary.total || 1
|
||||
const statuses = [
|
||||
{ key: 0, label: '待执行', tag: 'info', color: '#909399' },
|
||||
{ key: 1, label: '通过', tag: 'success', color: '#67c23a' },
|
||||
{ key: 2, label: '失败', tag: 'danger', color: '#f56c6c' },
|
||||
{ key: 3, label: '阻塞', tag: 'warning', color: '#e6a23c' }
|
||||
]
|
||||
return statuses.map(item => {
|
||||
const count = this.toNumber(this.statusCountMap[item.key] || 0)
|
||||
const percent = Math.round((count / total) * 100)
|
||||
return Object.assign({}, item, { count, percent })
|
||||
})
|
||||
},
|
||||
progressColor() {
|
||||
const value = this.summary.progressPercent
|
||||
if (value >= 80) return '#67c23a'
|
||||
if (value >= 50) return '#409eff'
|
||||
if (value >= 30) return '#e6a23c'
|
||||
return '#f56c6c'
|
||||
},
|
||||
displayRows() {
|
||||
return this.planCaseTableData
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchProgress() {
|
||||
if (!this.planId) {
|
||||
handleCaseListSizeChange(size) {
|
||||
this.pageSize = size
|
||||
this.pageNo = 1
|
||||
this.fetchProgressBoard()
|
||||
},
|
||||
handleCaseListPageChange(page) {
|
||||
this.pageNo = page
|
||||
this.fetchProgressBoard()
|
||||
},
|
||||
fetchProgressBoard() {
|
||||
if (!this.planId || !this.projectId) {
|
||||
this.progress = {}
|
||||
this.planCaseTableData = []
|
||||
this.caseListTotal = 0
|
||||
return
|
||||
}
|
||||
getPlanProgress(this.projectId, this.planId).then(res => {
|
||||
this.progress = (res && res.data) || res || {}
|
||||
}).catch(() => {
|
||||
this.progress = {}
|
||||
this.loading = true
|
||||
Promise.all([
|
||||
getPlanProgress(this.projectId, this.planId),
|
||||
getPlanCaseList(this.projectId, this.planId, { pageNo: this.pageNo, pageSize: this.pageSize })
|
||||
])
|
||||
.then(([progressRes, listRes]) => {
|
||||
this.progress = (progressRes && progressRes.data) || progressRes || {}
|
||||
const listData = (listRes && listRes.data) || listRes || {}
|
||||
const list = listData.list || listData.items || listData.data || []
|
||||
this.caseListTotal = Number(
|
||||
listData.total != null ? listData.total : Array.isArray(list) ? list.length : 0
|
||||
)
|
||||
this.planCaseTableData = (Array.isArray(list) ? list : []).map(item => ({
|
||||
id: item.id,
|
||||
caseId: item.case_id || item.caseId,
|
||||
moduleName: item.module_name || item.moduleName || '',
|
||||
moduleId: item.module_id || item.moduleId || '',
|
||||
caseKey: item.case_key || item.caseKey || '',
|
||||
caseTitle: item.case_title || item.caseTitle || item.title || '',
|
||||
title: item.title || item.case_title || item.caseTitle || '',
|
||||
statusCode: this.toNumber(item.status),
|
||||
actualResult: item.actual_result || item.actualResult || ''
|
||||
}))
|
||||
})
|
||||
.catch(() => {
|
||||
this.progress = {}
|
||||
this.planCaseTableData = []
|
||||
this.caseListTotal = 0
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
toNumber(value) {
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) ? num : 0
|
||||
},
|
||||
statusLabel(status) {
|
||||
const map = { 0: '待执行', 1: '通过', 2: '失败', 3: '阻塞' }
|
||||
return map[this.toNumber(status)] || '未知'
|
||||
},
|
||||
statusTagType(status) {
|
||||
const map = { 0: 'info', 1: 'success', 2: 'danger', 3: 'warning' }
|
||||
return map[this.toNumber(status)] || 'info'
|
||||
},
|
||||
formatModuleName(row) {
|
||||
if (!row) return '-'
|
||||
if (row.moduleName) return row.moduleName
|
||||
return '-'
|
||||
},
|
||||
goBack() {
|
||||
this.$router.push({
|
||||
path: '/test-platform/plan',
|
||||
query: {
|
||||
productId: this.$route.query.productId || undefined,
|
||||
projectId: this.projectId || undefined
|
||||
}
|
||||
})
|
||||
},
|
||||
normalizeRouteQuery() {
|
||||
if (!this.projectId && this.$route.query.project_id) {
|
||||
this.projectId = this.$route.query.project_id
|
||||
}
|
||||
if (!this.planId && this.$route.query.id) {
|
||||
this.planId = this.$route.query.id
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchProgress()
|
||||
this.normalizeRouteQuery()
|
||||
this.fetchProgressBoard()
|
||||
},
|
||||
watch: {
|
||||
'$route.query.planId'(val) {
|
||||
if (val !== undefined) {
|
||||
this.planId = val
|
||||
this.fetchProgressBoard()
|
||||
}
|
||||
},
|
||||
'$route.query.projectId'(val) {
|
||||
if (val !== undefined) {
|
||||
this.projectId = val
|
||||
this.fetchProgressBoard()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -69,5 +295,99 @@ export default {
|
||||
<style scoped>
|
||||
.page-wrap {
|
||||
padding: 20px;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.metric-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f7fbff 100%);
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
min-height: 84px;
|
||||
box-shadow: 0 4px 10px rgba(31, 45, 61, 0.06);
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
margin-top: 8px;
|
||||
color: #303133;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.metric-value.success {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.metric-value.warning {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.metric-value.primary {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.board-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
padding: 16px;
|
||||
box-shadow: 0 6px 14px rgba(31, 45, 61, 0.08);
|
||||
}
|
||||
|
||||
.board-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.dashboard-wrap {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dashboard-desc {
|
||||
color: #606266;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.status-bar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
width: 72px;
|
||||
}
|
||||
|
||||
.status-bar-track {
|
||||
flex: 1;
|
||||
height: 10px;
|
||||
border-radius: 10px;
|
||||
background: #f1f2f4;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
width: 110px;
|
||||
text-align: right;
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user