feat(container): show business line workloads from Wayne

This commit is contained in:
mac
2026-07-22 14:57:11 +08:00
parent 89a23ef60d
commit 7f6f6fa41e
6 changed files with 824 additions and 2 deletions
+99
View File
@@ -0,0 +1,99 @@
import { getToken } from '@/utils/auth'
export interface ContainerServiceSummary {
businessLineId: number
businessLineName: string
namespaces: number
clusters: number
nodes: number
readyNodes: number
schedulableNodes: number
workloads: number
pods: number
readyPods: number
cpuUsed: number
cpuTotal: number
cpuPercent: number
memoryUsed: number
memoryTotal: number
memoryPercent: number
errors?: string[]
}
export interface ContainerWorkload {
name: string
cluster: string
namespace: string
workload: string
ready: string
readyPods: number
pods: number
image: string
biz: string
status: string
statusClass: string
}
export const emptyContainerServiceSummary: ContainerServiceSummary = {
businessLineId: 0,
businessLineName: '',
namespaces: 0,
clusters: 0,
nodes: 0,
readyNodes: 0,
schedulableNodes: 0,
workloads: 0,
pods: 0,
readyPods: 0,
cpuUsed: 0,
cpuTotal: 0,
cpuPercent: 0,
memoryUsed: 0,
memoryTotal: 0,
memoryPercent: 0,
}
export const containerServiceApi = {
async getSummary(businessLineId: number): Promise<ContainerServiceSummary> {
return authRequest(`/auth/api/v1/container-services/business-lines/${businessLineId}/summary`)
},
async listWorkloads(businessLineId: number): Promise<{ items: ContainerWorkload[]; summary: ContainerServiceSummary }> {
const data = await authRequest(`/auth/api/v1/container-services/business-lines/${businessLineId}/workloads`)
return {
items: Array.isArray(data.items) ? data.items : [],
summary: data.summary || emptyContainerServiceSummary,
}
},
}
async function authRequest(path: string, init: RequestInit = {}) {
const token = getToken()
const response = await fetch(path, {
...init,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...init.headers,
},
})
const text = await response.text()
const data = parseResponseBody(text)
if (!response.ok) {
const message = data?.error || data?.message || text || `HTTP ${response.status}`
throw new Error(message)
}
return data || {}
}
function parseResponseBody(text: string) {
if (!text.trim()) {
return {}
}
try {
return JSON.parse(text)
} catch {
return { error: text }
}
}
+115 -1
View File
@@ -36,6 +36,40 @@
</div>
</div>
<div class="panel">
<div class="panel-head">
<h3>容器化服务 · 资源概览</h3>
<span class="meta">{{ containerLoading ? '同步中...' : containerMetaText }}</span>
</div>
<div class="container-overview">
<div class="container-metric">
<span>命名空间</span>
<strong>{{ containerSummary.namespaces }}</strong>
<small>{{ currentName }} 业务线绑定</small>
</div>
<div class="container-metric">
<span>工作负载</span>
<strong>{{ containerSummary.workloads }}</strong>
<small>Deployment</small>
</div>
<div class="container-metric">
<span>Pod 实例</span>
<strong>{{ containerSummary.pods }}</strong>
<small>{{ containerSummary.readyPods }} ready</small>
</div>
<div class="container-metric">
<span>CPU 已用</span>
<strong>{{ formatMetric(containerSummary.cpuUsed) }}C</strong>
<small>{{ containerSummary.cpuPercent }}% of allocatable</small>
</div>
<div class="container-metric">
<span>Memory 已用</span>
<strong>{{ formatMetric(containerSummary.memoryUsed) }}G</strong>
<small>{{ containerSummary.memoryPercent }}% of allocatable</small>
</div>
</div>
</div>
<div class="cols-2">
<div class="panel">
<div class="panel-head">
@@ -121,13 +155,57 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { containerServiceApi, emptyContainerServiceSummary, type ContainerServiceSummary } from '@/api/containerService'
import { useBusinessLineStore } from '@/stores/businessLine'
import { useBusinessLineMockProfile } from '@/utils/businessLineMock'
const { currentName, profile } = useBusinessLineMockProfile()
const businessLineStore = useBusinessLineStore()
const containerSummary = ref<ContainerServiceSummary>({ ...emptyContainerServiceSummary })
const containerLoading = ref(false)
const containerError = ref('')
const containerMetaText = computed(() => {
if (containerError.value) {
return containerError.value
}
const errors = containerSummary.value.errors?.length || 0
return errors > 0 ? `数据源:Kubernetes 集群运行态 · ${errors} 项同步失败` : '数据源:Kubernetes 集群运行态'
})
const loadContainerSummary = async () => {
const businessLineId = businessLineStore.current?.id
if (!businessLineId) {
containerSummary.value = { ...emptyContainerServiceSummary }
return
}
containerLoading.value = true
containerError.value = ''
try {
containerSummary.value = await containerServiceApi.getSummary(businessLineId)
} catch (error) {
containerSummary.value = { ...emptyContainerServiceSummary }
containerError.value = error instanceof Error ? error.message : '容器化服务数据同步失败'
} finally {
containerLoading.value = false
}
}
const refresh = () => {
// 刷新数据
loadContainerSummary()
}
const formatMetric = (value: number) => Number(value || 0).toFixed(1).replace(/\.0$/, '')
watch(
() => businessLineStore.current?.id,
() => {
loadContainerSummary()
},
{ immediate: true },
)
</script>
<style scoped>
@@ -202,4 +280,40 @@ const refresh = () => {
font-size: 11px;
color: var(--text-dim);
}
.container-overview {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
padding: 16px;
}
.container-metric {
min-width: 0;
padding: 13px 14px;
border: 1px solid var(--line);
border-radius: var(--radius-lg);
background: var(--bg-panel-2);
}
.container-metric span,
.container-metric small {
display: block;
color: var(--text-dim);
font-size: 11px;
}
.container-metric strong {
display: block;
margin: 6px 0 4px;
color: var(--text-hi);
font-family: var(--mono);
font-size: 20px;
}
@media (max-width: 960px) {
.container-overview {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
+87 -1
View File
@@ -107,14 +107,58 @@
</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 } from 'vue'
import { computed, ref, watch } from 'vue'
import { containerServiceApi, emptyContainerServiceSummary, type ContainerServiceSummary, type ContainerWorkload } from '@/api/containerService'
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' },
@@ -125,8 +169,50 @@ const services = ref([
{ name: 'las-order-svc', dc: '达拉斯 IDC', dcClass: '', biz: 'las', instances: 5, healthy: '5 / 5', ip: '10.66.2.20', status: '健康', statusClass: 'ok' },
])
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 serviceInstances = computed(() => filteredServices.value.reduce((sum, service) => sum + service.instances, 0))
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 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,
() => {
loadContainerServices()
},
{ immediate: true },
)
</script>
<style scoped>