Files
u-ppt/src/components/modals/LibraryModal.vue
T

213 lines
7.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.
<!-- =====================================================================
LibraryModal.vue 演示文库弹窗
===================================================================== -->
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import type { LibItem, Slide } from '../../core/types'
import { store, resolveBg } from '../../core/store'
import { appPrompt, appConfirm } from '../../core/dialog'
import ElementView from '../editor/ElementView.vue'
import Icon from '../common/Icon.vue'
const props = defineProps<{ visible: boolean }>()
const emit = defineEmits<{
(e: 'close'): void
(e: 'toast', msg: string): void
(e: 'switch-tab', tab: string): void
(e: 'open-deck'): void
}>()
const libVersion = ref(0)
const nameInput = ref('')
const opsOpenId = ref<string | null>(null)
function toggleOps(id: string) {
opsOpenId.value = opsOpenId.value === id ? null : id
}
function onDocClick(e: MouseEvent) {
const t = e.target as HTMLElement
if (!t.closest('.lib-ops')) opsOpenId.value = null
}
onMounted(() => document.addEventListener('mousedown', onDocClick))
onUnmounted(() => document.removeEventListener('mousedown', onDocClick))
/** 读文库列表(非响应式,靠 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')
emit('open-deck')
toast('已新建空白演示,在右侧添加元素')
emit('close')
}
function onOpen(id: string) {
if (store.loadFromLibrary(id)) {
opsOpenId.value = null
toast('已打开')
emit('open-deck')
emit('close')
}
}
async function onRename(id: string) {
const cur = store.getLibrary().find(x => x.id === id)
const name = await appPrompt('重命名', { defaultValue: cur ? cur.name : '' })
opsOpenId.value = null
if (name != null && name.trim()) {
store.renameInLibrary(id, name.trim())
bump()
}
}
function onDuplicate(id: string) {
opsOpenId.value = null
store.duplicateInLibrary(id)
toast('已复制')
bump()
}
async function onDelete(id: string) {
opsOpenId.value = null
if (await appConfirm('删除这份演示?', '此操作不可撤销', { danger: true, okText: '删除' })) {
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">
<div class="lib-head">
<h3>演示文库</h3>
<button class="btn ghost" @click="onNewBlank"> 新建空白</button>
</div>
<p class="modal-tip">保存多套演示文稿到本地随时切换</p>
<div class="lib-save-card">
<div class="lib-save-title">
<span><Icon name="save" :size="14" /> 保存当前演示</span>
<span v-if="activeId" class="lib-autosave-flag">改动已自动同步到文库</span>
</div>
<div class="lib-save-row">
<input type="text" v-model="nameInput" :placeholder="placeholder" @keydown="onNameKeydown" />
<button class="btn primary" @click="onSave">存入文库</button>
</div>
</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 primary small" @click="onOpen(it.id)">打开</button>
<button class="btn" @click="toggleOps(it.id)"></button>
<div v-if="opsOpenId === it.id" class="ops-menu">
<button @click="onRename(it.id)">重命名</button>
<button @click="onDuplicate(it.id)">复制</button>
<button class="danger" @click="onDelete(it.id)">删除</button>
</div>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn" @click="emit('close')">关闭</button>
</div>
</div>
</div>
</template>
<style scoped>
.lib-head { display: flex; align-items: center; justify-content: space-between; }
.lib-head h3 { margin: 0; }
.lib-save-card {
border: 1px solid color-mix(in srgb, var(--ui-primary, #5b5bd6) 22%, transparent);
background: color-mix(in srgb, var(--ui-primary, #5b5bd6) 5%, #fff);
border-radius: 10px;
padding: 12px 14px;
margin-bottom: 12px;
}
.lib-save-title { font-size: 13px; font-weight: 600; color: var(--ui-text, #1e293b); margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; }
.lib-autosave-flag { font-size: 11px; font-weight: 400; color: var(--ui-muted, #64748b); }
.lib-save-row { display: flex; gap: 8px; }
.lib-save-row input { flex: 1; }
.lib-ops { position: relative; }
.ops-menu {
position: absolute; right: 0; top: calc(100% + 4px);
background: #fff; border: 1px solid var(--ui-border, #e2e8f0);
border-radius: 8px; box-shadow: 0 4px 16px rgba(15,23,42,.12);
min-width: 96px; z-index: 10; overflow: hidden; padding: 4px 0;
}
.ops-menu button { display: block; width: 100%; text-align: left; padding: 6px 12px; border: none; background: none; font-size: 12px; cursor: pointer; }
.ops-menu button:hover { background: var(--ui-hover, #f1f5f9); }
.ops-menu button.danger { color: var(--ui-danger, #dc2626); }
</style>