diff --git a/README.md b/README.md index dd16cec8..a6f1ace1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/README_zh.md b/README_zh.md index 1e1aeefc..514026c5 100644 --- a/README_zh.md +++ b/README_zh.md @@ -40,6 +40,7 @@ npm run dev - 页面添加、删除 - 页面顺序调整 - 页面复制粘贴 +- 幻灯片分节 - 背景设置(纯色、渐变、图片) - 设置画布尺寸 - 网格线 diff --git a/src/hooks/useSlideHandler.ts b/src/hooks/useSlideHandler.ts index 51acf7d1..762c6258 100644 --- a/src/hooks/useSlideHandler.ts +++ b/src/hooks/useSlideHandler.ts @@ -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, } } \ No newline at end of file diff --git a/src/store/slides.ts b/src/store/slides.ts index 2311abf4..5028505d 100644 --- a/src/store/slides.ts +++ b/src/store/slides.ts @@ -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) { - const slideIndex = this.slideIndex + updateSlide(props: Partial, 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 diff --git a/src/types/slides.ts b/src/types/slides.ts index f9a514d8..3ece86fa 100644 --- a/src/types/slides.ts +++ b/src/types/slides.ts @@ -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 } /** diff --git a/src/views/Editor/SelectPanel.vue b/src/views/Editor/SelectPanel.vue index a54b62cf..db5f7e37 100644 --- a/src/views/Editor/SelectPanel.vue +++ b/src/views/Editor/SelectPanel.vue @@ -34,7 +34,7 @@ @dblclick="enterEdit(groupItem.id)" > { 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() }) } diff --git a/src/views/Editor/Thumbnails/index.vue b/src/views/Editor/Thumbnails/index.vue index 60db8891..9d5e8b14 100644 --- a/src/views/Editor/Thumbnails/index.vue +++ b/src/views/Editor/Thumbnails/index.vue @@ -26,20 +26,38 @@ itemKey="id" > @@ -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; + } +} \ No newline at end of file