- 新增每页数量localStorage缓存,刷新后保持用户选择 - 增加表格显示高度从600px到800px,显示更多数据 - 支持点击表格行进行选择,提升操作便利性 - 格式化创建时间显示为详细时间(年/月/日 时:分) - 优化复选框和操作按钮的点击事件冲突 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			305 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			305 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { useState, useEffect, useCallback } from 'react';
 | ||
| import { 
 | ||
|   Account, 
 | ||
|   ListAccountsBody, 
 | ||
|   ListAccountsResponse, 
 | ||
|   StatsOverview, 
 | ||
|   BusinessCode,
 | ||
|   BatchUpdateBody,
 | ||
|   BatchDeleteBody,
 | ||
|   ScriptUploadItem
 | ||
| } from '@/lib/types';
 | ||
| import { apiClient } from '@/lib/api';
 | ||
| import { toast } from 'sonner';
 | ||
| 
 | ||
| export interface UseAccountsReturn {
 | ||
|   // 数据状态
 | ||
|   accounts: Account[];
 | ||
|   stats: StatsOverview | null;
 | ||
|   loading: boolean;
 | ||
|   selectedIds: number[];
 | ||
|   
 | ||
|   // 分页和筛选
 | ||
|   pagination: {
 | ||
|     page: number;
 | ||
|     pageSize: number;
 | ||
|     total: number;
 | ||
|     totalPages: number;
 | ||
|   };
 | ||
|   filters: {
 | ||
|     platform: string;
 | ||
|     status: string[];
 | ||
|     ownerId: string;
 | ||
|     search: string;
 | ||
|   };
 | ||
|   sort: {
 | ||
|     field: keyof Account;
 | ||
|     order: 'asc' | 'desc';
 | ||
|   };
 | ||
|   
 | ||
|   // 操作方法
 | ||
|   fetchAccounts: () => Promise<void>;
 | ||
|   fetchStats: () => Promise<void>;
 | ||
|   handleFilterChange: (key: string, value: any) => void;
 | ||
|   handlePageChange: (page: number) => void;
 | ||
|   handlePageSizeChange: (pageSize: number) => void;
 | ||
|   handleSortChange: (field: keyof Account, order: 'asc' | 'desc') => void;
 | ||
|   handleSelectAll: (checked: boolean) => void;
 | ||
|   handleSelectOne: (id: number, checked: boolean) => void;
 | ||
|   handleBatchDelete: (showConfirm?: (options: any) => void) => Promise<void>;
 | ||
|   handleBatchUpdate: (payload: Partial<Pick<Account, 'status' | 'ownerId' | 'notes' | 'platform'>>, targetIds?: number[]) => Promise<void>;
 | ||
|   handleUploadAccounts: (accounts: ScriptUploadItem[], ownerId: string) => Promise<void>;
 | ||
|   setSelectedIds: (ids: number[]) => void;
 | ||
| }
 | ||
| 
 | ||
| export function useAccounts(): UseAccountsReturn {
 | ||
|   const [accounts, setAccounts] = useState<Account[]>([]);
 | ||
|   const [stats, setStats] = useState<StatsOverview | null>(null);
 | ||
|   const [loading, setLoading] = useState(false);
 | ||
|   const [selectedIds, setSelectedIds] = useState<number[]>([]);
 | ||
|   
 | ||
|   // 从 localStorage 获取缓存的分页大小,默认为 20
 | ||
|   const getInitialPageSize = () => {
 | ||
|     if (typeof window !== 'undefined') {
 | ||
|       const saved = localStorage.getItem('accounts-page-size');
 | ||
|       if (saved) {
 | ||
|         const pageSize = parseInt(saved, 10);
 | ||
|         // 验证是否为有效值
 | ||
|         if ([20, 100, 1000, 10000].includes(pageSize)) {
 | ||
|           return pageSize;
 | ||
|         }
 | ||
|       }
 | ||
|     }
 | ||
|     return 20;
 | ||
|   };
 | ||
|   
 | ||
|   const [pagination, setPagination] = useState({
 | ||
|     page: 1,
 | ||
|     pageSize: getInitialPageSize(),
 | ||
|     total: 0,
 | ||
|     totalPages: 0
 | ||
|   });
 | ||
|   
 | ||
|   const [filters, setFilters] = useState({
 | ||
|     platform: '',
 | ||
|     status: [] as string[],
 | ||
|     ownerId: '',
 | ||
|     search: ''
 | ||
|   });
 | ||
|   
 | ||
|   const [sort, setSort] = useState({
 | ||
|     field: 'id' as keyof Account,
 | ||
|     order: 'desc' as 'asc' | 'desc'
 | ||
|   });
 | ||
| 
 | ||
|   // 获取统计数据
 | ||
|   const fetchStats = async () => {
 | ||
|     try {
 | ||
|       const response = await apiClient.getStatsOverview();
 | ||
|       if (response.code === BusinessCode.Success && response.data) {
 | ||
|         setStats(response.data);
 | ||
|       }
 | ||
|     } catch (error) {
 | ||
|       console.error('Failed to fetch stats:', error);
 | ||
|       toast.error('获取统计数据失败');
 | ||
|     }
 | ||
|   };
 | ||
| 
 | ||
|   // 获取账户列表
 | ||
|   const fetchAccounts = useCallback(async () => {
 | ||
|     setLoading(true);
 | ||
|     try {
 | ||
|       const body: ListAccountsBody = {
 | ||
|         filters,
 | ||
|         pagination: {
 | ||
|           page: pagination.page,
 | ||
|           pageSize: pagination.pageSize
 | ||
|         },
 | ||
|         sort
 | ||
|       };
 | ||
|       
 | ||
|       const response = await apiClient.getAccountsList(body);
 | ||
|       if (response.code === BusinessCode.Success && response.data) {
 | ||
|         setAccounts(response.data.list);
 | ||
|         setPagination(prev => ({
 | ||
|           ...prev,
 | ||
|           total: response.data!.pagination.total,
 | ||
|           totalPages: response.data!.pagination.totalPages
 | ||
|         }));
 | ||
|       }
 | ||
|     } catch (error) {
 | ||
|       console.error('Failed to fetch accounts:', error);
 | ||
|       toast.error('获取账户列表失败');
 | ||
|     } finally {
 | ||
|       setLoading(false);
 | ||
|     }
 | ||
|   }, [filters, pagination.page, pagination.pageSize, sort]);
 | ||
| 
 | ||
|   // 处理筛选变化
 | ||
|   const handleFilterChange = (key: string, value: any) => {
 | ||
|     setFilters({ ...filters, [key]: value });
 | ||
|     setPagination({ ...pagination, page: 1 });
 | ||
|   };
 | ||
| 
 | ||
|   // 处理分页
 | ||
|   const handlePageChange = (page: number) => {
 | ||
|     setPagination({ ...pagination, page });
 | ||
|   };
 | ||
| 
 | ||
|   // 处理每页大小变化
 | ||
|   const handlePageSizeChange = (pageSize: number) => {
 | ||
|     // 保存到 localStorage
 | ||
|     if (typeof window !== 'undefined') {
 | ||
|       localStorage.setItem('accounts-page-size', pageSize.toString());
 | ||
|     }
 | ||
|     
 | ||
|     setPagination(prev => ({
 | ||
|       ...prev, 
 | ||
|       pageSize, 
 | ||
|       page: 1
 | ||
|     }));
 | ||
|   };
 | ||
| 
 | ||
|   // 处理排序
 | ||
|   const handleSortChange = (field: keyof Account, order: 'asc' | 'desc') => {
 | ||
|     setSort({ field, order });
 | ||
|   };
 | ||
| 
 | ||
|   // 处理全选
 | ||
|   const handleSelectAll = (checked: boolean) => {
 | ||
|     if (checked) {
 | ||
|       setSelectedIds(accounts.map(account => account.id));
 | ||
|     } else {
 | ||
|       setSelectedIds([]);
 | ||
|     }
 | ||
|   };
 | ||
| 
 | ||
|   // 处理单选
 | ||
|   const handleSelectOne = (id: number, checked: boolean) => {
 | ||
|     if (checked) {
 | ||
|       setSelectedIds([...selectedIds, id]);
 | ||
|     } else {
 | ||
|       setSelectedIds(selectedIds.filter(selectedId => selectedId !== id));
 | ||
|     }
 | ||
|   };
 | ||
| 
 | ||
|   // 批量删除
 | ||
|   const handleBatchDelete = async (showConfirm?: (options: any) => void) => {
 | ||
|     if (selectedIds.length === 0) return;
 | ||
|     
 | ||
|     const executeDelete = async () => {
 | ||
|       try {
 | ||
|         const response = await apiClient.batchDeleteAccounts({ ids: selectedIds });
 | ||
|         if (response.code === BusinessCode.Success) {
 | ||
|           setSelectedIds([]);
 | ||
|           await fetchAccounts();
 | ||
|           await fetchStats();
 | ||
|           toast.success(`成功删除 ${response.data?.deletedCount || 0} 个账户`);
 | ||
|         }
 | ||
|       } catch (error) {
 | ||
|         console.error('Failed to delete accounts:', error);
 | ||
|         toast.error('删除失败,请检查网络连接后重试');
 | ||
|       }
 | ||
|     };
 | ||
| 
 | ||
|     if (showConfirm) {
 | ||
|       showConfirm({
 | ||
|         title: '确认删除',
 | ||
|         description: `确认删除 ${selectedIds.length} 个账户?此操作不可撤销。`,
 | ||
|         confirmText: '确认删除',
 | ||
|         cancelText: '取消',
 | ||
|         variant: 'destructive',
 | ||
|         onConfirm: executeDelete
 | ||
|       });
 | ||
|     } else {
 | ||
|       // 如果没有提供确认函数,直接执行删除(用于兼容)
 | ||
|       if (confirm(`确认删除 ${selectedIds.length} 个账户?`)) {
 | ||
|         await executeDelete();
 | ||
|       }
 | ||
|     }
 | ||
|   };
 | ||
| 
 | ||
|   // 批量更新
 | ||
|   const handleBatchUpdate = async (payload: Partial<Pick<Account, 'status' | 'ownerId' | 'notes' | 'platform'>>, targetIds?: number[]) => {
 | ||
|     const idsToUpdate = targetIds || selectedIds;
 | ||
|     console.log('handleBatchUpdate 被调用, idsToUpdate:', idsToUpdate);
 | ||
|     console.log('payload:', payload);
 | ||
|     
 | ||
|     if (idsToUpdate.length === 0) {
 | ||
|       console.log('idsToUpdate为空,直接返回');
 | ||
|       return;
 | ||
|     }
 | ||
|     
 | ||
|     if (Object.keys(payload).length === 0) {
 | ||
|       toast.warning('请至少选择一个字段进行更新');
 | ||
|       return;
 | ||
|     }
 | ||
|     
 | ||
|     try {
 | ||
|       console.log('发送批量更新请求...');
 | ||
|       const response = await apiClient.batchUpdateAccounts({ ids: idsToUpdate, payload });
 | ||
|       console.log('批量更新响应:', response);
 | ||
|       
 | ||
|       if (response.code === BusinessCode.Success) {
 | ||
|         if (!targetIds) {
 | ||
|           // 只有在没有指定目标ID时才清空选中状态(批量操作时)
 | ||
|           setSelectedIds([]);
 | ||
|         }
 | ||
|         await fetchAccounts();
 | ||
|         await fetchStats();
 | ||
|         toast.success(`成功更新 ${response.data?.updatedCount || 0} 个账户`);
 | ||
|       }
 | ||
|     } catch (error) {
 | ||
|       console.error('Failed to update accounts:', error);
 | ||
|       toast.error('更新失败,请检查网络连接后重试');
 | ||
|     }
 | ||
|   };
 | ||
| 
 | ||
|   // 上传账户
 | ||
|   const handleUploadAccounts = async (uploadAccounts: ScriptUploadItem[], ownerId: string) => {
 | ||
|     try {
 | ||
|       const response = await apiClient.uploadAccounts(uploadAccounts, ownerId);
 | ||
|       if (response.code === BusinessCode.Success) {
 | ||
|         await fetchAccounts();
 | ||
|         await fetchStats();
 | ||
|         toast.success(`成功处理 ${response.data?.processedCount || 0} 个账户 (${response.data?.createdCount || 0} 个新建, ${response.data?.updatedCount || 0} 个更新)`);
 | ||
|         return;
 | ||
|       }
 | ||
|     } catch (error) {
 | ||
|       console.error('Failed to upload accounts:', error);
 | ||
|       toast.error('上传失败,请检查网络连接后重试');
 | ||
|     }
 | ||
|   };
 | ||
| 
 | ||
|   // 初始化数据
 | ||
|   useEffect(() => {
 | ||
|     fetchAccounts();
 | ||
|   }, [fetchAccounts]);
 | ||
| 
 | ||
|   // 只在开始时获取统计数据
 | ||
|   useEffect(() => {
 | ||
|     fetchStats();
 | ||
|   }, []);
 | ||
| 
 | ||
|   return {
 | ||
|     accounts,
 | ||
|     stats,
 | ||
|     loading,
 | ||
|     selectedIds,
 | ||
|     pagination,
 | ||
|     filters,
 | ||
|     sort,
 | ||
|     fetchAccounts,
 | ||
|     fetchStats,
 | ||
|     handleFilterChange,
 | ||
|     handlePageChange,
 | ||
|     handlePageSizeChange,
 | ||
|     handleSortChange,
 | ||
|     handleSelectAll,
 | ||
|     handleSelectOne,
 | ||
|     handleBatchDelete,
 | ||
|     handleBatchUpdate,
 | ||
|     handleUploadAccounts,
 | ||
|     setSelectedIds
 | ||
|   };
 | ||
| } |