From 58ad4b26436bdebf8084ee7b634fed154b777c7b Mon Sep 17 00:00:00 2001 From: dengqichen Date: Thu, 20 Nov 2025 16:32:33 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B6=88=E6=81=AF=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Menu/List/components/EditDialog.tsx | 17 +- .../src/pages/System/Online/List/index.tsx | 326 ++++++++++++++++++ .../src/pages/System/Online/List/service.ts | 17 + .../src/pages/System/Online/List/types.ts | 31 ++ 4 files changed, 380 insertions(+), 11 deletions(-) create mode 100644 frontend/src/pages/System/Online/List/index.tsx create mode 100644 frontend/src/pages/System/Online/List/service.ts create mode 100644 frontend/src/pages/System/Online/List/types.ts diff --git a/frontend/src/pages/System/Menu/List/components/EditDialog.tsx b/frontend/src/pages/System/Menu/List/components/EditDialog.tsx index b8c401aa..64865b35 100644 --- a/frontend/src/pages/System/Menu/List/components/EditDialog.tsx +++ b/frontend/src/pages/System/Menu/List/components/EditDialog.tsx @@ -238,6 +238,12 @@ const EditDialog: React.FC = ({ )} + setIconSelectVisible(false)} + value={formData.icon} + onChange={(value) => setFormData({ ...formData, icon: value })} + />
@@ -293,17 +299,6 @@ const EditDialog: React.FC = ({ - - {/* 图标选择器 */} - setIconSelectVisible(false)} - value={formData.icon} - onChange={(value) => { - setFormData({ ...formData, icon: value }); - setIconSelectVisible(false); - }} - /> ); }; diff --git a/frontend/src/pages/System/Online/List/index.tsx b/frontend/src/pages/System/Online/List/index.tsx new file mode 100644 index 00000000..d00fbd3c --- /dev/null +++ b/frontend/src/pages/System/Online/List/index.tsx @@ -0,0 +1,326 @@ +import React, { useEffect, useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { useToast } from '@/components/ui/use-toast'; +import { Search, 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 dayjs from 'dayjs'; + +const OnlineUserList: React.FC = () => { + const { toast } = useToast(); + const [loading, setLoading] = useState(false); + const [data, setData] = useState | null>(null); + const [statistics, setStatistics] = useState(null); + const [keyword, setKeyword] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize] = useState(10); + const [kickDialogOpen, setKickDialogOpen] = useState(false); + const [selectedUser, setSelectedUser] = useState(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 { + const stats = await getOnlineStatistics(); + setStatistics(stats); + } catch (error) { + console.error('加载统计失败:', error); + } + }; + + useEffect(() => { + loadData(); + loadStatistics(); + }, [currentPage, pageSize]); + + // 搜索 + const handleSearch = () => { + setCurrentPage(1); + loadData(); + }; + + // 刷新 + const handleRefresh = () => { + loadData(); + loadStatistics(); + }; + + // 打开强制下线确认对话框 + const handleKickClick = (user: OnlineUserResponse) => { + setSelectedUser(user); + setKickDialogOpen(true); + }; + + // 确认强制下线 + const handleKickConfirm = async () => { + if (!selectedUser) return; + + try { + await kickUser(selectedUser.userId); + toast({ + title: '操作成功', + description: `用户 "${selectedUser.username}" 已被强制下线`, + }); + setKickDialogOpen(false); + setSelectedUser(null); + loadData(); + loadStatistics(); + } catch (error) { + console.error('强制下线失败:', error); + toast({ + title: '操作失败', + description: error instanceof Error ? error.message : '未知错误', + variant: 'destructive', + }); + } + }; + + // 格式化在线时长 + const formatDuration = (seconds: number): string => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + if (hours > 0) { + return `${hours}小时${minutes}分钟`; + } else if (minutes > 0) { + return `${minutes}分钟`; + } else { + return `${seconds}秒`; + } + }; + + // 格式化时间 + const formatTime = (time: string): string => { + return dayjs(time).format('YYYY-MM-DD HH:mm:ss'); + }; + + return ( +
+ {/* 统计卡片 */} +
+ + + 当前在线 + + + +
+ {statistics?.totalOnline || 0} +
+

实时在线用户数

+
+
+ + + + 今日峰值 + + + +
+ {statistics?.todayPeak || 0} +
+

今日最高在线人数

+
+
+ + + + 平均时长 + + + +
+ {statistics?.averageOnlineTime || '-'} +
+

平均在线时长

+
+
+
+ + {/* 主内容卡片 */} + + +
+ 在线用户列表 +
+
+ setKeyword(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + className="w-64" + /> + +
+ +
+
+
+ +
+ + + + 用户名 + 昵称 + 部门 + 登录时间 + 在线时长 + 最后活跃 + IP地址 + 浏览器 + 操作系统 + 操作 + + + + {loading ? ( + + + 加载中... + + + ) : data?.content && data.content.length > 0 ? ( + data.content.map((user) => ( + + {user.username} + {user.nickname} + {user.departmentName || '-'} + {formatTime(user.loginTime)} + {formatDuration(user.onlineDuration)} + {formatTime(user.lastActiveTime)} + {user.ipAddress || '-'} + {user.browser || '-'} + {user.os || '-'} + + + + + )) + ) : ( + + + 暂无数据 + + + )} + +
+
+ + {/* 分页 */} + {data && data.totalElements > 0 && ( +
+
+ 共 {data.totalElements} 条记录,第 {currentPage} / {data.totalPages} 页 +
+
+ + +
+
+ )} +
+
+ + {/* 强制下线确认对话框 */} + + + + 确认强制下线 + + 确定要强制用户 "{selectedUser?.username}" 下线吗? +
+ 该操作将立即终止用户的会话。 +
+
+ + 取消 + + 确认下线 + + +
+
+
+ ); +}; + +export default OnlineUserList; diff --git a/frontend/src/pages/System/Online/List/service.ts b/frontend/src/pages/System/Online/List/service.ts new file mode 100644 index 00000000..f245624c --- /dev/null +++ b/frontend/src/pages/System/Online/List/service.ts @@ -0,0 +1,17 @@ +import request from '@/utils/request'; +import type { OnlineUserResponse, OnlineUserQuery, OnlineStatistics } from './types'; +import { Page } from '@/types/base'; + +const BASE_URL = '/api/v1/online'; + +// 获取在线用户列表(分页) +export const getOnlineUsers = (params?: OnlineUserQuery) => + request.get>(`${BASE_URL}/users`, { params }); + +// 获取在线统计 +export const getOnlineStatistics = () => + request.get(`${BASE_URL}/statistics`); + +// 强制用户下线 +export const kickUser = (userId: number) => + request.delete(`${BASE_URL}/kick/${userId}`); diff --git a/frontend/src/pages/System/Online/List/types.ts b/frontend/src/pages/System/Online/List/types.ts new file mode 100644 index 00000000..bd73108a --- /dev/null +++ b/frontend/src/pages/System/Online/List/types.ts @@ -0,0 +1,31 @@ +import type { BaseResponse } from '@/types/base'; + +// 在线用户查询参数 +export interface OnlineUserQuery { + keyword?: string; // 搜索关键词(用户名/昵称) + pageNum?: number; // 页码,从1开始 + pageSize?: number; // 每页大小 + sortField?: string; // 排序字段 + sortOrder?: string; // 排序方向 +} + +// 在线用户响应 +export interface OnlineUserResponse extends BaseResponse { + userId: number; // 用户ID + username: string; // 用户名 + nickname: string; // 昵称 + departmentName: string; // 部门名称 + loginTime: string; // 登录时间 + lastActiveTime: string; // 最后活跃时间 + onlineDuration: number; // 在线时长(秒) + ipAddress: string; // IP地址 + browser: string; // 浏览器 + os: string; // 操作系统 +} + +// 在线统计 +export interface OnlineStatistics { + totalOnline: number; // 当前在线人数 + todayPeak: number; // 今日峰值 + averageOnlineTime: string; // 平均在线时长 +}