mirror of
https://github.com/certd/certd.git
synced 2026-05-15 04:27:31 +08:00
Merge branch 'v2-dev' into v2-dev-buy
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<div class="domain-test-card">
|
||||
<div class="card-header flex flex-wrap justify-start">
|
||||
<div v-if="title">{{ title }}</div>
|
||||
<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 class="summary">
|
||||
<a-alert :message="testSummary.title" :type="testSummary.status === 'success' ? 'success' : testSummary.status === 'failed' ? 'error' : 'warning'" show-icon :closable="false">
|
||||
<template v-if="testSummary.text" #description>
|
||||
<pre class="summary-text pre">{{ testSummary.text }}</pre>
|
||||
</template>
|
||||
</a-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted, watch } from "vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import { DomainResolve, PingTest, TelnetTest } from "./api";
|
||||
import TestCase from "./TestCase.vue";
|
||||
|
||||
// 组件属性
|
||||
const props = defineProps<{
|
||||
title?: string;
|
||||
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;
|
||||
};
|
||||
|
||||
// 获取各测试用例的状态
|
||||
const getTestStatus = (testRef: any) => {
|
||||
const result = testRef?.getResult();
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isNetTestResult = typeof result === "object" && result !== null && "success" in result && "message" in result;
|
||||
|
||||
return {
|
||||
success: isNetTestResult ? result.success : false,
|
||||
message: isNetTestResult ? result.message : "测试失败",
|
||||
};
|
||||
};
|
||||
|
||||
// 生成测试总结
|
||||
const testSummary = computed(() => {
|
||||
if (loading.value) {
|
||||
return { status: "waiting", title: "测试中,请稍后..." };
|
||||
}
|
||||
// 通过computed获取各测试结果
|
||||
const domainResolveResult = getTestStatus(domainResolveRef.value);
|
||||
const pingTestResult = getTestStatus(pingTestRef.value);
|
||||
const telnetTestResult = getTestStatus(telnetTestRef.value);
|
||||
|
||||
// 检查是否有测试结果
|
||||
const testDone = domainResolveResult != null && pingTestResult != null && telnetTestResult != null;
|
||||
if (!testDone) {
|
||||
return { status: "waiting", title: '请点击"开始测试"按钮进行网络测试' };
|
||||
}
|
||||
|
||||
// 详细分析不同的测试结果组合
|
||||
// 1. 三个测试都失败
|
||||
if (domainResolveResult?.success === false && pingTestResult?.success === false && telnetTestResult?.success === false) {
|
||||
return {
|
||||
status: "failed",
|
||||
title: "所有测试均未通过",
|
||||
text: `这表明应用容器内的网络可能完全不通。建议:\n1. 检查宿主机的网络连接状态\n2. 确认容器网络配置是否正确\n3. 检查防火墙设置是否阻止了网络访问`,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 域名解析成功,但Ping不通
|
||||
if (domainResolveResult?.success === true && pingTestResult?.success === false) {
|
||||
return {
|
||||
status: "partial",
|
||||
title: "域名解析成功,但Ping不通",
|
||||
text: `可能原因:\n1. DNS被劫持,解析到了错误的IP地址\n2. 目标服务器禁止了Ping请求\n3. 目标服务器IP被墙\n4. 目标服务器网络不通或已下线`,
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 域名解析和Ping都成功,但Telnet连接失败
|
||||
if (domainResolveResult?.success === true && pingTestResult?.success === true && telnetTestResult?.success === false) {
|
||||
return {
|
||||
status: "partial",
|
||||
title: "域名解析和Ping测试均通过,但Telnet连接失败",
|
||||
text: `可能原因:\n1. 端口号输入错误,请确认目标服务使用的正确端口\n2. 目标服务器上该端口未开放或服务未启动\n3. 防火墙或安全组限制了该端口的访问\n4. 目标网站被墙`,
|
||||
};
|
||||
}
|
||||
|
||||
// 4. 域名解析失败,但其他测试可能成功或未执行
|
||||
if (domainResolveResult?.success === false) {
|
||||
return {
|
||||
status: "partial",
|
||||
title: "域名解析失败",
|
||||
text: `可能原因:\n1. 域名输入错误或不存在\n2. DNS服务器配置问题\n3. 本地网络DNS解析故障\n4. 域名已过期或被注销`,
|
||||
};
|
||||
}
|
||||
|
||||
// 5. 所有测试都成功
|
||||
if (domainResolveResult?.success === true && pingTestResult?.success === true && telnetTestResult?.success === true) {
|
||||
return {
|
||||
status: "success",
|
||||
title: "所有测试均通过",
|
||||
text: `域名${formData.domain}解析正常,能够正常Ping通,且端口${formData.port}可访问。`,
|
||||
};
|
||||
}
|
||||
|
||||
// 6. 其他部分成功的情况
|
||||
return {
|
||||
status: "partial",
|
||||
title: "部分测试未通过",
|
||||
text: `请结合具体测试结果进行分析:\n- 域名解析:${domainResolveResult ? (domainResolveResult.success ? "成功" : "失败") : "未执行"}\n- Ping测试:${pingTestResult ? (pingTestResult.success ? "成功" : "失败") : "未执行"}\n- Telnet测试:${telnetTestResult ? (telnetTestResult.success ? "成功" : "失败") : "未执行"}`,
|
||||
};
|
||||
});
|
||||
|
||||
// 运行全部测试
|
||||
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()]);
|
||||
} catch (error) {
|
||||
message.error("部分测试执行失败,请查看详细结果");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autoStart) {
|
||||
runAllTests();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.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;
|
||||
}
|
||||
|
||||
.summary {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
.summary-text {
|
||||
}
|
||||
}
|
||||
|
||||
/* 调整按钮大小 */
|
||||
.ant-btn {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,140 @@
|
||||
<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 && serverInfo.localIP.length > 0" class="info-value">
|
||||
<a-tag v-for="ip in serverInfo.localIP" :key="ip" type="info" color="blue">{{ ip }}</a-tag>
|
||||
</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 && serverInfo.publicIP.length > 0" class="info-value">
|
||||
<a-tag v-for="ip in serverInfo.publicIP" :key="ip" type="info" color="green">{{ ip }}</a-tag>
|
||||
</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">
|
||||
<a-tag v-for="dns in serverInfo.dnsServers" :key="dns" type="info" color="cyan">{{ dns }}</a-tag>
|
||||
</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>
|
||||
@@ -0,0 +1,186 @@
|
||||
<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,
|
||||
getResult: () => result.value,
|
||||
});
|
||||
|
||||
// 辅助计算属性,用于模板中显示结果
|
||||
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>
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<fs-page class="page-sys-nettest">
|
||||
<template #header>
|
||||
<div class="title">
|
||||
网络测试
|
||||
<span class="sub">测试您的服务器容器网络连接是否正常</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="nettest-container">
|
||||
<!-- 服务端信息 -->
|
||||
<server-info-card />
|
||||
|
||||
<!-- 测试区域 -->
|
||||
<div class="test-areas flex-wrap md:flex-nowrap">
|
||||
<!-- 百度域名测试 (用于对比) -->
|
||||
<domain-test-card class="test-card" :domain="'baidu.com'" :port="443" :auto-start="true" />
|
||||
<!-- 用户输入域名测试 -->
|
||||
<domain-test-card class="test-card" :title="'自定义域名测试'" />
|
||||
</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;
|
||||
}
|
||||
|
||||
.test-card {
|
||||
min-width: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -79,6 +79,14 @@ export async function SysSettingsSave(data: SysSettings) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function TestCaptcha(form: any) {
|
||||
return await request({
|
||||
url: apiPrefix + "/captchaTest",
|
||||
method: "post",
|
||||
data: form,
|
||||
});
|
||||
}
|
||||
|
||||
export async function TestProxy() {
|
||||
return await request({
|
||||
url: apiPrefix + "/testProxy",
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
<a-tab-pane key="safe" :tab="t('certd.sys.setting.safeSetting')">
|
||||
<SettingSafe v-if="activeKey === 'safe'" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="captcha" :tab="t('certd.sys.setting.captchaSetting')">
|
||||
<SettingCaptcha v-if="activeKey === 'captcha'" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="pipeline" :tab="t('certd.sys.setting.pipelineSetting')">
|
||||
<SettingPipeline v-if="activeKey === 'pipeline'" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</fs-page>
|
||||
@@ -27,6 +33,8 @@ import SettingBase from "/@/views/sys/settings/tabs/base.vue";
|
||||
import SettingRegister from "/@/views/sys/settings/tabs/register.vue";
|
||||
import SettingPayment from "/@/views/sys/settings/tabs/payment.vue";
|
||||
import SettingSafe from "/@/views/sys/settings/tabs/safe.vue";
|
||||
import SettingCaptcha from "/@/views/sys/settings/tabs/captcha.vue";
|
||||
import SettingPipeline from "/@/views/sys/settings/tabs/pipeline.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ref } from "vue";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
@@ -58,7 +66,7 @@ function onChange(value: string) {
|
||||
<style lang="less">
|
||||
.page-sys-settings {
|
||||
.sys-settings-form {
|
||||
width: 600px;
|
||||
width: 800px;
|
||||
max-width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="sys-settings-form sys-settings-base">
|
||||
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finish-failed="onFinishFailed">
|
||||
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish">
|
||||
<a-form-item :label="t('certd.icpRegistrationNumber')" :name="['public', 'icpNo']">
|
||||
<a-input v-model:value="formState.public.icpNo" :placeholder="t('certd.icpPlaceholder')" />
|
||||
</a-form-item>
|
||||
@@ -47,18 +47,6 @@
|
||||
<div class="helper" v-html="t('certd.commonCnameHelper')"></div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="t('certd.sys.setting.captchaEnabled')" :name="['public', 'captchaEnabled']">
|
||||
<a-switch v-model:checked="formState.public.captchaEnabled" />
|
||||
<div class="helper" v-html="t('certd.sys.setting.captchaHelper')"></div>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('certd.sys.setting.captchaType')" :name="['public', 'captchaAddonId']">
|
||||
<addon-selector v-model:model-value="formState.public.captchaAddonId" addon-type="captcha" from="sys" @selected-change="onAddonChanged" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :name="['public', 'captchaType']" class="hidden">
|
||||
<a-input v-model:model-value="formState.public.captchaType"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
|
||||
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
|
||||
</a-form-item>
|
||||
@@ -76,6 +64,7 @@ import { notification } from "ant-design-vue";
|
||||
import { util } from "/@/utils";
|
||||
import { useI18n } from "/src/locales";
|
||||
import AddonSelector from "../../../certd/addon/addon-selector/index.vue";
|
||||
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
|
||||
const { t } = useI18n();
|
||||
|
||||
defineOptions({
|
||||
@@ -106,6 +95,7 @@ const settingsStore = useSettingStore();
|
||||
const onFinish = async (form: any) => {
|
||||
try {
|
||||
saveLoading.value = true;
|
||||
|
||||
await api.SysSettingsSave(form);
|
||||
await settingsStore.loadSysSettings();
|
||||
notification.success({
|
||||
@@ -116,21 +106,6 @@ const onFinish = async (form: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onFinishFailed = (errorInfo: any) => {
|
||||
// console.log("Failed:", errorInfo);
|
||||
};
|
||||
|
||||
async function stopOtherUserTimer() {
|
||||
await api.stopOtherUserTimer();
|
||||
notification.success({
|
||||
message: t("certd.stopSuccess"),
|
||||
});
|
||||
}
|
||||
|
||||
function onAddonChanged(target: any) {
|
||||
formState.public.captchaType = target.type;
|
||||
}
|
||||
|
||||
const testProxyLoading = ref(false);
|
||||
async function testProxy() {
|
||||
testProxyLoading.value = true;
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="sys-settings-form sys-settings-base">
|
||||
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish">
|
||||
<a-form-item :label="t('certd.sys.setting.captchaEnabled')" :name="['public', 'captchaEnabled']">
|
||||
<a-switch v-model:checked="formState.public.captchaEnabled" />
|
||||
<div class="helper" v-html="t('certd.sys.setting.captchaHelper')"></div>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('certd.sys.setting.captchaType')" :name="['public', 'captchaAddonId']">
|
||||
<addon-selector v-model:model-value="formState.public.captchaAddonId" addon-type="captcha" from="sys" @selected-change="onAddonChanged" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="formState.public.captchaType === settingsStore.sysPublic.captchaType" :label="t('certd.sys.setting.captchaTest')">
|
||||
<div class="flex">
|
||||
<CaptchaInput v-model:model-value="captchaTestForm.captcha" class="w-50%"></CaptchaInput>
|
||||
<a-button class="ml-2" type="primary" @click="doCaptchaValidate">后端验证</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :name="['public', 'captchaType']" class="hidden">
|
||||
<a-input v-model:model-value="formState.public.captchaType"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
|
||||
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { reactive, ref } from "vue";
|
||||
import { SysSettings } from "/@/views/sys/settings/api";
|
||||
import * as api from "/@/views/sys/settings/api";
|
||||
import { merge } from "lodash-es";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
import { notification } from "ant-design-vue";
|
||||
import { util } from "/@/utils";
|
||||
import { useI18n } from "/src/locales";
|
||||
import AddonSelector from "../../../certd/addon/addon-selector/index.vue";
|
||||
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
|
||||
const { t } = useI18n();
|
||||
|
||||
defineOptions({
|
||||
name: "SettingCaptcha",
|
||||
});
|
||||
|
||||
const captchaTestForm = reactive({
|
||||
captcha: null,
|
||||
pass: false,
|
||||
});
|
||||
|
||||
async function doCaptchaValidate() {
|
||||
if (!captchaTestForm.captcha) {
|
||||
notification.error({
|
||||
message: "请进行验证码验证",
|
||||
});
|
||||
return;
|
||||
}
|
||||
await api.TestCaptcha(captchaTestForm.captcha);
|
||||
notification.success({
|
||||
message: "校验通过",
|
||||
});
|
||||
captchaTestForm.pass = true;
|
||||
}
|
||||
|
||||
const formState = reactive<Partial<SysSettings>>({
|
||||
public: {
|
||||
icpNo: "",
|
||||
mpsNo: "",
|
||||
},
|
||||
private: {},
|
||||
});
|
||||
|
||||
async function loadSysSettings() {
|
||||
const data: any = await api.SysSettingsGet();
|
||||
merge(formState, data);
|
||||
}
|
||||
|
||||
const saveLoading = ref(false);
|
||||
loadSysSettings();
|
||||
const settingsStore = useSettingStore();
|
||||
const onFinish = async (form: any) => {
|
||||
try {
|
||||
saveLoading.value = true;
|
||||
|
||||
if (form.public.captchaEnabled && !captchaTestForm.pass) {
|
||||
if (form.public.captchaType === settingsStore.sysPublic.captchaType) {
|
||||
notification.error({
|
||||
message: "您正在开启登录验证码,请先通过验证码测试,后端校验成功后才能保存",
|
||||
});
|
||||
} else {
|
||||
notification.error({
|
||||
message: "您正在开启登录验证码,请先关闭登录验证码开关,保存,然后会显示验证码,进行验证码测试,后端校验成功,之后再开启登录验证码,并保存",
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await api.SysSettingsSave(form);
|
||||
await settingsStore.loadSysSettings();
|
||||
notification.success({
|
||||
message: t("certd.saveSuccess"),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
clearValidState();
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
function clearValidState() {
|
||||
captchaTestForm.pass = false;
|
||||
captchaTestForm.captcha = null;
|
||||
}
|
||||
|
||||
function onAddonChanged(target: any) {
|
||||
formState.public.captchaType = target.type;
|
||||
clearValidState();
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
.sys-settings-base {
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="sys-settings-form sys-settings-pipeline">
|
||||
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish">
|
||||
<a-form-item :label="t('certd.manageOtherUserPipeline')" :name="['public', 'managerOtherUserPipeline']">
|
||||
<a-switch v-model:checked="formState.public.managerOtherUserPipeline" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('certd.limitUserPipelineCount')" :name="['public', 'limitUserPipelineCount']">
|
||||
<a-input-number v-model:value="formState.public.limitUserPipelineCount" />
|
||||
<div class="helper">{{ t("certd.limitUserPipelineCountHelper") }}</div>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('certd.sys.setting.pipelineValidTimeEnabled')" :name="['public', 'pipelineValidTimeEnabled']">
|
||||
<div class="flex items-center">
|
||||
<a-switch v-model:checked="formState.public.pipelineValidTimeEnabled" :disabled="!settingsStore.isPlus" />
|
||||
<vip-button class="ml-5" mode="button"></vip-button>
|
||||
</div>
|
||||
|
||||
<div class="helper">{{ t("certd.sys.setting.pipelineValidTimeEnabledHelper") }}</div>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('certd.sys.setting.certDomainAddToMonitorEnabled')" :name="['public', 'certDomainAddToMonitorEnabled']">
|
||||
<div class="flex items-center">
|
||||
<a-switch v-model:checked="formState.public.certDomainAddToMonitorEnabled" :disabled="!settingsStore.isPlus" />
|
||||
<vip-button class="ml-5" mode="button"></vip-button>
|
||||
</div>
|
||||
<div class="helper">{{ t("certd.sys.setting.certDomainAddToMonitorEnabledHelper") }}</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
|
||||
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { reactive, ref } from "vue";
|
||||
import { SysSettings } from "/@/views/sys/settings/api";
|
||||
import * as api from "/@/views/sys/settings/api";
|
||||
import { merge } from "lodash-es";
|
||||
import { useSettingStore } from "/@/store/settings";
|
||||
import { notification } from "ant-design-vue";
|
||||
import { useI18n } from "/src/locales";
|
||||
const { t } = useI18n();
|
||||
|
||||
defineOptions({
|
||||
name: "SettingPipeline",
|
||||
});
|
||||
|
||||
const formState = reactive<Partial<SysSettings>>({
|
||||
public: {},
|
||||
private: {},
|
||||
});
|
||||
|
||||
async function loadSysSettings() {
|
||||
const data: any = await api.SysSettingsGet();
|
||||
merge(formState, data);
|
||||
}
|
||||
|
||||
const saveLoading = ref(false);
|
||||
loadSysSettings();
|
||||
const settingsStore = useSettingStore();
|
||||
const onFinish = async (form: any) => {
|
||||
try {
|
||||
saveLoading.value = true;
|
||||
|
||||
await api.SysSettingsSave(form);
|
||||
await settingsStore.loadSysSettings();
|
||||
notification.success({
|
||||
message: t("certd.saveSuccess"),
|
||||
});
|
||||
} finally {
|
||||
saveLoading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="less"></style>
|
||||
@@ -1,13 +1,6 @@
|
||||
<template>
|
||||
<div class="sys-settings-form sys-settings-register">
|
||||
<a-form :model="formState" name="register" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish">
|
||||
<a-form-item :label="t('certd.manageOtherUserPipeline')" :name="['public', 'managerOtherUserPipeline']">
|
||||
<a-switch v-model:checked="formState.public.managerOtherUserPipeline" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('certd.limitUserPipelineCount')" :name="['public', 'limitUserPipelineCount']">
|
||||
<a-input-number v-model:value="formState.public.limitUserPipelineCount" />
|
||||
<div class="helper">{{ t("certd.limitUserPipelineCountHelper") }}</div>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('certd.enableSelfRegistration')" :name="['public', 'registerEnabled']">
|
||||
<a-switch v-model:checked="formState.public.registerEnabled" />
|
||||
</a-form-item>
|
||||
|
||||
@@ -55,15 +55,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
groups: {
|
||||
base: {
|
||||
header: t("certd.basicInfo"),
|
||||
columns: ["title", "type", "disabled", "order", "supportBuy", "intro"]
|
||||
columns: ["title", "type", "disabled", "order", "supportBuy", "intro"],
|
||||
},
|
||||
content: {
|
||||
header: t("certd.packageContent"),
|
||||
columns: ["content.maxDomainCount", "content.maxPipelineCount", "content.maxDeployCount", "content.maxMonitorCount"]
|
||||
columns: ["content.maxDomainCount", "content.maxPipelineCount", "content.maxDeployCount", "content.maxMonitorCount"],
|
||||
},
|
||||
price: {
|
||||
header: t("certd.price"),
|
||||
columns: ["durationPrices"]
|
||||
columns: ["durationPrices"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
|
||||
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
|
||||
rowSelection: {
|
||||
enabled: true,
|
||||
order: -2,
|
||||
order: -99,
|
||||
before: true,
|
||||
// handle: (pluginProps,useCrudProps)=>CrudOptions,
|
||||
props: {
|
||||
|
||||
Reference in New Issue
Block a user