feat(douyu): 支持工作台分页与一键导入
This commit is contained in:
@@ -211,6 +211,37 @@ def list_task_accounts(
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/accounts/ids")
|
||||
def list_task_account_ids(
|
||||
search: str = Query(""),
|
||||
tag: str = Query(""),
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""返回当前用户可导入工作台的全部账号 ID,不受列表分页限制。"""
|
||||
query = _visible_task_accounts_query(db, current)
|
||||
tag_text = (tag or "").strip()
|
||||
if tag_text:
|
||||
query = query.filter(Account.tag == tag_text)
|
||||
search_text = (search or "").strip()
|
||||
if search_text:
|
||||
pattern = f"%{search_text}%"
|
||||
query = query.filter(or_(
|
||||
Account.username.ilike(pattern),
|
||||
Account.uid.ilike(pattern),
|
||||
Account.nickname.ilike(pattern),
|
||||
Account.tag.ilike(pattern),
|
||||
Account.game_name.ilike(pattern),
|
||||
Account.esports_game_name.ilike(pattern),
|
||||
Account.xpd_game_name.ilike(pattern),
|
||||
))
|
||||
account_ids = [
|
||||
account_id
|
||||
for account_id, in query.enable_eagerloads(False).order_by(Account.id.desc()).with_entities(Account.id).all()
|
||||
]
|
||||
return {"account_ids": account_ids, "total": len(account_ids)}
|
||||
|
||||
|
||||
@router.get("/config", response_model=DouyuConfigOut)
|
||||
def get_config(
|
||||
db: Session = Depends(get_db),
|
||||
|
||||
@@ -16,8 +16,10 @@ export const douyuApi = {
|
||||
taskTypes: () => api.get<Record<string, string>, Record<string, string>>('/douyu/task-types'),
|
||||
listAccounts: (params?: { search?: string; tag?: string; ids?: string }) =>
|
||||
api.get<DouyuTaskAccountItem[], DouyuTaskAccountItem[]>('/douyu/accounts', { params }),
|
||||
listAccountsPaged: (params: PageParams & { ids?: string }) =>
|
||||
listAccountsPaged: (params: PageParams & { ids?: string; tag?: string }) =>
|
||||
api.get<PaginatedResponse<DouyuTaskAccountItem>, PaginatedResponse<DouyuTaskAccountItem>>('/douyu/accounts', { params }),
|
||||
listAccountIds: (params?: { search?: string; tag?: string }) =>
|
||||
api.get<{ account_ids: number[]; total: number }, { account_ids: number[]; total: number }>('/douyu/accounts/ids', { params }),
|
||||
getConfig: () => api.get<DouyuConfig, DouyuConfig>('/douyu/config'),
|
||||
updateConfig: (data: Partial<DouyuConfig>) => api.put<DouyuConfig, DouyuConfig>('/douyu/config', data),
|
||||
listGoods: () => api.get<DouyuGoodsItem[], DouyuGoodsItem[]>('/douyu/goods'),
|
||||
|
||||
@@ -348,6 +348,8 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
const v = Number(localStorage.getItem('douyu_task_account_page_size'));
|
||||
return [10, 20, 50, 100].includes(v) ? v : 20;
|
||||
});
|
||||
const [accountPage, setAccountPage] = useState(1);
|
||||
const [accountTotal, setAccountTotal] = useState(0);
|
||||
const tasksLoadingRef = useRef(false);
|
||||
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
@@ -357,6 +359,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
const [importTags, setImportTags] = useState<string[]>([]);
|
||||
const [importSelectedIds, setImportSelectedIds] = useState<number[]>([]);
|
||||
const [importLoading, setImportLoading] = useState(false);
|
||||
const [importAllLoading, setImportAllLoading] = useState(false);
|
||||
const [importPage, setImportPage] = useState(1);
|
||||
const [importTotal, setImportTotal] = useState(0);
|
||||
const importPageSize = 20;
|
||||
|
||||
// 绑定/支付弹窗以 task 为唯一数据源;二维码弹窗支持多账号并行(Tabs 切换)
|
||||
const [qrTaskIds, setQrTaskIds] = useState<number[]>([]);
|
||||
@@ -526,7 +532,14 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
setLoading(true);
|
||||
try {
|
||||
const [accResult, goodsResult, taskResult, cfgResult, typeResult] = await Promise.allSettled([
|
||||
workbenchIds.length > 0 ? douyuApi.listAccounts({ ids: workbenchIds.join(',') }) : Promise.resolve([]),
|
||||
workbenchIds.length > 0
|
||||
? douyuApi.listAccountsPaged({
|
||||
ids: workbenchIds.join(','),
|
||||
search: accountSearch.trim() || undefined,
|
||||
page: accountPage,
|
||||
page_size: accountPageSize,
|
||||
})
|
||||
: Promise.resolve({ items: [] as DouyuTaskAccountItem[], total: 0, page: accountPage, page_size: accountPageSize }),
|
||||
isPeaceHandbook
|
||||
? douyuApi.listPeaceGoods()
|
||||
: (isEsportsHandbook ? douyuApi.listEsportsGoods() : douyuApi.listGoods()),
|
||||
@@ -535,12 +548,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
douyuApi.taskTypes(),
|
||||
]);
|
||||
if (accResult.status === 'fulfilled') {
|
||||
setAccounts(accResult.value);
|
||||
// 账号被删除等场景下清理已失效的工作台 ID,避免请求残留空 ID
|
||||
if (workbenchIds.length > 0) {
|
||||
const validIds = accResult.value.map((a) => a.id);
|
||||
const next = workbenchIds.filter((id) => validIds.includes(id));
|
||||
if (next.length !== workbenchIds.length) setWorkbenchIds(next);
|
||||
setAccounts(accResult.value.items);
|
||||
setAccountTotal(accResult.value.total);
|
||||
if (accountPage > 1 && accResult.value.items.length === 0) {
|
||||
setAccountPage(Math.max(1, Math.ceil(accResult.value.total / accountPageSize)));
|
||||
}
|
||||
}
|
||||
if (goodsResult.status === 'fulfilled') setGoods(goodsResult.value);
|
||||
@@ -575,7 +586,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canConfig, isEsportsHandbook, isPeaceHandbook, workbenchIds]);
|
||||
}, [accountPage, accountPageSize, accountSearch, canConfig, isEsportsHandbook, isPeaceHandbook, workbenchIds]);
|
||||
|
||||
const loadTasks = useCallback(async () => {
|
||||
if (tasksLoadingRef.current) return;
|
||||
@@ -920,19 +931,29 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
setImportLoading(true);
|
||||
try {
|
||||
// 候选池 = 账号库按搜索/标签过滤,剔除已导入工作台的账号
|
||||
const result = await douyuApi.listAccounts({ search: importSearch, tag: importTag || undefined });
|
||||
setImportPool(result.filter((a) => !workbenchIds.includes(a.id)));
|
||||
const result = await douyuApi.listAccountsPaged({
|
||||
search: importSearch.trim() || undefined,
|
||||
tag: importTag || undefined,
|
||||
page: importPage,
|
||||
page_size: importPageSize,
|
||||
});
|
||||
setImportPool(result.items.filter((a) => !workbenchIds.includes(a.id)));
|
||||
setImportTotal(result.total);
|
||||
} finally {
|
||||
setImportLoading(false);
|
||||
}
|
||||
}, [importSearch, importTag, workbenchIds]);
|
||||
}, [importPage, importSearch, importTag, workbenchIds]);
|
||||
useEffect(() => { if (importOpen) void loadImportPool(); }, [importOpen, loadImportPool]);
|
||||
useEffect(() => {
|
||||
if (!importOpen) return;
|
||||
accountApi.listTags().then(setImportTags).catch(() => {});
|
||||
}, [importOpen]);
|
||||
// 切换搜索/标签时清空已选,避免计数与可见行不一致
|
||||
useEffect(() => { setImportSelectedIds([]); }, [importSearch, importTag]);
|
||||
useEffect(() => {
|
||||
setImportPage(1);
|
||||
setImportSelectedIds([]);
|
||||
}, [importSearch, importTag]);
|
||||
useEffect(() => { setImportSelectedIds([]); }, [importPage]);
|
||||
|
||||
const importAccounts = (ids: number[]) => {
|
||||
const toImport = importPool.filter((a) => ids.includes(a.id));
|
||||
@@ -946,6 +967,29 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
message.success(`已导入 ${toImport.length} 个账号`);
|
||||
};
|
||||
|
||||
const importAllAccounts = async () => {
|
||||
setImportAllLoading(true);
|
||||
try {
|
||||
const result = await douyuApi.listAccountIds({
|
||||
search: importSearch.trim() || undefined,
|
||||
tag: importTag || undefined,
|
||||
});
|
||||
const imported = result.account_ids.filter((id) => !workbenchIds.includes(id));
|
||||
if (imported.length === 0) {
|
||||
message.info('当前筛选条件下没有可新增的账号');
|
||||
return;
|
||||
}
|
||||
setWorkbenchIds((prev) => [...new Set([...prev, ...imported])]);
|
||||
setImportSelectedIds([]);
|
||||
setImportOpen(false);
|
||||
message.success(`已一键导入 ${imported.length} 个账号`);
|
||||
} catch (error) {
|
||||
message.error(getErrorMessage(error));
|
||||
} finally {
|
||||
setImportAllLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const removeSelected = () => {
|
||||
const sel = new Set(selectedIds);
|
||||
setWorkbenchIds((prev) => prev.filter((id) => !sel.has(id)));
|
||||
@@ -1427,18 +1471,6 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
},
|
||||
];
|
||||
|
||||
const filteredAccounts = useMemo(() => {
|
||||
if (!accountSearch.trim()) return accounts;
|
||||
const kw = accountSearch.toLowerCase();
|
||||
return accounts.filter((a) =>
|
||||
(a.nickname || '').toLowerCase().includes(kw)
|
||||
|| (a.username || '').toLowerCase().includes(kw)
|
||||
|| (a.uid || '').toLowerCase().includes(kw)
|
||||
|| (a.game_name || '').toLowerCase().includes(kw)
|
||||
|| (a.esports_game_name || '').toLowerCase().includes(kw),
|
||||
);
|
||||
}, [accounts, accountSearch]);
|
||||
|
||||
const [configFormValues, setConfigFormValues] = useState<DouyuConfig | null>(null);
|
||||
useEffect(() => {
|
||||
if (configOpen && config) setConfigFormValues({ ...config });
|
||||
@@ -1700,7 +1732,11 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
<Space>
|
||||
<Input
|
||||
size="small" placeholder="搜索账号/昵称/游戏名" prefix={<SearchOutlined />}
|
||||
value={accountSearch} onChange={(e) => setAccountSearch(e.target.value)}
|
||||
value={accountSearch}
|
||||
onChange={(e) => {
|
||||
setAccountSearch(e.target.value);
|
||||
setAccountPage(1);
|
||||
}}
|
||||
style={{ width: 200 }} allowClear
|
||||
/>
|
||||
</Space>
|
||||
@@ -1723,15 +1759,18 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
onChange: (keys) => setSelectedIds(keys.map(Number)),
|
||||
}}
|
||||
columns={accountColumns}
|
||||
dataSource={filteredAccounts}
|
||||
dataSource={accounts}
|
||||
pagination={{
|
||||
current: accountPage,
|
||||
pageSize: accountPageSize,
|
||||
total: accountTotal,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
size: 'small',
|
||||
showLessItems: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (_page, size) => {
|
||||
onChange: (page, size) => {
|
||||
setAccountPage(size === accountPageSize ? page : 1);
|
||||
if (size !== accountPageSize) {
|
||||
setAccountPageSize(size);
|
||||
localStorage.setItem('douyu_task_account_page_size', String(size));
|
||||
@@ -1834,7 +1873,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
<div style={{ flex: 1, minHeight: 0, display: 'flex', gap: 12, overflow: 'hidden' }}>
|
||||
<Card
|
||||
size="small" title="账号"
|
||||
extra={<Tag color="blue">已选 {selectedIds.length}/{accounts.length}</Tag>}
|
||||
extra={<Tag color="blue">已选 {selectedIds.length}/{accountTotal}</Tag>}
|
||||
style={{ flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||
styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}
|
||||
>
|
||||
@@ -1863,7 +1902,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
</Card>
|
||||
<Card
|
||||
size="small" title="账号"
|
||||
extra={<Tag color="blue">已选 {selectedIds.length}/{accounts.length}</Tag>}
|
||||
extra={<Tag color="blue">已选 {selectedIds.length}/{accountTotal}</Tag>}
|
||||
style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||
styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}
|
||||
>
|
||||
@@ -1906,30 +1945,40 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
footer={null}
|
||||
width={700}
|
||||
>
|
||||
<Space style={{ marginBottom: 12, width: '100%', justifyContent: 'space-between' }}>
|
||||
<Space>
|
||||
<div style={{ marginBottom: 12, display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
<Space wrap>
|
||||
<Input
|
||||
placeholder="搜索账号/昵称" prefix={<SearchOutlined />}
|
||||
value={importSearch} onChange={(e) => setImportSearch(e.target.value)}
|
||||
value={importSearch}
|
||||
onChange={(e) => {
|
||||
setImportSearch(e.target.value);
|
||||
setImportPage(1);
|
||||
}}
|
||||
style={{ width: 220 }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="按标签筛选" allowClear showSearch
|
||||
value={importTag || undefined}
|
||||
onChange={(value?: string) => setImportTag(value || '')}
|
||||
onChange={(value?: string) => {
|
||||
setImportTag(value || '');
|
||||
setImportPage(1);
|
||||
}}
|
||||
style={{ width: 160 }}
|
||||
options={importTags.map((t) => ({ value: t, label: t }))}
|
||||
/>
|
||||
</Space>
|
||||
<Space>
|
||||
<Space wrap style={{ width: '100%', justifyContent: 'flex-start' }}>
|
||||
<Button onClick={() => importAccounts(importPool.map((a) => a.id))}>
|
||||
导入全部 ({importPool.length})
|
||||
导入本页 ({importPool.length})
|
||||
</Button>
|
||||
<Button loading={importAllLoading} onClick={() => void importAllAccounts()}>
|
||||
一键导入全部
|
||||
</Button>
|
||||
<Button type="primary" onClick={() => importAccounts(importSelectedIds)} disabled={importSelectedIds.length === 0}>
|
||||
导入选中 ({importSelectedIds.length})
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="id" size="small" loading={importLoading}
|
||||
rowSelection={{ selectedRowKeys: importSelectedIds, onChange: (keys) => setImportSelectedIds(keys.map(Number)) }}
|
||||
@@ -1949,7 +1998,15 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
render: (v) => v ?? '-',
|
||||
},
|
||||
]}
|
||||
pagination={{ pageSize: 10 }}
|
||||
pagination={{
|
||||
current: importPage,
|
||||
pageSize: importPageSize,
|
||||
total: importTotal,
|
||||
size: 'small',
|
||||
showLessItems: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: setImportPage,
|
||||
}}
|
||||
locale={{ emptyText: '没有可导入的账号' }}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user