| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022 |
- 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 // 只处理非常接近白色的像素
- // 1. 将输入统一转为可加载的 URL
- let imageUrl: string
- let needRevoke = false
- if (typeof data === 'string') {
- imageUrl = data.startsWith('data:') ? data : `data:image/png;base64,${data}`
- }
- else if (data instanceof Blob) {
- imageUrl = URL.createObjectURL(data)
- needRevoke = true
- }
- else {
- throw new Error('Unsupported data type: expected string or Blob')
- }
- // 2. 加载图像
- const img = await new Promise<HTMLImageElement>((resolve, reject) => {
- const image = new Image()
- image.onload = () => resolve(image)
- image.onerror = reject
- if (typeof data === 'string' && !data.startsWith('data:')) {
- image.crossOrigin = 'anonymous'
- }
- image.src = imageUrl
- })
- const canvas = document.createElement('canvas')
- try {
- // 3. 绘制到 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
- // 5. 遍历像素:只将白色背景区域设为透明,其他颜色原样保留
- for (let i = 0; i < dataArray.length; i += 4) {
- const r = dataArray[i]
- const g = dataArray[i + 1]
- const b = dataArray[i + 2]
- // 计算与纯白色 (255,255,255) 的欧几里得距离
- 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
- }
- // 其他所有像素(包括浅蓝、灰色、黑色等)保持原样,不修改颜色和透明度
- }
- // 6. 将修改后的像素放回 Canvas
- ctx.putImageData(imageData, 0, 0)
- }
- finally {
- if (needRevoke) {
- URL.revokeObjectURL(imageUrl)
- }
- }
- // 7. 导出为 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')
- })
- // 8. 返回 File 对象
- 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.5,
- 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 {
- let file: File
- 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)
- 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
- }
- }
|