feat: 账号管理支持按账号名批量选号打标签,并支持复制账号名
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""账号管理「按账号名批量选号」相关测试。"""
|
||||
|
||||
from tests.support.db import get_test_engine, make_test_session
|
||||
from web.backend.models import Account, User
|
||||
from web.backend.routers.accounts import (
|
||||
_parse_account_names,
|
||||
batch_tag_selection,
|
||||
list_account_ids,
|
||||
list_accounts,
|
||||
)
|
||||
from web.backend.schemas import AccountBulkTag
|
||||
|
||||
|
||||
class TestAccountNameBatchSelection:
|
||||
def setup_method(self):
|
||||
self.engine = get_test_engine()
|
||||
self.session = make_test_session(self.engine)
|
||||
self.admin = User(
|
||||
username="name-filter-admin",
|
||||
password_hash="hash",
|
||||
role="super_admin",
|
||||
is_active=True,
|
||||
)
|
||||
self.session.add(self.admin)
|
||||
self.session.commit()
|
||||
|
||||
self.accounts = {}
|
||||
for name in ("name-filter-a", "name-filter-b", "name-filter-c"):
|
||||
account = Account(
|
||||
username=name,
|
||||
password="password",
|
||||
email=f"{name}@example.com",
|
||||
email_password="mail-password",
|
||||
cookie="acf_uid=1",
|
||||
)
|
||||
self.session.add(account)
|
||||
self.session.flush()
|
||||
self.accounts[name] = account
|
||||
self.session.commit()
|
||||
|
||||
def teardown_method(self):
|
||||
self.session.close()
|
||||
|
||||
def test_parse_account_names_trims_and_dedupes(self):
|
||||
assert _parse_account_names(
|
||||
"name-filter-c\n\nname-filter-a\r\nname-filter-c\n name-filter-b "
|
||||
) == ["name-filter-c", "name-filter-a", "name-filter-b"]
|
||||
|
||||
def test_list_accounts_filters_by_exact_names(self):
|
||||
result = list_accounts(
|
||||
account_names="name-filter-c\nname-filter-missing\nname-filter-a",
|
||||
page=1,
|
||||
page_size=20,
|
||||
db=self.session,
|
||||
current=self.admin,
|
||||
)
|
||||
|
||||
assert result["total"] == 2
|
||||
assert {item.username for item in result["items"]} == {
|
||||
"name-filter-a",
|
||||
"name-filter-c",
|
||||
}
|
||||
|
||||
def test_list_account_ids_follows_paste_order(self):
|
||||
result = list_account_ids(
|
||||
account_names="name-filter-c\nname-filter-missing\nname-filter-a",
|
||||
db=self.session,
|
||||
current=self.admin,
|
||||
)
|
||||
|
||||
assert result["account_ids"] == [
|
||||
self.accounts["name-filter-c"].id,
|
||||
self.accounts["name-filter-a"].id,
|
||||
]
|
||||
assert result["usernames"] == ["name-filter-c", "name-filter-a"]
|
||||
assert result["total"] == 2
|
||||
|
||||
def test_batch_tag_selection_supports_account_names(self):
|
||||
result = batch_tag_selection(
|
||||
req=AccountBulkTag(
|
||||
all_matching=True,
|
||||
account_names="name-filter-a\nname-filter-b",
|
||||
tag_value="按名单标签",
|
||||
),
|
||||
db=self.session,
|
||||
current=self.admin,
|
||||
)
|
||||
|
||||
assert result["count"] == 2
|
||||
for name in ("name-filter-a", "name-filter-b"):
|
||||
self.session.refresh(self.accounts[name])
|
||||
assert self.accounts[name].tag == "按名单标签"
|
||||
self.session.refresh(self.accounts["name-filter-c"])
|
||||
assert self.accounts["name-filter-c"].tag == ""
|
||||
@@ -98,6 +98,20 @@ def _query_bool(value) -> bool:
|
||||
return value is True
|
||||
|
||||
|
||||
def _parse_account_names(raw_names: str) -> list[str]:
|
||||
"""解析粘贴的账号名:每行一个,去空白并按输入顺序去重。"""
|
||||
names: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for value in (
|
||||
(raw_names or "").replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
||||
):
|
||||
name = value.strip()
|
||||
if name and name not in seen:
|
||||
names.append(name)
|
||||
seen.add(name)
|
||||
return names
|
||||
|
||||
|
||||
def _filter_accounts_query(
|
||||
db: Session,
|
||||
query,
|
||||
@@ -109,6 +123,7 @@ def _filter_accounts_query(
|
||||
tag: str | None = None,
|
||||
has_cookie: bool = False,
|
||||
search: str = "",
|
||||
account_names: str = "",
|
||||
status: str = "",
|
||||
ck_status: str = "",
|
||||
failure_kind: str = "",
|
||||
@@ -140,6 +155,9 @@ def _filter_accounts_query(
|
||||
tag_value = _query_str(tag)
|
||||
if tag_value:
|
||||
query = query.filter(Account.tag == tag_value)
|
||||
selected_names = _parse_account_names(_query_str(account_names))
|
||||
if selected_names:
|
||||
query = query.filter(Account.username.in_(selected_names))
|
||||
status_value = _query_str(status)
|
||||
if status_value:
|
||||
query = query.filter(Account.status == status_value)
|
||||
@@ -248,6 +266,7 @@ def _selected_account_ids(
|
||||
tag=req.tag,
|
||||
has_cookie=req.has_cookie,
|
||||
search=req.search,
|
||||
account_names=req.account_names,
|
||||
status=req.status,
|
||||
ck_status=req.ck_status,
|
||||
failure_kind=req.failure_kind,
|
||||
@@ -398,6 +417,9 @@ def list_accounts(
|
||||
),
|
||||
assigned_user_id: int | None = Query(None, description="只看某个客服名下的账号"),
|
||||
tag: str = Query(None),
|
||||
account_names: str = Query(
|
||||
"", description="多行斗鱼账号名(每行一个),精确匹配并自动去重"
|
||||
),
|
||||
has_cookie: bool = Query(False),
|
||||
search: str = Query(""),
|
||||
status: str = Query("", description="按生命周期状态筛选;空=全部(不含归档)"),
|
||||
@@ -446,6 +468,7 @@ def list_accounts(
|
||||
tag=tag,
|
||||
has_cookie=has_cookie,
|
||||
search=search,
|
||||
account_names=account_names,
|
||||
status=status,
|
||||
ck_status=ck_status,
|
||||
failure_kind=failure_kind,
|
||||
@@ -623,6 +646,79 @@ def list_accounts(
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/ids")
|
||||
def list_account_ids(
|
||||
assigned_only: bool = Query(False),
|
||||
assign_status: str = Query(""),
|
||||
assigned_user_id: int | None = Query(None),
|
||||
tag: str = Query(None),
|
||||
has_cookie: bool = Query(False),
|
||||
search: str = Query(""),
|
||||
account_names: str = Query(
|
||||
"", description="多行斗鱼账号名(每行一个),精确匹配并自动去重"
|
||||
),
|
||||
status: str = Query(""),
|
||||
ck_status: str = Query(""),
|
||||
failure_kind: str = Query(""),
|
||||
bind_status: str = Query(""),
|
||||
high_value_filter: str = Query(""),
|
||||
min_points: int | None = Query(None),
|
||||
max_points: int | None = Query(None),
|
||||
min_gold: float | None = Query(None),
|
||||
max_gold: float | None = Query(None),
|
||||
elite_opened: bool | None = Query(None),
|
||||
esports_opened: bool | None = Query(None),
|
||||
role_bind_status: str = Query(""),
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""返回当前筛选条件下的全部账号 ID,并按粘贴的账号名顺序排序。
|
||||
|
||||
前端「按账号名批量选号」用它一次性拿到所有匹配 ID,再写入表格的选择集,
|
||||
这样跨分页勾选和后续批量标签都不受当前页限制。
|
||||
"""
|
||||
query = _filter_accounts_query(
|
||||
db,
|
||||
_visible_accounts_query(db, current),
|
||||
current,
|
||||
assigned_only=assigned_only,
|
||||
assign_status=assign_status,
|
||||
assigned_user_id=assigned_user_id,
|
||||
tag=tag,
|
||||
has_cookie=has_cookie,
|
||||
search=search,
|
||||
account_names=account_names,
|
||||
status=status,
|
||||
ck_status=ck_status,
|
||||
failure_kind=failure_kind,
|
||||
bind_status=bind_status,
|
||||
high_value_filter=high_value_filter,
|
||||
min_points=min_points,
|
||||
max_points=max_points,
|
||||
min_gold=min_gold,
|
||||
max_gold=max_gold,
|
||||
elite_opened=elite_opened,
|
||||
esports_opened=esports_opened,
|
||||
role_bind_status=role_bind_status,
|
||||
)
|
||||
selected_names = _parse_account_names(_query_str(account_names))
|
||||
if selected_names:
|
||||
input_order = case(
|
||||
{name: index for index, name in enumerate(selected_names)},
|
||||
value=Account.username,
|
||||
else_=len(selected_names),
|
||||
)
|
||||
query = query.order_by(input_order, Account.id.desc())
|
||||
else:
|
||||
query = query.order_by(Account.id.desc())
|
||||
rows = query.with_entities(Account.id, Account.username).all()
|
||||
return {
|
||||
"account_ids": [account_id for account_id, _ in rows],
|
||||
"usernames": [username for _, username in rows],
|
||||
"total": len(rows),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
def accounts_summary(
|
||||
db: Session = Depends(get_db),
|
||||
|
||||
@@ -130,6 +130,8 @@ class AccountBulkSelection(BaseModel):
|
||||
all_matching: bool = False
|
||||
search: str = ""
|
||||
tag: str = ""
|
||||
#: 多行斗鱼账号名(每行一个),与其它筛选条件叠加
|
||||
account_names: str = ""
|
||||
assigned_only: bool = False
|
||||
#: 归属筛选:""=全部 / unassigned=未分配 / assigned=已分配
|
||||
assign_status: str = ""
|
||||
|
||||
@@ -45,6 +45,34 @@ export const accountApi = {
|
||||
},
|
||||
) =>
|
||||
api.get<PaginatedResponse<AccountItem>, PaginatedResponse<AccountItem>>('/accounts', { params }),
|
||||
// 按当前筛选条件返回全部账号 ID(不受分页限制),供「按账号名批量选号」使用
|
||||
listIds: (
|
||||
params?: {
|
||||
search?: string;
|
||||
assigned_only?: boolean;
|
||||
assign_status?: string;
|
||||
assigned_user_id?: number;
|
||||
tag?: string;
|
||||
has_cookie?: boolean;
|
||||
status?: string;
|
||||
ck_status?: string;
|
||||
failure_kind?: string;
|
||||
bind_status?: string;
|
||||
high_value_filter?: string;
|
||||
min_points?: number;
|
||||
max_points?: number;
|
||||
min_gold?: number;
|
||||
max_gold?: number;
|
||||
elite_opened?: boolean;
|
||||
esports_opened?: boolean;
|
||||
role_bind_status?: string;
|
||||
account_names?: string;
|
||||
},
|
||||
) =>
|
||||
api.get<
|
||||
{ account_ids: number[]; usernames: string[]; total: number },
|
||||
{ account_ids: number[]; usernames: string[]; total: number }
|
||||
>('/accounts/ids', { params }),
|
||||
export: (data: AccountExportRequest) =>
|
||||
api.post('/accounts/export', data, { responseType: 'blob' }),
|
||||
summary: () => api.get<BasicSummary, BasicSummary>('/accounts/summary'),
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface AccountBulkSelection {
|
||||
all_matching?: boolean;
|
||||
search?: string;
|
||||
tag?: string;
|
||||
/** 多行斗鱼账号名(每行一个),与其它筛选条件叠加 */
|
||||
account_names?: string;
|
||||
assigned_only?: boolean;
|
||||
/** 归属筛选:unassigned=未分配 / assigned=已分配;空=全部 */
|
||||
assign_status?: string;
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
clearStoredFilters,
|
||||
} from './accounts/storage';
|
||||
import { AccountToolbar } from './accounts/components/AccountToolbar';
|
||||
import { AccountNameBatchSelector } from './accounts/components/AccountNameBatchSelector';
|
||||
import type { AccountNameMatchInfo } from './accounts/components/AccountNameBatchSelector';
|
||||
import { StatusCards } from './accounts/components/StatusCards';
|
||||
import { FilterPanel } from './accounts/components/FilterPanel';
|
||||
import { buildAccountColumns } from './accounts/components/AccountColumns';
|
||||
@@ -35,6 +37,23 @@ import {
|
||||
ExchangeRecordsModal,
|
||||
} from './accounts/components/AccountModals';
|
||||
|
||||
function parseAccountNames(raw: string): string[] {
|
||||
const names: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
raw
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
.split('\n')
|
||||
.forEach((line) => {
|
||||
const name = line.trim();
|
||||
if (name && !seen.has(name)) {
|
||||
names.push(name);
|
||||
seen.add(name);
|
||||
}
|
||||
});
|
||||
return names;
|
||||
}
|
||||
|
||||
export default function AccountsPage() {
|
||||
const navigate = useNavigate();
|
||||
const { modal } = App.useApp();
|
||||
@@ -64,6 +83,12 @@ export default function AccountsPage() {
|
||||
const [searchText, setSearchText] = useState(() => initialFilters.searchText || '');
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
// 按账号名批量选号:粘贴名单 -> 匹配 ID -> 自动勾选 -> 批量改标签
|
||||
const [accountNameText, setAccountNameText] = useState('');
|
||||
const [accountNamesFilter, setAccountNamesFilter] = useState('');
|
||||
const [accountNameMatchInfo, setAccountNameMatchInfo] = useState<AccountNameMatchInfo | null>(null);
|
||||
const [accountNameMatching, setAccountNameMatching] = useState(false);
|
||||
|
||||
// 高价值资产与高级自定义筛选状态
|
||||
const [highValueFilter, setHighValueFilter] = useState<string>(() => initialFilters.highValueFilter || '');
|
||||
const [minPoints, setMinPoints] = useState<number | undefined>(() => initialFilters.minPoints);
|
||||
@@ -99,8 +124,12 @@ export default function AccountsPage() {
|
||||
setBindFilter('');
|
||||
setAssignFilter('');
|
||||
setSearchText('');
|
||||
setAccountNameText('');
|
||||
setAccountNamesFilter('');
|
||||
setAccountNameMatchInfo(null);
|
||||
resetAdvancedFiltersState();
|
||||
setCurrentPage(1);
|
||||
clearSelection();
|
||||
clearStoredFilters();
|
||||
message.success('已重置所有筛选条件');
|
||||
};
|
||||
@@ -172,6 +201,7 @@ export default function AccountsPage() {
|
||||
include_sensitive: canViewFull && sensitiveVisible,
|
||||
};
|
||||
if (tagFilter) params.tag = tagFilter;
|
||||
if (accountNamesFilter) params.account_names = accountNamesFilter;
|
||||
if (statusFilter) params.status = statusFilter;
|
||||
if (ckFilter) params.ck_status = ckFilter;
|
||||
if (failureKindFilter) params.failure_kind = failureKindFilter;
|
||||
@@ -206,6 +236,7 @@ export default function AccountsPage() {
|
||||
searchText,
|
||||
sensitiveVisible,
|
||||
tagFilter,
|
||||
accountNamesFilter,
|
||||
statusFilter,
|
||||
ckFilter,
|
||||
failureKindFilter,
|
||||
@@ -271,6 +302,7 @@ export default function AccountsPage() {
|
||||
all_matching: selectedAllMatching,
|
||||
search: searchText.trim(),
|
||||
tag: tagFilter,
|
||||
account_names: accountNamesFilter || undefined,
|
||||
status: statusFilter || undefined,
|
||||
ck_status: ckFilter || undefined,
|
||||
failure_kind: failureKindFilter || undefined,
|
||||
@@ -291,6 +323,7 @@ export default function AccountsPage() {
|
||||
selectedAllMatching,
|
||||
selectedRowKeys,
|
||||
tagFilter,
|
||||
accountNamesFilter,
|
||||
statusFilter,
|
||||
ckFilter,
|
||||
failureKindFilter,
|
||||
@@ -468,6 +501,61 @@ export default function AccountsPage() {
|
||||
roleBindStatus,
|
||||
]);
|
||||
|
||||
const handleApplyAccountNames = async () => {
|
||||
const names = parseAccountNames(accountNameText);
|
||||
if (names.length === 0) {
|
||||
message.warning('请先粘贴斗鱼账号名,每行一个');
|
||||
return;
|
||||
}
|
||||
setAccountNameMatching(true);
|
||||
try {
|
||||
const result = await accountApi.listIds({
|
||||
search: searchText.trim() || undefined,
|
||||
tag: tagFilter || undefined,
|
||||
status: statusFilter || undefined,
|
||||
ck_status: ckFilter || undefined,
|
||||
failure_kind: failureKindFilter || undefined,
|
||||
bind_status: bindFilter || undefined,
|
||||
assign_status: assignStatusParam || undefined,
|
||||
assigned_user_id: assignedUserIdParam,
|
||||
high_value_filter: highValueFilter || undefined,
|
||||
min_points: minPoints,
|
||||
max_points: maxPoints,
|
||||
min_gold: minGold,
|
||||
max_gold: maxGold,
|
||||
elite_opened: eliteOpened,
|
||||
esports_opened: esportsOpened,
|
||||
role_bind_status: roleBindStatus || undefined,
|
||||
account_names: names.join('\n'),
|
||||
});
|
||||
if (result.total === 0) {
|
||||
message.warning('没有匹配到账号,请检查账号名或当前筛选条件');
|
||||
return;
|
||||
}
|
||||
setAccountNamesFilter(names.join('\n'));
|
||||
setAccountNameMatchInfo({ submitted: names.length, matched: result.total });
|
||||
setSelectedRowKeys(result.account_ids);
|
||||
setSelectedAllMatching(false);
|
||||
setCurrentPage(1);
|
||||
const unmatched = Math.max(0, names.length - result.total);
|
||||
message.success(
|
||||
`已匹配并勾选 ${result.total} 个账号${unmatched > 0 ? `,${unmatched} 个账号名未匹配` : ''}`,
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setAccountNameMatching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearAccountNames = () => {
|
||||
setAccountNameText('');
|
||||
setAccountNamesFilter('');
|
||||
setAccountNameMatchInfo(null);
|
||||
clearSelection();
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!importText.trim()) {
|
||||
message.warning('请输入账号数据');
|
||||
@@ -642,7 +730,8 @@ export default function AccountsPage() {
|
||||
);
|
||||
|
||||
const hasAnyFilter = Boolean(
|
||||
tagFilter ||
|
||||
accountNamesFilter ||
|
||||
tagFilter ||
|
||||
statusFilter ||
|
||||
ckFilter ||
|
||||
failureKindFilter ||
|
||||
@@ -750,6 +839,21 @@ export default function AccountsPage() {
|
||||
setCurrentPage={setCurrentPage}
|
||||
/>
|
||||
|
||||
<AccountNameBatchSelector
|
||||
value={accountNameText}
|
||||
onChange={setAccountNameText}
|
||||
onApply={handleApplyAccountNames}
|
||||
onClear={handleClearAccountNames}
|
||||
onOpenBatchTag={() => {
|
||||
setBatchTagInput('');
|
||||
setBatchTagVisible(true);
|
||||
}}
|
||||
loading={accountNameMatching}
|
||||
canTag={canTag}
|
||||
selectedCount={selectedCount}
|
||||
matchInfo={accountNameMatchInfo}
|
||||
/>
|
||||
|
||||
<StatusCards
|
||||
summary={summary}
|
||||
statusFilter={statusFilter}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { TableProps } from 'antd';
|
||||
import { Space, Button, Typography, Tag, Tooltip, Select, Popconfirm } from 'antd';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { CopyOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import type { NavigateFunction } from 'react-router-dom';
|
||||
import type { AccountItem } from '../../../api/modules';
|
||||
import { message } from '../../../utils/antdMessage';
|
||||
import { copyTextToClipboard } from '../../../utils/clipboard';
|
||||
import { formatTimeShort } from '../../../utils/time';
|
||||
import {
|
||||
type AccountSortKey,
|
||||
@@ -86,15 +88,49 @@ export function buildAccountColumns({
|
||||
ellipsis: true,
|
||||
render: (username: string, record: AccountItem) => (
|
||||
<Space direction="vertical" size={0} style={{ maxWidth: '100%' }}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{ padding: 0, maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||
title={username}
|
||||
onClick={() => navigate(`/accounts/${record.id}`)}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
maxWidth: '100%',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{username}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={{
|
||||
padding: 0,
|
||||
minWidth: 0,
|
||||
flex: '0 1 auto',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
title={username}
|
||||
onClick={() => navigate(`/accounts/${record.id}`)}
|
||||
>
|
||||
{username}
|
||||
</Button>
|
||||
<Tooltip title="复制账号名">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
aria-label="复制账号名"
|
||||
icon={<CopyOutlined style={{ fontSize: 12, color: '#8c8c8c' }} />}
|
||||
style={{ padding: 0, width: 18, height: 18, minWidth: 18, flex: 'none' }}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await copyTextToClipboard(username);
|
||||
message.success('账号名已复制');
|
||||
} catch {
|
||||
message.error('复制失败');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
#{record.id}
|
||||
</Text>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Button, Card, Input, Space, Typography } from 'antd';
|
||||
import {
|
||||
CheckSquareOutlined,
|
||||
ClearOutlined,
|
||||
TagsOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export interface AccountNameMatchInfo {
|
||||
submitted: number;
|
||||
matched: number;
|
||||
}
|
||||
|
||||
export interface AccountNameBatchSelectorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onApply: () => void;
|
||||
onClear: () => void;
|
||||
onOpenBatchTag: () => void;
|
||||
loading: boolean;
|
||||
canTag: boolean;
|
||||
selectedCount: number;
|
||||
matchInfo: AccountNameMatchInfo | null;
|
||||
}
|
||||
|
||||
export function AccountNameBatchSelector({
|
||||
value,
|
||||
onChange,
|
||||
onApply,
|
||||
onClear,
|
||||
onOpenBatchTag,
|
||||
loading,
|
||||
canTag,
|
||||
selectedCount,
|
||||
matchInfo,
|
||||
}: AccountNameBatchSelectorProps) {
|
||||
const unmatched = matchInfo ? matchInfo.submitted - matchInfo.matched : 0;
|
||||
|
||||
return (
|
||||
<Card
|
||||
size="small"
|
||||
title={
|
||||
<Space size={6}>
|
||||
<TagsOutlined />
|
||||
<span>按账号名批量选号</span>
|
||||
</Space>
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={8}>
|
||||
<Input.TextArea
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder="从 Excel 复制斗鱼账号名后粘贴到这里,每行一个;匹配后会自动勾选,可直接批量改标签"
|
||||
autoSize={{ minRows: 3, maxRows: 7 }}
|
||||
/>
|
||||
<Space wrap>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CheckSquareOutlined />}
|
||||
loading={loading}
|
||||
onClick={onApply}
|
||||
>
|
||||
匹配并选中
|
||||
</Button>
|
||||
<Button
|
||||
icon={<TagsOutlined />}
|
||||
disabled={!canTag || selectedCount === 0}
|
||||
onClick={onOpenBatchTag}
|
||||
>
|
||||
批量改标签{selectedCount > 0 ? `(${selectedCount})` : ''}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ClearOutlined />}
|
||||
disabled={!value.trim() && selectedCount === 0 && !matchInfo}
|
||||
onClick={onClear}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
{matchInfo && (
|
||||
<Text type={unmatched > 0 ? 'warning' : 'secondary'}>
|
||||
已粘贴 {matchInfo.submitted} 个账号名,匹配到 {matchInfo.matched} 个账号
|
||||
{unmatched > 0
|
||||
? `;${unmatched} 个未匹配或不在当前筛选范围内`
|
||||
: ''}
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user