|
<template> |
|
<div class="aippt-dialog"> |
|
<div class="header"> |
|
<span class="title">AIPPT</span> |
|
<span class="subtite" v-if="step === 'template'">从下方挑选合适的模板,开始生成PPT</span> |
|
<span class="subtite" v-else-if="step === 'outline'">确认下方内容大纲(点击编辑内容,右键添加/删除大纲项),开始选择模板</span> |
|
<span class="subtite" v-else>在下方输入您的PPT主题,并适当补充信息,如行业、岗位、学科、用途等</span> |
|
</div> |
|
|
|
<template v-if="step === 'setup'"> |
|
<Input class="input" |
|
ref="inputRef" |
|
v-model:value="keyword" |
|
:maxlength="50" |
|
placeholder="请输入PPT主题,如:大学生职业生涯规划" |
|
@enter="createOutline()" |
|
> |
|
<template #suffix> |
|
<span class="count">{{ keyword.length }} / 50</span> |
|
<span class="language" v-tooltip="'切换语言'" @click="language = language === 'zh' ? 'en' : 'zh'">{{ language === 'zh' ? '中' : '英' }}</span> |
|
<div class="submit" type="primary" @click="createOutline()"><IconSend class="icon" /> AI 生成</div> |
|
</template> |
|
</Input> |
|
<div class="recommends"> |
|
<div class="recommend" v-for="(item, index) in recommends" :key="index" @click="setKeyword(item)">{{ item }}</div> |
|
</div> |
|
<div class="model-selector"> |
|
<div class="label">选择AI模型:</div> |
|
<Select |
|
style="width: 160px;" |
|
v-model:value="model" |
|
:options="[ |
|
{ label: 'GLM-4-Flash', value: 'GLM-4-Flash' }, |
|
{ label: 'GLM-4-FlashX', value: 'GLM-4-FlashX' }, |
|
]" |
|
/> |
|
</div> |
|
</template> |
|
<div class="preview" v-if="step === 'outline'"> |
|
<pre ref="outlineRef" v-if="outlineCreating">{{ outline }}</pre> |
|
<div class="outline-view" v-else> |
|
<OutlineEditor v-model:value="outline" /> |
|
</div> |
|
<div class="btns" v-if="!outlineCreating"> |
|
<Button class="btn" type="primary" @click="step = 'template'">选择模板</Button> |
|
<Button class="btn" @click="outline = ''; step = 'setup'">返回重新生成</Button> |
|
</div> |
|
</div> |
|
<div class="select-template" v-if="step === 'template'"> |
|
<div class="templates"> |
|
<div class="template" |
|
:class="{ 'selected': selectedTemplate === template.id }" |
|
v-for="template in templates" |
|
:key="template.id" |
|
@click="selectedTemplate = template.id" |
|
> |
|
<img :src="template.cover" :alt="template.name"> |
|
</div> |
|
</div> |
|
<div class="btns"> |
|
<Button class="btn" type="primary" @click="createPPT()">生成</Button> |
|
<Button class="btn" @click="step = 'outline'">返回大纲</Button> |
|
</div> |
|
</div> |
|
|
|
<FullscreenSpin :loading="loading" tip="AI生成中,请耐心等待 ..." /> |
|
</div> |
|
</template> |
|
|
|
<script lang="ts" setup> |
|
import { ref, onMounted } from 'vue' |
|
import { storeToRefs } from 'pinia' |
|
import api from '@/services' |
|
import useAIPPT from '@/hooks/useAIPPT' |
|
import type { AIPPTSlide } from '@/types/AIPPT' |
|
import type { Slide, SlideTheme } from '@/types/slides' |
|
import message from '@/utils/message' |
|
import { useMainStore, useSlidesStore } from '@/store' |
|
import Input from '@/components/Input.vue' |
|
import Button from '@/components/Button.vue' |
|
import Select from '@/components/Select.vue' |
|
import FullscreenSpin from '@/components/FullscreenSpin.vue' |
|
import OutlineEditor from '@/components/OutlineEditor.vue' |
|
|
|
const mainStore = useMainStore() |
|
const slideStore = useSlidesStore() |
|
const { templates } = storeToRefs(slideStore) |
|
const { AIPPT, getMdContent } = useAIPPT() |
|
|
|
const language = ref<'zh' | 'en'>('zh') |
|
const keyword = ref('') |
|
const outline = ref('') |
|
const selectedTemplate = ref('template_1') |
|
const loading = ref(false) |
|
const outlineCreating = ref(false) |
|
const outlineRef = ref<HTMLElement>() |
|
const inputRef = ref<InstanceType<typeof Input>>() |
|
const step = ref<'setup' | 'outline' | 'template'>('setup') |
|
const model = ref('GLM-4-Flash') |
|
|
|
const recommends = ref([ |
|
'大学生职业生涯规划', |
|
'公司年会策划方案', |
|
'大数据如何改变世界', |
|
'餐饮市场调查与研究', |
|
'AIGC在教育领域的应用', |
|
'5G技术如何改变我们的生活', |
|
'社交媒体与品牌营销', |
|
'年度工作总结与展望', |
|
'区块链技术及其应用', |
|
]) |
|
|
|
onMounted(() => { |
|
setTimeout(() => { |
|
inputRef.value!.focus() |
|
}, 500) |
|
}) |
|
|
|
const setKeyword = (value: string) => { |
|
keyword.value = value |
|
inputRef.value!.focus() |
|
} |
|
|
|
const createOutline = async () => { |
|
if (!keyword.value) return message.error('请先输入PPT主题') |
|
|
|
loading.value = true |
|
outlineCreating.value = true |
|
|
|
const stream = await api.AIPPT_Outline(keyword.value, language.value, model.value) |
|
|
|
loading.value = false |
|
step.value = 'outline' |
|
|
|
const reader: ReadableStreamDefaultReader = stream.body.getReader() |
|
const decoder = new TextDecoder('utf-8') |
|
|
|
const readStream = () => { |
|
reader.read().then(({ done, value }) => { |
|
if (done) { |
|
outline.value = getMdContent(outline.value) |
|
outline.value = outline.value.replace(/<!--[\s\S]*?-->/g, '').replace(/<think>[\s\S]*?<\/think>/g, '') |
|
outlineCreating.value = false |
|
return |
|
} |
|
|
|
const chunk = decoder.decode(value, { stream: true }) |
|
outline.value += chunk |
|
|
|
if (outlineRef.value) { |
|
outlineRef.value.scrollTop = outlineRef.value.scrollHeight + 20 |
|
} |
|
|
|
readStream() |
|
}) |
|
} |
|
readStream() |
|
} |
|
|
|
const createPPT = async () => { |
|
loading.value = true |
|
|
|
const stream = await api.AIPPT(outline.value, language.value, model.value) |
|
const templateData = await api.getFileData(selectedTemplate.value) |
|
const templateSlides: Slide[] = templateData.slides |
|
const templateTheme: SlideTheme = templateData.theme |
|
|
|
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) |
|
slideStore.setTheme(templateTheme) |
|
return |
|
} |
|
|
|
const chunk = decoder.decode(value, { stream: true }) |
|
try { |
|
const slide: AIPPTSlide = JSON.parse(chunk) |
|
AIPPT(templateSlides, [slide]) |
|
} |
|
catch (err) { |
|
|
|
console.error(err) |
|
} |
|
|
|
readStream() |
|
}) |
|
} |
|
readStream() |
|
} |
|
</script> |
|
|
|
<style lang="scss" scoped> |
|
.aippt-dialog { |
|
margin: -20px; |
|
padding: 30px; |
|
} |
|
.header { |
|
margin-bottom: 12px; |
|
|
|
.title { |
|
font-weight: 700; |
|
font-size: 20px; |
|
margin-right: 8px; |
|
background: linear-gradient(270deg, #d897fd, #33bcfc); |
|
background-clip: text; |
|
color: transparent; |
|
vertical-align: text-bottom; |
|
line-height: 1.1; |
|
} |
|
.subtite { |
|
color: #888; |
|
font-size: 12px; |
|
} |
|
} |
|
.preview { |
|
pre { |
|
max-height: 450px; |
|
padding: 10px; |
|
margin-bottom: 15px; |
|
background-color: #f1f1f1; |
|
overflow: auto; |
|
} |
|
.outline-view { |
|
max-height: 450px; |
|
padding: 10px; |
|
margin-bottom: 15px; |
|
background-color: #f1f1f1; |
|
overflow: auto; |
|
} |
|
.btns { |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
|
|
.btn { |
|
width: 120px; |
|
margin: 0 5px; |
|
} |
|
} |
|
} |
|
.select-template { |
|
.templates { |
|
display: flex; |
|
margin-bottom: 10px; |
|
@include flex-grid-layout(); |
|
|
|
.template { |
|
border: 2px solid $borderColor; |
|
border-radius: $borderRadius; |
|
width: 304px; |
|
height: 172.75px; |
|
margin-bottom: 12px; |
|
|
|
&:not(:nth-child(2n)) { |
|
margin-right: 12px; |
|
} |
|
|
|
&.selected { |
|
border-color: $themeColor; |
|
} |
|
|
|
img { |
|
width: 100%; |
|
} |
|
} |
|
} |
|
.btns { |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
|
|
.btn { |
|
width: 120px; |
|
margin: 0 5px; |
|
} |
|
} |
|
} |
|
.configs { |
|
margin-top: 5px; |
|
display: flex; |
|
justify-content: space-between; |
|
|
|
.items { |
|
display: flex; |
|
} |
|
.item { |
|
margin-right: 5px; |
|
} |
|
} |
|
.recommends { |
|
display: flex; |
|
flex-wrap: wrap; |
|
margin-top: 10px; |
|
|
|
.recommend { |
|
font-size: 12px; |
|
background-color: #f1f1f1; |
|
border-radius: $borderRadius; |
|
padding: 3px 5px; |
|
margin-right: 5px; |
|
margin-top: 5px; |
|
cursor: pointer; |
|
|
|
&:hover { |
|
color: $themeColor; |
|
} |
|
} |
|
} |
|
.model-selector { |
|
margin-top: 10px; |
|
font-size: 13px; |
|
display: flex; |
|
align-items: center; |
|
} |
|
.count { |
|
font-size: 12px; |
|
color: #999; |
|
margin-right: 10px; |
|
} |
|
.language { |
|
font-size: 12px; |
|
margin-right: 10px; |
|
color: $themeColor; |
|
cursor: pointer; |
|
} |
|
.submit { |
|
height: 20px; |
|
font-size: 12px; |
|
background-color: $themeColor; |
|
color: #fff; |
|
display: flex; |
|
align-items: center; |
|
padding: 0 5px; |
|
border-radius: $borderRadius; |
|
cursor: pointer; |
|
|
|
&:hover { |
|
background-color: $themeHoverColor; |
|
} |
|
|
|
.icon { |
|
font-size: 15px; |
|
margin-right: 3px; |
|
} |
|
} |
|
</style> |