This commit is contained in:
2026-04-13 23:18:15 +08:00
parent f030f9c5c9
commit e84cb75dda
3 changed files with 876 additions and 986 deletions

View File

@@ -80,6 +80,8 @@ export interface HostHardwareDeviceUpsert {
snmp_collect_enabled?: boolean
redfish_collect_enabled?: boolean
collect_interval?: number
/** 是否启用监控调度 */
enabled?: boolean
}
export interface HostHardwareDeviceListPayload {
@@ -191,9 +193,7 @@ export function isHostHardwareApiSuccess(res: HostHardwareApiEnvelope | null | u
return false
}
export function unwrapHostHardwareDetails<T>(
res: (HostHardwareApiEnvelope<T> & { data?: T }) | null | undefined,
): T | null {
export function unwrapHostHardwareDetails<T>(res: (HostHardwareApiEnvelope<T> & { data?: T }) | null | undefined): T | null {
if (!res || !isHostHardwareApiSuccess(res)) return null
// 部分网关/SDK 将载荷放在 data 而非 details与 logs 等模块一致做兼容
return res.details ?? res.data ?? null
@@ -203,15 +203,13 @@ export function unwrapHostHardwareDetails<T>(
export function fetchHostHardwareLatestCollection(serverIdentity: string) {
return request.get<HostHardwareApiEnvelope<HostHardwareLatestCollectionPayload>>(
`${HW_PREFIX}/devices/by-server-identity/collection/latest`,
{ params: { server_identity: serverIdentity } },
{ params: { server_identity: serverIdentity } }
)
}
/** 设备详情(含最新一条 status */
export function fetchHostHardwareDevice(deviceId: string) {
return request.get<HostHardwareApiEnvelope<HostHardwareDeviceDetailPayload>>(
`${HW_PREFIX}/devices/${encodeURIComponent(deviceId)}`,
)
return request.get<HostHardwareApiEnvelope<HostHardwareDeviceDetailPayload>>(`${HW_PREFIX}/devices/${encodeURIComponent(deviceId)}`)
}
/** 分页列表(可按 type / status / asset_id 筛选) */
@@ -234,10 +232,7 @@ export function createHostHardwareDevice(data: HostHardwareDeviceUpsert) {
/** 更新设备(全量必填字段;密码留空则不修改) */
export function updateHostHardwareDevice(deviceId: string, data: HostHardwareDeviceUpsert) {
return request.put<HostHardwareApiEnvelope<HostHardwareDevice>>(
`${HW_PREFIX}/devices/${encodeURIComponent(deviceId)}`,
data,
)
return request.put<HostHardwareApiEnvelope<HostHardwareDevice>>(`${HW_PREFIX}/devices/${encodeURIComponent(deviceId)}`, data)
}
/** 异步立即采集 */
@@ -256,12 +251,7 @@ export function disableHostHardwareDevice(deviceId: string) {
}
/** 单指标历史曲线JWT */
export function fetchHostHardwareMetricHistory(
deviceId: string,
metricName: string,
startTime?: string,
endTime?: string,
) {
export function fetchHostHardwareMetricHistory(deviceId: string, metricName: string, startTime?: string, endTime?: string) {
return request.get<HostHardwareApiEnvelope<HostHardwareMetricHistoryPayload>>(
`${HW_PREFIX}/metrics/devices/${encodeURIComponent(deviceId)}/history`,
{
@@ -270,26 +260,19 @@ export function fetchHostHardwareMetricHistory(
...(startTime ? { start_time: startTime } : {}),
...(endTime ? { end_time: endTime } : {}),
},
},
}
)
}
/** 日汇总统计 */
export function fetchHostHardwareStatistics(
deviceId: string,
startDate?: string,
endDate?: string,
) {
return request.get<HostHardwareApiEnvelope<HostHardwareStatisticsRow[]>>(
`${HW_PREFIX}/metrics/statistics`,
{
export function fetchHostHardwareStatistics(deviceId: string, startDate?: string, endDate?: string) {
return request.get<HostHardwareApiEnvelope<HostHardwareStatisticsRow[]>>(`${HW_PREFIX}/metrics/statistics`, {
params: {
device_id: deviceId,
...(startDate ? { start_date: startDate } : {}),
...(endDate ? { end_date: endDate } : {}),
},
},
)
})
}
/**
@@ -297,7 +280,7 @@ export function fetchHostHardwareStatistics(
*/
export function normalizeHostHardwareMetrics(
status: HostHardwareStatus | null | undefined,
metricsFromApi: HostHardwareMetricsRow[] | null | undefined,
metricsFromApi: HostHardwareMetricsRow[] | null | undefined
): NormalizedHostHardwareMetric[] {
const fromApi = metricsFromApi ?? []
if (fromApi.length > 0) {

View File

@@ -11,10 +11,9 @@
unmount-on-close
@ok="handleSubmit"
@cancel="handleCancel"
@update:visible="(v: boolean) => emit('update:visible', v)"
>
<div class="modal-scroll">
<a-spin :loading="loading" style="width: 100%; min-height: 200px">
<a-spin :loading="loading" style="width: 98%; margin: 0 auto; min-height: 200px">
<div v-if="!loading && blockedNoIdentity" class="blocked-wrap">
<a-alert type="warning" show-icon>
当前服务器未配置唯一标识请先在编辑服务器中填写并保存以便与 DC-Hardware 带外设备关联
@@ -22,17 +21,22 @@
</div>
<template v-else-if="!loading">
<a-form ref="formRef" :model="form" :rules="rules" layout="vertical">
<a-collapse :default-active-key="['base', 'proto', 'collect']" :bordered="false">
<a-collapse-item key="base" header="基础信息">
<a-divider orientation="left">基础信息</a-divider>
<a-row :gutter="16">
<a-col :span="24">
<a-col :span="12">
<a-form-item label="管理 IPBMC / 带外)">
<span class="readonly-field">{{ managementIp || '—' }}</span>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item field="collect_interval" label="采集间隔(秒)">
<a-input-number v-model="form.collect_interval" :min="0" style="width: 100%" />
<template #extra>
<span class="form-extra">0 或留空表示使用服务默认间隔</span>
</template>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item field="description" label="备注">
<a-textarea v-model="form.description" :rows="2" placeholder="可选" allow-clear />
@@ -44,15 +48,10 @@
</a-form-item>
</a-col>
</a-row>
</a-collapse-item>
<a-collapse-item key="proto" header="采集协议与连接">
<a-divider orientation="left">采集协议与连接</a-divider>
<a-form-item field="protocol" label="协议" required>
<a-radio-group
:model-value="form.protocol"
type="button"
@update:model-value="onProtocolModelUpdate"
>
<a-radio-group :model-value="form.protocol" type="button" @update:model-value="onProtocolModelUpdate">
<a-radio value="ipmi">IPMI</a-radio>
<a-radio value="snmp">SNMP</a-radio>
<a-radio value="redfish">Redfish</a-radio>
@@ -62,7 +61,6 @@
</template>
</a-form-item>
<!-- IPMI / Redfish 共用账号 -->
<template v-if="form.protocol === 'ipmi' || form.protocol === 'redfish'">
<a-row :gutter="16">
<a-col :span="12">
@@ -139,11 +137,7 @@
<a-row :gutter="16">
<a-col :span="24">
<a-form-item field="redfish_base_url" label="Redfish 根 URL">
<a-input
v-model="form.redfish_base_url"
placeholder="留空则默认 https://{管理IP}/redfish/v1"
allow-clear
/>
<a-input v-model="form.redfish_base_url" placeholder="留空则默认 https://{管理IP}/redfish/v1" allow-clear />
</a-form-item>
</a-col>
<a-col :span="8">
@@ -164,52 +158,21 @@
</a-col>
</a-row>
</template>
</a-collapse-item>
<a-collapse-item key="collect" header="采集调度与扩展">
<a-divider orientation="left">扩展配置</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item field="collect_interval" label="采集间隔(秒)">
<a-input-number v-model="form.collect_interval" :min="0" style="width: 100%" />
<template #extra>
<span class="form-extra">0 或留空表示使用服务默认间隔YAML</span>
</template>
</a-form-item>
</a-col>
<a-col v-if="isEdit && hwEnabled !== undefined" :span="12">
<a-form-item label="监控开关(只读)">
<a-space>
<a-tag :color="hwEnabled ? 'green' : 'gray'">{{ hwEnabled ? '已启用' : '已禁用' }}</a-tag>
<span class="form-extra">保存后可用下方快捷操作切换</span>
</a-space>
<a-form-item label="启用监控">
<a-switch v-model="form.enabled" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item field="extra_config" label="扩展配置JSON 字符串)">
<a-textarea
v-model="form.extra_config"
:rows="4"
placeholder='可选,非空须为合法 JSON如 {"key":"value"}'
allow-clear
/>
<a-textarea v-model="form.extra_config" :rows="4" placeholder='可选,非空须为合法 JSON如 {"key":"value"}' allow-clear />
</a-form-item>
</a-col>
</a-row>
</a-collapse-item>
</a-collapse>
<a-divider v-if="isEdit && deviceId" orientation="left">快捷操作</a-divider>
<a-space v-if="isEdit && deviceId" wrap>
<a-button type="outline" :loading="actionLoading === 'collect'" @click="doCollect">
立即采集
</a-button>
<a-button v-if="!hwEnabled" type="outline" status="success" :loading="actionLoading === 'enable'" @click="doEnable">
启用监控
</a-button>
<a-button v-else type="outline" status="warning" :loading="actionLoading === 'disable'" @click="doDisable">
禁用监控
</a-button>
</a-space>
</a-form>
</template>
</a-spin>
@@ -228,9 +191,6 @@ import {
fetchHostHardwareDeviceList,
createHostHardwareDevice,
updateHostHardwareDevice,
triggerHostHardwareCollect,
enableHostHardwareDevice,
disableHostHardwareDevice,
isHostHardwareApiSuccess,
unwrapHostHardwareDetails,
type HostHardwareDevice,
@@ -250,8 +210,6 @@ const loading = ref(false)
const submitLoading = ref(false)
const blockedNoIdentity = ref(false)
const deviceId = ref<string | null>(null)
const hwEnabled = ref<boolean | undefined>(undefined)
const actionLoading = ref<'collect' | 'enable' | 'disable' | ''>('')
/** 防止多次触发 loadHardware 时,较早返回的请求覆盖用户已切换的协议/表单 */
let hardwareLoadGeneration = 0
@@ -285,13 +243,9 @@ const managementIp = computed(() => {
const hardwareType = ref('server')
const hardwareAssetId = ref('')
const passwordPlaceholder = computed(() =>
isEdit.value ? '留空则不修改已保存的密码' : 'BMC / Redfish 密码',
)
const passwordPlaceholder = computed(() => (isEdit.value ? '留空则不修改已保存的密码' : 'BMC / Redfish 密码'))
const passwordHint = computed(() =>
isEdit.value ? '编辑时留空将保留原密码' : '按机房安全要求妥善保管',
)
const passwordHint = computed(() => (isEdit.value ? '编辑时留空将保留原密码' : '按机房安全要求妥善保管'))
const form = reactive({
protocol: 'ipmi' as 'ipmi' | 'snmp' | 'redfish',
@@ -313,6 +267,7 @@ const form = reactive({
snmp_collect_enabled: true,
redfish_collect_enabled: true,
collect_interval: 0,
enabled: true,
})
const rules: Record<string, FieldRule | FieldRule[]> = {
@@ -341,6 +296,7 @@ function resetForm() {
form.snmp_collect_enabled = true
form.redfish_collect_enabled = true
form.collect_interval = 0
form.enabled = true
}
function applyFromServer(r: ServerItem) {
@@ -372,7 +328,7 @@ function applyFromDevice(d: HostHardwareDevice) {
form.snmp_collect_enabled = d.snmp_collect_enabled !== false
form.redfish_collect_enabled = d.redfish_collect_enabled !== false
form.collect_interval = d.collect_interval ?? 0
hwEnabled.value = d.enabled
form.enabled = d.enabled !== false
}
function validateExtraConfigJson(): boolean {
@@ -417,6 +373,7 @@ function buildPayload(): HostHardwareDeviceUpsert {
snmp_collect_enabled: form.snmp_collect_enabled,
redfish_collect_enabled: form.redfish_collect_enabled,
collect_interval: form.collect_interval,
enabled: form.enabled,
}
const pw = form.password?.trim()
if (pw) {
@@ -454,7 +411,6 @@ async function loadDeviceByServerIdentityFallback(sid: string, gen: number): Pro
async function loadHardware() {
const gen = ++hardwareLoadGeneration
deviceId.value = null
hwEnabled.value = undefined
blockedNoIdentity.value = false
if (!props.record) {
return
@@ -474,6 +430,7 @@ async function loadHardware() {
if (isHostHardwareApiSuccess(colRes)) {
const col = unwrapHostHardwareDetails(colRes)
const did = col?.device_id
deviceId.value = col?.device_id
if (did) {
const detailOk = await loadDeviceDetailIntoForm(did, gen)
if (gen !== hardwareLoadGeneration) return
@@ -516,7 +473,7 @@ watch(
deviceId.value = null
blockedNoIdentity.value = false
}
},
}
)
async function handleSubmit() {
@@ -531,6 +488,7 @@ async function handleSubmit() {
if (!validateExtraConfigJson()) return
const r = props.record
console.log('r,', r)
if (!(r?.name || '').trim()) {
Message.error('服务器名称为空,请先在编辑服务器中填写名称')
return
@@ -541,13 +499,14 @@ async function handleSubmit() {
}
submitLoading.value = true
console.log('deviceId.value,', deviceId.value)
try {
const payload = buildPayload()
let res: { code?: number | string; message?: string }
if (deviceId.value) {
res = await updateHostHardwareDevice(deviceId.value, payload) as typeof res
res = (await updateHostHardwareDevice(deviceId.value, payload)) as typeof res
} else {
res = await createHostHardwareDevice(payload) as typeof res
res = (await createHostHardwareDevice(payload)) as typeof res
}
if (isHostHardwareApiSuccess(res)) {
Message.success(deviceId.value ? '修改成功' : '保存成功')
@@ -557,8 +516,9 @@ async function handleSubmit() {
Message.error((res as { message?: string }).message || '保存失败')
}
} catch (e: unknown) {
const msg = (e as { response?: { data?: { message?: string } }; message?: string })?.response?.data?.message
|| (e as { message?: string })?.message
const msg =
(e as { response?: { data?: { message?: string } }; message?: string })?.response?.data?.message ||
(e as { message?: string })?.message
Message.error(msg || '保存失败')
} finally {
submitLoading.value = false
@@ -568,59 +528,6 @@ async function handleSubmit() {
function handleCancel() {
emit('update:visible', false)
}
async function doCollect() {
if (!deviceId.value) return
actionLoading.value = 'collect'
try {
const res = await triggerHostHardwareCollect(deviceId.value)
if (isHostHardwareApiSuccess(res)) {
Message.success('采集任务已启动')
} else {
Message.error((res as { message?: string }).message || '操作失败')
}
} catch {
Message.error('请求失败')
} finally {
actionLoading.value = ''
}
}
async function doEnable() {
if (!deviceId.value) return
actionLoading.value = 'enable'
try {
const res = await enableHostHardwareDevice(deviceId.value)
if (isHostHardwareApiSuccess(res)) {
Message.success('已启用监控')
hwEnabled.value = true
} else {
Message.error((res as { message?: string }).message || '操作失败')
}
} catch {
Message.error('请求失败')
} finally {
actionLoading.value = ''
}
}
async function doDisable() {
if (!deviceId.value) return
actionLoading.value = 'disable'
try {
const res = await disableHostHardwareDevice(deviceId.value)
if (isHostHardwareApiSuccess(res)) {
Message.success('已禁用监控')
hwEnabled.value = false
} else {
Message.error((res as { message?: string }).message || '操作失败')
}
} catch {
Message.error('请求失败')
} finally {
actionLoading.value = ''
}
}
</script>
<script lang="ts">

File diff suppressed because one or more lines are too long