feat: 样板库整合材料系列选择(商家选材闭环: 预览对话框系列选择器+offer-price系列成本估算+QuoteDialog选材参考面板)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# CHANGELOG
|
||||
|
||||
- [2026-10-06 08:05] FEAT: v2.42 样板库整合材料系列选择——商家选材闭环。样板库预览对话框(编辑/非编辑态)新增「材料系列」选择器(GET /api/costbook/series: 75框扇齐平1.4/85框扇齐平1.8/95窗纱一体1.8, 来自鲁铭成本核算 Excel); 选择后算料报价携带 series_id → /api/offer-price 响应附加 series(系列名/壁厚)+series_estimate(按图纸实际宽高的系列八分区成本估算: 总成本/每平/型材/玻璃/五金/人工/margin_ladder 毛利阶梯); QuoteDialog 新增「材料系列成本参考」面板(统计卡+分区明细+建议售价50%毛利), 与报价总额并列对比辅助商家决策; 端到端实测: 样板704(1800x1600)+系列75 → 报价761.08元, 系列成本参考1251.1元/樘(每平434.41, 50%毛利建议1876.65); 回归: 冒烟 60/60+24 页巡检 0 异常 (Files: src/huamenchuang/api/quote.py, frontend/src/components/QuoteDialog.vue, frontend/src/views/TemplatesView.vue, doc/API.md)
|
||||
- [2026-10-05 07:52] FIX: v2.41 门窗渲染语义区分(对齐官方图纸)。对照官方参考图(windoor-images/31337/31343/31637)逐项核对门的视觉语义, 三渲染器同步: ①门把手贯穿长条(约55%扇高, 官方门统一长条贯穿中缝侧) vs 窗短把手(20%) ②门上下锁点小方块(RenderCanvas 此前缺失, 补齐; WindoorCanvas 已有) ③裙板色=型材同色系(RenderCanvas 取 shape.cm.g 暗化 62%, 解析失败退中性深灰; WindoorCanvas 图纸深灰+竖纹肌理+浅色编号; 3D 0x606369)——此前裙板米色与型材脱节; 门窗开向符号(V形虚线顶点指铰链侧)经官方图核对为同一语义无需区分; 视觉验收: 31343/31337 门(长把手+锁点+型材色裙板)与 704 窗(短把手)对比官方图一致; 回归: 24 页巡检 0 异常+冒烟 60/60 (Files: frontend/src/components/RenderCanvas.vue, frontend/src/components/WindoorCanvas.vue, frontend/src/components/WindoorThreed.vue)
|
||||
- [2026-10-05 07:30] TEST: v2.40 全量 254 套模板视觉校验——v2.39 消费路径全量回归 0 缺陷。新增两个常驻校验资产: ①frontend/scripts/verify-sash-panels.mjs(结构级: 渲染器同路径全量跑 重放/扇推断/锚点匹配/inferSashPanels, A~E 五级异常分类+子格覆盖度断言, 实测 254 文件/409 形状/570 扇/726 子格(裙板 150/格条 58) 0 异常) ②frontend/scripts/patrol-render-all.mjs(渲染级: 无头浏览器逐套切换样板真实渲染, 收集 JS 异常+画布非透明像素活性, 实测 254/254 OK 0 空白 0 异常); 6 套代表性样板视觉抽验通过(704 平开窗/31637 推拉门格条裙板/31343 双扇门裙板/31353 三扇推拉16条扇内分割/31593 平开窗/31603 四扇推拉)——v2.39 为渲染器/工具层共性修复, 全量样板自动受益, 无需逐套修改 (Files: frontend/scripts/verify-sash-panels.mjs, frontend/scripts/patrol-render-all.mjs)
|
||||
- [2026-10-05 07:14] FEAT: v2.39 渲染视觉与图纸对齐——源站原生扇内结构全量消费(扇内分割/裙板/格条/编号)。数据层诊断: 704(mm.sp=2 三区域+A1 平开扇)/31637(Frame 无分割+Slide 双推拉扇, 扇自带 mm.sp 裙板分割+mm.fls 玻璃/Panel 挂接+mm.dbs.bars decorationChinese 格条, hc=2 主格+gp=80 回纹)数据本身完整, 自研渲染器只消费顶层 mm.sp 未下钻扇内。新增 types/drawing.ts inferSashPanels(扇矩形构造临时 shape 复用 replayShape 全语义重放扇内分割, fls 按 polyId{idx,pos} 树寻址挂玻璃/裙板(type=Panel 判定), dbs.bars 格条解析 tp/gp/hc); 三渲染器消费: WindoorCanvas(2D 图纸: 裙板实色+格条内框/主格/四角回纹+玻璃编号 A1~A4)+RenderCanvas(效果图: 同语义)+WindoorThreed(3D: 裙板实体板+格条条体; 修复单 region 多扇时第二扇被 used 丢弃(改 glyph 自身矩形+共享区域保留静态底玻璃)); 扇形状按 sp 锚点归属匹配(修 sashes 数组序与视觉序交叉); WindoorCanvas 挂载加 ResizeObserver+零尺寸保护(修 el-dialog 内首帧 0 尺寸空白, 与 v2.38 3D 同型); 视觉验收: 704 渲染(开向三角/把手/编号)与 31637 四视图(双推拉+中式格条+裙板)均与官方参考图对齐; 回归: 254 套数据校验 0 失败+24 页巡检 0 异常+冒烟 60/60 (Files: frontend/src/types/drawing.ts, frontend/src/components/WindoorCanvas.vue, frontend/src/components/RenderCanvas.vue, frontend/src/components/WindoorThreed.vue)
|
||||
|
||||
+3
-2
@@ -259,11 +259,12 @@
|
||||
|---|---|---|---|---|
|
||||
| 一维优化 | POST | `/api/optimize1d` | `{demands:[{length,qty,label}], stocks:[{length,qty,label}], kerf:5}` | FFD 首递减装箱;返回 `plans[]`(每根原料的切割组合+余料)、`unplaced[]`、`stats.utilization`;需求>库存时 code=0 |
|
||||
|
||||
### 12.8 报价算料 `/api/offer-price`(v0.3 新增)
|
||||
### 12.8 报价算料 `/api/offer-price`(v0.3 新增,v2.42 增加系列选材)
|
||||
|
||||
| 接口名称 | 方式 | URL 路径 | 请求参数 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| 算料报价 | POST | `/api/offer-price` | `{drawing_data \| template_id \| order_id(三选一), prices?}` | 从 drawing_data 几何提取:外框/中梃/扇框型材清单(工程近似)+ 玻璃面积(鞋带公式)+ 五金,套价格本计价。默认价: 型材40-50元/米、玻璃120元/㎡、五金180元/扇;`prices` 可覆盖 |
|
||||
| 算料报价 | POST | `/api/offer-price` | `{drawing_data \| template_id \| order_id(三选一), prices?, series_id?}` | 从 drawing_data 几何提取:外框/中梃/扇框型材清单(工程近似)+ 玻璃面积(鞋带公式)+ 五金,套价格本计价。默认价: 型材40-50元/米、玻璃120元/㎡、五金180元/扇;`prices` 可覆盖 |
|
||||
| **系列选材** | (series_id 参数) | — | `series_id`(成本系列模板 id,见 12.27) | 传入后响应附加 `series`(系列名/壁厚)与 `series_estimate`(按图纸实际尺寸的系列成本估算: total/per_sqm/分区成本/margin_ladder 毛利阶梯)——商家在样板库选择材料系列后算料报价即得该系列的选材成本参考 |
|
||||
|
||||
### 12.9 仓库库存 `/api/warehouse`(v0.3 新增)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="算料报价" width="640px">
|
||||
<el-dialog v-model="visible" title="算料报价" width="760px">
|
||||
<div v-if="result" v-loading="loading">
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="窗型尺寸">{{ result.width_mm }} × {{ result.height_mm }} mm</el-descriptions-item>
|
||||
@@ -10,6 +10,32 @@
|
||||
type="warning" effect="plain" style="margin-left:6px">脚本计价</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 材料系列选材参考 -->
|
||||
<el-card v-if="result.series_estimate" shadow="never" class="series-card">
|
||||
<template #header>
|
||||
<div class="series-head">
|
||||
<span>材料系列成本参考</span>
|
||||
<el-tag size="small" type="primary" effect="plain">
|
||||
{{ result.series.name }} · 系列 {{ result.series.series }} · 壁厚 {{ result.series.wall_thick }}mm
|
||||
</el-tag>
|
||||
<span class="dim">按当前窗型 {{ result.series_estimate.width }}×{{ result.series_estimate.height }}mm 估算</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-row :gutter="10">
|
||||
<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>
|
||||
<el-col :span="6"><el-statistic title="报价总额(元)" :value="result.total" :precision="2" /></el-col>
|
||||
</el-row>
|
||||
<el-descriptions :column="4" border size="small" style="margin-top:10px">
|
||||
<el-descriptions-item label="型材">{{ result.series_estimate.profile_cost }} 元</el-descriptions-item>
|
||||
<el-descriptions-item label="玻璃">{{ result.series_estimate.glass_cost }} 元</el-descriptions-item>
|
||||
<el-descriptions-item label="五金">{{ result.series_estimate.hardware_cost }} 元</el-descriptions-item>
|
||||
<el-descriptions-item label="人工">{{ result.series_estimate.labor_cost }} 元</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<el-table :data="result.rows" size="small" border style="margin-top:10px">
|
||||
<el-table-column prop="note" label="项目" min-width="110">
|
||||
<template #default="{ row }">{{ row.note || kindLabel(row.kind) }}</template>
|
||||
@@ -38,7 +64,7 @@ import { api } from '../api'
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const result = ref<any>(null)
|
||||
let source: { drawing_data?: string; template_id?: number; order_id?: number } = {}
|
||||
let source: { drawing_data?: string; template_id?: number; order_id?: number; series_id?: number } = {}
|
||||
|
||||
function kindLabel(k: string) {
|
||||
return ({ frame: '外框', mullion: '中梃', sash: '扇框', glass: '玻璃', hardware: '五金' } as Record<string, string>)[k] ?? k
|
||||
@@ -62,5 +88,8 @@ 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 .dim { font-size: 12px; color: #909399; font-weight: 400; }
|
||||
@media print { .el-dialog__footer { display: none; } }
|
||||
</style>
|
||||
|
||||
@@ -77,14 +77,24 @@
|
||||
<template v-if="!editing">
|
||||
<span>尺寸限制: {{ current.low_width }}-{{ current.high_width }} × {{ current.low_height }}-{{ current.high_height }} mm</span>
|
||||
<span>引用次数: {{ current.times }}</span>
|
||||
<el-button type="success" size="small" @click="quoteRef.open({ template_id: current.id })">算料报价</el-button>
|
||||
<el-select v-model="seriesId" placeholder="选择材料系列" clearable size="small" style="width:170px"
|
||||
@change="onSeriesChange">
|
||||
<el-option v-for="s in seriesList" :key="s.id" :value="s.id"
|
||||
:label="`${s.name}(${s.wall_thick}mm)`" />
|
||||
</el-select>
|
||||
<el-button type="success" size="small" @click="doQuote">算料报价</el-button>
|
||||
<el-button size="small" @click="editing = true">编辑样板</el-button>
|
||||
<el-button type="primary" size="small" @click="useInOrder">用此样板下单</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="tip">点击玻璃格输入格宽高;点中梃输入位置或拖动;改 W/H 缩放整体;+竖梃/+横梃分割</span>
|
||||
<span class="live-price" v-if="liveTotal !== null">当前报价 ¥{{ liveTotal }}</span>
|
||||
<el-button type="success" size="small" @click="quoteRef.open({ template_id: current.id })">算料报价</el-button>
|
||||
<el-select v-model="seriesId" placeholder="选择材料系列" clearable size="small" style="width:170px"
|
||||
@change="onSeriesChange">
|
||||
<el-option v-for="s in seriesList" :key="s.id" :value="s.id"
|
||||
:label="`${s.name}(${s.wall_thick}mm)`" />
|
||||
</el-select>
|
||||
<el-button type="success" size="small" @click="doQuote">算料报价</el-button>
|
||||
<el-button size="small" @click="cancelEdit">取消</el-button>
|
||||
<el-button type="primary" size="small" :disabled="!dirty" @click="saveAsTemplate">另存为我的样板</el-button>
|
||||
<el-button size="small" @click="useInOrder">用此样板下单</el-button>
|
||||
@@ -122,6 +132,24 @@ const editRef = ref()
|
||||
const liveTotal = ref<number | null>(null)
|
||||
const tab = ref<'official' | 'mine' | 'ai'>('official')
|
||||
let quoteTimer: number | undefined
|
||||
// 材料系列(成本系列模板): 商家按系列选材, 随算料报价出系列成本参考
|
||||
const seriesList = ref<any[]>([])
|
||||
const seriesId = ref<number | undefined>()
|
||||
const seriesTip = ref('')
|
||||
|
||||
async function loadSeries() {
|
||||
if (seriesList.value.length) return
|
||||
try { seriesList.value = await api<any>('get', '/api/costbook/series') } catch { /* 无系列时不阻塞 */ }
|
||||
}
|
||||
|
||||
function doQuote() {
|
||||
quoteRef.value?.open({ template_id: current.value.id, series_id: seriesId.value })
|
||||
}
|
||||
|
||||
function onSeriesChange(id: number | undefined) {
|
||||
const s = seriesList.value.find(x => x.id === id)
|
||||
if (s) ElMessage.info(`已选「${s.name}」材料系列,点击算料报价查看该系列成本参考`)
|
||||
}
|
||||
|
||||
// ---- AI 智能推荐 ----
|
||||
const recLoading = ref(false)
|
||||
@@ -171,6 +199,8 @@ async function preview(t: any) {
|
||||
editing.value = true
|
||||
dirty.value = false
|
||||
liveTotal.value = null
|
||||
seriesId.value = undefined
|
||||
loadSeries()
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class QuoteRequest(BaseModel):
|
||||
drawing_data: str | dict | None = Field(default=None, description="画布 drawing_data(与 template_id/order_id 三选一)")
|
||||
template_id: int | None = Field(default=None, description="或直接指定样板")
|
||||
order_id: int | None = Field(default=None, description="或对已有订单报价")
|
||||
series_id: int | None = Field(default=None, description="材料系列(成本系列模板): 附加该系列的成本估算供商家选材")
|
||||
prices: dict | None = Field(default=None, description="价格本覆盖 {frame,mullion,sash,glass,hardware}")
|
||||
use_price_book: bool = Field(default=False, description="从产品管理价格本取价(按材料类别均价)")
|
||||
use_scripts: bool = Field(default=False, description="启用价格脚本计价(公司启用脚本覆盖默认单价/总价)")
|
||||
@@ -79,4 +80,20 @@ async def offer_price(body: QuoteRequest, user: CurrentUser, db: DbSession):
|
||||
except Exception as e: # 公式/图纸解析错误 → 用户可读失败
|
||||
msg = str(e) if "公式" in str(e) or "未知变量" in str(e) else "图纸数据解析失败"
|
||||
return fail(msg)
|
||||
# 材料系列成本参考:商家选材依据(系列模板八分区估算, 按图纸实际尺寸)
|
||||
if body.series_id:
|
||||
from huamenchuang.models.business import CostSeriesTemplate
|
||||
from huamenchuang.services import costbook_service as _svc
|
||||
tpl = await db.get(CostSeriesTemplate, body.series_id)
|
||||
if tpl is not None and tpl.company_id == user.company_id:
|
||||
w = result.get("width_mm") or 1600
|
||||
h = result.get("height_mm") or 1600
|
||||
est = _svc.estimate(tpl, w, h, 1)
|
||||
result["series"] = {"id": tpl.id, "name": tpl.name, "series": tpl.series, "wall_thick": tpl.wall_thick}
|
||||
result["series_estimate"] = {
|
||||
"width": w, "height": h, "total": est["total"], "per_sqm": est["per_sqm"],
|
||||
"profile_cost": est["profile_cost"], "glass_cost": est["glass_cost"],
|
||||
"hardware_cost": est["hardware_cost"], "labor_cost": est["labor_cost"],
|
||||
"margin_ladder": est["margin_ladder"],
|
||||
}
|
||||
return ok(data=result, message="报价成功")
|
||||
|
||||
Reference in New Issue
Block a user