diff --git a/package-lock.json b/package-lock.json index b4b392e..32a1ae0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", @@ -25,6 +26,7 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.15", + "@radix-ui/react-tooltip": "^1.2.8", "@tauri-apps/api": "^2.0.0", "@tauri-apps/plugin-shell": "^2.3.3", "class-variance-authority": "^0.7.1", @@ -1332,6 +1334,71 @@ } } }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", + "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -2188,6 +2255,58 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -2258,6 +2377,24 @@ } } }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", diff --git a/package.json b/package.json index d125fc7..1b8ec37 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", @@ -58,6 +59,7 @@ "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.15", + "@radix-ui/react-tooltip": "^1.2.8", "@tauri-apps/api": "^2.0.0", "@tauri-apps/plugin-shell": "^2.3.3", "class-variance-authority": "^0.7.1", diff --git a/src-tauri/src/services/tool/downloader.rs b/src-tauri/src/services/tool/downloader.rs index 4854187..564dcb9 100644 --- a/src-tauri/src/services/tool/downloader.rs +++ b/src-tauri/src/services/tool/downloader.rs @@ -16,16 +16,17 @@ pub enum DownloadEvent { /// 文件下载器 #[derive(Clone)] -pub struct FileDownloader { - client: reqwest::Client, -} +pub struct FileDownloader {} impl FileDownloader { pub fn new() -> Self { - Self { - client: crate::http_client::build_client() - .expect("Failed to create HTTP client for downloader"), - } + Self {} + } + + /// 创建 HTTP 客户端(每次调用时动态创建,以确保使用最新的代理配置) + fn create_client() -> Result { + crate::http_client::build_client() + .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {e}")) } /// 异步下载文件,支持进度回调 @@ -49,8 +50,8 @@ impl FileDownloader { } // 发起HTTP请求 - let response = self - .client + let client = Self::create_client()?; + let response = client .get(url) .send() .await @@ -127,7 +128,8 @@ impl FileDownloader { /// 获取文件大小(如果支持) pub async fn get_file_size(&self, url: &str) -> Result> { - match self.client.head(url).send().await { + let client = Self::create_client()?; + match client.head(url).send().await { Ok(response) => Ok(response.content_length()), Err(e) => { // 如果HEAD请求失败,可能是服务器不支持HEAD请求,记录但不阻断下载 diff --git a/src/components/layout/AppSidebar.tsx b/src/components/layout/AppSidebar.tsx index ae28d9f..f75875d 100644 --- a/src/components/layout/AppSidebar.tsx +++ b/src/components/layout/AppSidebar.tsx @@ -1,5 +1,7 @@ import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; +// 预留 +// import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { LayoutDashboard, Wrench, @@ -9,18 +11,61 @@ import { Radio, Settings as SettingsIcon, HelpCircle, + ChevronsLeft, + ChevronsRight, + Sun, + Moon, + Monitor, + //预留 + // User, } from 'lucide-react'; import DuckLogo from '@/assets/duck-logo.png'; import { useToast } from '@/hooks/use-toast'; +import { useTheme } from '@/hooks/useTheme'; +import { useState, useEffect } from 'react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, + DropdownMenuLabel, +} from '@/components/ui/dropdown-menu'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; interface AppSidebarProps { activeTab: string; onTabChange: (tab: string) => void; - restrictNavigation?: boolean; // 是否限制导航(引导模式) + restrictNavigation?: boolean; } +// 导航项配置 +const navigationItems = [ + { id: 'dashboard', label: '仪表板', icon: LayoutDashboard }, + { id: 'tool-management', label: '工具管理', icon: Wrench }, + { id: 'profile-management', label: '配置管理', icon: Settings2 }, + { id: 'statistics', label: '用量统计', icon: BarChart3 }, + { id: 'balance', label: '余额查询', icon: Wallet }, + { id: 'transparent-proxy', label: '透明代理', icon: Radio }, +]; + +const secondaryItems = [ + { id: 'help', label: '帮助', icon: HelpCircle }, + { id: 'settings', label: '设置', icon: SettingsIcon }, +]; + export function AppSidebar({ activeTab, onTabChange, restrictNavigation }: AppSidebarProps) { const { toast } = useToast(); + const { theme, actualTheme, setTheme } = useTheme(); + + const [isCollapsed, setIsCollapsed] = useState(() => { + const stored = localStorage.getItem('duckcoding-sidebar-collapsed'); + return stored === 'true'; + }); + + useEffect(() => { + localStorage.setItem('duckcoding-sidebar-collapsed', String(isCollapsed)); + }, [isCollapsed]); const handleTabChange = (tab: string) => { if (restrictNavigation && tab !== activeTab) { @@ -33,102 +78,222 @@ export function AppSidebar({ activeTab, onTabChange, restrictNavigation }: AppSi } onTabChange(tab); }; + + const ThemeIcon = actualTheme === 'dark' ? Moon : Sun; + + // 导航按钮组件 + const NavButton = ({ item }: { item: (typeof navigationItems)[0] }) => { + const Icon = item.icon; + const isActive = activeTab === item.id; + + const button = ( + + ); + + if (isCollapsed) { + return ( + + {button} + +

{item.label}

+
+
+ ); + } + + return button; + }; + return ( - + ); } diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..f98d678 --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; + +import { cn } from '@/lib/utils'; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..109dc84 --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import * as TooltipPrimitive from '@radix-ui/react-tooltip'; + +import { cn } from '@/lib/utils'; + +const TooltipProvider = TooltipPrimitive.Provider; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/src/hooks/useTheme.tsx b/src/hooks/useTheme.tsx new file mode 100644 index 0000000..0bd151a --- /dev/null +++ b/src/hooks/useTheme.tsx @@ -0,0 +1,69 @@ +import { createContext, useContext, useEffect, useState } from 'react'; + +type Theme = 'light' | 'dark' | 'system'; + +interface ThemeContextType { + theme: Theme; + actualTheme: 'light' | 'dark'; // 实际应用的主题(考虑系统设置) + setTheme: (theme: Theme) => void; +} + +const ThemeContext = createContext(undefined); + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + // 从 localStorage 读取主题设置,默认为 system + const [theme, setThemeState] = useState(() => { + const stored = localStorage.getItem('duckcoding-theme'); + return (stored as Theme) || 'system'; + }); + + // 获取系统主题偏好 + const getSystemTheme = (): 'light' | 'dark' => { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + }; + + // 计算实际应用的主题 + const actualTheme: 'light' | 'dark' = theme === 'system' ? getSystemTheme() : theme; + + // 设置主题 + const setTheme = (newTheme: Theme) => { + setThemeState(newTheme); + localStorage.setItem('duckcoding-theme', newTheme); + }; + + // 应用主题到 DOM + useEffect(() => { + const root = window.document.documentElement; + root.classList.remove('light', 'dark'); + root.classList.add(actualTheme); + }, [actualTheme]); + + // 监听系统主题变化 + useEffect(() => { + if (theme !== 'system') return; + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleChange = () => { + const root = window.document.documentElement; + root.classList.remove('light', 'dark'); + root.classList.add(getSystemTheme()); + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, [theme]); + + return ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} diff --git a/src/index.css b/src/index.css index 10c2d37..362b897 100644 --- a/src/index.css +++ b/src/index.css @@ -33,8 +33,8 @@ --card-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; + --primary: 189 94% 43%; + --primary-foreground: 210 40% 98%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; @@ -45,7 +45,7 @@ --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; + --ring: 189 94% 43%; } } diff --git a/src/main.tsx b/src/main.tsx index 9aa52ff..84b791d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,9 +2,12 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; +import { ThemeProvider } from './hooks/useTheme'; ReactDOM.createRoot(document.getElementById('root')!).render( - + + + , );