All checks were successful
continuous-integration/drone/push Build is passing
- Add migration script to introduce rack_type field (0=ODF, 1=optical box) - Add RackType, LeftPortsCount, RightPortsCount properties to OdfRacks model and DTOs - Add rack type selector and port count inputs to OdfRackForm component - Display rack type labels in OdfRacks management table - Add rack type badges to uni-app rack list cards - Implement dual-column layout for optical box type in rack detail page with left/right port sections - Add optical box port naming format (A-1, A-2, etc.) with row-based labeling - Add visual distinction with background colors (left: #E3F2FD, right: #FFF3E0) and center divider - Update import/export DTOs to support rack type and optical box port naming - Mark all v1.0.2.1 tasks as completed
498 lines
11 KiB
Vue
498 lines
11 KiB
Vue
<template>
|
||
<view class="rack-detail-page">
|
||
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
|
||
|
||
<view class="content">
|
||
<!-- 顶部导航栏 -->
|
||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||
<view class="nav-bar-inner">
|
||
<image
|
||
class="nav-icon"
|
||
src="/static/images/ic_back.png"
|
||
mode="aspectFit"
|
||
@click="goBack"
|
||
/>
|
||
<text class="nav-title">{{ rackName }}详情</text>
|
||
<view class="nav-icon-placeholder" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 机房名称 + 机架类型 -->
|
||
<view class="room-name-bar">
|
||
<text class="room-name-text">{{ roomName }}</text>
|
||
<text class="rack-type-text">类型:{{ rackType === 1 ? '光交箱' : 'ODF' }}</text>
|
||
</view>
|
||
|
||
<!-- 状态图例 -->
|
||
<view class="legend-bar">
|
||
<view class="legend-item">
|
||
<view class="legend-dot legend-dot-green" />
|
||
<text class="legend-label">已连接</text>
|
||
</view>
|
||
<view class="legend-item">
|
||
<view class="legend-dot legend-dot-red" />
|
||
<text class="legend-label">已断开</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载中 -->
|
||
<view v-if="loading" class="loading-box">
|
||
<text class="loading-text">loading...</text>
|
||
</view>
|
||
|
||
<!-- Frame 列表 -->
|
||
<view v-else class="frame-list">
|
||
<view class="frame-card" v-for="frame in frameList" :key="frame.id">
|
||
<text class="frame-name">{{ frame.name }}</text>
|
||
|
||
<!-- 光交箱类型:左右双栏布局 -->
|
||
<template v-if="rackType === 1">
|
||
<scroll-view class="port-scroll" scroll-x>
|
||
<view class="optical-box-wrapper">
|
||
<!-- 左栏:光交箱端子(粉色背景) -->
|
||
<view class="optical-left-col">
|
||
<view class="optical-col-header">
|
||
<text class="optical-col-title">光交箱端子</text>
|
||
</view>
|
||
<view
|
||
class="optical-port-row"
|
||
v-for="(row, rowIdx) in frame.odfPortsList"
|
||
:key="'left-' + rowIdx"
|
||
>
|
||
<text class="optical-row-name">{{ rowIdx + 1 }}</text>
|
||
<view class="port-list">
|
||
<view
|
||
class="port-item"
|
||
v-for="(port, portIdx) in row.rowList"
|
||
:key="'lp-' + port.id"
|
||
@click="openPortEdit(port)"
|
||
>
|
||
<view
|
||
class="port-circle"
|
||
:class="port.status === 1 ? 'port-green' : 'port-red'"
|
||
>
|
||
<text class="port-tips">{{ port.tips }}</text>
|
||
</view>
|
||
<text class="port-name">{{ getOpticalBoxPortName(rowIdx, portIdx) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 中间分隔线 -->
|
||
<view class="optical-divider" />
|
||
|
||
<!-- 右栏:ODF端子(浅蓝背景) -->
|
||
<view class="optical-right-col">
|
||
<view class="optical-col-header">
|
||
<text class="optical-col-title">ODF端子</text>
|
||
</view>
|
||
<view
|
||
class="optical-port-row"
|
||
v-for="(row, rowIdx) in frame.odfPortsList"
|
||
:key="'right-' + rowIdx"
|
||
>
|
||
<text class="optical-row-name">{{ rowIdx + 1 }}</text>
|
||
<view class="port-list">
|
||
<view
|
||
class="port-item"
|
||
v-for="(port, portIdx) in row.rowList"
|
||
:key="'rp-' + port.id"
|
||
@click="openPortEdit(port)"
|
||
>
|
||
<view
|
||
class="port-circle"
|
||
:class="port.status === 1 ? 'port-green' : 'port-red'"
|
||
>
|
||
<text class="port-tips">{{ port.tips }}</text>
|
||
</view>
|
||
<text class="port-name">{{ getOdfPortName(rowIdx, portIdx) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<!-- ODF 类型:保持现有布局不变 -->
|
||
<template v-else>
|
||
<scroll-view class="port-scroll" scroll-x>
|
||
<view class="port-rows-wrapper">
|
||
<view class="port-row" v-for="(row, rowIdx) in frame.odfPortsList" :key="rowIdx">
|
||
<text class="row-name">{{ row.name }}</text>
|
||
<view class="port-list">
|
||
<view
|
||
class="port-item"
|
||
v-for="port in row.rowList"
|
||
:key="port.id"
|
||
@click="openPortEdit(port)"
|
||
>
|
||
<view
|
||
class="port-circle"
|
||
:class="port.status === 1 ? 'port-green' : 'port-red'"
|
||
>
|
||
<text class="port-tips">{{ port.tips }}</text>
|
||
</view>
|
||
<text class="port-name">{{ port.name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 端口编辑弹窗 -->
|
||
<portEditDialog
|
||
:visible="showPortEdit"
|
||
:portId="currentPortId"
|
||
@close="showPortEdit = false"
|
||
@saved="onPortSaved"
|
||
/>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import { getRackDetail } from '@/services/machine'
|
||
import store from '@/store'
|
||
import portEditDialog from '@/components/port-edit-dialog.vue'
|
||
|
||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||
const rackId = ref('')
|
||
const rackName = ref('')
|
||
const roomName = ref('')
|
||
const rackType = ref(0)
|
||
const frameList = ref([])
|
||
const loading = ref(false)
|
||
const showPortEdit = ref(false)
|
||
const currentPortId = ref('')
|
||
let pendingPortId = ''
|
||
|
||
// 光交箱侧端子命名(左栏)
|
||
function getOpticalBoxPortName(rowIndex, portIndex) {
|
||
const rowLetter = String.fromCharCode(65 + rowIndex) // A=65, B=66...
|
||
return `${rowLetter}-${portIndex + 1}` // A-1, A-2, B-1, B-2...
|
||
}
|
||
|
||
// ODF侧端子命名(右栏)
|
||
function getOdfPortName(rowIndex, portIndex) {
|
||
return `${rowIndex + 1}-${portIndex + 1}` // 1-1, 1-2, 2-1, 2-2...
|
||
}
|
||
|
||
async function loadRackDetail() {
|
||
loading.value = true
|
||
try {
|
||
const res = await getRackDetail(rackId.value)
|
||
if (res.code === 200 && res.data) {
|
||
frameList.value = res.data
|
||
}
|
||
} finally {
|
||
loading.value = false
|
||
// 如果有待打开的端口(从搜索页跳转),加载完成后自动弹出
|
||
if (pendingPortId) {
|
||
currentPortId.value = pendingPortId
|
||
showPortEdit.value = true
|
||
pendingPortId = ''
|
||
}
|
||
}
|
||
}
|
||
|
||
function goBack() {
|
||
uni.navigateBack()
|
||
}
|
||
|
||
function openPortEdit(port) {
|
||
currentPortId.value = port.id
|
||
showPortEdit.value = true
|
||
}
|
||
|
||
function onPortSaved() {
|
||
showPortEdit.value = false
|
||
loadRackDetail()
|
||
}
|
||
|
||
onLoad((options) => {
|
||
if (options.rackId) {
|
||
rackId.value = options.rackId
|
||
}
|
||
if (options.rackName) {
|
||
rackName.value = decodeURIComponent(options.rackName)
|
||
}
|
||
if (options.roomName) {
|
||
roomName.value = decodeURIComponent(options.roomName)
|
||
}
|
||
if (options.rackType) {
|
||
rackType.value = parseInt(options.rackType)
|
||
}
|
||
if (options.portId) {
|
||
pendingPortId = options.portId
|
||
}
|
||
loadRackDetail()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.rack-detail-page {
|
||
position: relative;
|
||
min-height: 100vh;
|
||
background-color: #F5F5F5;
|
||
}
|
||
|
||
.bg-image {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 500rpx;
|
||
z-index: 0;
|
||
}
|
||
|
||
.content {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.nav-bar {
|
||
width: 100%;
|
||
}
|
||
|
||
.nav-bar-inner {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 88rpx;
|
||
padding: 0 24rpx;
|
||
}
|
||
|
||
.nav-icon {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
}
|
||
|
||
.nav-icon-placeholder {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
}
|
||
|
||
.nav-title {
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
}
|
||
|
||
.room-name-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 8rpx 24rpx 0;
|
||
}
|
||
|
||
.room-name-text {
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #1A73EC;
|
||
}
|
||
|
||
.rack-type-text {
|
||
font-size: 26rpx;
|
||
color: #fff;
|
||
}
|
||
|
||
.legend-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 16rpx 24rpx;
|
||
gap: 32rpx;
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.legend-dot {
|
||
width: 20rpx;
|
||
height: 20rpx;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.legend-dot-green {
|
||
background-color: #4CAF50;
|
||
}
|
||
|
||
.legend-dot-red {
|
||
background-color: #F44336;
|
||
}
|
||
|
||
.legend-label {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.loading-box {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 60rpx 0;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.frame-list {
|
||
padding: 0 24rpx 24rpx;
|
||
}
|
||
|
||
.frame-card {
|
||
background-color: #fff;
|
||
border-radius: 12rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 20rpx;
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.frame-name {
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
/* === ODF 类型:保持现有布局 === */
|
||
.port-scroll {
|
||
width: 100%;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.port-rows-wrapper {
|
||
display: inline-block;
|
||
min-width: 100%;
|
||
}
|
||
|
||
.port-row {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.row-name {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.port-list {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.port-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.port-circle {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.port-green {
|
||
background-color: #4CAF50;
|
||
}
|
||
|
||
.port-red {
|
||
background-color: #F44336;
|
||
}
|
||
|
||
.port-tips {
|
||
font-size: 20rpx;
|
||
color: #fff;
|
||
text-align: center;
|
||
}
|
||
|
||
.port-name {
|
||
font-size: 20rpx;
|
||
color: #333;
|
||
margin-top: 6rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
/* === 光交箱类型:双栏布局 === */
|
||
.optical-box-wrapper {
|
||
display: inline-flex;
|
||
flex-direction: row;
|
||
min-width: 100%;
|
||
}
|
||
|
||
.optical-left-col {
|
||
background-color: #FFC0CB;
|
||
padding: 16rpx;
|
||
border-radius: 8rpx 0 0 8rpx;
|
||
}
|
||
|
||
.optical-left-col .optical-port-row {
|
||
background-color: #FFB6C1;
|
||
border-radius: 8rpx;
|
||
padding: 12rpx;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.optical-right-col {
|
||
background-color: #E0F7FA;
|
||
padding: 16rpx;
|
||
border-radius: 0 8rpx 8rpx 0;
|
||
}
|
||
|
||
.optical-right-col .optical-port-row {
|
||
background-color: #B3E5FC;
|
||
border-radius: 8rpx;
|
||
padding: 12rpx;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.optical-divider {
|
||
width: 4rpx;
|
||
background-color: #999;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.optical-col-header {
|
||
padding: 0 0 12rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.optical-col-title {
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.optical-port-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.optical-row-name {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
font-weight: 600;
|
||
margin-right: 12rpx;
|
||
flex-shrink: 0;
|
||
width: 32rpx;
|
||
text-align: center;
|
||
}
|
||
</style>
|