feat: refine admin forms, attendance and finance workflows
Squash merge PR #23. Included changes: - complete occupancy check-in required fields/default payload - improve responsive admin management pages - fix attendance edge cases and attendance period config - refine wallet/finance-related workflow handling Checks: - npm run typecheck -w apps/admin - npm run typecheck -w apps/server
This commit is contained in:
@@ -79,7 +79,10 @@ const MainLayout: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
api.get<{ id: number; username: string; permissions: string[]; roles?: string[] }>('/auth/profile')
|
||||
api
|
||||
.get<{ id: number; username: string; permissions: string[]; roles?: string[] }>(
|
||||
'/auth/profile',
|
||||
)
|
||||
.then((profile) => {
|
||||
if (cancelled) return;
|
||||
writePermissions(profile.permissions || []);
|
||||
@@ -91,13 +94,16 @@ const MainLayout: React.FC = () => {
|
||||
.catch(() => {
|
||||
// The API interceptor handles expired/invalid sessions.
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const screens = Grid.useBreakpoint();
|
||||
const isMobile = !screens.sm; // < 576px (仅 xs)
|
||||
const isMobile = !screens.sm; // < 576px (仅 xs)
|
||||
const isTablet = (screens.sm || screens.md) && !screens.lg; // 576-991px
|
||||
const isDesktop = !!screens.lg; // >= 992px
|
||||
const isDesktop = !!screens.lg; // >= 992px
|
||||
const usesDrawer = !isDesktop;
|
||||
|
||||
const menuItems = useMemo(
|
||||
() => buildMenu(user.roles ?? [], permissions),
|
||||
@@ -111,10 +117,13 @@ const MainLayout: React.FC = () => {
|
||||
navigate('/login');
|
||||
}, [navigate]);
|
||||
|
||||
const handleMenuClick = useCallback((key: string) => {
|
||||
navigate(key);
|
||||
if (isMobile) setDrawerOpen(false);
|
||||
}, [navigate, isMobile]);
|
||||
const handleMenuClick = useCallback(
|
||||
(key: string) => {
|
||||
navigate(key);
|
||||
if (usesDrawer) setDrawerOpen(false);
|
||||
},
|
||||
[navigate, usesDrawer],
|
||||
);
|
||||
|
||||
const findSelectedKeys = (items: AppMenuItem[], pathname: string): string[] => {
|
||||
for (const item of items) {
|
||||
@@ -130,7 +139,12 @@ const MainLayout: React.FC = () => {
|
||||
const findOpenKeys = (items: AppMenuItem[], pathname: string): string[] => {
|
||||
for (const item of items) {
|
||||
if (item.children) {
|
||||
if (item.children.some((c) => c.key === pathname || (c.children && c.children.some((gc) => gc.key === pathname)))) {
|
||||
if (
|
||||
item.children.some(
|
||||
(c) =>
|
||||
c.key === pathname || (c.children && c.children.some((gc) => gc.key === pathname)),
|
||||
)
|
||||
) {
|
||||
return [item.key];
|
||||
}
|
||||
}
|
||||
@@ -138,7 +152,10 @@ const MainLayout: React.FC = () => {
|
||||
return [];
|
||||
};
|
||||
|
||||
const selectedKeys = useMemo(() => findSelectedKeys(menuItems, location.pathname), [menuItems, location.pathname]);
|
||||
const selectedKeys = useMemo(
|
||||
() => findSelectedKeys(menuItems, location.pathname),
|
||||
[menuItems, location.pathname],
|
||||
);
|
||||
// 路径变化时同步展开的菜单(不干扰用户手动展开/收起)
|
||||
useEffect(() => {
|
||||
if (location.pathname !== prevPathname.current) {
|
||||
@@ -153,8 +170,6 @@ const MainLayout: React.FC = () => {
|
||||
setOpenKeys(latestKey ? [latestKey] : []);
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const transformToMenuItems = (items: AppMenuItem[]): any[] => {
|
||||
return items.map((item) => ({
|
||||
key: item.key,
|
||||
@@ -163,26 +178,29 @@ const MainLayout: React.FC = () => {
|
||||
children: item.children ? transformToMenuItems(item.children) : undefined,
|
||||
}));
|
||||
};
|
||||
const menuContent = useMemo(() => (
|
||||
<Menu
|
||||
theme="light"
|
||||
mode="inline"
|
||||
selectedKeys={selectedKeys}
|
||||
openKeys={openKeys}
|
||||
onOpenChange={handleOpenChange}
|
||||
items={transformToMenuItems(menuItems)}
|
||||
onClick={({ key }) => handleMenuClick(key)}
|
||||
style={{ border: 'none' }}
|
||||
/>
|
||||
), [selectedKeys, openKeys, menuItems, handleMenuClick]);
|
||||
const menuContent = useMemo(
|
||||
() => (
|
||||
<Menu
|
||||
theme="light"
|
||||
mode="inline"
|
||||
selectedKeys={selectedKeys}
|
||||
openKeys={openKeys}
|
||||
onOpenChange={handleOpenChange}
|
||||
items={transformToMenuItems(menuItems)}
|
||||
onClick={({ key }) => handleMenuClick(key)}
|
||||
style={{ border: 'none' }}
|
||||
/>
|
||||
),
|
||||
[selectedKeys, openKeys, menuItems, handleMenuClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout className="app-shell" style={{ minHeight: '100vh' }}>
|
||||
{!isMobile && (
|
||||
{isDesktop && (
|
||||
<Sider
|
||||
trigger={null}
|
||||
collapsible
|
||||
collapsed={isTablet ? true : collapsed}
|
||||
collapsed={collapsed}
|
||||
theme="light"
|
||||
className="app-sidebar"
|
||||
style={{ background: '#fff', borderRight: '1px solid #e5e5e7' }}
|
||||
@@ -194,17 +212,17 @@ const MainLayout: React.FC = () => {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#1d1d1f',
|
||||
fontSize: (isTablet || collapsed) ? 16 : 17,
|
||||
fontSize: collapsed ? 16 : 17,
|
||||
fontWeight: 600,
|
||||
borderBottom: '1px solid #e5e5e7',
|
||||
}}
|
||||
>
|
||||
{(isTablet || collapsed) ? '恭' : '恭学教育基地'}
|
||||
{collapsed ? '恭' : '恭学教育基地'}
|
||||
</div>
|
||||
{menuContent}
|
||||
</Sider>
|
||||
)}
|
||||
{isMobile && (
|
||||
{usesDrawer && (
|
||||
<Drawer
|
||||
placement="left"
|
||||
open={drawerOpen}
|
||||
@@ -232,9 +250,9 @@ const MainLayout: React.FC = () => {
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
aria-label={isMobile || isTablet ? '打开菜单' : collapsed ? '展开侧边栏' : '收起侧边栏'}
|
||||
aria-label={usesDrawer ? '打开菜单' : collapsed ? '展开侧边栏' : '收起侧边栏'}
|
||||
icon={
|
||||
isMobile || isTablet ? (
|
||||
usesDrawer ? (
|
||||
<MenuUnfoldOutlined />
|
||||
) : collapsed ? (
|
||||
<MenuUnfoldOutlined />
|
||||
@@ -242,7 +260,7 @@ const MainLayout: React.FC = () => {
|
||||
<MenuFoldOutlined />
|
||||
)
|
||||
}
|
||||
onClick={() => (isMobile || isTablet ? setDrawerOpen(true) : setCollapsed(!collapsed))}
|
||||
onClick={() => (usesDrawer ? setDrawerOpen(true) : setCollapsed(!collapsed))}
|
||||
/>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
{hasPermission('notification:view') && <NotificationBell />}
|
||||
|
||||
Reference in New Issue
Block a user