| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |