Files
front/src/views/ops/pages/dc/server/index.vue
2026-04-12 16:40:33 +08:00

582 lines
16 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 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>