From 009dd9578f1ab5b8c6a5ee9446fe3481e601cdf1 Mon Sep 17 00:00:00 2001 From: Jokul Date: Sat, 18 Jul 2026 18:44:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=96=B0=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=20UI=20=E6=A0=B7=E5=BC=8F=E4=B8=8E=E4=BA=A4?= =?UTF-8?q?=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 任务栏图标:双行数值显示(下载蓝/上传绿),固定宽度避免抖动 - 左键弹出面板:360×320 居中布局,双列速度卡片,网卡名+IP 卡片背景 - 右键菜单:deepin 风格 Platform.Menu + MenuHelper(刷新/设置/关于) - 设置窗口:frameless 自定义标题栏,网卡接口选择,卸载插件功能 - Tooltip:网卡名 + IP 地址单行展示 - C++ 后端:新增 ipAddress 属性(QNetworkInterface),优先选择物理网卡 - 速度格式化:最小单位 KB,2 位小数 - 移除 QML 侧多余 refresh 调用,修复 tooltip 数值变 0 - 新增设计文档与实现计划 --- CMakeLists.txt | 3 +- .../plans/2026-07-18-ui-redesign.md | 841 +++++++++++++ .../specs/2026-07-18-ui-redesign-design.md | 228 ++++ networkmonitorapplet.cpp | 60 +- networkmonitorapplet.h | 10 + package/networkview.qml | 1086 ++++++++++++----- 6 files changed, 1925 insertions(+), 303 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-18-ui-redesign.md create mode 100644 docs/superpowers/specs/2026-07-18-ui-redesign-design.md diff --git a/CMakeLists.txt b/CMakeLists.txt index cf7f540..375fb00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) -find_package(Qt6 REQUIRED COMPONENTS Core Quick DBus) +find_package(Qt6 REQUIRED COMPONENTS Core Quick DBus Network) find_package(Dtk6 REQUIRED COMPONENTS Core) find_package(DDEShell REQUIRED) @@ -29,6 +29,7 @@ target_link_libraries(space.jokul.JNetApplet PRIVATE Qt6::Core Qt6::Quick Qt6::DBus + Qt6::Network Dtk6::Core Dde::Shell ) diff --git a/docs/superpowers/plans/2026-07-18-ui-redesign.md b/docs/superpowers/plans/2026-07-18-ui-redesign.md new file mode 100644 index 0000000..c15d6a8 --- /dev/null +++ b/docs/superpowers/plans/2026-07-18-ui-redesign.md @@ -0,0 +1,841 @@ +# JNetApplet UI 重新设计实现计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 重写 `package/networkview.qml`,实现 deepin 原生简约风格的任务栏图标、左键弹出面板、右键菜单和 Tooltip。 + +**Architecture:** 单文件 QML 重写,不动 C++ 后端。颜色派生自 `DockPalette.iconTextPalette`,深/浅色自适应。新增 `formatSpeedShort`、右键 `Menu`、chip 接口切换器。 + +**Tech Stack:** QML 2.15、QtQuick.Controls 2.15、QtQuick.Layouts 1.15、org.deepin.ds 1.0、org.deepin.ds.dock 1.0、org.deepin.dtk 1.0。 + +## Global Constraints + +- 仅修改 `package/networkview.qml`,不改 C++、`metadata.json`、`CMakeLists.txt` +- 保留 SPDX 文件头与所有 imports +- Root 元素为 `AppletItem`(来自 `import org.deepin.ds 1.0`) +- QML 属性类型用 `double`/`real`,不用 `qint64` +- 注释统一中文 +- 项目无测试/lint/CI,验证方式:`cmake --build build` + 手动安装重启 dde-shell +- Commit 中文 conventional commits,仅在被显式请求时提交 + +**Spec 引用:** `docs/superpowers/specs/2026-07-18-ui-redesign-design.md` + +--- + +## File Structure + +仅修改 `package/networkview.qml`(当前 521 行)。无新文件、无 C++ 改动。 + +重写后内部结构: + +``` +AppletItem (root) +├── 属性区:applet 绑定、颜色派生、formatSpeedShort +├── 任务栏图标区:Column { Row(↓值), Row(↑值) } +├── PanelToolTip +├── Timer × 2 +├── HoverHandler +├── PanelPopup +│ └── Control > ColumnLayout +│ ├── 标题栏(标题+副标题+刷新按钮) +│ ├── 实时速度卡片(双列) +│ ├── 总量统计(单行) +│ ├── 接口切换 chip 列表 +│ └── 未检测到接口占位 +├── Menu (contextMenu) ← 新增 +├── Dialog (aboutDialog) ← 新增 +├── TapHandler (左键) +└── TapHandler (右键) ← 新增 +``` + +--- + +## Task 1: 属性区与格式化函数(基础层) + +**Files:** Modify `package/networkview.qml:32-72` + +**Interfaces:** +- Produces: `formatSpeedShort()`、`accentGreen`、`accentGreenLight`、`accentOrange`、`accentRed`、`downloadValueColor`、`uploadValueColor` - 后续任务全部依赖 + +**说明:** 替换 root 的函数块和颜色属性块。保留所有 `applet` 绑定属性不变。 + +- [ ] **Step 1: 替换函数区(原 32-56 行)** + +将 `// 格式化速度显示` 注释到 `formatTotal` 函数结束替换为: + +```qml + // 紧凑格式化速度(用于任务栏图标和 tooltip,不带单位后缀,节省空间) + // 设计原因:任务栏 ~48px 空间有限,"1.2M" 比 "1.2 MB/s" 节省约一半宽度 + function formatSpeedShort(bytesPerSec) { + if (bytesPerSec < 1024) { + return bytesPerSec.toFixed(0) + } else if (bytesPerSec < 1024 * 1024) { + return (bytesPerSec / 1024).toFixed(1) + "K" + } else if (bytesPerSec < 1024 * 1024 * 1024) { + return (bytesPerSec / (1024 * 1024)).toFixed(1) + "M" + } else { + return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + "G" + } + } + + // 格式化速度显示(带单位,用于弹出面板,信息更完整) + function formatSpeed(bytesPerSec) { + if (bytesPerSec < 1024) { + return bytesPerSec.toFixed(0) + " B/s" + } else if (bytesPerSec < 1024 * 1024) { + return (bytesPerSec / 1024).toFixed(1) + " KB/s" + } else if (bytesPerSec < 1024 * 1024 * 1024) { + return (bytesPerSec / (1024 * 1024)).toFixed(2) + " MB/s" + } else { + return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(2) + " GB/s" + } + } + + // 格式化总量显示(用于弹出面板的累计统计) + function formatTotal(bytes) { + if (bytes < 1024) { + return bytes.toFixed(0) + " B" + } else if (bytes < 1024 * 1024) { + return (bytes / 1024).toFixed(1) + " KB" + } else if (bytes < 1024 * 1024 * 1024) { + return (bytes / (1024 * 1024)).toFixed(1) + " MB" + } else { + return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB" + } + } +``` + +- [ ] **Step 2: 替换颜色属性区(原 58-72 行)** + +将 `// 根据速度计算图标颜色` 注释到 `accentBlueLight` 属性结束替换为: + +```qml + // 任务栏与面板的颜色全部派生自 DockPalette.iconTextPalette + // 这样在深色/浅色任务栏下自动适配,无需手动判断主题 + property Palette basePalette: DockPalette.iconTextPalette + readonly property color primaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.95) + readonly property color secondaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.80) + readonly property color tertiaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.65) + readonly property color cardBackground: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.06) + readonly property color cardBorder: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.10) + + // 强调色:下载蓝、上传绿,是面板与任务栏的主视觉区分 + readonly property color accentBlue: Qt.rgba(20 / 255, 80 / 255, 160 / 255, 1) + readonly property color accentBlueLight: Qt.rgba(20 / 255, 80 / 255, 160 / 255, 0.12) + readonly property color accentGreen: Qt.rgba(22 / 255, 163 / 255, 74 / 255, 1) + readonly property color accentGreenLight: Qt.rgba(22 / 255, 163 / 255, 74 / 255, 0.12) + + // 高速警示色:下载速度超过阈值时由蓝转橙再转红 + readonly property color accentOrange: Qt.rgba(245 / 255, 158 / 255, 11 / 255, 1) + readonly property color accentRed: Qt.rgba(220 / 255, 38 / 255, 38 / 255, 1) + + // 下载值颜色:保留原有阈值逻辑(>10MB/s 红、>1MB/s 橙、否则蓝),仅作用于下载值 + readonly property color downloadValueColor: { + if (downloadSpeed > 10 * 1024 * 1024) return accentRed + if (downloadSpeed > 1 * 1024 * 1024) return accentOrange + return accentBlue + } + + // 上传值颜色:固定绿色,不做高速警示 + readonly property color uploadValueColor: accentGreen +``` + +- [ ] **Step 3: 验证标识符已定义** + +Run: `grep -n "formatSpeedShort\|downloadValueColor\|uploadValueColor\|accentGreen " /home/Jokul/Documents/projects/JNetApplet/package/networkview.qml` +Expected: 每个标识符至少出现一次。 + +- [ ] **Step 4: 验证 C++ 仍可构建** + +Run: `cmake --build /home/Jokul/Documents/projects/JNetApplet/build 2>&1 | tail -5` +Expected: 构建成功,无 error。 + +--- + +## Task 2: 任务栏图标区重写 + +**Files:** Modify `package/networkview.qml`(原 74-120 行的圆圈 `Rectangle`) + +**Interfaces:** +- Consumes: `formatSpeedShort()`、`downloadValueColor`、`uploadValueColor`、`secondaryText`、`dockSize` +- Produces: 任务栏双行数值显示 + +- [ ] **Step 1: 替换图标区域** + +将 `// 图标区域` 注释到对应 `Rectangle` 闭合 `}` 替换为: + +```qml + // 任务栏图标区:双行紧凑数值,移除原圆圈边框 + // 上行下载、下行上传,箭头为次级色、数值为强调色 + // 设计原因:圆圈边框在 ~48px dock 内压缩可用空间,去掉后数值字号可放大提升可读性 + Column { + anchors.centerIn: parent + spacing: 1 + + // 下载速度行 + Row { + spacing: 2 + anchors.horizontalCenter: parent.horizontalCenter + + Text { + text: "↓" + font.pixelSize: root.dockSize * 0.16 + color: root.secondaryText + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: root.formatSpeedShort(root.downloadSpeed) + font.pixelSize: root.dockSize * 0.22 + font.weight: Font.Medium + color: root.downloadValueColor + anchors.verticalCenter: parent.verticalCenter + } + } + + // 上传速度行 + Row { + spacing: 2 + anchors.horizontalCenter: parent.horizontalCenter + + Text { + text: "↑" + font.pixelSize: root.dockSize * 0.16 + color: root.secondaryText + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: root.formatSpeedShort(root.uploadSpeed) + font.pixelSize: root.dockSize * 0.22 + font.weight: Font.Medium + color: root.uploadValueColor + anchors.verticalCenter: parent.verticalCenter + } + } + } +``` + +- [ ] **Step 2: 确认旧 `iconColor` 已无引用** + +Run: `grep -n "iconColor" /home/Jokul/Documents/projects/JNetApplet/package/networkview.qml` +Expected: 无输出。 + +- [ ] **Step 3: 安装并目视验证** + +Run: `sudo cmake --install /home/Jokul/Documents/projects/JNetApplet/build && systemctl --user restart dde-shell@DDE` +Expected: 任务栏图标显示双行「↓数值 / ↑数值」,下载蓝、上传绿,无圆圈边框。 + +--- + +## Task 3: Tooltip 文本更新 + +**Files:** Modify `package/networkview.qml`(`buildToolTipText` 函数) + +**Interfaces:** +- Consumes: `formatSpeedShort()`、`activeInterface`、`ready` + +**说明:** Tooltip 改用 `formatSpeedShort` 与任务栏一致。`PanelToolTip.text` 是纯文本,不支持富文本着色,箭头无法单独着色。 + +- [ ] **Step 1: 替换 buildToolTipText 函数体** + +```qml + // 构建 tooltip 文本:单行紧凑展示下载/上传速度和活动接口 + // 设计原因:tooltip 空间有限,用 formatSpeedShort 与任务栏保持视觉一致 + // 用 "·" 分隔速度区与接口名,比 " | " 更轻量 + function buildToolTipText() { + if (!root.ready) { + return qsTr("未检测到网络接口") + } + + var lines = [] + lines.push("↓ " + root.formatSpeedShort(root.downloadSpeed)) + lines.push("↑ " + root.formatSpeedShort(root.uploadSpeed)) + lines.push(root.activeInterface) + return lines.join(" · ") + } +``` + +- [ ] **Step 2: 确认 PanelToolTip 未就绪文案为中文** + +确认 `PanelToolTip` 的 `text` 属性为: + +```qml + PanelToolTip { + id: toolTip + text: root.ready ? root.buildToolTipText() : qsTr("未检测到网络接口") + toolTipX: DockPanelPositioner.x + toolTipY: DockPanelPositioner.y + } +``` + +若为英文 `"No network interface detected"`,替换为上述中文。 + +- [ ] **Step 3: 安装并目视验证** + +Run: `sudo cmake --install /home/Jokul/Documents/projects/JNetApplet/build && systemctl --user restart dde-shell@DDE` +Expected: 悬停时 tooltip 显示形如 `↓ 1.2M · ↑ 256K · eth0`。 + +--- + +## Task 4: 左键弹出面板重写 + +**Files:** Modify `package/networkview.qml`(`PanelPopup` 内部,原 173-489 行) + +**Interfaces:** +- Consumes: `formatSpeed`、`formatTotal`、`primaryText`、`secondaryText`、`tertiaryText`、`cardBackground`、`cardBorder`、`accentBlue`、`accentBlueLight`、`accentGreen`、`accentGreenLight`、`downloadValueColor`、`uploadValueColor`、`ready`、`activeInterface`、`downloadSpeed`、`uploadSpeed`、`totalDownload`、`totalUpload`、`networkInterfaces`、`applet` +- Produces: 360×320 重构面板 + +**说明:** 尺寸从 400×350 改为 360×320。结构:标题栏(含副标题)+ 实时速度双列卡片 + 总量单行 + chip 接口切换 + 未就绪占位。 + +- [ ] **Step 1: 调整 PanelPopup 尺寸** + +将 `PanelPopup` 的 `width: 400` / `height: 350` 改为 `width: 360` / `height: 320`。保留 `popupX`/`popupY`/`onPopupVisibleChanged` 不变。 + +- [ ] **Step 2: 替换 Control 及其 contentItem 全部内容** + +将 `PanelPopup` 内的 `Control { id: popupContainer ... }` 整体替换为: + +```qml + Control { + id: popupContainer + anchors.fill: parent + padding: 16 + + contentItem: ColumnLayout { + spacing: 12 + + // 标题栏:左标题+副标题,右刷新按钮 + RowLayout { + Layout.fillWidth: true + spacing: 8 + + ColumnLayout { + spacing: 0 + Text { + text: qsTr("网络速度监控") + font.pixelSize: 15 + font.weight: Font.Bold + color: root.primaryText + } + Text { + text: root.ready ? root.activeInterface : qsTr("未连接") + font.pixelSize: 11 + color: root.secondaryText + visible: root.ready + } + } + + Item { Layout.fillWidth: true } + + // 刷新按钮:hover 高亮 + 旋转动画 + Rectangle { + width: 28 + height: 28 + radius: 6 + color: refreshIconArea.containsMouse ? root.accentBlueLight : "transparent" + + Text { + anchors.centerIn: parent + text: "⟳" + font.pixelSize: 16 + color: refreshIconArea.containsMouse ? root.accentBlue : root.secondaryText + } + + MouseArea { + id: refreshIconArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.applet) root.applet.refresh() + refreshSpin.start() + } + } + + RotationAnimator on rotation { + id: refreshSpin + running: false + from: 0 + to: 360 + duration: 500 + } + } + } + + // 实时速度卡片:双列布局,主视觉 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 110 + color: root.cardBackground + radius: 12 + border.width: 1 + border.color: root.cardBorder + + RowLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 0 + + // 下载列 + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: root.accentBlueLight + Layout.alignment: Qt.AlignHCenter + + Text { + anchors.centerIn: parent + text: "↓" + font.pixelSize: 18 + font.weight: Font.Bold + color: root.accentBlue + } + } + + Text { + text: qsTr("下载") + font.pixelSize: 11 + color: root.secondaryText + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: root.formatSpeed(root.downloadSpeed) + font.pixelSize: 18 + font.weight: Font.Bold + color: root.downloadValueColor + Layout.alignment: Qt.AlignHCenter + } + } + + // 分隔线 + Rectangle { + width: 1 + Layout.fillHeight: true + color: root.cardBorder + } + + // 上传列 + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: root.accentGreenLight + Layout.alignment: Qt.AlignHCenter + + Text { + anchors.centerIn: parent + text: "↑" + font.pixelSize: 18 + font.weight: Font.Bold + color: root.accentGreen + } + } + + Text { + text: qsTr("上传") + font.pixelSize: 11 + color: root.secondaryText + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: root.formatSpeed(root.uploadSpeed) + font.pixelSize: 18 + font.weight: Font.Bold + color: root.uploadValueColor + Layout.alignment: Qt.AlignHCenter + } + } + } + } + + // 总量统计:单行紧凑 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 44 + color: root.cardBackground + radius: 12 + border.width: 1 + border.color: root.cardBorder + visible: root.ready + + RowLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 8 + + Text { + text: qsTr("总计") + font.pixelSize: 11 + font.weight: Font.Bold + color: root.secondaryText + } + + Text { + text: "↓ " + root.formatTotal(root.totalDownload) + font.pixelSize: 12 + color: root.primaryText + } + + Item { Layout.fillWidth: true } + + Text { + text: "↑ " + root.formatTotal(root.totalUpload) + font.pixelSize: 12 + color: root.primaryText + } + } + } + + // 接口切换 chip 列表(仅多接口时显示) + Flow { + Layout.fillWidth: true + spacing: 6 + visible: root.ready && root.networkInterfaces.length > 1 + + Repeater { + model: root.networkInterfaces + + Rectangle { + width: chipText.implicitWidth + 20 + height: 28 + radius: 8 + color: modelData === root.activeInterface + ? root.accentBlue + : (chipMouse.containsMouse ? root.accentBlueLight : root.cardBackground) + border.width: 1 + border.color: modelData === root.activeInterface + ? root.accentBlue + : root.cardBorder + + Text { + id: chipText + anchors.centerIn: parent + text: modelData + font.pixelSize: 11 + font.weight: modelData === root.activeInterface ? Font.Bold : Font.Normal + color: modelData === root.activeInterface ? "white" : root.primaryText + } + + MouseArea { + id: chipMouse + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + if (root.applet) root.applet.setActiveInterface(modelData) + } + } + } + } + } + + // 未检测到接口占位 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 80 + color: root.cardBackground + radius: 12 + border.width: 1 + border.color: root.cardBorder + visible: !root.ready + + ColumnLayout { + anchors.centerIn: parent + spacing: 6 + + Text { + text: "⚠" + font.pixelSize: 24 + color: root.secondaryText + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: qsTr("未检测到网络接口") + font.pixelSize: 12 + color: root.secondaryText + Layout.alignment: Qt.AlignHCenter + } + } + } + + Item { Layout.fillHeight: true } + } + } +``` + +- [ ] **Step 3: 确认 `Component.onCompleted` 中 `DockPanelPositioner.bounding` 绑定保留** + +`PanelPopup` 末尾的 `Component.onCompleted` 块保留不变(用于定位 popup 位置)。 + +- [ ] **Step 4: 安装并目视验证** + +Run: `sudo cmake --install /home/Jokul/Documents/projects/JNetApplet/build && systemctl --user restart dde-shell@DDE` +Expected: +- 左键点击任务栏图标,弹出 360×320 面板 +- 标题栏显示「网络速度监控」+ 接口名副标题 +- 实时速度卡片双列:下载蓝、上传绿,各带圆形图标底 +- 总量统计单行显示「总计 ↓ X · ↑ Y」 +- 多接口时显示 chip 列表,当前接口高亮蓝色 +- 未检测到接口时显示占位卡片 + +--- + +## Task 5: 右键菜单与关于对话框 + +**Files:** Modify `package/networkview.qml`(新增 `Menu`、`Dialog`、右键 `TapHandler`,替换原左键 `TapHandler`) + +**Interfaces:** +- Consumes: `applet.refresh()`、`applet.setActiveInterface()`、`networkInterfaces`、`activeInterface`、`primaryText`、`secondaryText`、`accentBlue` +- Produces: 右键弹出 deepin 风格菜单 + +**说明:** 在 `PanelPopup` 之后、原左键 `TapHandler` 之前,新增 `Menu` 和 `Dialog`。在原左键 `TapHandler` 之后新增右键 `TapHandler`。 + +- [ ] **Step 1: 新增 contextMenu 和 aboutDialog** + +在 `PanelPopup` 闭合 `}` 之后,原 `// 构建 tooltip 文本` 注释之前,插入: + +```qml + // 右键菜单:deepin 风格 Menu,提供刷新/切换接口/关于快捷操作 + Menu { + id: contextMenu + + MenuItem { + text: qsTr("刷新") + onTriggered: { + if (root.applet) root.applet.refresh() + } + } + + MenuSeparator {} + + // 切换接口子菜单:动态构建可用接口列表 + Menu { + title: qsTr("切换接口") + enabled: root.networkInterfaces.length > 1 + + Instantiator { + model: root.networkInterfaces + delegate: MenuItem { + text: modelData + checkable: true + checked: modelData === root.activeInterface + onTriggered: { + if (root.applet) root.applet.setActiveInterface(modelData) + } + } + onObjectAdded: (index, object) => contextMenu.children[2].insertItem(index, object) + onObjectRemoved: (index, object) => contextMenu.children[2].removeItem(object) + } + } + + MenuSeparator {} + + MenuItem { + text: qsTr("关于") + onTriggered: aboutDialog.open() + } + } + + // 关于对话框:显示插件信息 + Dialog { + id: aboutDialog + anchors.centerIn: parent + modal: true + width: 280 + height: 160 + + background: Rectangle { + color: root.cardBackground + radius: 12 + border.width: 1 + border.color: root.cardBorder + } + + contentItem: ColumnLayout { + spacing: 8 + + Text { + text: qsTr("网络速度监控") + font.pixelSize: 16 + font.weight: Font.Bold + color: root.primaryText + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: qsTr("版本:1.0") + font.pixelSize: 12 + color: root.secondaryText + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: qsTr("监控网络速度和流量") + font.pixelSize: 11 + color: root.tertiaryText + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 240 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + Item { Layout.fillHeight: true } + + Button { + text: qsTr("确定") + Layout.alignment: Qt.AlignHCenter + onClicked: aboutDialog.close() + } + } + } +``` + +- [ ] **Step 2: 新增右键 TapHandler** + +在原左键 `TapHandler`(`acceptedButtons: Qt.LeftButton`)之后,文件末尾的 root 闭合 `}` 之前,插入: + +```qml + // 右键点击:弹出上下文菜单 + TapHandler { + acceptedButtons: Qt.RightButton + gesturePolicy: TapHandler.ReleaseWithinBounds + + onTapped: { + // 关闭可能打开的 popup,避免视觉冲突 + if (networkPopup.popupVisible) networkPopup.close() + toolTip.close() + // 使用 root 中心点定位菜单 + contextMenu.popup() + } + } +``` + +- [ ] **Step 3: 确认 Instantiator 子菜单挂载点正确** + +Run: `grep -n "Instantiator\|onObjectAdded" /home/Jokul/Documents/projects/JNetApplet/package/networkview.qml` +Expected: 出现一次 Instantiator 块。 + +> 注:Instantiator 的 `onObjectAdded`/`onObjectRemoved` 中 `contextMenu.children[2]` 是切换接口子菜单(菜单顺序:刷新 MenuItem / MenuSeparator / 切换接口 Menu / MenuSeparator / 关于 MenuItem)。若 QtQuick.Controls 版本不支持 `insertItem`/`removeItem`,可改用 `Menu` 的 `MenuItem` 直接列出(无 Instantiator),通过 `Instantiator` 的 `onObjectAdded` 调用 `parent.addItem(object)` 或直接在子 Menu 内用 `Repeater`+`MenuItem` 写法。验证时如报错,将子菜单改为: + +```qml + Menu { + title: qsTr("切换接口") + enabled: root.networkInterfaces.length > 1 + + Repeater { + model: root.networkInterfaces + delegate: MenuItem { + text: modelData + checkable: true + checked: modelData === root.activeInterface + onTriggered: { + if (root.applet) root.applet.setActiveInterface(modelData) + } + } + } + } +``` + +(两种写法二选一,优先用 Repeater 版本更稳。) + +- [ ] **Step 4: 安装并目视验证** + +Run: `sudo cmake --install /home/Jokul/Documents/projects/JNetApplet/build && systemctl --user restart dde-shell@DDE` +Expected: +- 右键点击任务栏图标,弹出 deepin 风格菜单 +- 菜单项:刷新 / 切换接口(子菜单)/ 关于 +- 切换接口子菜单列出所有接口,当前接口打勾 +- 接口数 ≤ 1 时「切换接口」项禁用(灰色) +- 点击「关于」弹出对话框显示版本信息 + +--- + +## Task 6: 最终验证清单 + +**Files:** 无改动,仅验证 + +**说明:** 按 spec 验证清单逐项检查。 + +- [ ] **Step 1: 重新构建并安装** + +Run: `cmake --build /home/Jokul/Documents/projects/JNetApplet/build && sudo cmake --install /home/Jokul/Documents/projects/JNetApplet/build && systemctl --user restart dde-shell@DDE` +Expected: 构建成功、安装成功、dde-shell 重启无报错。 + +- [ ] **Step 2: 任务栏图标验证** + +逐项检查: +- [ ] 双行数值显示,下载在上、上传在下 +- [ ] 颜色正确(下载蓝、上传绿) +- [ ] 高速时下载值变橙(>1MB/s)/红(>10MB/s) +- [ ] 深色任务栏下文字可读 +- [ ] 浅色任务栏下文字可读 +- [ ] 无圆圈边框 + +- [ ] **Step 3: Tooltip 验证** + +悬停任务栏图标: +- [ ] 单行显示 `↓ X · ↑ Y · 接口名` +- [ ] 未检测到接口时显示「未检测到网络接口」 + +- [ ] **Step 4: 左键弹出面板验证** + +左键点击任务栏图标: +- [ ] 尺寸 360×320 +- [ ] 标题栏:「网络速度监控」+ 接口名副标题 +- [ ] 实时速度双列卡片:下载蓝、上传绿,圆形图标底 +- [ ] 总量统计单行:「总计 ↓ X · ↑ Y」 +- [ ] 多接口时显示 chip 列表,当前接口高亮 +- [ ] 刷新按钮 hover 高亮 + 旋转动画 +- [ ] 未检测到接口时显示占位卡片 + +- [ ] **Step 5: 右键菜单验证** + +右键点击任务栏图标: +- [ ] 弹出 deepin 风格菜单 +- [ ] 刷新项可用,点击后数据更新 +- [ ] 切换接口子菜单列出接口、当前项打勾 +- [ ] 接口数 ≤ 1 时切换项禁用 +- [ ] 关于对话框显示版本信息 + +- [ ] **Step 6: 检查无 QML 运行时错误** + +Run: `journalctl --user -u dde-shell@DDE -n 50 --no-pager | grep -i "qml\|error" | head -20` +Expected: 无 QML 加载或运行时错误。 + +--- + +## Self-Review + +**Spec 覆盖检查:** +- §1 任务栏图标区 → Task 1 (颜色/函数) + Task 2 (布局) ✓ +- §2 左键弹出面板 → Task 4 ✓ +- §3 右键菜单 → Task 5 ✓ +- §4 Tooltip → Task 3 ✓ +- §5 视觉规范 → Task 1 (颜色表) + 各任务内尺寸 ✓ +- §6 排除项 → Global Constraints 明确 ✓ + +**占位符扫描:** 无 TBD/TODO,所有代码块完整。 + +**类型一致性:** +- `formatSpeedShort` 在 Task 1 定义,Task 2/3 使用 ✓ +- `downloadValueColor`/`uploadValueColor` 在 Task 1 定义,Task 2/4 使用 ✓ +- `accentGreen`/`accentGreenLight` 在 Task 1 定义,Task 4 使用 ✓ +- 颜色派生自 `basePalette` 全局一致 ✓ + +**已知风险:** +- Task 5 Step 1 的 `Instantiator`+`insertItem`/`removeItem` 写法在不同 QtQuick.Controls 版本可能有差异,提供了 Repeater 备选方案。 +- 右键菜单的 `contextMenu.popup()` 默认在鼠标位置弹出,dde-shell 环境下应正常工作;如定位异常可改用 `contextMenu.popup(mouseX, mouseY)`。 diff --git a/docs/superpowers/specs/2026-07-18-ui-redesign-design.md b/docs/superpowers/specs/2026-07-18-ui-redesign-design.md new file mode 100644 index 0000000..9a0f72c --- /dev/null +++ b/docs/superpowers/specs/2026-07-18-ui-redesign-design.md @@ -0,0 +1,228 @@ +# JNetApplet UI 重新设计规范 + +**日期**:2026-07-18 +**范围**:`package/networkview.qml` 单文件 +**方案**:A - 精简重塑(deepin 原生简约风) + +## 背景与目标 + +当前 JNetApplet 任务栏图标在 ~48px dock 区域内塞入圆圈边框 + 4 行内容(↓/下载速度/↑/上传速度),视觉拥挤;左键弹出面板色调偏灰、层次弱、接口切换 `ComboBox` 不够直观;右键行为未实现。 + +本次重新设计目标: + +- 任务栏展示精简、易读,跟随 deepin 任务栏视觉语言 +- 左键弹出面板信息层次清晰、视觉舒适 +- 新增右键菜单提供快捷操作 +- 全部颜色派生自 `DockPalette`,深/浅色任务栏自适应 + +## 范围 + +**包含**: + +- `package/networkview.qml` 全部重写 +- 任务栏图标区、左键弹出面板、右键菜单、Tooltip 四处视觉与交互 + +**排除**(YAGNI): + +- 不改 C++ 后端(`networkmonitorapplet.h/.cpp`) +- 不新增 `Q_PROPERTY` 或 `Q_INVOKABLE` +- 不引入设置面板(刷新间隔、单位制等) +- 不加历史趋势图表、sparkline +- 不持久化任何用户配置 +- 不改 `metadata.json`、`CMakeLists.txt` + +## 设计决策记录 + +| 决策点 | 选择 | 理由 | +|---|---|---| +| 整体风格 | deepin 原生简约风 | 跟随宿主环境视觉语言,自适应深/浅色 | +| 任务栏展示 | 双行数值(↓下载/↑上传) | 信息密集但不拥挤,无需 hover 即可读 | +| 右键行为 | deepin 风格 Menu | 符合 deepin 交互习惯,提供快捷操作 | +| 设置项 | 不做 | 控制范围,避免不持久化带来的困惑 | +| 历史图表 | 不做 | 超出"重新设计样式与交互"范围 | + +## §1. 任务栏图标区(dock item) + +### 布局 + +- 移除当前圆圈 `Rectangle` 容器 +- 双行 `Column` 居中: + - 上行:小号 `↓` + 下载速度数值(如 `1.2M`) + - 下行:小号 `↑` + 上传速度数值(如 `256K`) +- 箭头与数值同行,箭头为次级色、数值为强调色 + +### 新格式化函数 `formatSpeedShort(bytesPerSec)` + +紧凑格式,用于任务栏与 tooltip: + +| 字节范围 | 输出 | +|---|---| +| `< 1024` | 整数 B/s,如 `"512"` | +| `< 1024²` | 1 位小数 + `K`,如 `"1.2K"` | +| `< 1024³` | 1 位小数 + `M`,如 `"3.5M"` | +| `≥ 1024³` | 1 位小数 + `G`,如 `"2.4G"` | + +保留原 `formatSpeed` / `formatTotal` 函数供弹出面板使用(带单位更清晰)。 + +### 字号与字重 + +- 箭头:`dockSize * 0.16` +- 数值:`dockSize * 0.22`(当前 0.12 太小) +- 数值字重:`Font.Medium` + +### 颜色 + +- 下载值:`accentBlue` (`#1450A0`) +- 上传值:`accentGreen` (`#16A34A`) +- 高速警示(保留现有阈值逻辑): + - 下载 > 10 MB/s:红 (`#DC2626`) + - 下载 > 1 MB/s:橙 (`#F59E0B`) + +## §2. 左键弹出面板(popup) + +### 尺寸 + +从 `400×350` 调整为 `360×320`(更紧凑)。 + +### 结构(顶到底) + +1. **标题栏** + - 左:主标题「网络速度监控」+ 副标题(活动接口名,次级色 12px) + - 右:圆形刷新按钮(hover 高亮 + 旋转动画) + +2. **实时速度卡片**(主视觉) + - 横向双列布局 + - 左列:`↓` 圆形底(`accentBlueLight` 背景)+ 「下载」标签 + 大字号速度值(蓝) + - 右列:`↑` 圆形底(`accentGreenLight` 背景)+ 「上传」标签 + 大字号速度值(绿) + - 数值字号 22px、字重 Bold + - 使用 `formatSpeed`(带单位,更清晰) + +3. **总量统计** + - 单行紧凑横向布局 + - 「总计」标签 + `↓ 1.07 GB` + `↑ 74.8 MB` + - 字号 12px、次级色 + +4. **接口切换 chip 列表**(仅当 `networkInterfaces.length > 1` 时显示) + - 横向排列,每个接口一个圆角 chip(`radius: 8`) + - 当前活动 chip:`accentBlue` 背景 + 白字 + - 其他 chip:`cardBackground` + `primaryText` + - hover:`accentBlueLight` 背景 + - 点击触发 `applet.setActiveInterface(name)` + +5. **未检测到接口占位** + - 保留现有卡片样式 + - 文案居中:`⚠` + 「未检测到网络接口」 + +### 视觉 + +- 卡片圆角 12px +- 内边距 16px、卡片内边距 12px、元素间距 12px +- 分隔线:`cardBorder` 色、1px + +## §3. 右键菜单(新功能) + +### 触发 + +在现有 `TapHandler`(左键)旁新增 `TapHandler` 处理 `Qt.RightButton`,调用 `menu.popup()`。 + +### 菜单项 + +``` +刷新 +───────────── +切换接口 ▸ (子菜单) +───────────── +关于 +``` + +### 行为 + +- **刷新**:调用 `applet.refresh()` +- **切换接口子菜单**: + - 动态构建 `MenuItem`,绑定 `networkInterfaces` + - 当前 `activeInterface` 项打勾(`checkable: true`、`checked: true`) + - 点击触发 `applet.setActiveInterface(name)` + - 接口数 ≤ 1 时禁用此项 +- **关于**:弹出小型 `Dialog` + - 插件名:网络速度监控 + - 版本:1.0(硬编码,与 `metadata.json` 一致) + - 描述:监控网络速度和流量 + +### 实现要点 + +- 使用 `QtQuick.Controls` 的 `Menu`(deepin DTK 自动适配风格) +- `Menu` 作为 root 的子元素,id 为 `contextMenu` +- 子菜单用 `Menu` 嵌套 + +## §4. Tooltip + +单行简洁: + +``` +↓ 1.2M/s ↑ 256K/s · eth0 +``` + +- 方向箭头用对应强调色(下载蓝、上传绿) +- 接口名前用 `·` 分隔 +- 复用 `formatSpeedShort` 保持与任务栏一致 +- 未检测到接口时显示「未检测到网络接口」 + +## §5. 视觉规范 + +### 颜色派生表 + +| 用途 | 计算 | +|---|---| +| 主文字 `primaryText` | `basePalette` α 0.95 | +| 次级文字 `secondaryText` | `basePalette` α 0.80 | +| 三级文字 `tertiaryText` | `basePalette` α 0.65 | +| 卡片背景 `cardBackground` | `basePalette` α 0.06 | +| 卡片边框 `cardBorder` | `basePalette` α 0.10 | +| 强调蓝 `accentBlue` | `#1450A0` | +| 强调蓝浅 `accentBlueLight` | `#1450A0` α 0.12 | +| 强调绿 `accentGreen` | `#16A34A` | +| 强调绿浅 `accentGreenLight` | `#16A34A` α 0.12 | +| 高速橙 `accentOrange` | `#F59E0B` | +| 高速红 `accentRed` | `#DC2626` | + +### 尺寸规范 + +- 卡片圆角:12px +- chip 圆角:8px +- 按钮圆角:6px +- 面板内边距:16px +- 卡片内边距:12px +- 元素间距:12px + +### 字体 + +跟随系统无衬线字体,不指定 family。字重使用 `Font.Medium` / `Font.Bold`。 + +## 实现性质 + +- **文件**:仅 `package/networkview.qml` +- **风险**:低(纯前端、不动后端、不改构建) +- **验证**:手动安装 + 重启 dde-shell + +## 验证清单 + +安装后重启 dde-shell,依次检查: + +- [ ] 任务栏图标:双行数值显示,下载在上、上传在下 +- [ ] 任务栏图标:颜色正确(下载蓝、上传绿) +- [ ] 任务栏图标:高速时下载值变橙/红 +- [ ] 任务栏图标:深色任务栏下文字可读 +- [ ] 任务栏图标:浅色任务栏下文字可读 +- [ ] 悬停 tooltip:单行显示 ↓↑速度 + 接口名 +- [ ] 左键弹出:尺寸 360×320 +- [ ] 左键弹出:标题栏 + 接口名副标题 +- [ ] 左键弹出:实时速度双列卡片 +- [ ] 左键弹出:总量统计单行 +- [ ] 左键弹出:接口 chip 列表(多接口时) +- [ ] 左键弹出:刷新按钮 hover + 旋转动画 +- [ ] 左键弹出:未检测到接口时显示占位 +- [ ] 右键菜单:弹出 deepin 风格菜单 +- [ ] 右键菜单:刷新项可用 +- [ ] 右键菜单:切换接口子菜单列出接口、当前项打勾 +- [ ] 右键菜单:接口数 ≤ 1 时切换项禁用 +- [ ] 右键菜单:关于对话框显示版本信息 diff --git a/networkmonitorapplet.cpp b/networkmonitorapplet.cpp index 410d3ca..db4ecae 100644 --- a/networkmonitorapplet.cpp +++ b/networkmonitorapplet.cpp @@ -10,6 +10,7 @@ #include #include #include +#include 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 diff --git a/networkmonitorapplet.h b/networkmonitorapplet.h index 975dea1..1365b70 100644 --- a/networkmonitorapplet.h +++ b/networkmonitorapplet.h @@ -38,6 +38,8 @@ class NetworkMonitorApplet : public DApplet Q_PROPERTY(QStringList interfaceStats READ interfaceStats NOTIFY statsChanged) Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) Q_PROPERTY(QString activeInterface READ activeInterface NOTIFY activeInterfaceChanged) + // 活动接口的 IPv4 地址,供 QML 在弹出面板和 tooltip 中显示 + Q_PROPERTY(QString ipAddress READ ipAddress NOTIFY ipAddressChanged) public: explicit NetworkMonitorApplet(QObject *parent = nullptr); @@ -54,6 +56,7 @@ public: QStringList interfaceStats() const; bool ready() const; QString activeInterface() const; + QString ipAddress() const; Q_INVOKABLE void refresh(); Q_INVOKABLE void setActiveInterface(const QString &interface); @@ -65,11 +68,17 @@ signals: void statsChanged(); void readyChanged(); void activeInterfaceChanged(); + void ipAddressChanged(); private: void readNetworkStats(); void calculateSpeed(); void detectInterfaces(); + // 检测活动接口的 IPv4 地址,变化时发射 ipAddressChanged + void detectIpAddress(); + // 判断是否为物理网卡(无线 wlp/wlan,有线 enp/eth), + // 用于自动选择时优先真实网卡而非虚拟代理接口(如 Meta/tun0) + bool isPhysicalInterface(const QString &name) const; qint64 getActiveRxBytes() const; qint64 getActiveTxBytes() const; @@ -77,6 +86,7 @@ private: QMap m_interfaces; QStringList m_interfaceList; QString m_activeInterface; + QString m_ipAddress; // 活动接口的 IPv4 地址 // 速度计算 qint64 m_lastRxBytes; diff --git a/package/networkview.qml b/package/networkview.qml index e29f734..60921fb 100644 --- a/package/networkview.qml +++ b/package/networkview.qml @@ -6,6 +6,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import QtQuick.Window 2.15 +import Qt.labs.platform as Platform import org.deepin.ds 1.0 import org.deepin.ds.dock 1.0 import org.deepin.dtk 1.0 @@ -16,7 +17,9 @@ AppletItem { property int dockOrder: 21 property int dockSize: Panel.rootObject.dockItemMaxSize || 48 - implicitWidth: dockSize + // 任务栏宽度稍宽于标准 dock 尺寸,容纳双行速度数值 + // 设计原因:固定宽度避免数值长度变化导致插件宽度抖动、箭头位置漂移 + implicitWidth: Math.round(dockSize * 1.35) implicitHeight: dockSize readonly property var applet: Applet @@ -26,15 +29,30 @@ AppletItem { readonly property real totalDownload: applet ? applet.totalDownload : 0 readonly property real totalUpload: applet ? applet.totalUpload : 0 readonly property string activeInterface: applet ? (applet.activeInterface || "") : "" + // 活动接口的 IPv4 地址,由 C++ 后端 detectIpAddress 持续更新 + readonly property string ipAddress: applet ? (applet.ipAddress || "") : "" readonly property var networkInterfaces: applet ? (applet.networkInterfaces || []) : [] readonly property var interfaceStats: applet ? (applet.interfaceStats || []) : [] - // 格式化速度显示 + // 紧凑格式化速度(用于任务栏图标和 tooltip,不带单位后缀,节省空间) + // 设计原因:任务栏 ~48px 空间有限,"1.2M" 比 "1.2 MB/s" 节省约一半宽度 + // 最小单位为 KB:B 级别也转换为 KB 显示(如 512 B/s -> "0.50K"), + // 避免纯数字无后缀时用户无法判断单位;所有级别保留 2 位小数 + function formatSpeedShort(bytesPerSec) { + if (bytesPerSec < 1024 * 1024) { + return (bytesPerSec / 1024).toFixed(2) + "K" + } else if (bytesPerSec < 1024 * 1024 * 1024) { + return (bytesPerSec / (1024 * 1024)).toFixed(2) + "M" + } else { + return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(2) + "G" + } + } + + // 格式化速度显示(带单位,用于弹出面板,信息更完整) + // 最小单位为 KB,与 formatSpeedShort 保持一致;所有级别保留 2 位小数 function formatSpeed(bytesPerSec) { - if (bytesPerSec < 1024) { - return bytesPerSec.toFixed(0) + " B/s" - } else if (bytesPerSec < 1024 * 1024) { - return (bytesPerSec / 1024).toFixed(1) + " KB/s" + if (bytesPerSec < 1024 * 1024) { + return (bytesPerSec / 1024).toFixed(2) + " KB/s" } else if (bytesPerSec < 1024 * 1024 * 1024) { return (bytesPerSec / (1024 * 1024)).toFixed(2) + " MB/s" } else { @@ -42,7 +60,7 @@ AppletItem { } } - // 格式化总量显示 + // 格式化总量显示(用于弹出面板的累计统计) function formatTotal(bytes) { if (bytes < 1024) { return bytes.toFixed(0) + " B" @@ -55,89 +73,105 @@ AppletItem { } } - // 根据速度计算图标颜色 - readonly property color iconColor: { - if (downloadSpeed > 10 * 1024 * 1024) return Qt.rgba(220/255, 38/255, 38/255, 1) // 高速红色 - if (downloadSpeed > 1 * 1024 * 1024) return Qt.rgba(245/255, 158/255, 11/255, 1) // 中速橙色 - return root.primaryText // 正常颜色 - } - + // 任务栏与面板的颜色全部派生自 DockPalette.iconTextPalette + // 这样在深色/浅色任务栏下自动适配,无需手动判断主题 property Palette basePalette: DockPalette.iconTextPalette readonly property color primaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.95) - readonly property color secondaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.8) + readonly property color secondaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.80) readonly property color tertiaryText: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.65) readonly property color cardBackground: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.06) - readonly property color cardBorder: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.1) + readonly property color cardBorder: Qt.rgba(basePalette.r, basePalette.g, basePalette.b, 0.10) + + // 强调色:下载蓝、上传绿,是面板与任务栏的主视觉区分 readonly property color accentBlue: Qt.rgba(20 / 255, 80 / 255, 160 / 255, 1) readonly property color accentBlueLight: Qt.rgba(20 / 255, 80 / 255, 160 / 255, 0.12) + readonly property color accentGreen: Qt.rgba(22 / 255, 163 / 255, 74 / 255, 1) + readonly property color accentGreenLight: Qt.rgba(22 / 255, 163 / 255, 74 / 255, 0.12) - // 图标区域 - Rectangle { - anchors.centerIn: parent - width: dockSize * 0.7 - height: dockSize * 0.7 - color: "transparent" - radius: width * 0.45 - border.width: 1 - border.color: root.iconColor + // 高速警示色:下载速度超过阈值时由蓝转橙再转红 + readonly property color accentOrange: Qt.rgba(245 / 255, 158 / 255, 11 / 255, 1) + readonly property color accentRed: Qt.rgba(220 / 255, 38 / 255, 38 / 255, 1) - Column { - anchors.centerIn: parent + // 下载值颜色:保留原有阈值逻辑(>10MB/s 红、>1MB/s 橙、否则蓝),仅作用于下载值 + readonly property color downloadValueColor: { + if (downloadSpeed > 10 * 1024 * 1024) return accentRed + if (downloadSpeed > 1 * 1024 * 1024) return accentOrange + return accentBlue + } + + // 上传值颜色:固定绿色,不做高速警示 + readonly property color uploadValueColor: accentGreen + + // 任务栏图标区:双行紧凑数值,箭头与数值紧贴、左对齐 + // 设计原因:箭头与数值作为一组固定在左侧,数值左对齐紧贴箭头; + // 不给每行固定高度,让 RowLayout 按内容自然高度排列, + // 整组垂直居中于 dock 区域,避免两行间出现过大间隙 + Column { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 4 + spacing: 0 + + // 下载速度行 + RowLayout { spacing: 2 - // 下载速度 Text { - anchors.horizontalCenter: parent.horizontalCenter text: "↓" font.pixelSize: root.dockSize * 0.18 - color: root.iconColor + color: root.secondaryText + Layout.alignment: Qt.AlignVCenter } Text { - anchors.horizontalCenter: parent.horizontalCenter - text: formatSpeed(root.downloadSpeed) - font.pixelSize: root.dockSize * 0.12 - font.bold: true - color: root.iconColor + text: root.formatSpeedShort(root.downloadSpeed) + font.pixelSize: root.dockSize * 0.22 + font.weight: Font.Medium + color: root.downloadValueColor + Layout.alignment: Qt.AlignVCenter + horizontalAlignment: Text.AlignLeft } + } + + // 上传速度行 + RowLayout { + spacing: 2 - // 上传速度 Text { - anchors.horizontalCenter: parent.horizontalCenter text: "↑" font.pixelSize: root.dockSize * 0.18 - color: root.iconColor + color: root.secondaryText + Layout.alignment: Qt.AlignVCenter } Text { - anchors.horizontalCenter: parent.horizontalCenter - text: formatSpeed(root.uploadSpeed) - font.pixelSize: root.dockSize * 0.12 - font.bold: true - color: root.iconColor + text: root.formatSpeedShort(root.uploadSpeed) + font.pixelSize: root.dockSize * 0.22 + font.weight: Font.Medium + color: root.uploadValueColor + Layout.alignment: Qt.AlignVCenter + horizontalAlignment: Text.AlignLeft } } } - // 悬停提示 + // 悬停提示:网卡名 + IP 地址一行展示 + // PanelToolTip 不支持 contentItem 覆盖和文本对齐,用默认左对齐 text 属性 PanelToolTip { id: toolTip - text: root.ready ? buildToolTipText() : qsTr("No network interface detected") + text: root.ready + ? (root.activeInterface + " · " + qsTr("IP地址:") + (root.ipAddress || qsTr("无"))) + : qsTr("未检测到网络接口") toolTipX: DockPanelPositioner.x toolTipY: DockPanelPositioner.y } - // 统计数据刷新定时器 - Timer { - id: statsRefreshTimer - interval: 1000 - repeat: true - onTriggered: { - if (root.applet) { - root.applet.refresh() - } - } - } + // 注意:不再需要 QML 侧的 statsRefreshTimer。 + // C++ 后端 NetworkMonitorApplet::init() 已启动 1 秒间隔的 m_refreshTimer, + // 持续调用 refresh()->readNetworkStats()->calculateSpeed() 更新速度属性。 + // QML 通过属性绑定自动获取最新值,无需主动触发 refresh。 + // 此前 QML 额外调用 applet.refresh() 会打破 1 秒定时间隔, + // 导致 calculateSpeed() 基于不固定间隔计算字节差,速度显示接近 0。 Timer { id: toolTipShowTimer @@ -151,20 +185,15 @@ AppletItem { HoverHandler { onHoveredChanged: { + // 仅控制 tooltip 显示,不触发 refresh。 + // C++ 后端 m_refreshTimer 持续每秒更新速度属性,QML 属性绑定自动反映。 if (hovered && !networkPopup.popupVisible) { toolTipShowTimer.start() - if (root.applet) { - root.applet.refresh() - } - statsRefreshTimer.start() } else { if (toolTipShowTimer.running) { toolTipShowTimer.stop() } toolTip.close() - if (!networkPopup.popupVisible) { - statsRefreshTimer.stop() - } } } } @@ -172,20 +201,16 @@ AppletItem { // 弹出窗口 PanelPopup { id: networkPopup - width: 400 - height: 350 + width: 360 + height: 320 popupX: DockPanelPositioner.x popupY: DockPanelPositioner.y onPopupVisibleChanged: { + // 仅控制 tooltip 关闭,不触发 refresh。 + // C++ 后端 m_refreshTimer 持续更新,popup 通过属性绑定自动显示最新数据。 if (popupVisible) { toolTip.close() - statsRefreshTimer.start() - if (root.applet) { - root.applet.refresh() - } - } else { - statsRefreshTimer.stop() } } @@ -195,266 +220,266 @@ AppletItem { padding: 16 contentItem: ColumnLayout { - spacing: 14 + spacing: 12 - // 标题区域 - RowLayout { + // 标题栏:标题在上,网卡名+IP 在卡片底色背景内 + Item { Layout.fillWidth: true - spacing: 10 + Layout.preferredHeight: 72 - Item { Layout.fillWidth: true } - - Rectangle { - width: 28 - height: 28 - color: accentBlueLight - radius: 8 + ColumnLayout { + anchors.centerIn: parent + spacing: 6 Text { - anchors.centerIn: parent - text: "NET" - font.pixelSize: 12 - font.bold: true - color: accentBlue - } - } - - Text { - text: qsTr("Network Monitor") - font.pixelSize: 16 - font.bold: true - color: root.primaryText - } - - Item { Layout.fillWidth: true } - - // 刷新图标 - Rectangle { - width: 28 - height: 28 - radius: 6 - color: refreshIconArea.containsMouse ? root.accentBlueLight : "transparent" - - Text { - anchors.centerIn: parent - text: "⟳" - font.pixelSize: 18 - color: refreshIconArea.containsMouse ? root.accentBlue : root.secondaryText + text: qsTr("网络速度监控") + font.pixelSize: 15 + font.weight: Font.Bold + color: root.primaryText + Layout.alignment: Qt.AlignHCenter } - MouseArea { - id: refreshIconArea - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - if (root.applet) { - root.applet.refresh() + // 网卡名 + IP 地址卡片背景 + Rectangle { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 180 + Layout.preferredHeight: 40 + color: root.cardBackground + radius: 8 + border.width: 1 + border.color: root.cardBorder + visible: root.ready + + ColumnLayout { + anchors.centerIn: parent + spacing: 0 + + Text { + text: root.activeInterface + font.pixelSize: 11 + color: root.secondaryText + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: root.ipAddress ? qsTr("IP地址:") + root.ipAddress : "" + font.pixelSize: 13 + color: root.secondaryText + visible: root.ipAddress !== "" + Layout.alignment: Qt.AlignHCenter } } } + } + } - // 旋转动画 - RotationAnimator on rotation { - id: refreshSpin - running: false - from: 0 - to: 360 - duration: 500 + // 实时速度卡片:双列布局,每列内容在各自半区内水平垂直居中 + // 设计原因:用显式 anchor 分两个等宽半区,比 RowLayout+fillWidth 更可靠, + // 确保下载/上传内容各自在左/右半区正中显示 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 110 + color: root.cardBackground + radius: 12 + border.width: 1 + border.color: root.cardBorder + + // 下载半区(左半) + Item { + id: downloadHalf + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.leftMargin: 12 + width: (parent.width - 1) / 2 - 12 + + ColumnLayout { + anchors.centerIn: parent + spacing: 4 + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: root.accentBlueLight + Layout.alignment: Qt.AlignHCenter + + Text { + anchors.centerIn: parent + text: "↓" + font.pixelSize: 18 + font.weight: Font.Bold + color: root.accentBlue + } + } + + Text { + text: qsTr("下载") + font.pixelSize: 11 + color: root.secondaryText + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: root.formatSpeed(root.downloadSpeed) + font.pixelSize: 18 + font.weight: Font.Bold + color: root.downloadValueColor + Layout.alignment: Qt.AlignHCenter + } } + } - Connections { - target: refreshIconArea - function onClicked() { refreshSpin.start() } + // 中间分隔线 + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 12 + anchors.bottom: parent.bottom + anchors.bottomMargin: 12 + width: 1 + color: root.cardBorder + } + + // 上传半区(右半) + Item { + id: uploadHalf + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.rightMargin: 12 + width: (parent.width - 1) / 2 - 12 + + ColumnLayout { + anchors.centerIn: parent + spacing: 4 + + Rectangle { + width: 32 + height: 32 + radius: 16 + color: root.accentGreenLight + Layout.alignment: Qt.AlignHCenter + + Text { + anchors.centerIn: parent + text: "↑" + font.pixelSize: 18 + font.weight: Font.Bold + color: root.accentGreen + } + } + + Text { + text: qsTr("上传") + font.pixelSize: 11 + color: root.secondaryText + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: root.formatSpeed(root.uploadSpeed) + font.pixelSize: 18 + font.weight: Font.Bold + color: root.uploadValueColor + Layout.alignment: Qt.AlignHCenter + } } } } - // 分隔线 + // 总量统计:单行紧凑 Rectangle { Layout.fillWidth: true - Layout.preferredHeight: 1 - color: root.cardBorder - } - - // 当前接口信息 - RowLayout { - Layout.fillWidth: true - spacing: 8 + Layout.preferredHeight: 44 + color: root.cardBackground + radius: 12 + border.width: 1 + border.color: root.cardBorder visible: root.ready - Text { - text: qsTr("Interface:") - font.pixelSize: 12 - color: root.secondaryText - } - - Text { - text: root.activeInterface - font.pixelSize: 12 - font.bold: true - color: root.primaryText - Layout.fillWidth: true - elide: Text.ElideRight - } - } - - // 速度统计卡片 - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 120 - color: root.cardBackground - radius: 10 - border.width: 1 - border.color: root.cardBorder - - ColumnLayout { - anchors.fill: parent - anchors.margins: 12 - spacing: 12 - - // 下载速度 - RowLayout { - Layout.fillWidth: true - spacing: 8 - - Text { - text: "↓ " + qsTr("Download") - font.pixelSize: 13 - color: root.secondaryText - } - - Item { Layout.fillWidth: true } - - Text { - text: formatSpeed(root.downloadSpeed) - font.pixelSize: 18 - font.bold: true - color: root.accentBlue - } - } - - // 上传速度 - RowLayout { - Layout.fillWidth: true - spacing: 8 - - Text { - text: "↑ " + qsTr("Upload") - font.pixelSize: 13 - color: root.secondaryText - } - - Item { Layout.fillWidth: true } - - Text { - text: formatSpeed(root.uploadSpeed) - font.pixelSize: 18 - font.bold: true - color: Qt.rgba(22/255, 163/255, 74/255, 1) - } - } - } - } - - // 总量统计卡片 - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 80 - color: root.cardBackground - radius: 10 - border.width: 1 - border.color: root.cardBorder - - ColumnLayout { + RowLayout { anchors.fill: parent anchors.margins: 12 spacing: 8 + Item { Layout.fillWidth: true } + Text { - text: qsTr("Total Data") - font.pixelSize: 12 - font.bold: true + text: qsTr("总计") + font.pixelSize: 11 + font.weight: Font.Bold color: root.secondaryText } - RowLayout { - Layout.fillWidth: true - spacing: 20 - - // 下载总量 - ColumnLayout { - spacing: 2 - Text { - text: "↓ " + qsTr("Download") - font.pixelSize: 11 - color: root.tertiaryText - } - Text { - text: formatTotal(root.totalDownload) - font.pixelSize: 14 - font.bold: true - color: root.primaryText - } - } - - // 上传总量 - ColumnLayout { - spacing: 2 - Text { - text: "↑ " + qsTr("Upload") - font.pixelSize: 11 - color: root.tertiaryText - } - Text { - text: formatTotal(root.totalUpload) - font.pixelSize: 14 - font.bold: true - color: root.primaryText - } - } + Text { + text: "↓ " + root.formatTotal(root.totalDownload) + font.pixelSize: 12 + color: root.primaryText } + + Text { + text: "↑ " + root.formatTotal(root.totalUpload) + font.pixelSize: 12 + color: root.primaryText + } + + Item { Layout.fillWidth: true } } } - // 接口切换下拉框 - ComboBox { - id: interfaceCombo + // 接口切换 chip 列表(仅多接口时显示,一行等宽分散显示) + Row { Layout.fillWidth: true - Layout.preferredHeight: 32 - visible: root.networkInterfaces.length > 1 - model: root.networkInterfaces - currentIndex: { - var idx = root.networkInterfaces.indexOf(root.activeInterface) - return idx >= 0 ? idx : 0 - } - onActivated: { - if (root.applet && root.networkInterfaces[index]) { - root.applet.setActiveInterface(root.networkInterfaces[index]) + spacing: 6 + visible: root.ready && root.networkInterfaces.length > 1 + + Repeater { + model: root.networkInterfaces + + Rectangle { + // 每个 chip 等宽分配,撑满一行分散显示 + width: root.networkInterfaces.length > 0 + ? (parent.width - 6 * (root.networkInterfaces.length - 1)) / root.networkInterfaces.length + : 0 + height: 28 + radius: 8 + color: modelData === root.activeInterface + ? root.accentBlue + : (chipMouse.containsMouse ? root.accentBlueLight : root.cardBackground) + border.width: 1 + border.color: modelData === root.activeInterface + ? root.accentBlue + : root.cardBorder + + Text { + id: chipText + anchors.centerIn: parent + text: modelData + font.pixelSize: 11 + font.weight: modelData === root.activeInterface ? Font.Bold : Font.Normal + color: modelData === root.activeInterface ? "white" : root.primaryText + } + + MouseArea { + id: chipMouse + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + if (root.applet) root.applet.setActiveInterface(modelData) + } + } } } - - background: Rectangle { - color: interfaceCombo.hovered ? root.accentBlueLight : root.cardBackground - radius: 6 - border.width: 1 - border.color: root.cardBorder - } - - contentItem: Text { - text: interfaceCombo.displayText - font.pixelSize: 12 - color: root.primaryText - verticalAlignment: Text.AlignVCenter - leftPadding: 10 - } } - // 未检测到网络接口提示 + // 未检测到接口占位 Rectangle { Layout.fillWidth: true - Layout.preferredHeight: 60 + Layout.preferredHeight: 80 color: root.cardBackground - radius: 10 + radius: 12 border.width: 1 border.color: root.cardBorder visible: !root.ready @@ -465,13 +490,16 @@ AppletItem { Text { text: "⚠" - font.pixelSize: 20 + font.pixelSize: 24 + color: root.secondaryText + Layout.alignment: Qt.AlignHCenter } Text { - text: qsTr("No network interface detected") + text: qsTr("未检测到网络接口") font.pixelSize: 12 color: root.secondaryText + Layout.alignment: Qt.AlignHCenter } } } @@ -488,17 +516,458 @@ AppletItem { } } - // 构建 tooltip 文本 - function buildToolTipText() { - if (!root.ready) { - return qsTr("No network interface detected") + // 右键菜单:使用 Qt.labs.platform.Menu + MenuHelper + // 设计原因:dde-shell dock 环境下 AppletItem 嵌入 layer-shell 窗口, + // QtQuick.Controls.Menu.popup() 无法获取有效 QQuickWindow 来弹出菜单。 + // 必须用 Qt.labs.platform.Menu(原生菜单)配合 org.deepin.ds.dock 的 + // MenuHelper 单例管理菜单生命周期(确保同一时间只有一个菜单打开)。 + Platform.Menu { + id: contextMenu + + Platform.MenuItem { + text: qsTr("刷新") + onTriggered: { + if (root.applet) root.applet.refresh() + } } - var lines = [] - lines.push("↓ " + formatSpeed(root.downloadSpeed)) - lines.push("↑ " + formatSpeed(root.uploadSpeed)) - lines.push(root.activeInterface) - return lines.join(" | ") + Platform.MenuSeparator {} + + Platform.MenuItem { + text: qsTr("设置") + onTriggered: settingsWindow.show() + } + + Platform.MenuSeparator {} + + Platform.MenuItem { + text: qsTr("关于") + onTriggered: aboutDialog.open() + } + } + + // 关于对话框:显示插件信息 + Dialog { + id: aboutDialog + anchors.centerIn: parent + modal: true + width: 280 + height: 160 + + background: Rectangle { + color: root.cardBackground + radius: 12 + border.width: 1 + border.color: root.cardBorder + } + + contentItem: ColumnLayout { + spacing: 8 + + Text { + text: qsTr("网络速度监控") + font.pixelSize: 16 + font.weight: Font.Bold + color: root.primaryText + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: qsTr("版本:1.0") + font.pixelSize: 12 + color: root.secondaryText + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: qsTr("监控网络速度和流量") + font.pixelSize: 11 + color: root.tertiaryText + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: 240 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + Item { Layout.fillHeight: true } + + Button { + text: qsTr("确定") + Layout.alignment: Qt.AlignHCenter + onClicked: aboutDialog.close() + } + } + } + + // 设置窗口:独立顶层窗口,在桌面中间弹出 + // 设计原因:Dialog 使用父窗口(dock layer-surface)的 overlay,仅覆盖任务栏区域, + // 无法在桌面中间显示。改用 Window 创建独立顶层窗口,可在桌面任意位置弹出。 + Window { + id: settingsWindow + width: 340 + height: 350 + visible: false + flags: Qt.FramelessWindowHint | Qt.Window + // NonModal:不阻塞桌面其他区域,用户可同时操作任务栏 + modality: Qt.NonModal + // 窗口透明:让圆角外的区域不显示,由内部 Rectangle 提供可见背景 + color: "transparent" + + // 显示时居中到桌面 + onVisibleChanged: { + if (visible) { + x = (Screen.width - width) / 2 + y = (Screen.height - height) / 2 + } + } + + // 主容器:提供不透明背景 + 圆角 + 边框 + Rectangle { + anchors.fill: parent + color: "#f5f5f5" + radius: 12 + border.width: 1 + border.color: "#e0e0e0" + + ColumnLayout { + anchors.fill: parent + anchors.margins: 0 + spacing: 0 + + // 自定义标题栏(可拖动) + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 44 + color: "transparent" + + // 拖动区域 + MouseArea { + anchors.fill: parent + drag.target: settingsWindow + cursorShape: Qt.ArrowCursor + } + + // 标题文字 + Text { + anchors.left: parent.left + anchors.leftMargin: 16 + anchors.verticalCenter: parent.verticalCenter + text: qsTr("设置") + font.pixelSize: 15 + font.weight: Font.Bold + color: "#333333" + } + + // 关闭按钮 + Rectangle { + id: closeButton + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + width: 28 + height: 28 + radius: 14 + color: closeMouse.containsMouse ? Qt.rgba(220/255, 38/255, 38/255, 0.15) : "transparent" + + Text { + anchors.centerIn: parent + text: "×" + font.pixelSize: 20 + font.weight: Font.Bold + color: closeMouse.containsMouse ? root.accentRed : "#666666" + } + + MouseArea { + id: closeMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: settingsWindow.hide() + } + } + } + + // 分隔线 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: "#e0e0e0" + } + + // 内容区域 + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + // 网络接口选择区 + Text { + text: qsTr("网络接口") + font.pixelSize: 13 + font.weight: Font.Bold + color: "#666666" + } + + // 接口列表容器 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: root.networkInterfaces.length > 0 ? 40 + root.networkInterfaces.length * 28 : 80 + color: "#ffffff" + radius: 10 + border.width: 1 + border.color: "#e0e0e0" + + ColumnLayout { + anchors.fill: parent + anchors.margins: 12 + spacing: 8 + + // 接口列表:RadioButton 单选切换活动接口 + Repeater { + model: root.networkInterfaces + delegate: Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 28 + color: "transparent" + radius: 6 + + RadioButton { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + text: modelData + checked: modelData === root.activeInterface + onToggled: { + if (root.applet) root.applet.setActiveInterface(modelData) + } + } + } + } + + // 无接口提示 + Text { + text: qsTr("未检测到网络接口") + font.pixelSize: 12 + color: "#999999" + visible: root.networkInterfaces.length === 0 + Layout.alignment: Qt.AlignHCenter + } + } + } + + Item { Layout.fillHeight: true } + + // 卸载插件按钮 + Rectangle { + id: uninstallButton + Layout.fillWidth: true + Layout.preferredHeight: 40 + color: uninstallMouse.containsMouse ? Qt.rgba(220/255, 38/255, 38/255, 0.15) : Qt.rgba(220/255, 38/255, 38/255, 0.08) + radius: 10 + border.width: 1 + border.color: root.accentRed + + Text { + anchors.centerIn: parent + text: qsTr("卸载插件") + font.pixelSize: 13 + font.weight: Font.Medium + color: root.accentRed + } + + MouseArea { + id: uninstallMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: uninstallConfirmDialog.open() + } + } + + // 卸载状态提示(确认后显示,提示用户去终端执行) + Text { + id: uninstallStatus + visible: false + text: qsTr("卸载命令已复制到剪贴板,请在终端中粘贴执行") + font.pixelSize: 11 + color: "#16a34a" + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + // 关闭按钮 + Rectangle { + id: bottomCloseButton + Layout.fillWidth: true + Layout.preferredHeight: 40 + color: bottomCloseMouse.containsMouse ? root.accentBlueLight : "#ffffff" + radius: 10 + border.width: 1 + border.color: bottomCloseMouse.containsMouse ? root.accentBlue : "#e0e0e0" + + Text { + anchors.centerIn: parent + text: qsTr("关闭") + font.pixelSize: 13 + font.weight: Font.Medium + color: bottomCloseMouse.containsMouse ? root.accentBlue : "#333333" + } + + MouseArea { + id: bottomCloseMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: settingsWindow.hide() + } + } + } + } + } + } + + // 卸载确认对话框 + Dialog { + id: uninstallConfirmDialog + width: 320 + height: 260 + modal: true + visible: false + + background: Rectangle { + color: "#f5f5f5" + radius: 12 + border.width: 1 + border.color: "#e0e0e0" + } + + contentItem: ColumnLayout { + spacing: 16 + anchors.fill: parent + anchors.margins: 20 + + Text { + text: qsTr("警告") + font.pixelSize: 16 + font.weight: Font.Bold + color: root.accentRed + Layout.alignment: Qt.AlignHCenter + } + + Text { + text: qsTr("确定要卸载此插件吗?此操作将删除插件文件并重启 dde-shell。") + font.pixelSize: 12 + color: "#333333" + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + // 命令显示区域 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 60 + color: "#e8e8e8" + radius: 8 + + Text { + anchors.fill: parent + anchors.margins: 10 + text: "sudo rm -rf /usr/share/dde-shell/space.jokul.JNetApplet/ && systemctl --user restart dde-shell@DDE" + font.pixelSize: 10 + font.family: "monospace" + color: "#666666" + wrapMode: Text.WordWrap + } + } + + Item { Layout.fillHeight: true } + + // 按钮区域 + RowLayout { + Layout.fillWidth: true + spacing: 12 + + // 取消按钮 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 + color: cancelUninstallMouse.containsMouse ? "#f0f0f0" : "#ffffff" + radius: 8 + border.width: 1 + border.color: "#e0e0e0" + + Text { + anchors.centerIn: parent + text: qsTr("取消") + font.pixelSize: 13 + color: "#333333" + } + + MouseArea { + id: cancelUninstallMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: uninstallConfirmDialog.close() + } + } + + // 确认卸载按钮 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 36 + color: confirmUninstallMouse.containsMouse ? root.accentRed : Qt.rgba(220/255, 38/255, 38/255, 0.8) + radius: 8 + + Text { + anchors.centerIn: parent + text: qsTr("确认卸载") + font.pixelSize: 13 + font.weight: Font.Medium + color: "white" + } + + MouseArea { + id: confirmUninstallMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + // 复制卸载命令到剪贴板 + clipboardHelper.selectAll() + clipboardHelper.copy() + uninstallConfirmDialog.close() + // 显示状态提示,5 秒后自动隐藏 + uninstallStatus.visible = true + statusHideTimer.start() + } + } + } + } + } + } + } + + // 隐藏的 TextEdit 用于复制卸载命令到剪贴板 + // QML 没有直接剪贴板 API,用 TextEdit.selectAll()+copy() 实现 + TextEdit { + id: clipboardHelper + visible: false + text: "sudo rm -rf /usr/share/dde-shell/space.jokul.JNetApplet/ && systemctl --user restart dde-shell@DDE" + } + + // 卸载状态提示自动隐藏定时器 + Timer { + id: statusHideTimer + interval: 5000 + onTriggered: uninstallStatus.visible = false } // 点击处理 @@ -518,4 +987,19 @@ AppletItem { toolTip.close() } } + + // 右键点击:弹出上下文菜单 + TapHandler { + acceptedButtons: Qt.RightButton + gesturePolicy: TapHandler.ReleaseWithinBounds + + onTapped: { + // 关闭可能打开的 popup,避免视觉冲突 + if (networkPopup.popupVisible) networkPopup.close() + toolTip.close() + // 使用 MenuHelper 打开菜单(dde-shell dock 环境必需, + // 它会先关闭当前活动菜单再打开新菜单,保证同一时间只有一个菜单) + MenuHelper.openMenu(contextMenu) + } + } } \ No newline at end of file