feat: update delivery and resource frontends
This commit is contained in:
@@ -3,14 +3,15 @@
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>集群与容器</h1>
|
||||
<p>统一建设容器池 · RKE2 上建 Wayne 后新服务的标准发布流程 · Calico BGP 扁平网络</p>
|
||||
<p>统一建设容器池 · RKE2 上建 Wayne 后新服务的标准发布流程 · 数据源:Wayne 原生接口</p>
|
||||
</div>
|
||||
<el-button type="primary">同步</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="loadClusters">同步</el-button>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-body">
|
||||
<table>
|
||||
<div v-if="error" class="error-line">{{ error }}</div>
|
||||
<table v-loading="loading">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>集群</th>
|
||||
@@ -24,7 +25,15 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="cluster in businessLineClusters" :key="cluster.name" class="tr-hover">
|
||||
<tr v-if="!businessLineClusters.length">
|
||||
<td colspan="8" class="text-dim">当前业务线暂无 Wayne 集群数据</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="cluster in businessLineClusters"
|
||||
:key="cluster.name"
|
||||
:class="['tr-hover', { selected: cluster.name === selectedCluster }]"
|
||||
@click="selectedCluster = cluster.name"
|
||||
>
|
||||
<td class="strong">{{ cluster.name }}</td>
|
||||
<td><span class="tag" :class="cluster.zoneClass">{{ cluster.zone }}</span></td>
|
||||
<td :class="['status-text', cluster.statusClass]">● {{ cluster.status }}</td>
|
||||
@@ -43,11 +52,11 @@
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>{{ businessLineClusters[0]?.name }} · 节点列表(节选)</h3>
|
||||
<h3>{{ selectedCluster || businessLineClusters[0]?.name || '未选择集群' }} · 节点列表</h3>
|
||||
<span class="meta">node-label 多租户隔离</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table>
|
||||
<table v-loading="loading">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>节点</th>
|
||||
@@ -60,13 +69,21 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="!businessLineNodes.length">
|
||||
<td colspan="7" class="text-dim">当前集群暂无 Wayne 节点数据</td>
|
||||
</tr>
|
||||
<tr v-for="node in businessLineNodes" :key="node.name" class="tr-hover">
|
||||
<td class="strong mono">{{ node.name }}</td>
|
||||
<td class="mono">{{ node.ip }}</td>
|
||||
<td><span class="tag biz" :style="{ color: node.labelColor, borderColor: node.labelBorder }">{{ node.label }}</span></td>
|
||||
<td><span class="tag biz">{{ node.label }}</span></td>
|
||||
<td class="mono text-xs">{{ node.taint }}</td>
|
||||
<td class="mono">{{ node.spec }}</td>
|
||||
<td><span class="bar-wrap"><span class="bar-fill" :class="node.cpuClass" :style="{ width: node.cpu + '%' }"></span></span>{{ node.cpu }}%</td>
|
||||
<td>
|
||||
<template v-if="node.cpu > 0">
|
||||
<span class="bar-wrap"><span class="bar-fill" :class="node.cpuClass" :style="{ width: node.cpu + '%' }"></span></span>{{ node.cpu }}%
|
||||
</template>
|
||||
<span v-else class="text-dim">-</span>
|
||||
</td>
|
||||
<td :class="['status-text', node.statusClass]">● {{ node.status }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -77,61 +94,77 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useBusinessLineMockProfile } from '@/utils/businessLineMock'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { containerServiceApi, type ContainerCluster, type ContainerNode } from '@/api/containerService'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
|
||||
const { currentName, profile } = useBusinessLineMockProfile()
|
||||
const businessLineStore = useBusinessLineStore()
|
||||
const clusters = ref<ContainerCluster[]>([])
|
||||
const nodes = ref<ContainerNode[]>([])
|
||||
const selectedCluster = ref('')
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const clusters = ref([
|
||||
{ name: 'rke2-bj-prod-01', zone: '华北 · IDC', zoneClass: 'zone-a', status: '健康', statusClass: 'ok', nodes: 46, cpu: 64, cpuClass: '', version: 'v1.28.9+rke2r1', calico: 'BGP AS64512' },
|
||||
{ name: 'rke2-sh-prod-01', zone: '华东 · IDC', zoneClass: 'zone-b', status: '健康', statusClass: 'ok', nodes: 52, cpu: 71, cpuClass: '', version: 'v1.28.9+rke2r1', calico: 'BGP AS64513' },
|
||||
{ name: 'rke2-ali-sz-01', zone: '阿里云 · 华南', zoneClass: 'zone-c', status: '1 节点告警', statusClass: 'warn', nodes: 30, cpu: 83, cpuClass: 'warn', version: 'v1.28.6+rke2r1', calico: 'BGP AS64514' },
|
||||
])
|
||||
|
||||
const nodes = ref([
|
||||
{ name: 'bj-node-014', ip: '10.21.4.14', label: 'business-line=kodo', labelColor: 'var(--tag-blue-text)', labelBorder: 'var(--tag-blue-border)', taint: 'kodo:NoSchedule', spec: '64C/256G', cpu: 58, cpuClass: '', status: 'Ready', statusClass: 'ok' },
|
||||
{ name: 'bj-node-015', ip: '10.21.4.15', label: 'business-line=linxi', labelColor: 'var(--tag-blue-text)', labelBorder: 'var(--tag-blue-border)', taint: 'linxi:NoSchedule', spec: '64C/256G', cpu: 62, cpuClass: '', status: 'Ready', statusClass: 'ok' },
|
||||
{ name: 'bj-node-031', ip: '10.21.4.31', label: 'business-line=xinfra', labelColor: 'var(--tag-purple-text)', labelBorder: 'var(--tag-purple-border)', taint: 'xinfra:NoSchedule', spec: '32C/128G', cpu: 88, cpuClass: 'warn', status: '资源告警', statusClass: 'warn' },
|
||||
{ name: 'bj-node-048', ip: '10.21.4.48', label: 'business-line=las', labelColor: 'var(--text-dim)', labelBorder: 'var(--line)', taint: 'las:NoSchedule', spec: '32C/128G', cpu: 4, cpuClass: '', status: 'Ready · 空闲', statusClass: 'ok' },
|
||||
])
|
||||
|
||||
const businessLineClusters = computed(() => {
|
||||
const cpu = profile.value.cpuAllocated
|
||||
return clusters.value.map((cluster, index) => ({
|
||||
...cluster,
|
||||
name: `${cluster.name}-${currentName.value}`,
|
||||
nodes: Math.max(1, Math.round(profile.value.nodes * ([0.42, 0.36, 0.22][index] || 0.2))),
|
||||
cpu: Math.min(96, Math.max(8, cpu + (index - 1) * 7)),
|
||||
cpuClass: cpu > 75 ? 'warn' : '',
|
||||
status: profile.value.alertsP1 && index === 2 ? `${profile.value.alertsP1} 节点告警` : '健康',
|
||||
statusClass: profile.value.alertsP1 && index === 2 ? 'warn' : 'ok',
|
||||
}))
|
||||
})
|
||||
const businessLineClusters = computed(() => clusters.value)
|
||||
|
||||
const businessLineNodes = computed(() => {
|
||||
const selected = nodes.value.filter((node) => node.label === `business-line=${currentName.value}`)
|
||||
if (selected.length) return selected
|
||||
return [
|
||||
{
|
||||
name: `${currentName.value}-node-001`,
|
||||
ip: '10.21.4.14',
|
||||
label: `business-line=${currentName.value}`,
|
||||
labelColor: 'var(--tag-blue-text)',
|
||||
labelBorder: 'var(--tag-blue-border)',
|
||||
taint: `${currentName.value}:NoSchedule`,
|
||||
spec: '64C/256G',
|
||||
cpu: profile.value.cpuAllocated,
|
||||
cpuClass: profile.value.cpuAllocated > 75 ? 'warn' : '',
|
||||
status: profile.value.alertsP1 ? '资源告警' : 'Ready',
|
||||
statusClass: profile.value.alertsP1 ? 'warn' : 'ok',
|
||||
},
|
||||
]
|
||||
if (!selectedCluster.value) return []
|
||||
return nodes.value.filter((node) => node.cluster === selectedCluster.value)
|
||||
})
|
||||
|
||||
async function loadClusters() {
|
||||
const businessLineId = businessLineStore.current?.id
|
||||
if (!businessLineId) {
|
||||
clusters.value = []
|
||||
nodes.value = []
|
||||
selectedCluster.value = ''
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const data = await containerServiceApi.listClusters(businessLineId)
|
||||
clusters.value = data.items
|
||||
nodes.value = data.nodes
|
||||
if (!clusters.value.some((cluster) => cluster.name === selectedCluster.value)) {
|
||||
selectedCluster.value = clusters.value[0]?.name || ''
|
||||
}
|
||||
if (data.summary.errors?.length) {
|
||||
error.value = data.summary.errors.join(';')
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Wayne 集群数据同步失败'
|
||||
clusters.value = []
|
||||
nodes.value = []
|
||||
selectedCluster.value = ''
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => businessLineStore.current?.id,
|
||||
() => {
|
||||
void loadClusters()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(loadClusters)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 公共样式已在 global.css 中定义 */
|
||||
|
||||
.tr-hover.selected {
|
||||
background: rgba(22, 93, 255, 0.08);
|
||||
}
|
||||
|
||||
.error-line {
|
||||
margin-bottom: 12px;
|
||||
color: var(--danger);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tag.biz {
|
||||
color: var(--text-mid);
|
||||
}
|
||||
|
||||
@@ -5,60 +5,37 @@
|
||||
<h1>机器管理</h1>
|
||||
<p>统一纳管物理机与虚机资源 · 基础数据来自 SINA CMDB</p>
|
||||
</div>
|
||||
<el-button type="primary">同步</el-button>
|
||||
<el-button type="primary" :loading="syncing" @click="handleSync">同步</el-button>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<div class="stat-card">
|
||||
<div class="label">资源总数</div>
|
||||
<div class="value">{{ filteredResources.length }}</div>
|
||||
<div class="delta">物理机 {{ physicalCount }} · 虚机 {{ vmCount }}</div>
|
||||
<div class="value">{{ overview.total }}</div>
|
||||
<div class="delta">物理机 {{ overview.physical }} · 虚机 {{ overview.virtual }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">SINA CMDB</div>
|
||||
<div class="value">368</div>
|
||||
<div class="delta">物理机 146 · 虚机 222</div>
|
||||
<div class="value">{{ overview.cmdb.total }}</div>
|
||||
<div class="delta">物理机 {{ overview.cmdb.physical }} · 虚机 {{ overview.cmdb.virtual }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">阿里云同步</div>
|
||||
<div class="value">154</div>
|
||||
<div class="delta">ECS · 增量同步</div>
|
||||
<div class="value">{{ overview.aliyun.total }}</div>
|
||||
<div class="delta">{{ overview.aliyun.label }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">AWS / 七牛同步</div>
|
||||
<div class="value">136</div>
|
||||
<div class="delta">AWS 58 · 七牛 78</div>
|
||||
<div class="value">{{ overview.awsQiniu.total }}</div>
|
||||
<div class="delta">AWS {{ overview.awsQiniu.aws }} · 七牛 {{ overview.awsQiniu.qiniu }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">最近一次同步</div>
|
||||
<div class="value accent-pill">● 正常</div>
|
||||
<div class="delta">07:39:10 · 新增 3 条</div>
|
||||
<div class="value accent-pill">● {{ syncStatusText }}</div>
|
||||
<div class="delta">{{ overview.lastSync.time }} · 新增 {{ overview.lastSync.created }} 条</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="las-toolbar">
|
||||
<el-select placeholder="全部资源类型">
|
||||
<el-option label="全部资源类型" value="" />
|
||||
<el-option label="物理机" value="physical" />
|
||||
<el-option label="虚机" value="vm" />
|
||||
</el-select>
|
||||
<el-select placeholder="全部数据来源">
|
||||
<el-option label="全部数据来源" value="" />
|
||||
<el-option label="SINA CMDB" value="cmdb" />
|
||||
<el-option label="阿里云同步" value="aliyun" />
|
||||
<el-option label="AWS 同步" value="aws" />
|
||||
<el-option label="七牛同步" value="qiniu" />
|
||||
</el-select>
|
||||
<el-select placeholder="全部业务线">
|
||||
<el-option label="全部业务线" value="" />
|
||||
<el-option label="kodo" value="kodo" />
|
||||
<el-option label="linxi" value="linxi" />
|
||||
<el-option label="xinfra" value="xinfra" />
|
||||
<el-option label="las" value="las" />
|
||||
</el-select>
|
||||
<el-input placeholder="搜索 hostname / asset_number / IP" class="search-input" />
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>资源台账 · 物理机 / 虚机</h3>
|
||||
@@ -78,31 +55,120 @@
|
||||
<th>数据来源</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
<tr class="filter-row">
|
||||
<th>
|
||||
<el-input
|
||||
v-model="filters.hostname"
|
||||
clearable
|
||||
size="small"
|
||||
placeholder="筛选主机名"
|
||||
@clear="reloadFirstPage"
|
||||
@keyup.enter="reloadFirstPage"
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<el-input
|
||||
v-model="filters.assetNumber"
|
||||
clearable
|
||||
size="small"
|
||||
placeholder="筛选资产编号"
|
||||
@clear="reloadFirstPage"
|
||||
@keyup.enter="reloadFirstPage"
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<el-select v-model="filters.type" clearable size="small" placeholder="全部" @change="reloadFirstPage">
|
||||
<el-option label="物理机" value="physical" />
|
||||
<el-option label="虚机" value="vm" />
|
||||
</el-select>
|
||||
</th>
|
||||
<th>
|
||||
<el-input
|
||||
v-model="filters.location"
|
||||
clearable
|
||||
size="small"
|
||||
placeholder="筛选机房"
|
||||
@clear="reloadFirstPage"
|
||||
@keyup.enter="reloadFirstPage"
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<el-input
|
||||
v-model="filters.ip"
|
||||
clearable
|
||||
size="small"
|
||||
placeholder="筛选 IP"
|
||||
@clear="reloadFirstPage"
|
||||
@keyup.enter="reloadFirstPage"
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<el-input
|
||||
v-model="filters.spec"
|
||||
clearable
|
||||
size="small"
|
||||
placeholder="筛选规格"
|
||||
@clear="reloadFirstPage"
|
||||
@keyup.enter="reloadFirstPage"
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<el-input
|
||||
v-model="filters.businessLine"
|
||||
clearable
|
||||
size="small"
|
||||
placeholder="筛选业务线"
|
||||
@clear="reloadFirstPage"
|
||||
@keyup.enter="reloadFirstPage"
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<el-select v-model="filters.source" clearable size="small" placeholder="全部" @change="reloadFirstPage">
|
||||
<el-option label="SINA CMDB" value="cmdb" />
|
||||
<el-option label="阿里云同步" value="aliyun" />
|
||||
<el-option label="AWS 同步" value="aws" />
|
||||
<el-option label="七牛同步" value="qiniu" />
|
||||
</el-select>
|
||||
</th>
|
||||
<th>
|
||||
<el-select v-model="filters.status" clearable size="small" placeholder="全部" @change="reloadFirstPage">
|
||||
<el-option label="production" value="production" />
|
||||
<el-option label="stock" value="stock" />
|
||||
<el-option label="SREshutdown" value="SREshutdown" />
|
||||
<el-option label="unmon" value="unmon" />
|
||||
<el-option label="ignore" value="ignore" />
|
||||
</el-select>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in filteredResources" :key="item.hostname" class="tr-hover">
|
||||
<tr v-for="item in resources" :key="`${item.hostname}-${item.ip}`" class="tr-hover">
|
||||
<td class="strong mono">{{ item.hostname }}</td>
|
||||
<td class="mono text-xs">{{ item.asset_number }}</td>
|
||||
<td class="mono text-xs">{{ item.assetNumber }}</td>
|
||||
<td><span class="tag" :class="item.type === '物理机' ? '' : 'vm'">{{ item.type }}</span></td>
|
||||
<td><span class="tag zone-a">{{ item.location }}</span></td>
|
||||
<td class="mono">{{ item.ip }}</td>
|
||||
<td class="mono text-xs">{{ item.spec }}</td>
|
||||
<td class="mono">{{ item.business_line }}</td>
|
||||
<td class="mono">{{ item.businessLine }}</td>
|
||||
<td><span class="tag src-cmdb">{{ item.source }}</span></td>
|
||||
<td class="status-text ok">● {{ item.status }}</td>
|
||||
</tr>
|
||||
<tr v-if="!loading && resources.length === 0">
|
||||
<td colspan="9" class="empty-cell">暂无机器数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination">
|
||||
<span>共 {{ filteredResources.length }} 条 · 当前业务线:{{ currentName }}</span>
|
||||
<span>共 {{ total }} 条</span>
|
||||
<div class="pg-btns">
|
||||
<span class="pg-btn disabled">‹</span>
|
||||
<span class="pg-btn active">1</span>
|
||||
<span class="pg-btn">2</span>
|
||||
<span class="pg-btn">3</span>
|
||||
<span class="pg-sep">…</span>
|
||||
<span class="pg-btn">94</span>
|
||||
<span class="pg-btn">›</span>
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="size"
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:total="total"
|
||||
@current-change="loadResources"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,24 +177,81 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useBusinessLineMockProfile } from '@/utils/businessLineMock'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { emptyMachineOverview, machineApi, type MachineResource } from '@/api/machine'
|
||||
|
||||
const { currentName } = useBusinessLineMockProfile()
|
||||
const overview = ref(emptyMachineOverview)
|
||||
const resources = ref<MachineResource[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const size = ref(20)
|
||||
const loading = ref(false)
|
||||
const syncing = ref(false)
|
||||
const filters = reactive({
|
||||
hostname: '',
|
||||
assetNumber: '',
|
||||
type: '',
|
||||
location: '',
|
||||
ip: '',
|
||||
spec: '',
|
||||
businessLine: '',
|
||||
source: '',
|
||||
status: '',
|
||||
})
|
||||
|
||||
const resources = ref([
|
||||
{ hostname: 'xs291', asset_number: 'SERV00003502', type: '物理机', location: '华东·杭州下沙', ip: '10.34.37.52', spec: '20C/192G/13.52T', business_line: 'kodo', source: 'SINA CMDB', status: 'production' },
|
||||
{ hostname: 'yzh-las-014', asset_number: 'SERV00004187', type: '虚机', location: '华北·YZH', ip: '10.21.4.214', spec: '32C/128G/4.0T', business_line: 'las', source: 'SINA CMDB', status: 'production' },
|
||||
{ hostname: 'jf-bm-118', asset_number: 'SERV00004290', type: '物理机', location: '华南·JF', ip: '10.45.2.18', spec: '16C/64G/2.0T', business_line: 'linxi', source: 'SINA CMDB', status: 'production' },
|
||||
{ hostname: 'ali-ecs-sz-0231', asset_number: '—', type: '虚机', location: '阿里云·华南', ip: '172.18.4.31', spec: '16C/64G/500G', business_line: 'xinfra', source: '阿里云同步', status: 'production' },
|
||||
{ hostname: 'aws-us-i-0a13fe2', asset_number: '—', type: '虚机', location: 'AWS · 美国', ip: '10.66.2.12', spec: '8C/32G/200G', business_line: 'las', source: 'AWS 同步', status: 'production' },
|
||||
{ hostname: 'sg-las-007', asset_number: 'SERV00004511', type: '虚机', location: '七牛 · 新加坡', ip: '10.88.1.07', spec: '16C/64G/2.0T', business_line: 'las', source: '七牛同步', status: 'idle' },
|
||||
{ hostname: 'hk-bm-001', asset_number: 'SERV00004602', type: '物理机', location: '香港 IDC', ip: '10.90.0.11', spec: '8C/32G/1.0T', business_line: 'xinfra', source: 'SINA CMDB', status: 'production' },
|
||||
])
|
||||
const syncStatusText = computed(() => {
|
||||
if (overview.value.lastSync.status === 'normal') return '正常'
|
||||
if (overview.value.lastSync.status === 'failed') return '失败'
|
||||
return '未知'
|
||||
})
|
||||
|
||||
const filteredResources = computed(() => resources.value.filter((item) => item.business_line === currentName.value))
|
||||
const physicalCount = computed(() => filteredResources.value.filter((item) => item.type === '物理机').length)
|
||||
const vmCount = computed(() => filteredResources.value.filter((item) => item.type === '虚机').length)
|
||||
async function loadOverview() {
|
||||
overview.value = await machineApi.getOverview()
|
||||
}
|
||||
|
||||
async function loadResources() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await machineApi.listResources({
|
||||
page: page.value,
|
||||
size: size.value,
|
||||
hostname: filters.hostname.trim(),
|
||||
assetNumber: filters.assetNumber.trim(),
|
||||
type: filters.type,
|
||||
location: filters.location.trim(),
|
||||
ip: filters.ip.trim(),
|
||||
spec: filters.spec.trim(),
|
||||
businessLine: filters.businessLine.trim(),
|
||||
source: filters.source,
|
||||
status: filters.status,
|
||||
})
|
||||
resources.value = data.items
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadFirstPage() {
|
||||
page.value = 1
|
||||
await loadResources()
|
||||
}
|
||||
|
||||
async function handleSync() {
|
||||
syncing.value = true
|
||||
try {
|
||||
await machineApi.sync()
|
||||
await Promise.all([loadOverview(), loadResources()])
|
||||
ElMessage.success('机器数据同步完成')
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadOverview(), loadResources()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -145,4 +268,20 @@ const vmCount = computed(() => filteredResources.value.filter((item) => item.typ
|
||||
border-color: var(--tag-green-border);
|
||||
background: var(--tag-green-bg);
|
||||
}
|
||||
|
||||
.empty-cell {
|
||||
color: var(--text-tertiary);
|
||||
padding: 28px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.filter-row th {
|
||||
background: var(--bg-elevated);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.filter-row :deep(.el-input),
|
||||
.filter-row :deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>服务管理</h1>
|
||||
<p>同步全部机房 Consul 注册中心服务目录,统一查看服务名 / 服务 IP / 业务标签 / 实例数</p>
|
||||
<p>同步基础服务实例台账,统一查看服务名 / 服务 IP / 业务标签 / 实例数</p>
|
||||
</div>
|
||||
<el-button type="primary">同步</el-button>
|
||||
<el-button type="primary" :loading="serviceLoading" @click="loadServiceLedger">同步</el-button>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<div class="stat-card">
|
||||
<div class="label">接入 Consul Datacenter</div>
|
||||
<div class="value">7</div>
|
||||
<div class="delta">国内 3 · 海外 4</div>
|
||||
<div class="label">接入 Datacenter</div>
|
||||
<div class="value">{{ serviceDatacenters }}</div>
|
||||
<div class="delta">来自基础服务实例台账</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">服务总数</div>
|
||||
@@ -22,17 +22,17 @@
|
||||
<div class="stat-card">
|
||||
<div class="label">服务实例总数</div>
|
||||
<div class="value">{{ serviceInstances }}</div>
|
||||
<div class="delta">所有机房注册 IP 汇总</div>
|
||||
<div class="delta">当前业务线实例汇总</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">健康实例占比</div>
|
||||
<div class="value accent">98.6%</div>
|
||||
<div class="delta warn">12 个实例 critical</div>
|
||||
<div class="value accent">{{ serviceHealthPercent }}%</div>
|
||||
<div class="delta warn">{{ unhealthyInstances }} 个实例异常</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">最近同步</div>
|
||||
<div class="value accent-pill">● 正常</div>
|
||||
<div class="delta">07:41:05 · 间隔 30s</div>
|
||||
<div class="value accent-pill">● {{ serviceError ? '异常' : '正常' }}</div>
|
||||
<div class="delta">{{ serviceMetaText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>服务台账 · 按 Consul Datacenter 同步</h3>
|
||||
<span class="meta">数据源:Consul Catalog API</span>
|
||||
<span class="meta">{{ serviceLoading ? '同步中...' : serviceMetaText }}</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table>
|
||||
@@ -82,6 +82,9 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="!serviceLoading && filteredServices.length === 0">
|
||||
<td colspan="7" class="text-dim">当前业务线暂无基础服务实例</td>
|
||||
</tr>
|
||||
<tr v-for="service in filteredServices" :key="service.name" class="tr-hover">
|
||||
<td class="strong mono">{{ service.name }}</td>
|
||||
<td><span class="tag" :class="service.dcClass">{{ service.dc }}</span></td>
|
||||
@@ -154,28 +157,45 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { containerServiceApi, emptyContainerServiceSummary, type ContainerServiceSummary, type ContainerWorkload } from '@/api/containerService'
|
||||
import { deliveryApi, type MySQLServiceLedgerItem } from '@/api/delivery'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
import { useBusinessLineMockProfile } from '@/utils/businessLineMock'
|
||||
|
||||
const { currentName } = useBusinessLineMockProfile()
|
||||
const businessLineStore = useBusinessLineStore()
|
||||
|
||||
const services = ref([
|
||||
{ name: 'kodo-gateway-svc', dc: 'YZH', dcClass: 'zone-a', biz: 'kodo', instances: 12, healthy: '12 / 12', ip: '10.21.4.51', status: '健康', statusClass: 'ok' },
|
||||
{ name: 'kodo-upload-svc', dc: 'XS', dcClass: 'zone-b', biz: 'kodo', instances: 10, healthy: '10 / 10', ip: '10.34.37.66', status: '健康', statusClass: 'ok' },
|
||||
{ name: 'linxi-render-svc', dc: 'JF', dcClass: 'zone-c', biz: 'linxi', instances: 6, healthy: '6 / 6', ip: '10.45.2.30', status: '健康', statusClass: 'ok' },
|
||||
{ name: 'xinfra-portal-svc', dc: 'YZH', dcClass: 'zone-a', biz: 'xinfra', instances: 8, healthy: '8 / 8', ip: '10.21.4.88', status: '健康', statusClass: 'ok' },
|
||||
{ name: 'las-search-api', dc: 'XS', dcClass: 'zone-b', biz: 'las', instances: 18, healthy: '17 / 18', ip: '10.34.37.20', status: '部分异常', statusClass: 'warn' },
|
||||
{ name: 'las-order-svc', dc: '达拉斯 IDC', dcClass: '', biz: 'las', instances: 5, healthy: '5 / 5', ip: '10.66.2.20', status: '健康', statusClass: 'ok' },
|
||||
])
|
||||
interface ServiceLedgerRow {
|
||||
name: string
|
||||
dc: string
|
||||
dcClass: string
|
||||
biz: string
|
||||
instances: number
|
||||
healthy: string
|
||||
healthyCount: number
|
||||
ip: string
|
||||
status: string
|
||||
statusClass: string
|
||||
}
|
||||
|
||||
const services = ref<ServiceLedgerRow[]>([])
|
||||
const serviceLoading = ref(false)
|
||||
const serviceError = ref('')
|
||||
|
||||
const containerServices = ref<ContainerWorkload[]>([])
|
||||
const containerSummary = ref<ContainerServiceSummary>({ ...emptyContainerServiceSummary })
|
||||
const containerLoading = ref(false)
|
||||
const containerError = ref('')
|
||||
|
||||
const filteredServices = computed(() => services.value.filter((service) => service.biz === currentName.value))
|
||||
const filteredServices = computed(() => services.value)
|
||||
const serviceInstances = computed(() => filteredServices.value.reduce((sum, service) => sum + service.instances, 0))
|
||||
const healthyInstances = computed(() => filteredServices.value.reduce((sum, service) => sum + service.healthyCount, 0))
|
||||
const unhealthyInstances = computed(() => Math.max(serviceInstances.value - healthyInstances.value, 0))
|
||||
const serviceDatacenters = computed(() => new Set(filteredServices.value.map((service) => service.dc).filter(Boolean)).size)
|
||||
const serviceHealthPercent = computed(() => {
|
||||
if (serviceInstances.value === 0) return '0.0'
|
||||
return ((healthyInstances.value / serviceInstances.value) * 100).toFixed(1)
|
||||
})
|
||||
const serviceMetaText = computed(() => serviceError.value || '数据源:deployment_results')
|
||||
const containerMetaText = computed(() => {
|
||||
if (containerError.value) {
|
||||
return containerError.value
|
||||
@@ -184,6 +204,42 @@ const containerMetaText = computed(() => {
|
||||
return errors > 0 ? `数据源:Kubernetes Workload API · ${errors} 项同步失败` : '数据源:Kubernetes Workload API'
|
||||
})
|
||||
|
||||
const loadServiceLedger = async () => {
|
||||
const businessLineId = businessLineStore.current?.id
|
||||
if (!businessLineId) {
|
||||
services.value = []
|
||||
return
|
||||
}
|
||||
serviceLoading.value = true
|
||||
serviceError.value = ''
|
||||
try {
|
||||
const items = await deliveryApi.listMySQLServices(businessLineId)
|
||||
services.value = items.map(serviceLedgerRow)
|
||||
} catch (error) {
|
||||
services.value = []
|
||||
serviceError.value = error instanceof Error ? error.message : '基础服务实例同步失败'
|
||||
} finally {
|
||||
serviceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function serviceLedgerRow(item: MySQLServiceLedgerItem): ServiceLedgerRow {
|
||||
const instances = item.instances || 1
|
||||
const healthy = item.healthy || 0
|
||||
return {
|
||||
name: item.name,
|
||||
dc: item.datacenter || '-',
|
||||
dcClass: 'zone-a',
|
||||
biz: item.business_tag || currentName.value,
|
||||
instances,
|
||||
healthy: `${healthy} / ${instances}`,
|
||||
healthyCount: healthy,
|
||||
ip: item.address || '-',
|
||||
status: item.status || '未知',
|
||||
statusClass: item.status_class || 'warn',
|
||||
}
|
||||
}
|
||||
|
||||
const loadContainerServices = async () => {
|
||||
const businessLineId = businessLineStore.current?.id
|
||||
if (!businessLineId) {
|
||||
@@ -209,6 +265,7 @@ const loadContainerServices = async () => {
|
||||
watch(
|
||||
() => businessLineStore.current?.id,
|
||||
() => {
|
||||
loadServiceLedger()
|
||||
loadContainerServices()
|
||||
},
|
||||
{ immediate: true },
|
||||
|
||||
Reference in New Issue
Block a user