OK
This commit is contained in:
@@ -2,8 +2,8 @@ import concurrent.futures
|
|||||||
import csv
|
import csv
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
|
import unicodedata
|
||||||
import re
|
import re
|
||||||
import socket
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
@@ -75,7 +75,6 @@ class DeviceDiscoveryOptions:
|
|||||||
class DeviceInfo:
|
class DeviceInfo:
|
||||||
ip: str
|
ip: str
|
||||||
mac: str
|
mac: str
|
||||||
hostname: str
|
|
||||||
vendor: str
|
vendor: str
|
||||||
adapter: str
|
adapter: str
|
||||||
interface_index: str
|
interface_index: str
|
||||||
@@ -112,6 +111,12 @@ class DeviceDiscovery:
|
|||||||
adapters = self._active_adapters(self.network.get_network_info())
|
adapters = self._active_adapters(self.network.get_network_info())
|
||||||
return [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
|
return [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
|
||||||
|
|
||||||
|
def get_adapter_choices_and_default_range(self) -> tuple[list[str], str, str]:
|
||||||
|
adapters = self._active_adapters(self.network.get_network_info())
|
||||||
|
choices = [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
|
||||||
|
default_adapter = preferred_discovery_adapter(adapters)
|
||||||
|
return choices, adapter_to_safe_range(default_adapter) if default_adapter else "", default_adapter.get("name", "") if default_adapter else ""
|
||||||
|
|
||||||
def default_scan_range(self, adapter_name: str = ALL_ADAPTERS) -> str:
|
def default_scan_range(self, adapter_name: str = ALL_ADAPTERS) -> str:
|
||||||
adapters = self._select_adapters(adapter_name)
|
adapters = self._select_adapters(adapter_name)
|
||||||
if not adapters:
|
if not adapters:
|
||||||
@@ -136,7 +141,7 @@ class DeviceDiscovery:
|
|||||||
self.last_summary = ""
|
self.last_summary = ""
|
||||||
self.output(
|
self.output(
|
||||||
f"开始局域网设备发现: {adapter_name or ALL_ADAPTERS},"
|
f"开始局域网设备发现: {adapter_name or ALL_ADAPTERS},"
|
||||||
f"目标 {len(targets)} 个,并发 {discovery_options.workers},超时 {discovery_options.timeout_ms}ms\n",
|
f"目标 {len(targets)} 个,并发 {min(discovery_options.workers, 24, max(1, len(targets)))},超时 {discovery_options.timeout_ms}ms\n",
|
||||||
"muted",
|
"muted",
|
||||||
)
|
)
|
||||||
self.status(self._status("扫描中", adapter_name or ALL_ADAPTERS, describe_targets(targets), len(targets), 0, 0, 0))
|
self.status(self._status("扫描中", adapter_name or ALL_ADAPTERS, describe_targets(targets), len(targets), 0, 0, 0))
|
||||||
@@ -196,13 +201,14 @@ class DeviceDiscovery:
|
|||||||
def scan_targets(self, targets: list[str], options: DeviceDiscoveryOptions, started: float) -> dict[str, dict]:
|
def scan_targets(self, targets: list[str], options: DeviceDiscoveryOptions, started: float) -> dict[str, dict]:
|
||||||
results = {}
|
results = {}
|
||||||
completed = 0
|
completed = 0
|
||||||
|
workers = min(options.workers, 24, max(1, len(targets)))
|
||||||
|
|
||||||
def task(ip: str) -> dict:
|
def task(ip: str) -> dict:
|
||||||
if self.stop_event.is_set():
|
if self.stop_event.is_set():
|
||||||
return {"ip": ip, "ok": False, "rtt": 0.0}
|
return {"ip": ip, "ok": False, "rtt": 0.0}
|
||||||
return ping_once(ip, options.timeout_ms)
|
return ping_once(ip, options.timeout_ms)
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=options.workers) as executor:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
||||||
futures = {executor.submit(task, ip): ip for ip in targets}
|
futures = {executor.submit(task, ip): ip for ip in targets}
|
||||||
for future in concurrent.futures.as_completed(futures):
|
for future in concurrent.futures.as_completed(futures):
|
||||||
completed += 1
|
completed += 1
|
||||||
@@ -241,7 +247,6 @@ class DeviceDiscovery:
|
|||||||
devices[local_ip] = DeviceInfo(
|
devices[local_ip] = DeviceInfo(
|
||||||
ip=local_ip,
|
ip=local_ip,
|
||||||
mac=normalize_mac(adapter.get("mac", "")),
|
mac=normalize_mac(adapter.get("mac", "")),
|
||||||
hostname=resolve_hostname(local_ip),
|
|
||||||
vendor=vendor_name(adapter.get("mac", "")),
|
vendor=vendor_name(adapter.get("mac", "")),
|
||||||
adapter=adapter.get("name", ""),
|
adapter=adapter.get("name", ""),
|
||||||
interface_index=str(adapter.get("interface_index", "")),
|
interface_index=str(adapter.get("interface_index", "")),
|
||||||
@@ -270,7 +275,6 @@ class DeviceDiscovery:
|
|||||||
devices[ip] = DeviceInfo(
|
devices[ip] = DeviceInfo(
|
||||||
ip=ip,
|
ip=ip,
|
||||||
mac=mac,
|
mac=mac,
|
||||||
hostname=resolve_hostname(ip),
|
|
||||||
vendor=vendor_name(mac),
|
vendor=vendor_name(mac),
|
||||||
adapter=adapter.get("name", ""),
|
adapter=adapter.get("name", ""),
|
||||||
interface_index=str(adapter.get("interface_index", "")),
|
interface_index=str(adapter.get("interface_index", "")),
|
||||||
@@ -285,20 +289,15 @@ class DeviceDiscovery:
|
|||||||
adapter = adapter_for_ip(ip, adapters)
|
adapter = adapter_for_ip(ip, adapters)
|
||||||
if not adapter:
|
if not adapter:
|
||||||
continue
|
continue
|
||||||
# Ping succeeded but no valid ARP MAC was visible; keep it out of
|
|
||||||
# the asset list unless it is a known local/gateway address.
|
|
||||||
if ip not in gateways and ip not in local_ips:
|
|
||||||
continue
|
|
||||||
devices[ip] = DeviceInfo(
|
devices[ip] = DeviceInfo(
|
||||||
ip=ip,
|
ip=ip,
|
||||||
mac="",
|
mac="",
|
||||||
hostname=resolve_hostname(ip),
|
|
||||||
vendor="未知",
|
vendor="未知",
|
||||||
adapter=adapter.get("name", ""),
|
adapter=adapter.get("name", ""),
|
||||||
interface_index=str(adapter.get("interface_index", "")),
|
interface_index=str(adapter.get("interface_index", "")),
|
||||||
latency_ms=ping.get("rtt", 0.0),
|
latency_ms=ping.get("rtt", 0.0),
|
||||||
method="在线",
|
method="在线",
|
||||||
note="网关" if ip in gateways else "本机",
|
note="网关" if ip in gateways else "本机" if ip in local_ips else "未读取到 MAC",
|
||||||
)
|
)
|
||||||
|
|
||||||
return list(devices.values())
|
return list(devices.values())
|
||||||
@@ -346,7 +345,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
|
|||||||
|
|
||||||
def build_scan_targets(self, adapters: list[dict], options: DeviceDiscoveryOptions) -> list[str]:
|
def build_scan_targets(self, adapters: list[dict], options: DeviceDiscoveryOptions) -> list[str]:
|
||||||
if options.scan_range:
|
if options.scan_range:
|
||||||
return parse_target_range(options.scan_range, options.max_hosts)
|
return filter_targets_for_adapters(parse_target_range(options.scan_range, options.max_hosts), adapters)
|
||||||
targets = []
|
targets = []
|
||||||
for adapter in adapters:
|
for adapter in adapters:
|
||||||
targets.extend(parse_target_range(adapter_to_safe_range(adapter), options.max_hosts))
|
targets.extend(parse_target_range(adapter_to_safe_range(adapter), options.max_hosts))
|
||||||
@@ -361,7 +360,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
|
|||||||
def export_results(self, path: str) -> None:
|
def export_results(self, path: str) -> None:
|
||||||
if not self.last_results:
|
if not self.last_results:
|
||||||
raise RuntimeError("还没有可导出的设备发现结果")
|
raise RuntimeError("还没有可导出的设备发现结果")
|
||||||
fields = ["ip", "mac", "hostname", "vendor", "adapter", "interface_index", "latency_ms", "method", "note", "checked_at"]
|
fields = ["ip", "mac", "vendor", "adapter", "interface_index", "latency_ms", "method", "note", "checked_at"]
|
||||||
with open(path, "w", newline="", encoding="utf-8-sig") as file:
|
with open(path, "w", newline="", encoding="utf-8-sig") as file:
|
||||||
writer = csv.DictWriter(file, fieldnames=fields)
|
writer = csv.DictWriter(file, fieldnames=fields)
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
@@ -372,7 +371,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
|
|||||||
if not self.last_results:
|
if not self.last_results:
|
||||||
return self.last_summary
|
return self.last_summary
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
f"{row.get('ip', '')}\t{row.get('mac', '')}\t{row.get('hostname', '')}\t{row.get('vendor', '')}\t{row.get('method', '')}"
|
f"{row.get('ip', '')}\t{row.get('mac', '')}\t{row.get('vendor', '')}\t{row.get('method', '')}"
|
||||||
for row in sorted(self.last_results, key=lambda item: ip_sort_key(item.get("ip", "")))
|
for row in sorted(self.last_results, key=lambda item: ip_sort_key(item.get("ip", "")))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -389,7 +388,8 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
|
|||||||
adapters = self._active_adapters(self.network.get_network_info())
|
adapters = self._active_adapters(self.network.get_network_info())
|
||||||
if not adapter_name or adapter_name == ALL_ADAPTERS:
|
if not adapter_name or adapter_name == ALL_ADAPTERS:
|
||||||
return adapters
|
return adapters
|
||||||
return [adapter for adapter in adapters if adapter.get("name") == adapter_name]
|
selected = normalize_adapter_name(adapter_name)
|
||||||
|
return [adapter for adapter in adapters if normalize_adapter_name(adapter.get("name", "")) == selected]
|
||||||
|
|
||||||
def _active_adapters(self, adapters: list[dict]) -> list[dict]:
|
def _active_adapters(self, adapters: list[dict]) -> list[dict]:
|
||||||
active = []
|
active = []
|
||||||
@@ -397,7 +397,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
|
|||||||
if not adapter.get("ipv4"):
|
if not adapter.get("ipv4"):
|
||||||
continue
|
continue
|
||||||
status = str(adapter.get("status", "")).lower()
|
status = str(adapter.get("status", "")).lower()
|
||||||
if "disconnect" in status or "断开" in status:
|
if any(value in status for value in ("disconnect", "disabled", "not present", "断开", "禁用")):
|
||||||
continue
|
continue
|
||||||
active.append(adapter)
|
active.append(adapter)
|
||||||
return active
|
return active
|
||||||
@@ -419,7 +419,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
|
|||||||
|
|
||||||
def ping_once(ip: str, timeout_ms: int) -> dict:
|
def ping_once(ip: str, timeout_ms: int) -> dict:
|
||||||
try:
|
try:
|
||||||
result = run_hidden(["ping", ip, "-n", "1", "-w", str(timeout_ms)], timeout=max(2, timeout_ms / 1000 + 2))
|
result = run_hidden(["ping", ip, "-n", "1", "-w", str(timeout_ms)], timeout=max(1.2, timeout_ms / 1000 + 0.8))
|
||||||
output = result.stdout
|
output = result.stdout
|
||||||
if re.search(r"\bTTL=", output, re.IGNORECASE):
|
if re.search(r"\bTTL=", output, re.IGNORECASE):
|
||||||
match = re.search(r"(?:time|时间)[=<]?\s*(\d+(?:\.\d+)?)\s*(?:ms|毫秒)", output, re.IGNORECASE)
|
match = re.search(r"(?:time|时间)[=<]?\s*(\d+(?:\.\d+)?)\s*(?:ms|毫秒)", output, re.IGNORECASE)
|
||||||
@@ -480,6 +480,49 @@ def adapter_to_safe_range(adapter: dict) -> str:
|
|||||||
return ".".join(parts[:3]) + ".0/24" if len(parts) == 4 else ""
|
return ".".join(parts[:3]) + ".0/24" if len(parts) == 4 else ""
|
||||||
|
|
||||||
|
|
||||||
|
def preferred_discovery_adapter(adapters: list[dict]) -> Optional[dict]:
|
||||||
|
if not adapters:
|
||||||
|
return None
|
||||||
|
for adapter in adapters:
|
||||||
|
if adapter.get("gateway"):
|
||||||
|
return adapter
|
||||||
|
for adapter in adapters:
|
||||||
|
prefix = adapter.get("prefix_length")
|
||||||
|
try:
|
||||||
|
if prefix not in ("", None) and int(prefix) <= 24:
|
||||||
|
return adapter
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return adapters[0]
|
||||||
|
|
||||||
|
|
||||||
|
def filter_targets_for_adapters(targets: list[str], adapters: list[dict]) -> list[str]:
|
||||||
|
return [target for target in targets if target_in_adapter_network(target, adapters)]
|
||||||
|
|
||||||
|
|
||||||
|
def target_in_adapter_network(target: str, adapters: list[dict]) -> bool:
|
||||||
|
try:
|
||||||
|
address = ipaddress.ip_address(target)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
for adapter in adapters:
|
||||||
|
local_ip = adapter.get("ipv4", "")
|
||||||
|
if not local_ip:
|
||||||
|
continue
|
||||||
|
prefix = adapter.get("prefix_length") or netmask_to_prefix(adapter.get("netmask", "")) or 24
|
||||||
|
try:
|
||||||
|
if address in ipaddress.ip_network(f"{local_ip}/{prefix}", strict=False):
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_adapter_name(value: str) -> str:
|
||||||
|
normalized = unicodedata.normalize("NFKC", str(value or ""))
|
||||||
|
return re.sub(r"[\s\u200b-\u200d\ufeff]+", " ", normalized).strip().casefold()
|
||||||
|
|
||||||
|
|
||||||
def netmask_to_prefix(netmask: str) -> Optional[int]:
|
def netmask_to_prefix(netmask: str) -> Optional[int]:
|
||||||
if not netmask:
|
if not netmask:
|
||||||
return None
|
return None
|
||||||
@@ -499,27 +542,6 @@ def normalize_neighbor(item: dict) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def resolve_hostname(ip: str) -> str:
|
|
||||||
try:
|
|
||||||
old_timeout = socket.getdefaulttimeout()
|
|
||||||
socket.setdefaulttimeout(0.8)
|
|
||||||
try:
|
|
||||||
return socket.gethostbyaddr(ip)[0]
|
|
||||||
finally:
|
|
||||||
socket.setdefaulttimeout(old_timeout)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
result = run_hidden(["nbtstat", "-A", ip], timeout=2)
|
|
||||||
for line in result.stdout.splitlines():
|
|
||||||
match = re.match(r"\s*([^\s<]+)\s+<00>\s+UNIQUE", line, re.IGNORECASE)
|
|
||||||
if match:
|
|
||||||
return match.group(1).strip()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def vendor_name(mac: str) -> str:
|
def vendor_name(mac: str) -> str:
|
||||||
normalized = normalize_mac(mac)
|
normalized = normalize_mac(mac)
|
||||||
if len(normalized) < 8:
|
if len(normalized) < 8:
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class DnsDiagnostic:
|
|||||||
self.done = done or (lambda: None)
|
self.done = done or (lambda: None)
|
||||||
self.status = status or (lambda _stats: None)
|
self.status = status or (lambda _stats: None)
|
||||||
self.result = result or (lambda _row: None)
|
self.result = result or (lambda _row: None)
|
||||||
self.network = NetworkManager(lambda _text, _tag=None: None)
|
self.network = NetworkManager(output)
|
||||||
self.stop_event = threading.Event()
|
self.stop_event = threading.Event()
|
||||||
self.worker = None
|
self.worker = None
|
||||||
self.last_results: list[dict] = []
|
self.last_results: list[dict] = []
|
||||||
@@ -76,14 +76,35 @@ class DnsDiagnostic:
|
|||||||
self.local_dns_servers: set[str] = set()
|
self.local_dns_servers: set[str] = set()
|
||||||
|
|
||||||
def get_adapter_choices(self) -> list[str]:
|
def get_adapter_choices(self) -> list[str]:
|
||||||
adapters = self._active_adapters(self.network.get_network_info())
|
adapters = self.network.get_network_info()
|
||||||
return [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
|
return [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
|
||||||
|
|
||||||
|
def get_adapter_choices_and_default_dns(self) -> tuple[list[str], str]:
|
||||||
|
adapters = self.network.get_network_info()
|
||||||
|
choices = [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
|
||||||
|
servers = collect_adapter_dns(self._active_adapters(adapters))
|
||||||
|
default_dns = ",".join(list(dict.fromkeys(servers + PUBLIC_DNS_SERVERS)))
|
||||||
|
return choices, default_dns
|
||||||
|
|
||||||
def default_dns_servers(self, adapter_name: str = ALL_ADAPTERS) -> str:
|
def default_dns_servers(self, adapter_name: str = ALL_ADAPTERS) -> str:
|
||||||
adapters = self._select_adapters(adapter_name)
|
adapters = self._select_dns_source_adapters(adapter_name)
|
||||||
servers = collect_adapter_dns(adapters)
|
servers = collect_adapter_dns(adapters)
|
||||||
return ",".join(list(dict.fromkeys(servers + PUBLIC_DNS_SERVERS)))
|
return ",".join(list(dict.fromkeys(servers + PUBLIC_DNS_SERVERS)))
|
||||||
|
|
||||||
|
def repair_abnormal_dns(self, adapter_name: str = ALL_ADAPTERS) -> dict:
|
||||||
|
if self.is_running():
|
||||||
|
raise RuntimeError("DNS 诊断正在运行,请先停止当前任务")
|
||||||
|
|
||||||
|
adapters = self._select_repair_adapters(adapter_name)
|
||||||
|
if not adapters:
|
||||||
|
raise ValueError("没有找到可修复的活动网卡")
|
||||||
|
|
||||||
|
servers = PUBLIC_DNS_SERVERS[:2]
|
||||||
|
for adapter in adapters:
|
||||||
|
self.network.set_dns_servers(adapter["name"], servers)
|
||||||
|
self.network.flush_dns_cache()
|
||||||
|
return {"adapters": [adapter["name"] for adapter in adapters], "servers": servers}
|
||||||
|
|
||||||
def start_diagnosis(self, adapter_name: str = ALL_ADAPTERS, options: Optional[dict] = None) -> None:
|
def start_diagnosis(self, adapter_name: str = ALL_ADAPTERS, options: Optional[dict] = None) -> None:
|
||||||
if self.is_running():
|
if self.is_running():
|
||||||
raise RuntimeError("DNS 诊断正在运行,请先停止当前任务")
|
raise RuntimeError("DNS 诊断正在运行,请先停止当前任务")
|
||||||
@@ -302,13 +323,25 @@ class DnsDiagnostic:
|
|||||||
return adapters
|
return adapters
|
||||||
return [adapter for adapter in adapters if adapter.get("name") == adapter_name]
|
return [adapter for adapter in adapters if adapter.get("name") == adapter_name]
|
||||||
|
|
||||||
|
def _select_dns_source_adapters(self, adapter_name: str) -> list[dict]:
|
||||||
|
adapters = self.network.get_network_info()
|
||||||
|
if not adapter_name or adapter_name == ALL_ADAPTERS:
|
||||||
|
return self._active_adapters(adapters)
|
||||||
|
return [adapter for adapter in adapters if adapter.get("name") == adapter_name]
|
||||||
|
|
||||||
|
def _select_repair_adapters(self, adapter_name: str) -> list[dict]:
|
||||||
|
adapters = self._select_adapters(adapter_name)
|
||||||
|
if adapter_name and adapter_name != ALL_ADAPTERS:
|
||||||
|
return adapters
|
||||||
|
return [adapter for adapter in adapters if adapter.get("gateway") or adapter.get("dns1") or adapter.get("dns2")]
|
||||||
|
|
||||||
def _active_adapters(self, adapters: list[dict]) -> list[dict]:
|
def _active_adapters(self, adapters: list[dict]) -> list[dict]:
|
||||||
active = []
|
active = []
|
||||||
for adapter in adapters:
|
for adapter in adapters:
|
||||||
if not adapter.get("ipv4"):
|
if not adapter.get("ipv4"):
|
||||||
continue
|
continue
|
||||||
status = str(adapter.get("status", "")).lower()
|
status = str(adapter.get("status", "")).lower()
|
||||||
if "disconnect" in status or "断开" in status:
|
if any(value in status for value in ("disconnect", "disabled", "not present", "断开", "禁用")):
|
||||||
continue
|
continue
|
||||||
active.append(adapter)
|
active.append(adapter)
|
||||||
return active
|
return active
|
||||||
|
|||||||
@@ -487,7 +487,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
|
|||||||
if not adapter.get("ipv4"):
|
if not adapter.get("ipv4"):
|
||||||
continue
|
continue
|
||||||
status = str(adapter.get("status", "")).lower()
|
status = str(adapter.get("status", "")).lower()
|
||||||
if "disconnect" in status or "断开" in status:
|
if any(value in status for value in ("disconnect", "disabled", "not present", "断开", "禁用")):
|
||||||
continue
|
continue
|
||||||
active.append(adapter)
|
active.append(adapter)
|
||||||
return active
|
return active
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
|
|||||||
if not adapter.get("ipv4"):
|
if not adapter.get("ipv4"):
|
||||||
continue
|
continue
|
||||||
status = str(adapter.get("status", "")).lower()
|
status = str(adapter.get("status", "")).lower()
|
||||||
if "disconnect" in status or "断开" in status:
|
if any(value in status for value in ("disconnect", "disabled", "not present", "断开", "禁用")):
|
||||||
continue
|
continue
|
||||||
active.append(adapter)
|
active.append(adapter)
|
||||||
return active
|
return active
|
||||||
|
|||||||
@@ -24,33 +24,55 @@ class NetworkManager:
|
|||||||
def get_network_info(self) -> list[dict]:
|
def get_network_info(self) -> list[dict]:
|
||||||
script = r"""
|
script = r"""
|
||||||
$ErrorActionPreference = "SilentlyContinue"
|
$ErrorActionPreference = "SilentlyContinue"
|
||||||
$items = Get-NetIPConfiguration | ForEach-Object {
|
$ipconfigs = @{}
|
||||||
$alias = $_.InterfaceAlias
|
Get-NetIPConfiguration | ForEach-Object { $ipconfigs[[string]$_.InterfaceIndex] = $_ }
|
||||||
$index = $_.InterfaceIndex
|
|
||||||
$adapter = Get-NetAdapter -InterfaceAlias $alias -ErrorAction SilentlyContinue
|
$ipifs = @{}
|
||||||
$ipif = Get-NetIPInterface -InterfaceAlias $alias -AddressFamily IPv4 -ErrorAction SilentlyContinue | Select-Object -First 1
|
Get-NetIPInterface -AddressFamily IPv4 | ForEach-Object {
|
||||||
$cim = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True" -ErrorAction SilentlyContinue | Where-Object { $_.InterfaceIndex -eq $index } | Select-Object -First 1
|
$key = [string]$_.InterfaceIndex
|
||||||
|
if (-not $ipifs.ContainsKey($key)) { $ipifs[$key] = $_ }
|
||||||
|
}
|
||||||
|
|
||||||
|
$cims = @{}
|
||||||
|
Get-CimInstance Win32_NetworkAdapterConfiguration | ForEach-Object {
|
||||||
|
if ($_.InterfaceIndex -ne $null) { $cims[[string]$_.InterfaceIndex] = $_ }
|
||||||
|
}
|
||||||
|
|
||||||
|
$dnsMap = @{}
|
||||||
|
Get-DnsClientServerAddress -AddressFamily IPv4 | ForEach-Object {
|
||||||
|
$dnsMap[[string]$_.InterfaceIndex] = @($_.ServerAddresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = Get-NetAdapter | Sort-Object Name | ForEach-Object {
|
||||||
|
$adapter = $_
|
||||||
|
$alias = $adapter.Name
|
||||||
|
$index = $adapter.ifIndex
|
||||||
|
$key = [string]$index
|
||||||
|
$ipconfig = $ipconfigs[$key]
|
||||||
|
$ipif = $ipifs[$key]
|
||||||
|
$cim = $cims[$key]
|
||||||
|
$dns = @($dnsMap[$key])
|
||||||
[PSCustomObject]@{
|
[PSCustomObject]@{
|
||||||
name = $alias
|
name = $alias
|
||||||
description = $_.InterfaceDescription
|
description = $adapter.InterfaceDescription
|
||||||
mac = if ($adapter) { $adapter.MacAddress } else { "" }
|
mac = $adapter.MacAddress
|
||||||
status = if ($adapter) { [string]$adapter.Status } else { "" }
|
status = [string]$adapter.Status
|
||||||
link_speed = if ($adapter) { [string]$adapter.LinkSpeed } else { "" }
|
link_speed = [string]$adapter.LinkSpeed
|
||||||
interface_index = $index
|
interface_index = $index
|
||||||
ipv4 = @($_.IPv4Address | Select-Object -ExpandProperty IPAddress)[0]
|
ipv4 = @($ipconfig.IPv4Address | Select-Object -ExpandProperty IPAddress)[0]
|
||||||
ipv6 = @($_.IPv6Address | Select-Object -ExpandProperty IPAddress)
|
ipv6 = @($ipconfig.IPv6Address | Select-Object -ExpandProperty IPAddress)
|
||||||
prefix_length = @($_.IPv4Address | Select-Object -ExpandProperty PrefixLength)[0]
|
prefix_length = @($ipconfig.IPv4Address | Select-Object -ExpandProperty PrefixLength)[0]
|
||||||
gateway = @($_.IPv4DefaultGateway | Select-Object -ExpandProperty NextHop)[0]
|
gateway = @($ipconfig.IPv4DefaultGateway | Select-Object -ExpandProperty NextHop)[0]
|
||||||
dns = @($_.DNSServer.ServerAddresses)
|
dns = $dns
|
||||||
dhcp_server = if ($cim) { $cim.DHCPServer } else { "" }
|
dhcp_server = if ($cim) { $cim.DHCPServer } else { "" }
|
||||||
dhcp_lease_obtained = if ($cim) { [string]$cim.DHCPLeaseObtained } else { "" }
|
dhcp_lease_obtained = if ($cim) { [string]$cim.DHCPLeaseObtained } else { "" }
|
||||||
dhcp_lease_expires = if ($cim) { [string]$cim.DHCPLeaseExpires } else { "" }
|
dhcp_lease_expires = if ($cim) { [string]$cim.DHCPLeaseExpires } else { "" }
|
||||||
dhcp_enabled = if ($ipif) { [string]$ipif.Dhcp -eq "Enabled" } else { $false }
|
dhcp_enabled = if ($ipif) { [string]$ipif.Dhcp -eq "Enabled" } elseif ($cim) { [bool]$cim.DHCPEnabled } else { $false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$items | ConvertTo-Json -Depth 5 -Compress
|
$items | ConvertTo-Json -Depth 5 -Compress
|
||||||
"""
|
"""
|
||||||
result = run_hidden(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], timeout=15)
|
result = run_hidden(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], timeout=30)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise RuntimeError(result.stdout.strip() or "PowerShell 获取网卡信息失败")
|
raise RuntimeError(result.stdout.strip() or "PowerShell 获取网卡信息失败")
|
||||||
|
|
||||||
@@ -60,6 +82,8 @@ $items | ConvertTo-Json -Depth 5 -Compress
|
|||||||
return self._get_network_info_from_ipconfig()
|
return self._get_network_info_from_ipconfig()
|
||||||
|
|
||||||
data = json.loads(output[json_start:])
|
data = json.loads(output[json_start:])
|
||||||
|
if data is None:
|
||||||
|
return []
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
data = [data]
|
data = [data]
|
||||||
|
|
||||||
@@ -263,6 +287,54 @@ $items | ConvertTo-Json -Depth 5 -Compress
|
|||||||
|
|
||||||
self.output("静态 IPv4 配置已应用\n", "success")
|
self.output("静态 IPv4 配置已应用\n", "success")
|
||||||
|
|
||||||
|
def set_dns_servers(self, name: str, dns_servers: list[str]) -> None:
|
||||||
|
adapter_name = name.strip()
|
||||||
|
if not adapter_name:
|
||||||
|
raise ValueError("请选择网卡")
|
||||||
|
|
||||||
|
servers = [validate_ip(server) for server in dns_servers if str(server).strip()]
|
||||||
|
if not servers:
|
||||||
|
raise ValueError("请输入至少一个 DNS 服务器")
|
||||||
|
|
||||||
|
self._run_netsh(
|
||||||
|
[
|
||||||
|
"interface",
|
||||||
|
"ip",
|
||||||
|
"set",
|
||||||
|
"dnsservers",
|
||||||
|
f"name={adapter_name}",
|
||||||
|
"source=static",
|
||||||
|
f"address={servers[0]}",
|
||||||
|
"index=1",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for index, server in enumerate(servers[1:], start=2):
|
||||||
|
self._run_netsh(["interface", "ip", "add", "dnsservers", f"name={adapter_name}", f"address={server}", f"index={index}"])
|
||||||
|
self.output(f"已设置 DNS: {adapter_name} -> {', '.join(servers)}\n", "success")
|
||||||
|
|
||||||
|
def flush_dns_cache(self) -> None:
|
||||||
|
result = run_hidden(["ipconfig", "/flushdns"], timeout=10)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(result.stdout.strip() or "刷新 DNS 缓存失败")
|
||||||
|
self.output("已刷新 DNS 缓存\n", "success")
|
||||||
|
|
||||||
|
def set_adapter_enabled(self, name: str, enabled: bool) -> None:
|
||||||
|
adapter_name = name.strip()
|
||||||
|
if not adapter_name:
|
||||||
|
raise ValueError("请选择网卡")
|
||||||
|
|
||||||
|
action = "启用" if enabled else "禁用"
|
||||||
|
self.output(f"准备{action}网卡: {adapter_name}\n", "warning")
|
||||||
|
command = "Enable-NetAdapter" if enabled else "Disable-NetAdapter"
|
||||||
|
script = f"""
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
{command} -Name {self._ps_quote(adapter_name)} -Confirm:$false
|
||||||
|
"""
|
||||||
|
result = run_hidden(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], timeout=20)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(result.stdout.strip() or f"{action}网卡失败,请确认已用管理员权限运行")
|
||||||
|
self.output(f"网卡已{action}: {adapter_name}\n", "success")
|
||||||
|
|
||||||
def load_profiles(self) -> dict:
|
def load_profiles(self) -> dict:
|
||||||
if not os.path.exists(self.profiles_file):
|
if not os.path.exists(self.profiles_file):
|
||||||
return {}
|
return {}
|
||||||
@@ -301,3 +373,6 @@ $items | ConvertTo-Json -Depth 5 -Compress
|
|||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
message = (result.stdout + result.stderr).strip()
|
message = (result.stdout + result.stderr).strip()
|
||||||
raise RuntimeError(message or "netsh 命令执行失败,请确认已用管理员权限运行")
|
raise RuntimeError(message or "netsh 命令执行失败,请确认已用管理员权限运行")
|
||||||
|
|
||||||
|
def _ps_quote(self, value: str) -> str:
|
||||||
|
return "'" + value.replace("'", "''") + "'"
|
||||||
|
|||||||
+98
-32
@@ -1,36 +1,72 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import scrolledtext, ttk
|
from tkinter import ttk
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
from core.ui.theme import COLORS, FONT_MONO, FONT_SECTION, FONT_SMALL
|
from core.ui.theme import COLORS, FONT_MONO, FONT_SMALL
|
||||||
|
|
||||||
|
|
||||||
|
BUTTON_ICONS = {
|
||||||
|
"应用": "✓",
|
||||||
|
"开始": "▶",
|
||||||
|
"批量": "▶",
|
||||||
|
"测试": "▶",
|
||||||
|
"停止": "■",
|
||||||
|
"刷新": "⟳",
|
||||||
|
"重新": "⟳",
|
||||||
|
"自动": "◎",
|
||||||
|
"启用": "▷",
|
||||||
|
"禁用": "⊗",
|
||||||
|
"修复": "◇",
|
||||||
|
"复制": "⧉",
|
||||||
|
"导出": "⇩",
|
||||||
|
"导入": "⇧",
|
||||||
|
"保存": "+",
|
||||||
|
"删除": "×",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Console:
|
class Console:
|
||||||
def __init__(self, parent, height: int = 16):
|
def __init__(self, parent, height: int = 16):
|
||||||
self.widget = scrolledtext.ScrolledText(
|
self.frame = tk.Frame(parent, bg=COLORS["console_bg"], bd=0, highlightthickness=0)
|
||||||
parent,
|
self.frame.rowconfigure(0, weight=1)
|
||||||
|
self.frame.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
self.widget = tk.Text(
|
||||||
|
self.frame,
|
||||||
height=height,
|
height=height,
|
||||||
wrap="word",
|
wrap="word",
|
||||||
bg=COLORS["console_bg"],
|
bg=COLORS["console_bg"],
|
||||||
fg=COLORS["console_fg"],
|
fg=COLORS["console_fg"],
|
||||||
insertbackground=COLORS["console_fg"],
|
insertbackground=COLORS["console_fg"],
|
||||||
selectbackground="#2b4a6f",
|
selectbackground="#17385c",
|
||||||
relief="flat",
|
relief="flat",
|
||||||
borderwidth=0,
|
borderwidth=0,
|
||||||
font=FONT_MONO,
|
font=FONT_MONO,
|
||||||
padx=14,
|
padx=16,
|
||||||
pady=12,
|
pady=14,
|
||||||
)
|
)
|
||||||
|
self.widget.grid(row=0, column=0, sticky="nsew")
|
||||||
|
|
||||||
|
scrollbar = ttk.Scrollbar(
|
||||||
|
self.frame,
|
||||||
|
orient="vertical",
|
||||||
|
command=self.widget.yview,
|
||||||
|
style="Modern.Vertical.TScrollbar",
|
||||||
|
)
|
||||||
|
scrollbar.grid(row=0, column=1, sticky="ns")
|
||||||
|
self.widget.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
self.widget.tag_config("muted", foreground=COLORS["console_muted"])
|
self.widget.tag_config("muted", foreground=COLORS["console_muted"])
|
||||||
|
self.widget.tag_config("accent", foreground=COLORS["console_accent"])
|
||||||
self.widget.tag_config("success", foreground="#86efac")
|
self.widget.tag_config("success", foreground="#86efac")
|
||||||
self.widget.tag_config("warning", foreground="#fde68a")
|
self.widget.tag_config("warning", foreground="#fde68a")
|
||||||
self.widget.tag_config("error", foreground="#fca5a5")
|
self.widget.tag_config("error", foreground="#fca5a5")
|
||||||
|
|
||||||
def grid(self, **kwargs):
|
def grid(self, **kwargs):
|
||||||
self.widget.grid(**kwargs)
|
self.frame.grid(**kwargs)
|
||||||
|
|
||||||
def pack(self, **kwargs):
|
def pack(self, **kwargs):
|
||||||
self.widget.pack(**kwargs)
|
self.frame.pack(**kwargs)
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
self.widget.delete("1.0", tk.END)
|
self.widget.delete("1.0", tk.END)
|
||||||
@@ -45,30 +81,35 @@ class Console:
|
|||||||
|
|
||||||
class Page(ttk.Frame):
|
class Page(ttk.Frame):
|
||||||
def __init__(self, parent, title: str, subtitle: str):
|
def __init__(self, parent, title: str, subtitle: str):
|
||||||
super().__init__(parent, style="Panel.TFrame")
|
super().__init__(parent, style="Page.TFrame")
|
||||||
self.columnconfigure(0, weight=1)
|
self.columnconfigure(0, weight=1)
|
||||||
self.rowconfigure(1, weight=1)
|
self.rowconfigure(1, weight=1)
|
||||||
|
|
||||||
header = ttk.Frame(self, style="Panel.TFrame")
|
header = ttk.Frame(self, style="Page.TFrame")
|
||||||
header.grid(row=0, column=0, sticky="ew", padx=28, pady=(24, 8))
|
header.grid(row=0, column=0, sticky="ew", padx=28, pady=(28, 12))
|
||||||
header.columnconfigure(0, weight=1)
|
header.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
ttk.Label(header, text=title, style="Title.TLabel").grid(row=0, column=0, sticky="w")
|
ttk.Label(header, text=title, style="Title.TLabel").grid(row=0, column=0, sticky="w")
|
||||||
ttk.Label(header, text=subtitle, style="Muted.TLabel").grid(row=1, column=0, sticky="w", pady=(4, 0))
|
ttk.Label(header, text=subtitle, style="PageMuted.TLabel").grid(row=1, column=0, sticky="w", pady=(8, 0))
|
||||||
|
|
||||||
body_shell = ttk.Frame(self, style="Panel.TFrame")
|
body_shell = ttk.Frame(self, style="Page.TFrame")
|
||||||
body_shell.grid(row=1, column=0, sticky="nsew", padx=28, pady=(4, 20))
|
body_shell.grid(row=1, column=0, sticky="nsew", padx=28, pady=(0, 22))
|
||||||
body_shell.columnconfigure(0, weight=1)
|
body_shell.columnconfigure(0, weight=1)
|
||||||
body_shell.rowconfigure(0, weight=1)
|
body_shell.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
self._canvas = tk.Canvas(body_shell, bg=COLORS["panel"], highlightthickness=0, bd=0)
|
self._canvas = tk.Canvas(body_shell, bg=COLORS["bg"], highlightthickness=0, bd=0)
|
||||||
self._canvas.grid(row=0, column=0, sticky="nsew")
|
self._canvas.grid(row=0, column=0, sticky="nsew")
|
||||||
|
|
||||||
scrollbar = ttk.Scrollbar(body_shell, orient="vertical", command=self._canvas.yview)
|
self._scrollbar = ttk.Scrollbar(
|
||||||
scrollbar.grid(row=0, column=1, sticky="ns", padx=(8, 0))
|
body_shell,
|
||||||
self._canvas.configure(yscrollcommand=scrollbar.set)
|
orient="vertical",
|
||||||
|
command=self._canvas.yview,
|
||||||
|
style="Modern.Vertical.TScrollbar",
|
||||||
|
)
|
||||||
|
self._scrollbar.grid(row=0, column=1, sticky="ns", padx=(8, 0))
|
||||||
|
self._canvas.configure(yscrollcommand=self._sync_scrollbar)
|
||||||
|
|
||||||
self.body = ttk.Frame(self._canvas, style="Panel.TFrame")
|
self.body = ttk.Frame(self._canvas, style="Page.TFrame")
|
||||||
self.body.columnconfigure(0, weight=1)
|
self.body.columnconfigure(0, weight=1)
|
||||||
self._body_window = self._canvas.create_window((0, 0), window=self.body, anchor="nw")
|
self._body_window = self._canvas.create_window((0, 0), window=self.body, anchor="nw")
|
||||||
|
|
||||||
@@ -77,14 +118,22 @@ class Page(ttk.Frame):
|
|||||||
self._bind_mousewheel(self._canvas)
|
self._bind_mousewheel(self._canvas)
|
||||||
|
|
||||||
def section(self, title: str, row: int, columns: int = 4):
|
def section(self, title: str, row: int, columns: int = 4):
|
||||||
frame = tk.Frame(self.body, bg=COLORS["panel"], highlightbackground=COLORS["border"], highlightthickness=1)
|
frame = tk.Frame(
|
||||||
frame.grid(row=row, column=0, sticky="ew", pady=(0, 14))
|
self.body,
|
||||||
|
bg=COLORS["panel"],
|
||||||
|
highlightbackground=COLORS["border"],
|
||||||
|
highlightcolor=COLORS["border"],
|
||||||
|
highlightthickness=1,
|
||||||
|
bd=0,
|
||||||
|
)
|
||||||
|
frame.grid(row=row, column=0, sticky="ew", pady=(0, 16))
|
||||||
for col in range(columns):
|
for col in range(columns):
|
||||||
frame.columnconfigure(col, weight=1)
|
frame.columnconfigure(col, weight=1)
|
||||||
|
|
||||||
ttk.Label(frame, text=title, style="Section.TLabel").grid(
|
title_bar = ttk.Frame(frame, style="Panel.TFrame")
|
||||||
row=0, column=0, columnspan=columns, sticky="w", padx=18, pady=(14, 8)
|
title_bar.grid(row=0, column=0, columnspan=columns, sticky="ew", padx=22, pady=(18, 10))
|
||||||
)
|
ttk.Label(title_bar, text="▣", style="Section.TLabel", foreground=COLORS["primary"]).pack(side="left", padx=(0, 10))
|
||||||
|
ttk.Label(title_bar, text=title, style="Section.TLabel").pack(side="left")
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
def _update_scroll_region(self, _event=None) -> None:
|
def _update_scroll_region(self, _event=None) -> None:
|
||||||
@@ -93,6 +142,13 @@ class Page(ttk.Frame):
|
|||||||
def _resize_body(self, event) -> None:
|
def _resize_body(self, event) -> None:
|
||||||
self._canvas.itemconfigure(self._body_window, width=event.width)
|
self._canvas.itemconfigure(self._body_window, width=event.width)
|
||||||
|
|
||||||
|
def _sync_scrollbar(self, first: str, last: str) -> None:
|
||||||
|
self._scrollbar.set(first, last)
|
||||||
|
if float(first) <= 0 and float(last) >= 1:
|
||||||
|
self._scrollbar.grid_remove()
|
||||||
|
else:
|
||||||
|
self._scrollbar.grid()
|
||||||
|
|
||||||
def _bind_mousewheel(self, widget) -> None:
|
def _bind_mousewheel(self, widget) -> None:
|
||||||
widget.bind("<Enter>", lambda _event: widget.bind_all("<MouseWheel>", self._on_mousewheel))
|
widget.bind("<Enter>", lambda _event: widget.bind_all("<MouseWheel>", self._on_mousewheel))
|
||||||
widget.bind("<Leave>", lambda _event: widget.unbind_all("<MouseWheel>"))
|
widget.bind("<Leave>", lambda _event: widget.unbind_all("<MouseWheel>"))
|
||||||
@@ -105,10 +161,10 @@ class Page(ttk.Frame):
|
|||||||
|
|
||||||
def field(parent, label: str, row: int, column: int, value: str = "", width: int = 24, colspan: int = 1):
|
def field(parent, label: str, row: int, column: int, value: str = "", width: int = 24, colspan: int = 1):
|
||||||
frame = ttk.Frame(parent, style="Panel.TFrame")
|
frame = ttk.Frame(parent, style="Panel.TFrame")
|
||||||
frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=18, pady=(4, 14))
|
frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=22, pady=(4, 16))
|
||||||
frame.columnconfigure(0, weight=1)
|
frame.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 5))
|
ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 7))
|
||||||
var = tk.StringVar(value=value)
|
var = tk.StringVar(value=value)
|
||||||
entry = ttk.Entry(frame, textvariable=var, width=width)
|
entry = ttk.Entry(frame, textvariable=var, width=width)
|
||||||
entry.grid(row=1, column=0, sticky="ew")
|
entry.grid(row=1, column=0, sticky="ew")
|
||||||
@@ -118,10 +174,10 @@ def field(parent, label: str, row: int, column: int, value: str = "", width: int
|
|||||||
def combo(parent, label: str, row: int, column: int, values=None, value: str = "", width: int = 24, colspan: int = 1):
|
def combo(parent, label: str, row: int, column: int, values=None, value: str = "", width: int = 24, colspan: int = 1):
|
||||||
values = values or []
|
values = values or []
|
||||||
frame = ttk.Frame(parent, style="Panel.TFrame")
|
frame = ttk.Frame(parent, style="Panel.TFrame")
|
||||||
frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=18, pady=(4, 14))
|
frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=22, pady=(4, 16))
|
||||||
frame.columnconfigure(0, weight=1)
|
frame.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 5))
|
ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 7))
|
||||||
var = tk.StringVar(value=value)
|
var = tk.StringVar(value=value)
|
||||||
control = ttk.Combobox(frame, textvariable=var, values=values, width=width, state="readonly")
|
control = ttk.Combobox(frame, textvariable=var, values=values, width=width, state="readonly")
|
||||||
control.grid(row=1, column=0, sticky="ew")
|
control.grid(row=1, column=0, sticky="ew")
|
||||||
@@ -130,15 +186,25 @@ def combo(parent, label: str, row: int, column: int, values=None, value: str = "
|
|||||||
|
|
||||||
def action_bar(parent, row: int, columnspan: int = 4):
|
def action_bar(parent, row: int, columnspan: int = 4):
|
||||||
frame = ttk.Frame(parent, style="Panel.TFrame")
|
frame = ttk.Frame(parent, style="Panel.TFrame")
|
||||||
frame.grid(row=row, column=0, columnspan=columnspan, sticky="ew", padx=18, pady=(0, 16))
|
frame.grid(row=row, column=0, columnspan=columnspan, sticky="ew", padx=22, pady=(0, 18))
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
|
|
||||||
def button(parent, text: str, command: Callable, style: str = "Secondary.TButton"):
|
def button(parent, text: str, command: Callable, style: str = "Secondary.TButton"):
|
||||||
btn = ttk.Button(parent, text=text, command=command, style=style)
|
btn = ttk.Button(parent, text=_button_label(text), command=command, style=style)
|
||||||
btn.pack(side="left", padx=(0, 10))
|
btn.pack(side="left", padx=(0, 12))
|
||||||
return btn
|
return btn
|
||||||
|
|
||||||
|
|
||||||
|
def _button_label(text: str) -> str:
|
||||||
|
stripped = text.strip()
|
||||||
|
if not stripped:
|
||||||
|
return text
|
||||||
|
for keyword, icon in BUTTON_ICONS.items():
|
||||||
|
if stripped.startswith(keyword):
|
||||||
|
return f"{icon} {stripped}"
|
||||||
|
return stripped
|
||||||
|
|
||||||
|
|
||||||
def set_entry_state(item, enabled: bool) -> None:
|
def set_entry_state(item, enabled: bool) -> None:
|
||||||
item["entry"].configure(state="normal" if enabled else "disabled")
|
item["entry"].configure(state="normal" if enabled else "disabled")
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import tkinter as tk
|
|||||||
from tkinter import filedialog, messagebox, ttk
|
from tkinter import filedialog, messagebox, ttk
|
||||||
|
|
||||||
from core.Function.device_discovery_fun import ALL_ADAPTERS, DeviceDiscovery
|
from core.Function.device_discovery_fun import ALL_ADAPTERS, DeviceDiscovery
|
||||||
from core.ui.components import Console, Page, action_bar, button, combo, field
|
from core.ui.components import Page, action_bar, button, combo, field
|
||||||
|
|
||||||
|
|
||||||
class DeviceDiscoveryTab(Page):
|
class DeviceDiscoveryTab(Page):
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, console):
|
||||||
super().__init__(parent, "设备发现", "发现局域网在线与 ARP 可见设备,整理 IP、MAC、主机名、厂商与来源网卡。")
|
super().__init__(parent, "设备发现", "发现局域网在线与 ARP 可见设备,整理 IP、MAC、厂商与来源网卡。")
|
||||||
self.body.rowconfigure(3, weight=1)
|
self.console = console
|
||||||
|
self.body.rowconfigure(2, weight=1)
|
||||||
|
|
||||||
status = self.section("实时状态", 0, columns=6)
|
status = self.section("实时状态", 0, columns=6)
|
||||||
self.state = field(status, "状态", 1, 0, "等待", 10)
|
self.state = field(status, "状态", 1, 0, "等待", 10)
|
||||||
@@ -23,31 +24,33 @@ class DeviceDiscoveryTab(Page):
|
|||||||
|
|
||||||
params = self.section("发现参数", 1, columns=6)
|
params = self.section("发现参数", 1, columns=6)
|
||||||
self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 22)
|
self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 22)
|
||||||
|
self.adapter["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.fill_default_range(silent=True))
|
||||||
self.scan_range = field(params, "扫描范围", 1, 1, "", 34, colspan=2)
|
self.scan_range = field(params, "扫描范围", 1, 1, "", 34, colspan=2)
|
||||||
self.workers = field(params, "并发数", 1, 3, "64", 10)
|
self.workers = field(params, "并发数", 1, 3, "24", 10)
|
||||||
self.timeout = field(params, "超时 ms", 1, 4, "500", 10)
|
self.timeout = field(params, "超时 ms", 1, 4, "500", 10)
|
||||||
self.max_hosts = field(params, "最大地址数", 1, 5, "254", 10)
|
self.max_hosts = field(params, "最大地址数", 1, 5, "254", 10)
|
||||||
actions = action_bar(params, 2, 6)
|
primary_actions = action_bar(params, 2, 6)
|
||||||
self.start_btn = button(actions, "开始发现", self.start_discovery, "Primary.TButton")
|
self.start_btn = button(primary_actions, "开始发现", self.start_discovery, "Primary.TButton")
|
||||||
self.stop_btn = button(actions, "停止", self.stop_discovery, "Danger.TButton")
|
self.stop_btn = button(primary_actions, "停止", self.stop_discovery, "Danger.TButton")
|
||||||
self.refresh_btn = button(actions, "刷新网卡", self.load_adapters, "Secondary.TButton")
|
self.refresh_btn = button(primary_actions, "刷新网卡", self.load_adapters, "Secondary.TButton")
|
||||||
self.auto_range_btn = button(actions, "自动范围", self.fill_default_range, "Secondary.TButton")
|
self.auto_range_btn = button(primary_actions, "自动范围", self.fill_default_range, "Secondary.TButton")
|
||||||
self.copy_btn = button(actions, "复制清单", self.copy_inventory, "Secondary.TButton")
|
self.stop_btn.configure(state="disabled")
|
||||||
self.export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
|
secondary_actions = action_bar(params, 3, 6)
|
||||||
|
self.copy_btn = button(secondary_actions, "复制清单", self.copy_inventory, "Secondary.TButton")
|
||||||
|
self.export_btn = button(secondary_actions, "导出CSV", self.export_results, "Secondary.TButton")
|
||||||
|
|
||||||
results = self.section("发现结果", 2, columns=1)
|
results = self.section("发现结果", 2, columns=1)
|
||||||
results.rowconfigure(1, weight=1)
|
results.rowconfigure(1, weight=1)
|
||||||
results.columnconfigure(0, weight=1)
|
results.columnconfigure(0, weight=1)
|
||||||
self.results_tree = ttk.Treeview(
|
self.results_tree = ttk.Treeview(
|
||||||
results,
|
results,
|
||||||
columns=("ip", "mac", "hostname", "vendor", "adapter", "latency", "method", "note"),
|
columns=("ip", "mac", "vendor", "adapter", "latency", "method", "note"),
|
||||||
show="headings",
|
show="headings",
|
||||||
height=10,
|
height=10,
|
||||||
)
|
)
|
||||||
headings = {
|
headings = {
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"mac": "MAC",
|
"mac": "MAC",
|
||||||
"hostname": "主机名",
|
|
||||||
"vendor": "厂商",
|
"vendor": "厂商",
|
||||||
"adapter": "来源网卡",
|
"adapter": "来源网卡",
|
||||||
"latency": "延迟",
|
"latency": "延迟",
|
||||||
@@ -55,14 +58,13 @@ class DeviceDiscoveryTab(Page):
|
|||||||
"note": "备注",
|
"note": "备注",
|
||||||
}
|
}
|
||||||
widths = {
|
widths = {
|
||||||
"ip": 130,
|
"ip": 112,
|
||||||
"mac": 150,
|
"mac": 132,
|
||||||
"hostname": 180,
|
"vendor": 120,
|
||||||
"vendor": 130,
|
|
||||||
"adapter": 150,
|
"adapter": 150,
|
||||||
"latency": 85,
|
"latency": 72,
|
||||||
"method": 90,
|
"method": 80,
|
||||||
"note": 130,
|
"note": 140,
|
||||||
}
|
}
|
||||||
for column, title in headings.items():
|
for column, title in headings.items():
|
||||||
self.results_tree.heading(column, text=title)
|
self.results_tree.heading(column, text=title)
|
||||||
@@ -73,13 +75,9 @@ class DeviceDiscoveryTab(Page):
|
|||||||
self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
|
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 = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview)
|
||||||
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
||||||
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
x_scroll = ttk.Scrollbar(results, orient="horizontal", command=self.results_tree.xview)
|
||||||
|
x_scroll.grid(row=2, column=0, sticky="ew", padx=18, pady=(0, 18))
|
||||||
output = self.section("发现控制台", 3, columns=1)
|
self.results_tree.configure(yscrollcommand=result_scroll.set, xscrollcommand=x_scroll.set)
|
||||||
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.discovery = DeviceDiscovery(self.write, self.on_task_done, self.update_status, self.add_result)
|
self.discovery = DeviceDiscovery(self.write, self.on_task_done, self.update_status, self.add_result)
|
||||||
self.after(350, self.load_adapters)
|
self.after(350, self.load_adapters)
|
||||||
@@ -106,48 +104,58 @@ class DeviceDiscoveryTab(Page):
|
|||||||
def load_adapters(self):
|
def load_adapters(self):
|
||||||
def worker():
|
def worker():
|
||||||
try:
|
try:
|
||||||
values = self.discovery.get_adapter_choices()
|
values, default_range, default_adapter = self.discovery.get_adapter_choices_and_default_range()
|
||||||
default_range = self.discovery.default_scan_range(ALL_ADAPTERS)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
values = [ALL_ADAPTERS]
|
values = [ALL_ADAPTERS]
|
||||||
default_range = ""
|
default_range = ""
|
||||||
|
default_adapter = ""
|
||||||
self.write(f"读取网卡失败: {exc}\n", "warning")
|
self.write(f"读取网卡失败: {exc}\n", "warning")
|
||||||
self.after(0, lambda: self.apply_adapters(values, default_range))
|
self.after(0, lambda: self.apply_adapters(values, default_range, default_adapter))
|
||||||
|
|
||||||
threading.Thread(target=worker, daemon=True).start()
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
def apply_adapters(self, values, default_range):
|
def apply_adapters(self, values, default_range, default_adapter=""):
|
||||||
values = values or [ALL_ADAPTERS]
|
values = values or [ALL_ADAPTERS]
|
||||||
values = list(dict.fromkeys(values))
|
values = list(dict.fromkeys(values))
|
||||||
self.adapter["combobox"]["values"] = values
|
self.adapter["combobox"]["values"] = values
|
||||||
if self.adapter["var"].get() not in values:
|
if self.adapter["var"].get() not in values or self.adapter["var"].get() == ALL_ADAPTERS:
|
||||||
self.adapter["var"].set(values[1] if len(values) > 1 else values[0])
|
self.adapter["var"].set(default_adapter if default_adapter in values else values[1] if len(values) > 1 else values[0])
|
||||||
if not self.scan_range["var"].get() and default_range:
|
if not self.scan_range["var"].get() and default_range:
|
||||||
self.scan_range["var"].set(default_range)
|
self.scan_range["var"].set(default_range)
|
||||||
|
|
||||||
def fill_default_range(self):
|
def fill_default_range(self, silent=False):
|
||||||
try:
|
try:
|
||||||
value = self.discovery.default_scan_range(self.adapter["var"].get())
|
value = self.discovery.default_scan_range(self.adapter["var"].get())
|
||||||
if not value:
|
if not value:
|
||||||
|
if not silent:
|
||||||
messagebox.showinfo("提示", "未能根据当前网卡生成扫描范围")
|
messagebox.showinfo("提示", "未能根据当前网卡生成扫描范围")
|
||||||
return
|
return
|
||||||
self.scan_range["var"].set(value)
|
self.scan_range["var"].set(value)
|
||||||
|
if not silent:
|
||||||
self.write(f"已生成安全扫描范围: {value}\n", "success")
|
self.write(f"已生成安全扫描范围: {value}\n", "success")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
if not silent:
|
||||||
messagebox.showwarning("生成失败", str(exc))
|
messagebox.showwarning("生成失败", str(exc))
|
||||||
|
|
||||||
def start_discovery(self):
|
def start_discovery(self):
|
||||||
try:
|
try:
|
||||||
self.clear()
|
self.clear()
|
||||||
self.start_btn.configure(state="disabled")
|
self.start_btn.configure(state="disabled")
|
||||||
|
self.stop_btn.configure(state="normal")
|
||||||
|
self.refresh_btn.configure(state="disabled")
|
||||||
|
self.auto_range_btn.configure(state="disabled")
|
||||||
self.discovery.start_discovery(self.adapter["var"].get(), self.options())
|
self.discovery.start_discovery(self.adapter["var"].get(), self.options())
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.start_btn.configure(state="normal")
|
self.start_btn.configure(state="normal")
|
||||||
|
self.stop_btn.configure(state="disabled")
|
||||||
|
self.refresh_btn.configure(state="normal")
|
||||||
|
self.auto_range_btn.configure(state="normal")
|
||||||
messagebox.showwarning("无法开始设备发现", str(exc))
|
messagebox.showwarning("无法开始设备发现", str(exc))
|
||||||
|
|
||||||
def stop_discovery(self):
|
def stop_discovery(self):
|
||||||
try:
|
try:
|
||||||
self.discovery.stop_discovery()
|
self.discovery.stop_discovery()
|
||||||
|
self.stop_btn.configure(state="disabled")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
messagebox.showinfo("提示", str(exc))
|
messagebox.showinfo("提示", str(exc))
|
||||||
|
|
||||||
@@ -217,7 +225,6 @@ class DeviceDiscoveryTab(Page):
|
|||||||
values=(
|
values=(
|
||||||
row.get("ip", ""),
|
row.get("ip", ""),
|
||||||
row.get("mac", ""),
|
row.get("mac", ""),
|
||||||
row.get("hostname", ""),
|
|
||||||
row.get("vendor", ""),
|
row.get("vendor", ""),
|
||||||
row.get("adapter", ""),
|
row.get("adapter", ""),
|
||||||
latency_text,
|
latency_text,
|
||||||
@@ -230,4 +237,12 @@ class DeviceDiscoveryTab(Page):
|
|||||||
self.after(0, apply)
|
self.after(0, apply)
|
||||||
|
|
||||||
def on_task_done(self):
|
def on_task_done(self):
|
||||||
self.after(0, lambda: self.start_btn.configure(state="normal"))
|
self.after(
|
||||||
|
0,
|
||||||
|
lambda: (
|
||||||
|
self.start_btn.configure(state="normal"),
|
||||||
|
self.stop_btn.configure(state="disabled"),
|
||||||
|
self.refresh_btn.configure(state="normal"),
|
||||||
|
self.auto_range_btn.configure(state="normal"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
+56
-21
@@ -3,13 +3,14 @@ import tkinter as tk
|
|||||||
from tkinter import filedialog, messagebox, ttk
|
from tkinter import filedialog, messagebox, ttk
|
||||||
|
|
||||||
from core.Function.dns_diag_fun import ALL_ADAPTERS, DEFAULT_DOMAINS, DEFAULT_RECORD_TYPES, DnsDiagnostic
|
from core.Function.dns_diag_fun import ALL_ADAPTERS, DEFAULT_DOMAINS, DEFAULT_RECORD_TYPES, DnsDiagnostic
|
||||||
from core.ui.components import Console, Page, action_bar, button, combo, field
|
from core.ui.components import Page, action_bar, button, combo, field
|
||||||
|
|
||||||
|
|
||||||
class DnsTab(Page):
|
class DnsTab(Page):
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, console):
|
||||||
super().__init__(parent, "DNS 诊断", "对比本机 DNS 与常用 DNS 的解析结果、耗时和失败原因。")
|
super().__init__(parent, "DNS 诊断", "对比本机 DNS 与常用 DNS 的解析结果、耗时和失败原因。")
|
||||||
self.body.rowconfigure(3, weight=1)
|
self.console = console
|
||||||
|
self.body.rowconfigure(2, weight=1)
|
||||||
|
|
||||||
status = self.section("实时状态", 0, columns=6)
|
status = self.section("实时状态", 0, columns=6)
|
||||||
self.state = field(status, "状态", 1, 0, "等待", 10)
|
self.state = field(status, "状态", 1, 0, "等待", 10)
|
||||||
@@ -33,6 +34,7 @@ class DnsTab(Page):
|
|||||||
self.stop_btn = button(actions, "停止", self.stop_diagnosis, "Danger.TButton")
|
self.stop_btn = button(actions, "停止", self.stop_diagnosis, "Danger.TButton")
|
||||||
self.refresh_btn = button(actions, "刷新网卡", self.load_adapters, "Secondary.TButton")
|
self.refresh_btn = button(actions, "刷新网卡", self.load_adapters, "Secondary.TButton")
|
||||||
self.auto_dns_btn = button(actions, "自动 DNS", self.fill_default_dns, "Secondary.TButton")
|
self.auto_dns_btn = button(actions, "自动 DNS", self.fill_default_dns, "Secondary.TButton")
|
||||||
|
self.repair_btn = button(actions, "修复异常", self.repair_dns, "Secondary.TButton")
|
||||||
self.copy_btn = button(actions, "复制摘要", self.copy_summary, "Secondary.TButton")
|
self.copy_btn = button(actions, "复制摘要", self.copy_summary, "Secondary.TButton")
|
||||||
self.export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
|
self.export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
|
||||||
|
|
||||||
@@ -55,13 +57,13 @@ class DnsTab(Page):
|
|||||||
"verdict": "错误 / 判断",
|
"verdict": "错误 / 判断",
|
||||||
}
|
}
|
||||||
widths = {
|
widths = {
|
||||||
"domain": 160,
|
"domain": 130,
|
||||||
"type": 70,
|
"type": 56,
|
||||||
"server": 130,
|
"server": 116,
|
||||||
"status": 80,
|
"status": 64,
|
||||||
"elapsed": 85,
|
"elapsed": 76,
|
||||||
"values": 300,
|
"values": 220,
|
||||||
"verdict": 360,
|
"verdict": 260,
|
||||||
}
|
}
|
||||||
for column, title in headings.items():
|
for column, title in headings.items():
|
||||||
self.results_tree.heading(column, text=title)
|
self.results_tree.heading(column, text=title)
|
||||||
@@ -72,13 +74,9 @@ class DnsTab(Page):
|
|||||||
self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
|
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 = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview)
|
||||||
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
||||||
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
x_scroll = ttk.Scrollbar(results, orient="horizontal", command=self.results_tree.xview)
|
||||||
|
x_scroll.grid(row=2, column=0, sticky="ew", padx=18, pady=(0, 18))
|
||||||
output = self.section("诊断控制台", 3, columns=1)
|
self.results_tree.configure(yscrollcommand=result_scroll.set, xscrollcommand=x_scroll.set)
|
||||||
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.diagnostic = DnsDiagnostic(self.write, self.on_task_done, self.update_status, self.add_result)
|
self.diagnostic = DnsDiagnostic(self.write, self.on_task_done, self.update_status, self.add_result)
|
||||||
self.after(350, self.load_adapters)
|
self.after(350, self.load_adapters)
|
||||||
@@ -104,8 +102,7 @@ class DnsTab(Page):
|
|||||||
def load_adapters(self):
|
def load_adapters(self):
|
||||||
def worker():
|
def worker():
|
||||||
try:
|
try:
|
||||||
values = self.diagnostic.get_adapter_choices()
|
values, default_dns = self.diagnostic.get_adapter_choices_and_default_dns()
|
||||||
default_dns = self.diagnostic.default_dns_servers(values[1] if len(values) > 1 else ALL_ADAPTERS)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
values = [ALL_ADAPTERS]
|
values = [ALL_ADAPTERS]
|
||||||
default_dns = ""
|
default_dns = ""
|
||||||
@@ -119,7 +116,7 @@ class DnsTab(Page):
|
|||||||
values = list(dict.fromkeys(values))
|
values = list(dict.fromkeys(values))
|
||||||
self.adapter["combobox"]["values"] = values
|
self.adapter["combobox"]["values"] = values
|
||||||
if self.adapter["var"].get() not in values:
|
if self.adapter["var"].get() not in values:
|
||||||
self.adapter["var"].set(values[1] if len(values) > 1 else values[0])
|
self.adapter["var"].set(ALL_ADAPTERS)
|
||||||
if not self.dns_servers["var"].get() and default_dns:
|
if not self.dns_servers["var"].get() and default_dns:
|
||||||
self.dns_servers["var"].set(default_dns)
|
self.dns_servers["var"].set(default_dns)
|
||||||
|
|
||||||
@@ -138,9 +135,11 @@ class DnsTab(Page):
|
|||||||
try:
|
try:
|
||||||
self.clear()
|
self.clear()
|
||||||
self.start_btn.configure(state="disabled")
|
self.start_btn.configure(state="disabled")
|
||||||
|
self.repair_btn.configure(state="disabled")
|
||||||
self.diagnostic.start_diagnosis(self.adapter["var"].get(), self.options())
|
self.diagnostic.start_diagnosis(self.adapter["var"].get(), self.options())
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.start_btn.configure(state="normal")
|
self.start_btn.configure(state="normal")
|
||||||
|
self.repair_btn.configure(state="normal")
|
||||||
messagebox.showwarning("无法开始 DNS 诊断", str(exc))
|
messagebox.showwarning("无法开始 DNS 诊断", str(exc))
|
||||||
|
|
||||||
def stop_diagnosis(self):
|
def stop_diagnosis(self):
|
||||||
@@ -172,6 +171,42 @@ class DnsTab(Page):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
messagebox.showwarning("导出失败", str(exc))
|
messagebox.showwarning("导出失败", str(exc))
|
||||||
|
|
||||||
|
def repair_dns(self):
|
||||||
|
adapter = self.adapter["var"].get() or ALL_ADAPTERS
|
||||||
|
if not messagebox.askyesno(
|
||||||
|
"确认修复 DNS",
|
||||||
|
f"将把“{adapter}”的 DNS 设置为 223.5.5.5 和 114.114.114.114,并刷新 DNS 缓存。\n\n此操作需要管理员权限,确定继续吗?",
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.repair_btn.configure(state="disabled")
|
||||||
|
self.start_btn.configure(state="disabled")
|
||||||
|
self.write("\n开始修复 DNS 异常...\n", "warning")
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
try:
|
||||||
|
result = self.diagnostic.repair_abnormal_dns(adapter)
|
||||||
|
self.after(0, lambda: self.on_repair_success(result))
|
||||||
|
except Exception as exc:
|
||||||
|
self.after(0, lambda: self.on_repair_failed(exc))
|
||||||
|
|
||||||
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
def on_repair_success(self, result):
|
||||||
|
names = "、".join(result.get("adapters", []))
|
||||||
|
servers = ",".join(result.get("servers", []))
|
||||||
|
self.write(f"DNS 修复完成: {names} -> {servers}\n", "success")
|
||||||
|
self.dns_servers["var"].set(servers)
|
||||||
|
self.load_adapters()
|
||||||
|
self.start_btn.configure(state="normal")
|
||||||
|
self.repair_btn.configure(state="normal")
|
||||||
|
|
||||||
|
def on_repair_failed(self, exc):
|
||||||
|
self.write(f"DNS 修复失败: {exc}\n", "error")
|
||||||
|
self.start_btn.configure(state="normal")
|
||||||
|
self.repair_btn.configure(state="normal")
|
||||||
|
messagebox.showerror("DNS 修复失败", f"{exc}\n\n请确认程序已用管理员权限运行。")
|
||||||
|
|
||||||
def copy_summary(self):
|
def copy_summary(self):
|
||||||
text = self.diagnostic.copy_summary()
|
text = self.diagnostic.copy_summary()
|
||||||
if not text:
|
if not text:
|
||||||
@@ -226,4 +261,4 @@ class DnsTab(Page):
|
|||||||
self.after(0, apply)
|
self.after(0, apply)
|
||||||
|
|
||||||
def on_task_done(self):
|
def on_task_done(self):
|
||||||
self.after(0, lambda: self.start_btn.configure(state="normal"))
|
self.after(0, lambda: (self.start_btn.configure(state="normal"), self.repair_btn.configure(state="normal")))
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import tkinter as tk
|
|||||||
from tkinter import filedialog, messagebox, ttk
|
from tkinter import filedialog, messagebox, ttk
|
||||||
|
|
||||||
from core.Function.ip_conflict_fun import ALL_ADAPTERS, MODE_BOTH, MODE_LOCAL, MODE_SUBNET, IpConflictDetector
|
from core.Function.ip_conflict_fun import ALL_ADAPTERS, MODE_BOTH, MODE_LOCAL, MODE_SUBNET, IpConflictDetector
|
||||||
from core.ui.components import Console, Page, action_bar, button, combo, field
|
from core.ui.components import Page, action_bar, button, combo, field
|
||||||
|
|
||||||
|
|
||||||
class IpConflictTab(Page):
|
class IpConflictTab(Page):
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, console):
|
||||||
super().__init__(parent, "IP 冲突", "检测本机 IP 是否被占用,并安全扫描网段内 IP/MAC 异常。")
|
super().__init__(parent, "IP 冲突", "检测本机 IP 是否被占用,并安全扫描网段内 IP/MAC 异常。")
|
||||||
self.body.rowconfigure(3, weight=1)
|
self.console = console
|
||||||
|
self.body.rowconfigure(2, weight=1)
|
||||||
|
|
||||||
status = self.section("实时状态", 0, columns=6)
|
status = self.section("实时状态", 0, columns=6)
|
||||||
self.state = field(status, "状态", 1, 0, "等待", 10)
|
self.state = field(status, "状态", 1, 0, "等待", 10)
|
||||||
@@ -74,12 +75,6 @@ class IpConflictTab(Page):
|
|||||||
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
||||||
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
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 = IpConflictDetector(self.write, self.on_task_done, self.update_status, self.add_result)
|
self.detector = IpConflictDetector(self.write, self.on_task_done, self.update_status, self.add_result)
|
||||||
self.after(350, self.load_adapters)
|
self.after(350, self.load_adapters)
|
||||||
|
|
||||||
|
|||||||
+4
-9
@@ -3,13 +3,14 @@ import tkinter as tk
|
|||||||
from tkinter import filedialog, messagebox, ttk
|
from tkinter import filedialog, messagebox, ttk
|
||||||
|
|
||||||
from core.Function.loop_fun import ALL_ADAPTERS, LoopDetector
|
from core.Function.loop_fun import ALL_ADAPTERS, LoopDetector
|
||||||
from core.ui.components import Console, Page, action_bar, button, combo, field
|
from core.ui.components import Page, action_bar, button, combo, field
|
||||||
|
|
||||||
|
|
||||||
class LoopTab(Page):
|
class LoopTab(Page):
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, console):
|
||||||
super().__init__(parent, "环网检测", "基于本机证据判断疑似二层环路、广播风暴和网关抖动风险。")
|
super().__init__(parent, "环网检测", "基于本机证据判断疑似二层环路、广播风暴和网关抖动风险。")
|
||||||
self.body.rowconfigure(3, weight=1)
|
self.console = console
|
||||||
|
self.body.rowconfigure(2, weight=1)
|
||||||
|
|
||||||
status = self.section("实时状态", 0, columns=6)
|
status = self.section("实时状态", 0, columns=6)
|
||||||
self.state = field(status, "状态", 1, 0, "等待", 10)
|
self.state = field(status, "状态", 1, 0, "等待", 10)
|
||||||
@@ -79,12 +80,6 @@ class LoopTab(Page):
|
|||||||
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
||||||
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
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.detector = LoopDetector(self.write, self.on_task_done, self.update_status, self.add_result)
|
||||||
self.after(350, self.load_adapters)
|
self.after(350, self.load_adapters)
|
||||||
|
|
||||||
|
|||||||
+65
-9
@@ -2,13 +2,14 @@ import threading
|
|||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
|
|
||||||
from core.Function.network_fun import NetworkManager
|
from core.Function.network_fun import NetworkManager
|
||||||
from core.ui.components import Console, Page, action_bar, button, combo, field, set_entry_state
|
from core.ui.components import Page, action_bar, button, combo, field, set_entry_state
|
||||||
|
|
||||||
|
|
||||||
class NetworkTab(Page):
|
class NetworkTab(Page):
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, console):
|
||||||
super().__init__(parent, "网卡配置", "查看本机网卡信息,切换 DHCP,或写入静态 IPv4 / DNS 配置。")
|
super().__init__(parent, "网卡配置", "查看本机网卡信息,切换 DHCP,或写入静态 IPv4 / DNS 配置。")
|
||||||
self.body.rowconfigure(4, weight=1)
|
self.console = console
|
||||||
|
self.body.rowconfigure(3, weight=1)
|
||||||
self.adapters = []
|
self.adapters = []
|
||||||
self.profiles = {}
|
self.profiles = {}
|
||||||
|
|
||||||
@@ -23,6 +24,9 @@ class NetworkTab(Page):
|
|||||||
identity = self.section("网卡信息", 1, columns=4)
|
identity = self.section("网卡信息", 1, columns=4)
|
||||||
self.description = field(identity, "设备描述", 1, 0, "", 46, colspan=2)
|
self.description = field(identity, "设备描述", 1, 0, "", 46, colspan=2)
|
||||||
self.mac = field(identity, "MAC 地址", 1, 2, "", 24)
|
self.mac = field(identity, "MAC 地址", 1, 2, "", 24)
|
||||||
|
adapter_actions = action_bar(identity, 2, 4)
|
||||||
|
self.enable_adapter_btn = button(adapter_actions, "启用网卡", self.enable_adapter, "Primary.TButton")
|
||||||
|
self.disable_adapter_btn = button(adapter_actions, "禁用网卡", self.disable_adapter, "Danger.TButton")
|
||||||
for item in (self.description, self.mac):
|
for item in (self.description, self.mac):
|
||||||
item["entry"].configure(state="disabled")
|
item["entry"].configure(state="disabled")
|
||||||
|
|
||||||
@@ -46,12 +50,6 @@ class NetworkTab(Page):
|
|||||||
self.apply_profile_btn = button(profile_actions, "套用模板到表单", self.apply_profile_to_form, "Secondary.TButton")
|
self.apply_profile_btn = button(profile_actions, "套用模板到表单", self.apply_profile_to_form, "Secondary.TButton")
|
||||||
self.delete_profile_btn = button(profile_actions, "删除模板", self.delete_profile, "Danger.TButton")
|
self.delete_profile_btn = button(profile_actions, "删除模板", self.delete_profile, "Danger.TButton")
|
||||||
|
|
||||||
output = self.section("输出控制台", 4, 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.netmgr = NetworkManager(self.write)
|
self.netmgr = NetworkManager(self.write)
|
||||||
self.load_profiles()
|
self.load_profiles()
|
||||||
self.after(250, self.refresh_adapters)
|
self.after(250, self.refresh_adapters)
|
||||||
@@ -62,6 +60,8 @@ class NetworkTab(Page):
|
|||||||
def refresh_adapters(self, clear=True):
|
def refresh_adapters(self, clear=True):
|
||||||
self.refresh_btn.configure(state="disabled")
|
self.refresh_btn.configure(state="disabled")
|
||||||
self.reload_btn.configure(state="disabled")
|
self.reload_btn.configure(state="disabled")
|
||||||
|
self.enable_adapter_btn.configure(state="disabled")
|
||||||
|
self.disable_adapter_btn.configure(state="disabled")
|
||||||
if clear:
|
if clear:
|
||||||
self.console.clear()
|
self.console.clear()
|
||||||
self.write("正在读取本机网卡信息...\n", "muted")
|
self.write("正在读取本机网卡信息...\n", "muted")
|
||||||
@@ -86,6 +86,7 @@ class NetworkTab(Page):
|
|||||||
self.write(f"读取完成,共发现 {len(names)} 个网卡\n", "success")
|
self.write(f"读取完成,共发现 {len(names)} 个网卡\n", "success")
|
||||||
else:
|
else:
|
||||||
self.write("未发现可用网卡\n", "warning")
|
self.write("未发现可用网卡\n", "warning")
|
||||||
|
self.update_adapter_action_state()
|
||||||
self.refresh_btn.configure(state="normal")
|
self.refresh_btn.configure(state="normal")
|
||||||
self.reload_btn.configure(state="normal")
|
self.reload_btn.configure(state="normal")
|
||||||
|
|
||||||
@@ -93,6 +94,7 @@ class NetworkTab(Page):
|
|||||||
self.write(f"读取网卡失败: {exc}\n", "error")
|
self.write(f"读取网卡失败: {exc}\n", "error")
|
||||||
self.refresh_btn.configure(state="normal")
|
self.refresh_btn.configure(state="normal")
|
||||||
self.reload_btn.configure(state="normal")
|
self.reload_btn.configure(state="normal")
|
||||||
|
self.update_adapter_action_state()
|
||||||
messagebox.showerror("读取网卡失败", str(exc))
|
messagebox.showerror("读取网卡失败", str(exc))
|
||||||
|
|
||||||
def load_selected_adapter(self):
|
def load_selected_adapter(self):
|
||||||
@@ -118,6 +120,7 @@ class NetworkTab(Page):
|
|||||||
self.description["entry"].configure(state="disabled")
|
self.description["entry"].configure(state="disabled")
|
||||||
self.mac["entry"].configure(state="disabled")
|
self.mac["entry"].configure(state="disabled")
|
||||||
self.update_entry_state()
|
self.update_entry_state()
|
||||||
|
self.update_adapter_action_state(adapter)
|
||||||
self.write_current_adapter(adapter)
|
self.write_current_adapter(adapter)
|
||||||
|
|
||||||
def current_adapter(self):
|
def current_adapter(self):
|
||||||
@@ -132,6 +135,59 @@ class NetworkTab(Page):
|
|||||||
for item in (self.ipv4, self.netmask, self.gateway, self.dns1, self.dns2):
|
for item in (self.ipv4, self.netmask, self.gateway, self.dns1, self.dns2):
|
||||||
set_entry_state(item, static)
|
set_entry_state(item, static)
|
||||||
|
|
||||||
|
def update_adapter_action_state(self, adapter=None):
|
||||||
|
adapter = adapter or self.current_adapter()
|
||||||
|
if not adapter:
|
||||||
|
self.enable_adapter_btn.configure(state="disabled")
|
||||||
|
self.disable_adapter_btn.configure(state="disabled")
|
||||||
|
return
|
||||||
|
|
||||||
|
status = str(adapter.get("status", "")).lower()
|
||||||
|
disabled = "disabled" in status or "禁用" in status
|
||||||
|
self.enable_adapter_btn.configure(state="normal" if disabled else "disabled")
|
||||||
|
self.disable_adapter_btn.configure(state="disabled" if disabled else "normal")
|
||||||
|
|
||||||
|
def enable_adapter(self):
|
||||||
|
self.set_adapter_enabled(True)
|
||||||
|
|
||||||
|
def disable_adapter(self):
|
||||||
|
adapter = self.current_adapter()
|
||||||
|
if not adapter:
|
||||||
|
messagebox.showwarning("无法禁用网卡", "请先选择网卡")
|
||||||
|
return
|
||||||
|
name = adapter.get("name", "")
|
||||||
|
if not messagebox.askyesno("确认禁用网卡", f"确定禁用网卡“{name}”吗?\n\n这可能会中断当前网络连接。"):
|
||||||
|
return
|
||||||
|
self.set_adapter_enabled(False)
|
||||||
|
|
||||||
|
def set_adapter_enabled(self, enabled: bool):
|
||||||
|
adapter = self.current_adapter()
|
||||||
|
if not adapter:
|
||||||
|
messagebox.showwarning("无法操作网卡", "请先选择网卡")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.enable_adapter_btn.configure(state="disabled")
|
||||||
|
self.disable_adapter_btn.configure(state="disabled")
|
||||||
|
action = "启用" if enabled else "禁用"
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
try:
|
||||||
|
self.netmgr.set_adapter_enabled(adapter.get("name", ""), enabled)
|
||||||
|
self.after(0, lambda: self.on_adapter_action_success(action))
|
||||||
|
except Exception as exc:
|
||||||
|
self.after(0, lambda: self.on_adapter_action_failed(action, exc))
|
||||||
|
|
||||||
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
def on_adapter_action_success(self, action: str):
|
||||||
|
self.write(f"网卡{action}完成,正在刷新网卡信息...\n", "success")
|
||||||
|
self.refresh_adapters(clear=False)
|
||||||
|
|
||||||
|
def on_adapter_action_failed(self, action: str, exc):
|
||||||
|
self.write(f"网卡{action}失败: {exc}\n", "error")
|
||||||
|
self.update_adapter_action_state()
|
||||||
|
messagebox.showerror(f"网卡{action}失败", f"{exc}\n\n请确认程序已用管理员权限运行。")
|
||||||
|
|
||||||
def write_current_adapter(self, adapter):
|
def write_current_adapter(self, adapter):
|
||||||
self.write("\n当前网卡:\n", "muted")
|
self.write("\n当前网卡:\n", "muted")
|
||||||
rows = [
|
rows = [
|
||||||
|
|||||||
+4
-9
@@ -5,7 +5,7 @@ from tkinter import filedialog, messagebox, ttk
|
|||||||
|
|
||||||
from core.Function.network_fun import NetworkManager
|
from core.Function.network_fun import NetworkManager
|
||||||
from core.Function.ping_fun import PingFun
|
from core.Function.ping_fun import PingFun
|
||||||
from core.ui.components import Console, Page, action_bar, button, combo, field
|
from core.ui.components import Page, action_bar, button, combo, field
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_SOURCE = "默认路由"
|
DEFAULT_SOURCE = "默认路由"
|
||||||
@@ -13,9 +13,10 @@ IP_PATTERN = re.compile(r"(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)")
|
|||||||
|
|
||||||
|
|
||||||
class PingTab(Page):
|
class PingTab(Page):
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, console):
|
||||||
super().__init__(parent, "Ping 探测", "单点 Ping、批量探活、参数化诊断与结果导出。")
|
super().__init__(parent, "Ping 探测", "单点 Ping、批量探活、参数化诊断与结果导出。")
|
||||||
self.body.rowconfigure(2, weight=1)
|
self.console = console
|
||||||
|
self.body.rowconfigure(1, weight=1)
|
||||||
|
|
||||||
status = self.section("实时状态", 0, columns=7)
|
status = self.section("实时状态", 0, columns=7)
|
||||||
self.state = field(status, "状态", 1, 0, "等待", 12)
|
self.state = field(status, "状态", 1, 0, "等待", 12)
|
||||||
@@ -38,12 +39,6 @@ class PingTab(Page):
|
|||||||
self.build_single_tab()
|
self.build_single_tab()
|
||||||
self.build_batch_tab()
|
self.build_batch_tab()
|
||||||
|
|
||||||
output = self.section("输出控制台", 2, columns=1)
|
|
||||||
output.rowconfigure(1, weight=1)
|
|
||||||
output.columnconfigure(0, weight=1)
|
|
||||||
self.console = Console(output, height=16)
|
|
||||||
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
|
|
||||||
|
|
||||||
self.ping_fun = PingFun(self.write, self.on_task_done, self.update_status)
|
self.ping_fun = PingFun(self.write, self.on_task_done, self.update_status)
|
||||||
self.source_loader = NetworkManager(lambda _text, _tag=None: None)
|
self.source_loader = NetworkManager(lambda _text, _tag=None: None)
|
||||||
self.after(350, self.load_source_ips)
|
self.after(350, self.load_source_ips)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import tkinter as tk
|
|||||||
from tkinter import filedialog, messagebox, ttk
|
from tkinter import filedialog, messagebox, ttk
|
||||||
|
|
||||||
from core.Function.telnet_fun import PortScanner
|
from core.Function.telnet_fun import PortScanner
|
||||||
from core.ui.components import Console, Page, action_bar, button, combo, field
|
from core.ui.components import Page, action_bar, button, combo, field
|
||||||
|
|
||||||
|
|
||||||
PORT_PRESETS = {
|
PORT_PRESETS = {
|
||||||
@@ -16,9 +16,10 @@ PORT_PRESETS = {
|
|||||||
|
|
||||||
|
|
||||||
class TelnetTab(Page):
|
class TelnetTab(Page):
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, console):
|
||||||
super().__init__(parent, "端口扫描", "单端口测试、端口扫描、批量主机巡检、服务识别与结果导出。")
|
super().__init__(parent, "端口扫描", "单端口测试、端口扫描、批量主机巡检、服务识别与结果导出。")
|
||||||
self.body.rowconfigure(3, weight=1)
|
self.console = console
|
||||||
|
self.body.rowconfigure(2, weight=1)
|
||||||
|
|
||||||
status = self.section("实时状态", 0, columns=7)
|
status = self.section("实时状态", 0, columns=7)
|
||||||
self.state = field(status, "状态", 1, 0, "等待", 10)
|
self.state = field(status, "状态", 1, 0, "等待", 10)
|
||||||
@@ -84,12 +85,6 @@ class TelnetTab(Page):
|
|||||||
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
||||||
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
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.scanner = PortScanner(self.write, self.on_task_done, self.update_status, self.add_result)
|
self.scanner = PortScanner(self.write, self.on_task_done, self.update_status, self.add_result)
|
||||||
|
|
||||||
def build_single_tab(self):
|
def build_single_tab(self):
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import tkinter as tk
|
|||||||
from tkinter import filedialog, messagebox, ttk
|
from tkinter import filedialog, messagebox, ttk
|
||||||
|
|
||||||
from core.Function.tracert_fun import TracertFun
|
from core.Function.tracert_fun import TracertFun
|
||||||
from core.ui.components import Console, Page, action_bar, button, combo, field
|
from core.ui.components import Page, action_bar, button, combo, field
|
||||||
|
|
||||||
|
|
||||||
class TracertTab(Page):
|
class TracertTab(Page):
|
||||||
def __init__(self, parent):
|
def __init__(self, parent, console):
|
||||||
super().__init__(parent, "路由追踪", "结构化查看跳点、延迟、超时、波动和诊断摘要。")
|
super().__init__(parent, "路由追踪", "结构化查看跳点、延迟、超时、波动和诊断摘要。")
|
||||||
self.body.rowconfigure(3, weight=1)
|
self.console = console
|
||||||
|
self.body.rowconfigure(2, weight=1)
|
||||||
|
|
||||||
status = self.section("实时状态", 0, columns=7)
|
status = self.section("实时状态", 0, columns=7)
|
||||||
self.state = field(status, "状态", 1, 0, "等待", 10)
|
self.state = field(status, "状态", 1, 0, "等待", 10)
|
||||||
@@ -91,12 +92,6 @@ class TracertTab(Page):
|
|||||||
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
||||||
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
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.tracert_fun = TracertFun(self.write, self.on_task_done, self.update_status, self.add_result)
|
self.tracert_fun = TracertFun(self.write, self.on_task_done, self.update_status, self.add_result)
|
||||||
|
|
||||||
def _check(self, parent, text, variable, row, column):
|
def _check(self, parent, text, variable, row, column):
|
||||||
|
|||||||
+100
-32
@@ -3,30 +3,36 @@ from tkinter import ttk
|
|||||||
|
|
||||||
|
|
||||||
COLORS = {
|
COLORS = {
|
||||||
"bg": "#eef3f8",
|
"bg": "#f4f7fb",
|
||||||
"panel": "#ffffff",
|
"panel": "#ffffff",
|
||||||
"panel_alt": "#f7f9fc",
|
"panel_alt": "#f8fafc",
|
||||||
"sidebar": "#102033",
|
"topbar": "#071424",
|
||||||
"sidebar_hover": "#1a3552",
|
"sidebar": "#0b1b2f",
|
||||||
"sidebar_active": "#246bfe",
|
"sidebar_hover": "#132943",
|
||||||
"text": "#172033",
|
"sidebar_active": "#2f6df6",
|
||||||
"muted": "#607086",
|
"text": "#0f1f35",
|
||||||
"border": "#dbe4ef",
|
"muted": "#5f7088",
|
||||||
"primary": "#246bfe",
|
"border": "#dce5f0",
|
||||||
"primary_dark": "#1451c8",
|
"border_dark": "#cbd8e6",
|
||||||
"danger": "#df3b3b",
|
"primary": "#2f6df6",
|
||||||
"success": "#1a9b6c",
|
"primary_dark": "#1f56d8",
|
||||||
|
"primary_soft": "#edf4ff",
|
||||||
|
"danger": "#ef4444",
|
||||||
|
"danger_soft": "#fff1f2",
|
||||||
|
"success": "#16a36a",
|
||||||
|
"success_soft": "#dcfce7",
|
||||||
"warning": "#b7791f",
|
"warning": "#b7791f",
|
||||||
"console_bg": "#0f1724",
|
"console_bg": "#07111f",
|
||||||
"console_fg": "#dbeafe",
|
"console_fg": "#dbeafe",
|
||||||
"console_muted": "#9fb4d0",
|
"console_muted": "#93a8c5",
|
||||||
|
"console_accent": "#22d3ee",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
FONT_BODY = ("Microsoft YaHei UI", 10)
|
FONT_BODY = ("Microsoft YaHei UI", 10)
|
||||||
FONT_SMALL = ("Microsoft YaHei UI", 9)
|
FONT_SMALL = ("Microsoft YaHei UI", 9)
|
||||||
FONT_TITLE = ("Microsoft YaHei UI", 17, "bold")
|
FONT_TITLE = ("Microsoft YaHei UI", 18, "bold")
|
||||||
FONT_SECTION = ("Microsoft YaHei UI", 11, "bold")
|
FONT_SECTION = ("Microsoft YaHei UI", 12, "bold")
|
||||||
FONT_MONO = ("Consolas", 10)
|
FONT_MONO = ("Consolas", 10)
|
||||||
|
|
||||||
|
|
||||||
@@ -43,9 +49,11 @@ def apply_theme(root: tk.Tk) -> None:
|
|||||||
style.configure("TFrame", background=COLORS["bg"])
|
style.configure("TFrame", background=COLORS["bg"])
|
||||||
style.configure("Panel.TFrame", background=COLORS["panel"])
|
style.configure("Panel.TFrame", background=COLORS["panel"])
|
||||||
style.configure("Alt.TFrame", background=COLORS["panel_alt"])
|
style.configure("Alt.TFrame", background=COLORS["panel_alt"])
|
||||||
|
style.configure("Page.TFrame", background=COLORS["bg"])
|
||||||
style.configure("TLabel", background=COLORS["panel"], foreground=COLORS["text"])
|
style.configure("TLabel", background=COLORS["panel"], foreground=COLORS["text"])
|
||||||
style.configure("Muted.TLabel", background=COLORS["panel"], foreground=COLORS["muted"], font=FONT_SMALL)
|
style.configure("Muted.TLabel", background=COLORS["panel"], foreground=COLORS["muted"], font=FONT_SMALL)
|
||||||
style.configure("Title.TLabel", background=COLORS["panel"], foreground=COLORS["text"], font=FONT_TITLE)
|
style.configure("PageMuted.TLabel", background=COLORS["bg"], foreground=COLORS["muted"], font=FONT_SMALL)
|
||||||
|
style.configure("Title.TLabel", background=COLORS["bg"], foreground=COLORS["text"], font=FONT_TITLE)
|
||||||
style.configure("Section.TLabel", background=COLORS["panel"], foreground=COLORS["text"], font=FONT_SECTION)
|
style.configure("Section.TLabel", background=COLORS["panel"], foreground=COLORS["text"], font=FONT_SECTION)
|
||||||
|
|
||||||
style.configure(
|
style.configure(
|
||||||
@@ -55,44 +63,104 @@ def apply_theme(root: tk.Tk) -> None:
|
|||||||
bordercolor=COLORS["border"],
|
bordercolor=COLORS["border"],
|
||||||
lightcolor=COLORS["border"],
|
lightcolor=COLORS["border"],
|
||||||
darkcolor=COLORS["border"],
|
darkcolor=COLORS["border"],
|
||||||
padding=7,
|
relief="solid",
|
||||||
|
padding=(10, 8),
|
||||||
)
|
)
|
||||||
|
style.map("TEntry", bordercolor=[("focus", COLORS["primary"]), ("disabled", COLORS["border"])])
|
||||||
style.configure(
|
style.configure(
|
||||||
"TCombobox",
|
"TCombobox",
|
||||||
fieldbackground="#ffffff",
|
fieldbackground="#ffffff",
|
||||||
foreground=COLORS["text"],
|
foreground=COLORS["text"],
|
||||||
bordercolor=COLORS["border"],
|
bordercolor=COLORS["border"],
|
||||||
arrowcolor=COLORS["muted"],
|
arrowcolor=COLORS["muted"],
|
||||||
padding=6,
|
relief="solid",
|
||||||
|
padding=(10, 7),
|
||||||
)
|
)
|
||||||
|
style.map("TCombobox", bordercolor=[("focus", COLORS["primary"])], fieldbackground=[("readonly", "#ffffff")])
|
||||||
|
|
||||||
style.configure(
|
style.configure(
|
||||||
"Primary.TButton",
|
"Primary.TButton",
|
||||||
background=COLORS["primary"],
|
background=COLORS["primary"],
|
||||||
foreground="#ffffff",
|
foreground="#ffffff",
|
||||||
borderwidth=0,
|
borderwidth=1,
|
||||||
|
bordercolor=COLORS["primary"],
|
||||||
focusthickness=0,
|
focusthickness=0,
|
||||||
padding=(14, 8),
|
padding=(16, 9),
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"Primary.TButton",
|
||||||
|
background=[("active", COLORS["primary_dark"]), ("disabled", "#a9c0f4")],
|
||||||
|
bordercolor=[("active", COLORS["primary_dark"]), ("disabled", "#a9c0f4")],
|
||||||
|
foreground=[("disabled", "#eef4ff")],
|
||||||
)
|
)
|
||||||
style.map("Primary.TButton", background=[("active", COLORS["primary_dark"]), ("disabled", "#9bb7f5")])
|
|
||||||
|
|
||||||
style.configure(
|
style.configure(
|
||||||
"Secondary.TButton",
|
"Secondary.TButton",
|
||||||
background="#e7eef8",
|
background="#ffffff",
|
||||||
foreground=COLORS["text"],
|
foreground=COLORS["primary"],
|
||||||
borderwidth=0,
|
borderwidth=1,
|
||||||
padding=(14, 8),
|
bordercolor="#bcd0ff",
|
||||||
|
lightcolor="#bcd0ff",
|
||||||
|
darkcolor="#bcd0ff",
|
||||||
|
focusthickness=0,
|
||||||
|
padding=(16, 9),
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"Secondary.TButton",
|
||||||
|
background=[("active", COLORS["primary_soft"]), ("disabled", "#f1f5f9")],
|
||||||
|
foreground=[("disabled", "#9aa8ba")],
|
||||||
|
bordercolor=[("active", COLORS["primary"]), ("disabled", COLORS["border"])],
|
||||||
)
|
)
|
||||||
style.map("Secondary.TButton", background=[("active", "#d6e2f3")])
|
|
||||||
|
|
||||||
style.configure(
|
style.configure(
|
||||||
"Danger.TButton",
|
"Danger.TButton",
|
||||||
background=COLORS["danger"],
|
background="#ffffff",
|
||||||
foreground="#ffffff",
|
foreground=COLORS["danger"],
|
||||||
borderwidth=0,
|
borderwidth=1,
|
||||||
padding=(14, 8),
|
bordercolor="#fca5a5",
|
||||||
|
lightcolor="#fca5a5",
|
||||||
|
darkcolor="#fca5a5",
|
||||||
|
focusthickness=0,
|
||||||
|
padding=(16, 9),
|
||||||
)
|
)
|
||||||
style.map("Danger.TButton", background=[("active", "#bd2929")])
|
style.map("Danger.TButton", background=[("active", COLORS["danger_soft"])], bordercolor=[("active", COLORS["danger"])])
|
||||||
|
|
||||||
|
style.configure(
|
||||||
|
"Treeview",
|
||||||
|
background="#ffffff",
|
||||||
|
fieldbackground="#ffffff",
|
||||||
|
foreground=COLORS["text"],
|
||||||
|
rowheight=30,
|
||||||
|
bordercolor=COLORS["border"],
|
||||||
|
borderwidth=1,
|
||||||
|
)
|
||||||
|
style.configure(
|
||||||
|
"Treeview.Heading",
|
||||||
|
background=COLORS["panel_alt"],
|
||||||
|
foreground=COLORS["muted"],
|
||||||
|
font=("Microsoft YaHei UI", 9, "bold"),
|
||||||
|
relief="flat",
|
||||||
|
padding=(8, 7),
|
||||||
|
)
|
||||||
|
style.map("Treeview", background=[("selected", COLORS["primary_soft"])], foreground=[("selected", COLORS["text"])])
|
||||||
|
|
||||||
|
style.configure("TNotebook", background=COLORS["panel"], borderwidth=0)
|
||||||
|
style.configure("TNotebook.Tab", padding=(18, 8), background=COLORS["panel_alt"], foreground=COLORS["muted"])
|
||||||
|
style.map("TNotebook.Tab", background=[("selected", "#ffffff")], foreground=[("selected", COLORS["primary"])])
|
||||||
|
|
||||||
|
style.configure(
|
||||||
|
"Modern.Vertical.TScrollbar",
|
||||||
|
gripcount=0,
|
||||||
|
background="#d7e0ec",
|
||||||
|
darkcolor="#d7e0ec",
|
||||||
|
lightcolor="#d7e0ec",
|
||||||
|
troughcolor=COLORS["bg"],
|
||||||
|
bordercolor=COLORS["bg"],
|
||||||
|
arrowcolor=COLORS["muted"],
|
||||||
|
relief="flat",
|
||||||
|
width=10,
|
||||||
|
)
|
||||||
|
style.map("Modern.Vertical.TScrollbar", background=[("active", "#bfccdc")])
|
||||||
|
|
||||||
|
|
||||||
def build_app_icon(size: int = 64) -> tk.PhotoImage:
|
def build_app_icon(size: int = 64) -> tk.PhotoImage:
|
||||||
|
|||||||
+136
-24
@@ -1,6 +1,10 @@
|
|||||||
|
import ctypes
|
||||||
|
import platform
|
||||||
|
import socket
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
|
||||||
|
from core.ui.components import Console
|
||||||
from core.ui.tab_device_discovery import DeviceDiscoveryTab
|
from core.ui.tab_device_discovery import DeviceDiscoveryTab
|
||||||
from core.ui.tab_dns import DnsTab
|
from core.ui.tab_dns import DnsTab
|
||||||
from core.ui.tab_ip_conflict import IpConflictTab
|
from core.ui.tab_ip_conflict import IpConflictTab
|
||||||
@@ -17,33 +21,80 @@ class MainUI:
|
|||||||
self.root = root
|
self.root = root
|
||||||
self.base_dir = base_dir
|
self.base_dir = base_dir
|
||||||
self.root.title("NetPilot 网络调试工具")
|
self.root.title("NetPilot 网络调试工具")
|
||||||
self.root.geometry("1160x720")
|
self.root.geometry("1440x820")
|
||||||
|
self.root.minsize(1280, 760)
|
||||||
apply_theme(root)
|
apply_theme(root)
|
||||||
|
|
||||||
self.icon_image = build_app_icon()
|
self.icon_image = build_app_icon()
|
||||||
self.root.iconphoto(True, self.icon_image)
|
self.root.iconphoto(True, self.icon_image)
|
||||||
|
|
||||||
self.root.columnconfigure(1, weight=1)
|
self.root.columnconfigure(1, weight=1)
|
||||||
self.root.rowconfigure(0, weight=1)
|
self.root.rowconfigure(1, weight=1)
|
||||||
|
|
||||||
self.sidebar = tk.Frame(root, width=232, bg=COLORS["sidebar"])
|
self._build_topbar()
|
||||||
self.sidebar.grid(row=0, column=0, sticky="ns")
|
|
||||||
|
self.sidebar = tk.Frame(root, width=260, bg=COLORS["sidebar"])
|
||||||
|
self.sidebar.grid(row=1, column=0, rowspan=2, sticky="ns")
|
||||||
self.sidebar.grid_propagate(False)
|
self.sidebar.grid_propagate(False)
|
||||||
|
|
||||||
self.content = ttk.Frame(root, style="Panel.TFrame")
|
self.content = ttk.Frame(root, style="Page.TFrame")
|
||||||
self.content.grid(row=0, column=1, sticky="nsew")
|
self.content.grid(row=1, column=1, sticky="nsew")
|
||||||
self.content.rowconfigure(0, weight=1)
|
self.content.rowconfigure(0, weight=1)
|
||||||
self.content.columnconfigure(0, weight=1)
|
self.content.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
self.console_shell = tk.Frame(root, width=410, bg=COLORS["bg"])
|
||||||
|
self.console_shell.grid(row=1, column=2, sticky="nsew", padx=(0, 16), pady=(16, 0))
|
||||||
|
self.console_shell.grid_propagate(False)
|
||||||
|
self.console_shell.rowconfigure(0, weight=1)
|
||||||
|
self.console_shell.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
self.console_panel = tk.Frame(
|
||||||
|
self.console_shell,
|
||||||
|
bg=COLORS["panel"],
|
||||||
|
highlightbackground=COLORS["border"],
|
||||||
|
highlightcolor=COLORS["border"],
|
||||||
|
highlightthickness=1,
|
||||||
|
bd=0,
|
||||||
|
)
|
||||||
|
self.console_panel.grid(row=0, column=0, sticky="nsew")
|
||||||
|
self.console_panel.grid_propagate(False)
|
||||||
|
self.console_panel.rowconfigure(1, weight=1)
|
||||||
|
self.console_panel.columnconfigure(0, weight=1)
|
||||||
|
self._build_console_panel()
|
||||||
|
self._build_statusbar()
|
||||||
|
|
||||||
self.pages = {}
|
self.pages = {}
|
||||||
self.nav_buttons = {}
|
self.nav_buttons = {}
|
||||||
self._build_sidebar()
|
self._build_sidebar()
|
||||||
self._build_pages()
|
self._build_pages()
|
||||||
self.show_page("network")
|
self.show_page("network")
|
||||||
|
|
||||||
|
def _build_topbar(self) -> None:
|
||||||
|
bar = tk.Frame(self.root, height=44, bg=COLORS["topbar"])
|
||||||
|
bar.grid(row=0, column=0, columnspan=3, sticky="ew")
|
||||||
|
bar.grid_propagate(False)
|
||||||
|
|
||||||
|
icon = tk.Label(
|
||||||
|
bar,
|
||||||
|
text="NP",
|
||||||
|
bg=COLORS["primary"],
|
||||||
|
fg="#ffffff",
|
||||||
|
font=("Microsoft YaHei UI", 9, "bold"),
|
||||||
|
width=3,
|
||||||
|
height=1,
|
||||||
|
)
|
||||||
|
icon.pack(side="left", padx=(24, 12), pady=8)
|
||||||
|
tk.Label(
|
||||||
|
bar,
|
||||||
|
text="NetPilot 网络调试工具",
|
||||||
|
bg=COLORS["topbar"],
|
||||||
|
fg="#ffffff",
|
||||||
|
font=("Microsoft YaHei UI", 11, "bold"),
|
||||||
|
).pack(side="left")
|
||||||
|
|
||||||
def _build_sidebar(self) -> None:
|
def _build_sidebar(self) -> None:
|
||||||
brand = tk.Frame(self.sidebar, bg=COLORS["sidebar"])
|
brand = tk.Frame(self.sidebar, bg=COLORS["sidebar"])
|
||||||
brand.pack(fill="x", padx=20, pady=(24, 26))
|
brand.pack(fill="x", padx=28, pady=(28, 28))
|
||||||
|
|
||||||
logo = tk.Label(
|
logo = tk.Label(
|
||||||
brand,
|
brand,
|
||||||
@@ -52,14 +103,14 @@ class MainUI:
|
|||||||
height=2,
|
height=2,
|
||||||
bg=COLORS["primary"],
|
bg=COLORS["primary"],
|
||||||
fg="#ffffff",
|
fg="#ffffff",
|
||||||
font=("Microsoft YaHei UI", 14, "bold"),
|
font=("Microsoft YaHei UI", 18, "bold"),
|
||||||
)
|
)
|
||||||
logo.pack(side="left")
|
logo.pack(side="left")
|
||||||
|
|
||||||
title = tk.Frame(brand, bg=COLORS["sidebar"])
|
title = tk.Frame(brand, bg=COLORS["sidebar"])
|
||||||
title.pack(side="left", padx=12)
|
title.pack(side="left", padx=12)
|
||||||
tk.Label(title, text="NetPilot", bg=COLORS["sidebar"], fg="#ffffff", font=("Microsoft YaHei UI", 16, "bold")).pack(anchor="w")
|
tk.Label(title, text="NetPilot", bg=COLORS["sidebar"], fg="#ffffff", font=("Microsoft YaHei UI", 17, "bold")).pack(anchor="w")
|
||||||
tk.Label(title, text="网络调试控制台", bg=COLORS["sidebar"], fg="#9fb4d0", font=("Microsoft YaHei UI", 9)).pack(anchor="w")
|
tk.Label(title, text="网络调试控制台", bg=COLORS["sidebar"], fg="#9fb4d0", font=("Microsoft YaHei UI", 10)).pack(anchor="w", pady=(4, 0))
|
||||||
|
|
||||||
items = [
|
items = [
|
||||||
("network", "◎", "网卡配置", "IP / DNS / DHCP"),
|
("network", "◎", "网卡配置", "IP / DNS / DHCP"),
|
||||||
@@ -85,16 +136,16 @@ class MainUI:
|
|||||||
|
|
||||||
def _nav_button(self, key: str, icon: str, title: str, subtitle: str) -> tk.Frame:
|
def _nav_button(self, key: str, icon: str, title: str, subtitle: str) -> tk.Frame:
|
||||||
frame = tk.Frame(self.sidebar, bg=COLORS["sidebar"], cursor="hand2")
|
frame = tk.Frame(self.sidebar, bg=COLORS["sidebar"], cursor="hand2")
|
||||||
frame.pack(fill="x", padx=14, pady=4)
|
frame.pack(fill="x", padx=16, pady=5)
|
||||||
|
|
||||||
icon_label = tk.Label(frame, text=icon, width=3, bg=COLORS["sidebar"], fg="#c8d7ec", font=("Microsoft YaHei UI", 18))
|
icon_label = tk.Label(frame, text=icon, width=3, bg=COLORS["sidebar"], fg="#d9e6f7", font=("Microsoft YaHei UI", 18))
|
||||||
icon_label.pack(side="left", padx=(8, 6), pady=10)
|
icon_label.pack(side="left", padx=(12, 8), pady=12)
|
||||||
|
|
||||||
text_frame = tk.Frame(frame, bg=COLORS["sidebar"])
|
text_frame = tk.Frame(frame, bg=COLORS["sidebar"])
|
||||||
text_frame.pack(side="left", fill="x", expand=True)
|
text_frame.pack(side="left", fill="x", expand=True)
|
||||||
title_label = tk.Label(text_frame, text=title, bg=COLORS["sidebar"], fg="#ffffff", font=("Microsoft YaHei UI", 10, "bold"))
|
title_label = tk.Label(text_frame, text=title, bg=COLORS["sidebar"], fg="#ffffff", font=("Microsoft YaHei UI", 11, "bold"))
|
||||||
title_label.pack(anchor="w")
|
title_label.pack(anchor="w")
|
||||||
subtitle_label = tk.Label(text_frame, text=subtitle, bg=COLORS["sidebar"], fg="#9fb4d0", font=("Microsoft YaHei UI", 8))
|
subtitle_label = tk.Label(text_frame, text=subtitle, bg=COLORS["sidebar"], fg="#a6b7ce", font=("Microsoft YaHei UI", 9))
|
||||||
subtitle_label.pack(anchor="w", pady=(2, 0))
|
subtitle_label.pack(anchor="w", pady=(2, 0))
|
||||||
|
|
||||||
widgets = (frame, icon_label, text_frame, title_label, subtitle_label)
|
widgets = (frame, icon_label, text_frame, title_label, subtitle_label)
|
||||||
@@ -107,14 +158,14 @@ class MainUI:
|
|||||||
|
|
||||||
def _build_pages(self) -> None:
|
def _build_pages(self) -> None:
|
||||||
self.pages = {
|
self.pages = {
|
||||||
"network": NetworkTab(self.content),
|
"network": NetworkTab(self.content, self.console),
|
||||||
"dns": DnsTab(self.content),
|
"dns": DnsTab(self.content, self.console),
|
||||||
"ping": PingTab(self.content),
|
"ping": PingTab(self.content, self.console),
|
||||||
"ports": TelnetTab(self.content),
|
"ports": TelnetTab(self.content, self.console),
|
||||||
"trace": TracertTab(self.content),
|
"trace": TracertTab(self.content, self.console),
|
||||||
"loop": LoopTab(self.content),
|
"loop": LoopTab(self.content, self.console),
|
||||||
"ip_conflict": IpConflictTab(self.content),
|
"ip_conflict": IpConflictTab(self.content, self.console),
|
||||||
"devices": DeviceDiscoveryTab(self.content),
|
"devices": DeviceDiscoveryTab(self.content, self.console),
|
||||||
}
|
}
|
||||||
for page in self.pages.values():
|
for page in self.pages.values():
|
||||||
page.grid(row=0, column=0, sticky="nsew")
|
page.grid(row=0, column=0, sticky="nsew")
|
||||||
@@ -129,8 +180,69 @@ class MainUI:
|
|||||||
active = getattr(self, "active_page", None) == key
|
active = getattr(self, "active_page", None) == key
|
||||||
bg = COLORS["sidebar_active"] if active else COLORS["sidebar_hover"] if hover else COLORS["sidebar"]
|
bg = COLORS["sidebar_active"] if active else COLORS["sidebar_hover"] if hover else COLORS["sidebar"]
|
||||||
fg = "#ffffff" if active or hover else "#c8d7ec"
|
fg = "#ffffff" if active or hover else "#c8d7ec"
|
||||||
muted = "#dce9ff" if active else "#9fb4d0"
|
muted = "#dce9ff" if active else "#a6b7ce"
|
||||||
for index, widget in enumerate(widgets):
|
for index, widget in enumerate(widgets):
|
||||||
widget.configure(bg=bg)
|
widget.configure(bg=bg)
|
||||||
if isinstance(widget, tk.Label):
|
if isinstance(widget, tk.Label):
|
||||||
widget.configure(fg=fg if index in (1, 3) else muted)
|
widget.configure(fg=fg if index in (1, 3) else muted)
|
||||||
|
|
||||||
|
def _build_console_panel(self) -> None:
|
||||||
|
header = tk.Frame(self.console_panel, bg=COLORS["panel"])
|
||||||
|
header.grid(row=0, column=0, sticky="ew", padx=18, pady=(16, 12))
|
||||||
|
header.columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
tk.Label(
|
||||||
|
header,
|
||||||
|
text="输出控制台",
|
||||||
|
bg=COLORS["panel"],
|
||||||
|
fg=COLORS["text"],
|
||||||
|
font=("Microsoft YaHei UI", 13, "bold"),
|
||||||
|
).grid(row=0, column=0, sticky="w")
|
||||||
|
|
||||||
|
ttk.Button(header, text="⌫ 清空", command=lambda: self.console.clear(), style="Secondary.TButton").grid(row=0, column=1, sticky="e")
|
||||||
|
|
||||||
|
self.console = Console(self.console_panel)
|
||||||
|
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 16))
|
||||||
|
|
||||||
|
def _build_statusbar(self) -> None:
|
||||||
|
status = tk.Frame(
|
||||||
|
self.root,
|
||||||
|
height=40,
|
||||||
|
bg=COLORS["panel"],
|
||||||
|
highlightbackground=COLORS["border"],
|
||||||
|
highlightthickness=1,
|
||||||
|
bd=0,
|
||||||
|
)
|
||||||
|
status.grid(row=2, column=1, columnspan=2, sticky="ew")
|
||||||
|
status.grid_propagate(False)
|
||||||
|
status.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
left = tk.Frame(status, bg=COLORS["panel"])
|
||||||
|
left.grid(row=0, column=0, sticky="w", padx=28, pady=9)
|
||||||
|
self._status_dot(left)
|
||||||
|
self._status_label(left, "就绪", color=COLORS["muted"])
|
||||||
|
self._status_separator(left)
|
||||||
|
self._status_label(left, "本地连接正常", color=COLORS["muted"])
|
||||||
|
|
||||||
|
right = tk.Frame(status, bg=COLORS["panel"])
|
||||||
|
right.grid(row=0, column=1, sticky="e", padx=28, pady=9)
|
||||||
|
self._status_label(right, f"本机名:{socket.gethostname()}", color=COLORS["muted"])
|
||||||
|
self._status_separator(right)
|
||||||
|
self._status_label(right, f"操作系统:{platform.system()} {platform.release()}", color=COLORS["muted"])
|
||||||
|
self._status_separator(right)
|
||||||
|
self._status_label(right, "管理员权限" if self._is_admin() else "普通权限", color=COLORS["muted"])
|
||||||
|
|
||||||
|
def _status_dot(self, parent) -> None:
|
||||||
|
tk.Label(parent, text="●", bg=COLORS["panel"], fg="#22c55e", font=("Microsoft YaHei UI", 10)).pack(side="left", padx=(0, 10))
|
||||||
|
|
||||||
|
def _status_label(self, parent, text: str, color: str) -> None:
|
||||||
|
tk.Label(parent, text=text, bg=COLORS["panel"], fg=color, font=("Microsoft YaHei UI", 9)).pack(side="left")
|
||||||
|
|
||||||
|
def _status_separator(self, parent) -> None:
|
||||||
|
tk.Label(parent, text="|", bg=COLORS["panel"], fg=COLORS["border_dark"], font=("Microsoft YaHei UI", 9)).pack(side="left", padx=14)
|
||||||
|
|
||||||
|
def _is_admin(self) -> bool:
|
||||||
|
try:
|
||||||
|
return bool(ctypes.windll.shell32.IsUserAnAdmin())
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|||||||
Reference in New Issue
Block a user