feat: complete TASK-WP5-02 sticker catalog
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user