feat(douyu): 支持工作台分页与一键导入

This commit is contained in:
yml2213
2026-08-07 23:06:08 +08:00
parent 5d8fdf8620
commit c77e8f4641
3 changed files with 128 additions and 38 deletions
+3 -1
View File
@@ -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'),
+94 -37
View File
@@ -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>