fix: close documented platform polish gaps

This commit is contained in:
hectorzhao
2026-07-11 18:20:38 +08:00
parent 6236547871
commit 2ecbb920a6
25 changed files with 193 additions and 110 deletions
+3 -2
View File
@@ -210,6 +210,7 @@ export type DashboardResponse = {
transactions: { _count: { _all: number }; _sum: { amountCents?: number | null; smsUnits?: number | null } };
gatewayConnections: Array<{ status: string; _count: { _all: number }; _sum: { currentConnections?: number | null; desiredConnections?: number | null } }>;
pendingAuditCount: number;
pendingAudits: { enterpriseCertifications: number; smsAudits: number; templates: number; signatures: number; total: number };
downstreamDeliverySummary?: {
pending: number;
failed: number;
@@ -981,8 +982,8 @@ export const adminApi = {
createEnterpriseBlacklist: (body: { tenantId: string; applicationId: string; phoneNumber: string; reason?: string; status?: string; operatorId?: string }) =>
request<DictionaryItem>('/admin/dictionaries/blacklists/enterprise', { method: 'POST', body: JSON.stringify(body) }),
deleteEnterpriseBlacklist: (id: string) => request<DictionaryItem>(`/admin/dictionaries/blacklists/enterprise/${id}`, { method: 'DELETE' }),
listPhoneSegments: (query: { keyword?: string; cursor?: string; pageSize?: number } = {}) =>
request<CursorPage<DictionaryItem>>(withQuery('/admin/dictionaries/phone-segments', query)),
listPhoneSegments: (query: { keyword?: string; page?: number; pageSize?: number } = {}) =>
request<{ items: DictionaryItem[]; total: number; page: number; pageSize: number }>(withQuery('/admin/dictionaries/phone-segments', query)),
createPhoneSegment: (body: { prefix: string; carrier: string; province?: string; city?: string }) =>
request<DictionaryItem>('/admin/dictionaries/phone-segments', { method: 'POST', body: JSON.stringify(body) }),
listPhoneCarrierRules: (query: { keyword?: string; page?: number; pageSize?: number } = {}) =>
+1 -1
View File
@@ -73,7 +73,7 @@ export function LoginPage({ portal }: LoginPageProps) {
<div className="login-brand">
<img alt={isAdmin ? 'CMPP 运营端 logo' : 'CMPP 客户端 logo'} src="/logo/logo1.png" />
<div>
<h1></h1>
<h1></h1>
<p>{isAdmin ? '运营端登录' : '客户端登录'}</p>
</div>
</div>
+4 -10
View File
@@ -3,6 +3,7 @@ import { CheckCircle2, Copy, Eye, ExternalLink, FileText, Info, Pencil, Plus, Po
import { useNavigate } from 'react-router-dom';
import { adminApi, type AdminChannel, type ChannelConnectionLogResponse, type ChannelTestResponse, type CmppConnectionState } from '@/api/adminApi';
import { Breadcrumb, Button, Input, Modal, Pagination, Select, Tag, Textarea } from '@/components/ui';
import { formatDateTime } from '@/utils/dateTime';
type Carrier = 'mobile' | 'unicom' | 'telecom' | 'all';
type ChannelStatus = 'normal' | 'stopped' | 'connecting' | 'failed';
@@ -97,13 +98,6 @@ const regionOptions = [
...'北京,天津,河北,山西,内蒙古,辽宁,吉林,黑龙江,上海,江苏,浙江,安徽,福建,江西,山东,河南,湖北,湖南,广东,广西,海南,重庆,四川,贵州,云南,西藏,陕西,甘肃,青海,宁夏,新疆,香港,澳门,台湾'.split(',').map((province) => ({ label: province, value: province })),
];
const extensionOptions = [
{ label: '0', value: '0' },
{ label: '2', value: '2' },
{ label: '4', value: '4' },
{ label: '6', value: '6' },
];
const carrierLabelMap: Record<Carrier, string> = {
mobile: '移动',
unicom: '联通',
@@ -321,7 +315,7 @@ function ChannelFormModal({
/>
<div className="sms-channel-inline-field">
<Input label="* 接入号" onChange={(event) => setAccessNo(event.target.value)} placeholder="请输入通道接入号" value={accessNo} />
<Select label="展位数" onChange={(event) => setExtensionDigits(event.target.value)} options={extensionOptions} value={extensionDigits} />
<Input label="展位数" max="20" min="0" onChange={(event) => setExtensionDigits(event.target.value)} type="number" value={extensionDigits} />
</div>
<Input label="* 通道流速" max="2000" min="1" onChange={(event) => setFlowLimit(event.target.value)} suffix="条/秒" type="number" value={flowLimit} />
<Input label="* 期望连接数" onChange={(event) => setDesiredConnections(event.target.value)} placeholder="1" value={desiredConnections} />
@@ -732,7 +726,7 @@ export function AdminChannelsPage() {
</div>
<div>
<span></span>
<strong>{connection.lastHeartbeatAt ? new Date(connection.lastHeartbeatAt).toLocaleString('zh-CN', { hour12: false }) : '-'}</strong>
<strong>{formatDateTime(connection.lastHeartbeatAt)}</strong>
</div>
{connection.lastError ? <p>{connection.lastError}</p> : null}
</article>
@@ -749,7 +743,7 @@ export function AdminChannelsPage() {
<article className="channel-log-item" key={log.id}>
<div>
<strong>{log.event}</strong>
<span>{new Date(log.time).toLocaleString('zh-CN', { hour12: false })}</span>
<span>{formatDateTime(log.time)}</span>
</div>
<div>
<span>{log.resourceId}</span>
+2 -1
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { Check, FileSearch, Search, X } from 'lucide-react';
import { adminApi, type EnterpriseCertification } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
import { Breadcrumb, Button, Input, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui';
type EnterpriseAuditStatus = 'pending' | 'approved' | 'rejected';
@@ -59,7 +60,7 @@ function mapCertification(record: EnterpriseCertification): EnterpriseAuditRecor
contactName: record.contactName ?? '',
contactPhone: record.contactPhone ?? '',
contactEmail: String(materials.contactEmail ?? ''),
submittedAt: new Date(record.submittedAt).toLocaleString('zh-CN', { hour12: false }),
submittedAt: formatDateTime(record.submittedAt),
reviewRemark: record.rejectReason ?? String(materials.reviewRemark ?? ''),
status: record.status as EnterpriseAuditStatus,
};
@@ -455,7 +455,7 @@ function DrainageReportModal({ item, onClose }: { item: DrainageInfo; onClose: (
<Modal footer={<Button onClick={onClose}></Button>} onClose={onClose} open title="引流信息报备详情">
<div className="detail-grid">
<div><span></span><strong>{item.siteName}</strong></div>
<div><span></span><strong>{item.url}</strong></div>
<div><span></span><strong>{item.url}</strong></div>
<div><span></span><StatusTag status={item.mobile} /></div>
<div><span></span><StatusTag status={item.unicom} /></div>
<div><span></span><StatusTag status={item.telecom} /></div>
+23 -10
View File
@@ -85,6 +85,7 @@ export function AdminHome() {
const todaySpend = (dashboard?.today.spendCents ?? 0) / 100;
const activeConnectionCount = dashboard?.gatewayConnections.reduce((sum, item) => sum + (item._sum.currentConnections ?? 0), 0) ?? 0;
const downstreamAlertCount = dashboard?.downstreamDeliverySummary?.alertCount ?? 0;
const pendingAudits = dashboard?.pendingAudits ?? { enterpriseCertifications: 0, smsAudits: 0, templates: 0, signatures: 0, total: 0 };
const sendTrendOption = useMemo(
() => createLineOption({
@@ -99,12 +100,12 @@ export function AdminHome() {
const auditTrendOption = useMemo(
() => createBarOption({
labels: ['待审核'],
labels: ['企业认证', '短信审核', '模板', '签名'],
series: [
{ name: '待审', data: [dashboard?.pendingAuditCount ?? 0] },
{ name: '待审', data: [pendingAudits.enterpriseCertifications, pendingAudits.smsAudits, pendingAudits.templates, pendingAudits.signatures] },
],
}),
[dashboard],
[pendingAudits],
);
const enterpriseColumns: Array<TableColumn<EnterpriseSpendRank>> = [
@@ -230,14 +231,26 @@ export function AdminHome() {
<BarChart3 size={20} className="status-info" />
</div>
<div className="overview-grid overview-grid--three">
<div className="mini-status-card">
<Button className="mini-status-card" onClick={() => navigate('/admin/enterprise-audit')} variant="ghost">
<FileCheck2 size={22} />
<div>
<span></span>
<strong>{dashboard?.pendingAuditCount ?? 0} </strong>
<small></small>
</div>
</div>
<span></span>
<strong>{pendingAudits.enterpriseCertifications} </strong>
</Button>
<Button className="mini-status-card" onClick={() => navigate('/admin/sms-audit')} variant="ghost">
<FileCheck2 size={22} />
<span></span>
<strong>{pendingAudits.smsAudits} </strong>
</Button>
<Button className="mini-status-card" onClick={() => navigate('/admin/templates')} variant="ghost">
<FileCheck2 size={22} />
<span></span>
<strong>{pendingAudits.templates} </strong>
</Button>
<Button className="mini-status-card" onClick={() => navigate('/admin/enterprise-signatures')} variant="ghost">
<FileCheck2 size={22} />
<span></span>
<strong>{pendingAudits.signatures} </strong>
</Button>
<div className="mini-status-card">
<ShieldCheck size={22} />
<div>
+10 -19
View File
@@ -20,6 +20,7 @@ type CarrierRule = DictionaryItem & {
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');
@@ -39,16 +40,12 @@ export function AdminPhoneSegmentsPage() {
const [segmentQuery, setSegmentQuery] = useState('');
const [page, setPage] = useState(1);
const [rulePage, setRulePage] = 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);
useEffect(() => {
const timer = window.setTimeout(() => {
setSegmentQuery(keyword.trim());
setPage(1);
setPageCursors([undefined]);
}, 300);
return () => window.clearTimeout(timer);
}, [keyword]);
@@ -57,14 +54,13 @@ export function AdminPhoneSegmentsPage() {
let cancelled = false;
setLoading(true);
Promise.all([
adminApi.listPhoneSegments({ keyword: segmentQuery || undefined, cursor: pageCursors[page - 1], pageSize }),
adminApi.listPhoneSegments({ keyword: segmentQuery || undefined, page, pageSize }),
adminApi.listPhoneCarrierRules({ keyword: activeTab === 'rules' ? segmentQuery || undefined : undefined, page: rulePage, pageSize }),
])
.then(([segmentPage, ruleResponse]) => {
if (cancelled) return;
setSegments(segmentPage.items as PhoneSegment[]);
setHasMore(segmentPage.hasMore);
setNextCursor(segmentPage.nextCursor);
setSegmentTotal(segmentPage.total);
setRules(ruleResponse.items as CarrierRule[]);
setRuleTotal(ruleResponse.total);
setError('');
@@ -78,8 +74,9 @@ export function AdminPhoneSegmentsPage() {
return () => {
cancelled = true;
};
}, [activeTab, page, pageCursors, reloadKey, rulePage, segmentQuery]);
}, [activeTab, page, reloadKey, rulePage, segmentQuery]);
const segmentTotalPages = Math.max(1, Math.ceil(segmentTotal / pageSize));
const ruleTotalPages = Math.max(1, Math.ceil(ruleTotal / pageSize));
function createSegment() {
@@ -90,7 +87,6 @@ export function AdminPhoneSegmentsPage() {
setCity('');
setCreating(false);
setPage(1);
setPageCursors([undefined]);
setReloadKey((current) => current + 1);
})
.catch((failure: Error) => setError(failure.message || '手机号段新增失败'));
@@ -157,17 +153,12 @@ export function AdminPhoneSegmentsPage() {
<Pagination
page={page}
previousDisabled={page <= 1 || loading}
nextDisabled={!hasMore || loading}
nextDisabled={page >= segmentTotalPages || 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);
}}
onNext={() => setPage((current) => Math.min(segmentTotalPages, current + 1))}
onPageChange={setPage}
total={segmentTotal}
totalPages={segmentTotalPages}
/>
</>
),
@@ -146,7 +146,6 @@ export function AdminRechargeRecordsPage() {
<th style={{ width: '130px' }}></th>
<th style={{ width: '140px' }}></th>
<th style={{ width: '120px' }}></th>
<th style={{ width: '140px' }}></th>
<th style={{ width: '300px' }}></th>
</tr>
</thead>
@@ -166,7 +165,6 @@ export function AdminRechargeRecordsPage() {
<td>¥{formatAmount(record.amountCents / 100)}</td>
<td>{record.balanceAfterCents === null || record.balanceAfterCents === undefined ? '-' : `¥${formatAmount(record.balanceAfterCents / 100)}`}</td>
<td><Tag tone="warning"></Tag></td>
<td>{record.operatorId || '运营'}</td>
<td><RemarkCell value={record.remark ?? undefined} /></td>
</tr>
);
+17 -2
View File
@@ -267,6 +267,7 @@ function SendDetailModal({
}
export function AdminSmsRecordsPage() {
const pageSize = 25;
const [records, setRecords] = useState<SmsMessageRecord[]>([]);
const [enterprise, setEnterprise] = useState('all');
const [application, setApplication] = useState('all');
@@ -279,6 +280,7 @@ export function AdminSmsRecordsPage() {
const [segmentAudits, setSegmentAudits] = useState<SmsMessageSegmentAudit[]>([]);
const [segmentLoading, setSegmentLoading] = useState(false);
const [error, setError] = useState('');
const [page, setPage] = useState(1);
function loadData() {
adminApi.listOperationMessages({
@@ -293,6 +295,7 @@ export function AdminSmsRecordsPage() {
})
.then((items) => {
setRecords(items);
setPage(1);
setError('');
})
.catch((failure: Error) => setError(failure.message || '短信记录加载失败'));
@@ -336,6 +339,9 @@ export function AdminSmsRecordsPage() {
}, [enterprise, records]);
const filteredRows = records;
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
const currentPage = Math.min(page, totalPages);
const visibleRows = filteredRows.slice((currentPage - 1) * pageSize, currentPage * pageSize);
const segmentColumns: Array<TableColumn<SmsMessageSegmentAudit>> = [
{ key: 'segment', title: '分片', width: '90px', render: (record) => `${record.segmentIndex}/${record.segmentTotal}` },
@@ -421,7 +427,7 @@ export function AdminSmsRecordsPage() {
<tr>
<td className="ui-table__empty" colSpan={5}></td>
</tr>
) : filteredRows.map((record) => (
) : visibleRows.map((record) => (
<tr key={record.id}>
<td>
<div className="admin-sms-record-sender">
@@ -453,7 +459,16 @@ export function AdminSmsRecordsPage() {
</tbody>
</table>
</div>
<Pagination total={filteredRows.length} />
<Pagination
nextDisabled={currentPage >= totalPages}
onNext={() => setPage((current) => Math.min(totalPages, current + 1))}
onPageChange={setPage}
onPrevious={() => setPage((current) => Math.max(1, current - 1))}
page={currentPage}
previousDisabled={currentPage <= 1}
total={filteredRows.length}
totalPages={totalPages}
/>
</div>
{selectedRecord ? (
+4 -3
View File
@@ -1,6 +1,7 @@
import { Fragment, useEffect, useMemo, useState } from 'react';
import { BarChart3, CalendarClock, Eye, MapPin, Search, Send, Smartphone, StopCircle, TrendingUp } from 'lucide-react';
import { adminApi, type SmsBatchTask, type SmsMessageRecord } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
import {
Breadcrumb,
Button,
@@ -85,7 +86,7 @@ function formatNumber(value: number) {
}
function formatTime(value?: string | null) {
return value ? `${value.slice(0, 10)} ${value.slice(11, 16)}` : '-';
return formatDateTime(value);
}
function normalizeTaskStatus(status: string): TaskStatus {
@@ -468,7 +469,7 @@ export function AdminSmsTaskProgressPage() {
<span>{record.application}</span>
</div>
</td>
<td><span>{record.submittedAt.slice(0, 10)}<br />{record.submittedAt.slice(11, 16)}</span></td>
<td><span>{formatTime(record.submittedAt)}</span></td>
<td>
<div className="admin-task-counts">
<strong>{formatNumber(record.phoneCount)}</strong>
@@ -481,7 +482,7 @@ export function AdminSmsTaskProgressPage() {
{record.sendType === 'scheduled' ? <CalendarClock size={13} /> : null}
{sendTypeLabels[record.sendType]}
</Tag>
{record.scheduledAt ? <span>{record.scheduledAt.slice(0, 10)}<br />{record.scheduledAt.slice(11, 16)}</span> : null}
{record.scheduledAt ? <span>{formatTime(record.scheduledAt)}</span> : null}
</div>
</td>
<td>
+2 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { Check, Search, X } from 'lucide-react';
import { Breadcrumb, Button, Input, Select, Table, Tag, type TableColumn } from '@/components/ui';
import { adminApi, type SmsTemplateAudit } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
const auditStatusLabelMap: Record<string, string> = {
pending: '待审核',
@@ -41,7 +42,7 @@ export function AdminTemplateAuditPage() {
{ key: 'customer', title: '客户', render: (record) => record.tenant?.name ?? record.tenantId },
{ key: 'application', title: '短信应用', render: (record) => record.application?.name ?? record.applicationId },
{ key: 'content', title: '短信模板内容', render: (record) => record.content },
{ key: 'submittedAt', title: '提交时间', render: (record) => new Date(record.createdAt).toLocaleString('zh-CN', { hour12: false }) },
{ key: 'submittedAt', title: '提交时间', render: (record) => formatDateTime(record.createdAt) },
{
key: 'status',
title: '状态',
+37 -4
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { KeyRound, Plus, Search } from 'lucide-react';
import { Eye, EyeOff, KeyRound, Plus, RefreshCw, Search } from 'lucide-react';
import { adminApi, type ManagedUser, type TenantOption, type UserPayload } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
import { readSession } from '@/api/session';
@@ -51,6 +51,13 @@ function toForm(user?: ManagedUser): UserForm {
} : emptyForm;
}
function generateInitialPassword() {
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%';
const values = new Uint32Array(14);
crypto.getRandomValues(values);
return Array.from(values, (value) => alphabet[value % alphabet.length]).join('');
}
export function AdminUsersPage() {
const session = readSession();
const [users, setUsers] = useState<ManagedUser[]>([]);
@@ -61,6 +68,7 @@ export function AdminUsersPage() {
const [form, setForm] = useState<UserForm>(emptyForm);
const [passwordUser, setPasswordUser] = useState<ManagedUser | null>(null);
const [newPassword, setNewPassword] = useState('');
const [showInitialPassword, setShowInitialPassword] = useState(false);
const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null);
const [error, setError] = useState('');
const [saving, setSaving] = useState(false);
@@ -84,7 +92,8 @@ export function AdminUsersPage() {
}, [keyword, users]);
function openCreate() {
setForm({ ...emptyForm, tenantId: tenants[0]?.id ?? '' });
setForm({ ...emptyForm, password: generateInitialPassword(), tenantId: tenants[0]?.id ?? '' });
setShowInitialPassword(false);
setCreating(true);
}
@@ -223,8 +232,32 @@ export function AdminUsersPage() {
value={form.tenantId}
/>
) : null}
{creating ? <Input label="初始密码" onChange={(event) => updateField('password', event.target.value)} required type="password" value={form.password} /> : null}
<Select label="状态" onChange={(event) => updateField('status', event.target.value)} options={[{ label: '启用', value: 'active' }, { label: '禁用', value: 'disabled' }]} value={form.status} />
{creating ? (
<Input
label="初始密码"
onChange={(event) => updateField('password', event.target.value)}
required
suffix={(
<>
<button aria-label={showInitialPassword ? '隐藏初始密码' : '显示初始密码'} className="icon-button" onClick={() => setShowInitialPassword((current) => !current)} type="button">
{showInitialPassword ? <EyeOff size={15} /> : <Eye size={15} />}
</button>
<button aria-label="随机生成初始密码" className="icon-button" onClick={() => updateField('password', generateInitialPassword())} type="button">
<RefreshCw size={15} />
</button>
</>
)}
type={showInitialPassword ? 'text' : 'password'}
value={form.password}
/>
) : null}
<div className="admin-app-form-row admin-app-form-row--wide">
<span></span>
<div className="radio-row">
<label><input checked={form.status === 'active'} onChange={() => updateField('status', 'active')} type="radio" /></label>
<label><input checked={form.status === 'disabled'} onChange={() => updateField('status', 'disabled')} type="radio" /></label>
</div>
</div>
</div>
</Modal>
) : null}
+6 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { Check, Download, FileText, Plus, Search, Send, Trash2 } from 'lucide-react';
import { Button, DateTimeInput, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
import { clientApi, type ClientSmsApplication, type ClientSmsSignature, type ClientSmsTemplate, type ImportPreviewResponse, type SmsBatchTask } from '@/api/adminApi';
import { formatCents } from '@/utils/currency';
type Recipient = {
id: string;
@@ -59,6 +60,10 @@ export function ClientSendPage() {
() => templates.find((item) => item.id === templateId),
[templates, templateId],
);
const selectedApplication = useMemo(
() => applications.find((item) => item.id === applicationId),
[applicationId, applications],
);
const filteredTemplates = templates.filter((item) => (
item.name.includes(templateKeyword) || item.content.includes(templateKeyword)
));
@@ -378,7 +383,7 @@ export function ClientSendPage() {
</div>
<div>
<span></span>
<strong>¥0.05 / </strong>
<strong>¥{formatCents(selectedApplication?.customerUnitPrice)} / </strong>
</div>
</div>
<div className="preview-note"> 70 / 67 /</div>
+2
View File
@@ -91,10 +91,12 @@ export function Table<T>({ columns, data, rowKey, emptyText = '暂无数据', pa
<Pagination
nextDisabled={activePage >= totalPages}
onNext={() => setPage((current) => Math.min(totalPages, current + 1))}
onPageChange={setPage}
onPrevious={() => setPage((current) => Math.max(1, current - 1))}
page={activePage}
previousDisabled={activePage <= 1}
total={data.length}
totalPages={totalPages}
/>
) : null}
</div>
+7 -4
View File
@@ -37,16 +37,16 @@ import { AppShell } from '@/layouts/AppShell';
export function AdminLayout() {
const session = readSession();
const [pendingAuditCount, setPendingAuditCount] = useState(0);
const [pendingAudits, setPendingAudits] = useState({ enterpriseCertifications: 0, smsAudits: 0, templates: 0, signatures: 0 });
const [downstreamAlertCount, setDownstreamAlertCount] = useState(0);
const loadPendingAuditCount = useCallback(() => {
adminApi.getDashboard()
.then((dashboard) => {
setPendingAuditCount(dashboard.pendingAuditCount ?? 0);
setPendingAudits(dashboard.pendingAudits ?? { enterpriseCertifications: 0, smsAudits: 0, templates: 0, signatures: 0 });
setDownstreamAlertCount(dashboard.downstreamDeliverySummary?.alertCount ?? 0);
})
.catch(() => {
setPendingAuditCount(0);
setPendingAudits({ enterpriseCertifications: 0, smsAudits: 0, templates: 0, signatures: 0 });
setDownstreamAlertCount(0);
});
}, []);
@@ -78,7 +78,10 @@ export function AdminLayout() {
userName={session.user.displayName}
userRole="平台管理员"
auditNotifications={[
{ label: '待处理审核', count: pendingAuditCount, to: '/admin/sms-audit' },
{ label: '企业认证待审', count: pendingAudits.enterpriseCertifications, to: '/admin/enterprise-audit' },
{ label: '短信审核待审', count: pendingAudits.smsAudits, to: '/admin/sms-audit' },
{ label: '模板待审', count: pendingAudits.templates, to: '/admin/templates' },
{ label: '签名待审', count: pendingAudits.signatures, to: '/admin/enterprise-signatures' },
{ label: '下游投递告警', count: downstreamAlertCount, to: '/admin/downstream-deliveries' },
]}
navSections={[
+1 -1
View File
@@ -22,7 +22,7 @@ export function ClientLayout() {
return (
<AppShell
title="短信平台客户端"
title="短信服务平台"
subtitle="短信服务控制台"
workspaceName={session.user.tenantName ?? '企业客户空间'}
loginPath="/client/login"
+4 -4
View File
@@ -201,9 +201,8 @@
.ui-input:focus-within,
.ui-select:focus-within,
.ui-select--open {
border-color: var(--color-selected);
background: color-mix(in srgb, var(--color-selected) 4%, transparent);
box-shadow: var(--focus-ring);
border-color: var(--color-border-strong);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-border-strong) 18%, transparent);
}
.ui-input input::selection,
@@ -213,7 +212,8 @@
}
.ui-textarea:focus {
background: color-mix(in srgb, var(--color-selected) 4%, transparent);
border-color: var(--color-border-strong);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-border-strong) 18%, transparent);
}
.ui-input--error,