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:
@@ -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 | 判定 | 依据 |
|
||||
|---|------|------|
|
||||
| H1–H3 | 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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user