feat
This commit is contained in:
199
src/api/ops/database.ts
Normal file
199
src/api/ops/database.ts
Normal 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
191
src/api/ops/middleware.ts
Normal 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 })
|
||||
}
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
644
src/views/ops/pages/alert/detail/index.vue
Normal file
644
src/views/ops/pages/alert/detail/index.vue
Normal 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>
|
||||
296
src/views/ops/pages/dc/database/components/CollectDialog.vue
Normal file
296
src/views/ops/pages/dc/database/components/CollectDialog.vue
Normal 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>
|
||||
285
src/views/ops/pages/dc/database/components/DetailDialog.vue
Normal file
285
src/views/ops/pages/dc/database/components/DetailDialog.vue
Normal 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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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: '操作',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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: '状态',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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` 等)维护。列表与详情不内嵌采集器数组。
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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: '操作',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
// 查看更多警告
|
||||
|
||||
Reference in New Issue
Block a user