Files
d8d-admin-mobile-starter-pu…/client/admin/pages_map.tsx

216 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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[] => {
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>
);
};