331 lines
8.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div
class="thumbnails"
@mousedown="() => setThumbnailsFocus(true)"
v-click-outside="() => setThumbnailsFocus(false)"
v-contextmenu="contextmenusThumbnails"
>
<div class="add-slide">
<div class="btn" @click="createSlide()"><IconPlus class="icon" />添加幻灯片</div>
<Popover trigger="click" placement="bottomLeft" v-model:visible="presetLayoutPopoverVisible">
<template #content>
<LayoutPool @select="slide => { createSlideByTemplate(slide); presetLayoutPopoverVisible = false }" />
</template>
<div class="select-btn"><IconDown /></div>
</Popover>
</div>
<Draggable
class="thumbnail-list"
:modelValue="slides"
:animation="300"
:scroll="true"
:scrollSensitivity="50"
:setData="null"
@end="handleDragEnd"
itemKey="id"
>
<template #item="{ element, index }">
<div
class="thumbnail-item"
:class="{
'active': slideIndex === index,
'selected': selectedSlidesIndex.includes(index),
}"
@mousedown="$event => handleClickSlideThumbnail($event, index)"
v-contextmenu="contextmenusThumbnailItem"
>
<div class="label" :class="{ 'offset-left': index >= 99 }">{{ fillDigit(index + 1, 2) }}</div>
<ThumbnailSlide class="thumbnail" :slide="element" :size="120" :visible="index < slidesLoadLimit" />
</div>
</template>
</Draggable>
<div class="page-number">幻灯片 {{slideIndex + 1}} / {{slides.length}}</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
import { fillDigit } from '@/utils/common'
import { ContextmenuItem } from '@/components/Contextmenu/types'
import useSlideHandler from '@/hooks/useSlideHandler'
import useScreening from '@/hooks/useScreening'
import useLoadSlides from '@/hooks/useLoadSlides'
import Draggable from 'vuedraggable'
import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
import LayoutPool from './LayoutPool.vue'
import { Popover } from 'ant-design-vue'
const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const keyboardStore = useKeyboardStore()
const { selectedSlidesIndex: _selectedSlidesIndex, thumbnailsFocus } = storeToRefs(mainStore)
const { slides, slideIndex } = storeToRefs(slidesStore)
const { ctrlKeyState, shiftKeyState } = storeToRefs(keyboardStore)
const { slidesLoadLimit } = useLoadSlides()
const selectedSlidesIndex = computed(() => [..._selectedSlidesIndex.value, slideIndex.value])
const presetLayoutPopoverVisible = ref(false)
const {
copySlide,
pasteSlide,
createSlide,
createSlideByTemplate,
copyAndPasteSlide,
deleteSlide,
cutSlide,
selectAllSlide,
sortSlides,
} = useSlideHandler()
// 切换页面
const changeSlideIndex = (index: number) => {
mainStore.setActiveElementIdList([])
if (slideIndex.value === index) return
slidesStore.updateSlideIndex(index)
}
// 点击缩略图
const handleClickSlideThumbnail = (e: MouseEvent, index: number) => {
const isMultiSelected = selectedSlidesIndex.value.length > 1
if (isMultiSelected && selectedSlidesIndex.value.includes(index) && e.button !== 0) return
// 按住Ctrl键点选幻灯片再次点击已选中的页面则取消选中
if (ctrlKeyState.value) {
if (slideIndex.value === index) {
if (!isMultiSelected) return
const newSelectedSlidesIndex = selectedSlidesIndex.value.filter(item => item !== index)
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
changeSlideIndex(selectedSlidesIndex.value[0])
}
else {
if (selectedSlidesIndex.value.includes(index)) {
const newSelectedSlidesIndex = selectedSlidesIndex.value.filter(item => item !== index)
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
}
else {
const newSelectedSlidesIndex = [...selectedSlidesIndex.value, index]
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
changeSlideIndex(index)
}
}
}
// 按住Shift键选择范围内的全部幻灯片
else if (shiftKeyState.value) {
if (slideIndex.value === index && !isMultiSelected) return
let minIndex = Math.min(...selectedSlidesIndex.value)
let maxIndex = index
if (index < minIndex) {
maxIndex = Math.max(...selectedSlidesIndex.value)
minIndex = index
}
const newSelectedSlidesIndex = []
for (let i = minIndex; i <= maxIndex; i++) newSelectedSlidesIndex.push(i)
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
changeSlideIndex(index)
}
// 正常切换页面
else {
mainStore.updateSelectedSlidesIndex([])
changeSlideIndex(index)
}
}
// 设置缩略图工具栏聚焦状态(只有聚焦状态下,该部分的快捷键才能生效)
const setThumbnailsFocus = (focus: boolean) => {
if (thumbnailsFocus.value === focus) return
mainStore.setThumbnailsFocus(focus)
if (!focus) mainStore.updateSelectedSlidesIndex([])
}
// 拖拽调整顺序后进行数据的同步
const handleDragEnd = (eventData: { newIndex: number; oldIndex: number }) => {
const { newIndex, oldIndex } = eventData
sortSlides(newIndex, oldIndex)
}
const { enterScreening, enterScreeningFromStart } = useScreening()
const contextmenusThumbnails = (): ContextmenuItem[] => {
return [
{
text: '粘贴',
subText: 'Ctrl + V',
handler: pasteSlide,
},
{
text: '全选',
subText: 'Ctrl + A',
handler: selectAllSlide,
},
{
text: '新建页面',
subText: 'Enter',
handler: createSlide,
},
{
text: '幻灯片放映',
subText: 'F5',
handler: enterScreeningFromStart,
},
]
}
const contextmenusThumbnailItem = (): ContextmenuItem[] => {
return [
{
text: '剪切',
subText: 'Ctrl + X',
handler: cutSlide,
},
{
text: '复制',
subText: 'Ctrl + C',
handler: copySlide,
},
{
text: '粘贴',
subText: 'Ctrl + V',
handler: pasteSlide,
},
{
text: '全选',
subText: 'Ctrl + A',
handler: selectAllSlide,
},
{ divider: true },
{
text: '新建页面',
subText: 'Enter',
handler: createSlide,
},
{
text: '复制页面',
subText: 'Ctrl + D',
handler: copyAndPasteSlide,
},
{
text: '删除页面',
subText: 'Delete',
handler: () => deleteSlide(),
},
{ divider: true },
{
text: '从当前放映',
subText: 'Shift + F5',
handler: enterScreening,
},
]
}
</script>
<style lang="scss" scoped>
.thumbnails {
border-right: solid 1px $borderColor;
background-color: #fff;
display: flex;
flex-direction: column;
user-select: none;
}
.add-slide {
height: 40px;
font-size: 12px;
display: flex;
flex-shrink: 0;
border-bottom: 1px solid $borderColor;
cursor: pointer;
.btn {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
&:hover {
background-color: $lightGray;
}
}
.select-btn {
width: 30px;
display: flex;
justify-content: center;
align-items: center;
border-left: 1px solid $borderColor;
&:hover {
background-color: $lightGray;
}
}
.icon {
margin-right: 3px;
font-size: 14px;
}
}
.thumbnail-list {
padding: 5px 0;
flex: 1;
overflow: auto;
}
.thumbnail-item {
display: flex;
justify-content: center;
align-items: center;
padding: 5px 0;
.thumbnail {
outline: 1px solid rgba($color: $themeColor, $alpha: .15);
}
&.active {
.label {
color: $themeColor;
}
.thumbnail {
outline-color: $themeColor;
}
}
&.selected {
.thumbnail {
outline-color: $themeColor;
}
}
}
.label {
font-size: 12px;
color: #999;
width: 20px;
cursor: grab;
&.offset-left {
position: relative;
left: -4px;
}
&:active {
cursor: grabbing;
}
}
.page-number {
height: 40px;
font-size: 12px;
border-top: 1px solid $borderColor;
line-height: 40px;
text-align: center;
color: #666;
}
</style>