生产管理系统 - 用户管理接口集成
This commit is contained in:
@@ -1,112 +1,282 @@
|
||||
'use client';
|
||||
/**
|
||||
* filekorolheader: 用户详情对话框组件 - 用户详细信息展示界面
|
||||
* 功能:用户详细信息展示、多标签页布局、状态和权限信息
|
||||
* 路径:/central-config/tenant/user-management/components/UserDetailDialog
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { User } from '../types';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { User, UserStatus, UserType } from '../types';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { User, Mail, Phone, Calendar, Building, Shield, CheckCircle, XCircle, Clock } from 'lucide-react';
|
||||
|
||||
interface UserDetailDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
selectedUser: User | null;
|
||||
user: User | null;
|
||||
}
|
||||
|
||||
export function UserDetailDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
selectedUser
|
||||
user
|
||||
}: UserDetailDialogProps) {
|
||||
const getStatusBadge = (status: UserStatus) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return <Badge className="bg-green-100 text-green-700">正常</Badge>;
|
||||
case 'frozen':
|
||||
return <Badge className="bg-gray-100 text-gray-700">已冻结</Badge>;
|
||||
case 'inactive':
|
||||
return <Badge className="bg-red-100 text-red-700">停用</Badge>;
|
||||
default:
|
||||
return <Badge>{status}</Badge>;
|
||||
const getStatusBadge = (isActive: boolean) => {
|
||||
return isActive ? (
|
||||
<Badge className="bg-green-100 text-green-700">正常</Badge>
|
||||
) : (
|
||||
<Badge className="bg-red-100 text-red-700">停用</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const getRoleBadge = (isSuperuser: boolean) => {
|
||||
return isSuperuser ? (
|
||||
<Badge className="bg-purple-100 text-purple-700">超级管理员</Badge>
|
||||
) : (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).replace(/\//g, '-');
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
const getUserTypeBadge = (type: UserType) => {
|
||||
switch (type) {
|
||||
case 'super_admin':
|
||||
return <Badge className="bg-purple-100 text-purple-700">超级管理员</Badge>;
|
||||
case 'enterprise_admin':
|
||||
return <Badge className="bg-blue-100 text-blue-700">企业管理员</Badge>;
|
||||
case 'employee':
|
||||
return <Badge className="bg-green-100 text-green-700">员工</Badge>;
|
||||
default:
|
||||
return <Badge>{type}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedUser) return null;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogContent className="w-[80vw] max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>用户详情</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
查看用户的详细信息
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>用户名</Label>
|
||||
<div className="mt-1">{selectedUser.username}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>姓名</Label>
|
||||
<div className="mt-1">{selectedUser.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>电话</Label>
|
||||
<div className="mt-1">{selectedUser.phone}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>邮箱</Label>
|
||||
<div className="mt-1">{selectedUser.email || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>用户类型</Label>
|
||||
<div className="mt-1">{getUserTypeBadge(selectedUser.userType)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>所属企业</Label>
|
||||
<div className="mt-1">{selectedUser.enterpriseName || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>状态</Label>
|
||||
<div className="mt-1">{getStatusBadge(selectedUser.status)}</div>
|
||||
</div>
|
||||
{selectedUser.lastLoginTime && (
|
||||
<div>
|
||||
<Label>最后登录</Label>
|
||||
<div className="mt-1">
|
||||
{new Date(selectedUser.lastLoginTime).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Label>创建时间</Label>
|
||||
<div className="mt-1">
|
||||
{new Date(selectedUser.createdAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>更新时间</Label>
|
||||
<div className="mt-1">
|
||||
{new Date(selectedUser.updatedAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
<div className="flex items-center justify-between pr-8">
|
||||
<DialogTitle>用户详情</DialogTitle>
|
||||
<div className="flex gap-2">
|
||||
{getRoleBadge(user.isSuperuser)}
|
||||
{getVerifiedBadge(user.isVerified)}
|
||||
{getStatusBadge(user.isActive)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogDescription className="sr-only">
|
||||
查看用户的详细信息和权限
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="max-h-[calc(90vh-200px)]">
|
||||
<Tabs defaultValue="basic" className="space-y-4">
|
||||
<TabsList className="grid grid-cols-3 w-full">
|
||||
<TabsTrigger value="basic">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
基本信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="permissions">
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
权限信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="activity">
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
活动信息
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<TabsContent value="basic" className="space-y-4">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
{user.avatarUrl ? (
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
alt={user.username}
|
||||
className="w-16 h-16 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-full bg-gray-200 flex items-center justify-center">
|
||||
<User className="w-8 h-8 text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{user.displayName || user.fullName || user.username}</h3>
|
||||
<p className="text-sm text-muted-foreground">@{user.username}</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
{getRoleBadge(user.isSuperuser)}
|
||||
{getVerifiedBadge(user.isVerified)}
|
||||
{getStatusBadge(user.isActive)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>用户名</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{user.username}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>显示名称</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||
{user.displayName || user.fullName || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>邮箱</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md flex items-center gap-2">
|
||||
<Mail className="w-4 h-4 text-muted-foreground" />
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>电话</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md flex items-center gap-2">
|
||||
<Phone className="w-4 h-4 text-muted-foreground" />
|
||||
{user.phone || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>所属企业</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md flex items-center gap-2">
|
||||
<Building className="w-4 h-4 text-muted-foreground" />
|
||||
{user.companyName || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>权限范围</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||
{user.scope === 'tenant' ? '租户级' : user.scope || '-'}
|
||||
</div>
|
||||
</div>
|
||||
{user.departmentName && (
|
||||
<div>
|
||||
<Label>部门</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{user.departmentName}</div>
|
||||
</div>
|
||||
)}
|
||||
{user.bio && (
|
||||
<div className="col-span-2">
|
||||
<Label>个人简介</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md min-h-[80px] whitespace-pre-wrap">
|
||||
{user.bio}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 权限信息 */}
|
||||
<TabsContent value="permissions" className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">系统权限</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>超级管理员权限</span>
|
||||
{user.isSuperuser ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
) : (
|
||||
<XCircle className="w-5 h-5 text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>邮箱已验证</span>
|
||||
{user.isVerified ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
) : (
|
||||
<XCircle className="w-5 h-5 text-orange-600" />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">访问状态</h4>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>账户状态</span>
|
||||
{getStatusBadge(user.isActive)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{user.tenantId && (
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">关联信息</h4>
|
||||
<div className="space-y-4">
|
||||
<Card className="p-4">
|
||||
<Label>租户ID</Label>
|
||||
<div className="mt-1.5 text-sm font-mono bg-gray-100 p-2 rounded">
|
||||
{user.tenantId}
|
||||
</div>
|
||||
</Card>
|
||||
{user.departmentId && (
|
||||
<Card className="p-4">
|
||||
<Label>部门ID</Label>
|
||||
<div className="mt-1.5 text-sm font-mono bg-gray-100 p-2 rounded">
|
||||
{user.departmentId}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 活动信息 */}
|
||||
<TabsContent value="activity" className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">时间信息</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Card className="p-4">
|
||||
<Label className="text-xs">创建时间</Label>
|
||||
<div className="mt-1.5 text-sm">{formatDate(user.createdAt)}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<Label className="text-xs">更新时间</Label>
|
||||
<div className="mt-1.5 text-sm">{formatDate(user.updatedAt)}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<Label className="text-xs">最后登录时间</Label>
|
||||
<div className="mt-1.5 text-sm">
|
||||
{user.lastLoginAt ? formatDate(user.lastLoginAt) : '从未登录'}
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<Label className="text-xs">账户状态</Label>
|
||||
<div className="mt-1.5">
|
||||
{getStatusBadge(user.isActive)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
|
||||
@@ -1,140 +1,245 @@
|
||||
'use client';
|
||||
/**
|
||||
* filekorolheader: 用户列表组件 - 用户数据表格展示界面
|
||||
* 功能:用户数据表格展示、状态徽章、操作按钮、分页功能
|
||||
* 路径:/central-config/tenant/user-management/components/UserList
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
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, Lock, Trash2, UserX, UserCheck } from 'lucide-react';
|
||||
import { User, UserStatus, UserType } from '../types';
|
||||
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;
|
||||
onResetPassword: (user: User) => void;
|
||||
onToggleStatus: (user: User) => void;
|
||||
onDelete: (id: string) => 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,
|
||||
onResetPassword,
|
||||
onDelete,
|
||||
onToggleStatus,
|
||||
onDelete
|
||||
onResetPassword
|
||||
}: UserListProps) {
|
||||
const getStatusBadge = (status: UserStatus) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return <Badge className="bg-green-100 text-green-700">正常</Badge>;
|
||||
case 'frozen':
|
||||
return <Badge className="bg-gray-100 text-gray-700">已冻结</Badge>;
|
||||
case 'inactive':
|
||||
return <Badge className="bg-red-100 text-red-700">停用</Badge>;
|
||||
default:
|
||||
return <Badge>{status}</Badge>;
|
||||
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 = (type: UserType) => {
|
||||
switch (type) {
|
||||
case 'super_admin':
|
||||
return <Badge className="bg-purple-100 text-purple-700">超级管理员</Badge>;
|
||||
case 'enterprise_admin':
|
||||
return <Badge className="bg-blue-100 text-blue-700">企业管理员</Badge>;
|
||||
case 'employee':
|
||||
return <Badge className="bg-green-100 text-green-700">员工</Badge>;
|
||||
default:
|
||||
return <Badge>{type}</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 (
|
||||
<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 ? (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center text-muted-foreground py-8">
|
||||
暂无数据
|
||||
</TableCell>
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>姓名</TableHead>
|
||||
<TableHead>电话</TableHead>
|
||||
<TableHead>所属企业</TableHead>
|
||||
<TableHead>用户类型</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
) : (
|
||||
users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="text-muted-foreground">{user.username}</TableCell>
|
||||
<TableCell>{user.name}</TableCell>
|
||||
<TableCell>{user.phone}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{user.enterpriseName || '-'}</TableCell>
|
||||
<TableCell>{getUserTypeBadge(user.userType)}</TableCell>
|
||||
<TableCell>
|
||||
{user.roles && user.roles.length > 0 ? user.roles.join(', ') : '-'}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center text-muted-foreground py-8">
|
||||
暂无用户数据
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(user.status)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewDetail(user)}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onEdit(user)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onResetPassword(user)}
|
||||
>
|
||||
<Lock className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onToggleStatus(user)}
|
||||
>
|
||||
{user.status === 'active' ? (
|
||||
<UserX className="w-4 h-4 text-orange-600" />
|
||||
) : (
|
||||
<UserCheck className="w-4 h-4 text-green-600" />
|
||||
</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"
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
{user.userType !== 'super_admin' && (
|
||||
<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={() => onDelete(user.id)}
|
||||
onClick={() => onViewDetail(user)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-destructive" />
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@@ -9,16 +9,17 @@ import { UserFilters } from '../types';
|
||||
|
||||
interface UserManagementFiltersProps {
|
||||
filters: UserFilters;
|
||||
onFiltersChange: (filters: UserFilters) => void;
|
||||
onSearchChange: (value: string) => void;
|
||||
onStatusFilterChange: (value: string) => void;
|
||||
onTypeFilterChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export function UserManagementFilters({ filters, onFiltersChange }: UserManagementFiltersProps) {
|
||||
const updateFilter = (key: keyof UserFilters, value: string) => {
|
||||
onFiltersChange({
|
||||
...filters,
|
||||
[key]: value
|
||||
});
|
||||
};
|
||||
export function UserManagementFilters({
|
||||
filters,
|
||||
onSearchChange,
|
||||
onStatusFilterChange,
|
||||
onTypeFilterChange
|
||||
}: UserManagementFiltersProps) {
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
@@ -29,12 +30,12 @@ export function UserManagementFilters({ filters, onFiltersChange }: UserManageme
|
||||
<Input
|
||||
placeholder="搜索用户名、姓名、电话、企业..."
|
||||
value={filters.searchKeyword}
|
||||
onChange={(e) => updateFilter('searchKeyword', e.target.value)}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Select value={filters.typeFilter} onValueChange={(value) => updateFilter('typeFilter', value)}>
|
||||
<Select value={filters.typeFilter} onValueChange={onTypeFilterChange}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -45,7 +46,7 @@ export function UserManagementFilters({ filters, onFiltersChange }: UserManageme
|
||||
<SelectItem value="employee">员工</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={filters.statusFilter} onValueChange={(value) => updateFilter('statusFilter', value)}>
|
||||
<Select value={filters.statusFilter} onValueChange={onStatusFilterChange}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -1,31 +1,58 @@
|
||||
/**
|
||||
* filekorolheader: 用户管理页面头部组件 - 页面标题和操作按钮
|
||||
* 功能:页面标题显示、刷新功能、统计数据展示
|
||||
* 路径:/central-config/tenant/user-management/components/UserManagementHeader
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Plus, Download } from 'lucide-react';
|
||||
import { Users, RefreshCw } from 'lucide-react';
|
||||
|
||||
interface UserManagementHeaderProps {
|
||||
onAddUser: () => void;
|
||||
onExport: () => void;
|
||||
stats: Array<{
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
bg: string;
|
||||
}>;
|
||||
onRefresh: () => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export function UserManagementHeader({ onAddUser, onExport }: UserManagementHeaderProps) {
|
||||
export function UserManagementHeader({ stats, onRefresh, loading }: UserManagementHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800">用户管理</h2>
|
||||
<p className="text-muted-foreground">平台所有用户账户的集中管理</p>
|
||||
<Card className="p-6 bg-gradient-to-r from-blue-50 dark:from-blue-950 to-indigo-50 dark:to-indigo-950 border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<Users className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-1" />
|
||||
<div className="flex-1">
|
||||
<h2 className="mb-2">用户管理</h2>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
平台所有用户账户的集中管理,支持搜索、筛选和详情查看
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="inline-flex items-center text-sm bg-white dark:bg-gray-800 px-3 py-1 rounded-full">
|
||||
搜索功能
|
||||
</span>
|
||||
<span className="inline-flex items-center text-sm bg-white dark:bg-gray-800 px-3 py-1 rounded-full">
|
||||
状态筛选
|
||||
</span>
|
||||
<span className="inline-flex items-center text-sm bg-white dark:bg-gray-800 px-3 py-1 rounded-full">
|
||||
详情查看
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={onRefresh} disabled={loading}>
|
||||
<RefreshCw className={`w-4 h-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={onExport}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出
|
||||
</Button>
|
||||
<Button onClick={onAddUser}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加用户
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +1,49 @@
|
||||
/**
|
||||
* filekorolheader: 用户管理统计卡片组件 - 用户统计数据展示界面
|
||||
* 功能:总用户数、活跃用户、管理员、已验证用户统计展示
|
||||
* 路径:/central-config/tenant/user-management/components/UserManagementStatsCards
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { UserManagementStats, User } from '../types';
|
||||
|
||||
interface UserManagementStatsCardsProps {
|
||||
users: User[];
|
||||
stats: Array<{
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
bg: string;
|
||||
}>;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function UserManagementStatsCards({ users }: UserManagementStatsCardsProps) {
|
||||
const stats: UserManagementStats[] = [
|
||||
{
|
||||
label: '总用户数',
|
||||
value: users.length,
|
||||
color: 'text-blue-600',
|
||||
bg: 'bg-blue-100',
|
||||
},
|
||||
{
|
||||
label: '超级管理员',
|
||||
value: users.filter(u => u.userType === 'super_admin').length,
|
||||
color: 'text-purple-600',
|
||||
bg: 'bg-purple-100',
|
||||
},
|
||||
{
|
||||
label: '企业管理员',
|
||||
value: users.filter(u => u.userType === 'enterprise_admin').length,
|
||||
color: 'text-blue-600',
|
||||
bg: 'bg-blue-100',
|
||||
},
|
||||
{
|
||||
label: '正常用户',
|
||||
value: users.filter(u => u.status === 'active').length,
|
||||
color: 'text-green-600',
|
||||
bg: 'bg-green-100',
|
||||
},
|
||||
];
|
||||
export function UserManagementStatsCards({
|
||||
stats,
|
||||
loading = false
|
||||
}: UserManagementStatsCardsProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{stats.map((_, index) => (
|
||||
<Card key={index} className="p-4">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 bg-gray-200 rounded mb-2"></div>
|
||||
<div className="h-8 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{stats.map((stat, index) => (
|
||||
<Card key={index} className="p-4">
|
||||
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
||||
<div className={`mt-2 ${stat.color} text-2xl font-semibold`}>{stat.value}</div>
|
||||
<div className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* filekorolheader: 用户管理API接口 - 用户数据查询接口服务
|
||||
* 功能:API请求封装、数据转换、错误处理、分页查询
|
||||
* 路径:/central-config/tenant/user-management/components/userManagementApi
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用SDK API调用,TypeScript类型安全
|
||||
*/
|
||||
|
||||
import { getAuthToken } from "@/utils/token";
|
||||
import {
|
||||
getUsersApiV1UsersGet,
|
||||
} from "@/lib/api/sdk.gen";
|
||||
|
||||
// API返回的用户数据类型
|
||||
export interface UserData {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
full_name: string | null;
|
||||
phone: string | null;
|
||||
is_active: boolean;
|
||||
is_superuser: boolean;
|
||||
is_verified: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
last_login_at: string | null;
|
||||
avatar_url: string | null;
|
||||
bio: string | null;
|
||||
display_name: string | null;
|
||||
department_id: string | null;
|
||||
department_name: string | null;
|
||||
scope: string;
|
||||
company_name: string | null;
|
||||
}
|
||||
|
||||
// API响应接口
|
||||
export interface UsersApiResponse {
|
||||
data: UserData[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
total_pages: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
}
|
||||
|
||||
// 查询参数接口
|
||||
export interface UsersQueryParams {
|
||||
search?: string;
|
||||
is_active?: boolean;
|
||||
page?: number;
|
||||
size?: number;
|
||||
order_by?: string;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// 页面使用的用户数据类型(转换后的)
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
fullName: string | null;
|
||||
phone: string | null;
|
||||
isActive: boolean;
|
||||
isSuperuser: boolean;
|
||||
isVerified: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastLoginAt: string | null;
|
||||
avatarUrl: string | null;
|
||||
bio: string | null;
|
||||
displayName: string | null;
|
||||
departmentId: string | null;
|
||||
departmentName: string | null;
|
||||
scope: string;
|
||||
companyName: string | null;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表数据
|
||||
*/
|
||||
export async function fetchUsers(params: UsersQueryParams = {}): Promise<UsersApiResponse> {
|
||||
try {
|
||||
// 构建查询参数对象
|
||||
const queryParams: any = {};
|
||||
|
||||
if (params.search) queryParams.search = params.search;
|
||||
if (params.is_active !== undefined) queryParams.is_active = params.is_active;
|
||||
if (params.page) queryParams.page = params.page;
|
||||
if (params.size) queryParams.size = params.size;
|
||||
if (params.order_by) queryParams.order_by = params.order_by;
|
||||
if (params.sort_order) queryParams.sort_order = params.sort_order;
|
||||
|
||||
// 默认参数
|
||||
if (!params.page) queryParams.page = 1;
|
||||
if (!params.size) queryParams.size = 10;
|
||||
if (!params.sort_order) queryParams.sort_order = 'desc';
|
||||
|
||||
// 使用SDK API调用用户查询接口,添加缓存破坏器和认证头部
|
||||
const token = getAuthToken();
|
||||
console.log('用户管理API调用参数:', queryParams);
|
||||
|
||||
const response = await getUsersApiV1UsersGet({
|
||||
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响应:', data);
|
||||
|
||||
// 转换响应数据格式以匹配现有的接口
|
||||
// API返回的数据结构: { data: [...], total: 25, page: 1, size: 10, ... }
|
||||
return {
|
||||
data: data?.data || [], // 注意:实际数据在 data.data 中
|
||||
total: data?.total || 0,
|
||||
page: data?.page || 1,
|
||||
size: data?.size || 10,
|
||||
total_pages: data?.total_pages || 0,
|
||||
has_next: data?.has_next || false,
|
||||
has_prev: data?.has_prev || false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch users:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将API数据转换为页面所需的用户数据格式
|
||||
* 优先显示display_name,其次full_name,最后username
|
||||
*/
|
||||
export function transformUserData(user: UserData): User {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
fullName: user.full_name,
|
||||
phone: user.phone,
|
||||
isActive: user.is_active,
|
||||
isSuperuser: user.is_superuser,
|
||||
isVerified: user.is_verified,
|
||||
createdAt: formatDate(user.created_at),
|
||||
updatedAt: formatDate(user.updated_at),
|
||||
lastLoginAt: user.last_login_at ? formatDate(user.last_login_at) : null,
|
||||
avatarUrl: user.avatar_url,
|
||||
bio: user.bio,
|
||||
displayName: user.display_name || user.full_name || user.username,
|
||||
departmentId: user.department_id,
|
||||
departmentName: user.department_name,
|
||||
scope: user.scope,
|
||||
companyName: user.company_name,
|
||||
tenantId: user.tenant_id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
function formatDate(dateString: string): string {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).replace(/\//g, '-');
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination state interface for page components
|
||||
export interface PaginationState {
|
||||
page: number;
|
||||
size: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
}
|
||||
@@ -1,271 +1,330 @@
|
||||
/**
|
||||
* filekorolheader: 用户管理页面 - 用户查询和管理页面
|
||||
* 功能:用户列表查询、搜索筛选、详情查看、用户管理
|
||||
* 路径:/central-config/tenant/user-management
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,shadcn语义化样式
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useReducer, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { fetchUsers, transformUserData, UsersQueryParams, User, UsersApiResponse, PaginationState } from './components/userManagementApi';
|
||||
import { UserManagementHeader } from './components/UserManagementHeader';
|
||||
import { UserManagementStatsCards } from './components/UserManagementStatsCards';
|
||||
import { UserManagementFilters } from './components/UserManagementFilters';
|
||||
import { UserList } from './components/UserList';
|
||||
import { UserFormDialog } from './components/UserFormDialog';
|
||||
import { UserDetailDialog } from './components/UserDetailDialog';
|
||||
import { User, Enterprise, UserFilters, UserFormData } from './types';
|
||||
import { Enterprise, UserFilters } from './types';
|
||||
|
||||
export default function TenantUserManagementPage() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [enterprises, setEnterprises] = useState<Enterprise[]>([]);
|
||||
const [filters, setFilters] = useState<UserFilters>({
|
||||
// 用户管理状态管理
|
||||
interface UserManagementState {
|
||||
users: User[];
|
||||
enterprises: Enterprise[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
pagination: PaginationState;
|
||||
filters: UserFilters;
|
||||
sortBy?: string;
|
||||
sortOrder: 'asc' | 'desc';
|
||||
selectedUser: User | null;
|
||||
showDetailDialog: boolean;
|
||||
}
|
||||
|
||||
type UserManagementAction =
|
||||
| { type: 'SET_USERS'; payload: { data: User[]; pagination: PaginationState } }
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
| { type: 'SET_ERROR'; payload: string | null }
|
||||
| { type: 'SET_FILTERS'; payload: Partial<UserFilters> }
|
||||
| { type: 'SET_SORT'; payload: { sortBy?: string; sortOrder: 'asc' | 'desc' } }
|
||||
| { type: 'SET_PAGINATION'; payload: Partial<PaginationState> }
|
||||
| { type: 'SET_ENTERPRISES'; payload: Enterprise[] }
|
||||
| { type: 'SET_SELECTED_USER'; payload: User | null }
|
||||
| { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean }
|
||||
| { type: 'REFRESH_DATA' };
|
||||
|
||||
const userManagementReducer = (state: UserManagementState, action: UserManagementAction): UserManagementState => {
|
||||
switch (action.type) {
|
||||
case 'SET_USERS':
|
||||
return {
|
||||
...state,
|
||||
users: action.payload.data,
|
||||
pagination: action.payload.pagination,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
case 'SET_LOADING':
|
||||
return { ...state, loading: action.payload };
|
||||
case 'SET_ERROR':
|
||||
return { ...state, error: action.payload, loading: false };
|
||||
case 'SET_FILTERS':
|
||||
return { ...state, filters: { ...state.filters, ...action.payload } };
|
||||
case 'SET_SORT':
|
||||
return { ...state, sortBy: action.payload.sortBy, sortOrder: action.payload.sortOrder };
|
||||
case 'SET_PAGINATION':
|
||||
return { ...state, pagination: { ...state.pagination, ...action.payload } };
|
||||
case 'SET_ENTERPRISES':
|
||||
return { ...state, enterprises: action.payload };
|
||||
case 'SET_SELECTED_USER':
|
||||
return { ...state, selectedUser: action.payload };
|
||||
case 'TOGGLE_DETAIL_DIALOG':
|
||||
return { ...state, showDetailDialog: !state.showDetailDialog };
|
||||
case 'REFRESH_DATA':
|
||||
return { ...state, error: null };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const initialState: UserManagementState = {
|
||||
users: [],
|
||||
enterprises: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
pagination: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
},
|
||||
filters: {
|
||||
searchKeyword: '',
|
||||
statusFilter: 'all',
|
||||
typeFilter: 'all'
|
||||
});
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
const [formData, setFormData] = useState<UserFormData>({
|
||||
userType: 'enterprise_admin',
|
||||
status: 'active',
|
||||
roleIds: [],
|
||||
});
|
||||
},
|
||||
sortBy: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
selectedUser: null,
|
||||
showDetailDialog: false,
|
||||
};
|
||||
|
||||
export default function TenantUserManagementPage() {
|
||||
const [state, dispatch] = useReducer(userManagementReducer, initialState);
|
||||
|
||||
// 加载用户数据
|
||||
const loadUsers = async (resetPage = false) => {
|
||||
try {
|
||||
dispatch({ type: 'SET_LOADING', payload: true });
|
||||
|
||||
const params: UsersQueryParams = {
|
||||
search: state.filters.searchKeyword || undefined,
|
||||
page: resetPage ? 1 : state.pagination.page,
|
||||
size: state.pagination.size,
|
||||
order_by: state.sortBy,
|
||||
sort_order: state.sortOrder,
|
||||
};
|
||||
|
||||
// 根据状态筛选器设置 is_active 参数
|
||||
if (state.filters.statusFilter === 'active') {
|
||||
params.is_active = true;
|
||||
} else if (state.filters.statusFilter === 'inactive') {
|
||||
params.is_active = false;
|
||||
}
|
||||
|
||||
const response: UsersApiResponse = await fetchUsers(params);
|
||||
const transformedUsers = response.data.map(transformUserData);
|
||||
|
||||
dispatch({
|
||||
type: 'SET_USERS',
|
||||
payload: {
|
||||
data: transformedUsers,
|
||||
pagination: {
|
||||
page: response.page,
|
||||
size: response.size,
|
||||
total: response.total,
|
||||
totalPages: response.total_pages,
|
||||
hasNext: response.has_next,
|
||||
hasPrev: response.has_prev,
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load users:', error);
|
||||
dispatch({
|
||||
type: 'SET_ERROR',
|
||||
payload: error instanceof Error ? error.message : '加载用户数据失败'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 加载企业数据(这里暂时使用mock数据,后续可以添加企业API)
|
||||
const loadEnterprises = () => {
|
||||
// 这里可以添加企业API调用,现在使用mock数据
|
||||
const mockEnterprises: Enterprise[] = [
|
||||
{ id: 'ent-1', name: '丰收现代农业集团' },
|
||||
{ id: 'ent-2', name: '绿色种植科技有限公司' },
|
||||
{ id: 'ent-3', name: '智慧农业示范区' },
|
||||
];
|
||||
dispatch({ type: 'SET_ENTERPRISES', payload: mockEnterprises });
|
||||
};
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (value: string) => {
|
||||
dispatch({ type: 'SET_FILTERS', payload: { searchKeyword: value } });
|
||||
};
|
||||
|
||||
// 状态筛选
|
||||
const handleStatusFilter = (value: string) => {
|
||||
dispatch({ type: 'SET_FILTERS', payload: { statusFilter: value } });
|
||||
};
|
||||
|
||||
// 类型筛选
|
||||
const handleTypeFilter = (value: string) => {
|
||||
dispatch({ type: 'SET_FILTERS', payload: { typeFilter: value } });
|
||||
};
|
||||
|
||||
// 排序处理
|
||||
const handleSort = (sortBy: string) => {
|
||||
const newSortOrder = state.sortBy === sortBy && state.sortOrder === 'desc' ? 'asc' : 'desc';
|
||||
dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder: newSortOrder } });
|
||||
};
|
||||
|
||||
// 分页处理
|
||||
const handlePageChange = (page: number) => {
|
||||
// 边界检查,确保页码在有效范围内
|
||||
if (page < 1) {
|
||||
page = 1;
|
||||
} else if (page > state.pagination.totalPages && state.pagination.totalPages > 0) {
|
||||
page = state.pagination.totalPages;
|
||||
}
|
||||
dispatch({ type: 'SET_PAGINATION', payload: { page } });
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = (user: User) => {
|
||||
dispatch({ type: 'SET_SELECTED_USER', payload: user });
|
||||
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: true });
|
||||
};
|
||||
|
||||
// 编辑用户
|
||||
const handleEdit = (user: User) => {
|
||||
// 这里可以添加编辑逻辑,比如打开编辑对话框
|
||||
toast.info('编辑功能开发中...');
|
||||
};
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = (user: User) => {
|
||||
if (!confirm(`确定要删除用户 ${user.fullName || user.username} 吗?`)) return;
|
||||
// 这里可以添加删除逻辑,调用API删除用户
|
||||
toast.info('删除功能开发中...');
|
||||
};
|
||||
|
||||
// 切换用户状态
|
||||
const handleToggleStatus = (user: User) => {
|
||||
const newStatus = !user.isActive;
|
||||
const statusText = newStatus ? '激活' : '停用';
|
||||
if (!confirm(`确定要${statusText}用户 ${user.fullName || user.username} 吗?`)) return;
|
||||
// 这里可以添加状态切换逻辑,调用API更新用户状态
|
||||
toast.info(`${statusText}功能开发中...`);
|
||||
};
|
||||
|
||||
// 重置密码
|
||||
const handleResetPassword = (user: User) => {
|
||||
if (!confirm(`确定要重置用户 ${user.fullName || user.username} 的密码吗?`)) return;
|
||||
// 这里可以添加重置密码逻辑,调用API重置密码
|
||||
toast.info('重置密码功能开发中...');
|
||||
};
|
||||
|
||||
// 刷新数据
|
||||
const handleRefresh = () => {
|
||||
dispatch({ type: 'REFRESH_DATA' });
|
||||
loadUsers(true);
|
||||
toast.success('数据已刷新');
|
||||
};
|
||||
|
||||
// 统计数据计算
|
||||
const stats = [
|
||||
{
|
||||
label: '总用户数',
|
||||
value: state.pagination.total,
|
||||
color: 'text-blue-600',
|
||||
bg: 'bg-blue-100',
|
||||
},
|
||||
{
|
||||
label: '活跃用户',
|
||||
value: state.users.filter(u => u.isActive).length,
|
||||
color: 'text-green-600',
|
||||
bg: 'bg-green-100',
|
||||
},
|
||||
{
|
||||
label: '管理员',
|
||||
value: state.users.filter(u => u.isSuperuser).length,
|
||||
color: 'text-purple-600',
|
||||
bg: 'bg-purple-100',
|
||||
},
|
||||
{
|
||||
label: '已验证',
|
||||
value: state.users.filter(u => u.isVerified).length,
|
||||
color: 'text-orange-600',
|
||||
bg: 'bg-orange-100',
|
||||
},
|
||||
];
|
||||
|
||||
// 初始化和监听器
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
loadEnterprises();
|
||||
}, []);
|
||||
|
||||
const loadEnterprises = () => {
|
||||
const data = localStorage.getItem('smart_agriculture_enterprises');
|
||||
if (data) {
|
||||
const allEnterprises = JSON.parse(data);
|
||||
setEnterprises(allEnterprises.map((e: any) => ({ id: e.id, name: e.name })));
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
loadUsers();
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [state.filters.searchKeyword, state.filters.statusFilter, state.filters.typeFilter, state.sortBy, state.sortOrder]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.pagination.page > 1) {
|
||||
loadUsers();
|
||||
}
|
||||
};
|
||||
|
||||
const loadUsers = () => {
|
||||
const data = localStorage.getItem('smart_agriculture_users');
|
||||
if (data) {
|
||||
setUsers(JSON.parse(data));
|
||||
} else {
|
||||
// 初始化示例数据
|
||||
const mockUsers: User[] = [
|
||||
{
|
||||
id: 'user-1',
|
||||
username: 'admin',
|
||||
name: '系统管理员',
|
||||
phone: '13900000000',
|
||||
email: 'admin@system.com',
|
||||
userType: 'super_admin',
|
||||
roleIds: ['role-1'],
|
||||
roles: ['超级管理员'],
|
||||
status: 'active',
|
||||
createdAt: '2024-01-01T00:00:00',
|
||||
updatedAt: '2024-01-01T00:00:00',
|
||||
lastLoginTime: '2024-10-14T10:00:00',
|
||||
},
|
||||
{
|
||||
id: 'user-2',
|
||||
username: 'ent_admin_1',
|
||||
name: '李总',
|
||||
phone: '13900139002',
|
||||
email: 'litotal@fengshou.com',
|
||||
enterpriseId: 'ent-2',
|
||||
enterpriseName: '丰收现代农业集团',
|
||||
userType: 'enterprise_admin',
|
||||
roleIds: ['role-2'],
|
||||
roles: ['企业管理员'],
|
||||
status: 'active',
|
||||
createdAt: '2024-10-05T10:00:00',
|
||||
updatedAt: '2024-10-05T10:00:00',
|
||||
lastLoginTime: '2024-10-14T09:00:00',
|
||||
},
|
||||
{
|
||||
id: 'user-3',
|
||||
username: 'zhangsan',
|
||||
name: '张三',
|
||||
phone: '13800138001',
|
||||
email: 'zhangsan@fengshou.com',
|
||||
enterpriseId: 'ent-2',
|
||||
enterpriseName: '丰收现代农业集团',
|
||||
userType: 'employee',
|
||||
roleIds: ['role-3'],
|
||||
roles: ['操作员'],
|
||||
status: 'active',
|
||||
createdAt: '2024-10-01T08:00:00',
|
||||
updatedAt: '2024-10-01T08:00:00',
|
||||
lastLoginTime: '2024-10-14T08:30:00',
|
||||
},
|
||||
];
|
||||
localStorage.setItem('smart_agriculture_users', JSON.stringify(mockUsers));
|
||||
setUsers(mockUsers);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredUsers = users.filter(user => {
|
||||
const matchKeyword = !filters.searchKeyword ||
|
||||
user.name.includes(filters.searchKeyword) ||
|
||||
user.username.includes(filters.searchKeyword) ||
|
||||
user.phone.includes(filters.searchKeyword) ||
|
||||
(user.enterpriseName && user.enterpriseName.includes(filters.searchKeyword));
|
||||
|
||||
const matchStatus = filters.statusFilter === 'all' || user.status === filters.statusFilter;
|
||||
const matchType = filters.typeFilter === 'all' || user.userType === filters.typeFilter;
|
||||
|
||||
return matchKeyword && matchStatus && matchType;
|
||||
});
|
||||
|
||||
const handleAddUser = () => {
|
||||
setEditingUser(null);
|
||||
setFormData({
|
||||
userType: 'enterprise_admin',
|
||||
status: 'active',
|
||||
roleIds: [],
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleEdit = (user: User) => {
|
||||
setEditingUser(user);
|
||||
setFormData(user);
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!formData.username || !formData.name || !formData.phone) {
|
||||
toast.error('请填写必填项');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是企业管理员,必须选择企业
|
||||
if (formData.userType === 'enterprise_admin' && !formData.enterpriseId) {
|
||||
toast.error('企业管理员必须选择所属企业');
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据选择的企业ID设置企业名称
|
||||
let enterpriseName = formData.enterpriseName;
|
||||
if (formData.enterpriseId && !enterpriseName) {
|
||||
const selectedEnterprise = enterprises.find(e => e.id === formData.enterpriseId);
|
||||
if (selectedEnterprise) {
|
||||
enterpriseName = selectedEnterprise.name;
|
||||
}
|
||||
}
|
||||
|
||||
if (editingUser) {
|
||||
const updated = users.map(user =>
|
||||
user.id === editingUser.id
|
||||
? {
|
||||
...user,
|
||||
...formData,
|
||||
enterpriseName,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
: user
|
||||
);
|
||||
setUsers(updated);
|
||||
localStorage.setItem('smart_agriculture_users', JSON.stringify(updated));
|
||||
toast.success('用户信息更新成功');
|
||||
} else {
|
||||
const newUser: User = {
|
||||
id: `user-${Date.now()}`,
|
||||
...formData as User,
|
||||
enterpriseName,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const updated = [...users, newUser];
|
||||
setUsers(updated);
|
||||
localStorage.setItem('smart_agriculture_users', JSON.stringify(updated));
|
||||
toast.success('用户添加成功');
|
||||
}
|
||||
|
||||
setShowForm(false);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (!confirm('确定要删除该用户吗?')) return;
|
||||
|
||||
const updated = users.filter(user => user.id !== id);
|
||||
setUsers(updated);
|
||||
localStorage.setItem('smart_agriculture_users', JSON.stringify(updated));
|
||||
toast.success('用户删除成功');
|
||||
};
|
||||
|
||||
const handleToggleStatus = (user: User) => {
|
||||
const newStatus = user.status === 'active' ? 'frozen' : 'active';
|
||||
const updated = users.map(u =>
|
||||
u.id === user.id
|
||||
? { ...u, status: newStatus, updatedAt: new Date().toISOString() }
|
||||
: u
|
||||
);
|
||||
setUsers(updated);
|
||||
localStorage.setItem('smart_agriculture_users', JSON.stringify(updated));
|
||||
toast.success(newStatus === 'active' ? '账户已激活' : '账户已冻结');
|
||||
};
|
||||
|
||||
const handleResetPassword = (user: User) => {
|
||||
if (!confirm(`确定要重置 ${user.name} 的密码吗?`)) return;
|
||||
toast.success('密码已重置为:123456');
|
||||
};
|
||||
|
||||
const handleViewDetail = (user: User) => {
|
||||
setSelectedUser(user);
|
||||
setShowDetailDialog(true);
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
const dataStr = JSON.stringify(users, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `users_${new Date().getTime()}.json`;
|
||||
link.click();
|
||||
toast.success('用户数据导出成功');
|
||||
};
|
||||
}, [state.pagination.page]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<UserManagementHeader
|
||||
onAddUser={handleAddUser}
|
||||
onExport={handleExport}
|
||||
/>
|
||||
{/* 页面标题和统计 */}
|
||||
<UserManagementHeader stats={stats} onRefresh={handleRefresh} loading={state.loading} />
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<UserManagementStatsCards users={users} />
|
||||
<UserManagementStatsCards stats={stats} />
|
||||
|
||||
{/* 搜索和筛选 */}
|
||||
<UserManagementFilters
|
||||
filters={filters}
|
||||
onFiltersChange={setFilters}
|
||||
filters={state.filters}
|
||||
onSearchChange={handleSearch}
|
||||
onStatusFilterChange={handleStatusFilter}
|
||||
onTypeFilterChange={handleTypeFilter}
|
||||
/>
|
||||
|
||||
{/* 错误显示 */}
|
||||
{state.error && (
|
||||
<div className="p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
|
||||
<span>{state.error}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 用户列表 */}
|
||||
<UserList
|
||||
users={filteredUsers}
|
||||
users={state.users}
|
||||
loading={state.loading}
|
||||
pagination={state.pagination}
|
||||
onPageChange={handlePageChange}
|
||||
onViewDetail={handleViewDetail}
|
||||
onEdit={handleEdit}
|
||||
onResetPassword={handleResetPassword}
|
||||
onToggleStatus={handleToggleStatus}
|
||||
onDelete={handleDelete}
|
||||
onToggleStatus={handleToggleStatus}
|
||||
onResetPassword={handleResetPassword}
|
||||
/>
|
||||
|
||||
{/* 添加/编辑表单 */}
|
||||
<UserFormDialog
|
||||
open={showForm}
|
||||
onOpenChange={setShowForm}
|
||||
editingUser={editingUser}
|
||||
formData={formData}
|
||||
onFormDataChange={setFormData}
|
||||
onSave={handleSave}
|
||||
enterprises={enterprises}
|
||||
/>
|
||||
|
||||
{/* 详情对话框 */}
|
||||
{/* 用户详情对话框 */}
|
||||
<UserDetailDialog
|
||||
open={showDetailDialog}
|
||||
onOpenChange={setShowDetailDialog}
|
||||
selectedUser={selectedUser}
|
||||
open={state.showDetailDialog}
|
||||
onOpenChange={(open) => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: open })}
|
||||
user={state.selectedUser}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,18 +3,37 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
email?: string;
|
||||
enterpriseId?: string;
|
||||
enterpriseName?: string;
|
||||
roleIds: string[];
|
||||
roles?: string[];
|
||||
userType: UserType;
|
||||
status: UserStatus;
|
||||
email: string;
|
||||
fullName: string | null;
|
||||
phone: string | null;
|
||||
isActive: boolean;
|
||||
isSuperuser: boolean;
|
||||
isVerified: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastLoginAt: string | null;
|
||||
avatarUrl: string | null;
|
||||
bio: string | null;
|
||||
displayName: string | null;
|
||||
departmentId: string | null;
|
||||
departmentName: string | null;
|
||||
scope: string;
|
||||
companyName: string | null;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
// 为了兼容现有代码,保留一些映射属性
|
||||
export interface UserWithLegacyFields extends User {
|
||||
// 向后兼容的属性
|
||||
name: string;
|
||||
phone: string;
|
||||
enterpriseId?: string;
|
||||
enterpriseName?: string;
|
||||
userType: UserType;
|
||||
status: UserStatus;
|
||||
lastLoginTime?: string;
|
||||
roleIds: string[];
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
export type UserType = 'super_admin' | 'enterprise_admin' | 'employee';
|
||||
@@ -40,6 +59,37 @@ export interface UserFilters {
|
||||
typeFilter: string;
|
||||
}
|
||||
|
||||
// API响应数据类型
|
||||
export interface UsersApiResponse {
|
||||
data: User[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
total_pages: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
}
|
||||
|
||||
// 分页状态
|
||||
export interface PaginationState {
|
||||
page: number;
|
||||
size: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
}
|
||||
|
||||
// API查询参数
|
||||
export interface UsersQueryParams {
|
||||
search?: string;
|
||||
is_active?: boolean;
|
||||
page?: number;
|
||||
size?: number;
|
||||
order_by?: string;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
export interface UserFormData {
|
||||
username?: string;
|
||||
|
||||
Reference in New Issue
Block a user