| 1 | /** |
| 2 | * 账户选择器组件 |
| 3 | * 显示所有可选账户,支持选中/取消选中 |
| 4 | */ |
| 5 | |
| 6 | import type { CSSProperties } from 'react' |
| 7 | import type { SocialAccount } from '@/api/accounts/account.types' |
| 8 | import type { PlatType } from '@/app/config/platConfig' |
| 9 | import type { PubItem } from '@/components/PublishDialog/publishDialog.type' |
| 10 | |
| 11 | import { Plus } from 'lucide-react' |
| 12 | import { memo, useCallback, useEffect, useMemo } from 'react' |
| 13 | import { useShallow } from 'zustand/react/shallow' |
| 14 | import { useTransClient } from '@/app/i18n/client' |
| 15 | import { useChannelManagerStore } from '@/components/ChannelManager/channelManagerStore' |
| 16 | import AvatarPlat from '@/components/common/AvatarPlat' |
| 17 | import { useAccountClickHandler } from '@/components/PublishDialog/hooks/useAccountClickHandler' |
| 18 | import { getPlatformAccountBorderColor } from '@/components/PublishDialog/PublishDialog.util' |
| 19 | import { usePublishDialog } from '@/components/PublishDialog/usePublishDialog' |
| 20 | import { Button } from '@/components/ui/button' |
| 21 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' |
| 22 | import { usePlatformInfoMap } from '@/hooks/usePlatformMetadata' |
| 23 | import { useAccountStore } from '@/store/account' |
| 24 | import { isPlatformEnabledSync } from '@/store/platformMetadata' |
| 25 | |
| 26 | interface AccountSelectorProps { |
| 27 | // 外部回调 |
| 28 | onOfflineClick: (account: SocialAccount) => void |
| 29 | // 可选的账户列表(用于频道筛选) |
| 30 | pubList?: PubItem[] |
| 31 | } |
| 32 | |
| 33 | type AccountBorderStyle = CSSProperties & { |
| 34 | '--publish-account-platform-border': string |
| 35 | } |
| 36 | |
| 37 | function getPlatformAccountBorderStyle(platType: PlatType): AccountBorderStyle { |
| 38 | return { |
| 39 | '--publish-account-platform-border': getPlatformAccountBorderColor(platType), |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * 账户选择器组件 |
| 45 | */ |
| 46 | export const AccountSelector = memo( |
| 47 | ({ onOfflineClick, pubList: externalPubList }: AccountSelectorProps) => { |
| 48 | const { t } = useTransClient('publish') |
| 49 | const platformInfoMap = usePlatformInfoMap() |
| 50 | |
| 51 | // ============ 从 Store 获取状态 ============ |
| 52 | |
| 53 | const { |
| 54 | pubList: storePubList, |
| 55 | pubListChoosed, |
| 56 | step, |
| 57 | setStep, |
| 58 | setExpandedPubItem, |
| 59 | setPubListChoosed, |
| 60 | } = usePublishDialog( |
| 61 | useShallow(state => ({ |
| 62 | pubList: state.pubList, |
| 63 | pubListChoosed: state.pubListChoosed, |
| 64 | step: state.step, |
| 65 | setStep: state.setStep, |
| 66 | setExpandedPubItem: state.setExpandedPubItem, |
| 67 | setPubListChoosed: state.setPubListChoosed, |
| 68 | })), |
| 69 | ) |
| 70 | |
| 71 | // 使用外部传入的 pubList 或 store 中的 pubList |
| 72 | // 注意:如果 externalPubList 是空数组,也应该使用它(表示筛选后没有账户) |
| 73 | // 只有当 externalPubList 是 undefined 时才使用 storePubList |
| 74 | const pubList = externalPubList !== undefined ? externalPubList : storePubList |
| 75 | const visiblePubList = useMemo( |
| 76 | () => pubList.filter(pubItem => platformInfoMap.has(pubItem.account.type) && isPlatformEnabledSync(pubItem.account.type)), |
| 77 | [platformInfoMap, pubList], |
| 78 | ) |
| 79 | |
| 80 | // ============ Custom Hooks ============ |
| 81 | |
| 82 | const { handleAccountClick } = useAccountClickHandler({ |
| 83 | pubListChoosed, |
| 84 | step, |
| 85 | setStep, |
| 86 | setExpandedPubItem, |
| 87 | setPubListChoosed, |
| 88 | }) |
| 89 | |
| 90 | const { openConnectList, setOnAuthSuccess } = useChannelManagerStore( |
| 91 | useShallow(state => ({ |
| 92 | openConnectList: state.openConnectList, |
| 93 | setOnAuthSuccess: state.setOnAuthSuccess, |
| 94 | })), |
| 95 | ) |
| 96 | |
| 97 | const handleChannelAuthSuccess = useCallback((account: SocialAccount) => { |
| 98 | const { |
| 99 | pubList: currentPubList, |
| 100 | syncAccounts, |
| 101 | } = usePublishDialog.getState() |
| 102 | |
| 103 | syncAccounts([...currentPubList.map(pubItem => pubItem.account), account]) |
| 104 | useChannelManagerStore.getState().setOnAuthSuccess(null) |
| 105 | }, []) |
| 106 | |
| 107 | useEffect(() => { |
| 108 | return () => { |
| 109 | const channelManagerState = useChannelManagerStore.getState() |
| 110 | if (channelManagerState.onAuthSuccess === handleChannelAuthSuccess) { |
| 111 | channelManagerState.setOnAuthSuccess(null) |
| 112 | } |
| 113 | } |
| 114 | }, [handleChannelAuthSuccess]) |
| 115 | |
| 116 | const handleAddChannelClick = useCallback(() => { |
| 117 | setOnAuthSuccess(handleChannelAuthSuccess) |
| 118 | openConnectList(useAccountStore.getState().activeSpaceId) |
| 119 | }, [handleChannelAuthSuccess, openConnectList, setOnAuthSuccess]) |
| 120 | |
| 121 | // ============ Render ============ |
| 122 | |
| 123 | return ( |
| 124 | <div className="flex flex-wrap items-center gap-2.5" data-testid="publish-account-selector"> |
| 125 | {visiblePubList.map((pubItem) => { |
| 126 | const platConfig = platformInfoMap.get(pubItem.account.type) |
| 127 | if (!platConfig) |
| 128 | return null |
| 129 | const isChoosed = pubListChoosed.find(v => v.account.id === pubItem.account.id) |
| 130 | const isOffline = pubItem.account.status === 0 |
| 131 | const platformBorderStyle = getPlatformAccountBorderStyle(pubItem.account.type) |
| 132 | |
| 133 | return ( |
| 134 | <TooltipProvider key={pubItem.account.id}> |
| 135 | <Tooltip> |
| 136 | <TooltipTrigger asChild> |
| 137 | <div |
| 138 | className={`border-2 rounded-full ${ |
| 139 | isChoosed |
| 140 | ? 'border-(--publish-account-platform-border) active:border-(--publish-account-platform-border)' |
| 141 | : 'border-transparent' |
| 142 | }`} |
| 143 | style={platformBorderStyle} |
| 144 | data-testid={`publish-account-item-${pubItem.account.id}`} |
| 145 | onClick={(e) => { |
| 146 | e.stopPropagation() |
| 147 | // 离线账户的点击由头像容器处理,这里不处理 |
| 148 | if (isOffline) { |
| 149 | return |
| 150 | } |
| 151 | handleAccountClick(pubItem) |
| 152 | }} |
| 153 | > |
| 154 | {/* 账号头像:离线显示遮罩并禁用 */} |
| 155 | <div className="relative flex"> |
| 156 | <AvatarPlat |
| 157 | className={`cursor-pointer p-[1px] ${ |
| 158 | isChoosed && !isOffline |
| 159 | ? '[&>img]:grayscale-0' |
| 160 | : '[&>img]:grayscale hover:[&>img]:grayscale-0' |
| 161 | }`} |
| 162 | account={pubItem.account} |
| 163 | size="large" |
| 164 | disabled={isOffline || !isChoosed} |
| 165 | /> |
| 166 | {isOffline && ( |
| 167 | <div |
| 168 | onClick={(e) => { |
| 169 | e.stopPropagation() |
| 170 | // 离线账户统一复用频道管理授权流程 |
| 171 | onOfflineClick(pubItem.account) |
| 172 | }} |
| 173 | className="absolute inset-0 bg-black/45 rounded-full flex items-center justify-center text-white text-xs font-semibold pointer-events-auto cursor-pointer" |
| 174 | > |
| 175 | {t('badges.offline')} |
| 176 | </div> |
| 177 | )} |
| 178 | </div> |
| 179 | </div> |
| 180 | </TooltipTrigger> |
| 181 | {isOffline && ( |
| 182 | <TooltipContent> |
| 183 | {t('tips.accountOffline')} |
| 184 | </TooltipContent> |
| 185 | )} |
| 186 | </Tooltip> |
| 187 | </TooltipProvider> |
| 188 | ) |
| 189 | })} |
| 190 | |
| 191 | {visiblePubList.length > 0 && ( |
| 192 | <TooltipProvider> |
| 193 | <Tooltip> |
| 194 | <TooltipTrigger asChild> |
| 195 | <Button |
| 196 | type="button" |
| 197 | variant="outline" |
| 198 | size="icon" |
| 199 | aria-label={t('tips.addChannel')} |
| 200 | className="h-11 w-11 shrink-0 cursor-pointer rounded-full border-2 border-dashed border-border bg-card p-0 text-muted-foreground shadow-sm transition-all duration-200 hover:border-primary/60 hover:bg-primary/5 hover:text-primary" |
| 201 | data-testid="publish-add-channel-button" |
| 202 | onClick={(e) => { |
| 203 | e.stopPropagation() |
| 204 | handleAddChannelClick() |
| 205 | }} |
| 206 | > |
| 207 | <Plus className="h-4 w-4" /> |
| 208 | </Button> |
| 209 | </TooltipTrigger> |
| 210 | <TooltipContent>{t('tips.addChannel')}</TooltipContent> |
| 211 | </Tooltip> |
| 212 | </TooltipProvider> |
| 213 | )} |
| 214 | </div> |
| 215 | ) |
| 216 | }, |
| 217 | ) |
| 218 | |
| 219 | AccountSelector.displayName = 'AccountSelector' |
| 220 | |
| 221 | export default AccountSelector |
| 222 |