This commit is contained in:
zxr
2026-04-14 16:17:31 +08:00
parent b8a7ba1cbb
commit 8db158c390
16 changed files with 1174 additions and 42 deletions

View File

@@ -17,6 +17,7 @@ export interface ServerItem {
server_type: string
tags: string
location: string
asset_id?: number
remote_access: string
remote_port: number
agent_config: string
@@ -60,6 +61,7 @@ export interface ServerFormData {
server_type?: string
tags?: string
location?: string
asset_id?: number
remote_access?: string
remote_port?: number
agent_config?: string

View File

@@ -766,6 +766,22 @@ export const localMenuFlatItems: MenuItem[] = [
sort_key: 39,
created_at: '2025-12-26T13:23:52.220159+08:00',
},
{
id: 53,
identity: '019d0000-0000-7000-8000-000000000053',
title: '机房管理',
title_en: 'Room Management',
code: 'ops:数据中心管理:机房管理',
description: '数据中心管理 - 机房管理',
app_id: 2,
parent_id: 49,
menu_path: '/datacenter/room',
menu_icon: 'appstore',
component: 'ops/pages/datacenter/room',
type: 1,
sort_key: 39.5,
created_at: '2026-04-14T10:00:00+08:00',
},
{
id: 54,
identity: '019b591d-0343-7ce7-91bd-d82497ea0a11',

View File

@@ -824,6 +824,23 @@ export const localMenuItems: MenuItem[] = [
created_at: '2025-12-26T13:23:52.220159+08:00',
children: [],
},
{
id: 53,
identity: '019d0000-0000-7000-8000-000000000053',
title: '机房管理',
title_en: 'Room Management',
code: 'ops:数据中心管理:机房管理',
description: '数据中心管理 - 机房管理',
app_id: 2,
parent_id: 49,
menu_path: '/datacenter/room',
menu_icon: 'appstore',
component: 'ops/pages/datacenter/room',
type: 1,
sort_key: 9,
created_at: '2026-04-14T10:00:00+08:00',
children: [],
},
],
},
{

View File

@@ -208,7 +208,7 @@
<!-- 位置信息 -->
<a-card class="info-card" title="位置信息">
<a-row :gutter="16">
<a-col :span="6">
<a-col :span="5">
<a-form-item label="所属数据中心" field="datacenter_id">
<a-select
v-model="form.datacenter_id"
@@ -229,7 +229,7 @@
</a-select>
</a-form-item>
</a-col>
<a-col :span="6">
<a-col :span="5">
<a-form-item label="所属楼层" field="floor_id">
<a-select
v-model="form.floor_id"
@@ -251,7 +251,30 @@
</a-select>
</a-form-item>
</a-col>
<a-col :span="6">
<a-col :span="5">
<a-form-item label="所属机房" field="room_id">
<a-select
v-model="form.room_id"
placeholder="请选择机房"
allow-clear
allow-search
:loading="roomLoading"
:disabled="!form.floor_id"
:filter-option="false"
@search="handleRoomSearch"
@change="handleRoomChange"
>
<a-option
v-for="item in roomOptions"
:key="item.id"
:value="item.id"
>
{{ item.name }} ({{ item.code }})
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="5">
<a-form-item label="所属机柜" field="rack_id">
<a-select
v-model="form.rack_id"
@@ -259,7 +282,7 @@
allow-clear
allow-search
:loading="rackLoading"
:disabled="!form.floor_id"
:disabled="!form.room_id"
:filter-option="false"
@search="handleRackSearch"
>
@@ -273,7 +296,7 @@
</a-select>
</a-form-item>
</a-col>
<a-col :span="3">
<a-col :span="2">
<a-form-item label="起始U位" field="unit_start">
<a-input-number
v-model="form.unit_start"
@@ -284,7 +307,7 @@
/>
</a-form-item>
</a-col>
<a-col :span="3">
<a-col :span="2">
<a-form-item label="结束U位" field="unit_end">
<a-input-number
v-model="form.unit_end"
@@ -387,8 +410,9 @@ import {
AssetForm,
} from '@/api/ops/asset'
import { fetchAllSuppliers } from '@/api/ops/supplier'
import { fetchDatacenterList, fetchRackListByFloor } from '@/api/ops/rack'
import { fetchDatacenterList, fetchRackListByRoom } from '@/api/ops/rack'
import { fetchFloorListByDatacenter } from '@/api/ops/floor'
import { fetchRoomListByFloor } from '@/api/ops/room'
const router = useRouter()
const route = useRoute()
@@ -406,6 +430,7 @@ const categoryOptions = ref<any[]>([])
const supplierOptions = ref<any[]>([])
const datacenterOptions = ref<{ label: string; value: number }[]>([])
const floorOptions = ref<{ label: string; value: number }[]>([])
const roomOptions = ref<any[]>([])
const rackOptions = ref<any[]>([])
// 加载状态
@@ -413,11 +438,13 @@ const categoryLoading = ref(false)
const supplierLoading = ref(false)
const datacenterLoading = ref(false)
const floorLoading = ref(false)
const roomLoading = ref(false)
const rackLoading = ref(false)
// 搜索防抖定时器
let datacenterSearchTimer: number | undefined
let floorSearchTimer: number | undefined
let roomSearchTimer: number | undefined
let rackSearchTimer: number | undefined
// 表单数据
@@ -574,13 +601,13 @@ const handleFloorSearch = (keyword: string) => {
// 加载机柜列表
const fetchRacks = async (keyword?: string) => {
if (!form.value.floor_id) {
if (!form.value.room_id) {
rackOptions.value = []
return
}
rackLoading.value = true
try {
const res: any = await fetchRackListByFloor(form.value.floor_id, { name: keyword })
const res: any = await fetchRackListByRoom(form.value.room_id, { name: keyword })
rackOptions.value = extractList(res)
} catch (error) {
console.error('获取机柜列表失败:', error)
@@ -592,7 +619,7 @@ const fetchRacks = async (keyword?: string) => {
// 机柜搜索(带防抖)
const handleRackSearch = (keyword: string) => {
if (!form.value.floor_id) return
if (!form.value.room_id) return
if (rackSearchTimer) {
window.clearTimeout(rackSearchTimer)
}
@@ -601,11 +628,44 @@ const handleRackSearch = (keyword: string) => {
}, 300)
}
// 加载机房列表
const fetchRooms = async (keyword?: string) => {
if (!form.value.floor_id) {
roomOptions.value = []
return
}
roomLoading.value = true
try {
const res: any = await fetchRoomListByFloor(form.value.floor_id, {
name: keyword || undefined,
})
roomOptions.value = extractList(res)
} catch (error) {
console.error('获取机房列表失败:', error)
roomOptions.value = []
} finally {
roomLoading.value = false
}
}
// 机房搜索(带防抖)
const handleRoomSearch = (keyword: string) => {
if (!form.value.floor_id) return
if (roomSearchTimer) {
window.clearTimeout(roomSearchTimer)
}
roomSearchTimer = window.setTimeout(() => {
fetchRooms(keyword?.trim() || undefined)
}, 300)
}
// 数据中心变化时加载楼层
const handleDatacenterChange = async (value: number | undefined) => {
form.value.floor_id = undefined
form.value.room_id = undefined
form.value.rack_id = undefined
floorOptions.value = []
roomOptions.value = []
rackOptions.value = []
if (value) {
@@ -615,6 +675,18 @@ const handleDatacenterChange = async (value: number | undefined) => {
// 楼层变化时加载机柜
const handleFloorChange = async (value: number | undefined) => {
form.value.room_id = undefined
form.value.rack_id = undefined
roomOptions.value = []
rackOptions.value = []
if (value) {
await fetchRooms()
}
}
// 机房变化时加载机柜
const handleRoomChange = async (value: number | undefined) => {
form.value.rack_id = undefined
rackOptions.value = []
@@ -675,7 +747,17 @@ const loadDeviceDetail = async () => {
// 如果有楼层,加载机柜
if (res.details.floor_id) {
try {
const rackRes: any = await fetchRackListByFloor(res.details.floor_id)
const roomRes: any = await fetchRoomListByFloor(res.details.floor_id)
roomOptions.value = extractList(roomRes)
} catch (error) {
console.error('加载机房失败:', error)
}
}
// 如果有机房,加载机柜
if (res.details.room_id) {
try {
const rackRes: any = await fetchRackListByRoom(res.details.room_id)
rackOptions.value = extractList(rackRes)
} catch (error) {
console.error('加载机柜失败:', error)

View File

@@ -56,6 +56,16 @@
/>
</a-form-item>
<a-form-item label="状态" field="status">
<a-select v-model="form.status" placeholder="请选择状态">
<a-option value="planning">规划中</a-option>
<a-option value="construction">建设中</a-option>
<a-option value="operating">运营中</a-option>
<a-option value="maintenance">维护中</a-option>
<a-option value="offline">已下线</a-option>
</a-select>
</a-form-item>
<a-form-item
label="面积(平方米)"
field="area"
@@ -152,6 +162,7 @@ interface Floor {
name?: string
datacenter_id?: number
floor_number?: number
status?: string
area?: number
height?: number
load_bearing?: number
@@ -184,6 +195,7 @@ const form = ref({
name: '',
datacenter_id: undefined as number | undefined,
floor_number: 1,
status: 'planning',
area: undefined as number | undefined,
height: undefined as number | undefined,
load_bearing: undefined as number | undefined,
@@ -252,6 +264,7 @@ watch(
name: props.floor.name || '',
datacenter_id: props.floor.datacenter_id,
floor_number: props.floor.floor_number || 1,
status: props.floor.status || 'planning',
area: props.floor.area,
height: props.floor.height,
load_bearing: props.floor.load_bearing,
@@ -277,6 +290,7 @@ watch(
name: '',
datacenter_id: undefined,
floor_number: 1,
status: 'planning',
area: undefined,
height: undefined,
load_bearing: undefined,
@@ -301,6 +315,7 @@ const handleOk = async () => {
name: form.value.name,
datacenter_id: form.value.datacenter_id,
floor_number: form.value.floor_number,
status: form.value.status,
area: form.value.area,
height: form.value.height,
load_bearing: form.value.load_bearing,

View File

@@ -74,7 +74,9 @@
v-model="form.floor_id"
placeholder="请选择所属楼层"
:loading="loadingFloors"
:disabled="!form.datacenter_id"
allow-search
@change="handleFloorChange"
@search="handleFloorSearch"
>
<a-option
@@ -88,6 +90,32 @@
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="所属机房"
field="room_id"
:rules="[{ required: true, message: '请选择所属机房' }]"
>
<a-select
v-model="form.room_id"
placeholder="请选择所属机房"
:loading="loadingRooms"
:disabled="!form.floor_id"
allow-search
@search="handleRoomSearch"
>
<a-option
v-for="item in roomList"
: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>
@@ -438,9 +466,10 @@ import { Message } from '@arco-design/web-vue'
import { createRack, updateRack } from '@/api/ops/rack'
import {
fetchDatacenterList,
fetchRackListByDatacenter,
fetchSupplierList,
} from '@/api/ops/rack'
import { fetchFloorListByDatacenter } from '@/api/ops/floor'
import { fetchRoomListByFloor } from '@/api/ops/room'
interface Rack {
id?: number
@@ -448,6 +477,7 @@ interface Rack {
code?: string
datacenter_id?: number
floor_id?: number
room_id?: number
height?: number
width?: number
depth?: number
@@ -496,12 +526,15 @@ const emit = defineEmits<Emits>()
const formRef = ref()
const loadingDatacenters = ref(false)
const loadingFloors = ref(false)
const loadingRooms = ref(false)
const loadingSuppliers = ref(false)
const submitting = ref(false)
const datacenterList = ref<any[]>([])
const floorList = ref<any[]>([])
const roomList = ref<any[]>([])
const supplierList = ref<any[]>([])
let floorSearchTimer: number | undefined
let roomSearchTimer: number | undefined
// 表单数据
const form = ref({
@@ -509,6 +542,7 @@ const form = ref({
code: '',
datacenter_id: undefined as number | undefined,
floor_id: undefined as number | undefined,
room_id: undefined as number | undefined,
height: 42,
width: undefined as number | undefined,
depth: undefined as number | undefined,
@@ -559,7 +593,7 @@ const loadDatacenterList = async () => {
}
}
// 加载楼层列表(通过机柜下拉接口提取去重楼层)
// 加载楼层列表
const loadFloorList = async (datacenterId?: number, keyword?: string) => {
if (!datacenterId) {
floorList.value = []
@@ -567,20 +601,10 @@ const loadFloorList = async (datacenterId?: number, keyword?: string) => {
}
loadingFloors.value = true
try {
const res: any = await fetchRackListByDatacenter(datacenterId, { name: keyword })
const res: any = await fetchFloorListByDatacenter(datacenterId, { name: keyword })
if (res.code === 0) {
const list = res.details?.data ?? res.data ?? res.details ?? []
const rows = Array.isArray(list) ? list : []
const floorMap = new Map<number, { id: number; name: string }>()
rows.forEach((rack: any) => {
const floor = rack?.floor
if (!floor?.id || floorMap.has(floor.id)) return
floorMap.set(floor.id, {
id: floor.id,
name: floor.name || String(floor.id),
})
})
floorList.value = Array.from(floorMap.values())
floorList.value = Array.isArray(list) ? list : []
}
} catch (error) {
console.error('获取楼层列表失败:', error)
@@ -607,9 +631,36 @@ const loadSupplierList = async () => {
// 数据中心变化时重新加载楼层列表
const handleDatacenterChange = async (value: number) => {
form.value.floor_id = undefined
form.value.room_id = undefined
roomList.value = []
await loadFloorList(value)
}
const loadRoomList = async (floorId?: number, keyword?: string) => {
if (!floorId) {
roomList.value = []
return
}
loadingRooms.value = true
try {
const res: any = await fetchRoomListByFloor(floorId, { name: keyword })
if (res.code === 0) {
const list = res.details?.data ?? res.data ?? res.details ?? []
roomList.value = Array.isArray(list) ? list : []
}
} catch (error) {
console.error('获取机房列表失败:', error)
roomList.value = []
} finally {
loadingRooms.value = false
}
}
const handleFloorChange = async (value: number) => {
form.value.room_id = undefined
await loadRoomList(value)
}
const handleFloorSearch = (keyword: string) => {
if (!form.value.datacenter_id) return
if (floorSearchTimer) {
@@ -620,6 +671,16 @@ const handleFloorSearch = (keyword: string) => {
}, 300)
}
const handleRoomSearch = (keyword: string) => {
if (!form.value.floor_id) return
if (roomSearchTimer) {
window.clearTimeout(roomSearchTimer)
}
roomSearchTimer = window.setTimeout(() => {
loadRoomList(form.value.floor_id, keyword?.trim() || undefined)
}, 300)
}
// 监听对话框显示状态
watch(
() => props.visible,
@@ -632,6 +693,7 @@ watch(
code: props.rack.code || '',
datacenter_id: props.rack.datacenter_id,
floor_id: props.rack.floor_id,
room_id: props.rack.room_id,
height: props.rack.height || 42,
width: props.rack.width,
depth: props.rack.depth,
@@ -667,6 +729,9 @@ watch(
if (props.rack.datacenter_id) {
loadFloorList(props.rack.datacenter_id)
}
if (props.rack.floor_id) {
loadRoomList(props.rack.floor_id)
}
} else {
// 新建模式:重置表单
form.value = {
@@ -674,6 +739,7 @@ watch(
code: '',
datacenter_id: undefined,
floor_id: undefined,
room_id: undefined,
height: 42,
width: undefined,
depth: undefined,
@@ -705,6 +771,8 @@ watch(
description: '',
remarks: '',
}
floorList.value = []
roomList.value = []
}
}
}
@@ -722,6 +790,7 @@ const handleOk = async () => {
code: form.value.code,
datacenter_id: form.value.datacenter_id,
floor_id: form.value.floor_id,
room_id: form.value.room_id,
height: form.value.height,
width: form.value.width,
depth: form.value.depth,

View File

@@ -24,6 +24,14 @@ export const searchFormConfig: FormItem[] = [
options: [], // 需要动态加载
span: 6,
},
{
field: 'room_id',
label: '机房',
type: 'select',
placeholder: '请选择机房',
options: [], // 需要动态加载
span: 6,
},
{
field: 'rack_type',
label: '机柜类型',

View File

@@ -113,9 +113,10 @@ import { columns as columnsConfig } from './config/columns'
import {
fetchRackList,
fetchDatacenterList,
fetchRackListByDatacenter,
deleteRack,
} from '@/api/ops/rack'
import { fetchFloorListByDatacenter } from '@/api/ops/floor'
import { fetchRoomListByFloor } from '@/api/ops/room'
import RackDetailDialog from './components/RackDetailDialog.vue'
import RackFormDialog from './components/RackFormDialog.vue'
@@ -128,6 +129,7 @@ const formModel = ref({
keyword: '',
datacenter_id: undefined,
floor_id: undefined,
room_id: undefined,
rack_type: undefined,
status: undefined,
})
@@ -141,8 +143,10 @@ const pagination = reactive({
// 表单项配置
const datacenterSelectOptions = ref<{ label: string; value: number }[]>([])
const floorSelectOptions = ref<{ label: string; value: number }[]>([])
const roomSelectOptions = ref<{ label: string; value: number }[]>([])
let datacenterSearchTimer: number | undefined
let floorSearchTimer: number | undefined
let roomSearchTimer: number | undefined
const formItems = computed<FormItem[]>(() =>
searchFormConfig.map((item) => {
@@ -163,6 +167,15 @@ const formItems = computed<FormItem[]>(() =>
disabled: !formModel.value.datacenter_id,
}
}
if (item.field === 'room_id') {
return {
...item,
options: roomSelectOptions.value,
allowSearch: true,
onSearch: handleRoomSearch,
disabled: !formModel.value.floor_id,
}
}
return item
}),
)
@@ -199,25 +212,18 @@ const loadDatacenterOptions = async (keyword?: string) => {
const loadFloorOptions = async (datacenterId?: number, keyword?: string) => {
if (!datacenterId) {
floorSelectOptions.value = []
roomSelectOptions.value = []
return
}
try {
const res: any = await fetchRackListByDatacenter(datacenterId, {
name: keyword,
})
const res: any = await fetchFloorListByDatacenter(datacenterId, { name: keyword })
if (res.code === 0) {
const list = res.details?.data ?? res.data ?? res.details ?? []
const rows = Array.isArray(list) ? list : []
const floorMap = new Map<number, { label: string; value: number }>()
rows.forEach((rack: any) => {
const floor = rack?.floor
if (!floor?.id || floorMap.has(floor.id)) return
floorMap.set(floor.id, {
label: floor.name || String(floor.id),
value: floor.id,
})
})
floorSelectOptions.value = Array.from(floorMap.values())
floorSelectOptions.value = rows.map((floor: any) => ({
label: floor.name || String(floor.id),
value: floor.id,
}))
}
} catch (error) {
console.error('加载楼层列表失败:', error)
@@ -226,6 +232,28 @@ const loadFloorOptions = async (datacenterId?: number, keyword?: string) => {
}
}
const loadRoomOptions = async (floorId?: number, keyword?: string) => {
if (!floorId) {
roomSelectOptions.value = []
return
}
try {
const res: any = await fetchRoomListByFloor(floorId, { name: keyword })
if (res.code === 0) {
const list = res.details?.data ?? res.data ?? res.details ?? []
const rows = Array.isArray(list) ? list : []
roomSelectOptions.value = rows.map((room: any) => ({
label: room.name || String(room.id),
value: room.id,
}))
}
} catch (error) {
console.error('加载机房列表失败:', error)
Message.error('加载机房列表失败')
roomSelectOptions.value = []
}
}
const handleDatacenterSearch = (keyword: string) => {
if (datacenterSearchTimer) {
window.clearTimeout(datacenterSearchTimer)
@@ -245,16 +273,37 @@ const handleFloorSearch = (keyword: string) => {
}, 300)
}
const handleRoomSearch = (keyword: string) => {
if (!formModel.value.floor_id) return
if (roomSearchTimer) {
window.clearTimeout(roomSearchTimer)
}
roomSearchTimer = window.setTimeout(() => {
loadRoomOptions(formModel.value.floor_id, keyword?.trim() || undefined)
}, 300)
}
watch(
() => formModel.value.datacenter_id,
(newId, oldId) => {
if (newId !== oldId) {
formModel.value.floor_id = undefined
formModel.value.room_id = undefined
}
loadFloorOptions(newId)
},
)
watch(
() => formModel.value.floor_id,
(newId, oldId) => {
if (newId !== oldId) {
formModel.value.room_id = undefined
}
loadRoomOptions(newId)
},
)
// 获取机柜类型颜色
const getRackTypeColor = (type?: string) => {
const colorMap: Record<string, string> = {
@@ -314,6 +363,7 @@ const fetchRacks = async () => {
keyword: formModel.value.keyword || undefined,
datacenter_id: formModel.value.datacenter_id ?? undefined,
floor_id: formModel.value.floor_id ?? undefined,
room_id: formModel.value.room_id ?? undefined,
rack_type: formModel.value.rack_type || undefined,
status: formModel.value.status || undefined,
}
@@ -350,6 +400,7 @@ const handleReset = () => {
keyword: '',
datacenter_id: undefined,
floor_id: undefined,
room_id: undefined,
rack_type: undefined,
status: undefined,
}
@@ -411,7 +462,7 @@ const handleDelete = async (record: any) => {
// U位管理
const handleUnitManagement = (record: any) => {
router.push({
path: '/ops/datacenter/u-position',
path: '/datacenter/u-position',
query: { rack_id: record.id, rack_name: record.name },
})
}

View File

@@ -0,0 +1,124 @@
<template>
<a-modal
:visible="visible"
title="机房详情"
width="700px"
@cancel="handleCancel"
@update:visible="handleVisibleChange"
:footer="false"
>
<a-spin :loading="loading" style="width: 100%">
<a-descriptions :column="2" bordered v-if="roomDetail">
<a-descriptions-item label="机房名称" :span="2">
{{ roomDetail.name || '-' }}
</a-descriptions-item>
<a-descriptions-item label="机房编码">
{{ roomDetail.code || '-' }}
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="statusMap[roomDetail.status]?.color || 'gray'">
{{ statusMap[roomDetail.status]?.text || roomDetail.status || '-' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="所属中心">
{{ roomDetail.datacenter?.name || '-' }}
</a-descriptions-item>
<a-descriptions-item label="所属楼层">
{{ roomDetail.floor?.name || '-' }}
</a-descriptions-item>
<a-descriptions-item label="描述" :span="2">
{{ roomDetail.description || '-' }}
</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ formatDate(roomDetail.created_at) }}
</a-descriptions-item>
<a-descriptions-item label="更新时间">
{{ formatDate(roomDetail.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 { fetchRoomDetail } from '@/api/ops/room'
interface Props {
visible: boolean
roomId?: number
}
interface Emits {
(e: 'update:visible', value: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const loading = ref(false)
const roomDetail = ref<any>(null)
const statusMap: Record<string, { text: string; color: string }> = {
planning: { text: '规划中', color: 'blue' },
construction: { text: '建设中', color: 'orange' },
operating: { text: '运营中', color: 'green' },
maintenance: { text: '维护中', color: 'gold' },
offline: { text: '已下线', color: 'red' },
}
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 loadRoomDetail = async () => {
if (!props.roomId) return
loading.value = true
try {
const res: any = await fetchRoomDetail(props.roomId)
if (res.code === 0) {
roomDetail.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.roomId) {
loadRoomDetail()
}
},
)
const handleCancel = () => {
emit('update:visible', false)
}
const handleVisibleChange = (visible: boolean) => {
emit('update:visible', visible)
}
</script>
<script lang="ts">
export default {
name: 'RoomDetailDialog',
}
</script>

View File

@@ -0,0 +1,269 @@
<template>
<a-modal
:visible="visible"
:title="isEdit ? '编辑机房' : '新建机房'"
width="700px"
@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="200" />
</a-form-item>
<a-form-item
label="机房编码"
field="code"
:rules="[{ required: true, message: '请输入机房编码' }]"
>
<a-input v-model="form.code" placeholder="请输入机房编码" :max-length="100" />
</a-form-item>
<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-form-item
label="所属楼层"
field="floor_id"
:rules="[{ required: true, message: '请选择所属楼层' }]"
>
<a-select
v-model="form.floor_id"
placeholder="请选择所属楼层"
:loading="loadingFloors"
:disabled="!form.datacenter_id"
allow-search
@search="handleFloorSearch"
>
<a-option v-for="item in floorList" :key="item.id" :value="item.id">
{{ item.name }}
</a-option>
</a-select>
</a-form-item>
<a-form-item label="状态" field="status">
<a-select v-model="form.status" placeholder="请选择状态">
<a-option value="planning">规划中</a-option>
<a-option value="construction">建设中</a-option>
<a-option value="operating">运营中</a-option>
<a-option value="maintenance">维护中</a-option>
<a-option value="offline">已下线</a-option>
</a-select>
</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 { computed, ref, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { createRoom, updateRoom } from '@/api/ops/room'
import { fetchDatacenterList } from '@/api/ops/floor'
import { fetchFloorListByDatacenter } from '@/api/ops/floor'
interface Room {
id?: number
name?: string
code?: string
datacenter_id?: number
floor_id?: number
status?: string
description?: string
}
interface Props {
visible: boolean
room: Room | 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 submitting = ref(false)
const datacenterList = ref<any[]>([])
const floorList = ref<any[]>([])
let floorSearchTimer: number | undefined
const form = ref({
name: '',
code: '',
datacenter_id: undefined as number | undefined,
floor_id: undefined as number | undefined,
status: 'planning',
description: '',
})
const isEdit = computed(() => !!props.room?.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, keyword?: string) => {
if (!datacenterId) {
floorList.value = []
return
}
loadingFloors.value = true
try {
const res: any = await fetchFloorListByDatacenter(datacenterId, { name: keyword })
if (res.code === 0) {
const list = res.details?.data ?? res.data ?? res.details ?? []
floorList.value = Array.isArray(list) ? list : []
}
} catch (error) {
console.error('获取楼层列表失败:', error)
floorList.value = []
} finally {
loadingFloors.value = false
}
}
const handleDatacenterChange = async (value: number) => {
form.value.floor_id = undefined
await loadFloorList(value)
}
const handleFloorSearch = (keyword: string) => {
if (!form.value.datacenter_id) return
if (floorSearchTimer) {
window.clearTimeout(floorSearchTimer)
}
floorSearchTimer = window.setTimeout(() => {
loadFloorList(form.value.datacenter_id, keyword?.trim() || undefined)
}, 300)
}
watch(
() => props.visible,
async (newVal) => {
if (!newVal) return
if (props.room && isEdit.value) {
form.value = {
name: props.room.name || '',
code: props.room.code || '',
datacenter_id: props.room.datacenter_id,
floor_id: props.room.floor_id,
status: props.room.status || 'planning',
description: props.room.description || '',
}
if (props.room.datacenter_id) {
await loadFloorList(props.room.datacenter_id)
}
} else {
form.value = {
name: '',
code: '',
datacenter_id: undefined,
floor_id: undefined,
status: 'planning',
description: '',
}
floorList.value = []
}
},
)
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,
status: form.value.status,
description: form.value.description,
}
let res
if (isEdit.value && props.room?.id) {
data.id = props.room.id
res = await updateRoom(data)
} else {
res = await createRoom(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)
}
loadDatacenterList()
</script>
<script lang="ts">
export default {
name: 'RoomFormDialog',
}
</script>

View File

@@ -0,0 +1,52 @@
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: 'code',
width: 140,
},
{
title: '所属中心',
dataIndex: 'datacenter',
slotName: 'datacenter',
width: 160,
},
{
title: '所属楼层',
dataIndex: 'floor',
slotName: 'floor',
width: 160,
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
width: 120,
},
{
title: '描述',
dataIndex: 'description',
ellipsis: true,
tooltip: true,
width: 220,
},
{
title: '操作',
dataIndex: 'actions',
slotName: 'actions',
width: 240,
fixed: 'right' as const,
},
]

View File

@@ -0,0 +1,37 @@
import type { FormItem } from '@/components/search-form/types'
export const searchFormConfig: FormItem[] = [
{
field: 'keyword',
label: '关键词',
type: 'input',
placeholder: '请输入机房名称或编码',
},
{
field: 'datacenter_id',
label: '数据中心',
type: 'select',
placeholder: '请选择数据中心',
options: [],
},
{
field: 'floor_id',
label: '所属楼层',
type: 'select',
placeholder: '请选择所属楼层',
options: [],
},
{
field: 'status',
label: '状态',
type: 'select',
placeholder: '请选择状态',
options: [
{ label: '规划中', value: 'planning' },
{ label: '建设中', value: 'construction' },
{ label: '运营中', value: 'operating' },
{ label: '维护中', value: 'maintenance' },
{ label: '已下线', value: 'offline' },
],
},
]

View File

@@ -0,0 +1,326 @@
<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 #datacenter="{ record }">
{{ record.datacenter?.name || '-' }}
</template>
<template #floor="{ record }">
{{ record.floor?.name || '-' }}
</template>
<template #status="{ record }">
<a-tag :color="statusMap[record.status]?.color || 'gray'">
{{ statusMap[record.status]?.text || record.status }}
</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)">
删除
</a-button>
</template>
</search-table>
<room-form-dialog
v-model:visible="formVisible"
:room="editingRoom"
@success="handleFormSuccess"
/>
<room-detail-dialog
v-model:visible="detailVisible"
:room-id="currentRoomId"
/>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, ref, watch } 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 { deleteRoom, fetchRoomList } from '@/api/ops/room'
import { fetchDatacenterList, fetchFloorListByDatacenter } from '@/api/ops/floor'
import RoomFormDialog from './components/RoomFormDialog.vue'
import RoomDetailDialog from './components/RoomDetailDialog.vue'
const statusMap: Record<string, { text: string; color: string }> = {
planning: { text: '规划中', color: 'blue' },
construction: { text: '建设中', color: 'orange' },
operating: { text: '运营中', color: 'green' },
maintenance: { text: '维护中', color: 'gold' },
offline: { text: '已下线', color: 'red' },
}
const loading = ref(false)
const tableData = ref<any[]>([])
const formModel = ref({
keyword: '',
datacenter_id: undefined as number | undefined,
floor_id: undefined as number | undefined,
status: '',
})
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
})
const datacenterSelectOptions = ref<{ label: string; value: number }[]>([])
const floorSelectOptions = ref<{ label: string; value: number }[]>([])
let datacenterSearchTimer: number | undefined
let floorSearchTimer: number | undefined
const formItems = computed<FormItem[]>(() =>
searchFormConfig.map((item) => {
if (item.field === 'datacenter_id') {
return {
...item,
options: datacenterSelectOptions.value,
allowSearch: true,
onSearch: handleDatacenterSearch,
}
}
if (item.field === 'floor_id') {
return {
...item,
options: floorSelectOptions.value,
allowSearch: true,
onSearch: handleFloorSearch,
disabled: !formModel.value.datacenter_id,
}
}
return item
}),
)
const columns = computed(() => columnsConfig)
const currentRoomId = ref<number | undefined>(undefined)
const editingRoom = ref<any>(null)
const formVisible = ref(false)
const detailVisible = ref(false)
const loadDatacenterOptions = async (keyword?: string) => {
try {
const res: any = await fetchDatacenterList({ keyword })
if (res.code === 0) {
const list = res.details || []
datacenterSelectOptions.value = Array.isArray(list)
? list.map((d: any) => ({
label: d.name || d.code || String(d.id),
value: d.id,
}))
: []
}
} catch (error) {
console.error('获取数据中心列表失败:', error)
Message.error('获取数据中心列表失败')
datacenterSelectOptions.value = []
}
}
const loadFloorOptions = async (datacenterId?: number, keyword?: string) => {
if (!datacenterId) {
floorSelectOptions.value = []
return
}
try {
const res: any = await fetchFloorListByDatacenter(datacenterId, { name: keyword })
if (res.code === 0) {
const list = res.details?.data ?? res.data ?? res.details ?? []
floorSelectOptions.value = Array.isArray(list)
? list.map((f: any) => ({
label: f.name || String(f.id),
value: f.id,
}))
: []
}
} catch (error) {
console.error('获取楼层列表失败:', error)
Message.error('获取楼层列表失败')
floorSelectOptions.value = []
}
}
const handleDatacenterSearch = (keyword: string) => {
if (datacenterSearchTimer) {
window.clearTimeout(datacenterSearchTimer)
}
datacenterSearchTimer = window.setTimeout(() => {
loadDatacenterOptions(keyword?.trim() || undefined)
}, 300)
}
const handleFloorSearch = (keyword: string) => {
if (!formModel.value.datacenter_id) return
if (floorSearchTimer) {
window.clearTimeout(floorSearchTimer)
}
floorSearchTimer = window.setTimeout(() => {
loadFloorOptions(formModel.value.datacenter_id, keyword?.trim() || undefined)
}, 300)
}
watch(
() => formModel.value.datacenter_id,
(newId, oldId) => {
if (newId !== oldId) {
formModel.value.floor_id = undefined
}
loadFloorOptions(newId)
},
)
const fetchRooms = 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,
status: formModel.value.status || undefined,
}
const res: any = await fetchRoomList(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
fetchRooms()
}
const handleFormModelUpdate = (value: any) => {
formModel.value = value
}
const handleReset = () => {
formModel.value = {
keyword: '',
datacenter_id: undefined,
floor_id: undefined,
status: '',
}
pagination.current = 1
fetchRooms()
}
const handlePageChange = (current: number) => {
pagination.current = current
fetchRooms()
}
const handleRefresh = () => {
fetchRooms()
Message.success('数据已刷新')
}
const handleCreate = () => {
editingRoom.value = null
formVisible.value = true
}
const handleEdit = (record: any) => {
editingRoom.value = record
formVisible.value = true
}
const handleDetail = (record: any) => {
currentRoomId.value = record.id
detailVisible.value = true
}
const handleDelete = async (record: any) => {
try {
Modal.confirm({
title: '确认删除',
content: `确认删除机房 ${record.name} 吗?`,
onOk: async () => {
const res: any = await deleteRoom(record.id)
if (res.code === 0) {
Message.success('删除成功')
fetchRooms()
} else {
Message.error(res.message || '删除失败')
}
},
})
} catch (error) {
console.error('删除机房失败:', error)
}
}
const handleFormSuccess = () => {
formVisible.value = false
fetchRooms()
}
onMounted(() => {
loadDatacenterOptions()
fetchRooms()
})
</script>
<script lang="ts">
export default {
name: 'DataCenterRoom',
}
</script>
<style scoped lang="less">
.container {
margin-top: 20px;
}
</style>

View File

@@ -182,6 +182,7 @@
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 { useRoute } from 'vue-router'
import { fetchUnitList, allocateUnit, reserveUnit, cancelReservation, releaseUnit, updateUnitStatus } from '@/api/ops/unit'
import { fetchDatacenterList, fetchRackListByRoom } from '@/api/ops/rack'
import { fetchFloorListByDatacenter } from '@/api/ops/floor'
@@ -210,6 +211,7 @@ let datacenterSearchTimer: number | undefined
let floorSearchTimer: number | undefined
let roomSearchTimer: number | undefined
let rackSearchTimer: number | undefined
const route = useRoute()
// 对话框可见性
const allocateVisible = ref(false)
@@ -613,6 +615,11 @@ const handleCancelReservation = async (record: any) => {
// 初始化
onMounted(() => {
fetchDatacenters()
const rackId = Number(route.query.rack_id)
if (Number.isFinite(rackId) && rackId > 0) {
selectedRackId.value = rackId
fetchUnits(rackId)
}
})
</script>

View File

@@ -75,6 +75,23 @@
<a-input v-model="formData.location" placeholder="机房A-机架01" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item field="asset_id" label="关联资产">
<a-select
v-model="formData.asset_id"
:options="assetOptions"
:loading="assetLoading"
allow-clear
show-search
:filter-option="false"
placeholder="请选择资产(可选)"
@search="handleAssetSearch"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="20">
<a-col :span="8">
<a-form-item field="tags" label="标签">
<a-input v-model="formData.tags" placeholder="多个标签逗号分隔" />
@@ -147,6 +164,7 @@ import { Message } from '@arco-design/web-vue'
import type { FormInstance } from '@arco-design/web-vue'
import { createServer, updateServer } from '@/api/ops/server'
import type { ServerFormData, ServerItem } from '@/api/ops/server'
import { fetchAssetAll } from '@/api/ops/asset'
interface Props {
visible: boolean
@@ -161,6 +179,9 @@ const emit = defineEmits(['update:visible', 'success'])
const formRef = ref<FormInstance>()
const confirmLoading = ref(false)
const assetLoading = ref(false)
const assetOptions = ref<{ label: string; value: number }[]>([])
let assetSearchTimer: ReturnType<typeof setTimeout> | null = null
const isEdit = computed(() => !!props.record?.id)
@@ -176,6 +197,7 @@ const formData = reactive<ServerFormData>({
server_type: '',
tags: '',
location: '',
asset_id: undefined,
remote_access: '',
remote_port: 0,
agent_config: '',
@@ -280,6 +302,7 @@ watch(
() => props.visible,
(val) => {
if (val) {
loadAssetOptions()
if (isEdit.value && props.record) {
Object.assign(formData, {
server_identity: props.record.server_identity || '',
@@ -293,6 +316,7 @@ watch(
server_type: props.record.server_type || '',
tags: props.record.tags || '',
location: props.record.location || '',
asset_id: props.record.asset_id,
remote_access: props.record.remote_access || '',
remote_port: props.record.remote_port || 0,
agent_config: props.record.agent_config || '',
@@ -316,6 +340,7 @@ watch(
server_type: '',
tags: '',
location: '',
asset_id: undefined,
remote_access: '',
remote_port: 0,
agent_config: '',
@@ -330,6 +355,39 @@ watch(
}
)
async function loadAssetOptions(keyword?: string) {
assetLoading.value = true
try {
const res: any = await fetchAssetAll({ keyword: (keyword || '').trim() || undefined })
if (res?.code !== 0) {
Message.warning(res?.message || '加载资产列表失败')
assetOptions.value = []
return
}
const rows = res?.details || []
assetOptions.value = rows.map((item: any) => ({
value: Number(item.id),
label: `${item.asset_code || item.id} | ${item.asset_name || '-'}`,
}))
} catch (error) {
console.error('加载资产列表失败:', error)
Message.warning('加载资产列表失败')
assetOptions.value = []
} finally {
assetLoading.value = false
}
}
// 资产下拉使用远程模糊查询,避免一次加载全部资产。
function handleAssetSearch(keyword: string) {
if (assetSearchTimer) {
clearTimeout(assetSearchTimer)
}
assetSearchTimer = setTimeout(() => {
loadAssetOptions(keyword)
}, 300)
}
watch(
() => formData.host,
() => {
@@ -362,6 +420,7 @@ const handleOk = async () => {
server_type: formData.server_type,
tags: formData.tags,
location: formData.location,
asset_id: formData.asset_id ?? (isEdit.value ? 0 : undefined),
remote_access: formData.remote_access,
remote_port: formData.remote_port,
agent_config: formData.agent_config,

View File

@@ -289,8 +289,6 @@ const subStatusItems = computed(() => {
{ key: 'cpu_status', label: 'CPU' },
{ key: 'memory_status', label: '内存' },
{ key: 'disk_status', label: '磁盘' },
{ key: 'network_status', label: '网络' },
{ key: 'raid_status', label: 'RAID' },
]
return fields.map((f) => ({
key: String(f.key),