继续收口工具弹窗与分段切换预设
新增 PlatformToolModalShell 承接白底工具弹窗壳层和固定可访问名称 新增 PlatformSegmentedTabPresets 沉淀频道下划线、创作 pill rail 与二列 option segment 迁移拼图、抓大鹅、历史素材弹窗和首页 / 作品架 / 充值切换的重复组件写法 同步 PlatformUiKit 文档和 Hermes 决策记录
This commit is contained in:
135
src/components/common/PlatformSegmentedTabPresets.test.tsx
Normal file
135
src/components/common/PlatformSegmentedTabPresets.test.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
PlatformOptionSegment,
|
||||
PlatformPillTabRail,
|
||||
PlatformUnderlineTabRail,
|
||||
} from './PlatformSegmentedTabPresets';
|
||||
|
||||
test('underline tab rail keeps channel preset classes and tab semantics', () => {
|
||||
const onChange = vi.fn();
|
||||
|
||||
render(
|
||||
<PlatformUnderlineTabRail
|
||||
items={[
|
||||
{ id: 'recommend', label: '推荐' },
|
||||
{ id: 'ranking', label: '排行' },
|
||||
]}
|
||||
activeId="recommend"
|
||||
onChange={onChange}
|
||||
ariaLabel="发现频道"
|
||||
className="min-w-0"
|
||||
/>,
|
||||
);
|
||||
|
||||
const tablist = screen.getByRole('tablist', { name: '发现频道' });
|
||||
const recommendTab = screen.getByRole('tab', { name: '推荐' });
|
||||
const rankingTab = screen.getByRole('tab', { name: '排行' });
|
||||
|
||||
expect(tablist.className).toContain('platform-mobile-home-channelbar');
|
||||
expect(tablist.className).toContain('min-w-0');
|
||||
expect(recommendTab.className).toContain('platform-mobile-home-channel');
|
||||
expect(recommendTab.className).toContain('platform-mobile-home-channel--active');
|
||||
expect(rankingTab.className).not.toContain(
|
||||
'platform-mobile-home-channel--active',
|
||||
);
|
||||
|
||||
fireEvent.click(rankingTab);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('ranking');
|
||||
});
|
||||
|
||||
test('underline tab rail supports ranking preset', () => {
|
||||
render(
|
||||
<PlatformUnderlineTabRail
|
||||
items={[
|
||||
{ id: 'hot', label: '热门' },
|
||||
{ id: 'new', label: '最新' },
|
||||
]}
|
||||
activeId="hot"
|
||||
onChange={vi.fn()}
|
||||
ariaLabel="作品排行"
|
||||
variant="ranking"
|
||||
/>,
|
||||
);
|
||||
|
||||
const hotTab = screen.getByRole('tab', { name: '热门' });
|
||||
|
||||
expect(hotTab.closest('div')?.className).toContain('platform-ranking-tabs');
|
||||
expect(hotTab.className).toContain('platform-ranking-tab');
|
||||
expect(hotTab.className).toContain('platform-ranking-tab--active');
|
||||
});
|
||||
|
||||
test('option segment keeps category filter preset classes', () => {
|
||||
render(
|
||||
<PlatformOptionSegment
|
||||
items={[
|
||||
{ id: 'all', label: '全部' },
|
||||
{ id: 'rpg', label: '文字冒险' },
|
||||
]}
|
||||
activeId="all"
|
||||
onChange={vi.fn()}
|
||||
variant="categoryFilter"
|
||||
/>,
|
||||
);
|
||||
|
||||
const allButton = screen.getByRole('button', { name: '全部' });
|
||||
|
||||
expect(allButton.closest('div')?.className).toContain(
|
||||
'platform-category-filter-dialog__options',
|
||||
);
|
||||
expect(allButton.className).toContain('platform-category-filter-dialog__option');
|
||||
expect(allButton.className).toContain(
|
||||
'platform-category-filter-dialog__option--active',
|
||||
);
|
||||
});
|
||||
|
||||
test('option segment supports profile tab semantics', () => {
|
||||
const onChange = vi.fn();
|
||||
|
||||
render(
|
||||
<PlatformOptionSegment
|
||||
items={[
|
||||
{ id: 'points', label: '泥点充值' },
|
||||
{ id: 'membership', label: '会员卡' },
|
||||
]}
|
||||
activeId="points"
|
||||
onChange={onChange}
|
||||
variant="profile"
|
||||
ariaLabel="充值类型"
|
||||
/>,
|
||||
);
|
||||
|
||||
const tablist = screen.getByRole('tablist', { name: '充值类型' });
|
||||
const membershipTab = screen.getByRole('tab', { name: '会员卡' });
|
||||
|
||||
expect(tablist.className).toContain('grid-cols-2');
|
||||
expect(membershipTab.className).toContain('w-full');
|
||||
|
||||
fireEvent.click(membershipTab);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('membership');
|
||||
});
|
||||
|
||||
test('pill tab rail keeps creation entry preset classes', () => {
|
||||
render(
|
||||
<PlatformPillTabRail
|
||||
items={[
|
||||
{ id: 'recent', label: '最近创作' },
|
||||
{ id: 'recommend', label: '热门推荐' },
|
||||
]}
|
||||
activeId="recent"
|
||||
onChange={vi.fn()}
|
||||
ariaLabel="创作入口页签"
|
||||
/>,
|
||||
);
|
||||
|
||||
const recentTab = screen.getByRole('tab', { name: '最近创作' });
|
||||
|
||||
expect(recentTab.closest('div')?.className).toContain('snap-x');
|
||||
expect(recentTab.className).toContain('snap-start');
|
||||
expect(recentTab.className).toContain('after:bg-[#d9793f]');
|
||||
});
|
||||
182
src/components/common/PlatformSegmentedTabPresets.tsx
Normal file
182
src/components/common/PlatformSegmentedTabPresets.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import {
|
||||
PlatformSegmentedTabs,
|
||||
type PlatformSegmentedTabItem,
|
||||
} from './PlatformSegmentedTabs';
|
||||
|
||||
type PlatformSegmentedTabPresetProps<TId extends string> = {
|
||||
items: readonly PlatformSegmentedTabItem<TId>[];
|
||||
activeId: TId;
|
||||
onChange: (id: TId) => void;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export type PlatformUnderlineTabRailVariant = 'channel' | 'ranking';
|
||||
|
||||
const PLATFORM_UNDERLINE_TAB_RAIL_CLASS: Record<
|
||||
PlatformUnderlineTabRailVariant,
|
||||
string
|
||||
> = {
|
||||
channel: 'platform-mobile-home-channelbar pb-1',
|
||||
ranking: 'platform-ranking-tabs pb-1',
|
||||
};
|
||||
|
||||
const PLATFORM_UNDERLINE_TAB_ITEM_CLASS: Record<
|
||||
PlatformUnderlineTabRailVariant,
|
||||
{ base: string; active: string }
|
||||
> = {
|
||||
channel: {
|
||||
base: 'platform-mobile-home-channel shrink-0 !min-h-8 !rounded-none !border-0 !bg-transparent !px-0 !shadow-none hover:!bg-transparent',
|
||||
active: 'platform-mobile-home-channel--active',
|
||||
},
|
||||
ranking: {
|
||||
base: 'platform-ranking-tab shrink-0 !min-h-[2.35rem] !rounded-none !border-0 !bg-transparent !px-[0.15rem] !shadow-none hover:!bg-transparent',
|
||||
active: 'platform-ranking-tab--active',
|
||||
},
|
||||
};
|
||||
|
||||
export type PlatformOptionSegmentVariant = 'categoryFilter' | 'profile';
|
||||
|
||||
const PLATFORM_OPTION_SEGMENT_CLASS: Record<
|
||||
PlatformOptionSegmentVariant,
|
||||
{
|
||||
rail: string;
|
||||
active: string;
|
||||
idle: string;
|
||||
}
|
||||
> = {
|
||||
categoryFilter: {
|
||||
rail: 'platform-category-filter-dialog__options',
|
||||
active:
|
||||
'platform-category-filter-dialog__option--active !border-[var(--platform-cool-border)] !bg-[var(--platform-cool-bg)] !text-[var(--platform-cool-text)]',
|
||||
idle:
|
||||
'platform-category-filter-dialog__option !min-h-[2.35rem] !rounded-[0.78rem] !border !border-[var(--platform-subpanel-border)] !bg-[var(--platform-subpanel-fill)] !px-3 !text-[0.88rem] !font-black !text-[var(--platform-text-base)] !shadow-none hover:!bg-[var(--platform-subpanel-fill)]',
|
||||
},
|
||||
profile: {
|
||||
rail: '',
|
||||
active:
|
||||
'!border-[var(--platform-cool-border)] !bg-[var(--platform-cool-bg)] !text-[var(--platform-cool-text)]',
|
||||
idle:
|
||||
'w-full !min-h-[2.25rem] !rounded-[0.78rem] !border !border-[var(--platform-subpanel-border)] !bg-[rgba(255,255,255,0.04)] !px-3 !text-sm !font-extrabold !text-[var(--platform-text-base)] !shadow-none hover:!bg-[rgba(255,255,255,0.08)]',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一首页、作品架这类横向文字 rail,只沉淀稳定的滚动与下划线皮肤。
|
||||
*/
|
||||
export function PlatformUnderlineTabRail<TId extends string>({
|
||||
items,
|
||||
activeId,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
className,
|
||||
variant = 'channel',
|
||||
}: PlatformSegmentedTabPresetProps<TId> & {
|
||||
variant?: PlatformUnderlineTabRailVariant;
|
||||
}) {
|
||||
return (
|
||||
<PlatformSegmentedTabs
|
||||
items={items}
|
||||
activeId={activeId}
|
||||
onChange={onChange}
|
||||
layout="scroll"
|
||||
gap="md"
|
||||
frame="bare"
|
||||
surface="transparent"
|
||||
size="sm"
|
||||
tone="neutral"
|
||||
semantics="tabs"
|
||||
ariaLabel={ariaLabel}
|
||||
className={[PLATFORM_UNDERLINE_TAB_RAIL_CLASS[variant], className]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
itemClassName={(_, active) =>
|
||||
[
|
||||
PLATFORM_UNDERLINE_TAB_ITEM_CLASS[variant].base,
|
||||
active ? PLATFORM_UNDERLINE_TAB_ITEM_CLASS[variant].active : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一二列按钮式切换,只负责稳定的视觉 preset,不承接业务语义。
|
||||
*/
|
||||
export function PlatformOptionSegment<TId extends string>({
|
||||
items,
|
||||
activeId,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
className,
|
||||
variant,
|
||||
}: PlatformSegmentedTabPresetProps<TId> & {
|
||||
variant: PlatformOptionSegmentVariant;
|
||||
}) {
|
||||
return (
|
||||
<PlatformSegmentedTabs
|
||||
items={items}
|
||||
activeId={activeId}
|
||||
onChange={onChange}
|
||||
columns="two"
|
||||
layout="grid"
|
||||
gap={variant === 'profile' ? 'sm' : 'md'}
|
||||
frame="bare"
|
||||
surface="transparent"
|
||||
size="sm"
|
||||
semantics={variant === 'profile' ? 'tabs' : 'segment'}
|
||||
ariaLabel={ariaLabel}
|
||||
className={[PLATFORM_OPTION_SEGMENT_CLASS[variant].rail, className]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
itemClassName={(_, active) =>
|
||||
[
|
||||
PLATFORM_OPTION_SEGMENT_CLASS[variant].idle,
|
||||
active ? PLATFORM_OPTION_SEGMENT_CLASS[variant].active : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创作入口使用的轻量 pill rail,保留 snap 与下划线的组合语义。
|
||||
*/
|
||||
export function PlatformPillTabRail<TId extends string>({
|
||||
items,
|
||||
activeId,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
className,
|
||||
}: PlatformSegmentedTabPresetProps<TId>) {
|
||||
return (
|
||||
<PlatformSegmentedTabs
|
||||
items={items}
|
||||
activeId={activeId}
|
||||
onChange={onChange}
|
||||
layout="scroll"
|
||||
gap="md"
|
||||
frame="bare"
|
||||
surface="transparent"
|
||||
size="sm"
|
||||
tone="neutral"
|
||||
semantics="tabs"
|
||||
ariaLabel={ariaLabel}
|
||||
className={['-mx-0.5 snap-x px-0.5 pb-1 scroll-px-2 sm:!gap-3', className]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
itemClassName={(_, active) =>
|
||||
[
|
||||
"relative shrink-0 snap-start !min-h-8 !rounded-full !border-0 !bg-transparent !px-2.5 !text-xs !font-black !shadow-none sm:!min-h-9 sm:!px-3.5 sm:!text-sm",
|
||||
active
|
||||
? "!text-[#6f2f21] after:absolute after:bottom-0 after:left-3 after:right-3 after:h-1 after:rounded-full after:bg-[#d9793f] after:content-['']"
|
||||
: '!text-[#7a6558] hover:!bg-transparent hover:!text-[#6f2f21]',
|
||||
].join(' ')
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
54
src/components/common/PlatformToolModalShell.test.tsx
Normal file
54
src/components/common/PlatformToolModalShell.test.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import { PlatformToolModalShell } from './PlatformToolModalShell';
|
||||
|
||||
vi.mock('../auth/AuthUiContext', () => ({
|
||||
useAuthUi: () => ({ platformTheme: 'light' }),
|
||||
}));
|
||||
|
||||
test('renders shared platform tool modal shell with remapped panel chrome', () => {
|
||||
render(
|
||||
<PlatformToolModalShell
|
||||
open
|
||||
title="发布拼图作品"
|
||||
onClose={() => {}}
|
||||
footer={<button type="button">取消</button>}
|
||||
panelClassName="!max-h-[min(90vh,42rem)]"
|
||||
>
|
||||
<div>这里是正文</div>
|
||||
</PlatformToolModalShell>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '发布拼图作品' });
|
||||
expect(dialog.parentElement?.className).toContain('platform-theme--light');
|
||||
expect(dialog.className).toContain('platform-remap-surface');
|
||||
expect(dialog.className).toContain('shadow-[0_24px_80px_rgba(0,0,0,0.55)]');
|
||||
expect(dialog.className).toContain('!max-h-[min(90vh,42rem)]');
|
||||
expect(within(dialog).getByText('这里是正文')).toBeTruthy();
|
||||
expect(within(dialog).getByRole('button', { name: '取消' })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('supports fixed aria label while keeping visible title text', () => {
|
||||
const onClose = vi.fn();
|
||||
|
||||
render(
|
||||
<PlatformToolModalShell
|
||||
open
|
||||
title="雨夜猫街"
|
||||
ariaLabel="关卡详情"
|
||||
onClose={onClose}
|
||||
>
|
||||
<div>这里是正文</div>
|
||||
</PlatformToolModalShell>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '关卡详情' });
|
||||
expect(within(dialog).getByText('雨夜猫街')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '关闭关卡详情' })).toBeTruthy();
|
||||
|
||||
fireEvent.click(dialog.parentElement as HTMLElement);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
90
src/components/common/PlatformToolModalShell.tsx
Normal file
90
src/components/common/PlatformToolModalShell.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { UnifiedModal } from './UnifiedModal';
|
||||
|
||||
type PlatformToolModalShellProps = {
|
||||
open: boolean;
|
||||
title: string;
|
||||
ariaLabel?: string;
|
||||
description?: ReactNode;
|
||||
children: ReactNode;
|
||||
footer?: ReactNode;
|
||||
onClose: () => void;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen';
|
||||
closeLabel?: string;
|
||||
closeDisabled?: boolean;
|
||||
closeOnBackdrop?: boolean;
|
||||
closeOnEscape?: boolean;
|
||||
zIndexClassName?: string;
|
||||
panelClassName?: string;
|
||||
titleClassName?: string;
|
||||
bodyClassName?: string;
|
||||
footerClassName?: string;
|
||||
};
|
||||
|
||||
function joinClassNames(...classNames: Array<string | false | null | undefined>) {
|
||||
return classNames.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 结果页 / 工具页里的白底 portal 弹窗壳。
|
||||
* 这里只收口平台主题 overlay、白底 panel 和标准 header/body/footer 节奏,不吸收各玩法正文与动作语义。
|
||||
*/
|
||||
export function PlatformToolModalShell({
|
||||
open,
|
||||
title,
|
||||
ariaLabel,
|
||||
description,
|
||||
children,
|
||||
footer,
|
||||
onClose,
|
||||
size = 'xl',
|
||||
closeLabel,
|
||||
closeDisabled = false,
|
||||
closeOnBackdrop = true,
|
||||
closeOnEscape = true,
|
||||
zIndexClassName = 'z-[140]',
|
||||
panelClassName,
|
||||
titleClassName,
|
||||
bodyClassName,
|
||||
footerClassName,
|
||||
}: PlatformToolModalShellProps) {
|
||||
const resolvedPlatformTheme =
|
||||
useAuthUi()?.platformTheme ?? 'light';
|
||||
|
||||
return (
|
||||
<UnifiedModal
|
||||
open={open}
|
||||
title={title}
|
||||
// 某些工具弹窗标题会直接显示当前关卡/物品名,但读屏和测试更适合使用稳定的弹窗语义名。
|
||||
ariaLabel={ariaLabel}
|
||||
description={description}
|
||||
onClose={onClose}
|
||||
footer={footer}
|
||||
size={size}
|
||||
closeLabel={closeLabel ?? `关闭${ariaLabel ?? title}`}
|
||||
closeDisabled={closeDisabled}
|
||||
closeOnBackdrop={closeOnBackdrop}
|
||||
closeOnEscape={closeOnEscape}
|
||||
zIndexClassName={zIndexClassName}
|
||||
overlayClassName={`platform-theme platform-theme--${resolvedPlatformTheme}`}
|
||||
panelClassName={joinClassNames(
|
||||
'platform-remap-surface shadow-[0_24px_80px_rgba(0,0,0,0.55)]',
|
||||
panelClassName,
|
||||
)}
|
||||
headerClassName="!items-center !px-5 !py-4"
|
||||
titleClassName={titleClassName}
|
||||
bodyClassName={joinClassNames(
|
||||
'!px-5 !py-4 sm:!px-5 sm:!py-4',
|
||||
bodyClassName,
|
||||
)}
|
||||
footerClassName={joinClassNames(
|
||||
'!px-5 !py-4 sm:!px-5 sm:!py-4',
|
||||
footerClassName,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</UnifiedModal>
|
||||
);
|
||||
}
|
||||
@@ -85,6 +85,25 @@ test('supports headerless dialogs while preserving the accessible name', () => {
|
||||
expect(screen.getByText('窗口内容')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('supports a stable aria label while keeping the visible title', () => {
|
||||
render(
|
||||
<UnifiedModal
|
||||
open
|
||||
title="雨夜猫街"
|
||||
ariaLabel="关卡详情"
|
||||
onClose={() => {}}
|
||||
portal={false}
|
||||
>
|
||||
<div>窗口内容</div>
|
||||
</UnifiedModal>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '关卡详情' });
|
||||
|
||||
expect(dialog).toBeTruthy();
|
||||
expect(screen.getByText('雨夜猫街')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('respects closeDisabled for every default close path', () => {
|
||||
const onClose = vi.fn();
|
||||
render(
|
||||
|
||||
@@ -22,6 +22,7 @@ type UnifiedModalCloseIcon = ComponentProps<
|
||||
type UnifiedModalProps = {
|
||||
open: boolean;
|
||||
title: string;
|
||||
ariaLabel?: string;
|
||||
titleId?: string;
|
||||
description?: ReactNode;
|
||||
children: ReactNode;
|
||||
@@ -86,6 +87,7 @@ function getPanelStyle(
|
||||
function UnifiedModalContent({
|
||||
open,
|
||||
title,
|
||||
ariaLabel,
|
||||
titleId: titleIdProp,
|
||||
description,
|
||||
children,
|
||||
@@ -183,8 +185,8 @@ function UnifiedModalContent({
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={showHeader ? titleId : undefined}
|
||||
aria-label={showHeader ? undefined : title}
|
||||
aria-labelledby={showHeader && !ariaLabel ? titleId : undefined}
|
||||
aria-label={ariaLabel ?? (!showHeader ? title : undefined)}
|
||||
aria-describedby={description ? descriptionId : undefined}
|
||||
className={joinClassNames(panelClasses, sizeClassName, panelClassName)}
|
||||
style={getPanelStyle(variant, panelStyle)}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
CreationEntryConfig,
|
||||
CreationEntryEventBannerConfig,
|
||||
} from '../../services/creationEntryConfigService';
|
||||
import { PlatformSegmentedTabs } from '../common/PlatformSegmentedTabs';
|
||||
import { PlatformPillTabRail } from '../common/PlatformSegmentedTabPresets';
|
||||
import {
|
||||
groupVisiblePlatformCreationTypes,
|
||||
type PlatformCreationTypeCard,
|
||||
@@ -280,27 +280,11 @@ export function CustomWorldCreationStartCard({
|
||||
</section>
|
||||
|
||||
<section className="creation-template-list -mx-1 px-1 sm:-mx-2 sm:px-2">
|
||||
<PlatformSegmentedTabs
|
||||
<PlatformPillTabRail
|
||||
items={categoryTabs}
|
||||
activeId={activeTabId ?? ''}
|
||||
onChange={setActiveCategoryId}
|
||||
layout="scroll"
|
||||
gap="md"
|
||||
frame="bare"
|
||||
surface="transparent"
|
||||
size="sm"
|
||||
tone="neutral"
|
||||
semantics="tabs"
|
||||
ariaLabel="创作入口页签"
|
||||
className="-mx-0.5 snap-x px-0.5 pb-1 scroll-px-2 sm:!gap-3"
|
||||
itemClassName={(_, active) =>
|
||||
[
|
||||
"relative shrink-0 snap-start !min-h-8 !rounded-full !border-0 !bg-transparent !px-2.5 !text-xs !font-black !shadow-none sm:!min-h-9 sm:!px-3.5 sm:!text-sm",
|
||||
active
|
||||
? "!text-[#6f2f21] after:absolute after:bottom-0 after:left-3 after:right-3 after:h-1 after:rounded-full after:bg-[#d9793f] after:content-['']"
|
||||
: '!text-[#7a6558] hover:!bg-transparent hover:!text-[#6f2f21]',
|
||||
].join(' ')
|
||||
}
|
||||
/>
|
||||
|
||||
{isRecentTabActive ? (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PlatformSegmentedTabs } from '../common/PlatformSegmentedTabs';
|
||||
import { PlatformUnderlineTabRail } from '../common/PlatformSegmentedTabPresets';
|
||||
|
||||
export type CustomWorldWorkFilter = 'all' | 'draft' | 'published';
|
||||
|
||||
@@ -39,27 +39,12 @@ export function CustomWorldWorkTabs({
|
||||
});
|
||||
|
||||
return (
|
||||
<PlatformSegmentedTabs
|
||||
<PlatformUnderlineTabRail
|
||||
items={filterTabs}
|
||||
activeId={activeFilter}
|
||||
onChange={onChange}
|
||||
layout="scroll"
|
||||
gap="md"
|
||||
frame="bare"
|
||||
surface="transparent"
|
||||
size="sm"
|
||||
tone="neutral"
|
||||
semantics="tabs"
|
||||
ariaLabel="作品筛选"
|
||||
className="pb-1 !gap-4 xl:pb-0"
|
||||
itemClassName={(_, active) =>
|
||||
[
|
||||
'platform-mobile-home-channel shrink-0 !min-h-8 !rounded-none !border-0 !bg-transparent !px-0 !shadow-none hover:!bg-transparent',
|
||||
active ? 'platform-mobile-home-channel--active' : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type { CreationAudioAsset } from '../../../packages/shared/src/contracts/creationAudio';
|
||||
import type { Match3DResultDraft } from '../../../packages/shared/src/contracts/match3dAgent';
|
||||
@@ -51,7 +50,6 @@ import {
|
||||
type Match3DDecodedSpritesheetRegion,
|
||||
} from '../../services/match3dSpritesheetParser';
|
||||
import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformBackActionButton } from '../common/PlatformBackActionButton';
|
||||
import { PlatformAssetPickerGrid } from '../common/PlatformAssetPickerCard';
|
||||
@@ -59,7 +57,6 @@ import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||
import { PlatformIconButton } from '../common/PlatformIconButton';
|
||||
import { PlatformMediaFrame } from '../common/PlatformMediaFrame';
|
||||
import { PlatformMediaTileGrid } from '../common/PlatformMediaTileGrid';
|
||||
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
|
||||
import { PlatformMudPointConfirmDialog } from '../common/PlatformMudPointConfirmDialog';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import { PlatformPillSwitch } from '../common/PlatformPillSwitch';
|
||||
@@ -70,6 +67,7 @@ import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import { PlatformTagEditor } from '../common/PlatformTagEditor';
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import { PlatformToolModalShell } from '../common/PlatformToolModalShell';
|
||||
import { PlatformUploadPreviewCard } from '../common/PlatformUploadPreviewCard';
|
||||
import { useMudPointConfirmController } from '../common/useMudPointConfirmController';
|
||||
import {
|
||||
@@ -1459,43 +1457,18 @@ function Match3DModalShell({
|
||||
children: ReactNode;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[146] flex items-end justify-center p-3 backdrop-blur-sm sm:items-center sm:p-4`}
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
return (
|
||||
<PlatformToolModalShell
|
||||
open
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
closeLabel="关闭"
|
||||
zIndexClassName="z-[146]"
|
||||
size="xl"
|
||||
panelClassName="!max-h-[min(92vh,48rem)]"
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
className="platform-modal-shell platform-remap-surface flex max-h-[min(92vh,48rem)] w-full max-w-5xl flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
|
||||
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
|
||||
{title}
|
||||
</div>
|
||||
<PlatformModalCloseButton
|
||||
onClick={onClose}
|
||||
label="关闭"
|
||||
variant="platformIcon"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
{children}
|
||||
</PlatformToolModalShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1766,92 +1739,22 @@ function Match3DPublishDialog({
|
||||
onUploadedImageRemove: () => void;
|
||||
onSubmitCover: () => void;
|
||||
}) {
|
||||
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
|
||||
const publishReady = blockers.length === 0;
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[146] flex items-end justify-center p-3 backdrop-blur-sm sm:items-center sm:p-4`}
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget && !isGeneratingCover) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="发布抓大鹅作品"
|
||||
className="platform-modal-shell platform-remap-surface flex max-h-[min(92vh,50rem)] w-full max-w-5xl flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
|
||||
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
|
||||
发布抓大鹅作品
|
||||
</div>
|
||||
<PlatformModalCloseButton
|
||||
onClick={onClose}
|
||||
disabled={isGeneratingCover}
|
||||
label="关闭"
|
||||
variant="platformIcon"
|
||||
className={isGeneratingCover ? 'cursor-not-allowed opacity-55' : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||
<div className="mb-4 space-y-2">
|
||||
<PlatformFieldLabel variant="section">发布检查</PlatformFieldLabel>
|
||||
{publishError ? (
|
||||
<PlatformStatusMessage tone="error" surface="platform">
|
||||
{publishError}
|
||||
</PlatformStatusMessage>
|
||||
) : publishReady ? (
|
||||
<PlatformStatusMessage tone="success" surface="platform">
|
||||
当前作品已满足发布条件。
|
||||
</PlatformStatusMessage>
|
||||
) : (
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{blockers.map((blocker, index) => (
|
||||
<PlatformStatusMessage
|
||||
key={`match3d-publish-blocker-${index}-${blocker}`}
|
||||
tone="warning"
|
||||
surface="platform"
|
||||
>
|
||||
{blocker}
|
||||
</PlatformStatusMessage>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PlatformFieldLabel variant="section" className="mb-3 block">
|
||||
封面图
|
||||
</PlatformFieldLabel>
|
||||
<Match3DCoverImageEditor
|
||||
editState={editState}
|
||||
sourceAssets={sourceAssets}
|
||||
isGenerating={isGeneratingCover}
|
||||
uploadedImageSrc={uploadedImageSrc}
|
||||
referenceImages={referenceImages}
|
||||
aiRedraw={aiRedraw}
|
||||
prompt={prompt}
|
||||
error={coverError}
|
||||
onAiRedrawChange={onAiRedrawChange}
|
||||
onFileChange={onFileChange}
|
||||
onPromptChange={onPromptChange}
|
||||
onReferenceSelect={onReferenceSelect}
|
||||
onReferenceFileChange={onReferenceFileChange}
|
||||
onReferenceRemove={onReferenceRemove}
|
||||
onUploadedImageRemove={onUploadedImageRemove}
|
||||
onSubmit={onSubmitCover}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col-reverse gap-3 border-t border-[var(--platform-subpanel-border)] px-5 py-4 sm:flex-row sm:justify-end">
|
||||
return (
|
||||
<PlatformToolModalShell
|
||||
open
|
||||
title="发布抓大鹅作品"
|
||||
onClose={onClose}
|
||||
closeLabel="关闭"
|
||||
closeDisabled={isGeneratingCover}
|
||||
closeOnBackdrop={!isGeneratingCover}
|
||||
zIndexClassName="z-[146]"
|
||||
size="xl"
|
||||
panelClassName="!max-h-[min(92vh,50rem)]"
|
||||
footerClassName="flex-col-reverse sm:flex-row sm:justify-end"
|
||||
footer={
|
||||
<>
|
||||
<PlatformActionButton
|
||||
onClick={onClose}
|
||||
disabled={isGeneratingCover || isPublishing}
|
||||
@@ -1870,10 +1773,56 @@ function Match3DPublishDialog({
|
||||
)}
|
||||
发布到广场
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="mb-4 space-y-2">
|
||||
<PlatformFieldLabel variant="section">发布检查</PlatformFieldLabel>
|
||||
{publishError ? (
|
||||
<PlatformStatusMessage tone="error" surface="platform">
|
||||
{publishError}
|
||||
</PlatformStatusMessage>
|
||||
) : publishReady ? (
|
||||
<PlatformStatusMessage tone="success" surface="platform">
|
||||
当前作品已满足发布条件。
|
||||
</PlatformStatusMessage>
|
||||
) : (
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{blockers.map((blocker, index) => (
|
||||
<PlatformStatusMessage
|
||||
key={`match3d-publish-blocker-${index}-${blocker}`}
|
||||
tone="warning"
|
||||
surface="platform"
|
||||
>
|
||||
{blocker}
|
||||
</PlatformStatusMessage>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
|
||||
<PlatformFieldLabel variant="section" className="mb-3 block">
|
||||
封面图
|
||||
</PlatformFieldLabel>
|
||||
<Match3DCoverImageEditor
|
||||
editState={editState}
|
||||
sourceAssets={sourceAssets}
|
||||
isGenerating={isGeneratingCover}
|
||||
uploadedImageSrc={uploadedImageSrc}
|
||||
referenceImages={referenceImages}
|
||||
aiRedraw={aiRedraw}
|
||||
prompt={prompt}
|
||||
error={coverError}
|
||||
onAiRedrawChange={onAiRedrawChange}
|
||||
onFileChange={onFileChange}
|
||||
onPromptChange={onPromptChange}
|
||||
onReferenceSelect={onReferenceSelect}
|
||||
onReferenceFileChange={onReferenceFileChange}
|
||||
onReferenceRemove={onReferenceRemove}
|
||||
onUploadedImageRemove={onUploadedImageRemove}
|
||||
onSubmit={onSubmitCover}
|
||||
/>
|
||||
</PlatformToolModalShell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel';
|
||||
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import { PlatformProfileSkeletonList } from '../common/PlatformProfileSkeletonList';
|
||||
import { PlatformSegmentedTabs } from '../common/PlatformSegmentedTabs';
|
||||
import { PlatformOptionSegment } from '../common/PlatformSegmentedTabPresets';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import { PlatformProfileModalShell } from './PlatformProfileModalShell';
|
||||
@@ -173,29 +173,12 @@ export function PlatformProfileRechargeModal({
|
||||
panelClassName="platform-recharge-modal !max-w-[34rem] rounded-[1.4rem]"
|
||||
bodyClassName="max-h-[min(76vh,36rem)] overflow-y-auto px-5 py-5"
|
||||
>
|
||||
<PlatformSegmentedTabs
|
||||
<PlatformOptionSegment
|
||||
items={RECHARGE_TAB_ITEMS}
|
||||
activeId={activeTab}
|
||||
onChange={onTabChange}
|
||||
columns="two"
|
||||
layout="grid"
|
||||
gap="sm"
|
||||
frame="bare"
|
||||
surface="transparent"
|
||||
size="sm"
|
||||
tone="neutral"
|
||||
semantics="tabs"
|
||||
variant="profile"
|
||||
ariaLabel="充值类型"
|
||||
itemClassName={(_, active) =>
|
||||
[
|
||||
'w-full !min-h-[2.25rem] !rounded-[0.78rem] !border !px-3 !text-sm !font-extrabold !shadow-none',
|
||||
active
|
||||
? '!border-[var(--platform-cool-border)] !bg-[var(--platform-cool-bg)] !text-[var(--platform-cool-text)]'
|
||||
: '!border-[var(--platform-subpanel-border)] !bg-[rgba(255,255,255,0.04)] !text-[var(--platform-text-base)] hover:!bg-[rgba(255,255,255,0.08)]',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}
|
||||
/>
|
||||
|
||||
<PlatformAsyncStatePanel
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type { CreativeDraftEditResult } from '../../../packages/shared/src/contracts/creativeAgent';
|
||||
import type { PuzzleAgentActionRequest } from '../../../packages/shared/src/contracts/puzzleAgentActions';
|
||||
@@ -19,7 +18,6 @@ import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/co
|
||||
import { updatePuzzleWork } from '../../services/puzzle-works';
|
||||
import { getPuzzleHistoryAssetReferenceLabel } from '../../services/puzzle-works/puzzleHistoryAsset';
|
||||
import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { CreativeImageInputPanel } from '../common/CreativeImageInputPanel';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformBackActionButton } from '../common/PlatformBackActionButton';
|
||||
@@ -28,7 +26,6 @@ import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||
import { PlatformIconBadge } from '../common/PlatformIconBadge';
|
||||
import { PlatformIconButton } from '../common/PlatformIconButton';
|
||||
import { PlatformMediaFrame } from '../common/PlatformMediaFrame';
|
||||
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
|
||||
import { PlatformMudPointConfirmDialog } from '../common/PlatformMudPointConfirmDialog';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import { PlatformProgressBar } from '../common/PlatformProgressBar';
|
||||
@@ -37,6 +34,7 @@ import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import { PlatformTagEditor } from '../common/PlatformTagEditor';
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import { PlatformToolModalShell } from '../common/PlatformToolModalShell';
|
||||
import { PlatformUploadPreviewCard } from '../common/PlatformUploadPreviewCard';
|
||||
import PuzzleHistoryAssetPickerDialog from '../unified-creation/shared/PuzzleHistoryAssetPickerDialog';
|
||||
import {
|
||||
@@ -513,7 +511,6 @@ function PuzzleLevelDetailDialog({
|
||||
onLevelChange: (nextLevel: PuzzleDraftLevel) => void;
|
||||
onStartTestRun?: (levelId: string) => void;
|
||||
}) {
|
||||
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
|
||||
const [referenceImageSrc, setReferenceImageSrc] = useState('');
|
||||
const [referenceImageLabel, setReferenceImageLabel] = useState('');
|
||||
const [referenceImageError, setReferenceImageError] = useState<string | null>(
|
||||
@@ -622,164 +619,20 @@ function PuzzleLevelDetailDialog({
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[138] flex items-end justify-center p-3 backdrop-blur-sm sm:items-center sm:p-4`}
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="关卡详情"
|
||||
className="platform-modal-shell platform-remap-surface flex max-h-[min(94vh,50rem)] w-full max-w-[56rem] flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
|
||||
<div className="min-w-0 truncate text-base font-semibold text-[var(--platform-text-strong)]">
|
||||
{level.levelName || '关卡详情'}
|
||||
</div>
|
||||
<PlatformModalCloseButton
|
||||
variant="platformIcon"
|
||||
label="关闭关卡详情"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||
<div className="puzzle-level-detail-list divide-y divide-[var(--platform-subpanel-border)]">
|
||||
<section className="grid gap-2 pb-4 sm:grid-cols-[7.5rem_minmax(0,1fr)] sm:items-center">
|
||||
<label
|
||||
htmlFor={`puzzle-level-name-${level.levelId}`}
|
||||
className="contents"
|
||||
>
|
||||
<PlatformFieldLabel variant="section">
|
||||
关卡名称
|
||||
</PlatformFieldLabel>
|
||||
</label>
|
||||
<PlatformTextField
|
||||
id={`puzzle-level-name-${level.levelId}`}
|
||||
value={level.levelName}
|
||||
disabled={isBusy}
|
||||
onChange={(event) =>
|
||||
onLevelChange({ ...level, levelName: event.target.value })
|
||||
}
|
||||
size="lg"
|
||||
density="roomy"
|
||||
aria-label="关卡名称"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="pt-4">
|
||||
<CreativeImageInputPanel
|
||||
className="puzzle-level-detail-image-editor"
|
||||
disabled={isBusy || generationProgress.isGenerating}
|
||||
isSubmitting={generationProgress.isGenerating}
|
||||
uploadedImageSrc={displayImageSrc}
|
||||
uploadedImageAlt={displayImageAlt}
|
||||
uploadedImageRefreshKey={`${imageRefreshKey}:${level.levelId}`}
|
||||
canRemoveMainImage={Boolean(effectiveReferenceImageSrc)}
|
||||
canToggleAiRedraw={Boolean(effectiveReferenceImageSrc)}
|
||||
canUploadPromptReferences={!effectiveReferenceImageSrc}
|
||||
mainImageMeta={
|
||||
shouldShowReferenceMeta ? (
|
||||
<PlatformUploadPreviewCard
|
||||
layout="inline"
|
||||
imageSrc={effectiveReferenceImageSrc}
|
||||
imageAlt="拼图参考图"
|
||||
caption={referenceImageLabel || '已选择参考图'}
|
||||
removeLabel="移除参考图"
|
||||
resolveAsset
|
||||
className="bg-white/72 py-3"
|
||||
imageShellClassName="rounded-[0.85rem] bg-[var(--platform-subpanel-fill)]"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
mainImageInputId={`puzzle-level-reference-upload-${level.levelId}`}
|
||||
promptTextareaId={`puzzle-level-picture-description-${level.levelId}`}
|
||||
prompt={level.pictureDescription}
|
||||
promptLabel={
|
||||
effectiveReferenceImageSrc
|
||||
? '画面AI重绘要求(提示词)'
|
||||
: '画面描述'
|
||||
}
|
||||
promptRows={7}
|
||||
aiRedraw={aiRedraw}
|
||||
promptReferenceImages={promptReferenceImages}
|
||||
promptReferenceLimit={PUZZLE_LEVEL_PROMPT_REFERENCE_LIMIT}
|
||||
imageLimitHint="图片≤6MB"
|
||||
imageModelPicker={
|
||||
<PuzzleImageModelPicker
|
||||
value={imageModel}
|
||||
disabled={isBusy || generationProgress.isGenerating}
|
||||
onChange={setImageModel}
|
||||
/>
|
||||
}
|
||||
inputError={referenceImageError}
|
||||
submitLabel={hasFormalImage ? '重新生成画面' : '生成画面'}
|
||||
submitCostLabel={`消耗${PUZZLE_IMAGE_GENERATION_POINT_COST}泥点`}
|
||||
submitDisabled={
|
||||
isBusy ||
|
||||
generationProgress.isGenerating ||
|
||||
(!level.pictureDescription.trim() &&
|
||||
!effectiveReferenceImageSrc)
|
||||
}
|
||||
labels={{
|
||||
imageField: '画面图',
|
||||
uploadImage: '上传参考图',
|
||||
replaceImage: '更换参考图',
|
||||
emptyImageHint: '上传图片/填写画面描述',
|
||||
removeImage: '移除参考图',
|
||||
removeImageConfirmTitle: '移除参考图?',
|
||||
removeImageConfirmBody: '移除后可重新上传或选择历史图片。',
|
||||
promptReferenceUpload: '上传描述参考图',
|
||||
promptReferencePreviewAlt: '参考图预览',
|
||||
closePromptReferencePreview: '关闭参考图预览',
|
||||
previewMainImage: '查看关卡图片',
|
||||
closeMainImagePreview: '关闭关卡图片预览',
|
||||
history: '选择历史图片',
|
||||
}}
|
||||
onMainImageFileSelect={(file) => {
|
||||
void handleReferenceImageFile(file);
|
||||
}}
|
||||
onMainImageRemove={() => {
|
||||
setReferenceImageSrc('');
|
||||
setReferenceImageLabel('');
|
||||
setReferenceImageError(null);
|
||||
setAiRedraw(true);
|
||||
onLevelChange({ ...level, pictureReference: null });
|
||||
}}
|
||||
onAiRedrawChange={setAiRedraw}
|
||||
onPromptChange={(value) =>
|
||||
onLevelChange({
|
||||
...level,
|
||||
pictureDescription: value,
|
||||
})
|
||||
}
|
||||
onPromptReferenceFilesSelect={(files) => {
|
||||
void handlePromptReferenceImageFiles(files);
|
||||
}}
|
||||
onPromptReferenceRemove={(referenceId) => {
|
||||
setPromptReferenceImages((current) =>
|
||||
current.filter((image) => image.id !== referenceId),
|
||||
);
|
||||
setReferenceImageError(null);
|
||||
}}
|
||||
onHistoryClick={() => setIsHistoryPickerOpen(true)}
|
||||
onSubmit={() => setIsCostConfirmOpen(true)}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 space-y-3 border-t border-[var(--platform-subpanel-border)] bg-[var(--platform-page-fill)] px-5 py-4 pb-[calc(env(safe-area-inset-bottom,0px)+1rem)]">
|
||||
return (
|
||||
<PlatformToolModalShell
|
||||
open
|
||||
title={level.levelName || '关卡详情'}
|
||||
ariaLabel="关卡详情"
|
||||
onClose={onClose}
|
||||
closeLabel="关闭关卡详情"
|
||||
zIndexClassName="z-[138]"
|
||||
size="xl"
|
||||
panelClassName="!max-h-[min(94vh,50rem)] !max-w-[56rem]"
|
||||
titleClassName="truncate"
|
||||
footerClassName="!block shrink-0 space-y-3 bg-[var(--platform-page-fill)] pb-[calc(env(safe-area-inset-bottom,0px)+1rem)]"
|
||||
footer={
|
||||
<>
|
||||
{onStartTestRun && hasFormalImage ? (
|
||||
<PlatformActionButton
|
||||
disabled={isBusy}
|
||||
@@ -807,37 +660,160 @@ function PuzzleLevelDetailDialog({
|
||||
</div>
|
||||
</PlatformProgressBar>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<PlatformMudPointConfirmDialog
|
||||
open={isCostConfirmOpen}
|
||||
points={PUZZLE_IMAGE_GENERATION_POINT_COST}
|
||||
onClose={() => setIsCostConfirmOpen(false)}
|
||||
onConfirm={executeGeneration}
|
||||
confirmDisabled={isBusy || generationProgress.isGenerating}
|
||||
portal={false}
|
||||
overlayClassName="absolute z-20 bg-black/45"
|
||||
panelClassName="platform-remap-surface rounded-[1.5rem] shadow-[0_24px_80px_rgba(0,0,0,0.45)]"
|
||||
/>
|
||||
|
||||
{isHistoryPickerOpen ? (
|
||||
<PuzzleHistoryAssetPickerDialog
|
||||
isBusy={isBusy}
|
||||
onClose={() => setIsHistoryPickerOpen(false)}
|
||||
onSelect={(asset) => {
|
||||
setReferenceImageSrc(asset.imageSrc);
|
||||
setReferenceImageLabel(
|
||||
getPuzzleHistoryAssetReferenceLabel(asset.imageSrc),
|
||||
);
|
||||
setAiRedraw(true);
|
||||
setReferenceImageError(null);
|
||||
setIsHistoryPickerOpen(false);
|
||||
}}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="puzzle-level-detail-list divide-y divide-[var(--platform-subpanel-border)]">
|
||||
<section className="grid gap-2 pb-4 sm:grid-cols-[7.5rem_minmax(0,1fr)] sm:items-center">
|
||||
<label
|
||||
htmlFor={`puzzle-level-name-${level.levelId}`}
|
||||
className="contents"
|
||||
>
|
||||
<PlatformFieldLabel variant="section">
|
||||
关卡名称
|
||||
</PlatformFieldLabel>
|
||||
</label>
|
||||
<PlatformTextField
|
||||
id={`puzzle-level-name-${level.levelId}`}
|
||||
value={level.levelName}
|
||||
disabled={isBusy}
|
||||
onChange={(event) =>
|
||||
onLevelChange({ ...level, levelName: event.target.value })
|
||||
}
|
||||
size="lg"
|
||||
density="roomy"
|
||||
aria-label="关卡名称"
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className="pt-4">
|
||||
<CreativeImageInputPanel
|
||||
className="puzzle-level-detail-image-editor"
|
||||
disabled={isBusy || generationProgress.isGenerating}
|
||||
isSubmitting={generationProgress.isGenerating}
|
||||
uploadedImageSrc={displayImageSrc}
|
||||
uploadedImageAlt={displayImageAlt}
|
||||
uploadedImageRefreshKey={`${imageRefreshKey}:${level.levelId}`}
|
||||
canRemoveMainImage={Boolean(effectiveReferenceImageSrc)}
|
||||
canToggleAiRedraw={Boolean(effectiveReferenceImageSrc)}
|
||||
canUploadPromptReferences={!effectiveReferenceImageSrc}
|
||||
mainImageMeta={
|
||||
shouldShowReferenceMeta ? (
|
||||
<PlatformUploadPreviewCard
|
||||
layout="inline"
|
||||
imageSrc={effectiveReferenceImageSrc}
|
||||
imageAlt="拼图参考图"
|
||||
caption={referenceImageLabel || '已选择参考图'}
|
||||
removeLabel="移除参考图"
|
||||
resolveAsset
|
||||
className="bg-white/72 py-3"
|
||||
imageShellClassName="rounded-[0.85rem] bg-[var(--platform-subpanel-fill)]"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
mainImageInputId={`puzzle-level-reference-upload-${level.levelId}`}
|
||||
promptTextareaId={`puzzle-level-picture-description-${level.levelId}`}
|
||||
prompt={level.pictureDescription}
|
||||
promptLabel={
|
||||
effectiveReferenceImageSrc
|
||||
? '画面AI重绘要求(提示词)'
|
||||
: '画面描述'
|
||||
}
|
||||
promptRows={7}
|
||||
aiRedraw={aiRedraw}
|
||||
promptReferenceImages={promptReferenceImages}
|
||||
promptReferenceLimit={PUZZLE_LEVEL_PROMPT_REFERENCE_LIMIT}
|
||||
imageLimitHint="图片≤6MB"
|
||||
imageModelPicker={
|
||||
<PuzzleImageModelPicker
|
||||
value={imageModel}
|
||||
disabled={isBusy || generationProgress.isGenerating}
|
||||
onChange={setImageModel}
|
||||
/>
|
||||
}
|
||||
inputError={referenceImageError}
|
||||
submitLabel={hasFormalImage ? '重新生成画面' : '生成画面'}
|
||||
submitCostLabel={`消耗${PUZZLE_IMAGE_GENERATION_POINT_COST}泥点`}
|
||||
submitDisabled={
|
||||
isBusy ||
|
||||
generationProgress.isGenerating ||
|
||||
(!level.pictureDescription.trim() &&
|
||||
!effectiveReferenceImageSrc)
|
||||
}
|
||||
labels={{
|
||||
imageField: '画面图',
|
||||
uploadImage: '上传参考图',
|
||||
replaceImage: '更换参考图',
|
||||
emptyImageHint: '上传图片/填写画面描述',
|
||||
removeImage: '移除参考图',
|
||||
removeImageConfirmTitle: '移除参考图?',
|
||||
removeImageConfirmBody: '移除后可重新上传或选择历史图片。',
|
||||
promptReferenceUpload: '上传描述参考图',
|
||||
promptReferencePreviewAlt: '参考图预览',
|
||||
closePromptReferencePreview: '关闭参考图预览',
|
||||
previewMainImage: '查看关卡图片',
|
||||
closeMainImagePreview: '关闭关卡图片预览',
|
||||
history: '选择历史图片',
|
||||
}}
|
||||
onMainImageFileSelect={(file) => {
|
||||
void handleReferenceImageFile(file);
|
||||
}}
|
||||
onMainImageRemove={() => {
|
||||
setReferenceImageSrc('');
|
||||
setReferenceImageLabel('');
|
||||
setReferenceImageError(null);
|
||||
setAiRedraw(true);
|
||||
onLevelChange({ ...level, pictureReference: null });
|
||||
}}
|
||||
onAiRedrawChange={setAiRedraw}
|
||||
onPromptChange={(value) =>
|
||||
onLevelChange({
|
||||
...level,
|
||||
pictureDescription: value,
|
||||
})
|
||||
}
|
||||
onPromptReferenceFilesSelect={(files) => {
|
||||
void handlePromptReferenceImageFiles(files);
|
||||
}}
|
||||
onPromptReferenceRemove={(referenceId) => {
|
||||
setPromptReferenceImages((current) =>
|
||||
current.filter((image) => image.id !== referenceId),
|
||||
);
|
||||
setReferenceImageError(null);
|
||||
}}
|
||||
onHistoryClick={() => setIsHistoryPickerOpen(true)}
|
||||
onSubmit={() => setIsCostConfirmOpen(true)}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
|
||||
<PlatformMudPointConfirmDialog
|
||||
open={isCostConfirmOpen}
|
||||
points={PUZZLE_IMAGE_GENERATION_POINT_COST}
|
||||
onClose={() => setIsCostConfirmOpen(false)}
|
||||
onConfirm={executeGeneration}
|
||||
confirmDisabled={isBusy || generationProgress.isGenerating}
|
||||
portal={false}
|
||||
overlayClassName="absolute z-20 bg-black/45"
|
||||
panelClassName="platform-remap-surface rounded-[1.5rem] shadow-[0_24px_80px_rgba(0,0,0,0.45)]"
|
||||
/>
|
||||
|
||||
{isHistoryPickerOpen ? (
|
||||
<PuzzleHistoryAssetPickerDialog
|
||||
isBusy={isBusy}
|
||||
onClose={() => setIsHistoryPickerOpen(false)}
|
||||
onSelect={(asset) => {
|
||||
setReferenceImageSrc(asset.imageSrc);
|
||||
setReferenceImageLabel(
|
||||
getPuzzleHistoryAssetReferenceLabel(asset.imageSrc),
|
||||
);
|
||||
setAiRedraw(true);
|
||||
setReferenceImageError(null);
|
||||
setIsHistoryPickerOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</PlatformToolModalShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -860,103 +836,23 @@ function PuzzlePublishDialog({
|
||||
onClose: () => void;
|
||||
onPublish: () => void;
|
||||
}) {
|
||||
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
|
||||
const primaryLevel = editState.levels[0] ?? null;
|
||||
const formalImageSrc = primaryLevel
|
||||
? resolveLevelFormalImageSrc(primaryLevel)
|
||||
: '';
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[140] flex items-end justify-center p-3 backdrop-blur-sm sm:items-center sm:p-4`}
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="发布拼图作品"
|
||||
className="platform-modal-shell platform-remap-surface flex max-h-[min(90vh,42rem)] w-full max-w-3xl flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
|
||||
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
|
||||
发布拼图作品
|
||||
</div>
|
||||
<PlatformModalCloseButton
|
||||
variant="platformIcon"
|
||||
label="关闭发布拼图作品"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_14rem]">
|
||||
<div className="space-y-3">
|
||||
<PlatformFieldLabel variant="section">
|
||||
发布检查
|
||||
</PlatformFieldLabel>
|
||||
{actionError ? (
|
||||
<PlatformStatusMessage tone="error" surface="platform">
|
||||
{actionError}
|
||||
</PlatformStatusMessage>
|
||||
) : publishReady ? (
|
||||
<div className="space-y-2">
|
||||
<PlatformStatusMessage tone="success" surface="platform">
|
||||
当前作品已满足发布条件。
|
||||
</PlatformStatusMessage>
|
||||
<PlatformStatusMessage
|
||||
tone="warning"
|
||||
surface="platform"
|
||||
className="font-semibold"
|
||||
>
|
||||
消耗 {PUZZLE_PUBLISH_POINT_COST} 泥点
|
||||
</PlatformStatusMessage>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{blockers.map((blocker, index) => (
|
||||
<PlatformStatusMessage
|
||||
key={`puzzle-publish-blocker-${index}-${blocker}`}
|
||||
tone="warning"
|
||||
surface="platform"
|
||||
>
|
||||
{blocker}
|
||||
</PlatformStatusMessage>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<PlatformFieldLabel variant="section">
|
||||
封面关卡
|
||||
</PlatformFieldLabel>
|
||||
<PlatformMediaFrame
|
||||
src={formalImageSrc}
|
||||
refreshKey={imageRefreshKey}
|
||||
alt={primaryLevel?.levelName || editState.workTitle}
|
||||
fallbackLabel="封面关卡"
|
||||
fallbackContent={<span className="sr-only">封面关卡</span>}
|
||||
aspect="square"
|
||||
surface="soft"
|
||||
className="rounded-[1.15rem]"
|
||||
/>
|
||||
<div className="text-sm font-black text-[var(--platform-text-strong)]">
|
||||
{editState.workTitle}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col-reverse gap-3 border-t border-[var(--platform-subpanel-border)] px-5 py-4 sm:flex-row sm:justify-end">
|
||||
return (
|
||||
<PlatformToolModalShell
|
||||
open
|
||||
title="发布拼图作品"
|
||||
onClose={onClose}
|
||||
closeLabel="关闭发布拼图作品"
|
||||
zIndexClassName="z-[140]"
|
||||
size="lg"
|
||||
panelClassName="!max-h-[min(90vh,42rem)]"
|
||||
footerClassName="flex-col-reverse sm:flex-row sm:justify-end"
|
||||
footer={
|
||||
<>
|
||||
<PlatformActionButton onClick={onClose} tone="ghost">
|
||||
取消
|
||||
</PlatformActionButton>
|
||||
@@ -968,10 +864,62 @@ function PuzzlePublishDialog({
|
||||
? '发布中...'
|
||||
: `发布到广场 · ${PUZZLE_PUBLISH_POINT_COST}泥点`}
|
||||
</PlatformActionButton>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_14rem]">
|
||||
<div className="space-y-3">
|
||||
<PlatformFieldLabel variant="section">发布检查</PlatformFieldLabel>
|
||||
{actionError ? (
|
||||
<PlatformStatusMessage tone="error" surface="platform">
|
||||
{actionError}
|
||||
</PlatformStatusMessage>
|
||||
) : publishReady ? (
|
||||
<div className="space-y-2">
|
||||
<PlatformStatusMessage tone="success" surface="platform">
|
||||
当前作品已满足发布条件。
|
||||
</PlatformStatusMessage>
|
||||
<PlatformStatusMessage
|
||||
tone="warning"
|
||||
surface="platform"
|
||||
className="font-semibold"
|
||||
>
|
||||
消耗 {PUZZLE_PUBLISH_POINT_COST} 泥点
|
||||
</PlatformStatusMessage>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{blockers.map((blocker, index) => (
|
||||
<PlatformStatusMessage
|
||||
key={`puzzle-publish-blocker-${index}-${blocker}`}
|
||||
tone="warning"
|
||||
surface="platform"
|
||||
>
|
||||
{blocker}
|
||||
</PlatformStatusMessage>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<PlatformFieldLabel variant="section">封面关卡</PlatformFieldLabel>
|
||||
<PlatformMediaFrame
|
||||
src={formalImageSrc}
|
||||
refreshKey={imageRefreshKey}
|
||||
alt={primaryLevel?.levelName || editState.workTitle}
|
||||
fallbackLabel="封面关卡"
|
||||
fallbackContent={<span className="sr-only">封面关卡</span>}
|
||||
aspect="square"
|
||||
surface="soft"
|
||||
className="rounded-[1.15rem]"
|
||||
/>
|
||||
<div className="text-sm font-black text-[var(--platform-text-strong)]">
|
||||
{editState.workTitle}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
</PlatformToolModalShell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,10 @@ import { PlatformIconButton } from '../common/PlatformIconButton';
|
||||
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
|
||||
import { PlatformNavigableListItem } from '../common/PlatformNavigableListItem';
|
||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||
import { PlatformSegmentedTabs } from '../common/PlatformSegmentedTabs';
|
||||
import {
|
||||
PlatformOptionSegment,
|
||||
PlatformUnderlineTabRail,
|
||||
} from '../common/PlatformSegmentedTabPresets';
|
||||
import { PlatformStatusDialog } from '../common/PlatformStatusDialog';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
@@ -2178,27 +2181,11 @@ function PlatformCategoryFilterDialog({
|
||||
<div className="text-xs font-black text-[var(--platform-text-soft)]">
|
||||
玩法
|
||||
</div>
|
||||
<PlatformSegmentedTabs
|
||||
<PlatformOptionSegment
|
||||
items={kindFilterTabs}
|
||||
activeId={kindFilter}
|
||||
onChange={onKindFilterChange}
|
||||
columns="two"
|
||||
gap="md"
|
||||
frame="bare"
|
||||
surface="transparent"
|
||||
size="sm"
|
||||
semantics="segment"
|
||||
className="platform-category-filter-dialog__options"
|
||||
itemClassName={(_, active) =>
|
||||
[
|
||||
'platform-category-filter-dialog__option !min-h-[2.35rem] !rounded-[0.78rem] !border !border-[var(--platform-subpanel-border)] !bg-[var(--platform-subpanel-fill)] !px-3 !text-[0.88rem] !font-black !text-[var(--platform-text-base)] !shadow-none hover:!bg-[var(--platform-subpanel-fill)]',
|
||||
active
|
||||
? 'platform-category-filter-dialog__option--active !border-[var(--platform-cool-border)] !bg-[var(--platform-cool-bg)] !text-[var(--platform-cool-text)]'
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}
|
||||
variant="categoryFilter"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2206,27 +2193,11 @@ function PlatformCategoryFilterDialog({
|
||||
<div className="text-xs font-black text-[var(--platform-text-soft)]">
|
||||
排序
|
||||
</div>
|
||||
<PlatformSegmentedTabs
|
||||
<PlatformOptionSegment
|
||||
items={sortModeTabs}
|
||||
activeId={sortMode}
|
||||
onChange={onSortModeChange}
|
||||
columns="two"
|
||||
gap="md"
|
||||
frame="bare"
|
||||
surface="transparent"
|
||||
size="sm"
|
||||
semantics="segment"
|
||||
className="platform-category-filter-dialog__options"
|
||||
itemClassName={(_, active) =>
|
||||
[
|
||||
'platform-category-filter-dialog__option !min-h-[2.35rem] !rounded-[0.78rem] !border !border-[var(--platform-subpanel-border)] !bg-[var(--platform-subpanel-fill)] !px-3 !text-[0.88rem] !font-black !text-[var(--platform-text-base)] !shadow-none hover:!bg-[var(--platform-subpanel-fill)]',
|
||||
active
|
||||
? 'platform-category-filter-dialog__option--active !border-[var(--platform-cool-border)] !bg-[var(--platform-cool-bg)] !text-[var(--platform-cool-text)]'
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}
|
||||
variant="categoryFilter"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3604,30 +3575,15 @@ export function RpgEntryHomeView({
|
||||
<section
|
||||
className={`${PANEL_SURFACE_CLASS} platform-ranking-panel px-4 py-3.5 sm:px-5 sm:py-4`}
|
||||
>
|
||||
<PlatformSegmentedTabs
|
||||
<PlatformUnderlineTabRail
|
||||
items={PLATFORM_RANKING_TABS.map((tab) => ({
|
||||
id: tab.id,
|
||||
label: tab.label,
|
||||
}))}
|
||||
activeId={activeRankingTab}
|
||||
onChange={setActiveRankingTab}
|
||||
layout="scroll"
|
||||
gap="md"
|
||||
frame="bare"
|
||||
surface="transparent"
|
||||
size="sm"
|
||||
tone="neutral"
|
||||
semantics="tabs"
|
||||
ariaLabel="作品排行"
|
||||
className="platform-ranking-tabs pb-1"
|
||||
itemClassName={(_, active) =>
|
||||
[
|
||||
'platform-ranking-tab shrink-0 !min-h-[2.35rem] !rounded-none !border-0 !bg-transparent !px-[0.15rem] !shadow-none hover:!bg-transparent',
|
||||
active ? 'platform-ranking-tab--active' : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}
|
||||
variant="ranking"
|
||||
/>
|
||||
|
||||
<PlatformAsyncStatePanel
|
||||
@@ -3801,27 +3757,11 @@ export function RpgEntryHomeView({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<PlatformSegmentedTabs
|
||||
<PlatformUnderlineTabRail
|
||||
items={discoverChannelTabs}
|
||||
activeId={discoverChannel}
|
||||
onChange={handleDiscoverChannelChange}
|
||||
layout="scroll"
|
||||
gap="md"
|
||||
frame="bare"
|
||||
surface="transparent"
|
||||
size="sm"
|
||||
tone="neutral"
|
||||
semantics="tabs"
|
||||
ariaLabel="发现频道"
|
||||
className="platform-mobile-home-channelbar pb-1"
|
||||
itemClassName={(_, active) =>
|
||||
[
|
||||
'platform-mobile-home-channel shrink-0 !min-h-8 !rounded-none !border-0 !bg-transparent !px-0 !shadow-none hover:!bg-transparent',
|
||||
active ? 'platform-mobile-home-channel--active' : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}
|
||||
/>
|
||||
|
||||
{platformError ? (
|
||||
@@ -3969,27 +3909,11 @@ export function RpgEntryHomeView({
|
||||
|
||||
const desktopDiscoverContent: ReactNode = (
|
||||
<div className={DESKTOP_DISCOVER_PAGE_STAGE_CLASS}>
|
||||
<PlatformSegmentedTabs
|
||||
<PlatformUnderlineTabRail
|
||||
items={discoverChannelTabs}
|
||||
activeId={discoverChannel}
|
||||
onChange={handleDiscoverChannelChange}
|
||||
layout="scroll"
|
||||
gap="md"
|
||||
frame="bare"
|
||||
surface="transparent"
|
||||
size="sm"
|
||||
tone="neutral"
|
||||
semantics="tabs"
|
||||
ariaLabel="发现频道"
|
||||
className="platform-mobile-home-channelbar pb-1"
|
||||
itemClassName={(_, active) =>
|
||||
[
|
||||
'platform-mobile-home-channel shrink-0 !min-h-8 !rounded-none !border-0 !bg-transparent !px-0 !shadow-none hover:!bg-transparent',
|
||||
active ? 'platform-mobile-home-channel--active' : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}
|
||||
/>
|
||||
|
||||
{platformError ? (
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import {
|
||||
puzzleAssetClient,
|
||||
type PuzzleHistoryAsset,
|
||||
} from '../../../services/puzzle-works/puzzleAssetClient';
|
||||
import { formatPuzzleHistoryAssetCreatedAt } from '../../../services/puzzle-works/puzzleHistoryAsset';
|
||||
import { useAuthUi } from '../../auth/AuthUiContext';
|
||||
import { PlatformAssetPickerGrid } from '../../common/PlatformAssetPickerCard';
|
||||
import { PlatformModalCloseButton } from '../../common/PlatformModalCloseButton';
|
||||
import { PlatformToolModalShell } from '../../common/PlatformToolModalShell';
|
||||
|
||||
type PuzzleHistoryAssetPickerDialogProps = {
|
||||
isBusy: boolean;
|
||||
@@ -21,7 +19,6 @@ export function PuzzleHistoryAssetPickerDialog({
|
||||
onClose,
|
||||
onSelect,
|
||||
}: PuzzleHistoryAssetPickerDialogProps) {
|
||||
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
|
||||
const [assets, setAssets] = useState<PuzzleHistoryAsset[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -57,62 +54,37 @@ export function PuzzleHistoryAssetPickerDialog({
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={`platform-theme platform-theme--${platformTheme} platform-overlay fixed inset-0 z-[145] flex items-end justify-center p-3 backdrop-blur-sm sm:items-center sm:p-4`}
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
return (
|
||||
<PlatformToolModalShell
|
||||
open
|
||||
title="选择历史图片"
|
||||
onClose={onClose}
|
||||
closeLabel="关闭历史图片选择器"
|
||||
zIndexClassName="z-[145]"
|
||||
size="xl"
|
||||
panelClassName="!max-h-[min(92vh,46rem)]"
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="选择历史图片"
|
||||
className="platform-modal-shell platform-remap-surface flex max-h-[min(92vh,46rem)] w-full max-w-5xl flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-5 py-4">
|
||||
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
|
||||
选择历史图片
|
||||
</div>
|
||||
<PlatformModalCloseButton
|
||||
variant="platformIcon"
|
||||
label="关闭历史图片选择器"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||
<PlatformAssetPickerGrid
|
||||
items={assets}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
loadingLabel="读取中..."
|
||||
emptyLabel="暂无历史拼图素材"
|
||||
disabled={isBusy}
|
||||
getKey={(asset) => asset.assetObjectId}
|
||||
getImageSrc={(asset) => asset.imageSrc}
|
||||
getImageAlt={() => ''}
|
||||
getSubtitle={(asset) =>
|
||||
formatPuzzleHistoryAssetCreatedAt(asset.createdAt)
|
||||
}
|
||||
getAriaLabel={(asset) =>
|
||||
`选择${formatPuzzleHistoryAssetCreatedAt(asset.createdAt)}的历史图片`
|
||||
}
|
||||
onSelect={onSelect}
|
||||
cardRadiusClassName="rounded-[1.25rem]"
|
||||
bodyClassName="px-4 py-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
<PlatformAssetPickerGrid
|
||||
items={assets}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
loadingLabel="读取中..."
|
||||
emptyLabel="暂无历史拼图素材"
|
||||
disabled={isBusy}
|
||||
getKey={(asset) => asset.assetObjectId}
|
||||
getImageSrc={(asset) => asset.imageSrc}
|
||||
getImageAlt={() => ''}
|
||||
getSubtitle={(asset) =>
|
||||
formatPuzzleHistoryAssetCreatedAt(asset.createdAt)
|
||||
}
|
||||
getAriaLabel={(asset) =>
|
||||
`选择${formatPuzzleHistoryAssetCreatedAt(asset.createdAt)}的历史图片`
|
||||
}
|
||||
onSelect={onSelect}
|
||||
cardRadiusClassName="rounded-[1.25rem]"
|
||||
bodyClassName="px-4 py-3"
|
||||
/>
|
||||
</PlatformToolModalShell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user