import { useState } from 'react'; import { WordcloudMaskSource, WordcloudStickerPayload } from '../types'; import { apiUrl, ensureOk } from '../lib/api'; interface ExportPanelProps { jobId: string | null; svgUrl?: string; imageUrl?: string; onOpenCanvas?: () => void; maskSource?: WordcloudMaskSource | null; onImportWordcloudSticker?: (payload: WordcloudStickerPayload) => void; embedded?: boolean; } type Format = 'jpg' | 'png'; type FillMode = 'fill' | 'dot' | 'line' | 'ring'; export default function ExportPanel({ jobId, svgUrl, imageUrl, onOpenCanvas, maskSource, onImportWordcloudSticker, embedded = false, }: ExportPanelProps) { const [bmpFormat, setBmpFormat] = useState('png'); const [exportW, setExportW] = useState('1920'); const [exportH, setExportH] = useState('1080'); const [isSavingSticker, setIsSavingSticker] = useState(false); const [stroke, setStroke] = useState(false); const [fillMode, setFillMode] = useState('fill'); const [dotSpacing, setDotSpacing] = useState(10); const [dotRadius, setDotRadius] = useState(2); const [lineSpacing, setLineSpacing] = useState(6); const [lineWidth, setLineWidth] = useState(1); const [lineAngle, setLineAngle] = useState(0); const [ringRadius, setRingRadius] = useState(3); const [ringWidth, setRingWidth] = useState(1); const [ringSpacing, setRingSpacing] = useState(8); const resolveUrl = (field: string | undefined, kind: string) => { if (field) return apiUrl(field); if (jobId) return apiUrl(`/api/jobs/${jobId}/files/${kind}`); return null; }; const buildCustomSvgUrl = () => { if (!jobId) return null; const params = new URLSearchParams(); params.set('fill', fillMode); params.set('stroke', stroke ? '1' : '0'); if (fillMode === 'dot') { params.set('spacing', String(dotSpacing)); params.set('radius', String(dotRadius)); } if (fillMode === 'line') { params.set('line_spacing', String(lineSpacing)); params.set('line_width', String(lineWidth)); params.set('line_angle', String(lineAngle)); } if (fillMode === 'ring') { params.set('ring_radius', String(ringRadius)); params.set('ring_width', String(ringWidth)); params.set('ring_spacing', String(ringSpacing)); } return apiUrl(`/api/jobs/${jobId}/custom.svg?${params}`); }; const handleExportSvg = () => { const url = buildCustomSvgUrl(); if (!url) return; triggerDownload(url, 'wordcloud.svg'); }; const handleSaveAsSticker = async () => { const url = buildCustomSvgUrl() || resolveUrl(svgUrl, 'svg'); if (!url || isSavingSticker) return; setIsSavingSticker(true); try { const res = await ensureOk(await fetch(url), '读取 SVG 失败'); const svg = await res.text(); if (onImportWordcloudSticker) { onImportWordcloudSticker({ svg, mask: maskSource || undefined }); } else { onOpenCanvas?.(); } } catch (error) { const message = error instanceof Error ? error.message : '保存失败'; alert(message); } finally { setIsSavingSticker(false); } }; const handleExportBitmap = () => { const url = resolveUrl(imageUrl, 'png'); if (!url) return; triggerDownload(url, `wordcloud.${bmpFormat}`); }; const triggerDownload = (url: string, filename: string) => { const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); }; const hasResult = !!jobId; return ( <> {!embedded && (
导出
)}
{/* ── SVG 导出 ── */}
矢量 SVG
{/* 1. 描边 */}
{/* 2. 填充模式 */}
{/* 点阵参数 */} {fillMode === 'dot' && (
间距 setDotSpacing(parseInt(e.target.value) || 10)} min={2} max={100} style={{ width: 48, padding: '4px 6px', fontSize: 11 }} /> px 半径 setDotRadius(parseInt(e.target.value) || 2)} min={1} max={20} style={{ width: 48, padding: '4px 6px', fontSize: 11 }} /> px
)} {/* 线条参数 */} {fillMode === 'line' && (
间距 setLineSpacing(parseInt(e.target.value) || 6)} min={2} max={100} style={{ width: 48, padding: '4px 6px', fontSize: 11 }} /> px 粗细 setLineWidth(parseFloat(e.target.value) || 1)} min={0.5} max={10} step={0.5} style={{ width: 48, padding: '4px 6px', fontSize: 11 }} /> px
角度 setLineAngle(parseInt(e.target.value) || 0)} min={0} max={359} style={{ width: 48, padding: '4px 6px', fontSize: 11 }} /> °
)} {/* 空心圆参数 */} {fillMode === 'ring' && (
半径 setRingRadius(parseInt(e.target.value) || 3)} min={1} max={20} style={{ width: 48, padding: '4px 6px', fontSize: 11 }} /> px 粗细 setRingWidth(parseFloat(e.target.value) || 1)} min={0.5} max={10} step={0.5} style={{ width: 48, padding: '4px 6px', fontSize: 11 }} /> px
间距 setRingSpacing(parseInt(e.target.value) || 8)} min={2} max={100} style={{ width: 48, padding: '4px 6px', fontSize: 11 }} /> px
)}
{/* ── 位图导出 ── */}
位图
{(['png', 'jpg'] as Format[]).map(f => ( ))}
长= setExportW(e.target.value)} min={1} max={10000} style={{ width: '100%', padding: '4px 6px', fontSize: 11 }} /> px
宽= setExportH(e.target.value)} min={1} max={10000} style={{ width: '100%', padding: '4px 6px', fontSize: 11 }} /> px
注:为等比放大取最小值
{!hasResult && (

请先生成词云后再导出

)}
); }