Files
accounts-manager-web/components/accounts/batch-operations.tsx
cloud370 33cdc42b1f fix: 限制账户导出框高度,防止按钮被遮挡
- 设置导出对话框最大高度为80vh
- 优化布局使用flex确保按钮始终可见
- 文本区域自适应高度并支持滚动
- 提升大量账户导出时的用户体验

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

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

299 lines
10 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 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 = `accounts_export_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.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>
</>
);
}