feat: chip列表稳定排序+自动滚动到选中网卡

- sortedInterfaces 不再将活动接口移到最前,改为物理网卡在前、虚拟网卡
  在后各自按名称排序,切换网卡时列表顺序不再跳动
- 新增 scrollToActiveChip 函数,活动接口变化时自动滚动 Flickable 使选中
  chip 完整可见,左侧截断对齐左缘、右侧截断对齐右缘
- 用 NumberAnimation(200ms OutCubic)驱动 contentX 平滑滚动,用户拖拽时
  自动停止动画避免争抢
- popup 打开时用 Qt.callLater 直接定位活动 chip(无动画)
This commit is contained in:
2026-07-22 21:40:20 +08:00
parent 539442ef48
commit c4246cd122
+61 -7
View File
@@ -48,23 +48,21 @@ AppletItem {
|| name.startsWith("enp") || name.startsWith("eth") || name.startsWith("enp") || name.startsWith("eth")
} }
// 排序后的接口列表:活动接口最前,其次物理网卡,最后虚拟网卡 // 排序后的接口列表:物理网卡在前,虚拟网卡在后,各自按名称排序
// 设计原因:用户希望启动监测的网卡在最左边,方便查看和切换 // 设计原因:保持稳定排序,活动接口不再移到最前,避免切换网卡时 chip 顺序跳动;
// 活动 chip 若被截断,由 chipFlickable 自动滚动露出完整样式
readonly property var sortedInterfaces: { readonly property var sortedInterfaces: {
if (!root.ready || root.networkInterfaces.length === 0) return [] if (!root.ready || root.networkInterfaces.length === 0) return []
var physical = [] var physical = []
var virtual = [] var virtual = []
for (var i = 0; i < root.networkInterfaces.length; i++) { for (var i = 0; i < root.networkInterfaces.length; i++) {
var name = root.networkInterfaces[i] var name = root.networkInterfaces[i]
if (name === root.activeInterface) continue
if (root.isPhysicalIf(name)) physical.push(name) if (root.isPhysicalIf(name)) physical.push(name)
else virtual.push(name) else virtual.push(name)
} }
physical.sort() physical.sort()
virtual.sort() virtual.sort()
var result = [] return physical.concat(virtual)
if (root.activeInterface) result.push(root.activeInterface)
return result.concat(physical).concat(virtual)
} }
// 紧凑格式化速度(用于任务栏图标和 tooltip,不带单位后缀,节省空间) // 紧凑格式化速度(用于任务栏图标和 tooltip,不带单位后缀,节省空间)
@@ -316,6 +314,9 @@ AppletItem {
// C++ 后端 m_refreshTimer 持续更新,popup 通过属性绑定自动显示最新数据。 // C++ 后端 m_refreshTimer 持续更新,popup 通过属性绑定自动显示最新数据。
if (popupVisible) { if (popupVisible) {
toolTip.close() toolTip.close()
// popup 打开时确保活动 chip 完整可见:直接定位不播放动画,避免看到打开瞬间的滚动;
// Qt.callLater 等待首次打开时 Repeater 完成实例化与布局后再计算位置
Qt.callLater(function() { chipFlickable.scrollToActiveChip(false) })
} }
} }
@@ -543,8 +544,9 @@ AppletItem {
} }
} }
// 接口切换 chip 列表:水平滚动,活动接口在最左,物理网卡次之,虚拟网卡在后 // 接口切换 chip 列表:水平滚动,物理网卡在前、虚拟网卡在后,顺序稳定不随切换变化
// chip 少时分散撑满一行;chip 多超出宽度时固定宽度 + 水平滚动 // chip 少时分散撑满一行;chip 多超出宽度时固定宽度 + 水平滚动
// 活动 chip 不在可视区域或被截断时,自动滚动露出完整样式(见 scrollToActiveChip
Flickable { Flickable {
id: chipFlickable id: chipFlickable
Layout.fillWidth: true Layout.fillWidth: true
@@ -557,6 +559,57 @@ AppletItem {
flickableDirection: Flickable.HorizontalFlick flickableDirection: Flickable.HorizontalFlick
clip: true 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 等宽撑满时的单个宽度 // 等宽分配宽度:假设所有 chip 等宽撑满时的单个宽度
readonly property real equalChipWidth: root.sortedInterfaces.length > 0 readonly property real equalChipWidth: root.sortedInterfaces.length > 0
? (width - 6 * (root.sortedInterfaces.length - 1)) / root.sortedInterfaces.length ? (width - 6 * (root.sortedInterfaces.length - 1)) / root.sortedInterfaces.length
@@ -567,6 +620,7 @@ AppletItem {
spacing: 6 spacing: 6
Repeater { Repeater {
id: chipRepeater
model: root.sortedInterfaces model: root.sortedInterfaces
Rectangle { Rectangle {