Files
EasytierGame/pages/nodesList.vue
T

365 lines
9.4 KiB
Vue

<template>
<div class="relative flex h-full flex-col flex-wrap gap-[0] overflow-hidden">
<div class="flex h-[40px] w-full flex-nowrap items-center gap-[0_5px]">
<div class="ml-[5px] mr-auto flex flex-1">
<ElText class="text-lg font-semibold">公共服务器节点列表</ElText>
</div>
<div>
<ElButton
:loading="loading"
size="small"
type="primary"
class="mr-[5px]"
@click="refreshNodes"
>
刷新
</ElButton>
<ElButton
:disabled="selectedNodes.length === 0"
size="small"
type="success"
class="mr-[5px]"
@click="addSelectedToServerList"
>
添加选中 ({{ selectedNodes.length }})
</ElButton>
<ElTag
v-if="lastUpdateTime"
size="small"
type="info"
>
最后更新: {{ lastUpdateTime }}
</ElTag>
</div>
</div>
<div class="flex w-full flex-1 flex-wrap gap-[0] overflow-auto">
<ElTable
:data="nodes"
stripe
height="100%"
ref="tableRef"
:empty-text="loading ? '加载中...' : '暂无可用服务器'"
:default-sort="{ prop: 'last_response_time', order: 'ascending' }"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
>
<ElTableColumn
type="selection"
width="55"
/>
<ElTableColumn
prop="name"
label="服务器名称"
width="200"
show-overflow-tooltip
sortable
/>
<ElTableColumn
prop="protocol"
label="协议"
width="80"
sortable
>
<template #default="{ row }">
<ElTag
:type="row.protocol === 'tcp' ? 'success' : 'warning'"
size="small"
>
{{ row.protocol.toUpperCase() }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn
prop="address"
label="地址"
width="200"
show-overflow-tooltip
sortable
/>
<ElTableColumn
prop="last_response_time"
label="响应时间"
width="100"
sortable
>
<template #default="{ row }">
<ElTag
:type="getResponseTimeType(row.last_response_time)"
size="small"
>
{{ row.last_response_time }}ms
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn
prop="current_health_status"
label="状态/健康率"
width="120"
sortable
>
<template #default="{ row }">
<div class="flex flex-col gap-1">
<ElTag
:type="row.current_health_status === 'healthy' ? 'success' : 'danger'"
size="small"
>
{{ row.current_health_status === "healthy" ? "健康" : "异常" }}
</ElTag>
<ElTag
:type="getHealthType(row.health_percentage_24h)"
size="small"
>
{{ Number(row.health_percentage_24h).toFixed(1) }}%
</ElTag>
</div>
</template>
</ElTableColumn>
<ElTableColumn
prop="usage_percentage"
label="使用率"
width="100"
sortable
>
<template #default="{ row }">
<div class="flex items-center">
<ElProgress
:percentage="row.usage_percentage"
:stroke-width="6"
:show-text="false"
:color="getUsageColor(row.usage_percentage)"
class="mr-2 flex-1"
/>
<span class="text-xs">{{ row.usage_percentage.toFixed(1) }}%</span>
</div>
</template>
</ElTableColumn>
<ElTableColumn
prop="description"
label="描述"
width="300"
sortable
>
<template #default="{ row }">
<div class="whitespace-pre-wrap">{{ row.description }}</div>
</template>
</ElTableColumn>
</ElTable>
</div>
</div>
</template>
<script setup lang="ts">
import useMainStore from "@/stores/index";
import { invoke } from "@tauri-apps/api/core";
import { reactive, onMounted, onBeforeUnmount, ref, useTemplateRef } from "vue";
import { ElMessage, type TableInstance } from "element-plus";
import { dataSubscribe } from "~/composables/windows";
import { uniq } from "lodash-es";
const mainStore = useMainStore();
// 节点数据
const nodes = reactive<NodeItem[]>([]);
const loading = ref(false);
const lastUpdateTime = ref("");
const selectedNodes = ref<NodeItem[]>([]);
const tableRef = useTemplateRef<TableInstance>("tableRef");
// 定时器
let refreshTimer: number | null = null;
// 获取使用率颜色
const getUsageColor = (percentage: number) => {
if (percentage < 50) return "#67c23a";
if (percentage < 80) return "#e6a23c";
return "#f56c6c";
};
// 获取响应时间类型
const getResponseTimeType = (responseTime: number) => {
if (responseTime < 1000) return "success";
if (responseTime < 5000) return "warning";
return "danger";
};
// 获取健康率类型
const getHealthType = (health: number) => {
if (health >= 95) return "success";
if (health >= 90) return "warning";
return "danger";
};
// 刷新节点数据
const refreshNodes = async () => {
loading.value = true;
try {
let nodesList = await invoke<string>("fetch_nodes_list", { page: 1, limit: 1000 });
if (nodesList === "_EasytierGameFetchNodesError_") {
nodesList = "";
}
if (nodesList) {
const nodesListData: NodesResponse = JSON.parse(nodesList);
if (nodesListData.success && nodesListData.data.items) {
// 过滤可用节点
const availableNodes = filterAvailableNodes(nodesListData.data.items);
// 按优先级排序
const sortedNodes = sortNodesByPriority(availableNodes);
// 更新节点数据
nodes.splice(0, nodes.length, ...sortedNodes);
// 更新最后更新时间
lastUpdateTime.value = new Date().toLocaleTimeString();
// console.log(`已更新 ${sortedNodes.length} 个可用节点`);
}
}
} catch (err) {
console.error("获取节点数据失败:", err);
ElMessage.error("获取节点数据失败");
} finally {
loading.value = false;
}
};
// 过滤可用节点的函数
const filterAvailableNodes = (nodes: NodeItem[]): NodeItem[] => {
return nodes.filter(node => {
// 基本条件:节点必须激活且被批准
if (!node.is_active || !node.is_approved) {
return false;
}
// 健康状态检查
if (node.current_health_status !== "healthy") {
return false;
}
// 使用率检查:使用率不能超过95%
if (node.usage_percentage > 95) {
return false;
}
// 24小时健康率检查:必须大于90%
if (node.health_percentage_24h < 90) {
return false;
}
// 响应时间检查:响应时间不能超过60秒
if (node.last_response_time > 60000) {
return false;
}
// 连接数检查:当前连接数不能超过最大连接数的90%
if (node.current_connections > node.max_connections * 0.9) {
return false;
}
// 必须有有效的地址
if (!node.address || !node.host || !node.port) {
return false;
}
return true;
});
};
// 按优先级排序节点的函数
const sortNodesByPriority = (nodes: NodeItem[]): NodeItem[] => {
return nodes.sort((a, b) => {
// 优先级排序规则:
// 1. 响应时间越短越好
// 2. 使用率越低越好
// 3. 24小时健康率越高越好
// 4. 当前连接数越少越好
// 响应时间权重最高
const responseTimeDiff = a.last_response_time - b.last_response_time;
if (Math.abs(responseTimeDiff) > 1000) {
// 如果响应时间差异超过1秒
return responseTimeDiff;
}
// 使用率权重次之
const usageDiff = a.usage_percentage - b.usage_percentage;
if (Math.abs(usageDiff) > 5) {
// 如果使用率差异超过5%
return usageDiff;
}
// 24小时健康率
const healthDiff = b.health_percentage_24h - a.health_percentage_24h;
if (Math.abs(healthDiff) > 1) {
// 如果健康率差异超过1%
return healthDiff;
}
// 当前连接数
return a.current_connections - b.current_connections;
});
};
// 启动定时器
const startRefreshTimer = () => {
refreshTimer = setInterval(() => {
refreshNodes();
}, 10000); // 10秒刷新一次
};
// 停止定时器
const stopRefreshTimer = () => {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
};
// 处理选择变化
const handleSelectionChange = (selection: NodeItem[]) => {
selectedNodes.value = selection;
};
const handleRowClick = (row: NodeItem) => {
tableRef.value?.toggleRowSelection(row);
};
// 添加选中的服务器到主页面服务器列表
const addSelectedToServerList = async () => {
if (selectedNodes.value.length === 0) {
ElMessage.warning("请先选择要添加的服务器");
return;
}
try {
// 提取服务器地址(去掉协议前缀)
const protocols = selectedNodes.value.map(node => node.protocol.toLowerCase());
const serverAddresses = selectedNodes.value.map(node => node.address.replace(`${node.protocol}://`, ""));
mainStore.basePeers = uniq([...mainStore.basePeers, ...serverAddresses]);
mainStore.config.serverUrl = uniq([...serverAddresses, ...mainStore.config.serverUrl]);
mainStore.config.protocol = uniq([...protocols, ...mainStore.config.protocol]);
ElMessage.success(`${selectedNodes.value.length} 个服务器,添加成功`);
// 清空选择
tableRef.value?.clearSelection();
selectedNodes.value = [];
} catch (err) {
console.error("处理选中服务器失败:", err);
ElMessage.error("处理选中服务器失败");
}
};
onMounted(async () => {
// 初始加载数据
await refreshNodes();
// 启动定时器
startRefreshTimer();
});
onBeforeUnmount(() => {
// 清理定时器
stopRefreshTimer();
});
dataSubscribe();
</script>