完善虎牙账号管理并规范Cookie

This commit is contained in:
yml2213
2026-07-05 13:13:38 +08:00
parent 8f48b96fdc
commit 984096f758
15 changed files with 1320 additions and 59 deletions
+208 -22
View File
@@ -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}