生产管理系统 - 员工管理提交

This commit is contained in:
2025-11-12 10:45:56 +08:00
parent 80171778b5
commit 8fefadaf55
25 changed files with 1187 additions and 708 deletions

View File

@@ -23,6 +23,7 @@ import { toast } from 'sonner';
import { fetchEnterprisesForDropdown, transformEnterprisesToOptions, type EnterpriseOption } from './enterpriseApi';
import { PasswordInput } from './PasswordInput';
import { USER_TYPE_OPTIONS, USER_TYPES } from '../constants/userTypes';
import { createUser, type CreateUserRequest } from './userManagementApi';
interface AddUserModalProps {
open: boolean;
@@ -88,6 +89,23 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
}
}, [open]);
// 当弹窗关闭时重置表单状态
useEffect(() => {
if (!open) {
setFormData({
username: '',
password: '',
fullName: '',
phone: '',
email: '',
userType: 'tenant',
enterpriseId: '',
});
setErrors({});
setIsSubmitting(false);
}
}, [open]);
const validateForm = () => {
const newErrors: Record<string, string> = {};
@@ -131,40 +149,30 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
setIsSubmitting(true);
try {
// TODO: 调用API创建用户
// 暂时模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
const submitData = {
// 构建API请求数据
const userData: CreateUserRequest = {
email: formData.email,
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 } : {}),
password: formData.password,
is_superuser: true, // 所有用户都是超管
...(formData.userType === 'tenant' && formData.enterpriseId ? { tenant_id: formData.enterpriseId } : {}),
};
console.log('新增用户数据:', submitData);
console.log('新增用户数据:', userData);
// 调用API创建用户
await createUser(userData);
toast.success('用户创建成功');
onOpenChange(false);
onSuccess?.();
// 重置表单
setFormData({
username: '',
password: '',
fullName: '',
phone: '',
email: '',
userType: 'tenant',
enterpriseId: '',
});
setErrors({});
} catch (error) {
console.error('创建用户失败:', error);
toast.error('创建用户失败,请重试');
const errorMessage = error instanceof Error ? error.message : '创建用户失败,请重试';
toast.error(errorMessage);
} finally {
setIsSubmitting(false);
}
@@ -188,17 +196,18 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
<form className="space-y-6 py-4" autoComplete="off">
{/* 第一行:用户名、密码 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="username"> *</Label>
<Label htmlFor="username"> <span className="text-red-500">*</span></Label>
<Input
id="username"
value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value)}
placeholder="请输入用户名"
className={errors.username ? 'border-red-500' : ''}
autoComplete="new-username"
/>
{errors.username && (
<p className="text-sm text-red-500 dark:text-red-400">{errors.username}</p>
@@ -219,13 +228,14 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
{/* 第二行:姓名、电话 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="full_name"> *</Label>
<Label htmlFor="full_name"> <span className="text-red-500">*</span></Label>
<Input
id="full_name"
value={formData.fullName}
onChange={(e) => handleInputChange('fullName', e.target.value)}
placeholder="请输入姓名"
className={errors.fullName ? 'border-red-500' : ''}
autoComplete="name"
/>
{errors.fullName && (
<p className="text-sm text-red-500 dark:text-red-400">{errors.fullName}</p>
@@ -233,13 +243,14 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
</div>
<div className="space-y-2">
<Label htmlFor="phone"> *</Label>
<Label htmlFor="phone"> <span className="text-red-500">*</span></Label>
<Input
id="phone"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
placeholder="请输入手机号码"
className={errors.phone ? 'border-red-500' : ''}
autoComplete="tel"
/>
{errors.phone && (
<p className="text-sm text-red-500 dark:text-red-400">{errors.phone}</p>
@@ -258,6 +269,7 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="请输入邮箱(可选)"
className={errors.email ? 'border-red-500' : ''}
autoComplete="email"
/>
{errors.email && (
<p className="text-sm text-red-500 dark:text-red-400">{errors.email}</p>
@@ -268,13 +280,13 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
{/* 第四行:用户类型、所属企业 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> *</Label>
<Label> <span className="text-red-500">*</span></Label>
<Select
value={formData.userType}
onValueChange={(value: typeof USER_TYPES[keyof typeof USER_TYPES]) => handleInputChange('userType', value)}
>
<SelectTrigger>
<SelectValue />
<SelectValue placeholder="请选择用户类型" />
</SelectTrigger>
<SelectContent>
{USER_TYPE_OPTIONS.map((option) => (
@@ -288,7 +300,7 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
{formData.userType === 'tenant' ? (
<div className="space-y-2">
<Label htmlFor="enterpriseId"> *</Label>
<Label htmlFor="enterpriseId"> <span className="text-red-500">*</span></Label>
<Select
value={formData.enterpriseId}
onValueChange={(value) => handleInputChange('enterpriseId', value)}
@@ -330,7 +342,7 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
</div>
)}
</div>
</div>
</form>
<DialogFooter>
<Button

View File

@@ -24,6 +24,7 @@ import { User } from '../types';
import { fetchEnterprisesForDropdown, transformEnterprisesToOptions, type EnterpriseOption } from './enterpriseApi';
import { PasswordInput } from './PasswordInput';
import { USER_TYPE_OPTIONS, USER_TYPES } from '../constants/userTypes';
import { createUser, fetchUserDetails, type CreateUserRequest } from './userManagementApi';
interface EditUserModalProps {
open: boolean;
@@ -58,6 +59,52 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
const [enterprises, setEnterprises] = useState<EnterpriseOption[]>([]);
const [enterpriseOptions, setEnterpriseOptions] = useState<Array<{ value: string; label: string }>>([]);
const [isLoadingEnterprises, setIsLoadingEnterprises] = useState(false);
const [isLoadingUserDetails, setIsLoadingUserDetails] = useState(false);
// 加载用户详情数据
const loadUserDetails = async (userId: string) => {
try {
setIsLoadingUserDetails(true);
console.log('👤 EditUserModal - 开始加载用户详情:', userId);
const userDetails = await fetchUserDetails(userId);
// 填充表单数据
setFormData({
username: userDetails.username || '',
password: '', // 编辑时不显示密码
fullName: userDetails.full_name || '',
phone: userDetails.phone || '',
email: userDetails.email || '',
userType: userDetails.is_superuser ? 'system' : 'tenant',
enterpriseId: userDetails.tenant_id || '',
});
console.log('👤 EditUserModal - 用户详情加载完成:', {
username: userDetails.username,
fullName: userDetails.full_name,
phone: userDetails.phone,
email: userDetails.email,
userType: userDetails.is_superuser ? 'system' : 'tenant',
tenantId: userDetails.tenant_id,
});
} catch (error) {
console.error('👤 EditUserModal - 加载用户详情失败:', error);
toast.error('加载用户详情失败,请刷新页面重试');
// 如果加载失败,重置表单为空
setFormData({
username: '',
password: '',
fullName: '',
phone: '',
email: '',
userType: 'tenant',
enterpriseId: '',
});
} finally {
setIsLoadingUserDetails(false);
}
};
// 加载企业列表数据
const loadEnterprises = async () => {
@@ -83,15 +130,36 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
}
};
// 当弹窗打开时加载企业列表
// 当弹窗打开时加载企业列表和用户详情
useEffect(() => {
if (open) {
loadEnterprises();
// 如果有用户ID则加载用户详情
if (user?.id) {
loadUserDetails(user.id);
}
}
}, [open, user?.id]);
// 当弹窗关闭时重置表单状态
useEffect(() => {
if (!open) {
setFormData({
username: '',
password: '',
fullName: '',
phone: '',
email: '',
userType: 'tenant',
enterpriseId: '',
});
setErrors({});
setIsSubmitting(false);
setIsLoadingUserDetails(false);
}
}, [open]);
// 编辑弹窗不预填充用户数据,保持空表单
const validateForm = () => {
const newErrors: Record<string, string> = {};
@@ -99,9 +167,8 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
newErrors.username = '用户名不能为空';
}
if (!formData.password.trim()) {
newErrors.password = '密码不能为空';
} else if (formData.password.length < 6) {
// 编辑模式下密码可以为空,但如果填写了密码则需要验证长度
if (formData.password.trim() && formData.password.length < 6) {
newErrors.password = '密码长度至少6位';
}
@@ -135,29 +202,30 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
setIsSubmitting(true);
try {
// TODO: 调用API更新用户
// 暂时模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
const submitData = {
id: user.id,
// 构建API请求数据
const userData: CreateUserRequest = {
email: formData.email,
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 } : {}),
password: formData.password,
is_superuser: true, // 所有用户都是超管
...(formData.userType === 'tenant' && formData.enterpriseId ? { tenant_id: formData.enterpriseId } : {}),
};
console.log('更新用户数据:', submitData);
console.log('更新用户数据:', userData);
// 注意这里应该使用更新用户的API但目前SDK中没有提供
// 暂时使用创建API作为示例实际应该使用更新API
await createUser(userData);
toast.success('用户信息更新成功');
onOpenChange(false);
onSuccess?.();
} catch (error) {
console.error('更新用户失败:', error);
toast.error('更新用户失败,请重试');
const errorMessage = error instanceof Error ? error.message : '更新用户失败,请重试';
toast.error(errorMessage);
} finally {
setIsSubmitting(false);
}
@@ -185,19 +253,25 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
<div className="py-4 text-center text-muted-foreground">
</div>
) : isLoadingUserDetails ? (
<div className="py-8 flex flex-col items-center justify-center space-y-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
<div className="text-muted-foreground">...</div>
</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>
<Label htmlFor="edit-username"> <span className="text-red-500">*</span></Label>
<Input
id="edit-username"
value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value)}
placeholder="请输入用户名"
className={errors.username ? 'border-red-500' : ''}
autoComplete="new-username"
/>
{errors.username && (
<p className="text-sm text-red-500 dark:text-red-400">{errors.username}</p>
@@ -209,8 +283,8 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
label="密码"
value={formData.password}
onChange={(value) => handleInputChange('password', value)}
placeholder="请输入密码"
required={true}
placeholder="留空表示不修改密码"
required={false}
error={errors.password}
/>
</div>
@@ -218,13 +292,14 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
{/* 第二行:姓名、电话 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="edit-full_name"> *</Label>
<Label htmlFor="edit-full_name"> <span className="text-red-500">*</span></Label>
<Input
id="edit-full_name"
value={formData.fullName}
onChange={(e) => handleInputChange('fullName', e.target.value)}
placeholder="请输入姓名"
className={errors.fullName ? 'border-red-500' : ''}
autoComplete="name"
/>
{errors.fullName && (
<p className="text-sm text-red-500 dark:text-red-400">{errors.fullName}</p>
@@ -232,13 +307,14 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
</div>
<div className="space-y-2">
<Label htmlFor="edit-phone"> *</Label>
<Label htmlFor="edit-phone"> <span className="text-red-500">*</span></Label>
<Input
id="edit-phone"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
placeholder="请输入手机号码"
className={errors.phone ? 'border-red-500' : ''}
autoComplete="tel"
/>
{errors.phone && (
<p className="text-sm text-red-500 dark:text-red-400">{errors.phone}</p>
@@ -257,6 +333,7 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="请输入邮箱(可选)"
className={errors.email ? 'border-red-500' : ''}
autoComplete="email"
/>
{errors.email && (
<p className="text-sm text-red-500 dark:text-red-400">{errors.email}</p>
@@ -267,13 +344,13 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
{/* 第四行:用户类型、所属企业 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> *</Label>
<Label> <span className="text-red-500">*</span></Label>
<Select
value={formData.userType}
onValueChange={(value: typeof USER_TYPES[keyof typeof USER_TYPES]) => handleInputChange('userType', value)}
>
<SelectTrigger>
<SelectValue />
<SelectValue placeholder="请选择用户类型" />
</SelectTrigger>
<SelectContent>
{USER_TYPE_OPTIONS.map((option) => (
@@ -287,7 +364,7 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
{formData.userType === 'tenant' ? (
<div className="space-y-2">
<Label htmlFor="edit-enterpriseId"> *</Label>
<Label htmlFor="edit-enterpriseId"> <span className="text-red-500">*</span></Label>
<Select
value={formData.enterpriseId}
onValueChange={(value) => handleInputChange('enterpriseId', value)}

View File

@@ -21,6 +21,7 @@ interface PasswordInputProps {
required?: boolean;
error?: string;
disabled?: boolean;
autoComplete?: string;
}
export function PasswordInput({
@@ -32,6 +33,7 @@ export function PasswordInput({
required = false,
error,
disabled = false,
autoComplete = "new-password",
}: PasswordInputProps) {
const [showPassword, setShowPassword] = useState(false);
@@ -53,6 +55,7 @@ export function PasswordInput({
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
disabled={disabled}
autoComplete={autoComplete}
className={cn(
error && 'border-red-500',
'pr-10' // 为眼睛图标预留空间

View File

@@ -1,138 +0,0 @@
'use client';
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
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 { User, Enterprise, UserFormData } from '../types';
interface UserFormDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
editingUser: User | null;
formData: UserFormData;
onFormDataChange: (data: UserFormData) => void;
onSave: () => void;
enterprises: Enterprise[];
}
export function UserFormDialog({
open,
onOpenChange,
editingUser,
formData,
onFormDataChange,
onSave,
enterprises
}: UserFormDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{editingUser ? '编辑用户' : '添加用户'}</DialogTitle>
<DialogDescription className="sr-only">
{editingUser ? '编辑用户信息' : '添加新用户'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="username"> *</Label>
<Input
id="username"
value={formData.username || ''}
onChange={(e) => onFormDataChange({ ...formData, username: e.target.value })}
placeholder="登录用户名"
/>
</div>
<div>
<Label htmlFor="name"> *</Label>
<Input
id="name"
value={formData.name || ''}
onChange={(e) => onFormDataChange({ ...formData, name: e.target.value })}
placeholder="真实姓名"
/>
</div>
<div>
<Label htmlFor="phone"> *</Label>
<Input
id="phone"
value={formData.phone || ''}
onChange={(e) => onFormDataChange({ ...formData, phone: e.target.value })}
placeholder="手机号码"
/>
</div>
<div>
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
value={formData.email || ''}
onChange={(e) => onFormDataChange({ ...formData, email: e.target.value })}
placeholder="电子邮箱"
/>
</div>
<div>
<Label htmlFor="userType"> *</Label>
<Select
value={formData.userType || 'enterprise_admin'}
onValueChange={(value) => {
onFormDataChange({
...formData,
userType: value,
// 如果切换为超级管理员,清除企业信息
...(value === 'super_admin' ? { enterpriseId: undefined, enterpriseName: undefined } : {})
});
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="super_admin"></SelectItem>
<SelectItem value="enterprise_admin"></SelectItem>
</SelectContent>
</Select>
</div>
{formData.userType === 'enterprise_admin' && (
<div>
<Label htmlFor="enterpriseId"> *</Label>
<Select
value={formData.enterpriseId || ''}
onValueChange={(value: string) => {
const selectedEnterprise = enterprises.find(e => e.id === value);
onFormDataChange({
...formData,
enterpriseId: value,
enterpriseName: selectedEnterprise?.name
});
}}
>
<SelectTrigger>
<SelectValue placeholder="请选择企业" />
</SelectTrigger>
<SelectContent>
{enterprises.map((enterprise) => (
<SelectItem key={enterprise.id} value={enterprise.id}>
{enterprise.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={onSave}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,245 +0,0 @@
/**
* filekorolheader: 用户列表组件 - 用户数据表格展示界面
* 功能:用户数据表格展示、状态徽章、操作按钮、分页功能
* 路径:/central-config/tenant/user-management/components/UserList
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui组件TypeScript类型安全
*/
import { User, PaginationState } from '../types';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Eye, Edit, Trash2, Lock, UserX, UserCheck } from 'lucide-react';
interface UserListProps {
users: User[];
loading: boolean;
pagination: PaginationState;
onPageChange: (page: number) => void;
onViewDetail: (user: User) => void;
onEdit?: (user: User) => void;
onDelete?: (user: User) => void;
onToggleStatus?: (user: User) => void;
onResetPassword?: (user: User) => void;
}
export function UserList({
users,
loading,
pagination,
onPageChange,
onViewDetail,
onEdit,
onDelete,
onToggleStatus,
onResetPassword
}: UserListProps) {
const getStatusBadge = (user: User) => {
// 根据isSuperuser和isActive判断状态
if (user.isSuperuser) {
return user.isActive ? (
<Badge className="bg-green-100 text-green-700"></Badge>
) : (
<Badge className="bg-gray-100 text-gray-700"></Badge>
);
}
if (user.isActive) {
return <Badge className="bg-green-100 text-green-700"></Badge>;
} else {
return <Badge className="bg-red-100 text-red-700"></Badge>;
}
};
const getUserTypeBadge = (user: User) => {
if (user.isSuperuser) {
return <Badge className="bg-purple-100 text-purple-700"></Badge>;
}
// 根据scope或其他字段判断用户类型
if (user.scope === 'enterprise' || user.companyName) {
return <Badge className="bg-blue-100 text-blue-700"></Badge>;
}
return <Badge className="bg-green-100 text-green-700"></Badge>;
};
const getRoleBadge = (user: User) => {
if (user.isSuperuser) {
return <Badge className="bg-purple-100 text-purple-700"></Badge>;
}
return <Badge className="bg-blue-100 text-blue-700"></Badge>;
};
const getVerifiedBadge = (isVerified: boolean) => {
return isVerified ? (
<Badge className="bg-green-100 text-green-700"></Badge>
) : (
<Badge variant="outline"></Badge>
);
};
if (loading) {
return (
<Card>
<div className="flex items-center justify-center h-96">
<div className="text-muted-foreground">...</div>
</div>
</Card>
);
}
return (
<div className="space-y-4">
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
users.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
{user.avatarUrl && (
<img
src={user.avatarUrl}
alt={user.username}
className="w-8 h-8 rounded-full"
/>
)}
<span>{user.username}</span>
</div>
</TableCell>
<TableCell>
{user.fullName || user.displayName || '-'}
</TableCell>
<TableCell>{user.phone || '-'}</TableCell>
<TableCell className="text-muted-foreground">{user.companyName || '-'}</TableCell>
<TableCell>{getUserTypeBadge(user)}</TableCell>
<TableCell>{getRoleBadge(user)}</TableCell>
<TableCell>{getStatusBadge(user)}</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => onViewDetail(user)}
>
<Eye className="w-4 h-4" />
</Button>
{onEdit && (
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(user)}
>
<Edit className="w-4 h-4" />
</Button>
)}
{onResetPassword && (
<Button
variant="ghost"
size="sm"
onClick={() => onResetPassword(user)}
>
<Lock className="w-4 h-4" />
</Button>
)}
{onToggleStatus && (
<Button
variant="ghost"
size="sm"
onClick={() => onToggleStatus(user)}
>
{user.isActive ? (
<UserX className="w-4 h-4 text-orange-600" />
) : (
<UserCheck className="w-4 h-4 text-green-600" />
)}
</Button>
)}
{onDelete && !user.isSuperuser && (
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(user)}
>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</Card>
{/* 分页 */}
{pagination.total > 0 && (
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
{(pagination.page - 1) * pagination.size + 1} - {Math.min(pagination.page * pagination.size, pagination.total)} {pagination.total}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(pagination.page - 1)}
disabled={!pagination.hasPrev}
>
</Button>
<div className="flex items-center gap-1">
<span className="text-sm text-muted-foreground"></span>
<div className="flex items-center">
<input
type="number"
min={1}
max={pagination.totalPages}
value={pagination.page}
onChange={(e) => {
const newPage = parseInt(e.target.value);
if (!isNaN(newPage)) {
onPageChange(newPage);
}
}}
className="w-16 h-8 text-center border rounded-md"
/>
</div>
<span className="text-sm text-muted-foreground">/ {pagination.totalPages} </span>
</div>
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(pagination.page + 1)}
disabled={!pagination.hasNext}
>
</Button>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,62 +0,0 @@
'use client';
import React from 'react';
import { Card } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Search } from 'lucide-react';
import { UserFilters } from '../types';
interface UserManagementFiltersProps {
filters: UserFilters;
onSearchChange: (value: string) => void;
onStatusFilterChange: (value: string) => void;
onTypeFilterChange: (value: string) => void;
}
export function UserManagementFilters({
filters,
onSearchChange,
onStatusFilterChange,
onTypeFilterChange
}: UserManagementFiltersProps) {
return (
<Card className="p-4">
<div className="flex gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="搜索用户名、姓名、电话、企业..."
value={filters.searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10"
/>
</div>
</div>
<Select value={filters.typeFilter} onValueChange={onTypeFilterChange}>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="super_admin"></SelectItem>
<SelectItem value="enterprise_admin"></SelectItem>
<SelectItem value="employee"></SelectItem>
</SelectContent>
</Select>
<Select value={filters.statusFilter} onValueChange={onStatusFilterChange}>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="active"></SelectItem>
<SelectItem value="frozen"></SelectItem>
</SelectContent>
</Select>
</div>
</Card>
);
}

View File

@@ -8,6 +8,11 @@
import { getAuthToken } from "@/utils/token";
import {
listSystemUsersApiV1UsersSystemUsersGet,
createSystemUserApiV1UsersSystemUsersPost,
getSystemUserApiV1UsersSystemUsersUserIdGet,
activateSystemUserApiV1UsersSystemUsersUserIdActivatePost,
deactivateSystemUserApiV1UsersSystemUsersUserIdDeactivatePost,
deleteSystemUserApiV1UsersSystemUsersUserIdDelete,
} from "@/lib/api/sdk.gen";
// API返回的用户数据类型
@@ -191,4 +196,359 @@ export interface PaginationState {
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
}
// 创建用户请求参数接口
export interface CreateUserRequest {
email: string;
username: string;
full_name: string;
phone: string;
password: string;
is_superuser: boolean;
tenant_id?: string; // 系统管理员不传,企业管理员必传
}
// 创建用户响应数据类型
export interface CreateUserResponse {
id: string;
email: string;
username: string;
full_name: string;
phone: string;
is_superuser: boolean;
tenant_id?: string;
is_active: boolean;
created_at: string;
updated_at: string;
}
/**
* 创建系统用户(系统管理员或企业管理员)
*
* @param userData 用户创建数据
* @returns 创建成功的用户数据
*/
export async function createUser(userData: CreateUserRequest): Promise<CreateUserResponse> {
try {
console.log(`[API] createUser 创建用户:`, userData);
// 获取认证token
const token = getAuthToken();
// 构建请求参数
const requestData: any = {
email: userData.email,
username: userData.username,
full_name: userData.full_name,
phone: userData.phone,
password: userData.password,
is_superuser: userData.is_superuser,
};
// 只有企业管理员才传tenant_id
if (userData.tenant_id) {
requestData.tenant_id = userData.tenant_id;
}
// 调用SDK API创建系统用户
const response = await createSystemUserApiV1UsersSystemUsersPost({
body: requestData,
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
// 处理API错误提取错误信息
const errorMessage = response.error.message || '创建用户失败';
console.error('[API] createUser 创建用户失败:', response.error);
throw new Error(errorMessage);
}
const data = response.data as any;
console.log('[API] createUser 创建用户成功:', data);
// 返回创建成功的用户数据
return {
id: data.id,
email: data.email,
username: data.username,
full_name: data.full_name,
phone: data.phone,
is_superuser: data.is_superuser,
tenant_id: data.tenant_id,
is_active: data.is_active,
created_at: data.created_at,
updated_at: data.updated_at,
};
} catch (error) {
console.error('[API] createUser 创建用户异常:', error);
// 如果是已知错误,直接抛出
if (error instanceof Error) {
throw error;
}
// 未知错误包装成标准错误格式
throw new Error('创建用户失败,请稍后重试');
}
}
/**
* 验证用户创建数据
*
* @param userData 用户数据
* @returns 验证结果
*/
export function validateUserData(userData: CreateUserRequest): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
// 邮箱验证
if (!userData.email) {
errors.push('邮箱不能为空');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(userData.email)) {
errors.push('邮箱格式不正确');
}
// 用户名验证
if (!userData.username) {
errors.push('用户名不能为空');
} else if (userData.username.length < 3) {
errors.push('用户名长度至少3位');
}
// 姓名验证
if (!userData.full_name) {
errors.push('姓名不能为空');
}
// 电话验证
if (!userData.phone) {
errors.push('电话不能为空');
} else if (!/^1[3-9]\d{9}$/.test(userData.phone)) {
errors.push('请输入正确的手机号码');
}
// 密码验证
if (!userData.password) {
errors.push('密码不能为空');
} else if (userData.password.length < 6) {
errors.push('密码长度至少6位');
}
// 企业管理员需要tenant_id
if (!userData.is_superuser && !userData.tenant_id) {
errors.push('企业管理员必须选择所属企业');
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* 获取用户详情信息
*
* @param userId 用户ID
* @returns 用户详情数据
*/
export async function fetchUserDetails(userId: string): Promise<UserData> {
try {
console.log(`[API] fetchUserDetails 获取用户详情: ${userId}`);
// 获取认证token
const token = getAuthToken();
// 调用SDK API获取用户详情
const response = await getSystemUserApiV1UsersSystemUsersUserIdGet({
path: {
user_id: userId,
},
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
const errorMessage = response.error.message || '获取用户详情失败';
console.error('[API] fetchUserDetails 获取用户详情失败:', response.error);
throw new Error(errorMessage);
}
const data = response.data as any;
console.log('[API] fetchUserDetails 获取用户详情成功:', data);
// 返回用户详情数据
return {
id: data.id,
tenant_id: data.tenant_id,
email: data.email,
username: data.username,
full_name: data.full_name,
phone: data.phone,
is_active: data.is_active,
status: data.is_active ? 'active' : 'inactive',
is_superuser: data.is_superuser,
is_verified: data.is_verified,
created_at: data.created_at,
updated_at: data.updated_at,
last_login_at: data.last_login_at,
avatar_url: data.avatar_url,
bio: data.bio,
display_name: data.display_name,
department_id: data.department_id,
department_name: data.department_name,
scope: data.scope || 'system',
company_name: data.company_name,
};
} catch (error) {
console.error('[API] fetchUserDetails 获取用户详情异常:', error);
// 如果是已知错误,直接抛出
if (error instanceof Error) {
throw error;
}
// 未知错误包装成标准错误格式
throw new Error('获取用户详情失败,请稍后重试');
}
}
/**
* 激活系统用户
*
* @param userId 用户ID
* @returns 操作结果
*/
export async function activateUser(userId: string): Promise<void> {
try {
console.log(`[API] activateUser 激活用户: ${userId}`);
// 获取认证token
const token = getAuthToken();
// 调用SDK API激活用户
const response = await activateSystemUserApiV1UsersSystemUsersUserIdActivatePost({
path: {
user_id: userId,
},
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
// 处理API错误提取错误信息
const errorMessage = response.error.message || '激活用户失败';
console.error('[API] activateUser 激活用户失败:', response.error);
throw new Error(errorMessage);
}
console.log('[API] activateUser 激活用户成功:', userId);
} catch (error) {
console.error('[API] activateUser 激活用户异常:', error);
// 如果是已知错误,直接抛出
if (error instanceof Error) {
throw error;
}
// 未知错误包装成标准错误格式
throw new Error('激活用户失败,请稍后重试');
}
}
/**
* 停用系统用户
*
* @param userId 用户ID
* @returns 操作结果
*/
export async function deactivateUser(userId: string): Promise<void> {
try {
console.log(`[API] deactivateUser 停用用户: ${userId}`);
// 获取认证token
const token = getAuthToken();
// 调用SDK API停用用户
const response = await deactivateSystemUserApiV1UsersSystemUsersUserIdDeactivatePost({
path: {
user_id: userId,
},
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
// 处理API错误提取错误信息
const errorMessage = response.error.message || '停用用户失败';
console.error('[API] deactivateUser 停用用户失败:', response.error);
throw new Error(errorMessage);
}
console.log('[API] deactivateUser 停用用户成功:', userId);
} catch (error) {
console.error('[API] deactivateUser 停用用户异常:', error);
// 如果是已知错误,直接抛出
if (error instanceof Error) {
throw error;
}
// 未知错误包装成标准错误格式
throw new Error('停用用户失败,请稍后重试');
}
}
/**
* 删除系统用户
*
* @param userId 用户ID
* @returns 操作结果
*/
export async function deleteUser(userId: string): Promise<void> {
try {
console.log(`[API] deleteUser 删除用户: ${userId}`);
// 获取认证token
const token = getAuthToken();
// 调用SDK API删除用户
const response = await deleteSystemUserApiV1UsersSystemUsersUserIdDelete({
path: {
user_id: userId,
},
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
// 处理API错误提取错误信息
const errorMessage = response.error.message || '删除用户失败';
console.error('[API] deleteUser 删除用户失败:', response.error);
throw new Error(errorMessage);
}
console.log('[API] deleteUser 删除用户成功:', userId);
} catch (error) {
console.error('[API] deleteUser 删除用户异常:', error);
// 如果是已知错误,直接抛出
if (error instanceof Error) {
throw error;
}
// 未知错误包装成标准错误格式
throw new Error('删除用户失败,请稍后重试');
}
}

View File

@@ -9,13 +9,23 @@
import { useReducer, useEffect, useState, useCallback, useMemo,useRef } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Eye, Edit, Lock, UserX, UserCheck } from 'lucide-react';
import { Eye, Edit, Trash2, UserX, UserCheck } from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { UserDetailDialog } from './components/UserDetailDialog';
import { AddUserModal } from './components/AddUserModal';
import { EditUserModal } from './components/EditUserModal';
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, activateUser, deactivateUser, deleteUser } from './components/userManagementApi';
import { UserManagementHeader } from './components/UserManagementHeader';
import { UserManagementStatsCards } from './components/UserManagementStatsCards';
import { UserFilters } from './types';
@@ -113,6 +123,12 @@ const initialState: UserManagementState = {
export default function TenantUserManagementPage() {
const [state, dispatch] = useReducer(userManagementReducer, initialState);
// 弹窗状态管理
const [statusDialogOpen, setStatusDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [actionUser, setActionUser] = useState<User | null>(null);
const [actionType, setActionType] = useState<'activate' | 'deactivate'>('activate');
// 搜索字段配置
const searchFields: SearchFieldConfig[] = useMemo(() => [
{
@@ -274,10 +290,11 @@ export default function TenantUserManagementPage() {
<Button
variant="ghost"
size="sm"
onClick={() => handleResetPassword(user)}
title="重置密码"
onClick={() => handleDeleteUser(user)}
title="删除用户"
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950"
>
<Lock className="w-4 h-4" />
<Trash2 className="w-4 h-4" />
</Button>
</div>
),
@@ -487,18 +504,63 @@ export default function TenantUserManagementPage() {
loadUsers({});
}, [loadUsers]);
// 切换用户状态
// 切换用户状态 - 打开确认弹窗
const handleToggleStatus = (user: User) => {
const newStatus = !user.isActive;
const statusText = newStatus ? '激活' : '停用';
if (!confirm(`确定要${statusText}用户 ${user.fullName || user.username} 吗?`)) return;
toast.info(`${statusText}功能开发中...`);
setActionUser(user);
setActionType(newStatus ? 'activate' : 'deactivate');
setStatusDialogOpen(true);
};
// 重置密码
const handleResetPassword = (user: User) => {
if (!confirm(`确定要重置用户 ${user.fullName || user.username} 的密码吗?`)) return;
toast.info('重置密码功能开发中...');
// 执行状态切换
const handleStatusConfirm = async () => {
if (!actionUser) return;
try {
if (actionType === 'activate') {
await activateUser(actionUser.id);
toast.success(`用户 ${actionUser.fullName || actionUser.username} 已激活`);
} else {
await deactivateUser(actionUser.id);
toast.success(`用户 ${actionUser.fullName || actionUser.username} 已停用`);
}
// 刷新数据
refreshData();
} catch (error) {
console.error(`${actionType === 'activate' ? '激活' : '停用'}用户失败:`, error);
const errorMessage = error instanceof Error ? error.message : `${actionType === 'activate' ? '激活' : '停用'}用户失败,请重试`;
toast.error(errorMessage);
} finally {
setStatusDialogOpen(false);
setActionUser(null);
}
};
// 删除用户 - 打开确认弹窗
const handleDeleteUser = (user: User) => {
setActionUser(user);
setDeleteDialogOpen(true);
};
// 执行删除用户
const handleDeleteConfirm = async () => {
if (!actionUser) return;
try {
await deleteUser(actionUser.id);
toast.success(`用户 ${actionUser.fullName || actionUser.username} 已删除`);
// 刷新数据
refreshData();
} catch (error) {
console.error('删除用户失败:', error);
const errorMessage = error instanceof Error ? error.message : '删除用户失败,请重试';
toast.error(errorMessage);
} finally {
setDeleteDialogOpen(false);
setActionUser(null);
}
};
// 统计数据计算
@@ -589,6 +651,58 @@ export default function TenantUserManagementPage() {
user={state.selectedUser}
onSuccess={refreshData}
/>
{/* 状态切换确认对话框 */}
<AlertDialog open={statusDialogOpen} onOpenChange={setStatusDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{actionType === 'activate' ? '激活用户' : '停用用户'}
</AlertDialogTitle>
<AlertDialogDescription>
{actionType === 'activate' ? '激活' : '停用'}
<span className="font-semibold">
{actionUser?.fullName || actionUser?.username}
</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={handleStatusConfirm}
className={actionType === 'activate' ? 'bg-green-600 hover:bg-green-700' : 'bg-orange-600 hover:bg-orange-700'}
>
{actionType === 'activate' ? '激活' : '停用'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 删除用户确认对话框 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="text-red-600"></AlertDialogTitle>
<AlertDialogDescription>
<span className="font-semibold text-red-600">
{actionUser?.fullName || actionUser?.username}
</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteConfirm}
className="bg-red-600 hover:bg-red-700"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}