2022-01-09 15:30:45 +08:00

352 lines
9.2 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>
</template>
<script lang="ts">
import { computed, defineComponent, 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'
export default defineComponent({
name: 'thumbnails',
components: {
Draggable,
ThumbnailSlide,
LayoutPool,
},
setup() {
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,
} = useSlideHandler()
// 切换页面
const changSlideIndex = (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)
changSlideIndex(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)
changSlideIndex(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)
changSlideIndex(index)
}
// 正常切换页面
else {
mainStore.updateSelectedSlidesIndex([])
changSlideIndex(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
if (oldIndex === newIndex) return
const _slides = JSON.parse(JSON.stringify(slides.value))
const _slide = _slides[oldIndex]
_slides.splice(oldIndex, 1)
_slides.splice(newIndex, 0, _slide)
slidesStore.setSlides(_slides)
slidesStore.updateSlideIndex(newIndex)
}
const { enterScreening } = useScreening()
const contextmenusThumbnails = (): ContextmenuItem[] => {
return [
{
text: '粘贴',
subText: 'Ctrl + V',
handler: pasteSlide,
},
{
text: '全选',
subText: 'Ctrl + A',
handler: selectAllSlide,
},
{
text: '新建页面',
subText: 'Enter',
handler: createSlide,
},
{
text: '开始演示',
subText: 'Ctrl + F',
handler: enterScreening,
},
]
}
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: 'Ctrl + F',
handler: enterScreening,
},
]
}
return {
slides,
slideIndex,
selectedSlidesIndex,
presetLayoutPopoverVisible,
slidesLoadLimit,
createSlide,
createSlideByTemplate,
setThumbnailsFocus,
handleClickSlideThumbnail,
contextmenusThumbnails,
contextmenusThumbnailItem,
fillDigit,
handleDragEnd,
}
},
})
</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;
}
}
</style>