feat(pictureManagement): add advanced filtering and search functionality for pictures

This commit is contained in:
shiyu
2025-06-02 12:52:46 +08:00
parent 686cc5fd11
commit 67168a99b6
5 changed files with 151 additions and 47 deletions
@@ -13,11 +13,14 @@ public class PictureManagementController(IPictureManagementService pictureManage
{ {
[HttpGet("get_pictures")] [HttpGet("get_pictures")]
public async Task<ActionResult<PaginatedResult<PictureResponse>>> GetPictures( public async Task<ActionResult<PaginatedResult<PictureResponse>>> GetPictures(
[FromQuery] int page = 1, [FromQuery] int pageSize = 10) [FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string? searchQuery = null,
[FromQuery] int? userId = null)
{ {
try try
{ {
var pictures = await pictureManagementService.GetPicturesAsync(page, pageSize); var pictures = await pictureManagementService.GetPicturesAsync(page, pageSize, searchQuery, userId);
return PaginatedSuccess(pictures.Data, pictures.TotalCount, pictures.Page, pictures.PageSize); return PaginatedSuccess(pictures.Data, pictures.TotalCount, pictures.Page, pictures.PageSize);
} }
catch (Exception ex) catch (Exception ex)
@@ -5,7 +5,7 @@ namespace Foxel.Services.Management;
public interface IPictureManagementService public interface IPictureManagementService
{ {
Task<PaginatedResult<PictureResponse>> GetPicturesAsync(int page = 1, int pageSize = 10); Task<PaginatedResult<PictureResponse>> GetPicturesAsync(int page = 1, int pageSize = 10, string? searchQuery = null, int? userId = null);
Task<PictureResponse> GetPictureByIdAsync(int id); Task<PictureResponse> GetPictureByIdAsync(int id);
Task<bool> DeletePictureAsync(int id); Task<bool> DeletePictureAsync(int id);
Task<BatchDeleteResult> BatchDeletePicturesAsync(List<int> ids); Task<BatchDeleteResult> BatchDeletePicturesAsync(List<int> ids);
@@ -10,7 +10,7 @@ public class PictureManagementService(
IDbContextFactory<MyDbContext> contextFactory, IDbContextFactory<MyDbContext> contextFactory,
IStorageService storageService) : IPictureManagementService IStorageService storageService) : IPictureManagementService
{ {
public async Task<PaginatedResult<PictureResponse>> GetPicturesAsync(int page = 1, int pageSize = 10) public async Task<PaginatedResult<PictureResponse>> GetPicturesAsync(int page = 1, int pageSize = 10, string? searchQuery = null, int? userId = null)
{ {
if (page < 1) page = 1; if (page < 1) page = 1;
if (pageSize < 1) pageSize = 10; if (pageSize < 1) pageSize = 10;
@@ -20,7 +20,21 @@ public class PictureManagementService(
// 构建查询 // 构建查询
var query = dbContext.Pictures var query = dbContext.Pictures
.Include(p => p.User) .Include(p => p.User)
.OrderByDescending(p => p.CreatedAt); .AsQueryable();
// 应用筛选条件
if (!string.IsNullOrWhiteSpace(searchQuery))
{
query = query.Where(p => p.Name.Contains(searchQuery) ||
(p.Description != null && p.Description.Contains(searchQuery)));
}
if (userId.HasValue)
{
query = query.Where(p => p.UserId == userId.Value);
}
query = query.OrderByDescending(p => p.CreatedAt);
// 获取总数和分页数据 // 获取总数和分页数据
var totalCount = await query.CountAsync(); var totalCount = await query.CountAsync();
+12 -2
View File
@@ -9,9 +9,19 @@ import {
// 获取图片列表 // 获取图片列表
export const getManagementPictures = async ( export const getManagementPictures = async (
page: number = 1, page: number = 1,
pageSize: number = 10 pageSize: number = 10,
searchQuery?: string,
userId?: number
): Promise<PaginatedResult<PictureResponse>> => { ): Promise<PaginatedResult<PictureResponse>> => {
const response = await fetchApi(`/management/picture/get_pictures?page=${page}&pageSize=${pageSize}`); const params = new URLSearchParams({
page: page.toString(),
pageSize: pageSize.toString()
});
if (searchQuery) params.append('searchQuery', searchQuery);
if (userId) params.append('userId', userId.toString());
const response = await fetchApi(`/management/picture/get_pictures?${params.toString()}`);
return response as PaginatedResult<PictureResponse>; return response as PaginatedResult<PictureResponse>;
}; };
+117 -40
View File
@@ -1,23 +1,23 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { import {
Table, Button, Card, Input, Space, Modal, Table, Button, Card, Input, Space, Modal,
message, Typography, Popconfirm, Row, Col, Image, Select message, Typography, Popconfirm, Row, Col, Image,
AutoComplete, Form, Divider
} from 'antd'; } from 'antd';
import { import {
PictureOutlined, DeleteOutlined, PictureOutlined, DeleteOutlined,
SearchOutlined, ExclamationCircleOutlined, ReloadOutlined, SearchOutlined, ExclamationCircleOutlined, ReloadOutlined,
FileImageOutlined, UserOutlined FileImageOutlined, UserOutlined, FilterOutlined, ClearOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import { import {
getManagementPictures, deleteManagementPicture, batchDeleteManagementPictures, getManagementPictures, deleteManagementPicture, batchDeleteManagementPictures,
getUsers getUsers
} from '../../../api'; } from '../../../api';
import type { PictureResponse, UserResponse } from '../../../api/types'; import type { PictureResponse } from '../../../api/types';
import { useOutletContext } from 'react-router'; import { useOutletContext } 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 { confirm } = Modal; const { confirm } = Modal;
const PictureManagement: React.FC = () => { const PictureManagement: React.FC = () => {
@@ -25,32 +25,55 @@ const PictureManagement: React.FC = () => {
// 状态管理 // 状态管理
const [pictures, setPictures] = useState<PictureResponse[]>([]); const [pictures, setPictures] = useState<PictureResponse[]>([]);
const [users, setUsers] = useState<UserResponse[]>([]); const [userOptions, setUserOptions] = useState<{ value: number; label: string }[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
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 [searchQuery, setSearchQuery] = useState('');
const [selectedUserId, setSelectedUserId] = useState<number | undefined>(); const [selectedUserId, setSelectedUserId] = useState<number | undefined>();
const [showFilters, setShowFilters] = useState(false);
const [filterForm] = Form.useForm();
// 加载用户列表 // 搜索用户(自动补全)
const fetchUsers = useCallback(async () => { const searchUsers = useCallback(async (searchValue: string) => {
if (!searchValue.trim()) {
setUserOptions([]);
return;
}
try { try {
const response = await getUsers(1, 1000); // 获取所有用户用于筛选 const response = await getUsers({
page: 1,
pageSize: 20,
searchQuery: searchValue
});
if (response.success && response.data) { if (response.success && response.data) {
setUsers(response.data || []); const options = response.data.map(user => ({
value: user.id,
label: `${user.userName} (${user.email})`
}));
setUserOptions(options);
} }
} catch (error) { } catch (error) {
console.error('Error fetching users:', error); console.error('Error searching users:', error);
} }
}, []); }, []);
// 加载图片数据 // 加载图片数据
const fetchPictures = useCallback(async (page = currentPage, size = pageSize) => { const fetchPictures = useCallback(async (
page = currentPage,
size = pageSize,
query = searchQuery,
userId = selectedUserId
) => {
setLoading(true); setLoading(true);
try { try {
const response = await getManagementPictures(page, size); const response = await getManagementPictures(page, size, query, userId);
if (response.success && response.data) { if (response.success && response.data) {
setPictures(response.data || []); setPictures(response.data || []);
setTotal(response.totalCount || 0); setTotal(response.totalCount || 0);
@@ -63,13 +86,12 @@ const PictureManagement: React.FC = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [currentPage, pageSize]); }, [currentPage, pageSize, searchQuery, selectedUserId]);
// 初始加载 // 初始加载
useEffect(() => { useEffect(() => {
fetchUsers();
fetchPictures(); fetchPictures();
}, [fetchUsers, fetchPictures]); }, [fetchPictures]);
// 处理页面变化 // 处理页面变化
const handlePageChange = (page: number, size?: number) => { const handlePageChange = (page: number, size?: number) => {
@@ -78,12 +100,35 @@ const PictureManagement: React.FC = () => {
fetchPictures(page, size || pageSize); fetchPictures(page, size || pageSize);
}; };
// 处理搜索 // 处理快速搜索
const handleSearch = () => { const handleQuickSearch = (value: string) => {
setSearchQuery(value);
setCurrentPage(1); setCurrentPage(1);
fetchPictures(1, pageSize); fetchPictures(1, pageSize, value, selectedUserId);
}; };
// 处理筛选
const handleFilter = async () => {
try {
const values = await filterForm.validateFields();
setSearchQuery(values.searchQuery || '');
setSelectedUserId(values.userId);
setCurrentPage(1);
fetchPictures(1, pageSize, values.searchQuery, values.userId);
} catch (error) {
console.error('Filter validation failed:', error);
}
};
// 清除筛选
const handleClearFilters = () => {
filterForm.resetFields();
setSearchQuery('');
setSelectedUserId(undefined);
setUserOptions([]);
setCurrentPage(1);
fetchPictures(1, pageSize, '', undefined);
};
// 处理删除图片 // 处理删除图片
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
@@ -137,12 +182,6 @@ const PictureManagement: React.FC = () => {
}; };
// 处理用户筛选 // 处理用户筛选
const handleUserFilter = (userId: number | undefined) => {
setSelectedUserId(userId);
// 这里应该根据用户ID筛选图片,但目前先简单刷新
setCurrentPage(1);
fetchPictures(1, pageSize);
};
// 表格列定义 // 表格列定义
const columns = [ const columns = [
@@ -251,33 +290,71 @@ const PictureManagement: React.FC = () => {
> >
</Button> </Button>
<Select <Button
style={{ width: 150 }} icon={<FilterOutlined />}
placeholder="筛选用户" onClick={() => setShowFilters(!showFilters)}
allowClear type={showFilters ? 'primary' : 'default'}
value={selectedUserId}
onChange={handleUserFilter}
> >
{users.map(user => (
<Option key={user.id} value={user.id}> </Button>
{user.userName}
</Option>
))}
</Select>
</Space> </Space>
</Col> </Col>
<Col xs={24} sm={10} md={8}> <Col xs={24} sm={10} md={8}>
<Input.Search <Input.Search
placeholder="搜索图片名称" placeholder="搜索图片名称或描述"
allowClear allowClear
enterButton={<SearchOutlined />} enterButton={<SearchOutlined />}
onSearch={handleSearch} onSearch={handleQuickSearch}
onChange={(e) => setSearchQuery(e.target.value)}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/> />
</Col> </Col>
</Row> </Row>
{/* 高级筛选面板 */}
{showFilters && (
<>
<Card size="small" style={{ marginBottom: 16, backgroundColor: '#fafafa' }}>
<Form
form={filterForm}
layout="inline"
onFinish={handleFilter}
initialValues={{
searchQuery: searchQuery,
userId: selectedUserId
}}
>
<Form.Item name="searchQuery" label="搜索关键词">
<Input placeholder="图片名称或描述" style={{ width: 200 }} />
</Form.Item>
<Form.Item name="userId" label="上传用户">
<AutoComplete
style={{ width: 250 }}
options={userOptions}
onSearch={searchUsers}
placeholder="输入用户名或邮箱搜索"
allowClear
filterOption={false}
/>
</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}