fix: v2.16 样板库画布每块可编辑+模型框污染根治

- 预览对话框打开即编辑态, 报价/下单入口保留
- 格尺寸贴外框方向联动等比缩放整窗, 任意格任意方向均可编辑
- 根因修复: computeModelBox 误含扇 bd 退化点致 W/H 与梃位/缩放参考边全错
- 悬停格淡高亮 + pointer 光标; HUD 标签改格宽/格高
- 验证: 格尺寸/梃位/贴边缩放/组合写回四项截图全过, ui-shots 6 页无报错
This commit is contained in:
HHY
2026-09-23 10:39:28 -07:00
parent 6a5a3d1ed3
commit 21760d1887
13 changed files with 108 additions and 41 deletions
+25 -13
View File
@@ -13,7 +13,7 @@ const page = await browser.newPage()
page.on('pageerror', e => console.log(`[JS异常] ${String(e).slice(0, 200)}`))
const shots = 'D:/Code/huamenchuang/shots'
// 在画布内依次尝试点击候选点, 直到 HUD 出现"格W"(选中玻璃格)
// 在画布内依次尝试点击候选点, 直到 HUD 出现"格宽"(选中玻璃格)
async function clickUntilRegion(canvasBox) {
const candidates = [[0.35, 0.30], [0.30, 0.25], [0.40, 0.35], [0.32, 0.22]]
for (const [fx, fy] of candidates) {
@@ -21,7 +21,7 @@ async function clickUntilRegion(canvasBox) {
await sleep(350)
const ok = await page.evaluate(() => {
const inputs = [...document.querySelectorAll('.hud .dim-input')]
return inputs.some(d => d.textContent.includes('格W'))
return inputs.some(d => d.textContent.includes('格宽'))
})
if (ok) return true
}
@@ -51,25 +51,21 @@ await page.goto(`http://localhost:8000/templates?token=${encodeURIComponent(toke
await sleep(1500)
await page.click('.sample-card .cap, .card .cap, .cap')
await sleep(1200)
await page.evaluate(() => {
const btn = [...document.querySelectorAll('.el-dialog button')].find(b => b.textContent.includes('编辑分格'))
btn?.click()
})
await sleep(900)
// 打开即编辑态(无需再点编辑按钮)
let cv = await (await page.$('.el-dialog .canvas-box canvas')).boundingBox()
await page.screenshot({ path: `${shots}/detail-1-editor.png`, clip: cv })
if (!(await clickUntilRegion(cv))) { console.error('✗ 未能选中玻璃格'); process.exit(1) }
await page.screenshot({ path: `${shots}/detail-2-region-selected.png`, clip: cv })
const beforeW = await page.evaluate(() => {
const wrap = [...document.querySelectorAll('.hud .dim-input')].find(d => d.textContent.includes('格W'))
const wrap = [...document.querySelectorAll('.hud .dim-input')].find(d => d.textContent.includes('格宽'))
return Number(wrap.querySelector('input').value)
})
await setDim('格H', 800) // 亮子高度调到 800 → 横梃下移
await setDim('格高', 800) // 亮子高度调到 800 → 横梃下移
await page.screenshot({ path: `${shots}/detail-3-region-resized.png`, clip: cv })
// 梃位编辑: 点击中央竖梃(画布中线附近)
await page.mouse.click(cv.x + cv.width * 0.50, cv.y + cv.height * 0.55)
await page.mouse.click(cv.x + cv.width * 0.433, cv.y + cv.height * 0.55)
await sleep(350)
if (await hasDim('梃位')) {
await setDim('梃位', 900)
@@ -78,7 +74,23 @@ if (await hasDim('梃位')) {
} else {
console.log('△ 未命中中梃(点击位置在格上), 跳过梃位编辑截图')
}
console.log(`✓ 格尺寸编辑完成 (格W ${beforeW} → 调格H=800)`)
// 贴边格等比缩放: 选中右侧整列格(高贴外框) → 改格高 → 整窗等比缩放
await page.mouse.click(cv.x + cv.width * 0.684, cv.y + cv.height * 0.5)
await sleep(350)
if (await hasDim('格宽')) {
await setDim('格高', 1200)
const wh = await page.evaluate(() => {
const wraps = [...document.querySelectorAll('.hud .dim-input')]
const w = wraps.find(d => d.textContent.trim().startsWith('W'))
const h = wraps.find(d => d.textContent.trim().startsWith('H'))
return { w: w?.querySelector('input')?.value, h: h?.querySelector('input')?.value }
})
await page.screenshot({ path: `${shots}/detail-4b-edge-scale.png`, clip: cv })
console.log(`✓ 贴边格等比缩放: 整窗 ${wh.w} × ${wh.h} (期望 1800 × 1200)`)
} else {
console.log('△ 未选中贴边格, 跳过等比缩放验证')
}
console.log(`✓ 格尺寸编辑完成 (格W ${beforeW} → 调格高=800)`)
await page.close()
// ============ ② 组合页: 细节编辑对话框 + 保存写回 ============
@@ -107,7 +119,7 @@ if (!(await (async () => {
await sleep(350)
const ok = await page2.evaluate(() => {
const inputs = [...document.querySelectorAll('.hud .dim-input')]
return inputs.some(d => d.textContent.includes('格W'))
return inputs.some(d => d.textContent.includes('格宽'))
})
if (ok) return true
}
@@ -115,7 +127,7 @@ if (!(await (async () => {
})())) { console.error('✗ 组合细节编辑未能选中玻璃格'); process.exit(1) }
await page2.screenshot({ path: `${shots}/detail-5-combine-dialog.png` })
await page2.evaluate(() => {
const wrap = [...document.querySelectorAll('.hud .dim-input')].find(d => d.textContent.includes('格H'))
const wrap = [...document.querySelectorAll('.hud .dim-input')].find(d => d.textContent.includes('格高'))
const input = wrap.querySelector('input')
input.value = '900'
input.dispatchEvent(new Event('input', { bubbles: true }))
+75 -25
View File
@@ -10,9 +10,9 @@
<span class="dim-input">H <el-input-number v-model="sizeH" :min="300" :step="10" size="small"
controls-position="right" style="width:104px" @change="onSizeInput" /></span>
<template v-if="selRegion">
<span class="dim-input">格W <el-input-number v-model="regW" :min="100" :step="10" size="small"
<span class="dim-input">格宽 <el-input-number v-model="regW" :min="100" :step="10" size="small"
controls-position="right" style="width:92px" @change="onRegionSize" /></span>
<span class="dim-input">格H <el-input-number v-model="regH" :min="100" :step="10" size="small"
<span class="dim-input">格高 <el-input-number v-model="regH" :min="100" :step="10" size="small"
controls-position="right" style="width:92px" @change="onRegionSize" /></span>
</template>
<template v-else-if="selected">
@@ -70,6 +70,7 @@ let moveDrag: null | { shapeIdx: number; spIdx: number; startPos: number; startX
const selected = ref<{ shapeIdx: number; spIdx: number } | null>(null)
// 细节编辑: 选中的玻璃格(可输入格宽高) ; selRegion 与 selected(梃) 互斥
const selRegion = ref<{ shapeIdx: number; ri: number } | null>(null)
let hoverRegion: { shapeIdx: number; ri: number } | null = null
const regW = ref(0)
const regH = ref(0)
const mulPos = ref(0)
@@ -121,36 +122,56 @@ function moveGroup(shapeIdx: number, axis: 'v' | 'h', pos: number, delta: number
return applied
}
/** 格尺寸编辑: 输入目标宽/高, 平移该格的边界中梃(优先右/下边界, 其次左/上) */
/** 找格在该方向的可动边界梃: 优先 pos1(右/下)侧, 其次 pos0(左/上)侧; 返回 {pos, sign} 或 null */
function findRegionEdge(sps: SplitLine[], axis: 'v' | 'h', bb: { x0: number; y0: number; x1: number; y1: number }) {
const pos1 = axis === 'v' ? bb.x1 : bb.y1
const e1 = sps.find(s => spAxis(s) === axis && Math.abs(spPos(s) - pos1) < 1.5)
if (e1) return { pos: spPos(e1), sign: 1 }
const pos0 = axis === 'v' ? bb.x0 : bb.y0
const e0 = sps.find(s => spAxis(s) === axis && Math.abs(spPos(s) - pos0) < 1.5)
if (e0) return { pos: spPos(e0), sign: -1 }
return null
}
/** 格尺寸编辑: 输入目标宽/高 → 平移边界中梃; 格两边都贴外框时联动等比缩放整窗 */
function onRegionSize() {
if (!selRegion.value || !dd) return
undoStack.push(JSON.stringify(dd))
const { shapeIdx, ri } = selRegion.value
const bb = regionBb(shapeIdx, ri)
if (!bb) return
const sps = dd.sm[shapeIdx]?.mm?.sp ?? []
const curW = bb.x1 - bb.x0, curH = bb.y1 - bb.y0
const targetW = Math.max(100, Math.round(regW.value))
const targetH = Math.max(100, Math.round(regH.value))
let moved = false
if (Math.abs(targetW - curW) >= 1) {
const edge = sps.find(s => spAxis(s) === 'v' && Math.abs(spPos(s) - bb.x1) < 1.5)
?? sps.find(s => spAxis(s) === 'v' && Math.abs(spPos(s) - bb.x0) < 1.5)
let changed = false
// 宽
let bb = regionBb(shapeIdx, ri)
if (!bb) { undoStack.pop(); return }
let cur = bb.x1 - bb.x0
if (Math.abs(targetW - cur) >= 1) {
const edge = findRegionEdge(sps, 'v', bb)
if (edge) {
const sign = Math.abs(spPos(edge) - bb.x1) < 1.5 ? 1 : -1
moveGroup(shapeIdx, 'v', spPos(edge), (targetW - curW) * sign)
moved = true
moveGroup(shapeIdx, 'v', edge.pos, (targetW - cur) * edge.sign)
changed = true
} else if (Math.abs(bb.x0 - modelBox.x0) < 1.5 && Math.abs(bb.x1 - modelBox.x1) < 1.5) {
// 格宽=整窗宽(两边界均为外框): 等比缩放整窗
resizeTo(Math.round((modelBox.x1 - modelBox.x0) * targetW / cur), Math.round(modelBox.y1 - modelBox.y0))
changed = true
}
}
if (Math.abs(targetH - curH) >= 1) {
const edge = sps.find(s => spAxis(s) === 'h' && Math.abs(spPos(s) - bb.y1) < 1.5)
?? sps.find(s => spAxis(s) === 'h' && Math.abs(spPos(s) - bb.y0) < 1.5)
// 高(宽调整可能已缩放整窗, 重新取格 bbox)
bb = regionBb(shapeIdx, ri)
if (!bb) { undoStack.pop(); return }
cur = bb.y1 - bb.y0
if (Math.abs(targetH - cur) >= 1) {
const edge = findRegionEdge(sps, 'h', bb)
if (edge) {
const sign = Math.abs(spPos(edge) - bb.y1) < 1.5 ? 1 : -1
moveGroup(shapeIdx, 'h', spPos(edge), (targetH - curH) * sign)
moved = true
moveGroup(shapeIdx, 'h', edge.pos, (targetH - cur) * edge.sign)
changed = true
} else if (Math.abs(bb.y0 - modelBox.y0) < 1.5 && Math.abs(bb.y1 - modelBox.y1) < 1.5) {
resizeTo(Math.round(modelBox.x1 - modelBox.x0), Math.round((modelBox.y1 - modelBox.y0) * targetH / cur))
changed = true
}
}
if (!moved) { syncRegionInputs(); ElMessage.warning('该格贴外框, 请先加分格梃再调整'); return }
if (!changed) { undoStack.pop(); syncRegionInputs(); return }
replayOne(shapeIdx)
computeModelBox()
syncSizeInputs()
@@ -501,6 +522,19 @@ function drawFrame(
}
ctx.textBaseline = 'alphabetic'
// 悬停格淡高亮(可编辑提示)
if (props.editable && hoverRegion && hoverRegion.shapeIdx === shapeIdx
&& !(selRegion.value && selRegion.value.shapeIdx === shapeIdx && selRegion.value.ri === hoverRegion.ri)) {
const r = rp.regions[hoverRegion.ri]
if (r) {
ctx.beginPath()
regionPoints(r).forEach((p, i) => (i ? ctx.lineTo(tx(p.x), ty(p.y)) : ctx.moveTo(tx(p.x), ty(p.y))))
ctx.closePath()
ctx.fillStyle = 'rgba(64,158,255,0.14)'
ctx.fill()
}
}
// 6) 选中玻璃格高亮: 蓝虚线框 + 格尺寸
if (selRegion.value && selRegion.value.shapeIdx === shapeIdx) {
const r = rp.regions[selRegion.value.ri]
@@ -751,12 +785,10 @@ function computeModelBox() {
if (!dd) return
const pts: Pt[] = []
for (const s of dd.sm) {
// 外框只由基底多边形决定; 扇 bd 序列化常退化为单点, 计入会把模型框拉大
for (const r of (s.polygon?.polygon ?? [])) {
for (const seg of r) { pts.push(seg.ps); pts.push(seg.pe) }
}
for (const sub of (s.sm ?? []) as SashShape[]) {
if (sub.bd) { pts.push(sub.bd.from, sub.bd.to) }
}
}
if (!pts.length) { modelBox = { x0: 0, y0: 0, x1: 1000, y1: 1000 }; return }
modelBox = bboxOf(pts)
@@ -827,12 +859,30 @@ function onMove(e: MouseEvent) {
}
return
}
// 光标提示
// 光标提示 + 悬停格高亮
if (props.editable && !tool.value && rect) {
const sx = e.clientX - rect.left, sy = e.clientY - rect.top
const hit = hitResizeHandle(sx, sy)
const m = hit ? null : hitMullion(sx, sy)
let hover: { shapeIdx: number; ri: number } | null = null
if (!hit && !m) {
const p = toModel(sx, sy)
let best = Infinity
for (let si = 0; si < replays.length; si++) {
const regions = replays[si].regions
for (let ri = 0; ri < regions.length; ri++) {
const pts = regionPoints(regions[ri])
if (pts.length < 3 || !pointInPoly(p, pts)) continue
const b = bboxOf(pts)
const area = (b.x1 - b.x0) * (b.y1 - b.y0)
if (area < best) { best = area; hover = { shapeIdx: si, ri } }
}
}
}
cvRef.value!.style.cursor = hit === 'wh' ? 'nwse-resize' : hit === 'w' ? 'ew-resize' : hit === 'h' ? 'ns-resize'
: hitMullion(sx, sy) ? 'move' : 'grab'
: m ? 'move' : hover ? 'pointer' : 'grab'
const changed = hover?.shapeIdx !== hoverRegion?.shapeIdx || hover?.ri !== hoverRegion?.ri
if (changed) { hoverRegion = hover; render() }
}
if (!drag) return
view.cx += e.clientX - drag.x
+6 -3
View File
@@ -39,14 +39,16 @@
<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-button size="small" @click="editing = true">编辑分格/尺寸</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="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-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>
</template>
</div>
</div>
@@ -108,7 +110,8 @@ async function load() {
async function preview(t: any) {
current.value = await api<any>('get', `/api/windoor/templates/${t.id}`)
editing.value = false
// 点击进入即编辑: 每一块(玻璃格/中梃)都可直接编辑
editing.value = true
dirty.value = false
liveTotal.value = null
previewVisible.value = true