You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React+TypeScript应用中实现卡片按库存与价格排序的方案求助

解决方案:统一管理完整卡片数据实现排序与筛选

问题根源

你遇到的排序失效问题,本质是关键排序字段(库存、价格)在子组件Card.tsx中异步获取,父组件Search无法拿到全局的完整数据集合,导致排序逻辑无法基于有效字段执行。之前新建页面处理库存筛选的方案虽然可行,但会造成代码冗余,最优方案是在父组件统一整合所有数据。

具体修改步骤

  1. 定义完整的卡片类型:包含基础数据和库存、价格等附加字段
  2. 在Search组件中统一获取附加数据:拿到基础卡片数据后,批量请求每张卡片的库存/价格接口,合并成完整数据
  3. 添加排序状态与逻辑:维护排序类型(按库存/价格升序/降序),对完整数据数组执行排序
  4. 优化筛选逻辑:无需跳转页面,直接在当前组件基于完整数据过滤库存状态
  5. 基于完整数据分页渲染:排序、筛选后的数据再做分页处理

修改后的完整代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 22:11:01