perf: 支持网络测试

This commit is contained in:
xiaojunnuo
2025-09-30 23:27:31 +08:00
parent aee13ad909
commit 2bef608e07
12 changed files with 916 additions and 61 deletions

View File

@@ -175,6 +175,7 @@ export default {
suiteSetting: "Suite Settings",
orderManager: "Order Management",
userSuites: "User Suites",
netTest: "Network Test",
},
certificateRepo: {
title: "Certificate Repository",

View File

@@ -181,6 +181,7 @@ export default {
suiteSetting: "套餐设置",
orderManager: "订单管理",
userSuites: "用户套餐",
netTest: "网络测试",
},
certificateRepo: {
title: "证书仓库",

View File

@@ -249,6 +249,17 @@ export const sysResources = [
},
],
},
{
title: "certd.sysResources.netTest",
name: "NetTest",
path: "/sys/nettest",
component: "/sys/nettest/index.vue",
meta: {
icon: "ion:build-outline",
auth: true,
keepAlive: true,
},
},
],
},
];

View File

@@ -0,0 +1,186 @@
<template>
<div class="domain-test-card">
<div class="card-header">
<a-form v-if="editing" layout="inline" :model="formData">
<a-form-item label="域名">
<a-input v-model:value="formData.domain" placeholder="请输入要测试的域名或IP" style="width: 240px" />
</a-form-item>
<a-form-item label="端口">
<a-input-number v-model:value="formData.port" placeholder="请输入端口" :min="1" :max="65535" style="width: 120px" />
</a-form-item>
</a-form>
<div v-else class="domain-info">
<span>域名: {{ formData.domain }}</span>
<span>端口: {{ formData.port }}</span>
</div>
<a-button :disabled="!formData.domain" size="small" type="primary" :loading="loading" @click="runAllTests"> 开始测试 </a-button>
</div>
<div class="card-content">
<div class="test-results">
<!-- 域名解析结果 -->
<test-case ref="domainResolveRef" title="域名解析" :test-method="() => createDomainResolveMethod()" :disabled="!getCurrentDomain()" />
<!-- Ping测试结果 -->
<test-case ref="pingTestRef" title="Ping测试" :test-method="() => createPingTestMethod()" :disabled="!getCurrentDomain()" />
<!-- Telnet测试结果 -->
<test-case ref="telnetTestRef" title="Telnet测试" :port="getCurrentPort()" :test-method="() => createTelnetTestMethod()" :disabled="!getCurrentDomain() || !getCurrentPort()" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted } from "vue";
import { message } from "ant-design-vue";
import { DomainResolve, PingTest, TelnetTest } from "./api";
import TestCase from "./TestCase.vue";
// 组件属性
const props = defineProps<{
domain?: string;
port?: number;
autoStart?: boolean;
}>();
const editing = ref(!props.domain);
// 测试组件的引用
const domainResolveRef = ref();
const pingTestRef = ref();
const telnetTestRef = ref();
// 表单数据
const formData = reactive({
domain: props.domain || "",
port: props.port || 443,
});
// 加载状态
const loading = ref(false);
// 创建域名解析测试方法
const createDomainResolveMethod = async () => {
const domain = getCurrentDomain();
return DomainResolve(domain);
};
// 创建Ping测试方法
const createPingTestMethod = async () => {
const domain = getCurrentDomain();
return PingTest(domain);
};
// 创建Telnet测试方法
const createTelnetTestMethod = async () => {
const domain = getCurrentDomain();
const port = getCurrentPort();
return TelnetTest(domain, port);
};
// 获取当前使用的域名
const getCurrentDomain = () => {
return formData.domain;
};
// 获取当前使用的端口
const getCurrentPort = () => {
return formData.port;
};
// 运行全部测试
async function runAllTests() {
const domain = getCurrentDomain();
// 检查是否有域名
if (!domain) {
message.error("请输入域名");
return;
}
loading.value = true;
// 通过组件引用调用测试方法
try {
await Promise.allSettled([domainResolveRef.value?.test(), pingTestRef.value?.test(), telnetTestRef.value?.test()]);
message.success("所有测试已完成");
} catch (error) {
message.error("部分测试执行失败请查看详细结果");
} finally {
loading.value = false;
}
}
onMounted(() => {
if (props.autoStart) {
runAllTests();
}
});
</script>
<style lang="less" scoped>
.domain-test-card {
border: 1px solid #e8e8e8;
border-radius: 4px;
overflow: hidden;
background-color: #fff;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #fafafa;
border-bottom: 1px solid #e8e8e8;
}
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
}
.card-content {
padding: 16px;
}
.input-form {
margin-bottom: 12px;
padding: 12px;
background-color: #fafafa;
border-radius: 4px;
}
.domain-info {
padding: 5.5px 12px;
background-color: #f0f0f0;
border-radius: 4px;
display: flex;
gap: 16px;
font-size: 14px;
color: #666;
}
.test-buttons {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.test-results {
margin-top: 0px;
}
/* 调整按钮大小 */
.ant-btn {
font-size: 12px;
padding: 2px 8px;
height: 24px;
}
</style>

View File

@@ -0,0 +1,146 @@
<template>
<a-card title="服务端信息" class="server-info-card">
<template #extra>
<a-button size="small" :loading="loading" @click="refreshServerInfo">
<template #icon>
<a-icon type="sync" :spin="loading" />
</template>
刷新
</a-button>
</template>
<div v-if="loading" class="loading">
<a-spin size="small" />
<span style="margin-left: 8px">加载中...</span>
</div>
<div v-else-if="error" class="error">
<a-alert message="获取服务器信息失败" :description="error" type="error" show-icon />
</div>
<div v-else class="server-info-grid">
<!-- 本地IP -->
<div class="info-item">
<div class="info-label">本地IP:</div>
<div v-if="serverInfo.localIP" class="info-value">
<a-list item-layout="horizontal" :data-source="serverInfo.localIP">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta description="{{ item }}" />
</a-list-item>
</template>
</a-list>
</div>
<div v-else class="info-empty">暂无信息</div>
</div>
<!-- 外网IP -->
<div class="info-item">
<div class="info-label">外网IP:</div>
<div v-if="serverInfo.publicIP" class="info-value">
{{ serverInfo.publicIP }}
</div>
<div v-else class="info-empty">暂无信息</div>
</div>
<!-- DNS服务器 -->
<div class="info-item">
<div class="info-label">DNS服务器:</div>
<div v-if="serverInfo.dnsServers && serverInfo.dnsServers.length > 0" class="info-value">
{{ serverInfo.dnsServers.join(", ") }}
</div>
<div v-else class="info-empty">暂无信息</div>
</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { message } from "ant-design-vue";
import { GetServerInfo } from "./api";
// 服务器信息类型
interface ServerInfo {
localIP?: string[];
publicIP?: string;
dnsServers?: string[];
}
const loading = ref(false);
const error = ref<string | null>(null);
const serverInfo = ref<ServerInfo>({});
// 加载服务器信息
const loadServerInfo = async () => {
loading.value = true;
error.value = null;
try {
serverInfo.value = await GetServerInfo();
} catch (e) {
error.value = e instanceof Error ? e.message : String(e);
message.error("获取服务器信息失败");
} finally {
loading.value = false;
}
};
// 刷新服务器信息
const refreshServerInfo = () => {
loadServerInfo();
};
// 组件挂载时加载数据
onMounted(() => {
loadServerInfo();
});
</script>
<style lang="less">
.server-info-card {
margin-bottom: 16px;
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #666;
}
.error {
margin: 0;
}
.server-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.info-item {
background-color: #fafafa;
border-radius: 4px;
padding: 12px;
.info-label {
font-size: 14px;
font-weight: 500;
color: #666;
margin-bottom: 8px;
}
.info-value {
font-size: 14px;
color: #333;
.ant-list-item {
padding: 4px 0;
}
}
.info-empty {
font-size: 14px;
color: #999;
font-style: italic;
}
}
}
</style>

View File

@@ -0,0 +1,185 @@
<template>
<div class="test-case" :class="{ loading }">
<div class="case-header">
<span class="flex items-center">
<fs-button size="small" type="text" icon="ion:play-circle" :loading="loading" :disabled="disabled" class="test-button" @click="runTest" />
<a-tag color="blue" class="case-title">
{{ title }}
</a-tag>
<span v-if="port" class="port-info">{{ port }}</span>
</span>
<span v-if="result && isNetTestResult" class="result-status flex-1" :style="{ color: isSuccess ? 'green' : 'red' }">
<span>
{{ isSuccess ? "✓" : "✗" }}
</span>
<span class="ml-2">
{{ result.message }}
</span>
</span>
</div>
<div v-if="result" class="result-content">
<div v-if="error" class="error-message">
<span style="color: red">{{ error }}</span>
</div>
<div v-else-if="isNetTestResult">
<div v-if="resultTestLog" class="test-log">
<pre>{{ resultTestLog }}</pre>
</div>
</div>
<div v-else-if="typeof result === 'object'" class="object-result">
<pre>{{ JSON.stringify(result, null, 2) }}</pre>
</div>
<div v-else class="text-result">
<pre>{{ result }}</pre>
</div>
</div>
<div v-else class="no-result">
<p>暂无结果</p>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { message } from "ant-design-vue";
// 组件属性
const props = defineProps<{
title: string;
port?: number | string;
testMethod: () => Promise<any>;
disabled?: boolean;
}>();
// 内部状态
const loading = ref(false);
const result = ref<any>(null);
const error = ref<string | null>(null);
// 运行测试
const runTest = async () => {
loading.value = true;
error.value = null;
result.value = null;
try {
const testResult = await props.testMethod();
// 如果结果有 data 属性,则使用 data否则使用整个结果
result.value = testResult.data || testResult;
} catch (err: any) {
result.value = null;
error.value = err.message || "测试失败";
message.error(`${props.title} 测试失败: ${error.value}`);
} finally {
loading.value = false;
}
};
// 暴露方法给父组件
defineExpose({
test: runTest,
});
// 辅助计算属性,用于模板中显示结果
const isNetTestResult = computed(() => {
return typeof result.value === "object" && result.value !== null && "success" in result.value && "message" in result.value && "testLog" in result.value;
});
const isSuccess = computed(() => {
return isNetTestResult.value && result.value.success;
});
const resultMessage = computed(() => {
return isNetTestResult.value ? result.value.message : "";
});
const resultTestLog = computed(() => {
return isNetTestResult.value ? result.value.testLog : "";
});
const resultError = computed(() => {
return isNetTestResult.value ? result.value.error : "";
});
</script>
<style lang="less" scoped>
.test-case {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
position: relative;
&:last-child {
border-bottom: none;
}
&.loading {
opacity: 0.7;
}
}
.case-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.result-status {
font-size: 14px;
color: #999;
margin-right: 10px;
}
}
.case-title {
font-weight: 500;
font-size: 14px;
}
.port-info {
font-size: 12px;
color: #999;
background-color: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
margin-right: 8px;
}
.test-button {
color: #1890ff;
font-size: 12px;
margin-right: 5px;
}
.result-content {
.error-message,
.object-result,
.text-result {
background-color: #f8f8f8;
padding: 8px 10px;
border-radius: 3px;
overflow-x: auto;
}
pre {
margin: 0;
font-size: 12px;
line-height: 1.4;
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
white-space: pre-wrap;
word-wrap: break-word;
}
.test-log {
background-color: #f8f8f8;
padding: 8px 10px;
border-radius: 3px;
overflow-x: auto;
}
}
.no-result {
padding: 12px 0;
text-align: center;
color: #999;
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,33 @@
import { request } from "/@/api/service";
export async function DomainResolve(domain: string) {
return await request({
url: "/sys/nettest/domainResolve",
method: "post",
data: { domain },
});
}
export async function PingTest(domain: string) {
return await request({
url: "/sys/nettest/ping",
method: "post",
data: { domain },
});
}
export async function TelnetTest(domain: string, port: number) {
return await request({
url: "/sys/nettest/telnet",
method: "post",
data: { domain, port },
});
}
// 获取服务器信息包括本地IP、外网IP和DNS服务器
export async function GetServerInfo() {
return await request({
url: "/sys/nettest/serverInfo",
method: "post",
});
}

View File

@@ -0,0 +1,40 @@
<template>
<fs-page class="page-sys-nettest">
<template #header>
<div class="title">网络测试</div>
</template>
<div class="nettest-container">
<!-- 服务端信息 -->
<server-info-card />
<!-- 测试区域 -->
<div class="test-areas">
<!-- 用户输入域名测试 -->
<domain-test-card class="w-50%" />
<!-- 百度域名测试 (用于对比) -->
<domain-test-card class="w-50%" :domain="'baidu.com'" :port="443" :auto-start="true" />
</div>
</div>
</fs-page>
</template>
<script lang="ts" setup>
import DomainTestCard from "./DomainTestCard.vue";
import ServerInfoCard from "./ServerInfoCard.vue";
</script>
<style lang="less">
.page-sys-nettest {
.nettest-container {
padding: 16px;
background-color: #fff;
}
.test-areas {
display: flex;
gap: 16px;
margin-top: 16px;
}
}
</style>