jack 19 часов назад
Родитель
Сommit
d70b90baf1
2 измененных файлов с 830 добавлено и 8 удалено
  1. 49 0
      src/global.d.ts
  2. 781 8
      src/hooks/useImport.ts

+ 49 - 0
src/global.d.ts

@@ -16,6 +16,7 @@ interface Document {
 }
 
 // AWS SDK 类型声明
+/*
 interface Window {
   AWS: {
     config: {
@@ -26,4 +27,52 @@ interface Window {
       getObject: (params: { Bucket: string; Key: string }, callback: (err: any, data: any) => void) => void
     }
   }
+}
+*/
+
+interface Window {
+  AWS: {
+    config: {
+      update: (credentials: { accessKeyId: string; secretAccessKey: string }) => void;
+      region: string;
+    };
+    S3: new (config: { params: { Bucket: string } }) => S3Instance;
+  };
+}
+
+// 定义 S3 实例的方法
+interface S3Instance {
+  getObject(params: { Bucket: string; Key: string }, callback: (err: any, data: any) => void): void;
+  upload(params: S3UploadParams, options?: S3UploadOptions): S3ManagedUpload;
+}
+
+// upload 方法的参数
+interface S3UploadParams {
+  Key: string;
+  ContentType: string;
+  Body: File | Blob;
+  ACL?: string;
+  // 其他可选参数...
+}
+
+// upload 方法的选项
+interface S3UploadOptions {
+  partSize?: number;
+  queueSize?: number;
+  leavePartsOnError?: boolean;
+}
+
+// upload 返回的管理对象
+interface S3ManagedUpload {
+  promise(): Promise<S3UploadResult>;
+  on(event: string, listener: (...args: any[]) => void): this;
+  send(callback: (err: any, data: any) => void): void;
+}
+
+// upload 成功返回的数据
+interface S3UploadResult {
+  Location: string;
+  ETag: string;
+  Bucket: string;
+  Key: string;
 }

+ 781 - 8
src/hooks/useImport.ts

@@ -21,6 +21,8 @@ import type {
   PPTImageElement,
   ShapeTextAlign,
   PPTTextElement,
+  PPTVideoElement,
+  PPTAudioElement,
   ChartOptions,
   Gradient,
 } from '@/types/slides'
@@ -382,6 +384,140 @@ export default () => {
     return { x: graphicX, y: graphicY }
   }
 
+  /**
+   * 将 base64 字符串或 Blob 转换为 File 对象
+   */
+  const dataToFile = (data: string | Blob, filename: string, mimeType?: string): File => {
+    if (typeof data === 'string') {
+      // 移除可能的 data:image/png;base64, 前缀
+      const base64Data = data.includes('base64,') ? data.split('base64,')[1] : data;
+      const byteCharacters = atob(base64Data);
+      const byteNumbers = new Array(byteCharacters.length);
+      for (let i = 0; i < byteCharacters.length; i++) {
+        byteNumbers[i] = byteCharacters.charCodeAt(i);
+      }
+      const byteArray = new Uint8Array(byteNumbers);
+      const blob = new Blob([byteArray], { type: mimeType || 'image/png' });
+      return new File([blob], filename, { type: blob.type });
+    } 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; // 容差值,控制哪些颜色被视为白色
+  
+    // 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' });
+  };
+
+  /**
+   * 上传 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' } });
+      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: 2048 * 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 }) => {
     console.log('导入', files)
@@ -478,7 +614,7 @@ export default () => {
             el.height = el.height * ratio
             el.left = el.left * ratio
             el.top = el.top * ratio
-  
+
             if (el.type === 'text') {
               const textEl: PPTTextElement = {
                 type: 'text',
@@ -702,7 +838,7 @@ export default () => {
             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,
@@ -742,7 +878,7 @@ export default () => {
                 }
                 data.push(rowCells)
               }
-  
+
               const allWidth = el.colWidths.reduce((a, b) => a + b, 0)
               const colWidths: number[] = el.colWidths.map(item => item / allWidth)
 
@@ -758,7 +894,7 @@ export default () => {
               const borderWidth = border?.borderWidth || 0
               const borderStyle = border?.borderType || 'solid'
               const borderColor = border?.borderColor || '#eeece1'
-  
+
               slide.elements.push({
                 type: 'table',
                 id: nanoid(10),
@@ -781,7 +917,7 @@ export default () => {
               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']
@@ -795,7 +931,7 @@ export default () => {
               }
 
               const options: ChartOptions = {}
-  
+
               let chartType: ChartType = 'bar'
 
               switch (el.chartType) {
@@ -831,7 +967,7 @@ export default () => {
                   break
                 default:
               }
-  
+
               slide.elements.push({
                 type: 'chart',
                 id: nanoid(10),
@@ -905,6 +1041,641 @@ export default () => {
     }
     reader.readAsArrayBuffer(file)
   }
+*/
+
+  const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean }) => {
+    console.log('导入', files);
+    const defaultOptions = {
+      cover: false,
+      fixedViewport: false,
+    };
+    const { cover, fixedViewport } = { ...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 => {
+      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; // PPTX 默认 72 DPI,屏幕 96 DPI
+      const width = json.size.width;
+      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[]) => {
+          // 按绘制顺序排序
+          const sortedElements = elements.sort((a, b) => a.order - b.order);
+
+          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)
+            }
+            // ---------- 图片 ----------
+            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`);
+                    const url = await uploadFileToS3(file);
+                    element.src = url; // 替换为远程 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.src && typeof el.src === 'string' && el.src.startsWith('data:')) {
+                const uploadTask = (async () => {
+                  try {
+                    const file = makeWhiteTransparent(el.src, `image_${Date.now()}.png`, 'image/png');
+                    const url = await uploadFileToS3(file);
+                    element.src = url; // 替换为远程 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,
+              };
+
+              if (el.blob instanceof Blob) {
+                const uploadTask = (async () => {
+                  try {
+                    const file = dataToFile(el.blob, `audio_${Date.now()}.mp3`, el.blob.type);
+                    const url = await uploadFileToS3(file);
+                    element.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;
+                    if (localData instanceof Blob) {
+                      file = dataToFile(localData, `video_${Date.now()}.mp4`, localData.type);
+                    } else {
+                      file = dataToFile(localData, `video_${Date.now()}.mp4`, 'video/mp4');
+                    }
+                    const url = await uploadFileToS3(file);
+                    element.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 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((_, 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,
+                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);
+            }
+
+            // ---------- 图表组合(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);
+            }
+          }
+        };
+
+        // 开始解析当前幻灯片的所有元素(包括布局元素)
+        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);
+      }
+
+      // 等待当前幻灯片内所有上传任务完成
+      Promise.all(uploadTasks);
+
+      exporting.value = false;
+    };
+
+    reader.readAsArrayBuffer(file);
+  };
 
   const getFile = (url: string): Promise<{ data: any }> => {
     return new Promise((resolve, reject) => {
@@ -992,6 +1763,8 @@ export default () => {
     exportJSON2,
     exporting,
     getFile,
-    getFile2
+    getFile2,
+    dataToFile,
+    uploadFileToS3
   }
 }