返回 AiToEarn
UnifiedChannelSpaceList.tsx
根目录 / project / aitoearn-web / src / components / ChannelManager / components / UnifiedChannelSpaceList.tsx
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
264 lines Plain Text