13 changed files with 230 additions and 15 deletions
@ -0,0 +1,41 @@ |
|||
import * as React from 'react'; |
|||
import { Slot } from '@radix-ui/react-slot'; |
|||
import { cn } from '@/lib/utils'; |
|||
|
|||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { |
|||
asChild?: boolean; |
|||
variant?: 'default' | 'destructive' | 'outline' | 'secondary'; |
|||
size?: 'default' | 'sm' | 'lg'; |
|||
} |
|||
|
|||
const buttonVariants = { |
|||
default: 'bg-blue-600 text-white hover:bg-blue-700', |
|||
destructive: 'bg-red-600 text-white hover:bg-red-700', |
|||
outline: 'border border-gray-300 text-gray-700 bg-white hover:bg-gray-50', |
|||
secondary: 'bg-gray-100 text-gray-800 hover:bg-gray-200', |
|||
}; |
|||
|
|||
const sizeVariants = { |
|||
default: 'px-4 py-2 text-sm', |
|||
sm: 'px-3 py-1 text-xs', |
|||
lg: 'px-6 py-3 text-base', |
|||
}; |
|||
|
|||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( |
|||
({ className, variant = 'default', size = 'default', asChild = false, ...props }, ref) => { |
|||
const Comp = asChild ? Slot : 'button'; |
|||
return ( |
|||
<Comp |
|||
className={cn( |
|||
'inline-flex items-center justify-center rounded font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none', |
|||
buttonVariants[variant], |
|||
sizeVariants[size], |
|||
className |
|||
)} |
|||
ref={ref} |
|||
{...props} |
|||
/> |
|||
); |
|||
} |
|||
); |
|||
Button.displayName = 'Button'; |
@ -0,0 +1,26 @@ |
|||
import * as React from 'react'; |
|||
import * as DialogPrimitive from '@radix-ui/react-dialog'; |
|||
import { cn } from '@/lib/utils'; |
|||
|
|||
export const Dialog = DialogPrimitive.Root; |
|||
export const DialogTrigger = DialogPrimitive.Trigger; |
|||
|
|||
export const DialogContent = React.forwardRef< |
|||
React.ElementRef<typeof DialogPrimitive.Content>, |
|||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> |
|||
>(({ className, children, ...props }, ref) => ( |
|||
<DialogPrimitive.Portal> |
|||
<DialogPrimitive.Overlay className="fixed inset-0 bg-black/30 z-40" /> |
|||
<DialogPrimitive.Content |
|||
ref={ref} |
|||
className={cn( |
|||
'fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded shadow-lg focus:outline-none', |
|||
className |
|||
)} |
|||
{...props} |
|||
> |
|||
{children} |
|||
</DialogPrimitive.Content> |
|||
</DialogPrimitive.Portal> |
|||
)); |
|||
DialogContent.displayName = 'DialogContent'; |
@ -0,0 +1,5 @@ |
|||
import { Outlet } from 'react-router-dom'; |
|||
|
|||
export default function UserManagePage() { |
|||
return <Outlet />; |
|||
} |
@ -0,0 +1,43 @@ |
|||
import React, { useState } from 'react'; |
|||
import { Button } from '@/components/ui/button'; |
|||
|
|||
interface RoleFormProps { |
|||
onSubmit: (data: { name: string; description?: string }) => void; |
|||
} |
|||
|
|||
export default function RoleForm({ onSubmit }: RoleFormProps) { |
|||
const [name, setName] = useState(''); |
|||
const [description, setDescription] = useState(''); |
|||
|
|||
const handleSubmit = (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
onSubmit({ name, description }); |
|||
setName(''); |
|||
setDescription(''); |
|||
}; |
|||
|
|||
return ( |
|||
<form onSubmit={handleSubmit} className="space-y-4"> |
|||
<div> |
|||
<label className="block text-sm font-medium mb-1">角色名</label> |
|||
<input |
|||
className="w-full border rounded px-3 py-2 focus:outline-none focus:ring focus:border-blue-300" |
|||
value={name} |
|||
onChange={e => setName(e.target.value)} |
|||
required |
|||
/> |
|||
</div> |
|||
<div> |
|||
<label className="block text-sm font-medium mb-1">描述</label> |
|||
<input |
|||
className="w-full border rounded px-3 py-2 focus:outline-none focus:ring focus:border-blue-300" |
|||
value={description} |
|||
onChange={e => setDescription(e.target.value)} |
|||
/> |
|||
</div> |
|||
<div className="flex justify-end"> |
|||
<Button type="submit">保存</Button> |
|||
</div> |
|||
</form> |
|||
); |
|||
} |
@ -0,0 +1,45 @@ |
|||
import React from 'react'; |
|||
import { Button } from '@/components/ui/button'; |
|||
|
|||
interface RoleTableProps { |
|||
roles: any[]; |
|||
loading: boolean; |
|||
onDelete: (roleId: string) => void; |
|||
} |
|||
|
|||
export default function RoleTable({ roles, loading, onDelete }: RoleTableProps) { |
|||
return ( |
|||
<div className="bg-white rounded shadow"> |
|||
<table className="min-w-full divide-y divide-gray-200"> |
|||
<thead className="bg-gray-50"> |
|||
<tr> |
|||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">角色名</th> |
|||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">描述</th> |
|||
<th className="px-6 py-3"></th> |
|||
</tr> |
|||
</thead> |
|||
<tbody className="bg-white divide-y divide-gray-200"> |
|||
{loading ? ( |
|||
<tr> |
|||
<td colSpan={3} className="text-center py-6">加载中...</td> |
|||
</tr> |
|||
) : roles.length === 0 ? ( |
|||
<tr> |
|||
<td colSpan={3} className="text-center py-6">暂无数据</td> |
|||
</tr> |
|||
) : ( |
|||
roles.map((role) => ( |
|||
<tr key={role.id}> |
|||
<td className="px-6 py-4 whitespace-nowrap">{role.name}</td> |
|||
<td className="px-6 py-4 whitespace-nowrap">{role.description}</td> |
|||
<td className="px-6 py-4 whitespace-nowrap text-right"> |
|||
<Button variant="destructive" size="sm" onClick={() => onDelete(role.id)}>删除</Button> |
|||
</td> |
|||
</tr> |
|||
)) |
|||
)} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
); |
|||
} |
@ -0,0 +1,53 @@ |
|||
import React, { useEffect, useState } from 'react'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog'; |
|||
import { getAllRoles, createRole, deleteRole } from '@/services/roleService'; |
|||
import RoleTable from './RoleTable'; |
|||
import RoleForm from './RoleForm'; |
|||
|
|||
export default function RolesView() { |
|||
const [roles, setRoles] = useState<any[]>([]); |
|||
const [loading, setLoading] = useState(false); |
|||
const [open, setOpen] = useState(false); |
|||
|
|||
const fetchRoles = async () => { |
|||
setLoading(true); |
|||
const res = await getAllRoles(); |
|||
setRoles(res.data || []); |
|||
setLoading(false); |
|||
}; |
|||
|
|||
useEffect(() => { |
|||
fetchRoles(); |
|||
}, []); |
|||
|
|||
const handleCreate = async (data: { name: string; description?: string }) => { |
|||
await createRole(data); |
|||
setOpen(false); |
|||
fetchRoles(); |
|||
}; |
|||
|
|||
const handleDelete = async (roleId: string) => { |
|||
await deleteRole(roleId); |
|||
fetchRoles(); |
|||
}; |
|||
|
|||
return ( |
|||
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6"> |
|||
<div className="w-full space-y-6"> |
|||
<div className="flex justify-between items-center mb-4"> |
|||
<h1 className="text-2xl font-bold">角色管理</h1> |
|||
<Dialog open={open} onOpenChange={setOpen}> |
|||
<DialogTrigger asChild> |
|||
<Button>新建角色</Button> |
|||
</DialogTrigger> |
|||
<DialogContent> |
|||
<RoleForm onSubmit={handleCreate} /> |
|||
</DialogContent> |
|||
</Dialog> |
|||
</div> |
|||
<RoleTable roles={roles} loading={loading} onDelete={handleDelete} /> |
|||
</div> |
|||
</main> |
|||
); |
|||
} |
@ -0,0 +1,6 @@ |
|||
import { httpClient } from '@/lib/http-client'; |
|||
|
|||
export const getAllRoles = () => httpClient.get('/api/Roles/GetAllRoles'); |
|||
export const getRole = (roleId: string) => httpClient.get(`/api/Roles/GetRole/${roleId}`); |
|||
export const createRole = (data: { name: string; description?: string }) => httpClient.post('/api/Roles/CreateRole', data); |
|||
export const deleteRole = (roleId: string) => httpClient.delete(`/api/Roles/DeleteRole/${roleId}`); |
Loading…
Reference in new issue