| 1 | /** |
| 2 | * PluginEntry - 插件入口组件 |
| 3 | * 根据插件状态显示不同颜色 |
| 4 | */ |
| 5 | |
| 6 | 'use client' |
| 7 | |
| 8 | import type { SidebarCommonProps } from '../../types' |
| 9 | import { Puzzle } from 'lucide-react' |
| 10 | import { useShallow } from 'zustand/react/shallow' |
| 11 | import { useTransClient } from '@/app/i18n/client' |
| 12 | import { PluginModal, PluginUpdatePopover } from '@/components/Plugin' |
| 13 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' |
| 14 | import { usePluginStore } from '@/store/plugin' |
| 15 | import { PluginStatus } from '@/store/plugin/types/baseTypes' |
| 16 | import { cn } from '@/utils/className' |
| 17 | |
| 18 | export function PluginEntry({ collapsed }: SidebarCommonProps) { |
| 19 | const { t } = useTransClient('common') |
| 20 | |
| 21 | const { hostAccessGranted, pluginNeedsUpdate, pluginStatus, pluginVersion, pluginModalVisible, openPluginModal, closePluginModal } = usePluginStore( |
| 22 | useShallow(state => ({ |
| 23 | hostAccessGranted: state.hostAccessGranted, |
| 24 | pluginNeedsUpdate: state.pluginNeedsUpdate, |
| 25 | pluginStatus: state.status, |
| 26 | pluginVersion: state.pluginVersion, |
| 27 | pluginModalVisible: state.pluginModalVisible, |
| 28 | openPluginModal: state.openPluginModal, |
| 29 | closePluginModal: state.closePluginModal, |
| 30 | })), |
| 31 | ) |
| 32 | |
| 33 | // 根据插件状态返回对应的颜色和状态文本 |
| 34 | const getStatusInfo = () => { |
| 35 | if (pluginStatus === PluginStatus.READY && pluginNeedsUpdate) { |
| 36 | return { |
| 37 | iconColor: 'text-warning', |
| 38 | dotColor: 'bg-warning', |
| 39 | statusText: t('pluginStatus.updateAvailable'), |
| 40 | statusColor: 'text-warning', |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | switch (pluginStatus) { |
| 45 | case PluginStatus.READY: |
| 46 | return { |
| 47 | iconColor: 'text-success', |
| 48 | dotColor: 'bg-success', |
| 49 | statusText: t('pluginStatus.ready'), |
| 50 | statusColor: 'text-success', |
| 51 | } |
| 52 | case PluginStatus.INSTALLED_NO_PERMISSION: |
| 53 | return { |
| 54 | iconColor: 'text-warning', |
| 55 | dotColor: 'bg-warning', |
| 56 | statusText: hostAccessGranted === false |
| 57 | ? t('pluginStatus.siteAccessRequired') |
| 58 | : t('pluginStatus.noPermission'), |
| 59 | statusColor: 'text-warning', |
| 60 | } |
| 61 | case PluginStatus.CHECKING: |
| 62 | return { |
| 63 | iconColor: 'text-info', |
| 64 | dotColor: 'bg-info animate-pulse', |
| 65 | statusText: t('pluginStatus.checking'), |
| 66 | statusColor: 'text-info', |
| 67 | } |
| 68 | case PluginStatus.NOT_INSTALLED: |
| 69 | case PluginStatus.UNKNOWN: |
| 70 | default: |
| 71 | return { |
| 72 | iconColor: 'text-muted-foreground/70', |
| 73 | dotColor: 'bg-muted-foreground/70', |
| 74 | statusText: t('pluginStatus.notInstalled'), |
| 75 | statusColor: 'text-muted-foreground/70', |
| 76 | } |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | const { iconColor, dotColor, statusText, statusColor } = getStatusInfo() |
| 81 | const statusNode = (() => { |
| 82 | if (collapsed) |
| 83 | return null |
| 84 | |
| 85 | if (pluginNeedsUpdate && pluginVersion) { |
| 86 | return ( |
| 87 | <PluginUpdatePopover currentVersion={pluginVersion} side="right" align="end"> |
| 88 | <span |
| 89 | className={cn('text-xs', statusColor)} |
| 90 | data-testid="sidebar-plugin-status" |
| 91 | > |
| 92 | {statusText} |
| 93 | </span> |
| 94 | </PluginUpdatePopover> |
| 95 | ) |
| 96 | } |
| 97 | |
| 98 | return ( |
| 99 | <span className={cn('text-xs', statusColor)} data-testid="sidebar-plugin-status"> |
| 100 | {statusText} |
| 101 | </span> |
| 102 | ) |
| 103 | })() |
| 104 | |
| 105 | const content = ( |
| 106 | <button |
| 107 | onClick={openPluginModal} |
| 108 | data-testid="sidebar-plugin" |
| 109 | className={cn( |
| 110 | 'flex w-full cursor-pointer items-center rounded-lg border-none bg-transparent text-muted-foreground transition-colors hover:bg-brand-cyan/10 hover:text-brand-cyan', |
| 111 | collapsed ? 'h-9 w-9 justify-center' : 'justify-between px-3 py-2', |
| 112 | )} |
| 113 | > |
| 114 | <div className="flex items-center gap-2"> |
| 115 | <div className="relative"> |
| 116 | <Puzzle size={18} className={iconColor} /> |
| 117 | {/* 状态指示点 */} |
| 118 | <span |
| 119 | className={cn( |
| 120 | 'absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full border border-background', |
| 121 | dotColor, |
| 122 | )} |
| 123 | /> |
| 124 | </div> |
| 125 | {!collapsed && <span className="text-sm">{t('plugin')}</span>} |
| 126 | </div> |
| 127 | {statusNode} |
| 128 | </button> |
| 129 | ) |
| 130 | |
| 131 | return ( |
| 132 | <> |
| 133 | {collapsed ? ( |
| 134 | <TooltipProvider> |
| 135 | <Tooltip> |
| 136 | <TooltipTrigger asChild>{content}</TooltipTrigger> |
| 137 | <TooltipContent side="right"> |
| 138 | <p> |
| 139 | {t('plugin')} |
| 140 | {' '} |
| 141 | - |
| 142 | {statusText} |
| 143 | </p> |
| 144 | </TooltipContent> |
| 145 | </Tooltip> |
| 146 | </TooltipProvider> |
| 147 | ) : ( |
| 148 | content |
| 149 | )} |
| 150 | |
| 151 | {/* 插件状态弹框 */} |
| 152 | <PluginModal visible={pluginModalVisible} onClose={closePluginModal} /> |
| 153 | </> |
| 154 | ) |
| 155 | } |
| 156 |