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

@@ -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>