diff --git a/frontend/package.json b/frontend/package.json index d63ed1b..4270933 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,15 +11,15 @@ "test:unit": "node --experimental-strip-types --test src/utils/*.test.mjs" }, "dependencies": { - "@ant-design/icons": "^6.3.2", "@monaco-editor/react": "^4.7.0", "antd": "^6.5.4", "axios": "^1.8.4", + "dayjs": "^1.11.19", + "lucide-react": "^0.544.0", "qrcode": "^1.5.4", "react": "^19.2.8", "react-dom": "^19.2.8", "react-router-dom": "^7.18.2", - "dayjs": "^1.11.19", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 357ab81..112129a 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -6,7 +6,7 @@ import { UserOutlined, GlobalOutlined, BgColorsOutlined, -} from '@ant-design/icons'; +} from '../icons'; import { useAuthStore } from '../stores/authStore'; import { useThemeStore, ThemeMode } from '../stores/themeStore'; import { useI18n, LOCALE_OPTIONS, type Locale } from '../i18n'; diff --git a/frontend/src/components/ListenerConfigFields.tsx b/frontend/src/components/ListenerConfigFields.tsx index 79dab0f..972394c 100644 --- a/frontend/src/components/ListenerConfigFields.tsx +++ b/frontend/src/components/ListenerConfigFields.tsx @@ -3,7 +3,7 @@ import { message, Form, Input, InputNumber, Select, Switch, Divider, Alert, Space, Typography, Button, Card, Radio, } from 'antd'; import { generateMaterial } from '../api/listeners'; -import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { MinusCircleOutlined, PlusOutlined } from '../icons'; import { useI18n } from '../i18n'; import RealityTargetFields from './RealityTargetFields'; diff --git a/frontend/src/components/ListenerRuntime.tsx b/frontend/src/components/ListenerRuntime.tsx index d475418..e8dc57f 100644 --- a/frontend/src/components/ListenerRuntime.tsx +++ b/frontend/src/components/ListenerRuntime.tsx @@ -1,5 +1,5 @@ import { Alert, Button, Descriptions, Drawer, Space, Table, Tag, Typography } from 'antd'; -import { ReloadOutlined } from '@ant-design/icons'; +import { ReloadOutlined } from '../icons'; import type { ListenerRuntime } from '../api/listenerRuntime'; import type { Listener } from '../api/nodes'; import { listenerAvailability, listenerDiagnosticReason } from '../utils/listenerAvailability'; diff --git a/frontend/src/components/MobileBottomNav.tsx b/frontend/src/components/MobileBottomNav.tsx index 9fc22b6..1f7c311 100644 --- a/frontend/src/components/MobileBottomNav.tsx +++ b/frontend/src/components/MobileBottomNav.tsx @@ -14,7 +14,7 @@ import { ProfileOutlined, ControlOutlined, LogoutOutlined, -} from '@ant-design/icons'; +} from '../icons'; import { useLocation, useNavigate } from 'react-router-dom'; import { useI18n } from '../i18n'; import { useAuthStore } from '../stores/authStore'; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 379c99e..ab5126c 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -13,7 +13,7 @@ import { ControlOutlined, ToolOutlined, LogoutOutlined, -} from '@ant-design/icons'; +} from '../icons'; import { useNavigate, useLocation } from 'react-router-dom'; import { useAuthStore } from '../stores/authStore'; import { useI18n } from '../i18n'; diff --git a/frontend/src/icons.tsx b/frontend/src/icons.tsx new file mode 100644 index 0000000..59010b4 --- /dev/null +++ b/frontend/src/icons.tsx @@ -0,0 +1,166 @@ +/** + * Panel icons — Lucide under the hood, Ant Design–style *Outlined names for drop-in replacement. + * Import from `../icons` or `../../icons` instead of `@ant-design/icons`. + */ +import React from 'react'; +import type { LucideProps } from 'lucide-react'; +import { + Plus, + RefreshCw, + Trash2, + Pencil, + Globe, + Palette, + Users, + Link2, + Share2, + Copy, + LayoutDashboard, + FileText, + Server, + User, + Lock, + Shield, + PlayCircle, + Square, + RotateCcw, + Download, + Send, + Eraser, + MoreHorizontal, + QrCode, + Save, + BadgeCheck, + LayoutGrid, + Boxes, + MonitorPlay, + GitFork, + Rocket, + SlidersHorizontal, + Wrench, + LogOut, + ArrowLeft, + Undo2, + HeartPulse, + RefreshCcw, + Monitor, + HardDrive, + LogIn, + LineChart, + Check, + GitBranch, + History, + Power, + FileDiff, + Info, + CloudDownload, + CloudUpload, + Cable, + Settings, + Bell, + Network, + Timer, + PanelLeftClose, + PanelLeftOpen, + MinusCircle, +} from 'lucide-react'; + +type IconProps = LucideProps & { + /** Ant Design icons often pass style.fontSize — map to Lucide `size`. */ + style?: React.CSSProperties & { fontSize?: number | string }; + className?: string; + twoToneColor?: string; + spin?: boolean; + rotate?: number; +}; + +function wrap(Icon: React.ComponentType, displayName: string) { + const Comp = React.forwardRef(function LucideAntIcon( + { style, className, spin, rotate, size, ...rest }, + ref, + ) { + const fontSize = style?.fontSize; + const resolvedSize = + size ?? + (typeof fontSize === 'number' ? fontSize : typeof fontSize === 'string' && fontSize.endsWith('px') ? parseFloat(fontSize) : 16); + const mergedStyle: React.CSSProperties = { + ...style, + ...(spin ? { animation: 'spin 1s linear infinite' } : null), + ...(rotate != null ? { transform: `rotate(${rotate}deg)` } : null), + verticalAlign: style?.verticalAlign ?? 'middle', + }; + return ( + + ); + }); + Comp.displayName = displayName; + return Comp; +} + +export const PlusOutlined = wrap(Plus, 'PlusOutlined'); +export const ReloadOutlined = wrap(RefreshCw, 'ReloadOutlined'); +export const DeleteOutlined = wrap(Trash2, 'DeleteOutlined'); +export const EditOutlined = wrap(Pencil, 'EditOutlined'); +export const GlobalOutlined = wrap(Globe, 'GlobalOutlined'); +export const BgColorsOutlined = wrap(Palette, 'BgColorsOutlined'); +export const TeamOutlined = wrap(Users, 'TeamOutlined'); +export const LinkOutlined = wrap(Link2, 'LinkOutlined'); +export const ShareAltOutlined = wrap(Share2, 'ShareAltOutlined'); +export const CopyOutlined = wrap(Copy, 'CopyOutlined'); +export const DashboardOutlined = wrap(LayoutDashboard, 'DashboardOutlined'); +export const ProfileOutlined = wrap(FileText, 'ProfileOutlined'); +export const CloudServerOutlined = wrap(Server, 'CloudServerOutlined'); +export const UserOutlined = wrap(User, 'UserOutlined'); +export const LockOutlined = wrap(Lock, 'LockOutlined'); +export const SafetyOutlined = wrap(Shield, 'SafetyOutlined'); +export const PlayCircleOutlined = wrap(PlayCircle, 'PlayCircleOutlined'); +export const StopOutlined = wrap(Square, 'StopOutlined'); +export const RedoOutlined = wrap(RotateCcw, 'RedoOutlined'); +export const DownloadOutlined = wrap(Download, 'DownloadOutlined'); +export const SendOutlined = wrap(Send, 'SendOutlined'); +export const ClearOutlined = wrap(Eraser, 'ClearOutlined'); +export const MoreOutlined = wrap(MoreHorizontal, 'MoreOutlined'); +export const QrcodeOutlined = wrap(QrCode, 'QrcodeOutlined'); +export const FileTextOutlined = wrap(FileText, 'FileTextOutlined'); +export const SaveOutlined = wrap(Save, 'SaveOutlined'); +export const SafetyCertificateOutlined = wrap(BadgeCheck, 'SafetyCertificateOutlined'); +export const AppstoreOutlined = wrap(LayoutGrid, 'AppstoreOutlined'); +export const DeploymentUnitOutlined = wrap(Boxes, 'DeploymentUnitOutlined'); +export const FundProjectionScreenOutlined = wrap(MonitorPlay, 'FundProjectionScreenOutlined'); +export const ForkOutlined = wrap(GitFork, 'ForkOutlined'); +export const RocketOutlined = wrap(Rocket, 'RocketOutlined'); +export const ControlOutlined = wrap(SlidersHorizontal, 'ControlOutlined'); +export const ToolOutlined = wrap(Wrench, 'ToolOutlined'); +export const LogoutOutlined = wrap(LogOut, 'LogoutOutlined'); +export const ArrowLeftOutlined = wrap(ArrowLeft, 'ArrowLeftOutlined'); +export const RollbackOutlined = wrap(Undo2, 'RollbackOutlined'); +export const MedicineBoxOutlined = wrap(HeartPulse, 'MedicineBoxOutlined'); +export const CloudSyncOutlined = wrap(RefreshCcw, 'CloudSyncOutlined'); +export const DesktopOutlined = wrap(Monitor, 'DesktopOutlined'); +export const HddOutlined = wrap(HardDrive, 'HddOutlined'); +export const LoginOutlined = wrap(LogIn, 'LoginOutlined'); +export const FundOutlined = wrap(LineChart, 'FundOutlined'); +export const CheckOutlined = wrap(Check, 'CheckOutlined'); +export const BranchesOutlined = wrap(GitBranch, 'BranchesOutlined'); +export const HistoryOutlined = wrap(History, 'HistoryOutlined'); +export const PoweroffOutlined = wrap(Power, 'PoweroffOutlined'); +export const DiffOutlined = wrap(FileDiff, 'DiffOutlined'); +export const InfoCircleOutlined = wrap(Info, 'InfoCircleOutlined'); +export const CloudDownloadOutlined = wrap(CloudDownload, 'CloudDownloadOutlined'); +export const CloudUploadOutlined = wrap(CloudUpload, 'CloudUploadOutlined'); +export const ApiOutlined = wrap(Cable, 'ApiOutlined'); +export const SettingOutlined = wrap(Settings, 'SettingOutlined'); +export const BellOutlined = wrap(Bell, 'BellOutlined'); +export const ClusterOutlined = wrap(Network, 'ClusterOutlined'); +export const FieldTimeOutlined = wrap(Timer, 'FieldTimeOutlined'); +export const MenuFoldOutlined = wrap(PanelLeftClose, 'MenuFoldOutlined'); +export const MenuUnfoldOutlined = wrap(PanelLeftOpen, 'MenuUnfoldOutlined'); +export const MinusCircleOutlined = wrap(MinusCircle, 'MinusCircleOutlined'); diff --git a/frontend/src/pages/Cluster.tsx b/frontend/src/pages/Cluster.tsx index 74d82d3..b26604c 100644 --- a/frontend/src/pages/Cluster.tsx +++ b/frontend/src/pages/Cluster.tsx @@ -14,7 +14,7 @@ import { HddOutlined, LoginOutlined, ReloadOutlined, -} from '@ant-design/icons'; +} from '../icons'; import { fetchCluster, createClusterNode, diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index 4c9459f..79d422d 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Card, Table, Button, Space, Modal, Form, Input, Select, message, Popconfirm, Tabs } from 'antd'; -import { PlusOutlined, DeleteOutlined, EditOutlined, DownloadOutlined, CheckOutlined, FileTextOutlined } from '@ant-design/icons'; +import { PlusOutlined, DeleteOutlined, EditOutlined, DownloadOutlined, CheckOutlined, FileTextOutlined } from '../icons'; import { fetchProxies, createProxy, updateProxy, deleteProxy, fetchConfigYAML, generateConfig, validateConfigYAML, applyConfigYAML, rollbackConfig, diff --git a/frontend/src/pages/Core.tsx b/frontend/src/pages/Core.tsx index 12b5456..49f9159 100644 --- a/frontend/src/pages/Core.tsx +++ b/frontend/src/pages/Core.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from 'react'; import { Alert, Button, Card, Descriptions, Modal, Select, Space, Spin, Steps, Typography, message } from 'antd'; -import { PlayCircleOutlined, StopOutlined, RedoOutlined, DownloadOutlined, RollbackOutlined } from '@ant-design/icons'; +import { PlayCircleOutlined, StopOutlined, RedoOutlined, DownloadOutlined, RollbackOutlined } from '../icons'; import { coreAPI, type CoreStatus, type CoreRelease, type CoreUpdateStatus } from '../api/core'; import { isCanceledError } from '../api/client'; import { useI18n } from '../i18n'; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 82d6b5c..1c7bf76 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Card, Row, Col, Statistic, Button, Space, Tag, Progress, Typography, message, theme } from 'antd'; -import { PlayCircleOutlined, StopOutlined, RedoOutlined } from '@ant-design/icons'; +import { PlayCircleOutlined, StopOutlined, RedoOutlined } from '../icons'; import { fetchDashboard, startMihomo, stopMihomo, restartMihomo } from '../api/system'; import { isCanceledError } from '../api/client'; import { useI18n } from '../i18n'; diff --git a/frontend/src/pages/Listeners.tsx b/frontend/src/pages/Listeners.tsx index 80b2424..e215c36 100644 --- a/frontend/src/pages/Listeners.tsx +++ b/frontend/src/pages/Listeners.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Table, Button, Space, Tag, Modal, Form, Input, InputNumber, Select, Switch, message, Popconfirm, Tooltip, Card, Tabs, Descriptions, Divider, Dropdown, Checkbox, Spin, Alert } from 'antd'; -import { PlusOutlined, ReloadOutlined, QrcodeOutlined, DeleteOutlined, EditOutlined, CopyOutlined, BranchesOutlined, HistoryOutlined, SaveOutlined, PoweroffOutlined, DiffOutlined, MoreOutlined, SafetyCertificateOutlined } from '@ant-design/icons'; +import { PlusOutlined, ReloadOutlined, QrcodeOutlined, DeleteOutlined, EditOutlined, CopyOutlined, BranchesOutlined, HistoryOutlined, SaveOutlined, PoweroffOutlined, DiffOutlined, MoreOutlined, SafetyCertificateOutlined } from '../icons'; import { fetchListeners, createListener, quickCreateListener, updateListener, deleteListener, reloadListener, exportNodeURI, normalizeId, Listener, } from '../api/nodes'; diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index cf47687..c94ee0c 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -7,7 +7,7 @@ import { BgColorsOutlined, SafetyOutlined, ArrowLeftOutlined, -} from '@ant-design/icons'; +} from '../icons'; import { useNavigate, useLocation } from 'react-router-dom'; import { login } from '../api/auth'; import { useAuthStore } from '../stores/authStore'; diff --git a/frontend/src/pages/Logs.tsx b/frontend/src/pages/Logs.tsx index 650f6cc..d84628b 100644 --- a/frontend/src/pages/Logs.tsx +++ b/frontend/src/pages/Logs.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Card, List, Tag, Button, Space, Empty, Spin, message, theme } from 'antd'; -import { ReloadOutlined, ClearOutlined } from '@ant-design/icons'; +import { ReloadOutlined, ClearOutlined } from '../icons'; import dayjs from 'dayjs'; import client from '../api/client'; import { useI18n } from '../i18n'; diff --git a/frontend/src/pages/Routing.tsx b/frontend/src/pages/Routing.tsx index dd67eaa..8955137 100644 --- a/frontend/src/pages/Routing.tsx +++ b/frontend/src/pages/Routing.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Card, Table, Button, Space, Modal, Form, Input, InputNumber, Select, message, Popconfirm } from 'antd'; -import { PlusOutlined, DeleteOutlined, SaveOutlined } from '@ant-design/icons'; +import { PlusOutlined, DeleteOutlined, SaveOutlined } from '../icons'; import { fetchGroups, saveGroups, fetchRules, saveRules, GroupEntry } from '../api/routing'; import { useI18n } from '../i18n'; import useIsMobile from '../hooks/useIsMobile'; diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 8d9a99e..3e34418 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -48,7 +48,7 @@ import { ProfileOutlined, CloudServerOutlined, FieldTimeOutlined, -} from '@ant-design/icons'; +} from '../icons'; import { downloadBackup, restoreDatabase, diff --git a/frontend/src/pages/Share.tsx b/frontend/src/pages/Share.tsx index d2bf7f7..b9770b1 100644 --- a/frontend/src/pages/Share.tsx +++ b/frontend/src/pages/Share.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Alert, Button, Card, Collapse, Empty, Input, Select, Space, Spin, Tabs, Tag, Typography, message, } from 'antd'; -import { CopyOutlined, QrcodeOutlined, ReloadOutlined, LinkOutlined } from '@ant-design/icons'; +import { CopyOutlined, QrcodeOutlined, ReloadOutlined, LinkOutlined } from '../icons'; import { useSearchParams } from 'react-router-dom'; import { useI18n } from '../i18n'; import useIsMobile from '../hooks/useIsMobile'; diff --git a/frontend/src/pages/Traffic.tsx b/frontend/src/pages/Traffic.tsx index 980e664..3d11b8f 100644 --- a/frontend/src/pages/Traffic.tsx +++ b/frontend/src/pages/Traffic.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Card, Table, Tag, Space, Statistic, Row, Col, message, Button } from 'antd'; -import { ReloadOutlined } from '@ant-design/icons'; +import { ReloadOutlined } from '../icons'; import { fetchTrafficStatus, fetchTrafficUsers, fetchConnections, UserTraffic, ConnectionView, TrafficStatus } from '../api/traffic'; import { useI18n } from '../i18n'; import PageHeader from '../components/PageHeader'; diff --git a/frontend/src/pages/Users.tsx b/frontend/src/pages/Users.tsx index b43b85c..7a0169a 100644 --- a/frontend/src/pages/Users.tsx +++ b/frontend/src/pages/Users.tsx @@ -3,7 +3,7 @@ import { Card, Table, Button, Space, Modal, Form, Input, Switch, message, Popconfirm, Select, Tag, InputNumber, DatePicker, Progress, Tooltip, Dropdown, Checkbox, Spin, } from 'antd'; -import { PlusOutlined, DeleteOutlined, EditOutlined, LinkOutlined, ClearOutlined, ShareAltOutlined, CopyOutlined, MoreOutlined, FundOutlined } from '@ant-design/icons'; +import { PlusOutlined, DeleteOutlined, EditOutlined, LinkOutlined, ClearOutlined, ShareAltOutlined, CopyOutlined, MoreOutlined, FundOutlined } from '../icons'; import dayjs from 'dayjs'; import { fetchUsers, createUser, updateUser, deleteUser, resetUserTraffic, deleteDepletedUsers, batchUsers, diff --git a/frontend/src/styles/responsive.css b/frontend/src/styles/responsive.css index 1c1eba3..ea0b8e3 100644 --- a/frontend/src/styles/responsive.css +++ b/frontend/src/styles/responsive.css @@ -1189,3 +1189,9 @@ html.dark .app-header-bar, padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)) !important; } } + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +