688d3b103a
- 优化工具栏布局 - 属性化客户端入口组件,支持按钮文案、样式及窄屏选项配置
209 lines
7.0 KiB
TypeScript
209 lines
7.0 KiB
TypeScript
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>
|
||
</>
|
||
);
|
||
}
|