|
|
@@ -21,6 +21,8 @@ import type {
|
|
|
PPTImageElement,
|
|
|
ShapeTextAlign,
|
|
|
PPTTextElement,
|
|
|
+ PPTVideoElement,
|
|
|
+ PPTAudioElement,
|
|
|
ChartOptions,
|
|
|
Gradient,
|
|
|
} from '@/types/slides'
|
|
|
@@ -382,6 +384,140 @@ export default () => {
|
|
|
return { x: graphicX, y: graphicY }
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 将 base64 字符串或 Blob 转换为 File 对象
|
|
|
+ */
|
|
|
+ const dataToFile = (data: string | Blob, filename: string, mimeType?: string): File => {
|
|
|
+ if (typeof data === 'string') {
|
|
|
+ // 移除可能的 data:image/png;base64, 前缀
|
|
|
+ const base64Data = data.includes('base64,') ? data.split('base64,')[1] : data;
|
|
|
+ const byteCharacters = atob(base64Data);
|
|
|
+ const byteNumbers = new Array(byteCharacters.length);
|
|
|
+ for (let i = 0; i < byteCharacters.length; i++) {
|
|
|
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
|
+ }
|
|
|
+ const byteArray = new Uint8Array(byteNumbers);
|
|
|
+ const blob = new Blob([byteArray], { type: mimeType || 'image/png' });
|
|
|
+ return new File([blob], filename, { type: blob.type });
|
|
|
+ } else if (data instanceof Blob) {
|
|
|
+ return new File([data], filename, { type: data.type });
|
|
|
+ }
|
|
|
+ throw new Error('Unsupported data type');
|
|
|
+ };
|
|
|
+
|
|
|
+ const makeWhiteTransparent = async (
|
|
|
+ data: string | Blob,
|
|
|
+ filename: string,
|
|
|
+ options?: { tolerance?: number }
|
|
|
+ ): Promise<File> => {
|
|
|
+ const tolerance = options?.tolerance ?? 30; // 容差值,控制哪些颜色被视为白色
|
|
|
+
|
|
|
+ // 1. 将输入数据统一为 Blob 或可直接用于加载的 URL
|
|
|
+ let imageUrl: string;
|
|
|
+ let blob: Blob;
|
|
|
+
|
|
|
+ if (typeof data === 'string') {
|
|
|
+ // 如果是 Base64,直接用作 src(data URL)
|
|
|
+ imageUrl = data.startsWith('data:') ? data : `data:image/png;base64,${data}`;
|
|
|
+ } else if (data instanceof Blob) {
|
|
|
+ // 如果是 Blob,创建对象 URL
|
|
|
+ imageUrl = URL.createObjectURL(data);
|
|
|
+ blob = data; // 暂存,后续释放 URL 用
|
|
|
+ } else {
|
|
|
+ throw new Error('Unsupported data type');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 加载图像到 Image 元素
|
|
|
+ const img = await new Promise<HTMLImageElement>((resolve, reject) => {
|
|
|
+ const image = new Image();
|
|
|
+ image.onload = () => resolve(image);
|
|
|
+ image.onerror = reject;
|
|
|
+ image.src = imageUrl;
|
|
|
+ // 如果图像来自跨域,可能需要设置 crossOrigin
|
|
|
+ // image.crossOrigin = 'anonymous';
|
|
|
+ });
|
|
|
+
|
|
|
+ // 3. 创建 Canvas 并绘制图像
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
+ canvas.width = img.width;
|
|
|
+ canvas.height = img.height;
|
|
|
+ const ctx = canvas.getContext('2d')!;
|
|
|
+ ctx.drawImage(img, 0, 0);
|
|
|
+
|
|
|
+ // 4. 获取像素数据并处理
|
|
|
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
|
+ const dataArray = imageData.data;
|
|
|
+
|
|
|
+ for (let i = 0; i < dataArray.length; i += 4) {
|
|
|
+ const r = dataArray[i];
|
|
|
+ const g = dataArray[i + 1];
|
|
|
+ const b = dataArray[i + 2];
|
|
|
+
|
|
|
+ // 判断颜色是否接近白色(RGB 都大于 255 - tolerance)
|
|
|
+ if (r > 255 - tolerance && g > 255 - tolerance && b > 255 - tolerance) {
|
|
|
+ dataArray[i + 3] = 0; // 设置 Alpha 为 0(完全透明)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 将修改后的像素放回 Canvas
|
|
|
+ ctx.putImageData(imageData, 0, 0);
|
|
|
+
|
|
|
+ // 6. 将 Canvas 转换为 PNG Blob
|
|
|
+ const outputBlob = await new Promise<Blob>((resolve) =>
|
|
|
+ canvas.toBlob((blob) => resolve(blob!), 'image/png')
|
|
|
+ );
|
|
|
+
|
|
|
+ // 7. 清理对象 URL(如果之前创建过)
|
|
|
+ if (typeof data !== 'string') {
|
|
|
+ URL.revokeObjectURL(imageUrl);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 8. 返回 File 对象
|
|
|
+ return new File([outputBlob], filename, { type: 'image/png' });
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传 File 到 S3,返回公开访问的 URL
|
|
|
+ */
|
|
|
+ const uploadFileToS3 = (file: File): Promise<string> => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ if (typeof window === 'undefined' || !window.AWS) {
|
|
|
+ reject(new Error('AWS SDK not available'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const credentials = {
|
|
|
+ accessKeyId: 'AKIATLPEDU37QV5CHLMH',
|
|
|
+ secretAccessKey: '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)
|
|
|
@@ -478,7 +614,7 @@ export default () => {
|
|
|
el.height = el.height * ratio
|
|
|
el.left = el.left * ratio
|
|
|
el.top = el.top * ratio
|
|
|
-
|
|
|
+
|
|
|
if (el.type === 'text') {
|
|
|
const textEl: PPTTextElement = {
|
|
|
type: 'text',
|
|
|
@@ -702,7 +838,7 @@ export default () => {
|
|
|
else if (el.type === 'table') {
|
|
|
const row = el.data.length
|
|
|
const col = el.data[0].length
|
|
|
-
|
|
|
+
|
|
|
const style: TableCellStyle = {
|
|
|
fontname: theme.value.fontName,
|
|
|
color: theme.value.fontColor,
|
|
|
@@ -742,7 +878,7 @@ export default () => {
|
|
|
}
|
|
|
data.push(rowCells)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
const allWidth = el.colWidths.reduce((a, b) => a + b, 0)
|
|
|
const colWidths: number[] = el.colWidths.map(item => item / allWidth)
|
|
|
|
|
|
@@ -758,7 +894,7 @@ export default () => {
|
|
|
const borderWidth = border?.borderWidth || 0
|
|
|
const borderStyle = border?.borderType || 'solid'
|
|
|
const borderColor = border?.borderColor || '#eeece1'
|
|
|
-
|
|
|
+
|
|
|
slide.elements.push({
|
|
|
type: 'table',
|
|
|
id: nanoid(10),
|
|
|
@@ -781,7 +917,7 @@ export default () => {
|
|
|
let labels: string[]
|
|
|
let legends: string[]
|
|
|
let series: number[][]
|
|
|
-
|
|
|
+
|
|
|
if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
|
|
|
labels = el.data[0].map((item, index) => `坐标${index + 1}`)
|
|
|
legends = ['X', 'Y']
|
|
|
@@ -795,7 +931,7 @@ export default () => {
|
|
|
}
|
|
|
|
|
|
const options: ChartOptions = {}
|
|
|
-
|
|
|
+
|
|
|
let chartType: ChartType = 'bar'
|
|
|
|
|
|
switch (el.chartType) {
|
|
|
@@ -831,7 +967,7 @@ export default () => {
|
|
|
break
|
|
|
default:
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
slide.elements.push({
|
|
|
type: 'chart',
|
|
|
id: nanoid(10),
|
|
|
@@ -905,6 +1041,641 @@ export default () => {
|
|
|
}
|
|
|
reader.readAsArrayBuffer(file)
|
|
|
}
|
|
|
+*/
|
|
|
+
|
|
|
+ const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean }) => {
|
|
|
+ console.log('导入', files);
|
|
|
+ const defaultOptions = {
|
|
|
+ cover: false,
|
|
|
+ fixedViewport: false,
|
|
|
+ };
|
|
|
+ const { cover, fixedViewport } = { ...defaultOptions, ...options };
|
|
|
+
|
|
|
+ const file = files[0];
|
|
|
+ if (!file) return;
|
|
|
+
|
|
|
+ exporting.value = true; // 假设 exporting 是一个全局 ref
|
|
|
+
|
|
|
+ // 预加载形状库(用于后续形状匹配)
|
|
|
+ const shapeList: ShapePoolItem[] = [];
|
|
|
+ for (const item of SHAPE_LIST) {
|
|
|
+ shapeList.push(...item.children);
|
|
|
+ }
|
|
|
+
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = async e => {
|
|
|
+ let json = null;
|
|
|
+ try {
|
|
|
+ json = await parse(e.target!.result as ArrayBuffer);
|
|
|
+ } catch (error) {
|
|
|
+ exporting.value = false;
|
|
|
+ console.log('导入PPTX文件失败:', error);
|
|
|
+ message.error('无法正确读取 / 解析该文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算缩放比例
|
|
|
+ let ratio = 96 / 72; // PPTX 默认 72 DPI,屏幕 96 DPI
|
|
|
+ const width = json.size.width;
|
|
|
+ if (fixedViewport) {
|
|
|
+ ratio = 1000 / width; // 固定视口宽度为 1000px
|
|
|
+ } else {
|
|
|
+ slidesStore.setViewportSize(width * ratio); // 调整画布大小
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置主题色
|
|
|
+ slidesStore.setTheme({ themeColors: json.themeColors });
|
|
|
+
|
|
|
+ const slides: Slide[] = [];
|
|
|
+
|
|
|
+ // 收集当前幻灯片内所有上传任务
|
|
|
+ const uploadTasks: Promise<void>[] = [];
|
|
|
+
|
|
|
+ // 遍历每一张幻灯片
|
|
|
+ for (const item of json.slides) {
|
|
|
+ // ----- 解析背景 -----
|
|
|
+ const { type, value } = item.fill;
|
|
|
+ let background: SlideBackground;
|
|
|
+ if (type === 'image') {
|
|
|
+ // 背景图片也可能需要上传(但 PPTX 背景图通常是内嵌 base64)
|
|
|
+ // 这里为了简化,暂不处理背景图片上传,如有需要可类似元素上传
|
|
|
+ background = {
|
|
|
+ type: 'image',
|
|
|
+ image: {
|
|
|
+ src: value.picBase64,
|
|
|
+ size: 'cover',
|
|
|
+ },
|
|
|
+ };
|
|
|
+ } else if (type === 'gradient') {
|
|
|
+ background = {
|
|
|
+ type: 'gradient',
|
|
|
+ gradient: {
|
|
|
+ type: value.path === 'line' ? 'linear' : 'radial',
|
|
|
+ colors: value.colors.map(item => ({
|
|
|
+ ...item,
|
|
|
+ pos: parseInt(item.pos),
|
|
|
+ })),
|
|
|
+ rotate: value.rot + 90,
|
|
|
+ },
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ background = {
|
|
|
+ type: 'solid',
|
|
|
+ color: value || '#fff',
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const slide: Slide = {
|
|
|
+ id: nanoid(10),
|
|
|
+ elements: [],
|
|
|
+ background,
|
|
|
+ remark: item.note || '',
|
|
|
+ };
|
|
|
+
|
|
|
+ // ----- 解析元素(递归函数)-----
|
|
|
+ const parseElements = async (elements: any[]) => {
|
|
|
+ // 按绘制顺序排序
|
|
|
+ const sortedElements = elements.sort((a, b) => a.order - b.order);
|
|
|
+
|
|
|
+ for (const el of sortedElements) {
|
|
|
+ // 保存原始尺寸用于后续可能的路径计算
|
|
|
+ const originWidth = el.width || 1;
|
|
|
+ const originHeight = el.height || 1;
|
|
|
+ const originLeft = el.left;
|
|
|
+ const originTop = el.top;
|
|
|
+
|
|
|
+ // 应用缩放
|
|
|
+ el.width = el.width * ratio;
|
|
|
+ el.height = el.height * ratio;
|
|
|
+ el.left = el.left * ratio;
|
|
|
+ el.top = el.top * ratio;
|
|
|
+
|
|
|
+ if (el.type === 'text') {
|
|
|
+ const textEl: PPTTextElement = {
|
|
|
+ type: 'text',
|
|
|
+ id: nanoid(10),
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ rotate: el.rotate,
|
|
|
+ defaultFontName: theme.value.fontName,
|
|
|
+ defaultColor: theme.value.fontColor,
|
|
|
+ content: convertFontSizePtToPx(el.content, ratio),
|
|
|
+ lineHeight: 1,
|
|
|
+ outline: {
|
|
|
+ color: el.borderColor,
|
|
|
+ width: +(el.borderWidth * ratio).toFixed(2),
|
|
|
+ style: el.borderType,
|
|
|
+ },
|
|
|
+ fill: el.fill.type === 'color' ? el.fill.value : '',
|
|
|
+ vertical: el.isVertical,
|
|
|
+ }
|
|
|
+ if (el.shadow) {
|
|
|
+ textEl.shadow = {
|
|
|
+ h: el.shadow.h * ratio,
|
|
|
+ v: el.shadow.v * ratio,
|
|
|
+ blur: el.shadow.blur * ratio,
|
|
|
+ color: el.shadow.color,
|
|
|
+ }
|
|
|
+ }
|
|
|
+ slide.elements.push(textEl)
|
|
|
+ }
|
|
|
+ // ---------- 图片 ----------
|
|
|
+ if (el.type === 'image') {
|
|
|
+ const element: PPTImageElement = {
|
|
|
+ type: 'image',
|
|
|
+ id: nanoid(10),
|
|
|
+ src: el.src, // 可能是 base64 或已有 URL
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ fixedRatio: true,
|
|
|
+ rotate: el.rotate,
|
|
|
+ flipH: el.isFlipH,
|
|
|
+ flipV: el.isFlipV,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 边框
|
|
|
+ if (el.borderWidth) {
|
|
|
+ element.outline = {
|
|
|
+ color: el.borderColor,
|
|
|
+ width: +(el.borderWidth * ratio).toFixed(2),
|
|
|
+ style: el.borderType,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 裁剪(形状剪裁)
|
|
|
+ const clipShapeTypes = ['roundRect', 'ellipse', 'triangle', 'rhombus', 'pentagon', 'hexagon', 'heptagon', 'octagon', 'parallelogram', 'trapezoid'];
|
|
|
+ if (el.rect) {
|
|
|
+ element.clip = {
|
|
|
+ shape: (el.geom && clipShapeTypes.includes(el.geom)) ? el.geom : 'rect',
|
|
|
+ range: [
|
|
|
+ [el.rect.l || 0, el.rect.t || 0],
|
|
|
+ [100 - (el.rect.r || 0), 100 - (el.rect.b || 0)],
|
|
|
+ ],
|
|
|
+ };
|
|
|
+ } else if (el.geom && clipShapeTypes.includes(el.geom)) {
|
|
|
+ element.clip = {
|
|
|
+ shape: el.geom,
|
|
|
+ range: [[0, 0], [100, 100]],
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果 src 是 base64,触发上传
|
|
|
+ if (el.src && typeof el.src === 'string' && el.src.startsWith('data:')) {
|
|
|
+ const uploadTask = (async () => {
|
|
|
+ try {
|
|
|
+ const file = await makeWhiteTransparent(el.src, `image_${Date.now()}.png`);
|
|
|
+ const url = await uploadFileToS3(file);
|
|
|
+ element.src = url; // 替换为远程 URL
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Image upload failed:', error);
|
|
|
+ // 失败时保留原 base64(或可置空)
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ uploadTasks.push(uploadTask);
|
|
|
+ }
|
|
|
+
|
|
|
+ slide.elements.push(element);
|
|
|
+ }
|
|
|
+ else if (el.type === 'math') {
|
|
|
+ const element: PPTImageElement = {
|
|
|
+ type: 'image',
|
|
|
+ id: nanoid(10),
|
|
|
+ src: el.picBase64,
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ fixedRatio: true,
|
|
|
+ rotate: 0,
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果 src 是 base64,触发上传
|
|
|
+ if (el.src && typeof el.src === 'string' && el.src.startsWith('data:')) {
|
|
|
+ const uploadTask = (async () => {
|
|
|
+ try {
|
|
|
+ const file = makeWhiteTransparent(el.src, `image_${Date.now()}.png`, 'image/png');
|
|
|
+ const url = await uploadFileToS3(file);
|
|
|
+ element.src = url; // 替换为远程 URL
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Image upload failed:', error);
|
|
|
+ // 失败时保留原 base64(或可置空)
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ uploadTasks.push(uploadTask);
|
|
|
+ }
|
|
|
+
|
|
|
+ slide.elements.push(element)
|
|
|
+
|
|
|
+ }
|
|
|
+ // ---------- 音频 ----------
|
|
|
+ else if (el.type === 'audio') {
|
|
|
+ const element: PPTAudioElement = {
|
|
|
+ type: 'audio',
|
|
|
+ id: nanoid(10),
|
|
|
+ src: el.blob,
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ rotate: 0,
|
|
|
+ fixedRatio: false,
|
|
|
+ color: theme.value.themeColors[0],
|
|
|
+ loop: false,
|
|
|
+ autoplay: false,
|
|
|
+ };
|
|
|
+
|
|
|
+ if (el.blob instanceof Blob) {
|
|
|
+ const uploadTask = (async () => {
|
|
|
+ try {
|
|
|
+ const file = dataToFile(el.blob, `audio_${Date.now()}.mp3`, el.blob.type);
|
|
|
+ const url = await uploadFileToS3(file);
|
|
|
+ element.src = url;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Audio upload failed:', error);
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ uploadTasks.push(uploadTask);
|
|
|
+ }
|
|
|
+
|
|
|
+ slide.elements.push(element);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- 视频 ----------
|
|
|
+ else if (el.type === 'video') {
|
|
|
+ const element: PPTVideoElement = {
|
|
|
+ type: 'video',
|
|
|
+ id: nanoid(10),
|
|
|
+ src: (el.blob || el.src)!,
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ rotate: 0,
|
|
|
+ autoplay: false,
|
|
|
+ };
|
|
|
+
|
|
|
+ const localData = el.blob || (el.src && typeof el.src === 'string' && el.src.startsWith('data:') ? el.src : null);
|
|
|
+ if (localData) {
|
|
|
+ const uploadTask = (async () => {
|
|
|
+ try {
|
|
|
+ let file: File;
|
|
|
+ if (localData instanceof Blob) {
|
|
|
+ file = dataToFile(localData, `video_${Date.now()}.mp4`, localData.type);
|
|
|
+ } else {
|
|
|
+ file = dataToFile(localData, `video_${Date.now()}.mp4`, 'video/mp4');
|
|
|
+ }
|
|
|
+ const url = await uploadFileToS3(file);
|
|
|
+ element.src = url;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Video upload failed:', error);
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ uploadTasks.push(uploadTask);
|
|
|
+ }
|
|
|
+
|
|
|
+ slide.elements.push(element);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- 形状 ----------
|
|
|
+ else if (el.type === 'shape') {
|
|
|
+ if (el.shapType === 'line' || /Connector/.test(el.shapType)) {
|
|
|
+ // 线条元素(单独处理)
|
|
|
+ const lineElement = parseLineElement(el, ratio);
|
|
|
+ slide.elements.push(lineElement);
|
|
|
+ } else {
|
|
|
+ const shape = shapeList.find(item => item.pptxShapeType === el.shapType);
|
|
|
+
|
|
|
+ const vAlignMap: { [key: string]: ShapeTextAlign } = {
|
|
|
+ mid: 'middle',
|
|
|
+ down: 'bottom',
|
|
|
+ up: 'top',
|
|
|
+ };
|
|
|
+
|
|
|
+ const gradient: Gradient | undefined = el.fill?.type === 'gradient'
|
|
|
+ ? {
|
|
|
+ type: el.fill.value.path === 'line' ? 'linear' : 'radial',
|
|
|
+ colors: el.fill.value.colors.map(item => ({
|
|
|
+ ...item,
|
|
|
+ pos: parseInt(item.pos),
|
|
|
+ })),
|
|
|
+ rotate: el.fill.value.rot,
|
|
|
+ }
|
|
|
+ : undefined;
|
|
|
+
|
|
|
+ const pattern: string | undefined = el.fill?.type === 'image' ? el.fill.value.picBase64 : undefined;
|
|
|
+ const fill = el.fill?.type === 'color' ? el.fill.value : '';
|
|
|
+
|
|
|
+ const element: PPTShapeElement = {
|
|
|
+ type: 'shape',
|
|
|
+ id: nanoid(10),
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ viewBox: [200, 200],
|
|
|
+ path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
|
|
|
+ fill,
|
|
|
+ gradient,
|
|
|
+ pattern,
|
|
|
+ fixedRatio: false,
|
|
|
+ rotate: el.rotate,
|
|
|
+ outline: {
|
|
|
+ color: el.borderColor,
|
|
|
+ width: +(el.borderWidth * ratio).toFixed(2),
|
|
|
+ style: el.borderType,
|
|
|
+ },
|
|
|
+ text: {
|
|
|
+ content: convertFontSizePtToPx(el.content, ratio),
|
|
|
+ defaultFontName: theme.value.fontName,
|
|
|
+ defaultColor: theme.value.fontColor,
|
|
|
+ align: vAlignMap[el.vAlign] || 'middle',
|
|
|
+ },
|
|
|
+ flipH: el.isFlipH,
|
|
|
+ flipV: el.isFlipV,
|
|
|
+ };
|
|
|
+
|
|
|
+ if (el.shadow) {
|
|
|
+ element.shadow = {
|
|
|
+ h: el.shadow.h * ratio,
|
|
|
+ v: el.shadow.v * ratio,
|
|
|
+ blur: el.shadow.blur * ratio,
|
|
|
+ color: el.shadow.color,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (shape) {
|
|
|
+ element.path = shape.path;
|
|
|
+ element.viewBox = shape.viewBox;
|
|
|
+
|
|
|
+ if (shape.pathFormula) {
|
|
|
+ element.pathFormula = shape.pathFormula;
|
|
|
+ element.viewBox = [el.width, el.height];
|
|
|
+
|
|
|
+ const pathFormula = SHAPE_PATH_FORMULAS[shape.pathFormula];
|
|
|
+ if ('editable' in pathFormula && pathFormula.editable) {
|
|
|
+ element.path = pathFormula.formula(el.width, el.height, pathFormula.defaultValue);
|
|
|
+ element.keypoints = pathFormula.defaultValue;
|
|
|
+ } else {
|
|
|
+ element.path = pathFormula.formula(el.width, el.height);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (el.path && el.path.indexOf('NaN') === -1) {
|
|
|
+ const { maxX, maxY } = getSvgPathRange(el.path);
|
|
|
+ element.path = el.path;
|
|
|
+ element.viewBox = [maxX || originWidth, maxY || originHeight];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (el.shapType === 'custom') {
|
|
|
+ if (el.path!.indexOf('NaN') !== -1) {
|
|
|
+ if (element.width === 0) element.width = 0.1;
|
|
|
+ if (element.height === 0) element.height = 0.1;
|
|
|
+ element.path = el.path!.replace(/NaN/g, '0');
|
|
|
+ } else {
|
|
|
+ element.special = true;
|
|
|
+ element.path = el.path!;
|
|
|
+ }
|
|
|
+ const { maxX, maxY } = getSvgPathRange(element.path);
|
|
|
+ element.viewBox = [maxX || originWidth, maxY || originHeight];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (element.path) slide.elements.push(element);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- 表格 ----------
|
|
|
+ else if (el.type === 'table') {
|
|
|
+ const row = el.data.length;
|
|
|
+ const col = el.data[0].length;
|
|
|
+
|
|
|
+ const style: TableCellStyle = {
|
|
|
+ fontname: theme.value.fontName,
|
|
|
+ color: theme.value.fontColor,
|
|
|
+ };
|
|
|
+ const data: TableCell[][] = [];
|
|
|
+
|
|
|
+ for (let i = 0; i < row; i++) {
|
|
|
+ const rowCells: TableCell[] = [];
|
|
|
+ for (let j = 0; j < col; j++) {
|
|
|
+ const cellData = el.data[i][j];
|
|
|
+
|
|
|
+ let textDiv: HTMLDivElement | null = document.createElement('div');
|
|
|
+ textDiv.innerHTML = cellData.text;
|
|
|
+ const p = textDiv.querySelector('p');
|
|
|
+ const align = p?.style.textAlign || 'left';
|
|
|
+
|
|
|
+ const span = textDiv.querySelector('span');
|
|
|
+ const fontsize = span?.style.fontSize
|
|
|
+ ? (parseInt(span?.style.fontSize) * ratio).toFixed(1) + 'px'
|
|
|
+ : '';
|
|
|
+ const fontname = span?.style.fontFamily || '';
|
|
|
+ const color = span?.style.color || cellData.fontColor;
|
|
|
+
|
|
|
+ rowCells.push({
|
|
|
+ id: nanoid(10),
|
|
|
+ colspan: cellData.colSpan || 1,
|
|
|
+ rowspan: cellData.rowSpan || 1,
|
|
|
+ text: textDiv.innerText,
|
|
|
+ style: {
|
|
|
+ ...style,
|
|
|
+ align: ['left', 'right', 'center'].includes(align) ? (align as 'left' | 'right' | 'center') : 'left',
|
|
|
+ fontsize,
|
|
|
+ fontname,
|
|
|
+ color,
|
|
|
+ bold: cellData.fontBold,
|
|
|
+ backcolor: cellData.fillColor,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ textDiv = null;
|
|
|
+ }
|
|
|
+ data.push(rowCells);
|
|
|
+ }
|
|
|
+
|
|
|
+ const allWidth = el.colWidths.reduce((a, b) => a + b, 0);
|
|
|
+ const colWidths: number[] = el.colWidths.map(item => item / allWidth);
|
|
|
+
|
|
|
+ const firstCell = el.data[0][0];
|
|
|
+ const border = firstCell.borders.top ||
|
|
|
+ firstCell.borders.bottom ||
|
|
|
+ el.borders.top ||
|
|
|
+ el.borders.bottom ||
|
|
|
+ firstCell.borders.left ||
|
|
|
+ firstCell.borders.right ||
|
|
|
+ el.borders.left ||
|
|
|
+ el.borders.right;
|
|
|
+ const borderWidth = border?.borderWidth || 0;
|
|
|
+ const borderStyle = border?.borderType || 'solid';
|
|
|
+ const borderColor = border?.borderColor || '#eeece1';
|
|
|
+
|
|
|
+ slide.elements.push({
|
|
|
+ type: 'table',
|
|
|
+ id: nanoid(10),
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ colWidths,
|
|
|
+ rotate: 0,
|
|
|
+ data,
|
|
|
+ outline: {
|
|
|
+ width: +(borderWidth * ratio || 2).toFixed(2),
|
|
|
+ style: borderStyle,
|
|
|
+ color: borderColor,
|
|
|
+ },
|
|
|
+ cellMinHeight: el.rowHeights[0] ? el.rowHeights[0] * ratio : 36,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- 图表 ----------
|
|
|
+ else if (el.type === 'chart') {
|
|
|
+ let labels: string[];
|
|
|
+ let legends: string[];
|
|
|
+ let series: number[][];
|
|
|
+
|
|
|
+ if (el.chartType === 'scatterChart' || el.chartType === 'bubbleChart') {
|
|
|
+ labels = el.data[0].map((_, index) => `坐标${index + 1}`);
|
|
|
+ legends = ['X', 'Y'];
|
|
|
+ series = el.data;
|
|
|
+ } else {
|
|
|
+ const data = el.data as ChartItem[];
|
|
|
+ labels = Object.values(data[0].xlabels);
|
|
|
+ legends = data.map(item => item.key);
|
|
|
+ series = data.map(item => item.values.map(v => v.y));
|
|
|
+ }
|
|
|
+
|
|
|
+ const options: ChartOptions = {};
|
|
|
+
|
|
|
+ let chartType: ChartType = 'bar';
|
|
|
+ switch (el.chartType) {
|
|
|
+ case 'barChart':
|
|
|
+ case 'bar3DChart':
|
|
|
+ chartType = 'bar';
|
|
|
+ if (el.barDir === 'bar') chartType = 'column';
|
|
|
+ if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true;
|
|
|
+ break;
|
|
|
+ case 'lineChart':
|
|
|
+ case 'line3DChart':
|
|
|
+ if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true;
|
|
|
+ chartType = 'line';
|
|
|
+ break;
|
|
|
+ case 'areaChart':
|
|
|
+ case 'area3DChart':
|
|
|
+ if (el.grouping === 'stacked' || el.grouping === 'percentStacked') options.stack = true;
|
|
|
+ chartType = 'area';
|
|
|
+ break;
|
|
|
+ case 'scatterChart':
|
|
|
+ case 'bubbleChart':
|
|
|
+ chartType = 'scatter';
|
|
|
+ break;
|
|
|
+ case 'pieChart':
|
|
|
+ case 'pie3DChart':
|
|
|
+ chartType = 'pie';
|
|
|
+ break;
|
|
|
+ case 'radarChart':
|
|
|
+ chartType = 'radar';
|
|
|
+ break;
|
|
|
+ case 'doughnutChart':
|
|
|
+ chartType = 'ring';
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ }
|
|
|
+
|
|
|
+ slide.elements.push({
|
|
|
+ type: 'chart',
|
|
|
+ id: nanoid(10),
|
|
|
+ chartType,
|
|
|
+ width: el.width,
|
|
|
+ height: el.height,
|
|
|
+ left: el.left,
|
|
|
+ top: el.top,
|
|
|
+ rotate: 0,
|
|
|
+ themeColors: el.colors.length ? el.colors : theme.value.themeColors,
|
|
|
+ textColor: theme.value.fontColor,
|
|
|
+ data: {
|
|
|
+ labels,
|
|
|
+ legends,
|
|
|
+ series,
|
|
|
+ },
|
|
|
+ options,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- 组合 ----------
|
|
|
+ else if (el.type === 'group') {
|
|
|
+ // 先将子元素坐标转换到画布绝对坐标
|
|
|
+ let elements: BaseElement[] = el.elements.map((_el: any) => {
|
|
|
+ let left = _el.left + originLeft;
|
|
|
+ let top = _el.top + originTop;
|
|
|
+
|
|
|
+ if (el.rotate) {
|
|
|
+ const { x, y } = calculateRotatedPosition(
|
|
|
+ originLeft, originTop, originWidth, originHeight,
|
|
|
+ _el.left, _el.top, el.rotate
|
|
|
+ );
|
|
|
+ left = x;
|
|
|
+ top = y;
|
|
|
+ }
|
|
|
+
|
|
|
+ const element = {
|
|
|
+ ..._el,
|
|
|
+ left,
|
|
|
+ top,
|
|
|
+ };
|
|
|
+ if (el.isFlipH && 'isFlipH' in element) element.isFlipH = true;
|
|
|
+ if (el.isFlipV && 'isFlipV' in element) element.isFlipV = true;
|
|
|
+
|
|
|
+ return element;
|
|
|
+ });
|
|
|
+
|
|
|
+ if (el.isFlipH) elements = flipGroupElements(elements, 'y');
|
|
|
+ if (el.isFlipV) elements = flipGroupElements(elements, 'x');
|
|
|
+
|
|
|
+ // 递归解析子元素(注意:子元素的上传任务会加入同一个 uploadTasks 数组)
|
|
|
+ await parseElements(elements);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------- 图表组合(SmartArt)----------
|
|
|
+ else if (el.type === 'diagram') {
|
|
|
+ const elements = el.elements.map((_el: any) => ({
|
|
|
+ ..._el,
|
|
|
+ left: _el.left + originLeft,
|
|
|
+ top: _el.top + originTop,
|
|
|
+ }));
|
|
|
+ await parseElements(elements);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 开始解析当前幻灯片的所有元素(包括布局元素)
|
|
|
+ await parseElements([...item.elements, ...item.layoutElements]);
|
|
|
+
|
|
|
+ // 幻灯片构建完成,加入数组
|
|
|
+ slides.push(slide);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据选项将幻灯片插入 store
|
|
|
+ if (cover) {
|
|
|
+ slidesStore.updateSlideIndex(0);
|
|
|
+ slidesStore.setSlides(slides);
|
|
|
+ addHistorySnapshot();
|
|
|
+ } else if (isEmptySlide.value) {
|
|
|
+ slidesStore.setSlides(slides);
|
|
|
+ addHistorySnapshot();
|
|
|
+ } else {
|
|
|
+ addSlidesFromData(slides);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 等待当前幻灯片内所有上传任务完成
|
|
|
+ Promise.all(uploadTasks);
|
|
|
+
|
|
|
+ exporting.value = false;
|
|
|
+ };
|
|
|
+
|
|
|
+ reader.readAsArrayBuffer(file);
|
|
|
+ };
|
|
|
|
|
|
const getFile = (url: string): Promise<{ data: any }> => {
|
|
|
return new Promise((resolve, reject) => {
|
|
|
@@ -992,6 +1763,8 @@ export default () => {
|
|
|
exportJSON2,
|
|
|
exporting,
|
|
|
getFile,
|
|
|
- getFile2
|
|
|
+ getFile2,
|
|
|
+ dataToFile,
|
|
|
+ uploadFileToS3
|
|
|
}
|
|
|
}
|