odf_new/odf-uniapp/pages/rack-detail/index.vue
zpc 5b0de386f1
All checks were successful
continuous-integration/drone/push Build is passing
feat(odf-v1021): Add optical box rack type support with dual-column layout
- 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
2026-04-02 16:40:09 +08:00

498 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>