import { useCallback, useEffect, useMemo, useState } from 'react'; import { Button, Card, Col, Input, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, Typography, } from 'antd'; import { message } from '../utils/antdMessage'; import type { TableProps } from 'antd'; import { DeleteOutlined, FilterOutlined, ImportOutlined, LoginOutlined, MobileOutlined, ReloadOutlined, SearchOutlined, TagOutlined } from '@ant-design/icons'; import { huyaApi, type HuyaAccountItem, type HuyaPasswordLoginBatchItem, type SupportUserItem, } from '../api/modules'; import { usePermissions } from '../hooks/usePermissions'; import { formatTime } from '../utils/time'; import { getErrorMessage } from '../utils/error'; const { Text, Paragraph } = Typography; const { TextArea } = Input; const STATUS_LABELS: Record = { imported: '已导入', updated: '已更新', password_imported: '待登录', login_success: '登录成功', password_changed: '已改密', login_failed: '登录失败', active: '正常', invalid: '失效', }; const STATUS_COLORS: Record = { imported: 'blue', updated: 'cyan', password_imported: 'warning', login_success: 'success', password_changed: 'success', login_failed: 'error', active: 'success', invalid: 'error', }; export default function HuyaAccountsPage() { 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 [passwordImportOpen, setPasswordImportOpen] = useState(false); const [passwordImportText, setPasswordImportText] = useState(''); const [passwordImportTag, setPasswordImportTag] = useState([]); const [passwordImporting, setPasswordImporting] = useState(false); const [passwordLoginResultOpen, setPasswordLoginResultOpen] = useState(false); const [passwordLogging, setPasswordLogging] = useState(false); const [passwordLoginResults, setPasswordLoginResults] = useState([]); const [smsLoginOpen, setSmsLoginOpen] = useState(false); const [smsPhone, setSmsPhone] = useState(''); const [smsCode, setSmsCode] = useState(''); const [smsTag, setSmsTag] = useState([]); const [smsState, setSmsState] = useState(''); const [smsSending, setSmsSending] = useState(false); const [smsLogging, setSmsLogging] = useState(false); const [searchText, setSearchText] = useState(''); 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('huya_account_page_size'); return v ? Number(v) || 20 : 20; }); const [currentPage, setCurrentPage] = useState(1); 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 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(); if (canAssign) loadUsers(); loadTags(); }, [loadAccounts, canAssign, loadUsers, loadTags]); const tagColorMap = useMemo(() => { const map: Record = {}; 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(); if (!s) return accounts; return accounts.filter((item) => ( item.uid.toLowerCase().includes(s) || item.yyuid.toLowerCase().includes(s) || item.username.toLowerCase().includes(s) || item.nickname.toLowerCase().includes(s) || item.tag.toLowerCase().includes(s) || item.game_name.toLowerCase().includes(s) || item.game_phone.toLowerCase().includes(s) )); }, [accounts, searchText]); const handleImport = async () => { if (!importText.trim()) { message.warning('请先粘贴虎牙 CK'); return; } setImporting(true); try { const result = await huyaApi.importCookies(importText, importTag); message.success(result.message); setImportOpen(false); setImportText(''); setImportTag(''); loadAccounts(); loadTags(); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setImporting(false); } }; const openPasswordImport = () => { setPasswordImportText(''); setPasswordImportTag([]); setPasswordLoginResults([]); setPasswordImportOpen(true); }; const openSmsLogin = () => { setSmsPhone(''); setSmsCode(''); setSmsTag([]); setSmsState(''); setSmsLoginOpen(true); }; const handleSendSmsCode = async () => { const phone = smsPhone.trim(); if (!phone) { message.warning('请先输入手机号'); return; } setSmsSending(true); try { const result = await huyaApi.smsCode({ phone }); setSmsState(result.state); message.success(result.message || '短信已发送'); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setSmsSending(false); } }; const handleSmsLogin = async () => { const phone = smsPhone.trim(); const authcode = smsCode.trim(); if (!smsState) { message.warning('请先发送短信验证码'); return; } if (!authcode) { message.warning('请先输入短信验证码'); return; } setSmsLogging(true); try { const tag = smsTag.length > 0 ? smsTag[smsTag.length - 1].trim() : ''; const result = await huyaApi.smsLogin({ phone, authcode, state: smsState, tag, }); message.success(result.message || '登录成功,Cookie 已保存'); setSmsLoginOpen(false); setSmsPhone(''); setSmsCode(''); setSmsTag([]); setSmsState(''); loadAccounts(); loadTags(); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setSmsLogging(false); } }; const handleImportPasswordAccounts = async () => { if (!passwordImportText.trim()) { message.warning('请先粘贴虎牙账号密码'); return; } setPasswordImporting(true); try { const tag = passwordImportTag.length > 0 ? passwordImportTag[passwordImportTag.length - 1].trim() : ''; const result = await huyaApi.importPasswordAccounts(passwordImportText, tag); message.success(result.message); setPasswordImportOpen(false); setPasswordImportText(''); setPasswordImportTag([]); loadAccounts(); loadTags(); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setPasswordImporting(false); } }; const handleLoginSelected = async () => { if (selectedRowKeys.length === 0) { message.warning('请先选择虎牙账号'); return; } setPasswordLogging(true); try { const result = await huyaApi.passwordLoginSelected({ account_ids: selectedRowKeys.map((key) => Number(key)), }); setPasswordLoginResults(result.results || []); setPasswordLoginResultOpen(true); if (result.failed > 0) { message.warning(result.message); } else { message.success(result.message || '登录成功,Cookie 已保存'); } loadAccounts(); loadTags(); } catch (e: unknown) { message.error(getErrorMessage(e)); } finally { setPasswordLogging(false); } }; const handleDelete = async (id: number) => { try { await huyaApi.deleteAccount(id); message.success('已删除'); setSelectedRowKeys((prev) => prev.filter((key) => key !== id)); loadAccounts(); loadTags(); } catch (e: unknown) { message.error(getErrorMessage(e)); } }; const handleDeleteSelected = async () => { if (selectedRowKeys.length === 0) { message.warning('请先选择虎牙账号'); return; } try { const result = await huyaApi.deleteAccounts(selectedRowKeys.map((key) => Number(key))); 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)); } }; 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 passwordReadyCount = accounts.filter((item) => item.has_password).length; const passwordLoginResultColumns: TableProps['columns'] = [ { title: '账号ID', dataIndex: 'line', width: 80, align: 'center' }, { title: '账号', dataIndex: 'username', width: 160, ellipsis: true }, { title: '状态', dataIndex: 'success', width: 90, align: 'center', render: (success: boolean) => ( {success ? '成功' : '失败'} ), }, { title: '保存账号', width: 160, ellipsis: true, render: (_: unknown, record) => record.account?.nickname || record.account?.username || record.account?.uid || '-', }, { title: '提示', dataIndex: 'message', ellipsis: true }, ]; const columns: TableProps['columns'] = [ { title: 'ID', dataIndex: 'id', width: 70, align: 'center' }, { title: '虎牙账号', width: 180, render: (_: unknown, record) => ( {record.nickname || record.username || record.uid || '-'} UID {record.uid || record.yyuid || '-'} {record.has_password ? 已导入密码 : null} ), }, { title: '标签', dataIndex: 'tag', width: 110, render: (tag: string, record) => { if (!tag) { if (canImport) { return ( { 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 -; } if (canImport) { return ( { e.preventDefault(); handleSetTag(record.id, ''); }} > {tag} ); } return {tag}; }, }, { title: '积分', dataIndex: 'points', width: 90, align: 'center', render: (points: number | null) => points ?? 未查, }, { title: '游戏名', dataIndex: 'game_name', width: 160, ellipsis: true, render: (value: string, record) => value ? ( {value} {record.game_channel ? ( {record.game_channel} ) : null} ) : 未查, }, { title: '手机号', dataIndex: 'game_phone', width: 140, ellipsis: true, render: (value: string) => value || -, }, { title: 'Cookie', dataIndex: 'cookie_preview', ellipsis: true, render: (value: string) => ( {canViewCookie ? (value || '-') : '***'} ), }, { title: '分配给', dataIndex: 'assigned_username', width: 150, render: (_: unknown, record) => { if (canAssign) { return ( setTagFilter(value || '')} options={tags.map((tag) => ({ value: tag, label: tag }))} prefix={} /> {canImport && ( )} {canImport && ( )} {canDelete && selectedRowKeys.length > 0 && ( )} {canImport && ( )} {canImport && ( )} {canImport && ( )}
setSearchText(e.target.value)} style={{ width: 300 }} prefix={} /> {tags.map((tag) => ( setSearchText(tag)} style={{ cursor: 'pointer' }}> {tag} ))}
setSelectedRowKeys(keys), } : undefined} columns={columns} dataSource={filteredAccounts} rowKey="id" loading={loading} size="small" pagination={{ current: currentPage, pageSize, showSizeChanger: true, showTotal: (total) => `共 ${total} 条`, onChange: (page, size) => { setCurrentPage(page); if (size !== pageSize) { setPageSize(size); localStorage.setItem('huya_account_page_size', String(size)); setCurrentPage(1); } }, }} scroll={{ x: 1120 }} /> setImportOpen(false)} onOk={handleImport} okText="导入" confirmLoading={importing} width={720} > setImportTag(e.target.value)} />