feat(douyu): 支持账号批量撤回并刷新未分配状态
新增按勾选或标签批量撤回账号归属,撤回只处理确实已分配的账号。账号管理和分配管理增加可见时自动刷新、窗口聚焦刷新及手动刷新,解决批量登录完成后未分配账号延迟出现。
This commit is contained in:
@@ -227,8 +227,8 @@ def _filter_accounts_query(
|
||||
# 只是商城接口对它稳定失败 —— 那是下面 high_value_never 那一档。
|
||||
query = query.filter(Account.overview_queried_at.is_(None))
|
||||
elif hv == "high_value_never":
|
||||
# 从没成功读到官方 storehome:商城接口失败 / 没配 manualID。
|
||||
# 高价值三态里的「unknown」要求 checked_at 非空(查过但没结论),
|
||||
# 从未形成高价值结论:商城接口失败 / 没配 manualID / 新账号尚未查询。
|
||||
# 高价值三态里的「unknown」要求 checked_at 非空(形成过 unknown 结论),
|
||||
# 所以这一档必须单独筛,否则这批账号在界面上**无处可寻**。
|
||||
query = query.filter(Account.high_value_checked_at.is_(None))
|
||||
elif hv == "high_value_unknown":
|
||||
@@ -819,7 +819,7 @@ def accounts_summary(
|
||||
"can_bairimeng": int(hv_row[1] or 0) if hv_row else 0,
|
||||
"can_mengtu": int(hv_row[2] or 0) if hv_row else 0,
|
||||
"unqueried": int(hv_row[3] or 0) if hv_row else 0,
|
||||
# 从没成功读到官方商城结果的账号数(与「查过但没结论」分开)。
|
||||
# 从未形成高价值结论的账号数(与「形成过 unknown/过期结论」分开)。
|
||||
"high_value_never": int(hv_row[4] or 0) if hv_row else 0,
|
||||
"high_value_unknown": int(hv_row[5] or 0) if hv_row else 0,
|
||||
"high_points": int(hv_row[6] or 0) if hv_row else 0,
|
||||
@@ -1082,14 +1082,22 @@ def batch_assign_accounts(
|
||||
detail=f"以下账号尚未成功登录,无法分配:{name_list}{suffix}",
|
||||
)
|
||||
|
||||
# 先取出账号对象(批量时间线要逐个写事件,光有 count 不够)
|
||||
target_accounts = db.query(Account).filter(Account.id.in_(ids)).all()
|
||||
# 先取出账号对象(批量时间线要逐个写事件,光有 count 不够)。
|
||||
# 「撤回」只处理当前确实有归属的账号;显式勾选里混入未分配账号时,
|
||||
# 不应把它们算进撤回数量,也不该写一条假的「取消分配」时间线。
|
||||
target_query = db.query(Account).filter(Account.id.in_(ids))
|
||||
if req.assigned_to is None:
|
||||
target_query = target_query.filter(Account.assigned_to.isnot(None))
|
||||
target_accounts = target_query.all()
|
||||
target_ids = [account.id for account in target_accounts]
|
||||
if not target_ids:
|
||||
raise HTTPException(status_code=400, detail="所选账号均未分配,无需撤回")
|
||||
count = (
|
||||
db.query(Account)
|
||||
.filter(Account.id.in_(ids))
|
||||
.filter(Account.id.in_(target_ids))
|
||||
.update({Account.assigned_to: req.assigned_to}, synchronize_session=False)
|
||||
)
|
||||
db.query(DouyuTask).filter(DouyuTask.account_id.in_(ids)).update(
|
||||
db.query(DouyuTask).filter(DouyuTask.account_id.in_(target_ids)).update(
|
||||
{DouyuTask.account_assigned_to: req.assigned_to}, synchronize_session=False
|
||||
)
|
||||
target_user = (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Table, Alert, Space, Button, App, type MenuProps } from 'antd';
|
||||
import { message } from '../utils/antdMessage';
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
BatchImportModal,
|
||||
BatchTagModal,
|
||||
BatchAssignModal,
|
||||
BatchUnassignModal,
|
||||
type BatchUnassignMode,
|
||||
ExchangeRecordsModal,
|
||||
} from './accounts/components/AccountModals';
|
||||
|
||||
@@ -80,8 +82,17 @@ export default function AccountsPage() {
|
||||
const [assignModalOpen, setAssignModalOpen] = useState(false);
|
||||
const [assignTargetId, setAssignTargetId] = useState<number | undefined>(undefined);
|
||||
const [assigning, setAssigning] = useState(false);
|
||||
const [unassignModalOpen, setUnassignModalOpen] = useState(false);
|
||||
const [unassignMode, setUnassignMode] = useState<BatchUnassignMode>('selected');
|
||||
const [unassignTag, setUnassignTag] = useState('');
|
||||
const [unassignTagCount, setUnassignTagCount] = useState<number | null>(null);
|
||||
const [unassignTagCountLoading, setUnassignTagCountLoading] = useState(false);
|
||||
const [unassigning, setUnassigning] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [searchText, setSearchText] = useState(() => initialFilters.searchText || '');
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const accountRequestRef = useRef(0);
|
||||
const unassignTagCountRequestRef = useRef(0);
|
||||
|
||||
// 按账号名批量选号:粘贴名单 -> 匹配 ID -> 自动勾选 -> 批量改标签
|
||||
const [accountNameText, setAccountNameText] = useState('');
|
||||
@@ -191,8 +202,9 @@ export default function AccountsPage() {
|
||||
? Number(assignFilter.slice(5))
|
||||
: undefined;
|
||||
|
||||
const loadAccounts = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const loadAccounts = useCallback(async (silent = false) => {
|
||||
const requestId = ++accountRequestRef.current;
|
||||
if (!silent) setLoading(true);
|
||||
try {
|
||||
const params: Parameters<typeof accountApi.listPaged>[0] = {
|
||||
page: currentPage,
|
||||
@@ -222,12 +234,17 @@ export default function AccountsPage() {
|
||||
}
|
||||
|
||||
const data = await accountApi.listPaged(params);
|
||||
if (requestId !== accountRequestRef.current) return;
|
||||
setAccounts(data.items);
|
||||
setTotal(data.total);
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
if (requestId === accountRequestRef.current && !silent) {
|
||||
message.error(getErrorMessage(e));
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (requestId === accountRequestRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
canViewFull,
|
||||
@@ -290,11 +307,77 @@ export default function AccountsPage() {
|
||||
loadAccounts();
|
||||
}, [loadAccounts]);
|
||||
|
||||
useEffect(() => {
|
||||
const refreshVisibleData = () => {
|
||||
if (document.visibilityState !== 'visible') return;
|
||||
void loadAccounts(true);
|
||||
void loadSummary();
|
||||
if (canAssign) void loadAssignUsers();
|
||||
};
|
||||
const timer = window.setInterval(refreshVisibleData, 15000);
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState === 'visible') refreshVisibleData();
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
window.addEventListener('focus', refreshVisibleData);
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
document.removeEventListener('visibilitychange', handleVisibility);
|
||||
window.removeEventListener('focus', refreshVisibleData);
|
||||
};
|
||||
}, [canAssign, loadAccounts, loadAssignUsers, loadSummary]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTags();
|
||||
loadSummary();
|
||||
}, [loadTags, loadSummary]);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await Promise.allSettled([
|
||||
loadAccounts(),
|
||||
loadSummary(),
|
||||
loadTags(),
|
||||
...(canAssign ? [loadAssignUsers()] : []),
|
||||
]);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!unassignModalOpen || unassignMode !== 'tag' || !unassignTag) {
|
||||
unassignTagCountRequestRef.current += 1;
|
||||
setUnassignTagCount(null);
|
||||
setUnassignTagCountLoading(false);
|
||||
return;
|
||||
}
|
||||
const requestId = ++unassignTagCountRequestRef.current;
|
||||
setUnassignTagCountLoading(true);
|
||||
setUnassignTagCount(null);
|
||||
accountApi
|
||||
.listIds({
|
||||
assign_status: 'assigned',
|
||||
tag: unassignTag,
|
||||
})
|
||||
.then((result) => {
|
||||
if (requestId === unassignTagCountRequestRef.current) {
|
||||
setUnassignTagCount(result.total);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (requestId === unassignTagCountRequestRef.current) {
|
||||
setUnassignTagCount(null);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (requestId === unassignTagCountRequestRef.current) {
|
||||
setUnassignTagCountLoading(false);
|
||||
}
|
||||
});
|
||||
}, [unassignMode, unassignModalOpen, unassignTag]);
|
||||
|
||||
const selectedCount = selectedAllMatching ? total : selectedRowKeys.length;
|
||||
const selectionPayload = useMemo(
|
||||
() => ({
|
||||
@@ -630,6 +713,42 @@ export default function AccountsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchUnassign = async () => {
|
||||
if (unassignMode === 'selected' && selectedCount === 0) {
|
||||
message.warning('请先勾选账号,或先选择要撤回的标签');
|
||||
return;
|
||||
}
|
||||
if (unassignMode === 'tag' && !unassignTag.trim()) {
|
||||
message.warning('请选择要撤回的账号标签');
|
||||
return;
|
||||
}
|
||||
setUnassigning(true);
|
||||
try {
|
||||
const result =
|
||||
unassignMode === 'tag'
|
||||
? await accountApi.batchAssignSelection({
|
||||
all_matching: true,
|
||||
assign_status: 'assigned',
|
||||
tag: unassignTag,
|
||||
assigned_to: null,
|
||||
})
|
||||
: await accountApi.batchAssignSelection({
|
||||
...selectionPayload,
|
||||
assigned_to: null,
|
||||
});
|
||||
message.success(result.message);
|
||||
setUnassignModalOpen(false);
|
||||
setUnassignTag('');
|
||||
clearSelection();
|
||||
await Promise.all([loadAccounts(), loadSummary()]);
|
||||
if (canAssign) await loadAssignUsers();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setUnassigning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetTag = async (accountId: number, tag: string) => {
|
||||
try {
|
||||
await accountApi.setTag(accountId, tag);
|
||||
@@ -755,6 +874,8 @@ export default function AccountsPage() {
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
// 这些操作回调只会在点击/提交时读取请求序号 ref,渲染阶段不会访问。
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
buildAccountColumns({
|
||||
navigate,
|
||||
sortKey,
|
||||
@@ -825,6 +946,11 @@ export default function AccountsPage() {
|
||||
setAssignTargetId(undefined);
|
||||
setAssignModalOpen(true);
|
||||
}}
|
||||
onOpenBatchUnassign={() => {
|
||||
setUnassignMode(selectedCount > 0 ? 'selected' : 'tag');
|
||||
setUnassignTag(tagFilter || '');
|
||||
setUnassignModalOpen(true);
|
||||
}}
|
||||
canDelete={canDelete}
|
||||
selectedAllMatching={selectedAllMatching}
|
||||
onBatchDelete={handleBatchDelete}
|
||||
@@ -836,6 +962,8 @@ export default function AccountsPage() {
|
||||
setImportTag('');
|
||||
setImportOpen(true);
|
||||
}}
|
||||
onRefresh={handleRefresh}
|
||||
refreshing={refreshing}
|
||||
setCurrentPage={setCurrentPage}
|
||||
/>
|
||||
|
||||
@@ -1021,6 +1149,25 @@ export default function AccountsPage() {
|
||||
onBatchAssign={handleBatchAssign}
|
||||
/>
|
||||
|
||||
<BatchUnassignModal
|
||||
open={unassignModalOpen}
|
||||
onCancel={() => {
|
||||
setUnassignModalOpen(false);
|
||||
setUnassignTag('');
|
||||
}}
|
||||
onConfirm={handleBatchUnassign}
|
||||
unassigning={unassigning}
|
||||
selectedCount={selectedCount}
|
||||
selectedAllMatching={selectedAllMatching}
|
||||
mode={unassignMode}
|
||||
setMode={setUnassignMode}
|
||||
tag={unassignTag}
|
||||
setTag={setUnassignTag}
|
||||
tags={tags}
|
||||
tagCount={unassignTagCount}
|
||||
tagCountLoading={unassignTagCountLoading}
|
||||
/>
|
||||
|
||||
<ExchangeRecordsModal
|
||||
exAccount={exAccount}
|
||||
exLoading={exLoading}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import {
|
||||
Card, Table, Button, Select, Input, Tag, Row, Col, Statistic,
|
||||
Space, Tabs, Typography, Badge, theme, Tooltip,
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { message } from '../utils/antdMessage';
|
||||
import {
|
||||
UserOutlined, CheckCircleOutlined, TeamOutlined,
|
||||
SwapOutlined, ClearOutlined, UsergroupAddOutlined,
|
||||
SwapOutlined, ClearOutlined, UsergroupAddOutlined, ReloadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
accountApi,
|
||||
@@ -36,6 +36,9 @@ export default function AssignmentsPage() {
|
||||
const [filterTag, setFilterTag] = useState<string | undefined>(undefined);
|
||||
const [activeTab, setActiveTab] = useState<'unassigned' | 'assigned'>('unassigned');
|
||||
const [assigning, setAssigning] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const summaryRequestRef = useRef(0);
|
||||
const pagedRequestRef = useRef(0);
|
||||
const [pageSize, setPageSize] = useState(() => {
|
||||
const v = localStorage.getItem('assignment_page_size');
|
||||
return v ? Number(v) || 15 : 15;
|
||||
@@ -43,14 +46,18 @@ export default function AssignmentsPage() {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
// 加载分配概览统计
|
||||
const loadSummary = useCallback(async () => {
|
||||
const loadSummary = useCallback(async (silent = false) => {
|
||||
const requestId = ++summaryRequestRef.current;
|
||||
try {
|
||||
const data = await accountApi.assignmentsSummary();
|
||||
if (requestId !== summaryRequestRef.current) return [];
|
||||
setSummary(data);
|
||||
setSupportUsers(data.support_users);
|
||||
return data.support_users;
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
if (requestId === summaryRequestRef.current && !silent) {
|
||||
message.error(getErrorMessage(e));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
@@ -66,8 +73,9 @@ export default function AssignmentsPage() {
|
||||
}, []);
|
||||
|
||||
// 分页按需加载账号(服务端过滤与分页)
|
||||
const loadPagedAccounts = useCallback(async () => {
|
||||
setTableLoading(true);
|
||||
const loadPagedAccounts = useCallback(async (silent = false) => {
|
||||
const requestId = ++pagedRequestRef.current;
|
||||
if (!silent) setTableLoading(true);
|
||||
try {
|
||||
const params: Parameters<typeof accountApi.listPaged>[0] = {
|
||||
page: currentPage,
|
||||
@@ -87,12 +95,17 @@ export default function AssignmentsPage() {
|
||||
}
|
||||
|
||||
const data = await accountApi.listPaged(params);
|
||||
if (requestId !== pagedRequestRef.current) return;
|
||||
setPagedAccounts(data.items);
|
||||
setTotalCount(data.total);
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
if (requestId === pagedRequestRef.current && !silent) {
|
||||
message.error(getErrorMessage(e));
|
||||
}
|
||||
} finally {
|
||||
setTableLoading(false);
|
||||
if (requestId === pagedRequestRef.current) {
|
||||
setTableLoading(false);
|
||||
}
|
||||
}
|
||||
}, [activeTab, selectedUser, currentPage, pageSize, searchText, filterTag]);
|
||||
|
||||
@@ -115,6 +128,34 @@ export default function AssignmentsPage() {
|
||||
loadPagedAccounts();
|
||||
}, [loadPagedAccounts]);
|
||||
|
||||
useEffect(() => {
|
||||
const refreshVisibleData = () => {
|
||||
if (document.visibilityState !== 'visible') return;
|
||||
void loadSummary(true);
|
||||
void loadPagedAccounts(true);
|
||||
};
|
||||
const timer = window.setInterval(refreshVisibleData, 15000);
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState === 'visible') refreshVisibleData();
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
window.addEventListener('focus', refreshVisibleData);
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
document.removeEventListener('visibilitychange', handleVisibility);
|
||||
window.removeEventListener('focus', refreshVisibleData);
|
||||
};
|
||||
}, [loadPagedAccounts, loadSummary]);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await Promise.all([loadSummary(), loadPagedAccounts()]);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换客服
|
||||
const handleSelectUser = (user: SupportUserItem) => {
|
||||
setSelectedUser(user);
|
||||
@@ -249,7 +290,12 @@ export default function AssignmentsPage() {
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<h2 style={{ margin: 0, flexShrink: 0 }}>分配管理</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<h2 style={{ margin: 0 }}>分配管理</h2>
|
||||
<Button icon={<ReloadOutlined />} loading={refreshing} onClick={handleRefresh}>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<Row gutter={12} style={{ flexShrink: 0 }}>
|
||||
@@ -270,12 +316,14 @@ export default function AssignmentsPage() {
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="未分配"
|
||||
value={unassignedCount}
|
||||
styles={{ content: { color: token.colorError } }}
|
||||
prefix={<SwapOutlined />}
|
||||
/>
|
||||
<Tooltip title="仅统计持 CK 且 CK 未判失效、未停用/归档的账号。批量登录成功写入 CK 后会自动进入;本页每 15 秒刷新一次。">
|
||||
<Statistic
|
||||
title="未分配"
|
||||
value={unassignedCount}
|
||||
styles={{ content: { color: token.colorError } }}
|
||||
prefix={<SwapOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { Modal, Button, Input, Select, Typography, Table, Spin, Space } from 'antd';
|
||||
import {
|
||||
Modal,
|
||||
Alert,
|
||||
Button,
|
||||
Input,
|
||||
Select,
|
||||
Typography,
|
||||
Table,
|
||||
Spin,
|
||||
Space,
|
||||
Radio,
|
||||
} from 'antd';
|
||||
import type { AccountItem, AccountExchangeRecords, SupportUserItem } from '../../../api/modules';
|
||||
import { EXCHANGE_SCOPES, EXCHANGE_COLUMNS } from '../types';
|
||||
|
||||
@@ -116,6 +127,115 @@ export interface BatchAssignModalProps {
|
||||
onBatchAssign: (target: number | null) => void;
|
||||
}
|
||||
|
||||
export type BatchUnassignMode = 'selected' | 'tag';
|
||||
|
||||
export interface BatchUnassignModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
unassigning: boolean;
|
||||
selectedCount: number;
|
||||
selectedAllMatching: boolean;
|
||||
mode: BatchUnassignMode;
|
||||
setMode: (mode: BatchUnassignMode) => void;
|
||||
tag: string;
|
||||
setTag: (tag: string) => void;
|
||||
tags: string[];
|
||||
tagCount: number | null;
|
||||
tagCountLoading: boolean;
|
||||
}
|
||||
|
||||
export function BatchUnassignModal({
|
||||
open,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
unassigning,
|
||||
selectedCount,
|
||||
selectedAllMatching,
|
||||
mode,
|
||||
setMode,
|
||||
tag,
|
||||
setTag,
|
||||
tags,
|
||||
tagCount,
|
||||
tagCountLoading,
|
||||
}: BatchUnassignModalProps) {
|
||||
const noSelection = selectedCount === 0;
|
||||
const noTag = !tag.trim();
|
||||
const noTagMatch = mode === 'tag' && tagCount === 0;
|
||||
const description = selectedAllMatching
|
||||
? `当前筛选下全部 ${selectedCount} 个账号`
|
||||
: `已勾选的 ${selectedCount} 个账号`;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="批量撤回已分配账号"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
confirmLoading={unassigning}
|
||||
okText="确认撤回"
|
||||
okButtonProps={{
|
||||
danger: true,
|
||||
disabled: (mode === 'selected' && noSelection) || (mode === 'tag' && (noTag || noTagMatch)),
|
||||
}}
|
||||
onOk={onConfirm}
|
||||
width={480}
|
||||
>
|
||||
<Space direction="vertical" size={14} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="撤回只会清空账号归属,不会删除账号或 CK。"
|
||||
description="撤回后的账号会回到「未分配」。未通过登录、没有 CK 的账号不会出现在分配管理页的未分配池中。"
|
||||
/>
|
||||
|
||||
<Radio.Group
|
||||
value={mode}
|
||||
onChange={(event) => setMode(event.target.value as BatchUnassignMode)}
|
||||
style={{ display: 'flex', gap: 20 }}
|
||||
>
|
||||
<Radio value="selected" disabled={noSelection}>
|
||||
勾选/当前筛选账号
|
||||
</Radio>
|
||||
<Radio value="tag" disabled={tags.length === 0}>
|
||||
按标签撤回
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
|
||||
{mode === 'selected' ? (
|
||||
<Text>
|
||||
{noSelection ? '请先勾选账号,或先点「全选当前筛选」。' : `将撤回${description}的分配归属。`}
|
||||
</Text>
|
||||
) : (
|
||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
optionFilterProp="label"
|
||||
style={{ width: '100%' }}
|
||||
placeholder="选择要撤回的账号标签"
|
||||
value={tag || undefined}
|
||||
onChange={(value) => setTag(value || '')}
|
||||
options={tags.map((item) => ({ value: item, label: item }))}
|
||||
/>
|
||||
<Text type="secondary">
|
||||
{!tag
|
||||
? '选择标签后会统计该标签下当前仍处于已分配状态的账号。'
|
||||
: tagCountLoading
|
||||
? '正在统计该标签下的已分配账号…'
|
||||
: tagCount === null
|
||||
? '暂时无法统计,仍可提交后由后端确认。'
|
||||
: tagCount === 0
|
||||
? `标签「${tag}」下没有已分配账号。`
|
||||
: `标签「${tag}」下共有 ${tagCount} 个已分配账号,将全部撤回。`}
|
||||
</Text>
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function BatchAssignModal({
|
||||
open,
|
||||
onCancel,
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
ThunderboltOutlined,
|
||||
ImportOutlined,
|
||||
ClearOutlined,
|
||||
ReloadOutlined,
|
||||
UndoOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
CK_OPTIONS,
|
||||
@@ -46,6 +48,7 @@ export interface AccountToolbarProps {
|
||||
onOpenBatchTag: () => void;
|
||||
canAssign: boolean;
|
||||
onOpenBatchAssign: () => void;
|
||||
onOpenBatchUnassign: () => void;
|
||||
canDelete: boolean;
|
||||
selectedAllMatching: boolean;
|
||||
onBatchDelete: () => void;
|
||||
@@ -54,6 +57,8 @@ export interface AccountToolbarProps {
|
||||
onBatchOverview: () => void;
|
||||
canImport: boolean;
|
||||
onOpenImport: () => void;
|
||||
onRefresh: () => void;
|
||||
refreshing: boolean;
|
||||
setCurrentPage: (page: number) => void;
|
||||
}
|
||||
|
||||
@@ -84,6 +89,7 @@ export function AccountToolbar({
|
||||
onOpenBatchTag,
|
||||
canAssign,
|
||||
onOpenBatchAssign,
|
||||
onOpenBatchUnassign,
|
||||
canDelete,
|
||||
selectedAllMatching,
|
||||
onBatchDelete,
|
||||
@@ -92,6 +98,8 @@ export function AccountToolbar({
|
||||
onBatchOverview,
|
||||
canImport,
|
||||
onOpenImport,
|
||||
onRefresh,
|
||||
refreshing,
|
||||
setCurrentPage,
|
||||
}: AccountToolbarProps) {
|
||||
return (
|
||||
@@ -205,6 +213,9 @@ export function AccountToolbar({
|
||||
{sensitiveVisible ? '隐藏凭据' : '显示凭据'}
|
||||
</Button>
|
||||
)}
|
||||
<Button icon={<ReloadOutlined />} loading={refreshing} onClick={onRefresh}>
|
||||
刷新
|
||||
</Button>
|
||||
{canTag && (
|
||||
<Button
|
||||
disabled={selectedCount === 0}
|
||||
@@ -215,13 +226,23 @@ export function AccountToolbar({
|
||||
</Button>
|
||||
)}
|
||||
{canAssign && (
|
||||
<Button
|
||||
disabled={selectedCount === 0}
|
||||
icon={<UsergroupAddOutlined />}
|
||||
onClick={onOpenBatchAssign}
|
||||
>
|
||||
批量分配
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
disabled={selectedCount === 0}
|
||||
icon={<UsergroupAddOutlined />}
|
||||
onClick={onOpenBatchAssign}
|
||||
>
|
||||
批量分配
|
||||
</Button>
|
||||
<Button
|
||||
disabled={selectedCount === 0 && tags.length === 0}
|
||||
icon={<UndoOutlined />}
|
||||
onClick={onOpenBatchUnassign}
|
||||
title="可撤回勾选账号,也可按标签撤回全部已分配账号"
|
||||
>
|
||||
批量撤回
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{canDelete && (
|
||||
<Popconfirm
|
||||
|
||||
Reference in New Issue
Block a user