<template>
|
<div class="material-node">
|
<!-- 当前节点卡片 -->
|
<div :class="['node-card', isRoot ? 'root-card' : (row.nodeType === 'semiFinished' ? 'semi-finished-card' : 'child-card')]">
|
<div class="node-header">
|
<div class="node-label">
|
<el-tag :type="isRoot ? '' : (row.nodeType === 'semiFinished' ? 'warning' : 'success')" size="small" effect="dark">
|
{{ isRoot ? '成品' : (row.nodeType === 'semiFinished' ? '半成品' : '原料') }}
|
</el-tag>
|
<span class="node-title">{{ row.productName || '未选择产品' }}</span>
|
<span v-if="row.model" class="node-sub">规格: {{ row.model }}</span>
|
<span v-if="row.unit" class="node-sub">单位: {{ row.unit }}</span>
|
</div>
|
<div class="node-actions">
|
<template v-if="editable && (isRoot || row.nodeType === 'semiFinished')">
|
<el-button type="primary"
|
text
|
size="small"
|
@click="handleAdd('semiFinished')">
|
+ 添加半成品
|
</el-button>
|
<el-button type="primary"
|
text
|
size="small"
|
@click="handleAdd('rawMaterial')">
|
+ 添加原料
|
</el-button>
|
</template>
|
<el-button v-if="editable"
|
type="danger"
|
text
|
size="small"
|
@click="$emit('remove', row.tempId)">
|
删除
|
</el-button>
|
</div>
|
</div>
|
|
<!-- 编辑模式下的表单 -->
|
<div v-if="editable" class="node-body">
|
<el-row :gutter="12">
|
<el-col :span="7">
|
<el-form-item label="产品" :rules="[{ required: true, message: '请选择产品' }]" style="margin:0">
|
<el-input :model-value="row.productName || ''"
|
readonly
|
placeholder="点击选择产品"
|
@click="openSelect"
|
style="width:100%">
|
<template #suffix>
|
<el-icon><component :is="SearchIcon" /></el-icon>
|
</template>
|
</el-input>
|
</el-form-item>
|
</el-col>
|
<el-col :span="5">
|
<el-form-item label="规格" style="margin:0">
|
<el-select v-model="row.model"
|
placeholder="请选择规格"
|
clearable
|
style="width:100%"
|
@visible-change="(v:boolean) => { if (v) openSelect() }">
|
<el-option v-if="row.model" :label="row.model" :value="row.model" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
<el-col v-if="!isRoot" :span="5">
|
<el-form-item label="工序" :rules="[{ required: true, message: '请选择工序' }]" style="margin:0">
|
<el-select v-model="row.processId"
|
placeholder="请选择"
|
filterable
|
clearable
|
style="width:100%"
|
@change="(v:any) => $emit('processChange', row, v)">
|
<el-option v-for="item in processOptions"
|
:key="item.id"
|
:label="item.name"
|
:value="item.id" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
<el-col :span="4">
|
<el-form-item label="数量" :rules="[{ required: true, message: '请填写数量' }]" style="margin:0">
|
<el-input-number v-model="row.unitQuantity"
|
:min="0"
|
:precision="2"
|
:step="1"
|
controls-position="right"
|
style="width:100%"
|
@change="$emit('quantityChange')" />
|
</el-form-item>
|
</el-col>
|
<el-col :span="3">
|
<el-form-item label="单位" style="margin:0">
|
<el-input v-model="row.unit" placeholder="单位" clearable style="width:100%" />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</div>
|
|
<!-- 非编辑模式:简洁显示 -->
|
<div v-else class="node-view">
|
<span v-if="!isRoot && row.processName">工序: {{ row.processName }} | </span>
|
<span>数量: {{ row.unitQuantity || '-' }}</span>
|
</div>
|
</div>
|
|
<!-- 递归渲染子节点 -->
|
<div v-if="row.children && row.children.length > 0" class="node-children">
|
<MaterialCard
|
v-for="child in row.children"
|
:key="child.tempId"
|
:row="child"
|
:depth="depth + 1"
|
:editable="editable"
|
:process-options="processOptions"
|
@remove="(id:string) => $emit('remove', id)"
|
@add="(id:string, nodeType:string) => $emit('add', id, nodeType)"
|
@select-product="(tempId: string, data: any) => $emit('selectProduct', tempId, data)"
|
@process-change="(row: any, v: any) => $emit('processChange', row, v)"
|
@quantity-change="$emit('quantityChange')"
|
/>
|
</div>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
import { computed } from 'vue'
|
import { Search } from '@element-plus/icons-vue'
|
|
const SearchIcon = Search
|
|
const props = defineProps<{
|
row: any
|
depth: number
|
editable: boolean
|
processOptions: any[]
|
}>()
|
|
const emit = defineEmits<{
|
remove: [tempId: string]
|
add: [tempId: string, nodeType: string]
|
selectProduct: [tempId: string, data: any]
|
processChange: [row: any, value: any]
|
quantityChange: []
|
}>()
|
|
const isRoot = computed(() => props.depth === 0)
|
|
const openSelect = () => {
|
emit('selectProduct', props.row.tempId, null)
|
}
|
|
const handleAdd = (nodeType: string) => {
|
emit('add', props.row.tempId, nodeType)
|
}
|
</script>
|
|
<script lang="ts">
|
export default { name: 'MaterialCard' }
|
</script>
|
|
<style scoped>
|
.material-node {
|
margin: 4px 0;
|
}
|
|
.node-card {
|
border: 1px solid #e4e7ed;
|
border-radius: 8px;
|
overflow: hidden;
|
}
|
|
.root-card {
|
border-color: #409eff;
|
border-left: 4px solid #409eff;
|
background-color: #f0f5ff;
|
}
|
|
.child-card {
|
border-left: 4px solid #67c23a;
|
background-color: #f0f9eb;
|
}
|
|
.semi-finished-card {
|
border-left: 4px solid #e6a23c;
|
background-color: #fdf6ec;
|
}
|
|
.node-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 8px 12px;
|
background-color: rgba(0,0,0,0.03);
|
flex-wrap: wrap;
|
gap: 4px;
|
}
|
|
.node-label {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
flex-wrap: wrap;
|
}
|
|
.node-title {
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.node-sub {
|
font-size: 12px;
|
color: #909399;
|
}
|
|
.node-actions {
|
display: flex;
|
gap: 4px;
|
}
|
|
.node-body {
|
padding: 10px 12px;
|
}
|
|
.node-view {
|
padding: 6px 12px;
|
font-size: 13px;
|
color: #606266;
|
}
|
|
.node-children {
|
margin-left: 36px;
|
padding-left: 16px;
|
border-left: 2px dashed #dcdfe6;
|
}
|
</style>
|