|
<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> |