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
+70 -38
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { Plus, RadioTower, Search, Smartphone } from 'lucide-react';
import { Breadcrumb, Button, Input, Modal, Select, Table, Tabs, type TableColumn } from '@/components/ui';
import { Plus, Search } from 'lucide-react';
import { Breadcrumb, Button, Input, Modal, Pagination, Select, Table, Tabs, type TableColumn } from '@/components/ui';
import { adminApi, type DictionaryItem } from '@/api/adminApi';
type PhoneSegment = DictionaryItem & {
@@ -18,6 +18,7 @@ type CarrierRule = DictionaryItem & {
};
export function AdminPhoneSegmentsPage() {
const pageSize = 20;
const [segments, setSegments] = useState<PhoneSegment[]>([]);
const [rules, setRules] = useState<CarrierRule[]>([]);
const [activeTab, setActiveTab] = useState<'segments' | 'rules'>('segments');
@@ -33,25 +34,48 @@ export function AdminPhoneSegmentsPage() {
const [rulePriority, setRulePriority] = useState('100');
const [ruleRemark, setRuleRemark] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [segmentQuery, setSegmentQuery] = useState('');
const [page, setPage] = useState(1);
const [pageCursors, setPageCursors] = useState<Array<string | undefined>>([undefined]);
const [hasMore, setHasMore] = useState(false);
const [nextCursor, setNextCursor] = useState<string | null>(null);
const [reloadKey, setReloadKey] = useState(0);
function loadData() {
Promise.all([adminApi.listPhoneSegments(), adminApi.listPhoneCarrierRules()])
.then(([segmentItems, ruleItems]) => {
setSegments(segmentItems as PhoneSegment[]);
useEffect(() => {
const timer = window.setTimeout(() => {
setSegmentQuery(keyword.trim());
setPage(1);
setPageCursors([undefined]);
}, 300);
return () => window.clearTimeout(timer);
}, [keyword]);
useEffect(() => {
let cancelled = false;
setLoading(true);
Promise.all([
adminApi.listPhoneSegments({ keyword: segmentQuery || undefined, cursor: pageCursors[page - 1], pageSize }),
adminApi.listPhoneCarrierRules(),
])
.then(([segmentPage, ruleItems]) => {
if (cancelled) return;
setSegments(segmentPage.items as PhoneSegment[]);
setHasMore(segmentPage.hasMore);
setNextCursor(segmentPage.nextCursor);
setRules(ruleItems as CarrierRule[]);
setError('');
})
.catch((failure: Error) => setError(failure.message || '手机号段加载失败'));
}
useEffect(() => {
loadData();
}, []);
const filteredSegments = useMemo(
() => segments.filter((segment) => [segment.prefix, segment.carrier, segment.province, segment.city].some((value) => String(value ?? '').includes(keyword))),
[keyword, segments],
);
.catch((failure: Error) => {
if (!cancelled) setError(failure.message || '手机号段加载失败');
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [page, pageCursors, reloadKey, segmentQuery]);
const filteredRules = useMemo(
() => rules.filter((rule) => [rule.carrier, rule.pattern, rule.remark].some((value) => String(value ?? '').includes(keyword))),
@@ -65,7 +89,9 @@ export function AdminPhoneSegmentsPage() {
setProvince('');
setCity('');
setCreating(false);
loadData();
setPage(1);
setPageCursors([undefined]);
setReloadKey((current) => current + 1);
})
.catch((failure: Error) => setError(failure.message || '手机号段新增失败'));
}
@@ -76,7 +102,7 @@ export function AdminPhoneSegmentsPage() {
setRulePattern('');
setRuleRemark('');
setCreatingRule(false);
loadData();
setReloadKey((current) => current + 1);
})
.catch((failure: Error) => setError(failure.message || '运营商区分规则新增失败'));
}
@@ -106,23 +132,6 @@ export function AdminPhoneSegmentsPage() {
</div>
{error ? <p className="form-error">{error}</p> : null}
<div className="phone-segment-overview">
<section>
<span><Smartphone size={20} /></span>
<div>
<strong>{segments.length}</strong>
<p></p>
</div>
</section>
<section>
<span><RadioTower size={20} /></span>
<div>
<strong>{rules.length}</strong>
<p></p>
</div>
</section>
</div>
<div className="surface admin-system-toolbar phone-segment-toolbar">
<Input onChange={(event) => setKeyword(event.target.value)} placeholder={activeTab === 'segments' ? '搜索手机号段、运营商、省份或城市' : '搜索运营商、正则或备注'} prefix={<Search size={16} />} value={keyword} />
<Button icon={<Plus size={16} />} onClick={() => activeTab === 'segments' ? setCreating(true) : setCreatingRule(true)}>
@@ -135,8 +144,31 @@ export function AdminPhoneSegmentsPage() {
onChange={(value) => setActiveTab(value as 'segments' | 'rules')}
value={activeTab}
items={[
{ label: `手机号段 ${segments.length}`, value: 'segments', content: <Table columns={columns} data={filteredSegments} emptyText="暂无手机号段" rowKey="id" /> },
{ label: `运营商区分规则 ${rules.length}`, value: 'rules', content: <Table columns={ruleColumns} data={filteredRules} emptyText="暂无运营商区分规则" rowKey="id" /> },
{
label: '手机号段',
value: 'segments',
content: (
<>
<Table columns={columns} data={segments} emptyText={loading ? '加载中...' : '暂无手机号段'} pagination={false} rowKey="id" />
<Pagination
page={page}
previousDisabled={page <= 1 || loading}
nextDisabled={!hasMore || loading}
onPrevious={() => setPage((current) => Math.max(1, current - 1))}
onNext={() => {
if (!nextCursor) return;
setPageCursors((current) => {
const updated = [...current];
updated[page] = nextCursor;
return updated;
});
setPage((current) => current + 1);
}}
/>
</>
),
},
{ label: '运营商区分规则', value: 'rules', content: <Table columns={ruleColumns} data={filteredRules} emptyText="暂无运营商区分规则" rowKey="id" /> },
]}
/>
</div>