diff --git a/src/components/Message.vue b/src/components/Message.vue index 878dacd76..a322fcdcf 100644 --- a/src/components/Message.vue +++ b/src/components/Message.vue @@ -16,6 +16,7 @@ +
{{ title }}
@@ -44,6 +45,7 @@ const { IconCloseOne, IconInfo, IconCloseSmall, + IconRefresh, } = icons const props = withDefaults(defineProps<{ @@ -179,4 +181,17 @@ defineExpose({ margin-top: -45px; } } + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.loading-icon { + animation: spin 1s linear infinite; +} \ No newline at end of file diff --git a/src/hooks/useAIPPT.ts b/src/hooks/useAIPPT.ts index 0f6ad053e..0f2730a1f 100644 --- a/src/hooks/useAIPPT.ts +++ b/src/hooks/useAIPPT.ts @@ -2,7 +2,7 @@ import { ref } from 'vue' import { nanoid } from 'nanoid' import type { ImageClipDataRange, PPTElement, PPTImageElement, PPTShapeElement, PPTTextElement, Slide, TextType } from '@/types/slides' import type { AIPPTSlide } from '@/types/AIPPT' -import { useSlidesStore } from '@/store' +import { useSlidesStore, useMainStore } from '@/store' import useAddSlidesOrElements from './useAddSlidesOrElements' import useSlideHandler from './useSlideHandler' @@ -15,6 +15,7 @@ interface ImgPoolItem { export default () => { const slidesStore = useSlidesStore() + const mainStore = useMainStore() const { addSlidesFromData } = useAddSlidesOrElements() const { isEmptySlide } = useSlideHandler() @@ -248,7 +249,56 @@ export default () => { imgPool.value = imgs } - const AIPPT = (templateSlides: Slide[], _AISlides: AIPPTSlide[], imgs?: ImgPoolItem[]) => { + /** + * 渐进式添加幻灯片,模拟流式生成效果 + * @param slides + * @param delayPerElement + */ + const progressiveAddSlides = async (slides: Slide[], delayPerElement = 200) => { + for (let i = 0; i < slides.length; i++) { + const slide = slides[i] + const elements = slide.elements + // 先添加一个空的幻灯片(无元素) + const emptySlide: Slide = { + ...slide, + elements: [] + } + // 判断是否应该覆盖还是追加 + const shouldSetSlides = i === 0 && isEmptySlide.value + if (shouldSetSlides) { + slidesStore.setSlides([emptySlide]) + } + else { + // 临时切换到最后一个幻灯片,确保在正确位置添加 + const lastSlideIndex = slidesStore.slides.length - 1 + slidesStore.updateSlideIndex(lastSlideIndex) + // 现在在最后添加空幻灯片 + addSlidesFromData([emptySlide]) + } + + // 确定目标幻灯片索引(总是在最后一张) + const targetSlideIndex = slidesStore.slides.length - 1 + + // 逐个添加元素 + for (const element of elements) { + // 直接操作目标幻灯片,不依赖当前选中的幻灯片 + const targetSlide = slidesStore.slides[targetSlideIndex] + targetSlide.elements.push(element) + // 选中当前添加的元素,产生选中效果 + mainStore.setActiveElementIdList([element.id]) + // 等待一段时间,让用户看到选中效果 + await new Promise(resolve => setTimeout(resolve, delayPerElement)) + } + // 清除选中状态 + mainStore.setActiveElementIdList([]) + // 幻灯片之间稍微长一点的延迟 + if (i < slides.length - 1) { + await new Promise(resolve => setTimeout(resolve, delayPerElement * 2)) + } + } + } + + const AIPPT = async (templateSlides: Slide[], _AISlides: AIPPTSlide[], imgs?: ImgPoolItem[]) => { slidesStore.updateSlideIndex(slidesStore.slides.length - 1) if (imgs) imgPool.value = imgs @@ -533,8 +583,7 @@ export default () => { }) } } - if (isEmptySlide.value) slidesStore.setSlides(slides) - else addSlidesFromData(slides) + await progressiveAddSlides(slides) } return { diff --git a/src/utils/message.ts b/src/utils/message.ts index 84a4ce382..64f84533c 100644 --- a/src/utils/message.ts +++ b/src/utils/message.ts @@ -2,7 +2,7 @@ import { createVNode, render, type AppContext } from 'vue' import MessageComponent from '@/components/Message.vue' export interface MessageOptions { - type?: 'info' | 'success' | 'warning' | 'error' + type?: 'info' | 'success' | 'warning' | 'error' | 'loading' title?: string message?: string duration?: number @@ -24,6 +24,7 @@ export interface Message { success: MessageFn error: MessageFn warning: MessageFn + loading: MessageFn closeAll: () => void _context?: AppContext | null } @@ -93,6 +94,7 @@ message.success = (msg: string, options?: MessageTypeOptions) => message({ ...op message.info = (msg: string, options?: MessageTypeOptions) => message({ ...options, type: 'info', message: msg }) message.warning = (msg: string, options?: MessageTypeOptions) => message({ ...options, type: 'warning', message: msg }) message.error = (msg: string, options?: MessageTypeOptions) => message({ ...options, type: 'error', message: msg }) +message.loading = (msg: string, options?: MessageTypeOptions) => message({ ...options, type: 'loading', message: msg, duration: 0}) message.closeAll = function() { for (let i = instances.length - 1; i >= 0; i--) { diff --git a/src/views/Editor/AIPPTDialog.vue b/src/views/Editor/AIPPTDialog.vue index 2f4d947b2..039b5d56d 100644 --- a/src/views/Editor/AIPPTDialog.vue +++ b/src/views/Editor/AIPPTDialog.vue @@ -225,7 +225,9 @@ const createOutline = async () => { } const createPPT = async (template?: { slides: Slide[], theme: SlideTheme }) => { + mainStore.setAIPPTDialogState(false) loading.value = true + const loadingMessage = message.loading('PPT生成中,请耐心等待 ...') if (overwrite.value) resetSlides() @@ -249,30 +251,30 @@ const createPPT = async (template?: { slides: Slide[], theme: SlideTheme }) => { const reader: ReadableStreamDefaultReader = stream.body.getReader() const decoder = new TextDecoder('utf-8') - const readStream = () => { - reader.read().then(({ done, value }) => { - if (done) { - loading.value = false - mainStore.setAIPPTDialogState(false) - slidesStore.setTheme(templateTheme) - return - } - - const chunk = decoder.decode(value, { stream: true }) - try { - const text = chunk.replace('```json', '').replace('```', '').trim() - if (text) { - const slide: AIPPTSlide = JSON.parse(chunk) - AIPPT(templateSlides, [slide]) - } - } - catch (err) { - // eslint-disable-next-line - console.error(err) - } + const readStream = async () => { + const { done, value } = await reader.read() + if (done) { + loading.value = false + loadingMessage.close() + message.success('PPT生成完成!') + slidesStore.setTheme(templateTheme) + return + } - readStream() - }) + const chunk = decoder.decode(value, { stream: true }) + try { + const text = chunk.replace('```json', '').replace('```', '').trim() + if (text) { + const slide: AIPPTSlide = JSON.parse(chunk) + await AIPPT(templateSlides, [slide]) + } + } + catch (err) { + // eslint-disable-next-line + console.error(err) + loadingMessage.close() + } + readStream() } readStream() }