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

This commit is contained in:
shiyu
2025-06-02 12:43:05 +08:00
parent 6243b9b62a
commit 686cc5fd11
11 changed files with 380 additions and 86 deletions

View File

@@ -67,7 +67,8 @@ export {
createUser,
updateUser,
deleteUser,
batchDeleteUsers
batchDeleteUsers,
getUserDetail
} from './userManagementApi';
// 导出PictureManagement API

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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}

View File

@@ -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>

View File

@@ -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 />,