feat
This commit is contained in:
50
src/api/ops/alertLevel.ts
Normal file
50
src/api/ops/alertLevel.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取告警级别列表(分页) */
|
||||
export const fetchAlertLevelList = (data?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
enabled?: string;
|
||||
}) => {
|
||||
return request.get("/Alert/v1/severity/list", data || {});
|
||||
};
|
||||
|
||||
/** 获取告警级别详情 */
|
||||
export const fetchAlertLevelDetail = (id: number) => {
|
||||
return request.get(`/Alert/v1/severity/get/${id}`);
|
||||
};
|
||||
|
||||
/** 创建告警级别 */
|
||||
export const createAlertLevel = (data: {
|
||||
name: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
color: string;
|
||||
icon?: string;
|
||||
priority?: number;
|
||||
enabled?: boolean;
|
||||
config?: string;
|
||||
}) => {
|
||||
return request.post("/Alert/v1/severity/create", data);
|
||||
};
|
||||
|
||||
/** 更新告警级别 */
|
||||
export const updateAlertLevel = (data: {
|
||||
id: number;
|
||||
name?: string;
|
||||
code?: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
priority?: number;
|
||||
enabled?: boolean;
|
||||
config?: string;
|
||||
}) => {
|
||||
return request.post("/Alert/v1/severity/update", data);
|
||||
};
|
||||
|
||||
/** 删除告警级别 */
|
||||
export const deleteAlertLevel = (id: number) => {
|
||||
return request.delete(`/Alert/v1/severity/delete/${id}`);
|
||||
};
|
||||
58
src/api/ops/noticeChannel.ts
Normal file
58
src/api/ops/noticeChannel.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取通知渠道列表(分页) */
|
||||
export const fetchNoticeChannelList = (data?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
type?: string;
|
||||
}) => {
|
||||
return request.get("/Alert/v1/channel/list", { params: data || {} });
|
||||
};
|
||||
|
||||
/** 获取通知渠道详情 */
|
||||
export const fetchNoticeChannelDetail = (id: number) => {
|
||||
return request.get(`/Alert/v1/channel/get/${id}`);
|
||||
};
|
||||
|
||||
/** 创建通知渠道 */
|
||||
export const createNoticeChannel = (data: {
|
||||
name: string;
|
||||
type: string;
|
||||
config: string;
|
||||
message_template?: string;
|
||||
rate_limit?: number;
|
||||
rate_limit_window?: number;
|
||||
severity_filter?: string;
|
||||
quiet_hours?: string;
|
||||
retry_times?: number;
|
||||
retry_interval?: number;
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
}) => {
|
||||
return request.post("/Alert/v1/channel/create", data);
|
||||
};
|
||||
|
||||
/** 更新通知渠道 */
|
||||
export const updateNoticeChannel = (data: {
|
||||
id: number;
|
||||
name?: string;
|
||||
type?: string;
|
||||
config?: string;
|
||||
message_template?: string;
|
||||
rate_limit?: number;
|
||||
rate_limit_window?: number;
|
||||
severity_filter?: string;
|
||||
quiet_hours?: string;
|
||||
retry_times?: number;
|
||||
retry_interval?: number;
|
||||
enabled?: boolean;
|
||||
description?: string;
|
||||
}) => {
|
||||
return request.post("/Alert/v1/channel/update", data);
|
||||
};
|
||||
|
||||
/** 删除通知渠道 */
|
||||
export const deleteNoticeChannel = (id: number) => {
|
||||
return request.delete(`/Alert/v1/channel/delete/${id}`);
|
||||
};
|
||||
51
src/api/ops/rack.ts
Normal file
51
src/api/ops/rack.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
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;
|
||||
}) => {
|
||||
return request.post("/Assets/v1/rack/list", data || {});
|
||||
};
|
||||
|
||||
/** 获取机柜详情 */
|
||||
export const fetchRackDetail = (id: number) => {
|
||||
return request.get(`/Assets/v1/rack/detail/${id}`);
|
||||
};
|
||||
|
||||
/** 创建机柜 */
|
||||
export const createRack = (data: any) => {
|
||||
return request.post("/Assets/v1/rack/create", data);
|
||||
};
|
||||
|
||||
/** 更新机柜 */
|
||||
export const updateRack = (data: any) => {
|
||||
return request.put("/Assets/v1/rack/update", data);
|
||||
};
|
||||
|
||||
/** 删除机柜 */
|
||||
export const deleteRack = (id: number) => {
|
||||
return request.delete(`/Assets/v1/rack/delete/${id}`);
|
||||
};
|
||||
|
||||
/** 获取供应商列表(用于下拉选择) */
|
||||
export const fetchSupplierList = () => {
|
||||
return request.get("/Assets/v1/supplier/all");
|
||||
};
|
||||
|
||||
/** 获取数据中心列表(用于下拉选择) */
|
||||
export const fetchDatacenterList = () => {
|
||||
return request.get("/Assets/v1/datacenter/all");
|
||||
};
|
||||
|
||||
/** 获取楼层列表(用于下拉选择) */
|
||||
export const fetchFloorListForSelect = (datacenterId?: number) => {
|
||||
return request.get("/Assets/v1/floor/all", { params: { datacenter_id: datacenterId } });
|
||||
};
|
||||
64
src/api/ops/unit.ts
Normal file
64
src/api/ops/unit.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 获取机柜U位状态 */
|
||||
export const fetchRackUnits = (rackId: number) => {
|
||||
return request.get(`/Assets/v1/unit/rack/${rackId}`);
|
||||
};
|
||||
|
||||
/** 获取U位列表(别名) */
|
||||
export const fetchUnitList = fetchRackUnits;
|
||||
|
||||
/** 分配U位 */
|
||||
export const allocateUnit = (data: {
|
||||
rack_id: number;
|
||||
start_unit: number;
|
||||
occupied_units: number;
|
||||
asset_id?: number;
|
||||
asset_code?: string;
|
||||
asset_name: string;
|
||||
asset_type: string;
|
||||
power_consumption?: number;
|
||||
description?: string;
|
||||
}) => {
|
||||
return request.post("/Assets/v1/unit/allocate", data);
|
||||
};
|
||||
|
||||
/** 释放U位 */
|
||||
export const releaseUnit = (data: {
|
||||
rack_id: number;
|
||||
start_unit: number;
|
||||
end_unit: number;
|
||||
}) => {
|
||||
return request.post("/Assets/v1/unit/release", data);
|
||||
};
|
||||
|
||||
/** 预留U位 */
|
||||
export const reserveUnit = (data: {
|
||||
rack_id: number;
|
||||
start_unit: number;
|
||||
occupied_units: number;
|
||||
reserved_for: string;
|
||||
reserved_until: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
return request.post("/Assets/v1/unit/reserve", data);
|
||||
};
|
||||
|
||||
/** 取消预留 */
|
||||
export const cancelReservation = (data: {
|
||||
rack_id: number;
|
||||
start_unit: number;
|
||||
end_unit: number;
|
||||
}) => {
|
||||
return request.post("/Assets/v1/unit/cancel_reservation", data);
|
||||
};
|
||||
|
||||
/** 更新U位状态 */
|
||||
export const updateUnitStatus = (data: {
|
||||
rack_id: number;
|
||||
start_unit: number;
|
||||
end_unit: number;
|
||||
status: "available" | "disabled";
|
||||
}) => {
|
||||
return request.put("/Assets/v1/unit/status", data);
|
||||
};
|
||||
147
src/views/ops/pages/alert/level/components/LevelDetailDialog.vue
Normal file
147
src/views/ops/pages/alert/level/components/LevelDetailDialog.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="告警级别详情"
|
||||
width="600px"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:footer="false"
|
||||
>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-descriptions :column="2" bordered v-if="levelDetail">
|
||||
<a-descriptions-item label="级别名称" :span="2">
|
||||
{{ levelDetail.name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="级别代码">
|
||||
{{ levelDetail.code }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="优先级">
|
||||
{{ levelDetail.priority }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="颜色">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div
|
||||
:style="{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
backgroundColor: levelDetail.color,
|
||||
border: '1px solid var(--color-border-2)',
|
||||
borderRadius: '4px'
|
||||
}"
|
||||
></div>
|
||||
<span>{{ levelDetail.color }}</span>
|
||||
</div>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="图标">
|
||||
{{ levelDetail.icon || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态" :span="2">
|
||||
<a-tag :color="levelDetail.enabled ? 'green' : 'red'">
|
||||
{{ levelDetail.enabled ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="系统级别" :span="2">
|
||||
<a-tag :color="levelDetail.is_default ? 'blue' : 'gray'">
|
||||
{{ levelDetail.is_default ? '是' : '否' }}
|
||||
</a-tag>
|
||||
<span v-if="levelDetail.is_default" style="color: var(--color-text-3); font-size: 12px; margin-left: 8px;">
|
||||
(系统预置的默认级别)
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="描述" :span="2">
|
||||
{{ levelDetail.description || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ formatDate(levelDetail.created_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">
|
||||
{{ formatDate(levelDetail.updated_at) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { fetchAlertLevelDetail } from '@/api/ops/alertLevel'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
levelId?: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const loading = ref(false)
|
||||
const levelDetail = ref<any>(null)
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
// 加载告警级别详情
|
||||
const loadLevelDetail = async () => {
|
||||
if (!props.levelId) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await fetchAlertLevelDetail(props.levelId)
|
||||
if (res.code === 0) {
|
||||
levelDetail.value = res.details
|
||||
} else {
|
||||
Message.error(res.message || '获取告警级别详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取告警级别详情失败:', error)
|
||||
Message.error('获取告警级别详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal && props.levelId) {
|
||||
loadLevelDetail()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'LevelDetailDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
// 样式可以根据需要添加
|
||||
</style>
|
||||
248
src/views/ops/pages/alert/level/components/LevelFormDialog.vue
Normal file
248
src/views/ops/pages/alert/level/components/LevelFormDialog.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑告警级别' : '新建告警级别'"
|
||||
width="600px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:confirm-loading="submitting"
|
||||
>
|
||||
<a-form :model="form" layout="vertical" ref="formRef">
|
||||
<a-form-item
|
||||
label="级别名称"
|
||||
field="name"
|
||||
:rules="[{ required: true, message: '请输入级别名称' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入级别名称,如:紧急、严重、警告、提示"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="级别代码(英文)"
|
||||
field="code"
|
||||
:rules="[
|
||||
{ required: true, message: '请输入级别代码' },
|
||||
{ validator: validateCode }
|
||||
]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.code"
|
||||
placeholder="请输入级别代码,如:critical、warning、info"
|
||||
:max-length="50"
|
||||
:disabled="isEdit && level?.is_default"
|
||||
/>
|
||||
<template v-if="isEdit && level?.is_default">
|
||||
<div style="color: var(--color-text-3); font-size: 12px; margin-top: 4px;">
|
||||
默认级别的代码不可修改
|
||||
</div>
|
||||
</template>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="颜色"
|
||||
field="color"
|
||||
:rules="[{ required: true, message: '请选择颜色' }]"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<input
|
||||
type="color"
|
||||
v-model="form.color"
|
||||
style="width: 60px; height: 32px; cursor: pointer; border: 1px solid var(--color-border-2); border-radius: 4px;"
|
||||
/>
|
||||
<a-input
|
||||
v-model="form.color"
|
||||
placeholder="请输入颜色值,如:#FF5722"
|
||||
style="flex: 1;"
|
||||
/>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="图标"
|
||||
field="icon"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.icon"
|
||||
placeholder="请输入图标名称或图标类名"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="是否启用"
|
||||
field="enabled"
|
||||
>
|
||||
<a-switch v-model="form.enabled">
|
||||
<template #checked>启用</template>
|
||||
<template #unchecked>禁用</template>
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="描述"
|
||||
field="description"
|
||||
>
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入级别描述"
|
||||
:auto-size="{ minRows: 4, maxRows: 8 }"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { createAlertLevel, updateAlertLevel } from '@/api/ops/alertLevel'
|
||||
|
||||
interface AlertLevel {
|
||||
id?: number
|
||||
name?: string
|
||||
code?: string
|
||||
description?: string
|
||||
color?: string
|
||||
icon?: string
|
||||
priority?: number
|
||||
enabled?: boolean
|
||||
is_default?: boolean
|
||||
config?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
level: AlertLevel | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
|
||||
// 验证器函数:验证级别代码格式
|
||||
const validateCode = (value: string, callback: (error?: string) => void) => {
|
||||
if (!/^[a-z0-9_-]+$/.test(value)) {
|
||||
callback('级别代码只能包含小写字母、数字、下划线和短横线')
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
color: '#FF5722',
|
||||
icon: '',
|
||||
enabled: true,
|
||||
priority: 0,
|
||||
})
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.level?.id)
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
if (props.level && isEdit.value) {
|
||||
// 编辑模式:填充表单
|
||||
form.value = {
|
||||
name: props.level.name || '',
|
||||
code: props.level.code || '',
|
||||
description: props.level.description || '',
|
||||
color: props.level.color || '#FF5722',
|
||||
icon: props.level.icon || '',
|
||||
enabled: props.level.enabled !== undefined ? props.level.enabled : true,
|
||||
priority: props.level.priority || 0,
|
||||
}
|
||||
} else {
|
||||
// 新建模式:重置表单
|
||||
form.value = {
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
color: '#FF5722',
|
||||
icon: '',
|
||||
enabled: true,
|
||||
priority: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 确认提交
|
||||
const handleOk = async () => {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (valid) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const data: any = {
|
||||
name: form.value.name,
|
||||
code: form.value.code,
|
||||
description: form.value.description,
|
||||
color: form.value.color,
|
||||
icon: form.value.icon,
|
||||
enabled: form.value.enabled,
|
||||
}
|
||||
|
||||
let res
|
||||
if (isEdit.value && props.level?.id) {
|
||||
// 编辑告警级别
|
||||
data.id = props.level.id
|
||||
res = await updateAlertLevel(data)
|
||||
} else {
|
||||
// 新建告警级别
|
||||
res = await createAlertLevel(data)
|
||||
}
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success(isEdit.value ? '编辑成功' : '创建成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} else {
|
||||
Message.error(res.message || (isEdit.value ? '编辑失败' : '创建失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error(isEdit.value ? '编辑失败' : '创建失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'LevelFormDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
</style>
|
||||
47
src/views/ops/pages/alert/level/config/columns.ts
Normal file
47
src/views/ops/pages/alert/level/config/columns.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
export const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '级别名称',
|
||||
dataIndex: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '图标',
|
||||
dataIndex: 'icon',
|
||||
slotName: 'icon',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '颜色标识',
|
||||
dataIndex: 'color',
|
||||
slotName: 'color',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'enabled',
|
||||
slotName: 'enabled',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
slotName: 'actions',
|
||||
width: 240,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
]
|
||||
20
src/views/ops/pages/alert/level/config/search-form.ts
Normal file
20
src/views/ops/pages/alert/level/config/search-form.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
export const searchFormConfig: FormItem[] = [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入级别名称、代码或描述',
|
||||
},
|
||||
{
|
||||
field: 'enabled',
|
||||
label: '状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: [
|
||||
{ label: '启用', value: 'true' },
|
||||
{ label: '禁用', value: 'false' },
|
||||
],
|
||||
},
|
||||
]
|
||||
262
src/views/ops/pages/alert/level/index.vue
Normal file
262
src/views/ops/pages/alert/level/index.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<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="handleCreate">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新建级别
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 颜色标识 -->
|
||||
<template #color="{ record }">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div
|
||||
:style="{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
backgroundColor: record.color,
|
||||
border: '1px solid var(--color-border-2)',
|
||||
borderRadius: '4px'
|
||||
}"
|
||||
></div>
|
||||
<span>{{ record.color }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 状态 -->
|
||||
<template #enabled="{ record }">
|
||||
<a-tag :color="record.enabled ? 'green' : 'red'">
|
||||
{{ record.enabled ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
status="danger"
|
||||
@click="handleDelete(record)"
|
||||
:disabled="record.is_default"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 告警级别表单对话框(新建/编辑) -->
|
||||
<level-form-dialog
|
||||
v-model:visible="formVisible"
|
||||
:level="editingLevel"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 告警级别详情对话框 -->
|
||||
<level-detail-dialog
|
||||
v-model:visible="detailVisible"
|
||||
:level-id="currentLevelId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus } 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 { columns as columnsConfig } from './config/columns'
|
||||
import {
|
||||
fetchAlertLevelList,
|
||||
deleteAlertLevel,
|
||||
} from '@/api/ops/alertLevel'
|
||||
import LevelDetailDialog from './components/LevelDetailDialog.vue'
|
||||
import LevelFormDialog from './components/LevelFormDialog.vue'
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
enabled: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => columnsConfig)
|
||||
|
||||
// 当前选中的级别
|
||||
const currentLevelId = ref<number | undefined>(undefined)
|
||||
const editingLevel = ref<any>(null)
|
||||
|
||||
// 对话框可见性
|
||||
const formVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
|
||||
// 获取告警级别列表
|
||||
const fetchLevels = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
keyword: formModel.value.keyword || undefined,
|
||||
enabled: formModel.value.enabled || undefined,
|
||||
}
|
||||
|
||||
const res = await fetchAlertLevelList(params)
|
||||
|
||||
tableData.value = res.details?.data || []
|
||||
pagination.total = res.details?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取告警级别列表失败:', error)
|
||||
Message.error('获取告警级别列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchLevels()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = value
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
enabled: '',
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchLevels()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchLevels()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchLevels()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新建级别
|
||||
const handleCreate = () => {
|
||||
editingLevel.value = null
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑级别
|
||||
const handleEdit = (record: any) => {
|
||||
console.log('编辑告警级别:', record)
|
||||
editingLevel.value = record
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 详情
|
||||
const handleDetail = (record: any) => {
|
||||
console.log('查看详情:', record)
|
||||
currentLevelId.value = record.id
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 删除级别
|
||||
const handleDelete = async (record: any) => {
|
||||
console.log('删除告警级别:', record)
|
||||
|
||||
// 系统默认级别不允许删除
|
||||
if (record.is_default) {
|
||||
Message.warning('系统默认级别不允许删除')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确认删除告警级别 ${record.name} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await deleteAlertLevel(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('删除成功')
|
||||
fetchLevels()
|
||||
} else {
|
||||
Message.error(res.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除告警级别失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
formVisible.value = false
|
||||
fetchLevels()
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
fetchLevels()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AlertLevelManagement',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="通知渠道详情"
|
||||
width="800px"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:footer="false"
|
||||
>
|
||||
<a-descriptions
|
||||
v-if="channelDetail"
|
||||
:column="2"
|
||||
bordered
|
||||
size="large"
|
||||
>
|
||||
<a-descriptions-item label="渠道名称">
|
||||
{{ channelDetail.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="渠道类型">
|
||||
<a-tag :color="getChannelTypeColor(channelDetail.type)">
|
||||
{{ getChannelTypeName(channelDetail.type) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="状态" :span="2">
|
||||
<a-tag :color="channelDetail.enabled ? 'green' : 'red'">
|
||||
{{ channelDetail.enabled ? '开启' : '关闭' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="消息模板" :span="2">
|
||||
<div class="template-content">
|
||||
{{ channelDetail.message_template || '-' }}
|
||||
</div>
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="限流次数">
|
||||
{{ channelDetail.rate_limit === 0 ? '不限制' : channelDetail.rate_limit || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="限流窗口(秒)">
|
||||
{{ channelDetail.rate_limit_window || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="告警级别过滤" :span="2">
|
||||
<a-space wrap>
|
||||
<a-tag
|
||||
v-for="(level, index) in getSeverityLevels(channelDetail.severity_filter)"
|
||||
:key="index"
|
||||
>
|
||||
{{ level }}
|
||||
</a-tag>
|
||||
<span v-if="!channelDetail.severity_filter">-</span>
|
||||
</a-space>
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="静默时间段" :span="2">
|
||||
<div v-if="getQuietHoursDisplay()">
|
||||
<a-tag color="blue">已启用</a-tag>
|
||||
<span style="margin-left: 8px">
|
||||
{{ getQuietHoursDisplay() }}
|
||||
</span>
|
||||
</div>
|
||||
<a-tag v-else color="gray">未启用</a-tag>
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="重试次数">
|
||||
{{ channelDetail.retry_times || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="重试间隔(秒)">
|
||||
{{ channelDetail.retry_interval || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="描述" :span="2">
|
||||
<div class="description-content">
|
||||
{{ channelDetail.description || '-' }}
|
||||
</div>
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ formatTime(channelDetail.created_at) || '-' }}
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="更新时间">
|
||||
{{ formatTime(channelDetail.updated_at) || '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-spin v-else :loading="loading" style="display: block; padding: 60px 0;">
|
||||
<a-empty description="加载中..." />
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { fetchNoticeChannelDetail } from '@/api/ops/noticeChannel'
|
||||
|
||||
interface NoticeChannel {
|
||||
id?: number
|
||||
name?: string
|
||||
type?: string
|
||||
config?: string
|
||||
message_template?: string
|
||||
rate_limit?: number
|
||||
rate_limit_window?: number
|
||||
severity_filter?: string
|
||||
quiet_hours?: string
|
||||
retry_times?: number
|
||||
retry_interval?: number
|
||||
enabled?: boolean
|
||||
description?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
channelId: number | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const channelDetail = ref<NoticeChannel | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
// 渠道类型映射
|
||||
const channelTypeMap: Record<string, { name: string; color: string }> = {
|
||||
email: { name: '邮件', color: 'blue' },
|
||||
sms: { name: '短信', color: 'cyan' },
|
||||
webhook: { name: 'Webhook', color: 'purple' },
|
||||
dingtalk: { name: '钉钉', color: 'red' },
|
||||
wechat: { name: '企业微信', color: 'green' },
|
||||
feishu: { name: '飞书', color: 'orange' },
|
||||
slack: { name: 'Slack', color: 'arcoblue' },
|
||||
}
|
||||
|
||||
// 获取渠道类型名称
|
||||
const getChannelTypeName = (type?: string) => {
|
||||
return channelTypeMap[type || '']?.name || type || '-'
|
||||
}
|
||||
|
||||
// 获取渠道类型颜色
|
||||
const getChannelTypeColor = (type?: string) => {
|
||||
return channelTypeMap[type || '']?.color || 'gray'
|
||||
}
|
||||
|
||||
// 获取告警级别列表
|
||||
const getSeverityLevels = (filter?: string) => {
|
||||
if (!filter) return []
|
||||
return filter.split(',').filter((v) => v.trim())
|
||||
}
|
||||
|
||||
// 获取静默时间段显示
|
||||
const getQuietHoursDisplay = () => {
|
||||
if (!channelDetail.value?.quiet_hours) return ''
|
||||
try {
|
||||
const quietHoursData = JSON.parse(channelDetail.value.quiet_hours)
|
||||
if (quietHoursData.enabled) {
|
||||
return `${quietHoursData.start || '--:--'} 至 ${quietHoursData.end || '--:--'} (${quietHoursData.timezone || 'Asia/Shanghai'})`
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析静默时间段失败:', error)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time?: string) => {
|
||||
if (!time) return ''
|
||||
try {
|
||||
const date = new Date(time)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
} catch (error) {
|
||||
return time
|
||||
}
|
||||
}
|
||||
|
||||
// 获取渠道详情
|
||||
const fetchDetail = async () => {
|
||||
if (!props.channelId) {
|
||||
channelDetail.value = null
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchNoticeChannelDetail(props.channelId)
|
||||
if (res.code === 0) {
|
||||
channelDetail.value = res.details || res.data
|
||||
} else {
|
||||
Message.error(res.message || '获取详情失败')
|
||||
channelDetail.value = null
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error('获取详情失败')
|
||||
console.error(error)
|
||||
channelDetail.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal && props.channelId) {
|
||||
fetchDetail()
|
||||
} else {
|
||||
channelDetail.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ChannelDetailDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.template-content,
|
||||
.description-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,423 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑通知渠道' : '新建通知渠道'"
|
||||
width="800px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:confirm-loading="submitting"
|
||||
>
|
||||
<a-form :model="form" layout="vertical" ref="formRef">
|
||||
<a-form-item
|
||||
label="渠道名称"
|
||||
field="name"
|
||||
:rules="[{ required: true, message: '请输入渠道名称' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入渠道名称"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="渠道类型"
|
||||
field="type"
|
||||
:rules="[{ required: true, message: '请选择渠道类型' }]"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.type"
|
||||
placeholder="请选择渠道类型"
|
||||
>
|
||||
<a-option value="email">邮件</a-option>
|
||||
<a-option value="sms">短信</a-option>
|
||||
<a-option value="webhook">Webhook</a-option>
|
||||
<a-option value="dingtalk">钉钉</a-option>
|
||||
<a-option value="wechat">企业微信</a-option>
|
||||
<a-option value="feishu">飞书</a-option>
|
||||
<a-option value="slack">Slack</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="消息模板"
|
||||
field="message_template"
|
||||
>
|
||||
<a-textarea
|
||||
v-model="form.message_template"
|
||||
placeholder="请输入消息模板,支持变量:{{.Severity}}、{{.RuleName}}、{{.Summary}}、{{.FiredAt}}等"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
:max-length="1000"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="限流次数"
|
||||
field="rate_limit"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="form.rate_limit"
|
||||
placeholder="0表示不限制"
|
||||
:min="0"
|
||||
:max="10000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="限流窗口(秒)"
|
||||
field="rate_limit_window"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="form.rate_limit_window"
|
||||
placeholder="限流时间窗口"
|
||||
:min="1"
|
||||
:max="86400"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item
|
||||
label="告警级别过滤"
|
||||
field="severity_filter"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.severity_filter"
|
||||
placeholder="请选择告警级别(可多选)"
|
||||
multiple
|
||||
allow-clear
|
||||
>
|
||||
<a-option
|
||||
v-for="level in severityLevels"
|
||||
:key="level.code"
|
||||
:value="level.code"
|
||||
>
|
||||
{{ level.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="静默时间段"
|
||||
field="quiet_hours"
|
||||
>
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<a-switch
|
||||
v-model="quietHoursEnabled"
|
||||
@change="handleQuietHoursChange"
|
||||
>
|
||||
<template #checked>启用静默</template>
|
||||
<template #unchecked>禁用静默</template>
|
||||
</a-switch>
|
||||
<a-time-picker
|
||||
v-if="quietHoursEnabled"
|
||||
v-model="quietHoursStart"
|
||||
placeholder="开始时间"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
style="width: 180px"
|
||||
@change="handleQuietHoursChange"
|
||||
/>
|
||||
<span v-if="quietHoursEnabled">至</span>
|
||||
<a-time-picker
|
||||
v-if="quietHoursEnabled"
|
||||
v-model="quietHoursEnd"
|
||||
placeholder="结束时间"
|
||||
format="HH:mm"
|
||||
value-format="HH:mm"
|
||||
style="width: 180px"
|
||||
@change="handleQuietHoursChange"
|
||||
/>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="重试次数"
|
||||
field="retry_times"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="form.retry_times"
|
||||
placeholder="发送失败重试次数"
|
||||
:min="0"
|
||||
:max="10"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="重试间隔(秒)"
|
||||
field="retry_interval"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="form.retry_interval"
|
||||
placeholder="重试间隔时间"
|
||||
:min="1"
|
||||
:max="3600"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item
|
||||
label="是否开启"
|
||||
field="enabled"
|
||||
>
|
||||
<a-switch v-model="form.enabled">
|
||||
<template #checked>开启</template>
|
||||
<template #unchecked>关闭</template>
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="描述"
|
||||
field="description"
|
||||
>
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入渠道描述"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { createNoticeChannel, updateNoticeChannel } from '@/api/ops/noticeChannel'
|
||||
import { fetchAlertLevelList } from '@/api/ops/alertLevel'
|
||||
|
||||
interface NoticeChannel {
|
||||
id?: number
|
||||
name?: string
|
||||
type?: string
|
||||
config?: string
|
||||
message_template?: string
|
||||
rate_limit?: number
|
||||
rate_limit_window?: number
|
||||
severity_filter?: string
|
||||
quiet_hours?: string
|
||||
retry_times?: number
|
||||
retry_interval?: number
|
||||
enabled?: boolean
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
channel: NoticeChannel | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref()
|
||||
const submitting = ref(false)
|
||||
const severityLevels = ref<any[]>([])
|
||||
|
||||
// 静默时间段相关状态
|
||||
const quietHoursEnabled = ref(false)
|
||||
const quietHoursStart = ref('22:00')
|
||||
const quietHoursEnd = ref('08:00')
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
name: '',
|
||||
type: 'email',
|
||||
config: '{}',
|
||||
message_template: '',
|
||||
rate_limit: 0,
|
||||
rate_limit_window: 60,
|
||||
severity_filter: [] as string[],
|
||||
quiet_hours: '',
|
||||
retry_times: 3,
|
||||
retry_interval: 60,
|
||||
enabled: true,
|
||||
description: '',
|
||||
})
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.channel?.id)
|
||||
|
||||
// 获取告警级别列表
|
||||
const fetchSeverityLevels = async () => {
|
||||
try {
|
||||
const res = await fetchAlertLevelList({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
enabled: 'true',
|
||||
})
|
||||
if (res.code === 0 && res.details?.data) {
|
||||
severityLevels.value = res.details.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取告警级别列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理静默时间段变化
|
||||
const handleQuietHoursChange = () => {
|
||||
if (quietHoursEnabled.value) {
|
||||
const quietHoursData = {
|
||||
enabled: true,
|
||||
start: quietHoursStart.value,
|
||||
end: quietHoursEnd.value,
|
||||
timezone: 'Asia/Shanghai',
|
||||
}
|
||||
form.value.quiet_hours = JSON.stringify(quietHoursData)
|
||||
} else {
|
||||
form.value.quiet_hours = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
if (props.channel && isEdit.value) {
|
||||
// 编辑模式:填充表单
|
||||
form.value = {
|
||||
name: props.channel.name || '',
|
||||
type: props.channel.type || 'email',
|
||||
config: props.channel.config || '{}',
|
||||
message_template: props.channel.message_template || '',
|
||||
rate_limit: props.channel.rate_limit ?? 0,
|
||||
rate_limit_window: props.channel.rate_limit_window ?? 60,
|
||||
severity_filter: props.channel.severity_filter
|
||||
? props.channel.severity_filter.split(',').filter((v: string) => v)
|
||||
: [],
|
||||
quiet_hours: props.channel.quiet_hours || '',
|
||||
retry_times: props.channel.retry_times ?? 3,
|
||||
retry_interval: props.channel.retry_interval ?? 60,
|
||||
enabled: props.channel.enabled !== undefined ? props.channel.enabled : true,
|
||||
description: props.channel.description || '',
|
||||
}
|
||||
|
||||
// 解析静默时间段
|
||||
if (props.channel.quiet_hours) {
|
||||
try {
|
||||
const quietHoursData = JSON.parse(props.channel.quiet_hours)
|
||||
quietHoursEnabled.value = quietHoursData.enabled
|
||||
quietHoursStart.value = quietHoursData.start || '22:00'
|
||||
quietHoursEnd.value = quietHoursData.end || '08:00'
|
||||
} catch (error) {
|
||||
quietHoursEnabled.value = false
|
||||
}
|
||||
} else {
|
||||
quietHoursEnabled.value = false
|
||||
}
|
||||
} else {
|
||||
// 新建模式:重置表单
|
||||
form.value = {
|
||||
name: '',
|
||||
type: 'email',
|
||||
config: '{}',
|
||||
message_template: '',
|
||||
rate_limit: 0,
|
||||
rate_limit_window: 60,
|
||||
severity_filter: [],
|
||||
quiet_hours: '',
|
||||
retry_times: 3,
|
||||
retry_interval: 60,
|
||||
enabled: true,
|
||||
description: '',
|
||||
}
|
||||
quietHoursEnabled.value = false
|
||||
quietHoursStart.value = '22:00'
|
||||
quietHoursEnd.value = '08:00'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 确认提交
|
||||
const handleOk = async () => {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (valid) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const data: any = {
|
||||
name: form.value.name,
|
||||
type: form.value.type,
|
||||
config: form.value.config,
|
||||
message_template: form.value.message_template,
|
||||
rate_limit: form.value.rate_limit,
|
||||
rate_limit_window: form.value.rate_limit_window,
|
||||
severity_filter: Array.isArray(form.value.severity_filter)
|
||||
? form.value.severity_filter.join(',')
|
||||
: form.value.severity_filter,
|
||||
quiet_hours: form.value.quiet_hours,
|
||||
retry_times: form.value.retry_times,
|
||||
retry_interval: form.value.retry_interval,
|
||||
enabled: form.value.enabled,
|
||||
description: form.value.description,
|
||||
}
|
||||
|
||||
let res
|
||||
if (isEdit.value && props.channel?.id) {
|
||||
// 编辑通知渠道
|
||||
data.id = props.channel.id
|
||||
res = await updateNoticeChannel(data)
|
||||
} else {
|
||||
// 新建通知渠道
|
||||
res = await createNoticeChannel(data)
|
||||
}
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success(isEdit.value ? '编辑成功' : '创建成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} else {
|
||||
Message.error(res.message || (isEdit.value ? '编辑失败' : '创建失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error(isEdit.value ? '编辑失败' : '创建失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
|
||||
// 初始化加载告警级别
|
||||
onMounted(() => {
|
||||
fetchSeverityLevels()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ChannelFormDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
</style>
|
||||
38
src/views/ops/pages/alert/notice/config/columns.ts
Normal file
38
src/views/ops/pages/alert/notice/config/columns.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
export const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '渠道名称',
|
||||
dataIndex: 'name',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '渠道类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '限流次数',
|
||||
dataIndex: 'rate_limit',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '限流窗口(秒)',
|
||||
dataIndex: 'rate_limit_window',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
slotName: 'actions',
|
||||
width: 240,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
]
|
||||
25
src/views/ops/pages/alert/notice/config/search-form.ts
Normal file
25
src/views/ops/pages/alert/notice/config/search-form.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
export const searchFormConfig: FormItem[] = [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入渠道名称或描述',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
label: '渠道类型',
|
||||
type: 'select',
|
||||
placeholder: '请选择渠道类型',
|
||||
options: [
|
||||
{ label: '邮件', value: 'email' },
|
||||
{ label: '短信', value: 'sms' },
|
||||
{ label: 'Webhook', value: 'webhook' },
|
||||
{ label: '钉钉', value: 'dingtalk' },
|
||||
{ label: '企业微信', value: 'wechat' },
|
||||
{ label: '飞书', value: 'feishu' },
|
||||
{ label: 'Slack', value: 'slack' },
|
||||
],
|
||||
},
|
||||
]
|
||||
298
src/views/ops/pages/alert/notice/index.vue
Normal file
298
src/views/ops/pages/alert/notice/index.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<template>
|
||||
<div class="notice-channel-container">
|
||||
<SearchTable
|
||||
v-model:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="通知渠道管理"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<!-- 工具栏左侧按钮 -->
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleCreate">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新建渠道
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 表格自定义列:序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 表格自定义列:渠道类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="getChannelTypeColor(record.type)">
|
||||
{{ getChannelTypeName(record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 表格自定义列:操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" size="small" @click="handleDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
content="确认删除该通知渠道吗?"
|
||||
@ok="handleDelete(record)"
|
||||
>
|
||||
<a-button type="text" size="small" status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</SearchTable>
|
||||
|
||||
<!-- 表单对话框 -->
|
||||
<channel-form-dialog
|
||||
v-model:visible="formDialogVisible"
|
||||
:channel="currentChannel"
|
||||
@success="fetchList"
|
||||
/>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<channel-detail-dialog
|
||||
v-model:visible="detailDialogVisible"
|
||||
:channel-id="currentChannelId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import ChannelFormDialog from './components/ChannelFormDialog.vue'
|
||||
import ChannelDetailDialog from './components/ChannelDetailDialog.vue'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import { fetchNoticeChannelList, deleteNoticeChannel } from '@/api/ops/noticeChannel'
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 搜索表单数据
|
||||
const formModel = ref<Record<string, any>>({
|
||||
name: '',
|
||||
type: '',
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'name',
|
||||
label: '渠道名称',
|
||||
type: 'input',
|
||||
placeholder: '请输入渠道名称',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
label: '渠道类型',
|
||||
type: 'select',
|
||||
placeholder: '请选择渠道类型',
|
||||
options: [
|
||||
{ label: '邮件', value: 'email' },
|
||||
{ label: '短信', value: 'sms' },
|
||||
{ label: 'Webhook', value: 'webhook' },
|
||||
{ label: '钉钉', value: 'dingtalk' },
|
||||
{ label: '企业微信', value: 'wechat' },
|
||||
{ label: '飞书', value: 'feishu' },
|
||||
{ label: 'Slack', value: 'slack' },
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed<TableColumnData[]>(() => [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '渠道名称',
|
||||
dataIndex: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '渠道类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '限流次数',
|
||||
dataIndex: 'rate_limit',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '限流窗口',
|
||||
dataIndex: 'rate_limit_window',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
slotName: 'actions',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
},
|
||||
])
|
||||
|
||||
// 表单对话框
|
||||
const formDialogVisible = ref(false)
|
||||
const currentChannel = ref<any>(null)
|
||||
|
||||
// 详情对话框
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentChannelId = ref<number | null>(null)
|
||||
|
||||
// 渠道类型映射
|
||||
const channelTypeMap: Record<string, { name: string; color: string }> = {
|
||||
email: { name: '邮件', color: 'blue' },
|
||||
sms: { name: '短信', color: 'cyan' },
|
||||
webhook: { name: 'Webhook', color: 'purple' },
|
||||
dingtalk: { name: '钉钉', color: 'red' },
|
||||
wechat: { name: '企业微信', color: 'green' },
|
||||
feishu: { name: '飞书', color: 'orange' },
|
||||
slack: { name: 'Slack', color: 'arcoblue' },
|
||||
}
|
||||
|
||||
// 获取渠道类型名称
|
||||
const getChannelTypeName = (type?: string) => {
|
||||
return channelTypeMap[type || '']?.name || type || '-'
|
||||
}
|
||||
|
||||
// 获取渠道类型颜色
|
||||
const getChannelTypeColor = (type?: string) => {
|
||||
return channelTypeMap[type || '']?.color || 'gray'
|
||||
}
|
||||
|
||||
// 获取列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
...formModel.value,
|
||||
}
|
||||
|
||||
const res = await fetchNoticeChannelList(params)
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.details?.data || []
|
||||
pagination.total = res.details?.total || 0
|
||||
} else {
|
||||
Message.error(res.message || '获取列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error('获取列表失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
name: '',
|
||||
type: '',
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 新建
|
||||
const handleCreate = () => {
|
||||
currentChannel.value = null
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: any) => {
|
||||
currentChannel.value = { ...record }
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 详情
|
||||
const handleDetail = (record: any) => {
|
||||
currentChannelId.value = record.id
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (record: any) => {
|
||||
try {
|
||||
const res = await deleteNoticeChannel(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('删除成功')
|
||||
fetchList()
|
||||
} else {
|
||||
Message.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error('删除失败')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化加载
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'NoticeChannel',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.notice-channel-container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,386 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
title="机柜详情"
|
||||
width="900px"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:footer="false"
|
||||
>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<div v-if="rackDetail">
|
||||
<!-- 基础信息 -->
|
||||
<a-descriptions title="基础信息" :column="2" bordered>
|
||||
<a-descriptions-item label="机柜名称">{{
|
||||
rackDetail.name
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="机柜编码">{{
|
||||
rackDetail.code
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="所属中心">{{
|
||||
rackDetail.datacenter?.name || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="所属楼层">{{
|
||||
rackDetail.floor?.name || '-'
|
||||
}}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 规格参数 -->
|
||||
<a-descriptions title="规格参数" :column="2" bordered>
|
||||
<a-descriptions-item label="高度(U)">{{
|
||||
rackDetail.height || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="宽度(mm)">{{
|
||||
rackDetail.width || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="深度(mm)">{{
|
||||
rackDetail.depth || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="重量(kg)">{{
|
||||
rackDetail.weight || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="最大承重(kg)">{{
|
||||
rackDetail.max_load || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="电力容量(KW)">{{
|
||||
rackDetail.power_capacity || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="电源相位">{{
|
||||
rackDetail.power_phase || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="PDU数量">{{
|
||||
rackDetail.pdu_count || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="网络接入方式">{{
|
||||
rackDetail.network_access || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="交换机端口数">{{
|
||||
rackDetail.switch_ports || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="制冷方式">{{
|
||||
rackDetail.cooling_type || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="已用电力(KW)">{{
|
||||
rackDetail.used_power || '-'
|
||||
}}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- U位使用情况 -->
|
||||
<a-descriptions title="U位使用情况" :column="2" bordered>
|
||||
<a-descriptions-item label="总U位数">{{
|
||||
rackDetail.total_units || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="已用U位数">{{
|
||||
rackDetail.used_units || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="预留U位数">{{
|
||||
rackDetail.reserved_units || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="可用U位数">{{
|
||||
rackDetail.available_units || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="使用率">
|
||||
<a-progress
|
||||
:percent="(rackDetail.utilization_rate || 0)"
|
||||
:color="
|
||||
(rackDetail.utilization_rate || 0) > 80
|
||||
? '#f53f3f'
|
||||
: (rackDetail.utilization_rate || 0) > 50
|
||||
? '#ff7d00'
|
||||
: '#00b42a'
|
||||
"
|
||||
/>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 状态信息 -->
|
||||
<a-descriptions title="状态信息" :column="2" bordered>
|
||||
<a-descriptions-item label="机柜类型">
|
||||
<a-tag :color="getRackTypeColor(rackDetail.rack_type)">
|
||||
{{ getRackTypeText(rackDetail.rack_type) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="getStatusColor(rackDetail.status)">
|
||||
{{ getStatusText(rackDetail.status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="供应商">{{
|
||||
rackDetail.supplier?.name || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="启用状态">
|
||||
<a-tag :color="rackDetail.enabled ? 'green' : 'red'">
|
||||
{{ rackDetail.enabled ? '已启用' : '已禁用' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 制造商信息 -->
|
||||
<a-descriptions title="制造商信息" :column="2" bordered>
|
||||
<a-descriptions-item label="制造商">{{
|
||||
rackDetail.manufacturer || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="型号">{{
|
||||
rackDetail.model || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="序列号">{{
|
||||
rackDetail.serial_number || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="价格(元)">{{
|
||||
rackDetail.price || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="采购日期">{{
|
||||
rackDetail.purchase_date || '-'
|
||||
}}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 责任人信息 -->
|
||||
<a-descriptions title="责任人信息" :column="2" bordered>
|
||||
<a-descriptions-item label="负责人">{{
|
||||
rackDetail.owner || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">{{
|
||||
rackDetail.contact_phone || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="使用部门">{{
|
||||
rackDetail.department || '-'
|
||||
}}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 位置信息 -->
|
||||
<a-descriptions title="位置信息" :column="2" bordered>
|
||||
<a-descriptions-item label="行号">{{
|
||||
rackDetail.row || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="列号">{{
|
||||
rackDetail.column || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="X坐标">{{
|
||||
rackDetail.position_x || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="Y坐标">{{
|
||||
rackDetail.position_y || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="颜色标识">
|
||||
<span
|
||||
v-if="rackDetail.color"
|
||||
:style="{ backgroundColor: rackDetail.color }"
|
||||
class="color-preview"
|
||||
>
|
||||
{{ rackDetail.color }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<!-- 其他信息 -->
|
||||
<a-descriptions title="其他信息" :column="2" bordered>
|
||||
<a-descriptions-item label="描述" :span="2">
|
||||
{{ rackDetail.description || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="备注" :span="2">
|
||||
{{ rackDetail.remarks || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建人">{{
|
||||
rackDetail.created_by || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">{{
|
||||
rackDetail.created_at || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="更新人">{{
|
||||
rackDetail.updated_by || '-'
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">{{
|
||||
rackDetail.updated_at || '-'
|
||||
}}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { fetchRackDetail } from '@/api/ops/rack'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
interface RackDetail {
|
||||
id?: number
|
||||
name?: string
|
||||
code?: string
|
||||
datacenter_id?: number
|
||||
datacenter?: { id?: number; name?: string }
|
||||
floor_id?: number
|
||||
floor?: { id?: number; name?: string }
|
||||
height?: number
|
||||
width?: number
|
||||
depth?: number
|
||||
weight?: number
|
||||
max_load?: number
|
||||
power_capacity?: number
|
||||
used_power?: number
|
||||
power_phase?: string
|
||||
pdu_count?: number
|
||||
network_access?: string
|
||||
switch_ports?: number
|
||||
cooling_type?: string
|
||||
rack_type?: string
|
||||
status?: string
|
||||
supplier_id?: number
|
||||
supplier?: { id?: number; name?: string }
|
||||
manufacturer?: string
|
||||
model?: string
|
||||
serial_number?: string
|
||||
purchase_date?: string
|
||||
price?: number
|
||||
owner?: string
|
||||
contact_phone?: string
|
||||
department?: string
|
||||
row?: number
|
||||
column?: number
|
||||
position_x?: number
|
||||
position_y?: number
|
||||
color?: string
|
||||
enabled?: boolean
|
||||
description?: string
|
||||
remarks?: string
|
||||
total_units?: number
|
||||
used_units?: number
|
||||
reserved_units?: number
|
||||
available_units?: number
|
||||
utilization_rate?: number
|
||||
created_by?: string
|
||||
created_at?: string
|
||||
updated_by?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
rackId: number | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const loading = ref(false)
|
||||
const rackDetail = ref<RackDetail | null>(null)
|
||||
|
||||
// 获取机柜类型颜色
|
||||
const getRackTypeColor = (type?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
standard: 'blue',
|
||||
blade: 'cyan',
|
||||
network: 'purple',
|
||||
storage: 'orange',
|
||||
custom: 'gray',
|
||||
}
|
||||
return colorMap[type || ''] || 'gray'
|
||||
}
|
||||
|
||||
// 获取机柜类型文本
|
||||
const getRackTypeText = (type?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
standard: '标准机柜',
|
||||
blade: '刀片机柜',
|
||||
network: '网络机柜',
|
||||
storage: '存储机柜',
|
||||
custom: '定制机柜',
|
||||
}
|
||||
return textMap[type || ''] || '-'
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
idle: 'green',
|
||||
in_use: 'blue',
|
||||
reserved: 'orange',
|
||||
maintenance: 'red',
|
||||
offline: 'gray',
|
||||
}
|
||||
return colorMap[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
idle: '空闲',
|
||||
in_use: '使用中',
|
||||
reserved: '已预留',
|
||||
maintenance: '维护中',
|
||||
offline: '已下线',
|
||||
}
|
||||
return textMap[status || ''] || '-'
|
||||
}
|
||||
|
||||
// 加载机柜详情
|
||||
const loadRackDetail = async () => {
|
||||
if (!props.rackId) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await fetchRackDetail(props.rackId)
|
||||
if (res.code === 0) {
|
||||
rackDetail.value = res.details
|
||||
} else {
|
||||
Message.error(res.message || '获取机柜详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取机柜详情失败:', error)
|
||||
Message.error('获取机柜详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal && props.rackId) {
|
||||
loadRackDetail()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'RackDetailDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.color-preview {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.arco-descriptions) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.arco-descriptions-title) {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,779 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑机柜' : '新建机柜'"
|
||||
width="800px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleVisibleChange"
|
||||
:confirm-loading="submitting"
|
||||
>
|
||||
<a-form :model="form" layout="vertical" ref="formRef">
|
||||
<!-- 基础信息 -->
|
||||
<a-divider orientation="left">基础信息</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="机柜名称"
|
||||
field="name"
|
||||
:rules="[{ required: true, message: '请输入机柜名称' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.name"
|
||||
placeholder="请输入机柜名称"
|
||||
:max-length="200"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="机柜编码"
|
||||
field="code"
|
||||
:rules="[{ required: true, message: '请输入机柜编码' }]"
|
||||
>
|
||||
<a-input
|
||||
v-model="form.code"
|
||||
placeholder="请输入机柜编码"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="所属中心"
|
||||
field="datacenter_id"
|
||||
:rules="[{ required: true, message: '请选择所属中心' }]"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.datacenter_id"
|
||||
placeholder="请选择所属中心"
|
||||
:loading="loadingDatacenters"
|
||||
allow-search
|
||||
@change="handleDatacenterChange"
|
||||
>
|
||||
<a-option
|
||||
v-for="item in datacenterList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="所属楼层"
|
||||
field="floor_id"
|
||||
:rules="[{ required: true, message: '请选择所属楼层' }]"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.floor_id"
|
||||
placeholder="请选择所属楼层"
|
||||
:loading="loadingFloors"
|
||||
allow-search
|
||||
>
|
||||
<a-option
|
||||
v-for="item in floorList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 规格参数 -->
|
||||
<a-divider orientation="left">规格参数</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="高度(U)" field="height">
|
||||
<a-input-number
|
||||
v-model="form.height"
|
||||
placeholder="默认42"
|
||||
:min="1"
|
||||
:max="100"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="宽度(mm)" field="width">
|
||||
<a-input-number
|
||||
v-model="form.width"
|
||||
placeholder="请输入宽度"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="深度(mm)" field="depth">
|
||||
<a-input-number
|
||||
v-model="form.depth"
|
||||
placeholder="请输入深度"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="重量(kg)" field="weight">
|
||||
<a-input-number
|
||||
v-model="form.weight"
|
||||
placeholder="请输入重量"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="最大承重(kg)" field="max_load">
|
||||
<a-input-number
|
||||
v-model="form.max_load"
|
||||
placeholder="请输入最大承重"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="电力容量(KW)" field="power_capacity">
|
||||
<a-input-number
|
||||
v-model="form.power_capacity"
|
||||
placeholder="请输入电力容量"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="电源相位" field="power_phase">
|
||||
<a-select v-model="form.power_phase" placeholder="请选择电源相位">
|
||||
<a-option value="单相">单相</a-option>
|
||||
<a-option value="三相">三相</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="PDU数量" field="pdu_count">
|
||||
<a-input-number
|
||||
v-model="form.pdu_count"
|
||||
placeholder="请输入PDU数量"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="网络接入方式" field="network_access">
|
||||
<a-input
|
||||
v-model="form.network_access"
|
||||
placeholder="请输入网络接入方式"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="交换机端口数" field="switch_ports">
|
||||
<a-input-number
|
||||
v-model="form.switch_ports"
|
||||
placeholder="请输入交换机端口数"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="制冷方式" field="cooling_type">
|
||||
<a-input
|
||||
v-model="form.cooling_type"
|
||||
placeholder="请输入制冷方式"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 状态信息 -->
|
||||
<a-divider orientation="left">状态信息</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="机柜类型" field="rack_type">
|
||||
<a-select v-model="form.rack_type" placeholder="请选择机柜类型">
|
||||
<a-option value="standard">标准机柜</a-option>
|
||||
<a-option value="blade">刀片机柜</a-option>
|
||||
<a-option value="network">网络机柜</a-option>
|
||||
<a-option value="storage">存储机柜</a-option>
|
||||
<a-option value="custom">定制机柜</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" field="status">
|
||||
<a-select v-model="form.status" placeholder="请选择状态">
|
||||
<a-option value="idle">空闲</a-option>
|
||||
<a-option value="in_use">使用中</a-option>
|
||||
<a-option value="reserved">已预留</a-option>
|
||||
<a-option value="maintenance">维护中</a-option>
|
||||
<a-option value="offline">已下线</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item
|
||||
label="供应商"
|
||||
field="supplier_id"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.supplier_id"
|
||||
placeholder="请选择供应商"
|
||||
:loading="loadingSuppliers"
|
||||
allow-search
|
||||
>
|
||||
<a-option
|
||||
v-for="item in supplierList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 制造商信息 -->
|
||||
<a-divider orientation="left">制造商信息</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="制造商" field="manufacturer">
|
||||
<a-input
|
||||
v-model="form.manufacturer"
|
||||
placeholder="请输入制造商"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="型号" field="model">
|
||||
<a-input
|
||||
v-model="form.model"
|
||||
placeholder="请输入型号"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="序列号" field="serial_number">
|
||||
<a-input
|
||||
v-model="form.serial_number"
|
||||
placeholder="请输入序列号"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="价格(元)" field="price">
|
||||
<a-input-number
|
||||
v-model="form.price"
|
||||
placeholder="请输入价格"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="采购日期" field="purchase_date">
|
||||
<a-date-picker
|
||||
v-model="form.purchase_date"
|
||||
placeholder="请选择采购日期"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 责任人信息 -->
|
||||
<a-divider orientation="left">责任人信息</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="负责人" field="owner">
|
||||
<a-input
|
||||
v-model="form.owner"
|
||||
placeholder="请输入负责人"
|
||||
:max-length="50"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="联系电话" field="contact_phone">
|
||||
<a-input
|
||||
v-model="form.contact_phone"
|
||||
placeholder="请输入联系电话"
|
||||
:max-length="20"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="使用部门" field="department">
|
||||
<a-input
|
||||
v-model="form.department"
|
||||
placeholder="请输入使用部门"
|
||||
:max-length="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 位置信息 -->
|
||||
<a-divider orientation="left">位置信息</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-form-item label="行号" field="row">
|
||||
<a-input-number
|
||||
v-model="form.row"
|
||||
placeholder="请输入行号"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="列号" field="column">
|
||||
<a-input-number
|
||||
v-model="form.column"
|
||||
placeholder="请输入列号"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="X坐标" field="position_x">
|
||||
<a-input-number
|
||||
v-model="form.position_x"
|
||||
placeholder="请输入X坐标"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="Y坐标" field="position_y">
|
||||
<a-input-number
|
||||
v-model="form.position_y"
|
||||
placeholder="请输入Y坐标"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 其他 -->
|
||||
<a-divider orientation="left">其他</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="颜色标识" field="color">
|
||||
<a-input
|
||||
v-model="form.color"
|
||||
placeholder="请输入颜色值,如#FF0000"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="启用" field="enabled">
|
||||
<a-switch v-model="form.enabled" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="描述" field="description">
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
placeholder="请输入描述"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" field="remarks">
|
||||
<a-textarea
|
||||
v-model="form.remarks"
|
||||
placeholder="请输入备注"
|
||||
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||
:max-length="500"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { createRack, updateRack } from '@/api/ops/rack'
|
||||
import {
|
||||
fetchDatacenterList,
|
||||
fetchFloorListForSelect,
|
||||
fetchSupplierList,
|
||||
} from '@/api/ops/rack'
|
||||
|
||||
interface Rack {
|
||||
id?: number
|
||||
name?: string
|
||||
code?: string
|
||||
datacenter_id?: number
|
||||
floor_id?: number
|
||||
height?: number
|
||||
width?: number
|
||||
depth?: number
|
||||
weight?: number
|
||||
max_load?: number
|
||||
power_capacity?: number
|
||||
power_phase?: string
|
||||
pdu_count?: number
|
||||
network_access?: string
|
||||
switch_ports?: number
|
||||
cooling_type?: string
|
||||
rack_type?: string
|
||||
status?: string
|
||||
supplier_id?: number
|
||||
manufacturer?: string
|
||||
model?: string
|
||||
serial_number?: string
|
||||
purchase_date?: string
|
||||
price?: number
|
||||
owner?: string
|
||||
contact_phone?: string
|
||||
department?: string
|
||||
row?: number
|
||||
column?: number
|
||||
position_x?: number
|
||||
position_y?: number
|
||||
color?: string
|
||||
enabled?: boolean
|
||||
description?: string
|
||||
remarks?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
rack: Rack | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref()
|
||||
const loadingDatacenters = ref(false)
|
||||
const loadingFloors = ref(false)
|
||||
const loadingSuppliers = ref(false)
|
||||
const submitting = ref(false)
|
||||
const datacenterList = ref<any[]>([])
|
||||
const floorList = ref<any[]>([])
|
||||
const supplierList = ref<any[]>([])
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
name: '',
|
||||
code: '',
|
||||
datacenter_id: undefined as number | undefined,
|
||||
floor_id: undefined as number | undefined,
|
||||
height: 42,
|
||||
width: undefined as number | undefined,
|
||||
depth: undefined as number | undefined,
|
||||
weight: undefined as number | undefined,
|
||||
max_load: undefined as number | undefined,
|
||||
power_capacity: undefined as number | undefined,
|
||||
power_phase: '',
|
||||
pdu_count: 0,
|
||||
network_access: '',
|
||||
switch_ports: 0,
|
||||
cooling_type: '',
|
||||
rack_type: 'standard',
|
||||
status: 'idle',
|
||||
supplier_id: undefined as number | undefined,
|
||||
manufacturer: '',
|
||||
model: '',
|
||||
serial_number: '',
|
||||
purchase_date: '',
|
||||
price: undefined as number | undefined,
|
||||
owner: '',
|
||||
contact_phone: '',
|
||||
department: '',
|
||||
row: 0,
|
||||
column: 0,
|
||||
position_x: undefined as number | undefined,
|
||||
position_y: undefined as number | undefined,
|
||||
color: '',
|
||||
enabled: true,
|
||||
description: '',
|
||||
remarks: '',
|
||||
})
|
||||
|
||||
// 是否为编辑模式
|
||||
const isEdit = computed(() => !!props.rack?.id)
|
||||
|
||||
// 加载数据中心列表
|
||||
const loadDatacenterList = async () => {
|
||||
loadingDatacenters.value = true
|
||||
try {
|
||||
const res: any = await fetchDatacenterList()
|
||||
if (res.code === 0) {
|
||||
datacenterList.value = res.details || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据中心列表失败:', error)
|
||||
} finally {
|
||||
loadingDatacenters.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载楼层列表
|
||||
const loadFloorList = async (datacenterId?: number) => {
|
||||
loadingFloors.value = true
|
||||
try {
|
||||
const res: any = await fetchFloorListForSelect(datacenterId)
|
||||
if (res.code === 0) {
|
||||
floorList.value = res.details || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取楼层列表失败:', error)
|
||||
} finally {
|
||||
loadingFloors.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载供应商列表
|
||||
const loadSupplierList = async () => {
|
||||
loadingSuppliers.value = true
|
||||
try {
|
||||
const res: any = await fetchSupplierList()
|
||||
if (res.code === 0) {
|
||||
supplierList.value = res.details || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取供应商列表失败:', error)
|
||||
} finally {
|
||||
loadingSuppliers.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 数据中心变化时重新加载楼层列表
|
||||
const handleDatacenterChange = async (value: number) => {
|
||||
form.value.floor_id = undefined
|
||||
await loadFloorList(value)
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
if (props.rack && isEdit.value) {
|
||||
// 编辑模式:填充表单
|
||||
form.value = {
|
||||
name: props.rack.name || '',
|
||||
code: props.rack.code || '',
|
||||
datacenter_id: props.rack.datacenter_id,
|
||||
floor_id: props.rack.floor_id,
|
||||
height: props.rack.height || 42,
|
||||
width: props.rack.width,
|
||||
depth: props.rack.depth,
|
||||
weight: props.rack.weight,
|
||||
max_load: props.rack.max_load,
|
||||
power_capacity: props.rack.power_capacity,
|
||||
power_phase: props.rack.power_phase || '',
|
||||
pdu_count: props.rack.pdu_count || 0,
|
||||
network_access: props.rack.network_access || '',
|
||||
switch_ports: props.rack.switch_ports || 0,
|
||||
cooling_type: props.rack.cooling_type || '',
|
||||
rack_type: props.rack.rack_type || 'standard',
|
||||
status: props.rack.status || 'idle',
|
||||
supplier_id: props.rack.supplier_id,
|
||||
manufacturer: props.rack.manufacturer || '',
|
||||
model: props.rack.model || '',
|
||||
serial_number: props.rack.serial_number || '',
|
||||
purchase_date: props.rack.purchase_date || '',
|
||||
price: props.rack.price,
|
||||
owner: props.rack.owner || '',
|
||||
contact_phone: props.rack.contact_phone || '',
|
||||
department: props.rack.department || '',
|
||||
row: props.rack.row || 0,
|
||||
column: props.rack.column || 0,
|
||||
position_x: props.rack.position_x,
|
||||
position_y: props.rack.position_y,
|
||||
color: props.rack.color || '',
|
||||
enabled: props.rack.enabled !== undefined ? props.rack.enabled : true,
|
||||
description: props.rack.description || '',
|
||||
remarks: props.rack.remarks || '',
|
||||
}
|
||||
// 加载对应数据中心的楼层列表
|
||||
if (props.rack.datacenter_id) {
|
||||
loadFloorList(props.rack.datacenter_id)
|
||||
}
|
||||
} else {
|
||||
// 新建模式:重置表单
|
||||
form.value = {
|
||||
name: '',
|
||||
code: '',
|
||||
datacenter_id: undefined,
|
||||
floor_id: undefined,
|
||||
height: 42,
|
||||
width: undefined,
|
||||
depth: undefined,
|
||||
weight: undefined,
|
||||
max_load: undefined,
|
||||
power_capacity: undefined,
|
||||
power_phase: '',
|
||||
pdu_count: 0,
|
||||
network_access: '',
|
||||
switch_ports: 0,
|
||||
cooling_type: '',
|
||||
rack_type: 'standard',
|
||||
status: 'idle',
|
||||
supplier_id: undefined,
|
||||
manufacturer: '',
|
||||
model: '',
|
||||
serial_number: '',
|
||||
purchase_date: '',
|
||||
price: undefined,
|
||||
owner: '',
|
||||
contact_phone: '',
|
||||
department: '',
|
||||
row: 0,
|
||||
column: 0,
|
||||
position_x: undefined,
|
||||
position_y: undefined,
|
||||
color: '',
|
||||
enabled: true,
|
||||
description: '',
|
||||
remarks: '',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 确认提交
|
||||
const handleOk = async () => {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (valid) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const data: any = {
|
||||
name: form.value.name,
|
||||
code: form.value.code,
|
||||
datacenter_id: form.value.datacenter_id,
|
||||
floor_id: form.value.floor_id,
|
||||
height: form.value.height,
|
||||
width: form.value.width,
|
||||
depth: form.value.depth,
|
||||
weight: form.value.weight,
|
||||
max_load: form.value.max_load,
|
||||
power_capacity: form.value.power_capacity,
|
||||
power_phase: form.value.power_phase,
|
||||
pdu_count: form.value.pdu_count,
|
||||
network_access: form.value.network_access,
|
||||
switch_ports: form.value.switch_ports,
|
||||
cooling_type: form.value.cooling_type,
|
||||
rack_type: form.value.rack_type,
|
||||
status: form.value.status,
|
||||
supplier_id: form.value.supplier_id,
|
||||
manufacturer: form.value.manufacturer,
|
||||
model: form.value.model,
|
||||
serial_number: form.value.serial_number,
|
||||
purchase_date: form.value.purchase_date,
|
||||
price: form.value.price,
|
||||
owner: form.value.owner,
|
||||
contact_phone: form.value.contact_phone,
|
||||
department: form.value.department,
|
||||
row: form.value.row,
|
||||
column: form.value.column,
|
||||
position_x: form.value.position_x,
|
||||
position_y: form.value.position_y,
|
||||
color: form.value.color,
|
||||
enabled: form.value.enabled,
|
||||
description: form.value.description,
|
||||
remarks: form.value.remarks,
|
||||
}
|
||||
|
||||
let res
|
||||
if (isEdit.value && props.rack?.id) {
|
||||
// 编辑机柜
|
||||
data.id = props.rack.id
|
||||
res = await updateRack(data)
|
||||
} else {
|
||||
// 新建机柜
|
||||
res = await createRack(data)
|
||||
}
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success(isEdit.value ? '编辑成功' : '创建成功')
|
||||
emit('success')
|
||||
emit('update:visible', false)
|
||||
} else {
|
||||
Message.error(res.message || (isEdit.value ? '编辑失败' : '创建失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
Message.error(isEdit.value ? '编辑失败' : '创建失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 处理对话框可见性变化
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadDatacenterList()
|
||||
loadSupplierList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'RackFormDialog',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
</style>
|
||||
77
src/views/ops/pages/datacenter/rack/config/columns.ts
Normal file
77
src/views/ops/pages/datacenter/rack/config/columns.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
export const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '机柜名称',
|
||||
dataIndex: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '机柜编码',
|
||||
dataIndex: 'code',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '所属中心',
|
||||
dataIndex: 'datacenter',
|
||||
slotName: 'datacenter',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '所属楼层',
|
||||
dataIndex: 'floor',
|
||||
slotName: 'floor',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '机柜类型',
|
||||
dataIndex: 'rack_type',
|
||||
slotName: 'rack_type',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '高度(U)',
|
||||
dataIndex: 'height',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '总U位',
|
||||
dataIndex: 'total_units',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '已用U位',
|
||||
dataIndex: 'used_units',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '可用U位',
|
||||
dataIndex: 'available_units',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '使用率',
|
||||
dataIndex: 'utilization_rate',
|
||||
slotName: 'utilization_rate',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'actions',
|
||||
slotName: 'actions',
|
||||
width: 320,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
]
|
||||
55
src/views/ops/pages/datacenter/rack/config/search-form.ts
Normal file
55
src/views/ops/pages/datacenter/rack/config/search-form.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
export const searchFormConfig: FormItem[] = [
|
||||
{
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入机柜名称或编码',
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'datacenter_id',
|
||||
label: '数据中心',
|
||||
type: 'select',
|
||||
placeholder: '请选择数据中心',
|
||||
options: [], // 需要动态加载
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'floor_id',
|
||||
label: '楼层',
|
||||
type: 'select',
|
||||
placeholder: '请选择楼层',
|
||||
options: [], // 需要动态加载
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'rack_type',
|
||||
label: '机柜类型',
|
||||
type: 'select',
|
||||
placeholder: '请选择机柜类型',
|
||||
options: [
|
||||
{ label: '标准机柜', value: 'standard' },
|
||||
{ label: '刀片机柜', value: 'blade' },
|
||||
{ label: '网络机柜', value: 'network' },
|
||||
{ label: '存储机柜', value: 'storage' },
|
||||
{ label: '定制机柜', value: 'custom' },
|
||||
],
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: [
|
||||
{ label: '空闲', value: 'idle' },
|
||||
{ label: '使用中', value: 'in_use' },
|
||||
{ label: '已预留', value: 'reserved' },
|
||||
{ label: '维护中', value: 'maintenance' },
|
||||
{ label: '已下线', value: 'offline' },
|
||||
],
|
||||
span: 6,
|
||||
},
|
||||
]
|
||||
333
src/views/ops/pages/datacenter/rack/index.vue
Normal file
333
src/views/ops/pages/datacenter/rack/index.vue
Normal file
@@ -0,0 +1,333 @@
|
||||
<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 #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 所属中心 -->
|
||||
<template #datacenter="{ record }">
|
||||
{{ record.datacenter?.name || '-' }}
|
||||
</template>
|
||||
|
||||
<!-- 所属楼层 -->
|
||||
<template #floor="{ record }">
|
||||
{{ record.floor?.name || '-' }}
|
||||
</template>
|
||||
|
||||
<!-- 机柜类型 -->
|
||||
<template #rack_type="{ record }">
|
||||
<a-tag :color="getRackTypeColor(record.rack_type)">
|
||||
{{ getRackTypeText(record.rack_type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 使用率 -->
|
||||
<template #utilization_rate="{ record }">
|
||||
<a-progress
|
||||
:percent="(record.utilization_rate || 0)"
|
||||
:size="'small'"
|
||||
:color="
|
||||
(record.utilization_rate || 0) > 80
|
||||
? '#f53f3f'
|
||||
: (record.utilization_rate || 0) > 50
|
||||
? '#ff7d00'
|
||||
: '#00b42a'
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template #actions="{ record }">
|
||||
<a-button type="text" size="small" @click="handleView(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleUnitManagement(record)">
|
||||
U位管理
|
||||
</a-button>
|
||||
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">
|
||||
删除
|
||||
</a-button>
|
||||
</template>
|
||||
</search-table>
|
||||
|
||||
<!-- 机柜表单对话框(新建/编辑) -->
|
||||
<rack-form-dialog
|
||||
v-model:visible="formVisible"
|
||||
:rack="editingRack"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 机柜详情对话框 -->
|
||||
<rack-detail-dialog
|
||||
v-model:visible="detailVisible"
|
||||
:rack-id="currentRackId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import { searchFormConfig } from './config/search-form'
|
||||
import { columns as columnsConfig } from './config/columns'
|
||||
import {
|
||||
fetchRackList,
|
||||
deleteRack,
|
||||
} from '@/api/ops/rack'
|
||||
import RackDetailDialog from './components/RackDetailDialog.vue'
|
||||
import RackFormDialog from './components/RackFormDialog.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
datacenter_id: undefined,
|
||||
floor_id: undefined,
|
||||
rack_type: undefined,
|
||||
status: undefined,
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => searchFormConfig)
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => columnsConfig)
|
||||
|
||||
// 当前选中的机柜
|
||||
const currentRackId = ref<number | null>(null)
|
||||
const editingRack = ref<any>(null)
|
||||
|
||||
// 对话框可见性
|
||||
const formVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
|
||||
// 获取机柜类型颜色
|
||||
const getRackTypeColor = (type?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
standard: 'blue',
|
||||
blade: 'cyan',
|
||||
network: 'purple',
|
||||
storage: 'orange',
|
||||
custom: 'gray',
|
||||
}
|
||||
return colorMap[type || ''] || 'gray'
|
||||
}
|
||||
|
||||
// 获取机柜类型文本
|
||||
const getRackTypeText = (type?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
standard: '标准机柜',
|
||||
blade: '刀片机柜',
|
||||
network: '网络机柜',
|
||||
storage: '存储机柜',
|
||||
custom: '定制机柜',
|
||||
}
|
||||
return textMap[type || ''] || '-'
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
idle: 'green',
|
||||
in_use: 'blue',
|
||||
reserved: 'orange',
|
||||
maintenance: 'red',
|
||||
offline: 'gray',
|
||||
}
|
||||
return colorMap[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
idle: '空闲',
|
||||
in_use: '使用中',
|
||||
reserved: '已预留',
|
||||
maintenance: '维护中',
|
||||
offline: '已下线',
|
||||
}
|
||||
return textMap[status || ''] || '-'
|
||||
}
|
||||
|
||||
// 获取机柜列表
|
||||
const fetchRacks = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const params: any = {
|
||||
page: pagination.current,
|
||||
page_size: pagination.pageSize,
|
||||
keyword: formModel.value.keyword || undefined,
|
||||
datacenter_id: formModel.value.datacenter_id || undefined,
|
||||
floor_id: formModel.value.floor_id || undefined,
|
||||
rack_type: formModel.value.rack_type || undefined,
|
||||
status: formModel.value.status || undefined,
|
||||
}
|
||||
|
||||
const res = await fetchRackList(params)
|
||||
|
||||
tableData.value = res.data?.data || []
|
||||
pagination.total = res.data?.total || 0
|
||||
} catch (error) {
|
||||
console.error('获取机柜列表失败:', error)
|
||||
Message.error('获取机柜列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchRacks()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = value
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
datacenter_id: undefined,
|
||||
floor_id: undefined,
|
||||
rack_type: undefined,
|
||||
status: undefined,
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchRacks()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchRacks()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchRacks()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新增机柜
|
||||
const handleAdd = () => {
|
||||
editingRack.value = null
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑机柜
|
||||
const handleEdit = (record: any) => {
|
||||
editingRack.value = record
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleView = (record: any) => {
|
||||
currentRackId.value = record.id
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 删除机柜
|
||||
const handleDelete = async (record: any) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确认删除机柜 ${record.name} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await deleteRack(record.id)
|
||||
if (res.code === 0) {
|
||||
Message.success('删除成功')
|
||||
fetchRacks()
|
||||
} else {
|
||||
Message.error(res.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除机柜失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// U位管理
|
||||
const handleUnitManagement = (record: any) => {
|
||||
router.push({
|
||||
path: '/ops/datacenter/u-position',
|
||||
query: { rack_id: record.id, rack_name: record.name },
|
||||
})
|
||||
}
|
||||
|
||||
// 表单成功回调
|
||||
const handleFormSuccess = () => {
|
||||
formVisible.value = false
|
||||
fetchRacks()
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
fetchRacks()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DataCenterRack',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="dialogVisible"
|
||||
title="分配U位"
|
||||
:width="600"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
|
||||
<a-form-item field="start_unit" label="起始U位" required>
|
||||
<a-input-number
|
||||
v-model="formData.start_unit"
|
||||
:min="1"
|
||||
:max="rackHeight"
|
||||
placeholder="请输入起始U位"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="occupied_units" label="占用U位数量" required>
|
||||
<a-input-number
|
||||
v-model="formData.occupied_units"
|
||||
:min="1"
|
||||
:max="rackHeight"
|
||||
placeholder="请输入占用U位数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="asset_id" label="选择设备">
|
||||
<a-select
|
||||
v-model="formData.asset_id"
|
||||
placeholder="请选择设备"
|
||||
allow-search
|
||||
@change="handleAssetChange"
|
||||
>
|
||||
<a-option
|
||||
v-for="asset in assetList"
|
||||
:key="asset.id"
|
||||
:value="asset.id"
|
||||
:label="asset.asset_name"
|
||||
/>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="asset_type" label="设备类型" required>
|
||||
<a-input
|
||||
v-model="formData.asset_type"
|
||||
placeholder="设备类型(选择设备后自动填入)"
|
||||
disabled
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="power_consumption" label="功耗 (W)">
|
||||
<a-input-number
|
||||
v-model="formData.power_consumption"
|
||||
:min="0"
|
||||
placeholder="请输入功耗"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="description" label="描述">
|
||||
<a-textarea
|
||||
v-model="formData.description"
|
||||
placeholder="请输入描述信息"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AllocateUnitDialog',
|
||||
})
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
interface Asset {
|
||||
id: number
|
||||
asset_code: string
|
||||
asset_name: string
|
||||
asset_type: string
|
||||
power_consumption?: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
rackId: number
|
||||
rackHeight: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value),
|
||||
})
|
||||
|
||||
const formRef = ref()
|
||||
const formData = reactive({
|
||||
start_unit: 1,
|
||||
occupied_units: 1,
|
||||
asset_id: undefined as number | undefined,
|
||||
asset_code: '',
|
||||
asset_name: '',
|
||||
asset_type: '',
|
||||
power_consumption: undefined as number | undefined,
|
||||
description: '',
|
||||
})
|
||||
|
||||
const assetList = ref<Asset[]>([
|
||||
// 模拟设备列表,实际应从API获取
|
||||
{ id: 1, asset_code: 'SRV001', asset_name: '服务器-001', asset_type: '服务器设备', power_consumption: 500 },
|
||||
{ id: 2, asset_code: 'SRV002', asset_name: '服务器-002', asset_type: '服务器设备', power_consumption: 600 },
|
||||
{ id: 3, asset_code: 'NET001', asset_name: '交换机-001', asset_type: '网络设备', power_consumption: 200 },
|
||||
{ id: 4, asset_code: 'STO001', asset_name: '存储-001', asset_type: '存储设备', power_consumption: 400 },
|
||||
])
|
||||
|
||||
const rules = {
|
||||
start_unit: [{ required: true, message: '请输入起始U位' }],
|
||||
occupied_units: [{ required: true, message: '请输入占用U位数量' }],
|
||||
asset_type: [{ required: true, message: '设备类型不能为空' }],
|
||||
}
|
||||
|
||||
const handleAssetChange = (value: number) => {
|
||||
const asset = assetList.value.find((a) => a.id === value)
|
||||
if (asset) {
|
||||
formData.asset_code = asset.asset_code
|
||||
formData.asset_name = asset.asset_name
|
||||
formData.asset_type = asset.asset_type
|
||||
formData.power_consumption = asset.power_consumption
|
||||
}
|
||||
}
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (!valid) {
|
||||
// 发送分配请求
|
||||
const { allocateUnit } = await import('@/api/ops/unit')
|
||||
const params = {
|
||||
rack_id: props.rackId,
|
||||
start_unit: formData.start_unit,
|
||||
occupied_units: formData.occupied_units,
|
||||
asset_id: formData.asset_id,
|
||||
asset_code: formData.asset_code,
|
||||
asset_name: formData.asset_name || `未命名设备-${formData.start_unit}`,
|
||||
asset_type: formData.asset_type,
|
||||
power_consumption: formData.power_consumption,
|
||||
description: formData.description,
|
||||
}
|
||||
const res = await allocateUnit(params)
|
||||
if (res.code === 0) {
|
||||
Message.success('分配成功')
|
||||
emit('success')
|
||||
handleCancel()
|
||||
} else {
|
||||
Message.error(res.message || '分配失败')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('分配失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
dialogVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.start_unit = 1
|
||||
formData.occupied_units = 1
|
||||
formData.asset_id = undefined
|
||||
formData.asset_code = ''
|
||||
formData.asset_name = ''
|
||||
formData.asset_type = ''
|
||||
formData.power_consumption = undefined
|
||||
formData.description = ''
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="dialogVisible"
|
||||
title="预留U位"
|
||||
:width="600"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
|
||||
<a-form-item field="start_unit" label="起始U位" required>
|
||||
<a-input-number
|
||||
v-model="formData.start_unit"
|
||||
:min="1"
|
||||
:max="rackHeight"
|
||||
placeholder="请输入起始U位"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="occupied_units" label="占用U位数量" required>
|
||||
<a-input-number
|
||||
v-model="formData.occupied_units"
|
||||
:min="1"
|
||||
:max="rackHeight"
|
||||
placeholder="请输入占用U位数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="reserved_for" label="预留对象" required>
|
||||
<a-input
|
||||
v-model="formData.reserved_for"
|
||||
placeholder="请输入预留对象(部门或项目)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="reserved_until" label="预留截止时间" required>
|
||||
<a-date-picker
|
||||
v-model="formData.reserved_until"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择预留截止时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="description" label="预留信息">
|
||||
<a-textarea
|
||||
v-model="formData.description"
|
||||
placeholder="请输入预留信息"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ReserveUnitDialog',
|
||||
})
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
rackId: number
|
||||
rackHeight: number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit('update:visible', value),
|
||||
})
|
||||
|
||||
const formRef = ref()
|
||||
const formData = reactive({
|
||||
start_unit: 1,
|
||||
occupied_units: 1,
|
||||
reserved_for: '',
|
||||
reserved_until: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
const rules = {
|
||||
start_unit: [{ required: true, message: '请输入起始U位' }],
|
||||
occupied_units: [{ required: true, message: '请输入占用U位数量' }],
|
||||
reserved_for: [{ required: true, message: '请输入预留对象' }],
|
||||
reserved_until: [{ required: true, message: '请选择预留截止时间' }],
|
||||
}
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
const valid = await formRef.value?.validate()
|
||||
if (!valid) {
|
||||
// 发送预留请求
|
||||
const { reserveUnit } = await import('@/api/ops/unit')
|
||||
|
||||
// 将日期转换为ISO格式
|
||||
let reservedUntil = ''
|
||||
if (formData.reserved_until) {
|
||||
const date = new Date(formData.reserved_until as any)
|
||||
reservedUntil = date.toISOString()
|
||||
}
|
||||
|
||||
const params = {
|
||||
rack_id: props.rackId,
|
||||
start_unit: formData.start_unit,
|
||||
occupied_units: formData.occupied_units,
|
||||
reserved_for: formData.reserved_for,
|
||||
reserved_until: reservedUntil,
|
||||
description: formData.description,
|
||||
}
|
||||
const res = await reserveUnit(params)
|
||||
if (res.code === 0) {
|
||||
Message.success('预留成功')
|
||||
emit('success')
|
||||
handleCancel()
|
||||
} else {
|
||||
Message.error(res.message || '预留失败')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('预留失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
dialogVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.start_unit = 1
|
||||
formData.occupied_units = 1
|
||||
formData.reserved_for = ''
|
||||
formData.reserved_until = ''
|
||||
formData.description = ''
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (!val) {
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
557
src/views/ops/pages/datacenter/u-position/detail.vue
Normal file
557
src/views/ops/pages/datacenter/u-position/detail.vue
Normal file
@@ -0,0 +1,557 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<a-button type="text" @click="handleBack">
|
||||
<template #icon>
|
||||
<icon-left />
|
||||
</template>
|
||||
返回列表
|
||||
</a-button>
|
||||
<h2 class="page-title">{{ pageTitle }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- 机柜信息卡片 -->
|
||||
<a-card class="info-card" :loading="loading">
|
||||
<template #title>
|
||||
<icon-storage /> 机柜信息
|
||||
</template>
|
||||
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">机柜编码</div>
|
||||
<div class="info-value">{{ rackInfo.code || '-' }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">机柜总U位</div>
|
||||
<div class="info-value">{{ rackInfo.height || 0 }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">已使用U位</div>
|
||||
<div class="info-value">{{ usedUnits }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">使用率</div>
|
||||
<div class="info-value">
|
||||
<a-progress
|
||||
:percent="usagePercentage"
|
||||
:size="'small'"
|
||||
:color="
|
||||
usagePercentage > 80
|
||||
? '#f53f3f'
|
||||
: usagePercentage > 50
|
||||
? '#ff7d00'
|
||||
: '#00b42a'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">空余U位</div>
|
||||
<div class="info-value">{{ availableUnits }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">空余率</div>
|
||||
<div class="info-value">
|
||||
<a-progress
|
||||
:percent="availablePercentage"
|
||||
:size="'small'"
|
||||
:color="'#00b42a'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">总功耗</div>
|
||||
<div class="info-value">{{ totalPower }}W</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">状态</div>
|
||||
<div class="info-value">
|
||||
<a-tag :color="getRackStatusColor(rackInfo.status)">
|
||||
{{ getRackStatusText(rackInfo.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- U位管理卡片 -->
|
||||
<a-card class="u-position-card" :loading="loading">
|
||||
<template #title>
|
||||
<icon-apps /> U位列表
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAllocate">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
分配U位
|
||||
</a-button>
|
||||
<a-button type="outline" @click="handleReserve">
|
||||
<template #icon>
|
||||
<icon-lock />
|
||||
</template>
|
||||
预留U位
|
||||
</a-button>
|
||||
<a-button type="outline" @click="handleRefresh">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<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 }">
|
||||
{{ rowIndex + 1 }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="U位编号" data-index="unit_number" :width="100" />
|
||||
<a-table-column title="设备名称" data-index="asset_name" :width="150" />
|
||||
<a-table-column title="设备编号" data-index="asset_code" :width="150" />
|
||||
<a-table-column title="设备类型" data-index="asset_type" :width="120" />
|
||||
<a-table-column title="占用U位" data-index="occupied_units" :width="100" />
|
||||
<a-table-column title="预留信息" data-index="reserved_for" :width="150" />
|
||||
<a-table-column title="功耗(W)" data-index="power_consumption" :width="100" />
|
||||
<a-table-column title="状态" :width="100">
|
||||
<template #cell="{ record }">
|
||||
<a-tag :color="getUnitStatusColor(record.status)">
|
||||
{{ getUnitStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" :width="200" fixed="right">
|
||||
<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 === 'occupied'"
|
||||
type="text"
|
||||
size="small"
|
||||
status="danger"
|
||||
@click="handleRelease(record)"
|
||||
>
|
||||
释放
|
||||
</a-button>
|
||||
|
||||
<!-- 已预留状态:显示取消预留按钮 -->
|
||||
<a-button
|
||||
v-if="record.status === 'reserved'"
|
||||
type="text"
|
||||
size="small"
|
||||
status="danger"
|
||||
@click="handleCancelReservation(record)"
|
||||
>
|
||||
取消预留
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 分配U位对话框 -->
|
||||
<allocate-unit-dialog
|
||||
v-model:visible="allocateVisible"
|
||||
:rack-id="rackId"
|
||||
:rack-height="rackInfo.height"
|
||||
@success="handleRefresh"
|
||||
/>
|
||||
|
||||
<!-- 预留U位对话框 -->
|
||||
<reserve-unit-dialog
|
||||
v-model:visible="reserveVisible"
|
||||
:rack-id="rackId"
|
||||
:rack-height="rackInfo.height"
|
||||
@success="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconLeft, IconStorage, IconApps, IconPlus, IconLock, IconRefresh } from '@arco-design/web-vue/es/icon'
|
||||
import {
|
||||
fetchUnitList,
|
||||
allocateUnit,
|
||||
reserveUnit,
|
||||
cancelReservation,
|
||||
releaseUnit,
|
||||
updateUnitStatus,
|
||||
} from '@/api/ops/unit'
|
||||
import AllocateUnitDialog from './components/AllocateUnitDialog.vue'
|
||||
import ReserveUnitDialog from './components/ReserveUnitDialog.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const rackId = ref<number>(0)
|
||||
const rackName = ref<string>('')
|
||||
const rackInfo = ref<any>({})
|
||||
const unitList = ref<any[]>([])
|
||||
|
||||
// 对话框可见性
|
||||
const allocateVisible = ref(false)
|
||||
const reserveVisible = ref(false)
|
||||
|
||||
// 页面标题
|
||||
const pageTitle = computed(() => {
|
||||
return rackName.value ? `${rackName.value} - U位详情` : 'U位详情'
|
||||
})
|
||||
|
||||
// 已使用U位
|
||||
const usedUnits = computed(() => {
|
||||
return unitList.value.filter(
|
||||
(unit) => unit.status === 'occupied' || unit.status === 'reserved'
|
||||
).length
|
||||
})
|
||||
|
||||
// 空余U位
|
||||
const availableUnits = computed(() => {
|
||||
return rackInfo.value.height - usedUnits.value
|
||||
})
|
||||
|
||||
// 使用率
|
||||
const usagePercentage = computed(() => {
|
||||
if (!rackInfo.value.height) return 0
|
||||
return Math.round((usedUnits.value / rackInfo.value.height) * 100)
|
||||
})
|
||||
|
||||
// 空余率
|
||||
const availablePercentage = computed(() => {
|
||||
if (!rackInfo.value.height) return 0
|
||||
return Math.round((availableUnits.value / rackInfo.value.height) * 100)
|
||||
})
|
||||
|
||||
// 总功耗
|
||||
const totalPower = computed(() => {
|
||||
return unitList.value.reduce((total, unit) => {
|
||||
return total + (unit.power_consumption || 0)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
// 获取机柜状态颜色
|
||||
const getRackStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
idle: 'green',
|
||||
in_use: 'blue',
|
||||
reserved: 'orange',
|
||||
maintenance: 'red',
|
||||
offline: 'gray',
|
||||
}
|
||||
return colorMap[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
// 获取机柜状态文本
|
||||
const getRackStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
idle: '空闲',
|
||||
in_use: '使用中',
|
||||
reserved: '已预留',
|
||||
maintenance: '维护中',
|
||||
offline: '已下线',
|
||||
}
|
||||
return textMap[status || ''] || '-'
|
||||
}
|
||||
|
||||
// 获取U位状态颜色
|
||||
const getUnitStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
available: 'green',
|
||||
occupied: 'blue',
|
||||
reserved: 'orange',
|
||||
disabled: 'gray',
|
||||
}
|
||||
return colorMap[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
// 获取U位状态文本
|
||||
const getUnitStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
available: '可用',
|
||||
occupied: '已占用',
|
||||
reserved: '已预留',
|
||||
disabled: '已禁用',
|
||||
}
|
||||
return textMap[status || ''] || '-'
|
||||
}
|
||||
|
||||
// 获取U位列表
|
||||
const fetchUnits = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const res = await fetchUnitList(rackId.value)
|
||||
|
||||
if (res.code === 0) {
|
||||
rackInfo.value = res.data?.rack || {}
|
||||
unitList.value = res.data?.units || []
|
||||
} else {
|
||||
Message.error(res.message || '获取U位列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取U位列表失败:', error)
|
||||
Message.error('获取U位列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回列表
|
||||
const handleBack = () => {
|
||||
router.push('/ops/datacenter/u-position')
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchUnits()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 分配U位
|
||||
const handleAllocate = () => {
|
||||
allocateVisible.value = true
|
||||
}
|
||||
|
||||
// 预留U位
|
||||
const handleReserve = () => {
|
||||
reserveVisible.value = true
|
||||
}
|
||||
|
||||
// 禁用U位
|
||||
const handleDisable = async (record: any) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认禁用',
|
||||
content: `确认禁用 U位 ${record.unit_number} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await updateUnitStatus({
|
||||
rack_id: rackId.value,
|
||||
start_unit: record.unit_number,
|
||||
end_unit: record.unit_number,
|
||||
status: 'disabled',
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success('禁用成功')
|
||||
fetchUnits()
|
||||
} else {
|
||||
Message.error(res.message || '禁用失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('禁用U位失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 启用U位
|
||||
const handleEnable = async (record: any) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认启用',
|
||||
content: `确认启用 U位 ${record.unit_number} 吗?`,
|
||||
onOk: async () => {
|
||||
const res = await updateUnitStatus({
|
||||
rack_id: rackId.value,
|
||||
start_unit: record.unit_number,
|
||||
end_unit: record.unit_number,
|
||||
status: 'available',
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success('启用成功')
|
||||
fetchUnits()
|
||||
} else {
|
||||
Message.error(res.message || '启用失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('启用U位失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 释放U位
|
||||
const handleRelease = async (record: any) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
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 res = await releaseUnit({
|
||||
rack_id: rackId.value,
|
||||
start_unit: record.unit_number,
|
||||
end_unit: endUnit,
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success('释放成功')
|
||||
fetchUnits()
|
||||
} else {
|
||||
Message.error(res.message || '释放失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('释放U位失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消预留
|
||||
const handleCancelReservation = async (record: any) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
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 res = await cancelReservation({
|
||||
rack_id: rackId.value,
|
||||
start_unit: record.unit_number,
|
||||
end_unit: endUnit,
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success('取消预留成功')
|
||||
fetchUnits()
|
||||
} else {
|
||||
Message.error(res.message || '取消预留失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('取消预留失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
rackId.value = Number(route.query.rack_id)
|
||||
rackName.value = (route.query.rack_name as string) || ''
|
||||
|
||||
if (!rackId.value) {
|
||||
Message.error('缺少机柜ID')
|
||||
router.push('/ops/datacenter/u-position')
|
||||
return
|
||||
}
|
||||
|
||||
fetchUnits()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UPositionDetail',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
background: #fff;
|
||||
padding: 16px 20px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.page-title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
margin-left: 16px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.info-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.info-label {
|
||||
font-size: 13px;
|
||||
color: #86909c;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.u-position-card {
|
||||
:deep(.arco-card-body) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
490
src/views/ops/pages/datacenter/u-position/index.vue
Normal file
490
src/views/ops/pages/datacenter/u-position/index.vue
Normal file
@@ -0,0 +1,490 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 机柜选择卡片 -->
|
||||
<a-card class="rack-select-card">
|
||||
<template #title>
|
||||
<icon-storage /> 选择机柜
|
||||
</template>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-select
|
||||
v-model="selectedRackId"
|
||||
placeholder="请选择机柜"
|
||||
:loading="rackListLoading"
|
||||
@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-select>
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<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-space>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<a-card class="action-card">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleAllocate" :disabled="!selectedRackId">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
分配U位
|
||||
</a-button>
|
||||
<a-button type="outline" @click="handleReserve" :disabled="!selectedRackId">
|
||||
<template #icon>
|
||||
<icon-lock />
|
||||
</template>
|
||||
预留U位
|
||||
</a-button>
|
||||
<a-button type="outline" @click="handleRefresh" :disabled="!selectedRackId">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-card>
|
||||
|
||||
<!-- U位列表 -->
|
||||
<a-card class="u-position-card" :loading="loading">
|
||||
<template #title>
|
||||
<icon-apps /> U位列表
|
||||
</template>
|
||||
|
||||
<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 }">
|
||||
{{ rowIndex + 1 }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="U位编号" data-index="unit_number" :width="100" />
|
||||
<a-table-column title="设备名称" data-index="asset_name" :width="150" />
|
||||
<a-table-column title="设备编号" data-index="asset_code" :width="150" />
|
||||
<a-table-column title="设备类型" data-index="asset_type" :width="120" />
|
||||
<a-table-column title="占用U位" data-index="occupied_units" :width="100" />
|
||||
<a-table-column title="预留信息" data-index="reserved_for" :width="150" />
|
||||
<a-table-column title="功耗(W)" data-index="power_consumption" :width="100" />
|
||||
<a-table-column title="状态" :width="100">
|
||||
<template #cell="{ record }">
|
||||
<a-tag :color="getUnitStatusColor(record.status)">
|
||||
{{ getUnitStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" :width="200" fixed="right">
|
||||
<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 === 'occupied'"
|
||||
type="text"
|
||||
size="small"
|
||||
status="danger"
|
||||
@click="handleRelease(record)"
|
||||
>
|
||||
释放
|
||||
</a-button>
|
||||
|
||||
<!-- 已预留状态:显示取消预留按钮 -->
|
||||
<a-button
|
||||
v-if="record.status === 'reserved'"
|
||||
type="text"
|
||||
size="small"
|
||||
status="danger"
|
||||
@click="handleCancelReservation(record)"
|
||||
>
|
||||
取消预留
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 分配U位对话框 -->
|
||||
<allocate-unit-dialog
|
||||
v-model:visible="allocateVisible"
|
||||
:rack-id="selectedRackId"
|
||||
:rack-height="rackInfo.height"
|
||||
@success="handleRefresh"
|
||||
/>
|
||||
|
||||
<!-- 预留U位对话框 -->
|
||||
<reserve-unit-dialog
|
||||
v-model:visible="reserveVisible"
|
||||
:rack-id="selectedRackId"
|
||||
:rack-height="rackInfo.height"
|
||||
@success="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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 { fetchRackList } from '@/api/ops/rack'
|
||||
import AllocateUnitDialog from './components/AllocateUnitDialog.vue'
|
||||
import ReserveUnitDialog from './components/ReserveUnitDialog.vue'
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const rackListLoading = ref(false)
|
||||
const selectedRackId = ref<number | undefined>(undefined)
|
||||
const rackInfo = ref<any>({})
|
||||
const rackList = ref<any[]>([])
|
||||
const unitList = ref<any[]>([])
|
||||
|
||||
// 对话框可见性
|
||||
const allocateVisible = ref(false)
|
||||
const reserveVisible = ref(false)
|
||||
|
||||
// 已使用U位
|
||||
const usedUnits = computed(() => {
|
||||
return unitList.value.filter(
|
||||
(unit) => unit.status === 'occupied' || unit.status === 'reserved'
|
||||
).length
|
||||
})
|
||||
|
||||
// 空余U位
|
||||
const availableUnits = computed(() => {
|
||||
return rackInfo.value.height - usedUnits.value
|
||||
})
|
||||
|
||||
// 使用率
|
||||
const usagePercentage = computed(() => {
|
||||
if (!rackInfo.value.height) return 0
|
||||
return Math.round((usedUnits.value / rackInfo.value.height) * 100)
|
||||
})
|
||||
|
||||
// 获取U位状态颜色
|
||||
const getUnitStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
available: 'green',
|
||||
occupied: 'blue',
|
||||
reserved: 'orange',
|
||||
disabled: 'gray',
|
||||
}
|
||||
return colorMap[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
// 获取U位状态文本
|
||||
const getUnitStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
available: '可用',
|
||||
occupied: '已占用',
|
||||
reserved: '已预留',
|
||||
disabled: '已禁用',
|
||||
}
|
||||
return textMap[status || ''] || '-'
|
||||
}
|
||||
|
||||
// 获取机柜列表
|
||||
const fetchRacks = async () => {
|
||||
rackListLoading.value = true
|
||||
|
||||
try {
|
||||
const res = await fetchRackList({
|
||||
page: 1,
|
||||
page_size: 1000,
|
||||
status: 'in_use', // 只获取使用中的机柜
|
||||
})
|
||||
|
||||
rackList.value = res.details?.data || []
|
||||
} catch (error) {
|
||||
console.error('获取机柜列表失败:', error)
|
||||
Message.error('获取机柜列表失败')
|
||||
rackList.value = []
|
||||
} finally {
|
||||
rackListLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 机柜变化
|
||||
const handleRackChange = (rackId: number) => {
|
||||
if (rackId) {
|
||||
fetchUnits(rackId)
|
||||
} else {
|
||||
unitList.value = []
|
||||
rackInfo.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取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) {
|
||||
rackInfo.value = res.data?.rack || {}
|
||||
unitList.value = res.data?.units || []
|
||||
} else {
|
||||
Message.error(res.message || '获取U位列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取U位列表失败:', error)
|
||||
Message.error('获取U位列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
if (selectedRackId.value) {
|
||||
fetchUnits(selectedRackId.value)
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
}
|
||||
|
||||
// 分配U位
|
||||
const handleAllocate = () => {
|
||||
if (!selectedRackId.value) {
|
||||
Message.warning('请先选择机柜')
|
||||
return
|
||||
}
|
||||
allocateVisible.value = true
|
||||
}
|
||||
|
||||
// 预留U位
|
||||
const handleReserve = () => {
|
||||
if (!selectedRackId.value) {
|
||||
Message.warning('请先选择机柜')
|
||||
return
|
||||
}
|
||||
reserveVisible.value = true
|
||||
}
|
||||
|
||||
// 禁用U位
|
||||
const handleDisable = async (record: any) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认禁用',
|
||||
content: `确认禁用 U位 ${record.unit_number} 吗?`,
|
||||
onOk: async () => {
|
||||
if (!selectedRackId.value) {
|
||||
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()
|
||||
} else {
|
||||
Message.error(res.message || '禁用失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('禁用U位失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 启用U位
|
||||
const handleEnable = async (record: any) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认启用',
|
||||
content: `确认启用 U位 ${record.unit_number} 吗?`,
|
||||
onOk: async () => {
|
||||
if (!selectedRackId.value) {
|
||||
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()
|
||||
} else {
|
||||
Message.error(res.message || '启用失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('启用U位失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 释放U位
|
||||
const handleRelease = async (record: any) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认释放',
|
||||
content: `确认释放 U位 ${record.unit_number} 吗?`,
|
||||
onOk: async () => {
|
||||
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()
|
||||
} else {
|
||||
Message.error(res.message || '释放失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('释放U位失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 取消预留
|
||||
const handleCancelReservation = async (record: any) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认取消预留',
|
||||
content: `确认取消 U位 ${record.unit_number} 的预留吗?`,
|
||||
onOk: async () => {
|
||||
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()
|
||||
} else {
|
||||
Message.error(res.message || '取消预留失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('取消预留失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchRacks()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'UPositionManagement',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.rack-select-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.u-position-card {
|
||||
:deep(.arco-card-body) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user