feat: P0-4 按库存优化+切割准备清单(缺料告警)

This commit is contained in:
HHY
2026-09-25 03:16:09 -07:00
parent 87f9869829
commit cc5b3de6b2
4 changed files with 143 additions and 24 deletions
+57 -7
View File
@@ -1,18 +1,51 @@
<template>
<el-card>
<div class="toolbar">
<div class="toolbar no-print">
<el-select v-model="contractId" filterable placeholder="选择合同" style="width:300px">
<el-option v-for="c in contracts" :key="c.id" :label="`${c.contract_no} ${c.customer_name || ''}`" :value="c.id" />
</el-select>
<span>锯口</span>
<el-input-number v-model="kerf" :min="0" :max="50" size="small" controls-position="right" style="width:90px" />
<el-divider direction="vertical" />
<el-checkbox v-model="useStock">使用仓库库存原料</el-checkbox>
<template v-if="!useStock">
<span>标准原料长度</span>
<el-input-number v-model="stockLength" :min="1000" :max="12000" :step="500" size="small"
controls-position="right" style="width:110px" />
</template>
<el-button type="primary" :loading="loading" :disabled="!contractId" @click="run">生成下料方案</el-button>
<el-button v-if="result" type="success" plain @click="print">打印方案</el-button>
</div>
<template v-if="result">
<el-alert type="success" :closable="false" show-icon
:title="`合同 ${contractNo} · ${result.order_count} 个订单 · 利用率 ${(result.stats.utilization * 100).toFixed(1)}% · 用料 ${result.stats.stock_bars_used} 根`"
<el-alert type="success" :closable="false" show-icon class="no-print"
:title="`合同 ${contractNo} · ${result.order_count} 个订单 · 利用率 ${(result.stats.utilization * 100).toFixed(1)}% · 用料 ${result.stats.stock_bars_used} 根${result.use_stock ? ' · 按仓库库存' : ''}`"
style="margin-bottom:12px" />
<!-- 切割准备/领料清单 -->
<h4 class="sec-title">切割准备(领料单)<span id="print-title" v-if="false"></span></h4>
<el-table :data="result.preparation" border size="small">
<el-table-column label="原料" min-width="160">
<template #default="{ row }">{{ row.stock_label }}({{ row.stock_length }}mm)</template>
</el-table-column>
<el-table-column prop="bars" label="领料根数" width="100" />
<el-table-column prop="pieces" label="切割件数" width="100" />
<el-table-column label="合计余料(mm)" width="130">
<template #default="{ row }">{{ row.waste }}(均 {{ Math.round(row.waste / row.bars) }})</template>
</el-table-column>
</el-table>
<!-- 缺料清单(按库存模式) -->
<template v-if="result.use_stock && result.shortage?.length">
<h4 class="sec-title" style="color:#e6a23c">缺料清单(需求超出库存,需补库)</h4>
<el-table :data="result.shortage" border size="small" :row-style="{ background: '#fdf6ec' }">
<el-table-column prop="label" label="缺料用途" min-width="140" />
<el-table-column prop="length" label="单件长度(mm)" width="120" />
<el-table-column prop="qty" label="缺口(件)" width="100" />
</el-table>
</template>
<h4 class="sec-title">切割方案明细</h4>
<el-table :data="result.plans" border size="small">
<el-table-column label="原料" width="180">
<template #default="{ row }">{{ row.stock_label }}({{ row.stock_length }}mm)</template>
@@ -25,8 +58,11 @@
</el-table-column>
<el-table-column prop="waste" label="余料mm" width="90" />
</el-table>
<el-alert v-if="result.unplaced.length" type="warning" :closable="false"
:title="`未排入 ${result.unplaced.length} 件(库存不足)`" style="margin-top:10px" />
<template v-if="!result.use_stock && result.unplaced.length">
<el-alert type="warning" :closable="false" class="no-print"
:title="`未排入 ${result.unplaced.length} 件`" style="margin-top:10px" />
</template>
</template>
</el-card>
</template>
@@ -40,19 +76,26 @@ const contracts = ref<any[]>([])
const contractId = ref<number | undefined>()
const contractNo = ref('')
const kerf = ref(5)
const useStock = ref(false)
const stockLength = ref(6000)
const loading = ref(false)
const result = ref<any>(null)
async function run() {
loading.value = true
try {
const d = await api<any>('post', '/api/cut_solution', { contract_id: contractId.value, kerf: kerf.value })
const d = await api<any>('post', '/api/cut_solution', {
contract_id: contractId.value, kerf: kerf.value,
use_stock: useStock.value, stock_length: stockLength.value,
})
result.value = d
contractNo.value = d.contract_no
ElMessage.success(`已生成:利用率 ${(d.stats.utilization * 100).toFixed(1)}%`)
} finally { loading.value = false }
}
function print() { window.print() }
onMounted(async () => {
const d = await api<any>('get', '/api/contracts?page=1&page_size=100')
contracts.value = d.contracts?.data ?? []
@@ -60,5 +103,12 @@ onMounted(async () => {
</script>
<style scoped>
.toolbar { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; }
.toolbar { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
.sec-title { margin: 16px 0 8px; color: #303133; }
@media print {
.no-print { display: none !important; }
body * { visibility: hidden; }
.el-card, .el-card * { visibility: visible; }
.el-card { position: absolute; left: 0; top: 0; width: 100%; box-shadow: none; }
}
</style>