fix: 机柜图形化
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
/** 创建机房 */
|
||||
|
||||
@@ -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位 */
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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('获取设备列表失败')
|
||||
|
||||
@@ -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="可容纳机柜数">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -12,6 +12,11 @@ export const columns: TableColumnData[] = [
|
||||
dataIndex: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '楼层编码',
|
||||
dataIndex: 'code',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '所属中心',
|
||||
dataIndex: 'datacenter',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => ({
|
||||
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>
|
||||
|
||||
@@ -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 = {
|
||||
if (!props.roomId || !formData.asset_id) {
|
||||
Message.error('机房或设备信息不完整')
|
||||
return false
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
power_consumption: formData.power_consumption ?? 0,
|
||||
})
|
||||
if (res.code !== 0) {
|
||||
Message.error(res.message || '分配失败')
|
||||
return false
|
||||
}
|
||||
|
||||
const res = await allocateUnit(params)
|
||||
if (res.code === 0) {
|
||||
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()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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('释放成功')
|
||||
|
||||
@@ -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,25 +487,36 @@ 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 {
|
||||
if (isCurrentRequest()) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
@@ -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,12 +570,13 @@ 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
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await updateUnitStatus({
|
||||
rack_id: selectedRackId.value,
|
||||
start_unit: record.unit_number,
|
||||
@@ -503,9 +586,15 @@ const handleDisable = async (record: any) => {
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success('禁用成功')
|
||||
fetchUnits()
|
||||
} else {
|
||||
await fetchUnits()
|
||||
return true
|
||||
}
|
||||
Message.error(res.message || '禁用失败')
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('禁用U位失败:', error)
|
||||
Message.error('禁用失败')
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -520,12 +609,13 @@ 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
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await updateUnitStatus({
|
||||
rack_id: selectedRackId.value,
|
||||
start_unit: record.unit_number,
|
||||
@@ -535,9 +625,15 @@ const handleEnable = async (record: any) => {
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success('启用成功')
|
||||
fetchUnits()
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
const res = await unplaceAsset(rackInfo.value.room_id, record.asset_id, roomRes.details.layout_version)
|
||||
|
||||
if (res.code === 0) {
|
||||
Message.success('释放成功')
|
||||
fetchUnits()
|
||||
} else {
|
||||
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 {
|
||||
const cancelReservationRange = (startUnit: number, endUnit: number) => {
|
||||
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
|
||||
|
||||
content: `确认取消 U位 ${startUnit}${endUnit > startUnit ? ` 至 ${endUnit}` : ''} 的预留吗?`,
|
||||
onBeforeOk: async () => {
|
||||
if (!selectedRackId.value) {
|
||||
Message.warning('请先选择机柜')
|
||||
return
|
||||
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 {
|
||||
await fetchUnits()
|
||||
return true
|
||||
}
|
||||
Message.error(res.message || '取消预留失败')
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('取消预留失败:', error)
|
||||
Message.error('取消预留失败')
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('取消预留失败:', error)
|
||||
}
|
||||
|
||||
// 取消预留
|
||||
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)
|
||||
}
|
||||
|
||||
// 初始化
|
||||
|
||||
1289
src/views/ops/pages/datacenter/u-position/prototype-preview.html
Normal file
1289
src/views/ops/pages/datacenter/u-position/prototype-preview.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
}))
|
||||
}
|
||||
@@ -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('获取机柜列表失败')
|
||||
|
||||
Reference in New Issue
Block a user