- 新增 ui-shots.mjs/open-shot.mjs: puppeteer-core 驱动系统 Edge 无头截图 + 页面 console/pageerror 捕获 - 修复 3D 解析失败: watch immediate 首跑 camera 未创建 - 修复 inferSashGlyphs 兜底 bboxOf 传段对象(286 NaN 扇矩形→0) - profileGeo 自交轮廓重写为简单多边形 - 样板接口返回 total, 前端消费方适配(修复计数 49) - 等高对齐落地到加入时(组合画布两单元同高底对齐) - 渲染页默认模式随路由+自动选中首样板+选择器回填 - router 支持 token 直通(开发便捷) - shadowMap.type 移除(three r186 已移除 PCFSoft) - 综合截图验证: 样板库/组合/3D/工作台全部正常无报错
163 lines
6.9 KiB
Vue
163 lines
6.9 KiB
Vue
<template>
|
||
<div>
|
||
<el-card>
|
||
<div class="toolbar">
|
||
<el-radio-group v-model="tab" size="small" @change="load">
|
||
<el-radio-button value="official">官方样板</el-radio-button>
|
||
<el-radio-button value="mine">我的样板</el-radio-button>
|
||
</el-radio-group>
|
||
<el-tree-select v-model="catelogId" :data="catelogs" node-key="id"
|
||
:props="{ label: 'name', children: 'children' }" check-strictly default-expand-all
|
||
clearable style="width:170px" placeholder="全部分类" @change="load" />
|
||
<el-input v-model="name" placeholder="模板名称" clearable style="width:200px" @keyup.enter="load" />
|
||
<el-button type="primary" @click="load">查询</el-button>
|
||
<div style="flex:1" />
|
||
<span class="count">共 {{ total }} 套{{ tab === 'mine' ? '我的' : '官方' }}样板</span>
|
||
</div>
|
||
<div v-loading="loading" class="grid">
|
||
<div v-for="t in rows" :key="t.id" class="item" @click="preview(t)">
|
||
<el-badge :value="t.times ? `${t.times}次` : ''" :hidden="!t.times" class="bdg">
|
||
<el-image :src="t.pic_url" fit="contain" lazy class="pic"
|
||
:preview-src-list="[t.pic_url]" preview-teleported :hide-on-click-modal="true"
|
||
@click.stop />
|
||
</el-badge>
|
||
<div class="cap">{{ t.name || `样板#${t.id}` }}</div>
|
||
</div>
|
||
</div>
|
||
<el-pagination style="margin-top:12px; justify-content:center" background layout="total, prev, pager, next"
|
||
:total="total" :page-size="page_size" v-model:current-page="page" @current-change="load" />
|
||
</el-card>
|
||
|
||
<el-dialog v-model="previewVisible" :title="current?.name || `样板#${current?.id}`" width="72%" top="4vh">
|
||
<div v-if="current" class="preview">
|
||
<div class="canvas-box">
|
||
<WindoorCanvas ref="editRef" :drawing-data="current.drawing_data"
|
||
:editable="editing" @change="onCanvasChange" />
|
||
</div>
|
||
<div class="meta">
|
||
<template v-if="!editing">
|
||
<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 type="primary" size="small" @click="useInOrder">用此样板下单</el-button>
|
||
</template>
|
||
<template v-else>
|
||
<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>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
<QuoteDialog ref="quoteRef" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onMounted, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import { api } from '../api'
|
||
import WindoorCanvas from '../components/WindoorCanvas.vue'
|
||
import QuoteDialog from '../components/QuoteDialog.vue'
|
||
|
||
const router = useRouter()
|
||
const quoteRef = ref()
|
||
const rows = ref<any[]>([])
|
||
const total = ref(0)
|
||
const page = ref(1)
|
||
const page_size = 48
|
||
const name = ref('')
|
||
const catelogId = ref<number | undefined>()
|
||
const catelogs = ref<any[]>([])
|
||
const loading = ref(false)
|
||
const previewVisible = ref(false)
|
||
const current = ref<any>(null)
|
||
const editing = ref(false)
|
||
const dirty = ref(false)
|
||
const editRef = ref()
|
||
const liveTotal = ref<number | null>(null)
|
||
const tab = ref<'official' | 'mine'>('official')
|
||
let quoteTimer: number | undefined
|
||
|
||
// 画布变化(改尺寸/加分格)→ 防抖自动重算报价
|
||
function onCanvasChange() {
|
||
dirty.value = true
|
||
clearTimeout(quoteTimer)
|
||
quoteTimer = window.setTimeout(async () => {
|
||
const drawing = editRef.value?.exportDrawing()
|
||
if (!drawing) return
|
||
try {
|
||
const r = await api<any>('post', '/api/offer-price', { drawing_data: drawing })
|
||
liveTotal.value = r.total
|
||
} catch { /* 报价失败不打断编辑 */ }
|
||
}, 600)
|
||
}
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
const qs = `page=${page.value}&page_size=${page_size}&name=${name.value}&is_default=${tab.value === 'official' ? 1 : 0}${catelogId.value ? `&catelog_id=${catelogId.value}` : ''}`
|
||
const d = await api<any>('get', `/api/windoor/templates?${qs}`)
|
||
rows.value = d.data
|
||
total.value = d.total
|
||
} finally { loading.value = false }
|
||
}
|
||
|
||
async function preview(t: any) {
|
||
current.value = await api<any>('get', `/api/windoor/templates/${t.id}`)
|
||
editing.value = false
|
||
dirty.value = false
|
||
liveTotal.value = null
|
||
previewVisible.value = true
|
||
}
|
||
|
||
function cancelEdit() {
|
||
editing.value = false
|
||
dirty.value = false
|
||
// 重新拉取以还原画布
|
||
const id = current.value.id
|
||
api<any>('get', `/api/windoor/templates/${id}`).then(d => (current.value = d))
|
||
}
|
||
|
||
async function saveAsTemplate() {
|
||
const drawing = editRef.value?.exportDrawing()
|
||
if (!drawing) return
|
||
const base = current.value.name || `样板${current.value.id}`
|
||
const saved = await api<any>('post', '/api/windoor/templates', {
|
||
name: `${base}-改`, catelog_id: current.value.catelog_id,
|
||
drawing_data: drawing, note: `基于样板#${current.value.id}修改`,
|
||
})
|
||
ElMessage.success(`已另存为我的样板 #${saved.id}`)
|
||
editing.value = false
|
||
dirty.value = false
|
||
}
|
||
|
||
function useInOrder() {
|
||
router.push({ path: '/orders', query: { template: current.value.id } })
|
||
}
|
||
|
||
onMounted(async () => {
|
||
catelogs.value = await api<any[]>('get', '/api/windoor/catelogs')
|
||
load()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.toolbar { display: flex; gap: 10px; margin-bottom: 12px; align-items: center; }
|
||
.count { color: #909399; font-size: 13px; }
|
||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; min-height: 200px; }
|
||
.item { border: 1px solid #e4e7ed; border-radius: 8px; padding: 8px; cursor: pointer; background: #fff;
|
||
transition: box-shadow .2s; display: flex; flex-direction: column; align-items: center; }
|
||
.item:hover { box-shadow: 0 2px 12px rgba(0,0,0,.12); }
|
||
.pic { width: 100%; height: 110px; }
|
||
.bdg :deep(.el-badge__content) { z-index: 3; }
|
||
.cap { font-size: 12px; color: #606266; margin-top: 6px; text-align: center; }
|
||
.preview .canvas-box { height: 60vh; border: 1px solid #e4e7ed; border-radius: 6px; overflow: hidden; }
|
||
.preview .meta { display: flex; gap: 20px; margin-top: 10px; color: #606266; font-size: 13px; align-items: center; flex-wrap: wrap; }
|
||
.tip { color: #909399; }
|
||
.live-price { color: #e6483d; font-size: 16px; font-weight: 700; }
|
||
</style>
|