完善虎牙账号管理并规范Cookie
This commit is contained in:
@@ -16,6 +16,8 @@ const ProxyPage = lazy(() => import('./pages/ProxyPage'));
|
||||
const UsersPage = lazy(() => import('./pages/UsersPage'));
|
||||
const CookiePage = lazy(() => import('./pages/CookiePage'));
|
||||
const HuyaAccountsPage = lazy(() => import('./pages/HuyaAccountsPage'));
|
||||
const HuyaAssignmentsPage = lazy(() => import('./pages/HuyaAssignmentsPage'));
|
||||
const HuyaCookiePage = lazy(() => import('./pages/HuyaCookiePage'));
|
||||
const HuyaTasksPage = lazy(() => import('./pages/HuyaTasksPage'));
|
||||
|
||||
function RouteFallback() {
|
||||
@@ -64,6 +66,8 @@ function AppContent() {
|
||||
<Route path="login-tasks" element={lazyRoute(<LoginTasksPage />)} />
|
||||
<Route path="cookies" element={lazyRoute(<CookiePage />)} />
|
||||
<Route path="huya/accounts" element={lazyRoute(<HuyaAccountsPage />)} />
|
||||
<Route path="huya/assignments" element={lazyRoute(<HuyaAssignmentsPage />)} />
|
||||
<Route path="huya/cookies" element={lazyRoute(<HuyaCookiePage />)} />
|
||||
<Route path="huya/tasks" element={lazyRoute(<HuyaTasksPage />)} />
|
||||
<Route path="proxy" element={lazyRoute(<ProxyPage />)} />
|
||||
<Route path="users" element={lazyRoute(<UsersPage />)} />
|
||||
|
||||
@@ -2,6 +2,7 @@ import api from './client';
|
||||
import type {
|
||||
HuyaAccountItem,
|
||||
HuyaConfig,
|
||||
HuyaCookieItem,
|
||||
HuyaCookieImportResult,
|
||||
HuyaGoodsItem,
|
||||
HuyaPasswordLoginRequest,
|
||||
@@ -10,21 +11,38 @@ import type {
|
||||
HuyaTaskBatchRequest,
|
||||
HuyaTaskBatchResult,
|
||||
HuyaTaskItem,
|
||||
MessageCountResponse,
|
||||
MessageDeletedResponse,
|
||||
MessageResponse,
|
||||
} from './types';
|
||||
|
||||
export const huyaApi = {
|
||||
taskTypes: () => api.get<Record<string, string>, Record<string, string>>('/huya/task-types'),
|
||||
listAccounts: (params?: { tag?: string }) =>
|
||||
listAccounts: (params?: { assigned_only?: boolean; tag?: string; has_cookie?: boolean }) =>
|
||||
api.get<HuyaAccountItem[], HuyaAccountItem[]>('/huya/accounts', { params }),
|
||||
importCookies: (text: string, tag: string = '') =>
|
||||
api.post<HuyaCookieImportResult, HuyaCookieImportResult>('/huya/accounts/import-cookies', { text, tag }),
|
||||
passwordLogin: (data: HuyaPasswordLoginRequest) =>
|
||||
api.post<HuyaPasswordLoginResult, HuyaPasswordLoginResult>('/huya/accounts/password-login', data),
|
||||
assign: (id: number, assigned_to: number | null) =>
|
||||
api.put<MessageResponse, MessageResponse>(`/huya/accounts/${id}/assign`, { assigned_to }),
|
||||
batchAssign: (account_ids: number[], assigned_to: number | null) =>
|
||||
api.post<MessageCountResponse, MessageCountResponse>('/huya/accounts/batch-assign', { account_ids, assigned_to }),
|
||||
assignmentsSummary: () =>
|
||||
api.get<{ support_users: { id: number; username: string; assigned_count: number }[]; unassigned_count: number }, { support_users: { id: number; username: string; assigned_count: number }[]; unassigned_count: number }>('/huya/accounts/assignments/summary'),
|
||||
setTag: (id: number, tag: string) =>
|
||||
api.put<MessageResponse, MessageResponse>(`/huya/accounts/${id}/tag`, { tag }),
|
||||
batchTag: (account_ids: number[], tag: string) =>
|
||||
api.put<MessageCountResponse, MessageCountResponse>('/huya/accounts/batch-tag', { account_ids, tag }),
|
||||
listTags: () => api.get<string[], string[]>('/huya/accounts/tags/list'),
|
||||
deleteAccount: (id: number) => api.delete<MessageResponse, MessageResponse>(`/huya/accounts/${id}`),
|
||||
deleteAccounts: (accountIds: number[]) =>
|
||||
api.delete<MessageDeletedResponse, MessageDeletedResponse>('/huya/accounts/batch', { params: { account_ids: accountIds.join(',') } }),
|
||||
listCookies: () => api.get<HuyaCookieItem[], HuyaCookieItem[]>('/huya/cookies'),
|
||||
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[]) =>
|
||||
api.delete<MessageDeletedResponse, MessageDeletedResponse>('/huya/cookies/batch', { params: { account_ids: accountIds.join(',') } }),
|
||||
getConfig: () => api.get<HuyaConfig, HuyaConfig>('/huya/config'),
|
||||
updateConfig: (data: Partial<HuyaConfig>) => api.put<HuyaConfig, HuyaConfig>('/huya/config', data),
|
||||
listGoods: () => api.get<HuyaGoodsItem[], HuyaGoodsItem[]>('/huya/goods'),
|
||||
|
||||
@@ -148,6 +148,19 @@ export interface HuyaPasswordLoginResult extends MessageResponse {
|
||||
sdid: string;
|
||||
}
|
||||
|
||||
export interface HuyaCookieItem {
|
||||
id: number;
|
||||
account_id: number;
|
||||
account_username: string;
|
||||
uid: string;
|
||||
yyuid: string;
|
||||
assigned_to: number | null;
|
||||
assigned_username: string | null;
|
||||
created_at: string | null;
|
||||
cookie: string;
|
||||
cookie_preview: string;
|
||||
}
|
||||
|
||||
export interface HuyaConfig {
|
||||
room_pid: string;
|
||||
sid: string;
|
||||
|
||||
@@ -68,9 +68,15 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
||||
}
|
||||
|
||||
// 虎牙
|
||||
if (can('huya:account')) {
|
||||
if (canAny(['huya:account', 'huya:view_all', 'huya:view_assigned'])) {
|
||||
huyaItems.push({ key: '/huya/accounts', label: '账号管理', icon: <GiftOutlined /> });
|
||||
}
|
||||
if (canAny(['huya:account', 'huya:assign'])) {
|
||||
huyaItems.push({ key: '/huya/assignments', label: '分配管理', icon: <SwapOutlined /> });
|
||||
}
|
||||
if (canAny(['huya:account', 'huya:cookie:view', 'huya:cookie:export'])) {
|
||||
huyaItems.push({ key: '/huya/cookies', label: 'Cookie 管理', icon: <KeyOutlined /> });
|
||||
}
|
||||
if (can('huya:task')) {
|
||||
huyaItems.push({ key: '/huya/tasks', label: '任务操作台', icon: <ShoppingCartOutlined /> });
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function DashboardPage() {
|
||||
const canViewDouyuAccounts = canAny(['account:view_all', 'account:view_assigned']);
|
||||
const canViewDouyuTasks = canAny(['login:batch', 'login:view_all', 'login:view_assigned']);
|
||||
const canViewCookies = can('cookie:view');
|
||||
const canViewHuyaAccounts = can('huya:account');
|
||||
const canViewHuyaAccounts = canAny(['huya:account', 'huya:view_all', 'huya:view_assigned']);
|
||||
const canViewHuyaTasks = can('huya:task');
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Button, Card, Col, Form, Input, message, Modal, Popconfirm, Row, Space, Statistic, Table, Tag, Typography,
|
||||
Button, Card, Col, Form, Input, message, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, Typography,
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import { DeleteOutlined, ImportOutlined, LoginOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { huyaApi, type HuyaAccountItem } from '../api/modules';
|
||||
import { DeleteOutlined, FilterOutlined, ImportOutlined, LoginOutlined, ReloadOutlined, SearchOutlined, TagOutlined } from '@ant-design/icons';
|
||||
import { huyaApi, type HuyaAccountItem, type SupportUserItem } from '../api/modules';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { formatTime } from '../utils/time';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
@@ -37,6 +37,8 @@ interface PasswordLoginFormValues {
|
||||
|
||||
export default function HuyaAccountsPage() {
|
||||
const [accounts, setAccounts] = useState<HuyaAccountItem[]>([]);
|
||||
const [users, setUsers] = useState<SupportUserItem[]>([]);
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [importText, setImportText] = useState('');
|
||||
@@ -45,7 +47,10 @@ export default function HuyaAccountsPage() {
|
||||
const [passwordLoginOpen, setPasswordLoginOpen] = useState(false);
|
||||
const [passwordLogging, setPasswordLogging] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [tagFilter, setTagFilter] = useState('');
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [batchTagInput, setBatchTagInput] = useState('');
|
||||
const [batchTagVisible, setBatchTagVisible] = useState(false);
|
||||
const [pageSize, setPageSize] = useState(() => {
|
||||
const v = localStorage.getItem('huya_account_page_size');
|
||||
return v ? Number(v) || 20 : 20;
|
||||
@@ -55,26 +60,56 @@ export default function HuyaAccountsPage() {
|
||||
const { can } = usePermissions();
|
||||
|
||||
const canManage = can('huya:account');
|
||||
const canImport = can('huya:import') || canManage;
|
||||
const canAssign = can('huya:assign') || canManage;
|
||||
const canDelete = can('huya:delete') || canManage;
|
||||
const canViewCookie = can('huya:cookie:view') || can('huya:cookie:export') || canManage;
|
||||
|
||||
const loadAccounts = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await huyaApi.listAccounts();
|
||||
const params: { tag?: string } = {};
|
||||
if (tagFilter) params.tag = tagFilter;
|
||||
const data = await huyaApi.listAccounts(params);
|
||||
setAccounts(data);
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [tagFilter]);
|
||||
|
||||
const loadUsers = useCallback(async () => {
|
||||
try {
|
||||
const data = await huyaApi.assignmentsSummary();
|
||||
setUsers(data.support_users);
|
||||
} catch {
|
||||
// 忽略客服列表加载失败,账号列表仍可继续使用。
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadTags = useCallback(async () => {
|
||||
try {
|
||||
const data = await huyaApi.listTags();
|
||||
setTags(data);
|
||||
} catch {
|
||||
// 忽略标签加载失败,页面会退化为无标签筛选。
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadAccounts();
|
||||
}, [loadAccounts]);
|
||||
if (canAssign) loadUsers();
|
||||
loadTags();
|
||||
}, [loadAccounts, canAssign, loadUsers, loadTags]);
|
||||
|
||||
const tags = useMemo(() => {
|
||||
return [...new Set(accounts.map((item) => item.tag.trim()).filter(Boolean))].sort();
|
||||
}, [accounts]);
|
||||
const tagColorMap = useMemo(() => {
|
||||
const map: Record<string, string> = {};
|
||||
tags.forEach((tag, index) => {
|
||||
map[tag] = ['blue', 'green', 'cyan', 'geekblue', 'purple', 'orange', 'magenta', 'volcano'][index % 8];
|
||||
});
|
||||
return map;
|
||||
}, [tags]);
|
||||
|
||||
const filteredAccounts = useMemo(() => {
|
||||
const s = searchText.trim().toLowerCase();
|
||||
@@ -103,6 +138,7 @@ export default function HuyaAccountsPage() {
|
||||
setImportText('');
|
||||
setImportTag('');
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
@@ -134,6 +170,7 @@ export default function HuyaAccountsPage() {
|
||||
setPasswordLoginOpen(false);
|
||||
passwordLoginForm.resetFields();
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
@@ -147,6 +184,7 @@ export default function HuyaAccountsPage() {
|
||||
message.success('已删除');
|
||||
setSelectedRowKeys((prev) => prev.filter((key) => key !== id));
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
}
|
||||
@@ -162,6 +200,46 @@ export default function HuyaAccountsPage() {
|
||||
message.success(result.message);
|
||||
setSelectedRowKeys([]);
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssign = async (accountId: number, assignedTo: number | null) => {
|
||||
try {
|
||||
await huyaApi.assign(accountId, assignedTo);
|
||||
message.success('已分配');
|
||||
loadAccounts();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetTag = async (accountId: number, tag: string) => {
|
||||
try {
|
||||
await huyaApi.setTag(accountId, tag);
|
||||
message.success('标签已更新');
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchTag = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择虎牙账号');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await huyaApi.batchTag(selectedRowKeys.map((key) => Number(key)), batchTagInput);
|
||||
message.success(`已为 ${selectedRowKeys.length} 个虎牙账号设置标签`);
|
||||
setBatchTagVisible(false);
|
||||
setBatchTagInput('');
|
||||
setSelectedRowKeys([]);
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
}
|
||||
@@ -169,6 +247,7 @@ 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 columns: TableProps<HuyaAccountItem>['columns'] = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 70, align: 'center' },
|
||||
@@ -188,7 +267,43 @@ export default function HuyaAccountsPage() {
|
||||
title: '标签',
|
||||
dataIndex: 'tag',
|
||||
width: 110,
|
||||
render: (tag: string) => tag ? <Tag color="blue">{tag}</Tag> : <Text type="secondary">-</Text>,
|
||||
render: (tag: string, record) => {
|
||||
if (!tag) {
|
||||
if (canImport) {
|
||||
return (
|
||||
<Input
|
||||
size="small"
|
||||
placeholder="输入标签"
|
||||
style={{ width: 90 }}
|
||||
onPressEnter={(e) => {
|
||||
const value = (e.target as HTMLInputElement).value.trim();
|
||||
if (value) handleSetTag(record.id, value);
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const value = e.target.value.trim();
|
||||
if (value) handleSetTag(record.id, value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <Text type="secondary">-</Text>;
|
||||
}
|
||||
if (canImport) {
|
||||
return (
|
||||
<Tag
|
||||
color={tagColorMap[tag]}
|
||||
closable
|
||||
onClose={(e) => {
|
||||
e.preventDefault();
|
||||
handleSetTag(record.id, '');
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
return <Tag color={tagColorMap[tag]}>{tag}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '积分',
|
||||
@@ -224,10 +339,30 @@ export default function HuyaAccountsPage() {
|
||||
ellipsis: true,
|
||||
render: (value: string) => (
|
||||
<Text code style={{ fontSize: 12 }}>
|
||||
{value || '-'}
|
||||
{canViewCookie ? (value || '-') : '***'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '分配给',
|
||||
dataIndex: 'assigned_username',
|
||||
width: 150,
|
||||
render: (_: unknown, record) => {
|
||||
if (canAssign) {
|
||||
return (
|
||||
<Select
|
||||
style={{ width: 130 }}
|
||||
allowClear
|
||||
placeholder="未分配"
|
||||
value={record.assigned_to}
|
||||
onChange={(value) => handleAssign(record.id, value ?? null)}
|
||||
options={users.map((user) => ({ value: user.id, label: user.username }))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return record.assigned_username || <Text type="secondary">未分配</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
@@ -249,9 +384,11 @@ export default function HuyaAccountsPage() {
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
render: (_: unknown, record) => (
|
||||
<Popconfirm title="确认删除这条虎牙 CK?" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button danger size="small" icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
canDelete ? (
|
||||
<Popconfirm title="确认删除这条虎牙账号?" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button danger size="small" icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
) : null
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -259,24 +396,45 @@ export default function HuyaAccountsPage() {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
||||
<h2 style={{ margin: 0 }}>虎牙 CK 管理</h2>
|
||||
<h2 style={{ margin: 0 }}>虎牙账号管理</h2>
|
||||
<Space wrap>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="按标签筛选"
|
||||
style={{ width: 150 }}
|
||||
value={tagFilter || undefined}
|
||||
onChange={(value) => setTagFilter(value || '')}
|
||||
options={tags.map((tag) => ({ value: tag, label: tag }))}
|
||||
prefix={<FilterOutlined />}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadAccounts} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
{selectedRowKeys.length > 0 && (
|
||||
{canImport && (
|
||||
<Button
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
icon={<TagOutlined />}
|
||||
onClick={() => {
|
||||
setBatchTagInput('');
|
||||
setBatchTagVisible(true);
|
||||
}}
|
||||
>
|
||||
批量打标签
|
||||
</Button>
|
||||
)}
|
||||
{canDelete && selectedRowKeys.length > 0 && (
|
||||
<Popconfirm title={`确认删除选中的 ${selectedRowKeys.length} 条虎牙 CK?`} onConfirm={handleDeleteSelected}>
|
||||
<Button danger icon={<DeleteOutlined />}>
|
||||
删除选中 ({selectedRowKeys.length})
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{canManage && (
|
||||
{canImport && (
|
||||
<Button icon={<LoginOutlined />} onClick={openPasswordLogin}>
|
||||
密码登录
|
||||
</Button>
|
||||
)}
|
||||
{canManage && (
|
||||
{canImport && (
|
||||
<Button type="primary" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>
|
||||
粘贴 CK
|
||||
</Button>
|
||||
@@ -286,14 +444,23 @@ export default function HuyaAccountsPage() {
|
||||
|
||||
<Row gutter={12} style={{ marginBottom: 16 }}>
|
||||
<Col xs={24} sm={8}>
|
||||
<Card size="small"><Statistic title="CK 总数" value={accounts.length} /></Card>
|
||||
<Card size="small"><Statistic title="账号总数" value={accounts.length} /></Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Col xs={24} sm={4}>
|
||||
<Card size="small"><Statistic title="标签数" value={tags.length} /></Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={4}>
|
||||
<Card size="small"><Statistic title="已查积分" value={pointCount} /></Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Col xs={24} sm={4}>
|
||||
<Card size="small"><Statistic title="已绑定信息" value={boundCount} /></Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={4}>
|
||||
<Card size="small"><Statistic title="已分配" value={assignedCount} /></Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={4}>
|
||||
<Card size="small"><Statistic title="未分配" value={accounts.length - assignedCount} /></Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
@@ -313,10 +480,10 @@ export default function HuyaAccountsPage() {
|
||||
</div>
|
||||
|
||||
<Table
|
||||
rowSelection={{
|
||||
rowSelection={(canImport || canDelete || canAssign) ? {
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys),
|
||||
}}
|
||||
} : undefined}
|
||||
columns={columns}
|
||||
dataSource={filteredAccounts}
|
||||
rowKey="id"
|
||||
@@ -366,6 +533,25 @@ export default function HuyaAccountsPage() {
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="批量设置标签"
|
||||
open={batchTagVisible}
|
||||
onCancel={() => setBatchTagVisible(false)}
|
||||
onOk={handleBatchTag}
|
||||
okText="确定"
|
||||
width={400}
|
||||
>
|
||||
<p>为选中的 {selectedRowKeys.length} 个虎牙账号设置标签:</p>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="输入或选择标签"
|
||||
value={batchTagInput ? [batchTagInput] : []}
|
||||
onChange={(values) => setBatchTagInput(values.length > 0 ? values[values.length - 1] : '')}
|
||||
options={tags.map((tag) => ({ value: tag, label: tag }))}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="虎牙密码登录"
|
||||
open={passwordLoginOpen}
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Badge, Button, Card, Col, Input, message, Row, Select, Space, Statistic, Table, Tabs, Tag, theme, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined, ClearOutlined, SwapOutlined, TeamOutlined, UsergroupAddOutlined, UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { huyaApi, type HuyaAccountItem, type SupportUserItem } from '../api/modules';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
|
||||
const { Text } = Typography;
|
||||
const STORAGE_KEY_SELECTED_USER = 'huya_assignments_selected_user_id';
|
||||
|
||||
function accountName(account: HuyaAccountItem) {
|
||||
return account.nickname || account.username || account.uid || `#${account.id}`;
|
||||
}
|
||||
|
||||
export default function HuyaAssignmentsPage() {
|
||||
const { token } = theme.useToken();
|
||||
const [accounts, setAccounts] = useState<HuyaAccountItem[]>([]);
|
||||
const [supportUsers, setSupportUsers] = useState<SupportUserItem[]>([]);
|
||||
const [selectedUser, setSelectedUser] = useState<SupportUserItem | null>(null);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [filterTag, setFilterTag] = useState<string | undefined>(undefined);
|
||||
const [activeTab, setActiveTab] = useState('unassigned');
|
||||
const [assigning, setAssigning] = useState(false);
|
||||
const [pageSize, setPageSize] = useState(() => {
|
||||
const value = localStorage.getItem('huya_assignment_page_size');
|
||||
return value ? Number(value) || 15 : 15;
|
||||
});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const loadSummary = async () => {
|
||||
try {
|
||||
const data = await huyaApi.assignmentsSummary();
|
||||
setSupportUsers(data.support_users);
|
||||
return data.support_users;
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const loadAccounts = async () => {
|
||||
try {
|
||||
const data = await huyaApi.listAccounts({ has_cookie: true });
|
||||
setAccounts(data);
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
void loadAccounts();
|
||||
const users = await loadSummary();
|
||||
if (users.length > 0) {
|
||||
const savedId = localStorage.getItem(STORAGE_KEY_SELECTED_USER);
|
||||
const savedUser = savedId ? users.find((user) => user.id === Number(savedId)) : null;
|
||||
setSelectedUser(savedUser || users[0]);
|
||||
}
|
||||
};
|
||||
void init();
|
||||
}, []);
|
||||
|
||||
const allTags = useMemo(() => {
|
||||
return [...new Set(accounts.map((account) => (account.tag || '').trim()).filter(Boolean))].sort();
|
||||
}, [accounts]);
|
||||
|
||||
const unassignedAccounts = useMemo(() => accounts.filter((account) => !account.assigned_to), [accounts]);
|
||||
|
||||
const assignedAccounts = useMemo(() => {
|
||||
if (!selectedUser) return [];
|
||||
return accounts.filter((account) => account.assigned_to === selectedUser.id);
|
||||
}, [accounts, selectedUser]);
|
||||
|
||||
const displayAccounts = useMemo(() => {
|
||||
let list = activeTab === 'unassigned' ? unassignedAccounts : assignedAccounts;
|
||||
if (searchText) {
|
||||
const search = searchText.toLowerCase();
|
||||
list = list.filter((account) => (
|
||||
accountName(account).toLowerCase().includes(search) ||
|
||||
account.uid.toLowerCase().includes(search) ||
|
||||
account.yyuid.toLowerCase().includes(search) ||
|
||||
account.game_phone.toLowerCase().includes(search)
|
||||
));
|
||||
}
|
||||
if (filterTag) {
|
||||
list = list.filter((account) => (account.tag || '').trim() === filterTag);
|
||||
}
|
||||
return list;
|
||||
}, [activeTab, assignedAccounts, filterTag, searchText, unassignedAccounts]);
|
||||
|
||||
const handleSelectUser = (user: SupportUserItem) => {
|
||||
setSelectedUser(user);
|
||||
setSelectedRowKeys([]);
|
||||
setActiveTab('unassigned');
|
||||
setSearchText('');
|
||||
setFilterTag(undefined);
|
||||
localStorage.setItem(STORAGE_KEY_SELECTED_USER, String(user.id));
|
||||
};
|
||||
|
||||
const refreshAfterAssign = async () => {
|
||||
await loadAccounts();
|
||||
const users = await loadSummary();
|
||||
const refreshed = users.find((user) => user.id === selectedUser?.id);
|
||||
if (refreshed) setSelectedUser(refreshed);
|
||||
};
|
||||
|
||||
const handleBatchAssign = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择虎牙账号');
|
||||
return;
|
||||
}
|
||||
if (!selectedUser) {
|
||||
message.warning('请先选择客服');
|
||||
return;
|
||||
}
|
||||
setAssigning(true);
|
||||
try {
|
||||
await huyaApi.batchAssign(selectedRowKeys, selectedUser.id);
|
||||
message.success(`已将 ${selectedRowKeys.length} 个虎牙账号分配给 ${selectedUser.username}`);
|
||||
setSelectedRowKeys([]);
|
||||
await refreshAfterAssign();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setAssigning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchUnassign = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择虎牙账号');
|
||||
return;
|
||||
}
|
||||
setAssigning(true);
|
||||
try {
|
||||
await huyaApi.batchAssign(selectedRowKeys, null);
|
||||
message.success(`已取消 ${selectedRowKeys.length} 个虎牙账号的分配`);
|
||||
setSelectedRowKeys([]);
|
||||
await refreshAfterAssign();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setAssigning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssignAllUnassigned = async () => {
|
||||
if (!selectedUser) {
|
||||
message.warning('请先选择客服');
|
||||
return;
|
||||
}
|
||||
const ids = displayAccounts.map((account) => account.id);
|
||||
if (ids.length === 0) {
|
||||
message.info('没有未分配的虎牙账号');
|
||||
return;
|
||||
}
|
||||
setAssigning(true);
|
||||
try {
|
||||
await huyaApi.batchAssign(ids, selectedUser.id);
|
||||
message.success(`已将 ${ids.length} 个虎牙账号分配给 ${selectedUser.username}`);
|
||||
await refreshAfterAssign();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setAssigning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const assignedCount = accounts.filter((account) => !!account.assigned_to).length;
|
||||
const unassignedCount = accounts.length - assignedCount;
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 70 },
|
||||
{
|
||||
title: '虎牙账号',
|
||||
render: (_: unknown, record: HuyaAccountItem) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text strong>{accountName(record)}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>UID {record.uid || record.yyuid || '-'}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'tag',
|
||||
width: 100,
|
||||
render: (tag: string) => tag ? <Tag color="blue">{tag}</Tag> : <Text type="secondary">-</Text>,
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'game_phone',
|
||||
width: 130,
|
||||
render: (value: string) => value || <Text type="secondary">-</Text>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<h2 style={{ margin: 0, flexShrink: 0 }}>虎牙分配管理</h2>
|
||||
|
||||
<Row gutter={12} style={{ flexShrink: 0 }}>
|
||||
<Col span={6}>
|
||||
<Card size="small"><Statistic title="虎牙账号" value={accounts.length} prefix={<TeamOutlined />} /></Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="已分配" value={assignedCount} styles={{ content: { color: token.colorSuccess } }} prefix={<CheckCircleOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="未分配" value={unassignedCount} styles={{ content: { color: token.colorError } }} prefix={<SwapOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small"><Statistic title="客服人数" value={supportUsers.length} prefix={<UsergroupAddOutlined />} /></Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={12} style={{ flex: 1, minHeight: 0 }}>
|
||||
<Col span={8} style={{ height: '100%', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||
<Card
|
||||
title="客服列表"
|
||||
size="small"
|
||||
style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}
|
||||
styles={{ body: { flex: 1, overflow: 'auto', padding: 8 } }}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{supportUsers.map((user) => {
|
||||
const isSelected = selectedUser?.id === user.id;
|
||||
return (
|
||||
<div
|
||||
key={user.id}
|
||||
onClick={() => handleSelectUser(user)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '10px 14px',
|
||||
borderRadius: 6,
|
||||
cursor: 'pointer',
|
||||
border: `1px solid ${isSelected ? token.colorPrimary : token.colorBorderSecondary}`,
|
||||
background: isSelected ? token.colorPrimaryBg : token.colorBgContainer,
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<UserOutlined style={{ color: token.colorPrimary }} />
|
||||
<Text strong>{user.username}</Text>
|
||||
</Space>
|
||||
<Badge count={user.assigned_count} showZero style={{ backgroundColor: token.colorPrimary }} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{supportUsers.length === 0 && (
|
||||
<Text type="secondary" style={{ textAlign: 'center', padding: 20, display: 'block' }}>
|
||||
暂无客服用户
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={16} style={{ height: '100%', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||
<Card
|
||||
size="small"
|
||||
title={selectedUser ? (
|
||||
<Space>
|
||||
<span>已选中客服:</span>
|
||||
<Tag color="blue" style={{ fontSize: 14, padding: '2px 10px' }}>
|
||||
{selectedUser.username}(已分配 {selectedUser.assigned_count} 个)
|
||||
</Tag>
|
||||
</Space>
|
||||
) : <Text type="secondary">请选择左侧客服进行操作</Text>}
|
||||
style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}
|
||||
styles={{ body: { flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, padding: 0 } }}
|
||||
>
|
||||
<div style={{ padding: '8px 16px', borderBottom: `1px solid ${token.colorBorderSecondary}`, display: 'flex', gap: 8, alignItems: 'center', flexShrink: 0 }}>
|
||||
<Input.Search
|
||||
placeholder="搜索昵称、UID、手机号"
|
||||
allowClear
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
style={{ width: 220 }}
|
||||
size="small"
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="按标签筛选"
|
||||
value={filterTag}
|
||||
onChange={(value) => setFilterTag(value || undefined)}
|
||||
style={{ width: 150 }}
|
||||
size="small"
|
||||
options={allTags.map((tag) => ({ value: tag, label: tag }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
size="small"
|
||||
activeKey={activeTab}
|
||||
onChange={(key) => { setActiveTab(key); setSelectedRowKeys([]); }}
|
||||
style={{ padding: '0 16px', flexShrink: 0, marginBottom: 0 }}
|
||||
items={[
|
||||
{ key: 'unassigned', label: `未分配 (${unassignedAccounts.length})` },
|
||||
{
|
||||
key: 'assigned',
|
||||
label: selectedUser ? `已分配给 ${selectedUser.username} (${assignedAccounts.length})` : '已分配账号',
|
||||
disabled: !selectedUser,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div style={{ padding: '0 16px 8px', flexShrink: 0 }}>
|
||||
<Space>
|
||||
{activeTab === 'unassigned' && selectedUser && (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<SwapOutlined />}
|
||||
loading={assigning}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
onClick={handleBatchAssign}
|
||||
>
|
||||
分配选中 ({selectedRowKeys.length}) 给 {selectedUser.username}
|
||||
</Button>
|
||||
<Button size="small" loading={assigning} onClick={handleAssignAllUnassigned}>
|
||||
全部分配给 {selectedUser.username}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{activeTab === 'assigned' && selectedUser && (
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
icon={<ClearOutlined />}
|
||||
loading={assigning}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
onClick={handleBatchUnassign}
|
||||
>
|
||||
取消分配选中 ({selectedRowKeys.length})
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={displayAccounts}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
size: 'small',
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page, size) => {
|
||||
setCurrentPage(page);
|
||||
if (size !== pageSize) {
|
||||
setPageSize(size);
|
||||
localStorage.setItem('huya_assignment_page_size', String(size));
|
||||
setCurrentPage(1);
|
||||
}
|
||||
},
|
||||
}}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys as number[]),
|
||||
}}
|
||||
scroll={{ y: 'calc(100vh - 500px)' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
Button, Card, Col, Dropdown, Input, message, Popconfirm, Row, Space, Statistic, Table, Tag, theme, Typography,
|
||||
} from 'antd';
|
||||
import { CopyOutlined, DeleteOutlined, DownloadOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { huyaApi, type HuyaCookieItem } from '../api/modules';
|
||||
import { usePermissions } from '../hooks/usePermissions';
|
||||
import { formatTime } from '../utils/time';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
function copyToClipboard(text: string): Promise<void> {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.left = '-9999px';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
const ok = document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
if (ok) resolve();
|
||||
else reject(new Error('execCommand copy failed'));
|
||||
} catch (e) {
|
||||
document.body.removeChild(textarea);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default function HuyaCookiePage() {
|
||||
const { token } = theme.useToken();
|
||||
const [cookies, setCookies] = useState<HuyaCookieItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [pageSize, setPageSize] = useState(() => {
|
||||
const value = localStorage.getItem('huya_cookie_page_size');
|
||||
return value ? Number(value) || 20 : 20;
|
||||
});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const { can } = usePermissions();
|
||||
|
||||
const canManage = can('huya:account');
|
||||
const canView = can('huya:cookie:view') || can('huya:cookie:export') || canManage;
|
||||
const canExport = can('huya:cookie:export') || canManage;
|
||||
|
||||
const loadCookies = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await huyaApi.listCookies();
|
||||
setCookies(data);
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadCookies();
|
||||
}, [loadCookies]);
|
||||
|
||||
const handleExport = async (format: string = 'csv') => {
|
||||
try {
|
||||
const blob = await huyaApi.exportCookies(format);
|
||||
const url = URL.createObjectURL(blob instanceof Blob ? blob : new Blob([blob]));
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = format === 'custom' ? 'huya_cookies_custom.txt' : 'huya_cookies.csv';
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
message.success('已导出');
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyCookie = (record: HuyaCookieItem) => {
|
||||
if (!record.cookie) {
|
||||
message.warning('Cookie 为空');
|
||||
return;
|
||||
}
|
||||
const text = `${record.account_username}----${record.cookie}`;
|
||||
copyToClipboard(text).then(() => {
|
||||
message.success(`已复制 ${record.account_username} 的 Cookie`);
|
||||
}).catch(() => {
|
||||
message.error('复制失败');
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopySelected = () => {
|
||||
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(() => {
|
||||
message.success(`已复制 ${selected.length} 条 Cookie`);
|
||||
}).catch(() => {
|
||||
message.error('复制失败');
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await huyaApi.deleteCookie(id);
|
||||
message.success('已清除');
|
||||
setSelectedRowKeys((prev) => prev.filter((key) => key !== id));
|
||||
loadCookies();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSelected = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择要清除的 Cookie');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await huyaApi.deleteCookies(selectedRowKeys.map((key) => Number(key)));
|
||||
message.success(result.message);
|
||||
setSelectedRowKeys([]);
|
||||
loadCookies();
|
||||
} 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 },
|
||||
{
|
||||
title: '虎牙账号',
|
||||
dataIndex: 'account_username',
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'UID',
|
||||
dataIndex: 'uid',
|
||||
width: 130,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '分配',
|
||||
dataIndex: 'assigned_username',
|
||||
width: 110,
|
||||
align: 'center' as const,
|
||||
render: (name: string | null) =>
|
||||
name ? <Tag color="green">{name}</Tag> : <Text type="secondary" style={{ fontSize: 12 }}>未分配</Text>,
|
||||
},
|
||||
{
|
||||
title: 'Cookie',
|
||||
dataIndex: 'cookie_preview',
|
||||
ellipsis: true,
|
||||
render: (value: string) => {
|
||||
if (!canView) return <Tag>***</Tag>;
|
||||
return <span style={{ fontFamily: 'monospace', fontSize: 12 }}>{value}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 180,
|
||||
align: 'center' as const,
|
||||
render: (value: string | null) => value ? formatTime(value) : <Text type="secondary">-</Text>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 130,
|
||||
align: 'center' as const,
|
||||
fixed: 'right' as const,
|
||||
render: (_: unknown, record: HuyaCookieItem) => (
|
||||
<Space size={4}>
|
||||
<Button size="small" icon={<CopyOutlined />} onClick={() => handleCopyCookie(record)}>
|
||||
复制
|
||||
</Button>
|
||||
{canExport && (
|
||||
<Popconfirm title="确认清除这条 Cookie?" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button danger size="small" icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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' }}>
|
||||
<h2 style={{ margin: 0 }}>虎牙 Cookie 管理</h2>
|
||||
<Space>
|
||||
<Button icon={<CopyOutlined />} onClick={handleCopySelected} disabled={selectedRowKeys.length === 0}>
|
||||
复制选中 {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
|
||||
</Button>
|
||||
{canExport && (
|
||||
<Popconfirm
|
||||
title={`确认清除选中的 ${selectedRowKeys.length} 条虎牙 Cookie?`}
|
||||
onConfirm={handleDeleteSelected}
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
>
|
||||
<Button danger icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0}>
|
||||
清除选中 {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{canExport && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'csv', label: 'CSV(账号, UID, Cookie, 时间)', onClick: () => handleExport('csv') },
|
||||
{ key: 'custom', label: '自定义(账号----ck)', onClick: () => handleExport('custom') },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Button type="primary" icon={<DownloadOutlined />}>
|
||||
导出
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={12} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}>
|
||||
<Card size="small"><Statistic title="Cookie 总数" value={cookies.length} /></Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="已分配" value={assignedCount} styles={{ content: { color: token.colorSuccess } }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="未分配" value={unassignedCount} styles={{ content: { color: token.colorError } }} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Input.Search
|
||||
placeholder="搜索账号、UID 或分配客服"
|
||||
allowClear
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
style={{ width: 300 }}
|
||||
size="small"
|
||||
prefix={<SearchOutlined />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys),
|
||||
}}
|
||||
columns={columns}
|
||||
dataSource={filteredCookies}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="small"
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
onChange: (page, size) => {
|
||||
setCurrentPage(page);
|
||||
if (size !== pageSize) {
|
||||
setPageSize(size);
|
||||
localStorage.setItem('huya_cookie_page_size', String(size));
|
||||
setCurrentPage(1);
|
||||
}
|
||||
},
|
||||
}}
|
||||
scroll={{ x: 960 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user