Files
live-hub-py/web/frontend/src/pages/AccountsPage.tsx
T
2026-06-22 13:11:15 +08:00

175 lines
4.8 KiB
TypeScript

import { useEffect, useState } from 'react';
import {
Table, Button, Modal, Input, Select, message, Popconfirm, Typography,
} from 'antd';
import { ImportOutlined, DeleteOutlined } from '@ant-design/icons';
import { accountApi, userApi } from '../api/modules';
import { getUser, hasPerm } from '../store/auth';
const { TextArea } = Input;
const { Text } = Typography;
export default function AccountsPage() {
const [accounts, setAccounts] = useState<any[]>([]);
const [users, setUsers] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [importOpen, setImportOpen] = useState(false);
const [importText, setImportText] = useState('');
const [importing, setImporting] = useState(false);
const user = getUser();
const canViewAll = hasPerm(user, 'account:view_all');
const canImport = hasPerm(user, 'account:import');
const canAssign = hasPerm(user, 'account:assign');
const canDelete = hasPerm(user, 'account:delete');
const loadAccounts = async () => {
setLoading(true);
try {
const data = await accountApi.list();
setAccounts(data);
} catch (e: any) {
message.error(e.message);
} finally {
setLoading(false);
}
};
const loadUsers = async () => {
try {
const data = await userApi.list();
setUsers(data.filter((u: any) => u.role === 'support'));
} catch {}
};
useEffect(() => {
loadAccounts();
if (canAssign) loadUsers();
}, []);
const handleImport = async () => {
if (!importText.trim()) {
message.warning('请输入账号数据');
return;
}
setImporting(true);
try {
const result = await accountApi.import(importText);
message.success(result.message);
setImportOpen(false);
setImportText('');
loadAccounts();
} catch (e: any) {
message.error(e.message);
} finally {
setImporting(false);
}
};
const handleAssign = async (accountId: number, assignedTo: number | null) => {
try {
await accountApi.assign(accountId, assignedTo);
message.success('已分配');
loadAccounts();
} catch (e: any) {
message.error(e.message);
}
};
const handleDelete = async (id: number) => {
try {
await accountApi.delete(id);
message.success('已删除');
loadAccounts();
} catch (e: any) {
message.error(e.message);
}
};
const columns: any[] = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '用户名', dataIndex: 'username' },
];
if (canViewAll) {
columns.push(
{ title: '密码', dataIndex: 'password', width: 120 },
{ title: '邮箱', dataIndex: 'email' },
{ title: '邮箱密码', dataIndex: 'email_password', width: 120 },
);
}
columns.push({
title: '分配给',
dataIndex: 'assigned_username',
render: (_: any, record: any) => {
if (canAssign) {
return (
<Select
style={{ width: 140 }}
allowClear
placeholder="未分配"
value={record.assigned_to}
onChange={(val) => handleAssign(record.id, val ?? null)}
options={users.map((u: any) => ({ value: u.id, label: u.username }))}
/>
);
}
return record.assigned_username || <Text type="secondary">未分配</Text>;
},
});
if (canDelete) {
columns.push({
title: '操作',
width: 80,
render: (_: any, record: any) => (
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button danger size="small" icon={<DeleteOutlined />}>删除</Button>
</Popconfirm>
),
});
}
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<h2>账号管理</h2>
{canImport && (
<Button type="primary" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>
批量导入
</Button>
)}
</div>
<Table
columns={columns}
dataSource={accounts}
rowKey="id"
loading={loading}
size="small"
pagination={{ pageSize: 20 }}
/>
<Modal
title="批量导入账号"
open={importOpen}
onCancel={() => setImportOpen(false)}
onOk={handleImport}
confirmLoading={importing}
okText="导入"
width={600}
>
<Text type="secondary">
格式:用户名|密码|邮箱|邮箱密码(每行一个)
</Text>
<TextArea
rows={10}
value={importText}
onChange={(e) => setImportText(e.target.value)}
placeholder="用户名|密码|邮箱|邮箱密码&#10;用户名|密码|邮箱|邮箱密码"
style={{ marginTop: 8 }}
/>
</Modal>
</div>
);
}