Browse Source

Merge branch 'beta' of https://git.cocorobo.cn/jack/PPT into beta

lsc 2 days ago
parent
commit
fd8857e365

+ 1333 - 1288
src/hooks/useImport.ts

@@ -421,17 +421,24 @@ export default () => {
     throw new Error('Unsupported data type')
     throw new Error('Unsupported data type')
   }
   }
 
 
+
   const makeWhiteTransparent = async (
   const makeWhiteTransparent = async (
     data: string | Blob,
     data: string | Blob,
     filename: string,
     filename: string,
     options?: { tolerance?: number }
     options?: { tolerance?: number }
   ): Promise<File> => {
   ): Promise<File> => {
-    const tolerance = options?.tolerance ?? 30 // 容差值,控制哪些颜色被视为白色
-  
+    //const tolerance = options?.tolerance ?? 30 // 容差值,控制哪些颜色被视为白色
+
+    // 容差:颜色到白色的欧几里得距离阈值
+    const distThreshold = options?.tolerance ?? 50; // 50 是经验值,可根据效果调整
+
+    // 是否启用边缘恢复(去除白边)
+    const removeWhiteMatte = true; // 可改为配置项
+
     // 1. 将输入数据统一为 Blob 或可直接用于加载的 URL
     // 1. 将输入数据统一为 Blob 或可直接用于加载的 URL
     let imageUrl: string
     let imageUrl: string
     let blob: Blob
     let blob: Blob
-  
+
     if (typeof data === 'string') {
     if (typeof data === 'string') {
       // 如果是 Base64,直接用作 src(data URL)
       // 如果是 Base64,直接用作 src(data URL)
       imageUrl = data.startsWith('data:') ? data : `data:image/png;base64,${data}`
       imageUrl = data.startsWith('data:') ? data : `data:image/png;base64,${data}`
@@ -454,1455 +461,1493 @@ export default () => {
       // 如果图像来自跨域,可能需要设置 crossOrigin
       // 如果图像来自跨域,可能需要设置 crossOrigin
       // image.crossOrigin = 'anonymous';
       // image.crossOrigin = 'anonymous';
     })
     })
-  
+
     // 3. 创建 Canvas 并绘制图像
     // 3. 创建 Canvas 并绘制图像
     const canvas = document.createElement('canvas')
     const canvas = document.createElement('canvas')
     canvas.width = img.width
     canvas.width = img.width
     canvas.height = img.height
     canvas.height = img.height
     const ctx = canvas.getContext('2d')!
     const ctx = canvas.getContext('2d')!
     ctx.drawImage(img, 0, 0)
     ctx.drawImage(img, 0, 0)
-  
+
     // 4. 获取像素数据并处理
     // 4. 获取像素数据并处理
     const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
     const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
     const dataArray = imageData.data
     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(完全透明)
+    /*
+      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
+    for (let i = 0; i < dataArray.length; i += 4) {
+      const r = dataArray[i];
+      const g = dataArray[i + 1];
+      const b = dataArray[i + 2];
+      const a = dataArray[i + 3]; // 当前 alpha(可能是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 <= distThreshold) {
+        // 完全视为背景,设为全透明
+        dataArray[i + 3] = 0;
+      } else if (removeWhiteMatte && a === 255) {
+        // 尝试恢复边缘(仅针对完全不透明但受背景影响的像素)
+        // 估计背景混合度:用最小通道值近似计算 alpha
+        const minChannel = Math.min(r, g, b);
+        const bgWeight = 1 - minChannel / 255; // 背景混合权重
+        if (bgWeight > 0.01 && bgWeight < 0.99) {
+          // 存在混合,反推前景色
+          const alpha = 1 - bgWeight; // 前景透明度
+          // 反推公式:前景色 = (当前颜色 - 背景色*(1-alpha)) / alpha
+          const newR = (r - 255 * (1 - alpha)) / alpha;
+          const newG = (g - 255 * (1 - alpha)) / alpha;
+          const newB = (b - 255 * (1 - alpha)) / alpha;
+
+          // 将计算结果写回,并设置alpha
+          dataArray[i] = Math.max(0, Math.min(255, newR));
+          dataArray[i + 1] = Math.max(0, Math.min(255, newG));
+          dataArray[i + 2] = Math.max(0, Math.min(255, newB));
+          dataArray[i + 3] = Math.round(alpha * 255);
+        }
       }
       }
 
 
-      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,
-      }
+      // 5. 将修改后的像素放回 Canvas
+      ctx.putImageData(imageData, 0, 0)
 
 
-      bucket
-        .upload(params, options)
-        .promise()
-        .then(data => resolve(data.Location))
-        .catch(err => reject(err))
-    })
-  }
+      // 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)
+      }
 
 
-  /*
-  // 导入PPTX文件
-  const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal }) => {
-    console.log('导入', files)
-    const defaultOptions = {
-      cover: false,
-      fixedViewport: false, 
+      // 8. 返回 File 对象
+      return new File([outputBlob], filename, { type: 'image/png' })
     }
     }
-    const { cover, fixedViewport, signal } = { ...defaultOptions, ...options }
 
 
-    const file = files[0]
-    if (!file) return
+    /**
+     * 上传 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
+        }
 
 
-    exporting.value = true
+        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,
+        }
 
 
-    const shapeList: ShapePoolItem[] = []
-    for (const item of SHAPE_LIST) {
-      shapeList.push(...item.children)
+        bucket
+          .upload(params, options)
+          .promise()
+          .then(data => resolve(data.Location))
+          .catch(err => reject(err))
+      })
     }
     }
-    
-    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)
+
+    /*
+    // 导入PPTX文件
+    const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal }) => {
+      console.log('导入', files)
+      const defaultOptions = {
+        cover: false,
+        fixedViewport: false, 
       }
       }
-      catch (error) {
-        exporting.value = false
-        console.log('导入PPTX文件失败:', error)
-        message.error('无法正确读取 / 解析该文件')
-        return
+      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)
       }
       }
-
-      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,
-            },
-          }
+      const reader = new FileReader()
+      reader.onload = async (e: ProgressEvent<FileReader>) => {
+        // 检查是否已取消
+        if (signal?.aborted) {
+          exporting.value = false
+          return
         }
         }
-        else {
-          background = {
-            type: 'solid',
-            color: value || '#fff',
-          }
+        
+        let json = null
+        try {
+          json = await parse(e.target!.result as ArrayBuffer)
         }
         }
-
-        const slide: Slide = {
-          id: nanoid(10),
-          elements: [],
-          background,
-          remark: item.note || '',
+        catch (error) {
+          exporting.value = false
+          console.log('导入PPTX文件失败:', error)
+          message.error('无法正确读取 / 解析该文件')
+          return
         }
         }
-
-        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,
-              })
+  
+        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 (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 (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 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 {
+            background = {
+              type: 'solid',
+              color: value || '#fff',
             }
             }
-            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',
+          }
+  
+          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),
                   id: nanoid(10),
                   width: el.width,
                   width: el.width,
                   height: el.height,
                   height: el.height,
                   left: el.left,
                   left: el.left,
                   top: el.top,
                   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,
                   rotate: el.rotate,
+                  defaultFontName: theme.value.fontName,
+                  defaultColor: theme.value.fontColor,
+                  content: convertFontSizePtToPx(el.content, ratio),
+                  lineHeight: 1,
                   outline: {
                   outline: {
                     color: el.borderColor,
                     color: el.borderColor,
                     width: +(el.borderWidth * ratio).toFixed(2),
                     width: +(el.borderWidth * ratio).toFixed(2),
                     style: el.borderType,
                     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,
+                  fill: el.fill.type === 'color' ? el.fill.value : '',
+                  vertical: el.isVertical,
                 }
                 }
                 if (el.shadow) {
                 if (el.shadow) {
-                  element.shadow = {
+                  textEl.shadow = {
                     h: el.shadow.h * ratio,
                     h: el.shadow.h * ratio,
                     v: el.shadow.v * ratio,
                     v: el.shadow.v * ratio,
                     blur: el.shadow.blur * ratio,
                     blur: el.shadow.blur * ratio,
                     color: el.shadow.color,
                     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)
-                  }
+                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,
                 }
                 }
-                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.borderWidth) {
+                  element.outline = {
+                    color: el.borderColor,
+                    width: +(el.borderWidth * ratio).toFixed(2),
+                    style: el.borderType,
+                  }
                 }
                 }
-                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 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 {
-                    element.special = true
-                    element.path = el.path!
+                }
+                else if (el.geom && clipShapeTypes.includes(el.geom)) {
+                  element.clip = {
+                    shape: el.geom,
+                    range: [[0, 0], [100, 100]]
                   }
                   }
-                  const { maxX, maxY } = getSvgPathRange(element.path)
-                  element.viewBox = [maxX || originWidth, maxY || originHeight]
                 }
                 }
-    
-                if (element.path) slide.elements.push(element)
+                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,
+              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,
+                })
               }
               }
-              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({
+              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),
                     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,
+                    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,
                     },
                     },
-                  })
-                  textDiv = null
+                    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)
                 }
                 }
-                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 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 {
-                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))
+              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,
+                })
               }
               }
-
-              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:
+              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)
               }
               }
-
-              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 = {
+              else if (el.type === 'diagram') {
+                const elements = el.elements.map(_el => ({
                   ..._el,
                   ..._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)
+                  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()
         }
         }
-        parseElements([...item.elements, ...item.layoutElements])
-        slides.push(slide)
+        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
+      })
+    }
+  */
 
 
-      if (cover) {
-        slidesStore.updateSlideIndex(0)
-        slidesStore.setSlides(slides)
-        addHistorySnapshot()
-      }
-      else if (isEmptySlide.value) {
-        slidesStore.setSlides(slides)
-        addHistorySnapshot()
+    const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal, onclose?: () => void }) => {
+      console.log('导入', files)
+      const defaultOptions = {
+        cover: false,
+        fixedViewport: false,
       }
       }
-      else addSlidesFromData(slides)
+      const { cover, fixedViewport, signal, onclose } = { ...defaultOptions, ...options }
 
 
-      exporting.value = false
-    }
-    reader.readAsArrayBuffer(file)
-    
-    // 监听取消信号
-    signal?.addEventListener('abort', () => {
-      reader.abort()
-      exporting.value = false
-    })
-    
-    // 监听取消信号
-    signal?.addEventListener('abort', () => {
-      reader.abort()
-      exporting.value = false
-    })
-  }
-*/
+      const file = files[0]
+      if (!file) return
 
 
-  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 }
+      exporting.value = true // 假设 exporting 是一个全局 ref
 
 
-    const file = files[0]
-    if (!file) return
+      // 预加载形状库(用于后续形状匹配)
+      const shapeList: ShapePoolItem[] = []
+      for (const item of SHAPE_LIST) {
+        shapeList.push(...item.children)
+      }
 
 
-    exporting.value = true // 假设 exporting 是一个全局 ref
+      const reader = new FileReader()
+      reader.onload = async (e: ProgressEvent<FileReader>) => {
+        // 检查是否已取消
+        if (signal?.aborted) {
+          exporting.value = false
+          return
+        }
 
 
-    // 预加载形状库(用于后续形状匹配)
-    const shapeList: ShapePoolItem[] = []
-    for (const item of SHAPE_LIST) {
-      shapeList.push(...item.children)
-    }
+        let json = null
+        try {
+          json = await parse(e.target!.result as ArrayBuffer)
+        }
+        catch (error) {
+          exporting.value = false
+          console.log('导入PPTX文件失败:', error)
+          message.error('无法正确读取 / 解析该文件')
+          return
+        }
 
 
-    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
-      }
-
-      if (signal?.aborted) {
-        exporting.value = false
-        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) // 调整画布大小
-      }
+        // 计算缩放比例
+        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',
-            },
+        // 设置主题色
+        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 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',
+          else {
+            background = {
+              type: 'solid',
+              color: value || '#fff',
+            }
           }
           }
-        }
 
 
-        const slide: Slide = {
-          id: nanoid(10),
-          elements: [],
-          background,
-          remark: item.note || '',
-        }
+          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,
+          // ----- 解析元素(递归函数)-----
+          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,
                 }
                 }
-              }
-              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,
+                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)
               }
               }
-
-              // 裁剪(形状剪裁)
-              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)],
-                  ],
+              // ---------- 图片 ----------
+              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,
                 }
                 }
-              }
-              else if (el.geom && clipShapeTypes.includes(el.geom)) {
-                element.clip = {
-                  shape: el.geom,
-                  range: [[0, 0], [100, 100]],
+
+                // 边框
+                if (el.borderWidth) {
+                  element.outline = {
+                    color: el.borderColor,
+                    width: +(el.borderWidth * ratio).toFixed(2),
+                    style: el.borderType,
+                  }
                 }
                 }
-              }
 
 
-              // 如果 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
+                // 裁剪(形状剪裁)
+                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)],
+                    ],
                   }
                   }
-                  catch (error) {
-                    console.error('Image upload failed:', error)
-                    // 失败时保留原 base64(或可置空)
+                }
+                else if (el.geom && clipShapeTypes.includes(el.geom)) {
+                  element.clip = {
+                    shape: el.geom,
+                    range: [[0, 0], [100, 100]],
                   }
                   }
-                })()
-                uploadTasks.push(uploadTask)
+                }
+
+                // 如果 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)
 
 
-              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,
               }
               }
+              // ---------- 音频 ----------
+              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,
+                }
 
 
-              // 如果 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)
+                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,
+                }
 
 
-              slide.elements.push(element)
+                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)
+                }
 
 
-            }
-            // ---------- 音频 ----------
-            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,
+                slide.elements.push(element)
               }
               }
 
 
-              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)
+
+              // ---------- 形状 ----------
+              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',
                   }
                   }
-                })()
-                uploadTasks.push(uploadTask)
-              }
 
 
-              slide.elements.push(element)
-            }
+                  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;
 
 
-            // ---------- 视频 ----------
-            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 pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined;
+                  const fill = el.fill?.type === 'color' ? el.fill.value : '';
+                  let 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,
+                  }
 
 
-              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)
+                  if (el.shadow) {
+                    element.shadow = {
+                      h: el.shadow.h * ratio,
+                      v: el.shadow.v * ratio,
+                      blur: el.shadow.blur * ratio,
+                      color: el.shadow.color,
                     }
                     }
-                    else {
-                      file = dataToFile(localData, `video_${Date.now()}.mp4`, 'video/mp4')
+                  }
+
+                  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)
+                      }
                     }
                     }
-                    const url = await uploadFileToS3(file)
-                    element.src = url
                   }
                   }
-                  catch (error) {
-                    console.error('Video upload failed:', error)
+                  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];
                   }
                   }
-                })()
-                uploadTasks.push(uploadTask)
-              }
-
-              slide.elements.push(element)
-            }
 
 
+                  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];
+                  }
 
 
-            // ---------- 形状 ----------
-            else if (el.type === 'shape') {
-              if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
-                // 线条元素(单独处理)
-                const lineElement = parseLineElement(el, ratio)
-                slide.elements.push(lineElement)
+                  if (element.path) slide.elements.push(element)
+                }
               }
               }
-              else {
-                const shape = shapeList.find(item => item.pptxShapeType === el.shapType)
 
 
-                const vAlignMap: { [key: string]: ShapeTextAlign } = {
-                  mid: 'middle',
-                  down: 'bottom',
-                  up: 'top',
-                }
+              // ---------- 表格 ----------
+              else if (el.type === 'table') {
+                const row = el.data.length
+                const col = el.data[0].length
 
 
-                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,
+                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
                   }
                   }
-                  : undefined;
+                  data.push(rowCells)
+                }
 
 
-                const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined;
-                const fill = el.fill?.type === 'color' ? el.fill.value : '';
-                let 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',
+                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),
                   id: nanoid(10),
                   width: el.width,
                   width: el.width,
                   height: el.height,
                   height: el.height,
                   left: el.left,
                   left: el.left,
                   top: el.top,
                   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,
+                  colWidths,
+                  rotate: 0,
+                  data,
                   outline: {
                   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',
+                    width: +(borderWidth * ratio || 2).toFixed(2),
+                    style: borderStyle,
+                    color: borderColor,
                   },
                   },
-                  flipH: el.isFlipH,
-                  flipV: el.isFlipV,
-                }
+                  cellMinHeight: el.rowHeights[0] ? el.rowHeights[0] * ratio : 36,
+                })
+              }
 
 
-                if (el.shadow) {
-                  element.shadow = {
-                    h: el.shadow.h * ratio,
-                    v: el.shadow.v * ratio,
-                    blur: el.shadow.blur * ratio,
-                    color: el.shadow.color,
-                  }
-                }
+              // ---------- 图表 ----------
+              else if (el.type === 'chart') {
+                let labels: string[]
+                let legends: string[]
+                let series: number[][]
 
 
-                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)
-                    }
-                  }
+                if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
+                  labels = el.data[0].map((_, index) => `坐标${index + 1}`)
+                  legends = ['X', 'Y']
+                  series = el.data
                 }
                 }
-                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];
+                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))
                 }
                 }
 
 
-                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];
+                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:
                 }
                 }
 
 
-                if (element.path) slide.elements.push(element)
+                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 === 'table') {
-              const row = el.data.length
-              const col = el.data[0].length
+              // ---------- 组合 ----------
+              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 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 element = {
+                    ..._el,
+                    left,
+                    top,
+                  }
+                  if (el.isFlipH && 'isFlipH' in element) element.isFlipH = true
+                  if (el.isFlipV && 'isFlipV' in element) element.isFlipV = true
 
 
-              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,
-              })
-            }
+                  return element
+                })
 
 
-            // ---------- 图表 ----------
-            else if (el.type === 'chart') {
-              let labels: string[]
-              let legends: string[]
-              let series: number[][]
+                if (el.isFlipH) elements = flipGroupElements(elements, 'y')
+                if (el.isFlipV) elements = flipGroupElements(elements, 'x')
 
 
-              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))
+                // 递归解析子元素(注意:子元素的上传任务会加入同一个 uploadTasks 数组)
+                await parseElements(elements, el)
               }
               }
 
 
-              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:
+              // ---------- 图表组合(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)
               }
               }
-
-              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
+          // 开始解析当前幻灯片的所有元素(包括布局元素)
+          await parseElements([...item.elements, ...item.layoutElements])
 
 
-                return element
-              })
+          // 幻灯片构建完成,加入数组
+          slides.push(slide)
+        }
 
 
-              if (el.isFlipH) elements = flipGroupElements(elements, 'y')
-              if (el.isFlipV) elements = flipGroupElements(elements, 'x')
+        // 根据选项将幻灯片插入 store
+        if (cover) {
+          slidesStore.updateSlideIndex(0)
+          slidesStore.setSlides(slides)
+          addHistorySnapshot()
+        }
+        else if (isEmptySlide.value) {
+          slidesStore.setSlides(slides)
+          addHistorySnapshot()
+        }
+        else {
+          addSlidesFromData(slides)
+        }
 
 
-              // 递归解析子元素(注意:子元素的上传任务会加入同一个 uploadTasks 数组)
-              await parseElements(elements, el)
-            }
+        // 等待当前幻灯片内所有上传任务完成
+        await Promise.all(uploadTasks)
 
 
-            // ---------- 图表组合(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)
+        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('视口尺寸更新事件已触发')
         }
         }
-
-        // 开始解析当前幻灯片的所有元素(包括布局元素)
-        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)
+        // 导入成功后,触发画布尺寸更新
+        // 使用 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))
           }
           }
-        }
-        
-        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)
-  }
+          
+          console.log('画布尺寸更新事件处理完成')
+          
+  
+        })
+  */
 
 
-  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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',
-      } // 秘钥形式的登录上传
+      reader.readAsArrayBuffer(file)
+    }
 
 
-      window.AWS.config.update(credentials)
-      window.AWS.config.region = 'cn-northwest-1' // 设置区域
+    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 s3 = new window.AWS.S3({ params: { Bucket: 'ccrb' } })
+        const credentials = {
+          accessKeyId: 'AKIATLPEDU37QV5CHLMH',
+          secretAccessKey: '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',
+        } // 秘钥形式的登录上传
 
 
-      // 解析文件名
-      const bucketUrl = 'https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/'
-      if (!url.startsWith(bucketUrl)) {
-        reject(new Error('Invalid S3 URL format'))
-        return
-      }
+        window.AWS.config.update(credentials)
+        window.AWS.config.region = 'cn-northwest-1' // 设置区域
 
 
-      const name = decodeURIComponent(url.split(bucketUrl)[1])
-      // const name = url.split(bucketUrl)[1]
-      console.log('aws-name:', name)
+        const s3 = new window.AWS.S3({ params: { Bucket: 'ccrb' } })
 
 
-      if (!name) {
-        reject(new Error('Could not extract file name from URL'))
-        return
-      }
+        // 解析文件名
+        const bucketUrl = 'https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/'
+        if (!url.startsWith(bucketUrl)) {
+          reject(new Error('Invalid S3 URL format'))
+          return
+        }
 
 
-      const params = {
-        Bucket: 'ccrb',
-        Key: name,
-      }
+        const name = decodeURIComponent(url.split(bucketUrl)[1])
+        // const name = url.split(bucketUrl)[1]
+        console.log('aws-name:', name)
 
 
-      s3.getObject(params, (err: any, data: any) => {
-        if (err) {
-          console.error('S3 getObject error:', err, err.stack)
-          reject(err)
+        if (!name) {
+          reject(new Error('Could not extract file name from URL'))
+          return
         }
         }
-        else {
-          console.log('S3 getObject success:', data)
-          resolve({ data: data.Body })
+
+        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)
+    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)
-        })
-    })
-  }
+        // 直接使用 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
-  }
-}
+    return {
+      importSpecificFile,
+      importJSON,
+      importPPTXFile,
+      readJSON,
+      exportJSON2,
+      exporting,
+      getFile,
+      getFile2,
+      dataToFile,
+      uploadFileToS3
+    }
+  }

+ 8 - 1
src/views/components/element/ShapeElement/BaseShapeElement.vue

@@ -57,7 +57,7 @@
           </g>
           </g>
         </svg>
         </svg>
 
 
-        <div class="shape-text" :class="text.align">
+        <div class="shape-text" :style="text.style" :class="[text.align, { 'editable': editable || text.content }]">
           <div class="ProseMirror-static" v-html="text.content"></div>
           <div class="ProseMirror-static" v-html="text.content"></div>
         </div>
         </div>
       </div>
       </div>
@@ -131,6 +131,8 @@ const text = computed<ShapeText>(() => {
   }
   }
 }
 }
 .shape-text {
 .shape-text {
+  width:100%;
+  height:100%;
   position: absolute;
   position: absolute;
   top: 0;
   top: 0;
   bottom: 0;
   bottom: 0;
@@ -140,6 +142,11 @@ const text = computed<ShapeText>(() => {
   flex-direction: column;
   flex-direction: column;
   padding: 5px;
   padding: 5px;
   word-break: break-word;
   word-break: break-word;
+  pointer-events: none;
+
+  &.editable {
+    pointer-events: all;
+  }
 
 
   &.top {
   &.top {
     justify-content: space-around;
     justify-content: space-around;

+ 49 - 14
src/views/components/element/TextElement/BaseTextElement.vue

@@ -16,7 +16,7 @@
         class="element-content"
         class="element-content"
         :style="{
         :style="{
           width: elementInfo.vertical ? 'auto' : elementInfo.width + 'px',
           width: elementInfo.vertical ? 'auto' : elementInfo.width + 'px',
-          height: elementInfo.vertical ? elementInfo.height + 'px' : 'auto',
+          height: elementInfo.vertical ? elementInfo.height + 'px' :  elementInfo.height + 'px',
           backgroundColor: elementInfo.fill,
           backgroundColor: elementInfo.fill,
           opacity: elementInfo.opacity,
           opacity: elementInfo.opacity,
           textShadow: shadowStyle,
           textShadow: shadowStyle,
@@ -30,19 +30,21 @@
           overflow: hidden,
           overflow: hidden,
         }"
         }"
       >
       >
-        <ElementOutline
-          :width="elementInfo.width"
-          :height="elementInfo.height"
-          :outline="elementInfo.outline"
-        />
-        <div 
-          class="text ProseMirror-static" 
-          :class="{ 'thumbnail': target === 'thumbnail' }"
-          :style="{
-            '--paragraphSpace': `${elementInfo.paragraphSpace === undefined ? 5 : elementInfo.paragraphSpace}px`,
-          }"
-          v-html="elementInfo.content"
-        ></div>
+        <div class="shape-text" :style="elementInfo.style" :class="[elementInfo.align, { 'editable': editable || elementInfo.content }]">
+          <ElementOutline
+            :width="elementInfo.width"
+            :height="elementInfo.height"
+            :outline="elementInfo.outline"
+          />
+          <div 
+            class="text ProseMirror-static" 
+            :class="{ 'thumbnail': target === 'thumbnail' }"
+            :style="{
+              '--paragraphSpace': `${elementInfo.paragraphSpace === undefined ? 5 : elementInfo.paragraphSpace}px`,
+            }"
+            v-html="elementInfo.content"
+          ></div>
+        </div>
       </div>
       </div>
     </div>
     </div>
   </div>
   </div>
@@ -86,4 +88,37 @@ const { shadowStyle } = useElementShadow(shadow)
     }
     }
   }
   }
 }
 }
+
+.shape-text {
+  width:100%;
+  height:100%;
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  flex-direction: column;
+  word-break: break-word;
+  pointer-events: none;
+
+  &.editable {
+    pointer-events: all;
+  }
+
+  &.top {
+    justify-content: space-around;
+  }
+  &.middle {
+    justify-content: center;
+    left: 50%;
+    top: 50%;
+    -webkit-transform: translate(-50%,-50%);
+    transform: translate(-50%,-50%);
+  }
+  &.bottom {
+    justify-content: flex-end;
+  }
+}
+
 </style>
 </style>