feat: 部位级材料选择(对应部位使用对应材料: 估算器overrides+series-materials聚合端点+QuoteDialog部位选材面板实时重算)

This commit is contained in:
HHY
2026-10-06 05:58:10 -07:00
parent 07eda3ff35
commit 5abfe61beb
6 changed files with 143 additions and 14 deletions
+51 -4
View File
@@ -22,7 +22,18 @@
<span class="dim">按当前窗型 {{ result.series_estimate.width }}×{{ result.series_estimate.height }}mm 估算</span>
</div>
</template>
<el-row :gutter="10">
<!-- 部位选材: 对应部位使用对应材料(可跨系列混搭) -->
<div class="part-row">
<div v-for="p in partKeys" :key="p.key" class="part-item">
<div class="part-label">{{ p.label }}</div>
<el-select v-model="parts[p.key]" placeholder="系列默认" clearable filterable size="small"
style="width:100%" @change="requote">
<el-option v-for="(m, i) in (materialGroups[p.key] ?? [])" :key="i" :value="i"
:label="`${m.name} · ${m.unit_price}${p.key === 'glass' ? '元/㎡' : '元'}${m.series ? ' · ' + m.series : ''}`" />
</el-select>
</div>
</div>
<el-row :gutter="10" style="margin-top:10px">
<el-col :span="6"><el-statistic title="系列成本(元/樘)" :value="result.series_estimate.total" :precision="2" /></el-col>
<el-col :span="6"><el-statistic title="每平成本(元/㎡)" :value="result.series_estimate.per_sqm" :precision="2" /></el-col>
<el-col :span="6"><el-statistic title="建议售价(50%毛利)" :value="result.series_estimate.margin_ladder['50%']" :precision="2" /></el-col>
@@ -58,7 +69,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { reactive, ref } from 'vue'
import { api } from '../api'
const visible = ref(false)
@@ -66,6 +77,34 @@ const loading = ref(false)
const result = ref<any>(null)
let source: { drawing_data?: string; template_id?: number; order_id?: number; series_id?: number } = {}
// 部位选材: 对应部位使用对应材料(选项聚合自各系列用料清单, 可跨系列混搭)
const partKeys = [
{ key: 'frame', label: '外框型材' },
{ key: 'mullion', label: '中梃型材' },
{ key: 'sash', label: '扇框型材' },
{ key: 'glass', label: '玻璃配置' },
{ key: 'hardware', label: '五金套件' },
] as const
const materialGroups = ref<Record<string, any[]>>({})
const parts = reactive<Record<string, number | undefined>>({ frame: undefined, mullion: undefined, sash: undefined, glass: undefined, hardware: undefined })
function buildMaterials(): Record<string, any> | undefined {
const out: Record<string, any> = {}
for (const { key } of partKeys) {
const idx = parts[key]
const m = idx !== undefined ? (materialGroups.value[key] ?? [])[idx] : undefined
if (m) out[key] = { name: m.name, unit_price: m.unit_price, weight_per_m: m.weight_per_m }
}
return Object.keys(out).length ? out : undefined
}
async function requote() {
loading.value = true
try {
result.value = await api<any>('post', '/api/offer-price', { ...source, materials: buildMaterials() })
} finally { loading.value = false }
}
function kindLabel(k: string) {
return ({ frame: '外框', mullion: '中梃', sash: '扇框', glass: '玻璃', hardware: '五金' } as Record<string, string>)[k] ?? k
}
@@ -74,6 +113,11 @@ async function open(src: typeof source) {
source = src
visible.value = true
loading.value = true
// 重置部位选择
for (const { key } of partKeys) parts[key] = undefined
if (src.series_id && !Object.keys(materialGroups.value).length) {
try { materialGroups.value = await api<any>('get', '/api/costbook/series-materials') } catch { /* 无系列数据时隐藏 */ }
}
try {
result.value = await api<any>('post', '/api/offer-price', src)
} finally { loading.value = false }
@@ -89,7 +133,10 @@ defineExpose({ open })
<style scoped>
.total { color: #e6483d; font-size: 18px; font-weight: 700; }
.series-card { margin-top: 10px; }
.series-head { display: flex; align-items: center; gap: 8px; font-weight: 600; }
.series-head { display: flex; align-items: center; gap: 8px; font-weight: 600; flex-wrap: wrap; }
.series-head .dim { font-size: 12px; color: #909399; font-weight: 400; }
@media print { .el-dialog__footer { display: none; } }
.part-row { display: flex; gap: 8px; margin-top: 4px; }
.part-item { flex: 1; min-width: 0; }
.part-label { font-size: 12px; color: #909399; margin-bottom: 4px; }
@media print { .el-dialog__footer, .part-row { display: none; } }
</style>