diff --git a/web/backend/routers/douyu.py b/web/backend/routers/douyu.py index 2e54504..63aae54 100644 --- a/web/backend/routers/douyu.py +++ b/web/backend/routers/douyu.py @@ -169,11 +169,18 @@ def task_types(current: User = Depends(require_permission("douyu:task"))): @router.get("/accounts", response_model=list[DouyuTaskAccountOut]) def list_task_accounts( search: str = Query(""), + ids: str = Query(""), db: Session = Depends(get_db), current: User = Depends(get_current_user), ): - """查看可执行斗鱼任务的账号(必须有成功 Cookie)。""" + """查看可执行斗鱼任务的账号(必须有成功 Cookie)。 + + ids 为逗号分隔的账号 ID,用于工作台按已导入账号过滤;为空时返回全部。 + """ query = _visible_task_accounts_query(db, current) + id_list = [int(x) for x in ids.split(",") if x.strip().isdigit()] + if id_list: + query = query.filter(Account.id.in_(id_list)) search_text = (search or "").strip() if search_text: pattern = f"%{search_text}%" diff --git a/web/frontend/src/api/douyu.ts b/web/frontend/src/api/douyu.ts index c075b53..11167aa 100644 --- a/web/frontend/src/api/douyu.ts +++ b/web/frontend/src/api/douyu.ts @@ -11,7 +11,7 @@ import type { export const douyuApi = { taskTypes: () => api.get, Record>('/douyu/task-types'), - listAccounts: (params?: { search?: string }) => + listAccounts: (params?: { search?: string; ids?: string }) => api.get('/douyu/accounts', { params }), getConfig: () => api.get('/douyu/config'), updateConfig: (data: Partial) => api.put('/douyu/config', data), diff --git a/web/frontend/src/pages/DouyuTasksPage.tsx b/web/frontend/src/pages/DouyuTasksPage.tsx index 90ffc7d..9f63c15 100644 --- a/web/frontend/src/pages/DouyuTasksPage.tsx +++ b/web/frontend/src/pages/DouyuTasksPage.tsx @@ -117,6 +117,7 @@ const ESPORTS_TASK_TYPES = new Set([ const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount'; const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count'; const DOUYU_LAYOUT_MODE_STORAGE_KEY = 'douyu_task_layout_mode'; +const DOUYU_WORKBENCH_IDS_STORAGE_KEY = (kind: HandbookKind) => `douyu_task_workbench_ids_${kind}`; const CONFIRM_FAIL_TIP_SECONDS = 6; function resultText(result: Record | null | undefined, key: string): string { @@ -272,6 +273,12 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) const quickActions = isEsportsHandbook ? ESPORTS_QUICK_ACTIONS : ELITE_QUICK_ACTIONS; const { can } = usePermissions(); const [accounts, setAccounts] = useState([]); + // 工作台已导入账号 ID(localStorage 持久化,默认空白,用户手动导入/移除) + const [workbenchIds, setWorkbenchIds] = useState(() => { + const raw = localStorage.getItem(DOUYU_WORKBENCH_IDS_STORAGE_KEY(handbook)); + if (!raw) return []; + return raw.split(',').map(Number).filter(Number.isInteger).filter((id) => id >= 1); + }); const [selectedIds, setSelectedIds] = useState([]); const [goods, setGoods] = useState([]); const [tasks, setTasks] = useState([]); @@ -450,13 +457,21 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) setLoading(true); try { const [accResult, goodsResult, taskResult, cfgResult, typeResult] = await Promise.allSettled([ - douyuApi.listAccounts(), + workbenchIds.length > 0 ? douyuApi.listAccounts({ ids: workbenchIds.join(',') }) : Promise.resolve([]), isEsportsHandbook ? douyuApi.listEsportsGoods() : douyuApi.listGoods(), douyuApi.listTasks(), canConfig ? douyuApi.getConfig() : Promise.resolve(null), douyuApi.taskTypes(), ]); - if (accResult.status === 'fulfilled') setAccounts(accResult.value); + 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); + } + } if (goodsResult.status === 'fulfilled') setGoods(goodsResult.value); if (taskResult.status === 'fulfilled') { setTasks(taskResult.value); @@ -489,7 +504,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) } finally { setLoading(false); } - }, [canConfig, isEsportsHandbook]); + }, [canConfig, isEsportsHandbook, workbenchIds]); const loadTasks = useCallback(async () => { try { @@ -510,6 +525,14 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) localStorage.setItem(DOUYU_LAYOUT_MODE_STORAGE_KEY, layoutMode); }, [layoutMode]); + // 工作台账号集合持久化:移除账号后刷新不再出现 + useEffect(() => { + localStorage.setItem( + DOUYU_WORKBENCH_IDS_STORAGE_KEY(handbook), + workbenchIds.length ? workbenchIds.join(',') : '', + ); + }, [handbook, workbenchIds]); + // 监听账号表格区域高度变化,动态计算 scroll.y 实现表体内部滚动 useEffect(() => { const node = accountTableAreaRef.current; @@ -810,16 +833,18 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) const loadImportPool = useCallback(async () => { setImportLoading(true); try { + // 候选池 = 账号库全量,过滤已导入工作台的账号 const result = await douyuApi.listAccounts({ search: importSearch }); - setImportPool(result.filter((a) => !accounts.find((x) => x.id === a.id))); + setImportPool(result.filter((a) => !workbenchIds.includes(a.id))); } finally { setImportLoading(false); } - }, [importSearch, accounts]); + }, [importSearch, workbenchIds]); useEffect(() => { if (importOpen) void loadImportPool(); }, [importOpen, loadImportPool]); const importAccounts = (ids: number[]) => { const toImport = importPool.filter((a) => ids.includes(a.id)); + setWorkbenchIds((prev) => [...new Set([...prev, ...toImport.map((a) => a.id)])]); setAccounts((prev) => { const existing = new Set(prev.map((a) => a.id)); return [...prev, ...toImport.filter((a) => !existing.has(a.id))]; @@ -831,8 +856,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) const removeSelected = () => { const sel = new Set(selectedIds); + setWorkbenchIds((prev) => prev.filter((id) => !sel.has(id))); setAccounts((prev) => prev.filter((a) => !sel.has(a.id))); setSelectedIds([]); + message.success(`已移出 ${sel.size} 个账号`); }; // ---- Context menu ---- @@ -1524,6 +1551,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) size="small" className="douyu-task-record-table" loading={loading} + locale={{ emptyText: '工作台暂无账号,点击右上角「导入账号」添加' }} rowSelection={{ selectedRowKeys: selectedIds, onChange: (keys) => setSelectedIds(keys.map(Number)), @@ -1746,7 +1774,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) }, ]} pagination={{ pageSize: 10 }} - locale={{ emptyText: '没有可导入的账号(操作台已包含所有账号)' }} + locale={{ emptyText: '没有可导入的账号' }} />