This commit is contained in:
2026-04-08 23:14:40 +08:00
parent 720c6fbe74
commit ac759da246
13 changed files with 1389 additions and 329 deletions

View File

@@ -1,130 +1,145 @@
import { request } from "@/api/request";
import { request } from '@/api/request'
/** 获取 告警策略列表 */
export const fetchPolicyList = (data: {
page?: number,
page_size?: number,
keyword?: string,
enabled?: boolean,
priority?: number,
created_at_start?: string,
created_at_end?: string,
order_by?: string,
export const fetchPolicyList = (data: {
page?: number
page_size?: number
keyword?: string
enabled?: boolean
priority?: number
created_at_start?: string
created_at_end?: string
order_by?: string
order?: string
}) => request.get("/Alert/v1/policy/list", { params: data });
}) => request.get('/Alert/v1/policy/list', { params: data })
/** 获取 告警策略详情 */
export const fetchPolicyDetail = (id: number) => request.get(`/Alert/v1/policy/get/${id}`);
export const fetchPolicyDetail = (id: number) => request.get(`/Alert/v1/policy/get/${id}`)
/** 创建 告警策略 */
export const createPolicy = (data: {
name: string;
description?: string;
enabled?: boolean;
priority?: number;
labels?: string;
template_id?: number;
auto_create_ticket?: boolean;
feedback_template_id?: number;
dispatch_rule?: string;
}) => request.post("/Alert/v1/policy/create", data);
name: string
description?: string
enabled?: boolean
priority?: number
labels?: string
template_id?: number
auto_create_ticket?: boolean
feedback_template_id?: number
dispatch_rule?: string
}) => request.post('/Alert/v1/policy/create', data)
/** 更新 告警策略 */
export const updatePolicy = (data: {
id: number;
name?: string;
description?: string;
enabled?: boolean;
priority?: number;
labels?: string;
template_id?: number;
auto_create_ticket?: boolean;
feedback_template_id?: number;
dispatch_rule?: string;
}) => request.post("/Alert/v1/policy/update", data);
id: number
name?: string
description?: string
enabled?: boolean
priority?: number
labels?: string
template_id?: number
auto_create_ticket?: boolean
feedback_template_id?: number
dispatch_rule?: string
}) => request.post('/Alert/v1/policy/update', data)
/** 删除 告警策略 */
export const deletePolicy = (id: number) => request.delete(`/Alert/v1/policy/delete/${id}`);
export const deletePolicy = (id: number) => request.delete(`/Alert/v1/policy/delete/${id}`)
/** 获取 告警规则列表 */
export const fetchRuleList = (data: {
policy_id?: number;
page?: number;
page_size?: number;
keyword?: string;
sort?: string;
policy_id?: number
page?: number
page_size?: number
keyword?: string
sort?: string
order?: string
}) => request.get("/Alert/v1/rule/list", { params: data });
}) => request.get('/Alert/v1/rule/list', { params: data })
/** 获取 告警规则详情 */
export const fetchRuleDetail = (id: number) => request.get(`/Alert/v1/rule/get/${id}`);
export const fetchRuleDetail = (id: number) => request.get(`/Alert/v1/rule/get/${id}`)
/** 创建 告警规则 */
export const createRule = (data: {
policy_id: number;
name: string;
description?: string;
rule_type: string;
severity_id: number;
enabled?: boolean;
metric_name?: string;
query_expr?: string;
threshold?: number;
compare_op?: string;
duration?: number;
baseline_config?: string;
labels?: string;
annotations?: string;
}) => request.post("/Alert/v1/rule/create", data);
policy_id: number
name: string
description?: string
rule_type: string
severity_id: number
enabled?: boolean
metric_name?: string
query_expr?: string
threshold?: number
compare_op?: string
duration?: number
baseline_config?: string
labels?: string
annotations?: string
}) => request.post('/Alert/v1/rule/create', data)
/** 更新 告警规则 */
export const updateRule = (data: {
id: number;
name?: string;
description?: string;
rule_type?: string;
severity_id?: number;
enabled?: boolean;
metric_name?: string;
query_expr?: string;
threshold?: number;
compare_op?: string;
duration?: number;
baseline_config?: string;
labels?: string;
annotations?: string;
}) => request.post("/Alert/v1/rule/update", data);
id: number
name?: string
description?: string
rule_type?: string
severity_id?: number
enabled?: boolean
metric_name?: string
query_expr?: string
threshold?: number
compare_op?: string
duration?: number
baseline_config?: string
labels?: string
annotations?: string
}) => request.post('/Alert/v1/rule/update', data)
/** 删除 告警规则 */
export const deleteRule = (id: number) => request.delete(`/Alert/v1/rule/delete/${id}`);
export const deleteRule = (id: number) => request.delete(`/Alert/v1/rule/delete/${id}`)
/** 获取 告警模板列表 */
export const fetchTemplateList = (data: {
page?: number;
page_size?: number;
keyword?: string;
}) => request.get("/Alert/v1/template/list", { params: data });
export const fetchTemplateList = (data: { page?: number; page_size?: number; keyword?: string }) =>
request.get('/Alert/v1/template/list', { params: data })
/** 获取 告警级别列表 */
export const fetchSeverityList = (data: {
page?: number;
page_size?: number;
enabled?: string;
}) => request.get("/Alert/v1/severity/list", { params: data });
export const fetchSeverityList = (data: { page?: number; page_size?: number; enabled?: string }) =>
request.get('/Alert/v1/severity/list', { params: data })
/** 获取告警级别下拉选项(不分页),支持 keyword 模糊搜索与 enabled=true 过滤 */
export const fetchSeverityOptions = (data?: {
keyword?: string;
enabled?: 'true' | 'false';
}) =>
request.get("/Alert/v1/severity/options", {
export const fetchSeverityOptions = (data?: { keyword?: string; enabled?: 'true' | 'false' }) =>
request.get('/Alert/v1/severity/options', {
params: {
keyword: data?.keyword || undefined,
enabled: data?.enabled ?? 'true',
},
});
})
/** 获取工单模板下拉选项 */
export const fetchFeedbackTemplateOptions = (data?: {
status?: 'active' | 'inactive'
}) => request.get('/Feedback/v1/templates/options', { params: data || {} });
export const fetchFeedbackTemplateOptions = (data?: { status?: 'active' | 'inactive' }) =>
request.get('/Feedback/v1/templates/options', { params: data || {} })
/** 告警策略选项项 */
export interface PolicyOptionItem {
id: number
created_at: string
updated_at: string
name: string
description: string
enabled: boolean
priority: number
labels: string
template_id: number
auto_create_ticket: boolean
feedback_template_id: number
dispatch_rule: string
}
/** 告警策略选项查询参数 */
export interface PolicyOptionsParams {
keyword?: string
enabled?: boolean
}
/** 获取告警策略下拉选项(不分页),支持 keyword 模糊搜索与 enabled 过滤 */
export const fetchPolicyOptions = (params?: PolicyOptionsParams) => request.get<PolicyOptionItem[]>('/Alert/v1/policy/options', { params })

View File

@@ -1,12 +1,12 @@
import { request } from "@/api/request";
import { request } from '@/api/request'
/** 资产状态枚举 */
export enum AssetStatus {
IN_USE = 'in_use', // 在用
IDLE = 'idle', // 闲置
MAINTAIN = 'maintain', // 维修中
SCRAP = 'scrap', // 待报废
DISPOSED = 'disposed', // 已报废
IN_USE = 'in_use', // 在用
IDLE = 'idle', // 闲置
MAINTAIN = 'maintain', // 维修中
SCRAP = 'scrap', // 待报废
DISPOSED = 'disposed', // 已报废
}
/** 资产状态选项 */
@@ -16,13 +16,13 @@ export const assetStatusOptions = [
{ label: '维修中', value: AssetStatus.MAINTAIN },
{ label: '待报废', value: AssetStatus.SCRAP },
{ label: '已报废', value: AssetStatus.DISPOSED },
];
]
/** 获取资产状态文本 */
export const getAssetStatusText = (status: string) => {
const item = assetStatusOptions.find(opt => opt.value === status);
return item?.label || status;
};
const item = assetStatusOptions.find((opt) => opt.value === status)
return item?.label || status
}
/** 获取资产状态颜色 */
export const getAssetStatusColor = (status: string) => {
@@ -32,113 +32,117 @@ export const getAssetStatusColor = (status: string) => {
[AssetStatus.MAINTAIN]: 'orange',
[AssetStatus.SCRAP]: 'red',
[AssetStatus.DISPOSED]: 'gray',
};
return colorMap[status] || 'gray';
};
}
return colorMap[status] || 'gray'
}
/** 资产列表查询参数 */
export interface AssetListParams {
page?: number;
page_size?: number;
keyword?: string;
status?: string;
category_id?: number;
supplier_id?: number;
datacenter_id?: number;
department?: string;
sort?: string;
order?: string;
page?: number
page_size?: number
keyword?: string
status?: string
category_id?: number
supplier_id?: number
datacenter_id?: number
floor_id?: number
room_id?: number
rack_id?: number
department?: string
sort?: string
order?: string
}
/** 资产表单数据 */
export interface AssetForm {
id?: number;
asset_name: string;
asset_code: string;
category_id?: number;
model?: string;
manufacturer?: string;
serial_number?: string;
purchase_date?: string;
original_value?: number;
supplier_id?: number;
warranty_period?: string;
warranty_expiry?: string;
department?: string;
user?: string;
status?: string;
location?: string;
datacenter_id?: number;
floor_id?: number;
rack_id?: number;
unit_start?: number;
unit_end?: number;
qr_code?: string;
rfid_tag?: string;
asset_tag?: string;
specifications?: string;
description?: string;
remarks?: string;
id?: number
asset_name: string
asset_code: string
category_id?: number
model?: string
manufacturer?: string
serial_number?: string
purchase_date?: string
original_value?: number
supplier_id?: number
warranty_period?: string
warranty_expiry?: string
department?: string
user?: string
status?: string
location?: string
datacenter_id?: number
floor_id?: number
room_id?: number
rack_id?: number
unit_start?: number
unit_end?: number
qr_code?: string
rfid_tag?: string
asset_tag?: string
specifications?: string
description?: string
remarks?: string
}
/** 获取资产列表(分页) */
export const fetchAssetList = (data?: AssetListParams) => {
return request.post("/Assets/v1/asset/list", data || {});
};
return request.post('/Assets/v1/asset/list', data || {})
}
/** 获取资产列表(不分页,下拉) */
export const fetchAssetAll = (params?: { keyword?: string }) => {
return request.get("/Assets/v1/asset/all", { params });
};
return request.get('/Assets/v1/asset/all', { params })
}
/** 获取资产详情 */
export const fetchAssetDetail = (id: number) => {
return request.get(`/Assets/v1/asset/detail/${id}`);
};
return request.get(`/Assets/v1/asset/detail/${id}`)
}
/** 创建资产 */
export const createAsset = (data: AssetForm) => {
return request.post("/Assets/v1/asset/create", data);
};
return request.post('/Assets/v1/asset/create', data)
}
/** 更新资产 */
export const updateAsset = (data: AssetForm) => {
return request.put("/Assets/v1/asset/update", data);
};
return request.put('/Assets/v1/asset/update', data)
}
/** 删除资产 */
export const deleteAsset = (id: number) => {
return request.delete(`/Assets/v1/asset/delete/${id}`);
};
return request.delete(`/Assets/v1/asset/delete/${id}`)
}
/** 导出资产 */
export const exportAssets = (keyword?: string) => {
const params: any = {};
if (keyword) params.keyword = keyword;
return request.get("/Assets/v1/asset/export", { params });
};
const params: any = {}
if (keyword) params.keyword = keyword
return request.get('/Assets/v1/asset/export', { params })
}
/** 获取资产分类列表(下拉) */
export const fetchCategoryOptions = () => {
return request.get("/Assets/v1/category/all");
};
return request.get('/Assets/v1/category/all')
}
/** 获取供应商列表(下拉) */
export const fetchSupplierOptions = () => {
return request.get("/Assets/v1/supplier/all");
};
return request.get('/Assets/v1/supplier/all')
}
/** 获取数据中心列表(下拉) */
export const fetchDatacenterOptions = () => {
return request.get("/Assets/v1/datacenter/list");
};
return request.get('/Assets/v1/datacenter/list')
}
/** 根据数据中心获取楼层列表 */
export const fetchFloorOptions = (datacenterId: number) => {
return request.get(`/Assets/v1/datacenter/${datacenterId}`);
};
return request.get(`/Assets/v1/datacenter/${datacenterId}`)
}
/** 获取机柜列表(下拉) */
export const fetchRackOptions = (params?: { datacenter_id?: number; floor_id?: number }) => {
return request.post("/Assets/v1/rack/list", params || {});
};
return request.post('/Assets/v1/rack/list', params || {})
}

View File

@@ -1,73 +1,69 @@
import { request } from "@/api/request";
import { request } from '@/api/request'
/** 获取机柜列表(分页) */
export const fetchRackList = (data?: {
page?: number;
page_size?: number;
keyword?: string;
datacenter_id?: number;
floor_id?: number;
rack_type?: string;
status?: string;
sort?: string;
order?: string;
page?: number
page_size?: number
keyword?: string
datacenter_id?: number
floor_id?: number
room_id?: number
rack_type?: string
status?: string
sort?: string
order?: string
}) => {
return request.post("/Assets/v1/rack/list", data || {});
};
return request.post('/Assets/v1/rack/list', data || {})
}
/** 根据数据中心获取机柜列表(下拉,不分页) */
export const fetchRackListByDatacenter = (
datacenterId: number,
params?: { name?: string }
) => {
return request.get(`/Assets/v1/rack/datacenter/${datacenterId}`, { params });
};
export const fetchRackListByDatacenter = (datacenterId: number, params?: { name?: string }) => {
return request.get(`/Assets/v1/rack/datacenter/${datacenterId}`, { params })
}
/** 根据楼层获取机柜列表(下拉,不分页) */
export const fetchRackListByFloor = (
floorId: number,
params?: { name?: string }
) => {
return request.get(`/Assets/v1/rack/floor/${floorId}`, { params });
};
/** 根据楼层获取机柜列表(下拉,不分页)- 已废弃,请使用 fetchRackListByRoom */
export const fetchRackListByFloor = (floorId: number, params?: { name?: string }) => {
return request.get(`/Assets/v1/rack/floor/${floorId}`, { params })
}
/** 根据机房获取机柜列表(下拉,不分页) */
export const fetchRackListByRoom = (roomId: number, params?: { name?: string }) => {
return request.get(`/Assets/v1/rack/room/${roomId}`, { params })
}
/** 获取机柜详情 */
export const fetchRackDetail = (id: number) => {
return request.get(`/Assets/v1/rack/detail/${id}`);
};
return request.get(`/Assets/v1/rack/detail/${id}`)
}
/** 创建机柜 */
export const createRack = (data: any) => {
return request.post("/Assets/v1/rack/create", data);
};
return request.post('/Assets/v1/rack/create', data)
}
/** 更新机柜 */
export const updateRack = (data: any) => {
return request.put("/Assets/v1/rack/update", data);
};
return request.put('/Assets/v1/rack/update', data)
}
/** 删除机柜 */
export const deleteRack = (id: number) => {
return request.delete(`/Assets/v1/rack/delete/${id}`);
};
return request.delete(`/Assets/v1/rack/delete/${id}`)
}
/** 获取供应商列表(用于下拉选择) */
export const fetchSupplierList = () => {
return request.get("/Assets/v1/supplier/all");
};
return request.get('/Assets/v1/supplier/all')
}
/** 获取数据中心列表(用于下拉选择) */
export const fetchDatacenterList = (params?: { keyword?: string; name?: string }) => {
const normalizedParams = params?.keyword
? { ...params, name: params.name ?? params.keyword }
: params
return request.get("/Assets/v1/datacenter/all", { params: normalizedParams });
};
const normalizedParams = params?.keyword ? { ...params, name: params.name ?? params.keyword } : params
return request.get('/Assets/v1/datacenter/all', { params: normalizedParams })
}
/** 获取楼层列表(用于下拉选择) */
export const fetchFloorListForSelect = (params?: { datacenter_id?: number; keyword?: string; name?: string }) => {
const normalizedParams = params?.keyword
? { ...params, name: params.name ?? params.keyword }
: params
return request.get("/Assets/v1/floor/all", { params: normalizedParams });
};
const normalizedParams = params?.keyword ? { ...params, name: params.name ?? params.keyword } : params
return request.get('/Assets/v1/floor/all', { params: normalizedParams })
}

155
src/api/ops/room-device.ts Normal file
View File

@@ -0,0 +1,155 @@
import { request } from '@/api/request'
/** 机房设备服务项 */
export interface RoomDeviceItem {
id: number
created_at: string
updated_at: string
deleted_at: string | null
service_identity: string
name: string
description: string
room_id: string
device_category: string
agent_config: string
enabled: boolean
collect_on: boolean
collect_interval: number
collect_last_result: string
policy_ids?: number[]
}
/** 机房设备列表响应 */
export interface RoomDeviceListResponse {
total: number
page: number
page_size: number
data: RoomDeviceItem[]
}
/** 机房设备列表查询参数 */
export interface RoomDeviceListParams {
page?: number
size?: number
keyword?: string
enabled?: boolean
room_id?: string
device_category?: string
}
/** 机房设备创建数据 */
export interface RoomDeviceCreateData {
service_identity?: string
name: string
description?: string
room_id: string
device_category: string
agent_config?: string
enabled?: boolean
collect_on?: boolean
collect_interval?: number
policy_ids?: number[]
}
/** 机房设备更新数据 */
export interface RoomDeviceUpdateData {
name?: string
description?: string
room_id?: string
device_category?: string
agent_config?: string
enabled?: boolean
collect_on?: boolean
collect_interval?: number
policy_ids?: number[]
}
/** 机房设备采集配置数据 */
export interface RoomDeviceCollectData {
collect_on?: boolean
collect_interval?: number
agent_config?: string
}
/** 指标数据项 */
export interface MetricItem {
timestamp: string
service_identity: string
room_id: string
device_category: string
type?: string
metric_name: string
metric_value: number
metric_unit?: string
}
/** 指标上报数据 */
export interface MetricsUploadData {
metrics: MetricItem[]
}
/** 获取机房设备列表(分页) */
export const fetchRoomDeviceList = (params?: RoomDeviceListParams) => {
return request.get<RoomDeviceListResponse>('/DC-Control/v1/room-devices', { params })
}
/** 获取机房设备详情 */
export const fetchRoomDeviceDetail = (id: number) => {
return request.get<RoomDeviceItem>(`/DC-Control/v1/room-devices/${id}`)
}
/** 创建机房设备 */
export const createRoomDevice = (data: RoomDeviceCreateData) => {
return request.post<{ message: string; id: number }>('/DC-Control/v1/room-devices', data)
}
/** 更新机房设备 */
export const updateRoomDevice = (id: number, data: RoomDeviceUpdateData) => {
return request.put<{ message: string }>(`/DC-Control/v1/room-devices/${id}`, data)
}
/** 删除机房设备 */
export const deleteRoomDevice = (id: number) => {
return request.delete<{ message: string }>(`/DC-Control/v1/room-devices/${id}`)
}
/** 更新采集配置 */
export const patchRoomDeviceCollect = (id: number, data: RoomDeviceCollectData) => {
return request.patch<{ message: string }>(`/DC-Control/v1/room-devices/${id}/collect`, data)
}
/** 查询最新指标 */
export const fetchLatestMetrics = (serviceIdentity: string) => {
return request.get<MetricItem[]>('/DC-Control/v1/room-devices/metrics/latest', {
params: { service_identity: serviceIdentity },
})
}
/** 上报指标(匿名接口) */
export const uploadMetrics = (data: MetricsUploadData) => {
return request.post<{ message: string }>('/DC-Control/v1/room-devices/metrics/upload', data)
}
/** 设备分类选项 */
export const DEVICE_CATEGORY_OPTIONS = [
{ label: '电力', value: 'power' },
{ label: 'UPS', value: 'ups' },
{ label: '空调', value: 'air_conditioner' },
{ label: '温湿度', value: 'temp_humidity' },
{ label: '消防', value: 'fire_control' },
{ label: '门禁', value: 'access_control' },
{ label: '漏水', value: 'water_leak' },
{ label: '有害气体', value: 'hazardous_gas' },
]
/** 设备分类映射 */
export const DEVICE_CATEGORY_MAP: Record<string, string> = {
power: '电力',
ups: 'UPS',
air_conditioner: '空调',
temp_humidity: '温湿度',
fire_control: '消防',
access_control: '门禁',
water_leak: '漏水',
hazardous_gas: '有害气体',
}

87
src/api/ops/room.ts Normal file
View File

@@ -0,0 +1,87 @@
import { request } from '@/api/request'
/** 机房项 */
export interface RoomItem {
id: number
created_at: string
updated_at: string
deleted_at: string | null
name: string
code: string
description?: string
floor_id: number
datacenter_id: number
status?: string
floor?: { id: number; name: string }
datacenter?: { id: number; name: string }
}
/** 机房列表响应 */
export interface RoomListResponse {
code: number
details: {
data: RoomItem[]
total: number
}
}
/** 机房列表查询参数 */
export interface RoomListParams {
page?: number
page_size?: number
keyword?: string
datacenter_id?: number
floor_id?: number
status?: string
}
/** 机房创建数据 */
export interface RoomCreateData {
name: string
code: string
description?: string
floor_id: number
datacenter_id: number
status?: string
}
/** 机房更新数据 */
export interface RoomUpdateData {
id: number
name?: string
code?: string
description?: string
floor_id?: number
datacenter_id?: number
status?: string
}
/** 获取机房列表(分页) */
export const fetchRoomList = (data?: RoomListParams) => {
return request.post<RoomListResponse>('/Assets/v1/room/list', data || {})
}
/** 获取机房详情 */
export const fetchRoomDetail = (id: number) => {
return request.get<RoomItem>(`/Assets/v1/room/detail/${id}`)
}
/** 根据楼层获取机房列表(下拉,不分页) */
export const fetchRoomListByFloor = (floorId: number, params?: { name?: string }) => {
return request.get<RoomItem[]>(`/Assets/v1/room/floor/${floorId}`, { params })
}
/** 创建机房 */
export const createRoom = (data: RoomCreateData) => {
return request.post('/Assets/v1/room/create', data)
}
/** 更新机房 */
export const updateRoom = (data: RoomUpdateData) => {
return request.put('/Assets/v1/room/update', data)
}
/** 删除机房 */
export const deleteRoom = (id: number) => {
return request.delete(`/Assets/v1/room/delete/${id}`)
}

View File

@@ -3,10 +3,11 @@
<!-- 机柜选择卡片 -->
<a-card class="rack-select-card">
<template #title>
<icon-storage /> 选择机柜
<icon-storage />
选择机柜
</template>
<a-row :gutter="16">
<a-col :span="8">
<a-col :span="6">
<a-select
v-model="selectedDatacenterId"
placeholder="请选择数据中心"
@@ -16,16 +17,12 @@
@change="handleDatacenterChange"
style="width: 100%"
>
<a-option
v-for="datacenter in datacenterList"
:key="datacenter.value"
:value="datacenter.value"
>
<a-option v-for="datacenter in datacenterList" :key="datacenter.value" :value="datacenter.value">
{{ datacenter.label }}
</a-option>
</a-select>
</a-col>
<a-col :span="8">
<a-col :span="6">
<a-select
v-model="selectedFloorId"
placeholder="请选择楼层"
@@ -36,49 +33,47 @@
@change="handleFloorChange"
style="width: 100%"
>
<a-option
v-for="floor in floorList"
:key="floor.value"
:value="floor.value"
>
<a-option v-for="floor in floorList" :key="floor.value" :value="floor.value">
{{ floor.label }}
</a-option>
</a-select>
</a-col>
<a-col :span="8">
<a-col :span="6">
<a-select
v-model="selectedRoomId"
placeholder="请选择机房"
:loading="roomListLoading"
:disabled="!selectedFloorId"
allow-search
@search="handleRoomSearch"
@change="handleRoomChange"
style="width: 100%"
>
<a-option v-for="room in roomList" :key="room.id" :value="room.id">{{ room.name }} ({{ room.code }})</a-option>
</a-select>
</a-col>
<a-col :span="6">
<a-select
v-model="selectedRackId"
placeholder="请选择机柜"
:loading="rackListLoading"
:disabled="!selectedFloorId"
:disabled="!selectedRoomId"
allow-search
@search="handleRackSearch"
@change="handleRackChange"
style="width: 100%"
>
<a-option
v-for="rack in rackList"
:key="rack.id"
:value="rack.id"
>
{{ rack.name }} ({{ rack.code }})
</a-option>
<a-option v-for="rack in rackList" :key="rack.id" :value="rack.id">{{ rack.name }} ({{ rack.code }})</a-option>
</a-select>
</a-col>
<a-col :span="16">
</a-row>
<a-row :gutter="16" style="margin-top: 16px">
<a-col :span="24">
<a-space>
<a-tag v-if="rackInfo.height">
总U位: {{ rackInfo.height }}
</a-tag>
<a-tag v-if="usedUnits" color="blue">
已使用: {{ usedUnits }}
</a-tag>
<a-tag v-if="availableUnits" color="green">
空余: {{ availableUnits }}
</a-tag>
<a-tag v-if="usagePercentage" :color="usagePercentage > 80 ? 'red' : 'orange'">
使用率: {{ usagePercentage }}%
</a-tag>
<a-tag v-if="rackInfo.height">总U位: {{ rackInfo.height }}</a-tag>
<a-tag v-if="usedUnits" color="blue">已使用: {{ usedUnits }}</a-tag>
<a-tag v-if="availableUnits" color="green">空余: {{ availableUnits }}</a-tag>
<a-tag v-if="usagePercentage" :color="usagePercentage > 80 ? 'red' : 'orange'">使用率: {{ usagePercentage }}%</a-tag>
</a-space>
</a-col>
</a-row>
@@ -111,17 +106,11 @@
<!-- U位列表 -->
<a-card class="u-position-card" :loading="loading">
<template #title>
<icon-apps /> U位列表
<icon-apps />
U位列表
</template>
<a-table
:data="unitList"
:pagination="false"
:bordered="{ cell: true }"
:scroll="{ x: 1400 }"
:loading="loading"
size="small"
>
<a-table :data="unitList" :pagination="false" :bordered="{ cell: true }" :scroll="{ x: 1400 }" :loading="loading" size="small">
<template #columns>
<!-- <a-table-column title="序号" :width="80">
<template #cell="{ rowIndex }">
@@ -146,34 +135,14 @@
<template #cell="{ record }">
<a-space size="small">
<!-- 禁用/启用按钮所有状态都显示 -->
<a-button
v-if="record.status !== 'disabled'"
type="text"
size="small"
@click="handleDisable(record)"
>
禁用
</a-button>
<a-button
v-else
type="text"
size="small"
@click="handleEnable(record)"
>
启用
</a-button>
<a-button v-if="record.status !== 'disabled'" type="text" size="small" @click="handleDisable(record)">禁用</a-button>
<a-button v-else type="text" size="small" @click="handleEnable(record)">启用</a-button>
<!-- 已占用状态显示释放按钮 -->
<a-button
v-if="record.status === 'occupied'"
type="text"
size="small"
status="danger"
@click="handleRelease(record)"
>
<a-button v-if="record.status === 'occupied'" type="text" size="small" status="danger" @click="handleRelease(record)">
释放
</a-button>
<!-- 已预留状态显示取消预留按钮 -->
<a-button
v-if="record.status === 'reserved'"
@@ -213,19 +182,10 @@
import { ref, reactive, computed, onMounted } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { IconStorage, IconApps, IconPlus, IconLock, IconRefresh } from '@arco-design/web-vue/es/icon'
import {
fetchUnitList,
allocateUnit,
reserveUnit,
cancelReservation,
releaseUnit,
updateUnitStatus,
} from '@/api/ops/unit'
import {
fetchDatacenterList,
fetchRackListByFloor,
} from '@/api/ops/rack'
import { fetchUnitList, allocateUnit, reserveUnit, cancelReservation, releaseUnit, updateUnitStatus } from '@/api/ops/unit'
import { fetchDatacenterList, fetchRackListByRoom } from '@/api/ops/rack'
import { fetchFloorListByDatacenter } from '@/api/ops/floor'
import { fetchRoomListByFloor } from '@/api/ops/room'
import { normalizeUnitList } from './utils/unitFallback'
import AllocateUnitDialog from './components/AllocateUnitDialog.vue'
import ReserveUnitDialog from './components/ReserveUnitDialog.vue'
@@ -235,16 +195,20 @@ const loading = ref(false)
const rackListLoading = ref(false)
const datacenterListLoading = ref(false)
const floorListLoading = ref(false)
const roomListLoading = ref(false)
const selectedDatacenterId = ref<number | undefined>(undefined)
const selectedFloorId = ref<number | undefined>(undefined)
const selectedRoomId = ref<number | undefined>(undefined)
const selectedRackId = ref<number | undefined>(undefined)
const rackInfo = ref<any>({})
const datacenterList = ref<{ label: string; value: number }[]>([])
const floorList = ref<{ label: string; value: number }[]>([])
const roomList = ref<any[]>([])
const rackList = ref<any[]>([])
const unitList = ref<any[]>([])
let datacenterSearchTimer: number | undefined
let floorSearchTimer: number | undefined
let roomSearchTimer: number | undefined
let rackSearchTimer: number | undefined
// 对话框可见性
@@ -253,9 +217,7 @@ const reserveVisible = ref(false)
// 已使用U位
const usedUnits = computed(() => {
return unitList.value.filter(
(unit) => unit.status === 'occupied' || unit.status === 'reserved'
).length
return unitList.value.filter((unit) => unit.status === 'occupied' || unit.status === 'reserved').length
})
// 空余U位
@@ -270,12 +232,7 @@ const usagePercentage = computed(() => {
})
const extractList = (res: any): any[] => {
const candidate =
res?.data?.data ??
res?.details?.data ??
res?.data ??
res?.details ??
[]
const candidate = res?.data?.data ?? res?.details?.data ?? res?.data ?? res?.details ?? []
return Array.isArray(candidate) ? candidate : []
}
@@ -303,14 +260,14 @@ const getUnitStatusText = (status?: string) => {
// 获取机柜列表
const fetchRacks = async (keyword?: string) => {
if (!selectedFloorId.value) {
if (!selectedRoomId.value) {
rackList.value = []
return
}
rackListLoading.value = true
try {
const res: any = await fetchRackListByFloor(selectedFloorId.value, { name: keyword })
const res: any = await fetchRackListByRoom(selectedRoomId.value, { name: keyword })
rackList.value = extractList(res)
} catch (error) {
console.error('获取机柜列表失败:', error)
@@ -363,6 +320,26 @@ const fetchFloors = async (keyword?: string) => {
}
}
const fetchRooms = async (keyword?: string) => {
if (!selectedFloorId.value) {
roomList.value = []
return
}
roomListLoading.value = true
try {
const res: any = await fetchRoomListByFloor(selectedFloorId.value, {
name: keyword || undefined,
})
roomList.value = extractList(res)
} catch (error) {
console.error('获取机房列表失败:', error)
Message.error('获取机房列表失败')
roomList.value = []
} finally {
roomListLoading.value = false
}
}
const handleDatacenterSearch = (keyword: string) => {
if (datacenterSearchTimer) {
window.clearTimeout(datacenterSearchTimer)
@@ -382,8 +359,18 @@ const handleFloorSearch = (keyword: string) => {
}, 300)
}
const handleRackSearch = (keyword: string) => {
const handleRoomSearch = (keyword: string) => {
if (!selectedFloorId.value) return
if (roomSearchTimer) {
window.clearTimeout(roomSearchTimer)
}
roomSearchTimer = window.setTimeout(() => {
fetchRooms(keyword?.trim() || undefined)
}, 300)
}
const handleRackSearch = (keyword: string) => {
if (!selectedRoomId.value) return
if (rackSearchTimer) {
window.clearTimeout(rackSearchTimer)
}
@@ -397,9 +384,11 @@ const handleDatacenterChange = async (datacenterId?: number | string) => {
selectedDatacenterId.value = Number(datacenterId)
}
selectedFloorId.value = undefined
selectedRoomId.value = undefined
selectedRackId.value = undefined
rackInfo.value = {}
rackList.value = []
roomList.value = []
floorList.value = []
unitList.value = []
await fetchFloors()
@@ -409,6 +398,19 @@ const handleFloorChange = async (floorId?: number | string) => {
if (floorId !== undefined && floorId !== null && floorId !== '') {
selectedFloorId.value = Number(floorId)
}
selectedRoomId.value = undefined
selectedRackId.value = undefined
rackInfo.value = {}
rackList.value = []
roomList.value = []
unitList.value = []
await fetchRooms()
}
const handleRoomChange = async (roomId?: number | string) => {
if (roomId !== undefined && roomId !== null && roomId !== '') {
selectedRoomId.value = Number(roomId)
}
selectedRackId.value = undefined
rackInfo.value = {}
rackList.value = []
@@ -429,14 +431,14 @@ const handleRackChange = (rackId: number) => {
// 获取U位列表
const fetchUnits = async (rackId?: number) => {
const targetRackId = rackId || selectedRackId.value
if (!targetRackId) return
loading.value = true
try {
const res = await fetchUnitList(targetRackId)
if (res.code === 0) {
const payload = res?.details ?? res?.data ?? {}
rackInfo.value = payload?.rack || {}
@@ -489,14 +491,14 @@ const handleDisable = async (record: any) => {
Message.warning('请先选择机柜')
return
}
const res = await updateUnitStatus({
rack_id: selectedRackId.value,
start_unit: record.unit_number,
end_unit: record.unit_number,
status: 'disabled',
})
if (res.code === 0) {
Message.success('禁用成功')
fetchUnits()
@@ -521,14 +523,14 @@ const handleEnable = async (record: any) => {
Message.warning('请先选择机柜')
return
}
const res = await updateUnitStatus({
rack_id: selectedRackId.value,
start_unit: record.unit_number,
end_unit: record.unit_number,
status: 'available',
})
if (res.code === 0) {
Message.success('启用成功')
fetchUnits()
@@ -549,21 +551,19 @@ const handleRelease = async (record: any) => {
title: '确认释放',
content: `确认释放 U位 ${record.unit_number} 吗?`,
onOk: async () => {
const endUnit = record.occupied_units > 1
? record.unit_number + record.occupied_units - 1
: record.unit_number
const endUnit = record.occupied_units > 1 ? record.unit_number + record.occupied_units - 1 : record.unit_number
if (!selectedRackId.value) {
Message.warning('请先选择机柜')
return
}
const res = await releaseUnit({
rack_id: selectedRackId.value,
start_unit: record.unit_number,
end_unit: endUnit,
})
if (res.code === 0) {
Message.success('释放成功')
fetchUnits()
@@ -584,21 +584,19 @@ const handleCancelReservation = async (record: any) => {
title: '确认取消预留',
content: `确认取消 U位 ${record.unit_number} 的预留吗?`,
onOk: async () => {
const endUnit = record.occupied_units > 1
? record.unit_number + record.occupied_units - 1
: record.unit_number
const endUnit = record.occupied_units > 1 ? record.unit_number + record.occupied_units - 1 : record.unit_number
if (!selectedRackId.value) {
Message.warning('请先选择机柜')
return
}
const res = await cancelReservation({
rack_id: selectedRackId.value,
start_unit: record.unit_number,
end_unit: endUnit,
})
if (res.code === 0) {
Message.success('取消预留成功')
fetchUnits()
@@ -637,7 +635,7 @@ export default {
.action-card {
margin-bottom: 16px;
:deep(.arco-card-body) {
padding: 12px 20px;
}

View File

@@ -0,0 +1,132 @@
<template>
<div class="detail-container">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="ID">{{ record.id }}</a-descriptions-item>
<a-descriptions-item label="服务标识">{{ record.service_identity }}</a-descriptions-item>
<a-descriptions-item label="服务名称">{{ record.name }}</a-descriptions-item>
<a-descriptions-item label="设备分类">
{{ DEVICE_CATEGORY_MAP[record.device_category] || record.device_category }}
</a-descriptions-item>
<a-descriptions-item label="机房ID">{{ record.room_id }}</a-descriptions-item>
<a-descriptions-item label="描述信息" :span="2">{{ record.description || '-' }}</a-descriptions-item>
<a-descriptions-item label="采集地址" :span="2">
<a-link v-if="record.agent_config" :href="record.agent_config" target="_blank">{{ record.agent_config }}</a-link>
<span v-else>-</span>
</a-descriptions-item>
<a-descriptions-item label="启用状态">
<a-tag :color="record.enabled ? 'green' : 'gray'">
{{ record.enabled ? '已启用' : '已禁用' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="参与周期采集">
<a-tag :color="record.collect_on ? 'green' : 'gray'">
{{ record.collect_on ? '已启用' : '未启用' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="采集间隔">{{ record.collect_interval }}</a-descriptions-item>
<a-descriptions-item label="采集结果摘要" :span="2">{{ record.collect_last_result || '-' }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ formatTime(record.created_at) }}</a-descriptions-item>
<a-descriptions-item label="更新时间">{{ formatTime(record.updated_at) }}</a-descriptions-item>
</a-descriptions>
<div class="action-bar">
<a-space>
<a-button type="primary" @click="$emit('edit')">
<template #icon><icon-edit /></template>
编辑
</a-button>
<a-button type="outline" @click="$emit('quick-config')">
<template #icon><icon-settings /></template>
采集配置
</a-button>
<a-button type="outline" @click="handleViewMetrics">
<template #icon><icon-eye /></template>
查看最新指标
</a-button>
<a-button type="outline" status="danger" @click="$emit('delete')">
<template #icon><icon-delete /></template>
删除
</a-button>
</a-space>
</div>
<a-drawer v-model:visible="metricsVisible" :width="800" title="最新指标数据" :footer="false" unmount-on-close>
<a-spin :loading="metricsLoading" style="width: 100%">
<div v-if="metricsData.length > 0">
<a-list :bordered="false">
<a-list-item v-for="(item, index) in metricsData" :key="index">
<a-descriptions :column="3" size="small">
<a-descriptions-item label="指标名称">{{ item.metric_name }}</a-descriptions-item>
<a-descriptions-item label="指标值">{{ item.metric_value }} {{ item.metric_unit || '' }}</a-descriptions-item>
<a-descriptions-item label="类型">{{ item.type || '-' }}</a-descriptions-item>
<a-descriptions-item label="时间">{{ formatTime(item.timestamp) }}</a-descriptions-item>
</a-descriptions>
</a-list-item>
</a-list>
</div>
<a-empty v-else description="暂无指标数据" />
</a-spin>
</a-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { IconEdit, IconDelete, IconSettings, IconEye } from '@arco-design/web-vue/es/icon'
import { DEVICE_CATEGORY_MAP, fetchLatestMetrics, type RoomDeviceItem, type MetricItem } from '@/api/ops/room-device'
interface Props {
record: RoomDeviceItem
}
const props = defineProps<Props>()
defineEmits(['edit', 'quick-config', 'delete'])
const metricsVisible = ref(false)
const metricsLoading = ref(false)
const metricsData = ref<MetricItem[]>([])
const handleViewMetrics = async () => {
metricsVisible.value = true
metricsLoading.value = true
try {
const response = await fetchLatestMetrics(props.record.service_identity)
metricsData.value = (response as any)?.details?.data || []
} catch (error) {
console.error('获取最新指标失败:', error)
Message.error('获取最新指标失败')
metricsData.value = []
} finally {
metricsLoading.value = false
}
}
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}`
}
</script>
<style scoped lang="less">
.detail-container {
padding: 16px;
}
.action-bar {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--color-border);
}
</style>

View File

@@ -0,0 +1,232 @@
<template>
<a-modal
:visible="visible"
:title="isEdit ? '编辑机房设备' : '新增机房设备'"
@ok="handleOk"
@cancel="handleCancel"
@update:visible="handleUpdateVisible"
:confirm-loading="confirmLoading"
width="800px"
>
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="name" label="服务名称">
<a-input v-model="formData.name" placeholder="请输入服务名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="room_id" label="机房ID">
<a-input v-model="formData.room_id" placeholder="请输入机房ID" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="12">
<a-form-item field="device_category" label="设备分类">
<a-select v-model="formData.device_category" placeholder="请选择设备分类">
<a-option v-for="item in DEVICE_CATEGORY_OPTIONS" :key="item.value" :value="item.value">
{{ item.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item field="description" label="描述信息">
<a-textarea v-model="formData.description" placeholder="请输入描述信息" :rows="2" />
</a-form-item>
<a-form-item field="agent_config" label="采集地址(HTTP/HTTPS GET)">
<a-input v-model="formData.agent_config" placeholder="周期采集地址,返回 JSON 格式指标数组" />
</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">
<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="默认60秒" :min="10" :max="3600" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-form-item field="policy_ids" label="告警策略">
<a-select v-model="formData.policy_ids" placeholder="请选择告警策略" multiple allow-clear>
<a-option v-for="policy in policyOptions" :key="policy.id" :value="policy.id">
{{ policy.name }}
</a-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { FormInstance } from '@arco-design/web-vue'
import {
createRoomDevice,
updateRoomDevice,
DEVICE_CATEGORY_OPTIONS,
type RoomDeviceCreateData,
type RoomDeviceUpdateData,
} from '@/api/ops/room-device'
import { fetchPolicyOptions, type PolicyOptionItem } from '@/api/ops/alertPolicy'
interface Props {
visible: boolean
record?: any
}
const props = withDefaults(defineProps<Props>(), {
record: () => ({}),
})
const emit = defineEmits(['update:visible', 'success'])
const formRef = ref<FormInstance>()
const confirmLoading = ref(false)
const policyOptions = ref<PolicyOptionItem[]>([])
const isEdit = computed(() => !!props.record?.id)
const formData = reactive({
name: '',
description: '',
room_id: '',
device_category: '',
agent_config: '',
enabled: true,
collect_on: true,
collect_interval: 60,
policy_ids: [] as number[],
})
const rules = {
name: [{ required: true, message: '请输入服务名称' }],
room_id: [{ required: true, message: '请输入机房ID' }],
device_category: [{ required: true, message: '请选择设备分类' }],
}
const loadPolicyOptions = async () => {
try {
const response: any = await fetchPolicyOptions({ enabled: true })
if (Array.isArray(response)) {
policyOptions.value = response
} else if (response && response.details) {
policyOptions.value = Array.isArray(response.details) ? response.details : response.details.data || []
} else {
policyOptions.value = []
}
} catch (error) {
console.error('加载告警策略列表失败:', error)
policyOptions.value = []
}
}
watch(
() => props.visible,
(val) => {
if (val) {
if (isEdit.value && props.record) {
Object.assign(formData, {
name: props.record.name || '',
description: props.record.description || '',
room_id: props.record.room_id || '',
device_category: props.record.device_category || '',
agent_config: props.record.agent_config || '',
enabled: props.record.enabled ?? true,
collect_on: props.record.collect_on ?? true,
collect_interval: props.record.collect_interval || 60,
policy_ids: props.record.policy_ids || [],
})
} else {
Object.assign(formData, {
name: '',
description: '',
room_id: '',
device_category: '',
agent_config: '',
enabled: true,
collect_on: true,
collect_interval: 60,
policy_ids: [],
})
}
}
}
)
const handleOk = async () => {
try {
await formRef.value?.validate()
confirmLoading.value = true
if (isEdit.value) {
const updateData: RoomDeviceUpdateData = {
name: formData.name,
description: formData.description,
room_id: formData.room_id,
device_category: formData.device_category,
agent_config: formData.agent_config,
enabled: formData.enabled,
collect_on: formData.collect_on,
collect_interval: formData.collect_interval,
policy_ids: formData.policy_ids,
}
await updateRoomDevice(props.record.id, updateData)
Message.success('更新成功')
} else {
const createData: RoomDeviceCreateData = {
name: formData.name,
description: formData.description,
room_id: formData.room_id,
device_category: formData.device_category,
agent_config: formData.agent_config,
enabled: formData.enabled,
collect_on: formData.collect_on,
collect_interval: formData.collect_interval,
policy_ids: formData.policy_ids,
}
await createRoomDevice(createData)
Message.success('创建成功')
}
emit('success')
handleCancel()
} catch (error) {
console.error('操作失败:', error)
Message.error('操作失败')
} finally {
confirmLoading.value = false
}
}
const handleUpdateVisible = (value: boolean) => {
emit('update:visible', value)
}
const handleCancel = () => {
emit('update:visible', false)
formRef.value?.resetFields()
}
onMounted(() => {
loadPolicyOptions()
})
</script>

View File

@@ -0,0 +1,81 @@
<template>
<a-modal :visible="visible" title="采集配置" :mask-closable="false" :ok-loading="loading" @ok="handleSubmit" @cancel="handleCancel">
<a-form :model="form" layout="vertical">
<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="默认60秒" :min="10" :max="3600" style="width: 100%" allow-clear />
<template #extra>
<span style="color: #86909c">分桶间隔系统将按此间隔周期采集</span>
</template>
</a-form-item>
<a-form-item label="采集地址(HTTP/HTTPS GET)">
<a-input v-model="form.agent_config" placeholder="周期采集地址,返回 JSON 格式指标数组" />
<template #extra>
<span style="color: #86909c">采集地址应返回 JSON 格式的指标数据支持 {"metrics":[...]} 或数组形式</span>
</template>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { patchRoomDeviceCollect, type RoomDeviceCollectData } from '@/api/ops/room-device'
interface Props {
visible: boolean
record: any
}
const props = defineProps<Props>()
const emit = defineEmits(['update:visible', 'success'])
const loading = ref(false)
const form = ref({
collect_on: true,
collect_interval: 60,
agent_config: '',
})
watch(
() => props.visible,
(val) => {
if (val && props.record) {
form.value.collect_on = props.record.collect_on ?? true
form.value.collect_interval = props.record.collect_interval || 60
form.value.agent_config = props.record.agent_config || ''
}
}
)
const handleSubmit = async () => {
loading.value = true
try {
const data: RoomDeviceCollectData = {
collect_on: form.value.collect_on,
collect_interval: form.value.collect_interval,
agent_config: form.value.agent_config,
}
await patchRoomDeviceCollect(props.record.id, data)
Message.success('配置成功')
emit('success')
emit('update:visible', false)
} catch (error) {
Message.error('配置失败')
} finally {
loading.value = false
}
}
const handleCancel = () => {
emit('update:visible', false)
}
</script>

View File

@@ -0,0 +1,66 @@
import { DEVICE_CATEGORY_MAP } from '@/api/ops/room-device'
export const columns = [
{
dataIndex: 'id',
title: 'ID',
width: 80,
slotName: 'id',
},
{
dataIndex: 'name',
title: '服务名称',
width: 150,
},
{
dataIndex: 'room_id',
title: '机房ID',
width: 120,
},
{
dataIndex: 'device_category',
title: '设备分类',
width: 100,
render: ({ record }: any) => {
return DEVICE_CATEGORY_MAP[record.device_category] || record.device_category
},
},
{
dataIndex: 'agent_config',
title: '采集地址',
width: 200,
ellipsis: true,
tooltip: true,
},
{
dataIndex: 'enabled',
title: '启用状态',
width: 100,
slotName: 'enabled',
},
{
dataIndex: 'collect_on',
title: '数据采集',
width: 100,
slotName: 'data_collection',
},
{
dataIndex: 'collect_interval',
title: '采集间隔(秒)',
width: 120,
},
{
dataIndex: 'collect_last_result',
title: '采集结果',
width: 150,
ellipsis: true,
tooltip: true,
},
{
dataIndex: 'actions',
title: '操作',
width: 180,
fixed: 'right' as const,
slotName: 'actions',
},
]

View File

@@ -0,0 +1,31 @@
import type { FormItem } from '@/components/search-form/types'
import { DEVICE_CATEGORY_OPTIONS } from '@/api/ops/room-device'
export const searchFormConfig: FormItem[] = [
{
field: 'keyword',
label: '关键词',
type: 'input',
placeholder: '请输入服务名称',
span: 6,
},
{
field: 'enabled',
label: '启用状态',
type: 'select',
placeholder: '请选择启用状态',
options: [
{ label: '已启用', value: true },
{ label: '已禁用', value: false },
],
span: 6,
},
{
field: 'device_category',
label: '设备分类',
type: 'select',
placeholder: '请选择设备分类',
options: DEVICE_CATEGORY_OPTIONS,
span: 6,
},
]

View File

@@ -0,0 +1,263 @@
<template>
<div class="container">
<search-table
:form-model="formModel"
:form-items="formItems"
:data="tableData"
:columns="columns"
:loading="loading"
:pagination="pagination"
title="机房设备数据采集"
search-button-text="查询"
reset-button-text="重置"
@update:form-model="handleFormModelUpdate"
@search="handleSearch"
@reset="handleReset"
@page-change="handlePageChange"
@refresh="handleRefresh"
>
<template #toolbar-left>
<a-button type="primary" @click="handleAdd">
<template #icon>
<icon-plus />
</template>
新增设备
</a-button>
</template>
<template #id="{ record }">
{{ record.id }}
</template>
<template #enabled="{ record }">
<a-tag :color="record.enabled ? 'green' : 'gray'">
{{ record.enabled ? '已启用' : '已禁用' }}
</a-tag>
</template>
<template #data_collection="{ record }">
<a-tag :color="record.collect_on ? 'green' : 'gray'">
{{ record.collect_on ? '已启用' : '未启用' }}
</a-tag>
</template>
<template #actions="{ record }">
<a-space>
<a-dropdown trigger="hover">
<a-button type="primary" size="small">
管理
<icon-down />
</a-button>
<template #content>
<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="handleQuickConfig(record)">
<template #icon>
<icon-settings />
</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>
<FormDialog v-model:visible="formDialogVisible" :record="currentRecord" @success="handleFormSuccess" />
<QuickConfigDialog v-model:visible="quickConfigVisible" :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"
/>
</a-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { IconPlus, IconDown, IconEdit, IconDelete, IconEye, IconSettings } from '@arco-design/web-vue/es/icon'
import type { FormItem } from '@/components/search-form/types'
import SearchTable from '@/components/search-table/index.vue'
import { searchFormConfig } from './config/search-form'
import 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 { fetchRoomDeviceList, deleteRoomDevice, type RoomDeviceItem, type RoomDeviceListParams } from '@/api/ops/room-device'
const loading = ref(false)
const tableData = ref<RoomDeviceItem[]>([])
const formDialogVisible = ref(false)
const quickConfigVisible = ref(false)
const detailVisible = ref(false)
const currentRecord = ref<RoomDeviceItem | null>(null)
const formModel = ref({
keyword: '',
enabled: undefined as boolean | undefined,
device_category: undefined as string | undefined,
})
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
})
const formItems = computed<FormItem[]>(() => searchFormConfig)
const columns = computed(() => columnsConfig)
const fetchRoomDeviceData = async () => {
loading.value = true
try {
const params: RoomDeviceListParams = {
page: pagination.current,
size: pagination.pageSize,
keyword: formModel.value.keyword,
enabled: formModel.value.enabled,
device_category: formModel.value.device_category,
}
const response: any = await fetchRoomDeviceList(params)
if (response && response.details) {
tableData.value = response.details?.data || []
pagination.total = response.details?.total || 0
} else {
tableData.value = []
pagination.total = 0
}
} catch (error) {
console.error('获取机房设备列表失败:', error)
Message.error('获取机房设备列表失败')
tableData.value = []
pagination.total = 0
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.current = 1
fetchRoomDeviceData()
}
const handleFormModelUpdate = (value: any) => {
formModel.value = value
}
const handleReset = () => {
formModel.value = {
keyword: '',
enabled: undefined,
device_category: undefined,
}
pagination.current = 1
fetchRoomDeviceData()
}
const handlePageChange = (current: number) => {
pagination.current = current
fetchRoomDeviceData()
}
const handleRefresh = () => {
fetchRoomDeviceData()
Message.success('数据已刷新')
}
const handleAdd = () => {
currentRecord.value = null
formDialogVisible.value = true
}
const handleQuickConfig = (record: RoomDeviceItem) => {
currentRecord.value = record
quickConfigVisible.value = true
}
const handleEdit = (record: RoomDeviceItem) => {
currentRecord.value = record
formDialogVisible.value = true
}
const handleDetail = (record: RoomDeviceItem) => {
currentRecord.value = record
detailVisible.value = true
}
const handleDetailEdit = () => {
detailVisible.value = false
formDialogVisible.value = true
}
const handleDetailQuickConfig = () => {
detailVisible.value = false
quickConfigVisible.value = true
}
const handleDetailDelete = () => {
detailVisible.value = false
if (currentRecord.value) {
handleDelete(currentRecord.value)
}
}
const handleFormSuccess = () => {
fetchRoomDeviceData()
}
const handleDelete = (record: RoomDeviceItem) => {
Modal.confirm({
title: '确认删除',
content: `确认删除机房设备 "${record.name}" 吗?`,
onOk: async () => {
try {
await deleteRoomDevice(record.id)
Message.success('删除成功')
fetchRoomDeviceData()
} catch (error) {
console.error('删除机房设备失败:', error)
Message.error('删除失败')
}
},
})
}
fetchRoomDeviceData()
</script>
<script lang="ts">
export default {
name: 'DeviceCollectManagement',
}
</script>
<style scoped lang="less">
.container {
margin-top: 20px;
}
</style>

File diff suppressed because one or more lines are too long