feat(douyu): 精英宝典导入支持按多行账户名筛选

对齐 Cookie 自定义 CK 的粘贴导入方式,按账户名精确匹配并保持粘贴顺序。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-29 15:02:53 +08:00
co-authored by Cursor
parent 1ac6a13a2c
commit 4806d4bbf1
4 changed files with 238 additions and 59 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ export const douyuApi = {
api.get<DouyuTaskAccountItem[], DouyuTaskAccountItem[]>('/douyu/accounts', { params }),
listAccountsPaged: (params: PageParams & { ids?: string; tag?: string }) =>
api.get<PaginatedResponse<DouyuTaskAccountItem>, PaginatedResponse<DouyuTaskAccountItem>>('/douyu/accounts', { params }),
listAccountIds: (params?: { search?: string; tag?: string }) =>
listAccountIds: (params?: { search?: string; tag?: string; account_names?: string }) =>
api.get<{ account_ids: number[]; total: number }, { account_ids: number[]; total: number }>('/douyu/accounts/ids', { params }),
getConfig: () => api.get<DouyuConfig, DouyuConfig>('/douyu/config'),
getRechargeChannel: () => api.get<DouyuRechargeChannel, DouyuRechargeChannel>('/douyu/recharge-channel'),
@@ -4,7 +4,7 @@ import {
} from 'antd';
import type { TableProps } from 'antd';
import {
CheckCircleOutlined, ColumnWidthOutlined, CopyOutlined, CreditCardOutlined, ExclamationCircleOutlined, FieldTimeOutlined, GiftOutlined,
CheckCircleOutlined, ColumnWidthOutlined, CopyOutlined, CreditCardOutlined, ExclamationCircleOutlined, FieldTimeOutlined, FilterOutlined, GiftOutlined,
ImportOutlined, QrcodeOutlined, ReloadOutlined,
SearchOutlined, SettingOutlined, ShoppingOutlined, StopOutlined,
} from '@ant-design/icons';
@@ -61,6 +61,16 @@ import {
const { Text } = Typography;
function parseAccountNames(text: string): string[] {
const seen = new Set<string>();
return text
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.split('\n')
.map((name) => name.trim())
.filter((name) => name.length > 0 && !seen.has(name) && seen.add(name));
}
export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) {
const isPeaceHandbook = handbook === 'peace';
const isEsportsHandbook = handbook === 'esports';
@@ -110,6 +120,8 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
const [importAllLoading, setImportAllLoading] = useState(false);
const [importPage, setImportPage] = useState(1);
const [importTotal, setImportTotal] = useState(0);
const [importNamesText, setImportNamesText] = useState('');
const [importAccountNames, setImportAccountNames] = useState<string[]>([]);
const importPageSize = 20;
// 绑定/支付弹窗以 task 为唯一数据源;二维码弹窗支持多账号并行(Tabs 切换)
@@ -783,10 +795,11 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
const loadImportPool = useCallback(async () => {
setImportLoading(true);
try {
// 候选池 = 账号库按搜索/标签过滤,剔除已导入工作台的账号
// 候选池 = 账号库按搜索/标签/粘贴账户名过滤,剔除已导入工作台的账号
const result = await douyuApi.listAccountsPaged({
search: importSearch.trim() || undefined,
tag: importTag || undefined,
account_names: importAccountNames.length > 0 ? importAccountNames.join('\n') : undefined,
page: importPage,
page_size: importPageSize,
});
@@ -795,17 +808,17 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
} finally {
setImportLoading(false);
}
}, [importPage, importSearch, importTag, workbenchIds]);
}, [importAccountNames, importPage, importSearch, importTag, workbenchIds]);
useEffect(() => { if (importOpen) void loadImportPool(); }, [importOpen, loadImportPool]);
useEffect(() => {
if (!importOpen) return;
accountApi.listTags().then(setImportTags).catch(() => {});
}, [importOpen]);
// 切换搜索/标签时清空已选,避免计数与可见行不一致
// 切换搜索/标签/自定义账户名时清空已选,避免计数与可见行不一致
useEffect(() => {
setImportPage(1);
setImportSelectedIds([]);
}, [importSearch, importTag]);
}, [importSearch, importTag, importAccountNames]);
useEffect(() => { setImportSelectedIds([]); }, [importPage]);
const saveWorkbenchIds = async (nextIds: number[]) => {
@@ -828,25 +841,50 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
return [...prev, ...toImport.filter((a) => !existing.has(a.id))];
});
setImportSelectedIds([]);
setImportOpen(false);
closeImportModal();
message.success(`已导入 ${toImport.length} 个账号`);
};
const closeImportModal = () => {
setImportOpen(false);
setImportNamesText('');
setImportAccountNames([]);
setImportSelectedIds([]);
};
const applyImportNames = () => {
const names = parseAccountNames(importNamesText);
if (names.length === 0) {
message.warning('请先粘贴斗鱼账户名,每行一个');
return;
}
setImportAccountNames(names);
message.success(`已载入 ${names.length} 个账户名,正在筛选账号`);
};
const clearImportNames = () => {
setImportNamesText('');
setImportAccountNames([]);
};
const importAllAccounts = async () => {
const names = importAccountNames.length > 0
? importAccountNames
: parseAccountNames(importNamesText);
setImportAllLoading(true);
try {
const result = await douyuApi.listAccountIds({
search: importSearch.trim() || undefined,
tag: importTag || undefined,
account_names: names.length > 0 ? names.join('\n') : undefined,
});
const imported = result.account_ids.filter((id) => !workbenchIds.includes(id));
if (imported.length === 0) {
message.info('当前筛选条件下没有可新增的账号');
message.info(names.length > 0 ? '没有匹配到可新增的账号' : '当前筛选条件下没有可新增的账号');
return;
}
await saveWorkbenchIds([...new Set([...workbenchIds, ...imported])]);
setImportSelectedIds([]);
setImportOpen(false);
closeImportModal();
message.success(`已一键导入 ${imported.length} 个账号`);
} catch (error) {
message.error(getErrorMessage(error));
@@ -1906,10 +1944,29 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
<Modal
title="导入账号到操作台"
open={importOpen}
onCancel={() => setImportOpen(false)}
onCancel={closeImportModal}
footer={null}
width={700}
width={720}
>
<Space direction="vertical" style={{ width: '100%', marginBottom: 12 }} size={8}>
<Input.TextArea
value={importNamesText}
onChange={(e) => setImportNamesText(e.target.value)}
placeholder="从 Excel 复制斗鱼账户名后粘贴到这里,每行一个"
autoSize={{ minRows: 3, maxRows: 6 }}
/>
<Space wrap>
<Button type="primary" icon={<FilterOutlined />} onClick={applyImportNames}>
按账号名筛选
</Button>
<Button onClick={clearImportNames} disabled={!importNamesText && importAccountNames.length === 0}>
清空
</Button>
{importAccountNames.length > 0 && (
<Text type="secondary">已载入 {importAccountNames.length} 个账户名,匹配结果共 {importTotal} 条</Text>
)}
</Space>
</Space>
<div style={{ marginBottom: 12, display: 'flex', flexWrap: 'wrap', gap: 8 }}>
<Space wrap>
<Input
@@ -1937,7 +1994,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
导入本页 ({importPool.length})
</Button>
<Button loading={importAllLoading} onClick={() => void importAllAccounts()}>
一键导入全部
{(importAccountNames.length > 0 || importNamesText.trim()) ? '导入全部匹配' : '一键导入全部'}
</Button>
<Button type="primary" onClick={() => void importAccounts(importSelectedIds)} disabled={importSelectedIds.length === 0}>
导入选中 ({importSelectedIds.length})