Files
lislgosms/src/apps/admin/AdminPhoneSegmentsPage.tsx
T

294 lines
12 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { Database, ListFilter, Plus, RotateCcw, Search, Trash2 } from 'lucide-react';
import { Breadcrumb, Button, Input, Modal, Pagination, Select, Table, Tabs, type TableColumn } from '@/components/ui';
import { adminApi, type DictionaryItem } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
type PhoneSegment = DictionaryItem & {
prefix?: string;
carrier?: string;
province?: string | null;
city?: string | null;
};
type CarrierRule = DictionaryItem & {
carrier?: string;
pattern?: string;
priority?: number;
remark?: string | null;
};
export function AdminPhoneSegmentsPage() {
const pageSize = 25;
const [segments, setSegments] = useState<PhoneSegment[]>([]);
const [segmentTotal, setSegmentTotal] = useState(0);
const [rules, setRules] = useState<CarrierRule[]>([]);
const [ruleTotal, setRuleTotal] = useState(0);
const [activeTab, setActiveTab] = useState<'segments' | 'rules'>('segments');
const [keyword, setKeyword] = useState('');
const [creating, setCreating] = useState(false);
const [creatingRule, setCreatingRule] = useState(false);
const [prefix, setPrefix] = useState('');
const [carrier, setCarrier] = useState('中国移动');
const [province, setProvince] = useState('');
const [city, setCity] = useState('');
const [ruleCarrier, setRuleCarrier] = useState('mobile');
const [rulePattern, setRulePattern] = useState('');
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 [rulePage, setRulePage] = useState(1);
const [reloadKey, setReloadKey] = useState(0);
const [deleteTarget, setDeleteTarget] = useState<PhoneSegment | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
Promise.all([
adminApi.listPhoneSegments({ keyword: segmentQuery || undefined, page, pageSize }),
adminApi.listPhoneCarrierRules({ keyword: segmentQuery || undefined, page: rulePage, pageSize }),
])
.then(([segmentPage, ruleResponse]) => {
if (cancelled) return;
setSegments(segmentPage.items as PhoneSegment[]);
setSegmentTotal(segmentPage.total);
setRules(ruleResponse.items as CarrierRule[]);
setRuleTotal(ruleResponse.total);
setError('');
})
.catch((failure: Error) => {
if (!cancelled) setError(failure.message || '手机号段加载失败');
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [page, reloadKey, rulePage, segmentQuery]);
const segmentTotalPages = Math.max(1, Math.ceil(segmentTotal / pageSize));
const ruleTotalPages = Math.max(1, Math.ceil(ruleTotal / pageSize));
function createSegment() {
adminApi.createPhoneSegment({ prefix, carrier, province, city })
.then(() => {
setPrefix('');
setProvince('');
setCity('');
setCreating(false);
setPage(1);
setReloadKey((current) => current + 1);
})
.catch((failure: Error) => setError(failure.message || '手机号段新增失败'));
}
function createCarrierRule() {
adminApi.createPhoneCarrierRule({ carrier: ruleCarrier, pattern: rulePattern, priority: Number(rulePriority) || 100, remark: ruleRemark })
.then(() => {
setRulePattern('');
setRuleRemark('');
setCreatingRule(false);
setReloadKey((current) => current + 1);
})
.catch((failure: Error) => setError(failure.message || '运营商区分规则新增失败'));
}
function query() {
setSegmentQuery(keyword.trim());
setPage(1);
setRulePage(1);
}
function reset() {
setKeyword('');
setSegmentQuery('');
setPage(1);
setRulePage(1);
}
function deleteSegment() {
if (!deleteTarget) return;
adminApi.deletePhoneSegment(deleteTarget.id)
.then(() => {
setDeleteTarget(null);
setReloadKey((current) => current + 1);
})
.catch((failure: Error) => setError(failure.message || '手机号段删除失败'));
}
const columns = useMemo<Array<TableColumn<PhoneSegment>>>(() => [
{ key: 'segment', title: '手机号段(手机号码前7位)', width: '230px', render: (record) => <strong>{record.prefix}</strong> },
{ key: 'carrier', title: '运营商', width: '150px', render: (record) => record.carrier ?? '-' },
{ key: 'province', title: '省份', width: '140px', render: (record) => record.province ?? '-' },
{ key: 'city', title: '城市', width: '140px', render: (record) => record.city ?? '-' },
{ key: 'createdAt', title: '创建时间', width: '190px', render: (record) => formatDateTime(record.createdAt) },
{ key: 'actions', title: '操作', width: '120px', align: 'right', render: (record) => <Button icon={<Trash2 size={14} />} onClick={() => setDeleteTarget(record)} size="sm" variant="danger">删除</Button> },
], []);
const ruleColumns = useMemo<Array<TableColumn<CarrierRule>>>(() => [
{ key: 'carrier', title: '运营商', width: '140px', render: (record) => record.carrier ?? '-' },
{ key: 'pattern', title: '号码前缀正则', render: (record) => <strong>{record.pattern}</strong> },
{ key: 'priority', title: '优先级', width: '120px', render: (record) => record.priority ?? 100 },
{ key: 'remark', title: '备注', render: (record) => record.remark ?? '-' },
], []);
const queryPanel = (
<div className="phone-segment-query">
<Input label="关键词" onChange={(event) => setKeyword(event.target.value)} placeholder={activeTab === 'segments' ? '手机号段、运营商、省份或城市' : '运营商、正则或备注'} prefix={<Search size={16} />} value={keyword} />
<div className="phone-segment-query__actions">
<Button icon={<Search size={16} />} onClick={query}>查询</Button>
<Button icon={<RotateCcw size={16} />} onClick={reset} variant="ghost">重置</Button>
</div>
</div>
);
return (
<section className="page-stack admin-system-page phone-segment-workbench">
<div className="page-heading">
<div>
<Breadcrumb items={['系统管理', '手机号段库']} />
<h1>手机号段库</h1>
</div>
<Button icon={<Plus size={16} />} onClick={() => activeTab === 'segments' ? setCreating(true) : setCreatingRule(true)}>
{activeTab === 'segments' ? '新增号段' : '新增规则'}
</Button>
</div>
{error ? <p className="form-error">{error}</p> : null}
<div className="surface admin-system-table-card">
<Tabs
className="phone-segment-workbench__tabs"
onChange={(value) => {
setActiveTab(value as 'segments' | 'rules');
}}
value={activeTab}
items={[
{
label: '手机号段',
value: 'segments',
content: (
<div className="phone-segment-tab-content">
<div className="phone-segment-overview phone-segment-overview--single" aria-label="手机号段统计">
<section>
<span><Database size={20} /></span>
<div><strong>{segmentTotal.toLocaleString('zh-CN')}</strong><p>已收录手机号段</p></div>
</section>
</div>
{queryPanel}
<Table columns={columns} data={segments} emptyText={loading ? '加载中...' : '暂无手机号段'} pagination={false} rowKey="id" />
<Pagination
page={page}
previousDisabled={page <= 1 || loading}
nextDisabled={page >= segmentTotalPages || loading}
onPrevious={() => setPage((current) => Math.max(1, current - 1))}
onNext={() => setPage((current) => Math.min(segmentTotalPages, current + 1))}
onPageChange={setPage}
total={segmentTotal}
totalPages={segmentTotalPages}
/>
</div>
),
},
{
label: '运营商区分规则',
value: 'rules',
content: (
<div className="phone-segment-tab-content">
<div className="phone-segment-overview phone-segment-overview--single" aria-label="运营商区分规则统计">
<section>
<span><ListFilter size={20} /></span>
<div><strong>{ruleTotal.toLocaleString('zh-CN')}</strong><p>运营商识别规则</p></div>
</section>
</div>
{queryPanel}
<Table columns={ruleColumns} data={rules} emptyText={loading ? '加载中...' : '暂无运营商区分规则'} pagination={false} rowKey="id" />
<Pagination
page={rulePage}
total={ruleTotal}
totalPages={ruleTotalPages}
previousDisabled={rulePage <= 1 || loading}
nextDisabled={rulePage >= ruleTotalPages || loading}
onPrevious={() => setRulePage((current) => Math.max(1, current - 1))}
onNext={() => setRulePage((current) => Math.min(ruleTotalPages, current + 1))}
onPageChange={setRulePage}
/>
</div>
),
},
]}
/>
</div>
<Modal
footer={(
<>
<Button onClick={() => setCreating(false)} variant="ghost">取消</Button>
<Button disabled={!prefix} onClick={createSegment}>保存</Button>
</>
)}
onClose={() => setCreating(false)}
open={creating}
title="新增手机号段"
>
<div className="admin-system-modal-form">
<Input label="手机号段" maxLength={7} onChange={(event) => setPrefix(event.target.value)} placeholder="手机号码前7位" value={prefix} />
<Select
label="运营商"
onChange={(event) => setCarrier(event.target.value)}
options={[
{ label: '中国移动', value: '中国移动' },
{ label: '中国联通', value: '中国联通' },
{ label: '中国电信', value: '中国电信' },
]}
value={carrier}
/>
<Input label="省份" onChange={(event) => setProvince(event.target.value)} value={province} />
<Input label="城市" onChange={(event) => setCity(event.target.value)} value={city} />
</div>
</Modal>
<Modal
footer={(
<>
<Button onClick={() => setCreatingRule(false)} variant="ghost">取消</Button>
<Button disabled={!rulePattern} onClick={createCarrierRule}>保存</Button>
</>
)}
onClose={() => setCreatingRule(false)}
open={creatingRule}
title="新增运营商区分规则"
>
<div className="admin-system-modal-form">
<Select
label="运营商"
onChange={(event) => setRuleCarrier(event.target.value)}
options={[
{ label: '移动', value: 'mobile' },
{ label: '联通', value: 'unicom' },
{ label: '电信', value: 'telecom' },
]}
value={ruleCarrier}
/>
<Input label="号码前缀正则" onChange={(event) => setRulePattern(event.target.value)} placeholder="例如 ^13[4-9]" value={rulePattern} />
<Input label="优先级" onChange={(event) => setRulePriority(event.target.value)} value={rulePriority} />
<Input label="备注" onChange={(event) => setRuleRemark(event.target.value)} value={ruleRemark} />
</div>
</Modal>
{deleteTarget ? (
<Modal
footer={<><Button onClick={() => setDeleteTarget(null)} variant="ghost">取消</Button><Button onClick={deleteSegment} variant="danger">确认删除</Button></>}
onClose={() => setDeleteTarget(null)}
open
title="删除手机号段"
>
<p>确认删除手机号段“{deleteTarget.prefix}”吗?删除后号码归属识别将不再使用该记录。</p>
</Modal>
) : null}
</section>
);
}