mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-04 23:29:00 +08:00
feat(userManagement): add user detail view and filtering capabilities
This commit is contained in:
@@ -14,11 +14,33 @@ public class UserManagementController(IUserManagementService userManagementServi
|
|||||||
{
|
{
|
||||||
[HttpGet("get_users")]
|
[HttpGet("get_users")]
|
||||||
public async Task<ActionResult<PaginatedResult<UserResponse>>> GetUsers(
|
public async Task<ActionResult<PaginatedResult<UserResponse>>> 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
|
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);
|
return PaginatedSuccess(users.Data, users.TotalCount, users.Page, users.PageSize);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -121,4 +143,22 @@ public class UserManagementController(IUserManagementService userManagementServi
|
|||||||
return Error<BatchDeleteResult>($"批量删除用户失败: {ex.Message}", 500);
|
return Error<BatchDeleteResult>($"批量删除用户失败: {ex.Message}", 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("get_user_detail/{id}")]
|
||||||
|
public async Task<ActionResult<BaseResult<UserDetailResponse>>> GetUserDetail(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userDetail = await userManagementService.GetUserDetailAsync(id);
|
||||||
|
return Success(userDetail, "用户详情获取成功");
|
||||||
|
}
|
||||||
|
catch (KeyNotFoundException)
|
||||||
|
{
|
||||||
|
return Error<UserDetailResponse>("找不到指定用户", 404);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Error<UserDetailResponse>($"获取用户详情失败: {ex.Message}", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,14 +5,13 @@ namespace Foxel.Models.DataBase;
|
|||||||
|
|
||||||
public class Favorite
|
public class Favorite
|
||||||
{
|
{
|
||||||
[Key]
|
[Key] public int Id { get; set; }
|
||||||
public int Id { get; set; }
|
|
||||||
|
public int UserId { get; set; }
|
||||||
public User User { get; set; }
|
[ForeignKey("UserId")] public User User { get; set; }
|
||||||
|
|
||||||
public int PictureId { get; set; }
|
public int PictureId { get; set; }
|
||||||
[ForeignKey("PictureId")]
|
[ForeignKey("PictureId")] public Picture Picture { get; set; }
|
||||||
public Picture Picture { get; set; }
|
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -5,8 +5,9 @@ namespace Foxel.Services.Management;
|
|||||||
|
|
||||||
public interface IUserManagementService
|
public interface IUserManagementService
|
||||||
{
|
{
|
||||||
Task<PaginatedResult<UserResponse>> GetUsersAsync(int page = 1, int pageSize = 10);
|
Task<PaginatedResult<UserResponse>> GetUsersAsync(int page = 1, int pageSize = 10, string? searchQuery = null, string? role = null, DateTime? startDate = null, DateTime? endDate = null);
|
||||||
Task<UserResponse> GetUserByIdAsync(int id);
|
Task<UserResponse> GetUserByIdAsync(int id);
|
||||||
|
Task<UserDetailResponse> GetUserDetailAsync(int id);
|
||||||
Task<UserResponse> CreateUserAsync(string userName, string email, string password, string role);
|
Task<UserResponse> CreateUserAsync(string userName, string email, string password, string role);
|
||||||
Task<UserResponse> UpdateUserAsync(int id, string userName, string email, string role);
|
Task<UserResponse> UpdateUserAsync(int id, string userName, string email, string role);
|
||||||
Task<bool> DeleteUserAsync(int id);
|
Task<bool> DeleteUserAsync(int id);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace Foxel.Services.Management;
|
|||||||
public class UserManagementService(
|
public class UserManagementService(
|
||||||
IDbContextFactory<MyDbContext> contextFactory) : IUserManagementService
|
IDbContextFactory<MyDbContext> contextFactory) : IUserManagementService
|
||||||
{
|
{
|
||||||
public async Task<PaginatedResult<UserResponse>> GetUsersAsync(int page = 1, int pageSize = 10)
|
public async Task<PaginatedResult<UserResponse>> 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 (page < 1) page = 1;
|
||||||
if (pageSize < 1) pageSize = 10;
|
if (pageSize < 1) pageSize = 10;
|
||||||
@@ -19,7 +19,39 @@ public class UserManagementService(
|
|||||||
// 构建查询
|
// 构建查询
|
||||||
var query = dbContext.Users
|
var query = dbContext.Users
|
||||||
.Include(u => u.Role)
|
.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();
|
var totalCount = await query.CountAsync();
|
||||||
@@ -216,6 +248,60 @@ public class UserManagementService(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<UserDetailResponse> 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)
|
private string HashPassword(string password)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ export {
|
|||||||
createUser,
|
createUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
batchDeleteUsers
|
batchDeleteUsers,
|
||||||
|
getUserDetail
|
||||||
} from './userManagementApi';
|
} from './userManagementApi';
|
||||||
|
|
||||||
// 导出PictureManagement API
|
// 导出PictureManagement API
|
||||||
|
|||||||
@@ -285,3 +285,33 @@ export interface BindAccountRequest {
|
|||||||
bindType: BindType;
|
bindType: BindType;
|
||||||
thirdPartyUserId: string;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,15 +5,28 @@ import {
|
|||||||
type UserResponse,
|
type UserResponse,
|
||||||
type CreateUserRequest,
|
type CreateUserRequest,
|
||||||
type AdminUpdateUserRequest,
|
type AdminUpdateUserRequest,
|
||||||
type BatchDeleteResult
|
type BatchDeleteResult,
|
||||||
|
type UserFilterRequest,
|
||||||
|
type UserDetailResponse
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
// 获取用户列表
|
// 获取用户列表
|
||||||
export const getUsers = async (
|
export const getUsers = async (
|
||||||
page: number = 1,
|
filters: UserFilterRequest = {}
|
||||||
pageSize: number = 10
|
|
||||||
): Promise<PaginatedResult<UserResponse>> => {
|
): Promise<PaginatedResult<UserResponse>> => {
|
||||||
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<UserResponse>;
|
return response as PaginatedResult<UserResponse>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,6 +38,14 @@ export const getUserById = async (id: number): Promise<BaseResult<UserResponse>>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 根据ID获取用户详情
|
||||||
|
export const getUserDetail = async (id: number): Promise<BaseResult<UserDetailResponse>> => {
|
||||||
|
return fetchApi<UserDetailResponse>(
|
||||||
|
`/management/user/get_user_detail/${id}`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// 创建用户
|
// 创建用户
|
||||||
export const createUser = async (
|
export const createUser = async (
|
||||||
userData: CreateUserRequest
|
userData: CreateUserRequest
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Table, Button, Card, Input, Space, Modal, Form,
|
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';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
UserOutlined, DeleteOutlined, EditOutlined,
|
UserOutlined, DeleteOutlined, EditOutlined,
|
||||||
SearchOutlined, ExclamationCircleOutlined, ReloadOutlined,
|
SearchOutlined, ExclamationCircleOutlined, ReloadOutlined,
|
||||||
UserAddOutlined, UserDeleteOutlined, TeamOutlined
|
UserAddOutlined, UserDeleteOutlined, TeamOutlined,
|
||||||
|
EyeOutlined, FilterOutlined, ClearOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
getUsers, deleteUser, createUser, updateUser, batchDeleteUsers, UserRole
|
getUsers, deleteUser, createUser, updateUser, batchDeleteUsers, UserRole
|
||||||
} from '../../../api';
|
} 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 { useOutletContext } from 'react-router';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
import type { Breakpoint } from 'antd';
|
import type { Breakpoint } from 'antd';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
const { confirm } = Modal;
|
const { confirm } = Modal;
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
const UserManagement: React.FC = () => {
|
const UserManagement: React.FC = () => {
|
||||||
const { isMobile } = useOutletContext<{ isMobile: boolean }>();
|
const { isMobile } = useOutletContext<{ isMobile: boolean }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [users, setUsers] = useState<UserResponse[]>([]);
|
const [users, setUsers] = useState<UserResponse[]>([]);
|
||||||
@@ -28,9 +33,13 @@ const UserManagement: React.FC = () => {
|
|||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
|
|
||||||
|
// 筛选状态
|
||||||
|
const [filters, setFilters] = useState<UserFilterRequest>({});
|
||||||
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
|
const [filterForm] = Form.useForm();
|
||||||
|
|
||||||
// 模态框状态
|
// 模态框状态
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const [modalTitle, setModalTitle] = useState('');
|
const [modalTitle, setModalTitle] = useState('');
|
||||||
@@ -38,10 +47,14 @@ const UserManagement: React.FC = () => {
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
// 加载用户数据
|
// 加载用户数据
|
||||||
const fetchUsers = useCallback(async (page = currentPage, size = pageSize) => {
|
const fetchUsers = useCallback(async (page = currentPage, size = pageSize, filterParams = filters) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await getUsers(page, size);
|
const response = await getUsers({
|
||||||
|
page,
|
||||||
|
pageSize: size,
|
||||||
|
...filterParams
|
||||||
|
});
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
setUsers(response.data || []);
|
setUsers(response.data || []);
|
||||||
setTotal(response.totalCount || 0);
|
setTotal(response.totalCount || 0);
|
||||||
@@ -54,7 +67,7 @@ const UserManagement: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [currentPage, pageSize]);
|
}, [currentPage, pageSize, filters]);
|
||||||
|
|
||||||
// 初始加载
|
// 初始加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -68,12 +81,38 @@ const UserManagement: React.FC = () => {
|
|||||||
fetchUsers(page, size || pageSize);
|
fetchUsers(page, size || pageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理搜索
|
// 处理筛选
|
||||||
const handleSearch = () => {
|
const handleFilter = async () => {
|
||||||
// 这里应该向后端发送搜索请求,但目前API不支持搜索,所以仅前端过滤
|
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);
|
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',
|
key: 'action',
|
||||||
render: (_: any, record: UserResponse) => (
|
render: (_: any, record: UserResponse) => (
|
||||||
<Space size="small">
|
<Space size="small">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => navigate(`/admin/users/${record.id}`)}
|
||||||
|
>
|
||||||
|
{isMobile ? '' : '查看'}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
@@ -309,6 +355,13 @@ const UserManagement: React.FC = () => {
|
|||||||
>
|
>
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<FilterOutlined />}
|
||||||
|
onClick={() => setShowFilters(!showFilters)}
|
||||||
|
type={showFilters ? 'primary' : 'default'}
|
||||||
|
>
|
||||||
|
高级筛选
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={10} md={8}>
|
<Col xs={24} sm={10} md={8}>
|
||||||
@@ -316,13 +369,51 @@ const UserManagement: React.FC = () => {
|
|||||||
placeholder="搜索用户名或邮箱"
|
placeholder="搜索用户名或邮箱"
|
||||||
allowClear
|
allowClear
|
||||||
enterButton={<SearchOutlined />}
|
enterButton={<SearchOutlined />}
|
||||||
onSearch={handleSearch}
|
onSearch={handleQuickSearch}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
value={searchQuery}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
{/* 高级筛选面板 */}
|
||||||
|
{showFilters && (
|
||||||
|
<>
|
||||||
|
<Card size="small" style={{ marginBottom: 16, backgroundColor: '#fafafa' }}>
|
||||||
|
<Form
|
||||||
|
form={filterForm}
|
||||||
|
layout="inline"
|
||||||
|
onFinish={handleFilter}
|
||||||
|
>
|
||||||
|
<Form.Item name="searchQuery" label="搜索关键词">
|
||||||
|
<Input placeholder="用户名或邮箱" style={{ width: 200 }} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="role" label="角色">
|
||||||
|
<Select placeholder="选择角色" style={{ width: 150 }} allowClear>
|
||||||
|
<Option value={UserRole.Administrator}>管理员</Option>
|
||||||
|
<Option value={UserRole.User}>普通用户</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="dateRange" label="注册时间">
|
||||||
|
<RangePicker style={{ width: 250 }} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit" icon={<SearchOutlined />}>
|
||||||
|
筛选
|
||||||
|
</Button>
|
||||||
|
<Button icon={<ClearOutlined />} onClick={handleClearFilters}>
|
||||||
|
清除
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
<Divider style={{ margin: '16px 0' }} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Card, Row, Col, Typography, Button, Space, Descriptions,
|
Card, Row, Col, Typography, Button, Space, Descriptions,
|
||||||
Avatar, Spin, Tabs, Statistic, message, Tag, Divider,
|
Spin, Tabs, Statistic, message, Tag, Divider,
|
||||||
Result
|
Result
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
UserOutlined, ArrowLeftOutlined, EditOutlined,
|
ArrowLeftOutlined,
|
||||||
PictureOutlined, FileImageOutlined, HeartOutlined
|
PictureOutlined, FileImageOutlined, HeartOutlined,
|
||||||
|
FolderOutlined, HddOutlined, CalendarOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useParams, useNavigate } from 'react-router';
|
import { useParams, useNavigate } from 'react-router';
|
||||||
import { getUserById } from '../../../api';
|
import { getUserDetail } from '../../../api';
|
||||||
import type { UserResponse } from '../../../api/types';
|
import type { UserDetailResponse } from '../../../api/types';
|
||||||
|
import UserAvatar from '../../../components/UserAvatar';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
const { TabPane } = Tabs;
|
const { TabPane } = Tabs;
|
||||||
@@ -19,7 +21,7 @@ const UserDetail: React.FC = () => {
|
|||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [user, setUser] = useState<UserResponse | null>(null);
|
const [user, setUser] = useState<UserDetailResponse | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
// 加载用户数据
|
// 加载用户数据
|
||||||
@@ -29,7 +31,7 @@ const UserDetail: React.FC = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await getUserById(parseInt(id));
|
const response = await getUserDetail(parseInt(id));
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
setUser(response.data);
|
setUser(response.data);
|
||||||
} else {
|
} else {
|
||||||
@@ -51,19 +53,23 @@ const UserDetail: React.FC = () => {
|
|||||||
navigate('/admin/users');
|
navigate('/admin/users');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 跳转到编辑页面
|
// 格式化存储大小
|
||||||
const handleEdit = () => {
|
const formatDiskUsage = (mb: number): string => {
|
||||||
navigate(`/admin/users/edit/${id}`);
|
if (mb < 1024) {
|
||||||
|
return `${mb.toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
return `${(mb / 1024).toFixed(2)} GB`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 模拟数据 - 实际项目中应该从API获取
|
// 格式化账户年龄
|
||||||
const userStats = {
|
const formatAccountAge = (days: number): string => {
|
||||||
totalPhotos: 125,
|
if (days < 30) {
|
||||||
totalAlbums: 14,
|
return `${days} 天`;
|
||||||
totalFavorites: 48,
|
} else if (days < 365) {
|
||||||
diskUsage: '1.2 GB',
|
return `${Math.floor(days / 30)} 个月`;
|
||||||
lastLogin: '2023-10-25 14:32',
|
} else {
|
||||||
accountAge: '268 天',
|
return `${Math.floor(days / 365)} 年 ${Math.floor((days % 365) / 30)} 个月`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -108,7 +114,7 @@ const UserDetail: React.FC = () => {
|
|||||||
<Col xs={24} lg={8}>
|
<Col xs={24} lg={8}>
|
||||||
<Card>
|
<Card>
|
||||||
<div style={{ textAlign: 'center', marginBottom: 24 }}>
|
<div style={{ textAlign: 'center', marginBottom: 24 }}>
|
||||||
<Avatar size={100} icon={<UserOutlined />} />
|
<UserAvatar size={100} email={user.email} />
|
||||||
<Title level={3} style={{ marginTop: 16, marginBottom: 0 }}>
|
<Title level={3} style={{ marginTop: 16, marginBottom: 0 }}>
|
||||||
{user.userName}
|
{user.userName}
|
||||||
</Title>
|
</Title>
|
||||||
@@ -118,9 +124,6 @@ const UserDetail: React.FC = () => {
|
|||||||
{user.role || '访客'}
|
{user.role || '访客'}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<Button type="primary" icon={<EditOutlined />} onClick={handleEdit}>
|
|
||||||
编辑用户
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
@@ -130,8 +133,11 @@ const UserDetail: React.FC = () => {
|
|||||||
<Descriptions.Item label="注册时间">
|
<Descriptions.Item label="注册时间">
|
||||||
{new Date(user.createdAt).toLocaleString()}
|
{new Date(user.createdAt).toLocaleString()}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="最近登录">
|
<Descriptions.Item label="账户年龄">
|
||||||
{user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : '未登录'}
|
{formatAccountAge(user.statistics.accountAgeDays)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="存储使用量">
|
||||||
|
{formatDiskUsage(user.statistics.diskUsageMB)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -148,62 +154,47 @@ const UserDetail: React.FC = () => {
|
|||||||
<Col xs={12} sm={8}>
|
<Col xs={12} sm={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="照片数量"
|
title="照片数量"
|
||||||
value={userStats.totalPhotos}
|
value={user.statistics.totalPictures}
|
||||||
prefix={<FileImageOutlined />}
|
prefix={<FileImageOutlined />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={8}>
|
<Col xs={12} sm={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="相册数量"
|
title="相册数量"
|
||||||
value={userStats.totalAlbums}
|
value={user.statistics.totalAlbums}
|
||||||
prefix={<PictureOutlined />}
|
prefix={<FolderOutlined />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={8}>
|
<Col xs={12} sm={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="收藏数量"
|
title="收藏数量"
|
||||||
value={userStats.totalFavorites}
|
value={user.statistics.totalFavorites}
|
||||||
prefix={<HeartOutlined />}
|
prefix={<HeartOutlined />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={8}>
|
<Col xs={12} sm={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="存储使用"
|
title="被收藏数量"
|
||||||
value={userStats.diskUsage}
|
value={user.statistics.favoriteReceivedCount}
|
||||||
|
prefix={<HeartOutlined style={{ color: '#ff4d4f' }} />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={8}>
|
<Col xs={12} sm={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="最近登录"
|
title="存储使用量"
|
||||||
value={userStats.lastLogin}
|
value={formatDiskUsage(user.statistics.diskUsageMB)}
|
||||||
|
prefix={<HddOutlined />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={8}>
|
<Col xs={12} sm={8}>
|
||||||
<Statistic
|
<Statistic
|
||||||
title="账户年龄"
|
title="账户年龄"
|
||||||
value={userStats.accountAge}
|
value={formatAccountAge(user.statistics.accountAgeDays)}
|
||||||
|
prefix={<CalendarOutlined />}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
<TabPane
|
|
||||||
tab={<span><FileImageOutlined />最近照片</span>}
|
|
||||||
key="2"
|
|
||||||
>
|
|
||||||
<div style={{ padding: '20px 0', textAlign: 'center' }}>
|
|
||||||
<Text type="secondary">此功能在开发中</Text>
|
|
||||||
</div>
|
|
||||||
</TabPane>
|
|
||||||
|
|
||||||
<TabPane
|
|
||||||
tab={<span><PictureOutlined />最近相册</span>}
|
|
||||||
key="3"
|
|
||||||
>
|
|
||||||
<div style={{ padding: '20px 0', textAlign: 'center' }}>
|
|
||||||
<Text type="secondary">此功能在开发中</Text>
|
|
||||||
</div>
|
|
||||||
</TabPane>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import AdminDashboard from '../pages/admin/dashboard/Index';
|
|||||||
import System from '../pages/admin/system/Index';
|
import System from '../pages/admin/system/Index';
|
||||||
import UserManagement from '../pages/admin/users/Index';
|
import UserManagement from '../pages/admin/users/Index';
|
||||||
import PictureManagement from '../pages/admin/pictures/Index';
|
import PictureManagement from '../pages/admin/pictures/Index';
|
||||||
|
import UserDetail from '../pages/admin/users/UserDetail';
|
||||||
|
|
||||||
export interface RouteConfig {
|
export interface RouteConfig {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -145,7 +146,19 @@ const routes: RouteConfig[] = [
|
|||||||
title: '用户管理'
|
title: '用户管理'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
path: 'users/:id',
|
||||||
|
key: 'user-detail',
|
||||||
|
label: '用户详情',
|
||||||
|
element: <UserDetail />,
|
||||||
|
area: 'admin',
|
||||||
|
hideInMenu: true,
|
||||||
|
breadcrumb: {
|
||||||
|
title: '用户详情',
|
||||||
|
parent: 'users'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
path: 'pictures',
|
path: 'pictures',
|
||||||
key: 'admin-picture',
|
key: 'admin-picture',
|
||||||
icon: <PictureOutlined />,
|
icon: <PictureOutlined />,
|
||||||
|
|||||||
Reference in New Issue
Block a user