Files
smart-crop-ui/crop-x-new/src/app/(app)/central-config/tenant/user-management/components/EditUserModal.tsx

359 lines
12 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.

/**
* filekorolheader: 修改用户弹窗组件 - 编辑用户功能界面
* 功能:用户信息编辑表单、表单验证、数据更新、状态管理
* 路径:/central-config/tenant/user-management/components/EditUserModal
* 规范遵循crop-x-new/docs/开发项目规范.md使用shadcn语义化样式支持暗色主题
*/
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { toast } from 'sonner';
import { User } from '../types';
import { fetchEnterprisesForDropdown, transformEnterprisesToOptions, type EnterpriseOption } from './enterpriseApi';
import { PasswordInput } from './PasswordInput';
import { USER_TYPE_OPTIONS, USER_TYPES } from '../constants/userTypes';
interface EditUserModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
user: User | null;
onSuccess?: () => void;
}
interface EditUserFormData {
username: string;
password: string;
fullName: string;
phone: string;
email: string;
userType: typeof USER_TYPES[keyof typeof USER_TYPES];
enterpriseId?: string;
}
export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserModalProps) {
const [formData, setFormData] = useState<EditUserFormData>({
username: '',
password: '',
fullName: '',
phone: '',
email: '',
userType: 'tenant',
enterpriseId: '',
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [enterprises, setEnterprises] = useState<EnterpriseOption[]>([]);
const [enterpriseOptions, setEnterpriseOptions] = useState<Array<{ value: string; label: string }>>([]);
const [isLoadingEnterprises, setIsLoadingEnterprises] = useState(false);
// 加载企业列表数据
const loadEnterprises = async () => {
try {
setIsLoadingEnterprises(true);
console.log('🏢 EditUserModal - 开始加载企业列表');
const enterpriseData = await fetchEnterprisesForDropdown();
setEnterprises(enterpriseData);
const options = transformEnterprisesToOptions(enterpriseData);
setEnterpriseOptions(options);
console.log('🏢 EditUserModal - 企业列表加载完成:', {
total: enterpriseData.length,
options: options.length,
});
} catch (error) {
console.error('🏢 EditUserModal - 加载企业列表失败:', error);
toast.error('加载企业列表失败,请刷新页面重试');
} finally {
setIsLoadingEnterprises(false);
}
};
// 当弹窗打开时加载企业列表
useEffect(() => {
if (open) {
loadEnterprises();
}
}, [open]);
// 编辑弹窗不预填充用户数据,保持空表单
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.username.trim()) {
newErrors.username = '用户名不能为空';
}
if (!formData.password.trim()) {
newErrors.password = '密码不能为空';
} else if (formData.password.length < 6) {
newErrors.password = '密码长度至少6位';
}
if (!formData.fullName.trim()) {
newErrors.fullName = '姓名不能为空';
}
if (!formData.phone.trim()) {
newErrors.phone = '电话不能为空';
} else if (!/^1[3-9]\d{9}$/.test(formData.phone)) {
newErrors.phone = '请输入正确的手机号码';
}
if (formData.email.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = '邮箱格式不正确';
}
if (formData.userType === 'tenant' && !formData.enterpriseId?.trim()) {
newErrors.enterpriseId = '企业管理员必须选择所属企业';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async () => {
if (!validateForm() || !user) {
return;
}
setIsSubmitting(true);
try {
// TODO: 调用API更新用户
// 暂时模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
const submitData = {
id: user.id,
username: formData.username,
password: formData.password,
full_name: formData.fullName,
phone: formData.phone,
email: formData.email || undefined,
user_type: formData.userType,
...(formData.userType === 'tenant' && formData.enterpriseId ? { enterprise_id: formData.enterpriseId } : {}),
};
console.log('更新用户数据:', submitData);
toast.success('用户信息更新成功');
onOpenChange(false);
onSuccess?.();
} catch (error) {
console.error('更新用户失败:', error);
toast.error('更新用户失败,请重试');
} finally {
setIsSubmitting(false);
}
};
const handleInputChange = (field: keyof EditUserFormData, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
// 清除对应字段的错误
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: '' }));
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-[70vw] max-w-4xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription className="sr-only">
</DialogDescription>
</DialogHeader>
{!user ? (
<div className="py-4 text-center text-muted-foreground">
</div>
) : (
<>
<div className="space-y-6 py-4">
{/* 第一行:用户名、密码 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="edit-username"> *</Label>
<Input
id="edit-username"
value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value)}
placeholder="请输入用户名"
className={errors.username ? 'border-red-500' : ''}
/>
{errors.username && (
<p className="text-sm text-red-500 dark:text-red-400">{errors.username}</p>
)}
</div>
<PasswordInput
id="edit-password"
label="密码"
value={formData.password}
onChange={(value) => handleInputChange('password', value)}
placeholder="请输入密码"
required={true}
error={errors.password}
/>
</div>
{/* 第二行:姓名、电话 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="edit-full_name"> *</Label>
<Input
id="edit-full_name"
value={formData.fullName}
onChange={(e) => handleInputChange('fullName', e.target.value)}
placeholder="请输入姓名"
className={errors.fullName ? 'border-red-500' : ''}
/>
{errors.fullName && (
<p className="text-sm text-red-500 dark:text-red-400">{errors.fullName}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="edit-phone"> *</Label>
<Input
id="edit-phone"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
placeholder="请输入手机号码"
className={errors.phone ? 'border-red-500' : ''}
/>
{errors.phone && (
<p className="text-sm text-red-500 dark:text-red-400">{errors.phone}</p>
)}
</div>
</div>
{/* 第三行:邮箱 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="edit-email"></Label>
<Input
id="edit-email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="请输入邮箱(可选)"
className={errors.email ? 'border-red-500' : ''}
/>
{errors.email && (
<p className="text-sm text-red-500 dark:text-red-400">{errors.email}</p>
)}
</div>
</div>
{/* 第四行:用户类型、所属企业 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> *</Label>
<Select
value={formData.userType}
onValueChange={(value: typeof USER_TYPES[keyof typeof USER_TYPES]) => handleInputChange('userType', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{USER_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{formData.userType === 'tenant' ? (
<div className="space-y-2">
<Label htmlFor="edit-enterpriseId"> *</Label>
<Select
value={formData.enterpriseId}
onValueChange={(value) => handleInputChange('enterpriseId', value)}
disabled={isLoadingEnterprises}
>
<SelectTrigger>
<SelectValue placeholder={
isLoadingEnterprises
? "正在加载..."
: "请选择所属企业"
} />
</SelectTrigger>
<SelectContent>
{isLoadingEnterprises ? (
<SelectItem value="loading" disabled>
...
</SelectItem>
) : (
enterpriseOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))
)}
</SelectContent>
</Select>
{errors.enterpriseId && (
<p className="text-sm text-red-500 dark:text-red-400">{errors.enterpriseId}</p>
)}
</div>
) : (
<div className="space-y-2">
<Label></Label>
<Input
value="系统管理员无需选择企业"
disabled
className="bg-muted"
/>
</div>
)}
</div>
{/* 用户ID显示 */}
<div className="text-xs text-muted-foreground">
ID: {user.id}
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
</Button>
<Button
onClick={handleSubmit}
disabled={isSubmitting}
>
{isSubmitting ? '更新中...' : '更新'}
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
);
}