forked from admin/hair-keeper
feat: 增加DEFAULT_USER_PASSWORD,作为创建用户时的默认密码,添加p-limit库,添加DB_PARALLEL_LIMIT = 32环境变量作为“数据库批次操作默认并发数” 限制只有超级管理员才能创建超级管理员用户 删除用户时可以级联删除SelectionLog 添加zustand全局状态管理 一对多的院系管理功能 ,支持增删改查院系管理员信息、用户可以在header中切换管理的院系
This commit is contained in:
@@ -32,12 +32,14 @@ import {
|
||||
import { useIsMobile } from '@/hooks/use-mobile'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
// FormDialog Context
|
||||
export interface FormDialogContextValue {
|
||||
form: UseFormReturn<any>
|
||||
close: () => void
|
||||
fields: FormFieldConfig[]
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const FormDialogContext = createContext<FormDialogContextValue | null>(null)
|
||||
@@ -77,7 +79,7 @@ export function FormCancelAction({ children = '取消', variant = 'outline', onC
|
||||
}
|
||||
|
||||
return (
|
||||
<Button type="button" variant={variant} onClick={handleClick} {...props}>
|
||||
<Button type="button" variant={variant} onClick={handleClick} disabled={props.disabled} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
@@ -99,7 +101,7 @@ export function FormResetAction({
|
||||
confirmDescription = '确定要重置表单吗?表单将回到打开时的状态。',
|
||||
...props
|
||||
}: FormResetActionProps) {
|
||||
const { form } = useFormDialogContext()
|
||||
const { form, isLoading } = useFormDialogContext()
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
|
||||
const handleConfirm = () => {
|
||||
@@ -113,7 +115,7 @@ export function FormResetAction({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="button" variant={variant} onClick={() => setShowConfirm(true)} {...props}>
|
||||
<Button type="button" variant={variant} onClick={() => setShowConfirm(true)} disabled={isLoading || props.disabled} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
|
||||
@@ -151,14 +153,14 @@ export function FormSubmitAction({
|
||||
variant = 'default',
|
||||
...props
|
||||
}: FormSubmitActionProps) {
|
||||
const { form } = useFormDialogContext()
|
||||
const { form, isLoading } = useFormDialogContext()
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant={variant}
|
||||
onClick={form.handleSubmit(onSubmit)}
|
||||
disabled={isSubmitting || disabled}
|
||||
disabled={isSubmitting || disabled || isLoading}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@@ -186,7 +188,21 @@ export interface FormGridContentProps {
|
||||
}
|
||||
|
||||
export function FormGridContent({ className = 'grid grid-cols-1 gap-4' }: FormGridContentProps) {
|
||||
const { form, fields } = useFormDialogContext()
|
||||
const { form, fields, isLoading } = useFormDialogContext()
|
||||
|
||||
// 如果正在加载,显示骨架屏
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={cn("p-1", className)}>
|
||||
{fields.map((fieldConfig) => (
|
||||
<div key={fieldConfig.name} className={cn("space-y-2", fieldConfig.className || '')}>
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("p-1", className)}>
|
||||
@@ -223,6 +239,7 @@ export interface FormDialogProps {
|
||||
className?: string // 允许自定义对话框内容样式,可控制宽度
|
||||
formClassName?: string // 允许自定义表格样式
|
||||
children: React.ReactNode // 操作按钮区域内容
|
||||
isLoading?: boolean // 是否正在加载数据
|
||||
}
|
||||
|
||||
export function FormDialog({
|
||||
@@ -235,13 +252,14 @@ export function FormDialog({
|
||||
className = 'max-w-md',
|
||||
formClassName,
|
||||
children,
|
||||
isLoading = false,
|
||||
}: FormDialogProps) {
|
||||
const isMobile = useIsMobile()
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
|
||||
// 当对话框打开时,自动聚焦到第一个表单输入控件
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (isOpen && !isLoading) {
|
||||
// 使当前拥有焦点的元素(通常是用来触发打开这个drawer的控件)失去焦点,不然控制台会警告焦点在一个要被隐藏于屏幕阅读器的控件上
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
// 使用 setTimeout 确保 DOM 已完全渲染
|
||||
@@ -259,7 +277,7 @@ export function FormDialog({
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isOpen])
|
||||
}, [isOpen, isLoading])
|
||||
|
||||
const close = () => {
|
||||
onClose()
|
||||
@@ -269,7 +287,8 @@ export function FormDialog({
|
||||
const contextValue: FormDialogContextValue = {
|
||||
form,
|
||||
close,
|
||||
fields
|
||||
fields,
|
||||
isLoading
|
||||
}
|
||||
|
||||
// 表单内容组件,在 Dialog 和 Drawer 中复用
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { User, LogOut, KeyRound } from 'lucide-react'
|
||||
import { DevPanel } from '@/app/(main)/dev/panel'
|
||||
import { ChangePasswordDialog } from '@/components/layout/change-password-dialog'
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||
import { signOut } from 'next-auth/react'
|
||||
@@ -23,6 +22,11 @@ import { useTheme } from 'next-themes'
|
||||
import { ThemeToggleButton, useThemeTransition } from '@/components/common/theme-toggle-button'
|
||||
import { getMenuTitle } from '@/constants/menu'
|
||||
import type { User as AppUser } from '@/types/user'
|
||||
import { useUserStore } from '@/lib/stores/userStore'
|
||||
import { toast } from 'sonner'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import { AdvancedSelect, SelectContent, SelectedName, SelectInput, SelectItemList, SelectPopover, SelectTrigger } from '@/components/common/advanced-select'
|
||||
import type { Dept } from '@prisma/client'
|
||||
|
||||
interface HeaderProps {
|
||||
user?: AppUser
|
||||
@@ -38,6 +42,47 @@ export function Header({ user }: HeaderProps) {
|
||||
|
||||
const pageTitle = getMenuTitle(pathname, 2) // 只匹配到第二级菜单
|
||||
|
||||
// 从zustand store获取当前管理的院系信息
|
||||
const { setCurrentManagedDept } = useUserStore()
|
||||
|
||||
// 获取可管理的院系列表(包含当前管理的院系信息)
|
||||
const { data: managedDeptsData } = trpc.users.getManagedDepts.useQuery()
|
||||
|
||||
// 本地状态管理
|
||||
const [currentManagedDeptCode, setCurrentManagedDeptCode] = useState<string | null>(null)
|
||||
const [managedDepts, setManagedDepts] = useState<Array<Dept>>([])
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
if (managedDeptsData && managedDeptsData.currentDept !== undefined) {
|
||||
setCurrentManagedDeptCode(managedDeptsData.currentDept)
|
||||
setManagedDepts(managedDeptsData.depts)
|
||||
|
||||
// 更新store中的当前管理院系信息
|
||||
const deptInfo = managedDeptsData.depts.find(dept => dept.code === managedDeptsData.currentDept) || null
|
||||
setCurrentManagedDept(deptInfo)
|
||||
}
|
||||
}, [managedDeptsData, setCurrentManagedDept])
|
||||
|
||||
// 切换管理院系
|
||||
const switchManagedDeptMutation = trpc.users.switchManagedDept.useMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success('切换管理院系成功')
|
||||
// 更新本地状态
|
||||
setCurrentManagedDeptCode(data.deptCode)
|
||||
// 更新store中的当前管理院系信息
|
||||
setCurrentManagedDept(managedDepts.find(dept => dept.code === data.deptCode) || null)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message || '切换管理院系失败')
|
||||
},
|
||||
})
|
||||
|
||||
// 处理院系切换
|
||||
const handleDeptChange = (deptCode: string | null) => {
|
||||
switchManagedDeptMutation.mutate({ deptCode })
|
||||
}
|
||||
|
||||
const handleThemeToggle = () => {
|
||||
startTransition(() => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark')
|
||||
@@ -105,6 +150,52 @@ export function Header({ user }: HeaderProps) {
|
||||
<DropdownMenuItem disabled>
|
||||
{user.isSuperAdmin ? '超级管理员' : (Array.isArray(user.roles) ? user.roles.join('、') : user.roles)}
|
||||
</DropdownMenuItem>
|
||||
{/* 管理院系 - 根据可管理院系数量决定显示方式 */}
|
||||
{managedDepts.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>管理院系</DropdownMenuLabel>
|
||||
{managedDepts.length === 1 ? (
|
||||
// 只有一个可管理院系时,直接显示院系名称
|
||||
<DropdownMenuItem disabled>
|
||||
{managedDepts[0].fullName}
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
// 多个可管理院系时,显示下拉选择器
|
||||
<div className="px-2 py-1.5">
|
||||
<AdvancedSelect
|
||||
value={currentManagedDeptCode}
|
||||
onChange={handleDeptChange}
|
||||
options={managedDepts.map(dept => ({
|
||||
id: dept.code,
|
||||
name: dept.fullName,
|
||||
shortName: dept.name
|
||||
}))}
|
||||
disabled={switchManagedDeptMutation.isPending}
|
||||
filterFunction={(option, searchValue) => {
|
||||
const search = searchValue.toLowerCase()
|
||||
return option.id.includes(search) ||
|
||||
option.name.toLowerCase().includes(search) ||
|
||||
(option.shortName && option.shortName.toLowerCase().includes(search))
|
||||
}}
|
||||
>
|
||||
<SelectPopover>
|
||||
<SelectTrigger
|
||||
placeholder="请选择管理院系"
|
||||
className="h-9"
|
||||
>
|
||||
<SelectedName />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{managedDepts.length > 5 && <SelectInput placeholder="搜索院系名称/代码" />}
|
||||
<SelectItemList />
|
||||
</SelectContent>
|
||||
</SelectPopover>
|
||||
</AdvancedSelect>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-red-600 cursor-pointer"
|
||||
@@ -131,4 +222,4 @@ export function Header({ user }: HeaderProps) {
|
||||
/>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user