feat: 机柜

This commit is contained in:
ygx
2026-03-27 22:43:14 +08:00
parent 519759ea58
commit 78405c9ec2
2 changed files with 394 additions and 37 deletions

View File

@@ -0,0 +1,357 @@
<template>
<a-input-group>
<a-select
v-model="selectedDatacenterId"
:placeholder="placeholders.datacenter"
:loading="datacenterListLoading"
:disabled="disabled"
style="width: 33.33%"
allow-search
@search="handleDatacenterSearch"
@change="handleDatacenterChange"
>
<a-option
v-for="datacenter in datacenterList"
:key="datacenter.value"
:value="datacenter.value"
>
{{ datacenter.label }}
</a-option>
</a-select>
<a-select
v-model="selectedFloorId"
:placeholder="placeholders.floor"
:loading="floorListLoading"
:disabled="disabled || !selectedDatacenterId"
style="width: 33.33%"
allow-search
@search="handleFloorSearch"
@change="handleFloorChange"
>
<a-option
v-for="floor in floorList"
:key="floor.value"
:value="floor.value"
>
{{ floor.label }}
</a-option>
</a-select>
<a-select
v-model="selectedRackId"
:placeholder="placeholders.rack"
:loading="rackListLoading"
:disabled="disabled || !selectedFloorId"
style="width: 33.33%"
allow-search
@search="handleRackSearch"
@change="handleRackChange"
>
<a-option
v-for="rack in rackList"
:key="rack.id"
:value="rack.id"
>
{{ rack.name }} ({{ rack.code }})
</a-option>
</a-select>
</a-input-group>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import { fetchDatacenterList, fetchRackListByFloor } from '@/api/ops/rack'
import { fetchFloorListByDatacenter } from '@/api/ops/floor'
interface Props {
datacenterId?: number
floorId?: number
rackId?: number
disabled?: boolean
placeholders?: {
datacenter: string
floor: string
rack: string
}
}
const props = withDefaults(defineProps<Props>(), {
datacenterId: undefined,
floorId: undefined,
rackId: undefined,
disabled: false,
placeholders: () => ({
datacenter: '请选择数据中心',
floor: '请选择楼层',
rack: '请选择机柜',
}),
})
const emit = defineEmits<{
'update:datacenterId': [value: number | undefined]
'update:floorId': [value: number | undefined]
'update:rackId': [value: number | undefined]
'change': [value: { datacenterId?: number; floorId?: number; rackId?: number }]
}>()
// 三级联动相关状态
const datacenterListLoading = ref(false)
const floorListLoading = ref(false)
const rackListLoading = ref(false)
const datacenterList = ref<{ label: string; value: number }[]>([])
const floorList = ref<{ label: string; value: number }[]>([])
const rackList = ref<any[]>([])
// 内部选中的值
const selectedDatacenterId = ref<number | undefined>(props.datacenterId)
const selectedFloorId = ref<number | undefined>(props.floorId)
const selectedRackId = ref<number | undefined>(props.rackId)
let datacenterSearchTimer: number | undefined
let floorSearchTimer: number | undefined
let rackSearchTimer: number | undefined
// 提取列表数据
const extractList = (res: any): any[] => {
const candidate =
res?.data?.data ??
res?.details?.data ??
res?.data ??
res?.details ??
[]
return Array.isArray(candidate) ? candidate : []
}
// 获取数据中心列表
const fetchDatacenters = async (keyword?: string) => {
datacenterListLoading.value = true
try {
const res: any = await fetchDatacenterList({ keyword })
const rows = extractList(res)
datacenterList.value = rows.map((d: any) => ({
label: d.name || d.code || String(d.id),
value: d.id,
}))
} catch (error) {
console.error('获取数据中心列表失败:', error)
Message.error('获取数据中心列表失败')
datacenterList.value = []
} finally {
datacenterListLoading.value = false
}
}
// 获取楼层列表
const fetchFloors = async (keyword?: string) => {
if (!selectedDatacenterId.value) {
floorList.value = []
return
}
floorListLoading.value = true
try {
const res: any = await fetchFloorListByDatacenter(selectedDatacenterId.value, {
name: keyword || undefined,
})
const rows = extractList(res)
floorList.value = rows.map((floor: any) => ({
label: floor.name || floor.code || String(floor.id),
value: floor.id,
}))
} catch (error) {
console.error('获取楼层列表失败:', error)
Message.error('获取楼层列表失败')
floorList.value = []
} finally {
floorListLoading.value = false
}
}
// 获取机柜列表
const fetchRacks = async (keyword?: string) => {
if (!selectedFloorId.value) {
rackList.value = []
return
}
rackListLoading.value = true
try {
const res: any = await fetchRackListByFloor(selectedFloorId.value, { name: keyword })
rackList.value = extractList(res)
} catch (error) {
console.error('获取机柜列表失败:', error)
Message.error('获取机柜列表失败')
rackList.value = []
} finally {
rackListLoading.value = false
}
}
// 数据中心搜索
const handleDatacenterSearch = (keyword: string) => {
if (datacenterSearchTimer) {
window.clearTimeout(datacenterSearchTimer)
}
datacenterSearchTimer = window.setTimeout(() => {
fetchDatacenters(keyword?.trim() || undefined)
}, 300)
}
// 楼层搜索
const handleFloorSearch = (keyword: string) => {
if (!selectedDatacenterId.value) return
if (floorSearchTimer) {
window.clearTimeout(floorSearchTimer)
}
floorSearchTimer = window.setTimeout(() => {
fetchFloors(keyword?.trim() || undefined)
}, 300)
}
// 机柜搜索
const handleRackSearch = (keyword: string) => {
if (!selectedFloorId.value) return
if (rackSearchTimer) {
window.clearTimeout(rackSearchTimer)
}
rackSearchTimer = window.setTimeout(() => {
fetchRacks(keyword?.trim() || undefined)
}, 300)
}
// 数据中心变化
const handleDatacenterChange = async (datacenterId?: number | string) => {
if (datacenterId !== undefined && datacenterId !== null && datacenterId !== '') {
selectedDatacenterId.value = Number(datacenterId)
} else {
selectedDatacenterId.value = undefined
}
selectedFloorId.value = undefined
selectedRackId.value = undefined
rackList.value = []
floorList.value = []
emit('update:datacenterId', selectedDatacenterId.value)
emit('update:floorId', undefined)
emit('update:rackId', undefined)
emit('change', {
datacenterId: selectedDatacenterId.value,
floorId: undefined,
rackId: undefined,
})
await fetchFloors()
}
// 楼层变化
const handleFloorChange = async (floorId?: number | string) => {
if (floorId !== undefined && floorId !== null && floorId !== '') {
selectedFloorId.value = Number(floorId)
} else {
selectedFloorId.value = undefined
}
selectedRackId.value = undefined
rackList.value = []
emit('update:floorId', selectedFloorId.value)
emit('update:rackId', undefined)
emit('change', {
datacenterId: selectedDatacenterId.value,
floorId: selectedFloorId.value,
rackId: undefined,
})
await fetchRacks()
}
// 机柜变化
const handleRackChange = (rackId?: number | string) => {
if (rackId !== undefined && rackId !== null && rackId !== '') {
selectedRackId.value = Number(rackId)
} else {
selectedRackId.value = undefined
}
emit('update:rackId', selectedRackId.value)
emit('change', {
datacenterId: selectedDatacenterId.value,
floorId: selectedFloorId.value,
rackId: selectedRackId.value,
})
}
// 重置方法
const reset = () => {
selectedDatacenterId.value = undefined
selectedFloorId.value = undefined
selectedRackId.value = undefined
floorList.value = []
rackList.value = []
}
// 初始化加载
const initLoad = async () => {
await fetchDatacenters()
// 如果有初始值,加载对应的下级列表
if (props.datacenterId) {
selectedDatacenterId.value = props.datacenterId
await fetchFloors()
}
if (props.floorId) {
selectedFloorId.value = props.floorId
await fetchRacks()
}
if (props.rackId) {
selectedRackId.value = props.rackId
}
}
// 监听外部传入的值变化
watch(
() => props.datacenterId,
(val) => {
if (val !== selectedDatacenterId.value) {
selectedDatacenterId.value = val
if (val) {
fetchFloors()
} else {
floorList.value = []
rackList.value = []
}
}
}
)
watch(
() => props.floorId,
(val) => {
if (val !== selectedFloorId.value) {
selectedFloorId.value = val
if (val) {
fetchRacks()
} else {
rackList.value = []
}
}
}
)
watch(
() => props.rackId,
(val) => {
selectedRackId.value = val
}
)
// 暴露方法给父组件
defineExpose({
reset,
initLoad,
fetchDatacenters,
fetchFloors,
fetchRacks,
})
onMounted(() => {
initLoad()
})
</script>

View File

@@ -47,27 +47,13 @@
</a-col> </a-col>
</a-row> </a-row>
<a-form-item field="location" label="位置/机房信息"> <a-form-item field="rack_id" label="数据中心/楼层/机柜">
<a-input-group> <DatacenterSelector
<a-input ref="datacenterSelectorRef"
v-model="formData.location" v-model:datacenter-id="formData.datacenter_id"
placeholder="请输入或选择位置信息" v-model:floor-id="formData.floor_id"
v-model:rack-id="formData.rack_id"
/> />
<a-select
v-model="selectedLocation"
placeholder="选择位置"
style="width: 200px"
@change="handleLocationSelect"
>
<a-option
v-for="location in locationOptions"
:key="location.value"
:value="location.value"
>
{{ location.label }}
</a-option>
</a-select>
</a-input-group>
</a-form-item> </a-form-item>
<a-form-item field="tags" label="服务器标签"> <a-form-item field="tags" label="服务器标签">
@@ -125,6 +111,7 @@ import { ref, reactive, computed, watch } from 'vue'
import { Message } from '@arco-design/web-vue' import { Message } from '@arco-design/web-vue'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import type { FormInstance } from '@arco-design/web-vue' import type { FormInstance } from '@arco-design/web-vue'
import DatacenterSelector from './DatacenterSelector.vue'
interface Props { interface Props {
visible: boolean visible: boolean
@@ -139,7 +126,7 @@ const emit = defineEmits(['update:visible', 'success'])
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const confirmLoading = ref(false) const confirmLoading = ref(false)
const selectedLocation = ref('') const datacenterSelectorRef = ref<InstanceType<typeof DatacenterSelector>>()
const isEdit = computed(() => !!props.record?.id) const isEdit = computed(() => !!props.record?.id)
@@ -148,7 +135,9 @@ const formData = reactive({
name: '', name: '',
server_type: '', server_type: '',
os: '', os: '',
location: '', datacenter_id: undefined as number | undefined,
floor_id: undefined as number | undefined,
rack_id: undefined as number | undefined,
tags: '', tags: '',
ip: '', ip: '',
remote_port: '', remote_port: '',
@@ -164,27 +153,41 @@ const rules = {
os: [{ required: true, message: '请选择操作系统' }], os: [{ required: true, message: '请选择操作系统' }],
} }
const locationOptions = ref([
{ label: 'A数据中心-3层-24机柜-5U位', value: 'A数据中心-3层-24机柜-5U位' },
{ label: 'A数据中心-3层-24机柜-6U位', value: 'A数据中心-3层-24机柜-6U位' },
{ label: 'B数据中心-1层-12机柜-1U位', value: 'B数据中心-1层-12机柜-1U位' },
{ label: 'B数据中心-1层-12机柜-2U位', value: 'B数据中心-1层-12机柜-2U位' },
{ label: 'C数据中心-2层-8机柜-3U位', value: 'C数据中心-2层-8机柜-3U位' },
])
watch( watch(
() => props.visible, () => props.visible,
(val) => { (val) => {
if (val) { if (val) {
if (isEdit.value && props.record) { if (isEdit.value && props.record) {
Object.assign(formData, props.record) Object.assign(formData, {
unique_id: props.record.unique_id || '',
name: props.record.name || '',
server_type: props.record.server_type || '',
os: props.record.os || '',
datacenter_id: props.record.datacenter_id,
floor_id: props.record.floor_id,
rack_id: props.record.rack_id,
tags: props.record.tags || '',
ip: props.record.ip || '',
remote_port: props.record.remote_port || '',
agent_url: props.record.agent_url || '',
data_collection: props.record.data_collection || false,
collection_interval: props.record.collection_interval || 5,
remark: props.record.remark || '',
})
// 编辑模式下初始化加载下级列表
if (props.record.datacenter_id) {
datacenterSelectorRef.value?.initLoad()
}
} else { } else {
Object.assign(formData, { Object.assign(formData, {
unique_id: '', unique_id: '',
name: '', name: '',
server_type: '', server_type: '',
os: '', os: '',
location: '', datacenter_id: undefined,
floor_id: undefined,
rack_id: undefined,
tags: '', tags: '',
ip: '', ip: '',
remote_port: '', remote_port: '',
@@ -193,15 +196,12 @@ watch(
collection_interval: 5, collection_interval: 5,
remark: '', remark: '',
}) })
datacenterSelectorRef.value?.reset()
} }
} }
} }
) )
const handleLocationSelect = (value: string) => {
formData.location = value
}
const handleOk = async () => { const handleOk = async () => {
try { try {
await formRef.value?.validate() await formRef.value?.validate()