feat(odf-v1021): Add optical box rack type support with dual-column layout
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
This commit is contained in:
zpc 2026-04-02 16:40:09 +08:00
parent 2b46490d54
commit 5b0de386f1
11 changed files with 314 additions and 46 deletions

View File

@ -3,39 +3,39 @@
## 任务列表
### Task 1: 数据库迁移脚本
- [ ] 创建 SQL 迁移脚本 `docs/v1.0.2.1/migration.sql`
- [ ] `odf_racks` 表新增 `rack_type`INT, DEFAULT 0
- [ ] `odf_racks` 表新增 `left_ports_count`INT, NULL
- [ ] `odf_racks` 表新增 `right_ports_count`INT, NULL
- [x] 创建 SQL 迁移脚本 `docs/v1.0.2.1/migration.sql`
- [x] `odf_racks` 表新增 `rack_type`INT, DEFAULT 0
- [x] `odf_racks` 表新增 `left_ports_count`INT, NULL
- [x] `odf_racks` 表新增 `right_ports_count`INT, NULL
### Task 2: 后端模型更新
- [ ] `server/ZR.Model/Business/OdfRacks.cs` 新增 `RackType`、`LeftPortsCount`、`RightPortsCount` 属性
- [ ] `server/ZR.Model/Business/Dto/OdfRacksDto.cs``OdfRacksDto``OdfRacksExpertDto` 新增对应属性
- [x] `server/ZR.Model/Business/OdfRacks.cs` 新增 `RackType`、`LeftPortsCount`、`RightPortsCount` 属性
- [x] `server/ZR.Model/Business/Dto/OdfRacksDto.cs``OdfRacksDto``OdfRacksExpertDto` 新增对应属性
### Task 3: 管理后台 - 机架表单增加类型选择
- [ ] `server/ZR.Vue/src/components/business/OdfRackForm.vue` 新增机架类型选择器
- [ ] 新增左侧端子数、右侧端子数输入框
- [ ] 表单验证规则更新
- [ ] 表单重置逻辑更新
- [x] `server/ZR.Vue/src/components/business/OdfRackForm.vue` 新增机架类型选择器
- [x] 新增左侧端子数、右侧端子数输入框
- [x] 表单验证规则更新
- [x] 表单重置逻辑更新
### Task 4: 管理后台 - 机架列表显示类型
- [ ] `server/ZR.Vue/src/views/business/OdfRacks.vue` 表格新增"机架类型"列
- [ ] 使用标签样式区分 ODF机架 / 光交箱
- [x] `server/ZR.Vue/src/views/business/OdfRacks.vue` 表格新增"机架类型"列
- [x] 使用标签样式区分 ODF机架 / 光交箱
### Task 5: uni-app 机架列表页显示类型
- [ ] `odf-uniapp/pages/rack/index.vue` 机架卡片新增类型标签
- [ ] 样式适配
- [x] `odf-uniapp/pages/rack/index.vue` 机架卡片新增类型标签
- [x] 样式适配
### Task 6: uni-app 机架详情页 - 类型显示与光交箱双侧布局
- [ ] `odf-uniapp/pages/rack-detail/index.vue` 显示机架类型
- [ ] 光交箱类型时实现左右双栏布局(左侧光交箱端子,右侧 ODF 端子)
- [ ] 左右区域不同背景色(左侧浅蓝 #E3F2FD,右侧浅橙 #FFF3E0
- [ ] 中间分隔线
- [ ] 光交箱端子命名:行按 A/B/C... 命名,端点为 A-1、A-2 格式
- [ ] ODF 类型保持现有布局不变
- [ ] 传递 rackType 参数到详情页
- [x] `odf-uniapp/pages/rack-detail/index.vue` 显示机架类型
- [x] 光交箱类型时实现左右双栏布局(左侧光交箱端子,右侧 ODF 端子)
- [x] 左右区域不同背景色(左侧浅蓝 #E3F2FD,右侧浅橙 #FFF3E0
- [x] 中间分隔线
- [x] 光交箱端子命名:行按 A/B/C... 命名,端点为 A-1、A-2 格式
- [x] ODF 类型保持现有布局不变
- [x] 传递 rackType 参数到详情页
### Task 7: 导入导出功能适配
- [ ] `server/ZR.Model/Business/Dto/OdfPortsDto.cs` 导入/导出 DTO 新增机架类型相关字段
- [ ] 导入逻辑适配光交箱端子命名格式A-1、B-2 等)
- [ ] 导出逻辑适配光交箱端子命名格式
- [x] `server/ZR.Model/Business/Dto/OdfPortsDto.cs` 导入/导出 DTO 新增机架类型相关字段
- [x] 导入逻辑适配光交箱端子命名格式A-1、B-2 等)
- [x] 导出逻辑适配光交箱端子命名格式

View File

@ -0,0 +1,6 @@
-- ODF v1.0.2.1 数据库迁移脚本
-- 新增光交箱机架类型支持
-- SQL Server 语法
ALTER TABLE odf_racks ADD rack_type INT NOT NULL DEFAULT 0;
-- rack_type: 机架类型0=ODF机架, 1=光交箱

View File

@ -17,9 +17,10 @@
</view>
</view>
<!-- 机房名称 -->
<!-- 机房名称 + 机架类型 -->
<view class="room-name-bar">
<text class="room-name-text">{{ roomName }}</text>
<text class="rack-type-text">类型{{ rackType === 1 ? '光交箱' : 'ODF' }}</text>
</view>
<!-- 状态图例 -->
@ -44,30 +45,102 @@
<view class="frame-card" v-for="frame in frameList" :key="frame.id">
<text class="frame-name">{{ frame.name }}</text>
<!-- 端口行整框一起横向滚动 -->
<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">
<!-- 光交箱类型左右双栏布局 -->
<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="port-item"
v-for="port in row.rowList"
:key="port.id"
@click="openPortEdit(port)"
class="optical-port-row"
v-for="(row, rowIdx) in frame.odfPortsList"
:key="'left-' + rowIdx"
>
<view
class="port-circle"
:class="port.status === 1 ? 'port-green' : 'port-red'"
>
<text class="port-tips">{{ port.tips }}</text>
<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>
<text class="port-name">{{ port.name }}</text>
</view>
</view>
</view>
</view>
</scroll-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>
@ -93,12 +166,24 @@ 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 {
@ -141,6 +226,9 @@ onLoad((options) => {
if (options.roomName) {
roomName.value = decodeURIComponent(options.roomName)
}
if (options.rackType) {
rackType.value = parseInt(options.rackType)
}
if (options.portId) {
pendingPortId = options.portId
}
@ -198,6 +286,9 @@ onLoad((options) => {
}
.room-name-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8rpx 24rpx 0;
}
@ -207,6 +298,11 @@ onLoad((options) => {
color: #1A73EC;
}
.rack-type-text {
font-size: 26rpx;
color: #fff;
}
.legend-bar {
display: flex;
align-items: center;
@ -269,6 +365,7 @@ onLoad((options) => {
margin-bottom: 16rpx;
}
/* === ODF 类型:保持现有布局 === */
.port-scroll {
width: 100%;
white-space: nowrap;
@ -331,4 +428,70 @@ onLoad((options) => {
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>

View File

@ -29,11 +29,13 @@
<view class="rack-list">
<view
class="rack-card"
:class="{ 'rack-card-optical': item.rackType === 1 }"
v-for="item in rackList"
:key="item.id"
@click="goDetail(item)"
>
<text class="rack-name">{{ item.rackName }}</text>
<text class="rack-type">类型{{ item.rackType === 1 ? '光交箱' : 'ODF' }}</text>
</view>
</view>
</view>
@ -90,7 +92,10 @@ function goSearch() {
function goDetail(item) {
uni.navigateTo({
url: '/pages/rack-detail/index?rackId=' + item.id + '&rackName=' + encodeURIComponent(item.rackName) + '&roomName=' + encodeURIComponent(roomName.value)
url: '/pages/rack-detail/index?rackId=' + item.id
+ '&rackName=' + encodeURIComponent(item.rackName)
+ '&roomName=' + encodeURIComponent(roomName.value)
+ '&rackType=' + (item.rackType || 0)
})
}
@ -201,6 +206,7 @@ onReachBottom(() => {
.rack-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 24rpx;
margin-bottom: 20rpx;
background-color: #fff;
@ -208,9 +214,18 @@ onReachBottom(() => {
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.rack-card-optical {
border-left: 6rpx solid #1A73EC;
}
.rack-name {
font-size: 30rpx;
font-weight: 500;
color: #333;
}
.rack-type {
font-size: 26rpx;
color: #666;
}
</style>

View File

@ -631,6 +631,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
DeptId = dept.DeptId,
FrameCount = 0,
SequenceNumber = sequenceNumber,
RackType = excelItem.RackType,
};
await _OdfRacksService.InsertReturnEntityAsync(rack);
addRackCount++;
@ -658,6 +659,18 @@ namespace ZR.Admin.WebApi.Controllers.Business
}
//添加端口
var port = _OdfPortsService.AsQueryable().Where(it => it.FrameId == frame.Id && it.RowNumber == excelItem.RowNumber && it.PortNumber == excelItem.PortNumber).First();
// 生成端口名称:光交箱用 A-1 格式ODF用 1-1 格式
string portName;
if (rack.RackType == 1)
{
// 光交箱:行号转字母 (1=A, 2=B, 3=C...)
string rowLetter = ((char)(64 + excelItem.RowNumber)).ToString();
portName = $"{rowLetter}-{excelItem.PortNumber}";
}
else
{
portName = excelItem.RowNumber + "-" + excelItem.PortNumber;
}
string remarks = "";
if (!string.IsNullOrEmpty(excelItem.Remarks))
{
@ -694,7 +707,7 @@ namespace ZR.Admin.WebApi.Controllers.Business
RackName = rack.RackName,
RoomId = room.Id,
RoomName = room.RoomName,
Name = excelItem.RowNumber + "-" + excelItem.PortNumber,
Name = portName,
RowNumber = excelItem.RowNumber,
PortNumber = excelItem.PortNumber,
Status = excelItem.Status,

View File

@ -149,6 +149,13 @@ namespace ZR.Model.Business.Dto
[ExcelColumnName("机架名称")]
public string RackName { get; set; }
/// <summary>
/// 机架类型0=ODF机架, 1=光交箱
/// </summary>
[ExcelColumn(Name = "机架类型")]
[ExcelColumnName("机架类型")]
public int RackType { get; set; }
[Required(ErrorMessage = "框ID不能为空")]
[ExcelColumn(Name = "框ID")]
[ExcelColumnName("框ID")]
@ -279,6 +286,13 @@ namespace ZR.Model.Business.Dto
[ExcelColumnName("机架名称")]
public string RackName { get; set; }
/// <summary>
/// 机架类型0=ODF机架, 1=光交箱
/// </summary>
[ExcelColumn(Name = "机架类型")]
[ExcelColumnName("机架类型")]
public int RackType { get; set; }
[Required(ErrorMessage = "框ID不能为空")]
[ExcelColumn(Name = "框ID")]
[ExcelColumnName("框ID")]
@ -394,6 +408,13 @@ namespace ZR.Model.Business.Dto
[ExcelColumnName("机架名称")]
public string RackName { get; set; }
/// <summary>
/// 机架类型0=ODF机架, 1=光交箱
/// </summary>
[ExcelColumn(Name = "机架类型")]
[ExcelColumnName("机架类型")]
public int RackType { get; set; }
[Required(ErrorMessage = "机框名称不能为空")]
@ -660,6 +681,13 @@ namespace ZR.Model.Business.Dto
[ExcelColumnName("机架名称")]
public string RackName { get; set; }
/// <summary>
/// 机架类型0=ODF机架, 1=光交箱
/// </summary>
[ExcelColumn(Name = "机架类型", Width = 12)]
[ExcelColumnName("机架类型")]
public int RackType { get; set; }
[ExcelColumn(Name = "机框名称", Width = 15)]
[ExcelColumnName("机框名称")]
public string FrameName { get; set; }

View File

@ -48,6 +48,9 @@ namespace ZR.Model.Business.Dto
[ExcelColumnName("UpdatedAt")]
public DateTime? UpdatedAt { get; set; }
[ExcelColumn(Name = "机架类型")]
[ExcelColumnName("机架类型")]
public int RackType { get; set; }
}

View File

@ -47,5 +47,11 @@ namespace ZR.Model.Business
/// 部门
/// </summary>
public long DeptId { get; set; }
/// <summary>
/// 机架类型0=ODF机架, 1=光交箱
/// </summary>
public int RackType { get; set; }
}
}

View File

@ -208,9 +208,24 @@ namespace ZR.Service.Business
})
.ToPage(parm);
// 查询所有相关机架的类型信息,用于填充 RackType
var rackIds = response.Result.Select(r => r.RackName).Distinct().ToList();
var racks = Context.Queryable<OdfRacks>()
.Where(r => rackIds.Contains(r.RackName))
.Select(r => new { r.Id, r.RackName, r.RackType })
.ToList();
var rackTypeDict = racks.GroupBy(r => r.RackName)
.ToDictionary(g => g.Key, g => g.First().RackType);
// 反向解析 Remarks 字段,拆分出 业务名称、1号端口、2号端口、3号端口
foreach (var item in response.Result)
{
// 填充机架类型
if (rackTypeDict.TryGetValue(item.RackName, out var rackType))
{
item.RackType = rackType;
}
ParseRemarksToFields(item);
}

View File

@ -42,6 +42,15 @@
</el-form-item>
</el-col>
<el-col :lg="12">
<el-form-item label="机架类型" prop="rackType">
<el-select v-model="form.rackType" :disabled="isView" placeholder="请选择机架类型">
<el-option label="ODF机架" :value="0"></el-option>
<el-option label="光交箱" :value="1"></el-option>
</el-select>
</el-form-item>
</el-col>
<!-- 查看模式下显示时间信息 -->
<template v-if="isView && isEdit">
<el-col :lg="12">
@ -114,6 +123,7 @@ const form = ref({
sequenceNumber: null,
rackName: null,
frameCount: 9, // 9
rackType: 0, // 0=ODF, 1=
createdAt: null,
updatedAt: null
})
@ -122,7 +132,8 @@ const form = ref({
const rules = {
roomId: [{ required: true, message: '机房不能为空', trigger: 'change', type: 'number' }],
sequenceNumber: [{ required: true, message: '序号不能为空', trigger: 'blur', type: 'number' }],
rackName: [{ required: true, message: 'ODF名称不能为空', trigger: 'blur' }]
rackName: [{ required: true, message: 'ODF名称不能为空', trigger: 'blur' }],
rackType: [{ required: true, message: '机架类型不能为空', trigger: 'change', type: 'number' }]
}
//
@ -146,6 +157,7 @@ function resetForm() {
sequenceNumber: null,
rackName: null,
frameCount: 9, // 9
rackType: 0, // ODF
createdAt: null,
updatedAt: null
}

View File

@ -61,6 +61,12 @@
</el-table-column>
<el-table-column prop="sequenceNumber" label="序号" align="center" v-if="columns.showColumn('sequenceNumber')" />
<el-table-column prop="rackName" label="ODF名称" align="center" :show-overflow-tooltip="true" v-if="columns.showColumn('rackName')" />
<el-table-column prop="rackType" label="机架类型" align="center" v-if="columns.showColumn('rackType')">
<template #default="scope">
<el-tag v-if="scope.row.rackType === 1" type="success">光交箱</el-tag>
<el-tag v-else type="info">ODF机架</el-tag>
</template>
</el-table-column>
<el-table-column prop="frameCount" label="框数量固定9框" align="center" v-if="columns.showColumn('frameCount')" />
<el-table-column prop="createdAt" label="创建时间" :show-overflow-tooltip="true" v-if="columns.showColumn('createdAt')" />
<el-table-column prop="updatedAt" label="修改时间" :show-overflow-tooltip="true" v-if="columns.showColumn('updatedAt')" />
@ -118,6 +124,7 @@ const columns = ref([
{ visible: true, align: 'center', type: 'dict', prop: 'roomId', label: '机房', dictType: 'sql_odf_room' },
{ visible: true, align: 'center', type: '', prop: 'sequenceNumber', label: '序号' },
{ visible: true, align: 'center', type: '', prop: 'rackName', label: 'ODF名称', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'rackType', label: '机架类型' },
{ visible: true, align: 'center', type: '', prop: 'frameCount', label: '框数量固定9框' },
{ visible: true, align: 'center', type: '', prop: 'createdAt', label: '创建时间', showOverflowTooltip: true },
{ visible: true, align: 'center', type: '', prop: 'updatedAt', label: '修改时间', showOverflowTooltip: true }