103 lines
3.9 KiB
TypeScript
103 lines
3.9 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';
|
|
|
|
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">
|
|
<button
|
|
type="button"
|
|
disabled={!onSaveRun || isSaving}
|
|
onClick={onSaveRun}
|
|
className="flex min-h-12 w-full items-center justify-center gap-2 rounded-full border border-[var(--platform-subpanel-border)] bg-[var(--platform-button-primary-fill)] px-4 text-sm font-black text-white transition hover:brightness-105 disabled:cursor-not-allowed disabled:opacity-55"
|
|
>
|
|
{isSaving ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Bookmark className="h-4 w-4" />
|
|
)}
|
|
<span>{isSaving ? '保存中' : '保存'}</span>
|
|
</button>
|
|
|
|
{isLoadingArchives ? (
|
|
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/74 px-4 py-5 text-center text-sm font-semibold text-[var(--platform-text-soft)]">
|
|
读取中
|
|
</div>
|
|
) : saveArchives.length > 0 ? (
|
|
<div className="grid gap-3">
|
|
{saveArchives.map((entry) => {
|
|
const isResuming = resumingWorldKey === entry.worldKey;
|
|
return (
|
|
<button
|
|
key={entry.worldKey}
|
|
type="button"
|
|
disabled={isResuming || !onResumeSaveArchive}
|
|
onClick={() => onResumeSaveArchive?.(entry.worldKey)}
|
|
className="flex min-h-20 items-center gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/78 p-3 text-left transition hover:bg-white disabled:cursor-not-allowed disabled:opacity-55"
|
|
>
|
|
<span className="grid h-11 w-11 shrink-0 place-items-center rounded-[0.85rem] bg-[var(--platform-neutral-bg)] text-[var(--platform-neutral-text)]">
|
|
{isResuming ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Play className="h-4 w-4" />
|
|
)}
|
|
</span>
|
|
<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>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/74 px-4 py-5 text-center text-sm font-semibold text-[var(--platform-text-soft)]">
|
|
暂无存档
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default VisualNovelSavePanel;
|