feat: improve application access and money precision

This commit is contained in:
hectorzhao
2026-07-16 17:54:05 +08:00
parent 9d5c507007
commit faa716b8d0
49 changed files with 1699 additions and 489 deletions
+45 -9
View File
@@ -7,10 +7,12 @@ import {
CircleHelp,
KeyRound,
LogOut,
Menu,
PanelLeftClose,
PanelLeftOpen,
X,
} from 'lucide-react';
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { adminApi } from '@/api/adminApi';
import {
clearSession,
@@ -63,6 +65,7 @@ export function AppShell({
auditNotifications = [],
}: AppShellProps) {
const [collapsed, setCollapsed] = useState(false);
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const [closedSections, setClosedSections] = useState<Record<string, boolean>>({});
const [userMenuOpen, setUserMenuOpen] = useState(false);
const [noticeOpen, setNoticeOpen] = useState(false);
@@ -85,6 +88,7 @@ export function AppShell({
const reauthenticationReject = useRef<((error: Error) => void) | null>(null);
const lockRequested = useRef(false);
const navigate = useNavigate();
const location = useLocation();
const ToggleIcon = collapsed ? PanelLeftOpen : PanelLeftClose;
const auditTotal = useMemo(
() => auditNotifications.reduce((sum, item) => sum + item.count, 0),
@@ -180,6 +184,19 @@ export function AppShell({
}
}
useEffect(() => {
setMobileNavOpen(false);
}, [location.pathname]);
useEffect(() => {
if (!mobileNavOpen) return;
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') setMobileNavOpen(false);
};
window.addEventListener('keydown', closeOnEscape);
return () => window.removeEventListener('keydown', closeOnEscape);
}, [mobileNavOpen]);
useEffect(() => {
const activityEvents = ['pointerdown', 'keydown', 'touchstart', 'scroll'] as const;
const onActivity = () => markUserActivity();
@@ -263,11 +280,16 @@ export function AppShell({
}, [auditTotal, title]);
return (
<div className={['app-shell', collapsed ? 'app-shell--collapsed' : ''].filter(Boolean).join(' ')}>
<aside className="sidebar">
<div className="brand-block">
<img alt={`${title} logo`} className="brand-logo brand-logo--full" src="/logo/logo1.png" />
<img alt={`${title} logo`} className="brand-logo brand-logo--compact" src="/logo/logo2.png" />
<div className={['app-shell', collapsed ? 'app-shell--collapsed' : '', mobileNavOpen ? 'app-shell--mobile-nav-open' : ''].filter(Boolean).join(' ')}>
<aside aria-label="应用导航" className={['sidebar', mobileNavOpen ? 'sidebar--mobile-open' : ''].filter(Boolean).join(' ')}>
<div className="sidebar-brand-row">
<div className="brand-block">
<img alt={`${title} logo`} className="brand-logo brand-logo--full" src="/logo/logo1.png" />
<img alt={`${title} logo`} className="brand-logo brand-logo--compact" src="/logo/logo2.png" />
</div>
<button aria-label="关闭导航" className="icon-button mobile-nav-close" onClick={() => setMobileNavOpen(false)} type="button">
<X size={20} />
</button>
</div>
<nav className="side-nav" aria-label="主导航">
@@ -296,7 +318,7 @@ export function AppShell({
const Icon = item.icon;
return (
<NavLink key={item.to} to={item.to} end title={item.pending ? `${item.label}(待开发)` : item.label}>
<NavLink key={item.to} onClick={() => setMobileNavOpen(false)} to={item.to} end title={item.pending ? `${item.label}(待开发)` : item.label}>
<Icon size={17} strokeWidth={2.1} />
<span className="side-nav-label">
<span className="side-nav-label-text">{item.label}</span>
@@ -316,21 +338,35 @@ export function AppShell({
</div>
</aside>
{mobileNavOpen ? <button aria-label="关闭导航遮罩" className="mobile-nav-backdrop" onClick={() => setMobileNavOpen(false)} type="button" /> : null}
<main className="main-area">
<header className="topbar">
<div className="topbar-left">
<button
className="icon-button"
className="icon-button desktop-nav-toggle"
onClick={() => setCollapsed((value) => !value)}
type="button"
aria-label={collapsed ? '展开导航' : '收起导航'}
>
<ToggleIcon size={18} />
</button>
<button
aria-expanded={mobileNavOpen}
aria-label={mobileNavOpen ? '关闭导航' : '打开导航'}
className="icon-button mobile-nav-toggle"
onClick={() => setMobileNavOpen((open) => !open)}
type="button"
>
<Menu size={20} />
</button>
<div className="mobile-topbar-brand">
<img alt={`${title} logo`} src="/logo/logo1.png" />
</div>
</div>
<div className="topbar-actions">
<button className="icon-button" type="button" aria-label="帮助中心">
<button className="icon-button topbar-help" type="button" aria-label="帮助中心">
<CircleHelp size={18} />
</button>
<div className="notice-menu-wrap">