feat: 完成AIPPT文本替换逻辑

This commit is contained in:
pipipi-pikachu 2025-01-04 14:09:17 +08:00
parent 349f101c9e
commit b107a3105e
2 changed files with 128 additions and 57 deletions

View File

@ -1,17 +1,21 @@
import api from '@/services'
import { nanoid } from 'nanoid'
import type { PPTTextElement, Slide, TextType } from '@/types/slides'
import type { PPTElement, PPTShapeElement, PPTTextElement, Slide, TextType } from '@/types/slides'
import type { AIPPTSlide } from '@/types/AIPPT'
import { useSlidesStore } from '@/store'
import useAddSlidesOrElements from './useAddSlidesOrElements'
import useSlideHandler from './useSlideHandler'
const findClosestGreaterThanN = (templates: Slide[], n: number, type: TextType) => {
const checkTextType = (el: PPTElement, type: TextType) => {
return (el.type === 'text' && el.textType === type) || (el.type === 'shape' && el.text && el.text.type === type)
}
const getUseableTemplates = (templates: Slide[], n: number, type: TextType) => {
if (n === 1) {
const list = templates.filter(slide => {
const items = slide.elements.filter(el => el.type === 'text' && el.textType === type)
const titles = slide.elements.filter(el => el.type === 'text' && el.textType === 'title')
const texts = slide.elements.filter(el => el.type === 'text' && el.textType === 'content')
const items = slide.elements.filter(el => checkTextType(el, type))
const titles = slide.elements.filter(el => checkTextType(el, 'title'))
const texts = slide.elements.filter(el => checkTextType(el, 'content'))
return !items.length && titles.length === 1 && texts.length === 1
})
@ -22,33 +26,33 @@ const findClosestGreaterThanN = (templates: Slide[], n: number, type: TextType)
let target: Slide | null = null
const list = templates.filter(slide => {
const len = slide.elements.filter(el => el.type === 'text' && el.textType === type).length
const len = slide.elements.filter(el => checkTextType(el, type)).length
return len >= n
})
if (list.length === 0) {
const sorted = templates.sort((a, b) => {
const aLen = a.elements.filter(el => el.type === 'text' && el.textType === type).length
const bLen = b.elements.filter(el => el.type === 'text' && el.textType === type).length
const aLen = a.elements.filter(el => checkTextType(el, type)).length
const bLen = b.elements.filter(el => checkTextType(el, type)).length
return aLen - bLen
})
target = sorted[sorted.length - 1]
}
else {
target = list.reduce((closest, current) => {
const currentLen = current.elements.filter(el => el.type === 'text' && el.textType === type).length
const closestLen = closest.elements.filter(el => el.type === 'text' && el.textType === type).length
const currentLen = current.elements.filter(el => checkTextType(el, type)).length
const closestLen = closest.elements.filter(el => checkTextType(el, type)).length
return (currentLen - n) <= (closestLen - n) ? current : closest
})
}
return templates.filter(slide => {
const len = slide.elements.filter(el => el.type === 'text' && el.textType === type).length
const targetLen = target!.elements.filter(el => el.type === 'text' && el.textType === type).length
const len = slide.elements.filter(el => checkTextType(el, type)).length
const targetLen = target!.elements.filter(el => checkTextType(el, type)).length
return len === targetLen
})
}
const getFontsizeInBox = ({
const getAdaptedFontsize = ({
text,
fontSize,
fontFamily,
@ -99,22 +103,26 @@ const getFontInfo = (htmlString: string) => {
}
}
const getNewTextElData = ({
const getNewTextElement = ({
el,
text,
maxLine,
longestText,
digitPadding,
}: {
el: PPTTextElement
el: PPTTextElement | PPTShapeElement
text: string
maxLine: number
longestText?: string
}) => {
digitPadding?: boolean
}): PPTTextElement | PPTShapeElement => {
const padding = 10
const width = el.width - padding * 2 - 10
const fontInfo = getFontInfo(el.content)
const size = getFontsizeInBox({
let content = el.type === 'text' ? el.content : el.text!.content
const fontInfo = getFontInfo(content)
const size = getAdaptedFontsize({
text: longestText || text,
fontSize: fontInfo.fontSize,
fontFamily: fontInfo.fontFamily,
@ -122,15 +130,18 @@ const getNewTextElData = ({
maxLine,
})
let content = el.content
const parser = new DOMParser()
const doc = parser.parseFromString(content, 'text/html')
const treeWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT)
const firstTextNode = treeWalker.nextNode()
if (firstTextNode) firstTextNode.textContent = text
if (firstTextNode) {
if (digitPadding && firstTextNode.textContent && firstTextNode.textContent.length === 2 && text.length === 1) {
firstTextNode.textContent = '0' + text
}
else firstTextNode.textContent = text
}
if (doc.body.innerHTML.indexOf('font-size') === -1) {
const p = doc.querySelector('p')
@ -139,7 +150,7 @@ const getNewTextElData = ({
content = doc.body.innerHTML.replace(/font-size:(.+?)px/g, `font-size: ${size}px`)
return { ...el, content }
return el.type === 'text' ? { ...el, content } : { ...el, text: { ...el.text!, content } }
}
export default () => {
@ -151,7 +162,7 @@ export default () => {
const templateSlides: Slide[] = await api.getMockData('template').then(ret => ret.slides)
const _AISlides: AIPPTSlide[] = await api.getMockData('AIPPT')
const AISlides = []
const AISlides: AIPPTSlide[] = []
for (const template of _AISlides) {
if (template.type === 'content') {
const items = template.data.items
@ -159,29 +170,63 @@ export default () => {
const items1 = items.slice(0, 3)
const items2 = items.slice(3)
AISlides.push({ ...template, data: { ...template.data, items: items1 } })
AISlides.push({ ...template, data: { ...template.data, items: items2 } })
AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 3 })
}
else if (items.length === 7 || items.length === 8) {
const items1 = items.slice(0, 4)
const items2 = items.slice(4)
AISlides.push({ ...template, data: { ...template.data, items: items1 } })
AISlides.push({ ...template, data: { ...template.data, items: items2 } })
AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 4 })
}
else if (items.length === 9 || items.length === 10) {
const items1 = items.slice(0, 3)
const items2 = items.slice(3, 6)
const items3 = items.slice(6)
AISlides.push({ ...template, data: { ...template.data, items: items1 } })
AISlides.push({ ...template, data: { ...template.data, items: items2 } })
AISlides.push({ ...template, data: { ...template.data, items: items3 } })
AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 3 })
AISlides.push({ ...template, data: { ...template.data, items: items3 }, offset: 6 })
}
else if (items.length > 10) {
const items1 = items.slice(0, 4)
const items2 = items.slice(4, 8)
const items3 = items.slice(8)
AISlides.push({ ...template, data: { ...template.data, items: items1 } })
AISlides.push({ ...template, data: { ...template.data, items: items2 } })
AISlides.push({ ...template, data: { ...template.data, items: items3 } })
AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 4 })
AISlides.push({ ...template, data: { ...template.data, items: items3 }, offset: 8 })
}
else {
AISlides.push(template)
}
}
else if (template.type === 'contents') {
const items = template.data.items
if (items.length === 7) {
const items1 = items.slice(0, 5)
const items2 = items.slice(5)
AISlides.push({ ...template, data: { ...template.data, items: items1 } })
AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 5 })
}
else if (items.length > 7 && items.length <= 12) {
const items1 = items.slice(0, 6)
const items2 = items.slice(6)
AISlides.push({ ...template, data: { ...template.data, items: items1 } })
AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 6 })
}
else if (items.length === 13) {
const items1 = items.slice(0, 6)
const items2 = items.slice(6, 11)
const items3 = items.slice(11)
AISlides.push({ ...template, data: { ...template.data, items: items1 } })
AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 6 })
AISlides.push({ ...template, data: { ...template.data, items: items3 }, offset: 11 })
}
else if (items.length > 13) {
const items1 = items.slice(0, 6)
const items2 = items.slice(6, 12)
const items3 = items.slice(12)
AISlides.push({ ...template, data: { ...template.data, items: items1 } })
AISlides.push({ ...template, data: { ...template.data, items: items2 }, offset: 6 })
AISlides.push({ ...template, data: { ...template.data, items: items3 }, offset: 12 })
}
else {
AISlides.push(template)
@ -207,11 +252,12 @@ export default () => {
for (const item of AISlides) {
if (item.type === 'cover') {
const elements = coverTemplate.elements.map(el => {
if (el.type === 'text' && el.textType === 'title' && item.data.title) {
return getNewTextElData({ el, text: item.data.title, maxLine: 1 })
if (el.type !== 'text' && el.type !== 'shape') return el
if (checkTextType(el, 'title') && item.data.title) {
return getNewTextElement({ el, text: item.data.title, maxLine: 1 })
}
if (el.type === 'text' && el.textType === 'content' && item.data.text) {
return getNewTextElData({ el, text: item.data.text, maxLine: 3 })
if (checkTextType(el, 'content') && item.data.text) {
return getNewTextElement({ el, text: item.data.text, maxLine: 3 })
}
return el
})
@ -222,10 +268,16 @@ export default () => {
})
}
else if (item.type === 'contents') {
const _contentsTemplates = findClosestGreaterThanN(contentsTemplates, item.data.items.length, 'item')
const _contentsTemplates = getUseableTemplates(contentsTemplates, item.data.items.length, 'item')
const contentsTemplate = _contentsTemplates[Math.floor(Math.random() * _contentsTemplates.length)]
const sortedItemIds = contentsTemplate.elements.filter(el => el.type === 'text' && el.textType === 'item').sort((a, b) => {
const sortedItemIds = contentsTemplate.elements.filter(el => checkTextType(el, 'item')).sort((a, b) => {
const aIndex = a.left + a.top * 2
const bIndex = b.left + b.top * 2
return aIndex - bIndex
}).map(el => el.id)
const sortedNumberItemIds = contentsTemplate.elements.filter(el => checkTextType(el, 'itemNumber')).sort((a, b) => {
const aIndex = a.left + a.top * 2
const bIndex = b.left + b.top * 2
return aIndex - bIndex
@ -234,10 +286,16 @@ export default () => {
const longestText = item.data.items.reduce((longest, current) => current.length > longest.length ? current : longest, '')
const elements = contentsTemplate.elements.map(el => {
if (el.type === 'text' && el.textType === 'item') {
if (el.type !== 'text' && el.type !== 'shape') return el
if (checkTextType(el, 'item')) {
const index = sortedItemIds.findIndex(id => id === el.id)
const itemTitle = item.data.items[index]
if (itemTitle) return getNewTextElData({ el, text: itemTitle, maxLine: 1, longestText })
if (itemTitle) return getNewTextElement({ el, text: itemTitle, maxLine: 1, longestText })
}
if (checkTextType(el, 'itemNumber')) {
const index = sortedNumberItemIds.findIndex(id => id === el.id)
const offset = item.offset || 0
return getNewTextElement({ el, text: index + offset + 1 + '', maxLine: 1, digitPadding: true })
}
return el
})
@ -250,14 +308,15 @@ export default () => {
else if (item.type === 'transition') {
transitionIndex++
const elements = transitionTemplate.elements.map(el => {
if (el.type === 'text' && el.textType === 'title' && item.data.title) {
return getNewTextElData({ el, text: item.data.title, maxLine: 1 })
if (el.type !== 'text' && el.type !== 'shape') return el
if (checkTextType(el, 'title') && item.data.title) {
return getNewTextElement({ el, text: item.data.title, maxLine: 1 })
}
if (el.type === 'text' && el.textType === 'content' && item.data.text) {
return getNewTextElData({ el, text: item.data.text, maxLine: 3 })
if (checkTextType(el, 'content') && item.data.text) {
return getNewTextElement({ el, text: item.data.text, maxLine: 3 })
}
if (el.type === 'text' && el.textType === 'partNumber') {
return getNewTextElData({ el, text: transitionIndex + '', maxLine: 1 })
if (checkTextType(el, 'partNumber')) {
return getNewTextElement({ el, text: transitionIndex + '', maxLine: 1, digitPadding: true })
}
return el
})
@ -268,16 +327,22 @@ export default () => {
})
}
else if (item.type === 'content') {
const _contentTemplates = findClosestGreaterThanN(contentTemplates, item.data.items.length, 'item')
const _contentTemplates = getUseableTemplates(contentTemplates, item.data.items.length, 'item')
const contentTemplate = _contentTemplates[Math.floor(Math.random() * _contentTemplates.length)]
const sortedTitleItemIds = contentTemplate.elements.filter(el => el.type === 'text' && el.textType === 'itemTitle').sort((a, b) => {
const sortedTitleItemIds = contentTemplate.elements.filter(el => checkTextType(el, 'itemTitle')).sort((a, b) => {
const aIndex = a.left + a.top * 2
const bIndex = b.left + b.top * 2
return aIndex - bIndex
}).map(el => el.id)
const sortedTextItemIds = contentTemplate.elements.filter(el => el.type === 'text' && el.textType === 'item').sort((a, b) => {
const sortedTextItemIds = contentTemplate.elements.filter(el => checkTextType(el, 'item')).sort((a, b) => {
const aIndex = a.left + a.top * 2
const bIndex = b.left + b.top * 2
return aIndex - bIndex
}).map(el => el.id)
const sortedNumberItemIds = contentTemplate.elements.filter(el => checkTextType(el, 'itemNumber')).sort((a, b) => {
const aIndex = a.left + a.top * 2
const bIndex = b.left + b.top * 2
return aIndex - bIndex
@ -294,32 +359,36 @@ export default () => {
const longestText = itemTexts.reduce((longest, current) => current.length > longest.length ? current : longest, '')
const elements = contentTemplate.elements.map(el => {
if (el.type !== 'text' && el.type !== 'shape') return el
if (item.data.items.length === 1) {
const contentItem = item.data.items[0]
if (el.type === 'text' && el.textType === 'content' && contentItem.text) {
return getNewTextElData({ el, text: contentItem.text, maxLine: 6 })
if (checkTextType(el, 'content') && contentItem.text) {
return getNewTextElement({ el, text: contentItem.text, maxLine: 6 })
}
}
else {
if (el.type === 'text' && el.textType === 'itemTitle') {
if (checkTextType(el, 'itemTitle')) {
const index = sortedTitleItemIds.findIndex(id => id === el.id)
const contentItem = item.data.items[index]
if (contentItem && contentItem.title) {
return getNewTextElData({ el, text: contentItem.title, longestText: longestTitle, maxLine: 1 })
return getNewTextElement({ el, text: contentItem.title, longestText: longestTitle, maxLine: 1 })
}
return { ...el, isInvalid: true }
}
if (el.type === 'text' && el.textType === 'item') {
if (checkTextType(el, 'item')) {
const index = sortedTextItemIds.findIndex(id => id === el.id)
const contentItem = item.data.items[index]
if (contentItem && contentItem.text) {
return getNewTextElData({ el, text: contentItem.text, longestText, maxLine: 4 })
return getNewTextElement({ el, text: contentItem.text, longestText, maxLine: 4 })
}
return { ...el, isInvalid: true }
}
if (checkTextType(el, 'itemNumber')) {
const index = sortedNumberItemIds.findIndex(id => id === el.id)
const offset = item.offset || 0
return getNewTextElement({ el, text: index + offset + 1 + '', maxLine: 1, digitPadding: true })
}
}
if (el.type === 'text' && el.textType === 'title' && item.data.title) {
return getNewTextElData({ el, text: item.data.title, maxLine: 1 })
if (checkTextType(el, 'title') && item.data.title) {
return getNewTextElement({ el, text: item.data.title, maxLine: 1 })
}
return el
})

View File

@ -11,6 +11,7 @@ export interface AIPPTContents {
data: {
items: string[]
}
offset?: number
}
export interface AIPPTTransition {
@ -29,7 +30,8 @@ export interface AIPPTContent {
title: string
text: string
}[]
}
},
offset?: number
}
export interface AIPPTEnd {