feat: add carrier-aware signature retirement alerts

This commit is contained in:
hectorzhao
2026-08-10 20:54:05 +08:00
parent 232d1c22a3
commit 55aa054005
52 changed files with 3074 additions and 152 deletions
+11 -4
View File
@@ -46,6 +46,7 @@ export function AdminLayout() {
function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
const [pendingAudits, setPendingAudits] = useState(EMPTY_PENDING_AUDITS);
const [retirementUnreadCount, setRetirementUnreadCount] = useState(0);
const [sessionLocked, setSessionLocked] = useState(Boolean(session.locked));
const loadPendingAuditCount = useCallback(() => {
const currentSession = readSession('admin');
@@ -54,12 +55,14 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
return;
}
// This runs globally and on a timer, so it must not fan out through the full dashboard aggregation.
adminApi.getPendingAudits()
.then((counts) => {
setPendingAudits(counts);
Promise.allSettled([adminApi.getPendingAudits(), adminApi.getSignatureRetirementUnreadCount()])
.then(([audits, retirement]) => {
setPendingAudits(audits.status === 'fulfilled' ? audits.value : EMPTY_PENDING_AUDITS);
setRetirementUnreadCount(retirement.status === 'fulfilled' ? retirement.value.count : 0);
})
.catch(() => {
setPendingAudits(EMPTY_PENDING_AUDITS);
setRetirementUnreadCount(0);
});
}, []);
@@ -73,10 +76,12 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
const onAuditRefresh = () => loadPendingAuditCount();
window.addEventListener('focus', onFocus);
window.addEventListener('cmpp-audit-count-refresh', onAuditRefresh);
window.addEventListener('cmpp-retirement-count-refresh', onAuditRefresh);
return () => {
window.clearInterval(timer);
window.removeEventListener('focus', onFocus);
window.removeEventListener('cmpp-audit-count-refresh', onAuditRefresh);
window.removeEventListener('cmpp-retirement-count-refresh', onAuditRefresh);
};
}, [loadPendingAuditCount, session.portal, sessionLocked]);
@@ -90,6 +95,7 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
userName={session.user.displayName}
userRole="平台管理员"
onSessionLockedChange={setSessionLocked}
retirementAlert={{ count: retirementUnreadCount, to: '/admin/signature-retirement' }}
auditNotifications={[
{ label: '企业认证待审', count: pendingAudits.enterpriseCertifications, to: '/admin/enterprise-audit' },
{ label: '短信审核待审', count: pendingAudits.smsAudits, to: '/admin/sms-audit' },
@@ -105,7 +111,7 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
{ label: '运营看板', to: '/admin', icon: Gauge },
{ label: '发送监控', to: '/admin/monitor', icon: Activity },
{ label: '网关异常', to: '/admin/gateway-submit-exceptions', icon: AlertTriangle },
{ label: '数据统计', to: '/admin/analytics', icon: BarChart3 },
{ label: '签名质量检测', to: '/admin/analytics', icon: BarChart3 },
],
},
{
@@ -181,6 +187,7 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
icon: Shield,
items: [
{ label: '风控规则', to: '/admin/risk-rules', icon: Shield },
{ label: '签名清退预警', to: '/admin/signature-retirement', icon: AlertTriangle },
{ label: '企业黑名单', to: '/admin/enterprise-blacklist', icon: UserX },
{ label: '全局黑名单', to: '/admin/global-blacklist', icon: ShieldOff },
{ label: '敏感词管理', to: '/admin/sensitive-words', icon: Shield },
+15 -1
View File
@@ -2,6 +2,7 @@ import type { ComponentType } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import {
Bell,
ClipboardList,
ChevronDown,
ChevronRight,
CircleHelp,
@@ -59,6 +60,7 @@ type AppShellProps = {
userRole: string;
navSections: ShellNavSection[];
auditNotifications?: AuditNotificationItem[];
retirementAlert?: { count: number; to: string };
onSessionLockedChange?: (locked: boolean) => void;
};
@@ -71,6 +73,7 @@ export function AppShell({
userRole,
navSections,
auditNotifications = [],
retirementAlert,
onSessionLockedChange,
}: AppShellProps) {
const [collapsed, setCollapsed] = useState(false);
@@ -414,6 +417,17 @@ export function AppShell({
<button className="icon-button topbar-help" type="button" aria-label="帮助中心">
<CircleHelp size={18} />
</button>
{retirementAlert ? (
<Link
aria-label={`今日未读且未抑制签名清退预警 ${retirementAlert.count}`}
className={['icon-button', retirementAlert.count > 0 ? 'has-dot' : ''].filter(Boolean).join(' ')}
title="今日未读且未抑制签名清退预警"
to={retirementAlert.to}
>
<Bell size={18} />
{retirementAlert.count > 0 ? <span className="notice-count">{retirementAlert.count}</span> : null}
</Link>
) : null}
<div className="notice-menu-wrap">
<button
aria-expanded={noticeOpen}
@@ -423,7 +437,7 @@ export function AppShell({
type="button"
aria-label="通知"
>
<Bell size={18} />
<ClipboardList size={18} />
{auditTotal > 0 ? <span className="notice-count">{auditTotal}</span> : null}
</button>
{noticeOpen ? (