feat(userManagement): add user detail view and filtering capabilities

This commit is contained in:
shiyu
2025-06-02 12:46:11 +08:00
parent 6243b9b62a
commit 686cc5fd11
11 changed files with 380 additions and 86 deletions
+42 -2
View File
@@ -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);
}
}
} }
+8 -9
View File
@@ -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);
+88 -2
View File
@@ -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)
{ {
+2 -1
View File
@@ -67,7 +67,8 @@ export {
createUser, createUser,
updateUser, updateUser,
deleteUser, deleteUser,
batchDeleteUsers batchDeleteUsers,
getUserDetail
} from './userManagementApi'; } from './userManagementApi';
// 导出PictureManagement API // 导出PictureManagement API
+30
View File
@@ -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;
}
+25 -4
View File
@@ -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
+106 -15
View File
@@ -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}
+42 -51
View File
@@ -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>
+14 -1
View File
@@ -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 />,