修改客户端UI连接显示逻辑

This commit is contained in:
lijun 2026-01-17 17:27:27 +08:00
parent 6ed3118df1
commit 09d5c69072
4 changed files with 339 additions and 55 deletions

View File

@ -59,6 +59,10 @@ export const GetVPNServerOnlineListHandler = () => {
return request.get('/vpn/get_server_online') return request.get('/vpn/get_server_online')
} }
export const GetSurppotVPNServerOnlineListHandler = () => {
return request.get('/vpn/get_support_vpn_server')
}
export const DeleteVPNTunnelHandler = (Data) => { export const DeleteVPNTunnelHandler = (Data) => {
return request.delete('/vpn/delete_vpn_tunnel', { data: Data }) return request.delete('/vpn/delete_vpn_tunnel', { data: Data })
} }
@ -78,6 +82,18 @@ export const DeleteVPNServerHandler = (Data) => {
return request.delete('/vpn/delete_vpn_server', { data: Data }) return request.delete('/vpn/delete_vpn_server', { data: Data })
} }
export const GetClientDownloadURLHandler = () => {
return request.get("/vpn/clients_url")
}
export const LocalClientConnectHandler = (Data) => { export const LocalClientConnectHandler = (Data) => {
return local_request.post('/vpn/connect', Data) return local_request.post('/vpn/connect', Data)
}
export const LocalClientDisConnectHandler = (Data) => {
let url = '/vpn/disconnect?server_id=' + Data.server_id;
return local_request.get(url)
}
//获取本地客户端连接状态
export const LocalClientStatusHandler = () => {
return local_request.get('/vpn/get_status')
} }

View File

@ -79,7 +79,7 @@ request.interceptors.response.use(
} }
if(result.data.code == 1) { if(result.data.code == 1) {
ElMessage.error('请求失败,请稍后重试'); ElMessage.error('请求失败,请运行客户端');
} else { } else {
return result.data; return result.data;
} }

View File

@ -14,34 +14,35 @@
<el-empty v-if="!loading && vpnServers.length === 0" description="暂无在线VPN服务器" /> <el-empty v-if="!loading && vpnServers.length === 0" description="暂无在线VPN服务器" />
<el-row :gutter="20" v-else> <el-row :gutter="20" v-else>
<el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="server in vpnServers" :key="server.id"> <el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="server in vpnServers" :key="server.server_id">
<el-card class="vpn-server-card" shadow="hover"> <el-card class="vpn-server-card" shadow="hover">
<div class="server-info"> <div class="server-info">
<div class="server-header"> <div class="server-header">
<el-icon class="server-icon"><Monitor /></el-icon> <el-icon class="server-icon"><Monitor /></el-icon>
<h3>{{ server.name || server.server_name || 'VPN服务器' }}</h3> <h3>{{ server.name || server.name || 'VPN服务器' }}</h3>
</div> </div>
<div class="server-details"> <div class="server-details">
<div class="detail-item"> <div class="detail-item">
<span class="label">IP地址:</span> <span class="label">IP地址:</span>
<span class="value">{{ server.ip || server.server_ip || 'N/A' }}</span> <span class="value">{{ server.server_ip || server.server_ip || 'N/A' }}</span>
</div>
<div class="detail-item" v-if="server.protocol">
<span class="label">协议:</span>
<span class="value">{{ server.protocol === 1 ? 'TCP' : 'UDP'}}</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<span class="label">端口:</span> <span class="label">端口:</span>
<span class="value">{{ server.port || 'N/A' }}</span> <span class="value" v-if="server.protocol == 1">{{ server.tcp_port || 'N/A' }}</span>
<span class="value" v-if="server.protocol == 2">{{ server.udp_port || 'N/A' }}</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<span class="label">状态:</span> <span class="label">状态:</span>
<el-tag type="success" size="small">在线</el-tag> <el-tag type="success" size="small">在线</el-tag>
</div> </div>
<div class="detail-item" v-if="server.location"> <div class="detail-item" v-if="server.server_info">
<span class="label">位置:</span> <span class="label">描述信息:</span>
<span class="value">{{ server.location }}</span> <span class="value">{{ server.server_info }}</span>
</div>
<div class="detail-item" v-if="server.protocol">
<span class="label">协议:</span>
<span class="value">{{ server.protocol }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -51,11 +52,32 @@
type="primary" type="primary"
size="small" size="small"
@click="connectToServer(server)" @click="connectToServer(server)"
:loading="connectingServers.includes(server.id)" :loading="connectingServers.includes(server.server_id)"
v-if = "clientIsConnectServerID == ''"
> >
<el-icon><Connection /></el-icon> <el-icon><Connection /></el-icon>
连接 连接
</el-button> </el-button>
<el-button
type="primary"
size="small"
@click="disConnectToServer(server)"
:loading="connectingServers.includes(server.server_id)"
v-if = "clientIsConnectServerID == server.server_id"
>
<el-icon><Connection /></el-icon>
断开
</el-button>
<el-button
type="primary"
size="small"
@click="showOnlineInfo(server)"
:loading="connectingServers.includes(server.server_id)"
v-if = "clientIsConnectServerID == server.server_id"
>
<el-icon><Connection /></el-icon>
显示在线信息
</el-button>
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
@ -70,28 +92,15 @@
:before-close="handleDialogClose" :before-close="handleDialogClose"
> >
<div class="download-options"> <div class="download-options">
<p>请选择您的操作系统类型</p> <p>{{ clientUrls.length ? '请选择您的操作系统类型:' : '暂不支持下载客户端' }}</p>
<div class="client-options">
<div v-for="clientUrl in clientUrls" :key="clientUrl.platform">
<el-button <el-button
class="download-btn" class="download-btn"
@click="downloadClient('windows')" @click="downloadClient(clientUrl.platform)"
:icon="Monitor" :icon="Monitor"
> >
Windows 客户端 {{ clientUrl.platform }} 客户端
</el-button>
<el-button
class="download-btn"
@click="downloadClient('linux')"
:icon="Monitor"
>
Linux 客户端
</el-button>
<el-button
class="download-btn"
@click="downloadClient('macos')"
:icon="Monitor"
>
macOS 客户端
</el-button> </el-button>
</div> </div>
</div> </div>
@ -100,37 +109,215 @@
<el-button @click="showDownloadDialog = false">取消</el-button> <el-button @click="showDownloadDialog = false">取消</el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 在线信息对话框 -->
<el-dialog
v-model="showOnlineInfoDialog"
title="VPN连接详细信息"
width="800px"
:before-close="handleOnlineInfoDialogClose"
>
<div class="online-info-content" v-loading="onlineInfoLoading">
<el-tabs v-model="activeOnlineInfoTab" type="card">
<!-- 基本信息标签页 -->
<el-tab-pane label="基本信息" name="basic">
<el-descriptions :column="2" border v-if="onlineInfoData.online_info">
<el-descriptions-item label="服务器ID">{{ onlineInfoData.online_info.server_id }}</el-descriptions-item>
<el-descriptions-item label="服务器IP">{{ onlineInfoData.online_info.server_ip }}</el-descriptions-item>
<el-descriptions-item label="TCP端口">{{ onlineInfoData.online_info.tcp_port }}</el-descriptions-item>
<el-descriptions-item label="UDP端口">{{ onlineInfoData.online_info.udp_port }}</el-descriptions-item>
<el-descriptions-item label="协议">{{ onlineInfoData.online_info.protocol === 1 ? 'TCP' : 'UDP' }}</el-descriptions-item>
<el-descriptions-item label="IP类型">{{ onlineInfoData.online_info.ip_type === 46 ? 'IPv4/IPv6' : 'IPv4' }}</el-descriptions-item>
<el-descriptions-item label="私有IPv4">{{ onlineInfoData.online_info.private_ipv4 }}</el-descriptions-item>
<el-descriptions-item label="IPv4前缀">{{ onlineInfoData.online_info.ipv4_prefix }}</el-descriptions-item>
<el-descriptions-item label="网关">{{ onlineInfoData.online_info.gateway }}</el-descriptions-item>
<el-descriptions-item label="加密方式">{{ onlineInfoData.online_info.encryption }}</el-descriptions-item>
<el-descriptions-item label="哈希算法">{{ onlineInfoData.online_info.hash }}</el-descriptions-item>
<el-descriptions-item label="连接时间" v-if="onlineInfoData.connect_status">
{{ onlineInfoData.connect_status.connect_time }}
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 流量统计标签页 -->
<el-tab-pane label="流量统计" name="traffic">
<el-descriptions :column="2" border v-if="onlineInfoData.connect_status">
<el-descriptions-item label="发送字节">
{{ formatBytes(onlineInfoData.connect_status.send_bytes) }}
</el-descriptions-item>
<el-descriptions-item label="接收字节">
{{ formatBytes(onlineInfoData.connect_status.receive_byes) }}
</el-descriptions-item>
<el-descriptions-item label="发送数据包">{{ onlineInfoData.connect_status.send_packets }}</el-descriptions-item>
<el-descriptions-item label="接收数据包">{{ onlineInfoData.connect_status.receive_packets }}</el-descriptions-item>
<el-descriptions-item label="私有IP">{{ onlineInfoData.connect_status.private_ip }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 路由信息标签页 -->
<el-tab-pane label="路由信息" name="routing">
<div v-if="onlineInfoData.connect_status?.router?.length > 0">
<h4>IPv4路由表</h4>
<el-table :data="onlineInfoData.connect_status.router" style="width: 100%">
<el-table-column prop="type" label="类型" width="100">
<template #default="scope">
{{ scope.row.type === 4 ? 'IPv4' : 'IPv6' }}
</template>
</el-table-column>
<el-table-column prop="ip" label="目标网络" />
<el-table-column prop="prefix" label="前缀长度" width="100" />
</el-table>
</div>
<div v-else>
<el-empty description="暂无路由信息" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<el-button @click="showOnlineInfoDialog = false">关闭</el-button>
<el-button type="primary" @click="refreshOnlineInfo">刷新</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Download, Monitor, Connection } from '@element-plus/icons-vue' import { Download, Monitor, Connection } from '@element-plus/icons-vue'
import { GetVPNServerOnlineListHandler, LocalClientConnectHandler } from '@/api/vpn' import { GetSurppotVPNServerOnlineListHandler, LocalClientConnectHandler, LocalClientStatusHandler,LocalClientDisConnectHandler,GetClientDownloadURLHandler } from '@/api/vpn'
interface VPNServer { interface ServerInfo {
id: string | number /** 服务器名称 */
name?: string name: string;
server_name?: string /** 服务器唯一标识ID */
ip?: string server_id: string;
server_ip?: string /** 服务器IPv4地址 */
port?: string | number server_ip: string;
location?: string /** 服务器附加信息(预留字段) */
protocol?: string server_info: string;
[key: string]: any /** 服务器IPv6地址 */
server_ipv6: string;
/** UDP协议端口号 */
udp_port: number;
/** TCP协议端口号 */
tcp_port: number;
/** 协议类型标识2可能代表TCP/UDP双协议等 */
protocol: number;
} }
const vpnServers = ref<VPNServer[]>([]) interface ClientUrl {
platform: string;
download_url: string;
}
const vpnServers = ref<ServerInfo[]>([])
const loading = ref(false) const loading = ref(false)
const connectingServers = ref<(string | number)[]>([]) const connectingServers = ref<(string | number)[]>([])
const clientIsConnectServerID = ref('')
const showDownloadDialog = ref(false) const showDownloadDialog = ref(false)
let statusTimerId: number | null = null
let serverListTimerId: number | null = null
const clientUrls = ref<ClientUrl[]>([])
const showOnlineInfoDialog = ref(false)
const onlineInfoLoading = ref(false)
const activeOnlineInfoTab = ref('basic')
const onlineInfoData = ref({
online_info: null,
connect_status: null,
status: 0
})
//
const formatBytes = (bytes: number): string => {
if (!bytes || bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// 线
const showOnlineInfo = async (server: ServerInfo) => {
showOnlineInfoDialog.value = true
activeOnlineInfoTab.value = 'basic'
await refreshOnlineInfo()
}
// 线
const refreshOnlineInfo = async () => {
onlineInfoLoading.value = true
try {
const response = await LocalClientStatusHandler()
if (response && response["code"] === 0) {
onlineInfoData.value = response["data"] || {}
} else {
ElMessage.error(response["message"] || '获取在线信息失败')
}
} catch (error) {
console.error('获取在线信息错误:', error)
ElMessage.error('获取在线信息失败,请稍后重试')
} finally {
onlineInfoLoading.value = false
}
}
// 线
const handleOnlineInfoDialogClose = () => {
showOnlineInfoDialog.value = false
}
const LocalClientStatus = async () => {
const response = await LocalClientStatusHandler()
if (response && response["code"] === 0) {
let data = response["data"]
if (data && data["status"] == 2001) {
clientIsConnectServerID.value = data["online_info"]["server_id"]
onlineInfoData.value = response["data"] || {}
console.log('clientIsConnectServerID:', clientIsConnectServerID.value)
}else{
clientIsConnectServerID.value = ''
}
} else {
ElMessage.error(response["message"] || '获取VPN客户端状态失败')
}
}
const disConnectToServer = async (server: ServerInfo) => {
try {
await ElMessageBox.confirm(
`确定要断开连接到服务器 ${server.name || server.name || server.server_id} 吗?`,
'确认断开',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
}
)
connectingServers.value.push(server.server_id)
const requestData = {
server_id: server.server_id,
}
const response = await LocalClientDisConnectHandler(requestData)
if (response && response["code"] === 0) {
ElMessage.success('断开连接成功')
} else {
ElMessage.error(response["message"] || '断开连接失败')
}
} catch (error) {
}finally {
connectingServers.value = connectingServers.value.filter(id => id !== server.server_id)
}
}
// 线VPN // 线VPN
const fetchVPNServers = async () => { const fetchVPNServers = async () => {
loading.value = true loading.value = true
try { try {
const response = await GetVPNServerOnlineListHandler() const response = await GetSurppotVPNServerOnlineListHandler()
if (response && response["code"] === 0) { if (response && response["code"] === 0) {
vpnServers.value = response.data || [] vpnServers.value = response.data || []
} else { } else {
@ -145,10 +332,10 @@ const fetchVPNServers = async () => {
} }
// VPN // VPN
const connectToServer = async (server: VPNServer) => { const connectToServer = async (server: ServerInfo) => {
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(
`确定要连接到服务器 ${server.name || server.server_name || server.id} 吗?`, `确定要连接到服务器 ${server.name || server.name || server.server_id} 吗?`,
'确认连接', '确认连接',
{ {
confirmButtonText: '确定', confirmButtonText: '确定',
@ -173,6 +360,7 @@ const connectToServer = async (server: VPNServer) => {
} else if (response && response["code"] === 20){ } else if (response && response["code"] === 20){
//线 //线
ElMessage.error(response["message"]) ElMessage.error(response["message"])
clientIsConnectServerID.value = server.server_id
} else { } else {
ElMessage.error(response["message"]) ElMessage.error(response["message"])
} }
@ -182,7 +370,7 @@ const connectToServer = async (server: VPNServer) => {
ElMessage.error('VPN连接失败请稍后重试') ElMessage.error('VPN连接失败请稍后重试')
} }
} finally { } finally {
connectingServers.value = connectingServers.value.filter(id => id !== server.id) connectingServers.value = connectingServers.value.filter(id => id !== server.server_id)
} }
} }
@ -193,13 +381,13 @@ const downloadClient = (clientType: string) => {
// //
setTimeout(() => { setTimeout(() => {
const downloadUrls = { let url = "";
windows: 'https://gitee.com/junleea/my-vpn-client/releases/download/0.1/myvpn-client-windows_amd_x64.exe', for (let client_url of clientUrls.value) {
linux: 'https://gitee.com/junleea/my-vpn-client/releases/download/0.1/myvpn-client-windows_amd_x64.exe', if (client_url.platform == clientType) {
macos: 'https://gitee.com/junleea/my-vpn-client/releases/download/0.1/myvpn-client-windows_amd_x64.exe' url = client_url.download_url;
break;
}
} }
const url = downloadUrls[clientType as keyof typeof downloadUrls]
if (url) { if (url) {
const a = document.createElement('a') const a = document.createElement('a')
a.href = url a.href = url
@ -214,6 +402,19 @@ const downloadClient = (clientType: string) => {
}, 1000) }, 1000)
} }
const GetClientDownloadUrl =async () =>{
try{
let resp =await GetClientDownloadURLHandler()
if (resp && resp["code"] == 0){
if (resp["data"]){
clientUrls.value = resp["data"]
}
}
}catch(error){
console.error('获取客户端下载URL失败:', error)
}
}
// //
const handleDialogClose = () => { const handleDialogClose = () => {
showDownloadDialog.value = false showDownloadDialog.value = false
@ -221,7 +422,21 @@ const handleDialogClose = () => {
// //
onMounted(() => { onMounted(() => {
GetClientDownloadUrl()
//
fetchVPNServers() fetchVPNServers()
LocalClientStatus()
// 2
statusTimerId = setInterval(LocalClientStatus, 2000)
// 10
serverListTimerId = setInterval(fetchVPNServers, 10000)
})
//
onUnmounted(() => {
if (statusTimerId) clearInterval(statusTimerId)
if (serverListTimerId) clearInterval(serverListTimerId)
}) })
</script> </script>
@ -372,4 +587,57 @@ onMounted(() => {
margin-bottom: 2px; margin-bottom: 2px;
} }
} }
.online-info-content {
max-height: 600px;
overflow-y: auto;
}
.online-info-content h4 {
margin: 20px 0 10px 0;
color: #303133;
font-size: 16px;
font-weight: 500;
}
.online-info-content h4:first-child {
margin-top: 0;
}
/* 自定义标签页样式 */
.el-tabs--card .el-tabs__item {
height: 36px;
line-height: 36px;
}
/* 描述列表样式调整 */
.el-descriptions__body .el-descriptions__table {
table-layout: fixed;
}
.el-descriptions__label {
font-weight: 500;
color: #606266;
}
/* 表格样式 */
.el-table .el-table__header th {
background-color: #f8f9fa;
color: #606266;
font-weight: 500;
}
/* 响应式设计补充 */
@media (max-width: 768px) {
.online-info-content {
max-height: 400px;
}
.el-descriptions :deep(.el-descriptions__body) .el-descriptions__table {
width: 100%;
}
.el-descriptions :deep(.el-descriptions__body) .el-descriptions__cell {
width: 50%;
}
}
</style> </style>

View File

@ -27,7 +27,7 @@
IPv6: {{ tunnel.config.auto_ipv6 ? '自动' : tunnel.config.ipv6_address }} IPv6: {{ tunnel.config.auto_ipv6 ? '自动' : tunnel.config.ipv6_address }}
</div> </div>
<div class="tunnel-limits"> <div class="tunnel-limits">
上行: {{ tunnel.config.upload_limit }} Mbps | 下行: {{ tunnel.config.download_limit }} Mbps 上行: {{ tunnel.config.upload_limit }} Kbps | 下行: {{ tunnel.config.download_limit }} Kbps
</div> </div>
</div> </div>
<div class="tunnel-actions"> <div class="tunnel-actions">