增加了账号管理的分组功能

This commit is contained in:
yml2213
2026-06-22 15:09:51 +08:00
parent 5fa91c1bb4
commit 9af92fc2a9
13 changed files with 437 additions and 55 deletions
+190 -11
View File
@@ -1,21 +1,31 @@
import { useEffect, useState } from 'react';
import { useEffect, useState, useMemo } from 'react';
import {
Table, Button, Modal, Input, Select, message, Popconfirm, Typography,
Table, Button, Modal, Input, Select, message, Popconfirm, Typography, Tag, Space,
Row, Col, Card, Statistic,
} from 'antd';
import { ImportOutlined, DeleteOutlined } from '@ant-design/icons';
import { ImportOutlined, DeleteOutlined, TagOutlined, FilterOutlined } from '@ant-design/icons';
import { accountApi, userApi } from '../api/modules';
import { getUser, hasPerm } from '../store/auth';
const { TextArea } = Input;
const { Text } = Typography;
const TAG_COLORS = [
'blue', 'green', 'cyan', 'geekblue', 'purple', 'orange', 'magenta', 'volcano',
];
export default function AccountsPage() {
const [accounts, setAccounts] = useState<any[]>([]);
const [users, setUsers] = useState<any[]>([]);
const [tags, setTags] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [importOpen, setImportOpen] = useState(false);
const [importText, setImportText] = useState('');
const [importing, setImporting] = useState(false);
const [tagFilter, setTagFilter] = useState<string>('');
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [batchTagInput, setBatchTagInput] = useState('');
const [batchTagVisible, setBatchTagVisible] = useState(false);
const user = getUser();
const canViewAll = hasPerm(user, 'account:view_all');
@@ -26,7 +36,9 @@ export default function AccountsPage() {
const loadAccounts = async () => {
setLoading(true);
try {
const data = await accountApi.list();
const params: any = {};
if (tagFilter) params.tag = tagFilter;
const data = await accountApi.list(params);
setAccounts(data);
} catch (e: any) {
message.error(e.message);
@@ -42,11 +54,31 @@ export default function AccountsPage() {
} catch {}
};
const loadTags = async () => {
try {
const data = await accountApi.listTags();
setTags(data);
} catch {}
};
useEffect(() => {
loadAccounts();
if (canAssign) loadUsers();
loadTags();
}, []);
useEffect(() => {
loadAccounts();
}, [tagFilter]);
const tagColorMap = useMemo(() => {
const map: Record<string, string> = {};
tags.forEach((t, i) => {
map[t] = TAG_COLORS[i % TAG_COLORS.length];
});
return map;
}, [tags]);
const handleImport = async () => {
if (!importText.trim()) {
message.warning('请输入账号数据');
@@ -59,6 +91,7 @@ export default function AccountsPage() {
setImportOpen(false);
setImportText('');
loadAccounts();
loadTags();
} catch (e: any) {
message.error(e.message);
} finally {
@@ -76,11 +109,42 @@ export default function AccountsPage() {
}
};
const handleSetTag = async (accountId: number, tag: string) => {
try {
await accountApi.setTag(accountId, tag);
message.success('标签已更新');
loadAccounts();
loadTags();
} catch (e: any) {
message.error(e.message);
}
};
const handleBatchTag = async () => {
if (selectedRowKeys.length === 0) {
message.warning('请先选择账号');
return;
}
try {
await accountApi.batchTag(selectedRowKeys as number[], batchTagInput);
message.success(`已为 ${selectedRowKeys.length} 个账号设置标签`);
setBatchTagVisible(false);
setBatchTagInput('');
setSelectedRowKeys([]);
loadAccounts();
loadTags();
} catch (e: any) {
message.error(e.message);
}
};
const handleDelete = async (id: number) => {
try {
await accountApi.delete(id);
message.success('已删除');
setSelectedRowKeys((prev) => prev.filter((k) => k !== id));
loadAccounts();
loadTags();
} catch (e: any) {
message.error(e.message);
}
@@ -89,6 +153,48 @@ export default function AccountsPage() {
const columns: any[] = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '用户名', dataIndex: 'username' },
{
title: '标签',
dataIndex: 'tag',
width: 120,
render: (tag: string, record: any) => {
if (!tag) {
if (canImport) {
return (
<Input
size="small"
placeholder="输入标签"
style={{ width: 90 }}
onPressEnter={(e) => {
const val = (e.target as HTMLInputElement).value.trim();
if (val) handleSetTag(record.id, val);
}}
onBlur={(e) => {
const val = e.target.value.trim();
if (val) handleSetTag(record.id, val);
}}
/>
);
}
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>;
},
},
];
if (canViewAll) {
@@ -135,13 +241,66 @@ export default function AccountsPage() {
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<h2></h2>
{canImport && (
<Button type="primary" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>
</Button>
)}
<Space>
<Select
allowClear
placeholder="按标签筛选"
style={{ width: 150 }}
value={tagFilter || undefined}
onChange={(val) => setTagFilter(val || '')}
options={tags.map((t) => ({ value: t, label: t }))}
prefix={<FilterOutlined />}
/>
{canImport && (
<>
<Button
disabled={selectedRowKeys.length === 0}
icon={<TagOutlined />}
onClick={() => {
setBatchTagInput('');
setBatchTagVisible(true);
}}
>
</Button>
<Button type="primary" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>
</Button>
</>
)}
</Space>
</div>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card size="small"><Statistic title="账号总数" value={accounts.length} /></Card>
</Col>
<Col span={6}>
<Card size="small"><Statistic title="标签数" value={tags.length} /></Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="已分配"
value={accounts.filter((a) => a.assigned_to).length}
/>
</Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic
title="未分配"
value={accounts.filter((a) => !a.assigned_to).length}
/>
</Card>
</Col>
</Row>
<Table
rowSelection={canImport ? {
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
} : undefined}
columns={columns}
dataSource={accounts}
rowKey="id"
@@ -149,6 +308,7 @@ export default function AccountsPage() {
size="small"
pagination={{ pageSize: 20 }}
/>
<Modal
title="批量导入账号"
open={importOpen}
@@ -159,16 +319,35 @@ export default function AccountsPage() {
width={600}
>
<Text type="secondary">
|||
||||
</Text>
<TextArea
rows={10}
value={importText}
onChange={(e) => setImportText(e.target.value)}
placeholder="用户名|密码|邮箱|邮箱密码&#10;用户名|密码|邮箱|邮箱密码"
placeholder={`用户名|密码|邮箱|邮箱密码|标签\n用户名|密码|邮箱|邮箱密码|标签`}
style={{ marginTop: 8 }}
/>
</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={(vals) => setBatchTagInput(vals.length > 0 ? vals[vals.length - 1] : '')}
options={tags.map((t) => ({ value: t, label: t }))}
/>
</Modal>
</div>
);
}