diff --git a/CHANGELOG b/CHANGELOG index dbba4e2..6580bd2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # CHANGELOG +- [2026-09-25 21:00] FEAT: v2.30 P1 批次2 报价/产品域 3 项。①型材系列管理: /api/product-catelogs 树 CRUD(系列下有产品禁删)+产品挂系列(catelog_id)+按系列筛选, 产品页左树右表布局+编辑表单系列选择+补回丢失的产品编辑对话框; ②副梃计价收尾: 产品类别加 subbar(按尺寸/米计价, 随逐格编辑 assigns.subBar 算料); ③成本报价: 报价保存/更新按产品名匹配价格本成本写行 _cost(quote_by_cost 语义)+响应加 cost_total/profit, 报价页加成本/毛利列+分享链接(quote_link 走 shares type=quote); 实测: 成本匹配 6×38=228 毛利 84; vue-tsc 0 错+构建通过 (Files: src/huamenchuang/api/business.py, frontend/src/views/ProductsView.vue, frontend/src/views/QuotationsView.vue, doc/API.md) + - [2026-09-25 20:10] FEAT: v2.29 P1 批次1 设计域 5 项。①AI 推荐窗型: api/recommend.py 规则引擎(洞口宽高比/玻璃面积比/开扇数/门类/纱扇打分)+需求标签规则(通风/采光/隔音/安全/景观/通行)+可解释理由 TOP5, 样板库页"AI 推荐"Tab; ②通用分享体系: ShareItem 模型+POST/GET /api/shares(render/combine/quote/sunny 四类)+公开页 /share/s/:token(大图/报价明细/组合只读画布), 组合页/AI 炫图/报价单三处分享按钮; ③阳光房设计: SunroomDesign 模型+CRUD+SunroomCanvas 参数化绘图(flat 平顶/gable 人字顶 正视+俯视+尺寸标注)+AI 效果图(scene=sunroom-*, 提示词含 bays/footprint)+设计分享; ④量尺拍照标注: 照片上传+Canvas 拖拽画尺寸线(颜色/标签/撤销)+合成图存洞口行 photo; ⑤扇/玻璃逐格指定: SashAssign(decs 同型自研扩展)+画布"扇/玻璃"对话框(五类开启类型/内外开/铰链侧/副梃/玻璃配置)+2D 开向三角渲染+takeoff 计指定扇框料五金副梃(subbar 25 元/米); vue-tsc 0 错+构建通过, 推荐/阳光房/分享 API 实测通过 (Files: src/huamenchuang/models/extend.py, src/huamenchuang/api/shares.py, src/huamenchuang/api/recommend.py, src/huamenchuang/api/sunroom.py, src/huamenchuang/services/ai_prompt.py, src/huamenchuang/services/quote_service.py, frontend/src/views/ShareView.vue, frontend/src/views/SunnyHouseView.vue, frontend/src/views/TemplatesView.vue, frontend/src/views/MeasureView.vue, frontend/src/views/CombineView.vue, frontend/src/views/AIRenderView.vue, frontend/src/views/QuotationsView.vue, frontend/src/components/SunroomCanvas.vue, frontend/src/components/WindoorCanvas.vue, frontend/src/types/drawing.ts, frontend/src/router/index.ts, doc/API.md) - [2026-09-25 18:30] FEAT: v2.28 AI 炫图接入 gpt-image-2.5 云端生图+提示词工程。①services/ai_prompt.py 结构化提示词构建器: 从 drawing_data 提取窗型特征(组合单元/宽高mm/格数/扇类型平开推拉内开内倒对开门/开向/纱扇/圆弧/格条)→专业英文提示词(主体先行/材质风格映射/摄影光照/负面约束"保持分格勿增删扇"/质量词)+按宽高比自动选尺寸(1024/1536x1024/1024x1536); ②api/ai_render.py 代理(Key 仅后端 .env HMC_AI_API_KEY, 已 gitignore): POST /ai-render/prompt 提示词预览 + /ai-render/generate(quick→generations, photo→edits 参考图合成, httpx 180s 超时, 错误分级); ③config .env 锚定项目根(修复 cwd 相对路径失效); ④前端 AIRenderView 双模式主按钮接云端(photo 模式参考图压缩≤1536px JPEG), 结果大图+提示词折叠面板+下载, 未配 Key 降级本地 4 方案; ⑤实测: quick 生图 28s 返回三格平开窗产品级渲染(把手/铰链/投影正确), photo 合成 55s 返回窗型融入照片; 端到端 puppeteer 验证通过(shots/verify-aicloud.png); vue-tsc 0 错+构建通过 (Files: pyproject.toml, src/huamenchuang/services/ai_prompt.py, src/huamenchuang/api/ai_render.py, src/huamenchuang/api/router.py, src/huamenchuang/core/config.py, frontend/src/views/AIRenderView.vue, frontend/src/api/index.ts, frontend/scripts/verify-aicloud.mjs, doc/API.md) diff --git a/doc/API.md b/doc/API.md index b4d5bb3..104bbfc 100644 --- a/doc/API.md +++ b/doc/API.md @@ -492,3 +492,12 @@ uv run uvicorn huamenchuang.main:app --reload # 启动,http://localhost:8000 | 删除阳光房设计 | DELETE | `/api/sunroom/designs/{id}` | 路径 | | **逐格扇/玻璃指定**(自研扩展 `drawing_data.sm[i].assigns[] = [{ri, sash:{kind: fixed/casement/awning/sliding/kfc, openToward, openDirection}, subBar, glass}]`):画布选格 → "扇/玻璃"对话框指定开启类型/方向/铰链侧/副梃/玻璃配置;2D 画开向三角与副梃线;算料计入指定扇框料+五金(`sash_count` 含 assigns)与副梃长度(`subbar` 默认 25 元/米,价格本可覆盖)。量尺拍照标注:洞口行 `photo` 字段存标注合成图(前端 Canvas 拖线标注)。 + +### 12.25 P1 报价/产品域(v2.30) + +| 接口名称 | 方式 | URL 路径 | 请求参数 | 说明 | +|---|---|---|---|---| +| 系列树 | GET | `/api/product-catelogs` | — | 型材系列树(对齐 product-catelog),产品价格本页左侧管理 | +| 新增/改名/删系列 | POST/PUT/DELETE | `/api/product-catelogs[/{id}]` | `{name, fid?}` | 系列下有产品时禁止删除 | +| 产品筛选 | GET | `/api/products` | 增加 `catelog_id` | 产品挂系列(catelog_id);material 新增 `subbar`(副梃) | +| **成本口径** | — | (自动) | — | 对齐 quote_by_cost:报价保存/更新时按产品名匹配价格本成本写入行 `_cost`;报价响应新增 `cost_total`(成本合计)与 `profit`(毛利);报价链接分享(quote_link)走 `/api/shares` type=quote 公开页 | diff --git a/frontend/src/views/ProductsView.vue b/frontend/src/views/ProductsView.vue index 06353c3..e5ba44c 100644 --- a/frontend/src/views/ProductsView.vue +++ b/frontend/src/views/ProductsView.vue @@ -3,34 +3,85 @@ -
- - - - - - 查询 -
- 新增产品 -
- - - - - - - - - - - - - - + + + + +
+ + 加系列 + 加子类 +
+
+ 改名 + 删除 + 已选:{{ currentCat.name }} +
+
+
+ +
+ + + + + + + 查询 + 清除系列筛选 +
+ 新增产品 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -107,12 +158,55 @@ const loading = ref(false) const editVisible = ref(false) const form = reactive({ id: 0, name: '', material: 'profile', spec: '', unit: '米', price: 0, cost: 0, note: '' }) -const label = (m: string) => ({ profile: '型材', glass: '玻璃', hardware: '五金', addon: '配件' } as Record)[m] ?? m +const label = (m: string) => ({ profile: '型材', glass: '玻璃', hardware: '五金', addon: '配件', subbar: '副梃' } as Record)[m] ?? m + +// ---- 型材系列(product-catelog) ---- +const catelogs = ref([]) +const currentCat = ref(null) +const newCat = ref('') +const catTreeRef = ref() + +async function loadCats() { + catelogs.value = (await api('get', '/api/product-catelogs')) ?? [] +} + +function onCatClick(node: any) { + currentCat.value = node + load() +} + +async function addCat(fid: number) { + if (!newCat.value.trim()) return ElMessage.warning('请填写系列名') + await api('post', '/api/product-catelogs', { name: newCat.value.trim(), fid }) + newCat.value = '' + loadCats() +} + +async function addChild() { + if (!newCat.value.trim()) return ElMessage.warning('请填写子类名') + await addCat(currentCat.value.id) +} + +async function renameCat() { + const name = await ElMessageBox.prompt('修改系列名称', '改名', { inputValue: currentCat.value.name }) + await api('put', `/api/product-catelogs/${currentCat.value.id}`, { name: name.value, fid: currentCat.value.fid }) + ElMessage.success('已改名') + loadCats() +} + +async function removeCat() { + await ElMessageBox.confirm(`确定删除系列「${currentCat.value.name}」?`, '提示', { type: 'warning' }) + try { + await api('delete', `/api/product-catelogs/${currentCat.value.id}`) + currentCat.value = null + loadCats(); load() + } catch { /* 拦截器已提示(系列下有产品) */ } +} async function load() { loading.value = true try { - const qs = `page=${page.value}&page_size=${page_size}&keyword=${keyword.value}${material.value ? `&material=${material.value}` : ''}` + const qs = `page=${page.value}&page_size=${page_size}&keyword=${keyword.value}${material.value ? `&material=${material.value}` : ''}${currentCat.value ? `&catelog_id=${currentCat.value.id}` : ''}` const d = await api('get', `/api/products?${qs}`) rows.value = d.data total.value = d.total @@ -120,7 +214,7 @@ async function load() { } function openEdit(row?: any) { - Object.assign(form, row ?? { id: 0, name: '', material: 'profile', spec: '', unit: '米', price: 0, cost: 0, note: '' }) + Object.assign(form, row ?? { id: 0, name: '', material: 'profile', spec: '', unit: '米', price: 0, cost: 0, note: '', catelog_id: currentCat.value?.id ?? null }) editVisible.value = true } @@ -188,7 +282,7 @@ async function test(row: any) { ElMessage.success(`试算结果 ¥${d.result}`) } -onMounted(() => { load(); loadScripts() }) +onMounted(() => { load(); loadScripts(); loadCats() }) diff --git a/frontend/src/views/QuotationsView.vue b/frontend/src/views/QuotationsView.vue index 08acfcf..1e8b019 100644 --- a/frontend/src/views/QuotationsView.vue +++ b/frontend/src/views/QuotationsView.vue @@ -14,6 +14,12 @@ + + +