Last active 1757414721

Aaron revised this gist 1757414721. Go to revision

3 files changed, 900 insertions

network-fluctuation.js(file created)

@@ -0,0 +1,96 @@
1 + const 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";
3 + const selectorSection =
4 + "#root > div > main > div.mx-auto.w-full.max-w-5xl.px-0.flex.flex-col.gap-4.server-info > section";
5 + const 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)";
7 + const 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 +
10 + let hasClicked = false;
11 + let divVisible = false;
12 + let swapping = false;
13 +
14 + function 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 +
23 + function hideSection() {
24 + const section = document.querySelector(selectorSection);
25 + if (section) {
26 + section.style.display = "none";
27 + }
28 + }
29 +
30 + function tryClickButton() {
31 + const btn = document.querySelector(selectorButton);
32 + if (btn && !hasClicked) {
33 + btn.click();
34 + hasClicked = true;
35 + setTimeout(forceBothVisible, 500);
36 + }
37 + }
38 +
39 + function 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 +
62 + const 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 +
88 + const root = document.querySelector("#root");
89 + if (root) {
90 + observer.observe(root, {
91 + childList: true,
92 + attributes: true,
93 + subtree: true,
94 + attributeFilter: ["style", "class"],
95 + });
96 + }

style.css(file created)

@@ -0,0 +1,245 @@
1 + /* 默认和浅色主题的CSS变量 */
2 + :root,
3 + html.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变量 */
11 + html.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 +
18 + html.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,
40 + html.dark .bg-card,
41 + html.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,
54 + html.dark .text-muted-foreground,
55 + html.light .text-muted-foreground {
56 + color: var(--custom-text-color);
57 + }
58 +
59 + html.dark .dark\:bg-stone-700,
60 + html.light .bg-stone-700,
61 + .bg-stone-700 {
62 + --tw-bg-opacity: 0.5;
63 + background-color: var(--custom-background-color);
64 + }
65 +
66 + html *,
67 + html.dark *,
68 + html.light * {
69 + border-color: var(--custom-border-color);
70 + }
71 +
72 + html body,
73 + html.dark body,
74 + html.light body {
75 + color: var(--custom-text-color);
76 + background: unset;
77 + position: relative;
78 + }
79 +
80 + img {
81 + border: none;
82 + }
83 +
84 + html.dark .dark\:border-neutral-800,
85 + html.light .border-neutral-800,
86 + .border-neutral-800 {
87 + border-color: var(--custom-border-color);
88 + }
89 +
90 + html.dark .dark\:bg-stone-800,
91 + html.light .bg-stone-800,
92 + .bg-stone-800 {
93 + --tw-bg-opacity: 0.5;
94 + background-color: var(--custom-background-color);
95 + }
96 +
97 + html.dark .dark\:text-stone-500,
98 + html.light .text-stone-500,
99 + .text-stone-500 {
100 + color: var(--custom-text-color);
101 + }
102 +
103 + .bg-secondary,
104 + html.dark .bg-secondary,
105 + html.light .bg-secondary {
106 + background-color: var(--custom-background-color);
107 + opacity: 0.7;
108 + }
109 +
110 + .bg-popover,
111 + html.dark .bg-popover,
112 + html.light .bg-popover {
113 + background-color: var(--custom-background-color);
114 + }
115 +
116 + .bg-muted,
117 + html.dark .bg-muted,
118 + html.light .bg-muted {
119 + background-color: var(--custom-border-color);
120 + }
121 +
122 + html.dark .dark\:bg-black,
123 + html.light .bg-black,
124 + .bg-black {
125 + background-color: var(--custom-border-color);
126 + }
127 +
128 + .bg-border,
129 + html.dark .bg-border,
130 + html.light .bg-border {
131 + background-color: var(--custom-border-color);
132 + }
133 +
134 + .border-input,
135 + html.dark .border-input,
136 + html.light .border-input {
137 + border-color: var(--custom-border-color);
138 + }
139 +
140 + html.dark .dark\:text-neutral-300\/50,
141 + html.light .text-neutral-300,
142 + .text-neutral-300 {
143 + color: var(--custom-text-color);
144 + opacity: 0.7;
145 + }
146 +
147 + html.dark .dark\:text-stone-400,
148 + html.light .text-stone-400,
149 + .text-stone-400 {
150 + color: var(--custom-text-color);
151 + }
152 +
153 + div#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,
167 + html.dark .vps-info,
168 + html.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,
176 + html.dark .font-medium.opacity-40,
177 + html.light .font-medium.opacity-40 {
178 + opacity: 0.8 !important;
179 + }
180 +
181 + .font-medium.opacity-50,
182 + html.dark .font-medium.opacity-50,
183 + html.light .font-medium.opacity-50 {
184 + opacity: 0.8 !important;
185 + }
186 +
187 + .max-w-5xl.gap-4 > div:first-child,
188 + html.dark .max-w-5xl.gap-4 > div:first-child,
189 + html.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 +
198 + img[alt="BackIcon"] {
199 + margin-right: 12px;
200 + }
201 +
202 + .flex.items-center.gap-1.rounded-\[50px\].bg-stone-100.p-\[3px\],
203 + html.dark .flex.items-center.gap-1.rounded-\[50px\].bg-stone-100.p-\[3px\],
204 + html.light .flex.items-center.gap-1.rounded-\[50px\].bg-stone-100.p-\[3px\],
205 + html.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 +
216 + html.dark .dark\:fill-neutral-800,
217 + html.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 +
239 + html.dark .dark\:bg-black\/70,
240 + html.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(file created)

@@ -0,0 +1,559 @@
1 + const SCRIPT_VERSION = "v20250617";
2 + // == 样式注入模块 ==
3 + // 注入自定义CSS隐藏特定元素
4 + function 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 + }
14 + injectCustomCSS();
15 +
16 + // == 工具函数模块 ==
17 + const 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 + // == 流量统计渲染模块 ==
138 + const 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 + // == 数据请求和缓存模块 ==
347 + const 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变化监听模块 ==
394 + const 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 + })();
Newer Older