feat: bootstrap auto virtual tryon admin frontend
This commit is contained in:
194
src/features/libraries/library-page.tsx
Normal file
194
src/features/libraries/library-page.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Card, CardContent, CardDescription, CardEyebrow, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { PageHeader } from "@/components/ui/page-header";
|
||||
import type { LibraryItemVM, LibraryType } from "@/lib/types/view-models";
|
||||
|
||||
type LibraryPageProps = {
|
||||
isLoading?: boolean;
|
||||
items: LibraryItemVM[];
|
||||
libraryType: LibraryType;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type LibraryEnvelope = {
|
||||
data?: {
|
||||
items?: LibraryItemVM[];
|
||||
};
|
||||
message?: string;
|
||||
};
|
||||
|
||||
const LIBRARY_META: Record<
|
||||
LibraryType,
|
||||
{ title: string; description: string; eyebrow: string }
|
||||
> = {
|
||||
models: {
|
||||
title: "模特库",
|
||||
description: "继续复用提单页所需的 mock 模特数据,同时保留正式资源库的结构和标签体系。",
|
||||
eyebrow: "Model library",
|
||||
},
|
||||
scenes: {
|
||||
title: "场景库",
|
||||
description: "场景库先用 mock 占位数据承接,等真实后台资源列表可用后再切换数据源。",
|
||||
eyebrow: "Scene library",
|
||||
},
|
||||
garments: {
|
||||
title: "服装库",
|
||||
description: "服装资源暂时继续走 mock 资产,但页面本身已经按正式资源库组织。",
|
||||
eyebrow: "Garment library",
|
||||
},
|
||||
};
|
||||
|
||||
const TITLE_MESSAGE = "当前资源库仍使用 mock 数据";
|
||||
const DEFAULT_MESSAGE = "当前只保留页面结构和 mock 资源项,真实资源查询能力将在后端支持后接入。";
|
||||
|
||||
export function LibraryPage({
|
||||
isLoading = false,
|
||||
items,
|
||||
libraryType,
|
||||
message = DEFAULT_MESSAGE,
|
||||
}: LibraryPageProps) {
|
||||
const meta = LIBRARY_META[libraryType];
|
||||
|
||||
return (
|
||||
<section className="space-y-8">
|
||||
<PageHeader
|
||||
eyebrow={meta.eyebrow}
|
||||
title={meta.title}
|
||||
description={meta.description}
|
||||
meta="正式占位模块"
|
||||
/>
|
||||
|
||||
<div className="rounded-[28px] border border-[rgba(145,104,46,0.2)] bg-[rgba(202,164,97,0.12)] px-6 py-5">
|
||||
<p className="text-lg font-semibold tracking-[-0.02em] text-[#7a5323]">
|
||||
{TITLE_MESSAGE}
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-7 text-[#7a5323]">{message}</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="space-y-2">
|
||||
<CardEyebrow>Library inventory</CardEyebrow>
|
||||
<div className="space-y-1">
|
||||
<CardTitle>{meta.title}占位清单</CardTitle>
|
||||
<CardDescription>
|
||||
每个资源条目都保留预览地址、说明和标签,后续只需要替换 BFF 数据源。
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="rounded-[24px] border border-dashed border-[var(--border-strong)] bg-[var(--surface-muted)] px-5 py-6 text-sm text-[var(--ink-muted)]">
|
||||
正在加载资源库数据…
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isLoading && items.length ? (
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="rounded-[24px] border border-[var(--border-soft)] bg-[var(--surface-muted)] px-4 py-4"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-[var(--ink-strong)]">
|
||||
{item.name}
|
||||
</p>
|
||||
<p className="mt-1 text-sm leading-6 text-[var(--ink-muted)]">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
<code className="block rounded-[18px] bg-[rgba(74,64,53,0.08)] px-3 py-3 text-xs leading-6 text-[var(--ink-muted)]">
|
||||
{item.previewUri}
|
||||
</code>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{item.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded-full bg-[rgba(110,98,84,0.08)] px-2.5 py-1 font-[var(--font-mono)] text-[11px] uppercase tracking-[0.18em] text-[var(--ink-muted)]"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isLoading && !items.length ? (
|
||||
<EmptyState
|
||||
eyebrow="Library empty"
|
||||
title="暂无资源条目"
|
||||
description="当前库还没有占位数据,等后端或运营资源列表准备好后再补齐。"
|
||||
/>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function LibraryPageScreen({ libraryType }: { libraryType: LibraryType }) {
|
||||
const [items, setItems] = useState<LibraryItemVM[]>([]);
|
||||
const [message, setMessage] = useState(
|
||||
"当前资源库仍使用 mock 数据,真实资源查询能力将在后端支持后接入。",
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
async function loadLibrary() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/libraries/${libraryType}`);
|
||||
const payload = (await response.json()) as LibraryEnvelope;
|
||||
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
setItems(payload.data?.items ?? []);
|
||||
setMessage(
|
||||
payload.message ??
|
||||
"当前资源库仍使用 mock 数据,真实资源查询能力将在后端支持后接入。",
|
||||
);
|
||||
} catch {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
setItems([]);
|
||||
setMessage("资源库数据加载失败,请稍后重试。");
|
||||
} finally {
|
||||
if (active) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void loadLibrary();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [libraryType]);
|
||||
|
||||
return (
|
||||
<LibraryPage
|
||||
isLoading={isLoading}
|
||||
items={items}
|
||||
libraryType={libraryType}
|
||||
message={message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user