diff --git a/docs/changelog/2026-06-11-watch-slot-click-fix.md b/docs/changelog/2026-06-11-watch-slot-click-fix.md new file mode 100644 index 00000000..e20c7bc7 --- /dev/null +++ b/docs/changelog/2026-06-11-watch-slot-click-fix.md @@ -0,0 +1,30 @@ +# 2026-06-11 监测槽点击交互修复 + +## 摘要 + +修复服务器页实时监测槽「点击无反应」:空槽可点击选机、已填槽点击进历史;列表「+」已在监测时给出提示。 + +## 动机 + +设计文档要求空槽显示「+ 添加监测」并可点击选机,实现中未绑定事件;已填槽仅小字「历史」可点,整体卡片无交互。 + +## 涉及文件 + +- `frontend/src/components/watch/WatchSlotCard.vue` — 空/满槽点击、hover、文案 +- `frontend/src/components/watch/WatchSlotPickDialog.vue` — 选机对话框(新增) +- `frontend/src/components/watch/WatchSlotRow.vue` — 串联选机与 pin API +- `frontend/src/composables/useWatchPins.ts` — `pinServer` 支持 `slot_index` +- `frontend/src/components/watch/WatchSparkline.vue` — `pointer-events: none` 避免图表挡点击 +- `frontend/src/pages/ServersPage.vue` — 已在监测时 snackbar 提示 + +## 迁移 / 重启 + +无。前端构建部署即可。 + +## 验证 + +```bash +cd frontend && npm run type-check +``` + +浏览器 `#/servers`:点击虚线空槽 → 搜索选服务器 → 加入;点击有数据的槽卡片 → 跳转监测历史;列表对已监测服务器点 + → 提示槽位号。 diff --git a/frontend/src/components/watch/WatchSlotCard.vue b/frontend/src/components/watch/WatchSlotCard.vue index 10044e02..c8c18131 100644 --- a/frontend/src/components/watch/WatchSlotCard.vue +++ b/frontend/src/components/watch/WatchSlotCard.vue @@ -12,6 +12,7 @@ const emit = defineEmits<{ remove: [slotIndex: number] processes: [slot: WatchSlot] history: [slot: WatchSlot] + pick: [slotIndex: number] }>() const chartMetric = ref<'cpu_pct' | 'mem_pct' | 'disk_pct'>('cpu_pct') @@ -27,10 +28,14 @@ function pct(v: number | null | undefined) { elevation="0" border rounded="lg" - class="watch-slot-card pa-3" + class="watch-slot-card pa-3 watch-slot-card--filled" :class="{ 'watch-slot-card--error': slot.metrics?.probe_status && slot.metrics.probe_status !== 'ok' }" + role="button" + tabindex="0" + @click="emit('history', slot)" + @keydown.enter.prevent="emit('history', slot)" > -
+
{{ slot.server_name || `ID ${slot.server_id}` }}
@@ -40,6 +45,7 @@ function pct(v: number | null | undefined) {
+
剩余 {{ formatRemaining(slot.remaining_sec) }}
- 进程 - 历史 + 进程 + 历史
+
mdi-plus-circle-outline -
槽 {{ slot.slot_index + 1 }} · 空
+
槽 {{ slot.slot_index + 1 }} · 添加监测
@@ -109,6 +120,17 @@ function pct(v: number | null | undefined) { .watch-slot-card--empty { min-height: 220px; border-style: dashed !important; + cursor: pointer; + transition: background-color 0.15s ease; +} +.watch-slot-card--empty:hover { + background: rgba(var(--v-theme-primary), 0.04); +} +.watch-slot-card--filled { + cursor: pointer; +} +.watch-slot-card--filled:hover { + background: rgba(var(--v-theme-surface-variant), 0.3); } .watch-slot-card--error { border-color: rgb(var(--v-theme-error)) !important; diff --git a/frontend/src/components/watch/WatchSlotPickDialog.vue b/frontend/src/components/watch/WatchSlotPickDialog.vue new file mode 100644 index 00000000..4dfdf6e6 --- /dev/null +++ b/frontend/src/components/watch/WatchSlotPickDialog.vue @@ -0,0 +1,108 @@ + + + diff --git a/frontend/src/components/watch/WatchSlotRow.vue b/frontend/src/components/watch/WatchSlotRow.vue index 3c5c3838..fd3ff2c4 100644 --- a/frontend/src/components/watch/WatchSlotRow.vue +++ b/frontend/src/components/watch/WatchSlotRow.vue @@ -1,16 +1,21 @@ diff --git a/frontend/src/components/watch/WatchSparkline.vue b/frontend/src/components/watch/WatchSparkline.vue index d625c97e..499eaa72 100644 --- a/frontend/src/components/watch/WatchSparkline.vue +++ b/frontend/src/components/watch/WatchSparkline.vue @@ -52,5 +52,6 @@ const chartOption = computed(() => { diff --git a/frontend/src/composables/useWatchPins.ts b/frontend/src/composables/useWatchPins.ts index 2b5249df..d64189bb 100644 --- a/frontend/src/composables/useWatchPins.ts +++ b/frontend/src/composables/useWatchPins.ts @@ -157,10 +157,15 @@ async function refreshPins() { } } -async function pinServer(serverId: number, replaceSlot?: number) { +async function pinServer( + serverId: number, + opts?: number | { replaceSlot?: number; slotIndex?: number }, +) { + const options = typeof opts === 'number' ? { replaceSlot: opts } : opts try { const body: Record = { server_id: serverId } - if (replaceSlot != null) body.replace_slot = replaceSlot + if (options?.replaceSlot != null) body.replace_slot = options.replaceSlot + if (options?.slotIndex != null) body.slot_index = options.slotIndex const data = await http.post<{ slots: WatchSlot[] }>('/watch/pins', body) applySlots(data) return { ok: true as const } diff --git a/frontend/src/pages/ServersPage.vue b/frontend/src/pages/ServersPage.vue index da815006..96759edf 100644 --- a/frontend/src/pages/ServersPage.vue +++ b/frontend/src/pages/ServersPage.vue @@ -809,7 +809,11 @@ const showReplacePin = ref(false) const replacePinServerId = ref(null) async function onPinServer(item: ServerApiItem) { - if (pinnedMap.value[item.id] != null) return + const existingSlot = pinnedMap.value[item.id] + if (existingSlot != null) { + snackbar(`已在监测 · 槽 ${existingSlot + 1}`, 'info') + return + } pinLoadingId.value = item.id try { const res = await doPinServer(item.id)