mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-08 00:56:49 +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")]
|
||||
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
|
||||
{
|
||||
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<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
|
||||
{
|
||||
[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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
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<UserDetailResponse> GetUserDetailAsync(int id);
|
||||
Task<UserResponse> CreateUserAsync(string userName, string email, string password, string role);
|
||||
Task<UserResponse> UpdateUserAsync(int id, string userName, string email, string role);
|
||||
Task<bool> DeleteUserAsync(int id);
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Foxel.Services.Management;
|
||||
public class UserManagementService(
|
||||
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 (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<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)
|
||||
{
|
||||
|
||||
@@ -67,7 +67,8 @@ export {
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
batchDeleteUsers
|
||||
batchDeleteUsers,
|
||||
getUserDetail
|
||||
} from './userManagementApi';
|
||||
|
||||
// 导出PictureManagement API
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<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>;
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
userData: CreateUserRequest
|
||||
|
||||
@@ -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<UserResponse[]>([]);
|
||||
@@ -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<React.Key[]>([]);
|
||||
|
||||
// 筛选状态
|
||||
const [filters, setFilters] = useState<UserFilterRequest>({});
|
||||
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) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => navigate(`/admin/users/${record.id}`)}
|
||||
>
|
||||
{isMobile ? '' : '查看'}
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
@@ -309,6 +355,13 @@ const UserManagement: React.FC = () => {
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
icon={<FilterOutlined />}
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
type={showFilters ? 'primary' : 'default'}
|
||||
>
|
||||
高级筛选
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24} sm={10} md={8}>
|
||||
@@ -316,13 +369,51 @@ const UserManagement: React.FC = () => {
|
||||
placeholder="搜索用户名或邮箱"
|
||||
allowClear
|
||||
enterButton={<SearchOutlined />}
|
||||
onSearch={handleSearch}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
value={searchQuery}
|
||||
onSearch={handleQuickSearch}
|
||||
/>
|
||||
</Col>
|
||||
</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
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card, Row, Col, Typography, Button, Space, Descriptions,
|
||||
Avatar, Spin, Tabs, Statistic, message, Tag, Divider,
|
||||
Spin, Tabs, Statistic, message, Tag, Divider,
|
||||
Result
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined, ArrowLeftOutlined, EditOutlined,
|
||||
PictureOutlined, FileImageOutlined, HeartOutlined
|
||||
ArrowLeftOutlined,
|
||||
PictureOutlined, FileImageOutlined, HeartOutlined,
|
||||
FolderOutlined, HddOutlined, CalendarOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
import { getUserById } from '../../../api';
|
||||
import type { UserResponse } from '../../../api/types';
|
||||
import { getUserDetail } from '../../../api';
|
||||
import type { UserDetailResponse } from '../../../api/types';
|
||||
import UserAvatar from '../../../components/UserAvatar';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { TabPane } = Tabs;
|
||||
@@ -19,7 +21,7 @@ const UserDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [user, setUser] = useState<UserResponse | null>(null);
|
||||
const [user, setUser] = useState<UserDetailResponse | null>(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 = () => {
|
||||
<Col xs={24} lg={8}>
|
||||
<Card>
|
||||
<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 }}>
|
||||
{user.userName}
|
||||
</Title>
|
||||
@@ -118,9 +124,6 @@ const UserDetail: React.FC = () => {
|
||||
{user.role || '访客'}
|
||||
</Tag>
|
||||
</div>
|
||||
<Button type="primary" icon={<EditOutlined />} onClick={handleEdit}>
|
||||
编辑用户
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
@@ -130,8 +133,11 @@ const UserDetail: React.FC = () => {
|
||||
<Descriptions.Item label="注册时间">
|
||||
{new Date(user.createdAt).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最近登录">
|
||||
{user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : '未登录'}
|
||||
<Descriptions.Item label="账户年龄">
|
||||
{formatAccountAge(user.statistics.accountAgeDays)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="存储使用量">
|
||||
{formatDiskUsage(user.statistics.diskUsageMB)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
@@ -148,62 +154,47 @@ const UserDetail: React.FC = () => {
|
||||
<Col xs={12} sm={8}>
|
||||
<Statistic
|
||||
title="照片数量"
|
||||
value={userStats.totalPhotos}
|
||||
value={user.statistics.totalPictures}
|
||||
prefix={<FileImageOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={8}>
|
||||
<Statistic
|
||||
title="相册数量"
|
||||
value={userStats.totalAlbums}
|
||||
prefix={<PictureOutlined />}
|
||||
value={user.statistics.totalAlbums}
|
||||
prefix={<FolderOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={8}>
|
||||
<Statistic
|
||||
title="收藏数量"
|
||||
value={userStats.totalFavorites}
|
||||
value={user.statistics.totalFavorites}
|
||||
prefix={<HeartOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={8}>
|
||||
<Statistic
|
||||
title="存储使用"
|
||||
value={userStats.diskUsage}
|
||||
title="被收藏数量"
|
||||
value={user.statistics.favoriteReceivedCount}
|
||||
prefix={<HeartOutlined style={{ color: '#ff4d4f' }} />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={8}>
|
||||
<Statistic
|
||||
title="最近登录"
|
||||
value={userStats.lastLogin}
|
||||
title="存储使用量"
|
||||
value={formatDiskUsage(user.statistics.diskUsageMB)}
|
||||
prefix={<HddOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={12} sm={8}>
|
||||
<Statistic
|
||||
title="账户年龄"
|
||||
value={userStats.accountAge}
|
||||
value={formatAccountAge(user.statistics.accountAgeDays)}
|
||||
prefix={<CalendarOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</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>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
@@ -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: <UserDetail />,
|
||||
area: 'admin',
|
||||
hideInMenu: true,
|
||||
breadcrumb: {
|
||||
title: '用户详情',
|
||||
parent: 'users'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'pictures',
|
||||
key: 'admin-picture',
|
||||
icon: <PictureOutlined />,
|
||||
|
||||
Reference in New Issue
Block a user