| 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 |