提交说明

This commit is contained in:
hy 2026-04-23 13:10:14 +08:00
parent 4317dd9a02
commit 4fe0bb7a60
3 changed files with 432 additions and 234 deletions

View File

@ -768,6 +768,10 @@ body.auth-page {
linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 95%, var(--accent-soft)) 0%, var(--panel) 100%);
}
.summary-strip .status-band-list {
box-shadow: none;
}
.overview-calm-strip {
display: grid;
gap: 10px;
@ -932,6 +936,10 @@ body.auth-page {
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
}
.status-band-item-major {
min-width: 220px;
}
.status-band-item::before {
content: "";
position: absolute;
@ -984,6 +992,13 @@ body.auth-page {
text-transform: uppercase;
}
.status-band-meta {
color: var(--muted);
font-size: 10px;
font-weight: 600;
line-height: 1.35;
}
.summary-card small {
display: block;
color: var(--muted);
@ -1151,14 +1166,12 @@ body.auth-page {
--group-color: var(--accent);
display: grid;
gap: 10px;
padding: 10px;
padding: 10px 0 0;
border: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong));
border-radius: 16px;
background:
linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, color-mix(in oklab, var(--panel-strong) 96%, var(--panel-muted)) 100%);
box-shadow:
var(--shadow-soft),
inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent);
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent);
}
.matrix-stack::before {
@ -1175,7 +1188,7 @@ body.auth-page {
justify-content: space-between;
gap: 10px;
align-items: center;
padding: 0 2px;
padding: 0 12px;
}
.matrix-stack-headline {
@ -1264,6 +1277,12 @@ body.auth-page {
background: color-mix(in oklab, var(--panel-strong) 96%, var(--panel-muted));
}
.matrix-stack .matrix-scroll {
border: 0;
border-radius: 0;
background: transparent;
}
.matrix-row {
cursor: pointer;
}
@ -1710,6 +1729,126 @@ body.auth-page {
gap: 8px;
}
.host-row-list-overview {
gap: 7px;
}
.host-strip-overview {
border-radius: 12px;
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 98%, var(--panel-muted)) 0%, color-mix(in oklab, var(--panel-muted) 90%, var(--panel-strong)) 100%);
box-shadow: none;
}
.host-strip-overview:hover {
transform: none;
box-shadow: none;
}
.host-strip-quiet {
border-color: color-mix(in oklab, var(--line) 88%, var(--panel-strong));
}
.host-strip-main-overview {
grid-template-columns: minmax(156px, 196px) minmax(260px, 1fr) minmax(220px, 0.9fr) auto;
gap: 12px;
}
.host-metric-band {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
min-width: 0;
}
.host-metric-band-fallback {
justify-content: flex-start;
}
.host-metric-inline {
display: inline-flex;
align-items: baseline;
gap: 6px;
min-height: 0;
padding: 0;
border: 0;
background: transparent;
color: var(--muted);
}
.host-metric-inline small,
.host-metric-inline strong {
display: inline-block;
}
.host-metric-inline small {
color: inherit;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.host-metric-inline strong {
font-size: 11px;
line-height: 1.2;
}
.host-metric-inline.tone-ok strong {
color: color-mix(in oklab, var(--ok) 62%, var(--text));
}
.host-metric-inline.tone-warn strong {
color: color-mix(in oklab, var(--warn) 76%, var(--text));
}
.host-metric-inline.tone-bad strong {
color: color-mix(in oklab, var(--bad) 82%, var(--text));
}
.host-service-summary {
display: grid;
gap: 4px;
min-width: 0;
}
.host-service-summary small,
.host-service-summary strong {
display: block;
}
.host-service-summary small {
color: var(--muted);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.host-service-summary-text {
overflow: hidden;
font-size: 11px;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.host-service-summary.tone-bad .host-service-summary-text {
color: color-mix(in oklab, var(--bad) 82%, var(--text));
}
.host-service-summary.tone-warn .host-service-summary-text {
color: color-mix(in oklab, var(--warn) 72%, var(--text));
}
.host-service-summary.tone-neutral .host-service-summary-text {
color: color-mix(in oklab, var(--muted) 76%, var(--text));
}
.host-inline-state-overview {
justify-content: flex-end;
}
.quiet-host-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
@ -2787,6 +2926,35 @@ body.auth-page {
z-index: 12;
}
.release-task-head {
display: flex;
justify-content: space-between;
gap: 14px;
align-items: flex-start;
padding-bottom: 10px;
border-bottom: 1px solid var(--line);
}
.release-task-context {
display: grid;
gap: 3px;
min-width: 0;
}
.release-task-context strong {
color: color-mix(in oklab, var(--text) 88%, var(--accent));
font-size: 11px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.release-task-context span {
color: var(--muted);
font-size: 10px;
letter-spacing: 0.04em;
}
.release-action-bar {
display: flex;
justify-content: space-between;
@ -2801,71 +2969,56 @@ body.auth-page {
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.release-status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 8px;
margin-top: 10px;
}
.release-status-item {
position: relative;
min-height: 0;
padding: 8px 10px;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--panel-strong);
box-shadow: none;
.release-meta-strip {
margin-top: 10px;
align-items: stretch;
}
.release-status-item::before {
content: "";
position: absolute;
left: 0;
top: 8px;
bottom: 8px;
width: 3px;
border-radius: 999px;
background: color-mix(in oklab, currentColor 46%, transparent);
opacity: 0.82;
.release-inline-meta {
display: inline-grid;
gap: 3px;
min-width: 120px;
white-space: normal;
}
.release-status-item.tone-ok {
border-color: color-mix(in oklab, var(--ok) 26%, var(--line));
background: color-mix(in oklab, var(--ok-soft) 42%, var(--panel-strong));
}
.release-status-item.tone-warn {
border-color: color-mix(in oklab, var(--warn) 26%, var(--line));
background: color-mix(in oklab, var(--warn-soft) 44%, var(--panel-strong));
}
.release-status-item.tone-bad {
border-color: color-mix(in oklab, var(--bad) 26%, var(--line));
background: color-mix(in oklab, var(--bad-soft) 42%, var(--panel-strong));
}
.release-status-item small,
.release-status-item strong {
.release-inline-meta small,
.release-inline-meta strong {
display: block;
}
.release-status-item small {
.release-inline-meta small {
color: var(--muted);
font-size: 10px;
text-transform: uppercase;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.release-status-item strong {
margin-top: 6px;
font-size: 12.5px;
line-height: 1.45;
.release-inline-meta strong {
font-size: 11px;
line-height: 1.4;
word-break: break-word;
}
.release-inline-meta.tone-ok strong {
color: color-mix(in oklab, var(--ok) 72%, var(--text));
}
.release-inline-meta.tone-warn strong {
color: color-mix(in oklab, var(--warn) 74%, var(--text));
}
.release-inline-meta.tone-bad strong {
color: color-mix(in oklab, var(--bad) 82%, var(--text));
}
.release-check-strip {
margin-top: 8px;
}
.release-card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
@ -2882,6 +3035,7 @@ body.auth-page {
border: 1px solid var(--line);
border-radius: 12px;
background: var(--panel-strong);
cursor: pointer;
transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
box-shadow: none;
}
@ -2901,6 +3055,12 @@ body.auth-page {
border-color: color-mix(in oklab, var(--accent) 28%, var(--line));
}
.release-service-card:focus-visible {
outline: 0;
border-color: color-mix(in oklab, var(--accent) 56%, var(--line-strong));
box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent-soft) 84%, transparent);
}
.release-service-card.selected {
border-color: color-mix(in oklab, var(--accent) 38%, var(--line));
background: color-mix(in oklab, var(--accent-soft) 26%, var(--panel-strong));
@ -2975,7 +3135,7 @@ body.auth-page {
.release-service-actions {
display: grid;
gap: 8px;
gap: 6px;
justify-items: end;
align-self: center;
}
@ -2984,17 +3144,39 @@ body.auth-page {
align-self: flex-start;
}
.release-service-action-row {
display: flex;
gap: 8px;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
}
.release-select-state {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0 9px;
border: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong));
border-radius: 999px;
background: color-mix(in oklab, var(--panel-muted) 94%, var(--panel-strong));
color: var(--muted);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
}
.release-select-state.is-selected {
border-color: color-mix(in oklab, var(--accent) 34%, var(--line));
background: color-mix(in oklab, var(--accent-soft) 42%, var(--panel-strong));
color: var(--accent);
}
.release-service-note {
color: var(--muted);
margin: 0;
}
.release-service-actions .inline-copy {
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.precheck-strip {
display: flex;
gap: 8px;
@ -4160,17 +4342,41 @@ body.auth-page {
font-size: 12px;
}
.update-field-toggle {
justify-content: flex-end;
}
.update-check {
justify-content: center;
padding: 12px 14px;
flex-direction: row;
align-items: center;
gap: 10px;
min-height: 44px;
padding: 0 12px;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--panel-muted);
border-radius: 12px;
background: color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong));
}
.update-check input {
width: 18px;
height: 18px;
margin: 0;
}
.update-check strong,
.update-check small {
display: block;
}
.update-check strong {
font-size: 12px;
line-height: 1.2;
}
.update-check small {
color: var(--muted);
font-size: 10px;
line-height: 1.2;
}
.operation-meta {
@ -4567,10 +4773,12 @@ body.auth-page {
.taskbar-context,
.taskbar-inline,
.metadata-strip,
.release-meta-strip,
.mongo-pager,
.precheck-strip,
.compact-workbench-head,
.activity-item-side,
.release-task-head,
.release-action-bar,
.release-action-summary,
.log-viewer-head,
@ -4599,6 +4807,7 @@ body.auth-page {
}
.release-action-bar,
.release-task-head,
.log-viewer-head,
.compact-workbench-head,
.release-service-head,
@ -4611,7 +4820,7 @@ body.auth-page {
justify-items: start;
}
.release-service-actions .inline-copy {
.release-service-action-row {
justify-content: flex-start;
}

View File

@ -1568,76 +1568,62 @@ createApp({
updateAffectedServicesCount() {
return this.updateOps.result?.services?.length || this.updateOps.selectedServices.length || 0;
},
summaryStripCards() {
overviewStatusSegments() {
const serviceDownTotal = this.allServiceRows.filter((row) => row.downCount > 0).length;
const hostAlertTotal = this.allHostRows.filter((host) => host.hasAlert).length;
const instanceDownTotal = this.summary.down || 0;
const hostAlertTotal = this.overviewAlertHosts.length;
const hostMetricDown = this.summary.hostMetricDown || 0;
const hostTotal = this.summary.hostTotal || 0;
const nacosUnavailable = !(this.payload.nacos && this.payload.nacos.ok);
const nacosUnhealthyTotal = this.summary.nacosUnhealthyInstanceTotal || 0;
const mongoUnavailable = !(this.payload.mongo && this.payload.mongo.ok);
const tatOk = this.summary.hostMetricOk || 0;
const tatTotal = this.summary.hostTotal || 0;
const hasCriticalAlert = serviceDownTotal > 0 || instanceDownTotal > 0 || nacosUnavailable || mongoUnavailable;
const hasWarning = hostAlertTotal > 0 || hostMetricDown > 0 || nacosUnhealthyTotal > 0;
return [
{
key: "service-down",
label: "服务",
value: `${serviceDownTotal}/${this.allServiceRows.length}`,
suffix: "异常",
tone: serviceDownTotal > 0 ? "bad" : "ok",
visible: serviceDownTotal > 0,
key: "overall",
label: "状态",
value: hasCriticalAlert ? "存在异常" : hasWarning ? "需要关注" : "当前无异常",
meta: hasCriticalAlert
? `${serviceDownTotal} 服务 / ${instanceDownTotal} 实例`
: hasWarning
? `${hostAlertTotal} 主机 / ${nacosUnhealthyTotal} Nacos`
: `${this.allServiceRows.length} 服务稳定`,
tone: hasCriticalAlert ? "bad" : hasWarning ? "warn" : "ok",
},
{
key: "instance-down",
label: "实例",
value: `${this.summary.down || 0}/${this.summary.total || 0}`,
suffix: "异常",
tone: (this.summary.down || 0) > 0 ? "bad" : "ok",
visible: (this.summary.down || 0) > 0,
},
{
key: "host-alert",
label: "主机",
value: `${hostAlertTotal}/${this.summary.hostTotal || 0}`,
suffix: "异常",
tone: hostAlertTotal > 0 ? "warn" : "ok",
visible: hostAlertTotal > 0,
key: "hosts",
label: "业务主机",
value: `${this.overviewHostRows.length}`,
meta: hostAlertTotal > 0 ? `${hostAlertTotal} 台异常` : `${this.overviewQuietServiceTotal} 服务在线`,
tone: hostAlertTotal > 0 ? (this.overviewAlertHosts.some((host) => host.downCount > 0) ? "bad" : "warn") : "ok",
},
{
key: "nacos",
label: "Nacos",
value: nacosUnavailable ? "ERR" : String(this.summary.nacosUnhealthyInstanceTotal || 0),
suffix: nacosUnavailable ? "" : "异常",
tone: nacosUnavailable || (this.summary.nacosUnhealthyInstanceTotal || 0) > 0 ? "bad" : "ok",
visible: nacosUnavailable || (this.summary.nacosUnhealthyInstanceTotal || 0) > 0,
value: nacosUnavailable ? "连接异常" : nacosUnhealthyTotal > 0 ? `${nacosUnhealthyTotal} 异常` : "连接正常",
meta: `${this.payload.nacos?.summary?.serviceTotal || 0} 服务`,
tone: nacosUnavailable ? "bad" : nacosUnhealthyTotal > 0 ? "warn" : "ok",
},
{
key: "mongo",
label: "Mongo",
value: mongoUnavailable ? "ERR" : String(this.summary.mongoDatabaseTotal || 0),
suffix: mongoUnavailable ? "" : "库",
value: mongoUnavailable ? "连接异常" : `${this.summary.mongoDatabaseTotal || 0}`,
meta: mongoUnavailable ? "deploy 直连失败" : "连接正常",
tone: mongoUnavailable ? "bad" : "ok",
visible: mongoUnavailable,
},
{
key: "tat",
label: "TAT",
value: `${tatOk}/${tatTotal}`,
suffix: "正常",
tone: (this.summary.hostMetricDown || 0) > 0 ? "warn" : "ok",
visible: (this.summary.hostMetricDown || 0) > 0,
value: `${this.summary.hostMetricOk || 0}/${hostTotal}`,
meta: hostMetricDown > 0 ? `${hostMetricDown} 台异常` : "采集正常",
tone: hostMetricDown > 0 ? "warn" : "ok",
},
].filter((card) => card.visible);
];
},
overviewHealthyCards() {
overviewHealthyInfraCards() {
const cards = [];
if (this.overviewQuietHosts.length > 0) {
cards.push({
key: "hosts",
label: "业务主机",
value: `${this.overviewQuietHosts.length}`,
meta: "正常",
tone: "ok",
});
}
if (this.payload.nacos && this.payload.nacos.ok && (this.summary.nacosUnhealthyInstanceTotal || 0) === 0) {
cards.push({
key: "nacos",
@ -1667,9 +1653,6 @@ createApp({
}
return cards;
},
overviewHealthyInfraCards() {
return this.overviewHealthyCards.filter((card) => card.key !== "hosts");
},
overviewHealthyHostGroups() {
const groups = new Map();
this.overviewQuietHosts.forEach((host) => {

View File

@ -300,34 +300,16 @@
<p v-if="error" class="toolbar-error">加载失败:{{ error }}</p>
<template v-if="!showLoadingSkeleton && (activeView === 'overview' || activeView === 'services')">
<section v-if="activeView === 'overview' && summaryStripCards.length > 0" class="panel summary-strip">
<section v-if="activeView === 'overview' && overviewStatusSegments.length > 0" class="panel summary-strip">
<div class="summary-cards status-band-list">
<article
v-for="card in summaryStripCards"
v-for="card in overviewStatusSegments"
:key="card.key"
:class="['summary-tile', 'status-band-item', `tone-${card.tone}`]"
:class="['summary-tile', 'status-band-item', card.key === 'overall' ? 'status-band-item-major' : '', `tone-${card.tone}`]"
>
<span class="summary-strip-label">{{ card.label }}</span>
<strong>{{ card.value }}</strong>
<span v-if="card.suffix" class="status-band-suffix">{{ card.suffix }}</span>
</article>
</div>
</section>
<section v-else-if="activeView === 'overview' && overviewHealthyCards.length > 0" class="panel overview-calm-strip">
<div class="overview-calm-head">
<span class="badge ok">当前无异常</span>
<span class="overview-calm-meta">{{ overviewHealthyCards.length }} 组健康状态</span>
</div>
<div class="overview-calm-grid">
<article
v-for="card in overviewHealthyCards"
:key="'healthy-' + card.key"
:class="['overview-calm-card', `tone-${card.tone}`]"
>
<small>{{ card.label }}</small>
<strong>{{ card.value }}</strong>
<span>{{ card.meta }}</span>
<span class="status-band-meta">{{ card.meta }}</span>
</article>
</div>
</section>
@ -572,58 +554,54 @@
<span>{{ overviewAlertHosts.filter((host) => host.hasMetricIssue).length }} 采集异常</span>
</div>
</div>
<div class="host-row-list">
<div class="host-row-list host-row-list-overview">
<article
v-for="host in overviewAlertHosts"
:key="host.host"
:class="[
'host-strip',
'host-strip-overview',
host.hasAlert ? 'host-strip-alert' : '',
isHostHighlighted(host) ? 'host-strip-focused' : ''
]"
:data-group="host.group"
>
<div class="host-strip-main" @click="selectHost(host.host)">
<div class="host-strip-main host-strip-main-overview" @click="selectHost(host.host)">
<div class="host-strip-id">
<strong>{{ host.host }}</strong>
<span>{{ host.ip }} · {{ host.groupLabel }}</span>
</div>
<div v-if="hostHasMetricData(host)" class="host-inline-metrics compact-metrics">
<div
<div v-if="hostHasMetricData(host)" class="host-metric-band">
<span
v-for="metric in hostMetricEntries(host)"
:key="host.host + '-' + metric.key"
:class="['metric-ring-chip', `tone-${metric.level}`]"
:class="['host-metric-inline', `tone-${metric.level}`]"
>
<span class="metric-ring" :style="metricRingStyle(metric.value, metric.level)">
<span class="metric-ring-hole"></span>
</span>
<span class="metric-ring-copy">
<small>{{ metric.label }}</small>
<strong>{{ metric.display }}</strong>
</span>
</div>
<span :class="['metric-stat-chip', `tone-${host.metrics.processLevel || thresholdTone('processCount', host.metrics.processCount)}`]">
<small>{{ metric.label }}</small>
<strong>{{ metric.display }}</strong>
</span>
<span :class="['host-metric-inline', `tone-${host.metrics.processLevel || thresholdTone('processCount', host.metrics.processCount)}`]">
<small>PROC</small>
<strong>{{ host.metrics.processCount ?? '-' }}</strong>
</span>
</div>
<div v-else class="host-inline-metrics compact-metrics compact-metrics-fallback">
<span :class="['metric-state-chip', host.hasMetricIssue ? 'tone-warn' : 'tone-neutral']">
<div v-else class="host-metric-band host-metric-band-fallback">
<span :class="['host-metric-inline', host.hasMetricIssue ? 'tone-warn' : 'tone-neutral']">
<small>TAT</small>
<strong>{{ hostMetricStatusLabel(host) }}</strong>
</span>
</div>
<div :class="['host-alert-summary', `tone-${hostSummaryTone(host)}`]">
<div :class="['host-service-summary', `tone-${hostSummaryTone(host)}`]">
<small>{{ host.downCount > 0 ? '异常服务' : host.hasMetricIssue ? '采集状态' : '在线服务' }}</small>
<span class="host-alert-path">{{ host.downCount > 0 ? host.alertSummary : host.quietSummary }}</span>
<strong class="host-service-summary-text">{{ host.downCount > 0 ? host.alertSummary : host.quietSummary }}</strong>
</div>
<div class="host-inline-state">
<div class="host-inline-state host-inline-state-overview">
<span class="badge neutral">{{ host.serviceTotal }} 服务</span>
<span :class="['badge', host.downCount > 0 ? 'bad' : 'neutral']">{{ host.downCount }} 异常</span>
<span :class="['badge', host.hasMetricIssue ? 'bad' : 'neutral']">{{ host.hasMetricIssue ? '采集异常' : '指标正常' }}</span>
<span :class="['badge', host.hasMetricIssue ? 'warn' : 'neutral']">{{ host.hasMetricIssue ? '采集异常' : (isHostExpanded(host.host) ? '收起' : '展开') }}</span>
</div>
</div>
@ -663,44 +641,70 @@
<small>{{ overviewHealthyHostsOpen ? '收起' : '展开' }}</small>
</button>
<div v-if="overviewHealthyHostsOpen || overviewAlertHosts.length === 0" class="quiet-host-grid">
<button
<div v-if="overviewHealthyHostsOpen || overviewAlertHosts.length === 0" class="host-row-list host-row-list-overview">
<article
v-for="host in overviewQuietHosts"
:key="'quiet-' + host.host"
:class="['quiet-host-card', isHostHighlighted(host) ? 'active' : '']"
:class="[
'host-strip',
'host-strip-overview',
'host-strip-quiet',
isHostHighlighted(host) ? 'host-strip-focused' : ''
]"
:data-group="host.group"
@click="selectHost(host.host)"
>
<div class="quiet-host-head">
<strong>{{ host.host }}</strong>
<span>{{ host.ip }} · {{ host.groupLabel }}</span>
</div>
<div v-if="hostHasMetricData(host)" class="quiet-host-metrics">
<div
v-for="metric in hostMetricEntries(host)"
:key="'quiet-' + host.host + '-' + metric.key"
:class="['metric-ring-chip', 'metric-ring-chip-quiet', `tone-${metric.level}`]"
>
<span class="metric-ring" :style="metricRingStyle(metric.value, metric.level)">
<span class="metric-ring-hole"></span>
</span>
<span class="metric-ring-copy">
<div class="host-strip-main host-strip-main-overview">
<div class="host-strip-id">
<strong>{{ host.host }}</strong>
<span>{{ host.ip }} · {{ host.groupLabel }}</span>
</div>
<div v-if="hostHasMetricData(host)" class="host-metric-band">
<span
v-for="metric in hostMetricEntries(host)"
:key="'quiet-' + host.host + '-' + metric.key"
:class="['host-metric-inline', `tone-${metric.level}`]"
>
<small>{{ metric.label }}</small>
<strong>{{ metric.display }}</strong>
</span>
<span :class="['host-metric-inline', `tone-${host.metrics.processLevel || thresholdTone('processCount', host.metrics.processCount)}`]">
<small>PROC</small>
<strong>{{ host.metrics.processCount ?? '-' }}</strong>
</span>
</div>
<div v-else class="host-metric-band host-metric-band-fallback">
<span class="host-metric-inline tone-neutral">
<small>TAT</small>
<strong>{{ hostMetricStatusLabel(host) }}</strong>
</span>
</div>
<div class="host-service-summary tone-neutral">
<small>在线服务</small>
<strong class="host-service-summary-text">{{ host.quietSummary }}</strong>
</div>
<div class="host-inline-state host-inline-state-overview">
<span class="badge neutral">{{ host.serviceTotal }} 服务</span>
<span class="badge neutral">{{ isHostExpanded(host.host) ? '收起' : '展开' }}</span>
</div>
</div>
<div v-else class="quiet-host-metrics quiet-host-metrics-fallback">
<span class="metric-state-chip tone-neutral">
<small>TAT</small>
<strong>{{ hostMetricStatusLabel(host) }}</strong>
</span>
<div v-if="isHostExpanded(host.host)" class="host-strip-detail host-strip-detail-list">
<button
v-for="item in host.items"
:key="'quiet-detail-' + host.host + '-' + item.service"
:class="['host-detail-line', toneClass(item.level || (item.ok ? 'ok' : 'bad'))]"
@click.stop="selectService(item.service); activeView = 'services'"
>
<strong>{{ item.service }}</strong>
<span>{{ item.statusCode || 'UP' }} · {{ formatLatency(item.latencyMs) }}</span>
</button>
<div v-if="host.items.length === 0" class="empty-panel compact">当前主机没有服务实例</div>
</div>
<div class="quiet-host-foot">
<span class="badge neutral">{{ host.serviceTotal }} 服务</span>
<span class="badge neutral">{{ host.quietSummary }}</span>
</div>
</button>
</article>
</div>
</section>
</div>
@ -2930,16 +2934,10 @@
<template v-if="!showLoadingSkeleton && activeView === 'release'">
<section class="panel release-status-strip sticky-release-strip">
<div class="release-action-bar">
<div class="release-action-summary">
<span :class="['badge', updateStatusBadgeClass()]">{{ updateStatusLabel() }}</span>
<span class="badge neutral">更新目标 {{ releaseTargetRows.length }}</span>
<span class="badge neutral">重启目标 {{ restartTargetRows.length }}</span>
<span class="badge neutral">已选更新 {{ updateOps.selectedServices.length }}</span>
<span class="badge neutral">已选重启 {{ restartOps.selectedServices.length }}</span>
<span :class="['badge', updateOps.error || restartOps.error ? 'bad' : releasePrecheck.ok ? 'ok' : 'neutral']">
{{ releasePrecheck.ranAt ? releasePrecheck.title : '未预检查' }}
</span>
<div class="release-task-head">
<div class="release-task-context">
<strong>发布工作台</strong>
<span>构建参数 → 更新目标 → 执行日志 → 滚动重启</span>
</div>
<div class="action-group">
<button class="refresh ghost" @click="refreshReleaseTargets" :disabled="updateOps.loading || restartOps.loading || updateOps.running || restartOps.running">
@ -2955,14 +2953,29 @@
</div>
</div>
<div class="release-status-grid">
<article v-for="item in releaseStatusSummary" :key="item.label" :class="['release-status-item', `tone-${item.tone}`]">
<small>{{ item.label }}</small>
<strong :title="item.value">{{ item.value }}</strong>
</article>
<div class="release-action-summary">
<span :class="['badge', updateStatusBadgeClass()]">{{ updateStatusLabel() }}</span>
<span class="badge neutral">更新目标 {{ releaseTargetRows.length }}</span>
<span class="badge neutral">重启目标 {{ restartTargetRows.length }}</span>
<span class="badge neutral">已选更新 {{ updateOps.selectedServices.length }}</span>
<span class="badge neutral">已选重启 {{ restartOps.selectedServices.length }}</span>
<span :class="['badge', updateOps.error || restartOps.error ? 'bad' : releasePrecheck.ok ? 'ok' : 'neutral']">
{{ releasePrecheck.ranAt ? releasePrecheck.title : '未预检查' }}
</span>
</div>
<div v-if="releasePrecheck.ranAt" class="precheck-strip">
<div class="metadata-strip metadata-strip-tight release-meta-strip">
<span
v-for="item in releaseStatusSummary"
:key="item.label"
:class="['release-inline-meta', `tone-${item.tone}`]"
>
<small>{{ item.label }}</small>
<strong :title="item.value">{{ item.value }}</strong>
</span>
</div>
<div v-if="releasePrecheck.ranAt" class="precheck-strip release-check-strip">
<span :class="['badge', releasePrecheck.tone]">{{ releasePrecheck.title }}</span>
<span class="badge neutral">执行于 {{ formatDateTime(releasePrecheck.ranAt) }}</span>
<span
@ -3011,10 +3024,14 @@
<span>Image Tag</span>
<input v-model.trim="updateOps.imageTag" class="search" placeholder="可选,不填则自动生成" />
</label>
<label class="update-check">
<input v-model="updateOps.skipMavenBuild" type="checkbox" />
<span>跳过 Maven</span>
</label>
<div class="update-field update-field-toggle">
<span>构建选项</span>
<label class="update-check">
<input v-model="updateOps.skipMavenBuild" type="checkbox" />
<strong>跳过 Maven</strong>
<small>仅影响 Java 构建阶段</small>
</label>
</div>
</div>
</section>
@ -3037,6 +3054,11 @@
:key="'update-' + item.service"
:class="['release-service-card', updateOps.selectedServices.includes(item.service) ? 'selected' : '']"
:data-kind="item.kind"
role="button"
tabindex="0"
@click="toggleUpdateService(item.service)"
@keydown.enter.prevent="toggleUpdateService(item.service)"
@keydown.space.prevent="toggleUpdateService(item.service)"
>
<header class="release-service-head">
<div class="release-service-title">
@ -3052,23 +3074,20 @@
</div>
</div>
<div class="release-service-actions">
<span :class="['release-select-state', updateOps.selectedServices.includes(item.service) ? 'is-selected' : '']">
{{ updateOps.selectedServices.includes(item.service) ? '已选' : '未选' }}
</span>
<small class="table-subtext mono release-service-note" :title="item.repository">{{ item.repositoryShort }}</small>
<div class="inline-copy">
<div class="release-service-action-row">
<button
v-if="item.copyValue"
class="refresh ghost inline-button"
@click="copyText(item.copyValue, 'image-' + item.service)"
@click.stop="copyText(item.copyValue, 'image-' + item.service)"
@keydown.enter.stop
@keydown.space.stop
>
{{ isCopied('image-' + item.service) ? '已复制' : '复制' }}
</button>
<label :class="['table-check', updateOps.selectedServices.includes(item.service) ? 'checked' : '']">
<input
:checked="updateOps.selectedServices.includes(item.service)"
type="checkbox"
@change="toggleUpdateService(item.service)"
/>
<span>{{ updateOps.selectedServices.includes(item.service) ? '已选' : '选择' }}</span>
</label>
</div>
</div>
</header>
@ -3111,19 +3130,6 @@
<div v-else class="empty-panel compact">当前还没有可展示的执行日志</div>
</section>
<div class="activity-list compact activity-grid" v-if="releaseHistoryRows.length > 0">
<article v-for="item in releaseHistoryRows" :key="item.id" :class="['activity-item', 'compact', `tone-${item.tone}`]">
<div class="activity-item-main">
<strong>{{ item.title }}</strong>
<span>{{ item.detail }}</span>
</div>
<div class="activity-item-side">
<small>{{ item.scope }}</small>
<small>{{ item.time }}</small>
</div>
</article>
</div>
<details v-if="updateOps.result && ((updateOps.result.build || updateOps.result.builds) || updateOps.result.preparedHosts || updateOps.result.steps)" class="result-disclosure">
<summary>执行结果明细</summary>
@ -3232,6 +3238,11 @@
:key="'restart-' + item.service"
:class="['release-service-card', restartOps.selectedServices.includes(item.service) ? 'selected' : '']"
:data-kind="item.kind"
role="button"
tabindex="0"
@click="toggleRestartService(item.service)"
@keydown.enter.prevent="toggleRestartService(item.service)"
@keydown.space.prevent="toggleRestartService(item.service)"
>
<header class="release-service-head">
<div class="release-service-title">
@ -3245,14 +3256,9 @@
</div>
</div>
<div class="release-service-actions release-service-actions-tight">
<label :class="['table-check', restartOps.selectedServices.includes(item.service) ? 'checked' : '']">
<input
:checked="restartOps.selectedServices.includes(item.service)"
type="checkbox"
@change="toggleRestartService(item.service)"
/>
<span>{{ restartOps.selectedServices.includes(item.service) ? '已选' : '选择' }}</span>
</label>
<span :class="['release-select-state', restartOps.selectedServices.includes(item.service) ? 'is-selected' : '']">
{{ restartOps.selectedServices.includes(item.service) ? '已选' : '未选' }}
</span>
</div>
</header>
</article>