Files
Genarrative/src/components/creation-home/ClientDownloadEntry.tsx
T
k88936 688d3b103a 为游戏发布页添加客户端引导组件
- 优化工具栏布局
- 属性化客户端入口组件,支持按钮文案、样式及窄屏选项配置
2026-09-30 16:18:31 +08:00

209 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
getPlatformActionButtonClassName,
type PlatformActionButtonSize,
type PlatformActionButtonTone,
} from '@genarrative/shared/components';
import { Download, Monitor } from 'lucide-react';
import { useEffect, useState } from 'react';
import type { ClientDownloadResponse } from '../../../packages/shared/src/contracts/clientDownload';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
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 }
| { status: 'error' };
const platformNames = { windows: 'Windows', macos: 'macOS' } as const;
function downloadLabel(download: ClientDownloadResponse['downloads'][number]) {
if (download.platform === 'windows') return 'Windows 64 位';
return `macOS(${download.architecture === 'aarch64' ? 'Apple Silicon' : 'Intel'})`;
}
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' });
useEffect(() => {
if (!open) return;
const controller = new AbortController();
let disposed = false;
const timeout = window.setTimeout(() => {
if (disposed) return;
disposed = true;
controller.abort();
setState({ status: 'error' });
}, 15_000);
setState({ status: 'loading' });
void (async () => {
try {
const response = await fetch('/api/client-downloads', {
signal: controller.signal,
cache: 'no-store',
credentials: 'omit',
});
if (!response.ok) throw new Error(DOWNLOAD_ERROR);
const release = (await response.json()) as ClientDownloadResponse;
if (
!Array.isArray(release.downloads) ||
!Array.isArray(release.unavailablePlatforms)
) {
throw new Error(DOWNLOAD_ERROR);
}
if (!disposed) setState({ status: 'ready', release });
} catch {
if (!disposed) setState({ status: 'error' });
} finally {
window.clearTimeout(timeout);
}
})();
return () => {
disposed = true;
window.clearTimeout(timeout);
controller.abort();
};
}, [open, attempt]);
return (
<>
<PlatformActionButton
tone={tone}
size={size}
shape="pill"
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={iconOnlyOnNarrowScreens ? 'hidden sm:inline' : undefined}
>
{label}
</span>
</PlatformActionButton>
<UnifiedModal
open={open}
onClose={() => setOpen(false)}
title="下载陶泥儿客户端"
size="sm"
>
{state.status === 'loading' ? (
<PlatformStatusMessage tone="info" role="status">
正在获取最新版本…
</PlatformStatusMessage>
) : state.status === 'error' ? (
<div className="flex flex-col gap-4">
<PlatformStatusMessage tone="error" role="alert">
{DOWNLOAD_ERROR}
</PlatformStatusMessage>
<PlatformActionButton
onClick={() => setAttempt((value) => value + 1)}
>
重试
</PlatformActionButton>
</div>
) : (
<div className="flex flex-col gap-4">
{state.release.downloads.map((download) => (
<PlatformSubpanel
key={`${download.platform}-${download.architecture}`}
as="section"
aria-label={downloadLabel(download)}
padding="md"
radius="md"
>
<div className="mb-4 flex items-center gap-3">
<Monitor
className="h-7 w-7 shrink-0 text-[var(--platform-accent-strong)]"
aria-hidden="true"
/>
<div>
<p className="font-semibold text-[var(--platform-text-strong)]">
{downloadLabel(download)}
</p>
<p className="text-sm text-[var(--platform-text-soft)]">
最新版本 v{download.version}
</p>
</div>
</div>
<a
href={download.downloadUrl}
className={getPlatformActionButtonClassName({
size: 'md',
fullWidth: true,
})}
referrerPolicy="no-referrer"
>
<Download className="h-4 w-4 shrink-0" aria-hidden="true" />
{download.platform === 'windows'
? '下载 Windows 版'
: `下载 ${downloadLabel(download)}`}
</a>
</PlatformSubpanel>
))}
{state.release.downloads.length === 0 ? (
<PlatformStatusMessage tone="info" role="status">
暂无可下载版本
</PlatformStatusMessage>
) : null}
{state.release.unavailablePlatforms.length > 0 ? (
<PlatformStatusMessage tone="warning" role="alert">
{state.release.unavailablePlatforms
.map((platform) => platformNames[platform])
.join('、')}{' '}
版本暂时无法获取
</PlatformStatusMessage>
) : null}
{state.release.downloads.length === 0 ||
state.release.unavailablePlatforms.length > 0 ? (
<PlatformActionButton
tone="secondary"
onClick={() => setAttempt((value) => value + 1)}
>
重试
</PlatformActionButton>
) : null}
</div>
)}
</UnifiedModal>
</>
);
}