fix(web): 添加登录状态拦截和修复类型错误
- dashboard layout: 未登录时重定向到登录页,携带 redirect 参数 - sidebar: 移除静态菜单 fallback,仅使用动态权限菜单 - MenuEditDialog: 修复 type 字段类型为 MenuType - RoleEditDialog: 修复 isSystem 属性访问使用 roleDetail Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,26 +6,23 @@ import { useEffect } from 'react';
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { Sidebar, MobileSidebar } from '@/components/layout/Sidebar';
|
||||
import { getRoutePermission, isPublicRoute } from '@/config/route-permissions';
|
||||
import { getLoginUrl } from '@/lib/auth';
|
||||
import { getUserMenusAndPermissions } from '@/services/permission.service';
|
||||
import { useAuthStore, usePermissionStore } from '@/stores';
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { token, isHydrated: authHydrated } = useAuthStore();
|
||||
const { isLoaded, setPermissionData, hasPermission, clearPermissionData } = usePermissionStore();
|
||||
|
||||
// 加载用户权限数据
|
||||
// 登录状态检查:未登录时重定向到登录页
|
||||
useEffect(() => {
|
||||
if (!authHydrated) return;
|
||||
|
||||
// 未登录时清空权限数据
|
||||
if (!token) {
|
||||
clearPermissionData();
|
||||
router.replace(getLoginUrl(pathname));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,7 +38,7 @@ export default function DashboardLayout({
|
||||
setPermissionData({ menus: [], permissions: [], isSuperAdmin: false });
|
||||
});
|
||||
}
|
||||
}, [authHydrated, token, isLoaded, setPermissionData, clearPermissionData]);
|
||||
}, [authHydrated, token, isLoaded, pathname, router, setPermissionData, clearPermissionData]);
|
||||
|
||||
// 路由权限检查
|
||||
useEffect(() => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import { Button } from '@/components/ui/button';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Sheet, SheetContent, SheetTitle } from '@/components/ui/sheet';
|
||||
import { sidebarNav } from '@/config/nav';
|
||||
import { siteConfig } from '@/config/site';
|
||||
import { getIcon } from '@/lib/icons';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -182,28 +181,12 @@ function SidebarContent({ collapsed = false, onItemClick }: SidebarContentProps)
|
||||
const pathname = usePathname();
|
||||
const { menus, isLoaded } = usePermissionStore();
|
||||
|
||||
// 如果权限数据已加载且有菜单,使用动态菜单
|
||||
// 否则使用静态菜单作为 fallback
|
||||
// 只使用动态菜单,未加载时返回空数组
|
||||
const menuItems = useMemo(() => {
|
||||
if (isLoaded && menus.length > 0) {
|
||||
return menus;
|
||||
if (!isLoaded) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 将静态导航转换为 MenuTreeNode 格式
|
||||
return sidebarNav.flatMap((group) =>
|
||||
group.items.map((item) => ({
|
||||
id: item.href,
|
||||
parentId: null,
|
||||
code: item.href,
|
||||
name: item.title,
|
||||
type: 'menu' as const,
|
||||
path: item.href,
|
||||
icon: item.icon.displayName || null,
|
||||
permission: null,
|
||||
isHidden: false,
|
||||
meta: null,
|
||||
}))
|
||||
);
|
||||
return menus;
|
||||
}, [isLoaded, menus]);
|
||||
|
||||
// 将动态菜单按分组渲染(如果有 parentId 则是子菜单)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { MenuResponse, MenuTreeNode } from '@seclusion/shared';
|
||||
import { MenuType } from '@seclusion/shared';
|
||||
import { useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -48,7 +48,7 @@ interface MenuFormValues {
|
||||
parentId: string;
|
||||
code: string;
|
||||
name: string;
|
||||
type: string;
|
||||
type: MenuType;
|
||||
path: string;
|
||||
icon: string;
|
||||
permission: string;
|
||||
@@ -67,7 +67,8 @@ function renderMenuOptions(menus: MenuTreeNode[], level = 0): React.ReactNode[]
|
||||
const prefix = '\u00A0\u00A0'.repeat(level);
|
||||
options.push(
|
||||
<SelectItem key={menu.id} value={menu.id}>
|
||||
{prefix}{menu.name}
|
||||
{prefix}
|
||||
{menu.name}
|
||||
</SelectItem>
|
||||
);
|
||||
|
||||
@@ -173,9 +174,7 @@ export function MenuEditDialog({ menu, open, onOpenChange }: MenuEditDialogProps
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEditing ? '编辑菜单' : '新建菜单'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEditing ? '修改菜单配置。' : '创建新的菜单项。'}
|
||||
</DialogDescription>
|
||||
<DialogDescription>{isEditing ? '修改菜单配置。' : '创建新的菜单项。'}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
@@ -221,11 +220,7 @@ export function MenuEditDialog({ menu, open, onOpenChange }: MenuEditDialogProps
|
||||
<FormItem>
|
||||
<FormLabel>菜单编码</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="如:user-management"
|
||||
{...field}
|
||||
disabled={isEditing}
|
||||
/>
|
||||
<Input placeholder="如:user-management" {...field} disabled={isEditing} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -317,7 +312,15 @@ export function MenuEditDialog({ menu, open, onOpenChange }: MenuEditDialogProps
|
||||
<Input placeholder="Lucide 图标名,如:Users" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
参考 <a href="https://lucide.dev/icons" target="_blank" rel="noreferrer" className="text-primary hover:underline">Lucide Icons</a>
|
||||
参考{' '}
|
||||
<a
|
||||
href="https://lucide.dev/icons"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Lucide Icons
|
||||
</a>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -335,9 +338,7 @@ export function MenuEditDialog({ menu, open, onOpenChange }: MenuEditDialogProps
|
||||
<FormControl>
|
||||
<Input placeholder="如:user:read(可选)" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
关联的权限编码,格式为 资源:操作
|
||||
</FormDescription>
|
||||
<FormDescription>关联的权限编码,格式为 资源:操作</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -350,12 +351,9 @@ export function MenuEditDialog({ menu, open, onOpenChange }: MenuEditDialogProps
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center gap-2">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">隐藏菜单</FormLabel>
|
||||
<FormLabel className="mt-0!">隐藏菜单</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -366,12 +364,9 @@ export function MenuEditDialog({ menu, open, onOpenChange }: MenuEditDialogProps
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center gap-2">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<FormLabel className="!mt-0">启用</FormLabel>
|
||||
<FormLabel className="mt-0!">启用</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -184,7 +184,7 @@ export function RoleEditDialog({ role, open, onOpenChange }: RoleEditDialogProps
|
||||
<Input
|
||||
placeholder="如:manager"
|
||||
{...field}
|
||||
disabled={isEditing || role?.isSystem}
|
||||
disabled={isEditing || roleDetail?.isSystem}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>唯一标识,创建后不可修改</FormDescription>
|
||||
|
||||
Reference in New Issue
Block a user