diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 6fde274..8ad1866 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -559,7 +559,7 @@ dependencies = [
[[package]]
name = "cc-switch"
-version = "3.1.1"
+version = "3.1.2"
dependencies = [
"dirs 5.0.1",
"log",
diff --git a/src/App.tsx b/src/App.tsx
index fb3ceb8..901130e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -7,6 +7,7 @@ import EditProviderModal from "./components/EditProviderModal";
import { ConfirmDialog } from "./components/ConfirmDialog";
import { AppSwitcher } from "./components/AppSwitcher";
import SettingsModal from "./components/SettingsModal";
+import { UpdateBadge } from "./components/UpdateBadge";
import { Plus, Settings, Moon, Sun } from "lucide-react";
import { buttonStyles } from "./lib/styles";
import { useDarkMode } from "./hooks/useDarkMode";
@@ -220,13 +221,16 @@ function App() {
>
{isDarkMode ? : }
-
+
+
+ setIsSettingsOpen(true)} />
+
diff --git a/src/components/ProviderForm.tsx b/src/components/ProviderForm.tsx
index 2aa5e59..015581e 100644
--- a/src/components/ProviderForm.tsx
+++ b/src/components/ProviderForm.tsx
@@ -419,14 +419,12 @@ const ProviderForm: React.FC
= ({
// 初始时从配置中同步 API Key(编辑模式)
useEffect(() => {
- if (initialData) {
- const parsedKey = getApiKeyFromConfig(
- JSON.stringify(initialData.settingsConfig),
- );
- if (parsedKey) setApiKey(parsedKey);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ if (!initialData) return;
+ const parsedKey = getApiKeyFromConfig(
+ JSON.stringify(initialData.settingsConfig),
+ );
+ if (parsedKey) setApiKey(parsedKey);
+ }, [initialData]);
// 支持按下 ESC 关闭弹窗
useEffect(() => {
diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx
index 2e7c746..95f993b 100644
--- a/src/components/SettingsModal.tsx
+++ b/src/components/SettingsModal.tsx
@@ -1,8 +1,9 @@
import { useState, useEffect } from "react";
-import { X, Info, RefreshCw, FolderOpen } from "lucide-react";
+import { X, Info, RefreshCw, FolderOpen, Download, ExternalLink } from "lucide-react";
import { getVersion } from "@tauri-apps/api/app";
import "../lib/tauri-api";
-import { runUpdateFlow } from "../lib/updater";
+import { relaunchApp } from "../lib/updater";
+import { useUpdate } from "../contexts/UpdateContext";
import type { Settings } from "../types";
interface SettingsModalProps {
@@ -16,6 +17,8 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
const [configPath, setConfigPath] = useState("");
const [version, setVersion] = useState("");
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
+ const [isDownloading, setIsDownloading] = useState(false);
+ const { hasUpdate, updateInfo, updateHandle, checkUpdate, resetDismiss } = useUpdate();
useEffect(() => {
loadSettings();
@@ -29,7 +32,8 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
setVersion(appVersion);
} catch (error) {
console.error("获取版本信息失败:", error);
- setVersion("3.1.1"); // 降级使用默认版本
+ // 失败时不硬编码版本号,显示为未知
+ setVersion("未知");
}
};
@@ -65,15 +69,32 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
};
const handleCheckUpdate = async () => {
- setIsCheckingUpdate(true);
- try {
- // 优先使用 Tauri Updater 流程;失败时回退到打开 Releases 页面
- await runUpdateFlow({ timeout: 30000 });
- } catch (error) {
- console.error("检查更新失败,回退到 Releases 页面:", error);
- await window.api.checkForUpdates();
- } finally {
- setIsCheckingUpdate(false);
+ if (hasUpdate && updateHandle) {
+ // 已检测到更新:直接复用 updateHandle 下载并安装,避免重复检查
+ setIsDownloading(true);
+ try {
+ resetDismiss();
+ await updateHandle.downloadAndInstall();
+ await relaunchApp();
+ } catch (error) {
+ console.error("更新失败:", error);
+ // 更新失败时回退到打开 Releases 页面
+ await window.api.checkForUpdates();
+ } finally {
+ setIsDownloading(false);
+ }
+ } else {
+ // 尚未检测到更新:先检查
+ setIsCheckingUpdate(true);
+ try {
+ await checkUpdate();
+ // 检查后若有更新,让用户再次点击执行
+ } catch (error) {
+ console.error("检查更新失败,回退到 Releases 页面:", error);
+ await window.api.checkForUpdates();
+ } finally {
+ setIsCheckingUpdate(false);
+ }
}
};
@@ -85,6 +106,23 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
}
};
+ const handleOpenReleaseNotes = async () => {
+ try {
+ const targetVersion = updateInfo?.availableVersion || version;
+ // 如果未知或为空,回退到 releases 首页
+ if (!targetVersion || targetVersion === "未知") {
+ await window.api.openExternal("https://github.com/farion1231/cc-switch/releases");
+ return;
+ }
+ const tag = targetVersion.startsWith("v") ? targetVersion : `v${targetVersion}`;
+ await window.api.openExternal(
+ `https://github.com/farion1231/cc-switch/releases/tag/${tag}`,
+ );
+ } catch (error) {
+ console.error("打开更新日志失败:", error);
+ }
+ };
+
return (
@@ -168,24 +206,48 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
-
diff --git a/src/components/UpdateBadge.tsx b/src/components/UpdateBadge.tsx
new file mode 100644
index 0000000..f217c48
--- /dev/null
+++ b/src/components/UpdateBadge.tsx
@@ -0,0 +1,59 @@
+import { X, Sparkles } from "lucide-react";
+import { useUpdate } from "../contexts/UpdateContext";
+
+interface UpdateBadgeProps {
+ className?: string;
+ onClick?: () => void; // 点击徽标的回调(例如打开设置)
+}
+
+export function UpdateBadge({ className = "", onClick }: UpdateBadgeProps) {
+ const { hasUpdate, updateInfo, isDismissed, dismissUpdate } = useUpdate();
+
+ // 如果没有更新或已关闭,不显示
+ if (!hasUpdate || isDismissed || !updateInfo) {
+ return null;
+ }
+
+ return (
+ {
+ if (!onClick) return;
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onClick();
+ }
+ }}
+ >
+
+
+ 新版本 {updateInfo.availableVersion}
+
+ {
+ e.stopPropagation();
+ dismissUpdate();
+ }}
+ className="
+ -mr-1 p-0.5 rounded-full
+ hover:bg-white/10 transition-colors
+ focus:outline-none focus:ring-2 focus:ring-blue-500/50
+ "
+ aria-label="关闭更新提醒"
+ >
+
+
+
+ );
+}
diff --git a/src/contexts/UpdateContext.tsx b/src/contexts/UpdateContext.tsx
new file mode 100644
index 0000000..259cd49
--- /dev/null
+++ b/src/contexts/UpdateContext.tsx
@@ -0,0 +1,147 @@
+import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from "react";
+import type { UpdateInfo, UpdateHandle } from "../lib/updater";
+import { checkForUpdate } from "../lib/updater";
+
+interface UpdateContextValue {
+ // 更新状态
+ hasUpdate: boolean;
+ updateInfo: UpdateInfo | null;
+ updateHandle: UpdateHandle | null;
+ isChecking: boolean;
+ error: string | null;
+
+ // 提示状态
+ isDismissed: boolean;
+ dismissUpdate: () => void;
+
+ // 操作方法
+ checkUpdate: () => Promise;
+ resetDismiss: () => void;
+}
+
+const UpdateContext = createContext(undefined);
+
+export function UpdateProvider({ children }: { children: React.ReactNode }) {
+ const DISMISSED_VERSION_KEY = "ccswitch:update:dismissedVersion";
+ const LEGACY_DISMISSED_KEY = "dismissedUpdateVersion"; // 兼容旧键
+
+ const [hasUpdate, setHasUpdate] = useState(false);
+ const [updateInfo, setUpdateInfo] = useState(null);
+ const [updateHandle, setUpdateHandle] = useState(null);
+ const [isChecking, setIsChecking] = useState(false);
+ const [error, setError] = useState(null);
+ const [isDismissed, setIsDismissed] = useState(false);
+
+ // 从 localStorage 读取已关闭的版本
+ useEffect(() => {
+ const current = updateInfo?.availableVersion;
+ if (!current) return;
+
+ // 读取新键;若不存在,尝试迁移旧键
+ let dismissedVersion = localStorage.getItem(DISMISSED_VERSION_KEY);
+ if (!dismissedVersion) {
+ const legacy = localStorage.getItem(LEGACY_DISMISSED_KEY);
+ if (legacy) {
+ localStorage.setItem(DISMISSED_VERSION_KEY, legacy);
+ localStorage.removeItem(LEGACY_DISMISSED_KEY);
+ dismissedVersion = legacy;
+ }
+ }
+
+ setIsDismissed(dismissedVersion === current);
+ }, [updateInfo?.availableVersion]);
+
+ const isCheckingRef = useRef(false);
+
+ const checkUpdate = useCallback(async () => {
+ if (isCheckingRef.current) return;
+ isCheckingRef.current = true;
+ setIsChecking(true);
+ setError(null);
+
+ try {
+ const result = await checkForUpdate({ timeout: 30000 });
+
+ if (result.status === "available") {
+ setHasUpdate(true);
+ setUpdateInfo(result.info);
+ setUpdateHandle(result.update);
+
+ // 检查是否已经关闭过这个版本的提醒
+ let dismissedVersion = localStorage.getItem(DISMISSED_VERSION_KEY);
+ if (!dismissedVersion) {
+ const legacy = localStorage.getItem(LEGACY_DISMISSED_KEY);
+ if (legacy) {
+ localStorage.setItem(DISMISSED_VERSION_KEY, legacy);
+ localStorage.removeItem(LEGACY_DISMISSED_KEY);
+ dismissedVersion = legacy;
+ }
+ }
+ setIsDismissed(dismissedVersion === result.info.availableVersion);
+ } else {
+ setHasUpdate(false);
+ setUpdateInfo(null);
+ setUpdateHandle(null);
+ setIsDismissed(false);
+ }
+ } catch (err) {
+ console.error("检查更新失败:", err);
+ setError(err instanceof Error ? err.message : "检查更新失败");
+ setHasUpdate(false);
+ } finally {
+ setIsChecking(false);
+ isCheckingRef.current = false;
+ }
+ }, []);
+
+ const dismissUpdate = useCallback(() => {
+ setIsDismissed(true);
+ if (updateInfo?.availableVersion) {
+ localStorage.setItem(DISMISSED_VERSION_KEY, updateInfo.availableVersion);
+ // 清理旧键
+ localStorage.removeItem(LEGACY_DISMISSED_KEY);
+ }
+ }, [updateInfo?.availableVersion]);
+
+ const resetDismiss = useCallback(() => {
+ setIsDismissed(false);
+ localStorage.removeItem(DISMISSED_VERSION_KEY);
+ localStorage.removeItem(LEGACY_DISMISSED_KEY);
+ }, []);
+
+ // 应用启动时自动检查更新
+ useEffect(() => {
+ // 延迟1秒后检查,避免影响启动体验
+ const timer = setTimeout(() => {
+ checkUpdate().catch(console.error);
+ }, 1000);
+
+ return () => clearTimeout(timer);
+ }, [checkUpdate]);
+
+ const value: UpdateContextValue = {
+ hasUpdate,
+ updateInfo,
+ updateHandle,
+ isChecking,
+ error,
+ isDismissed,
+ dismissUpdate,
+ checkUpdate,
+ resetDismiss,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useUpdate() {
+ const context = useContext(UpdateContext);
+ if (!context) {
+ throw new Error("useUpdate must be used within UpdateProvider");
+ }
+ return context;
+}
diff --git a/src/lib/updater.ts b/src/lib/updater.ts
index 017406b..2926cb1 100644
--- a/src/lib/updater.ts
+++ b/src/lib/updater.ts
@@ -122,25 +122,5 @@ export async function relaunchApp(): Promise {
await relaunch();
}
-export async function runUpdateFlow(
- opts: CheckOptions = {},
-): Promise<{ status: "up-to-date" | "done" }> {
- const result = await checkForUpdate(opts);
- if (result.status === "up-to-date") return result;
-
- let downloaded = 0;
- let total = 0;
- await result.update.downloadAndInstall((e) => {
- if (e.event === "Started") {
- total = e.total ?? 0;
- downloaded = 0;
- } else if (e.event === "Progress") {
- downloaded += e.downloaded ?? 0;
- // 调用方可监听此处并更新 UI(目前设置页仅显示加载态)
- console.debug("update progress", { downloaded, total });
- }
- });
-
- await relaunchApp();
- return { status: "done" };
-}
+// 旧的聚合更新流程已由调用方直接使用 updateHandle 取代
+// 如需单函数封装,可在需要时基于 checkForUpdate + updateHandle 复合调用
diff --git a/src/main.tsx b/src/main.tsx
index caefd86..5d77b49 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,6 +1,7 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
+import { UpdateProvider } from "./contexts/UpdateContext";
import "./index.css";
// 导入 Tauri API(自动绑定到 window.api)
import "./lib/tauri-api";
@@ -19,6 +20,8 @@ try {
ReactDOM.createRoot(document.getElementById("root")!).render(
-
+
+
+
,
);