|
@@ -9,9 +9,18 @@
|
|
|
<div class="btn" @click="createSlide()"><IconPlus class="icon" />添加幻灯片</div>
|
|
|
<Popover trigger="click" placement="bottom-start" v-model:value="presetLayoutPopoverVisible" center>
|
|
|
<template #content>
|
|
|
- <LayoutPool @select="slide => { createSlideByTemplate(slide); presetLayoutPopoverVisible = false }" />
|
|
|
+ <LayoutPool
|
|
|
+ @select="
|
|
|
+ slide => {
|
|
|
+ createSlideByTemplate(slide)
|
|
|
+ presetLayoutPopoverVisible = false
|
|
|
+ }
|
|
|
+ "
|
|
|
+ />
|
|
|
</template>
|
|
|
- <div class="select-btn"><IconDown /></div>
|
|
|
+ <div class="select-btn">
|
|
|
+ <img src="./imgs/list.png" alt="" />
|
|
|
+ </div>
|
|
|
</Popover>
|
|
|
</div>
|
|
|
|
|
@@ -28,7 +37,8 @@
|
|
|
>
|
|
|
<template #item="{ element, index }">
|
|
|
<div class="thumbnail-container">
|
|
|
- <div class="section-title"
|
|
|
+ <div
|
|
|
+ class="section-title"
|
|
|
:data-section-id="element?.sectionTag?.id || ''"
|
|
|
v-if="element.sectionTag || (hasSection && index === 0)"
|
|
|
v-contextmenu="contextmenusSection"
|
|
@@ -41,23 +51,29 @@
|
|
|
@blur="$event => saveSection($event)"
|
|
|
@keydown.enter.stop="$event => saveSection($event)"
|
|
|
v-if="editingSectionId === element?.sectionTag?.id || (index === 0 && editingSectionId === 'default')"
|
|
|
- >
|
|
|
+ />
|
|
|
<span class="text" v-else>
|
|
|
- <div class="text-content">{{ element?.sectionTag ? (element?.sectionTag?.title || '无标题节') : '默认节' }}</div>
|
|
|
+ <div class="text-content">{{ element?.sectionTag ? element?.sectionTag?.title || "无标题节" : "默认节" }}</div>
|
|
|
</span>
|
|
|
</div>
|
|
|
<div
|
|
|
class="thumbnail-item"
|
|
|
:class="{
|
|
|
- 'active': slideIndex === index,
|
|
|
- 'selected': selectedSlidesIndex.includes(index),
|
|
|
+ active: slideIndex === index,
|
|
|
+ selected: selectedSlidesIndex.includes(index)
|
|
|
}"
|
|
|
@mousedown="$event => handleClickSlideThumbnail($event, index)"
|
|
|
@dblclick="enterScreening()"
|
|
|
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 class="thumbnail">
|
|
|
+ <ThumbnailSlide :slide="element" :size="180" :visible="index < slidesLoadLimit" />
|
|
|
+ <div class="tools" v-if="slideIndex === index">
|
|
|
+ <img src="./imgs/play.png" @click="enterScreening" alt="" />
|
|
|
+ <img src="./imgs/add.png" @click="createSlide" alt="" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
<div class="note-flag" v-if="element.notes && element.notes.length" @click="openNotesPanel()">{{ element.notes.length }}</div>
|
|
|
</div>
|
|
@@ -65,26 +81,26 @@
|
|
|
</template>
|
|
|
</Draggable>
|
|
|
|
|
|
- <div class="page-number">幻灯片 {{slideIndex + 1}} / {{slides.length}}</div>
|
|
|
+ <div class="page-number">幻灯片 {{ slideIndex + 1 }} / {{ slides.length }}</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
-import { computed, nextTick, ref, watch } from 'vue'
|
|
|
-import { storeToRefs } from 'pinia'
|
|
|
-import { useMainStore, useSlidesStore, useKeyboardStore } from '@/store'
|
|
|
-import { fillDigit } from '@/utils/common'
|
|
|
-import { isElementInViewport } from '@/utils/element'
|
|
|
-import type { ContextmenuItem } from '@/components/Contextmenu/types'
|
|
|
-import useSlideHandler from '@/hooks/useSlideHandler'
|
|
|
-import useSectionHandler from '@/hooks/useSectionHandler'
|
|
|
-import useScreening from '@/hooks/useScreening'
|
|
|
-import useLoadSlides from '@/hooks/useLoadSlides'
|
|
|
-
|
|
|
-import ThumbnailSlide from '@/views/components/ThumbnailSlide/index.vue'
|
|
|
-import LayoutPool from './LayoutPool.vue'
|
|
|
-import Popover from '@/components/Popover.vue'
|
|
|
-import Draggable from 'vuedraggable'
|
|
|
+import { computed, nextTick, ref, watch } from "vue"
|
|
|
+import { storeToRefs } from "pinia"
|
|
|
+import { useMainStore, useSlidesStore, useKeyboardStore } from "@/store"
|
|
|
+import { fillDigit } from "@/utils/common"
|
|
|
+import { isElementInViewport } from "@/utils/element"
|
|
|
+import type { ContextmenuItem } from "@/components/Contextmenu/types"
|
|
|
+import useSlideHandler from "@/hooks/useSlideHandler"
|
|
|
+import useSectionHandler from "@/hooks/useSectionHandler"
|
|
|
+import useScreening from "@/hooks/useScreening"
|
|
|
+import useLoadSlides from "@/hooks/useLoadSlides"
|
|
|
+
|
|
|
+import ThumbnailSlide from "@/views/components/ThumbnailSlide/index.vue"
|
|
|
+import LayoutPool from "./LayoutPool.vue"
|
|
|
+import Popover from "@/components/Popover.vue"
|
|
|
+import Draggable from "vuedraggable"
|
|
|
|
|
|
const mainStore = useMainStore()
|
|
|
const slidesStore = useSlidesStore()
|
|
@@ -103,45 +119,32 @@ const hasSection = computed(() => {
|
|
|
return slides.value.some(item => item.sectionTag)
|
|
|
})
|
|
|
|
|
|
-const {
|
|
|
- copySlide,
|
|
|
- pasteSlide,
|
|
|
- createSlide,
|
|
|
- createSlideByTemplate,
|
|
|
- copyAndPasteSlide,
|
|
|
- deleteSlide,
|
|
|
- cutSlide,
|
|
|
- selectAllSlide,
|
|
|
- sortSlides,
|
|
|
-} = useSlideHandler()
|
|
|
-
|
|
|
-const {
|
|
|
- createSection,
|
|
|
- removeSection,
|
|
|
- removeAllSection,
|
|
|
- removeSectionSlides,
|
|
|
- updateSectionTitle,
|
|
|
-} = useSectionHandler()
|
|
|
+const { copySlide, pasteSlide, createSlide, createSlideByTemplate, copyAndPasteSlide, deleteSlide, cutSlide, selectAllSlide, sortSlides } =
|
|
|
+ useSlideHandler()
|
|
|
+
|
|
|
+const { createSection, removeSection, removeAllSection, removeSectionSlides, updateSectionTitle } = useSectionHandler()
|
|
|
|
|
|
// 页面被切换时
|
|
|
const thumbnailsRef = ref<InstanceType<typeof Draggable>>()
|
|
|
-watch(() => slideIndex.value, () => {
|
|
|
+watch(
|
|
|
+ () => slideIndex.value,
|
|
|
+ () => {
|
|
|
+ // 清除多选状态的幻灯片
|
|
|
+ if (selectedSlidesIndex.value.length) {
|
|
|
+ mainStore.updateSelectedSlidesIndex([])
|
|
|
+ }
|
|
|
|
|
|
- // 清除多选状态的幻灯片
|
|
|
- if (selectedSlidesIndex.value.length) {
|
|
|
- mainStore.updateSelectedSlidesIndex([])
|
|
|
+ // 检查当前页缩略图是否在可视范围,不在的话需要滚动到对应的位置
|
|
|
+ nextTick(() => {
|
|
|
+ const activeThumbnailRef: HTMLElement = thumbnailsRef.value?.$el?.querySelector(".thumbnail-item.active")
|
|
|
+ if (thumbnailsRef.value && activeThumbnailRef && !isElementInViewport(activeThumbnailRef, thumbnailsRef.value.$el)) {
|
|
|
+ setTimeout(() => {
|
|
|
+ activeThumbnailRef.scrollIntoView({ behavior: "smooth" })
|
|
|
+ }, 100)
|
|
|
+ }
|
|
|
+ })
|
|
|
}
|
|
|
-
|
|
|
- // 检查当前页缩略图是否在可视范围,不在的话需要滚动到对应的位置
|
|
|
- nextTick(() => {
|
|
|
- const activeThumbnailRef: HTMLElement = thumbnailsRef.value?.$el?.querySelector('.thumbnail-item.active')
|
|
|
- if (thumbnailsRef.value && activeThumbnailRef && !isElementInViewport(activeThumbnailRef, thumbnailsRef.value.$el)) {
|
|
|
- setTimeout(() => {
|
|
|
- activeThumbnailRef.scrollIntoView({ behavior: 'smooth' })
|
|
|
- }, 100)
|
|
|
- }
|
|
|
- })
|
|
|
-})
|
|
|
+)
|
|
|
|
|
|
// 切换页面
|
|
|
const changeSlideIndex = (index: number) => {
|
|
@@ -168,13 +171,11 @@ const handleClickSlideThumbnail = (e: MouseEvent, index: number) => {
|
|
|
const newSelectedSlidesIndex = selectedSlidesIndex.value.filter(item => item !== index)
|
|
|
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
|
|
|
changeSlideIndex(selectedSlidesIndex.value[0])
|
|
|
- }
|
|
|
- else {
|
|
|
+ } else {
|
|
|
if (selectedSlidesIndex.value.includes(index)) {
|
|
|
const newSelectedSlidesIndex = selectedSlidesIndex.value.filter(item => item !== index)
|
|
|
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
|
|
|
- }
|
|
|
- else {
|
|
|
+ } else {
|
|
|
const newSelectedSlidesIndex = [...selectedSlidesIndex.value, index]
|
|
|
mainStore.updateSelectedSlidesIndex(newSelectedSlidesIndex)
|
|
|
}
|
|
@@ -223,14 +224,14 @@ const openNotesPanel = () => {
|
|
|
mainStore.setNotesPanelState(true)
|
|
|
}
|
|
|
|
|
|
-const editingSectionId = ref('')
|
|
|
+const editingSectionId = ref("")
|
|
|
|
|
|
const editSection = (id: string) => {
|
|
|
mainStore.setDisableHotkeysState(true)
|
|
|
- editingSectionId.value = id || 'default'
|
|
|
+ editingSectionId.value = id || "default"
|
|
|
|
|
|
nextTick(() => {
|
|
|
- const inputRef = document.querySelector(`#section-title-input-${id || 'default'}`) as HTMLInputElement
|
|
|
+ const inputRef = document.querySelector(`#section-title-input-${id || "default"}`) as HTMLInputElement
|
|
|
inputRef.focus()
|
|
|
})
|
|
|
}
|
|
@@ -239,7 +240,7 @@ const saveSection = (e: FocusEvent | KeyboardEvent) => {
|
|
|
const title = (e.target as HTMLInputElement).value
|
|
|
updateSectionTitle(editingSectionId.value, title)
|
|
|
|
|
|
- editingSectionId.value = ''
|
|
|
+ editingSectionId.value = ""
|
|
|
mainStore.setDisableHotkeysState(false)
|
|
|
}
|
|
|
|
|
@@ -248,24 +249,24 @@ const contextmenusSection = (el: HTMLElement): ContextmenuItem[] => {
|
|
|
|
|
|
return [
|
|
|
{
|
|
|
- text: '删除节',
|
|
|
- handler: () => removeSection(sectionId),
|
|
|
+ text: "删除节",
|
|
|
+ handler: () => removeSection(sectionId)
|
|
|
},
|
|
|
{
|
|
|
- text: '删除节和幻灯片',
|
|
|
+ text: "删除节和幻灯片",
|
|
|
handler: () => {
|
|
|
mainStore.setActiveElementIdList([])
|
|
|
removeSectionSlides(sectionId)
|
|
|
- },
|
|
|
+ }
|
|
|
},
|
|
|
{
|
|
|
- text: '删除所有节',
|
|
|
- handler: removeAllSection,
|
|
|
+ text: "删除所有节",
|
|
|
+ handler: removeAllSection
|
|
|
},
|
|
|
{
|
|
|
- text: '重命名节',
|
|
|
- handler: () => editSection(sectionId),
|
|
|
- },
|
|
|
+ text: "重命名节",
|
|
|
+ handler: () => editSection(sectionId)
|
|
|
+ }
|
|
|
]
|
|
|
}
|
|
|
|
|
@@ -274,77 +275,77 @@ const { enterScreening, enterScreeningFromStart } = useScreening()
|
|
|
const contextmenusThumbnails = (): ContextmenuItem[] => {
|
|
|
return [
|
|
|
{
|
|
|
- text: '粘贴',
|
|
|
- subText: 'Ctrl + V',
|
|
|
- handler: pasteSlide,
|
|
|
+ text: "粘贴",
|
|
|
+ subText: "Ctrl + V",
|
|
|
+ handler: pasteSlide
|
|
|
},
|
|
|
{
|
|
|
- text: '全选',
|
|
|
- subText: 'Ctrl + A',
|
|
|
- handler: selectAllSlide,
|
|
|
+ text: "全选",
|
|
|
+ subText: "Ctrl + A",
|
|
|
+ handler: selectAllSlide
|
|
|
},
|
|
|
{
|
|
|
- text: '新建页面',
|
|
|
- subText: 'Enter',
|
|
|
- handler: createSlide,
|
|
|
+ text: "新建页面",
|
|
|
+ subText: "Enter",
|
|
|
+ handler: createSlide
|
|
|
},
|
|
|
{
|
|
|
- text: '幻灯片放映',
|
|
|
- subText: 'F5',
|
|
|
- handler: enterScreeningFromStart,
|
|
|
- },
|
|
|
+ text: "幻灯片放映",
|
|
|
+ subText: "F5",
|
|
|
+ handler: enterScreeningFromStart
|
|
|
+ }
|
|
|
]
|
|
|
}
|
|
|
|
|
|
const contextmenusThumbnailItem = (): ContextmenuItem[] => {
|
|
|
return [
|
|
|
{
|
|
|
- text: '剪切',
|
|
|
- subText: 'Ctrl + X',
|
|
|
- handler: cutSlide,
|
|
|
+ text: "剪切",
|
|
|
+ subText: "Ctrl + X",
|
|
|
+ handler: cutSlide
|
|
|
},
|
|
|
{
|
|
|
- text: '复制',
|
|
|
- subText: 'Ctrl + C',
|
|
|
- handler: copySlide,
|
|
|
+ text: "复制",
|
|
|
+ subText: "Ctrl + C",
|
|
|
+ handler: copySlide
|
|
|
},
|
|
|
{
|
|
|
- text: '粘贴',
|
|
|
- subText: 'Ctrl + V',
|
|
|
- handler: pasteSlide,
|
|
|
+ text: "粘贴",
|
|
|
+ subText: "Ctrl + V",
|
|
|
+ handler: pasteSlide
|
|
|
},
|
|
|
{
|
|
|
- text: '全选',
|
|
|
- subText: 'Ctrl + A',
|
|
|
- handler: selectAllSlide,
|
|
|
+ text: "全选",
|
|
|
+ subText: "Ctrl + A",
|
|
|
+ handler: selectAllSlide
|
|
|
},
|
|
|
{ divider: true },
|
|
|
{
|
|
|
- text: '新建页面',
|
|
|
- subText: 'Enter',
|
|
|
- handler: createSlide,
|
|
|
+ text: "新建页面",
|
|
|
+ subText: "Enter",
|
|
|
+ handler: createSlide
|
|
|
},
|
|
|
{
|
|
|
- text: '复制页面',
|
|
|
- subText: 'Ctrl + D',
|
|
|
- handler: copyAndPasteSlide,
|
|
|
+ text: "复制页面",
|
|
|
+ subText: "Ctrl + D",
|
|
|
+ handler: copyAndPasteSlide
|
|
|
},
|
|
|
{
|
|
|
- text: '删除页面',
|
|
|
- subText: 'Delete',
|
|
|
- handler: () => deleteSlide(),
|
|
|
+ text: "删除页面",
|
|
|
+ subText: "Delete",
|
|
|
+ handler: () => deleteSlide()
|
|
|
},
|
|
|
{
|
|
|
- text: '增加节',
|
|
|
+ text: "增加节",
|
|
|
handler: createSection,
|
|
|
- disable: !!currentSlide.value.sectionTag,
|
|
|
+ disable: !!currentSlide.value.sectionTag
|
|
|
},
|
|
|
{ divider: true },
|
|
|
{
|
|
|
- text: '从当前放映',
|
|
|
- subText: 'Shift + F5',
|
|
|
- handler: enterScreening,
|
|
|
- },
|
|
|
+ text: "从当前放映",
|
|
|
+ subText: "Shift + F5",
|
|
|
+ handler: enterScreening
|
|
|
+ }
|
|
|
]
|
|
|
}
|
|
|
</script>
|
|
@@ -358,34 +359,48 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
|
|
|
user-select: none;
|
|
|
}
|
|
|
.add-slide {
|
|
|
- height: 40px;
|
|
|
+ margin-top: 16px;
|
|
|
+ margin-bottom: 8px;
|
|
|
font-size: 12px;
|
|
|
display: flex;
|
|
|
flex-shrink: 0;
|
|
|
- border-bottom: 1px solid $borderColor;
|
|
|
- cursor: pointer;
|
|
|
-
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
.btn {
|
|
|
- flex: 1;
|
|
|
+ cursor: pointer;
|
|
|
+ flex-shrink: 1;
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
-
|
|
|
+ width: 180px;
|
|
|
+ height: 30px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1px solid #c4c4c4;
|
|
|
&:hover {
|
|
|
background-color: $lightGray;
|
|
|
}
|
|
|
}
|
|
|
.select-btn {
|
|
|
+ cursor: pointer;
|
|
|
+ margin-left: 12px;
|
|
|
width: 30px;
|
|
|
height: 100%;
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
- border-left: 1px solid $borderColor;
|
|
|
-
|
|
|
+ width: 30px;
|
|
|
+ height: 30px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1px solid #c4c4c4;
|
|
|
&:hover {
|
|
|
background-color: $lightGray;
|
|
|
}
|
|
|
+ & > img {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.icon {
|
|
@@ -394,7 +409,6 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
|
|
|
}
|
|
|
}
|
|
|
.thumbnail-list {
|
|
|
- padding: 5px 0;
|
|
|
flex: 1;
|
|
|
overflow: auto;
|
|
|
}
|
|
@@ -402,12 +416,32 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
- padding: 5px 0;
|
|
|
+ padding: 8px 0;
|
|
|
position: relative;
|
|
|
|
|
|
.thumbnail {
|
|
|
- border-radius: $borderRadius;
|
|
|
- outline: 2px solid rgba($color: $themeColor, $alpha: .15);
|
|
|
+ overflow: hidden;
|
|
|
+ border-radius: 4px;
|
|
|
+ outline: 1px solid #dedede;
|
|
|
+ position: relative;
|
|
|
+ .tools {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 8px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ display: flex;
|
|
|
+ & > img {
|
|
|
+ cursor: pointer;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ &:hover {
|
|
|
+ opacity: 0.8;
|
|
|
+ }
|
|
|
+ &:last-child {
|
|
|
+ margin-left: 40px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
&.active {
|
|
@@ -420,7 +454,7 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
|
|
|
}
|
|
|
&.selected {
|
|
|
.thumbnail {
|
|
|
- outline-color: $themeColor;
|
|
|
+ outline: 2px solid $themeColor;
|
|
|
}
|
|
|
.note-flag {
|
|
|
background-color: $themeColor;
|
|
@@ -436,24 +470,24 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
|
|
|
height: 12px;
|
|
|
border-radius: 1px;
|
|
|
position: absolute;
|
|
|
- left: 8px;
|
|
|
- top: 13px;
|
|
|
+ left: 22px;
|
|
|
+ top: 26px;
|
|
|
font-size: 8px;
|
|
|
- background-color: rgba($color: $themeColor, $alpha: .75);
|
|
|
+ background-color: rgba($color: $themeColor, $alpha: 0.75);
|
|
|
color: #fff;
|
|
|
text-align: center;
|
|
|
line-height: 12px;
|
|
|
cursor: pointer;
|
|
|
|
|
|
&::after {
|
|
|
- content: '';
|
|
|
+ content: "";
|
|
|
width: 0;
|
|
|
height: 0;
|
|
|
position: absolute;
|
|
|
top: 10px;
|
|
|
left: 4px;
|
|
|
border: 4px solid transparent;
|
|
|
- border-top-color: rgba($color: $themeColor, $alpha: .75);
|
|
|
+ border-top-color: rgba($color: $themeColor, $alpha: 0.75);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -461,7 +495,10 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
|
|
|
font-size: 12px;
|
|
|
color: #999;
|
|
|
width: 20px;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 14px;
|
|
|
cursor: grab;
|
|
|
+ margin-right: 18px;
|
|
|
|
|
|
&.offset-left {
|
|
|
position: relative;
|
|
@@ -502,7 +539,7 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
|
|
|
position: relative;
|
|
|
|
|
|
&::before {
|
|
|
- content: '';
|
|
|
+ content: "";
|
|
|
width: 0;
|
|
|
height: 0;
|
|
|
border-top: 3px solid transparent;
|