feat: complete TASK-WP5-02 sticker catalog

This commit is contained in:
suyx
2026-08-03 17:05:12 +08:00
parent 2b189e3186
commit 609c71a3f7
17 changed files with 758 additions and 19 deletions
+10
View File
@@ -184,6 +184,16 @@
gap: 8px;
}
.editor-sticker-virtual-list {
height: 280px;
overflow-y: auto;
overscroll-behavior: contain;
padding-right: 4px;
scrollbar-gutter: stable;
}
.editor-sticker-count { margin: 0 0 8px; color: #62625d; font-size: 12px; }
.editor-sticker-grid button {
display: grid;
min-width: 0;
+13 -9
View File
@@ -49,6 +49,7 @@ import {
type DynamicTemplateId,
} from "./dynamic-provider.js";
import { dynamicFontOptionsFor } from "./dynamic-render-models.js";
import { P0A_STATIC_STICKER_CATALOG, P0A_STATIC_STICKER_COUNT, stickerWindow } from "./static-sticker-catalog.js";
import {
createColorCardElement,
extractPaletteFromImage,
@@ -112,11 +113,6 @@ function formatDate(value: string) {
return new Intl.DateTimeFormat("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(new Date(value));
}
const stickerFixtures = [
{ assetId: "STK001", label: "part1 原版草莓" },
{ assetId: "STK002", label: "part2 原版小狗" },
] as const;
function newElementIdentity(): CanvasElementIdentity {
return { createdAt: new Date().toISOString(), elementId: crypto.randomUUID() };
}
@@ -139,6 +135,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
const [pendingBackground, setPendingBackground] = useState<string>();
const [notice, setNotice] = useState("");
const [activePanel, setActivePanel] = useState<EditorAssetPanel>("background");
const [stickerScrollTop, setStickerScrollTop] = useState(0);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [guides, setGuides] = useState<string[]>([]);
const [multiMode, setMultiMode] = useState(false);
@@ -816,7 +813,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
{ label: "普通贴纸", panel: "stickers" as const },
{ label: "色卡", panel: "color" as const },
{ label: "动态贴纸", panel: "dynamic" as const },
]).map((item) => <button aria-current={item.panel === activePanel ? "page" : undefined} key={item.label} onClick={() => setActivePanel(item.panel)} type="button">{item.label}</button>)}
]).map((item) => <button aria-current={item.panel === activePanel ? "page" : undefined} key={item.label} onClick={() => { setActivePanel(item.panel); if (item.panel === "stickers") setStickerScrollTop(0); }} type="button">{item.label}</button>)}
</nav>
{activePanel === "background" ? <section><h2></h2><button className="editor-source active" type="button"><span className="editor-thumb" style={{ backgroundImage: `url(${imageUrl})` }} /><span></span></button></section> : null}
{activePanel === "history" ? <section><h2></h2><div className="editor-history-list">
@@ -834,9 +831,16 @@ export function EditorPage({ projectId }: { projectId: string }) {
templates={P0A_TEXT_TEMPLATES}
{...(templateCategory ? { category: templateCategory } : {})}
/> : null}
{activePanel === "stickers" ? <section><h2></h2><div className="editor-sticker-grid">
{stickerFixtures.map((sticker) => <button aria-label={`添加贴纸 ${sticker.assetId}`} disabled={!canEdit || canvasState.elements.length >= 50} key={sticker.assetId} onClick={() => addSticker(sticker.assetId)} type="button"><img alt="" className="editor-sticker-preview" src={`/api/v1/assets/public/fixture-v1/${sticker.assetId}`} /><strong>{sticker.assetId}</strong><span>{sticker.label}</span></button>)}
</div>{canvasState.elements.length >= 50 ? <p className="editor-limit" role="status"> 50 </p> : null}</section> : null}
{activePanel === "stickers" ? (() => {
const visibleStickers = stickerWindow(P0A_STATIC_STICKER_CATALOG, stickerScrollTop, 280);
return <section><h2></h2><p aria-live="polite" className="editor-sticker-count"> {P0A_STATIC_STICKER_COUNT.toLocaleString("zh-CN")} </p><div className="editor-sticker-virtual-list" data-testid="static-sticker-list" onScroll={(event) => setStickerScrollTop(event.currentTarget.scrollTop)} role="list">
<div style={{ paddingTop: visibleStickers.top_spacer_px, paddingBottom: visibleStickers.bottom_spacer_px }}>
<div className="editor-sticker-grid">
{visibleStickers.items.map((sticker) => <button aria-label={`添加贴纸 ${sticker.stable_id}`} data-sticker-id={sticker.stable_id} disabled={!canEdit || canvasState.elements.length >= 50} key={sticker.stable_id} onClick={() => addSticker(sticker.stable_id)} type="button"><img alt="" className="editor-sticker-preview" decoding="async" loading="lazy" src={sticker.thumbnail_reference.url} /><strong>{sticker.stable_id}</strong><span>part{sticker.part} · {sticker.order}</span></button>)}
</div>
</div>
</div>{canvasState.elements.length >= 50 ? <p className="editor-limit" role="status"> 50 </p> : null}</section>;
})() : null}
{activePanel === "color" ? <ColorCardPanel canAdd={canEdit && canvasState.elements.length < 50} hasBackground={Boolean(canvasState.background.asset_id)} onAdd={(definition) => { void addColorCard(definition); }} /> : null}
{activePanel === "dynamic" ? <DynamicStickerPanel canAdd={canEdit && canvasState.elements.length < 50} fontStatuses={fontStatuses} onAdd={chooseDynamicSticker} /> : null}
</aside>
+47
View File
@@ -0,0 +1,47 @@
import {
getVirtualStickerWindow,
staticStickerOriginalUrl,
staticStickerThumbnailUrl,
type StaticStickerCatalogItem,
type VirtualStickerWindow,
} from "@dada/static-sticker-catalog";
const resourceVersion = "fixture-v1";
const partCounts = [203, 36, 27, 48, 38, 75, 37, 67, 48, 24, 40, 30, 27, 51, 62, 19, 36, 45, 92, 53, 69, 31, 36, 30, 183] as const;
function buildCatalog(): StaticStickerCatalogItem[] {
let index = 0;
return partCounts.flatMap((count, partIndex) => Array.from({ length: count }, (_, orderIndex) => {
index += 1;
const stableId = `STK${String(index).padStart(index < 1_000 ? 3 : 4, "0")}`;
const item = {
enabled: true,
height: 0,
mime: "image/png" as const,
mime_type: "image/png" as const,
order: orderIndex + 1,
original_filename: `${stableId}.png`,
original_reference: "",
origin: "bundled_read_only" as const,
part: partIndex + 1,
relative_path: `sticker_part${partIndex + 1}/${stableId}.png`,
resource_version: resourceVersion,
sha256: "",
stable_id: stableId,
thumbnail_reference: { media: "thumbnail" as const, resource_id: stableId, resource_version: resourceVersion, url: "" },
width: 0,
} satisfies StaticStickerCatalogItem;
item.original_reference = staticStickerOriginalUrl(item);
item.thumbnail_reference.url = staticStickerThumbnailUrl(item);
return item;
}));
}
export const P0A_STATIC_STICKER_CATALOG = buildCatalog();
export const P0A_STATIC_STICKER_COUNT = P0A_STATIC_STICKER_CATALOG.length;
export function stickerWindow(items: readonly StaticStickerCatalogItem[], scrollTop: number, viewportHeight: number): VirtualStickerWindow<StaticStickerCatalogItem> {
return getVirtualStickerWindow(items, { columns: 2, itemHeight: 144, overscanScreens: 2, scrollTop, viewportHeight });
}
export { staticStickerOriginalUrl, staticStickerThumbnailUrl };