7f8400fd3a
将 PlatformAsyncStatePanel 扩展到公共素材选择网格 将 PlatformAsyncStatePanel 扩展到视觉小说存档面板与账号安全子区块 将兑换码弹窗提交动作改为使用标准 profile modal footer 补充对应测试并更新 PlatformUiKit 收口计划与共享决策记录
123 lines
4.0 KiB
TypeScript
123 lines
4.0 KiB
TypeScript
import { Bookmark, Loader2, Play } from 'lucide-react';
|
|
|
|
import type { ProfileSaveArchiveSummary } from '../../../packages/shared/src/contracts/runtime';
|
|
import type { VisualNovelRunSnapshot } from '../../../packages/shared/src/contracts/visualNovel';
|
|
import { PlatformActionButton } from '../common/PlatformActionButton';
|
|
import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel';
|
|
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
|
import { PlatformIconBadge } from '../common/PlatformIconBadge';
|
|
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
|
|
|
type VisualNovelSavePanelProps = {
|
|
run: VisualNovelRunSnapshot;
|
|
saveArchives?: ProfileSaveArchiveSummary[];
|
|
isSaving?: boolean;
|
|
isLoadingArchives?: boolean;
|
|
resumingWorldKey?: string | null;
|
|
onSaveRun?: () => void;
|
|
onResumeSaveArchive?: (worldKey: string) => void;
|
|
};
|
|
|
|
function formatArchiveTime(value: string) {
|
|
const timestamp = new Date(value).getTime();
|
|
if (Number.isNaN(timestamp)) {
|
|
return value;
|
|
}
|
|
|
|
return new Intl.DateTimeFormat('zh-CN', {
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}).format(timestamp);
|
|
}
|
|
|
|
export function VisualNovelSavePanel({
|
|
run,
|
|
saveArchives = [],
|
|
isSaving = false,
|
|
isLoadingArchives = false,
|
|
resumingWorldKey = null,
|
|
onSaveRun,
|
|
onResumeSaveArchive,
|
|
}: VisualNovelSavePanelProps) {
|
|
return (
|
|
<div className="space-y-3">
|
|
<PlatformActionButton
|
|
disabled={!onSaveRun || isSaving}
|
|
onClick={onSaveRun}
|
|
shape="pill"
|
|
fullWidth
|
|
className="min-h-12"
|
|
>
|
|
{isSaving ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Bookmark className="h-4 w-4" />
|
|
)}
|
|
<span>{isSaving ? '保存中' : '保存'}</span>
|
|
</PlatformActionButton>
|
|
|
|
<PlatformAsyncStatePanel
|
|
isLoading={isLoadingArchives}
|
|
loadingState={
|
|
<PlatformEmptyState surface="subpanel" size="inline">
|
|
读取中
|
|
</PlatformEmptyState>
|
|
}
|
|
isEmpty={saveArchives.length === 0}
|
|
emptyState={
|
|
<PlatformEmptyState surface="subpanel" size="inline">
|
|
暂无存档
|
|
</PlatformEmptyState>
|
|
}
|
|
>
|
|
<div className="grid gap-3">
|
|
{saveArchives.map((entry) => {
|
|
const isResuming = resumingWorldKey === entry.worldKey;
|
|
return (
|
|
<PlatformSubpanel
|
|
key={entry.worldKey}
|
|
as="button"
|
|
type="button"
|
|
disabled={isResuming || !onResumeSaveArchive}
|
|
onClick={() => onResumeSaveArchive?.(entry.worldKey)}
|
|
surface="flat"
|
|
radius="sm"
|
|
padding="sm"
|
|
interactive
|
|
className="flex min-h-20 items-center gap-3"
|
|
>
|
|
<PlatformIconBadge
|
|
size="md"
|
|
shape="rounded"
|
|
icon={
|
|
isResuming ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Play className="h-4 w-4" />
|
|
)
|
|
}
|
|
/>
|
|
<span className="min-w-0 flex-1">
|
|
<span className="block truncate text-sm font-black text-[var(--platform-text-strong)]">
|
|
{entry.worldName || run.profileId}
|
|
</span>
|
|
<span className="mt-1 block truncate text-xs font-semibold text-[var(--platform-text-soft)]">
|
|
{entry.subtitle || entry.summaryText || run.runId}
|
|
</span>
|
|
<span className="mt-1 block text-[11px] font-bold text-[var(--platform-text-soft)]">
|
|
{formatArchiveTime(entry.lastPlayedAt)}
|
|
</span>
|
|
</span>
|
|
</PlatformSubpanel>
|
|
);
|
|
})}
|
|
</div>
|
|
</PlatformAsyncStatePanel>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default VisualNovelSavePanel;
|