Files
menchuang/frontend/src/views/TemplatesView.vue
T
HHY b6d7db58d6 fix: v2.5 浏览器级可视化验证与修复闭环
- 新增 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/工作台全部正常无报错
2026-09-21 15:02:06 -07:00

163 lines
6.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>