feat: v1.3 样板组合升级为画布形式
- 新组件 CombineCanvas: 交互对齐原站画图编辑器 库面板可拖拽(dragstart/drop) → 画布落点摆放 自由拖移(同uid单元整体移动) + 边缘吸附对齐(12px容差+红色辅助线) 点选高亮/底边对齐/Del键删除/平移缩放/双击复位 - CombineView 重构: 拖放+点击双入口, 保留2D编辑与3D预览切换 - 另存我的样板时剥离内部标记字段, 保持标准 drawing_data 格式 - 原'顺序拼接'模式由画布自由摆放取代
This commit is contained in:
+158
-154
@@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<el-row :gutter="16" class="combine-page">
|
||||
<!-- 左:样板库 -->
|
||||
<el-col :xs="24" :md="6">
|
||||
<el-card header="样板库(点击加入组合)">
|
||||
<!-- 左:样板库(可拖拽) -->
|
||||
<el-col :xs="24" :md="5">
|
||||
<el-card header="样板库(拖入画布或点击加入)">
|
||||
<el-input v-model="keyword" placeholder="搜索样板" clearable size="small" @keyup.enter="loadLib" @clear="loadLib">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-scrollbar height="56vh" style="margin-top:10px">
|
||||
<div class="lib-grid">
|
||||
<div v-for="t in lib" :key="t.id" class="lib-item" @click="addTemplate(t)">
|
||||
<div v-for="t in lib" :key="t.id" class="lib-item" draggable="true"
|
||||
@dragstart="onDragStart($event, t)" @click="addTemplate(t)">
|
||||
<el-image :src="t.pic_url" fit="contain" lazy class="lib-pic" />
|
||||
<span class="lib-name">{{ t.name || `#${t.id}` }}</span>
|
||||
</div>
|
||||
@@ -17,253 +18,256 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 中:组合预览 -->
|
||||
<el-col :xs="24" :md="12">
|
||||
<!-- 中:组合画布 -->
|
||||
<el-col :xs="24" :md="13">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="head-row">
|
||||
<span>组合预览({{ items.length }} 个单元)</span>
|
||||
<span>组合画布({{ units.length }} 个单元 · 拖动摆放 · 边缘自动吸附)</span>
|
||||
<div style="display:flex; gap:8px; align-items:center">
|
||||
<el-tooltip content="开启后,新加入的样板自动缩放到组合高度" placement="top">
|
||||
<span style="font-size:12px; color:#909399">等高对齐</span>
|
||||
</el-tooltip>
|
||||
<el-switch v-model="equalHeight" />
|
||||
<el-radio-group v-model="viewMode" size="small">
|
||||
<el-radio-button value="2d">2D</el-radio-button>
|
||||
<el-radio-button value="3d">3D</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button size="small" :disabled="!items.length" @click="clearAll">清空</el-button>
|
||||
<el-button size="small" :disabled="!selectedUid" @click="alignBottom">底边对齐</el-button>
|
||||
<el-button size="small" :disabled="!selectedUid" type="danger" plain @click="deleteSelected">删除单元</el-button>
|
||||
<el-button size="small" :disabled="!shapes.length" @click="clearAll">清空</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="canvas-box">
|
||||
<WindoorCanvas v-if="combinedJson && viewMode === '2d'" :drawing-data="combinedJson" />
|
||||
<WindoorThreed v-else-if="combinedJson && viewMode === '3d'" :drawing-data="combinedJson" />
|
||||
<el-empty v-else description="从左侧样板库点击加入样板开始组合" />
|
||||
<div class="canvas-box" @drop="onDrop" style="position:relative">
|
||||
<CombineCanvas ref="canvasRef" :shapes="shapes" @select="onSelect" @change="refreshMeta" />
|
||||
</div>
|
||||
<!-- 保存 -->
|
||||
<div class="save-bar">
|
||||
<el-input v-model="saveName" placeholder="组合样板名称(如:三扇组合窗)" style="flex:1" />
|
||||
<el-button type="primary" :disabled="!items.length || !saveName.trim()" :loading="saving"
|
||||
<el-button type="primary" :disabled="!shapes.length || !saveName.trim()" :loading="saving"
|
||||
@click="saveAsTemplate">另存为我的样板</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右:组合清单 -->
|
||||
<!-- 右:单元清单 + 3D 预览 -->
|
||||
<el-col :xs="24" :md="6">
|
||||
<el-card header="组合单元">
|
||||
<el-empty v-if="!items.length" description="尚未加入样板" :image-size="60" />
|
||||
<el-empty v-if="!units.length" description="拖入或点击样板开始" :image-size="60" />
|
||||
<transition-group name="list" tag="div">
|
||||
<div v-for="(it, i) in items" :key="it.uid" class="unit">
|
||||
<el-image :src="it.pic" fit="contain" class="unit-pic" />
|
||||
<div v-for="u in units" :key="u.uid" class="unit" :class="{ pick: u.uid === selectedUid }"
|
||||
@click="selectUnit(u.uid)">
|
||||
<el-image :src="u.pic" fit="contain" class="unit-pic" />
|
||||
<div class="unit-info">
|
||||
<div class="unit-name">{{ it.name }}</div>
|
||||
<div class="unit-size">{{ it.w }} × {{ it.h }} mm</div>
|
||||
</div>
|
||||
<div class="unit-ops">
|
||||
<el-button size="small" text :disabled="i === 0" @click="move(i, -1)"><el-icon><ArrowLeft /></el-icon></el-button>
|
||||
<el-button size="small" text :disabled="i === items.length - 1" @click="move(i, 1)"><el-icon><ArrowRight /></el-icon></el-button>
|
||||
<el-button size="small" text type="danger" @click="removeAt(i)"><el-icon><Delete /></el-icon></el-button>
|
||||
<div class="unit-name">{{ u.name }}</div>
|
||||
<div class="unit-size">{{ u.w }} × {{ u.h }} mm</div>
|
||||
</div>
|
||||
<el-button size="small" text type="danger" @click.stop="deleteUnit(u.uid)"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
</transition-group>
|
||||
</el-card>
|
||||
<el-card header="3D 预览" style="margin-top:16px">
|
||||
<div class="threed-box">
|
||||
<WindoorThreed v-if="shapes.length" :drawing-data="combinedJson" />
|
||||
<el-empty v-else description="—" :image-size="50" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 样板组合工场:把样板库中的窗型作为子组件拼接为组合窗。
|
||||
* 产物为标准 drawing_data(多形状 sm[],紧贴排布、可等高对齐),
|
||||
* 与渲染/报价/AI 炫图/3D 全链路兼容;可另存为"我的样板"用于下单。
|
||||
* 样板组合工场(画布形式,交互对齐原站画图编辑器:库面板拖拽 → 画布摆放 → 自由拖移 + 边缘吸附)
|
||||
* 数据模型:shapes = 多形状 drawing_data 的 sm[](每形状打 __uid 单元标记);
|
||||
* 组合结果与渲染/报价/AI 炫图/3D 全链路兼容,可另存为"我的样板"。
|
||||
*/
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, onBeforeUnmount, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { api } from '../api'
|
||||
import { parseDrawing, type Segment } from '../types/drawing'
|
||||
import WindoorCanvas from '../components/WindoorCanvas.vue'
|
||||
import { parseDrawing } from '../types/drawing'
|
||||
import CombineCanvas from '../components/CombineCanvas.vue'
|
||||
import WindoorThreed from '../components/WindoorThreed.vue'
|
||||
|
||||
const viewMode = ref<'2d' | '3d'>('2d')
|
||||
|
||||
interface CombineItem {
|
||||
uid: number
|
||||
templateId: number
|
||||
name: string
|
||||
pic: string
|
||||
raw: any // 原始 drawing_data 对象
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
interface UnitMeta { uid: number; name: string; pic: string; templateId: number }
|
||||
|
||||
const lib = ref<any[]>([])
|
||||
const keyword = ref('')
|
||||
const items = ref<CombineItem[]>([])
|
||||
const equalHeight = ref(true)
|
||||
const shapes = ref<any[]>([])
|
||||
const units = ref<UnitMeta[]>([])
|
||||
const selectedUid = ref<number | null>(null)
|
||||
const saveName = ref('')
|
||||
const saving = ref(false)
|
||||
const canvasRef = ref()
|
||||
let uidSeq = 1
|
||||
|
||||
const combinedJson = computed(() =>
|
||||
shapes.value.length ? JSON.stringify({ s: { cs: 60, cjs: 100 }, cp: [], sm: shapes.value,
|
||||
epi: [], sbf: [], wlc: '', wall: [], scale: 0, note: [] }) : '')
|
||||
|
||||
// ---------- 样板库 ----------
|
||||
async function loadLib() {
|
||||
lib.value = await api<any>('get', `/api/windoor/templates?page=1&page_size=254&is_default=1&name=${keyword.value}`)
|
||||
}
|
||||
|
||||
// ---------- 几何工具 ----------
|
||||
function shapePts(shape: any): { x: number; y: number }[] {
|
||||
const out: { x: number; y: number }[] = []
|
||||
for (const r of (shape.polygon?.polygon ?? [])) {
|
||||
for (const seg of r) {
|
||||
if (seg.ps && Number.isFinite(seg.ps.x)) out.push(seg.ps)
|
||||
if (seg.pe && Number.isFinite(seg.pe.x)) out.push(seg.pe)
|
||||
}
|
||||
}
|
||||
return out
|
||||
// ---------- 加入样板 ----------
|
||||
function shapesOf(detail: any): any[] {
|
||||
const dd = parseDrawing(detail.drawing_data)
|
||||
return dd.sm.map(s => JSON.parse(JSON.stringify(s)))
|
||||
}
|
||||
|
||||
function groupBBox(shapes: any[]) {
|
||||
const pts = shapes.flatMap(shapePts)
|
||||
function bbOf(shapes: any[]) {
|
||||
const pts = shapes.flatMap(s => (s.polygon?.polygon ?? []).flat()
|
||||
.flatMap((seg: any) => [seg.ps, seg.pe]).filter((p: any) => p && Number.isFinite(p.x)))
|
||||
if (!pts.length) return null
|
||||
const xs = pts.map(p => p.x), ys = pts.map(p => p.y)
|
||||
return { x0: Math.min(...xs), y0: Math.min(...ys), x1: Math.max(...xs), y1: Math.max(...ys) }
|
||||
}
|
||||
|
||||
function translateShape(shape: any, dx: number, dy: number) {
|
||||
function translate(shapes: any[], dx: number, dy: number) {
|
||||
const mv = (p: any) => { p.x += dx; p.y += dy }
|
||||
for (const r of (shape.polygon?.polygon ?? [])) {
|
||||
for (const seg of r as Segment[]) { mv(seg.ps); mv(seg.pe) }
|
||||
}
|
||||
for (const sp of (shape.mm?.sp ?? [])) mv(sp.line.pt)
|
||||
for (const sub of (shape.sm ?? [])) {
|
||||
if (sub.bd) { mv(sub.bd.from); mv(sub.bd.to) }
|
||||
for (const s of shapes) {
|
||||
for (const r of (s.polygon?.polygon ?? [])) for (const seg of r) { mv(seg.ps); mv(seg.pe) }
|
||||
for (const sp of (s.mm?.sp ?? [])) mv(sp.line.pt)
|
||||
for (const sub of (s.sm ?? [])) if (sub.bd) { mv(sub.bd.from); mv(sub.bd.to) }
|
||||
}
|
||||
}
|
||||
|
||||
function scaleShapeY(shape: any, factor: number, anchorY: number) {
|
||||
const sc = (p: any) => { p.y = anchorY + (p.y - anchorY) * factor }
|
||||
for (const r of (shape.polygon?.polygon ?? [])) {
|
||||
for (const seg of r as Segment[]) { sc(seg.ps); sc(seg.pe) }
|
||||
}
|
||||
for (const sp of (shape.mm?.sp ?? [])) sc(sp.line.pt)
|
||||
for (const sub of (shape.sm ?? [])) {
|
||||
if (sub.bd) { sc(sub.bd.from); sc(sub.bd.to) }
|
||||
}
|
||||
function registerUnit(detail: any, uid: number, name: string) {
|
||||
units.value = [...units.value, { uid, name, pic: detail.pic_url || '', templateId: detail.id }]
|
||||
}
|
||||
|
||||
// ---------- 组合与排布 ----------
|
||||
function buildCombined(): any | null {
|
||||
if (!items.value.length) return null
|
||||
// 深拷贝各单元形状
|
||||
const groups = items.value.map(it => {
|
||||
const dd = parseDrawing(JSON.parse(JSON.stringify(it.raw)))
|
||||
return dd.sm.map(s => JSON.parse(JSON.stringify(s)))
|
||||
})
|
||||
// 参考高度:第一个单元
|
||||
const bb0 = groupBBox(groups[0])
|
||||
if (!bb0) return null
|
||||
const refH = bb0.y1 - bb0.y0
|
||||
|
||||
const combined: any[] = []
|
||||
let xCursor = 0
|
||||
groups.forEach((shapes, gi) => {
|
||||
const bb = groupBBox(shapes)!
|
||||
// 等高对齐:以参考高度缩放 Y(锚点取该组最低点)
|
||||
if (equalHeight.value && groups.length > 1 && bb.y1 - bb.y0 > 0) {
|
||||
const f = refH / (bb.y1 - bb.y0)
|
||||
for (const s of shapes) scaleShapeY(s, f, bb.y0)
|
||||
}
|
||||
const nb = groupBBox(shapes)!
|
||||
// 平移:x 紧贴排布,y 底边对齐参考底边
|
||||
const dx = xCursor - nb.x0
|
||||
const dy = bb0.y0 - nb.y0
|
||||
for (const s of shapes) translateShape(s, dx, dy)
|
||||
combined.push(...shapes)
|
||||
xCursor += nb.x1 - nb.x0
|
||||
})
|
||||
return {
|
||||
s: { cs: 60, cjs: 100 },
|
||||
cp: [], sm: combined, epi: [], sbf: [], wlc: '', wall: [], scale: 0, note: [],
|
||||
function addShapesAt(detail: any, cx: number | null, cy: number | null, name: string) {
|
||||
const uid = uidSeq++
|
||||
const newShapes = shapesOf(detail)
|
||||
if (!newShapes.length) { ElMessage.warning('该样板没有可用的几何数据'); return false }
|
||||
const bb = bbOf(newShapes)!
|
||||
let dx = 0, dy = 0
|
||||
if (cx !== null && cy !== null) {
|
||||
dx = cx - (bb.x0 + bb.x1) / 2
|
||||
dy = cy - (bb.y0 + bb.y1) / 2
|
||||
} else {
|
||||
const cur = bbOf(shapes.value)
|
||||
dx = cur ? cur.x1 - bb.x0 : 0
|
||||
dy = cur ? cur.y0 - bb.y0 : 0
|
||||
}
|
||||
translate(newShapes, dx, dy)
|
||||
for (const s of newShapes) { s.__uid = uid; s.__name = name }
|
||||
shapes.value = [...shapes.value, ...newShapes]
|
||||
registerUnit(detail, uid, name)
|
||||
canvasRef.value?.refresh()
|
||||
refreshMeta()
|
||||
return true
|
||||
}
|
||||
|
||||
const combinedJson = computed(() => {
|
||||
const dd = buildCombined()
|
||||
return dd ? JSON.stringify(dd) : ''
|
||||
})
|
||||
|
||||
function relayout() {
|
||||
// 触发 combinedJson 重算(computed 依赖 items 引用变化)
|
||||
items.value = [...items.value]
|
||||
}
|
||||
|
||||
// ---------- 清单操作 ----------
|
||||
async function addTemplate(t: any) {
|
||||
const raw = await api<any>('get', `/api/windoor/templates/${t.id}`)
|
||||
const dd = parseDrawing(raw.drawing_data)
|
||||
const bb = groupBBox(dd.sm)
|
||||
if (!bb) { ElMessage.warning('该样板没有可用的几何数据'); return }
|
||||
items.value = [...items.value, {
|
||||
uid: uidSeq++, templateId: t.id, name: t.name || `样板#${t.id}`,
|
||||
pic: t.pic_url, raw, w: Math.round(bb.x1 - bb.x0), h: Math.round(bb.y1 - bb.y0),
|
||||
}]
|
||||
ElMessage.success(`已加入:${t.name || `样板#${t.id}`}`)
|
||||
const detail = await api<any>('get', `/api/windoor/templates/${t.id}`)
|
||||
if (addShapesAt(detail, null, null, t.name || `样板#${t.id}`))
|
||||
ElMessage.success(`已加入:${t.name || `样板#${t.id}`}`)
|
||||
}
|
||||
|
||||
function move(i: number, dir: -1 | 1) {
|
||||
const j = i + dir
|
||||
if (j < 0 || j >= items.value.length) return
|
||||
const arr = [...items.value]
|
||||
;[arr[i], arr[j]] = [arr[j], arr[i]]
|
||||
items.value = arr
|
||||
function onDragStart(e: DragEvent, t: any) {
|
||||
e.dataTransfer?.setData('template_id', String(t.id))
|
||||
e.dataTransfer!.effectAllowed = 'copy'
|
||||
}
|
||||
|
||||
function removeAt(i: number) {
|
||||
items.value = items.value.filter((_, k) => k !== i)
|
||||
async function onDrop(e: DragEvent) {
|
||||
const id = e.dataTransfer?.getData('template_id')
|
||||
if (!id) return
|
||||
const detail = await api<any>('get', `/api/windoor/templates/${id}`)
|
||||
const p = canvasRef.value?.modelAt(e.clientX, e.clientY)
|
||||
if (addShapesAt(detail, p?.x ?? null, p?.y ?? null, detail.name || `样板#${detail.id}`))
|
||||
ElMessage.success('已放入画布')
|
||||
}
|
||||
|
||||
// ---------- 单元操作 ----------
|
||||
function onSelect(uid: number | null) { selectedUid.value = uid }
|
||||
function selectUnit(uid: number) { selectedUid.value = uid; canvasRef.value?.refresh() }
|
||||
|
||||
function refreshMeta() {
|
||||
// 更新右侧清单的单元尺寸
|
||||
units.value = units.value.map(u => {
|
||||
const bb = bbOf(shapes.value.filter(s => s.__uid === u.uid))
|
||||
return bb ? { ...u, w: Math.round(bb.x1 - bb.x0), h: Math.round(bb.y1 - bb.y0) } : u
|
||||
})
|
||||
}
|
||||
|
||||
function deleteUnit(uid: number) {
|
||||
shapes.value = shapes.value.filter(s => s.__uid !== uid)
|
||||
units.value = units.value.filter(u => u.uid !== uid)
|
||||
if (selectedUid.value === uid) selectedUid.value = null
|
||||
canvasRef.value?.refresh()
|
||||
}
|
||||
|
||||
function deleteSelected() {
|
||||
if (selectedUid.value !== null) deleteUnit(selectedUid.value)
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
items.value = []
|
||||
shapes.value = []
|
||||
units.value = []
|
||||
selectedUid.value = null
|
||||
saveName.value = ''
|
||||
canvasRef.value?.refresh()
|
||||
}
|
||||
|
||||
function unitBb(uid: number) { return bbOf(shapes.value.filter(s => s.__uid === uid)) }
|
||||
|
||||
function alignBottom() {
|
||||
if (selectedUid.value === null) return
|
||||
const others = units.value.filter(u => u.uid !== selectedUid.value)
|
||||
.map(u => unitBb(u.uid)).filter(b => b)
|
||||
if (!others.length) return
|
||||
const target = Math.min(...others.map(b => b!.y0))
|
||||
const bb = unitBb(selectedUid.value)!
|
||||
translate(shapes.value.filter(s => s.__uid === selectedUid.value), 0, target - bb.y0)
|
||||
canvasRef.value?.refresh()
|
||||
ElMessage.success('已底边对齐')
|
||||
}
|
||||
|
||||
// ---------- 保存 ----------
|
||||
async function saveAsTemplate() {
|
||||
const dd = buildCombined()
|
||||
if (!dd) return
|
||||
const json = canvasRef.value?.exportDrawing()
|
||||
if (!json) return
|
||||
// 剥离内部标记字段
|
||||
const dd = JSON.parse(json)
|
||||
dd.sm = dd.sm.map((s: any) => {
|
||||
const { __uid, __name, __templateId, ...rest } = s
|
||||
void __uid; void __name; void __templateId
|
||||
return rest
|
||||
})
|
||||
saving.value = true
|
||||
try {
|
||||
const d = await api<any>('post', '/api/windoor/templates', {
|
||||
name: saveName.value.trim(),
|
||||
drawing_data: JSON.stringify(dd),
|
||||
catelog_id: null,
|
||||
note: `组合样板(${items.value.map(i => i.name).join(' + ')})`,
|
||||
note: `组合样板(${units.value.map(u => u.name).join(' + ')})`,
|
||||
})
|
||||
ElMessage.success(`已保存为我的样板 #${d.id},可在「样板库 → 我的样板」使用`)
|
||||
} finally { saving.value = false }
|
||||
}
|
||||
|
||||
onMounted(loadLib)
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Delete' && selectedUid.value !== null) deleteSelected()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadLib()
|
||||
window.addEventListener('keydown', onKeydown)
|
||||
})
|
||||
onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.combine-page { min-height: 100%; }
|
||||
.head-row { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }
|
||||
.lib-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
|
||||
.lib-item { border: 1px solid #e4e7ed; border-radius: 8px; padding: 6px; cursor: pointer; text-align: center;
|
||||
transition: all .15s; }
|
||||
.lib-item { border: 1px solid #e4e7ed; border-radius: 8px; padding: 6px; cursor: grab; text-align: center; transition: all .15s; }
|
||||
.lib-item:hover { border-color: var(--el-color-primary); box-shadow: var(--shadow-card); }
|
||||
.lib-pic { width: 100%; height: 64px; }
|
||||
.lib-item:active { cursor: grabbing; }
|
||||
.lib-pic { width: 100%; height: 64px; pointer-events: none; }
|
||||
.lib-name { font-size: 11px; color: #909399; display: block; overflow: hidden; text-overflow: ellipsis; }
|
||||
.canvas-box { height: 56vh; border: 1px solid #e4e7ed; border-radius: 8px; overflow: hidden; }
|
||||
.canvas-box { height: 62vh; border: 1px solid #e4e7ed; border-radius: 8px; overflow: hidden; }
|
||||
.save-bar { display: flex; gap: 10px; margin-top: 12px; }
|
||||
.unit { display: flex; align-items: center; gap: 8px; border: 1px solid #e4e7ed; border-radius: 8px;
|
||||
padding: 8px; margin-bottom: 8px; background: #fff; }
|
||||
padding: 8px; margin-bottom: 8px; background: #fff; cursor: pointer; }
|
||||
.unit.pick { border-color: var(--el-color-primary); }
|
||||
.unit-pic { width: 64px; height: 44px; flex-shrink: 0; }
|
||||
.unit-info { flex: 1; min-width: 0; }
|
||||
.unit-name { font-size: 13px; color: #303133; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.unit-size { font-size: 11px; color: #909399; }
|
||||
.unit-ops { display: flex; }
|
||||
.threed-box { height: 300px; }
|
||||
.list-enter-active, .list-leave-active { transition: all .2s; }
|
||||
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(12px); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user