feat(settings): add minimal settings panel

- Add settings icon button next to app title
- Create SettingsModal component with:
  - Show in Dock option (macOS)
  - Version info and check for updates button
  - Config file location with open folder button
- Add settings-related APIs in tauri-api
- Update type definitions for new API methods
This commit is contained in:
Jason
2025-09-07 10:48:27 +08:00
parent 48bd37a74b
commit 77bdeb02fb
4 changed files with 256 additions and 4 deletions

View File

@@ -6,7 +6,8 @@ import AddProviderModal from "./components/AddProviderModal";
import EditProviderModal from "./components/EditProviderModal";
import { ConfirmDialog } from "./components/ConfirmDialog";
import { AppSwitcher } from "./components/AppSwitcher";
import { Plus } from "lucide-react";
import SettingsModal from "./components/SettingsModal";
import { Plus, Settings } from "lucide-react";
function App() {
const [activeApp, setActiveApp] = useState<AppType>("claude");
@@ -31,6 +32,7 @@ function App() {
message: string;
onConfirm: () => void;
} | null>(null);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// 设置通知的辅助函数
@@ -218,9 +220,18 @@ function App() {
{/* Linear 风格的顶部导航 */}
<header className="bg-white border-b border-[var(--color-border)] px-6 py-4">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold text-[var(--color-primary)]">
CC Switch
</h1>
<div className="flex items-center gap-2">
<h1 className="text-xl font-semibold text-[var(--color-primary)]">
CC Switch
</h1>
<button
onClick={() => setIsSettingsOpen(true)}
className="p-1.5 text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] hover:bg-[var(--color-bg-tertiary)] rounded-md transition-all"
title="设置"
>
<Settings size={18} />
</button>
</div>
<div className="flex items-center gap-4">
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} />
@@ -317,6 +328,12 @@ function App() {
onCancel={() => setConfirmDialog(null)}
/>
)}
{isSettingsOpen && (
<SettingsModal
onClose={() => setIsSettingsOpen(false)}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,203 @@
import { useState, useEffect } from "react";
import { X, Info, RefreshCw, FolderOpen } from "lucide-react";
import "../lib/tauri-api";
interface Settings {
showInDock: boolean;
}
interface SettingsModalProps {
onClose: () => void;
}
export default function SettingsModal({ onClose }: SettingsModalProps) {
const [settings, setSettings] = useState<Settings>({
showInDock: true,
});
const [configPath, setConfigPath] = useState<string>("");
const [version] = useState("1.0.0");
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
useEffect(() => {
loadSettings();
loadConfigPath();
}, []);
const loadSettings = async () => {
try {
const loadedSettings = await window.api.getSettings();
if (loadedSettings?.showInDock !== undefined) {
setSettings({ showInDock: loadedSettings.showInDock });
}
} catch (error) {
console.error("加载设置失败:", error);
}
};
const loadConfigPath = async () => {
try {
const status = await window.api.getConfigStatus("claude");
if (status?.path) {
setConfigPath(status.path.replace("/claude_code_config.json", ""));
}
} catch (error) {
console.error("获取配置路径失败:", error);
}
};
const saveSettings = async () => {
try {
await window.api.saveSettings(settings);
onClose();
} catch (error) {
console.error("保存设置失败:", error);
}
};
const handleCheckUpdate = async () => {
setIsCheckingUpdate(true);
try {
await window.api.checkForUpdates();
} catch (error) {
console.error("检查更新失败:", error);
} finally {
setTimeout(() => setIsCheckingUpdate(false), 2000);
}
};
const handleOpenConfigFolder = async () => {
try {
await window.api.openConfigFolder("claude");
} catch (error) {
console.error("打开配置文件夹失败:", error);
}
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-2xl w-[500px] overflow-hidden">
{/* 标题栏 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--color-border)]">
<h2 className="text-lg font-semibold text-[var(--color-primary)]">
</h2>
<button
onClick={onClose}
className="p-1.5 hover:bg-[var(--color-bg-tertiary)] rounded-md transition-colors"
>
<X size={20} className="text-[var(--color-text-secondary)]" />
</button>
</div>
{/* 设置内容 */}
<div className="px-6 py-4 space-y-6">
{/* 显示设置 */}
<div>
<h3 className="text-sm font-medium text-[var(--color-text-primary)] mb-3">
</h3>
<label className="flex items-center justify-between">
<span className="text-sm text-[var(--color-text-secondary)]">
Dock macOS
</span>
<input
type="checkbox"
checked={settings.showInDock}
onChange={(e) =>
setSettings({ ...settings, showInDock: e.target.checked })
}
className="w-4 h-4 text-[var(--color-primary)] rounded focus:ring-[var(--color-primary)]/20"
/>
</label>
</div>
{/* 配置文件位置 */}
<div>
<h3 className="text-sm font-medium text-[var(--color-text-primary)] mb-3">
</h3>
<div className="flex items-center gap-2">
<div className="flex-1 px-3 py-2 bg-[var(--color-bg-tertiary)] rounded-lg">
<span className="text-xs font-mono text-[var(--color-text-secondary)]">
{configPath || "加载中..."}
</span>
</div>
<button
onClick={handleOpenConfigFolder}
className="p-2 hover:bg-[var(--color-bg-tertiary)] rounded-lg transition-colors"
title="打开文件夹"
>
<FolderOpen
size={18}
className="text-[var(--color-text-secondary)]"
/>
</button>
</div>
</div>
{/* 关于 */}
<div>
<h3 className="text-sm font-medium text-[var(--color-text-primary)] mb-3">
</h3>
<div className="p-4 bg-[var(--color-bg-tertiary)] rounded-lg">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<Info
size={18}
className="text-[var(--color-text-secondary)] mt-0.5"
/>
<div className="text-sm">
<p className="font-medium text-[var(--color-text-primary)]">
CC Switch
</p>
<p className="mt-1 text-[var(--color-text-secondary)]">
{version}
</p>
<p className="mt-2 text-xs text-[var(--color-text-tertiary)]">
Claude Code Codex MCP
</p>
</div>
</div>
<button
onClick={handleCheckUpdate}
disabled={isCheckingUpdate}
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-all ${
isCheckingUpdate
? "bg-[var(--color-bg-secondary)] text-[var(--color-text-tertiary)]"
: "bg-white hover:bg-[var(--color-bg-primary)] text-[var(--color-primary)]"
}`}
>
{isCheckingUpdate ? (
<span className="flex items-center gap-1">
<RefreshCw size={12} className="animate-spin" />
...
</span>
) : (
"检查更新"
)}
</button>
</div>
</div>
</div>
</div>
{/* 底部按钮 */}
<div className="flex justify-end gap-3 px-6 py-4 border-t border-[var(--color-border)]">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-tertiary)] rounded-lg transition-colors"
>
</button>
<button
onClick={saveSettings}
className="px-4 py-2 text-sm font-medium text-white bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] rounded-lg transition-colors"
>
</button>
</div>
</div>
</div>
);
}

View File

@@ -194,6 +194,35 @@ export const tauriAPI = {
console.warn("selectConfigFile 在 Tauri 版本中暂不支持");
return null;
},
// 获取设置
getSettings: async (): Promise<any> => {
try {
return await invoke("get_settings");
} catch (error) {
console.error("获取设置失败:", error);
return { showInDock: true };
}
},
// 保存设置
saveSettings: async (settings: any): Promise<boolean> => {
try {
return await invoke("save_settings", { settings });
} catch (error) {
console.error("保存设置失败:", error);
return false;
}
},
// 检查更新
checkForUpdates: async (): Promise<void> => {
try {
await invoke("check_for_updates");
} catch (error) {
console.error("检查更新失败:", error);
}
},
};
// 创建全局 API 对象,兼容现有代码

3
src/vite-env.d.ts vendored
View File

@@ -35,6 +35,9 @@ declare global {
onProviderSwitched: (
callback: (data: { appType: string; providerId: string }) => void,
) => Promise<UnlistenFn>;
getSettings: () => Promise<any>;
saveSettings: (settings: any) => Promise<boolean>;
checkForUpdates: () => Promise<void>;
};
platform: {
isMac: boolean;