sawAdmin/src/views/system/vpn-policy.vue

1247 lines
42 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-policy-management">
<el-row :gutter="20">
<!-- 左侧服务器列表 -->
<el-col :span="8">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>VPN服务器列表</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>
<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>{{ selectedServer.name }} - 策略管理</span>
<div class="header-actions">
<el-button type="primary" @click="showAddDialog">新增策略</el-button>
<el-button type="primary" @click="showMatchDialog">策略匹配</el-button>
<el-button type="success" @click="refreshPolicies">刷新</el-button>
</div>
</div>
</template>
<!-- 搜索栏 -->
<div class="search-bar">
<el-form :model="searchForm" inline size="default">
<el-form-item label="源类型">
<el-select v-model="searchForm.src_type" placeholder="请选择源类型" clearable style="width: 120px;">
<el-option label="IP地址" :value="0" />
<el-option label="网段" :value="1" />
<el-option label="用户ID" :value="2" />
<el-option label="组ID" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="目标类型">
<el-select v-model="searchForm.dst_type" placeholder="请选择目标类型" clearable style="width: 120px;">
<el-option label="IP地址" :value="0" />
<el-option label="网段" :value="1" />
<el-option label="用户ID" :value="2" />
<el-option label="组ID" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="协议">
<el-select v-model="searchForm.protocol" placeholder="请选择协议" clearable style="width: 120px;">
<el-option label="全部" :value="0" />
<el-option label="ICMP" :value="1" />
<el-option label="TCP" :value="6" />
<el-option label="UDP" :value="17" />
</el-select>
</el-form-item>
<el-form-item label="动作">
<el-select v-model="searchForm.action" placeholder="请选择动作" clearable style="width: 100px;">
<el-option label="拒绝" :value="0" />
<el-option label="允许" :value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchPolicies">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 策略表格 -->
<el-table
:data="policyList"
style="width: 100%; margin-top: 20px;"
v-loading="loading"
stripe
>
<el-table-column prop="ID" label="ID" width="80" />
<!-- 名称 -->
<el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
<el-table-column label="源信息" min-width="150">
<template #default="scope">
<div>{{ getSrcTypeText(scope.row.src_type) }}</div>
<div class="text-secondary">{{ getSrcValue(scope.row) }}</div>
</template>
</el-table-column>
<el-table-column label="目标信息" min-width="150">
<template #default="scope">
<div>{{ getDstTypeText(scope.row.dst_type) }}</div>
<div class="text-secondary">{{ getDstValue(scope.row) }}</div>
</template>
</el-table-column>
<el-table-column label="协议/动作" width="120">
<template #default="scope">
<div>{{ getProtocolText(scope.row.protocol) }}</div>
<el-tag :type="scope.row.action === 1 ? 'success' : 'danger'" size="small">
{{ scope.row.action === 1 ? '允许' : '拒绝' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="info" label="描述" min-width="120" show-overflow-tooltip />
<el-table-column prop="created_at" label="创建时间" width="160">
<template #default="scope">
{{ formatTime(scope.row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" text @click="editPolicy(scope.row)">
编辑
</el-button>
<el-button type="danger" size="small" text @click="deletePolicy(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<el-card shadow="hover" v-else>
<el-empty description="请选择VPN服务器以管理策略" />
</el-card>
</el-col>
</el-row>
<!-- 策略编辑对话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="800px"
@close="resetForm"
>
<el-form
:model="policyForm"
:rules="formRules"
ref="policyFormRef"
label-width="120px"
size="default"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="策略名称" prop="name">
<el-input v-model="policyForm.name"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="服务器ID" prop="server_id">
<el-input v-model="policyForm.server_id" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="IP类型" prop="ip_type">
<el-select v-model="policyForm.ip_type" placeholder="请选择IP类型">
<el-option label="IPv4" :value="4" />
<el-option label="IPv6" :value="6" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="源类型" prop="src_type">
<el-select v-model="policyForm.src_type" placeholder="请选择源类型" @change="onSrcTypeChange">
<el-option label="IP地址" :value="0" />
<el-option label="网段" :value="1" />
<el-option label="用户ID" :value="2" />
<el-option label="组ID" :value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="源值" prop="src_value" :rules="getSrcRules()">
<el-input
v-model="policyForm.src_value"
:placeholder="getSrcPlaceholder()"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="目标类型" prop="dst_type">
<el-select v-model="policyForm.dst_type" placeholder="请选择目标类型" @change="onDstTypeChange">
<el-option label="IP地址" :value="0" />
<el-option label="网段" :value="1" />
<el-option label="用户ID" :value="2" />
<el-option label="组ID" :value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="目标值" prop="dst_value" :rules="getDstRules()">
<el-input
v-model="policyForm.dst_value"
:placeholder="getDstPlaceholder()"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="协议" prop="protocol">
<el-select v-model="policyForm.protocol" placeholder="请选择协议">
<el-option label="全部协议" :value="0" />
<el-option label="ICMP" :value="1" />
<el-option label="TCP" :value="6" />
<el-option label="UDP" :value="17" />
<el-option label="其他" :value="255" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="动作" prop="action">
<el-radio-group v-model="policyForm.action">
<el-radio :label="1">允许</el-radio>
<el-radio :label="0">拒绝</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="策略描述" prop="info">
<el-input
v-model="policyForm.info"
type="textarea"
:rows="3"
placeholder="请输入策略描述信息"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitPolicy" :loading="submitLoading">
确定
</el-button>
</span>
</template>
</el-dialog>
<!-- 策略匹配对话框 -->
<el-dialog
title="策略匹配测试"
v-model="matchDialogVisible"
width="900px"
@close="resetMatchForm"
>
<el-form
:model="matchForm"
ref="matchFormRef"
label-width="120px"
size="default"
class="match-form"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="服务器" prop="server_id">
<el-select v-model="matchForm.server_id" placeholder="请选择服务器" style="width: 100%;">
<el-option
v-for="server in serverList"
:key="server.server_id"
:label="server.name"
:value="server.server_id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="IP类型" prop="ip_type">
<el-select v-model="matchForm.ip_type" placeholder="请选择IP类型">
<el-option label="IPv4" :value="4" />
<el-option label="IPv6" :value="6" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="源类型" prop="src_type">
<el-select v-model="matchForm.src_type" placeholder="请选择源类型" @change="onMatchSrcTypeChange">
<el-option label="IP地址" :value="0" />
<el-option label="网段" :value="1" />
<el-option label="用户ID" :value="2" />
<el-option label="组ID" :value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="源值" prop="src_value">
<el-input
v-model="matchForm.src_value"
:placeholder="getSrcPlaceholder()"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="目标类型" prop="dst_type">
<el-select v-model="matchForm.dst_type" placeholder="请选择目标类型" @change="onMatchDstTypeChange">
<el-option label="IP地址" :value="0" />
<el-option label="网段" :value="1" />
<el-option label="用户ID" :value="2" />
<el-option label="组ID" :value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="目标值" prop="dst_value">
<el-input
v-model="matchForm.dst_value"
:placeholder="getDstPlaceholder()"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="协议" prop="protocol">
<el-select v-model="matchForm.protocol" placeholder="请选择协议">
<el-option label="全部协议" :value="0" />
<el-option label="ICMP" :value="1" />
<el-option label="TCP" :value="6" />
<el-option label="UDP" :value="17" />
<el-option label="其他" :value="255" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="match-actions">
<el-button type="primary" @click="matchPolicy" :loading="matchLoading">匹配策略</el-button>
<el-button @click="resetMatchForm">重置</el-button>
</div>
<!-- 匹配结果 -->
<div v-if="matchResult" class="match-result">
<h4>匹配结果:</h4>
<div class="match-result-item">
<span>策略ID:</span>
<span>{{ matchResult.ID>0? matchResult.ID : '未匹配到策略,使用服务器配置'}}</span>
</div>
<div class="match-result-item" v-if="matchResult.ID>0">
<span>策略名称:</span>
<span>{{ matchResult.name }}</span>
</div>
<div class="match-result-item">
<span>处置动作:</span>
<el-tag :type="matchResult.action === 1 ? 'success' : 'danger'" size="small">
{{ matchResult.action === 1 ? '允许' : '拒绝' }}
</el-tag>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="matchDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import {
GetVPNServerConfigHandler,
GetVPNServerOnlineListHandler,
GetMyVPNPolicyHandler,
CreateMyVPNPolicyHandler,
UpdateMyVPNPolicyHandler,
DeleteMyVPNPolicyHandler,
MatchVPNPolicyHandler
} from '@/api/vpn';
import { match } from 'assert';
import { el } from 'element-plus/es/locale';
// VPN策略基础接口定义
interface VPNPolicyBase {
name: string;
server_id: string;
ip_type: number; // 4, 6
src_type: number; // 0-ip,1-network, 2-userID, 3-groupID
src_ip: string; // type 1, set 0.0.0.0/0 is all
src_user_id: number; // 0-all, more than 0, user
dst_type: number; // 0-ip,1-network, 2-userID, 3-groupID
dst_ip: string;
dst_user_id: number;
protocol: number; // 0:is all, 1-ICMP, 17-UDP etc.
action: number; // 0-deny, 1-permit
info: string;
src_value: string;
dst_value: string;
}
// VPN策略完整接口定义
interface VPNPolicy extends VPNPolicyBase {
ID: number;
CreatedAt: string;
UpdatedAt: string;
}
// 服务器配置接口
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: Array<{id: number}>;
encryption: string;
hash: string;
no_policy_action: number;
user_max_device: number;
duration_time: number;
ipv4_router: Array<{type: number, ip: string, prefix: number}>;
ipv6_router: Array<{type: number, ip: string, prefix: number}>;
}
// 响应式数据
const serverList = ref<ServerConfig[]>([]);
const selectedServer = ref<ServerConfig | null>(null);
const policyList = ref<VPNPolicy[]>([]);
const onlineServers = ref<string[]>([]);
const loading = ref(false);
const submitLoading = ref(false);
const dialogVisible = ref(false);
const matchDialogVisible = ref(false);
const isEdit = ref(false);
const policyFormRef = ref();
// 匹配表单
const matchForm = reactive({
server_id: '',
ip_type: 4,
src_type: 0,
src_value: '',
dst_type: 0,
dst_value: '',
protocol: 0,
src_ip: '',
src_user_id: 0,
dst_ip: '',
dst_user_id: 0
});
// 匹配结果
const matchResult = ref<VPNPolicy>();
const matchLoading = ref(false);
// 搜索表单
const searchForm = reactive({
src_type: undefined as number | undefined,
dst_type: undefined as number | undefined,
protocol: undefined as number | undefined,
action: undefined as number | undefined
});
// 分页信息
const pagination = reactive({
page: 1,
size: 20,
total: 0
});
// 策略表单
const policyForm = reactive<VPNPolicyBase & { id?: number }>({
name: '',
id: undefined,
server_id: '',
ip_type: 4,
src_type: 0,
src_value: '',
dst_value: '',
src_ip: '',
src_user_id: 0,
dst_type: 0,
dst_ip: '',
dst_user_id: 0,
protocol: 0,
action: 1,
info: ''
});
// 表单验证规则
const formRules = {
server_id: [{ required: true, message: '服务器ID不能为空', trigger: 'blur' }],
ip_type: [{ required: true, message: '请选择IP类型', trigger: 'change' }],
src_type: [{ required: true, message: '请选择源类型', trigger: 'change' }],
dst_type: [{ required: true, message: '请选择目标类型', trigger: 'change' }],
protocol: [{ required: true, message: '请选择协议', trigger: 'change' }],
action: [{ required: true, message: '请选择动作', trigger: 'change' }]
};
// 计算属性
const dialogTitle = computed(() => isEdit.value ? '编辑策略' : '新增策略');
// 获取服务器配置列表
const getServerConfigs = async () => {
try {
const response = await GetVPNServerConfigHandler();
serverList.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();
setInterval(() => {
getOnlineServers();
}, 20000);
};
// 选择服务器
const selectServer = (server: ServerConfig) => {
selectedServer.value = server;
policyForm.server_id = server.server_id;
loadPolicies();
};
// 加载策略列表
const loadPolicies = async () => {
if (!selectedServer.value) return;
loading.value = true;
try {
const params = {
server_id: selectedServer.value.server_id,
page: pagination.page,
size: pagination.size,
...searchForm
};
const response = await GetMyVPNPolicyHandler(params);
if (response["code"] === 0) {
policyList.value = response.data|| [];
pagination.total = policyList.value.length;
} else {
ElMessage.error(response["message"] || '获取策略列表失败');
}
} catch (error) {
ElMessage.error('获取策略列表失败');
console.error(error);
} finally {
loading.value = false;
}
};
// 搜索策略
const searchPolicies = () => {
pagination.page = 1;
loadPolicies();
};
// 重置搜索
const resetSearch = () => {
Object.keys(searchForm).forEach(key => {
searchForm[key] = undefined;
});
pagination.page = 1;
loadPolicies();
};
// 刷新策略
const refreshPolicies = () => {
pagination.page = 1;
loadPolicies();
};
//显示匹配对话框
const showMatchDialog = () => {
matchDialogVisible.value = true;
// 默认选择当前选中的服务器
if (selectedServer.value) {
matchForm.server_id = selectedServer.value.server_id;
}
};
// 匹配策略
const matchPolicy = async () => {
if (!matchForm.server_id) {
ElMessage.warning('请选择服务器');
return;
}
// 根据类型设置对应的值
switch (matchForm.src_type) {
case 0:
case 1:
matchForm.src_ip = matchForm.src_value;
matchForm.src_user_id = 0;
break;
case 2:
case 3:
matchForm.src_user_id = parseInt(matchForm.src_value) || 0;
matchForm.src_ip = '';
break;
default:
break;
}
switch (matchForm.dst_type) {
case 0:
case 1:
matchForm.dst_ip = matchForm.dst_value;
matchForm.dst_user_id = 0;
break;
case 2:
case 3:
matchForm.dst_user_id = parseInt(matchForm.dst_value) || 0;
matchForm.dst_ip = '';
break;
default:
break;
}
matchLoading.value = true;
try {
const response = await MatchVPNPolicyHandler(matchForm);
if (response["code"] == 0) {
if (response["data"] === 0){
if (!matchResult.value){
matchResult.value = {} as VPNPolicy;
}
matchResult.value.ID = 0;
matchResult.value.name = '未匹配到策略';
matchResult.value.action = serverList.value.find(item => item.server_id === matchForm.server_id)?.no_policy_action;
}else{
matchResult.value = response.data;
}
ElMessage.success('策略匹配成功');
} else {
ElMessage.error(response["message"] || '策略匹配失败');
}
} catch (error) {
ElMessage.error('策略匹配失败');
console.error(error);
} finally {
matchLoading.value = false;
}
};
// 重置匹配表单
const resetMatchForm = () => {
matchForm.server_id = selectedServer.value?.server_id || '';
matchForm.ip_type = 4;
matchForm.src_type = 0;
matchForm.src_value = '';
matchForm.dst_type = 0;
matchForm.dst_value = '';
matchForm.protocol = 0;
matchForm.src_ip = '';
matchForm.src_user_id = 0;
matchForm.dst_ip = '';
matchForm.dst_user_id = 0;
};
// 匹配表单源类型改变
const onMatchSrcTypeChange = () => {
matchForm.src_ip = '';
matchForm.src_user_id = 0;
};
// 匹配表单目标类型改变
const onMatchDstTypeChange = () => {
matchForm.dst_ip = '';
matchForm.dst_user_id = 0;
};
// 显示新增对话框
const showAddDialog = () => {
isEdit.value = false;
resetForm();
dialogVisible.value = true;
};
// 编辑策略
const editPolicy = (policy: VPNPolicy) => {
isEdit.value = true;
Object.assign(policyForm, {
id: policy.ID,
server_id: policy.server_id,
ip_type: policy.ip_type,
src_type: policy.src_type,
src_ip: policy.src_ip,
src_user_id: policy.src_user_id,
dst_type: policy.dst_type,
dst_ip: policy.dst_ip,
dst_user_id: policy.dst_user_id,
protocol: policy.protocol,
action: policy.action,
info: policy.info
});
switch (policyForm.src_type) {
case 0:
case 1:
policyForm.src_value = policy.src_ip;
break;
case 2:
case 3:
policyForm.src_value = policy.src_user_id.toString();
break;
default:
break;
}
switch (policyForm.dst_type) {
case 0:
case 1:
policyForm.dst_value = policy.dst_ip;
break;
case 2:
case 3:
policyForm.dst_value = policy.dst_user_id.toString();
break;
default:
break;
}
dialogVisible.value = true;
};
// 删除策略
const deletePolicy = async (policy: VPNPolicy) => {
try {
await ElMessageBox.confirm('确定要删除此策略吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
const response = await DeleteMyVPNPolicyHandler({
id: policy.ID,
});
if (response["code"] === 0) {
ElMessage.success('删除成功');
loadPolicies();
} else {
ElMessage.error(response["message"] || '删除失败');
}
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error('删除失败');
console.error(error);
}
}
};
// 提交策略
const submitPolicy = async () => {
if (!policyFormRef.value) return;
try {
await policyFormRef.value.validate();
submitLoading.value = true;
switch (policyForm.src_type) {
case 0:
case 1:
policyForm.src_ip = policyForm.src_value;
break;
case 2:
case 3:
policyForm.src_user_id = parseInt(policyForm.src_value);
break;
default:
break;
}
switch (policyForm.dst_type) {
case 0:
case 1:
policyForm.dst_ip = policyForm.dst_value;
break;
case 2:
case 3:
policyForm.dst_user_id = parseInt(policyForm.dst_value);
break;
default:
break;
}
console.log('policyForm:', policyForm);
let response;
if (isEdit.value && policyForm.id) {
response = await UpdateMyVPNPolicyHandler(policyForm);
} else {
response = await CreateMyVPNPolicyHandler(policyForm);
}
if (response["code"] === 0) {
ElMessage.success(isEdit.value ? '更新成功' : '创建成功');
dialogVisible.value = false;
loadPolicies();
} else {
ElMessage.error(response["message"] || '操作失败');
}
} catch (error) {
console.error('表单验证失败:', error);
} finally {
submitLoading.value = false;
}
};
// 重置表单
const resetForm = () => {
Object.assign(policyForm, {
id: undefined,
server_id: selectedServer.value?.server_id || '',
ip_type: 4,
src_type: 0,
src_ip: '',
src_user_id: 0,
dst_type: 0,
dst_ip: '',
dst_user_id: 0,
protocol: 0,
action: 1,
info: ''
});
policyFormRef.value?.clearValidate();
};
// 源类型改变处理
const onSrcTypeChange = () => {
policyForm.src_ip = '';
policyForm.src_user_id = 0;
};
// 目标类型改变处理
const onDstTypeChange = () => {
policyForm.dst_ip = '';
policyForm.dst_user_id = 0;
};
// 获取源类型验证规则
const getSrcRules = () => {
const rules = [{ required: true, message: '此项不能为空', trigger: 'blur' }];
switch (policyForm.src_type) {
case 0: // IP地址
case 1: // 网段
rules.push({
validator: (rule: any, value: string, callback: Function) => {
if (!value) {
callback(new Error('IP地址不能为空'));
return;
}
if (policyForm.src_type === 1) {
// 网段格式验证
const cidrRegex = /^([0-9]{1,3}\.){3}[0-9]{1,3}\/[0-9]{1,2}$/;
if (!cidrRegex.test(value)) {
callback(new Error('请输入正确的CIDR格式192.168.1.0/24'));
return;
}
} else {
// IP地址格式验证
const ipRegex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
if (!ipRegex.test(value)) {
callback(new Error('请输入正确的IP地址格式'));
return;
}
}
callback();
},
trigger: 'blur'
});
break;
case 2: // 用户ID
rules.push({
validator: (rule: any, value: number, callback: Function) => {
if (!value || value <= 0) {
callback(new Error('用户ID必须大于0'));
return;
}
callback();
},
trigger: 'blur'
});
break;
case 3: // 组ID
rules.push({
validator: (rule: any, value: number, callback: Function) => {
if (!value || value <= 0) {
callback(new Error('组ID必须大于0'));
return;
}
callback();
},
trigger: 'blur'
});
break;
}
return rules;
};
// 获取目标类型验证规则
const getDstRules = () => {
const rules = [{ required: true, message: '此项不能为空', trigger: 'blur' }];
switch (policyForm.dst_type) {
case 0: // IP地址
case 1: // 网段
rules.push({
validator: (rule: any, value: string, callback: Function) => {
if (!value) {
callback(new Error('IP地址不能为空'));
return;
}
if (policyForm.dst_type === 1) {
// 网段格式验证
const cidrRegex = /^([0-9]{1,3}\.){3}[0-9]{1,3}\/[0-9]{1,2}$/;
if (!cidrRegex.test(value)) {
callback(new Error('请输入正确的CIDR格式192.168.1.0/24'));
return;
}
} else {
// IP地址格式验证
const ipRegex = /^([0-9]{1,3}\.){3}[0-9]{1,3}$/;
if (!ipRegex.test(value)) {
callback(new Error('请输入正确的IP地址格式'));
return;
}
}
callback();
},
trigger: 'blur'
});
break;
case 2: // 用户ID
rules.push({
validator: (rule: any, value: number, callback: Function) => {
if (!value || value <= 0) {
callback(new Error('用户ID必须大于0'));
return;
}
callback();
},
trigger: 'blur'
});
break;
case 3: // 组ID
rules.push({
validator: (rule: any, value: number, callback: Function) => {
if (!value || value <= 0) {
callback(new Error('组ID必须大于0'));
return;
}
},
trigger: 'blur'
});
break;
}
return rules;
};
// 获取源类型占位符
const getSrcPlaceholder = () => {
switch (policyForm.src_type) {
case 0: return '请输入IP地址192.168.1.1';
case 1: return '请输入网段192.168.1.0/24';
case 2: return '请输入用户ID';
case 3: return '请输入组ID';
default: return '请输入值';
}
};
// 获取目标类型占位符
const getDstPlaceholder = () => {
switch (policyForm.dst_type) {
case 0: return '请输入IP地址192.168.1.1';
case 1: return '请输入网段192.168.1.0/24';
case 2: return '请输入用户ID';
case 3: return '请输入组ID';
default: return '请输入值';
}
};
// 获取源类型文本
const getSrcTypeText = (type: number) => {
const types: {[key: number]: string} = {
0: 'IP地址',
1: '网段',
2: '用户ID',
3: '组ID'
};
return types[type] || '未知';
};
// 获取目标类型文本
const getDstTypeText = (type: number) => {
const types: {[key: number]: string} = {
0: 'IP地址',
1: '网段',
2: '用户ID',
3: '组ID'
};
return types[type] || '未知';
};
// 获取源值显示
const getSrcValue = (policy: VPNPolicy) => {
switch (policy.src_type) {
case 0: return policy.src_ip || '-';
case 1: return policy.src_ip || '-';
case 2: return `用户ID: ${policy.src_user_id}`;
case 3: return `组ID: ${policy.src_user_id}`;
default: return '-';
}
};
// 获取目标值显示
const getDstValue = (policy: VPNPolicy) => {
switch (policy.dst_type) {
case 0: return policy.dst_ip || '-';
case 1: return policy.dst_ip || '-';
case 2: return `用户ID: ${policy.dst_user_id}`;
case 3: return `组ID: ${policy.dst_user_id}`;
default: return '-';
}
};
// 获取协议文本
const getProtocolText = (protocol: number) => {
const protocols: {[key: number]: string} = {
0: '全部',
1: 'ICMP',
6: 'TCP',
17: 'UDP',
255: '其他'
};
return protocols[protocol] || '未知';
};
// 格式化时间
const formatTime = (timeStr: string) => {
if (!timeStr) return '-';
return new Date(timeStr).toLocaleString('zh-CN');
};
// 分页处理
const handleSizeChange = (size: number) => {
pagination.size = size;
pagination.page = 1;
loadPolicies();
};
const handleCurrentChange = (page: number) => {
pagination.page = page;
loadPolicies();
};
// 组件挂载
onMounted(() => {
getServerConfigs();
startOnlineStatusTimer();
});
</script>
<style scoped>
.vpn-policy-management {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
}
.header-actions {
display: flex;
gap: 10px;
}
.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;
}
.search-bar {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 10px;
}
.text-secondary {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.pagination-wrapper {
margin-top: 20px;
display: flex;
justify-content: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.match-form {
max-height: 500px;
overflow-y: auto;
}
.match-actions {
margin: 20px 0;
text-align: center;
}
.match-result h4 {
margin: 0 0 10px 0;
color: #303133;
font-weight: bold;
}
.no-match-result {
margin-top: 20px;
text-align: center;
}
</style>