fix(watch): polish paused slot UI and header action spacing

Replace the large dashed pause panel with dimmed rings and a resume CTA,
and add gap between the monitoring switch and remove button.
This commit is contained in:
Nexus Agent
2026-06-14 03:34:17 +08:00
parent 3faa7a4f1c
commit c91cb1cd9e
4 changed files with 164 additions and 45 deletions
@@ -0,0 +1,30 @@
# 审计 — 监测槽暂停态 UI 与顶栏间距
**Changelog**: `docs/changelog/2026-06-14-watch-slot-paused-ui-polish.md`
## 审计范围
| 文件 | 变更 | 状态 |
|------|------|------|
| `WatchSlotCard.vue` | 暂停横幅/圆环占位/恢复按钮;顶栏操作区间距 | ☑ |
| `WatchMetricRing.vue` | `dimmed` 占位态 | ☑ |
## Step 3 规则扫描
| H | 规则 | 结论 |
|---|------|------|
| H1 | 无新 API | SAFE — 仍 `emit('monitoring'/'remove')` |
| H2 | 误触移除 | SAFE — 开关与 ✕ 间距加大 |
| H3 | 无障碍 | SAFE — `aria-label` on 开关与移除 |
## Closure
| H | 判定 | 依据 |
|---|------|------|
| H1H3 | SAFE | 纯前端 |
## DoD
- [x] changelog ≥10 行
- [x] `vite build`
- [ ] 生产终验:暂停态无大虚线框;开启后开关与 ✕ 不贴在一起
@@ -0,0 +1,27 @@
# 2026-06-14 — 监测槽暂停态 UI 优化
## 摘要
关闭实时监测后,去掉大号居中开关与虚线框;改为顶栏统一开关、浅色提示条、半透明圆环占位与「恢复实时监测」按钮,与开启态卡片结构一致。
## 动机
用户反馈暂停态「开启实时监测 + 槽位保留…」居中区域观感差,与开启后圆环布局割裂。
## 变更
- `WatchSlotCard.vue`:暂停横幅 + 四圆环占位 + 恢复按钮;底栏文案精简
- `WatchMetricRing.vue`:新增 `dimmed` 占位态(灰色细环、不可交互)
## 涉及文件
- `frontend/src/components/watch/WatchSlotCard.vue`
- `frontend/src/components/watch/WatchMetricRing.vue`
## 迁移 / 重启
仅前端构建。
## 验证
`cd frontend && npx vite build`;开启监测后顶栏开关与 ✕ 间距适中、不易误触。
@@ -10,27 +10,40 @@ const props = withDefaults(defineProps<{
center?: string
hint?: string
size?: number
/** 暂停态占位:灰色细环、不可交互 */
dimmed?: boolean
}>(), {
size: 68,
dimmed: false,
})
const ringWidth = computed(() => Math.max(5, Math.round(props.size * 0.1)))
const ringColor = computed(() => metricRingColor(props.accent, props.value))
const ringWidth = computed(() => Math.max(4, Math.round(props.size * (props.dimmed ? 0.08 : 0.1))))
const ringColor = computed(() => {
if (props.dimmed) return 'rgba(var(--v-theme-on-surface), 0.22)'
return metricRingColor(props.accent, props.value)
})
const centerText = computed(() => {
if (props.dimmed) return '—'
if (props.center != null && props.center !== '') return props.center
return props.value != null ? `${Math.round(props.value)}%` : '—'
})
const ringValue = computed(() => {
if (props.dimmed) return 0
if (props.value == null || Number.isNaN(props.value)) return 0
return Math.max(0, Math.min(100, props.value))
})
const showTooltip = computed(() => !props.dimmed && Boolean(props.hint))
const centerFontPx = computed(() => `${Math.max(11, Math.round(props.size * 0.2))}px`)
</script>
<template>
<v-tooltip :text="hint" location="top" :disabled="!hint">
<v-tooltip :text="hint" location="top" :disabled="!showTooltip">
<template #activator="{ props: tipProps }">
<div class="watch-metric-ring text-center" v-bind="tipProps">
<div
class="watch-metric-ring text-center"
:class="{ 'watch-metric-ring--dimmed': dimmed }"
v-bind="tipProps"
>
<v-progress-circular
:model-value="ringValue"
:size="size"
@@ -67,4 +80,13 @@ const centerFontPx = computed(() => `${Math.max(11, Math.round(props.size * 0.2)
.watch-metric-ring__label {
letter-spacing: 0.02em;
}
.watch-metric-ring--dimmed .watch-metric-ring__label {
opacity: 0.55;
}
.watch-metric-ring--dimmed .watch-metric-ring__value {
color: rgba(var(--v-theme-on-surface), 0.38) !important;
font-weight: 600;
}
</style>
+81 -41
View File
@@ -77,24 +77,33 @@ const showSwap = computed(() => (props.slot.metrics?.swap_total_gb ?? 0) > 0)
@click="monitoringOn ? emit('history', slot) : undefined"
@keydown.enter.prevent="monitoringOn ? emit('history', slot) : undefined"
>
<div class="d-flex align-center mb-2" @click.stop>
<div class="text-subtitle-2 text-truncate flex-grow-1" :title="slot.server_name">
<div class="watch-slot-header d-flex align-center mb-2" @click.stop>
<div class="text-subtitle-2 text-truncate flex-grow-1 min-width-0" :title="slot.server_name">
{{ slot.server_name || `ID ${slot.server_id}` }}
</div>
<v-chip size="x-small" variant="tonal" color="info" label>
{{ slot.slot_index + 1 }}
</v-chip>
<v-switch
v-if="monitoringOn"
:model-value="true"
density="compact"
hide-details
color="primary"
class="watch-slot-switch ml-1"
@click.stop
@update:model-value="emit('monitoring', slot.slot_index, $event ?? false)"
/>
<v-btn icon="mdi-close" size="x-small" variant="text" @click="emit('remove', slot.slot_index)" />
<div class="watch-slot-header-actions d-flex align-center flex-shrink-0">
<v-chip size="x-small" variant="tonal" :color="monitoringOn ? 'info' : 'warning'" label>
{{ slot.slot_index + 1 }}
</v-chip>
<v-switch
:model-value="monitoringOn"
density="compact"
hide-details
color="primary"
class="watch-slot-switch"
:aria-label="monitoringOn ? '关闭实时监测' : '开启实时监测'"
@click.stop
@update:model-value="emit('monitoring', slot.slot_index, $event ?? false)"
/>
<v-btn
icon="mdi-close"
size="x-small"
variant="text"
class="watch-slot-remove-btn"
aria-label="移除此槽"
@click="emit('remove', slot.slot_index)"
/>
</div>
</div>
<div class="watch-ttl-row mb-2" @click.stop>
@@ -117,19 +126,36 @@ const showSwap = computed(() => (props.slot.metrics?.swap_total_gb ?? 0) > 0)
<div
v-if="!monitoringOn"
class="watch-slot-paused-center flex-grow-1"
class="watch-slot-paused-body flex-grow-1"
@click.stop
>
<v-switch
:model-value="false"
hide-details
color="primary"
class="watch-slot-paused-switch"
@update:model-value="emit('monitoring', slot.slot_index, $event ?? false)"
/>
<div class="text-body-1 font-weight-medium mt-2">开启实时监测</div>
<div class="text-caption text-medium-emphasis mt-1 text-center px-3">
槽位保留 · 关闭期间 Agent 60s 智能上报
<div class="watch-slot-paused-banner d-flex align-center ga-2 mb-3">
<v-icon icon="mdi-radar-off" size="18" class="text-medium-emphasis" />
<span class="text-body-2 font-weight-medium">实时监测已暂停</span>
<v-spacer />
<v-chip size="x-small" variant="tonal" color="warning" label>暂停</v-chip>
</div>
<div class="watch-metric-rings watch-metric-rings--paused d-flex justify-space-between mb-3">
<WatchMetricRing label="CPU" accent="cpu" :size="56" dimmed />
<WatchMetricRing label="内存" accent="mem" :size="56" dimmed />
<WatchMetricRing label="硬盘" accent="disk" :size="56" dimmed />
<WatchMetricRing label="负载" accent="load" :size="56" dimmed />
</div>
<div class="watch-slot-paused-cta text-center">
<v-btn
size="small"
variant="tonal"
color="primary"
prepend-icon="mdi-play-circle-outline"
@click="emit('monitoring', slot.slot_index, true)"
>
恢复实时监测
</v-btn>
<div class="text-caption text-medium-emphasis mt-2 px-2">
槽位与子机绑定保留 · 关闭期间由 Agent 60 秒上报
</div>
</div>
</div>
@@ -259,11 +285,11 @@ const showSwap = computed(() => (props.slot.metrics?.swap_total_gb ?? 0) > 0)
<div
v-if="!monitoringOn"
class="d-flex align-center justify-space-between mt-2"
class="d-flex align-center justify-space-between mt-auto pt-2"
@click.stop
>
<span class="text-caption text-medium-emphasis">
已暂停 · {{ watchTtlShortLabel(slotTtlMinutes) }} · Agent 60s 上报
{{ watchTtlShortLabel(slotTtlMinutes) }} · 槽位保留
</span>
<v-btn size="x-small" variant="text" @click.stop="emit('history', slot)">历史</v-btn>
</div>
@@ -320,31 +346,45 @@ const showSwap = computed(() => (props.slot.metrics?.swap_total_gb ?? 0) > 0)
border-color: rgb(var(--v-theme-error)) !important;
}
.watch-slot-card--paused {
opacity: 1;
background: rgba(var(--v-theme-surface-variant), 0.12);
cursor: default;
}
.watch-slot-card--paused:hover {
border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important;
}
.watch-slot-paused-center {
.watch-slot-paused-body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1 1 auto;
min-height: 200px;
margin: 8px 0 4px;
border-radius: 12px;
background: rgba(var(--v-theme-surface-variant), 0.2);
border: 1px dashed rgba(var(--v-border-color), var(--v-border-opacity));
min-height: 0;
padding-top: 4px;
}
.watch-slot-paused-switch {
transform: scale(1.35);
.watch-slot-paused-banner {
padding: 6px 10px;
border-radius: 10px;
background: rgba(var(--v-theme-warning), 0.08);
border: 1px solid rgba(var(--v-theme-warning), 0.18);
}
.watch-slot-paused-cta {
margin-top: auto;
padding-bottom: 4px;
}
.watch-metric-rings--paused {
opacity: 0.92;
}
.watch-slot-header-actions {
gap: 6px;
margin-left: 8px;
}
.watch-slot-switch {
flex: 0 0 auto;
transform: scale(0.85);
margin-right: 4px;
}
.watch-slot-switch :deep(.v-selection-control) {
min-height: 32px;
}
.watch-slot-remove-btn {
margin-left: 2px;
}
.watch-ttl-row {
display: flex;