perf(assignments): 优化分配管理页面加载性能与角标数量展示
- 前端改造为服务端分页查询 (accountApi.listPaged),避免一次性拉取 1.6 万+ 账号导致页面卡死 - 优化后端 assignments_summary 聚合查询,消除大子查询临时表 - 修复 Badge 默认 99+ 限制,设置 overflowCount 支持展示完整账号数量 - 修复测试用例 test_business_events.py 当前时间跨天断言问题
This commit is contained in:
@@ -454,8 +454,9 @@ class TestPointsExpireJob(_Base):
|
||||
def test_expired_points_are_recorded_once(self):
|
||||
from datetime import timedelta
|
||||
|
||||
now = datetime.now(UTC)
|
||||
self.account.points = 500
|
||||
self.account.points_expire_at = NOW - timedelta(days=1)
|
||||
self.account.points_expire_at = now - timedelta(days=1)
|
||||
self.session.commit()
|
||||
|
||||
out = self._run()
|
||||
@@ -472,8 +473,9 @@ class TestPointsExpireJob(_Base):
|
||||
def test_not_yet_expired_is_left_alone(self):
|
||||
from datetime import timedelta
|
||||
|
||||
now = datetime.now(UTC)
|
||||
self.account.points = 500
|
||||
self.account.points_expire_at = NOW + timedelta(days=3)
|
||||
self.account.points_expire_at = now + timedelta(days=3)
|
||||
self.session.commit()
|
||||
|
||||
assert self._run()["written"] == 0
|
||||
@@ -484,8 +486,9 @@ class TestPointsExpireJob(_Base):
|
||||
"""积分已经为 0 就没有东西可到期 —— 写一条 -0 只是噪声。"""
|
||||
from datetime import timedelta
|
||||
|
||||
now = datetime.now(UTC)
|
||||
self.account.points = 0
|
||||
self.account.points_expire_at = NOW - timedelta(days=1)
|
||||
self.account.points_expire_at = now - timedelta(days=1)
|
||||
self.session.commit()
|
||||
|
||||
out = self._run()
|
||||
|
||||
@@ -124,7 +124,7 @@ def _filter_accounts_query(
|
||||
):
|
||||
"""复用列表筛选条件,供分页列表和全量批量操作保持一致。"""
|
||||
if _query_bool(has_cookie):
|
||||
query = query.filter(Account.id.in_(cookie_account_ids_query(db)))
|
||||
query = query.filter(Account.cookie.isnot(None), Account.cookie != "")
|
||||
if _query_bool(assigned_only) and user_has_permission(current, "account:view_all"):
|
||||
query = query.filter(Account.assigned_to.isnot(None))
|
||||
# 归属筛选:未分配 / 已分配,再叠加「指定客服」。与其它条件(标签、CK、
|
||||
@@ -1009,30 +1009,40 @@ def assignments_summary(
|
||||
current: User = Depends(require_permission("account:assign")),
|
||||
):
|
||||
"""分配概览:每个客服分配了多少账号(仅统计当前仍持有 CK 的账号)。"""
|
||||
cookie_subq = select(Account.id).where(cookie_holding_filter())
|
||||
cookie_accounts = db.query(Account).filter(Account.id.in_(cookie_subq)).subquery()
|
||||
|
||||
results = (
|
||||
db.query(
|
||||
User.id, User.username, func.count(cookie_accounts.c.id).label("count")
|
||||
db.query(User.id, User.username, func.count(Account.id).label("count"))
|
||||
.outerjoin(
|
||||
Account,
|
||||
and_(
|
||||
Account.assigned_to == User.id,
|
||||
Account.cookie.isnot(None),
|
||||
Account.cookie != "",
|
||||
),
|
||||
)
|
||||
.outerjoin(cookie_accounts, cookie_accounts.c.assigned_to == User.id)
|
||||
.filter(User.role == "support")
|
||||
.group_by(User.id, User.username)
|
||||
.order_by(func.count(cookie_accounts.c.id).desc())
|
||||
.order_by(func.count(Account.id).desc())
|
||||
.all()
|
||||
)
|
||||
total_unassigned = (
|
||||
db.query(func.count(cookie_accounts.c.id))
|
||||
.filter(cookie_accounts.c.assigned_to.is_(None))
|
||||
db.query(func.count(Account.id))
|
||||
.filter(
|
||||
Account.assigned_to.is_(None),
|
||||
Account.cookie.isnot(None),
|
||||
Account.cookie != "",
|
||||
)
|
||||
.scalar()
|
||||
) or 0
|
||||
total_assigned = sum(int(cnt or 0) for _, _, cnt in results)
|
||||
total_accounts = total_assigned + total_unassigned
|
||||
return {
|
||||
"support_users": [
|
||||
{"id": uid, "username": uname, "assigned_count": cnt}
|
||||
{"id": uid, "username": uname, "assigned_count": int(cnt or 0)}
|
||||
for uid, uname, cnt in results
|
||||
],
|
||||
"unassigned_count": total_unassigned,
|
||||
"assigned_count": total_assigned,
|
||||
"total_count": total_accounts,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -335,6 +335,8 @@ export interface AccountItem {
|
||||
export interface AssignmentsSummary {
|
||||
support_users: SupportUserItem[];
|
||||
unassigned_count: number;
|
||||
assigned_count?: number;
|
||||
total_count?: number;
|
||||
}
|
||||
|
||||
export interface SupportUserItem {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
Card, Table, Button, Select, Input, Tag, Row, Col, Statistic,
|
||||
Space, Tabs, Typography, Badge, theme,
|
||||
Space, Tabs, Typography, Badge, theme, Tooltip,
|
||||
} from 'antd';
|
||||
import { message } from '../utils/antdMessage';
|
||||
import {
|
||||
UserOutlined, CheckCircleOutlined, TeamOutlined,
|
||||
SwapOutlined, ClearOutlined, UsergroupAddOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { accountApi, type AccountItem, type SupportUserItem } from '../api/modules';
|
||||
import {
|
||||
accountApi,
|
||||
type AccountItem,
|
||||
type SupportUserItem,
|
||||
type AssignmentsSummary,
|
||||
} from '../api/modules';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -17,13 +22,19 @@ const STORAGE_KEY_SELECTED_USER = 'assignments_selected_user_id';
|
||||
|
||||
export default function AssignmentsPage() {
|
||||
const { token } = theme.useToken();
|
||||
const [accounts, setAccounts] = useState<AccountItem[]>([]);
|
||||
const [summary, setSummary] = useState<AssignmentsSummary | null>(null);
|
||||
const [supportUsers, setSupportUsers] = useState<SupportUserItem[]>([]);
|
||||
const [selectedUser, setSelectedUser] = useState<SupportUserItem | null>(null);
|
||||
const [allTags, setAllTags] = useState<string[]>([]);
|
||||
|
||||
// 表格分页与筛选状态
|
||||
const [pagedAccounts, setPagedAccounts] = useState<AccountItem[]>([]);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [tableLoading, setTableLoading] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [filterTag, setFilterTag] = useState<string | undefined>(undefined);
|
||||
const [activeTab, setActiveTab] = useState('unassigned');
|
||||
const [activeTab, setActiveTab] = useState<'unassigned' | 'assigned'>('unassigned');
|
||||
const [assigning, setAssigning] = useState(false);
|
||||
const [pageSize, setPageSize] = useState(() => {
|
||||
const v = localStorage.getItem('assignment_page_size');
|
||||
@@ -31,83 +42,86 @@ export default function AssignmentsPage() {
|
||||
});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
// 加载分配概览
|
||||
const loadSummary = async () => {
|
||||
// 加载分配概览统计
|
||||
const loadSummary = useCallback(async () => {
|
||||
try {
|
||||
const data = await accountApi.assignmentsSummary();
|
||||
setSummary(data);
|
||||
setSupportUsers(data.support_users);
|
||||
return data.support_users;
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
return [];
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 加载账号(仅已成功登录过的)
|
||||
const loadAccounts = async () => {
|
||||
// 加载标签列表
|
||||
const loadTags = useCallback(async () => {
|
||||
try {
|
||||
const all = await accountApi.list({ has_cookie: true, include_sensitive: false });
|
||||
setAccounts(all);
|
||||
const tags = await accountApi.listTags();
|
||||
setAllTags(tags);
|
||||
} catch {
|
||||
// 忽略标签加载异常
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 分页按需加载账号(服务端过滤与分页)
|
||||
const loadPagedAccounts = useCallback(async () => {
|
||||
setTableLoading(true);
|
||||
try {
|
||||
const params: Parameters<typeof accountApi.listPaged>[0] = {
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
has_cookie: true,
|
||||
include_sensitive: false,
|
||||
search: searchText.trim() || undefined,
|
||||
tag: filterTag || undefined,
|
||||
};
|
||||
|
||||
if (activeTab === 'unassigned') {
|
||||
params.assign_status = 'unassigned';
|
||||
} else if (selectedUser) {
|
||||
params.assigned_user_id = selectedUser.id;
|
||||
}
|
||||
|
||||
const data = await accountApi.listPaged(params);
|
||||
setPagedAccounts(data.items);
|
||||
setTotalCount(data.total);
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setTableLoading(false);
|
||||
}
|
||||
};
|
||||
}, [activeTab, selectedUser, currentPage, pageSize, searchText, filterTag]);
|
||||
|
||||
// 初始化加载 + 自动选中客服
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
loadAccounts();
|
||||
loadTags();
|
||||
const users = await loadSummary();
|
||||
if (users.length > 0) {
|
||||
const savedId = localStorage.getItem(STORAGE_KEY_SELECTED_USER);
|
||||
const savedUser = savedId ? users.find((u: SupportUserItem) => u.id === Number(savedId)) : null;
|
||||
const savedUser = savedId ? users.find((u) => u.id === Number(savedId)) : null;
|
||||
setSelectedUser(savedUser || users[0]);
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
}, [loadSummary, loadTags]);
|
||||
|
||||
// 标签列表
|
||||
const allTags = useMemo(() => {
|
||||
const tags = [...new Set(accounts.map((a) => (a.tag || '').trim()).filter(Boolean))];
|
||||
return tags.sort();
|
||||
}, [accounts]);
|
||||
|
||||
// 未分配账号
|
||||
const unassignedAccounts = useMemo(() => {
|
||||
return accounts.filter((a) => !a.assigned_to);
|
||||
}, [accounts]);
|
||||
|
||||
// 当前选中客服的已分配账号
|
||||
const assignedAccounts = useMemo(() => {
|
||||
if (!selectedUser) return [];
|
||||
return accounts.filter((a) => a.assigned_to === selectedUser.id);
|
||||
}, [accounts, selectedUser]);
|
||||
|
||||
// 当前显示的账号集合
|
||||
const displayAccounts = useMemo(() => {
|
||||
let list = activeTab === 'unassigned' ? unassignedAccounts : assignedAccounts;
|
||||
if (searchText) {
|
||||
const s = searchText.toLowerCase();
|
||||
list = list.filter((a) => a.username.toLowerCase().includes(s));
|
||||
}
|
||||
if (filterTag) {
|
||||
list = list.filter((a) => (a.tag || '').trim() === filterTag);
|
||||
}
|
||||
return list;
|
||||
}, [activeTab, unassignedAccounts, assignedAccounts, searchText, filterTag]);
|
||||
// 当筛选或选中客服变化时触发分页请求
|
||||
useEffect(() => {
|
||||
loadPagedAccounts();
|
||||
}, [loadPagedAccounts]);
|
||||
|
||||
// 切换客服
|
||||
const handleSelectUser = (user: SupportUserItem) => {
|
||||
setSelectedUser(user);
|
||||
setSelectedRowKeys([]);
|
||||
setActiveTab('unassigned');
|
||||
setSearchText('');
|
||||
setFilterTag(undefined);
|
||||
setCurrentPage(1);
|
||||
localStorage.setItem(STORAGE_KEY_SELECTED_USER, String(user.id));
|
||||
};
|
||||
|
||||
// 批量分配
|
||||
// 批量分配选中账号
|
||||
const handleBatchAssign = async () => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('请先选择账号');
|
||||
@@ -122,10 +136,8 @@ export default function AssignmentsPage() {
|
||||
await accountApi.batchAssign(selectedRowKeys, selectedUser.id);
|
||||
message.success(`已将 ${selectedRowKeys.length} 个账号分配给 ${selectedUser.username}`);
|
||||
setSelectedRowKeys([]);
|
||||
await loadAccounts();
|
||||
const users = await loadSummary();
|
||||
const refreshed = users.find((u: SupportUserItem) => u.id === selectedUser.id);
|
||||
if (refreshed) setSelectedUser(refreshed);
|
||||
await loadSummary();
|
||||
await loadPagedAccounts();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
@@ -144,10 +156,8 @@ export default function AssignmentsPage() {
|
||||
await accountApi.batchAssign(selectedRowKeys, null);
|
||||
message.success(`已取消 ${selectedRowKeys.length} 个账号的分配`);
|
||||
setSelectedRowKeys([]);
|
||||
await loadAccounts();
|
||||
const users = await loadSummary();
|
||||
const refreshed = users.find((u: SupportUserItem) => u.id === selectedUser?.id);
|
||||
if (refreshed) setSelectedUser(refreshed);
|
||||
await loadSummary();
|
||||
await loadPagedAccounts();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
@@ -155,31 +165,29 @@ export default function AssignmentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 一键将未分配账号全部分配给选中客服
|
||||
// 一键将当前未分配(含搜索/标签筛选)全部分配给选中客服
|
||||
const handleAssignAllUnassigned = async () => {
|
||||
if (!selectedUser) {
|
||||
message.warning('请先选择客服');
|
||||
return;
|
||||
}
|
||||
const ids = unassignedAccounts
|
||||
.filter((a) => {
|
||||
if (searchText && !a.username.toLowerCase().includes(searchText.toLowerCase())) return false;
|
||||
if (filterTag && (a.tag || '').trim() !== filterTag) return false;
|
||||
return true;
|
||||
})
|
||||
.map((a) => a.id);
|
||||
if (ids.length === 0) {
|
||||
message.info('没有未分配的账号');
|
||||
if (totalCount === 0) {
|
||||
message.info('当前筛选下没有未分配的账号');
|
||||
return;
|
||||
}
|
||||
setAssigning(true);
|
||||
try {
|
||||
await accountApi.batchAssign(ids, selectedUser.id);
|
||||
message.success(`已将 ${ids.length} 个账号分配给 ${selectedUser.username}`);
|
||||
await loadAccounts();
|
||||
const users = await loadSummary();
|
||||
const refreshed = users.find((u: SupportUserItem) => u.id === selectedUser.id);
|
||||
if (refreshed) setSelectedUser(refreshed);
|
||||
const res = await accountApi.batchAssignSelection({
|
||||
all_matching: true,
|
||||
assign_status: 'unassigned',
|
||||
tag: filterTag || undefined,
|
||||
search: searchText.trim() || undefined,
|
||||
assigned_to: selectedUser.id,
|
||||
});
|
||||
message.success(res.message || `已全部分配给 ${selectedUser.username}`);
|
||||
setSelectedRowKeys([]);
|
||||
await loadSummary();
|
||||
await loadPagedAccounts();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
@@ -187,18 +195,52 @@ export default function AssignmentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 统计
|
||||
const assignedCount = accounts.filter((a) => !!a.assigned_to).length;
|
||||
const unassignedCount = accounts.length - assignedCount;
|
||||
// 统计数据
|
||||
const assignedCount = summary?.assigned_count ?? 0;
|
||||
const unassignedCount = summary?.unassigned_count ?? 0;
|
||||
const totalAccountCount = summary?.total_count ?? (assignedCount + unassignedCount);
|
||||
|
||||
// 获取当前选中客服已分配总数
|
||||
const currentSelectedUserAssignedCount =
|
||||
(selectedUser ? supportUsers.find((u) => u.id === selectedUser.id)?.assigned_count : 0) ??
|
||||
selectedUser?.assigned_count ??
|
||||
0;
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 60 },
|
||||
{ title: '用户名', dataIndex: 'username' },
|
||||
{ title: 'ID', dataIndex: 'id', width: 70 },
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
render: (text: string) => <Text strong>{text}</Text>,
|
||||
},
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'tag',
|
||||
width: 100,
|
||||
render: (tag: string) => tag ? <Tag color="blue">{tag}</Tag> : <Text type="secondary">-</Text>,
|
||||
width: 120,
|
||||
render: (tag: string) => (tag ? <Tag color="blue">{tag}</Tag> : <Text type="secondary">-</Text>),
|
||||
},
|
||||
{
|
||||
title: '积分 / 鱼翅',
|
||||
dataIndex: 'points',
|
||||
width: 140,
|
||||
render: (_: unknown, record: AccountItem) => (
|
||||
<Space size={4}>
|
||||
<span>积分 {record.points ?? '-'}</span>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
/ 鱼翅 {record.gold_balance ?? '-'}
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 110,
|
||||
render: (status: string, record: AccountItem) => (
|
||||
<Tag color={status === 'active' ? 'green' : 'default'}>
|
||||
{record.status_label || status || '-'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -208,22 +250,38 @@ export default function AssignmentsPage() {
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<Row gutter={12} style={{ flexShrink: 0 }}>
|
||||
<Col span={6}>
|
||||
<Card size="small"><Statistic title="总账号" value={accounts.length} prefix={<TeamOutlined />} /></Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="已分配" value={assignedCount} styles={{ content: { color: token.colorSuccess } }} prefix={<CheckCircleOutlined />} />
|
||||
<Statistic title="总账号 (持CK)" value={totalAccountCount} prefix={<TeamOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="未分配" value={unassignedCount} styles={{ content: { color: token.colorError } }} prefix={<SwapOutlined />} />
|
||||
<Statistic
|
||||
title="已分配"
|
||||
value={assignedCount}
|
||||
styles={{ content: { color: token.colorSuccess } }}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="客服人数" value={supportUsers.length} prefix={<UsergroupAddOutlined />} />
|
||||
<Statistic
|
||||
title="未分配"
|
||||
value={unassignedCount}
|
||||
styles={{ content: { color: token.colorError } }}
|
||||
prefix={<SwapOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="客服人数"
|
||||
value={supportUsers.length}
|
||||
prefix={<UsergroupAddOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -236,7 +294,7 @@ export default function AssignmentsPage() {
|
||||
title="客服列表"
|
||||
size="small"
|
||||
style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}
|
||||
bodyStyle={{ flex: 1, overflow: 'auto', padding: '8px' }}
|
||||
styles={{ body: { flex: 1, overflow: 'auto', padding: '8px' } }}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{supportUsers.map((user) => {
|
||||
@@ -267,7 +325,14 @@ export default function AssignmentsPage() {
|
||||
<UserOutlined style={{ color: token.colorPrimary }} />
|
||||
<Text strong>{user.username}</Text>
|
||||
</Space>
|
||||
<Badge count={user.assigned_count} showZero style={{ backgroundColor: token.colorPrimary }} />
|
||||
<Tooltip title={`已分配 ${user.assigned_count} 个账号`}>
|
||||
<Badge
|
||||
count={user.assigned_count}
|
||||
overflowCount={999999}
|
||||
showZero
|
||||
style={{ backgroundColor: token.colorPrimary }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -290,7 +355,7 @@ export default function AssignmentsPage() {
|
||||
<>
|
||||
<span>已选中客服:</span>
|
||||
<Tag color="blue" style={{ fontSize: 14, padding: '2px 10px' }}>
|
||||
{selectedUser.username}(已分配 {selectedUser.assigned_count} 个)
|
||||
{selectedUser.username}(已分配 {currentSelectedUserAssignedCount} 个)
|
||||
</Tag>
|
||||
</>
|
||||
) : (
|
||||
@@ -299,15 +364,27 @@ export default function AssignmentsPage() {
|
||||
</Space>
|
||||
}
|
||||
style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}
|
||||
bodyStyle={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, padding: 0 }}
|
||||
styles={{ body: { flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, padding: 0 } }}
|
||||
>
|
||||
{/* 筛选栏 */}
|
||||
<div style={{ padding: '8px 16px', borderBottom: `1px solid ${token.colorBorderSecondary}`, display: 'flex', gap: 8, alignItems: 'center', flexShrink: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Input.Search
|
||||
placeholder="搜索用户名"
|
||||
allowClear
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setSearchText(e.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
size="small"
|
||||
/>
|
||||
@@ -316,7 +393,10 @@ export default function AssignmentsPage() {
|
||||
showSearch
|
||||
placeholder="按标签筛选"
|
||||
value={filterTag}
|
||||
onChange={(v) => setFilterTag(v || undefined)}
|
||||
onChange={(v) => {
|
||||
setFilterTag(v || undefined);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
filterOption={(input, option) =>
|
||||
String(option?.label || '').toLowerCase().includes(input.trim().toLowerCase())
|
||||
}
|
||||
@@ -330,17 +410,21 @@ export default function AssignmentsPage() {
|
||||
<Tabs
|
||||
size="small"
|
||||
activeKey={activeTab}
|
||||
onChange={(key) => { setActiveTab(key); setSelectedRowKeys([]); }}
|
||||
onChange={(key) => {
|
||||
setActiveTab(key as 'unassigned' | 'assigned');
|
||||
setSelectedRowKeys([]);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
style={{ padding: '0 16px', flexShrink: 0, marginBottom: 0 }}
|
||||
items={[
|
||||
{
|
||||
key: 'unassigned',
|
||||
label: `未分配 (${unassignedAccounts.length})`,
|
||||
label: `未分配 (${unassignedCount})`,
|
||||
},
|
||||
{
|
||||
key: 'assigned',
|
||||
label: selectedUser
|
||||
? `已分配给 ${selectedUser.username} (${assignedAccounts.length})`
|
||||
? `已分配给 ${selectedUser.username} (${currentSelectedUserAssignedCount})`
|
||||
: '已分配账号',
|
||||
disabled: !selectedUser,
|
||||
},
|
||||
@@ -389,12 +473,14 @@ export default function AssignmentsPage() {
|
||||
{/* 表格 */}
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={displayAccounts}
|
||||
dataSource={pagedAccounts}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={tableLoading}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
total: totalCount,
|
||||
size: 'small',
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
@@ -403,7 +489,7 @@ export default function AssignmentsPage() {
|
||||
if (size !== pageSize) {
|
||||
setPageSize(size);
|
||||
localStorage.setItem('assignment_page_size', String(size));
|
||||
setCurrentPage(1); // 切换 pageSize 时重置到第1页
|
||||
setCurrentPage(1);
|
||||
}
|
||||
},
|
||||
}}
|
||||
@@ -411,7 +497,7 @@ export default function AssignmentsPage() {
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys as number[]),
|
||||
}}
|
||||
scroll={{ y: 'calc(100vh - 500px)' }}
|
||||
scroll={{ y: 'calc(100vh - 460px)' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
@@ -253,7 +253,7 @@ export default function HuyaAssignmentsPage() {
|
||||
<UserOutlined style={{ color: token.colorPrimary }} />
|
||||
<Text strong>{user.username}</Text>
|
||||
</Space>
|
||||
<Badge count={user.assigned_count} showZero style={{ backgroundColor: token.colorPrimary }} />
|
||||
<Badge count={user.assigned_count} overflowCount={999999} showZero style={{ backgroundColor: token.colorPrimary }} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user