添加策略匹配

This commit is contained in:
lijun 2026-01-28 20:22:00 +08:00
parent 5508cc1f64
commit 9e706091d8
8 changed files with 1092 additions and 6 deletions

41
components.d.ts vendored
View File

@ -8,6 +8,44 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
Countup: typeof import('./src/components/countup.vue')['default']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCol: typeof import('element-plus/es')['ElCol']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
Header: typeof import('./src/components/header.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
@ -20,4 +58,7 @@ declare module '@vue/runtime-core' {
Upload_file2: typeof import('./src/components/upload_file2.vue')['default']
UploadFile: typeof import('./src/components/upload-file.vue')['default']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}

View File

@ -104,3 +104,58 @@ export const LocalClientDisConnectHandler = (Data) => {
export const LocalClientStatusHandler = () => {
return local_request.get('/vpn/get_status')
}
// VPN Policy Management APIs
// myVPNPolicyGroup := router.Group("/vpn_policy")
// myVPNPolicyGroup.GET("/get", GetMyVPNPolicyHandler)
// myVPNPolicyGroup.POST("/create", CreateMyVPNPolicyHandler)
// myVPNPolicyGroup.POST("/update", UpdateMyVPNPolicyHandler)
// myVPNPolicyGroup.DELETE("/delete", DeleteMyVPNPolicyHandler)
/**
* VPN策略列表
* @param {Object} params server_id等
* @returns {Promise} VPN策略列表的Promise对象
*/
export const GetMyVPNPolicyHandler = (params) => {
return request.get('/vpn_policy/get', { params })
}
/**
* VPN策略
* @param {Object} data VPN策略数据
* @returns {Promise} Promise对象
*/
export const CreateMyVPNPolicyHandler = (data) => {
return request.post('/vpn_policy/create', data)
}
/**
* VPN策略
* @param {Object} data VPN策略更新数据id
* @returns {Promise} Promise对象
*/
export const UpdateMyVPNPolicyHandler = (data) => {
return request.post('/vpn_policy/update', data)
}
/**
* VPN策略
* @param {Object} data id和server_id
* @returns {Promise} Promise对象
*/
export const DeleteMyVPNPolicyHandler = (data) => {
let url = '/vpn_policy/delete';
if (data.id){
url += "?id="+data.id
}
if (data.server_id){
url += "&server_id=" + data.server_id
}
return request.delete(url)
}
export const MatchVPNPolicyHandler = (data) => {
return request.post('/vpn_policy/match', data)
}

View File

@ -102,7 +102,13 @@ export const menuData: Menus[] = [
pid: '75',
index: '/vpn-online-user',
title: '在线用户',
}
},
{
id: '756',
pid: '75',
index: '/vpn-policy',
title: 'VPN策略',
},
],
},
{

View File

@ -173,6 +173,15 @@ const routes: RouteRecordRaw[] = [
},
component: () => import(/* webpackChunkName: "system-user" */ '../views/system/vpn-server-online-user.vue'),
},
{
path: '/vpn-policy',
name: 'vpn-policy',
meta: {
title: 'VPN策略',
permiss: '755',
},
component: () => import(/* webpackChunkName: "system-user" */ '../views/system/vpn-policy.vue'),
},
{
path: '/callback',

View File

@ -66,6 +66,7 @@ export const usePermissStore = defineStore("permiss", {
"753", //VPN隧道管理
"754", //VPN客户端UI
"755", //VPN在线用户连接
"756", //VPN策略
],
user: ["0", "8", "7", "9", "51" ,"53","55" ,"56", "57", "58", "59", "61", "71", "754"],
},

View File

@ -0,0 +1,973 @@
<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 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="服务器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>
</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
} from '@/api/vpn';
// VPN
interface VPNPolicyBase {
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 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 }>({
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;
};
//
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;
}
</style>

View File

@ -103,11 +103,10 @@
<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-form-item label="策略匹配失败">
<el-select v-model="selectedServer.no_policy_action" placeholder="请选择策略匹配失败动作">
<el-option label="拒绝" :value="0" />
<el-option label="允许" :value="1" />
</el-select>
</el-form-item>
</el-col>
@ -360,6 +359,7 @@ interface ServerConfig {
allow_user_id: UserID[];
encryption: string;
hash: string;
no_policy_action:number;
user_max_device: number;
duration_time: number;
ipv4_router: VPNRouter[];

View File

@ -106,6 +106,7 @@ interface ServerConfig {
allow_user_id: any[];
encryption: string;
hash: string;
no_policy_action:number;
user_max_device: number;
duration_time: number;
ipv4_router: any[];