feat(accounts): 账号列表按列排序,默认把还能换套装的排到前面
分页在服务端,点表头按全库排,不能只排当前这一页。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -35,6 +35,8 @@ export const accountApi = {
|
||||
elite_opened?: boolean;
|
||||
esports_opened?: boolean;
|
||||
role_bind_status?: string;
|
||||
sort?: 'username' | 'status' | 'bind' | 'points' | 'high_value' | 'exchange' | 'tag';
|
||||
order?: 'asc' | 'desc';
|
||||
},
|
||||
) =>
|
||||
api.get<PaginatedResponse<AccountItem>, PaginatedResponse<AccountItem>>('/accounts', { params }),
|
||||
|
||||
@@ -52,6 +52,25 @@ const { Text } = Typography;
|
||||
* 取值域见后端 `services/account_lifecycle.py`(6 个状态,不含 relogin ——
|
||||
* 「退避中」是 invalid 的调度细节,不单独成状态)。
|
||||
*/
|
||||
type AccountSortKey =
|
||||
| 'username'
|
||||
| 'status'
|
||||
| 'bind'
|
||||
| 'points'
|
||||
| 'high_value'
|
||||
| 'exchange'
|
||||
| 'tag';
|
||||
|
||||
const ACCOUNT_SORT_KEYS = new Set<string>([
|
||||
'username',
|
||||
'status',
|
||||
'bind',
|
||||
'points',
|
||||
'high_value',
|
||||
'exchange',
|
||||
'tag',
|
||||
]);
|
||||
|
||||
const STATUS_TONES: Record<string, string> = {
|
||||
imported: 'default',
|
||||
active: 'green',
|
||||
@@ -222,6 +241,9 @@ export default function AccountsPage() {
|
||||
return v ? Number(v) || 20 : 20;
|
||||
});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
// 打开列表就按套装价值排:两个都还能换的在前,没查过的在后。点表头可换方向。
|
||||
const [sortKey, setSortKey] = useState<AccountSortKey | ''>('high_value');
|
||||
const [sortOrder, setSortOrder] = useState<'ascend' | 'descend' | null>('ascend');
|
||||
const { can } = usePermissions();
|
||||
|
||||
const canViewFull = can('account:view_full');
|
||||
@@ -251,6 +273,10 @@ export default function AccountsPage() {
|
||||
if (eliteOpened !== undefined) params.elite_opened = eliteOpened;
|
||||
if (esportsOpened !== undefined) params.esports_opened = esportsOpened;
|
||||
if (roleBindStatus) params.role_bind_status = roleBindStatus;
|
||||
if (sortKey && sortOrder) {
|
||||
params.sort = sortKey;
|
||||
params.order = sortOrder === 'descend' ? 'desc' : 'asc';
|
||||
}
|
||||
|
||||
const data = await accountApi.listPaged(params);
|
||||
setAccounts(data.items);
|
||||
@@ -275,6 +301,8 @@ export default function AccountsPage() {
|
||||
maxPoints,
|
||||
minGold,
|
||||
maxGold,
|
||||
sortKey,
|
||||
sortOrder,
|
||||
eliteOpened,
|
||||
esportsOpened,
|
||||
roleBindStatus,
|
||||
@@ -503,11 +531,19 @@ export default function AccountsPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const sortProps = (key: AccountSortKey, tip?: string) => ({
|
||||
key,
|
||||
sorter: true,
|
||||
sortOrder: sortKey === key ? sortOrder : null,
|
||||
showSorterTooltip: tip ? { title: tip } : true,
|
||||
});
|
||||
|
||||
const columns: TableProps<AccountItem>['columns'] = [
|
||||
{
|
||||
// ID 不再单独占一列(客服不看),降到用户名下面做小字 —— 报障时还能念出来。
|
||||
title: '账号',
|
||||
dataIndex: 'username',
|
||||
...sortProps('username'),
|
||||
width: 200,
|
||||
// 用户名长短不一:定宽 + 省略号,别让它把整个表格撑开
|
||||
ellipsis: true,
|
||||
@@ -532,6 +568,7 @@ export default function AccountsPage() {
|
||||
// (检测时间、以及 `error` 这种「状态正常但检测没查明白」的情况)。
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
...sortProps('status', '正常在前'),
|
||||
width: 180,
|
||||
render: (status: string, record: AccountItem) => {
|
||||
const tone = STATUS_TONES[status] ?? 'default';
|
||||
@@ -564,6 +601,7 @@ export default function AccountsPage() {
|
||||
// 三角色绑定:三个作用域各一列,合成一格紧凑展示
|
||||
title: '绑定',
|
||||
dataIndex: 'role_bind_status',
|
||||
...sortProps('bind', '已绑定在前,先看精英,再看电竞和小店'),
|
||||
width: 150,
|
||||
render: (_, record: AccountItem) => {
|
||||
// 三个作用域全都 unknown 时不再显示「精? 电? 店?」(满屏问号只是噪声),
|
||||
@@ -602,6 +640,7 @@ export default function AccountsPage() {
|
||||
{
|
||||
title: '积分 / 鱼翅',
|
||||
dataIndex: 'points',
|
||||
...sortProps('points', '按积分排,没查过的在后;积分相同再看鱼翅'),
|
||||
width: 120,
|
||||
render: (_, record: AccountItem) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
@@ -619,6 +658,7 @@ export default function AccountsPage() {
|
||||
// 数据来自一键查询拉的**斗鱼官方兑换记录**。
|
||||
title: '高价值套装',
|
||||
dataIndex: 'high_value_owned',
|
||||
...sortProps('high_value', '两个都可兑换在前,未查询在后'),
|
||||
width: 170,
|
||||
render: (owned: string[] | undefined, record: AccountItem) => {
|
||||
const used = owned ?? [];
|
||||
@@ -655,6 +695,7 @@ export default function AccountsPage() {
|
||||
// 点这一格弹窗看全部(按作用域分组),不用进详情页。
|
||||
title: '兑换记录',
|
||||
dataIndex: 'exchange_count',
|
||||
...sortProps('exchange', '未兑换在前'),
|
||||
width: 170,
|
||||
render: (_, record: AccountItem) => {
|
||||
if (!record.exchange_count) {
|
||||
@@ -689,6 +730,7 @@ export default function AccountsPage() {
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'tag',
|
||||
...sortProps('tag'),
|
||||
width: 190,
|
||||
render: (tag: string, record: AccountItem) => {
|
||||
// 有权限时直接就地改(Select 输入新标签即保存);无权限则只读展示。
|
||||
@@ -1354,6 +1396,20 @@ export default function AccountsPage() {
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="small"
|
||||
onChange={(_pagination, _filters, sorter, extra) => {
|
||||
if (extra.action !== 'sort') return;
|
||||
const current = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||
const nextOrder = current?.order ?? null;
|
||||
const nextKey = nextOrder ? String(current?.columnKey ?? '') : '';
|
||||
if (ACCOUNT_SORT_KEYS.has(nextKey)) {
|
||||
setSortKey(nextKey as AccountSortKey);
|
||||
setSortOrder(nextOrder);
|
||||
} else {
|
||||
setSortKey('');
|
||||
setSortOrder(null);
|
||||
}
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
|
||||
Reference in New Issue
Block a user