feat(douyu): 支持工作台分页与一键导入
This commit is contained in:
@@ -211,6 +211,37 @@ def list_task_accounts(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/accounts/ids")
|
||||||
|
def list_task_account_ids(
|
||||||
|
search: str = Query(""),
|
||||||
|
tag: str = Query(""),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""返回当前用户可导入工作台的全部账号 ID,不受列表分页限制。"""
|
||||||
|
query = _visible_task_accounts_query(db, current)
|
||||||
|
tag_text = (tag or "").strip()
|
||||||
|
if tag_text:
|
||||||
|
query = query.filter(Account.tag == tag_text)
|
||||||
|
search_text = (search or "").strip()
|
||||||
|
if search_text:
|
||||||
|
pattern = f"%{search_text}%"
|
||||||
|
query = query.filter(or_(
|
||||||
|
Account.username.ilike(pattern),
|
||||||
|
Account.uid.ilike(pattern),
|
||||||
|
Account.nickname.ilike(pattern),
|
||||||
|
Account.tag.ilike(pattern),
|
||||||
|
Account.game_name.ilike(pattern),
|
||||||
|
Account.esports_game_name.ilike(pattern),
|
||||||
|
Account.xpd_game_name.ilike(pattern),
|
||||||
|
))
|
||||||
|
account_ids = [
|
||||||
|
account_id
|
||||||
|
for account_id, in query.enable_eagerloads(False).order_by(Account.id.desc()).with_entities(Account.id).all()
|
||||||
|
]
|
||||||
|
return {"account_ids": account_ids, "total": len(account_ids)}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config", response_model=DouyuConfigOut)
|
@router.get("/config", response_model=DouyuConfigOut)
|
||||||
def get_config(
|
def get_config(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ export const douyuApi = {
|
|||||||
taskTypes: () => api.get<Record<string, string>, Record<string, string>>('/douyu/task-types'),
|
taskTypes: () => api.get<Record<string, string>, Record<string, string>>('/douyu/task-types'),
|
||||||
listAccounts: (params?: { search?: string; tag?: string; ids?: string }) =>
|
listAccounts: (params?: { search?: string; tag?: string; ids?: string }) =>
|
||||||
api.get<DouyuTaskAccountItem[], DouyuTaskAccountItem[]>('/douyu/accounts', { params }),
|
api.get<DouyuTaskAccountItem[], DouyuTaskAccountItem[]>('/douyu/accounts', { params }),
|
||||||
listAccountsPaged: (params: PageParams & { ids?: string }) =>
|
listAccountsPaged: (params: PageParams & { ids?: string; tag?: string }) =>
|
||||||
api.get<PaginatedResponse<DouyuTaskAccountItem>, PaginatedResponse<DouyuTaskAccountItem>>('/douyu/accounts', { params }),
|
api.get<PaginatedResponse<DouyuTaskAccountItem>, PaginatedResponse<DouyuTaskAccountItem>>('/douyu/accounts', { params }),
|
||||||
|
listAccountIds: (params?: { search?: string; tag?: string }) =>
|
||||||
|
api.get<{ account_ids: number[]; total: number }, { account_ids: number[]; total: number }>('/douyu/accounts/ids', { params }),
|
||||||
getConfig: () => api.get<DouyuConfig, DouyuConfig>('/douyu/config'),
|
getConfig: () => api.get<DouyuConfig, DouyuConfig>('/douyu/config'),
|
||||||
updateConfig: (data: Partial<DouyuConfig>) => api.put<DouyuConfig, DouyuConfig>('/douyu/config', data),
|
updateConfig: (data: Partial<DouyuConfig>) => api.put<DouyuConfig, DouyuConfig>('/douyu/config', data),
|
||||||
listGoods: () => api.get<DouyuGoodsItem[], DouyuGoodsItem[]>('/douyu/goods'),
|
listGoods: () => api.get<DouyuGoodsItem[], DouyuGoodsItem[]>('/douyu/goods'),
|
||||||
|
|||||||
@@ -348,6 +348,8 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
const v = Number(localStorage.getItem('douyu_task_account_page_size'));
|
const v = Number(localStorage.getItem('douyu_task_account_page_size'));
|
||||||
return [10, 20, 50, 100].includes(v) ? v : 20;
|
return [10, 20, 50, 100].includes(v) ? v : 20;
|
||||||
});
|
});
|
||||||
|
const [accountPage, setAccountPage] = useState(1);
|
||||||
|
const [accountTotal, setAccountTotal] = useState(0);
|
||||||
const tasksLoadingRef = useRef(false);
|
const tasksLoadingRef = useRef(false);
|
||||||
|
|
||||||
const [importOpen, setImportOpen] = useState(false);
|
const [importOpen, setImportOpen] = useState(false);
|
||||||
@@ -357,6 +359,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
const [importTags, setImportTags] = useState<string[]>([]);
|
const [importTags, setImportTags] = useState<string[]>([]);
|
||||||
const [importSelectedIds, setImportSelectedIds] = useState<number[]>([]);
|
const [importSelectedIds, setImportSelectedIds] = useState<number[]>([]);
|
||||||
const [importLoading, setImportLoading] = useState(false);
|
const [importLoading, setImportLoading] = useState(false);
|
||||||
|
const [importAllLoading, setImportAllLoading] = useState(false);
|
||||||
|
const [importPage, setImportPage] = useState(1);
|
||||||
|
const [importTotal, setImportTotal] = useState(0);
|
||||||
|
const importPageSize = 20;
|
||||||
|
|
||||||
// 绑定/支付弹窗以 task 为唯一数据源;二维码弹窗支持多账号并行(Tabs 切换)
|
// 绑定/支付弹窗以 task 为唯一数据源;二维码弹窗支持多账号并行(Tabs 切换)
|
||||||
const [qrTaskIds, setQrTaskIds] = useState<number[]>([]);
|
const [qrTaskIds, setQrTaskIds] = useState<number[]>([]);
|
||||||
@@ -526,7 +532,14 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [accResult, goodsResult, taskResult, cfgResult, typeResult] = await Promise.allSettled([
|
const [accResult, goodsResult, taskResult, cfgResult, typeResult] = await Promise.allSettled([
|
||||||
workbenchIds.length > 0 ? douyuApi.listAccounts({ ids: workbenchIds.join(',') }) : Promise.resolve([]),
|
workbenchIds.length > 0
|
||||||
|
? douyuApi.listAccountsPaged({
|
||||||
|
ids: workbenchIds.join(','),
|
||||||
|
search: accountSearch.trim() || undefined,
|
||||||
|
page: accountPage,
|
||||||
|
page_size: accountPageSize,
|
||||||
|
})
|
||||||
|
: Promise.resolve({ items: [] as DouyuTaskAccountItem[], total: 0, page: accountPage, page_size: accountPageSize }),
|
||||||
isPeaceHandbook
|
isPeaceHandbook
|
||||||
? douyuApi.listPeaceGoods()
|
? douyuApi.listPeaceGoods()
|
||||||
: (isEsportsHandbook ? douyuApi.listEsportsGoods() : douyuApi.listGoods()),
|
: (isEsportsHandbook ? douyuApi.listEsportsGoods() : douyuApi.listGoods()),
|
||||||
@@ -535,12 +548,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
douyuApi.taskTypes(),
|
douyuApi.taskTypes(),
|
||||||
]);
|
]);
|
||||||
if (accResult.status === 'fulfilled') {
|
if (accResult.status === 'fulfilled') {
|
||||||
setAccounts(accResult.value);
|
setAccounts(accResult.value.items);
|
||||||
// 账号被删除等场景下清理已失效的工作台 ID,避免请求残留空 ID
|
setAccountTotal(accResult.value.total);
|
||||||
if (workbenchIds.length > 0) {
|
if (accountPage > 1 && accResult.value.items.length === 0) {
|
||||||
const validIds = accResult.value.map((a) => a.id);
|
setAccountPage(Math.max(1, Math.ceil(accResult.value.total / accountPageSize)));
|
||||||
const next = workbenchIds.filter((id) => validIds.includes(id));
|
|
||||||
if (next.length !== workbenchIds.length) setWorkbenchIds(next);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (goodsResult.status === 'fulfilled') setGoods(goodsResult.value);
|
if (goodsResult.status === 'fulfilled') setGoods(goodsResult.value);
|
||||||
@@ -575,7 +586,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [canConfig, isEsportsHandbook, isPeaceHandbook, workbenchIds]);
|
}, [accountPage, accountPageSize, accountSearch, canConfig, isEsportsHandbook, isPeaceHandbook, workbenchIds]);
|
||||||
|
|
||||||
const loadTasks = useCallback(async () => {
|
const loadTasks = useCallback(async () => {
|
||||||
if (tasksLoadingRef.current) return;
|
if (tasksLoadingRef.current) return;
|
||||||
@@ -920,19 +931,29 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
setImportLoading(true);
|
setImportLoading(true);
|
||||||
try {
|
try {
|
||||||
// 候选池 = 账号库按搜索/标签过滤,剔除已导入工作台的账号
|
// 候选池 = 账号库按搜索/标签过滤,剔除已导入工作台的账号
|
||||||
const result = await douyuApi.listAccounts({ search: importSearch, tag: importTag || undefined });
|
const result = await douyuApi.listAccountsPaged({
|
||||||
setImportPool(result.filter((a) => !workbenchIds.includes(a.id)));
|
search: importSearch.trim() || undefined,
|
||||||
|
tag: importTag || undefined,
|
||||||
|
page: importPage,
|
||||||
|
page_size: importPageSize,
|
||||||
|
});
|
||||||
|
setImportPool(result.items.filter((a) => !workbenchIds.includes(a.id)));
|
||||||
|
setImportTotal(result.total);
|
||||||
} finally {
|
} finally {
|
||||||
setImportLoading(false);
|
setImportLoading(false);
|
||||||
}
|
}
|
||||||
}, [importSearch, importTag, workbenchIds]);
|
}, [importPage, importSearch, importTag, workbenchIds]);
|
||||||
useEffect(() => { if (importOpen) void loadImportPool(); }, [importOpen, loadImportPool]);
|
useEffect(() => { if (importOpen) void loadImportPool(); }, [importOpen, loadImportPool]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!importOpen) return;
|
if (!importOpen) return;
|
||||||
accountApi.listTags().then(setImportTags).catch(() => {});
|
accountApi.listTags().then(setImportTags).catch(() => {});
|
||||||
}, [importOpen]);
|
}, [importOpen]);
|
||||||
// 切换搜索/标签时清空已选,避免计数与可见行不一致
|
// 切换搜索/标签时清空已选,避免计数与可见行不一致
|
||||||
useEffect(() => { setImportSelectedIds([]); }, [importSearch, importTag]);
|
useEffect(() => {
|
||||||
|
setImportPage(1);
|
||||||
|
setImportSelectedIds([]);
|
||||||
|
}, [importSearch, importTag]);
|
||||||
|
useEffect(() => { setImportSelectedIds([]); }, [importPage]);
|
||||||
|
|
||||||
const importAccounts = (ids: number[]) => {
|
const importAccounts = (ids: number[]) => {
|
||||||
const toImport = importPool.filter((a) => ids.includes(a.id));
|
const toImport = importPool.filter((a) => ids.includes(a.id));
|
||||||
@@ -946,6 +967,29 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
message.success(`已导入 ${toImport.length} 个账号`);
|
message.success(`已导入 ${toImport.length} 个账号`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const importAllAccounts = async () => {
|
||||||
|
setImportAllLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await douyuApi.listAccountIds({
|
||||||
|
search: importSearch.trim() || undefined,
|
||||||
|
tag: importTag || undefined,
|
||||||
|
});
|
||||||
|
const imported = result.account_ids.filter((id) => !workbenchIds.includes(id));
|
||||||
|
if (imported.length === 0) {
|
||||||
|
message.info('当前筛选条件下没有可新增的账号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setWorkbenchIds((prev) => [...new Set([...prev, ...imported])]);
|
||||||
|
setImportSelectedIds([]);
|
||||||
|
setImportOpen(false);
|
||||||
|
message.success(`已一键导入 ${imported.length} 个账号`);
|
||||||
|
} catch (error) {
|
||||||
|
message.error(getErrorMessage(error));
|
||||||
|
} finally {
|
||||||
|
setImportAllLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const removeSelected = () => {
|
const removeSelected = () => {
|
||||||
const sel = new Set(selectedIds);
|
const sel = new Set(selectedIds);
|
||||||
setWorkbenchIds((prev) => prev.filter((id) => !sel.has(id)));
|
setWorkbenchIds((prev) => prev.filter((id) => !sel.has(id)));
|
||||||
@@ -1427,18 +1471,6 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const filteredAccounts = useMemo(() => {
|
|
||||||
if (!accountSearch.trim()) return accounts;
|
|
||||||
const kw = accountSearch.toLowerCase();
|
|
||||||
return accounts.filter((a) =>
|
|
||||||
(a.nickname || '').toLowerCase().includes(kw)
|
|
||||||
|| (a.username || '').toLowerCase().includes(kw)
|
|
||||||
|| (a.uid || '').toLowerCase().includes(kw)
|
|
||||||
|| (a.game_name || '').toLowerCase().includes(kw)
|
|
||||||
|| (a.esports_game_name || '').toLowerCase().includes(kw),
|
|
||||||
);
|
|
||||||
}, [accounts, accountSearch]);
|
|
||||||
|
|
||||||
const [configFormValues, setConfigFormValues] = useState<DouyuConfig | null>(null);
|
const [configFormValues, setConfigFormValues] = useState<DouyuConfig | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (configOpen && config) setConfigFormValues({ ...config });
|
if (configOpen && config) setConfigFormValues({ ...config });
|
||||||
@@ -1700,7 +1732,11 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
<Space>
|
<Space>
|
||||||
<Input
|
<Input
|
||||||
size="small" placeholder="搜索账号/昵称/游戏名" prefix={<SearchOutlined />}
|
size="small" placeholder="搜索账号/昵称/游戏名" prefix={<SearchOutlined />}
|
||||||
value={accountSearch} onChange={(e) => setAccountSearch(e.target.value)}
|
value={accountSearch}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAccountSearch(e.target.value);
|
||||||
|
setAccountPage(1);
|
||||||
|
}}
|
||||||
style={{ width: 200 }} allowClear
|
style={{ width: 200 }} allowClear
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -1723,15 +1759,18 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
onChange: (keys) => setSelectedIds(keys.map(Number)),
|
onChange: (keys) => setSelectedIds(keys.map(Number)),
|
||||||
}}
|
}}
|
||||||
columns={accountColumns}
|
columns={accountColumns}
|
||||||
dataSource={filteredAccounts}
|
dataSource={accounts}
|
||||||
pagination={{
|
pagination={{
|
||||||
|
current: accountPage,
|
||||||
pageSize: accountPageSize,
|
pageSize: accountPageSize,
|
||||||
|
total: accountTotal,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
pageSizeOptions: [10, 20, 50, 100],
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
size: 'small',
|
size: 'small',
|
||||||
showLessItems: true,
|
showLessItems: true,
|
||||||
showTotal: (total) => `共 ${total} 条`,
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
onChange: (_page, size) => {
|
onChange: (page, size) => {
|
||||||
|
setAccountPage(size === accountPageSize ? page : 1);
|
||||||
if (size !== accountPageSize) {
|
if (size !== accountPageSize) {
|
||||||
setAccountPageSize(size);
|
setAccountPageSize(size);
|
||||||
localStorage.setItem('douyu_task_account_page_size', String(size));
|
localStorage.setItem('douyu_task_account_page_size', String(size));
|
||||||
@@ -1834,7 +1873,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
<div style={{ flex: 1, minHeight: 0, display: 'flex', gap: 12, overflow: 'hidden' }}>
|
<div style={{ flex: 1, minHeight: 0, display: 'flex', gap: 12, overflow: 'hidden' }}>
|
||||||
<Card
|
<Card
|
||||||
size="small" title="账号"
|
size="small" title="账号"
|
||||||
extra={<Tag color="blue">已选 {selectedIds.length}/{accounts.length}</Tag>}
|
extra={<Tag color="blue">已选 {selectedIds.length}/{accountTotal}</Tag>}
|
||||||
style={{ flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
style={{ flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||||
styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}
|
styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}
|
||||||
>
|
>
|
||||||
@@ -1863,7 +1902,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
</Card>
|
</Card>
|
||||||
<Card
|
<Card
|
||||||
size="small" title="账号"
|
size="small" title="账号"
|
||||||
extra={<Tag color="blue">已选 {selectedIds.length}/{accounts.length}</Tag>}
|
extra={<Tag color="blue">已选 {selectedIds.length}/{accountTotal}</Tag>}
|
||||||
style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||||
styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}
|
styles={{ body: { flex: 1, minHeight: 0, padding: '4px 6px', overflow: 'hidden', display: 'flex', flexDirection: 'column' } }}
|
||||||
>
|
>
|
||||||
@@ -1906,30 +1945,40 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
footer={null}
|
footer={null}
|
||||||
width={700}
|
width={700}
|
||||||
>
|
>
|
||||||
<Space style={{ marginBottom: 12, width: '100%', justifyContent: 'space-between' }}>
|
<div style={{ marginBottom: 12, display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||||
<Space>
|
<Space wrap>
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索账号/昵称" prefix={<SearchOutlined />}
|
placeholder="搜索账号/昵称" prefix={<SearchOutlined />}
|
||||||
value={importSearch} onChange={(e) => setImportSearch(e.target.value)}
|
value={importSearch}
|
||||||
|
onChange={(e) => {
|
||||||
|
setImportSearch(e.target.value);
|
||||||
|
setImportPage(1);
|
||||||
|
}}
|
||||||
style={{ width: 220 }}
|
style={{ width: 220 }}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
placeholder="按标签筛选" allowClear showSearch
|
placeholder="按标签筛选" allowClear showSearch
|
||||||
value={importTag || undefined}
|
value={importTag || undefined}
|
||||||
onChange={(value?: string) => setImportTag(value || '')}
|
onChange={(value?: string) => {
|
||||||
|
setImportTag(value || '');
|
||||||
|
setImportPage(1);
|
||||||
|
}}
|
||||||
style={{ width: 160 }}
|
style={{ width: 160 }}
|
||||||
options={importTags.map((t) => ({ value: t, label: t }))}
|
options={importTags.map((t) => ({ value: t, label: t }))}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space wrap style={{ width: '100%', justifyContent: 'flex-start' }}>
|
||||||
<Button onClick={() => importAccounts(importPool.map((a) => a.id))}>
|
<Button onClick={() => importAccounts(importPool.map((a) => a.id))}>
|
||||||
导入全部 ({importPool.length})
|
导入本页 ({importPool.length})
|
||||||
|
</Button>
|
||||||
|
<Button loading={importAllLoading} onClick={() => void importAllAccounts()}>
|
||||||
|
一键导入全部
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="primary" onClick={() => importAccounts(importSelectedIds)} disabled={importSelectedIds.length === 0}>
|
<Button type="primary" onClick={() => importAccounts(importSelectedIds)} disabled={importSelectedIds.length === 0}>
|
||||||
导入选中 ({importSelectedIds.length})
|
导入选中 ({importSelectedIds.length})
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</div>
|
||||||
<Table
|
<Table
|
||||||
rowKey="id" size="small" loading={importLoading}
|
rowKey="id" size="small" loading={importLoading}
|
||||||
rowSelection={{ selectedRowKeys: importSelectedIds, onChange: (keys) => setImportSelectedIds(keys.map(Number)) }}
|
rowSelection={{ selectedRowKeys: importSelectedIds, onChange: (keys) => setImportSelectedIds(keys.map(Number)) }}
|
||||||
@@ -1949,7 +1998,15 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
render: (v) => v ?? '-',
|
render: (v) => v ?? '-',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
pagination={{ pageSize: 10 }}
|
pagination={{
|
||||||
|
current: importPage,
|
||||||
|
pageSize: importPageSize,
|
||||||
|
total: importTotal,
|
||||||
|
size: 'small',
|
||||||
|
showLessItems: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
onChange: setImportPage,
|
||||||
|
}}
|
||||||
locale={{ emptyText: '没有可导入的账号' }}
|
locale={{ emptyText: '没有可导入的账号' }}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
Reference in New Issue
Block a user