refactor: 前端应用骨架迁移至 zustand 并统一路由权限壳
This commit is contained in:
24
apps/admin/src/components/BrandLogo.tsx
Normal file
24
apps/admin/src/components/BrandLogo.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ReadOutlined } from '@ant-design/icons';
|
||||
|
||||
const BRAND_COLOR = '#7e14ff';
|
||||
|
||||
/** 全局品牌标识:登录页 / 侧边栏 / 页头统一使用 */
|
||||
export function BrandLogo({ size = 32 }: { size?: number }) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: 8,
|
||||
background: BRAND_COLOR,
|
||||
color: '#fff',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<ReadOutlined style={{ fontSize: size * 0.55 }} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { Navigate } from 'react-router';
|
||||
import { Result, Spin } from 'antd';
|
||||
import { usePermission } from '../hooks/usePermission';
|
||||
import { findRoleAwareLandingPath } from '../auth/menu-policy';
|
||||
@@ -7,10 +7,10 @@ import { useUserStore } from '../store/user/userStore';
|
||||
|
||||
const DefaultRoute: React.FC = () => {
|
||||
const { permissions, permissionsReady } = usePermission();
|
||||
const roles = useUserStore((state) => state.user?.roles ?? []);
|
||||
if (!permissionsReady) {
|
||||
return <Spin size="large" style={{ display: 'block', margin: '80px auto' }} />;
|
||||
}
|
||||
const roles = useUserStore((state) => state.user?.roles ?? []);
|
||||
const firstPath = findRoleAwareLandingPath(roles, permissions);
|
||||
if (firstPath) return <Navigate to={firstPath} replace />;
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Badge, Popover, Button, List, Typography, Empty } from 'antd';
|
||||
import { BellOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router';
|
||||
import dayjs from 'dayjs';
|
||||
import { useInterval } from 'usehooks-ts';
|
||||
import api from '../api';
|
||||
import { formatNotificationText, notificationTypeLabels } from '../utils/notification-display';
|
||||
import { useUserStore } from '../store/user/userStore';
|
||||
@@ -17,25 +19,19 @@ interface NotificationItem {
|
||||
}
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
const diff = Date.now() - new Date(dateStr).getTime();
|
||||
const mins = Math.floor(diff / 60000);
|
||||
if (mins < 1) return '刚刚';
|
||||
if (mins < 60) return `${mins}分钟前`;
|
||||
const hours = Math.floor(mins / 60);
|
||||
if (hours < 24) return `${hours}小时前`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}天前`;
|
||||
return dayjs(dateStr).fromNow();
|
||||
}
|
||||
|
||||
const NotificationBell: React.FC = () => {
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [sseDown, setSseDown] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetchNotifications = async () => {
|
||||
try {
|
||||
const data = (await api.get('/notifications?limit=20')) as unknown as NotificationItem[];
|
||||
const data = await api.get<NotificationItem[]>('/notifications?limit=20');
|
||||
setNotifications(data);
|
||||
} catch {
|
||||
/* ignore */
|
||||
@@ -44,7 +40,7 @@ const NotificationBell: React.FC = () => {
|
||||
|
||||
const fetchUnread = async () => {
|
||||
try {
|
||||
const data = (await api.get('/notifications/unread-count')) as unknown as { count: number };
|
||||
const data = await api.get<{ count: number }>('/notifications/unread-count');
|
||||
setUnreadCount(data.count);
|
||||
} catch {
|
||||
/* ignore */
|
||||
@@ -52,7 +48,9 @@ const NotificationBell: React.FC = () => {
|
||||
};
|
||||
const openRef = useRef(open);
|
||||
openRef.current = open;
|
||||
const retryRef = useRef<number | null>(null);
|
||||
useInterval(() => {
|
||||
void fetchUnread();
|
||||
}, sseDown ? 60_000 : null);
|
||||
|
||||
// SSE connection — decoupled from popover open state
|
||||
useEffect(() => {
|
||||
@@ -71,13 +69,11 @@ const NotificationBell: React.FC = () => {
|
||||
};
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
if (retryRef.current !== null) clearInterval(retryRef.current);
|
||||
retryRef.current = window.setInterval(fetchUnread, 60_000);
|
||||
setSseDown(true);
|
||||
};
|
||||
return () => {
|
||||
es.close();
|
||||
clearInterval(retryRef.current ?? undefined);
|
||||
retryRef.current = null;
|
||||
setSseDown(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Button } from 'antd';
|
||||
import type { ButtonProps } from 'antd';
|
||||
import { Button, type ButtonProps } from 'antd';
|
||||
import { usePermission } from '../hooks/usePermission';
|
||||
|
||||
interface PermissionButtonProps extends ButtonProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Result, Button, Spin } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { findRoleAwareLandingPath } from '../auth/menu-policy';
|
||||
import { usePermission } from '../hooks/usePermission';
|
||||
import { useUserStore } from '../store/user/userStore';
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
horizontalListSortingStrategy,
|
||||
@@ -8,9 +14,8 @@ import {
|
||||
useSortable,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Tabs } from 'antd';
|
||||
import type { TabsProps } from 'antd';
|
||||
import type { Location } from 'react-router-dom';
|
||||
import { Tabs, type TabsProps } from 'antd';
|
||||
import type { Location } from 'react-router';
|
||||
import type { AppMenuItem } from '../../auth/menu-policy';
|
||||
import { useAppStore } from '../../store';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { MemoryRouter, Route, Routes, useNavigate } from 'react-router-dom';
|
||||
import { MemoryRouter, Route, Routes, useNavigate } from 'react-router';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { RouteKeeper } from './RouteKeeper';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { useLocation, useOutlet } from 'react-router-dom';
|
||||
import { useLocation, useOutlet } from 'react-router';
|
||||
|
||||
const MAX_CACHED_PAGES = 30;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user