feat: AI生成工程大样图(整合autocad项目大样图提示词资产, blueprint模式2D图纸转黑白CAD大样图, 修复loadTemplate不回写templateId)
This commit is contained in:
@@ -1008,6 +1008,13 @@ function exportDrawing(): string | null {
|
||||
return dd ? JSON.stringify(dd) : null
|
||||
}
|
||||
|
||||
/** 画布截图(dataURL):供 AI 大样图等以 2D 图纸为底图的场景。 */
|
||||
function exportImage(): string | null {
|
||||
const cv = cvRef.value
|
||||
if (!cv || !cv.width) return null
|
||||
try { return cv.toDataURL('image/png') } catch { return null }
|
||||
}
|
||||
|
||||
// ---- 装饰格条设计(P0-格条设计器) ----
|
||||
const DEC_COLORS = [
|
||||
{ label: '黑', v: '#2b2b2b' }, { label: '白', v: '#ffffff' }, { label: '灰', v: '#9aa0a6' },
|
||||
@@ -1120,7 +1127,7 @@ function removeAssign() {
|
||||
emit('change')
|
||||
ElMessage.success('已清除该格指定')
|
||||
}
|
||||
defineExpose({ exportDrawing })
|
||||
defineExpose({ exportDrawing, exportImage })
|
||||
|
||||
// ============ 视图控制 ============
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<el-radio-group v-model="tab" size="small">
|
||||
<el-radio-button value="photo">照片合成</el-radio-button>
|
||||
<el-radio-button value="quick">风格速览</el-radio-button>
|
||||
<el-radio-button value="blueprint">工程大样图</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
@@ -53,7 +54,7 @@
|
||||
</template>
|
||||
|
||||
<!-- ============ 风格速览(原程序化模式) ============ -->
|
||||
<template v-else>
|
||||
<template v-else-if="tab === 'quick'">
|
||||
<div class="controls">
|
||||
<el-select v-model="templateId" filterable placeholder="选择样板" style="width:220px" @change="loadTemplate">
|
||||
<el-option v-for="t in templates" :key="t.id" :label="t.name || `样板#${t.id}`" :value="t.id" />
|
||||
@@ -83,6 +84,25 @@
|
||||
<el-empty v-if="!drawing" description="请先选择样板" />
|
||||
</template>
|
||||
|
||||
<!-- ============ 工程大样图(2D 图纸 → 黑白 CAD 大样图, 整合自 autocad 项目草图标准化能力) ============ -->
|
||||
<template v-else-if="tab === 'blueprint'">
|
||||
<div class="controls">
|
||||
<el-select v-model="templateId" filterable placeholder="选择样板" style="width:220px" @change="loadTemplate">
|
||||
<el-option v-for="t in templates" :key="t.id" :label="t.name || `样板#${t.id}`" :value="t.id" />
|
||||
</el-select>
|
||||
<span class="tip">以 2D 图纸为底图,AI 重绘为黑白 CAD 工程大样图(含尺寸线/标题栏,可作报价与合同附件)</span>
|
||||
<div style="flex:1" />
|
||||
<el-button type="primary" :disabled="!drawing || generating" :loading="generating" @click="generateBlueprint">
|
||||
{{ generating ? 'AI 绘制中…' : '✦ AI 生成大样图' }}</el-button>
|
||||
</div>
|
||||
<el-progress v-if="generating" :percentage="progress" :stroke-width="10" style="margin: 10px 0"
|
||||
:format="() => stageText" />
|
||||
<div v-if="drawing" class="bp-stage">
|
||||
<WindoorCanvas ref="bpCanvasRef" :drawing-data="drawing" :editable="false" />
|
||||
</div>
|
||||
<el-empty v-else description="请先选择样板" />
|
||||
</template>
|
||||
|
||||
<div v-if="variants.length" class="gallery-foot">
|
||||
<span class="tip">本地 4 方案(程序化渲染降级模式)· 点击图标下载</span>
|
||||
</div>
|
||||
@@ -109,9 +129,11 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
import AIRenderCanvas from '../components/AIRenderCanvas.vue'
|
||||
import WindoorCanvas from '../components/WindoorCanvas.vue'
|
||||
import { PHOTO_SCENES, type PhotoScene } from '../components/photoCompose'
|
||||
|
||||
const tab = ref<'photo' | 'quick'>('photo')
|
||||
const tab = ref<'photo' | 'quick' | 'blueprint'>('photo')
|
||||
const bpCanvasRef = ref()
|
||||
const templates = ref<any[]>([])
|
||||
const templateId = ref<number | undefined>()
|
||||
const drawing = ref('')
|
||||
@@ -142,9 +164,10 @@ let dragStart: { x: number; y: number } | null = null
|
||||
async function loadTemplate(tid: number | undefined) {
|
||||
drawing.value = ''
|
||||
variants.value = []
|
||||
if (!tid) return
|
||||
if (!tid) { templateId.value = undefined; return }
|
||||
const t = await api<any>('get', `/api/windoor/templates/${tid}`)
|
||||
drawing.value = t.drawing_data
|
||||
templateId.value = tid
|
||||
}
|
||||
|
||||
// ---- 照片上传与框选 ----
|
||||
@@ -229,14 +252,14 @@ const aiImage = ref('')
|
||||
const aiPrompt = ref('')
|
||||
const aiFeatures = ref('')
|
||||
|
||||
async function generateCloud(mode: 'quick' | 'photo') {
|
||||
async function generateCloud(mode: 'quick' | 'photo' | 'blueprint', sketch_b64?: string) {
|
||||
if (!drawing.value) return
|
||||
if (mode === 'photo' && (!photo.value || !rect.value)) return
|
||||
generating.value = true
|
||||
aiImage.value = ''
|
||||
variants.value = []
|
||||
progress.value = 8
|
||||
stageText.value = mode === 'photo' ? '上传照片与窗型特征' : '解析窗型结构'
|
||||
stageText.value = mode === 'photo' ? '上传照片与窗型特征' : mode === 'blueprint' ? '上传 2D 图纸与分格特征' : '解析窗型结构'
|
||||
const timer = window.setInterval(() => {
|
||||
progress.value = Math.min(92, progress.value + 2)
|
||||
if (progress.value > 40) stageText.value = 'AI 绘制中(约 30~90 秒, 请勿关闭页面)'
|
||||
@@ -254,13 +277,13 @@ async function generateCloud(mode: 'quick' | 'photo') {
|
||||
const d = await api<any>('post', '/api/ai-render/generate', {
|
||||
template_id: templateId.value, mode,
|
||||
style: form.ai_style, time: form.ai_time,
|
||||
scene: mode === 'photo' ? scene.value : form.scene, photo_b64, rect: rectPayload,
|
||||
scene: mode === 'photo' ? scene.value : form.scene, photo_b64, rect: rectPayload, sketch_b64,
|
||||
}, { timeout: 200000 })
|
||||
aiImage.value = `data:image/png;base64,${d.image_b64}`
|
||||
aiPrompt.value = d.prompt
|
||||
aiFeatures.value = d.features?.summary ?? ''
|
||||
progress.value = 100
|
||||
ElMessage.success('AI 炫图已生成')
|
||||
ElMessage.success(mode === 'blueprint' ? 'AI 大样图已生成' : 'AI 炫图已生成')
|
||||
} catch { /* 拦截器已提示 */ } finally {
|
||||
window.clearInterval(timer)
|
||||
generating.value = false
|
||||
@@ -268,6 +291,14 @@ async function generateCloud(mode: 'quick' | 'photo') {
|
||||
}
|
||||
}
|
||||
|
||||
/** 工程大样图: 以 2D 图纸画布截图为底图, AI 重绘为黑白 CAD 大样图 */
|
||||
async function generateBlueprint() {
|
||||
await nextTick()
|
||||
const dataUrl = bpCanvasRef.value?.exportImage?.()
|
||||
if (!dataUrl) { ElMessage.warning('图纸截图失败,请稍后重试'); return }
|
||||
await generateCloud('blueprint', dataUrl.split(',')[1])
|
||||
}
|
||||
|
||||
/** 照片模式参考图: 全图压至 ≤1536px JPEG base64 */
|
||||
function photoB64(): string {
|
||||
const img = photo.value!
|
||||
@@ -344,6 +375,8 @@ onMounted(async () => {
|
||||
templates.value = (await api<any>('get', '/api/windoor/templates?page=1&page_size=254')).data
|
||||
Object.assign(form, { ai_style: form.ai_style, ai_time: form.ai_time, scene: form.scene },
|
||||
await api<any>('get', '/api/rendering-info'))
|
||||
// 自动选中首个样板, 三种模式即刻有底图
|
||||
if (templates.value.length && !templateId.value) await loadTemplate(templates.value[0].id)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -358,11 +391,11 @@ onMounted(async () => {
|
||||
.drop-hint:hover { border-color: #5b3fd4; background: #faf8ff; }
|
||||
.drop-hint p { margin: 10px 0 0; }
|
||||
.drop-hint .sub { font-size: 12px; }
|
||||
.photo-stage { position: relative; display: flex; justify-content: center; }
|
||||
.photo-cv { max-width: 100%; border-radius: 10px; cursor: crosshair; box-shadow: 0 2px 12px rgba(0,0,0,.12); }
|
||||
.photo-stage { position: relative; display: flex; justify-content: center; }.photo-cv { max-width: 100%; border-radius: 10px; cursor: crosshair; box-shadow: 0 2px 12px rgba(0,0,0,.12); }
|
||||
.stage-tip { position: absolute; left: 12px; top: 12px; color: #fff; font-size: 12px;
|
||||
background: rgba(0,0,0,0.5); padding: 4px 10px; border-radius: 10px; pointer-events: none; }
|
||||
.preview { height: 58vh; }
|
||||
.bp-stage { height: 58vh; border: 1px solid #e4e7ed; border-radius: 6px; overflow: hidden; }
|
||||
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; margin-top: 14px; }
|
||||
.gallery-foot { margin-top: 8px; }
|
||||
.ai-result { margin-top: 14px; text-align: center; }
|
||||
|
||||
Reference in New Issue
Block a user