feat(frontend): give IPSec peer setup a responsive workflow

This commit is contained in:
Codex committed 2026-10-23 16:29:48 +08:00
1 parent 94ec3b042b
commit 3e6e481b79
1 file changed
+115 -97
+115 -97
View File
@@ -1,102 +1,102 @@
<template>
<div>
<el-row :gutter="16">
<!-- 左:本端配置 -->
<el-col :span="12">
<el-card class="page-card" shadow="never">
<template #header>
<b>{{ $t('ipsecPeer.localParams') }}</b>
<el-select v-model="selectedTemplate" :placeholder="$t('ipsecPeer.loadTemplate')" size="small" style="width:200px;float:right"
@change="applyTemplate">
<el-option v-for="t in templates" :key="t.key" :label="t.name" :value="t.key" />
</el-select>
</template>
<el-alert v-if="templateNote" :title="templateNote" type="info" :closable="false" style="margin-bottom:14px" />
<el-form label-width="130px" size="default">
<el-form-item :label="$t('ipsecPeer.localPublicIp')" required><el-input v-model="local.public_endpoint" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.remoteGateway')" required><el-input v-model="local.remote_gateway" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.localSubnets')" required><el-input v-model="local.local_subnets" placeholder="192.168.1.0/24" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.remoteSubnets')" required><el-input v-model="local.remote_subnets" placeholder="10.0.0.0/24" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.psk')" required><el-input v-model="local.psk" type="password" show-password /></el-form-item>
<el-divider content-position="left">{{ $t('ipsecPeer.phase1') }}</el-divider>
<el-form-item :label="$t('ipsecPeer.ikeVersion')">
<el-radio-group v-model="local.ike_version">
<el-radio-button value="ikev2">IKEv2</el-radio-button>
<el-radio-button value="ikev1">IKEv1</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('ipsecPeer.proposal')">
<el-input v-model="local.ike_proposal" placeholder="aes256-sha256-modp2048" />
</el-form-item>
<el-form-item :label="$t('ipsecPeer.localId')"><el-input v-model="local.local_id" placeholder="@vpn-local" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.remoteId')"><el-input v-model="local.remote_id" placeholder="@vpn-remote" /></el-form-item>
<el-divider content-position="left">{{ $t('ipsecPeer.phase2') }}</el-divider>
<el-form-item :label="$t('ipsecPeer.espProposal')"><el-input v-model="local.esp_proposal" placeholder="aes256-sha256" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.pfsGroup')"><el-input v-model="local.pfs_group" placeholder="modp2048" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.dpdPolicy')">
<el-select v-model="local.dpd_action" style="width:100%">
<el-option :label="$t('ipsecPeer.dpdRestart')" value="restart" />
<el-option :label="$t('ipsecPeer.dpdClear')" value="clear" />
<el-option :label="$t('ipsecPeer.dpdTrap')" value="trap" />
</el-select>
</el-form-item>
</el-form>
<el-button type="primary" style="width:100%" :loading="creating" @click="createTunnel">
{{ $t('ipsecPeer.createIpsecTunnel') }}
</el-button>
</el-card>
</el-col>
<!-- 右:对端参数对比 + 连通性测试 -->
<el-col :span="12">
<el-card class="page-card" shadow="never">
<template #header><b>{{ $t('ipsecPeer.paramConsistency') }}</b></template>
<el-form label-width="130px" size="default">
<el-form-item :label="$t('ipsecPeer.peerIkeVersion')">
<el-radio-group v-model="peer.ike_version">
<el-radio-button value="ikev2">IKEv2</el-radio-button>
<el-radio-button value="ikev1">IKEv1</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('ipsecPeer.peerPhase1')"><el-input v-model="peer.ike_proposal" :placeholder="$t('ipsecPeer.peerIkePlaceholder')" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.peerPhase2')"><el-input v-model="peer.esp_proposal" :placeholder="$t('ipsecPeer.peerEspPlaceholder')" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.peerPfsGroup')"><el-input v-model="peer.pfs_group" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.peerLocalSubnets')"><el-input v-model="peer.local_subnets" :placeholder="$t('ipsecPeer.peerSubnetsPlaceholder')" /></el-form-item>
</el-form>
<el-button style="width:100%" :loading="checking" @click="compare">{{ $t('ipsecPeer.startCompare') }}</el-button>
<div v-if="compareResult" style="margin-top:14px">
<el-alert v-if="compareResult.consistent" type="success" :title="$t('ipsecPeer.paramsConsistent')" :closable="false" show-icon />
<div v-else>
<el-alert type="error" :title="$t('ipsecPeer.paramsInconsistent')" :closable="false" />
<ul class="diff-list">
<li v-for="(d, i) in compareResult.differences" :key="i">{{ d }}</li>
</ul>
</div>
</div>
</el-card>
<el-card class="page-card" shadow="never" style="margin-top:16px">
<template #header><b>{{ $t('ipsecPeer.connectivityTest') }}</b></template>
<el-select v-model="testTunnelId" :placeholder="$t('ipsecPeer.selectIpsecTunnel')" style="width:100%">
<el-option v-for="tun in ipsecTunnels" :key="tun.id" :label="`${tun.name}(${tun.state})`" :value="tun.id" />
<div class="ipsec-page">
<el-card class="page-card" shadow="never">
<template #header>
<div class="card-heading">
<b>{{ $t('ipsecPeer.localParams') }}</b>
<el-select v-model="selectedTemplate" :placeholder="$t('ipsecPeer.loadTemplate')" class="template-select" @change="applyTemplate">
<el-option v-for="t in templates" :key="t.key" :label="t.name" :value="t.key" />
</el-select>
<el-button type="primary" style="width:100%;margin-top:12px" :loading="testing" :disabled="!testTunnelId"
@click="testConnectivity">{{ $t('ipsecPeer.testConnectivity') }}</el-button>
<div v-if="testResult" style="margin-top:14px">
<el-descriptions :column="1" border size="small">
<el-descriptions-item :label="$t('common.target')">{{ testResult.target }}</el-descriptions-item>
<el-descriptions-item :label="$t('common.status')">
<el-tag :type="testResult.reachable ? 'success' : 'danger'">
{{ testResult.reachable ? $t('ipsecPeer.reachable') : $t('ipsecPeer.unreachable') }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item :label="$t('ipsecPeer.latency')">{{ testResult.latency_ms }} ms</el-descriptions-item>
<el-descriptions-item :label="$t('common.description')">{{ testResult.note || testResult.ike_udp500_note || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<el-alert v-if="templateNote" :title="templateNote" type="info" :closable="false" class="template-note" />
<el-form label-position="top" class="ipsec-form">
<div class="field-grid">
<el-form-item :label="$t('ipsecPeer.localPublicIp')" required><el-input v-model="local.public_endpoint" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.remoteGateway')" required><el-input v-model="local.remote_gateway" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.localSubnets')" required><el-input v-model="local.local_subnets" placeholder="192.168.1.0/24" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.remoteSubnets')" required><el-input v-model="local.remote_subnets" placeholder="10.0.0.0/24" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.psk')" required><el-input v-model="local.psk" type="password" show-password /></el-form-item>
</div>
<el-collapse class="advanced-options">
<el-collapse-item :title="$t('ipsecPeer.advancedOptions')" name="advanced">
<div class="section-heading">{{ $t('ipsecPeer.phase1') }}</div>
<div class="field-grid">
<el-form-item :label="$t('ipsecPeer.ikeVersion')">
<el-radio-group v-model="local.ike_version">
<el-radio-button value="ikev2">IKEv2</el-radio-button>
<el-radio-button value="ikev1">IKEv1</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('ipsecPeer.proposal')"><el-input v-model="local.ike_proposal" placeholder="aes256-sha256-modp2048" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.localId')"><el-input v-model="local.local_id" placeholder="@vpn-local" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.remoteId')"><el-input v-model="local.remote_id" placeholder="@vpn-remote" /></el-form-item>
</div>
<div class="section-heading">{{ $t('ipsecPeer.phase2') }}</div>
<div class="field-grid">
<el-form-item :label="$t('ipsecPeer.espProposal')"><el-input v-model="local.esp_proposal" placeholder="aes256-sha256" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.pfsGroup')"><el-input v-model="local.pfs_group" placeholder="modp2048" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.dpdPolicy')">
<el-select v-model="local.dpd_action" style="width:100%">
<el-option :label="$t('ipsecPeer.dpdRestart')" value="restart" />
<el-option :label="$t('ipsecPeer.dpdClear')" value="clear" />
<el-option :label="$t('ipsecPeer.dpdTrap')" value="trap" />
</el-select>
</el-form-item>
</div>
</el-collapse-item>
</el-collapse>
</el-form>
<el-button type="primary" :loading="creating" @click="createTunnel">{{ $t('ipsecPeer.createIpsecTunnel') }}</el-button>
</el-card>
<el-card class="page-card" shadow="never">
<template #header><b>{{ $t('ipsecPeer.paramConsistency') }}</b></template>
<el-form label-position="top" class="ipsec-form field-grid">
<el-form-item :label="$t('ipsecPeer.peerIkeVersion')">
<el-radio-group v-model="peer.ike_version">
<el-radio-button value="ikev2">IKEv2</el-radio-button>
<el-radio-button value="ikev1">IKEv1</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('ipsecPeer.peerPhase1')"><el-input v-model="peer.ike_proposal" :placeholder="$t('ipsecPeer.peerIkePlaceholder')" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.peerPhase2')"><el-input v-model="peer.esp_proposal" :placeholder="$t('ipsecPeer.peerEspPlaceholder')" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.peerPfsGroup')"><el-input v-model="peer.pfs_group" /></el-form-item>
<el-form-item :label="$t('ipsecPeer.peerLocalSubnets')"><el-input v-model="peer.local_subnets" :placeholder="$t('ipsecPeer.peerSubnetsPlaceholder')" /></el-form-item>
</el-form>
<el-button :loading="checking" @click="compare">{{ $t('ipsecPeer.startCompare') }}</el-button>
<div v-if="compareResult" style="margin-top:14px">
<el-alert v-if="compareResult.consistent" type="success" :title="$t('ipsecPeer.paramsConsistent')" :closable="false" show-icon />
<div v-else>
<el-alert type="error" :title="$t('ipsecPeer.paramsInconsistent')" :closable="false" />
<ul class="diff-list">
<li v-for="(d, i) in compareResult.differences" :key="i">{{ d }}</li>
</ul>
</div>
</div>
</el-card>
<el-card class="page-card" shadow="never">
<template #header><b>{{ $t('ipsecPeer.connectivityTest') }}</b></template>
<div class="test-controls">
<el-select v-model="testTunnelId" :placeholder="$t('ipsecPeer.selectIpsecTunnel')" class="tunnel-select">
<el-option v-for="tun in ipsecTunnels" :key="tun.id" :label="`${tun.name}(${tun.state})`" :value="tun.id" />
</el-select>
<el-button type="primary" :loading="testing" :disabled="!testTunnelId" @click="testConnectivity">{{ $t('ipsecPeer.testConnectivity') }}</el-button>
</div>
<div v-if="testResult" style="margin-top:14px">
<el-descriptions :column="1" border size="small">
<el-descriptions-item :label="$t('common.target')">{{ testResult.target }}</el-descriptions-item>
<el-descriptions-item :label="$t('common.status')">
<el-tag :type="testResult.reachable ? 'success' : 'danger'">
{{ testResult.reachable ? $t('ipsecPeer.reachable') : $t('ipsecPeer.unreachable') }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item :label="$t('ipsecPeer.latency')">{{ testResult.latency_ms }} ms</el-descriptions-item>
<el-descriptions-item :label="$t('common.description')">{{ testResult.note || testResult.ike_udp500_note || '-' }}</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
</div>
</template>
@@ -188,6 +188,24 @@ onMounted(async () => {
</script>
<style scoped>
.ipsec-page { max-width: 1040px; margin: 0 auto; display: grid; gap: 20px; }
.ipsec-page .page-card + .page-card { margin-top: 0; }
.card-heading, .test-controls { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.template-select { width: min(280px, 100%); }
.template-note { margin-bottom: 20px; }
.field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 32px; }
.ipsec-form :deep(.el-form-item) { min-width: 0; margin-bottom: 20px; }
.ipsec-form :deep(.el-form-item__label) { height: auto; line-height: 1.4; margin-bottom: 8px; }
.advanced-options { margin: 0 0 20px; }
.advanced-options :deep(.el-collapse-item__content) { padding: 12px 0 4px; }
.section-heading { font-weight: 600; margin: 8px 0 16px; }
.section-heading:not(:first-child) { border-top: 1px solid var(--el-border-color-lighter); padding-top: 20px; }
.tunnel-select { width: min(440px, 100%); }
@media (max-width: 680px) {
.field-grid { grid-template-columns: minmax(0, 1fr); }
.test-controls { align-items: stretch; }
.test-controls .el-button, .test-controls .tunnel-select { width: 100%; margin-left: 0; }
}
.diff-list { margin: 10px 0 0; padding-left: 20px; color: #f56c6c; font-size: 13px; }
.diff-list li { margin-bottom: 6px; }
</style>