修复: 演示元素消失+画布错位+PDF横版导出+工具栏信息架构重构
This commit is contained in:
@@ -1,30 +1,32 @@
|
||||
<!-- =====================================================================
|
||||
PrintModal.vue — PDF 导出:全量幻灯片打印预览
|
||||
复用 ElementView 渲染每个元素(与编辑器/演示一致,图表/表格/公式不降级)
|
||||
页面几何:A4 横向满幅(297mm × 209.8mm ≈ 16:9),元素 % 定位原样映射
|
||||
===================================================================== -->
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { store, resolveBg } from '../../core/store'
|
||||
import ElementView from '../editor/ElementView.vue'
|
||||
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
|
||||
const deck = store.getDeck()
|
||||
const slides = deck.slides
|
||||
// 响应式引用:预览随 deck 变化(快照会导致预览与数据脱节)
|
||||
const slides = computed(() => store.getDeck().slides)
|
||||
|
||||
function onPrint() { window.print() }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="print-modal" @print="onPrint">
|
||||
<div class="print-modal">
|
||||
<!-- 操作栏(打印时不显示) -->
|
||||
<div class="print-bar no-print">
|
||||
<button class="btn primary" @click="onPrint">🖨 打印 / 另存为 PDF</button>
|
||||
<span class="muted">{{ slides.length }} 页</span>
|
||||
<span class="muted">{{ slides.length }} 页 · A4 横向</span>
|
||||
<span class="sep"></span>
|
||||
<button class="btn ghost" @click="emit('close')">✕ 关闭</button>
|
||||
</div>
|
||||
|
||||
<!-- 所有幻灯片逐页渲染(复用编辑器元素渲染) -->
|
||||
<!-- 所有幻灯片逐页渲染(复用编辑器元素渲染,几何与编辑画布同构) -->
|
||||
<div
|
||||
v-for="(slide, i) in slides"
|
||||
:key="slide.id"
|
||||
@@ -56,18 +58,18 @@ function onPrint() { window.print() }
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
/* 打印页:A4 横向满幅 16:9(297 × 209.8mm),元素 % 坐标系与 1280×720 画布同构 */
|
||||
.print-page {
|
||||
width: 210mm; min-height: 297mm;
|
||||
width: 297mm; height: 209.8mm;
|
||||
margin: 12mm auto;
|
||||
padding: 20mm 25mm;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 16px rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
page-break-after: always;
|
||||
overflow: hidden;
|
||||
transform-origin: top left;
|
||||
}
|
||||
.print-number {
|
||||
position: absolute; bottom: 8mm; right: 10mm;
|
||||
position: absolute; bottom: 3mm; right: 5mm;
|
||||
font-size: 11px; color: #94a3b8;
|
||||
}
|
||||
|
||||
@@ -76,15 +78,18 @@ function onPrint() { window.print() }
|
||||
.no-print { display: none !important; }
|
||||
.print-modal { position: static; background: none; overflow: visible; }
|
||||
.print-page {
|
||||
margin: 0; padding: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
width: 100%; height: 100vh;
|
||||
min-height: auto;
|
||||
page-break-after: always;
|
||||
width: 297mm; height: 209.8mm;
|
||||
break-after: page;
|
||||
/* 确保背景色打印 */
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
.print-number { bottom: 2mm; right: 3mm; }
|
||||
/* 最后一页不再分页(避免尾部空白页) */
|
||||
.print-page:last-child { break-after: auto; }
|
||||
/* 入场动画在打印上下文中不可播放,冻结到最终态(防 opacity:0 截帧) */
|
||||
.print-page * { animation: none !important; transition: none !important; }
|
||||
.print-number { bottom: 3mm; right: 5mm; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,6 +21,23 @@ const tick = ref(0)
|
||||
const templates = computed(() => { tick.value; return store.getTemplates() })
|
||||
function bump() { tick.value++ }
|
||||
|
||||
/* ---------- 按分组展示:内置模板分节,自存模板单独一节 ---------- */
|
||||
const GROUP_LABELS: Record<string, string> = {
|
||||
basic: '基础版式', content: '内容版式', data: '数据版式', ending: '收尾版式', user: '我的模板'
|
||||
}
|
||||
const groupedTemplates = computed(() => {
|
||||
const all = templates.value
|
||||
const userTpls = all.filter(t => t.category === 'user')
|
||||
const builtin = all.filter(t => t.category === 'built-in')
|
||||
const groups: Array<{ key: string; label: string; items: typeof all }> = []
|
||||
for (const key of ['basic', 'content', 'data', 'ending']) {
|
||||
const items = builtin.filter(t => (t.group || 'content') === key)
|
||||
if (items.length) groups.push({ key, label: GROUP_LABELS[key], items })
|
||||
}
|
||||
if (userTpls.length) groups.push({ key: 'user', label: GROUP_LABELS.user, items: userTpls })
|
||||
return groups
|
||||
})
|
||||
|
||||
function onSave() {
|
||||
const v = nameInput.value.trim()
|
||||
if (!v) { emit('toast', '请输入模板名'); return }
|
||||
@@ -68,20 +85,25 @@ function onDelete(tpl: PageTemplate) {
|
||||
<button class="btn primary" @click="onSave">💾 保存为模板</button>
|
||||
</div>
|
||||
|
||||
<!-- 模板网格 -->
|
||||
<div class="tpl-grid">
|
||||
<div v-for="tpl in templates" :key="tpl.id" class="tpl-card" :class="tpl.category" @click="onPick(tpl)">
|
||||
<div class="tpl-thumb" :style="{ background: resolveBg(tpl.background) }">
|
||||
<div class="tpl-thumb-layer">
|
||||
<ElementView v-for="el in tpl.elements" :key="el.id" :el="el" :bg="tpl.background" />
|
||||
<!-- 模板网格(按分组分节) -->
|
||||
<div class="tpl-groups">
|
||||
<section v-for="g in groupedTemplates" :key="g.key" class="tpl-group">
|
||||
<h4 class="tpl-group-title">{{ g.label }} <span class="tpl-group-count">{{ g.items.length }}</span></h4>
|
||||
<div class="tpl-grid">
|
||||
<div v-for="tpl in g.items" :key="tpl.id" class="tpl-card" :class="tpl.category" @click="onPick(tpl)">
|
||||
<div class="tpl-thumb" :style="{ background: resolveBg(tpl.background) }">
|
||||
<div class="tpl-thumb-layer">
|
||||
<ElementView v-for="el in tpl.elements" :key="el.id" :el="el" :bg="tpl.background" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="tpl-info">
|
||||
<span class="tpl-name">{{ tpl.name }}</span>
|
||||
<span class="tpl-badge" :class="tpl.category">{{ tpl.category === 'built-in' ? '内置' : '自存' }}</span>
|
||||
<button v-if="tpl.category === 'user'" class="tpl-del" title="删除" @click.stop="onDelete(tpl)">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tpl-info">
|
||||
<span class="tpl-name">{{ tpl.name }}</span>
|
||||
<span class="tpl-badge" :class="tpl.category">{{ tpl.category === 'built-in' ? '内置' : '自存' }}</span>
|
||||
<button v-if="tpl.category === 'user'" class="tpl-del" title="删除" @click.stop="onDelete(tpl)">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
@@ -110,12 +132,37 @@ function onDelete(tpl: PageTemplate) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tpl-groups {
|
||||
overflow-y: auto;
|
||||
padding: .2em;
|
||||
}
|
||||
|
||||
.tpl-group {
|
||||
margin-bottom: 1.2em;
|
||||
}
|
||||
|
||||
.tpl-group-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--ui-muted, #64748b);
|
||||
margin: .6em 0 .6em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5em;
|
||||
}
|
||||
|
||||
.tpl-group-count {
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
background: rgba(100, 116, 139, .12);
|
||||
padding: 0 6px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tpl-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1em;
|
||||
overflow-y: auto;
|
||||
padding: .2em;
|
||||
}
|
||||
|
||||
.tpl-card {
|
||||
|
||||
Reference in New Issue
Block a user