2
0

pages_map.tsx 6.9 KB

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