310 lines
9.1 KiB
Vue
310 lines
9.1 KiB
Vue
<!-- =====================================================================
|
||
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>
|