mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
添加放映部分
This commit is contained in:
parent
c58a6a4d24
commit
636c27c37e
12
src/App.vue
12
src/App.vue
@ -1,26 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<Editor />
|
<Editor v-if="!screening" />
|
||||||
|
<Screen v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted } from 'vue'
|
import { computed, defineComponent, onMounted } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import { MutationTypes, ActionTypes, State } from '@/store'
|
import { MutationTypes, ActionTypes, State } from '@/store'
|
||||||
|
|
||||||
import Editor from './views/Editor/index.vue'
|
import Editor from './views/Editor/index.vue'
|
||||||
|
import Screen from './views/Screen/index.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'app',
|
name: 'app',
|
||||||
components: {
|
components: {
|
||||||
Editor,
|
Editor,
|
||||||
|
Screen,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const store = useStore<State>()
|
const store = useStore<State>()
|
||||||
|
const screening = computed(() => store.state.screening)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
store.commit(MutationTypes.SET_AVAILABLE_FONTS)
|
store.commit(MutationTypes.SET_AVAILABLE_FONTS)
|
||||||
store.dispatch(ActionTypes.INIT_SNAPSHOT_DATABASE)
|
store.dispatch(ActionTypes.INIT_SNAPSHOT_DATABASE)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
screening,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -6,6 +6,7 @@ export enum KEYS {
|
|||||||
A = 'A',
|
A = 'A',
|
||||||
G = 'G',
|
G = 'G',
|
||||||
L = 'L',
|
L = 'L',
|
||||||
|
F = 'F',
|
||||||
DELETE = 'DELETE',
|
DELETE = 'DELETE',
|
||||||
UP = 'ARROWUP',
|
UP = 'ARROWUP',
|
||||||
DOWN = 'ARROWDOWN',
|
DOWN = 'ARROWDOWN',
|
||||||
|
13
src/global.d.ts
vendored
Normal file
13
src/global.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
interface HTMLElement {
|
||||||
|
webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
|
||||||
|
mozRequestFullScreen(options?: FullscreenOptions): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Document {
|
||||||
|
mozFullScreen: boolean;
|
||||||
|
webkitIsFullScreen: boolean;
|
||||||
|
webkitFullScreen: boolean;
|
||||||
|
|
||||||
|
mozCancelFullScreen(): Promise<void>;
|
||||||
|
webkitCancelFullScreen(): Promise<void>;
|
||||||
|
}
|
@ -16,7 +16,6 @@ export const slides: Slide[] = [
|
|||||||
width: 320,
|
width: 320,
|
||||||
height: 104,
|
height: 104,
|
||||||
rotate: 0,
|
rotate: 0,
|
||||||
fill: 'rgba(220, 220, 220, 0.8)',
|
|
||||||
shadow: {
|
shadow: {
|
||||||
h: 1,
|
h: 1,
|
||||||
v: 1,
|
v: 1,
|
||||||
@ -56,15 +55,6 @@ export const slides: Slide[] = [
|
|||||||
width: 150,
|
width: 150,
|
||||||
height: 150,
|
height: 150,
|
||||||
rotate: 0,
|
rotate: 0,
|
||||||
outline: {
|
|
||||||
width: 6,
|
|
||||||
style: 'solid',
|
|
||||||
color: '#333'
|
|
||||||
},
|
|
||||||
clip: {
|
|
||||||
range: [[0, 0], [100, 100]],
|
|
||||||
shape: 'roundRect'
|
|
||||||
},
|
|
||||||
fixedRatio: true,
|
fixedRatio: true,
|
||||||
lock: false,
|
lock: false,
|
||||||
src: 'https://img.lessonplan.cn/IMG/Show/ppt/3ab74e91-c34f-499d-9711-166e423d4dd6/62d9adb3-e7a6-4dc4-a352-095cffb49f08/b1be1a2f-f893-47d3-a8a3-eac7d04d395f/1596159381259v2-b2c69096d25ae16bf6ca09e30add3e65_hd.jpg',
|
src: 'https://img.lessonplan.cn/IMG/Show/ppt/3ab74e91-c34f-499d-9711-166e423d4dd6/62d9adb3-e7a6-4dc4-a352-095cffb49f08/b1be1a2f-f893-47d3-a8a3-eac7d04d395f/1596159381259v2-b2c69096d25ae16bf6ca09e30add3e65_hd.jpg',
|
||||||
|
@ -27,6 +27,9 @@ export enum MutationTypes {
|
|||||||
// keyboard
|
// keyboard
|
||||||
SET_CTRL_KEY_STATE = 'setCtrlKeyState',
|
SET_CTRL_KEY_STATE = 'setCtrlKeyState',
|
||||||
SET_SHIFT_KEY_STATE = 'setShiftKeyState',
|
SET_SHIFT_KEY_STATE = 'setShiftKeyState',
|
||||||
|
|
||||||
|
// screen
|
||||||
|
SET_SCREENING = 'SET_SCREENING'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ActionTypes {
|
export enum ActionTypes {
|
||||||
|
@ -26,6 +26,7 @@ export interface State {
|
|||||||
snapshotLength: number;
|
snapshotLength: number;
|
||||||
ctrlKeyState: boolean;
|
ctrlKeyState: boolean;
|
||||||
shiftKeyState: boolean;
|
shiftKeyState: boolean;
|
||||||
|
screening: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: State = {
|
const state: State = {
|
||||||
@ -44,6 +45,7 @@ const state: State = {
|
|||||||
snapshotLength: 0,
|
snapshotLength: 0,
|
||||||
ctrlKeyState: false,
|
ctrlKeyState: false,
|
||||||
shiftKeyState: false,
|
shiftKeyState: false,
|
||||||
|
screening: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
|
@ -120,4 +120,10 @@ export const mutations: MutationTree<State> = {
|
|||||||
[MutationTypes.SET_SHIFT_KEY_STATE](state, isActive: boolean) {
|
[MutationTypes.SET_SHIFT_KEY_STATE](state, isActive: boolean) {
|
||||||
state.shiftKeyState = isActive
|
state.shiftKeyState = isActive
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// screen
|
||||||
|
|
||||||
|
[MutationTypes.SET_SCREENING](state, screening) {
|
||||||
|
state.screening = screening
|
||||||
|
},
|
||||||
}
|
}
|
@ -1,8 +1,21 @@
|
|||||||
// 进入全屏
|
// 进入全屏
|
||||||
export const enterFullscreen = document.documentElement.requestFullscreen
|
export const enterFullscreen = () => {
|
||||||
|
const docElm = document.documentElement
|
||||||
|
if(docElm.requestFullscreen) docElm.requestFullscreen()
|
||||||
|
else if(docElm.mozRequestFullScreen) docElm.mozRequestFullScreen()
|
||||||
|
else if(docElm.webkitRequestFullScreen) docElm.webkitRequestFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
// 退出全屏
|
// 退出全屏
|
||||||
export const exitFullscreen = document.exitFullscreen
|
export const exitFullscreen = () => {
|
||||||
|
if(document.exitFullscreen) document.exitFullscreen()
|
||||||
|
else if(document.mozCancelFullScreen) document.mozCancelFullScreen()
|
||||||
|
else if(document.webkitCancelFullScreen) document.webkitCancelFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
// 判断是否全屏
|
// 判断是否全屏
|
||||||
export const isFullscreen = () => document.fullscreenEnabled
|
export const isFullscreen = () => (
|
||||||
|
document.mozFullScreen ||
|
||||||
|
document.webkitIsFullScreen ||
|
||||||
|
document.webkitFullScreen
|
||||||
|
)
|
@ -2,6 +2,7 @@ import { computed, onMounted, onUnmounted } from 'vue'
|
|||||||
import { useStore } from 'vuex'
|
import { useStore } from 'vuex'
|
||||||
import { State, MutationTypes } from '@/store'
|
import { State, MutationTypes } from '@/store'
|
||||||
import { KEYS } from '@/configs/hotkey'
|
import { KEYS } from '@/configs/hotkey'
|
||||||
|
import { enterFullscreen } from '@/utils/fullscreen'
|
||||||
|
|
||||||
import useSlideHandler from '@/hooks/useSlideHandler'
|
import useSlideHandler from '@/hooks/useSlideHandler'
|
||||||
import useLockElement from '@/hooks/useLockElement'
|
import useLockElement from '@/hooks/useLockElement'
|
||||||
@ -89,10 +90,20 @@ export default () => {
|
|||||||
createSlide()
|
createSlide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enterScreening = () => {
|
||||||
|
enterFullscreen()
|
||||||
|
store.commit(MutationTypes.SET_SCREENING, true)
|
||||||
|
}
|
||||||
|
|
||||||
const keydownListener = (e: KeyboardEvent) => {
|
const keydownListener = (e: KeyboardEvent) => {
|
||||||
const { ctrlKey, shiftKey } = e
|
const { ctrlKey, shiftKey } = e
|
||||||
const key = e.key.toUpperCase()
|
const key = e.key.toUpperCase()
|
||||||
|
|
||||||
|
if(ctrlKey && key === KEYS.F) {
|
||||||
|
e.preventDefault()
|
||||||
|
enterScreening()
|
||||||
|
}
|
||||||
|
|
||||||
if(ctrlKey && !ctrlKeyActive.value) store.commit(MutationTypes.SET_CTRL_KEY_STATE, true)
|
if(ctrlKey && !ctrlKeyActive.value) store.commit(MutationTypes.SET_CTRL_KEY_STATE, true)
|
||||||
if(shiftKey && !shiftKeyActive.value) store.commit(MutationTypes.SET_SHIFT_KEY_STATE, true)
|
if(shiftKey && !shiftKeyActive.value) store.commit(MutationTypes.SET_SHIFT_KEY_STATE, true)
|
||||||
|
|
||||||
|
68
src/views/Screen/ScreenSlide.vue
Normal file
68
src/views/Screen/ScreenSlide.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="screen-slide"
|
||||||
|
:style="{
|
||||||
|
width: VIEWPORT_SIZE + 'px',
|
||||||
|
height: VIEWPORT_SIZE * VIEWPORT_ASPECT_RATIO + 'px',
|
||||||
|
transform: `scale(${scale})`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="background" :style="{ ...backgroundStyle }"></div>
|
||||||
|
<BaseElement
|
||||||
|
v-for="(element, index) in slide.elements"
|
||||||
|
:key="element.id"
|
||||||
|
:elementInfo="element"
|
||||||
|
:elementIndex="index + 1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, PropType, defineComponent } from 'vue'
|
||||||
|
import { Slide } from '@/types/slides'
|
||||||
|
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
|
||||||
|
import useSlideBackgroundStyle from '@/hooks/useSlideBackgroundStyle'
|
||||||
|
|
||||||
|
import BaseElement from '@/views/_common/_element/BaseElement.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'screen-slide',
|
||||||
|
components: {
|
||||||
|
BaseElement,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
slide: {
|
||||||
|
type: Object as PropType<Slide>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const background = computed(() => props.slide.background)
|
||||||
|
const { backgroundStyle } = useSlideBackgroundStyle(background)
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundStyle,
|
||||||
|
VIEWPORT_SIZE,
|
||||||
|
VIEWPORT_ASPECT_RATIO,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.screen-slide {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
191
src/views/Screen/index.vue
Normal file
191
src/views/Screen/index.vue
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
<template>
|
||||||
|
<div class="hamster-ppt-screen">
|
||||||
|
<div
|
||||||
|
class="slide-list"
|
||||||
|
@mousewheel="$event => mousewheelListener($event)"
|
||||||
|
v-contextmenu="contextmenus"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'slide-item',
|
||||||
|
{
|
||||||
|
'show': index === slideIndex,
|
||||||
|
'prev': index < slideIndex,
|
||||||
|
'next': index > slideIndex,
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
v-for="(slide, index) in slides"
|
||||||
|
:key="slide.id"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="slide-content"
|
||||||
|
:style="{
|
||||||
|
width: slideWidth + 'px',
|
||||||
|
height: slideHeight + 'px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ScreenSlide :scale="scale" :slide="slide" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import throttle from 'lodash/throttle'
|
||||||
|
import { MutationTypes, State } from '@/store'
|
||||||
|
import { exitFullscreen, isFullscreen } from '@/utils/fullscreen'
|
||||||
|
import { VIEWPORT_ASPECT_RATIO, VIEWPORT_SIZE } from '@/configs/canvas'
|
||||||
|
import { KEYS } from '@/configs/hotkey'
|
||||||
|
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||||
|
|
||||||
|
import ScreenSlide from './ScreenSlide.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'screen',
|
||||||
|
components: {
|
||||||
|
ScreenSlide,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const store = useStore<State>()
|
||||||
|
const slides = computed(() => store.state.slides)
|
||||||
|
const slideIndex = computed(() => store.state.slideIndex)
|
||||||
|
|
||||||
|
const slideWidth = ref(0)
|
||||||
|
const slideHeight = ref(0)
|
||||||
|
const scale = computed(() => slideWidth.value / VIEWPORT_SIZE)
|
||||||
|
|
||||||
|
const setSlideContentSize = () => {
|
||||||
|
const winWidth = document.body.clientWidth
|
||||||
|
const winHeight = document.body.clientHeight
|
||||||
|
let width, height
|
||||||
|
|
||||||
|
if(winHeight / winWidth === VIEWPORT_ASPECT_RATIO) {
|
||||||
|
width = winWidth
|
||||||
|
height = winHeight
|
||||||
|
}
|
||||||
|
else if(winHeight / winWidth > VIEWPORT_ASPECT_RATIO) {
|
||||||
|
width = winWidth
|
||||||
|
height = winWidth * VIEWPORT_ASPECT_RATIO
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
width = winHeight / VIEWPORT_ASPECT_RATIO
|
||||||
|
height = winHeight
|
||||||
|
}
|
||||||
|
slideWidth.value = width
|
||||||
|
slideHeight.value = height
|
||||||
|
}
|
||||||
|
|
||||||
|
const windowResizeListener = () => {
|
||||||
|
setSlideContentSize()
|
||||||
|
if(!isFullscreen()) store.commit(MutationTypes.SET_SCREENING, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const turnPrevSlide = () => {
|
||||||
|
if(slideIndex.value <= 0) return
|
||||||
|
store.commit(MutationTypes.UPDATE_SLIDE_INDEX, slideIndex.value - 1)
|
||||||
|
}
|
||||||
|
const turnNextSlide = () => {
|
||||||
|
if(slideIndex.value >= slides.value.length - 1) return
|
||||||
|
store.commit(MutationTypes.UPDATE_SLIDE_INDEX, slideIndex.value + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const keydownListener = (e: KeyboardEvent) => {
|
||||||
|
const key = e.key.toUpperCase()
|
||||||
|
if(key === KEYS.UP || key === KEYS.LEFT) turnPrevSlide()
|
||||||
|
else if(key === KEYS.DOWN || key === KEYS.RIGHT) turnNextSlide()
|
||||||
|
}
|
||||||
|
|
||||||
|
const mousewheelListener = throttle(function(e: WheelEvent) {
|
||||||
|
if(e.deltaY > 0) turnNextSlide()
|
||||||
|
else if(e.deltaY < 0) turnPrevSlide()
|
||||||
|
}, 500, { leading: true, trailing: false })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', windowResizeListener)
|
||||||
|
document.addEventListener('keydown', keydownListener)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', windowResizeListener)
|
||||||
|
document.removeEventListener('keydown', keydownListener)
|
||||||
|
})
|
||||||
|
|
||||||
|
const contextmenus = (): ContextmenuItem[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: '上一页',
|
||||||
|
disable: slideIndex.value <= 0,
|
||||||
|
handler: () => turnPrevSlide(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '下一页',
|
||||||
|
disable: slideIndex.value >= slides.value.length - 1,
|
||||||
|
handler: () => turnNextSlide(),
|
||||||
|
},
|
||||||
|
{ divider: true },
|
||||||
|
{
|
||||||
|
text: '结束放映',
|
||||||
|
subText: 'ESC',
|
||||||
|
handler: exitFullscreen,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
slides,
|
||||||
|
slideIndex,
|
||||||
|
slideWidth,
|
||||||
|
slideHeight,
|
||||||
|
scale,
|
||||||
|
mousewheelListener,
|
||||||
|
contextmenus,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.hamster-ppt-screen {
|
||||||
|
width: 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%;
|
||||||
|
transition-property: transform;
|
||||||
|
transition-duration: .4s;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
&.prev {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
&.next {
|
||||||
|
transform: translateX(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>
|
@ -1,13 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="hamster-ppt-slide-show">
|
|
||||||
slide-show
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'slide-show',
|
|
||||||
})
|
|
||||||
</script>
|
|
Loading…
x
Reference in New Issue
Block a user