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

397 lines
11 KiB
TypeScript
Executable File
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 * 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>
);
}