收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
@@ -11,6 +11,28 @@ vi.mock('../../services/jump-hop/useJumpHopLeaderboard', () => ({
|
||||
useJumpHopLeaderboard: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../ResolvedAssetImage', () => ({
|
||||
ResolvedAssetImage: ({
|
||||
src,
|
||||
alt,
|
||||
className,
|
||||
refreshKey,
|
||||
}: {
|
||||
src?: string | null;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
refreshKey?: string | number | null;
|
||||
}) =>
|
||||
src ? (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={className}
|
||||
data-refresh-key={refreshKey ?? undefined}
|
||||
/>
|
||||
) : null,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(useJumpHopLeaderboard).mockReturnValue({
|
||||
@@ -62,7 +84,17 @@ test('跳一跳结果页展示排行榜列表', () => {
|
||||
);
|
||||
|
||||
expect(screen.getByText('排行榜')).toBeTruthy();
|
||||
expect(screen.getByText('排行榜').closest('div.bg-white\\/70')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText('排行榜').closest('div.bg-white\\/70')?.className,
|
||||
).toContain('border-[var(--platform-subpanel-border)]');
|
||||
expect(screen.getByText('陶泥儿玩家')).toBeTruthy();
|
||||
const leaderboardEntry = screen.getAllByTestId(
|
||||
'jump-hop-leaderboard-entry',
|
||||
)[0];
|
||||
expect(leaderboardEntry?.className).toContain('bg-white/72');
|
||||
expect(leaderboardEntry?.className).toContain('rounded-[1rem]');
|
||||
expect(leaderboardEntry?.className).toContain('p-3');
|
||||
expect(screen.queryByText('user-secret-1')).toBeNull();
|
||||
expect(screen.getByText('12 跳')).toBeTruthy();
|
||||
expect(screen.getByText('00:40')).toBeTruthy();
|
||||
@@ -70,6 +102,27 @@ test('跳一跳结果页展示排行榜列表', () => {
|
||||
expect(screen.queryByText('user-secret-2')).toBeNull();
|
||||
});
|
||||
|
||||
test('跳一跳排行榜空态使用平台空态外壳', () => {
|
||||
render(
|
||||
<JumpHopResultView
|
||||
profile={buildProfile({ publicationStatus: 'published' })}
|
||||
onBack={() => {}}
|
||||
onEdit={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
onPublish={() => {}}
|
||||
onRegenerateTiles={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const emptyState = screen.getByTestId('jump-hop-leaderboard-empty');
|
||||
|
||||
expect(emptyState.textContent).toContain('暂无成绩');
|
||||
expect(emptyState.className).toContain('bg-white/74');
|
||||
expect(emptyState.className).toContain('rounded-[1rem]');
|
||||
expect(emptyState.className).toContain('px-2');
|
||||
expect(emptyState.className).toContain('py-2');
|
||||
});
|
||||
|
||||
test('跳一跳结果页默认角色预览使用陶泥儿透明 logo', () => {
|
||||
render(
|
||||
<JumpHopResultView
|
||||
@@ -82,9 +135,125 @@ test('跳一跳结果页默认角色预览使用陶泥儿透明 logo', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('jump-hop-result-character-logo').getAttribute('src')).toBe(
|
||||
'/branding/jump-hop-taonier-character.png',
|
||||
expect(
|
||||
screen.getByTestId('jump-hop-result-character-logo').getAttribute('src'),
|
||||
).toBe('/branding/jump-hop-taonier-character.png');
|
||||
});
|
||||
|
||||
test('跳一跳结果页标准白底面板使用 PlatformSubpanel 外壳', () => {
|
||||
const { container } = render(
|
||||
<JumpHopResultView
|
||||
profile={buildProfile()}
|
||||
onBack={() => {}}
|
||||
onEdit={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
onPublish={() => {}}
|
||||
onRegenerateTiles={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const panels = Array.from(
|
||||
container.querySelectorAll('section.platform-subpanel'),
|
||||
);
|
||||
const mediaFrames = Array.from(
|
||||
container.querySelectorAll('div.bg-white\\/80'),
|
||||
);
|
||||
const actionPanel = screen.getByText('结果操作').closest('section');
|
||||
|
||||
expect(panels).toHaveLength(2);
|
||||
for (const panel of panels) {
|
||||
expect(panel.className).toContain('rounded-[1.25rem]');
|
||||
expect(panel.className).toContain('p-4');
|
||||
}
|
||||
expect(mediaFrames).toHaveLength(3);
|
||||
for (const frame of mediaFrames) {
|
||||
expect(frame.className).toContain(
|
||||
'border-[var(--platform-subpanel-border)]',
|
||||
);
|
||||
expect(frame.className).toContain('rounded-[1rem]');
|
||||
}
|
||||
expect(actionPanel?.className).toContain('flex');
|
||||
expect(screen.getByText('结果操作').className).toContain('tracking-[0.18em]');
|
||||
});
|
||||
|
||||
test('跳一跳结果页图集整图预览使用 PlatformMediaFrame', () => {
|
||||
const profile = buildProfile({ tileAtlasImageSrc: '/jump-hop/atlas.png' });
|
||||
|
||||
const { container } = render(
|
||||
<JumpHopResultView
|
||||
profile={profile}
|
||||
onBack={() => {}}
|
||||
onEdit={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
onPublish={() => {}}
|
||||
onRegenerateTiles={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const atlasImage = container.querySelector('img[src="/jump-hop/atlas.png"]');
|
||||
const mediaFrame = atlasImage?.closest('div.relative');
|
||||
|
||||
expect(atlasImage).toBeTruthy();
|
||||
expect(atlasImage?.getAttribute('data-refresh-key')).toBe('asset-atlas');
|
||||
expect(mediaFrame?.className).toContain('aspect-square');
|
||||
expect(mediaFrame?.className).toContain('rounded-none');
|
||||
expect(mediaFrame?.className).toContain('bg-white/78');
|
||||
expect(mediaFrame?.className).not.toContain(
|
||||
'bg-[var(--platform-subpanel-fill)]',
|
||||
);
|
||||
});
|
||||
|
||||
test('跳一跳结果页地块池预览使用 PlatformMediaTileGrid', () => {
|
||||
const { container } = render(
|
||||
<JumpHopResultView
|
||||
profile={buildProfile({
|
||||
tileAssets: [
|
||||
{
|
||||
tileId: 'tile-1',
|
||||
tileType: 'normal',
|
||||
imageSrc: '/jump-hop/tile-1.png',
|
||||
imageObjectKey: '',
|
||||
assetObjectId: 'asset-tile-1',
|
||||
sourceAtlasCell: '0:0:0',
|
||||
visualWidth: 1,
|
||||
visualHeight: 1,
|
||||
topSurfaceRadius: 0.42,
|
||||
landingRadius: 0.36,
|
||||
},
|
||||
{
|
||||
tileId: 'tile-2',
|
||||
tileType: 'bonus',
|
||||
imageSrc: '',
|
||||
imageObjectKey: '',
|
||||
assetObjectId: 'asset-tile-2',
|
||||
sourceAtlasCell: '1:0:0',
|
||||
visualWidth: 1,
|
||||
visualHeight: 1,
|
||||
topSurfaceRadius: 0.42,
|
||||
landingRadius: 0.36,
|
||||
},
|
||||
],
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onEdit={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
onPublish={() => {}}
|
||||
onRegenerateTiles={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const grid = container.querySelector('.platform-media-tile-grid');
|
||||
const image = container.querySelector('img[src="/jump-hop/tile-1.png"]');
|
||||
const tileItems = container.querySelectorAll(
|
||||
'.platform-media-tile-grid__item',
|
||||
);
|
||||
|
||||
expect(grid?.className).toContain('grid-cols-5');
|
||||
expect(grid?.className).toContain('aspect-[1/1]');
|
||||
expect(grid?.className).toContain('bg-white/78');
|
||||
expect(image?.getAttribute('data-refresh-key')).toBe('asset-tile-1');
|
||||
expect(image?.className).toContain('object-contain');
|
||||
expect(tileItems).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('跳一跳结果页根容器允许移动端向下滚动到操作按钮', () => {
|
||||
@@ -124,8 +293,12 @@ test('跳一跳草稿结果页不请求公开排行榜', () => {
|
||||
function buildProfile(
|
||||
options: {
|
||||
publicationStatus?: JumpHopWorkProfileResponse['summary']['publicationStatus'];
|
||||
tileAtlasImageSrc?: string | null;
|
||||
tileAssets?: JumpHopWorkProfileResponse['tileAssets'];
|
||||
} = {},
|
||||
): JumpHopWorkProfileResponse {
|
||||
const tileAtlasImageSrc =
|
||||
options.tileAtlasImageSrc ?? 'builtin://jump-hop/default-character';
|
||||
return {
|
||||
summary: {
|
||||
runtimeKind: 'jump-hop',
|
||||
@@ -178,16 +351,16 @@ function buildProfile(
|
||||
height: 0,
|
||||
},
|
||||
tileAtlasAsset: {
|
||||
assetId: 'builtin',
|
||||
imageSrc: 'builtin://jump-hop/default-character',
|
||||
assetId: 'asset-atlas',
|
||||
imageSrc: tileAtlasImageSrc,
|
||||
imageObjectKey: '',
|
||||
assetObjectId: 'builtin',
|
||||
assetObjectId: 'asset-atlas',
|
||||
generationProvider: 'builtin-three',
|
||||
prompt: '默认角色',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
tileAssets: [],
|
||||
tileAssets: options.tileAssets ?? [],
|
||||
path: null,
|
||||
coverComposite: null,
|
||||
backButtonAsset: null,
|
||||
@@ -212,16 +385,16 @@ function buildProfile(
|
||||
height: 0,
|
||||
},
|
||||
tileAtlasAsset: {
|
||||
assetId: 'builtin',
|
||||
imageSrc: 'builtin://jump-hop/default-character',
|
||||
assetId: 'asset-atlas',
|
||||
imageSrc: tileAtlasImageSrc,
|
||||
imageObjectKey: '',
|
||||
assetObjectId: 'builtin',
|
||||
assetObjectId: 'asset-atlas',
|
||||
generationProvider: 'builtin-three',
|
||||
prompt: '默认角色',
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
tileAssets: [],
|
||||
tileAssets: options.tileAssets ?? [],
|
||||
backButtonAsset: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
ArrowLeft,
|
||||
Loader2,
|
||||
Play,
|
||||
Send,
|
||||
Shuffle,
|
||||
} from 'lucide-react';
|
||||
import { ArrowLeft, Loader2, Play, Send, Shuffle } from 'lucide-react';
|
||||
import { type CSSProperties, useState } from 'react';
|
||||
|
||||
import type {
|
||||
@@ -18,6 +12,12 @@ import {
|
||||
selectJumpHopTileAsset,
|
||||
} from '../../services/jump-hop/jumpHopRuntimeModel';
|
||||
import { useJumpHopLeaderboard } from '../../services/jump-hop/useJumpHopLeaderboard';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
||||
import { PlatformMediaFrame } from '../common/PlatformMediaFrame';
|
||||
import { PlatformMediaTileGrid } from '../common/PlatformMediaTileGrid';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
|
||||
type JumpHopResultViewProps = {
|
||||
@@ -89,58 +89,69 @@ function JumpHopTilePoolPreview({
|
||||
const atlasRefreshKey = tileAtlasAsset?.assetObjectId || atlasSrc;
|
||||
if (visibleTiles.length > 0) {
|
||||
return (
|
||||
<div className="grid aspect-[1/1] grid-cols-5 gap-1 bg-white/78 p-2">
|
||||
{visibleTiles.map((tile, index) => (
|
||||
<div
|
||||
key={tile.tileId ?? `${tile.sourceAtlasCell}-${index}`}
|
||||
className="grid min-h-0 place-items-center overflow-hidden rounded-[0.45rem] border border-white/80 bg-slate-50"
|
||||
>
|
||||
{tile.imageSrc ? (
|
||||
<ResolvedAssetImage
|
||||
src={tile.imageSrc}
|
||||
refreshKey={tile.assetObjectId}
|
||||
alt=""
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="h-4 w-4 rounded-full"
|
||||
style={{
|
||||
background:
|
||||
tileToneByType[tile.tileType] ?? tileToneByType.normal,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<PlatformMediaTileGrid
|
||||
columns="five"
|
||||
gap="xs"
|
||||
aspect="square"
|
||||
surface="soft"
|
||||
tileSurface="slate"
|
||||
imageClassName="h-full w-full object-contain"
|
||||
items={visibleTiles.map((tile, index) => ({
|
||||
id: tile.tileId ?? `${tile.sourceAtlasCell}-${index}`,
|
||||
src: tile.imageSrc,
|
||||
refreshKey: tile.assetObjectId,
|
||||
fallbackLabel: '地块',
|
||||
fallbackContent: (
|
||||
<span
|
||||
className="h-4 w-4 rounded-full"
|
||||
style={{
|
||||
background:
|
||||
tileToneByType[tile.tileType] ?? tileToneByType.normal,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (atlasSrc) {
|
||||
return (
|
||||
<ResolvedAssetImage
|
||||
<PlatformMediaFrame
|
||||
src={atlasSrc}
|
||||
refreshKey={atlasRefreshKey}
|
||||
alt=""
|
||||
className="aspect-[1/1] w-full object-cover"
|
||||
fallbackLabel="地块图集"
|
||||
aspect="square"
|
||||
surface="none"
|
||||
className="rounded-none bg-white/78"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid aspect-[1/1] grid-cols-5 gap-1 bg-white/78 p-2">
|
||||
{Array.from({ length: 25 }).map((_, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="rounded-[0.45rem] border border-white/80"
|
||||
style={{
|
||||
background:
|
||||
Object.values(tileToneByType)[index % Object.values(tileToneByType).length],
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<PlatformMediaTileGrid
|
||||
columns="five"
|
||||
gap="xs"
|
||||
aspect="square"
|
||||
surface="soft"
|
||||
tileSurface="bare"
|
||||
items={Array.from({ length: 25 }).map((_, index) => ({
|
||||
id: `fallback-${index}`,
|
||||
fallbackLabel: '地块',
|
||||
fallbackContent: (
|
||||
<span
|
||||
className="h-full w-full"
|
||||
style={{
|
||||
background:
|
||||
Object.values(tileToneByType)[
|
||||
index % Object.values(tileToneByType).length
|
||||
],
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -213,7 +224,13 @@ function JumpHopResultLeaderboard({
|
||||
const items = leaderboard?.items ?? [];
|
||||
|
||||
return (
|
||||
<div className="mt-4 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/70 p-3">
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
surface="flat"
|
||||
radius="sm"
|
||||
padding="sm"
|
||||
className="mt-4 bg-white/70"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-sm font-black text-[var(--platform-text-strong)]">
|
||||
排行榜
|
||||
@@ -224,9 +241,14 @@ function JumpHopResultLeaderboard({
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2">
|
||||
{items.slice(0, 5).map((entry) => (
|
||||
<div
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
key={`${entry.rank}-${entry.playerId}`}
|
||||
className="grid grid-cols-[1.8rem_minmax(0,1fr)_auto_auto] items-center gap-2 rounded-[0.75rem] bg-white/70 px-2 py-2 text-xs font-bold text-[var(--platform-text-base)]"
|
||||
surface="flat"
|
||||
radius="sm"
|
||||
padding="sm"
|
||||
className="grid grid-cols-[1.8rem_minmax(0,1fr)_auto_auto] items-center gap-2 text-xs font-bold text-[var(--platform-text-base)]"
|
||||
data-testid="jump-hop-leaderboard-entry"
|
||||
>
|
||||
<span className="text-[var(--platform-text-soft)]">
|
||||
{entry.rank}
|
||||
@@ -236,15 +258,20 @@ function JumpHopResultLeaderboard({
|
||||
</span>
|
||||
<span>{entry.successfulJumpCount} 跳</span>
|
||||
<span>{formatJumpHopDurationLabel(entry.durationMs)}</span>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
))}
|
||||
{items.length === 0 ? (
|
||||
<div className="rounded-[0.75rem] bg-white/60 px-2 py-2 text-xs font-bold text-[var(--platform-text-soft)]">
|
||||
<PlatformEmptyState
|
||||
surface="subpanel"
|
||||
size="compact"
|
||||
className="px-2 py-2 text-left text-xs font-bold"
|
||||
data-testid="jump-hop-leaderboard-empty"
|
||||
>
|
||||
{error ?? '暂无成绩'}
|
||||
</div>
|
||||
</PlatformEmptyState>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -289,7 +316,7 @@ export function JumpHopResultView({
|
||||
profile.pathPreviewImageSrc?.trim() ||
|
||||
tileAtlasAsset?.imageSrc?.trim() ||
|
||||
tileAssets.length > 0 ||
|
||||
path?.platforms.length,
|
||||
Boolean(path?.platforms.length),
|
||||
);
|
||||
|
||||
const handlePublish = async () => {
|
||||
@@ -304,29 +331,31 @@ export function JumpHopResultView({
|
||||
return (
|
||||
<div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col overflow-y-auto overscroll-contain px-3 pb-[max(1.5rem,env(safe-area-inset-bottom))] pt-3 sm:px-4 sm:pt-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
onClick={onBack}
|
||||
className="platform-button platform-button--ghost min-h-0 px-3 py-2 text-sm"
|
||||
tone="ghost"
|
||||
size="xs"
|
||||
className="min-h-0 gap-2 py-2 text-sm"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
返回
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
onClick={onRegenerateTiles}
|
||||
disabled={isBusy}
|
||||
className="platform-button platform-button--ghost min-h-0 px-3 py-2 text-sm"
|
||||
tone="ghost"
|
||||
size="xs"
|
||||
className="min-h-0 gap-2 py-2 text-sm"
|
||||
>
|
||||
<Shuffle className="h-4 w-4" />
|
||||
地块
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-0 flex-1 gap-3 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,0.95fr)]">
|
||||
<section className="platform-subpanel rounded-[1.25rem] p-4">
|
||||
<PlatformSubpanel>
|
||||
<div className="text-2xl font-black text-[var(--platform-text-strong)]">
|
||||
{title}
|
||||
</div>
|
||||
@@ -336,73 +365,99 @@ export function JumpHopResultView({
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-3">
|
||||
<div className="overflow-hidden rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/80">
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
surface="flat"
|
||||
radius="sm"
|
||||
padding="none"
|
||||
className="overflow-hidden bg-white/80"
|
||||
>
|
||||
<JumpHopDefaultCharacterPreview />
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/80">
|
||||
</PlatformSubpanel>
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
surface="flat"
|
||||
radius="sm"
|
||||
padding="none"
|
||||
className="overflow-hidden bg-white/80"
|
||||
>
|
||||
<JumpHopTilePoolPreview
|
||||
tileAssets={tileAssets}
|
||||
tileAtlasAsset={tileAtlasAsset}
|
||||
tileAtlasFallbackSrc={
|
||||
('tileAtlasImageSrc' in profile
|
||||
? profile.tileAtlasImageSrc
|
||||
: null) ??
|
||||
null
|
||||
: null) ?? null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/80">
|
||||
</PlatformSubpanel>
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
surface="flat"
|
||||
radius="sm"
|
||||
padding="none"
|
||||
className="overflow-hidden bg-white/80"
|
||||
>
|
||||
<JumpHopFirstPlatformsPreview
|
||||
path={path}
|
||||
tileAssets={tileAssets}
|
||||
/>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
</div>
|
||||
{!hasAssets ? (
|
||||
<div className="platform-banner platform-banner--neutral mt-3 rounded-2xl text-sm leading-6">
|
||||
<PlatformStatusMessage
|
||||
tone="neutral"
|
||||
surface="platform"
|
||||
size="md"
|
||||
className="mt-3 rounded-2xl"
|
||||
>
|
||||
生成资源尚未准备完成。
|
||||
</div>
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
</section>
|
||||
</PlatformSubpanel>
|
||||
|
||||
<section className="platform-subpanel flex flex-col rounded-[1.25rem] p-4">
|
||||
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
|
||||
结果操作
|
||||
</div>
|
||||
<PlatformSubpanel title="结果操作" className="flex flex-col">
|
||||
{canShowLeaderboard ? (
|
||||
<JumpHopResultLeaderboard profileId={profileId} />
|
||||
) : null}
|
||||
{error ? (
|
||||
<div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
surface="platform"
|
||||
size="md"
|
||||
className="mt-3"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
<div className="mt-auto grid gap-2 pt-3">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
onClick={onEdit}
|
||||
disabled={isBusy}
|
||||
className="platform-button platform-button--ghost min-h-11 justify-center gap-2 px-4 py-3 text-sm"
|
||||
tone="ghost"
|
||||
size="md"
|
||||
className="min-h-11 gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
返回编辑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</PlatformActionButton>
|
||||
<PlatformActionButton
|
||||
onClick={onStartTestRun}
|
||||
disabled={isBusy}
|
||||
className="platform-button platform-button--secondary min-h-11 justify-center gap-2 px-4 py-3 text-sm"
|
||||
tone="secondary"
|
||||
size="md"
|
||||
className="min-h-11 gap-2"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
试玩
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</PlatformActionButton>
|
||||
<PlatformActionButton
|
||||
onClick={() => {
|
||||
void handlePublish();
|
||||
}}
|
||||
disabled={isBusy || isPublishing}
|
||||
className="platform-button platform-button--primary min-h-11 justify-center gap-2 px-4 py-3 text-sm"
|
||||
size="md"
|
||||
className="min-h-11 gap-2"
|
||||
>
|
||||
{isPublishing ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
@@ -410,9 +465,9 @@ export function JumpHopResultView({
|
||||
<Send className="h-4 w-4" />
|
||||
)}
|
||||
发布
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</section>
|
||||
</PlatformSubpanel>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user