From c77e8f46417f4be9a7bea18bdb75f16cab3cd89d Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 7 Aug 2026 23:06:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(douyu):=20=E6=94=AF=E6=8C=81=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8F=B0=E5=88=86=E9=A1=B5=E4=B8=8E=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/backend/routers/douyu.py | 31 +++++ web/frontend/src/api/douyu.ts | 4 +- web/frontend/src/pages/DouyuTasksPage.tsx | 131 ++++++++++++++++------ 3 files changed, 128 insertions(+), 38 deletions(-) diff --git a/web/backend/routers/douyu.py b/web/backend/routers/douyu.py index a2209e4..36773f7 100644 --- a/web/backend/routers/douyu.py +++ b/web/backend/routers/douyu.py @@ -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), diff --git a/web/frontend/src/api/douyu.ts b/web/frontend/src/api/douyu.ts index 0d92c06..46e548f 100644 --- a/web/frontend/src/api/douyu.ts +++ b/web/frontend/src/api/douyu.ts @@ -16,8 +16,10 @@ export const douyuApi = { taskTypes: () => api.get, Record>('/douyu/task-types'), listAccounts: (params?: { search?: string; tag?: string; ids?: string }) => api.get('/douyu/accounts', { params }), - listAccountsPaged: (params: PageParams & { ids?: string }) => + listAccountsPaged: (params: PageParams & { ids?: string; tag?: string }) => api.get, PaginatedResponse>('/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('/douyu/config'), updateConfig: (data: Partial) => api.put('/douyu/config', data), listGoods: () => api.get('/douyu/goods'), diff --git a/web/frontend/src/pages/DouyuTasksPage.tsx b/web/frontend/src/pages/DouyuTasksPage.tsx index 4c1a740..c18904e 100644 --- a/web/frontend/src/pages/DouyuTasksPage.tsx +++ b/web/frontend/src/pages/DouyuTasksPage.tsx @@ -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([]); const [importSelectedIds, setImportSelectedIds] = useState([]); 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([]); @@ -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(null); useEffect(() => { if (configOpen && config) setConfigFormValues({ ...config }); @@ -1700,7 +1732,11 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) } - value={accountSearch} onChange={(e) => setAccountSearch(e.target.value)} + value={accountSearch} + onChange={(e) => { + setAccountSearch(e.target.value); + setAccountPage(1); + }} style={{ width: 200 }} allowClear /> @@ -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 })
已选 {selectedIds.length}/{accounts.length}} + extra={已选 {selectedIds.length}/{accountTotal}} 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 }) 已选 {selectedIds.length}/{accounts.length}} + extra={已选 {selectedIds.length}/{accountTotal}} 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} > - - +
+ } - value={importSearch} onChange={(e) => setImportSearch(e.target.value)} + value={importSearch} + onChange={(e) => { + setImportSearch(e.target.value); + setImportPage(1); + }} style={{ width: 220 }} />