😎增加接口压测菜单和页面
This commit is contained in:
parent
3e19165557
commit
d99bd99c0d
@ -208,6 +208,7 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
|
|||||||
new SysMenu{ Id=1310000000621, Pid=1310000000601, Title="代码生成", Path="/develop/codeGen", Name="sysCodeGen", Component="/system/codeGen/index", Icon="ele-Crop", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 },
|
new SysMenu{ Id=1310000000621, Pid=1310000000601, Title="代码生成", Path="/develop/codeGen", Name="sysCodeGen", Component="/system/codeGen/index", Icon="ele-Crop", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 },
|
||||||
new SysMenu{ Id=1310000000631, Pid=1310000000601, Title="表单设计", Path="/develop/formDes", Name="sysFormDes", Component="/system/formDes/index", Icon="ele-Edit", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 },
|
new SysMenu{ Id=1310000000631, Pid=1310000000601, Title="表单设计", Path="/develop/formDes", Name="sysFormDes", Component="/system/formDes/index", Icon="ele-Edit", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 },
|
||||||
new SysMenu{ Id=1310000000641, Pid=1310000000601, Title="系统接口", Path="/develop/api", Name="sysApi", Component="layout/routerView/iframe", IsIframe=true, OutLink="http://localhost:5005", Icon="ele-Help", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 },
|
new SysMenu{ Id=1310000000641, Pid=1310000000601, Title="系统接口", Path="/develop/api", Name="sysApi", Component="layout/routerView/iframe", IsIframe=true, OutLink="http://localhost:5005", Icon="ele-Help", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 },
|
||||||
|
new SysMenu{ Id=1310000000651, Pid=1310000000601, Title="接口压测", Path="/develop/stressTest", Name="sysStressTest", Component="/system/stressTest/index", Icon="ele-Compass", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2024-12-28 00:00:00"), OrderNo=140 },
|
||||||
|
|
||||||
new SysMenu{ Id=1310000000701, Pid=0, Title="帮助文档", Path="/doc", Name="doc", Component="Layout", Icon="ele-Notebook", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=14000 },
|
new SysMenu{ Id=1310000000701, Pid=0, Title="帮助文档", Path="/doc", Name="doc", Component="Layout", Icon="ele-Notebook", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=14000 },
|
||||||
new SysMenu{ Id=1310000000711, Pid=1310000000701, Title="框架教程", Path="/doc/admin", Name="sysAdmin", Component="layout/routerView/link", IsIframe=false, IsKeepAlive=false, OutLink="http://101.43.53.74:5050/", Icon="ele-Sunny", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
|
new SysMenu{ Id=1310000000711, Pid=1310000000701, Title="框架教程", Path="/doc/admin", Name="sysAdmin", Component="layout/routerView/link", IsIframe=false, IsKeepAlive=false, OutLink="http://101.43.53.74:5050/", Icon="ele-Sunny", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
|
||||||
|
|||||||
@ -9,6 +9,7 @@ namespace Admin.NET.Plugin.ApprovalFlow;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 流程类型枚举
|
/// 流程类型枚举
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Description("流程类型枚举")]
|
||||||
public enum FlowTypeEnum
|
public enum FlowTypeEnum
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
203
Web/src/views/system/stressTest/component/editStressTest.vue
Normal file
203
Web/src/views/system/stressTest/component/editStressTest.vue
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sys-stress-test">
|
||||||
|
<el-dialog v-model="state.isShowDialog" draggable :close-on-click-modal="false" width="40vw">
|
||||||
|
<template #header>
|
||||||
|
<div style="color: #fff">
|
||||||
|
<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-Odometer /> </el-icon>
|
||||||
|
<span> 接口压测参数 </span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :model="state.ruleForm" ref="ruleFormRef" label-width="auto" v-loading="state.loading">
|
||||||
|
<el-row :gutter="35">
|
||||||
|
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||||
|
<el-form-item label="请求地址" :rules="[{ required: true, message: '请求地址不能为空', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="state.ruleForm.requestUri" placeholder="请求地址" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||||
|
<el-form-item label="请求方式" :rules="[{ required: true, message: '请求方式不能为空', trigger: 'blur' }]">
|
||||||
|
<el-select v-model="state.ruleForm.requestMethod" placeholder="请求方式">
|
||||||
|
<el-option :value="'GET'">GET</el-option>
|
||||||
|
<el-option :value="'PUT'">PUT</el-option>
|
||||||
|
<el-option :value="'POST'">POST</el-option>
|
||||||
|
<el-option :value="'DELETE'">DELETE</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||||
|
<el-form-item label="压测轮数" :rules="[{ required: true, message: '压测轮数不能为空', trigger: 'blur' }]">
|
||||||
|
<el-input-number v-model="state.ruleForm.numberOfRounds" placeholder="压测轮数" class="w100" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||||
|
<el-form-item label="每轮请求数" :rules="[{ required: true, message: '每轮请求数不能为空', trigger: 'blur' }]">
|
||||||
|
<el-input-number v-model="state.ruleForm.numberOfRequests" :step="100" placeholder="每轮请求数" class="w100" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
|
||||||
|
<el-form-item label="最大并发量">
|
||||||
|
<el-input-number v-model="state.ruleForm.maxDegreeOfParallelism" :step="5" placeholder="最大并发量" class="w100" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
|
||||||
|
<el-tabs v-model="state.activeName" addable @tab-add="addParams()">
|
||||||
|
<el-tab-pane label="请求头(Headers)" name="1">
|
||||||
|
<el-row v-for="(item, index) in state.ruleForm.headers" :key="index" :gutter="25" class="w100">
|
||||||
|
<el-col :xs="24" :sm="2" :md="2" :lg="2" :xl="2" class="mb10">
|
||||||
|
<el-button type="danger" size="small" icon="ele-Delete" text @click="() => state.ruleForm.headers.splice(index, 1)" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="4" :md="4" :lg="4" :xl="4" class="mb10">
|
||||||
|
<el-input v-model="item[0]" placeholder="参数名" clearable />
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="18" :md="18" :lg="18" :xl="18" class="mb10">
|
||||||
|
<el-input v-model="item[1]" placeholder="参数值" clearable />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="请求体(Body)" name="2">
|
||||||
|
<el-row v-for="(item, index) in state.ruleForm.requestParameters" :key="index" :gutter="25" class="w100">
|
||||||
|
<el-col :xs="24" :sm="2" :md="2" :lg="2" :xl="2" class="mb10">
|
||||||
|
<el-button type="danger" size="small" icon="ele-Delete" text @click="() => state.ruleForm.requestParameters.splice(index, 1)" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="4" :md="4" :lg="4" :xl="4" class="mb10">
|
||||||
|
<el-input v-model="item[0]" placeholder="参数名" clearable />
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="18" :md="18" :lg="18" :xl="18" class="mb10">
|
||||||
|
<el-input v-model="item[1]" placeholder="参数值" clearable />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="路径参数(Path)" name="3">
|
||||||
|
<el-row v-for="(item, index) in state.ruleForm.pathParameters" :key="index" :gutter="25" class="w100">
|
||||||
|
<el-col :xs="24" :sm="2" :md="2" :lg="2" :xl="2" class="mb10">
|
||||||
|
<el-button type="danger" size="small" icon="ele-Delete" text @click="() => state.ruleForm.pathParameters.splice(index, 1)" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="4" :md="4" :lg="4" :xl="4" class="mb10">
|
||||||
|
<el-input v-model="item[0]" placeholder="参数名" clearable />
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="18" :md="18" :lg="18" :xl="18" class="mb10">
|
||||||
|
<el-input v-model="item[1]" placeholder="参数值" clearable />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="查询参数(Query)" name="4">
|
||||||
|
<el-row v-for="(item, index) in state.ruleForm.queryParameters" :key="index" :gutter="25" class="w100">
|
||||||
|
<el-col :xs="24" :sm="2" :md="2" :lg="2" :xl="2" class="mb10">
|
||||||
|
<el-button type="danger" size="small" icon="ele-Delete" text @click="() => state.ruleForm.queryParameters.splice(index, 1)" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="4" :md="4" :lg="4" :xl="4" class="mb10">
|
||||||
|
<el-input v-model="item[0]" placeholder="参数名" clearable />
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="18" :md="18" :lg="18" :xl="18" class="mb10">
|
||||||
|
<el-input v-model="item[1]" placeholder="参数值" clearable />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer" v-loading="state.loading">
|
||||||
|
<el-button @click="() => (state.isShowDialog = false)">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="submit" v-reclick="1000">确 定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup name="sysStressTest">
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { SysCommonApi } from '/@/api-services';
|
||||||
|
import { getAPI } from '/@/utils/axios-utils';
|
||||||
|
|
||||||
|
const emits = defineEmits(['refreshData']);
|
||||||
|
const ruleFormRef = ref();
|
||||||
|
const state = reactive({
|
||||||
|
isShowDialog: false,
|
||||||
|
activeName: '1',
|
||||||
|
loading: false,
|
||||||
|
ruleForm: {
|
||||||
|
requestUri: '',
|
||||||
|
requestMethod: 'GET',
|
||||||
|
numberOfRounds: 1,
|
||||||
|
numberOfRequests: 100,
|
||||||
|
maxDegreeOfParallelism: 500,
|
||||||
|
requestParameters: [[]] as [[]] | {},
|
||||||
|
queryParameters: [[]] as [[]] | {},
|
||||||
|
pathParameters: [[]] as [[]] | {},
|
||||||
|
headers: [[]] as [[]] | {},
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 格式化参数
|
||||||
|
const formatParameter = (params: any[] | {}) => {
|
||||||
|
if (Array.isArray(params)) {
|
||||||
|
return Object.fromEntries(params.filter((e) => e.length === 2));
|
||||||
|
} else if (typeof params === 'object' && params !== null) {
|
||||||
|
return Object.entries(params);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开弹窗
|
||||||
|
const openDialog = (row: any) => {
|
||||||
|
const newRow = { ...state.ruleForm, ...row }; // 合并默认值和新值
|
||||||
|
state.ruleForm = {
|
||||||
|
...newRow,
|
||||||
|
requestMethod: row.requestMethod?.toUpperCase() ?? 'GET',
|
||||||
|
};
|
||||||
|
state.isShowDialog = true;
|
||||||
|
ruleFormRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交
|
||||||
|
const submit = () => {
|
||||||
|
ruleFormRef.value.validate(async (valid: boolean) => {
|
||||||
|
if (!valid) return;
|
||||||
|
try {
|
||||||
|
state.loading = true;
|
||||||
|
|
||||||
|
// 创建一个新的对象来保存格式化后的数据
|
||||||
|
const formattedRuleForm = {
|
||||||
|
...state.ruleForm,
|
||||||
|
headers: formatParameter(state.ruleForm.headers),
|
||||||
|
pathParameters: formatParameter(state.ruleForm.pathParameters),
|
||||||
|
queryParameters: formatParameter(state.ruleForm.queryParameters),
|
||||||
|
requestParameters: formatParameter(state.ruleForm.requestParameters),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// 确保所有可能是空对象的参数被正确设置为 undefined
|
||||||
|
['headers', 'pathParameters', 'queryParameters', 'requestParameters'].forEach((paramKey) => {
|
||||||
|
if (Object.keys(formattedRuleForm[paramKey] || {}).length === 0) {
|
||||||
|
formattedRuleForm[paramKey] = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emits(
|
||||||
|
'refreshData',
|
||||||
|
await getAPI(SysCommonApi)
|
||||||
|
.apiSysCommonStressTestPost(formattedRuleForm, { timeout: 0 })
|
||||||
|
.then((res) => res.data.result)
|
||||||
|
);
|
||||||
|
state.isShowDialog = false;
|
||||||
|
} finally {
|
||||||
|
state.loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加参数事件
|
||||||
|
const addParams = () => {
|
||||||
|
const paramType = ['headers', 'requestParameters', 'pathParameters', 'queryParameters'][+state.activeName - 1];
|
||||||
|
if (Array.isArray(state.ruleForm[paramType])) {
|
||||||
|
state.ruleForm[paramType].push([null, null]);
|
||||||
|
} else if (typeof state.ruleForm[paramType] === 'object') {
|
||||||
|
state.ruleForm[paramType] = [[null, null]];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出对象
|
||||||
|
defineExpose({ openDialog });
|
||||||
|
</script>
|
||||||
284
Web/src/views/system/stressTest/index.vue
Normal file
284
Web/src/views/system/stressTest/index.vue
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sys-stress-test h100 overlay-none">
|
||||||
|
<div>
|
||||||
|
<NoticeBar text="接口压测会占用服务器大量的系统资源(内存、带宽),请慎重操作!!!" style="margin: 4px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<splitpanes class="default-theme overlay-hidden">
|
||||||
|
<pane size="20" class="vh100">
|
||||||
|
<el-card class="vh80" shadow="hover" header="" v-loading="state.loading">
|
||||||
|
<el-row :gutter="35">
|
||||||
|
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb10">
|
||||||
|
<el-select v-model="state.swaggerUrl" placeholder="接口分组">
|
||||||
|
<el-option v-for="(item, index) in state.apiGroupList" :key="index" :label="item.name" :value="item.url" />
|
||||||
|
</el-select>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb10">
|
||||||
|
<el-input v-model="state.keywords" placeholder="接口名称" clearable>
|
||||||
|
<template #append>
|
||||||
|
<el-button icon="ele-Search" v-reclick="100" @click="queryTreeNode()" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-tree
|
||||||
|
ref="treeRef"
|
||||||
|
class="filter-tree overlay-y vh68"
|
||||||
|
style="padding-bottom: 60px"
|
||||||
|
:data="state.apiList"
|
||||||
|
:props="{ children: 'children', label: 'summary' }"
|
||||||
|
:filter-node-method="filterNode"
|
||||||
|
node-key="id"
|
||||||
|
highlight-current
|
||||||
|
check-strictly
|
||||||
|
>
|
||||||
|
<template #default="{ node }">
|
||||||
|
<el-icon v-if="node.level == 1" size="16" style="margin-right: 3px; display: inline; vertical-align: middle"><ele-Menu /></el-icon>
|
||||||
|
<el-icon v-else size="16" style="margin-right: 3px; display: inline; vertical-align: middle"><ele-Link /></el-icon>
|
||||||
|
{{ node.label }}
|
||||||
|
<span class="node-button" v-if="!node.data.children">
|
||||||
|
<el-button type="warning" plain icon="ele-Position" @click="treeNodeTest(node.data)" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</el-card>
|
||||||
|
</pane>
|
||||||
|
|
||||||
|
<pane size="80" class="vh100">
|
||||||
|
<el-card class="main-container vh80" shadow="hover" header="" v-loading="state.loading" body-style="height:100vh; overflow:auto">
|
||||||
|
<template #header>
|
||||||
|
<el-button type="primary" icon="ele-Odometer" @click="showDialog(undefined)">接口压测</el-button>
|
||||||
|
</template>
|
||||||
|
<el-descriptions title="⚡压测参数" label-width="180px" :column="1" class="mb20" border>
|
||||||
|
<el-descriptions-item label="请求地址" label-align="left" align="left">
|
||||||
|
{{ state.ruleForm.requestUri }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="请求方式" label-align="left" align="left">
|
||||||
|
<el-tag>{{ state.ruleForm.requestMethod?.toUpperCase() }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="压测轮数" label-align="left" align="left">
|
||||||
|
{{ state.ruleForm.numberOfRounds ?? 1 }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="每轮请求数" label-align="left" align="left">
|
||||||
|
{{ state.ruleForm.numberOfRequests ?? 1 }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最大并发量" label-align="left" align="left">
|
||||||
|
{{ state.ruleForm.maxDegreeOfParallelism ?? 1 }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<el-descriptions title="⚡压测结果" label-width="180px" :column="4" border>
|
||||||
|
<el-descriptions-item label="总用时(秒)" label-align="left" align="left">
|
||||||
|
<el-tag>{{ (state.result.totalTimeInSeconds ?? 0).toFixed(2) }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="成功请求次数" label-align="left" align="left">
|
||||||
|
<el-tag type="success">{{ state.result.successfulRequests ?? 0 }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="失败请求次数" label-align="left" align="left">
|
||||||
|
<el-tag type="danger">{{ state.result.failedRequests ?? 0 }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="每秒查询率(QPS)" label-align="left" align="left">
|
||||||
|
<el-tag>{{ (state.result.queriesPerSecond ?? 0).toFixed(2) }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="最小响应时间(毫秒)" label-align="left" align="left">
|
||||||
|
{{ (state.result.minResponseTime ?? 0).toFixed(2) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最大响应时间(毫秒)" label-align="left" align="left">
|
||||||
|
{{ (state.result.maxResponseTime ?? 0).toFixed(2) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="平均响应时间(毫秒)" span="3" label-align="left" align="left">
|
||||||
|
{{ (state.result.averageResponseTime ?? 0).toFixed(2) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="P10 响应时间(毫秒)" label-align="left" align="left">
|
||||||
|
{{ (state.result.percentile10ResponseTime ?? 0).toFixed(2) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="P25 响应时间(毫秒)" label-align="left" align="left">
|
||||||
|
{{ (state.result.percentile25ResponseTime ?? 0).toFixed(2) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="P50 响应时间(毫秒)" label-align="left" align="left">
|
||||||
|
{{ (state.result.percentile50ResponseTime ?? 0).toFixed(2) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="P75 响应时间(毫秒)" label-align="left" align="left">
|
||||||
|
{{ (state.result.percentile75ResponseTime ?? 0).toFixed(2) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="P90 响应时间(毫秒)" label-align="left" align="left">
|
||||||
|
{{ (state.result.percentile90ResponseTime ?? 0).toFixed(2) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="P99 响应时间(毫秒)" label-align="left" align="left">
|
||||||
|
{{ (state.result.percentile99ResponseTime ?? 0).toFixed(2) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="P999 响应时间(毫秒)" label-align="left" align="left">
|
||||||
|
{{ (state.result.percentile9999ResponseTime ?? 0).toFixed(2) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
</pane>
|
||||||
|
</splitpanes>
|
||||||
|
|
||||||
|
<EditStressTest ref="editStressTestRef" @refreshData="refreshData" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup name="sysStressTest">
|
||||||
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
import { ElTree } from 'element-plus';
|
||||||
|
import { Splitpanes, Pane } from 'splitpanes';
|
||||||
|
import 'splitpanes/dist/splitpanes.css';
|
||||||
|
import 'vue-json-pretty/lib/styles.css';
|
||||||
|
|
||||||
|
import EditStressTest from './component/editStressTest.vue';
|
||||||
|
import NoticeBar from '/@/components/noticeBar/index.vue';
|
||||||
|
|
||||||
|
import request, { getToken } from '/@/utils/request';
|
||||||
|
import { StressTestHarnessResult } from '/@/api-services';
|
||||||
|
|
||||||
|
const editStressTestRef = ref();
|
||||||
|
const treeRef = ref<InstanceType<typeof ElTree>>();
|
||||||
|
const state = reactive({
|
||||||
|
loading: false,
|
||||||
|
activeName: '',
|
||||||
|
ruleForm: {
|
||||||
|
requestUri: '',
|
||||||
|
requestMethod: 'GET',
|
||||||
|
numberOfRounds: 1,
|
||||||
|
numberOfRequests: 100,
|
||||||
|
maxDegreeOfParallelism: 500,
|
||||||
|
requestParameters: [[]],
|
||||||
|
queryParameters: [[]],
|
||||||
|
pathParameters: [[]],
|
||||||
|
headers: [[]],
|
||||||
|
},
|
||||||
|
keywords: undefined,
|
||||||
|
swaggerUrl: '/swagger/Default/swagger.json',
|
||||||
|
result: {} as StressTestHarnessResult,
|
||||||
|
apiList: [] as Array<any>,
|
||||||
|
apiGroupList: [] as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页面初始化
|
||||||
|
onMounted(async () => {
|
||||||
|
state.apiGroupList = await getApiGroupList();
|
||||||
|
state.apiList = await getApiList('');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取接口分组列表
|
||||||
|
const getApiGroupList = async () => {
|
||||||
|
try {
|
||||||
|
const html = await request(`/index.html`, { method: 'get' }).then(({ data }) => data);
|
||||||
|
const prefixText = "var configObject = JSON.parse('";
|
||||||
|
const jsonStr = html
|
||||||
|
.substring(html.indexOf(prefixText) + prefixText.length, html.indexOf('var oauthConfigObject = JSON.parse('))
|
||||||
|
?.trim()
|
||||||
|
.replace("');", '');
|
||||||
|
return JSON.parse(jsonStr).urls;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取所有接口列表
|
||||||
|
const getApiList = (keywords: string | undefined) => {
|
||||||
|
const emojiPattern =
|
||||||
|
/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu;
|
||||||
|
return request(state.swaggerUrl, { method: 'get' }).then(({ data }) => {
|
||||||
|
const pathMap = data.paths;
|
||||||
|
const result = data.tags.map((e: any) => ({ path: e.name, summary: e.description.replaceAll(emojiPattern, ''), children: [] }));
|
||||||
|
Object.keys(pathMap).map((path) => {
|
||||||
|
const method = Object.keys(pathMap[path])[0];
|
||||||
|
const apiInfo = pathMap[path][method];
|
||||||
|
if (keywords && apiInfo.summary?.indexOf(keywords) === -1) return;
|
||||||
|
result
|
||||||
|
.find((u: any) => u.path === apiInfo.tags[0])
|
||||||
|
.children.push({
|
||||||
|
path: path,
|
||||||
|
method: method,
|
||||||
|
summary: apiInfo.summary?.replaceAll(emojiPattern, '') ?? path,
|
||||||
|
parameters: apiInfo.parameters,
|
||||||
|
requestBody: apiInfo.requestBody,
|
||||||
|
data: apiInfo,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return result.filter((u: any) => u.children.length > 0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const refreshData = (data: StressTestHarnessResult) => {
|
||||||
|
state.result = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开接口参数设置页面
|
||||||
|
const showDialog = async (row: any) => {
|
||||||
|
const newRow = row ?? { ...state.ruleForm };
|
||||||
|
const convertToKeyValuePairs = (params: any) => {
|
||||||
|
if (Array.isArray(params) && params.every((item) => Array.isArray(item) && item.length === 2)) {
|
||||||
|
return params;
|
||||||
|
} else if (typeof params === 'object' && params !== null) {
|
||||||
|
return Object.entries(params);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
state.ruleForm = {
|
||||||
|
...newRow,
|
||||||
|
requestParameters: convertToKeyValuePairs(newRow.requestParameters),
|
||||||
|
queryParameters: convertToKeyValuePairs(newRow.queryParameters),
|
||||||
|
pathParameters: convertToKeyValuePairs(newRow.pathParameters),
|
||||||
|
headers: convertToKeyValuePairs(newRow.headers),
|
||||||
|
};
|
||||||
|
editStressTestRef.value.openDialog(state.ruleForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 接口树节点按钮事件
|
||||||
|
const treeNodeTest = async (node: any) => {
|
||||||
|
if (node.id == 0) return;
|
||||||
|
state.ruleForm = {
|
||||||
|
requestUri: location.origin + node.path,
|
||||||
|
requestMethod: node.method,
|
||||||
|
numberOfRounds: 1,
|
||||||
|
numberOfRequests: 100,
|
||||||
|
maxDegreeOfParallelism: 500,
|
||||||
|
requestParameters: [],
|
||||||
|
queryParameters: [],
|
||||||
|
pathParameters: [],
|
||||||
|
headers: [['Authorization', 'Bearer ' + getToken()]] as any,
|
||||||
|
};
|
||||||
|
showDialog(state.ruleForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查询树节点
|
||||||
|
const queryTreeNode = async () => {
|
||||||
|
state.apiList = await getApiList(state.keywords);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterNode = (value: string, data: any) => {
|
||||||
|
if (!value) return true;
|
||||||
|
return data.name.includes(value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.card-header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
:deep(.el-collapse-item) {
|
||||||
|
.el-collapse-item__arrow {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.main-container) {
|
||||||
|
.el-card__header {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.node-button {
|
||||||
|
position: absolute;
|
||||||
|
scale: 0.7;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user