feat: add scoped cookie operations for support
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, Input, Popconfirm, Space, Table, Tag, Typography } from 'antd';
|
||||
import { message } from '../utils/antdMessage';
|
||||
import { LoadingOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { cookieApi, type CookieOperationCheckResult, type CookieOperationItem } from '../api/modules';
|
||||
import { formatTime } from '../utils/time';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function CookieOperationsPage() {
|
||||
const [items, setItems] = useState<CookieOperationItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [checkingIds, setCheckingIds] = useState<Set<number>>(new Set());
|
||||
const [reloginIds, setReloginIds] = useState<Set<number>>(new Set());
|
||||
const [checkResults, setCheckResults] = useState<Map<number, CookieOperationCheckResult>>(new Map());
|
||||
const [total, setTotal] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(() => {
|
||||
const value = localStorage.getItem('cookie_operations_page_size');
|
||||
return value ? Number(value) || 20 : 20;
|
||||
});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const loadItems = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await cookieApi.listOperations({
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
search: search.trim() || undefined,
|
||||
});
|
||||
setItems(data.items);
|
||||
setTotal(data.total);
|
||||
setCheckResults((previous) => {
|
||||
const next = new Map(previous);
|
||||
for (const item of data.items) {
|
||||
if (!item.ck_check_status) continue;
|
||||
next.set(item.id, {
|
||||
id: item.id,
|
||||
valid: item.ck_check_status === 'valid',
|
||||
checked_at: item.ck_checked_at ?? '',
|
||||
});
|
||||
}
|
||||
return next;
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
message.error(getErrorMessage(error));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [currentPage, pageSize, search]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadItems();
|
||||
}, [loadItems]);
|
||||
|
||||
const handleCheck = async (ids: number[]) => {
|
||||
if (ids.length === 0) return;
|
||||
setCheckingIds((previous) => new Set([...previous, ...ids]));
|
||||
try {
|
||||
const response = await cookieApi.checkOperations(ids);
|
||||
setCheckResults((previous) => {
|
||||
const next = new Map(previous);
|
||||
response.results.forEach((item) => next.set(item.id, item));
|
||||
return next;
|
||||
});
|
||||
message.success(`检测完成:${response.results.filter((item) => item.valid).length}/${response.results.length} 条有效`);
|
||||
void loadItems();
|
||||
} catch (error: unknown) {
|
||||
message.error(getErrorMessage(error));
|
||||
} finally {
|
||||
setCheckingIds((previous) => {
|
||||
const next = new Set(previous);
|
||||
ids.forEach((id) => next.delete(id));
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRelogin = async (ids: number[]) => {
|
||||
if (ids.length === 0) return;
|
||||
setReloginIds((previous) => new Set([...previous, ...ids]));
|
||||
try {
|
||||
const response = await cookieApi.reloginOperations(ids);
|
||||
const skipped = response.skipped ? `,${response.skipped} 条因缺少登录凭据跳过` : '';
|
||||
message.success(`已开始重登 ${response.count} 个账号${skipped}`);
|
||||
setSelectedRowKeys([]);
|
||||
window.setTimeout(() => void loadItems(), 1000);
|
||||
} catch (error: unknown) {
|
||||
message.error(getErrorMessage(error));
|
||||
} finally {
|
||||
setReloginIds((previous) => {
|
||||
const next = new Set(previous);
|
||||
ids.forEach((id) => next.delete(id));
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resultFor = (item: CookieOperationItem) => checkResults.get(item.id);
|
||||
const selectedIds = selectedRowKeys.map((key) => Number(key));
|
||||
const columns = [
|
||||
{ title: '账号', dataIndex: 'account_username', ellipsis: true },
|
||||
{
|
||||
title: '有效性',
|
||||
width: 110,
|
||||
render: (_: unknown, item: CookieOperationItem) => {
|
||||
if (checkingIds.has(item.id)) return <Tag color="processing" icon={<LoadingOutlined />}>检测中</Tag>;
|
||||
const result = resultFor(item);
|
||||
if (!result) return <Text type="secondary">未检测</Text>;
|
||||
return <Tag color={result.valid ? 'success' : 'error'}>{result.valid ? '有效' : '无效'}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '检测时间',
|
||||
width: 180,
|
||||
render: (_: unknown, item: CookieOperationItem) => {
|
||||
const checkedAt = resultFor(item)?.checked_at || item.ck_checked_at;
|
||||
return checkedAt ? formatTime(checkedAt) : <Text type="secondary">-</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 190,
|
||||
render: (_: unknown, item: CookieOperationItem) => (
|
||||
<Space size={4}>
|
||||
<Button
|
||||
size="small"
|
||||
loading={checkingIds.has(item.id)}
|
||||
disabled={checkingIds.has(item.id)}
|
||||
onClick={() => void handleCheck([item.id])}
|
||||
>
|
||||
检测
|
||||
</Button>
|
||||
<Popconfirm title="将使用已保存的账号信息重新登录并替换 CK,确认继续?" onConfirm={() => void handleRelogin([item.id])}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={reloginIds.has(item.id)}
|
||||
disabled={reloginIds.has(item.id)}
|
||||
>
|
||||
重登
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
||||
<h2 style={{ margin: 0 }}>CK 检测与重登</h2>
|
||||
<Space wrap>
|
||||
<Button loading={checkingIds.size > 0} disabled={selectedIds.length === 0} onClick={() => void handleCheck(selectedIds)}>
|
||||
检测选中 {selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={`将重新登录选中的 ${selectedIds.length} 个账号并替换 CK,确认继续?`}
|
||||
disabled={selectedIds.length === 0}
|
||||
onConfirm={() => void handleRelogin(selectedIds)}
|
||||
>
|
||||
<Button icon={<ReloadOutlined />} disabled={selectedIds.length === 0}>
|
||||
重登选中 {selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</div>
|
||||
<Input.Search
|
||||
allowClear
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="搜索账号"
|
||||
style={{ width: 280, marginBottom: 12 }}
|
||||
value={search}
|
||||
onChange={(event) => {
|
||||
setSearch(event.target.value);
|
||||
setCurrentPage(1);
|
||||
setSelectedRowKeys([]);
|
||||
}}
|
||||
/>
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys, preserveSelectedRowKeys: true }}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (count) => `共 ${count} 条`,
|
||||
onChange: (page, size) => {
|
||||
setCurrentPage(size === pageSize ? page : 1);
|
||||
if (size !== pageSize) {
|
||||
setPageSize(size);
|
||||
localStorage.setItem('cookie_operations_page_size', String(size));
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user