Files
u-ppt/src/App.vue
绝尘 056dc58318 新增: 导出 JSON + 导入 JSON + 打印/PDF 导出(P4)
- 工具栏新增三个按钮: 导出 JSON / 导入 JSON / 打印 PDF
- 导出 JSON: Blob 下载 store.exportJSON()
- 导入 JSON: 文件选择 + FileReader → store.importJSON()
- 打印 PDF: window.print() + @media print CSS,逐页 page-break
- 打印时隐藏编辑器/HUD/工具栏,只保留幻灯片内容
- 零新依赖(PDF 走浏览器原生打印)
2026-07-12 15:02:27 +08:00

225 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.
<!-- =====================================================================
App.vue 根组件编辑/演示模式切换工具栏三栏布局弹窗
===================================================================== -->
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { store } from './core/store'
import Toolbar from './components/editor/Toolbar.vue'
import ThumbBar from './components/editor/ThumbBar.vue'
import Canvas from './components/editor/Canvas.vue'
import PropsPanel from './components/editor/PropsPanel.vue'
import AiPanel from './components/ai/AiPanel.vue'
import SettingsModal from './components/modals/SettingsModal.vue'
import LibraryModal from './components/modals/LibraryModal.vue'
import TemplateModal from './components/modals/TemplateModal.vue'
import PresentMode from './components/present/PresentMode.vue'
/* ---------- 模式 ---------- */
const mode = ref<'editor' | 'present'>('editor')
const presentVisible = ref(false)
const presentStartIndex = ref(0)
/* ---------- 活动面板 tab ---------- */
const activeTab = ref<'props' | 'ai'>('props')
function switchTab(name: 'props' | 'ai') {
activeTab.value = name
}
/* ---------- 弹窗 ---------- */
const settingsVisible = ref(false)
const libraryVisible = ref(false)
const templateVisible = ref(false)
/* ---------- toast ---------- */
const toastText = ref('')
const toastShow = ref(false)
let toastTimer: ReturnType<typeof setTimeout> | null = null
function toast(msg: string) {
toastText.value = msg
toastShow.value = true
if (toastTimer) clearTimeout(toastTimer)
toastTimer = setTimeout(() => { toastShow.value = false }, 2200)
}
/* ---------- AI busy 状态(禁用部分按钮) ---------- */
const aiBusy = ref(false)
const disabledActions = computed(() => aiBusy.value
? ['present', 'library', 'save', 'reset', 'add-slide', 'dup-slide', 'del-slide']
: []
)
function onBusyChange(busy: boolean) { aiBusy.value = busy }
/* ---------- 工具栏动作 ---------- */
function onPresent() {
presentStartIndex.value = store.currentIndex.value
presentVisible.value = true
mode.value = 'present'
document.body.dataset.mode = 'present'
}
function onPresentExit() {
presentVisible.value = false
mode.value = 'editor'
document.body.dataset.mode = 'editor'
}
function onSave() {
if (store.getActiveLibId()) {
store.saveToLibrary()
toast('已更新到文库')
} else {
libraryVisible.value = true
}
}
function openAi() { switchTab('ai') }
/* ---------- 导出/导入 ---------- */
function onExportJson() {
const json = store.exportJSON()
const blob = new Blob([json], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'u-ppt-export.json'
a.click()
URL.revokeObjectURL(url)
toast('已导出 JSON')
}
function onImportJson() {
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json,application/json'
input.onchange = () => {
const file = input.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = () => {
try {
store.importJSON(reader.result as string)
toast('已导入')
} catch (e: any) {
toast('导入失败:' + (e?.message || String(e)))
}
}
reader.readAsText(file)
}
input.click()
}
function onExportPdf() {
// 用浏览器打印 + @media print CSS 实现 PDF 导出
// 演示模式的 slide-layer 已有 print 样式
document.body.classList.add('printing')
window.print()
setTimeout(() => document.body.classList.remove('printing'), 500)
toast('在打印对话框中选择「另存为 PDF」')
}
/* ---------- 全局快捷键(编辑模式) ---------- */
function onKey(e: KeyboardEvent) {
if (document.body.dataset.mode === 'present') return
const inField = /^(INPUT|TEXTAREA|SELECT)$/.test((e.target as HTMLElement).tagName) || (e.target as HTMLElement).isContentEditable
if (e.key === 'Escape') {
if (settingsVisible.value) { settingsVisible.value = false; return }
if (libraryVisible.value) { libraryVisible.value = false; return }
}
if ((e.ctrlKey || e.metaKey) && (e.key === 'z' || e.key === 'Z')) {
if (inField) return
e.preventDefault()
if (e.shiftKey ? store.redo() : store.undo()) toast(e.shiftKey ? '已重做' : '已撤销')
return
}
if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || e.key === 'Y')) {
if (inField) return; e.preventDefault(); if (store.redo()) toast('已重做'); return
}
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault(); if (!inField) toast('已自动保存到本地'); return
}
if (e.key === 'Delete' && !inField) {
const id = store.getSelectedId()
if (id) { e.preventDefault(); store.delElement(id) }
}
}
onMounted(() => {
document.addEventListener('keydown', onKey)
})
onUnmounted(() => {
document.removeEventListener('keydown', onKey)
})
</script>
<template>
<!-- ===================== 编辑模式 ===================== -->
<div v-show="mode === 'editor'" class="app-editor">
<Toolbar
:disabled-actions="disabledActions"
@present="onPresent"
@open-library="libraryVisible = true"
@open-settings="settingsVisible = true"
@open-ai="openAi"
@save="onSave"
@open-templates="templateVisible = true"
@export-json="onExportJson"
@import-json="onImportJson"
@export-pdf="onExportPdf"
/>
<div class="editor-body">
<!-- 缩略图 -->
<ThumbBar />
<!-- 画布 -->
<Canvas />
<!-- Tabs属性 / AI -->
<aside class="side-panel">
<div class="panel-tabs">
<button class="panel-tab" :class="{ active: activeTab === 'props' }" @click="switchTab('props')">🎨 属性</button>
<button class="panel-tab" :class="{ active: activeTab === 'ai' }" @click="switchTab('ai')">🤖 AI 助手</button>
</div>
<PropsPanel v-show="activeTab === 'props'" />
<AiPanel
v-show="activeTab === 'ai'"
@busy-change="onBusyChange"
@toast="toast"
@open-settings="settingsVisible = true"
@switch-tab="(t: string) => switchTab(t as 'props' | 'ai')"
/>
</aside>
</div>
</div>
<!-- ===================== 演示模式 ===================== -->
<PresentMode
:visible="presentVisible"
:start-index="presentStartIndex"
@exit="onPresentExit"
/>
<!-- ===================== 弹窗 ===================== -->
<SettingsModal
:visible="settingsVisible"
@close="settingsVisible = false"
@toast="toast"
/>
<LibraryModal
:visible="libraryVisible"
@close="libraryVisible = false"
@toast="toast"
@switch-tab="(t: string) => switchTab(t as 'props' | 'ai')"
/>
<TemplateModal
:visible="templateVisible"
@close="templateVisible = false"
@toast="toast"
/>
<!-- 轻提示 -->
<div class="toast" :class="{ show: toastShow }">{{ toastText }}</div>
</template>