From ee3b7745b7b6e6c97aaea5683ff5f671a78d3bc4 Mon Sep 17 00:00:00 2001 From: obroccolio Date: Thu, 13 Aug 2026 16:01:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(orders):=20=E4=B8=80=E9=94=AE=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=94=B9=E4=B8=BA=E6=89=80=E6=9C=89=E5=88=86=E5=B1=82?= =?UTF-8?q?/=E5=88=86=E7=BB=84=E7=9A=84=20SVG=20=E5=8E=8B=E7=BC=A9?= =?UTF-8?q?=E5=8C=85=EF=BC=88=E5=89=8D=E7=AB=AF=20createLayerExportZip=20?= =?UTF-8?q?=E6=89=93=E5=8C=85=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/service/app.py | 30 ++++--- frontend/src/pages/OrdersPage.tsx | 132 ++++++++++++++++-------------- 2 files changed, 87 insertions(+), 75 deletions(-) diff --git a/backend/service/app.py b/backend/service/app.py index 5cd738e..7915862 100644 --- a/backend/service/app.py +++ b/backend/service/app.py @@ -1001,7 +1001,7 @@ def get_order(request: Request, order_no: str) -> dict: @app.get("/api/orders/{order_no}/design") def order_design(request: Request, order_no: str) -> dict: - """订单设计详情:画布 + 各层元素(含素材 URL),供前端做分层/整体预览。""" + """订单设计详情:完整 CanvasDocument + 素材映射,供前端分层/整体预览与分层 SVG 导出。""" _require_orders_auth(request) path = Path(_order_dir(order_no)) / "order.json" if not path.exists(): @@ -1014,19 +1014,21 @@ def order_design(request: Request, order_no: str) -> dict: data = json.loads(tjson.read_text(encoding="utf-8")) doc = data.get("document") or {} - elements = [] + # 素材映射:元素 assetId -> {id,type(source 是 svg 还是 image),source(下载 URL)} + assets: dict[str, dict] = {} for e in doc.get("elements") or []: if not isinstance(e, dict): continue - item = dict(e) - asset_id = e.get("assetId") - if asset_id: - meta = _read_asset_meta(_asset_dir(str(asset_id))) or {} - item["assetUrl"] = meta.get("file_url") or f"/api/assets/{asset_id}/download" - item["assetName"] = meta.get("name") or str(asset_id) - else: - item["assetUrl"] = "" - elements.append(item) + aid = e.get("assetId") + if not aid or str(aid) in assets: + continue + meta = _read_asset_meta(_asset_dir(str(aid))) or {} + mime = str(meta.get("mime_type") or "") + assets[str(aid)] = { + "id": str(aid), + "type": "svg" if mime in ("image/svg+xml",) else "image", + "source": meta.get("file_url") or f"/api/assets/{aid}/download", + } try: status = manager.get_status(str(o.get("job_id") or "")).status @@ -1038,13 +1040,15 @@ def order_design(request: Request, order_no: str) -> dict: "status": status, "job_id": o.get("job_id"), "template_id": tid, - "design": { + "document": { "width": doc.get("width"), "height": doc.get("height"), "background": doc.get("background", "#ffffff"), "layers": doc.get("layers") or [], - "elements": elements, + "layerFolders": doc.get("layerFolders") or [], + "elements": doc.get("elements") or [], }, + "assets": assets, } diff --git a/frontend/src/pages/OrdersPage.tsx b/frontend/src/pages/OrdersPage.tsx index 8eb692f..d968c3c 100644 --- a/frontend/src/pages/OrdersPage.tsx +++ b/frontend/src/pages/OrdersPage.tsx @@ -9,29 +9,22 @@ import { IconTrash, } from '../components/Icons'; import { apiUrl, ensureOk } from '../lib/api'; +import { createLayerExportZip } from '../lib/svgExport'; +import type { CanvasDocument, StickerAsset } from '../types'; -interface OrderItem { +interface OrderList { order_id: string; order_no: string; - job_id: string; - template_id: string; + job_id?: string; + template_id?: string; status: string; created_at: string; } -interface DesignElement { - id?: string; - type?: string; - name?: string; - assetId?: string; - assetUrl?: string; - assetName?: string; - x?: number; - y?: number; - width?: number; - height?: number; - rotation?: number; - opacity?: number; +interface DesignAsset { + id: string; + type: 'svg' | 'image'; + source: string; } interface OrderDesign { @@ -39,13 +32,26 @@ interface OrderDesign { status: string; job_id?: string; template_id?: string; - design: { + document: { width?: number; height?: number; background?: string; - layers?: unknown[]; - elements: DesignElement[]; + layers?: { id: string; name?: string; visible?: boolean }[]; + layerFolders?: { id: string; name?: string; layerIds?: string[] }[]; + elements: Array<{ + id?: string; + type?: string; + name?: string; + assetId?: string; + x?: number; + y?: number; + width?: number; + height?: number; + rotation?: number; + opacity?: number; + }>; }; + assets: Record; } const TOKEN_KEY = 'wordcloud-orders-token'; @@ -57,40 +63,38 @@ interface OrdersPageProps { onOpenHome: () => void; } -/** 在画布上按坐标渲染一组元素(用于分层/整体预览)。 */ -function LayerCanvas({ design, onlyIndex, maxWidth }: { - design: NonNullable; +/** 按画布坐标渲染文档元素(用于分层/整体预览)。 */ +function LayerCanvas({ document: doc, assets, onlyIndex, maxWidth }: { + document: OrderDesign['document']; + assets: OrderDesign['assets']; onlyIndex?: number; maxWidth: number; }) { - const width = design.width || 1200; - const height = design.height || 1200; + const width = doc.width || 1200; + const height = doc.height || 1200; const scale = maxWidth / width; - const elements = design.elements.filter((_, i) => onlyIndex === undefined || i === onlyIndex); + const elements = doc.elements.filter((_, i) => onlyIndex === undefined || i === onlyIndex); return (
{elements.map((el, i) => { - const w = (el.width ?? 100) * scale; - const h = (el.height ?? 100) * scale; + if (!el.assetId) return null; + const src = assets[el.assetId]; + if (!src) return null; return ( {el.assetName(() => localStorage.getItem(TOKEN_KEY) || ''); const [password, setPassword] = useState(''); const [loginError, setLoginError] = useState(''); - const [orders, setOrders] = useState([]); + const [orders, setOrders] = useState([]); const [selectedNo, setSelectedNo] = useState(null); const [design, setDesign] = useState(null); const [previewMode, setPreviewMode] = useState<'overall' | 'layers'>('overall'); @@ -142,7 +146,7 @@ export default function OrdersPage({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [token]); - const selectOrder = async (o: OrderItem) => { + const selectOrder = async (o: OrderList) => { setSelectedNo(o.order_no); setPreviewMode('overall'); setError(''); @@ -158,17 +162,24 @@ export default function OrdersPage({ } }; - const downloadZip = async (path: string) => { + /** 一键导出:所有分层 + 分组打包成 SVG 压缩包(复用画布图层导出逻辑)。 */ + const onExport = async () => { + if (!design) return; setExporting(true); setError(''); try { - const res = await fetch(apiUrl(path), { headers: { Authorization: `Bearer ${token}` } }); - await ensureOk(res, '导出失败'); - const blob = await res.blob(); - const url = URL.createObjectURL(blob); + const doc = design.document as unknown as CanvasDocument; + const stickerById = new Map(); + for (const [aid, a] of Object.entries(design.assets)) { + stickerById.set(aid, { id: a.id, type: a.type, source: a.source } as StickerAsset); + } + const layerIds = (doc.layers || []).map((l) => l.id); + const folderIds = (doc.layerFolders || []).map((f) => f.id); + const zip = await createLayerExportZip(doc, stickerById, layerIds, folderIds); + const url = URL.createObjectURL(zip); const a = document.createElement('a'); a.href = url; - a.download = path.startsWith('/api/orders/export') ? 'orders-export.zip' : `${selectedNo}.zip`; + a.download = `${design.order_no}-layers.zip`; document.body.appendChild(a); a.click(); a.remove(); @@ -283,9 +294,7 @@ export default function OrdersPage({
{loading &&
加载中…
} - {!loading && orders.length === 0 && ( -
暂无订单
- )} + {!loading && orders.length === 0 &&
暂无订单
} {orders.map((o) => (
@@ -325,9 +334,9 @@ export default function OrdersPage({ 分层预览 -
@@ -338,31 +347,30 @@ export default function OrdersPage({
整体预览
{design.job_id ? ( - 整体预览 + 整体预览 ) : ( - + )}
) : (
分层预览(每层单独展示)
- {design.design.elements.map((el, i) => ( + {design.document.elements.map((el, i) => (
- + +
+
+ {el.name || design.assets[el.assetId || ''] ? + design.assets[el.assetId || '']?.source?.split('/').pop() || `层 ${i + 1}` : `层 ${i + 1}`}
-
{el.assetName || el.name || `层 ${i + 1}`}
))} - {design.design.elements.length === 0 &&
该订单没有独立图层
} + {design.document.elements.length === 0 &&
该订单没有独立图层
}
合成预览(全部层叠加)
- +
)}