feat: client-side chart export (PNG/JPG/SVG/PDF/Excel/PPT/HTML)
Replaced backend export polling with direct browser-side export: - PNG/JPG: ECharts getDataURL() with configurable pixelRatio - SVG: ECharts SVG renderer output - PDF: jsPDF with chart images (dynamic import) - Excel: SheetJS json_to_sheet (dynamic import) - PPT: PptxGenJS with chart slides (dynamic import) - HTML: Standalone page with ECharts CDN + inline options - Template: JSON config export Added global ECharts instance registry for export access. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b3098eefbd
commit
dc6ac54c86
|
|
@ -10,9 +10,11 @@ import { DataTable } from './DataTable';
|
||||||
export interface ChartRendererProps {
|
export interface ChartRendererProps {
|
||||||
chart: ChartInstance;
|
chart: ChartInstance;
|
||||||
data: Record<string, any>[];
|
data: Record<string, any>[];
|
||||||
|
/** Forwarded to EChartsBase for global instance registry. */
|
||||||
|
chartId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChartRenderer: React.FC<ChartRendererProps> = ({ chart, data }) => {
|
export const ChartRenderer: React.FC<ChartRendererProps> = ({ chart, data, chartId }) => {
|
||||||
// Use JSON key to force recalculation when any chart property changes
|
// Use JSON key to force recalculation when any chart property changes
|
||||||
const chartKey = JSON.stringify({ type: chart.type, bindings: chart.bindings, style: chart.style });
|
const chartKey = JSON.stringify({ type: chart.type, bindings: chart.bindings, style: chart.style });
|
||||||
|
|
||||||
|
|
@ -37,7 +39,7 @@ export const ChartRenderer: React.FC<ChartRendererProps> = ({ chart, data }) =>
|
||||||
const borderRadius = chart.style?.background?.borderRadius ?? 0;
|
const borderRadius = chart.style?.background?.borderRadius ?? 0;
|
||||||
const overflow = borderRadius > 0 ? 'hidden' as const : undefined;
|
const overflow = borderRadius > 0 ? 'hidden' as const : undefined;
|
||||||
return echartsOption
|
return echartsOption
|
||||||
? <EChartsBase option={echartsOption} style={{ width: '100%', height: '100%', minHeight: 250, borderRadius, overflow }} />
|
? <EChartsBase chartId={chartId} option={echartsOption} style={{ width: '100%', height: '100%', minHeight: 250, borderRadius, overflow }} />
|
||||||
: <div style={{ padding: 16, color: '#999', textAlign: 'center' }}>请绑定数据字段</div>;
|
: <div style={{ padding: 16, color: '#999', textAlign: 'center' }}>请绑定数据字段</div>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ export const ChartWrapper: React.FC<ChartWrapperProps> = ({ chartId }) => {
|
||||||
|
|
||||||
{/* Chart content */}
|
{/* Chart content */}
|
||||||
<div style={{ flex: 1, minHeight: 0 }}>
|
<div style={{ flex: 1, minHeight: 0 }}>
|
||||||
<ChartRenderer chart={chart} data={rows} />
|
<ChartRenderer chart={chart} data={rows} chartId={chartId} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hover styles via inline <style> to show toolbar */}
|
{/* Hover styles via inline <style> to show toolbar */}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,29 @@ import React, { useEffect, useRef } from 'react';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
import { useThemeStore } from '@/adapters/state/zustand/themeStore';
|
import { useThemeStore } from '@/adapters/state/zustand/themeStore';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Global ECharts instance registry – keyed by chartId
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const instanceMap = new Map<string, echarts.ECharts>();
|
||||||
|
|
||||||
|
/** Retrieve a live ECharts instance by its chartId. */
|
||||||
|
export function getEChartsInstance(chartId: string): echarts.ECharts | null {
|
||||||
|
return instanceMap.get(chartId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return all currently registered chart IDs. */
|
||||||
|
export function getRegisteredChartIds(): string[] {
|
||||||
|
return Array.from(instanceMap.keys());
|
||||||
|
}
|
||||||
|
|
||||||
export interface EChartsBaseProps {
|
export interface EChartsBaseProps {
|
||||||
option: Record<string, any>;
|
option: Record<string, any>;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
onEvents?: Record<string, (params: any) => void>;
|
onEvents?: Record<string, (params: any) => void>;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
/** Optional id used to register the instance in the global map for export. */
|
||||||
|
chartId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EChartsBase: React.FC<EChartsBaseProps> = ({
|
export const EChartsBase: React.FC<EChartsBaseProps> = ({
|
||||||
|
|
@ -18,6 +35,7 @@ export const EChartsBase: React.FC<EChartsBaseProps> = ({
|
||||||
className,
|
className,
|
||||||
onEvents,
|
onEvents,
|
||||||
loading = false,
|
loading = false,
|
||||||
|
chartId,
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const chartRef = useRef<echarts.ECharts | null>(null);
|
const chartRef = useRef<echarts.ECharts | null>(null);
|
||||||
|
|
@ -29,6 +47,8 @@ export const EChartsBase: React.FC<EChartsBaseProps> = ({
|
||||||
|
|
||||||
// Dispose previous instance if theme changed
|
// Dispose previous instance if theme changed
|
||||||
if (chartRef.current) {
|
if (chartRef.current) {
|
||||||
|
// Unregister from global map before disposing
|
||||||
|
if (chartId) instanceMap.delete(chartId);
|
||||||
chartRef.current.dispose();
|
chartRef.current.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,6 +58,9 @@ export const EChartsBase: React.FC<EChartsBaseProps> = ({
|
||||||
);
|
);
|
||||||
chartRef.current = instance;
|
chartRef.current = instance;
|
||||||
|
|
||||||
|
// Register in global map
|
||||||
|
if (chartId) instanceMap.set(chartId, instance);
|
||||||
|
|
||||||
// ResizeObserver for responsive sizing
|
// ResizeObserver for responsive sizing
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
instance.resize();
|
instance.resize();
|
||||||
|
|
@ -46,10 +69,11 @@ export const EChartsBase: React.FC<EChartsBaseProps> = ({
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
|
if (chartId) instanceMap.delete(chartId);
|
||||||
instance.dispose();
|
instance.dispose();
|
||||||
chartRef.current = null;
|
chartRef.current = null;
|
||||||
};
|
};
|
||||||
}, [currentTheme]);
|
}, [currentTheme, chartId]);
|
||||||
|
|
||||||
// Update option
|
// Update option
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -94,29 +94,53 @@ export const ExportDialog: React.FC = () => {
|
||||||
setSelectedFormat(null);
|
setSelectedFormat(null);
|
||||||
}, [setVisible]);
|
}, [setVisible]);
|
||||||
|
|
||||||
|
// Resolve the active dataset rows / columns for Excel export
|
||||||
|
const activeDataSet = useAppSelector((s) =>
|
||||||
|
s.data.dataSets.find((ds) => ds.id === activeDataSetId),
|
||||||
|
);
|
||||||
|
const layouts = useAppSelector((s) => s.layout.layouts);
|
||||||
|
|
||||||
const handleDoExport = useCallback(async () => {
|
const handleDoExport = useCallback(async () => {
|
||||||
if (!selectedFormat) {
|
if (!selectedFormat) {
|
||||||
message.warning('请先选择导出格式');
|
message.warning('请先选择导出格式');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (chartIds.length === 0) {
|
if (chartIds.length === 0 && selectedFormat !== 'excel') {
|
||||||
message.warning('当前看板没有图表可供导出');
|
message.warning('当前看板没有图表可供导出');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ext = selectedFormat === 'template' ? 'json' : selectedFormat === 'excel' ? 'xlsx' : selectedFormat;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handleExport({
|
await handleExport({
|
||||||
format: selectedFormat,
|
format: selectedFormat,
|
||||||
chartIds,
|
chartIds,
|
||||||
datasetId: activeDataSetId ?? '',
|
datasetId: activeDataSetId ?? '',
|
||||||
fileName: `dataviz-export.${selectedFormat === 'template' ? 'json' : selectedFormat}`,
|
fileName: `dataviz-export.${ext}`,
|
||||||
|
options: {
|
||||||
|
quality,
|
||||||
|
pixelRatio: resolution,
|
||||||
|
pageSize,
|
||||||
|
orientation,
|
||||||
|
sheetName,
|
||||||
|
htmlTitle,
|
||||||
|
rows: activeDataSet?.rows,
|
||||||
|
columns: activeDataSet?.columns.map((c) => c.name),
|
||||||
|
charts: charts as Record<string, any>[],
|
||||||
|
layouts: layouts as Record<string, any>[],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
message.success('导出成功');
|
message.success('导出成功');
|
||||||
handleClose();
|
handleClose();
|
||||||
} catch {
|
} catch {
|
||||||
message.error(error ?? '导出失败,请重试');
|
message.error(error ?? '导出失败,请重试');
|
||||||
}
|
}
|
||||||
}, [selectedFormat, chartIds, activeDataSetId, handleExport, handleClose, error]);
|
}, [
|
||||||
|
selectedFormat, chartIds, activeDataSetId, handleExport, handleClose, error,
|
||||||
|
quality, resolution, pageSize, orientation, sheetName, htmlTitle,
|
||||||
|
activeDataSet, charts, layouts,
|
||||||
|
]);
|
||||||
|
|
||||||
const renderFormatOptions = () => {
|
const renderFormatOptions = () => {
|
||||||
if (!selectedFormat) return null;
|
if (!selectedFormat) return null;
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,285 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { exportServiceClient } from '@/frameworks/di/container';
|
import { getEChartsInstance } from '@/frameworks/components/charts/EChartsBase';
|
||||||
|
import { type ExportFormat } from '@/domain/valueObjects/ExportFormat';
|
||||||
|
|
||||||
export interface ExportRequest {
|
export interface ExportRequest {
|
||||||
format: string;
|
format: ExportFormat;
|
||||||
chartIds: string[];
|
chartIds: string[];
|
||||||
datasetId: string;
|
datasetId: string;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
|
/** Extra options forwarded from the dialog UI. */
|
||||||
|
options?: ExportOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExportOptions {
|
||||||
|
/** 10–100, used for JPG quality. */
|
||||||
|
quality?: number;
|
||||||
|
/** Pixel ratio for raster exports. */
|
||||||
|
pixelRatio?: number;
|
||||||
|
/** PDF page size. */
|
||||||
|
pageSize?: string;
|
||||||
|
/** PDF orientation. */
|
||||||
|
orientation?: 'portrait' | 'landscape';
|
||||||
|
/** Excel sheet name. */
|
||||||
|
sheetName?: string;
|
||||||
|
/** HTML page title. */
|
||||||
|
htmlTitle?: string;
|
||||||
|
/** Rows for excel export (passed from Redux). */
|
||||||
|
rows?: Record<string, any>[];
|
||||||
|
/** Column names for excel export. */
|
||||||
|
columns?: string[];
|
||||||
|
/** Full chart configs for template JSON export. */
|
||||||
|
charts?: Record<string, any>[];
|
||||||
|
/** Layout items for template JSON export. */
|
||||||
|
layouts?: Record<string, any>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Blob builders per format
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function exportPNG(chartId: string, pixelRatio = 2): Promise<Blob> {
|
||||||
|
const instance = getEChartsInstance(chartId);
|
||||||
|
if (!instance) throw new Error(`Chart instance "${chartId}" not found`);
|
||||||
|
const dataURL = instance.getDataURL({ type: 'png', pixelRatio, backgroundColor: '#fff' });
|
||||||
|
const res = await fetch(dataURL);
|
||||||
|
return res.blob();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportJPG(chartId: string, pixelRatio = 2, quality = 0.9): Promise<Blob> {
|
||||||
|
const instance = getEChartsInstance(chartId);
|
||||||
|
if (!instance) throw new Error(`Chart instance "${chartId}" not found`);
|
||||||
|
// ECharts getDataURL doesn't support quality for jpeg, so we render to canvas manually.
|
||||||
|
const dataURL = instance.getDataURL({ type: 'png', pixelRatio, backgroundColor: '#fff' });
|
||||||
|
// Convert PNG dataURL -> canvas -> JPG blob with quality
|
||||||
|
const img = new Image();
|
||||||
|
const loaded = new Promise<void>((resolve, reject) => {
|
||||||
|
img.onload = () => resolve();
|
||||||
|
img.onerror = reject;
|
||||||
|
});
|
||||||
|
img.src = dataURL;
|
||||||
|
await loaded;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
return new Promise<Blob>((resolve, reject) => {
|
||||||
|
canvas.toBlob(
|
||||||
|
(blob) => (blob ? resolve(blob) : reject(new Error('toBlob failed'))),
|
||||||
|
'image/jpeg',
|
||||||
|
quality,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportSVG(chartId: string): Promise<Blob> {
|
||||||
|
const instance = getEChartsInstance(chartId);
|
||||||
|
if (!instance) throw new Error(`Chart instance "${chartId}" not found`);
|
||||||
|
// getDataURL with svg type returns a data:image/svg+xml;... string
|
||||||
|
const dataURL = instance.getDataURL({ type: 'svg' });
|
||||||
|
if (dataURL.startsWith('data:image/svg+xml;charset=utf-8,')) {
|
||||||
|
const svgContent = decodeURIComponent(dataURL.split(',')[1] || '');
|
||||||
|
return new Blob([svgContent], { type: 'image/svg+xml' });
|
||||||
|
}
|
||||||
|
// base64 variant
|
||||||
|
if (dataURL.startsWith('data:image/svg+xml;base64,')) {
|
||||||
|
const b64 = dataURL.split(',')[1] || '';
|
||||||
|
const svgContent = atob(b64);
|
||||||
|
return new Blob([svgContent], { type: 'image/svg+xml' });
|
||||||
|
}
|
||||||
|
// Fallback – return as-is
|
||||||
|
const res = await fetch(dataURL);
|
||||||
|
return res.blob();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportPDF(
|
||||||
|
chartIds: string[],
|
||||||
|
pixelRatio = 2,
|
||||||
|
orientation: 'portrait' | 'landscape' = 'landscape',
|
||||||
|
pageSize = 'a4',
|
||||||
|
): Promise<Blob> {
|
||||||
|
const { jsPDF } = await import('jspdf');
|
||||||
|
const doc = new jsPDF({ orientation, format: pageSize.toLowerCase(), unit: 'mm' });
|
||||||
|
const pageW = doc.internal.pageSize.getWidth();
|
||||||
|
const pageH = doc.internal.pageSize.getHeight();
|
||||||
|
const margin = 10;
|
||||||
|
const imgW = pageW - margin * 2;
|
||||||
|
const imgH = pageH - margin * 2;
|
||||||
|
let added = 0;
|
||||||
|
for (const id of chartIds) {
|
||||||
|
const instance = getEChartsInstance(id);
|
||||||
|
if (!instance) continue;
|
||||||
|
if (added > 0) doc.addPage();
|
||||||
|
const dataURL = instance.getDataURL({ type: 'png', pixelRatio, backgroundColor: '#fff' });
|
||||||
|
doc.addImage(dataURL, 'PNG', margin, margin, imgW, imgH);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
if (added === 0) throw new Error('No chart instances found for PDF export');
|
||||||
|
return doc.output('blob');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportExcel(
|
||||||
|
rows: Record<string, any>[],
|
||||||
|
columns?: string[],
|
||||||
|
sheetName = 'Sheet1',
|
||||||
|
): Promise<Blob> {
|
||||||
|
const XLSX = await import('xlsx');
|
||||||
|
const ws = columns
|
||||||
|
? XLSX.utils.json_to_sheet(rows, { header: columns })
|
||||||
|
: XLSX.utils.json_to_sheet(rows);
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, sheetName);
|
||||||
|
const buf = XLSX.write(wb, { type: 'array', bookType: 'xlsx' });
|
||||||
|
return new Blob([buf], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportPPT(chartIds: string[], pixelRatio = 2): Promise<Blob> {
|
||||||
|
const PptxGenJS = (await import('pptxgenjs')).default;
|
||||||
|
const pptx = new PptxGenJS();
|
||||||
|
let added = 0;
|
||||||
|
for (const id of chartIds) {
|
||||||
|
const instance = getEChartsInstance(id);
|
||||||
|
if (!instance) continue;
|
||||||
|
const dataURL = instance.getDataURL({ type: 'png', pixelRatio, backgroundColor: '#fff' });
|
||||||
|
const slide = pptx.addSlide();
|
||||||
|
slide.addImage({ data: dataURL, x: 0.5, y: 0.5, w: 9, h: 6 });
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
if (added === 0) throw new Error('No chart instances found for PPT export');
|
||||||
|
const arrayBuf = await pptx.write({ outputType: 'arraybuffer' });
|
||||||
|
return new Blob([arrayBuf as ArrayBuffer], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportHTML(chartIds: string[], title = 'DataViz Pro Export'): Blob {
|
||||||
|
let chartsHtml = '';
|
||||||
|
for (const id of chartIds) {
|
||||||
|
const instance = getEChartsInstance(id);
|
||||||
|
if (!instance) continue;
|
||||||
|
const option = instance.getOption();
|
||||||
|
chartsHtml += `
|
||||||
|
<div id="chart-${id}" style="width:800px;height:500px;margin:20px auto;background:#fff;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.08);"></div>
|
||||||
|
<script>echarts.init(document.getElementById('chart-${id}')).setOption(${JSON.stringify(option)});<\/script>`;
|
||||||
|
}
|
||||||
|
const html = `<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>${title}</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"><\/script>
|
||||||
|
<style>body{background:#f5f5f5;padding:20px;font-family:sans-serif;}h1{text-align:center;color:#333;}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>${title}</h1>
|
||||||
|
${chartsHtml}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
return new Blob([html], { type: 'text/html' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportTemplate(
|
||||||
|
charts: Record<string, any>[],
|
||||||
|
layouts: Record<string, any>[],
|
||||||
|
): Blob {
|
||||||
|
const template = {
|
||||||
|
version: '1.0',
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
charts,
|
||||||
|
layouts,
|
||||||
|
};
|
||||||
|
return new Blob([JSON.stringify(template, null, 2)], { type: 'application/json' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Trigger a browser download from a Blob
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function downloadBlob(blob: Blob, fileName: string): void {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = fileName;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Hook
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export function useExport() {
|
export function useExport() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleExport = useCallback(async (options: ExportRequest) => {
|
const handleExport = useCallback(async (request: ExportRequest) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
// Create the export job on the backend
|
const { format, chartIds, fileName, options = {} } = request;
|
||||||
const { id } = await exportServiceClient.createExport({
|
const pixelRatio = options.pixelRatio ?? 2;
|
||||||
format: options.format,
|
let blob: Blob;
|
||||||
chart_ids: options.chartIds,
|
|
||||||
dataset_id: options.datasetId,
|
|
||||||
file_name: options.fileName,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Poll until the export is ready, then download the blob
|
switch (format) {
|
||||||
const blob = await exportServiceClient.waitForExport(id);
|
case 'png': {
|
||||||
|
// Export first chart as single PNG
|
||||||
|
blob = await exportPNG(chartIds[0], pixelRatio);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'jpg': {
|
||||||
|
const quality = (options.quality ?? 90) / 100;
|
||||||
|
blob = await exportJPG(chartIds[0], pixelRatio, quality);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'svg': {
|
||||||
|
blob = await exportSVG(chartIds[0]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'pdf': {
|
||||||
|
blob = await exportPDF(
|
||||||
|
chartIds,
|
||||||
|
pixelRatio,
|
||||||
|
options.orientation ?? 'landscape',
|
||||||
|
options.pageSize ?? 'a4',
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'excel': {
|
||||||
|
const rows = options.rows ?? [];
|
||||||
|
if (rows.length === 0) throw new Error('没有可导出的数据');
|
||||||
|
blob = await exportExcel(rows, options.columns, options.sheetName ?? 'Sheet1');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ppt': {
|
||||||
|
blob = await exportPPT(chartIds, pixelRatio);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'html': {
|
||||||
|
blob = exportHTML(chartIds, options.htmlTitle ?? 'DataViz Pro Export');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'template': {
|
||||||
|
blob = exportTemplate(options.charts ?? [], options.layouts ?? []);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported format: ${format}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Trigger browser download
|
const ext = format === 'template' ? 'json' : format === 'excel' ? 'xlsx' : format;
|
||||||
const url = URL.createObjectURL(blob);
|
downloadBlob(blob, fileName ?? `dataviz-export.${ext}`);
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = options.fileName ?? `export.${options.format}`;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e.message ?? 'Export failed');
|
const msg = e?.message ?? 'Export failed';
|
||||||
|
setError(msg);
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue