397 lines
14 KiB
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>
|