2026-01-28 20:22:00 +08:00
|
|
|
|
<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
|
|
|
|
|
|
>
|
2026-01-28 21:33:40 +08:00
|
|
|
|
<el-table-column prop="ID" label="ID" width="80" />
|
|
|
|
|
|
<!-- 名称 -->
|
|
|
|
|
|
<el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
|
2026-01-28 20:22:00 +08:00
|
|
|
|
<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">
|
2026-01-28 21:33:40 +08:00
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-form-item label="策略名称" prop="name">
|
|
|
|
|
|
<el-input v-model="policyForm.name"/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
2026-01-28 20:22:00 +08:00
|
|
|
|
<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>
|
2026-01-28 21:33:40 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 策略匹配对话框 -->
|
|
|
|
|
|
<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>
|
2026-01-28 20:22:00 +08:00
|
|
|
|
</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,
|
2026-01-28 21:33:40 +08:00
|
|
|
|
DeleteMyVPNPolicyHandler,
|
|
|
|
|
|
MatchVPNPolicyHandler
|
2026-01-28 20:22:00 +08:00
|
|
|
|
} from '@/api/vpn';
|
2026-01-28 21:33:40 +08:00
|
|
|
|
import { match } from 'assert';
|
|
|
|
|
|
import { el } from 'element-plus/es/locale';
|
2026-01-28 20:22:00 +08:00
|
|
|
|
|
|
|
|
|
|
// VPN策略基础接口定义
|
|
|
|
|
|
interface VPNPolicyBase {
|
2026-01-28 21:33:40 +08:00
|
|
|
|
name: string;
|
2026-01-28 20:22:00 +08:00
|
|
|
|
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();
|
|
|
|
|
|
|
2026-01-28 21:33:40 +08:00
|
|
|
|
// 匹配表单
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-01-28 20:22:00 +08:00
|
|
|
|
// 搜索表单
|
|
|
|
|
|
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 }>({
|
2026-01-28 21:33:40 +08:00
|
|
|
|
name: '',
|
2026-01-28 20:22:00 +08:00
|
|
|
|
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;
|
2026-01-28 21:33:40 +08:00
|
|
|
|
// 默认选择当前选中的服务器
|
|
|
|
|
|
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;
|
2026-01-28 20:22:00 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 显示新增对话框
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-01-28 21:33:40 +08:00
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2026-01-28 20:22:00 +08:00
|
|
|
|
</style>
|