优化列表分页和数据加载

This commit is contained in:
yml2213
2026-07-24 13:25:27 +08:00
parent 6840728da6
commit f91c05c108
17 changed files with 647 additions and 161 deletions
+7 -1
View File
@@ -2,14 +2,20 @@ import api from './client';
import type {
AccountItem,
AssignmentsSummary,
BasicSummary,
MessageCountResponse,
MessageDeletedResponse,
MessageResponse,
PageParams,
PaginatedResponse,
} from './types';
export const accountApi = {
list: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean }) =>
list: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_sensitive?: boolean }) =>
api.get<AccountItem[], AccountItem[]>('/accounts', { params }),
listPaged: (params: PageParams & { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_sensitive?: boolean }) =>
api.get<PaginatedResponse<AccountItem>, PaginatedResponse<AccountItem>>('/accounts', { params }),
summary: () => api.get<BasicSummary, BasicSummary>('/accounts/summary'),
import: (text: string) => api.post<MessageCountResponse, MessageCountResponse>('/accounts/import', { text }),
assign: (id: number, assigned_to: number | null) =>
api.put<MessageResponse, MessageResponse>(`/accounts/${id}/assign`, { assigned_to }),
+5 -1
View File
@@ -1,8 +1,12 @@
import api from './client';
import type { CookieItem, MessageDeletedResponse, MessageResponse } from './types';
import type { BasicSummary, CookieItem, PageParams, PaginatedResponse, MessageDeletedResponse, MessageResponse } from './types';
export const cookieApi = {
list: () => api.get<CookieItem[], CookieItem[]>('/cookies'),
listPaged: (params: PageParams & { include_cookie?: boolean }) =>
api.get<PaginatedResponse<CookieItem>, PaginatedResponse<CookieItem>>('/cookies', { params }),
summary: () => api.get<BasicSummary, BasicSummary>('/cookies/summary'),
get: (id: number) => api.get<CookieItem, CookieItem>(`/cookies/${id}`),
exportCsv: (format?: string) => api.get<Blob, Blob>('/cookies/export', { responseType: 'blob', params: format ? { format } : {} }),
delete: (id: number) => api.delete<MessageResponse, MessageResponse>(`/cookies/${id}`),
deleteBatch: (ids: number[]) => api.delete<MessageDeletedResponse, MessageDeletedResponse>('/cookies/batch', { params: { task_ids: ids.join(',') } }),
+12 -1
View File
@@ -1,6 +1,8 @@
import api from './client';
import type {
BasicSummary,
HuyaAccountItem,
HuyaAccountSummary,
HuyaAutoRegisterBatch,
HuyaAutoRegisterRequest,
HuyaAutoRegisterRetryRequest,
@@ -25,12 +27,17 @@ import type {
MessageCountResponse,
MessageDeletedResponse,
MessageResponse,
PageParams,
PaginatedResponse,
} from './types';
export const huyaApi = {
taskTypes: () => api.get<Record<string, string>, Record<string, string>>('/huya/task-types'),
listAccounts: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean }) =>
listAccounts: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_cookie?: boolean }) =>
api.get<HuyaAccountItem[], HuyaAccountItem[]>('/huya/accounts', { params }),
listAccountsPaged: (params: PageParams & { assigned_only?: boolean; tag?: string; has_cookie?: boolean; include_cookie?: boolean }) =>
api.get<PaginatedResponse<HuyaAccountItem>, PaginatedResponse<HuyaAccountItem>>('/huya/accounts', { params }),
accountsSummary: () => api.get<HuyaAccountSummary, HuyaAccountSummary>('/huya/accounts/summary'),
importCookies: (text: string, tag: string = '') =>
api.post<HuyaCookieImportResult, HuyaCookieImportResult>('/huya/accounts/import-cookies', { text, tag }),
importPasswordAccounts: (text: string, tag: string = '') =>
@@ -76,6 +83,10 @@ export const huyaApi = {
deleteAccounts: (accountIds: number[]) =>
api.delete<MessageDeletedResponse, MessageDeletedResponse>('/huya/accounts/batch', { params: { account_ids: accountIds.join(',') } }),
listCookies: () => api.get<HuyaCookieItem[], HuyaCookieItem[]>('/huya/cookies'),
listCookiesPaged: (params: PageParams & { include_cookie?: boolean }) =>
api.get<PaginatedResponse<HuyaCookieItem>, PaginatedResponse<HuyaCookieItem>>('/huya/cookies', { params }),
cookiesSummary: () => api.get<BasicSummary, BasicSummary>('/huya/cookies/summary'),
getCookie: (id: number) => api.get<HuyaCookieItem, HuyaCookieItem>(`/huya/cookies/${id}`),
exportCookies: (format?: string) => api.get<Blob, Blob>('/huya/cookies/export', { responseType: 'blob', params: format ? { format } : {} }),
deleteCookie: (id: number) => api.delete<MessageResponse, MessageResponse>(`/huya/cookies/${id}`),
deleteCookies: (accountIds: number[]) =>
+26
View File
@@ -17,6 +17,26 @@ export interface AppInfo {
version: string;
}
export interface PageParams {
page: number;
page_size: number;
search?: string;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
page_size: number;
}
export interface BasicSummary {
total: number;
assigned_count: number;
unassigned_count: number;
tag_count?: number;
}
// ==================== Auth ====================
export interface LoginResult {
@@ -172,6 +192,12 @@ export interface HuyaAccountItem {
updated_at: string | null;
}
export interface HuyaAccountSummary extends BasicSummary {
password_ready_count: number;
point_count: number;
bound_count: number;
}
export interface HuyaCookieImportResult extends MessageCountResponse {
skipped: number;
}
+4 -2
View File
@@ -12,6 +12,8 @@ interface ConnectOptions {
onResult?: () => void;
}
const MAX_LOGS = 1000;
function toWebSocketUrl(pathOrUrl: string): string {
if (pathOrUrl.startsWith('ws://') || pathOrUrl.startsWith('wss://')) {
return pathOrUrl;
@@ -63,9 +65,9 @@ export function useWebSocketLogs() {
callbacksRef.current.onResult?.();
return;
}
setLogs((prev) => [...prev, msg]);
setLogs((prev) => [...prev, msg].slice(-MAX_LOGS));
} catch {
setLogs((prev) => [...prev, { level: 'info', message: String(event.data) }]);
setLogs((prev) => [...prev, { level: 'info', message: String(event.data) }].slice(-MAX_LOGS));
}
};
+53 -10
View File
@@ -6,7 +6,7 @@ import {
import { message } from '../utils/antdMessage';
import type { TableProps } from 'antd';
import { ImportOutlined, DeleteOutlined, TagOutlined, FilterOutlined } from '@ant-design/icons';
import { accountApi, userApi, type AccountItem, type UserInfo } from '../api/modules';
import { accountApi, userApi, type AccountItem, type BasicSummary, type UserInfo } from '../api/modules';
import { usePermissions } from '../hooks/usePermissions';
import { formatTime } from '../utils/time';
import { getErrorMessage } from '../utils/error';
@@ -27,9 +27,12 @@ export default function AccountsPage() {
const [importText, setImportText] = useState('');
const [importing, setImporting] = useState(false);
const [tagFilter, setTagFilter] = useState<string>('');
const [searchText, setSearchText] = useState('');
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [batchTagInput, setBatchTagInput] = useState('');
const [batchTagVisible, setBatchTagVisible] = useState(false);
const [total, setTotal] = useState(0);
const [summary, setSummary] = useState<BasicSummary>({ total: 0, assigned_count: 0, unassigned_count: 0, tag_count: 0 });
const [pageSize, setPageSize] = useState(() => {
const v = localStorage.getItem('account_page_size');
return v ? Number(v) || 20 : 20;
@@ -47,14 +50,30 @@ export default function AccountsPage() {
try {
const params: { tag?: string } = {};
if (tagFilter) params.tag = tagFilter;
const data = await accountApi.list(params);
setAccounts(data);
const data = await accountApi.listPaged({
...params,
page: currentPage,
page_size: pageSize,
search: searchText.trim() || undefined,
include_sensitive: canViewFull,
});
setAccounts(data.items);
setTotal(data.total);
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setLoading(false);
}
}, [tagFilter]);
}, [canViewFull, currentPage, pageSize, searchText, tagFilter]);
const loadSummary = useCallback(async () => {
try {
const data = await accountApi.summary();
setSummary(data);
} catch {
// 统计加载失败时不影响主列表操作。
}
}, []);
const loadUsers = useCallback(async () => {
try {
@@ -76,9 +95,13 @@ export default function AccountsPage() {
useEffect(() => {
loadAccounts();
}, [loadAccounts]);
useEffect(() => {
if (canAssign) loadUsers();
loadTags();
}, [loadAccounts, canAssign, loadUsers, loadTags]);
loadSummary();
}, [canAssign, loadUsers, loadTags, loadSummary]);
const tagColorMap = useMemo(() => {
const map: Record<string, string> = {};
@@ -101,6 +124,7 @@ export default function AccountsPage() {
setImportText('');
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
@@ -142,6 +166,7 @@ export default function AccountsPage() {
setSelectedRowKeys([]);
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -154,6 +179,7 @@ export default function AccountsPage() {
setSelectedRowKeys((prev) => prev.filter((k) => k !== id));
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -170,6 +196,7 @@ export default function AccountsPage() {
setSelectedRowKeys([]);
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -279,10 +306,24 @@ export default function AccountsPage() {
placeholder="按标签筛选"
style={{ width: 150 }}
value={tagFilter || undefined}
onChange={(val) => setTagFilter(val || '')}
onChange={(val) => {
setTagFilter(val || '');
setCurrentPage(1);
}}
options={tags.map((t) => ({ value: t, label: t }))}
prefix={<FilterOutlined />}
/>
<Input.Search
allowClear
size="small"
placeholder="搜索账号/标签/备注"
style={{ width: 220 }}
value={searchText}
onChange={(e) => {
setSearchText(e.target.value);
setCurrentPage(1);
}}
/>
{canImport && (
<>
<Button
@@ -323,16 +364,16 @@ export default function AccountsPage() {
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card size="small"><Statistic title="账号总数" value={accounts.length} /></Card>
<Card size="small"><Statistic title="账号总数" value={summary.total} /></Card>
</Col>
<Col span={6}>
<Card size="small"><Statistic title="标签数" value={tags.length} /></Card>
<Card size="small"><Statistic title="标签数" value={summary.tag_count || tags.length} /></Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="已分配"
value={accounts.filter((a) => a.assigned_to).length}
value={summary.assigned_count}
/>
</Card>
</Col>
@@ -340,7 +381,7 @@ export default function AccountsPage() {
<Card size="small">
<Statistic
title="未分配"
value={accounts.filter((a) => !a.assigned_to).length}
value={summary.unassigned_count}
/>
</Card>
</Col>
@@ -350,6 +391,7 @@ export default function AccountsPage() {
rowSelection={canImport ? {
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
preserveSelectedRowKeys: true,
} : undefined}
columns={columns}
dataSource={accounts}
@@ -359,6 +401,7 @@ export default function AccountsPage() {
pagination={{
current: currentPage,
pageSize,
total,
showSizeChanger: true,
showTotal: (t) => `${t}`,
onChange: (page, size) => {
+1 -1
View File
@@ -46,7 +46,7 @@ export default function AssignmentsPage() {
// 加载账号(仅已成功登录过的)
const loadAccounts = async () => {
try {
const all = await accountApi.list({ has_cookie: true });
const all = await accountApi.list({ has_cookie: true, include_sensitive: false });
setAccounts(all);
} catch (e: unknown) {
message.error(getErrorMessage(e));
+58 -40
View File
@@ -2,7 +2,7 @@ import { useEffect, useState, useCallback } from 'react';
import { Table, Button, Card, Row, Col, Statistic, Tag, Popconfirm, Space, Typography, Input, theme, Dropdown } from 'antd';
import { message } from '../utils/antdMessage';
import { DownloadOutlined, DeleteOutlined, CopyOutlined, SearchOutlined } from '@ant-design/icons';
import { cookieApi, type CookieItem } from '../api/modules';
import { cookieApi, type BasicSummary, type CookieItem } from '../api/modules';
import { usePermissions } from '../hooks/usePermissions';
import { formatTime } from '../utils/time';
import { getErrorMessage } from '../utils/error';
@@ -43,6 +43,8 @@ export default function CookiePage() {
const [loading, setLoading] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [searchText, setSearchText] = useState('');
const [total, setTotal] = useState(0);
const [summary, setSummary] = useState<BasicSummary>({ total: 0, assigned_count: 0, unassigned_count: 0 });
const [pageSize, setPageSize] = useState(() => {
const v = localStorage.getItem('cookie_page_size');
return v ? Number(v) || 20 : 20;
@@ -56,19 +58,38 @@ export default function CookiePage() {
const loadCookies = useCallback(async () => {
setLoading(true);
try {
const data = await cookieApi.list();
setCookies(data);
const data = await cookieApi.listPaged({
page: currentPage,
page_size: pageSize,
search: searchText.trim() || undefined,
include_cookie: false,
});
setCookies(data.items);
setTotal(data.total);
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setLoading(false);
}
}, [currentPage, pageSize, searchText]);
const loadSummary = useCallback(async () => {
try {
const data = await cookieApi.summary();
setSummary(data);
} catch {
// 统计加载失败不影响列表操作。
}
}, []);
useEffect(() => {
loadCookies();
}, [loadCookies]);
useEffect(() => {
loadSummary();
}, [loadSummary]);
const handleExport = async (format: string = 'csv') => {
try {
const blob = await cookieApi.exportCsv(format);
@@ -84,33 +105,36 @@ export default function CookiePage() {
}
};
const handleCopyCookie = (record: CookieItem) => {
if (!record.cookie) {
message.warning('Cookie 为空');
return;
const handleCopyCookie = async (record: CookieItem) => {
try {
const detail = await cookieApi.get(record.id);
if (!detail.cookie) {
message.warning('Cookie 为空');
return;
}
const text = `${detail.account_username}----${detail.account_password || ''}----${detail.cookie}`;
await copyToClipboard(text);
message.success(`已复制 ${detail.account_username} 的 Cookie`);
} catch (e: unknown) {
message.error(getErrorMessage(e) || '复制失败');
}
const text = `${record.account_username}----${record.account_password || ''}----${record.cookie}`;
copyToClipboard(text).then(() => {
message.success(`已复制 ${record.account_username} 的 Cookie`);
}).catch(() => {
message.error('复制失败');
});
};
const handleCopySelected = () => {
const handleCopySelected = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择 Cookie');
return;
}
const selected = cookies.filter((c) => selectedRowKeys.includes(c.id));
const text = selected
.map((c) => `${c.account_username}----${c.account_password || ''}----${c.cookie || ''}`)
.join('\r\n');
copyToClipboard(text).then(() => {
try {
const selected = await Promise.all(selectedRowKeys.map((key) => cookieApi.get(Number(key))));
const text = selected
.map((c) => `${c.account_username}----${c.account_password || ''}----${c.cookie || ''}`)
.join('\r\n');
await copyToClipboard(text);
message.success(`已复制 ${selected.length} 条 Cookie`);
}).catch(() => {
message.error('复制失败');
});
} catch (e: unknown) {
message.error(getErrorMessage(e) || '复制失败');
}
};
const handleDelete = async (id: number) => {
@@ -119,6 +143,7 @@ export default function CookiePage() {
message.success('已删除');
setSelectedRowKeys((prev) => prev.filter((k) => k !== id));
loadCookies();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -135,21 +160,12 @@ export default function CookiePage() {
message.success(res.message);
setSelectedRowKeys([]);
loadCookies();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
};
// 筛选
const filteredCookies = cookies.filter((c) => {
if (!searchText) return true;
const s = searchText.toLowerCase();
return (
c.account_username?.toLowerCase().includes(s) ||
c.assigned_username?.toLowerCase().includes(s)
);
});
const columns = [
{ title: 'ID', dataIndex: 'id', width: 60, align: 'center' as const },
{
@@ -206,9 +222,6 @@ export default function CookiePage() {
},
];
const assignedCount = cookies.filter((c) => c.assigned_to).length;
const unassignedCount = cookies.length - assignedCount;
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
@@ -254,13 +267,13 @@ export default function CookiePage() {
</div>
<Row gutter={12} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card size="small"><Statistic title="Cookie 总数" value={cookies.length} /></Card>
<Card size="small"><Statistic title="Cookie 总数" value={summary.total} /></Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="已分配"
value={assignedCount}
value={summary.assigned_count}
styles={{ content: { color: token.colorSuccess } }}
/>
</Card>
@@ -269,7 +282,7 @@ export default function CookiePage() {
<Card size="small">
<Statistic
title="未分配"
value={unassignedCount}
value={summary.unassigned_count}
styles={{ content: { color: token.colorError } }}
/>
</Card>
@@ -280,7 +293,10 @@ export default function CookiePage() {
placeholder="搜索账号或分配客服"
allowClear
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onChange={(e) => {
setSearchText(e.target.value);
setCurrentPage(1);
}}
style={{ width: 260 }}
size="small"
prefix={<SearchOutlined />}
@@ -290,15 +306,17 @@ export default function CookiePage() {
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
preserveSelectedRowKeys: true,
}}
columns={columns}
dataSource={filteredCookies}
dataSource={cookies}
rowKey="id"
loading={loading}
size="small"
pagination={{
current: currentPage,
pageSize,
total,
showSizeChanger: true,
showTotal: (t) => `${t}`,
onChange: (page, size) => {
+9 -9
View File
@@ -97,10 +97,10 @@ export default function DashboardPage() {
huyaGoodsResult,
huyaRechargeGoodsResult,
] = await Promise.allSettled([
canViewDouyuAccounts ? accountApi.list() : Promise.resolve([]),
canViewDouyuAccounts ? accountApi.summary() : Promise.resolve(null),
canViewDouyuTasks ? loginApi.listTasks() : Promise.resolve([]),
canViewCookies ? cookieApi.list() : Promise.resolve([]),
canViewHuyaAccounts ? huyaApi.listAccounts() : Promise.resolve([]),
canViewCookies ? cookieApi.summary() : Promise.resolve(null),
canViewHuyaAccounts ? huyaApi.accountsSummary() : Promise.resolve(null),
canViewHuyaTasks ? huyaApi.listTasks() : Promise.resolve([]),
canViewHuyaTasks ? huyaApi.listGoods() : Promise.resolve([]),
canViewHuyaTasks ? huyaApi.listRechargeGoods() : Promise.resolve([]),
@@ -108,24 +108,24 @@ export default function DashboardPage() {
if (ignore) return;
const douyuAccounts = douyuAccountsResult.status === 'fulfilled' ? douyuAccountsResult.value : [];
const douyuAccounts = douyuAccountsResult.status === 'fulfilled' ? douyuAccountsResult.value : null;
const douyuTasks = douyuTasksResult.status === 'fulfilled' ? douyuTasksResult.value : [];
const cookies = cookiesResult.status === 'fulfilled' ? cookiesResult.value : [];
const huyaAccounts = huyaAccountsResult.status === 'fulfilled' ? huyaAccountsResult.value : [];
const cookies = cookiesResult.status === 'fulfilled' ? cookiesResult.value : null;
const huyaAccounts = huyaAccountsResult.status === 'fulfilled' ? huyaAccountsResult.value : null;
const huyaTasks = huyaTasksResult.status === 'fulfilled' ? huyaTasksResult.value : [];
const huyaGoods = huyaGoodsResult.status === 'fulfilled' ? huyaGoodsResult.value : [];
const huyaRechargeGoods = huyaRechargeGoodsResult.status === 'fulfilled' ? huyaRechargeGoodsResult.value : [];
setStats({
douyu: {
accounts: douyuAccounts.length,
accounts: douyuAccounts?.total || 0,
tasks: douyuTasks.length,
success: douyuTasks.filter((task: LoginTaskItem) => task.status === 'success').length,
failed: countFailed(douyuTasks),
cookies: cookies.length,
cookies: cookies?.total || 0,
},
huya: {
accounts: huyaAccounts.length,
accounts: huyaAccounts?.total || 0,
tasks: huyaTasks.length,
success: huyaTasks.filter((task: HuyaTaskItem) => task.status === 'success').length,
failed: countFailed(huyaTasks),
+68 -34
View File
@@ -8,6 +8,7 @@ import { DeleteOutlined, FilterOutlined, ImportOutlined, LoginOutlined, MobileOu
import {
huyaApi,
type HuyaAccountItem,
type HuyaAccountSummary,
type HuyaPasswordLoginBatchItem,
type SupportUserItem,
} from '../api/modules';
@@ -68,6 +69,16 @@ export default function HuyaAccountsPage() {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [batchTagInput, setBatchTagInput] = useState('');
const [batchTagVisible, setBatchTagVisible] = useState(false);
const [total, setTotal] = useState(0);
const [summary, setSummary] = useState<HuyaAccountSummary>({
total: 0,
assigned_count: 0,
unassigned_count: 0,
tag_count: 0,
password_ready_count: 0,
point_count: 0,
bound_count: 0,
});
const [pageSize, setPageSize] = useState(() => {
const v = localStorage.getItem('huya_account_page_size');
return v ? Number(v) || 20 : 20;
@@ -86,14 +97,30 @@ export default function HuyaAccountsPage() {
try {
const params: { tag?: string } = {};
if (tagFilter) params.tag = tagFilter;
const data = await huyaApi.listAccounts(params);
setAccounts(data);
const data = await huyaApi.listAccountsPaged({
...params,
page: currentPage,
page_size: pageSize,
search: searchText.trim() || undefined,
include_cookie: false,
});
setAccounts(data.items);
setTotal(data.total);
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setLoading(false);
}
}, [tagFilter]);
}, [currentPage, pageSize, searchText, tagFilter]);
const loadSummary = useCallback(async () => {
try {
const data = await huyaApi.accountsSummary();
setSummary(data);
} catch {
// 统计加载失败时不影响主列表操作。
}
}, []);
const loadUsers = useCallback(async () => {
try {
@@ -115,9 +142,13 @@ export default function HuyaAccountsPage() {
useEffect(() => {
loadAccounts();
}, [loadAccounts]);
useEffect(() => {
if (canAssign) loadUsers();
loadTags();
}, [loadAccounts, canAssign, loadUsers, loadTags]);
loadSummary();
}, [canAssign, loadUsers, loadTags, loadSummary]);
const tagColorMap = useMemo(() => {
const map: Record<string, string> = {};
@@ -127,20 +158,6 @@ export default function HuyaAccountsPage() {
return map;
}, [tags]);
const filteredAccounts = useMemo(() => {
const s = searchText.trim().toLowerCase();
if (!s) return accounts;
return accounts.filter((item) => (
item.uid.toLowerCase().includes(s) ||
item.yyuid.toLowerCase().includes(s) ||
item.username.toLowerCase().includes(s) ||
item.nickname.toLowerCase().includes(s) ||
item.tag.toLowerCase().includes(s) ||
item.game_name.toLowerCase().includes(s) ||
item.game_phone.toLowerCase().includes(s)
));
}, [accounts, searchText]);
const handleImport = async () => {
if (!importText.trim()) {
message.warning('请先粘贴虎牙 CK');
@@ -155,6 +172,7 @@ export default function HuyaAccountsPage() {
setImportTag('');
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
@@ -223,6 +241,7 @@ export default function HuyaAccountsPage() {
setSmsState('');
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
@@ -245,6 +264,7 @@ export default function HuyaAccountsPage() {
setPasswordImportTag([]);
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
@@ -271,6 +291,7 @@ export default function HuyaAccountsPage() {
}
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
@@ -285,6 +306,7 @@ export default function HuyaAccountsPage() {
setSelectedRowKeys((prev) => prev.filter((key) => key !== id));
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -301,6 +323,7 @@ export default function HuyaAccountsPage() {
setSelectedRowKeys([]);
loadAccounts();
loadTags();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -345,11 +368,6 @@ export default function HuyaAccountsPage() {
}
};
const boundCount = accounts.filter((item) => item.game_name || item.game_channel || item.game_phone).length;
const pointCount = accounts.filter((item) => item.points !== null && item.points !== undefined).length;
const assignedCount = accounts.filter((item) => item.assigned_to).length;
const passwordReadyCount = accounts.filter((item) => item.has_password).length;
const passwordLoginResultColumns: TableProps<HuyaPasswordLoginBatchItem>['columns'] = [
{ title: '账号ID', dataIndex: 'line', width: 80, align: 'center' },
{ title: '账号', dataIndex: 'username', width: 160, ellipsis: true },
@@ -526,7 +544,10 @@ export default function HuyaAccountsPage() {
placeholder="按标签筛选"
style={{ width: 150 }}
value={tagFilter || undefined}
onChange={(value) => setTagFilter(value || '')}
onChange={(value) => {
setTagFilter(value || '');
setCurrentPage(1);
}}
options={tags.map((tag) => ({ value: tag, label: tag }))}
prefix={<FilterOutlined />}
/>
@@ -586,25 +607,25 @@ export default function HuyaAccountsPage() {
<Row gutter={12} style={{ marginBottom: 16 }}>
<Col xs={24} sm={8}>
<Card size="small"><Statistic title="账号总数" value={accounts.length} /></Card>
<Card size="small"><Statistic title="账号总数" value={summary.total} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="标签数" value={tags.length} /></Card>
<Card size="small"><Statistic title="标签数" value={summary.tag_count || tags.length} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="已导入密码" value={passwordReadyCount} /></Card>
<Card size="small"><Statistic title="已导入密码" value={summary.password_ready_count} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="已查积分" value={pointCount} /></Card>
<Card size="small"><Statistic title="已查积分" value={summary.point_count} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="已绑定信息" value={boundCount} /></Card>
<Card size="small"><Statistic title="已绑定信息" value={summary.bound_count} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="已分配" value={assignedCount} /></Card>
<Card size="small"><Statistic title="已分配" value={summary.assigned_count} /></Card>
</Col>
<Col xs={24} sm={4}>
<Card size="small"><Statistic title="未分配" value={accounts.length - assignedCount} /></Card>
<Card size="small"><Statistic title="未分配" value={summary.unassigned_count} /></Card>
</Col>
</Row>
@@ -613,12 +634,23 @@ export default function HuyaAccountsPage() {
placeholder="搜索 UID、昵称、标签、游戏名、手机号"
allowClear
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onChange={(e) => {
setSearchText(e.target.value);
setCurrentPage(1);
}}
style={{ width: 300 }}
prefix={<SearchOutlined />}
/>
{tags.map((tag) => (
<Tag key={tag} color="blue" onClick={() => setSearchText(tag)} style={{ cursor: 'pointer' }}>
<Tag
key={tag}
color="blue"
onClick={() => {
setSearchText(tag);
setCurrentPage(1);
}}
style={{ cursor: 'pointer' }}
>
{tag}
</Tag>
))}
@@ -628,15 +660,17 @@ export default function HuyaAccountsPage() {
rowSelection={(canImport || canDelete || canAssign) ? {
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
preserveSelectedRowKeys: true,
} : undefined}
columns={columns}
dataSource={filteredAccounts}
dataSource={accounts}
rowKey="id"
loading={loading}
size="small"
pagination={{
current: currentPage,
pageSize,
total,
showSizeChanger: true,
showTotal: (total) => `${total}`,
onChange: (page, size) => {
@@ -45,7 +45,7 @@ export default function HuyaAssignmentsPage() {
const loadAccounts = async () => {
try {
const data = await huyaApi.listAccounts({ has_cookie: true });
const data = await huyaApi.listAccounts({ has_cookie: true, include_cookie: false });
setAccounts(data);
} catch (e: unknown) {
message.error(getErrorMessage(e));
+56 -39
View File
@@ -4,7 +4,7 @@ import {
} from 'antd';
import { message } from '../utils/antdMessage';
import { CopyOutlined, DeleteOutlined, DownloadOutlined, SearchOutlined } from '@ant-design/icons';
import { huyaApi, type HuyaCookieItem } from '../api/modules';
import { huyaApi, type BasicSummary, type HuyaCookieItem } from '../api/modules';
import { usePermissions } from '../hooks/usePermissions';
import { formatTime } from '../utils/time';
import { getErrorMessage } from '../utils/error';
@@ -40,6 +40,8 @@ export default function HuyaCookiePage() {
const [loading, setLoading] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [searchText, setSearchText] = useState('');
const [total, setTotal] = useState(0);
const [summary, setSummary] = useState<BasicSummary>({ total: 0, assigned_count: 0, unassigned_count: 0 });
const [pageSize, setPageSize] = useState(() => {
const value = localStorage.getItem('huya_cookie_page_size');
return value ? Number(value) || 20 : 20;
@@ -54,19 +56,38 @@ export default function HuyaCookiePage() {
const loadCookies = useCallback(async () => {
setLoading(true);
try {
const data = await huyaApi.listCookies();
setCookies(data);
const data = await huyaApi.listCookiesPaged({
page: currentPage,
page_size: pageSize,
search: searchText.trim() || undefined,
include_cookie: false,
});
setCookies(data.items);
setTotal(data.total);
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setLoading(false);
}
}, [currentPage, pageSize, searchText]);
const loadSummary = useCallback(async () => {
try {
const data = await huyaApi.cookiesSummary();
setSummary(data);
} catch {
// 统计加载失败时不影响主列表操作。
}
}, []);
useEffect(() => {
loadCookies();
}, [loadCookies]);
useEffect(() => {
loadSummary();
}, [loadSummary]);
const handleExport = async (format: string = 'csv') => {
try {
const blob = await huyaApi.exportCookies(format);
@@ -82,31 +103,34 @@ export default function HuyaCookiePage() {
}
};
const handleCopyCookie = (record: HuyaCookieItem) => {
if (!record.cookie) {
message.warning('Cookie 为空');
return;
const handleCopyCookie = async (record: HuyaCookieItem) => {
try {
const detail = await huyaApi.getCookie(record.id);
if (!detail.cookie) {
message.warning('Cookie 为空');
return;
}
const text = `${detail.account_username}----${detail.cookie}`;
await copyToClipboard(text);
message.success(`已复制 ${detail.account_username} 的 Cookie`);
} catch (e: unknown) {
message.error(getErrorMessage(e) || '复制失败');
}
const text = `${record.account_username}----${record.cookie}`;
copyToClipboard(text).then(() => {
message.success(`已复制 ${record.account_username} 的 Cookie`);
}).catch(() => {
message.error('复制失败');
});
};
const handleCopySelected = () => {
const handleCopySelected = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择 Cookie');
return;
}
const selected = cookies.filter((item) => selectedRowKeys.includes(item.id));
const text = selected.map((item) => `${item.account_username}----${item.cookie || ''}`).join('\r\n');
copyToClipboard(text).then(() => {
try {
const selected = await Promise.all(selectedRowKeys.map((key) => huyaApi.getCookie(Number(key))));
const text = selected.map((item) => `${item.account_username}----${item.cookie || ''}`).join('\r\n');
await copyToClipboard(text);
message.success(`已复制 ${selected.length} 条 Cookie`);
}).catch(() => {
message.error('复制失败');
});
} catch (e: unknown) {
message.error(getErrorMessage(e) || '复制失败');
}
};
const handleDelete = async (id: number) => {
@@ -115,6 +139,7 @@ export default function HuyaCookiePage() {
message.success('已清除');
setSelectedRowKeys((prev) => prev.filter((key) => key !== id));
loadCookies();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
@@ -130,22 +155,12 @@ export default function HuyaCookiePage() {
message.success(result.message);
setSelectedRowKeys([]);
loadCookies();
loadSummary();
} catch (e: unknown) {
message.error(getErrorMessage(e));
}
};
const filteredCookies = cookies.filter((item) => {
if (!searchText) return true;
const search = searchText.toLowerCase();
return (
item.account_username.toLowerCase().includes(search) ||
item.uid.toLowerCase().includes(search) ||
item.yyuid.toLowerCase().includes(search) ||
(item.assigned_username || '').toLowerCase().includes(search)
);
});
const columns = [
{ title: 'ID', dataIndex: 'id', width: 70, align: 'center' as const },
{
@@ -204,9 +219,6 @@ export default function HuyaCookiePage() {
},
];
const assignedCount = cookies.filter((item) => item.assigned_to).length;
const unassignedCount = cookies.length - assignedCount;
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
@@ -245,16 +257,16 @@ export default function HuyaCookiePage() {
<Row gutter={12} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card size="small"><Statistic title="Cookie 总数" value={cookies.length} /></Card>
<Card size="small"><Statistic title="Cookie 总数" value={summary.total} /></Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic title="已分配" value={assignedCount} styles={{ content: { color: token.colorSuccess } }} />
<Statistic title="已分配" value={summary.assigned_count} styles={{ content: { color: token.colorSuccess } }} />
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic title="未分配" value={unassignedCount} styles={{ content: { color: token.colorError } }} />
<Statistic title="未分配" value={summary.unassigned_count} styles={{ content: { color: token.colorError } }} />
</Card>
</Col>
</Row>
@@ -264,7 +276,10 @@ export default function HuyaCookiePage() {
placeholder="搜索账号、UID 或分配客服"
allowClear
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onChange={(e) => {
setSearchText(e.target.value);
setCurrentPage(1);
}}
style={{ width: 300 }}
size="small"
prefix={<SearchOutlined />}
@@ -275,15 +290,17 @@ export default function HuyaCookiePage() {
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
preserveSelectedRowKeys: true,
}}
columns={columns}
dataSource={filteredCookies}
dataSource={cookies}
rowKey="id"
loading={loading}
size="small"
pagination={{
current: currentPage,
pageSize,
total,
showSizeChanger: true,
showTotal: (total) => `${total}`,
onChange: (page, size) => {
+1 -1
View File
@@ -392,7 +392,7 @@ export default function HuyaTasksPage() {
setLoading(true);
try {
const [accountResult, taskResult, goodsResult, rechargeGoodsResult, configResult, taskTypeResult, tagResult] = await Promise.allSettled([
huyaApi.listAccounts(),
huyaApi.listAccounts({ include_cookie: false }),
huyaApi.listTasks(),
huyaApi.listGoods(),
huyaApi.listRechargeGoods(),
+1 -1
View File
@@ -106,7 +106,7 @@ export default function LoginTasksPage() {
const loadAccounts = useCallback(async () => {
try {
const data = await accountApi.list();
const data = await accountApi.list({ include_sensitive: false });
setAccounts(data);
} catch (e: unknown) {
message.error(getErrorMessage(e));