Utoljára aktív 1757414721

network-fluctuation.js Eredeti
1const selectorButton =
2 "#root > div > main > div.mx-auto.w-full.max-w-5xl.px-0.flex.flex-col.gap-4.server-info > section > div.flex.justify-center.w-full.max-w-\\[200px\\] > div > div > div.relative.cursor-pointer.rounded-3xl.px-2\\.5.py-\\[8px\\].text-\\[13px\\].font-\\[600\\].transition-all.duration-500.text-stone-400.dark\\:text-stone-500";
3const selectorSection =
4 "#root > div > main > div.mx-auto.w-full.max-w-5xl.px-0.flex.flex-col.gap-4.server-info > section";
5const selector3 =
6 "#root > div > main > div.mx-auto.w-full.max-w-5xl.px-0.flex.flex-col.gap-4.server-info > div:nth-child(3)";
7const selector4 =
8 "#root > div > main > div.mx-auto.w-full.max-w-5xl.px-0.flex.flex-col.gap-4.server-info > div:nth-child(4)";
9
10let hasClicked = false;
11let divVisible = false;
12let swapping = false;
13
14function forceBothVisible() {
15 const div3 = document.querySelector(selector3);
16 const div4 = document.querySelector(selector4);
17 if (div3 && div4) {
18 div3.style.display = "block";
19 div4.style.display = "block";
20 }
21}
22
23function hideSection() {
24 const section = document.querySelector(selectorSection);
25 if (section) {
26 section.style.display = "none";
27 }
28}
29
30function tryClickButton() {
31 const btn = document.querySelector(selectorButton);
32 if (btn && !hasClicked) {
33 btn.click();
34 hasClicked = true;
35 setTimeout(forceBothVisible, 500);
36 }
37}
38
39function swapDiv3AndDiv4() {
40 if (swapping) return;
41 swapping = true;
42
43 const div3 = document.querySelector(selector3);
44 const div4 = document.querySelector(selector4);
45 if (!div3 || !div4) {
46 swapping = false;
47 return;
48 }
49 const parent = div3.parentNode;
50 if (parent !== div4.parentNode) {
51 swapping = false;
52 return;
53 }
54
55 // 交换 div3 和 div4 的位置
56 parent.insertBefore(div4, div3);
57 parent.insertBefore(div3, div4.nextSibling);
58
59 swapping = false;
60}
61
62const observer = new MutationObserver(() => {
63 const div3 = document.querySelector(selector3);
64 const div4 = document.querySelector(selector4);
65
66 const isDiv3Visible = div3 && getComputedStyle(div3).display !== "none";
67 const isDiv4Visible = div4 && getComputedStyle(div4).display !== "none";
68
69 const isAnyDivVisible = isDiv3Visible || isDiv4Visible;
70
71 if (isAnyDivVisible && !divVisible) {
72 hideSection();
73 tryClickButton();
74 setTimeout(swapDiv3AndDiv4, 100);
75 } else if (!isAnyDivVisible && divVisible) {
76 hasClicked = false;
77 }
78
79 divVisible = isAnyDivVisible;
80
81 if (div3 && div4) {
82 if (!isDiv3Visible || !isDiv4Visible) {
83 forceBothVisible();
84 }
85 }
86});
87
88const root = document.querySelector("#root");
89if (root) {
90 observer.observe(root, {
91 childList: true,
92 attributes: true,
93 subtree: true,
94 attributeFilter: ["style", "class"],
95 });
96}
97
style.css Eredeti
1/* 默认和浅色主题的CSS变量 */
2:root,
3html.light {
4 --custom-border-color: rgba(244, 244, 244, 0.382);
5 --custom-background-color: rgba(244, 244, 244, 0.618);
6 --custom-text-color: #333;
7 --custom-shadow-color: rgba(244, 244, 244, 0.5);
8}
9
10/* 暗色主题的CSS变量 */
11html.dark {
12 --custom-border-color: rgba(11, 11, 11, 0.382);
13 --custom-background-color: rgba(11, 11, 11, 0.618);
14 --custom-text-color: #fff;
15 --custom-shadow-color: rgba(11, 11, 11, 0.5);
16}
17
18html.dark #root {
19 background-color: unset !important;
20}
21
22/* 背景图片压暗和模糊, 开了背景图建议开启 */
23.dark .bg-cover::after {
24 content: "";
25 position: absolute;
26 inset: 0;
27 backdrop-filter: blur(6px);
28 background-color: rgba(0, 0, 0, 0.3);
29}
30
31.light .bg-cover::after {
32 content: "";
33 position: absolute;
34 inset: 0;
35 backdrop-filter: blur(3px);
36 background-color: rgba(255, 255, 255, 0.3);
37}
38
39.bg-card,
40html.dark .bg-card,
41html.light .bg-card {
42 background-color: var(--custom-background-color);
43 backdrop-filter: blur(4px);
44 border: 1px solid var(--custom-border-color);
45 box-shadow: 0 4px 6px var(--custom-shadow-color);
46}
47
48.focus\:text-accent-foreground:focus {
49 background-color: var(--custom-background-color);
50 opacity: 0.8;
51}
52
53.text-muted-foreground,
54html.dark .text-muted-foreground,
55html.light .text-muted-foreground {
56 color: var(--custom-text-color);
57}
58
59html.dark .dark\:bg-stone-700,
60html.light .bg-stone-700,
61.bg-stone-700 {
62 --tw-bg-opacity: 0.5;
63 background-color: var(--custom-background-color);
64}
65
66html *,
67html.dark *,
68html.light * {
69 border-color: var(--custom-border-color);
70}
71
72html body,
73html.dark body,
74html.light body {
75 color: var(--custom-text-color);
76 background: unset;
77 position: relative;
78}
79
80img {
81 border: none;
82}
83
84html.dark .dark\:border-neutral-800,
85html.light .border-neutral-800,
86.border-neutral-800 {
87 border-color: var(--custom-border-color);
88}
89
90html.dark .dark\:bg-stone-800,
91html.light .bg-stone-800,
92.bg-stone-800 {
93 --tw-bg-opacity: 0.5;
94 background-color: var(--custom-background-color);
95}
96
97html.dark .dark\:text-stone-500,
98html.light .text-stone-500,
99.text-stone-500 {
100 color: var(--custom-text-color);
101}
102
103.bg-secondary,
104html.dark .bg-secondary,
105html.light .bg-secondary {
106 background-color: var(--custom-background-color);
107 opacity: 0.7;
108}
109
110.bg-popover,
111html.dark .bg-popover,
112html.light .bg-popover {
113 background-color: var(--custom-background-color);
114}
115
116.bg-muted,
117html.dark .bg-muted,
118html.light .bg-muted {
119 background-color: var(--custom-border-color);
120}
121
122html.dark .dark\:bg-black,
123html.light .bg-black,
124.bg-black {
125 background-color: var(--custom-border-color);
126}
127
128.bg-border,
129html.dark .bg-border,
130html.light .bg-border {
131 background-color: var(--custom-border-color);
132}
133
134.border-input,
135html.dark .border-input,
136html.light .border-input {
137 border-color: var(--custom-border-color);
138}
139
140html.dark .dark\:text-neutral-300\/50,
141html.light .text-neutral-300,
142.text-neutral-300 {
143 color: var(--custom-text-color);
144 opacity: 0.7;
145}
146
147html.dark .dark\:text-stone-400,
148html.light .text-stone-400,
149.text-stone-400 {
150 color: var(--custom-text-color);
151}
152
153div#radix-\:r4\: {
154 background: var(--custom-background-color);
155 backdrop-filter: blur(4px);
156}
157
158.text-green-600 {
159 color: rgb(34, 197, 94);
160}
161
162.bg-green-600 {
163 background-color: rgb(34, 197, 94);
164}
165
166.vps-info,
167html.dark .vps-info,
168html.light .vps-info {
169 border-radius: 12px;
170 padding: 12px;
171 background-color: var(--custom-background-color);
172 backdrop-filter: blur(4px);
173}
174
175.font-medium.opacity-40,
176html.dark .font-medium.opacity-40,
177html.light .font-medium.opacity-40 {
178 opacity: 0.8 !important;
179}
180
181.font-medium.opacity-50,
182html.dark .font-medium.opacity-50,
183html.light .font-medium.opacity-50 {
184 opacity: 0.8 !important;
185}
186
187.max-w-5xl.gap-4 > div:first-child,
188html.dark .max-w-5xl.gap-4 > div:first-child,
189html.light .max-w-5xl.gap-4 > div:first-child {
190 background-color: var(--custom-background-color);
191 backdrop-filter: blur(4px);
192 border: 1px solid var(--custom-border-color);
193 box-shadow: 0 4px 6px var(--custom-shadow-color);
194 border-radius: 12px;
195 padding: 12px;
196}
197
198img[alt="BackIcon"] {
199 margin-right: 12px;
200}
201
202.flex.items-center.gap-1.rounded-\[50px\].bg-stone-100.p-\[3px\],
203html.dark .flex.items-center.gap-1.rounded-\[50px\].bg-stone-100.p-\[3px\],
204html.light .flex.items-center.gap-1.rounded-\[50px\].bg-stone-100.p-\[3px\],
205html.dark .dark\:bg-stone-800 {
206 background-color: var(--custom-background-color);
207 backdrop-filter: blur(4px);
208}
209
210.\[\&_\.recharts-cartesian-axis-tick_text\]\:fill-muted-foreground
211 .recharts-cartesian-axis-tick
212 text {
213 fill: var(--custom-text-color);
214}
215
216html.dark .dark\:fill-neutral-800,
217html.light .fill-neutral-800,
218.fill-neutral-800 {
219 fill: var(--custom-text-color);
220}
221
222.data-\[state\=unchecked\]\:bg-input[data-state="unchecked"] {
223 background-color: var(--custom-background-color);
224 backdrop-filter: blur(4px);
225}
226
227.data-\[state\=checked\]\:bg-primary[data-state="checked"] {
228 background-color: var(--custom-background-color);
229 backdrop-filter: blur(4px);
230}
231
232.rounded-lg {
233 background-color: var(--custom-background-color) !important;
234 backdrop-filter: blur(5px) !important;
235 border-radius: 15px !important;
236 border: 1px solid var(--custom-border-color) !important;
237}
238
239html.dark .dark\:bg-black\/70,
240html.light .bg-black\/70,
241.bg-black\/70 {
242 background-color: var(--custom-background-color) !important;
243 backdrop-filter: blur(5px) !important;
244 border: 1px solid var(--custom-border-color) !important;
245}
traffic-progress-bar.js Eredeti
1const SCRIPT_VERSION = "v20250617";
2// == 样式注入模块 ==
3// 注入自定义CSS隐藏特定元素
4function injectCustomCSS() {
5 const style = document.createElement("style");
6 style.textContent = `
7 /* 隐藏父级类名为 mt-4 w-full mx-auto 下的所有 div */
8 .mt-4.w-full.mx-auto > div {
9 display: none;
10 }
11 `;
12 document.head.appendChild(style);
13}
14injectCustomCSS();
15
16// == 工具函数模块 ==
17const utils = (() => {
18 /**
19 * 格式化文件大小,自动转换单位
20 * @param {number} bytes - 字节数
21 * @returns {{value: string, unit: string}} 格式化后的数值和单位
22 */
23 function formatFileSize(bytes) {
24 if (bytes === 0) return { value: "0", unit: "B" };
25 const units = ["B", "KB", "MB", "GB", "TB", "PB"];
26 let size = bytes;
27 let unitIndex = 0;
28 while (size >= 1024 && unitIndex < units.length - 1) {
29 size /= 1024;
30 unitIndex++;
31 }
32 return {
33 value: size.toFixed(unitIndex === 0 ? 0 : 2),
34 unit: units[unitIndex],
35 };
36 }
37
38 /**
39 * 计算百分比,输入可为大数,支持自动缩放
40 * @param {number} used - 已使用量
41 * @param {number} total - 总量
42 * @returns {string} 百分比字符串,保留2位小数
43 */
44 function calculatePercentage(used, total) {
45 used = Number(used);
46 total = Number(total);
47 // 大数缩放,防止数值溢出
48 if (used > 1e15 || total > 1e15) {
49 used /= 1e10;
50 total /= 1e10;
51 }
52 return total === 0 ? "0.00" : ((used / total) * 100).toFixed(2);
53 }
54
55 /**
56 * 格式化日期字符串,返回 yyyy-MM-dd 格式
57 * @param {string} dateString - 日期字符串
58 * @returns {string} 格式化日期
59 */
60 function formatDate(dateString) {
61 const date = new Date(dateString);
62 if (isNaN(date)) return "";
63 return date.toLocaleDateString("zh-CN", {
64 year: "numeric",
65 month: "2-digit",
66 day: "2-digit",
67 });
68 }
69
70 /**
71 * 安全设置子元素文本内容,避免空引用错误
72 * @param {HTMLElement} parent - 父元素
73 * @param {string} selector - 子元素选择器
74 * @param {string} text - 要设置的文本
75 */
76 function safeSetTextContent(parent, selector, text) {
77 const el = parent.querySelector(selector);
78 if (el) el.textContent = text;
79 }
80
81 /**
82 * 根据百分比返回渐变HSL颜色(绿→橙→红)
83 * @param {number} percentage - 0~100的百分比
84 * @returns {string} hsl颜色字符串
85 */
86 function getHslGradientColor(percentage) {
87 const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
88 const lerp = (start, end, t) => start + (end - start) * t;
89 const p = clamp(Number(percentage), 0, 100);
90 let h, s, l;
91
92 if (p <= 35) {
93 const t = p / 35;
94 h = lerp(142, 32, t); // 绿色到橙色
95 s = lerp(69, 85, t);
96 l = lerp(45, 55, t);
97 } else if (p <= 85) {
98 const t = (p - 35) / 50;
99 h = lerp(32, 0, t); // 橙色到红色
100 s = lerp(85, 75, t);
101 l = lerp(55, 50, t);
102 } else {
103 const t = (p - 85) / 15;
104 h = 0; // 红色加深
105 s = 75;
106 l = lerp(50, 45, t);
107 }
108 return `hsl(${h.toFixed(0)}, ${s.toFixed(0)}%, ${l.toFixed(0)}%)`;
109 }
110
111 /**
112 * 透明度渐隐渐现切换内容
113 * @param {HTMLElement} element - 目标元素
114 * @param {string} newContent - 新HTML内容
115 * @param {number} duration - 动画持续时间,毫秒
116 */
117 function fadeOutIn(element, newContent, duration = 500) {
118 element.style.transition = `opacity ${duration / 2}ms`;
119 element.style.opacity = "0";
120 setTimeout(() => {
121 element.innerHTML = newContent;
122 element.style.transition = `opacity ${duration / 2}ms`;
123 element.style.opacity = "1";
124 }, duration / 2);
125 }
126
127 return {
128 formatFileSize,
129 calculatePercentage,
130 formatDate,
131 safeSetTextContent,
132 getHslGradientColor,
133 fadeOutIn,
134 };
135})();
136
137// == 流量统计渲染模块 ==
138const trafficRenderer = (() => {
139 const toggleElements = []; // 存储需周期切换显示的元素及其内容
140
141 /**
142 * 渲染流量统计条目
143 * @param {Object} trafficData - 后台返回的流量数据
144 * @param {Object} config - 配置项
145 */
146 function renderTrafficStats(trafficData, config) {
147 const serverMap = new Map();
148
149 // 解析流量数据,按服务器名聚合
150 for (const cycleId in trafficData) {
151 const cycle = trafficData[cycleId];
152 if (!cycle.server_name || !cycle.transfer) continue;
153 for (const serverId in cycle.server_name) {
154 const serverName = cycle.server_name[serverId];
155 const transfer = cycle.transfer[serverId];
156 const max = cycle.max;
157 const from = cycle.from;
158 const to = cycle.to;
159 const next_update = cycle.next_update[serverId];
160 if (serverName && transfer !== undefined && max && from && to) {
161 serverMap.set(serverName, {
162 id: serverId,
163 transfer,
164 max,
165 name: cycle.name,
166 from,
167 to,
168 next_update,
169 });
170 }
171 }
172 }
173
174 serverMap.forEach((serverData, serverName) => {
175 // 查找对应显示区域
176 const targetElement = Array.from(
177 document.querySelectorAll("section.grid.items-center.gap-2"),
178 ).find((section) => {
179 const firstText = section.querySelector("p")?.textContent.trim();
180 return firstText === serverName.trim();
181 });
182 if (!targetElement) return;
183
184 // 格式化数据
185 const usedFormatted = utils.formatFileSize(serverData.transfer);
186 const totalFormatted = utils.formatFileSize(serverData.max);
187 const percentage = utils.calculatePercentage(
188 serverData.transfer,
189 serverData.max,
190 );
191 const fromFormatted = utils.formatDate(serverData.from);
192 const toFormatted = utils.formatDate(serverData.to);
193 const nextUpdateFormatted = new Date(
194 serverData.next_update,
195 ).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
196 const uniqueClassName = "traffic-stats-for-server-" + serverData.id;
197 const progressColor = utils.getHslGradientColor(percentage);
198 const containerDiv = targetElement.closest("div");
199 if (!containerDiv) return;
200
201 // 日志输出函数
202 const log = (...args) => {
203 if (config.enableLog) console.log("[renderTrafficStats]", ...args);
204 };
205
206 // 查找是否已有对应流量条目元素
207 const existing = Array.from(
208 containerDiv.querySelectorAll(".new-inserted-element"),
209 ).find((el) => el.classList.contains(uniqueClassName));
210
211 if (!config.showTrafficStats) {
212 // 不显示时移除对应元素
213 if (existing) {
214 existing.remove();
215 log(`移除流量条目: ${serverName}`);
216 }
217 return;
218 }
219
220 if (existing) {
221 // 更新已存在元素内容
222 utils.safeSetTextContent(
223 existing,
224 ".used-traffic",
225 usedFormatted.value,
226 );
227 utils.safeSetTextContent(existing, ".used-unit", usedFormatted.unit);
228 utils.safeSetTextContent(
229 existing,
230 ".total-traffic",
231 totalFormatted.value,
232 );
233 utils.safeSetTextContent(existing, ".total-unit", totalFormatted.unit);
234 utils.safeSetTextContent(existing, ".from-date", fromFormatted);
235 utils.safeSetTextContent(existing, ".to-date", toFormatted);
236 utils.safeSetTextContent(
237 existing,
238 ".percentage-value",
239 percentage + "%",
240 );
241 utils.safeSetTextContent(
242 existing,
243 ".next-update",
244 `next update: ${nextUpdateFormatted}`,
245 );
246
247 const progressBar = existing.querySelector(".progress-bar");
248 if (progressBar) {
249 progressBar.style.width = percentage + "%";
250 progressBar.style.backgroundColor = progressColor;
251 }
252 log(`更新流量条目: ${serverName}`);
253 } else {
254 // 插入新的流量条目元素
255 let oldSection = null;
256 if (config.insertAfter) {
257 oldSection =
258 containerDiv.querySelector(
259 "section.flex.items-center.w-full.justify-between.gap-1",
260 ) || containerDiv.querySelector("section.grid.items-center.gap-3");
261 } else {
262 oldSection = containerDiv.querySelector(
263 "section.grid.items-center.gap-3",
264 );
265 }
266 if (!oldSection) return;
267
268 // 时间区间内容,用于切换显示
269 const defaultTimeInfoHTML = `<span class="from-date">${fromFormatted}</span>
270 <span class="text-neutral-500 dark:text-neutral-400">-</span>
271 <span class="to-date">${toFormatted}</span>`;
272 const contents = [
273 defaultTimeInfoHTML,
274 `<span class="text-[10px] font-medium text-neutral-800 dark:text-neutral-200 percentage-value">${percentage}%</span>`,
275 `<span class="text-[10px] font-medium text-neutral-600 dark:text-neutral-300">${nextUpdateFormatted}</span>`,
276 ];
277
278 const newElement = document.createElement("div");
279 newElement.classList.add(
280 "space-y-1.5",
281 "new-inserted-element",
282 uniqueClassName,
283 );
284 newElement.style.width = "100%";
285 newElement.innerHTML = `
286 <div class="flex items-center justify-between">
287 <div class="flex items-baseline gap-1">
288 <span class="text-[10px] font-medium text-neutral-800 dark:text-neutral-200 used-traffic">${usedFormatted.value}</span>
289 <span class="text-[10px] font-medium text-neutral-800 dark:text-neutral-200 used-unit">${usedFormatted.unit}</span>
290 <span class="text-[10px] text-neutral-500 dark:text-neutral-400">/ </span>
291 <span class="text-[10px] text-neutral-500 dark:text-neutral-400 total-traffic">${totalFormatted.value}</span>
292 <span class="text-[10px] text-neutral-500 dark:text-neutral-400 total-unit">${totalFormatted.unit}</span>
293 </div>
294 <div class="text-[10px] font-medium text-neutral-600 dark:text-neutral-300 time-info" style="opacity:1; transition: opacity 0.3s;">
295 ${defaultTimeInfoHTML}
296 </div>
297 </div>
298 <div class="relative h-1.5">
299 <div class="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full"></div>
300 <div class="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300 progress-bar" style="width: ${percentage}%; max-width: 100%; background-color: ${progressColor};"></div>
301 </div>
302 `;
303
304 oldSection.after(newElement);
305 log(`插入新流量条目: ${serverName}`);
306
307 // 启用切换时,将元素及其内容保存以便周期切换
308 if (config.toggleInterval > 0) {
309 const timeInfoElement = newElement.querySelector(".time-info");
310 if (timeInfoElement) {
311 toggleElements.push({
312 el: timeInfoElement,
313 contents,
314 });
315 }
316 }
317 }
318 });
319 }
320
321 /**
322 * 启动周期切换内容显示(用于时间、百分比等轮播)
323 * @param {number} toggleInterval - 切换间隔,毫秒
324 * @param {number} duration - 动画时长,毫秒
325 */
326 function startToggleCycle(toggleInterval, duration) {
327 if (toggleInterval <= 0) return;
328 let toggleIndex = 0;
329
330 setInterval(() => {
331 toggleIndex++;
332 toggleElements.forEach(({ el, contents }) => {
333 if (!document.body.contains(el)) return;
334 const index = toggleIndex % contents.length;
335 utils.fadeOutIn(el, contents[index], duration);
336 });
337 }, toggleInterval);
338 }
339
340 return {
341 renderTrafficStats,
342 startToggleCycle,
343 };
344})();
345
346// == 数据请求和缓存模块 ==
347const trafficDataManager = (() => {
348 let trafficCache = null;
349
350 /**
351 * 请求流量数据,支持缓存
352 * @param {string} apiUrl - 接口地址
353 * @param {Object} config - 配置项
354 * @param {Function} callback - 请求成功后的回调,参数为流量数据
355 */
356 function fetchTrafficData(apiUrl, config, callback) {
357 const now = Date.now();
358 // 使用缓存数据
359 if (trafficCache && now - trafficCache.timestamp < config.interval) {
360 if (config.enableLog) console.log("[fetchTrafficData] 使用缓存数据");
361 callback(trafficCache.data);
362 return;
363 }
364
365 if (config.enableLog) console.log("[fetchTrafficData] 请求新数据...");
366 fetch(apiUrl)
367 .then((res) => res.json())
368 .then((data) => {
369 if (!data.success) {
370 if (config.enableLog)
371 console.warn("[fetchTrafficData] 请求成功但数据异常");
372 return;
373 }
374 if (config.enableLog) console.log("[fetchTrafficData] 成功获取新数据");
375 const trafficData = data.data.cycle_transfer_stats;
376 trafficCache = {
377 timestamp: now,
378 data: trafficData,
379 };
380 callback(trafficData);
381 })
382 .catch((err) => {
383 if (config.enableLog)
384 console.error("[fetchTrafficData] 请求失败:", err);
385 });
386 }
387
388 return {
389 fetchTrafficData,
390 };
391})();
392
393// == DOM变化监听模块 ==
394const domObserver = (() => {
395 const TARGET_SELECTOR =
396 "section.server-card-list, section.server-inline-list";
397 let currentSection = null;
398 let childObserver = null;
399
400 /**
401 * DOM 子节点变更回调,调用传入的函数
402 * @param {Function} onChangeCallback - 变更处理函数
403 */
404 function onDomChildListChange(onChangeCallback) {
405 onChangeCallback();
406 }
407
408 /**
409 * 监听指定section子节点变化
410 * @param {HTMLElement} section - 目标section元素
411 * @param {Function} onChangeCallback - 变更处理函数
412 */
413 function observeSection(section, onChangeCallback) {
414 if (childObserver) {
415 childObserver.disconnect();
416 }
417 currentSection = section;
418 childObserver = new MutationObserver((mutations) => {
419 for (const m of mutations) {
420 if (
421 m.type === "childList" &&
422 (m.addedNodes.length || m.removedNodes.length)
423 ) {
424 onDomChildListChange(onChangeCallback);
425 break;
426 }
427 }
428 });
429 childObserver.observe(currentSection, { childList: true, subtree: false });
430 // 初始调用一次
431 onChangeCallback();
432 }
433
434 /**
435 * 启动顶层section监听,检测section切换
436 * @param {Function} onChangeCallback - section变化时回调
437 * @returns {MutationObserver} sectionDetector实例
438 */
439 function startSectionDetector(onChangeCallback) {
440 const sectionDetector = new MutationObserver(() => {
441 const section = document.querySelector(TARGET_SELECTOR);
442 if (section && section !== currentSection) {
443 observeSection(section, onChangeCallback);
444 }
445 });
446 const root = document.querySelector("main") || document.body;
447 sectionDetector.observe(root, { childList: true, subtree: true });
448 return sectionDetector;
449 }
450
451 /**
452 * 断开所有监听
453 * @param {MutationObserver} sectionDetector - 顶层section监听实例
454 */
455 function disconnectAll(sectionDetector) {
456 if (childObserver) childObserver.disconnect();
457 if (sectionDetector) sectionDetector.disconnect();
458 }
459
460 return {
461 startSectionDetector,
462 disconnectAll,
463 };
464})();
465
466// == 主程序入口 ==
467(function main() {
468 // 默认配置
469 const defaultConfig = {
470 showTrafficStats: true,
471 insertAfter: true,
472 interval: 60000,
473 toggleInterval: 5000,
474 duration: 500,
475 apiUrl: "/api/v1/service",
476 enableLog: false,
477 };
478 // 合并用户自定义配置
479 const config = Object.assign(
480 {},
481 defaultConfig,
482 window.TrafficScriptConfig || {},
483 );
484 if (config.enableLog) {
485 console.log(`[TrafficScript] 版本: ${SCRIPT_VERSION}`);
486 console.log("[TrafficScript] 最终配置如下:", config);
487 }
488 /**
489 * 获取并刷新流量统计
490 */
491 function updateTrafficStats() {
492 trafficDataManager.fetchTrafficData(
493 config.apiUrl,
494 config,
495 (trafficData) => {
496 trafficRenderer.renderTrafficStats(trafficData, config);
497 },
498 );
499 }
500
501 /**
502 * DOM变更处理函数,触发刷新
503 */
504 function onDomChange() {
505 if (config.enableLog) console.log("[main] DOM变化,刷新流量数据");
506 updateTrafficStats();
507 if (!trafficTimer) startPeriodicRefresh();
508 }
509
510 // 定时器句柄,防止重复启动
511 let trafficTimer = null;
512
513 /**
514 * 启动周期刷新任务
515 */
516 function startPeriodicRefresh() {
517 if (!trafficTimer) {
518 if (config.enableLog) console.log("[main] 启动周期刷新任务");
519 trafficTimer = setInterval(() => {
520 updateTrafficStats();
521 }, config.interval);
522 }
523 }
524
525 // 启动内容切换轮播(如时间、百分比)
526 trafficRenderer.startToggleCycle(config.toggleInterval, config.duration);
527 // 监听section变化及其子节点变化
528 const sectionDetector = domObserver.startSectionDetector(onDomChange);
529 // 初始化调用一次
530 onDomChange();
531
532 // 延迟 100ms 后尝试读取用户配置并覆盖
533 setTimeout(() => {
534 const newConfig = Object.assign(
535 {},
536 defaultConfig,
537 window.TrafficScriptConfig || {},
538 );
539 // 判断配置是否变化(简单粗暴比较JSON字符串)
540 if (JSON.stringify(newConfig) !== JSON.stringify(config)) {
541 if (config.enableLog)
542 console.log("[main] 100ms后检测到新配置,更新配置并重启任务");
543 config = newConfig;
544 // 重新启动周期刷新任务
545 startPeriodicRefresh();
546 // 重新启动内容切换轮播(传入新配置)
547 trafficRenderer.startToggleCycle(config.toggleInterval, config.duration);
548 // 立即刷新数据
549 updateTrafficStats();
550 } else {
551 if (config.enableLog) console.log("[main] 100ms后无新配置,保持原配置");
552 }
553 }, 100);
554 // 页面卸载时清理监听和定时器
555 window.addEventListener("beforeunload", () => {
556 domObserver.disconnectAll(sectionDetector);
557 if (trafficTimer) clearInterval(trafficTimer);
558 });
559})();
560