生产管理系统 - 提交用户管理页面代码
This commit is contained in:
@@ -0,0 +1,353 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 新增用户弹窗组件 - 新建用户功能界面
|
||||||
|
* 功能:用户信息录入表单、表单验证、数据提交、状态管理
|
||||||
|
* 路径:/central-config/tenant/user-management/components/AddUserModal
|
||||||
|
* 规范:遵循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 { fetchEnterprisesForDropdown, transformEnterprisesToOptions, type EnterpriseOption } from './enterpriseApi';
|
||||||
|
import { PasswordInput } from './PasswordInput';
|
||||||
|
import { USER_TYPE_OPTIONS, USER_TYPES } from '../constants/userTypes';
|
||||||
|
|
||||||
|
interface AddUserModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddUserFormData {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
fullName: string;
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
userType: typeof USER_TYPES[keyof typeof USER_TYPES];
|
||||||
|
enterpriseId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProps) {
|
||||||
|
const [formData, setFormData] = useState<AddUserFormData>({
|
||||||
|
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('🏢 AddUserModal - 开始加载企业列表');
|
||||||
|
|
||||||
|
const enterpriseData = await fetchEnterprisesForDropdown();
|
||||||
|
setEnterprises(enterpriseData);
|
||||||
|
|
||||||
|
const options = transformEnterprisesToOptions(enterpriseData);
|
||||||
|
setEnterpriseOptions(options);
|
||||||
|
|
||||||
|
console.log('🏢 AddUserModal - 企业列表加载完成:', {
|
||||||
|
total: enterpriseData.length,
|
||||||
|
options: options.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('🏢 AddUserModal - 加载企业列表失败:', 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()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: 调用API创建用户
|
||||||
|
// 暂时模拟API调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
const submitData = {
|
||||||
|
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?.();
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
setFormData({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
fullName: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
userType: 'tenant',
|
||||||
|
enterpriseId: '',
|
||||||
|
});
|
||||||
|
setErrors({});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建用户失败:', error);
|
||||||
|
toast.error('创建用户失败,请重试');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (field: keyof AddUserFormData, 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>
|
||||||
|
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
{/* 第一行:用户名、密码 */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="username">用户名 *</Label>
|
||||||
|
<Input
|
||||||
|
id="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="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="full_name">姓名 *</Label>
|
||||||
|
<Input
|
||||||
|
id="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="phone">电话 *</Label>
|
||||||
|
<Input
|
||||||
|
id="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="email">邮箱</Label>
|
||||||
|
<Input
|
||||||
|
id="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="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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? '创建中...' : '创建'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 密码输入组件 - 支持显示/隐藏密码功能
|
||||||
|
* 功能:密码输入、显示/隐藏切换、表单验证
|
||||||
|
* 路径:/central-config/tenant/user-management/components/PasswordInput
|
||||||
|
* 规范:遵循crop-x-new/docs/开发项目规范.md,使用shadcn语义化样式,支持暗色主题
|
||||||
|
*/
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Eye, EyeOff } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface PasswordInputProps {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
required?: boolean;
|
||||||
|
error?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PasswordInput({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = "请输入密码",
|
||||||
|
required = false,
|
||||||
|
error,
|
||||||
|
disabled = false,
|
||||||
|
}: PasswordInputProps) {
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
const togglePasswordVisibility = () => {
|
||||||
|
setShowPassword(!showPassword);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
id={id}
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
error && 'border-red-500',
|
||||||
|
'pr-10' // 为眼睛图标预留空间
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={togglePasswordVisibility}
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
'absolute right-3 top-1/2 transform -translate-y-1/2',
|
||||||
|
'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200',
|
||||||
|
'focus:outline-none',
|
||||||
|
disabled && 'opacity-50 cursor-not-allowed'
|
||||||
|
)}
|
||||||
|
tabIndex={-1} // 防止Tab键聚焦到眼睛图标
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm text-red-500 dark:text-red-400">{error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 企业下拉列表API接口 - 用户管理页面企业数据获取服务
|
||||||
|
* 功能:企业列表查询、下拉框数据准备、错误处理
|
||||||
|
* 路径:/central-config/tenant/user-management/components/enterpriseApi
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用SDK API调用,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getAuthToken } from "@/utils/token.ts";
|
||||||
|
import {
|
||||||
|
listTenantsApiV1TenantsGet
|
||||||
|
} from "@/lib/api/sdk.gen";
|
||||||
|
|
||||||
|
// 企业数据类型(简化版,用于下拉框)
|
||||||
|
export interface EnterpriseOption {
|
||||||
|
id: string;
|
||||||
|
company_name: string;
|
||||||
|
tenant_code: string;
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API响应数据类型
|
||||||
|
export interface EnterpriseApiResponse {
|
||||||
|
data: EnterpriseOption[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
total_pages: number;
|
||||||
|
has_next: boolean;
|
||||||
|
has_prev: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询参数接口
|
||||||
|
export interface EnterpriseQueryParams {
|
||||||
|
search?: string;
|
||||||
|
audit_status?: string;
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
order_by?: string;
|
||||||
|
sort_order?: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取企业列表数据(用于下拉框)
|
||||||
|
* 固定查询100条数据,获取所有活跃企业
|
||||||
|
*/
|
||||||
|
export async function fetchEnterprisesForDropdown(params: EnterpriseQueryParams = {}): Promise<EnterpriseOption[]> {
|
||||||
|
try {
|
||||||
|
console.log('🏢 用户管理页面 - 获取企业下拉列表数据');
|
||||||
|
|
||||||
|
// 构建查询参数,固定查询100条数据
|
||||||
|
const queryParams: any = {
|
||||||
|
page: 1,
|
||||||
|
size: 100, // 固定查询100条
|
||||||
|
sort_order: 'desc',
|
||||||
|
order_by: 'created_at',
|
||||||
|
// 只查询活跃的企业
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加可选参数
|
||||||
|
if (params.search) queryParams.search = params.search;
|
||||||
|
if (params.audit_status) queryParams.audit_status = params.audit_status;
|
||||||
|
|
||||||
|
// 获取认证token
|
||||||
|
const token = getAuthToken();
|
||||||
|
|
||||||
|
// 使用SDK API调用企业查询接口
|
||||||
|
const response = await listTenantsApiV1TenantsGet({
|
||||||
|
query: {
|
||||||
|
...queryParams,
|
||||||
|
// 添加时间戳防止缓存
|
||||||
|
_t: Date.now(),
|
||||||
|
},
|
||||||
|
headers: token ? {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(`API error: ${response.error.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data as any;
|
||||||
|
|
||||||
|
console.log('🏢 企业下拉列表API响应:', {
|
||||||
|
total: data?.total || 0,
|
||||||
|
dataCount: data?.data?.length || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 转换响应数据格式,只返回需要的字段
|
||||||
|
const enterprises: EnterpriseOption[] = (data?.data || []).map((tenant: any) => ({
|
||||||
|
id: tenant.id,
|
||||||
|
company_name: tenant.company_name,
|
||||||
|
tenant_code: tenant.tenant_code,
|
||||||
|
is_active: tenant.is_active,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('🏢 转换后的企业下拉列表数据:', enterprises.length, '条');
|
||||||
|
|
||||||
|
return enterprises;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('🏢 获取企业下拉列表失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将企业数据转换为下拉框选项格式
|
||||||
|
*/
|
||||||
|
export function transformEnterprisesToOptions(enterprises: EnterpriseOption[]): Array<{
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}> {
|
||||||
|
return enterprises
|
||||||
|
.filter(enterprise => enterprise.is_active) // 只显示活跃企业
|
||||||
|
.map(enterprise => ({
|
||||||
|
value: enterprise.id,
|
||||||
|
label: `${enterprise.company_name} (${enterprise.tenant_code})`,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.label.localeCompare(b.label, 'zh-CN')); // 按中文名称排序
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 用户类型常量定义 - 用户类型映射关系
|
||||||
|
* 功能:用户类型枚举、中文名称映射、下拉选项数据
|
||||||
|
* 路径:/central-config/tenant/user-management/constants/userTypes
|
||||||
|
* 规范:遵循crop-x-new/docs/开发项目规范.md,使用TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 用户类型枚举
|
||||||
|
export const USER_TYPES = {
|
||||||
|
TENANT: 'tenant',
|
||||||
|
SYSTEM: 'system',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 用户类型中文映射
|
||||||
|
export const USER_TYPE_LABELS = {
|
||||||
|
[USER_TYPES.TENANT]: '企业管理员',
|
||||||
|
[USER_TYPES.SYSTEM]: '系统管理员',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 用户类型下拉选项
|
||||||
|
export const USER_TYPE_OPTIONS = [
|
||||||
|
{
|
||||||
|
value: USER_TYPES.TENANT,
|
||||||
|
label: USER_TYPE_LABELS[USER_TYPES.TENANT],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: USER_TYPES.SYSTEM,
|
||||||
|
label: USER_TYPE_LABELS[USER_TYPES.SYSTEM],
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// 根据值获取标签
|
||||||
|
export function getUserTypeLabel(type: string): string {
|
||||||
|
return USER_TYPE_LABELS[type as keyof typeof USER_TYPE_LABELS] || type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export type UserType = keyof typeof USER_TYPE_LABELS;
|
||||||
@@ -11,6 +11,8 @@ import { toast } from 'sonner';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Eye, Edit, Lock, UserX, UserCheck } from 'lucide-react';
|
import { Eye, Edit, Lock, UserX, UserCheck } from 'lucide-react';
|
||||||
import { UserDetailDialog } from './components/UserDetailDialog';
|
import { UserDetailDialog } from './components/UserDetailDialog';
|
||||||
|
import { AddUserModal } from './components/AddUserModal';
|
||||||
|
import { EditUserModal } from './components/EditUserModal';
|
||||||
import { SearchFormPagination, SearchFieldConfig, TableColumnConfig } from '@/components/common/searchFormPagination';
|
import { SearchFormPagination, SearchFieldConfig, TableColumnConfig } from '@/components/common/searchFormPagination';
|
||||||
|
|
||||||
import { fetchUsers, transformUserData, UsersQueryParams, User, UsersApiResponse, PaginationState } from './components/userManagementApi';
|
import { fetchUsers, transformUserData, UsersQueryParams, User, UsersApiResponse, PaginationState } from './components/userManagementApi';
|
||||||
@@ -31,6 +33,8 @@ interface UserManagementState {
|
|||||||
sortOrder: 'asc' | 'desc';
|
sortOrder: 'asc' | 'desc';
|
||||||
selectedUser: User | null;
|
selectedUser: User | null;
|
||||||
showDetailDialog: boolean;
|
showDetailDialog: boolean;
|
||||||
|
showAddDialog: boolean;
|
||||||
|
showEditDialog: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserManagementAction =
|
type UserManagementAction =
|
||||||
@@ -42,6 +46,8 @@ type UserManagementAction =
|
|||||||
| { type: 'SET_PAGINATION'; payload: Partial<PaginationState> }
|
| { type: 'SET_PAGINATION'; payload: Partial<PaginationState> }
|
||||||
| { type: 'SET_SELECTED_USER'; payload: User | null }
|
| { type: 'SET_SELECTED_USER'; payload: User | null }
|
||||||
| { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean }
|
| { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'TOGGLE_ADD_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'TOGGLE_EDIT_DIALOG'; payload: boolean }
|
||||||
| { type: 'REFRESH_DATA' };
|
| { type: 'REFRESH_DATA' };
|
||||||
|
|
||||||
const userManagementReducer = (state: UserManagementState, action: UserManagementAction): UserManagementState => {
|
const userManagementReducer = (state: UserManagementState, action: UserManagementAction): UserManagementState => {
|
||||||
@@ -68,6 +74,10 @@ const userManagementReducer = (state: UserManagementState, action: UserManagemen
|
|||||||
return { ...state, selectedUser: action.payload };
|
return { ...state, selectedUser: action.payload };
|
||||||
case 'TOGGLE_DETAIL_DIALOG':
|
case 'TOGGLE_DETAIL_DIALOG':
|
||||||
return { ...state, showDetailDialog: !state.showDetailDialog };
|
return { ...state, showDetailDialog: !state.showDetailDialog };
|
||||||
|
case 'TOGGLE_ADD_DIALOG':
|
||||||
|
return { ...state, showAddDialog: !state.showAddDialog };
|
||||||
|
case 'TOGGLE_EDIT_DIALOG':
|
||||||
|
return { ...state, showEditDialog: !state.showEditDialog };
|
||||||
case 'REFRESH_DATA':
|
case 'REFRESH_DATA':
|
||||||
return { ...state, error: null };
|
return { ...state, error: null };
|
||||||
default:
|
default:
|
||||||
@@ -96,6 +106,8 @@ const initialState: UserManagementState = {
|
|||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
selectedUser: null,
|
selectedUser: null,
|
||||||
showDetailDialog: false,
|
showDetailDialog: false,
|
||||||
|
showAddDialog: false,
|
||||||
|
showEditDialog: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TenantUserManagementPage() {
|
export default function TenantUserManagementPage() {
|
||||||
@@ -461,9 +473,20 @@ export default function TenantUserManagementPage() {
|
|||||||
|
|
||||||
// 编辑用户
|
// 编辑用户
|
||||||
const handleEdit = (user: User) => {
|
const handleEdit = (user: User) => {
|
||||||
toast.info('编辑功能开发中...');
|
dispatch({ type: 'SET_SELECTED_USER', payload: user });
|
||||||
|
dispatch({ type: 'TOGGLE_EDIT_DIALOG', payload: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 新增用户
|
||||||
|
const handleAdd = () => {
|
||||||
|
dispatch({ type: 'TOGGLE_ADD_DIALOG', payload: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新数据(用于新增/编辑成功后重新加载数据)
|
||||||
|
const refreshData = useCallback(() => {
|
||||||
|
loadUsers({});
|
||||||
|
}, [loadUsers]);
|
||||||
|
|
||||||
// 切换用户状态
|
// 切换用户状态
|
||||||
const handleToggleStatus = (user: User) => {
|
const handleToggleStatus = (user: User) => {
|
||||||
const newStatus = !user.isActive;
|
const newStatus = !user.isActive;
|
||||||
@@ -525,7 +548,7 @@ export default function TenantUserManagementPage() {
|
|||||||
<SearchFormPagination
|
<SearchFormPagination
|
||||||
formTitle="用户列表"
|
formTitle="用户列表"
|
||||||
formRightContent={
|
formRightContent={
|
||||||
<Button onClick={() => toast.info('新建用户功能开发中...')}>
|
<Button onClick={handleAdd}>
|
||||||
新建用户
|
新建用户
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -551,6 +574,21 @@ export default function TenantUserManagementPage() {
|
|||||||
onOpenChange={(open) => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: open })}
|
onOpenChange={(open) => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: open })}
|
||||||
user={state.selectedUser}
|
user={state.selectedUser}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 新增用户对话框 */}
|
||||||
|
<AddUserModal
|
||||||
|
open={state.showAddDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'TOGGLE_ADD_DIALOG', payload: open })}
|
||||||
|
onSuccess={refreshData}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 编辑用户对话框 */}
|
||||||
|
<EditUserModal
|
||||||
|
open={state.showEditDialog}
|
||||||
|
onOpenChange={(open) => dispatch({ type: 'TOGGLE_EDIT_DIALOG', payload: open })}
|
||||||
|
user={state.selectedUser}
|
||||||
|
onSuccess={refreshData}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user