mirror of
https://github.com/certd/certd.git
synced 2025-11-25 09:10:11 +08:00
perf: 支持网络测试
This commit is contained in:
@@ -175,6 +175,7 @@ export default {
|
||||
suiteSetting: "Suite Settings",
|
||||
orderManager: "Order Management",
|
||||
userSuites: "User Suites",
|
||||
netTest: "Network Test",
|
||||
},
|
||||
certificateRepo: {
|
||||
title: "Certificate Repository",
|
||||
|
||||
@@ -181,6 +181,7 @@ export default {
|
||||
suiteSetting: "套餐设置",
|
||||
orderManager: "订单管理",
|
||||
userSuites: "用户套餐",
|
||||
netTest: "网络测试",
|
||||
},
|
||||
certificateRepo: {
|
||||
title: "证书仓库",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
185
packages/ui/certd-client/src/views/sys/nettest/TestCase.vue
Normal file
185
packages/ui/certd-client/src/views/sys/nettest/TestCase.vue
Normal 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>
|
||||
33
packages/ui/certd-client/src/views/sys/nettest/api.ts
Normal file
33
packages/ui/certd-client/src/views/sys/nettest/api.ts
Normal 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",
|
||||
});
|
||||
}
|
||||
40
packages/ui/certd-client/src/views/sys/nettest/index.vue
Normal file
40
packages/ui/certd-client/src/views/sys/nettest/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user