2
0

pages_map.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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. if (!Array.isArray(locations)) return [];
  100. return locations
  101. .filter(location => location?.longitude !== null && location?.latitude !== null)
  102. .map(location => ({
  103. id: location.id?.toString() || '',
  104. longitude: location.longitude as number,
  105. latitude: location.latitude as number,
  106. title: location.user?.nickname || location.user?.username || '未知用户',
  107. description: `登录时间: ${dayjs(location.loginTime).format('YYYY-MM-DD HH:mm:ss')}\nIP地址: ${location.ipAddress}`,
  108. status: 'online',
  109. type: 'login',
  110. extraData: location
  111. }));
  112. };
  113. return (
  114. <div className="h-full">
  115. <Card style={{ marginBottom: 16 }}>
  116. <Space direction="horizontal" size={16} wrap>
  117. <RangePicker
  118. showTime
  119. onChange={(dates) => setSelectedTimeRange(dates as [dayjs.Dayjs, dayjs.Dayjs])}
  120. placeholder={['开始时间', '结束时间']}
  121. />
  122. <Select
  123. style={{ width: 200 }}
  124. placeholder="选择用户"
  125. allowClear
  126. onChange={(value) => setSelectedUserId(value)}
  127. options={users.map((user: User) => ({
  128. label: user.nickname || user.username,
  129. value: user.id
  130. }))}
  131. />
  132. <Button
  133. type="primary"
  134. onClick={() => {
  135. setSelectedTimeRange(null);
  136. setSelectedUserId(null);
  137. }}
  138. >
  139. 重置筛选
  140. </Button>
  141. </Space>
  142. </Card>
  143. <Card style={{ height: 'calc(100% - 80px)' }}>
  144. <Spin spinning={markersLoading}>
  145. <div style={{ height: '100%', minHeight: '500px' }}>
  146. <AMap
  147. markers={renderMarkers(locations || [])}
  148. center={locations[0] && locations[0].longitude !== null && locations[0].latitude !== null
  149. ? [locations[0].longitude, locations[0].latitude] as [number, number]
  150. : undefined}
  151. onMarkerClick={handleMarkerClick}
  152. height={'100%'}
  153. />
  154. </div>
  155. </Spin>
  156. </Card>
  157. <Drawer
  158. title="登录位置详情"
  159. placement="right"
  160. onClose={() => {
  161. setDrawerVisible(false);
  162. setSelectedMarker(null);
  163. }}
  164. open={drawerVisible}
  165. width={400}
  166. >
  167. {detailLoading ? (
  168. <Spin />
  169. ) : markerDetail ? (
  170. <Descriptions column={1}>
  171. <Descriptions.Item label={<><UserOutlined /> 用户</>}>
  172. {markerDetail.user?.nickname || markerDetail.user?.username || '未知用户'}
  173. </Descriptions.Item>
  174. <Descriptions.Item label={<><ClockCircleOutlined /> 登录时间</>}>
  175. {dayjs(markerDetail.login_time).format('YYYY-MM-DD HH:mm:ss')}
  176. </Descriptions.Item>
  177. <Descriptions.Item label={<><GlobalOutlined /> IP地址</>}>
  178. {markerDetail.ip_address}
  179. </Descriptions.Item>
  180. <Descriptions.Item label={<><EnvironmentOutlined /> 位置名称</>}>
  181. {markerDetail.location_name || '未知位置'}
  182. </Descriptions.Item>
  183. <Descriptions.Item label="经度">
  184. {markerDetail.longitude}
  185. </Descriptions.Item>
  186. <Descriptions.Item label="纬度">
  187. {markerDetail.latitude}
  188. </Descriptions.Item>
  189. <Descriptions.Item label="浏览器信息">
  190. <Typography.Paragraph ellipsis={{ rows: 2 }}>
  191. {markerDetail.user_agent}
  192. </Typography.Paragraph>
  193. </Descriptions.Item>
  194. </Descriptions>
  195. ) : (
  196. <div>暂无详细信息</div>
  197. )}
  198. </Drawer>
  199. </div>
  200. );
  201. };