Feature/ui redesign #1
+2
-1
@@ -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
|
||||
)
|
||||
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/bin/bash
|
||||
|
||||
# JNetApplet deb 一键打包脚本
|
||||
# 用法:bash build-deb.sh
|
||||
# 产物:jnetapplet_<version>_<arch>.deb
|
||||
|
||||
set -e
|
||||
|
||||
# 项目根目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# 从 CMakeLists.txt 读取版本号
|
||||
VERSION=$(grep -oP 'project\([^)]*VERSION\s+\K[0-9]+\.[0-9]+\.[0-9]+' CMakeLists.txt)
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "错误:无法从 CMakeLists.txt 读取版本号"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 架构判断
|
||||
ARCH=$(dpkg --print-architecture)
|
||||
if [ -z "$ARCH" ]; then
|
||||
ARCH="amd64"
|
||||
fi
|
||||
|
||||
PKG_NAME="jnetapplet"
|
||||
DEB_FILE="${PKG_NAME}_${VERSION}_${ARCH}.deb"
|
||||
|
||||
echo "=========================================="
|
||||
echo " JNetApplet deb 打包"
|
||||
echo " 版本: $VERSION"
|
||||
echo " 架构: $ARCH"
|
||||
echo " 产物: $DEB_FILE"
|
||||
echo "=========================================="
|
||||
|
||||
# 检查依赖工具
|
||||
for cmd in cmake dpkg-deb; do
|
||||
if ! command -v "$cmd" &> /dev/null; then
|
||||
echo "错误:缺少 $cmd,请先安装"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# 清理并创建构建目录
|
||||
echo "[1/5] 构建 CMake 项目..."
|
||||
rm -rf build
|
||||
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release 2>&1 | tail -3
|
||||
cmake --build build -j"$(nproc)" 2>&1 | tail -3
|
||||
|
||||
# 创建 staging 目录(模拟安装根目录)
|
||||
STAGE_DIR="$(mktemp -d)"
|
||||
trap "rm -rf '$STAGE_DIR'" EXIT
|
||||
|
||||
echo "[2/5] 安装到 staging 目录..."
|
||||
# DESTDIR 指定安装根目录前缀,cmake 会将 /usr/... 安装到 $STAGE_DIR/usr/...
|
||||
DESTDIR="$STAGE_DIR" cmake --install build 2>&1 | tail -3
|
||||
|
||||
# 生成 DEBIAN/control
|
||||
echo "[3/5] 生成 control 文件..."
|
||||
mkdir -p "$STAGE_DIR/DEBIAN"
|
||||
|
||||
# 计算安装后大小(KB)
|
||||
INSTALLED_SIZE=$(du -sk "$STAGE_DIR/usr" | cut -f1)
|
||||
|
||||
cat > "$STAGE_DIR/DEBIAN/control" << EOF
|
||||
Package: ${PKG_NAME}
|
||||
Version: ${VERSION}
|
||||
Architecture: ${ARCH}
|
||||
Maintainer: Jokul <jokul@git.jokul.space>
|
||||
Installed-Size: ${INSTALLED_SIZE}
|
||||
Depends: dde-shell, libc6, libqt6core6, libqt6gui6, libqt6quick6, libqt6network6, libdtk6core
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Description: 网络速度监控 dde-shell 任务栏插件
|
||||
实时监控网络下载/上传速度,支持多网卡切换、IP 地址显示、
|
||||
总流量统计。适用于 deepin 桌面环境的 dde-shell 任务栏。
|
||||
Homepage: https://git.jokul.space/Jokul/JNetApplet
|
||||
EOF
|
||||
|
||||
# 生成 postinst 脚本:安装后重启桌面用户的 dde-shell
|
||||
# 注意:dpkg 以 root 运行此脚本,systemctl --user 需切换到桌面用户身份
|
||||
cat > "$STAGE_DIR/DEBIAN/postinst" << 'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
# 找到活跃桌面用户(UID >= 1000),以其身份重启 dde-shell 用户服务
|
||||
for user in $(who | awk '{print $1}' | sort -u); do
|
||||
[ "$user" = "root" ] && continue
|
||||
uid=$(id -u "$user" 2>/dev/null) || continue
|
||||
[ "$uid" -lt 1000 ] && continue
|
||||
su "$user" -c "systemctl --user restart dde-shell@DDE" 2>/dev/null || true
|
||||
done
|
||||
exit 0
|
||||
EOF
|
||||
chmod 755 "$STAGE_DIR/DEBIAN/postinst"
|
||||
|
||||
# 生成 prerm 脚本:卸载前重启桌面用户的 dde-shell 移除插件
|
||||
cat > "$STAGE_DIR/DEBIAN/prerm" << 'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
# 以桌面用户身份重启 dde-shell 以卸载插件
|
||||
for user in $(who | awk '{print $1}' | sort -u); do
|
||||
[ "$user" = "root" ] && continue
|
||||
uid=$(id -u "$user" 2>/dev/null) || continue
|
||||
[ "$uid" -lt 1000 ] && continue
|
||||
su "$user" -c "systemctl --user restart dde-shell@DDE" 2>/dev/null || true
|
||||
done
|
||||
exit 0
|
||||
EOF
|
||||
chmod 755 "$STAGE_DIR/DEBIAN/prerm"
|
||||
|
||||
# 修正文件权限(so 文件需要 755,qml/json 需要 644)
|
||||
echo "[4/5] 修正文件权限..."
|
||||
find "$STAGE_DIR/usr" -type f -name "*.so" -exec chmod 755 {} \;
|
||||
find "$STAGE_DIR/usr" -type f \( -name "*.qml" -o -name "*.json" \) -exec chmod 644 {} \;
|
||||
|
||||
# 构建 deb
|
||||
echo "[5/5] 构建 deb 包..."
|
||||
rm -f "$DEB_FILE"
|
||||
dpkg-deb --build --root-owner-group "$STAGE_DIR" "$DEB_FILE"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 打包完成!"
|
||||
echo " 产物: $DEB_FILE ($(du -h "$DEB_FILE" | cut -f1))"
|
||||
echo ""
|
||||
echo " 安装: sudo dpkg -i $DEB_FILE"
|
||||
echo " 卸载: sudo dpkg -r $PKG_NAME"
|
||||
echo "=========================================="
|
||||
@@ -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)`。
|
||||
@@ -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 时切换项禁用
|
||||
- [ ] 右键菜单:关于对话框显示版本信息
|
||||
@@ -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,7 +176,18 @@ 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) {
|
||||
@@ -175,6 +195,8 @@ void NetworkMonitorApplet::readNetworkStats()
|
||||
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
|
||||
|
||||
@@ -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<QString, NetworkInterface> m_interfaces;
|
||||
QStringList m_interfaceList;
|
||||
QString m_activeInterface;
|
||||
QString m_ipAddress; // 活动接口的 IPv4 地址
|
||||
|
||||
// 速度计算
|
||||
qint64 m_lastRxBytes;
|
||||
|
||||
+912
-278
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user