import { useEffect, useState, useMemo, useCallback } from 'react'; import { Table, Button, Modal, Input, Select, message, Popconfirm, Typography, Tag, Space, Row, Col, Card, Statistic, } from 'antd'; import type { TableProps } from 'antd'; import { ImportOutlined, DeleteOutlined, TagOutlined, FilterOutlined } from '@ant-design/icons'; import { accountApi, userApi, type AccountItem, type UserInfo } from '../api/modules'; import { usePermissions } from '../hooks/usePermissions'; import { formatTime } from '../utils/time'; import { getErrorMessage } from '../utils/error'; 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([]); const [users, setUsers] = useState([]); const [tags, setTags] = useState([]); const [loading, setLoading] = useState(false); const [importOpen, setImportOpen] = useState(false); const [importText, setImportText] = useState(''); const [importing, setImporting] = useState(false); const [tagFilter, setTagFilter] = useState(''); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [batchTagInput, setBatchTagInput] = useState(''); const [batchTagVisible, setBatchTagVisible] = useState(false); const [pageSize, setPageSize] = useState(() => { const v = localStorage.getItem('account_page_size'); return v ? Number(v) || 20 : 20; }); const [currentPage, setCurrentPage] = useState(1); const { can } = usePermissions(); const canViewFull = can('account:view_full'); const canImport = can('account:import'); const canAssign = can('account:assign'); const canDelete = can('account:delete'); const loadAccounts = useCallback(async () => { setLoading(true); try { const params: { tag?: string } = {}; if (tagFilter) params.tag = tagFilter; const data = await accountApi.list(params); setAccounts(data); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setLoading(false); } }, [tagFilter]); const loadUsers = useCallback(async () => { try { const data = await userApi.list(); setUsers(data.filter((u) => u.role === 'support')); } catch { // 忽略客服列表加载失败,账号列表仍可继续使用。 } }, []); const loadTags = useCallback(async () => { try { const data = await accountApi.listTags(); setTags(data); } catch { // 忽略标签加载失败,页面会退化为无标签筛选。 } }, []); useEffect(() => { loadAccounts(); if (canAssign) loadUsers(); loadTags(); }, [loadAccounts, canAssign, loadUsers, loadTags]); const tagColorMap = useMemo(() => { const map: Record = {}; tags.forEach((t, i) => { map[t] = TAG_COLORS[i % TAG_COLORS.length]; }); return map; }, [tags]); 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(); loadTags(); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setImporting(false); } }; const handleAssign = async (accountId: number, assignedTo: number | null) => { try { await accountApi.assign(accountId, assignedTo); message.success('已分配'); loadAccounts(); } catch (e: unknown) { message.error(getErrorMessage(e)); } }; const handleSetTag = async (accountId: number, tag: string) => { try { await accountApi.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 accountApi.batchTag(selectedRowKeys as number[], batchTagInput); message.success(`已为 ${selectedRowKeys.length} 个账号设置标签`); setBatchTagVisible(false); setBatchTagInput(''); setSelectedRowKeys([]); loadAccounts(); loadTags(); } catch (e: unknown) { message.error(getErrorMessage(e)); } }; const handleDelete = async (id: number) => { try { await accountApi.delete(id); message.success('已删除'); setSelectedRowKeys((prev) => prev.filter((k) => k !== id)); loadAccounts(); loadTags(); } catch (e: unknown) { message.error(getErrorMessage(e)); } }; const handleBatchDelete = async () => { if (selectedRowKeys.length === 0) { message.warning('请先选择账号'); return; } try { const result = await accountApi.batchDelete(selectedRowKeys as number[]); message.success(result.message); setSelectedRowKeys([]); loadAccounts(); loadTags(); } catch (e: unknown) { message.error(getErrorMessage(e)); } }; const columns: TableProps['columns'] = [ { title: 'ID', dataIndex: 'id', width: 60 }, { title: '用户名', dataIndex: 'username' }, { title: '标签', dataIndex: 'tag', width: 120, render: (tag: string, record: AccountItem) => { if (!tag) { if (canImport) { return ( { 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 -; } if (canImport) { return ( { e.preventDefault(); handleSetTag(record.id, ''); }} > {tag} ); } return {tag}; }, }, { title: '上传时间', dataIndex: 'created_at', width: 180, render: (val: string | null) => val ? formatTime(val) : -, }, ]; // 只有管理员可看完整字段(密码、邮箱等) if (canViewFull) { columns.push( { title: '密码', dataIndex: 'password', width: 120 }, { title: '邮箱', dataIndex: 'email' }, { title: '邮箱密码', dataIndex: 'email_password', width: 120 }, ); } columns.push({ title: '分配给', dataIndex: 'assigned_username', render: (_: unknown, record: AccountItem) => { if (canAssign) { return ( setTagFilter(val || '')} options={tags.map((t) => ({ value: t, label: t }))} prefix={} /> {canImport && ( <> {canDelete && ( )} )} a.assigned_to).length} /> !a.assigned_to).length} /> setSelectedRowKeys(keys), } : undefined} columns={columns} dataSource={accounts} rowKey="id" loading={loading} size="small" pagination={{ current: currentPage, pageSize, showSizeChanger: true, showTotal: (t) => `共 ${t} 条`, onChange: (page, size) => { setCurrentPage(page); if (size !== pageSize) { setPageSize(size); localStorage.setItem('account_page_size', String(size)); setCurrentPage(1); // 切换 pageSize 时重置到第1页 } }, }} /> setImportOpen(false)} onOk={handleImport} confirmLoading={importing} okText="导入" width={600} > 格式:用户名|密码|邮箱|邮箱密码|标签(可选,每行一个)