casualroom/apps/fenghuo/web/components/user/user-edit-form.tsx

397 lines
11 KiB
TypeScript
Raw Normal View History

2025-07-28 07:50:50 +08:00
'use client';
import * as React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@nice/ui/components/button';
import { Input } from '@nice/ui/components/input';
import { Textarea } from '@nice/ui/components/textarea';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@nice/ui/components/form';
import { CheckIcon, X as XIcon } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { useTRPC, useUser } from '@fenghuo/client';
import { DeptSelect } from '@/components/selector/dept-select';
import { MultipleRoleSelector } from '@/components/selector/role-select';
import { UserWithRelations, userWithRelationsSelect } from '@fenghuo/common';
import { toast } from '@nice/ui/components/sonner';
import { SystemPermission, useAuth } from '@/components/providers/auth-provider';
import { useEffect } from 'react';
const UserEditFormSchema = z.object({
username: z.string().min(1, { message: '请输入账户名称' }),
password: z.string().optional(),
confirmPassword: z.string().optional(),
roleIds: z.string().array(),
organizationId: z.string().optional(),
description: z.string().optional(),
}).refine((data) => {
// 如果设置了密码,则必须确认密码一致(仅对当前用户修改自己密码时)
if (data.password && data.password.length > 0) {
return data.password === data.confirmPassword;
}
return true;
}, {
message: '两次输入的密码不一致',
path: ['confirmPassword'],
});
type UserEditFormValues = z.infer<typeof UserEditFormSchema>;
// 加载状态组件
function LoadingIndicator() {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="flex flex-col items-center space-y-4">
<div className="size-8 animate-spin rounded-full border-2 border-muted border-t-primary" />
<p className="text-sm text-muted-foreground">...</p>
</div>
</div>
);
}
interface UserEditFormProps {
userId: string;
isCurrentUser?: boolean;
}
export function UserEditForm({ userId }: UserEditFormProps) {
const { update, changePassword } = useUser();
const { user: currentUser } = useAuth();
const isCurrentUser = React.useMemo(() => {
return currentUser!.id === userId;
}, [currentUser]);
// 权限检查
const userPermissions = currentUser?.permissions || [];
const isSuperAdmin = userPermissions.includes(SystemPermission.SUPER_ADMIN);
const hasUserEditPermission = userPermissions.includes(SystemPermission.USER_EDIT);
const canEditUser = isSuperAdmin || hasUserEditPermission || isCurrentUser;
// 密码修改权限超级管理员和有USER_EDIT权限的用户可以直接修改他人密码
const canDirectlyChangePassword = isSuperAdmin || hasUserEditPermission;
// 当前用户修改自己密码需要确认密码
const needPasswordConfirmation = isCurrentUser && !canDirectlyChangePassword;
const trpc = useTRPC();
// 初始化表单
const form = useForm<UserEditFormValues>({
resolver: zodResolver(UserEditFormSchema),
mode: 'onChange', // 启用onChange模式以实时检测变化
defaultValues: {
username: '',
password: '',
confirmPassword: '',
roleIds: [],
organizationId: '',
description: '',
},
});
// 获取用户数据
const { data: user, isLoading: isLoadingUser } = useQuery({
...trpc.user.findFirst.queryOptions({
where: {
id: userId,
},
select: userWithRelationsSelect,
}),
enabled: !!userId,
}) as { data: UserWithRelations | undefined; isLoading: boolean };
// 当用户数据加载完成时,初始化表单
useEffect(() => {
if (user) {
// 获取用户的所有角色ID
const userRoleIds = user.roles?.map((r) => r.id) || [];
const organization = user.organization;
const initialValues = {
username: user.username || '',
roleIds: userRoleIds,
organizationId: organization?.id || '',
description: user.description || '',
password: '',
confirmPassword: ''
};
form.reset(initialValues);
}
}, [user, form]);
// 检测表单是否有变化
const hasChanges = form.formState.isDirty;
// 检测密码字段是否有值
const passwordValue = form.watch('password');
const hasPasswordChange = passwordValue && passwordValue.length > 0;
// 处理表单提交
const onSubmit = async (values: UserEditFormValues) => {
if (!canEditUser) {
toast.error('您没有权限编辑此用户');
return;
}
try {
const { roleIds, confirmPassword, ...others } = values;
// 如果密码为空,则不更新密码字段
const updateData: any = {
...others,
roles: {
set: roleIds.map(id => ({ id }))
}
};
// 只有当密码不为空时才包含密码字段
if (!others.password || others.password.trim() === '') {
delete updateData.password;
}
await update.mutateAsync({
where: {
id: userId
},
data: updateData
});
// 清空密码字段
form.setValue('password', '');
form.setValue('confirmPassword', '');
toast.success('用户信息更新成功');
} catch (error: any) {
console.error('更新用户失败:', error);
toast.error('保存失败', {
description: error?.message || '更新失败,请重试',
duration: 5000,
});
}
};
// 显示加载状态
if (isLoadingUser) {
return <LoadingIndicator />;
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* 基本信息区域 */}
<div className="bg-card border rounded-lg p-6 space-y-6">
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold"></h3>
<div className="flex-1 h-px bg-border"></div>
</div>
<div className="grid gap-4 md:grid-cols-2">
{/* 账户名称 */}
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel> <span className="text-destructive">*</span></FormLabel>
<FormControl>
<Input
placeholder="请输入账户名称"
autoComplete="username"
disabled={!canEditUser}
{...field}
value={field.value || ''}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 角色选择 */}
<FormField
control={form.control}
name="roleIds"
render={({ field }) => (
<FormItem>
<FormLabel> <span className="text-destructive">*</span></FormLabel>
<FormControl>
<MultipleRoleSelector
value={field.value}
onValueChange={field.onChange}
placeholder="选择角色"
className="w-full"
showDescription={true}
disabled={!canEditUser}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 部门选择 */}
<FormField
control={form.control}
name="organizationId"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<DeptSelect
value={field.value}
onValueChange={(value) => {
const departmentValue = Array.isArray(value) ? value[0] || '' : value;
field.onChange(departmentValue);
}}
placeholder="选择部门"
className="w-full"
disabled={!canEditUser}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{/* 个人简介区域 */}
<div className="bg-card border rounded-lg p-6 space-y-4">
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold"></h3>
<div className="flex-1 h-px bg-border"></div>
</div>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Textarea
placeholder="请输入个人简介,让其他人更好地了解您..."
rows={4}
disabled={!canEditUser}
className="resize-none"
{...field}
value={field.value || ''}
/>
</FormControl>
<FormDescription className="text-xs text-muted-foreground">
500 {(field.value || '').length}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* 安全设置区域 - 仅可编辑密码时显示 */}
{canEditUser && (
<div className="bg-card border rounded-lg p-6 space-y-4">
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold"></h3>
<div className="flex-1 h-px bg-border"></div>
</div>
<div className="grid gap-4 md:grid-cols-2">
{/* 新密码 */}
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>
{isCurrentUser ? '修改密码' : '重置用户密码'}
</FormLabel>
<FormControl>
<Input
type="password"
placeholder="请输入新密码至少6个字符"
autoComplete="new-password"
disabled={!canEditUser}
{...field}
value={field.value || ''}
/>
</FormControl>
<FormDescription className="text-xs text-muted-foreground">
{isCurrentUser ? '留空则不修改密码' : '留空则不重置密码'}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* 确认密码 - 仅当前用户修改自己密码时显示 */}
{needPasswordConfirmation && hasPasswordChange && (
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
type="password"
placeholder="请再次输入新密码"
autoComplete="new-password"
disabled={!canEditUser}
{...field}
value={field.value || ''}
/>
</FormControl>
<FormDescription className="text-xs text-muted-foreground">
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
</div>
)}
{/* 操作按钮区域 - 只有在有变化且有编辑权限时才显示 */}
{canEditUser && hasChanges && (
<div className="flex items-center justify-end gap-3 pt-6 border-t">
<Button
type="button"
variant="outline"
onClick={() => form.reset()}
disabled={form.formState.isSubmitting}
>
<XIcon className="size-4 mr-2" />
</Button>
<Button
type="submit"
disabled={form.formState.isSubmitting || update.isPending}
>
{form.formState.isSubmitting || update.isPending ? (
<>
<div className="size-4 animate-spin rounded-full border-2 border-background border-t-foreground mr-2" />
...
</>
) : (
<>
<CheckIcon className="size-4 mr-2" />
</>
)}
</Button>
</div>
)}
</form>
</Form>
);
}