|
<template>
|
|
<div class="public-viewer">
|
|
<FullscreenSpin v-if="loading" tip="正在加载演示文稿..." loading :mask="false" />
|
|
|
|
<div v-else-if="error" class="error-container">
|
|
<div class="error-message">
|
|
<h3>{{ error }}</h3>
|
|
<p>请检查分享链接是否正确或联系分享者</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="presentation-container">
|
|
|
|
<div class="presentation-header">
|
|
<h1>{{ presentationTitle }}</h1>
|
|
<div class="controls">
|
|
<button @click="toggleFullscreen" class="control-btn">
|
|
{{ isFullscreen ? '退出全屏' : '全屏查看' }}
|
|
</button>
|
|
<button @click="startSlideshow" class="control-btn primary">
|
|
开始演示
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="slides-container" ref="slidesContainer">
|
|
<div
|
|
v-for="(slide, index) in slides"
|
|
:key="slide.id"
|
|
class="slide-preview"
|
|
:class="{ active: currentSlideIndex === index }"
|
|
@click="currentSlideIndex = index"
|
|
>
|
|
<div class="slide-number">{{ index + 1 }}</div>
|
|
<div class="slide-content">
|
|
<SlideThumbnail :slide="slide" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="current-slide-container">
|
|
<div class="slide-navigation">
|
|
<button
|
|
@click="previousSlide"
|
|
:disabled="currentSlideIndex === 0"
|
|
class="nav-btn"
|
|
>
|
|
上一页
|
|
</button>
|
|
<span class="slide-counter">
|
|
{{ currentSlideIndex + 1 }} / {{ slides.length }}
|
|
</span>
|
|
<button
|
|
@click="nextSlide"
|
|
:disabled="currentSlideIndex === slides.length - 1"
|
|
class="nav-btn"
|
|
>
|
|
下一页
|
|
</button>
|
|
</div>
|
|
|
|
<div class="current-slide" ref="currentSlideRef">
|
|
<Slide
|
|
v-if="currentSlide"
|
|
:slide="currentSlide"
|
|
:editable="false"
|
|
class="slide-display"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div v-if="isSlideshow" class="slideshow-mode" @keydown="handleKeydown">
|
|
<div class="slideshow-container">
|
|
<Slide
|
|
v-if="currentSlide"
|
|
:slide="currentSlide"
|
|
:editable="false"
|
|
class="slideshow-slide"
|
|
/>
|
|
|
|
<div class="slideshow-controls">
|
|
<button @click="exitSlideshow" class="exit-btn">退出演示</button>
|
|
<div class="slideshow-navigation">
|
|
<button @click="previousSlide" :disabled="currentSlideIndex === 0">←</button>
|
|
<span>{{ currentSlideIndex + 1 }} / {{ slides.length }}</span>
|
|
<button @click="nextSlide" :disabled="currentSlideIndex === slides.length - 1">→</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { ref, onMounted, computed, onUnmounted } from 'vue'
|
|
import { useMessage } from '@/hooks/useMessage'
|
|
import api from '@/services'
|
|
import type { Slide as SlideType } from '@/types/slides'
|
|
|
|
import FullscreenSpin from '@/components/FullscreenSpin.vue'
|
|
import Slide from '@/views/components/Slide/index.vue'
|
|
import SlideThumbnail from '@/views/components/SlideThumbnail/index.vue'
|
|
|
|
const { message } = useMessage()
|
|
|
|
const loading = ref(true)
|
|
const error = ref('')
|
|
const slides = ref<SlideType[]>([])
|
|
const presentationTitle = ref('')
|
|
const currentSlideIndex = ref(0)
|
|
const isFullscreen = ref(false)
|
|
const isSlideshow = ref(false)
|
|
const slidesContainer = ref<HTMLElement>()
|
|
const currentSlideRef = ref<HTMLElement>()
|
|
|
|
const currentSlide = computed(() => slides.value[currentSlideIndex.value])
|
|
|
|
|
|
const getShareIdFromUrl = () => {
|
|
const url = window.location.href
|
|
const match = url.match(/\/public\/view\/([^/?]+)/)
|
|
return match ? match[1] : null
|
|
}
|
|
|
|
|
|
const loadPublicPresentation = async () => {
|
|
try {
|
|
loading.value = true
|
|
const shareId = getShareIdFromUrl()
|
|
|
|
if (!shareId) {
|
|
error.value = '无效的分享链接'
|
|
return
|
|
}
|
|
|
|
const response = await api.get(`/api/public/presentation/${shareId}`)
|
|
|
|
if (response.data.success) {
|
|
slides.value = response.data.slides
|
|
presentationTitle.value = response.data.title || '未命名演示文稿'
|
|
} else {
|
|
error.value = response.data.message || '加载失败'
|
|
}
|
|
} catch (err) {
|
|
console.error('加载公共演示文稿失败:', err)
|
|
error.value = '加载演示文稿时出错,请稍后重试'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
|
|
const previousSlide = () => {
|
|
if (currentSlideIndex.value > 0) {
|
|
currentSlideIndex.value--
|
|
}
|
|
}
|
|
|
|
const nextSlide = () => {
|
|
if (currentSlideIndex.value < slides.value.length - 1) {
|
|
currentSlideIndex.value++
|
|
}
|
|
}
|
|
|
|
|
|
const toggleFullscreen = () => {
|
|
if (!document.fullscreenElement) {
|
|
currentSlideRef.value?.requestFullscreen()
|
|
isFullscreen.value = true
|
|
} else {
|
|
document.exitFullscreen()
|
|
isFullscreen.value = false
|
|
}
|
|
}
|
|
|
|
|
|
const startSlideshow = () => {
|
|
isSlideshow.value = true
|
|
currentSlideIndex.value = 0
|
|
document.addEventListener('keydown', handleKeydown)
|
|
}
|
|
|
|
const exitSlideshow = () => {
|
|
isSlideshow.value = false
|
|
document.removeEventListener('keydown', handleKeydown)
|
|
}
|
|
|
|
|
|
const handleKeydown = (event: KeyboardEvent) => {
|
|
switch (event.key) {
|
|
case 'ArrowLeft':
|
|
case 'ArrowUp':
|
|
previousSlide()
|
|
break
|
|
case 'ArrowRight':
|
|
case 'ArrowDown':
|
|
case ' ':
|
|
nextSlide()
|
|
break
|
|
case 'Escape':
|
|
exitSlideshow()
|
|
break
|
|
}
|
|
}
|
|
|
|
|
|
const handleFullscreenChange = () => {
|
|
isFullscreen.value = !!document.fullscreenElement
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadPublicPresentation()
|
|
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener('keydown', handleKeydown)
|
|
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.public-viewer {
|
|
width: 100%;
|
|
height: 100vh;
|
|
background: #f5f5f5;
|
|
}
|
|
|
|
.error-container {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100vh;
|
|
|
|
.error-message {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
|
|
h3 {
|
|
color: #ff4757;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
p {
|
|
color: #666;
|
|
}
|
|
}
|
|
}
|
|
|
|
.presentation-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
}
|
|
|
|
.presentation-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1rem 2rem;
|
|
background: white;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
|
|
h1 {
|
|
margin: 0;
|
|
color: #333;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.control-btn {
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
background: white;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
|
|
&:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
&.primary {
|
|
background: #007bff;
|
|
color: white;
|
|
border-color: #007bff;
|
|
|
|
&:hover {
|
|
background: #0056b3;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.slides-container {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
padding: 1rem;
|
|
overflow-y: auto;
|
|
max-height: 200px;
|
|
}
|
|
|
|
.slide-preview {
|
|
position: relative;
|
|
width: 200px;
|
|
height: 150px;
|
|
border: 2px solid #ddd;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
background: white;
|
|
|
|
&:hover {
|
|
border-color: #007bff;
|
|
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.15);
|
|
}
|
|
|
|
&.active {
|
|
border-color: #007bff;
|
|
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
|
|
}
|
|
|
|
.slide-number {
|
|
position: absolute;
|
|
top: 8px;
|
|
left: 8px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
color: white;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 0.8rem;
|
|
z-index: 10;
|
|
}
|
|
|
|
.slide-content {
|
|
width: 100%;
|
|
height: 100%;
|
|
padding: 8px;
|
|
}
|
|
}
|
|
|
|
.current-slide-container {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 1rem;
|
|
background: white;
|
|
}
|
|
|
|
.slide-navigation {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-bottom: 1rem;
|
|
|
|
.nav-btn {
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
background: white;
|
|
cursor: pointer;
|
|
|
|
&:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
&:not(:disabled):hover {
|
|
background: #f8f9fa;
|
|
}
|
|
}
|
|
|
|
.slide-counter {
|
|
font-weight: 500;
|
|
color: #666;
|
|
}
|
|
}
|
|
|
|
.current-slide {
|
|
flex: 1;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
|
|
.slide-display {
|
|
max-width: 90%;
|
|
max-height: 90%;
|
|
}
|
|
}
|
|
|
|
.slideshow-mode {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background: black;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.slideshow-container {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.slideshow-slide {
|
|
max-width: 95%;
|
|
max-height: 95%;
|
|
}
|
|
|
|
.slideshow-controls {
|
|
position: absolute;
|
|
bottom: 2rem;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 2rem;
|
|
|
|
.exit-btn {
|
|
padding: 0.5rem 1rem;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: white;
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
|
|
&:hover {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
}
|
|
}
|
|
|
|
.slideshow-navigation {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
color: white;
|
|
|
|
button {
|
|
padding: 0.5rem;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: white;
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
|
|
&:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
&:not(:disabled):hover {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.presentation-header {
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
|
|
.controls {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
.slides-container {
|
|
max-height: 120px;
|
|
}
|
|
|
|
.slide-preview {
|
|
width: 120px;
|
|
height: 90px;
|
|
}
|
|
}
|
|
</style> |