refactor(settings): simplify settings page layout and auto-save
Reorganize settings page structure and integrate autoSaveSettings. - Move save button inline within Advanced tab content - Remove sticky footer for cleaner layout - Use autoSaveSettings for General tab settings - Simplify dialog close behavior - Refactor ImportExportSection layout
This commit is contained in:
@@ -54,86 +54,63 @@ export function ImportExportSection({
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="space-y-4 rounded-xl glass-card p-6 border border-white/10">
|
<div className="space-y-4 rounded-xl glass-card p-6 border border-white/10">
|
||||||
{/* Export Section */}
|
{/* Import and Export Buttons Side by Side */}
|
||||||
<div className="space-y-3">
|
<div className="grid grid-cols-2 gap-4 items-stretch">
|
||||||
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
{/* Import Button */}
|
||||||
<Save className="h-4 w-4" />
|
<div className="relative">
|
||||||
<span>导出配置</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="w-full bg-primary/90 hover:bg-primary text-primary-foreground shadow-lg shadow-primary/20"
|
|
||||||
onClick={onExport}
|
|
||||||
>
|
|
||||||
<Save className="mr-2 h-4 w-4" />
|
|
||||||
{t("settings.exportConfig")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div className="border-t border-white/10" />
|
|
||||||
|
|
||||||
{/* Import Section */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
|
||||||
<FolderOpen className="h-4 w-4" />
|
|
||||||
<span>导入配置</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
className={`w-full h-auto py-3 px-4 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white ${selectedFile && !isImporting ? "flex-col items-start" : "items-center"}`}
|
||||||
className="flex-1 min-w-[180px] hover:bg-black/5 dark:hover:bg-white/5 border-white/10"
|
onClick={!selectedFile ? onSelectFile : onImport}
|
||||||
onClick={onSelectFile}
|
disabled={isImporting}
|
||||||
>
|
>
|
||||||
<FolderOpen className="mr-2 h-4 w-4" />
|
<div className="flex items-center gap-2 w-full justify-center">
|
||||||
{t("settings.selectConfigFile")}
|
{isImporting ? (
|
||||||
</Button>
|
<Loader2 className="h-4 w-4 animate-spin flex-shrink-0" />
|
||||||
<Button
|
) : selectedFile ? (
|
||||||
type="button"
|
<CheckCircle2 className="h-4 w-4 flex-shrink-0" />
|
||||||
disabled={!selectedFile || isImporting}
|
) : (
|
||||||
className="bg-primary hover:bg-primary/90"
|
<FolderOpen className="h-4 w-4 flex-shrink-0" />
|
||||||
onClick={onImport}
|
)}
|
||||||
>
|
<span className="font-medium">
|
||||||
{isImporting ? (
|
{isImporting
|
||||||
<span className="inline-flex items-center gap-2">
|
? t("settings.importing")
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
: selectedFile
|
||||||
{t("settings.importing")}
|
? t("settings.import")
|
||||||
|
: t("settings.selectConfigFile")}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
</div>
|
||||||
<>
|
{selectedFile && !isImporting && (
|
||||||
<CheckCircle2 className="mr-2 h-4 w-4" />
|
<div className="mt-2 w-full text-left">
|
||||||
{t("settings.import")}
|
<p className="text-xs font-mono text-white/80 truncate">
|
||||||
</>
|
📄 {selectedFileName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
{selectedFile ? (
|
{selectedFile && (
|
||||||
<Button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
className="hover:bg-black/5 dark:hover:bg-white/5"
|
className="absolute -top-2 -right-2 h-6 w-6 rounded-full bg-red-500 hover:bg-red-600 text-white flex items-center justify-center shadow-lg transition-colors z-10"
|
||||||
|
aria-label="Clear selection"
|
||||||
>
|
>
|
||||||
<XCircle className="mr-2 h-4 w-4" />
|
<XCircle className="h-4 w-4" />
|
||||||
{t("common.clear")}
|
</button>
|
||||||
</Button>
|
)}
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedFile ? (
|
{/* Export Button */}
|
||||||
<div className="glass rounded-lg border border-white/10 p-3">
|
<div>
|
||||||
<p className="text-xs font-mono text-foreground/80 truncate">
|
<Button
|
||||||
📄 {selectedFileName}
|
type="button"
|
||||||
</p>
|
className="w-full h-full py-3 px-4 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white items-center"
|
||||||
</div>
|
onClick={onExport}
|
||||||
) : (
|
>
|
||||||
<div className="glass rounded-lg border border-white/10 p-3">
|
<Save className="mr-2 h-4 w-4" />
|
||||||
<p className="text-xs text-muted-foreground italic">
|
{t("settings.exportConfig")}
|
||||||
{t("settings.noFileSelected")}
|
</Button>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ImportStatusMessage
|
<ImportStatusMessage
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function SettingsPage({
|
|||||||
resetDirectory,
|
resetDirectory,
|
||||||
resetAppConfigDir,
|
resetAppConfigDir,
|
||||||
saveSettings,
|
saveSettings,
|
||||||
resetSettings,
|
autoSaveSettings,
|
||||||
requiresRestart,
|
requiresRestart,
|
||||||
acknowledgeRestart,
|
acknowledgeRestart,
|
||||||
} = useSettings();
|
} = useSettings();
|
||||||
@@ -83,21 +83,6 @@ export function SettingsPage({
|
|||||||
}
|
}
|
||||||
}, [requiresRestart]);
|
}, [requiresRestart]);
|
||||||
|
|
||||||
const closeDialog = useCallback(() => {
|
|
||||||
// 取消/直接关闭:恢复到初始设置(包括语言回滚)
|
|
||||||
resetSettings();
|
|
||||||
acknowledgeRestart();
|
|
||||||
clearSelection();
|
|
||||||
resetStatus();
|
|
||||||
onOpenChange(false);
|
|
||||||
}, [
|
|
||||||
acknowledgeRestart,
|
|
||||||
clearSelection,
|
|
||||||
onOpenChange,
|
|
||||||
resetSettings,
|
|
||||||
resetStatus,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const closeAfterSave = useCallback(() => {
|
const closeAfterSave = useCallback(() => {
|
||||||
// 保存成功后关闭:不再重置语言,避免需要“保存两次”才生效
|
// 保存成功后关闭:不再重置语言,避免需要“保存两次”才生效
|
||||||
acknowledgeRestart();
|
acknowledgeRestart();
|
||||||
@@ -118,7 +103,7 @@ export function SettingsPage({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SettingsPage] Failed to save settings", error);
|
console.error("[SettingsPage] Failed to save settings", error);
|
||||||
}
|
}
|
||||||
}, [closeDialog, saveSettings]);
|
}, [closeAfterSave, saveSettings]);
|
||||||
|
|
||||||
const handleRestartLater = useCallback(() => {
|
const handleRestartLater = useCallback(() => {
|
||||||
setShowRestartPrompt(false);
|
setShowRestartPrompt(false);
|
||||||
@@ -144,15 +129,13 @@ export function SettingsPage({
|
|||||||
}, [closeAfterSave, t]);
|
}, [closeAfterSave, t]);
|
||||||
|
|
||||||
// 通用设置即时保存(无需手动点击)
|
// 通用设置即时保存(无需手动点击)
|
||||||
|
// 使用 autoSaveSettings 避免误触发系统 API(开机自启、Claude 插件等)
|
||||||
const handleAutoSave = useCallback(
|
const handleAutoSave = useCallback(
|
||||||
async (updates: Partial<SettingsFormState>) => {
|
async (updates: Partial<SettingsFormState>) => {
|
||||||
if (!settings) return;
|
if (!settings) return;
|
||||||
updateSettings(updates);
|
updateSettings(updates);
|
||||||
try {
|
try {
|
||||||
const result = await saveSettings(updates, { silent: true });
|
await autoSaveSettings(updates);
|
||||||
if (result?.requiresRestart) {
|
|
||||||
setShowRestartPrompt(true);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SettingsPage] Failed to autosave settings", error);
|
console.error("[SettingsPage] Failed to autosave settings", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
@@ -162,7 +145,7 @@ export function SettingsPage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[saveSettings, settings, t, updateSettings],
|
[autoSaveSettings, settings, t, updateSettings],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isBusy = useMemo(() => isLoading && !settings, [isLoading, settings]);
|
const isBusy = useMemo(() => isLoading && !settings, [isLoading, settings]);
|
||||||
@@ -206,7 +189,7 @@ export function SettingsPage({
|
|||||||
) : null}
|
) : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="advanced" className="space-y-6 mt-0">
|
<TabsContent value="advanced" className="space-y-6 mt-0 pb-6">
|
||||||
{settings ? (
|
{settings ? (
|
||||||
<>
|
<>
|
||||||
<DirectorySettings
|
<DirectorySettings
|
||||||
@@ -233,6 +216,25 @@ export function SettingsPage({
|
|||||||
onExport={exportConfig}
|
onExport={exportConfig}
|
||||||
onClear={clearSelection}
|
onClear={clearSelection}
|
||||||
/>
|
/>
|
||||||
|
<div className="pt-6 border-t border-gray-200 dark:border-white/10">
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="w-full"
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<span className="inline-flex items-center gap-2">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
{t("settings.saving")}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save className="mr-2 h-4 w-4" />
|
||||||
|
{t("common.save")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -241,28 +243,6 @@ export function SettingsPage({
|
|||||||
<AboutSection isPortable={isPortable} />
|
<AboutSection isPortable={isPortable} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeTab === "advanced" ? (
|
|
||||||
<div className="flex-shrink-0 pt-6 border-t border-white/5 sticky bottom-0 bg-background/95 backdrop-blur-sm">
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
className="w-full bg-primary hover:bg-primary/90"
|
|
||||||
disabled={isSaving}
|
|
||||||
>
|
|
||||||
{isSaving ? (
|
|
||||||
<span className="inline-flex items-center gap-2">
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
{t("settings.saving")}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Save className="mr-2 h-4 w-4" />
|
|
||||||
{t("common.save")}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user