mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-04 23:29:00 +08:00
feat(pictureManagement): add advanced filtering and search functionality for pictures
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user