mirror of
https://github.com/weiaiweiai/NezhaAgentHTTPBridge.git
synced 2026-05-13 21:49:09 +08:00
Add files via upload
This commit is contained in:
531
UI.html
Normal file
531
UI.html
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
<div id="wp-co-nd-host"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
/* 核心配置项 */
|
||||||
|
const CFG = {
|
||||||
|
api: 'https://127.0.0.1/WeatherForecast/lastws',//请求数据地址
|
||||||
|
poll: 3000,
|
||||||
|
offlineSec: 30,
|
||||||
|
debug: true
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 元素ID定义 */
|
||||||
|
const IDS = {
|
||||||
|
container: 'wp-co-nd-host',
|
||||||
|
wrapper: 'wp-co-nd-wrapper',
|
||||||
|
grid: 'wp-co-nd-grid',
|
||||||
|
status: 'wp-co-nd-status',
|
||||||
|
error: 'wp-co-nd-error',
|
||||||
|
warning: 'wp-co-nd-warning'
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 状态管理变量 */
|
||||||
|
let servers = [];
|
||||||
|
let isLoading = false;
|
||||||
|
|
||||||
|
/* ===================== 工具函数 ===================== */
|
||||||
|
function getSafe(obj, path, defaultValue = null, logMissing = true) {
|
||||||
|
const result = path.split('.').reduce((acc, part) => {
|
||||||
|
if (acc === null || acc === undefined) return defaultValue;
|
||||||
|
return acc[part] !== undefined ? acc[part] : defaultValue;
|
||||||
|
}, obj);
|
||||||
|
if (result === defaultValue && logMissing && CFG.debug) {
|
||||||
|
console.log(`[服务器监控] 缺失字段: ${path}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPercent(used, total) {
|
||||||
|
if (total <= 0) return "0.0%";
|
||||||
|
const pct = Math.min(100, Math.max(0, (used / total) * 100));
|
||||||
|
return pct.toFixed(1) + "%";
|
||||||
|
}
|
||||||
|
function formatBytesToGB(bytes) {
|
||||||
|
if (bytes === null || isNaN(bytes)) return "未知";
|
||||||
|
const gb = bytes / (1024 * 1024 * 1024);
|
||||||
|
return gb >= 1 ? `${gb.toFixed(2)}GB` : `${(gb * 1024).toFixed(1)}MB`;
|
||||||
|
}
|
||||||
|
function formatSpeed(bytes) {
|
||||||
|
if (bytes === null || isNaN(bytes)) return "未知";
|
||||||
|
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)}MB/s`;
|
||||||
|
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)}KB/s`;
|
||||||
|
return `${bytes}B/s`;
|
||||||
|
}
|
||||||
|
function formatTraffic(bytes) {
|
||||||
|
if (bytes === null || isNaN(bytes)) return "未知";
|
||||||
|
const gb = bytes / (1024 * 1024 * 1024);
|
||||||
|
return gb >= 1 ? `${gb.toFixed(2)}GB` : `${(gb * 1024).toFixed(1)}MB`;
|
||||||
|
}
|
||||||
|
function formatUptime(seconds) {
|
||||||
|
if (seconds === null || isNaN(seconds)) return "未知";
|
||||||
|
if (seconds < 60) return `${seconds}秒`;
|
||||||
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`;
|
||||||
|
if (seconds < 86400) return `${(seconds / 3600).toFixed(1)}小时`;
|
||||||
|
return `${(seconds / 86400).toFixed(1)}天`;
|
||||||
|
}
|
||||||
|
function formatCountryCode(code) {
|
||||||
|
if (!code) return "未知地区";
|
||||||
|
const regionMap = { 'hk': '中国香港', 'cn': '中国大陆' };
|
||||||
|
return regionMap[code] || code;
|
||||||
|
}
|
||||||
|
function formatCpuInfo(cpuArr) {
|
||||||
|
return cpuArr && cpuArr.length > 0 ? cpuArr.join(' | ') : "未知CPU信息";
|
||||||
|
}
|
||||||
|
function getStatusColor(cpu) {
|
||||||
|
if (cpu === null || isNaN(cpu)) return "#a0aec0";
|
||||||
|
if (cpu > 85) return "#e53e3e";
|
||||||
|
if (cpu > 70) return "#ed8936";
|
||||||
|
if (cpu > 40) return "#ecc94b";
|
||||||
|
return "#48bb78";
|
||||||
|
}
|
||||||
|
function isServerOnline(server) {
|
||||||
|
const lastActive = getSafe(server, 'last_active');
|
||||||
|
if (!lastActive) return false;
|
||||||
|
const lastActiveTime = new Date(lastActive).getTime();
|
||||||
|
return (Date.now() - lastActiveTime) < CFG.offlineSec * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== 样式注入 ===================== */
|
||||||
|
function injectStyles() {
|
||||||
|
const oldStyle = document.getElementById('wp-co-nd-styles');
|
||||||
|
if (oldStyle) oldStyle.remove();
|
||||||
|
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'wp-co-nd-styles';
|
||||||
|
style.textContent = `
|
||||||
|
#${IDS.wrapper} {
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
padding: 15px !important;
|
||||||
|
font-family: "Microsoft YaHei", Arial, sans-serif !important;
|
||||||
|
max-width: 1400px !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#${IDS.grid} {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) !important;
|
||||||
|
gap: 18px !important;
|
||||||
|
margin-top: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-server-card {
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
padding: 18px !important;
|
||||||
|
box-shadow: 0 3px 8px rgba(0,0,0,0.06) !important;
|
||||||
|
transition: box-shadow 0.2s ease !important;
|
||||||
|
}
|
||||||
|
.wp-co-server-card:hover {
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.08) !important;
|
||||||
|
}
|
||||||
|
.wp-co-server-card.offline {
|
||||||
|
opacity: 0.72 !important;
|
||||||
|
filter: grayscale(0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-status-indicator {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 12px !important;
|
||||||
|
height: 12px !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
margin-right: 8px !important;
|
||||||
|
vertical-align: middle !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-server-header {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
align-items: center !important;
|
||||||
|
margin-bottom: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-server-name {
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-server-region {
|
||||||
|
font-size: 12px !important;
|
||||||
|
padding: 3px 8px !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-cpu-container {
|
||||||
|
margin-bottom: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-cpu-label {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
margin-bottom: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-cpu-bar-wrap {
|
||||||
|
height: 6px !important;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-cpu-bar {
|
||||||
|
height: 100% !important;
|
||||||
|
transition: width 1s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-section-title {
|
||||||
|
font-size: 14px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
margin: 12px 0 6px 0 !important;
|
||||||
|
padding-left: 4px !important;
|
||||||
|
border-left: 2px solid #4299e1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-info-grid {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: 1fr 1fr !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-info-item {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-info-label {
|
||||||
|
font-size: 12px !important;
|
||||||
|
margin-bottom: 2px !important;
|
||||||
|
opacity: 0.8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-footer-stats {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
margin-top: 12px !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#${IDS.status} {
|
||||||
|
font-size: 13px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#${IDS.error}, #${IDS.warning} {
|
||||||
|
margin-top: 15px !important;
|
||||||
|
padding: 10px 12px !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
display: none !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#${IDS.error} {
|
||||||
|
border: 1px solid #ffe3e3 !important;
|
||||||
|
}
|
||||||
|
#${IDS.warning} {
|
||||||
|
border: 1px solid #ffeaa7 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-header {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
align-items: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-page-title {
|
||||||
|
font-size: 18px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-co-unknown-data {
|
||||||
|
font-style: italic !important;
|
||||||
|
opacity: 0.7 !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== 结构创建 ===================== */
|
||||||
|
function createBaseStructure() {
|
||||||
|
const container = document.getElementById(IDS.container);
|
||||||
|
if (!container) {
|
||||||
|
console.error("[服务器监控] 容器元素不存在");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div id="${IDS.wrapper}">
|
||||||
|
<div class="wp-co-header">
|
||||||
|
<div class="wp-co-page-title">
|
||||||
|
<i class="fa fa-server"></i>
|
||||||
|
服务器状态监控
|
||||||
|
</div>
|
||||||
|
<div id="${IDS.status}">
|
||||||
|
<i class="fa fa-refresh fa-spin"></i>
|
||||||
|
加载中...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="${IDS.grid}">
|
||||||
|
<div style="grid-column: 1 / -1; text-align: center; padding: 40px 0; font-size: 14px;">
|
||||||
|
<i class="fa fa-spinner fa-spin" style="font-size: 20px; margin-bottom: 8px;"></i>
|
||||||
|
<p>正在获取服务器数据...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="${IDS.error}">
|
||||||
|
<i class="fa fa-exclamation-circle"></i>
|
||||||
|
<span>获取数据失败,请稍后重试</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="${IDS.warning}">
|
||||||
|
<i class="fa fa-exclamation-triangle"></i>
|
||||||
|
<span>部分数据加载异常,详情请查看控制台</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== 提示控制函数 ===================== */
|
||||||
|
function showError(show) {
|
||||||
|
const errorEl = document.getElementById(IDS.error);
|
||||||
|
if (errorEl) errorEl.style.display = show ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
function showWarning(show) {
|
||||||
|
const warningEl = document.getElementById(IDS.warning);
|
||||||
|
if (warningEl) warningEl.style.display = show ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== 数据渲染 ===================== */
|
||||||
|
function renderServers() {
|
||||||
|
const grid = document.getElementById(IDS.grid);
|
||||||
|
const statusEl = document.getElementById(IDS.status);
|
||||||
|
if (!grid || !statusEl) return;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
statusEl.innerHTML = `
|
||||||
|
<i class="fa fa-clock-o"></i>
|
||||||
|
最后更新: ${now.toLocaleTimeString()}
|
||||||
|
`;
|
||||||
|
|
||||||
|
let hasMissingData = false;
|
||||||
|
|
||||||
|
if (servers.length === 0) {
|
||||||
|
showError(true);
|
||||||
|
showWarning(false);
|
||||||
|
grid.innerHTML = `
|
||||||
|
<div style="grid-column: 1 / -1; text-align: center; padding: 40px 0; font-size: 14px;">
|
||||||
|
<i class="fa fa-folder-open-o" style="font-size: 20px; margin-bottom: 8px;"></i>
|
||||||
|
<p>暂无服务器数据</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
showError(false);
|
||||||
|
if (CFG.debug) console.log(`[服务器监控] 渲染 ${servers.length} 台服务器数据`);
|
||||||
|
|
||||||
|
grid.innerHTML = servers.map((server, index) => {
|
||||||
|
const serverName = getSafe(server, 'name', `未命名服务器 #${index + 1}`);
|
||||||
|
if (CFG.debug) console.log(`[服务器监控] 处理服务器: ${serverName}`);
|
||||||
|
|
||||||
|
const online = isServerOnline(server);
|
||||||
|
const cpuUsed = getSafe(server, 'state.cpu', 0);
|
||||||
|
const cpuUsage = parseFloat(formatPercent(cpuUsed, 100));
|
||||||
|
const memUsed = getSafe(server, 'state.mem_used', 0);
|
||||||
|
const memTotal = getSafe(server, 'host.mem_total', 0);
|
||||||
|
const memUsage = formatPercent(memUsed, memTotal);
|
||||||
|
const diskUsed = getSafe(server, 'state.disk_used', 0);
|
||||||
|
const diskTotalVal = getSafe(server, 'host.disk_total', 0);
|
||||||
|
const diskUsage = formatPercent(diskUsed, diskTotalVal);
|
||||||
|
const statusColor = getStatusColor(cpuUsage);
|
||||||
|
|
||||||
|
const os = getSafe(server, 'host.platform', "未知系统");
|
||||||
|
const cpu = formatCpuInfo(getSafe(server, 'host.cpu', []));
|
||||||
|
const arch = getSafe(server, 'host.arch', "未知架构");
|
||||||
|
const region = formatCountryCode(getSafe(server, 'country_code'));
|
||||||
|
const uptime = formatUptime(getSafe(server, 'state.uptime'));
|
||||||
|
|
||||||
|
const netIn = formatSpeed(getSafe(server, 'state.net_in_speed'));
|
||||||
|
const netOut = formatSpeed(getSafe(server, 'state.net_out_speed'));
|
||||||
|
const trafficIn = formatTraffic(getSafe(server, 'state.net_in_transfer'));
|
||||||
|
const trafficOut = formatTraffic(getSafe(server, 'state.net_out_transfer'));
|
||||||
|
const tcpConn = getSafe(server, 'state.tcp_conn_count', "未知");
|
||||||
|
const processCount = getSafe(server, 'state.process_count', "未知");
|
||||||
|
|
||||||
|
const serverHasMissingData = [
|
||||||
|
os === "未知系统",
|
||||||
|
cpu === "未知CPU信息",
|
||||||
|
arch === "未知架构",
|
||||||
|
tcpConn === "未知",
|
||||||
|
uptime === "未知",
|
||||||
|
processCount === "未知"
|
||||||
|
].some(Boolean);
|
||||||
|
|
||||||
|
if (serverHasMissingData) {
|
||||||
|
hasMissingData = true;
|
||||||
|
if (CFG.debug) console.log(`[服务器监控] 服务器 "${serverName}" 存在缺失数据`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="wp-co-server-card ${online ? '' : 'offline'}">
|
||||||
|
<div class="wp-co-server-header">
|
||||||
|
<div class="wp-co-server-name">
|
||||||
|
<span class="wp-co-status-indicator" style="background-color: ${statusColor};"></span>
|
||||||
|
${serverName}
|
||||||
|
</div>
|
||||||
|
<div class="wp-co-server-region">${region}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-co-cpu-container">
|
||||||
|
<div class="wp-co-cpu-label">
|
||||||
|
<span>CPU 使用率</span>
|
||||||
|
<span style="font-weight: 500;">${cpuUsage.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="wp-co-cpu-bar-wrap">
|
||||||
|
<div class="wp-co-cpu-bar" style="width: ${cpuUsage}%; background-color: ${statusColor};"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-co-section-title">系统配置</div>
|
||||||
|
<div class="wp-co-info-grid">
|
||||||
|
<div class="wp-co-info-item">
|
||||||
|
<div class="wp-co-info-label">操作系统</div>
|
||||||
|
<div>${os === "未知系统" ? `<span class="wp-co-unknown-data">${os}</span>` : os}</div>
|
||||||
|
</div>
|
||||||
|
<div class="wp-co-info-item">
|
||||||
|
<div class="wp-co-info-label">CPU信息</div>
|
||||||
|
<div>${cpu === "未知CPU信息" ? `<span class="wp-co-unknown-data">${cpu}</span>` : cpu}</div>
|
||||||
|
</div>
|
||||||
|
<div class="wp-co-info-item">
|
||||||
|
<div class="wp-co-info-label">架构</div>
|
||||||
|
<div>${arch === "未知架构" ? `<span class="wp-co-unknown-data">${arch}</span>` : arch}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-co-section-title">实时性能</div>
|
||||||
|
<div class="wp-co-info-grid">
|
||||||
|
<div class="wp-co-info-item">
|
||||||
|
<div class="wp-co-info-label">内存使用率</div>
|
||||||
|
<div>${memUsage}</div>
|
||||||
|
</div>
|
||||||
|
<div class="wp-co-info-item">
|
||||||
|
<div class="wp-co-info-label">磁盘使用率</div>
|
||||||
|
<div>${diskUsage}</div>
|
||||||
|
</div>
|
||||||
|
<div class="wp-co-info-item">
|
||||||
|
<div class="wp-co-info-label">网络上传</div>
|
||||||
|
<div>${netOut} (${trafficOut})</div>
|
||||||
|
</div>
|
||||||
|
<div class="wp-co-info-item">
|
||||||
|
<div class="wp-co-info-label">网络下载</div>
|
||||||
|
<div>${netIn} (${trafficIn})</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wp-co-footer-stats">
|
||||||
|
<div>TCP连接: ${tcpConn === "未知" ? `<span class="wp-co-unknown-data">${tcpConn}</span>` : tcpConn}</div>
|
||||||
|
<div>进程数: ${processCount === "未知" ? `<span class="wp-co-unknown-data">${processCount}</span>` : processCount} | 运行: ${uptime === "未知" ? `<span class="wp-co-unknown-data">${uptime}</span>` : uptime}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
showWarning(hasMissingData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== 数据请求 ===================== */
|
||||||
|
async function fetchServers() {
|
||||||
|
if (isLoading) return;
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
|
const statusEl = document.getElementById(IDS.status);
|
||||||
|
let hadError = false;
|
||||||
|
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.innerHTML = `
|
||||||
|
<i class="fa fa-refresh fa-spin"></i>
|
||||||
|
加载中...
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CFG.debug) console.log(`[服务器监控] 开始请求数据: ${CFG.api}`);
|
||||||
|
showError(false);
|
||||||
|
showWarning(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(CFG.api, { cache: 'no-store' });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
hadError = true;
|
||||||
|
console.error(`[服务器监控] 请求失败: HTTP状态码 ${response.status}`);
|
||||||
|
if (servers.length === 0) throw new Error(`HTTP状态错误: ${response.status}`);
|
||||||
|
} else {
|
||||||
|
if (CFG.debug) console.log(`[服务器监控] 请求成功: ${response.status}`);
|
||||||
|
const rawData = await response.text();
|
||||||
|
let parsedData = null;
|
||||||
|
try {
|
||||||
|
parsedData = JSON.parse(rawData);
|
||||||
|
if (CFG.debug) console.log(`[服务器监控] 数据解析成功`);
|
||||||
|
} catch (e) {
|
||||||
|
hadError = true;
|
||||||
|
console.error(`[服务器监控] 数据解析失败:`, e);
|
||||||
|
if (servers.length === 0) throw new Error("数据解析失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedData) {
|
||||||
|
let newServers = [];
|
||||||
|
if (parsedData?.state && parsedData?.host) {
|
||||||
|
newServers = [parsedData];
|
||||||
|
} else if (parsedData?.servers) {
|
||||||
|
newServers = parsedData.servers;
|
||||||
|
} else if (parsedData?.list || parsedData?.data) {
|
||||||
|
newServers = parsedData.list || parsedData.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newServers.length > 0) {
|
||||||
|
servers = newServers;
|
||||||
|
hadError = false;
|
||||||
|
if (CFG.debug) console.log(`[服务器监控] 获取到 ${newServers.length} 台服务器数据`);
|
||||||
|
} else {
|
||||||
|
hadError = true;
|
||||||
|
console.log(`[服务器监控] 未获取到有效服务器数据`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[服务器监控] 数据请求错误:`, error);
|
||||||
|
hadError = true;
|
||||||
|
if (servers.length === 0) servers = [];
|
||||||
|
} finally {
|
||||||
|
renderServers();
|
||||||
|
isLoading = false;
|
||||||
|
if (CFG.debug) console.log(`[服务器监控] 数据处理完成`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================== 初始化 ===================== */
|
||||||
|
function init() {
|
||||||
|
injectStyles();
|
||||||
|
if (!createBaseStructure()) {
|
||||||
|
setTimeout(init, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetchServers();
|
||||||
|
setInterval(fetchServers, CFG.poll);
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
30
WebApplication4.sln
Normal file
30
WebApplication4.sln
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.14.36414.22
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication4", "WebApplication4\WebApplication4.csproj", "{4414834D-B663-48A6-B4ED-97E81AD8D3FF}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "解决方案项", "解决方案项", "{9FA3D6BD-1EC1-3BA5-80CB-CE02773A58D5}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
UI.html = UI.html
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{4414834D-B663-48A6-B4ED-97E81AD8D3FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4414834D-B663-48A6-B4ED-97E81AD8D3FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4414834D-B663-48A6-B4ED-97E81AD8D3FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4414834D-B663-48A6-B4ED-97E81AD8D3FF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {E0942CCF-C02D-48D4-8861-7C9F9814D520}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
43
WebApplication4/Controllers/WeatherForecastController.cs
Normal file
43
WebApplication4/Controllers/WeatherForecastController.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using WebApplication4.Services;
|
||||||
|
|
||||||
|
namespace WebApplication4.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("[controller]")]
|
||||||
|
public class WeatherForecastController : ControllerBase
|
||||||
|
{
|
||||||
|
|
||||||
|
private readonly ILogger<WeatherForecastController> _logger;
|
||||||
|
private readonly WebSocketMessageStore _wsStore;
|
||||||
|
|
||||||
|
public WeatherForecastController(
|
||||||
|
ILogger<WeatherForecastController> logger,
|
||||||
|
WebSocketMessageStore wsStore)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_wsStore = wsStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB> WebSocket <20><>Ϣ
|
||||||
|
[HttpGet("lastws")]
|
||||||
|
public IActionResult GetLastWebSocketMessage()
|
||||||
|
{
|
||||||
|
var (message, ts) = _wsStore.Get();
|
||||||
|
if (message == null)
|
||||||
|
{
|
||||||
|
return NotFound(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
message = "<22><>δ<EFBFBD>յ<EFBFBD><D5B5>κ<EFBFBD> WebSocket <20><>Ϣ<EFBFBD><CFA2>"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
WebApplication4/Program.cs
Normal file
34
WebApplication4/Program.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using WebApplication4.Services;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
// ȫ<><C8AB><EFBFBD>ſ<EFBFBD> CORS<52><53><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ս<EFBFBD><D5BD><EFBFBD>
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowAll", policy =>
|
||||||
|
policy.AllowAnyOrigin()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod());
|
||||||
|
});
|
||||||
|
|
||||||
|
// WebSocket <20><><EFBFBD><EFBFBD>
|
||||||
|
builder.Services.AddSingleton<WebSocketMessageStore>();
|
||||||
|
builder.Services.AddHostedService<WebSocketClientBackgroundService>();
|
||||||
|
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
// builder.Services.AddSwaggerGen(); // <20><><EFBFBD><EFBFBD><EFBFBD>ٴ<EFBFBD><D9B4><EFBFBD>
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// <20><>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD> MapControllers ֮ǰ
|
||||||
|
app.UseCors("AllowAll");
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD>м<EFBFBD><D0BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD>ɷ<EFBFBD><C9B7>ڴ˴<DAB4><CBB4><EFBFBD>
|
||||||
|
// app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
31
WebApplication4/Properties/launchSettings.json
Normal file
31
WebApplication4/Properties/launchSettings.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:28450",
|
||||||
|
"sslPort": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "weatherforecast",
|
||||||
|
"applicationUrl": "http://localhost:5020",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "weatherforecast",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
WebApplication4/Services/WebSocketClientBackgroundService.cs
Normal file
109
WebApplication4/Services/WebSocketClientBackgroundService.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace WebApplication4.Services
|
||||||
|
{
|
||||||
|
public class WebSocketClientBackgroundService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly ILogger<WebSocketClientBackgroundService> _logger;
|
||||||
|
private readonly WebSocketMessageStore _store;
|
||||||
|
// Ŀ<><C4BF> WebSocket <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ
|
||||||
|
private readonly Uri _uri = new("ws://127.0.0.1:8008/api/v1/ws/server");
|
||||||
|
|
||||||
|
// <20>ɵ<EFBFBD><C9B5><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
private readonly TimeSpan _reconnectDelay = TimeSpan.FromSeconds(5);
|
||||||
|
private const int ReceiveBufferSize = 8 * 1024;
|
||||||
|
|
||||||
|
public WebSocketClientBackgroundService(
|
||||||
|
ILogger<WebSocketClientBackgroundService> logger,
|
||||||
|
WebSocketMessageStore store)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("WebSocket <20><>̨<EFBFBD><CCA8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, Ŀ<><C4BF>: {Url}", _uri);
|
||||||
|
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
using var client = new ClientWebSocket();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
client.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
|
||||||
|
_logger.LogInformation("<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> WebSocket...");
|
||||||
|
await client.ConnectAsync(_uri, stoppingToken);
|
||||||
|
_logger.LogInformation("WebSocket <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {State}", client.State);
|
||||||
|
|
||||||
|
await ReceiveLoopAsync(client, stoppingToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("ֹͣ<CDA3><D6B9><EFBFBD><EFBFBD><EFBFBD>յ<EFBFBD><D5B5><EFBFBD><EFBFBD><EFBFBD>ֹ WebSocket ѭ<><D1AD><EFBFBD><EFBFBD>");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "WebSocket <20><><EFBFBD>ӻ<EFBFBD><D3BB><EFBFBD><EFBFBD>շ<EFBFBD><D5B7><EFBFBD><EFBFBD>쳣<EFBFBD><ECB3A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD> {Delay}s <20><><EFBFBD><EFBFBD><EFBFBD>ԡ<EFBFBD>",
|
||||||
|
_reconnectDelay.TotalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(_reconnectDelay, stoppingToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("WebSocket <20><>̨<EFBFBD><CCA8><EFBFBD><EFBFBD><EFBFBD>ѽ<EFBFBD><D1BD><EFBFBD><EFBFBD><EFBFBD>");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReceiveLoopAsync(ClientWebSocket client, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var buffer = new byte[ReceiveBufferSize];
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested && client.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
WebSocketReceiveResult? result;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var segment = new ArraySegment<byte>(buffer);
|
||||||
|
result = await client.ReceiveAsync(segment, ct);
|
||||||
|
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD>: {Desc}", result.CloseStatusDescription);
|
||||||
|
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ms.Write(buffer, 0, result.Count);
|
||||||
|
}
|
||||||
|
while (!result.EndOfMessage);
|
||||||
|
|
||||||
|
if (result.MessageType == WebSocketMessageType.Text)
|
||||||
|
{
|
||||||
|
var text = Encoding.UTF8.GetString(ms.ToArray());
|
||||||
|
_store.Set(text);
|
||||||
|
_logger.LogDebug("<22>յ<EFBFBD><D5B5>ı<EFBFBD><C4B1><EFBFBD>Ϣ<EFBFBD><CFA2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {Len}", text.Length);
|
||||||
|
}
|
||||||
|
else if (result.MessageType == WebSocketMessageType.Binary)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("<22><><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ<EFBFBD><CFA2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {Len}", ms.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
WebApplication4/Services/WebSocketMessageStore.cs
Normal file
30
WebApplication4/Services/WebSocketMessageStore.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace WebApplication4.Services
|
||||||
|
{
|
||||||
|
public class WebSocketMessageStore
|
||||||
|
{
|
||||||
|
private string? _lastMessage;
|
||||||
|
private DateTime? _receivedAtUtc;
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
public void Set(string message)
|
||||||
|
{
|
||||||
|
if (message == null) return;
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_lastMessage = message;
|
||||||
|
_receivedAtUtc = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public (string? Message, DateTime? ReceivedAtUtc) Get()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return (_lastMessage, _receivedAtUtc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
WebApplication4/WeatherForecast.cs
Normal file
13
WebApplication4/WeatherForecast.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace WebApplication4
|
||||||
|
{
|
||||||
|
public class WeatherForecast
|
||||||
|
{
|
||||||
|
public DateOnly Date { get; set; }
|
||||||
|
|
||||||
|
public int TemperatureC { get; set; }
|
||||||
|
|
||||||
|
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||||
|
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
WebApplication4/WebApplication4.csproj
Normal file
9
WebApplication4/WebApplication4.csproj
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
8
WebApplication4/appsettings.Development.json
Normal file
8
WebApplication4/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
WebApplication4/appsettings.json
Normal file
9
WebApplication4/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user