<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="接口分组" size="small">
|
<el-option :label="item.name" :value="item.url" v-for="(item, index) in state.groupList" :key="index" />
|
</el-select>
|
</el-col>
|
<el-col :xs="24" :sm="18" :md="18" :lg="18" :xl="18" class="mb10">
|
<el-input v-model="state.keywords" size="small" placeholder="关键字" clearable />
|
</el-col>
|
<el-col :xs="24" :sm="6" :md="6" :lg="6" :xl="6" class="mb10">
|
<el-button size="small" icon="ele-Search" v-reclick="1000" @click="queryTreeNode()" />
|
</el-col>
|
</el-row>
|
<el-tree
|
ref="treeRef"
|
class="filter-tree overlay-y vh68"
|
style="padding-bottom: 60px"
|
:data="state.data"
|
:props="{ children: 'children', label: 'summary' }"
|
:filter-node-method="filterNode"
|
node-key="id"
|
highlight-current
|
check-strictly>
|
<template #default="{ node }">
|
{{ node.label }}
|
<span class="node-button" v-if="!node.data.children">
|
<el-button size="small" icon="ele-DataLine" @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" @click="showDialog(undefined)">开始测试</el-button>
|
</template>
|
<el-descriptions title="压测参数" label-width="180px" :column="2" class="mb20" border>
|
<el-descriptions-item label="请求方式" label-align="left" align="left">
|
{{ state.ruleForm.requestMethod?.toUpperCase() }}
|
</el-descriptions-item>
|
<el-descriptions-item label="请求地址" label-align="left" align="left">
|
{{ state.ruleForm.requestUri }}
|
</el-descriptions-item>
|
<el-descriptions-item label="轮数" label-align="left" align="left">
|
{{ state.ruleForm.numberOfRounds ?? 0 }}
|
</el-descriptions-item>
|
<el-descriptions-item label="每轮请求数" label-align="left" align="left">
|
{{ state.ruleForm.numberOfRequests ?? 0 }}
|
</el-descriptions-item>
|
<el-descriptions-item label="最大并发量" label-align="left" align="left">
|
{{ state.ruleForm.maxDegreeOfParallelism ?? 0 }}
|
</el-descriptions-item>
|
</el-descriptions>
|
<el-descriptions title="压测结果" label-width="180px" :column="3" border>
|
<el-descriptions-item label="总用时(秒)" label-align="left" align="left">
|
{{ (state.result.totalTimeInSeconds ?? 0).toFixed(2) }}
|
</el-descriptions-item>
|
<el-descriptions-item label="成功请求次数" label-align="left" align="left">
|
{{ state.result.successfulRequests ?? 0 }}
|
</el-descriptions-item>
|
<el-descriptions-item label="失败请求次数" label-align="left" align="left">
|
{{ state.result.failedRequests ?? 0 }}
|
</el-descriptions-item>
|
<el-descriptions-item label="每秒查询率(QPS)" label-align="left" align="left">
|
{{ (state.result.queriesPerSecond ?? 0).toFixed(2) }}
|
</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.percentile999ResponseTime ?? 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 EditStressTest from './component/editStressTest.vue';
|
import NoticeBar from '/@/components/noticeBar/index.vue';
|
import request, { getToken } from '/@/utils/request';
|
import { StressTestOutput } from "/@/api-services";
|
import { Splitpanes, Pane } from 'splitpanes';
|
import { ElTree } from 'element-plus';
|
import 'splitpanes/dist/splitpanes.css';
|
import 'vue-json-pretty/lib/styles.css';
|
|
const editStressTestRef = ref();
|
const treeRef = ref<InstanceType<typeof ElTree>>();
|
const state = reactive({
|
loading: false,
|
activeName: '',
|
ruleForm: {
|
requestUri: '',
|
requestMethod: 'GET',
|
numberOfRounds: 1,
|
numberOfRequests: 100,
|
maxDegreeOfParallelism: 200,
|
requestParameters: [[]],
|
queryParameters: [[]],
|
pathParameters: [[]],
|
headers: [[]],
|
},
|
keywords: undefined,
|
swaggerUrl: '/swagger/Default/swagger.json',
|
result: {} as StressTestOutput,
|
data: [] as Array<any>,
|
groupList: [],
|
});
|
|
onMounted(async () => {
|
state.groupList = await getGroupList();
|
state.data = await getApiList();
|
});
|
|
// 获取分组列表
|
const getGroupList = 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 treeNodeTest = async (node: any) => {
|
if (node.id == 0) return;
|
state.ruleForm = {
|
requestUri: location.origin + node.path,
|
requestMethod: node.method,
|
numberOfRounds: 1,
|
numberOfRequests: 100,
|
maxDegreeOfParallelism: 200,
|
requestParameters: [],
|
queryParameters: [],
|
pathParameters: [],
|
headers: [
|
['Authorization', 'Bearer ' + getToken()]
|
],
|
};
|
showDialog(state.ruleForm)
|
};
|
|
const showDialog = async (row: any) => {
|
const newRow = row ?? { ...state.ruleForm };
|
const convertToKeyValuePairs = (params) => {
|
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 refreshData = (data: StressTestOutput) => {
|
state.result = data;
|
}
|
|
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 => u.children.length > 0);
|
});
|
}
|
|
// 查询树节点
|
const queryTreeNode = async () => {
|
state.data = 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>
|