新增: 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:
2026-07-12 13:12:09 +08:00
commit cfeabf2d37
35 changed files with 7919 additions and 0 deletions

178
src/App.vue Normal file
View 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>