Files
menchuang/frontend/src/views/TemplatesView.vue
T
HHY 6ec30c198f feat: v0.5 画布尺寸编辑+自动报价+扇样式保真修复
- 参数化尺寸编辑: W/H输入框 + 拖拽外框右缘/下缘/右下角手柄
- 全部几何按比例自动缩放(型材料宽不变, 最小300mm)
- 拖拽过程静默更新, 松手提交或回滚撤销点
- 自动报价: 画布变化600ms防抖调用offer-price, 实时显示总价
- 修复样板显示为默认矩形的问题: 扇样式推断引擎三级解析
  (有效bd → polyId区域映射双扇对半 → 帧bbox按扇数竖向均分)
- 还原真实分格与开向(od角度/inward空心/门标注)
- 无图纸订单显示引导提示
2026-09-20 10:39:54 -07:00

155 lines
6.4 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-select v-model="catelogId" placeholder="全部分类" clearable style="width:160px" @change="load">
<el-option v-for="c in catelogs" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
<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 }} 套官方样板</span>
</div>
<div v-loading="loading" class="grid">
<div v-for="t in rows" :key="t.id" class="item" @click="preview(t)">
<el-image :src="t.pic_url" fit="contain" lazy class="pic" />
<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)
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}${catelogId.value ? `&catelog_id=${catelogId.value}` : ''}`
const d = await api<any>('get', `/api/windoor/templates?${qs}`)
rows.value = d
// 接口无分页元数据时按返回量估计
total.value = d.length === page_size ? (page.value - 1) * page_size + d.length + 1 : (page.value - 1) * page_size + d.length
} 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 () => {
const tree = await api<any[]>('get', '/api/windoor/catelogs')
catelogs.value = tree.flatMap(t => [t, ...((t.children ?? []) as any[])])
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; }
.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>