feat: P0-2 订单BOM计算单+Excel导出(openpyxl)

This commit is contained in:
HHY
2026-09-25 03:09:21 -07:00
parent 8c2bb5e12d
commit d5e3cdbede
10 changed files with 247 additions and 1 deletions
+10
View File
@@ -41,3 +41,13 @@ export async function api<T = any>(method: string, url: string, body?: object):
const res = await http.request({ method, url, data: body })
return res.data.data as T
}
/** 下载二进制文件(自动带 token),filename 为保存文件名 */
export async function apiDownload(url: string, filename: string): Promise<void> {
const res = await http.get(url, { responseType: 'blob' })
const link = document.createElement('a')
link.href = URL.createObjectURL(res.data)
link.download = filename
link.click()
URL.revokeObjectURL(link.href)
}
+48 -1
View File
@@ -82,6 +82,7 @@
</el-descriptions>
<div class="canvas-actions">
<el-button size="small" type="success" plain @click="quoteRef.open({ order_id: detail.id })">算料报价</el-button>
<el-button size="small" type="primary" plain @click="openBom">BOM 单</el-button>
<el-button size="small" @click="printOrder">打印订单</el-button>
</div>
<el-tabs model-value="canvas">
@@ -130,6 +131,35 @@
</el-tabs>
</div>
</el-drawer>
<!-- BOM 计算单 -->
<el-dialog v-model="bomVisible" :title="`BOM 计算单 — ${bom?.order_no ?? ''}`" width="640px">
<div v-if="bom" v-loading="bomLoading">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="窗型尺寸">{{ bom.width_mm }} × {{ bom.height_mm }} mm</el-descriptions-item>
<el-descriptions-item label="玻璃面积">{{ bom.glass_area_m2 }} ㎡</el-descriptions-item>
<el-descriptions-item label="五金">{{ bom.sash_count }} 套</el-descriptions-item>
</el-descriptions>
<el-table :data="bom.bars" size="small" border style="margin-top:10px">
<el-table-column prop="note" label="用途" min-width="120">
<template #default="{ row }">{{ row.note || row.kind }}</template>
</el-table-column>
<el-table-column prop="length" label="单件长度(mm)" width="120" />
<el-table-column prop="qty" label="数量(件)" width="100" />
<el-table-column label="总长(m)" width="100">
<template #default="{ row }">{{ (row.length * row.qty / 1000).toFixed(2) }}</template>
</el-table-column>
</el-table>
<div class="bom-total">
型材合计:{{ ((bom.bars ?? []).reduce((s: number, b: any) => s + b.length * b.qty, 0) / 1000).toFixed(2) }} m
</div>
</div>
<template #footer>
<el-button @click="bomVisible = false">关闭</el-button>
<el-button type="primary" @click="exportOrder">导出 Excel</el-button>
</template>
</el-dialog>
<QuoteDialog ref="quoteRef" />
</div>
</template>
@@ -138,7 +168,7 @@
import { onMounted, reactive, ref } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { api } from '../api'
import { api, apiDownload } from '../api'
import WindoorCanvas from '../components/WindoorCanvas.vue'
import QuoteDialog from '../components/QuoteDialog.vue'
@@ -315,6 +345,22 @@ async function updateProcess() {
processActive.value = detail.value.processes.filter((p: any) => p.status === 2).length
}
// ---- BOM 计算单 ----
const bomVisible = ref(false)
const bomLoading = ref(false)
const bom = ref<any>(null)
async function openBom() {
bomVisible.value = true
bomLoading.value = true
try { bom.value = await api<any>('get', `/api/orders/${detail.value.id}/bom`) } finally { bomLoading.value = false }
}
async function exportOrder() {
await apiDownload(`/api/orders/${detail.value.id}/export`, `order_${detail.value.order_no}.xlsx`)
ElMessage.success('已导出')
}
onMounted(load)
</script>
@@ -325,4 +371,5 @@ onMounted(load)
.empty-tip { height: 100%; display: flex; align-items: center; justify-content: center; color: #909399; font-size: 14px; }
.tip { color: #909399; font-size: 12px; }
.live-price { color: #e6483d; font-size: 15px; font-weight: 700; }
.bom-total { margin-top: 8px; text-align: right; font-weight: 600; color: #303133; }
</style>