账号导入--可以直接打标签

This commit is contained in:
yml2213
2026-08-01 12:24:19 +08:00
parent d417d15d9b
commit 11b249f1fb
6 changed files with 43 additions and 9 deletions
+5 -2
View File
@@ -186,8 +186,11 @@ def import_accounts(
db: Session = Depends(get_db),
current: User = Depends(require_permission("account:import")),
):
"""批量导入账号。格式:用户名|密码|邮箱|邮箱密码|标签(可选)"""
accounts, skipped = parse_and_build_accounts(req.text)
"""批量导入账号。格式:用户名|密码|邮箱|邮箱密码|标签(可选)
req.tag 为统一标签兜底:某行未单独写标签时使用该值,行内标签优先。
"""
accounts, skipped = parse_and_build_accounts(req.text, req.tag)
if accounts:
db.add_all(accounts)
+6 -1
View File
@@ -80,8 +80,13 @@ class UserUpdate(BaseModel):
# ---- 账号 ----
class AccountImport(BaseModel):
"""批量导入,文本格式:用户名|密码|邮箱|邮箱密码"""
"""批量导入,文本格式:用户名|密码|邮箱|邮箱密码|标签(可选)
tag 为本次导入的统一标签兜底:某行未单独写标签时使用该值,
行内第5列标签优先。
"""
text: str
tag: str = ""
class AccountAssign(BaseModel):
+10 -2
View File
@@ -31,15 +31,21 @@ def cookie_account_ids_query(db: Session):
).distinct()
def parse_and_build_accounts(text: str) -> tuple[list[Account], int]:
def parse_and_build_accounts(text: str, default_tag: str = "") -> tuple[list[Account], int]:
"""
解析批量导入文本,构建 Account ORM 对象列表。
Args:
text: 批量导入文本,每行格式:用户名|密码|邮箱|邮箱密码|标签(可选)
default_tag: 统一标签兜底。某行未单独写标签时使用该值,
行内第5列标签优先。
Returns:
(accounts, skipped_count)
"""
from core.douyu.email_verifier import get_email_config_for_account
default_tag = (default_tag or "").strip()
accounts = []
skipped = 0
for line in text.strip().split('\n'):
@@ -52,7 +58,9 @@ def parse_and_build_accounts(text: str) -> tuple[list[Account], int]:
continue
username, password, email, email_password = [p.strip() for p in parts[:4]]
tag = parts[4].strip() if len(parts) > 4 else ""
line_tag = parts[4].strip() if len(parts) > 4 else ""
# 行内标签优先,未写则用统一兜底标签
tag = line_tag or default_tag
if not all([username, password, email, email_password]):
skipped += 1
continue
+2 -1
View File
@@ -18,7 +18,8 @@ export const accountApi = {
listPaged: (params: PageParams & { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_sensitive?: boolean }) =>
api.get<PaginatedResponse<AccountItem>, PaginatedResponse<AccountItem>>('/accounts', { params }),
summary: () => api.get<BasicSummary, BasicSummary>('/accounts/summary'),
import: (text: string) => api.post<MessageCountResponse, MessageCountResponse>('/accounts/import', { text }),
import: (text: string, tag?: string) =>
api.post<MessageCountResponse, MessageCountResponse>('/accounts/import', { text, tag: tag || '' }),
assign: (id: number, assigned_to: number | null) =>
api.put<MessageResponse, MessageResponse>(`/accounts/${id}/assign`, { assigned_to }),
batchAssign: (account_ids: number[], assigned_to: number | null) =>
+15 -2
View File
@@ -25,6 +25,7 @@ export default function AccountsPage() {
const [loading, setLoading] = useState(false);
const [importOpen, setImportOpen] = useState(false);
const [importText, setImportText] = useState('');
const [importTag, setImportTag] = useState('');
const [importing, setImporting] = useState(false);
const [tagFilter, setTagFilter] = useState<string>('');
const [searchText, setSearchText] = useState('');
@@ -137,10 +138,11 @@ export default function AccountsPage() {
}
setImporting(true);
try {
const result = await accountApi.import(importText);
const result = await accountApi.import(importText, importTag);
message.success(result.message);
setImportOpen(false);
setImportText('');
setImportTag('');
loadAccounts();
loadTags();
loadSummary();
@@ -375,7 +377,7 @@ export default function AccountsPage() {
</Popconfirm>
)}
{canImport && (
<Button type="primary" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>
<Button type="primary" icon={<ImportOutlined />} onClick={() => { setImportTag(''); setImportOpen(true); }}>
</Button>
)}
@@ -480,6 +482,17 @@ export default function AccountsPage() {
placeholder={`用户名|密码|邮箱|邮箱密码|标签\n用户名|密码|邮箱|邮箱密码|标签`}
style={{ marginTop: 8 }}
/>
<div style={{ marginTop: 12 }}>
<Text type="secondary">使</Text>
<Select
mode="tags"
style={{ width: '100%', marginTop: 4 }}
placeholder="输入或选择标签"
value={importTag ? [importTag] : []}
onChange={(vals) => setImportTag(vals.length > 0 ? vals[vals.length - 1] : '')}
options={tags.map((t) => ({ value: t, label: t }))}
/>
</div>
</Modal>
<Modal
+5 -1
View File
@@ -44,6 +44,8 @@ const ESPORTS_QUICK_ACTIONS = [
{ key: 'query_esports_game_name', icon: <SearchOutlined /> },
{ key: 'create_esports_qr', icon: <CreditCardOutlined /> },
{ key: 'query_esports_points', icon: <SearchOutlined /> },
{ key: 'query_change_bind_time', icon: <FieldTimeOutlined /> },
{ key: 'query_limited_goods', icon: <SearchOutlined /> },
{ key: 'query_gold_balance', icon: <SearchOutlined /> },
{ key: 'refresh_esports_goods', icon: <ReloadOutlined /> },
{ key: 'exchange_esports_goods', icon: <ShoppingOutlined /> },
@@ -76,6 +78,8 @@ const ESPORTS_TASK_TYPES = new Set([
'create_esports_qr',
'query_esports_points',
'query_gold_balance',
'query_change_bind_time',
'query_limited_goods',
'refresh_esports_goods',
'exchange_esports_goods',
'create_gold_qr',
@@ -1308,7 +1312,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
dataSource={filteredAccounts}
pagination={{ pageSize: 15, showSizeChanger: false, size: 'small', showLessItems: true }}
tableLayout="fixed"
scroll={{ y: Math.max(120, accountTableAreaHeight - 80) }}
scroll={{ x: 960, y: Math.max(120, accountTableAreaHeight - 80) }}
onRow={(record) => ({
onContextMenu: (e) => handleRowContextMenu(record, e),
style: { cursor: 'context-menu' },