新增: 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:
@@ -0,0 +1,154 @@
|
||||
<!-- =====================================================================
|
||||
LibraryModal.vue — 演示文库弹窗
|
||||
===================================================================== -->
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { LibItem, Slide } from '../../core/types'
|
||||
import { store, resolveBg } from '../../core/store'
|
||||
import ElementView from '../editor/ElementView.vue'
|
||||
|
||||
const props = defineProps<{ visible: boolean }>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
(e: 'toast', msg: string): void
|
||||
(e: 'switch-tab', tab: string): void
|
||||
}>()
|
||||
|
||||
const libVersion = ref(0)
|
||||
const nameInput = ref('')
|
||||
|
||||
/** 读文库列表(非响应式,靠 libVersion 触发重算) */
|
||||
const library = computed<LibItem[]>(() => {
|
||||
void libVersion.value
|
||||
return store.getLibrary()
|
||||
})
|
||||
const activeId = computed(() => {
|
||||
void libVersion.value
|
||||
return store.getActiveLibId()
|
||||
})
|
||||
|
||||
const libCount = computed(() => library.value.length)
|
||||
const placeholder = computed(() => {
|
||||
const cur = library.value.find(x => x.id === activeId.value)
|
||||
return cur
|
||||
? '当前:「' + cur.name + '」,留空同名覆盖,输入新名另存为新份'
|
||||
: '为当前演示命名后存入文库…'
|
||||
})
|
||||
|
||||
function bump() { libVersion.value++ }
|
||||
|
||||
watch(() => props.visible, (v) => {
|
||||
if (v) { bump(); nameInput.value = '' }
|
||||
})
|
||||
|
||||
function toast(msg: string) { emit('toast', msg) }
|
||||
|
||||
function firstSlide(item: LibItem): Slide | null {
|
||||
const s = item?.deck?.slides
|
||||
return s && s.length ? s[0] : null
|
||||
}
|
||||
|
||||
function formatTime(ts: number): string {
|
||||
if (!ts) return ''
|
||||
const diff = Date.now() - ts
|
||||
if (diff < 60000) return '刚刚'
|
||||
if (diff < 3600000) return Math.floor(diff / 60000) + ' 分钟前'
|
||||
if (diff < 86400000) return Math.floor(diff / 3600000) + ' 小时前'
|
||||
const d = new Date(ts)
|
||||
return (d.getMonth() + 1) + '月' + d.getDate() + '日'
|
||||
}
|
||||
|
||||
function onNameKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); onSave() }
|
||||
}
|
||||
|
||||
function onSave() {
|
||||
const name = nameInput.value.trim()
|
||||
if (!name && !store.getActiveLibId()) {
|
||||
toast('请输入名称')
|
||||
return
|
||||
}
|
||||
store.saveToLibrary(name || null)
|
||||
toast(name ? ('已存入文库:' + name) : '已更新到文库')
|
||||
nameInput.value = ''
|
||||
bump()
|
||||
}
|
||||
|
||||
function onNewBlank() {
|
||||
store.newBlankDeck()
|
||||
emit('switch-tab', 'props')
|
||||
toast('已新建空白演示,在右侧添加元素')
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function onOpen(id: string) {
|
||||
if (store.loadFromLibrary(id)) {
|
||||
toast('已打开')
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
function onRename(id: string) {
|
||||
const cur = store.getLibrary().find(x => x.id === id)
|
||||
const name = prompt('重命名为', cur ? cur.name : '')
|
||||
if (name != null && name.trim()) {
|
||||
store.renameInLibrary(id, name.trim())
|
||||
bump()
|
||||
}
|
||||
}
|
||||
|
||||
function onDuplicate(id: string) {
|
||||
store.duplicateInLibrary(id)
|
||||
toast('已复制')
|
||||
bump()
|
||||
}
|
||||
|
||||
function onDelete(id: string) {
|
||||
if (confirm('删除这份演示?此操作不可撤销。')) {
|
||||
store.deleteFromLibrary(id)
|
||||
toast('已删除')
|
||||
bump()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="modal-mask lib-modal-mask" :class="{ hidden: !visible }" @click.self="emit('close')">
|
||||
<div class="modal lib-modal">
|
||||
<h3>演示文库</h3>
|
||||
<p class="modal-tip">保存多套演示文稿到本地,随时切换。</p>
|
||||
|
||||
<div class="lib-save-row">
|
||||
<input type="text" v-model="nameInput" :placeholder="placeholder" @keydown="onNameKeydown" />
|
||||
<button class="btn primary" @click="onSave">存入文库</button>
|
||||
<button class="btn" @click="onNewBlank">新建空白</button>
|
||||
</div>
|
||||
|
||||
<div class="lib-hint">共 {{ libCount }} 份</div>
|
||||
|
||||
<div class="lib-list">
|
||||
<div v-for="it in library" :key="it.id" class="lib-item" :class="{ active: it.id === activeId }" :data-id="it.id">
|
||||
<div class="lib-thumb" :style="{ background: firstSlide(it) ? resolveBg(firstSlide(it)!.background) : '#fff' }">
|
||||
<div v-if="firstSlide(it)" style="position:absolute;top:0;left:0;width:1280px;height:720px;transform:scale(0.05625);transform-origin:top left;pointer-events:none">
|
||||
<ElementView v-for="el in firstSlide(it)!.elements" :key="el.id" :el="el" :bg="firstSlide(it)!.background" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="lib-info">
|
||||
<div class="lib-name">{{ it.name }}<span v-if="it.id === activeId"> (当前)</span></div>
|
||||
<div class="lib-meta">{{ (it.deck && it.deck.slides ? it.deck.slides.length : 0) + ' 页 · ' + formatTime(it.updatedAt || it.createdAt) }}</div>
|
||||
</div>
|
||||
<div class="lib-ops">
|
||||
<button class="btn" @click="onOpen(it.id)">打开</button>
|
||||
<button class="btn" @click="onRename(it.id)">重命名</button>
|
||||
<button class="btn" @click="onDuplicate(it.id)">复制</button>
|
||||
<button class="btn danger" @click="onDelete(it.id)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="btn" @click="emit('close')">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user