优化系统管理的布局和操作交互

This commit is contained in:
xintaofei
2026-03-23 17:55:53 +08:00
parent 166b408bb9
commit 42e7e66997
11 changed files with 164 additions and 162 deletions

View File

@@ -3,10 +3,10 @@
import { useCallback, useEffect, useMemo, useState } from "react"
import {
ArrowUpCircle,
CheckCircle2,
Languages,
Loader2,
RefreshCw,
Save,
Wifi,
} from "lucide-react"
import type { Update } from "@tauri-apps/plugin-updater"
@@ -153,7 +153,11 @@ export function SystemNetworkSettings() {
loadSettings().catch((err) => {
console.error("[Settings] load system settings failed:", err)
})
}, [loadSettings])
checkForUpdates().catch((err) => {
console.error("[Settings] auto check update failed:", err)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
return () => {
@@ -164,48 +168,49 @@ export function SystemNetworkSettings() {
}
}, [availableUpdate])
const saveSettings = useCallback(async () => {
if (enabled && !proxyUrl.trim()) {
toast.error(t("proxyRequired"))
return
}
const saveProxySettings = useCallback(
async (nextEnabled: boolean, nextProxyUrl: string) => {
if (nextEnabled && !nextProxyUrl.trim()) return
setSaving(true)
try {
const next = await updateSystemProxySettings({
enabled,
proxy_url: proxyUrl.trim() || null,
})
setEnabled(next.enabled)
setProxyUrl(next.proxy_url ?? "")
toast.success(t("saveSuccess"))
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("saveFailed", { message }))
} finally {
setSaving(false)
}
}, [enabled, proxyUrl, t])
setSaving(true)
try {
const next = await updateSystemProxySettings({
enabled: nextEnabled,
proxy_url: nextProxyUrl.trim() || null,
})
setEnabled(next.enabled)
setProxyUrl(next.proxy_url ?? "")
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("saveFailed", { message }))
} finally {
setSaving(false)
}
},
[t]
)
const saveLanguage = useCallback(async () => {
setSavingLanguage(true)
const saveLanguage = useCallback(
async (lang: LanguageSelectValue) => {
setSavingLanguage(true)
try {
const next = await updateSystemLanguageSettings({
mode: appLanguage === "system" ? "system" : "manual",
language:
appLanguage === "system" ? languageSettings.language : appLanguage,
})
try {
const next = await updateSystemLanguageSettings({
mode: lang === "system" ? "system" : "manual",
language:
lang === "system" ? languageSettings.language : lang,
})
setLanguageSettings(next)
toast.success(t("languageSaveSuccess"))
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("languageSaveFailed", { message }))
} finally {
setSavingLanguage(false)
}
}, [appLanguage, languageSettings.language, setLanguageSettings, t])
setLanguageSettings(next)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("languageSaveFailed", { message }))
} finally {
setSavingLanguage(false)
}
},
[languageSettings.language, setLanguageSettings, t]
)
const formatUpdateError = useCallback(
(error: unknown, action: UpdateAction): string => {
@@ -302,128 +307,16 @@ export function SystemNetworkSettings() {
<section className="rounded-xl border bg-card p-4 space-y-4">
<div className="flex items-center gap-2">
<Wifi className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">{t("proxyTitle")}</h2>
</div>
<p className="text-xs text-muted-foreground leading-5">
{t("proxyDescription")}
</p>
{loadError && (
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-400">
{t("loadFailed", { message: loadError })}
</div>
)}
<label className="inline-flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={enabled}
onChange={(event) => setEnabled(event.target.checked)}
/>
{t("enableProxy")}
</label>
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
{t("proxyAddress")}
</label>
<Input
value={proxyUrl}
onChange={(event) => setProxyUrl(event.target.value)}
placeholder={PROXY_EXAMPLE}
/>
<p className="text-[11px] text-muted-foreground">
{t("proxyHint", { example: PROXY_EXAMPLE })}
</p>
</div>
<div className="flex justify-end">
<Button size="sm" onClick={saveSettings} disabled={saving}>
{saving ? (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
{t("saving")}
</>
) : (
<>
<Save className="h-3.5 w-3.5" />
{t("save")}
</>
)}
</Button>
</div>
</section>
<section className="rounded-xl border bg-card p-4 space-y-4">
<div className="flex items-center gap-2">
<Languages className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">{t("languageTitle")}</h2>
</div>
<p className="text-xs text-muted-foreground leading-5">
{t("languageDescription")}
</p>
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
{t("appLanguage")}
</label>
<Select
value={appLanguage}
onValueChange={(value) => {
if (value === "system") {
setAppLanguage("system")
return
}
if (!isAppLocale(value)) return
setAppLanguage(value)
}}
disabled={savingLanguage || !languageSettingsLoaded}
>
<SelectTrigger className="w-full sm:w-56">
<SelectValue />
</SelectTrigger>
<SelectContent align="start">
<SelectItem value="system">
{tLanguage("followSystem")}
</SelectItem>
<SelectItem value="en">{languageLabels.en}</SelectItem>
<SelectItem value="zh_cn">{languageLabels.zh_cn}</SelectItem>
<SelectItem value="zh_tw">{languageLabels.zh_tw}</SelectItem>
<SelectItem value="ja">{languageLabels.ja}</SelectItem>
<SelectItem value="ko">{languageLabels.ko}</SelectItem>
<SelectItem value="es">{languageLabels.es}</SelectItem>
<SelectItem value="de">{languageLabels.de}</SelectItem>
<SelectItem value="fr">{languageLabels.fr}</SelectItem>
<SelectItem value="pt">{languageLabels.pt}</SelectItem>
<SelectItem value="ar">{languageLabels.ar}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex justify-end">
<Button size="sm" onClick={saveLanguage} disabled={savingLanguage}>
{savingLanguage ? (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
{t("saving")}
</>
) : (
<>
<Save className="h-3.5 w-3.5" />
{t("save")}
</>
)}
</Button>
</div>
</section>
<section className="rounded-xl border bg-card p-4 space-y-4">
<div className="flex items-center gap-2">
<RefreshCw className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">{t("updateTitle")}</h2>
{checkingUpdate ? (
<RefreshCw className="h-4 w-4 text-muted-foreground animate-spin" />
) : availableUpdate ? (
<ArrowUpCircle className="h-4 w-4 text-muted-foreground" />
) : lastCheckedAt ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<RefreshCw className="h-4 w-4 text-muted-foreground" />
)}
<h2 className="text-sm font-semibold">{t("versionTitle")}</h2>
</div>
<p className="text-xs text-muted-foreground leading-5">
@@ -514,6 +407,105 @@ export function SystemNetworkSettings() {
</div>
)}
</section>
<section className="rounded-xl border bg-card p-4 space-y-4">
<div className="flex items-center gap-2">
<Wifi className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">{t("proxyTitle")}</h2>
</div>
<p className="text-xs text-muted-foreground leading-5">
{t("proxyDescription")}
</p>
{loadError && (
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-400">
{t("loadFailed", { message: loadError })}
</div>
)}
<label className="inline-flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={enabled}
disabled={saving}
onChange={(event) => {
const next = event.target.checked
setEnabled(next)
saveProxySettings(next, proxyUrl)
}}
/>
{t("enableProxy")}
</label>
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
{t("proxyAddress")}
</label>
<Input
value={proxyUrl}
onChange={(event) => setProxyUrl(event.target.value)}
onBlur={() => saveProxySettings(enabled, proxyUrl)}
placeholder={PROXY_EXAMPLE}
disabled={saving}
/>
<p className="text-[11px] text-muted-foreground">
{t("proxyHint", { example: PROXY_EXAMPLE })}
</p>
</div>
</section>
<section className="rounded-xl border bg-card p-4 space-y-4">
<div className="flex items-center gap-2">
<Languages className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">{t("languageTitle")}</h2>
</div>
<p className="text-xs text-muted-foreground leading-5">
{t("languageDescription")}
</p>
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">
{t("appLanguage")}
</label>
<Select
value={appLanguage}
onValueChange={(value) => {
let nextLang: LanguageSelectValue
if (value === "system") {
nextLang = "system"
} else if (isAppLocale(value)) {
nextLang = value
} else {
return
}
setAppLanguage(nextLang)
saveLanguage(nextLang)
}}
disabled={savingLanguage || !languageSettingsLoaded}
>
<SelectTrigger className="w-full sm:w-56">
<SelectValue />
</SelectTrigger>
<SelectContent align="start">
<SelectItem value="system">
{tLanguage("followSystem")}
</SelectItem>
<SelectItem value="en">{languageLabels.en}</SelectItem>
<SelectItem value="zh_cn">{languageLabels.zh_cn}</SelectItem>
<SelectItem value="zh_tw">{languageLabels.zh_tw}</SelectItem>
<SelectItem value="ja">{languageLabels.ja}</SelectItem>
<SelectItem value="ko">{languageLabels.ko}</SelectItem>
<SelectItem value="es">{languageLabels.es}</SelectItem>
<SelectItem value="de">{languageLabels.de}</SelectItem>
<SelectItem value="fr">{languageLabels.fr}</SelectItem>
<SelectItem value="pt">{languageLabels.pt}</SelectItem>
<SelectItem value="ar">{languageLabels.ar}</SelectItem>
</SelectContent>
</Select>
</div>
</section>
</div>
</div>
)

View File

@@ -127,6 +127,7 @@
"languageSaveSuccess": "تم حفظ إعدادات اللغة",
"languageSaveFailed": "فشل حفظ إعدادات اللغة: {message}",
"updateTitle": "تحديث التطبيق",
"versionTitle": "تحديث البرنامج",
"updateDescription": "تحقق من المصدر المهيأ للإصدارات الأحدث وثبّت التحديث مباشرة عند توفره.",
"currentVersion": "الإصدار الحالي",
"upgradableVersion": "أحدث إصدار",

View File

@@ -127,6 +127,7 @@
"languageSaveSuccess": "Spracheinstellungen wurden gespeichert",
"languageSaveFailed": "Spracheinstellungen konnten nicht gespeichert werden: {message}",
"updateTitle": "App-Update",
"versionTitle": "Softwareupdate",
"updateDescription": "Prüft die konfigurierte Release-Quelle auf neue Versionen und installiert sie bei Verfügbarkeit direkt.",
"currentVersion": "Aktuelle Version",
"upgradableVersion": "Neueste Version",

View File

@@ -127,6 +127,7 @@
"languageSaveSuccess": "Language settings saved",
"languageSaveFailed": "Failed to save language settings: {message}",
"updateTitle": "App Update",
"versionTitle": "Software Update",
"updateDescription": "Check the configured release source for newer versions and install directly when available.",
"currentVersion": "Current version",
"upgradableVersion": "Latest version",

View File

@@ -127,6 +127,7 @@
"languageSaveSuccess": "La configuración de idioma se guardó",
"languageSaveFailed": "No se pudo guardar la configuración de idioma: {message}",
"updateTitle": "Actualización de la app",
"versionTitle": "Actualización de software",
"updateDescription": "Comprueba versiones más nuevas en la fuente de versiones configurada e instálalas directamente cuando estén disponibles.",
"currentVersion": "Versión actual",
"upgradableVersion": "Última versión",

View File

@@ -127,6 +127,7 @@
"languageSaveSuccess": "Les paramètres de langue ont été enregistrés",
"languageSaveFailed": "Échec de lenregistrement des paramètres de langue : {message}",
"updateTitle": "Mise à jour de lapp",
"versionTitle": "Mise à jour logicielle",
"updateDescription": "Vérifiez les nouvelles versions depuis la source de publication configurée et installez-les directement si disponibles.",
"currentVersion": "Version actuelle",
"upgradableVersion": "Dernière version",

View File

@@ -127,6 +127,7 @@
"languageSaveSuccess": "言語設定を保存しました",
"languageSaveFailed": "言語設定の保存に失敗しました: {message}",
"updateTitle": "アプリ更新",
"versionTitle": "ソフトウェアアップデート",
"updateDescription": "設定されたリリースソースで新しいバージョンを確認し、利用可能なら直接インストールします。",
"currentVersion": "現在のバージョン",
"upgradableVersion": "最新バージョン",

View File

@@ -127,6 +127,7 @@
"languageSaveSuccess": "언어 설정이 저장되었습니다",
"languageSaveFailed": "언어 설정 저장 실패: {message}",
"updateTitle": "앱 업데이트",
"versionTitle": "소프트웨어 업데이트",
"updateDescription": "설정된 릴리스 소스에서 새 버전을 확인하고 가능하면 바로 설치합니다.",
"currentVersion": "현재 버전",
"upgradableVersion": "최신 버전",

View File

@@ -127,6 +127,7 @@
"languageSaveSuccess": "As configurações de idioma foram salvas",
"languageSaveFailed": "Falha ao salvar as configurações de idioma: {message}",
"updateTitle": "Atualização do app",
"versionTitle": "Atualização de software",
"updateDescription": "Verifique versões mais novas na fonte de releases configurada e instale diretamente quando disponíveis.",
"currentVersion": "Versão atual",
"upgradableVersion": "Versão mais recente",

View File

@@ -127,6 +127,7 @@
"languageSaveSuccess": "语言设置已保存",
"languageSaveFailed": "语言设置保存失败:{message}",
"updateTitle": "应用升级",
"versionTitle": "软件更新",
"updateDescription": "点击检查后会从配置的发布源拉取最新版本信息,有新版本时可直接下载并安装。",
"currentVersion": "当前版本",
"upgradableVersion": "最新版本",

View File

@@ -127,6 +127,7 @@
"languageSaveSuccess": "語言設定已儲存",
"languageSaveFailed": "語言設定儲存失敗:{message}",
"updateTitle": "應用升級",
"versionTitle": "軟體更新",
"updateDescription": "點擊檢查後會從設定的發佈來源拉取最新版本資訊,有新版本時可直接下載並安裝。",
"currentVersion": "目前版本",
"upgradableVersion": "最新版本",