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