重构前端逻辑
This commit is contained in:
parent
161978b1be
commit
13ae354929
@ -1,17 +1,18 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
|
DialogBody,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Input } from '@/components/ui/input';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { Loader2, Tag as TagIcon } from 'lucide-react';
|
import { Loader2, Tag as TagIcon, Search, CheckCircle2 } from 'lucide-react';
|
||||||
import type { RoleTagResponse } from '../types';
|
import type { RoleTagResponse } from '../types';
|
||||||
import { getAllTags, assignTags } from '../service';
|
import { getAllTags, assignTags } from '../service';
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ const AssignTagDialog: React.FC<AssignTagDialogProps> = ({
|
|||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [allTags, setAllTags] = useState<RoleTagResponse[]>([]);
|
const [allTags, setAllTags] = useState<RoleTagResponse[]>([]);
|
||||||
const [selectedTagIds, setSelectedTagIds] = useState<number[]>([]);
|
const [selectedTagIds, setSelectedTagIds] = useState<number[]>([]);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@ -92,59 +94,122 @@ const AssignTagDialog: React.FC<AssignTagDialogProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 过滤标签
|
||||||
|
const filteredTags = useMemo(() => {
|
||||||
|
if (!searchText.trim()) return allTags;
|
||||||
|
const searchLower = searchText.toLowerCase();
|
||||||
|
return allTags.filter(tag =>
|
||||||
|
tag.name.toLowerCase().includes(searchLower) ||
|
||||||
|
tag.description?.toLowerCase().includes(searchLower)
|
||||||
|
);
|
||||||
|
}, [allTags, searchText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-[500px]">
|
<DialogContent className="sm:max-w-[580px]">
|
||||||
<DialogHeader>
|
<DialogHeader className="space-y-3">
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2 text-lg">
|
||||||
<TagIcon className="h-5 w-5" /> 分配标签
|
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-gradient-to-br from-purple-500 to-pink-500">
|
||||||
|
<TagIcon className="h-5 w-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-semibold">分配标签</div>
|
||||||
|
<div className="text-sm font-normal text-muted-foreground mt-0.5">
|
||||||
|
为角色选择适合的标签分类
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="px-6 py-4">
|
|
||||||
|
<DialogBody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||||
<span className="text-sm text-muted-foreground">加载中...</span>
|
<span className="ml-3 text-sm text-muted-foreground">加载标签中...</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="space-y-4">
|
||||||
<Label className="mb-3 block">选择标签</Label>
|
{/* 搜索框 */}
|
||||||
<div className="space-y-3 max-h-[300px] overflow-y-auto border rounded-md p-4">
|
{allTags.length > 0 && (
|
||||||
{allTags.length > 0 ? (
|
<div className="relative">
|
||||||
allTags.map(tag => (
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<div key={tag.id} className="flex items-center space-x-3">
|
<Input
|
||||||
|
placeholder="搜索标签名称或描述..."
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
className="pl-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 统计信息 */}
|
||||||
|
<div className="flex items-center justify-between px-1 py-2 rounded-lg bg-muted/50">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
共 {filteredTags.length} 个标签
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
已选 {selectedTagIds.length} 个
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标签列表 */}
|
||||||
|
<div className="space-y-2 max-h-[360px] overflow-y-auto pr-2 -mr-2">
|
||||||
|
{filteredTags.length > 0 ? (
|
||||||
|
filteredTags.map(tag => (
|
||||||
|
<div
|
||||||
|
key={tag.id}
|
||||||
|
className="flex items-center gap-3 p-3 rounded-lg border bg-card hover:bg-accent/50 transition-colors cursor-pointer group"
|
||||||
|
onClick={() => handleToggle(tag.id)}
|
||||||
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={`tag-${tag.id}`}
|
id={`tag-${tag.id}`}
|
||||||
checked={selectedTagIds.includes(tag.id)}
|
checked={selectedTagIds.includes(tag.id)}
|
||||||
onCheckedChange={() => handleToggle(tag.id)}
|
onCheckedChange={() => handleToggle(tag.id)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
<label
|
<div className="flex-1 min-w-0">
|
||||||
htmlFor={`tag-${tag.id}`}
|
<div className="flex items-center gap-2 mb-1">
|
||||||
className="flex items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer flex-1"
|
|
||||||
>
|
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="secondary"
|
||||||
style={{ backgroundColor: tag.color, color: '#fff' }}
|
className="font-medium"
|
||||||
|
style={{
|
||||||
|
backgroundColor: tag.color + '20',
|
||||||
|
color: tag.color,
|
||||||
|
borderColor: tag.color + '40'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
</div>
|
||||||
{tag.description && (
|
{tag.description && (
|
||||||
<span className="text-muted-foreground text-xs">
|
<p className="text-xs text-muted-foreground line-clamp-1">
|
||||||
{tag.description}
|
{tag.description}
|
||||||
</span>
|
</p>
|
||||||
)}
|
)}
|
||||||
</label>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
) : searchText ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
|
<Search className="w-12 h-12 mb-3 opacity-20" />
|
||||||
|
<p className="text-sm">未找到匹配的标签</p>
|
||||||
|
<p className="text-xs mt-1">请尝试其他搜索词</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-muted-foreground text-center py-4">
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||||
暂无可用标签
|
<TagIcon className="w-12 h-12 mb-3 opacity-20" />
|
||||||
|
<p className="text-sm">暂无可用标签</p>
|
||||||
|
<p className="text-xs mt-1">请先创建标签</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogBody>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -153,7 +218,12 @@ const AssignTagDialog: React.FC<AssignTagDialogProps> = ({
|
|||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSubmit} disabled={submitting || loading}>
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={submitting || loading}
|
||||||
|
className="min-w-[100px]"
|
||||||
|
>
|
||||||
|
{submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
{submitting ? '保存中...' : '确定'}
|
{submitting ? '保存中...' : '确定'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@ -16,8 +16,9 @@ import { getPermissionTree } from '../service';
|
|||||||
interface PermissionDialogProps {
|
interface PermissionDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onConfirm: (permissionIds: number[]) => Promise<void>;
|
onConfirm: (menuIds: number[], permissionIds: number[]) => Promise<void>;
|
||||||
defaultCheckedKeys?: number[];
|
defaultMenuIds?: number[];
|
||||||
|
defaultPermissionIds?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Permission {
|
interface Permission {
|
||||||
@ -40,17 +41,30 @@ interface MenuItem {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 权限分配对话框(使用 Accordion 实现树形结构)
|
* 权限分配对话框(使用 Accordion 实现树形结构)
|
||||||
|
*
|
||||||
|
* 级联选择规则(与后端一致):
|
||||||
|
* 1. ✅ 勾选权限 → 自动勾选所属菜单及所有父菜单
|
||||||
|
* 2. ✅ 勾选菜单 → 自动勾选该菜单、所有子菜单、所有父菜单
|
||||||
|
* 3. ✅ 取消菜单 → 自动取消该菜单及所有子菜单的菜单ID和权限ID
|
||||||
|
* 4. ✅ 半选状态 → 菜单部分权限被选中时显示半选状态
|
||||||
|
*
|
||||||
|
* 示例:
|
||||||
|
* - 勾选"用户管理"权限 → 自动勾选"用户管理"菜单 + "系统管理"父菜单
|
||||||
|
* - 勾选"用户管理"菜单 → 自动勾选"系统管理"父菜单 + 所有子菜单和权限
|
||||||
|
* - 提交时同时包含菜单ID和权限ID,后端有兜底逻辑确保数据完整性
|
||||||
*/
|
*/
|
||||||
const PermissionDialog: React.FC<PermissionDialogProps> = ({
|
const PermissionDialog: React.FC<PermissionDialogProps> = ({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
defaultCheckedKeys = []
|
defaultMenuIds = [],
|
||||||
|
defaultPermissionIds = []
|
||||||
}) => {
|
}) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [menuTree, setMenuTree] = useState<MenuItem[]>([]);
|
const [menuTree, setMenuTree] = useState<MenuItem[]>([]);
|
||||||
|
const [checkedMenuIds, setCheckedMenuIds] = useState<number[]>([]);
|
||||||
const [checkedPermissionIds, setCheckedPermissionIds] = useState<number[]>([]);
|
const [checkedPermissionIds, setCheckedPermissionIds] = useState<number[]>([]);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
|
|
||||||
@ -62,7 +76,8 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
|
|||||||
try {
|
try {
|
||||||
const response = await getPermissionTree();
|
const response = await getPermissionTree();
|
||||||
setMenuTree(response);
|
setMenuTree(response);
|
||||||
setCheckedPermissionIds(defaultCheckedKeys);
|
setCheckedMenuIds(defaultMenuIds);
|
||||||
|
setCheckedPermissionIds(defaultPermissionIds);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取权限树失败:', error);
|
console.error('获取权限树失败:', error);
|
||||||
toast({
|
toast({
|
||||||
@ -76,17 +91,89 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
loadPermissionTree();
|
loadPermissionTree();
|
||||||
}, [open, defaultCheckedKeys]);
|
}, [open, defaultMenuIds, defaultPermissionIds]);
|
||||||
|
|
||||||
// 切换权限选中状态
|
// 获取某个菜单的所有父菜单ID(递归向上查找)
|
||||||
const handleTogglePermission = (permissionId: number) => {
|
const getParentMenuIds = (targetMenuId: number, menus: MenuItem[], parentIds: number[] = []): number[] => {
|
||||||
setCheckedPermissionIds(prev =>
|
for (const menu of menus) {
|
||||||
prev.includes(permissionId)
|
if (menu.id === targetMenuId) {
|
||||||
? prev.filter(id => id !== permissionId)
|
return parentIds;
|
||||||
: [...prev, permissionId]
|
}
|
||||||
|
if (menu.permissionChildren && menu.permissionChildren.length > 0) {
|
||||||
|
const found = menu.permissionChildren.some(child => child.id === targetMenuId);
|
||||||
|
if (found) {
|
||||||
|
return [...parentIds, menu.id];
|
||||||
|
}
|
||||||
|
const result = getParentMenuIds(targetMenuId, menu.permissionChildren, [...parentIds, menu.id]);
|
||||||
|
if (result.length > 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据权限ID找到它所属的菜单
|
||||||
|
const getMenuByPermissionId = (permissionId: number, menus: MenuItem[]): MenuItem | null => {
|
||||||
|
for (const menu of menus) {
|
||||||
|
if (menu.permissions?.some(p => p.id === permissionId)) {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
if (menu.permissionChildren && menu.permissionChildren.length > 0) {
|
||||||
|
const result = getMenuByPermissionId(permissionId, menu.permissionChildren);
|
||||||
|
if (result) return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换菜单选中状态
|
||||||
|
const handleToggleMenuId = (menuId: number) => {
|
||||||
|
setCheckedMenuIds(prev =>
|
||||||
|
prev.includes(menuId)
|
||||||
|
? prev.filter(id => id !== menuId)
|
||||||
|
: [...prev, menuId]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 切换权限选中状态(带级联)
|
||||||
|
const handleTogglePermission = (permissionId: number) => {
|
||||||
|
setCheckedPermissionIds(prev => {
|
||||||
|
if (prev.includes(permissionId)) {
|
||||||
|
// 取消勾选权限
|
||||||
|
return prev.filter(id => id !== permissionId);
|
||||||
|
} else {
|
||||||
|
// 勾选权限:自动勾选所属菜单及其所有父菜单
|
||||||
|
const menu = getMenuByPermissionId(permissionId, menuTree);
|
||||||
|
if (menu) {
|
||||||
|
const parentIds = getParentMenuIds(menu.id, menuTree);
|
||||||
|
const allParentMenuIds = [...parentIds, menu.id];
|
||||||
|
|
||||||
|
// 添加所有父菜单ID
|
||||||
|
setCheckedMenuIds(prevMenuIds => {
|
||||||
|
const newMenuIds = new Set(prevMenuIds);
|
||||||
|
allParentMenuIds.forEach(id => newMenuIds.add(id));
|
||||||
|
return Array.from(newMenuIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加权限ID
|
||||||
|
return [...prev, permissionId];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 递归获取菜单下所有子菜单ID(包括自己)
|
||||||
|
const getAllMenuIds = (menu: MenuItem): number[] => {
|
||||||
|
const ids: number[] = [menu.id];
|
||||||
|
if (menu.permissionChildren) {
|
||||||
|
menu.permissionChildren.forEach(child => {
|
||||||
|
ids.push(...getAllMenuIds(child));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
};
|
||||||
|
|
||||||
// 递归获取菜单下所有权限ID
|
// 递归获取菜单下所有权限ID
|
||||||
const getAllPermissionIds = (menu: MenuItem): number[] => {
|
const getAllPermissionIds = (menu: MenuItem): number[] => {
|
||||||
const ids: number[] = [];
|
const ids: number[] = [];
|
||||||
@ -101,37 +188,64 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
|
|||||||
return ids;
|
return ids;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查菜单下所有权限是否全选
|
// 检查菜单是否应该显示为选中状态
|
||||||
|
const isMenuChecked = (menu: MenuItem): boolean => {
|
||||||
|
// 如果菜单ID在checkedMenuIds中,直接返回true
|
||||||
|
if (checkedMenuIds.includes(menu.id)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 否则检查菜单下所有权限是否都被选中
|
||||||
|
const allPermissionIds = getAllPermissionIds(menu);
|
||||||
|
return allPermissionIds.length > 0 && allPermissionIds.every(id => checkedPermissionIds.includes(id));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查菜单下所有权限是否全选(用于判断取消逻辑)
|
||||||
const isMenuFullyChecked = (menu: MenuItem): boolean => {
|
const isMenuFullyChecked = (menu: MenuItem): boolean => {
|
||||||
const allIds = getAllPermissionIds(menu);
|
const allIds = getAllPermissionIds(menu);
|
||||||
return allIds.length > 0 && allIds.every(id => checkedPermissionIds.includes(id));
|
return allIds.length > 0 && allIds.every(id => checkedPermissionIds.includes(id));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查菜单下是否有部分权限选中
|
// 检查菜单下是否有部分权限选中(用于半选状态)
|
||||||
const isMenuPartiallyChecked = (menu: MenuItem): boolean => {
|
const isMenuPartiallyChecked = (menu: MenuItem): boolean => {
|
||||||
|
// 如果菜单ID在checkedMenuIds中,不显示半选
|
||||||
|
if (checkedMenuIds.includes(menu.id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 检查是否有部分权限被选中
|
||||||
const allIds = getAllPermissionIds(menu);
|
const allIds = getAllPermissionIds(menu);
|
||||||
const checkedCount = allIds.filter(id => checkedPermissionIds.includes(id)).length;
|
const checkedCount = allIds.filter(id => checkedPermissionIds.includes(id)).length;
|
||||||
return checkedCount > 0 && checkedCount < allIds.length;
|
return checkedCount > 0 && checkedCount < allIds.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换菜单的全选/取消全选
|
// 切换菜单的全选/取消全选(带级联)
|
||||||
const handleToggleMenu = (menu: MenuItem) => {
|
const handleToggleMenu = (menu: MenuItem) => {
|
||||||
const allIds = getAllPermissionIds(menu);
|
const allPermissionIds = getAllPermissionIds(menu);
|
||||||
const isFullyChecked = isMenuFullyChecked(menu);
|
const allMenuIds = getAllMenuIds(menu);
|
||||||
|
// 使用 isMenuChecked 来判断当前状态
|
||||||
|
const currentChecked = isMenuChecked(menu);
|
||||||
|
|
||||||
if (isFullyChecked) {
|
if (currentChecked) {
|
||||||
// 取消全选
|
// 取消全选:移除菜单ID和权限ID
|
||||||
setCheckedPermissionIds(prev => prev.filter(id => !allIds.includes(id)));
|
setCheckedMenuIds(prev => prev.filter(id => !allMenuIds.includes(id)));
|
||||||
|
setCheckedPermissionIds(prev => prev.filter(id => !allPermissionIds.includes(id)));
|
||||||
} else {
|
} else {
|
||||||
// 全选
|
// 全选:添加菜单ID、权限ID,并自动勾选所有父菜单
|
||||||
setCheckedPermissionIds(prev => {
|
const parentIds = getParentMenuIds(menu.id, menuTree);
|
||||||
const newIds = [...prev];
|
|
||||||
allIds.forEach(id => {
|
setCheckedMenuIds(prev => {
|
||||||
if (!newIds.includes(id)) {
|
const newIds = new Set(prev);
|
||||||
newIds.push(id);
|
// 添加当前菜单及所有子菜单ID
|
||||||
}
|
allMenuIds.forEach(id => newIds.add(id));
|
||||||
|
// 添加所有父菜单ID
|
||||||
|
parentIds.forEach(id => newIds.add(id));
|
||||||
|
return Array.from(newIds);
|
||||||
});
|
});
|
||||||
return newIds;
|
|
||||||
|
setCheckedPermissionIds(prev => {
|
||||||
|
const newIds = new Set(prev);
|
||||||
|
// 添加当前菜单及所有子菜单的权限ID
|
||||||
|
allPermissionIds.forEach(id => newIds.add(id));
|
||||||
|
return Array.from(newIds);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -179,7 +293,7 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
|
|||||||
const renderMenuItem = (menu: MenuItem, level: number = 0): React.ReactNode => {
|
const renderMenuItem = (menu: MenuItem, level: number = 0): React.ReactNode => {
|
||||||
const hasChildren = menu.permissionChildren && menu.permissionChildren.length > 0;
|
const hasChildren = menu.permissionChildren && menu.permissionChildren.length > 0;
|
||||||
const hasPermissions = menu.permissions && menu.permissions.length > 0;
|
const hasPermissions = menu.permissions && menu.permissions.length > 0;
|
||||||
const isChecked = isMenuFullyChecked(menu);
|
const isChecked = isMenuChecked(menu);
|
||||||
const isPartial = isMenuPartiallyChecked(menu);
|
const isPartial = isMenuPartiallyChecked(menu);
|
||||||
|
|
||||||
if (!hasChildren && !hasPermissions) {
|
if (!hasChildren && !hasPermissions) {
|
||||||
@ -281,7 +395,7 @@ const PermissionDialog: React.FC<PermissionDialogProps> = ({
|
|||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await onConfirm(checkedPermissionIds);
|
await onConfirm(checkedMenuIds, checkedPermissionIds);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('分配权限失败:', error);
|
console.error('分配权限失败:', error);
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
ShieldCheck, Settings, Shield
|
ShieldCheck, Settings, Shield
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { getRoleList, deleteRole, getRolePermissions, assignPermissions } from './service';
|
import { getRoleList, deleteRole, getRoleMenusAndPermissions, assignMenusAndPermissions } from './service';
|
||||||
import type { RoleResponse, RoleQuery } from './types';
|
import type { RoleResponse, RoleQuery } from './types';
|
||||||
import type { Page } from '@/types/base';
|
import type { Page } from '@/types/base';
|
||||||
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||||
@ -36,7 +36,8 @@ const RolePage: React.FC = () => {
|
|||||||
const [tagDialogOpen, setTagDialogOpen] = useState(false);
|
const [tagDialogOpen, setTagDialogOpen] = useState(false);
|
||||||
const [assignTagDialogOpen, setAssignTagDialogOpen] = useState(false);
|
const [assignTagDialogOpen, setAssignTagDialogOpen] = useState(false);
|
||||||
const [selectedRole, setSelectedRole] = useState<RoleResponse | null>(null);
|
const [selectedRole, setSelectedRole] = useState<RoleResponse | null>(null);
|
||||||
const [defaultCheckedKeys, setDefaultCheckedKeys] = useState<number[]>([]);
|
const [defaultMenuIds, setDefaultMenuIds] = useState<number[]>([]);
|
||||||
|
const [defaultPermissionIds, setDefaultPermissionIds] = useState<number[]>([]);
|
||||||
const [query, setQuery] = useState<RoleQuery>({
|
const [query, setQuery] = useState<RoleQuery>({
|
||||||
pageNum: DEFAULT_CURRENT - 1,
|
pageNum: DEFAULT_CURRENT - 1,
|
||||||
pageSize: DEFAULT_PAGE_SIZE,
|
pageSize: DEFAULT_PAGE_SIZE,
|
||||||
@ -123,16 +124,11 @@ const RolePage: React.FC = () => {
|
|||||||
// 分配权限
|
// 分配权限
|
||||||
const handleAssignPermissions = async (record: RoleResponse) => {
|
const handleAssignPermissions = async (record: RoleResponse) => {
|
||||||
try {
|
try {
|
||||||
const permissions = await getRolePermissions(record.id);
|
const data = await getRoleMenusAndPermissions(record.id);
|
||||||
setSelectedRole(record);
|
setSelectedRole(record);
|
||||||
setPermissionDialogOpen(true);
|
setPermissionDialogOpen(true);
|
||||||
setDefaultCheckedKeys(
|
setDefaultMenuIds(data.menuIds || []);
|
||||||
Array.isArray(permissions)
|
setDefaultPermissionIds(data.permissionIds || []);
|
||||||
? typeof permissions[0] === 'number'
|
|
||||||
? permissions
|
|
||||||
: permissions.map((p: any) => p.id)
|
|
||||||
: []
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取角色权限失败:', error);
|
console.error('获取角色权限失败:', error);
|
||||||
toast({
|
toast({
|
||||||
@ -143,10 +139,10 @@ const RolePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmAssignPermissions = async (permissionIds: number[]) => {
|
const confirmAssignPermissions = async (menuIds: number[], permissionIds: number[]) => {
|
||||||
if (!selectedRole) return;
|
if (!selectedRole) return;
|
||||||
try {
|
try {
|
||||||
await assignPermissions(selectedRole.id, permissionIds);
|
await assignMenusAndPermissions(selectedRole.id, { menuIds, permissionIds });
|
||||||
toast({
|
toast({
|
||||||
title: '分配成功',
|
title: '分配成功',
|
||||||
description: `已为角色 "${selectedRole.name}" 分配权限`,
|
description: `已为角色 "${selectedRole.name}" 分配权限`,
|
||||||
@ -409,7 +405,8 @@ const RolePage: React.FC = () => {
|
|||||||
open={permissionDialogOpen}
|
open={permissionDialogOpen}
|
||||||
onOpenChange={setPermissionDialogOpen}
|
onOpenChange={setPermissionDialogOpen}
|
||||||
onConfirm={confirmAssignPermissions}
|
onConfirm={confirmAssignPermissions}
|
||||||
defaultCheckedKeys={defaultCheckedKeys}
|
defaultMenuIds={defaultMenuIds}
|
||||||
|
defaultPermissionIds={defaultPermissionIds}
|
||||||
/>
|
/>
|
||||||
<AssignTagDialog
|
<AssignTagDialog
|
||||||
open={assignTagDialogOpen}
|
open={assignTagDialogOpen}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type {RoleResponse, RoleRequest, RoleQuery, RoleTagResponse, RoleTagRequest, Permission} from './types';
|
import type {RoleResponse, RoleRequest, RoleQuery, RoleTagResponse, RoleTagRequest, Permission, MenusAndPermissions} from './types';
|
||||||
import {Page} from "@/types/base.ts";
|
import {Page} from "@/types/base.ts";
|
||||||
|
|
||||||
const BASE_URL = '/api/v1/role';
|
const BASE_URL = '/api/v1/role';
|
||||||
@ -31,11 +31,11 @@ export const updateRoleTag = (id: number, data: RoleTagRequest) => request.put(`
|
|||||||
|
|
||||||
export const createRoleTag = (data: RoleTagRequest) => request.post<RoleTagResponse>(ROLE_TAG_BASE_URL, data);
|
export const createRoleTag = (data: RoleTagRequest) => request.post<RoleTagResponse>(ROLE_TAG_BASE_URL, data);
|
||||||
|
|
||||||
// 获取角色的权限列表
|
// 获取角色的菜单和权限列表
|
||||||
export const getRolePermissions = (roleId: number) => request.get<string[]>(`${BASE_URL}/${roleId}/permissions`);
|
export const getRoleMenusAndPermissions = (roleId: number) => request.get<MenusAndPermissions>(`${BASE_URL}/${roleId}/menus-and-permissions`);
|
||||||
|
|
||||||
// 分配权限
|
// 分配菜单和权限
|
||||||
export const assignPermissions = (roleId: number, permissionIds: Number[]) => request.post(`${BASE_URL}/${roleId}/permissions`, permissionIds);
|
export const assignMenusAndPermissions = (roleId: number, data: MenusAndPermissions) => request.post(`${BASE_URL}/${roleId}/menus-and-permissions`, data);
|
||||||
|
|
||||||
// 获取所有权限列表
|
// 获取所有权限列表
|
||||||
export const getAllPermissions = () => request.get<Permission[]>(`${PERMISSION_BASE_URL}/list`);
|
export const getAllPermissions = () => request.get<Permission[]>(`${PERMISSION_BASE_URL}/list`);
|
||||||
|
|||||||
@ -68,3 +68,9 @@ export interface Permission {
|
|||||||
sort: number;
|
sort: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 菜单和权限数据
|
||||||
|
export interface MenusAndPermissions {
|
||||||
|
menuIds: number[];
|
||||||
|
permissionIds: number[];
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user