mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-22 16:50:09 +08:00
feat(userManagement): add user detail view and filtering capabilities
This commit is contained in:
@@ -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