b43a663fe2
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m5s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m26s
Project CI / Backend tests (pull_request) Successful in 4m5s
Project CI / Frontend tests (pull_request) Successful in 2m9s
Project CI / Native shell tests (pull_request) Successful in 6m3s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m46s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m28s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m42s
Project CI / Repository checks (pull_request) Failing after 2m3s
- 主站顶栏在所有断点保持单行:去掉顶栏与动作区的 flex-wrap,品牌和下载、泥点、账户入口不再折成两行
- 窄屏(<640px)下载入口收起为 44×44 图标按钮,保留 aria-label/title,≥640px 恢复「下载客户端」完整文案
- 窄屏(<480px)顶栏品牌只保留 IP 标识,让出空间给下载、泥点与账户入口,并收紧窄屏动作区间距
- 头像裁剪弹窗去掉 portal={false},改由 UnifiedModal 默认 portal 渲染到 document.body
- 个人中心两级弹窗壳层同步去掉 portal={false},昵称等个人中心弹窗一并回到页面级层级
- 弹窗不再受 .platform-desktop-layout 的 z-index:1 stacking context 限制,层级高于固定底部主菜单(z-index:60)
- 共享记忆补记单行顶栏口径与「内联弹窗被固定底栏压住」的排障经验
177 lines
6.2 KiB
TypeScript
177 lines
6.2 KiB
TypeScript
import { getPlatformActionButtonClassName } 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 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() {
|
||
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="secondary"
|
||
size="sm"
|
||
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"
|
||
onClick={() => {
|
||
setState({ status: 'loading' });
|
||
setOpen(true);
|
||
}}
|
||
>
|
||
<Download className="h-4 w-4 shrink-0" aria-hidden="true" />
|
||
<span className="hidden sm:inline">下载客户端</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>
|
||
</>
|
||
);
|
||
}
|