功能更新:新增Bug管理模块,完善用户角色分配,优化项目设置

This commit is contained in:
qiaoxinjiu
2026-05-07 19:43:05 +08:00
parent 916248483c
commit f8211673ee
44 changed files with 10410 additions and 437 deletions

View File

@@ -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>