@@ -27,7 +27,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
2727import { ScrollArea } from '@/components/ui/scroll-area' ;
2828import { useTranslation } from 'react-i18next' ;
2929import { FreeQuotaUsage } from './free-quota-usage' ;
30- import { getGeneralSettings } from '../../../../services/store/src' ;
30+ import { getGeneralSettings , useMihomoRuntimeStore } from '../../../../services/store/src' ;
3131import { ClashIcon } from '../../../../pages/proxy-center/src/mihomo/clash-icon' ;
3232import { MihomoConnectDialog } from '../../../../pages/proxy-center/src/mihomo/mihomo-connect-dialog' ;
3333import { getMihomoStatus } from '../../../../pages/proxy-center/src/mihomo/api' ;
@@ -99,6 +99,8 @@ interface NavItemProps {
9999 actionIcon ?: LucideIcon | IconType ;
100100 actionNode ?: ReactNode ;
101101 actionLabel ?: string ;
102+ actionButtonClassName ?: string ;
103+ actionButtonAnimated ?: boolean ;
102104 onActionClick ?: ( ) => void ;
103105}
104106
@@ -120,6 +122,8 @@ const NavItem: React.FC<NavItemProps> = ({
120122 actionIcon : ActionIcon ,
121123 actionNode,
122124 actionLabel,
125+ actionButtonClassName,
126+ actionButtonAnimated,
123127 onActionClick,
124128} ) => {
125129 const [ collapsedActionVisible , setCollapsedActionVisible ] = useState ( false ) ;
@@ -208,18 +212,20 @@ const NavItem: React.FC<NavItemProps> = ({
208212 { ! collapsed && hasAction && (
209213 < Tooltip >
210214 < TooltipTrigger asChild >
211- < button
212- type = "button"
213- onClick = { ( event ) => {
214- event . preventDefault ( ) ;
215- event . stopPropagation ( ) ;
216- onActionClick ( ) ;
217- } }
218- className = "absolute right-1 top-1/2 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-md border border-border/60 bg-background/60 text-sidebar-foreground/80 transition-colors hover:bg-accent hover:text-foreground"
219- aria-label = { actionLabel || label }
220- >
221- { actionNode || ( ActionIcon ? < ActionIcon className = "h-3.5 w-3.5" /> : null ) }
222- </ button >
215+ < div className = "absolute right-1 top-1/2 -translate-y-1/2" >
216+ < button
217+ type = "button"
218+ onClick = { ( event ) => {
219+ event . preventDefault ( ) ;
220+ event . stopPropagation ( ) ;
221+ onActionClick ( ) ;
222+ } }
223+ className = { `flex h-8 w-8 items-center justify-center rounded-md border bg-background/60 transition-colors ${ actionButtonAnimated ? 'animate-[sidebar-proxy-action-drift_10s_ease-in-out_infinite]' : '' } ${ actionButtonClassName || 'border-border/60 text-sidebar-foreground/80 hover:bg-accent hover:text-foreground' } ` }
224+ aria-label = { actionLabel || label }
225+ >
226+ { actionNode || ( ActionIcon ? < ActionIcon className = "h-3.5 w-3.5" /> : null ) }
227+ </ button >
228+ </ div >
223229 </ TooltipTrigger >
224230 < TooltipContent side = "right" > { actionLabel || label } </ TooltipContent >
225231 </ Tooltip >
@@ -238,9 +244,9 @@ const NavItem: React.FC<NavItemProps> = ({
238244 className = { `absolute inset-0 z-10 flex h-11 w-11 items-center justify-center rounded-lg border transform-gpu transition-all duration-200 ${ collapsedActionVisible
239245 ? 'pointer-events-auto translate-x-0 opacity-100'
240246 : 'pointer-events-none translate-x-2 opacity-0'
241- } ${ isActive
247+ } ${ actionButtonClassName || ( isActive
242248 ? 'border-primary/40 bg-primary/10 text-primary'
243- : 'border-border/60 bg-background/90 text-sidebar-foreground/80 hover:bg-accent hover:text-foreground'
249+ : 'border-border/60 bg-background/90 text-sidebar-foreground/80 hover:bg-accent hover:text-foreground' )
244250 } `}
245251 aria-label = { actionLabel || label }
246252 >
@@ -262,6 +268,9 @@ interface NavGroupComponentProps {
262268 collapsed : boolean ;
263269 currentPath : string ;
264270 proxyActionLabel : string ;
271+ proxyActionNode : ReactNode ;
272+ proxyActionButtonClassName ?: string ;
273+ proxyActionButtonAnimated ?: boolean ;
265274 onProxyActionClick : ( ) => void ;
266275}
267276
@@ -273,6 +282,9 @@ const NavGroupComponent: React.FC<NavGroupComponentProps> = ({
273282 collapsed,
274283 currentPath,
275284 proxyActionLabel,
285+ proxyActionNode,
286+ proxyActionButtonClassName,
287+ proxyActionButtonAnimated,
276288 onProxyActionClick,
277289} ) => {
278290 return (
@@ -290,8 +302,10 @@ const NavGroupComponent: React.FC<NavGroupComponentProps> = ({
290302 icon = { item . icon }
291303 isActive = { isRouteActive ( currentPath , item . href ) }
292304 collapsed = { collapsed }
293- actionNode = { item . href === '/proxy' ? < ClashIcon className = "h-4 w-4" /> : undefined }
305+ actionNode = { item . href === '/proxy' ? proxyActionNode : undefined }
294306 actionLabel = { item . href === '/proxy' ? proxyActionLabel : undefined }
307+ actionButtonClassName = { item . href === '/proxy' ? proxyActionButtonClassName : undefined }
308+ actionButtonAnimated = { item . href === '/proxy' ? proxyActionButtonAnimated : undefined }
295309 onActionClick = { item . href === '/proxy' ? onProxyActionClick : undefined }
296310 />
297311 ) ) }
@@ -304,6 +318,9 @@ interface NavListProps {
304318 collapsed : boolean ;
305319 currentPath : string ;
306320 proxyActionLabel : string ;
321+ proxyActionNode : ReactNode ;
322+ proxyActionButtonClassName ?: string ;
323+ proxyActionButtonAnimated ?: boolean ;
307324 onProxyActionClick : ( ) => void ;
308325}
309326
@@ -315,6 +332,9 @@ const NavList: React.FC<NavListProps> = ({
315332 collapsed,
316333 currentPath,
317334 proxyActionLabel,
335+ proxyActionNode,
336+ proxyActionButtonClassName,
337+ proxyActionButtonAnimated,
318338 onProxyActionClick,
319339} ) => {
320340 return (
@@ -329,6 +349,9 @@ const NavList: React.FC<NavListProps> = ({
329349 collapsed = { collapsed }
330350 currentPath = { currentPath }
331351 proxyActionLabel = { proxyActionLabel }
352+ proxyActionNode = { proxyActionNode }
353+ proxyActionButtonClassName = { proxyActionButtonClassName }
354+ proxyActionButtonAnimated = { proxyActionButtonAnimated }
332355 onProxyActionClick = { onProxyActionClick }
333356 />
334357 </ div >
@@ -550,6 +573,21 @@ export const AppSidebar: React.FC = () => {
550573 const [ settingsLoaded , setSettingsLoaded ] = useState ( false ) ;
551574 const [ mihomoDialogOpen , setMihomoDialogOpen ] = useState ( false ) ;
552575 const [ mihomoAttached , setMihomoAttached ] = useState ( false ) ;
576+ const mihomoRunning = useMihomoRuntimeStore ( ( state ) => state . running ) ;
577+ const mihomoCheckedAt = useMihomoRuntimeStore ( ( state ) => state . checkedAt ) ;
578+
579+ const refreshMihomoAttached = useRef ( async ( cancelledRef ?: { current : boolean } ) => {
580+ try {
581+ const status = await getMihomoStatus ( ) ;
582+ if ( ! cancelledRef ?. current ) {
583+ setMihomoAttached ( status . attached ) ;
584+ }
585+ } catch {
586+ if ( ! cancelledRef ?. current ) {
587+ setMihomoAttached ( false ) ;
588+ }
589+ }
590+ } ) ;
553591
554592 useEffect ( ( ) => {
555593 let cancelled = false ;
@@ -573,26 +611,28 @@ export const AppSidebar: React.FC = () => {
573611 } , [ ] ) ;
574612
575613 useEffect ( ( ) => {
576- let cancelled = false ;
614+ const cancelledRef = { current : false } ;
577615
578- void getMihomoStatus ( )
579- . then ( ( status ) => {
580- if ( ! cancelled ) {
581- setMihomoAttached ( status . attached ) ;
582- }
583- } )
584- . catch ( ( ) => {
585- if ( ! cancelled ) {
586- setMihomoAttached ( false ) ;
587- }
588- } ) ;
616+ if ( ! mihomoRunning ) {
617+ setMihomoAttached ( false ) ;
618+ return ( ) => {
619+ cancelledRef . current = true ;
620+ } ;
621+ }
622+
623+ void refreshMihomoAttached . current ( cancelledRef ) ;
589624
590625 return ( ) => {
591- cancelled = true ;
626+ cancelledRef . current = true ;
592627 } ;
593- } , [ ] ) ;
628+ } , [ location . pathname , mihomoRunning ] ) ;
594629
595630 const handleOpenMihomo = ( ) => {
631+ if ( ! mihomoRunning ) {
632+ navigate ( '/proxy?mode=local' ) ;
633+ return ;
634+ }
635+
596636 if ( mihomoAttached ) {
597637 navigate ( '/proxy/mihomo' ) ;
598638 return ;
@@ -614,6 +654,33 @@ export const AppSidebar: React.FC = () => {
614654 return < SidebarPlaceholder collapsed = { collapsed } /> ;
615655 }
616656
657+ const mihomoStatusTone = mihomoRunning
658+ ? mihomoAttached
659+ ? 'connected'
660+ : 'warning'
661+ : mihomoCheckedAt == null
662+ ? 'unknown'
663+ : 'offline' ;
664+
665+ const mihomoActionNode = (
666+ < span className = "relative flex items-center justify-center" >
667+ < ClashIcon className = "h-4 w-4 text-current" />
668+ { mihomoStatusTone === 'connected' ? (
669+ < span className = "absolute -right-1 -top-1 h-2.5 w-2.5 rounded-full border border-background bg-emerald-500" />
670+ ) : mihomoStatusTone === 'warning' ? (
671+ < span className = "absolute -right-1 -top-1 flex h-3.5 w-3.5 items-center justify-center rounded-full border border-background bg-amber-500 text-[9px] font-bold leading-none text-white" >
672+ !
673+ </ span >
674+ ) : mihomoStatusTone === 'unknown' ? (
675+ < span className = "absolute -right-1 -top-1 h-2.5 w-2.5 rounded-full border border-background bg-muted-foreground/60" />
676+ ) : (
677+ < span className = "absolute -right-1 -top-1 flex h-3.5 w-3.5 items-center justify-center rounded-full border border-background bg-rose-500 text-[9px] font-bold leading-none text-white" >
678+ !
679+ </ span >
680+ ) }
681+ </ span >
682+ ) ;
683+
617684 return (
618685 < aside
619686 className = { `flex flex-col h-full shrink-0 transition-[width] duration-300 ease-out relative before:absolute before:inset-0 before:bg-background/10 before:backdrop-blur-2xl before:-z-10 px-1 ${ collapsed ? 'w-16' : 'w-58'
@@ -628,13 +695,22 @@ export const AppSidebar: React.FC = () => {
628695 </ div >
629696
630697 { /* 导航列表区域 */ }
631- < NavList
632- groups = { localizeNavGroups ( t ) }
633- collapsed = { collapsed }
634- currentPath = { location . pathname }
635- proxyActionLabel = { t ( 'nav.item.mihomo' , { defaultValue : 'Mihomo' } ) }
636- onProxyActionClick = { handleOpenMihomo }
637- />
698+ < NavList
699+ groups = { localizeNavGroups ( t ) }
700+ collapsed = { collapsed }
701+ currentPath = { location . pathname }
702+ proxyActionLabel = { t ( 'nav.item.mihomo' , { defaultValue : 'Mihomo' } ) }
703+ proxyActionNode = { mihomoActionNode }
704+ proxyActionButtonClassName = {
705+ mihomoStatusTone === 'connected'
706+ ? 'border-emerald-500/30 bg-background/90 text-emerald-600 hover:bg-muted hover:text-foreground'
707+ : mihomoStatusTone === 'warning'
708+ ? 'border-amber-500/30 bg-background/90 text-amber-600 hover:bg-muted hover:text-foreground'
709+ : 'border-rose-500/30 bg-background/90 text-rose-600 hover:bg-muted hover:text-rose-600'
710+ }
711+ proxyActionButtonAnimated = { ! collapsed }
712+ onProxyActionClick = { handleOpenMihomo }
713+ />
638714
639715 { /* 底部导航项(指纹审计、系统设置) */ }
640716 < div className = "shrink-0" >
0 commit comments