feat(cookies): Cookie 失效后可账号重新登录替换, 并修复 Cookie 列空白过多

This commit is contained in:
yml2213
2026-08-08 01:39:02 +08:00
parent 64b017fd6a
commit 39ea0d2184
5 changed files with 269 additions and 60 deletions
+5 -1
View File
@@ -1,5 +1,5 @@
import api from './client';
import type { BasicSummary, CookieCheckResult, CookieItem, PageParams, PaginatedResponse, MessageDeletedResponse, MessageResponse } from './types';
import type { BasicSummary, CookieCheckResult, CookieItem, LoginTaskItem, PageParams, PaginatedResponse, MessageDeletedResponse, MessageResponse } from './types';
export const cookieApi = {
list: () => api.get<CookieItem[], CookieItem[]>('/cookies'),
@@ -10,6 +10,10 @@ export const cookieApi = {
exportCsv: (format?: string) => api.get<Blob, Blob>('/cookies/export', { responseType: 'blob', params: format ? { format } : {} }),
check: (ids: number[]) =>
api.post<{ results: CookieCheckResult[] }, { results: CookieCheckResult[] }>('/cookies/check', null, { params: { ids: ids.join(',') } }),
relogin: (ids: number[]) =>
api.post<{ batch_id: string; count: number; skipped: number; success: boolean }, { batch_id: string; count: number; skipped: number; success: boolean }>('/cookies/relogin', { ids }),
loginTasks: (batchId: string) =>
api.get<LoginTaskItem[], LoginTaskItem[]>('/login/tasks', { params: { batch_id: batchId } }),
delete: (id: number) => api.delete<MessageResponse, MessageResponse>(`/cookies/${id}`),
deleteBatch: (ids: number[]) => api.delete<MessageDeletedResponse, MessageDeletedResponse>('/cookies/batch', { params: { task_ids: ids.join(',') } }),
};
+86 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useState, useCallback } from 'react';
import { Table, Button, Card, Row, Col, Statistic, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown, Tooltip } from 'antd';
import { message } from '../utils/antdMessage';
import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined, LoadingOutlined } from '@ant-design/icons';
import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined, LoadingOutlined, ReloadOutlined } from '@ant-design/icons';
import { cookieApi, type BasicSummary, type CookieCheckResult, type CookieItem } from '../api/modules';
import { usePermissions } from '../hooks/usePermissions';
import { formatTime } from '../utils/time';
@@ -52,10 +52,64 @@ export default function CookiePage() {
const [currentPage, setCurrentPage] = useState(1);
const [checkingIds, setCheckingIds] = useState<Set<number>>(new Set());
const [checkResults, setCheckResults] = useState<Map<number, CookieCheckResult>>(new Map());
const [reloginIds, setReloginIds] = useState<Set<number>>(new Set());
const { can } = usePermissions();
const canView = can('cookie:view');
const canExport = can('cookie:export');
const canRelogin = can('login:batch');
const pollRelogin = (batchId: string, count: number) => {
let attempts = 0;
const timer = window.setInterval(async () => {
attempts += 1;
try {
const tasks = await cookieApi.loginTasks(batchId);
const done = tasks.every((t) => !['pending', 'running'].includes(t.status));
if (done) {
window.clearInterval(timer);
message.success(`重新登录完成: ${tasks.filter((t) => t.status === 'success').length}/${tasks.length} 个成功`);
loadCookies();
loadSummary();
} else if (attempts > 120) {
window.clearInterval(timer);
message.warning('重新登录超时,请到登录任务页查看进度');
}
} catch {
window.clearInterval(timer);
message.warning('查询重登进度失败,请稍后手动刷新');
}
}, 3000);
void count;
};
const handleRelogin = async (ids: number[]) => {
if (ids.length === 0) return;
setReloginIds((prev) => new Set([...prev, ...ids]));
try {
const res = await cookieApi.relogin(ids);
const tip = res.skipped > 0 ? `${res.skipped} 条因账号缺少登录凭据被跳过` : '';
message.success(`已开始重新登录 ${res.count} 个账号${tip}`);
pollRelogin(res.batch_id, res.count);
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setReloginIds((prev) => {
const next = new Set(prev);
for (const id of ids) next.delete(id);
return next;
});
}
};
const handleReloginSelected = () => {
const ids = selectedRowKeys.map((k) => Number(k));
if (ids.length === 0) {
message.warning('请先选择要重新登录的 Cookie');
return;
}
void handleRelogin(ids);
};
const handleCheck = async (ids: number[]) => {
if (ids.length === 0) return;
@@ -234,6 +288,7 @@ export default function CookiePage() {
{
title: 'Cookie',
dataIndex: 'cookie_preview',
width: 300,
ellipsis: true,
render: (val: string) => {
if (!canView) return <Tag>***</Tag>;
@@ -306,6 +361,21 @@ export default function CookiePage() {
</Button>
)}
{canRelogin && (
<Popconfirm
title="用账号信息重新登录并替换失效 Cookie?"
onConfirm={() => handleRelogin([record.id])}
>
<Button
size="small"
icon={<ReloadOutlined />}
loading={reloginIds.has(record.id)}
disabled={reloginIds.has(record.id)}
>
</Button>
</Popconfirm>
)}
{canExport && (
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button danger size="small" icon={<DeleteOutlined />} />
@@ -337,6 +407,20 @@ export default function CookiePage() {
{selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
</Button>
)}
{canRelogin && (
<Popconfirm
title={`确认重新登录选中的 ${selectedRowKeys.length} 条 Cookie?将用账号信息重新登录并替换`}
onConfirm={handleReloginSelected}
disabled={selectedRowKeys.length === 0}
>
<Button
icon={<ReloadOutlined />}
disabled={selectedRowKeys.length === 0}
>
{selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
</Button>
</Popconfirm>
)}
{canExport && (
<Popconfirm
title={`确认删除选中的 ${selectedRowKeys.length} 条 Cookie`}
@@ -431,7 +515,7 @@ export default function CookiePage() {
}
},
}}
scroll={{ x: 900 }}
scroll={{ x: 1210 }}
/>
</div>
);