feat: wire admin workflows to real APIs
This commit is contained in:
+87
-12
@@ -1,10 +1,12 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Bell,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircleHelp,
|
||||
KeyRound,
|
||||
LogOut,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
Search,
|
||||
@@ -25,6 +27,12 @@ export type ShellNavSection = {
|
||||
items: ShellNavItem[];
|
||||
};
|
||||
|
||||
export type AuditNotificationItem = {
|
||||
label: string;
|
||||
count: number;
|
||||
to: string;
|
||||
};
|
||||
|
||||
type AppShellProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
@@ -32,6 +40,7 @@ type AppShellProps = {
|
||||
userName: string;
|
||||
userRole: string;
|
||||
navSections: ShellNavSection[];
|
||||
auditNotifications?: AuditNotificationItem[];
|
||||
};
|
||||
|
||||
export function AppShell({
|
||||
@@ -41,10 +50,32 @@ export function AppShell({
|
||||
userName,
|
||||
userRole,
|
||||
navSections,
|
||||
auditNotifications = [],
|
||||
}: AppShellProps) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [closedSections, setClosedSections] = useState<Record<string, boolean>>({});
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const [noticeOpen, setNoticeOpen] = useState(false);
|
||||
const ToggleIcon = collapsed ? PanelLeftOpen : PanelLeftClose;
|
||||
const auditTotal = useMemo(
|
||||
() => auditNotifications.reduce((sum, item) => sum + item.count, 0),
|
||||
[auditNotifications],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (auditTotal <= 0 || typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const alertedKey = `cmpp-audit-alert-${title}`;
|
||||
if (window.sessionStorage.getItem(alertedKey)) {
|
||||
return;
|
||||
}
|
||||
window.sessionStorage.setItem(alertedKey, '1');
|
||||
const timer = window.setTimeout(() => {
|
||||
window.alert(`有新的审核任务进入待办,共 ${auditTotal} 条。`);
|
||||
}, 1200);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [auditTotal, title]);
|
||||
|
||||
return (
|
||||
<div className={['app-shell', collapsed ? 'app-shell--collapsed' : ''].filter(Boolean).join(' ')}>
|
||||
@@ -126,17 +157,61 @@ export function AppShell({
|
||||
<button className="icon-button" type="button" aria-label="帮助中心">
|
||||
<CircleHelp size={18} />
|
||||
</button>
|
||||
<button className="icon-button has-dot" type="button" aria-label="通知">
|
||||
<Bell size={18} />
|
||||
</button>
|
||||
<button className="user-menu" type="button">
|
||||
<span className="user-avatar">{userName.slice(0, 1)}</span>
|
||||
<span>
|
||||
<strong>{userName}</strong>
|
||||
<small>{userRole}</small>
|
||||
</span>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
<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)}
|
||||
type="button"
|
||||
aria-label="通知"
|
||||
>
|
||||
<Bell size={18} />
|
||||
{auditTotal > 0 ? <span className="notice-count">{auditTotal}</span> : null}
|
||||
</button>
|
||||
{noticeOpen ? (
|
||||
<div className="notice-popover" role="menu">
|
||||
<div className="notice-popover__header">
|
||||
<strong>待审核任务</strong>
|
||||
<span>{auditTotal} 条</span>
|
||||
</div>
|
||||
{auditNotifications.length ? auditNotifications.map((item) => (
|
||||
<NavLink key={item.to} onClick={() => setNoticeOpen(false)} role="menuitem" to={item.to}>
|
||||
<span>{item.label}</span>
|
||||
<strong>{item.count}</strong>
|
||||
</NavLink>
|
||||
)) : <p>暂无待审核任务</p>}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="user-menu-wrap">
|
||||
<button
|
||||
aria-expanded={userMenuOpen}
|
||||
aria-haspopup="menu"
|
||||
className="user-menu"
|
||||
onClick={() => setUserMenuOpen((open) => !open)}
|
||||
type="button"
|
||||
>
|
||||
<span className="user-avatar">{userName.slice(0, 1)}</span>
|
||||
<span>
|
||||
<strong>{userName}</strong>
|
||||
<small>{userRole}</small>
|
||||
</span>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
{userMenuOpen ? (
|
||||
<div className="user-menu-popover" role="menu">
|
||||
<button onClick={() => setUserMenuOpen(false)} role="menuitem" type="button">
|
||||
<KeyRound size={16} />
|
||||
修改密码
|
||||
</button>
|
||||
<button onClick={() => setUserMenuOpen(false)} role="menuitem" type="button">
|
||||
<LogOut size={16} />
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user