diff --git a/src/api/ops/asset.ts b/src/api/ops/asset.ts index 5ebe900..818430e 100644 --- a/src/api/ops/asset.ts +++ b/src/api/ops/asset.ts @@ -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 diff --git a/src/api/ops/floor.ts b/src/api/ops/floor.ts index 3401e1b..b3825a2 100644 --- a/src/api/ops/floor.ts +++ b/src/api/ops/floor.ts @@ -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 }) } diff --git a/src/api/ops/rack.ts b/src/api/ops/rack.ts index b8c0fbf..2a2974b 100644 --- a/src/api/ops/rack.ts +++ b/src/api/ops/rack.ts @@ -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 }) } diff --git a/src/api/ops/room.ts b/src/api/ops/room.ts index c7cbf31..e0eb353 100644 --- a/src/api/ops/room.ts +++ b/src/api/ops/room.ts @@ -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 { + 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(`/Assets/v1/room/detail/${id}`) + return request.get(`/Assets/v1/room/detail/${id}`) } /** 获取机房列表(下拉,不分页) */ export const fetchRoomOptions = (params?: RoomOptionsParams) => { - return request.get('/Assets/v1/room/all', { params }) + return request.get>('/Assets/v1/room/all', { params }) } /** 根据楼层获取机房列表(下拉,不分页) */ export const fetchRoomListByFloor = (floorId: number, params?: { name?: string }) => { - return request.get(`/Assets/v1/room/floor/${floorId}`, { params }) + return request.get>(`/Assets/v1/room/floor/${floorId}`, { params }) } /** 创建机房 */ diff --git a/src/api/ops/unit.ts b/src/api/ops/unit.ts index df22082..be0b818 100644 --- a/src/api/ops/unit.ts +++ b/src/api/ops/unit.ts @@ -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(`/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位 */ diff --git a/src/views/ops/pages/assets/device/form/index.vue b/src/views/ops/pages/assets/device/form/index.vue index f662c7c..7b0a825 100644 --- a/src/views/ops/pages/assets/device/form/index.vue +++ b/src/views/ops/pages/assets/device/form/index.vue @@ -97,7 +97,7 @@ - + @@ -146,8 +146,9 @@ + 已放置资产的位置由 U 位或 3D 机房页面维护。 - + @@ -163,7 +165,7 @@ - + - + {{ item.name }} ({{ item.code }}) - - - - {{ item.name }} ({{ item.code }}) - - - - - - - - - - - - - @@ -282,13 +257,13 @@ diff --git a/src/views/ops/pages/datacenter/room/components/RoomFormDialog.vue b/src/views/ops/pages/datacenter/room/components/RoomFormDialog.vue index bf29b0e..177e051 100644 --- a/src/views/ops/pages/datacenter/room/components/RoomFormDialog.vue +++ b/src/views/ops/pages/datacenter/room/components/RoomFormDialog.vue @@ -56,10 +56,6 @@ - - - - @@ -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, } diff --git a/src/views/ops/pages/datacenter/room/index.vue b/src/views/ops/pages/datacenter/room/index.vue index 67a216b..30da5c7 100644 --- a/src/views/ops/pages/datacenter/room/index.vue +++ b/src/views/ops/pages/datacenter/room/index.vue @@ -45,16 +45,9 @@ @@ -67,9 +60,9 @@ diff --git a/src/views/ops/pages/datacenter/u-position/components/AllocateUnitDialog.vue b/src/views/ops/pages/datacenter/u-position/components/AllocateUnitDialog.vue index c6c910b..68cb7cd 100644 --- a/src/views/ops/pages/datacenter/u-position/components/AllocateUnitDialog.vue +++ b/src/views/ops/pages/datacenter/u-position/components/AllocateUnitDialog.vue @@ -54,6 +54,8 @@ export default defineComponent({ import { ref, reactive, computed, watch } from 'vue' import { Message } from '@arco-design/web-vue' import { fetchAssetAll } from '@/api/ops/asset' +import { fetchRoomDetail } from '@/api/ops/room' +import { placeAssetInRack } from '@/api/ops/unit' interface Asset { id: number @@ -69,6 +71,8 @@ interface Props { visible: boolean rackId: number rackHeight: number + roomId?: number + initialStartUnit: number } interface Emits { @@ -129,15 +133,13 @@ const loadAssetList = async (keyword?: string) => { try { const res: any = await fetchAssetAll({ keyword: keyword || undefined }) if (res.code === 0) { - const rowsCandidate = res?.data?.data ?? res?.details?.data ?? res?.data ?? res?.details ?? [] - const rows = Array.isArray(rowsCandidate) ? rowsCandidate : [] - assetList.value = rows.map((item: any) => ({ + assetList.value = res.details.map((item: any) => ({ id: item.id, - asset_code: item.asset_code || '', - asset_name: item.asset_name || '', - asset_type_name: item?.category?.name || item?.category_name || item?.asset_type_name || '', - asset_type_code: item?.category?.code ?? item?.category_code ?? item?.asset_type ?? '', - category_id: item?.category_id, + asset_code: item.asset_code, + asset_name: item.asset_name, + asset_type_name: item.category?.name || '', + asset_type_code: item.category?.code || '', + category_id: item.category_id, power_consumption: item.power_consumption, })) } else { @@ -173,27 +175,36 @@ const handleBeforeOk = async () => { // 防止 change 事件未触发时字段未回写 syncAssetFields(formData.asset_id) - const { allocateUnit } = await import('@/api/ops/unit') - const params = { - rack_id: props.rackId, - start_unit: formData.start_unit, - occupied_units: formData.occupied_units, - asset_id: formData.asset_id, - asset_code: formData.asset_code, - asset_name: formData.asset_name || `未命名设备-${formData.start_unit}`, - // 后端字段 asset_type 使用分类标识(category.code) - asset_type: formData.asset_type_code, - power_consumption: formData.power_consumption, - description: formData.description, + if (!props.roomId || !formData.asset_id) { + Message.error('机房或设备信息不完整') + return false } - const res = await allocateUnit(params) - if (res.code === 0) { + try { + const roomRes = await fetchRoomDetail(props.roomId) + if (roomRes.code !== 0) { + Message.error(roomRes.message || '获取机房布局版本失败') + return false + } + + const res = await placeAssetInRack(props.roomId, formData.asset_id, { + expected_version: roomRes.details.layout_version, + rack_id: props.rackId, + start_unit: formData.start_unit, + occupied_units: formData.occupied_units, + power_consumption: formData.power_consumption ?? 0, + }) + if (res.code !== 0) { + Message.error(res.message || '分配失败') + return false + } + Message.success('分配成功') emit('success') return true - } else { - Message.error(res.message || '分配失败') + } catch (error) { + console.error('分配U位失败:', error) + Message.error('分配失败') return false } } @@ -218,12 +229,13 @@ const resetForm = () => { watch( () => props.visible, - (val) => { - if (val) { + (visible) => { + if (visible) { + formData.start_unit = props.initialStartUnit loadAssetList() - } else { - resetForm() + return } + resetForm() } ) diff --git a/src/views/ops/pages/datacenter/u-position/components/RackUnitGraphic.vue b/src/views/ops/pages/datacenter/u-position/components/RackUnitGraphic.vue new file mode 100644 index 0000000..60ffb38 --- /dev/null +++ b/src/views/ops/pages/datacenter/u-position/components/RackUnitGraphic.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/src/views/ops/pages/datacenter/u-position/components/ReserveUnitDialog.vue b/src/views/ops/pages/datacenter/u-position/components/ReserveUnitDialog.vue index 23181ac..b464f7c 100644 --- a/src/views/ops/pages/datacenter/u-position/components/ReserveUnitDialog.vue +++ b/src/views/ops/pages/datacenter/u-position/components/ReserveUnitDialog.vue @@ -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() } ) diff --git a/src/views/ops/pages/datacenter/u-position/detail.vue b/src/views/ops/pages/datacenter/u-position/detail.vue index 5b4671a..58a3de2 100644 --- a/src/views/ops/pages/datacenter/u-position/detail.vue +++ b/src/views/ops/pages/datacenter/u-position/detail.vue @@ -42,7 +42,7 @@
使用率
@@ -55,11 +55,23 @@
{{ availableUnits }}
+ +
+
预留U位
+
{{ reservedUnits }}
+
+
+ +
+
禁用U位
+
{{ disabledUnits }}
+
+
空余率
- +
@@ -135,12 +147,17 @@ @@ -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('释放成功') diff --git a/src/views/ops/pages/datacenter/u-position/index.vue b/src/views/ops/pages/datacenter/u-position/index.vue index 0b64b26..e40eff5 100644 --- a/src/views/ops/pages/datacenter/u-position/index.vue +++ b/src/views/ops/pages/datacenter/u-position/index.vue @@ -72,6 +72,8 @@ 总U位: {{ rackInfo.height }} 已使用: {{ usedUnits }} + 已预留: {{ reservedUnits }} + 已禁用: {{ disabledUnits }} 空余: {{ availableUnits }} 使用率: {{ usagePercentage }}% @@ -100,6 +102,14 @@ 刷新 + + + + {{ viewMode === 'list' ? '图形视图' : '列表视图' }} + @@ -107,10 +117,23 @@ - + + +