style: format codebase with Prettier\n\n- Apply Prettier across src to ensure consistent styling\n- No functional changes; whitespace and ordering only\n- Unblocks format:check for release pipeline

This commit is contained in:
Jason
2025-09-13 15:36:43 +08:00
parent c8327f7632
commit 6df5dfc123
14 changed files with 203 additions and 158 deletions

View File

@@ -116,7 +116,6 @@ function App() {
}
};
// 生成唯一ID
const generateId = () => {
return crypto.randomUUID();
@@ -204,7 +203,6 @@ function App() {
}
};
return (
<div className="min-h-screen flex flex-col bg-gray-50 dark:bg-gray-950">
{/* Linear 风格的顶部导航 */}
@@ -270,7 +268,6 @@ function App() {
onDelete={handleDeleteProvider}
onEdit={setEditingProviderId}
/>
</div>
</main>

View File

@@ -96,10 +96,15 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const [kimiAnthropicModel, setKimiAnthropicModel] = useState("");
const [kimiAnthropicSmallFastModel, setKimiAnthropicSmallFastModel] =
useState("");
// 初始化自定义模式的默认配置
useEffect(() => {
if (showPresets && selectedPreset === -1 && !initialData && formData.settingsConfig === "") {
if (
showPresets &&
selectedPreset === -1 &&
!initialData &&
formData.settingsConfig === ""
) {
// 设置自定义模板
const customTemplate = {
env: {
@@ -108,10 +113,10 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// 可选配置
// ANTHROPIC_MODEL: "your-model-name",
// ANTHROPIC_SMALL_FAST_MODEL: "your-fast-model-name"
}
},
};
setFormData(prev => ({
setFormData((prev) => ({
...prev,
settingsConfig: JSON.stringify(customTemplate, null, 2),
}));
@@ -138,7 +143,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
setClaudeModel(config.env.ANTHROPIC_MODEL || "");
setClaudeSmallFastModel(config.env.ANTHROPIC_SMALL_FAST_MODEL || "");
setBaseUrl(config.env.ANTHROPIC_BASE_URL || ""); // 初始化基础 URL
// 初始化 Kimi 模型选择
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
setKimiAnthropicSmallFastModel(
@@ -155,14 +160,18 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
if (!isCodex) {
if (selectedPreset !== null && selectedPreset >= 0) {
const preset = providerPresets[selectedPreset];
setCategory(preset?.category || (preset?.isOfficial ? "official" : undefined));
setCategory(
preset?.category || (preset?.isOfficial ? "official" : undefined),
);
} else if (selectedPreset === -1) {
setCategory("custom");
}
} else {
if (selectedCodexPreset !== null && selectedCodexPreset >= 0) {
const preset = codexProviderPresets[selectedCodexPreset];
setCategory(preset?.category || (preset?.isOfficial ? "official" : undefined));
setCategory(
preset?.category || (preset?.isOfficial ? "official" : undefined),
);
} else if (selectedCodexPreset === -1) {
setCategory("custom");
}
@@ -288,7 +297,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
websiteUrl: preset.websiteUrl,
settingsConfig: configString,
});
setCategory(preset.category || (preset.isOfficial ? "official" : undefined));
setCategory(
preset.category || (preset.isOfficial ? "official" : undefined),
);
// 设置选中的预设
setSelectedPreset(index);
@@ -307,11 +318,13 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
if (config.env) {
setClaudeModel(config.env.ANTHROPIC_MODEL || "");
setClaudeSmallFastModel(config.env.ANTHROPIC_SMALL_FAST_MODEL || "");
// 如果是 Kimi 预设,同步 Kimi 模型选择
if (preset.name?.includes("Kimi")) {
setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || "");
setKimiAnthropicSmallFastModel(config.env.ANTHROPIC_SMALL_FAST_MODEL || "");
setKimiAnthropicSmallFastModel(
config.env.ANTHROPIC_SMALL_FAST_MODEL || "",
);
}
} else {
setClaudeModel("");
@@ -323,7 +336,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// 处理点击自定义按钮
const handleCustomClick = () => {
setSelectedPreset(-1);
// 设置自定义模板
const customTemplate = {
env: {
@@ -332,9 +345,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// 可选配置
// ANTHROPIC_MODEL: "your-model-name",
// ANTHROPIC_SMALL_FAST_MODEL: "your-fast-model-name"
}
},
};
setFormData({
name: "",
websiteUrl: "",
@@ -366,7 +379,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
}));
setSelectedCodexPreset(index);
setCategory(preset.category || (preset.isOfficial ? "official" : undefined));
setCategory(
preset.category || (preset.isOfficial ? "official" : undefined),
);
// 清空 API Key让用户重新输入
setCodexApiKey("");
@@ -410,17 +425,17 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// 处理基础 URL 变化
const handleBaseUrlChange = (url: string) => {
setBaseUrl(url);
try {
const config = JSON.parse(formData.settingsConfig || "{}");
if (!config.env) {
config.env = {};
}
config.env.ANTHROPIC_BASE_URL = url.trim();
setFormData(prev => ({
setFormData((prev) => ({
...prev,
settingsConfig: JSON.stringify(config, null, 2)
settingsConfig: JSON.stringify(config, null, 2),
}));
} catch {
// ignore
@@ -442,7 +457,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// 根据当前配置决定是否展示 API Key 输入框
// 自定义模式(-1)也需要显示 API Key 输入框
const showApiKey =
(selectedPreset !== null) ||
selectedPreset !== null ||
(!showPresets && hasApiKeyField(formData.settingsConfig));
// 判断当前选中的预设是否是官方
@@ -474,13 +489,18 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
const showBaseUrlInput = selectedPreset === -1 && !isCodex;
// 判断是否显示"获取 API Key"链接(国产官方、聚合站和第三方显示)
const shouldShowApiKeyLink = !isCodex && !isOfficialPreset &&
(category === "cn_official" || category === "aggregator" || category === "third_party" ||
(selectedPreset !== null && selectedPreset >= 0 &&
(providerPresets[selectedPreset]?.category === "cn_official" ||
providerPresets[selectedPreset]?.category === "aggregator" ||
providerPresets[selectedPreset]?.category === "third_party")));
const shouldShowApiKeyLink =
!isCodex &&
!isOfficialPreset &&
(category === "cn_official" ||
category === "aggregator" ||
category === "third_party" ||
(selectedPreset !== null &&
selectedPreset >= 0 &&
(providerPresets[selectedPreset]?.category === "cn_official" ||
providerPresets[selectedPreset]?.category === "aggregator" ||
providerPresets[selectedPreset]?.category === "third_party")));
// 获取当前供应商的网址
const getCurrentWebsiteUrl = () => {
if (selectedPreset !== null && selectedPreset >= 0) {
@@ -522,16 +542,27 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
category === "official";
// 判断是否显示 Codex 的"获取 API Key"链接
const shouldShowCodexApiKeyLink = isCodex && !isCodexOfficialPreset &&
(category === "cn_official" || category === "aggregator" || category === "third_party" ||
(selectedCodexPreset !== null && selectedCodexPreset >= 0 &&
(codexProviderPresets[selectedCodexPreset]?.category === "cn_official" ||
codexProviderPresets[selectedCodexPreset]?.category === "aggregator" ||
codexProviderPresets[selectedCodexPreset]?.category === "third_party")));
const shouldShowCodexApiKeyLink =
isCodex &&
!isCodexOfficialPreset &&
(category === "cn_official" ||
category === "aggregator" ||
category === "third_party" ||
(selectedCodexPreset !== null &&
selectedCodexPreset >= 0 &&
(codexProviderPresets[selectedCodexPreset]?.category ===
"cn_official" ||
codexProviderPresets[selectedCodexPreset]?.category ===
"aggregator" ||
codexProviderPresets[selectedCodexPreset]?.category ===
"third_party")));
// 处理模型输入变化,自动更新 JSON 配置
const handleModelChange = (field: 'ANTHROPIC_MODEL' | 'ANTHROPIC_SMALL_FAST_MODEL', value: string) => {
if (field === 'ANTHROPIC_MODEL') {
const handleModelChange = (
field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL",
value: string,
) => {
if (field === "ANTHROPIC_MODEL") {
setClaudeModel(value);
} else {
setClaudeSmallFastModel(value);
@@ -539,16 +570,18 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
// 更新 JSON 配置
try {
const currentConfig = formData.settingsConfig ? JSON.parse(formData.settingsConfig) : { env: {} };
const currentConfig = formData.settingsConfig
? JSON.parse(formData.settingsConfig)
: { env: {} };
if (!currentConfig.env) currentConfig.env = {};
if (value.trim()) {
currentConfig.env[field] = value.trim();
} else {
delete currentConfig.env[field];
}
setFormData(prev => ({
setFormData((prev) => ({
...prev,
settingsConfig: JSON.stringify(currentConfig, null, 2),
}));
@@ -619,9 +652,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
<div className="relative bg-white rounded-xl shadow-lg max-w-3xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900">
{title}
</h2>
<h2 className="text-xl font-semibold text-gray-900">{title}</h2>
<button
type="button"
onClick={onClose}
@@ -636,13 +667,8 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
<div className="flex-1 overflow-auto p-6 space-y-6">
{error && (
<div className="flex items-center gap-3 p-4 bg-red-100 border border-red-500/20 rounded-lg">
<AlertCircle
size={20}
className="text-red-500 flex-shrink-0"
/>
<p className="text-red-500 text-sm font-medium">
{error}
</p>
<AlertCircle size={20} className="text-red-500 flex-shrink-0" />
<p className="text-red-500 text-sm font-medium">{error}</p>
</div>
)}
@@ -844,13 +870,15 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
type="text"
id="anthropicModel"
value={claudeModel}
onChange={(e) => handleModelChange('ANTHROPIC_MODEL', e.target.value)}
onChange={(e) =>
handleModelChange("ANTHROPIC_MODEL", e.target.value)
}
placeholder="例如: GLM-4.5"
autoComplete="off"
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
/>
</div>
<div className="space-y-2">
<label
htmlFor="anthropicSmallFastModel"
@@ -862,14 +890,19 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
type="text"
id="anthropicSmallFastModel"
value={claudeSmallFastModel}
onChange={(e) => handleModelChange('ANTHROPIC_SMALL_FAST_MODEL', e.target.value)}
onChange={(e) =>
handleModelChange(
"ANTHROPIC_SMALL_FAST_MODEL",
e.target.value,
)
}
placeholder="例如: GLM-4.5-Air"
autoComplete="off"
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
/>
</div>
</div>
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg">
<p className="text-xs text-amber-600">
💡 使
@@ -877,7 +910,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
</div>
</div>
)}
<ClaudeConfigEditor
value={formData.settingsConfig}
onChange={(value) =>

View File

@@ -34,10 +34,7 @@ const ApiKeyInput: React.FC<ApiKeyInputProps> = ({
return (
<div className="space-y-2">
<label
htmlFor={id}
className="block text-sm font-medium text-gray-900"
>
<label htmlFor={id} className="block text-sm font-medium text-gray-900">
{label} {required && "*"}
</label>
<div className="relative">

View File

@@ -36,9 +36,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
required
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors resize-y min-h-[8rem]"
/>
<p className="text-xs text-gray-500">
Codex auth.json
</p>
<p className="text-xs text-gray-500">Codex auth.json </p>
</div>
<div className="space-y-2">
@@ -56,9 +54,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
rows={8}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors resize-y min-h-[10rem]"
/>
<p className="text-xs text-gray-500">
Codex config.toml
</p>
<p className="text-xs text-gray-500">Codex config.toml </p>
</div>
</div>
);

View File

@@ -98,9 +98,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
onChange: (value: string) => void;
}> = ({ label, value, onChange }) => (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-900">
{label}
</label>
<label className="block text-sm font-medium text-gray-900">{label}</label>
<div className="relative">
<select
value={value}
@@ -132,9 +130,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-900">
</h3>
<h3 className="text-sm font-medium text-gray-900"></h3>
<button
type="button"
onClick={() => debouncedKey && fetchModelsWithKey(debouncedKey)}
@@ -148,10 +144,7 @@ const KimiModelSelector: React.FC<KimiModelSelectorProps> = ({
{error && (
<div className="flex items-center gap-2 p-3 bg-red-100 border border-red-500/20 rounded-lg">
<AlertCircle
size={16}
className="text-red-500 flex-shrink-0"
/>
<AlertCircle size={16} className="text-red-500 flex-shrink-0" />
<p className="text-red-500 text-xs">{error}</p>
</div>
)}

View File

@@ -25,7 +25,11 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
onCustomClick,
customLabel = "自定义",
}) => {
const getButtonClass = (index: number, isOfficial?: boolean, category?: ProviderCategory) => {
const getButtonClass = (
index: number,
isOfficial?: boolean,
category?: ProviderCategory,
) => {
const isSelected = selectedIndex === index;
const baseClass =
"inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors";
@@ -63,7 +67,7 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
<div className="flex flex-wrap gap-2">
<button
type="button"
className={`${getButtonClass(-1)} ${selectedIndex === -1 ? '' : ''}`}
className={`${getButtonClass(-1)} ${selectedIndex === -1 ? "" : ""}`}
onClick={onCustomClick}
>
{customLabel}
@@ -72,19 +76,23 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
<button
key={index}
type="button"
className={getButtonClass(index, preset.isOfficial, preset.category)}
className={getButtonClass(
index,
preset.isOfficial,
preset.category,
)}
onClick={() => onSelectPreset(index)}
>
{(preset.isOfficial || preset.category === "official") && <Zap size={14} />}
{(preset.isOfficial || preset.category === "official") && (
<Zap size={14} />
)}
{preset.name}
</button>
))}
</div>
</div>
{getDescription() && (
<p className="text-sm text-gray-500">
{getDescription()}
</p>
<p className="text-sm text-gray-500">{getDescription()}</p>
)}
</div>
);

View File

@@ -53,16 +53,16 @@ const ProviderList: React.FC<ProviderListProps> = ({
// 有时间戳的按时间升序排列
const timeA = a.createdAt || 0;
const timeB = b.createdAt || 0;
// 如果都没有时间戳,按名称排序
if (timeA === 0 && timeB === 0) {
return a.name.localeCompare(b.name, 'zh-CN');
return a.name.localeCompare(b.name, "zh-CN");
}
// 如果只有一个没有时间戳,没有时间戳的排在前面
if (timeA === 0) return -1;
if (timeB === 0) return 1;
// 都有时间戳,按时间升序
return timeA - timeB;
});
@@ -91,7 +91,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
<div
key={provider.id}
className={cn(
isCurrent ? cardStyles.selected : cardStyles.interactive
isCurrent ? cardStyles.selected : cardStyles.interactive,
)}
>
<div className="flex items-start justify-between">
@@ -140,7 +140,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors",
isCurrent
? "bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500 cursor-not-allowed"
: "bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
: "bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700",
)}
>
<Play size={14} />
@@ -162,7 +162,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
buttonStyles.icon,
isCurrent
? "text-gray-400 cursor-not-allowed"
: "text-gray-500 hover:text-red-500 hover:bg-red-100 dark:text-gray-400 dark:hover:text-red-400 dark:hover:bg-red-500/10"
: "text-gray-500 hover:text-red-500 hover:bg-red-100 dark:text-gray-400 dark:hover:text-red-400 dark:hover:bg-red-500/10",
)}
title="删除供应商"
>

View File

@@ -141,7 +141,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
// 如果未知或为空,回退到 releases 首页
if (!targetVersion || targetVersion === "未知") {
await window.api.openExternal(
"https://github.com/farion1231/cc-switch/releases"
"https://github.com/farion1231/cc-switch/releases",
);
return;
}
@@ -149,7 +149,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) {
? targetVersion
: `v${targetVersion}`;
await window.api.openExternal(
`https://github.com/farion1231/cc-switch/releases/tag/${tag}`
`https://github.com/farion1231/cc-switch/releases/tag/${tag}`,
);
} catch (error) {
console.error("打开更新日志失败:", error);

View File

@@ -8,12 +8,12 @@ interface UpdateBadgeProps {
export function UpdateBadge({ className = "", onClick }: UpdateBadgeProps) {
const { hasUpdate, updateInfo, isDismissed, dismissUpdate } = useUpdate();
// 如果没有更新或已关闭,不显示
if (!hasUpdate || isDismissed || !updateInfo) {
return null;
}
return (
<div
className={`

View File

@@ -52,7 +52,8 @@ export const providerPresets: ProviderPreset[] = [
websiteUrl: "https://bailian.console.aliyun.com",
settingsConfig: {
env: {
ANTHROPIC_BASE_URL: "https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy",
ANTHROPIC_BASE_URL:
"https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy",
ANTHROPIC_AUTH_TOKEN: "",
ANTHROPIC_MODEL: "qwen3-coder-plus",
ANTHROPIC_SMALL_FAST_MODEL: "qwen3-coder-plus",

View File

@@ -1,4 +1,11 @@
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from "react";
import React, {
createContext,
useContext,
useState,
useEffect,
useCallback,
useRef,
} from "react";
import type { UpdateInfo, UpdateHandle } from "../lib/updater";
import { checkForUpdate } from "../lib/updater";
@@ -9,11 +16,11 @@ interface UpdateContextValue {
updateHandle: UpdateHandle | null;
isChecking: boolean;
error: string | null;
// 提示状态
isDismissed: boolean;
dismissUpdate: () => void;
// 操作方法
checkUpdate: () => Promise<boolean>;
resetDismiss: () => void;
@@ -31,7 +38,7 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
const [isChecking, setIsChecking] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isDismissed, setIsDismissed] = useState(false);
// 从 localStorage 读取已关闭的版本
useEffect(() => {
const current = updateInfo?.availableVersion;
@@ -50,7 +57,7 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
setIsDismissed(dismissedVersion === current);
}, [updateInfo?.availableVersion]);
const isCheckingRef = useRef(false);
const checkUpdate = useCallback(async () => {
@@ -96,7 +103,7 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
isCheckingRef.current = false;
}
}, []);
const dismissUpdate = useCallback(() => {
setIsDismissed(true);
if (updateInfo?.availableVersion) {
@@ -105,13 +112,13 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
localStorage.removeItem(LEGACY_DISMISSED_KEY);
}
}, [updateInfo?.availableVersion]);
const resetDismiss = useCallback(() => {
setIsDismissed(false);
localStorage.removeItem(DISMISSED_VERSION_KEY);
localStorage.removeItem(LEGACY_DISMISSED_KEY);
}, []);
// 应用启动时自动检查更新
useEffect(() => {
// 延迟1秒后检查避免影响启动体验
@@ -121,7 +128,7 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
return () => clearTimeout(timer);
}, [checkUpdate]);
const value: UpdateContextValue = {
hasUpdate,
updateInfo,
@@ -133,11 +140,9 @@ export function UpdateProvider({ children }: { children: React.ReactNode }) {
checkUpdate,
resetDismiss,
};
return (
<UpdateContext.Provider value={value}>
{children}
</UpdateContext.Provider>
<UpdateContext.Provider value={value}>{children}</UpdateContext.Provider>
);
}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect } from "react";
export function useDarkMode() {
// 初始设为 false挂载后在 useEffect 中加载真实值
@@ -7,49 +7,51 @@ export function useDarkMode() {
// 组件挂载后加载初始值(兼容 Tauri 环境)
useEffect(() => {
if (typeof window === 'undefined') return;
if (typeof window === "undefined") return;
try {
// 尝试读取已保存的偏好
const saved = localStorage.getItem('darkMode');
const saved = localStorage.getItem("darkMode");
if (saved !== null) {
const savedBool = saved === 'true';
const savedBool = saved === "true";
setIsDarkMode(savedBool);
console.log('[DarkMode] Loaded from localStorage:', savedBool);
console.log("[DarkMode] Loaded from localStorage:", savedBool);
} else {
// 回退到系统偏好
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const prefersDark =
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches;
setIsDarkMode(prefersDark);
console.log('[DarkMode] Using system preference:', prefersDark);
console.log("[DarkMode] Using system preference:", prefersDark);
}
} catch (error) {
console.error('[DarkMode] Error loading preference:', error);
console.error("[DarkMode] Error loading preference:", error);
setIsDarkMode(false);
}
setIsInitialized(true);
}, []); // 仅在首次挂载时运行
// 将 dark 类应用到文档根节点
useEffect(() => {
if (!isInitialized) return;
// 添加短暂延迟以确保 Tauri 中 DOM 已就绪
const timer = setTimeout(() => {
try {
if (isDarkMode) {
document.documentElement.classList.add('dark');
console.log('[DarkMode] Added dark class to document');
document.documentElement.classList.add("dark");
console.log("[DarkMode] Added dark class to document");
} else {
document.documentElement.classList.remove('dark');
console.log('[DarkMode] Removed dark class from document');
document.documentElement.classList.remove("dark");
console.log("[DarkMode] Removed dark class from document");
}
// 检查类名是否已成功应用
const hasClass = document.documentElement.classList.contains('dark');
console.log('[DarkMode] Document has dark class:', hasClass);
const hasClass = document.documentElement.classList.contains("dark");
console.log("[DarkMode] Document has dark class:", hasClass);
} catch (error) {
console.error('[DarkMode] Error applying dark class:', error);
console.error("[DarkMode] Error applying dark class:", error);
}
}, 0);
@@ -59,19 +61,19 @@ export function useDarkMode() {
// 将偏好保存到 localStorage
useEffect(() => {
if (!isInitialized) return;
try {
localStorage.setItem('darkMode', isDarkMode.toString());
console.log('[DarkMode] Saved to localStorage:', isDarkMode);
localStorage.setItem("darkMode", isDarkMode.toString());
console.log("[DarkMode] Saved to localStorage:", isDarkMode);
} catch (error) {
console.error('[DarkMode] Error saving preference:', error);
console.error("[DarkMode] Error saving preference:", error);
}
}, [isDarkMode, isInitialized]);
const toggleDarkMode = () => {
setIsDarkMode(prev => {
setIsDarkMode((prev) => {
const newValue = !prev;
console.log('[DarkMode] Toggling from', prev, 'to', newValue);
console.log("[DarkMode] Toggling from", prev, "to", newValue);
return newValue;
});
};

View File

@@ -21,7 +21,9 @@ body {
}
/* 暗色模式下启用暗色原生控件/滚动条配色 */
html.dark { color-scheme: dark; }
html.dark {
color-scheme: dark;
}
/* 滚动条样式 */
::-webkit-scrollbar {

View File

@@ -5,20 +5,24 @@
// 按钮样式
export const buttonStyles = {
// 主按钮:蓝底白字
primary: "px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium",
primary:
"px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 transition-colors text-sm font-medium",
// 次按钮:灰背景,深色文本
secondary: "px-4 py-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 rounded-lg transition-colors text-sm font-medium",
secondary:
"px-4 py-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 rounded-lg transition-colors text-sm font-medium",
// 危险按钮:用于不可撤销/破坏性操作
danger: "px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-700 transition-colors text-sm font-medium",
danger:
"px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-700 transition-colors text-sm font-medium",
// 幽灵按钮:无背景,仅悬浮反馈
ghost: "px-4 py-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors text-sm font-medium",
ghost:
"px-4 py-2 text-gray-500 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors text-sm font-medium",
// 图标按钮:小尺寸,仅图标
icon: "p-1.5 text-gray-500 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors",
// 禁用态:可与其他样式组合
disabled: "opacity-50 cursor-not-allowed pointer-events-none",
} as const;
@@ -27,42 +31,49 @@ export const buttonStyles = {
export const cardStyles = {
// 基础卡片容器
base: "bg-white rounded-lg border border-gray-200 p-4 dark:bg-gray-900 dark:border-gray-700",
// 带悬浮效果的卡片
interactive: "bg-white rounded-lg border border-gray-200 p-4 hover:border-gray-300 hover:shadow-sm dark:bg-gray-900 dark:border-gray-700 dark:hover:border-gray-600 transition-all duration-200",
interactive:
"bg-white rounded-lg border border-gray-200 p-4 hover:border-gray-300 hover:shadow-sm dark:bg-gray-900 dark:border-gray-700 dark:hover:border-gray-600 transition-all duration-200",
// 选中/激活态卡片
selected: "bg-white rounded-lg border border-blue-500 ring-1 ring-blue-500/20 bg-blue-500/5 p-4 dark:bg-gray-900 dark:border-blue-400 dark:ring-blue-400/20 dark:bg-blue-400/10",
selected:
"bg-white rounded-lg border border-blue-500 ring-1 ring-blue-500/20 bg-blue-500/5 p-4 dark:bg-gray-900 dark:border-blue-400 dark:ring-blue-400/20 dark:bg-blue-400/10",
} as const;
// 输入控件样式
export const inputStyles = {
// 文本输入框
text: "w-full px-3 py-2 border border-gray-200 rounded-lg focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 outline-none dark:bg-gray-900 dark:border-gray-700 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:ring-blue-400/20 transition-colors",
// 下拉选择框
select: "w-full px-3 py-2 border border-gray-200 rounded-lg focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 outline-none bg-white dark:bg-gray-900 dark:border-gray-700 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:ring-blue-400/20 transition-colors",
select:
"w-full px-3 py-2 border border-gray-200 rounded-lg focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20 outline-none bg-white dark:bg-gray-900 dark:border-gray-700 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:ring-blue-400/20 transition-colors",
// 复选框
checkbox: "w-4 h-4 text-blue-500 rounded focus:ring-blue-500/20 border-gray-300 dark:border-gray-600 dark:bg-gray-800",
checkbox:
"w-4 h-4 text-blue-500 rounded focus:ring-blue-500/20 border-gray-300 dark:border-gray-600 dark:bg-gray-800",
} as const;
// 徽标Badge样式
export const badgeStyles = {
// 成功徽标
success: "inline-flex items-center gap-1 px-2 py-1 bg-green-500/10 text-green-500 rounded-md text-xs font-medium",
success:
"inline-flex items-center gap-1 px-2 py-1 bg-green-500/10 text-green-500 rounded-md text-xs font-medium",
// 信息徽标
info: "inline-flex items-center gap-1 px-2 py-1 bg-blue-500/10 text-blue-500 rounded-md text-xs font-medium",
// 警告徽标
warning: "inline-flex items-center gap-1 px-2 py-1 bg-amber-500/10 text-amber-500 rounded-md text-xs font-medium",
warning:
"inline-flex items-center gap-1 px-2 py-1 bg-amber-500/10 text-amber-500 rounded-md text-xs font-medium",
// 错误徽标
error: "inline-flex items-center gap-1 px-2 py-1 bg-red-500/10 text-red-500 rounded-md text-xs font-medium",
error:
"inline-flex items-center gap-1 px-2 py-1 bg-red-500/10 text-red-500 rounded-md text-xs font-medium",
} as const;
// 组合类名的工具函数
export function cn(...classes: (string | undefined | false)[]) {
return classes.filter(Boolean).join(' ');
return classes.filter(Boolean).join(" ");
}