- 工具栏新增三个按钮: 导出 JSON / 导入 JSON / 打印 PDF - 导出 JSON: Blob 下载 store.exportJSON() - 导入 JSON: 文件选择 + FileReader → store.importJSON() - 打印 PDF: window.print() + @media print CSS,逐页 page-break - 打印时隐藏编辑器/HUD/工具栏,只保留幻灯片内容 - 零新依赖(PDF 走浏览器原生打印)
225 lines
6.9 KiB
Vue
225 lines
6.9 KiB
Vue
<!-- =====================================================================
|
||
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>
|