账号导入--可以直接打标签
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -80,8 +80,13 @@ class UserUpdate(BaseModel):
|
||||
|
||||
# ---- 账号 ----
|
||||
class AccountImport(BaseModel):
|
||||
"""批量导入,文本格式:用户名|密码|邮箱|邮箱密码"""
|
||||
"""批量导入,文本格式:用户名|密码|邮箱|邮箱密码|标签(可选)
|
||||
|
||||
tag 为本次导入的统一标签兜底:某行未单独写标签时使用该值,
|
||||
行内第5列标签优先。
|
||||
"""
|
||||
text: str
|
||||
tag: str = ""
|
||||
|
||||
|
||||
class AccountAssign(BaseModel):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user