1822 lines
107 KiB
HTML
1822 lines
107 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>HY App Monitor</title>
|
||
<link rel="stylesheet" href="app.css" />
|
||
<script defer src="vue.js"></script>
|
||
<script defer src="app.js"></script>
|
||
</head>
|
||
|
||
<body>
|
||
<div id="boot-indicator" class="boot-indicator">加载中...</div>
|
||
<noscript>
|
||
<div class="boot-indicator">请启用 JavaScript</div>
|
||
</noscript>
|
||
|
||
<div id="app" v-cloak class="shell">
|
||
<header class="topbar">
|
||
<div class="brand-stack">
|
||
<div class="brand-row">
|
||
<p class="brand-mark">HY APP MONITOR</p>
|
||
<span class="env-pill">DEPLOY</span>
|
||
</div>
|
||
<p class="sync-note">最后刷新 {{ formattedUpdatedAt }}</p>
|
||
</div>
|
||
|
||
<nav class="topbar-nav" aria-label="主导航">
|
||
<button
|
||
v-for="tab in viewTabs"
|
||
:key="tab.id"
|
||
:class="['nav-chip', activeView === tab.id ? 'active' : '']"
|
||
@click="activeView = tab.id"
|
||
>
|
||
{{ tab.label }}
|
||
</button>
|
||
</nav>
|
||
|
||
<div class="topbar-status">
|
||
<span v-if="auth.username" class="badge neutral mono">{{ auth.username }}</span>
|
||
<span :class="['badge', loading ? 'warn' : 'neutral']">{{ loading ? '刷新中' : '在线' }}</span>
|
||
<span v-if="error" class="badge bad">接口异常</span>
|
||
<label class="interval-field topbar-interval">
|
||
<span>刷新</span>
|
||
<select v-model.number="refreshIntervalSeconds" class="interval-select">
|
||
<option v-for="seconds in refreshIntervalOptions" :key="seconds" :value="seconds">{{ seconds }}s</option>
|
||
</select>
|
||
</label>
|
||
<button class="refresh" @click="refresh" :disabled="loading">{{ loading ? '刷新中...' : '立即刷新' }}</button>
|
||
<button class="refresh ghost" @click="logout" :disabled="auth.loggingOut">{{ auth.loggingOut ? '退出中...' : '退出' }}</button>
|
||
</div>
|
||
</header>
|
||
|
||
<template v-if="activeView === 'overview' || activeView === 'services' || activeView === 'hosts'">
|
||
<section class="control-bar">
|
||
<div class="control-group">
|
||
<button
|
||
v-for="option in primaryFilters"
|
||
:key="option.id"
|
||
:class="['filter-chip', filter === option.id ? 'active' : '']"
|
||
@click="filter = option.id"
|
||
>
|
||
{{ option.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="control-group control-group-spread">
|
||
<div class="density-switch">
|
||
<button :class="['filter-chip', densityMode === 'compact' ? 'active' : '']" @click="densityMode = 'compact'">紧凑</button>
|
||
<button :class="['filter-chip', densityMode === 'detail' ? 'active' : '']" @click="densityMode = 'detail'">详情</button>
|
||
</div>
|
||
|
||
<input v-model.trim="keyword" class="search" placeholder="搜索主机 / 服务 / IP" />
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-else-if="activeView === 'infra'">
|
||
<section class="control-bar task-control-bar">
|
||
<div class="taskbar-context">
|
||
<strong>健康与排障</strong>
|
||
<span>总览 / Nacos / Mongo / TAT</span>
|
||
</div>
|
||
|
||
<div class="control-group">
|
||
<button
|
||
v-for="tab in infraTabs"
|
||
:key="tab.id"
|
||
:class="['filter-chip', infraTab === tab.id ? 'active' : '']"
|
||
@click="infraTab = tab.id"
|
||
>
|
||
{{ tab.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="control-group control-group-spread">
|
||
<button
|
||
v-if="infraTab === 'nacos'"
|
||
:class="['filter-chip', infraHealthyNacosOpen ? 'active' : '']"
|
||
@click="infraHealthyNacosOpen = !infraHealthyNacosOpen"
|
||
>
|
||
{{ infraHealthyNacosOpen ? '收起正常服务' : '展开正常服务' }}
|
||
</button>
|
||
<button
|
||
v-if="infraTab === 'tat'"
|
||
:class="['filter-chip', infraHealthyTatOpen ? 'active' : '']"
|
||
@click="infraHealthyTatOpen = !infraHealthyTatOpen"
|
||
>
|
||
{{ infraHealthyTatOpen ? '收起正常主机' : '展开正常主机' }}
|
||
</button>
|
||
<button
|
||
v-if="infraTab === 'mongo'"
|
||
:class="['filter-chip', infraMongoDetailsOpen ? 'active' : '']"
|
||
@click="infraMongoDetailsOpen = !infraMongoDetailsOpen"
|
||
>
|
||
{{ infraMongoDetailsOpen ? '收起次要指标' : '展开次要指标' }}
|
||
</button>
|
||
<input
|
||
v-model.trim="infraKeyword"
|
||
class="search"
|
||
:placeholder="infraTab === 'mongo' ? '搜索数据库 / 指标' : infraTab === 'tat' ? '搜索主机 / IP / 节点' : '搜索服务 / namespace / IP'"
|
||
/>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-else-if="activeView === 'config'">
|
||
<section class="control-bar task-control-bar">
|
||
<div class="taskbar-context">
|
||
<strong>编辑与校验</strong>
|
||
<span>Nacos / Go Env / 历史版本</span>
|
||
</div>
|
||
|
||
<div class="control-group">
|
||
<button
|
||
v-for="tab in configTabs"
|
||
:key="tab.id"
|
||
:class="['filter-chip', configTab === tab.id ? 'active' : '']"
|
||
@click="configTab = tab.id"
|
||
>
|
||
{{ tab.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="control-group control-group-spread">
|
||
<input
|
||
v-if="configTab !== 'go'"
|
||
v-model.trim="nacosConfig.keyword"
|
||
class="search"
|
||
:placeholder="configTab === 'history' ? '搜索操作 / 结果 / 时间' : '搜索 dataId / group / appName'"
|
||
/>
|
||
<div v-else class="taskbar-inline">
|
||
<span class="badge neutral">{{ goConfig.hosts.length }} 台主机</span>
|
||
<span :class="['badge', goConfig.diffKeys.length > 0 ? 'warn' : 'neutral']">{{ goConfig.diffKeys.length }} 差异 key</span>
|
||
</div>
|
||
|
||
<button
|
||
v-if="configTab === 'nacos'"
|
||
class="refresh"
|
||
@click="refreshNacosConfigs({ preserveSelection: true, reloadSelected: false })"
|
||
:disabled="nacosConfig.loading"
|
||
>
|
||
{{ nacosConfig.loading ? '加载中...' : '刷新配置列表' }}
|
||
</button>
|
||
<button
|
||
v-else-if="configTab === 'go'"
|
||
class="refresh"
|
||
@click="refreshGoConfig"
|
||
:disabled="goConfig.loading"
|
||
>
|
||
{{ goConfig.loading ? '加载中...' : '刷新 Go 配置' }}
|
||
</button>
|
||
<button
|
||
v-else
|
||
class="refresh ghost"
|
||
@click="configHistory = []"
|
||
:disabled="configHistory.length === 0"
|
||
>
|
||
清空记录
|
||
</button>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-else-if="activeView === 'release'">
|
||
<section class="control-bar task-control-bar">
|
||
<div class="taskbar-context">
|
||
<strong>变更与执行</strong>
|
||
<span>构建参数 / 更新目标 / 执行日志 / 滚动重启</span>
|
||
</div>
|
||
|
||
<div class="control-group release-section-nav">
|
||
<button
|
||
v-for="section in releaseSections"
|
||
:key="section.id"
|
||
class="section-link"
|
||
@click="scrollToSection(section.id)"
|
||
>
|
||
{{ section.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="control-group control-group-spread">
|
||
<label class="interval-field">
|
||
<span>类型</span>
|
||
<select v-model="releaseKindFilter" class="interval-select release-kind-select">
|
||
<option value="all">全部</option>
|
||
<option value="java">Java</option>
|
||
<option value="golang">Golang</option>
|
||
<option value="frontend">前端</option>
|
||
</select>
|
||
</label>
|
||
<input v-model.trim="releaseKeyword" class="search" placeholder="搜索服务 / 主机 / 镜像" />
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<p v-if="error" class="toolbar-error">加载失败:{{ error }}</p>
|
||
|
||
<template v-if="activeView === 'overview' || activeView === 'services'">
|
||
<section v-if="activeView === 'overview'" class="panel summary-strip">
|
||
<div class="summary-cards status-band-list">
|
||
<article
|
||
v-for="card in summaryStripCards"
|
||
:key="card.key"
|
||
:class="['summary-tile', 'status-band-item', `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 :class="['content-grid', detailPanelVisible ? 'service-layout-open' : 'service-layout-solo']">
|
||
<section class="panel matrix-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>服务状态矩阵</h2>
|
||
<p>{{ serviceMatrixRows.length }} 行 · {{ serviceMatrixColumns.length }} 列</p>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span class="badge neutral">{{ allServiceRows.length }} 服务</span>
|
||
<span :class="['badge', summary.down > 0 ? 'bad' : 'ok']">{{ summary.down }} 异常实例</span>
|
||
<span v-if="focusedHostKey" class="badge warn mono">{{ focusedHostKey }}</span>
|
||
<button v-if="focusedHostKey" class="refresh ghost" @click="clearFocusedHost">清除主机</button>
|
||
<button class="refresh ghost" @click="toggleDetailPanel">
|
||
{{ detailPanelVisible ? '收起详情' : `展开详情 ${alertFeed.length > 0 ? `(${alertFeed.length})` : ''}` }}
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div v-if="serviceMatrixSections.length > 0" class="matrix-stacks">
|
||
<section v-for="section in serviceMatrixSections" :key="section.group" class="matrix-stack">
|
||
<header class="matrix-stack-head">
|
||
<strong>{{ section.label }} · {{ section.rows.length }} 服务 / {{ section.columns.length }} 节点</strong>
|
||
</header>
|
||
|
||
<div class="matrix-scroll">
|
||
<table :class="['matrix-table', densityMode === 'detail' ? 'is-detail' : 'is-compact']">
|
||
<thead>
|
||
<tr>
|
||
<th class="matrix-sticky-col">服务</th>
|
||
<th v-if="densityMode === 'detail'">类型</th>
|
||
<th v-for="column in section.columns" :key="section.group + '-' + column.host">{{ column.host }}</th>
|
||
<th>实例</th>
|
||
<th>异常</th>
|
||
<th v-if="densityMode === 'detail'">均值</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr
|
||
v-for="row in section.rows"
|
||
:key="row.key"
|
||
:class="[
|
||
'matrix-row',
|
||
isSelectedService(row.key) ? 'selected' : '',
|
||
focusedHostKey && row.instanceMap[focusedHostKey] ? 'focused' : ''
|
||
]"
|
||
@click="selectService(row.key)"
|
||
>
|
||
<td class="matrix-service matrix-sticky-col">
|
||
<strong>{{ row.service }}</strong>
|
||
</td>
|
||
<td v-if="densityMode === 'detail'" class="matrix-kind">{{ row.kind }}</td>
|
||
<td
|
||
v-for="cell in row.sectionCells"
|
||
:key="row.key + '-' + cell.host"
|
||
:title="matrixCellTitle(cell)"
|
||
:class="[
|
||
'matrix-state',
|
||
cell.present ? toneClass(cell.level || (cell.ok ? 'ok' : 'bad')) : 'matrix-state-empty'
|
||
]"
|
||
>
|
||
<template v-if="cell.present">
|
||
<span class="matrix-state-marker" aria-hidden="true"></span>
|
||
<span class="matrix-state-code">{{ statusCellLabel(cell) }}</span>
|
||
<small v-if="cell.latencyMs != null" class="matrix-state-latency">{{ formatLatency(cell.latencyMs) }}</small>
|
||
</template>
|
||
<span v-else class="matrix-state-empty-mark">{{ densityMode === 'detail' ? '—' : '·' }}</span>
|
||
</td>
|
||
<td>{{ row.deployedCount }}</td>
|
||
<td :class="row.downCount > 0 ? 'bad-text' : 'ok-text'">{{ row.downCount }}</td>
|
||
<td v-if="densityMode === 'detail'">{{ formatLatency(row.avgLatencyMs) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<div v-else class="empty-panel">当前筛选下没有服务</div>
|
||
</section>
|
||
|
||
<aside v-if="detailPanelVisible" class="panel detail-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>{{ selectedServiceDetail ? selectedServiceDetail.service : '服务详情' }}</h2>
|
||
<p v-if="selectedServiceDetail">{{ selectedServiceDetail.groupLabel }} · {{ selectedServiceDetail.kind }}</p>
|
||
<p v-else>{{ alertFeed.length }} 条异常</p>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="toggleDetailPanel">收起</button>
|
||
<button class="refresh ghost" @click="activeView = 'config'">配置</button>
|
||
<button class="refresh ghost" @click="activeView = 'release'">发布</button>
|
||
</div>
|
||
</header>
|
||
|
||
<section v-if="alertFeed.length > 0" class="detail-alert-panel">
|
||
<header class="detail-subhead">
|
||
<strong>最近异常</strong>
|
||
<span>{{ alertFeed.length }} 条</span>
|
||
</header>
|
||
<div class="detail-alert-feed">
|
||
<button
|
||
v-for="alert in alertFeed"
|
||
:key="'detail-' + alert.key"
|
||
:class="['alert-item', 'compact', `tone-${alert.tone}`]"
|
||
@click="jumpToAlert(alert)"
|
||
>
|
||
<strong>{{ alert.title }}</strong>
|
||
<span>{{ alert.text }}</span>
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<template v-if="selectedServiceDetail">
|
||
<div class="service-meta-grid">
|
||
<div class="summary-card">
|
||
<small>部署</small>
|
||
<strong>{{ selectedServiceDetail.deployedCount }}</strong>
|
||
</div>
|
||
<div :class="['summary-card', selectedServiceDetail.downCount > 0 ? 'tone-bad' : 'tone-ok']">
|
||
<small>异常</small>
|
||
<strong>{{ selectedServiceDetail.downCount }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>均值</small>
|
||
<strong>{{ formatLatency(selectedServiceDetail.avgLatencyMs) }}</strong>
|
||
</div>
|
||
<div v-if="densityMode === 'detail'" class="summary-card">
|
||
<small>分组</small>
|
||
<strong>{{ selectedServiceDetail.groupLabel }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-instance-list">
|
||
<article v-for="instance in selectedServiceDetail.instances" :key="instance.host" class="detail-instance">
|
||
<div class="detail-instance-top">
|
||
<div>
|
||
<strong>{{ instance.host }}</strong>
|
||
<p>{{ instance.ip }} · {{ instance.port }}</p>
|
||
</div>
|
||
<div class="detail-instance-actions">
|
||
<span :class="['badge', instance.ok ? 'ok' : 'bad']">{{ instance.ok ? 'UP' : 'DOWN' }}</span>
|
||
<button
|
||
class="refresh ghost detail-log-trigger"
|
||
@click="openServiceLog(instance)"
|
||
:disabled="serviceLog.loading && isServiceLogActive(instance)"
|
||
>
|
||
{{ serviceLog.loading && isServiceLogActive(instance) ? '读取中...' : '查看日志' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-inline">
|
||
<span>HTTP {{ instance.statusCode || '-' }}</span>
|
||
<span>耗时 {{ formatLatency(instance.latencyMs) }}</span>
|
||
</div>
|
||
|
||
<div v-if="densityMode === 'detail'" class="detail-secondary mono">{{ instance.url || '-' }}</div>
|
||
<div v-if="densityMode === 'detail'" class="detail-note">{{ instance.detail || '无返回内容' }}</div>
|
||
</article>
|
||
</div>
|
||
</template>
|
||
|
||
<div v-else class="empty-panel">当前没有服务详情</div>
|
||
</aside>
|
||
</section>
|
||
|
||
<section v-if="activeView === 'overview' && overviewBridgeAlerts.length > 0" class="panel bridge-panel">
|
||
<div class="bridge-list">
|
||
<button
|
||
v-for="item in overviewBridgeAlerts"
|
||
:key="'bridge-' + item.key"
|
||
class="bridge-item"
|
||
@click="selectService(item.service); detailPanelOpen = true"
|
||
>
|
||
<strong>{{ item.service }}</strong>
|
||
<span>{{ item.hosts.join(' / ') }}</span>
|
||
<em>{{ item.downCount }} 异常</em>
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="activeView === 'overview'" class="panel hosts-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>主机条带</h2>
|
||
<p>{{ overviewAlertHosts.length }} 台异常业务主机 · {{ overviewQuietHosts.length }} 台正常主机</p>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span class="badge neutral">主机指标正常 {{ summary.hostMetricOk }} / {{ summary.hostTotal }}</span>
|
||
<span v-if="focusedHostKey" class="badge warn mono">锁定 {{ focusedHostKey }}</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="host-sections">
|
||
<section class="host-section">
|
||
<div class="section-caption-row">
|
||
<div class="section-caption">异常主机</div>
|
||
<div class="section-caption-meta">
|
||
<span>{{ overviewAlertHosts.length }} 台</span>
|
||
<span>{{ overviewAlertHosts.filter((host) => host.hasMetricIssue).length }} 采集异常</span>
|
||
</div>
|
||
</div>
|
||
<div class="host-row-list">
|
||
<article
|
||
v-for="host in overviewAlertHosts"
|
||
:key="host.host"
|
||
:class="[
|
||
'host-strip',
|
||
host.hasAlert ? 'host-strip-alert' : '',
|
||
isHostHighlighted(host) ? 'host-strip-focused' : ''
|
||
]"
|
||
>
|
||
<div class="host-strip-main" @click="selectHost(host.host)">
|
||
<div class="host-strip-id">
|
||
<strong>{{ host.host }}</strong>
|
||
<span>{{ host.ip }} · {{ host.groupLabel }}</span>
|
||
</div>
|
||
|
||
<div class="host-inline-metrics compact-metrics">
|
||
<div
|
||
v-for="metric in hostMetricEntries(host)"
|
||
:key="host.host + '-' + metric.key"
|
||
:class="['metric-ring-chip', `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>PROC</small>
|
||
<strong>{{ host.metrics.processCount ?? '-' }}</strong>
|
||
</span>
|
||
</div>
|
||
|
||
<div :class="['host-alert-summary', `tone-${hostSummaryTone(host)}`]">
|
||
<small>{{ host.downCount > 0 ? '异常服务' : host.hasMetricIssue ? '采集状态' : '在线服务' }}</small>
|
||
<span class="host-alert-path">{{ host.downCount > 0 ? host.alertSummary : host.quietSummary }}</span>
|
||
</div>
|
||
|
||
<div class="host-inline-state">
|
||
<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>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="isHostExpanded(host.host)" class="host-strip-detail host-strip-detail-list">
|
||
<div v-if="host.hasMetricIssue" class="host-detail-line host-detail-line-warn">
|
||
<strong>主机指标</strong>
|
||
<span>{{ host.metrics.error || '采集异常' }}</span>
|
||
</div>
|
||
<button
|
||
v-for="item in host.alertItems"
|
||
:key="host.host + '-' + item.service"
|
||
class="host-detail-line"
|
||
@click.stop="selectService(item.service); activeView = 'services'"
|
||
>
|
||
<strong>{{ item.service }}</strong>
|
||
<span>{{ item.statusCode || 'ERR' }} · {{ formatLatency(item.latencyMs) }}</span>
|
||
</button>
|
||
<div v-if="!host.hasMetricIssue && host.alertItems.length === 0" class="empty-panel compact">当前主机没有异常服务</div>
|
||
</div>
|
||
|
||
<p v-if="isHostExpanded(host.host) && host.metrics.error" class="metric-error">{{ host.metrics.error }}</p>
|
||
</article>
|
||
</div>
|
||
<div v-if="overviewAlertHosts.length === 0" class="empty-panel compact">当前没有异常业务主机</div>
|
||
</section>
|
||
|
||
<section v-if="overviewQuietHosts.length > 0" class="host-section">
|
||
<button class="host-fold-toggle" @click="toggleOverviewHealthyHosts">
|
||
<span>正常主机</span>
|
||
<strong>{{ overviewQuietHosts.length }}</strong>
|
||
<small>{{ overviewHealthyHostsOpen ? '收起' : '展开' }}</small>
|
||
</button>
|
||
|
||
<div v-if="overviewHealthyHostsOpen" class="host-row-list">
|
||
<article
|
||
v-for="host in overviewQuietHosts"
|
||
:key="'quiet-' + host.host"
|
||
:class="[
|
||
'host-strip',
|
||
host.hasAlert ? 'host-strip-alert' : '',
|
||
isHostHighlighted(host) ? 'host-strip-focused' : ''
|
||
]"
|
||
>
|
||
<div class="host-strip-main" @click="selectHost(host.host)">
|
||
<div class="host-strip-id">
|
||
<strong>{{ host.host }}</strong>
|
||
<span>{{ host.ip }} · {{ host.groupLabel }}</span>
|
||
</div>
|
||
|
||
<div class="host-inline-metrics compact-metrics">
|
||
<div
|
||
v-for="metric in hostMetricEntries(host)"
|
||
:key="'quiet-' + host.host + '-' + metric.key"
|
||
:class="['metric-ring-chip', `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>PROC</small>
|
||
<strong>{{ host.metrics.processCount ?? '-' }}</strong>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="host-alert-summary tone-neutral">
|
||
<small>在线服务</small>
|
||
<span class="host-alert-path">{{ host.quietSummary }}</span>
|
||
</div>
|
||
|
||
<div class="host-inline-state">
|
||
<span class="badge neutral">{{ host.serviceTotal }} 服务</span>
|
||
<span class="badge neutral">{{ host.downCount }} 异常</span>
|
||
<span class="badge neutral">{{ host.hasMetricIssue ? '采集异常' : '指标正常' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="isHostExpanded(host.host)" class="host-strip-detail host-strip-detail-list">
|
||
<div v-if="host.hasMetricIssue" class="host-detail-line host-detail-line-warn">
|
||
<strong>主机指标</strong>
|
||
<span>{{ host.metrics.error || '采集异常' }}</span>
|
||
</div>
|
||
<div v-if="!host.hasMetricIssue && host.alertItems.length === 0" class="empty-panel compact">当前主机没有异常服务</div>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-if="activeView === 'hosts'">
|
||
<section class="panel hosts-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>主机视图</h2>
|
||
<p>{{ filteredHostRows.length }} 台主机</p>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span class="badge neutral">{{ filteredHostAlertTotal }} 台异常</span>
|
||
<span :class="['badge', filteredHostMetricIssueTotal > 0 ? 'warn' : 'neutral']">{{ filteredHostMetricIssueTotal }} 采集异常</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="host-sections">
|
||
<section v-for="section in hostRowGroups" :key="section.group" class="host-section">
|
||
<div class="section-caption-row">
|
||
<div class="section-caption">{{ section.label }}</div>
|
||
<div class="section-caption-meta">
|
||
<span>{{ section.items.length }} 台</span>
|
||
<span>{{ section.serviceCount }} 服务</span>
|
||
<span>{{ section.alertCount }} 异常主机</span>
|
||
</div>
|
||
</div>
|
||
<div class="host-row-list">
|
||
<article
|
||
v-for="host in section.items"
|
||
:key="host.host"
|
||
:class="[
|
||
'host-strip',
|
||
host.hasAlert ? 'host-strip-alert' : '',
|
||
isHostHighlighted(host) ? 'host-strip-focused' : ''
|
||
]"
|
||
>
|
||
<div class="host-strip-main" @click="selectHost(host.host)">
|
||
<div class="host-strip-id">
|
||
<strong>{{ host.host }}</strong>
|
||
<span>{{ host.ip }} · {{ host.groupLabel }} · {{ host.instanceId }}</span>
|
||
</div>
|
||
|
||
<div class="host-inline-metrics compact-metrics">
|
||
<div
|
||
v-for="metric in hostMetricEntries(host)"
|
||
:key="'host-' + host.host + '-' + metric.key"
|
||
:class="['metric-ring-chip', `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>PROC</small>
|
||
<strong>{{ host.metrics.processCount ?? '-' }}</strong>
|
||
</span>
|
||
</div>
|
||
|
||
<div :class="['host-alert-summary', `tone-${hostSummaryTone(host)}`]">
|
||
<small>{{ host.downCount > 0 ? '异常服务' : host.hasMetricIssue ? '采集状态' : '在线服务' }}</small>
|
||
<span class="host-alert-path">{{ host.downCount > 0 ? host.alertSummary : host.quietSummary }}</span>
|
||
</div>
|
||
|
||
<div class="host-inline-state">
|
||
<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>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="isHostExpanded(host.host)" class="host-strip-detail host-strip-detail-list host-strip-detail-wide">
|
||
<button
|
||
v-for="item in host.items"
|
||
:key="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 || 'ERR' }} · {{ formatLatency(item.latencyMs) }}</span>
|
||
<small v-if="densityMode === 'detail'">{{ item.url }}</small>
|
||
</button>
|
||
|
||
<div v-if="host.items.length === 0" class="empty-panel compact">当前主机没有服务实例</div>
|
||
</div>
|
||
|
||
<p v-if="isHostExpanded(host.host) && host.metrics.error" class="metric-error">{{ host.metrics.error }}</p>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-if="activeView === 'infra'">
|
||
<section class="panel task-page-head">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>基础设施</h2>
|
||
<p>健康与排障</p>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span class="badge neutral">Nacos {{ payload.nacos && payload.nacos.ok ? '在线' : '异常' }}</span>
|
||
<span class="badge neutral">Mongo {{ payload.mongo && payload.mongo.ok ? '在线' : '异常' }}</span>
|
||
<span :class="['badge', (summary.hostMetricDown || 0) > 0 ? 'warn' : 'neutral']">TAT {{ summary.hostMetricOk || 0 }} / {{ summary.hostTotal || 0 }}</span>
|
||
</div>
|
||
</header>
|
||
</section>
|
||
|
||
<section class="overview-infra-grid">
|
||
<article v-for="card in overviewInfraCards" :key="'infra-' + card.key" class="panel infra-mini-card">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>{{ card.label }}</h2>
|
||
<p>{{ card.meta }}</p>
|
||
</div>
|
||
<span :class="['badge', card.tone === 'bad' ? 'bad' : card.tone === 'warn' ? 'warn' : 'ok']">{{ card.badge }}</span>
|
||
</header>
|
||
<div class="infra-mini-metrics">
|
||
<div v-for="item in card.metrics" :key="'infra-' + card.key + '-' + item.label" class="summary-card">
|
||
<small>{{ item.label }}</small>
|
||
<strong>{{ item.value }}</strong>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
|
||
<section v-if="infraTab === 'overview'" class="panel infra-overview-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>状态总览</h2>
|
||
<p>异常前置,正常折叠</p>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span class="badge neutral">{{ infraAlertNacosServices.length }} Nacos 异常服务</span>
|
||
<span class="badge neutral">{{ tatAlertHosts.length }} TAT 异常主机</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="infra-overview-grid">
|
||
<article class="infra-focus-card">
|
||
<header class="infra-focus-head">
|
||
<strong>Nacos</strong>
|
||
<span :class="['badge', payload.nacos && payload.nacos.ok ? 'ok' : 'bad']">{{ payload.nacos && payload.nacos.ok ? '连接正常' : '连接异常' }}</span>
|
||
</header>
|
||
<div class="infra-focus-metrics">
|
||
<span>命名空间 {{ payload.nacos.summary.namespaceTotal || 0 }}</span>
|
||
<span>服务 {{ payload.nacos.summary.serviceTotal || 0 }}</span>
|
||
<span>实例 {{ payload.nacos.summary.instanceTotal || 0 }}</span>
|
||
<span>异常 {{ payload.nacos.summary.unhealthyInstanceTotal || 0 }}</span>
|
||
</div>
|
||
<div class="infra-inline-list">
|
||
<span v-if="infraAlertNacosServices.length === 0" class="badge neutral">当前无异常服务</span>
|
||
<button
|
||
v-for="service in infraAlertNacosServices.slice(0, 6)"
|
||
:key="'overview-nacos-' + service.rawServiceName"
|
||
class="infra-inline-link"
|
||
@click="infraTab = 'nacos'; toggleNacosService(service.rawServiceName)"
|
||
>
|
||
{{ service.serviceName }} · {{ service.unhealthyTotal }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="infra-focus-card">
|
||
<header class="infra-focus-head">
|
||
<strong>Mongo</strong>
|
||
<span :class="['badge', payload.mongo && payload.mongo.ok ? 'ok' : 'bad']">{{ payload.mongo && payload.mongo.ok ? '连接正常' : '连接异常' }}</span>
|
||
</header>
|
||
<div class="infra-focus-metrics">
|
||
<span>连接 {{ formatNumber(payload.mongo.connections.current) }}</span>
|
||
<span>数据库 {{ formatNumber(payload.mongo.summary.databaseTotal) }}</span>
|
||
<span>集合 {{ formatNumber(payload.mongo.summary.collectionTotal) }}</span>
|
||
<span>流量 {{ formatBytes(payload.mongo.network.bytesOut) }}</span>
|
||
</div>
|
||
<button class="refresh ghost" @click="infraTab = 'mongo'">查看 Mongo 指标</button>
|
||
</article>
|
||
|
||
<article class="infra-focus-card">
|
||
<header class="infra-focus-head">
|
||
<strong>TAT</strong>
|
||
<span :class="['badge', (summary.hostMetricDown || 0) > 0 ? 'warn' : 'ok']">{{ (summary.hostMetricDown || 0) > 0 ? '采集异常' : '采集正常' }}</span>
|
||
</header>
|
||
<div class="infra-focus-metrics">
|
||
<span>主机 {{ summary.hostTotal || 0 }}</span>
|
||
<span>正常 {{ summary.hostMetricOk || 0 }}</span>
|
||
<span>异常 {{ summary.hostMetricDown || 0 }}</span>
|
||
<span>服务异常 {{ hostServiceAlertTotal }}</span>
|
||
</div>
|
||
<div class="infra-inline-list">
|
||
<span v-if="tatAlertHosts.length === 0" class="badge neutral">当前无采集异常</span>
|
||
<button
|
||
v-for="host in tatAlertHosts.slice(0, 6)"
|
||
:key="'overview-tat-' + host.host"
|
||
class="infra-inline-link"
|
||
@click="infraTab = 'tat'"
|
||
>
|
||
{{ host.host }} · {{ host.metrics.error || '采集异常' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="infraTab === 'nacos'" class="panel infra-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>Nacos 服务</h2>
|
||
<p>{{ payload.nacos.baseUrl || '-' }} · {{ payload.nacos.namespaceName || 'default' }} · {{ truncateMiddle(payload.nacos.namespaceId, 10, 8) || '-' }}</p>
|
||
</div>
|
||
<span :class="['badge', payload.nacos && payload.nacos.ok ? 'ok' : 'bad']">
|
||
{{ payload.nacos && payload.nacos.ok ? '连接正常' : '连接异常' }}
|
||
</span>
|
||
</header>
|
||
|
||
<div class="metadata-strip">
|
||
<span>命名空间 {{ payload.nacos.summary.namespaceTotal || 0 }}</span>
|
||
<span>服务 {{ payload.nacos.summary.serviceTotal || 0 }}</span>
|
||
<span>实例 {{ payload.nacos.summary.instanceTotal || 0 }}</span>
|
||
<span :class="(payload.nacos.summary.unhealthyInstanceTotal || 0) > 0 ? 'bad-text' : 'ok-text'">异常 {{ payload.nacos.summary.unhealthyInstanceTotal || 0 }}</span>
|
||
</div>
|
||
|
||
<p v-if="payload.nacos.error" class="metric-error">{{ explainError(payload.nacos.error) }}</p>
|
||
|
||
<div class="table-wrap">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>服务</th>
|
||
<th>命名空间</th>
|
||
<th>分组</th>
|
||
<th>实例数</th>
|
||
<th>健康数</th>
|
||
<th>展开</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template v-for="service in infraAlertNacosServices" :key="'nacos-alert-' + service.rawServiceName">
|
||
<tr class="nacos-summary-row is-alert">
|
||
<td>{{ service.serviceName }}</td>
|
||
<td>{{ service.namespaceName || 'public' }}</td>
|
||
<td>{{ service.groupName }}</td>
|
||
<td>{{ service.instanceTotal }}</td>
|
||
<td :class="service.unhealthyTotal > 0 ? 'bad-text' : 'ok-text'">{{ service.healthyTotal }}</td>
|
||
<td><button class="refresh ghost inline-button" @click="toggleNacosService(service.rawServiceName)">{{ isNacosServiceExpanded(service.rawServiceName) ? '收起' : '展开' }}</button></td>
|
||
</tr>
|
||
<tr v-if="isNacosServiceExpanded(service.rawServiceName)">
|
||
<td colspan="6" class="nacos-service-detail-cell">
|
||
<div class="table-wrap compact">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>IP</th>
|
||
<th>端口</th>
|
||
<th>集群</th>
|
||
<th>健康</th>
|
||
<th>启用</th>
|
||
<th>权重</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="instance in service.instances" :key="service.rawServiceName + '-' + instance.ip + '-' + instance.port">
|
||
<td class="mono">{{ instance.ip }}</td>
|
||
<td>{{ instance.port }}</td>
|
||
<td>{{ instance.clusterName }}</td>
|
||
<td :class="instance.healthy ? 'ok-text' : 'bad-text'">{{ instance.healthy ? 'Y' : 'N' }}</td>
|
||
<td :class="instance.enabled ? 'ok-text' : 'bad-text'">{{ instance.enabled ? 'Y' : 'N' }}</td>
|
||
<td>{{ instance.weight }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
|
||
<tr v-if="infraAlertNacosServices.length === 0">
|
||
<td colspan="6" class="table-empty-cell">当前没有异常 Nacos 服务</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<section v-if="infraHealthyNacosServices.length > 0" class="fold-group">
|
||
<button class="host-fold-toggle" @click="infraHealthyNacosOpen = !infraHealthyNacosOpen">
|
||
<span>全部正常服务</span>
|
||
<strong>{{ infraHealthyNacosServices.length }}</strong>
|
||
<small>{{ infraHealthyNacosOpen ? '收起' : '展开' }}</small>
|
||
</button>
|
||
|
||
<div v-if="infraHealthyNacosOpen" class="table-wrap">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>服务</th>
|
||
<th>命名空间</th>
|
||
<th>分组</th>
|
||
<th>实例数</th>
|
||
<th>健康数</th>
|
||
<th>展开</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template v-for="service in infraVisibleHealthyNacosServices" :key="'nacos-ok-' + service.rawServiceName">
|
||
<tr class="nacos-summary-row">
|
||
<td>{{ service.serviceName }}</td>
|
||
<td>{{ service.namespaceName || 'public' }}</td>
|
||
<td>{{ service.groupName }}</td>
|
||
<td>{{ service.instanceTotal }}</td>
|
||
<td class="ok-text">{{ service.healthyTotal }}</td>
|
||
<td><button class="refresh ghost inline-button" @click="toggleNacosService(service.rawServiceName)">{{ isNacosServiceExpanded(service.rawServiceName) ? '收起' : '展开' }}</button></td>
|
||
</tr>
|
||
<tr v-if="isNacosServiceExpanded(service.rawServiceName)">
|
||
<td colspan="6" class="nacos-service-detail-cell">
|
||
<div class="table-wrap compact">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>IP</th>
|
||
<th>端口</th>
|
||
<th>集群</th>
|
||
<th>健康</th>
|
||
<th>启用</th>
|
||
<th>权重</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="instance in service.instances" :key="service.rawServiceName + '-healthy-' + instance.ip + '-' + instance.port">
|
||
<td class="mono">{{ instance.ip }}</td>
|
||
<td>{{ instance.port }}</td>
|
||
<td>{{ instance.clusterName }}</td>
|
||
<td :class="instance.healthy ? 'ok-text' : 'bad-text'">{{ instance.healthy ? 'Y' : 'N' }}</td>
|
||
<td :class="instance.enabled ? 'ok-text' : 'bad-text'">{{ instance.enabled ? 'Y' : 'N' }}</td>
|
||
<td>{{ instance.weight }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
|
||
<section v-if="infraTab === 'mongo'" class="panel infra-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>MongoDB</h2>
|
||
<p>{{ payload.mongo.info.host || '-' }} · {{ payload.mongo.info.version || '-' }} · {{ payload.mongo.info.process || '-' }}</p>
|
||
</div>
|
||
<span :class="['badge', payload.mongo && payload.mongo.ok ? 'ok' : 'bad']">
|
||
{{ payload.mongo && payload.mongo.ok ? '连接正常' : '连接异常' }}
|
||
</span>
|
||
</header>
|
||
|
||
<div class="mongo-group-grid">
|
||
<article class="mongo-stat-group">
|
||
<header class="infra-focus-head">
|
||
<strong>连接</strong>
|
||
<span class="badge neutral">当前 {{ formatNumber(payload.mongo.connections.current) }}</span>
|
||
</header>
|
||
<div class="infra-focus-metrics">
|
||
<span>运行时长 {{ formatDuration(payload.mongo.info.uptimeSeconds) }}</span>
|
||
<span>请求 {{ formatNumber(payload.mongo.network.numRequests) }}</span>
|
||
<span>副本 {{ payload.mongo.replicaSet && payload.mongo.replicaSet.configured ? payload.mongo.replicaSet.members.length : 0 }}</span>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="mongo-stat-group">
|
||
<header class="infra-focus-head">
|
||
<strong>容量</strong>
|
||
<span class="badge neutral">数据库 {{ formatNumber(payload.mongo.summary.databaseTotal) }}</span>
|
||
</header>
|
||
<div class="infra-focus-metrics">
|
||
<span>集合 {{ formatNumber(payload.mongo.summary.collectionTotal) }}</span>
|
||
<span>对象 {{ formatNumber(payload.mongo.summary.objectTotal) }}</span>
|
||
<span>数据量 {{ formatMb(payload.mongo.summary.dataSizeMb) }}</span>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="mongo-stat-group">
|
||
<header class="infra-focus-head">
|
||
<strong>流量</strong>
|
||
<span class="badge neutral">Bytes Out {{ formatBytes(payload.mongo.network.bytesOut) }}</span>
|
||
</header>
|
||
<div class="infra-focus-metrics">
|
||
<span>Bytes In {{ formatBytes(payload.mongo.network.bytesIn) }}</span>
|
||
<span>WT Cache {{ formatBytes(payload.mongo.wiredTigerCache.bytesCurrentlyInCache) }}</span>
|
||
<span>Resident {{ formatMb(payload.mongo.memory.residentMb) }}</span>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
|
||
<p v-if="payload.mongo.error" class="metric-error">{{ payload.mongo.error }}</p>
|
||
|
||
<section class="fold-group">
|
||
<button class="host-fold-toggle" @click="infraMongoDetailsOpen = !infraMongoDetailsOpen">
|
||
<span>次要指标与明细</span>
|
||
<strong>{{ payload.mongo.databases.length }}</strong>
|
||
<small>{{ infraMongoDetailsOpen ? '收起' : '展开' }}</small>
|
||
</button>
|
||
|
||
<div v-if="infraMongoDetailsOpen" class="infra-detail-stack">
|
||
<div class="metric-grid">
|
||
<div class="summary-card">
|
||
<small>Virtual</small>
|
||
<strong>{{ formatMb(payload.mongo.memory.virtualMb) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>Resident</small>
|
||
<strong>{{ formatMb(payload.mongo.memory.residentMb) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>Requests</small>
|
||
<strong>{{ formatNumber(payload.mongo.network.numRequests) }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-wrap">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>数据库</th>
|
||
<th>集合</th>
|
||
<th>对象</th>
|
||
<th>数据量</th>
|
||
<th>存储量</th>
|
||
<th>索引数</th>
|
||
<th>索引量</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="db in filteredMongoDatabases" :key="db.name">
|
||
<td>{{ db.name }}</td>
|
||
<td>{{ formatNumber(db.collections) }}</td>
|
||
<td>{{ formatNumber(db.objects) }}</td>
|
||
<td>{{ formatMb(db.dataSizeMb) }}</td>
|
||
<td>{{ formatMb(db.storageSizeMb) }}</td>
|
||
<td>{{ formatNumber(db.indexCount) }}</td>
|
||
<td>{{ formatMb(db.indexSizeMb) }}</td>
|
||
</tr>
|
||
<tr v-if="filteredMongoDatabases.length === 0">
|
||
<td colspan="7" class="table-empty-cell">当前没有匹配的数据库</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-if="payload.mongo.replicaSet && payload.mongo.replicaSet.configured" class="table-wrap">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>成员</th>
|
||
<th>状态</th>
|
||
<th>健康</th>
|
||
<th>运行时长</th>
|
||
<th>复制延迟</th>
|
||
<th>最后 Optime</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="member in payload.mongo.replicaSet.members" :key="member.name">
|
||
<td class="mono">{{ member.name }}</td>
|
||
<td>{{ member.stateStr }}</td>
|
||
<td :class="member.health >= 1 ? 'ok-text' : 'bad-text'">{{ member.health }}</td>
|
||
<td>{{ formatDuration(member.uptime) }}</td>
|
||
<td>{{ member.lagSeconds == null ? '-' : `${member.lagSeconds}s` }}</td>
|
||
<td>{{ formatDateTime(member.optimeDate) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
|
||
<section v-if="infraTab === 'tat'" class="panel infra-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>TAT 主机采集</h2>
|
||
<p>主机资源与采集状态</p>
|
||
</div>
|
||
<span :class="['badge', (summary.hostMetricDown || 0) > 0 ? 'warn' : 'ok']">{{ (summary.hostMetricDown || 0) > 0 ? '采集异常' : '采集正常' }}</span>
|
||
</header>
|
||
|
||
<div class="metadata-strip">
|
||
<span>主机 {{ summary.hostTotal || 0 }}</span>
|
||
<span>正常 {{ summary.hostMetricOk || 0 }}</span>
|
||
<span :class="(summary.hostMetricDown || 0) > 0 ? 'bad-text' : 'ok-text'">异常 {{ summary.hostMetricDown || 0 }}</span>
|
||
</div>
|
||
|
||
<div class="table-wrap">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>主机</th>
|
||
<th>IP</th>
|
||
<th>节点</th>
|
||
<th>CPU</th>
|
||
<th>MEM</th>
|
||
<th>DISK</th>
|
||
<th>PROC</th>
|
||
<th>采集</th>
|
||
<th>服务异常</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="host in tatAlertHosts" :key="'tat-alert-' + host.host">
|
||
<td>{{ host.host }}</td>
|
||
<td class="mono">{{ host.ip }}</td>
|
||
<td>{{ host.groupLabel }}</td>
|
||
<td>{{ formatPercent(host.metrics.cpuPercent) }}</td>
|
||
<td>{{ formatPercent(host.metrics.memoryPercent) }}</td>
|
||
<td>{{ formatPercent(host.metrics.diskPercent) }}</td>
|
||
<td>{{ host.metrics.processCount ?? '-' }}</td>
|
||
<td class="bad-text">{{ host.metrics.error || '异常' }}</td>
|
||
<td>{{ host.downCount }}</td>
|
||
</tr>
|
||
<tr v-if="tatAlertHosts.length === 0">
|
||
<td colspan="9" class="table-empty-cell">当前没有采集异常主机</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<section v-if="tatHealthyHosts.length > 0" class="fold-group">
|
||
<button class="host-fold-toggle" @click="infraHealthyTatOpen = !infraHealthyTatOpen">
|
||
<span>全部正常主机</span>
|
||
<strong>{{ tatHealthyHosts.length }}</strong>
|
||
<small>{{ infraHealthyTatOpen ? '收起' : '展开' }}</small>
|
||
</button>
|
||
|
||
<div v-if="infraHealthyTatOpen" class="table-wrap">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>主机</th>
|
||
<th>IP</th>
|
||
<th>节点</th>
|
||
<th>CPU</th>
|
||
<th>MEM</th>
|
||
<th>DISK</th>
|
||
<th>PROC</th>
|
||
<th>采集</th>
|
||
<th>服务异常</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="host in visibleTatHealthyHosts" :key="'tat-ok-' + host.host">
|
||
<td>{{ host.host }}</td>
|
||
<td class="mono">{{ host.ip }}</td>
|
||
<td>{{ host.groupLabel }}</td>
|
||
<td>{{ formatPercent(host.metrics.cpuPercent) }}</td>
|
||
<td>{{ formatPercent(host.metrics.memoryPercent) }}</td>
|
||
<td>{{ formatPercent(host.metrics.diskPercent) }}</td>
|
||
<td>{{ host.metrics.processCount ?? '-' }}</td>
|
||
<td class="ok-text">正常</td>
|
||
<td>{{ host.downCount }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-if="activeView === 'config'">
|
||
<section class="panel task-page-head">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>配置</h2>
|
||
<p>{{ activeConfigTabLabel }} · 编辑与校验</p>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span :class="['badge', nacosConfig.error || nacosConfig.detailError || goConfig.error ? 'bad' : 'ok']">
|
||
{{ nacosConfig.error || nacosConfig.detailError || goConfig.error ? '存在错误' : '可操作' }}
|
||
</span>
|
||
<span class="badge neutral">本地记录 {{ configHistory.length }}</span>
|
||
</div>
|
||
</header>
|
||
</section>
|
||
|
||
<section v-if="configTab === 'nacos'" class="panel infra-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>Nacos 配置</h2>
|
||
<p>{{ nacosConfig.namespaceName || 'default' }} · {{ truncateMiddle(nacosConfig.namespaceId, 10, 8) || '-' }}</p>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span class="badge neutral">{{ filteredNacosConfigs.length }} 项</span>
|
||
<span :class="['badge', nacosConfig.error || nacosConfig.detailError ? 'bad' : 'ok']">{{ nacosConfig.error || nacosConfig.detailError ? '存在错误' : '已连接' }}</span>
|
||
</div>
|
||
</header>
|
||
|
||
<p v-if="nacosConfig.error" class="metric-error">{{ explainError(nacosConfig.error) }}</p>
|
||
<p v-if="nacosConfig.detailError" class="metric-error">{{ explainError(nacosConfig.detailError) }}</p>
|
||
|
||
<div class="config-layout">
|
||
<aside class="config-sidebar">
|
||
<div class="config-sidebar-head">
|
||
<div class="config-sidebar-meta">
|
||
<span class="badge neutral mono">{{ truncateMiddle(nacosConfig.baseUrl, 16, 12) || '-' }}</span>
|
||
<span class="badge neutral">{{ nacosConfigGroups.length }} 组</span>
|
||
</div>
|
||
<div class="config-sidebar-path" v-if="nacosConfig.editor.dataId">
|
||
<strong>{{ nacosConfig.editor.dataId }}</strong>
|
||
<span>{{ nacosConfig.editor.group }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="config-list config-list-ide">
|
||
<section v-for="group in nacosConfigGroups" :key="group.key" class="config-group">
|
||
<button class="config-group-toggle" @click="toggleConfigGroup(group.key)">
|
||
<strong>{{ group.label }}</strong>
|
||
<span>{{ group.items.length }} 项</span>
|
||
<small>{{ isConfigGroupCollapsed(group.key) ? '展开' : '收起' }}</small>
|
||
</button>
|
||
|
||
<div v-if="!isConfigGroupCollapsed(group.key)" class="config-group-items">
|
||
<button
|
||
v-for="item in group.items"
|
||
:key="configKey(item.group, item.dataId)"
|
||
:id="`config-item-${configKey(item.group, item.dataId)}`"
|
||
:class="['config-item', 'config-item-ide', configKey(item.group, item.dataId) === nacosConfig.selectedKey ? 'active' : '']"
|
||
@click="openNacosConfig(item)"
|
||
>
|
||
<div class="config-item-main">
|
||
<strong>{{ item.dataId }}</strong>
|
||
<small v-if="item.appName">{{ item.appName }}</small>
|
||
</div>
|
||
<div class="config-item-side">
|
||
<span>{{ item.type || 'text' }}</span>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<div v-if="!nacosConfig.loading && filteredNacosConfigs.length === 0" class="config-empty">
|
||
当前没有可展示的配置项
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<section class="config-editor">
|
||
<template v-if="nacosConfig.editor.dataId">
|
||
<div class="editor-toolbar sticky-toolbar">
|
||
<div class="editor-toolbar-main">
|
||
<span v-if="nacosConfig.saveMessage" class="badge ok">{{ nacosConfig.saveMessage }}</span>
|
||
<span v-else-if="nacosConfig.validationMessage" class="badge ok">{{ nacosConfig.validationMessage }}</span>
|
||
<span v-else-if="nacosConfig.detailLoading" class="badge neutral">配置加载中</span>
|
||
<span v-else-if="nacosConfigDirty" class="badge warn">有未保存修改</span>
|
||
<span v-else class="badge neutral">内容已同步</span>
|
||
|
||
<div class="density-switch">
|
||
<button :class="['filter-chip', configEditorMode === 'diff' ? 'active' : '']" @click="configEditorMode = 'diff'">Diff</button>
|
||
<button :class="['filter-chip', configEditorMode === 'source' ? 'active' : '']" @click="configEditorMode = 'source'">源码</button>
|
||
</div>
|
||
|
||
<div class="config-jump-group">
|
||
<button class="refresh ghost" @click="openPrevNacosConfig" :disabled="!selectedPrevNacosConfigKey || nacosConfig.detailLoading || nacosConfig.saving">上一项</button>
|
||
<button class="refresh ghost" @click="openNextNacosConfig" :disabled="!selectedNextNacosConfigKey || nacosConfig.detailLoading || nacosConfig.saving">下一项</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="validateNacosConfig" :disabled="nacosConfig.validating || nacosConfig.saving || nacosConfig.detailLoading">
|
||
{{ nacosConfig.validating ? '校验中...' : '校验' }}
|
||
</button>
|
||
<button class="refresh ghost" @click="restoreNacosContent" :disabled="!nacosConfigDirty || nacosConfig.saving || nacosConfig.detailLoading">恢复</button>
|
||
<button class="refresh" @click="saveNacosConfig" :disabled="nacosConfig.saving || nacosConfig.detailLoading">
|
||
{{ nacosConfig.saving ? '保存中...' : '保存' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="metadata-strip">
|
||
<span class="mono">dataId {{ nacosConfig.editor.dataId }}</span>
|
||
<span class="mono">group {{ nacosConfig.editor.group }}</span>
|
||
<span>type {{ nacosConfig.editor.type || 'text' }}</span>
|
||
<span>最近修改 {{ formatDateTime(nacosConfig.editor.lastModifiedTime) }}</span>
|
||
<span>{{ nacosConfig.validationMessage ? '已校验' : '未校验' }}</span>
|
||
</div>
|
||
|
||
<p v-if="nacosConfig.editor.description || nacosConfig.editor.appName || nacosConfig.editor.md5" class="config-description">
|
||
<span v-if="nacosConfig.editor.appName">appName {{ nacosConfig.editor.appName }}</span>
|
||
<span v-if="nacosConfig.editor.description">{{ nacosConfig.editor.description }}</span>
|
||
<span v-if="nacosConfig.editor.md5" class="mono">{{ truncateMiddle(nacosConfig.editor.md5, 8, 8) }}</span>
|
||
</p>
|
||
|
||
<section v-if="configEditorMode === 'diff'" class="diff-panel">
|
||
<div v-if="showNacosDiffEmpty" class="config-empty">当前没有变更,切到源码继续编辑</div>
|
||
<div v-else class="diff-list mono">
|
||
<div v-for="row in currentNacosDiffRows" :key="row.key" :class="['diff-row', row.type === 'add' ? 'is-add' : 'is-remove']">
|
||
<span class="diff-gutter">{{ row.leftNo || '-' }}</span>
|
||
<span class="diff-gutter">{{ row.rightNo || '-' }}</span>
|
||
<span class="diff-mark">{{ row.type === 'add' ? '+' : '-' }}</span>
|
||
<code>{{ row.text || ' ' }}</code>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<textarea
|
||
v-else
|
||
v-model="nacosConfig.editor.content"
|
||
class="config-textarea mono"
|
||
spellcheck="false"
|
||
:disabled="nacosConfig.detailLoading || nacosConfig.saving"
|
||
></textarea>
|
||
</template>
|
||
|
||
<div v-else class="config-empty">
|
||
请选择左侧配置项
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="configTab === 'go'" class="panel infra-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>Go Env</h2>
|
||
<p>app 节点 service.env</p>
|
||
</div>
|
||
<span :class="['badge', goConfig.error ? 'bad' : goConfig.diffKeys.length > 0 ? 'warn' : 'ok']">
|
||
{{ goConfig.error ? '存在错误' : goConfig.diffKeys.length > 0 ? '双机有差异' : '已同步' }}
|
||
</span>
|
||
</header>
|
||
|
||
<div class="metadata-strip">
|
||
<span v-for="host in goConfig.hosts" :key="host.host" class="mono">{{ host.host }} {{ truncateMiddle(host.path, 14, 10) }}</span>
|
||
</div>
|
||
|
||
<p v-if="goConfig.error" class="metric-error">{{ explainError(goConfig.error) }}</p>
|
||
<p v-if="goConfig.diffKeys.length > 0" class="metric-error">差异 key:{{ goConfig.diffKeys.join(', ') }}</p>
|
||
|
||
<section class="config-editor">
|
||
<div class="editor-toolbar sticky-toolbar">
|
||
<div class="editor-toolbar-main">
|
||
<span v-if="goConfig.saveMessage" class="badge ok">{{ goConfig.saveMessage }}</span>
|
||
<span v-else-if="goConfig.validationMessage" class="badge ok">{{ goConfig.validationMessage }}</span>
|
||
<span v-else-if="goConfigDirty" class="badge warn">有未保存修改</span>
|
||
<span v-else class="badge neutral">内容已同步</span>
|
||
|
||
<div class="density-switch">
|
||
<button :class="['filter-chip', goEditorMode === 'diff' ? 'active' : '']" @click="goEditorMode = 'diff'">Diff</button>
|
||
<button :class="['filter-chip', goEditorMode === 'source' ? 'active' : '']" @click="goEditorMode = 'source'">源码</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="validateGoConfig" :disabled="goConfig.validating || goConfig.saving || goConfig.loading">
|
||
{{ goConfig.validating ? '校验中...' : '校验' }}
|
||
</button>
|
||
<button class="refresh ghost" @click="restoreGoContent" :disabled="!goConfigDirty || goConfig.loading || goConfig.saving">恢复</button>
|
||
<button class="refresh" @click="saveGoConfig" :disabled="goConfig.saving || goConfig.loading">
|
||
{{ goConfig.saving ? '保存中...' : '保存' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="metadata-strip">
|
||
<span>主机 {{ goConfig.hosts.length }}</span>
|
||
<span>差异 key {{ goConfig.diffKeys.length }}</span>
|
||
<span>基线 key {{ goConfig.hosts.length > 0 ? '已加载' : '未加载' }}</span>
|
||
</div>
|
||
|
||
<section v-if="goEditorMode === 'diff'" class="diff-panel">
|
||
<div v-if="showGoDiffEmpty" class="config-empty">当前没有变更,切到源码继续编辑</div>
|
||
<div v-else class="diff-list mono">
|
||
<div v-for="row in currentGoDiffRows" :key="row.key" :class="['diff-row', row.type === 'add' ? 'is-add' : 'is-remove']">
|
||
<span class="diff-gutter">{{ row.leftNo || '-' }}</span>
|
||
<span class="diff-gutter">{{ row.rightNo || '-' }}</span>
|
||
<span class="diff-mark">{{ row.type === 'add' ? '+' : '-' }}</span>
|
||
<code>{{ row.text || ' ' }}</code>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<textarea
|
||
v-else
|
||
v-model="goConfig.editorContent"
|
||
class="config-textarea mono"
|
||
spellcheck="false"
|
||
:disabled="goConfig.loading || goConfig.saving"
|
||
></textarea>
|
||
</section>
|
||
</section>
|
||
|
||
<section v-if="configTab === 'history'" class="panel infra-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>历史版本</h2>
|
||
<p>当前接口未返回远端版本历史,先保留本地操作记录</p>
|
||
</div>
|
||
<span class="badge neutral">本地记录</span>
|
||
</header>
|
||
|
||
<div class="activity-list">
|
||
<article v-for="item in configHistoryRows" :key="item.id" :class="['activity-item', `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 v-if="configHistoryRows.length === 0" class="config-empty">当前没有可展示的记录</div>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-if="activeView === 'release'">
|
||
<section class="panel task-page-head">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>发布</h2>
|
||
<p>变更与执行</p>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span :class="['badge', updateStatusBadgeClass()]">{{ updateStatusLabel() }}</span>
|
||
<span class="badge neutral">已选 {{ updateOps.selectedServices.length }} 项更新</span>
|
||
<span class="badge neutral">已选 {{ restartOps.selectedServices.length }} 项重启</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="release-action-bar">
|
||
<div class="release-action-summary">
|
||
<span class="badge neutral">更新目标 {{ releaseTargetRows.length }}</span>
|
||
<span class="badge neutral">重启目标 {{ restartTargetRows.length }}</span>
|
||
<span :class="['badge', updateOps.error || restartOps.error ? 'bad' : releasePrecheck.ok ? 'ok' : 'neutral']">
|
||
{{ releasePrecheck.ranAt ? releasePrecheck.title : '未预检查' }}
|
||
</span>
|
||
</div>
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="refreshReleaseTargets" :disabled="updateOps.loading || restartOps.loading || updateOps.running || restartOps.running">
|
||
{{ updateOps.loading || restartOps.loading ? '刷新中...' : '刷新目标' }}
|
||
</button>
|
||
<button class="refresh ghost" @click="runReleasePrecheck" :disabled="updateOps.running || restartOps.running">预检查</button>
|
||
<button class="refresh ghost" @click="openUpdateLogViewer" :disabled="!updateOps.operationId && !updateOps.logs.length">
|
||
查看日志
|
||
</button>
|
||
<button class="refresh danger" @click="runServiceUpdate" :disabled="updateOps.running || updateOps.selectedServices.length === 0">
|
||
{{ updateOps.running ? '更新中...' : `执行更新 ${updateOps.selectedServices.length}` }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel release-status-strip sticky-release-strip">
|
||
<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>
|
||
|
||
<div v-if="releasePrecheck.ranAt" class="precheck-strip">
|
||
<span :class="['badge', releasePrecheck.tone]">{{ releasePrecheck.title }}</span>
|
||
<span class="badge neutral">执行于 {{ formatDateTime(releasePrecheck.ranAt) }}</span>
|
||
<span
|
||
v-for="check in releasePrecheck.checks"
|
||
:key="'precheck-' + check.label"
|
||
:class="['badge', check.ok ? 'ok' : 'warn']"
|
||
:title="check.detail"
|
||
>
|
||
{{ check.label }}
|
||
</span>
|
||
</div>
|
||
|
||
<p v-if="updateOps.error" class="metric-error">{{ explainError(updateOps.error) }}</p>
|
||
<p v-else-if="updateOps.message" :class="updateOps.result && updateOps.result.ok ? 'ok-text' : 'metric-error'">{{ updateOps.message }}</p>
|
||
</section>
|
||
|
||
<section id="release-build" class="panel infra-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>构建参数</h2>
|
||
<p>先定参数,再选目标</p>
|
||
</div>
|
||
<span class="badge neutral">平台 {{ updateOps.defaults.dockerPlatform }}</span>
|
||
</header>
|
||
|
||
<div class="metadata-strip">
|
||
<span class="mono">Harbor {{ updateOps.builder.harborRegistry }} / {{ updateOps.builder.harborProject }}</span>
|
||
<span class="mono">Java {{ truncateMiddle(updateOps.builder.javaRepoRoot, 16, 12) }}</span>
|
||
<span class="mono">Go {{ truncateMiddle(updateOps.builder.golangRepoRoot, 16, 12) }}</span>
|
||
<span class="mono">Admin {{ truncateMiddle(updateOps.builder.frontendRepoRoot || updateOps.builder.frontendRepoUrl, 16, 12) }}</span>
|
||
</div>
|
||
|
||
<div class="update-form-grid">
|
||
<label class="update-field">
|
||
<span>Java Git Ref</span>
|
||
<input v-model.trim="updateOps.javaGitRef" class="search" placeholder="main / branch / tag / commit" />
|
||
</label>
|
||
<label class="update-field">
|
||
<span>Go Git Ref</span>
|
||
<input v-model.trim="updateOps.goGitRef" class="search" placeholder="main / branch / tag / commit" />
|
||
</label>
|
||
<label class="update-field">
|
||
<span>Admin Git Ref</span>
|
||
<input v-model.trim="updateOps.adminGitRef" class="search" placeholder="main / branch / tag / commit" />
|
||
</label>
|
||
<label class="update-field">
|
||
<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>
|
||
</section>
|
||
|
||
<section id="release-targets" class="panel infra-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>更新目标</h2>
|
||
<p>紧凑表格</p>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span class="badge neutral">{{ releaseTargetRows.length }} 项</span>
|
||
<span class="badge neutral">已选 {{ updateOps.selectedServices.length }} 项</span>
|
||
</div>
|
||
</header>
|
||
|
||
<p v-if="updateOps.error" class="metric-error">{{ explainError(updateOps.error) }}</p>
|
||
|
||
<div class="table-wrap">
|
||
<table class="table compact release-table">
|
||
<thead>
|
||
<tr>
|
||
<th>服务</th>
|
||
<th>语言</th>
|
||
<th>部署节点</th>
|
||
<th>当前版本</th>
|
||
<th>明细</th>
|
||
<th>选择</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="item in releaseTargetRows" :key="'update-' + item.service">
|
||
<td>
|
||
<strong>{{ item.service }}</strong>
|
||
<small class="table-subtext mono" :title="item.repository">{{ item.repositoryShort }}</small>
|
||
</td>
|
||
<td>{{ item.kind }}</td>
|
||
<td :title="item.hostsLabel">{{ item.hostsLabel }}</td>
|
||
<td :title="item.currentTitle">{{ item.currentLabel }}</td>
|
||
<td>
|
||
<div class="inline-copy">
|
||
<span class="mono" :title="item.detailTitle || '-'">{{ item.detailLabel || '-' }}</span>
|
||
<button
|
||
v-if="item.copyValue"
|
||
class="refresh ghost inline-button"
|
||
@click="copyText(item.copyValue, 'image-' + item.service)"
|
||
>
|
||
{{ isCopied('image-' + item.service) ? '已复制' : '复制' }}
|
||
</button>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<label class="table-check">
|
||
<input
|
||
:checked="updateOps.selectedServices.includes(item.service)"
|
||
type="checkbox"
|
||
@change="toggleUpdateService(item.service)"
|
||
/>
|
||
<span>{{ updateOps.selectedServices.includes(item.service) ? '已选' : '选择' }}</span>
|
||
</label>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="!updateOps.loading && releaseTargetRows.length === 0">
|
||
<td colspan="6" class="table-empty-cell">当前没有可展示的更新目标</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="release-log" class="panel infra-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>执行日志</h2>
|
||
<p>页内只看摘要,完整日志弹出</p>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span v-if="updateOps.operationId" class="badge neutral mono">任务 {{ updateOps.operationId }}</span>
|
||
<span class="badge neutral mono">阶段 {{ updateOps.stage || '-' }}</span>
|
||
<button class="refresh ghost" @click="openUpdateLogViewer" :disabled="!updateOps.operationId && !updateOps.logs.length">查看日志</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="metadata-strip">
|
||
<span>开始 {{ formatDateTime(updateOps.startedAt) }}</span>
|
||
<span>结束 {{ formatDateTime(updateOps.finishedAt) }}</span>
|
||
<span>状态 {{ updateStatusLabel() }}</span>
|
||
<span>影响服务 {{ updateAffectedServicesCount }}</span>
|
||
</div>
|
||
|
||
<section class="release-log-preview">
|
||
<div class="release-log-preview-head">
|
||
<strong>{{ updateOps.logs.length }} 条日志</strong>
|
||
<span v-if="updateOps.logDroppedCount > 0" class="badge warn">截断 {{ updateOps.logDroppedCount }}</span>
|
||
</div>
|
||
<div v-if="recentUpdateLogs.length > 0" class="release-log-snippets">
|
||
<article v-for="entry in recentUpdateLogs" :key="entry.key" :class="['release-log-snippet', entry.level === 'error' ? 'tone-bad' : 'tone-neutral']">
|
||
<strong>{{ formatDateTime(entry.time) }}</strong>
|
||
<span>{{ entry.stage || '-' }}</span>
|
||
<p>{{ entry.message || '-' }}</p>
|
||
</article>
|
||
</div>
|
||
<div v-else class="empty-panel compact">当前还没有可展示的执行日志</div>
|
||
</section>
|
||
|
||
<div class="activity-list compact" 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>
|
||
|
||
<div v-if="(updateOps.result.build || updateOps.result.builds) && (updateOps.result.build || updateOps.result.builds).length > 0" class="table-wrap">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>仓库类型</th>
|
||
<th>Git Ref</th>
|
||
<th>Commit</th>
|
||
<th>版本</th>
|
||
<th>服务</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="build in (updateOps.result.build || updateOps.result.builds)" :key="build.kind + '-' + build.commit">
|
||
<td>{{ build.kind }}</td>
|
||
<td class="mono">{{ build.gitRef }}</td>
|
||
<td class="mono">{{ truncateMiddle(build.commit, 10, 6) }}</td>
|
||
<td class="mono">{{ build.imageTag || '-' }}</td>
|
||
<td>{{ (build.services || []).join(', ') }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-if="updateOps.result && updateOps.result.preparedHosts && updateOps.result.preparedHosts.length > 0" class="table-wrap">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>主机</th>
|
||
<th>服务</th>
|
||
<th>Compose</th>
|
||
<th>预拉镜像</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="item in updateOps.result.preparedHosts" :key="item.host">
|
||
<td>{{ item.host }}</td>
|
||
<td>{{ (item.services || []).join(', ') }}</td>
|
||
<td>{{ item.compose && item.compose.changed ? '已更新' : '未变化' }}</td>
|
||
<td :class="item.prepull && item.prepull.status === 'SUCCESS' ? 'ok-text' : 'bad-text'">
|
||
{{ item.prepull ? item.prepull.status : '-' }}
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-if="updateOps.result && updateOps.result.steps && updateOps.result.steps.length > 0" class="table-wrap">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>服务</th>
|
||
<th>主机</th>
|
||
<th>版本</th>
|
||
<th>状态</th>
|
||
<th>HTTP</th>
|
||
<th>Nacos</th>
|
||
<th>耗时</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="step in updateOps.result.steps" :key="step.service + '-' + step.host + '-' + step.image">
|
||
<td>{{ step.service }}</td>
|
||
<td>{{ step.host }}</td>
|
||
<td class="mono" :title="step.image">{{ truncateMiddle(step.image, 14, 12) }}</td>
|
||
<td :class="step.status === 'success' ? 'ok-text' : 'bad-text'">{{ step.status }}</td>
|
||
<td>
|
||
<span v-if="step.http">{{ step.http.statusCode }} / {{ step.http.latencyMs }}ms</span>
|
||
<span v-else>-</span>
|
||
</td>
|
||
<td>
|
||
<span v-if="step.nacos">{{ step.nacos.source || 'ok' }}</span>
|
||
<span v-else>-</span>
|
||
</td>
|
||
<td>{{ step.durationSeconds || '-' }}s</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</details>
|
||
</section>
|
||
|
||
<section id="release-restart" class="panel infra-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>滚动重启</h2>
|
||
<p>按服务勾选执行</p>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span :class="['badge', restartOps.error ? 'bad' : restartOps.running ? 'warn' : 'ok']">
|
||
{{ restartOps.error ? '存在错误' : restartOps.running ? '执行中' : '可操作' }}
|
||
</span>
|
||
<button class="refresh danger" @click="runRollingRestart" :disabled="restartOps.running || restartOps.selectedServices.length === 0">
|
||
{{ restartOps.running ? '重启中...' : `执行重启 ${restartOps.selectedServices.length}` }}
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<p v-if="restartOps.error" class="metric-error">{{ explainError(restartOps.error) }}</p>
|
||
<p v-if="restartOps.message" :class="restartOps.result && restartOps.result.ok ? 'ok-text' : 'metric-error'">{{ restartOps.message }}</p>
|
||
|
||
<div class="table-wrap">
|
||
<table class="table compact release-table">
|
||
<thead>
|
||
<tr>
|
||
<th>服务</th>
|
||
<th>语言</th>
|
||
<th>主机</th>
|
||
<th>实例数</th>
|
||
<th>选择</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="item in restartTargetRows" :key="'restart-' + item.service">
|
||
<td><strong>{{ item.service }}</strong></td>
|
||
<td>{{ item.kind }}</td>
|
||
<td :title="item.hostsLabel">{{ item.hostsLabel }}</td>
|
||
<td>{{ item.hostCount }}</td>
|
||
<td>
|
||
<label class="table-check">
|
||
<input
|
||
:checked="restartOps.selectedServices.includes(item.service)"
|
||
type="checkbox"
|
||
@change="toggleRestartService(item.service)"
|
||
/>
|
||
<span>{{ restartOps.selectedServices.includes(item.service) ? '已选' : '选择' }}</span>
|
||
</label>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="!restartOps.loading && restartTargetRows.length === 0">
|
||
<td colspan="5" class="table-empty-cell">当前没有可展示的重启目标</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<details v-if="restartOps.result && restartOps.result.steps && restartOps.result.steps.length > 0" class="result-disclosure">
|
||
<summary>重启结果明细</summary>
|
||
<div class="table-wrap">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>服务</th>
|
||
<th>主机</th>
|
||
<th>类型</th>
|
||
<th>状态</th>
|
||
<th>HTTP</th>
|
||
<th>Nacos</th>
|
||
<th>耗时</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="step in restartOps.result.steps" :key="step.service + '-' + step.host">
|
||
<td>{{ step.service }}</td>
|
||
<td>{{ step.host }}</td>
|
||
<td>{{ step.kind }}</td>
|
||
<td :class="step.status === 'success' ? 'ok-text' : 'bad-text'">{{ step.status }}</td>
|
||
<td>
|
||
<span v-if="step.http">{{ step.http.statusCode }} / {{ step.http.latencyMs }}ms</span>
|
||
<span v-else>-</span>
|
||
</td>
|
||
<td>
|
||
<span v-if="step.nacos">{{ step.nacos.source || 'ok' }}</span>
|
||
<span v-else>-</span>
|
||
</td>
|
||
<td>{{ step.durationSeconds || '-' }}s</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</details>
|
||
</section>
|
||
</template>
|
||
|
||
<div v-if="logViewer.open" class="log-viewer-overlay" @click.self="closeLogViewer">
|
||
<section class="log-viewer" role="dialog" aria-modal="true" :aria-labelledby="'log-viewer-title'">
|
||
<header class="log-viewer-head">
|
||
<div class="panel-title">
|
||
<h2 id="log-viewer-title">{{ activeLogViewerTitle }}</h2>
|
||
<p>{{ activeLogViewerSubtitle }}</p>
|
||
</div>
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="refreshActiveLogViewer" :disabled="activeLogViewerRefreshDisabled">
|
||
{{ activeLogViewerLoading ? '刷新中...' : '刷新' }}
|
||
</button>
|
||
<button class="refresh ghost" @click="downloadActiveLogViewer" :disabled="activeLogViewerDownloadDisabled">
|
||
{{ activeLogViewerExporting ? '导出中...' : '导出' }}
|
||
</button>
|
||
<button class="refresh ghost" @click="closeLogViewer">关闭</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="log-viewer-toolbar">
|
||
<span v-if="activeLogViewerBadge" class="badge neutral mono">{{ activeLogViewerBadge }}</span>
|
||
<span v-if="activeLogViewerSecondaryBadge" class="badge neutral">{{ activeLogViewerSecondaryBadge }}</span>
|
||
<label v-if="logViewer.kind === 'service'" class="interval-field detail-log-field">
|
||
<span>导出</span>
|
||
<input v-model.number="serviceLog.exportLines" type="number" min="1" :max="serviceLog.exportMaxLines" class="detail-log-input" />
|
||
</label>
|
||
</div>
|
||
|
||
<p v-if="activeLogViewerError" class="metric-error">{{ activeLogViewerError }}</p>
|
||
<pre id="service-update-log-output" class="log-viewer-output mono">{{ activeLogViewerContent || '暂无日志' }}</pre>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
|
||
</html>
|