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:
230
src/app/(main)/users/user-info/components/UserUpdateDialog.tsx
Normal file
230
src/app/(main)/users/user-info/components/UserUpdateDialog.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
import { updateUserSchema, userStatusOptions } from '@/lib/schema/user'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { toast } from 'sonner'
|
||||
import { FormDialog, FormActionBar, FormGridContent, FormCancelAction, FormSubmitAction, type FormFieldConfig } from '@/components/common/form-dialog'
|
||||
import { CheckboxGroup } from '@/components/common/checkbox-group'
|
||||
import {
|
||||
AdvancedSelect,
|
||||
SelectPopover,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectInput,
|
||||
SelectItemList,
|
||||
SelectedName
|
||||
} from '@/components/common/advanced-select'
|
||||
import { useSmartSelectOptions } from '@/hooks/use-smart-select-options'
|
||||
|
||||
type UpdateUserInput = z.infer<typeof updateUserSchema>
|
||||
|
||||
const updateUserDefaultValues: UpdateUserInput = {
|
||||
id: '',
|
||||
name: '',
|
||||
status: '',
|
||||
deptCode: '',
|
||||
password: '',
|
||||
roleIds: [],
|
||||
isSuperAdmin: false,
|
||||
}
|
||||
|
||||
interface UserUpdateDialogProps {
|
||||
userId: string | null
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onUserUpdated: () => void
|
||||
}
|
||||
|
||||
export function UserUpdateDialog({ userId, isOpen, onClose, onUserUpdated }: UserUpdateDialogProps) {
|
||||
// react-hook-form 管理更新表单
|
||||
const updateForm = useForm<UpdateUserInput>({
|
||||
resolver: zodResolver(updateUserSchema),
|
||||
defaultValues: updateUserDefaultValues,
|
||||
})
|
||||
|
||||
// 获取用户详情
|
||||
const { data: user, isLoading: isLoadingUser } = trpc.users.getById.useQuery(
|
||||
{ id: userId! },
|
||||
{ enabled: !!userId && isOpen }
|
||||
)
|
||||
|
||||
// 获取角色列表和院系列表
|
||||
const { data: roles } = trpc.users.getRoles.useQuery()
|
||||
const { data: depts } = trpc.common.getDepts.useQuery()
|
||||
|
||||
const deptOptions = React.useMemo(() => depts?.map(dept => ({ id: dept.code, name: dept.fullName, shortName: dept.name })) || [], [depts])
|
||||
const { sortedOptions: sortedDeptOptions, logSelection: logDeptSelection } = useSmartSelectOptions({
|
||||
options: deptOptions,
|
||||
context: 'user.update.dept',
|
||||
scope: 'personal',
|
||||
})
|
||||
|
||||
// 更新用户 mutation
|
||||
const updateUserMutation = trpc.users.update.useMutation({
|
||||
onSuccess: () => {
|
||||
onClose()
|
||||
toast.success('用户更新成功')
|
||||
onUserUpdated()
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message || '更新用户失败')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// 定义字段配置
|
||||
const formFields: FormFieldConfig[] = React.useMemo(() => [
|
||||
{
|
||||
name: 'id',
|
||||
label: '用户ID',
|
||||
required: true,
|
||||
render: ({ field }) => (
|
||||
<Input {...field} placeholder="请输入用户ID(职工号)" disabled />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
label: '姓名',
|
||||
render: ({ field }) => (
|
||||
<Input {...field} placeholder="请输入姓名" />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: '状态',
|
||||
render: ({ field }) => (
|
||||
<AdvancedSelect
|
||||
{...field}
|
||||
options={userStatusOptions}
|
||||
>
|
||||
<SelectPopover>
|
||||
<SelectTrigger placeholder="请选择状态">
|
||||
<SelectedName />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItemList />
|
||||
</SelectContent>
|
||||
</SelectPopover>
|
||||
</AdvancedSelect>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'deptCode',
|
||||
label: '所属院系',
|
||||
render: ({ field }) => (
|
||||
<AdvancedSelect
|
||||
{...field}
|
||||
options={sortedDeptOptions}
|
||||
onChange={(value) => {logDeptSelection(value); field.onChange(value)}}
|
||||
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="请选择院系" clearable>
|
||||
<SelectedName />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectInput placeholder="搜索院系名称/代码" />
|
||||
<SelectItemList />
|
||||
</SelectContent>
|
||||
</SelectPopover>
|
||||
</AdvancedSelect>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
label: '新密码',
|
||||
render: ({ field }) => (
|
||||
<Input {...field} type="password" placeholder="留空则不修改密码" />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'roleIds',
|
||||
label: '角色',
|
||||
render: ({ field }) => (
|
||||
<CheckboxGroup
|
||||
{...field}
|
||||
options={roles || []}
|
||||
idPrefix="role"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'isSuperAdmin',
|
||||
label: '超级管理员',
|
||||
render: ({ field }) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="isSuperAdmin"
|
||||
checked={field.value || false}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Label htmlFor="isSuperAdmin" className="text-sm">
|
||||
超级管理员
|
||||
</Label>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
], [sortedDeptOptions, logDeptSelection, roles])
|
||||
|
||||
// 当用户数据加载完成时,重置表单
|
||||
useEffect(() => {
|
||||
if (user && isOpen) {
|
||||
const defaultValues: UpdateUserInput = {
|
||||
id: user.id,
|
||||
name: user.name || '',
|
||||
status: user.status || '',
|
||||
deptCode: user.deptCode || '',
|
||||
password: '', // 密码字段默认为空,只有填写时才更新
|
||||
roleIds: user.roles?.map((role) => role.id) || [],
|
||||
isSuperAdmin: user.isSuperAdmin || false,
|
||||
}
|
||||
updateForm.reset(defaultValues)
|
||||
}
|
||||
}, [user, isOpen, updateForm])
|
||||
|
||||
// 当对话框关闭时,清理状态
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
updateForm.reset()
|
||||
}
|
||||
}, [isOpen, updateForm])
|
||||
|
||||
const handleSubmit = async (data: UpdateUserInput) => {
|
||||
updateUserMutation.mutate(data)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
onClose()
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<FormDialog
|
||||
isOpen={isOpen}
|
||||
title="编辑用户"
|
||||
description="请填写用户信息"
|
||||
form={updateForm}
|
||||
fields={formFields}
|
||||
onClose={handleClose}
|
||||
isLoading={isLoadingUser}
|
||||
>
|
||||
<FormGridContent />
|
||||
<FormActionBar>
|
||||
<FormCancelAction />
|
||||
<FormSubmitAction onSubmit={handleSubmit} isSubmitting={updateUserMutation.isPending}>更新</FormSubmitAction>
|
||||
</FormActionBar>
|
||||
</FormDialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user