Repository navigation
Expand file tree
/
Copy pathInstallStates.tsx
More file actions
157 lines (153 loc) · 5.06 KB
/
Copy pathInstallStates.tsx
File metadata and controls
157 lines (153 loc) · 5.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import { useTranslation } from "react-i18next";
import type { LucideIcon } from "lucide-react";
import { CloudOff, Download, Globe, Loader2, RefreshCw, TimerOff } from "lucide-react";
import { Button } from "@App/pages/components/ui/button";
import { Progress } from "@App/pages/components/ui/progress";
import { StateScreen } from "@App/pages/components/ui/state-screen";
import { InstallTopBar } from "./InstallTopBar";
/**
* 状态屏外壳:保留品牌顶栏(对照设计稿,加载/失败态不丢失外壳),内容区垂直居中。
* chip 只在确知场景时才给——从更新页点进来的必然是更新,写死「脚本安装」会先闪一次错的上下文;
* 底部留一条与就绪态操作栏等高的占位,避免就绪瞬间内容区高度再跳一次。
*/
function StateShell({
title,
titleIcon,
children,
}: {
title?: string;
titleIcon?: LucideIcon;
children: React.ReactNode;
}) {
return (
<div className="flex h-dvh flex-col bg-background">
<InstallTopBar title={title} titleIcon={titleIcon} />
<main className="flex min-h-0 flex-1 flex-col items-center justify-center gap-5 px-6 text-center">
{children}
</main>
<div data-testid="state-action-placeholder" className="min-h-[68px] shrink-0 border-t border-border bg-card" />
</div>
);
}
export function InstallLoading({
source,
bytesText,
percent,
mode,
}: {
source?: string;
bytesText?: string;
percent?: number;
/** 已确知的安装/更新场景;未确知时不渲染上下文 chip,不猜 */
mode?: "install" | "update";
}) {
const { t } = useTranslation(["install", "common"]);
const isUpdate = mode === "update";
return (
<StateShell
title={mode ? (isUpdate ? t("install:context_update") : t("install:context_install")) : undefined}
titleIcon={isUpdate ? RefreshCw : Download}
>
<StateScreen
icon={Loader2}
iconClassName="animate-spin"
tone="primary"
compact
title={isUpdate ? t("install:loading_title_update") : t("install:loading_title")}
description={
// 只有 url= 入口才真的在下载;uuid 入口的代码服务端早已备好,说「正在下载」是假的
source
? t("install:loading_desc")
: isUpdate
? t("install:loading_desc_prepare_update")
: t("install:loading_desc_prepare")
}
progress={
<div className="flex flex-col items-center gap-3">
{source && (
<span className="flex items-center gap-1.5 rounded-full bg-muted px-3 py-1 text-xs text-muted-foreground">
<Globe className="size-3.5" />
{source}
</span>
)}
{bytesText && <span className="font-mono text-xs text-muted-foreground">{bytesText}</span>}
<Progress
aria-label={t("install:loading_title")}
value={percent}
indeterminate={typeof percent !== "number"}
className="w-80"
indicatorTestId={typeof percent === "number" ? "install-progress" : undefined}
/>
</div>
}
/>
</StateShell>
);
}
export function InstallError({
title,
message,
onRetry,
onClose,
}: {
title?: string;
message: string;
onRetry?: () => void;
onClose: () => void;
}) {
const { t } = useTranslation(["install", "common"]);
return (
<StateShell>
<StateScreen
data-testid="install-error"
icon={CloudOff}
tone="error"
compact
title={title ?? t("install:page_load_failed")}
detail={message}
action={
<div className="flex gap-3">
{onRetry && (
<Button onClick={onRetry} className="min-w-24">
{t("install:error_retry")}
</Button>
)}
<Button variant="outline" onClick={onClose} className="min-w-24">
{t("common:close")}
</Button>
</div>
}
/>
</StateShell>
);
}
/**
* 待安装代码已过期。这条路的失败几乎只有一个原因——暂存条目被定时清理清掉,
* 此时「重试」重跑同一段取数必然还是同样结果,出口必须换成让服务端重新备料。
*/
export function InstallExpired({ onRecheck, onClose }: { onRecheck: () => void; onClose: () => void }) {
const { t } = useTranslation(["install", "common"]);
return (
<StateShell title={t("install:context_update")} titleIcon={RefreshCw}>
<StateScreen
data-testid="install-expired"
icon={TimerOff}
tone="error"
compact
title={t("install:expired_title")}
description={t("install:expired_desc")}
action={
<div className="flex gap-3">
<Button onClick={onRecheck} className="min-w-24">
<RefreshCw />
{t("install:expired_recheck")}
</Button>
<Button variant="outline" onClick={onClose} className="min-w-24">
{t("common:close")}
</Button>
</div>
}
/>
</StateShell>
);
}