pages_map.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import React, { useState, useEffect } from 'react';
  2. import {
  3. Layout, Menu, Button, Table, Space,
  4. Form, Input, Select, message, Modal,
  5. Card, Spin, Row, Col, Breadcrumb, Avatar,
  6. Dropdown, ConfigProvider, theme, Typography,
  7. Switch, Badge, Image, Upload, Divider, Descriptions,
  8. Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer,
  9. Tree
  10. } from 'antd';
  11. import {
  12. MenuFoldOutlined,
  13. MenuUnfoldOutlined,
  14. AppstoreOutlined,
  15. EnvironmentOutlined,
  16. SearchOutlined,
  17. ClockCircleOutlined,
  18. UserOutlined,
  19. GlobalOutlined
  20. } from '@ant-design/icons';
  21. import {
  22. useQuery,
  23. } from '@tanstack/react-query';
  24. import 'dayjs/locale/zh-cn';
  25. import AMap from './components_amap.tsx'; // 导入地图组件
  26. // 从share/types.ts导入所有类型,包括MapMode
  27. import type {
  28. MarkerData, LoginLocation, LoginLocationDetail, User
  29. } from '../share/types.ts';
  30. import { MapAPI,UserAPI } from './api.ts';
  31. import dayjs from 'dayjs';
  32. const { RangePicker } = DatePicker;
  33. // 地图页面组件
  34. export const LoginMapPage = () => {
  35. const [selectedTimeRange, setSelectedTimeRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
  36. const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
  37. const [selectedMarker, setSelectedMarker] = useState<MarkerData | null>(null);
  38. const [drawerVisible, setDrawerVisible] = useState(false);
  39. // 获取登录位置数据
  40. const { data: locations = [], isLoading: markersLoading } = useQuery<LoginLocation[]>({
  41. queryKey: ['loginLocations', selectedTimeRange, selectedUserId],
  42. queryFn: async () => {
  43. try {
  44. let params: any = {};
  45. if (selectedTimeRange) {
  46. params.startTime = selectedTimeRange[0].format('YYYY-MM-DD HH:mm:ss');
  47. params.endTime = selectedTimeRange[1].format('YYYY-MM-DD HH:mm:ss');
  48. }
  49. if (selectedUserId) {
  50. params.userId = selectedUserId;
  51. }
  52. const result = await MapAPI.getMarkers(params);
  53. return result.data;
  54. } catch (error) {
  55. console.error("获取登录位置数据失败:", error);
  56. message.error("获取登录位置数据失败");
  57. return [];
  58. }
  59. },
  60. refetchInterval: 30000 // 30秒刷新一次
  61. });
  62. // 获取用户列表
  63. const { data: users = [] } = useQuery<User[]>({
  64. queryKey: ['users'],
  65. queryFn: async () => {
  66. try {
  67. const response = await UserAPI.getUsers();
  68. return response.data || [];
  69. } catch (error) {
  70. console.error("获取用户列表失败:", error);
  71. message.error("获取用户列表失败");
  72. return [];
  73. }
  74. }
  75. });
  76. // 获取选中标记点的详细信息
  77. const { data: markerDetail, isLoading: detailLoading } = useQuery<LoginLocationDetail | undefined>({
  78. queryKey: ['loginLocation', selectedMarker?.id],
  79. queryFn: async () => {
  80. if (!selectedMarker?.id) return undefined;
  81. try {
  82. const result = await MapAPI.getLocationDetail(Number(selectedMarker.id));
  83. return result.data;
  84. } catch (error) {
  85. console.error("获取登录位置详情失败:", error);
  86. message.error("获取登录位置详情失败");
  87. return undefined;
  88. }
  89. },
  90. enabled: !!selectedMarker?.id
  91. });
  92. // 处理标记点点击
  93. const handleMarkerClick = (marker: MarkerData) => {
  94. setSelectedMarker(marker);
  95. setDrawerVisible(true);
  96. };
  97. // 渲染地图标记点
  98. const renderMarkers = (locations: LoginLocation[]): MarkerData[] => {
  99. return locations
  100. .filter(location => location.longitude !== null && location.latitude !== null)
  101. .map(location => ({
  102. id: location.id?.toString() || '',
  103. longitude: location.longitude as number,
  104. latitude: location.latitude as number,
  105. title: location.user?.nickname || location.user?.username || '未知用户',
  106. description: `登录时间: ${dayjs(location.login_time).format('YYYY-MM-DD HH:mm:ss')}\nIP地址: ${location.ip_address}`,
  107. status: 'online',
  108. type: 'login',
  109. extraData: location
  110. }));
  111. };
  112. return (
  113. <div className="h-full">
  114. <Card style={{ marginBottom: 16 }}>
  115. <Space direction="horizontal" size={16} wrap>
  116. <RangePicker
  117. showTime
  118. onChange={(dates) => setSelectedTimeRange(dates as [dayjs.Dayjs, dayjs.Dayjs])}
  119. placeholder={['开始时间', '结束时间']}
  120. />
  121. <Select
  122. style={{ width: 200 }}
  123. placeholder="选择用户"
  124. allowClear
  125. onChange={(value) => setSelectedUserId(value)}
  126. options={users.map((user: User) => ({
  127. label: user.nickname || user.username,
  128. value: user.id
  129. }))}
  130. />
  131. <Button
  132. type="primary"
  133. onClick={() => {
  134. setSelectedTimeRange(null);
  135. setSelectedUserId(null);
  136. }}
  137. >
  138. 重置筛选
  139. </Button>
  140. </Space>
  141. </Card>
  142. <Card style={{ height: 'calc(100% - 80px)' }}>
  143. <Spin spinning={markersLoading}>
  144. <div style={{ height: '100%', minHeight: '500px' }}>
  145. <AMap
  146. markers={renderMarkers(locations)}
  147. center={locations[0] && locations[0].longitude !== null && locations[0].latitude !== null
  148. ? [locations[0].longitude, locations[0].latitude] as [number, number]
  149. : undefined}
  150. onMarkerClick={handleMarkerClick}
  151. height={'100%'}
  152. />
  153. </div>
  154. </Spin>
  155. </Card>
  156. <Drawer
  157. title="登录位置详情"
  158. placement="right"
  159. onClose={() => {
  160. setDrawerVisible(false);
  161. setSelectedMarker(null);
  162. }}
  163. open={drawerVisible}
  164. width={400}
  165. >
  166. {detailLoading ? (
  167. <Spin />
  168. ) : markerDetail ? (
  169. <Descriptions column={1}>
  170. <Descriptions.Item label={<><UserOutlined /> 用户</>}>
  171. {markerDetail.user?.nickname || markerDetail.user?.username || '未知用户'}
  172. </Descriptions.Item>
  173. <Descriptions.Item label={<><ClockCircleOutlined /> 登录时间</>}>
  174. {dayjs(markerDetail.login_time).format('YYYY-MM-DD HH:mm:ss')}
  175. </Descriptions.Item>
  176. <Descriptions.Item label={<><GlobalOutlined /> IP地址</>}>
  177. {markerDetail.ip_address}
  178. </Descriptions.Item>
  179. <Descriptions.Item label={<><EnvironmentOutlined /> 位置名称</>}>
  180. {markerDetail.location_name || '未知位置'}
  181. </Descriptions.Item>
  182. <Descriptions.Item label="经度">
  183. {markerDetail.longitude}
  184. </Descriptions.Item>
  185. <Descriptions.Item label="纬度">
  186. {markerDetail.latitude}
  187. </Descriptions.Item>
  188. <Descriptions.Item label="浏览器信息">
  189. <Typography.Paragraph ellipsis={{ rows: 2 }}>
  190. {markerDetail.user_agent}
  191. </Typography.Paragraph>
  192. </Descriptions.Item>
  193. </Descriptions>
  194. ) : (
  195. <div>暂无详细信息</div>
  196. )}
  197. </Drawer>
  198. </div>
  199. );
  200. };