354 lines
11 KiB
TypeScript
354 lines
11 KiB
TypeScript
import { Template } from "interface/template";
|
|
import {
|
|
Container,
|
|
Heading,
|
|
Text,
|
|
Box,
|
|
Flex,
|
|
Table,
|
|
Button,
|
|
TextField,
|
|
ScrollArea,
|
|
Dialog,
|
|
Avatar,
|
|
DropdownMenu,
|
|
Badge,
|
|
} from "@radix-ui/themes";
|
|
import {
|
|
PlusIcon,
|
|
MagnifyingGlassIcon,
|
|
Pencil1Icon,
|
|
TrashIcon,
|
|
PersonIcon,
|
|
DotsHorizontalIcon,
|
|
LockClosedIcon,
|
|
ExclamationTriangleIcon,
|
|
} from "@radix-ui/react-icons";
|
|
import { useState } from "react";
|
|
import type { User } from "interface/fields";
|
|
|
|
// 模拟用户数据
|
|
const mockUsers: (User & { id: number })[] = [
|
|
{
|
|
id: 1,
|
|
username: "admin",
|
|
email: "admin@example.com",
|
|
avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=1",
|
|
role: "admin",
|
|
createdAt: new Date("2024-01-01"),
|
|
updatedAt: new Date("2024-03-15"),
|
|
lastLoginAt: new Date("2024-03-15"),
|
|
passwordHash: "",
|
|
},
|
|
{
|
|
id: 2,
|
|
username: "editor",
|
|
email: "editor@example.com",
|
|
avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
|
|
role: "editor",
|
|
createdAt: new Date("2024-02-01"),
|
|
updatedAt: new Date("2024-03-14"),
|
|
lastLoginAt: new Date("2024-03-14"),
|
|
passwordHash: "",
|
|
},
|
|
{
|
|
id: 3,
|
|
username: "user",
|
|
email: "user@example.com",
|
|
avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=3",
|
|
role: "user",
|
|
createdAt: new Date("2024-03-01"),
|
|
updatedAt: new Date("2024-03-13"),
|
|
lastLoginAt: new Date("2024-03-13"),
|
|
passwordHash: "",
|
|
},
|
|
];
|
|
|
|
export default new Template({}, ({ http, args }) => {
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
|
const [selectedUser, setSelectedUser] = useState<
|
|
(typeof mockUsers)[0] | null
|
|
>(null);
|
|
const [newUserData, setNewUserData] = useState({
|
|
username: "",
|
|
email: "",
|
|
password: "",
|
|
role: "user",
|
|
});
|
|
|
|
// 获取角色标签样式
|
|
const getRoleBadgeColor = (role: string) => {
|
|
switch (role) {
|
|
case "admin":
|
|
return "red";
|
|
case "editor":
|
|
return "blue";
|
|
default:
|
|
return "gray";
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Box>
|
|
{/* 页面标题和操作栏 */}
|
|
<Flex justify="between" align="center" className="mb-6">
|
|
<Box>
|
|
<Heading size="6" className="text-[--gray-12] mb-2">
|
|
用户管理
|
|
</Heading>
|
|
<Text className="text-[--gray-11]">共 {mockUsers.length} 个用户</Text>
|
|
</Box>
|
|
<Button
|
|
className="bg-[--accent-9]"
|
|
onClick={() => setIsAddDialogOpen(true)}
|
|
>
|
|
<PlusIcon className="w-4 h-4" />
|
|
新建用户
|
|
</Button>
|
|
</Flex>
|
|
|
|
{/* 搜索栏 */}
|
|
<Box className="w-full sm:w-64 mb-6">
|
|
<TextField.Root
|
|
placeholder="搜索用户..."
|
|
value={searchTerm}
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
setSearchTerm(e.target.value)
|
|
}
|
|
>
|
|
<TextField.Slot>
|
|
<MagnifyingGlassIcon height="16" width="16" />
|
|
</TextField.Slot>
|
|
</TextField.Root>
|
|
</Box>
|
|
|
|
{/* 用户列表 */}
|
|
<Box className="border border-[--gray-6] rounded-lg overflow-hidden">
|
|
<ScrollArea>
|
|
<Table.Root>
|
|
<Table.Header>
|
|
<Table.Row>
|
|
<Table.ColumnHeaderCell>用户</Table.ColumnHeaderCell>
|
|
<Table.ColumnHeaderCell>邮箱</Table.ColumnHeaderCell>
|
|
<Table.ColumnHeaderCell>角色</Table.ColumnHeaderCell>
|
|
<Table.ColumnHeaderCell>注册时间</Table.ColumnHeaderCell>
|
|
<Table.ColumnHeaderCell>最后登录</Table.ColumnHeaderCell>
|
|
<Table.ColumnHeaderCell>操作</Table.ColumnHeaderCell>
|
|
</Table.Row>
|
|
</Table.Header>
|
|
|
|
<Table.Body>
|
|
{mockUsers.map((user) => (
|
|
<Table.Row key={user.id}>
|
|
<Table.Cell>
|
|
<Flex align="center" gap="2">
|
|
<Avatar
|
|
src={user.avatarUrl}
|
|
fallback={user.username[0].toUpperCase()}
|
|
size="2"
|
|
radius="full"
|
|
/>
|
|
<Text weight="medium">{user.username}</Text>
|
|
</Flex>
|
|
</Table.Cell>
|
|
<Table.Cell>{user.email}</Table.Cell>
|
|
<Table.Cell>
|
|
<Badge color={getRoleBadgeColor(user.role)}>
|
|
{user.role}
|
|
</Badge>
|
|
</Table.Cell>
|
|
<Table.Cell>{user.createdAt.toLocaleDateString()}</Table.Cell>
|
|
<Table.Cell>
|
|
{user.lastLoginAt?.toLocaleDateString()}
|
|
</Table.Cell>
|
|
<Table.Cell>
|
|
<Flex gap="2">
|
|
<Button
|
|
variant="ghost"
|
|
size="1"
|
|
onClick={() => {
|
|
setSelectedUser(user);
|
|
setIsEditDialogOpen(true);
|
|
}}
|
|
>
|
|
<Pencil1Icon className="w-4 h-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="1" color="red">
|
|
<TrashIcon className="w-4 h-4" />
|
|
</Button>
|
|
</Flex>
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
))}
|
|
</Table.Body>
|
|
</Table.Root>
|
|
</ScrollArea>
|
|
</Box>
|
|
|
|
{/* 新建用户对话框 */}
|
|
<Dialog.Root open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
|
<Dialog.Content style={{ maxWidth: 450 }}>
|
|
<Dialog.Title>新建用户</Dialog.Title>
|
|
<Dialog.Description size="2" mb="4">
|
|
创建一个新的用户账号
|
|
</Dialog.Description>
|
|
|
|
<Flex direction="column" gap="4">
|
|
<Box>
|
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
|
用户名
|
|
</Text>
|
|
<TextField.Root
|
|
placeholder="输入用户名"
|
|
value={newUserData.username}
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
setNewUserData({ ...newUserData, username: e.target.value })
|
|
}
|
|
/>
|
|
</Box>
|
|
|
|
<Box>
|
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
|
邮箱
|
|
</Text>
|
|
<TextField.Root
|
|
placeholder="输入邮箱"
|
|
value={newUserData.email}
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
setNewUserData({ ...newUserData, email: e.target.value })
|
|
}
|
|
/>
|
|
</Box>
|
|
|
|
<Box>
|
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
|
密码
|
|
</Text>
|
|
<TextField.Root
|
|
type="password"
|
|
placeholder="输入密码"
|
|
value={newUserData.password}
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
setNewUserData({ ...newUserData, password: e.target.value })
|
|
}
|
|
/>
|
|
</Box>
|
|
|
|
<Box>
|
|
<Text as="label" size="2" weight="bold" className="block mb-2">
|
|
角色
|
|
</Text>
|
|
<select
|
|
className="w-full h-9 px-3 rounded-md bg-[--gray-1] border border-[--gray-6] text-[--gray-12]"
|
|
value={newUserData.role}
|
|
onChange={(e) =>
|
|
setNewUserData({ ...newUserData, role: e.target.value })
|
|
}
|
|
>
|
|
<option value="user">普通用户</option>
|
|
<option value="editor">编辑</option>
|
|
<option value="admin">管理员</option>
|
|
</select>
|
|
</Box>
|
|
</Flex>
|
|
|
|
<Flex gap="3" mt="4" justify="end">
|
|
<Dialog.Close>
|
|
<Button variant="soft" color="gray">
|
|
取消
|
|
</Button>
|
|
</Dialog.Close>
|
|
<Dialog.Close>
|
|
<Button className="bg-[--accent-9]">创建</Button>
|
|
</Dialog.Close>
|
|
</Flex>
|
|
</Dialog.Content>
|
|
</Dialog.Root>
|
|
|
|
{/* 编辑用户对话框 */}
|
|
<Dialog.Root open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
|
<Dialog.Content style={{ maxWidth: 450 }}>
|
|
{selectedUser && (
|
|
<>
|
|
<Dialog.Title>编辑用户</Dialog.Title>
|
|
<Dialog.Description size="2" mb="4">
|
|
修改用户信息
|
|
</Dialog.Description>
|
|
|
|
<Flex direction="column" gap="4">
|
|
<Box>
|
|
<Text
|
|
as="label"
|
|
size="2"
|
|
weight="bold"
|
|
className="block mb-2"
|
|
>
|
|
用户名
|
|
</Text>
|
|
<TextField.Root defaultValue={selectedUser.username} />
|
|
</Box>
|
|
|
|
<Box>
|
|
<Text
|
|
as="label"
|
|
size="2"
|
|
weight="bold"
|
|
className="block mb-2"
|
|
>
|
|
邮箱
|
|
</Text>
|
|
<TextField.Root defaultValue={selectedUser.email} />
|
|
</Box>
|
|
|
|
<Box>
|
|
<Text
|
|
as="label"
|
|
size="2"
|
|
weight="bold"
|
|
className="block mb-2"
|
|
>
|
|
角色
|
|
</Text>
|
|
<select
|
|
className="w-full h-9 px-3 rounded-md bg-[--gray-1] border border-[--gray-6] text-[--gray-12]"
|
|
defaultValue={selectedUser.role}
|
|
>
|
|
<option value="user">普通用户</option>
|
|
<option value="editor">编辑</option>
|
|
<option value="admin">管理员</option>
|
|
</select>
|
|
</Box>
|
|
|
|
<Box>
|
|
<Text
|
|
as="label"
|
|
size="2"
|
|
weight="bold"
|
|
className="block mb-2"
|
|
>
|
|
重置密码
|
|
</Text>
|
|
<TextField.Root type="password" placeholder="留空则不修改" />
|
|
</Box>
|
|
</Flex>
|
|
|
|
<Flex gap="3" mt="4" justify="end">
|
|
<Dialog.Close>
|
|
<Button variant="soft" color="gray">
|
|
取消
|
|
</Button>
|
|
</Dialog.Close>
|
|
<Dialog.Close>
|
|
<Button className="bg-[--accent-9]">保存</Button>
|
|
</Dialog.Close>
|
|
</Flex>
|
|
</>
|
|
)}
|
|
</Dialog.Content>
|
|
</Dialog.Root>
|
|
</Box>
|
|
);
|
|
});
|