Files
accounts-manager-web/components/accounts/batch-operations.tsx
cloud370 2f16ff959b feat: 重命名pm2.sh为deploy.sh并实现自动部署功能
- 重命名pm2.sh为deploy.sh
- 实现自动从远程仓库拉取代码功能
- 添加自动构建功能
- 智能检测PM2进程状态,自动重载或创建进程
- 添加中文输出信息和错误处理

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 05:54:23 +08:00

316 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
</>
);
}