import { useEffect, useState, useMemo, useCallback } from 'react'; import { Alert, Table, Button, Modal, Input, Select, Popconfirm, Typography, Tag, Space, Row, Col, Card, Statistic, } from 'antd'; import { message } from '../utils/antdMessage'; import type { TableProps } from 'antd'; import { ImportOutlined, DeleteOutlined, TagOutlined, FilterOutlined, EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; import { accountApi, userApi, type AccountItem, type BasicSummary, 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 [importTag, setImportTag] = useState(''); const [importing, setImporting] = useState(false); const [tagFilter, setTagFilter] = useState(''); const [searchText, setSearchText] = useState(''); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [selectedAllMatching, setSelectedAllMatching] = useState(false); const [batchTagInput, setBatchTagInput] = useState(''); const [batchTagVisible, setBatchTagVisible] = useState(false); const [sensitiveVisible, setSensitiveVisible] = useState(false); const [total, setTotal] = useState(0); const [summary, setSummary] = useState({ total: 0, assigned_count: 0, unassigned_count: 0, tag_count: 0 }); 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 canTag = can('account:tag') || canImport; const canAssign = can('account:assign'); const canDelete = can('account:delete'); const canSelectRows = canTag || canDelete || canAssign; const loadAccounts = useCallback(async () => { setLoading(true); try { const params: { tag?: string } = {}; if (tagFilter) params.tag = tagFilter; const data = await accountApi.listPaged({ ...params, page: currentPage, page_size: pageSize, search: searchText.trim() || undefined, include_sensitive: canViewFull && sensitiveVisible, }); setAccounts(data.items); setTotal(data.total); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setLoading(false); } }, [canViewFull, currentPage, pageSize, searchText, sensitiveVisible, tagFilter]); const loadSummary = useCallback(async () => { try { const data = await accountApi.summary(); setSummary(data); } catch { // 统计加载失败时不影响主列表操作。 } }, []); 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(); }, [loadAccounts]); useEffect(() => { if (canAssign) loadUsers(); loadTags(); loadSummary(); }, [canAssign, loadUsers, loadTags, loadSummary]); const tagColorMap = useMemo(() => { const map: Record = {}; tags.forEach((t, i) => { map[t] = TAG_COLORS[i % TAG_COLORS.length]; }); return map; }, [tags]); const selectedCount = selectedAllMatching ? total : selectedRowKeys.length; const selectionPayload = useMemo(() => ({ account_ids: selectedAllMatching ? [] : selectedRowKeys.map((key) => Number(key)), all_matching: selectedAllMatching, search: searchText.trim(), tag: tagFilter, }), [searchText, selectedAllMatching, selectedRowKeys, tagFilter]); const clearSelection = () => { setSelectedRowKeys([]); setSelectedAllMatching(false); }; useEffect(() => { clearSelection(); }, [searchText, tagFilter]); const handleImport = async () => { if (!importText.trim()) { message.warning('请输入账号数据'); return; } setImporting(true); try { const result = await accountApi.import(importText, importTag); message.success(result.message); setImportOpen(false); setImportText(''); setImportTag(''); loadAccounts(); loadTags(); loadSummary(); } 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 (selectedCount === 0) { message.warning('请先选择账号'); return; } try { const result = await accountApi.batchTagSelection({ ...selectionPayload, tag_value: batchTagInput }); message.success(result.message); setBatchTagVisible(false); setBatchTagInput(''); clearSelection(); loadAccounts(); loadTags(); loadSummary(); } 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)); setSelectedAllMatching(false); loadAccounts(); loadTags(); loadSummary(); } catch (e: unknown) { message.error(getErrorMessage(e)); } }; const handleBatchDelete = async () => { if (selectedCount === 0) { message.warning('请先选择账号'); return; } try { const result = await accountApi.batchDeleteSelection(selectionPayload); message.success(result.message); clearSelection(); loadAccounts(); loadTags(); loadSummary(); } catch (e: unknown) { message.error(getErrorMessage(e)); } }; const handleSensitiveVisibility = () => { if (sensitiveVisible) { // 立即清除已加载的敏感内容,再由 effect 重新获取脱敏列表。 setAccounts((previous) => previous.map((account) => ({ ...account, password: undefined, email: undefined, email_password: undefined, }))); setSensitiveVisible(false); return; } Modal.confirm({ title: '显示账号凭据?', content: '密码、邮箱和邮箱密码将临时显示在当前页面。关闭或刷新页面后会恢复脱敏。', okText: '显示', cancelText: '取消', onOk: () => setSensitiveVisible(true), }); }; const columns: TableProps['columns'] = [ { title: 'ID', dataIndex: 'id', width: 60 }, { title: '用户名', dataIndex: 'username' }, { title: '标签', dataIndex: 'tag', width: 170, render: (tag: string, record: AccountItem) => { if (canTag) { return ( handleAssign(record.id, val ?? null)} options={users.map((u) => ({ value: u.id, label: u.username }))} /> ); } return record.assigned_username || 未分配; }, }); if (canDelete) { columns.push({ title: '操作', width: 80, render: (_: unknown, record: AccountItem) => ( handleDelete(record.id)}> ), }); } return (

账号管理