返回 DeepSeek-Reasonix
StorageSettingsPage.tsx
根目录 / desktop / frontend / src / components / StorageSettingsPage.tsx
1 import { useCallback, useEffect, useState, type ReactNode } from "react";
2 import { Copy, Check } from "lucide-react";
3 import { app } from "../lib/bridge";
4 import { useT } from "../lib/i18n";
5 import { HistoricalImportList } from "./HistoricalImportList";
6 import { HistoricalRecoveryList } from "./HistoricalRecoveryList";
7 import { useManagementT } from "../lib/managementLocale";
8
9 type StorageSettingsView = Awaited<ReturnType<typeof app.StorageSettings>>;
10
11 export function StorageSettingsPage() {
12 const t = useT();
13 const m = useManagementT();
14 const [view, setView] = useState<StorageSettingsView | null>(null);
15 const [loading, setLoading] = useState(true);
16 const [failed, setFailed] = useState(false);
17
18 const load = useCallback(async () => {
19 setLoading(true);
20 setFailed(false);
21 try {
22 setView(await app.StorageSettings());
23 } catch {
24 setView(null);
25 setFailed(true);
26 } finally {
27 setLoading(false);
28 }
29 }, []);
30
31 useEffect(() => {
32 void load();
33 }, [load]);
34
35 if (loading) return <div className="empty">{t("settings.loading")}</div>;
36 if (failed || !view) {
37 return (
38 <div className="banner banner--error settings-load-error" role="alert">
39 <span>{t("settings.loadFailed")}</span>
40 <button className="btn btn--small" type="button" onClick={() => void load()}>{t("common.retry")}</button>
41 </div>
42 );
43 }
44
45 return (<>
46 <section className="settings-section">
47 <div className="settings-section__head">
48 <div>
49 <div className="settings-section__title">{t("settings.storageTitle")}</div>
50 <div className="settings-section__desc">{t("settings.storageHint")}</div>
51 </div>
52 </div>
53 <div className="settings-section__body">
54 <StoragePathField label={t("settings.defaultWorkspace")} hint={t("settings.defaultWorkspaceHint")} value={view.defaultWorkspace} />
55 <StoragePathField label={t("settings.storageState")} value={view.statePath} />
56 <StoragePathField label={t("settings.storageCache")} value={view.cachePath} />
57 <StoragePathField label={t("settings.storageExtensions")} value={view.extensionsPath} />
58 </div>
59 </section>
60 <section className="settings-section">
61 <div className="settings-section__head"><div>
62 <div className="settings-section__title">{m("historicalTitle")}</div>
63 <div className="settings-section__desc">{m("historicalDescription")}</div>
64 </div></div>
65 <div className="settings-section__body">
66 <HistoricalImportList active />
67 <details><summary>{t("history.recoveryReview")}</summary><HistoricalRecoveryList active /></details>
68 </div>
69 </section>
70 </>);
71 }
72
73 function StoragePathField({ label, hint, value }: { label: ReactNode; hint?: ReactNode; value: string }) {
74 const t = useT();
75 const [copied, setCopied] = useState(false);
76 const [copyError, setCopyError] = useState(false);
77 return (
78 <div className="settings-field">
79 <div className="settings-field__copy">
80 <div className="settings-field__label">{label}</div>
81 {hint && <div className="settings-field__hint"><span className="settings-field__hint-line">{hint}</span></div>}
82 </div>
83 <div className="settings-field__control settings-path-value">
84 <input className="mem-input" value={value} placeholder={t("common.none")} aria-label={String(label)} readOnly />
85 <button className="btn settings-icon-button" disabled={!value} title={t(copied ? "richLink.copied" : "common.copy")} aria-label={`${t("common.copy")}: ${String(label)}`} onClick={async () => { try { await navigator.clipboard.writeText(value); setCopied(true); setCopyError(false); } catch { setCopyError(true); } }}>{copied ? <Check size={16} /> : <Copy size={16} />}</button>
86 {copyError && <span role="alert">{t("settings.hooksJsonClipboardUnavailable")}</span>}
87 </div>
88 </div>
89 );
90 }
91
91 lines Plain Text