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:
2025-12-23 16:58:55 +08:00
parent 42be39b343
commit 5020bd1532
49 changed files with 2209 additions and 290 deletions

View 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>
)
}