feat: Hair Keeper v1.1.0 版本更新
本次更新包含以下主要改进: ## 新功能 - 添加quickstart.sh脚本帮助用户快速使用模板项目 - 添加simple_deploy.sh便于部署 - 新增院系管理功能(DeptAdmin),支持增删改查院系管理员信息 - 用户可以在header中切换管理的院系 - 添加zustand全局状态管理 - 添加DEFAULT_USER_PASSWORD环境变量,作为创建用户时的默认密码 - 添加p-limit库和DB_PARALLEL_LIMIT环境变量控制数据库批次操作并发数 ## 安全修复 - 修复Next.js CVE-2025-66478漏洞 - 限制只有超级管理员才能创建超级管理员用户 ## 开发环境优化 - 开发终端兼容云端环境 - MinIO客户端直传兼容云端环境 - 开发容器增加vim和Claude Code插件 - 编程代理改用Claude - docker-compose.yml添加全局name属性 ## Bug修复与代码优化 - 删除用户时级联删除SelectionLog - 手机端关闭侧边栏后刷新页面延迟调整(300ms=>350ms) - instrumentation.ts移至src内部以适配生产环境 - 删除部分引发类型错误的无用代码 - 优化quickstart.sh远程仓库推送相关配置 ## 文件变更 - 新增49个文件,修改多个配置和源代码文件 - 重构用户管理模块目录结构 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
260
src/app/(main)/users/user-info/columns.tsx
Normal file
260
src/app/(main)/users/user-info/columns.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
'use client'
|
||||
|
||||
import { ColumnDef } from '@tanstack/react-table'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Edit, Trash2, MoreHorizontal } from 'lucide-react'
|
||||
import { formatDate } from '@/lib/format'
|
||||
import { userStatusOptions } from '@/lib/schema/user'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { DataTableColumnHeader } from '@/components/data-table/column-header'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import type { User } from '@/server/routers/users'
|
||||
|
||||
// 操作回调类型
|
||||
export type UserActions = {
|
||||
onEdit: (userId: string) => void
|
||||
onDelete: (userId: string) => void
|
||||
}
|
||||
|
||||
// 列定义选项类型
|
||||
export type UserColumnsOptions = {
|
||||
roles?: Array<{ id: number; name: string }>
|
||||
permissions?: Array<{ id: number; name: string }>
|
||||
depts?: Array<{ code: string; name: string; fullName: string }>
|
||||
}
|
||||
|
||||
// 创建用户表格列定义
|
||||
export const createUserColumns = (
|
||||
actions: UserActions,
|
||||
options: UserColumnsOptions = {}
|
||||
): ColumnDef<User>[] => [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) =>
|
||||
table.toggleAllPageRowsSelected(!!value)
|
||||
}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
size: 32,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
accessorKey: 'id',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="用户ID" />
|
||||
),
|
||||
cell: ({ row }) => <div className="font-medium">{row.original.id}</div>,
|
||||
enableColumnFilter: true,
|
||||
meta: {
|
||||
label: '用户ID',
|
||||
filter: {
|
||||
placeholder: '请输入用户ID',
|
||||
variant: 'text',
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="姓名" />
|
||||
),
|
||||
cell: ({ row }) => <div>{row.original.name || '-'}</div>,
|
||||
enableColumnFilter: true,
|
||||
meta: {
|
||||
label: '姓名',
|
||||
filter: {
|
||||
placeholder: '请输入姓名',
|
||||
variant: 'text',
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
accessorKey: 'status',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="状态" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status
|
||||
return (
|
||||
<Badge variant={status === '在校' ? 'default' : 'secondary'}>
|
||||
{status || '未知'}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
enableColumnFilter: true,
|
||||
meta: {
|
||||
label: '状态',
|
||||
filter: {
|
||||
variant: 'select',
|
||||
options: userStatusOptions,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'dept',
|
||||
accessorKey: 'dept',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="所属院系" />
|
||||
),
|
||||
cell: ({ row }) => <div>{row.original.dept?.name || '-'}</div>,
|
||||
enableColumnFilter: true,
|
||||
meta: {
|
||||
label: '所属院系',
|
||||
filter: {
|
||||
variant: 'multiSelect',
|
||||
options: options.depts?.map(dept => ({
|
||||
id: dept.code,
|
||||
name: dept.fullName,
|
||||
})) || [],
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'roles',
|
||||
accessorKey: 'roles',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="角色" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const roles = row.original.roles
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{roles.map((role) => (
|
||||
<Badge key={role.id} variant="secondary">
|
||||
{role.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
enableColumnFilter: true,
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
label: '角色',
|
||||
filter: {
|
||||
variant: 'select',
|
||||
options: options.roles?.map(role => ({
|
||||
id: role.id.toString(),
|
||||
name: role.name,
|
||||
})) || [],
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'permissions',
|
||||
accessorFn: row => Array.from(
|
||||
new Set(
|
||||
row.roles.flatMap((role) => role.permissions.map((p) => p.name))
|
||||
)
|
||||
),
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="权限" />
|
||||
),
|
||||
cell: ({ getValue }) => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{getValue<string[]>().map((permName) => (
|
||||
<Badge key={permName} variant="outline" className="text-xs">
|
||||
{permName}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
enableColumnFilter: true,
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
label: '权限',
|
||||
filter: {
|
||||
variant: 'select',
|
||||
options: options.permissions?.map(permission => ({
|
||||
id: permission.id.toString(),
|
||||
name: permission.name,
|
||||
})) || [],
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'lastLoginAt',
|
||||
accessorKey: 'lastLoginAt',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="最后登录" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const lastLoginAt = row.original.lastLoginAt as Date | null
|
||||
return <div>{lastLoginAt ? formatDate(lastLoginAt) : '从未登录'}</div>
|
||||
},
|
||||
meta: {
|
||||
label: '最后登录',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'createdAt',
|
||||
accessorKey: 'createdAt',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="创建时间" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return <div>{formatDate(row.original.createdAt) || '-'}</div>
|
||||
},
|
||||
meta: {
|
||||
label: '创建时间',
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
const user = row.original
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 md:w-9">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">打开菜单</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => actions.onEdit(user.id)}>
|
||||
<Edit className="h-4 w-4" />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => actions.onDelete(user.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
},
|
||||
size: 32,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
]
|
||||
Reference in New Issue
Block a user