288 lines
9.1 KiB
Vue
288 lines
9.1 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>机器管理</h1>
|
|
<p>统一纳管物理机与虚机资源 · 基础数据来自 SINA CMDB</p>
|
|
</div>
|
|
<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">{{ 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">{{ 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">{{ overview.aliyun.total }}</div>
|
|
<div class="delta">{{ overview.aliyun.label }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">AWS / 七牛同步</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">● {{ syncStatusText }}</div>
|
|
<div class="delta">{{ overview.lastSync.time }} · 新增 {{ overview.lastSync.created }} 条</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-head">
|
|
<h3>资源台账 · 物理机 / 虚机</h3>
|
|
<span class="meta">SINA CMDB asset items + 云厂商同步增量</span>
|
|
</div>
|
|
<div class="panel-body">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>主机名</th>
|
|
<th>资产编号</th>
|
|
<th>类型</th>
|
|
<th>机房 / 区域</th>
|
|
<th>内网 IP</th>
|
|
<th>规格</th>
|
|
<th>业务线</th>
|
|
<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 resources" :key="`${item.hostname}-${item.ip}`" class="tr-hover">
|
|
<td class="strong mono">{{ item.hostname }}</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.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>共 {{ total }} 条</span>
|
|
<div class="pg-btns">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { emptyMachineOverview, machineApi, type MachineResource } from '@/api/machine'
|
|
|
|
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 syncStatusText = computed(() => {
|
|
if (overview.value.lastSync.status === 'normal') return '正常'
|
|
if (overview.value.lastSync.status === 'failed') return '失败'
|
|
return '未知'
|
|
})
|
|
|
|
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>
|
|
/* 公共样式已在 global.css 中定义 */
|
|
|
|
.tag.vm {
|
|
color: var(--tag-blue-text);
|
|
border-color: var(--tag-blue-border);
|
|
background: var(--tag-blue-bg);
|
|
}
|
|
|
|
.tag.src-cmdb {
|
|
color: var(--tag-green-text);
|
|
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>
|