| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038 |
- import { ref, nextTick } from 'vue'
- import { storeToRefs } from 'pinia'
- import { parse, type Shape, type Element, type ChartItem, type BaseElement } from 'pptxtojson'
- import { nanoid } from 'nanoid'
- import { useSlidesStore } from '@/store'
- import { lang } from '@/main'
- import { decrypt } from '@/utils/crypto'
- import { type ShapePoolItem, SHAPE_LIST, SHAPE_PATH_FORMULAS } from '@/configs/shapes'
- import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements'
- import useSlideHandler from '@/hooks/useSlideHandler'
- import useHistorySnapshot from './useHistorySnapshot'
- import message from '@/utils/message'
- import { getSvgPathRange } from '@/utils/svgPathParser'
- import type {
- Slide,
- TableCellStyle,
- TableCell,
- ChartType,
- SlideBackground,
- PPTShapeElement,
- PPTLineElement,
- PPTImageElement,
- ShapeTextAlign,
- PPTTextElement,
- PPTVideoElement,
- PPTAudioElement,
- ChartOptions,
- Gradient,
- } from '@/types/slides'
- const convertFontSizePtToPx = (html: string, ratio: number) => {
- // return html;
- return html.replace(/\s*([\d.]+)pt/g, (match, p1) => {
- return `${(parseFloat(p1) * ratio - 1) | 0}px `
- })
- }
- const getStyle = (htmlString: string) => {
- // return html;
- // 1. 创建 DOMParser 实例
- const parser = new DOMParser()
- // 2. 解析 HTML 字符串为文档对象
- const doc = parser.parseFromString(htmlString, 'text/html')
- // 3. 获取 p 元素
- const p = doc.querySelector('p')
- // 4. 读取 style 属性(内联样式字符串)
- const styleAttr = p?.getAttribute('allstyle')
- console.log(styleAttr) // 输出完整的 style 字符串
- return styleAttr || ''
- }
- export default () => {
- const slidesStore = useSlidesStore()
- const { slides, theme, viewportRatio, title, viewportSize } = storeToRefs(useSlidesStore())
- const { addHistorySnapshot } = useHistorySnapshot()
- const { addSlidesFromData } = useAddSlidesOrElements()
- const { isEmptySlide } = useSlideHandler()
- const exporting = ref(false)
- // 导入JSON文件
- const importJSON = (files: FileList, cover = false) => {
- const file = files[0]
- const reader = new FileReader()
- reader.addEventListener('load', () => {
- try {
- const { slides } = JSON.parse(reader.result as string)
- if (cover) {
- slidesStore.updateSlideIndex(0)
- slidesStore.setSlides(slides)
- addHistorySnapshot()
- }
- else if (isEmptySlide.value) {
- slidesStore.setSlides(slides)
- addHistorySnapshot()
- }
- else addSlidesFromData(slides)
- }
- catch {
- message.error(lang.ssFileReadFail)
- }
- })
- reader.readAsText(file)
- }
- // 直接读取JSON功能,暴露到window.readJSON
- const readJSON = (jsonData: string | any, cover = false) => {
- try {
- console.log('readJSON 开始执行:', { jsonData, cover })
- let parsedData
- if (typeof jsonData === 'string') {
- parsedData = JSON.parse(jsonData)
- console.log('解析字符串后的数据:', parsedData)
- }
- else {
- parsedData = jsonData
- }
- // 提取所有可能的数据
- const slides = parsedData.slides || parsedData
- const title = parsedData.title
- const theme = parsedData.theme
- const width = parsedData.width
- const height = parsedData.height
- const viewportRatio = parsedData.viewportRatio || (height && width ? height / width : undefined)
- console.log('提取的数据:', { slides: slides.length, title, theme, width, height, viewportRatio })
- // 更新幻灯片数据
- if (cover) {
- console.log('覆盖模式:更新幻灯片数据')
- slidesStore.updateSlideIndex(0)
- slidesStore.setSlides(slides)
- addHistorySnapshot()
- }
- else if (isEmptySlide.value) {
- console.log('空幻灯片模式:更新幻灯片数据')
- slidesStore.setSlides(slides)
- addHistorySnapshot()
- }
- else {
- console.log('添加模式:添加幻灯片数据')
- addSlidesFromData(slides)
- }
- // 同步更新其他相关内容
- if (title !== undefined) {
- console.log('正在更新标题:', title)
- slidesStore.setTitle(title)
- console.log('标题更新完成')
- }
- if (theme !== undefined) {
- console.log('正在更新主题:', theme)
- slidesStore.setTheme(theme)
- console.log('主题更新完成')
- }
- // 更新视口尺寸(如果提供了的话)
- if (width !== undefined && height !== undefined) {
- console.log('正在触发视口尺寸更新事件:', { width, height, viewportRatio })
- // 同时也要更新slidesStore中的相关数据
- if (slidesStore.setViewportSize) {
- console.log('正在更新store中的视口尺寸')
- slidesStore.setViewportSize(width)
- if (slidesStore.setViewportRatio && viewportRatio !== undefined) {
- slidesStore.setViewportRatio(viewportRatio)
- console.log('视口比例已更新:', viewportRatio)
- }
- }
- window.dispatchEvent(new CustomEvent('viewportSizeUpdated', {
- detail: { width, height, viewportRatio }
- }))
- console.log('视口尺寸更新事件已触发')
- }
- // 导入成功后,触发画布尺寸更新
- // 使用 nextTick 确保DOM更新完成后再触发
- console.log('开始触发画布尺寸更新事件...')
- nextTick(() => {
- console.log('DOM更新完成,触发 slidesDataUpdated 事件')
- // 触发自定义事件,通知需要更新画布尺寸的组件
- window.dispatchEvent(new CustomEvent('slidesDataUpdated', {
- detail: {
- slides,
- cover,
- title,
- theme,
- width,
- height,
- viewportRatio,
- timestamp: Date.now()
- }
- }))
- console.log('slidesDataUpdated 事件已触发')
- // 检查并调整幻灯片索引,确保在有效范围内
- const newSlideCount = slides.length
- const currentIndex = slidesStore.slideIndex
- if (currentIndex >= newSlideCount) {
- console.log('调整幻灯片索引:', currentIndex, '->', Math.max(0, newSlideCount - 1))
- slidesStore.updateSlideIndex(Math.max(0, newSlideCount - 1))
- }
- console.log('画布尺寸更新事件处理完成')
- })
- console.log('readJSON 执行成功')
- return { success: true, slides, title, theme, width, height, viewportRatio }
- }
- catch (error) {
- console.error('readJSON 执行失败:', error)
- const errorMsg = lang.ssJsonReadFail
- message.error(errorMsg)
- return { success: false, error: errorMsg, details: error }
- }
- }
- // 导出JSON文件
- const exportJSON2 = () => {
- const json = {
- title: title.value,
- width: viewportSize.value,
- height: viewportSize.value * viewportRatio.value,
- theme: theme.value,
- slides: slides.value,
- }
- return json
- }
- // 优化暴露到 window 对象的方式,避免重复赋值
- if (typeof window !== 'undefined') {
- const win = window as any
- if (!win.exportJSON) win.exportJSON = exportJSON2
- if (!win.readJSON) win.readJSON = readJSON
- }
- // 导入pptist文件
- const importSpecificFile = (files: FileList, cover = false) => {
- const file = files[0]
- const reader = new FileReader()
- reader.addEventListener('load', () => {
- try {
- const { slides } = JSON.parse(decrypt(reader.result as string))
- if (cover) {
- slidesStore.updateSlideIndex(0)
- slidesStore.setSlides(slides)
- addHistorySnapshot()
- }
- else if (isEmptySlide.value) {
- slidesStore.setSlides(slides)
- addHistorySnapshot()
- }
- else addSlidesFromData(slides)
- }
- catch {
- message.error(lang.ssFileReadFail)
- }
- })
- reader.readAsText(file)
- }
- const rotateLine = (line: PPTLineElement, angleDeg: number) => {
- const { start, end } = line
- const angleRad = angleDeg * Math.PI / 180
- const midX = (start[0] + end[0]) / 2
- const midY = (start[1] + end[1]) / 2
- const startTransX = start[0] - midX
- const startTransY = start[1] - midY
- const endTransX = end[0] - midX
- const endTransY = end[1] - midY
- const cosA = Math.cos(angleRad)
- const sinA = Math.sin(angleRad)
- const startRotX = startTransX * cosA - startTransY * sinA
- const startRotY = startTransX * sinA + startTransY * cosA
- const endRotX = endTransX * cosA - endTransY * sinA
- const endRotY = endTransX * sinA + endTransY * cosA
- const startNewX = startRotX + midX
- const startNewY = startRotY + midY
- const endNewX = endRotX + midX
- const endNewY = endRotY + midY
- const beforeMinX = Math.min(start[0], end[0])
- const beforeMinY = Math.min(start[1], end[1])
- const afterMinX = Math.min(startNewX, endNewX)
- const afterMinY = Math.min(startNewY, endNewY)
- const startAdjustedX = startNewX - afterMinX
- const startAdjustedY = startNewY - afterMinY
- const endAdjustedX = endNewX - afterMinX
- const endAdjustedY = endNewY - afterMinY
- const startAdjusted: [number, number] = [startAdjustedX, startAdjustedY]
- const endAdjusted: [number, number] = [endAdjustedX, endAdjustedY]
- const offset = [afterMinX - beforeMinX, afterMinY - beforeMinY]
- return {
- start: startAdjusted,
- end: endAdjusted,
- offset,
- }
- }
- const parseLineElement = (el: Shape, ratio: number) => {
- let start: [number, number] = [0, 0]
- let end: [number, number] = [0, 0]
- if (!el.isFlipV && !el.isFlipH) { // 右下
- start = [0, 0]
- end = [el.width, el.height]
- }
- else if (el.isFlipV && el.isFlipH) { // 左上
- start = [el.width, el.height]
- end = [0, 0]
- }
- else if (el.isFlipV && !el.isFlipH) { // 右上
- start = [0, el.height]
- end = [el.width, 0]
- }
- else { // 左下
- start = [el.width, 0]
- end = [0, el.height]
- }
- const data: PPTLineElement = {
- type: 'line',
- id: nanoid(10),
- width: +((el.borderWidth || 1) * ratio).toFixed(2),
- left: el.left,
- top: el.top,
- start,
- end,
- style: el.borderType,
- color: el.borderColor,
- points: ['', /straightConnector/.test(el.shapType) ? 'arrow' : '']
- }
- if (el.rotate) {
- const { start, end, offset } = rotateLine(data, el.rotate)
- data.start = start
- data.end = end
- data.left = data.left + offset[0]
- data.top = data.top + offset[1]
- }
- if (/bentConnector/.test(el.shapType)) {
- data.broken2 = [
- Math.abs(data.start[0] - data.end[0]) / 2,
- Math.abs(data.start[1] - data.end[1]) / 2,
- ]
- }
- if (/curvedConnector/.test(el.shapType)) {
- const cubic: [number, number] = [
- Math.abs(data.start[0] - data.end[0]) / 2,
- Math.abs(data.start[1] - data.end[1]) / 2,
- ]
- data.cubic = [cubic, cubic]
- }
- return data
- }
- const flipGroupElements = (elements: BaseElement[], axis: 'x' | 'y') => {
- const minX = Math.min(...elements.map(el => el.left))
- const maxX = Math.max(...elements.map(el => el.left + el.width))
- const minY = Math.min(...elements.map(el => el.top))
- const maxY = Math.max(...elements.map(el => el.top + el.height))
- const centerX = (minX + maxX) / 2
- const centerY = (minY + maxY) / 2
- return elements.map(element => {
- const newElement = { ...element }
- if (axis === 'y') newElement.left = 2 * centerX - element.left - element.width
- if (axis === 'x') newElement.top = 2 * centerY - element.top - element.height
- return newElement
- })
- }
- const calculateRotatedPosition = (
- x: number,
- y: number,
- w: number,
- h: number,
- ox: number,
- oy: number,
- k: number,
- ) => {
- const radians = k * (Math.PI / 180)
- const containerCenterX = x + w / 2
- const containerCenterY = y + h / 2
- const relativeX = ox - w / 2
- const relativeY = oy - h / 2
- const rotatedX = relativeX * Math.cos(radians) + relativeY * Math.sin(radians)
- const rotatedY = -relativeX * Math.sin(radians) + relativeY * Math.cos(radians)
- const graphicX = containerCenterX + rotatedX
- const graphicY = containerCenterY + rotatedY
- return { x: graphicX, y: graphicY }
- }
- /**
- * 将 base64 字符串或 Blob 转换为 File 对象
- */
- const dataToFile = async (data: string | Blob, filename: string, videoMimeType: string): File => {
- if (typeof data === 'string') {
- // 1. 通过 fetch 获取 Blob 数据
- const response = await fetch(data)
- if (!response.ok) {
- throw new Error(`Failed to fetch blob: ${response.statusText}`)
- }
- const blob = await response.blob()
- // 2. 将 Blob 转换为 File 对象
- // 如果原 Blob 有 type,会自动保留;否则可手动指定 videoMimeType
- const file = new File([blob], filename, { type: videoMimeType || blob.type })
- return file
- }
- else if (data instanceof Blob) {
- return new File([data], filename, { type: data.type })
- }
- throw new Error('Unsupported data type')
- }
- /*
- const makeWhiteTransparent = async (
- data: string | Blob,
- filename: string,
- options?: { tolerance?: number }
- ): Promise<File> => {
- const tolerance = options?.tolerance ?? 30 // 容差值,控制哪些颜色被视为白色
- const distThreshold = options?.tolerance ?? 50;
- // 1. 将输入数据统一为 Blob 或可直接用于加载的 URL
- let imageUrl: string
- let blob: Blob
- if (typeof data === 'string') {
- // 如果是 Base64,直接用作 src(data URL)
- imageUrl = data.startsWith('data:') ? data : `data:image/png;base64,${data}`
- }
- else if (data instanceof Blob) {
- // 如果是 Blob,创建对象 URL
- imageUrl = URL.createObjectURL(data)
- blob = data // 暂存,后续释放 URL 用
- }
- else {
- throw new Error('Unsupported data type')
- }
- // 2. 加载图像到 Image 元素
- const img = await new Promise<HTMLImageElement>((resolve, reject) => {
- const image = new Image()
- image.onload = () => resolve(image)
- image.onerror = reject
- image.src = imageUrl
- // 如果图像来自跨域,可能需要设置 crossOrigin
- // image.crossOrigin = 'anonymous';
- })
- // 3. 创建 Canvas 并绘制图像
- const canvas = document.createElement('canvas')
- canvas.width = img.width
- canvas.height = img.height
- const ctx = canvas.getContext('2d')!
- ctx.drawImage(img, 0, 0)
- // 4. 获取像素数据并处理
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
- const dataArray = imageData.data
- for (let i = 0; i < dataArray.length; i += 4) {
- const r = dataArray[i]
- const g = dataArray[i + 1]
- const b = dataArray[i + 2]
- // 判断颜色是否接近白色(RGB 都大于 255 - tolerance)
- if (r > 255 - tolerance && g > 255 - tolerance && b > 255 - tolerance) {
- dataArray[i + 3] = 0 // 设置 Alpha 为 0(完全透明)
- }
- }
- // 5. 将修改后的像素放回 Canvas
- ctx.putImageData(imageData, 0, 0)
- // 6. 将 Canvas 转换为 PNG Blob
- const outputBlob = await new Promise<Blob>((resolve) =>
- canvas.toBlob((blob) => resolve(blob!), 'image/png')
- )
- // 7. 清理对象 URL(如果之前创建过)
- if (typeof data !== 'string') {
- URL.revokeObjectURL(imageUrl)
- }
- // 8. 返回 File 对象
- return new File([outputBlob], filename, { type: 'image/png' })
- }
- */
- /**
- * 将图片中的白色背景变为透明
- * @param data 图片数据(Base64字符串 或 Blob)
- * @param filename 输出文件名
- * @param options 可选配置
- * @param options.tolerance 颜色距离容差(默认50,值越大越多的浅色被变透明)
- * @param options.removeMatte 是否去除白色边缘(默认true,可改善白边)
- * @returns 处理后的 PNG 格式 File 对象
- */
- const makeWhiteTransparent = async (
- data: string | Blob,
- filename: string,
- options?: { tolerance?: number }
- ): Promise<File> => {
- const tolerance = options?.tolerance ?? 15
-
- // ----- 辅助函数:将输入统一转换为 { blob, mime } -----
- async function getBlobAndMime(input: string | Blob): Promise<{ blob: Blob; mime: string }> {
- // 1. 已经是 Blob
- if (input instanceof Blob) {
- return { blob: input, mime: input.type }
- }
-
- // 2. 处理字符串
- if (input.startsWith('data:')) {
- // data URL → 通过 fetch 获取 Blob(自动获得正确的 MIME 类型)
- const response = await fetch(input)
- const blob = await response.blob()
- return { blob, mime: blob.type }
- }
- // 纯 base64 字符串 → 按原逻辑默认当作 PNG
- const binary = atob(input)
- const bytes = new Uint8Array(binary.length)
- for (let i = 0; i < binary.length; i++) {
- bytes[i] = binary.charCodeAt(i)
- }
- // 默认 MIME 为 image/png(与原函数行为一致)
- const blob = new Blob([bytes], { type: 'image/png' })
- return { blob, mime: 'image/png' }
-
- }
-
- // 获取统一的 blob 和实际 MIME 类型
- const { blob, mime } = await getBlobAndMime(data)
-
- // ----- 非 PNG 格式:直接返回原始文件(不处理透明)-----
- if (mime !== 'image/png') {
- return new File([blob], filename, { type: mime })
- }
-
- // ----- PNG 格式:执行白色变透明处理 -----
- // 1. 创建对象 URL 用于加载图片
- const imageUrl = URL.createObjectURL(blob)
- const needRevoke = true
-
- // 2. 加载图像
- const img = await new Promise<HTMLImageElement>((resolve, reject) => {
- const image = new Image()
- image.onload = () => resolve(image)
- image.onerror = reject
- // Blob URL 不需要设置 crossOrigin
- image.src = imageUrl
- })
-
- const canvas = document.createElement('canvas')
- try {
- canvas.width = img.width
- canvas.height = img.height
- const ctx = canvas.getContext('2d')!
- ctx.drawImage(img, 0, 0)
-
- // 3. 获取像素数据,将接近白色的像素设为透明
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
- const dataArray = imageData.data
-
- for (let i = 0; i < dataArray.length; i += 4) {
- const r = dataArray[i]
- const g = dataArray[i + 1]
- const b = dataArray[i + 2]
-
- const dr = r - 255
- const dg = g - 255
- const db = b - 255
- const dist = Math.sqrt(dr * dr + dg * dg + db * db)
-
- if (dist <= tolerance) {
- dataArray[i + 3] = 0 // 完全透明
- }
- }
-
- ctx.putImageData(imageData, 0, 0)
- }
- finally {
- if (needRevoke) {
- URL.revokeObjectURL(imageUrl)
- }
- }
-
- // 4. 导出为 PNG Blob
- const outputBlob = await new Promise<Blob>((resolve, reject) => {
- canvas.toBlob((blob) => {
- if (blob) resolve(blob)
- else reject(new Error('Canvas toBlob failed'))
- }, 'image/png')
- })
-
- return new File([outputBlob], filename, { type: 'image/png' })
- }
- /**
- * 上传 File 到 S3,返回公开访问的 URL
- */
- const uploadFileToS3 = (file: File): Promise<string> => {
- return new Promise((resolve, reject) => {
- if (typeof window === 'undefined' || !window.AWS) {
- reject(new Error('AWS SDK not available'))
- return
- }
- const credentials = {
- accessKeyId: 'AKIATLPEDU37QV5CHLMH',
- secretAccessKey: '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',
- }
- window.AWS.config.update(credentials)
- window.AWS.config.region = 'cn-northwest-1'
- const bucket = new window.AWS.S3({
- params: { Bucket: 'ccrb' }, httpOptions: {
- timeout: 600000 // 10分钟超时
- }
- })
- const ext = file.name.split('.').pop() || 'bin'
- const key = `${file.name.split('.')[0]}_${Date.now()}.${ext}`
- const params = {
- Key: 'pptto/' + key,
- ContentType: file.type,
- Body: file,
- ACL: 'public-read',
- }
- const options = {
- partSize: 5 * 1024 * 1024, // 2GB 分片,可酌情调小
- queueSize: 2,
- leavePartsOnError: true,
- }
- bucket
- .upload(params, options)
- .promise()
- .then(data => resolve(data.Location))
- .catch(err => reject(err))
- })
- }
- /*
- // 导入PPTX文件
- const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal }) => {
- console.log('导入', files)
- const defaultOptions = {
- cover: false,
- fixedViewport: false,
- }
- const { cover, fixedViewport, signal } = { ...defaultOptions, ...options }
-
- const file = files[0]
- if (!file) return
-
- exporting.value = true
-
- const shapeList: ShapePoolItem[] = []
- for (const item of SHAPE_LIST) {
- shapeList.push(...item.children)
- }
-
- const reader = new FileReader()
- reader.onload = async (e: ProgressEvent<FileReader>) => {
- // 检查是否已取消
- if (signal?.aborted) {
- exporting.value = false
- return
- }
-
- let json = null
- try {
- json = await parse(e.target!.result as ArrayBuffer)
- }
- catch (error) {
- exporting.value = false
- console.log('导入PPTX文件失败:', error)
- message.error('无法正确读取 / 解析该文件')
- return
- }
-
- let ratio = 96 / 72;
- //let ratio = 1
- const width = json.size.width
-
- if (fixedViewport) ratio = 1000 / width
- else slidesStore.setViewportSize(width * ratio)
-
- slidesStore.setTheme({ themeColors: json.themeColors })
-
- const slides: Slide[] = []
- for (const item of json.slides) {
- const { type, value } = item.fill
- let background: SlideBackground
- if (type === 'image') {
- background = {
- type: 'image',
- image: {
- src: value.picBase64,
- size: 'cover',
- },
- }
- }
- else if (type === 'gradient') {
- background = {
- type: 'gradient',
- gradient: {
- type: value.path === 'line' ? 'linear' : 'radial',
- colors: value.colors.map(item => ({
- ...item,
- pos: parseInt(item.pos),
- })),
- rotate: value.rot + 90,
- },
- }
- }
- else {
- background = {
- type: 'solid',
- color: value || '#fff',
- }
- }
-
- const slide: Slide = {
- id: nanoid(10),
- elements: [],
- background,
- remark: item.note || '',
- }
-
- const parseElements = (elements: Element[]) => {
-
- const sortedElements = elements.sort((a, b) => a.order - b.order)
- console.log(sortedElements)
-
- for (const el of sortedElements) {
- const originWidth = el.width || 1
- const originHeight = el.height || 1
- const originLeft = el.left
- const originTop = el.top
-
- el.width = el.width * ratio
- el.height = el.height * ratio
- el.left = el.left * ratio
- el.top = el.top * ratio
-
- if (el.type === 'text') {
- const textEl: PPTTextElement = {
- type: 'text',
- id: nanoid(10),
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- rotate: el.rotate,
- defaultFontName: theme.value.fontName,
- defaultColor: theme.value.fontColor,
- content: convertFontSizePtToPx(el.content, ratio),
- lineHeight: 1,
- outline: {
- color: el.borderColor,
- width: +(el.borderWidth * ratio).toFixed(2),
- style: el.borderType,
- },
- fill: el.fill.type === 'color' ? el.fill.value : '',
- vertical: el.isVertical,
- }
- if (el.shadow) {
- textEl.shadow = {
- h: el.shadow.h * ratio,
- v: el.shadow.v * ratio,
- blur: el.shadow.blur * ratio,
- color: el.shadow.color,
- }
- }
- slide.elements.push(textEl)
- }
- else if (el.type === 'image') {
- const element: PPTImageElement = {
- type: 'image',
- id: nanoid(10),
- src: el.src,
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- fixedRatio: true,
- rotate: el.rotate,
- flipH: el.isFlipH,
- flipV: el.isFlipV,
- }
- if (el.borderWidth) {
- element.outline = {
- color: el.borderColor,
- width: +(el.borderWidth * ratio).toFixed(2),
- style: el.borderType,
- }
- }
- const clipShapeTypes = ['roundRect', 'ellipse', 'triangle', 'rhombus', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'parallelogram', 'trapezoid']
- if (el.rect) {
- element.clip = {
- shape: (el.geom && clipShapeTypes.includes(el.geom)) ? el.geom : 'rect',
- range: [
- [
- el.rect.l || 0,
- el.rect.t || 0,
- ],
- [
- 100 - (el.rect.r || 0),
- 100 - (el.rect.b || 0),
- ],
- ]
- }
- }
- else if (el.geom && clipShapeTypes.includes(el.geom)) {
- element.clip = {
- shape: el.geom,
- range: [[0, 0], [100, 100]]
- }
- }
- slide.elements.push(element)
- }
- else if (el.type === 'math') {
- slide.elements.push({
- type: 'image',
- id: nanoid(10),
- src: el.picBase64,
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- fixedRatio: true,
- rotate: 0,
- })
- }
- else if (el.type === 'audio') {
- slide.elements.push({
- type: 'audio',
- id: nanoid(10),
- src: el.blob,
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- rotate: 0,
- fixedRatio: false,
- color: theme.value.themeColors[0],
- loop: false,
- autoplay: false,
- })
- }
- else if (el.type === 'video') {
- slide.elements.push({
- type: 'video',
- id: nanoid(10),
- src: (el.blob || el.src)!,
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- rotate: 0,
- autoplay: false,
- })
- }
- else if (el.type === 'shape') {
- if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
- const lineElement = parseLineElement(el, ratio)
- slide.elements.push(lineElement)
- }
- else {
- const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
-
- const vAlignMap: { [key: string]: ShapeTextAlign } = {
- 'mid': 'middle',
- 'down': 'bottom',
- 'up': 'top',
- }
-
- const gradient: Gradient | undefined = el.fill?.type === 'gradient' ? {
- type: el.fill.value.path === 'line' ? 'linear' : 'radial',
- colors: el.fill.value.colors.map(item => ({
- ...item,
- pos: parseInt(item.pos),
- })),
- rotate: el.fill.value.rot,
- } : undefined
-
- const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
-
- const fill = el.fill?.type === 'color' ? el.fill.value : ''
-
- const element: PPTShapeElement = {
- type: 'shape',
- id: nanoid(10),
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- viewBox: [200, 200],
- path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
- fill,
- gradient,
- pattern,
- fixedRatio: false,
- rotate: el.rotate,
- outline: {
- color: el.borderColor,
- width: +(el.borderWidth * ratio).toFixed(2),
- style: el.borderType,
- },
- text: {
- content: convertFontSizePtToPx(el.content, ratio),
- defaultFontName: theme.value.fontName,
- defaultColor: theme.value.fontColor,
- align: vAlignMap[el.vAlign] || 'middle',
- },
- flipH: el.isFlipH,
- flipV: el.isFlipV,
- }
- if (el.shadow) {
- element.shadow = {
- h: el.shadow.h * ratio,
- v: el.shadow.v * ratio,
- blur: el.shadow.blur * ratio,
- color: el.shadow.color,
- }
- }
-
- if (shape) {
- element.path = shape.path
- element.viewBox = shape.viewBox
-
- if (shape.pathFormula) {
- element.pathFormula = shape.pathFormula
- element.viewBox = [el.width, el.height]
-
- const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
- if ('editable' in pathFormula && pathFormula.editable) {
- element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
- element.keypoints = pathFormula.defaultValue
- }
- else element.path = pathFormula.formula(el.width, el.height)
- }
- }
- else if (el.path && el.path.indexOf('NaN') === -1) {
- const { maxX, maxY } = getSvgPathRange(el.path)
- element.path = el.path
- element.viewBox = [maxX || originWidth, maxY || originHeight]
- }
- if (el.shapType === 'custom') {
- if (el.path!.indexOf('NaN') !== -1) {
- if (element.width === 0) element.width = 0.1
- if (element.height === 0) element.height = 0.1
- element.path = el.path!.replace(/NaN/g, '0')
- }
- else {
- element.special = true
- element.path = el.path!
- }
- const { maxX, maxY } = getSvgPathRange(element.path)
- element.viewBox = [maxX || originWidth, maxY || originHeight]
- }
-
- if (element.path) slide.elements.push(element)
- }
- }
- else if (el.type === 'table') {
- const row = el.data.length
- const col = el.data[0].length
-
- const style: TableCellStyle = {
- fontname: theme.value.fontName,
- color: theme.value.fontColor,
- }
- const data: TableCell[][] = []
- for (let i = 0; i < row; i++) {
- const rowCells: TableCell[] = []
- for (let j = 0; j < col; j++) {
- const cellData = el.data[i][j]
-
- let textDiv: HTMLDivElement | null = document.createElement('div')
- textDiv.innerHTML = cellData.text
- const p = textDiv.querySelector('p')
- const align = p?.style.textAlign || 'left'
-
- const span = textDiv.querySelector('span')
- const fontsize = span?.style.fontSize ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px' : ''
- const fontname = span?.style.fontFamily || ''
- const color = span?.style.color || cellData.fontColor
-
- rowCells.push({
- id: nanoid(10),
- colspan: cellData.colSpan || 1,
- rowspan: cellData.rowSpan || 1,
- text: textDiv.innerText,
- style: {
- ...style,
- align: ['left', 'right', 'center'].includes(align) ? (align as 'left' | 'right' | 'center') : 'left',
- fontsize,
- fontname,
- color,
- bold: cellData.fontBold,
- backcolor: cellData.fillColor,
- },
- })
- textDiv = null
- }
- data.push(rowCells)
- }
-
- const allWidth = el.colWidths.reduce((a, b) => a + b, 0)
- const colWidths: number[] = el.colWidths.map(item => item / allWidth)
-
- const firstCell = el.data[0][0]
- const border = firstCell.borders.top ||
- firstCell.borders.bottom ||
- el.borders.top ||
- el.borders.bottom ||
- firstCell.borders.left ||
- firstCell.borders.right ||
- el.borders.left ||
- el.borders.right
- const borderWidth = border?.borderWidth || 0
- const borderStyle = border?.borderType || 'solid'
- const borderColor = border?.borderColor || '#eeece1'
-
- slide.elements.push({
- type: 'table',
- id: nanoid(10),
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- colWidths,
- rotate: 0,
- data,
- outline: {
- width: +(borderWidth * ratio || 2).toFixed(2),
- style: borderStyle,
- color: borderColor,
- },
- cellMinHeight: el.rowHeights[0] ? el.rowHeights[0] * ratio : 36,
- })
- }
- else if (el.type === 'chart') {
- let labels: string[]
- let legends: string[]
- let series: number[][]
-
- if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
- labels = el.data[0].map((item, index) => `坐标${index + 1}`)
- legends = ['X', 'Y']
- series = el.data
- }
- else {
- const data = el.data as ChartItem[]
- labels = Object.values(data[0].xlabels)
- legends = data.map(item => item.key)
- series = data.map(item => item.values.map(v => v.y))
- }
-
- const options: ChartOptions = {}
-
- let chartType: ChartType = 'bar'
-
- switch (el.chartType) {
- case 'barChart':
- case 'bar3DChart':
- chartType = 'bar'
- if (el.barDir === 'bar') chartType = 'column'
- if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
- break
- case 'lineChart':
- case 'line3DChart':
- if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
- chartType = 'line'
- break
- case 'areaChart':
- case 'area3DChart':
- if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
- chartType = 'area'
- break
- case 'scatterChart':
- case 'bubbleChart':
- chartType = 'scatter'
- break
- case 'pieChart':
- case 'pie3DChart':
- chartType = 'pie'
- break
- case 'radarChart':
- chartType = 'radar'
- break
- case 'doughnutChart':
- chartType = 'ring'
- break
- default:
- }
-
- slide.elements.push({
- type: 'chart',
- id: nanoid(10),
- chartType: chartType,
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- rotate: 0,
- themeColors: el.colors.length ? el.colors : theme.value.themeColors,
- textColor: theme.value.fontColor,
- data: {
- labels,
- legends,
- series,
- },
- options,
- })
- }
- else if (el.type === 'group') {
- let elements: BaseElement[] = el.elements.map(_el => {
- let left = _el.left + originLeft
- let top = _el.top + originTop
-
- if (el.rotate) {
- const { x, y } = calculateRotatedPosition(originLeft, originTop, originWidth, originHeight, _el.left, _el.top, el.rotate)
- left = x
- top = y
- }
-
- const element = {
- ..._el,
- left,
- top,
- }
- if (el.isFlipH && 'isFlipH' in element) element.isFlipH = true
- if (el.isFlipV && 'isFlipV' in element) element.isFlipV = true
-
- return element
- })
- if (el.isFlipH) elements = flipGroupElements(elements, 'y')
- if (el.isFlipV) elements = flipGroupElements(elements, 'x')
- parseElements(elements)
- }
- else if (el.type === 'diagram') {
- const elements = el.elements.map(_el => ({
- ..._el,
- left: _el.left + originLeft,
- top: _el.top + originTop,
- }))
- parseElements(elements)
- }
- }
- }
- parseElements([...item.elements, ...item.layoutElements])
- slides.push(slide)
- }
-
- if (cover) {
- slidesStore.updateSlideIndex(0)
- slidesStore.setSlides(slides)
- addHistorySnapshot()
- }
- else if (isEmptySlide.value) {
- slidesStore.setSlides(slides)
- addHistorySnapshot()
- }
- else addSlidesFromData(slides)
-
- exporting.value = false
- }
- reader.readAsArrayBuffer(file)
-
- // 监听取消信号
- signal?.addEventListener('abort', () => {
- reader.abort()
- exporting.value = false
- })
-
- // 监听取消信号
- signal?.addEventListener('abort', () => {
- reader.abort()
- exporting.value = false
- })
- }
- */
- const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal, onclose?: () => void }) => {
- console.log('导入', files)
- const defaultOptions = {
- cover: false,
- fixedViewport: false,
- }
- const { cover, fixedViewport, signal, onclose } = { ...defaultOptions, ...options }
- const file = files[0]
- if (!file) return
- exporting.value = true // 假设 exporting 是一个全局 ref
- // 预加载形状库(用于后续形状匹配)
- const shapeList: ShapePoolItem[] = []
- for (const item of SHAPE_LIST) {
- shapeList.push(...item.children)
- }
- const reader = new FileReader()
- reader.onload = async (e: ProgressEvent<FileReader>) => {
- // 检查是否已取消
- if (signal?.aborted) {
- exporting.value = false
- return
- }
- let json = null
- try {
- json = await parse(e.target!.result as ArrayBuffer)
- }
- catch (error) {
- exporting.value = false
- console.log('导入PPTX文件失败:', error)
- message.error(lang.ssFileReadFail)
- return
- }
- if (signal?.aborted) {
- exporting.value = false
- return
- }
- // 计算缩放比例
- let ratio = 96 / 72 // PPTX 默认 72 DPI,屏幕 96 DPI
- const width = json.size.width
- const height = json.size.height
- const viewportRatio = json.size.viewportRatio || (height && width ? height / width : undefined)
- if (fixedViewport) {
- ratio = 1000 / width // 固定视口宽度为 1000px
- }
- else {
- slidesStore.setViewportSize(width * ratio) // 调整画布大小
- }
- // 设置主题色
- slidesStore.setTheme({ themeColors: json.themeColors })
- const slides: Slide[] = []
- // 收集当前幻灯片内所有上传任务
- const uploadTasks: Promise<void>[] = []
- // 遍历每一张幻灯片
- for (const item of json.slides) {
- // ----- 解析背景 -----
- const { type, value } = item.fill
- let background: SlideBackground
- if (type === 'image') {
- // 背景图片也可能需要上传(但 PPTX 背景图通常是内嵌 base64)
- // 这里为了简化,暂不处理背景图片上传,如有需要可类似元素上传
- background = {
- type: 'image',
- image: {
- src: value.picBase64,
- size: 'cover',
- },
- }
- }
- else if (type === 'gradient') {
- background = {
- type: 'gradient',
- gradient: {
- type: value.path === 'line' ? 'linear' : 'radial',
- colors: value.colors.map(item => ({
- ...item,
- pos: parseInt(item.pos),
- })),
- rotate: value.rot + 90,
- },
- }
- }
- else {
- background = {
- type: 'solid',
- color: value || '#fff',
- }
- }
- const slide: Slide = {
- id: nanoid(10),
- elements: [],
- background,
- remark: item.note || '',
- }
- // ----- 解析元素(递归函数)-----
- const parseElements = async (elements: any[], pelements: any = null) => {
- // 按绘制顺序排序
- const sortedElements = elements.sort((a, b) => a.order - b.order)
- console.log(sortedElements)
- for (const el of sortedElements) {
- // 保存原始尺寸用于后续可能的路径计算
- const originWidth = el.width || 1
- const originHeight = el.height || 1
- const originLeft = el.left
- const originTop = el.top
- // 保存原始尺寸用于后续可能的路径计算
- const poriginWidth = pelements?.width
- const poriginHeight = pelements?.height
- const poriginLeft = pelements?.left
- const poriginTop = pelements?.top
- // 应用缩放
- el.width = el.width * ratio
- el.height = el.height * ratio
- el.left = el.left * ratio
- el.top = el.top * ratio
- if (el.type === 'text') {
- const textEl: PPTTextElement = {
- type: 'text',
- id: nanoid(10),
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- rotate: el.rotate,
- defaultFontName: theme.value.fontName,
- defaultColor: theme.value.fontColor,
- content: convertFontSizePtToPx(el.content, ratio),
- style: getStyle(convertFontSizePtToPx(el.content, ratio)),
- lineHeight: 1.15,
- outline: {
- color: el.borderColor,
- width: +(el.borderWidth * ratio).toFixed(2),
- style: el.borderType,
- },
- fill: el.fill.type === 'color' ? el.fill.value : '',
- vertical: el.isVertical,
- }
- if (el.shadow) {
- textEl.shadow = {
- h: el.shadow.h * ratio,
- v: el.shadow.v * ratio,
- blur: el.shadow.blur * ratio,
- color: el.shadow.color,
- }
- }
- slide.elements.push(textEl)
- }
- // ---------- 图片 ----------
- if (el.type === 'image') {
- const element: PPTImageElement = {
- type: 'image',
- id: nanoid(10),
- src: el.src, // 可能是 base64 或已有 URL
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- fixedRatio: true,
- rotate: el.rotate,
- flipH: el.isFlipH,
- flipV: el.isFlipV,
- }
- // 边框
- if (el.borderWidth) {
- element.outline = {
- color: el.borderColor,
- width: +(el.borderWidth * ratio).toFixed(2),
- style: el.borderType,
- }
- }
- // 裁剪(形状剪裁)
- const clipShapeTypes = ['roundRect', 'ellipse', 'triangle', 'rhombus', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'parallelogram', 'trapezoid']
- if (el.rect) {
- element.clip = {
- shape: (el.geom && clipShapeTypes.includes(el.geom)) ? el.geom : 'rect',
- range: [
- [el.rect.l || 0, el.rect.t || 0],
- [100 - (el.rect.r || 0), 100 - (el.rect.b || 0)],
- ],
- }
- }
- else if (el.geom && clipShapeTypes.includes(el.geom)) {
- element.clip = {
- shape: el.geom,
- range: [[0, 0], [100, 100]],
- }
- }
- // 如果 src 是 base64,触发上传
- if (el.src && typeof el.src === 'string' && el.src.startsWith('data:')) {
- const uploadTask = (async () => {
- try {
- const file = await makeWhiteTransparent(el.src, `image_${Date.now()}.png`)
- if (file) {
- const url = await uploadFileToS3(file)
- element.src = url // 替换为远程 URL
- const slidesStore = useSlidesStore()
- slidesStore.updateElement({ id: element.id, props: { src: url } })
- }
- }
- catch (error) {
- console.error('Image upload failed:', error)
- // 失败时保留原 base64(或可置空)
- }
- })()
- uploadTasks.push(uploadTask)
- }
- slide.elements.push(element)
- }
- else if (el.type === 'math') {
- const element: PPTImageElement = {
- type: 'image',
- id: nanoid(10),
- src: el.picBase64,
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- fixedRatio: true,
- rotate: 0,
- }
- // 如果 src 是 base64,触发上传
- if (el.picBase64 && typeof el.picBase64 === 'string' && el.picBase64.startsWith('data:')) {
- const uploadTask = (async () => {
- try {
- const file = makeWhiteTransparent(el.picBase64, `image_${Date.now()}.png`)
- if (file) {
- const url = await uploadFileToS3(file)
- element.src = url // 替换为远程 URL
- const slidesStore = useSlidesStore()
- slidesStore.updateElement({ id: element.id, props: { src: url } })
- }
- }
- catch (error) {
- console.error('Image upload failed:', error)
- // 失败时保留原 base64(或可置空)
- }
- })()
- uploadTasks.push(uploadTask)
- }
- slide.elements.push(element)
- }
- // ---------- 音频 ----------
- else if (el.type === 'audio') {
- const element: PPTAudioElement = {
- type: 'audio',
- id: nanoid(10),
- src: el.blob,
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- rotate: 0,
- fixedRatio: false,
- color: theme.value.themeColors[0],
- loop: false,
- autoplay: false,
- }
- const localData = el.blob || (el.src && typeof el.src === 'string' && el.src.startsWith('data:') ? el.src : null)
- if (localData) {
- const uploadTask = (async () => {
- try {
- const file = await dataToFile(localData, `audio_${Date.now()}.mp3`, 'audio/mpeg')
- if (file) {
- const url = await uploadFileToS3(file)
- element.src = url
- const slidesStore = useSlidesStore()
- slidesStore.updateElement({ id: element.id, props: { src: url } })
- }
- }
- catch (error) {
- console.error('Audio upload failed:', error)
- }
- })()
- uploadTasks.push(uploadTask)
- }
- slide.elements.push(element)
- }
- // ---------- 视频 ----------
- else if (el.type === 'video') {
- const element: PPTVideoElement = {
- type: 'video',
- id: nanoid(10),
- src: (el.blob || el.src)!,
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- rotate: 0,
- autoplay: false,
- }
- const localData = el.blob || (el.src && typeof el.src === 'string' && el.src.startsWith('data:') ? el.src : null)
- if (localData) {
- const uploadTask = (async () => {
- try {
- const file = await dataToFile(localData, `video_${Date.now()}.mp4`, 'video/mp4')
- if (file) {
- const url = await uploadFileToS3(file)
- element.src = url
- const slidesStore = useSlidesStore()
- slidesStore.updateElement({ id: element.id, props: { src: url } })
- }
- }
- catch (error) {
- console.error('Video upload failed:', error)
- }
- })()
- uploadTasks.push(uploadTask)
- }
- slide.elements.push(element)
- }
- // ---------- 形状 ----------
- else if (el.type === 'shape') {
- if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
- // 线条元素(单独处理)
- const lineElement = parseLineElement(el, ratio)
- slide.elements.push(lineElement)
- }
- else {
- const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
- const vAlignMap: { [key: string]: ShapeTextAlign } = {
- mid: 'middle',
- down: 'bottom',
- up: 'top',
- }
- const gradient: Gradient | undefined = el.fill?.type === 'gradient'
- ? {
- type: el.fill.value.path === 'line' ? 'linear' : 'radial',
- colors: el.fill.value.colors.map(item => ({
- ...item,
- pos: parseInt(item.pos),
- })),
- rotate: el.fill.value.rot,
- }
- : undefined
- const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined
- const fill = el.fill?.type === 'color' ? el.fill.value : ''
- const style = getStyle(convertFontSizePtToPx(el.content, ratio)) + (el.pathBBox.pWidth ? ';width:' + (el.pathBBox.pWidth * ratio) + 'px;height:' + (el.pathBBox.pHeight * ratio) + 'px;' : '') // 设置字体的样式等,这里由于不支持的样式在里面会过滤
- const element: PPTShapeElement = {
- type: 'shape',
- id: nanoid(10),
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- viewBox: [200, 200],
- path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
- fill,
- gradient,
- pattern,
- fixedRatio: false,
- rotate: el.rotate,
- pathBBox: el.pathBBox,
- outline: {
- color: el.borderColor,
- width: +(el.borderWidth * ratio).toFixed(2),
- style: el.borderType,
- },
- text: {
- content: convertFontSizePtToPx(el.content, ratio),
- style: style,
- defaultFontName: theme.value.fontName,
- defaultColor: theme.value.fontColor,
- align: vAlignMap[el.vAlign] || 'middle',
- },
- flipH: el.isFlipH,
- flipV: el.isFlipV,
- }
- if (el.shadow) {
- element.shadow = {
- h: el.shadow.h * ratio,
- v: el.shadow.v * ratio,
- blur: el.shadow.blur * ratio,
- color: el.shadow.color,
- }
- }
- if (shape) {
- element.path = shape.path
- // const { maxX, maxY } = getSvgPathRange(el.path);
- element.viewBox = shape.viewBox
- // element.viewBox = [originWidth || maxX, originHeight || maxY];
- if (shape.pathFormula) {
- element.pathFormula = shape.pathFormula
- element.viewBox = [el.width, el.height]
- // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];
- const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula]
- if ('editable' in pathFormula && pathFormula.editable) {
- element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue)
- element.keypoints = pathFormula.defaultValue
- }
- else {
- element.path = pathFormula.formula(el.width, el.height)
- }
- }
- }
- else if (el.path && el.path.indexOf('NaN') === -1) {
- const { maxX, maxY } = getSvgPathRange(el.path)
- element.path = el.path
- element.viewBox = poriginWidth ? [maxX, maxY] : [originWidth, originHeight]
- // element.viewBox = [originWidth || maxX, originHeight || maxY];
- // element.viewBox = originWidth? [(originWidth/(poriginWidth||1)), (originHeight/(poriginHeight||1))] : [maxX, maxY];
- // element.viewBox = [poriginWidth || maxX, poriginHeight || maxY];
- }
- if (el.shapType === 'custom') {
- if (el.path!.indexOf('NaN') !== -1) {
- if (element.width === 0) element.width = 0.1
- if (element.height === 0) element.height = 0.1
- element.path = el.path!.replace(/NaN/g, '0')
- }
- const { maxX, maxY } = getSvgPathRange(element.path)
- element.viewBox = poriginWidth ? [maxX, maxY] : [originWidth, originHeight]
- // element.viewBox = [originWidth || maxX, originHeight || maxY];
- // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];
- // element.viewBox = [poriginWidth || originWidth || maxX, poriginHeight || originHeight || maxY];
- // element.viewBox = [Math.max(maxX, originWidth), Math.max(maxY, originHeight)];
- // element.viewBox = [originWidth, originHeight];
- }
- if (element.path) slide.elements.push(element)
- }
- }
- // ---------- 表格 ----------
- else if (el.type === 'table') {
- const row = el.data.length
- const col = el.data[0].length
- const style: TableCellStyle = {
- fontname: theme.value.fontName,
- color: theme.value.fontColor,
- }
- const data: TableCell[][] = []
- for (let i = 0; i < row; i++) {
- const rowCells: TableCell[] = []
- for (let j = 0; j < col; j++) {
- const cellData = el.data[i][j]
- let textDiv: HTMLDivElement | null = document.createElement('div')
- textDiv.innerHTML = cellData.text
- const p = textDiv.querySelector('p')
- const align = p?.style.textAlign || 'left'
- const span = textDiv.querySelector('span')
- const fontsize = span?.style.fontSize
- ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px'
- : ''
- const fontname = span?.style.fontFamily || ''
- const color = span?.style.color || cellData.fontColor
- rowCells.push({
- id: nanoid(10),
- colspan: cellData.colSpan || 1,
- rowspan: cellData.rowSpan || 1,
- text: textDiv.innerText,
- style: {
- ...style,
- align: ['left', 'right', 'center'].includes(align) ? (align as 'left' | 'right' | 'center') : 'left',
- fontsize,
- fontname,
- color,
- bold: cellData.fontBold,
- backcolor: cellData.fillColor,
- },
- })
- textDiv = null
- }
- data.push(rowCells)
- }
- const allWidth = el.colWidths.reduce((a, b) => a + b, 0)
- const colWidths: number[] = el.colWidths.map(item => item / allWidth)
- const firstCell = el.data[0][0]
- const border = firstCell.borders.top ||
- firstCell.borders.bottom ||
- el.borders.top ||
- el.borders.bottom ||
- firstCell.borders.left ||
- firstCell.borders.right ||
- el.borders.left ||
- el.borders.right
- const borderWidth = border?.borderWidth || 0
- const borderStyle = border?.borderType || 'solid'
- const borderColor = border?.borderColor || '#eeece1'
- slide.elements.push({
- type: 'table',
- id: nanoid(10),
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- colWidths,
- rotate: 0,
- data,
- outline: {
- width: +(borderWidth * ratio || 2).toFixed(2),
- style: borderStyle,
- color: borderColor,
- },
- cellMinHeight: el.rowHeights[0] ? el.rowHeights[0] * ratio : 36,
- })
- }
- // ---------- 图表 ----------
- else if (el.type === 'chart') {
- let labels: string[]
- let legends: string[]
- let series: number[][]
- if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
- labels = el.data[0].map((_, index) => lang.ssCoord.replace(/\*/g, (index + 1)))
- legends = ['X', 'Y']
- series = el.data
- }
- else {
- const data = el.data as ChartItem[]
- labels = Object.values(data[0].xlabels)
- legends = data.map(item => item.key)
- series = data.map(item => item.values.map(v => v.y))
- }
- const options: ChartOptions = {}
- let chartType: ChartType = 'bar'
- switch (el.chartType) {
- case 'barChart':
- case 'bar3DChart':
- chartType = 'bar'
- if (el.barDir === 'bar') chartType = 'column'
- if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
- break
- case 'lineChart':
- case 'line3DChart':
- if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
- chartType = 'line'
- break
- case 'areaChart':
- case 'area3DChart':
- if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true
- chartType = 'area'
- break
- case 'scatterChart':
- case 'bubbleChart':
- chartType = 'scatter'
- break
- case 'pieChart':
- case 'pie3DChart':
- chartType = 'pie'
- break
- case 'radarChart':
- chartType = 'radar'
- break
- case 'doughnutChart':
- chartType = 'ring'
- break
- default:
- }
- slide.elements.push({
- type: 'chart',
- id: nanoid(10),
- chartType,
- width: el.width,
- height: el.height,
- left: el.left,
- top: el.top,
- rotate: 0,
- themeColors: el.colors.length ? el.colors : theme.value.themeColors,
- textColor: theme.value.fontColor,
- data: {
- labels,
- legends,
- series,
- },
- options,
- })
- }
- // ---------- 组合 ----------
- else if (el.type === 'group') {
- // 先将子元素坐标转换到画布绝对坐标
- let elements: BaseElement[] = el.elements.map((_el: any) => {
- let left = _el.left + originLeft
- let top = _el.top + originTop
- if (el.rotate) {
- const { x, y } = calculateRotatedPosition(
- originLeft, originTop, originWidth, originHeight,
- _el.left, _el.top, el.rotate
- )
- left = x
- top = y
- }
- const element = {
- ..._el,
- left,
- top,
- }
- if (el.isFlipH && 'isFlipH' in element) element.isFlipH = true
- if (el.isFlipV && 'isFlipV' in element) element.isFlipV = true
- return element
- })
- if (el.isFlipH) elements = flipGroupElements(elements, 'y')
- if (el.isFlipV) elements = flipGroupElements(elements, 'x')
- // 递归解析子元素(注意:子元素的上传任务会加入同一个 uploadTasks 数组)
- await parseElements(elements, el)
- }
- // ---------- 图表组合(SmartArt)----------
- else if (el.type === 'diagram') {
- const elements = el.elements.map((_el: any) => ({
- ..._el,
- left: _el.left + originLeft,
- top: _el.top + originTop,
- }))
- await parseElements(elements, el)
- }
- }
- }
- // 开始解析当前幻灯片的所有元素(包括布局元素)
- await parseElements([...item.elements, ...item.layoutElements])
- // 幻灯片构建完成,加入数组
- slides.push(slide)
- }
- // 根据选项将幻灯片插入 store
- if (cover) {
- slidesStore.updateSlideIndex(0)
- slidesStore.setSlides(slides)
- addHistorySnapshot()
- }
- else if (isEmptySlide.value) {
- slidesStore.setSlides(slides)
- addHistorySnapshot()
- }
- else {
- addSlidesFromData(slides)
- }
- // 等待当前幻灯片内所有上传任务完成
- // await Promise.all(uploadTasks)
- Promise.all(uploadTasks)
- exporting.value = false
- onclose?.()
- /*
- // 更新视口尺寸(如果提供了的话)
- if (width !== undefined && height !== undefined) {
- console.log('正在触发视口尺寸更新事件:', { width, height, viewportRatio })
-
- // 同时也要更新slidesStore中的相关数据
- if (slidesStore.setViewportSize) {
- console.log('正在更新store中的视口尺寸')
- slidesStore.setViewportSize(width)
- if (slidesStore.setViewportRatio && viewportRatio !== undefined) {
- slidesStore.setViewportRatio(viewportRatio)
- console.log('视口比例已更新:', viewportRatio)
- }
- }
-
- window.dispatchEvent(new CustomEvent('viewportSizeUpdated', {
- detail: { width, height, viewportRatio }
- }))
- console.log('视口尺寸更新事件已触发')
- }
-
- // 导入成功后,触发画布尺寸更新
- // 使用 nextTick 确保DOM更新完成后再触发
- console.log('开始触发画布尺寸更新事件...')
- nextTick(() => {
- console.log('DOM更新完成,触发 slidesDataUpdated 事件')
- // 触发自定义事件,通知需要更新画布尺寸的组件
- window.dispatchEvent(new CustomEvent('slidesDataUpdated', {
- detail: {
- slides,
- cover,
- title,
- theme,
- width,
- height,
- viewportRatio,
- timestamp: Date.now()
- }
- }))
- console.log('slidesDataUpdated 事件已触发')
-
- // 检查并调整幻灯片索引,确保在有效范围内
- const newSlideCount = slides.length
- const currentIndex = slidesStore.slideIndex
- if (currentIndex >= newSlideCount) {
- console.log('调整幻灯片索引:', currentIndex, '->', Math.max(0, newSlideCount - 1))
- slidesStore.updateSlideIndex(Math.max(0, newSlideCount - 1))
- }
-
- console.log('画布尺寸更新事件处理完成')
-
-
- })
- */
- }
- reader.readAsArrayBuffer(file)
- }
- const getFile = (url: string): Promise<{ data: any }> => {
- return new Promise((resolve, reject) => {
- // 检查 AWS SDK 是否可用
- if (typeof window !== 'undefined' && !window.AWS) {
- reject(new Error('AWS SDK not available'))
- return
- }
- const credentials = {
- accessKeyId: 'AKIATLPEDU37QV5CHLMH',
- secretAccessKey: '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',
- } // 秘钥形式的登录上传
- window.AWS.config.update(credentials)
- window.AWS.config.region = 'cn-northwest-1' // 设置区域
- const s3 = new window.AWS.S3({ params: { Bucket: 'ccrb' } })
- // 解析文件名
- const bucketUrl = 'https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/'
- if (!url.startsWith(bucketUrl)) {
- reject(new Error('Invalid S3 URL format'))
- return
- }
- const name = decodeURIComponent(url.split(bucketUrl)[1])
- // const name = url.split(bucketUrl)[1]
- console.log('aws-name:', name)
- if (!name) {
- reject(new Error('Could not extract file name from URL'))
- return
- }
- const params = {
- Bucket: 'ccrb',
- Key: name,
- }
- s3.getObject(params, (err: any, data: any) => {
- if (err) {
- console.error('S3 getObject error:', err, err.stack)
- reject(err)
- }
- else {
- console.log('S3 getObject success:', data)
- resolve({ data: data.Body })
- }
- })
- })
- }
- const getFile2 = (url: string): Promise<{ data: any }> => {
- return new Promise((resolve, reject) => {
- console.log('直接使用原始 URL 获取文件:', url)
- // 直接使用 fetch 获取文件,浏览器会自动处理 URL 解码
- fetch(url)
- .then(response => {
- if (!response.ok) {
- console.error('HTTP 错误:', response.status, response.statusText)
- throw new Error(`HTTP error! status: ${response.status}`)
- }
- console.log('文件获取成功,大小:', response.headers.get('content-length'))
- return response.arrayBuffer()
- })
- .then(buffer => {
- console.log('文件内容读取成功,大小:', buffer.byteLength)
- resolve({ data: buffer })
- })
- .catch(error => {
- console.error('Fetch error:', error)
- reject(error)
- })
- })
- }
- return {
- importSpecificFile,
- importJSON,
- importPPTXFile,
- readJSON,
- exportJSON2,
- exporting,
- getFile,
- getFile2,
- dataToFile,
- uploadFileToS3
- }
- }
|