React+TypeScript应用中实现卡片按库存与价格排序的方案求助
解决方案:统一管理完整卡片数据实现排序与筛选
问题根源
你遇到的排序失效问题,本质是关键排序字段(库存、价格)在子组件Card.tsx中异步获取,父组件Search无法拿到全局的完整数据集合,导致排序逻辑无法基于有效字段执行。之前新建页面处理库存筛选的方案虽然可行,但会造成代码冗余,最优方案是在父组件统一整合所有数据。
具体修改步骤
- 定义完整的卡片类型:包含基础数据和库存、价格等附加字段
- 在Search组件中统一获取附加数据:拿到基础卡片数据后,批量请求每张卡片的库存/价格接口,合并成完整数据
- 添加排序状态与逻辑:维护排序类型(按库存/价格升序/降序),对完整数据数组执行排序
- 优化筛选逻辑:无需跳转页面,直接在当前组件基于完整数据过滤库存状态
- 基于完整数据分页渲染:排序、筛选后的数据再做分页处理
修改后的完整代码
import { useEffect, useState } from "react"; import { useLocation } from "react-router-dom"; import { useAuth } from "../components/AuthContext"; import Cart from "../components/cart"; import Card from "../components/card"; import { useNavigate } from "react-router-dom"; // 定义完整的卡片类型,包含基础数据和附加的库存、价格字段 interface CardFullData { id: string; name: string; mana_cost?: string; colors?: Array<string>; set_name: string; lang: string; stock: number; // 库存数量 price: number; // 价格 } const Search = () => { // 存储包含库存、价格的完整卡片数据 const [fullCardData, setFullCardData] = useState<CardFullData[]>([]); const { search } = useLocation(); const query = new URLSearchParams(search).get("query"); const { user, fetchLatestUserDetails } = useAuth(); const [noResults, setNoResults] = useState(false); const [filterInStock, setFilterInStock] = useState(true); const navigate = useNavigate(); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const pageSize = 10; // 排序状态:存储排序字段和排序方向 const [sortConfig, setSortConfig] = useState<{ key: 'stock' | 'price'; direction: 'asc' | 'desc' } | null>(null); useEffect(() => { fetchLatestUserDetails().catch(console.error); }, []); // 获取单张卡片的库存、价格附加数据 const fetchCardExtraData = async (cardId: string): Promise<{ stock: number; price: number }> => { // 替换成你的库存/价格API地址 const response = await fetch(`https://your-api.com/cards/${cardId}/stock-price`); const data = await response.json(); return { stock: data.stock, price: data.price }; }; // 合并基础数据与附加数据 const mergeCardData = async (baseCards: any[]) => { try { // 用Promise.all批量请求所有卡片的附加数据 const extraDataPromises = baseCards.map(card => fetchCardExtraData(card.id)); const extraDataList = await Promise.all(extraDataPromises); // 合并成完整数据 const fullData = baseCards.map((card, index) => ({ ...card, stock: extraDataList[index].stock, price: extraDataList[index].price })) as CardFullData[]; setFullCardData(fullData); // 初始化分页 updateTotalPages(fullData); } catch (error) { console.error("获取附加数据失败:", error); } }; // 更新总页数 const updateTotalPages = (data: CardFullData[]) => { const filteredData = filterInStock ? data.filter(card => card.stock > 0) : data; setTotalPages(Math.ceil(filteredData.length / pageSize)); // 分页超出范围时重置到第一页 if (currentPage > Math.ceil(filteredData.length / pageSize)) { setCurrentPage(1); } }; useEffect(() => { const fetchBaseData = async () => { if (!query) return; const response = await fetch( `https://api.scryfall.com/cards/search?unique=prints&include_multilingual=true&q=${query}` ); const data = await response.json(); if (data.code === "not_found" || !data.data) { setNoResults(true); setFullCardData([]); return; } // 过滤语言 const filteredBaseCards = data.data.filter( (card: { lang: string }) => card.lang === "en" || card.lang === "es" ); if (filteredBaseCards.length === 0) { setNoResults(true); setFullCardData([]); return; } setNoResults(false); // 请求附加数据并合并 await mergeCardData(filteredBaseCards); }; fetchBaseData().catch(console.error); }, [query]); // 库存筛选变化时更新分页 useEffect(() => { updateTotalPages(fullCardData); }, [filterInStock, fullCardData]); // 排序逻辑 const handleSort = (key: 'stock' | 'price') => { let direction: 'asc' | 'desc' = 'asc'; if (sortConfig?.key === key && sortConfig.direction === 'asc') { direction = 'desc'; } setSortConfig({ key, direction }); }; // 获取经过筛选和排序后的数据集 const getProcessedData = () => { let processedData = [...fullCardData]; // 库存筛选 if (filterInStock) { processedData = processedData.filter(card => card.stock > 0); } // 排序 if (sortConfig) { processedData.sort((a, b) => { if (a[sortConfig.key] < b[sortConfig.key]) { return sortConfig.direction === 'asc' ? -1 : 1; } if (a[sortConfig.key] > b[sortConfig.key]) { return sortConfig.direction === 'asc' ? 1 : -1; } return 0; }); } return processedData; }; const goToNextPage = () => { const processedData = getProcessedData(); const maxPage = Math.ceil(processedData.length / pageSize); if (currentPage < maxPage) { setCurrentPage(prev => prev + 1); } }; const goToPrevPage = () => { if (currentPage > 1) { setCurrentPage(prev => prev - 1); } }; return ( <div className="searchpage"> <div className="filters"> <h1>Filtros</h1> <div> <input type="checkbox" id="stockCheckbox" checked={filterInStock} onChange={(e) => setFilterInStock(e.target.checked)} /> <label htmlFor="stockCheckbox">En Stock</label> </div> {/* 新增排序按钮 */} <div className="sort-options"> <button onClick={() => handleSort('stock')}> {sortConfig?.key === 'stock' ? sortConfig.direction === 'asc' ? '库存↑' : '库存↓' : '按库存排序'} </button> <button onClick={() => handleSort('price')}> {sortConfig?.key === 'price' ? sortConfig.direction === 'asc' ? '价格↑' : '价格↓' : '按价格排序'} </button> </div> </div> <div className="search-content"> {noResults ? ( <h3>No se encontraron resultados para: {query}</h3> ) : fullCardData.length === 0 ? ( <h3>Cargando datos...</h3> ) : ( <div> <h3>Resultados de busqueda para: {query}</h3> <div className="order-buttons"> <button onClick={goToPrevPage}>Anterior</button> <span>{currentPage} / {Math.ceil(getProcessedData().length / pageSize)}</span> <button onClick={goToNextPage}>Siguiente</button> </div> <ul> {getProcessedData() .slice((currentPage - 1) * pageSize, currentPage * pageSize) .map((card) => ( <li key={card.id} style={{ listStyle: "none" }}> <Card id={card.id} name={card.name} set_name={card.set_name} lang={card.lang} stock={card.stock} price={card.price} /> </li> ))} </ul> </div> )} </div> <div> {user ? ( <Cart items={user.cart} userConnected={true} /> ) : ( <Cart items={null} userConnected={false} /> )} </div> </div> ); }; export default Search;
关键优化点说明
- 数据统一管理:所有卡片数据(基础+附加)在父组件整合,排序、筛选逻辑基于完整数据集执行,避免状态分散
- 批量异步处理:用
Promise.all批量请求附加数据,提升加载效率,避免逐个请求的性能问题 - 无跳转筛选:直接在当前组件处理库存筛选,无需新建页面,减少代码冗余
- 响应式排序:点击排序按钮切换排序方向,实时更新展示顺序
- 类型安全:用TypeScript接口定义完整卡片类型,避免类型错误
内容的提问来源于stack exchange,提问作者Martin Garrido
相关产品推荐
相关产品推荐

