重构消息通知弹窗
This commit is contained in:
parent
6de57aff67
commit
f1f8b963f1
@ -1,15 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useMemo, useRef } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { PaginatedTable, type ColumnDef, type SearchFieldDef, type PaginatedTableRef } from '@/components/ui/paginated-table';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@ -21,47 +14,18 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { Search, RefreshCw, Users, TrendingUp, Clock, LogOut } from 'lucide-react';
|
||||
import { RefreshCw, Users, TrendingUp, Clock, LogOut } from 'lucide-react';
|
||||
import { getOnlineUsers, getOnlineStatistics, kickUser } from './service';
|
||||
import type { OnlineUserResponse, OnlineStatistics } from './types';
|
||||
import type { Page } from '@/types/base';
|
||||
import type { OnlineUserResponse, OnlineStatistics, OnlineUserQuery } from './types';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const OnlineUserList: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<Page<OnlineUserResponse> | null>(null);
|
||||
const tableRef = useRef<PaginatedTableRef<OnlineUserResponse>>(null);
|
||||
const [statistics, setStatistics] = useState<OnlineStatistics | null>(null);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize] = useState(10);
|
||||
const [kickDialogOpen, setKickDialogOpen] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState<OnlineUserResponse | null>(null);
|
||||
|
||||
// 加载在线用户列表
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getOnlineUsers({
|
||||
keyword: keyword || undefined,
|
||||
pageNum: currentPage,
|
||||
pageSize,
|
||||
sortField: 'loginTime',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
setData(response);
|
||||
} catch (error) {
|
||||
console.error('加载失败:', error);
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载统计数据
|
||||
const loadStatistics = async () => {
|
||||
try {
|
||||
@ -73,19 +37,22 @@ const OnlineUserList: React.FC = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
loadStatistics();
|
||||
}, [currentPage, pageSize]);
|
||||
}, []);
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1);
|
||||
loadData();
|
||||
// 包装 fetchFn,转换页码(后端从1开始)
|
||||
const fetchData = async (query: OnlineUserQuery) => {
|
||||
return await getOnlineUsers({
|
||||
...query,
|
||||
pageNum: (query.pageNum || 0) + 1, // 转换为从1开始
|
||||
sortField: 'loginTime',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
};
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
loadData();
|
||||
tableRef.current?.refresh();
|
||||
loadStatistics();
|
||||
};
|
||||
|
||||
@ -107,7 +74,7 @@ const OnlineUserList: React.FC = () => {
|
||||
});
|
||||
setKickDialogOpen(false);
|
||||
setSelectedUser(null);
|
||||
loadData();
|
||||
tableRef.current?.refresh();
|
||||
loadStatistics();
|
||||
} catch (error) {
|
||||
console.error('强制下线失败:', error);
|
||||
@ -133,10 +100,64 @@ const OnlineUserList: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string): string => {
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
|
||||
};
|
||||
// 搜索字段定义
|
||||
const searchFields: SearchFieldDef[] = useMemo(() => [
|
||||
{ key: 'keyword', type: 'input', placeholder: '搜索用户名/昵称', width: 'w-[220px]' },
|
||||
], []);
|
||||
|
||||
// 列定义
|
||||
const columns: ColumnDef<OnlineUserResponse>[] = useMemo(() => [
|
||||
{ key: 'username', title: '用户名', dataIndex: 'username', width: '120px' },
|
||||
{ key: 'nickname', title: '昵称', dataIndex: 'nickname', width: '120px' },
|
||||
{ key: 'departmentName', title: '部门', dataIndex: 'departmentName', width: '120px' },
|
||||
{
|
||||
key: 'loginTime',
|
||||
title: '登录时间',
|
||||
width: '180px',
|
||||
render: (_, record) => dayjs(record.loginTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
key: 'onlineDuration',
|
||||
title: '在线时长',
|
||||
width: '120px',
|
||||
render: (_, record) => formatDuration(record.onlineDuration),
|
||||
},
|
||||
{
|
||||
key: 'lastActiveTime',
|
||||
title: '最后活跃',
|
||||
width: '180px',
|
||||
render: (_, record) => dayjs(record.lastActiveTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{ key: 'ipAddress', title: 'IP地址', dataIndex: 'ipAddress', width: '140px' },
|
||||
{ key: 'browser', title: '浏览器', dataIndex: 'browser', width: '120px' },
|
||||
{ key: 'os', title: '操作系统', dataIndex: 'os', width: '120px' },
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
width: '120px',
|
||||
sticky: true,
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleKickClick(record)}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-1" />
|
||||
强制下线
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
], []);
|
||||
|
||||
// 工具栏
|
||||
const toolbar = (
|
||||
<Button onClick={handleRefresh} variant="outline" size="sm">
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
刷新
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
@ -185,120 +206,19 @@ const OnlineUserList: React.FC = () => {
|
||||
{/* 主内容卡片 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>在线用户列表</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
placeholder="搜索用户名/昵称"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="w-64"
|
||||
/>
|
||||
<Button onClick={handleSearch} size="sm">
|
||||
<Search className="h-4 w-4 mr-1" />
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={handleRefresh} variant="outline" size="sm">
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<CardTitle>在线用户列表</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="rounded-md border">
|
||||
<Table minWidth="1400px">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead width="100px">用户名</TableHead>
|
||||
<TableHead width="100px">昵称</TableHead>
|
||||
<TableHead width="120px">部门</TableHead>
|
||||
<TableHead width="160px">登录时间</TableHead>
|
||||
<TableHead width="100px">在线时长</TableHead>
|
||||
<TableHead width="160px">最后活跃</TableHead>
|
||||
<TableHead width="130px">IP地址</TableHead>
|
||||
<TableHead width="110px">浏览器</TableHead>
|
||||
<TableHead width="120px">操作系统</TableHead>
|
||||
<TableHead width="120px" sticky>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={10} className="text-center py-8 text-muted-foreground">
|
||||
加载中...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : data?.content && data.content.length > 0 ? (
|
||||
data.content.map((user) => (
|
||||
<TableRow key={user.userId}>
|
||||
<TableCell width="100px" className="font-medium">{user.username}</TableCell>
|
||||
<TableCell width="100px">{user.nickname}</TableCell>
|
||||
<TableCell width="120px">{user.departmentName || '-'}</TableCell>
|
||||
<TableCell width="160px">{formatTime(user.loginTime)}</TableCell>
|
||||
<TableCell width="100px">{formatDuration(user.onlineDuration)}</TableCell>
|
||||
<TableCell width="160px">{formatTime(user.lastActiveTime)}</TableCell>
|
||||
<TableCell width="130px">{user.ipAddress || '-'}</TableCell>
|
||||
<TableCell width="110px">{user.browser || '-'}</TableCell>
|
||||
<TableCell width="120px">{user.os || '-'}</TableCell>
|
||||
<TableCell width="120px" sticky>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleKickClick(user)}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-1" />
|
||||
强制下线
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={10} className="text-center py-8 text-muted-foreground">
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{data && data.totalElements > 0 && (
|
||||
<div className="flex items-center justify-between mt-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
共 {data.totalElements} 条记录,第 {currentPage} / {data.totalPages} 页
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(currentPage + 1)}
|
||||
disabled={currentPage >= data.totalPages}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Separator />
|
||||
<CardContent className="pt-6">
|
||||
<PaginatedTable<OnlineUserResponse, OnlineUserQuery>
|
||||
ref={tableRef}
|
||||
fetchFn={fetchData}
|
||||
columns={columns}
|
||||
searchFields={searchFields}
|
||||
toolbar={toolbar}
|
||||
rowKey="userId"
|
||||
minWidth="1400px"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@ -1,19 +1,16 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useRef } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { DataTablePagination } from '@/components/ui/pagination';
|
||||
import { PaginatedTable, type ColumnDef, type SearchFieldDef, type PaginatedTableRef } from '@/components/ui/paginated-table';
|
||||
import {
|
||||
Loader2, Plus, Search, Edit, Trash2, KeyRound, Tag as TagIcon,
|
||||
Plus, Edit, Trash2, KeyRound, Tag as TagIcon,
|
||||
ShieldCheck, Settings, Shield
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { getRoleList, deleteRole, getRoleMenusAndPermissions, assignMenusAndPermissions } from './service';
|
||||
import type { RoleResponse, RoleQuery } from './types';
|
||||
import type { Page } from '@/types/base';
|
||||
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
|
||||
import EditDialog from './components/EditDialog';
|
||||
import DeleteDialog from './components/DeleteDialog';
|
||||
import PermissionDialog from './components/PermissionDialog';
|
||||
@ -26,8 +23,8 @@ import dayjs from 'dayjs';
|
||||
*/
|
||||
const RolePage: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<Page<RoleResponse> | null>(null);
|
||||
const tableRef = useRef<PaginatedTableRef<RoleResponse>>(null);
|
||||
const [stats, setStats] = useState({ total: 0 });
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [editRecord, setEditRecord] = useState<RoleResponse>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@ -38,63 +35,30 @@ const RolePage: React.FC = () => {
|
||||
const [selectedRole, setSelectedRole] = useState<RoleResponse | null>(null);
|
||||
const [defaultMenuIds, setDefaultMenuIds] = useState<number[]>([]);
|
||||
const [defaultPermissionIds, setDefaultPermissionIds] = useState<number[]>([]);
|
||||
const [query, setQuery] = useState<RoleQuery>({
|
||||
pageNum: DEFAULT_PAGE_NUM,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
code: '',
|
||||
name: '',
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await getRoleList(query);
|
||||
setData(result);
|
||||
} catch (error) {
|
||||
console.error('加载角色列表失败:', error);
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
variant: 'destructive'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// 包装 fetchFn,同时更新统计数据
|
||||
const fetchData = async (query: RoleQuery) => {
|
||||
const result = await getRoleList(query);
|
||||
setStats({ total: result.totalElements || 0 });
|
||||
return result;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [query]);
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
setQuery(prev => ({ ...prev, pageNum: 0 }));
|
||||
};
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
setQuery({
|
||||
pageNum: 0,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
code: '',
|
||||
name: '',
|
||||
});
|
||||
};
|
||||
|
||||
// 新建
|
||||
const handleCreate = () => {
|
||||
setEditRecord(undefined);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: RoleResponse) => {
|
||||
setEditRecord(record);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleSuccess = () => {
|
||||
setEditDialogOpen(false);
|
||||
setEditRecord(undefined);
|
||||
tableRef.current?.refresh();
|
||||
};
|
||||
|
||||
const handleDelete = (record: RoleResponse) => {
|
||||
setDeleteRecord(record);
|
||||
setDeleteDialogOpen(true);
|
||||
@ -108,7 +72,7 @@ const RolePage: React.FC = () => {
|
||||
title: '删除成功',
|
||||
description: `角色 "${deleteRecord.name}" 已删除`,
|
||||
});
|
||||
loadData();
|
||||
tableRef.current?.refresh();
|
||||
setDeleteDialogOpen(false);
|
||||
setDeleteRecord(null);
|
||||
} catch (error) {
|
||||
@ -148,7 +112,7 @@ const RolePage: React.FC = () => {
|
||||
description: `已为角色 "${selectedRole.name}" 分配权限`,
|
||||
});
|
||||
setPermissionDialogOpen(false);
|
||||
loadData();
|
||||
tableRef.current?.refresh();
|
||||
} catch (error) {
|
||||
console.error('分配权限失败:', error);
|
||||
toast({
|
||||
@ -165,13 +129,132 @@ const RolePage: React.FC = () => {
|
||||
setAssignTagDialogOpen(true);
|
||||
};
|
||||
|
||||
// 统计数据
|
||||
const stats = useMemo(() => {
|
||||
const total = data?.totalElements || 0;
|
||||
return { total };
|
||||
}, [data]);
|
||||
// 搜索字段定义
|
||||
const searchFields: SearchFieldDef[] = useMemo(() => [
|
||||
{ key: 'code', type: 'input', placeholder: '搜索角色编码', width: 'w-[180px]' },
|
||||
{ key: 'name', type: 'input', placeholder: '搜索角色名称', width: 'w-[180px]' },
|
||||
], []);
|
||||
|
||||
const pageCount = data?.totalElements ? Math.ceil(data.totalElements / (query.pageSize || DEFAULT_PAGE_SIZE)) : 0;
|
||||
// 列定义
|
||||
const columns: ColumnDef<RoleResponse>[] = useMemo(() => [
|
||||
{ key: 'id', title: 'ID', dataIndex: 'id', width: '80px' },
|
||||
{
|
||||
key: 'code',
|
||||
title: '角色编码',
|
||||
width: '150px',
|
||||
render: (_, record) => <code className="text-sm">{record.code}</code>,
|
||||
},
|
||||
{ key: 'name', title: '角色名称', dataIndex: 'name', width: '150px' },
|
||||
{
|
||||
key: 'isAdmin',
|
||||
title: '类型',
|
||||
width: '120px',
|
||||
render: (_, record) => record.isAdmin ? (
|
||||
<Badge variant="default" className="bg-amber-500 hover:bg-amber-600">
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
管理员
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">普通角色</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'tags',
|
||||
title: '标签',
|
||||
width: '200px',
|
||||
render: (_, record) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{record.tags && record.tags.length > 0 ? (
|
||||
record.tags.map(tag => (
|
||||
<Badge
|
||||
key={tag.id}
|
||||
variant="outline"
|
||||
style={{ backgroundColor: tag.color, color: '#fff' }}
|
||||
className="text-xs"
|
||||
>
|
||||
{tag.name}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ key: 'sort', title: '排序', dataIndex: 'sort', width: '80px' },
|
||||
{
|
||||
key: 'description',
|
||||
title: '描述',
|
||||
width: '200px',
|
||||
render: (_, record) => (
|
||||
<div className="truncate max-w-[200px]" title={record.description || ''}>
|
||||
{record.description || '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
title: '创建时间',
|
||||
width: '180px',
|
||||
render: (_, record) => record.createTime ? dayjs(record.createTime).format('YYYY-MM-DD HH:mm') : '-',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
width: '200px',
|
||||
sticky: true,
|
||||
render: (_, record) => (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(record)} title="编辑">
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleAssignPermissions(record)}
|
||||
title="分配权限"
|
||||
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50"
|
||||
>
|
||||
<KeyRound className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleAssignTags(record)}
|
||||
title="分配标签"
|
||||
className="text-purple-600 hover:text-purple-700 hover:bg-purple-50"
|
||||
>
|
||||
<TagIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
{!record.isAdmin && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(record)}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
], []);
|
||||
|
||||
// 工具栏
|
||||
const toolbar = (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setTagDialogOpen(true)}>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
标签管理
|
||||
</Button>
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建角色
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@ -197,188 +280,20 @@ const RolePage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardHeader>
|
||||
<CardTitle>角色列表</CardTitle>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setTagDialogOpen(true)}>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
标签管理
|
||||
</Button>
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建角色
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* 搜索栏 */}
|
||||
<div className="flex flex-wrap items-center gap-4 mb-4">
|
||||
<div className="flex-1 max-w-xs">
|
||||
<Input
|
||||
placeholder="搜索角色编码"
|
||||
value={query.code}
|
||||
onChange={(e) => setQuery(prev => ({ ...prev, code: e.target.value }))}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 max-w-xs">
|
||||
<Input
|
||||
placeholder="搜索角色名称"
|
||||
value={query.name}
|
||||
onChange={(e) => setQuery(prev => ({ ...prev, name: e.target.value }))}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleSearch} className="h-9">
|
||||
<Search className="h-4 w-4 mr-2" />
|
||||
搜索
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleReset} className="h-9">
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 表格 */}
|
||||
<div className="rounded-md border">
|
||||
<Table minWidth="1280px">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead width="150px">角色编码</TableHead>
|
||||
<TableHead width="150px">角色名称</TableHead>
|
||||
<TableHead width="100px">类型</TableHead>
|
||||
<TableHead width="200px">标签</TableHead>
|
||||
<TableHead width="100px">排序</TableHead>
|
||||
<TableHead width="200px">描述</TableHead>
|
||||
<TableHead width="180px">创建时间</TableHead>
|
||||
<TableHead width="200px" sticky>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="h-24 text-center">
|
||||
<div className="flex items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||
<span className="text-sm text-muted-foreground">加载中...</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : data?.content && data.content.length > 0 ? (
|
||||
data.content.map((record) => {
|
||||
return (
|
||||
<TableRow key={record.id} className="hover:bg-muted/50">
|
||||
<TableCell width="150px" className="font-medium">
|
||||
<code className="text-sm">{record.code}</code>
|
||||
</TableCell>
|
||||
<TableCell width="150px">{record.name}</TableCell>
|
||||
<TableCell width="100px">
|
||||
{record.isAdmin ? (
|
||||
<Badge variant="default" className="bg-amber-500 hover:bg-amber-600">
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
管理员
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">普通角色</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell width="200px">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{record.tags && record.tags.length > 0 ? (
|
||||
record.tags.map(tag => (
|
||||
<Badge
|
||||
key={tag.id}
|
||||
variant="outline"
|
||||
style={{ backgroundColor: tag.color, color: '#fff' }}
|
||||
className="text-xs"
|
||||
>
|
||||
{tag.name}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell width="100px">{record.sort}</TableCell>
|
||||
<TableCell width="200px" className="text-sm max-w-[200px] truncate" title={record.description}>
|
||||
{record.description || '-'}
|
||||
</TableCell>
|
||||
<TableCell width="180px" className="text-sm">
|
||||
{record.createTime ? dayjs(record.createTime).format('YYYY-MM-DD HH:mm') : '-'}
|
||||
</TableCell>
|
||||
<TableCell width="200px" sticky>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(record)}
|
||||
title="编辑"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleAssignPermissions(record)}
|
||||
title="分配权限"
|
||||
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50"
|
||||
>
|
||||
<KeyRound className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleAssignTags(record)}
|
||||
title="分配标签"
|
||||
className="text-purple-600 hover:text-purple-700 hover:bg-purple-50"
|
||||
>
|
||||
<TagIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
{!record.isAdmin && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(record)}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="h-24 text-center">
|
||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||
<ShieldCheck className="w-16 h-16 mb-4 text-muted-foreground/50" />
|
||||
<div className="text-lg font-semibold mb-2">暂无角色数据</div>
|
||||
<div className="text-sm">点击右上角"新建角色"开始创建角色。</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{pageCount > 1 && (
|
||||
<DataTablePagination
|
||||
pageIndex={query.pageNum || 0}
|
||||
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
|
||||
pageCount={pageCount}
|
||||
onPageChange={(page) => setQuery(prev => ({
|
||||
...prev,
|
||||
pageNum: page - 1
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
<Separator />
|
||||
<CardContent className="pt-6">
|
||||
<PaginatedTable<RoleResponse, RoleQuery>
|
||||
ref={tableRef}
|
||||
fetchFn={fetchData}
|
||||
columns={columns}
|
||||
searchFields={searchFields}
|
||||
toolbar={toolbar}
|
||||
rowKey="id"
|
||||
minWidth="1280px"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -387,7 +302,7 @@ const RolePage: React.FC = () => {
|
||||
open={editDialogOpen}
|
||||
record={editRecord}
|
||||
onOpenChange={setEditDialogOpen}
|
||||
onSuccess={loadData}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
@ -414,7 +329,7 @@ const RolePage: React.FC = () => {
|
||||
onOpenChange={setAssignTagDialogOpen}
|
||||
onSuccess={() => {
|
||||
setAssignTagDialogOpen(false);
|
||||
loadData();
|
||||
tableRef.current?.refresh();
|
||||
}}
|
||||
selectedTags={selectedRole.tags}
|
||||
/>
|
||||
@ -427,7 +342,7 @@ const RolePage: React.FC = () => {
|
||||
onOpenChange={setTagDialogOpen}
|
||||
onSuccess={() => {
|
||||
setTagDialogOpen(false);
|
||||
loadData();
|
||||
tableRef.current?.refresh();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { DataTablePagination } from '@/components/ui/pagination';
|
||||
import { PaginatedTable, type ColumnDef, type SearchFieldDef, type PaginatedTableRef } from '@/components/ui/paginated-table';
|
||||
import {
|
||||
Loader2, Plus, Search, Edit, Trash2, KeyRound, Users as UsersIcon,
|
||||
Plus, Edit, Trash2, KeyRound, Users as UsersIcon,
|
||||
UserCheck, UserX, Activity
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
@ -15,8 +13,6 @@ import { getUsers, deleteUser, resetPassword, assignRoles, getAllRoles } from '.
|
||||
import { getDepartmentTree } from '../../Department/List/service';
|
||||
import type { UserResponse, UserQuery, Role } from './types';
|
||||
import type { DepartmentResponse } from '../../Department/List/types';
|
||||
import type { Page } from '@/types/base';
|
||||
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUM } from '@/utils/page';
|
||||
import EditModal from './components/EditModal';
|
||||
import ResetPasswordDialog from './components/ResetPasswordDialog';
|
||||
import AssignRolesDialog from './components/AssignRolesDialog';
|
||||
@ -28,8 +24,8 @@ import dayjs from 'dayjs';
|
||||
*/
|
||||
const UserPage: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<Page<UserResponse> | null>(null);
|
||||
const tableRef = useRef<PaginatedTableRef<UserResponse>>(null);
|
||||
const [stats, setStats] = useState({ total: 0, enabledCount: 0, disabledCount: 0 });
|
||||
const [departments, setDepartments] = useState<DepartmentResponse[]>([]);
|
||||
const [allRoles, setAllRoles] = useState<Role[]>([]);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
@ -40,25 +36,17 @@ const UserPage: React.FC = () => {
|
||||
const [assignRolesRecord, setAssignRolesRecord] = useState<UserResponse | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteRecord, setDeleteRecord] = useState<UserResponse | null>(null);
|
||||
const [query, setQuery] = useState<UserQuery>({
|
||||
pageNum: DEFAULT_PAGE_NUM,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
username: '',
|
||||
email: '',
|
||||
enabled: undefined,
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await getUsers(query);
|
||||
setData(result);
|
||||
} catch (error) {
|
||||
console.error('加载用户列表失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// 包装 fetchFn,同时更新统计数据
|
||||
const fetchData = async (query: UserQuery) => {
|
||||
const result = await getUsers(query);
|
||||
const all = result.content || [];
|
||||
setStats({
|
||||
total: result.totalElements || 0,
|
||||
enabledCount: all.filter(d => d.enabled).length,
|
||||
disabledCount: all.filter(d => !d.enabled).length,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// 加载部门和角色
|
||||
@ -79,38 +67,22 @@ const UserPage: React.FC = () => {
|
||||
loadDepartmentsAndRoles();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [query]);
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
setQuery(prev => ({ ...prev, pageNum: 0 }));
|
||||
};
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
setQuery({
|
||||
pageNum: 0,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
username: '',
|
||||
email: '',
|
||||
enabled: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
// 新建
|
||||
const handleCreate = () => {
|
||||
setEditRecord(undefined);
|
||||
setEditModalOpen(true);
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (record: UserResponse) => {
|
||||
setEditRecord(record);
|
||||
setEditModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
setEditModalOpen(false);
|
||||
setEditRecord(undefined);
|
||||
tableRef.current?.refresh();
|
||||
};
|
||||
|
||||
// 重置密码
|
||||
const handleResetPassword = (record: UserResponse) => {
|
||||
setResetPasswordRecord(record);
|
||||
@ -151,7 +123,7 @@ const UserPage: React.FC = () => {
|
||||
title: '分配成功',
|
||||
description: `已为用户 "${assignRolesRecord.username}" 分配角色`,
|
||||
});
|
||||
loadData();
|
||||
tableRef.current?.refresh();
|
||||
setAssignRolesDialogOpen(false);
|
||||
setAssignRolesRecord(null);
|
||||
} catch (error) {
|
||||
@ -178,7 +150,7 @@ const UserPage: React.FC = () => {
|
||||
title: '删除成功',
|
||||
description: `用户 "${deleteRecord.username}" 已删除`,
|
||||
});
|
||||
loadData();
|
||||
tableRef.current?.refresh();
|
||||
setDeleteDialogOpen(false);
|
||||
setDeleteRecord(null);
|
||||
} catch (error) {
|
||||
@ -191,30 +163,125 @@ const UserPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 状态徽章
|
||||
const getStatusBadge = (enabled: boolean) => {
|
||||
return enabled ? (
|
||||
<Badge variant="success" className="inline-flex items-center gap-1">
|
||||
<UserCheck className="h-3 w-3" />
|
||||
启用
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" className="inline-flex items-center gap-1">
|
||||
<UserX className="h-3 w-3" />
|
||||
禁用
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
// 搜索字段定义
|
||||
const searchFields: SearchFieldDef[] = useMemo(() => [
|
||||
{ key: 'username', type: 'input', placeholder: '搜索用户名', width: 'w-[180px]' },
|
||||
{ key: 'email', type: 'input', placeholder: '搜索邮箱', width: 'w-[180px]' },
|
||||
{
|
||||
key: 'enabled',
|
||||
type: 'select',
|
||||
placeholder: '状态',
|
||||
width: 'w-[120px]',
|
||||
options: [
|
||||
{ label: '启用', value: 'true' },
|
||||
{ label: '禁用', value: 'false' },
|
||||
],
|
||||
},
|
||||
], []);
|
||||
|
||||
// 统计数据
|
||||
const stats = useMemo(() => {
|
||||
const total = data?.totalElements || 0;
|
||||
const enabledCount = data?.content?.filter(d => d.enabled).length || 0;
|
||||
const disabledCount = data?.content?.filter(d => !d.enabled).length || 0;
|
||||
return { total, enabledCount, disabledCount };
|
||||
}, [data]);
|
||||
// 列定义
|
||||
const columns: ColumnDef<UserResponse>[] = useMemo(() => [
|
||||
{ key: 'id', title: 'ID', dataIndex: 'id', width: '80px' },
|
||||
{ key: 'username', title: '用户名', dataIndex: 'username', width: '120px' },
|
||||
{ key: 'nickname', title: '昵称', dataIndex: 'nickname', width: '120px' },
|
||||
{ key: 'email', title: '邮箱', dataIndex: 'email', width: '200px' },
|
||||
{ key: 'phone', title: '手机号', dataIndex: 'phone', width: '120px' },
|
||||
{ key: 'departmentName', title: '部门', dataIndex: 'departmentName', width: '120px' },
|
||||
{
|
||||
key: 'roles',
|
||||
title: '角色',
|
||||
width: '150px',
|
||||
render: (_, record) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{record.roles && record.roles.length > 0 ? (
|
||||
record.roles.map(role => (
|
||||
<Badge key={role.id} variant="outline" className="text-xs">
|
||||
{role.name}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
title: '状态',
|
||||
width: '100px',
|
||||
render: (_, record) => record.enabled ? (
|
||||
<Badge variant="success" className="inline-flex items-center gap-1">
|
||||
<UserCheck className="h-3 w-3" />
|
||||
启用
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" className="inline-flex items-center gap-1">
|
||||
<UserX className="h-3 w-3" />
|
||||
禁用
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
title: '创建时间',
|
||||
width: '180px',
|
||||
render: (_, record) => record.createTime ? dayjs(record.createTime).format('YYYY-MM-DD HH:mm') : '-',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
title: '操作',
|
||||
width: '200px',
|
||||
sticky: true,
|
||||
render: (_, record) => {
|
||||
const isAdmin = record.username === 'admin';
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(record)} title="编辑">
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleResetPassword(record)}
|
||||
title="重置密码"
|
||||
className="text-orange-600 hover:text-orange-700 hover:bg-orange-50"
|
||||
>
|
||||
<KeyRound className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleAssignRoles(record)}
|
||||
title="分配角色"
|
||||
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50"
|
||||
disabled={isAdmin}
|
||||
>
|
||||
<UsersIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
{!isAdmin && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(record)}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
], []);
|
||||
|
||||
const pageCount = data?.totalElements ? Math.ceil(data.totalElements / (query.pageSize || DEFAULT_PAGE_SIZE)) : 0;
|
||||
// 工具栏
|
||||
const toolbar = (
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新增用户
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
@ -260,186 +327,20 @@ const UserPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardHeader>
|
||||
<CardTitle>用户列表</CardTitle>
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新增用户
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* 搜索栏 */}
|
||||
<div className="flex flex-wrap items-center gap-4 mb-4">
|
||||
<div className="flex-1 max-w-xs">
|
||||
<Input
|
||||
placeholder="搜索用户名"
|
||||
value={query.username}
|
||||
onChange={(e) => setQuery(prev => ({ ...prev, username: e.target.value }))}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 max-w-xs">
|
||||
<Input
|
||||
placeholder="搜索邮箱"
|
||||
value={query.email}
|
||||
onChange={(e) => setQuery(prev => ({ ...prev, email: e.target.value }))}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
value={query.enabled === undefined ? 'all' : query.enabled ? 'true' : 'false'}
|
||||
onValueChange={(value) => setQuery(prev => ({
|
||||
...prev,
|
||||
enabled: value === 'all' ? undefined : value === 'true'
|
||||
}))}
|
||||
>
|
||||
<SelectTrigger className="w-[130px] h-9">
|
||||
<SelectValue placeholder="全部状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="true">启用</SelectItem>
|
||||
<SelectItem value="false">禁用</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={handleSearch} className="h-9">
|
||||
<Search className="h-4 w-4 mr-2" />
|
||||
搜索
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleReset} className="h-9">
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 表格 */}
|
||||
<div className="rounded-md border">
|
||||
<Table minWidth="1340px">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead width="120px">用户名</TableHead>
|
||||
<TableHead width="150px">昵称</TableHead>
|
||||
<TableHead width="200px">邮箱</TableHead>
|
||||
<TableHead width="120px">手机号</TableHead>
|
||||
<TableHead width="120px">部门</TableHead>
|
||||
<TableHead width="150px">角色</TableHead>
|
||||
<TableHead width="100px">状态</TableHead>
|
||||
<TableHead width="180px">创建时间</TableHead>
|
||||
<TableHead width="200px" sticky>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="h-24 text-center">
|
||||
<div className="flex items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||
<span className="text-sm text-muted-foreground">加载中...</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : data?.content && data.content.length > 0 ? (
|
||||
data.content.map((record) => {
|
||||
const isAdmin = record.username === 'admin';
|
||||
return (
|
||||
<TableRow key={record.id} className="hover:bg-muted/50">
|
||||
<TableCell width="120px" className="font-medium">
|
||||
{record.username}
|
||||
</TableCell>
|
||||
<TableCell width="150px">{record.nickname || '-'}</TableCell>
|
||||
<TableCell width="200px" className="text-sm">{record.email || '-'}</TableCell>
|
||||
<TableCell width="120px" className="text-sm">{record.phone || '-'}</TableCell>
|
||||
<TableCell width="120px">{record.departmentName || '-'}</TableCell>
|
||||
<TableCell width="150px">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{record.roles && record.roles.length > 0 ? (
|
||||
record.roles.map(role => (
|
||||
<Badge key={role.id} variant="outline" className="text-xs">
|
||||
{role.name}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell width="100px">{getStatusBadge(record.enabled)}</TableCell>
|
||||
<TableCell width="180px" className="text-sm">
|
||||
{record.createTime ? dayjs(record.createTime).format('YYYY-MM-DD HH:mm') : '-'}
|
||||
</TableCell>
|
||||
<TableCell width="200px" sticky>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(record)}
|
||||
title="编辑"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleResetPassword(record)}
|
||||
title="重置密码"
|
||||
className="text-orange-600 hover:text-orange-700 hover:bg-orange-50"
|
||||
>
|
||||
<KeyRound className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleAssignRoles(record)}
|
||||
title="分配角色"
|
||||
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50"
|
||||
disabled={isAdmin}
|
||||
>
|
||||
<UsersIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
{!isAdmin && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(record)}
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="h-24 text-center">
|
||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||
<UsersIcon className="w-16 h-16 mb-4 text-muted-foreground/50" />
|
||||
<div className="text-lg font-semibold mb-2">暂无用户数据</div>
|
||||
<div className="text-sm">点击右上角"新增用户"开始创建用户。</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{pageCount > 1 && (
|
||||
<DataTablePagination
|
||||
pageIndex={query.pageNum || 0}
|
||||
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
|
||||
pageCount={pageCount}
|
||||
onPageChange={(page) => setQuery(prev => ({
|
||||
...prev,
|
||||
pageNum: page - 1
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
<Separator />
|
||||
<CardContent className="pt-6">
|
||||
<PaginatedTable<UserResponse, UserQuery>
|
||||
ref={tableRef}
|
||||
fetchFn={fetchData}
|
||||
columns={columns}
|
||||
searchFields={searchFields}
|
||||
toolbar={toolbar}
|
||||
rowKey="id"
|
||||
minWidth="1340px"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -449,7 +350,7 @@ const UserPage: React.FC = () => {
|
||||
record={editRecord}
|
||||
departments={departments}
|
||||
onOpenChange={setEditModalOpen}
|
||||
onSuccess={loadData}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
|
||||
{/* 重置密码对话框 */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user