diff --git a/Api/Management/UserManagementController.cs b/Api/Management/UserManagementController.cs index a02f85c..26baae5 100644 --- a/Api/Management/UserManagementController.cs +++ b/Api/Management/UserManagementController.cs @@ -14,11 +14,33 @@ public class UserManagementController(IUserManagementService userManagementServi { [HttpGet("get_users")] public async Task>> GetUsers( - [FromQuery] int page = 1, [FromQuery] int pageSize = 10) + [FromQuery] int page = 1, + [FromQuery] int pageSize = 10, + [FromQuery] string? searchQuery = null, + [FromQuery] string? role = null, + [FromQuery] DateTime? startDate = null, + [FromQuery] DateTime? endDate = null) { try { - var users = await userManagementService.GetUsersAsync(page, pageSize); + DateTime? utcStartDate = null; + DateTime? utcEndDate = null; + + if (startDate.HasValue) + { + utcStartDate = startDate.Value.Kind == DateTimeKind.Utc + ? startDate.Value + : DateTime.SpecifyKind(startDate.Value, DateTimeKind.Utc); + } + + if (endDate.HasValue) + { + utcEndDate = endDate.Value.Kind == DateTimeKind.Utc + ? endDate.Value + : DateTime.SpecifyKind(endDate.Value, DateTimeKind.Utc); + } + + var users = await userManagementService.GetUsersAsync(page, pageSize, searchQuery, role, utcStartDate, utcEndDate); return PaginatedSuccess(users.Data, users.TotalCount, users.Page, users.PageSize); } catch (Exception ex) @@ -121,4 +143,22 @@ public class UserManagementController(IUserManagementService userManagementServi return Error($"批量删除用户失败: {ex.Message}", 500); } } + + [HttpGet("get_user_detail/{id}")] + public async Task>> GetUserDetail(int id) + { + try + { + var userDetail = await userManagementService.GetUserDetailAsync(id); + return Success(userDetail, "用户详情获取成功"); + } + catch (KeyNotFoundException) + { + return Error("找不到指定用户", 404); + } + catch (Exception ex) + { + return Error($"获取用户详情失败: {ex.Message}", 500); + } + } } \ No newline at end of file diff --git a/Models/DataBase/Favorite.cs b/Models/DataBase/Favorite.cs index cc53125..b712eb8 100644 --- a/Models/DataBase/Favorite.cs +++ b/Models/DataBase/Favorite.cs @@ -5,14 +5,13 @@ namespace Foxel.Models.DataBase; public class Favorite { - [Key] - public int Id { get; set; } - - public User User { get; set; } - + [Key] public int Id { get; set; } + + public int UserId { get; set; } + [ForeignKey("UserId")] public User User { get; set; } + public int PictureId { get; set; } - [ForeignKey("PictureId")] - public Picture Picture { get; set; } - + [ForeignKey("PictureId")] public Picture Picture { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; -} +} \ No newline at end of file diff --git a/Models/Response/User/UserDetailResponse.cs b/Models/Response/User/UserDetailResponse.cs new file mode 100644 index 0000000..6209bd9 --- /dev/null +++ b/Models/Response/User/UserDetailResponse.cs @@ -0,0 +1,21 @@ +namespace Foxel.Models.Response.User; + +public class UserDetailResponse +{ + public int Id { get; set; } + public string UserName { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Role { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } + public UserStatistics Statistics { get; set; } = new(); +} + +public class UserStatistics +{ + public int TotalPictures { get; set; } + public int TotalAlbums { get; set; } + public int TotalFavorites { get; set; } + public int FavoriteReceivedCount { get; set; } + public double DiskUsageMB { get; set; } + public int AccountAgeDays { get; set; } +} diff --git a/Services/Management/IUserManagementService.cs b/Services/Management/IUserManagementService.cs index 47af034..d78ac6e 100644 --- a/Services/Management/IUserManagementService.cs +++ b/Services/Management/IUserManagementService.cs @@ -5,8 +5,9 @@ namespace Foxel.Services.Management; public interface IUserManagementService { - Task> GetUsersAsync(int page = 1, int pageSize = 10); + Task> GetUsersAsync(int page = 1, int pageSize = 10, string? searchQuery = null, string? role = null, DateTime? startDate = null, DateTime? endDate = null); Task GetUserByIdAsync(int id); + Task GetUserDetailAsync(int id); Task CreateUserAsync(string userName, string email, string password, string role); Task UpdateUserAsync(int id, string userName, string email, string role); Task DeleteUserAsync(int id); diff --git a/Services/Management/UserManagementService.cs b/Services/Management/UserManagementService.cs index 150d15a..fe836dc 100644 --- a/Services/Management/UserManagementService.cs +++ b/Services/Management/UserManagementService.cs @@ -9,7 +9,7 @@ namespace Foxel.Services.Management; public class UserManagementService( IDbContextFactory contextFactory) : IUserManagementService { - public async Task> GetUsersAsync(int page = 1, int pageSize = 10) + public async Task> GetUsersAsync(int page = 1, int pageSize = 10, string? searchQuery = null, string? role = null, DateTime? startDate = null, DateTime? endDate = null) { if (page < 1) page = 1; if (pageSize < 1) pageSize = 10; @@ -19,7 +19,39 @@ public class UserManagementService( // 构建查询 var query = dbContext.Users .Include(u => u.Role) - .OrderByDescending(u => u.CreatedAt); + .AsQueryable(); + + // 应用筛选条件 + if (!string.IsNullOrWhiteSpace(searchQuery)) + { + query = query.Where(u => u.UserName.Contains(searchQuery) || u.Email.Contains(searchQuery)); + } + + if (!string.IsNullOrWhiteSpace(role)) + { + query = query.Where(u => u.Role != null && u.Role.Name == role); + } + + if (startDate.HasValue) + { + // 确保DateTime是UTC时区 + var utcStartDate = startDate.Value.Kind == DateTimeKind.Utc + ? startDate.Value + : DateTime.SpecifyKind(startDate.Value, DateTimeKind.Utc); + query = query.Where(u => u.CreatedAt >= utcStartDate); + } + + if (endDate.HasValue) + { + // 确保DateTime是UTC时区,并设置为当天结束时间 + var utcEndDate = endDate.Value.Kind == DateTimeKind.Utc + ? endDate.Value.Date.AddDays(1).AddTicks(-1) + : DateTime.SpecifyKind(endDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc); + query = query.Where(u => u.CreatedAt <= utcEndDate); + } + + // 排序 + query = query.OrderByDescending(u => u.CreatedAt); // 获取总数和分页数据 var totalCount = await query.CountAsync(); @@ -216,6 +248,60 @@ public class UserManagementService( return result; } + public async Task GetUserDetailAsync(int id) + { + await using var dbContext = await contextFactory.CreateDbContextAsync(); + + var user = await dbContext.Users + .Include(u => u.Role) + .FirstOrDefaultAsync(u => u.Id == id); + + if (user == null) + throw new KeyNotFoundException($"找不到ID为{id}的用户"); + + // 获取用户统计数据 + var pictureCount = await dbContext.Pictures + .Where(p => p.UserId == id) + .CountAsync(); + + var albumCount = await dbContext.Albums + .Where(a => a.UserId == id) + .CountAsync(); + + var favoriteCount = await dbContext.Favorites + .Where(f => f.UserId == id) + .CountAsync(); + + var favoriteReceivedCount = await dbContext.Favorites + .Join(dbContext.Pictures, f => f.PictureId, p => p.Id, (f, p) => new { f, p }) + .Where(fp => fp.p.UserId == id) + .CountAsync(); + + // 计算存储使用量 + var diskUsage = 0; + + // 计算账户年龄 + var accountAge = (DateTime.UtcNow - user.CreatedAt).Days; + + return new UserDetailResponse + { + Id = user.Id, + UserName = user.UserName, + Email = user.Email, + Role = user.Role?.Name ?? "User", + CreatedAt = user.CreatedAt, + Statistics = new UserStatistics + { + TotalPictures = pictureCount, + TotalAlbums = albumCount, + TotalFavorites = favoriteCount, + FavoriteReceivedCount = favoriteReceivedCount, + DiskUsageMB = diskUsage, + AccountAgeDays = accountAge + } + }; + } + // 辅助方法:生成密码哈希 private string HashPassword(string password) { diff --git a/Web/src/api/index.ts b/Web/src/api/index.ts index 42ecdce..8f63f0a 100644 --- a/Web/src/api/index.ts +++ b/Web/src/api/index.ts @@ -67,7 +67,8 @@ export { createUser, updateUser, deleteUser, - batchDeleteUsers + batchDeleteUsers, + getUserDetail } from './userManagementApi'; // 导出PictureManagement API diff --git a/Web/src/api/types.ts b/Web/src/api/types.ts index 77e57e4..a4587c6 100644 --- a/Web/src/api/types.ts +++ b/Web/src/api/types.ts @@ -285,3 +285,33 @@ export interface BindAccountRequest { bindType: BindType; thirdPartyUserId: string; } + +// 用户筛选请求参数 +export interface UserFilterRequest { + page?: number; + pageSize?: number; + searchQuery?: string; + role?: string; + startDate?: string; + endDate?: string; +} + +// 用户统计信息 +export interface UserStatistics { + totalPictures: number; + totalAlbums: number; + totalFavorites: number; + favoriteReceivedCount: number; + diskUsageMB: number; + accountAgeDays: number; +} + +// 用户详情响应 +export interface UserDetailResponse { + id: number; + userName: string; + email: string; + role: string; + createdAt: Date; + statistics: UserStatistics; +} diff --git a/Web/src/api/userManagementApi.ts b/Web/src/api/userManagementApi.ts index 8144ddd..59ede97 100644 --- a/Web/src/api/userManagementApi.ts +++ b/Web/src/api/userManagementApi.ts @@ -5,15 +5,28 @@ import { type UserResponse, type CreateUserRequest, type AdminUpdateUserRequest, - type BatchDeleteResult + type BatchDeleteResult, + type UserFilterRequest, + type UserDetailResponse } from './types'; // 获取用户列表 export const getUsers = async ( - page: number = 1, - pageSize: number = 10 + filters: UserFilterRequest = {} ): Promise> => { - const response = await fetchApi(`/management/user/get_users?page=${page}&pageSize=${pageSize}`); + const { page = 1, pageSize = 10, searchQuery, role, startDate, endDate } = filters; + + const params = new URLSearchParams({ + page: page.toString(), + pageSize: pageSize.toString(), + }); + + if (searchQuery) params.append('searchQuery', searchQuery); + if (role) params.append('role', role); + if (startDate) params.append('startDate', startDate); + if (endDate) params.append('endDate', endDate); + + const response = await fetchApi(`/management/user/get_users?${params.toString()}`); return response as PaginatedResult; }; @@ -25,6 +38,14 @@ export const getUserById = async (id: number): Promise> ); }; +// 根据ID获取用户详情 +export const getUserDetail = async (id: number): Promise> => { + return fetchApi( + `/management/user/get_user_detail/${id}`, + { method: 'GET' } + ); +}; + // 创建用户 export const createUser = async ( userData: CreateUserRequest diff --git a/Web/src/pages/admin/users/Index.tsx b/Web/src/pages/admin/users/Index.tsx index 10c14bd..0132c0f 100644 --- a/Web/src/pages/admin/users/Index.tsx +++ b/Web/src/pages/admin/users/Index.tsx @@ -1,26 +1,31 @@ import React, { useState, useEffect, useCallback } from 'react'; import { Table, Button, Card, Input, Space, Modal, Form, - message, Tag, Typography, Popconfirm, Row, Col, Select + message, Tag, Typography, Popconfirm, Row, Col, Select, + DatePicker, Divider } from 'antd'; import { UserOutlined, DeleteOutlined, EditOutlined, SearchOutlined, ExclamationCircleOutlined, ReloadOutlined, - UserAddOutlined, UserDeleteOutlined, TeamOutlined + UserAddOutlined, UserDeleteOutlined, TeamOutlined, + EyeOutlined, FilterOutlined, ClearOutlined } from '@ant-design/icons'; import { getUsers, deleteUser, createUser, updateUser, batchDeleteUsers, UserRole } from '../../../api'; -import type { UserResponse, CreateUserRequest, AdminUpdateUserRequest } from '../../../api/types'; +import type { UserResponse, CreateUserRequest, AdminUpdateUserRequest, UserFilterRequest } from '../../../api/types'; import { useOutletContext } from 'react-router'; +import { useNavigate } from 'react-router'; import type { Breakpoint } from 'antd'; const { Title, Text } = Typography; const { Option } = Select; const { confirm } = Modal; +const { RangePicker } = DatePicker; const UserManagement: React.FC = () => { const { isMobile } = useOutletContext<{ isMobile: boolean }>(); + const navigate = useNavigate(); // 状态管理 const [users, setUsers] = useState([]); @@ -28,9 +33,13 @@ const UserManagement: React.FC = () => { const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); - const [searchQuery, setSearchQuery] = useState(''); const [selectedRowKeys, setSelectedRowKeys] = useState([]); + // 筛选状态 + const [filters, setFilters] = useState({}); + const [showFilters, setShowFilters] = useState(false); + const [filterForm] = Form.useForm(); + // 模态框状态 const [isModalVisible, setIsModalVisible] = useState(false); const [modalTitle, setModalTitle] = useState(''); @@ -38,10 +47,14 @@ const UserManagement: React.FC = () => { const [form] = Form.useForm(); // 加载用户数据 - const fetchUsers = useCallback(async (page = currentPage, size = pageSize) => { + const fetchUsers = useCallback(async (page = currentPage, size = pageSize, filterParams = filters) => { setLoading(true); try { - const response = await getUsers(page, size); + const response = await getUsers({ + page, + pageSize: size, + ...filterParams + }); if (response.success && response.data) { setUsers(response.data || []); setTotal(response.totalCount || 0); @@ -54,7 +67,7 @@ const UserManagement: React.FC = () => { } finally { setLoading(false); } - }, [currentPage, pageSize]); + }, [currentPage, pageSize, filters]); // 初始加载 useEffect(() => { @@ -68,12 +81,38 @@ const UserManagement: React.FC = () => { fetchUsers(page, size || pageSize); }; - // 处理搜索 - const handleSearch = () => { - // 这里应该向后端发送搜索请求,但目前API不支持搜索,所以仅前端过滤 - // 实际项目中应该添加后端搜索支持 + // 处理筛选 + const handleFilter = async () => { + try { + const values = await filterForm.validateFields(); + const newFilters: UserFilterRequest = { + searchQuery: values.searchQuery, + role: values.role, + startDate: values.dateRange?.[0]?.format('YYYY-MM-DD'), + endDate: values.dateRange?.[1]?.format('YYYY-MM-DD'), + }; + setFilters(newFilters); + setCurrentPage(1); + fetchUsers(1, pageSize, newFilters); + } catch (error) { + console.error('Filter validation failed:', error); + } + }; + + // 清除筛选 + const handleClearFilters = () => { + filterForm.resetFields(); + setFilters({}); setCurrentPage(1); - fetchUsers(1, pageSize); + fetchUsers(1, pageSize, {}); + }; + + // 处理搜索(快速搜索) + const handleQuickSearch = (searchQuery: string) => { + const newFilters = { ...filters, searchQuery }; + setFilters(newFilters); + setCurrentPage(1); + fetchUsers(1, pageSize, newFilters); }; // 打开创建用户模态框 @@ -244,6 +283,13 @@ const UserManagement: React.FC = () => { key: 'action', render: (_: any, record: UserResponse) => ( + + @@ -316,13 +369,51 @@ const UserManagement: React.FC = () => { placeholder="搜索用户名或邮箱" allowClear enterButton={} - onSearch={handleSearch} - onChange={(e) => setSearchQuery(e.target.value)} - value={searchQuery} + onSearch={handleQuickSearch} /> + {/* 高级筛选面板 */} + {showFilters && ( + <> + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + )} + { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const [user, setUser] = useState(null); + const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); // 加载用户数据 @@ -29,7 +31,7 @@ const UserDetail: React.FC = () => { try { setLoading(true); - const response = await getUserById(parseInt(id)); + const response = await getUserDetail(parseInt(id)); if (response.success && response.data) { setUser(response.data); } else { @@ -51,19 +53,23 @@ const UserDetail: React.FC = () => { navigate('/admin/users'); }; - // 跳转到编辑页面 - const handleEdit = () => { - navigate(`/admin/users/edit/${id}`); + // 格式化存储大小 + const formatDiskUsage = (mb: number): string => { + if (mb < 1024) { + return `${mb.toFixed(1)} MB`; + } + return `${(mb / 1024).toFixed(2)} GB`; }; - // 模拟数据 - 实际项目中应该从API获取 - const userStats = { - totalPhotos: 125, - totalAlbums: 14, - totalFavorites: 48, - diskUsage: '1.2 GB', - lastLogin: '2023-10-25 14:32', - accountAge: '268 天', + // 格式化账户年龄 + const formatAccountAge = (days: number): string => { + if (days < 30) { + return `${days} 天`; + } else if (days < 365) { + return `${Math.floor(days / 30)} 个月`; + } else { + return `${Math.floor(days / 365)} 年 ${Math.floor((days % 365) / 30)} 个月`; + } }; if (loading) { @@ -108,7 +114,7 @@ const UserDetail: React.FC = () => {
- } /> + {user.userName} @@ -118,9 +124,6 @@ const UserDetail: React.FC = () => { {user.role || '访客'}
- @@ -130,8 +133,11 @@ const UserDetail: React.FC = () => { {new Date(user.createdAt).toLocaleString()} - - {user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : '未登录'} + + {formatAccountAge(user.statistics.accountAgeDays)} + + + {formatDiskUsage(user.statistics.diskUsageMB)}
@@ -148,62 +154,47 @@ const UserDetail: React.FC = () => { } /> } + value={user.statistics.totalAlbums} + prefix={} /> } /> } /> } /> } /> - - 最近照片} - key="2" - > -
- 此功能在开发中 -
-
- - 最近相册} - key="3" - > -
- 此功能在开发中 -
-
diff --git a/Web/src/routes/index.tsx b/Web/src/routes/index.tsx index 4c7a797..60149da 100644 --- a/Web/src/routes/index.tsx +++ b/Web/src/routes/index.tsx @@ -21,6 +21,7 @@ import AdminDashboard from '../pages/admin/dashboard/Index'; import System from '../pages/admin/system/Index'; import UserManagement from '../pages/admin/users/Index'; import PictureManagement from '../pages/admin/pictures/Index'; +import UserDetail from '../pages/admin/users/UserDetail'; export interface RouteConfig { path: string; @@ -145,7 +146,19 @@ const routes: RouteConfig[] = [ title: '用户管理' } }, - { + { + path: 'users/:id', + key: 'user-detail', + label: '用户详情', + element: , + area: 'admin', + hideInMenu: true, + breadcrumb: { + title: '用户详情', + parent: 'users' + } + }, + { path: 'pictures', key: 'admin-picture', icon: ,