返回 AiToEarn
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
222 lines Plain Text