diff --git a/web/frontend/src/pages/HuyaAccountsPage.tsx b/web/frontend/src/pages/HuyaAccountsPage.tsx index fbf6939..e6e8448 100644 --- a/web/frontend/src/pages/HuyaAccountsPage.tsx +++ b/web/frontend/src/pages/HuyaAccountsPage.tsx @@ -1,10 +1,41 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { - Alert, Button, Card, Col, Divider, Input, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, Tooltip, Typography, + Alert, + Avatar, + Button, + Card, + Input, + Modal, + Popconfirm, + Select, + Space, + Table, + Tag, + Tooltip, + Typography, + theme, } from 'antd'; import { message } from '../utils/antdMessage'; import type { TableProps } from 'antd'; -import { DeleteOutlined, FilterOutlined, ImportOutlined, LoginOutlined, MailOutlined, MobileOutlined, ReloadOutlined, SearchOutlined, TagOutlined } from '@ant-design/icons'; +import { + CheckCircleOutlined, + ClockCircleOutlined, + CopyOutlined, + DeleteOutlined, + FilterOutlined, + GiftOutlined, + ImportOutlined, + KeyOutlined, + LoginOutlined, + MailOutlined, + MobileOutlined, + ReloadOutlined, + SafetyCertificateOutlined, + SearchOutlined, + TagOutlined, + TrophyOutlined, + UserOutlined, +} from '@ant-design/icons'; import { huyaApi, type HuyaAccountItem, @@ -28,6 +59,7 @@ const STATUS_LABELS: Record = { login_failed: '登录失败', active: '正常', invalid: '失效', + exchange_record: '已兑换', }; // 登录渠道展示 (对应后端 HuyaAccount.login_channel: web/app/sms) @@ -46,9 +78,34 @@ const STATUS_COLORS: Record = { login_failed: 'error', active: 'success', invalid: 'error', + exchange_record: 'purple', }; +function copyToClipboard(text: string): Promise { + if (navigator.clipboard?.writeText) { + return navigator.clipboard.writeText(text); + } + return new Promise((resolve, reject) => { + const ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.left = '-9999px'; + document.body.appendChild(ta); + ta.select(); + try { + const ok = document.execCommand('copy'); + document.body.removeChild(ta); + if (ok) resolve(); + else reject(new Error('copy failed')); + } catch (e) { + document.body.removeChild(ta); + reject(e); + } + }); +} + export default function HuyaAccountsPage() { + const { token } = theme.useToken(); const [accounts, setAccounts] = useState([]); const [users, setUsers] = useState([]); const [tags, setTags] = useState([]); @@ -532,24 +589,88 @@ export default function HuyaAccountsPage() { ]; const columns: TableProps['columns'] = [ - { title: 'ID', dataIndex: 'id', width: 70, align: 'center' }, + { + title: 'ID', + dataIndex: 'id', + width: 60, + align: 'center', + render: (id: number) => ( + + {id} + + ), + }, { title: '虎牙账号', - width: 180, - render: (_: unknown, record) => ( - - {record.nickname || record.username || record.uid || '-'} - - UID {record.uid || record.yyuid || '-'} - - {record.has_password ? 已导入密码 : null} - - ), + width: 210, + render: (_: unknown, record) => { + const name = record.nickname || record.username || '-'; + const initial = (name.replace(/^hy_/, '') || 'H').slice(0, 1).toUpperCase(); + return ( + + + {initial} + + +
+ + {name} + + {record.has_password ? ( + + + 密 + + + ) : null} +
+
+ + UID {record.uid || record.yyuid || '未同步'} + + {(record.uid || record.yyuid) && ( + +
+
+
+ ); + }, }, { title: '标签', dataIndex: 'tag', - width: 170, + width: 150, render: (tag: string, record) => { if (canTag) { return ( @@ -560,8 +681,8 @@ export default function HuyaAccountsPage() { showSearch optionFilterProp="label" size="small" - placeholder="选择或输入标签" - style={{ width: 150 }} + placeholder="+ 标签" + style={{ width: 130 }} value={tag ? [tag] : []} onChange={(values) => { const value = values.at(-1)?.trim() || ''; @@ -571,65 +692,133 @@ export default function HuyaAccountsPage() { /> ); } - return tag ? {tag} : -; + return tag ? ( + + {tag} + + ) : ( + - + ); }, }, { title: '积分', dataIndex: 'points', - width: 90, + width: 85, align: 'center', - render: (points: number | null) => points ?? 未查, + render: (points: number | null) => + points !== null && points !== undefined ? ( + 0 ? '#52c41a' : undefined, fontSize: 13 }}> + {points} + + ) : ( + + 未查 + + ), }, { - title: '游戏名', + 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: 150, ellipsis: true, - render: (value: string, record) => ( - - {value || '-'} + render: (value: string, record) => + value ? ( + + {value} + {record.game_channel ? ( + + {record.game_channel} + + ) : null} + + ) : ( + + 未查 + + ), + }, + { + title: '手机号 / 接码', + width: 170, + render: (_: unknown, record) => ( + + + {record.game_phone || '-'} + {record.sms_url ? ( - - 接码 + + } + > + 自动接码 + - ) : null} + ) : ( + + 未绑接码 + + )} ), }, { - title: 'Cookie', - dataIndex: 'cookie_preview', - ellipsis: true, - render: (value: string) => ( - - {canViewCookie ? (value || '-') : '***'} - - ), + title: 'Cookie 凭据', + width: 110, + align: 'center', + render: (_: unknown, record) => { + const hasCookie = Boolean(record.has_cookie || record.cookie_preview || record.cookie); + if (!hasCookie) { + return ( + + 未获取 + + ); + } + return ( + + { + if (canViewCookie && (record.cookie_preview || record.cookie)) { + copyToClipboard(record.cookie_preview || record.cookie || ''); + message.success('Cookie 凭据已复制到剪贴板'); + } + }} + > + 已就绪 {canViewCookie ? : null} + + + ); + }, }, { - title: '分配给', + title: '分配客服', dataIndex: 'assigned_username', - width: 150, + width: 130, render: (_: unknown, record) => { if (canAssign) { return ( { - setTagFilter(value || ''); - setCurrentPage(1); + {/* 页面头部操作栏 */} +
+ + + 虎牙账号管理 + + ({ value: tag, label: tag }))} - prefix={} - /> - - {canTag && ( + > + 共 {total} 个账号 + + + + + {canImport && ( )} - {canImport && } {canImport && ( - - - - )} - {canImport && ( - - - - )} - {canImport && } - {canDelete && selectedCount > 0 && ( - - - + )} {canImport && ( - )} - {canImport && ( - - )} +
- - - - - - - - - - - - - - - - - - - - - - - - -
- { - setSearchText(e.target.value); - setCurrentPage(1); + {/* KPI 数据卡片栏 (整齐等宽响应式栅格,彻底告别空缺折行) */} +
+
} - /> - {tags.map((tag) => ( - { - setSearchText(tag); - setCurrentPage(1); + > +
- {tag} - - ))} + 账号总数 + + + +
+
+ {summary.total} +
+
+ +
+
+ 已存密码 + + + +
+
+ {summary.password_ready_count} +
+
+ +
+
+ 已查积分 + + + +
+
+ {summary.point_count} +
+
+ +
+
+ 已绑礼盒 + + + +
+
+ {summary.bound_count} +
+
+ +
+
+ 标签数 + + + +
+
+ {summary.tag_count || tags.length} +
+
+ +
+
+ 已分配 + + + +
+
+ {summary.assigned_count} +
+
+ +
+
+ 未分配 + + + +
+
+ {summary.unassigned_count} +
+
- selectedRowKeys.length > 0 ? ( - +
+ + { + setSearchText(e.target.value); + setCurrentPage(1); + }} + style={{ width: 300 }} + prefix={} + /> +
{ + setSelectedRowKeys(keys); + setSelectedAllMatching(false); + }, + preserveSelectedRowKeys: true, + } + : undefined + } + columns={columns} + dataSource={accounts} + rowKey="id" + loading={loading} + size="middle" + pagination={{ + current: currentPage, + pageSize, + total, + showSizeChanger: true, + showTotal: (t) => `共 ${t} 个账号`, + onChange: (page, size) => { + setCurrentPage(page); + if (size !== pageSize) { + setPageSize(size); + localStorage.setItem('huya_account_page_size', String(size)); + setCurrentPage(1); + } + }, + }} + scroll={{ x: 1160 }} + /> +