3658 lines
230 KiB
HTML
3658 lines
230 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', showLoadingSkeleton ? 'shell-loading' : '']">
|
||
<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>
|
||
<nav class="app-switcher" aria-label="入口切换">
|
||
<a
|
||
v-for="entry in appSwitchLinks"
|
||
:key="entry.id"
|
||
:href="entry.href"
|
||
:class="['app-switch-link', currentAppEntryId === entry.id ? 'active' : '']"
|
||
>
|
||
{{ entry.label }}
|
||
</a>
|
||
</nav>
|
||
</div>
|
||
|
||
<nav v-if="viewTabs.length > 1" 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 :class="['badge', globalHealthTone]">{{ globalHealthLabel }}</span>
|
||
<input v-model.trim="globalSearchModel" class="search topbar-search" :placeholder="globalSearchPlaceholder" :disabled="globalSearchDisabled" />
|
||
<span v-if="auth.username" class="badge neutral mono">{{ auth.username }}</span>
|
||
<span v-if="loading" class="badge warn">刷新中</span>
|
||
<span v-if="error" class="badge bad">接口异常</span>
|
||
<label class="interval-field topbar-interval" aria-label="刷新间隔">
|
||
<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>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<section v-if="showLoadingSkeleton" class="panel loading-stage">
|
||
<div class="loading-stage-head">
|
||
<div class="loading-chip-row">
|
||
<span class="loading-chip"></span>
|
||
<span class="loading-chip"></span>
|
||
<span class="loading-chip"></span>
|
||
<span class="loading-chip"></span>
|
||
</div>
|
||
<div class="loading-line loading-line-short"></div>
|
||
</div>
|
||
|
||
<div class="loading-band-grid">
|
||
<span v-for="index in 5" :key="'loading-band-' + index" class="loading-band"></span>
|
||
</div>
|
||
|
||
<div class="loading-panels">
|
||
<article class="loading-card loading-card-wide">
|
||
<div class="loading-card-head">
|
||
<span class="loading-line loading-line-medium"></span>
|
||
<span class="loading-pill"></span>
|
||
</div>
|
||
<div class="loading-table">
|
||
<span v-for="index in 6" :key="'loading-row-' + index" class="loading-table-row"></span>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="loading-card">
|
||
<div class="loading-card-head">
|
||
<span class="loading-line loading-line-medium"></span>
|
||
<span class="loading-pill"></span>
|
||
</div>
|
||
<div class="loading-stack">
|
||
<span v-for="index in 5" :key="'loading-side-' + index" class="loading-block"></span>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
|
||
<template v-else-if="activeView === 'infra'">
|
||
<section class="control-bar task-control-bar">
|
||
<div class="taskbar-context">
|
||
<strong>基础设施</strong>
|
||
</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>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-else-if="activeView === 'database'">
|
||
<section class="control-bar task-control-bar database-control-bar">
|
||
<div class="taskbar-context mongo-taskbar-context">
|
||
<strong>数据库工作台</strong>
|
||
<span>{{ databaseContextLabel }}</span>
|
||
</div>
|
||
|
||
<div v-if="databaseEngines.length > 1" class="control-group database-engine-switch">
|
||
<button
|
||
v-for="engine in databaseEngines"
|
||
:key="engine.id"
|
||
:class="['filter-chip', databaseEngine === engine.id ? 'active' : '']"
|
||
@click="switchDatabaseEngine(engine.id)"
|
||
>
|
||
{{ engine.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="control-group">
|
||
<button
|
||
v-for="tab in activeDatabaseTabs"
|
||
:key="tab.id"
|
||
:class="['filter-chip', activeDatabaseTabId === tab.id ? 'active' : '']"
|
||
@click="activeDatabaseTabId = tab.id"
|
||
>
|
||
{{ tab.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="control-group control-group-spread">
|
||
<button class="refresh ghost" @click="refreshDatabaseTree" :disabled="databaseTreeLoading">
|
||
{{ databaseTreeLoading ? '刷新中...' : '刷新树' }}
|
||
</button>
|
||
<button
|
||
class="refresh"
|
||
@click="refreshDatabaseCurrent"
|
||
:disabled="!databaseHasSelection || databaseRefreshBusy"
|
||
>
|
||
{{ databaseRefreshBusy ? '加载中...' : databaseRefreshLabel }}
|
||
</button>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-else-if="activeView === 'common'">
|
||
<section class="control-bar task-control-bar database-control-bar common-control-bar">
|
||
<div class="taskbar-context mongo-taskbar-context">
|
||
<strong>通用工作台</strong>
|
||
<span>{{ commonToolContextLabel }}</span>
|
||
</div>
|
||
|
||
<div class="control-group common-tool-switch">
|
||
<button
|
||
v-for="tool in commonToolTabs"
|
||
:key="tool.id"
|
||
:class="['filter-chip', commonTool === tool.id ? 'active' : '']"
|
||
@click="switchCommonTool(tool.id)"
|
||
>
|
||
{{ tool.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="control-group control-group-spread">
|
||
<template v-if="commonTool === 'mysql'">
|
||
<button class="refresh ghost" @click="refreshMySqlDatabases({ preserveSelection: true })" :disabled="mySqlAdmin.loading">
|
||
{{ mySqlAdmin.loading ? '刷新中...' : '刷新树' }}
|
||
</button>
|
||
<button
|
||
class="refresh"
|
||
@click="refreshMySqlCurrent"
|
||
:disabled="!mySqlAdmin.selectedDatabase || databaseRefreshBusy"
|
||
>
|
||
{{ databaseRefreshBusy ? '加载中...' : databaseRefreshLabel }}
|
||
</button>
|
||
</template>
|
||
<template v-else-if="commonTool === 'cdn'">
|
||
<button class="refresh ghost" @click="refreshCdnOverview()" :disabled="cdnAdmin.loading">
|
||
{{ cdnAdmin.loading ? '刷新中...' : '刷新记录' }}
|
||
</button>
|
||
<button class="refresh" @click="submitCdnPurge" :disabled="cdnAdmin.submitting || cdnFormItemCount === 0">
|
||
{{ cdnAdmin.submitting ? '提交中...' : '执行刷新' }}
|
||
</button>
|
||
</template>
|
||
<template v-else>
|
||
<button class="refresh ghost" @click="refreshChatappCronOverview()" :disabled="chatappCron.loading">
|
||
{{ chatappCron.loading ? '刷新中...' : '刷新配置' }}
|
||
</button>
|
||
<button class="refresh ghost" @click="openChatappCronLogViewer" :disabled="!chatappCron.operationId">
|
||
查看日志
|
||
</button>
|
||
<button class="refresh" @click="runChatappCronDeploy" :disabled="chatappCronIsRunning || !chatappCron.overview.enabled">
|
||
{{ chatappCronIsRunning ? '部署中...' : '执行部署' }}
|
||
</button>
|
||
</template>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-else-if="activeView === 'config'">
|
||
<section class="control-bar task-control-bar">
|
||
<div class="taskbar-context">
|
||
<strong>编辑与校验</strong>
|
||
<span>{{ configContextLabel }}</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">
|
||
<div v-if="configTab === 'go'" 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>
|
||
|
||
<p v-if="error" class="toolbar-error">加载失败:{{ error }}</p>
|
||
|
||
<template v-if="!showLoadingSkeleton && (activeView === 'overview' || activeView === 'services')">
|
||
<section v-if="activeView === 'overview' && overviewStatusSegments.length > 0" class="panel summary-strip">
|
||
<div class="summary-cards status-band-list">
|
||
<article
|
||
v-for="card in overviewStatusSegments"
|
||
:key="card.key"
|
||
: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 class="status-band-meta">{{ card.meta }}</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" :data-group="section.group">
|
||
<header class="matrix-stack-head">
|
||
<div class="matrix-stack-headline">
|
||
<strong>{{ section.label }}</strong>
|
||
<span>{{ section.rows.length }} 服务 · {{ section.columns.length }} 节点</span>
|
||
</div>
|
||
<div class="matrix-stack-badges">
|
||
<span :class="['badge', matrixSectionDownCount(section) > 0 ? 'bad' : 'ok']">{{ matrixSectionDownCount(section) }} 异常</span>
|
||
</div>
|
||
</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>{{ showSelectedServiceDetail ? selectedServiceDetail.service : activeView === 'overview' ? '运行状态' : '服务详情' }}</h2>
|
||
<p v-if="showSelectedServiceDetail">{{ selectedServiceDetail.groupLabel }} · {{ selectedServiceDetail.kind }}</p>
|
||
<p v-else-if="activeView === 'overview'">{{ overviewDetailSubtitle }}</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="showSelectedServiceDetail">
|
||
<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>
|
||
|
||
<section v-else-if="activeView === 'overview'" class="overview-side-stack">
|
||
<div v-if="overviewHealthyInfraCards.length > 0" class="overview-side-grid">
|
||
<article v-for="card in overviewHealthyInfraCards" :key="'side-' + card.key" :class="['overview-side-card', `tone-${card.tone}`]">
|
||
<small>{{ card.label }}</small>
|
||
<strong>{{ card.value }}</strong>
|
||
<span>{{ card.meta }}</span>
|
||
</article>
|
||
</div>
|
||
<div v-if="overviewHealthyHostGroups.length > 0" class="overview-side-hosts">
|
||
<header class="detail-subhead">
|
||
<strong>正常主机</strong>
|
||
<span>{{ overviewQuietHosts.length }} 台</span>
|
||
</header>
|
||
<div class="overview-side-host-list">
|
||
<article v-for="group in overviewHealthyHostGroups" :key="'side-group-' + group.key" class="overview-side-host-chip" :data-group="group.key">
|
||
<div class="overview-side-host-chip-head">
|
||
<strong>{{ group.label }}</strong>
|
||
<em>{{ group.count }} 台</em>
|
||
</div>
|
||
<span>{{ group.summary }}</span>
|
||
</article>
|
||
</div>
|
||
</div>
|
||
<div v-if="overviewHealthyInfraCards.length === 0 && overviewHealthyHostGroups.length === 0" class="empty-panel compact">当前没有可展示的状态</div>
|
||
</section>
|
||
|
||
<div v-else class="empty-panel">当前没有服务详情</div>
|
||
</aside>
|
||
</section>
|
||
|
||
<section v-if="activeView === 'overview' && overviewBridgeAlerts.length > 0" class="panel bridge-panel">
|
||
<header class="bridge-head">
|
||
<strong>异常索引</strong>
|
||
<span>{{ overviewBridgeAlerts.length }} 项</span>
|
||
</header>
|
||
<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>{{ overviewHostPanelSummary }}</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 v-if="overviewAlertHosts.length > 0" 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 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 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-metric-band">
|
||
<span
|
||
v-for="metric in hostMetricEntries(host)"
|
||
:key="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', host.hasMetricIssue ? 'tone-warn' : 'tone-neutral']">
|
||
<small>TAT</small>
|
||
<strong>{{ hostMetricStatusLabel(host) }}</strong>
|
||
</span>
|
||
</div>
|
||
|
||
<div :class="['host-service-summary', `tone-${hostSummaryTone(host)}`]">
|
||
<small>{{ host.downCount > 0 ? '异常服务' : host.hasMetricIssue ? '采集状态' : '在线服务' }}</small>
|
||
<strong class="host-service-summary-text">{{ host.downCount > 0 ? host.alertSummary : host.quietSummary }}</strong>
|
||
</div>
|
||
|
||
<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 ? 'warn' : 'neutral']">{{ host.hasMetricIssue ? '采集异常' : (isHostExpanded(host.host) ? '收起' : '展开') }}</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>
|
||
</section>
|
||
|
||
<section v-if="overviewQuietHosts.length > 0" class="host-section">
|
||
<div v-if="overviewAlertHosts.length === 0" class="section-caption-row section-caption-row-quiet">
|
||
<div class="section-caption">正常主机</div>
|
||
<div class="section-caption-meta">
|
||
<span>{{ overviewQuietHosts.length }} 台</span>
|
||
<span>{{ overviewQuietServiceTotal }} 服务</span>
|
||
</div>
|
||
</div>
|
||
<button v-else class="host-fold-toggle" @click="toggleOverviewHealthyHosts">
|
||
<span>正常主机</span>
|
||
<strong>{{ overviewQuietHosts.length }}</strong>
|
||
<small>{{ overviewHealthyHostsOpen ? '收起' : '展开' }}</small>
|
||
</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="[
|
||
'host-strip',
|
||
'host-strip-overview',
|
||
'host-strip-quiet',
|
||
isHostHighlighted(host) ? 'host-strip-focused' : ''
|
||
]"
|
||
:data-group="host.group"
|
||
@click="selectHost(host.host)"
|
||
>
|
||
<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-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>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-if="!showLoadingSkeleton && 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' : ''
|
||
]"
|
||
:data-group="host.group"
|
||
>
|
||
<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 v-if="hostHasMetricData(host)" 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 v-else class="host-inline-metrics compact-metrics compact-metrics-fallback">
|
||
<span :class="['metric-state-chip', host.hasMetricIssue ? 'tone-warn' : 'tone-neutral']">
|
||
<small>TAT</small>
|
||
<strong>{{ hostMetricStatusLabel(host) }}</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="!showLoadingSkeleton && activeView === 'infra'">
|
||
<section class="overview-infra-grid">
|
||
<article v-for="card in overviewInfraCards" :key="'infra-' + card.key" class="infra-mini-card">
|
||
<header class="infra-mini-head">
|
||
<div class="infra-mini-title">
|
||
<strong>{{ card.label }}</strong>
|
||
<small>{{ card.meta }}</small>
|
||
</div>
|
||
<span :class="['badge', card.tone === 'bad' ? 'bad' : card.tone === 'warn' ? 'warn' : 'ok']">{{ card.badge }}</span>
|
||
</header>
|
||
<div class="infra-mini-metrics">
|
||
<span v-for="item in card.metrics" :key="'infra-' + card.key + '-' + item.label" class="infra-mini-metric">
|
||
<small>{{ item.label }}</small>
|
||
<strong>{{ item.value }}</strong>
|
||
</span>
|
||
</div>
|
||
<div class="infra-freshness">
|
||
<span>最近 {{ formattedUpdatedAt }}</span>
|
||
<span :class="dataFreshnessTone === 'warn' ? 'warn-text' : ''">延迟 {{ dataFreshnessLabel }}</span>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
|
||
<section v-if="infraTab === 'overview'" class="panel infra-overview-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>状态总览</h2>
|
||
</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 mongo-workbench-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="metadata-strip metadata-strip-tight mongo-workbench-strip">
|
||
<span class="mono">{{ truncateMiddle(payload.mongo.info.host || '-', 18, 12) }}</span>
|
||
<span>{{ formatNumber(payload.mongo.summary.databaseTotal) }} 库</span>
|
||
<span>{{ formatNumber(payload.mongo.summary.collectionTotal) }} 集合</span>
|
||
<span>连接 {{ formatNumber(payload.mongo.connections.current) }}</span>
|
||
<span>请求 {{ formatNumber(payload.mongo.network.numRequests) }}</span>
|
||
<span>副本 {{ payload.mongo.replicaSet && payload.mongo.replicaSet.configured ? payload.mongo.replicaSet.members.length : 0 }}</span>
|
||
</div>
|
||
|
||
<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>{{ filteredMongoDatabases.length }}</strong>
|
||
<small>{{ infraMongoDetailsOpen ? '收起' : '展开' }}</small>
|
||
</button>
|
||
|
||
<div v-if="infraMongoDetailsOpen" class="infra-detail-stack mongo-detail-stack">
|
||
<div class="metadata-strip infra-inline-strip mongo-secondary-strip">
|
||
<span>Virtual {{ formatMb(payload.mongo.memory.virtualMb) }}</span>
|
||
<span>Resident {{ formatMb(payload.mongo.memory.residentMb) }}</span>
|
||
<span>Requests {{ formatNumber(payload.mongo.network.numRequests) }}</span>
|
||
<span>WT Cache {{ formatBytes(payload.mongo.wiredTigerCache.bytesCurrentlyInCache) }}</span>
|
||
</div>
|
||
|
||
<section class="mongo-detail-block">
|
||
<header class="mongo-detail-head">
|
||
<div class="panel-title">
|
||
<h2>数据库</h2>
|
||
<p>{{ formatNumber(filteredMongoDatabases.length) }} 项</p>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span class="badge neutral">集合 {{ formatNumber(payload.mongo.summary.collectionTotal) }}</span>
|
||
<span class="badge neutral">对象 {{ formatNumber(payload.mongo.summary.objectTotal) }}</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="table-wrap">
|
||
<table class="table compact">
|
||
<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>
|
||
</section>
|
||
|
||
<section v-if="payload.mongo.replicaSet && payload.mongo.replicaSet.configured" class="mongo-detail-block">
|
||
<header class="mongo-detail-head">
|
||
<div class="panel-title">
|
||
<h2>副本集</h2>
|
||
<p>{{ payload.mongo.replicaSet.members.length }} 成员</p>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span class="badge neutral">角色 {{ payload.mongo.replicaSet.setName || '-' }}</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div 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>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
|
||
<section v-if="infraTab === 'tat'" class="panel infra-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>TAT 主机采集</h2>
|
||
</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>
|
||
<span>最近 {{ formattedUpdatedAt }}</span>
|
||
<span :class="dataFreshnessTone === 'warn' ? 'warn-text' : ''">延迟 {{ dataFreshnessLabel }}</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="showDatabaseWorkbench">
|
||
<section v-if="showMySqlWorkbench" class="panel infra-panel database-workbench-panel mysql-workbench-panel">
|
||
<div class="compact-workbench-head mysql-workbench-head">
|
||
<div class="metadata-strip metadata-strip-tight">
|
||
<span class="mono">{{ truncateMiddle(mySqlAdmin.overview.info.host || '未配置 MySQL', 20, 12) }}</span>
|
||
<span>{{ selectedMySqlDatabaseItem ? `db ${selectedMySqlDatabaseItem.name}` : `${formatNumber(mySqlFilteredDatabases.length)} 库` }}</span>
|
||
<span>{{ selectedMySqlTableItem ? `table ${selectedMySqlTableItem.name}` : `${formatNumber(mySqlAdmin.tables.length)} 表` }}</span>
|
||
<span>连接 {{ formatNumber(mySqlAdmin.overview.connections.current) }} / {{ formatNumber(mySqlAdmin.overview.connections.max) }}</span>
|
||
<span>复制 {{ mySqlReplicationLabel(mySqlAdmin.overview.replication) }}</span>
|
||
<span>只读 {{ (mySqlAdmin.overview.replication.readOnly || mySqlAdmin.overview.replication.superReadOnly) ? 'Y' : 'N' }}</span>
|
||
</div>
|
||
<span :class="['badge', mySqlOverviewStatusTone]">{{ mySqlOverviewStatusText }}</span>
|
||
</div>
|
||
|
||
<p v-if="mySqlAdmin.error" class="metric-error">{{ explainError(mySqlAdmin.error) }}</p>
|
||
<p v-else-if="mySqlOverviewWarningText" class="metric-error warn">{{ mySqlOverviewWarningText }}</p>
|
||
|
||
<div class="summary-cards mysql-overview-grid">
|
||
<article v-for="card in mySqlOverviewCards" :key="'mysql-overview-' + card.key" :class="['summary-card', 'mysql-overview-card', `tone-${card.tone}`]">
|
||
<small>{{ card.label }}</small>
|
||
<strong>{{ card.value }}</strong>
|
||
<span>{{ card.meta }}</span>
|
||
</article>
|
||
</div>
|
||
|
||
<div class="config-layout mongo-admin-layout mysql-admin-layout">
|
||
<aside class="config-sidebar mongo-sidebar mysql-sidebar">
|
||
<div class="metadata-strip metadata-strip-tight mysql-sidebar-strip">
|
||
<span>{{ formatNumber(mySqlFilteredDatabases.length) }} 库</span>
|
||
<span>{{ selectedMySqlDatabaseItem ? `${selectedMySqlDatabaseItem.tableTotal} 表` : `${formatNumber(mySqlAdmin.tables.length)} 表` }}</span>
|
||
<span v-if="selectedMySqlDatabaseItem" class="mono">{{ selectedMySqlDatabaseItem.name }}</span>
|
||
<span v-if="selectedMySqlDatabaseItem">{{ formatMb(selectedMySqlDatabaseItem.totalSizeMb) }}</span>
|
||
</div>
|
||
|
||
<section class="mysql-sidebar-section">
|
||
<div class="section-caption-row">
|
||
<div class="section-caption">数据库</div>
|
||
<div class="section-caption-meta">
|
||
<span>{{ formatNumber(mySqlFilteredDatabases.length) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="config-list config-list-ide mysql-sidebar-list">
|
||
<button
|
||
v-for="database in mySqlFilteredDatabases"
|
||
:key="'mysql-db-' + database.name"
|
||
:class="['config-item', 'config-item-ide', mySqlAdmin.selectedDatabase === database.name ? 'active' : '']"
|
||
@click="selectMySqlDatabase(database.name, { preserveTable: true })"
|
||
>
|
||
<strong>{{ database.name }}</strong>
|
||
<span>{{ database.tableTotal }} 表 · {{ formatMb(database.totalSizeMb) }}</span>
|
||
<small>{{ database.system ? 'system' : `${formatNumber(database.rowEstimate)} rows` }}</small>
|
||
</button>
|
||
|
||
<div v-if="!mySqlAdmin.loading && mySqlFilteredDatabases.length === 0" class="config-empty">
|
||
当前没有可展示的数据库
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="selectedMySqlDatabaseItem" class="mysql-sidebar-section">
|
||
<div class="section-caption-row">
|
||
<div class="section-caption">表</div>
|
||
<div class="section-caption-meta">
|
||
<span>{{ formatNumber(mySqlVisibleTables.length) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="config-list config-list-ide mysql-sidebar-list">
|
||
<button
|
||
v-for="table in mySqlVisibleTables"
|
||
:key="'mysql-table-' + table.name"
|
||
:class="['config-item', 'config-item-ide', mySqlAdmin.selectedTable === table.name ? 'active' : '']"
|
||
@click="selectMySqlTable(mySqlAdmin.selectedDatabase, table.name)"
|
||
>
|
||
<strong>{{ table.name }}</strong>
|
||
<span>{{ formatNumber(table.rowEstimate) }} rows · {{ formatMb(table.totalSizeMb) }}</span>
|
||
<small>{{ table.engine || '-' }} · {{ table.collation || '-' }}</small>
|
||
</button>
|
||
|
||
<div v-if="mySqlAdmin.tablesLoading" class="config-empty">
|
||
表加载中
|
||
</div>
|
||
<div v-else-if="mySqlVisibleTables.length === 0" class="config-empty">
|
||
当前没有匹配表
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</aside>
|
||
|
||
<section class="config-editor mongo-admin-editor mysql-admin-editor">
|
||
<div class="editor-toolbar sticky-toolbar mysql-editor-toolbar">
|
||
<div class="editor-toolbar-main">
|
||
<span
|
||
v-if="mySqlAdmin.message"
|
||
:class="[
|
||
'badge',
|
||
mySqlAdmin.messageTone === 'ok'
|
||
? 'ok'
|
||
: mySqlAdmin.messageTone === 'warn'
|
||
? 'warn'
|
||
: 'neutral',
|
||
]"
|
||
>
|
||
{{ mySqlAdmin.message }}
|
||
</span>
|
||
<span v-else-if="mySqlAdmin.tablesLoading" class="badge neutral">表加载中</span>
|
||
<span v-else-if="mySqlAdmin.schemaLoading" class="badge neutral">结构加载中</span>
|
||
<span v-else-if="mySqlAdmin.rowsLoading" class="badge neutral">查询中</span>
|
||
<span v-else-if="mySqlAdmin.sqlConsole.running" class="badge neutral">SQL 执行中</span>
|
||
<span v-else class="badge neutral">{{ activeMySqlAdminTabLabel || 'MySQL' }}</span>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="refreshMySqlSchema()" :disabled="!mySqlAdmin.selectedTable || mySqlAdmin.schemaLoading">
|
||
{{ mySqlAdmin.schemaLoading ? '加载中...' : '刷新结构' }}
|
||
</button>
|
||
<button
|
||
class="refresh ghost"
|
||
@click="mySqlAdmin.activeTab === 'sql' ? executeMySqlSql() : refreshMySqlRows({ resetPage: mySqlAdmin.activeTab === 'browse' })"
|
||
:disabled="!mySqlAdmin.selectedTable || mySqlAdmin.rowsLoading || mySqlAdmin.sqlConsole.running"
|
||
>
|
||
{{ mySqlAdmin.rowsLoading || mySqlAdmin.sqlConsole.running ? '执行中...' : '刷新结果' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="selectedMySqlDatabaseItem" class="metadata-strip mysql-context-strip">
|
||
<span class="mono">db {{ selectedMySqlDatabaseItem.name }}</span>
|
||
<span v-if="selectedMySqlTableItem" class="mono">table {{ selectedMySqlTableItem.name }}</span>
|
||
<span v-if="selectedMySqlTableItem">engine {{ selectedMySqlTableItem.engine || '-' }}</span>
|
||
<span v-if="selectedMySqlTableItem">rows {{ formatNumber(mySqlAdmin.rows.totalRows) }}</span>
|
||
<span v-if="selectedMySqlTableItem">size {{ formatMb(selectedMySqlTableItem.totalSizeMb) }}</span>
|
||
<span>只读 {{ (mySqlAdmin.overview.replication.readOnly || mySqlAdmin.overview.replication.superReadOnly) ? 'Y' : 'N' }}</span>
|
||
</div>
|
||
|
||
<div v-if="!selectedMySqlDatabaseItem" class="config-empty">
|
||
当前没有可操作的数据库
|
||
</div>
|
||
|
||
<template v-else-if="!selectedMySqlTableItem">
|
||
<section class="panel mongo-admin-subpanel mysql-table-browser">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>表</h2>
|
||
<p>{{ selectedMySqlDatabaseItem.name }}</p>
|
||
</div>
|
||
<span class="badge neutral">{{ formatNumber(mySqlAdmin.tables.length) }} 表</span>
|
||
</header>
|
||
|
||
<div class="table-wrap table-scroll-x">
|
||
<table class="table compact mysql-data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>表名</th>
|
||
<th>Engine</th>
|
||
<th>Rows</th>
|
||
<th>大小</th>
|
||
<th>更新时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="table in mySqlAdmin.tables" :key="'table-list-' + table.name">
|
||
<td>
|
||
<button class="refresh ghost inline-button" @click="selectMySqlTable(mySqlAdmin.selectedDatabase, table.name)">{{ table.name }}</button>
|
||
</td>
|
||
<td>{{ table.engine || '-' }}</td>
|
||
<td>{{ formatNumber(table.rowEstimate) }}</td>
|
||
<td>{{ formatMb(table.totalSizeMb) }}</td>
|
||
<td>{{ formatDateTime(table.updateTime) }}</td>
|
||
</tr>
|
||
<tr v-if="mySqlAdmin.tables.length === 0">
|
||
<td colspan="5" class="table-empty-cell">当前没有表</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-else-if="mySqlAdmin.activeTab === 'browse'">
|
||
<section class="panel mongo-admin-subpanel mysql-browse-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>数据</h2>
|
||
<p>{{ selectedMySqlTableItem.name }}</p>
|
||
</div>
|
||
<span class="badge neutral">{{ mySqlRowsRangeText }}</span>
|
||
</header>
|
||
|
||
<div class="mysql-query-stack">
|
||
<div class="mysql-filter-grid">
|
||
<label class="update-field">
|
||
<span>关键字</span>
|
||
<input v-model.trim="mySqlAdmin.rows.keyword" class="search" />
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>排序列</span>
|
||
<select v-model="mySqlAdmin.rows.orderBy" class="interval-select">
|
||
<option value="">自动</option>
|
||
<option v-for="column in mySqlAdmin.schema.columns" :key="'sort-' + column.name" :value="column.name">{{ column.name }}</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>方向</span>
|
||
<select v-model="mySqlAdmin.rows.orderDirection" class="interval-select">
|
||
<option value="asc">ASC</option>
|
||
<option value="desc">DESC</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>每页</span>
|
||
<select v-model.number="mySqlAdmin.rows.pageSize" class="interval-select">
|
||
<option v-for="size in mySqlAdmin.pageSizeOptions" :key="'mysql-size-' + size" :value="size">{{ size }}</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="mysql-filter-list">
|
||
<div v-for="(filter, index) in mySqlAdmin.rows.filters" :key="'mysql-filter-' + index" class="mysql-filter-row">
|
||
<label class="update-field">
|
||
<span>列</span>
|
||
<select v-model="filter.column" class="interval-select">
|
||
<option value="">选择列</option>
|
||
<option v-for="column in mySqlAdmin.schema.columns" :key="'filter-column-' + index + '-' + column.name" :value="column.name">{{ column.name }}</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>条件</span>
|
||
<select v-model="filter.operator" class="interval-select">
|
||
<option v-for="option in mySqlAdmin.filterOperators" :key="'filter-op-' + index + '-' + option.id" :value="option.id">{{ option.label }}</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>值</span>
|
||
<input
|
||
v-if="mySqlFilterNeedsValue(filter.operator)"
|
||
v-model="filter.value"
|
||
class="search mono"
|
||
:placeholder="
|
||
filter.operator === 'in' || filter.operator === 'not_in'
|
||
? '[1,2]'
|
||
: filter.operator === 'between' || filter.operator === 'not_between'
|
||
? '[min,max]'
|
||
: ''
|
||
"
|
||
/>
|
||
<div v-else class="mysql-filter-static">无需值</div>
|
||
</label>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost inline-button" @click="removeMySqlFilterRow(index)">删除</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="editor-toolbar mongo-subtoolbar mysql-result-toolbar">
|
||
<div class="editor-toolbar-main">
|
||
<span class="badge neutral">总数 {{ formatNumber(mySqlAdmin.rows.totalRows) }}</span>
|
||
<span class="badge neutral">页数 {{ mySqlAdmin.rows.page }} / {{ mySqlAdmin.rows.totalPages }}</span>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="fillMySqlInsertTemplate" :disabled="mySqlAdmin.rowsLoading">
|
||
插入模板
|
||
</button>
|
||
<button class="refresh ghost" @click="addMySqlFilterRow">增加筛选</button>
|
||
<button class="refresh ghost" @click="resetMySqlRowQuery" :disabled="mySqlAdmin.rowsLoading">重置</button>
|
||
<button class="refresh" @click="refreshMySqlRows({ resetPage: true })" :disabled="mySqlAdmin.rowsLoading">
|
||
{{ mySqlAdmin.rowsLoading ? '查询中...' : '查询' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-wrap table-scroll-x">
|
||
<table class="table compact mysql-data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>操作</th>
|
||
<th v-for="column in mySqlAdmin.rows.columns" :key="'header-' + column.name">
|
||
<button class="mysql-sort-button" @click="toggleMySqlSort(column.name)">
|
||
<span>{{ column.name }}</span>
|
||
<small v-if="mySqlSortLabel(column.name)" class="mysql-sort-indicator">{{ mySqlSortLabel(column.name) }}</small>
|
||
</button>
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="row in mySqlAdmin.rows.items" :key="'mysql-row-' + mySqlRowKey(row)" :class="isMySqlRowSelected(row) ? 'selected' : ''">
|
||
<td class="mysql-row-action-cell">
|
||
<div class="action-group action-group-tight">
|
||
<button class="refresh ghost inline-button" @click="loadMySqlRowForEdit(row)">编辑</button>
|
||
<button class="refresh ghost inline-button" @click="copyMySqlRowToInsert(row)">复制</button>
|
||
<button class="refresh danger inline-button" @click="deleteMySqlRow(row)">删除</button>
|
||
</div>
|
||
</td>
|
||
<td
|
||
v-for="column in mySqlAdmin.rows.columns"
|
||
:key="'cell-' + mySqlRowKey(row) + '-' + column.name"
|
||
:class="['mono', 'mysql-cell', row.values[column.name] == null ? 'is-null' : '']"
|
||
:title="mySqlCellText(row.values[column.name])"
|
||
>
|
||
{{ mySqlCellPreview(row.values[column.name]) }}
|
||
</td>
|
||
</tr>
|
||
<tr v-if="mySqlAdmin.rows.items.length === 0">
|
||
<td :colspan="Math.max(1, mySqlAdmin.rows.columns.length + 1)" class="table-empty-cell">当前没有数据</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="mongo-pager">
|
||
<button class="refresh ghost" @click="changeMySqlRowsPage(mySqlAdmin.rows.page - 1)" :disabled="mySqlAdmin.rows.page <= 1 || mySqlAdmin.rowsLoading">上一页</button>
|
||
<span>{{ mySqlAdmin.rows.page }} / {{ mySqlAdmin.rows.totalPages }}</span>
|
||
<button class="refresh ghost" @click="changeMySqlRowsPage(mySqlAdmin.rows.page + 1)" :disabled="mySqlAdmin.rows.page >= mySqlAdmin.rows.totalPages || mySqlAdmin.rowsLoading">下一页</button>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-else-if="mySqlAdmin.activeTab === 'write'">
|
||
<div class="mongo-panel-grid mysql-write-grid">
|
||
<article class="mongo-action-panel mysql-action-panel tone-write">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>新增行</h2>
|
||
<p>{{ selectedMySqlTableItem.name }}</p>
|
||
</div>
|
||
<button class="refresh ghost" @click="fillMySqlInsertTemplate" :disabled="mySqlAdmin.insertForm.saving">生成模板</button>
|
||
</header>
|
||
|
||
<textarea
|
||
v-model="mySqlAdmin.insertForm.valuesText"
|
||
class="config-textarea mongo-editor-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh" @click="insertMySqlRow" :disabled="mySqlAdmin.insertForm.saving">
|
||
{{ mySqlAdmin.insertForm.saving ? '提交中...' : '插入' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="mongo-action-panel mysql-action-panel tone-edit">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>编辑行</h2>
|
||
<p>{{ mySqlRowIdentityText(mySqlAdmin.rowEditor.identity) }}</p>
|
||
</div>
|
||
<button class="refresh ghost" @click="clearMySqlRowEditor" :disabled="mySqlAdmin.rowEditor.saving">清空</button>
|
||
</header>
|
||
|
||
<textarea
|
||
v-model="mySqlAdmin.rowEditor.valuesText"
|
||
class="config-textarea mongo-editor-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="copyMySqlRowToInsert" :disabled="!mySqlAdmin.rowEditor.identity || mySqlAdmin.rowEditor.saving">
|
||
复制为新增
|
||
</button>
|
||
<button class="refresh" @click="updateMySqlRow" :disabled="!mySqlAdmin.rowEditor.identity || mySqlAdmin.rowEditor.saving">
|
||
{{ mySqlAdmin.rowEditor.saving ? '保存中...' : '更新' }}
|
||
</button>
|
||
<button class="refresh danger" @click="deleteMySqlRow()" :disabled="!mySqlAdmin.rowEditor.identity || mySqlAdmin.rowEditor.saving">
|
||
删除
|
||
</button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="mySqlAdmin.activeTab === 'schema'">
|
||
<section class="panel mongo-admin-subpanel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>列</h2>
|
||
<p>{{ selectedMySqlTableItem.name }}</p>
|
||
</div>
|
||
<span class="badge neutral">{{ formatNumber(mySqlAdmin.schema.columns.length) }} 列</span>
|
||
</header>
|
||
|
||
<div class="table-wrap table-scroll-x">
|
||
<table class="table compact mysql-data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>列名</th>
|
||
<th>类型</th>
|
||
<th>默认值</th>
|
||
<th>属性</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="column in mySqlAdmin.schema.columns" :key="'column-' + column.name">
|
||
<td class="mono">{{ column.name }}</td>
|
||
<td>{{ column.columnType }}</td>
|
||
<td class="mono">{{ mySqlCellPreview(column.default, 48) }}</td>
|
||
<td>
|
||
<div class="mysql-tag-list">
|
||
<span class="badge neutral">nullable {{ column.nullable ? 'Y' : 'N' }}</span>
|
||
<span v-if="column.key" class="badge neutral">{{ column.key }}</span>
|
||
<span v-if="column.extra" class="badge neutral">{{ column.extra }}</span>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<button class="refresh danger inline-button" @click="dropMySqlColumn(column.name)" :disabled="mySqlAdmin.columnForm.saving">删除</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="mySqlAdmin.schema.columns.length === 0">
|
||
<td colspan="5" class="table-empty-cell">当前没有列</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="mongo-panel-grid mysql-schema-grid">
|
||
<article class="mongo-action-panel mysql-action-panel tone-write">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>新增列</h2>
|
||
</div>
|
||
</header>
|
||
|
||
<label class="update-field">
|
||
<span>列名</span>
|
||
<input v-model.trim="mySqlAdmin.columnForm.columnName" class="search mono" />
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>类型</span>
|
||
<input v-model.trim="mySqlAdmin.columnForm.columnType" class="search mono" />
|
||
</label>
|
||
|
||
<label class="table-check">
|
||
<input v-model="mySqlAdmin.columnForm.nullable" type="checkbox" />
|
||
<span>允许 NULL</span>
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>默认值</span>
|
||
<select v-model="mySqlAdmin.columnForm.defaultMode" class="interval-select">
|
||
<option v-for="mode in mySqlAdmin.defaultModes" :key="'default-mode-' + mode.id" :value="mode.id">{{ mode.label }}</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label v-if="mySqlAdmin.columnForm.defaultMode === 'literal'" class="update-field">
|
||
<span>默认值内容</span>
|
||
<input v-model="mySqlAdmin.columnForm.defaultValue" class="search mono" />
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>注释</span>
|
||
<input v-model="mySqlAdmin.columnForm.comment" class="search" />
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>位置</span>
|
||
<select v-model="mySqlAdmin.columnForm.position" class="interval-select">
|
||
<option value="last">末尾</option>
|
||
<option value="first">首列</option>
|
||
<option value="after">指定列后</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label v-if="mySqlAdmin.columnForm.position === 'after'" class="update-field">
|
||
<span>After</span>
|
||
<select v-model="mySqlAdmin.columnForm.afterColumn" class="interval-select">
|
||
<option value="">选择列</option>
|
||
<option v-for="column in mySqlAdmin.schema.columns" :key="'after-column-' + column.name" :value="column.name">{{ column.name }}</option>
|
||
</select>
|
||
</label>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh" @click="addMySqlColumn" :disabled="mySqlAdmin.columnForm.saving">
|
||
{{ mySqlAdmin.columnForm.saving ? '提交中...' : '增加列' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="mongo-action-panel mysql-action-panel tone-structure">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>新增索引</h2>
|
||
<p>{{ selectedMySqlTableItem.name }}</p>
|
||
</div>
|
||
</header>
|
||
|
||
<label class="update-field">
|
||
<span>索引名</span>
|
||
<input v-model.trim="mySqlAdmin.indexForm.indexName" class="search mono" />
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>类型</span>
|
||
<select v-model="mySqlAdmin.indexForm.indexMode" class="interval-select">
|
||
<option v-for="mode in mySqlAdmin.indexModes" :key="'index-mode-' + mode.id" :value="mode.id">{{ mode.label }}</option>
|
||
</select>
|
||
</label>
|
||
|
||
<div class="mysql-checkbox-grid">
|
||
<label v-for="column in mySqlAdmin.schema.columns" :key="'index-column-' + column.name" class="mysql-checkbox">
|
||
<input :checked="mySqlAdmin.indexForm.columns.includes(column.name)" type="checkbox" @change="toggleMySqlIndexColumn(column.name)" />
|
||
<span>{{ column.name }}</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh" @click="addMySqlIndex" :disabled="mySqlAdmin.indexForm.saving">
|
||
{{ mySqlAdmin.indexForm.saving ? '提交中...' : '增加索引' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
|
||
<section class="panel mongo-admin-subpanel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>索引</h2>
|
||
<p>{{ selectedMySqlTableItem.name }}</p>
|
||
</div>
|
||
<span class="badge neutral">{{ formatNumber(mySqlAdmin.schema.indexes.length) }} 项</span>
|
||
</header>
|
||
|
||
<div class="table-wrap table-scroll-x">
|
||
<table class="table compact mysql-data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>索引名</th>
|
||
<th>列</th>
|
||
<th>属性</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="index in mySqlAdmin.schema.indexes" :key="'mysql-index-' + index.name">
|
||
<td class="mono">{{ index.name }}</td>
|
||
<td class="mono">{{ (index.columns || []).map((column) => column.name).join(', ') }}</td>
|
||
<td>
|
||
<div class="mysql-tag-list">
|
||
<span class="badge neutral">{{ index.type || '-' }}</span>
|
||
<span class="badge neutral">primary {{ index.primary ? 'Y' : 'N' }}</span>
|
||
<span class="badge neutral">unique {{ index.unique ? 'Y' : 'N' }}</span>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<button class="refresh danger inline-button" @click="dropMySqlIndex(index.name)" :disabled="mySqlAdmin.indexForm.saving">删除</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="mySqlAdmin.schema.indexes.length === 0">
|
||
<td colspan="4" class="table-empty-cell">当前没有索引</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel mongo-admin-subpanel mysql-ddl-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>DDL</h2>
|
||
<p>{{ selectedMySqlTableItem.name }}</p>
|
||
</div>
|
||
</header>
|
||
|
||
<textarea
|
||
:value="mySqlAdmin.schema.createTable"
|
||
class="config-textarea mongo-editor-textarea mono mysql-ddl-textarea"
|
||
spellcheck="false"
|
||
readonly
|
||
></textarea>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-else-if="mySqlAdmin.activeTab === 'sql'">
|
||
<section class="panel mongo-admin-subpanel mysql-console-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>SQL</h2>
|
||
<p>{{ selectedMySqlDatabaseItem.name }}</p>
|
||
</div>
|
||
<span class="badge neutral">{{ mySqlAdmin.sqlConsole.result ? (mySqlAdmin.sqlConsole.result.kind || '-') : '-' }}</span>
|
||
</header>
|
||
|
||
<div class="mysql-shortcut-grid">
|
||
<button
|
||
v-for="shortcut in mySqlSqlShortcuts"
|
||
:key="'mysql-shortcut-' + shortcut.id"
|
||
class="refresh ghost inline-button"
|
||
:disabled="shortcut.disabled"
|
||
@click="applyMySqlSqlShortcut(shortcut)"
|
||
>
|
||
{{ shortcut.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<textarea
|
||
v-model="mySqlAdmin.sqlConsole.text"
|
||
class="config-textarea mongo-editor-textarea mono mysql-console-textarea"
|
||
spellcheck="false"
|
||
></textarea>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh" @click="executeMySqlSql" :disabled="mySqlAdmin.sqlConsole.running">
|
||
{{ mySqlAdmin.sqlConsole.running ? '执行中...' : '执行 SQL' }}
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="mySqlAdmin.sqlConsole.result" class="panel mongo-admin-subpanel mysql-console-result">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>结果</h2>
|
||
<p>{{ mySqlAdmin.sqlConsole.result.kind || '-' }}</p>
|
||
</div>
|
||
<div class="mysql-result-meta">
|
||
<span v-if="mySqlAdmin.sqlConsole.result.rowCount != null" class="badge neutral">{{ formatNumber(mySqlAdmin.sqlConsole.result.rowCount) }} 行</span>
|
||
<span v-if="mySqlAdmin.sqlConsole.result.affectedRows != null" class="badge neutral">影响 {{ formatNumber(mySqlAdmin.sqlConsole.result.affectedRows) }}</span>
|
||
<span v-if="mySqlAdmin.sqlConsole.result.lastInsertId" class="badge neutral">insertId {{ mySqlAdmin.sqlConsole.result.lastInsertId }}</span>
|
||
<span v-if="mySqlAdmin.sqlConsole.result.truncated" class="badge warn">已截断</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div v-if="Array.isArray(mySqlAdmin.sqlConsole.result.columns) && mySqlAdmin.sqlConsole.result.columns.length > 0" class="table-wrap table-scroll-x">
|
||
<table class="table compact mysql-data-table">
|
||
<thead>
|
||
<tr>
|
||
<th v-for="column in mySqlAdmin.sqlConsole.result.columns" :key="'sql-header-' + column">{{ column }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(row, rowIndex) in mySqlAdmin.sqlConsole.result.rows || []" :key="'sql-row-' + rowIndex">
|
||
<td
|
||
v-for="column in mySqlAdmin.sqlConsole.result.columns"
|
||
:key="'sql-cell-' + rowIndex + '-' + column"
|
||
:class="['mono', 'mysql-cell', row[column] == null ? 'is-null' : '']"
|
||
:title="mySqlCellText(row[column])"
|
||
>
|
||
{{ mySqlCellPreview(row[column]) }}
|
||
</td>
|
||
</tr>
|
||
<tr v-if="(mySqlAdmin.sqlConsole.result.rows || []).length === 0">
|
||
<td :colspan="mySqlAdmin.sqlConsole.result.columns.length" class="table-empty-cell">当前没有结果</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-else class="metadata-strip mysql-result-strip">
|
||
<span class="mono">{{ mySqlAdmin.sqlConsole.result.message || '执行完成' }}</span>
|
||
<span>影响 {{ formatNumber(mySqlAdmin.sqlConsole.result.affectedRows || 0) }}</span>
|
||
<span v-if="mySqlAdmin.sqlConsole.result.lastInsertId">insertId {{ mySqlAdmin.sqlConsole.result.lastInsertId }}</span>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-else class="panel infra-panel database-workbench-panel mongo-workbench-panel">
|
||
<div class="compact-workbench-head mongo-workbench-head">
|
||
<div class="metadata-strip metadata-strip-tight">
|
||
<span class="mono">{{ truncateMiddle(payload.mongo.info.host || '未配置 MongoDB', 20, 12) }}</span>
|
||
<span>{{ selectedMongoDatabaseItem ? `db ${selectedMongoDatabaseItem.name}` : `${formatNumber(mongoAdmin.databases.length)} 库` }}</span>
|
||
<span>{{ selectedMongoCollectionItem ? `collection ${selectedMongoCollectionItem.name}` : `${formatNumber(mongoAdminTotalCollectionCount)} 集合` }}</span>
|
||
<span>文档 {{ formatNumber(mongoAdminCollectionStats.count) }}</span>
|
||
<span>索引 {{ formatNumber(mongoAdminCollectionStats.indexCount) }}</span>
|
||
<span v-if="selectedMongoCollectionItem">容量 {{ formatMb(mongoAdminCollectionStats.storageSizeMb) }}</span>
|
||
</div>
|
||
<span :class="['badge', mongoAdmin.error ? 'bad' : 'ok']">{{ mongoAdmin.error ? '存在错误' : '已连接' }}</span>
|
||
</div>
|
||
|
||
<p v-if="mongoAdmin.error" class="metric-error">{{ explainError(mongoAdmin.error) }}</p>
|
||
|
||
<div class="summary-cards mongo-admin-overview-grid">
|
||
<article v-for="card in mongoAdminOverviewCards" :key="'mongo-overview-' + card.key" :class="['summary-card', 'mongo-admin-overview-card', `tone-${card.tone}`]">
|
||
<small>{{ card.label }}</small>
|
||
<strong>{{ card.value }}</strong>
|
||
<span>{{ card.meta }}</span>
|
||
</article>
|
||
</div>
|
||
|
||
<div class="config-layout mongo-admin-layout database-admin-layout">
|
||
<aside class="config-sidebar mongo-sidebar database-sidebar">
|
||
<div class="metadata-strip metadata-strip-tight mongo-sidebar-strip">
|
||
<span>{{ formatNumber(mongoAdmin.databases.length) }} 库</span>
|
||
<span>{{ formatNumber(mongoAdminTotalCollectionCount) }} 集合</span>
|
||
<span v-if="selectedMongoDatabaseItem" class="mono">{{ selectedMongoDatabaseItem.name }}</span>
|
||
<span v-if="selectedMongoCollectionItem">{{ selectedMongoCollectionItem.type || 'collection' }}</span>
|
||
</div>
|
||
|
||
<section class="mongo-sidebar-section">
|
||
<div class="section-caption-row">
|
||
<div class="section-caption">数据库</div>
|
||
<div class="section-caption-meta">
|
||
<span>{{ formatNumber(mongoAdminFilteredDatabases.length) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="config-list config-list-ide mongo-sidebar-list">
|
||
<div v-for="database in mongoAdminFilteredDatabases" :key="'mongo-db-' + database.name" class="config-group">
|
||
<button
|
||
:class="['config-group-toggle', mongoAdmin.selectedDatabase === database.name ? 'active' : '']"
|
||
@click="toggleMongoDatabaseGroup(database.name)"
|
||
>
|
||
<strong>{{ database.name }}</strong>
|
||
<span>{{ formatNumber(database.collectionCount || (database.collections || []).length) }} 项</span>
|
||
<small>{{ isMongoDatabaseCollapsed(database.name) ? '展开' : '收起' }}</small>
|
||
</button>
|
||
|
||
<div v-if="!isMongoDatabaseCollapsed(database.name)" class="config-group-items">
|
||
<button
|
||
v-for="collection in database.visibleCollections"
|
||
:key="'mongo-collection-' + database.name + '-' + collection.name"
|
||
:class="['config-item', 'config-item-ide', isMongoCollectionSelected(database.name, collection.name) ? 'active' : '']"
|
||
@click="selectMongoCollection(database.name, collection.name)"
|
||
>
|
||
<div class="config-item-main">
|
||
<strong>{{ collection.name }}</strong>
|
||
<span>{{ collection.type || 'collection' }}</span>
|
||
</div>
|
||
<div class="config-item-side">
|
||
<small>{{ formatNumber(collection.documentCount) }} docs</small>
|
||
<small>{{ formatNumber(collection.indexCount) }} idx</small>
|
||
</div>
|
||
</button>
|
||
|
||
<div v-if="database.visibleCollections.length === 0" class="config-empty compact">
|
||
当前没有匹配集合
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="!mongoAdmin.loading && mongoAdminFilteredDatabases.length === 0" class="config-empty">
|
||
当前没有可展示的数据库
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</aside>
|
||
|
||
<section class="config-editor mongo-admin-editor database-admin-editor">
|
||
<div class="editor-toolbar sticky-toolbar mongo-editor-toolbar">
|
||
<div class="editor-toolbar-main">
|
||
<span
|
||
v-if="mongoAdmin.message"
|
||
:class="[
|
||
'badge',
|
||
mongoAdmin.messageTone === 'ok'
|
||
? 'ok'
|
||
: mongoAdmin.messageTone === 'warn'
|
||
? 'warn'
|
||
: 'neutral',
|
||
]"
|
||
>
|
||
{{ mongoAdmin.message }}
|
||
</span>
|
||
<span v-else-if="mongoAdmin.loading" class="badge neutral">数据库加载中</span>
|
||
<span v-else-if="mongoAdmin.detailLoading" class="badge neutral">集合明细加载中</span>
|
||
<span v-else-if="mongoAdmin.querying" class="badge neutral">查询中</span>
|
||
<span v-else-if="mongoAdmin.aggregate.running" class="badge neutral">聚合中</span>
|
||
<span v-else class="badge neutral">{{ activeDatabaseTabLabel || 'MongoDB' }}</span>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="resetMongoQueryForm" :disabled="mongoAdmin.querying || mongoAdmin.aggregate.running">重置查询</button>
|
||
<button
|
||
class="refresh ghost"
|
||
@click="mongoAdmin.activeTab === 'aggregate' ? runMongoAggregate() : refreshMongoCollectionDetail()"
|
||
:disabled="!selectedMongoCollectionItem || databaseRefreshBusy"
|
||
>
|
||
{{ databaseRefreshBusy ? '执行中...' : (mongoAdmin.activeTab === 'aggregate' ? '刷新聚合' : '刷新明细') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="selectedMongoDatabaseItem" class="metadata-strip mongo-context-strip">
|
||
<span class="mono">db {{ selectedMongoDatabaseItem.name }}</span>
|
||
<span v-if="selectedMongoCollectionItem" class="mono">collection {{ selectedMongoCollectionItem.name }}</span>
|
||
<span v-if="selectedMongoCollectionItem">docs {{ formatNumber(mongoAdminCollectionStats.count) }}</span>
|
||
<span v-if="selectedMongoCollectionItem">size {{ formatMb(mongoAdminCollectionStats.storageSizeMb) }}</span>
|
||
<span v-if="selectedMongoCollectionItem">index {{ formatNumber(mongoAdminCollectionStats.indexCount) }}</span>
|
||
<span v-if="selectedMongoCollectionItem">capped {{ mongoAdminCollectionStats.capped ? 'Y' : 'N' }}</span>
|
||
<span v-if="mongoAdmin.collectionDetail.updatedAt">更新 {{ formatDateTime(mongoAdmin.collectionDetail.updatedAt) }}</span>
|
||
</div>
|
||
|
||
<div v-if="!selectedMongoDatabaseItem" class="config-empty">
|
||
当前没有可操作的数据库
|
||
</div>
|
||
|
||
<template v-else-if="!selectedMongoCollectionItem">
|
||
<section class="panel mongo-admin-subpanel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>集合</h2>
|
||
<p>{{ selectedMongoDatabaseItem.name }}</p>
|
||
</div>
|
||
<span class="badge neutral">{{ formatNumber(selectedMongoDatabaseItem.collectionCount || (selectedMongoDatabaseItem.collections || []).length) }} 项</span>
|
||
</header>
|
||
|
||
<div class="table-wrap table-scroll-x">
|
||
<table class="table compact mongo-document-table">
|
||
<thead>
|
||
<tr>
|
||
<th>集合</th>
|
||
<th>类型</th>
|
||
<th>文档</th>
|
||
<th>容量</th>
|
||
<th>索引</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="collection in selectedMongoDatabaseItem.collections || []" :key="'mongo-table-list-' + collection.name">
|
||
<td class="mono">{{ collection.name }}</td>
|
||
<td>{{ collection.type || 'collection' }}</td>
|
||
<td>{{ formatNumber(collection.documentCount) }}</td>
|
||
<td>{{ formatMb(collection.storageSizeMb) }}</td>
|
||
<td>{{ formatNumber(collection.indexCount) }}</td>
|
||
<td>
|
||
<button class="refresh ghost inline-button" @click="selectMongoCollection(selectedMongoDatabaseItem.name, collection.name)">打开</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="(selectedMongoDatabaseItem.collections || []).length === 0">
|
||
<td colspan="6" class="table-empty-cell">当前没有集合</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<article class="mongo-action-panel mysql-action-panel tone-write">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>新建集合</h2>
|
||
<p>{{ selectedMongoDatabaseItem.name }}</p>
|
||
</div>
|
||
</header>
|
||
|
||
<label class="update-field">
|
||
<span>库</span>
|
||
<input v-model.trim="mongoAdmin.collectionOps.createDatabaseName" class="search mono" />
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>集合</span>
|
||
<input v-model.trim="mongoAdmin.collectionOps.createName" class="search mono" />
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>Options</span>
|
||
<textarea
|
||
v-model="mongoAdmin.collectionOps.createOptionsText"
|
||
class="config-textarea mongo-json-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh" @click="createMongoCollection" :disabled="mongoAdmin.collectionOps.running">
|
||
{{ mongoAdmin.collectionOps.running ? '提交中...' : '创建集合' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
</template>
|
||
|
||
<template v-else-if="mongoAdmin.activeTab === 'browse'">
|
||
<section class="panel mongo-admin-subpanel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>文档</h2>
|
||
<p>{{ selectedMongoCollectionItem.name }}</p>
|
||
</div>
|
||
<span class="badge neutral">{{ mongoQueryRangeText }}</span>
|
||
</header>
|
||
|
||
<div class="mongo-query-grid">
|
||
<label class="update-field">
|
||
<span>Filter</span>
|
||
<textarea
|
||
v-model="mongoAdmin.query.filterText"
|
||
class="config-textarea mongo-json-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>Sort</span>
|
||
<textarea
|
||
v-model="mongoAdmin.query.sortText"
|
||
class="config-textarea mongo-json-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>Projection</span>
|
||
<textarea
|
||
v-model="mongoAdmin.query.projectionText"
|
||
class="config-textarea mongo-json-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="editor-toolbar mongo-subtoolbar">
|
||
<div class="editor-toolbar-main">
|
||
<span class="badge neutral">总数 {{ formatNumber(mongoAdmin.query.total) }}</span>
|
||
<span class="badge neutral">页数 {{ mongoAdmin.query.page }} / {{ mongoAdmin.query.pageCount }}</span>
|
||
<span class="badge neutral">{{ selectedMongoCollectionItem.type || 'collection' }}</span>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="resetMongoQueryForm" :disabled="mongoAdmin.querying">重置</button>
|
||
<button class="refresh" @click="runMongoQuery({ resetPage: true })" :disabled="mongoAdmin.querying">
|
||
{{ mongoAdmin.querying ? '查询中...' : '查询' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-wrap table-scroll-x">
|
||
<table class="table compact mongo-document-table">
|
||
<thead>
|
||
<tr>
|
||
<th>操作</th>
|
||
<th>_id</th>
|
||
<th>预览</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="item in mongoAdmin.query.items" :key="'mongo-query-row-' + item.key">
|
||
<td class="mysql-row-action-cell">
|
||
<div class="action-group action-group-tight">
|
||
<button class="refresh ghost inline-button" @click="loadMongoDocumentForEdit(item)">编辑</button>
|
||
<button class="refresh ghost inline-button" @click="copyText(item.jsonText, 'mongo-doc-' + item.key)">
|
||
{{ isCopied('mongo-doc-' + item.key) ? '已复制' : '复制' }}
|
||
</button>
|
||
<button class="refresh danger inline-button" @click="deleteMongoDocument(item)">删除</button>
|
||
</div>
|
||
</td>
|
||
<td class="mono">{{ item.idText || '-' }}</td>
|
||
<td class="mono mongo-cell-preview" :title="item.previewText">{{ item.previewText }}</td>
|
||
</tr>
|
||
<tr v-if="mongoAdmin.query.items.length === 0">
|
||
<td colspan="3" class="table-empty-cell">当前没有文档</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="mongo-pager">
|
||
<button class="refresh ghost" @click="changeMongoQueryPage(mongoAdmin.query.page - 1)" :disabled="mongoAdmin.query.page <= 1 || mongoAdmin.querying">上一页</button>
|
||
<span>{{ mongoAdmin.query.page }} / {{ mongoAdmin.query.pageCount }}</span>
|
||
<button class="refresh ghost" @click="changeMongoQueryPage(mongoAdmin.query.page + 1)" :disabled="mongoAdmin.query.page >= mongoAdmin.query.pageCount || mongoAdmin.querying">下一页</button>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="mongo-panel-grid">
|
||
<article class="mongo-action-panel mysql-action-panel tone-edit">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>编辑文档</h2>
|
||
<p>{{ mongoAdmin.editor.idText || selectedMongoCollectionItem.name }}</p>
|
||
</div>
|
||
<button class="refresh ghost" @click="clearMongoDocumentEditor" :disabled="mongoAdmin.editor.saving">清空</button>
|
||
</header>
|
||
|
||
<label class="update-field">
|
||
<span>Filter</span>
|
||
<textarea
|
||
v-model="mongoAdmin.editor.filterText"
|
||
class="config-textarea mongo-json-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>Document</span>
|
||
<textarea
|
||
v-model="mongoAdmin.editor.documentText"
|
||
class="config-textarea mongo-editor-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<label class="table-check">
|
||
<input v-model="mongoAdmin.editor.upsert" type="checkbox" />
|
||
<span>upsert</span>
|
||
</label>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh" @click="saveMongoEditedDocument" :disabled="mongoAdmin.editor.saving">
|
||
{{ mongoAdmin.editor.saving ? '保存中...' : '保存文档' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="mongo-action-panel mysql-action-panel tone-write">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>新增文档</h2>
|
||
<p>{{ selectedMongoCollectionItem.name }}</p>
|
||
</div>
|
||
</header>
|
||
|
||
<label class="update-field">
|
||
<span>Document</span>
|
||
<textarea
|
||
v-model="mongoAdmin.createDocument.documentText"
|
||
class="config-textarea mongo-editor-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<label class="table-check">
|
||
<input v-model="mongoAdmin.createDocument.ordered" type="checkbox" />
|
||
<span>ordered</span>
|
||
</label>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh" @click="insertMongoDocument" :disabled="mongoAdmin.createDocument.saving">
|
||
{{ mongoAdmin.createDocument.saving ? '写入中...' : '插入文档' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="mongoAdmin.activeTab === 'write'">
|
||
<div class="mongo-panel-grid">
|
||
<article class="mongo-action-panel mysql-action-panel tone-edit">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>更新文档</h2>
|
||
<p>{{ selectedMongoCollectionItem.name }}</p>
|
||
</div>
|
||
</header>
|
||
|
||
<label class="update-field">
|
||
<span>Filter</span>
|
||
<textarea
|
||
v-model="mongoAdmin.updateOp.filterText"
|
||
class="config-textarea mongo-json-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>Update</span>
|
||
<textarea
|
||
v-model="mongoAdmin.updateOp.updateText"
|
||
class="config-textarea mongo-editor-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<div class="mongo-check-grid">
|
||
<label class="table-check">
|
||
<input v-model="mongoAdmin.updateOp.many" type="checkbox" />
|
||
<span>updateMany</span>
|
||
</label>
|
||
<label class="table-check">
|
||
<input v-model="mongoAdmin.updateOp.upsert" type="checkbox" />
|
||
<span>upsert</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh" @click="runMongoUpdateDocuments" :disabled="mongoAdmin.updateOp.running">
|
||
{{ mongoAdmin.updateOp.running ? '执行中...' : '执行更新' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="mongo-action-panel mysql-action-panel tone-edit mongo-danger-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>删除文档</h2>
|
||
<p>{{ selectedMongoCollectionItem.name }}</p>
|
||
</div>
|
||
</header>
|
||
|
||
<label class="update-field">
|
||
<span>Filter</span>
|
||
<textarea
|
||
v-model="mongoAdmin.deleteOp.filterText"
|
||
class="config-textarea mongo-json-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<label class="table-check">
|
||
<input v-model="mongoAdmin.deleteOp.many" type="checkbox" />
|
||
<span>deleteMany</span>
|
||
</label>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh danger" @click="runMongoDeleteDocuments" :disabled="mongoAdmin.deleteOp.running">
|
||
{{ mongoAdmin.deleteOp.running ? '执行中...' : '删除文档' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="mongo-action-panel mysql-action-panel tone-structure">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>Distinct</h2>
|
||
<p>{{ selectedMongoCollectionItem.name }}</p>
|
||
</div>
|
||
</header>
|
||
|
||
<label class="update-field">
|
||
<span>Field</span>
|
||
<input v-model.trim="mongoAdmin.distinct.field" class="search mono" />
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>Filter</span>
|
||
<textarea
|
||
v-model="mongoAdmin.distinct.filterText"
|
||
class="config-textarea mongo-json-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh" @click="runMongoDistinct" :disabled="mongoAdmin.distinct.running">
|
||
{{ mongoAdmin.distinct.running ? '执行中...' : '执行去重' }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="metadata-strip mongo-result-strip">
|
||
<span>{{ formatNumber(mongoAdmin.distinct.count) }} 项</span>
|
||
<span class="mono">{{ truncateMiddle(mongoAdmin.distinct.field || '-', 20, 8) }}</span>
|
||
</div>
|
||
|
||
<textarea
|
||
:value="mongoAdmin.distinct.valuesText"
|
||
class="config-textarea mongo-json-textarea mono"
|
||
spellcheck="false"
|
||
readonly
|
||
></textarea>
|
||
</article>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="mongoAdmin.activeTab === 'aggregate'">
|
||
<section class="panel mongo-admin-subpanel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>聚合</h2>
|
||
<p>{{ selectedMongoCollectionItem.name }}</p>
|
||
</div>
|
||
<span class="badge neutral">{{ mongoAggregateRangeText }}</span>
|
||
</header>
|
||
|
||
<div class="mongo-panel-grid narrow">
|
||
<label class="update-field">
|
||
<span>Pipeline</span>
|
||
<textarea
|
||
v-model="mongoAdmin.aggregate.pipelineText"
|
||
class="config-textarea mongo-editor-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<div class="mongo-action-panel mongo-aggregate-side">
|
||
<label class="update-field">
|
||
<span>每页</span>
|
||
<select v-model.number="mongoAdmin.aggregate.pageSize" class="interval-select">
|
||
<option v-for="size in mongoAdmin.pageSizeOptions" :key="'mongo-agg-size-' + size" :value="size">{{ size }}</option>
|
||
</select>
|
||
</label>
|
||
|
||
<div class="metadata-strip mongo-result-strip">
|
||
<span>{{ formatNumber(mongoAdmin.aggregate.total) }} 条</span>
|
||
<span>{{ selectedMongoCollectionItem.type || 'collection' }}</span>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="mongoAdmin.aggregate.page = 1" :disabled="mongoAdmin.aggregate.running">回到第一页</button>
|
||
<button class="refresh" @click="runMongoAggregate({ resetPage: true })" :disabled="mongoAdmin.aggregate.running">
|
||
{{ mongoAdmin.aggregate.running ? '执行中...' : '执行聚合' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel mongo-admin-subpanel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>结果</h2>
|
||
<p>{{ selectedMongoCollectionItem.name }}</p>
|
||
</div>
|
||
<span class="badge neutral">{{ formatNumber(mongoAdmin.aggregate.items.length) }} 条</span>
|
||
</header>
|
||
|
||
<div class="table-wrap table-scroll-x">
|
||
<table class="table compact mongo-document-table">
|
||
<thead>
|
||
<tr>
|
||
<th>操作</th>
|
||
<th>_id</th>
|
||
<th>预览</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="item in mongoAdmin.aggregate.items" :key="'mongo-agg-row-' + item.key">
|
||
<td class="mysql-row-action-cell">
|
||
<div class="action-group action-group-tight">
|
||
<button v-if="item.idFilterText !== '{}'" class="refresh ghost inline-button" @click="loadMongoDocumentForEdit(item)">装载</button>
|
||
<button class="refresh ghost inline-button" @click="copyText(item.jsonText, 'mongo-agg-' + item.key)">
|
||
{{ isCopied('mongo-agg-' + item.key) ? '已复制' : '复制' }}
|
||
</button>
|
||
</div>
|
||
</td>
|
||
<td class="mono">{{ item.idText || '-' }}</td>
|
||
<td class="mono mongo-cell-preview" :title="item.previewText">{{ item.previewText }}</td>
|
||
</tr>
|
||
<tr v-if="mongoAdmin.aggregate.items.length === 0">
|
||
<td colspan="3" class="table-empty-cell">当前没有结果</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="mongo-pager">
|
||
<button class="refresh ghost" @click="changeMongoAggregatePage(mongoAdmin.aggregate.page - 1)" :disabled="mongoAdmin.aggregate.page <= 1 || mongoAdmin.aggregate.running">上一页</button>
|
||
<span>{{ mongoAdmin.aggregate.page }} / {{ mongoAdmin.aggregate.pageCount }}</span>
|
||
<button class="refresh ghost" @click="changeMongoAggregatePage(mongoAdmin.aggregate.page + 1)" :disabled="mongoAdmin.aggregate.page >= mongoAdmin.aggregate.pageCount || mongoAdmin.aggregate.running">下一页</button>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-else-if="mongoAdmin.activeTab === 'indexes'">
|
||
<section class="panel mongo-admin-subpanel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>索引</h2>
|
||
<p>{{ selectedMongoCollectionItem.name }}</p>
|
||
</div>
|
||
<span class="badge neutral">{{ formatNumber(mongoAdmin.collectionDetail.indexes.length) }} 项</span>
|
||
</header>
|
||
|
||
<div class="table-wrap table-scroll-x">
|
||
<table class="table compact mongo-document-table">
|
||
<thead>
|
||
<tr>
|
||
<th>索引名</th>
|
||
<th>Keys</th>
|
||
<th>属性</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="index in mongoAdmin.collectionDetail.indexes" :key="'mongo-index-' + index.name">
|
||
<td class="mono">{{ index.name }}</td>
|
||
<td class="mono">
|
||
<pre class="mongo-index-text">{{ index.keysText }}</pre>
|
||
</td>
|
||
<td>
|
||
<div class="mongo-tag-list">
|
||
<span v-if="index.unique" class="badge neutral">unique</span>
|
||
<span v-if="index.sparse" class="badge neutral">sparse</span>
|
||
<span v-if="index.hidden" class="badge neutral">hidden</span>
|
||
<span v-if="index.expireAfterSeconds != null" class="badge neutral">ttl {{ index.expireAfterSeconds }}</span>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<button class="refresh danger inline-button" @click="dropMongoIndex(index.name)" :disabled="mongoAdmin.indexForm.saving || index.name === '_id_'">删除</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="mongoAdmin.collectionDetail.indexes.length === 0">
|
||
<td colspan="4" class="table-empty-cell">当前没有索引</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="mongo-panel-grid">
|
||
<article class="mongo-action-panel mysql-action-panel tone-structure">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>新增索引</h2>
|
||
<p>{{ selectedMongoCollectionItem.name }}</p>
|
||
</div>
|
||
</header>
|
||
|
||
<label class="update-field">
|
||
<span>Keys</span>
|
||
<textarea
|
||
v-model="mongoAdmin.indexForm.keysText"
|
||
class="config-textarea mongo-json-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>Options</span>
|
||
<textarea
|
||
v-model="mongoAdmin.indexForm.optionsText"
|
||
class="config-textarea mongo-json-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh" @click="createMongoIndex" :disabled="mongoAdmin.indexForm.saving">
|
||
{{ mongoAdmin.indexForm.saving ? '提交中...' : '创建索引' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="mongo-action-panel mysql-action-panel tone-write">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>新建集合</h2>
|
||
<p>{{ selectedMongoDatabaseItem.name }}</p>
|
||
</div>
|
||
</header>
|
||
|
||
<label class="update-field">
|
||
<span>库</span>
|
||
<input v-model.trim="mongoAdmin.collectionOps.createDatabaseName" class="search mono" />
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>集合</span>
|
||
<input v-model.trim="mongoAdmin.collectionOps.createName" class="search mono" />
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>Options</span>
|
||
<textarea
|
||
v-model="mongoAdmin.collectionOps.createOptionsText"
|
||
class="config-textarea mongo-json-textarea mono"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh" @click="createMongoCollection" :disabled="mongoAdmin.collectionOps.running">
|
||
{{ mongoAdmin.collectionOps.running ? '提交中...' : '创建集合' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
|
||
<div class="mongo-panel-grid">
|
||
<article class="mongo-action-panel mysql-action-panel tone-edit">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>重命名集合</h2>
|
||
<p>{{ selectedMongoCollectionItem.name }}</p>
|
||
</div>
|
||
</header>
|
||
|
||
<label class="update-field">
|
||
<span>目标名</span>
|
||
<input v-model.trim="mongoAdmin.collectionOps.renameName" class="search mono" />
|
||
</label>
|
||
|
||
<label class="table-check">
|
||
<input v-model="mongoAdmin.collectionOps.renameDropTarget" type="checkbox" />
|
||
<span>dropTarget</span>
|
||
</label>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh" @click="renameMongoCollection" :disabled="mongoAdmin.collectionOps.running">
|
||
{{ mongoAdmin.collectionOps.running ? '执行中...' : '重命名集合' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="mongo-action-panel mysql-action-panel tone-edit mongo-danger-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>危险操作</h2>
|
||
<p>{{ selectedMongoDatabaseItem.name }}</p>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="action-group action-group-stack">
|
||
<button class="refresh danger" @click="dropMongoSecondaryIndexes" :disabled="mongoAdmin.indexForm.dropAllSaving">
|
||
{{ mongoAdmin.indexForm.dropAllSaving ? '执行中...' : '删除二级索引' }}
|
||
</button>
|
||
<button class="refresh danger" @click="dropMongoCollection" :disabled="mongoAdmin.collectionOps.dropRunning">
|
||
{{ mongoAdmin.collectionOps.dropRunning ? '执行中...' : '删除集合' }}
|
||
</button>
|
||
<button class="refresh danger" @click="dropMongoDatabase" :disabled="mongoAdmin.collectionOps.databaseDropRunning">
|
||
{{ mongoAdmin.collectionOps.databaseDropRunning ? '执行中...' : '删除数据库' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</template>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-if="activeView === 'common' && commonTool === 'cdn'">
|
||
<section class="panel infra-panel common-cdn-panel">
|
||
<div class="compact-workbench-head common-workbench-head">
|
||
<div class="metadata-strip metadata-strip-tight">
|
||
<span class="mono">{{ cdnAllowedDomains[0] || '未配置域名' }}</span>
|
||
<span>限制域名 {{ formatNumber(cdnAllowedDomains.length) }}</span>
|
||
<span>记录 {{ formatNumber(cdnAdmin.overview.taskTotal) }}</span>
|
||
<span>更新时间 {{ formatDateTime(cdnAdmin.overview.updatedAt) }}</span>
|
||
</div>
|
||
<span :class="['badge', cdnOverviewStatusTone]">{{ cdnOverviewStatusText }}</span>
|
||
</div>
|
||
|
||
<p v-if="cdnAdmin.error || cdnAdmin.overview.error" class="metric-error">{{ explainError(cdnAdmin.error || cdnAdmin.overview.error) }}</p>
|
||
<p v-else-if="(cdnAdmin.overview.warnings || []).length > 0" class="metric-error warn">{{ explainError(cdnAdmin.overview.warnings[0]) }}</p>
|
||
|
||
<div class="summary-cards common-cdn-summary">
|
||
<article v-for="card in cdnQuotaCards" :key="'cdn-quota-' + card.key" :class="['summary-card', `tone-${card.tone}`]">
|
||
<small>{{ card.label }}</small>
|
||
<strong>{{ card.value }}</strong>
|
||
<span>{{ card.meta }}</span>
|
||
</article>
|
||
<article v-if="cdnQuotaCards.length === 0" class="summary-card tone-neutral">
|
||
<small>CDN 配额</small>
|
||
<strong>-</strong>
|
||
<span>当前没有可展示配额</span>
|
||
</article>
|
||
</div>
|
||
|
||
<div class="mongo-panel-grid common-cdn-grid">
|
||
<article class="mongo-action-panel common-cdn-form-panel tone-write">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>刷新提交</h2>
|
||
<p>仅支持 https://h5.haiyihy.com/</p>
|
||
</div>
|
||
<span class="badge neutral">{{ formatNumber(cdnFormItemCount) }} 条</span>
|
||
</header>
|
||
|
||
<div class="metadata-strip metadata-strip-tight common-cdn-domain-strip">
|
||
<span class="mono">https://h5.haiyihy.com/</span>
|
||
<span>域名白名单校验已启用</span>
|
||
</div>
|
||
|
||
<label class="update-field">
|
||
<span>刷新类型</span>
|
||
<select v-model="cdnAdmin.form.purgeType" class="interval-select">
|
||
<option value="url">URL</option>
|
||
<option value="path">目录</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="update-field">
|
||
<span>区域</span>
|
||
<select v-model="cdnAdmin.form.area" class="interval-select">
|
||
<option value="">自动</option>
|
||
<option value="mainland">境内</option>
|
||
<option value="overseas">境外</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label v-if="cdnAdmin.form.purgeType === 'path'" class="update-field">
|
||
<span>目录模式</span>
|
||
<select v-model="cdnAdmin.form.flushType" class="interval-select">
|
||
<option value="flush">flush</option>
|
||
<option value="delete">delete</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="table-check">
|
||
<input v-model="cdnAdmin.form.urlEncode" type="checkbox" />
|
||
<span>中文路径自动编码</span>
|
||
</label>
|
||
|
||
<label class="update-field common-cdn-textarea-field">
|
||
<span>URL / 目录</span>
|
||
<textarea
|
||
v-model="cdnAdmin.form.itemsText"
|
||
class="config-textarea mongo-editor-textarea mono common-cdn-textarea"
|
||
:placeholder="cdnFormPlaceholder"
|
||
spellcheck="false"
|
||
></textarea>
|
||
</label>
|
||
|
||
<div v-if="cdnAdmin.lastResult" class="metadata-strip metadata-strip-tight common-cdn-result-strip">
|
||
<span class="mono">task {{ cdnAdmin.lastResult.taskId || '-' }}</span>
|
||
<span>{{ cdnPurgeTypeLabel(cdnAdmin.lastResult.purgeType) }}</span>
|
||
<span>{{ cdnAdmin.lastResult.flushType ? cdnFlushTypeLabel(cdnAdmin.lastResult.flushType) : '-' }}</span>
|
||
<span>{{ formatDateTime(cdnAdmin.lastResult.createdAt) }}</span>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="clearCdnForm" :disabled="cdnAdmin.submitting">清空</button>
|
||
<button class="refresh ghost" @click="refreshCdnOverview()" :disabled="cdnAdmin.loading">刷新记录</button>
|
||
<button class="refresh" @click="submitCdnPurge" :disabled="cdnAdmin.submitting || cdnFormItemCount === 0">
|
||
{{ cdnAdmin.submitting ? '提交中...' : '提交刷新' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="mongo-action-panel common-cdn-task-panel tone-edit">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>最近记录</h2>
|
||
<p>{{ formatNumber(filteredCdnTasks.length) }} 条</p>
|
||
</div>
|
||
<span v-if="cdnAdmin.message" :class="['badge', cdnAdmin.messageTone === 'ok' ? 'ok' : cdnAdmin.messageTone === 'warn' ? 'warn' : 'neutral']">
|
||
{{ cdnAdmin.message }}
|
||
</span>
|
||
<span v-else class="badge neutral">最近 7 天</span>
|
||
</header>
|
||
|
||
<div class="table-wrap table-scroll-x">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>Task</th>
|
||
<th>URL / 目录</th>
|
||
<th>状态</th>
|
||
<th>类型</th>
|
||
<th>模式</th>
|
||
<th>时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="task in filteredCdnTasks" :key="'cdn-task-' + task.taskId + '-' + task.url">
|
||
<td class="mono">{{ truncateMiddle(task.taskId, 14, 6) }}</td>
|
||
<td class="mono common-cdn-url" :title="task.url">{{ truncateMiddle(task.url, 48, 16) }}</td>
|
||
<td>
|
||
<span :class="['badge', cdnTaskStatusTone(task.status)]">{{ cdnTaskStatusLabel(task.status) }}</span>
|
||
</td>
|
||
<td>{{ cdnPurgeTypeLabel(task.purgeType) }}</td>
|
||
<td>{{ task.purgeType === 'path' ? cdnFlushTypeLabel(task.flushType) : '-' }}</td>
|
||
<td>{{ formatDateTime(task.createTime) }}</td>
|
||
</tr>
|
||
<tr v-if="filteredCdnTasks.length === 0">
|
||
<td colspan="6" class="table-empty-cell">当前没有匹配的刷新记录</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-if="activeView === 'common' && commonTool === 'cron'">
|
||
<section class="panel infra-panel common-cron-panel">
|
||
<div class="compact-workbench-head common-workbench-head">
|
||
<div class="metadata-strip metadata-strip-tight">
|
||
<span class="mono">{{ chatappCronSettings.serviceName || 'chatapp-cron' }}</span>
|
||
<span>{{ chatappCronSettings.host || '未配置目标机' }}</span>
|
||
<span class="mono">{{ chatappCronSettings.composeService || '-' }}</span>
|
||
<span>更新时间 {{ formatDateTime(chatappCron.overview.updatedAt) }}</span>
|
||
</div>
|
||
<span :class="['badge', chatappCronStatusTone]">{{ chatappCronStatusText }}</span>
|
||
</div>
|
||
|
||
<p v-if="chatappCron.error || chatappCronMetadata.error" class="metric-error">
|
||
{{ explainError(chatappCron.error || chatappCronMetadata.error) }}
|
||
</p>
|
||
|
||
<div class="summary-cards common-cron-summary">
|
||
<article class="summary-card tone-neutral">
|
||
<small>当前版本</small>
|
||
<strong class="mono">{{ truncateMiddle(chatappCronCurrentCommit, 14, 8) || '-' }}</strong>
|
||
<span>{{ formatDateTime(chatappCronMetadata.deployedAt) || '暂无部署记录' }}</span>
|
||
</article>
|
||
<article class="summary-card tone-neutral">
|
||
<small>Git Ref</small>
|
||
<strong class="mono">{{ chatappCronMetadata.gitRef || chatappCron.form.gitRef || '-' }}</strong>
|
||
<span>{{ chatappCronSettings.repoUrl ? truncateMiddle(chatappCronSettings.repoUrl, 18, 12) : '仓库未配置' }}</span>
|
||
</article>
|
||
<article class="summary-card tone-neutral">
|
||
<small>目标机</small>
|
||
<strong>{{ chatappCronSettings.host || '-' }}</strong>
|
||
<span class="mono">{{ chatappCronSettings.instanceId || chatappCronSettings.sshHost || '-' }}</span>
|
||
</article>
|
||
<article class="summary-card tone-neutral">
|
||
<small>运行目录</small>
|
||
<strong class="mono">{{ truncateMiddle(chatappCronSettings.remoteDir, 16, 12) || '-' }}</strong>
|
||
<span class="mono">{{ truncateMiddle(chatappCronSettings.binaryPath, 16, 12) || '-' }}</span>
|
||
</article>
|
||
</div>
|
||
|
||
<div class="mongo-panel-grid common-cron-grid">
|
||
<article class="mongo-action-panel common-cron-form-panel tone-write">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>部署参数</h2>
|
||
<p>{{ chatappCronSettings.dockerPlatform || 'linux/amd64' }}</p>
|
||
</div>
|
||
<span :class="['badge', chatappCron.overview.enabled ? 'ok' : 'warn']">
|
||
{{ chatappCron.overview.enabled ? '已配置' : '未配置' }}
|
||
</span>
|
||
</header>
|
||
|
||
<label class="update-field">
|
||
<span>Git Ref</span>
|
||
<input v-model.trim="chatappCron.form.gitRef" class="search mono" :placeholder="chatappCronSettings.defaultGitRef || 'main'" />
|
||
</label>
|
||
|
||
<div class="metadata-strip metadata-strip-tight common-cron-target-strip">
|
||
<span class="mono">{{ truncateMiddle(chatappCronSettings.repoRoot, 16, 12) || '-' }}</span>
|
||
<span class="mono">{{ truncateMiddle(chatappCronSettings.composeFile, 16, 12) || '-' }}</span>
|
||
<span>{{ chatappCronSettings.composeService || '-' }}</span>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="refreshChatappCronOverview()" :disabled="chatappCron.loading">
|
||
{{ chatappCron.loading ? '刷新中...' : '刷新配置' }}
|
||
</button>
|
||
<button class="refresh" @click="runChatappCronDeploy" :disabled="chatappCronIsRunning || !chatappCron.overview.enabled">
|
||
{{ chatappCronIsRunning ? '部署中...' : '部署 chatapp-cron' }}
|
||
</button>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="mongo-action-panel common-cron-log-panel tone-edit">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>部署状态</h2>
|
||
<p>{{ chatappCron.operationId ? '任务 ' + chatappCron.operationId : '未开始' }}</p>
|
||
</div>
|
||
<span v-if="chatappCron.message" :class="['badge', chatappCron.messageTone === 'ok' ? 'ok' : chatappCron.messageTone === 'warn' ? 'warn' : 'neutral']">
|
||
{{ chatappCron.message }}
|
||
</span>
|
||
<span v-else :class="['badge', chatappCronStatusTone]">{{ chatappCronStatusText }}</span>
|
||
</header>
|
||
|
||
<div class="metadata-strip metadata-strip-tight common-cron-status-strip">
|
||
<span>{{ chatappCron.stage || '-' }}</span>
|
||
<span>{{ formatDateTime(chatappCron.startedAt) || '未开始' }}</span>
|
||
<span>{{ formatDateTime(chatappCron.finishedAt) || '-' }}</span>
|
||
</div>
|
||
|
||
<div class="release-log-preview common-cron-log-preview">
|
||
<div class="release-log-preview-head">
|
||
<strong>最近日志</strong>
|
||
<button class="refresh ghost compact-button" @click="openChatappCronLogViewer" :disabled="!chatappCron.operationId">
|
||
完整日志
|
||
</button>
|
||
</div>
|
||
<div class="release-log-list">
|
||
<div
|
||
v-for="entry in recentChatappCronLogs"
|
||
:key="entry.key"
|
||
:class="['release-log-entry', entry.level === 'error' ? 'tone-bad' : '']"
|
||
>
|
||
<strong>{{ formatDateTime(entry.time) }}</strong>
|
||
<span>{{ entry.stage || '-' }}</span>
|
||
<small>{{ entry.message }}</small>
|
||
</div>
|
||
<div v-if="recentChatappCronLogs.length === 0" class="release-log-entry">
|
||
<strong>-</strong>
|
||
<span>idle</span>
|
||
<small>暂无部署日志</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<template v-if="!showLoadingSkeleton && activeView === 'config'">
|
||
<section v-if="configTab === 'nacos'" class="panel infra-panel config-workbench-panel">
|
||
<div class="compact-workbench-head config-head-strip">
|
||
<div class="metadata-strip metadata-strip-tight">
|
||
<span class="mono">{{ truncateMiddle(nacosConfig.baseUrl, 18, 12) || '-' }}</span>
|
||
<span>{{ nacosConfig.namespaceName || 'default' }}</span>
|
||
<span class="mono">{{ truncateMiddle(nacosConfig.namespaceId, 10, 8) || '-' }}</span>
|
||
<span>{{ nacosConfigGroups.length }} 组</span>
|
||
<span>{{ filteredNacosConfigs.length }} 项</span>
|
||
</div>
|
||
<span :class="['badge', nacosConfig.error || nacosConfig.detailError ? 'bad' : 'ok']">{{ nacosConfig.error || nacosConfig.detailError ? '存在错误' : '已连接' }}</span>
|
||
</div>
|
||
|
||
<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 config-only-layout">
|
||
<aside class="config-sidebar">
|
||
<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">
|
||
<div class="editor-doc-pill">
|
||
<strong>{{ nacosConfig.editor.dataId }}</strong>
|
||
<small>{{ nacosConfig.editor.group }} · {{ nacosConfig.editor.type || 'text' }}</small>
|
||
</div>
|
||
<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 || (nacosConfigDirty && configEditorMode !== 'diff')"
|
||
:title="nacosConfigDirty && configEditorMode !== 'diff' ? '保存前先切到 Diff' : ''"
|
||
>
|
||
{{ nacosConfig.saving ? '保存中...' : '保存' }}
|
||
</button>
|
||
</div>
|
||
<div class="editor-toolbar-meta">
|
||
<span>最近修改 {{ formatDateTime(nacosConfig.editor.lastModifiedTime) }}</span>
|
||
<span>{{ nacosConfig.validationMessage ? '已校验' : '未校验' }}</span>
|
||
<span v-if="nacosConfig.editor.appName">app {{ 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>
|
||
</div>
|
||
</div>
|
||
|
||
<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 config-workbench-panel">
|
||
<div class="compact-workbench-head config-head-strip">
|
||
<div class="metadata-strip metadata-strip-tight">
|
||
<span>{{ goConfig.hosts.length }} 台主机</span>
|
||
<span>差异 key {{ goConfig.diffKeys.length }}</span>
|
||
<span>service.env</span>
|
||
</div>
|
||
<span :class="['badge', goConfig.error ? 'bad' : goConfig.diffKeys.length > 0 ? 'warn' : 'ok']">
|
||
{{ goConfig.error ? '存在错误' : goConfig.diffKeys.length > 0 ? '双机有差异' : '已同步' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="metadata-strip metadata-strip-tight config-inline-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">
|
||
<div class="editor-doc-pill">
|
||
<strong>service.env</strong>
|
||
<small>Go Env</small>
|
||
</div>
|
||
<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 || (goConfigDirty && goEditorMode !== 'diff')"
|
||
:title="goConfigDirty && goEditorMode !== 'diff' ? '保存前先切到 Diff' : ''"
|
||
>
|
||
{{ goConfig.saving ? '保存中...' : '保存' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="metadata-strip metadata-strip-tight config-inline-strip">
|
||
<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 config-workbench-panel">
|
||
<div class="compact-workbench-head config-head-strip">
|
||
<div class="metadata-strip metadata-strip-tight">
|
||
<span>本地记录 {{ configHistoryRows.length }}</span>
|
||
<span>当前未接远端版本历史</span>
|
||
</div>
|
||
<span class="badge neutral">历史版本</span>
|
||
</div>
|
||
|
||
<div class="activity-list activity-grid config-history-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="!showLoadingSkeleton && activeView === 'release'">
|
||
<section class="panel release-status-strip sticky-release-strip">
|
||
<div class="release-command-row" aria-label="发布操作">
|
||
<div class="release-action-summary compact">
|
||
<span :class="['badge', releasePrimaryStatusTone()]">{{ releasePrimaryStatusLabel() }}</span>
|
||
<span class="badge neutral">更新 {{ updateOps.selectedServices.length }} / {{ releaseTargetRows.length }}</span>
|
||
<span class="badge neutral">重启 {{ restartOps.selectedServices.length }} / {{ restartTargetRows.length }}</span>
|
||
<span :class="['badge', releasePrecheck.ok ? 'ok' : releasePrecheck.ranAt ? 'warn' : 'neutral']">
|
||
{{ releasePrecheckBadgeLabel() }}
|
||
</span>
|
||
</div>
|
||
<div class="action-group release-command-actions">
|
||
<button class="refresh ghost" @click="buildParamsOpen = true">构建参数</button>
|
||
<template v-if="releaseIsRunning()">
|
||
<button class="refresh" @click="openUpdateLogViewer" :disabled="!updateOps.operationId && !updateOps.logs.length">查看日志</button>
|
||
<button class="refresh ghost" @click="fetchUpdateOperationStatus(updateOps.operationId)" :disabled="!updateOps.operationId || updateOps.statusRequestInFlight">
|
||
{{ updateOps.statusRequestInFlight ? '刷新中...' : '刷新状态' }}
|
||
</button>
|
||
<button class="refresh ghost" @click="stopUpdatePolling" :disabled="!updateOps.pollTimer">停止轮询</button>
|
||
<span class="badge warn">等待完成</span>
|
||
</template>
|
||
<template v-else>
|
||
<button class="refresh ghost" @click="refreshReleaseTargets" :disabled="updateOps.loading || restartOps.loading">
|
||
{{ updateOps.loading || restartOps.loading ? '刷新中...' : '刷新目标' }}
|
||
</button>
|
||
<button class="refresh ghost" @click="runReleasePrecheck">预检查</button>
|
||
<button class="refresh ghost" @click="openUpdateLogViewer" :disabled="!updateOps.operationId && !updateOps.logs.length">查看日志</button>
|
||
<button
|
||
:class="['refresh', updateOps.selectedServices.length > 0 ? 'danger' : 'ghost']"
|
||
@click="runServiceUpdate"
|
||
:disabled="updateOps.selectedServices.length === 0"
|
||
>
|
||
{{ updateOps.selectedServices.length > 0 ? `执行更新 ${updateOps.selectedServices.length}` : '执行更新' }}
|
||
</button>
|
||
<button
|
||
:class="['refresh', restartOps.selectedServices.length > 0 ? 'danger' : 'ghost']"
|
||
@click="runRollingRestart"
|
||
:disabled="restartOps.selectedServices.length === 0"
|
||
>
|
||
{{ restartOps.selectedServices.length > 0 ? `执行重启 ${restartOps.selectedServices.length}` : '执行重启' }}
|
||
</button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<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]">{{ releasePrecheckBadgeLabel() }}</span>
|
||
<span class="badge neutral">执行于 {{ formatDateTime(releasePrecheck.ranAt) }}</span>
|
||
</div>
|
||
|
||
<p v-if="updateOps.error" class="metric-error">{{ explainError(updateOps.error) }}</p>
|
||
<p v-else-if="updateOps.message && !releaseIsRunning()" :class="updateOps.result && updateOps.result.ok ? 'ok-text' : 'metric-error'">{{ updateOps.message }}</p>
|
||
</section>
|
||
|
||
<section id="release-targets" class="panel infra-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>更新目标</h2>
|
||
</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 v-if="releaseTargetRows.length > 0" class="release-card-grid">
|
||
<article
|
||
v-for="item in releaseTargetRows"
|
||
: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">
|
||
<div class="release-service-title-line">
|
||
<strong>{{ item.service }}</strong>
|
||
<span class="badge neutral">{{ item.kind }}</span>
|
||
<span class="badge neutral">{{ item.hostCount }} 节点</span>
|
||
</div>
|
||
<div class="release-service-meta">
|
||
<span :title="item.hostsLabel">{{ item.hostsLabel }}</span>
|
||
<span class="mono" :title="item.currentTitle">版本 {{ item.currentLabel }}</span>
|
||
<span class="mono" :title="item.detailTitle || '-'">摘要 {{ item.detailLabel || '-' }}</span>
|
||
</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="release-service-action-row">
|
||
<button
|
||
v-if="item.kind === 'frontend'"
|
||
class="refresh inline-button"
|
||
@click.stop="runServiceUpdate([item.service])"
|
||
@keydown.enter.stop
|
||
@keydown.space.stop
|
||
:disabled="releaseIsRunning() || !updateOps.adminGitRef.trim()"
|
||
:title="!updateOps.adminGitRef.trim() ? '先填写 Admin Git Ref' : ''"
|
||
>
|
||
{{ item.service === 'admin' ? 'admin部署' : item.service + '部署' }}
|
||
</button>
|
||
<button
|
||
v-if="item.copyValue"
|
||
class="refresh ghost inline-button"
|
||
@click.stop="copyText(item.copyValue, 'image-' + item.service)"
|
||
@keydown.enter.stop
|
||
@keydown.space.stop
|
||
>
|
||
{{ isCopied('image-' + item.service) ? '已复制' : '复制' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
</article>
|
||
</div>
|
||
<div v-else class="empty-panel compact">{{ updateOps.loading ? '加载服务中...' : '当前没有可展示的服务' }}</div>
|
||
</section>
|
||
|
||
<section id="release-log" class="panel infra-panel">
|
||
<header class="panel-header">
|
||
<div class="panel-title">
|
||
<h2>执行日志</h2>
|
||
</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-list">
|
||
<article v-for="entry in recentUpdateLogs" :key="entry.key" :class="['release-log-entry', entry.level === 'error' ? 'tone-bad' : 'tone-neutral']">
|
||
<strong>{{ formatDateTime(entry.time) }}</strong>
|
||
<span>{{ entry.stage || '-' }}</span>
|
||
<small>{{ entry.message || '-' }}</small>
|
||
</article>
|
||
</div>
|
||
<div v-else class="empty-panel compact">当前还没有可展示的执行日志</div>
|
||
</section>
|
||
|
||
<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>
|
||
</div>
|
||
<div class="panel-meta">
|
||
<span :class="['badge', restartOps.error ? 'bad' : restartOps.running ? 'warn' : 'ok']">
|
||
{{ restartOps.error ? '存在错误' : restartOps.running ? '执行中' : '可操作' }}
|
||
</span>
|
||
</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 v-if="restartTargetRows.length > 0" class="release-card-grid">
|
||
<article
|
||
v-for="item in restartTargetRows"
|
||
: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">
|
||
<div class="release-service-title-line">
|
||
<strong>{{ item.service }}</strong>
|
||
<span class="badge neutral">{{ item.kind }}</span>
|
||
<span class="badge neutral">{{ item.hostCount }} 实例</span>
|
||
</div>
|
||
<div class="release-service-meta">
|
||
<span :title="item.hostsLabel">{{ item.hostsLabel }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="release-service-actions release-service-actions-tight">
|
||
<span :class="['release-select-state', restartOps.selectedServices.includes(item.service) ? 'is-selected' : '']">
|
||
{{ restartOps.selectedServices.includes(item.service) ? '已选' : '未选' }}
|
||
</span>
|
||
</div>
|
||
</header>
|
||
</article>
|
||
</div>
|
||
<div v-else class="empty-panel compact">{{ restartOps.loading ? '加载重启目标中...' : '当前没有可展示的重启目标' }}</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="buildParamsOpen" class="log-viewer-overlay" @click.self="buildParamsOpen = false">
|
||
<section class="log-viewer build-params-dialog" role="dialog" aria-modal="true" aria-labelledby="build-params-title">
|
||
<header class="log-viewer-head">
|
||
<div class="panel-title">
|
||
<h2 id="build-params-title">构建参数</h2>
|
||
</div>
|
||
<div class="action-group">
|
||
<span class="badge neutral">平台 {{ updateOps.defaults.dockerPlatform }}</span>
|
||
<button class="refresh ghost" @click="buildParamsOpen = false">关闭</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="metadata-strip">
|
||
<span class="mono">Harbor {{ updateOps.builder.harborRegistry }} / {{ updateOps.builder.harborProject }}</span>
|
||
<span class="mono">Java {{ truncateMiddle(updateOps.builder.javaRepoRoot, 18, 14) }}</span>
|
||
<span class="mono">Go {{ truncateMiddle(updateOps.builder.golangRepoRoot, 18, 14) }}</span>
|
||
<span class="mono">Admin {{ truncateMiddle(updateOps.builder.frontendRepoRoot || updateOps.builder.frontendRepoUrl, 18, 14) }}</span>
|
||
</div>
|
||
|
||
<div class="update-form-grid build-params-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>
|
||
<div class="update-field update-field-toggle build-params-toggle">
|
||
<span>构建选项</span>
|
||
<label class="update-check">
|
||
<input v-model="updateOps.skipMavenBuild" type="checkbox" />
|
||
<strong>跳过 Maven</strong>
|
||
<small>仅影响 Java 构建阶段</small>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<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>
|