| 1 | /** |
| 2 | * UnifiedChannelSpaceList - 统一的空间和频道管理列表 |
| 3 | * |
| 4 | * 功能: |
| 5 | * - 空间列表(支持添加、编辑、删除、排序) |
| 6 | * - 每个空间下显示频道(支持删除) |
| 7 | * - 完全使用全局account store数据 |
| 8 | */ |
| 9 | |
| 10 | 'use client' |
| 11 | |
| 12 | import type { SocialAccount } from '@/api/accounts/account.types' |
| 13 | import { useCallback, useState } from 'react' |
| 14 | import { useShallow } from 'zustand/react/shallow' |
| 15 | import { apiUpdateAccountGroupSortRank, deleteAccountApi, deleteAccountGroupApi, updateAccountGroupApi } from '@/api/accounts/account.api' |
| 16 | |
| 17 | import { useTransClient } from '@/app/i18n/client' |
| 18 | import { ScrollArea } from '@/components/ui/scroll-area' |
| 19 | import { useAccountStore } from '@/store/account' |
| 20 | import { toast } from '@/utils/ui/toast' |
| 21 | import { CreateSpaceSection } from './CreateSpaceSection' |
| 22 | import { DeleteConfirmDialog } from './DeleteConfirmDialog' |
| 23 | import { SpaceItem } from './SpaceItem' |
| 24 | import { SpaceListSkeleton } from './SpaceListSkeleton' |
| 25 | |
| 26 | export function UnifiedChannelSpaceList() { |
| 27 | const { t } = useTransClient('account') |
| 28 | const { accountGroupList, accountList, accountLoading, getAccountGroup, getAccountList } |
| 29 | = useAccountStore( |
| 30 | useShallow(state => ({ |
| 31 | accountGroupList: state.accountGroupList, |
| 32 | accountList: state.accountList, |
| 33 | accountLoading: state.accountLoading, |
| 34 | getAccountGroup: state.getAccountGroup, |
| 35 | getAccountList: state.getAccountList, |
| 36 | })), |
| 37 | ) |
| 38 | |
| 39 | // UI状态 |
| 40 | const [editingSpace, setEditingSpace] = useState<string | null>(null) |
| 41 | const [editSpaceName, setEditSpaceName] = useState('') |
| 42 | const [editingSpaceLoading, setEditingSpaceLoading] = useState(false) |
| 43 | const [collapsedSpaces, setCollapsedSpaces] = useState<Set<string>>(new Set()) |
| 44 | const [deleteLoading, setDeleteLoading] = useState<string | null>(null) |
| 45 | const [sortingSpaceLoading, setSortingSpaceLoading] = useState<string | null>(null) |
| 46 | |
| 47 | // 删除确认对话框 |
| 48 | const [deleteDialog, setDeleteDialog] = useState<{ |
| 49 | open: boolean |
| 50 | type: 'space' | 'channel' |
| 51 | id: string |
| 52 | name: string |
| 53 | }>({ |
| 54 | open: false, |
| 55 | type: 'space', |
| 56 | id: '', |
| 57 | name: '', |
| 58 | }) |
| 59 | |
| 60 | // 开始编辑空间 |
| 61 | const startEditingSpace = useCallback((spaceId: string, currentName: string) => { |
| 62 | setEditingSpace(spaceId) |
| 63 | setEditSpaceName(currentName) |
| 64 | }, []) |
| 65 | |
| 66 | // 保存空间编辑 |
| 67 | const saveSpaceEdit = useCallback(async () => { |
| 68 | if (!editingSpace || !editSpaceName.trim()) |
| 69 | return |
| 70 | |
| 71 | setEditingSpaceLoading(true) |
| 72 | try { |
| 73 | await updateAccountGroupApi({ |
| 74 | id: editingSpace, |
| 75 | name: editSpaceName.trim(), |
| 76 | }) |
| 77 | await getAccountGroup() |
| 78 | toast.success(t('channelManager.editSpaceSuccess', '空间编辑成功')) |
| 79 | setEditingSpace(null) |
| 80 | setEditSpaceName('') |
| 81 | } |
| 82 | catch (error) { |
| 83 | toast.error(t('channelManager.editSpaceFailed', '空间编辑失败')) |
| 84 | } |
| 85 | finally { |
| 86 | setEditingSpaceLoading(false) |
| 87 | } |
| 88 | }, [editingSpace, editSpaceName, getAccountGroup, t]) |
| 89 | |
| 90 | // 取消编辑 |
| 91 | const cancelSpaceEdit = useCallback(() => { |
| 92 | setEditingSpace(null) |
| 93 | setEditSpaceName('') |
| 94 | }, []) |
| 95 | |
| 96 | // 删除空间或频道 |
| 97 | const confirmDelete = useCallback(async () => { |
| 98 | setDeleteLoading(deleteDialog.id) |
| 99 | try { |
| 100 | if (deleteDialog.type === 'space') { |
| 101 | await deleteAccountGroupApi([deleteDialog.id]) |
| 102 | toast.success(t('channelManager.deleteSpaceSuccess', '删除空间成功')) |
| 103 | await getAccountGroup() |
| 104 | } |
| 105 | else { |
| 106 | await deleteAccountApi(deleteDialog.id) |
| 107 | toast.success(t('channelManager.deleteChannelSuccess', '删除频道成功')) |
| 108 | // 删除频道后同时刷新账户列表和分组数据 |
| 109 | await Promise.all([getAccountList(), getAccountGroup()]) |
| 110 | } |
| 111 | // 删除成功后关闭对话框 |
| 112 | setDeleteDialog({ open: false, type: 'space', id: '', name: '' }) |
| 113 | } |
| 114 | catch (error) { |
| 115 | toast.error(deleteDialog.type === 'space' |
| 116 | ? t('channelManager.deleteSpaceFailed') |
| 117 | : t('channelManager.deleteChannelFailed')) |
| 118 | // 删除失败不关闭对话框,让用户可以重试 |
| 119 | } |
| 120 | finally { |
| 121 | setDeleteLoading(null) |
| 122 | } |
| 123 | }, [deleteDialog, getAccountGroup, getAccountList, t]) |
| 124 | |
| 125 | // 切换空间折叠状态 |
| 126 | const toggleSpaceCollapse = useCallback((spaceId: string) => { |
| 127 | setCollapsedSpaces((prev) => { |
| 128 | const newSet = new Set(prev) |
| 129 | if (newSet.has(spaceId)) { |
| 130 | newSet.delete(spaceId) |
| 131 | } |
| 132 | else { |
| 133 | newSet.add(spaceId) |
| 134 | } |
| 135 | return newSet |
| 136 | }) |
| 137 | }, []) |
| 138 | |
| 139 | // 空间排序函数 |
| 140 | const handleSpaceSort = useCallback( |
| 141 | async (spaceId: string, direction: 'up' | 'down') => { |
| 142 | if (sortingSpaceLoading) |
| 143 | return // 防止重复操作 |
| 144 | |
| 145 | setSortingSpaceLoading(spaceId) |
| 146 | |
| 147 | try { |
| 148 | const sortedGroups = [...accountGroupList].sort((a, b) => (a.rank || 0) - (b.rank || 0)) |
| 149 | const currentIndex = sortedGroups.findIndex(g => g.id === spaceId) |
| 150 | |
| 151 | if (currentIndex === -1) |
| 152 | return |
| 153 | |
| 154 | const newIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1 |
| 155 | if (newIndex < 0 || newIndex >= sortedGroups.length) { |
| 156 | return |
| 157 | |
| 158 | // 交换位置 |
| 159 | }[sortedGroups[currentIndex], sortedGroups[newIndex]] = [ |
| 160 | sortedGroups[newIndex], |
| 161 | sortedGroups[currentIndex], |
| 162 | ] |
| 163 | |
| 164 | // 更新rank |
| 165 | const updateList = sortedGroups.map((group, index) => ({ |
| 166 | id: group.id, |
| 167 | rank: index, |
| 168 | })) |
| 169 | |
| 170 | await apiUpdateAccountGroupSortRank({ list: updateList }) |
| 171 | await getAccountGroup() |
| 172 | toast.success(t('channelManager.sortSuccess', '排序成功')) |
| 173 | } |
| 174 | catch (error) { |
| 175 | toast.error(t('channelManager.sortFailed', '排序失败')) |
| 176 | } |
| 177 | finally { |
| 178 | setSortingSpaceLoading(null) |
| 179 | } |
| 180 | }, |
| 181 | [accountGroupList, getAccountGroup, t, sortingSpaceLoading], |
| 182 | ) |
| 183 | |
| 184 | // 获取空间下的频道 |
| 185 | const getChannelsInSpace = useCallback( |
| 186 | (spaceId: string): SocialAccount[] => { |
| 187 | // TODO: 从API获取真实频道数据 |
| 188 | // 暂时返回模拟数据 |
| 189 | return accountList.filter(account => account.groupId === spaceId) |
| 190 | }, |
| 191 | [accountList], |
| 192 | ) |
| 193 | |
| 194 | if (accountLoading) { |
| 195 | return <SpaceListSkeleton /> |
| 196 | } |
| 197 | |
| 198 | return ( |
| 199 | <div className="flex flex-col gap-4 h-full overflow-hidden"> |
| 200 | {/* 添加新空间 */} |
| 201 | <CreateSpaceSection onSpaceCreated={getAccountGroup} /> |
| 202 | |
| 203 | {/* 空间和频道列表 */} |
| 204 | <ScrollArea className="flex-1 h-full"> |
| 205 | <div className="space-y-2"> |
| 206 | {accountGroupList.map((space, index) => { |
| 207 | const channels = getChannelsInSpace(space.id) |
| 208 | const isEditing = editingSpace === space.id |
| 209 | const isCollapsed = collapsedSpaces.has(space.id) |
| 210 | |
| 211 | return ( |
| 212 | <SpaceItem |
| 213 | key={space.id} |
| 214 | space={space} |
| 215 | channels={channels} |
| 216 | index={index} |
| 217 | totalSpaces={accountGroupList.length} |
| 218 | isCollapsed={isCollapsed} |
| 219 | isEditing={isEditing} |
| 220 | editSpaceName={editSpaceName} |
| 221 | editingSpaceLoading={editingSpaceLoading} |
| 222 | deleteLoading={deleteLoading} |
| 223 | sortingLoading={sortingSpaceLoading} |
| 224 | onToggleCollapse={() => toggleSpaceCollapse(space.id)} |
| 225 | onStartEdit={() => startEditingSpace(space.id, space.name)} |
| 226 | onEditNameChange={setEditSpaceName} |
| 227 | onSaveEdit={saveSpaceEdit} |
| 228 | onCancelEdit={cancelSpaceEdit} |
| 229 | onMoveUp={() => handleSpaceSort(space.id, 'up')} |
| 230 | onMoveDown={() => handleSpaceSort(space.id, 'down')} |
| 231 | onDelete={() => |
| 232 | setDeleteDialog({ |
| 233 | open: true, |
| 234 | type: 'space', |
| 235 | id: space.id, |
| 236 | name: space.name, |
| 237 | })} |
| 238 | onChannelDelete={(channel: SocialAccount) => |
| 239 | setDeleteDialog({ |
| 240 | open: true, |
| 241 | type: 'channel', |
| 242 | id: channel.id, |
| 243 | name: channel.nickname, |
| 244 | })} |
| 245 | onRefresh={getAccountGroup} |
| 246 | /> |
| 247 | ) |
| 248 | })} |
| 249 | </div> |
| 250 | </ScrollArea> |
| 251 | |
| 252 | {/* 删除确认对话框 */} |
| 253 | <DeleteConfirmDialog |
| 254 | open={deleteDialog.open} |
| 255 | type={deleteDialog.type} |
| 256 | name={deleteDialog.name} |
| 257 | loading={!!deleteLoading} |
| 258 | onOpenChange={open => setDeleteDialog(prev => ({ ...prev, open }))} |
| 259 | onConfirm={confirmDelete} |
| 260 | /> |
| 261 | </div> |
| 262 | ) |
| 263 | } |
| 264 |