提交 f3f5d771 authored 作者: 陈泽健's avatar 陈泽健

feat(frontend): 菜单重构为自动化测试父菜单,性能/功能报告独立,修复性能任务编辑详情加载

- App.vue: 菜单重构(自动化测试整合 /auto 父菜单,性能测试、功能测试报告独立一级菜单,部署/设备模拟/系统管理重新排序)
- Cases.vue: 移除 deploy/performance/functional 用例类型选项
- Reports.vue: 报告类型标签映射(ui/api/security/functional)
- TaskList.vue: 编辑弹窗改用 getTask 获取完整详情,修复 stepConcurrency 数组处理
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 8a8af0dd
...@@ -37,7 +37,14 @@ ...@@ -37,7 +37,14 @@
<el-menu-item index="/modules/custom">项目定制模块</el-menu-item> <el-menu-item index="/modules/custom">项目定制模块</el-menu-item>
</el-sub-menu> </el-sub-menu>
<!-- 用例管理(父菜单) --> <!-- 自动化测试(父菜单,UI/接口/安全 三类) -->
<el-sub-menu index="/auto">
<template #title>
<el-icon><VideoPlay /></el-icon>
<span>自动化测试</span>
</template>
<!-- 用例管理 -->
<el-sub-menu index="/cases"> <el-sub-menu index="/cases">
<template #title> <template #title>
<el-icon><Document /></el-icon> <el-icon><Document /></el-icon>
...@@ -46,17 +53,9 @@ ...@@ -46,17 +53,9 @@
<el-menu-item index="/cases/ui">UI自动化</el-menu-item> <el-menu-item index="/cases/ui">UI自动化</el-menu-item>
<el-menu-item index="/cases/api">接口自动化</el-menu-item> <el-menu-item index="/cases/api">接口自动化</el-menu-item>
<el-menu-item index="/cases/security">安全自动化测试</el-menu-item> <el-menu-item index="/cases/security">安全自动化测试</el-menu-item>
<el-menu-item index="/cases/deploy">自动化部署测试</el-menu-item>
<el-menu-item index="/performance">性能测试</el-menu-item>
<el-menu-item index="/cases/functional">功能测试</el-menu-item>
</el-sub-menu> </el-sub-menu>
<el-menu-item index="/recorder"> <!-- 执行中心 -->
<el-icon><VideoCamera /></el-icon>
<span>用例录制</span>
</el-menu-item>
<!-- 执行中心(父菜单,6 种类型) -->
<el-sub-menu index="/execution"> <el-sub-menu index="/execution">
<template #title> <template #title>
<el-icon><VideoPlay /></el-icon> <el-icon><VideoPlay /></el-icon>
...@@ -65,12 +64,9 @@ ...@@ -65,12 +64,9 @@
<el-menu-item index="/execution/ui">UI自动化</el-menu-item> <el-menu-item index="/execution/ui">UI自动化</el-menu-item>
<el-menu-item index="/execution/api">接口自动化</el-menu-item> <el-menu-item index="/execution/api">接口自动化</el-menu-item>
<el-menu-item index="/execution/security">安全自动化测试</el-menu-item> <el-menu-item index="/execution/security">安全自动化测试</el-menu-item>
<el-menu-item index="/execution/deploy">自动化部署测试</el-menu-item>
<el-menu-item index="/performance">性能测试</el-menu-item>
<el-menu-item index="/execution/functional">功能测试</el-menu-item>
</el-sub-menu> </el-sub-menu>
<!-- 报告中心(父菜单,5 种类型,无部署) --> <!-- 报告中心 -->
<el-sub-menu index="/reports"> <el-sub-menu index="/reports">
<template #title> <template #title>
<el-icon><DataLine /></el-icon> <el-icon><DataLine /></el-icon>
...@@ -79,25 +75,45 @@ ...@@ -79,25 +75,45 @@
<el-menu-item index="/reports/ui">UI自动化测试</el-menu-item> <el-menu-item index="/reports/ui">UI自动化测试</el-menu-item>
<el-menu-item index="/reports/api">接口测试</el-menu-item> <el-menu-item index="/reports/api">接口测试</el-menu-item>
<el-menu-item index="/reports/security">安全测试</el-menu-item> <el-menu-item index="/reports/security">安全测试</el-menu-item>
<el-menu-item index="/performance">性能测试</el-menu-item> </el-sub-menu>
<el-menu-item index="/reports/functional">功能测试</el-menu-item>
</el-sub-menu> </el-sub-menu>
<!-- 系统管理(父菜单) --> <!-- 性能测试(独立一级菜单) -->
<el-sub-menu index="/system"> <el-menu-item index="/performance">
<el-icon><Odometer /></el-icon>
<span>性能测试</span>
</el-menu-item>
<!-- 功能测试报告(独立一级菜单) -->
<el-menu-item index="/reports/functional">
<el-icon><DocumentChecked /></el-icon>
<span>功能测试报告</span>
</el-menu-item>
<!-- 自动化部署测试(父菜单) -->
<el-sub-menu index="/deploy">
<template #title>
<el-icon><Promotion /></el-icon>
<span>自动化部署测试</span>
</template>
<el-menu-item index="/deploy/servers">服务器管理</el-menu-item>
<el-menu-item index="/deploy/scripts">部署脚本</el-menu-item>
<el-menu-item index="/deploy/executions">部署执行</el-menu-item>
</el-sub-menu>
<!-- 辅助工具(父菜单) -->
<el-sub-menu index="/tools">
<template #title> <template #title>
<el-icon><Tools /></el-icon> <el-icon><Tools /></el-icon>
<span>系统管理</span> <span>辅助工具</span>
</template> </template>
<el-menu-item index="/system/settings">系统配置</el-menu-item> <el-menu-item index="/recorder">用例录制</el-menu-item>
<el-menu-item index="/system/info">系统信息</el-menu-item>
<el-menu-item index="/system/logs">操作日志</el-menu-item>
</el-sub-menu> </el-sub-menu>
<!-- 设备模拟(菜单) --> <!-- 设备模拟(独立一级菜单) -->
<el-sub-menu index="/device-sim"> <el-sub-menu index="/device-sim">
<template #title> <template #title>
<el-icon><Connection /></el-icon> <el-icon><Iphone /></el-icon>
<span>设备模拟</span> <span>设备模拟</span>
</template> </template>
<el-menu-item index="/device-sim/settings">环境配置</el-menu-item> <el-menu-item index="/device-sim/settings">环境配置</el-menu-item>
...@@ -107,15 +123,15 @@ ...@@ -107,15 +123,15 @@
<el-menu-item index="/device-sim/client">集控客户端模拟</el-menu-item> <el-menu-item index="/device-sim/client">集控客户端模拟</el-menu-item>
</el-sub-menu> </el-sub-menu>
<!-- 自动化部署测试(父菜单) --> <!-- 系统管理(父菜单) -->
<el-sub-menu index="/deploy"> <el-sub-menu index="/system">
<template #title> <template #title>
<el-icon><Promotion /></el-icon> <el-icon><Setting /></el-icon>
<span>自动化部署测试</span> <span>系统管理</span>
</template> </template>
<el-menu-item index="/deploy/servers">服务器管理</el-menu-item> <el-menu-item index="/system/settings">系统配置</el-menu-item>
<el-menu-item index="/deploy/scripts">部署脚本</el-menu-item> <el-menu-item index="/system/info">系统信息</el-menu-item>
<el-menu-item index="/deploy/executions">部署执行</el-menu-item> <el-menu-item index="/system/logs">操作日志</el-menu-item>
</el-sub-menu> </el-sub-menu>
</el-menu> </el-menu>
</el-aside> </el-aside>
...@@ -164,12 +180,14 @@ import { ...@@ -164,12 +180,14 @@ import {
DataBoard, DataBoard,
Folder, Folder,
Document, Document,
VideoCamera,
Tools,
VideoPlay, VideoPlay,
DataLine, DataLine,
Connection, Odometer,
Promotion DocumentChecked,
Promotion,
Iphone,
Tools,
Setting
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
// ==================== 响应式数据 ==================== // ==================== 响应式数据 ====================
...@@ -186,9 +204,6 @@ const CASE_TYPE_LABELS: Record<string, string> = { ...@@ -186,9 +204,6 @@ const CASE_TYPE_LABELS: Record<string, string> = {
ui: 'UI自动化', ui: 'UI自动化',
api: '接口自动化', api: '接口自动化',
security: '安全自动化测试', security: '安全自动化测试',
deploy: '自动化部署测试',
performance: '性能测试',
functional: '功能测试',
} }
/** 设备模拟子页面标签映射 */ /** 设备模拟子页面标签映射 */
...@@ -200,13 +215,12 @@ const DEVICE_SIM_LABELS: Record<string, string> = { ...@@ -200,13 +215,12 @@ const DEVICE_SIM_LABELS: Record<string, string> = {
client: '集控客户端模拟', client: '集控客户端模拟',
} }
/** 分类标签映射:报告中心(不含部署) */ /** 分类标签映射:报告中心 */
const REPORT_TYPE_LABELS: Record<string, string> = { const REPORT_TYPE_LABELS: Record<string, string> = {
ui: 'UI自动化测试', ui: 'UI自动化测试',
api: '接口测试', api: '接口测试',
security: '安全测试', security: '安全测试',
performance: '性能测试', functional: '功能测试报告',
functional: '功能测试',
} }
/** 模块类型标签映射 */ /** 模块类型标签映射 */
...@@ -234,8 +248,8 @@ const currentRoute = computed(() => { ...@@ -234,8 +248,8 @@ const currentRoute = computed(() => {
const seg = route.path.split('/').filter(Boolean) const seg = route.path.split('/').filter(Boolean)
const root = '/' + (seg[0] || '') const root = '/' + (seg[0] || '')
// 无 type 参数时回退到默认子项,保证菜单高亮 // 无 type 参数时回退到默认子项,保证菜单高亮
if (seg.length === 1 && ['/modules', '/cases', '/execution', '/reports', '/device-sim', '/system', '/deploy', '/performance'].includes(root)) { if (seg.length === 1 && ['/modules', '/cases', '/execution', '/reports', '/system', '/deploy', '/device-sim'].includes(root)) {
const defaultType = root === '/modules' ? 'standard' : root === '/device-sim' ? 'settings' : root === '/system' ? 'settings' : root === '/deploy' ? 'servers' : 'ui' const defaultType = root === '/modules' ? 'standard' : root === '/system' ? 'settings' : root === '/deploy' ? 'servers' : root === '/device-sim' ? 'settings' : 'ui'
return `${root}/${defaultType}` return `${root}/${defaultType}`
} }
return route.path return route.path
...@@ -248,14 +262,16 @@ const pageTitle = computed(() => { ...@@ -248,14 +262,16 @@ const pageTitle = computed(() => {
const baseMap: Record<string, string> = { const baseMap: Record<string, string> = {
'/': '测试总览', '/': '测试总览',
'/modules': '模块管理', '/modules': '模块管理',
'/auto': '自动化测试',
'/cases': '用例管理', '/cases': '用例管理',
'/recorder': '用例录制', '/recorder': '用例录制',
'/execution': '执行中心', '/execution': '执行中心',
'/reports': '报告中心', '/reports': '报告中心',
'/system': '系统管理', '/performance': '性能测试',
'/device-sim': '设备模拟',
'/deploy': '自动化部署测试', '/deploy': '自动化部署测试',
'/performance': '性能测试' '/tools': '辅助工具',
'/device-sim': '设备模拟',
'/system': '系统管理',
} }
const base = baseMap[root] || '测试管理平台' const base = baseMap[root] || '测试管理平台'
const type = seg[1] const type = seg[1]
......
...@@ -600,9 +600,6 @@ const CASE_TYPE_OPTIONS = [ ...@@ -600,9 +600,6 @@ const CASE_TYPE_OPTIONS = [
{ label: 'UI自动化', value: 'ui' }, { label: 'UI自动化', value: 'ui' },
{ label: '接口自动化', value: 'api' }, { label: '接口自动化', value: 'api' },
{ label: '安全自动化测试', value: 'security' }, { label: '安全自动化测试', value: 'security' },
{ label: '自动化部署测试', value: 'deploy' },
{ label: '性能测试', value: 'performance' },
{ label: '功能测试', value: 'functional' },
] ]
/** 当前用例类型(来自路由参数,默认 ui) */ /** 当前用例类型(来自路由参数,默认 ui) */
......
...@@ -30,8 +30,8 @@ ...@@ -30,8 +30,8 @@
<el-table-column prop="name" label="执行名称" min-width="200" /> <el-table-column prop="name" label="执行名称" min-width="200" />
<el-table-column label="报告类型" width="120"> <el-table-column label="报告类型" width="120">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.case_type === 'security' ? 'danger' : 'primary'" size="small"> <el-tag :type="reportTypeTag(row.case_type)" size="small">
{{ row.case_type === 'security' ? '安全测试' : 'UI自动化' }} {{ reportTypeLabel(row.case_type) }}
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
...@@ -189,6 +189,22 @@ const currentSecurityExecutionId = ref('') ...@@ -189,6 +189,22 @@ const currentSecurityExecutionId = ref('')
/** 当前用例类型(来自路由参数,默认 ui) */ /** 当前用例类型(来自路由参数,默认 ui) */
const currentType = computed(() => (route.params.type as string) || 'ui') const currentType = computed(() => (route.params.type as string) || 'ui')
/** 报告类型标签映射 */
const REPORT_TYPE_LABELS: Record<string, string> = {
ui: 'UI自动化测试',
api: '接口测试',
security: '安全测试',
functional: '功能测试报告',
}
const REPORT_TYPE_TAGS: Record<string, string> = {
ui: 'primary',
api: 'success',
security: 'danger',
functional: 'warning',
}
const reportTypeLabel = (type: string) => REPORT_TYPE_LABELS[type] || type
const reportTypeTag = (type: string) => REPORT_TYPE_TAGS[type] || 'info'
/** 渲染后的 Markdown */ /** 渲染后的 Markdown */
const renderedMarkdown = computed(() => { const renderedMarkdown = computed(() => {
if (!securityReportContent.value) return '' if (!securityReportContent.value) return ''
......
...@@ -223,7 +223,7 @@ ...@@ -223,7 +223,7 @@
import { ref, onMounted, reactive } from 'vue' import { ref, onMounted, reactive } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Plus, Refresh } from '@element-plus/icons-vue' import { Plus, Refresh } from '@element-plus/icons-vue'
import { listTasks, createTask, updateTask, deleteTask, runTask, stopTask } from '@/api/performance' import { listTasks, getTask, createTask, updateTask, deleteTask, runTask, stopTask } from '@/api/performance'
import type { PerformanceTask, PerformanceTaskListItem, PerformanceTaskCreate } from '@/types/performance' import type { PerformanceTask, PerformanceTaskListItem, PerformanceTaskCreate } from '@/types/performance'
defineEmits<{ defineEmits<{
...@@ -319,27 +319,36 @@ function openCreateDialog() { ...@@ -319,27 +319,36 @@ function openCreateDialog() {
} }
/** 打开编辑弹窗 */ /** 打开编辑弹窗 */
function openEditDialog(task: PerformanceTask) { async function openEditDialog(task: PerformanceTaskListItem) {
editingTask.value = task // 表格行是 PerformanceTaskListItem(缺少 body/headers/stepConcurrency 等字段),
// 必须调用 getTask 获取完整详情再填充表单
let fullTask: PerformanceTask
try {
fullTask = await getTask(task.id)
} catch {
// 回退到行数据(至少能显示名称/URL/模式等基本信息)
fullTask = task as unknown as PerformanceTask
}
editingTask.value = fullTask
Object.assign(form, { Object.assign(form, {
name: task.name, name: fullTask.name,
description: task.description, description: fullTask.description,
targetUrl: task.targetUrl, targetUrl: fullTask.targetUrl,
method: task.method, method: fullTask.method,
headers: task.headers ? JSON.stringify(task.headers, null, 2) : '', headers: fullTask.headers ? JSON.stringify(fullTask.headers, null, 2) : '',
body: task.body || '', body: fullTask.body ? (typeof fullTask.body === 'string' ? fullTask.body : JSON.stringify(fullTask.body, null, 2)) : '',
bodyTemplate: task.bodyTemplate, bodyTemplate: fullTask.bodyTemplate,
mode: task.mode, mode: fullTask.mode,
concurrency: task.concurrency, concurrency: fullTask.concurrency,
qpsTarget: task.qpsTarget, qpsTarget: fullTask.qpsTarget,
duration: task.duration, duration: fullTask.duration,
rampUp: task.rampUp, rampUp: fullTask.rampUp,
stepConcurrency: task.stepConcurrency, stepConcurrency: Array.isArray(fullTask.stepConcurrency) ? Math.max(...fullTask.stepConcurrency) : fullTask.stepConcurrency,
stepDuration: task.stepDuration, stepDuration: fullTask.stepDuration,
authRequired: task.authRequired, authRequired: fullTask.authRequired,
accountKey: task.accountKey, accountKey: fullTask.accountKey,
signRequest: task.signRequest, signRequest: fullTask.signRequest,
assertions: task.assertions || [], assertions: fullTask.assertions || [],
}) })
dialogVisible.value = true dialogVisible.value = true
} }
...@@ -377,7 +386,7 @@ async function handleSave() { ...@@ -377,7 +386,7 @@ async function handleSave() {
qpsTarget: form.mode === 'qps' ? form.qpsTarget : null, qpsTarget: form.mode === 'qps' ? form.qpsTarget : null,
duration: form.duration, duration: form.duration,
rampUp: form.rampUp || null, rampUp: form.rampUp || null,
stepConcurrency: form.mode === 'step' ? form.stepConcurrency : null, stepConcurrency: form.mode === 'step' && form.stepConcurrency ? [form.stepConcurrency] : null,
stepDuration: form.mode === 'step' ? (form.stepDuration || 30) : null, stepDuration: form.mode === 'step' ? (form.stepDuration || 30) : null,
authRequired: form.authRequired, authRequired: form.authRequired,
accountKey: form.authRequired ? form.accountKey : null, accountKey: form.authRequired ? form.accountKey : null,
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论