feat: 幻灯片分节

This commit is contained in:
zxc 2024-07-28 12:50:29 +08:00
parent c8547cc898
commit 074ca6745c
7 changed files with 288 additions and 36 deletions

View File

@ -54,6 +54,7 @@ npm run dev
- Add/delete pages
- Copy/paste pages
- Adjust page order
- Create sections
- Background settings (solid color, gradient, image)
- Set canvas size
- Gridlines

View File

@ -40,6 +40,7 @@ npm run dev
- 页面添加、删除
- 页面顺序调整
- 页面复制粘贴
- 幻灯片分节
- 背景设置(纯色、渐变、图片)
- 设置画布尺寸
- 网格线

View File

@ -142,7 +142,25 @@ export default () => {
const sortSlides = (newIndex: number, oldIndex: number) => {
if (oldIndex === newIndex) return
const _slides = JSON.parse(JSON.stringify(slides.value))
const _slides: Slide[] = JSON.parse(JSON.stringify(slides.value))
const movingSlide = _slides[oldIndex]
const movingSlideSection = movingSlide.sectionTag
if (movingSlideSection) {
const movingSlideSectionNext = _slides[oldIndex + 1]
delete movingSlide.sectionTag
if (movingSlideSectionNext && !movingSlideSectionNext.sectionTag) {
movingSlideSectionNext.sectionTag = movingSlideSection
}
}
if (newIndex === 0) {
const firstSection = _slides[0].sectionTag
if (firstSection) {
delete _slides[0].sectionTag
movingSlide.sectionTag = firstSection
}
}
const _slide = _slides[oldIndex]
_slides.splice(oldIndex, 1)
_slides.splice(newIndex, 0, _slide)
@ -150,6 +168,77 @@ export default () => {
slidesStore.updateSlideIndex(newIndex)
}
const createSection = () => {
slidesStore.updateSlide({
sectionTag: {
id: nanoid(6),
},
})
addHistorySnapshot()
}
const removeSection = (sectionId: string) => {
if (!sectionId) return
const slide = slides.value.find(slide => slide.sectionTag?.id === sectionId)!
slidesStore.removeSlideProps({
id: slide.id,
propName: 'sectionTag',
})
addHistorySnapshot()
}
const removeAllSection = () => {
const _slides = slides.value.map(slide => {
if (slide.sectionTag) delete slide.sectionTag
return slide
})
slidesStore.setSlides(_slides)
addHistorySnapshot()
}
const removeSectionSlides = (sectionId: string) => {
let startIndex = 0
if (sectionId) {
startIndex = slides.value.findIndex(slide => slide.sectionTag?.id === sectionId)
}
const ids: string[] = []
for (let i = startIndex; i < slides.value.length; i++) {
const slide = slides.value[i]
if(i !== startIndex && slide.sectionTag) break
ids.push(slide.id)
}
deleteSlide(ids)
}
const updateSectionTitle = (sectionId: string, title: string) => {
if (!title) return
if (sectionId === 'default') {
slidesStore.updateSlide({
sectionTag: {
id: nanoid(6),
title,
},
}, slides.value[0].id)
}
else {
const slide = slides.value.find(slide => slide.sectionTag?.id === sectionId)
if (!slide) return
slidesStore.updateSlide({
sectionTag: {
...slide.sectionTag!,
title,
},
}, slide.id)
}
addHistorySnapshot()
}
return {
resetSlides,
updateSlideIndex,
@ -162,5 +251,10 @@ export default () => {
cutSlide,
selectAllSlide,
sortSlides,
createSection,
removeSection,
removeAllSection,
removeSectionSlides,
updateSectionTitle,
}
}

View File

@ -6,7 +6,7 @@ import { slides } from '@/mocks/slides'
import { theme } from '@/mocks/theme'
import { layouts } from '@/mocks/layout'
interface RemoveElementPropData {
interface RemovePropData {
id: string
propName: string | string[]
}
@ -126,31 +126,56 @@ export const useSlidesStore = defineStore('slides', {
addSlide(slide: Slide | Slide[]) {
const slides = Array.isArray(slide) ? slide : [slide]
for (const slide of slides) {
if (slide.sectionTag) delete slide.sectionTag
}
const addIndex = this.slideIndex + 1
this.slides.splice(addIndex, 0, ...slides)
this.slideIndex = addIndex
},
updateSlide(props: Partial<Slide>) {
const slideIndex = this.slideIndex
updateSlide(props: Partial<Slide>, slideId?: string) {
const slideIndex = slideId ? this.slides.findIndex(item => item.id === slideId) : this.slideIndex
this.slides[slideIndex] = { ...this.slides[slideIndex], ...props }
},
removeSlideProps(data: RemovePropData) {
const { id, propName } = data
const slides = this.slides.map(slide => {
return slide.id === id ? omit(slide, propName) : slide
}) as Slide[]
this.slides = slides
},
deleteSlide(slideId: string | string[]) {
const slidesId = Array.isArray(slideId) ? slideId : [slideId]
const slides: Slide[] = JSON.parse(JSON.stringify(this.slides))
const deleteSlidesIndex = []
for (let i = 0; i < slidesId.length; i++) {
const index = this.slides.findIndex(item => item.id === slidesId[i])
for (const deletedId of slidesId) {
const index = slides.findIndex(item => item.id === deletedId)
deleteSlidesIndex.push(index)
const deletedSlideSection = slides[index].sectionTag
if(deletedSlideSection) {
const handleSlideNext = slides[index + 1]
if(handleSlideNext && !handleSlideNext.sectionTag) {
delete slides[index].sectionTag
slides[index + 1].sectionTag = deletedSlideSection
}
}
slides.splice(index, 1)
}
let newIndex = Math.min(...deleteSlidesIndex)
const maxIndex = this.slides.length - slidesId.length - 1
const maxIndex = slides.length - 1
if (newIndex > maxIndex) newIndex = maxIndex
this.slideIndex = newIndex
this.slides = this.slides.filter(item => !slidesId.includes(item.id))
this.slides = slides
},
updateSlideIndex(index: number) {
@ -183,7 +208,7 @@ export const useSlidesStore = defineStore('slides', {
this.slides[slideIndex].elements = (elements as PPTElement[])
},
removeElementProps(data: RemoveElementPropData) {
removeElementProps(data: RemovePropData) {
const { id, propName } = data
const propsNames = typeof propName === 'string' ? [propName] : propName

View File

@ -677,6 +677,11 @@ export interface Note {
replies?: NoteReply[]
}
export interface SectionTag {
id: string
title?: string
}
/**
*
*
@ -702,6 +707,7 @@ export interface Slide {
background?: SlideBackground
animations?: PPTAnimation[]
turningMode?: TurningMode
sectionTag?: SectionTag
}
/**

View File

@ -34,7 +34,7 @@
@dblclick="enterEdit(groupItem.id)"
>
<input
:id="`input-${groupItem.id}`"
:id="`select-panel-input-${groupItem.id}`"
:value="groupItem.name || ELEMENT_TYPE_ZH[groupItem.type]"
class="input"
type="text"
@ -145,7 +145,7 @@ const saveElementName = (e: FocusEvent | KeyboardEvent, id: string) => {
const enterEdit = (id: string) => {
editingElId.value = id
nextTick(() => {
const inputRef = document.querySelector(`#input-${id}`) as HTMLInputElement
const inputRef = document.querySelector(`#select-panel-input-${id}`) as HTMLInputElement
inputRef.focus()
})
}

View File

@ -26,20 +26,38 @@
itemKey="id"
>
<template #item="{ element, index }">
<div
class="thumbnail-item"
:class="{
'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="note-flag" v-if="element.notes && element.notes.length" @click="openNotesPanel()">{{ element.notes.length }}</div>
<div class="thumbnail-container">
<div class="section-title"
:data-section-id="element?.sectionTag?.id || ''"
v-if="element.sectionTag || (hasSection && index === 0)"
v-contextmenu="contextmenusSection"
>
<input
:id="`section-title-input-${element?.sectionTag?.id || 'default'}`"
type="text"
:value="element?.sectionTag?.title || ''"
placeholder="输入节名称"
@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>{{ element?.sectionTag ? (element?.sectionTag?.title || '无标题节') : '默认节' }}</span>
</div>
<div
class="thumbnail-item"
:class="{
'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="note-flag" v-if="element.notes && element.notes.length" @click="openNotesPanel()">{{ element.notes.length }}</div>
</div>
</div>
</template>
</Draggable>
@ -68,7 +86,7 @@ const mainStore = useMainStore()
const slidesStore = useSlidesStore()
const keyboardStore = useKeyboardStore()
const { selectedSlidesIndex: _selectedSlidesIndex, thumbnailsFocus } = storeToRefs(mainStore)
const { slides, slideIndex } = storeToRefs(slidesStore)
const { slides, slideIndex, currentSlide } = storeToRefs(slidesStore)
const { ctrlKeyState, shiftKeyState } = storeToRefs(keyboardStore)
const { slidesLoadLimit } = useLoadSlides()
@ -77,6 +95,10 @@ const selectedSlidesIndex = computed(() => [..._selectedSlidesIndex.value, slide
const presetLayoutPopoverVisible = ref(false)
const hasSection = computed(() => {
return slides.value.some(item => item.sectionTag)
})
const {
copySlide,
pasteSlide,
@ -87,6 +109,11 @@ const {
cutSlide,
selectAllSlide,
sortSlides,
createSection,
removeSection,
removeAllSection,
removeSectionSlides,
updateSectionTitle,
} = useSlideHandler()
//
@ -187,6 +214,49 @@ const openNotesPanel = () => {
mainStore.setNotesPanelState(true)
}
const editingSectionId = ref('')
const editSection = (id: string) => {
mainStore.setDisableHotkeysState(true)
editingSectionId.value = id || 'default'
nextTick(() => {
const inputRef = document.querySelector(`#section-title-input-${id || 'default'}`) as HTMLInputElement
inputRef.focus()
})
}
const saveSection = (e: FocusEvent | KeyboardEvent) => {
const title = (e.target as HTMLInputElement).value
updateSectionTitle(editingSectionId.value, title)
editingSectionId.value = ''
mainStore.setDisableHotkeysState(false)
}
const contextmenusSection = (el: HTMLElement): ContextmenuItem[] => {
const sectionId = el.dataset.sectionId!
return [
{
text: '删除节',
handler: () => removeSection(sectionId),
},
{
text: '删除节和幻灯片',
handler: () => removeSectionSlides(sectionId),
},
{
text: '删除所有节',
handler: removeAllSection,
},
{
text: '重命名节',
handler: () => editSection(sectionId),
},
]
}
const { enterScreening, enterScreeningFromStart } = useScreening()
const contextmenusThumbnails = (): ContextmenuItem[] => {
@ -252,6 +322,11 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
subText: 'Delete',
handler: () => deleteSlide(),
},
{
text: '增加节',
handler: createSection,
disable: !!currentSlide.value.sectionTag,
},
{ divider: true },
{
text: '从当前放映',
@ -334,20 +409,27 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
.thumbnail {
outline-color: $themeColor;
}
.note-flag {
background-color: $themeColor;
&::after {
border-top-color: $themeColor;
}
}
}
.note-flag {
width: 18px;
height: 14px;
border-radius: 2px;
width: 16px;
height: 12px;
border-radius: 1px;
position: absolute;
left: 7px;
top: 10px;
left: 8px;
top: 13px;
font-size: 8px;
background-color: $themeColor;
background-color: rgba($color: $themeColor, $alpha: .75);
color: #fff;
text-align: center;
line-height: 14px;
line-height: 12px;
cursor: pointer;
&::after {
@ -355,10 +437,10 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
width: 0;
height: 0;
position: absolute;
top: 13px;
left: 5px;
top: 10px;
left: 4px;
border: 4px solid transparent;
border-top-color: $themeColor;
border-top-color: rgba($color: $themeColor, $alpha: .75);
}
}
}
@ -385,4 +467,47 @@ const contextmenusThumbnailItem = (): ContextmenuItem[] => {
text-align: center;
color: #666;
}
.section-title {
height: 26px;
font-size: 12px;
padding: 6px 8px 2px 18px;
color: #555;
&.contextmenu-active {
color: $themeColor;
.text::before {
border-bottom-color: $themeColor;
border-right-color: $themeColor;
}
}
.text {
width: 100%;
display: inline-block;
display: flex;
align-items: center;
position: relative;
@include ellipsis-oneline();
&::before {
content: '';
width: 0;
height: 0;
border-top: 3px solid transparent;
border-left: 3px solid transparent;
border-bottom: 3px solid #555;
border-right: 3px solid #555;
margin-right: 5px;
}
}
input {
width: 100%;
border: 0;
outline: 0;
padding: 0;
font-size: 12px;
}
}
</style>