This commit is contained in:
ygx
2026-03-29 20:44:50 +08:00
parent 564eb09b40
commit 9a6f224b9b
31 changed files with 4299 additions and 2764 deletions

199
src/api/ops/database.ts Normal file
View File

@@ -0,0 +1,199 @@
import { request } from '@/api/request'
/** 数据库类型枚举 */
export type DatabaseType = 'MySQL' | 'PostgreSQL' | 'Redis' | 'MongoDB' | 'DM' | 'KingBase'
/** 数据库服务状态 */
export type DatabaseStatus = 'online' | 'offline' | 'error'
/** 数据库服务接口返回数据结构 */
export interface DatabaseService {
id: number
created_at: string
updated_at: string
deleted_at: string | null
service_identity: string
server_identity: string
name: string
category: string
type: DatabaseType
host: string
port: number
username: string
password: string // 列表接口恒为空字符串,详情接口不返回
database: string
description: string
enabled: boolean
interval: number
extra: string
tags: string
status: DatabaseStatus
status_code: number
status_message: string
response_time: number
last_check_time: string
last_online_time: string | null
last_offline_time: string | null
continuous_errors: number
uptime: number
collect_on: boolean
collect_interval: number
collect_args: string
collect_last_result: string
}
/** 列表接口返回的分页数据 */
export interface DatabaseListResponse {
total: number
page: number
page_size: number
data: DatabaseService[]
}
/** 创建数据库服务请求参数 */
export interface CreateDatabaseParams {
service_identity?: string
server_identity?: string
name: string
category: string
type: DatabaseType
host: string
port: number
username?: string
password?: string
database?: string
description?: string
enabled?: boolean
interval?: number
extra?: string
tags?: string
collect_on?: boolean
collect_args?: string
collect_interval?: number
collect_last_result?: string
}
/** 更新数据库服务请求参数 */
export interface UpdateDatabaseParams {
service_identity?: string
server_identity?: string
name?: string
category?: string
type?: DatabaseType
host?: string
port?: number
username?: string
password?: string
database?: string
description?: string
enabled?: boolean
interval?: number
extra?: string
tags?: string
collect_on?: boolean
collect_args?: string
collect_interval?: number
collect_last_result?: string
}
/** 列表查询参数 */
export interface DatabaseQueryParams {
page?: number
size?: number
keyword?: string
enabled?: boolean
}
/** 指标采集请求参数 */
export interface CollectMetricsParams {
ids: number[]
persist?: boolean
}
/** 指标采集结果项 */
export interface CollectMetricsResultItem {
id: number
service_identity: string
metrics?: Record<string, any>
error?: string
}
/** 指标采集响应 */
export interface CollectMetricsResponse {
results: CollectMetricsResultItem[]
}
/** 指标元数据项 */
export interface MetricMetaItem {
metric_name: string
metric_unit: string
type: string
last_timestamp: string
}
/** 指标元数据响应 */
export interface MetricMetaResponse {
data_source: string
collector_identity: string
server_identity: string
count: number
metrics: MetricMetaItem[]
}
/** 指标元数据查询参数 */
export interface MetricMetaQueryParams {
data_source: string
server_identity?: string
keyword?: string
limit?: number
}
/**
* 获取数据库服务列表
* @param params 查询参数
*/
export const fetchDatabaseList = (params: DatabaseQueryParams) =>
request.get<DatabaseListResponse>('/DC-Control/v1/database', { params })
/**
* 获取数据库服务详情
* @param id 服务ID
*/
export const fetchDatabaseDetail = (id: number) =>
request.get<DatabaseService>(`/DC-Control/v1/database/${id}`)
/**
* 创建数据库服务
* @param data 创建参数
*/
export const createDatabase = (data: CreateDatabaseParams) =>
request.post<{ message: string; id: number }>('/DC-Control/v1/database', data)
/**
* 更新数据库服务
* @param id 服务ID
* @param data 更新参数
*/
export const updateDatabase = (id: number, data: UpdateDatabaseParams) =>
request.put<{ message: string }>(`/DC-Control/v1/database/${id}`, data)
/**
* 删除数据库服务(软删除)
* @param id 服务ID
*/
export const deleteDatabase = (id: number) =>
request.delete<{ message: string }>(`/DC-Control/v1/database/${id}`)
/**
* 按服务主键采集指标
* @param data 采集参数
*/
export const collectDatabaseMetrics = (data: CollectMetricsParams) =>
request.post<CollectMetricsResponse>('/DC-Control/v1/database/metrics/collect', data)
/**
* 查询指标元数据
* @param params 查询参数
*/
export const fetchMetricMeta = (params: MetricMetaQueryParams) =>
request.get<MetricMetaResponse>('/DC-Control/v1/services/metrics/meta', { params })

191
src/api/ops/middleware.ts Normal file
View File

@@ -0,0 +1,191 @@
import { request } from '@/api/request'
/** 中间件服务项 */
export interface MiddlewareItem {
id: number
created_at: string
updated_at: string
deleted_at: string | null
service_identity: string
server_identity: string
name: string
category: string
type: string
description: string
enabled: boolean
interval: number
extra: string
tags: string
status_url: string
status: string
status_code: number
status_message: string
response_time: number
last_check_time: string
last_online_time: string | null
last_offline_time: string | null
continuous_errors: number
uptime: number
agent_config: string
collect_on: boolean
collect_args: string
collect_interval: number
collect_last_result: string
}
/** 中间件列表响应 */
export interface MiddlewareListResponse {
total: number
page: number
page_size: number
data: MiddlewareItem[]
}
/** 中间件列表请求参数 */
export interface MiddlewareListParams {
page?: number
size?: number
keyword?: string
enabled?: boolean
}
/** 创建中间件请求参数 */
export interface MiddlewareCreateData {
service_identity?: string
server_identity?: string
name: string
category: string
type: string
description?: string
enabled?: boolean
interval?: number
extra?: string
tags?: string
status_url?: string
agent_config?: string
collect_on?: boolean
collect_args?: string
collect_interval?: number
policy_ids?: number[]
}
/** 更新中间件请求参数 */
export interface MiddlewareUpdateData {
service_identity?: string
server_identity?: string
name?: string
category?: string
type?: string
description?: string
enabled?: boolean
interval?: number
extra?: string
tags?: string
status_url?: string
agent_config?: string
collect_on?: boolean
collect_args?: string
collect_interval?: number
collect_last_result?: string
status?: string
status_code?: number
status_message?: string
response_time?: number
last_check_time?: string
last_online_time?: string | null
last_offline_time?: string | null
continuous_errors?: number
uptime?: number
policy_ids?: number[]
}
/** 采集配置更新参数 */
export interface MiddlewareCollectData {
collect_on?: boolean
collect_args?: string
collect_interval?: number
collect_last_result?: string
}
/** 指标上报项 */
export interface MetricItem {
service_identity: string
timestamp: string
[key: string]: any
}
/** 指标上报请求参数 */
export interface MetricsUploadData {
metrics: MetricItem[]
}
/** 指标上报响应 */
export interface MetricsUploadResponse {
message: string
count: number
}
/** 指标元数据项 */
export interface MetricMetaItem {
metric_name: string
metric_unit: string
type: string
last_timestamp: string
}
/** 指标元数据响应 */
export interface MetricsMetaResponse {
data_source: string
collector_identity: string
server_identity: string
count: number
metrics: MetricMetaItem[]
}
/** 指标元数据请求参数 */
export interface MetricsMetaParams {
data_source: string
server_identity?: string
keyword?: string
limit?: number
}
/** 获取中间件服务列表(匿名接口) */
export const fetchMiddlewareList = (params?: MiddlewareListParams) => {
return request.get<MiddlewareListResponse>('/DC-Control/v1/middleware', { params })
}
/** 获取中间件服务详情 */
export const fetchMiddlewareDetail = (id: number) => {
return request.get<MiddlewareItem>(`/DC-Control/v1/middleware/${id}`)
}
/** 创建中间件服务 */
export const createMiddleware = (data: MiddlewareCreateData) => {
return request.post<{ message: string; id: number }>('/DC-Control/v1/middleware', data)
}
/** 更新中间件服务 */
export const updateMiddleware = (id: number, data: MiddlewareUpdateData) => {
return request.put<{ message: string }>(`/DC-Control/v1/middleware/${id}`, data)
}
/** 部分更新采集配置 */
export const patchMiddlewareCollect = (id: number, data: MiddlewareCollectData) => {
return request.patch<{ message: string }>(`/DC-Control/v1/middleware/${id}/collect`, data)
}
/** 删除中间件服务 */
export const deleteMiddleware = (id: number) => {
return request.delete<{ message: string }>(`/DC-Control/v1/middleware/${id}`)
}
/** 上报中间件服务指标(匿名接口) */
export const uploadMiddlewareMetrics = (data: MetricsUploadData) => {
return request.post<MetricsUploadResponse>('/DC-Control/v1/middleware/metrics/upload', data)
}
/** 查询中间件服务指标元数据 */
export const fetchMiddlewareMetricsMeta = (params: MetricsMetaParams) => {
return request.get<MetricsMetaResponse>('/DC-Control/v1/services/metrics/meta', { params })
}

View File

@@ -1,40 +1,130 @@
import { request } from "@/api/request";
import { request } from '@/api/request'
/** PC 列表查询参数 */
export interface PCListParams {
page?: number
size?: number
keyword?: string
collect_on?: boolean
}
/** 资源使用率信息 */
export interface ResourceInfo {
value: number // 使用率百分比
total?: string // 总量描述
used?: string // 已用描述
}
/** PC 数据结构 */
export interface PCItem {
id: number
created_at: string
updated_at: string
deleted_at: string | null
pc_identity: string
name: string
host: string
ip_address: string
description: string
os: string
os_version: string
kernel: string
server_type: string
tags: string
location: string
remote_access: string
remote_port: number
agent_config: string
status: string
last_check_time: string
collect_on: boolean
collect_args: string
collect_interval: number
collect_last_result: string
// 资源使用率(前端展示用,可能来自采集数据)
cpu_info?: ResourceInfo
memory_info?: ResourceInfo
disk_info?: ResourceInfo
}
/** PC 列表返回数据 */
export interface PCListResult {
total: number
page: number
page_size: number
data: PCItem[]
}
/** PC 创建参数 */
export interface PCCreateParams {
pc_identity?: string
name: string
host: string
ip_address?: string
description?: string
os?: string
os_version?: string
kernel?: string
server_type?: string
tags?: string
location?: string
remote_access?: string
remote_port?: number
agent_config?: string
status?: string
last_check_time?: string
collect_on?: boolean
collect_args?: string
collect_interval?: number
collect_last_result?: string
}
/** PC 更新参数 */
export interface PCUpdateParams {
id: number
pc_identity?: string
name?: string
host?: string
ip_address?: string
description?: string
os?: string
os_version?: string
kernel?: string
server_type?: string
tags?: string
location?: string
remote_access?: string
remote_port?: number
agent_config?: string
status?: string
last_check_time?: string
collect_on?: boolean
collect_args?: string
collect_interval?: number
collect_last_result?: string
}
/** 获取PC列表分页 */
export const fetchPCList = (data?: {
page?: number;
page_size?: number;
keyword?: string;
datacenter_id?: number;
rack_id?: number;
status?: string;
sort?: string;
order?: string;
}) => {
return request.post("/Assets/v1/pc/list", data || {});
};
export const fetchPCList = (params?: PCListParams) => {
return request.get<PCListResult>('/DC-Control/v1/pcs', { params })
}
/** 获取PC详情 */
export const fetchPCDetail = (id: number) => {
return request.get(`/Assets/v1/pc/detail/${id}`);
};
return request.get<PCItem>(`/DC-Control/v1/pcs/${id}`)
}
/** 创建PC */
export const createPC = (data: any) => {
return request.post("/Assets/v1/pc/create", data);
};
export const createPC = (data: PCCreateParams) => {
return request.post<PCItem>('/DC-Control/v1/pcs', data)
}
/** 更新PC */
export const updatePC = (data: any) => {
return request.put("/Assets/v1/pc/update", data);
};
export const updatePC = (id: number, data: Omit<PCUpdateParams, 'id'>) => {
return request.put<{ message: string }>(`/DC-Control/v1/pcs/${id}`, data)
}
/** 删除PC */
export const deletePC = (id: number) => {
return request.delete(`/Assets/v1/pc/delete/${id}`);
};
/** 获取机柜列表(用于下拉选择) */
export const fetchRackListForSelect = (datacenterId?: number) => {
return request.get("/Assets/v1/rack/all", { params: { datacenter_id: datacenterId } });
};
return request.delete<{ message: string }>(`/DC-Control/v1/pcs/${id}`)
}

View File

@@ -1,31 +1,94 @@
import { request } from "@/api/request";
import { request } from '@/api/request'
/** 服务器类型 */
export interface ServerItem {
id: number
created_at: string
updated_at: string
deleted_at: string | null
server_identity: string
name: string
host: string
ip_address: string
description: string
os: string
os_version: string
kernel: string
server_type: string
tags: string
location: string
remote_access: string
remote_port: number
agent_config: string
status: string
last_check_time: string
collect_on: boolean
collect_args: string
collect_interval: number
collect_last_result: string
}
/** 服务器列表响应 */
export interface ServerListResponse {
total: number
page: number
page_size: number
data: ServerItem[]
}
/** 服务器列表请求参数 */
export interface ServerListParams {
page?: number
size?: number
keyword?: string
collect_on?: boolean
}
/** 创建/更新服务器请求参数 */
export interface ServerFormData {
server_identity?: string
name?: string
host?: string
ip_address?: string
description?: string
os?: string
os_version?: string
kernel?: string
server_type?: string
tags?: string
location?: string
remote_access?: string
remote_port?: number
agent_config?: string
status?: string
last_check_time?: string
collect_on?: boolean
collect_args?: string
collect_interval?: number
collect_last_result?: string
}
/** 获取服务器列表(分页) */
export const fetchServerList = (params?: {
page?: number;
size?: number;
keyword?: string;
collect_on?: boolean;
}) => {
return request.get("/DC-Control/v1/servers", { params });
};
export const fetchServerList = (params?: ServerListParams) => {
return request.get<ServerListResponse>('/DC-Control/v1/servers', { params })
}
/** 获取服务器详情 */
export const fetchServerDetail = (id: number) => {
return request.get(`/DC-Control/v1/servers/${id}`);
};
return request.get<ServerItem>(`/DC-Control/v1/servers/${id}`)
}
/** 创建服务器 */
export const createServer = (data: any) => {
return request.post("/DC-Control/v1/servers", data);
};
export const createServer = (data: ServerFormData) => {
return request.post<ServerItem>('/DC-Control/v1/servers', data)
}
/** 更新服务器 */
export const updateServer = (id: number, data: any) => {
return request.put(`/DC-Control/v1/servers/${id}`, data);
};
export const updateServer = (id: number, data: Partial<ServerFormData>) => {
return request.put<{ message: string }>(`/DC-Control/v1/servers/${id}`, data)
}
/** 删除服务器 */
export const deleteServer = (id: number) => {
return request.delete(`/DC-Control/v1/servers/${id}`);
};
return request.delete<{ message: string }>(`/DC-Control/v1/servers/${id}`)
}

View File

@@ -85,6 +85,15 @@ export const request = {
},
delete<T = any>(url: string, config?: RequestConfig): Promise<T> {
return instance.delete(url, config);
},
patch<T = any>(url: string, data = {}, config?: RequestConfig): Promise<T> {
let params: any
if (config?.needWorkspace) {
params = { workspace: import.meta.env.VITE_APP_WORKSPACE, ...data };
} else {
params = data;
}
return instance.patch(url, params, config);
}
};

View File

@@ -463,6 +463,23 @@ export const localMenuFlatItems: MenuItem[] = [
hide_menu: true,
created_at: '2025-12-26T13:23:52.047548+08:00',
},
{
id: 4002,
identity: '019b591d-026f-785d-b473-ac804133e253',
title: '告警详情',
title_en: 'Alert Detail',
code: 'ops:告警管理:告警详情',
description: '告警管理 - 告警详情',
app_id: 2,
parent_id: 39,
menu_path: '/alert/detail',
component: 'ops/pages/alert/detail/index',
menu_icon: 'appstore',
type: 1,
sort_key: 32,
hide_menu: true,
created_at: '2025-12-26T13:23:52.047548+08:00',
},
{
id: 41,
identity: '019b591d-027d-7eae-b9b9-23fd1b7ece75',

View File

@@ -497,6 +497,24 @@ export const localMenuItems: MenuItem[] = [
created_at: '2025-12-26T13:23:52.047548+08:00',
children: [],
},
{
id: 4002,
identity: '019b591d-026f-785d-b473-ac804133e253',
title: '告警详情',
title_en: 'Alert Detail',
code: 'ops:告警管理:告警详情',
description: '告警管理 - 告警详情',
app_id: 2,
parent_id: 39,
menu_path: '/alert/detail',
component: 'ops/pages/alert/detail/index',
menu_icon: 'appstore',
type: 1,
sort_key: 8,
hide_menu: true,
created_at: '2025-12-26T13:23:52.047548+08:00',
children: [],
},
{
id: 41,
identity: '019b591d-027d-7eae-b9b9-23fd1b7ece75',

View File

@@ -0,0 +1,644 @@
<template>
<div class="alert-detail-page">
<!-- 页面头部 -->
<div class="page-header">
<div class="header-left">
<!-- <a-button type="text" @click="handleGoBack">
<template #icon><icon-left /></template>
返回列表
</a-button> -->
<a-divider direction="vertical" />
<h2 class="page-title">告警详情</h2>
<a-tag v-if="record" :color="getStatusColor(record.status)" size="large" class="status-tag">
{{ getStatusText(record.status) }}
</a-tag>
</div>
<div class="header-right">
<a-space v-if="record && (record.status === 'pending' || record.status === 'firing')">
<a-button type="primary" @click="handleAck">
<template #icon><icon-check /></template>
确认
</a-button>
<a-button type="outline" status="success" @click="handleResolve">
<template #icon><icon-check-circle /></template>
解决
</a-button>
<a-button type="outline" status="warning" @click="handleSilence">
<template #icon><icon-mute /></template>
屏蔽
</a-button>
</a-space>
</div>
</div>
<!-- 页面内容 -->
<div class="page-content">
<a-spin :loading="loading" style="width: 100%">
<div v-if="!loading && record" class="detail-container">
<a-card class="detail-card">
<!-- 基础信息 -->
<a-descriptions title="基础信息" :column="2" bordered>
<a-descriptions-item label="告警ID">{{ record.id }}</a-descriptions-item>
<a-descriptions-item label="告警名称">{{ record.alert_name || '-' }}</a-descriptions-item>
<a-descriptions-item label="告警状态">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="告警级别">
<a-tag v-if="record.severity" :color="record.severity?.color || 'blue'">
{{ record.severity?.name || record.severity?.code || '-' }}
</a-tag>
<span v-else>-</span>
</a-descriptions-item>
<a-descriptions-item label="告警指纹">
<span class="code-text">{{ record.fingerprint || '-' }}</span>
</a-descriptions-item>
<a-descriptions-item label="策略ID / 规则ID">
{{ record.policy_id || '-' }} / {{ record.rule_id || '-' }}
</a-descriptions-item>
</a-descriptions>
<!-- 告警内容 -->
<a-descriptions title="告警内容" :column="2" bordered class="mt-4">
<a-descriptions-item label="摘要" :span="2">
{{ record.summary || '-' }}
</a-descriptions-item>
<a-descriptions-item label="描述" :span="2">
{{ record.description || '-' }}
</a-descriptions-item>
<a-descriptions-item label="当前值">
<span class="highlight-value">{{ record.value || '-' }}</span>
</a-descriptions-item>
<a-descriptions-item label="阈值">
<span class="threshold-value">{{ record.threshold || '-' }}</span>
</a-descriptions-item>
</a-descriptions>
<!-- 时间信息 -->
<a-descriptions title="时间信息" :column="2" bordered class="mt-4">
<a-descriptions-item label="开始时间">{{ formatDateTime(record.starts_at) }}</a-descriptions-item>
<a-descriptions-item label="结束时间">{{ record.ends_at ? formatDateTime(record.ends_at) : '-' }}</a-descriptions-item>
<a-descriptions-item label="持续时长">{{ formatDuration(record.duration) }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ formatDateTime(record.created_at) }}</a-descriptions-item>
<a-descriptions-item label="更新时间">{{ formatDateTime(record.updated_at) }}</a-descriptions-item>
</a-descriptions>
<!-- 处理信息 -->
<a-descriptions title="处理信息" :column="2" bordered class="mt-4">
<a-descriptions-item label="处理人">{{ record.processed_by || '-' }}</a-descriptions-item>
<a-descriptions-item label="处理时间">{{ record.processed_at ? formatDateTime(record.processed_at) : '-' }}</a-descriptions-item>
<a-descriptions-item label="关联工单">
<template v-if="record.feedback_ticket_id">
<a-link @click="handleGoToTicket(record.feedback_ticket_id)">
工单 #{{ record.feedback_ticket_id }}
</a-link>
</template>
<template v-else>-</template>
</a-descriptions-item>
</a-descriptions>
<!-- 通知信息 -->
<a-descriptions title="通知信息" :column="2" bordered class="mt-4">
<a-descriptions-item label="通知次数">{{ record.notify_count || 0 }}</a-descriptions-item>
<a-descriptions-item label="通知状态">
<a-tag :color="getNotifyStatusColor(record.notify_status)">
{{ getNotifyStatusText(record.notify_status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="最近通知时间">{{ record.notified_at ? formatDateTime(record.notified_at) : '-' }}</a-descriptions-item>
</a-descriptions>
<!-- 标签 -->
<a-descriptions title="标签 (Labels)" :column="1" bordered class="mt-4">
<a-descriptions-item label="Labels">
<div v-if="labels && Object.keys(labels).length > 0" class="tags-container">
<a-tag v-for="(value, key) in labels" :key="key" color="arcoblue">
{{ key }}: {{ value }}
</a-tag>
</div>
<span v-else>-</span>
</a-descriptions-item>
</a-descriptions>
<!-- 注解 -->
<a-descriptions title="注解 (Annotations)" :column="1" bordered class="mt-4">
<a-descriptions-item label="Annotations">
<div v-if="annotations && Object.keys(annotations).length > 0" class="tags-container">
<a-tag v-for="(value, key) in annotations" :key="key" color="green">
{{ key }}: {{ value }}
</a-tag>
</div>
<span v-else>-</span>
</a-descriptions-item>
</a-descriptions>
<!-- 分组信息 -->
<a-descriptions title="分组信息" :column="2" bordered class="mt-4">
<a-descriptions-item label="分组键">
<span class="code-text">{{ record.group_key || '-' }}</span>
</a-descriptions-item>
<a-descriptions-item label="分组标签">
<div v-if="groupLabels && Object.keys(groupLabels).length > 0" class="tags-container">
<a-tag v-for="(value, key) in groupLabels" :key="key" color="purple">
{{ key }}: {{ value }}
</a-tag>
</div>
<span v-else>-</span>
</a-descriptions-item>
</a-descriptions>
<!-- 原始数据 -->
<a-descriptions title="原始数据 (Raw Data)" :column="1" bordered class="mt-4">
<a-descriptions-item label="Raw Data">
<div class="raw-data-wrapper">
<a-button type="text" size="small" @click="toggleRawData" class="toggle-btn">
{{ showRawData ? '收起' : '展开' }}
<template #icon>
<icon-down v-if="!showRawData" />
<icon-up v-else />
</template>
</a-button>
<pre v-if="showRawData" class="raw-data">{{ rawDataFormatted }}</pre>
<span v-else class="empty-text">点击展开查看原始数据</span>
</div>
</a-descriptions-item>
</a-descriptions>
<!-- 处理记录 -->
<div class="mt-4">
<div class="section-title">处理记录</div>
<a-table
:data="processRecords"
:columns="processColumns"
:pagination="false"
:loading="processLoading"
size="medium"
>
<template #action="{ record }">
<a-tag :color="getActionColor(record.action)">
{{ getActionText(record.action) }}
</a-tag>
</template>
<template #created_at="{ record }">
{{ formatDateTime(record.created_at) }}
</template>
</a-table>
<a-empty v-if="!processLoading && processRecords.length === 0" description="暂无处理记录" />
</div>
</a-card>
</div>
<!-- 加载失败状态 -->
<div v-if="!loading && !record" class="error-state">
<a-result status="404" title="告警记录不存在">
<template #subtitle>
该告警记录可能已被删除或您没有权限查看
</template>
<template #extra>
<a-button type="primary" @click="handleGoBack">
返回列表
</a-button>
</template>
</a-result>
</div>
</a-spin>
</div>
<!-- 确认对话框 -->
<AckDialog
v-model:visible="ackDialogVisible"
:alert-record-id="alertId"
@success="handleActionSuccess"
/>
<!-- 解决对话框 -->
<ResolveDialog
v-model:visible="resolveDialogVisible"
:alert-record-id="alertId"
@success="handleActionSuccess"
/>
<!-- 屏蔽对话框 -->
<SilenceDialog
v-model:visible="silenceDialogVisible"
:alert-record-id="alertId"
@success="handleActionSuccess"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Message } from '@arco-design/web-vue'
import {
IconLeft,
IconCheck,
IconCheckCircle,
IconMute,
IconDown,
IconUp
} from '@arco-design/web-vue/es/icon'
import { fetchAlertRecordDetail, fetchAlertProcessList } from '@/api/ops/alertRecord'
import AckDialog from '../tackle/components/AckDialog.vue'
import ResolveDialog from '../tackle/components/ResolveDialog.vue'
import SilenceDialog from '../tackle/components/SilenceDialog.vue'
const route = useRoute()
const router = useRouter()
const loading = ref(true)
const processLoading = ref(false)
const record = ref<any>(null)
const processRecords = ref<any[]>([])
const showRawData = ref(false)
// 对话框状态
const ackDialogVisible = ref(false)
const resolveDialogVisible = ref(false)
const silenceDialogVisible = ref(false)
// 告警ID
const alertId = computed(() => Number(route.query.id))
// 解析标签
const labels = computed(() => {
if (!record.value?.labels) return null
try {
return JSON.parse(record.value.labels)
} catch {
return null
}
})
// 解析注解
const annotations = computed(() => {
if (!record.value?.annotations) return null
try {
return JSON.parse(record.value.annotations)
} catch {
return null
}
})
// 解析分组标签
const groupLabels = computed(() => {
if (!record.value?.group_labels) return null
try {
return JSON.parse(record.value.group_labels)
} catch {
return null
}
})
// 格式化原始数据
const rawDataFormatted = computed(() => {
if (!record.value?.raw_data) return '-'
try {
const data = JSON.parse(record.value.raw_data)
return JSON.stringify(data, null, 2)
} catch {
return record.value.raw_data
}
})
// 处理记录表格列
const processColumns = [
{ title: '时间', dataIndex: 'created_at', slotName: 'created_at', width: 180 },
{ title: '操作', dataIndex: 'action', slotName: 'action', width: 100 },
{ title: '操作人', dataIndex: 'operator', width: 120 },
{ title: '备注', dataIndex: 'comment', ellipsis: true, tooltip: true }
]
// 加载详情
const loadDetail = async () => {
if (!alertId.value) {
Message.error('告警ID无效')
return
}
loading.value = true
try {
const data = await fetchAlertRecordDetail(alertId.value)
record.value = data.details || data
} catch (error) {
console.error('加载详情失败:', error)
Message.error('加载详情失败')
} finally {
loading.value = false
}
}
// 加载处理记录
const loadProcessRecords = async () => {
if (!alertId.value) return
processLoading.value = true
try {
const result = await fetchAlertProcessList({
alert_record_id: alertId.value,
page: 1,
page_size: 100
})
processRecords.value = result.details?.data || result.data || []
} catch (error) {
console.error('加载处理记录失败:', error)
} finally {
processLoading.value = false
}
}
// 返回
const handleGoBack = () => {
router.back()
}
// 跳转工单
const handleGoToTicket = (ticketId: number) => {
router.push(`/ops/ticket/${ticketId}`)
}
// 确认
const handleAck = () => {
ackDialogVisible.value = true
}
// 解决
const handleResolve = () => {
resolveDialogVisible.value = true
}
// 屏蔽
const handleSilence = () => {
silenceDialogVisible.value = true
}
// 操作成功回调
const handleActionSuccess = () => {
loadDetail()
loadProcessRecords()
}
// 切换原始数据显示
const toggleRawData = () => {
showRawData.value = !showRawData.value
}
// 格式化日期时间
const formatDateTime = (datetime: string) => {
if (!datetime) return '-'
return new Date(datetime).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
// 格式化持续时间
const formatDuration = (seconds: number) => {
if (!seconds) return '-'
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
const parts: string[] = []
if (days > 0) parts.push(`${days}`)
if (hours > 0) parts.push(`${hours}小时`)
if (minutes > 0) parts.push(`${minutes}分钟`)
if (secs > 0 && parts.length === 0) parts.push(`${secs}`)
return parts.length > 0 ? parts.join('') : '-'
}
// 获取状态颜色
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
firing: 'red',
pending: 'orange',
acked: 'gold',
resolved: 'green',
silenced: 'gray',
suppressed: 'lightgray'
}
return colorMap[status] || 'blue'
}
// 获取状态文本
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
firing: '告警中',
pending: '待处理',
acked: '已确认',
resolved: '已解决',
silenced: '已屏蔽',
suppressed: '已抑制'
}
return textMap[status] || status
}
// 获取通知状态颜色
const getNotifyStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
success: 'green',
failed: 'red',
pending: 'orange'
}
return colorMap[status] || 'blue'
}
// 获取通知状态文本
const getNotifyStatusText = (status: string) => {
const textMap: Record<string, string> = {
success: '成功',
failed: '失败',
pending: '待发送'
}
return textMap[status] || status || '-'
}
// 获取操作颜色
const getActionColor = (action: string) => {
const colorMap: Record<string, string> = {
ack: 'gold',
resolve: 'green',
silence: 'gray',
comment: 'blue',
assign: 'purple',
escalate: 'orange',
close: 'red'
}
return colorMap[action] || 'blue'
}
// 获取操作文本
const getActionText = (action: string) => {
const textMap: Record<string, string> = {
ack: '确认',
resolve: '解决',
silence: '屏蔽',
comment: '评论',
assign: '分配',
escalate: '升级',
close: '关闭'
}
return textMap[action] || action
}
onMounted(() => {
loadDetail()
loadProcessRecords()
})
</script>
<script lang="ts">
export default {
name: 'AlertDetailPage',
}
</script>
<style scoped lang="less">
.alert-detail-page {
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--color-fill-2);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background-color: var(--color-bg-2);
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
.header-left {
display: flex;
align-items: center;
.page-title {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.status-tag {
margin-left: 12px;
}
}
}
.page-content {
flex: 1;
padding: 20px;
overflow: auto;
}
.detail-container {
max-width: 1200px;
margin: 0 auto;
}
.detail-card {
:deep(.arco-card-body) {
padding: 20px;
}
}
.mt-4 {
margin-top: 16px;
}
.code-text {
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
font-size: 13px;
background: var(--color-fill-2);
padding: 2px 6px;
border-radius: 4px;
}
.highlight-value {
color: rgb(var(--danger-6));
font-weight: 600;
}
.threshold-value {
color: rgb(var(--warning-6));
font-weight: 600;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.section-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 12px;
padding-left: 10px;
border-left: 3px solid rgb(var(--primary-6));
}
.raw-data-wrapper {
.toggle-btn {
margin-bottom: 8px;
}
.raw-data {
margin: 0;
padding: 12px;
background: var(--color-fill-1);
border-radius: 6px;
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
font-size: 12px;
line-height: 1.6;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow-y: auto;
}
.empty-text {
color: var(--color-text-3);
}
}
.error-state {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
// 响应式布局
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 12px;
align-items: flex-start;
.header-right {
width: 100%;
}
}
:deep(.arco-descriptions) {
.arco-descriptions-table {
table-layout: fixed;
}
.arco-descriptions-item-label {
width: 100px;
}
}
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<a-modal
:visible="visible"
title="指标采集"
@ok="handleOk"
@cancel="handleCancel"
@update:visible="handleUpdateVisible"
:confirm-loading="confirmLoading"
width="600px"
>
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
<a-form-item field="ids" label="选择服务" required>
<a-select
v-model="formData.ids"
placeholder="请选择要采集的服务最多20个"
multiple
:max-tag-count="5"
allow-clear
>
<a-option
v-for="item in serviceList"
:key="item.id"
:value="item.id"
:label="item.name"
:disabled="!item.enabled || !item.collect_on"
>
<div class="service-option">
<span>{{ item.name }}</span>
<a-tag
size="small"
:color="item.enabled && item.collect_on ? 'green' : 'red'"
style="margin-left: 8px"
>
{{ item.enabled && item.collect_on ? '可采集' : '不可采集' }}
</a-tag>
</div>
</a-option>
</a-select>
<template #extra>
<span style="color: rgb(var(--text-3))">
enabled=true collect_on=true 的服务可执行采集
</span>
</template>
</a-form-item>
<a-form-item field="persist" label="持久化到时序库">
<a-switch v-model="formData.persist" />
<template #extra>
<span style="color: rgb(var(--text-3))">
开启后将采集结果写入时序库
</span>
</template>
</a-form-item>
</a-form>
<!-- 采集结果展示 -->
<div v-if="collectResults.length > 0" class="collect-results">
<a-divider>采集结果</a-divider>
<a-list :bordered="false">
<a-list-item v-for="result in collectResults" :key="result.id">
<a-list-item-meta
:title="result.service_identity"
:description="`ID: ${result.id}`"
/>
<template #actions>
<a-tag v-if="result.metrics && result.metrics.length > 0" color="green">成功</a-tag>
<a-tag v-else color="red">失败</a-tag>
</template>
<div v-if="result.error" class="error-message">
{{ result.error }}
</div>
<div v-if="result.metrics && result.metrics.length > 0" class="metrics-display">
<div class="metrics-summary">
共采集 {{ result.metrics.length }} 个指标
</div>
<a-button size="small" @click="toggleMetrics(result.id)">
{{ expandedMetrics.includes(result.id) ? '隐藏' : '展开' }}详情
</a-button>
<div v-if="expandedMetrics.includes(result.id)" class="metrics-list">
<div v-for="metric in result.metrics" :key="metric.metric_name" class="metric-item">
<span class="metric-name">{{ metric.metric_name }}</span>
<span class="metric-value">{{ metric.metric_value }}</span>
<span v-if="metric.metric_unit" class="metric-unit">{{ metric.metric_unit }}</span>
<span class="metric-time">{{ formatTime(metric.timestamp) }}</span>
</div>
</div>
</div>
</a-list-item>
</a-list>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { FormInstance } from '@arco-design/web-vue'
import {
collectDatabaseMetrics,
} from '@/api/ops/database'
interface MetricItem {
metric_name: string
metric_value: number
metric_unit: string
service_identity: string
timestamp: string
type: string
}
interface CollectResultItem {
id: number
service_identity: string
metrics?: MetricItem[]
error?: string
}
interface Props {
visible: boolean
serviceList?: any[]
}
const props = withDefaults(defineProps<Props>(), {
serviceList: () => [],
})
const emit = defineEmits(['update:visible', 'success'])
const formRef = ref<FormInstance>()
const confirmLoading = ref(false)
const collectResults = ref<CollectResultItem[]>([])
const expandedMetrics = ref<number[]>([])
const formData = reactive({
ids: [] as number[],
persist: true,
})
const rules = {
ids: [
{ required: true, message: '请选择要采集的服务' },
{
validator: (value: number[], callback: (error?: string) => void) => {
if (value.length > 20) {
callback('单次最多选择20个服务')
} else {
callback()
}
},
},
],
}
// 格式化时间
const formatTime = (time: string) => {
if (!time) return '-'
const date = new Date(time)
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}`
}
watch(
() => props.visible,
(val) => {
if (val) {
formData.ids = []
formData.persist = true
collectResults.value = []
expandedMetrics.value = []
}
}
)
const toggleMetrics = (id: number) => {
const index = expandedMetrics.value.indexOf(id)
if (index > -1) {
expandedMetrics.value.splice(index, 1)
} else {
expandedMetrics.value.push(id)
}
}
const handleOk = async () => {
try {
await formRef.value?.validate()
confirmLoading.value = true
const res: any = await collectDatabaseMetrics({
ids: formData.ids,
persist: formData.persist,
})
collectResults.value = res?.details?.results || []
const successCount = collectResults.value.filter((r) => r.metrics && r.metrics.length > 0).length
const failCount = collectResults.value.filter((r) => r.error).length
if (failCount === 0) {
Message.success(`采集完成,成功 ${successCount}`)
} else if (successCount === 0) {
Message.error(`采集完成,失败 ${failCount}`)
} else {
Message.warning(`采集完成,成功 ${successCount} 个,失败 ${failCount}`)
}
emit('success')
} catch (error: any) {
console.error('采集失败:', error)
if (error?.response?.data?.message) {
Message.error(error.response.data.message)
} else {
Message.error('采集失败')
}
} finally {
confirmLoading.value = false
}
}
const handleUpdateVisible = (value: boolean) => {
emit('update:visible', value)
}
const handleCancel = () => {
emit('update:visible', false)
formRef.value?.resetFields()
}
</script>
<style scoped lang="less">
.service-option {
display: flex;
align-items: center;
justify-content: space-between;
}
.collect-results {
margin-top: 16px;
.error-message {
color: rgb(var(--danger-6));
font-size: 12px;
margin-top: 4px;
}
.metrics-display {
margin-top: 4px;
.metrics-summary {
font-size: 12px;
color: rgb(var(--text-3));
margin-bottom: 4px;
}
.metrics-list {
margin-top: 8px;
padding: 8px;
background: rgb(var(--gray-2));
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
.metric-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
font-size: 12px;
.metric-name {
color: rgb(var(--text-1));
font-weight: 500;
}
.metric-value {
color: rgb(var(--success-6));
}
.metric-unit {
color: rgb(var(--text-3));
}
.metric-time {
color: rgb(var(--text-3));
margin-left: auto;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,285 @@
<template>
<a-modal
:visible="visible"
title="数据库服务详情"
@cancel="handleCancel"
@update:visible="handleUpdateVisible"
:footer="false"
width="700px"
>
<a-spin :loading="loading" style="width: 100%">
<a-descriptions
:column="2"
bordered
size="medium"
:label-style="{ width: '140px' }"
>
<a-descriptions-item label="服务ID">
{{ detailData?.id }}
</a-descriptions-item>
<a-descriptions-item label="服务唯一标识">
{{ detailData?.service_identity || '-' }}
</a-descriptions-item>
<a-descriptions-item label="服务名称">
{{ detailData?.name || '-' }}
</a-descriptions-item>
<a-descriptions-item label="数据库类型">
<a-tag :color="getTypeColor(detailData?.type)">
{{ detailData?.type || '-' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="主机地址">
{{ detailData?.host || '-' }}
</a-descriptions-item>
<a-descriptions-item label="端口">
{{ detailData?.port || '-' }}
</a-descriptions-item>
<a-descriptions-item label="用户名">
{{ detailData?.username || '-' }}
</a-descriptions-item>
<a-descriptions-item label="数据库名">
{{ detailData?.database || '-' }}
</a-descriptions-item>
<a-descriptions-item label="服务器标识">
{{ detailData?.server_identity || '-' }}
</a-descriptions-item>
<a-descriptions-item label="分类">
{{ detailData?.category || '-' }}
</a-descriptions-item>
<a-descriptions-item label="启用监控">
<a-tag :color="detailData?.enabled ? 'green' : 'red'">
{{ detailData?.enabled ? '已启用' : '已禁用' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="采集间隔">
{{ detailData?.interval ? `${detailData.interval}` : '-' }}
</a-descriptions-item>
<a-descriptions-item label="进程内采集">
<a-tag :color="detailData?.collect_on ? 'green' : 'red'">
{{ detailData?.collect_on ? '已启用' : '已禁用' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="采集间隔">
{{ detailData?.collect_interval ? `${detailData.collect_interval}` : '-' }}
</a-descriptions-item>
<a-descriptions-item label="运行状态" :span="2">
<a-tag :color="getStatusColor(detailData?.status)">
{{ getStatusText(detailData?.status) }}
</a-tag>
<span v-if="detailData?.status_message" style="margin-left: 8px; color: rgb(var(--text-3))">
{{ detailData.status_message }}
</span>
</a-descriptions-item>
<a-descriptions-item label="状态码">
{{ detailData?.status_code || '-' }}
</a-descriptions-item>
<a-descriptions-item label="响应时间">
{{ detailData?.response_time ? `${detailData.response_time.toFixed(2)}ms` : '-' }}
</a-descriptions-item>
<a-descriptions-item label="连续错误次数">
<span :style="{ color: (detailData?.continuous_errors ?? 0) > 0 ? 'rgb(var(--danger-6))' : '' }">
{{ detailData?.continuous_errors ?? 0 }}
</span>
</a-descriptions-item>
<a-descriptions-item label="运行时长">
{{ formatUptime(detailData?.uptime) }}
</a-descriptions-item>
<a-descriptions-item label="最后检查时间">
{{ formatTime(detailData?.last_check_time) }}
</a-descriptions-item>
<a-descriptions-item label="最后在线时间">
{{ formatTime(detailData?.last_online_time) }}
</a-descriptions-item>
<a-descriptions-item label="最后离线时间">
{{ formatTime(detailData?.last_offline_time) }}
</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ formatTime(detailData?.created_at) }}
</a-descriptions-item>
<a-descriptions-item label="更新时间">
{{ formatTime(detailData?.updated_at) }}
</a-descriptions-item>
<a-descriptions-item label="标签" :span="2">
<a-tag v-for="(tag, index) in parseTags(detailData?.tags)" :key="index" style="margin-right: 4px">
{{ tag }}
</a-tag>
<span v-if="!parseTags(detailData?.tags).length">-</span>
</a-descriptions-item>
<a-descriptions-item label="描述" :span="2">
{{ detailData?.description || '-' }}
</a-descriptions-item>
<a-descriptions-item label="采集参数" :span="2">
{{ detailData?.collect_args || '-' }}
</a-descriptions-item>
<a-descriptions-item label="最近采集结果" :span="2">
<div v-if="detailData?.collect_last_result" class="collect-result">
{{ detailData.collect_last_result }}
</div>
<span v-else>-</span>
</a-descriptions-item>
<a-descriptions-item label="额外配置" :span="2">
<div v-if="detailData?.extra" class="extra-config">
<a-button size="small" @click="showExtraConfig = !showExtraConfig">
{{ showExtraConfig ? '隐藏' : '展开' }} JSON
</a-button>
<pre v-if="showExtraConfig" class="json-display">{{ formatJson(detailData?.extra) }}</pre>
</div>
<span v-else>-</span>
</a-descriptions-item>
</a-descriptions>
</a-spin>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { fetchDatabaseDetail, type DatabaseService } from '@/api/ops/database'
interface Props {
visible: boolean
recordId?: number
}
const props = withDefaults(defineProps<Props>(), {
recordId: 0,
})
const emit = defineEmits(['update:visible'])
const loading = ref(false)
const detailData = ref<DatabaseService | null>(null)
const showExtraConfig = ref(false)
watch(
() => props.visible,
(val) => {
if (val && props.recordId) {
fetchDetail()
showExtraConfig.value = false
}
}
)
const fetchDetail = async () => {
loading.value = true
try {
const res = await fetchDatabaseDetail(props.recordId)
detailData.value = res
} catch (error) {
console.error('获取详情失败:', error)
Message.error('获取详情失败')
detailData.value = null
} finally {
loading.value = false
}
}
const handleUpdateVisible = (value: boolean) => {
emit('update:visible', value)
}
const handleCancel = () => {
emit('update:visible', false)
}
// 获取数据库类型颜色
const getTypeColor = (type?: string) => {
const colorMap: Record<string, string> = {
MySQL: 'blue',
PostgreSQL: 'purple',
Redis: 'red',
MongoDB: 'green',
DM: 'orange',
KingBase: 'cyan',
}
return colorMap[type || ''] || 'gray'
}
// 获取状态颜色
const getStatusColor = (status?: string) => {
const colorMap: Record<string, string> = {
online: 'green',
offline: 'red',
error: 'orange',
}
return colorMap[status || ''] || 'gray'
}
// 获取状态文本
const getStatusText = (status?: string) => {
const textMap: Record<string, string> = {
online: '在线',
offline: '离线',
error: '错误',
}
return textMap[status || ''] || '-'
}
// 格式化运行时长
const formatUptime = (uptime?: number) => {
if (!uptime) return '-'
const days = Math.floor(uptime / 86400)
const hours = Math.floor((uptime % 86400) / 3600)
const minutes = Math.floor((uptime % 3600) / 60)
if (days > 0) {
return `${days}${hours}小时 ${minutes}分钟`
}
if (hours > 0) {
return `${hours}小时 ${minutes}分钟`
}
return `${minutes}分钟`
}
// 格式化时间
const formatTime = (time?: string | null) => {
if (!time) return '-'
const date = new Date(time)
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}`
}
// 解析标签
const parseTags = (tags?: string) => {
if (!tags) return []
return tags.split(',').filter((t) => t.trim())
}
// 格式化JSON
const formatJson = (json?: string) => {
if (!json) return ''
try {
return JSON.stringify(JSON.parse(json), null, 2)
} catch {
return json
}
}
</script>
<style scoped lang="less">
.collect-result {
max-height: 100px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.extra-config {
.json-display {
margin-top: 8px;
padding: 8px;
background: rgb(var(--gray-2));
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<a-modal
:visible="visible"
:title="isEdit ? '编辑数据库' : '新增数据库'"
:title="isEdit ? '编辑数据库服务' : '新增数据库服务'"
@ok="handleOk"
@cancel="handleCancel"
@update:visible="handleUpdateVisible"
@@ -11,125 +11,147 @@
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="unique_id" label="唯一标识">
<a-form-item field="service_identity" label="服务唯一标识">
<a-input
v-model="formData.unique_id"
placeholder="输入为空系统自动生成UUID"
v-model="formData.service_identity"
placeholder="输入为空系统自动生成 host:port:database"
:disabled="isEdit"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="name" label="数据库名称">
<a-input v-model="formData.name" placeholder="请输入数据库名称" />
<a-form-item field="name" label="服务名称" required>
<a-input v-model="formData.name" placeholder="请输入服务名称(唯一)" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="database" label="数据库类型">
<a-select v-model="formData.database" placeholder="请选择数据库类型">
<a-form-item field="server_identity" label="服务器标识">
<a-input v-model="formData.server_identity" placeholder="关联服务器唯一标识" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="type" label="数据库类型" required>
<a-select v-model="formData.type" placeholder="请选择数据库类型">
<a-option value="MySQL">MySQL</a-option>
<a-option value="PostgreSQL">PostgreSQL</a-option>
<a-option value="Oracle">Oracle</a-option>
<a-option value="SQL Server">SQL Server</a-option>
<a-option value="Redis">Redis</a-option>
<a-option value="MongoDB">MongoDB</a-option>
<a-option value="Elasticsearch">Elasticsearch</a-option>
<a-option value="Other">其它</a-option>
<a-option value="DM">DM达梦</a-option>
<a-option value="KingBase">KingBase人大金仓</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="version" label="版本">
<a-input v-model="formData.version" placeholder="请输入数据库版本" />
</a-form-item>
</a-col>
</a-row>
<a-form-item field="location" label="位置信息">
<a-input
v-model="formData.location"
placeholder="请输入位置信息"
/>
</a-form-item>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="tags" label="标签">
<a-input v-model="formData.tags" placeholder="多个标签,逗号间隔" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="ip" label="IP 地址">
<a-input v-model="formData.ip" placeholder="请输入 IP 地址" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="port" label="端口">
<a-input v-model="formData.port" placeholder="请输入数据库端口" />
<a-form-item field="host" label="主机地址" required>
<a-input v-model="formData.host" placeholder="请输入主机地址" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="remote_access" label="远程访问">
<a-switch v-model="formData.remote_access" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="agent_config" label="Agent 配置">
<a-switch v-model="formData.agent_config" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item v-if="formData.agent_config" field="agent_url" label="Agent URL">
<a-input v-model="formData.agent_url" placeholder="请输入 Agent URL" />
<a-form-item field="port" label="端口" required>
<a-input-number
v-model="formData.port"
placeholder="请输入端口"
:min="1"
:max="65535"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="data_collection" label="数据采集">
<a-switch v-model="formData.data_collection" />
<a-form-item field="username" label="用户名">
<a-input v-model="formData.username" placeholder="请输入用户名" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item v-if="formData.data_collection" field="collection_interval" label="采集间隔">
<a-select v-model="formData.collection_interval" placeholder="请选择采集间隔">
<a-option :value="1">1 分钟</a-option>
<a-option :value="5">5 分钟</a-option>
<a-option :value="10">10 分钟</a-option>
<a-option :value="30">30 分钟</a-option>
</a-select>
<a-form-item field="password" label="密码">
<a-input-password
v-model="formData.password"
placeholder="请输入密码"
/>
<template #extra v-if="isEdit">
<span style="color: rgb(var(--warning-6))">编辑时密码不回显如需修改请输入新密码</span>
</template>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="status" label="状态">
<a-select v-model="formData.status" placeholder="请选择状态">
<a-option value="online">在线</a-option>
<a-option value="offline">离线</a-option>
<a-option value="maintenance">维护中</a-option>
<a-option value="retired">已退役</a-option>
</a-select>
<a-form-item field="database" label="数据库名">
<a-input v-model="formData.database" placeholder="请输入数据库名" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="interval" label="采集间隔(秒)">
<a-input-number
v-model="formData.interval"
placeholder="默认60秒"
:min="10"
:max="3600"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item field="remark" label="备注信息">
<a-form-item field="description" label="描述信息">
<a-textarea
v-model="formData.remark"
placeholder="请输入备注信息"
:rows="4"
v-model="formData.description"
placeholder="请输入描述信息"
:rows="2"
/>
</a-form-item>
<a-form-item field="tags" label="标签">
<a-input v-model="formData.tags" placeholder="多个标签,逗号间隔" />
</a-form-item>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="enabled" label="启用监控">
<a-switch v-model="formData.enabled" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="collect_on" label="启用进程内采集">
<a-switch v-model="formData.collect_on" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20" v-if="formData.collect_on">
<a-col :span="12">
<a-form-item field="collect_interval" label="进程内采集间隔(秒)">
<a-input-number
v-model="formData.collect_interval"
placeholder="默认60秒"
:min="10"
:max="3600"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="collect_args" label="采集参数">
<a-input v-model="formData.collect_args" placeholder="采集参数配置" />
</a-form-item>
</a-col>
</a-row>
<a-form-item field="extra" label="额外配置(JSON)">
<a-textarea
v-model="formData.extra"
placeholder='JSON格式{"charset":"utf8mb4"}'
:rows="2"
/>
</a-form-item>
</a-form>
@@ -139,8 +161,14 @@
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { v4 as uuidv4 } from 'uuid'
import type { FormInstance } from '@arco-design/web-vue'
import {
createDatabase,
updateDatabase,
type CreateDatabaseParams,
type UpdateDatabaseParams,
type DatabaseType,
} from '@/api/ops/database'
interface Props {
visible: boolean
@@ -155,92 +183,155 @@ const emit = defineEmits(['update:visible', 'success'])
const formRef = ref<FormInstance>()
const confirmLoading = ref(false)
const selectedLocation = ref('')
const isEdit = computed(() => !!props.record?.id)
const formData = reactive({
unique_id: '',
service_identity: '',
server_identity: '',
name: '',
category: 'database',
type: '' as DatabaseType | '',
host: '',
port: 3306,
username: '',
password: '',
database: '',
version: '',
location: '',
description: '',
enabled: true,
interval: 60,
extra: '',
tags: '',
ip: '',
port: '',
remote_access: false,
agent_config: false,
agent_url: '',
data_collection: false,
collection_interval: 5,
status: 'offline',
remark: '',
collect_on: true,
collect_args: '',
collect_interval: 60,
})
const rules = {
name: [{ required: true, message: '请输入数据库名称' }],
database: [{ required: true, message: '请选择数据库类型' }],
ip: [{ required: true, message: '请输入 IP 地址' }],
name: [{ required: true, message: '请输入服务名称' }],
type: [{ required: true, message: '请选择数据库类型' }],
host: [{ required: true, message: '请输入主机地址' }],
port: [{ required: true, message: '请输入端口' }],
category: [{ required: true, message: '分类必须为database' }],
}
const locationOptions = ref([
{ label: 'A数据中心-3层-24机柜-5U位', value: 'A数据中心-3层-24机柜-5U位' },
{ label: 'A数据中心-3层-24机柜-6U位', value: 'A数据中心-3层-24机柜-6U位' },
{ label: 'B数据中心-1层-12机柜-1U位', value: 'B数据中心-1层-12机柜-1U位' },
{ label: 'B数据中心-1层-12机柜-2U位', value: 'B数据中心-1层-12机柜-2U位' },
{ label: 'C数据中心-2层-8机柜-3U位', value: 'C数据中心-2层-8机柜-3U位' },
])
watch(
() => props.visible,
(val) => {
if (val) {
if (isEdit.value && props.record) {
Object.assign(formData, props.record)
Object.assign(formData, {
service_identity: props.record.service_identity || '',
server_identity: props.record.server_identity || '',
name: props.record.name || '',
category: props.record.category || 'database',
type: props.record.type || '',
host: props.record.host || '',
port: props.record.port || 3306,
username: props.record.username || '',
password: '', // 编辑时密码不回显
database: props.record.database || '',
description: props.record.description || '',
enabled: props.record.enabled ?? true,
interval: props.record.interval || 60,
extra: props.record.extra || '',
tags: props.record.tags || '',
collect_on: props.record.collect_on ?? true,
collect_args: props.record.collect_args || '',
collect_interval: props.record.collect_interval || 60,
})
} else {
Object.assign(formData, {
unique_id: '',
service_identity: '',
server_identity: '',
name: '',
category: 'database',
type: '',
host: '',
port: 3306,
username: '',
password: '',
database: '',
version: '',
location: '',
description: '',
enabled: true,
interval: 60,
extra: '',
tags: '',
ip: '',
port: '',
remote_access: false,
agent_config: false,
agent_url: '',
data_collection: false,
collection_interval: 5,
status: 'offline',
remark: '',
collect_on: true,
collect_args: '',
collect_interval: 60,
})
}
}
}
)
const handleLocationSelect = (value: string) => {
formData.location = value
}
const handleOk = async () => {
try {
await formRef.value?.validate()
if (!formData.unique_id) {
formData.unique_id = uuidv4()
}
confirmLoading.value = true
await new Promise(resolve => setTimeout(resolve, 1000))
Message.success(isEdit.value ? '更新成功' : '创建成功')
if (isEdit.value) {
const updateData: UpdateDatabaseParams = {
service_identity: formData.service_identity,
server_identity: formData.server_identity,
name: formData.name,
category: formData.category,
type: formData.type as DatabaseType,
host: formData.host,
port: formData.port,
username: formData.username,
database: formData.database,
description: formData.description,
enabled: formData.enabled,
interval: formData.interval,
extra: formData.extra,
tags: formData.tags,
collect_on: formData.collect_on,
collect_args: formData.collect_args,
collect_interval: formData.collect_interval,
}
// 只有在编辑时输入了密码才传递
if (formData.password) {
updateData.password = formData.password
}
await updateDatabase(props.record.id, updateData)
Message.success('更新成功')
} else {
const createData: CreateDatabaseParams = {
service_identity: formData.service_identity,
server_identity: formData.server_identity,
name: formData.name,
category: formData.category,
type: formData.type as DatabaseType,
host: formData.host,
port: formData.port,
username: formData.username,
password: formData.password,
database: formData.database,
description: formData.description,
enabled: formData.enabled,
interval: formData.interval,
extra: formData.extra,
tags: formData.tags,
collect_on: formData.collect_on,
collect_args: formData.collect_args,
collect_interval: formData.collect_interval,
}
await createDatabase(createData)
Message.success('创建成功')
}
emit('success')
handleCancel()
} catch (error) {
console.error('验证失败:', error)
} catch (error: any) {
console.error('操作失败:', error)
if (error?.response?.data?.message) {
Message.error(error.response.data.message)
} else {
Message.error('操作失败')
}
} finally {
confirmLoading.value = false
}

View File

@@ -6,73 +6,96 @@ export const columns = [
slotName: 'id',
},
{
dataIndex: 'unique_id',
title: '唯一标识',
dataIndex: 'service_identity',
title: '服务标识',
width: 150,
ellipsis: true,
tooltip: true,
},
{
dataIndex: 'name',
title: '名称',
title: '服务名称',
width: 150,
ellipsis: true,
tooltip: true,
},
{
dataIndex: 'database',
title: '数据库',
dataIndex: 'type',
title: '数据库类型',
width: 120,
slotName: 'type',
},
{
dataIndex: 'version',
title: '版本',
width: 150,
},
{
dataIndex: 'location',
title: '位置信息',
width: 150,
},
{
dataIndex: 'tags',
title: '标签',
width: 120,
},
{
dataIndex: 'ip',
title: 'IP地址',
width: 150,
dataIndex: 'host',
title: '主机地址',
width: 140,
ellipsis: true,
tooltip: true,
},
{
dataIndex: 'port',
title: '端口',
width: 80,
},
{
dataIndex: 'database',
title: '数据库名',
width: 120,
slotName: 'database',
},
{
dataIndex: 'enabled',
title: '监控状态',
width: 100,
slotName: 'enabled',
},
{
dataIndex: 'sys_indicator',
title: '系统指标',
width: 200,
slotName: 'sys_indicator',
},
{
dataIndex: 'qps',
title: '数据库指标 QPS',
width: 180,
slotName: 'qps',
},
{
dataIndex: 'conn',
title: '数据库指标 Conn',
width: 150,
slotName: 'conn',
dataIndex: 'collect_on',
title: '进程内采集',
width: 120,
slotName: 'collect_on',
},
{
dataIndex: 'status',
title: '状态',
title: '运行状态',
width: 100,
slotName: 'status',
},
{
dataIndex: 'response_time',
title: '响应时间(ms)',
width: 120,
slotName: 'response_time',
},
{
dataIndex: 'uptime',
title: '运行时长',
width: 120,
slotName: 'uptime',
},
{
dataIndex: 'last_check_time',
title: '最后检查时间',
width: 160,
slotName: 'last_check_time',
},
{
dataIndex: 'tags',
title: '标签',
width: 150,
slotName: 'tags',
},
{
dataIndex: 'description',
title: '描述',
width: 200,
ellipsis: true,
tooltip: true,
},
{
dataIndex: 'actions',
title: '操作',
width: 180,
width: 280,
fixed: 'right' as const,
slotName: 'actions',
},

View File

@@ -5,35 +5,44 @@ export const searchFormConfig: FormItem[] = [
field: 'keyword',
label: '关键词',
type: 'input',
placeholder: '请输入服务名称、编码或IP',
placeholder: '请输入服务名称',
span: 6,
},
{
field: 'datacenter_id',
label: '数据中心',
field: 'type',
label: '数据库类型',
type: 'select',
placeholder: '请选择数据中心',
options: [], // 需要动态加载
placeholder: '请选择数据库类型',
options: [
{ label: 'MySQL', value: 'MySQL' },
{ label: 'PostgreSQL', value: 'PostgreSQL' },
{ label: 'Redis', value: 'Redis' },
{ label: 'MongoDB', value: 'MongoDB' },
{ label: 'DM', value: 'DM' },
{ label: 'KingBase', value: 'KingBase' },
],
span: 6,
},
{
field: 'rack_id',
label: '机柜',
field: 'enabled',
label: '监控状态',
type: 'select',
placeholder: '请选择机柜',
options: [], // 需要动态加载
placeholder: '请选择监控状态',
options: [
{ label: '已启用', value: true },
{ label: '已禁用', value: false },
],
span: 6,
},
{
field: 'status',
label: '状态',
label: '运行状态',
type: 'select',
placeholder: '请选择状态',
placeholder: '请选择运行状态',
options: [
{ label: '在线', value: 'online' },
{ label: '离线', value: 'offline' },
{ label: '维护中', value: 'maintenance' },
{ label: '已退役', value: 'retired' },
{ label: '错误', value: 'error' },
],
span: 6,
},

View File

@@ -7,7 +7,7 @@
:columns="columns"
:loading="loading"
:pagination="pagination"
title="数据库管理"
title="数据库服务管理"
search-button-text="查询"
reset-button-text="重置"
@update:form-model="handleFormModelUpdate"
@@ -17,168 +17,106 @@
@refresh="handleRefresh"
>
<template #toolbar-left>
<a-button type="primary" @click="handleAdd">
<template #icon>
<icon-plus />
</template>
新增数据库
</a-button>
<a-space>
<a-button type="primary" @click="handleAdd">
<template #icon>
<icon-plus />
</template>
新增数据库
</a-button>
<a-button type="outline" @click="handleCollect">
<template #icon>
<icon-sync />
</template>
指标采集
</a-button>
</a-space>
</template>
<!-- ID -->
<template #id="{ record }">
{{ record.id }}
</template>
<!-- 数据库类型 -->
<template #type="{ record }">
<a-tag :color="getTypeColor(record.type)">
{{ record.type }}
</a-tag>
</template>
<!-- 数据库名 -->
<template #database="{ record }">
{{ record.database || '-' }}
</template>
<!-- 版本 -->
<template #version="{ record }">
{{ record.version || '-' }}
</template>
<!-- 位置信息 -->
<template #location="{ record }">
{{ record.location || '-' }}
</template>
<!-- 标签 -->
<template #tags="{ record }">
<a-tag v-for="(tag, index) in (record.tags || '').split(',').filter(t => t.trim())" :key="index">
{{ tag.trim() }}
<!-- 监控状态 -->
<template #enabled="{ record }">
<a-tag :color="record.enabled ? 'green' : 'red'">
{{ record.enabled ? '已启用' : '已禁用' }}
</a-tag>
</template>
<!-- IP 地址 -->
<template #ip="{ record }">
{{ record.ip || '-' }}
<!-- 进程内采集 -->
<template #collect_on="{ record }">
<a-tag :color="record.collect_on ? 'green' : 'red'">
{{ record.collect_on ? '已启用' : '已禁用' }}
</a-tag>
</template>
<!-- 端口 -->
<template #port="{ record }">
{{ record.port || '-' }}
</template>
<!-- 系统指标 -->
<template #sys_indicator="{ record }">
<div v-if="!record.agent_config" class="not-configured">
未配置
</div>
<div v-else class="sys-indicator-display">
<!-- CPU -->
<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>
<!-- 内存 -->
<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>
<!-- 硬盘 -->
<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>
</div>
</template>
<!-- 数据库指标 QPS -->
<template #qps="{ record }">
<QpsChart :record-id="record.id" :configured="record.agent_config" />
</template>
<!-- 数据库指标 Conn -->
<template #conn="{ record }">
<ConnChart :record-id="record.id" :configured="record.agent_config" />
</template>
<!-- 状态 -->
<!-- 运行状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 操作栏 - 下拉菜单 -->
<!-- 响应时间 -->
<template #response_time="{ record }">
<span :style="{ color: getResponseTimeColor(record.response_time) }">
{{ record.response_time ? record.response_time.toFixed(2) : '-' }}
</span>
</template>
<!-- 运行时长 -->
<template #uptime="{ record }">
{{ formatUptime(record.uptime) }}
</template>
<!-- 最后检查时间 -->
<template #last_check_time="{ record }">
{{ formatTime(record.last_check_time) }}
</template>
<!-- 标签 -->
<template #tags="{ record }">
<a-tag
v-for="(tag, index) in parseTags(record.tags)"
:key="index"
style="margin-right: 4px"
>
{{ tag }}
</a-tag>
<span v-if="!parseTags(record.tags).length">-</span>
</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 type="text" size="small" @click="handleDetail(record)">
详情
</a-button>
<a-button type="text" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button
type="text"
size="small"
status="danger"
@click="handleDelete(record)"
>
删除
</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>
@@ -190,203 +128,58 @@
@success="handleFormSuccess"
/>
<!-- 快捷配置对话框 -->
<QuickConfigDialog
v-model:visible="quickConfigVisible"
:record="currentRecord"
@success="handleFormSuccess"
<!-- 详情对话框 -->
<DetailDialog
v-model:visible="detailDialogVisible"
:record-id="currentRecordId"
/>
<!-- 指标采集对话框 -->
<CollectDialog
v-model:visible="collectDialogVisible"
:service-list="tableData"
@success="handleRefresh"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ref, reactive, computed, onMounted } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import {
IconPlus,
IconDown,
IconEdit,
IconDesktop,
IconDelete,
IconRefresh,
import {
IconPlus,
IconSync,
IconEye,
IconSettings
IconEdit,
IconDelete,
} 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 FormDialog from '../pc/components/FormDialog.vue'
import QuickConfigDialog from '../pc/components/QuickConfigDialog.vue'
import { columns as columnsConfig } from './config/columns'
import QpsChart from './components/QpsChart.vue'
import ConnChart from './components/ConnChart.vue'
import FormDialog from './components/FormDialog.vue'
import DetailDialog from './components/DetailDialog.vue'
import CollectDialog from './components/CollectDialog.vue'
import {
fetchServerList,
deleteServer,
} from '@/api/ops/server'
const router = useRouter()
// Mock 假数据
const mockServerData = [
{
id: 1,
unique_id: 'DB-2024-0001',
name: 'MySQL 主库 -01',
database: 'MySQL',
version: '8.0.32',
location: '数据中心 A-1 楼 - 机柜 01-U1',
tags: '生产,核心,主库',
ip: '192.168.1.101',
port: '3306',
remote_access: true,
agent_config: true,
cpu_info: { value: 45, total: '8核', used: '3.6核' },
memory_info: { value: 62, total: '32GB', used: '19.8GB' },
disk_info: { value: 78, total: '1TB', used: '780GB' },
data_collection: true,
status: 'online',
},
{
id: 2,
unique_id: 'DB-2024-0002',
name: 'PostgreSQL 从库 -01',
database: 'PostgreSQL',
version: '15.2',
location: '数据中心 A-1 楼 - 机柜 02-U1',
tags: '生产,从库,备份',
ip: '192.168.1.102',
port: '5432',
remote_access: true,
agent_config: true,
cpu_info: { value: 78, total: '16核', used: '12.5核' },
memory_info: { value: 85, total: '64GB', used: '54.4GB' },
disk_info: { value: 92, total: '2TB', used: '1.84TB' },
data_collection: true,
status: 'online',
},
{
id: 3,
unique_id: 'DB-2024-0003',
name: 'Redis 缓存 -01',
database: 'Redis',
version: '7.0.9',
location: '数据中心 A-2 楼 - 机柜 05-U2',
tags: '缓存,会话存储',
ip: '192.168.1.103',
port: '6379',
remote_access: false,
agent_config: false,
cpu_info: { value: 0, total: '4核', used: '0核' },
memory_info: { value: 0, total: '16GB', used: '0GB' },
disk_info: { value: 0, total: '500GB', used: '0GB' },
data_collection: false,
status: 'offline',
},
{
id: 4,
unique_id: 'DB-2024-0004',
name: 'MongoDB 集群 -01',
database: 'MongoDB',
version: '6.0.5',
location: '数据中心 A-2 楼 - 机柜 06-U1',
tags: '文档存储,日志',
ip: '192.168.1.104',
port: '27017',
remote_access: true,
agent_config: true,
cpu_info: { value: 35, total: '8核', used: '2.8核' },
memory_info: { value: 68, total: '32GB', used: '21.8GB' },
disk_info: { value: 42, total: '1TB', used: '420GB' },
data_collection: true,
status: 'online',
},
{
id: 5,
unique_id: 'DB-2024-0005',
name: 'Oracle 数据库 -01',
database: 'Oracle',
version: '19c',
location: '数据中心 B-1 楼 - 机柜 03-U1',
tags: '财务系统,核心',
ip: '192.168.2.101',
port: '1521',
remote_access: true,
agent_config: true,
cpu_info: { value: 28, total: '12核', used: '3.4核' },
memory_info: { value: 45, total: '48GB', used: '21.6GB' },
disk_info: { value: 88, total: '10TB', used: '8.8TB' },
data_collection: true,
status: 'maintenance',
},
{
id: 6,
unique_id: 'DB-2024-0006',
name: 'SQL Server-01',
database: 'SQL Server',
version: '2019',
location: '数据中心 B-2 楼 - 机柜 10-U1',
tags: 'OA 系统,报表',
ip: '192.168.2.102',
port: '1433',
remote_access: false,
agent_config: false,
cpu_info: { value: 0, total: '4核', used: '0核' },
memory_info: { value: 0, total: '8GB', used: '0GB' },
disk_info: { value: 0, total: '256GB', used: '0GB' },
data_collection: false,
status: 'retired',
},
{
id: 7,
unique_id: 'DB-2024-0007',
name: 'Elasticsearch-01',
database: 'Elasticsearch',
version: '8.7.0',
location: '数据中心 A-1 楼 - 机柜 08-U1',
tags: '搜索,日志分析',
ip: '192.168.1.105',
port: '9200',
remote_access: true,
agent_config: true,
cpu_info: { value: 55, total: '8核', used: '4.4核' },
memory_info: { value: 72, total: '32GB', used: '23.0GB' },
disk_info: { value: 65, total: '1TB', used: '650GB' },
data_collection: true,
status: 'online',
},
{
id: 8,
unique_id: 'DB-2024-0008',
name: 'MySQL 测试库 -01',
database: 'MySQL',
version: '8.0.30',
location: '数据中心 B-1 楼 - 机柜 04-U1',
tags: '测试,开发',
ip: '192.168.2.103',
port: '3307',
remote_access: true,
agent_config: true,
cpu_info: { value: 68, total: '8核', used: '5.4核' },
memory_info: { value: 75, total: '16GB', used: '12GB' },
disk_info: { value: 55, total: '500GB', used: '275GB' },
data_collection: true,
status: 'online',
},
]
fetchDatabaseList,
deleteDatabase,
type DatabaseService,
type DatabaseQueryParams,
} from '@/api/ops/database'
// 状态管理
const loading = ref(false)
const tableData = ref<any[]>([])
const tableData = ref<DatabaseService[]>([])
const formDialogVisible = ref(false)
const quickConfigVisible = ref(false)
const currentRecord = ref<any>(null)
const detailDialogVisible = ref(false)
const collectDialogVisible = ref(false)
const currentRecord = ref<DatabaseService | null>(null)
const currentRecordId = ref(0)
const formModel = ref({
keyword: '',
datacenter_id: undefined,
rack_id: undefined,
status: undefined,
type: undefined as string | undefined,
enabled: undefined as boolean | undefined,
status: undefined as string | undefined,
})
const pagination = reactive({
@@ -401,67 +194,115 @@ const formItems = computed<FormItem[]>(() => searchFormConfig)
// 表格列配置
const columns = computed(() => columnsConfig)
// 获取数据库类型颜色
const getTypeColor = (type: string) => {
const colorMap: Record<string, string> = {
MySQL: 'blue',
PostgreSQL: 'purple',
Redis: 'red',
MongoDB: 'green',
DM: 'orange',
KingBase: 'cyan',
}
return colorMap[type] || 'gray'
}
// 获取状态颜色
const getStatusColor = (status?: string) => {
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
online: 'green',
offline: 'red',
maintenance: 'orange',
retired: 'gray',
error: 'orange',
}
return colorMap[status || ''] || 'gray'
return colorMap[status] || 'gray'
}
// 获取状态文本
const getStatusText = (status?: string) => {
const getStatusText = (status: string) => {
const textMap: Record<string, string> = {
online: '在线',
offline: '离线',
maintenance: '维护中',
retired: '已退役',
error: '错误',
}
return textMap[status || ''] || '-'
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 getResponseTimeColor = (responseTime: number) => {
if (!responseTime) return ''
if (responseTime >= 1000) return 'rgb(var(--danger-6))'
if (responseTime >= 500) return 'rgb(var(--warning-6))'
return 'rgb(var(--success-6))'
}
// 获取数据库列表(使用 Mock 数据)
const fetchServers = async () => {
// 格式化运行时长
const formatUptime = (uptime: number) => {
if (!uptime) return '-'
const days = Math.floor(uptime / 86400)
const hours = Math.floor((uptime % 86400) / 3600)
const minutes = Math.floor((uptime % 3600) / 60)
if (days > 0) {
return `${days}${hours}小时`
}
if (hours > 0) {
return `${hours}小时 ${minutes}分钟`
}
return `${minutes}分钟`
}
// 格式化时间
const formatTime = (time: string) => {
if (!time) return '-'
const date = new Date(time)
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}`
}
// 解析标签
const parseTags = (tags: string) => {
if (!tags) return []
return tags.split(',').filter((t) => t.trim())
}
// 获取数据库列表
const fetchList = async () => {
loading.value = true
try {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500))
// 使用 Mock 数据
tableData.value = mockServerData
pagination.total = mockServerData.length
// 如果有搜索条件,进行过滤
if (formModel.value.keyword || formModel.value.status) {
let filteredData = [...mockServerData]
if (formModel.value.keyword) {
const keyword = formModel.value.keyword.toLowerCase()
filteredData = filteredData.filter(item =>
item.name.toLowerCase().includes(keyword) ||
item.unique_id.toLowerCase().includes(keyword) ||
item.ip.toLowerCase().includes(keyword)
)
}
if (formModel.value.status) {
filteredData = filteredData.filter(item => item.status === formModel.value.status)
}
tableData.value = filteredData
pagination.total = filteredData.length
const params: DatabaseQueryParams = {
page: pagination.current,
size: pagination.pageSize,
}
if (formModel.value.keyword) {
params.keyword = formModel.value.keyword
}
if (formModel.value.enabled !== undefined) {
params.enabled = formModel.value.enabled
}
const res: any = await fetchDatabaseList(params)
tableData.value = res?.details?.data || []
pagination.total = res?.details?.total || 0
// 前端过滤数据库类型API不支持type过滤
if (formModel.value.type) {
tableData.value = tableData.value.filter(
(item) => item.type === formModel.value.type
)
pagination.total = tableData.value.length
}
// 前端过滤运行状态API不支持status过滤
if (formModel.value.status) {
tableData.value = tableData.value.filter(
(item) => item.status === formModel.value.status
)
pagination.total = tableData.value.length
}
} catch (error) {
console.error('获取数据库列表失败:', error)
@@ -476,7 +317,7 @@ const fetchServers = async () => {
// 搜索
const handleSearch = () => {
pagination.current = 1
fetchServers()
fetchList()
}
// 处理表单模型更新
@@ -488,23 +329,23 @@ const handleFormModelUpdate = (value: any) => {
const handleReset = () => {
formModel.value = {
keyword: '',
datacenter_id: undefined,
rack_id: undefined,
type: undefined,
enabled: undefined,
status: undefined,
}
pagination.current = 1
fetchServers()
fetchList()
}
// 分页变化
const handlePageChange = (current: number) => {
pagination.current = current
fetchServers()
fetchList()
}
// 刷新
const handleRefresh = () => {
fetchServers()
fetchList()
Message.success('数据已刷新')
}
@@ -514,91 +355,59 @@ const handleAdd = () => {
formDialogVisible.value = true
}
// 快捷配置
const handleQuickConfig = (record: any) => {
currentRecord.value = record
quickConfigVisible.value = true
// 查看详情
const handleDetail = (record: DatabaseService) => {
currentRecordId.value = record.id
detailDialogVisible.value = true
}
// 编辑数据库
const handleEdit = (record: any) => {
const handleEdit = (record: DatabaseService) => {
currentRecord.value = record
formDialogVisible.value = true
}
// 指标采集
const handleCollect = () => {
collectDialogVisible.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.ip,
status: record.status,
},
})
}
// 远程控制 - 在新窗口打开
const handleRemoteControl = (record: any) => {
const url = router.resolve({
path: '/dc/remote',
query: {
id: record.id,
name: record.name,
ip: record.ip,
status: record.status,
},
}).href
window.open(url, '_blank')
fetchList()
}
// 删除数据库
const handleDelete = async (record: any) => {
try {
Modal.confirm({
title: '确认删除',
content: `确认删除数据库 ${record.name} 吗?`,
onOk: async () => {
// Mock 删除操作
const index = mockServerData.findIndex(item => item.id === record.id)
if (index > -1) {
mockServerData.splice(index, 1)
Message.success('删除成功')
fetchServers()
const handleDelete = (record: DatabaseService) => {
Modal.confirm({
title: '确认删除',
content: `确认删除数据库服务「${record.name}」吗?此操作为软删除。`,
onOk: async () => {
try {
await deleteDatabase(record.id)
Message.success('删除成功')
fetchList()
} catch (error: any) {
console.error('删除数据库失败:', error)
if (error?.response?.data?.message) {
Message.error(error.response.data.message)
} else {
Message.error('删除失败')
}
},
})
} catch (error) {
console.error('删除数据库失败:', error)
}
}
},
})
}
// 初始化加载数据
fetchServers()
onMounted(() => {
fetchList()
})
</script>
<script lang="ts">
export default {
name: 'DataCenterServer',
name: 'DatabaseServiceManage',
}
</script>
@@ -606,55 +415,4 @@ export default {
.container {
margin-top: 20px;
}
.not-configured {
color: rgb(var(--text-3));
font-size: 12px;
text-align: center;
padding: 8px 0;
}
.sys-indicator-display {
display: flex;
flex-direction: column;
gap: 8px;
}
.resource-display {
display: flex;
flex-direction: column;
gap: 4px;
padding: 2px 0;
.resource-info {
display: flex;
align-items: center;
justify-content: space-between;
.resource-label {
font-size: 12px;
color: rgb(var(--text-2));
}
.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>

File diff suppressed because it is too large Load Diff

View File

@@ -11,93 +11,110 @@
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="unique_id" label="唯一标识">
<a-form-item field="service_identity" label="服务唯一标识">
<a-input
v-model="formData.unique_id"
placeholder="输入为空系统自动生成 UUID"
v-model="formData.service_identity"
placeholder="输入为空系统自动生成 ULID"
:disabled="isEdit"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="name" label="中间件名称">
<a-input v-model="formData.name" placeholder="请输入中间件名称" />
<a-form-item field="name" label="服务名称">
<a-input v-model="formData.name" placeholder="请输入服务名称" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="middleware" label="中间件类型">
<a-select v-model="formData.middleware" placeholder="请选择中间件类型">
<a-form-item field="server_identity" label="服务器标识">
<a-input v-model="formData.server_identity" placeholder="关联服务器唯一标识" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="type" label="中间件类型">
<a-select v-model="formData.type" placeholder="请选择中间件类型">
<a-option value="nginx">Nginx</a-option>
<a-option value="apache">Apache</a-option>
<a-option value="tomcat">Tomcat</a-option>
<a-option value="redis">Redis</a-option>
<a-option value="kafka">Kafka</a-option>
<a-option value="rabbitmq">RabbitMQ</a-option>
<a-option value="elasticsearch">Elasticsearch</a-option>
<a-option value="other">其它</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="version" label="版本">
<a-input v-model="formData.version" placeholder="请输入版本号" />
</a-form-item>
</a-col>
</a-row>
<a-form-item field="location" label="位置信息">
<a-input
v-model="formData.location"
placeholder="请输入位置信息"
/>
<a-form-item field="description" label="描述信息">
<a-textarea
v-model="formData.description"
placeholder="请输入描述信息"
:rows="2"
/>
</a-form-item>
<a-form-item field="tags" label="标签">
<a-input v-model="formData.tags" placeholder="多个标签,逗号间隔" />
</a-form-item>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="ip" label="IP地址">
<a-input v-model="formData.ip" placeholder="可以输入多个IP逗号做分隔" />
<a-form-item field="status_url" label="状态检查URL">
<a-input v-model="formData.status_url" placeholder="Nginx/Apache状态检查URL" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="remote_port" label="远程访问端口">
<a-input v-model="formData.remote_port" placeholder="为空则不可远程访问" />
<a-form-item field="agent_config" label="Agent配置URL">
<a-input v-model="formData.agent_config" placeholder="完整 http(s) GET URL" />
</a-form-item>
</a-col>
</a-row>
<a-form-item field="agent_url" label="Agent URL配置">
<a-input v-model="formData.agent_url" placeholder="请输入Agent URL配置" />
</a-form-item>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="data_collection" label="数据采集">
<a-switch v-model="formData.data_collection" />
<a-form-item field="enabled" label="启用监控">
<a-switch v-model="formData.enabled" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item v-if="formData.data_collection" field="collection_interval" label="采集时间">
<a-select v-model="formData.collection_interval" placeholder="请选择采集时间">
<a-option :value="1">1分钟</a-option>
<a-option :value="5">5分钟</a-option>
<a-option :value="10">10分钟</a-option>
<a-option :value="30">30分钟</a-option>
</a-select>
<a-form-item field="interval" label="采集间隔(秒)">
<a-input-number
v-model="formData.interval"
placeholder="默认60秒"
:min="10"
:max="3600"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item field="remark" label="备注信息">
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="collect_on" label="参与周期采集">
<a-switch v-model="formData.collect_on" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item v-if="formData.collect_on" field="collect_interval" label="采集间隔(秒)">
<a-input-number
v-model="formData.collect_interval"
placeholder="为0时使用interval"
:min="0"
:max="3600"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item field="extra" label="额外配置(JSON)">
<a-textarea
v-model="formData.remark"
placeholder="请输入备注信息"
:rows="4"
v-model="formData.extra"
placeholder='JSON格式{"config_path":"/etc/nginx/nginx.conf"}'
:rows="2"
/>
</a-form-item>
</a-form>
@@ -107,8 +124,13 @@
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { v4 as uuidv4 } from 'uuid'
import type { FormInstance } from '@arco-design/web-vue'
import {
createMiddleware,
updateMiddleware,
type MiddlewareCreateData,
type MiddlewareUpdateData,
} from '@/api/ops/middleware'
interface Props {
visible: boolean
@@ -123,86 +145,131 @@ const emit = defineEmits(['update:visible', 'success'])
const formRef = ref<FormInstance>()
const confirmLoading = ref(false)
const selectedLocation = ref('')
const isEdit = computed(() => !!props.record?.id)
const formData = reactive({
unique_id: '',
service_identity: '',
server_identity: '',
name: '',
middleware: '',
version: '',
location: '',
category: 'middleware',
type: '',
description: '',
enabled: true,
interval: 60,
extra: '',
tags: '',
ip: '',
remote_port: '',
agent_url: '',
data_collection: false,
collection_interval: 5,
remark: '',
status_url: '',
agent_config: '',
collect_on: true,
collect_args: '',
collect_interval: 60,
})
const rules = {
name: [{ required: true, message: '请输入中间件名称' }],
middleware: [{ required: true, message: '请选择中间件类型' }],
version: [{ required: true, message: '请输入版本号' }],
name: [{ required: true, message: '请输入服务名称' }],
type: [{ required: true, message: '请选择中间件类型' }],
category: [{ required: true, message: '分类必须为middleware' }],
}
const locationOptions = ref([
{ label: 'A数据中心-3层-24机柜-5U位', value: 'A数据中心-3层-24机柜-5U位' },
{ label: 'A数据中心-3层-24机柜-6U位', value: 'A数据中心-3层-24机柜-6U位' },
{ label: 'B数据中心-1层-12机柜-1U位', value: 'B数据中心-1层-12机柜-1U位' },
{ label: 'B数据中心-1层-12机柜-2U位', value: 'B数据中心-1层-12机柜-2U位' },
{ label: 'C数据中心-2层-8机柜-3U位', value: 'C数据中心-2层-8机柜-3U位' },
])
watch(
() => props.visible,
(val) => {
if (val) {
if (isEdit.value && props.record) {
Object.assign(formData, props.record)
Object.assign(formData, {
service_identity: props.record.service_identity || '',
server_identity: props.record.server_identity || '',
name: props.record.name || '',
category: props.record.category || 'middleware',
type: props.record.type || '',
description: props.record.description || '',
enabled: props.record.enabled ?? true,
interval: props.record.interval || 60,
extra: props.record.extra || '',
tags: props.record.tags || '',
status_url: props.record.status_url || '',
agent_config: props.record.agent_config || '',
collect_on: props.record.collect_on ?? true,
collect_args: props.record.collect_args || '',
collect_interval: props.record.collect_interval || 60,
})
} else {
Object.assign(formData, {
unique_id: '',
service_identity: '',
server_identity: '',
name: '',
middleware: '',
version: '',
location: '',
category: 'middleware',
type: '',
description: '',
enabled: true,
interval: 60,
extra: '',
tags: '',
ip: '',
remote_port: '',
agent_url: '',
data_collection: false,
collection_interval: 5,
remark: '',
status_url: '',
agent_config: '',
collect_on: true,
collect_args: '',
collect_interval: 60,
})
}
}
}
)
const handleLocationSelect = (value: string) => {
formData.location = value
}
const handleOk = async () => {
try {
await formRef.value?.validate()
if (!formData.unique_id) {
formData.unique_id = uuidv4()
}
confirmLoading.value = true
await new Promise(resolve => setTimeout(resolve, 1000))
if (isEdit.value) {
const updateData: MiddlewareUpdateData = {
service_identity: formData.service_identity,
server_identity: formData.server_identity,
name: formData.name,
category: formData.category,
type: formData.type,
description: formData.description,
enabled: formData.enabled,
interval: formData.interval,
extra: formData.extra,
tags: formData.tags,
status_url: formData.status_url,
agent_config: formData.agent_config,
collect_on: formData.collect_on,
collect_args: formData.collect_args,
collect_interval: formData.collect_interval,
}
await updateMiddleware(props.record.id, updateData)
Message.success('更新成功')
} else {
const createData: MiddlewareCreateData = {
service_identity: formData.service_identity,
server_identity: formData.server_identity,
name: formData.name,
category: formData.category,
type: formData.type,
description: formData.description,
enabled: formData.enabled,
interval: formData.interval,
extra: formData.extra,
tags: formData.tags,
status_url: formData.status_url,
agent_config: formData.agent_config,
collect_on: formData.collect_on,
collect_args: formData.collect_args,
collect_interval: formData.collect_interval,
}
await createMiddleware(createData)
Message.success('创建成功')
}
Message.success(isEdit.value ? '更新成功' : '创建成功')
emit('success')
handleCancel()
} catch (error) {
console.error('验证失败:', error)
console.error('操作失败:', error)
Message.error('操作失败')
} finally {
confirmLoading.value = false
}

View File

@@ -8,27 +8,34 @@
@cancel="handleCancel"
>
<a-form :model="form" layout="vertical">
<a-form-item label="远程访问端口">
<a-form-item label="Agent配置URL">
<a-input
v-model="form.agent_config"
placeholder="请输入完整 http(s) GET URL"
allow-clear
/>
<template #extra>
<span style="color: #86909c">完整 http(s) GET URL用于周期采集</span>
</template>
</a-form-item>
<a-form-item label="参与周期采集">
<a-switch v-model="form.collect_on" />
</a-form-item>
<a-form-item v-if="form.collect_on" label="采集间隔(秒)">
<a-input-number
v-model="form.remote_port"
placeholder="请输入远程访问端口,为空则不可远程访问"
:min="1"
:max="65535"
v-model="form.collect_interval"
placeholder="为0时使用interval"
:min="0"
:max="3600"
style="width: 100%"
allow-clear
/>
<template #extra>
<span style="color: #86909c">为空则不可远程访问</span>
<span style="color: #86909c">分桶间隔 0 时回退 interval</span>
</template>
</a-form-item>
<a-form-item label="Agent URL配置">
<a-input
v-model="form.agent_url"
placeholder="请输入Agent URL"
allow-clear
/>
</a-form-item>
</a-form>
</a-modal>
</template>
@@ -36,6 +43,10 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
patchMiddlewareCollect,
type MiddlewareCollectData,
} from '@/api/ops/middleware'
interface Props {
visible: boolean
@@ -48,16 +59,18 @@ const emit = defineEmits(['update:visible', 'success'])
const loading = ref(false)
const form = ref({
remote_port: undefined as number | undefined,
agent_url: '',
agent_config: '',
collect_on: true,
collect_interval: 60,
})
watch(
() => props.visible,
(val) => {
if (val && props.record) {
form.value.remote_port = props.record.remote_port
form.value.agent_url = props.record.agent_url || ''
form.value.agent_config = props.record.agent_config || ''
form.value.collect_on = props.record.collect_on ?? true
form.value.collect_interval = props.record.collect_interval || 60
}
}
)
@@ -65,14 +78,12 @@ watch(
const handleSubmit = async () => {
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const data: MiddlewareCollectData = {
collect_on: form.value.collect_on,
collect_interval: form.value.collect_interval,
}
// 更新记录
props.record.remote_port = form.value.remote_port
props.record.agent_url = form.value.agent_url
props.record.remote_access = !!form.value.remote_port
props.record.agent_config = !!form.value.agent_url
await patchMiddlewareCollect(props.record.id, data)
Message.success('配置成功')
emit('success')

View File

@@ -6,51 +6,42 @@ export const columns = [
slotName: 'id',
},
{
dataIndex: 'unique_id',
title: '唯一标识',
dataIndex: 'service_identity',
title: '服务标识',
width: 150,
},
{
dataIndex: 'name',
title: '名称',
title: '服务名称',
width: 150,
},
{
dataIndex: 'middleware',
title: '中间件',
dataIndex: 'type',
title: '中间件类型',
width: 120,
},
{
dataIndex: 'version',
title: '版本',
dataIndex: 'server_identity',
title: '服务器标识',
width: 150,
},
{
dataIndex: 'location',
title: '位置信息',
width: 150,
dataIndex: 'enabled',
title: '启用状态',
width: 100,
slotName: 'enabled',
},
{
dataIndex: 'tags',
title: '标签',
width: 120,
dataIndex: 'agent_config',
title: 'Agent配置',
width: 100,
slotName: 'agent_config',
},
{
dataIndex: 'ip',
title: 'IP地址',
width: 150,
},
{
dataIndex: 'sys_indicator',
title: '系统指标',
width: 150,
slotName: 'cpu',
},
{
dataIndex: 'middleware_indicator',
title: '中间件指标',
width: 150,
slotName: 'middleware_indicator',
dataIndex: 'collect_on',
title: '数据采集',
width: 100,
slotName: 'data_collection',
},
{
dataIndex: 'status',
@@ -58,6 +49,22 @@ export const columns = [
width: 100,
slotName: 'status',
},
{
dataIndex: 'response_time',
title: '响应时间(ms)',
width: 120,
},
{
dataIndex: 'uptime',
title: '运行时长',
width: 120,
slotName: 'uptime',
},
{
dataIndex: 'last_check_time',
title: '最后检查时间',
width: 180,
},
{
dataIndex: 'actions',
title: '操作',

View File

@@ -5,35 +5,45 @@ export const searchFormConfig: FormItem[] = [
field: 'keyword',
label: '关键词',
type: 'input',
placeholder: '请输入服务名称、编码或IP',
placeholder: '请输入服务名称或标识',
span: 6,
},
{
field: 'datacenter_id',
label: '数据中心',
field: 'type',
label: '中间件类型',
type: 'select',
placeholder: '请选择数据中心',
options: [], // 需要动态加载
placeholder: '请选择中间件类型',
options: [
{ label: 'Nginx', value: 'nginx' },
{ label: 'Apache', value: 'apache' },
{ label: 'Tomcat', value: 'tomcat' },
{ label: 'Redis', value: 'redis' },
{ label: 'Kafka', value: 'kafka' },
{ label: 'RabbitMQ', value: 'rabbitmq' },
{ label: 'Elasticsearch', value: 'elasticsearch' },
],
span: 6,
},
{
field: 'rack_id',
label: '机柜',
field: 'enabled',
label: '启用状态',
type: 'select',
placeholder: '请选择机柜',
options: [], // 需要动态加载
placeholder: '请选择启用状态',
options: [
{ label: '已启用', value: true },
{ label: '已禁用', value: false },
],
span: 6,
},
{
field: 'status',
label: '状态',
label: '运行状态',
type: 'select',
placeholder: '请选择状态',
placeholder: '请选择运行状态',
options: [
{ label: '在线', value: 'online' },
{ label: '离线', value: 'offline' },
{ label: '维护中', value: 'maintenance' },
{ label: '已退役', value: 'retired' },
{ label: '错误', value: 'error' },
],
span: 6,
},

View File

@@ -30,10 +30,10 @@
{{ record.id }}
</template>
<!-- 远程访问 -->
<template #remote_access="{ record }">
<a-tag :color="record.remote_access ? 'green' : 'gray'">
{{ record.remote_access ? '已启' : '未开启' }}
<!-- 启用状态 -->
<template #enabled="{ record }">
<a-tag :color="record.enabled ? 'green' : 'gray'">
{{ record.enabled ? '已启' : '已禁用' }}
</a-tag>
</template>
@@ -43,46 +43,19 @@
{{ record.agent_config ? '已配置' : '未配置' }}
</a-tag>
</template>
<!-- 系统指标 -->
<template #sys_indicator="{ 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 #middleware_indicator="{ 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 #data_collection="{ record }">
<a-tag :color="record.data_collection ? 'green' : 'gray'">
{{ record.data_collection ? '已启用' : '未启用' }}
<a-tag :color="record.collect_on ? 'green' : 'gray'">
{{ record.collect_on ? '已启用' : '未启用' }}
</a-tag>
</template>
<!-- 运行时长 -->
<template #uptime="{ record }">
{{ formatUptime(record.uptime) }}
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
@@ -110,12 +83,6 @@
<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 />
@@ -128,12 +95,12 @@
</template>
编辑
</a-doption>
<a-doption @click="handleRemoteControl(record)">
<!-- <a-doption @click="handleQuickConfig(record)">
<template #icon>
<icon-desktop />
<icon-settings />
</template>
远程控制
</a-doption>
采集配置
</a-doption> -->
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
<template #icon>
<icon-delete />
@@ -159,20 +126,35 @@
:record="currentRecord"
@success="handleFormSuccess"
/>
<!-- 详情抽屉 -->
<a-drawer
v-model:visible="detailVisible"
:width="800"
title="中间件详情"
:footer="false"
unmount-on-close
>
<Detail
v-if="currentRecord"
:record="currentRecord"
@edit="handleDetailEdit"
@quick-config="handleDetailQuickConfig"
@delete="handleDetailDelete"
@view-server="handleViewServer"
/>
</a-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } 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'
@@ -181,189 +163,27 @@ import SearchTable from '@/components/search-table/index.vue'
import { searchFormConfig } from './config/search-form'
import FormDialog from './components/FormDialog.vue'
import QuickConfigDialog from './components/QuickConfigDialog.vue'
import Detail from './components/Detail.vue'
import { columns as columnsConfig } from './config/columns'
import {
fetchServerList,
deleteServer,
} from '@/api/ops/server'
const router = useRouter()
// Mock 假数据
const mockServerData = [
{
id: 1,
unique_id: 'MW-2024-0001',
name: 'Nginx 服务器 -01',
middleware: 'nginx',
version: '1.24.0',
location: '数据中心 A-1 楼 -机柜 01-U1',
tags: 'Web,负载均衡',
ip: '192.168.1.101',
remote_access: true,
agent_config: true,
cpu_info: { value: 45, total: '8 核', used: '3.6 核' },
memory_info: { value: 62, total: '32GB', used: '19.8GB' },
data_collection: true,
status: 'online',
},
{
id: 2,
unique_id: 'MW-2024-0002',
name: 'Redis 缓存 -01',
middleware: 'redis',
version: '7.2.3',
location: '数据中心 A-1 楼 -机柜 02-U1',
tags: '缓存,高性能',
ip: '192.168.1.102',
remote_access: true,
agent_config: true,
cpu_info: { value: 78, total: '16 核', used: '12.5 核' },
memory_info: { value: 85, total: '64GB', used: '54.4GB' },
data_collection: true,
status: 'online',
},
{
id: 3,
unique_id: 'MW-2024-0003',
name: 'Tomcat 应用 -01',
middleware: 'tomcat',
version: '10.1.15',
location: '数据中心 A-2 楼 -机柜 05-U2',
tags: '应用Java',
ip: '192.168.1.103',
remote_access: false,
agent_config: false,
cpu_info: { value: 0, total: '4 核', used: '0 核' },
memory_info: { value: 0, total: '16GB', used: '0GB' },
data_collection: false,
status: 'offline',
},
{
id: 4,
unique_id: 'MW-2024-0004',
name: 'Kafka 消息队列 -01',
middleware: 'kafka',
version: '3.6.0',
location: '数据中心 A-2 楼 -机柜 06-U1',
tags: '消息队列,分布式',
ip: '192.168.1.104',
remote_access: true,
agent_config: true,
cpu_info: { value: 35, total: '8 核', used: '2.8 核' },
memory_info: { value: 68, total: '32GB', used: '21.8GB' },
data_collection: true,
status: 'online',
},
{
id: 5,
unique_id: 'MW-2024-0005',
name: 'Elasticsearch-01',
middleware: 'elasticsearch',
version: '8.11.1',
location: '数据中心 B-1 楼 -机柜 03-U1',
tags: '搜索,日志分析',
ip: '192.168.2.101',
remote_access: true,
agent_config: true,
cpu_info: { value: 28, total: '12 核', used: '3.4 核' },
memory_info: { value: 45, total: '48GB', used: '21.6GB' },
data_collection: true,
status: 'maintenance',
},
{
id: 6,
unique_id: 'MW-2024-0006',
name: 'RabbitMQ-01',
middleware: 'rabbitmq',
version: '3.12.10',
location: '数据中心 B-2 楼 -机柜 10-U1',
tags: '消息队列AMQP',
ip: '192.168.2.102',
remote_access: false,
agent_config: false,
cpu_info: { value: 0, total: '4 核', used: '0 核' },
memory_info: { value: 0, total: '8GB', used: '0GB' },
data_collection: false,
status: 'retired',
},
{
id: 7,
unique_id: 'MW-2024-0007',
name: 'Nginx 反向代理 -01',
middleware: 'nginx',
version: '1.25.3',
location: '数据中心 A-1 楼 -机柜 08-U1',
tags: '反向代理,负载均衡',
ip: '192.168.1.105',
remote_access: true,
agent_config: true,
cpu_info: { value: 55, total: '8 核', used: '4.4 核' },
memory_info: { value: 72, total: '32GB', used: '23.0GB' },
data_collection: true,
status: 'online',
},
{
id: 8,
unique_id: 'MW-2024-0008',
name: 'Redis 集群 -01',
middleware: 'redis',
version: '7.0.15',
location: '数据中心 B-1 楼 -机柜 04-U1',
tags: '缓存,集群',
ip: '192.168.2.103',
remote_access: true,
agent_config: true,
cpu_info: { value: 42, total: '16 核', used: '6.7 核' },
memory_info: { value: 38, total: '64GB', used: '24.3GB' },
data_collection: true,
status: 'online',
},
{
id: 9,
unique_id: 'MW-2024-0009',
name: 'Tomcat 集群 -01',
middleware: 'tomcat',
version: '9.0.83',
location: '数据中心 A-2 楼 -机柜 07-U1',
tags: '应用,集群',
ip: '192.168.1.106',
remote_access: true,
agent_config: true,
cpu_info: { value: 68, total: '8 核', used: '5.4 核' },
memory_info: { value: 75, total: '16GB', used: '12GB' },
data_collection: true,
status: 'online',
},
{
id: 10,
unique_id: 'MW-2024-0010',
name: 'Kafka 日志 -01',
middleware: 'kafka',
version: '3.5.2',
location: '数据中心 B-2 楼 -机柜 12-U1',
tags: '日志,流处理',
ip: '192.168.2.104',
remote_access: true,
agent_config: true,
cpu_info: { value: 0, total: '12 核', used: '0 核' },
memory_info: { value: 0, total: '48GB', used: '0GB' },
data_collection: true,
status: 'offline',
},
]
fetchMiddlewareList,
deleteMiddleware,
type MiddlewareItem,
type MiddlewareListParams,
} from '@/api/ops/middleware'
// 状态管理
const loading = ref(false)
const tableData = ref<any[]>([])
const tableData = ref<MiddlewareItem[]>([])
const formDialogVisible = ref(false)
const quickConfigVisible = ref(false)
const currentRecord = ref<any>(null)
const detailVisible = ref(false)
const currentRecord = ref<MiddlewareItem | null>(null)
const formModel = ref({
keyword: '',
datacenter_id: undefined,
rack_id: undefined,
status: undefined,
type: undefined as string | undefined,
enabled: undefined as boolean | undefined,
status: undefined as string | undefined,
})
const pagination = reactive({
@@ -383,8 +203,7 @@ const getStatusColor = (status?: string) => {
const colorMap: Record<string, string> = {
online: 'green',
offline: 'red',
maintenance: 'orange',
retired: 'gray',
error: 'orange',
}
return colorMap[status || ''] || 'gray'
}
@@ -394,51 +213,46 @@ const getStatusText = (status?: string) => {
const textMap: Record<string, string> = {
online: '在线',
offline: '离线',
maintenance: '维护中',
retired: '已退役',
error: '错误',
}
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 formatUptime = (uptime?: number) => {
if (!uptime) return '-'
const days = Math.floor(uptime / 86400)
const hours = Math.floor((uptime % 86400) / 3600)
const minutes = Math.floor((uptime % 3600) / 60)
if (days > 0) {
return `${days}${hours}小时`
}
if (hours > 0) {
return `${hours}小时${minutes}分钟`
}
return `${minutes}分钟`
}
// 获取中间件列表(使用 Mock 数据)
const fetchServers = async () => {
// 获取中间件列表
const fetchMiddlewareData = async () => {
loading.value = true
try {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500))
const params: MiddlewareListParams = {
page: pagination.current,
size: pagination.pageSize,
keyword: formModel.value.keyword,
enabled: formModel.value.enabled,
}
// 使用 Mock 数据
tableData.value = mockServerData
pagination.total = mockServerData.length
const response: any = await fetchMiddlewareList(params)
// 如果有搜索条件,进行过滤
if (formModel.value.keyword || formModel.value.status) {
let filteredData = [...mockServerData]
if (formModel.value.keyword) {
const keyword = formModel.value.keyword.toLowerCase()
filteredData = filteredData.filter(item =>
item.name.toLowerCase().includes(keyword) ||
item.unique_id.toLowerCase().includes(keyword) ||
item.ip.toLowerCase().includes(keyword)
)
}
if (formModel.value.status) {
filteredData = filteredData.filter(item => item.status === formModel.value.status)
}
tableData.value = filteredData
pagination.total = filteredData.length
if (response && response.details) {
tableData.value = response.details?.data
pagination.total = response.details?.total
} else {
tableData.value = []
pagination.total = 0
}
} catch (error) {
console.error('获取中间件列表失败:', error)
@@ -453,7 +267,7 @@ const fetchServers = async () => {
// 搜索
const handleSearch = () => {
pagination.current = 1
fetchServers()
fetchMiddlewareData()
}
// 处理表单模型更新
@@ -465,23 +279,23 @@ const handleFormModelUpdate = (value: any) => {
const handleReset = () => {
formModel.value = {
keyword: '',
datacenter_id: undefined,
rack_id: undefined,
type: undefined,
enabled: undefined,
status: undefined,
}
pagination.current = 1
fetchServers()
fetchMiddlewareData()
}
// 分页变化
const handlePageChange = (current: number) => {
pagination.current = current
fetchServers()
fetchMiddlewareData()
}
// 刷新
const handleRefresh = () => {
fetchServers()
fetchMiddlewareData()
Message.success('数据已刷新')
}
@@ -492,90 +306,76 @@ const handleAdd = () => {
}
// 快捷配置
const handleQuickConfig = (record: any) => {
const handleQuickConfig = (record: MiddlewareItem) => {
currentRecord.value = record
quickConfigVisible.value = true
}
// 编辑中间件
const handleEdit = (record: any) => {
const handleEdit = (record: MiddlewareItem) => {
currentRecord.value = record
formDialogVisible.value = true
}
// 表单提交成功
const handleFormSuccess = () => {
fetchServers()
// 查看详情
const handleDetail = (record: MiddlewareItem) => {
currentRecord.value = record
detailVisible.value = true
}
// 重启中间件
const handleRestart = (record: any) => {
Modal.confirm({
title: '确认重启',
content: `确认重启中间件 ${record.name} 吗?`,
onOk: () => {
Message.info('正在发送重启指令...')
},
})
// 详情页面操作
const handleDetailEdit = () => {
detailVisible.value = false
formDialogVisible.value = true
}
// 查看详情 - 在当前窗口打开
const handleDetail = (record: any) => {
router.push({
path: '/dc/detail',
query: {
id: record.id,
name: record.name,
ip: record.ip,
status: record.status,
},
})
const handleDetailQuickConfig = () => {
detailVisible.value = false
quickConfigVisible.value = true
}
// 远程控制 - 在新窗口打开
const handleRemoteControl = (record: any) => {
const url = router.resolve({
path: '/dc/remote',
query: {
id: record.id,
name: record.name,
ip: record.ip,
status: record.status,
},
}).href
window.open(url, '_blank')
}
// 删除中间件
const handleDelete = async (record: any) => {
try {
Modal.confirm({
title: '确认删除',
content: `确认删除中间件 ${record.name} 吗?`,
onOk: async () => {
// Mock 删除操作
const index = mockServerData.findIndex(item => item.id === record.id)
if (index > -1) {
mockServerData.splice(index, 1)
Message.success('删除成功')
fetchServers()
} else {
Message.error('删除失败')
}
},
})
} catch (error) {
console.error('删除中间件失败:', error)
const handleDetailDelete = () => {
detailVisible.value = false
if (currentRecord.value) {
handleDelete(currentRecord.value)
}
}
const handleViewServer = (serverIdentity: string) => {
Message.info(`查看服务器: ${serverIdentity}`)
// 可以跳转到服务器详情页面
}
// 表单提交成功
const handleFormSuccess = () => {
fetchMiddlewareData()
}
// 删除中间件
const handleDelete = (record: MiddlewareItem) => {
Modal.confirm({
title: '确认删除',
content: `确认删除中间件服务 "${record.name}" 吗?`,
onOk: async () => {
try {
await deleteMiddleware(record.id)
Message.success('删除成功')
fetchMiddlewareData()
} catch (error) {
console.error('删除中间件失败:', error)
Message.error('删除失败')
}
},
})
}
// 初始化加载数据
fetchServers()
fetchMiddlewareData()
</script>
<script lang="ts">
export default {
name: 'DataCenterServer',
name: 'MiddlewareManagement',
}
</script>
@@ -583,38 +383,4 @@ export default {
.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>

View File

@@ -4,18 +4,18 @@
:title="isEdit ? '编辑办公PC' : '新增办公PC'"
@ok="handleOk"
@cancel="handleCancel"
@update:visible="handleUpdateVisible"
:confirm-loading="confirmLoading"
:mask-closable="false"
width="800px"
>
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
<a-row :gutter="20">
<!-- 编辑时显示唯一标识只读 -->
<!-- <a-row v-if="isEdit" :gutter="20">
<a-col :span="12">
<a-form-item field="unique_id" label="唯一标识">
<a-form-item label="唯一标识">
<a-input
v-model="formData.unique_id"
placeholder="输入为空系统自动生成UUID"
:disabled="isEdit"
:model-value="formData.pc_identity"
disabled
/>
</a-form-item>
</a-col>
@@ -24,74 +24,116 @@
<a-input v-model="formData.name" placeholder="请输入办公PC名称" />
</a-form-item>
</a-col>
</a-row> -->
<!-- 新建时不显示唯一标识 -->
<a-row v-if="!isEdit" :gutter="20">
<a-col :span="24">
<a-form-item field="name" label="办公PC名称">
<a-input v-model="formData.name" placeholder="请输入办公PC名称" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="host" label="主机地址">
<a-input v-model="formData.host" placeholder="请输入主机地址" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="ip_address" label="IP地址">
<a-input v-model="formData.ip_address" placeholder="请输入IP地址" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="os" label="操作系统">
<a-select v-model="formData.os" placeholder="请选择操作系统">
<a-option value="windows">Windows</a-option>
<a-option value="linux">Linux</a-option>
<a-option value="other">其它</a-option>
<a-select v-model="formData.os" placeholder="请选择操作系统" allow-clear>
<a-option value="Windows">Windows</a-option>
<a-option value="Linux">Linux</a-option>
<a-option value="macOS">macOS</a-option>
<a-option value="Other">其它</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="tags" label="标签">
<a-input v-model="formData.tags" placeholder="多个标签,逗号间隔" />
<a-form-item field="os_version" label="系统版本">
<a-input v-model="formData.os_version" placeholder="请输入系统版本" />
</a-form-item>
</a-col>
</a-row>
<a-form-item field="location" label="位置信息">
<a-input
v-model="formData.location"
placeholder="请输入位置信息"
/>
</a-form-item>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="ip" label="IP地址">
<a-input v-model="formData.ip" placeholder="可以输入多个IP逗号做分隔" />
<a-form-item field="kernel" label="内核类型">
<a-select v-model="formData.kernel" placeholder="请选择内核类型" allow-clear>
<a-option value="x86">x86</a-option>
<a-option value="arm">ARM</a-option>
<a-option value="x64">x64</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="remote_port" label="远程访问端口">
<a-input v-model="formData.remote_port" placeholder="为空则不可远程访问" />
</a-form-item>
</a-col>
</a-row>
<a-form-item field="agent_url" label="Agent URL配置">
<a-input v-model="formData.agent_url" placeholder="请输入Agent URL配置" />
</a-form-item>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="data_collection" label="数据采集">
<a-switch v-model="formData.data_collection" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item v-if="formData.data_collection" field="collection_interval" label="采集时间">
<a-select v-model="formData.collection_interval" placeholder="请选择采集时间">
<a-option :value="1">1分钟</a-option>
<a-option :value="5">5分钟</a-option>
<a-option :value="10">10分钟</a-option>
<a-option :value="30">30分钟</a-option>
<a-form-item field="server_type" label="类型">
<a-select v-model="formData.server_type" placeholder="请选择类型" allow-clear>
<a-option value="physical">物理机</a-option>
<a-option value="virtual">虚拟机</a-option>
<a-option value="cloud">云主机</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item field="remark" label="备注信息">
<a-textarea
v-model="formData.remark"
placeholder="请输入备注信息"
:rows="4"
/>
<a-form-item field="location" label="位置">
<a-input v-model="formData.location" placeholder="请输入位置信息" />
</a-form-item>
<a-form-item field="tags" label="标签">
<a-input v-model="formData.tags" placeholder="多个标签,逗号间隔" />
</a-form-item>
<a-form-item field="description" label="描述">
<a-textarea v-model="formData.description" placeholder="请输入描述" :rows="3" />
</a-form-item>
<a-divider>远程访问配置</a-divider>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="remote_access" label="远程访问">
<a-input v-model="formData.remote_access" placeholder="请输入远程访问地址" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="remote_port" label="远程管理端口">
<a-input-number v-model="formData.remote_port" placeholder="请输入端口" :min="0" :max="65535" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-divider>Agent 配置</a-divider>
<a-form-item field="agent_config" label="Agent配置URL">
<a-input v-model="formData.agent_config" placeholder="请输入完整的 http(s) URL" />
</a-form-item>
<a-divider>采集配置</a-divider>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="collect_on" label="启用采集">
<a-switch v-model="formData.collect_on" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item v-if="formData.collect_on" field="collect_interval" label="采集间隔(秒)">
<a-input-number v-model="formData.collect_interval" placeholder="请输入采集间隔" :min="10" :max="3600" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
@@ -99,100 +141,151 @@
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { v4 as uuidv4 } from 'uuid'
import type { FormInstance } from '@arco-design/web-vue'
import { createPC, updatePC, type PCItem } from '@/api/ops/pc'
interface Props {
visible: boolean
record?: any
record?: PCItem | null
}
const props = withDefaults(defineProps<Props>(), {
record: () => ({}),
record: null,
})
const emit = defineEmits(['update:visible', 'success'])
const formRef = ref<FormInstance>()
const confirmLoading = ref(false)
const selectedLocation = ref('')
const isEdit = computed(() => !!props.record?.id)
const formData = reactive({
unique_id: '',
pc_identity: '',
name: '',
server_type: '',
host: '',
ip_address: '',
os: '',
os_version: '',
kernel: '',
server_type: '',
location: '',
tags: '',
ip: '',
remote_port: '',
agent_url: '',
data_collection: false,
collection_interval: 5,
remark: '',
description: '',
remote_access: '',
remote_port: 0,
agent_config: '',
collect_on: true,
collect_interval: 60,
})
const rules = {
name: [{ required: true, message: '请输入办公名称' }],
server_type: [{ required: true, message: '请选择办公类型' }],
os: [{ required: true, message: '请选择操作系统' }],
name: [{ required: true, message: '请输入办公PC名称' }],
host: [{ required: true, message: '请输入主机地址' }],
}
const locationOptions = ref([
{ label: 'A数据中心-3层-24机柜-5U位', value: 'A数据中心-3层-24机柜-5U位' },
{ label: 'A数据中心-3层-24机柜-6U位', value: 'A数据中心-3层-24机柜-6U位' },
{ label: 'B数据中心-1层-12机柜-1U位', value: 'B数据中心-1层-12机柜-1U位' },
{ label: 'B数据中心-1层-12机柜-2U位', value: 'B数据中心-1层-12机柜-2U位' },
{ label: 'C数据中心-2层-8机柜-3U位', value: 'C数据中心-2层-8机柜-3U位' },
])
watch(
() => props.visible,
(val) => {
if (val) {
if (isEdit.value && props.record) {
Object.assign(formData, props.record)
Object.assign(formData, {
pc_identity: props.record.pc_identity || '',
name: props.record.name || '',
host: props.record.host || '',
ip_address: props.record.ip_address || '',
os: props.record.os || '',
os_version: props.record.os_version || '',
kernel: props.record.kernel || '',
server_type: props.record.server_type || '',
location: props.record.location || '',
tags: props.record.tags || '',
description: props.record.description || '',
remote_access: props.record.remote_access || '',
remote_port: props.record.remote_port || 0,
agent_config: props.record.agent_config || '',
collect_on: props.record.collect_on ?? true,
collect_interval: props.record.collect_interval || 60,
})
} else {
Object.assign(formData, {
unique_id: '',
pc_identity: '',
name: '',
server_type: '',
host: '',
ip_address: '',
os: '',
os_version: '',
kernel: '',
server_type: '',
location: '',
tags: '',
ip: '',
remote_port: '',
agent_url: '',
data_collection: false,
collection_interval: 5,
remark: '',
description: '',
remote_access: '',
remote_port: 0,
agent_config: '',
collect_on: true,
collect_interval: 60,
})
}
}
}
)
const handleLocationSelect = (value: string) => {
formData.location = value
}
const handleOk = async () => {
try {
await formRef.value?.validate()
if (!formData.unique_id) {
formData.unique_id = uuidv4()
const vilid = await formRef.value?.validate()
if (vilid) {
return
}
confirmLoading.value = true
await new Promise(resolve => setTimeout(resolve, 1000))
// 基础提交数据
const baseData = {
name: formData.name,
host: formData.host,
ip_address: formData.ip_address,
os: formData.os,
os_version: formData.os_version,
kernel: formData.kernel,
server_type: formData.server_type,
location: formData.location,
tags: formData.tags,
description: formData.description,
remote_access: formData.remote_access,
remote_port: formData.remote_port,
agent_config: formData.agent_config,
collect_on: formData.collect_on,
collect_interval: formData.collect_interval,
}
Message.success(isEdit.value ? '更新成功' : '创建成功')
emit('success')
handleCancel()
if (isEdit.value && props.record?.id) {
// 编辑时包含 pc_identity
const submitData = {
...baseData,
pc_identity: formData.pc_identity,
}
const res: any = await updatePC(props.record.id, submitData)
if (res.code === 0) {
Message.success('更新成功')
emit('success')
handleCancel()
} else {
Message.error(res.message || '更新失败')
}
} else {
// 新建时不传 pc_identity由后端生成
const res: any = await createPC(baseData)
if (res.code === 0) {
Message.success('创建成功')
emit('success')
handleCancel()
} else {
Message.error(res.message || '创建失败')
}
}
} catch (error) {
console.error('验证失败:', error)
} finally {

View File

@@ -8,25 +8,25 @@
@cancel="handleCancel"
>
<a-form :model="form" layout="vertical">
<a-form-item label="远程访问端口">
<a-input-number
v-model="form.remote_port"
placeholder="请输入远程访问端口,为空则不可远程访问"
:min="1"
:max="65535"
style="width: 100%"
<a-form-item label="Agent配置URL">
<a-input
v-model="form.agent_config"
placeholder="请输入完整的 http(s) URL"
allow-clear
/>
<template #extra>
<span style="color: #86909c">为空则不可远程访问</span>
</template>
</a-form-item>
<a-form-item label="Agent URL配置">
<a-input
v-model="form.agent_url"
placeholder="请输入Agent URL"
allow-clear
<a-form-item label="启用采集">
<a-switch v-model="form.collect_on" />
</a-form-item>
<a-form-item v-if="form.collect_on" label="采集间隔(秒)">
<a-input-number
v-model="form.collect_interval"
placeholder="请输入采集间隔"
:min="10"
:max="3600"
style="width: 100%"
/>
</a-form-item>
</a-form>
@@ -36,10 +36,11 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { updatePC, type PCItem } from '@/api/ops/pc'
interface Props {
visible: boolean
record: any
record: PCItem | null
}
const props = defineProps<Props>()
@@ -48,35 +49,42 @@ const emit = defineEmits(['update:visible', 'success'])
const loading = ref(false)
const form = ref({
remote_port: undefined as number | undefined,
agent_url: '',
agent_config: '',
collect_on: true,
collect_interval: 60,
})
watch(
() => props.visible,
(val) => {
if (val && props.record) {
form.value.remote_port = props.record.remote_port
form.value.agent_url = props.record.agent_url || ''
form.value = {
agent_config: props.record.agent_config || '',
collect_on: props.record.collect_on ?? true,
collect_interval: props.record.collect_interval || 60,
}
}
}
)
const handleSubmit = async () => {
if (!props.record?.id) return
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
const res: any = await updatePC(props.record.id, {
agent_config: form.value.agent_config,
collect_on: form.value.collect_on,
collect_interval: form.value.collect_interval,
})
// 更新记录
props.record.remote_port = form.value.remote_port
props.record.agent_url = form.value.agent_url
props.record.remote_access = !!form.value.remote_port
props.record.agent_config = !!form.value.agent_url
Message.success('配置成功')
emit('success')
emit('update:visible', false)
if (res.code === 0) {
Message.success('配置成功')
emit('success')
emit('update:visible', false)
} else {
Message.error(res.message || '配置失败')
}
} catch (error) {
Message.error('配置失败')
} finally {
@@ -87,4 +95,4 @@ const handleSubmit = async () => {
const handleCancel = () => {
emit('update:visible', false)
}
</script>
</script>

View File

@@ -6,7 +6,7 @@ export const columns = [
slotName: 'id',
},
{
dataIndex: 'unique_id',
dataIndex: 'pc_identity',
title: '唯一标识',
width: 150,
},
@@ -15,14 +15,34 @@ export const columns = [
title: '名称',
width: 150,
},
{
dataIndex: 'host',
title: '主机地址',
width: 140,
},
{
dataIndex: 'ip_address',
title: 'IP地址',
width: 140,
},
{
dataIndex: 'os',
title: '操作系统',
width: 150,
width: 120,
},
{
dataIndex: 'os_version',
title: '系统版本',
width: 100,
},
{
dataIndex: 'server_type',
title: '类型',
width: 100,
},
{
dataIndex: 'location',
title: '位置信息',
title: '位置',
width: 150,
},
{
@@ -30,23 +50,6 @@ export const columns = [
title: '标签',
width: 120,
},
{
dataIndex: 'ip',
title: 'IP地址',
width: 150,
},
{
dataIndex: 'remote_access',
title: '远程访问',
width: 100,
slotName: 'remote_access',
},
{
dataIndex: 'agent_config',
title: 'Agent配置',
width: 150,
slotName: 'agent_config',
},
{
dataIndex: 'cpu',
title: 'CPU使用率',
@@ -65,6 +68,12 @@ export const columns = [
width: 150,
slotName: 'disk',
},
{
dataIndex: 'collect_on',
title: '采集状态',
width: 100,
slotName: 'collect_on',
},
{
dataIndex: 'status',
title: '状态',

View File

@@ -5,35 +5,18 @@ export const searchFormConfig: FormItem[] = [
field: 'keyword',
label: '关键词',
type: 'input',
placeholder: '请输入PC名称、编码或IP',
placeholder: '请输入PC名称或主机地址',
span: 6,
},
{
field: 'datacenter_id',
label: '数据中心',
field: 'collect_on',
label: '采集状态',
type: 'select',
placeholder: '请选择数据中心',
options: [], // 需要动态加载
span: 6,
},
{
field: 'rack_id',
label: '机柜',
type: 'select',
placeholder: '请选择机柜',
options: [], // 需要动态加载
span: 6,
},
{
field: 'status',
label: '状态',
type: 'select',
placeholder: '请选择状态',
placeholder: '请选择采集状态',
options: [
{ label: '在线', value: 'online' },
{ label: '离线', value: 'offline' },
{ label: '维护中', value: 'maintenance' },
{ label: '已退役', value: 'retired' },
{ label: '全部', value: '' },
{ label: '已启用', value: 'true' },
{ label: '已禁用', value: 'false' },
],
span: 6,
},

View File

@@ -30,20 +30,6 @@
{{ 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">
@@ -92,6 +78,13 @@
</div>
</template>
<!-- 采集状态 -->
<template #collect_on="{ record }">
<a-tag :color="record.collect_on ? 'green' : 'gray'">
{{ record.collect_on ? '已启用' : '已禁用' }}
</a-tag>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.status)">
@@ -102,7 +95,7 @@
<!-- 操作栏 -->
<template #actions="{ record }">
<a-space>
<a-button
<!-- <a-button
v-if="!record.agent_config"
type="outline"
size="small"
@@ -112,14 +105,14 @@
<icon-settings />
</template>
快捷配置
</a-button>
</a-button> -->
<a-dropdown trigger="hover">
<a-button type="primary" size="small">
管理
<icon-down />
</a-button>
<template #content>
<a-doption @click="handleRestart(record)">
<!-- <a-doption @click="handleRestart(record)">
<template #icon>
<icon-refresh />
</template>
@@ -130,19 +123,19 @@
<icon-eye />
</template>
详情
</a-doption>
</a-doption> -->
<a-doption @click="handleEdit(record)">
<template #icon>
<icon-edit />
</template>
编辑
</a-doption>
<a-doption @click="handleRemoteControl(record)">
<!-- <a-doption @click="handleRemoteControl(record)">
<template #icon>
<icon-desktop />
</template>
远程控制
</a-doption>
</a-doption> -->
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
<template #icon>
<icon-delete />
@@ -168,23 +161,22 @@
:record="currentRecord"
@success="handleFormSuccess"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { Message, Modal } from '@arco-design/web-vue'
import {
IconPlus,
import {
IconPlus,
IconDown,
IconEdit,
IconDesktop,
IconDelete,
IconRefresh,
IconEye,
IconSettings
IconSettings,
IconRefresh,
IconDesktop,
} from '@arco-design/web-vue/es/icon'
import type { FormItem } from '@/components/search-form/types'
import SearchTable from '@/components/search-table/index.vue'
@@ -193,150 +185,30 @@ import { columns as columnsConfig } from './config/columns'
import {
fetchPCList,
deletePC,
type PCItem,
} from '@/api/ops/pc'
import FormDialog from './components/FormDialog.vue'
import QuickConfigDialog from './components/QuickConfigDialog.vue'
import Detail from './components/Detail.vue'
import axios from 'axios'
// 创建独立的 axios 实例用于请求外部 agent绕过全局拦截器
const agentAxios = axios.create({
timeout: 5000,
})
const router = useRouter()
// Mock 假数据
const mockPCData = [
{
id: 1,
unique_id: 'PC-2024-0001',
name: '开发PC-01',
os: 'Windows 11',
location: '数据中心A-1楼-办公区01',
tags: '开发,前端',
ip: '192.168.1.201',
remote_access: true,
agent_config: true,
cpu_info: { value: 35, total: '8核', used: '2.8核' },
memory_info: { value: 52, total: '16GB', used: '8.3GB' },
disk_info: { value: 65, total: '512GB', used: '333GB' },
status: 'online',
},
{
id: 2,
unique_id: 'PC-2024-0002',
name: '测试PC-01',
os: 'Windows 10',
location: '数据中心A-1楼-办公区02',
tags: '测试,自动化',
ip: '192.168.1.202',
remote_access: true,
agent_config: true,
cpu_info: { value: 28, total: '4核', used: '1.1核' },
memory_info: { value: 45, total: '8GB', used: '3.6GB' },
disk_info: { value: 72, total: '256GB', used: '184GB' },
status: 'online',
},
{
id: 3,
unique_id: 'PC-2024-0003',
name: '设计PC-01',
os: 'macOS Sonoma',
location: '数据中心A-2楼-设计室01',
tags: '设计,创意',
ip: '192.168.1.203',
remote_access: false,
agent_config: false,
cpu_info: { value: 0, total: '12核', used: '0核' },
memory_info: { value: 0, total: '32GB', used: '0GB' },
disk_info: { value: 0, total: '1TB', used: '0GB' },
status: 'offline',
},
{
id: 4,
unique_id: 'PC-2024-0004',
name: '运维PC-01',
os: 'Windows 11',
location: '数据中心B-1楼-运维室01',
tags: '运维,监控',
ip: '192.168.2.201',
remote_access: true,
agent_config: true,
cpu_info: { value: 42, total: '6核', used: '2.5核' },
memory_info: { value: 58, total: '16GB', used: '9.3GB' },
disk_info: { value: 68, total: '512GB', used: '348GB' },
status: 'online',
},
{
id: 5,
unique_id: 'PC-2024-0005',
name: '财务PC-01',
os: 'Windows 10',
location: '数据中心B-2楼-财务室01',
tags: '财务,报表',
ip: '192.168.2.202',
remote_access: true,
agent_config: true,
cpu_info: { value: 22, total: '4核', used: '0.9核' },
memory_info: { value: 38, total: '8GB', used: '3.0GB' },
disk_info: { value: 55, total: '256GB', used: '141GB' },
status: 'online',
},
{
id: 6,
unique_id: 'PC-2024-0006',
name: '备用PC-01',
os: 'Windows 11',
location: '数据中心A-1楼-备用室01',
tags: '备用,测试',
ip: '192.168.1.204',
remote_access: false,
agent_config: false,
cpu_info: { value: 0, total: '4核', used: '0核' },
memory_info: { value: 0, total: '8GB', used: '0GB' },
disk_info: { value: 0, total: '256GB', used: '0GB' },
status: 'maintenance',
},
{
id: 7,
unique_id: 'PC-2024-0007',
name: '开发PC-02',
os: 'Ubuntu 22.04',
location: '数据中心A-1楼-办公区03',
tags: '开发,后端',
ip: '192.168.1.205',
remote_access: true,
agent_config: true,
cpu_info: { value: 48, total: '8核', used: '3.8核' },
memory_info: { value: 62, total: '16GB', used: '9.9GB' },
disk_info: { value: 58, total: '512GB', used: '297GB' },
status: 'online',
},
{
id: 8,
unique_id: 'PC-2024-0008',
name: '产品PC-01',
os: 'Windows 11',
location: '数据中心B-1楼-产品室01',
tags: '产品,设计',
ip: '192.168.2.203',
remote_access: true,
agent_config: true,
cpu_info: { value: 32, total: '6核', used: '1.9核' },
memory_info: { value: 45, total: '16GB', used: '7.2GB' },
disk_info: { value: 62, total: '512GB', used: '317GB' },
status: 'online',
},
]
// 状态管理
const loading = ref(false)
const tableData = ref<any[]>([])
const tableData = ref<PCItem[]>([])
const formModel = ref({
keyword: '',
datacenter_id: undefined,
rack_id: undefined,
status: undefined,
collect_on: undefined as boolean | undefined,
})
const dialogVisible = ref(false)
const quickConfigVisible = ref(false)
const currentRecord = ref<any>(null)
const currentRecord = ref<PCItem | null>(null)
const pagination = reactive({
current: 1,
@@ -355,8 +227,7 @@ const getStatusColor = (status?: string) => {
const colorMap: Record<string, string> = {
online: 'green',
offline: 'red',
maintenance: 'orange',
retired: 'gray',
unknown: 'gray',
}
return colorMap[status || ''] || 'gray'
}
@@ -366,8 +237,7 @@ const getStatusText = (status?: string) => {
const textMap: Record<string, string> = {
online: '在线',
offline: '离线',
maintenance: '维护中',
retired: '已退役',
unknown: '未知',
}
return textMap[status || ''] || '-'
}
@@ -380,37 +250,38 @@ const getProgressColor = (value: number) => {
return '#00B42A' // 绿色
}
// 获取PC列表(使用 Mock 数据)
// 获取PC列表
const fetchPCs = async () => {
loading.value = true
try {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500))
const params: {
page: number
size: number
keyword?: string
collect_on?: boolean
} = {
page: pagination.current,
size: pagination.pageSize,
}
// 使用 Mock 数据
tableData.value = mockPCData
pagination.total = mockPCData.length
if (formModel.value.keyword) {
params.keyword = formModel.value.keyword
}
// 如果有搜索条件,进行过滤
if (formModel.value.keyword || formModel.value.status) {
let filteredData = [...mockPCData]
if (formModel.value.collect_on !== undefined && formModel.value.collect_on !== null) {
params.collect_on = formModel.value.collect_on
}
const res: any = await fetchPCList(params)
if (res.code === 200 || res.code === 0) {
const responseData = res.details || res.data || {}
tableData.value = responseData.data || []
pagination.total = responseData.total || 0
if (formModel.value.keyword) {
const keyword = formModel.value.keyword.toLowerCase()
filteredData = filteredData.filter(item =>
item.name.toLowerCase().includes(keyword) ||
item.unique_id.toLowerCase().includes(keyword) ||
item.ip.toLowerCase().includes(keyword)
)
}
if (formModel.value.status) {
filteredData = filteredData.filter(item => item.status === formModel.value.status)
}
tableData.value = filteredData
pagination.total = filteredData.length
// 列表加载成功后,获取监控指标
await getAllMetrics()
}
} catch (error) {
console.error('获取PC列表失败:', error)
@@ -422,6 +293,75 @@ const fetchPCs = async () => {
}
}
// 获取所有PC的监控指标
const getAllMetrics = async () => {
try {
// 遍历每个PC记录
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(`PC ${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(`获取PC ${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('获取所有PC监控指标失败:', error)
}
}
// 搜索
const handleSearch = () => {
pagination.current = 1
@@ -437,9 +377,7 @@ const handleFormModelUpdate = (value: any) => {
const handleReset = () => {
formModel.value = {
keyword: '',
datacenter_id: undefined,
rack_id: undefined,
status: undefined,
collect_on: undefined,
}
pagination.current = 1
fetchPCs()
@@ -464,32 +402,32 @@ const handleAdd = () => {
}
// 快捷配置
const handleQuickConfig = (record: any) => {
const handleQuickConfig = (record: PCItem) => {
currentRecord.value = record
quickConfigVisible.value = true
}
// 编辑PC
const handleEdit = (record: any) => {
const handleEdit = (record: PCItem) => {
currentRecord.value = record
dialogVisible.value = true
}
// 详情 - 在当前窗口打开
const handleDetail = (record: any) => {
const handleDetail = (record: PCItem) => {
router.push({
path: '/dc/detail',
query: {
id: record.id,
name: record.name,
ip: record.ip,
host: record.host,
status: record.status,
},
})
}
// 重启
const handleRestart = (record: any) => {
const handleRestart = (record: PCItem) => {
Modal.confirm({
title: '确认重启',
content: `确认重启办公PC ${record.name} 吗?`,
@@ -500,13 +438,13 @@ const handleRestart = (record: any) => {
}
// 远程控制 - 在新窗口打开
const handleRemoteControl = (record: any) => {
const handleRemoteControl = (record: PCItem) => {
const url = router.resolve({
path: '/dc/remote',
query: {
id: record.id,
name: record.name,
ip: record.ip,
host: record.host,
status: record.status,
},
}).href
@@ -519,30 +457,27 @@ const handleFormSuccess = () => {
}
// 删除PC
const handleDelete = async (record: any) => {
try {
Modal.confirm({
title: '确认删除',
content: `确认删除PC ${record.name} 吗?`,
onOk: async () => {
// Mock 删除操作
const index = mockPCData.findIndex(item => item.id === record.id)
if (index > -1) {
mockPCData.splice(index, 1)
Message.success('删除成功')
fetchPCs()
} else {
Message.error('删除失败')
}
},
})
} catch (error) {
console.error('删除PC失败:', error)
}
const handleDelete = async (record: PCItem) => {
Modal.confirm({
title: '确认删除',
content: `确认删除PC ${record.name} 吗?`,
onOk: async () => {
try {
await deletePC(record.id)
Message.success('删除成功')
fetchPCs()
} catch (error) {
console.error('删除PC失败:', error)
Message.error('删除失败')
}
},
})
}
// 初始化加载数据
fetchPCs()
onMounted(() => {
fetchPCs()
})
</script>
<script lang="ts">
@@ -560,32 +495,21 @@ export default {
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;
align-items: center;
.resource-label {
font-size: 12px;
color: var(--color-text-3);
}
.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;
color: var(--color-text-1);
}
}
}

View File

@@ -1,365 +0,0 @@
# 服务器管理接口文档
## 基础信息
- **文档索引**: [README.md](README.md)
- **服务前缀**: `/DC-Control/v1/servers`
- **认证方式**: JWT认证所有接口都需要JWT Token
- **Content-Type**: `application/json`
---
## 1. 获取服务器列表
### 接口信息
- **路径**: `GET /DC-Control/v1/servers`
- **描述**: 分页获取服务器列表,支持关键词搜索、按是否启用采集(`collect_on`)过滤
### 请求参数
#### 查询参数 (Query Parameters)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| page | int | 否 | 1 | 页码(必须为正整数) |
| size | int | 否 | 20 | 每页数量必须为正整数最大100 |
| keyword | string | 否 | - | 搜索关键词(按**名称、主机地址**模糊匹配,实现见 `ListServers` |
| collect_on | bool | 否 | - | 是否启用采集过滤,仅 `true`/`false`;缺省或空串不按该条件过滤,非法值返回 400 |
### 请求示例
```http
GET /DC-Control/v1/servers?page=1&size=20&keyword=&collect_on=true
Authorization: Bearer {JWT_TOKEN}
```
### 返回参数
```json
{
"code": 200,
"message": "success",
"data": {
"total": 50,
"page": 1,
"page_size": 20,
"data": [
{
"id": 1,
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z",
"deleted_at": null,
"server_identity": "server-001",
"name": "生产服务器-001",
"host": "192.168.1.100",
"ip_address": "192.168.1.100",
"description": "生产环境主服务器",
"os": "Linux",
"os_version": "22.04",
"kernel": "x86",
"server_type": "physical",
"tags": "production,web",
"location": "机房A-机架01",
"remote_access": "ssh://user@host",
"agent_config": "",
"status": "online",
"last_check_time": "2024-01-01T10:00:00Z",
"collect_on": true,
"collect_args": "",
"collect_interval": 60,
"collect_last_result": ""
}
]
}
}
```
### 返回字段说明
| 字段名 | 类型 | 说明 |
|--------|------|------|
| total | int64 | 总记录数 |
| page | int | 当前页码 |
| page_size | int | 每页数量 |
| data | array | 服务器列表 |
| data[].id | uint | 服务器ID |
| data[].created_at | string | 创建时间 |
| data[].updated_at | string | 更新时间 |
| data[].deleted_at | string\|null | 删除时间(软删除) |
| data[].server_identity | string | 服务器唯一标识(唯一索引) |
| data[].name | string | 服务器名称 |
| data[].host | string | 服务器地址 |
| data[].ip_address | string | IP地址 |
| data[].description | string | 描述信息 |
| data[].os | string | 操作系统Windows/Linux/Mac/Other 等 |
| data[].os_version | string | 操作系统版本 |
| data[].kernel | string | 内核类型x86/arm 等 |
| data[].server_type | string | 服务器类型physical物理/virtual虚拟/cloud |
| data[].tags | string | 标签,逗号分隔的字符串(如 `prod,web`),可为空 |
| data[].location | string | 位置/机房信息 |
| data[].remote_access | string | 远程访问信息 |
| data[].agent_config | string | Agent 配置 |
| data[].status | string | 状态online在线/offline离线/unknown未知 |
| data[].last_check_time | string | 最后检查时间 |
| data[].collect_on | bool | 是否启用采集 |
| data[].collect_args | string | 采集参数 |
| data[].collect_interval | int | 采集间隔(秒),默认 60 |
| data[].collect_last_result | string | 采集最后结果 |
---
## 2. 获取服务器详情
### 接口信息
- **路径**: `GET /DC-Control/v1/servers/:id`
- **描述**: 根据ID获取单个服务器的详细信息
### 请求参数
#### 路径参数 (Path Parameters)
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|--------|
| id | uint | 是 | 服务器ID必须大于0 |
### 请求示例
```http
GET /DC-Control/v1/servers/1
Authorization: Bearer {JWT_TOKEN}
```
### 返回参数
返回单体字段与列表项一致(见上一节 `data[]` 字段说明),无嵌套 `collectors` 字段;采集器与服务器通过采集器表 `server_id` 关联,需通过采集器管理接口查询。
---
## 3. 创建服务器
### 接口信息
- **路径**: `POST /DC-Control/v1/servers`
- **描述**: 创建新的服务器记录
### 请求参数
#### 请求体 (Request Body)
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|--------|
| server_identity | string | 否 | 服务器唯一标识;**不传或空串时服务端生成 ULID** |
| name | string | 是 | 服务器名称最大100字符 |
| host | string | 是 | 服务器地址最大255字符 |
| ip_address | string | 否 | IP地址最大50字符 |
| description | string | 否 | 描述信息 |
| os | string | 否 | 操作系统类型 |
| os_version | string | 否 | 操作系统版本 |
| kernel | string | 否 | 内核类型 |
| server_type | string | 否 | 服务器类型physical/virtual/cloud最大50字符 |
| tags | string | 否 | 标签,逗号分隔(如 `env,role,tier`),最大约 500 字符,可为空 |
| location | string | 否 | 位置/机房信息最大200字符 |
| remote_access | string | 否 | 远程访问 |
| agent_config | string | 否 | Agent 配置 |
| status | string | 否 | 状态online/offline/unknown默认由库表 default 为 unknown |
| last_check_time | string (RFC3339) | 否 | 最后检查时间 |
| collect_on | bool | 否 | 是否启用采集,默认 true |
| collect_args | string | 否 | 采集参数 |
| collect_interval | int | 否 | 采集间隔(秒),默认 60 |
| collect_last_result | string | 否 | 采集最后结果 |
### 请求示例
```http
POST /DC-Control/v1/servers
Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json
{
"server_identity": "server-001",
"name": "-001",
"host": "192.168.1.100",
"ip_address": "192.168.1.100",
"description": "",
"server_type": "physical",
"tags": "production,web,tier1",
"location": "A-01",
"remote_access": "ssh://ops@192.168.1.100",
"status": "online",
"collect_on": true,
"collect_interval": 60
}
```
### 返回参数
```json
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z",
"deleted_at": null,
"server_identity": "server-001",
"name": "生产服务器-001",
"host": "192.168.1.100",
"ip_address": "192.168.1.100",
"description": "生产环境主服务器",
"os": "",
"os_version": "",
"kernel": "",
"server_type": "physical",
"tags": "production,web,tier1",
"location": "机房A-机架01",
"remote_access": "ssh://ops@192.168.1.100",
"agent_config": "",
"status": "online",
"last_check_time": "0001-01-01T00:00:00Z",
"collect_on": true,
"collect_args": "",
"collect_interval": 60,
"collect_last_result": ""
}
}
```
> 创建接口直接返回内存中的 `server` 对象。采集器与服务器的关联请在**采集器管理**接口中维护(如设置采集器的 `server_id`)。
### 错误响应
当达到许可证限制时,会返回以下错误:
```json
{
"code": 400,
"message": "已达到许可证允许的最大服务器数量限制({max_server}),无法创建更多服务器",
"data": null
}
```
---
## 4. 更新服务器
### 接口信息
- **路径**: `PUT /DC-Control/v1/servers/:id`
- **描述**: 更新服务器信息
### 请求参数
#### 路径参数 (Path Parameters)
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|--------|
| id | uint | 是 | 服务器ID必须大于0 |
#### 请求体 (Request Body)
| 字段名 | 类型 | 必填 | 说明 |
|--------|------|------|--------|
| server_identity | string | 否 | 服务器唯一标识 |
| name | string | 否 | 服务器名称 |
| host | string | 否 | 服务器地址 |
| ip_address | string | 否 | IP地址 |
| description | string | 否 | 描述信息 |
| server_type | string | 否 | 服务器类型 |
| tags | string | 否 | 标签,逗号分隔 |
| location | string | 否 | 位置/机房信息 |
| remote_access | string | 否 | 远程访问 |
| agent_config | string | 否 | Agent 配置 |
| status | string | 否 | 状态 |
| last_check_time | string (RFC3339) | 否 | 最后检查时间 |
| collect_on | bool | 否 | 是否启用采集(可显式更新为 false |
| collect_args | string | 否 | 采集参数 |
| collect_interval | int | 否 | 采集间隔(秒) |
| collect_last_result | string | 否 | 采集最后结果 |
| os / os_version / kernel | string | 否 | 操作系统与内核字段 |
**注意**: 只传需要更新的字段;未出现的列不修改。更新使用 `map` 写入,可将 `collect_on``collect_interval` 等设为 `false`/`0`
### 请求示例
```http
PUT /DC-Control/v1/servers/1
Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json
{
"description": "",
"status": "offline",
"collect_on": false
}
```
### 返回参数
```json
{
"code": 200,
"message": "success",
"data": {
"message": "updated"
}
}
```
---
## 5. 删除服务器
### 接口信息
- **路径**: `DELETE /DC-Control/v1/servers/:id`
- **描述**: 删除服务器(软删除)
### 请求参数
#### 路径参数 (Path Parameters)
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|--------|
| id | uint | 是 | 服务器ID必须大于0 |
### 请求示例
```http
DELETE /DC-Control/v1/servers/1
Authorization: Bearer {JWT_TOKEN}
```
### 返回参数
```json
{
"code": 200,
"message": "success",
"data": {
"message": "deleted"
}
}
```
---
## 错误码说明
| 错误码 | 说明 |
|--------|------|
| 200 | 成功 |
| 400 | 参数验证失败如ID为0、page/size无效等 |
| 401 | 未授权JWT Token无效或过期 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
## 注意事项
1. 所有接口都需要在请求头中携带JWT Token`Authorization: Bearer {JWT_TOKEN}`
2. 服务器ID必须大于0
3. 分页参数page和size必须为正整数size最大为100
4. `server_identity` 在库中必须唯一;若创建时不传则由服务端生成 ULID
5. 服务器类型(`server_type`支持physical物理、virtual虚拟、cloud
6. 服务器状态online在线、offline离线、unknown未知
7. 删除服务器为软删除,数据不会真正删除
8. **tags** 为可选,任意逗号分隔字符串(如 `env,app,tier`),长度受模型 `varchar(500)` 限制
9. **许可证限制**:创建服务器时会检查**服务器总条数**是否达到许可证 `MaxServer` 上限,超出则拒绝创建
10. 服务器 CRUD **不处理**采集器绑定;关联关系通过**采集器管理**接口(更新采集器 `server_id` 等)维护。列表与详情不内嵌采集器数组。

View File

@@ -11,23 +11,40 @@
<a-form-item label="远程访问端口">
<a-input-number
v-model="form.remote_port"
placeholder="请输入远程访问端口,为空则不可远程访问"
:min="1"
placeholder="请输入远程访问端口"
:min="0"
:max="65535"
style="width: 100%"
allow-clear
/>
<template #extra>
<span style="color: #86909c">为空则不可远程访问</span>
<span style="color: #86909c">SSH/RDP 等远程管理端口0表示未配置</span>
</template>
</a-form-item>
<a-form-item label="Agent URL配置">
<a-input
v-model="form.agent_url"
placeholder="请输入Agent URL"
v-model="form.agent_config"
placeholder="http://192.168.1.100:9100/dc-host/v1/control/command"
allow-clear
/>
<template #extra>
<span style="color: #86909c">完整 http(s) URL用于周期性 POST 指令</span>
</template>
</a-form-item>
<a-form-item label="启用采集">
<a-switch v-model="form.collect_on" />
</a-form-item>
<a-form-item v-if="form.collect_on" label="采集间隔(秒)">
<a-input-number
v-model="form.collect_interval"
:min="10"
:max="3600"
placeholder="默认60秒"
style="width: 100%"
/>
</a-form-item>
</a-form>
</a-modal>
@@ -36,10 +53,12 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { updateServer } from '@/api/ops/server'
import type { ServerItem } from '@/api/ops/server'
interface Props {
visible: boolean
record: any
record: ServerItem | null
}
const props = defineProps<Props>()
@@ -48,36 +67,48 @@ const emit = defineEmits(['update:visible', 'success'])
const loading = ref(false)
const form = ref({
remote_port: undefined as number | undefined,
agent_url: '',
remote_port: 0,
agent_config: '',
collect_on: true,
collect_interval: 60,
})
watch(
() => props.visible,
(val) => {
if (val && props.record) {
form.value.remote_port = props.record.remote_port
form.value.agent_url = props.record.agent_url || ''
form.value.remote_port = props.record.remote_port || 0
form.value.agent_config = props.record.agent_config || ''
form.value.collect_on = props.record.collect_on ?? true
form.value.collect_interval = props.record.collect_interval || 60
}
}
},
)
const handleSubmit = async () => {
if (!props.record?.id) {
Message.error('服务器ID不存在')
return
}
loading.value = true
try {
// 模拟API调用
await new Promise((resolve) => setTimeout(resolve, 500))
// 更新记录
props.record.remote_port = form.value.remote_port
props.record.agent_url = form.value.agent_url
props.record.remote_access = !!form.value.remote_port
props.record.agent_config = !!form.value.agent_url
Message.success('配置成功')
emit('success')
emit('update:visible', false)
const res: any = await updateServer(props.record.id, {
remote_port: form.value.remote_port,
agent_config: form.value.agent_config,
collect_on: form.value.collect_on,
collect_interval: form.value.collect_interval,
})
if (res.code === 0) {
Message.success('配置成功')
emit('success')
emit('update:visible', false)
} else {
Message.error(res.message || '配置失败')
}
} catch (error) {
console.error('配置失败:', error)
Message.error('配置失败')
} finally {
loading.value = false

View File

@@ -1,7 +1,7 @@
<template>
<a-modal
:visible="visible"
:title="isEdit ? '编辑服务器/PC' : '新增服务器/PC'"
:title="isEdit ? '编辑服务器' : '新增服务器'"
@ok="handleOk"
@cancel="handleCancel"
@update:visible="handleUpdateVisible"
@@ -11,21 +11,34 @@
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="unique_id" label="唯一标识">
<a-form-item field="server_identity" label="唯一标识">
<a-input
v-model="formData.unique_id"
placeholder="输入为空系统自动生成UUID"
v-model="formData.server_identity"
placeholder="输入为空系统自动生成ULID"
:disabled="isEdit"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="name" label="服务器名称">
<a-form-item field="name" label="服务器名称" required>
<a-input v-model="formData.name" placeholder="请输入服务器名称" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="host" label="主机地址" required>
<a-input v-model="formData.host" placeholder="请输入主机地址" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="ip_address" label="IP地址">
<a-input v-model="formData.ip_address" placeholder="请输入IP地址" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="8">
<a-form-item field="server_type" label="服务器类型">
@@ -39,75 +52,84 @@
<a-col :span="8">
<a-form-item field="os" label="操作系统">
<a-select v-model="formData.os" placeholder="请选择操作系统">
<a-option value="windows">Windows</a-option>
<a-option value="linux">Linux</a-option>
<a-option value="other">其它</a-option>
<a-option value="Windows">Windows</a-option>
<a-option value="Linux">Linux</a-option>
<a-option value="Mac">Mac</a-option>
<a-option value="Other">其它</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item field="os_version" label="系统版本">
<a-input v-model="formData.os_version" placeholder="如: 22.04" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="8">
<a-form-item field="kernel" label="内核类型">
<a-select v-model="formData.kernel" placeholder="请选择内核类型">
<a-option value="X86">X86</a-option>
<a-option value="ARM">ARM</a-option>
<a-option value="other">其它</a-option>
<a-option value="x86">x86</a-option>
<a-option value="arm">ARM</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item field="rack_id" label="数据中心/楼层/机柜">
<DatacenterSelector
ref="datacenterSelectorRef"
v-model:datacenter-id="formData.datacenter_id"
v-model:floor-id="formData.floor_id"
v-model:rack-id="formData.rack_id"
/>
</a-form-item>
<a-form-item field="tags" label="服务器标签">
<a-input v-model="formData.tags" placeholder="多个标签,逗号间隔" />
</a-form-item>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="ip" label="IP地址">
<a-input v-model="formData.ip" placeholder="可以输入多个IP逗号做分隔" />
<a-col :span="8">
<a-form-item field="location" label="位置/机房">
<a-input v-model="formData.location" placeholder="机房A-机架01" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="remote_access" label="远程访问端口">
<a-input v-model="formData.remote_access" placeholder="为空则不可远程访问" />
<a-col :span="8">
<a-form-item field="tags" label="标签">
<a-input v-model="formData.tags" placeholder="多个标签逗号分隔" />
</a-form-item>
</a-col>
</a-row>
<a-form-item field="agent_config" label="Agent 配置">
<a-input v-model="formData.agent_config" placeholder="请输入Agent 配置" />
</a-form-item>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="data_collection" label="数据采集">
<a-switch v-model="formData.data_collection" />
<a-form-item field="remote_access" label="远程访问">
<a-input v-model="formData.remote_access" placeholder="ssh://user@host" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item v-if="formData.data_collection" field="collection_interval" label="采集时间">
<a-select v-model="formData.collection_interval" placeholder="请选择采集时间">
<a-option :value="1">1分钟</a-option>
<a-option :value="5">5分钟</a-option>
<a-option :value="10">10分钟</a-option>
<a-option :value="30">30分钟</a-option>
<a-form-item field="remote_port" label="远程端口">
<a-input-number v-model="formData.remote_port" placeholder="SSH/RDP端口" :min="0" :max="65535" />
</a-form-item>
</a-col>
</a-row>
<a-form-item field="agent_config" label="Agent 配置 URL">
<a-input v-model="formData.agent_config" placeholder="http://192.168.1.100:9100/dc-host/v1/control/command" />
</a-form-item>
<a-row :gutter="20">
<a-col :span="8">
<a-form-item field="status" label="状态">
<a-select v-model="formData.status" placeholder="请选择状态">
<a-option value="online">在线</a-option>
<a-option value="offline">离线</a-option>
<a-option value="unknown">未知</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item field="collect_on" label="启用采集">
<a-switch v-model="formData.collect_on" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item v-if="formData.collect_on" field="collect_interval" label="采集间隔(秒)">
<a-input-number v-model="formData.collect_interval" :min="10" :max="3600" placeholder="默认60秒" />
</a-form-item>
</a-col>
</a-row>
<a-form-item field="remark" label="备注信息">
<a-form-item field="description" label="描述信息">
<a-textarea
v-model="formData.remark"
placeholder="请输入备注信息"
v-model="formData.description"
placeholder="请输入描述信息"
:rows="4"
/>
</a-form-item>
@@ -118,50 +140,49 @@
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { v4 as uuidv4 } from 'uuid'
import type { FormInstance } from '@arco-design/web-vue'
import DatacenterSelector from './DatacenterSelector.vue'
import { createServer, updateServer } from '@/api/ops/server'
import type { ServerFormData, ServerItem } from '@/api/ops/server'
interface Props {
visible: boolean
record?: any
record?: ServerItem | null
}
const props = withDefaults(defineProps<Props>(), {
record: () => ({}),
record: null,
})
const emit = defineEmits(['update:visible', 'success'])
const formRef = ref<FormInstance>()
const confirmLoading = ref(false)
const datacenterSelectorRef = ref<InstanceType<typeof DatacenterSelector>>()
const isEdit = computed(() => !!props.record?.id)
const formData = reactive({
unique_id: '',
const formData = reactive<ServerFormData>({
server_identity: '',
name: '',
server_type: '',
host: '',
ip_address: '',
description: '',
os: '',
os_version: '',
kernel: '',
datacenter_id: undefined as number | undefined,
floor_id: undefined as number | undefined,
rack_id: undefined as number | undefined,
server_type: '',
tags: '',
ip: '',
location: '',
remote_access: '',
remote_port: 0,
agent_config: '',
data_collection: false,
collection_interval: 5,
remark: '',
status: 'unknown',
collect_on: true,
collect_interval: 60,
})
const rules = {
name: [{ required: true, message: '请输入服务器名称' }],
server_type: [{ required: true, message: '请选择服务器类型' }],
os: [{ required: true, message: '请选择操作系统' }],
host: [{ required: true, message: '请输入主机地址' }],
}
watch(
@@ -170,76 +191,78 @@ watch(
if (val) {
if (isEdit.value && props.record) {
Object.assign(formData, {
unique_id: props.record.unique_id || '',
server_identity: props.record.server_identity || '',
name: props.record.name || '',
server_type: props.record.server_type || '',
host: props.record.host || '',
ip_address: props.record.ip_address || '',
description: props.record.description || '',
os: props.record.os || '',
datacenter_id: props.record.datacenter_id,
floor_id: props.record.floor_id,
rack_id: props.record.rack_id,
os_version: props.record.os_version || '',
kernel: props.record.kernel || '',
server_type: props.record.server_type || '',
tags: props.record.tags || '',
ip: props.record.ip || '',
location: props.record.location || '',
remote_access: props.record.remote_access || '',
remote_port: props.record.remote_port || 0,
agent_config: props.record.agent_config || '',
data_collection: props.record.data_collection || false,
collection_interval: props.record.collection_interval || 5,
remark: props.record.remark || '',
status: props.record.status || 'unknown',
collect_on: props.record.collect_on ?? true,
collect_interval: props.record.collect_interval || 60,
})
// 编辑模式下初始化加载下级列表
if (props.record.datacenter_id) {
datacenterSelectorRef.value?.initLoad()
}
} else {
Object.assign(formData, {
unique_id: '',
server_identity: '',
name: '',
server_type: '',
host: '',
ip_address: '',
description: '',
os: '',
datacenter_id: undefined,
floor_id: undefined,
rack_id: undefined,
os_version: '',
kernel: '',
server_type: '',
tags: '',
ip: '',
location: '',
remote_access: '',
remote_port: 0,
agent_config: '',
data_collection: false,
collection_interval: 5,
remark: '',
status: 'unknown',
collect_on: true,
collect_interval: 60,
})
datacenterSelectorRef.value?.reset()
}
}
}
},
)
const handleOk = async () => {
try {
await formRef.value?.validate()
confirmLoading.value = true
// 准备提交数据
const submitData: any = {
const submitData: ServerFormData = {
server_identity: formData.server_identity || undefined,
name: formData.name,
host: formData.ip,
ip_address: formData.ip,
description: formData.remark,
host: formData.host,
ip_address: formData.ip_address,
description: formData.description,
os: formData.os,
server_type: formData.server_type,
os_version: formData.os_version,
kernel: formData.kernel,
server_type: formData.server_type,
tags: formData.tags,
location: formData.location,
remote_access: formData.remote_access,
remote_port: formData.remote_port,
agent_config: formData.agent_config,
collect_on: formData.data_collection,
collect_interval: formData.collection_interval,
status: formData.status,
collect_on: formData.collect_on,
collect_interval: formData.collect_interval,
}
// 编辑模式或唯一标识
if (isEdit.value && props.record?.id) {
// 更新服务器
const res: any = await updateServer(props.record.id, submitData)
if (res.code === 200 || res.code === 0) {
if (res.code === 0) {
Message.success('更新成功')
emit('success')
handleCancel()
@@ -247,15 +270,8 @@ const handleOk = async () => {
Message.error(res.message || '更新失败')
}
} else {
// 创建服务器
if (!formData.unique_id) {
submitData.server_identity = uuidv4()
} else {
submitData.server_identity = formData.unique_id
}
const res: any = await createServer(submitData)
if (res.code === 200 || res.code === 0) {
if (res.code === 0) {
Message.success('创建成功')
emit('success')
handleCancel()

View File

@@ -15,6 +15,11 @@ export const columns = [
title: '名称',
width: 150,
},
{
dataIndex: 'host',
title: '主机地址',
width: 150,
},
{
dataIndex: 'ip_address',
title: 'IP 地址',
@@ -25,10 +30,25 @@ export const columns = [
title: '操作系统',
width: 120,
},
{
dataIndex: 'os_version',
title: '系统版本',
width: 100,
},
{
dataIndex: 'kernel',
title: '内核',
width: 80,
},
{
dataIndex: 'server_type',
title: '类型',
width: 120,
width: 100,
},
{
dataIndex: 'location',
title: '位置',
width: 150,
},
{
dataIndex: 'tags',
@@ -41,6 +61,11 @@ export const columns = [
width: 100,
slotName: 'remote_access',
},
{
dataIndex: 'remote_port',
title: '远程端口',
width: 100,
},
{
dataIndex: 'agent_config',
title: 'Agent 配置',
@@ -53,6 +78,11 @@ export const columns = [
width: 100,
slotName: 'data_collection',
},
{
dataIndex: 'collect_interval',
title: '采集间隔',
width: 100,
},
{
dataIndex: 'cpu_state',
title: 'CPU使用率',
@@ -77,6 +107,12 @@ export const columns = [
width: 100,
slotName: 'status',
},
{
dataIndex: 'last_check_time',
title: '最后检查',
width: 180,
slotName: 'last_check_time',
},
{
dataIndex: 'actions',
title: '操作',

View File

@@ -106,6 +106,11 @@
</a-tag>
</template>
<!-- 最后检查时间 -->
<template #last_check_time="{ record }">
{{ formatDateTime(record.last_check_time) }}
</template>
<!-- 操作栏 - 下拉菜单 -->
<template #actions="{ record }">
<a-space>
@@ -126,7 +131,7 @@
<icon-down />
</a-button>
<template #content>
<a-doption @click="handleRestart(record)">
<!-- <a-doption @click="handleRestart(record)">
<template #icon>
<icon-refresh />
</template>
@@ -137,19 +142,19 @@
<icon-eye />
</template>
详情
</a-doption>
</a-doption> -->
<a-doption @click="handleEdit(record)">
<template #icon>
<icon-edit />
</template>
编辑
</a-doption>
<a-doption @click="handleRemoteControl(record)">
<!-- <a-doption @click="handleRemoteControl(record)">
<template #icon>
<icon-desktop />
</template>
远程控制
</a-doption>
</a-doption> -->
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
<template #icon>
<icon-delete />
@@ -275,6 +280,28 @@ const getProgressColor = (value: number) => {
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
@@ -296,7 +323,7 @@ const fetchServers = async () => {
const res: any = await fetchServerList(params)
if (res.code === 0) {
const responseData = res.data || res.details || {}
const responseData = res.details || {}
tableData.value = responseData.data || []
pagination.total = responseData.total || 0
@@ -391,7 +418,7 @@ const handleDetail = (record: any) => {
query: {
id: record.id,
name: record.name,
ip: record.ip,
ip: record.host || record.ip_address,
status: record.status,
},
})
@@ -404,7 +431,7 @@ const handleRemoteControl = (record: any) => {
query: {
id: record.id,
name: record.name,
ip: record.ip,
ip: record.host || record.ip_address,
status: record.status,
},
}).href

View File

@@ -97,7 +97,7 @@
</a-avatar>
</div>
<div class="alert-info">
<div class="alert-title">{{ getAlertTitle(alert.title) || '告警信息' }}</div>
<div class="alert-title">{{ getAlertTitle(alert.alert_name) || '告警信息' }}</div>
<div class="alert-description">{{ getAlertDescription(alert.description) || '-' }}</div>
<div class="alert-meta">
<a-tag :color="getStatusTagColor(alert.status)" size="small">
@@ -609,7 +609,7 @@ const loadStatistics = async () => {
// 处理查看详情 - 打开新窗口
const handleViewDetail = (alertId: number) => {
window.open(`/alert/detail/${alertId}`, '_blank');
window.open(`/#/alert/detail?id=${alertId}`, '_blank');
};
// 查看更多警告