feat: scope blacklists and paginate phone segments

This commit is contained in:
hectorzhao
2026-07-10 13:42:39 +08:00
parent ff0756ff9c
commit d5e668a2e6
18 changed files with 322 additions and 86 deletions
+45 -12
View File
@@ -1,11 +1,13 @@
import { useEffect, useMemo, useState } from 'react';
import { Plus, Search, Trash2 } from 'lucide-react';
import { Breadcrumb, Button, Input, Modal, Select, Table, Textarea, type TableColumn } from '@/components/ui';
import { adminApi, type DictionaryItem, type TenantOption } from '@/api/adminApi';
import { adminApi, type DictionaryItem, type EnterpriseApplication, type TenantOption } from '@/api/adminApi';
type EnterpriseBlacklistItem = DictionaryItem & {
tenantId?: string;
applicationId?: string;
tenant?: TenantOption;
application?: EnterpriseApplication;
phoneNumber?: string;
reason?: string | null;
};
@@ -13,18 +15,27 @@ type EnterpriseBlacklistItem = DictionaryItem & {
export function AdminEnterpriseBlacklistPage() {
const [items, setItems] = useState<EnterpriseBlacklistItem[]>([]);
const [tenants, setTenants] = useState<TenantOption[]>([]);
const [applications, setApplications] = useState<EnterpriseApplication[]>([]);
const [keyword, setKeyword] = useState('');
const [tenantId, setTenantId] = useState('');
const [filterTenantId, setFilterTenantId] = useState('');
const [filterApplicationId, setFilterApplicationId] = useState('');
const [formTenantId, setFormTenantId] = useState('');
const [formApplicationId, setFormApplicationId] = useState('');
const [phone, setPhone] = useState('');
const [reason, setReason] = useState('');
const [modalOpen, setModalOpen] = useState(false);
const [error, setError] = useState('');
function loadData() {
Promise.all([adminApi.listEnterpriseBlacklist({ keyword }), adminApi.listTenants()])
.then(([blacklist, tenantItems]) => {
Promise.all([
adminApi.listEnterpriseBlacklist({ tenantId: filterTenantId || undefined, applicationId: filterApplicationId || undefined, keyword }),
adminApi.listTenants(),
adminApi.listEnterpriseApplications(),
])
.then(([blacklist, tenantItems, applicationItems]) => {
setItems(blacklist as EnterpriseBlacklistItem[]);
setTenants(tenantItems);
setApplications(applicationItems);
setError('');
})
.catch((failure: Error) => setError(failure.message || '企业黑名单加载失败'));
@@ -35,12 +46,15 @@ export function AdminEnterpriseBlacklistPage() {
}, []);
const filteredItems = useMemo(() => items.filter((item) => {
const text = [item.tenant?.name, item.phoneNumber, item.reason, item.status].join(' ');
const text = [item.tenant?.name, item.application?.name, item.phoneNumber, item.reason, item.status].join(' ');
return !keyword || text.includes(keyword);
}), [items, keyword]);
const filterApplications = applications.filter((application) => application.tenantId === filterTenantId && application.status !== 'deleted');
const modalApplications = applications.filter((application) => application.tenantId === formTenantId && application.status !== 'deleted');
const columns = useMemo<Array<TableColumn<EnterpriseBlacklistItem>>>(() => [
{ key: 'enterprise', title: '企业名称', width: '180px', render: (record) => <strong>{record.tenant?.name ?? record.tenantId}</strong> },
{ key: 'application', title: '应用名称', width: '180px', render: (record) => <span>{record.application?.name ?? record.applicationId}</span> },
{ key: 'phone', title: '手机号码', width: '150px', render: (record) => <strong>{record.phoneNumber}</strong> },
{ key: 'createdAt', title: '入库时间', width: '170px', render: (record) => record.createdAt ?? '-' },
{ key: 'reason', title: '入库原因', render: (record) => record.reason ?? '-' },
@@ -59,9 +73,10 @@ export function AdminEnterpriseBlacklistPage() {
], []);
function addItem() {
adminApi.createEnterpriseBlacklist({ tenantId, phoneNumber: phone, reason, status: 'active' })
adminApi.createEnterpriseBlacklist({ tenantId: formTenantId, applicationId: formApplicationId, phoneNumber: phone, reason, status: 'active' })
.then(() => {
setTenantId('');
setFormTenantId('');
setFormApplicationId('');
setPhone('');
setReason('');
setModalOpen(false);
@@ -85,13 +100,25 @@ export function AdminEnterpriseBlacklistPage() {
<Input
label="搜索"
onChange={(event) => setKeyword(event.target.value)}
placeholder="搜索企业、手机号或原因"
placeholder="搜索企业、应用、手机号或原因"
prefix={<Search size={16} />}
value={keyword}
/>
<Select
label="企业"
onChange={(event) => { setFilterTenantId(event.target.value); setFilterApplicationId(''); }}
options={[{ label: '全部企业', value: '' }, ...tenants.map((item) => ({ label: item.name, value: item.id }))]}
value={filterTenantId}
/>
<Select
label="短信应用"
onChange={(event) => setFilterApplicationId(event.target.value)}
options={[{ label: filterTenantId ? '全部应用' : '请先选择企业', value: '' }, ...filterApplications.map((item) => ({ label: item.name, value: item.id }))]}
value={filterApplicationId}
/>
<div className="admin-security-filter__actions">
<Button icon={<Search size={16} />} onClick={loadData}></Button>
<Button onClick={() => setKeyword('')} variant="ghost"></Button>
<Button onClick={() => { setKeyword(''); setFilterTenantId(''); setFilterApplicationId(''); }} variant="ghost"></Button>
</div>
</div>
@@ -103,7 +130,7 @@ export function AdminEnterpriseBlacklistPage() {
footer={(
<>
<Button onClick={() => setModalOpen(false)} variant="ghost"></Button>
<Button disabled={!tenantId || !phone} onClick={addItem}></Button>
<Button disabled={!formTenantId || !formApplicationId || !phone} onClick={addItem}></Button>
</>
)}
onClose={() => setModalOpen(false)}
@@ -113,9 +140,15 @@ export function AdminEnterpriseBlacklistPage() {
<div className="admin-security-form">
<Select
label="企业"
onChange={(event) => setTenantId(event.target.value)}
onChange={(event) => { setFormTenantId(event.target.value); setFormApplicationId(''); }}
options={[{ label: '请选择企业', value: '' }, ...tenants.map((item) => ({ label: item.name, value: item.id }))]}
value={tenantId}
value={formTenantId}
/>
<Select
label="短信应用"
onChange={(event) => setFormApplicationId(event.target.value)}
options={[{ label: formTenantId ? '请选择应用' : '请先选择企业', value: '' }, ...modalApplications.map((item) => ({ label: item.name, value: item.id }))]}
value={formApplicationId}
/>
<Input label="手机号码" onChange={(event) => setPhone(event.target.value)} placeholder="请输入手机号码" value={phone} />
<Textarea label="入库原因" onChange={(event) => setReason(event.target.value)} placeholder="请输入入库原因" rows={3} value={reason} />