{{ 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()
}