增加主题快速切换按钮
This commit is contained in:
@@ -54,7 +54,7 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
|
|||||||
const [currentConfig, setCurrentConfig] = useState<Record<string, string | null>>({});
|
const [currentConfig, setCurrentConfig] = useState<Record<string, string | null>>({});
|
||||||
const [loadingConfig, setLoadingConfig] = useState(false);
|
const [loadingConfig, setLoadingConfig] = useState(false);
|
||||||
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
||||||
|
|
||||||
// PackyCode 额度相关状态
|
// PackyCode 额度相关状态
|
||||||
const [quotaData, setQuotaData] = useState<Record<string, PackycodeUserQuota>>({});
|
const [quotaData, setQuotaData] = useState<Record<string, PackycodeUserQuota>>({});
|
||||||
const [loadingQuota, setLoadingQuota] = useState<Record<string, boolean>>({});
|
const [loadingQuota, setLoadingQuota] = useState<Record<string, boolean>>({});
|
||||||
@@ -399,7 +399,7 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
<div className="w-full h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`h-full transition-all ${
|
className={`h-full transition-all ${
|
||||||
(() => {
|
(() => {
|
||||||
const daily_spent = Number(quotaData[station.id].daily_spent_usd || 0);
|
const daily_spent = Number(quotaData[station.id].daily_spent_usd || 0);
|
||||||
@@ -438,7 +438,7 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
<div className="w-full h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={`h-full transition-all ${
|
className={`h-full transition-all ${
|
||||||
(() => {
|
(() => {
|
||||||
const monthly_spent = Number(quotaData[station.id].monthly_spent_usd || 0);
|
const monthly_spent = Number(quotaData[station.id].monthly_spent_usd || 0);
|
||||||
@@ -499,6 +499,7 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
|
|||||||
setShowEditDialog(true);
|
setShowEditDialog(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
测试
|
||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -617,8 +618,8 @@ const CreateStationDialog: React.FC<{
|
|||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
auth_method: 'api_key', // PackyCode 固定使用 API Key
|
auth_method: 'api_key', // PackyCode 固定使用 API Key
|
||||||
api_url: packycodeService === 'taxi'
|
api_url: packycodeService === 'taxi'
|
||||||
? 'https://share-api.packycode.com'
|
? 'https://share-api.packycode.com'
|
||||||
: packycodeNode
|
: packycodeNode
|
||||||
}));
|
}));
|
||||||
} else if (formData.adapter === 'custom') {
|
} else if (formData.adapter === 'custom') {
|
||||||
@@ -638,10 +639,10 @@ const CreateStationDialog: React.FC<{
|
|||||||
const fillStationName = (serviceType: string) => {
|
const fillStationName = (serviceType: string) => {
|
||||||
const serviceName = serviceType === 'taxi' ? t('relayStation.taxiService') : t('relayStation.busService');
|
const serviceName = serviceType === 'taxi' ? t('relayStation.taxiService') : t('relayStation.busService');
|
||||||
const newName = `PackyCode ${serviceName}`;
|
const newName = `PackyCode ${serviceName}`;
|
||||||
|
|
||||||
// 如果名称为空,或者当前名称是之前自动生成的PackyCode名称,则更新
|
// 如果名称为空,或者当前名称是之前自动生成的PackyCode名称,则更新
|
||||||
if (!formData.name.trim() ||
|
if (!formData.name.trim() ||
|
||||||
formData.name.startsWith('PackyCode ') ||
|
formData.name.startsWith('PackyCode ') ||
|
||||||
formData.name === `PackyCode ${t('relayStation.taxiService')}` ||
|
formData.name === `PackyCode ${t('relayStation.taxiService')}` ||
|
||||||
formData.name === `PackyCode ${t('relayStation.busService')}`) {
|
formData.name === `PackyCode ${t('relayStation.busService')}`) {
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
@@ -746,7 +747,7 @@ const CreateStationDialog: React.FC<{
|
|||||||
<div className="text-xs opacity-80 mt-1">{t('relayStation.taxiServiceDesc')}</div>
|
<div className="text-xs opacity-80 mt-1">{t('relayStation.taxiServiceDesc')}</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={packycodeService === 'bus' ? 'default' : 'outline'}
|
variant={packycodeService === 'bus' ? 'default' : 'outline'}
|
||||||
@@ -768,7 +769,7 @@ const CreateStationDialog: React.FC<{
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-3">
|
<p className="text-xs text-muted-foreground mt-3">
|
||||||
{packycodeService === 'taxi'
|
{packycodeService === 'taxi'
|
||||||
? `${t('relayStation.fixedUrl')}: https://share-api.packycode.com`
|
? `${t('relayStation.fixedUrl')}: https://share-api.packycode.com`
|
||||||
: t('relayStation.busServiceNote')
|
: t('relayStation.busServiceNote')
|
||||||
}
|
}
|
||||||
@@ -830,9 +831,9 @@ const CreateStationDialog: React.FC<{
|
|||||||
const best = await api.autoSelectBestNode();
|
const best = await api.autoSelectBestNode();
|
||||||
setPackycodeNode(best.url);
|
setPackycodeNode(best.url);
|
||||||
setFormData(prev => ({ ...prev, api_url: best.url }));
|
setFormData(prev => ({ ...prev, api_url: best.url }));
|
||||||
setFormToast({
|
setFormToast({
|
||||||
message: `已选择最快节点: ${best.name} (延迟: ${best.response_time}ms)`,
|
message: `已选择最快节点: ${best.name} (延迟: ${best.response_time}ms)`,
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFormToast({ message: "节点测速失败", type: "error" });
|
setFormToast({ message: "节点测速失败", type: "error" });
|
||||||
@@ -842,7 +843,7 @@ const CreateStationDialog: React.FC<{
|
|||||||
自动选择
|
自动选择
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t('relayStation.selectedNode') + ': ' + packycodeNode}
|
{t('relayStation.selectedNode') + ': ' + packycodeNode}
|
||||||
</p>
|
</p>
|
||||||
@@ -967,8 +968,8 @@ const CreateStationDialog: React.FC<{
|
|||||||
<Button type="button" variant="outline" onClick={() => {}}>
|
<Button type="button" variant="outline" onClick={() => {}}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="min-w-[120px]"
|
className="min-w-[120px]"
|
||||||
>
|
>
|
||||||
@@ -1010,7 +1011,7 @@ const EditStationDialog: React.FC<{
|
|||||||
});
|
});
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [formToast, setFormToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
const [formToast, setFormToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
||||||
|
|
||||||
// PackyCode 特定状态
|
// PackyCode 特定状态
|
||||||
const [packycodeService, setPackycodeService] = useState<string>(() => {
|
const [packycodeService, setPackycodeService] = useState<string>(() => {
|
||||||
// 从API URL判断服务类型
|
// 从API URL判断服务类型
|
||||||
@@ -1035,8 +1036,8 @@ const EditStationDialog: React.FC<{
|
|||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
auth_method: 'api_key', // PackyCode 固定使用 API Key
|
auth_method: 'api_key', // PackyCode 固定使用 API Key
|
||||||
api_url: packycodeService === 'taxi'
|
api_url: packycodeService === 'taxi'
|
||||||
? 'https://share-api.packycode.com'
|
? 'https://share-api.packycode.com'
|
||||||
: packycodeNode
|
: packycodeNode
|
||||||
}));
|
}));
|
||||||
} else if (formData.adapter === 'custom') {
|
} else if (formData.adapter === 'custom') {
|
||||||
@@ -1150,7 +1151,7 @@ const EditStationDialog: React.FC<{
|
|||||||
<div className="text-xs opacity-80 mt-1">{t('relayStation.taxiServiceDesc')}</div>
|
<div className="text-xs opacity-80 mt-1">{t('relayStation.taxiServiceDesc')}</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={packycodeService === 'bus' ? 'default' : 'outline'}
|
variant={packycodeService === 'bus' ? 'default' : 'outline'}
|
||||||
@@ -1171,7 +1172,7 @@ const EditStationDialog: React.FC<{
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-3">
|
<p className="text-xs text-muted-foreground mt-3">
|
||||||
{packycodeService === 'taxi'
|
{packycodeService === 'taxi'
|
||||||
? `${t('relayStation.fixedUrl')}: https://share-api.packycode.com`
|
? `${t('relayStation.fixedUrl')}: https://share-api.packycode.com`
|
||||||
: t('relayStation.busServiceNote')
|
: t('relayStation.busServiceNote')
|
||||||
}
|
}
|
||||||
@@ -1227,9 +1228,9 @@ const EditStationDialog: React.FC<{
|
|||||||
const best = await api.autoSelectBestNode();
|
const best = await api.autoSelectBestNode();
|
||||||
setPackycodeNode(best.url);
|
setPackycodeNode(best.url);
|
||||||
setFormData(prev => ({ ...prev, api_url: best.url }));
|
setFormData(prev => ({ ...prev, api_url: best.url }));
|
||||||
setFormToast({
|
setFormToast({
|
||||||
message: `已选择最快节点: ${best.name} (延迟: ${best.response_time}ms)`,
|
message: `已选择最快节点: ${best.name} (延迟: ${best.response_time}ms)`,
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFormToast({ message: "节点测速失败", type: "error" });
|
setFormToast({ message: "节点测速失败", type: "error" });
|
||||||
@@ -1239,7 +1240,7 @@ const EditStationDialog: React.FC<{
|
|||||||
自动选择
|
自动选择
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t('relayStation.selectedNode') + ': ' + packycodeNode}
|
{t('relayStation.selectedNode') + ': ' + packycodeNode}
|
||||||
</p>
|
</p>
|
||||||
@@ -1364,8 +1365,8 @@ const EditStationDialog: React.FC<{
|
|||||||
<Button type="button" variant="outline" onClick={onCancel}>
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="min-w-[120px]"
|
className="min-w-[120px]"
|
||||||
>
|
>
|
||||||
|
119
src/components/ThemeSwitcher.tsx
Normal file
119
src/components/ThemeSwitcher.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Sun, Moon, Monitor, Palette, Check } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { useTheme } from '@/hooks/useTheme';
|
||||||
|
import { useTranslation } from '@/hooks/useTranslation';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface ThemeSwitcherProps {
|
||||||
|
className?: string;
|
||||||
|
showText?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题快速切换组件
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <ThemeSwitcher />
|
||||||
|
* <ThemeSwitcher showText={true} className="ml-2" />
|
||||||
|
*/
|
||||||
|
export const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({
|
||||||
|
className,
|
||||||
|
showText = false
|
||||||
|
}) => {
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
const { currentLanguage } = useTranslation();
|
||||||
|
|
||||||
|
const themes = [
|
||||||
|
{
|
||||||
|
key: 'light',
|
||||||
|
name: currentLanguage === 'zh' ? '浅色' : 'Light',
|
||||||
|
icon: Sun,
|
||||||
|
description: currentLanguage === 'zh' ? '明亮模式' : 'Bright mode'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gray',
|
||||||
|
name: currentLanguage === 'zh' ? '灰色' : 'Gray',
|
||||||
|
icon: Monitor,
|
||||||
|
description: currentLanguage === 'zh' ? '舒适模式' : 'Comfortable mode'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dark',
|
||||||
|
name: currentLanguage === 'zh' ? '深色' : 'Dark',
|
||||||
|
icon: Moon,
|
||||||
|
description: currentLanguage === 'zh' ? '暗黑模式' : 'Dark mode'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'custom',
|
||||||
|
name: currentLanguage === 'zh' ? '自定义' : 'Custom',
|
||||||
|
icon: Palette,
|
||||||
|
description: currentLanguage === 'zh' ? '个性化' : 'Personalized'
|
||||||
|
}
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const getCurrentThemeIcon = () => {
|
||||||
|
const currentTheme = themes.find(t => t.key === theme);
|
||||||
|
const IconComponent = currentTheme?.icon || Monitor;
|
||||||
|
return IconComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentThemeName = () => {
|
||||||
|
const currentTheme = themes.find(t => t.key === theme);
|
||||||
|
return currentTheme?.name || (currentLanguage === 'zh' ? '主题' : 'Theme');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThemeChange = async (themeKey: string) => {
|
||||||
|
try {
|
||||||
|
await setTheme(themeKey as any);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to change theme:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const IconComponent = getCurrentThemeIcon();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={cn("gap-2", className)}
|
||||||
|
title={currentLanguage === 'zh' ? '切换主题' : 'Switch theme'}
|
||||||
|
>
|
||||||
|
<IconComponent className="h-4 w-4" />
|
||||||
|
{showText && <span className="hidden sm:inline">{getCurrentThemeName()}</span>}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
|
{themes.map((themeOption) => {
|
||||||
|
const ThemeIcon = themeOption.icon;
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={themeOption.key}
|
||||||
|
onClick={() => handleThemeChange(themeOption.key)}
|
||||||
|
className="flex items-center justify-between cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<ThemeIcon className="h-4 w-4" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{themeOption.name}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{themeOption.description}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{theme === themeOption.key && (
|
||||||
|
<Check className="h-4 w-4 text-primary" />
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
@@ -4,6 +4,7 @@ import { Circle, FileText, Settings, ExternalLink, BarChart3, Network, Info, Bot
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Popover } from "@/components/ui/popover";
|
import { Popover } from "@/components/ui/popover";
|
||||||
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
||||||
|
import { ThemeSwitcher } from "@/components/ThemeSwitcher";
|
||||||
import { useTranslation } from "@/hooks/useTranslation";
|
import { useTranslation } from "@/hooks/useTranslation";
|
||||||
import { api, type ClaudeVersionStatus } from "@/lib/api";
|
import { api, type ClaudeVersionStatus } from "@/lib/api";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -240,6 +241,9 @@ export const Topbar: React.FC<TopbarProps> = ({
|
|||||||
{/* Language Switcher */}
|
{/* Language Switcher */}
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
|
|
||||||
|
{/* Theme Switcher */}
|
||||||
|
<ThemeSwitcher />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
Reference in New Issue
Block a user