mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
331 lines
8.1 KiB
Vue
331 lines
8.1 KiB
Vue
<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> |