feat: unify creation entry templates
This commit is contained in:
@@ -11,14 +11,14 @@ import {
|
||||
type PlatformCreationTypeId,
|
||||
} from '../platform-entry/platformEntryCreationTypes';
|
||||
|
||||
/** 底部加号创作入口页的渲染参数,最近创作只接受作品架真实摘要。 */
|
||||
/** 底部加号创作入口页的渲染参数,最近创作用作品架摘要推导模板入口。 */
|
||||
type CustomWorldCreationStartCardProps = {
|
||||
busy?: boolean;
|
||||
entryConfig: CreationEntryConfig;
|
||||
creationTypes: readonly PlatformCreationTypeCard[];
|
||||
recentWorks?: readonly CreationEntryRecentWorkCard[];
|
||||
recentCreationTypeIds?: readonly PlatformCreationTypeId[];
|
||||
recentWindowDays?: number;
|
||||
onCreateType: (type: PlatformCreationTypeId) => void;
|
||||
onOpenRecentWork?: (index: number) => void;
|
||||
};
|
||||
|
||||
/** 创作入口公告卡兼容结构化和 HTML 两种后台配置。 */
|
||||
@@ -26,14 +26,6 @@ type CreationEventBannerCard = CreationEntryEventBannerConfig;
|
||||
const CREATION_ENTRY_BANNER_AUTOPLAY_MS = 4200;
|
||||
const CREATION_ENTRY_RECENT_TAB_ID = '__recent_creation__';
|
||||
|
||||
/** 底部加号创作入口页最近创作页签的展示数据,只来自后端作品架摘要。 */
|
||||
export type CreationEntryRecentWorkCard = {
|
||||
id: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
statusLabel: string;
|
||||
};
|
||||
|
||||
/** 判断模板 badge 是否需要展示,普通可创建态不额外占用卡片空间。 */
|
||||
function shouldShowCreationBadge(badge: string) {
|
||||
const normalizedBadge = badge.trim();
|
||||
@@ -84,29 +76,41 @@ export function CustomWorldCreationStartCard({
|
||||
busy = false,
|
||||
entryConfig,
|
||||
creationTypes,
|
||||
recentWorks = [],
|
||||
recentCreationTypeIds = [],
|
||||
recentWindowDays = 7,
|
||||
onCreateType,
|
||||
onOpenRecentWork,
|
||||
}: CustomWorldCreationStartCardProps) {
|
||||
const creationTypeGroups = useMemo(
|
||||
() => groupVisiblePlatformCreationTypes(creationTypes),
|
||||
[creationTypes],
|
||||
);
|
||||
const recentCreationTypes = useMemo(() => {
|
||||
const creationTypeById = new Map(
|
||||
creationTypes
|
||||
.filter((item) => !item.hidden)
|
||||
.map((item) => [item.id, item] as const),
|
||||
);
|
||||
return [...new Set(recentCreationTypeIds)]
|
||||
.map((id) => creationTypeById.get(id))
|
||||
.filter((item): item is PlatformCreationTypeCard => Boolean(item));
|
||||
}, [creationTypes, recentCreationTypeIds]);
|
||||
const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null);
|
||||
const hasRecentWorks = recentWorks.length > 0;
|
||||
const hasRecentCreationTypes = recentCreationTypes.length > 0;
|
||||
const activeTabId =
|
||||
activeCategoryId ??
|
||||
(hasRecentWorks
|
||||
(hasRecentCreationTypes
|
||||
? CREATION_ENTRY_RECENT_TAB_ID
|
||||
: creationTypeGroups[0]?.id ?? null);
|
||||
const isRecentTabActive =
|
||||
hasRecentWorks && activeTabId === CREATION_ENTRY_RECENT_TAB_ID;
|
||||
hasRecentCreationTypes && activeTabId === CREATION_ENTRY_RECENT_TAB_ID;
|
||||
const activeGroup = isRecentTabActive
|
||||
? null
|
||||
: creationTypeGroups.find((group) => group.id === activeTabId) ??
|
||||
creationTypeGroups[0] ??
|
||||
null;
|
||||
const visibleCreationTypes = activeGroup?.items ?? [];
|
||||
const visibleCreationTypes = isRecentTabActive
|
||||
? recentCreationTypes
|
||||
: activeGroup?.items ?? [];
|
||||
const eventBanners = useMemo(
|
||||
() => resolveCreationEntryEventBanners(entryConfig),
|
||||
[entryConfig],
|
||||
@@ -119,14 +123,14 @@ export function CustomWorldCreationStartCard({
|
||||
}, [eventBanners.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasRecentWorks) {
|
||||
if (hasRecentCreationTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveCategoryId((currentId) =>
|
||||
currentId === CREATION_ENTRY_RECENT_TAB_ID ? null : currentId,
|
||||
);
|
||||
}, [hasRecentWorks]);
|
||||
}, [hasRecentCreationTypes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (eventBanners.length <= 1) {
|
||||
@@ -263,7 +267,7 @@ export function CustomWorldCreationStartCard({
|
||||
role="tablist"
|
||||
aria-label="创作入口页签"
|
||||
>
|
||||
{hasRecentWorks ? (
|
||||
{hasRecentCreationTypes ? (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
@@ -306,77 +310,59 @@ export function CustomWorldCreationStartCard({
|
||||
</div>
|
||||
|
||||
{isRecentTabActive ? (
|
||||
<div className="creation-recent-work-grid mt-2 grid grid-cols-2 gap-2 sm:mt-3 sm:gap-3">
|
||||
{recentWorks.map((item, index) => (
|
||||
<div className="creation-template-list__recent-window mt-2 text-[11px] font-bold leading-4 text-[#8b6654] sm:text-xs">
|
||||
仅显示最近{recentWindowDays}天内使用过的模板
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="creation-template-list__grid mt-2 grid grid-cols-2 gap-2 sm:mt-3 sm:gap-3">
|
||||
{visibleCreationTypes.map((item) => {
|
||||
const disabled = item.locked || busy;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
aria-label={`打开最近创作 ${index + 1}`}
|
||||
className="creation-recent-work-card min-h-[7.5rem] rounded-[1rem] border border-[#eadbd3] bg-white p-3 text-left shadow-[0_10px_22px_rgba(174,111,73,0.1)]"
|
||||
onClick={() => onOpenRecentWork?.(index)}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
onCreateType(item.id);
|
||||
}}
|
||||
className={`creation-template-card platform-interactive-card relative flex min-h-[12.5rem] flex-col overflow-hidden rounded-[1rem] border bg-white p-0 text-left transition sm:min-h-[15rem] sm:rounded-[1.2rem] ${
|
||||
item.locked
|
||||
? 'cursor-not-allowed border-[#eadbd3] text-[#725b4d] opacity-72'
|
||||
: 'border-[#eadbd3] text-[#2f211b] hover:border-[#dc9a72] hover:shadow-[0_16px_34px_rgba(174,111,73,0.14)]'
|
||||
} ${busy && !item.locked ? 'opacity-70' : ''}`}
|
||||
>
|
||||
<div className="line-clamp-1 text-sm font-black text-[#2f211b]">
|
||||
{item.title}
|
||||
<div className="creation-template-card__media relative aspect-[1.32/1] w-full overflow-hidden bg-[#f7ebe3]">
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
{shouldShowCreationBadge(item.badge) ? (
|
||||
<span className="absolute left-2 top-2 max-w-[calc(100%-1rem)] rounded-full bg-[#b66a3e] px-2 py-0.5 text-xs font-black text-white shadow-sm sm:left-3 sm:top-3 sm:px-2.5 sm:py-1">
|
||||
{item.badge}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="creation-template-card__cost-badge absolute bottom-2 right-2 inline-flex max-w-[calc(100%-1rem)] items-center gap-1 rounded-full bg-[#fff7ec]/92 px-2 py-1 text-[11px] font-black leading-4 text-[#b65f2c] shadow-[0_8px_18px_rgba(119,72,44,0.16)]">
|
||||
<Coins className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">10-20泥点数</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-3 text-xs font-semibold leading-4 text-[#6f5a4c]">
|
||||
{item.summary}
|
||||
</div>
|
||||
<div className="mt-2 text-[11px] font-bold text-[#b65f2c]">
|
||||
{item.statusLabel}
|
||||
|
||||
<div className="creation-template-card__body flex min-h-[4.6rem] flex-1 flex-col bg-white px-2.5 pb-2.5 pt-2.5 text-[#2f211b] sm:min-h-[5.4rem] sm:px-3.5 sm:pb-3.5">
|
||||
<div className="creation-template-card__title line-clamp-1 text-sm font-black leading-5 text-[#2f211b]">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="creation-template-card__subtitle mt-1 line-clamp-2 text-xs font-semibold leading-4 text-[#6f5a4c] sm:leading-5">
|
||||
{item.subtitle}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="creation-template-list__grid mt-2 grid grid-cols-2 gap-2 sm:mt-3 sm:gap-3">
|
||||
{visibleCreationTypes.map((item) => {
|
||||
const disabled = item.locked || busy;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
onCreateType(item.id);
|
||||
}}
|
||||
className={`creation-template-card platform-interactive-card relative flex min-h-[12.5rem] flex-col overflow-hidden rounded-[1rem] border bg-white p-0 text-left transition sm:min-h-[15rem] sm:rounded-[1.2rem] ${
|
||||
item.locked
|
||||
? 'cursor-not-allowed border-[#eadbd3] text-[#725b4d] opacity-72'
|
||||
: 'border-[#eadbd3] text-[#2f211b] hover:border-[#dc9a72] hover:shadow-[0_16px_34px_rgba(174,111,73,0.14)]'
|
||||
} ${busy && !item.locked ? 'opacity-70' : ''}`}
|
||||
>
|
||||
<div className="creation-template-card__media relative aspect-[1.32/1] w-full overflow-hidden bg-[#f7ebe3]">
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
{shouldShowCreationBadge(item.badge) ? (
|
||||
<span className="absolute left-2 top-2 max-w-[calc(100%-1rem)] rounded-full bg-[#b66a3e] px-2 py-0.5 text-xs font-black text-white shadow-sm sm:left-3 sm:top-3 sm:px-2.5 sm:py-1">
|
||||
{item.badge}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="creation-template-card__cost-badge absolute bottom-2 right-2 inline-flex max-w-[calc(100%-1rem)] items-center gap-1 rounded-full bg-[#fff7ec]/92 px-2 py-1 text-[11px] font-black leading-4 text-[#b65f2c] shadow-[0_8px_18px_rgba(119,72,44,0.16)]">
|
||||
<Coins className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">10-20泥点数</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="creation-template-card__body flex min-h-[4.6rem] flex-1 flex-col bg-white px-2.5 pb-2.5 pt-2.5 text-[#2f211b] sm:min-h-[5.4rem] sm:px-3.5 sm:pb-3.5">
|
||||
<div className="creation-template-card__title line-clamp-1 text-sm font-black leading-5 text-[#2f211b]">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="creation-template-card__subtitle mt-1 line-clamp-2 text-xs font-semibold leading-4 text-[#6f5a4c] sm:leading-5">
|
||||
{item.subtitle}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user