Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/components/Message.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<IconCheckOne theme="filled" size="18" fill="#52c41a" v-if="type === 'success'" />
<IconCloseOne theme="filled" size="18" fill="#ff4d4f" v-if="type === 'error'" />
<IconInfo theme="filled" size="18" fill="#1677ff" v-if="type === 'info'" />
<IconRefresh theme="filled" size="18" fill="#1677ff" v-if="type === 'loading'" class="loading-icon" />
</div>
<div class="content">
<div class="title" v-if="title">{{ title }}</div>
Expand Down Expand Up @@ -44,6 +45,7 @@ const {
IconCloseOne,
IconInfo,
IconCloseSmall,
IconRefresh,
} = icons

const props = withDefaults(defineProps<{
Expand Down Expand Up @@ -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;
}
</style>
57 changes: 53 additions & 4 deletions src/hooks/useAIPPT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -15,6 +15,7 @@ interface ImgPoolItem {

export default () => {
const slidesStore = useSlidesStore()
const mainStore = useMainStore()
const { addSlidesFromData } = useAddSlidesOrElements()
const { isEmptySlide } = useSlideHandler()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -533,8 +583,7 @@ export default () => {
})
}
}
if (isEmptySlide.value) slidesStore.setSlides(slides)
else addSlidesFromData(slides)
await progressiveAddSlides(slides)
}

return {
Expand Down
4 changes: 3 additions & 1 deletion src/utils/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,6 +24,7 @@ export interface Message {
success: MessageFn
error: MessageFn
warning: MessageFn
loading: MessageFn
closeAll: () => void
_context?: AppContext | null
}
Expand Down Expand Up @@ -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--) {
Expand Down
48 changes: 25 additions & 23 deletions src/views/Editor/AIPPTDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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()
}
Expand Down