仰望星辰工作室

better-staridc-MNBT

Z zfhsh first commit 1 天前
1<!DOCTYPE html>
2<html lang="zh-CN">
3<head>
4<meta charset="UTF-8">
5<meta name="viewport" content="width=device-width, initial-scale=1.0">
6<title>MNBT Connector 管理面板</title>
7<style>
8:root {
9 --green: #36b370; --green-light: #e3f1ea; --green-hover: #ecf7f1;
10 --red: #e54545; --orange: #e6a23c; --blue: #409eff;
11 --t1: #333; --t2: #666; --t3: #888; --t4: #bbb;
12 --bg: #fff; --bg-gray: #f5f7fa; --bd: #e5e7eb; --bd-light: #f0f0f0;
13 --shadow-sm: 0 1px 2px rgba(0,0,0,.03); --shadow-hover: 0 2px 8px rgba(0,0,0,.06);
14 --r: 6px; --r-sm: 4px; --tr: .2s ease;
15}
16*{box-sizing:border-box;margin:0;padding:0;}
17body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;font-size:14px;color:var(--t1);background:var(--bg);}
18::-webkit-scrollbar{width:5px;height:5px}::-webkit-scrollbar-thumb{background:#ccc;border-radius:3px}::-webkit-scrollbar-track{background:transparent}
19
20.bt-w-main{width:1000px;height:820px;display:flex;background:var(--bg);overflow:hidden}
21
22/* Sidebar */
23.sidebar{width:120px;min-width:120px;background:var(--bg-gray);display:flex;flex-direction:column;border-right:1px solid var(--bd)}
24.sidebar-item{display:flex;align-items:center;height:44px;padding:0 18px;cursor:pointer;font-size:14px;color:var(--t2);border-left:3px solid transparent;transition:var(--tr)}
25.sidebar-item:hover{color:var(--green);background:var(--green-light)}
26.sidebar-item.active{color:var(--green);background:var(--green-light);border-left-color:var(--green);font-weight:600}
27
28.main{flex:1;overflow-y:auto;min-width:0}
29
30/* ========== Overview ========== */
31.p-ov{padding:20px 22px}
32.ov-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:12px}
33.ov-top-l{display:flex;align-items:center;gap:12px}
34.ov-top-l h2{font-size:18px;font-weight:700;color:var(--t1)}
35.ov-top-r{display:flex;gap:8px}
36.ov-cards{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:18px}
37.ov-card{background:var(--bg);border-radius:var(--r);border:1px solid var(--bd);padding:18px;transition:var(--tr);box-shadow:var(--shadow-sm)}
38.ov-card:hover{box-shadow:var(--shadow-hover)}
39.ov-card .cl{font-size:12px;color:var(--t3);margin-bottom:8px}
40.ov-card .cv{font-size:16px;font-weight:700;color:var(--t1);word-break:break-all}
41.ov-card .cs{font-size:12px;color:var(--t3);margin-top:2px}
42
43.section{background:var(--bg);border:1px solid var(--bd);border-radius:var(--r);margin-bottom:14px;overflow:hidden}
44.section-hd{display:flex;align-items:center;justify-content:space-between;padding:12px 18px;background:var(--bg-gray);border-bottom:1px solid var(--bd);font-size:14px;font-weight:600;color:var(--t1)}
45.section-bd{padding:16px 18px}
46.log-box{background:#fafafa;color:var(--t2);border:1px solid var(--bd);border-radius:var(--r-sm);font-family:"SF Mono","Cascadia Code","Fira Code","Consolas",monospace;font-size:12px;padding:14px;max-height:260px;overflow-y:auto;line-height:1.7;white-space:pre-wrap;word-break:break-all}
47.dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px;vertical-align:middle}
48.dot.online{background:var(--green)}.dot.offline{background:var(--red)}
49.dot.breath{animation:breathe 2s infinite}
50@keyframes breathe{0%{box-shadow:0 0 0 0 rgba(54,179,112,.5);transform:scale(1)}70%{box-shadow:0 0 0 8px rgba(54,179,112,0);transform:scale(1.15)}100%{box-shadow:0 0 0 0 rgba(54,179,112,0);transform:scale(1)}}
51.badge{display:inline-flex;align-items:center;padding:3px 12px;border-radius:20px;font-size:12px;font-weight:600}
52.badge-success{background:#f0fdf4;color:#15803d}.badge-danger{background:#fef2f2;color:#dc2626}.badge-warning{background:#fffbeb;color:#d97706}.badge-default{background:#f3f4f6;color:var(--t2)}
53.tag{display:inline-block;padding:3px 10px;border-radius:3px;font-size:11px;background:#f3f4f6;color:var(--t2);margin:1px 3px 1px 0}
54
55/* ========== Stats Page ========== */
56.p-stats,.p-config,.p-logs{padding:14px 20px;display:none}
57.fb{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:14px}
58.fb label{font-size:13px;color:var(--t2);white-space:nowrap}
59.fb select{font-size:13px;padding:6px 30px 6px 12px;border:1px solid var(--bd);border-radius:var(--r-sm);background:var(--bg);color:var(--t1);outline:none;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;cursor:pointer}
60.fb select:focus{border-color:var(--green);box-shadow:0 0 0 2px rgba(54,179,112,.12)}
61.btn-rf{margin-left:auto;font-size:13px;color:var(--green);cursor:pointer;text-decoration:none;padding:6px 16px;border-radius:var(--r-sm);border:1px solid var(--green);background:var(--bg);transition:var(--tr)}
62.btn-rf:hover{background:var(--green-light)}
63
64/* Sub-nav */
65.sub-nav{display:flex;margin-bottom:14px}
66
67/* Glued btns */
68.btns{display:flex}
69.btns-btn{padding:0 16px;height:30px;line-height:29px;cursor:pointer;border:1px solid #dcdfe6;border-radius:0;background:#fff;transition:var(--tr);color:var(--t2);font-size:13px;user-select:none;outline:none;white-space:nowrap}
70.btns-btn:first-child{border-radius:4px 0 0 4px}
71.btns-btn:last-child{border-radius:0 4px 4px 0}
72.btns-btn:not(:last-child){border-right:none}
73.btns-btn:hover{color:var(--green);background:var(--green-hover)}
74.btns-btn.on{color:var(--green);border-color:var(--green);background:var(--green-light);font-weight:600}
75.btns-btn.on+.btns-btn{border-left-color:var(--green)}
76
77/* Skeleton */
78.skel{display:none}
79.skel.show{display:block}
80.skel-row{display:flex;gap:12px;margin-bottom:14px}
81.skel-card{flex:1;height:86px;background:#fff;border:1px solid #e8e8e8;border-radius:var(--r);padding:12px}
82.skel-l1{width:50px;height:11px;background:#e8e8e8;border-radius:3px;animation:skel 1.5s infinite;margin-bottom:9px}
83.skel-l2{width:80px;height:22px;background:#e8e8e8;border-radius:3px;animation:skel 1.5s infinite .15s;margin-bottom:8px}
84.skel-l3{width:96px;height:10px;background:#e8e8e8;border-radius:3px;animation:skel 1.5s infinite .3s}
85.skel-chart{background:#fff;border:1px solid #e8e8e8;border-radius:var(--r);padding:14px;margin-bottom:12px}
86.skel-ch{display:flex;justify-content:space-between;margin-bottom:12px}
87.skel-ct{width:110px;height:14px;background:#e8e8e8;border-radius:3px;animation:skel 1.5s infinite}
88.skel-cts{display:flex;gap:0}
89.skel-ctb{width:46px;height:26px;background:#e8e8e8;border-radius:3px;animation:skel 1.5s infinite .15s;margin-left:-1px;border:1px solid #fff}
90.skel-cbox{width:100%;height:185px;background:#e8e8e8;border-radius:3px;animation:skel 1.5s infinite .3s}
91@keyframes skel{0%,100%{opacity:.45}50%{opacity:1}}
92
93/* Stat cards (overview page only) */
94.stats-row{display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap}
95.stat-card{flex:1;min-width:100px;background:var(--bg);border:1px solid var(--bd);border-radius:var(--r);padding:12px 14px;transition:var(--tr);box-shadow:var(--shadow-sm)}
96.stat-card:hover{box-shadow:var(--shadow-hover)}
97.stat-label{display:flex;align-items:center;gap:5px;font-size:12px;color:var(--t3);margin-bottom:6px}
98.stat-val{font-size:26px;font-weight:700;color:var(--t1);margin-bottom:4px}
99.stat-comp{font-size:12px;color:var(--t3);line-height:1.5}
100.stat-comp .up{color:var(--green)}.stat-comp .down{color:var(--red)}
101.stat-rf{display:flex;align-items:center;justify-content:center;min-width:50px}
102
103/* Charts (overview page only) */
104.chart-card{background:var(--bg);border:1px solid var(--bd);border-radius:var(--r);margin-bottom:12px;overflow:hidden}
105.chart-card-hd{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid var(--bd-light);flex-wrap:wrap;gap:8px}
106.chart-card-hd h3{font-size:15px;font-weight:600;color:var(--t1)}
107.chart-card-bd{padding:12px 12px 8px}
108.chart-box{height:210px}
109
110/* Page panels */
111.pp{display:none}
112.pp.active{display:block}
113
114/* Page header (specialized pages) */
115.pg-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;flex-wrap:wrap;gap:8px}
116.pg-hd-l{display:flex;align-items:center;gap:10px}
117.pg-hd-l .pg-tip{font-size:11px;color:var(--t3)}
118.pg-hd-r{display:flex;align-items:center;gap:8px}
119.pg-switch{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--t2)}
120.pg-switch input{width:36px;height:22px;appearance:none;background:#ccc;border-radius:11px;position:relative;cursor:pointer;transition:var(--tr);outline:none}
121.pg-switch input:checked{background:var(--green)}
122.pg-switch input:after{content:'';position:absolute;top:2px;left:2px;width:18px;height:18px;background:#fff;border-radius:50%;transition:var(--tr)}
123.pg-switch input:checked:after{left:16px}
124
125/* Table */
126.tbl-wrap{overflow-x:auto}
127.tbl{width:100%;border-collapse:collapse;font-size:13px}
128.tbl th{padding:9px 12px;text-align:left;font-weight:600;color:var(--t2);background:#fafbfc;border-bottom:1px solid var(--bd);white-space:nowrap;font-size:12px}
129.tbl td{padding:9px 12px;border-bottom:1px solid var(--bd-light);color:var(--t1);vertical-align:middle}
130.tbl tbody tr:hover td{background:#f9fafb}
131.empty{text-align:center;padding:40px 0;color:var(--t4);font-size:13px}
132
133/* Footer note */
134.pg-note{margin-top:10px;font-size:12px;color:var(--t3);line-height:1.6}
135.pg-note a{color:var(--green)}
136
137/* Progress */
138.bar-wrap{width:100%;height:6px;background:#f0f0f0;border-radius:3px;overflow:hidden;margin-top:2px}
139.bar{height:100%;background:var(--green);border-radius:3px;min-width:2px;transition:width .4s ease}
140
141/* Pager */
142.pager{display:flex;align-items:center;justify-content:center;gap:4px;padding:12px 16px;border-top:1px solid var(--bd-light);font-size:13px;color:var(--t2)}
143.pager-btn{display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:26px;padding:0 6px;border:1px solid var(--bd);border-radius:var(--r-sm);cursor:pointer;background:#fff;transition:var(--tr);font-size:12px}
144.pager-btn:hover:not(:disabled){border-color:var(--green);color:var(--green);background:var(--green-hover)}
145.pager-btn.on{background:var(--green);color:#fff;border-color:var(--green)}
146.pager-btn:disabled{color:var(--t4);cursor:default;opacity:.5}
147.pager-info{margin:0 4px;color:var(--t3);font-size:12px}
148.pager input{width:42px;height:26px;text-align:center;border:1px solid var(--bd);border-radius:var(--r-sm);font-size:12px;outline:none}
149.pager input:focus{border-color:var(--green)}
150.pager-go{display:inline-flex;align-items:center;justify-content:center;height:26px;padding:0 10px;border:1px solid var(--green);border-radius:var(--r-sm);cursor:pointer;background:var(--green);color:#fff;font-size:12px;transition:var(--tr)}
151.pager-go:hover{background:#2d9a5e}
152</style>
153</head>
154<body>
155<div class="bt-w-main">
156
157 <!-- Sidebar -->
158 <div class="sidebar" id="mnbtSidebar">
159 <div class="sidebar-item active" data-panel="p-ov">概览</div>
160 <div class="sidebar-item" data-panel="p-config">当前配置</div>
161 <div class="sidebar-item" data-panel="p-logs">运行日志</div>
162 <div class="sidebar-item" data-panel="p-stats">站点统计</div>
163 </div>
164
165 <!-- Main -->
166 <div class="main" id="mnbtMain">
167
168 <!-- ==================== 概览 ==================== -->
169 <div class="p-ov" id="p-ov">
170 <div class="ov-top">
171 <div class="ov-top-l"><h2>MNBT 连接器</h2><span class="badge badge-default" id="mnbtVersionBadge">v0.2</span><span id="mnbtStatusBadge" style="display:none;"></span></div>
172 <div class="ov-top-r">
173 <button class="btn btn-success btn-sm" onclick="MNBT.start()">启动</button>
174 <button class="btn btn-danger btn-sm" onclick="MNBT.stop()">停止</button>
175 <button class="btn btn-primary btn-sm" onclick="MNBT.runOnce()">执行一次</button>
176 <button class="btn btn-default btn-sm" onclick="MNBT.status()">刷新</button>
177 </div>
178 </div>
179 <div class="ov-cards">
180 <div class="ov-card"><div class="cl">运行状态</div><div class="cv" id="mnbtCardRunning"><span class="dot offline"></span>检测中...</div></div>
181 <div class="ov-card"><div class="cl">节点 ID</div><div class="cv" id="mnbtCardNodeId">-</div></div>
182 <div class="ov-card"><div class="cl">进程 PID</div><div class="cv" id="mnbtCardPid">-</div><div class="cs" id="mnbtCardPidSub">未运行</div></div>
183 <div class="ov-card"><div class="cl">运行版本</div><div class="cv" id="mnbtCardVersion">-</div></div>
184 </div>
185 </div>
186
187 <!-- ==================== 当前配置 ==================== -->
188 <div class="p-config" id="p-config">
189 <div class="section">
190 <div class="section-hd"><span>当前配置</span><button class="btn btn-default btn-xs" onclick="MNBT.loadConfig()">刷新</button></div>
191 <div class="section-bd">
192 <div class="ov-cards" style="grid-template-columns:repeat(3,1fr);margin-bottom:14px">
193 <div class="ov-card"><div class="cl">配置文件</div><div class="cv" id="mnbtConfigPath">-</div></div>
194 <div class="ov-card"><div class="cl">节点 ID</div><div class="cv" id="mnbtConfigNodeId">-</div></div>
195 <div class="ov-card"><div class="cl">工作间隔</div><div class="cv" id="mnbtConfigInterval">-</div></div>
196 </div>
197 <div id="mnbtConfigInfo" style="font-size:13px;color:var(--t3);margin-bottom:12px;line-height:1.8">加载中...</div>
198 <pre class="log-box" id="mnbtConfigJson" style="max-height:520px">加载中...</pre>
199 </div>
200 </div>
201 </div>
202
203 <!-- ==================== 运行日志 ==================== -->
204 <div class="p-logs" id="p-logs">
205 <div class="section">
206 <div class="section-hd">
207 <span>运行日志</span>
208 <div style="display:flex;align-items:center;gap:12px;font-size:12px;color:var(--t3);font-weight:400">
209 <label style="cursor:pointer"><input type="checkbox" id="mnbtLogAutoRefresh"> 自动刷新</label>
210 <label style="cursor:pointer"><input type="checkbox" id="mnbtAutoScroll" checked> 自动滚动</label>
211 <button class="btn btn-default btn-xs" onclick="MNBT.loadLogs()">刷新</button>
212 </div>
213 </div>
214 <div class="section-bd">
215 <div class="ov-cards" style="grid-template-columns:repeat(4,1fr);margin-bottom:14px">
216 <div class="ov-card"><div class="cl">日志文件</div><div class="cv" id="mnbtLogPath">-</div></div>
217 <div class="ov-card"><div class="cl">日志大小</div><div class="cv" id="mnbtLogSize">-</div></div>
218 <div class="ov-card"><div class="cl">更新时间</div><div class="cv" id="mnbtLogMtime">-</div></div>
219 <div class="ov-card"><div class="cl">进程状态</div><div class="cv" id="mnbtLogRunning">-</div></div>
220 </div>
221 <pre class="log-box" id="mnbtConnectorLog" style="max-height:590px">暂无日志</pre>
222 </div>
223 </div>
224 </div>
225
226 <!-- ==================== 站点统计 ==================== -->
227 <div class="p-stats" id="p-stats">
228
229 <!-- 1.筛选栏 -->
230 <div class="fb">
231 <label>站点</label><select id="ssSiteSelect" onchange="MNBT.reload()" style="min-width:150px;"><option value="">-- 选择站点 --</option></select>
232 <label style="margin-left:4px;">时间</label>
233 <div class="btns" id="rangeBtns"><div class="btns-btn" data-v="today">今日</div><div class="btns-btn" data-v="yesterday">昨日</div><div class="btns-btn on" data-v="7d">近7天</div><div class="btns-btn" data-v="30d">近30天</div></div>
234 <a class="btn-rf" href="javascript:;" onclick="MNBT.reload()">刷新</a>
235 </div>
236
237 <!-- 2.二级导航(8项:概况 + 7专项) -->
238 <div class="sub-nav"><div class="btns" id="subNav">
239 <div class="btns-btn on" onclick="MNBT.switchPage('overview',this)">概况</div>
240 <div class="btns-btn" onclick="MNBT.switchPage('spider',this)">蜘蛛</div>
241 <div class="btns-btn" onclick="MNBT.switchPage('client',this)">客户端</div>
242 <div class="btns-btn" onclick="MNBT.switchPage('method',this)">请求方式</div>
243 <div class="btns-btn" onclick="MNBT.switchPage('iprank',this)">IP 排行</div>
244 <div class="btns-btn" onclick="MNBT.switchPage('urirank',this)">URI 排行</div>
245 <div class="btns-btn" onclick="MNBT.switchPage('errors',this)">错误日志</div>
246 <div class="btns-btn" onclick="MNBT.switchPage('recent',this)">网站日志</div>
247 </div></div>
248
249 <!-- 3.骨架屏 -->
250 <div class="skel" id="ssSkel">
251 <div class="skel-row"><div class="skel-card"><div class="skel-l1"></div><div class="skel-l2"></div><div class="skel-l3"></div></div><div class="skel-card"><div class="skel-l1"></div><div class="skel-l2"></div><div class="skel-l3"></div></div><div class="skel-card"><div class="skel-l1"></div><div class="skel-l2"></div><div class="skel-l3"></div></div><div class="skel-card"><div class="skel-l1"></div><div class="skel-l2"></div><div class="skel-l3"></div></div><div class="skel-card"><div class="skel-l1"></div><div class="skel-l2"></div><div class="skel-l3"></div></div><div class="skel-card"><div class="skel-l1"></div><div class="skel-l2"></div><div class="skel-l3"></div></div><div class="skel-card"><div class="skel-l1"></div><div class="skel-l2"></div><div class="skel-l3"></div></div></div>
252 <div class="skel-chart"><div class="skel-ch"><div class="skel-ct"></div><div class="skel-cts"><div class="skel-ctb"></div><div class="skel-ctb"></div><div class="skel-ctb"></div><div class="skel-ctb"></div><div class="skel-ctb"></div></div></div><div class="skel-cbox"></div></div>
253 <div class="skel-chart"><div class="skel-ch"><div class="skel-ct"></div><div class="skel-cts"><div class="skel-ctb"></div><div class="skel-ctb"></div><div class="skel-ctb"></div><div class="skel-ctb"></div><div class="skel-ctb"></div></div></div><div class="skel-cbox"></div></div>
254 </div>
255
256 <!-- 4.内容区 -->
257 <div id="ssContent" style="display:none;">
258
259 <!-- ====== 概况页 (overview) ====== -->
260 <div class="pp active" id="pp-overview">
261 <div class="stats-row" id="ssStatsCards"></div>
262 <div class="chart-card">
263 <div class="chart-card-hd"><h3>数据指标</h3><div class="btns" id="metricType"><div class="btns-btn on" data-v="pv">PV</div><div class="btns-btn" data-v="uv">UV</div><div class="btns-btn" data-v="ip">IP</div><div class="btns-btn" data-v="traffic">流量</div></div></div>
264 <div class="chart-card-bd"><div class="chart-box" id="sevenChart"></div></div>
265 </div>
266 </div>
267
268 <!-- ====== Spider ====== -->
269 <div class="pp" id="pp-spider">
270 <div class="pg-hd"><div class="pg-hd-l"><span class="pg-tip">爬虫请求统计数据,基于 User-Agent 识别</span></div></div>
271 <div class="tbl-wrap"><table class="tbl"><thead><tr><th>蜘蛛名称</th><th style="text-align:center;width:14%;">请求数</th><th style="text-align:center;">占比</th></tr></thead><tbody id="ssSpiderBody"><tr><td colspan="3" class="empty">请先选择站点</td></tr></tbody></table></div>
272 <div class="pg-note">* 蜘蛛识别基于常见爬虫 User-Agent 规则匹配,部分非标准爬虫可能未被识别。统计仅记录匹配到的爬虫请求,不影响站点正常访问性能。</div>
273 <div class="pager" id="ssSpiderPager" style="display:none"></div>
274 </div>
275
276 <!-- ====== Client ====== -->
277 <div class="pp" id="pp-client">
278 <div class="pg-hd"><div class="pg-hd-l"><span class="pg-tip">浏览器 & 设备类型分布统计</span></div></div>
279 <div class="tbl-wrap"><table class="tbl"><thead><tr><th>客户端类型</th><th style="text-align:center;width:14%;">请求数</th><th style="text-align:center;">占比</th></tr></thead><tbody id="ssClientBody"><tr><td colspan="3" class="empty">请先选择站点</td></tr></tbody></table></div>
280 <div class="pg-note">* 客户端识别基于浏览器 User-Agent 字符串解析,区分 PC/移动端及主流浏览器。统计过程消耗少量 CPU 资源。</div>
281 <div class="pager" id="ssClientPager" style="display:none"></div>
282 </div>
283
284 <!-- ====== Method ====== -->
285 <div class="pp" id="pp-method">
286 <div class="pg-hd"><div class="pg-hd-l"><span class="pg-tip">HTTP 请求方式统计(GET / POST / HEAD 等)</span></div></div>
287 <div class="tbl-wrap"><table class="tbl"><thead><tr><th>请求方式</th><th style="text-align:center;width:14%;">请求数</th><th style="text-align:center;">占比</th></tr></thead><tbody id="ssMethodBody"><tr><td colspan="3" class="empty">请先选择站点</td></tr></tbody></table></div>
288 <div class="pg-note">* 统计来源于 Nginx 日志中的请求方法字段。覆盖 GET / POST / HEAD / PUT / DELETE / OPTIONS 等标准 HTTP 方法。</div>
289 <div class="pager" id="ssMethodPager" style="display:none"></div>
290 </div>
291
292 <!-- ====== IP Rank ====== -->
293 <div class="pp" id="pp-iprank">
294 <div class="pg-hd">
295 <div class="pg-hd-l"><span class="pg-tip">已开通 IP 精准数据包</span></div>
296 <div class="pg-hd-r">
297 <label class="pg-switch"><input type="checkbox" checked onchange="MNBT._toggleSwitch(this)"> 全局 IP 统计</label>
298 </div>
299 </div>
300 <div class="tbl-wrap"><table class="tbl"><thead><tr><th style="width:5%;">#</th><th>IP</th><th style="width:12%;">归属地</th><th style="width:10%;">运营商</th><th style="text-align:center;width:12%;">请求数</th><th style="text-align:center;width:12%;">流量</th><th style="text-align:center;">流量占比图</th></tr></thead><tbody id="ssIpRankBody"><tr><td colspan="7" class="empty">请先选择站点</td></tr></tbody></table></div>
301 <div class="pg-note">* IP 归属地数据来自离线 GeoIP 库,精确度约 95%。开启全局 IP 统计会略微增加日志解析耗时,建议在访问高峰期谨慎使用。如需关闭,请点击右上角开关。</div>
302 <div class="pager" id="ssIpPager" style="display:none"></div>
303 </div>
304
305 <!-- ====== URI Rank ====== -->
306 <div class="pp" id="pp-urirank">
307 <div class="pg-hd">
308 <div class="pg-hd-l"><span class="pg-tip">URI 访问频率排行</span></div>
309 <div class="pg-hd-r">
310 <label class="pg-switch"><input type="checkbox" checked onchange="MNBT._toggleSwitch(this)"> 全局 URI 统计</label>
311 </div>
312 </div>
313 <div class="tbl-wrap"><table class="tbl"><thead><tr><th style="width:5%;">#</th><th>URI</th><th style="text-align:center;width:14%;">请求数</th><th style="text-align:center;width:14%;">流量</th><th style="text-align:center;">流量占比图</th></tr></thead><tbody id="ssUriRankBody"><tr><td colspan="5" class="empty">请先选择站点</td></tr></tbody></table></div>
314 <div class="pg-note">* URI 流量统计需要解析每条访问日志的请求路径和响应大小,高流量站点建议按需开启。统计精度受日志格式影响,自定义日志格式可能需要调整解析规则。</div>
315 <div class="pager" id="ssUriPager" style="display:none"></div>
316 </div>
317
318 <!-- ====== Errors ====== -->
319 <div class="pp" id="pp-errors">
320 <div class="pg-hd">
321 <div class="pg-hd-l" style="gap:8px;">
322 <span class="pg-tip">状态码筛选</span>
323 <select id="ssErrFilter" onchange="MNBT._loadTable('errors',1)" style="font-size:11px;padding:4px 24px 4px 8px;min-width:auto;width:auto;"><option value="400">>= 400 全部错误</option><option value="500">>= 500 服务端错误</option></select>
324 </div>
325 </div>
326 <div class="tbl-wrap"><table class="tbl"><thead><tr><th style="width:15%;">时间</th><th style="width:11%;">IP</th><th style="width:7%;">方法</th><th>URI</th><th style="text-align:center;width:9%;">状态码</th><th style="text-align:center;width:9%;">字节</th></tr></thead><tbody id="ssErrorBody"><tr><td colspan="6" class="empty">请先选择站点</td></tr></tbody></table></div>
327 <div class="pg-note">* 4xx 状态码表示客户端请求错误,5xx 表示服务端处理异常。错误统计有助于排查站点问题。仅记录状态码 >= 400 的请求。</div>
328 <div class="pager" id="ssErrorPager" style="display:none"></div>
329 </div>
330
331 <!-- ====== Recent Logs ====== -->
332 <div class="pp" id="pp-recent">
333 <div class="pg-hd"><div class="pg-hd-l"><span class="pg-tip">实时 Nginx 访问日志</span></div></div>
334 <pre class="log-box" id="ssRecentLog" style="max-height:560px;">请先选择站点</pre>
335 <div class="pg-note">* 日志来自 Nginx 访问日志文件,默认保留最近 30 天。日志文件过大时读取可能较慢,建议定期清理过期日志。</div>
336 <div class="pager" id="ssRecentPager" style="display:none"></div>
337 </div>
338
339 </div>
340 </div>
341 </div>
342</div>
343
344<script>
345(function(){
346var PAGES=['overview','spider','client','method','iprank','urirank','errors','recent'];
347var PAGERS=['ssSpiderPager','ssClientPager','ssMethodPager','ssIpPager','ssUriPager','ssErrorPager','ssRecentPager'];
348
349var F={
350 bytes:function(n){if(!n||n==='0')return'0 B';var v=typeof n==='string'?parseFloat(n):n;if(isNaN(v)||v<0)return'0 B';var u=['B','KB','MB','GB','TB'],i=0;while(v>=1024&&i<u.length-1){v/=1024;i++;}return v.toFixed(i>0?1:0)+' '+u[i];},
351 num:function(n){n=Number(n);return isNaN(n)?'0':n.toString().replace(/\B(?=(\d{3})+(?!\d))/g,',');},
352 pct:function(v,t){return t?(v/t*100).toFixed(1)+'%':'0%';}
353};
354
355$('#mnbtSidebar .sidebar-item').click(function(){
356 var id=$(this).data('panel');
357 $(this).addClass('active').siblings().removeClass('active');
358 $('.main>.p-ov,.main>.p-config,.main>.p-logs,.main>.p-stats,.main>.p-cc').hide();
359 $('#'+id).show();
360 if(id==='p-config')MNBT.loadConfig();
361 else if(id==='p-logs')MNBT.loadLogs();
362 else if(id==='p-stats'){MNBT.loadSiteList();$('#ssSiteSelect').data('ready',1);}
363});
364
365function esc(s){return String(s).replace(/[&<>"']/g,function(m){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m];});}
366window.MNBT={
367 curPage:'overview', pageSize:10, site:'', range:'7d',
368 metricType:'pv',
369 sevenChart:null, cache:null,
370
371 status:function(){
372 $.post('/plugin?action=a&name=mnbt_connector&s=get_status',{},function(r){
373 var b=document.getElementById('mnbtStatusBadge');b.style.display='inline';b.className='badge '+(r.running?'badge-success':'badge-danger');b.innerText=r.running?'运行中':'已停止';
374 document.getElementById('mnbtCardRunning').innerHTML='<span class="dot '+(r.running?'online':'offline')+'"></span>'+(r.running?'运行中':'已停止');
375 document.getElementById('mnbtCardNodeId').textContent=r.node_id||'未配置';
376 document.getElementById('mnbtCardPid').innerHTML=r.pid||'-';document.getElementById('mnbtCardPidSub').textContent=r.pid?'运行中':'未运行';
377 document.getElementById('mnbtCardVersion').textContent=r.version||'-';
378 var vb=document.getElementById('mnbtVersionBadge');if(r.version)vb.textContent='v'+r.version;
379 },'json').fail(function(){document.getElementById('mnbtCardRunning').innerHTML='<span style="color:var(--red)">连接失败</span>';});
380 },
381 start:function(){$.post('/plugin?action=a&name=mnbt_connector&s=start',{},function(r){layer.msg(r.msg,{icon:r.status?1:2});if(r.status)setTimeout(MNBT.status,1e3);},'json');},
382 stop:function(){$.post('/plugin?action=a&name=mnbt_connector&s=stop',{},function(r){layer.msg(r.msg,{icon:r.status?1:2});if(r.status)setTimeout(MNBT.status,1e3);},'json');},
383 runOnce:function(){$.post('/plugin?action=a&name=mnbt_connector&s=run_once',{},function(r){layer.msg(r.status?'执行成功':'执行失败:'+r.msg,{icon:r.status?1:2});if(r.status){setTimeout(MNBT.status,1500);setTimeout(MNBT.loadLogs,1500);}},'json');},
384
385 loadConfig:function(){
386 $.post('/plugin?action=a&name=mnbt_connector&s=get_config_info',{},function(r){
387 if(!r||!r.status){document.getElementById('mnbtConfigInfo').textContent=r&&r.msg?r.msg:'读取配置失败';return;}
388 var c=r.config||{};
389 document.getElementById('mnbtConfigPath').textContent=r.config_path||'-';
390 document.getElementById('mnbtConfigNodeId').textContent=c.node_id||'-';
391 document.getElementById('mnbtConfigInterval').textContent=(c.interval_seconds||'-')+' 秒';
392 var caps=Array.isArray(c.capabilities)?c.capabilities:[];
393 document.getElementById('mnbtConfigInfo').innerHTML='主控地址:<strong>'+esc(c.mnbt_url||'-')+'</strong><br>节点名称:<strong>'+esc(c.node_name||'-')+'</strong><br>版本:<strong>'+esc(c.version||'-')+'</strong><br>能力列表:'+(caps.length?caps.map(function(x){return'<span class="tag">'+esc(x)+'</span>';}).join(''):'-');
394 document.getElementById('mnbtConfigJson').textContent=r.config_json||'{}';
395 },'json').fail(function(){document.getElementById('mnbtConfigInfo').textContent='读取配置失败';});
396 },
397 loadLogs:function(){
398 $.post('/plugin?action=a&name=mnbt_connector&s=get_logs',{max_bytes:50000},function(r){
399 if(!r||!r.status){document.getElementById('mnbtConnectorLog').textContent=r&&r.msg?r.msg:'读取日志失败';return;}
400 document.getElementById('mnbtLogPath').textContent=r.log_path||'-';
401 document.getElementById('mnbtLogSize').textContent=F.bytes(r.log_size||0);
402 document.getElementById('mnbtLogMtime').textContent=r.log_mtime||'-';
403 document.getElementById('mnbtLogRunning').innerHTML='<span class="dot '+(r.running?'online':'offline')+'"></span>'+(r.running?'运行中 PID '+(r.pid||'-'):'未运行');
404 var l=document.getElementById('mnbtConnectorLog');l.textContent=r.log_text||'暂无日志';
405 if(document.getElementById('mnbtAutoScroll').checked)l.scrollTop=l.scrollHeight;
406 },'json').fail(function(){document.getElementById('mnbtConnectorLog').textContent='读取日志失败';});
407 },
408
409 loadSiteList:function(){
410 $.post('/plugin?action=a&name=mnbt_connector&s=get_site_list',{},function(r){
411 var s=document.getElementById('ssSiteSelect'),h='<option value="">-- 选择站点 --</option>',first='';
412 if(Array.isArray(r.data)&&r.data.length){
413 r.data.forEach(function(x){
414 var n=typeof x==='string'?x:(x.site_name||x.name||'');
415 if(!n)return;
416 if(!first)first=n;
417 h+='<option value="'+esc(n)+'">'+esc(n)+'</option>';
418 });
419 }
420 s.innerHTML=h;
421 // 保留当前选中的站点;首次进入时才默认选中第一个站点
422 var keep=MNBT.site&&Array.prototype.some.call(s.options,function(o){return o.value===MNBT.site;});
423 if(keep){s.value=MNBT.site;}
424 else if(first){s.value=first;MNBT.site=first;MNBT.reload();}
425 else{MNBT.site='';}
426 },'json');
427 },
428
429 _toggleSwitch:function(el){layer.msg(el.checked?'已开启':'已关闭',{time:1200});},
430
431 reload:function(){
432 var site=document.getElementById('ssSiteSelect').value;
433 if(!site){document.getElementById('ssSkel').classList.remove('show');document.getElementById('ssContent').style.display='none';return;}
434 MNBT.site=site; MNBT.range=MNBT._getRange();
435 document.getElementById('ssSkel').classList.add('show');document.getElementById('ssContent').style.display='none';
436 $.post('/plugin?action=a&name=mnbt_connector&s=get_site_overview',{site:site,range:MNBT.range},function(r){
437 MNBT.cache=r;
438 // 先显示容器,再初始化图表,否则 ECharts 在隐藏容器中无法获取尺寸
439 document.getElementById('ssSkel').classList.remove('show');document.getElementById('ssContent').style.display='block';
440 if(MNBT.curPage==='overview'){
441 MNBT._cards(r.data||{},r.comparison||{});
442 MNBT._initCharts(function(){
443 var sd=document.getElementById('sevenChart');
444 if(MNBT.sevenChart)MNBT.sevenChart.dispose();
445 MNBT.sevenChart=window.echarts.init(sd);
446 MNBT._loadTrendData();
447 });
448 }else{
449 MNBT._loadTable(MNBT.curPage,1);
450 }
451 },'json').fail(function(){document.getElementById('ssSkel').classList.remove('show');document.getElementById('ssContent').style.display='block';});
452 },
453 _getRange:function(){var a=document.querySelector('#rangeBtns .btns-btn.on');return a?a.getAttribute('data-v'):'7d';},
454
455 switchPage:function(name,el){
456 $('#subNav .btns-btn').removeClass('on');if(el)el.classList.add('on');
457 MNBT.curPage=name;
458 PAGERS.forEach(function(id){var p=document.getElementById(id);if(p)p.style.display='none';});
459 $('.pp').removeClass('active');
460 $('#pp-'+name).addClass('active');
461 if(!MNBT.site)return;
462 if(name==='overview'){
463 // 切回概览时重新请求(避免缓存与当前时间范围不同步)
464 var self=this;
465 $.post('/plugin?action=a&name=mnbt_connector&s=get_site_overview',{site:MNBT.site,range:MNBT.range},function(r){
466 if(!r||!r.status)return;
467 MNBT.cache=r;
468 MNBT._cards(r.data||{},r.comparison||{});
469 MNBT._initCharts(function(){
470 var sd=document.getElementById('sevenChart');
471 if(MNBT.sevenChart)MNBT.sevenChart.dispose();
472 MNBT.sevenChart=window.echarts.init(sd);
473 MNBT._loadTrendData();
474 });
475 },'json');
476 }else{
477 MNBT._loadTable(name,1);
478 }
479 },
480
481 _cards:function(d,c){
482 var fields=[
483 {k:'total_bytes',l:'流量',f:F.bytes},{k:'requests',l:'请求数',f:F.num},
484 {k:'ip_count',l:'IP',f:F.num},{k:'uv',l:'UV',f:F.num},
485 {k:'pv',l:'PV',f:F.num},{k:'error_count',l:'错误请求',f:F.num},
486 {k:'spider_count',l:'蜘蛛请求',f:F.num}
487 ];
488 var h='';
489 fields.forEach(function(f){
490 var v=d[f.k]!=null?f.f(d[f.k]):'-',comp='';
491 if(c&&c.prev){
492 var pv=c.prev[f.k]||0,cv=d[f.k]||0,cls='neutral';
493 if(cv>pv)cls='up';else if(cv<pv)cls='down';
494 comp='<span class="'+cls+'">昨日 '+f.f(pv)+'</span>';
495 if(c.prev2)comp+='<br>前日 '+f.f(c.prev2[f.k]||0);
496 }
497 h+='<div class="stat-card"><div class="stat-label"><span class="dot breath"></span>'+f.l+'</div><div class="stat-val">'+v+'</div><div class="stat-comp">'+comp+'</div></div>';
498 });
499 h+='<div class="stat-rf"><a href="javascript:;" onclick="MNBT.reload()" style="font-size:12px;color:var(--green);text-decoration:none;">刷新</a></div>';
500 document.getElementById('ssStatsCards').innerHTML=h;
501 },
502
503 _initCharts:function(cb){if(window.echarts){cb();return;}$.ajax({url:'/static/js/echarts.min.js',dataType:'script',cache:true}).done(cb);},
504 // 重新渲染趋势图表
505 _renderTrend:function(data){
506 var self=MNBT,list=(data||[]).sort(function(a,b){return a.time>b.time?1:-1;}),isT=self.metricType==='traffic';
507 var map={traffic:'流量',pv:'PV',uv:'UV',ip:'IP'};
508 var vals=list.map(function(d){
509 var v=d[self.metricType]!==undefined?d[self.metricType]:(d.pv||0);
510 if(self.metricType==='traffic')v=v||d.total_bytes||0;
511 return v;
512 });
513 self.sevenChart.setOption({
514 tooltip:{trigger:'axis',formatter:function(p){var d=p[0];return d.name+'<br/>'+d.seriesName+': '+(isT?F.bytes(d.value):F.num(d.value));}},
515 grid:{left:'3%',right:'5%',top:'8%',bottom:'3%',containLabel:true},
516 xAxis:{type:'category',data:list.map(function(d){return d.time||'';}),axisLabel:{color:'#888',fontSize:10,rotate:45}},
517 yAxis:{type:'value',axisLabel:{formatter:function(v){return isT?F.bytes(v):F.num(v);},color:'#888',fontSize:11}},
518 series:[{name:map[self.metricType]||self.metricType,type:'line',smooth:true,symbol:'circle',symbolSize:8,itemStyle:{color:'#36b370'},lineStyle:{color:'#36b370',width:3},areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(54,179,112,0.25)'},{offset:1,color:'rgba(54,179,112,0.02)'}])},data:vals,label:{show:false}}]
519 });
520 },
521 _loadTrendData:function(){
522 $.post('/plugin?action=a&name=mnbt_connector&s=get_site_trend',{site:MNBT.site,range:MNBT.range,page:1,page_size:1000},function(r){
523 MNBT._renderTrend(r.data||[]);
524 },'json');
525 },
526
527 // Unified table loader
528 _tableCfgs:{
529 spider:{url:'/plugin?action=a&name=mnbt_connector&s=get_site_spider_analysis',bodyId:'ssSpiderBody',pagerId:'ssSpiderPager',rowFn:function(r,i,t){var pct=F.pct(r.count,t);return'<tr><td>'+esc(r.spider||'-')+'</td><td style="text-align:center;">'+F.num(r.count||0)+'</td><td style="text-align:center;">'+pct+'<div class="bar-wrap"><div class="bar" style="width:'+Math.min(parseFloat(pct),100)+'%"></div></div></td></tr>';}},
530 client:{url:'/plugin?action=a&name=mnbt_connector&s=get_site_client_stats',bodyId:'ssClientBody',pagerId:'ssClientPager',rowFn:function(r,i,t){var pct=F.pct(r.count,t);return'<tr><td>'+esc(r.client||r.os||'-')+'</td><td style="text-align:center;">'+F.num(r.count||0)+'</td><td style="text-align:center;">'+pct+'<div class="bar-wrap"><div class="bar" style="width:'+Math.min(parseFloat(pct),100)+'%"></div></div></td></tr>';}},
531 method:{url:'/plugin?action=a&name=mnbt_connector&s=get_site_method_stats',bodyId:'ssMethodBody',pagerId:'ssMethodPager',rowFn:function(r,i,t){var pct=F.pct(r.count,t);return'<tr><td>'+esc(r.method||'-')+'</td><td style="text-align:center;">'+F.num(r.count||0)+'</td><td style="text-align:center;">'+pct+'<div class="bar-wrap"><div class="bar" style="width:'+Math.min(parseFloat(pct),100)+'%"></div></div></td></tr>';}},
532 iprank:{url:'/plugin?action=a&name=mnbt_connector&s=get_site_ip_rank',bodyId:'ssIpRankBody',pagerId:'ssIpPager',rowFn:function(r,i,t,res){var pg=res.page||1,ps=res.page_size||MNBT.pageSize,sr=(pg-1)*ps+i+1,pct=F.pct(r.count,t);return'<tr><td>'+sr+'</td><td style="font-family:monospace;">'+esc(r.ip||'-')+'</td><td style="color:var(--t3);font-size:11px;">-</td><td style="color:var(--t3);font-size:11px;">-</td><td style="text-align:center;">'+F.num(r.count||0)+'</td><td style="text-align:center;">'+F.bytes(r.bytes||0)+'</td><td style="text-align:center;">'+pct+'<div class="bar-wrap"><div class="bar" style="width:'+Math.min(parseFloat(pct),100)+'%"></div></div></td></tr>';}},
533 urirank:{url:'/plugin?action=a&name=mnbt_connector&s=get_site_uri_rank',bodyId:'ssUriRankBody',pagerId:'ssUriPager',rowFn:function(r,i,t,res){var pg=res.page||1,ps=res.page_size||MNBT.pageSize,sr=(pg-1)*ps+i+1,pct=F.pct(r.count,t);return'<tr><td>'+sr+'</td><td style="word-break:break-all;font-family:monospace;font-size:11px;">'+esc(r.uri||'-')+'</td><td style="text-align:center;">'+F.num(r.count||0)+'</td><td style="text-align:center;">'+F.bytes(r.bytes||0)+'</td><td style="text-align:center;">'+pct+'<div class="bar-wrap"><div class="bar" style="width:'+Math.min(parseFloat(pct),100)+'%"></div></div></td></tr>';}},
534 errors:{url:'/plugin?action=a&name=mnbt_connector&s=get_site_error_logs',bodyId:'ssErrorBody',pagerId:'ssErrorPager',rowFn:function(r){var bc=r.status>=500?'badge-danger':'badge-warning';return'<tr><td style="font-size:11px;">'+esc(r.time||'-')+'</td><td style="font-family:monospace;font-size:11px;">'+esc(r.ip||'-')+'</td><td>'+esc(r.method||'-')+'</td><td style="word-break:break-all;font-size:11px;">'+esc(r.uri||'-')+'</td><td style="text-align:center;"><span class="badge '+bc+'">'+(r.status||'-')+'</span></td><td style="text-align:center;">'+F.bytes(r.bytes||0)+'</td></tr>';}},
535 recent:{custom:function(p){p=p||1;$.post('/plugin?action=a&name=mnbt_connector&s=get_site_recent_logs',{site:MNBT.site,page:p,page_size:MNBT.pageSize},function(r){var b=document.getElementById('ssRecentLog');if(!r||r.status===false){b.textContent=r&&r.msg?r.msg:'读取日志失败';document.getElementById('ssRecentPager').style.display='none';return;}if(!r.data||!r.data.length){b.textContent='暂无日志';document.getElementById('ssRecentPager').style.display='none';return;}var pg=parseInt(r.page)||p,ps=parseInt(r.page_size)||MNBT.pageSize,totalAll=parseInt(r.total)||r.data.length,data=r.data;if(totalAll>ps&&data.length>ps){var offset=(pg-1)*ps;data=data.slice(offset,offset+ps);}b.textContent=data.map(function(x){if(typeof x==='string')return x;if(x.raw)return x.raw;return'['+(x.time||'')+'] '+(x.ip||'')+' '+(x.method||'')+' '+(x.uri||'')+' '+(x.status||'')+' '+F.bytes(x.bytes||0)+' "'+(x.ua||'')+'"';}).join('\n');MNBT._pager('ssRecentPager',totalAll,pg,ps);},'json');}}
536 },
537 _loadTable:function(name,p){
538 p=p||1;
539 var cfg=MNBT._tableCfgs[name];if(!cfg)return;
540 if(cfg.custom){cfg.custom(p);return;}
541 var extra={};
542 if(name==='errors'){var fs=document.getElementById('ssErrFilter');if(fs)extra.min_status=parseInt(fs.value)||400;}
543 // 确保 page 和 page_size 以整数形式发送
544 var reqData = $.extend({}, {site:MNBT.site, range:MNBT.range}, extra);
545 reqData.page = p;
546 reqData.page_size = MNBT.pageSize;
547
548 $.post(cfg.url, reqData, function(r){
549 var b=document.getElementById(cfg.bodyId);
550 if(!r||r.status===false){b.innerHTML='<tr><td colspan="99" class="empty">'+esc(r&&r.msg?r.msg:'接口请求失败')+'</td></tr>';document.getElementById(cfg.pagerId).style.display='none';return;}
551 if(r.data&&!Array.isArray(r.data)){
552 if(r.data.pc||r.data.mobile){
553 var arr=[];
554 ['pc','mobile'].forEach(function(type){var g=r.data[type]||{},list=g.browsers||[];list.forEach(function(x){arr.push({client:(x.name||x.browser||'-')+'('+type+')',os:x.name||x.browser||'-',count:x.count||x.total||0});});});
555 r.sum_count=(r.data.pc&&r.data.pc.total||0)+(r.data.mobile&&r.data.mobile.total||0);r.data=arr;
556 }else r.data=[];
557 }
558 if(!r.data||!r.data.length){b.innerHTML='<tr><td colspan="99" class="empty">'+esc(r.msg||'暂无数据')+'</td></tr>';document.getElementById(cfg.pagerId).style.display='none';return;}
559 // 确保分页生效:若后端返回全量,前端切片
560 var pageFromResp = parseInt(r.page) || p;
561 var psFromResp = parseInt(r.page_size) || MNBT.pageSize;
562 var totalFromResp = parseInt(r.total) || r.data.length;
563 var displayData = r.data;
564 // 如果 total 大于 page_size 但返回了全部数据,前端切片
565 if (totalFromResp > psFromResp && r.data.length > psFromResp) {
566 var offset = (pageFromResp - 1) * psFromResp;
567 displayData = r.data.slice(offset, offset + psFromResp);
568 }
569 var totalFromAll = parseFloat(r.sum_count)||r.data.reduce(function(s,x){return s+(x.count||0);},0);
570 b.innerHTML = displayData.map(function(x,i){return cfg.rowFn(x,i,totalFromAll,r);}).join('');
571 MNBT._pager(cfg.pagerId, totalFromResp, pageFromResp, psFromResp);
572 },'json');
573 },
574
575 _pager:function(id,total,page,ps){
576 var el=document.getElementById(id);if(!el)return;
577 var tp=Math.ceil(total/ps);
578 if(tp<=1){el.style.display='none';return;}
579 el.style.display='flex';
580 var fn='MNBT._loadTable(\''+MNBT.curPage+'\',';
581 var h='<button class="pager-btn"'+(page<=1?' disabled':' onclick="'+fn+(page-1)+')"')+'>‹</button>';
582 var ms=7,hf=Math.floor(ms/2),s=Math.max(1,page-hf),e=Math.min(tp,s+ms-1);
583 if(e-s<ms-1)s=Math.max(1,e-ms+1);
584 if(s>1)h+='<button class="pager-btn" onclick="'+fn+'1)">1</button>';
585 if(s>2)h+='<span class="pager-info">…</span>';
586 for(var i=s;i<=e;i++)h+='<button class="pager-btn'+(i===page?' on':'')+'" onclick="'+fn+i+')">'+i+'</button>';
587 if(e<tp-1)h+='<span class="pager-info">…</span>';
588 if(e<tp)h+='<button class="pager-btn" onclick="'+fn+tp+')">'+tp+'</button>';
589 h+='<button class="pager-btn"'+(page>=tp?' disabled':' onclick="'+fn+(page+1)+')"')+'>›</button>';
590 h+='<span class="pager-info">共 '+total+' 条</span>';
591 h+='<input type="number" min="1" max="'+tp+'" id="'+id+'Jump" value="'+page+'">';
592 h+='<span class="pager-info">/ '+tp+' 页</span>';
593 h+='<button class="pager-go" onclick="var v=parseInt(document.getElementById(\''+id+'Jump\').value)||1;if(v>=1&&v<='+tp+')'+fn+'v)">跳转</button>';
594 el.innerHTML=h;
595 }
596};
597
598$(function(){
599 $(document).on('click','#rangeBtns .btns-btn',function(){var v=$(this).data('v');$('#rangeBtns .btns-btn').removeClass('on');$(this).addClass('on');MNBT.range=v;if(MNBT.site)MNBT.reload();});
600 $(document).on('click','#metricType .btns-btn',function(){var v=$(this).data('v');$('#metricType .btns-btn').removeClass('on');$(this).addClass('on');MNBT.metricType=v;if(MNBT.sevenChart)MNBT._loadTrendData();});
601 $('select[id="ssPageSize"]').on('change',function(){MNBT.pageSize=parseInt(this.value)||10;PAGERS.forEach(function(id){var p=document.getElementById(id);if(p)p.style.display='none';});MNBT._loadTable(MNBT.curPage,1);});
602
603 $(document).on('click','#mnbtLogAutoRefresh',function(){if(this.checked)MNBT.loadLogs();});
604 setInterval(function(){var el=document.getElementById('mnbtLogAutoRefresh');if(el&&el.checked)MNBT.loadLogs();},5000);
605
606 $(".layui-layer-page").css({width:"1000px",height:"820px",top:($(window).height()-820)/2+"px"});
607 MNBT.status();setInterval(MNBT.status,30000);
608 $(window).resize(function(){if(MNBT.sevenChart)MNBT.sevenChart.resize();});
609});
610})();
611</script>
612</body>
613</html>