feat: v2.13 细节尺寸编辑——组合与样板的每一格可精确编辑
- WindoorCanvas: 点击玻璃格选中并输入格W/格H(自动平移边界中梃, 共线多段组移动) - 选中中梃可输入距边位置精确移动; 中梃拖动改组移动修复多段错位 - CombineView: 编辑细节对话框(复用 WindoorCanvas), 保存原位写回对应单元并驱动 3D 重建 - CombineCanvas: 中梃按各自裁剪 bbox 绘制, 与真实分格一致 - 验证: 格尺寸/梃位/组合写回真实浏览器截图全通过
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
// 细节编辑验证: ①样板编辑对话框选中玻璃格→格W/H输入→边界梃平移 ②组合页编辑细节对话框→改格→保存写回
|
||||
// 用法: node verify-detail.mjs <token>
|
||||
import puppeteer from 'puppeteer-core'
|
||||
const EDGE = 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'
|
||||
const token = process.argv[2]
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: EDGE, headless: 'new',
|
||||
args: ['--window-size=1440,900', '--enable-unsafe-swiftshader'],
|
||||
defaultViewport: { width: 1440, height: 900 },
|
||||
})
|
||||
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"(选中玻璃格)
|
||||
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) {
|
||||
await page.mouse.click(canvasBox.x + canvasBox.width * fx, canvasBox.y + canvasBox.height * fy)
|
||||
await sleep(350)
|
||||
const ok = await page.evaluate(() => {
|
||||
const inputs = [...document.querySelectorAll('.hud .dim-input')]
|
||||
return inputs.some(d => d.textContent.includes('格W'))
|
||||
})
|
||||
if (ok) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 设置指定 dim-input 的数值(按标签文本查找)
|
||||
async function setDim(label, value) {
|
||||
await page.evaluate((label, value) => {
|
||||
const wrap = [...document.querySelectorAll('.hud .dim-input, .el-dialog .dim-input')]
|
||||
.find(d => d.textContent.includes(label))
|
||||
if (!wrap) throw new Error('未找到输入: ' + label)
|
||||
const input = wrap.querySelector('input')
|
||||
input.value = String(value)
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}, label, value)
|
||||
await sleep(500)
|
||||
}
|
||||
|
||||
const hasDim = label => page.evaluate((label) => {
|
||||
return [...document.querySelectorAll('.hud .dim-input')].some(d => d.textContent.includes(label))
|
||||
}, label)
|
||||
|
||||
// ============ ① 样板库编辑模式: 格尺寸编辑 ============
|
||||
await page.goto(`http://localhost:8000/templates?token=${encodeURIComponent(token)}`, { waitUntil: 'networkidle2' })
|
||||
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'))
|
||||
return Number(wrap.querySelector('input').value)
|
||||
})
|
||||
await setDim('格H', 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 sleep(350)
|
||||
if (await hasDim('梃位')) {
|
||||
await setDim('梃位', 900)
|
||||
await page.screenshot({ path: `${shots}/detail-4-mullion-moved.png`, clip: cv })
|
||||
console.log('✓ 梃位编辑完成')
|
||||
} else {
|
||||
console.log('△ 未命中中梃(点击位置在格上), 跳过梃位编辑截图')
|
||||
}
|
||||
console.log(`✓ 格尺寸编辑完成 (格W ${beforeW} → 调格H=800)`)
|
||||
await page.close()
|
||||
|
||||
// ============ ② 组合页: 细节编辑对话框 + 保存写回 ============
|
||||
const page2 = await browser.newPage()
|
||||
page2.on('pageerror', e => console.log(`[JS异常2] ${String(e).slice(0, 200)}`))
|
||||
await page2.goto(`http://localhost:8000/combine?token=${encodeURIComponent(token)}`, { waitUntil: 'networkidle2' })
|
||||
await sleep(1000)
|
||||
await page2.waitForSelector('.lib-item')
|
||||
await page2.click('.lib-item')
|
||||
await sleep(600)
|
||||
// 点击画布中心选中单元(编辑细节按钮依赖选中态)
|
||||
const mbox = await (await page2.$('.canvas-box canvas')).boundingBox()
|
||||
await page2.mouse.click(mbox.x + mbox.width / 2, mbox.y + mbox.height / 2)
|
||||
await sleep(400)
|
||||
await page2.evaluate(() => {
|
||||
const btn = [...document.querySelectorAll('button')].find(b => b.textContent.includes('编辑细节'))
|
||||
btn?.click()
|
||||
})
|
||||
await sleep(1200)
|
||||
const dcv = await (await page2.$('.el-dialog canvas')).boundingBox()
|
||||
if (!(await (async () => {
|
||||
// clickUntilRegion 绑定的是 page(已关闭), 这里内联同逻辑
|
||||
const candidates = [[0.35, 0.30], [0.30, 0.25], [0.40, 0.35], [0.32, 0.22]]
|
||||
for (const [fx, fy] of candidates) {
|
||||
await page2.mouse.click(dcv.x + dcv.width * fx, dcv.y + dcv.height * fy)
|
||||
await sleep(350)
|
||||
const ok = await page2.evaluate(() => {
|
||||
const inputs = [...document.querySelectorAll('.hud .dim-input')]
|
||||
return inputs.some(d => d.textContent.includes('格W'))
|
||||
})
|
||||
if (ok) return true
|
||||
}
|
||||
return false
|
||||
})())) { 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 input = wrap.querySelector('input')
|
||||
input.value = '900'
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
})
|
||||
await sleep(500)
|
||||
await page2.evaluate(() => {
|
||||
const btn = [...document.querySelectorAll('.el-dialog__footer button')].find(b => b.textContent.includes('保存到单元'))
|
||||
btn?.click()
|
||||
})
|
||||
await sleep(900)
|
||||
await page2.screenshot({ path: `${shots}/detail-6-combine-saved.png` })
|
||||
console.log('✓ 组合细节编辑+保存写回完成')
|
||||
await browser.close()
|
||||
@@ -208,7 +208,8 @@ function render() {
|
||||
ctx.lineJoin = 'round'
|
||||
for (const shape of props.shapes) {
|
||||
const sel = selectedUid.value !== null && shape.__uid === selectedUid.value
|
||||
for (const region of replayShape(shape).regions) {
|
||||
const rp = replayShape(shape)
|
||||
for (const region of rp.regions) {
|
||||
const rpts = regionPoints(region)
|
||||
if (rpts.length < 3) continue
|
||||
ctx.beginPath()
|
||||
@@ -221,24 +222,22 @@ function render() {
|
||||
ctx.stroke()
|
||||
}
|
||||
// 外框
|
||||
const rs = replayShape(shape).regions.map(regionPoints).filter(p => p.length >= 3)
|
||||
const rs = rp.regions.map(regionPoints).filter(p => p.length >= 3)
|
||||
if (rs.length) {
|
||||
const bb = bboxOf(rs.flat())
|
||||
ctx.strokeStyle = sel ? '#409eff' : '#5c6166'
|
||||
ctx.lineWidth = 3
|
||||
ctx.strokeRect(tx(bb.x0), ty(bb.y0), (bb.x1 - bb.x0) * scale, (bb.y1 - bb.y0) * scale)
|
||||
}
|
||||
// 中梃(按重放区域裁剪近似:画在其基底 bbox 内)
|
||||
const rb = rs.length ? bboxOf(rs.flat()) : null
|
||||
for (const sp of (shape.mm?.sp ?? [])) {
|
||||
if (!sp.line?.pt || !rb) continue
|
||||
// 中梃(按各自裁剪 bbox 绘制, 与真实分格一致)
|
||||
for (const { sp, bb } of rp.clips) {
|
||||
if (!sp.line?.pt) continue
|
||||
const n = sp.line.norm ?? { x: 1, y: 0 }
|
||||
const pos = Math.abs(n.x) >= Math.abs(n.y) ? sp.line.pt.x : sp.line.pt.y
|
||||
ctx.strokeStyle = sel ? '#66aaff' : '#7c828a'
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
if (Math.abs(n.x) >= Math.abs(n.y)) { ctx.moveTo(tx(pos), ty(rb.y0)); ctx.lineTo(tx(pos), ty(rb.y1)) }
|
||||
else { ctx.moveTo(tx(rb.x0), ty(pos)); ctx.lineTo(tx(rb.x1), ty(pos)) }
|
||||
if (Math.abs(n.x) >= Math.abs(n.y)) { ctx.moveTo(tx(sp.line.pt.x), ty(bb.y0)); ctx.lineTo(tx(sp.line.pt.x), ty(bb.y1)) }
|
||||
else { ctx.moveTo(tx(bb.x0), ty(sp.line.pt.y)); ctx.lineTo(tx(bb.x1), ty(sp.line.pt.y)) }
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,16 @@
|
||||
controls-position="right" style="width:104px" @change="onSizeInput" /></span>
|
||||
<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"
|
||||
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"
|
||||
controls-position="right" style="width:92px" @change="onRegionSize" /></span>
|
||||
</template>
|
||||
<template v-else-if="selected">
|
||||
<span class="dim-input">梃位 <el-input-number v-model="mulPos" :min="0" :step="10" size="small"
|
||||
controls-position="right" style="width:92px" @change="onMullionPos" /></span>
|
||||
</template>
|
||||
<el-button size="small" :type="tool === 'v' ? 'primary' : ''" @click="arm('v')">+竖梃</el-button>
|
||||
<el-button size="small" :type="tool === 'h' ? 'primary' : ''" @click="arm('h')">+横梃</el-button>
|
||||
<el-button size="small" :type="selected ? 'danger' : ''" :disabled="!selected" @click="deleteSelected">删除梃</el-button>
|
||||
@@ -58,6 +68,111 @@ const sizeH = ref(0)
|
||||
let resizeDrag: null | { axis: 'w' | 'h' | 'wh'; startW: number; startH: number; startX: number; startY: number } = null
|
||||
let moveDrag: null | { shapeIdx: number; spIdx: number; startPos: number; startX: number; startY: number; snapshot: string } = null
|
||||
const selected = ref<{ shapeIdx: number; spIdx: number } | null>(null)
|
||||
// 细节编辑: 选中的玻璃格(可输入格宽高) ; selRegion 与 selected(梃) 互斥
|
||||
const selRegion = ref<{ shapeIdx: number; ri: number } | null>(null)
|
||||
const regW = ref(0)
|
||||
const regH = ref(0)
|
||||
const mulPos = ref(0)
|
||||
|
||||
/** 选中格 bbox */
|
||||
function regionBb(shapeIdx: number, ri: number) {
|
||||
const r = replays[shapeIdx]?.regions[ri]
|
||||
if (!r) return null
|
||||
const pts = regionPoints(r)
|
||||
return pts.length >= 3 ? bboxOf(pts) : null
|
||||
}
|
||||
|
||||
function syncRegionInputs() {
|
||||
if (!selRegion.value) return
|
||||
const bb = regionBb(selRegion.value.shapeIdx, selRegion.value.ri)
|
||||
if (bb) { regW.value = Math.round(bb.x1 - bb.x0); regH.value = Math.round(bb.y1 - bb.y0) }
|
||||
}
|
||||
|
||||
function syncMullionPos() {
|
||||
if (!selected.value || !dd) return
|
||||
const sp = dd.sm[selected.value.shapeIdx]?.mm?.sp?.[selected.value.spIdx]
|
||||
if (!sp) return
|
||||
const axis = spAxis(sp)
|
||||
mulPos.value = Math.round(spPos(sp) - (axis === 'v' ? modelBox.x0 : modelBox.y0))
|
||||
}
|
||||
|
||||
/** 共线同位组: 同一物理梃被记录为多条共线分段, 移动须成组 */
|
||||
function mullionGroup(shapeIdx: number, axis: 'v' | 'h', pos: number) {
|
||||
return (dd?.sm[shapeIdx]?.mm?.sp ?? []).filter(s => spAxis(s) === axis && Math.abs(spPos(s) - pos) < 0.5)
|
||||
}
|
||||
|
||||
/** 组增量移动(返回实际位移): 限制在组内各段裁剪 bbox 内缩 30mm 的交集 */
|
||||
function moveGroup(shapeIdx: number, axis: 'v' | 'h', pos: number, delta: number): number {
|
||||
const group = mullionGroup(shapeIdx, axis, pos)
|
||||
if (!group.length) return 0
|
||||
let lo = -Infinity, hi = Infinity
|
||||
for (const s of group) {
|
||||
const clip = replays[shapeIdx]?.clips.find(c => c.sp === s)
|
||||
const bb = clip?.bb ?? modelBox
|
||||
if (axis === 'v') { lo = Math.max(lo, bb.x0 + 30); hi = Math.min(hi, bb.x1 - 30) }
|
||||
else { lo = Math.max(lo, bb.y0 + 30); hi = Math.min(hi, bb.y1 - 30) }
|
||||
}
|
||||
if (lo > hi) return 0
|
||||
const applied = Math.min(hi, Math.max(lo, pos + delta)) - pos
|
||||
for (const s of group) {
|
||||
if (axis === 'v') s.line.pt.x += applied
|
||||
else s.line.pt.y += applied
|
||||
}
|
||||
return applied
|
||||
}
|
||||
|
||||
/** 格尺寸编辑: 输入目标宽/高, 平移该格的边界中梃(优先右/下边界, 其次左/上) */
|
||||
function onRegionSize() {
|
||||
if (!selRegion.value || !dd) return
|
||||
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)
|
||||
if (edge) {
|
||||
const sign = Math.abs(spPos(edge) - bb.x1) < 1.5 ? 1 : -1
|
||||
moveGroup(shapeIdx, 'v', spPos(edge), (targetW - curW) * sign)
|
||||
moved = 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)
|
||||
if (edge) {
|
||||
const sign = Math.abs(spPos(edge) - bb.y1) < 1.5 ? 1 : -1
|
||||
moveGroup(shapeIdx, 'h', spPos(edge), (targetH - curH) * sign)
|
||||
moved = true
|
||||
}
|
||||
}
|
||||
if (!moved) { syncRegionInputs(); ElMessage.warning('该格贴外框, 请先加分格梃再调整'); return }
|
||||
replayOne(shapeIdx)
|
||||
computeModelBox()
|
||||
syncSizeInputs()
|
||||
syncRegionInputs()
|
||||
render()
|
||||
emit('change')
|
||||
}
|
||||
|
||||
/** 梃位精确编辑: 距左边缘(竖梃)/距上边缘(横梃)距离 */
|
||||
function onMullionPos() {
|
||||
if (!selected.value || !dd) return
|
||||
const sp = dd.sm[selected.value.shapeIdx]?.mm?.sp?.[selected.value.spIdx]
|
||||
if (!sp) return
|
||||
const axis = spAxis(sp)
|
||||
const target = mulPos.value + (axis === 'v' ? modelBox.x0 : modelBox.y0)
|
||||
const applied = moveGroup(selected.value.shapeIdx, axis, spPos(sp), target - spPos(sp))
|
||||
replayOne(selected.value.shapeIdx)
|
||||
syncMullionPos()
|
||||
render()
|
||||
emit('change')
|
||||
if (Math.abs(applied - (target - spPos(sp))) > 0.5) ElMessage.warning('超出可移动范围')
|
||||
}
|
||||
|
||||
type Replay = ReturnType<typeof replayShape>
|
||||
let replays: Replay[] = []
|
||||
@@ -294,6 +409,25 @@ function drawFrame(
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// 选中玻璃格高亮: 蓝虚线框 + 格尺寸
|
||||
if (selRegion.value && selRegion.value.shapeIdx === shapeIdx) {
|
||||
const r = rp.regions[selRegion.value.ri]
|
||||
if (r) {
|
||||
const bb = bboxOf(regionPoints(r))
|
||||
const x0 = tx(bb.x0), y0 = ty(bb.y0), x1 = tx(bb.x1), y1 = ty(bb.y1)
|
||||
ctx.strokeStyle = '#409eff'
|
||||
ctx.setLineDash([6, 4])
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeRect(x0, y0, x1 - x0, y1 - y0)
|
||||
ctx.setLineDash([])
|
||||
ctx.fillStyle = '#409eff'
|
||||
ctx.font = 'bold 12px sans-serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText(`${Math.round(bb.x1 - bb.x0)} × ${Math.round(bb.y1 - bb.y0)} mm`, (x0 + x1) / 2, y0 - 8)
|
||||
ctx.textAlign = 'left'
|
||||
}
|
||||
}
|
||||
|
||||
// 尺寸标注
|
||||
if (regions.length) {
|
||||
const bb = bboxOf(regions.flat())
|
||||
@@ -355,6 +489,7 @@ function onClick(e: MouseEvent) {
|
||||
const sx = e.clientX - rect.left, sy = e.clientY - rect.top
|
||||
// 加梃模式
|
||||
if (tool.value) {
|
||||
selRegion.value = null
|
||||
const click = toModel(sx, sy)
|
||||
undoStack.push(JSON.stringify(dd))
|
||||
const axis = tool.value
|
||||
@@ -386,9 +521,30 @@ function onClick(e: MouseEvent) {
|
||||
ElMessage.warning('请点击玻璃区域')
|
||||
return
|
||||
}
|
||||
// 选择模式: 点中梃选中/取消
|
||||
// 选择模式: 中梃优先, 其次玻璃格(细节尺寸编辑)
|
||||
const m = hitMullion(sx, sy)
|
||||
selected.value = m
|
||||
if (m) {
|
||||
selRegion.value = null
|
||||
syncMullionPos()
|
||||
render()
|
||||
return
|
||||
}
|
||||
const p = toModel(sx, sy)
|
||||
let hit: { shapeIdx: number; ri: number } | null = null
|
||||
let hitArea = 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 bb = bboxOf(pts)
|
||||
const area = (bb.x1 - bb.x0) * (bb.y1 - bb.y0)
|
||||
if (area < hitArea) { hit = { shapeIdx: si, ri }; hitArea = area }
|
||||
}
|
||||
}
|
||||
selRegion.value = hit
|
||||
if (hit) syncRegionInputs()
|
||||
render()
|
||||
}
|
||||
|
||||
@@ -425,6 +581,7 @@ function undo() {
|
||||
if (!undoStack.length || !dd) return
|
||||
dd = JSON.parse(undoStack.pop()!)
|
||||
selected.value = null
|
||||
selRegion.value = null
|
||||
rebuildReplays()
|
||||
computeModelBox()
|
||||
syncSizeInputs()
|
||||
@@ -506,8 +663,14 @@ function onMove(e: MouseEvent) {
|
||||
const delta = axis === 'v' ? (e.clientX - moveDrag.startX) / scale : (e.clientY - moveDrag.startY) / scale
|
||||
const lo = (axis === 'v' ? bb.x0 : bb.y0) + 30
|
||||
const hi = (axis === 'v' ? bb.x1 : bb.y1) - 30
|
||||
const newPos = Math.min(hi, Math.max(lo, moveDrag.startPos + delta))
|
||||
if (axis === 'v') sp.line.pt.x = newPos; else sp.line.pt.y = newPos
|
||||
const target = Math.min(hi, Math.max(lo, moveDrag.startPos + delta))
|
||||
// 组移动: 先归位到 startPos 再统一平移(幂等), 共线多段同步
|
||||
const cur = spPos(sp)
|
||||
for (const s of mullionGroup(moveDrag.shapeIdx, axis, cur)) {
|
||||
if (axis === 'v') s.line.pt.x = moveDrag.startPos
|
||||
else s.line.pt.y = moveDrag.startPos
|
||||
}
|
||||
moveGroup(moveDrag.shapeIdx, axis, moveDrag.startPos, target - moveDrag.startPos)
|
||||
replayOne(moveDrag.shapeIdx)
|
||||
render()
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
<el-tooltip content="选中单元水平镜像" placement="top">
|
||||
<el-button size="small" :disabled="!selectedUid" @click="canvasRef?.mirrorUnit(selectedUid!)">⇋ 镜像</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="编辑该单元每一格的尺寸与分格" placement="top">
|
||||
<el-button size="small" :disabled="!selectedUid" @click="openDetailEdit">编辑细节</el-button>
|
||||
</el-tooltip>
|
||||
<template v-if="selectedUid">
|
||||
<span style="font-size:12px; color:#909399">尺寸</span>
|
||||
<el-input-number v-model="selSize.w" :min="300" :step="10" size="small" controls-position="right"
|
||||
@@ -94,6 +97,21 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 细节编辑对话框: 复用 WindoorCanvas 编辑器, 保存后写回对应单元 -->
|
||||
<el-dialog v-model="detailVisible" :title="`细节编辑 · ${detailName}`" width="72%" top="4vh" destroy-on-close>
|
||||
<div style="height:62vh">
|
||||
<WindoorCanvas v-if="detailVisible" ref="detailEditRef" :drawing-data="detailData"
|
||||
editable @change="detailDirty = true" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<span style="float:left; font-size:12px; color:#909399">
|
||||
点击玻璃格输入格宽高;点中梃输入位置或拖动;支持加梃/删梃
|
||||
</span>
|
||||
<el-button @click="detailVisible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!detailDirty" @click="saveDetail">保存到单元</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -107,6 +125,7 @@ import { ElMessage } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
import { parseDrawing } from '../types/drawing'
|
||||
import CombineCanvas from '../components/CombineCanvas.vue'
|
||||
import WindoorCanvas from '../components/WindoorCanvas.vue'
|
||||
import WindoorThreed from '../components/WindoorThreed.vue'
|
||||
|
||||
interface UnitMeta { uid: number; name: string; pic: string; templateId: number; w: number; h: number }
|
||||
@@ -299,6 +318,49 @@ function alignBottom() {
|
||||
ElMessage.success('已底边对齐')
|
||||
}
|
||||
|
||||
// ---------- 细节编辑: 对话框内编辑单元每一格的尺寸/分格, 保存写回该单元 ----------
|
||||
const detailVisible = ref(false)
|
||||
const detailUid = ref<number | null>(null)
|
||||
const detailName = ref('')
|
||||
const detailData = ref('')
|
||||
const detailDirty = ref(false)
|
||||
const detailEditRef = ref()
|
||||
|
||||
function openDetailEdit() {
|
||||
if (selectedUid.value === null) return
|
||||
const unitShapes = shapes.value.filter(s => s.__uid === selectedUid.value)
|
||||
if (!unitShapes.length) return
|
||||
detailData.value = JSON.stringify({ s: { cs: 60, cjs: 100 }, cp: [], sm: unitShapes,
|
||||
epi: [], sbf: [], wlc: '', wall: [], scale: 0, note: [] })
|
||||
detailUid.value = selectedUid.value
|
||||
detailName.value = units.value.find(u => u.uid === selectedUid.value)?.name ?? ''
|
||||
detailDirty.value = false
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
function saveDetail() {
|
||||
const json = detailEditRef.value?.exportDrawing()
|
||||
if (!json || detailUid.value === null) return
|
||||
const unit = units.value.find(u => u.uid === detailUid.value)
|
||||
const uid = detailUid.value
|
||||
const patched = (parseDrawing(json).sm as any[]).map(s => ({ ...s, __uid: uid, __name: unit?.name ?? s.__name }))
|
||||
// 原位替换该单元的全部形状(保持单元顺序)
|
||||
const out: any[] = []
|
||||
let replaced = false
|
||||
for (const s of shapes.value) {
|
||||
if (s.__uid !== uid) { out.push(s); continue }
|
||||
if (!replaced) { out.push(...patched); replaced = true }
|
||||
}
|
||||
if (!replaced) out.push(...patched)
|
||||
shapes.value = out
|
||||
rev.value++
|
||||
refreshMeta()
|
||||
syncSelSize()
|
||||
canvasRef.value?.refresh()
|
||||
detailVisible.value = false
|
||||
ElMessage.success('细节已保存到单元')
|
||||
}
|
||||
|
||||
// ---------- 保存 ----------
|
||||
async function saveAsTemplate() {
|
||||
const json = canvasRef.value?.exportDrawing()
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<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 size="small" @click="cancelEdit">取消</el-button>
|
||||
<el-button type="primary" size="small" :disabled="!dirty" @click="saveAsTemplate">另存为我的样板</el-button>
|
||||
|
||||
Reference in New Issue
Block a user