fix: track live downstream cmpp connections
This commit is contained in:
@@ -24,7 +24,9 @@ const typeOptions = [
|
||||
export function AdminDrainageFieldsPage() {
|
||||
const [fields, setFields] = useState<DrainageField[]>([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [appliedKeyword, setAppliedKeyword] = useState('');
|
||||
const [type, setType] = useState('all');
|
||||
const [appliedType, setAppliedType] = useState('all');
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
@@ -47,11 +49,11 @@ export function AdminDrainageFieldsPage() {
|
||||
|
||||
const filteredFields = useMemo(
|
||||
() => fields.filter((field) => {
|
||||
const matchesKeyword = !keyword || [field.code, field.name, field.fieldType, field.description].some((value) => String(value ?? '').includes(keyword));
|
||||
const matchesType = type === 'all' || field.fieldType === type;
|
||||
const matchesKeyword = !appliedKeyword || [field.code, field.name, field.fieldType, field.description].some((value) => String(value ?? '').includes(appliedKeyword));
|
||||
const matchesType = appliedType === 'all' || field.fieldType === appliedType;
|
||||
return matchesKeyword && matchesType;
|
||||
}),
|
||||
[fields, keyword, type],
|
||||
[appliedKeyword, appliedType, fields],
|
||||
);
|
||||
|
||||
function createField() {
|
||||
@@ -88,6 +90,10 @@ export function AdminDrainageFieldsPage() {
|
||||
<div className="surface admin-drainage-toolbar">
|
||||
<Input onChange={(event) => setKeyword(event.target.value)} placeholder="搜索字段名称、代码或描述..." prefix={<Search size={16} />} value={keyword} />
|
||||
<Select onChange={(event) => setType(event.target.value)} options={typeOptions} value={type} />
|
||||
<div className="admin-system-toolbar__actions">
|
||||
<Button icon={<Search size={16} />} onClick={() => { setAppliedKeyword(keyword.trim()); setAppliedType(type); }}>查询</Button>
|
||||
<Button onClick={() => { setKeyword(''); setType('all'); setAppliedKeyword(''); setAppliedType('all'); }} variant="ghost">重置</Button>
|
||||
</div>
|
||||
<Button icon={<Plus size={18} />} onClick={() => setCreating(true)}>添加字段</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { Copy, Edit3, Plus, Search, Settings2, Trash2 } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Table, Tabs, Tag, type TableColumn } from '@/components/ui';
|
||||
import { adminApi, type ApplicationCmppParams, type CmppConnectionState, type EnterpriseApplication, type TenantOption } from '@/api/adminApi';
|
||||
import { adminApi, type ApplicationCmppParams, type CmppDownstreamConnection, type EnterpriseApplication, type TenantOption } from '@/api/adminApi';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
|
||||
type SmsApp = {
|
||||
@@ -197,11 +197,9 @@ function CmppParamsModal({ app, params, onClose }: { app: SmsApp; params?: Appli
|
||||
function CmppConnectionModal({
|
||||
app,
|
||||
onClose,
|
||||
onDeleteConnection,
|
||||
}: {
|
||||
app: SmsApp;
|
||||
onClose: () => void;
|
||||
onDeleteConnection: (connectionId: string) => void;
|
||||
}) {
|
||||
const activeConnections = app.cmppConnections.filter((item) => item.state === 'open').length;
|
||||
|
||||
@@ -216,7 +214,7 @@ function CmppConnectionModal({
|
||||
<div className="cmpp-connection-detail">
|
||||
<div className="cmpp-connection-summary">
|
||||
<div><span>当前连接数</span><strong>{activeConnections}</strong></div>
|
||||
<div><span>配置连接数</span><strong>{Math.max(activeConnections, app.cmppConnections.length)}</strong></div>
|
||||
<div><span>配置连接数</span><strong>{app.cmppParams.maxConnections}</strong></div>
|
||||
<div><span>AppID</span><strong>{app.appId}</strong></div>
|
||||
<div><span>连接状态</span><Tag tone={app.cmppStatus === 'connected' ? 'success' : app.cmppStatus === 'disconnected' ? 'danger' : 'neutral'}>{app.cmppStatus === 'connected' ? '在线' : app.cmppStatus === 'disconnected' ? '离线' : '未开通'}</Tag></div>
|
||||
</div>
|
||||
@@ -231,15 +229,6 @@ function CmppConnectionModal({
|
||||
{ key: 'lastHeartbeatAt', title: '上次心跳', width: '180px', render: (record: CmppConnection) => record.lastHeartbeatAt },
|
||||
{ key: 'lastSubmitAt', title: '上次提交', width: '180px', render: (record: CmppConnection) => record.lastSubmitAt },
|
||||
{ key: 'pendingWindow', title: '窗口占用', align: 'right', width: '120px', render: (record: CmppConnection) => record.pendingWindow },
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
align: 'right',
|
||||
width: '120px',
|
||||
render: (record: CmppConnection) => (
|
||||
<Button icon={<Trash2 size={14} />} onClick={() => onDeleteConnection(record.id)} size="sm" variant="danger">删除</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
data={app.cmppConnections}
|
||||
emptyText="暂无CMPP连接"
|
||||
@@ -254,6 +243,7 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [smsApps, setSmsApps] = useState<SmsApp[]>([]);
|
||||
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
|
||||
const [appliedEnterpriseKeyword, setAppliedEnterpriseKeyword] = useState('');
|
||||
const [connectionApp, setConnectionApp] = useState<SmsApp | null>(null);
|
||||
const [paramsApp, setParamsApp] = useState<SmsApp | null>(null);
|
||||
const [paramsDetail, setParamsDetail] = useState<ApplicationCmppParams | null>(null);
|
||||
@@ -268,9 +258,9 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
| null
|
||||
>(null);
|
||||
|
||||
async function loadSmsApps() {
|
||||
async function loadSmsApps(keyword = appliedEnterpriseKeyword) {
|
||||
try {
|
||||
const applications = await adminApi.listEnterpriseApplications({ keyword: enterpriseKeyword });
|
||||
const applications = await adminApi.listEnterpriseApplications({ keyword });
|
||||
setSmsApps(applications.map(mapApplication));
|
||||
setError('');
|
||||
} catch (err) {
|
||||
@@ -281,7 +271,7 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
|
||||
useEffect(() => {
|
||||
void loadSmsApps();
|
||||
}, [enterpriseKeyword]);
|
||||
}, [appliedEnterpriseKeyword]);
|
||||
|
||||
async function openAddModal() {
|
||||
setAddModalOpen(true);
|
||||
@@ -328,22 +318,14 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
setConfirmAction(null);
|
||||
}
|
||||
|
||||
async function deleteConnection(appId: string, connectionId: string) {
|
||||
await adminApi.disconnectApplicationConnection(appId, connectionId, '运营端断开企业应用 CMPP 连接');
|
||||
const data = await adminApi.listApplicationConnections(appId);
|
||||
const nextApp = mapApplication({ ...data.application, cmppConnections: data.connections, cmppStatus: data.summary.status as EnterpriseApplication['cmppStatus'] });
|
||||
setConnectionApp(nextApp);
|
||||
await loadSmsApps();
|
||||
}
|
||||
|
||||
async function openParams(app: SmsApp) {
|
||||
setParamsApp(app);
|
||||
setParamsDetail(await adminApi.getApplicationCmppParams(app.id));
|
||||
}
|
||||
|
||||
const filteredSmsApps = useMemo(
|
||||
() => smsApps.filter((item) => !enterpriseKeyword || item.enterprise.includes(enterpriseKeyword)),
|
||||
[enterpriseKeyword, smsApps],
|
||||
() => smsApps.filter((item) => !appliedEnterpriseKeyword || item.enterprise.includes(appliedEnterpriseKeyword)),
|
||||
[appliedEnterpriseKeyword, smsApps],
|
||||
);
|
||||
|
||||
const smsColumns = useMemo<Array<TableColumn<SmsApp>>>(() => [
|
||||
@@ -408,7 +390,10 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
prefix={<Search size={16} />}
|
||||
value={enterpriseKeyword}
|
||||
/>
|
||||
<Button onClick={() => setEnterpriseKeyword('')} variant="ghost">重置</Button>
|
||||
<div className="admin-split-filter__actions">
|
||||
<Button icon={<Search size={16} />} onClick={() => setAppliedEnterpriseKeyword(enterpriseKeyword.trim())}>查询</Button>
|
||||
<Button onClick={() => { setEnterpriseKeyword(''); setAppliedEnterpriseKeyword(''); }} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? <div className="surface ui-table__empty">{error}</div> : null}
|
||||
@@ -446,7 +431,6 @@ export function AdminEnterpriseApplicationsPage() {
|
||||
<CmppConnectionModal
|
||||
app={connectionApp}
|
||||
onClose={() => setConnectionApp(null)}
|
||||
onDeleteConnection={(connectionId) => { void deleteConnection(connectionApp.id, connectionId); }}
|
||||
/>
|
||||
) : null}
|
||||
{paramsApp ? <CmppParamsModal app={paramsApp} params={paramsDetail} onClose={() => { setParamsApp(null); setParamsDetail(null); }} /> : null}
|
||||
@@ -467,22 +451,22 @@ function mapApplication(application: EnterpriseApplication): SmsApp {
|
||||
deliveryRate: application.deliveryRate ?? 0,
|
||||
unitPrice: (application.customerUnitPrice ?? 0) / 100,
|
||||
cmppStatus: application.interfaceEnabled === false ? 'inactive' : application.cmppStatus === 'connected' ? 'connected' : application.cmppStatus === 'inactive' ? 'inactive' : 'disconnected',
|
||||
cmppParams: { host: '', port: 0, interfaceEnabled: application.interfaceEnabled !== false, interfaceType: application.interfaceType ?? 'cmpp20', enterpriseCode: application.cmppEnterpriseCode ?? application.tenant?.code ?? application.tenantId, account: application.cmppAccount ?? application.tenantId, password: '', accessNumber: '', maxConnections: 0, heartbeatSeconds: 30, windowSize: 16, protocolVersion: 'CMPP2.0' },
|
||||
cmppParams: { host: '', port: 0, interfaceEnabled: application.interfaceEnabled !== false, interfaceType: application.interfaceType ?? 'cmpp20', enterpriseCode: application.cmppEnterpriseCode ?? application.tenant?.code ?? application.tenantId, account: application.cmppAccount ?? application.tenantId, password: '', accessNumber: '', maxConnections: application.cmppMaxConnections ?? 1, heartbeatSeconds: 30, windowSize: application.cmppWindowSize ?? 16, protocolVersion: 'CMPP2.0' },
|
||||
cmppConnections: connections,
|
||||
};
|
||||
}
|
||||
|
||||
function mapConnection(connection: CmppConnectionState): CmppConnection {
|
||||
const isOpen = connection.status === 'connected' && connection.currentConnections > 0;
|
||||
function mapConnection(connection: CmppDownstreamConnection): CmppConnection {
|
||||
const isOpen = connection.status === 'connected';
|
||||
return {
|
||||
id: connection.connectionId,
|
||||
state: isOpen ? 'open' : connection.status === 'reconnecting' ? 'reconnecting' : 'closed',
|
||||
bindType: 'transceiver',
|
||||
clientIp: String(connection.channel?.gatewayHost ?? ''),
|
||||
sourceAddr: String(connection.channel?.enterpriseCode ?? ''),
|
||||
establishedAt: formatDateTime(connection.lastConnectedAt),
|
||||
clientIp: String(connection.remoteIp ?? ''),
|
||||
sourceAddr: connection.enterpriseCode,
|
||||
establishedAt: formatDateTime(connection.connectedAt),
|
||||
lastHeartbeatAt: formatDateTime(connection.lastHeartbeatAt),
|
||||
lastSubmitAt: formatDateTime(connection.updatedAt),
|
||||
pendingWindow: connection.currentConnections,
|
||||
lastSubmitAt: formatDateTime(connection.lastSubmitAt),
|
||||
pendingWindow: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -491,19 +491,21 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
const [drainageModal, setDrainageModal] = useState<{ signatureId: string; item?: DrainageInfo } | null>(null);
|
||||
const [drainageReport, setDrainageReport] = useState<DrainageInfo | null>(null);
|
||||
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
|
||||
const [appliedEnterpriseKeyword, setAppliedEnterpriseKeyword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [expandedSignatureId, setExpandedSignatureId] = useState('');
|
||||
const [signatureKeyword, setSignatureKeyword] = useState('');
|
||||
const [appliedSignatureKeyword, setAppliedSignatureKeyword] = useState('');
|
||||
const [signatureModal, setSignatureModal] = useState<ClientSmsSignature | 'new' | null>(null);
|
||||
const [signatureReport, setSignatureReport] = useState<ClientSmsSignature | null>(null);
|
||||
const [signatures, setSignatures] = useState<ClientSmsSignature[]>([]);
|
||||
const [tenants, setTenants] = useState<TenantOption[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
async function loadData() {
|
||||
async function loadData(filters = { enterpriseKeyword: appliedEnterpriseKeyword, signatureKeyword: appliedSignatureKeyword }) {
|
||||
try {
|
||||
const [signatureItems, tenantItems, applicationItems] = await Promise.all([
|
||||
adminApi.listEnterpriseSignatures({ keyword: [enterpriseKeyword, signatureKeyword].filter(Boolean).join(' ') }),
|
||||
adminApi.listEnterpriseSignatures({ keyword: [filters.enterpriseKeyword, filters.signatureKeyword].filter(Boolean).join(' ') }),
|
||||
adminApi.listTenants(),
|
||||
adminApi.listEnterpriseApplications(),
|
||||
]);
|
||||
@@ -523,9 +525,9 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
const filteredSignatures = useMemo(() => signatures.filter((item) => {
|
||||
const enterprise = item.tenant?.name ?? item.tenantId;
|
||||
const application = item.application?.name ?? '';
|
||||
return (!enterpriseKeyword || enterprise.includes(enterpriseKeyword))
|
||||
&& (!signatureKeyword || item.name.includes(signatureKeyword) || application.includes(signatureKeyword) || (item.purpose ?? '').includes(signatureKeyword));
|
||||
}), [enterpriseKeyword, signatureKeyword, signatures]);
|
||||
return (!appliedEnterpriseKeyword || enterprise.includes(appliedEnterpriseKeyword))
|
||||
&& (!appliedSignatureKeyword || item.name.includes(appliedSignatureKeyword) || application.includes(appliedSignatureKeyword) || (item.purpose ?? '').includes(appliedSignatureKeyword));
|
||||
}), [appliedEnterpriseKeyword, appliedSignatureKeyword, signatures]);
|
||||
const pageSize = 10;
|
||||
const totalPages = Math.max(1, Math.ceil(filteredSignatures.length / pageSize));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
@@ -533,7 +535,7 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [enterpriseKeyword, filteredSignatures.length, signatureKeyword]);
|
||||
}, [appliedEnterpriseKeyword, appliedSignatureKeyword, filteredSignatures.length]);
|
||||
|
||||
async function saveSignature(state: SignatureFormState) {
|
||||
const existing = signatureModal && signatureModal !== 'new' ? signatureModal : null;
|
||||
@@ -695,8 +697,22 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
<div className="surface admin-split-filter">
|
||||
<Input label="企业名称" onChange={(event) => setEnterpriseKeyword(event.target.value)} placeholder="请输入企业名称" prefix={<Search size={16} />} value={enterpriseKeyword} />
|
||||
<Input label="签名/应用" onChange={(event) => setSignatureKeyword(event.target.value)} placeholder="请输入签名或应用名称" prefix={<Search size={16} />} value={signatureKeyword} />
|
||||
<Button onClick={() => { setEnterpriseKeyword(''); setSignatureKeyword(''); void loadData(); }} variant="ghost">重置</Button>
|
||||
<Button icon={<Search size={16} />} onClick={() => { void loadData(); }}>查询</Button>
|
||||
<div className="admin-split-filter__actions">
|
||||
<Button icon={<Search size={16} />} onClick={() => {
|
||||
const filters = { enterpriseKeyword: enterpriseKeyword.trim(), signatureKeyword: signatureKeyword.trim() };
|
||||
setAppliedEnterpriseKeyword(filters.enterpriseKeyword);
|
||||
setAppliedSignatureKeyword(filters.signatureKeyword);
|
||||
void loadData(filters);
|
||||
}}>查询</Button>
|
||||
<Button onClick={() => {
|
||||
const filters = { enterpriseKeyword: '', signatureKeyword: '' };
|
||||
setEnterpriseKeyword('');
|
||||
setSignatureKeyword('');
|
||||
setAppliedEnterpriseKeyword('');
|
||||
setAppliedSignatureKeyword('');
|
||||
void loadData(filters);
|
||||
}} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
|
||||
@@ -250,18 +250,20 @@ export function AdminEnterpriseTemplatesPage() {
|
||||
const [applications, setApplications] = useState<ClientSmsApplication[]>([]);
|
||||
const [deleteTarget, setDeleteTarget] = useState<ClientSmsTemplate | null>(null);
|
||||
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
|
||||
const [appliedEnterpriseKeyword, setAppliedEnterpriseKeyword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [signatureItems, setSignatureItems] = useState<ClientSmsSignature[]>([]);
|
||||
const [templateModal, setTemplateModal] = useState<ClientSmsTemplate | 'new' | null>(null);
|
||||
const [templatePreview, setTemplatePreview] = useState<ClientSmsTemplate | null>(null);
|
||||
const [templates, setTemplates] = useState<ClientSmsTemplate[]>([]);
|
||||
const [templateKeyword, setTemplateKeyword] = useState('');
|
||||
const [appliedTemplateKeyword, setAppliedTemplateKeyword] = useState('');
|
||||
const [tenants, setTenants] = useState<TenantOption[]>([]);
|
||||
|
||||
async function loadData() {
|
||||
async function loadData(filters = { enterpriseKeyword: appliedEnterpriseKeyword, templateKeyword: appliedTemplateKeyword }) {
|
||||
try {
|
||||
const [templateItems, tenantItems, applicationItems, signatureList] = await Promise.all([
|
||||
adminApi.listEnterpriseTemplates({ keyword: [enterpriseKeyword, templateKeyword].filter(Boolean).join(' ') }),
|
||||
adminApi.listEnterpriseTemplates({ keyword: [filters.enterpriseKeyword, filters.templateKeyword].filter(Boolean).join(' ') }),
|
||||
adminApi.listTenants(),
|
||||
adminApi.listEnterpriseApplications(),
|
||||
adminApi.listEnterpriseSignatures(),
|
||||
@@ -283,9 +285,9 @@ export function AdminEnterpriseTemplatesPage() {
|
||||
const filteredTemplates = useMemo(() => templates.filter((item) => {
|
||||
const enterprise = item.tenant?.name ?? item.tenantId;
|
||||
const application = item.application?.name ?? '';
|
||||
return (!enterpriseKeyword || enterprise.includes(enterpriseKeyword))
|
||||
&& (!templateKeyword || item.name.includes(templateKeyword) || item.content.includes(templateKeyword) || application.includes(templateKeyword));
|
||||
}), [enterpriseKeyword, templateKeyword, templates]);
|
||||
return (!appliedEnterpriseKeyword || enterprise.includes(appliedEnterpriseKeyword))
|
||||
&& (!appliedTemplateKeyword || item.name.includes(appliedTemplateKeyword) || item.content.includes(appliedTemplateKeyword) || application.includes(appliedTemplateKeyword));
|
||||
}), [appliedEnterpriseKeyword, appliedTemplateKeyword, templates]);
|
||||
|
||||
async function saveTemplate(state: TemplateFormState) {
|
||||
const existing = templateModal && templateModal !== 'new' ? templateModal : null;
|
||||
@@ -363,8 +365,22 @@ export function AdminEnterpriseTemplatesPage() {
|
||||
<div className="surface admin-split-filter">
|
||||
<Input label="企业名称" onChange={(event) => setEnterpriseKeyword(event.target.value)} placeholder="请输入企业名称" prefix={<Search size={16} />} value={enterpriseKeyword} />
|
||||
<Input label="模板/应用/内容" onChange={(event) => setTemplateKeyword(event.target.value)} placeholder="请输入模板、应用或内容" prefix={<Search size={16} />} value={templateKeyword} />
|
||||
<Button onClick={() => { setEnterpriseKeyword(''); setTemplateKeyword(''); void loadData(); }} variant="ghost">重置</Button>
|
||||
<Button icon={<Search size={16} />} onClick={() => { void loadData(); }}>查询</Button>
|
||||
<div className="admin-split-filter__actions">
|
||||
<Button icon={<Search size={16} />} onClick={() => {
|
||||
const filters = { enterpriseKeyword: enterpriseKeyword.trim(), templateKeyword: templateKeyword.trim() };
|
||||
setAppliedEnterpriseKeyword(filters.enterpriseKeyword);
|
||||
setAppliedTemplateKeyword(filters.templateKeyword);
|
||||
void loadData(filters);
|
||||
}}>查询</Button>
|
||||
<Button onClick={() => {
|
||||
const filters = { enterpriseKeyword: '', templateKeyword: '' };
|
||||
setEnterpriseKeyword('');
|
||||
setTemplateKeyword('');
|
||||
setAppliedEnterpriseKeyword('');
|
||||
setAppliedTemplateKeyword('');
|
||||
void loadData(filters);
|
||||
}} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="surface section-stack">
|
||||
<Tabs
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Plus, Search } from 'lucide-react';
|
||||
import { Database, ListFilter, Plus, RotateCcw, 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';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
|
||||
type PhoneSegment = DictionaryItem & {
|
||||
prefix?: string;
|
||||
@@ -42,20 +43,12 @@ export function AdminPhoneSegmentsPage() {
|
||||
const [rulePage, setRulePage] = useState(1);
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
setSegmentQuery(keyword.trim());
|
||||
setPage(1);
|
||||
}, 300);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [keyword]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
Promise.all([
|
||||
adminApi.listPhoneSegments({ keyword: segmentQuery || undefined, page, pageSize }),
|
||||
adminApi.listPhoneCarrierRules({ keyword: activeTab === 'rules' ? segmentQuery || undefined : undefined, page: rulePage, pageSize }),
|
||||
adminApi.listPhoneCarrierRules({ keyword: segmentQuery || undefined, page: rulePage, pageSize }),
|
||||
])
|
||||
.then(([segmentPage, ruleResponse]) => {
|
||||
if (cancelled) return;
|
||||
@@ -74,7 +67,7 @@ export function AdminPhoneSegmentsPage() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [activeTab, page, reloadKey, rulePage, segmentQuery]);
|
||||
}, [page, reloadKey, rulePage, segmentQuery]);
|
||||
|
||||
const segmentTotalPages = Math.max(1, Math.ceil(segmentTotal / pageSize));
|
||||
const ruleTotalPages = Math.max(1, Math.ceil(ruleTotal / pageSize));
|
||||
@@ -103,12 +96,25 @@ export function AdminPhoneSegmentsPage() {
|
||||
.catch((failure: Error) => setError(failure.message || '运营商区分规则新增失败'));
|
||||
}
|
||||
|
||||
function query() {
|
||||
setSegmentQuery(keyword.trim());
|
||||
setPage(1);
|
||||
setRulePage(1);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setKeyword('');
|
||||
setSegmentQuery('');
|
||||
setPage(1);
|
||||
setRulePage(1);
|
||||
}
|
||||
|
||||
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) => record.createdAt ?? '-' },
|
||||
{ key: 'createdAt', title: '创建时间', width: '190px', render: (record) => formatDateTime(record.createdAt) },
|
||||
], []);
|
||||
|
||||
const ruleColumns = useMemo<Array<TableColumn<CarrierRule>>>(() => [
|
||||
@@ -119,28 +125,42 @@ export function AdminPhoneSegmentsPage() {
|
||||
], []);
|
||||
|
||||
return (
|
||||
<section className="page-stack admin-system-page">
|
||||
<section className="page-stack admin-system-page phone-segment-workbench">
|
||||
<div className="page-heading">
|
||||
<div>
|
||||
<Breadcrumb items={['系统管理', '手机号段库']} />
|
||||
<h1>手机号段库</h1>
|
||||
</div>
|
||||
</div>
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
|
||||
<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)}>
|
||||
{activeTab === 'segments' ? '新增号段' : '新增规则'}
|
||||
</Button>
|
||||
</div>
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
|
||||
<div className="phone-segment-overview" aria-label="号段数据概览">
|
||||
<section>
|
||||
<span><Database size={20} /></span>
|
||||
<div><strong>{segmentTotal.toLocaleString('zh-CN')}</strong><p>已收录手机号段</p></div>
|
||||
</section>
|
||||
<section>
|
||||
<span><ListFilter size={20} /></span>
|
||||
<div><strong>{ruleTotal.toLocaleString('zh-CN')}</strong><p>运营商识别规则</p></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="surface 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>
|
||||
|
||||
<div className="surface admin-system-table-card">
|
||||
<Tabs
|
||||
className="phone-segment-tabs"
|
||||
className="phone-segment-workbench__tabs"
|
||||
onChange={(value) => {
|
||||
setActiveTab(value as 'segments' | 'rules');
|
||||
setRulePage(1);
|
||||
}}
|
||||
value={activeTab}
|
||||
items={[
|
||||
|
||||
@@ -25,6 +25,7 @@ export function AdminSystemLogsPage() {
|
||||
const [level, setLevel] = useState('all');
|
||||
const [module, setModule] = useState('all');
|
||||
const [range, setRange] = useState('today');
|
||||
const [filters, setFilters] = useState({ keyword: '', level: 'all', module: 'all', range: 'today' });
|
||||
const [page, setPage] = useState(1);
|
||||
const pageSize = 5;
|
||||
const [logs, setLogs] = useState<OperationLogItem[]>([]);
|
||||
@@ -33,7 +34,7 @@ export function AdminSystemLogsPage() {
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
adminApi.listSystemLogs({ keyword, level, module, range, page, pageSize })
|
||||
adminApi.listSystemLogs({ ...filters, page, pageSize })
|
||||
.then((data) => {
|
||||
setLogs(data.items);
|
||||
setModules(data.modules);
|
||||
@@ -45,7 +46,7 @@ export function AdminSystemLogsPage() {
|
||||
setTotal(0);
|
||||
setError(err instanceof Error ? err.message : '系统日志加载失败');
|
||||
});
|
||||
}, [keyword, level, module, range, page]);
|
||||
}, [filters, page]);
|
||||
|
||||
const moduleOptions = useMemo(() => {
|
||||
return [{ label: '全部模块', value: 'all' }, ...modules.map((item) => ({ label: item, value: item }))];
|
||||
@@ -54,6 +55,20 @@ export function AdminSystemLogsPage() {
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
|
||||
function query() {
|
||||
setPage(1);
|
||||
setFilters({ keyword: keyword.trim(), level, module, range });
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setKeyword('');
|
||||
setLevel('all');
|
||||
setModule('all');
|
||||
setRange('today');
|
||||
setPage(1);
|
||||
setFilters({ keyword: '', level: 'all', module: 'all', range: 'today' });
|
||||
}
|
||||
|
||||
const columns = useMemo<Array<TableColumn<OperationLogItem>>>(() => [
|
||||
{ key: 'time', title: '时间', width: '180px', render: (record) => <span className="muted">{formatDateTime(record.time)}</span> },
|
||||
{ key: 'level', title: '级别', width: '120px', render: (record) => <Tag tone={levelToneMap[record.level]}>{levelLabelMap[record.level]}</Tag> },
|
||||
@@ -89,13 +104,13 @@ export function AdminSystemLogsPage() {
|
||||
|
||||
<div className="system-log-filters">
|
||||
<Input
|
||||
onChange={(event) => { setKeyword(event.target.value); setPage(1); }}
|
||||
onChange={(event) => setKeyword(event.target.value)}
|
||||
placeholder="搜索企业、操作人、动作、资源ID或详情"
|
||||
prefix={<Search size={16} />}
|
||||
value={keyword}
|
||||
/>
|
||||
<Select
|
||||
onChange={(event) => { setLevel(event.target.value); setPage(1); }}
|
||||
onChange={(event) => setLevel(event.target.value)}
|
||||
options={[
|
||||
{ label: '全部级别', value: 'all' },
|
||||
{ label: '信息', value: 'info' },
|
||||
@@ -105,7 +120,11 @@ export function AdminSystemLogsPage() {
|
||||
]}
|
||||
value={level}
|
||||
/>
|
||||
<Select onChange={(event) => { setModule(event.target.value); setPage(1); }} options={moduleOptions} value={module} />
|
||||
<Select onChange={(event) => setModule(event.target.value)} options={moduleOptions} value={module} />
|
||||
<div className="system-log-filters__actions">
|
||||
<Button icon={<Search size={16} />} onClick={query}>查询</Button>
|
||||
<Button onClick={reset} variant="ghost">重置</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="system-log-range">
|
||||
|
||||
@@ -63,6 +63,7 @@ export function AdminUsersPage() {
|
||||
const [users, setUsers] = useState<ManagedUser[]>([]);
|
||||
const [tenants, setTenants] = useState<TenantOption[]>([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [appliedKeyword, setAppliedKeyword] = useState('');
|
||||
const [editingUser, setEditingUser] = useState<ManagedUser | null>(null);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [form, setForm] = useState<UserForm>(emptyForm);
|
||||
@@ -84,12 +85,12 @@ export function AdminUsersPage() {
|
||||
}, []);
|
||||
|
||||
const filteredUsers = useMemo(() => {
|
||||
const value = keyword.trim().toLowerCase();
|
||||
const value = appliedKeyword.trim().toLowerCase();
|
||||
return users.filter((user) => {
|
||||
const target = `${user.displayName} ${user.username} ${user.email ?? ''} ${user.phone ?? ''} ${user.tenant?.name ?? ''} ${roleLabel[user.roles[0]?.role.code] ?? ''}`.toLowerCase();
|
||||
return !value || target.includes(value);
|
||||
});
|
||||
}, [keyword, users]);
|
||||
}, [appliedKeyword, users]);
|
||||
|
||||
function openCreate() {
|
||||
setForm({ ...emptyForm, password: generateInitialPassword(), tenantId: tenants[0]?.id ?? '' });
|
||||
@@ -199,6 +200,10 @@ export function AdminUsersPage() {
|
||||
|
||||
<div className="surface admin-system-toolbar">
|
||||
<Input onChange={(event) => setKeyword(event.target.value)} placeholder="搜索姓名、邮箱、手机号、角色或企业" prefix={<Search size={16} />} value={keyword} />
|
||||
<div className="admin-system-toolbar__actions">
|
||||
<Button icon={<Search size={16} />} onClick={() => setAppliedKeyword(keyword.trim())}>查询</Button>
|
||||
<Button onClick={() => { setKeyword(''); setAppliedKeyword(''); }} variant="ghost">重置</Button>
|
||||
</div>
|
||||
<Button icon={<Plus size={16} />} onClick={openCreate}>新增用户</Button>
|
||||
</div>
|
||||
{error ? <div className="surface empty-state">{error}</div> : null}
|
||||
|
||||
Reference in New Issue
Block a user