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

React 卡片点击仅展开单个及30秒隐藏后重新触发显示问题求解

问题根因

  • 你当前用了全局共享的showMeta、visible布尔状态控制所有卡片的显隐,所以任意卡片触发状态变更会同步作用到所有卡片,导致全量展开
  • useTimeout在组件挂载时就直接启动30秒倒计时,30秒后visible被永久设为false,没有重置逻辑,也没有和展开行为绑定,所以后续点击无法重新触发显示

修复方案

我们将状态改为存储当前展开的城市ID,而非全局布尔值,同时将useTimeout的启动时机和展开行为绑定,倒计时结束后自动清空当前展开ID,实现复用:

  1. 替换原有全局显隐状态,用expandedCityId存储当前展开的城市唯一标识
  2. 调整useTimeout的延迟参数,仅当有展开的城市时才启动30秒倒计时,倒计时结束后清空展开ID
  3. 卡片显隐逻辑改为判断当前卡片ID是否和expandedCityId匹配,仅匹配的卡片展开
  4. 额外修复原代码中直接对cities调用sort修改原数组的副作用问题,改为拷贝后排序

修复后的核心代码

import React from "react";
import { FaPlus } from "react-icons/fa";
import useTimeout from "../../../common/useTimeout";
import { Modal } from "../../../common/Modal";
import { ToggleState } from "../../../common/Toggle";
import { BsSearch } from "react-icons/bs";
import AdvancedFilter from "./AdvancedFilter";

export default function CitiesList({ cities }: { cities: any }): JSX.Element {
  const [filter, setFilter] = React.useState("");
  const [sortType, setSortType] = React.useState("");
  // 存储当前展开的城市ID,null表示无展开卡片
  const [expandedCityId, setExpandedCityId] = React.useState<number | string | null>(null);

  console.log(filter);

  // 修复原sort修改原数组的问题,先拷贝再排序
  const sorted = [...cities].sort((a: { name: string }, b: { name: any }) => {
    const isReversed = sortType === "asc" ? 1 : -1;
    return isReversed * a.name.localeCompare(b.name);
  });

  const onSort = (sortType: React.SetStateAction<string>) => {
    console.log("changed");
    setSortType(sortType);
  };

  // 倒计时结束后清空展开ID
  const hide = () => setExpandedCityId(null);

  // 仅当有展开的城市时才启动30秒倒计时,否则不启动
  useTimeout(hide, expandedCityId ? 30000 : null);

  console.log("CITIES", cities);
  console.log({ expandedCityId });

  return (
    <div style={{ marginTop: "3rem" }}>
      <div
        style={{
          display: "flex",
          justifyContent: "space-between",
          alignItems: "center",
          marginBottom: "20px",
        }}
      >
        <div>List of cities</div>
        <div style={{ display: "flex", alignItems: "center" }}>
          <div style={{ marginRight: "1rem" }}>
            <ToggleState
              render={({ isOpen, open, close }) => {
                return (
                  <>
                    <button
                      type="button"
                      className="btn btn-primary"
                      onClick={() => {
                        isOpen ? close() : open();
                      }}
                    >
                      Advanced Filter
                    </button>
                    <Modal
                      isActive={isOpen}
                      modalContentWidth={"30%"}
                      header={() => "Advanced Filter"}
                      close={() => close()}
                      renderBody={() => {
                        return <AdvancedFilter close={() => close()} />;
                      }}
                    ></Modal>
                  </>
                );
              }}
            />
          </div>

          <div style={{ position: "relative", marginRight: "1rem" }}>
            <input
              type="text"
              placeholder="Filter"
              name="namePrefix"
              style={{ padding: "0.35rem" }}
              onChange={(e: any) => {
                setFilter(e.target.value);
              }}
            />
            <div style={{ position: "absolute", top: "5px", right: "5px" }}>
              <BsSearch size="16" />
            </div>
          </div>

          <div style={{ width: "8rem" }}>
            <div className="btn-group">
              <button
                type="button"
                className="btn dropdown-toggle sort-button"
                data-bs-toggle="dropdown"
                aria-expanded="false"
              >
                {sortType === "asc"
                  ? "Ascending"
                  : sortType === "desc"
                  ? "Descending"
                  : "Select"}
              </button>
              <ul className="dropdown-menu sort-button">
                <li>
                  <button
                    className="dropdown-item"
                    type="button"
                    onClick={() => onSort("asc")}
                  >
                    Ascending
                  </button>
                </li>
                <li>
                  <button
                    className="dropdown-item"
                    type="button"
                    onClick={() => onSort("desc")}
                  >
                    Descending
                  </button>
                </li>
              </ul>
            </div>
          </div>
        </div>
      </div>
      <div>
        <div>
          <div className="row">
            {cities &&
              sorted.map((item: any, index: number) => (
                <div className="col-lg-3" key={index}>
                  <div
                    className="card"
                    style={{
                      textAlign: "center",
                      display: "flex",
                      justifyContent: "center",
                      paddingBottom: "1rem",
                      marginBottom: "1rem",
                      marginRight: "1rem",
                    }}
                  >
                    <div className="card-body">
                      <h5 className="card-title">{item.name}</h5>
                    </div>
                    {/* 仅当前卡片ID和展开ID匹配时显示详情 */}
                    {expandedCityId === item.id ? (
                      <div>
                        <p>Longitude: {item.longitude}</p>
                        <p>Latitude: {item.latitude}</p>
                        <p>Population: {item.population}</p>
                        {/* <p>Time Zone: America</p> */}
                      </div>
                    ) : (
                      <div
                        onClick={() => {
                          // 点击时把当前卡片ID设为展开ID
                          setExpandedCityId(item.id);
                        }}
                        style={{ cursor: "pointer" }}
                      >
                        <FaPlus size="18" />
                      </div>
                    )}
                  </div>
                </div>
              ))}
          </div>
        </div>
      </div>
      <div
        style={{ marginTop: "30px", display: "flex", justifyContent: "center" }}
      >
        {cities && cities.length > 10 ? (
          <button className="secondary-button">Load More</button>
        ) : (
          <p>There are no more cities</p>
        )}
      </div>
    </div>
  );
}

useTimeout代码无需改动。

效果说明

  • 点击单个卡片的加号,仅对应ID匹配的卡片会展开详情,其余卡片保持收起状态
  • 卡片展开后自动启动30秒倒计时,倒计时结束后自动收起
  • 收起后再次点击任意卡片的加号,会重新触发对应卡片展开和30秒倒计时,无需刷新页面

内容的提问来源于stack exchange,提问作者rafi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.04 09:39:01