| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- import React, { useState, useEffect } from 'react';
- import {
- Layout, Menu, Button, Table, Space,
- Form, Input, Select, message, Modal,
- Card, Spin, Row, Col, Breadcrumb, Avatar,
- Dropdown, ConfigProvider, theme, Typography,
- Switch, Badge, Image, Upload, Divider, Descriptions,
- Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer,
- Tree
- } from 'antd';
- import {
- MenuFoldOutlined,
- MenuUnfoldOutlined,
- AppstoreOutlined,
- EnvironmentOutlined,
- SearchOutlined,
- ClockCircleOutlined,
- UserOutlined,
- GlobalOutlined
- } from '@ant-design/icons';
- import {
- useQuery,
- } from '@tanstack/react-query';
- import 'dayjs/locale/zh-cn';
- import AMap from './components_amap.tsx'; // 导入地图组件
- // 从share/types.ts导入所有类型,包括MapMode
- import type {
- MarkerData, LoginLocation, LoginLocationDetail, User
- } from '../share/types.ts';
- import { MapAPI,UserAPI } from './api.ts';
- import dayjs from 'dayjs';
- const { RangePicker } = DatePicker;
- // 地图页面组件
- export const LoginMapPage = () => {
- const [selectedTimeRange, setSelectedTimeRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
- const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
- const [selectedMarker, setSelectedMarker] = useState<MarkerData | null>(null);
- const [drawerVisible, setDrawerVisible] = useState(false);
- // 获取登录位置数据
- const { data: locations = [], isLoading: markersLoading } = useQuery<LoginLocation[]>({
- queryKey: ['loginLocations', selectedTimeRange, selectedUserId],
- queryFn: async () => {
- try {
- let params: any = {};
-
- if (selectedTimeRange) {
- params.startTime = selectedTimeRange[0].format('YYYY-MM-DD HH:mm:ss');
- params.endTime = selectedTimeRange[1].format('YYYY-MM-DD HH:mm:ss');
- }
-
- if (selectedUserId) {
- params.userId = selectedUserId;
- }
-
- const result = await MapAPI.getMarkers(params);
- return result.data;
- } catch (error) {
- console.error("获取登录位置数据失败:", error);
- message.error("获取登录位置数据失败");
- return [];
- }
- },
- refetchInterval: 30000 // 30秒刷新一次
- });
- // 获取用户列表
- const { data: users = [] } = useQuery<User[]>({
- queryKey: ['users'],
- queryFn: async () => {
- try {
- const response = await UserAPI.getUsers();
- return response.data || [];
- } catch (error) {
- console.error("获取用户列表失败:", error);
- message.error("获取用户列表失败");
- return [];
- }
- }
- });
- // 获取选中标记点的详细信息
- const { data: markerDetail, isLoading: detailLoading } = useQuery<LoginLocationDetail | undefined>({
- queryKey: ['loginLocation', selectedMarker?.id],
- queryFn: async () => {
- if (!selectedMarker?.id) return undefined;
- try {
- const result = await MapAPI.getLocationDetail(Number(selectedMarker.id));
- return result.data;
- } catch (error) {
- console.error("获取登录位置详情失败:", error);
- message.error("获取登录位置详情失败");
- return undefined;
- }
- },
- enabled: !!selectedMarker?.id
- });
- // 处理标记点点击
- const handleMarkerClick = (marker: MarkerData) => {
- setSelectedMarker(marker);
- setDrawerVisible(true);
- };
- // 渲染地图标记点
- const renderMarkers = (locations: LoginLocation[] = []): MarkerData[] => {
- if (!Array.isArray(locations)) return [];
-
- return locations
- .filter(location => location?.longitude !== null && location?.latitude !== null)
- .map(location => ({
- id: location.id?.toString() || '',
- longitude: location.longitude as number,
- latitude: location.latitude as number,
- title: location.user?.nickname || location.user?.username || '未知用户',
- description: `登录时间: ${dayjs(location.loginTime).format('YYYY-MM-DD HH:mm:ss')}\nIP地址: ${location.ipAddress}`,
- status: 'online',
- type: 'login',
- extraData: location
- }));
- };
- return (
- <div className="h-full">
- <Card style={{ marginBottom: 16 }}>
- <Space direction="horizontal" size={16} wrap>
- <RangePicker
- showTime
- onChange={(dates) => setSelectedTimeRange(dates as [dayjs.Dayjs, dayjs.Dayjs])}
- placeholder={['开始时间', '结束时间']}
- />
- <Select
- style={{ width: 200 }}
- placeholder="选择用户"
- allowClear
- onChange={(value) => setSelectedUserId(value)}
- options={users.map((user: User) => ({
- label: user.nickname || user.username,
- value: user.id
- }))}
- />
- <Button
- type="primary"
- onClick={() => {
- setSelectedTimeRange(null);
- setSelectedUserId(null);
- }}
- >
- 重置筛选
- </Button>
- </Space>
- </Card>
- <Card style={{ height: 'calc(100% - 80px)' }}>
- <Spin spinning={markersLoading}>
- <div style={{ height: '100%', minHeight: '500px' }}>
- <AMap
- markers={renderMarkers(locations || [])}
- center={locations[0] && locations[0].longitude !== null && locations[0].latitude !== null
- ? [locations[0].longitude, locations[0].latitude] as [number, number]
- : undefined}
- onMarkerClick={handleMarkerClick}
- height={'100%'}
- />
- </div>
- </Spin>
- </Card>
- <Drawer
- title="登录位置详情"
- placement="right"
- onClose={() => {
- setDrawerVisible(false);
- setSelectedMarker(null);
- }}
- open={drawerVisible}
- width={400}
- >
- {detailLoading ? (
- <Spin />
- ) : markerDetail ? (
- <Descriptions column={1}>
- <Descriptions.Item label={<><UserOutlined /> 用户</>}>
- {markerDetail.user?.nickname || markerDetail.user?.username || '未知用户'}
- </Descriptions.Item>
- <Descriptions.Item label={<><ClockCircleOutlined /> 登录时间</>}>
- {dayjs(markerDetail.login_time).format('YYYY-MM-DD HH:mm:ss')}
- </Descriptions.Item>
- <Descriptions.Item label={<><GlobalOutlined /> IP地址</>}>
- {markerDetail.ip_address}
- </Descriptions.Item>
- <Descriptions.Item label={<><EnvironmentOutlined /> 位置名称</>}>
- {markerDetail.location_name || '未知位置'}
- </Descriptions.Item>
- <Descriptions.Item label="经度">
- {markerDetail.longitude}
- </Descriptions.Item>
- <Descriptions.Item label="纬度">
- {markerDetail.latitude}
- </Descriptions.Item>
- <Descriptions.Item label="浏览器信息">
- <Typography.Paragraph ellipsis={{ rows: 2 }}>
- {markerDetail.user_agent}
- </Typography.Paragraph>
- </Descriptions.Item>
- </Descriptions>
- ) : (
- <div>暂无详细信息</div>
- )}
- </Drawer>
- </div>
- );
- };
|