sawAdmin/src/views/system/vpn-server-status.vue

397 lines
14 KiB
Vue

<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();
}, 6000);
};
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>