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

View File

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