fix: harden tenant auth and quality gates
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
export type AuditStatus = 'pending' | 'approved' | 'rejected';
|
||||
|
||||
export type AuditItem = {
|
||||
id: string;
|
||||
customer: string;
|
||||
type: '模板' | '签名';
|
||||
content: string;
|
||||
risk: 'low' | 'medium' | 'high';
|
||||
status: AuditStatus;
|
||||
submittedAt: string;
|
||||
};
|
||||
@@ -3,164 +3,7 @@ import { AlertTriangle, BarChart3, CheckCircle2, Eye, RefreshCw, Search, TimerRe
|
||||
import { adminApi, type DownstreamDeliveryDashboard, type DownstreamDeliveryRecord, type DownstreamRequeuePreview, type DownstreamRequeueTask, type DownstreamRequeueTaskItem, type EnterpriseApplication, type TenantOption } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Tag, Textarea, type DateRangeValue } from '@/components/ui';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
|
||||
const statusTone: Record<string, 'neutral' | 'info' | 'success' | 'warning' | 'danger'> = {
|
||||
pending: 'warning',
|
||||
manual_requeueing: 'info',
|
||||
awaiting_ack: 'info',
|
||||
delivered: 'success',
|
||||
failed: 'danger',
|
||||
unconfirmed: 'warning',
|
||||
rejected: 'danger',
|
||||
};
|
||||
|
||||
const statusLabel: Record<string, string> = {
|
||||
manual_requeueing: '人工重投处理中',
|
||||
awaiting_ack: '等待客户端确认',
|
||||
delivered: '客户端已确认',
|
||||
failed: '投递失败',
|
||||
unconfirmed: '客户端未确认',
|
||||
rejected: '客户端拒绝',
|
||||
};
|
||||
|
||||
function deliveryStatusLabel(record: DownstreamDeliveryRecord) {
|
||||
if (record.status !== 'pending') return statusLabel[record.status] ?? record.status;
|
||||
if (record.manualRetryCount > 0) return '人工重投排队中';
|
||||
if (record.retryCount > 0) return '等待自动重试';
|
||||
return '待首次投递';
|
||||
}
|
||||
|
||||
const deliveryTypeLabel: Record<string, string> = {
|
||||
receipt: '状态回执',
|
||||
uplink: '上行短信',
|
||||
};
|
||||
|
||||
const attemptStatusLabel: Record<string, string> = {
|
||||
awaiting_ack: '等待客户端确认',
|
||||
acknowledged: '客户端已确认',
|
||||
rejected: '客户端拒绝',
|
||||
failed: '投递失败',
|
||||
};
|
||||
|
||||
const requeueTaskStatusLabel: Record<string, string> = {
|
||||
queued: '排队中', running: '执行中', paused: '已暂停', completed: '已完成',
|
||||
partial_completed: '部分完成', terminated: '已终止',
|
||||
};
|
||||
|
||||
const requeueItemStatusLabel: Record<string, string> = {
|
||||
queued: '排队中', processing: '处理中', waiting_connection: '等待连接',
|
||||
waiting_external_ack: '等待其他链路确认', waiting_ack: '等待客户确认', success: '成功',
|
||||
failed: '失败', skipped: '跳过', unprocessed: '未处理',
|
||||
};
|
||||
|
||||
function requeueTone(status: string) {
|
||||
if (status === 'completed' || status === 'success') return 'success' as const;
|
||||
if (status === 'partial_completed' || status === 'failed') return 'danger' as const;
|
||||
if (status === 'paused' || status === 'skipped' || status === 'unprocessed') return 'warning' as const;
|
||||
return 'info' as const;
|
||||
}
|
||||
|
||||
type RequeueTarget =
|
||||
| { kind: 'single'; record: DownstreamDeliveryRecord }
|
||||
| { kind: 'batch'; ids: string[] };
|
||||
|
||||
type RequeueResult = {
|
||||
status: 'success' | 'partial' | 'failed';
|
||||
title: string;
|
||||
message: string;
|
||||
failures?: string[];
|
||||
};
|
||||
|
||||
function formatLocalDate(value: Date) {
|
||||
return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function recentSevenDays(): DateRangeValue {
|
||||
const end = new Date();
|
||||
const start = new Date(end);
|
||||
start.setDate(end.getDate() - 6);
|
||||
return { start: formatLocalDate(start), end: formatLocalDate(end) };
|
||||
}
|
||||
|
||||
function attemptStatusTone(status: string) {
|
||||
if (status === 'acknowledged') return 'success' as const;
|
||||
if (status === 'rejected' || status === 'failed') return 'danger' as const;
|
||||
if (status === 'awaiting_ack') return 'info' as const;
|
||||
return 'neutral' as const;
|
||||
}
|
||||
|
||||
function DeliveryDetailModal({ record, onClose }: { record: DownstreamDeliveryRecord; onClose: () => void }) {
|
||||
const payloadText = useMemo(() => JSON.stringify(record.payload ?? {}, null, 2), [record.payload]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onClose={onClose}
|
||||
size="xl"
|
||||
title={<div className="template-modal-title"><h2>下游投递详情</h2><p>{record.id}</p></div>}
|
||||
footer={<Button onClick={onClose}>关闭</Button>}
|
||||
>
|
||||
<div className="report-record-detail">
|
||||
<div className="detail-grid">
|
||||
<div><span>企业</span><strong>{record.tenant?.name ?? record.tenantId}</strong></div>
|
||||
<div><span>应用</span><strong>{record.application?.name ?? record.applicationId}</strong></div>
|
||||
<div><span>投递类型</span><strong>{deliveryTypeLabel[record.deliveryType] ?? record.deliveryType}</strong></div>
|
||||
<div><span>当前状态</span><strong>{deliveryStatusLabel(record)}</strong></div>
|
||||
<div><span>消息 ID</span><strong>{record.messageId ?? '-'}</strong></div>
|
||||
<div><span>自动重试次数</span><strong>{record.retryCount}</strong></div>
|
||||
<div><span>人工重投次数</span><strong>{record.manualRetryCount}</strong></div>
|
||||
<div><span>最近人工重投</span><strong>{record.lastRetriedAt ? formatDateTime(record.lastRetriedAt) : '-'}</strong></div>
|
||||
<div><span>下次重试</span><strong>{record.nextRetryAt ?? '-'}</strong></div>
|
||||
<div><span>自动重试</span><strong>{record.retryEnabled ? '开启' : '关闭'}</strong></div>
|
||||
<div><span>写出时间</span><strong>{record.sentAt ?? '-'}</strong></div>
|
||||
<div><span>确认时间</span><strong>{record.acknowledgedAt ?? '-'}</strong></div>
|
||||
<div><span>ACK Result</span><strong>{record.ackResult ?? '-'}</strong></div>
|
||||
<div><span>ACK Sequence_Id</span><strong>{record.ackSequenceId ?? '-'}</strong></div>
|
||||
<div><span>ACK Msg_Id</span><strong>{record.ackMessageId ?? '-'}</strong></div>
|
||||
<div><span>连接 ID</span><strong>{record.connectionId ?? '-'}</strong></div>
|
||||
<div className="detail-grid__wide"><span>最后错误</span><strong>{record.lastError ?? '-'}</strong></div>
|
||||
</div>
|
||||
<section className="report-history">
|
||||
<h3>逐次投递记录</h3>
|
||||
<div className="downstream-attempt-timeline" aria-label="逐次投递记录">
|
||||
{(record.attempts ?? []).map((attempt) => (
|
||||
<article className="downstream-attempt-card" key={attempt.id}>
|
||||
<div className={`downstream-attempt-marker downstream-attempt-marker--${attemptStatusTone(attempt.status)}`}>
|
||||
{attempt.attemptNo}
|
||||
</div>
|
||||
<div className="downstream-attempt-card__body">
|
||||
<header>
|
||||
<strong>第 {attempt.attemptNo} 次投递</strong>
|
||||
<Tag tone={attemptStatusTone(attempt.status)}>{attemptStatusLabel[attempt.status] ?? attempt.status}</Tag>
|
||||
</header>
|
||||
<dl className="downstream-attempt-card__times">
|
||||
<div><dt>发送时间</dt><dd>{attempt.sentAt ? formatDateTime(attempt.sentAt) : '-'}</dd></div>
|
||||
<div><dt>ACK 时间</dt><dd>{attempt.acknowledgedAt ? formatDateTime(attempt.acknowledgedAt) : '-'}</dd></div>
|
||||
<div><dt>ACK 截止</dt><dd>{attempt.ackDeadlineAt ? formatDateTime(attempt.ackDeadlineAt) : '-'}</dd></div>
|
||||
</dl>
|
||||
<dl className="downstream-attempt-card__identifiers">
|
||||
<div><dt>连接 ID</dt><dd>{attempt.connectionId ?? '-'}</dd></div>
|
||||
<div><dt>Sequence_Id</dt><dd>{attempt.sequenceId ?? '-'}</dd></div>
|
||||
<div><dt>Msg_Id</dt><dd>{attempt.messageId ?? '-'}</dd></div>
|
||||
</dl>
|
||||
<div className={`downstream-attempt-result${attempt.errorMessage || attempt.failureType ? ' downstream-attempt-result--error' : ''}`}>
|
||||
<span>ACK Result:{attempt.ackResult ?? '-'}</span>
|
||||
<strong>{attempt.errorMessage ?? attempt.failureType ?? '本次投递未记录异常'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
{(record.attempts ?? []).length === 0 ? <p>暂无逐次投递记录</p> : null}
|
||||
</div>
|
||||
</section>
|
||||
<section className="report-history">
|
||||
<h3>Payload</h3>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0 }}>{payloadText}</pre>
|
||||
</section>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
import { DeliveryDetailModal, deliveryStatusLabel, deliveryTypeLabel, recentSevenDays, requeueItemStatusLabel, requeueTaskStatusLabel, requeueTone, statusLabel, statusTone, type RequeueResult, type RequeueTarget } from './downstreamDeliveryPresentation';
|
||||
|
||||
export function AdminDownstreamDeliveriesPage() {
|
||||
const [records, setRecords] = useState<DownstreamDeliveryRecord[]>([]);
|
||||
|
||||
@@ -9,13 +9,13 @@ import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Chart,
|
||||
Modal,
|
||||
MoneyText,
|
||||
Table,
|
||||
Tag,
|
||||
type TableColumn,
|
||||
} from '@/components/ui';
|
||||
import { Chart } from '@/components/ui/Chart';
|
||||
import { adminApi, type DashboardResponse, type SendQualityResponse } from '@/api/adminApi';
|
||||
import { createDualAxisBarLineOption, createLineOption } from '@/theme/chartOptions';
|
||||
import { formatAmount, moneyUnitsToYuan } from '@/utils/currency';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { Button, Tag, type TableColumn } from '@/components/ui';
|
||||
import type { AuditItem, AuditStatus } from '@/mock';
|
||||
import type { AuditItem, AuditStatus } from '@/api/types/audit';
|
||||
|
||||
const riskToneMap = {
|
||||
low: 'success',
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { DownstreamDeliveryRecord } from '@/api/adminApi';
|
||||
import { Button, Modal, Tag, type DateRangeValue } from '@/components/ui';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
|
||||
export const statusTone: Record<string, 'neutral' | 'info' | 'success' | 'warning' | 'danger'> = {
|
||||
pending: 'warning',
|
||||
manual_requeueing: 'info',
|
||||
awaiting_ack: 'info',
|
||||
delivered: 'success',
|
||||
failed: 'danger',
|
||||
unconfirmed: 'warning',
|
||||
rejected: 'danger',
|
||||
};
|
||||
|
||||
export const statusLabel: Record<string, string> = {
|
||||
manual_requeueing: '人工重投处理中',
|
||||
awaiting_ack: '等待客户端确认',
|
||||
delivered: '客户端已确认',
|
||||
failed: '投递失败',
|
||||
unconfirmed: '客户端未确认',
|
||||
rejected: '客户端拒绝',
|
||||
};
|
||||
|
||||
export function deliveryStatusLabel(record: DownstreamDeliveryRecord) {
|
||||
if (record.status !== 'pending') return statusLabel[record.status] ?? record.status;
|
||||
if (record.manualRetryCount > 0) return '人工重投排队中';
|
||||
if (record.retryCount > 0) return '等待自动重试';
|
||||
return '待首次投递';
|
||||
}
|
||||
|
||||
export const deliveryTypeLabel: Record<string, string> = {
|
||||
receipt: '状态回执',
|
||||
uplink: '上行短信',
|
||||
};
|
||||
|
||||
const attemptStatusLabel: Record<string, string> = {
|
||||
awaiting_ack: '等待客户端确认',
|
||||
acknowledged: '客户端已确认',
|
||||
rejected: '客户端拒绝',
|
||||
failed: '投递失败',
|
||||
};
|
||||
|
||||
export const requeueTaskStatusLabel: Record<string, string> = {
|
||||
queued: '排队中', running: '执行中', paused: '已暂停', completed: '已完成',
|
||||
partial_completed: '部分完成', terminated: '已终止',
|
||||
};
|
||||
|
||||
export const requeueItemStatusLabel: Record<string, string> = {
|
||||
queued: '排队中', processing: '处理中', waiting_connection: '等待连接',
|
||||
waiting_external_ack: '等待其他链路确认', waiting_ack: '等待客户确认', success: '成功',
|
||||
failed: '失败', skipped: '跳过', unprocessed: '未处理',
|
||||
};
|
||||
|
||||
export function requeueTone(status: string) {
|
||||
if (status === 'completed' || status === 'success') return 'success' as const;
|
||||
if (status === 'partial_completed' || status === 'failed') return 'danger' as const;
|
||||
if (status === 'paused' || status === 'skipped' || status === 'unprocessed') return 'warning' as const;
|
||||
return 'info' as const;
|
||||
}
|
||||
|
||||
export type RequeueTarget =
|
||||
| { kind: 'single'; record: DownstreamDeliveryRecord }
|
||||
| { kind: 'batch'; ids: string[] };
|
||||
|
||||
export type RequeueResult = {
|
||||
status: 'success' | 'partial' | 'failed';
|
||||
title: string;
|
||||
message: string;
|
||||
failures?: string[];
|
||||
};
|
||||
|
||||
function formatLocalDate(value: Date) {
|
||||
return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function recentSevenDays(): DateRangeValue {
|
||||
const end = new Date();
|
||||
const start = new Date(end);
|
||||
start.setDate(end.getDate() - 6);
|
||||
return { start: formatLocalDate(start), end: formatLocalDate(end) };
|
||||
}
|
||||
|
||||
function attemptStatusTone(status: string) {
|
||||
if (status === 'acknowledged') return 'success' as const;
|
||||
if (status === 'rejected' || status === 'failed') return 'danger' as const;
|
||||
if (status === 'awaiting_ack') return 'info' as const;
|
||||
return 'neutral' as const;
|
||||
}
|
||||
|
||||
export function DeliveryDetailModal({ record, onClose }: { record: DownstreamDeliveryRecord; onClose: () => void }) {
|
||||
const payloadText = useMemo(() => JSON.stringify(record.payload ?? {}, null, 2), [record.payload]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onClose={onClose}
|
||||
size="xl"
|
||||
title={<div className="template-modal-title"><h2>下游投递详情</h2><p>{record.id}</p></div>}
|
||||
footer={<Button onClick={onClose}>关闭</Button>}
|
||||
>
|
||||
<div className="report-record-detail">
|
||||
<div className="detail-grid">
|
||||
<div><span>企业</span><strong>{record.tenant?.name ?? record.tenantId}</strong></div>
|
||||
<div><span>应用</span><strong>{record.application?.name ?? record.applicationId}</strong></div>
|
||||
<div><span>投递类型</span><strong>{deliveryTypeLabel[record.deliveryType] ?? record.deliveryType}</strong></div>
|
||||
<div><span>当前状态</span><strong>{deliveryStatusLabel(record)}</strong></div>
|
||||
<div><span>消息 ID</span><strong>{record.messageId ?? '-'}</strong></div>
|
||||
<div><span>自动重试次数</span><strong>{record.retryCount}</strong></div>
|
||||
<div><span>人工重投次数</span><strong>{record.manualRetryCount}</strong></div>
|
||||
<div><span>最近人工重投</span><strong>{record.lastRetriedAt ? formatDateTime(record.lastRetriedAt) : '-'}</strong></div>
|
||||
<div><span>下次重试</span><strong>{record.nextRetryAt ?? '-'}</strong></div>
|
||||
<div><span>自动重试</span><strong>{record.retryEnabled ? '开启' : '关闭'}</strong></div>
|
||||
<div><span>写出时间</span><strong>{record.sentAt ?? '-'}</strong></div>
|
||||
<div><span>确认时间</span><strong>{record.acknowledgedAt ?? '-'}</strong></div>
|
||||
<div><span>ACK Result</span><strong>{record.ackResult ?? '-'}</strong></div>
|
||||
<div><span>ACK Sequence_Id</span><strong>{record.ackSequenceId ?? '-'}</strong></div>
|
||||
<div><span>ACK Msg_Id</span><strong>{record.ackMessageId ?? '-'}</strong></div>
|
||||
<div><span>连接 ID</span><strong>{record.connectionId ?? '-'}</strong></div>
|
||||
<div className="detail-grid__wide"><span>最后错误</span><strong>{record.lastError ?? '-'}</strong></div>
|
||||
</div>
|
||||
<section className="report-history">
|
||||
<h3>逐次投递记录</h3>
|
||||
<div className="downstream-attempt-timeline" aria-label="逐次投递记录">
|
||||
{(record.attempts ?? []).map((attempt) => (
|
||||
<article className="downstream-attempt-card" key={attempt.id}>
|
||||
<div className={`downstream-attempt-marker downstream-attempt-marker--${attemptStatusTone(attempt.status)}`}>
|
||||
{attempt.attemptNo}
|
||||
</div>
|
||||
<div className="downstream-attempt-card__body">
|
||||
<header>
|
||||
<strong>第 {attempt.attemptNo} 次投递</strong>
|
||||
<Tag tone={attemptStatusTone(attempt.status)}>{attemptStatusLabel[attempt.status] ?? attempt.status}</Tag>
|
||||
</header>
|
||||
<dl className="downstream-attempt-card__times">
|
||||
<div><dt>发送时间</dt><dd>{attempt.sentAt ? formatDateTime(attempt.sentAt) : '-'}</dd></div>
|
||||
<div><dt>ACK 时间</dt><dd>{attempt.acknowledgedAt ? formatDateTime(attempt.acknowledgedAt) : '-'}</dd></div>
|
||||
<div><dt>ACK 截止</dt><dd>{attempt.ackDeadlineAt ? formatDateTime(attempt.ackDeadlineAt) : '-'}</dd></div>
|
||||
</dl>
|
||||
<dl className="downstream-attempt-card__identifiers">
|
||||
<div><dt>连接 ID</dt><dd>{attempt.connectionId ?? '-'}</dd></div>
|
||||
<div><dt>Sequence_Id</dt><dd>{attempt.sequenceId ?? '-'}</dd></div>
|
||||
<div><dt>Msg_Id</dt><dd>{attempt.messageId ?? '-'}</dd></div>
|
||||
</dl>
|
||||
<div className={`downstream-attempt-result${attempt.errorMessage || attempt.failureType ? ' downstream-attempt-result--error' : ''}`}>
|
||||
<span>ACK Result:{attempt.ackResult ?? '-'}</span>
|
||||
<strong>{attempt.errorMessage ?? attempt.failureType ?? '本次投递未记录异常'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
{(record.attempts ?? []).length === 0 ? <p>暂无逐次投递记录</p> : null}
|
||||
</div>
|
||||
</section>
|
||||
<section className="report-history">
|
||||
<h3>Payload</h3>
|
||||
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0 }}>{payloadText}</pre>
|
||||
</section>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useState, type ComponentProps } from '
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import { AlertTriangle, Ban, BellRing, RefreshCw, Settings2, ShieldCheck, ShieldOff } from 'lucide-react';
|
||||
import { adminApi, type SecurityAlert, type SecurityBlock, type SecurityOverview, type SecurityProtectedNetwork, type SecurityRule } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Chart, Input, Modal as BaseModal, Select, Table, Tabs, Tag, Textarea, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal as BaseModal, Select, Table, Tabs, Tag, Textarea, type TableColumn } from '@/components/ui';
|
||||
import { Chart } from '@/components/ui/Chart';
|
||||
import './AdminSecurityDetectionPage.css';
|
||||
|
||||
const statusLabels: Record<string, string> = { open: '待处理', acknowledged: '已确认', blocked: '已封禁', block_failed: '封禁失败', ignored: '已忽略', requested: '执行中', failed: '失败', released: '已解封' };
|
||||
|
||||
@@ -24,7 +24,8 @@ import {
|
||||
type InfrastructureMonitoringOverview,
|
||||
type InfrastructureMonitoringRange,
|
||||
} from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Chart, Input, Modal, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Chart } from '@/components/ui/Chart';
|
||||
import './AdminSystemMonitoringPage.css';
|
||||
|
||||
const RANGE_OPTIONS: Array<{ value: InfrastructureMonitoringRange; label: string }> = [
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
WalletCards,
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Chart, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Button, Table, Tag, type TableColumn } from '@/components/ui';
|
||||
import { Chart } from '@/components/ui/Chart';
|
||||
import { clientApi, type DashboardResponse } from '@/api/adminApi';
|
||||
import { createLineOption, createPieOption } from '@/theme/chartOptions';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import * as echarts from 'echarts';
|
||||
import { BarChart, LineChart, PieChart } from 'echarts/charts';
|
||||
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components';
|
||||
import { init, use } from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
|
||||
use([BarChart, LineChart, PieChart, GridComponent, LegendComponent, TooltipComponent, CanvasRenderer]);
|
||||
|
||||
type ChartProps = {
|
||||
option: EChartsOption;
|
||||
@@ -15,7 +20,7 @@ export function Chart({ option, height = 280 }: ChartProps) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chart = echarts.init(chartRef.current);
|
||||
const chart = init(chartRef.current);
|
||||
chart.setOption(option);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => chart.resize());
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export { Button } from './Button';
|
||||
export { Breadcrumb } from './Breadcrumb';
|
||||
export { Chart } from './Chart';
|
||||
export { CarrierTag, normalizeCarrierTag } from './CarrierTag';
|
||||
export type { CarrierTagValue } from './CarrierTag';
|
||||
export { DateRangeInput } from './DateRangeInput';
|
||||
|
||||
+75
-59
@@ -1,69 +1,83 @@
|
||||
import { lazy, Suspense, type ComponentType, type LazyExoticComponent } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { AdminAnalyticsPage } from '@/apps/admin/AdminAnalyticsPage';
|
||||
import { AdminChannelGroupFormPage } from '@/apps/admin/AdminChannelGroupFormPage';
|
||||
import { AdminChannelGroupsPage } from '@/apps/admin/AdminChannelGroupsPage';
|
||||
import { AdminChannelsPage } from '@/apps/admin/AdminChannelsPage';
|
||||
import { AdminChannelReportPage } from '@/apps/admin/AdminChannelReportPage';
|
||||
import { AdminCustomerDetailPage } from '@/apps/admin/AdminCustomerDetailPage';
|
||||
import { AdminCustomerFormPage } from '@/apps/admin/AdminCustomerFormPage';
|
||||
import { AdminCustomersPage } from '@/apps/admin/AdminCustomersPage';
|
||||
import { AdminDrainageFieldsPage } from '@/apps/admin/AdminDrainageFieldsPage';
|
||||
import { AdminDrainageDetectionRulesPage } from '@/apps/admin/AdminDrainageDetectionRulesPage';
|
||||
import { AdminDownstreamDeliveriesPage } from '@/apps/admin/AdminDownstreamDeliveriesPage';
|
||||
import { AdminDownstreamRecoveryStatusesPage } from '@/apps/admin/AdminDownstreamRecoveryStatusesPage';
|
||||
import { AdminEnterpriseApplicationsPage } from '@/apps/admin/AdminEnterpriseApplicationsPage';
|
||||
import { AdminEnterpriseBlacklistPage } from '@/apps/admin/AdminEnterpriseBlacklistPage';
|
||||
import { AdminEnterpriseSignaturesPage } from '@/apps/admin/AdminEnterpriseSignaturesPage';
|
||||
import { AdminEnterpriseTemplatesPage } from '@/apps/admin/AdminEnterpriseTemplatesPage';
|
||||
import { AdminGlobalBlacklistPage } from '@/apps/admin/AdminGlobalBlacklistPage';
|
||||
import { AdminGatewaySubmitExceptionsPage } from '@/apps/admin/AdminGatewaySubmitExceptionsPage';
|
||||
import { AdminHome } from '@/apps/admin/AdminHome';
|
||||
import { AdminMonitorPage } from '@/apps/admin/AdminMonitorPage';
|
||||
import { AdminPhoneSegmentsPage } from '@/apps/admin/AdminPhoneSegmentsPage';
|
||||
import { AdminRechargeRecordsPage } from '@/apps/admin/AdminRechargeRecordsPage';
|
||||
import { AdminReconciliationReportsPage } from '@/apps/admin/AdminReconciliationReportsPage';
|
||||
import { AdminProfitReportsPage } from '@/apps/admin/AdminProfitReportsPage';
|
||||
import { AdminQualityReportsPage } from '@/apps/admin/AdminQualityReportsPage';
|
||||
import { AdminReportRecordsPage } from '@/apps/admin/AdminReportRecordsPage';
|
||||
import { AdminReportTasksPage } from '@/apps/admin/AdminReportTasksPage';
|
||||
import { AdminReportMaterialsPage } from '@/apps/admin/AdminReportMaterialsPage';
|
||||
import { AdminSensitiveWordsPage } from '@/apps/admin/AdminSensitiveWordsPage';
|
||||
import { AdminSmsAuditPage } from '@/apps/admin/AdminSmsAuditPage';
|
||||
import { AdminRiskRulesPage } from '@/apps/admin/AdminRiskRulesPage';
|
||||
import { AdminSmsApplicationFormPage } from '@/apps/admin/AdminSmsApplicationFormPage';
|
||||
import { AdminSmsRecordsPage } from '@/apps/admin/AdminSmsRecordsPage';
|
||||
import { AdminSmsTaskProgressPage } from '@/apps/admin/AdminSmsTaskProgressPage';
|
||||
import { AdminSmsUplinkRecordsPage } from '@/apps/admin/AdminSmsUplinkRecordsPage';
|
||||
import { AdminSignatureAuditPage } from '@/apps/admin/AdminSignatureAuditPage';
|
||||
import { AdminSignatureRetirementPage } from '@/apps/admin/AdminSignatureRetirementPage';
|
||||
import { AdminDrainageAuditPage } from '@/apps/admin/AdminDrainageAuditPage';
|
||||
import { AdminSystemLogsPage } from '@/apps/admin/AdminSystemLogsPage';
|
||||
import { AdminSystemMonitoringPage } from '@/apps/admin/system-monitoring/AdminSystemMonitoringPage';
|
||||
import { AdminSecurityDetectionPage } from '@/apps/admin/security-detection/AdminSecurityDetectionPage';
|
||||
import { AdminTemplateAuditPage } from '@/apps/admin/AdminTemplateAuditPage';
|
||||
import { AdminUsersPage } from '@/apps/admin/AdminUsersPage';
|
||||
import { AdminEnterpriseAuditPage } from '@/apps/admin/AdminEnterpriseAuditPage';
|
||||
import { ClientApplicationsPage } from '@/apps/client/ClientApplicationsPage';
|
||||
import { ClientBatchTasksPage } from '@/apps/client/ClientBatchTasksPage';
|
||||
import { ClientBillingPage } from '@/apps/client/ClientBillingPage';
|
||||
import { ClientEnterpriseAuthPage } from '@/apps/client/ClientEnterpriseAuthPage';
|
||||
import { ClientHome } from '@/apps/client/ClientHome';
|
||||
import { ClientHttpApiPage } from '@/apps/client/ClientHttpApiPage';
|
||||
import { ClientSendDetailPage } from '@/apps/client/ClientSendDetailPage';
|
||||
import { ClientSendPage } from '@/apps/client/ClientSendPage';
|
||||
import { ClientSignaturesPage } from '@/apps/client/ClientSignaturesPage';
|
||||
import { ClientSystemLogsPage } from '@/apps/client/ClientSystemLogsPage';
|
||||
import { ClientTemplatesPage } from '@/apps/client/ClientTemplatesPage';
|
||||
import { ClientUplinkMessagesPage } from '@/apps/client/ClientUplinkMessagesPage';
|
||||
import { ClientUsersPage } from '@/apps/client/ClientUsersPage';
|
||||
import { LoginPage } from '@/apps/LoginPage';
|
||||
import { PagePlaceholder } from '@/components/PagePlaceholder';
|
||||
import { AdminLayout } from '@/layouts/AdminLayout';
|
||||
import { ClientLayout } from '@/layouts/ClientLayout';
|
||||
import { RouteLoadBoundary } from './RouteLoadBoundary';
|
||||
|
||||
function lazyNamed(loader: () => Promise<unknown>, exportName: string): LazyExoticComponent<ComponentType<any>> {
|
||||
return lazy(async () => {
|
||||
const loaded = await loader() as Record<string, ComponentType>;
|
||||
const component = loaded[exportName];
|
||||
if (!component) throw new Error(`Lazy route export ${exportName} was not found`);
|
||||
return { default: component };
|
||||
});
|
||||
}
|
||||
|
||||
const AdminAnalyticsPage = lazyNamed(() => import('@/apps/admin/AdminAnalyticsPage'), 'AdminAnalyticsPage');
|
||||
const AdminChannelGroupFormPage = lazyNamed(() => import('@/apps/admin/AdminChannelGroupFormPage'), 'AdminChannelGroupFormPage');
|
||||
const AdminChannelGroupsPage = lazyNamed(() => import('@/apps/admin/AdminChannelGroupsPage'), 'AdminChannelGroupsPage');
|
||||
const AdminChannelsPage = lazyNamed(() => import('@/apps/admin/AdminChannelsPage'), 'AdminChannelsPage');
|
||||
const AdminChannelReportPage = lazyNamed(() => import('@/apps/admin/AdminChannelReportPage'), 'AdminChannelReportPage');
|
||||
const AdminCustomerDetailPage = lazyNamed(() => import('@/apps/admin/AdminCustomerDetailPage'), 'AdminCustomerDetailPage');
|
||||
const AdminCustomerFormPage = lazyNamed(() => import('@/apps/admin/AdminCustomerFormPage'), 'AdminCustomerFormPage');
|
||||
const AdminCustomersPage = lazyNamed(() => import('@/apps/admin/AdminCustomersPage'), 'AdminCustomersPage');
|
||||
const AdminDrainageFieldsPage = lazyNamed(() => import('@/apps/admin/AdminDrainageFieldsPage'), 'AdminDrainageFieldsPage');
|
||||
const AdminDrainageDetectionRulesPage = lazyNamed(() => import('@/apps/admin/AdminDrainageDetectionRulesPage'), 'AdminDrainageDetectionRulesPage');
|
||||
const AdminDownstreamDeliveriesPage = lazyNamed(() => import('@/apps/admin/AdminDownstreamDeliveriesPage'), 'AdminDownstreamDeliveriesPage');
|
||||
const AdminDownstreamRecoveryStatusesPage = lazyNamed(() => import('@/apps/admin/AdminDownstreamRecoveryStatusesPage'), 'AdminDownstreamRecoveryStatusesPage');
|
||||
const AdminEnterpriseApplicationsPage = lazyNamed(() => import('@/apps/admin/AdminEnterpriseApplicationsPage'), 'AdminEnterpriseApplicationsPage');
|
||||
const AdminEnterpriseBlacklistPage = lazyNamed(() => import('@/apps/admin/AdminEnterpriseBlacklistPage'), 'AdminEnterpriseBlacklistPage');
|
||||
const AdminEnterpriseSignaturesPage = lazyNamed(() => import('@/apps/admin/AdminEnterpriseSignaturesPage'), 'AdminEnterpriseSignaturesPage');
|
||||
const AdminEnterpriseTemplatesPage = lazyNamed(() => import('@/apps/admin/AdminEnterpriseTemplatesPage'), 'AdminEnterpriseTemplatesPage');
|
||||
const AdminGlobalBlacklistPage = lazyNamed(() => import('@/apps/admin/AdminGlobalBlacklistPage'), 'AdminGlobalBlacklistPage');
|
||||
const AdminGatewaySubmitExceptionsPage = lazyNamed(() => import('@/apps/admin/AdminGatewaySubmitExceptionsPage'), 'AdminGatewaySubmitExceptionsPage');
|
||||
const AdminHome = lazyNamed(() => import('@/apps/admin/AdminHome'), 'AdminHome');
|
||||
const AdminMonitorPage = lazyNamed(() => import('@/apps/admin/AdminMonitorPage'), 'AdminMonitorPage');
|
||||
const AdminPhoneSegmentsPage = lazyNamed(() => import('@/apps/admin/AdminPhoneSegmentsPage'), 'AdminPhoneSegmentsPage');
|
||||
const AdminRechargeRecordsPage = lazyNamed(() => import('@/apps/admin/AdminRechargeRecordsPage'), 'AdminRechargeRecordsPage');
|
||||
const AdminReconciliationReportsPage = lazyNamed(() => import('@/apps/admin/AdminReconciliationReportsPage'), 'AdminReconciliationReportsPage');
|
||||
const AdminProfitReportsPage = lazyNamed(() => import('@/apps/admin/AdminProfitReportsPage'), 'AdminProfitReportsPage');
|
||||
const AdminQualityReportsPage = lazyNamed(() => import('@/apps/admin/AdminQualityReportsPage'), 'AdminQualityReportsPage');
|
||||
const AdminReportRecordsPage = lazyNamed(() => import('@/apps/admin/AdminReportRecordsPage'), 'AdminReportRecordsPage');
|
||||
const AdminReportTasksPage = lazyNamed(() => import('@/apps/admin/AdminReportTasksPage'), 'AdminReportTasksPage');
|
||||
const AdminReportMaterialsPage = lazyNamed(() => import('@/apps/admin/AdminReportMaterialsPage'), 'AdminReportMaterialsPage');
|
||||
const AdminSensitiveWordsPage = lazyNamed(() => import('@/apps/admin/AdminSensitiveWordsPage'), 'AdminSensitiveWordsPage');
|
||||
const AdminSmsAuditPage = lazyNamed(() => import('@/apps/admin/AdminSmsAuditPage'), 'AdminSmsAuditPage');
|
||||
const AdminRiskRulesPage = lazyNamed(() => import('@/apps/admin/AdminRiskRulesPage'), 'AdminRiskRulesPage');
|
||||
const AdminSmsApplicationFormPage = lazyNamed(() => import('@/apps/admin/AdminSmsApplicationFormPage'), 'AdminSmsApplicationFormPage');
|
||||
const AdminSmsRecordsPage = lazyNamed(() => import('@/apps/admin/AdminSmsRecordsPage'), 'AdminSmsRecordsPage');
|
||||
const AdminSmsTaskProgressPage = lazyNamed(() => import('@/apps/admin/AdminSmsTaskProgressPage'), 'AdminSmsTaskProgressPage');
|
||||
const AdminSmsUplinkRecordsPage = lazyNamed(() => import('@/apps/admin/AdminSmsUplinkRecordsPage'), 'AdminSmsUplinkRecordsPage');
|
||||
const AdminSignatureAuditPage = lazyNamed(() => import('@/apps/admin/AdminSignatureAuditPage'), 'AdminSignatureAuditPage');
|
||||
const AdminSignatureRetirementPage = lazyNamed(() => import('@/apps/admin/AdminSignatureRetirementPage'), 'AdminSignatureRetirementPage');
|
||||
const AdminDrainageAuditPage = lazyNamed(() => import('@/apps/admin/AdminDrainageAuditPage'), 'AdminDrainageAuditPage');
|
||||
const AdminSystemLogsPage = lazyNamed(() => import('@/apps/admin/AdminSystemLogsPage'), 'AdminSystemLogsPage');
|
||||
const AdminSystemMonitoringPage = lazyNamed(() => import('@/apps/admin/system-monitoring/AdminSystemMonitoringPage'), 'AdminSystemMonitoringPage');
|
||||
const AdminSecurityDetectionPage = lazyNamed(() => import('@/apps/admin/security-detection/AdminSecurityDetectionPage'), 'AdminSecurityDetectionPage');
|
||||
const AdminTemplateAuditPage = lazyNamed(() => import('@/apps/admin/AdminTemplateAuditPage'), 'AdminTemplateAuditPage');
|
||||
const AdminUsersPage = lazyNamed(() => import('@/apps/admin/AdminUsersPage'), 'AdminUsersPage');
|
||||
const AdminEnterpriseAuditPage = lazyNamed(() => import('@/apps/admin/AdminEnterpriseAuditPage'), 'AdminEnterpriseAuditPage');
|
||||
const ClientApplicationsPage = lazyNamed(() => import('@/apps/client/ClientApplicationsPage'), 'ClientApplicationsPage');
|
||||
const ClientBatchTasksPage = lazyNamed(() => import('@/apps/client/ClientBatchTasksPage'), 'ClientBatchTasksPage');
|
||||
const ClientBillingPage = lazyNamed(() => import('@/apps/client/ClientBillingPage'), 'ClientBillingPage');
|
||||
const ClientEnterpriseAuthPage = lazyNamed(() => import('@/apps/client/ClientEnterpriseAuthPage'), 'ClientEnterpriseAuthPage');
|
||||
const ClientHome = lazyNamed(() => import('@/apps/client/ClientHome'), 'ClientHome');
|
||||
const ClientHttpApiPage = lazyNamed(() => import('@/apps/client/ClientHttpApiPage'), 'ClientHttpApiPage');
|
||||
const ClientSendDetailPage = lazyNamed(() => import('@/apps/client/ClientSendDetailPage'), 'ClientSendDetailPage');
|
||||
const ClientSendPage = lazyNamed(() => import('@/apps/client/ClientSendPage'), 'ClientSendPage');
|
||||
const ClientSignaturesPage = lazyNamed(() => import('@/apps/client/ClientSignaturesPage'), 'ClientSignaturesPage');
|
||||
const ClientSystemLogsPage = lazyNamed(() => import('@/apps/client/ClientSystemLogsPage'), 'ClientSystemLogsPage');
|
||||
const ClientTemplatesPage = lazyNamed(() => import('@/apps/client/ClientTemplatesPage'), 'ClientTemplatesPage');
|
||||
const ClientUplinkMessagesPage = lazyNamed(() => import('@/apps/client/ClientUplinkMessagesPage'), 'ClientUplinkMessagesPage');
|
||||
const ClientUsersPage = lazyNamed(() => import('@/apps/client/ClientUsersPage'), 'ClientUsersPage');
|
||||
|
||||
export function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<RouteLoadBoundary>
|
||||
<Suspense fallback={<div className="page-stack"><div className="surface ui-table__empty">页面加载中...</div></div>}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/client" replace />} />
|
||||
<Route path="/client/login" element={<LoginPage portal="client" />} />
|
||||
<Route path="/admin/login" element={<LoginPage portal="admin" />} />
|
||||
@@ -148,6 +162,8 @@ export function AppRoutes() {
|
||||
<Route path="security-detection" element={<AdminSecurityDetectionPage />} />
|
||||
<Route path="*" element={<PagePlaceholder />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</RouteLoadBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
||||
|
||||
type Props = { children: ReactNode };
|
||||
type State = { error?: Error };
|
||||
|
||||
export class RouteLoadBoundary extends Component<Props, State> {
|
||||
state: State = {};
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
console.error('Route chunk failed to load', error, info.componentStack);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.error) return this.props.children;
|
||||
return (
|
||||
<div className="page-stack">
|
||||
<div className="surface ui-table__empty" role="alert">
|
||||
<p>页面资源加载失败,请检查网络后重试。</p>
|
||||
<button className="ui-button ui-button--primary" type="button" onClick={() => window.location.reload()}>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user