|
|
@@ -21,14 +21,33 @@ import type {
|
|
|
PPTImageElement,
|
|
|
ShapeTextAlign,
|
|
|
PPTTextElement,
|
|
|
+ PPTVideoElement,
|
|
|
+ PPTAudioElement,
|
|
|
ChartOptions,
|
|
|
Gradient,
|
|
|
} from '@/types/slides'
|
|
|
|
|
|
const convertFontSizePtToPx = (html: string, ratio: number) => {
|
|
|
- return html.replace(/font-size:\s*([\d.]+)pt/g, (match, p1) => {
|
|
|
- return `font-size: ${(parseFloat(p1) * ratio).toFixed(1)}px`
|
|
|
+ //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 () => {
|
|
|
@@ -380,6 +399,140 @@ export default () => {
|
|
|
return { x: graphicX, y: graphicY }
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 将 base64 字符串或 Blob 转换为 File 对象
|
|
|
+ */
|
|
|
+ const dataToFile = (data: string | Blob, filename: string, mimeType?: string): File => {
|
|
|
+ if (typeof data === 'string') {
|
|
|
+ // 移除可能的 data:image/png;base64, 前缀
|
|
|
+ const base64Data = data.includes('base64,') ? data.split('base64,')[1] : data;
|
|
|
+ const byteCharacters = atob(base64Data);
|
|
|
+ const byteNumbers = new Array(byteCharacters.length);
|
|
|
+ for (let i = 0; i < byteCharacters.length; i++) {
|
|
|
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
|
+ }
|
|
|
+ const byteArray = new Uint8Array(byteNumbers);
|
|
|
+ const blob = new Blob([byteArray], { type: mimeType || 'image/png' });
|
|
|
+ return new File([blob], filename, { type: blob.type });
|
|
|
+ } else if (data instanceof Blob) {
|
|
|
+ return new File([data], filename, { type: data.type });
|
|
|
+ }
|
|
|
+ throw new Error('Unsupported data type');
|
|
|
+ };
|
|
|
+
|
|
|
+ const makeWhiteTransparent = async (
|
|
|
+ data: string | Blob,
|
|
|
+ filename: string,
|
|
|
+ options?: { tolerance?: number }
|
|
|
+ ): Promise<File> => {
|
|
|
+ const tolerance = options?.tolerance ?? 30; // 容差值,控制哪些颜色被视为白色
|
|
|
+
|
|
|
+ // 1. 将输入数据统一为 Blob 或可直接用于加载的 URL
|
|
|
+ let imageUrl: string;
|
|
|
+ let blob: Blob;
|
|
|
+
|
|
|
+ if (typeof data === 'string') {
|
|
|
+ // 如果是 Base64,直接用作 src(data URL)
|
|
|
+ imageUrl = data.startsWith('data:') ? data : `data:image/png;base64,${data}`;
|
|
|
+ } else if (data instanceof Blob) {
|
|
|
+ // 如果是 Blob,创建对象 URL
|
|
|
+ imageUrl = URL.createObjectURL(data);
|
|
|
+ blob = data; // 暂存,后续释放 URL 用
|
|
|
+ } else {
|
|
|
+ throw new Error('Unsupported data type');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 加载图像到 Image 元素
|
|
|
+ const img = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
|
+ const image = new Image();
|
|
|
+ image.onload = () => resolve(image);
|
|
|
+ image.onerror = reject;
|
|
|
+ image.src = imageUrl;
|
|
|
+ // 如果图像来自跨域,可能需要设置 crossOrigin
|
|
|
+ // image.crossOrigin = 'anonymous';
|
|
|
+ });
|
|
|
+
|
|
|
+ // 3. 创建 Canvas 并绘制图像
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
+ canvas.width = img.width;
|
|
|
+ canvas.height = img.height;
|
|
|
+ const ctx = canvas.getContext('2d')!;
|
|
|
+ ctx.drawImage(img, 0, 0);
|
|
|
+
|
|
|
+ // 4. 获取像素数据并处理
|
|
|
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
|
+ const dataArray = imageData.data;
|
|
|
+
|
|
|
+ for (let i = 0; i < dataArray.length; i += 4) {
|
|
|
+ const r = dataArray[i];
|
|
|
+ const g = dataArray[i + 1];
|
|
|
+ const b = dataArray[i + 2];
|
|
|
+
|
|
|
+ // 判断颜色是否接近白色(RGB 都大于 255 - tolerance)
|
|
|
+ if (r > 255 - tolerance && g > 255 - tolerance && b > 255 - tolerance) {
|
|
|
+ dataArray[i + 3] = 0; // 设置 Alpha 为 0(完全透明)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 将修改后的像素放回 Canvas
|
|
|
+ ctx.putImageData(imageData, 0, 0);
|
|
|
+
|
|
|
+ // 6. 将 Canvas 转换为 PNG Blob
|
|
|
+ const outputBlob = await new Promise<Blob>((resolve) =>
|
|
|
+ canvas.toBlob((blob) => resolve(blob!), 'image/png')
|
|
|
+ );
|
|
|
+
|
|
|
+ // 7. 清理对象 URL(如果之前创建过)
|
|
|
+ if (typeof data !== 'string') {
|
|
|
+ URL.revokeObjectURL(imageUrl);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 8. 返回 File 对象
|
|
|
+ return new File([outputBlob], filename, { type: 'image/png' });
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传 File 到 S3,返回公开访问的 URL
|
|
|
+ */
|
|
|
+ const uploadFileToS3 = (file: File): Promise<string> => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ if (typeof window === 'undefined' || !window.AWS) {
|
|
|
+ reject(new Error('AWS SDK not available'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const credentials = {
|
|
|
+ accessKeyId: 'AKIATLPEDU37QV5CHLMH',
|
|
|
+ secretAccessKey: 'Q2SQw37HfolS7yeaR1Ndpy9Jl4E2YZKUuuy2muZR',
|
|
|
+ };
|
|
|
+ window.AWS.config.update(credentials);
|
|
|
+ window.AWS.config.region = 'cn-northwest-1';
|
|
|
+
|
|
|
+ const bucket = new window.AWS.S3({ params: { Bucket: 'ccrb' } });
|
|
|
+ const ext = file.name.split('.').pop() || 'bin';
|
|
|
+ const key = `${file.name.split('.')[0]}_${Date.now()}.${ext}`;
|
|
|
+
|
|
|
+ const params = {
|
|
|
+ Key: "pptto/"+key,
|
|
|
+ ContentType: file.type,
|
|
|
+ Body: file,
|
|
|
+ ACL: 'public-read',
|
|
|
+ };
|
|
|
+ const options = {
|
|
|
+ partSize: 2048 * 1024 * 1024, // 2GB 分片,可酌情调小
|
|
|
+ queueSize: 2,
|
|
|
+ leavePartsOnError: true,
|
|
|
+ };
|
|
|
+
|
|
|
+ bucket
|
|
|
+ .upload(params, options)
|
|
|
+ .promise()
|
|
|
+ .then(data => resolve(data.Location))
|
|
|
+ .catch(err => reject(err));
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ /*
|
|
|
// 导入PPTX文件
|
|
|
const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean }) => {
|
|
|
console.log('导入', files)
|
|
|
@@ -405,13 +558,15 @@ export default () => {
|
|
|
try {
|
|
|
json = await parse(e.target!.result as ArrayBuffer)
|
|
|
}
|
|
|
- catch {
|
|
|
+ catch (error) {
|
|
|
exporting.value = false
|
|
|
+ console.log('导入PPTX文件失败:', error)
|
|
|
message.error('无法正确读取 / 解析该文件')
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- let ratio = 96 / 72
|
|
|
+ let ratio = 96 / 72;
|
|
|
+ //let ratio = 1
|
|
|
const width = json.size.width
|
|
|
|
|
|
if (fixedViewport) ratio = 1000 / width
|
|
|
@@ -460,7 +615,9 @@ export default () => {
|
|
|
}
|
|
|
|
|
|
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
|
|
|
@@ -472,7 +629,7 @@ export default () => {
|
|
|
el.height = el.height * ratio
|
|
|
el.left = el.left * ratio
|
|
|
el.top = el.top * ratio
|
|
|
-
|
|
|
+
|
|
|
if (el.type === 'text') {
|
|
|
const textEl: PPTTextElement = {
|
|
|
type: 'text',
|
|
|
@@ -696,7 +853,7 @@ export default () => {
|
|
|
else if (el.type === 'table') {
|
|
|
const row = el.data.length
|
|
|
const col = el.data[0].length
|
|
|
-
|
|
|
+
|
|
|
const style: TableCellStyle = {
|
|
|
fontname: theme.value.fontName,
|
|
|
color: theme.value.fontColor,
|
|
|
@@ -736,7 +893,7 @@ export default () => {
|
|
|
}
|
|
|
data.push(rowCells)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
const allWidth = el.colWidths.reduce((a, b) => a + b, 0)
|
|
|
const colWidths: number[] = el.colWidths.map(item => item / allWidth)
|
|
|
|
|
|
@@ -752,7 +909,7 @@ export default () => {
|
|
|
const borderWidth = border?.borderWidth || 0
|
|
|
const borderStyle = border?.borderType || 'solid'
|
|
|
const borderColor = border?.borderColor || '#eeece1'
|
|
|
-
|
|
|
+
|
|
|
slide.elements.push({
|
|
|
type: 'table',
|
|
|
id: nanoid(10),
|
|
|
@@ -775,7 +932,7 @@ export default () => {
|
|
|
let labels: string[]
|
|
|
let legends: string[]
|
|
|
let series: number[][]
|
|
|
-
|
|
|
+
|
|
|
if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
|
|
|
labels = el.data[0].map((item, index) => `坐标${index + 1}`)
|
|
|
legends = ['X', 'Y']
|
|
|
@@ -789,7 +946,7 @@ export default () => {
|
|
|
}
|
|
|
|
|
|
const options: ChartOptions = {}
|
|
|
-
|
|
|
+
|
|
|
let chartType: ChartType = 'bar'
|
|
|
|
|
|
switch (el.chartType) {
|
|
|
@@ -825,7 +982,7 @@ export default () => {
|
|
|
break
|
|
|
default:
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
slide.elements.push({
|
|
|
type: 'chart',
|
|
|
id: nanoid(10),
|
|
|
@@ -899,6 +1056,719 @@ export default () => {
|
|
|
}
|
|
|
reader.readAsArrayBuffer(file)
|
|
|
}
|
|
|
+*/
|
|
|
+
|
|
|
+ const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean }) => {
|
|
|
+ console.log('导入', files);
|
|
|
+ const defaultOptions = {
|
|
|
+ cover: false,
|
|
|
+ fixedViewport: false,
|
|
|
+ };
|
|
|
+ const { cover, fixedViewport } = { ...defaultOptions, ...options };
|
|
|
+
|
|
|
+ const file = files[0];
|
|
|
+ if (!file) return;
|
|
|
+
|
|
|
+ exporting.value = true; // 假设 exporting 是一个全局 ref
|
|
|
+
|
|
|
+ // 预加载形状库(用于后续形状匹配)
|
|
|
+ const shapeList: ShapePoolItem[] = [];
|
|
|
+ for (const item of SHAPE_LIST) {
|
|
|
+ shapeList.push(...item.children);
|
|
|
+ }
|
|
|
+
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = async e => {
|
|
|
+ let json = null;
|
|
|
+ try {
|
|
|
+ json = await parse(e.target!.result as ArrayBuffer);
|
|
|
+ } catch (error) {
|
|
|
+ exporting.value = false;
|
|
|
+ console.log('导入PPTX文件失败:', error);
|
|
|
+ message.error('无法正确读取 / 解析该文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算缩放比例
|
|
|
+ let ratio = 96 / 72; // PPTX 默认 72 DPI,屏幕 96 DPI
|
|
|
+ const width = json.size.width;
|
|
|
+ const height = json.size.height;
|
|
|
+ const viewportRatio = json.size.viewportRatio || (height && width ? height / width : undefined)
|
|
|
+ if (fixedViewport) {
|
|
|
+ ratio = 1000 / width; // 固定视口宽度为 1000px
|
|
|
+ } else {
|
|
|
+ slidesStore.setViewportSize(width * ratio); // 调整画布大小
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置主题色
|
|
|
+ slidesStore.setTheme({ themeColors: json.themeColors });
|
|
|
+
|
|
|
+ const slides: Slide[] = [];
|
|
|
+
|
|
|
+ // 收集当前幻灯片内所有上传任务
|
|
|
+ const uploadTasks: Promise<void>[] = [];
|
|
|
+
|
|
|
+ // 遍历每一张幻灯片
|
|
|
+ for (const item of json.slides) {
|
|
|
+ // ----- 解析背景 -----
|
|
|
+ const { type, value } = item.fill;
|
|
|
+ let background: SlideBackground;
|
|
|
+ if (type === 'image') {
|
|
|
+ // 背景图片也可能需要上传(但 PPTX 背景图通常是内嵌 base64)
|
|
|
+ // 这里为了简化,暂不处理背景图片上传,如有需要可类似元素上传
|
|
|
+ background = {
|
|
|
+ type: 'image',
|
|
|
+ image: {
|
|
|
+ src: value.picBase64,
|
|
|
+ size: 'cover',
|
|
|
+ },
|
|
|
+ };
|
|
|
+ } else if (type === 'gradient') {
|
|
|
+ background = {
|
|
|
+ type: 'gradient',
|
|
|
+ gradient: {
|
|
|
+ type: value.path === 'line' ? 'linear' : 'radial',
|
|
|
+ colors: value.colors.map(item => ({
|
|
|
+ ...item,
|
|
|
+ pos: parseInt(item.pos),
|
|
|
+ })),
|
|
|
+ rotate: value.rot + 90,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ background = {
|
|
|
+ type: 'solid',
|
|
|
+ color: value || '#fff',
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const slide: Slide = {
|
|
|
+ id: nanoid(10),
|
|
|
+ elements: [],
|
|
|
+ background,
|
|
|
+ remark: item.note || '',
|
|
|
+ };
|
|
|
+
|
|
|
+ // ----- 解析元素(递归函数)-----
|
|
|
+ const parseElements = async (elements: any[], pelements: any = null) => {
|
|
|
+ // 按绘制顺序排序
|
|
|
+ const sortedElements = elements.sort((a, b) => a.order - b.order);
|
|
|
+ console.log(sortedElements)
|
|
|
+
|
|
|
+ for (const el of sortedElements) {
|
|
|
+ // 保存原始尺寸用于后续可能的路径计算
|
|
|
+ const originWidth = el.width || 1;
|
|
|
+ const originHeight = el.height || 1;
|
|
|
+ const originLeft = el.left;
|
|
|
+ const originTop = el.top;
|
|
|
+ // 保存原始尺寸用于后续可能的路径计算
|
|
|
+ const poriginWidth = pelements?.width;
|
|
|
+ const poriginHeight = pelements?.height;
|
|
|
+ const poriginLeft = pelements?.left;
|
|
|
+ const poriginTop = pelements?.top;
|
|
|
+
|
|
|
+ // 应用缩放
|
|
|
+ el.width = el.width * ratio;
|
|
|
+ el.height = el.height * ratio;
|
|
|
+ el.left = el.left * ratio;
|
|
|
+ el.top = el.top * ratio;
|
|
|
+
|
|
|
+ if (el.type === 'text') {
|
|
|
+ const textEl: PPTTextElement = {
|
|
|
+ type: 'text',
|
|
|
+ id: nanoid(10),
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ rotate: el.rotate,
|
|
|
+ defaultFontName: theme.value.fontName,
|
|
|
+ defaultColor: theme.value.fontColor,
|
|
|
+ content: convertFontSizePtToPx(el.content, ratio),
|
|
|
+ style: getStyle(convertFontSizePtToPx(el.content, ratio)),
|
|
|
+ lineHeight: 1.5,
|
|
|
+ outline: {
|
|
|
+ color: el.borderColor,
|
|
|
+ width: +(el.borderWidth * ratio).toFixed(2),
|
|
|
+ style: el.borderType,
|
|
|
+ },
|
|
|
+ fill: el.fill.type === 'color' ? el.fill.value : '',
|
|
|
+ vertical: el.isVertical,
|
|
|
+ }
|
|
|
+ if (el.shadow) {
|
|
|
+ textEl.shadow = {
|
|
|
+ h: el.shadow.h * ratio,
|
|
|
+ v: el.shadow.v * ratio,
|
|
|
+ blur: el.shadow.blur * ratio,
|
|
|
+ color: el.shadow.color,
|
|
|
+ }
|
|
|
+ }
|
|
|
+ slide.elements.push(textEl)
|
|
|
+ }
|
|
|
+ // ---------- 图片 ----------
|
|
|
+ if (el.type === 'image') {
|
|
|
+ const element: PPTImageElement = {
|
|
|
+ type: 'image',
|
|
|
+ id: nanoid(10),
|
|
|
+ src: el.src, // 可能是 base64 或已有 URL
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ fixedRatio: true,
|
|
|
+ rotate: el.rotate,
|
|
|
+ flipH: el.isFlipH,
|
|
|
+ flipV: el.isFlipV,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 边框
|
|
|
+ if (el.borderWidth) {
|
|
|
+ element.outline = {
|
|
|
+ color: el.borderColor,
|
|
|
+ width: +(el.borderWidth * ratio).toFixed(2),
|
|
|
+ style: el.borderType,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 裁剪(形状剪裁)
|
|
|
+ const clipShapeTypes = ['roundRect', 'ellipse', 'triangle', 'rhombus', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'parallelogram', 'trapezoid'];
|
|
|
+ if (el.rect) {
|
|
|
+ element.clip = {
|
|
|
+ shape: (el.geom && clipShapeTypes.includes(el.geom)) ? el.geom : 'rect',
|
|
|
+ range: [
|
|
|
+ [el.rect.l || 0, el.rect.t || 0],
|
|
|
+ [100 - (el.rect.r || 0), 100 - (el.rect.b || 0)],
|
|
|
+ ],
|
|
|
+ };
|
|
|
+ } else if (el.geom && clipShapeTypes.includes(el.geom)) {
|
|
|
+ element.clip = {
|
|
|
+ shape: el.geom,
|
|
|
+ range: [[0, 0], [100, 100]],
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果 src 是 base64,触发上传
|
|
|
+ if (el.src && typeof el.src === 'string' && el.src.startsWith('data:')) {
|
|
|
+ const uploadTask = (async () => {
|
|
|
+ try {
|
|
|
+ const file = await makeWhiteTransparent(el.src, `image_${Date.now()}.png`);
|
|
|
+ const url = await uploadFileToS3(file);
|
|
|
+ element.src = url; // 替换为远程 URL
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Image upload failed:', error);
|
|
|
+ // 失败时保留原 base64(或可置空)
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ uploadTasks.push(uploadTask);
|
|
|
+ }
|
|
|
+
|
|
|
+ slide.elements.push(element);
|
|
|
+ }
|
|
|
+ else if (el.type === 'math') {
|
|
|
+ const element: PPTImageElement = {
|
|
|
+ type: 'image',
|
|
|
+ id: nanoid(10),
|
|
|
+ src: el.picBase64,
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ fixedRatio: true,
|
|
|
+ rotate: 0,
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果 src 是 base64,触发上传
|
|
|
+ if (el.src && typeof el.src === 'string' && el.src.startsWith('data:')) {
|
|
|
+ const uploadTask = (async () => {
|
|
|
+ try {
|
|
|
+ const file = makeWhiteTransparent(el.src, `image_${Date.now()}.png`, 'image/png');
|
|
|
+ const url = await uploadFileToS3(file);
|
|
|
+ element.src = url; // 替换为远程 URL
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Image upload failed:', error);
|
|
|
+ // 失败时保留原 base64(或可置空)
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ uploadTasks.push(uploadTask);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ slide.elements.push(element)
|
|
|
+
|
|
|
+ }
|
|
|
+ // ---------- 音频 ----------
|
|
|
+ else if (el.type === 'audio') {
|
|
|
+ const element: PPTAudioElement = {
|
|
|
+ type: 'audio',
|
|
|
+ id: nanoid(10),
|
|
|
+ src: el.blob,
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ rotate: 0,
|
|
|
+ fixedRatio: false,
|
|
|
+ color: theme.value.themeColors[0],
|
|
|
+ loop: false,
|
|
|
+ autoplay: false,
|
|
|
+ };
|
|
|
+
|
|
|
+ if (el.blob instanceof Blob) {
|
|
|
+ const uploadTask = (async () => {
|
|
|
+ try {
|
|
|
+ const file = dataToFile(el.blob, `audio_${Date.now()}.mp3`, el.blob.type);
|
|
|
+ const url = await uploadFileToS3(file);
|
|
|
+ element.src = url;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Audio upload failed:', error);
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ uploadTasks.push(uploadTask);
|
|
|
+ }
|
|
|
+
|
|
|
+ slide.elements.push(element);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- 视频 ----------
|
|
|
+ else if (el.type === 'video') {
|
|
|
+ const element: PPTVideoElement = {
|
|
|
+ type: 'video',
|
|
|
+ id: nanoid(10),
|
|
|
+ src: (el.blob || el.src)!,
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ rotate: 0,
|
|
|
+ autoplay: false,
|
|
|
+ };
|
|
|
+
|
|
|
+ const localData = el.blob || (el.src && typeof el.src === 'string' && el.src.startsWith('data:') ? el.src : null);
|
|
|
+ if (localData) {
|
|
|
+ const uploadTask = (async () => {
|
|
|
+ try {
|
|
|
+ let file: File;
|
|
|
+ if (localData instanceof Blob) {
|
|
|
+ file = dataToFile(localData, `video_${Date.now()}.mp4`, localData.type);
|
|
|
+ } else {
|
|
|
+ file = dataToFile(localData, `video_${Date.now()}.mp4`, 'video/mp4');
|
|
|
+ }
|
|
|
+ const url = await uploadFileToS3(file);
|
|
|
+ element.src = url;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Video upload failed:', error);
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ uploadTasks.push(uploadTask);
|
|
|
+ }
|
|
|
+
|
|
|
+ slide.elements.push(element);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // ---------- 形状 ----------
|
|
|
+ else if (el.type === 'shape') {
|
|
|
+ if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
|
|
|
+ // 线条元素(单独处理)
|
|
|
+ const lineElement = parseLineElement(el, ratio);
|
|
|
+ slide.elements.push(lineElement);
|
|
|
+ } else {
|
|
|
+ const shape = shapeList.find(item => item.pptxShapeType === el.shapType);
|
|
|
+
|
|
|
+ const vAlignMap: { [key: string]: ShapeTextAlign } = {
|
|
|
+ mid: 'middle',
|
|
|
+ down: 'bottom',
|
|
|
+ up: 'top',
|
|
|
+ };
|
|
|
+
|
|
|
+ const gradient: Gradient | undefined = el.fill?.type === 'gradient'
|
|
|
+ ? {
|
|
|
+ type: el.fill.value.path === 'line' ? 'linear' : 'radial',
|
|
|
+ colors: el.fill.value.colors.map(item => ({
|
|
|
+ ...item,
|
|
|
+ pos: parseInt(item.pos),
|
|
|
+ })),
|
|
|
+ rotate: el.fill.value.rot,
|
|
|
+ }
|
|
|
+ : undefined;
|
|
|
+
|
|
|
+ const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined;
|
|
|
+ const fill = el.fill?.type === 'color' ? el.fill.value : '';
|
|
|
+
|
|
|
+ const element: PPTShapeElement = {
|
|
|
+ type: 'shape',
|
|
|
+ id: nanoid(10),
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ viewBox: [200, 200],
|
|
|
+ path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
|
|
|
+ fill,
|
|
|
+ gradient,
|
|
|
+ pattern,
|
|
|
+ fixedRatio: false,
|
|
|
+ rotate: el.rotate,
|
|
|
+ outline: {
|
|
|
+ color: el.borderColor,
|
|
|
+ width: +(el.borderWidth * ratio).toFixed(2),
|
|
|
+ style: el.borderType,
|
|
|
+ },
|
|
|
+ text: {
|
|
|
+ content: convertFontSizePtToPx(el.content, ratio),
|
|
|
+ style: getStyle(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;
|
|
|
+ //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');
|
|
|
+ } else {
|
|
|
+ element.special = true;
|
|
|
+ element.path = el.path!;
|
|
|
+ }
|
|
|
+ const { maxX, maxY } = getSvgPathRange(element.path);
|
|
|
+ element.viewBox = [maxX, maxY];
|
|
|
+ //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) => `坐标${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);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 等待当前幻灯片内所有上传任务完成
|
|
|
+ Promise.all(uploadTasks);
|
|
|
+
|
|
|
+ exporting.value = false;
|
|
|
+
|
|
|
+ /*
|
|
|
+ // 更新视口尺寸(如果提供了的话)
|
|
|
+ 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) => {
|
|
|
@@ -986,6 +1856,8 @@ export default () => {
|
|
|
exportJSON2,
|
|
|
exporting,
|
|
|
getFile,
|
|
|
- getFile2
|
|
|
+ getFile2,
|
|
|
+ dataToFile,
|
|
|
+ uploadFileToS3
|
|
|
}
|
|
|
}
|