web_ppt / frontend /src /views /Screen /PresenterView.vue
CatPtain's picture
Upload 339 files
89ce340 verified
<template>
<div class="presenter-view">
<div class="toolbar">
<div class="tool-btn" @click="changeViewMode('base')"><IconListView class="tool-icon" /><span>普通视图</span></div>
<div class="tool-btn" :class="{ 'active': writingBoardToolVisible }" @click="writingBoardToolVisible = !writingBoardToolVisible"><IconWrite class="tool-icon" /><span>画笔</span></div>
<div class="tool-btn" :class="{ 'active': laserPen }" @click="laserPen = !laserPen"><IconMagic class="tool-icon" /><span>激光笔</span></div>
<div class="tool-btn" :class="{ 'active': timerlVisible }" @click="timerlVisible = !timerlVisible"><IconStopwatchStart class="tool-icon" /><span>计时器</span></div>
<div class="tool-btn" @click="() => fullscreenState ? manualExitFullscreen() : enterFullscreen()">
<IconOffScreenOne class="tool-icon" v-if="fullscreenState" />
<IconFullScreenOne class="tool-icon" v-else />
<span>{{ fullscreenState ? '退出全屏' : '全屏' }}</span>
</div>
<Divider class="divider" />
<div class="tool-btn" @click="exitScreening()"><IconPower class="tool-icon" /><span>结束放映</span></div>
</div>
<div class="content">
<div
class="slide-list-wrap"
:class="{ 'laser-pen': laserPen }"
ref="slideListWrapRef"
>
<ScreenSlideList
:slideWidth="slideWidth"
:slideHeight="slideHeight"
:animationIndex="animationIndex"
:turnSlideToId="turnSlideToId"
:manualExitFullscreen="manualExitFullscreen"
@wheel="$event => mousewheelListener($event)"
@touchstart="$event => touchStartListener($event)"
@touchend="$event => touchEndListener($event)"
v-contextmenu="contextmenus"
/>
<WritingBoardTool
:slideWidth="slideWidth"
:slideHeight="slideHeight"
:left="-365"
:top="-155"
v-if="writingBoardToolVisible"
@close="writingBoardToolVisible = false"
/>
<CountdownTimer
v-if="timerlVisible"
:left="75"
@close="timerlVisible = false"
/>
</div>
<div class="thumbnails"
ref="thumbnailsRef"
@wheel.prevent="$event => handleMousewheelThumbnails($event)"
>
<div
class="thumbnail"
:class="{ 'active': index === slideIndex }"
v-for="(slide, index) in slides"
:key="slide.id"
@click="turnSlideToIndex(index)"
>
<ThumbnailSlide :slide="slide" :size="120 / viewportRatio" :visible="index < slidesLoadLimit" />
</div>
</div>
</div>
<div class="remark">
<div class="header">
<span>演讲者备注</span>
<span>P {{slideIndex + 1}} / {{slides.length}}</span>
</div>
<div class="remark-content ProseMirror-static" :class="{ 'empty': !currentSlideRemark }" :style="{ fontSize: remarkFontSize + 'px' }" v-html="currentSlideRemark || '无备注'"></div>
<div class="remark-scale">
<div :class="['scale-btn', { 'disable': remarkFontSize === 12 }]" @click="setRemarkFontSize(remarkFontSize - 2)"><IconMinus /></div>
<div :class="['scale-btn', { 'disable': remarkFontSize === 40 }]" @click="setRemarkFontSize(remarkFontSize + 2)"><IconPlus /></div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, nextTick, ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useSlidesStore } from '@/store'
import type { ContextmenuItem } from '@/components/Contextmenu/types'
import { enterFullscreen } from '@/utils/fullscreen'
import { parseText2Paragraphs } from '@/utils/textParser'
import useScreening from '@/hooks/useScreening'
import useLoadSlides from '@/hooks/useLoadSlides'
import useExecPlay from './hooks/useExecPlay'
import useSlideSize from './hooks/useSlideSize'
import useFullscreen from './hooks/useFullscreen'
import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
import ScreenSlideList from './ScreenSlideList.vue'
import WritingBoardTool from './WritingBoardTool.vue'
import CountdownTimer from './CountdownTimer.vue'
import Divider from '@/components/Divider.vue'
const props = defineProps<{
changeViewMode: (mode: 'base' | 'presenter') => void
}>()
const { slides, slideIndex, viewportRatio, currentSlide } = storeToRefs(useSlidesStore())
const slideListWrapRef = ref<HTMLElement>()
const thumbnailsRef = ref<HTMLElement>()
const writingBoardToolVisible = ref(false)
const timerlVisible = ref(false)
const laserPen = ref(false)
const {
mousewheelListener,
touchStartListener,
touchEndListener,
turnPrevSlide,
turnNextSlide,
turnSlideToIndex,
turnSlideToId,
animationIndex,
} = useExecPlay()
const { slideWidth, slideHeight } = useSlideSize(slideListWrapRef)
const { exitScreening } = useScreening()
const { slidesLoadLimit } = useLoadSlides()
const { fullscreenState, manualExitFullscreen } = useFullscreen()
const remarkFontSize = ref(16)
const currentSlideRemark = computed(() => {
if (!currentSlide.value.remark) return ''
return parseText2Paragraphs(currentSlide.value.remark)
})
const handleMousewheelThumbnails = (e: WheelEvent) => {
if (!thumbnailsRef.value) return
thumbnailsRef.value.scrollBy(e.deltaY, 0)
}
const setRemarkFontSize = (fontSize: number) => {
if (fontSize < 12 || fontSize > 40) return
remarkFontSize.value = fontSize
}
watch(slideIndex, () => {
nextTick(() => {
if (!thumbnailsRef.value) return
const activeThumbnailRef: HTMLElement | null = thumbnailsRef.value.querySelector('.thumbnail.active')
if (!activeThumbnailRef) return
const width = thumbnailsRef.value.offsetWidth
const offsetLeft = activeThumbnailRef.offsetLeft + activeThumbnailRef.clientWidth / 2
thumbnailsRef.value.scrollTo({ left: offsetLeft - width / 2, behavior: 'smooth' })
})
})
const contextmenus = (): ContextmenuItem[] => {
return [
{
text: '上一页',
subText: '↑ ←',
disable: slideIndex.value <= 0,
handler: () => turnPrevSlide(),
},
{
text: '下一页',
subText: '↓ →',
disable: slideIndex.value >= slides.value.length - 1,
handler: () => turnNextSlide(),
},
{
text: '第一页',
disable: slideIndex.value === 0,
handler: () => turnSlideToIndex(0),
},
{
text: '最后一页',
disable: slideIndex.value === slides.value.length - 1,
handler: () => turnSlideToIndex(slides.value.length - 1),
},
{ divider: true },
{
text: '画笔工具',
handler: () => writingBoardToolVisible.value = true,
},
{
text: '普通视图',
handler: () => props.changeViewMode('base'),
},
{ divider: true },
{
text: '结束放映',
subText: 'ESC',
handler: exitScreening,
},
]
}
</script>
<style lang="scss" scoped>
.presenter-view {
width: 100%;
height: 100%;
display: flex;
}
.toolbar {
width: 70px;
height: 100%;
background-color: #fff;
border-right: solid 1px #eee;
font-size: 12px;
margin: 20px 0;
.tool-btn {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
& + .tool-btn {
margin-top: 22px;
}
&:hover, &.active {
color: $themeColor;
}
}
.divider {
width: 70%;
margin: 24px 15% !important;
}
.tool-icon {
margin-bottom: 8px;
font-size: 22px;
}
}
.content {
width: calc(100% - 430px);
height: 100%;
background-color: #1d1d1d;
}
.slide-list-wrap {
height: calc(100% - 190px);
margin: 20px;
overflow: hidden;
position: relative;
&.laser-pen {
cursor: url(data:image/png;base64,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) 20 20, default !important;
}
}
.thumbnails {
height: 150px;
padding: 15px;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
border-top: solid 1px #3a3a3a;
position: relative;
}
.thumbnail {
display: inline-block;
outline: 2px solid #aaa;
& + .thumbnail {
margin-left: 10px;
}
&:hover {
outline-color: $themeColor;
}
&.active {
outline-width: 3px;
outline-color: $themeColor;
}
}
.remark {
width: 360px;
height: 100%;
position: relative;
background-color: #2a2a2a;
border-left: solid 1px #3a3a3a;
color: #fff;
.header {
height: 60px;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 18px;
border-bottom: 1px solid #3a3a3a;
}
.remark-content {
height: calc(100% - 60px);
padding: 20px;
line-height: 1.5;
@include overflow-overlay();
&.empty {
color: #999;
font-style: italic;
}
}
.remark-scale {
position: absolute;
right: 5px;
bottom: 5px;
font-size: 22px;
display: flex;
}
.scale-btn {
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
cursor: pointer;
&.disable {
color: #666;
cursor: no-drop;
}
&:not(.disable):hover {
background-color: #333;
}
}
}
::-webkit-scrollbar {
width: 0;
height: 0;
}
</style>