Files
accounts-manager-web/components/accounts/account-upload.tsx
cloud370 7aaeffa498 feat: 实现账户批量导出功能和界面优化
- 新增批量导出功能,支持text模式导出账户数据
- 添加导出弹窗,支持文本全选和文件下载
- 移动刷新按钮到全局位置,统一刷新账户和统计数据
- 在统计卡片中将已导出状态计入可用账户
- 创建自定义确认对话框替换系统confirm弹窗
- 统一按钮尺寸,修复刷新和上传按钮大小不一致
- 添加已导出状态的中文映射和样式

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

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

299 lines
9.9 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, useEffect } 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, DialogTrigger } from '@/components/ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Upload, Plus, FileText } from 'lucide-react';
import { ScriptUploadItem, StatsOverview } from '@/lib/types';
import { PlatformSelector, OwnerSelector, StatusSelector } from '@/components/shared';
interface AccountUploadProps {
onUpload: (accounts: ScriptUploadItem[], ownerId: string) => Promise<void>;
stats?: StatsOverview | null;
}
export function AccountUpload({ onUpload, stats }: AccountUploadProps) {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [ownerId, setOwnerId] = useState('');
// 文本上传
const [textData, setTextData] = useState('');
const [defaultStatus, setDefaultStatus] = useState('available');
const [defaultPlatform, setDefaultPlatform] = useState('');
// 单个添加
const [singleAccount, setSingleAccount] = useState({
customId: '',
data: ''
});
const parseTextData = (text: string): ScriptUploadItem[] => {
const lines = text.trim().split('\n').filter(line => line.trim());
const accounts: ScriptUploadItem[] = [];
for (const line of lines) {
try {
// 支持多种格式
if (line.includes('----')) {
// ---- 分隔格式customId----datadata中可能包含更多的----
const firstSeparatorIndex = line.indexOf('----');
if (firstSeparatorIndex > 0) {
const customId = line.substring(0, firstSeparatorIndex).trim();
const data = line.substring(firstSeparatorIndex + 4).trim(); // 跳过第一个----
accounts.push({
platform: defaultPlatform,
customId,
data,
status: defaultStatus
});
}
} else if (line.includes('\t')) {
// 制表符分隔platform\tcustomId\tdata\tstatus
const parts = line.split('\t');
if (parts.length >= 3) {
accounts.push({
platform: parts[0].trim(),
customId: parts[1].trim(),
data: parts[2].trim(),
status: parts[3]?.trim() || defaultStatus
});
}
} else if (line.includes(',')) {
// 逗号分隔platform,customId,data,status
const parts = line.split(',');
if (parts.length >= 3) {
accounts.push({
platform: parts[0].trim(),
customId: parts[1].trim(),
data: parts[2].trim(),
status: parts[3]?.trim() || defaultStatus
});
}
} else if (line.startsWith('{') && line.endsWith('}')) {
// JSON格式
const parsed = JSON.parse(line);
if (parsed.platform && parsed.customId && parsed.data) {
accounts.push({
platform: parsed.platform,
customId: parsed.customId,
data: typeof parsed.data === 'string' ? parsed.data : JSON.stringify(parsed.data),
status: parsed.status || defaultStatus
});
}
} else {
// 默认格式整行作为ID和data
const trimmedLine = line.trim();
if (trimmedLine) {
accounts.push({
platform: defaultPlatform,
customId: trimmedLine,
data: trimmedLine,
status: defaultStatus
});
}
}
} catch (error) {
console.warn('Failed to parse line:', line, error);
}
}
return accounts;
};
const handleTextUpload = async () => {
if (!ownerId.trim()) {
alert('请输入所有者ID');
return;
}
if (!textData.trim()) {
alert('请输入账户数据');
return;
}
const accounts = parseTextData(textData);
if (accounts.length === 0) {
alert('未解析到有效的账户数据');
return;
}
setLoading(true);
try {
await onUpload(accounts, ownerId);
setOpen(false);
setTextData('');
setOwnerId('');
} finally {
setLoading(false);
}
};
const handleSingleUpload = async () => {
if (!ownerId.trim()) {
alert('请输入所有者ID');
return;
}
if (!singleAccount.customId || !singleAccount.data) {
alert('请填写完整的账户信息');
return;
}
setLoading(true);
try {
const accountToUpload = {
platform: defaultPlatform,
customId: singleAccount.customId,
data: singleAccount.data,
status: defaultStatus
};
await onUpload([accountToUpload], ownerId);
setOpen(false);
setSingleAccount({ customId: '', data: '' });
setOwnerId('');
} finally {
setLoading(false);
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button size="sm">
<Upload className="h-4 w-4 mr-2" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto space-y-4 pr-2">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div>
<label className="text-sm font-medium">ID</label>
<OwnerSelector
value={ownerId}
onValueChange={setOwnerId}
stats={stats || null}
placeholder="选择所有者ID"
/>
</div>
<div>
<label className="text-sm font-medium"></label>
<PlatformSelector
value={defaultPlatform}
onValueChange={setDefaultPlatform}
stats={stats || null}
placeholder="选择平台"
/>
</div>
<div>
<label className="text-sm font-medium"></label>
<StatusSelector
value={defaultStatus}
onValueChange={setDefaultStatus}
stats={stats || null}
placeholder="选择状态"
/>
</div>
</div>
<Tabs defaultValue="batch" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="batch">
<FileText className="h-4 w-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="single">
<Plus className="h-4 w-4 mr-2" />
</TabsTrigger>
</TabsList>
<TabsContent value="batch" className="space-y-4">
<div>
<label className="text-sm font-medium"></label>
<Textarea
placeholder={`支持以下格式:
1. ---- 分割格式(推荐):
user1@gmail.com----password123----cookie=abc123----token=xyz789
user2@gmail.com----{"password":"456789","token":"fb_token"}
user3@twitter.com----oauth_token----oauth_secret----user_data
2. 默认格式(一行一个):
user1@gmail.com
user2@facebook.com
simple_username
3. 制表符分隔platform customId data status
4. 逗号分隔platform,customId,data,status
5. JSON格式{"platform":"google","customId":"user@gmail.com","data":"..."}
格式说明:
- ---- 分割: customId----纯数据内容(第一个----后的所有内容作为data
- 默认格式: 整行既作为customId也作为data
- 平台和状态: 在上方外部设置`}
value={textData}
onChange={(e) => setTextData(e.target.value)}
className="h-[200px] max-h-[300px] font-mono text-sm resize-y overflow-y-auto"
/>
</div>
<Button
onClick={handleTextUpload}
disabled={loading}
className="w-full"
>
{loading ? '上传中...' : '批量上传'}
</Button>
</TabsContent>
<TabsContent value="single" className="space-y-4">
<div className="grid grid-cols-1 gap-4">
<div>
<label className="text-sm font-medium">ID</label>
<Input
placeholder="如user@gmail.com"
value={singleAccount.customId}
onChange={(e) => setSingleAccount({...singleAccount, customId: e.target.value})}
/>
</div>
</div>
<div>
<label className="text-sm font-medium"></label>
<Textarea
placeholder='纯数据内容password123----cookie=abc123----token=xyz789 或 {"username":"user@gmail.com","password":"123456","token":"abc"}'
value={singleAccount.data}
onChange={(e) => setSingleAccount({...singleAccount, data: e.target.value})}
className="min-h-[120px]"
/>
</div>
<Button
onClick={handleSingleUpload}
disabled={loading}
className="w-full"
>
{loading ? '添加中...' : '添加账户'}
</Button>
</TabsContent>
</Tabs>
</div>
</DialogContent>
</Dialog>
);
}