- 重命名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>
|
||
</>
|
||
);
|
||
} |