feat: 完善服务监控与下游重投
This commit is contained in:
@@ -48,6 +48,7 @@ export function AdminLayout() {
|
||||
function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
|
||||
const [pendingAudits, setPendingAudits] = useState(EMPTY_PENDING_AUDITS);
|
||||
const [retirementUnreadCount, setRetirementUnreadCount] = useState(0);
|
||||
const [securityAlertSummary, setSecurityAlertSummary] = useState({ count: 0, criticalCount: 0 });
|
||||
const [sessionLocked, setSessionLocked] = useState(Boolean(session.locked));
|
||||
const loadPendingAuditCount = useCallback(() => {
|
||||
const currentSession = readSession('admin');
|
||||
@@ -56,14 +57,16 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
|
||||
return;
|
||||
}
|
||||
// This runs globally and on a timer, so it must not fan out through the full dashboard aggregation.
|
||||
Promise.allSettled([adminApi.getPendingAudits(), adminApi.getSignatureRetirementUnreadCount()])
|
||||
.then(([audits, retirement]) => {
|
||||
Promise.allSettled([adminApi.getPendingAudits(), adminApi.getSignatureRetirementUnreadCount(), adminApi.getSecurityNotificationSummary()])
|
||||
.then(([audits, retirement, security]) => {
|
||||
setPendingAudits(audits.status === 'fulfilled' ? audits.value : EMPTY_PENDING_AUDITS);
|
||||
setRetirementUnreadCount(retirement.status === 'fulfilled' ? retirement.value.count : 0);
|
||||
setSecurityAlertSummary(security.status === 'fulfilled' ? security.value : { count: 0, criticalCount: 0 });
|
||||
})
|
||||
.catch(() => {
|
||||
setPendingAudits(EMPTY_PENDING_AUDITS);
|
||||
setRetirementUnreadCount(0);
|
||||
setSecurityAlertSummary({ count: 0, criticalCount: 0 });
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -78,11 +81,13 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
|
||||
window.addEventListener('focus', onFocus);
|
||||
window.addEventListener('cmpp-audit-count-refresh', onAuditRefresh);
|
||||
window.addEventListener('cmpp-retirement-count-refresh', onAuditRefresh);
|
||||
window.addEventListener('cmpp-security-alert-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);
|
||||
window.removeEventListener('cmpp-security-alert-count-refresh', onAuditRefresh);
|
||||
};
|
||||
}, [loadPendingAuditCount, session.portal, sessionLocked]);
|
||||
|
||||
@@ -96,7 +101,10 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
|
||||
userName={session.user.displayName}
|
||||
userRole="平台管理员"
|
||||
onSessionLockedChange={setSessionLocked}
|
||||
retirementAlert={{ count: retirementUnreadCount, to: '/admin/signature-retirement' }}
|
||||
alertNotifications={[
|
||||
{ label: '签名清退预警', count: retirementUnreadCount, description: '今日未读且未抑制', to: '/admin/signature-retirement' },
|
||||
{ label: '安全检测与封禁', count: securityAlertSummary.count, description: securityAlertSummary.criticalCount > 0 ? `${securityAlertSummary.criticalCount} 条严重告警待处置` : '待处置安全告警', to: '/admin/security-detection' },
|
||||
]}
|
||||
auditNotifications={[
|
||||
{ label: '企业认证待审', count: pendingAudits.enterpriseCertifications, to: '/admin/enterprise-audit' },
|
||||
{ label: '短信审核待审', count: pendingAudits.smsAudits, to: '/admin/sms-audit' },
|
||||
|
||||
+40
-13
@@ -50,6 +50,10 @@ export type AuditNotificationItem = {
|
||||
to: string;
|
||||
};
|
||||
|
||||
export type AlertNotificationItem = AuditNotificationItem & {
|
||||
description: string;
|
||||
};
|
||||
|
||||
type AppShellProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
@@ -60,7 +64,7 @@ type AppShellProps = {
|
||||
userRole: string;
|
||||
navSections: ShellNavSection[];
|
||||
auditNotifications?: AuditNotificationItem[];
|
||||
retirementAlert?: { count: number; to: string };
|
||||
alertNotifications?: AlertNotificationItem[];
|
||||
onSessionLockedChange?: (locked: boolean) => void;
|
||||
};
|
||||
|
||||
@@ -73,7 +77,7 @@ export function AppShell({
|
||||
userRole,
|
||||
navSections,
|
||||
auditNotifications = [],
|
||||
retirementAlert,
|
||||
alertNotifications = [],
|
||||
onSessionLockedChange,
|
||||
}: AppShellProps) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
@@ -81,6 +85,7 @@ export function AppShell({
|
||||
const [closedSections, setClosedSections] = useState<Record<string, boolean>>({});
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const [noticeOpen, setNoticeOpen] = useState(false);
|
||||
const [alertNoticeOpen, setAlertNoticeOpen] = useState(false);
|
||||
const [passwordModalOpen, setPasswordModalOpen] = useState(false);
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
@@ -107,6 +112,10 @@ export function AppShell({
|
||||
() => auditNotifications.reduce((sum, item) => sum + item.count, 0),
|
||||
[auditNotifications],
|
||||
);
|
||||
const alertTotal = useMemo(
|
||||
() => alertNotifications.reduce((sum, item) => sum + item.count, 0),
|
||||
[alertNotifications],
|
||||
);
|
||||
|
||||
async function changeOwnPassword() {
|
||||
if (!currentPassword || newPassword.length < 6) {
|
||||
@@ -417,23 +426,41 @@ 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>
|
||||
{alertNotifications.length ? (
|
||||
<div className="notice-menu-wrap">
|
||||
<button
|
||||
aria-expanded={alertNoticeOpen}
|
||||
aria-haspopup="menu"
|
||||
aria-label="预警通知"
|
||||
className={['icon-button', alertTotal > 0 ? 'has-dot' : ''].filter(Boolean).join(' ')}
|
||||
onClick={() => { setAlertNoticeOpen((open) => !open); setNoticeOpen(false); }}
|
||||
type="button"
|
||||
>
|
||||
<Bell size={18} />
|
||||
{alertTotal > 0 ? <span className="notice-count">{alertTotal}</span> : null}
|
||||
</button>
|
||||
{alertNoticeOpen ? (
|
||||
<div className="notice-popover notice-popover--alerts" role="menu">
|
||||
<div className="notice-popover__header">
|
||||
<strong>预警中心</strong>
|
||||
<span className={alertTotal === 0 ? 'is-zero' : ''}>{alertTotal} 条</span>
|
||||
</div>
|
||||
{alertNotifications.map((item) => (
|
||||
<NavLink key={item.to} onClick={() => setAlertNoticeOpen(false)} role="menuitem" to={item.to}>
|
||||
<span className="notice-popover__copy"><b>{item.label}</b><small>{item.description}</small></span>
|
||||
<strong className={item.count === 0 ? 'is-zero' : ''}>{item.count}</strong>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="notice-menu-wrap">
|
||||
<button
|
||||
aria-expanded={noticeOpen}
|
||||
aria-haspopup="menu"
|
||||
className={['icon-button', auditTotal > 0 ? 'has-dot' : ''].filter(Boolean).join(' ')}
|
||||
onClick={() => setNoticeOpen((open) => !open)}
|
||||
onClick={() => { setNoticeOpen((open) => !open); setAlertNoticeOpen(false); }}
|
||||
type="button"
|
||||
aria-label="通知"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user