278 lines
10 KiB
Vue
278 lines
10 KiB
Vue
<template>
|
||
<div>
|
||
<div class="page-head">
|
||
<div>
|
||
<h1>服务管理</h1>
|
||
<p>同步基础服务实例台账,统一查看服务名 / 服务 IP / 业务标签 / 实例数</p>
|
||
</div>
|
||
<el-button type="primary" :loading="serviceLoading" @click="loadServiceLedger">同步</el-button>
|
||
</div>
|
||
|
||
<div class="stat-row">
|
||
<div class="stat-card">
|
||
<div class="label">接入 Datacenter</div>
|
||
<div class="value">{{ serviceDatacenters }}</div>
|
||
<div class="delta">来自基础服务实例台账</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">服务总数</div>
|
||
<div class="value">{{ filteredServices.length }}</div>
|
||
<div class="delta">去重后唯一服务名</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">服务实例总数</div>
|
||
<div class="value">{{ serviceInstances }}</div>
|
||
<div class="delta">当前业务线实例汇总</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">健康实例占比</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">● {{ serviceError ? '异常' : '正常' }}</div>
|
||
<div class="delta">{{ serviceMetaText }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="las-toolbar">
|
||
<el-select placeholder="全部机房">
|
||
<el-option label="全部机房" value="" />
|
||
<el-option label="YZH" value="yzh" />
|
||
<el-option label="XS" value="xs" />
|
||
<el-option label="JF" value="jf" />
|
||
<el-option label="达拉斯" value="dallas" />
|
||
<el-option label="新加坡" value="singapore" />
|
||
<el-option label="香港" value="hk" />
|
||
<el-option label="东南亚" value="sea" />
|
||
</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-select placeholder="全部状态">
|
||
<el-option label="全部状态" value="" />
|
||
<el-option label="健康" value="healthy" />
|
||
<el-option label="部分异常" value="partial" />
|
||
<el-option label="全部下线" value="offline" />
|
||
</el-select>
|
||
<el-input placeholder="搜索服务名 / IP" class="search-input" />
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-head">
|
||
<h3>服务台账 · 按 Consul Datacenter 同步</h3>
|
||
<span class="meta">{{ serviceLoading ? '同步中...' : serviceMetaText }}</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>服务名</th>
|
||
<th>机房 / Datacenter</th>
|
||
<th>业务标签</th>
|
||
<th>实例数</th>
|
||
<th>健康实例</th>
|
||
<th>示例 IP</th>
|
||
<th>状态</th>
|
||
</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>
|
||
<td class="mono">{{ service.biz }}</td>
|
||
<td class="mono">{{ service.instances }}</td>
|
||
<td class="mono">{{ service.healthy }}</td>
|
||
<td class="mono">{{ service.ip }}</td>
|
||
<td :class="['status-text', service.statusClass]">● {{ service.status }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="pagination">
|
||
<span>共 {{ filteredServices.length }} 条 · 当前业务线:{{ currentName }}</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">27</span>
|
||
<span class="pg-btn">›</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<div class="panel-head">
|
||
<h3>容器化服务台账 · 按集群 / Namespace 同步</h3>
|
||
<span class="meta">{{ containerLoading ? '同步中...' : containerMetaText }}</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>服务名</th>
|
||
<th>集群</th>
|
||
<th>Namespace</th>
|
||
<th>工作负载</th>
|
||
<th>Pod Ready</th>
|
||
<th>镜像</th>
|
||
<th>业务标签</th>
|
||
<th>状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-if="!containerLoading && containerServices.length === 0">
|
||
<td colspan="8" class="text-dim">当前业务线暂无容器化服务</td>
|
||
</tr>
|
||
<tr v-for="service in containerServices" :key="`${service.cluster}-${service.namespace}-${service.name}`" class="tr-hover">
|
||
<td class="strong mono">{{ service.name }}</td>
|
||
<td><span class="tag">{{ service.cluster }}</span></td>
|
||
<td class="mono">{{ service.namespace }}</td>
|
||
<td>{{ service.workload }}</td>
|
||
<td class="mono">{{ service.ready }}</td>
|
||
<td class="mono text-xs">{{ service.image || '-' }}</td>
|
||
<td class="mono">{{ service.biz }}</td>
|
||
<td :class="['status-text', service.statusClass]">● {{ service.status }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="pagination">
|
||
<span>共 {{ containerServices.length }} 条容器化服务 · 当前业务线:{{ currentName }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<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()
|
||
|
||
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)
|
||
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
|
||
}
|
||
const errors = containerSummary.value.errors?.length || 0
|
||
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) {
|
||
containerServices.value = []
|
||
containerSummary.value = { ...emptyContainerServiceSummary }
|
||
return
|
||
}
|
||
containerLoading.value = true
|
||
containerError.value = ''
|
||
try {
|
||
const data = await containerServiceApi.listWorkloads(businessLineId)
|
||
containerServices.value = data.items
|
||
containerSummary.value = data.summary
|
||
} catch (error) {
|
||
containerServices.value = []
|
||
containerSummary.value = { ...emptyContainerServiceSummary }
|
||
containerError.value = error instanceof Error ? error.message : '容器化服务数据同步失败'
|
||
} finally {
|
||
containerLoading.value = false
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => businessLineStore.current?.id,
|
||
() => {
|
||
loadServiceLedger()
|
||
loadContainerServices()
|
||
},
|
||
{ immediate: true },
|
||
)
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 所有样式已在 global.css 中定义 */
|
||
</style>
|