feat(douyu): 精英宝典支持多批次并发与多二维码并行扫码
- useWebSocketLogs 改为多连接(Map),新增 connectBatch/closeBatch/closeAll,旧 connect/close 向后兼容 - DouyuTasksPage runningBatchId 单值改为 Set 集合,移除 batchBusy 全局锁,右键菜单按账号维度禁用 - 二维码弹窗支持多账号标签切换并行扫码,恢复三弹窗互斥 - connectBatch 已有连接时不清日志,修复 onerror 不更新 connected - 修复自动弹出循环覆盖 active 标签的 bug
This commit is contained in:
@@ -13,6 +13,7 @@ interface ConnectOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MAX_LOGS = 1000;
|
const MAX_LOGS = 1000;
|
||||||
|
const DEFAULT_KEY = '__default__';
|
||||||
|
|
||||||
function toWebSocketUrl(pathOrUrl: string): string {
|
function toWebSocketUrl(pathOrUrl: string): string {
|
||||||
if (pathOrUrl.startsWith('ws://') || pathOrUrl.startsWith('wss://')) {
|
if (pathOrUrl.startsWith('ws://') || pathOrUrl.startsWith('wss://')) {
|
||||||
@@ -26,43 +27,59 @@ function toWebSocketUrl(pathOrUrl: string): string {
|
|||||||
export function useWebSocketLogs() {
|
export function useWebSocketLogs() {
|
||||||
const [logs, setLogs] = useState<RealtimeLog[]>([]);
|
const [logs, setLogs] = useState<RealtimeLog[]>([]);
|
||||||
const [connected, setConnected] = useState(false);
|
const [connected, setConnected] = useState(false);
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsMapRef = useRef<Map<string, WebSocket>>(new Map());
|
||||||
const callbacksRef = useRef<ConnectOptions>({});
|
const callbacksMapRef = useRef<Map<string, ConnectOptions>>(new Map());
|
||||||
const suppressCloseRef = useRef(false);
|
const suppressCloseRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const syncConnected = useCallback(() => {
|
||||||
|
setConnected(wsMapRef.current.size > 0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const clearLogs = useCallback(() => {
|
const clearLogs = useCallback(() => {
|
||||||
setLogs([]);
|
setLogs([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const close = useCallback((notify = false) => {
|
const closeBatch = useCallback((key: string, notify = false) => {
|
||||||
if (!wsRef.current) {
|
const ws = wsMapRef.current.get(key);
|
||||||
setConnected(false);
|
if (!ws) return;
|
||||||
return;
|
if (!notify) suppressCloseRef.current.add(key);
|
||||||
}
|
ws.close();
|
||||||
suppressCloseRef.current = !notify;
|
wsMapRef.current.delete(key);
|
||||||
wsRef.current.close();
|
syncConnected();
|
||||||
wsRef.current = null;
|
}, [syncConnected]);
|
||||||
setConnected(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const connect = useCallback((pathOrUrl: string, options: ConnectOptions = {}) => {
|
// 向后兼容:单连接场景关闭默认通道。
|
||||||
close(false);
|
const close = useCallback((notify = false) => {
|
||||||
suppressCloseRef.current = false;
|
closeBatch(DEFAULT_KEY, notify);
|
||||||
callbacksRef.current = options;
|
}, [closeBatch]);
|
||||||
if (options.clear ?? true) {
|
|
||||||
|
const closeAll = useCallback((notify = false) => {
|
||||||
|
Array.from(wsMapRef.current.keys()).forEach((key) => closeBatch(key, notify));
|
||||||
|
}, [closeBatch]);
|
||||||
|
|
||||||
|
const connectBatch = useCallback((key: string, pathOrUrl: string, options: ConnectOptions = {}) => {
|
||||||
|
// 关闭同 key 旧连接,避免重复
|
||||||
|
if (wsMapRef.current.has(key)) {
|
||||||
|
closeBatch(key, false);
|
||||||
|
}
|
||||||
|
suppressCloseRef.current.delete(key);
|
||||||
|
callbacksMapRef.current.set(key, options);
|
||||||
|
// 已有其他批次连接时不清空日志,避免冲掉正在进行的批次日志
|
||||||
|
const shouldClear = (options.clear ?? true) && wsMapRef.current.size === 0;
|
||||||
|
if (shouldClear) {
|
||||||
setLogs([]);
|
setLogs([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ws = new WebSocket(toWebSocketUrl(pathOrUrl));
|
const ws = new WebSocket(toWebSocketUrl(pathOrUrl));
|
||||||
wsRef.current = ws;
|
wsMapRef.current.set(key, ws);
|
||||||
setConnected(true);
|
syncConnected();
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data) as RealtimeLog;
|
const msg = JSON.parse(event.data) as RealtimeLog;
|
||||||
if (msg.level === 'heartbeat') return;
|
if (msg.level === 'heartbeat') return;
|
||||||
if (msg.level === 'result') {
|
if (msg.level === 'result') {
|
||||||
callbacksRef.current.onResult?.();
|
callbacksMapRef.current.get(key)?.onResult?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLogs((prev) => [...prev, msg].slice(-MAX_LOGS));
|
setLogs((prev) => [...prev, msg].slice(-MAX_LOGS));
|
||||||
@@ -72,27 +89,32 @@ export function useWebSocketLogs() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
const isCurrent = wsRef.current === ws;
|
// 只处理自己这个实例的关闭,避免覆盖重连后的新连接
|
||||||
const shouldNotify = isCurrent && !suppressCloseRef.current;
|
if (wsMapRef.current.get(key) !== ws) return;
|
||||||
if (isCurrent) {
|
wsMapRef.current.delete(key);
|
||||||
wsRef.current = null;
|
syncConnected();
|
||||||
setConnected(false);
|
const cb = callbacksMapRef.current.get(key);
|
||||||
suppressCloseRef.current = false;
|
if (!suppressCloseRef.current.has(key)) {
|
||||||
}
|
cb?.onClose?.();
|
||||||
if (shouldNotify) {
|
|
||||||
callbacksRef.current.onClose?.();
|
|
||||||
}
|
}
|
||||||
|
callbacksMapRef.current.delete(key);
|
||||||
|
suppressCloseRef.current.delete(key);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
setConnected(false);
|
if (wsMapRef.current.get(key) === ws) setConnected(false);
|
||||||
callbacksRef.current.onError?.();
|
callbacksMapRef.current.get(key)?.onError?.();
|
||||||
};
|
};
|
||||||
}, [close]);
|
}, [closeBatch, syncConnected]);
|
||||||
|
|
||||||
|
// 向后兼容:connect(pathOrUrl, options) 等价于 connectBatch(DEFAULT_KEY, ...)
|
||||||
|
const connect = useCallback((pathOrUrl: string, options: ConnectOptions = {}) => {
|
||||||
|
connectBatch(DEFAULT_KEY, pathOrUrl, options);
|
||||||
|
}, [connectBatch]);
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
close(false);
|
closeAll(false);
|
||||||
}, [close]);
|
}, [closeAll]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
logs,
|
logs,
|
||||||
@@ -100,5 +122,8 @@ export function useWebSocketLogs() {
|
|||||||
clearLogs,
|
clearLogs,
|
||||||
connect,
|
connect,
|
||||||
close,
|
close,
|
||||||
|
connectBatch,
|
||||||
|
closeBatch,
|
||||||
|
closeAll,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
const [taskTypes, setTaskTypes] = useState<Record<string, string>>({});
|
const [taskTypes, setTaskTypes] = useState<Record<string, string>>({});
|
||||||
const [config, setConfig] = useState<DouyuConfig | null>(null);
|
const [config, setConfig] = useState<DouyuConfig | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [runningBatchId, setRunningBatchId] = useState('');
|
const [runningBatchIds, setRunningBatchIds] = useState<Set<string>>(new Set());
|
||||||
const [configOpen, setConfigOpen] = useState(false);
|
const [configOpen, setConfigOpen] = useState(false);
|
||||||
const [concurrency, setConcurrency] = useState(3);
|
const [concurrency, setConcurrency] = useState(3);
|
||||||
const [goldAmount, setGoldAmount] = useState(() => savedPositiveInteger(DOUYU_GOLD_AMOUNT_STORAGE_KEY));
|
const [goldAmount, setGoldAmount] = useState(() => savedPositiveInteger(DOUYU_GOLD_AMOUNT_STORAGE_KEY));
|
||||||
@@ -263,8 +263,9 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
const [importSelectedIds, setImportSelectedIds] = useState<number[]>([]);
|
const [importSelectedIds, setImportSelectedIds] = useState<number[]>([]);
|
||||||
const [importLoading, setImportLoading] = useState(false);
|
const [importLoading, setImportLoading] = useState(false);
|
||||||
|
|
||||||
// 绑定/支付弹窗以 task 为唯一数据源
|
// 绑定/支付弹窗以 task 为唯一数据源;二维码弹窗支持多账号并行(Tabs 切换)
|
||||||
const [qrTask, setQrTask] = useState<DouyuTaskItem | null>(null);
|
const [qrTaskIds, setQrTaskIds] = useState<number[]>([]);
|
||||||
|
const [activeQrTaskId, setActiveQrTaskId] = useState<number | null>(null);
|
||||||
const [esportsBindTask, setEsportsBindTask] = useState<DouyuTaskItem | null>(null);
|
const [esportsBindTask, setEsportsBindTask] = useState<DouyuTaskItem | null>(null);
|
||||||
const [payTask, setPayTask] = useState<DouyuTaskItem | null>(null);
|
const [payTask, setPayTask] = useState<DouyuTaskItem | null>(null);
|
||||||
const autoOpenedQrTaskIds = useRef<Set<number>>(new Set());
|
const autoOpenedQrTaskIds = useRef<Set<number>>(new Set());
|
||||||
@@ -302,6 +303,12 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
[activeTaskTypes, tasks],
|
[activeTaskTypes, tasks],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// qrTask 由 activeQrTaskId 从最新 tasks 中派生,切换 Tab 即切换关注账号
|
||||||
|
const qrTask = useMemo(
|
||||||
|
() => (activeQrTaskId == null ? null : tasks.find((t) => t.id === activeQrTaskId) || null),
|
||||||
|
[tasks, activeQrTaskId],
|
||||||
|
);
|
||||||
|
|
||||||
// 换绑时间:优先最近一次查询/拦截任务结果,回退账号落库字段
|
// 换绑时间:优先最近一次查询/拦截任务结果,回退账号落库字段
|
||||||
const latestChangeWaitByAccount = useMemo(() => {
|
const latestChangeWaitByAccount = useMemo(() => {
|
||||||
const map = new Map<number, {
|
const map = new Map<number, {
|
||||||
@@ -378,7 +385,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
setTasks(data);
|
setTasks(data);
|
||||||
if (!data.some((task) => ['pending', 'running', 'planned'].includes(task.status))) {
|
if (!data.some((task) => ['pending', 'running', 'planned'].includes(task.status))) {
|
||||||
// 批次已结束时清理 busy 标记,避免 onResult 丢失导致永久锁住
|
// 批次已结束时清理 busy 标记,避免 onResult 丢失导致永久锁住
|
||||||
setRunningBatchId((current) => (current ? '' : current));
|
setRunningBatchIds(new Set());
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// 轮询失败不打扰操作
|
// 轮询失败不打扰操作
|
||||||
@@ -426,42 +433,73 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
[visibleTasks],
|
[visibleTasks],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 正在执行任务的账号集合,用于按账号维度禁用,避免同一账号并发跑不同类型任务
|
||||||
|
const runningAccountIds = useMemo(() => {
|
||||||
|
const set = new Set<number>();
|
||||||
|
for (const task of visibleTasks) {
|
||||||
|
if (['pending', 'running', 'planned'].includes(task.status)) {
|
||||||
|
set.add(task.account_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}, [visibleTasks]);
|
||||||
|
|
||||||
|
const selectedHasRunning = selectedIds.some((id) => runningAccountIds.has(id));
|
||||||
|
|
||||||
// 有活跃任务或 WS 连接时 3 秒刷 tasks;空闲 15 秒轻量刷新
|
// 有活跃任务或 WS 连接时 3 秒刷 tasks;空闲 15 秒轻量刷新
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const intervalMs = hasActiveTasks || logs.connected || runningBatchId ? 3000 : 15000;
|
const intervalMs = hasActiveTasks || logs.connected || runningBatchIds.size > 0 ? 3000 : 15000;
|
||||||
const timer = setInterval(loadTasks, intervalMs);
|
const timer = setInterval(loadTasks, intervalMs);
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [hasActiveTasks, loadTasks, logs.connected, runningBatchId]);
|
}, [hasActiveTasks, loadTasks, logs.connected, runningBatchIds]);
|
||||||
|
|
||||||
const openQrTask = useCallback((task: DouyuTaskItem) => {
|
const openQrTask = useCallback((task: DouyuTaskItem, activate = true) => {
|
||||||
autoOpenedQrTaskIds.current.add(task.id);
|
autoOpenedQrTaskIds.current.add(task.id);
|
||||||
|
setQrTaskIds((prev) => (prev.includes(task.id) ? prev : [...prev, task.id]));
|
||||||
|
if (activate) setActiveQrTaskId(task.id);
|
||||||
|
// 绑定二维码与支付/电竞面板互斥,避免两个 Modal 叠加混淆
|
||||||
setEsportsBindTask(null);
|
setEsportsBindTask(null);
|
||||||
setPayTask(null);
|
setPayTask(null);
|
||||||
setQrTask(task);
|
}, []);
|
||||||
|
|
||||||
|
const closeQrTask = useCallback((taskId: number) => {
|
||||||
|
setQrTaskIds((prev) => {
|
||||||
|
const next = prev.filter((id) => id !== taskId);
|
||||||
|
setActiveQrTaskId((cur) => (cur === taskId ? (next.length > 0 ? next[next.length - 1] : null) : cur));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeAllQrTasks = useCallback(() => {
|
||||||
|
setQrTaskIds([]);
|
||||||
|
setActiveQrTaskId(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const openEsportsBindTask = useCallback((task: DouyuTaskItem) => {
|
const openEsportsBindTask = useCallback((task: DouyuTaskItem) => {
|
||||||
autoOpenedEsportsBindTaskIds.current.add(task.id);
|
autoOpenedEsportsBindTaskIds.current.add(task.id);
|
||||||
setQrTask(null);
|
closeAllQrTasks();
|
||||||
setPayTask(null);
|
setPayTask(null);
|
||||||
setEsportsBindTask(task);
|
setEsportsBindTask(task);
|
||||||
}, []);
|
}, [closeAllQrTasks]);
|
||||||
|
|
||||||
const openPayTask = useCallback((task: DouyuTaskItem) => {
|
const openPayTask = useCallback((task: DouyuTaskItem) => {
|
||||||
autoOpenedPayTaskIds.current.add(task.id);
|
autoOpenedPayTaskIds.current.add(task.id);
|
||||||
setQrTask(null);
|
closeAllQrTasks();
|
||||||
setEsportsBindTask(null);
|
setEsportsBindTask(null);
|
||||||
setPayTask(task);
|
setPayTask(task);
|
||||||
}, []);
|
}, [closeAllQrTasks]);
|
||||||
|
|
||||||
// 自动弹出最新绑定二维码(running 有 url 即可)
|
// 自动弹出最新绑定二维码(running 有 url 即可);多账号并行,不互斥
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!autoOpenQrReady.current || qrTask) return;
|
if (!autoOpenQrReady.current) return;
|
||||||
const nextQrTask = visibleTasks
|
const candidates = visibleTasks
|
||||||
.filter((task) => hasBindQrcode(task) && !autoOpenedQrTaskIds.current.has(task.id))
|
.filter((task) => hasBindQrcode(task) && !autoOpenedQrTaskIds.current.has(task.id))
|
||||||
.sort((a, b) => b.id - a.id)[0];
|
.sort((a, b) => b.id - a.id);
|
||||||
if (nextQrTask) openQrTask(nextQrTask);
|
if (candidates.length === 0) return;
|
||||||
}, [openQrTask, qrTask, visibleTasks]);
|
// 全部加入标签栏但不抢焦点;仅在当前无 active 标签时激活最新(id 最大)的
|
||||||
|
for (const candidate of candidates) openQrTask(candidate, false);
|
||||||
|
setActiveQrTaskId((cur) => cur ?? candidates[0].id);
|
||||||
|
}, [openQrTask, visibleTasks]);
|
||||||
|
|
||||||
// 电竞手册先查状态再打开独立绑定面板,不把切换角色二维码当作主流程。
|
// 电竞手册先查状态再打开独立绑定面板,不把切换角色二维码当作主流程。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -485,12 +523,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
if (nextPayTask) openPayTask(nextPayTask);
|
if (nextPayTask) openPayTask(nextPayTask);
|
||||||
}, [openPayTask, payTask, visibleTasks]);
|
}, [openPayTask, payTask, visibleTasks]);
|
||||||
|
|
||||||
// 弹窗跟随最新 task 状态(后端 progress 写入后 UI 自动更新)
|
// qrTask 已由 useMemo 从 tasks 派生,切换 Tab / 后端 progress 写入后自动更新,无需手动跟随
|
||||||
useEffect(() => {
|
|
||||||
if (!qrTask) return;
|
|
||||||
const latest = tasks.find((task) => task.id === qrTask.id);
|
|
||||||
if (latest && latest !== qrTask) setQrTask(latest);
|
|
||||||
}, [qrTask, tasks]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!esportsBindTask) return;
|
if (!esportsBindTask) return;
|
||||||
@@ -509,7 +542,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
if (latest !== payTask) setPayTask(latest);
|
if (latest !== payTask) setPayTask(latest);
|
||||||
}, [payTask, tasks]);
|
}, [payTask, tasks]);
|
||||||
|
|
||||||
const batchBusy = Boolean(runningBatchId || logs.connected || hasActiveTasks);
|
// 多批次并发:不再用全局 busy 标志阻塞 UI,刷新频率直接用各子条件判断
|
||||||
|
|
||||||
const startTask = async (
|
const startTask = async (
|
||||||
taskType: string,
|
taskType: string,
|
||||||
@@ -521,19 +554,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
message.warning('请先选择账号');
|
message.warning('请先选择账号');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 绑定确认/查询允许在其他账号仍轮询时继续,避免卡在多账号批次里
|
// 多批次并发:不再拦截,允许在已有批次运行时继续创建新批次
|
||||||
const allowDuringBatch = [
|
|
||||||
'confirm_bind',
|
|
||||||
'confirm_esports_bind',
|
|
||||||
'query_game_name',
|
|
||||||
'query_esports_game_name',
|
|
||||||
'prepare_esports_bind',
|
|
||||||
'get_esports_bind_qr',
|
|
||||||
].includes(taskType);
|
|
||||||
if (batchBusy && !allowDuringBatch) {
|
|
||||||
message.warning('当前已有批次在运行,请先停止或等待结束');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (['exchange_goods', 'exchange_esports_goods'].includes(taskType) && !selectedGoodsId) {
|
if (['exchange_goods', 'exchange_esports_goods'].includes(taskType) && !selectedGoodsId) {
|
||||||
message.warning('请先选择兑换商品');
|
message.warning('请先选择兑换商品');
|
||||||
return;
|
return;
|
||||||
@@ -551,11 +572,15 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
concurrency,
|
concurrency,
|
||||||
payload,
|
payload,
|
||||||
});
|
});
|
||||||
setRunningBatchId(result.batch_id);
|
setRunningBatchIds((prev) => new Set(prev).add(result.batch_id));
|
||||||
logs.connect(`/api/douyu/ws/${result.batch_id}`, {
|
logs.connectBatch(result.batch_id, `/api/douyu/ws/${result.batch_id}`, {
|
||||||
clear: true,
|
clear: true,
|
||||||
onResult: () => {
|
onResult: () => {
|
||||||
setRunningBatchId('');
|
setRunningBatchIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(result.batch_id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
void loadData();
|
void loadData();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -567,9 +592,9 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
};
|
};
|
||||||
|
|
||||||
const stopBatch = async () => {
|
const stopBatch = async () => {
|
||||||
if (!runningBatchId) return;
|
if (runningBatchIds.size === 0) return;
|
||||||
try {
|
try {
|
||||||
await douyuApi.stopBatch(runningBatchId);
|
await Promise.all(Array.from(runningBatchIds).map((id) => douyuApi.stopBatch(id)));
|
||||||
message.success('已停止');
|
message.success('已停止');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(getErrorMessage(error));
|
message.error(getErrorMessage(error));
|
||||||
@@ -805,7 +830,8 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const accountId = qrTask.account_id;
|
const accountId = qrTask.account_id;
|
||||||
setQrTask(null);
|
const taskId = qrTask.id;
|
||||||
|
closeQrTask(taskId);
|
||||||
void startTask('confirm_bind', [accountId]);
|
void startTask('confirm_bind', [accountId]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1070,9 +1096,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
const selectedText = selectedIds.length > 0 ? `已选 ${selectedIds.length} 个账号` : '请先勾选账号';
|
const selectedText = selectedIds.length > 0 ? `已选 ${selectedIds.length} 个账号` : '请先勾选账号';
|
||||||
const actionByKey = useMemo(() => new Map(quickActions.map((item) => [item.key, item])), [quickActions]);
|
const actionByKey = useMemo(() => new Map(quickActions.map((item) => [item.key, item])), [quickActions]);
|
||||||
const actionDisabled = (taskType: string) => {
|
const actionDisabled = (taskType: string) => {
|
||||||
if (batchBusy || selectedIds.length === 0) return true;
|
if (selectedIds.length === 0) return true;
|
||||||
if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) return !selectedGoodsId;
|
if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) return !selectedGoodsId;
|
||||||
return false;
|
// 选中账号中有正在执行的任务时禁用,避免同账号并发不同类型任务(并发写状态字段)
|
||||||
|
return selectedHasRunning;
|
||||||
};
|
};
|
||||||
const renderActionButton = (taskType: string, type: 'default' | 'primary' = 'default') => {
|
const renderActionButton = (taskType: string, type: 'default' | 'primary' = 'default') => {
|
||||||
const item = actionByKey.get(taskType);
|
const item = actionByKey.get(taskType);
|
||||||
@@ -1184,7 +1211,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
<Space>
|
<Space>
|
||||||
<Button icon={<ReloadOutlined />} onClick={loadData} loading={loading}>刷新</Button>
|
<Button icon={<ReloadOutlined />} onClick={loadData} loading={loading}>刷新</Button>
|
||||||
{canConfig && <Button icon={<SettingOutlined />} onClick={() => setConfigOpen(true)}>配置</Button>}
|
{canConfig && <Button icon={<SettingOutlined />} onClick={() => setConfigOpen(true)}>配置</Button>}
|
||||||
{runningBatchId && <Button danger icon={<StopOutlined />} onClick={stopBatch}>停止</Button>}
|
{runningBatchIds.size > 0 && <Button danger icon={<StopOutlined />} onClick={stopBatch}>停止</Button>}
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
@@ -1456,7 +1483,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
key={item.key} block size="small" type="text"
|
key={item.key} block size="small" type="text"
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
onClick={() => runContextAction(item.key)}
|
onClick={() => runContextAction(item.key)}
|
||||||
disabled={batchBusy}
|
disabled={contextMenu.accountIds.some((id) => runningAccountIds.has(id))}
|
||||||
style={{ justifyContent: 'flex-start' }}
|
style={{ justifyContent: 'flex-start' }}
|
||||||
>
|
>
|
||||||
{taskTypes[item.key] || item.key}
|
{taskTypes[item.key] || item.key}
|
||||||
@@ -1657,15 +1684,39 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<QrcodeOutlined />
|
<QrcodeOutlined />
|
||||||
<span>{qrBindReady ? '已识别新角色' : qrStatusText}</span>
|
<span>
|
||||||
|
{qrBindReady ? '已识别新角色' : qrStatusText}
|
||||||
|
{qrTaskIds.length > 1 && ` (${qrTaskIds.length})`}
|
||||||
|
</span>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
open={!!qrTask}
|
open={qrTaskIds.length > 0}
|
||||||
onCancel={() => setQrTask(null)}
|
onCancel={() => closeAllQrTasks()}
|
||||||
footer={null}
|
footer={null}
|
||||||
centered
|
centered
|
||||||
width={420}
|
width={420}
|
||||||
>
|
>
|
||||||
|
{qrTaskIds.length > 1 && (
|
||||||
|
<Space size={4} wrap style={{ marginBottom: 12, justifyContent: 'center' }}>
|
||||||
|
{qrTaskIds.map((id) => {
|
||||||
|
const t = tasks.find((x) => x.id === id);
|
||||||
|
if (!t) return null;
|
||||||
|
const name = t.account_nickname || t.account_username || t.account_uid || `#${t.account_id}`;
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
key={id}
|
||||||
|
color={id === activeQrTaskId ? 'blue' : 'default'}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
closable
|
||||||
|
onClose={(e) => { e.preventDefault(); closeQrTask(id); }}
|
||||||
|
onClick={() => setActiveQrTaskId(id)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
{qrTask && (
|
{qrTask && (
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
{qrAccountName && (
|
{qrAccountName && (
|
||||||
|
|||||||
Reference in New Issue
Block a user