Add floating canvas panels, theme settings, and SVG line-spacing analysis.

Canvas Studio now uses dockable floating panels, app settings/help navigation, and improved SVG export; the backend adds an SVG line-spacing analysis API with SciPy acceleration and new design templates.
This commit is contained in:
2026-07-13 22:01:01 +08:00
parent d5d8caef2f
commit f8a907e7c5
33 changed files with 4434 additions and 455 deletions
+12 -10
View File
@@ -1,14 +1,15 @@
import { useState } from 'react';
import { WordcloudMaskSource, WordcloudStickerPayload } from '../types';
import { apiUrl, ensureOk } from '../lib/api';
interface ExportPanelProps {
jobId: string | null;
apiBase: string;
svgUrl?: string;
imageUrl?: string;
onOpenCanvas?: () => void;
maskSource?: WordcloudMaskSource | null;
onImportWordcloudSticker?: (payload: WordcloudStickerPayload) => void;
embedded?: boolean;
}
type Format = 'jpg' | 'png';
@@ -16,12 +17,12 @@ type FillMode = 'fill' | 'dot' | 'line' | 'ring';
export default function ExportPanel({
jobId,
apiBase,
svgUrl,
imageUrl,
onOpenCanvas,
maskSource,
onImportWordcloudSticker,
embedded = false,
}: ExportPanelProps) {
const [bmpFormat, setBmpFormat] = useState<Format>('png');
const [exportW, setExportW] = useState('1920');
@@ -40,8 +41,8 @@ export default function ExportPanel({
const [ringSpacing, setRingSpacing] = useState(8);
const resolveUrl = (field: string | undefined, kind: string) => {
if (field) return field.startsWith('http') ? field : `${apiBase}${field}`;
if (jobId) return `${apiBase}/api/jobs/${jobId}/files/${kind}`;
if (field) return apiUrl(field);
if (jobId) return apiUrl(`/api/jobs/${jobId}/files/${kind}`);
return null;
};
@@ -64,7 +65,7 @@ export default function ExportPanel({
params.set('ring_width', String(ringWidth));
params.set('ring_spacing', String(ringSpacing));
}
return `${apiBase}/api/jobs/${jobId}/custom.svg?${params}`;
return apiUrl(`/api/jobs/${jobId}/custom.svg?${params}`);
};
const handleExportSvg = () => {
@@ -79,8 +80,7 @@ export default function ExportPanel({
setIsSavingSticker(true);
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`读取 SVG 失败 (${res.status})`);
const res = await ensureOk(await fetch(url), '读取 SVG 失败');
const svg = await res.text();
if (onImportWordcloudSticker) {
onImportWordcloudSticker({ svg, mask: maskSource || undefined });
@@ -114,9 +114,11 @@ export default function ExportPanel({
return (
<>
<div className="panel-header">
<div className="panel-title"></div>
</div>
{!embedded && (
<div className="panel-header">
<div className="panel-title"></div>
</div>
)}
<div className="panel-body">
{/* ── SVG 导出 ── */}