2026-02-06 10:16:37 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="app-container">
|
2026-02-11 09:53:46 +08:00
|
|
|
|
<el-card shadow="always" class="no-print-content">
|
2026-02-06 10:16:37 +08:00
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<div class="header-left">
|
2026-02-12 09:57:12 +08:00
|
|
|
|
<span class="title">出库拣货车</span>
|
|
|
|
|
|
<span class="subtitle">(请添加需要出库的物品)</span>
|
2026-02-06 10:16:37 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-02-12 09:57:12 +08:00
|
|
|
|
<el-button type="primary" :icon="Plus" @click="openManualSelect">
|
|
|
|
|
|
手动添加库存
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button type="warning" :icon="List" @click="openBomSelect">
|
|
|
|
|
|
按 BOM 套餐添加
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-divider direction="vertical" />
|
|
|
|
|
|
<el-button type="success" :icon="Printer" :disabled="selectedItems.length === 0" @click="handlePreview">
|
|
|
|
|
|
生成预览 & 打印
|
2026-02-06 10:16:37 +08:00
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-02-12 09:57:12 +08:00
|
|
|
|
<el-alert
|
|
|
|
|
|
v-if="selectedItems.length === 0"
|
|
|
|
|
|
title="清单为空,请点击右上角按钮添加物品"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
center
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
style="margin-bottom: 20px"
|
2026-02-06 10:16:37 +08:00
|
|
|
|
/>
|
|
|
|
|
|
|
2026-02-12 09:57:12 +08:00
|
|
|
|
<el-table
|
|
|
|
|
|
v-else
|
|
|
|
|
|
:data="selectedItems"
|
|
|
|
|
|
border
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
row-key="uniqueKey"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column type="index" label="序号" width="50" align="center" />
|
|
|
|
|
|
<el-table-column prop="typeLabel" label="类型" width="80" />
|
|
|
|
|
|
<el-table-column prop="name" label="名称" />
|
|
|
|
|
|
<el-table-column prop="standard" label="规格" />
|
|
|
|
|
|
<el-table-column prop="available_quantity" label="当前库存" width="100" />
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column label="本次出库" width="180" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input-number
|
|
|
|
|
|
v-model="row.export_quantity"
|
|
|
|
|
|
:min="1"
|
|
|
|
|
|
:max="row.available_quantity"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table-column label="操作" width="80" align="center">
|
|
|
|
|
|
<template #default="{ $index }">
|
|
|
|
|
|
<el-button type="danger" link @click="removeRow($index)">移除</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
2026-02-06 10:16:37 +08:00
|
|
|
|
</el-card>
|
|
|
|
|
|
|
2026-02-12 09:57:12 +08:00
|
|
|
|
<el-dialog v-model="manualDialogVisible" title="选择库存物品" width="80%" top="5vh">
|
|
|
|
|
|
<div class="filter-container">
|
|
|
|
|
|
<el-input v-model="searchKeyword" placeholder="搜索名称/规格" style="width: 300px" @input="filterStock" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
:data="filteredStockData"
|
|
|
|
|
|
height="500"
|
|
|
|
|
|
border
|
|
|
|
|
|
@selection-change="handleStockSelection"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column type="selection" width="55" />
|
2026-02-06 10:16:37 +08:00
|
|
|
|
<el-table-column prop="name" label="名称" />
|
|
|
|
|
|
<el-table-column prop="standard" label="规格" />
|
2026-02-12 09:57:12 +08:00
|
|
|
|
<el-table-column prop="available_quantity" label="可用库存" />
|
2026-02-12 10:00:17 +08:00
|
|
|
|
<el-table-column label="本次出库" width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input-number
|
|
|
|
|
|
v-model="row.export_quantity"
|
|
|
|
|
|
:min="1"
|
|
|
|
|
|
:max="row.available_quantity"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-02-12 09:57:12 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="manualDialogVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="confirmManualAdd">确认添加</el-button>
|
|
|
|
|
|
</template>
|
2026-02-06 10:16:37 +08:00
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
2026-02-12 09:57:12 +08:00
|
|
|
|
<el-dialog v-model="bomSelectVisible" title="选择 BOM 套餐" width="600px">
|
|
|
|
|
|
<el-form label-width="80px">
|
|
|
|
|
|
<el-form-item label="选择产品">
|
|
|
|
|
|
<el-select v-model="selectedBomNo" filterable placeholder="选择要出库的产品BOM" style="width: 100%">
|
|
|
|
|
|
<el-option
|
|
|
|
|
|
v-for="b in bomOptions"
|
|
|
|
|
|
:key="b.bom_no"
|
|
|
|
|
|
:label="`${b.parent_name} (${b.version})`"
|
|
|
|
|
|
:value="b.bom_no"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="生产套数">
|
|
|
|
|
|
<el-input-number v-model="bomSets" :min="1" label="套" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="bomSelectVisible = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="confirmBomAdd">一键添加</el-button>
|
|
|
|
|
|
</template>
|
2026-02-09 15:19:19 +08:00
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
2026-02-12 09:57:12 +08:00
|
|
|
|
<el-dialog v-model="previewVisible" title="打印预览" width="800px">
|
|
|
|
|
|
<el-button @click="window.print()">确认打印</el-button>
|
|
|
|
|
|
</el-dialog>
|
2026-02-06 10:16:37 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-02-12 09:57:12 +08:00
|
|
|
|
import { ref } from 'vue'
|
|
|
|
|
|
import { Plus, List, Printer } from '@element-plus/icons-vue'
|
2026-02-11 09:53:46 +08:00
|
|
|
|
import { ElMessage } from 'element-plus'
|
2026-02-12 09:57:12 +08:00
|
|
|
|
import { getAllStock } from '@/api/inbound/stock'
|
|
|
|
|
|
import { getBomList, getBomDetail } from '@/api/bom'
|
2026-02-06 10:16:37 +08:00
|
|
|
|
|
2026-02-12 09:57:12 +08:00
|
|
|
|
// 购物车数据
|
|
|
|
|
|
const selectedItems = ref<any[]>([])
|
2026-02-06 10:16:37 +08:00
|
|
|
|
|
2026-02-12 09:57:12 +08:00
|
|
|
|
// 手动选择相关
|
|
|
|
|
|
const manualDialogVisible = ref(false)
|
|
|
|
|
|
const allStockData = ref<any[]>([])
|
|
|
|
|
|
const filteredStockData = ref<any[]>([])
|
2026-02-11 09:53:46 +08:00
|
|
|
|
const searchKeyword = ref('')
|
2026-02-12 09:57:12 +08:00
|
|
|
|
const tempSelection = ref<any[]>([])
|
2026-02-06 10:16:37 +08:00
|
|
|
|
|
2026-02-09 15:19:19 +08:00
|
|
|
|
// BOM 相关
|
2026-02-12 09:57:12 +08:00
|
|
|
|
const bomSelectVisible = ref(false)
|
|
|
|
|
|
const bomOptions = ref<any[]>([])
|
|
|
|
|
|
const selectedBomNo = ref('')
|
|
|
|
|
|
const bomSets = ref(1)
|
2026-02-09 15:50:56 +08:00
|
|
|
|
|
2026-02-12 09:57:12 +08:00
|
|
|
|
const previewVisible = ref(false)
|
2026-02-09 15:19:19 +08:00
|
|
|
|
|
2026-02-12 09:57:12 +08:00
|
|
|
|
// --- 方法 ---
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 打开手动选择
|
|
|
|
|
|
const openManualSelect = async () => {
|
|
|
|
|
|
manualDialogVisible.value = true
|
|
|
|
|
|
if (allStockData.value.length === 0) {
|
|
|
|
|
|
const res:any = await getAllStock()
|
|
|
|
|
|
// 简单处理数据,实际应复用之前的 normalizeItem 逻辑
|
|
|
|
|
|
const list = res.materials ? [...res.materials, ...res.products, ...res.semis] : []
|
|
|
|
|
|
allStockData.value = list.map((i:any) => ({
|
|
|
|
|
|
...i,
|
|
|
|
|
|
name: i.name || i.material_name || i.product_name,
|
|
|
|
|
|
standard: i.standard || i.spec_model,
|
2026-02-12 10:00:17 +08:00
|
|
|
|
uniqueKey: i.id + '_' + i.type,
|
|
|
|
|
|
export_quantity: 1
|
2026-02-12 09:57:12 +08:00
|
|
|
|
}))
|
|
|
|
|
|
filteredStockData.value = allStockData.value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const filterStock = () => {
|
|
|
|
|
|
const kw = searchKeyword.value.toLowerCase()
|
|
|
|
|
|
filteredStockData.value = allStockData.value.filter(i =>
|
|
|
|
|
|
(i.name && i.name.toLowerCase().includes(kw)) || (i.standard && i.standard.toLowerCase().includes(kw))
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleStockSelection = (val: any[]) => { tempSelection.value = val }
|
|
|
|
|
|
|
|
|
|
|
|
const confirmManualAdd = () => {
|
|
|
|
|
|
const newItems = tempSelection.value.filter(item =>
|
|
|
|
|
|
!selectedItems.value.find(existing => existing.uniqueKey === item.uniqueKey)
|
|
|
|
|
|
)
|
|
|
|
|
|
selectedItems.value.push(...newItems)
|
|
|
|
|
|
manualDialogVisible.value = false
|
|
|
|
|
|
tempSelection.value = []
|
|
|
|
|
|
ElMessage.success(`添加了 ${newItems.length} 项`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 打开 BOM 选择
|
|
|
|
|
|
const openBomSelect = async () => {
|
|
|
|
|
|
bomSelectVisible.value = true
|
|
|
|
|
|
const res = await getBomList()
|
|
|
|
|
|
bomOptions.value = res.data || []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const confirmBomAdd = async () => {
|
|
|
|
|
|
if(!selectedBomNo.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
const detailRes = await getBomDetail(selectedBomNo.value)
|
|
|
|
|
|
const bomRows = detailRes.data || []
|
|
|
|
|
|
|
|
|
|
|
|
if (allStockData.value.length === 0) {
|
|
|
|
|
|
// 确保有库存数据用于匹配
|
|
|
|
|
|
await openManualSelect()
|
|
|
|
|
|
manualDialogVisible.value = false // 仅加载数据,不显示弹窗
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let addedCount = 0;
|
|
|
|
|
|
bomRows.forEach((bomItem:any) => {
|
|
|
|
|
|
const needQty = bomItem.dosage * bomSets.value
|
|
|
|
|
|
// 简单匹配逻辑:通过名称或ID匹配
|
|
|
|
|
|
const stockCandidate = allStockData.value.find(s =>
|
|
|
|
|
|
(s.base_id == bomItem.child_id)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (stockCandidate) {
|
|
|
|
|
|
const existing = selectedItems.value.find(e => e.uniqueKey === stockCandidate.uniqueKey)
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
|
existing.export_quantity += needQty
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const newItem = JSON.parse(JSON.stringify(stockCandidate))
|
|
|
|
|
|
newItem.export_quantity = needQty
|
|
|
|
|
|
selectedItems.value.push(newItem)
|
|
|
|
|
|
}
|
|
|
|
|
|
addedCount++
|
|
|
|
|
|
}
|
2026-02-06 10:16:37 +08:00
|
|
|
|
})
|
2026-02-12 09:57:12 +08:00
|
|
|
|
|
|
|
|
|
|
if(addedCount > 0) {
|
|
|
|
|
|
ElMessage.success(`已添加 BOM 相关物料`)
|
|
|
|
|
|
bomSelectVisible.value = false
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.warning('未在库存中找到匹配的物料')
|
2026-02-11 09:53:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 09:57:12 +08:00
|
|
|
|
const removeRow = (index: number) => {
|
|
|
|
|
|
selectedItems.value.splice(index, 1)
|
2026-02-11 09:53:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 10:16:37 +08:00
|
|
|
|
const handlePreview = () => {
|
2026-02-12 09:57:12 +08:00
|
|
|
|
previewVisible.value = true
|
|
|
|
|
|
setTimeout(() => window.print(), 500)
|
2026-02-09 15:50:56 +08:00
|
|
|
|
}
|
2026-02-06 10:16:37 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-02-11 09:53:46 +08:00
|
|
|
|
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
|
|
|
|
|
.header-left .title { font-size: 18px; font-weight: bold; margin-right: 10px; }
|
|
|
|
|
|
.header-left .subtitle { font-size: 12px; color: #909399; }
|
2026-02-12 09:57:12 +08:00
|
|
|
|
.filter-container { margin-bottom: 10px; }
|
|
|
|
|
|
</style>
|