当前位置: 首页 > news >正文

纯代码实战:MBA培训管理系统 (八) ——用户管理(上)数据模型与列表查询

目录

  • 前言
  • 一、 设计用户数据模型
    • 1. 更新 Prisma Schema
    • 2. 执行数据库迁移
    • 3. 添加种子数据
  • 二、 创建用户列表 API
    • 三、 安装所需组件
  • 四、 创建用户列表组件
  • 五、 创建用户管理页面
  • 六、 配置导航菜单
  • 七、 运行效果
  • 总结

前言

在上一章中,我们完成了部门管理的完整 CRUD 功能。本章我们将进入用户管理模块,这是企业管理系统的核心部分。

用户管理涉及员工信息的维护、部门归属的管理、以及复杂的查询筛选功能。与部门管理不同,用户管理需要处理更多的字段类型(文本、电话、日期、枚举等)和更复杂的查询条件。

本章目标:

  • 设计用户数据模型(关联部门)
  • 实现用户列表查询(支持搜索、筛选、排序)
  • 搭建用户管理页面布局
  • 配置前端导航菜单

一、 设计用户数据模型

1. 更新 Prisma Schema

打开prisma/schema.prisma,添加 User 模型(如果之前没有完整定义):

// prisma/schema.prisma model User { id String @id @default(uuid()) name String // 姓名 employeeId String? @unique // 工号(可选,唯一) phone String? // 手机号 email String? @unique // 邮箱(可选,唯一) avatar String? // 头像URL // 状态:1-在职, 2-离职, 3-休假 status Int @default(1) // 入职时间 hireDate DateTime? @map("hire_date") // 部门关联 departmentId String? @map("department_id") department Department? @relation(fields: [departmentId], references: [id]) // 角色关联(多对多) roles Role[] // 账号信息(用于登录) password String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("users") @@index([departmentId]) @@index([status]) } // 确保 Department 模型有 users 关联 model Department { id String @id @default(uuid()) name String code String? @unique description String? sortOrder Int @default(0) isActive Boolean @default(true) parentId String? parent Department? @relation("DepartmentChildren", fields: [parentId], references: [id], onDelete: SetNull) children Department[] @relation("DepartmentChildren") users User[] // 关联用户 createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("departments") }

关键点解析:

  • employeeIdemail标记为@unique,确保唯一性
  • status使用整数表示:1-在职, 2-离职, 3-休假
  • departmentId建立外键关联,并添加索引优化查询
  • @@index([status])为状态字段添加索引,方便按状态筛选

2. 执行数据库迁移

# 生成迁移文件npx prisma migrate dev--nameadd_user_model# 重新生成 Prisma Clientnpx prisma generate

3. 添加种子数据

prisma/seed.ts中添加用户数据:

// 在 seed.ts 中添加用户数据asyncfunctionmain(){// ... 之前的部门创建代码 ...// 5. 创建示例用户constusers=[{name:"张三",employeeId:"EMP001",phone:"13800138001",email:"zhangsan@example.com",status:1,departmentId:headquarters.id,},{name:"李四",employeeId:"EMP002",phone:"13800138002",email:"lisi@example.com",status:1,departmentId:salesDept.id,},{name:"王五",employeeId:"EMP003",phone:"13800138003",email:"wangwu@example.com",status:2,// 离职departmentId:techDept.id,},]for(constuserofusers){awaitprisma.user.upsert({where:{email:user.email},update:{},create:{...user,password:hashedPassword,hireDate:newDate("2023-01-01"),},})}console.log("用户数据创建完成")}

执行种子脚本:

npx prisma db seed

二、 创建用户列表 API

创建src/app/api/users/route.ts,支持查询、搜索、筛选、排序:

// src/app/api/users/route.tsimport{NextResponse}from"next/server"importprismafrom"@/lib/prisma"import{getServerSession}from"next-auth/next"import{authOptions}from"../auth/[...nextauth]/route"// GET /api/users - 获取用户列表(支持搜索、筛选、排序)exportasyncfunctionGET(request:Request){try{constsession=awaitgetServerSession(authOptions)if(!session){returnNextResponse.json({error:"未授权"},{status:401})}// 解析查询参数const{searchParams}=newURL(request.url)constkeyword=searchParams.get("keyword")||""// 搜索关键词constdepartmentId=searchParams.get("departmentId")||""// 部门筛选conststatus=searchParams.get("status")||""// 状态筛选constsortField=searchParams.get("sortField")||"createdAt"// 排序字段constsortOrder=searchParams.get("sortOrder")||"desc"// 排序方向// 构建查询条件constwhere:any={}// 关键词搜索(姓名、工号、邮箱、手机号)if(keyword){where.OR=[{name:{contains:keyword,mode:"insensitive"}},{employeeId:{contains:keyword,mode:"insensitive"}},{email:{contains:keyword,mode:"insensitive"}},{phone:{contains:keyword}},]}// 部门筛选if(departmentId){where.departmentId=departmentId}// 状态筛选if(status){where.status=parseInt(status)}// 构建排序条件constorderBy:any={}orderBy[sortField]=sortOrder// 查询用户列表constusers=awaitprisma.user.findMany({where,orderBy,include:{department:{select:{id:true,name:true,},},},})// 格式化返回数据constformattedUsers=users.map((user)=>({id:user.id,name:user.name,employeeId:user.employeeId,phone:user.phone,email:user.email,avatar:user.avatar,status:user.status,hireDate:user.hireDate?.toISOString()||null,departmentId:user.departmentId,departmentName:user.department?.name||"-",createdAt:user.createdAt.toISOString(),}))returnNextResponse.json(formattedUsers)}catch(error){console.error("获取用户列表失败:",error)returnNextResponse.json({error:"获取用户列表失败"},{status:500})}}

关键点解析:

  • 使用searchParams获取 URL 查询参数
  • where.OR实现多字段模糊搜索
  • mode: "insensitive"实现不区分大小写的搜索
  • include: { department: {...} }关联查询部门信息
  • 返回格式化后的数据,包含departmentName便于展示

三、 安装所需组件

# 安装 shadcn 组件npx shadcn@latestaddtable npx shadcn@latestaddbutton npx shadcn@latestaddinput npx shadcn@latestaddbadge npx shadcn@latestaddselectn# 安装图标库npminstalllucide-react

四、 创建用户列表组件

创建src/components/user/UserList.tsx

// src/components/user/UserList.tsx "use client" import { useState, useEffect } from "react" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Search, Plus, MoreHorizontal, Pencil, Trash2 } from "lucide-react" // 用户数据类型 interface User { id: string name: string employeeId: string | null phone: string | null email: string | null avatar: string | null status: number hireDate: string | null departmentId: string | null departmentName: string createdAt: string } // 状态映射 const statusMap: Record<number, { label: string; variant: "default" | "secondary" | "destructive" | "outline" }> = { 1: { label: "在职", variant: "default" }, 2: { label: "离职", variant: "secondary" }, 3: { label: "休假", variant: "outline" }, } export function UserList() { const [users, setUsers] = useState<User[]>([]) const [loading, setLoading] = useState(true) // 查询条件 const [keyword, setKeyword] = useState("") const [status, setStatus] = useState<string>("all") const [departmentId, setDepartmentId] = useState<string>("all") // 获取用户列表 const fetchUsers = async () => { setLoading(true) try { // 构建查询参数 const params = new URLSearchParams() if (keyword) params.append("keyword", keyword) if (status && status !== "all") params.append("status", status) if (departmentId && departmentId !== "all") params.append("departmentId", departmentId) const response = await fetch(`/api/users?${params.toString()}`) if (!response.ok) throw new Error("获取用户列表失败") const data = await response.json() setUsers(data) } catch (error) { console.error("加载用户数据失败:", error) } finally { setLoading(false) } } // 初始加载和条件变化时重新获取 useEffect(() => { fetchUsers() }, [status, departmentId]) // 下拉选择变化时自动查询 // 搜索按钮点击 const handleSearch = () => { fetchUsers() } // 重置查询 const handleReset = () => { setKeyword("") setStatus("all") setDepartmentId("all") fetchUsers() } if (loading) { return <div className="p-6">加载中...</div> } return ( <div className="bg-white rounded-lg shadow-sm border border-gray-200"> {/* 标题栏 */} <div className="flex items-center justify-between p-6 border-b border-gray-200"> <div> <h1 className="text-xl font-bold text-gray-900">用户管理</h1> <p className="text-sm text-gray-500 mt-1"> 管理企业员工信息,支持搜索、筛选、排序 </p> </div> <Button className="bg-blue-600 hover:bg-blue-700"> <Plus className="w-4 h-4 mr-2" /> 新增用户 </Button> </div> {/* 查询条件 */} <div className="p-6 border-b border-gray-200 bg-gray-50"> <div className="flex flex-wrap gap-4"> {/* 关键词搜索 */} <div className="flex-1 min-w-[200px]"> <div className="relative"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" /> <Input placeholder="搜索姓名、工号、邮箱、手机号" value={keyword} onChange={(e) => setKeyword(e.target.value)} className="pl-10" onKeyDown={(e) => e.key === "Enter" && handleSearch()} /> </div> </div> {/* 状态筛选 */} <Select value={status} onValueChange={setStatus}> <SelectTrigger className="w-[150px]"> <SelectValue placeholder="全部状态" /> </SelectTrigger> <SelectContent> <SelectItem value="all">全部状态</SelectItem> <SelectItem value="1">在职</SelectItem> <SelectItem value="2">离职</SelectItem> <SelectItem value="3">休假</SelectItem> </SelectContent> </Select> {/* 部门筛选(简化版,实际应从部门API获取) */} <Select value={departmentId} onValueChange={setDepartmentId}> <SelectTrigger className="w-[180px]"> <SelectValue placeholder="全部部门" /> </SelectTrigger> <SelectContent> <SelectItem value="all">全部部门</SelectItem> {/* 部门选项应从API动态获取 */} </SelectContent> </Select> {/* 操作按钮 */} <div className="flex gap-2"> <Button variant="outline" onClick={handleSearch}> 查询 </Button> <Button variant="ghost" onClick={handleReset}> 重置 </Button> </div> </div> </div> {/* 数据表格 */} <div className="p-0"> <Table> <TableHeader> <TableRow className="bg-gray-50"> <TableHead>姓名</TableHead> <TableHead>工号</TableHead> <TableHead>部门</TableHead> <TableHead>手机号</TableHead> <TableHead>邮箱</TableHead> <TableHead>状态</TableHead> <TableHead>入职时间</TableHead> <TableHead className="text-right">操作</TableHead> </TableRow> </TableHeader> <TableBody> {users.length === 0 ? ( <TableRow> <TableCell colSpan={8} className="text-center py-10 text-gray-500"> 暂无用户数据 </TableCell> </TableRow> ) : ( users.map((user) => { const statusInfo = statusMap[user.status] || { label: "未知", variant: "outline" } return ( <TableRow key={user.id} className="hover:bg-gray-50"> <TableCell className="font-medium">{user.name}</TableCell> <TableCell className="text-gray-500">{user.employeeId || "-"}</TableCell> <TableCell className="text-gray-500">{user.departmentName}</TableCell> <TableCell className="text-gray-500">{user.phone || "-"}</TableCell> <TableCell className="text-gray-500">{user.email || "-"}</TableCell> <TableCell> <Badge variant={statusInfo.variant}>{statusInfo.label}</Badge> </TableCell> <TableCell className="text-gray-500"> {user.hireDate ? new Date(user.hireDate).toLocaleDateString("zh-CN") : "-"} </TableCell> <TableCell className="text-right"> <div className="flex justify-end gap-2"> <Button variant="ghost" size="icon"> <Pencil className="w-4 h-4" /> </Button> <Button variant="ghost" size="icon" className="text-red-600"> <Trash2 className="w-4 h-4" /> </Button> </div> </TableCell> </TableRow> ) }) )} </TableBody> </Table> </div> </div> ) }

关键点解析:

  • 使用useState管理查询条件状态
  • URLSearchParams构建查询参数
  • Select组件实现下拉筛选
  • Badge组件展示状态标签
  • 支持回车键触发搜索

五、 创建用户管理页面

创建src/app/(admin)/users/page.tsx

// src/app/(admin)/users/page.tsx "use client" import { UserList } from "@/components/user/UserList" export default function UsersPage() { return ( <div className="p-6"> <UserList /> </div> ) }

六、 配置导航菜单

更新src/app/(admin)/layout.tsx,添加用户管理菜单:

// src/app/(admin)/layout.tsx // ... 其他导入 ... const adminMenus = [ { name: "首页", path: "/admin/dashboard", icon: Home }, { name: "部门管理", path: "/departments", icon: Building }, { name: "用户管理", path: "/users", icon: Users }, // 新增 { name: "角色权限", path: "/roles", icon: Shield }, { name: "系统设置", path: "/settings", icon: Settings }, ] // ... 其余代码保持不变 ...

七、 运行效果

启动项目:

npmrun dev
  1. 登录后进入管理后台
  2. 点击左侧"用户管理"菜单
  3. 可以看到用户列表表格
  4. 在搜索框输入关键词,点击查询或按回车
  5. 使用状态下拉筛选在职/离职/休假员工
  6. 表格会实时显示筛选结果


总结

本章我们完成了用户管理的基础部分:

  1. 数据模型设计:User 模型,包含员工基本信息和部门关联
  2. 数据库迁移:执行 Prisma migrate 创建表结构
  3. 用户列表 API:支持搜索、筛选、排序的查询接口
  4. 用户列表组件:带查询条件的表格展示
  5. 导航菜单配置:添加用户管理入口

下一章我们将实现用户管理的完整 CRUD:

  • 用户新增(含部门下拉选择)
  • 用户编辑(数据回显)
  • 用户删除(带确认)
  • 头像上传功能
  • 更复杂的表单验证

敬请期待!

http://www.cnnetsun.cn/news/1631097.html

相关文章:

  • 暗黑3 D3KeyHelper按键助手:新手到高手的终极操作优化指南
  • BilibiliDown:B站视频下载的完整解决方案
  • Umi-OCR:构建企业级离线OCR解决方案的技术架构与工程实践
  • 2025_NIPS_ROVER: Recursive Reasoning Over Videos with Vision-Language Models for Embodied Tasks
  • 5个Reloadium高级调试技巧:帧重载、错误处理和闭包调试终极指南
  • 10个C语言开源项目解析与学习指南
  • STM32实战:5分钟搞定SSD1306 OLED汉字显示(附16x16字库生成教程)
  • 【实战】CodeBuddy使用技巧:5个Skills让编程效率翻倍的隐藏操作
  • 革命性glob匹配库micromatch:10倍性能提升的终极指南
  • foobox-cn:5步打造专业级foobar2000音乐播放体验
  • 3个步骤掌握WVP-GB28181-Pro:从零部署到多品牌监控设备统一管理的完整指南
  • 用快马快速构建数据库概念可视化原型,告别枯燥理论
  • Docker容器项目无法访问MySQL的解决策略
  • 万字保姆级指南:Solidity智能合约开发从入门到精通,基于Remix全流程实战
  • VSCode玩转Jupyter Notebook:比浏览器更好用的5个高效技巧(含环境隔离配置)
  • GitHub Token配置实战指南:PakePlus云打包权限设置与安全加固
  • MAVLink垂直扩展:Emaxx导航板专用协议库设计与实践
  • SAP Script表单开发实战:如何在SE71中调用ABAP Perform实现动态数据填充
  • Z-Image-Turbo-辉夜巫女GPU显存优化:--n-gpu-layers参数调优实测
  • DeepSeek 聊天完成 API 的应用与使用
  • 爬虫对抗:分布式爬虫架构设计与IP代理池实战
  • 射频电路设计——传输线理论中的阻抗匹配与功率传输
  • 【故障诊断】用于轴承故障诊断的候选故障频率优化克改进包络频谱研究附Matlab代码
  • 手把手教学:Qwen-Image-Edit-2511在ComfyUI中的快速部署与配置
  • 终极暗黑3按键助手:D3KeyHelper完整使用指南
  • CogVideoX-2b效果提升:多阶段生成与后期处理结合策略
  • 别光看速度了!用Python脚本实测llama.cpp推理时的真实内存占用(附完整测试代码)
  • 深圳小学数学期末试卷创新题型引热议,数学与文学跨界融合成焦点
  • 告别创作瓶颈:像素剧本圣殿应用指南,打造你的专属剧本工作站
  • Xenia Canary:Xbox 360游戏现代化解决方案——技术原理与实战指南