import threading import tkinter as tk from tkinter import filedialog, messagebox, ttk from core.Function.loop_fun import ALL_ADAPTERS, LoopDetector from core.ui.components import Console, Page, action_bar, button, combo, field class LoopTab(Page): def __init__(self, parent): super().__init__(parent, "环网检测", "基于本机证据判断疑似二层环路、广播风暴和网关抖动风险。") self.body.rowconfigure(3, weight=1) status = self.section("实时状态", 0, columns=6) self.state = field(status, "状态", 1, 0, "等待", 10) self.adapter_total = field(status, "网卡数", 1, 1, "0", 8) self.current_adapter = field(status, "当前网卡", 1, 2, "-", 18) self.risk_level = field(status, "风险等级", 1, 3, "正常", 10) self.max_score = field(status, "最高分", 1, 4, "0", 8) self.elapsed = field(status, "耗时", 1, 5, "0.0s", 10) for item in (self.state, self.adapter_total, self.current_adapter, self.risk_level, self.max_score, self.elapsed): item["entry"].configure(state="disabled") params = self.section("检测参数", 1, columns=5) self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 24) self.duration = field(params, "检测时长 s", 1, 1, "15", 10) self.interval = field(params, "采样间隔 s", 1, 2, "1", 10) self.ping_timeout = field(params, "网关 Ping 超时 ms", 1, 3, "800", 12) actions = action_bar(params, 2, 5) self.start_btn = button(actions, "开始检测", self.start_detection, "Primary.TButton") self.stop_btn = button(actions, "停止", self.stop_detection, "Danger.TButton") self.refresh_btn = button(actions, "刷新网卡", self.load_adapters, "Secondary.TButton") self.copy_btn = button(actions, "复制摘要", self.copy_summary, "Secondary.TButton") self.export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton") results = self.section("检测结果", 2, columns=1) results.rowconfigure(1, weight=1) results.columnconfigure(0, weight=1) self.results_tree = ttk.Treeview( results, columns=("adapter", "ipv4", "gateway", "non_unicast", "ratio", "loss", "jitter", "arp", "score", "level", "verdict"), show="headings", height=9, ) headings = { "adapter": "网卡", "ipv4": "IPv4", "gateway": "网关", "non_unicast": "非单播/s", "ratio": "非单播占比", "loss": "网关丢包", "jitter": "网关抖动", "arp": "ARP 异常", "score": "分数", "level": "等级", "verdict": "判断", } widths = { "adapter": 140, "ipv4": 120, "gateway": 120, "non_unicast": 90, "ratio": 90, "loss": 80, "jitter": 80, "arp": 80, "score": 60, "level": 80, "verdict": 360, } for column, title in headings.items(): self.results_tree.heading(column, text=title) self.results_tree.column(column, width=widths[column], anchor="w") self.results_tree.tag_configure("正常", foreground="#15803d") self.results_tree.tag_configure("可疑", foreground="#b7791f") self.results_tree.tag_configure("高风险", foreground="#dc2626") self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18)) result_scroll = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview) result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18)) self.results_tree.configure(yscrollcommand=result_scroll.set) output = self.section("诊断控制台", 3, columns=1) output.rowconfigure(1, weight=1) output.columnconfigure(0, weight=1) self.console = Console(output, height=12) self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18)) self.detector = LoopDetector(self.write, self.on_task_done, self.update_status, self.add_result) self.after(350, self.load_adapters) def write(self, text, tag=None): self.console.write(text, tag) def clear(self): self.console.clear() for item in self.results_tree.get_children(): self.results_tree.delete(item) self.update_status( { "state": "等待", "total_adapters": 0, "current_adapter": "-", "risk_level": "正常", "max_score": 0, "elapsed": 0, } ) def load_adapters(self): def worker(): try: values = self.detector.get_adapter_choices() except Exception as exc: values = [ALL_ADAPTERS] self.write(f"读取检测网卡失败: {exc}\n", "warning") self.after(0, lambda: self.apply_adapters(values)) threading.Thread(target=worker, daemon=True).start() def apply_adapters(self, values): values = values or [ALL_ADAPTERS] values = list(dict.fromkeys(values)) self.adapter["combobox"]["values"] = values if self.adapter["var"].get() not in values: self.adapter["var"].set(values[0]) def start_detection(self): try: self.clear() self.start_btn.configure(state="disabled") self.detector.start_detection( self.adapter["var"].get(), { "duration_sec": self.duration["var"].get(), "interval_sec": self.interval["var"].get(), "ping_timeout_ms": self.ping_timeout["var"].get(), }, ) except Exception as exc: self.start_btn.configure(state="normal") messagebox.showwarning("无法开始环网检测", str(exc)) def stop_detection(self): try: self.detector.stop_detection() except Exception as exc: messagebox.showinfo("提示", str(exc)) def export_results(self): path = filedialog.asksaveasfilename( title="导出环网检测结果", defaultextension=".csv", filetypes=[("CSV 文件", "*.csv")], ) if not path: return try: self.detector.export_results(path) self.write(f"已导出结果: {path}\n", "success") except Exception as exc: messagebox.showwarning("导出失败", str(exc)) def copy_summary(self): text = self.detector.copy_summary() if not text: messagebox.showinfo("提示", "还没有可复制的诊断摘要") return self.clipboard_clear() self.clipboard_append(text) self.write("已复制诊断摘要到剪贴板\n", "success") def update_status(self, stats): def apply(): current = stats.get("current_adapter") or "-" values = [ (self.state, stats.get("state", "等待")), (self.adapter_total, str(stats.get("total_adapters", 0))), (self.current_adapter, current), (self.risk_level, stats.get("risk_level", "正常")), (self.max_score, str(stats.get("max_score", 0))), (self.elapsed, f"{stats.get('elapsed', 0):.1f}s"), ] for item, value in values: item["entry"].configure(state="normal") item["var"].set(value) item["entry"].configure(state="disabled") self.after(0, apply) def add_result(self, row): def apply(): arp_score = int(row.get("gateway_mac_changes", 0)) + int(row.get("ip_mac_changes", 0)) self.results_tree.insert( "", "end", values=( row.get("adapter", ""), row.get("ipv4", ""), row.get("gateway", ""), f"{row.get('non_unicast_pps', 0):.1f}", f"{row.get('non_unicast_ratio', 0):.1f}%", f"{row.get('gateway_ping_loss', 0):.1f}%", f"{row.get('gateway_jitter_ms', 0):.1f} ms", str(arp_score), row.get("risk_score", 0), row.get("risk_level", ""), row.get("verdict", ""), ), tags=(row.get("risk_level", ""),), ) self.after(0, apply) def on_task_done(self): self.after(0, lambda: self.start_btn.configure(state="normal"))