fix: 机柜图形化

This commit is contained in:
zxr
2026-08-20 18:06:05 +08:00
parent 43ce3433ce
commit 695755546f
26 changed files with 2163 additions and 495 deletions

View File

@@ -42,13 +42,6 @@ export interface AssetListParams {
page_size?: number
keyword?: string
status?: string
category_id?: number
supplier_id?: number
datacenter_id?: number
floor_id?: number
room_id?: number
rack_id?: number
department?: string
sort?: string
order?: string
}
@@ -66,7 +59,7 @@ export interface AssetForm {
purchase_date?: string
original_value?: number
supplier_id?: number
warranty_period?: string
warranty_period?: number
warranty_expiry?: string
department?: string
user?: string
@@ -75,9 +68,6 @@ export interface AssetForm {
datacenter_id?: number
floor_id?: number
room_id?: number
rack_id?: number
unit_start?: number
unit_end?: number
qr_code?: string
rfid_tag?: string
asset_tag?: string
@@ -138,21 +128,6 @@ export const fetchSupplierOptions = () => {
return request.get('/Assets/v1/supplier/all')
}
/** 获取数据中心列表(下拉) */
export const fetchDatacenterOptions = () => {
return request.get('/Assets/v1/datacenter/list')
}
/** 根据数据中心获取楼层列表 */
export const fetchFloorOptions = (datacenterId: number) => {
return request.get(`/Assets/v1/datacenter/${datacenterId}`)
}
/** 获取机柜列表(下拉) */
export const fetchRackOptions = (params?: { datacenter_id?: number; floor_id?: number }) => {
return request.post('/Assets/v1/rack/list', params || {})
}
/** 绑定资产和监控资源 */
export const linkAssetResource = (data: {
asset_id: number

View File

@@ -3,6 +3,7 @@ import { request } from '@/api/request'
export interface Floor {
id?: number
name?: string
code?: string
datacenter_id?: number
floor_number?: number
status?: string
@@ -54,7 +55,6 @@ export const deleteFloor = (id: number) => {
}
/** 获取数据中心列表(用于下拉选择) */
export const fetchDatacenterList = (params?: { keyword?: string; name?: string }) => {
const normalizedParams = params?.keyword ? { ...params, name: params.name ?? params.keyword } : params
return request.get('/Assets/v1/datacenter/all', { params: normalizedParams })
export const fetchDatacenterList = (params?: { keyword?: string }) => {
return request.get('/Assets/v1/datacenter/all', { params })
}

View File

@@ -21,9 +21,14 @@ export const fetchRackListByDatacenter = (datacenterId: number, params?: { name?
return request.get(`/Assets/v1/rack/datacenter/${datacenterId}`, { params })
}
/** 根据楼层获取机柜列表(下拉,不分页)- 已废弃,请使用 fetchRackListByRoom */
export const fetchRackListByFloor = (floorId: number, params?: { name?: string }) => {
return request.get(`/Assets/v1/rack/floor/${floorId}`, { params })
/** 根据楼层获取机柜列表 */
export const fetchRackListByFloor = (floorId: number, params?: { keyword?: string }) => {
return fetchRackList({
page: 1,
page_size: 100,
floor_id: floorId,
keyword: params?.keyword,
})
}
/** 根据机房获取机柜列表(下拉,不分页) */
@@ -62,13 +67,6 @@ export const fetchSupplierList = () => {
}
/** 获取数据中心列表(用于下拉选择) */
export const fetchDatacenterList = (params?: { keyword?: string; name?: string }) => {
const normalizedParams = params?.keyword ? { ...params, name: params.name ?? params.keyword } : params
return request.get('/Assets/v1/datacenter/all', { params: normalizedParams })
}
/** 获取楼层列表(用于下拉选择) */
export const fetchFloorListForSelect = (params?: { datacenter_id?: number; keyword?: string; name?: string }) => {
const normalizedParams = params?.keyword ? { ...params, name: params.name ?? params.keyword } : params
return request.get('/Assets/v1/floor/all', { params: normalizedParams })
export const fetchDatacenterList = (params?: { keyword?: string }) => {
return request.get('/Assets/v1/datacenter/all', { params })
}

View File

@@ -11,7 +11,8 @@ export interface RoomItem {
description?: string
floor_id: number
datacenter_id: number
status?: string
layout_version: number
status: string
has3D: boolean
floor?: { id: number; name: string }
datacenter?: { id: number; name: string }
@@ -26,6 +27,13 @@ export interface RoomListResponse {
}
}
/** 机房详情响应 */
export interface RoomDetailResponse {
code: number
message?: string
details: RoomItem
}
/** 机房列表查询参数 */
export interface RoomListParams {
page?: number
@@ -54,6 +62,12 @@ export interface RoomOptionItem {
sort: number
}
export interface ApiResponse<T> {
code: number
message?: string
details: T
}
/** 机房创建数据 */
export interface RoomCreateData {
name: string
@@ -61,8 +75,7 @@ export interface RoomCreateData {
description?: string
floor_id: number
datacenter_id: number
status?: string
has3D: boolean
status: string
}
/** 机房更新数据 */
@@ -74,7 +87,6 @@ export interface RoomUpdateData {
floor_id?: number
datacenter_id?: number
status?: string
has3D?: boolean
}
/** 获取机房列表(分页) */
@@ -84,17 +96,17 @@ export const fetchRoomList = (data?: RoomListParams) => {
/** 获取机房详情 */
export const fetchRoomDetail = (id: number) => {
return request.get<RoomItem>(`/Assets/v1/room/detail/${id}`)
return request.get<RoomDetailResponse>(`/Assets/v1/room/detail/${id}`)
}
/** 获取机房列表(下拉,不分页) */
export const fetchRoomOptions = (params?: RoomOptionsParams) => {
return request.get<RoomOptionItem[]>('/Assets/v1/room/all', { params })
return request.get<ApiResponse<RoomOptionItem[]>>('/Assets/v1/room/all', { params })
}
/** 根据楼层获取机房列表(下拉,不分页) */
export const fetchRoomListByFloor = (floorId: number, params?: { name?: string }) => {
return request.get<RoomItem[]>(`/Assets/v1/room/floor/${floorId}`, { params })
return request.get<ApiResponse<RoomItem[]>>(`/Assets/v1/room/floor/${floorId}`, { params })
}
/** 创建机房 */

View File

@@ -1,31 +1,100 @@
import { request } from '@/api/request'
/** U 位状态 */
export type RackUnitStatus = 'available' | 'occupied' | 'reserved' | 'disabled'
/** U 位关联资产摘要 */
export interface RackUnitAssetSummary {
unit_start: number
}
/** U 位公共字段 */
export interface RackUnitBase {
unit_number: number
occupied_units: number
description?: string
}
/** 可用 U 位 */
export interface AvailableRackUnit extends RackUnitBase {
status: 'available'
}
/** 已占用 U 位 */
export interface OccupiedRackUnit extends RackUnitBase {
status: 'occupied'
asset_id: number
asset_name: string
asset_code: string
asset_type: string
power_consumption: number
asset: RackUnitAssetSummary
}
/** 已预留 U 位 */
export interface ReservedRackUnit extends RackUnitBase {
status: 'reserved'
reserved_for: string
reserved_until: string
}
/** 已禁用 U 位 */
export interface DisabledRackUnit extends RackUnitBase {
status: 'disabled'
}
/** 单个 U 位数据 */
export type RackUnitItem = AvailableRackUnit | OccupiedRackUnit | ReservedRackUnit | DisabledRackUnit
/** U 位页面使用的机柜摘要 */
export interface RackUnitRackInfo {
id?: number
room_id: number
height: number
status?: string
used_power?: number
}
/** 获取机柜 U 位状态响应 */
export interface RackUnitListResponse {
code: number
message?: string
details: {
rack: RackUnitRackInfo
units: RackUnitItem[]
}
}
/** 获取机柜U位状态 */
export const fetchRackUnits = (rackId: number) => {
return request.get(`/Assets/v1/unit/rack/${rackId}`)
return request.get<RackUnitListResponse>(`/Assets/v1/unit/rack/${rackId}`)
}
/** 获取U位列表别名 */
export const fetchUnitList = fetchRackUnits
/** 分配U位 */
export const allocateUnit = (data: {
/** 机柜设备上架数据 */
export interface RackPlacementData {
expected_version: number
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)
power_consumption: number
}
/** 释放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 placeAssetInRack = (roomId: number, assetId: number, data: RackPlacementData) => {
return request.put(`/Assets/v1/three-d/rooms/${roomId}/devices/${assetId}/placement`, {
...data,
placement_type: 'rack',
})
}
/** 取消资产在机房中的放置 */
export const unplaceAsset = (roomId: number, assetId: number, expectedVersion: number) => {
return request.delete(`/Assets/v1/three-d/rooms/${roomId}/devices/${assetId}/placement`, {
data: { expected_version: expectedVersion },
})
}
/** 预留U位 */

View File

@@ -97,7 +97,7 @@
</a-col>
<a-col :span="6">
<a-form-item label="质保期" field="warranty_period">
<a-input v-model="form.warranty_period" placeholder="请输入质保36个月" :max-length="50" allow-clear />
<a-input-number v-model="form.warranty_period" placeholder="请输入质保月数" :min="0" :precision="0" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="6">
@@ -146,8 +146,9 @@
<!-- 位置信息 -->
<a-card class="info-card" title="位置信息">
<a-alert v-if="locationLocked" type="info" style="margin-bottom: 16px">已放置资产的位置由 U 位或 3D 机房页面维护</a-alert>
<a-row :gutter="16">
<a-col :span="5">
<a-col :span="8">
<a-form-item label="所属数据中心" field="datacenter_id">
<a-select
v-model="form.datacenter_id"
@@ -156,6 +157,7 @@
allow-search
:loading="datacenterLoading"
:filter-option="false"
:disabled="locationLocked"
@search="handleDatacenterSearch"
@change="handleDatacenterChange"
>
@@ -163,7 +165,7 @@
</a-select>
</a-form-item>
</a-col>
<a-col :span="5">
<a-col :span="8">
<a-form-item label="所属楼层" field="floor_id">
<a-select
v-model="form.floor_id"
@@ -171,7 +173,7 @@
allow-clear
allow-search
:loading="floorLoading"
:disabled="!form.datacenter_id"
:disabled="locationLocked || !form.datacenter_id"
:filter-option="false"
@search="handleFloorSearch"
@change="handleFloorChange"
@@ -180,7 +182,7 @@
</a-select>
</a-form-item>
</a-col>
<a-col :span="5">
<a-col :span="8">
<a-form-item label="所属机房" field="room_id">
<a-select
v-model="form.room_id"
@@ -188,41 +190,14 @@
allow-clear
allow-search
:loading="roomLoading"
:disabled="!form.floor_id"
:disabled="locationLocked || !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"
placeholder="请选择机柜"
allow-clear
allow-search
:loading="rackLoading"
:disabled="!form.room_id"
:filter-option="false"
@search="handleRackSearch"
>
<a-option v-for="item in rackOptions" :key="item.id" :value="item.id">{{ item.name }} ({{ item.code }})</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="2">
<a-form-item label="起始U位" field="unit_start">
<a-input-number v-model="form.unit_start" placeholder="起始U位" :min="1" :max="48" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="2">
<a-form-item label="结束U位" field="unit_end">
<a-input-number v-model="form.unit_end" placeholder="结束U位" :min="1" :max="48" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
</a-card>
@@ -282,13 +257,13 @@
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { computed, ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { Message } from '@arco-design/web-vue'
import type { FormInstance } from '@arco-design/web-vue/es/form'
import { createAsset, updateAsset, fetchAssetDetail, fetchCategoryOptions, assetStatusOptions, AssetForm } from '@/api/ops/asset'
import { fetchAllSuppliers } from '@/api/ops/supplier'
import { fetchDatacenterList, fetchRackListByRoom } from '@/api/ops/rack'
import { fetchDatacenterList } from '@/api/ops/rack'
import { fetchFloorListByDatacenter } from '@/api/ops/floor'
import { fetchRoomListByFloor } from '@/api/ops/room'
@@ -297,6 +272,7 @@ const route = useRoute()
// 页面状态
const isEdit = ref(false)
const placementType = ref('unplaced')
const deviceId = ref<number | null>(null)
const pageTitle = ref('新增设备')
const loading = ref(false)
@@ -309,7 +285,6 @@ 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[]>([])
// 加载状态
const categoryLoading = ref(false)
@@ -317,57 +292,50 @@ 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
// 表单数据
const form = ref<AssetForm>({
asset_name: '',
asset_code: '',
category_id: undefined,
model: '',
manufacturer: '',
serial_number: '',
source_address: '',
purchase_date: '',
original_value: undefined,
supplier_id: undefined,
warranty_period: '',
warranty_expiry: '',
department: '',
user: '',
status: 'in_use',
location: '',
datacenter_id: undefined,
floor_id: undefined,
rack_id: undefined,
unit_start: undefined,
unit_end: undefined,
qr_code: '',
rfid_tag: '',
asset_tag: '',
specifications: '',
description: '',
remarks: '',
const createAssetForm = (data: any = {}): AssetForm => ({
asset_name: data.asset_name || '',
asset_code: data.asset_code || '',
category_id: data.category_id,
model: data.model || '',
manufacturer: data.manufacturer || '',
serial_number: data.serial_number || '',
source_address: data.source_address || '',
purchase_date: data.purchase_date || '',
original_value: data.original_value,
supplier_id: data.supplier_id,
warranty_period: data.warranty_period,
warranty_expiry: data.warranty_expiry || '',
department: data.department || '',
user: data.user || '',
status: data.status || 'in_use',
location: data.location || '',
datacenter_id: data.datacenter_id,
floor_id: data.floor_id,
room_id: data.room_id,
qr_code: data.qr_code || '',
rfid_tag: data.rfid_tag || '',
asset_tag: data.asset_tag || '',
specifications: data.specifications || '',
description: data.description || '',
remarks: data.remarks || '',
})
const form = ref<AssetForm>(createAssetForm())
const locationLocked = computed(() => isEdit.value && placementType.value !== 'unplaced')
// 表单验证规则
const rules = {
asset_name: [{ required: true, message: '请输入资产名称' }],
asset_code: [{ required: true, message: '请输入资产编号' }],
}
// 提取列表数据的辅助函数
const extractList = (res: any): any[] => {
const candidate = res?.data?.data ?? res?.details?.data ?? res?.data ?? res?.details ?? []
return Array.isArray(candidate) ? candidate : []
}
// 加载下拉选项
const loadOptions = async () => {
// 加载资产分类
@@ -375,7 +343,7 @@ const loadOptions = async () => {
try {
const res = await fetchCategoryOptions()
if (res.code === 0) {
categoryOptions.value = res.details || []
categoryOptions.value = res.details
}
} catch (error) {
console.error('加载资产分类失败:', error)
@@ -393,7 +361,7 @@ const loadSupplierOptions = async (keyword?: string) => {
try {
const res = await fetchAllSuppliers(keyword, 'active', keyword ? undefined : 20)
if (res.code === 0) {
supplierOptions.value = res.details?.data || res.details || []
supplierOptions.value = res.details
}
} catch (error) {
console.error('加载供应商失败:', error)
@@ -419,7 +387,7 @@ const fetchDatacenters = async (keyword?: string) => {
datacenterLoading.value = true
try {
const res: any = await fetchDatacenterList({ keyword })
const rows = extractList(res)
const rows = res.details
datacenterOptions.value = rows.map((d: any) => ({
label: d.name || d.code || String(d.id),
value: d.id,
@@ -453,7 +421,7 @@ const fetchFloors = async (keyword?: string) => {
const res: any = await fetchFloorListByDatacenter(form.value.datacenter_id, {
name: keyword || undefined,
})
const rows = extractList(res)
const rows = res.details
floorOptions.value = rows.map((floor: any) => ({
label: floor.name || floor.code || String(floor.id),
value: floor.id,
@@ -477,35 +445,6 @@ const handleFloorSearch = (keyword: string) => {
}, 300)
}
// 加载机柜列表
const fetchRacks = async (keyword?: string) => {
if (!form.value.room_id) {
rackOptions.value = []
return
}
rackLoading.value = true
try {
const res: any = await fetchRackListByRoom(form.value.room_id, { name: keyword })
rackOptions.value = extractList(res)
} catch (error) {
console.error('获取机柜列表失败:', error)
rackOptions.value = []
} finally {
rackLoading.value = false
}
}
// 机柜搜索(带防抖)
const handleRackSearch = (keyword: string) => {
if (!form.value.room_id) return
if (rackSearchTimer) {
window.clearTimeout(rackSearchTimer)
}
rackSearchTimer = window.setTimeout(() => {
fetchRacks(keyword?.trim() || undefined)
}, 300)
}
// 加载机房列表
const fetchRooms = async (keyword?: string) => {
if (!form.value.floor_id) {
@@ -517,7 +456,7 @@ const fetchRooms = async (keyword?: string) => {
const res: any = await fetchRoomListByFloor(form.value.floor_id, {
name: keyword || undefined,
})
roomOptions.value = extractList(res)
roomOptions.value = res.details
} catch (error) {
console.error('获取机房列表失败:', error)
roomOptions.value = []
@@ -541,38 +480,24 @@ const handleRoomSearch = (keyword: string) => {
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) {
await fetchFloors()
}
}
// 楼层变化时加载机
// 楼层变化时加载机
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 = []
if (value) {
await fetchRacks()
}
}
// 初始化页面
const initPage = async () => {
await loadOptions()
@@ -594,14 +519,15 @@ const loadDeviceDetail = async () => {
try {
const res = await fetchAssetDetail(deviceId.value)
if (res.code === 0) {
form.value = { ...res.details } as AssetForm
placementType.value = res.details.placement_type
form.value = createAssetForm(res.details)
// 如果有供应商,加载供应商选项
if (res.details.supplier_id) {
try {
const supplierRes = await fetchAllSuppliers()
if (supplierRes.code === 0) {
supplierOptions.value = supplierRes.details?.data || supplierRes.details || []
supplierOptions.value = supplierRes.details
}
} catch (error) {
console.error('加载供应商失败:', error)
@@ -612,7 +538,7 @@ const loadDeviceDetail = async () => {
if (res.details.datacenter_id) {
try {
const floorRes: any = await fetchFloorListByDatacenter(res.details.datacenter_id)
const rows = extractList(floorRes)
const rows = floorRes.details
floorOptions.value = rows.map((floor: any) => ({
label: floor.name || floor.code || String(floor.id),
value: floor.id,
@@ -626,21 +552,11 @@ const loadDeviceDetail = async () => {
if (res.details.floor_id) {
try {
const roomRes: any = await fetchRoomListByFloor(res.details.floor_id)
roomOptions.value = extractList(roomRes)
roomOptions.value = roomRes.details
} 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)
}
}
} else {
Message.error(res.message || '获取详情失败')
router.push('/ops/assets/device')

View File

@@ -203,8 +203,8 @@ const fetchDevices = async () => {
const res = await fetchAssetList(params)
tableData.value = res.details?.data || []
pagination.total = res.details?.total || 0
tableData.value = res.details.data
pagination.total = res.details.total
} catch (error) {
console.error('获取设备列表失败:', error)
Message.error('获取设备列表失败')

View File

@@ -5,6 +5,9 @@
<a-descriptions-item label="楼层名称" :span="2">
{{ floorDetail.name }}
</a-descriptions-item>
<a-descriptions-item label="楼层编码">
{{ floorDetail.code || '-' }}
</a-descriptions-item>
<a-descriptions-item label="所属中心">
{{ floorDetail.datacenter?.name || '-' }}
</a-descriptions-item>
@@ -17,7 +20,7 @@
<a-descriptions-item label="楼高">
{{ floorDetail.height ? `${floorDetail.height}` : '-' }}
</a-descriptions-item>
<a-descriptions-item label="重">
<a-descriptions-item label="重">
{{ floorDetail.load_bearing ? `${floorDetail.load_bearing} kg/m²` : '-' }}
</a-descriptions-item>
<a-descriptions-item label="可容纳机柜数">

View File

@@ -13,8 +13,12 @@
<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>
<a-select v-model="form.datacenter_id" placeholder="请选择所属中心" :loading="loadingDatacenters" :disabled="isEdit" allow-search>
<a-option v-for="item in datacenterList" :key="item.id" :value="item.id">
{{ item.name }}
</a-option>
@@ -43,8 +47,8 @@
<a-input-number v-model="form.height" placeholder="请输入楼高" :min="0" :precision="2" style="width: 100%" />
</a-form-item>
<a-form-item label="kg/m²" field="load_bearing">
<a-input-number v-model="form.load_bearing" placeholder="请输入重" :min="0" :precision="2" style="width: 100%" />
<a-form-item label="kg/m²" field="load_bearing">
<a-input-number v-model="form.load_bearing" placeholder="请输入重" :min="0" :precision="2" style="width: 100%" />
</a-form-item>
<a-form-item label="启用" field="enabled">
@@ -59,8 +63,7 @@
:file-list="fileList"
list-type="picture-card"
accept="image/*"
>
</a-upload>
></a-upload>
</a-form-item>
<a-form-item label="描述" field="description">
@@ -100,6 +103,7 @@ const localPreviewURL = ref('')
// 表单数据
const form = ref({
name: '',
code: '',
datacenter_id: undefined as number | undefined,
floor_number: 1,
status: 'planning',
@@ -128,7 +132,7 @@ const loadDatacenterList = async () => {
try {
const res: any = await fetchDatacenterList()
if (res.code === 0) {
datacenterList.value = res.details || []
datacenterList.value = res.details
}
} catch (error) {
console.error('获取数据中心列表失败:', error)
@@ -177,6 +181,7 @@ watch(
// 编辑模式:填充表单
form.value = {
name: props.floor.name || '',
code: props.floor.code || '',
datacenter_id: props.floor.datacenter_id,
floor_number: props.floor.floor_number || 1,
status: props.floor.status || 'planning',
@@ -204,6 +209,7 @@ watch(
// 新建模式:重置表单
form.value = {
name: '',
code: '',
datacenter_id: undefined,
floor_number: 1,
status: 'planning',
@@ -230,6 +236,7 @@ const handleOk = async () => {
try {
const data: SaveFloorPayload = {
name: form.value.name,
code: form.value.code,
datacenter_id: form.value.datacenter_id,
floor_number: form.value.floor_number,
status: form.value.status,

View File

@@ -12,6 +12,11 @@ export const columns: TableColumnData[] = [
dataIndex: 'name',
width: 150,
},
{
title: '楼层编码',
dataIndex: 'code',
width: 140,
},
{
title: '所属中心',
dataIndex: 'datacenter',

View File

@@ -45,6 +45,7 @@
<!-- 操作 -->
<template #actions="{ record }">
<a-button type="text" size="small" @click="handleDetail(record)">详情</a-button>
<a-button type="text" size="small" @click="handleRooms(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>
@@ -60,6 +61,7 @@
<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 { IconPlus } from '@arco-design/web-vue/es/icon'
import type { FormItem } from '@/components/search-form/types'
@@ -70,6 +72,17 @@ import { fetchFloorList, fetchDatacenterList, deleteFloor } from '@/api/ops/floo
import FloorDetailDialog from './components/FloorDetailDialog.vue'
import FloorFormDialog from './components/FloorFormDialog.vue'
const route = useRoute()
const router = useRouter()
const getRouteDatacenterId = () => {
const value = route.query.datacenter_id
if (typeof value !== 'string') return undefined
const datacenterId = Number(value)
return Number.isInteger(datacenterId) && datacenterId > 0 ? datacenterId : undefined
}
// 状态映射
const statusMap: Record<string, { text: string; color: string }> = {
planning: { text: '规划中', color: 'blue' },
@@ -85,7 +98,7 @@ const tableData = ref<any[]>([])
const formModel = ref({
keyword: '',
status: '',
datacenter_id: undefined as number | undefined,
datacenter_id: getRouteDatacenterId() as number | undefined,
})
const pagination = reactive({
@@ -128,7 +141,7 @@ const loadDatacenterOptions = async (keyword?: string) => {
try {
const res: any = await fetchDatacenterList({ keyword })
if (res.code === 0) {
const list = res.details || []
const list = res.details
datacenterSelectOptions.value = Array.isArray(list)
? list.map((d: any) => ({
label: d.name || d.code || String(d.id),
@@ -167,8 +180,8 @@ const fetchFloors = async () => {
const res = await fetchFloorList(params)
tableData.value = res.details?.data || []
pagination.total = res.details?.total || 0
tableData.value = res.details.data
pagination.total = res.details.total
} catch (error) {
console.error('获取楼层列表失败:', error)
Message.error('获取楼层列表失败')
@@ -197,6 +210,10 @@ const handleReset = () => {
status: '',
datacenter_id: undefined,
}
router.replace({
path: route.path,
query: { ...route.query, datacenter_id: undefined },
})
pagination.current = 1
fetchFloors()
}
@@ -233,6 +250,16 @@ const handleDetail = (record: any) => {
detailVisible.value = true
}
const handleRooms = (record: any) => {
router.push({
path: '/datacenter/room',
query: {
datacenter_id: String(record.datacenter_id),
floor_id: String(record.id),
},
})
}
// 删除楼层
const handleDelete = async (record: any) => {
console.log('删除楼层:', record)

View File

@@ -113,16 +113,11 @@
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-col :span="24">
<a-form-item label="总电力KW" field="total_power">
<a-input-number v-model="form.total_power" placeholder="请输入总电力" :min="0" :precision="2" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="已用电力KW" field="used_power">
<a-input-number v-model="form.used_power" placeholder="请输入已用电力" :min="0" :precision="2" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="可容纳机柜数" field="max_rack_capacity">
@@ -178,7 +173,6 @@ interface Datacenter {
total_area?: number
it_area?: number
total_power?: number
used_power?: number
max_rack_capacity?: number
iso27001?: boolean
iso9001?: boolean
@@ -219,7 +213,6 @@ const form = ref({
total_area: undefined as number | undefined,
it_area: undefined as number | undefined,
total_power: undefined as number | undefined,
used_power: undefined as number | undefined,
max_rack_capacity: undefined as number | undefined,
iso27001: false,
iso9001: false,
@@ -254,7 +247,7 @@ const fetchProvinces = async (keyword = '') => {
provinceLoading.value = true
try {
const res = await fetchProvinceList({ keyword: keyword || undefined })
provinceOptions.value = res.details || []
provinceOptions.value = res.details
} catch (error) {
console.error('获取省份列表失败:', error)
provinceOptions.value = []
@@ -267,7 +260,7 @@ const fetchCitiesByProvince = async (provinceId: number, keyword = '') => {
cityLoading.value = true
try {
const res = await fetchCityList(provinceId, { keyword: keyword || undefined })
cityOptions.value = res.details || []
cityOptions.value = res.details
} catch (error) {
console.error('获取城市列表失败:', error)
cityOptions.value = []
@@ -323,7 +316,6 @@ watch(
total_area: props.datacenter.total_area,
it_area: props.datacenter.it_area,
total_power: props.datacenter.total_power,
used_power: props.datacenter.used_power,
max_rack_capacity: props.datacenter.max_rack_capacity,
iso27001: props.datacenter.iso27001 !== undefined ? props.datacenter.iso27001 : false,
iso9001: props.datacenter.iso9001 !== undefined ? props.datacenter.iso9001 : false,
@@ -351,7 +343,6 @@ watch(
total_area: undefined,
it_area: undefined,
total_power: undefined,
used_power: undefined,
max_rack_capacity: undefined,
iso27001: false,
iso9001: false,
@@ -387,7 +378,6 @@ const handleOk = async () => {
total_area: form.value.total_area,
it_area: form.value.it_area,
total_power: form.value.total_power,
used_power: form.value.used_power,
max_rack_capacity: form.value.max_rack_capacity,
iso27001: form.value.iso27001,
iso9001: form.value.iso9001,

View File

@@ -109,6 +109,7 @@
<!-- 操作 -->
<template #actions="{ record }">
<a-button type="text" size="small" @click="handleDetail(record)">详情</a-button>
<a-button type="text" size="small" @click="handleFloors(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>
@@ -129,6 +130,7 @@
<script lang="ts" setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
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'
@@ -139,6 +141,8 @@ import { fetchDatacenterList, deleteDatacenter, fetchProvinceList, fetchCityList
import DatacenterDetailDialog from './components/DatacenterDetailDialog.vue'
import DatacenterFormDialog from './components/DatacenterFormDialog.vue'
const router = useRouter()
// 状态映射
const statusMap: Record<string, { text: string; color: string }> = {
planning: { text: '规划中', color: 'blue' },
@@ -192,7 +196,7 @@ const fetchProvinces = async (keyword = '') => {
provinceSelectLoading.value = true
try {
const res = await fetchProvinceList({ keyword: keyword || undefined })
const list = res.details || []
const list = res.details
provinceSelectOptions.value = list
if (!keyword) {
provinces.value = list
@@ -210,7 +214,7 @@ const fetchCitiesByProvince = async (provinceId: number, keyword = '') => {
citySelectLoading.value = true
try {
const res = await fetchCityList(provinceId, { keyword: keyword || undefined })
citySelectOptions.value = res.details || []
citySelectOptions.value = res.details
} catch (error) {
console.error('获取城市列表失败:', error)
citySelectOptions.value = []
@@ -264,8 +268,8 @@ const fetchDatacenters = async () => {
const res = await fetchDatacenterList(params)
tableData.value = res.details?.data || []
pagination.total = res.details?.total || 0
tableData.value = res.details.data
pagination.total = res.details.total
} catch (error) {
console.error('获取数据中心列表失败:', error)
Message.error('获取数据中心列表失败')
@@ -333,6 +337,13 @@ const handleDetail = (record: any) => {
detailVisible.value = true
}
const handleFloors = (record: any) => {
router.push({
path: '/datacenter/floor',
query: { datacenter_id: String(record.id) },
})
}
// 删除数据中心
const handleDelete = async (record: any) => {
console.log('删除数据中心:', record)

View File

@@ -34,7 +34,7 @@
<a-descriptions-item label="可用U位数">{{ rackDetail.available_units || '-' }}</a-descriptions-item>
<a-descriptions-item label="使用率">
<a-progress
:percent="rackDetail.utilization_rate || 0"
:percent="(rackDetail.utilization_rate || 0) / 100"
:color="(rackDetail.utilization_rate || 0) > 80 ? '#f53f3f' : (rackDetail.utilization_rate || 0) > 50 ? '#ff7d00' : '#00b42a'"
/>
</a-descriptions-item>

View File

@@ -31,6 +31,7 @@
v-model="form.datacenter_id"
placeholder="请选择所属中心"
:loading="loadingDatacenters"
:disabled="isEdit"
allow-search
@change="handleDatacenterChange"
>
@@ -46,7 +47,7 @@
v-model="form.floor_id"
placeholder="请选择所属楼层"
:loading="loadingFloors"
:disabled="!form.datacenter_id"
:disabled="isEdit || !form.datacenter_id"
allow-search
@change="handleFloorChange"
@search="handleFloorSearch"
@@ -65,7 +66,7 @@
v-model="form.room_id"
placeholder="请选择所属机房"
:loading="loadingRooms"
:disabled="!form.floor_id"
:disabled="isEdit || !form.floor_id"
allow-search
@search="handleRoomSearch"
>
@@ -81,18 +82,18 @@
<a-divider orientation="left">规格参数</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="高度(U)" field="height">
<a-form-item label="高度(U)" field="height" :rules="[{ required: true, message: '请输入机柜高度' }]">
<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 label="宽度(mm)" field="width" :rules="[{ required: true, message: '请输入大于0的机柜宽度' }]">
<a-input-number v-model="form.width" placeholder="请输入宽度" :min="1" :precision="2" :disabled="isEdit" 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 label="深度(mm)" field="depth" :rules="[{ required: true, message: '请输入大于0的机柜深度' }]">
<a-input-number v-model="form.depth" placeholder="请输入深度" :min="1" :precision="2" :disabled="isEdit" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
@@ -244,24 +245,14 @@
<!-- 位置信息 -->
<a-divider orientation="left">位置信息</a-divider>
<a-row :gutter="16">
<a-col :span="6">
<a-col :span="12">
<a-form-item label="行号" field="row">
<a-input-number v-model="form.row" placeholder="请输入行号" :min="0" style="width: 100%" />
<a-input-number v-model="form.row" placeholder="请输入行号" :min="0" :disabled="isEdit" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-col :span="12">
<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-input-number v-model="form.column" placeholder="请输入列号" :min="0" :disabled="isEdit" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
@@ -332,8 +323,6 @@ interface Rack {
department?: string
row?: number
column?: number
position_x?: number
position_y?: number
color?: string
enabled?: boolean
description?: string
@@ -397,8 +386,6 @@ const form = ref({
department: '',
row: 0,
column: 0,
position_x: undefined as number | undefined,
position_y: undefined as number | undefined,
color: '',
enabled: true,
description: '',
@@ -414,7 +401,7 @@ const loadDatacenterList = async () => {
try {
const res: any = await fetchDatacenterList()
if (res.code === 0) {
datacenterList.value = res.details || []
datacenterList.value = res.details
}
} catch (error) {
console.error('获取数据中心列表失败:', error)
@@ -433,8 +420,7 @@ const loadFloorList = async (datacenterId?: number, keyword?: string) => {
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 : []
floorList.value = res.details
}
} catch (error) {
console.error('获取楼层列表失败:', error)
@@ -449,7 +435,7 @@ const loadSupplierList = async () => {
try {
const res: any = await fetchSupplierList()
if (res.code === 0) {
supplierList.value = res.details || []
supplierList.value = res.details
}
} catch (error) {
console.error('获取供应商列表失败:', error)
@@ -475,8 +461,7 @@ const loadRoomList = async (floorId?: number, keyword?: string) => {
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 : []
roomList.value = res.details
}
} catch (error) {
console.error('获取机房列表失败:', error)
@@ -548,8 +533,6 @@ watch(
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 || '',
@@ -594,8 +577,6 @@ watch(
department: '',
row: 0,
column: 0,
position_x: undefined,
position_y: undefined,
color: '',
enabled: true,
description: '',
@@ -645,8 +626,6 @@ const handleOk = async () => {
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,

View File

@@ -86,7 +86,7 @@
import { ref, reactive, computed, watch, onMounted } 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 { useRoute, 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'
@@ -97,16 +97,25 @@ import { fetchRoomListByFloor } from '@/api/ops/room'
import RackDetailDialog from './components/RackDetailDialog.vue'
import RackFormDialog from './components/RackFormDialog.vue'
const route = useRoute()
const router = useRouter()
const getRouteId = (key: 'datacenter_id' | 'floor_id' | 'room_id') => {
const value = route.query[key]
if (typeof value !== 'string') return undefined
const id = Number(value)
return Number.isInteger(id) && id > 0 ? id : undefined
}
// 状态管理
const loading = ref(false)
const tableData = ref<any[]>([])
const formModel = ref({
keyword: '',
datacenter_id: undefined,
floor_id: undefined,
room_id: undefined,
datacenter_id: getRouteId('datacenter_id') as number | undefined,
floor_id: getRouteId('floor_id') as number | undefined,
room_id: getRouteId('room_id') as number | undefined,
rack_type: undefined,
status: undefined,
})
@@ -172,9 +181,7 @@ const loadDatacenterOptions = async (keyword?: string) => {
try {
const res: any = await fetchDatacenterList({ keyword })
if (res.code === 0) {
const list = res.details?.data ?? res.data ?? res.details ?? []
const rows = Array.isArray(list) ? list : []
datacenterSelectOptions.value = rows.map((d: any) => ({
datacenterSelectOptions.value = res.details.map((d: any) => ({
label: d.name || d.code || String(d.id),
value: d.id,
}))
@@ -195,9 +202,7 @@ const loadFloorOptions = async (datacenterId?: number, keyword?: string) => {
try {
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 : []
floorSelectOptions.value = rows.map((floor: any) => ({
floorSelectOptions.value = res.details.map((floor: any) => ({
label: floor.name || String(floor.id),
value: floor.id,
}))
@@ -217,9 +222,7 @@ const loadRoomOptions = async (floorId?: number, keyword?: string) => {
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) => ({
roomSelectOptions.value = res.details.map((room: any) => ({
label: room.name || String(room.id),
value: room.id,
}))
@@ -347,9 +350,8 @@ const fetchRacks = async () => {
const res = await fetchRackList(params)
const payload = res?.data ?? res?.details ?? {}
tableData.value = Array.isArray(payload?.data) ? payload.data : []
pagination.total = payload?.total ?? 0
tableData.value = res.details.data
pagination.total = res.details.total
} catch (error) {
console.error('获取机柜列表失败:', error)
Message.error('获取机柜列表失败')
@@ -381,6 +383,15 @@ const handleReset = () => {
rack_type: undefined,
status: undefined,
}
router.replace({
path: route.path,
query: {
...route.query,
datacenter_id: undefined,
floor_id: undefined,
room_id: undefined,
},
})
pagination.current = 1
fetchRacks()
}
@@ -453,6 +464,8 @@ const handleFormSuccess = () => {
// 初始化加载数据
onMounted(() => {
loadDatacenterOptions()
loadFloorOptions(formModel.value.datacenter_id)
loadRoomOptions(formModel.value.floor_id)
fetchRacks()
})
</script>

View File

@@ -56,10 +56,6 @@
</a-select>
</a-form-item>
<a-form-item label="已创建3D机房" field="has3D">
<a-switch v-model="form.has3D" />
</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>
@@ -81,7 +77,6 @@ interface Room {
datacenter_id?: number
floor_id?: number
status?: string
has3D: boolean
description?: string
}
@@ -112,7 +107,6 @@ const form = ref({
datacenter_id: undefined as number | undefined,
floor_id: undefined as number | undefined,
status: 'planning',
has3D: false,
description: '',
})
@@ -123,7 +117,7 @@ const loadDatacenterList = async () => {
try {
const res: any = await fetchDatacenterList()
if (res.code === 0) {
datacenterList.value = res.details || []
datacenterList.value = res.details
}
} catch (error) {
console.error('获取数据中心列表失败:', error)
@@ -141,8 +135,7 @@ const loadFloorList = async (datacenterId?: number, keyword?: string) => {
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 : []
floorList.value = res.details
}
} catch (error) {
console.error('获取楼层列表失败:', error)
@@ -179,7 +172,6 @@ watch(
datacenter_id: props.room.datacenter_id,
floor_id: props.room.floor_id,
status: props.room.status || 'planning',
has3D: props.room.has3D,
description: props.room.description || '',
}
if (props.room.datacenter_id) {
@@ -192,7 +184,6 @@ watch(
datacenter_id: undefined,
floor_id: undefined,
status: 'planning',
has3D: false,
description: '',
}
floorList.value = []
@@ -212,7 +203,6 @@ const handleOk = async () => {
datacenter_id: form.value.datacenter_id,
floor_id: form.value.floor_id,
status: form.value.status,
has3D: form.value.has3D,
description: form.value.description,
}

View File

@@ -45,16 +45,9 @@
<template #actions="{ record }">
<a-button type="text" size="small" @click="handleDetail(record)">详情</a-button>
<a-button type="text" size="small" @click="handleRacks(record)">机柜</a-button>
<a-button type="text" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button
type="text"
size="small"
:disabled="!record.has3D"
:style="{ color: record.has3D ? undefined : 'rgb(var(--gray-6))' }"
@click="handleThreeDRoom"
>
3d机房
</a-button>
<a-button type="text" size="small" @click="handleThreeDRoom(record)">3D机房</a-button>
<a-button type="text" size="small" status="danger" @click="handleDelete(record)">删除</a-button>
</template>
</search-table>
@@ -67,9 +60,9 @@
<script lang="ts" setup>
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
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'
@@ -79,8 +72,17 @@ import { fetchDatacenterList, fetchFloorListByDatacenter } from '@/api/ops/floor
import RoomFormDialog from './components/RoomFormDialog.vue'
import RoomDetailDialog from './components/RoomDetailDialog.vue'
const route = useRoute()
const router = useRouter()
const getRouteId = (key: 'datacenter_id' | 'floor_id') => {
const value = route.query[key]
if (typeof value !== 'string') return undefined
const id = Number(value)
return Number.isInteger(id) && id > 0 ? id : undefined
}
const statusMap: Record<string, { text: string; color: string }> = {
planning: { text: '规划中', color: 'blue' },
construction: { text: '建设中', color: 'orange' },
@@ -93,8 +95,8 @@ const loading = ref(false)
const tableData = ref<any[]>([])
const formModel = ref({
keyword: '',
datacenter_id: undefined as number | undefined,
floor_id: undefined as number | undefined,
datacenter_id: getRouteId('datacenter_id') as number | undefined,
floor_id: getRouteId('floor_id') as number | undefined,
status: '',
})
@@ -144,7 +146,7 @@ const loadDatacenterOptions = async (keyword?: string) => {
try {
const res: any = await fetchDatacenterList({ keyword })
if (res.code === 0) {
const list = res.details || []
const list = res.details
datacenterSelectOptions.value = Array.isArray(list)
? list.map((d: any) => ({
label: d.name || d.code || String(d.id),
@@ -167,13 +169,10 @@ const loadFloorOptions = async (datacenterId?: number, keyword?: string) => {
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,
}))
: []
floorSelectOptions.value = res.details.map((f: any) => ({
label: f.name || String(f.id),
value: f.id,
}))
}
} catch (error) {
console.error('获取楼层列表失败:', error)
@@ -224,8 +223,8 @@ const fetchRooms = async () => {
}
const res: any = await fetchRoomList(params)
tableData.value = res.details?.data || []
pagination.total = res.details?.total || 0
tableData.value = res.details.data
pagination.total = res.details.total
} catch (error) {
console.error('获取机房列表失败:', error)
Message.error('获取机房列表失败')
@@ -252,6 +251,10 @@ const handleReset = () => {
floor_id: undefined,
status: '',
}
router.replace({
path: route.path,
query: { ...route.query, datacenter_id: undefined, floor_id: undefined },
})
pagination.current = 1
fetchRooms()
}
@@ -281,8 +284,19 @@ const handleDetail = (record: any) => {
detailVisible.value = true
}
const handleThreeDRoom = () => {
router.push('/datacenter/room')
const handleRacks = (record: any) => {
router.push({
path: '/datacenter/rack',
query: {
datacenter_id: String(record.datacenter_id),
floor_id: String(record.floor_id),
room_id: String(record.id),
},
})
}
const handleThreeDRoom = (record: any) => {
router.push(`/datacenter/room-3d/${record.id}`)
}
const handleDelete = async (record: any) => {
@@ -312,6 +326,7 @@ const handleFormSuccess = () => {
onMounted(() => {
loadDatacenterOptions()
loadFloorOptions(formModel.value.datacenter_id)
fetchRooms()
})
</script>

View File

@@ -54,6 +54,8 @@ export default defineComponent({
import { ref, reactive, computed, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import { fetchAssetAll } from '@/api/ops/asset'
import { fetchRoomDetail } from '@/api/ops/room'
import { placeAssetInRack } from '@/api/ops/unit'
interface Asset {
id: number
@@ -69,6 +71,8 @@ interface Props {
visible: boolean
rackId: number
rackHeight: number
roomId?: number
initialStartUnit: number
}
interface Emits {
@@ -129,15 +133,13 @@ const loadAssetList = async (keyword?: string) => {
try {
const res: any = await fetchAssetAll({ keyword: keyword || undefined })
if (res.code === 0) {
const rowsCandidate = res?.data?.data ?? res?.details?.data ?? res?.data ?? res?.details ?? []
const rows = Array.isArray(rowsCandidate) ? rowsCandidate : []
assetList.value = rows.map((item: any) => ({
assetList.value = res.details.map((item: any) => ({
id: item.id,
asset_code: item.asset_code || '',
asset_name: item.asset_name || '',
asset_type_name: item?.category?.name || item?.category_name || item?.asset_type_name || '',
asset_type_code: item?.category?.code ?? item?.category_code ?? item?.asset_type ?? '',
category_id: item?.category_id,
asset_code: item.asset_code,
asset_name: item.asset_name,
asset_type_name: item.category?.name || '',
asset_type_code: item.category?.code || '',
category_id: item.category_id,
power_consumption: item.power_consumption,
}))
} else {
@@ -173,27 +175,36 @@ const handleBeforeOk = async () => {
// 防止 change 事件未触发时字段未回写
syncAssetFields(formData.asset_id)
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 使用分类标识category.code
asset_type: formData.asset_type_code,
power_consumption: formData.power_consumption,
description: formData.description,
if (!props.roomId || !formData.asset_id) {
Message.error('机房或设备信息不完整')
return false
}
const res = await allocateUnit(params)
if (res.code === 0) {
try {
const roomRes = await fetchRoomDetail(props.roomId)
if (roomRes.code !== 0) {
Message.error(roomRes.message || '获取机房布局版本失败')
return false
}
const res = await placeAssetInRack(props.roomId, formData.asset_id, {
expected_version: roomRes.details.layout_version,
rack_id: props.rackId,
start_unit: formData.start_unit,
occupied_units: formData.occupied_units,
power_consumption: formData.power_consumption ?? 0,
})
if (res.code !== 0) {
Message.error(res.message || '分配失败')
return false
}
Message.success('分配成功')
emit('success')
return true
} else {
Message.error(res.message || '分配失败')
} catch (error) {
console.error('分配U位失败:', error)
Message.error('分配失败')
return false
}
}
@@ -218,12 +229,13 @@ const resetForm = () => {
watch(
() => props.visible,
(val) => {
if (val) {
(visible) => {
if (visible) {
formData.start_unit = props.initialStartUnit
loadAssetList()
} else {
resetForm()
return
}
resetForm()
}
)

View File

@@ -0,0 +1,227 @@
<template>
<div v-if="units.length" class="graphic-layout">
<div class="unit-grid">
<button
v-for="unit in sortedUnits"
:key="unit.unit_number"
type="button"
class="unit-tile"
:class="[unit.status, { selected: selectedUnitNumbers.has(unit.unit_number) }]"
:title="`U${unit.unit_number} · ${getTileText(unit)}`"
@click="emit('select', unit)"
>
<strong>U{{ unit.unit_number }}</strong>
<span>{{ getTileText(unit) }}</span>
</button>
</div>
<aside class="unit-detail">
<template v-if="selectedUnit">
<h3>U 位详情</h3>
<a-descriptions :column="1" size="small">
<a-descriptions-item label="U 位范围">U{{ selectedStartUnit }} - U{{ selectedEndUnit }}</a-descriptions-item>
<a-descriptions-item label="状态">{{ statusMeta[selectedUnit.status].text }}</a-descriptions-item>
<template v-if="selectedUnit.status === 'occupied'">
<a-descriptions-item label="设备名称">{{ selectedUnit.asset_name }}</a-descriptions-item>
<a-descriptions-item label="设备编号">{{ selectedUnit.asset_code }}</a-descriptions-item>
<a-descriptions-item label="设备类型">{{ selectedUnit.asset_type }}</a-descriptions-item>
<a-descriptions-item label="功耗">{{ selectedUnit.power_consumption }}W</a-descriptions-item>
</template>
<template v-else-if="selectedUnit.status === 'reserved'">
<a-descriptions-item label="预留对象">{{ selectedUnit.reserved_for }}</a-descriptions-item>
<a-descriptions-item label="截止时间">{{ selectedUnit.reserved_until }}</a-descriptions-item>
</template>
</a-descriptions>
<div class="detail-actions">
<template v-if="selectedUnit.status === 'available'">
<a-button type="primary" @click="emit('allocate', selectedUnit)">分配 U </a-button>
<a-button @click="emit('reserve', selectedUnit)">预留 U </a-button>
<a-button status="danger" @click="emit('disable', selectedUnit)">禁用</a-button>
</template>
<a-button v-else-if="selectedUnit.status === 'occupied'" status="danger" @click="emit('release', selectedUnit)">释放</a-button>
<a-button v-else-if="selectedUnit.status === 'reserved'" status="danger" @click="handleCancelReservation">取消预留</a-button>
<a-button v-else-if="selectedUnit.status === 'disabled'" @click="emit('enable', selectedUnit)">启用</a-button>
</div>
</template>
<a-empty v-else description="请选择 U 位" />
</aside>
</div>
<a-empty v-else description="请先选择机柜" />
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import type { RackUnitItem, RackUnitStatus } from '@/api/ops/unit'
interface Props {
units: RackUnitItem[]
selectedUnitNumber?: number
}
interface UnitRangeAction {
unit: RackUnitItem
startUnit: number
endUnit: number
}
interface Emits {
(event: 'select', unit: RackUnitItem): void
(event: 'allocate', unit: RackUnitItem): void
(event: 'reserve', unit: RackUnitItem): void
(event: 'disable', unit: RackUnitItem): void
(event: 'enable', unit: RackUnitItem): void
(event: 'release', unit: RackUnitItem): void
(event: 'cancel-reservation', payload: UnitRangeAction): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const statusMeta: Record<RackUnitStatus, { text: string; tileText: string }> = {
available: { text: '可用', tileText: '可用' },
occupied: { text: '已占用', tileText: '占用' },
reserved: { text: '已预留', tileText: '预留' },
disabled: { text: '已禁用', tileText: '禁用' },
}
const sortedUnits = computed(() => [...props.units].sort((left, right) => right.unit_number - left.unit_number))
const selectedUnit = computed(() => props.units.find((unit) => unit.unit_number === props.selectedUnitNumber))
const getTileText = (unit: RackUnitItem) => {
if (unit.status === 'occupied') return unit.asset_code
if (unit.status === 'reserved') return unit.reserved_for
return statusMeta[unit.status].tileText
}
const isSameReservation = (left: RackUnitItem, right: RackUnitItem) => {
return (
left.status === 'reserved' &&
right.status === 'reserved' &&
left.reserved_for === right.reserved_for &&
left.reserved_until === right.reserved_until
)
}
const getReservedGroup = (unit: RackUnitItem) => {
const unitMap = new Map(props.units.map((item) => [item.unit_number, item]))
const result = [unit]
for (const direction of [-1, 1]) {
let unitNumber = unit.unit_number + direction
let candidate = unitMap.get(unitNumber)
while (candidate && isSameReservation(unit, candidate)) {
result.push(candidate)
unitNumber += direction
candidate = unitMap.get(unitNumber)
}
}
return result
}
const selectedGroup = computed(() => {
const unit = selectedUnit.value
if (!unit) return []
if (unit.status === 'occupied') {
return props.units.filter((item) => item.status === 'occupied' && item.asset_id === unit.asset_id)
}
if (unit.status === 'reserved') return getReservedGroup(unit)
return [unit]
})
const selectedUnitNumbers = computed(() => new Set(selectedGroup.value.map((unit) => unit.unit_number)))
const selectedStartUnit = computed(() => Math.min(...selectedGroup.value.map((unit) => unit.unit_number)))
const selectedEndUnit = computed(() => Math.max(...selectedGroup.value.map((unit) => unit.unit_number)))
const handleCancelReservation = () => {
const unit = selectedUnit.value
if (!unit) return
emit('cancel-reservation', {
unit,
startUnit: selectedStartUnit.value,
endUnit: selectedEndUnit.value,
})
}
</script>
<style scoped lang="less">
.graphic-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 300px;
min-height: 520px;
}
.unit-grid {
display: grid;
grid-template-columns: repeat(6, minmax(84px, 1fr));
align-content: start;
gap: 10px;
padding: 20px;
}
.unit-tile {
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
height: 58px;
padding: 9px 10px;
border: 1px solid transparent;
border-radius: 5px;
text-align: left;
cursor: pointer;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.unit-tile.available {
color: #006d22;
border-color: #7be188;
background: #e8ffea;
}
.unit-tile.occupied {
color: #0e42d2;
border-color: #94bfff;
background: #e8f3ff;
}
.unit-tile.reserved {
color: #b54708;
border-color: #ffb65d;
background: #fff7e8;
}
.unit-tile.disabled {
color: #4e5969;
border-color: #c9cdd4;
background: repeating-linear-gradient(135deg, #f2f3f5 0, #f2f3f5 6px, #e5e6eb 6px, #e5e6eb 10px);
}
.unit-tile.selected {
outline: 3px solid rgba(114, 46, 209, 0.72);
outline-offset: 1px;
box-shadow: 0 0 0 5px rgba(114, 46, 209, 0.11);
}
.unit-detail {
padding: 20px;
border-left: 1px solid #e5e6eb;
h3 {
margin: 0 0 16px;
}
}
.detail-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 20px;
}
</style>

View File

@@ -46,6 +46,7 @@ interface Props {
visible: boolean
rackId: number
rackHeight: number
initialStartUnit: number
}
interface Emits {
@@ -129,10 +130,12 @@ const resetForm = () => {
watch(
() => props.visible,
(val) => {
if (!val) {
resetForm()
(visible) => {
if (visible) {
formData.start_unit = props.initialStartUnit
return
}
resetForm()
}
)
</script>

View File

@@ -42,7 +42,7 @@
<div class="info-label">使用率</div>
<div class="info-value">
<a-progress
:percent="usagePercentage"
:percent="usagePercentage / 100"
:size="'small'"
:color="usagePercentage > 80 ? '#f53f3f' : usagePercentage > 50 ? '#ff7d00' : '#00b42a'"
/>
@@ -55,11 +55,23 @@
<div class="info-value">{{ availableUnits }}</div>
</div>
</a-col>
<a-col :span="6">
<div class="info-item">
<div class="info-label">预留U位</div>
<div class="info-value">{{ reservedUnits }}</div>
</div>
</a-col>
<a-col :span="6">
<div class="info-item">
<div class="info-label">禁用U位</div>
<div class="info-value">{{ disabledUnits }}</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'" />
<a-progress :percent="availablePercentage / 100" :size="'small'" :color="'#00b42a'" />
</div>
</div>
</a-col>
@@ -135,12 +147,17 @@
<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 === 'available'" type="text" size="small" @click="handleDisable(record)">禁用</a-button>
<a-button v-else-if="record.status === 'disabled'" type="text" size="small" @click="handleEnable(record)"></a-button>
<!-- 已占用状态显示释放按钮 -->
<a-button v-if="record.status === 'occupied'" type="text" size="small" status="danger" @click="handleRelease(record)">
<a-button
v-if="record.status === 'occupied' && record.asset_id && record.unit_number === record.asset?.unit_start"
type="text"
size="small"
status="danger"
@click="handleRelease(record)"
>
释放
</a-button>
@@ -162,10 +179,23 @@
</a-card>
<!-- 分配U位对话框 -->
<allocate-unit-dialog v-model:visible="allocateVisible" :rack-id="rackId" :rack-height="rackInfo.height" @success="handleRefresh" />
<allocate-unit-dialog
v-model:visible="allocateVisible"
:rack-id="rackId"
:rack-height="rackInfo.height"
:room-id="rackInfo.room_id"
:initial-start-unit="1"
@success="handleRefresh"
/>
<!-- 预留U位对话框 -->
<reserve-unit-dialog v-model:visible="reserveVisible" :rack-id="rackId" :rack-height="rackInfo.height" @success="handleRefresh" />
<reserve-unit-dialog
v-model:visible="reserveVisible"
:rack-id="rackId"
:rack-height="rackInfo.height"
:initial-start-unit="1"
@success="handleRefresh"
/>
</div>
</template>
@@ -174,8 +204,8 @@ 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 { normalizeUnitList } from './utils/unitFallback'
import { fetchUnitList, reserveUnit, cancelReservation, unplaceAsset, updateUnitStatus } from '@/api/ops/unit'
import { fetchRoomDetail } from '@/api/ops/room'
import AllocateUnitDialog from './components/AllocateUnitDialog.vue'
import ReserveUnitDialog from './components/ReserveUnitDialog.vue'
@@ -200,12 +230,15 @@ const pageTitle = computed(() => {
// 已使用U位
const usedUnits = computed(() => {
return unitList.value.filter((unit) => unit.status === 'occupied' || unit.status === 'reserved').length
return unitList.value.filter((unit) => unit.status === 'occupied').length
})
const reservedUnits = computed(() => unitList.value.filter((unit) => unit.status === 'reserved').length)
const disabledUnits = computed(() => unitList.value.filter((unit) => unit.status === 'disabled').length)
// 空余U位
const availableUnits = computed(() => {
return rackInfo.value.height - usedUnits.value
return unitList.value.filter((unit) => unit.status === 'available').length
})
// 使用率
@@ -222,9 +255,7 @@ const availablePercentage = computed(() => {
// 总功耗
const totalPower = computed(() => {
return unitList.value.reduce((total, unit) => {
return total + (unit.power_consumption || 0)
}, 0)
return Math.round((Number(rackInfo.value.used_power) || 0) * 1000)
})
// 获取机柜状态颜色
@@ -281,9 +312,8 @@ const fetchUnits = async () => {
const res = await fetchUnitList(rackId.value)
if (res.code === 0) {
const payload = res?.details ?? res?.data ?? {}
rackInfo.value = payload?.rack || {}
unitList.value = normalizeUnitList(payload?.units, rackInfo.value?.height)
rackInfo.value = res.details.rack
unitList.value = res.details.units
} else {
Message.error(res.message || '获取U位列表失败')
}
@@ -377,13 +407,18 @@ const handleRelease = async (record: any) => {
title: '确认释放',
content: `确认释放 U位 ${record.unit_number} 吗?`,
onOk: async () => {
const endUnit = record.occupied_units > 1 ? record.unit_number + record.occupied_units - 1 : record.unit_number
if (!rackInfo.value.room_id || !record.asset_id) {
Message.error('机房或资产信息不完整')
return
}
const res = await releaseUnit({
rack_id: rackId.value,
start_unit: record.unit_number,
end_unit: endUnit,
})
const roomRes = await fetchRoomDetail(rackInfo.value.room_id)
if (roomRes.code !== 0) {
Message.error(roomRes.message || '获取机房布局版本失败')
return
}
const res = await unplaceAsset(rackInfo.value.room_id, record.asset_id, roomRes.details.layout_version)
if (res.code === 0) {
Message.success('释放成功')

View File

@@ -72,6 +72,8 @@
<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="reservedUnits" color="orange">已预留: {{ reservedUnits }}</a-tag>
<a-tag v-if="disabledUnits" color="gray">已禁用: {{ disabledUnits }}</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>
@@ -100,6 +102,14 @@
</template>
刷新
</a-button>
<a-divider direction="vertical" />
<a-button type="outline" @click="handleToggleView">
<template #icon>
<icon-apps v-if="viewMode === 'list'" />
<icon-list v-else />
</template>
{{ viewMode === 'list' ? '图形视图' : '列表视图' }}
</a-button>
</a-space>
</a-card>
@@ -107,10 +117,23 @@
<a-card class="u-position-card" :loading="loading">
<template #title>
<icon-apps />
U位列表
{{ viewMode === 'list' ? 'U位列表' : 'U位图形视图' }}
</template>
<a-table :data="unitList" :pagination="false" :bordered="{ cell: true }" :scroll="{ x: 1400 }" :loading="loading" size="small">
<rack-unit-graphic
v-if="viewMode === 'graphic'"
:units="unitList"
:selected-unit-number="selectedUnitNumber"
@select="handleGraphicSelect"
@allocate="openAllocateDialog"
@reserve="openReserveDialog"
@disable="handleDisable"
@enable="handleEnable"
@release="handleRelease"
@cancel-reservation="handleGraphicCancelReservation"
/>
<a-table v-else :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 }">
@@ -134,12 +157,17 @@
<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 === 'available'" type="text" size="small" @click="handleDisable(record)">禁用</a-button>
<a-button v-else-if="record.status === 'disabled'" type="text" size="small" @click="handleEnable(record)"></a-button>
<!-- 已占用状态显示释放按钮 -->
<a-button v-if="record.status === 'occupied'" type="text" size="small" status="danger" @click="handleRelease(record)">
<a-button
v-if="record.status === 'occupied' && record.asset_id && record.unit_number === record.asset?.unit_start"
type="text"
size="small"
status="danger"
@click="handleRelease(record)"
>
释放
</a-button>
@@ -162,17 +190,22 @@
<!-- 分配U位对话框 -->
<allocate-unit-dialog
v-if="selectedRackId && rackInfo.height"
v-model:visible="allocateVisible"
:rack-id="selectedRackId!"
:rack-id="selectedRackId"
:rack-height="rackInfo.height"
:room-id="rackInfo.room_id"
:initial-start-unit="dialogStartUnit"
@success="handleRefresh"
/>
<!-- 预留U位对话框 -->
<reserve-unit-dialog
v-if="selectedRackId && rackInfo.height"
v-model:visible="reserveVisible"
:rack-id="selectedRackId!"
:rack-id="selectedRackId"
:rack-height="rackInfo.height"
:initial-start-unit="dialogStartUnit"
@success="handleRefresh"
/>
</div>
@@ -181,16 +214,19 @@
<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 { IconStorage, IconApps, IconList, 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 { fetchUnitList, cancelReservation, unplaceAsset, updateUnitStatus } from '@/api/ops/unit'
import type { RackUnitItem, RackUnitRackInfo } from '@/api/ops/unit'
import { fetchDatacenterList, fetchRackListByRoom } from '@/api/ops/rack'
import { fetchFloorListByDatacenter } from '@/api/ops/floor'
import { fetchRoomListByFloor } from '@/api/ops/room'
import { normalizeUnitList } from './utils/unitFallback'
import { fetchRoomDetail, fetchRoomListByFloor } from '@/api/ops/room'
import AllocateUnitDialog from './components/AllocateUnitDialog.vue'
import RackUnitGraphic from './components/RackUnitGraphic.vue'
import ReserveUnitDialog from './components/ReserveUnitDialog.vue'
type ViewMode = 'list' | 'graphic'
// 状态管理
const loading = ref(false)
const rackListLoading = ref(false)
@@ -201,30 +237,42 @@ const selectedDatacenterId = ref<number | undefined>(undefined)
const selectedFloorId = ref<number | undefined>(undefined)
const selectedRoomId = ref<number | undefined>(undefined)
const selectedRackId = ref<number | undefined>(undefined)
const rackInfo = ref<any>({})
const viewMode = ref<ViewMode>('list')
const selectedUnitNumber = ref<number>()
const dialogStartUnit = ref(1)
const rackInfo = ref<Partial<RackUnitRackInfo>>({})
const datacenterList = ref<{ label: string; value: number }[]>([])
const floorList = ref<{ label: string; value: number }[]>([])
const roomList = ref<any[]>([])
const rackList = ref<any[]>([])
const unitList = ref<any[]>([])
const unitList = ref<RackUnitItem[]>([])
let datacenterSearchTimer: number | undefined
let floorSearchTimer: number | undefined
let roomSearchTimer: number | undefined
let rackSearchTimer: number | undefined
let unitRequestSequence = 0
const route = useRoute()
const invalidateUnitRequest = () => {
unitRequestSequence += 1
loading.value = false
}
// 对话框可见性
const allocateVisible = ref(false)
const reserveVisible = ref(false)
// 已使用U位
const usedUnits = computed(() => {
return unitList.value.filter((unit) => unit.status === 'occupied' || unit.status === 'reserved').length
return unitList.value.filter((unit) => unit.status === 'occupied').length
})
const reservedUnits = computed(() => unitList.value.filter((unit) => unit.status === 'reserved').length)
const disabledUnits = computed(() => unitList.value.filter((unit) => unit.status === 'disabled').length)
// 空余U位
const availableUnits = computed(() => {
return rackInfo.value.height - usedUnits.value
return unitList.value.filter((unit) => unit.status === 'available').length
})
// 使用率
@@ -233,11 +281,6 @@ const usagePercentage = computed(() => {
return Math.round((usedUnits.value / rackInfo.value.height) * 100)
})
const extractList = (res: any): any[] => {
const candidate = res?.data?.data ?? res?.details?.data ?? res?.data ?? res?.details ?? []
return Array.isArray(candidate) ? candidate : []
}
// 获取U位状态颜色
const getUnitStatusColor = (status?: string) => {
const colorMap: Record<string, string> = {
@@ -270,7 +313,7 @@ const fetchRacks = async (keyword?: string) => {
try {
const res: any = await fetchRackListByRoom(selectedRoomId.value, { name: keyword })
rackList.value = extractList(res)
rackList.value = res.details
} catch (error) {
console.error('获取机柜列表失败:', error)
Message.error('获取机柜列表失败')
@@ -284,7 +327,7 @@ const fetchDatacenters = async (keyword?: string) => {
datacenterListLoading.value = true
try {
const res: any = await fetchDatacenterList({ keyword })
const rows = extractList(res)
const rows = res.details
datacenterList.value = rows.map((d: any) => ({
label: d.name || d.code || String(d.id),
value: d.id,
@@ -308,7 +351,7 @@ const fetchFloors = async (keyword?: string) => {
const res: any = await fetchFloorListByDatacenter(selectedDatacenterId.value, {
name: keyword || undefined,
})
const rows = extractList(res)
const rows = res.details
floorList.value = rows.map((floor: any) => ({
label: floor.name || floor.code || String(floor.id),
value: floor.id,
@@ -332,7 +375,7 @@ const fetchRooms = async (keyword?: string) => {
const res: any = await fetchRoomListByFloor(selectedFloorId.value, {
name: keyword || undefined,
})
roomList.value = extractList(res)
roomList.value = res.details
} catch (error) {
console.error('获取机房列表失败:', error)
Message.error('获取机房列表失败')
@@ -385,6 +428,7 @@ const handleDatacenterChange = async (datacenterId?: number | string) => {
if (datacenterId !== undefined && datacenterId !== null && datacenterId !== '') {
selectedDatacenterId.value = Number(datacenterId)
}
invalidateUnitRequest()
selectedFloorId.value = undefined
selectedRoomId.value = undefined
selectedRackId.value = undefined
@@ -393,6 +437,7 @@ const handleDatacenterChange = async (datacenterId?: number | string) => {
roomList.value = []
floorList.value = []
unitList.value = []
selectedUnitNumber.value = undefined
await fetchFloors()
}
@@ -400,12 +445,14 @@ const handleFloorChange = async (floorId?: number | string) => {
if (floorId !== undefined && floorId !== null && floorId !== '') {
selectedFloorId.value = Number(floorId)
}
invalidateUnitRequest()
selectedRoomId.value = undefined
selectedRackId.value = undefined
rackInfo.value = {}
rackList.value = []
roomList.value = []
unitList.value = []
selectedUnitNumber.value = undefined
await fetchRooms()
}
@@ -413,15 +460,19 @@ const handleRoomChange = async (roomId?: number | string) => {
if (roomId !== undefined && roomId !== null && roomId !== '') {
selectedRoomId.value = Number(roomId)
}
invalidateUnitRequest()
selectedRackId.value = undefined
rackInfo.value = {}
rackList.value = []
unitList.value = []
selectedUnitNumber.value = undefined
await fetchRacks()
}
// 机柜变化
const handleRackChange = (rackId: number) => {
invalidateUnitRequest()
selectedUnitNumber.value = undefined
if (rackId) {
fetchUnits(rackId)
} else {
@@ -436,23 +487,34 @@ const fetchUnits = async (rackId?: number) => {
if (!targetRackId) return
const requestSequence = ++unitRequestSequence
const isCurrentRequest = () => requestSequence === unitRequestSequence && targetRackId === selectedRackId.value
loading.value = true
try {
const res = await fetchUnitList(targetRackId)
if (!isCurrentRequest()) return
if (res.code === 0) {
const payload = res?.details ?? res?.data ?? {}
rackInfo.value = payload?.rack || {}
unitList.value = normalizeUnitList(payload?.units, rackInfo.value?.height)
rackInfo.value = res.details.rack
unitList.value = res.details.units
if (selectedUnitNumber.value !== undefined && !unitList.value.some((unit) => unit.unit_number === selectedUnitNumber.value)) {
selectedUnitNumber.value = undefined
}
} else {
Message.error(res.message || '获取U位列表失败')
}
} catch (error) {
if (!isCurrentRequest()) return
console.error('获取U位列表失败:', error)
Message.error('获取U位列表失败')
} finally {
loading.value = false
if (isCurrentRequest()) {
loading.value = false
}
}
}
@@ -464,12 +526,31 @@ const handleRefresh = () => {
}
}
const handleToggleView = () => {
viewMode.value = viewMode.value === 'list' ? 'graphic' : 'list'
}
const handleGraphicSelect = (unit: RackUnitItem) => {
selectedUnitNumber.value = unit.unit_number
}
const openAllocateDialog = (unit: RackUnitItem) => {
dialogStartUnit.value = unit.unit_number
allocateVisible.value = true
}
const openReserveDialog = (unit: RackUnitItem) => {
dialogStartUnit.value = unit.unit_number
reserveVisible.value = true
}
// 分配U位
const handleAllocate = () => {
if (!selectedRackId.value) {
Message.warning('请先选择机柜')
return
}
dialogStartUnit.value = 1
allocateVisible.value = true
}
@@ -479,6 +560,7 @@ const handleReserve = () => {
Message.warning('请先选择机柜')
return
}
dialogStartUnit.value = 1
reserveVisible.value = true
}
@@ -488,24 +570,31 @@ const handleDisable = async (record: any) => {
Modal.confirm({
title: '确认禁用',
content: `确认禁用 U位 ${record.unit_number} 吗?`,
onOk: async () => {
onBeforeOk: async () => {
if (!selectedRackId.value) {
Message.warning('请先选择机柜')
return
return false
}
const res = await updateUnitStatus({
rack_id: selectedRackId.value,
start_unit: record.unit_number,
end_unit: record.unit_number,
status: 'disabled',
})
try {
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 {
if (res.code === 0) {
Message.success('禁用成功')
await fetchUnits()
return true
}
Message.error(res.message || '禁用失败')
return false
} catch (error) {
console.error('禁用U位失败:', error)
Message.error('禁用失败')
return false
}
},
})
@@ -520,24 +609,31 @@ const handleEnable = async (record: any) => {
Modal.confirm({
title: '确认启用',
content: `确认启用 U位 ${record.unit_number} 吗?`,
onOk: async () => {
onBeforeOk: async () => {
if (!selectedRackId.value) {
Message.warning('请先选择机柜')
return
return false
}
const res = await updateUnitStatus({
rack_id: selectedRackId.value,
start_unit: record.unit_number,
end_unit: record.unit_number,
status: 'available',
})
try {
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 {
if (res.code === 0) {
Message.success('启用成功')
await fetchUnits()
return true
}
Message.error(res.message || '启用失败')
return false
} catch (error) {
console.error('启用U位失败:', error)
Message.error('启用失败')
return false
}
},
})
@@ -548,29 +644,43 @@ const handleEnable = async (record: any) => {
// 释放U位
const handleRelease = async (record: any) => {
const occupiedUnitNumbers = unitList.value
.filter((unit) => unit.status === 'occupied' && unit.asset_id === record.asset_id)
.map((unit) => unit.unit_number)
const startUnit = Math.min(...occupiedUnitNumbers)
const endUnit = Math.max(...occupiedUnitNumbers)
const unitRange = startUnit === endUnit ? `U${startUnit}` : `U${startUnit}-U${endUnit}`
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
content: `确认释放设备 ${record.asset_name}${unitRange}吗?`,
onBeforeOk: async () => {
if (!rackInfo.value.room_id || !record.asset_id) {
Message.error('机房或资产信息不完整')
return false
}
const res = await releaseUnit({
rack_id: selectedRackId.value,
start_unit: record.unit_number,
end_unit: endUnit,
})
try {
const roomRes = await fetchRoomDetail(rackInfo.value.room_id)
if (roomRes.code !== 0) {
Message.error(roomRes.message || '获取机房布局版本失败')
return false
}
if (res.code === 0) {
Message.success('释放成功')
fetchUnits()
} else {
const res = await unplaceAsset(rackInfo.value.room_id, record.asset_id, roomRes.details.layout_version)
if (res.code === 0) {
Message.success('释放成功')
await fetchUnits()
return true
}
Message.error(res.message || '释放失败')
return false
} catch (error) {
console.error('释放U位失败:', error)
Message.error('释放失败')
return false
}
},
})
@@ -579,37 +689,47 @@ const handleRelease = async (record: any) => {
}
}
// 取消预留
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 cancelReservationRange = (startUnit: number, endUnit: number) => {
Modal.confirm({
title: '确认取消预留',
content: `确认取消 U位 ${startUnit}${endUnit > startUnit ? `${endUnit}` : ''} 的预留吗?`,
onBeforeOk: async () => {
if (!selectedRackId.value) {
Message.warning('请先选择机柜')
return false
}
try {
const res = await cancelReservation({
rack_id: selectedRackId.value,
start_unit: record.unit_number,
start_unit: startUnit,
end_unit: endUnit,
})
if (res.code === 0) {
Message.success('取消预留成功')
fetchUnits()
} else {
Message.error(res.message || '取消预留失败')
await fetchUnits()
return true
}
},
})
} catch (error) {
console.error('取消预留失败:', error)
}
Message.error(res.message || '取消预留失败')
return false
} catch (error) {
console.error('取消预留失败:', error)
Message.error('取消预留失败')
return false
}
},
})
}
// 取消预留
const handleCancelReservation = (record: RackUnitItem) => {
const endUnit = record.unit_number + record.occupied_units - 1
cancelReservationRange(record.unit_number, endUnit)
}
const handleGraphicCancelReservation = (payload: { unit: RackUnitItem; startUnit: number; endUnit: number }) => {
cancelReservationRange(payload.startUnit, payload.endUnit)
}
// 初始化

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
export const normalizeUnitList = (units: any, rackHeight?: number) => {
if (Array.isArray(units) && units.length > 0) {
return units
}
const height = Number(rackHeight) || 0
if (height <= 0) {
return []
}
return Array.from({ length: height }, (_, idx) => ({
id: `virtual-${height - idx}`,
unit_number: height - idx,
status: 'available',
asset_name: '',
asset_code: '',
asset_type: '',
occupied_units: 1,
reserved_for: '',
power_consumption: 0,
}))
}

View File

@@ -97,18 +97,12 @@ 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)
const rows = res.details
datacenterList.value = rows.map((d: any) => ({
label: d.name || d.code || String(d.id),
value: d.id,
@@ -133,7 +127,7 @@ const fetchFloors = async (keyword?: string) => {
const res: any = await fetchFloorListByDatacenter(selectedDatacenterId.value, {
name: keyword || undefined,
})
const rows = extractList(res)
const rows = res.details
floorList.value = rows.map((floor: any) => ({
label: floor.name || floor.code || String(floor.id),
value: floor.id,
@@ -155,8 +149,8 @@ const fetchRacks = async (keyword?: string) => {
}
rackListLoading.value = true
try {
const res: any = await fetchRackListByFloor(selectedFloorId.value, { name: keyword })
rackList.value = extractList(res)
const res: any = await fetchRackListByFloor(selectedFloorId.value, { keyword })
rackList.value = res.details.data
} catch (error) {
console.error('获取机柜列表失败:', error)
Message.error('获取机柜列表失败')