Files
xinfra/frontend/src/views/service/Management.vue
T

278 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>