Files
u-ppt/src/components/HomePage.vue
T

310 lines
9.1 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.
<!-- =====================================================================
HomePage.vue 首页品牌展示快捷入口最近文库
===================================================================== -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { store } from '../core/store'
import type { LibItem } from '../core/types'
const emit = defineEmits<{
(e: 'new-blank'): void
(e: 'open-library'): void
(e: 'import-materials'): void
(e: 'open-settings'): void
(e: 'open-oss'): void
(e: 'toast', msg: string): void
(e: 'open-deck'): void
(e: 'ai-create'): void
}>()
const libVersion = ref(0)
const library = computed<LibItem[]>(() => {
void libVersion.value
return store.getLibrary().slice(0, 6) // 最近 6 个
})
function bump() { libVersion.value++ }
onMounted(bump)
function loadItem(id: string) {
if (store.loadFromLibrary(id)) {
emit('toast', '已加载:' + (library.value.find(x => x.id === id)?.name || ''))
emit('open-deck')
}
}
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 firstSlideTitle(item: LibItem): string {
const el = item?.deck?.slides?.[0]?.elements?.[0]
return el?.type === 'title' ? (el.content || '无标题') : '无标题'
}
/* 上次未入库的工作区草稿(改了没保存就关/刷新),提示继续编辑 */
const workDraft = computed(() => {
if (!store.getActiveLibId() && store.hasWorkDeck()) {
const d = store.getDeck()
const t = d.slides[0]?.elements.find(e => e.type === 'title')
return { title: t?.content || '未命名草稿', pages: d.slides.length }
}
return null
})
</script>
<template>
<div class="home">
<!-- 背景装饰 -->
<div class="home-bg">
<div class="home-glow top-right"></div>
<div class="home-glow bottom-left"></div>
</div>
<div class="home-inner">
<!-- 品牌区 -->
<header class="home-brand">
<span class="home-logo"></span>
<h1 class="home-title">u-ppt</h1>
<p class="home-desc">轻量在线演示工具 · 支持 AI 创作与本地资料导入</p>
</header>
<!-- 未保存草稿恢复 -->
<button v-if="workDraft" class="draft-resume" @click="emit('open-deck')">
<span class="draft-icon"></span>
<span class="draft-info">
<strong>继续编辑{{ workDraft.title }}</strong>
<span>{{ workDraft.pages }} · 上次未保存的草稿已自动暂存</span>
</span>
</button>
<!-- 快捷入口 -->
<div class="home-actions">
<button class="action-card" @click="emit('new-blank')">
<span class="action-icon"></span>
<span class="action-label">新建空白</span>
<span class="action-hint">从空白页开始创作</span>
</button>
<button class="action-card" @click="emit('open-library')">
<span class="action-icon">📁</span>
<span class="action-label">从文库打开</span>
<span class="action-hint">继续已有的演示</span>
</button>
<button class="action-card" @click="emit('import-materials')">
<span class="action-icon">📂</span>
<span class="action-label">导入资料</span>
<span class="action-hint">图片/PDF/DOCX AI 生成</span>
</button>
<button class="action-card accent" @click="emit('ai-create')">
<span class="action-icon">🤖</span>
<span class="action-label">AI 创作</span>
<span class="action-hint">输入主题AI 生成全套演示</span>
</button>
</div>
<!-- 最近文库 -->
<div v-if="library.length > 0" class="home-recent">
<h2 class="section-title">最近文档</h2>
<div class="recent-list">
<button
v-for="item in library"
:key="item.id"
class="recent-item"
@click="loadItem(item.id)"
:title="item.name"
>
<span class="recent-icon">📄</span>
<span class="recent-info">
<span class="recent-name">{{ item.name }}</span>
<span class="recent-meta">
{{ item.deck.slides.length }} · {{ formatTime(item.updatedAt || item.createdAt) }}
</span>
</span>
<span class="recent-preview">{{ firstSlideTitle(item) }}</span>
</button>
</div>
</div>
<!-- 底部 -->
<footer class="home-footer">
<button class="btn ghost" @click="emit('open-oss')"> 云存储</button>
<button class="btn ghost" @click="emit('open-settings')"> 设置</button>
</footer>
</div>
</div>
</template>
<style scoped>
.home {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
background: var(--ui-bg, #f1f5f9);
}
/* 背景光晕 */
.home-bg { position: absolute; inset: 0; pointer-events: none; }
.home-glow {
position: absolute;
width: 480px; height: 480px;
border-radius: 50%;
filter: blur(120px);
opacity: .08;
}
.home-glow.top-right { top: -120px; right: -80px; background: var(--ui-primary, #4f46e5); }
.home-glow.bottom-left { bottom: -160px; left: -80px; background: var(--ui-primary, #4f46e5); }
.home-inner {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 40px;
padding: 40px 24px;
max-width: 680px;
width: 100%;
}
/* 品牌 */
.home-brand { text-align: center; }
.home-logo {
font-size: 56px; line-height: 1;
color: var(--ui-primary, #4f46e5);
display: block; margin-bottom: 8px;
}
.home-title {
font-size: 32px; font-weight: 700;
letter-spacing: -.02em;
color: var(--ui-text, #1e293b);
}
.home-desc {
margin-top: 6px;
font-size: 15px;
color: var(--ui-muted, #64748b);
}
/* 未保存草稿恢复卡片 */
.draft-resume {
display: flex; align-items: center; gap: 14px;
width: 100%; padding: 14px 18px;
border: 1px solid var(--ui-primary, #4f46e5);
border-radius: 12px;
background: color-mix(in srgb, var(--ui-primary, #4f46e5) 6%, var(--ui-panel, #fff));
cursor: pointer; text-align: left;
transition: box-shadow .15s, transform .15s;
}
.draft-resume:hover { box-shadow: var(--shadow-md, 0 4px 12px rgba(0,0,0,.08)); transform: translateY(-1px); }
.draft-icon {
width: 36px; height: 36px; border-radius: 50%;
background: var(--ui-primary, #4f46e5); color: #fff;
display: flex; align-items: center; justify-content: center;
font-size: 16px; flex-shrink: 0;
}
.draft-info { display: flex; flex-direction: column; gap: 2px; }
.draft-info strong { font-size: 14px; color: var(--ui-text, #1e293b); }
.draft-info span { font-size: 12px; color: var(--ui-muted, #64748b); }
/* 快捷入口网格 */
.home-actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
width: 100%;
}
.action-card {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
padding: 20px;
border: 1px solid var(--ui-border, #e2e8f0);
border-radius: 12px;
background: var(--ui-panel, #fff);
cursor: pointer;
transition: all .15s;
text-align: left;
}
.action-card:hover {
border-color: var(--ui-primary, #4f46e5);
box-shadow: var(--shadow-md, 0 4px 12px rgba(15,23,42,.08));
transform: translateY(-2px);
}
.action-card:active { transform: translateY(0); }
.action-icon { font-size: 28px; line-height: 1; margin-bottom: 4px; }
.action-label { font-size: 16px; font-weight: 600; color: var(--ui-text, #1e293b); }
.action-hint { font-size: 13px; color: var(--ui-muted, #64748b); }
/* 最近文档 */
.home-recent { width: 100%; }
.section-title {
font-size: 14px; font-weight: 600;
color: var(--ui-muted, #64748b);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: .05em;
}
.recent-list {
display: flex;
flex-direction: column;
border: 1px solid var(--ui-border, #e2e8f0);
border-radius: 10px;
overflow: hidden;
background: var(--ui-panel, #fff);
}
.recent-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border: none;
border-bottom: 1px solid var(--ui-border, #f1f5f9);
background: transparent;
cursor: pointer;
transition: background .1s;
text-align: left;
width: 100%;
}
.recent-item:last-child { border-bottom: none; }
.recent-item:hover { background: var(--ui-hover, #f1f5f9); }
.recent-icon { font-size: 20px; flex-shrink: 0; }
.recent-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.recent-name {
font-size: 14px; font-weight: 500;
color: var(--ui-text, #1e293b);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.recent-meta {
font-size: 12px;
color: var(--ui-muted, #64748b);
}
.recent-preview {
font-size: 13px;
color: var(--ui-muted, #64748b);
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 底部 */
.home-footer { display: flex; gap: 8px; }
</style>