| 1 | /** |
| 2 | * ConnectChannelList - 连接频道列表页 |
| 3 | * 显示所有可连接的平台,点击进入授权流程 |
| 4 | */ |
| 5 | |
| 6 | 'use client' |
| 7 | |
| 8 | import { ArrowLeft, Sparkles } from 'lucide-react' |
| 9 | import { toast } from 'sonner' |
| 10 | import { useShallow } from 'zustand/react/shallow' |
| 11 | import { PlatType } from '@/app/config/platConfig' |
| 12 | import { useTransClient } from '@/app/i18n/client' |
| 13 | import { PlatformIcon } from '@/components/common/PlatformIcon' |
| 14 | import { Button } from '@/components/ui/button' |
| 15 | import { ScrollArea } from '@/components/ui/scroll-area' |
| 16 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' |
| 17 | import { isChina } from '@/constant' |
| 18 | import { useIsMobile } from '@/hooks/useIsMobile' |
| 19 | import { useRegionSortedPlatforms } from '@/hooks/usePlatformMetadata' |
| 20 | import { useAccountStore } from '@/store/account' |
| 21 | import { isPlatformComingSoon, isPlatformRegionLimited } from '@/store/platformMetadata/utils' |
| 22 | import { useUserStore } from '@/store/user' |
| 23 | import { navigateToLogin } from '@/utils/auth' |
| 24 | import { confirmPlatformRegionRedirect } from '@/utils/region/redirect' |
| 25 | import { confirm } from '@/utils/ui/confirm' |
| 26 | import { useChannelManagerStore } from '../channelManagerStore' |
| 27 | |
| 28 | export function ConnectChannelList() { |
| 29 | const { t } = useTransClient('account') |
| 30 | const isMobile = useIsMobile() |
| 31 | const platforms = useRegionSortedPlatforms() |
| 32 | |
| 33 | const { isNewUser, setCurrentView, startAuth, targetSpaceId, setTargetSpaceId, closeModal } |
| 34 | = useChannelManagerStore( |
| 35 | useShallow(state => ({ |
| 36 | isNewUser: state.isNewUser, |
| 37 | setCurrentView: state.setCurrentView, |
| 38 | startAuth: state.startAuth, |
| 39 | targetSpaceId: state.targetSpaceId, |
| 40 | setTargetSpaceId: state.setTargetSpaceId, |
| 41 | closeModal: state.closeModal, |
| 42 | })), |
| 43 | ) |
| 44 | |
| 45 | const { accountGroupList } = useAccountStore( |
| 46 | useShallow(state => ({ |
| 47 | accountGroupList: state.accountGroupList, |
| 48 | })), |
| 49 | ) |
| 50 | |
| 51 | const { token } = useUserStore( |
| 52 | useShallow(state => ({ |
| 53 | token: state.token, |
| 54 | })), |
| 55 | ) |
| 56 | |
| 57 | // 返回主页 |
| 58 | const handleBack = () => { |
| 59 | setCurrentView('main') |
| 60 | } |
| 61 | |
| 62 | // 处理平台点击 |
| 63 | const handlePlatformClick = async (platform: PlatType) => { |
| 64 | const platInfo = platforms.find(([key]) => key === platform)?.[1] |
| 65 | const platformName = platInfo?.name || platform |
| 66 | |
| 67 | if (isPlatformComingSoon(platInfo)) { |
| 68 | toast.warning(t('channelManager.platformComingSoon', { platform: platformName })) |
| 69 | return |
| 70 | } |
| 71 | |
| 72 | if (isPlatformRegionLimited(platInfo)) { |
| 73 | await confirmPlatformRegionRedirect(platformName) |
| 74 | return |
| 75 | } |
| 76 | |
| 77 | // 移动端点击小红书时显示提示 |
| 78 | if (isMobile && platform === PlatType.Xhs) { |
| 79 | toast.warning(t('channelManager.xhsMobileNotSupported')) |
| 80 | return |
| 81 | } |
| 82 | |
| 83 | // 未登录时关闭频道弹框并跳转登录页 |
| 84 | if (!token) { |
| 85 | closeModal() |
| 86 | navigateToLogin() |
| 87 | return |
| 88 | } |
| 89 | |
| 90 | if (platform === PlatType.WxGzh) { |
| 91 | const confirmed = await confirm({ |
| 92 | title: t('channelManager.wxGzhAuthNoticeTitle'), |
| 93 | content: t('channelManager.wxGzhAuthNoticeContent'), |
| 94 | okText: t('channelManager.continueAuth'), |
| 95 | }) |
| 96 | |
| 97 | if (!confirmed) { |
| 98 | return |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | // 如果没有设置目标空间,使用默认空间 |
| 103 | let spaceId = targetSpaceId |
| 104 | if (!spaceId) { |
| 105 | const defaultSpace = accountGroupList.find(g => g.isDefault) |
| 106 | spaceId = defaultSpace?.id || null |
| 107 | if (spaceId) { |
| 108 | setTargetSpaceId(spaceId) |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | // 开始授权流程 |
| 113 | startAuth(platform, spaceId || undefined) |
| 114 | } |
| 115 | |
| 116 | return ( |
| 117 | <div className="flex h-full flex-col"> |
| 118 | {/* 头部 - 返回按钮 */} |
| 119 | <div className="flex items-center gap-3 border-b px-4 py-3"> |
| 120 | <Button data-testid="cm-connect-back-btn" variant="ghost" size="sm" className="cursor-pointer" onClick={handleBack}> |
| 121 | <ArrowLeft className="mr-1 h-4 w-4" /> |
| 122 | {t('channelManager.backToChannels')} |
| 123 | </Button> |
| 124 | </div> |
| 125 | |
| 126 | {/* 新用户提示 */} |
| 127 | {isNewUser && ( |
| 128 | <div data-testid="cm-connect-new-user-tip" className="mx-4 mt-4 flex items-center gap-3 rounded-lg border bg-muted/30 p-4"> |
| 129 | <Sparkles className="h-6 w-6 shrink-0 text-muted-foreground" /> |
| 130 | <p className="text-sm text-muted-foreground">{t('channelManager.newUserTip')}</p> |
| 131 | </div> |
| 132 | )} |
| 133 | |
| 134 | {/* 平台网格 */} |
| 135 | <ScrollArea className="flex-1 p-4"> |
| 136 | <div |
| 137 | data-testid="cm-connect-list" |
| 138 | className=" |
| 139 | grid w-full gap-2 sm:gap-3 |
| 140 | pt-3 |
| 141 | grid-cols-3 |
| 142 | sm:grid-cols-4 |
| 143 | md:grid-cols-5 |
| 144 | lg:grid-cols-6 |
| 145 | " |
| 146 | > |
| 147 | <TooltipProvider> |
| 148 | {platforms.map(([key, value]) => { |
| 149 | const isDisabled = isPlatformComingSoon(value) |
| 150 | const isRegionLimited = isPlatformRegionLimited(value) |
| 151 | const isBrowserPlugin = value.authType === 'plugin' |
| 152 | const isQrAuth = value.authType === 'qrcode' |
| 153 | const regionLimitedLabel = isRegionLimited |
| 154 | ? t(isChina ? 'channelManager.internationalSiteOnly' : 'channelManager.chinaSiteOnly') |
| 155 | : undefined |
| 156 | const disabledTip = isDisabled |
| 157 | ? t('channelManager.platformComingSoon', { platform: value.name }) |
| 158 | : regionLimitedLabel |
| 159 | |
| 160 | return ( |
| 161 | <Tooltip key={key}> |
| 162 | <TooltipTrigger asChild> |
| 163 | <Button |
| 164 | data-testid="cm-connect-platform-card" |
| 165 | variant="ghost" |
| 166 | className={` |
| 167 | group relative flex h-[90px] min-w-0 |
| 168 | w-full cursor-pointer flex-col items-center |
| 169 | justify-center overflow-hidden whitespace-normal |
| 170 | rounded-lg border border-border bg-card |
| 171 | p-2 transition-all duration-200 |
| 172 | hover:-translate-y-0.5 hover:border-foreground/30 hover:bg-accent |
| 173 | hover:shadow-md |
| 174 | active:translate-y-0 active:scale-[0.98] |
| 175 | sm:h-[100px] sm:rounded-xl sm:p-3 |
| 176 | md:h-[110px] |
| 177 | ${isDisabled ? 'opacity-50 grayscale' : ''} |
| 178 | ${isRegionLimited ? 'opacity-45 grayscale' : ''} |
| 179 | `} |
| 180 | onClick={() => handlePlatformClick(key as PlatType)} |
| 181 | > |
| 182 | {regionLimitedLabel && ( |
| 183 | <span className="absolute -left-px -top-px z-20 rounded-br rounded-tl-lg bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground"> |
| 184 | {regionLimitedLabel} |
| 185 | </span> |
| 186 | )} |
| 187 | {/* 浏览器插件标签 */} |
| 188 | {isBrowserPlugin && ( |
| 189 | <span className="absolute -right-px -top-px z-20 rounded-bl rounded-tr-lg bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground"> |
| 190 | {t('channelManager.browserPlugin')} |
| 191 | </span> |
| 192 | )} |
| 193 | {isQrAuth && ( |
| 194 | <span className="absolute -right-px -top-px z-20 rounded-bl rounded-tr-lg bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground"> |
| 195 | {t('channelManager.qrCodeAuth')} |
| 196 | </span> |
| 197 | )} |
| 198 | |
| 199 | {/* 光泽效果 */} |
| 200 | <div className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/40 to-transparent transition-transform duration-700 group-hover:translate-x-full" /> |
| 201 | |
| 202 | <div className="relative z-10 flex w-full flex-col items-center gap-1.5 sm:gap-2"> |
| 203 | <PlatformIcon |
| 204 | platform={key} |
| 205 | className=" |
| 206 | h-9 w-9 object-contain |
| 207 | drop-shadow-md filter transition-all |
| 208 | duration-300 group-hover:rotate-[5deg] |
| 209 | group-hover:scale-110 |
| 210 | sm:h-10 sm:w-10 |
| 211 | md:h-11 md:w-11 |
| 212 | " |
| 213 | width={44} |
| 214 | height={44} |
| 215 | /> |
| 216 | <span |
| 217 | className=" |
| 218 | line-clamp-2 w-full text-center |
| 219 | text-[11px] font-medium leading-tight |
| 220 | text-foreground transition-all duration-300 |
| 221 | group-hover:font-semibold |
| 222 | sm:text-xs |
| 223 | " |
| 224 | > |
| 225 | {value.name} |
| 226 | </span> |
| 227 | </div> |
| 228 | </Button> |
| 229 | </TooltipTrigger> |
| 230 | {disabledTip && ( |
| 231 | <TooltipContent className="max-w-[200px] text-xs"> |
| 232 | <p>{disabledTip}</p> |
| 233 | </TooltipContent> |
| 234 | )} |
| 235 | </Tooltip> |
| 236 | ) |
| 237 | })} |
| 238 | </TooltipProvider> |
| 239 | </div> |
| 240 | </ScrollArea> |
| 241 | </div> |
| 242 | ) |
| 243 | } |
| 244 |