添加管理端和移动端的多个新功能模块,包括文件上传、在线地图、用户认证、系统设置等,优化代码结构,提升可维护性和用户体验。

This commit is contained in:
zyh
2025-04-10 02:25:25 +00:00
parent 01dea6efa1
commit b1a6b608c6
31 changed files with 1366 additions and 46 deletions

211
client/admin/pages_map.tsx Normal file
View File

@@ -0,0 +1,211 @@
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.map(location => ({
id: location.id,
longitude: location.longitude,
latitude: location.latitude,
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, locations[0].latitude] : 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>
);
};