Upload useExport.ts
Browse files- frontend/src/hooks/useExport.ts +117 -4
frontend/src/hooks/useExport.ts
CHANGED
@@ -14,6 +14,8 @@ import { encrypt } from '@/utils/crypto'
|
|
14 |
import { svg2Base64 } from '@/utils/svg2Base64'
|
15 |
import { renderElementToBase64, isCanvasRenderSupported, getElementDimensions } from '@/utils/canvasRenderer'
|
16 |
import { renderWithHuggingfaceFix, isHuggingfaceEnvironment } from '@/utils/huggingfaceRenderer'
|
|
|
|
|
17 |
import message from '@/utils/message'
|
18 |
|
19 |
interface ExportImageConfig {
|
@@ -37,19 +39,68 @@ export default () => {
|
|
37 |
|
38 |
const exporting = ref(false)
|
39 |
|
40 |
-
//
|
41 |
const exportImage = (domRef: HTMLElement, format: string, quality: number, ignoreWebfont = true) => {
|
42 |
exporting.value = true
|
43 |
|
44 |
-
//
|
45 |
-
const isHF =
|
46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
|
48 |
const foreignObjectSpans = domRef.querySelectorAll('foreignObject [xmlns]')
|
49 |
foreignObjectSpans.forEach(spanRef => spanRef.removeAttribute('xmlns'))
|
50 |
|
51 |
setTimeout(async () => {
|
52 |
try {
|
|
|
|
|
|
|
53 |
let dataUrl: string
|
54 |
|
55 |
if (isHF) {
|
@@ -509,6 +560,46 @@ export default () => {
|
|
509 |
}
|
510 |
|
511 |
// 格式化元素
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
512 |
const formatElement = (element: any) => {
|
513 |
const baseStyle = `
|
514 |
left: ${element.left || 0}px;
|
@@ -538,6 +629,28 @@ export default () => {
|
|
538 |
}
|
539 |
|
540 |
if (element.type === 'shape') {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
541 |
const shapeStyle = `
|
542 |
background: ${element.fill || '#ffffff'};
|
543 |
border: ${element.outline?.width || 0}px solid ${element.outline?.color || '#000000'};
|
|
|
14 |
import { svg2Base64 } from '@/utils/svg2Base64'
|
15 |
import { renderElementToBase64, isCanvasRenderSupported, getElementDimensions } from '@/utils/canvasRenderer'
|
16 |
import { renderWithHuggingfaceFix, isHuggingfaceEnvironment } from '@/utils/huggingfaceRenderer'
|
17 |
+
import { vectorRenderManager, RenderStrategy } from '@/utils/VectorRenderManager'
|
18 |
+
import { VECTOR_EXPORT_CONFIG } from '@/config/vectorExportConfig'
|
19 |
import message from '@/utils/message'
|
20 |
|
21 |
interface ExportImageConfig {
|
|
|
39 |
|
40 |
const exporting = ref(false)
|
41 |
|
42 |
+
// 导出图片(生产环境优化版本)
|
43 |
const exportImage = (domRef: HTMLElement, format: string, quality: number, ignoreWebfont = true) => {
|
44 |
exporting.value = true
|
45 |
|
46 |
+
// 环境检测
|
47 |
+
const isHF = VECTOR_EXPORT_CONFIG.ENVIRONMENT.isHuggingface()
|
48 |
+
const isProd = VECTOR_EXPORT_CONFIG.ENVIRONMENT.isProduction()
|
49 |
+
|
50 |
+
if (!isProd) {
|
51 |
+
console.log('exportImage: Environment check:', { isHuggingface: isHF, format, production: isProd })
|
52 |
+
}
|
53 |
+
|
54 |
+
// 预处理矢量图形元素
|
55 |
+
const preprocessVectorElements = async () => {
|
56 |
+
const svgElements = domRef.querySelectorAll('svg');
|
57 |
+
const vectorShapes = domRef.querySelectorAll('.vector-shape');
|
58 |
+
|
59 |
+
// 处理SVG元素
|
60 |
+
for (const svg of Array.from(svgElements)) {
|
61 |
+
try {
|
62 |
+
// 移除problematic属性
|
63 |
+
svg.removeAttribute('vector-effect');
|
64 |
+
const vectorEffectElements = svg.querySelectorAll('[vector-effect]');
|
65 |
+
vectorEffectElements.forEach(el => el.removeAttribute('vector-effect'));
|
66 |
+
|
67 |
+
// 确保命名空间
|
68 |
+
if (!svg.hasAttribute('xmlns')) {
|
69 |
+
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
70 |
+
}
|
71 |
+
} catch (error) {
|
72 |
+
if (!isProd) {
|
73 |
+
console.warn('exportImage: SVG preprocessing failed:', error);
|
74 |
+
}
|
75 |
+
}
|
76 |
+
}
|
77 |
+
|
78 |
+
// 处理矢量形状
|
79 |
+
for (const shape of Array.from(vectorShapes)) {
|
80 |
+
try {
|
81 |
+
const svgChild = shape.querySelector('svg');
|
82 |
+
if (svgChild) {
|
83 |
+
svgChild.removeAttribute('vector-effect');
|
84 |
+
if (!svgChild.hasAttribute('xmlns')) {
|
85 |
+
svgChild.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
86 |
+
}
|
87 |
+
}
|
88 |
+
} catch (error) {
|
89 |
+
if (!isProd) {
|
90 |
+
console.warn('exportImage: Vector shape preprocessing failed:', error);
|
91 |
+
}
|
92 |
+
}
|
93 |
+
}
|
94 |
+
};
|
95 |
|
96 |
const foreignObjectSpans = domRef.querySelectorAll('foreignObject [xmlns]')
|
97 |
foreignObjectSpans.forEach(spanRef => spanRef.removeAttribute('xmlns'))
|
98 |
|
99 |
setTimeout(async () => {
|
100 |
try {
|
101 |
+
// 预处理矢量元素
|
102 |
+
await preprocessVectorElements();
|
103 |
+
|
104 |
let dataUrl: string
|
105 |
|
106 |
if (isHF) {
|
|
|
560 |
}
|
561 |
|
562 |
// 格式化元素
|
563 |
+
// 生产环境级别的矢量图形SVG生成函数
|
564 |
+
const generateSVGFromShape = (element: any): string => {
|
565 |
+
try {
|
566 |
+
const { width, height, path, fill, outline, viewBox, opacity = 1 } = element;
|
567 |
+
const [vbX, vbY, vbWidth, vbHeight] = viewBox || [0, 0, width, height];
|
568 |
+
|
569 |
+
// 安全的颜色处理
|
570 |
+
const fillColor = fill || '#000000';
|
571 |
+
const strokeColor = outline?.color || 'none';
|
572 |
+
const strokeWidth = outline?.width || 0;
|
573 |
+
|
574 |
+
// 构建SVG字符串,确保所有属性都被正确转义
|
575 |
+
const svgAttributes = [
|
576 |
+
`width="${width}"`,
|
577 |
+
`height="${height}"`,
|
578 |
+
`viewBox="${vbX} ${vbY} ${vbWidth} ${vbHeight}"`,
|
579 |
+
'xmlns="http://www.w3.org/2000/svg"',
|
580 |
+
'style="display: block; width: 100%; height: 100%;"'
|
581 |
+
].join(' ');
|
582 |
+
|
583 |
+
const pathAttributes = [
|
584 |
+
`d="${path}"`,
|
585 |
+
`fill="${fillColor}"`,
|
586 |
+
strokeWidth > 0 ? `stroke="${strokeColor}"` : '',
|
587 |
+
strokeWidth > 0 ? `stroke-width="${strokeWidth}"` : '',
|
588 |
+
opacity < 1 ? `opacity="${opacity}"` : '',
|
589 |
+
'vector-effect="non-scaling-stroke"'
|
590 |
+
].filter(Boolean).join(' ');
|
591 |
+
|
592 |
+
return `<svg ${svgAttributes}><path ${pathAttributes} /></svg>`;
|
593 |
+
} catch (error) {
|
594 |
+
console.error('generateSVGFromShape error:', error);
|
595 |
+
// 生产环境降级处理:返回简单的占位符
|
596 |
+
return `<svg width="${element.width || 100}" height="${element.height || 100}" xmlns="http://www.w3.org/2000/svg">
|
597 |
+
<rect width="100%" height="100%" fill="#f5f5f5" stroke="#ddd" stroke-width="1"/>
|
598 |
+
<text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-size="12" fill="#999">Vector</text>
|
599 |
+
</svg>`;
|
600 |
+
}
|
601 |
+
};
|
602 |
+
|
603 |
const formatElement = (element: any) => {
|
604 |
const baseStyle = `
|
605 |
left: ${element.left || 0}px;
|
|
|
629 |
}
|
630 |
|
631 |
if (element.type === 'shape') {
|
632 |
+
// 处理特殊矢量图形(生产环境优化)
|
633 |
+
if (element.special && element.path) {
|
634 |
+
try {
|
635 |
+
const svgContent = generateSVGFromShape(element);
|
636 |
+
return `<div class="element shape-element vector-shape" style="${baseStyle}">${svgContent}</div>`;
|
637 |
+
} catch (error) {
|
638 |
+
// 生产环境降级处理
|
639 |
+
if (VECTOR_EXPORT_CONFIG.ERROR_CONFIG.LOGGING.VERBOSE) {
|
640 |
+
console.warn('formatElement: Vector shape generation failed, using fallback:', error);
|
641 |
+
}
|
642 |
+
|
643 |
+
// 使用简化的矢量图形表示
|
644 |
+
const fallbackSvg = `<svg width="${element.width}" height="${element.height}" xmlns="http://www.w3.org/2000/svg">
|
645 |
+
<rect width="100%" height="100%" fill="${element.fill || '#f5f5f5'}" stroke="${element.outline?.color || '#ddd'}" stroke-width="${element.outline?.width || 1}"/>
|
646 |
+
<text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-size="12" fill="#999">Vector</text>
|
647 |
+
</svg>`;
|
648 |
+
|
649 |
+
return `<div class="element shape-element vector-shape-fallback" style="${baseStyle}">${fallbackSvg}</div>`;
|
650 |
+
}
|
651 |
+
}
|
652 |
+
|
653 |
+
// 处理普通形状
|
654 |
const shapeStyle = `
|
655 |
background: ${element.fill || '#ffffff'};
|
656 |
border: ${element.outline?.width || 0}px solid ${element.outline?.color || '#000000'};
|