添加服务器状态
This commit is contained in:
parent
52e6977ced
commit
f5f8ac3aee
|
|
@ -109,6 +109,12 @@ export const menuData: Menus[] = [
|
|||
index: '/vpn-policy',
|
||||
title: 'VPN策略',
|
||||
},
|
||||
{
|
||||
id: '757',
|
||||
pid: '75',
|
||||
index: '/vpn-status',
|
||||
title: 'VPN服务器状态',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -178,10 +178,19 @@ const routes: RouteRecordRaw[] = [
|
|||
name: 'vpn-policy',
|
||||
meta: {
|
||||
title: 'VPN策略',
|
||||
permiss: '755',
|
||||
permiss: '756',
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "system-user" */ '../views/system/vpn-policy.vue'),
|
||||
},
|
||||
{
|
||||
path: '/vpn-status',
|
||||
name: 'vpn-status',
|
||||
meta: {
|
||||
title: 'VPN服务器状态',
|
||||
permiss: '757',
|
||||
},
|
||||
component: () => import(/* webpackChunkName: "system-user" */ '../views/system/vpn-server-status.vue'),
|
||||
},
|
||||
|
||||
{
|
||||
path: '/callback',
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ export const usePermissStore = defineStore("permiss", {
|
|||
"754", //VPN客户端UI
|
||||
"755", //VPN在线用户连接
|
||||
"756", //VPN策略
|
||||
"757", //VPN服务器状态
|
||||
],
|
||||
user: ["0", "8", "7", "9", "51" ,"53","55" ,"56", "57", "58", "59", "61", "71", "754"],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,396 @@
|
|||
<template>
|
||||
<div class="vpn-server-status">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>服务器列表</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="server-list">
|
||||
<div
|
||||
v-for="server in serverList"
|
||||
:key="server.server_id"
|
||||
class="server-item"
|
||||
:class="{ active: selectedServer?.server_id === server.server_id }"
|
||||
@click="selectServer(server)"
|
||||
>
|
||||
<div class="server-info">
|
||||
<div class="server-name">
|
||||
<span class="status-indicator" :class="{ 'online': isServerOnline(server) }"></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 && selectedServer.vpn_status">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>服务器状态</span>
|
||||
<el-tag :type="getStatusType(selectedServer.vpn_status.status)" size="small">
|
||||
{{ getStatusText(selectedServer.vpn_status.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="服务器名称">
|
||||
{{ selectedServer.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="服务器ID">
|
||||
{{ selectedServer.server_id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="接收数据包">
|
||||
{{ selectedServer.vpn_status.receive_packets }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发送数据包">
|
||||
{{ selectedServer.vpn_status.send_packets }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="接收字节">
|
||||
{{ formatBytes(selectedServer.vpn_status.receive_bytes) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发送字节">
|
||||
{{ formatBytes(selectedServer.vpn_status.send_bytes) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="下行速率">
|
||||
<span class="rate-text">↓ {{ formatRate(serverRate.download) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上行速率">
|
||||
<span class="rate-text">↑ {{ formatRate(serverRate.upload) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="在线用户数" :span="2">
|
||||
{{ selectedServer.vpn_status.online_user_info?.length || 0 }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider content-position="left">在线用户</el-divider>
|
||||
|
||||
<el-table :data="sortedUsers" stripe style="width: 100%">
|
||||
<el-table-column prop="user_id" label="用户ID" width="100" />
|
||||
<el-table-column prop="session_id" label="会话ID" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="upload_packets" label="上传包" width="100" />
|
||||
<el-table-column prop="download_packets" label="下载包" width="100" />
|
||||
<el-table-column label="上传字节" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatBytes(row.upload_bytes) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下载字节" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatBytes(row.download_bytes) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上行速率" width="130">
|
||||
<template #default="{ row }">
|
||||
<span class="rate-text">↑ {{ formatRate(getUserRate(row.session_id).upload) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下行速率" width="130">
|
||||
<template #default="{ row }">
|
||||
<span class="rate-text">↓ {{ formatRate(getUserRate(row.session_id).download) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="hover" v-else>
|
||||
<el-empty description="请选择服务器查看状态" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { GetVPNServerConfigHandler, GetVPNServerOnlineListHandler } from '@/api/vpn';
|
||||
import { ServerConfig, VPNStatus, OnlineUserInfo } from '@/types/vpn';
|
||||
|
||||
interface ServerRate {
|
||||
upload: number;
|
||||
download: number;
|
||||
}
|
||||
|
||||
interface UserRateData {
|
||||
[sessionId: string]: {
|
||||
upload: number;
|
||||
download: number;
|
||||
lastUploadBytes: number;
|
||||
lastDownloadBytes: number;
|
||||
lastUpdateTime: number;
|
||||
};
|
||||
}
|
||||
|
||||
const serverList = ref<ServerConfig[]>([]);
|
||||
const selectedServer = ref<ServerConfig | null>(null);
|
||||
const onlineServersData = ref<ServerConfig[]>([]);
|
||||
let timer: number | null = null;
|
||||
|
||||
const serverRate = reactive<ServerRate>({
|
||||
upload: 0,
|
||||
download: 0
|
||||
});
|
||||
|
||||
const userRateData = reactive<UserRateData>({});
|
||||
|
||||
let lastServerData: {
|
||||
receive_bytes: number;
|
||||
send_bytes: number;
|
||||
last_update_time: number;
|
||||
} | null = null;
|
||||
|
||||
const formatBytes = (bytes: number): string => {
|
||||
if (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 formatRate = (bytesPerSecond: number): string => {
|
||||
if (bytesPerSecond === 0) return '0 B/s';
|
||||
const k = 1024;
|
||||
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
|
||||
const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));
|
||||
return parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const getStatusType = (status: number) => {
|
||||
return status === 2 ? 'success' : 'danger';
|
||||
};
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
return status === 2 ? '在线' : '离线';
|
||||
};
|
||||
|
||||
const isServerOnline = (server: ServerConfig): boolean => {
|
||||
const onlineServer = onlineServersData.value.find(s => s.server_id === server.server_id);
|
||||
return onlineServer?.vpn_status?.status === 2;
|
||||
};
|
||||
|
||||
const getServerConfig = 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)) {
|
||||
onlineServersData.value = response.data;
|
||||
|
||||
if (selectedServer.value) {
|
||||
const onlineServer = onlineServersData.value.find(
|
||||
s => s.server_id === selectedServer.value.server_id
|
||||
);
|
||||
if (onlineServer) {
|
||||
calculateServerRate(onlineServer.vpn_status);
|
||||
calculateUserRates(onlineServer.vpn_status?.online_user_info || []);
|
||||
selectedServer.value.vpn_status = onlineServer.vpn_status;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取在线服务器状态失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const calculateServerRate = (vpnStatus: VPNStatus | undefined) => {
|
||||
if (!vpnStatus) return;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (lastServerData) {
|
||||
const timeDiff = (now - lastServerData.last_update_time) / 1000;
|
||||
if (timeDiff > 0) {
|
||||
const downloadDiff = vpnStatus.receive_bytes - lastServerData.receive_bytes;
|
||||
const uploadDiff = vpnStatus.send_bytes - lastServerData.send_bytes;
|
||||
|
||||
serverRate.download = Math.max(0, downloadDiff / timeDiff);
|
||||
serverRate.upload = Math.max(0, uploadDiff / timeDiff);
|
||||
}
|
||||
}
|
||||
|
||||
lastServerData = {
|
||||
receive_bytes: vpnStatus.receive_bytes,
|
||||
send_bytes: vpnStatus.send_bytes,
|
||||
last_update_time: now
|
||||
};
|
||||
};
|
||||
|
||||
const calculateUserRates = (users: OnlineUserInfo[]) => {
|
||||
const now = Date.now();
|
||||
const currentSessionIds = new Set<string>();
|
||||
|
||||
users.forEach(user => {
|
||||
currentSessionIds.add(user.session_id);
|
||||
|
||||
if (userRateData[user.session_id]) {
|
||||
const timeDiff = (now - userRateData[user.session_id].lastUpdateTime) / 1000;
|
||||
if (timeDiff > 0) {
|
||||
const uploadDiff = user.upload_bytes - userRateData[user.session_id].lastUploadBytes;
|
||||
const downloadDiff = user.download_bytes - userRateData[user.session_id].lastDownloadBytes;
|
||||
|
||||
userRateData[user.session_id].upload = Math.max(0, uploadDiff / timeDiff);
|
||||
userRateData[user.session_id].download = Math.max(0, downloadDiff / timeDiff);
|
||||
}
|
||||
userRateData[user.session_id].lastUploadBytes = user.upload_bytes;
|
||||
userRateData[user.session_id].lastDownloadBytes = user.download_bytes;
|
||||
userRateData[user.session_id].lastUpdateTime = now;
|
||||
} else {
|
||||
userRateData[user.session_id] = {
|
||||
upload: 0,
|
||||
download: 0,
|
||||
lastUploadBytes: user.upload_bytes,
|
||||
lastDownloadBytes: user.download_bytes,
|
||||
lastUpdateTime: now
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(userRateData).forEach(sessionId => {
|
||||
if (!currentSessionIds.has(sessionId)) {
|
||||
delete userRateData[sessionId];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getUserRate = (sessionId: string) => {
|
||||
return userRateData[sessionId] || { upload: 0, download: 0 };
|
||||
};
|
||||
|
||||
const sortedUsers = computed(() => {
|
||||
if (!selectedServer.value?.vpn_status?.online_user_info) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [...selectedServer.value.vpn_status.online_user_info].sort((a, b) => {
|
||||
const rateA = getUserRate(a.session_id);
|
||||
const rateB = getUserRate(b.session_id);
|
||||
const totalA = rateA.upload + rateA.download;
|
||||
const totalB = rateB.upload + rateB.download;
|
||||
return totalB - totalA;
|
||||
});
|
||||
});
|
||||
|
||||
const selectServer = (server: ServerConfig) => {
|
||||
selectedServer.value = server;
|
||||
lastServerData = null;
|
||||
serverRate.upload = 0;
|
||||
serverRate.download = 0;
|
||||
|
||||
const onlineServer = onlineServersData.value.find(s => s.server_id === server.server_id);
|
||||
if (onlineServer?.vpn_status) {
|
||||
selectedServer.value.vpn_status = onlineServer.vpn_status;
|
||||
calculateServerRate(onlineServer.vpn_status);
|
||||
calculateUserRates(onlineServer.vpn_status.online_user_info || []);
|
||||
}
|
||||
};
|
||||
|
||||
const startTimer = () => {
|
||||
getOnlineServers();
|
||||
timer = window.setInterval(() => {
|
||||
getOnlineServers();
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const stopTimer = () => {
|
||||
if (timer !== null) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await getServerConfig();
|
||||
startTimer();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vpn-server-status {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.server-list {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.server-item {
|
||||
padding: 12px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.server-item:hover {
|
||||
border-color: #409eff;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.server-item.active {
|
||||
border-color: #409eff;
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
.server-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.server-name {
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.server-ip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #909399;
|
||||
}
|
||||
|
||||
.status-indicator.online {
|
||||
background-color: #67c23a;
|
||||
box-shadow: 0 0 4px #67c23a;
|
||||
}
|
||||
|
||||
.rate-text {
|
||||
font-weight: 500;
|
||||
color: #409eff;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue