feat: dark theme toggle + CPU/memory sparkline trend charts
- ui/theme.py: light/dark QSS stylesheets, persisted to ~/.sshclient/settings.json - main_window.py: View menu with dark theme checkbox, applies on startup - ui/widgets.py: SparklineChart widget for mini trend visualization - MonitorPanel: CPU and memory sparkline charts showing last 60 samples - Charts cleared on stop/switch host
This commit is contained in:
+102
@@ -356,6 +356,93 @@ class FileBrowser(QWidget):
|
||||
# ============================================================
|
||||
# 监控面板
|
||||
# ============================================================
|
||||
class SparklineChart(QWidget):
|
||||
"""迷你趋势图:绘制 0-100 的折线图,显示最近 N 个采样点"""
|
||||
|
||||
def __init__(self, title: str = "", color: str = "#4caf50", max_points: int = 60, parent=None):
|
||||
super().__init__(parent)
|
||||
self._title = title
|
||||
self._color = QColor(color)
|
||||
self._max_points = max_points
|
||||
self._data: list = []
|
||||
self.setMinimumHeight(64)
|
||||
self.setMinimumWidth(120)
|
||||
|
||||
def add_value(self, val: float):
|
||||
self._data.append(max(0, min(100, val)))
|
||||
if len(self._data) > self._max_points:
|
||||
self._data = self._data[-self._max_points:]
|
||||
self.update()
|
||||
|
||||
def clear(self):
|
||||
self._data.clear()
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, _ev):
|
||||
from PyQt5.QtGui import QPainter, QPen, QBrush, QPainterPath, QLinearGradient
|
||||
from PyQt5.QtCore import QRectF, QPointF
|
||||
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.Antialiasing)
|
||||
w, h = self.width(), self.height()
|
||||
# 背景
|
||||
p.fillRect(0, 0, w, h, QColor("#ffffff08"))
|
||||
# 标题
|
||||
if self._title:
|
||||
p.setPen(QPen(QColor("#888")))
|
||||
p.setFont(QFont("sans-serif", 8))
|
||||
p.drawText(4, 12, self._title)
|
||||
if len(self._data) < 2:
|
||||
p.setPen(QPen(QColor("#666")))
|
||||
p.setFont(QFont("sans-serif", 8))
|
||||
p.drawText(w // 2 - 20, h // 2, "等待数据...")
|
||||
return
|
||||
# 绘图区域(留出标题空间)
|
||||
top = 16
|
||||
bottom = h - 4
|
||||
left = 4
|
||||
right = w - 4
|
||||
plot_w = right - left
|
||||
plot_h = bottom - top
|
||||
# 点坐标
|
||||
n = len(self._data)
|
||||
step = plot_w / max(n - 1, 1)
|
||||
points = []
|
||||
for i, v in enumerate(self._data):
|
||||
x = left + i * step
|
||||
y = bottom - (v / 100.0) * plot_h
|
||||
points.append(QPointF(x, y))
|
||||
# 填充区域
|
||||
fill_path = QPainterPath()
|
||||
fill_path.moveTo(points[0].x(), bottom)
|
||||
for pt in points:
|
||||
fill_path.lineTo(pt)
|
||||
fill_path.lineTo(points[-1].x(), bottom)
|
||||
fill_path.closeSubpath()
|
||||
grad = QLinearGradient(0, top, 0, bottom)
|
||||
c = self._color
|
||||
grad.setColorAt(0, QColor(c.red(), c.green(), c.blue(), 80))
|
||||
grad.setColorAt(1, QColor(c.red(), c.green(), c.blue(), 10))
|
||||
p.fillPath(fill_path, QBrush(grad))
|
||||
# 折线
|
||||
p.setPen(QPen(self._color, 1.5))
|
||||
line_path = QPainterPath()
|
||||
line_path.moveTo(points[0])
|
||||
for pt in points[1:]:
|
||||
line_path.lineTo(pt)
|
||||
p.drawPath(line_path)
|
||||
# 当前值标签
|
||||
cur = self._data[-1]
|
||||
p.setPen(QPen(self._color))
|
||||
p.setFont(QFont("sans-serif", 9, QFont.Bold))
|
||||
label = f"{cur:.1f}%"
|
||||
p.drawText(right - 40, 12, label)
|
||||
# 当前点圆点
|
||||
p.setBrush(QBrush(self._color))
|
||||
p.setPen(QPen(self._color, 0))
|
||||
p.drawEllipse(points[-1], 3, 3)
|
||||
|
||||
|
||||
class MonitorPanel(QWidget):
|
||||
"""实时监控:CPU、内存、磁盘、网络"""
|
||||
|
||||
@@ -415,6 +502,14 @@ class MonitorPanel(QWidget):
|
||||
grid.addWidget(self._build_card("内存", "mem_card"))
|
||||
uv.addLayout(grid)
|
||||
|
||||
# CPU / 内存趋势迷你图
|
||||
spark_row = QHBoxLayout()
|
||||
self._spark_cpu = SparklineChart("CPU 趋势", "#4caf50")
|
||||
self._spark_mem = SparklineChart("内存趋势", "#7986cb")
|
||||
spark_row.addWidget(self._spark_cpu)
|
||||
spark_row.addWidget(self._spark_mem)
|
||||
uv.addLayout(spark_row)
|
||||
|
||||
# 负载 + 启动时间
|
||||
grid2 = QHBoxLayout()
|
||||
grid2.addWidget(self._build_card("系统负载", "load_card"))
|
||||
@@ -685,6 +780,11 @@ class MonitorPanel(QWidget):
|
||||
pass
|
||||
self.worker = None
|
||||
self._last_net.clear()
|
||||
# 清空趋势图
|
||||
if hasattr(self, "_spark_cpu"):
|
||||
self._spark_cpu.clear()
|
||||
if hasattr(self, "_spark_mem"):
|
||||
self._spark_mem.clear()
|
||||
|
||||
def _kick_one_sample(self):
|
||||
"""启动一个后台 worker 做一次采集;采完用 QTimer 调度下一次"""
|
||||
@@ -756,6 +856,7 @@ class MonitorPanel(QWidget):
|
||||
|
||||
# CPU
|
||||
cpu = m.get("cpu", 0)
|
||||
self._spark_cpu.add_value(cpu)
|
||||
|
||||
self._value_label("cpu_card").setText(f"{cpu:.1f}%")
|
||||
cores = m.get("cores", 1)
|
||||
@@ -763,6 +864,7 @@ class MonitorPanel(QWidget):
|
||||
|
||||
# 内存
|
||||
mp = m.get("mem_percent", 0)
|
||||
self._spark_mem.add_value(mp)
|
||||
mu = SystemMonitor.format_bytes(m.get("mem_used", 0))
|
||||
mt = SystemMonitor.format_bytes(m.get("mem_total", 0))
|
||||
self._value_label("mem_card").setText(f"{mp:.1f}%")
|
||||
|
||||
Reference in New Issue
Block a user