Files
JNetApplet/docs/superpowers/plans/2026-07-18-ui-redesign.md
T
Jokul 009dd9578f feat: 重新设计插件 UI 样式与交互
- 任务栏图标:双行数值显示(下载蓝/上传绿),固定宽度避免抖动
- 左键弹出面板:360×320 居中布局,双列速度卡片,网卡名+IP 卡片背景
- 右键菜单:deepin 风格 Platform.Menu + MenuHelper(刷新/设置/关于)
- 设置窗口:frameless 自定义标题栏,网卡接口选择,卸载插件功能
- Tooltip:网卡名 + IP 地址单行展示
- C++ 后端:新增 ipAddress 属性(QNetworkInterface),优先选择物理网卡
- 速度格式化:最小单位 KB,2 位小数
- 移除 QML 侧多余 refresh 调用,修复 tooltip 数值变 0
- 新增设计文档与实现计划
2026-07-18 18:44:12 +08:00

32 KiB
Raw Blame History

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.jsonCMakeLists.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()accentGreenaccentGreenLightaccentOrangeaccentReddownloadValueColoruploadValueColor - 后续任务全部依赖

说明: 替换 root 的函数块和颜色属性块。保留所有 applet 绑定属性不变。

  • Step 1: 替换函数区(原 32-56 行)

// 格式化速度显示 注释到 formatTotal 函数结束替换为:

    // 紧凑格式化速度(用于任务栏图标和 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 属性结束替换为:

    // 任务栏与面板的颜色全部派生自 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()downloadValueColoruploadValueColorsecondaryTextdockSize

  • Produces: 任务栏双行数值显示

  • Step 1: 替换图标区域

// 图标区域 注释到对应 Rectangle 闭合 } 替换为:

    // 任务栏图标区:双行紧凑数值,移除原圆圈边框
    // 上行下载、下行上传,箭头为次级色、数值为强调色
    // 设计原因:圆圈边框在 ~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.qmlbuildToolTipText 函数)

Interfaces:

  • Consumes: formatSpeedShort()activeInterfaceready

说明: Tooltip 改用 formatSpeedShort 与任务栏一致。PanelToolTip.text 是纯文本,不支持富文本着色,箭头无法单独着色。

  • Step 1: 替换 buildToolTipText 函数体
    // 构建 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 未就绪文案为中文

确认 PanelToolTiptext 属性为:

    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.qmlPanelPopup 内部,原 173-489 行)

Interfaces:

  • Consumes: formatSpeedformatTotalprimaryTextsecondaryTexttertiaryTextcardBackgroundcardBorderaccentBlueaccentBlueLightaccentGreenaccentGreenLightdownloadValueColoruploadValueColorreadyactiveInterfacedownloadSpeeduploadSpeedtotalDownloadtotalUploadnetworkInterfacesapplet
  • Produces: 360×320 重构面板

说明: 尺寸从 400×350 改为 360×320。结构:标题栏(含副标题)+ 实时速度双列卡片 + 总量单行 + chip 接口切换 + 未就绪占位。

  • Step 1: 调整 PanelPopup 尺寸

PanelPopupwidth: 400 / height: 350 改为 width: 360 / height: 320。保留 popupX/popupY/onPopupVisibleChanged 不变。

  • Step 2: 替换 Control 及其 contentItem 全部内容

PanelPopup 内的 Control { id: popupContainer ... } 整体替换为:

        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.onCompletedDockPanelPositioner.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(新增 MenuDialog、右键 TapHandler,替换原左键 TapHandler

Interfaces:

  • Consumes: applet.refresh()applet.setActiveInterface()networkInterfacesactiveInterfaceprimaryTextsecondaryTextaccentBlue
  • Produces: 右键弹出 deepin 风格菜单

说明:PanelPopup 之后、原左键 TapHandler 之前,新增 MenuDialog。在原左键 TapHandler 之后新增右键 TapHandler

  • Step 1: 新增 contextMenu 和 aboutDialog

PanelPopup 闭合 } 之后,原 // 构建 tooltip 文本 注释之前,插入:

    // 右键菜单: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

在原左键 TapHandleracceptedButtons: Qt.LeftButton)之后,文件末尾的 root 闭合 } 之前,插入:

    // 右键点击:弹出上下文菜单
    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/onObjectRemovedcontextMenu.children[2] 是切换接口子菜单(菜单顺序:刷新 MenuItem / MenuSeparator / 切换接口 Menu / MenuSeparator / 关于 MenuItem)。若 QtQuick.Controls 版本不支持 insertItem/removeItem,可改用 MenuMenuItem 直接列出(无 Instantiator),通过 InstantiatoronObjectAdded 调用 parent.addItem(object) 或直接在子 Menu 内用 Repeater+MenuItem 写法。验证时如报错,将子菜单改为:

        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)