- 重命名pm2.sh为deploy.sh - 实现自动从远程仓库拉取代码功能 - 添加自动构建功能 - 智能检测PM2进程状态,自动重载或创建进程 - 添加中文输出信息和错误处理 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			316 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			316 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| "use client";
 | ||
| 
 | ||
| import { useState } from 'react';
 | ||
| import { Button } from '@/components/ui/button';
 | ||
| import { Input } from '@/components/ui/input';
 | ||
| import { Textarea } from '@/components/ui/textarea';
 | ||
| import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
 | ||
| import { Edit3, Trash2, Download } from 'lucide-react';
 | ||
| import { Account, StatsOverview } from '@/lib/types';
 | ||
| import { toast } from 'sonner';
 | ||
| import { PlatformSelector, OwnerSelector, StatusSelector } from '@/components/shared';
 | ||
| import { apiClient } from '@/lib/api';
 | ||
| 
 | ||
| interface BatchOperationsProps {
 | ||
|   selectedCount: number;
 | ||
|   selectedAccounts: Account[];
 | ||
|   stats: StatsOverview | null;
 | ||
|   onBatchUpdate: (payload: Partial<Pick<Account, 'status' | 'ownerId' | 'notes' | 'platform'>>, targetIds?: number[]) => Promise<void>;
 | ||
|   onBatchDelete: () => Promise<void>;
 | ||
|   onRefresh?: () => Promise<void>;
 | ||
| }
 | ||
| 
 | ||
| export function BatchOperations({ selectedCount, selectedAccounts, stats, onBatchUpdate, onBatchDelete, onRefresh }: BatchOperationsProps) {
 | ||
|   const [updateDialog, setUpdateDialog] = useState(false);
 | ||
|   const [exportDialog, setExportDialog] = useState(false);
 | ||
|   const [exportData, setExportData] = useState({ count: 0, text: '' });
 | ||
|   const [updateData, setUpdateData] = useState({
 | ||
|     status: '',
 | ||
|     platform: '',
 | ||
|     ownerId: '',
 | ||
|     notes: ''
 | ||
|   });
 | ||
|   const [loading, setLoading] = useState(false);
 | ||
| 
 | ||
|   // 计算选中账户的统计信息
 | ||
|   const getSelectedStats = () => {
 | ||
|     const platforms = new Set(selectedAccounts.map(account => account.platform));
 | ||
|     const owners = new Set(selectedAccounts.map(account => account.ownerId));
 | ||
|     
 | ||
|     return {
 | ||
|       platformCount: platforms.size,
 | ||
|       ownerCount: owners.size,
 | ||
|       platforms: Array.from(platforms).slice(0, 3), // 只显示前3个
 | ||
|       owners: Array.from(owners).slice(0, 3) // 只显示前3个
 | ||
|     };
 | ||
|   };
 | ||
| 
 | ||
|   const selectedStats = getSelectedStats();
 | ||
| 
 | ||
| 
 | ||
|   const handleBatchExport = async () => {
 | ||
|     setLoading(true);
 | ||
|     try {
 | ||
|       const response = await apiClient.batchExportAccounts({
 | ||
|         ids: selectedAccounts.map(account => account.id),
 | ||
|         mode: 'text'
 | ||
|       });
 | ||
|       
 | ||
|       if (response.code === 0 && response.data) {
 | ||
|         setExportData({
 | ||
|           count: response.data.exportedCount,
 | ||
|           text: response.data.data
 | ||
|         });
 | ||
|         setExportDialog(true);
 | ||
|         toast.success(`成功导出 ${response.data.exportedCount} 个账户`);
 | ||
|         
 | ||
|         // 导出后刷新数据
 | ||
|         if (onRefresh) {
 | ||
|           await onRefresh();
 | ||
|         }
 | ||
|       } else {
 | ||
|         toast.error(response.message || '导出失败');
 | ||
|       }
 | ||
|     } catch (error) {
 | ||
|       console.error('Export error:', error);
 | ||
|       toast.error('导出失败,请稍后重试');
 | ||
|     } finally {
 | ||
|       setLoading(false);
 | ||
|     }
 | ||
|   };
 | ||
| 
 | ||
|   const handleDownloadTxt = () => {
 | ||
|     const platforms = Array.from(new Set(selectedAccounts.map(account => account.platform)));
 | ||
|     const owners = Array.from(new Set(selectedAccounts.map(account => account.ownerId)));
 | ||
|     
 | ||
|     // 构建文件名部分
 | ||
|     const platformPart = platforms.length === 1 ? platforms[0] : `${platforms.length}个平台`;
 | ||
|     const ownerPart = owners.length === 1 ? owners[0] : `${owners.length}个用户`;
 | ||
|     const countPart = `${exportData.count}个账户`;
 | ||
|     const datePart = new Date().toLocaleString('zh-CN', {
 | ||
|       timeZone: 'Asia/Shanghai',
 | ||
|       year: 'numeric',
 | ||
|       month: '2-digit',
 | ||
|       day: '2-digit',
 | ||
|       hour: '2-digit',
 | ||
|       minute: '2-digit',
 | ||
|       second: '2-digit'
 | ||
|     }).replace(/[\/\s:]/g, '-');
 | ||
|     
 | ||
|     const blob = new Blob([exportData.text], { type: 'text/plain;charset=utf-8' });
 | ||
|     const url = URL.createObjectURL(blob);
 | ||
|     const link = document.createElement('a');
 | ||
|     link.href = url;
 | ||
|     link.download = `${platformPart}_${ownerPart}_${countPart}_${datePart}.txt`;
 | ||
|     document.body.appendChild(link);
 | ||
|     link.click();
 | ||
|     document.body.removeChild(link);
 | ||
|     URL.revokeObjectURL(url);
 | ||
|   };
 | ||
| 
 | ||
|   const selectAllText = (event: React.MouseEvent<HTMLTextAreaElement>) => {
 | ||
|     const target = event.target as HTMLTextAreaElement;
 | ||
|     target.select();
 | ||
|   };
 | ||
| 
 | ||
|   const handleBatchUpdate = async () => {
 | ||
|     const payload: Partial<Pick<Account, 'status' | 'ownerId' | 'notes' | 'platform'>> = {};
 | ||
|     if (updateData.status) payload.status = updateData.status;
 | ||
|     if (updateData.platform) payload.platform = updateData.platform;
 | ||
|     if (updateData.ownerId) payload.ownerId = updateData.ownerId;
 | ||
|     if (updateData.notes) payload.notes = updateData.notes;
 | ||
|     
 | ||
|     if (Object.keys(payload).length === 0) {
 | ||
|       toast.warning('请至少选择一个字段进行更新');
 | ||
|       return;
 | ||
|     }
 | ||
|     
 | ||
|     setLoading(true);
 | ||
|     try {
 | ||
|       await onBatchUpdate(payload);
 | ||
|       setUpdateDialog(false);
 | ||
|       setUpdateData({ status: '', platform: '', ownerId: '', notes: '' });
 | ||
|     } finally {
 | ||
|       setLoading(false);
 | ||
|     }
 | ||
|   };
 | ||
| 
 | ||
|   const handleBatchDelete = async () => {
 | ||
|     setLoading(true);
 | ||
|     try {
 | ||
|       await onBatchDelete();
 | ||
|     } finally {
 | ||
|       setLoading(false);
 | ||
|     }
 | ||
|   };
 | ||
| 
 | ||
|   if (selectedCount === 0) {
 | ||
|     return null;
 | ||
|   }
 | ||
| 
 | ||
|   return (
 | ||
|     <>
 | ||
|       <div className="flex items-center space-x-2 p-3 bg-muted rounded-lg">
 | ||
|         <div className="flex-1">
 | ||
|           <div className="flex items-center space-x-4">
 | ||
|             <span className="text-sm font-medium">已选择 {selectedCount} 个账户</span>
 | ||
|             <div className="flex items-center space-x-3 text-xs text-muted-foreground">
 | ||
|               <span>
 | ||
|                 {selectedStats.ownerCount} 个用户
 | ||
|                 {selectedStats.ownerCount > 0 && (
 | ||
|                   <span className="ml-1">
 | ||
|                     ({selectedStats.owners.join(', ')}
 | ||
|                     {selectedStats.ownerCount > 3 && ` 等${selectedStats.ownerCount}个`})
 | ||
|                   </span>
 | ||
|                 )}
 | ||
|               </span>
 | ||
|               <span>•</span>
 | ||
|               <span>
 | ||
|                 {selectedStats.platformCount} 个平台
 | ||
|                 {selectedStats.platformCount > 0 && (
 | ||
|                   <span className="ml-1">
 | ||
|                     ({selectedStats.platforms.join(', ')}
 | ||
|                     {selectedStats.platformCount > 3 && ` 等${selectedStats.platformCount}个`})
 | ||
|                   </span>
 | ||
|                 )}
 | ||
|               </span>
 | ||
|             </div>
 | ||
|           </div>
 | ||
|         </div>
 | ||
|         <Button
 | ||
|           variant="outline"
 | ||
|           size="sm"
 | ||
|           onClick={() => setUpdateDialog(true)}
 | ||
|           disabled={loading}
 | ||
|         >
 | ||
|           <Edit3 className="h-4 w-4 mr-1" />
 | ||
|           批量更新
 | ||
|         </Button>
 | ||
|         <Button
 | ||
|           variant="outline"
 | ||
|           size="sm"
 | ||
|           onClick={handleBatchExport}
 | ||
|           disabled={loading}
 | ||
|         >
 | ||
|           <Download className="h-4 w-4 mr-1" />
 | ||
|           导出
 | ||
|         </Button>
 | ||
|         <Button
 | ||
|           variant="destructive"
 | ||
|           size="sm"
 | ||
|           onClick={handleBatchDelete}
 | ||
|           disabled={loading}
 | ||
|         >
 | ||
|           <Trash2 className="h-4 w-4 mr-1" />
 | ||
|           批量删除
 | ||
|         </Button>
 | ||
|       </div>
 | ||
| 
 | ||
|       <Dialog open={updateDialog} onOpenChange={setUpdateDialog}>
 | ||
|         <DialogContent>
 | ||
|           <DialogHeader>
 | ||
|             <DialogTitle>批量更新账户</DialogTitle>
 | ||
|             <DialogDescription>
 | ||
|               将对 {selectedCount} 个账户进行批量更新操作
 | ||
|             </DialogDescription>
 | ||
|           </DialogHeader>
 | ||
|           <div className="grid grid-cols-1 gap-6">
 | ||
|             <div className="space-y-3">
 | ||
|               <label className="text-sm font-medium">平台</label>
 | ||
|               <PlatformSelector
 | ||
|                 value={updateData.platform}
 | ||
|                 onValueChange={(value) => setUpdateData({...updateData, platform: value})}
 | ||
|                 stats={stats}
 | ||
|                 showNoneOption={true}
 | ||
|                 placeholder="选择新平台(可选)"
 | ||
|               />
 | ||
|             </div>
 | ||
|             
 | ||
|             <div className="space-y-3">
 | ||
|               <label className="text-sm font-medium">状态</label>
 | ||
|               <StatusSelector
 | ||
|                 value={updateData.status}
 | ||
|                 onValueChange={(value) => setUpdateData({...updateData, status: value})}
 | ||
|                 stats={stats}
 | ||
|                 showNoneOption={true}
 | ||
|                 placeholder="选择新状态(可选)"
 | ||
|               />
 | ||
|             </div>
 | ||
|             
 | ||
|             <div className="space-y-3">
 | ||
|               <label className="text-sm font-medium">所有者ID</label>
 | ||
|               <OwnerSelector
 | ||
|                 value={updateData.ownerId}
 | ||
|                 onValueChange={(value) => setUpdateData({...updateData, ownerId: value})}
 | ||
|                 stats={stats}
 | ||
|                 showNoneOption={true}
 | ||
|                 placeholder="选择所有者(可选)"
 | ||
|               />
 | ||
|             </div>
 | ||
|             
 | ||
|             <div className="space-y-3">
 | ||
|               <label className="text-sm font-medium">备注</label>
 | ||
|               <Input
 | ||
|                 placeholder="输入新的备注(可选)"
 | ||
|                 value={updateData.notes}
 | ||
|                 onChange={(e) => setUpdateData({...updateData, notes: e.target.value})}
 | ||
|               />
 | ||
|             </div>
 | ||
|           </div>
 | ||
|           <DialogFooter>
 | ||
|             <Button 
 | ||
|               variant="outline" 
 | ||
|               onClick={() => setUpdateDialog(false)}
 | ||
|               disabled={loading}
 | ||
|             >
 | ||
|               取消
 | ||
|             </Button>
 | ||
|             <Button 
 | ||
|               onClick={handleBatchUpdate}
 | ||
|               disabled={loading}
 | ||
|             >
 | ||
|               {loading ? '更新中...' : '确认更新'}
 | ||
|             </Button>
 | ||
|           </DialogFooter>
 | ||
|         </DialogContent>
 | ||
|       </Dialog>
 | ||
| 
 | ||
|       <Dialog open={exportDialog} onOpenChange={setExportDialog}>
 | ||
|         <DialogContent className="max-w-2xl max-h-[80vh] flex flex-col">
 | ||
|           <DialogHeader>
 | ||
|             <DialogTitle>账户导出</DialogTitle>
 | ||
|             <DialogDescription>
 | ||
|               成功导出 {exportData.count} 个账户
 | ||
|             </DialogDescription>
 | ||
|           </DialogHeader>
 | ||
|           <div className="flex-1 overflow-hidden">
 | ||
|             <div className="space-y-2 h-full flex flex-col">
 | ||
|               <label className="text-sm font-medium">导出内容(点击文本框全选):</label>
 | ||
|               <Textarea
 | ||
|                 value={exportData.text}
 | ||
|                 readOnly
 | ||
|                 onClick={selectAllText}
 | ||
|                 className="flex-1 font-mono text-sm resize-none"
 | ||
|                 placeholder="导出的账户数据将显示在这里..."
 | ||
|               />
 | ||
|             </div>
 | ||
|           </div>
 | ||
|           <DialogFooter className="mt-4">
 | ||
|             <Button 
 | ||
|               variant="outline" 
 | ||
|               onClick={() => setExportDialog(false)}
 | ||
|             >
 | ||
|               关闭
 | ||
|             </Button>
 | ||
|             <Button 
 | ||
|               onClick={handleDownloadTxt}
 | ||
|             >
 | ||
|               <Download className="h-4 w-4 mr-1" />
 | ||
|               下载 .txt 文件
 | ||
|             </Button>
 | ||
|           </DialogFooter>
 | ||
|         </DialogContent>
 | ||
|       </Dialog>
 | ||
|     </>
 | ||
|   );
 | ||
| } |