完善虎牙账号管理并规范Cookie
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user