From c4246cd12281e898326ef4861c592a833aa6e5f2 Mon Sep 17 00:00:00 2001 From: Jokul Date: Wed, 22 Jul 2026 21:40:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20chip=E5=88=97=E8=A1=A8=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=E6=8E=92=E5=BA=8F+=E8=87=AA=E5=8A=A8=E6=BB=9A?= =?UTF-8?q?=E5=8A=A8=E5=88=B0=E9=80=89=E4=B8=AD=E7=BD=91=E5=8D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sortedInterfaces 不再将活动接口移到最前,改为物理网卡在前、虚拟网卡 在后各自按名称排序,切换网卡时列表顺序不再跳动 - 新增 scrollToActiveChip 函数,活动接口变化时自动滚动 Flickable 使选中 chip 完整可见,左侧截断对齐左缘、右侧截断对齐右缘 - 用 NumberAnimation(200ms OutCubic)驱动 contentX 平滑滚动,用户拖拽时 自动停止动画避免争抢 - popup 打开时用 Qt.callLater 直接定位活动 chip(无动画) --- package/networkview.qml | 68 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/package/networkview.qml b/package/networkview.qml index 7ce2be0..33bed8c 100644 --- a/package/networkview.qml +++ b/package/networkview.qml @@ -48,23 +48,21 @@ AppletItem { || name.startsWith("enp") || name.startsWith("eth") } - // 排序后的接口列表:活动接口最前,其次物理网卡,最后虚拟网卡 - // 设计原因:用户希望启动监测的网卡在最左边,方便查看和切换 + // 排序后的接口列表:物理网卡在前,虚拟网卡在后,各自按名称排序 + // 设计原因:保持稳定排序,活动接口不再移到最前,避免切换网卡时 chip 顺序跳动; + // 活动 chip 若被截断,由 chipFlickable 自动滚动露出完整样式 readonly property var sortedInterfaces: { if (!root.ready || root.networkInterfaces.length === 0) return [] var physical = [] var virtual = [] for (var i = 0; i < root.networkInterfaces.length; i++) { var name = root.networkInterfaces[i] - if (name === root.activeInterface) continue if (root.isPhysicalIf(name)) physical.push(name) else virtual.push(name) } physical.sort() virtual.sort() - var result = [] - if (root.activeInterface) result.push(root.activeInterface) - return result.concat(physical).concat(virtual) + return physical.concat(virtual) } // 紧凑格式化速度(用于任务栏图标和 tooltip,不带单位后缀,节省空间) @@ -316,6 +314,9 @@ AppletItem { // C++ 后端 m_refreshTimer 持续更新,popup 通过属性绑定自动显示最新数据。 if (popupVisible) { toolTip.close() + // popup 打开时确保活动 chip 完整可见:直接定位不播放动画,避免看到打开瞬间的滚动; + // Qt.callLater 等待首次打开时 Repeater 完成实例化与布局后再计算位置 + Qt.callLater(function() { chipFlickable.scrollToActiveChip(false) }) } } @@ -543,8 +544,9 @@ AppletItem { } } - // 接口切换 chip 列表:水平滚动,活动接口在最左,物理网卡次之,虚拟网卡在后 + // 接口切换 chip 列表:水平滚动,物理网卡在前、虚拟网卡在后,顺序稳定不随切换变化 // chip 少时分散撑满一行;chip 多超出宽度时固定宽度 + 水平滚动 + // 活动 chip 不在可视区域或被截断时,自动滚动露出完整样式(见 scrollToActiveChip) Flickable { id: chipFlickable Layout.fillWidth: true @@ -557,6 +559,57 @@ AppletItem { flickableDirection: Flickable.HorizontalFlick clip: true + // 平滑滚动动画:直接赋值 contentX 会被 Flickable 的拖拽/回弹行为覆盖, + // 用 NumberAnimation 驱动 contentX 实现自动滚动,平滑且不冲突 + NumberAnimation { + id: chipScrollAnim + target: chipFlickable + property: "contentX" + duration: 200 + easing.type: Easing.OutCubic + } + + // 用户开始拖拽/滑动时停止自动滚动,避免动画与用户操作争抢 + onMovementStarted: chipScrollAnim.stop() + + // 自动滚动使活动接口的 chip 完整可见: + // 列表顺序稳定后活动 chip 可能位于可视区域外或被截断, + // 切换接口或 popup 打开时调用,将其滚动露出完整样式。 + // animated=false 用于 popup 刚打开时立即定位,跳过滚动过程 + function scrollToActiveChip(animated) { + if (!visible || width <= 0) return + var idx = root.sortedInterfaces.indexOf(root.activeInterface) + if (idx < 0) return + var chip = chipRepeater.itemAt(idx) + if (!chip) return + var targetX = contentX + if (chip.x < contentX) { + // chip 在可视区左侧之外:回滚使 chip 左边缘与可视区左缘对齐 + targetX = chip.x + } else if (chip.x + chip.width > contentX + width) { + // chip 在可视区右侧之外或被截断:滚动使 chip 右边缘与可视区右缘对齐 + targetX = chip.x + chip.width - width + } + // 钳制到合法滚动范围 [0, contentWidth - width],避免触发边界回弹 + targetX = Math.max(0, Math.min(targetX, Math.max(0, contentWidth - width))) + if (Math.abs(targetX - contentX) < 1) return + chipScrollAnim.stop() + if (animated) { + chipScrollAnim.to = targetX + chipScrollAnim.start() + } else { + contentX = targetX + } + } + + // 活动接口变化时(用户点击 chip 或后端自动切换)自动滚动露出活动 chip + Connections { + target: root + function onActiveInterfaceChanged() { + chipFlickable.scrollToActiveChip(true) + } + } + // 等宽分配宽度:假设所有 chip 等宽撑满时的单个宽度 readonly property real equalChipWidth: root.sortedInterfaces.length > 0 ? (width - 6 * (root.sortedInterfaces.length - 1)) / root.sortedInterfaces.length @@ -567,6 +620,7 @@ AppletItem { spacing: 6 Repeater { + id: chipRepeater model: root.sortedInterfaces Rectangle {