Files
PromdataPanel/public/index.html
2026-04-11 17:41:44 +08:00

822 lines
43 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="LDNET-GA">
<title></title>
<link rel="icon" id="siteFavicon" href="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">
<link rel="stylesheet" href="/css/style.css">
<script src="/vendor/echarts.min.js"></script>
<script>
// Prevent theme flicker
(function () {
const savedTheme = localStorage.getItem('theme');
const settings = window.SITE_SETTINGS || {};
const sanitizeAssetUrl = (url) => {
if (!url || typeof url !== 'string') return null;
const trimmed = url.trim();
if (!trimmed) return null;
return /^(https?:|data:image\/|\/)/i.test(trimmed) ? trimmed : null;
};
const defaultTheme = settings.default_theme || 'dark';
let theme = savedTheme || defaultTheme;
if (theme === 'auto') {
theme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
if (theme === 'light') {
document.documentElement.classList.add('light-theme');
}
// Also apply title and favicon if available to prevent flicker
if (settings.page_name) {
document.title = settings.page_name;
}
const safeFaviconUrl = sanitizeAssetUrl(settings.favicon_url);
if (safeFaviconUrl) {
const link = document.getElementById('siteFavicon');
if (link) link.href = safeFaviconUrl;
}
// Advanced Anti-Flicker: Wait for header elements to appear
const observer = new MutationObserver(function(mutations, me) {
const logoText = document.getElementById('logoText');
const logoIcon = document.getElementById('logoIconContainer');
const header = document.getElementById('header');
if (logoText || logoIcon) {
// If we found either, apply what we have
if (logoText) {
const displayTitle = settings.title || settings.page_name || '数据可视化展示大屏';
logoText.textContent = displayTitle;
if (settings.show_page_name === 0) logoText.style.display = 'none';
}
if (logoIcon) {
const actualTheme = document.documentElement.classList.contains('light-theme') ? 'light' : 'dark';
const logoToUse = sanitizeAssetUrl((actualTheme === 'dark' && settings.logo_url_dark) ? settings.logo_url_dark : (settings.logo_url || null));
if (logoToUse) {
const img = document.createElement('img');
img.src = logoToUse;
img.alt = 'Logo';
img.className = 'logo-icon-img';
logoIcon.replaceChildren(img);
} else {
// Only if we REALLY have no logo URL, we show the default SVG fallback
// (But since it's already in HTML, we just don't touch it or we show it if we hid it)
const svg = logoIcon.querySelector('svg');
if (svg) svg.style.visibility = 'visible';
}
}
// Once found everything or we are past header, we are done
if (logoText && logoIcon) me.disconnect();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
})();
</script>
<script>
// Global Error Logger for remote debugging
window.onerror = function(msg, url, line, col, error) {
var debugDiv = document.getElementById('js-debug-overlay');
if (!debugDiv) {
debugDiv = document.createElement('div');
debugDiv.id = 'js-debug-overlay';
debugDiv.style.cssText = 'position:fixed;top:0;left:0;width:100%;background:rgba(220,38,38,0.95);color:white;z-index:99999;padding:10px;font-family:monospace;font-size:12px;max-height:30vh;overflow:auto;pointer-events:none;';
document.body.appendChild(debugDiv);
}
debugDiv.innerHTML += '<div>[JS ERROR] ' + msg + ' at ' + line + ':' + col + '</div>';
return false;
};
</script>
</head>
<body>
<!-- Animated Background -->
<div class="bg-grid"></div>
<div class="bg-glow bg-glow-1"></div>
<div class="bg-glow bg-glow-2"></div>
<div class="bg-glow bg-glow-3"></div>
<!-- App Container -->
<div id="app">
<!-- Header -->
<header class="header" id="header">
<div class="header-left">
<div class="logo">
<div id="logoIconContainer">
<svg class="logo-icon" id="logoSvg" viewBox="0 0 32 32" fill="none" style="visibility: hidden;">
<rect x="2" y="2" width="28" height="28" rx="8" stroke="url(#logoGrad)" stroke-width="2.5" />
<path d="M8 22 L12 14 L16 18 L20 10 L24 16" stroke="url(#logoGrad)" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" fill="none" />
<circle cx="12" cy="14" r="2" fill="url(#logoGrad)" />
<circle cx="20" cy="10" r="2" fill="url(#logoGrad)" />
<defs>
<linearGradient id="logoGrad" x1="0" y1="0" x2="32" y2="32">
<stop offset="0%" stop-color="#6366f1" />
<stop offset="100%" stop-color="#06b6d4" />
</linearGradient>
</defs>
</svg>
</div>
<h1 class="logo-text" id="logoText"></h1>
</div>
</div>
<div class="header-right">
<div class="theme-switch-wrapper">
<label class="theme-switch" for="themeToggle">
<input type="checkbox" id="themeToggle" />
<div class="slider round">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="theme-icon sun-icon">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="theme-icon moon-icon" style="display: none;">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</div>
</label>
</div>
<div id="userSection">
<button class="btn btn-login" id="btnLogin">登录</button>
</div>
<button class="btn-settings" id="btnSettings" title="配置管理">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z">
</path>
</svg>
</button>
</div>
</header>
<!-- Main Dashboard -->
<main class="dashboard" id="dashboard">
<!-- Top Stat Cards -->
<section class="stat-cards">
<div class="stat-card stat-card-servers" id="cardServers">
<div class="stat-card-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="2" y="2" width="20" height="8" rx="2" />
<rect x="2" y="14" width="20" height="8" rx="2" />
<circle cx="6" cy="6" r="1" fill="currentColor" />
<circle cx="6" cy="18" r="1" fill="currentColor" />
</svg>
</div>
<div class="stat-card-content">
<span class="stat-card-label" id="totalServersLabel">服务器总数</span>
<span class="stat-card-value" id="totalServers">0</span>
</div>
</div>
<div class="stat-card stat-card-cpu" id="cardCpu">
<div class="stat-card-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="4" y="4" width="16" height="16" rx="2" />
<rect x="9" y="9" width="6" height="6" />
<line x1="9" y1="2" x2="9" y2="4" />
<line x1="15" y1="2" x2="15" y2="4" />
<line x1="9" y1="20" x2="9" y2="22" />
<line x1="15" y1="20" x2="15" y2="22" />
<line x1="2" y1="9" x2="4" y2="9" />
<line x1="2" y1="15" x2="4" y2="15" />
<line x1="20" y1="9" x2="22" y2="9" />
<line x1="20" y1="15" x2="22" y2="15" />
</svg>
</div>
<div class="stat-card-content">
<span class="stat-card-label">CPU 使用率</span>
<span class="stat-card-value" id="cpuPercent">0%</span>
<span class="stat-card-sub" id="cpuDetail">0 / 0 核心</span>
</div>
</div>
<div class="stat-card stat-card-mem" id="cardMem">
<div class="stat-card-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" />
<path d="M7 7h4v4H7zM13 7h4v4h-4zM7 13h4v4H7zM13 13h4v4h-4z" />
</svg>
</div>
<div class="stat-card-content">
<span class="stat-card-label">内存使用率</span>
<span class="stat-card-value" id="memPercent">0%</span>
<span class="stat-card-sub" id="memDetail">0 / 0 GB</span>
</div>
</div>
<div class="stat-card stat-card-disk" id="cardDisk">
<div class="stat-card-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<ellipse cx="12" cy="5" rx="9" ry="3" />
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
</svg>
</div>
<div class="stat-card-content">
<span class="stat-card-label">磁盘使用率</span>
<span class="stat-card-value" id="diskPercent">0%</span>
<span class="stat-card-sub" id="diskDetail">0 / 0 GB</span>
</div>
</div>
<div class="stat-card stat-card-bandwidth" id="cardBandwidth">
<div class="stat-card-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg>
</div>
<div class="stat-card-content">
<span class="stat-card-label">实时带宽 (MB/s ↑/↓)</span>
<div class="stat-card-value-group">
<span class="stat-card-value" id="totalBandwidthTx">0.00</span>
<span class="stat-card-separator">/</span>
<span class="stat-card-value" id="totalBandwidthRx">0.00</span>
</div>
</div>
</div>
</section>
<!-- Center Charts -->
<section class="charts-section">
<!-- Network Traffic 24h Chart -->
<div class="chart-card chart-card-wide" id="networkChart">
<div class="chart-card-header">
<div class="chart-header-left">
<h2 class="chart-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="chart-title-icon">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
</svg>
网络流量趋势 (24h)
</h2>
<div class="chart-header-actions">
<button class="btn-icon" id="btnRefreshNetwork" title="刷新流量趋势">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 16px; height: 16px;">
<path d="M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
</button>
</div>
</div>
<div class="chart-legend">
<span class="legend-item" id="legendRx" style="cursor: pointer;" title="点击切换 接收 (RX) 显示/隐藏"><span
class="legend-dot legend-rx"></span>接收 (RX)</span>
<span class="legend-item" id="legendTx" style="cursor: pointer;" title="点击切换 发送 (TX) 显示/隐藏"><span
class="legend-dot legend-tx"></span>发送 (TX)</span>
<span class="legend-item disabled" id="legendP95" style="cursor: pointer;" title="点击切换 P95 线显示/隐藏">
<span class="legend-dot legend-p95"></span>95计费 (<span id="p95LabelText">上行</span>)
</span>
</div>
</div>
<div class="chart-body">
<canvas id="networkCanvas"></canvas>
</div>
<div class="chart-footer">
<div class="traffic-stat">
<span class="traffic-label">24h 接收总量</span>
<span class="traffic-value" id="traffic24hRx">0 B</span>
</div>
<div class="traffic-stat">
<span class="traffic-label">24h 发送总量</span>
<span class="traffic-value" id="traffic24hTx">0 B</span>
</div>
<div class="traffic-stat traffic-stat-p95">
<span class="traffic-label">95计费 (上行)</span>
<span class="traffic-value" id="trafficP95">0 B/s</span>
</div>
<div class="traffic-stat traffic-stat-total">
<span class="traffic-label">24h 总流量</span>
<span class="traffic-value" id="traffic24hTotal">0 B</span>
</div>
<div class="traffic-stat traffic-stat-time">
<span class="traffic-label">当前时间</span>
<span class="traffic-value" id="footerTime">00:00:00</span>
</div>
</div>
</div>
<!-- Global Traffic 3D Globe -->
<div class="chart-card globe-card" id="globeCard">
<div class="chart-card-header">
<h2 class="chart-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="chart-title-icon">
<circle cx="12" cy="12" r="10" />
<path
d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</svg>
全球骨干分布
</h2>
<div class="chart-header-actions">
<button class="btn-icon" id="btnExpandGlobe" title="放大显示">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
style="width: 18px; height: 18px;">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" />
</svg>
</button>
</div>
</div>
<div class="globe-body" id="globeContainer"></div>
<div class="chart-footer">
<div class="traffic-stat">
<span class="traffic-label">全球节点总数</span>
<span class="traffic-value" id="globeTotalNodes">0</span>
</div>
<div class="traffic-stat">
<span class="traffic-label">覆盖地区/国家</span>
<span class="traffic-value" id="globeTotalRegions">0</span>
</div>
<div class="traffic-stat">
<span class="traffic-label">实时活跃状态</span>
<span class="traffic-value" style="color: var(--accent-emerald);">Active</span>
</div>
</div>
</div>
</section>
<!-- Server List -->
<section class="server-list-section" id="serverListSection">
<div class="chart-card">
<div class="chart-card-header">
<h2 class="chart-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="chart-title-icon">
<rect x="2" y="2" width="20" height="8" rx="2" />
<rect x="2" y="14" width="20" height="8" rx="2" />
<circle cx="6" cy="6" r="1" fill="currentColor" />
<circle cx="6" cy="18" r="1" fill="currentColor" />
</svg>
服务器详情
</h2>
<div class="chart-header-right">
<div class="search-box">
<input type="search" id="serverSearchFilter" name="q-filter-server" placeholder="检索服务器名称..."
autocomplete="one-time-code" spellcheck="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="search-icon">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
<button id="btnResetSort" class="btn-icon-sm" title="重置筛选与排序">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
<path d="M3 3v5h5"></path>
</svg>
</button>
<select id="sourceFilter" class="source-select">
<option value="all">所有数据源</option>
</select>
</div>
</div>
<div class="server-table-wrap">
<table class="server-table" id="serverTable">
<thead>
<tr>
<th class="sortable active" data-sort="up">状态 <span class="sort-icon"></span></th>
<th class="sortable" data-sort="job">Job / 实例 <span class="sort-icon"></span></th>
<th class="sortable" data-sort="source">数据源 <span class="sort-icon"></span></th>
<th class="sortable" data-sort="cpu">CPU <span class="sort-icon"></span></th>
<th class="sortable" data-sort="mem">内存 <span class="sort-icon"></span></th>
<th class="sortable" data-sort="disk">磁盘 <span class="sort-icon"></span></th>
<th class="sortable" data-sort="netRx">网络 ↓ <span class="sort-icon"></span></th>
<th class="sortable" data-sort="netTx">网络 ↑ <span class="sort-icon"></span></th>
</tr>
</thead>
<tbody id="serverTableBody">
<tr class="empty-row">
<td colspan="8">暂无数据 - 请先配置 Prometheus 数据源</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination-footer">
<div class="page-size-selector">
<span>每页显示</span>
<select id="pageSizeSelect" class="source-select">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<span></span>
</div>
<div class="pagination-controls" id="paginationControls">
<!-- Pagination buttons will be injected here -->
</div>
</div>
</div>
</section>
</main>
<!-- Footer -->
<footer class="site-footer">
<div class="footer-content">
<div class="copyright">© <span id="copyrightYear"></span> LDNET-GA-Service. All rights reserved.</div>
<div class="filings">
<a href="http://www.beian.gov.cn/portal/registerSystemInfo" target="_blank" id="psFilingDisplay" style="display: none;">
<span id="psFilingText"></span>
</a>
<span class="filing-sep"></span>
<a href="https://beian.miit.gov.cn/" target="_blank" id="icpFilingDisplay" style="display: none;"></a>
</div>
</div>
</footer>
<!-- Settings Modal -->
<div class="modal-overlay" id="settingsModal">
<div class="modal">
<div class="modal-header">
<div class="modal-tabs">
<button class="modal-tab active" data-tab="prom">数据源管理</button>
<button class="modal-tab" data-tab="site">大屏设置</button>
<button class="modal-tab" data-tab="security">安全设置</button>
<button class="modal-tab" data-tab="latency">延迟线路管理</button>
<button class="modal-tab" data-tab="auth">账号安全</button>
</div>
<button class="modal-close" id="modalClose">&times;</button>
</div>
<div class="modal-body">
<!-- Prometheus Sources Tab -->
<div class="tab-content active" id="tab-prom">
<!-- Add Source Form -->
<div class="add-source-form" id="addSourceForm">
<h3>添加数据源</h3>
<div class="form-row">
<div class="form-group" style="flex: 0.8;">
<label for="sourceType">类型</label>
<select id="sourceType"
style="padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary); outline: none;">
<option value="prometheus">Prometheus</option>
<option value="blackbox">Blackbox Exporter</option>
</select>
</div>
<div class="form-group" style="flex: 1;">
<label for="sourceName">名称</label>
<input type="text" id="sourceName" placeholder="例:生产环境" autocomplete="off">
</div>
<div class="form-group form-group-wide">
<label for="sourceUrl">URL 地址</label>
<input type="url" id="sourceUrl" placeholder="http://1.2.3.4:9090" autocomplete="off">
</div>
</div>
<div class="form-row">
<div class="form-group form-group-wide">
<label for="sourceDesc">描述 (可选)</label>
<input type="text" id="sourceDesc" placeholder="数据源描述" autocomplete="off">
</div>
<div class="form-group" id="serverSourceOption"
style="display: flex; align-items: flex-end; padding-bottom: 8px;">
<label
style="display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 0.85rem; color: var(--text-secondary); white-space: nowrap;">
<input type="checkbox" id="isServerSource" checked
style="width: 16px; height: 16px; accent-color: var(--accent-indigo);">
<span>用于服务器展示</span>
</label>
</div>
<div class="form-actions">
<button class="btn btn-test" id="btnTest">测试连接</button>
<button class="btn btn-add" id="btnAdd">添加</button>
</div>
</div>
<div class="form-message" id="formMessage"></div>
</div>
<!-- Source List -->
<div class="source-list" id="sourceList">
<h3>已配置数据源</h3>
<div class="source-items" id="sourceItems">
<div class="source-empty">暂无数据源</div>
</div>
</div>
</div>
<!-- Site Settings Tab -->
<div class="tab-content" id="tab-site">
<div class="site-settings-form">
<h3>自定义大屏展示</h3>
<div class="form-group">
<label for="pageNameInput">页面名称 (浏览器标签页标题)</label>
<input type="text" id="pageNameInput" placeholder="例:运维监控大屏">
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="siteTitleInput">标题 (大屏左上角显示名称)</label>
<input type="text" id="siteTitleInput" placeholder="例:数据可视化展示大屏">
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="showPageNameInput">是否显示左上角标题</label>
<select id="showPageNameInput"
style="padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary); width: 100%;">
<option value="1">显示 (Show)</option>
<option value="0">隐藏 (Hide)</option>
</select>
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="logoUrlInput">Logo URL (白天/默认,支持图片链接)</label>
<input type="url" id="logoUrlInput" placeholder="https://example.com/logo_light.png">
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="logoUrlDarkInput">Logo URL (黑夜模式,可为空则使用默认)</label>
<input type="url" id="logoUrlDarkInput" placeholder="https://example.com/logo_dark.png">
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="faviconUrlInput">Favicon URL (浏览器标签页图标)</label>
<input type="url" id="faviconUrlInput" placeholder="https://example.com/favicon.ico">
</div>
<div class="settings-section" style="margin-top: 25px; border-top: 1px solid var(--border-color); padding-top: 20px;">
<h4 style="font-size: 0.85rem; color: var(--accent-indigo); margin-bottom: 15px; text-transform: uppercase; letter-spacing: 0.5px;">界面外观 (Appearance)</h4>
<div class="form-group">
<label for="defaultThemeInput">色彩主题模式</label>
<select id="defaultThemeInput"
style="padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary); width: 100%;">
<option value="auto">跟随系统主题 (Sync with OS)</option>
<option value="dark">强制深色模式 (Always Dark)</option>
<option value="light">强制浅色模式 (Always Light)</option>
</select>
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 6px;">选择“跟随系统”后,应用将自动同步您操作系统或浏览器的黑暗/白天模式设置。</p>
</div>
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="show95BandwidthInput">24h趋势图默认显示 95计费线</label>
<select id="show95BandwidthInput"
style="padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary);">
<option value="1">显示</option>
<option value="0">不显示</option>
</select>
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="p95TypeSelect">95带宽计费统计类型</label>
<select id="p95TypeSelect"
style="padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary);">
<option value="tx">仅统计上行 (TX)</option>
<option value="rx">仅统计下行 (RX)</option>
<option value="both">统计上行+下行 (Sum)</option>
<option value="max">出入取大 (Max)</option>
</select>
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="psFilingInput">公安备案号 (如:京公网安备 11010102000001号)</label>
<input type="text" id="psFilingInput" placeholder="请输入公安备案号">
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="icpFilingInput">ICP 备案号 (如京ICP备12345678号)</label>
<input type="text" id="icpFilingInput" placeholder="请输入 ICP 备案号">
</div>
<div class="form-group" style="margin-top: 15px;">
<label>网络流量趋势 (24h) 统计数据源</label>
<div id="network-source-selector" class="network-source-list" style="margin-top: 8px; display: flex; flex-wrap: wrap; gap: 10px; background: var(--bg-input); padding: 12px; border-radius: var(--radius-sm); border: 1px solid var(--border-color);">
<div class="loading-inline" style="color: var(--text-muted); font-size: 0.9rem;">加载数据源中...</div>
</div>
<small style="display: block; margin-top: 6px; color: var(--text-muted);">选择参与 24 小时网络流量统计的 Prometheus 数据源。如果不勾选任何项,则统计所有数据源。</small>
</div>
<div class="form-actions" style="margin-top: 25px; display: flex; justify-content: flex-end;">
<button class="btn btn-add" id="btnSaveSiteSettings">保存基础设置</button>
</div>
<div class="form-message" id="siteSettingsMessage"></div>
</div>
</div>
<!-- Security Settings Tab -->
<div class="tab-content" id="tab-security">
<div class="security-settings-form">
<h3>安全与隐私设置</h3>
<div class="form-group" style="margin-top: 15px;">
<label for="requireLoginForServerDetailsInput">服务器详情是否仅登录后可查看</label>
<select id="requireLoginForServerDetailsInput"
style="padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary); width: 100%;">
<option value="1">仅登录后可查看</option>
<option value="0">允许公开查看</option>
</select>
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 6px;">开启后,未登录访客仍可看到大屏总览,但点击单台服务器时需要先登录。</p>
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="showServerIpInput">是否在服务器详情中显示公网 IP</label>
<select id="showServerIpInput"
style="padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary); width: 100%;">
<option value="1">显示 (Show)</option>
<option value="0">隐藏 (Hide)</option>
</select>
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 6px;">开启后,点击服务器详情时会显示该服务器的公网 IP 地址。</p>
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="ipMetricNameInput">自定义 IP 采集指标 (可选)</label>
<input type="text" id="ipMetricNameInput" placeholder="例node_network_address_info">
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 6px;">如果您的 Prometheus 中有专门记录 IP 的指标,请在此输入。留空则尝试自动发现。</p>
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="ipLabelNameInput">IP 指标中的 Label 名称</label>
<input type="text" id="ipLabelNameInput" placeholder="默认address">
</div>
<div class="form-actions" style="margin-top: 25px; display: flex; justify-content: flex-end;">
<button class="btn btn-add" id="btnSaveSecuritySettings">保存安全设置</button>
</div>
<div class="form-message" id="securitySettingsMessage"></div>
</div>
</div>
<!-- Custom Detail Metrics Tab -->
<div class="tab-content" id="tab-details-metrics">
<div class="metrics-settings-form">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0;">服务器详情指标配置</h3>
<button class="btn btn-add" id="btnAddCustomMetric" style="padding: 6px 12px; font-size: 0.8rem;">
<i class="fas fa-plus"></i> 添加指标
</button>
</div>
<div id="customMetricsList" class="custom-metrics-list" style="max-height: 400px; overflow-y: auto; padding-right: 5px;">
<!-- Dynamic rows will be added here -->
</div>
<div class="form-actions" style="margin-top: 25px; display: flex; justify-content: flex-end;">
<button class="btn btn-add" id="btnSaveCustomMetrics">保存指标配置</button>
</div>
<div class="form-message" id="customMetricsMessage"></div>
</div>
</div>
<!-- Latency Routes Tab -->
<div class="tab-content" id="tab-latency">
<div class="latency-settings-form">
<h3>Blackbox 延迟连线管理</h3>
<div class="latency-routes-manager">
<!-- Add Route Form -->
<div class="add-route-mini-form"
style="background: rgba(255,255,255,0.02); padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid var(--border-color);">
<div class="form-row">
<div class="form-group" style="flex: 1.5;">
<label>探测用服务器</label>
<select id="routeSourceSelect"
style="padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary);">
<option value="">-- 选择数据源 --</option>
</select>
</div>
<div class="form-group">
<label>起航点</label>
<input type="text" id="routeSourceInput" placeholder="例China">
</div>
<div class="form-group">
<label>目的地</label>
<input type="text" id="routeDestInput" placeholder="例United States">
</div>
</div>
<div class="form-row" style="margin-top: 10px; align-items: flex-end;">
<div class="form-group" style="flex: 2;">
<label>Blackbox 探测目标 (IP 或 域名)</label>
<input type="text" id="routeTargetInput" placeholder="例1.1.1.1 或 google.com">
</div>
<div class="form-actions" style="padding-bottom: 0; display: flex; gap: 8px;">
<button class="btn btn-add" id="btnAddRoute" style="padding: 10px 24px;">添加线路</button>
<button class="btn btn-test" id="btnCancelEditRoute"
style="display: none; padding: 10px 15px; background: rgba(0,0,0,0.3);">取消</button>
</div>
</div>
</div>
<!-- Routes List -->
<div class="latency-routes-list-container">
<h4
style="font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase;; margin-bottom: 10px;">
已配置线路</h4>
<div id="latencyRoutesList" class="latency-routes-list"
style="display: flex; flex-direction: column; gap: 10px;">
<!-- Routes will be injected here -->
<div class="route-empty"
style="text-align: center; padding: 20px; color: var(--text-muted); font-size: 0.85rem; background: rgba(0,0,0,0.1); border-radius: 8px;">
暂无线路</div>
</div>
</div>
</div>
</div>
</div>
<!-- Account Security Tab -->
<div class="tab-content" id="tab-auth">
<div class="security-settings-form">
<h3>修改登录密码</h3>
<div class="form-group">
<label for="oldPassword">当前密码</label>
<input type="password" id="oldPassword" placeholder="请输入当前旧密码">
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="newPassword">新密码</label>
<input type="password" id="newPassword" placeholder="请输入要设置的新密码">
</div>
<div class="form-group" style="margin-top: 15px;">
<label for="confirmNewPassword">确认新密码</label>
<input type="password" id="confirmNewPassword" placeholder="请再次确认新密码">
</div>
<div class="form-actions" style="margin-top: 25px; display: flex; justify-content: flex-end;">
<button class="btn btn-add" id="btnChangePassword">提交修改</button>
</div>
<div class="form-message" id="changePasswordMessage"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Server Detail Modal -->
<div class="modal-overlay" id="serverDetailModal">
<div class="modal" style="max-width: 800px; width: 95%;">
<div class="modal-header">
<div style="display: flex; flex-direction: column;">
<h2 id="serverDetailTitle" style="margin-bottom: 0;">服务器详情</h2>
</div>
<button class="modal-close" id="serverDetailClose">&times;</button>
</div>
<div class="modal-body" id="serverDetailBody" style="padding: 0;">
<div id="detailLoading" style="text-align: center; padding: 40px; display: none;">
<div class="dot dot-pulse"
style="display: inline-block; width: 12px; height: 12px; background: var(--accent-indigo);"></div>
<span style="margin-left: 10px; color: var(--text-secondary);">正在从数据源读取详情...</span>
</div>
<div class="detail-container" id="detailContainer">
<!-- Metric Items are injected here -->
<div class="detail-metrics-list" id="detailMetricsList"></div>
<div class="detail-partitions-container metric-item" id="detailPartitionsContainer" style="display: none;">
<div class="metric-item-header" id="partitionHeader">
<div class="metric-label-group">
<span class="metric-label">磁盘分区详情 (已挂载)</span>
<span class="metric-value" id="partitionSummary">读取中...</span>
</div>
<svg class="chevron-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
<div class="metric-item-content" id="partitionContent">
<div class="detail-partitions-list" id="detailPartitionsList"></div>
</div>
</div>
<div class="detail-info-grid" id="detailInfoGrid">
<div class="info-item">
<span class="info-label">CPU 核心总数</span>
<span class="info-value" id="detailCpuCores">0 核心</span>
</div>
<div class="info-item">
<span class="info-label">物理内存总量</span>
<span class="info-value" id="detailMemTotal">0 GB</span>
</div>
<div class="info-item">
<span class="info-label">运行时间 (Uptime)</span>
<span class="info-value" id="detailUptime">0天 0小时</span>
</div>
<div class="info-item">
<span class="info-label">硬盘总量统计</span>
<span class="info-value" id="detailDiskTotal">0 GB</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Login Modal -->
<div class="modal-overlay" id="loginModalOverlay">
<div class="modal" style="max-width: 400px;">
<div class="modal-header">
<h2>用户登录</h2>
<button class="modal-close" id="closeLoginModal">&times;</button>
</div>
<div class="modal-body">
<form id="loginForm">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" placeholder="请输入用户名" required>
</div>
<div class="form-group" style="margin-top: 16px;">
<label for="password">密码</label>
<input type="password" id="password" placeholder="请输入密码" required>
</div>
<div id="loginError" style="color: var(--accent-rose); font-size: 0.8rem; margin-top: 10px; display: none;">
</div>
<button type="submit" class="btn btn-primary"
style="width: 100%; margin-top: 24px; background: var(--gradient-primary); color: white; border: none; padding: 12px; border-radius: 8px; cursor: pointer;">
</button>
</form>
</div>
</div>
</div>
<script src="/js/utils.js"></script>
<script src="/js/chart.js"></script>
<script src="/js/app.js"></script>
</body>
</html>