feat(frontend): polish cluster usage bars, task times and sidebar default
- cluster list: rebuild CPU usage bars with progressbar semantics, NaN/out-of-range clamping and three-tier tone colors for cluster and node tables - task center: show task creation time next to runner with full timestamp on hover and invalid-date fallbacks - sidebar: collapse portal group by default
This commit is contained in:
@@ -100,7 +100,7 @@ import { taskLogApi } from '@/api/taskLog'
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const businessLineStore = useBusinessLineStore()
|
const businessLineStore = useBusinessLineStore()
|
||||||
const portalExpanded = ref(true)
|
const portalExpanded = ref(false)
|
||||||
const isPlatformAdmin = computed(() => authStore.isAdmin)
|
const isPlatformAdmin = computed(() => authStore.isAdmin)
|
||||||
const isBusinessLineAdmin = computed(() => businessLineStore.isCurrentAdmin)
|
const isBusinessLineAdmin = computed(() => businessLineStore.isCurrentAdmin)
|
||||||
const sidebarBadges = ref({
|
const sidebarBadges = ref({
|
||||||
|
|||||||
@@ -39,7 +39,22 @@
|
|||||||
<td :class="['status-text', cluster.statusClass]">● {{ cluster.status }}</td>
|
<td :class="['status-text', cluster.statusClass]">● {{ cluster.status }}</td>
|
||||||
<td class="mono">{{ cluster.nodes }}</td>
|
<td class="mono">{{ cluster.nodes }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="bar-wrap"><span class="bar-fill" :class="cluster.cpuClass" :style="{ width: cluster.cpu + '%' }"></span></span>{{ cluster.cpu }}%
|
<div class="usage-cell">
|
||||||
|
<span
|
||||||
|
class="usage-track"
|
||||||
|
role="progressbar"
|
||||||
|
aria-label="CPU 使用率"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
:aria-valuenow="normalizeUsage(cluster.cpu)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="['usage-fill', usageTone(cluster.cpu)]"
|
||||||
|
:style="{ width: `${normalizeUsage(cluster.cpu)}%` }"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
<span class="usage-value mono">{{ normalizeUsage(cluster.cpu) }}%</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="mono">{{ cluster.version }}</td>
|
<td class="mono">{{ cluster.version }}</td>
|
||||||
<td class="mono">{{ cluster.calico }}</td>
|
<td class="mono">{{ cluster.calico }}</td>
|
||||||
@@ -80,7 +95,22 @@
|
|||||||
<td class="mono">{{ node.spec }}</td>
|
<td class="mono">{{ node.spec }}</td>
|
||||||
<td>
|
<td>
|
||||||
<template v-if="node.cpu > 0">
|
<template v-if="node.cpu > 0">
|
||||||
<span class="bar-wrap"><span class="bar-fill" :class="node.cpuClass" :style="{ width: node.cpu + '%' }"></span></span>{{ node.cpu }}%
|
<div class="usage-cell">
|
||||||
|
<span
|
||||||
|
class="usage-track"
|
||||||
|
role="progressbar"
|
||||||
|
aria-label="CPU 使用率"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
:aria-valuenow="normalizeUsage(node.cpu)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="['usage-fill', usageTone(node.cpu)]"
|
||||||
|
:style="{ width: `${normalizeUsage(node.cpu)}%` }"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
<span class="usage-value mono">{{ normalizeUsage(node.cpu) }}%</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<span v-else class="text-dim">-</span>
|
<span v-else class="text-dim">-</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -112,6 +142,18 @@ const businessLineNodes = computed(() => {
|
|||||||
return nodes.value.filter((node) => node.cluster === selectedCluster.value)
|
return nodes.value.filter((node) => node.cluster === selectedCluster.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function normalizeUsage(value: number) {
|
||||||
|
if (!Number.isFinite(value)) return 0
|
||||||
|
return Math.min(100, Math.max(0, Math.round(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function usageTone(value: number) {
|
||||||
|
const normalized = normalizeUsage(value)
|
||||||
|
if (normalized >= 90) return 'critical'
|
||||||
|
if (normalized >= 70) return 'warning'
|
||||||
|
return 'normal'
|
||||||
|
}
|
||||||
|
|
||||||
async function loadClusters() {
|
async function loadClusters() {
|
||||||
const businessLineId = businessLineStore.current?.id
|
const businessLineId = businessLineStore.current?.id
|
||||||
if (!businessLineId) {
|
if (!businessLineId) {
|
||||||
@@ -168,4 +210,50 @@ onMounted(loadClusters)
|
|||||||
.tag.biz {
|
.tag.biz {
|
||||||
color: var(--text-mid);
|
color: var(--text-mid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usage-cell {
|
||||||
|
width: max-content;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 112px 38px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-track {
|
||||||
|
width: 112px;
|
||||||
|
height: 8px;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bar-bg);
|
||||||
|
box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-fill {
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 240ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-fill.normal {
|
||||||
|
background: #20a67a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-fill.warning {
|
||||||
|
background: var(--warn);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-fill.critical {
|
||||||
|
background: var(--err);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-value {
|
||||||
|
color: var(--text-mid);
|
||||||
|
font-size: 12px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -34,7 +34,12 @@
|
|||||||
<span :class="['task-status', task.status_class]">● {{ task.status_text }}</span>
|
<span :class="['task-status', task.status_class]">● {{ task.status_text }}</span>
|
||||||
<span class="task-main">
|
<span class="task-main">
|
||||||
<span class="task-name">{{ task.name }}</span>
|
<span class="task-name">{{ task.name }}</span>
|
||||||
<span class="task-runner mono">{{ task.runner }}</span>
|
<span class="task-meta">
|
||||||
|
<span class="task-runner mono">{{ task.runner }}</span>
|
||||||
|
<time class="task-time mono" :datetime="task.created_at" :title="formatTaskFullTime(task.created_at)">
|
||||||
|
{{ formatTaskTime(task.created_at) }}
|
||||||
|
</time>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,6 +151,33 @@ function formatTime(date: Date) {
|
|||||||
return date.toLocaleTimeString('zh-CN', { hour12: false })
|
return date.toLocaleTimeString('zh-CN', { hour12: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseTaskTime(value: string) {
|
||||||
|
const date = new Date(value)
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTaskTime(value: string) {
|
||||||
|
const date = parseTaskTime(value)
|
||||||
|
if (!date) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}).replace('/', '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTaskFullTime(value: string) {
|
||||||
|
const date = parseTaskTime(value)
|
||||||
|
return date
|
||||||
|
? date.toLocaleString('zh-CN', { hour12: false })
|
||||||
|
: '时间未知'
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => businessLineStore.current?.id,
|
() => businessLineStore.current?.id,
|
||||||
() => {
|
() => {
|
||||||
@@ -228,6 +260,14 @@ onMounted(() => {
|
|||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-meta {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.task-name,
|
.task-name,
|
||||||
.task-runner,
|
.task-runner,
|
||||||
.selected-name {
|
.selected-name {
|
||||||
@@ -243,10 +283,18 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-runner {
|
.task-runner {
|
||||||
|
min-width: 0;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-time {
|
||||||
|
flex: none;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 10.5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.task-log-panel .panel-head {
|
.task-log-panel .panel-head {
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user