582 lines
16 KiB
Vue
582 lines
16 KiB
Vue
<template>
|
||
<div class="container">
|
||
<search-table
|
||
:form-model="formModel"
|
||
:form-items="formItems"
|
||
:data="tableData"
|
||
:columns="columns"
|
||
:loading="loading"
|
||
:pagination="pagination"
|
||
title="服务器管理"
|
||
search-button-text="查询"
|
||
reset-button-text="重置"
|
||
@update:form-model="handleFormModelUpdate"
|
||
@search="handleSearch"
|
||
@reset="handleReset"
|
||
@page-change="handlePageChange"
|
||
@refresh="handleRefresh"
|
||
>
|
||
<template #toolbar-left>
|
||
<a-button type="primary" @click="handleAdd">
|
||
<template #icon>
|
||
<icon-plus />
|
||
</template>
|
||
新增服务器
|
||
</a-button>
|
||
</template>
|
||
|
||
<!-- ID -->
|
||
<template #id="{ record }">
|
||
{{ record.id }}
|
||
</template>
|
||
|
||
<!-- 远程访问 -->
|
||
<template #remote_access="{ record }">
|
||
<a-tag :color="record.remote_access ? 'green' : 'gray'">
|
||
{{ record.remote_access ? '已开启' : '未开启' }}
|
||
</a-tag>
|
||
</template>
|
||
|
||
<!-- Agent 配置 -->
|
||
<template #agent_config="{ record }">
|
||
<a-tag :color="record.agent_config ? 'green' : 'gray'">
|
||
{{ record.agent_config ? '已配置' : '未配置' }}
|
||
</a-tag>
|
||
</template>
|
||
|
||
<!-- CPU -->
|
||
<template #cpu="{ record }">
|
||
<div class="resource-display">
|
||
<div class="resource-info">
|
||
<span class="resource-label">CPU</span>
|
||
<span class="resource-value">{{ record.cpu_info?.value || 0 }}%</span>
|
||
</div>
|
||
<a-progress
|
||
:percent="(record.cpu_info?.value || 0) / 100"
|
||
:color="getProgressColor(record.cpu_info?.value || 0)"
|
||
size="small"
|
||
:show-text="false"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 内存 -->
|
||
<template #memory="{ record }">
|
||
<div class="resource-display">
|
||
<div class="resource-info">
|
||
<span class="resource-label">内存</span>
|
||
<span class="resource-value">{{ record.memory_info?.value || 0 }}%</span>
|
||
</div>
|
||
<a-progress
|
||
:percent="(record.memory_info?.value || 0) / 100"
|
||
:color="getProgressColor(record.memory_info?.value || 0)"
|
||
size="small"
|
||
:show-text="false"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 硬盘 -->
|
||
<template #disk="{ record }">
|
||
<div class="resource-display">
|
||
<div class="resource-info">
|
||
<span class="resource-label">硬盘</span>
|
||
<span class="resource-value">{{ record.disk_info?.value || 0 }}%</span>
|
||
</div>
|
||
<a-progress
|
||
:percent="(record.disk_info?.value || 0) / 100"
|
||
:color="getProgressColor(record.disk_info?.value || 0)"
|
||
size="small"
|
||
:show-text="false"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 数据采集 -->
|
||
<template #data_collection="{ record }">
|
||
<a-tag :color="record.collect_on ? 'green' : 'gray'">
|
||
{{ record.collect_on ? '已启用' : '未启用' }}
|
||
</a-tag>
|
||
</template>
|
||
|
||
<!-- 状态 -->
|
||
<template #status="{ record }">
|
||
<a-tag :color="getStatusColor(record.status)">
|
||
{{ getStatusText(record.status) }}
|
||
</a-tag>
|
||
</template>
|
||
|
||
<!-- 最后检查时间 -->
|
||
<template #last_check_time="{ record }">
|
||
{{ formatDateTime(record.last_check_time) }}
|
||
</template>
|
||
|
||
<!-- 操作栏 - 下拉菜单 -->
|
||
<template #actions="{ record }">
|
||
<a-space>
|
||
<a-button
|
||
v-if="!record.agent_config"
|
||
type="outline"
|
||
size="small"
|
||
@click="handleQuickConfig(record)"
|
||
>
|
||
<template #icon>
|
||
<icon-settings />
|
||
</template>
|
||
快捷配置
|
||
</a-button>
|
||
<a-dropdown trigger="hover">
|
||
<a-button type="primary" size="small">
|
||
管理
|
||
<icon-down />
|
||
</a-button>
|
||
<template #content>
|
||
<!-- <a-doption @click="handleRestart(record)">
|
||
<template #icon>
|
||
<icon-refresh />
|
||
</template>
|
||
重启
|
||
</a-doption>
|
||
<a-doption @click="handleDetail(record)">
|
||
<template #icon>
|
||
<icon-eye />
|
||
</template>
|
||
详情
|
||
</a-doption> -->
|
||
<a-doption @click="handleEdit(record)">
|
||
<template #icon>
|
||
<icon-edit />
|
||
</template>
|
||
编辑
|
||
</a-doption>
|
||
<!-- <a-doption @click="handleRemoteControl(record)">
|
||
<template #icon>
|
||
<icon-desktop />
|
||
</template>
|
||
远程控制
|
||
</a-doption> -->
|
||
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
|
||
<template #icon>
|
||
<icon-delete />
|
||
</template>
|
||
删除
|
||
</a-doption>
|
||
</template>
|
||
</a-dropdown>
|
||
</a-space>
|
||
</template>
|
||
</search-table>
|
||
|
||
<!-- 新增/编辑对话框 -->
|
||
<ServerFormDialog
|
||
v-model:visible="formDialogVisible"
|
||
:record="currentRecord"
|
||
@success="handleFormSuccess"
|
||
/>
|
||
|
||
<!-- 快捷配置对话框 -->
|
||
<QuickConfigDialog
|
||
v-model:visible="quickConfigVisible"
|
||
:record="currentRecord"
|
||
@success="handleFormSuccess"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { Message, Modal } from '@arco-design/web-vue'
|
||
import {
|
||
IconPlus,
|
||
IconDown,
|
||
IconEdit,
|
||
IconDesktop,
|
||
IconDelete,
|
||
IconRefresh,
|
||
IconEye,
|
||
IconSettings
|
||
} from '@arco-design/web-vue/es/icon'
|
||
import type { FormItem } from '@/components/search-form/types'
|
||
import SearchTable from '@/components/search-table/index.vue'
|
||
import { searchFormConfig } from './config/search-form'
|
||
import ServerFormDialog from './components/ServerFormDialog.vue'
|
||
import QuickConfigDialog from './components/QuickConfigDialog.vue'
|
||
import { columns as columnsConfig } from './config/columns'
|
||
import {
|
||
fetchServerList,
|
||
deleteServer,
|
||
} from '@/api/ops/server'
|
||
import axios from 'axios'
|
||
|
||
// 创建独立的 axios 实例用于请求外部 agent,绕过全局拦截器
|
||
const agentAxios = axios.create({
|
||
timeout: 5000,
|
||
})
|
||
|
||
const router = useRouter()
|
||
|
||
// 状态管理
|
||
const loading = ref(false)
|
||
const tableData = ref<any[]>([])
|
||
const formDialogVisible = ref(false)
|
||
const quickConfigVisible = ref(false)
|
||
const currentRecord = ref<any>(null)
|
||
const formModel = ref({
|
||
keyword: '',
|
||
collect_on: undefined as boolean | undefined,
|
||
})
|
||
|
||
const pagination = reactive({
|
||
current: 1,
|
||
pageSize: 20,
|
||
total: 0,
|
||
})
|
||
|
||
// 表单项配置
|
||
const formItems = computed<FormItem[]>(() =>
|
||
searchFormConfig.map((item) => {
|
||
if (item.field === 'collect_on') {
|
||
return {
|
||
...item,
|
||
options: [
|
||
{ label: '是', value: true },
|
||
{ label: '否', value: false },
|
||
],
|
||
}
|
||
}
|
||
return item
|
||
})
|
||
)
|
||
|
||
// 表格列配置
|
||
const columns = computed(() => columnsConfig)
|
||
|
||
// 获取状态颜色
|
||
const getStatusColor = (status?: string) => {
|
||
const colorMap: Record<string, string> = {
|
||
online: 'green',
|
||
offline: 'red',
|
||
unknown: 'gray',
|
||
}
|
||
return colorMap[status || ''] || 'gray'
|
||
}
|
||
|
||
// 获取状态文本
|
||
const getStatusText = (status?: string) => {
|
||
const textMap: Record<string, string> = {
|
||
online: '在线',
|
||
offline: '离线',
|
||
unknown: '未知',
|
||
}
|
||
return textMap[status || ''] || '-'
|
||
}
|
||
|
||
// 获取进度条颜色
|
||
const getProgressColor = (value: number) => {
|
||
if (value >= 90) return '#F53F3F' // 红色
|
||
if (value >= 70) return '#FF7D00' // 橙色
|
||
if (value >= 50) return '#FFD00B' // 黄色
|
||
return '#00B42A' // 绿色
|
||
}
|
||
|
||
// 格式化日期时间
|
||
const formatDateTime = (dateTime: string | null | undefined) => {
|
||
if (!dateTime || dateTime === '0001-01-01T00:00:00Z') {
|
||
return '-'
|
||
}
|
||
try {
|
||
const date = new Date(dateTime)
|
||
if (isNaN(date.getTime())) {
|
||
return '-'
|
||
}
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
const hours = String(date.getHours()).padStart(2, '0')
|
||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||
} catch {
|
||
return '-'
|
||
}
|
||
}
|
||
|
||
// 获取服务器列表
|
||
const fetchServers = async () => {
|
||
loading.value = true
|
||
|
||
try {
|
||
const params: any = {
|
||
page: pagination.current,
|
||
size: pagination.pageSize,
|
||
}
|
||
|
||
if (formModel.value.keyword) {
|
||
params.keyword = formModel.value.keyword
|
||
}
|
||
|
||
if (formModel.value.collect_on !== undefined) {
|
||
params.collect_on = formModel.value.collect_on
|
||
}
|
||
|
||
const res: any = await fetchServerList(params)
|
||
|
||
if (res.code === 0) {
|
||
const responseData = res.details || {}
|
||
tableData.value = responseData.data || []
|
||
pagination.total = responseData.total || 0
|
||
|
||
// 列表加载成功后,获取监控指标
|
||
await getAllMetrics()
|
||
} else {
|
||
Message.error(res.message || '获取服务器列表失败')
|
||
tableData.value = []
|
||
pagination.total = 0
|
||
}
|
||
} catch (error) {
|
||
console.error('获取服务器列表失败:', error)
|
||
Message.error('获取服务器列表失败')
|
||
tableData.value = []
|
||
pagination.total = 0
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 搜索
|
||
const handleSearch = () => {
|
||
pagination.current = 1
|
||
fetchServers()
|
||
}
|
||
|
||
// 处理表单模型更新
|
||
const handleFormModelUpdate = (value: any) => {
|
||
formModel.value = value
|
||
}
|
||
|
||
// 重置
|
||
const handleReset = () => {
|
||
formModel.value = {
|
||
keyword: '',
|
||
collect_on: undefined,
|
||
}
|
||
pagination.current = 1
|
||
fetchServers()
|
||
}
|
||
|
||
// 分页变化
|
||
const handlePageChange = (current: number) => {
|
||
pagination.current = current
|
||
fetchServers()
|
||
}
|
||
|
||
// 刷新
|
||
const handleRefresh = () => {
|
||
fetchServers()
|
||
Message.success('数据已刷新')
|
||
}
|
||
|
||
// 新增服务器
|
||
const handleAdd = () => {
|
||
currentRecord.value = null
|
||
formDialogVisible.value = true
|
||
}
|
||
|
||
// 快捷配置
|
||
const handleQuickConfig = (record: any) => {
|
||
currentRecord.value = record
|
||
quickConfigVisible.value = true
|
||
}
|
||
|
||
// 编辑服务器
|
||
const handleEdit = (record: any) => {
|
||
currentRecord.value = record
|
||
formDialogVisible.value = true
|
||
}
|
||
|
||
// 表单提交成功
|
||
const handleFormSuccess = () => {
|
||
fetchServers()
|
||
}
|
||
|
||
// 重启服务器
|
||
const handleRestart = (record: any) => {
|
||
Modal.confirm({
|
||
title: '确认重启',
|
||
content: `确认重启服务器 ${record.name} 吗?`,
|
||
onOk: () => {
|
||
Message.info('正在发送重启指令...')
|
||
},
|
||
})
|
||
}
|
||
|
||
// 查看详情 - 在当前窗口打开
|
||
const handleDetail = (record: any) => {
|
||
router.push({
|
||
path: '/dc/detail',
|
||
query: {
|
||
id: record.id,
|
||
name: record.name,
|
||
ip: record.host || record.ip_address,
|
||
status: record.status,
|
||
},
|
||
})
|
||
}
|
||
|
||
// 远程控制 - 在新窗口打开
|
||
const handleRemoteControl = (record: any) => {
|
||
const url = router.resolve({
|
||
path: '/dc/remote',
|
||
query: {
|
||
id: record.id,
|
||
name: record.name,
|
||
ip: record.host || record.ip_address,
|
||
status: record.status,
|
||
},
|
||
}).href
|
||
window.open(url, '_blank')
|
||
}
|
||
|
||
// 删除服务器
|
||
const handleDelete = async (record: any) => {
|
||
Modal.confirm({
|
||
title: '确认删除',
|
||
content: `确认删除服务器 ${record.name} 吗?`,
|
||
onOk: async () => {
|
||
try {
|
||
const res: any = await deleteServer(record.id)
|
||
if (res.code === 0) {
|
||
Message.success('删除成功')
|
||
fetchServers()
|
||
} else {
|
||
Message.error(res.message || '删除失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('删除服务器失败:', error)
|
||
Message.error('删除失败')
|
||
}
|
||
},
|
||
})
|
||
}
|
||
|
||
/** dc-host `GET agent_config`(一般为 `/dc-host/stats`)返回裸 Metrics JSON */
|
||
// 获取所有服务器的监控指标
|
||
const getAllMetrics = async () => {
|
||
try {
|
||
// 遍历每个服务器记录
|
||
const metricsPromises = tableData.value.map(async (record) => {
|
||
// 检查是否有 agent_config 配置
|
||
if (record.agent_config) {
|
||
try {
|
||
// 从 agent_config 中解析 URL
|
||
let metricsUrl = record.agent_config
|
||
|
||
// 验证 URL 是否合法
|
||
try {
|
||
new URL(metricsUrl)
|
||
} catch (urlError) {
|
||
console.warn(`服务器 ${record.name} 的 agent_config 不是合法的 URL:`, metricsUrl)
|
||
// 设置默认值 0
|
||
record.cpu_info = { value: 0, total: '', used: '' }
|
||
record.memory_info = { value: 0, total: '', used: '' }
|
||
record.disk_info = { value: 0, total: '', used: '' }
|
||
return
|
||
}
|
||
// 使用独立的 axios 实例请求外部 agent,绕过全局拦截器
|
||
const response = await agentAxios.get(metricsUrl)
|
||
console.log('获取指标数据:', response.data)
|
||
if (response.data) {
|
||
// 更新记录的监控数据
|
||
record.cpu_info = {
|
||
value: Number((response.data.cpu_usage || 0).toFixed(2)),
|
||
total: response.data.cpu?.length ? `${response.data.cpu.length}核` : '',
|
||
used: '',
|
||
}
|
||
|
||
record.memory_info = {
|
||
value: Number((response.data.mem_usage?.used_percent || 0).toFixed(2)),
|
||
total: response.data.mem_usage?.total ? `${(response.data.mem_usage.total / 1024 / 1024 / 1024).toFixed(1)}GB` : '',
|
||
used: response.data.mem_usage?.used ? `${(response.data.mem_usage.used / 1024 / 1024 / 1024).toFixed(1)}GB` : '',
|
||
}
|
||
|
||
record.disk_info = {
|
||
value: Number((response.data.disk_usage?.used_percent || 0).toFixed(2)),
|
||
total: response.data.disk_usage?.total ? `${(response.data.disk_usage.total / 1024 / 1024 / 1024).toFixed(0)}GB` : '',
|
||
used: response.data.disk_usage?.used ? `${(response.data.disk_usage.used / 1024 / 1024 / 1024).toFixed(0)}GB` : '',
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn(`获取服务器 ${record.name} 的监控指标失败:`, error)
|
||
// 初始化默认值
|
||
record.cpu_info = { value: 0, total: '', used: '' }
|
||
record.memory_info = { value: 0, total: '', used: '' }
|
||
record.disk_info = { value: 0, total: '', used: '' }
|
||
}
|
||
} else {
|
||
// 没有配置 agent,设置默认值
|
||
record.cpu_info = { value: 0, total: '', used: '' }
|
||
record.memory_info = { value: 0, total: '', used: '' }
|
||
record.disk_info = { value: 0, total: '', used: '' }
|
||
}
|
||
})
|
||
|
||
// 等待所有请求完成
|
||
await Promise.all(metricsPromises)
|
||
} catch (error) {
|
||
console.error('获取所有服务器监控指标失败:', error)
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchServers()
|
||
})
|
||
|
||
</script>
|
||
|
||
<script lang="ts">
|
||
export default {
|
||
name: 'DataCenterServer',
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.container {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.resource-display {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
padding: 4px 0;
|
||
|
||
.resource-info {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
> div {
|
||
display: inline-block;
|
||
}
|
||
.resource-value {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: rgb(var(--text-1));
|
||
}
|
||
}
|
||
|
||
:deep(.arco-progress) {
|
||
margin: 0;
|
||
|
||
.arco-progress-bar-bg {
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.arco-progress-bar {
|
||
border-radius: 2px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
}
|
||
}
|
||
</style>
|