Files
menchuang/frontend/src/views/TemplatesView.vue
T
HHY df96bd0322 feat: v1.1 Element Plus 组件全面升级
- 布局: 面包屑/头像下拉/公告气泡(点击外部关闭)/菜单滚动条/路由切换动效
- 工作台: el-statistic 彩色统计卡/el-skeleton 骨架屏/公告轮播/快捷入口
- 订单: 日期范围筛选/列排序/详情抽屉 el-descriptions+el-tabs+el-timeline 日志
- 样板库: el-tree-select 分类树/el-image 大图预览/el-badge 引用徽标
- 客户表单校验规则/优化下料 dashboard 利用率进度环
- 全接口回归 13/13 通过, SPA 路由验证通过
2026-09-20 13:42:22 -07:00

164 lines
7.0 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
// 接口无分页元数据时按返回量估计
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 () => {
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>