Merge pull request 'Feat/发布页引导用户使用客户端' (#557) from feat/guide-user-use-client into master
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 1m14s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 1m14s
Project CI / AI game creator shell Rust smoke (push) Failing after 1m13s
Project CI / AI game creator shell Rust crates (push) Successful in 1m37s
Project CI / Frontend tests (push) Successful in 3m37s
Project CI / Repository checks (push) Successful in 4m18s
Project CI / AI game creator shell web tests (push) Successful in 2m24s
Project CI / Backend tests (push) Successful in 6m42s
Project CI / Native shell tests (push) Successful in 7m35s

Reviewed-on: #557
This commit was merged in pull request #557.
This commit is contained in:
2026-09-30 16:35:20 +08:00
3 changed files with 82 additions and 11 deletions
@@ -1,4 +1,8 @@
import { getPlatformActionButtonClassName } from '@genarrative/shared/components';
import {
getPlatformActionButtonClassName,
type PlatformActionButtonSize,
type PlatformActionButtonTone,
} from '@genarrative/shared/components';
import { Download, Monitor } from 'lucide-react';
import { useEffect, useState } from 'react';
@@ -10,6 +14,16 @@ import { UnifiedModal } from '../common/UnifiedModal';
const DOWNLOAD_ERROR = '暂时无法获取最新版本,请稍后重试';
type ClientDownloadEntryProps = {
/** 按钮文案,默认「下载客户端」。 */
label?: string;
tone?: PlatformActionButtonTone;
size?: PlatformActionButtonSize;
/** 窄屏只留图标:平台顶栏默认开启,页面级引导入口传 false 保留完整文案。 */
iconOnlyOnNarrowScreens?: boolean;
className?: string;
};
type DownloadState =
| { status: 'loading' }
| { status: 'ready'; release: ClientDownloadResponse }
@@ -22,7 +36,13 @@ function downloadLabel(download: ClientDownloadResponse['downloads'][number]) {
return `macOS(${download.architecture === 'aarch64' ? 'Apple Silicon' : 'Intel'})`;
}
export function ClientDownloadEntry() {
export function ClientDownloadEntry({
label = '下载客户端',
tone = 'secondary',
size = 'sm',
iconOnlyOnNarrowScreens = true,
className,
}: ClientDownloadEntryProps = {}) {
const [open, setOpen] = useState(false);
const [attempt, setAttempt] = useState(0);
const [state, setState] = useState<DownloadState>({ status: 'loading' });
@@ -72,19 +92,31 @@ export function ClientDownloadEntry() {
return (
<>
<PlatformActionButton
tone="secondary"
size="sm"
tone={tone}
size={size}
shape="pill"
aria-label="下载客户端"
title="下载客户端"
className="min-h-11 shrink-0 gap-0 whitespace-nowrap max-sm:h-11 max-sm:w-11 max-sm:px-0 sm:gap-2 sm:px-4"
aria-label={label}
title={label}
className={[
'shrink-0 whitespace-nowrap',
iconOnlyOnNarrowScreens
? 'min-h-11 gap-0 max-sm:h-11 max-sm:w-11 max-sm:px-0 sm:gap-2 sm:px-4'
: 'gap-2',
className,
]
.filter(Boolean)
.join(' ')}
onClick={() => {
setState({ status: 'loading' });
setOpen(true);
}}
>
<Download className="h-4 w-4 shrink-0" aria-hidden="true" />
<span className="hidden sm:inline">下载客户端</span>
<span
className={iconOnlyOnNarrowScreens ? 'hidden sm:inline' : undefined}
>
{label}
</span>
</PlatformActionButton>
<UnifiedModal
open={open}
@@ -0,0 +1,35 @@
import type { HTMLAttributes } from 'react';
import { ClientDownloadEntry } from '../creation-home/ClientDownloadEntry';
type GamePublishClientGuideProps = HTMLAttributes<HTMLElement>;
/** 悬浮引导卡片:把还没有游戏包的作者引到客户端。 */
export function GamePublishClientGuide({
className,
...props
}: GamePublishClientGuideProps) {
return (
<section
className={`flex w-fit max-w-full flex-none flex-col items-center gap-3
rounded-2xl border px-[1.15rem] py-[0.9rem] text-center
[background:linear-gradient(135deg,color-mix(in_srgb,var(--platform-action-accent,#c7653d)_11%,var(--platform-panel-fill))_0%,color-mix(in_srgb,var(--platform-action-accent,#c7653d)_4%,var(--platform-panel-fill))_100%)]
border-[color-mix(in_srgb,var(--platform-action-accent,#c7653d)_26%,transparent)]
[box-shadow:0_0.55rem_1.1rem_color-mix(in_srgb,var(--platform-action-accent,#c7653d)_12%,transparent)]
${className ?? ''}`}
{...props}
>
<strong className="text-[0.82rem] text-(--platform-text-strong)">
还没有游戏包?
</strong>
<ClientDownloadEntry
label="下载客户端制作游戏"
tone="accentSoft"
size="sm"
iconOnlyOnNarrowScreens={false}
/>
</section>
);
}
export default GamePublishClientGuide;
@@ -29,6 +29,7 @@ import {
resolveGamePublishImagePreview,
uploadGamePublishImageAsset,
} from './gamePublishAssets';
import { GamePublishClientGuide } from './GamePublishClientGuide';
import {
clearPublishDraft,
type GamePublishDraft,
@@ -711,12 +712,12 @@ export function GamePublishPage({
<button type="button" className="game-back-button" onClick={onBack}>
<ArrowLeft /> 返回
</button>
<div className="game-toolbar">
<div className="game-toolbar__title">
<div className="game-toolbar [flex-wrap:nowrap] max-[720px]:[flex-wrap:wrap]">
<div className="game-toolbar__title [min-width:0]">
<span className="game-eyebrow">
{updateGameId ? '发布新版本' : '发布游戏'}
</span>
<h2>
<h2 className="[white-space:nowrap] max-[720px]:[white-space:normal]">
{updateGameId
? updateContext
? `为《${updateContext.title}》发布 v${updateContext.versionNumber + 1}`
@@ -724,6 +725,9 @@ export function GamePublishPage({
: '上传可在线游玩的网页游戏'}
</h2>
</div>
{!updateGameId ? (
<GamePublishClientGuide className="ml-auto max-[720px]:ml-0" />
) : null}
</div>
{error ? (