sawAdmin/src/views/system/vpn-server-config.vue

735 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="vpn-server-config">
<el-row :gutter="20">
<!-- 左侧服务器列表 -->
<el-col :span="8">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>服务器列表</span>
</div>
</template>
<div class="server-list">
<div
v-for="server in serverList"
:key="server.server_id"
class="server-item"
:class="{ active: selectedServer?.server_id === server.server_id }"
@click="selectServer(server)"
>
<div class="server-info">
<div class="server-name">
<span class="status-indicator" :class="{ 'online': onlineServers.includes(server.server_id) }"></span>
{{ server.name }}
</div>
<div class="server-ip">{{ server.server_ip }}</div>
</div>
<div class="server-actions">
<el-button type="danger" size="small" text @click.stop="deleteServer(server.server_id)">
删除
</el-button>
</div>
</div>
<el-empty v-if="serverList.length === 0" description="暂无服务器配置" />
</div>
</el-card>
</el-col>
<!-- 右侧编辑表单 -->
<el-col :span="16">
<el-card shadow="hover" v-if="selectedServer">
<template #header>
<div class="card-header">
<span>编辑服务器配置</span>
<el-button type="success" @click="saveConfig">保存配置</el-button>
</div>
</template>
<el-form :model="selectedServer" label-width="120px" size="default">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="服务器名称">
<el-input v-model="selectedServer.name" placeholder="请输入服务器名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="服务器ID">
<el-input v-model="selectedServer.server_id" placeholder="请输入服务器ID" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="服务器IP">
<el-input v-model="selectedServer.server_ip" placeholder="请输入服务器IP" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="服务器IPV6">
<el-input v-model="selectedServer.server_ipv6" placeholder="请输入服务器IPV6" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="IP类型">
<el-select v-model="selectedServer.server_ip_type" placeholder="请选择服务器支持IP类型">
<el-option label="IPv4" :value="4" />
<el-option label="IPv6" :value="6" />
<el-option label="IPv4/IPv6" :value="46" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="UDP端口">
<el-input-number v-model="selectedServer.udp_port" :min="1" :max="65535" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="TCP端口">
<el-input-number v-model="selectedServer.tcp_port" :min="1" :max="65535" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="协议类型">
<el-select v-model="selectedServer.protocol" placeholder="请选择协议">
<el-option label="TCP" :value="1" />
<el-option label="UDP" :value="2" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="IP类型">
<el-select v-model="selectedServer.ip_type" placeholder="请选择IP类型">
<el-option label="IPv4" :value="4" />
<el-option label="IPv6" :value="6" />
<el-option label="IPv4/IPv6" :value="46" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="DNS服务器">
<el-input v-model="selectedServer.dns_server" placeholder="请输入DNS服务器" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="加密算法">
<el-select v-model="selectedServer.encryption" placeholder="请选择加密算法">
<el-option label="AES-128-GCM" value="aes-128-gcm" />
<el-option label="AES-192-GCM" value="aes-192-gcm" />
<el-option label="AES-256-GCM" value="aes-256-gcm" />
<el-option label="SM4-GCM" value="SM4-GCM" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="摘要算法">
<el-select v-model="selectedServer.hash" placeholder="请选择摘要算法">
<el-option label="SHA256" value="sha256" />
<el-option label="SHA512" value="sha512" />
<el-option label="MD5" value="md5" />
<el-option label="SM3" value="sm3" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="最大设备数">
<el-input-number v-model="selectedServer.user_max_device" :min="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="空闲时长(秒)">
<el-input-number v-model="selectedServer.duration_time" :min="1" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="IPv4地址池">
<el-select
v-model="selectedServer.ipv4_address_pool"
placeholder="请选择IPv4地址池"
clearable
filterable
>
<el-option
v-for="pool in addressPools"
:key="pool.pool_name"
:label="`${pool.pool_name} (${pool.config.ipv4_address_pool.start_ip} - ${pool.config.ipv4_address_pool.end_ip}/${pool.config.ipv4_address_pool.prefix})`"
:value="pool.pool_name"
/>
</el-select>
</el-form-item>
<el-form-item label="IPv6地址池">
<el-select
v-model="selectedServer.ipv6_address_pool"
placeholder="请选择IPv6地址池"
clearable
filterable
>
<el-option
v-for="pool in addressPools"
:key="pool.pool_name"
:label="`${pool.pool_name} (${pool.config.ipv6_address_pool.start_ip} - ${pool.config.ipv6_address_pool.end_ip}/${pool.config.ipv6_address_pool.prefix})`"
:value="pool.pool_name"
/>
</el-select>
</el-form-item>
<el-form-item label="隧道类型">
<el-select
v-model="selectedServer.tunnel"
placeholder="请选择隧道类型"
clearable
filterable
>
<el-option
v-for="tunnel in tunnelConfigs"
:key="tunnel.tunnel_name"
:label="`${tunnel.tunnel_name} (MTU: IPv4=${tunnel.config.ipv4_mtu}, IPv6=${tunnel.config.ipv6_mtu}, 限速: ↑${tunnel.config.upload_limit}Kbps/↓${tunnel.config.download_limit}Kbps)`"
:value="tunnel.tunnel_name"
/>
</el-select>
</el-form-item>
<el-form-item label="服务器信息">
<el-input
v-model="selectedServer.server_info"
type="textarea"
:rows="3"
placeholder="请输入服务器信息"
/>
</el-form-item>
<el-form-item label="允许用户">
<el-select
v-model="user_select_ids"
placeholder="请选择允许的用户"
multiple
filterable
collapse-tags
collapse-tags-tooltip
:max-collapse-tags="3"
>
<el-option
v-for="user in user_select_opts"
:key="user.value"
:label="user.label"
:value="user.value"
/>
</el-select>
</el-form-item>
<el-divider content-position="left">IPv4路由配置</el-divider>
<el-form-item label="IPv4路由">
<div class="router-section">
<div
v-for="(router, index) in selectedServer.ipv4_router"
:key="index"
class="router-item"
>
<el-input v-model="router.ip" placeholder="IP地址 (如: 192.168.1.1)" style="flex: 1;" />
<el-input-number v-model="router.prefix" :min="1" :max="32" placeholder="前缀长度" style="flex: 1; margin-left: 10px;" />
<el-button type="danger" size="small" text @click="removeRouter('ipv4', index)" style="margin-left: 10px;">
删除
</el-button>
</div>
<el-button type="primary" size="small" text @click="addRouter('ipv4')">
添加IPv4路由
</el-button>
</div>
</el-form-item>
<el-divider content-position="left">IPv6路由配置</el-divider>
<el-form-item label="IPv6路由">
<div class="router-section">
<div
v-for="(router, index) in selectedServer.ipv6_router"
:key="index"
class="router-item"
>
<el-input v-model="router.ip" placeholder="IP地址 (如: 2001:db8::1)" style="flex: 1;" />
<el-input-number v-model="router.prefix" :min="1" :max="128" placeholder="前缀长度" style="flex: 1; margin-left: 10px;" />
<el-button type="danger" size="small" text @click="removeRouter('ipv6', index)" style="margin-left: 10px;">
删除
</el-button>
</div>
<el-button type="primary" size="small" text @click="addRouter('ipv6')">
添加IPv6路由
</el-button>
</div>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="hover" v-else>
<el-empty description="请选择要编辑的服务器" />
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import {getAllDefaultUsers} from '@/api/user';
import { GetVPNServerConfigHandler, SetVPNServerConfigHandler, DeleteVPNServerHandler, GetVPNAddressPoolHandler, GetVPNTunnelConfigHandler, GetVPNServerOnlineListHandler } from '@/api/vpn';
interface UserID {
id: number;
}
interface StringValue {
value: string;
}
interface UserIDBindIP {
user_id: number;
ip: string;
}
interface AddressPool {
start_ip: string;
end_ip: string;
prefix: number;
dns_ip: StringValue[];
ip_bind: UserIDBindIP[];
}
interface AddressPoolConfig {
ipv4_address_pool: AddressPool;
ipv6_address_pool: AddressPool;
}
interface AddressPoolRequest {
pool_name: string;
config: AddressPoolConfig;
}
interface TunnelConfig {
tunnel_name: string;
auto_ipv4: boolean;
auto_ipv6: boolean;
ipv4_address: string;
ipv6_address: string;
ipv4_mtu: number;
ipv6_mtu: number;
upload_limit: number;
download_limit: number;
}
interface TunnelRequestAndResponse {
tunnel_name: string;
config: TunnelConfig;
}
interface VPNRouter {
type: number;
ip: string;
prefix: number;
}
interface ServerConfig {
name: string;
server_id: string;
server_ip: string;
server_ipv6: string;
server_ip_type: number;
server_info: string;
udp_port: number;
tcp_port: number;
protocol: number;
ip_type: number;
ipv4_address_pool: string;
ipv6_address_pool: string;
dns_server: string;
tunnel: string;
allow_user_id: UserID[];
encryption: string;
hash: string;
user_max_device: number;
duration_time: number;
ipv4_router: VPNRouter[];
ipv6_router: VPNRouter[];
}
const serverList = ref<ServerConfig[]>([]);
const selectedServer = ref<ServerConfig | null>(null);
const addressPools = ref<AddressPoolRequest[]>([]);
const tunnelConfigs = ref<TunnelRequestAndResponse[]>([]);
const user_select_opts = ref<Array<{value: number, label: string}>>([]);
const user_select_ids = ref<number[]>([]);
const onlineServers = ref<string[]>([]);
let timer: number | null = null;
// 获取服务器配置列表
const getServerConfigs = async () => {
try {
const response = await GetVPNServerConfigHandler();
serverList.value = response.data;
} catch (error) {
ElMessage.error('获取服务器配置失败');
console.error(error);
}
};
// 获取地址池列表
const getAddressPools = async () => {
try {
const response = await GetVPNAddressPoolHandler();
addressPools.value = response.data;
} catch (error) {
ElMessage.error('获取地址池列表失败');
console.error(error);
}
};
// 获取隧道配置列表
const getTunnelConfigs = async () => {
try {
const response = await GetVPNTunnelConfigHandler();
tunnelConfigs.value = response.data;
} catch (error) {
ElMessage.error('获取隧道配置列表失败');
console.error(error);
}
};
// 获取在线服务器列表
const getOnlineServers = async () => {
try {
const response = await GetVPNServerOnlineListHandler();
if (response.data && Array.isArray(response.data)) {
onlineServers.value = response.data.map((server: any) => server.server_id);
}
} catch (error) {
console.error('获取在线服务器状态失败:', error);
}
};
// 启动定时查询
const startOnlineStatusTimer = () => {
// 立即查询一次
getOnlineServers();
// 设置定时器每2秒查询一次
timer = window.setInterval(() => {
getOnlineServers();
}, 2000);
};
// 停止定时查询
const stopOnlineStatusTimer = () => {
if (timer !== null) {
clearInterval(timer);
timer = null;
}
};
// 选择服务器
const selectServer = (server: ServerConfig) => {
selectedServer.value = JSON.parse(JSON.stringify(server)); // 深拷贝避免直接修改
// 确保allow_user_id是用户ID数组
if (server.allow_user_id && server.allow_user_id.length > 0) {
/**
* Extracts the id property from an item object
* @param {Object} item - The item object containing an id property
* @returns {*} The value of the item's id property
*/
user_select_ids.value = server.allow_user_id.map(item => item.id);
} else {
selectedServer.value.allow_user_id = [];
}
// 确保路由数组存在
if (!selectedServer.value.ipv4_router) {
selectedServer.value.ipv4_router = [];
}
if (!selectedServer.value.ipv6_router) {
selectedServer.value.ipv6_router = [];
}
};
// 删除服务器
const deleteServer = async (serverId: string) => {
try {
await ElMessageBox.confirm('确定要删除此服务器配置吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
await DeleteVPNServerHandler({ server_id: serverId });
const index = serverList.value.findIndex(server => server.server_id === serverId);
if (index > -1) {
serverList.value.splice(index, 1);
if (selectedServer.value?.server_id === serverId) {
selectedServer.value = null;
}
ElMessage.success('删除成功');
}
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error('删除失败');
console.error(error);
}
}
};
// 保存配置
const saveConfig = async () => {
if (!selectedServer.value) return;
// 验证IPv4路由配置
for (let i = 0; i < selectedServer.value.ipv4_router.length; i++) {
const router = selectedServer.value.ipv4_router[i];
if (!router.ip || router.ip.trim() === '') {
ElMessage.error(`IPv4路由第${i + 1}IP地址不能为空`);
return;
}
if (!isValidIPv4Address(router.ip)) {
ElMessage.error(`IPv4路由第${i + 1}IP地址格式不正确`);
return;
}
if (!router.prefix || router.prefix < 1 || router.prefix > 32) {
ElMessage.error(`IPv4路由第${i + 1}前缀长度必须在1-32之间`);
return;
}
}
// 验证IPv6路由配置
for (let i = 0; i < selectedServer.value.ipv6_router.length; i++) {
const router = selectedServer.value.ipv6_router[i];
if (!router.ip || router.ip.trim() === '') {
ElMessage.error(`IPv6路由第${i + 1}IP地址不能为空`);
return;
}
if (!isValidIPv6Address(router.ip)) {
ElMessage.error(`IPv6路由第${i + 1}IP地址格式不正确`);
return;
}
if (!router.prefix || router.prefix < 1 || router.prefix > 128) {
ElMessage.error(`IPv6路由第${i + 1}前缀长度必须在1-128之间`);
return;
}
}
// 转换数据格式确保与后端API兼容
const configData = { ...selectedServer.value };
// 将allow_user_id转换为后端期望的格式
configData.allow_user_id = user_select_ids.value.map(id => ({ id: id }));
let req = {
server_id: selectedServer.value.server_id,
server_ip: selectedServer.value.server_ip,
server_info: selectedServer.value.server_info,
config: configData
}
try {
let resp = await SetVPNServerConfigHandler(req);
if (resp["code"] !== 0) {
ElMessage.error(resp["message"]);
return;
}
ElMessage.success('配置保存成功');
//获取最新数据
getServerConfigs();
} catch (error) {
ElMessage.error('保存配置失败');
console.error(error);
}
};
// 添加路由
const addRouter = (type: 'ipv4' | 'ipv6') => {
if (!selectedServer.value) return;
const router: VPNRouter = {
type: type === 'ipv4' ? 4 : 6,
ip: '',
prefix: type === 'ipv4' ? 24 : 64
};
if (!selectedServer.value.ipv4_router) {
selectedServer.value.ipv4_router = [];
}
if (!selectedServer.value.ipv6_router) {
selectedServer.value.ipv6_router = [];
}
if (type === 'ipv4') {
selectedServer.value.ipv4_router.push(router);
} else {
selectedServer.value.ipv6_router.push(router);
}
};
// 删除路由
const removeRouter = (type: 'ipv4' | 'ipv6', index: number) => {
if (!selectedServer.value) return;
if (type === 'ipv4') {
selectedServer.value.ipv4_router.splice(index, 1);
} else {
selectedServer.value.ipv6_router.splice(index, 1);
}
};
// 验证IPv4地址格式
const isValidIPv4Address = (ip: string): boolean => {
if (!ip || ip.trim() === '') return false;
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
return ipv4Regex.test(ip);
};
// 验证IPv6地址格式
const isValidIPv6Address = (ip: string): boolean => {
if (!ip || ip.trim() === '') return false;
// 简化的IPv6验证支持标准IPv6格式
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,7}:$|^(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}$|^(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}$|^(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}$|^[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})$|^:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)$/;
return ipv6Regex.test(ip);
};
const GetAllDefaultUsers = async () => {
try {
const response = await getAllDefaultUsers();
if (response['code'] !== 0) {
console.error('获取默认用户失败:', response["message"]);
return;
}
user_select_opts.value.length = 0;
for (let i = 0; i < response.data.length; i++) {
let user = response.data[i];
if (user.type === 0) {
user_select_opts.value.push({
value: user.id,
label: user.name
});
}
}
//console.log("user_select_opts:", user_select_opts.value);
} catch (error) {
console.error('获取默认用户失败:', error);
}
};
// 组件挂载时获取数据
onMounted(() => {
getServerConfigs();
getAddressPools();
getTunnelConfigs();
GetAllDefaultUsers();
startOnlineStatusTimer();
});
// 组件卸载时清理定时器
onUnmounted(() => {
stopOnlineStatusTimer();
});
</script>
<style scoped>
.vpn-server-config {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
}
.server-list {
max-height: 600px;
overflow-y: auto;
}
.server-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
margin-bottom: 8px;
border: 1px solid #e4e7ed;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.server-item:hover {
background-color: #f5f7fa;
border-color: #409eff;
}
.server-item.active {
background-color: #ecf5ff;
border-color: #409eff;
}
.server-info {
flex: 1;
}
.server-name {
font-weight: bold;
margin-bottom: 4px;
display: flex;
align-items: center;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #909399;
margin-right: 8px;
transition: background-color 0.3s;
}
.status-indicator.online {
background-color: #67c23a;
box-shadow: 0 0 4px rgba(103, 194, 58, 0.5);
}
.server-ip {
font-size: 12px;
color: #909399;
}
.server-actions {
margin-left: 10px;
}
.el-form {
padding: 20px 0;
}
.router-section {
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 10px;
background-color: #fafafa;
}
.router-item {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.router-item:last-child {
margin-bottom: 0;
}
</style>