<template>
|
<view class="structure-item-wrapper"
|
:class="{ 'is-root': level === 0, 'is-last': isLast }">
|
<!-- 树形连接线 (非根节点显示) -->
|
<template v-if="level > 0">
|
<view class="line-v"></view>
|
<view class="line-h"></view>
|
</template>
|
<view class="structure-item-card"
|
:class="{ 'has-children': hasChildren }">
|
<view class="card-main">
|
<view class="item-header"
|
@click="toggleExpand">
|
<view class="header-left">
|
<view v-if="hasChildren"
|
class="expand-icon"
|
:class="{ 'is-expanded': isExpanded }">
|
<up-icon name="arrow-right"
|
size="14"
|
color="#999"></up-icon>
|
</view>
|
<view v-else
|
class="dot-icon"></view>
|
<text class="item-title">{{ item.productName || '未选择产品' }}</text>
|
</view>
|
<up-tag v-if="hasChildren"
|
text="组合"
|
type="primary"
|
size="mini"
|
plain
|
shape="circle" />
|
</view>
|
<view class="item-body">
|
<view class="info-grid">
|
<view class="info-item">
|
<text class="label">规格型号:</text>
|
<text class="value">{{ item.model || '-' }}</text>
|
</view>
|
<view class="info-item">
|
<text class="label">消耗工序:</text>
|
<text class="value">{{ getProcessName(item.processId) }}</text>
|
</view>
|
<view class="info-item">
|
<text class="label">单位数量:</text>
|
<text class="value highlight">{{ item.unitQuantity || 0 }}</text>
|
</view>
|
<view class="info-item">
|
<text class="label">需求总量:</text>
|
<text class="value highlight">{{ item.demandedQuantity || 0 }}</text>
|
</view>
|
<view class="info-item">
|
<text class="label">单位:</text>
|
<text class="value">{{ item.unit || '-' }}</text>
|
</view>
|
<view class="info-item">
|
<text class="label">盘数:</text>
|
<text class="value">{{ item.diskQuantity || 0 }}</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
</view>
|
<!-- 递归展示子节点 -->
|
<view v-if="hasChildren && isExpanded"
|
class="children-container">
|
<BomStructureItem v-for="(child, index) in item.children"
|
:key="index"
|
:item="child"
|
:level="level + 1"
|
:isLast="index === item.children.length - 1"
|
:processOptions="processOptions" />
|
</view>
|
</view>
|
</template>
|
|
<script setup>
|
import { ref, computed, defineProps } from "vue";
|
|
const props = defineProps({
|
item: {
|
type: Object,
|
required: true,
|
},
|
level: {
|
type: Number,
|
default: 0,
|
},
|
isLast: {
|
type: Boolean,
|
default: false,
|
},
|
processOptions: {
|
type: Array,
|
default: () => [],
|
},
|
});
|
|
const isExpanded = ref(true);
|
const hasChildren = computed(
|
() => props.item.children && props.item.children.length > 0
|
);
|
|
const toggleExpand = () => {
|
if (hasChildren.value) {
|
isExpanded.value = !isExpanded.value;
|
}
|
};
|
|
const getProcessName = id => {
|
const process = props.processOptions.find(p => p.id === id);
|
return process ? process.name : "-";
|
};
|
</script>
|
|
<script>
|
export default {
|
name: "BomStructureItem",
|
};
|
</script>
|
|
<style scoped lang="scss">
|
.structure-item-wrapper {
|
position: relative;
|
padding-left: 44rpx;
|
|
&.is-root {
|
padding-left: 0;
|
}
|
}
|
|
// 垂直连接线段
|
.line-v {
|
position: absolute;
|
left: 18rpx; // 居中于 44rpx 的缩进内
|
top: -20rpx; // 向上延伸覆盖上一个节点的 margin-bottom
|
bottom: 0;
|
width: 2rpx;
|
background-color: #ddd;
|
z-index: 1;
|
}
|
|
// 最后一个节点的垂直线只延伸到水平线位置
|
.is-last > .line-v {
|
bottom: auto;
|
height: 60rpx; // 20rpx (top offset) + 40rpx (to horizontal line)
|
}
|
|
// 水平连接线
|
.line-h {
|
position: absolute;
|
left: 18rpx;
|
top: 40rpx; // 对齐到卡片内部图标中心 (padding 24 + icon 32/2)
|
width: 26rpx;
|
height: 2rpx;
|
background-color: #ddd;
|
z-index: 1;
|
}
|
|
.structure-item-card {
|
position: relative;
|
background: #fff;
|
border-radius: 16rpx;
|
margin-bottom: 20rpx;
|
padding: 24rpx;
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
border: 1rpx solid #f0f0f0;
|
transition: all 0.3s;
|
z-index: 2;
|
|
&:active {
|
background-color: #f9f9f9;
|
}
|
|
&.has-children {
|
border-left: 6rpx solid #3c9cff;
|
}
|
}
|
|
.card-main {
|
.item-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 20rpx;
|
|
.header-left {
|
display: flex;
|
align-items: center;
|
flex: 1;
|
|
.expand-icon {
|
margin-right: 12rpx;
|
transition: transform 0.3s;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 32rpx;
|
height: 32rpx;
|
|
&.is-expanded {
|
transform: rotate(90deg);
|
}
|
}
|
|
.dot-icon {
|
width: 12rpx;
|
height: 12rpx;
|
border-radius: 50%;
|
background-color: #ccc;
|
margin-right: 20rpx;
|
margin-left: 10rpx;
|
}
|
|
.item-title {
|
font-size: 30rpx;
|
font-weight: bold;
|
color: #333;
|
line-height: 1.4;
|
}
|
}
|
}
|
|
.item-body {
|
.info-grid {
|
display: grid;
|
grid-template-columns: 1fr 1fr;
|
gap: 12rpx 20rpx;
|
|
.info-item {
|
display: flex;
|
font-size: 24rpx;
|
line-height: 1.5;
|
|
.label {
|
color: #999;
|
white-space: nowrap;
|
}
|
|
.value {
|
color: #666;
|
word-break: break-all;
|
|
&.highlight {
|
color: #3c9cff;
|
font-weight: 500;
|
}
|
}
|
}
|
}
|
}
|
}
|
|
.children-container {
|
position: relative;
|
}
|
</style>
|