Files
u-ppt/src/App.vue
T

283 lines
8.5 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, watch, 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 ImportModal from './components/modals/ImportModal.vue'
import PrintModal from './components/modals/PrintModal.vue'
import PresentMode from './components/present/PresentMode.vue'
import HomePage from './components/HomePage.vue'
/* ---------- 模式 ---------- */
const mode = ref<'home' | 'editor' | 'present'>('home')
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)
const importVisible = ref(false)
const printVisible = ref(false)
const deckLoaded = ref(false) // 文库/导入是否加载了新 deck,防止误切
/* ---------- 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('已导入')
if (mode.value === 'home') mode.value = 'editor'
} catch (e: any) {
toast('导入失败:' + (e?.message || String(e)))
}
}
reader.readAsText(file)
}
input.click()
}
function onNewBlank() {
store.newBlankDeck()
deckLoaded.value = true
mode.value = 'editor'
}
function onImportMaterials() { importVisible.value = true }
function onImportClose() {
importVisible.value = false
if (mode.value === 'home') {
const slides = store.getSlides()
// 不是仅有 1 页空白页时视为已导入内容
if (slides.length > 1 || slides[0]?.elements?.length > 0) {
mode.value = 'editor'
}
}
}
function onExportPdf() {
printVisible.value = true
}
/* ---------- 全局快捷键(编辑模式) ---------- */
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)
})
/* 持久化失败告警 → toast(防止静默丢数据) */
watch(() => store.saveWarning.text, (txt) => {
if (txt) toast(txt)
})
/* 文库关闭时若在首页且从文库加载了 deck 则切入编辑 */
watch(libraryVisible, (now, prev) => {
if (prev && !now && deckLoaded.value) {
deckLoaded.value = false
if (mode.value === 'home') mode.value = 'editor'
}
})
onUnmounted(() => {
document.removeEventListener('keydown', onKey)
})
</script>
<template>
<!-- ===================== 首页 ===================== -->
<HomePage
v-show="mode === 'home'"
@new-blank="onNewBlank"
@open-library="libraryVisible = true"
@import-materials="importVisible = true"
@open-deck="deckLoaded = true; mode = 'editor'"
@ai-create="onNewBlank(); switchTab('ai')"
@toast="toast"
@open-settings="settingsVisible = true"
/>
<!-- ===================== 编辑模式 ===================== -->
<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-materials="onImportMaterials"
@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')"
@open-deck="mode = 'editor'"
/>
<TemplateModal
:visible="templateVisible"
@close="templateVisible = false"
@toast="toast"
/>
<ImportModal
:visible="importVisible"
@close="onImportClose"
@toast="toast"
@open-settings="settingsVisible = true"
/>
<PrintModal
v-if="printVisible"
@close="printVisible = false"
/>
<!-- 轻提示 -->
<div class="toast" :class="{ show: toastShow }">{{ toastText }}</div>
</template>