新增: u-ppt Vue 3 在线演示工具初始版本
- 核心架构: Vue 3 + Vite + TypeScript,零运行时依赖(仅 Vue) - 编辑器: 幻灯片增删排序、元素拖拽八向缩放、双击编辑、12 种元素类型 (标题/正文/列表/数据/金句/图片/形状/图表/卡片/表格/代码/公式) - 图表: 8 种 SVG 自绘(柱状/条形/折线/面积/饼图/环形/雷达/进度) - 富文本: 结构化 segments(加粗/斜体/颜色/高亮/上下标/代码/链接) - AI 能力: 对话编辑、生成整套、润色本页、大纲→逐页生成、一键美化、AI 配图 - 会话绑定: 每份 PPT 独立会话,切换 PPT 自动切换对话历史 - 模板系统: 7 个内置版式 + 用户自存模板 - 演示模式: 全屏播放、键盘/鼠标/滚轮导航、入场动画 - 持久化: localStorage 存 deck/文库/会话/模板/配置 - 支持多服务商: 智谱/DeepSeek/通义/Kimi/豆包/OpenAI/Anthropic/Gemini/Groq/Ollama
This commit is contained in:
178
src/App.vue
Normal file
178
src/App.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<!-- =====================================================================
|
||||
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 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"
|
||||
/>
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user