mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 添加放映演讲者视图
This commit is contained in:
parent
7ea0fd4286
commit
fe5c3aa234
@ -287,6 +287,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.writing-board {
|
.writing-board {
|
||||||
|
z-index: 8;
|
||||||
cursor: none;
|
cursor: none;
|
||||||
@include absolute-0();
|
@include absolute-0();
|
||||||
}
|
}
|
||||||
|
@ -91,6 +91,7 @@ import {
|
|||||||
FullScreenOne,
|
FullScreenOne,
|
||||||
OffScreenOne,
|
OffScreenOne,
|
||||||
Power,
|
Power,
|
||||||
|
ListView,
|
||||||
} from '@icon-park/vue-next'
|
} from '@icon-park/vue-next'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -205,6 +206,7 @@ export default {
|
|||||||
app.component('IconFolderClose', FolderClose)
|
app.component('IconFolderClose', FolderClose)
|
||||||
app.component('IconElectronicPen', ElectronicPen)
|
app.component('IconElectronicPen', ElectronicPen)
|
||||||
app.component('IconPower', Power)
|
app.component('IconPower', Power)
|
||||||
|
app.component('IconListView', ListView)
|
||||||
|
|
||||||
// 视频播放器
|
// 视频播放器
|
||||||
app.component('IconPause', Pause)
|
app.component('IconPause', Pause)
|
||||||
|
272
src/views/Screen/BaseView.vue
Normal file
272
src/views/Screen/BaseView.vue
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
<template>
|
||||||
|
<div class="base-view">
|
||||||
|
<ScreenSlideList
|
||||||
|
:slideWidth="slideWidth"
|
||||||
|
:slideHeight="slideHeight"
|
||||||
|
:animationIndex="animationIndex"
|
||||||
|
:turnSlideToId="turnSlideToId"
|
||||||
|
@mousewheel="$event => mousewheelListener($event)"
|
||||||
|
@touchstart="$event => touchStartListener($event)"
|
||||||
|
@touchend="$event => touchEndListener($event)"
|
||||||
|
v-contextmenu="contextmenus"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SlideThumbnails
|
||||||
|
v-if="slideThumbnailModelVisible"
|
||||||
|
:turnSlideToIndex="turnSlideToIndex"
|
||||||
|
@close="slideThumbnailModelVisible = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<WritingBoardTool
|
||||||
|
:slideWidth="slideWidth"
|
||||||
|
:slideHeight="slideHeight"
|
||||||
|
v-if="writingBoardToolVisible"
|
||||||
|
@close="writingBoardToolVisible = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="tools-left">
|
||||||
|
<IconLeftTwo class="tool-btn" theme="two-tone" :fill="['#111', '#fff']" @click="execPrev()" />
|
||||||
|
<IconRightTwo class="tool-btn" theme="two-tone" :fill="['#111', '#fff']" @click="execNext()" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="tools-right" :class="{ 'visible': rightToolsVisible }"
|
||||||
|
@mouseleave="rightToolsVisible = false"
|
||||||
|
@mouseenter="rightToolsVisible = true"
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
<div class="tool-btn page-number" @click="slideThumbnailModelVisible = true">幻灯片 {{slideIndex + 1}} / {{slides.length}}</div>
|
||||||
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.3" title="画笔工具">
|
||||||
|
<IconWrite class="tool-btn" @click="writingBoardToolVisible = true" />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.3" title="演讲者视图">
|
||||||
|
<IconListView class="tool-btn" @click="changeViewMode('presenter')" />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.3" :title="fullscreenState ? '退出全屏' : '进入全屏'">
|
||||||
|
<IconOffScreenOne class="tool-btn" v-if="fullscreenState" @click="exitFullscreen()" />
|
||||||
|
<IconFullScreenOne class="tool-btn" v-else @click="enterFullscreen()" />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.3" title="结束放映">
|
||||||
|
<IconPower class="tool-btn" @click="exitScreening()" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType, ref } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useSlidesStore } from '@/store'
|
||||||
|
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||||
|
import { enterFullscreen, exitFullscreen } from '@/utils/fullscreen'
|
||||||
|
import useScreening from '@/hooks/useScreening'
|
||||||
|
import useExecPlay from './hooks/useExecPlay'
|
||||||
|
import useSlideSize from './hooks/useSlideSize'
|
||||||
|
import useFullscreenState from './hooks/useFullscreenState'
|
||||||
|
|
||||||
|
import ScreenSlideList from './ScreenSlideList.vue'
|
||||||
|
import SlideThumbnails from './SlideThumbnails.vue'
|
||||||
|
import WritingBoardTool from './WritingBoardTool.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'screen',
|
||||||
|
components: {
|
||||||
|
ScreenSlideList,
|
||||||
|
SlideThumbnails,
|
||||||
|
WritingBoardTool,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
changeViewMode: {
|
||||||
|
type: Function as PropType<(mode: 'base' | 'presenter') => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const { slides, slideIndex } = storeToRefs(useSlidesStore())
|
||||||
|
|
||||||
|
const {
|
||||||
|
autoPlayTimer,
|
||||||
|
autoPlay,
|
||||||
|
closeAutoPlay,
|
||||||
|
mousewheelListener,
|
||||||
|
touchStartListener,
|
||||||
|
touchEndListener,
|
||||||
|
turnPrevSlide,
|
||||||
|
turnNextSlide,
|
||||||
|
turnSlideToIndex,
|
||||||
|
turnSlideToId,
|
||||||
|
execPrev,
|
||||||
|
execNext,
|
||||||
|
animationIndex,
|
||||||
|
} = useExecPlay()
|
||||||
|
|
||||||
|
const { slideWidth, slideHeight } = useSlideSize()
|
||||||
|
const { exitScreening } = useScreening()
|
||||||
|
const { fullscreenState } = useFullscreenState()
|
||||||
|
|
||||||
|
const rightToolsVisible = ref(false)
|
||||||
|
const writingBoardToolVisible = ref(false)
|
||||||
|
const slideThumbnailModelVisible = ref(false)
|
||||||
|
|
||||||
|
|
||||||
|
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: () => rightToolsVisible.value = true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '查看所有幻灯片',
|
||||||
|
handler: () => slideThumbnailModelVisible.value = true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '画笔工具',
|
||||||
|
handler: () => writingBoardToolVisible.value = true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '演讲者视图',
|
||||||
|
handler: () => props.changeViewMode('presenter'),
|
||||||
|
},
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
text: autoPlayTimer.value ? '取消自动放映' : '自动放映',
|
||||||
|
handler: autoPlayTimer.value ? closeAutoPlay : autoPlay,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '结束放映',
|
||||||
|
subText: 'ESC',
|
||||||
|
handler: exitScreening,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
slides,
|
||||||
|
slideIndex,
|
||||||
|
slideWidth,
|
||||||
|
slideHeight,
|
||||||
|
mousewheelListener,
|
||||||
|
touchStartListener,
|
||||||
|
touchEndListener,
|
||||||
|
animationIndex,
|
||||||
|
contextmenus,
|
||||||
|
execPrev,
|
||||||
|
execNext,
|
||||||
|
turnSlideToIndex,
|
||||||
|
turnSlideToId,
|
||||||
|
slideThumbnailModelVisible,
|
||||||
|
writingBoardToolVisible,
|
||||||
|
rightToolsVisible,
|
||||||
|
fullscreenState,
|
||||||
|
exitScreening,
|
||||||
|
enterFullscreen,
|
||||||
|
exitFullscreen,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.base-view {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.tools-left {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 8px;
|
||||||
|
font-size: 25px;
|
||||||
|
color: #666;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.tool-btn {
|
||||||
|
opacity: .35;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: .9;
|
||||||
|
}
|
||||||
|
& + .tool-btn {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tools-right {
|
||||||
|
height: 66px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: -66px;
|
||||||
|
right: 0;
|
||||||
|
z-index: 5;
|
||||||
|
padding: 8px;
|
||||||
|
transition: bottom $transitionDelay;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
width: 100%;
|
||||||
|
height: 66px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: -66px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: $borderRadius;
|
||||||
|
font-size: 25px;
|
||||||
|
background-color: #fff;
|
||||||
|
color: $textColor;
|
||||||
|
padding: 8px 10px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
|
||||||
|
border: 1px solid #e2e6ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $themeColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .tool-btn {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.page-number {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
354
src/views/Screen/PresenterView.vue
Normal file
354
src/views/Screen/PresenterView.vue
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
<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" @click="writingBoardToolVisible = !writingBoardToolVisible"><IconWrite class="tool-icon" /><span>画笔</span></div>
|
||||||
|
<div class="tool-btn" @click="() => fullscreenState ? exitFullscreen() : enterFullscreen()">
|
||||||
|
<IconOffScreenOne class="tool-icon" v-if="fullscreenState" />
|
||||||
|
<IconOffScreenOne 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" ref="slideListWrapRef">
|
||||||
|
<ScreenSlideList
|
||||||
|
:slideWidth="slideWidth"
|
||||||
|
:slideHeight="slideHeight"
|
||||||
|
:animationIndex="animationIndex"
|
||||||
|
:turnSlideToId="turnSlideToId"
|
||||||
|
@mousewheel="$event => mousewheelListener($event)"
|
||||||
|
@touchstart="$event => touchStartListener($event)"
|
||||||
|
@touchend="$event => touchEndListener($event)"
|
||||||
|
v-contextmenu="contextmenus"
|
||||||
|
/>
|
||||||
|
<WritingBoardTool
|
||||||
|
:slideWidth="slideWidth"
|
||||||
|
:slideHeight="slideHeight"
|
||||||
|
:position="{
|
||||||
|
left: '75px',
|
||||||
|
top: '5px',
|
||||||
|
}"
|
||||||
|
v-if="writingBoardToolVisible"
|
||||||
|
@close="writingBoardToolVisible = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="thumbnails"
|
||||||
|
ref="thumbnailsRef"
|
||||||
|
@mousewheel.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" :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">
|
||||||
|
import { computed, defineComponent, nextTick, ref, watch, PropType } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useSlidesStore } from '@/store'
|
||||||
|
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||||
|
import { enterFullscreen, exitFullscreen } 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 useFullscreenState from './hooks/useFullscreenState'
|
||||||
|
|
||||||
|
import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
|
||||||
|
import ScreenSlideList from './ScreenSlideList.vue'
|
||||||
|
import WritingBoardTool from './WritingBoardTool.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'presenter-view',
|
||||||
|
components: {
|
||||||
|
ScreenSlideList,
|
||||||
|
ThumbnailSlide,
|
||||||
|
WritingBoardTool,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
changeViewMode: {
|
||||||
|
type: Function as PropType<(mode: 'base' | 'presenter') => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const { slides, slideIndex, viewportRatio, currentSlide } = storeToRefs(useSlidesStore())
|
||||||
|
|
||||||
|
const slideListWrapRef = ref<HTMLElement>()
|
||||||
|
const thumbnailsRef = ref<HTMLElement>()
|
||||||
|
const writingBoardToolVisible = 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 } = useFullscreenState()
|
||||||
|
|
||||||
|
const remarkFontSize = ref(16)
|
||||||
|
const currentSlideRemark = computed(() => {
|
||||||
|
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
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
slides,
|
||||||
|
slideIndex,
|
||||||
|
viewportRatio,
|
||||||
|
remarkFontSize,
|
||||||
|
currentSlideRemark,
|
||||||
|
setRemarkFontSize,
|
||||||
|
slideListWrapRef,
|
||||||
|
thumbnailsRef,
|
||||||
|
slideWidth,
|
||||||
|
slideHeight,
|
||||||
|
animationIndex,
|
||||||
|
turnSlideToId,
|
||||||
|
mousewheelListener,
|
||||||
|
touchStartListener,
|
||||||
|
touchEndListener,
|
||||||
|
turnSlideToIndex,
|
||||||
|
contextmenus,
|
||||||
|
slidesLoadLimit,
|
||||||
|
handleMousewheelThumbnails,
|
||||||
|
exitScreening,
|
||||||
|
fullscreenState,
|
||||||
|
enterFullscreen,
|
||||||
|
exitFullscreen,
|
||||||
|
writingBoardToolVisible,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</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 {
|
||||||
|
color: $themeColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
width: 70%;
|
||||||
|
min-width: 70%;
|
||||||
|
margin: 24px 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
.thumbnails {
|
||||||
|
height: 150px;
|
||||||
|
padding: 15px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
border-top: solid 1px #3a3a3a;
|
||||||
|
}
|
||||||
|
.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
@ -76,6 +76,7 @@ export default defineComponent({
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.background {
|
.background {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
153
src/views/Screen/ScreenSlideList.vue
Normal file
153
src/views/Screen/ScreenSlideList.vue
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<div class="screen-slide-list">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'slide-item',
|
||||||
|
`turning-mode-${slide.turningMode || 'slideY'}`,
|
||||||
|
{
|
||||||
|
'current': index === slideIndex,
|
||||||
|
'before': index < slideIndex,
|
||||||
|
'after': index > slideIndex,
|
||||||
|
'hide': (index === slideIndex - 1 || index === slideIndex + 1) && slide.turningMode !== currentSlide.turningMode,
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
v-for="(slide, index) in slides"
|
||||||
|
:key="slide.id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="slide-content"
|
||||||
|
:style="{
|
||||||
|
width: slideWidth + 'px',
|
||||||
|
height: slideHeight + 'px',
|
||||||
|
}"
|
||||||
|
v-if="Math.abs(slideIndex - index) < 2"
|
||||||
|
>
|
||||||
|
<ScreenSlide
|
||||||
|
:slide="slide"
|
||||||
|
:scale="scale"
|
||||||
|
:animationIndex="animationIndex"
|
||||||
|
:turnSlideToId="turnSlideToId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, PropType, provide } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useSlidesStore } from '@/store'
|
||||||
|
import { VIEWPORT_SIZE } from '@/configs/canvas'
|
||||||
|
|
||||||
|
import ScreenSlide from './ScreenSlide.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'screen-slide-list',
|
||||||
|
components: {
|
||||||
|
ScreenSlide,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
slideWidth: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
slideHeight: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
animationIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: -1,
|
||||||
|
},
|
||||||
|
turnSlideToId: {
|
||||||
|
type: Function as PropType<(id: string) => void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const { slides, slideIndex, currentSlide } = storeToRefs(useSlidesStore())
|
||||||
|
|
||||||
|
const scale = computed(() => props.slideWidth / VIEWPORT_SIZE)
|
||||||
|
provide('slideScale', scale)
|
||||||
|
|
||||||
|
return {
|
||||||
|
slides,
|
||||||
|
slideIndex,
|
||||||
|
currentSlide,
|
||||||
|
scale,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.screen-slide-list {
|
||||||
|
background: #1d1d1d;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.slide-item {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&.current {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hide {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.turning-mode-no {
|
||||||
|
&.before {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
&.after {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.turning-mode-fade {
|
||||||
|
transition: opacity .75s;
|
||||||
|
&.before {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
&.after {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.turning-mode-slideX {
|
||||||
|
transition: transform .35s;
|
||||||
|
&.before {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
&.after {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.turning-mode-slideY {
|
||||||
|
transition: transform .35s;
|
||||||
|
&.before {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
&.after {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.slide-content {
|
||||||
|
background-color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
@ -14,7 +14,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tools">
|
<div class="tools" :style="position">
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.3" title="画笔">
|
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.3" title="画笔">
|
||||||
<div class="btn" :class="{ 'active': writingBoardModel === 'pen' }" @click="changePen()"><IconWrite class="icon" /></div>
|
<div class="btn" :class="{ 'active': writingBoardModel === 'pen' }" @click="changePen()"><IconWrite class="icon" /></div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -45,11 +45,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, ref } from 'vue'
|
import { defineComponent, PropType, ref } from 'vue'
|
||||||
import WritingBoard from '@/components/WritingBoard.vue'
|
import WritingBoard from '@/components/WritingBoard.vue'
|
||||||
|
|
||||||
const writingBoardColors = ['#000000', '#ffffff', '#1e497b', '#4e81bb', '#e2534d', '#9aba60', '#8165a0', '#47acc5', '#f9974c']
|
const writingBoardColors = ['#000000', '#ffffff', '#1e497b', '#4e81bb', '#e2534d', '#9aba60', '#8165a0', '#47acc5', '#f9974c']
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
left?: number | string;
|
||||||
|
right?: number | string;
|
||||||
|
top?: number | string;
|
||||||
|
bottom?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'writing-board-tool',
|
name: 'writing-board-tool',
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
@ -65,6 +72,13 @@ export default defineComponent({
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
position: {
|
||||||
|
type: Object as PropType<Position>,
|
||||||
|
default: () => ({
|
||||||
|
right: '5px',
|
||||||
|
bottom: '5px',
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const writingBoardRef = ref()
|
const writingBoardRef = ref()
|
||||||
@ -130,8 +144,6 @@ export default defineComponent({
|
|||||||
.tools {
|
.tools {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 5px;
|
|
||||||
right: 5px;
|
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
|
171
src/views/Screen/hooks/useExecPlay.ts
Normal file
171
src/views/Screen/hooks/useExecPlay.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import { throttle } from 'lodash'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useSlidesStore } from '@/store'
|
||||||
|
import { KEYS } from '@/configs/hotkey'
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const slidesStore = useSlidesStore()
|
||||||
|
const { slides, slideIndex, currentSlide } = storeToRefs(slidesStore)
|
||||||
|
|
||||||
|
// 当前页的元素动画列表和当前执行到的位置
|
||||||
|
const animations = computed(() => currentSlide.value.animations || [])
|
||||||
|
const animationIndex = ref(0)
|
||||||
|
|
||||||
|
// 执行元素的入场动画
|
||||||
|
const runAnimation = () => {
|
||||||
|
const prefix = 'animate__'
|
||||||
|
const animation = animations.value[animationIndex.value]
|
||||||
|
animationIndex.value += 1
|
||||||
|
|
||||||
|
const elRef = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
|
||||||
|
if (elRef) {
|
||||||
|
const animationName = `${prefix}${animation.type}`
|
||||||
|
document.documentElement.style.setProperty('--animate-duration', `${animation.duration}ms`)
|
||||||
|
elRef.classList.add(`${prefix}animated`, animationName)
|
||||||
|
|
||||||
|
const handleAnimationEnd = () => {
|
||||||
|
document.documentElement.style.removeProperty('--animate-duration')
|
||||||
|
elRef.classList.remove(`${prefix}animated`, animationName)
|
||||||
|
}
|
||||||
|
elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭自动播放
|
||||||
|
const autoPlayTimer = ref(0)
|
||||||
|
const closeAutoPlay = () => {
|
||||||
|
if (autoPlayTimer.value) {
|
||||||
|
clearInterval(autoPlayTimer.value)
|
||||||
|
autoPlayTimer.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onUnmounted(closeAutoPlay)
|
||||||
|
|
||||||
|
const throttleMassage = throttle(function(msg) {
|
||||||
|
message.success(msg)
|
||||||
|
}, 1000, { leading: true, trailing: false })
|
||||||
|
|
||||||
|
// 向上/向下播放
|
||||||
|
// 遇到元素动画时,优先执行动画播放,无动画则执行翻页
|
||||||
|
// 向上播放遇到动画时,仅撤销到动画执行前的状态,不需要反向播放动画
|
||||||
|
const execPrev = () => {
|
||||||
|
if (animations.value.length && animationIndex.value > 0) {
|
||||||
|
animationIndex.value -= 1
|
||||||
|
}
|
||||||
|
else if (slideIndex.value > 0) {
|
||||||
|
slidesStore.updateSlideIndex(slideIndex.value - 1)
|
||||||
|
const lastIndex = animations.value ? animations.value.length : 0
|
||||||
|
animationIndex.value = lastIndex
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throttleMassage('已经是第一页了')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const execNext = () => {
|
||||||
|
if (animations.value.length && animationIndex.value < animations.value.length) {
|
||||||
|
runAnimation()
|
||||||
|
}
|
||||||
|
else if (slideIndex.value < slides.value.length - 1) {
|
||||||
|
slidesStore.updateSlideIndex(slideIndex.value + 1)
|
||||||
|
animationIndex.value = 0
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throttleMassage('已经是最后一页了')
|
||||||
|
closeAutoPlay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动播放
|
||||||
|
const autoPlay = () => {
|
||||||
|
closeAutoPlay()
|
||||||
|
message.success('开始自动放映')
|
||||||
|
autoPlayTimer.value = setInterval(execNext, 2500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 鼠标滚动翻页
|
||||||
|
const mousewheelListener = throttle(function(e: WheelEvent) {
|
||||||
|
if (e.deltaY < 0) execPrev()
|
||||||
|
else if (e.deltaY > 0) execNext()
|
||||||
|
}, 500, { leading: true, trailing: false })
|
||||||
|
|
||||||
|
// 触摸屏上下滑动翻页
|
||||||
|
const touchInfo = ref<{ x: number; y: number; } | null>(null)
|
||||||
|
|
||||||
|
const touchStartListener = (e: TouchEvent) => {
|
||||||
|
touchInfo.value = {
|
||||||
|
x: e.changedTouches[0].pageX,
|
||||||
|
y: e.changedTouches[0].pageY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const touchEndListener = (e: TouchEvent) => {
|
||||||
|
if (!touchInfo.value) return
|
||||||
|
|
||||||
|
const offsetX = Math.abs(touchInfo.value.x - e.changedTouches[0].pageX)
|
||||||
|
const offsetY = e.changedTouches[0].pageY - touchInfo.value.y
|
||||||
|
|
||||||
|
if ( Math.abs(offsetY) > offsetX && Math.abs(offsetY) > 50 ) {
|
||||||
|
touchInfo.value = null
|
||||||
|
|
||||||
|
if (offsetY > 0) execPrev()
|
||||||
|
else execNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快捷键翻页
|
||||||
|
const keydownListener = (e: KeyboardEvent) => {
|
||||||
|
const key = e.key.toUpperCase()
|
||||||
|
|
||||||
|
if (key === KEYS.UP || key === KEYS.LEFT) execPrev()
|
||||||
|
else if (
|
||||||
|
key === KEYS.DOWN ||
|
||||||
|
key === KEYS.RIGHT ||
|
||||||
|
key === KEYS.SPACE ||
|
||||||
|
key === KEYS.ENTER
|
||||||
|
) execNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => document.addEventListener('keydown', keydownListener))
|
||||||
|
onUnmounted(() => document.removeEventListener('keydown', keydownListener))
|
||||||
|
|
||||||
|
// 切换到上一张/上一张幻灯片(无视元素的入场动画)
|
||||||
|
const turnPrevSlide = () => {
|
||||||
|
slidesStore.updateSlideIndex(slideIndex.value - 1)
|
||||||
|
animationIndex.value = 0
|
||||||
|
}
|
||||||
|
const turnNextSlide = () => {
|
||||||
|
slidesStore.updateSlideIndex(slideIndex.value + 1)
|
||||||
|
animationIndex.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换幻灯片到指定的页面
|
||||||
|
const turnSlideToIndex = (index: number) => {
|
||||||
|
slidesStore.updateSlideIndex(index)
|
||||||
|
animationIndex.value = 0
|
||||||
|
}
|
||||||
|
const turnSlideToId = (id: string) => {
|
||||||
|
const index = slides.value.findIndex(slide => slide.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
slidesStore.updateSlideIndex(index)
|
||||||
|
animationIndex.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
autoPlayTimer,
|
||||||
|
autoPlay,
|
||||||
|
closeAutoPlay,
|
||||||
|
mousewheelListener,
|
||||||
|
touchStartListener,
|
||||||
|
touchEndListener,
|
||||||
|
turnPrevSlide,
|
||||||
|
turnNextSlide,
|
||||||
|
turnSlideToIndex,
|
||||||
|
turnSlideToId,
|
||||||
|
execPrev,
|
||||||
|
execNext,
|
||||||
|
animationIndex,
|
||||||
|
}
|
||||||
|
}
|
15
src/views/Screen/hooks/useFullscreenState.ts
Normal file
15
src/views/Screen/hooks/useFullscreenState.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import { isFullscreen } from '@/utils/fullscreen'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const fullscreenState = ref(true)
|
||||||
|
|
||||||
|
const windowResizeListener = () => fullscreenState.value = isFullscreen()
|
||||||
|
|
||||||
|
onMounted(() => window.addEventListener('resize', windowResizeListener))
|
||||||
|
onUnmounted(() => window.removeEventListener('resize', windowResizeListener))
|
||||||
|
|
||||||
|
return {
|
||||||
|
fullscreenState,
|
||||||
|
}
|
||||||
|
}
|
47
src/views/Screen/hooks/useSlideSize.ts
Normal file
47
src/views/Screen/hooks/useSlideSize.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { onMounted, onUnmounted, Ref, ref } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { useSlidesStore } from '@/store'
|
||||||
|
|
||||||
|
export default (wrapRef?: Ref<HTMLElement | undefined>) => {
|
||||||
|
const slidesStore = useSlidesStore()
|
||||||
|
const { viewportRatio } = storeToRefs(slidesStore)
|
||||||
|
|
||||||
|
const slideWidth = ref(0)
|
||||||
|
const slideHeight = ref(0)
|
||||||
|
|
||||||
|
// 计算和更新幻灯片内容的尺寸(按比例自适应屏幕)
|
||||||
|
const setSlideContentSize = () => {
|
||||||
|
const slideWrapRef = wrapRef?.value || document.body
|
||||||
|
const winWidth = slideWrapRef.clientWidth
|
||||||
|
const winHeight = slideWrapRef.clientHeight
|
||||||
|
let width, height
|
||||||
|
|
||||||
|
if (winHeight / winWidth === viewportRatio.value) {
|
||||||
|
width = winWidth
|
||||||
|
height = winHeight
|
||||||
|
}
|
||||||
|
else if (winHeight / winWidth > viewportRatio.value) {
|
||||||
|
width = winWidth
|
||||||
|
height = winWidth * viewportRatio.value
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
width = winHeight / viewportRatio.value
|
||||||
|
height = winHeight
|
||||||
|
}
|
||||||
|
slideWidth.value = width
|
||||||
|
slideHeight.value = height
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setSlideContentSize()
|
||||||
|
window.addEventListener('resize', setSlideContentSize)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', setSlideContentSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
slideWidth,
|
||||||
|
slideHeight,
|
||||||
|
}
|
||||||
|
}
|
@ -1,385 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="pptist-screen">
|
<div class="pptist-screen">
|
||||||
<div
|
<BaseView :changeViewMode="changeViewMode" v-if="viewMode === 'base'" />
|
||||||
class="slide-list"
|
<PresenterView :changeViewMode="changeViewMode" v-else-if="viewMode === 'presenter'" />
|
||||||
@mousewheel="$event => mousewheelListener($event)"
|
|
||||||
@touchstart="$event => touchStartListener($event)"
|
|
||||||
@touchend="$event => touchEndListener($event)"
|
|
||||||
v-contextmenu="contextmenus"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
'slide-item',
|
|
||||||
`turning-mode-${slide.turningMode || 'slideY'}`,
|
|
||||||
{
|
|
||||||
'current': index === slideIndex,
|
|
||||||
'before': index < slideIndex,
|
|
||||||
'after': index > slideIndex,
|
|
||||||
'hide': (index === slideIndex - 1 || index === slideIndex + 1) && slide.turningMode !== currentSlide.turningMode,
|
|
||||||
}
|
|
||||||
]"
|
|
||||||
v-for="(slide, index) in slides"
|
|
||||||
:key="slide.id"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="slide-content"
|
|
||||||
:style="{
|
|
||||||
width: slideWidth + 'px',
|
|
||||||
height: slideHeight + 'px',
|
|
||||||
}"
|
|
||||||
v-if="Math.abs(slideIndex - index) < 2"
|
|
||||||
>
|
|
||||||
<ScreenSlide
|
|
||||||
:slide="slide"
|
|
||||||
:scale="scale"
|
|
||||||
:animationIndex="animationIndex"
|
|
||||||
:turnSlideToId="turnSlideToId"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SlideThumbnails
|
|
||||||
v-if="slideThumbnailModelVisible"
|
|
||||||
:turnSlideToIndex="turnSlideToIndex"
|
|
||||||
@close="slideThumbnailModelVisible = false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<WritingBoardTool
|
|
||||||
:slideWidth="slideWidth"
|
|
||||||
:slideHeight="slideHeight"
|
|
||||||
v-if="writingBoardToolVisible"
|
|
||||||
@close="writingBoardToolVisible = false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="tools-left">
|
|
||||||
<IconLeftTwo class="tool-btn" theme="two-tone" :fill="['#111', '#fff']" @click="execPrev()" />
|
|
||||||
<IconRightTwo class="tool-btn" theme="two-tone" :fill="['#111', '#fff']" @click="execNext()" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="tools-right" :class="{ 'visible': rightToolsVisible }"
|
|
||||||
@mouseleave="rightToolsVisible = false"
|
|
||||||
@mouseenter="rightToolsVisible = true"
|
|
||||||
>
|
|
||||||
<div class="content">
|
|
||||||
<div class="tool-btn page-number" @click="slideThumbnailModelVisible = true">幻灯片 {{slideIndex + 1}} / {{slides.length}}</div>
|
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.3" title="画笔工具">
|
|
||||||
<IconWrite class="tool-btn" @click="writingBoardToolVisible = true" />
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.3" :title="fullscreenState ? '退出全屏' : '进入全屏'">
|
|
||||||
<IconOffScreenOne class="tool-btn" v-if="fullscreenState" @click="exitFullscreen()" />
|
|
||||||
<IconFullScreenOne class="tool-btn" v-else @click="enterFullscreen()" />
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip :mouseLeaveDelay="0" :mouseEnterDelay="0.3" title="结束放映">
|
|
||||||
<IconPower class="tool-btn" @click="exitScreening()" />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, onMounted, onUnmounted, provide, ref } from 'vue'
|
import { defineComponent, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { throttle } from 'lodash'
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { useSlidesStore } from '@/store'
|
|
||||||
import { VIEWPORT_SIZE } from '@/configs/canvas'
|
|
||||||
import { KEYS } from '@/configs/hotkey'
|
import { KEYS } from '@/configs/hotkey'
|
||||||
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
|
||||||
import { isFullscreen, enterFullscreen, exitFullscreen } from '@/utils/fullscreen'
|
|
||||||
import useScreening from '@/hooks/useScreening'
|
import useScreening from '@/hooks/useScreening'
|
||||||
|
|
||||||
import { message } from 'ant-design-vue'
|
import BaseView from './BaseView.vue'
|
||||||
|
import PresenterView from './PresenterView.vue'
|
||||||
import ScreenSlide from './ScreenSlide.vue'
|
|
||||||
import SlideThumbnails from './SlideThumbnails.vue'
|
|
||||||
import WritingBoardTool from './WritingBoardTool.vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'screen',
|
name: 'pptist-screen',
|
||||||
components: {
|
components: {
|
||||||
ScreenSlide,
|
BaseView,
|
||||||
SlideThumbnails,
|
PresenterView,
|
||||||
WritingBoardTool,
|
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const slidesStore = useSlidesStore()
|
const viewMode = ref<'base' | 'presenter'>('base')
|
||||||
const { slides, slideIndex, currentSlide, viewportRatio } = storeToRefs(slidesStore)
|
|
||||||
|
|
||||||
const slideWidth = ref(0)
|
const changeViewMode = (mode: 'base' | 'presenter') => {
|
||||||
const slideHeight = ref(0)
|
viewMode.value = mode
|
||||||
|
}
|
||||||
const scale = computed(() => slideWidth.value / VIEWPORT_SIZE)
|
|
||||||
|
|
||||||
const rightToolsVisible = ref(false)
|
|
||||||
const slideThumbnailModelVisible = ref(false)
|
|
||||||
const writingBoardToolVisible = ref(false)
|
|
||||||
|
|
||||||
const { exitScreening } = useScreening()
|
const { exitScreening } = useScreening()
|
||||||
|
|
||||||
// 计算和更新幻灯片内容的尺寸(按比例自适应屏幕)
|
// 快捷键退出放映
|
||||||
const setSlideContentSize = () => {
|
|
||||||
const winWidth = document.body.clientWidth
|
|
||||||
const winHeight = document.body.clientHeight
|
|
||||||
let width, height
|
|
||||||
|
|
||||||
if (winHeight / winWidth === viewportRatio.value) {
|
|
||||||
width = winWidth
|
|
||||||
height = winHeight
|
|
||||||
}
|
|
||||||
else if (winHeight / winWidth > viewportRatio.value) {
|
|
||||||
width = winWidth
|
|
||||||
height = winWidth * viewportRatio.value
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
width = winHeight / viewportRatio.value
|
|
||||||
height = winHeight
|
|
||||||
}
|
|
||||||
slideWidth.value = width
|
|
||||||
slideHeight.value = height
|
|
||||||
}
|
|
||||||
|
|
||||||
// 窗口尺寸变化监听:窗口发生变化时更新幻灯片的大小
|
|
||||||
const fullscreenState = ref(true)
|
|
||||||
|
|
||||||
const windowResizeListener = () => {
|
|
||||||
setSlideContentSize()
|
|
||||||
fullscreenState.value = isFullscreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
setSlideContentSize()
|
|
||||||
window.addEventListener('resize', windowResizeListener)
|
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', windowResizeListener)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 当前页的元素动画列表和当前执行到的位置
|
|
||||||
const animations = computed(() => currentSlide.value.animations || [])
|
|
||||||
const animationIndex = ref(0)
|
|
||||||
|
|
||||||
// 执行元素的入场动画
|
|
||||||
const runAnimation = () => {
|
|
||||||
const prefix = 'animate__'
|
|
||||||
const animation = animations.value[animationIndex.value]
|
|
||||||
animationIndex.value += 1
|
|
||||||
|
|
||||||
const elRef = document.querySelector(`#screen-element-${animation.elId} [class^=base-element-]`)
|
|
||||||
if (elRef) {
|
|
||||||
const animationName = `${prefix}${animation.type}`
|
|
||||||
document.documentElement.style.setProperty('--animate-duration', `${animation.duration}ms`)
|
|
||||||
elRef.classList.add(`${prefix}animated`, animationName)
|
|
||||||
|
|
||||||
const handleAnimationEnd = () => {
|
|
||||||
document.documentElement.style.removeProperty('--animate-duration')
|
|
||||||
elRef.classList.remove(`${prefix}animated`, animationName)
|
|
||||||
}
|
|
||||||
elRef.addEventListener('animationend', handleAnimationEnd, { once: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭自动播放
|
|
||||||
const autoPlayTimer = ref(0)
|
|
||||||
const closeAutoPlay = () => {
|
|
||||||
if (autoPlayTimer.value) {
|
|
||||||
clearInterval(autoPlayTimer.value)
|
|
||||||
autoPlayTimer.value = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onUnmounted(closeAutoPlay)
|
|
||||||
|
|
||||||
const throttleMassage = throttle(function(msg) {
|
|
||||||
message.success(msg)
|
|
||||||
}, 1000, { leading: true, trailing: false })
|
|
||||||
|
|
||||||
// 向上/向下播放
|
|
||||||
// 遇到元素动画时,优先执行动画播放,无动画则执行翻页
|
|
||||||
// 向上播放遇到动画时,仅撤销到动画执行前的状态,不需要反向播放动画
|
|
||||||
const execPrev = () => {
|
|
||||||
if (animations.value.length && animationIndex.value > 0) {
|
|
||||||
animationIndex.value -= 1
|
|
||||||
}
|
|
||||||
else if (slideIndex.value > 0) {
|
|
||||||
slidesStore.updateSlideIndex(slideIndex.value - 1)
|
|
||||||
const lastIndex = animations.value ? animations.value.length : 0
|
|
||||||
animationIndex.value = lastIndex
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throttleMassage('已经是第一页了')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const execNext = () => {
|
|
||||||
if (animations.value.length && animationIndex.value < animations.value.length) {
|
|
||||||
runAnimation()
|
|
||||||
}
|
|
||||||
else if (slideIndex.value < slides.value.length - 1) {
|
|
||||||
slidesStore.updateSlideIndex(slideIndex.value + 1)
|
|
||||||
animationIndex.value = 0
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throttleMassage('已经是最后一页了')
|
|
||||||
closeAutoPlay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动播放
|
|
||||||
const autoPlay = () => {
|
|
||||||
closeAutoPlay()
|
|
||||||
message.success('开始自动放映')
|
|
||||||
autoPlayTimer.value = setInterval(execNext, 2500)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 鼠标滚动翻页
|
|
||||||
const mousewheelListener = throttle(function(e: WheelEvent) {
|
|
||||||
if (e.deltaY < 0) execPrev()
|
|
||||||
else if (e.deltaY > 0) execNext()
|
|
||||||
}, 500, { leading: true, trailing: false })
|
|
||||||
|
|
||||||
// 触摸屏上下滑动翻页
|
|
||||||
const touchInfo = ref<{ x: number; y: number; } | null>(null)
|
|
||||||
|
|
||||||
const touchStartListener = (e: TouchEvent) => {
|
|
||||||
touchInfo.value = {
|
|
||||||
x: e.changedTouches[0].pageX,
|
|
||||||
y: e.changedTouches[0].pageY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const touchEndListener = (e: TouchEvent) => {
|
|
||||||
if (!touchInfo.value) return
|
|
||||||
|
|
||||||
const offsetX = Math.abs(touchInfo.value.x - e.changedTouches[0].pageX)
|
|
||||||
const offsetY = e.changedTouches[0].pageY - touchInfo.value.y
|
|
||||||
|
|
||||||
if ( Math.abs(offsetY) > offsetX && Math.abs(offsetY) > 50 ) {
|
|
||||||
touchInfo.value = null
|
|
||||||
|
|
||||||
if (offsetY > 0) execPrev()
|
|
||||||
else execNext()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 快捷键翻页/退出
|
|
||||||
const keydownListener = (e: KeyboardEvent) => {
|
const keydownListener = (e: KeyboardEvent) => {
|
||||||
const key = e.key.toUpperCase()
|
const key = e.key.toUpperCase()
|
||||||
|
|
||||||
if (key === KEYS.ESC) exitScreening()
|
if (key === KEYS.ESC) exitScreening()
|
||||||
else if (key === KEYS.UP || key === KEYS.LEFT) execPrev()
|
|
||||||
else if (
|
|
||||||
key === KEYS.DOWN ||
|
|
||||||
key === KEYS.RIGHT ||
|
|
||||||
key === KEYS.SPACE ||
|
|
||||||
key === KEYS.ENTER
|
|
||||||
) execNext()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => document.addEventListener('keydown', keydownListener))
|
||||||
document.addEventListener('keydown', keydownListener)
|
onUnmounted(() => document.removeEventListener('keydown', keydownListener))
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
document.removeEventListener('keydown', keydownListener)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 切换到上一张/上一张幻灯片(无视元素的入场动画)
|
|
||||||
const turnPrevSlide = () => {
|
|
||||||
slidesStore.updateSlideIndex(slideIndex.value - 1)
|
|
||||||
animationIndex.value = 0
|
|
||||||
}
|
|
||||||
const turnNextSlide = () => {
|
|
||||||
slidesStore.updateSlideIndex(slideIndex.value + 1)
|
|
||||||
animationIndex.value = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换幻灯片到指定的页面
|
|
||||||
const turnSlideToIndex = (index: number) => {
|
|
||||||
slideThumbnailModelVisible.value = false
|
|
||||||
slidesStore.updateSlideIndex(index)
|
|
||||||
animationIndex.value = 0
|
|
||||||
}
|
|
||||||
const turnSlideToId = (id: string) => {
|
|
||||||
const index = slides.value.findIndex(slide => slide.id === id)
|
|
||||||
if (index !== -1) {
|
|
||||||
slidesStore.updateSlideIndex(index)
|
|
||||||
animationIndex.value = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: () => rightToolsVisible.value = true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '查看所有幻灯片',
|
|
||||||
handler: () => slideThumbnailModelVisible.value = true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '画笔工具',
|
|
||||||
handler: () => writingBoardToolVisible.value = true,
|
|
||||||
},
|
|
||||||
{ divider: true },
|
|
||||||
{
|
|
||||||
text: autoPlayTimer.value ? '取消自动放映' : '自动放映',
|
|
||||||
handler: autoPlayTimer.value ? closeAutoPlay : autoPlay,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '结束放映',
|
|
||||||
subText: 'ESC',
|
|
||||||
handler: exitScreening,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
provide('slideScale', scale)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
slides,
|
viewMode,
|
||||||
slideIndex,
|
changeViewMode,
|
||||||
currentSlide,
|
|
||||||
slideWidth,
|
|
||||||
slideHeight,
|
|
||||||
scale,
|
|
||||||
mousewheelListener,
|
|
||||||
touchStartListener,
|
|
||||||
touchEndListener,
|
|
||||||
animationIndex,
|
|
||||||
contextmenus,
|
|
||||||
execPrev,
|
|
||||||
execNext,
|
|
||||||
turnSlideToIndex,
|
|
||||||
turnSlideToId,
|
|
||||||
slideThumbnailModelVisible,
|
|
||||||
writingBoardToolVisible,
|
|
||||||
rightToolsVisible,
|
|
||||||
fullscreenState,
|
|
||||||
exitScreening,
|
|
||||||
enterFullscreen,
|
|
||||||
exitFullscreen,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -389,151 +49,5 @@ export default defineComponent({
|
|||||||
.pptist-screen {
|
.pptist-screen {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
|
||||||
background-color: #111;
|
|
||||||
}
|
|
||||||
.slide-list {
|
|
||||||
background: #1d1d1d;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.slide-item {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
&.current {
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.hide {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.turning-mode-no {
|
|
||||||
&.before {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
&.after {
|
|
||||||
transform: translateY(100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.turning-mode-fade {
|
|
||||||
transition: opacity .75s;
|
|
||||||
&.before {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
&.after {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.turning-mode-slideX {
|
|
||||||
transition: transform .35s;
|
|
||||||
&.before {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
&.after {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.turning-mode-slideY {
|
|
||||||
transition: transform .35s;
|
|
||||||
&.before {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
&.after {
|
|
||||||
transform: translateY(100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.slide-content {
|
|
||||||
background-color: #fff;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tools-left {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 8px;
|
|
||||||
left: 8px;
|
|
||||||
font-size: 25px;
|
|
||||||
color: #666;
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
.tool-btn {
|
|
||||||
opacity: .35;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: .9;
|
|
||||||
}
|
|
||||||
& + .tool-btn {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tools-right {
|
|
||||||
height: 66px;
|
|
||||||
position: fixed;
|
|
||||||
bottom: -66px;
|
|
||||||
right: 0;
|
|
||||||
z-index: 5;
|
|
||||||
padding: 8px;
|
|
||||||
transition: bottom $transitionDelay;
|
|
||||||
|
|
||||||
&.visible {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
width: 100%;
|
|
||||||
height: 66px;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: -66px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: $borderRadius;
|
|
||||||
font-size: 25px;
|
|
||||||
background-color: #fff;
|
|
||||||
color: $textColor;
|
|
||||||
padding: 8px 10px;
|
|
||||||
box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
|
|
||||||
border: 1px solid #e2e6ed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-btn {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $themeColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
& + .tool-btn {
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.page-number {
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
Loading…
x
Reference in New Issue
Block a user