feat: add carrier-aware signature retirement alerts
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user