359 lines
12 KiB
TypeScript
359 lines
12 KiB
TypeScript
/**
|
||
* 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>
|
||
);
|
||
} |