feat: AI生成图持久化记录(AiRenderRecord模型+图片落盘+记录列表/图片/删除端点+前端生成记录抽屉, 三模式统一)
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
<el-radio-button value="quick">风格速览</el-radio-button>
|
||||
<el-radio-button value="blueprint">工程大样图</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div style="flex:1" />
|
||||
<el-button size="small" plain @click="openRecs">生成记录</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -164,13 +166,40 @@
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
|
||||
<!-- 生成记录抽屉(云端生成持久化) -->
|
||||
<el-drawer v-model="recDrawer" title="AI 生成记录" size="480px">
|
||||
<div v-loading="recLoading">
|
||||
<div v-for="r in recs" :key="r.id" class="rec-item">
|
||||
<el-image :src="recUrls[r.id]" fit="cover" class="rec-thumb"
|
||||
:preview-src-list="[recUrls[r.id]]" preview-teleported hide-on-click-modal />
|
||||
<div class="rec-meta">
|
||||
<div class="rec-line">
|
||||
<el-tag size="small" :type="r.mode === 'blueprint' ? 'warning' : (r.mode === 'photo' ? 'success' : 'primary')"
|
||||
effect="plain">{{ modeLabel(r.mode) }}</el-tag>
|
||||
<span v-if="r.template_name" class="rec-tpl">{{ r.template_name }}</span>
|
||||
</div>
|
||||
<div class="rec-dim">{{ r.created_at }} · {{ r.user_name || '—' }}
|
||||
<span v-if="r.width_mm">· {{ r.width_mm }}×{{ r.height_mm }}mm</span></div>
|
||||
<div class="rec-ops">
|
||||
<el-button size="small" text type="primary" @click="downloadRec(r)">下载</el-button>
|
||||
<el-button size="small" text type="danger" @click="removeRec(r)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!recs.length && !recLoading" description="暂无生成记录" />
|
||||
<el-pagination v-if="recTotal > 12" small background layout="total, prev, pager, next"
|
||||
:total="recTotal" :page-size="12" v-model:current-page="recPage" @current-change="loadRecs"
|
||||
style="margin-top:12px; justify-content:center" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
import { api, http } from '../api'
|
||||
import AIRenderCanvas from '../components/AIRenderCanvas.vue'
|
||||
import WindoorCanvas from '../components/WindoorCanvas.vue'
|
||||
import { PHOTO_SCENES, type PhotoScene } from '../components/photoCompose'
|
||||
@@ -327,7 +356,8 @@ async function generateCloud(mode: 'quick' | 'photo' | 'blueprint', sketch_b64?:
|
||||
aiPrompt.value = d.prompt
|
||||
aiFeatures.value = d.features?.summary ?? ''
|
||||
progress.value = 100
|
||||
ElMessage.success(mode === 'blueprint' ? 'AI 大样图已生成' : 'AI 炫图已生成')
|
||||
ElMessage.success((mode === 'blueprint' ? 'AI 大样图已生成' : 'AI 炫图已生成') + ',已存入生成记录')
|
||||
if (recDrawer.value) await loadRecs()
|
||||
} catch { /* 拦截器已提示 */ } finally {
|
||||
window.clearInterval(timer)
|
||||
generating.value = false
|
||||
@@ -483,6 +513,56 @@ async function generateBlueprint(regen = false) {
|
||||
await generateCloud('blueprint', base.split(',')[1], !!sketchImg.value)
|
||||
}
|
||||
|
||||
// ---- 生成记录(云端生成持久化) ----
|
||||
const recDrawer = ref(false)
|
||||
const recLoading = ref(false)
|
||||
const recs = ref<any[]>([])
|
||||
const recTotal = ref(0)
|
||||
const recPage = ref(1)
|
||||
const recUrls = ref<Record<number, string>>({})
|
||||
|
||||
function modeLabel(m: string) {
|
||||
return ({ quick: '风格速览', photo: '照片合成', blueprint: '工程大样图' } as Record<string, string>)[m] ?? m
|
||||
}
|
||||
|
||||
async function openRecs() {
|
||||
recDrawer.value = true
|
||||
await loadRecs()
|
||||
}
|
||||
|
||||
async function loadRecs() {
|
||||
recLoading.value = true
|
||||
try {
|
||||
const d = await api<any>('get', `/api/ai-render/records?page=${recPage.value}&page_size=12`)
|
||||
recs.value = d.data ?? []
|
||||
recTotal.value = d.total ?? 0
|
||||
Object.values(recUrls.value).forEach(u => URL.revokeObjectURL(u))
|
||||
recUrls.value = {}
|
||||
await Promise.all(recs.value.map(async r => {
|
||||
try {
|
||||
const res = await http.get(r.url, { responseType: 'blob' })
|
||||
recUrls.value[r.id] = URL.createObjectURL(res.data)
|
||||
} catch { /* 单图失败不影响列表 */ }
|
||||
}))
|
||||
} finally { recLoading.value = false }
|
||||
}
|
||||
|
||||
function downloadRec(r: any) {
|
||||
const url = recUrls.value[r.id]
|
||||
if (!url) return
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `ai-${r.mode}-${r.id}.png`
|
||||
a.click()
|
||||
}
|
||||
|
||||
async function removeRec(r: any) {
|
||||
await api('delete', `/api/ai-render/records/${r.id}`)
|
||||
ElMessage.success('记录已删除')
|
||||
delete recUrls.value[r.id]
|
||||
await loadRecs()
|
||||
}
|
||||
|
||||
/** 照片模式参考图: 全图压至 ≤1536px JPEG base64 */
|
||||
function photoB64(): string {
|
||||
const img = photo.value!
|
||||
@@ -590,6 +670,13 @@ onMounted(async () => {
|
||||
.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; }
|
||||
.rec-item { display: flex; gap: 10px; padding: 10px 4px; border-bottom: 1px solid #f0f1f3; }
|
||||
.rec-thumb { width: 110px; height: 78px; border-radius: 6px; flex-shrink: 0; cursor: zoom-in; background: #f5f6f8; }
|
||||
.rec-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
||||
.rec-line { display: flex; gap: 6px; align-items: center; }
|
||||
.rec-tpl { font-size: 13px; font-weight: 600; }
|
||||
.rec-dim { font-size: 12px; color: #909399; }
|
||||
.rec-ops { display: flex; gap: 4px; margin-top: auto; }
|
||||
.ai-img { max-width: 100%; max-height: 72vh; border-radius: 10px; box-shadow: 0 2px 14px rgba(0,0,0,.14); }
|
||||
.ai-ops { margin-top: 8px; }
|
||||
.prompt-box { margin-top: 10px; text-align: left; }
|
||||
|
||||
Reference in New Issue
Block a user