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