Files
hair-keeper/src/app/(main)/users/user-info/components/UserUpdateDialog.tsx
liuyh 5020bd1532 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>
2025-12-23 16:58:55 +08:00

230 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}