feat: 重新设计插件 UI 样式与交互

- 任务栏图标:双行数值显示(下载蓝/上传绿),固定宽度避免抖动
- 左键弹出面板:360×320 居中布局,双列速度卡片,网卡名+IP 卡片背景
- 右键菜单:deepin 风格 Platform.Menu + MenuHelper(刷新/设置/关于)
- 设置窗口:frameless 自定义标题栏,网卡接口选择,卸载插件功能
- Tooltip:网卡名 + IP 地址单行展示
- C++ 后端:新增 ipAddress 属性(QNetworkInterface),优先选择物理网卡
- 速度格式化:最小单位 KB,2 位小数
- 移除 QML 侧多余 refresh 调用,修复 tooltip 数值变 0
- 新增设计文档与实现计划
This commit is contained in:
2026-07-18 18:44:12 +08:00
parent 64efe509fb
commit 009dd9578f
6 changed files with 1925 additions and 303 deletions
+59 -1
View File
@@ -10,6 +10,7 @@
#include <QDebug>
#include <QDir>
#include <QRegularExpression>
#include <QNetworkInterface>
DS_BEGIN_NAMESPACE
@@ -103,6 +104,12 @@ QString NetworkMonitorApplet::activeInterface() const
return m_activeInterface;
}
// 返回活动接口的 IPv4 地址,供 QML 显示
QString NetworkMonitorApplet::ipAddress() const
{
return m_ipAddress;
}
void NetworkMonitorApplet::refresh()
{
readNetworkStats();
@@ -114,6 +121,8 @@ void NetworkMonitorApplet::setActiveInterface(const QString &interface)
m_activeInterface = interface;
m_firstUpdate = true;
emit activeInterfaceChanged();
// 接口切换后立即检测新接口的 IP 地址
detectIpAddress();
}
}
@@ -167,14 +176,27 @@ void NetworkMonitorApplet::readNetworkStats()
// 自动选择活动接口
if (m_activeInterface.isEmpty() && !m_interfaceList.isEmpty()) {
// 优先选择有流量的接口
// 优先选择物理网卡(wlp/wlan/enp/eth)中有流量的接口
// 避免选中 Meta/tun0 等虚拟代理接口作为默认显示
for (const QString &name : m_interfaceList) {
if (!isPhysicalInterface(name)) continue;
const NetworkInterface &iface = m_interfaces[name];
if (iface.rxBytes > 0 || iface.txBytes > 0) {
m_activeInterface = name;
break;
}
}
// 其次选择任意有流量的接口(物理网卡都无流量时的兜底)
if (m_activeInterface.isEmpty()) {
for (const QString &name : m_interfaceList) {
const NetworkInterface &iface = m_interfaces[name];
if (iface.rxBytes > 0 || iface.txBytes > 0) {
m_activeInterface = name;
break;
}
}
}
// 最后选第一个(无任何流量时)
if (m_activeInterface.isEmpty()) {
m_activeInterface = m_interfaceList.first();
}
@@ -190,6 +212,8 @@ void NetworkMonitorApplet::readNetworkStats()
calculateSpeed();
emit statsChanged();
// 每次刷新都检测 IP,以应对 DHCP 续约等 IP 变更场景
detectIpAddress();
}
void NetworkMonitorApplet::calculateSpeed()
@@ -255,6 +279,40 @@ qint64 NetworkMonitorApplet::getActiveTxBytes() const
return m_interfaces[m_activeInterface].txBytes;
}
// 检测活动接口的 IPv4 地址
// 跳过 IPv6 和 loopback,只取第一个有效 IPv4 地址
// IP 变化时发射 ipAddressChanged 信号通知 QML 更新
// 设计原因:DHCP 续约、网络切换等场景下 IP 可能变化,需持续检测
void NetworkMonitorApplet::detectIpAddress()
{
QString newIp;
if (!m_activeInterface.isEmpty()) {
QNetworkInterface iface = QNetworkInterface::interfaceFromName(m_activeInterface);
for (const QNetworkAddressEntry &entry : iface.addressEntries()) {
// 只取 IPv4 地址,跳过 IPv6 和 loopback
if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol
&& !entry.ip().isLoopback()) {
newIp = entry.ip().toString();
break;
}
}
}
if (m_ipAddress != newIp) {
m_ipAddress = newIp;
emit ipAddressChanged();
}
}
// 判断是否为物理网卡
// 物理网卡前缀:无线 wlp/wlan/wifi,有线 enp/eth
// 虚拟接口(Meta/tun/tap/docker/veth/br-)返回 false
// 用于自动选择时优先真实网卡而非代理 TUN 接口
bool NetworkMonitorApplet::isPhysicalInterface(const QString &name) const
{
return name.startsWith("wlp") || name.startsWith("wlan")
|| name.startsWith("enp") || name.startsWith("eth");
}
D_APPLET_CLASS(NetworkMonitorApplet)
DS_END_NAMESPACE