Просмотр исходного кода

feat: 😆增加接口压测前端页面

喵你个旺呀 1 год назад
Родитель
Сommit
7e1ce6b46f

+ 2 - 1
Admin.NET/Admin.NET.Core/SeedData/SysMenuSeedData.cs

@@ -212,7 +212,8 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             new SysMenu{ Id=1300500010101, Pid=1300500000101, Title="库表管理", Path="/develop/database", Name="sysDatabase", Component="/system/database/index",Icon="ele-Coin", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1300500020101, Pid=1300500000101, 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=1300500030101, Pid=1300500000101, 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=1300500040101, Pid=1300500000101, 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=1300500040101, Pid=1300500000101, Title="接口压测", Path="/develop/stressTest", Name="SysStressTest", Component="/system/stressTest/index", Icon="ele-DataLine", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 },
+            new SysMenu{ Id=1300500050101, Pid=1300500000101, 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=140 },
 
             new SysMenu{ Id=1300600000101, 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=1300600010101, Pid=1300600000101, 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 },

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/Common/Dto/SysCommonInput.cs

@@ -22,7 +22,7 @@ public class StressTestInput
     /// 请求方式
     /// </summary>
     [Required(ErrorMessage = "请求方式不能为空")]
-    public HttpMethod? RequestMethod { get; set; } = HttpMethod.Get;
+    public string RequestMethod { get; set; } = nameof(HttpMethod.Get);
 
     /// <summary>
     /// 每轮请求量

+ 10 - 2
Admin.NET/Admin.NET.Core/Service/Common/SysCommonService.cs

@@ -4,6 +4,7 @@
 //
 // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
 
+using Azure.Core;
 using Microsoft.AspNetCore.Mvc.ApiExplorer;
 using Org.BouncyCastle.Crypto.Parameters;
 using Org.BouncyCastle.Utilities.Encoders;
@@ -151,7 +152,14 @@ public class SysCommonService : IDynamicApiController, ITransient
                 uriBuilder.Query = queryString.ToString() ?? string.Empty;
                 var fullUri = uriBuilder.Uri.ToString();
 
-                HttpRequestMessage request = new(input.RequestMethod!, fullUri);
+                HttpRequestMessage request = input.RequestMethod.ToUpper() switch
+                {
+                    "GET" => new HttpRequestMessage(HttpMethod.Get, fullUri),
+                    "PUT" => new HttpRequestMessage(HttpMethod.Put, fullUri),
+                    "POST" => new HttpRequestMessage(HttpMethod.Post, fullUri),
+                    "DELETE" => new HttpRequestMessage(HttpMethod.Delete, fullUri),
+                    _ => throw Oops.Bah("请求方式异常")
+                };
 
                 // 设置请求头
                 foreach (var header in input.Headers)
@@ -159,7 +167,7 @@ public class SysCommonService : IDynamicApiController, ITransient
                     request.Headers.Add(header.Key, header.Value);
                 }
 
-                if (input.RequestMethod != HttpMethod.Get && input.RequestParameters.Any())
+                if (nameof(HttpMethod.Get).EqualIgnoreCase(input.RequestMethod) && input.RequestParameters.Any())
                 {
                     var content = new FormUrlEncodedContent(input.RequestParameters);
                     request.Content = content;

+ 206 - 0
Web/src/views/system/stressTest/component/editStressTest.vue

@@ -0,0 +1,206 @@
+<template>
+	<div class="sys-stress-test">
+		<el-dialog v-model="state.isShowDialog" draggable :close-on-click-modal="false" width="700px">
+			<template #header>
+				<div style="color: #fff">
+					<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-DataLine /> </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-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="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-input-number v-model="state.ruleForm.numberOfRounds" placeholder="轮数" />
+						</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="每轮请求数" />
+						</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="最大并发量" />
+						</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 :gutter="25" class="w100">
+									<template v-for="(item, index) in state.ruleForm.headers" :key="index">
+										<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="6" :md="6" :lg="6" :xl="6" class="mb10">
+											<el-input v-model="item[0]" placeholder="参数名" clearable />
+										</el-col>
+										<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16" class="mb10">
+											<el-input v-model="item[1]" placeholder="参数值" clearable />
+										</el-col>
+									</template>
+								</el-row>
+							</el-tab-pane>
+							<el-tab-pane label="Body" name="2">
+								<el-row :gutter="25" class="w100">
+									<template v-for="(item, index) in state.ruleForm.requestParameters" :key="index">
+										<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="6" :md="6" :lg="6" :xl="6" class="mb10">
+											<el-input v-model="item[0]" placeholder="参数名" clearable />
+										</el-col>
+										<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16" class="mb10">
+											<el-input v-model="item[1]" placeholder="参数值" clearable />
+										</el-col>
+									</template>
+								</el-row>
+							</el-tab-pane>
+							<el-tab-pane label="Path" name="3">
+								<el-row :gutter="25" class="w100">
+									<template v-for="(item, index) in state.ruleForm.pathParameters" :key="index">
+										<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="6" :md="6" :lg="6" :xl="6" class="mb10">
+											<el-input v-model="item[0]" placeholder="参数名" clearable />
+										</el-col>
+										<el-col :xs="24" :sm="14" :md="14" :lg="14" :xl="14" class="mb10">
+											<el-input v-model="item[1]" placeholder="参数值" clearable/>
+										</el-col>
+									</template>
+								</el-row>
+							</el-tab-pane>
+							<el-tab-pane label="Query" name="4">
+								<el-row :gutter="25" class="w100">
+									<template v-for="(item, index) in state.ruleForm.queryParameters" :key="index">
+										<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="6" :md="6" :lg="6" :xl="6" class="mb10">
+											<el-input v-model="item[0]" placeholder="参数名" clearable/>
+										</el-col>
+										<el-col :xs="24" :sm="14" :md="14" :lg="14" :xl="14" class="mb10">
+											<el-input v-model="item[1]" placeholder="参数值" clearable/>
+										</el-col>
+									</template>
+								</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">确 定</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: 200,
+		requestParameters: [[]] as [[]] | {},
+		queryParameters: [[]] as [[]] | {},
+		pathParameters: [[]] as [[]] | {},
+		headers: [[]] as [[]] | {},
+	},
+});
+
+// 格式化参数
+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),
+			};
+
+			// 确保所有可能是空对象的参数被正确设置为 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>

+ 266 - 0
Web/src/views/system/stressTest/index.vue

@@ -0,0 +1,266 @@
+<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" @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>
+	</div>
+	<EditStressTest ref="editStressTestRef" @refreshData="refreshData" />
+</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>