返回 AiToEarn
index.tsx
1 /**
2 * AccountsTopNav 组件
3 *
4 * 功能描述: 账号模块顶部导航栏
5 * - 左侧: "新建作品"按钮 + 添加账号
6 * - 右侧: 空间+频道融合选择器(支持按空间分组显示频道,可折叠)
7 */
8
9 'use client'
10
11 import type { SocialAccount } from '@/api/accounts/account.types'
12 import { Bot, SquarePen, UserPlus } from 'lucide-react'
13 import { useRouter } from 'next/navigation'
14 import { memo, useCallback, useMemo, useState } from 'react'
15 import { useShallow } from 'zustand/react/shallow'
16 import { apiUpdateAccountGroupSortRank } from '@/api/accounts/account.api'
17 import { useTransClient } from '@/app/i18n/client'
18 import { Button } from '@/components/ui/button'
19 import { useAccountStore } from '@/store/account'
20 import { toast } from '@/utils/ui/toast'
21 import AccountSelector from './components/AccountSelector'
22
23 export interface IAccountsTopNavProps {
24 onNewWork?: () => void
25 onAddAccount?: () => void
26 }
27
28 const AccountsTopNav = memo<IAccountsTopNavProps>(({ onNewWork, onAddAccount }) => {
29 const { t } = useTransClient('account')
30 const router = useRouter()
31 const [accountSearchText, setAccountSearchText] = useState('')
32 const [collapsedSpaces, setCollapsedSpaces] = useState<Set<string>>(new Set())
33 const [sortLoading, setSortLoading] = useState<string | null>(null)
34 const [deleteLoading, setDeleteLoading] = useState<string | null>(null)
35
36 const {
37 accountList,
38 accountActive,
39 setAccountActive,
40 accountGroupList,
41 getAccountGroup,
42 activeSpaceId,
43 setActiveSpaceId,
44 } = useAccountStore(
45 useShallow(state => ({
46 accountList: state.accountList,
47 accountActive: state.accountActive,
48 setAccountActive: state.setAccountActive,
49 accountGroupList: state.accountGroupList,
50 getAccountGroup: state.getAccountGroup,
51 activeSpaceId: state.activeSpaceId,
52 setActiveSpaceId: state.setActiveSpaceId,
53 })),
54 )
55
56 // 根据搜索文本筛选账户(不过滤离线账号)
57 const filteredAccounts = useMemo(
58 () =>
59 accountList.filter(
60 account =>
61 account.nickname?.toLowerCase().includes(accountSearchText.toLowerCase())
62 || account.uid?.toLowerCase().includes(accountSearchText.toLowerCase()),
63 ),
64 [accountList, accountSearchText],
65 )
66
67 // 处理账户选择
68 const handleAccountSelect = useCallback(
69 (account: SocialAccount | undefined) => {
70 setAccountActive(account)
71 setAccountSearchText('') // 清空搜索
72 },
73 [setAccountActive],
74 )
75
76 // 处理空间选择
77 const handleSpaceSelect = useCallback(
78 (spaceId: string | undefined) => {
79 setActiveSpaceId(spaceId)
80 setAccountActive(undefined) // 选择空间时清除账号选择
81 setAccountSearchText('') // 清空搜索
82 },
83 [setActiveSpaceId, setAccountActive],
84 )
85
86 // 处理空间排序
87 const handleGroupSort = useCallback(
88 async (groupId: string, direction: 'up' | 'down') => {
89 setSortLoading(groupId)
90 try {
91 const sortedGroups = [...accountGroupList].sort((a, b) => (a.rank || 0) - (b.rank || 0))
92 const currentIndex = sortedGroups.findIndex(g => g.id === groupId)
93
94 if (currentIndex === -1) {
95 setSortLoading(null)
96 return
97 }
98
99 const newIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1
100 if (newIndex < 0 || newIndex >= sortedGroups.length) {
101 setSortLoading(null)
102 return
103 }
104
105 // 交换位置
106 ;[sortedGroups[currentIndex], sortedGroups[newIndex]] = [
107 sortedGroups[newIndex],
108 sortedGroups[currentIndex],
109 ]
110
111 // 更新rank
112 const updateList = sortedGroups.map((group, index) => ({
113 id: group.id,
114 rank: index,
115 }))
116
117 const res = await apiUpdateAccountGroupSortRank({ list: updateList })
118 if (res?.code === 0) {
119 await getAccountGroup()
120 toast.success(t('messages.sortSuccess'))
121 }
122 else {
123 toast.error(res?.message || t('messages.sortFailed'))
124 }
125 }
126 catch (error) {
127 toast.error(t('messages.sortFailed'))
128 }
129 finally {
130 setSortLoading(null)
131 }
132 },
133 [accountGroupList, getAccountGroup, t],
134 )
135
136 // 切换空间折叠状态
137 const toggleSpaceCollapse = useCallback((spaceId: string) => {
138 setCollapsedSpaces((prev) => {
139 const newSet = new Set(prev)
140 if (newSet.has(spaceId)) {
141 newSet.delete(spaceId)
142 }
143 else {
144 newSet.add(spaceId)
145 }
146 return newSet
147 })
148 }, [])
149
150 // 显示空间分组的条件
151 const showSpaceGroups = accountGroupList.length > 1
152
153 // 获取排序后的空间列表
154 const sortedGroups = useMemo(() => {
155 return [...accountGroupList].sort((a, b) => (a.rank || 0) - (b.rank || 0))
156 }, [accountGroupList])
157
158 const handleNewWorkClick = useCallback(() => {
159 onNewWork?.()
160 }, [onNewWork])
161
162 const handleAgentCreateClick = useCallback(() => {
163 router.push('/ai-social')
164 }, [router])
165
166 return (
167 <>
168 <div className="h-14 md:h-16 flex items-center justify-between px-3 md:px-6 border-b bg-background shrink-0">
169 {/* 左侧: 操作按钮区域 */}
170 <div className="flex items-center gap-1 md:gap-2">
171 {/* 新建作品按钮 - 移动端显示文字,因为添加账号按钮移到下拉菜单了 */}
172 <Button
173 data-testid="accounts-new-work-btn"
174 onClick={handleNewWorkClick}
175 className="h-8 md:h-9 gap-1 md:gap-2 px-3 md:px-4 cursor-pointer"
176 >
177 <SquarePen className="h-4 w-4" />
178 <span>{t('newWork')}</span>
179 </Button>
180
181 <div className="border-l border-border h-6 mx-1 md:mx-2 hidden md:block" />
182
183 {/* 添加账号按钮 - 只在PC端显示,移动端在下拉菜单中 */}
184 <Button
185 data-testid="accounts-add-account-btn"
186 variant="outline"
187 onClick={onAddAccount}
188 className="h-8 md:h-9 gap-1 md:gap-2 px-2 md:px-4 cursor-pointer hidden md:flex"
189 >
190 <UserPlus className="h-4 w-4" />
191 <span>{t('addAccount')}</span>
192 </Button>
193
194 <div className="border-l border-border h-6 mx-1 md:mx-2" />
195
196 {/* Agent创建按钮 */}
197 <Button
198 variant="outline"
199 onClick={handleAgentCreateClick}
200 className="h-8 md:h-9 gap-1 md:gap-2 px-2 md:px-4 cursor-pointer"
201 >
202 <Bot className="h-4 w-4" />
203 <span>{t('agentCreate')}</span>
204 </Button>
205 </div>
206
207 {/* 右侧: 频道选择器(按空间分组) */}
208 <AccountSelector
209 accountActive={accountActive}
210 accountList={accountList}
211 accountGroupList={accountGroupList}
212 filteredAccounts={filteredAccounts}
213 collapsedSpaces={collapsedSpaces}
214 showSpaceGroups={showSpaceGroups}
215 sortedGroups={sortedGroups}
216 sortLoading={sortLoading}
217 deleteLoading={deleteLoading}
218 onAccountSelect={handleAccountSelect}
219 onToggleSpaceCollapse={toggleSpaceCollapse}
220 onGroupSort={handleGroupSort}
221 searchText={accountSearchText}
222 onSearchChange={setAccountSearchText}
223 onAddAccount={onAddAccount}
224 activeSpaceId={activeSpaceId}
225 onSpaceSelect={handleSpaceSelect}
226 />
227 </div>
228 </>
229 )
230 })
231
232 AccountsTopNav.displayName = 'AccountsTopNav'
233
234 export default AccountsTopNav
235
235 lines Plain Text