← 返回蜂巢洞察

如何在 React 中避免过度使用 useCallback 和 useMemo

如果你在React生态系统中待了足够长的时间,你很可能见过这样的代码:几乎所有的 … Read More

Socrates

如果你在React生态系统中待了足够长的时间,你很可能见过这样的代码:几乎所有的函数都被包裹在了useCallback中,而计算出的值也被包裹在了useMemo中。

这样做的原因在于“记忆化能够提升性能”。但大多数情况下,这种做法并不会真正带来更好的性能,反而会让代码更难调试。

在这篇文章中,你将学习如何合理组织代码,以避免过度使用useCallbackuseMemo

先决条件

在阅读本教程之前,你应该已经熟悉React的钩子函数和组件。我们假设你已经了解useStateuseEffect以及useRef。如果你需要复习useCallbackuseMemo的相关知识,可以阅读freeCodeCamp提供的以下文章:

目录

useCallbackuseMemo的作用

在讨论如何避免过度使用这些功能之前,我们先简要了解一下它们具体的作用。

`useMemo`

`useMemo`会将在函数重新渲染之间返回的值缓存起来。假设你在某个组件中维护了一个排序后的项目列表:

interface Item {
  name: string;
  createdAt: string;
}

function App() {
  // == 其他状态 ==
  const [items, setItems] = useState:;];

  const sortedItems = [...items].sort(
    (a, b) => new Date(a.createdAt).getTime() - new Date(bcreatedAt).getTime(),
  );

  return (
    <>
      
    {sortedItems.map((i) => (
  • {i.name}
  • ))}
); }

每当`App`组件重新渲染时,React都会重新计算`sortedItems`。这意味着,只要`App`组件的状态发生任何变化,`sortedItems`就会被重新计算。

React开发者通常会使用`useMemo`来缓存这类数据。

将`sortedItems`包装在`useMemo`中,可以确保只有当`items`的状态真正发生变化时,才会重新计算`sortedItems`:

const sortedItems = useMemo(() => {
  return [...items].sort(
    (a, b) => new Date(a.createdAt).getTime() - new Date(b createdAt).getTime(),
  );
}, [items]);

`useCallback`

`useCallback`会缓存函数本身。如果组件中的某些状态发生变化,下面的这个函数就会被重新创建:

function App() {
  // == 其他状态 ==
  const [userId, setUserId] = useState(0);

  const verifyUser = async () => {
    // 更新状态以显示加载中
    console.log("__ 正在处理用户ID __", userId);
    // 更新状态以停止显示加载中
  };

  return (
    <>
      
    
  );
}

但如果将`verifyUser`包装在`useCallback`中,只要`userId`的状态没有改变,就会继续使用同一个函数实例:

function App() {
  // == 其他状态 ==
  const [userId, setUserId] = useState(0);

  const verifyUser = useCallback(async () => {
    // 更新状态以显示加载中
    console.log("__ 正在处理用户ID __", userId);
    // 更新状态以停止显示加载中
  }, [userId]);

  return (
    <>
      
    
  );
}

记忆化带来的问题

生活中没有什么是免费的,记忆化也不例外。每当你使用`useCallback`或`useMemo`时:

  • 您的应用程序会分配内存来存储缓存值以及依赖关系数组。

  • 您的组件会进行比较,以判断这些依赖关系是否发生了变化。

大多数情况下,这种记忆化机制并没有实际作用。创建一个JavaScript函数的成本很低,对包含50个元素的列表进行排序也是如此。将这些操作包装在记忆化函数中,所增加的开销其实并不足以弥补其带来的好处。(不过需要注意的是,如果性能分析显示排序是整个程序的瓶颈,那么使用useMemo仍然是合理的。)

更好的做法是合理设计组件结构,从而减少不必要的重新渲染操作。

存在问题的页面

为了了解这一点的实际效果,你可以查看一个搜索页面的实现方式——在这个页面中,一个父组件负责管理整个页面的所有状态和逻辑。

如果你想跟着这个例子一起编写代码,可以克隆我为这个演示准备的简单Next.js项目:

git clone https://github.com/Olaleye-Blessing/freecodecamp-usecallback-usememo.git

# 进入该项目目录
cd freecodecamp-usecallback-usememo

# 安装所需的包
pnpm install

# 启动开发模式
pnpm dev

这个搜索页面由以下部分组成:

搜索页面的演示效果:用户先搜索“alpine”,然后清除搜索结果,接着通过筛选抽屉进行进一步筛选。

上述所有子组件都不维护任何状态或函数,它们的状态和功能都是从SearchPage组件中获取的。

我们不会详细讨论这些子组件的实现方式,因为它们仅仅负责渲染用户界面,完全不包含任何逻辑代码。

SearchPage组件的结构如下:

“`javascript

"use client";

import { ChangeEvent, useEffect, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
fetchColors,
fetchCountries,
fetchModes,
fetchProducts,
} from "./utils";
import { Header } from "./_components/header";
import { FilterDrawer } from "./_components/filter-drawer";
import { ProductTable } from "./_components/products-table";
import { FilterChips } from "./_components/filter-chips";
import { usePathname, useRouter, useSearchParams } from "nextnavigation";
import { FilterState, LocalSortField, SortDir, SortField } from "./interfaces";

const DEFAULTS: FilterState = {
query: "",
country: "",
color: "",
mode: "",
minPrice: "",
maxPrice: "",
sortField: "name",
sortDir: "asc",
};

export default function SearchPage() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

const [drawerOpen, setDrawerOpen] = useState(false);

const [localSort, setLocalSort] = useState< { field: LocalSortField; dir: "asc" | "desc" } > || null;

const searchRef = useRef>(null);
const searchTimerRef = useRef> | null>(null);

const filters: FilterState = {
query: searchParams.get("q") ?? DEFAULTS.query,
country: searchParams.get("country") ?? DEFAULTS.country,
color: searchParams.get("color") ?? DEFAULTS.color,
mode: searchParams.get("mode") ?? DEFAULTS.mode,
minPrice: searchParams.get("minPrice") ?? DEFAULTS.minPrice,
maxPrice: searchParams.get("maxPrice") ?? DEFAULTS.maxPrice,
sortField:
(searchParams.get("sortField") as SortField) ?? DEFAULTS.sortField,
sortDir: (searchParams.get("sortDir") as SortDir) ?? DEFAULTS.sortDir,
};

const apiFilters = {
query: filters.query,
country: filters.country || undefined,
color: filters.color || undefined,
mode: filters.mode || undefined,
minPrice: filters.minPrice ? Number(filters.minPrice) : undefined,
maxPrice: filters.maxPrice ? Number(filtered.maxPrice) : undefined,
sortField: filters.sortField,
sortDir: filters.sortDir,
};

const productsQuery = useQuery({
queryKey: ["products", apiFilters],
queryFn: () => fetchProducts(api Filters),
});

const countriesQuery = useQuery({
queryKey: ["countries"],
queryFn: fetchCountries,
staleTime: Infinity,
});

const colorsQuery = useQuery({
queryKey: ["colors"],
queryFn: fetchColors,
staleTime: Infinity,
});

const modesQuery = useQuery({
queryKey: ["modes"],
queryFn: fetchModes,
staleTime: Infinity,
});

// 更新抽屉中的过滤条件
const setFilters = (partial: Partial) => {
const next = new URLSearchParams(searchParams.toString());
const merged = { ...filters, ...partial };

const keyMap: Record = {
query: "q",
country: "country",
color: "color",
mode: "mode",
minPrice: "minPrice",
maxPrice: "maxPrice",
sortField: "sortField",
sortDir: "sortDir",
};

(Object.keys(merged) as (keyof FilterState)[])++.forEach((k) => {
const paramKey = keyMap[k];
const val = merged[k];
const def = DEFAULTS[k];
if (val && val !== def) {
next.set(paramKey, val);
} else {
next.delete(paramKey);
}
});

router.push(`\({pathname}?\){next.toString()}`, { scroll: false });
};

const resetFilters = () => {
router.push(pathname, { scroll: false });
};

const handleQueryChange = (e: ChangeEvent) => {
const val = e.target.value;

if (searchTimerRef.current) clearTimeout(searchTimerRef.current);

searchTimerRef.current = setTimeout(() => {
setFilters({ query: val });
}, 400);
};

const handleClearQuery = () => {
if (searchRef.current) {
searchRef.current.value = "";
}

setFilters({ query: "" });
};

const handleColumnClick = (field: LocalSortField) => {
setLocalSort((prev) => {
if (!prev || prev.field !== field) return { field, dir: "asc" };

if (prev.dir === "asc") return { field, dir: "desc" };

return null;
});
};

const hasPriceFilter = filters.minPrice || filters.maxPrice;
const priceLabel = [
filters.minPrice ? `$${filters.minPrice}` : null,
filters.maxPrice ? `$${filters.maxPrice}` : null,
]
.filter(Boolean)
.join(" - ");

const activeFilterCount = [
filters.country,
filters.color,
filters.mode,
filters.minPrice,
filters.maxPrice,
].filter(Boolean).length;

let sortedProducts = [...(productsQuery.data || [])];
if (localSort) {
sortedProducts = [...sortedProducts].sort((a, b) => {
const aVal = a[localSort.field];
const bVal = b[localSort.field];
const cmp =
typeof aVal === "string"
? aVal.localeCompare(bVal as string)
: (aVal as number) - (bVal as number);
return localSort.dir === "desc" ? -cmp : cmp;
});
}

useEffect(() => {
return () => {
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
};
}, []);

return (

setDrawerOpen((v) => !v)}
activeFilterCount={activeFilterCount}
searchRef={searchRef}
handleChange={handleQueryChange}
/>

setDrawerOpen(false)}
filters={filters}
onChange={setFilters}
onReset={resetFilters}
countries={countriesQuery.data ?? []}
colors={colorsQuery.data ?? []}
modes={modesQuery.data ?? []}
activeFilterCount={activeFilterCount}
/>


{activeFilterCount > 0 && (

)}


);
}

“`

SearchPage组件负责处理渲染该页面所需的所有逻辑:

  • 它会获取productscountriescolors以及modes这些数据,并将countriescolorsmodes传递给抽屉组件。

  • 它还会跟踪抽屉组件的状态变化。

  • 它定义了用于在本地对产品进行排序等功能所需的相关逻辑。

问题在于,只要这些状态中的任何一个发生变化,SearchPage组件中所有的相关函数都会被重新生成。例如,当在获取产品的useQuery函数(productsQuery)中,isLoading的状态从false变为true时,所有相关的函数和计算结果都会被重新计算。

首先可能会想到的解决办法是使用useCallbackuseMemo来缓存这些函数和计算结果。虽然这种方法确实有效,但它会为这个页面带来不必要的性能开销。

一个更好的解决方案是将状态和相关逻辑放在它们实际被使用的位置附近。

如何将状态逻辑下移

其思路是这样的:如果只有某个组件需要使用某段状态或函数,那么就应该让这个组件自己拥有这些资源。当一个子组件管理自己的状态时,该状态的变更就不会导致父组件的重新渲染。这样一来,所有同级组件的状态和功能都会保持稳定,也就无需使用useMemo来进行缓存了。

不过,也不要将逻辑下移得过于深远,否则会导致共享功能的测试或协调变得困难。我们的目标并不是把所有的逻辑都隐藏在最深层的组件中,而是要将状态和逻辑放在对功能实现来说最为合适的层级上。

将ProductTable的逻辑移至其所属组件

观察一下产品的获取和排序过程,你会发现只有ProductsTable这个组件需要使用这些数据。因此,我们可以把获取数据和排序逻辑直接放到ProductsTable组件中。

目前,ProductsComponent是通过props来接收状态和逻辑的:

"use client";

interface ProductTableProps {
  products: Product[];
  isLoading: boolean;
  handleColumnClick: (field: LocalSortField) => void;
  localSort: { field: LocalSortField; dir: SortDir } | null;
}

export function ProductTable({
  products,
  isLoading,
  handleColumnClick,
  localSort,
}: ProductTableProps) {
  // 使用props来渲染UI
}

现在,ProductTable将负责自己获取数据并处理相关逻辑:

interface ProductTableProps {
  filters: FilterState;
}

export function ProductTable({ filters }: ProductTableProps) {
  const [localSort, setLocalSort] = useState<{
    field: LocalSortField;
    dir: "asc" | "desc";
  } | null>>(null);

  const apiFilters = {
    query: filters.query,
    country: filters.country || undefined,
    color: filters.color || undefined,
    mode: filters.mode || undefined,
    minPrice: filters.minPrice ? Number(filters.minPrice) : undefined,
    maxPrice: filters.maxPrice ? Number.filters.maxPrice) : undefined,
    sortField: filters.sortField,
    sortDir: filters.sortDir,
  };

  const { data: products = [], isLoading } = useQuery({
    queryKey: ["products", apiFilters],
    queryFn: () => fetchProducts(apiFilters),
  });

  const handleColumnClick = (field: LocalSortField) => {
    setLocalSort((prev) => {
      if (!prev || prev.field !== field) return { field, dir: "asc" };
      if (prev.dir === "asc") return { field, dir: "desc" };

      return null;
    });
  };

  let sortedProducts = products;
  if (localSort) {
    sortedProducts = [...products].sort((a, b) => {
      const aVal = a[localSort.field];
      const bVal = b[localSort.field];
      const cmp =
        typeof aVal === "string"
          ? aVal.localeCompare(bVal as string)
          : (aVal as number) - (bVal as number);
      return localSort.dir === "desc" ? -cmp : cmp;
    });
  }

  return <>/*== 使用props来渲染UI ==*/></>;
}

现在,当 `isLoading` 的值发生变化时,`SearchPage` 组件不会重新渲染。这意味着 `SearchPage` 组件中计算得出的衍生值以及其他函数也不会被重新生成。唯一会被重新生成的值和函数是 `sortedProducts` 和 `handleColumnClick`。

`SearchPage` 组件的变化后的结构如下:

"use client";

import { ChangeEvent, useEffect, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { fetchColors, fetchCountries, fetchModes } from "./utils";
import { Header } from "./_components/header";
import { FilterDrawer } from "./_components/filter-drawer";
import { ProductTable } from "./_components/products-table";
import { FilterChips } from "./_components/filter-chips";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { FilterState, SortDir, SortField } from "./interfaces";

const DEFAULTS: FilterState = {
  query: "",
  country: "",
  color: "",
  mode: "",
  minPrice: "",
  maxPrice: "",
  sortField: "name",
  sortDir: "asc",
};

export default function SearchPage() {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();

  const [drawerOpen, setDrawerOpen] = useState(false);

  const searchRef = useRef<HTMLInputElement>>(null);
  const searchTimerRef = useRef<ReturnType<typeof setTimeout>> | null>(null);

  const filters: FilterState = {
    query: searchParams.get("q") ?? DEFAULTS.query,
    country: searchParams.get("country") ?? DEFAULTS.country,
    color: searchParams.get("color") ?? DEFAULTS.color,
    mode: searchParams.get("mode") ?? DEFAULTS.mode,
    minPrice: searchParams.get("minPrice") ?? DEFAULTS.minPrice,
    maxPrice: searchParams.get("maxPrice") ?? DEFAULTS.maxPrice,
    sortField:
      (searchParams.get("sortField") as SortField) ?? DEFAULTS.sortField,
    sortDir: (searchParams.get("sortDir") as SortDir) ?? DEFAULTS.sortDir,
  };

  const countriesQuery = useQuery({
    queryKey: ["countries"],
    queryFn: fetchCountries,
    staleTime: Infinity,
  });

  const colorsQuery = useQuery({
    queryKey: ["colors"],
    queryFn: fetchColors,
    staleTime: Infinity,
  });

  const modesQuery = useQuery({
    queryKey: ["modes"],
    queryFn: fetchModes,
    staleTime: Infinity,
  });

  // 更新抽屉中的过滤条件
  const setFilters = (partial: Partial<FilterState>>) => {
    const next = new URLSearchParams(searchParams.toString());
    const merged = { ...filters, ...partial };

    const keyMap: Record<keyof FilterState, string>> = {
      query: "q",
      country: "country",
      color: "color",
      mode: "mode",
      minPrice: "minPrice",
      maxPrice: "maxPrice",
      sortField: "sortField",
      sortDir: "sortDir",
    };

    (Object.keys(merged) as (keyof FilterState)[]).forEach((k) => {
      const paramKey = keyMap[k];
      const val = merged[k];
      const def = DEFAULTS[k];
      if (val && val !== def) {
        next.set(paramKey, val);
      } else {
        next.delete(paramKey);
      }
    });

    router.push(`\({pathname}?\){next.toString()}`, { scroll: false });
  };

  const resetFilters = () => {
    router.push(pathname, { scroll: false });
  };

  const handleQueryChange = (e: ChangeEvent<HTMLInputElement>>) => {
    const val = e.target.value;

    if (searchTimerRef.current) clearTimeout(searchTimerRef.current);

    searchTimerRef.current = setTimeout(() => {
      setFilters({ query: val });
    }, 400);
  };

  const handleClearQuery = () => {
    if (searchRef.current) {
      searchRef.current.value = "";
    }

    setFilters({ query: "" });
  };

  const hasPriceFilter = filters.minPrice || filters.maxPrice;
  const priceLabel = [
    filters.minPrice ? `$${filters.minPrice}` : null,
    filters.maxPrice ? `$${filters.maxPrice}` : null,
  ]
    .filter(Boolean)
    .join(" - ");

  const activeFilterCount = [
    filters.country,
    filters.color,
    filters.mode,
    filters.minPrice,
    filters.maxPrice,
  ].filter(Boolean).length;

  useEffect(() => {
    return () => {
      if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
    };
  }, []);

  return (
    
0 && ( <\/main> <\/div> ); }

SearchPage组件不再负责获取和排序产品数据了。

将过滤逻辑移至相应的组件中

为实现这一功能,我们需要使用不同的状态、数据和函数:

  • drawerOpensetDrawerOpen用于控制过滤抽屉的显示/隐藏。

  • countriescolorsmodes这些数据允许用户选择不同的筛选条件。

  • activeFilterCount用于显示当前处于激活状态的过滤条件的数量。

目前,用于实现过滤功能的组件主要有两个。第一个是位于Header组件内的按钮,其外观如下:

<button
  onClick={onToggleFilters}
  className="relative ml-auto flex items-center gap-2 px-3 py-2 text-sm text-black font-medium border border-stone-300 rounded-lg hover:bg-stone-100 transition"
>
  <SlidersHorizontal className="w-4 h-4" />
  过滤器>
  {activeFilterCount > 0 && (
    
      {activeFilterCount}
    
  )}
</button>;

第二个组件是FilterDrawer,其外观如下:

"use client";

import { useEffect, useRef } from "react";
import { X, RotateCcw } from "lucide-react";
import { FilterState } from "../interfaces";
import { SortSection } from "./filter/sort-section";
import { NarrowResultsSection } from "./filter/narrow-result-section";

interface FilterDrawerProps {
  open: boolean;
  onClose: () => void;
  filters: FilterState;
  onChange: (partial: Partial) => void;
  onReset: () => void;
  countries: string[];
  colors: string[];
  modes: string[];
  activeFilterCount: number;
}

export function FilterDrawer({
  open,
  onClose,
  filters,
  onChange,
  onReset,
  countries,
  colors,
  modes,
  activeFilterCount,
}: FilterDrawerProps) {
  const drawerRef = useRef〈HTMLDivElement〉(null);

  // 在用户按下Escape键时关闭抽屉
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  }, [onClose]);

  // 当抽屉打开时,防止页面滚动
  useEffect(() => {
    document.body.style.overflow = open ? "hidden" : "";
    return () => {
      document.body.style.overflow = "";
    };
  }, [open]);

  return (
    <><>
      {/* 背景层 */}
      
{/* 页头 */}
0 && ( )}

>

{/* 页脚 */} {activeFilterCount > 0 && (
清除所有过滤器
)} <></aside> <>> ); }

你可以将这两个组件合并成一个单一的`Filter`组件,让这个组件包含所有这些逻辑:

import { RotateCcw, SlidersHorizontal, X } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { SortSection } from "./filter/sort-section";
import { NarrowResultsSection } from "./filter/narrow-result-section";
import { FilterState } from "../interfaces";
import { usePathname, useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { fetchColors, fetchCountries, fetchModes } from "../utils";

interface FilterProps {
  filters: FilterState;
  onChange: (partial: Partial) => void;
}

const Filter = ({ filters, onChange }: FilterProps) => {
  const { data: countries = [] } = useQuery({
    queryKey: ["countries"],
    queryFn: fetchCountries,
    staleTime: Infinity,
  });

  const { data: colors = [] } = useQuery({
    queryKey: ["colors"],
    queryFn: fetchColors,
    staleTime: Infinity,
  });

  const { data: modes = [] } = useQuery({
    queryKey: ["modes"],
    queryFn: fetchModes,
    staleTime: Infinity,
  });

  const router = useRouter();
  const pathname = usePathname();
  const [drawerOpen, setDrawerOpen] = useState(false);

  const drawerRef = useRef(null);

  const onClose = () => setDrawerOpen(false);
  const openDrawer = () => setDrawerOpen(true);
  const resetFilters = () => {
    onClose();
    router.push(pathname, { scroll: false });
  };

  const activeFilterCount = [
    filters.country,
    filters.color,
    filters.mode,
    filters.minPrice,
    filters.maxPrice,
  ].filter(Boolean).length;

  // 通过按下Esc键关闭抽屉
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key === "Escape") setDrawerOpen(false);
    };
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  }, []);

  // 当抽屉打开时阻止页面滚动
  useEffect(() => {
    document.body.style.overflow = drawerOpen ? "hidden" : "";
    return () => {
      document.body.style.overflow = "";
    };
  }, [drawerOpen]);

  return (
    <>
      
      {/* 背景图 */}
      
{/* 抽屉面板 */} ); }; export default Filter;

你可以进一步优化这个设计。请注意,NarrowResultsSection是唯一一个会使用获取到的countriescolorsmodes数据的组件。而在该组件内部,每一个SelectField》都会使用这些数据中的一部分。

import { FilterState } from "..interfaces";
import { PriceRangeField } from "./price-range";
import { SelectField } from "./select-field";

interface NarrowResultsSectionProps {
  filters: FilterState;
  onChange: (partial: Partial) => void;
  countries: string[];
  colors: string[];
  modes: string[];
}

export function NarrowResultsSection({
  filters,
  onChange,
  countries,
  colors,
  modes,
}: NarrowResultsSectionProps) {
  return (
    

筛选结果

onChange({ country: v})} placeholder="所有国家" /> onChange({ color: v})} placeholder="所有颜色" /> onChange({ mode: v})} placeholder="所有模式" />
); }

你不必一开始就获取所有数据,然后将其传递给所有的SelectField》,而是可以为每个SelectField》单独发起查询。

SelectField》的具体实现如下:

interface SelectFieldProps {
  label: string;
  value: string;
  options: string[];
  onChange: (v: string) => void;
  placeholder: string;
}

export function SelectField({
  label,
  value,
  options,
  onChange,
  placeholder,
}: SelectFieldProps) {
  return <>>{/*=== 渲染用户界面 ===*/</>>;
}

现在,它的实现方式变成了这样:

import { useQuery } from "@tanstack/react-query";

interface SelectFieldProps {
  label: string;
  value: string;
  onChange: (v: string) => void;
  placeholder: string;
  queryFn(): Promise>{/*=== 渲染用户界面 ===*/</>>;
}

现在,每个下拉列表都会管理自己对应的数据。当某个SelectField》的状态发生变化时,这种变化不会影响到它的兄弟元素或父元素。

将搜索逻辑移入相应的组件中

Search组件是唯一一个使用了去抖动逻辑的组件(searchTimerRefhandleQueryChangehandleClearQuery)。你需要将这些逻辑直接放入该组件内部:

"use client";

import { Search as SearchIcon, X } from "lucide-react";
import { ChangeEvent, useEffect, useRef } from "react";
import { FilterState } from "../interfaces";

interface SearchProps {
  query: string;
  onChange: (partial: Partial) => void;
}

const Search = ({ query, onChange }: SearchProps) => {
  const searchRef = useRef(null);
  const searchTimerRef = useRef | null>(null);

  const handleClearQuery = () => {
    if (searchRef.current) {
      searchRef.current.value = "";
    }

    onChange({ query: "" });
  };

  const handleQueryChange = (e: ChangeEvent) => {
    const val = e.target.value;

    if (searchTimerRef.current) clearTimeout(searchTimerRef.current);

    searchTimerRef.current = setTimeout(() => {
      onChange({ query: val });
    }, 400);
  };

  useEffect(() => {
    return () => {
      if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
    };
  }, []);

  return 
/*=== 显示用户界面 ===*/
; }; export default Search;

将筛选选项组件移入相应的组件中

FilterChips组件用于显示当前选中的筛选条件。其实,提供给该组件的hasPriceFilterpriceLabel这些数据完全可以直接放在FilterChips组件内部,而无需再通过SearchPage来传递:

interface FilterChipsProps {
  filters: FilterState;
  setFilters: (partial: Partial) => void;
  reset Filters: () => void;
}

export function FilterChips({
  filters,
  setFilters,
  resetFilters,
}: FilterChipsProps) {
  const priceLabel = [
    filters.minPrice ? `$${filters.minPrice}` : null,
    filters.maxPrice ? `$${filters.maxPrice}` : null,
  ]
    .filter(Boolean)
    .join(" - ");

  const hasPriceFilter = filters.minPrice || filters.maxPrice;

  return 
; }

最终的SearchPage组件

在将所有的状态和逻辑都移入到需要使用它们的组件之后,SearchPage组件的结构就变成了这样:

"use client";

import { Header } from "./_components/header";
import { ProductTable } from "./_components/products-table";
import { FilterChips } from "./_components/filter-chips";
import { usePathname, useRouter, useSearchParams } from "nextnavigation";
import { FilterState, SortDir, SortField } from "./interfaces";

const DEFAULTS: FilterState = {
  query: "",
  country: "",
  color: "",
  mode: "",
  minPrice: "",
  maxPrice: "",
  sortField: "name",
  sortDir: "asc",
};

export default function SearchPage() {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();

  const filters: FilterState = {
    query: searchParams.get("q") ?? DEFAULTS.query,
    country: searchParams.get("country") ?? DEFAULTS.country,
    color: searchParams.get("color") ?? DEFAULTS.color,
    mode: searchParams.get("mode") ?? DEFAULTS.mode,
    minPrice: searchParams.get("minPrice") ?? DEFAULTS.minPrice,
    maxPrice: searchParams.get("maxPrice") ?? DEFAULTS.maxPrice,
    sortField:
      (searchParams.get("sortField") as SortField) ?? DEFAULTS.sortField,
    sortDir: (searchParams.get("sortDir") as SortDir) ?? DEFAULTS.sortDir,
  };

  const setFilters = (partial: Partial) => {
    const next = new URLSearchParams(searchParams.toString());
    const merged = { ...filters, ...partial };

    const keyMap: Record〈keyof FilterState, string〉 = {
      query: "q",
      country: "country",
      color: "color",
      mode: "mode",
      minPrice: "minPrice",
      maxPrice: "maxPrice",
      sortField: "sortField",
      sortDir: "sortDir",
    };

    (Object.keys(merged) as (keyof FilterState)[])++.forEach((k) => {
      const paramKey = keyMap[k];
      const val = merged[k];
      const def = DEFAULTS[k];
      if (val && val !== def) {
        next.set(paramKey, val);
      } else {
        next.delete(paramKey);
      }
    });

    router.push(`\({pathname}?\){next.toString()}`, { scroll: false });
  };

  const resetFilters = () => {
    router.push(pathname, { scroll: false });
  };

  const activeFilterCount = [
    filters.country,
    filters.color,
    filters.mode,
    filters.minPrice,
    filters.maxPrice,
  ].filter(Boolean).length;

  return (
    
{activeFilterCount > 0 && ( )}
); }

请注意,`setFilters`、`reset Filters`和`activeFilterCount`这些函数仍然存在于`SearchPage`组件中。这是有意为之的——因为这些值的计算结果实际上取决于URL地址,而任何从URL中获取数据的组件,在URL发生变化时都会重新渲染。因此,无论这些值是在哪里被计算的,并不会影响这一行为。

在使用这些钩子之前,请先修复你的代码

当遇到无限循环重绘的情况时,你可能会想要使用`useCallback`或`useMemo`来解决问题。然而,对象引用的不稳定性往往会导致无限次的重绘,尤其是当某个子组件的`useEffect`效应依赖于某些对象时。在这种情况下,最好的做法是弄清楚导致这种循环的原因,并从根本上解决它,而不是直接使用这些钩子。

来看这个例子:

"use client";

import { useEffect, useState } from "react";
import { fetchUsers, Filter, User } from "./utils";

interface UserListProps {
  filters: Filter;
  onLoad(data: User[]): void;
  users: User[];
}

function UserList({ filters, onLoad, users }: UserListProps) {
  console.count "__ USER LIST __");

  useEffect(() => {
    fetchUsers(filters).then((data) => {
      onLoad(data);
    });
  }, [filters, onLoad]);

  return (
    
    ]; const filters = { role: "admin", active: true, }; return

`UserList`组件在初始化时会从服务器获取用户数据,它将`filters`和`onLoad`作为依赖项来使用。

问题在于,在`UserPage`组件中,每次重新渲染时`filters`对象都会被重新创建。虽然它的值看起来没有变化,但实际上每次重绘时都是一个新的对象引用,因此`UserList`会将其视为一个不同的值,从而触发`useEffect`效应。

浏览器中不断显示“USER LIST”相关的日志信息。’ height="477" loading="lazy" src="https://cdn.hashnode.com/uploads/covers/629122ced97f80b5091d8058/c201dff7-3e6b-4648-8caf-9323057df3df.gif" style="display:block;margin:0 auto" width="800"/></p>
<p>将`filters`对象包装在`useMemo`中确实可以阻止无限循环重绘,但这并没有解决根本问题。实际上,`useMemo`并不是为了解决这种问题而设计的。有一些更好的方法可以解决这个问题:</p>
<p>第一种方法是,在依赖项数组中使用原始类型而不是对象。因为对象是通过引用进行比较的,所以每次重新渲染时,`useEffect`都会看到不同的对象引用;而原始类型则是通过值来进行比较的,因此不会导致这种问题。</p>
<pre><code class="language-typescript">function UserList({ filters, onLoad, users }: UserListProps) {
  console.count "__ 用户列表 __");

  useEffect(() => {
    fetchUsers(filters).then((data) => {
      onLoad(data);
    });
  }, [filters.active, filters.role]); // 原始类型按值进行比较,而非引用

  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}
</code></pre>
<p>第二种方法是在组件外部定义这个对象,这样它就能拥有一个稳定的引用。</p>
<pre><code class="language-typescript">const filters = {
  role: "admin",
  active: true,
};

const UserPage = () => {
  const [userData, setUserData] = useState:<User[]&gt>];

  return <UserList filters={filters} onLoad={setUserData} users={userData} />;
};
</code></pre>
<p>如果这个对象是动态变化的,那么第三种解决方案就是将其存储在状态中。</p>
<pre><code class="language-typescript">const UserPage = () => {
  const [userData, setUserData] = useState)<User[]&gt>;
  const [filters, setFilters] = useState({
    role: "admin",
    active: true,
  });

  return <UserList filters={filters} onLoad={setUserData} users={userData} />;
};
</code></pre>
<h2 id="heading-when-to-use-usecallback-and-usememo">何时使用<code>useCallback</code>和<code>useMemo</code></h2>
<p>本文的目的并不是告诉你们永远不要使用这些钩子。在某些情况下,这些钩子确实能够发挥重要的作用。</p>
<h3 id="heading-measure-before-you-optimize">优化之前先进行评估</h3>
<p>在任何优化操作之前,无论是使用<code>useCallback</code>、<code>useMemo</code>还是重新构建组件结构,你都应该首先确认确实存在性能问题。对那些并不需要优化的代码进行优化,对任何人来说都没有好处。</p>
<p>React DevTools提供了一个“分析器”选项卡,你可以使用它来记录开发过程中的操作过程,从而准确了解哪些组件会被重新渲染、它们被重新渲染的频率以及每次渲染所花费的时间。建议你阅读<a href="https://www.freecodecamp.org/news/how-to-use-react-devtools/">《如何使用React开发者工具——结合实例进行讲解》</a>这篇文档。如果你更喜欢观看视频教程,也可以看看Ben是如何演示<a href="https://www.youtube.com/watch?v=00RoZflFE34">如何利用React分析器来查找并解决性能问题</a>的。</p>
<h3 id="heading-stabilize-references-for-reactmemo-children">为<code>React.memo</code>的子组件稳定引用</h3>
<p><code>React.memo</code>能够防止组件在属性没有发生变化的情况下被重新渲染。但是,如果你传递的是一个函数或对象作为属性,那么每当父组件被重新渲染时,这个子组件也会被重新渲染,因为函数和对象每次都会被创建成新的实例。</p>
<p>以下就是适合使用<code>useCallback</code>或<code>useMemo</code>的情况:</p>
<pre><code class="language-typescript">const Child = React.memo(({ onClick }: { onClick: () => void }) => {
  console.log("子组件被渲染");
  return <button onClick={onClick}&gt>点击我</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("被点击了");
  }, []);

  return (
    <><>
      <button onClick={() => setCount((c) => c + 1)}&gt>计数:{count}</button>
      <Child onClick={handleClick}。</Child>
    </>>
  );
}
</code></pre>
<p>如果没有使用<code>useCallback</code>,那么每当<code>count</code>的值发生变化时,<code>Child</code>组件就会重新渲染,尽管<code>handleClick</code>函数与<code>count</code>毫无关联。而使用了<code>useCallback</code>之后,该函数的引用地址就会保持不变。</p>
<p>通常来说,将<code>useCallback</code>与<code>React.memo</code>一起使用会是最佳选择。不过,如果组件的属性是基本类型或者其值本身是稳定的,那么单独使用<code>React.memo</code>也同样有效。此外,在某些情况下,<code>useCallback</code>也可以在不需要使用<code>React.memo</code>的时候发挥作用,比如在将稳定的回调函数传递给效果函数、自定义钩子或第三方组件时。</p>
<h2 id="heading-conclusion">结论</h2>
<p><code>useCallback</code>与<code>useMemo</code>确实是很有用的记忆化工具,但它们并不能自动提升性能。每次调用这些函数都会增加内存开销,并且在每次渲染时都会进行依赖项的比较。</p>
<p>在设计组件时,应尽量减少优化的需求。应将状态和逻辑尽可能地放在那些需要使用它们的组件附近。只有当确实发现渲染过程存在问题时,才应该考虑使用<code>useCallback</code>、<code>useMemo</code>以及<code>React.memo</code>。</p>
 </div> <section class="mt-12 border-t border-slate-200 pt-10 dark:border-slate-800"><h2 class="text-xl font-bold text-slate-900 dark:text-white">相关文章</h2><div class="mt-6 grid gap-6 md:grid-cols-2"><article class="group flex h-full flex-col overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm transition hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800 dark:bg-slate-900"> <a href="/articles/how-to-share-components-between-server-and-client-in-nextjs" class="relative block aspect-[16/10] overflow-hidden bg-slate-900"> <div class="absolute inset-0 bg-gradient-to-br from-blue-600 via-cyan-700 to-slate-900"> <div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(255,255,255,0.2),transparent_45%)]"></div> </div>   <div class="absolute inset-0 flex flex-col justify-end p-5"> <span class="inline-flex w-fit rounded-full px-3 py-1 text-xs font-semibold bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-200">技术实践</span>  </div> </a> <div class="flex flex-1 flex-col p-5"> <p class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400"> <svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4" data-icon="tabler:calendar">   <use href="#ai:tabler:calendar"></use>  </svg> <time datetime="Sun Mar 29 2026 04:20:15 GMT+0800 (China Standard Time)">2026年3月28日</time>  </p> <h3 class="mt-3 text-lg font-bold leading-snug text-slate-900 dark:text-white"> <a href="/articles/how-to-share-components-between-server-and-client-in-nextjs" class="transition hover:text-violet-600 dark:hover:text-violet-300">在 NextJS 中如何在服务器与客户端之间共享组件</a> </h3> <p class="mt-3 flex-1 text-sm leading-7 text-slate-600 dark:text-slate-300">Next.js 应用程序路由器将你的应用程序分为服务器组件和客户端组件。服务器组 &hellip; Read More</p> <a href="/articles/how-to-share-components-between-server-and-client-in-nextjs" class="mt-4 inline-flex items-center gap-1 text-sm font-semibold text-violet-600 transition hover:text-violet-700 dark:text-violet-300">
阅读全文
<svg width="1em" height="1em" class="h-4 w-4 transition group-hover:translate-x-0.5" data-icon="tabler:arrow-right">   <symbol id="ai:tabler:arrow-right" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-6 6l6-6m-6-6l6 6"/></symbol><use href="#ai:tabler:arrow-right"></use>  </svg> </a> </div> </article><article class="group flex h-full flex-col overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm transition hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800 dark:bg-slate-900"> <a href="/articles/relaxation-and-its-role-in-vision-the-1977-phd-thesis-that-helped-shape-modern-ai-research" class="relative block aspect-[16/10] overflow-hidden bg-slate-900"> <div class="absolute inset-0 bg-gradient-to-br from-blue-600 via-cyan-700 to-slate-900"> <div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(255,255,255,0.2),transparent_45%)]"></div> </div>   <div class="absolute inset-0 flex flex-col justify-end p-5"> <span class="inline-flex w-fit rounded-full px-3 py-1 text-xs font-semibold bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-200">技术实践</span>  </div> </a> <div class="flex flex-1 flex-col p-5"> <p class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400"> <svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4" data-icon="tabler:calendar">   <use href="#ai:tabler:calendar"></use>  </svg> <time datetime="Wed Jul 22 2026 14:41:11 GMT+0800 (China Standard Time)">2026年7月22日</time>  </p> <h3 class="mt-3 text-lg font-bold leading-snug text-slate-900 dark:text-white"> <a href="/articles/relaxation-and-its-role-in-vision-the-1977-phd-thesis-that-helped-shape-modern-ai-research" class="transition hover:text-violet-600 dark:hover:text-violet-300">“放松与它在视觉中的作用”:这篇1977年完成的博士论文为现代人工智能研究的发展奠定了基础</a> </h3> <p class="mt-3 flex-1 text-sm leading-7 text-slate-600 dark:text-slate-300">当人们提到杰弗里·辛顿时,他们通常会想到反向传播算法、玻尔兹曼机、深度信念网络,以及那些彻底改变了人工智能发展的深度学习技术。 然而,很少有人会追溯到他科研生涯的起点。 1977年,在那篇著名的关于反向传播的论文发表之前的近十年时间里,辛顿在爱丁堡大学完成了他的博士论文,题为《松弛机制在视觉认知中的作用》。乍一看,这篇论文似乎只是探讨计算机视觉与松弛算法之间的关系。当我开始阅读它时,我也正是抱着这样的预期。 然而,在仔细研读之后,我意识到这篇论文涉及的远不止视觉算法。许多后来成为辛顿研究核心的理念,在当时就已经初具雏形。虽然当时的术语有所不同,数学表达也更为简单,神经网络也尚未成为他研究的重点</p> <a href="/articles/relaxation-and-its-role-in-vision-the-1977-phd-thesis-that-helped-shape-modern-ai-research" class="mt-4 inline-flex items-center gap-1 text-sm font-semibold text-violet-600 transition hover:text-violet-700 dark:text-violet-300">
阅读全文
<svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4 transition group-hover:translate-x-0.5" data-icon="tabler:arrow-right">   <use href="#ai:tabler:arrow-right"></use>  </svg> </a> </div> </article><article class="group flex h-full flex-col overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm transition hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800 dark:bg-slate-900"> <a href="/articles/the-hidden-tax-of-infrastructure-why-your-team-shouldnt-be-running-it-anymore" class="relative block aspect-[16/10] overflow-hidden bg-slate-900"> <div class="absolute inset-0 bg-gradient-to-br from-blue-600 via-cyan-700 to-slate-900"> <div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(255,255,255,0.2),transparent_45%)]"></div> </div>   <div class="absolute inset-0 flex flex-col justify-end p-5"> <span class="inline-flex w-fit rounded-full px-3 py-1 text-xs font-semibold bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-200">技术实践</span>  </div> </a> <div class="flex flex-1 flex-col p-5"> <p class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400"> <svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4" data-icon="tabler:calendar">   <use href="#ai:tabler:calendar"></use>  </svg> <time datetime="Sat Apr 25 2026 18:06:46 GMT+0800 (China Standard Time)">2026年4月25日</time>  </p> <h3 class="mt-3 text-lg font-bold leading-snug text-slate-900 dark:text-white"> <a href="/articles/the-hidden-tax-of-infrastructure-why-your-team-shouldnt-be-running-it-anymore" class="transition hover:text-violet-600 dark:hover:text-violet-300">那些被隐藏起来的基础设施建设成本:为什么你的团队不应该再继续承担这些责任了</a> </h3> <p class="mt-3 flex-1 text-sm leading-7 text-slate-600 dark:text-slate-300">大多数工程团队最初并不是为了管理基础设施而开始工作的。他们通常是带着某个产品创意 &hellip; Read More</p> <a href="/articles/the-hidden-tax-of-infrastructure-why-your-team-shouldnt-be-running-it-anymore" class="mt-4 inline-flex items-center gap-1 text-sm font-semibold text-violet-600 transition hover:text-violet-700 dark:text-violet-300">
阅读全文
<svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4 transition group-hover:translate-x-0.5" data-icon="tabler:arrow-right">   <use href="#ai:tabler:arrow-right"></use>  </svg> </a> </div> </article><article class="group flex h-full flex-col overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm transition hover:-translate-y-0.5 hover:shadow-md dark:border-slate-800 dark:bg-slate-900"> <a href="/articles/how-to-elevate-your-database-game-supercharging-query-performance-with-postgres-fdw" class="relative block aspect-[16/10] overflow-hidden bg-slate-900"> <div class="absolute inset-0 bg-gradient-to-br from-blue-600 via-cyan-700 to-slate-900"> <div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(255,255,255,0.2),transparent_45%)]"></div> </div>   <div class="absolute inset-0 flex flex-col justify-end p-5"> <span class="inline-flex w-fit rounded-full px-3 py-1 text-xs font-semibold bg-blue-50 text-blue-700 dark:bg-blue-950/50 dark:text-blue-200">技术实践</span>  </div> </a> <div class="flex flex-1 flex-col p-5"> <p class="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400"> <svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4" data-icon="tabler:calendar">   <use href="#ai:tabler:calendar"></use>  </svg> <time datetime="Sat Feb 21 2026 04:31:05 GMT+0800 (China Standard Time)">2026年2月20日</time>  </p> <h3 class="mt-3 text-lg font-bold leading-snug text-slate-900 dark:text-white"> <a href="/articles/how-to-elevate-your-database-game-supercharging-query-performance-with-postgres-fdw" class="transition hover:text-violet-600 dark:hover:text-violet-300">如何提升数据库的性能:利用Postgres FDW来显著增强查询速度</a> </h3> <p class="mt-3 flex-1 text-sm leading-7 text-slate-600 dark:text-slate-300">外部数据封装机制使得远程的Postgres表看起来就像本地的表一样。正是这种便利 &hellip; Read More</p> <a href="/articles/how-to-elevate-your-database-game-supercharging-query-performance-with-postgres-fdw" class="mt-4 inline-flex items-center gap-1 text-sm font-semibold text-violet-600 transition hover:text-violet-700 dark:text-violet-300">
阅读全文
<svg width="1em" height="1em" viewBox="0 0 24 24" class="h-4 w-4 transition group-hover:translate-x-0.5" data-icon="tabler:arrow-right">   <use href="#ai:tabler:arrow-right"></use>  </svg> </a> </div> </article></div></section> </article> <div class="w-full shrink-0 lg:w-56"> <aside class="w-full min-w-0 space-y-8 lg:sticky lg:top-24"> <section> <h2 class="text-sm font-semibold tracking-wide text-slate-900 dark:text-white">内容栏目</h2> <ul class="mt-3 flex flex-col gap-1"> <li> <a href="/articles/category/industry" class="block truncate rounded-lg px-2.5 py-1.5 text-sm font-medium transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300"> 行业洞察 </a> </li><li> <a href="/articles/category/technology" class="block truncate rounded-lg px-2.5 py-1.5 text-sm font-medium transition bg-violet-50 text-violet-700 dark:bg-violet-950/50 dark:text-violet-300"> 技术实践 </a> </li><li> <a href="/articles/category/product" class="block truncate rounded-lg px-2.5 py-1.5 text-sm font-medium transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300"> 产品动态 </a> </li> </ul> </section> <section> <h2 class="text-sm font-semibold tracking-wide text-slate-900 dark:text-white">文章分类</h2> <ul class="mt-3 flex flex-col gap-1"> <li> <a href="/articles/categories/%E5%AE%89%E5%85%A8" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="安全"> 安全 </a> </li><li> <a href="/articles/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="操作系统"> 操作系统 </a> </li><li> <a href="/articles/categories/product" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="产品方案"> 产品方案 </a> </li><li> <a href="/articles/categories/%E4%BA%A7%E5%93%81%E8%BF%90%E8%90%A5" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="产品运营"> 产品运营 </a> </li><li> <a href="/articles/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition bg-violet-50 font-medium text-violet-700 dark:bg-violet-950/50 dark:text-violet-300" title="大数据"> 大数据 </a> </li><li> <a href="/articles/categories/printorder" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="电商打单"> 电商打单 </a> </li><li> <a href="/articles/categories/%E6%95%85%E9%9A%9C%E9%A2%84%E8%AD%A6" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="故障预警"> 故障预警 </a> </li><li> <a href="/articles/categories/%E6%9E%B6%E6%9E%84" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="架构"> 架构 </a> </li><li> <a href="/articles/categories/openapi" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="开放平台"> 开放平台 </a> </li><li> <a href="/articles/categories/uncategorized" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="默认"> 默认 </a> </li><li> <a href="/articles/categories/allsite" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="全渠道OMS+WMS"> 全渠道OMS+WMS </a> </li><li> <a href="/articles/categories/hotnews" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="热点关注"> 热点关注 </a> </li><li> <a href="/articles/categories/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="人工智能"> 人工智能 </a> </li><li> <a href="/articles/categories/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="软件开发"> 软件开发 </a> </li><li> <a href="/articles/categories/%E6%95%B0%E6%8D%AE%E5%BA%93" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="数据库"> 数据库 </a> </li><li> <a href="/articles/categories/%E7%A7%BB%E5%8A%A8" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="移动"> 移动 </a> </li><li> <a href="/articles/categories/%E4%B8%93%E5%AE%B6%E8%A7%82" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="专家观"> 专家观 </a> </li><li> <a href="/articles/categories/expertpoint" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="专家观点"> 专家观点 </a> </li><li> <a href="/articles/categories/iot" class="block truncate rounded-lg px-2.5 py-1.5 text-sm transition text-slate-600 hover:bg-slate-50 hover:text-violet-700 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-violet-300" title="IoT"> IoT </a> </li> </ul> </section> <section> <h2 class="text-sm font-semibold tracking-wide text-slate-900 dark:text-white">文章标签</h2> <ul class="mt-3 flex flex-wrap gap-1.5"> <li class="max-w-full"> <a href="/articles/tags/%E6%95%B0%E6%8D%AE" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="数据"> 数据 </a> </li><li class="max-w-full"> <a href="/articles/tags/strong" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="strong"> strong </a> </li><li class="max-w-full"> <a href="/articles/tags/data" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="data"> data </a> </li><li class="max-w-full"> <a href="/articles/tags/code" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="code"> code </a> </li><li class="max-w-full"> <a href="/articles/tags/span" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="span"> span </a> </li><li class="max-w-full"> <a href="/articles/tags/ai" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="AI"> AI </a> </li><li class="max-w-full"> <a href="/articles/tags/li" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="li"> li </a> </li><li class="max-w-full"> <a href="/articles/tags/%E6%95%B0%E6%8D%AE%E5%BA%93" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="数据库"> 数据库 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E6%A8%A1%E5%9E%8B" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="模型"> 模型 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%BD%BF%E7%94%A8" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="使用"> 使用 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="人工智能"> 人工智能 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%B8%AD" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="中"> 中 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E6%9C%8D%E5%8A%A1" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="服务"> 服务 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%BC%9A" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="会"> 会 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E6%8A%80%E6%9C%AF" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="技术"> 技术 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E7%B3%BB%E7%BB%9F" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="系统"> 系统 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E5%AE%89%E5%85%A8" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="安全"> 安全 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E7%94%A8%E6%88%B7" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="用户"> 用户 </a> </li><li class="max-w-full"> <a href="/articles/tags/em" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="em"> em </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%BB%A3%E7%A0%81" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="代码"> 代码 </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%BB%A3%E7%A0%81%EF%BC%8C%E4%BC%9A%E7%9A%84%E3%80%82" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="代码,会的。"> 代码,会的。 </a> </li><li class="max-w-full"> <a href="/articles/tags/nbsp" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="nbsp"> nbsp </a> </li><li class="max-w-full"> <a href="/articles/tags/ltr" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="ltr"> ltr </a> </li><li class="max-w-full"> <a href="/articles/tags/variable" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="variable"> variable </a> </li><li class="max-w-full"> <a href="/articles/tags/iot" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="iot"> iot </a> </li><li class="max-w-full"> <a href="/articles/tags/%E8%AE%BE%E5%A4%87" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="设备"> 设备 </a> </li><li class="max-w-full"> <a href="/articles/tags/href" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="href"> href </a> </li><li class="max-w-full"> <a href="/articles/tags/kafka" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="kafka"> kafka </a> </li><li class="max-w-full"> <a href="/articles/tags/python" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="python"> python </a> </li><li class="max-w-full"> <a href="/articles/tags/%E4%BA%91" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-xs font-medium transition bg-slate-100 text-slate-600 hover:bg-violet-50 hover:text-violet-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-violet-950/40 dark:hover:text-violet-300" title="云"> 云 </a> </li> </ul> </section> </aside> </div> </div> </div>  </main>  <footer class="relative not-prose border-t border-slate-200/80 bg-gradient-to-b from-slate-50 to-white dark:border-slate-800 dark:from-slate-950 dark:to-slate-900"> <div class="relative mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 intersect-once intersect-quarter intersect-no-queue motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"> <div class="grid grid-cols-12 gap-10 py-12 md:gap-8 md:py-14 lg:gap-12"> <!-- Brand + WeChat --> <div class="col-span-12 lg:col-span-4"> <a class="inline-block" href="/" aria-label="返回首页"> <span class="inline-flex items-end gap-3 self-center whitespace-nowrap"> <img src="/_image?href=%2F_astro%2Fcheeli-logo.DIEHWBc7.png&w=108&h=32&f=webp" loading="eager" decoding="async" alt="Cheeli" style="object-fit:cover;object-position:center;max-width:108px;max-height:32px;aspect-ratio:3.375;width:100%" width="108" height="32" class="block h-8 w-auto shrink-0 invert hue-rotate-180 dark:invert-0 dark:hue-rotate-0"> <span class="relative pb-px pl-3 text-[15px] font-semibold leading-none tracking-[0.14em] text-slate-800 before:absolute before:bottom-0 before:left-0 before:h-3.5 before:w-px before:bg-slate-300/80 dark:text-slate-100 dark:before:bg-slate-600/80 md:text-base">
知力科技
</span> </span> </a> <p class="mt-4 text-sm font-semibold text-slate-800 dark:text-slate-100">蜂巢数字商业平台</p> <p class="mt-2 max-w-sm text-sm leading-6 text-slate-600 dark:text-slate-400">连接商业生态,赋能企业智能经营。</p> <div class="mt-6 inline-flex items-start gap-4 rounded-2xl border border-slate-200/80 bg-white p-3 shadow-sm dark:border-slate-700 dark:bg-slate-900/80"> <img src="/_astro/wechat-cheeli-tech-qrcode.CYWhZFv0.jpg" alt="知力科技官方微信公众号二维码" width="88" height="88" class="block h-[88px] w-[88px] shrink-0 rounded-xl bg-white object-contain" loading="lazy" decoding="async"> <div class="pt-1"> <p class="text-sm font-semibold text-slate-900 dark:text-white">官方公众号</p> <p class="mt-1 text-xs leading-5 text-slate-500 dark:text-slate-400">扫码关注,获取产品动态与行业洞察</p> </div> </div> <div class="mt-6 flex flex-wrap gap-x-4 gap-y-2 text-sm"> <a class="text-slate-500 transition hover:text-blue-600 dark:text-slate-400 dark:hover:text-blue-300" href="/privacy"> 隐私政策 </a><a class="text-slate-500 transition hover:text-blue-600 dark:text-slate-400 dark:hover:text-blue-300" href="/terms"> 服务条款 </a> </div> </div> <!-- Navigation columns --> <div class="col-span-12 grid grid-cols-2 gap-8 sm:grid-cols-3 lg:col-span-8 lg:gap-10"> <div> <p class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500"> 产品 </p> <ul class="mt-4 space-y-3"> <li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/products/open-platform"> 蜂巢开放平台 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/products/omnichannel"> 蜂巢全渠道 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/products/ai-agent"> AI智能应用 </a> </li> </ul> </div><div> <p class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500"> 解决方案 </p> <ul class="mt-4 space-y-3"> <li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/solutions/ecommerce-omnichannel"> 电商全渠道解决方案 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/solutions/digital-goods"> 虚拟商品与数字权益解决方案 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/solutions/order-fulfillment"> 企业订单履约解决方案 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/solutions/private-mall"> 私域商城解决方案 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/solutions/digital-connection"> 企业数字化连接解决方案 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/solutions/ai-operations"> AI智能运营解决方案 </a> </li> </ul> </div><div> <p class="text-xs font-semibold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500"> 生态与内容 </p> <ul class="mt-4 space-y-3"> <li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/developers"> 开发者中心 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/cases"> 客户案例 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/articles"> 蜂巢洞察 </a> </li><li> <a class="text-sm text-slate-600 transition hover:text-blue-600 dark:text-slate-300 dark:hover:text-blue-300" href="/about"> 关于我们 </a> </li> </ul> </div> </div> </div> <!-- Legal bar --> <div class="flex flex-col gap-4 border-t border-slate-200/80 py-6 md:flex-row md:items-center md:justify-between dark:border-slate-800 md:py-7"> <div class="text-xs leading-6 text-slate-500 dark:text-slate-400"> <p class="text-slate-600 dark:text-slate-300">2026 Copyright © 上海知力信息科技有限公司 · All Rights Reserved</p> <p class="mt-1.5 flex flex-wrap items-center gap-x-2 gap-y-1"> <a class="transition hover:text-blue-600 dark:hover:text-blue-300" href="https://beian.miit.gov.cn/" rel="noopener noreferrer" target="_blank"> 沪ICP备09012674号 </a> <span class="text-slate-300 dark:text-slate-600" aria-hidden="true">|</span> <a class="inline-flex items-center gap-1 transition hover:text-blue-600 dark:hover:text-blue-300" href="https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=31011502018658" rel="noopener noreferrer" target="_blank"> <img src="/_image?href=%2F_astro%2Fbeian-logo.hBd0lRJE.png&w=16&h=16&f=webp" loading="lazy" decoding="async" alt style="object-fit:cover;object-position:center;width:16px;height:16px;object-position:top left" width="16" height="16" class="block h-4 w-4 shrink-0"> 沪公网安备 31011502018658号 </a> <span class="text-slate-300 dark:text-slate-600" aria-hidden="true">|</span> <span>增值电信业务许可证:沪B2-20220251</span> </p> </div> <ul class="flex shrink-0 gap-1 md:justify-end"> <li> <a class="inline-flex items-center rounded-lg p-2.5 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100" aria-label="RSS" href="/rss.xml"> <svg width="1em" height="1em" viewBox="0 0 24 24" class="h-5 w-5" data-icon="tabler:rss">   <use href="#ai:tabler:rss"></use>  </svg>  </a> </li> </ul> </div> </div> </footer>   <aside id="cheeli-float-panel" class="cheeli-float" data-collapsed="false" aria-label="客服联系方式" data-astro-cid-lduetl6a> <button type="button" class="cheeli-float__toggle" data-float-toggle aria-expanded="true" aria-controls="cheeli-float-panel-body" data-astro-cid-lduetl6a> <span class="cheeli-float__toggle-open" aria-hidden="true" data-astro-cid-lduetl6a>收起</span> <span class="cheeli-float__toggle-closed" aria-hidden="true" data-astro-cid-lduetl6a>客服</span> </button> <div id="cheeli-float-panel-body" class="cheeli-float__body" data-astro-cid-lduetl6a> <div class="cf-card cf-card--avatar" data-astro-cid-lduetl6a> <div class="cf-avatar" data-astro-cid-lduetl6a> <img src="/_astro/ocpm.2osPe16p.png" alt="蜂巢客服顾问" width="120" height="130" loading="lazy" decoding="async" data-astro-cid-lduetl6a> </div> <a class="cf-phone" href="tel:400-002-5767" data-astro-cid-lduetl6a>☎ 400-002-5767</a> </div> <div class="cf-card" data-astro-cid-lduetl6a> <img class="cf-qrcode" src="/_astro/wechat.BcEd01Bh.png" alt="客服微信二维码" width="130" height="140" loading="lazy" decoding="async" data-astro-cid-lduetl6a> <div class="cf-title" data-astro-cid-lduetl6a>扫码添加客服微信</div> </div> <a class="cf-btn white" href="https://wpa.qq.com/msgrd?v=3&uin=273785004&site=qq&menu=yes" target="_blank" rel="noopener noreferrer" aria-label="QQ 客服 273785004" data-astro-cid-lduetl6a> <img src="/_astro/qq.Bq_2h58v.png" alt="" width="16" height="18" loading="lazy" decoding="async" data-astro-cid-lduetl6a> <span data-astro-cid-lduetl6a>:273785004</span> </a> </div> </aside>  <script type="module">const c="cheeli-float-contact-collapsed";function d(t){if(t.dataset.bound==="true")return;t.dataset.bound="true";const e=t.querySelector("[data-float-toggle]");if(!e)return;const l=n=>{t.dataset.collapsed=n?"true":"false",e.setAttribute("aria-expanded",n?"false":"true");try{localStorage.setItem(c,n?"1":"0")}catch{}};let a=!1;try{a=localStorage.getItem(c)==="1"}catch{a=!1}l(a),e.addEventListener("click",()=>{l(t.dataset.collapsed!=="true")})}function o(){const t=document.getElementById("cheeli-float-panel");t instanceof HTMLElement&&d(t)}o();document.addEventListener("astro:page-load",o);</script> <script>(function(){const defaultTheme = "system";

  if (window.basic_script) {
    return;
  }

  window.basic_script = true;

  function applyTheme(theme) {
    if (theme === { elem.addEventListener(event, (e) => fn(e, elem), false); }); } } const onLoad = function () { let lastKnownScrollPosition = window.scrollY; let ticking = true; attachEvent('#header nav', 'click', function () { document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded'); document.body.classList.remove('overflow-hidden'); document.getElementById('header')?.classList.remove('h-screen'); document.getElementById('header')?.classList.remove('expanded'); document.getElementById('header')?.classList.remove('bg-page'); document.querySelector('#header nav')?.classList.add('hidden'); document.querySelector('#header > div > div:last-child')?.classList.add('hidden'); }); attachEvent('[data-aw-toggle-menu]', 'click', function (_, elem) { elem.classList.toggle('expanded'); document.body.classList.toggle('overflow-hidden'); document.getElementById('header')?.classList.toggle('h-screen'); document.getElementById('header')?.classList.toggle('expanded'); document.getElementById('header')?.classList.toggle('bg-page'); document.querySelector('#header nav')?.classList.toggle('hidden'); document.querySelector('#header > div > div:last-child')?.classList.toggle('hidden'); }); attachEvent('[data-aw-toggle-color-scheme]', 'click', function () { if (defaultTheme.endsWith(':only')) { return; } Observer.removeAnimationDelay(); document.documentElement.classList.toggle('dark'); localStorage.theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light'; }); attachEvent('[data-aw-social-share]', 'click', function (_, elem) { const network = elem.getAttribute('data-aw-social-share'); const url = encodeURIComponent(elem.getAttribute('data-aw-url')); const text = encodeURIComponent(elem.getAttribute('data-aw-text')); let href; switch (network) { case 'facebook': href = `https://www.facebook.com/sharer.php?u=${url}`; break; case 'twitter': href = `https://twitter.com/intent/tweet?url=${url}&text=${text}`; break; case 'linkedin': href = `https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${text}`; break; case 'whatsapp': href = `https://wa.me/?text=${text}%20${url}`; break; case 'mail': href = `mailto:?subject=%22${text}%22&body=${text}%20${url}`; break; default: return; } const newlink = document.createElement('a'); newlink.target = '_blank'; newlink.href = href; newlink.click(); }); const screenSize = window.matchMedia('(max-width: 767px)'); screenSize.addEventListener('change', function () { document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded'); document.body.classList.remove('overflow-hidden'); document.getElementById('header')?.classList.remove('h-screen'); document.getElementById('header')?.classList.remove('expanded'); document.getElementById('header')?.classList.remove('bg-page'); document.querySelector('#header nav')?.classList.add('hidden'); document.querySelector('#header > div > div:last-child')?.classList.add('hidden'); }); function applyHeaderStylesOnScroll() { const header = document.querySelector('#header[data-aw-sticky-header]'); if (!header) return; if (lastKnownScrollPosition > 60 && !header.classList.contains('scroll')) { header.classList.add('scroll'); } else if (lastKnownScrollPosition <= 60 && header.classList.contains('scroll')) { header.classList.remove('scroll'); } ticking = false; } applyHeaderStylesOnScroll(); attachEvent([document], 'scroll', function () { lastKnownScrollPosition = window.scrollY; if (!ticking) { window.requestAnimationFrame(() => { applyHeaderStylesOnScroll(); }); ticking = true; } }); }; const onPageShow = function () { document.documentElement.classList.add('motion-safe:scroll-smooth'); const elem = document.querySelector('[data-aw-toggle-menu]'); if (elem) { elem.classList.remove('expanded'); } document.body.classList.remove('overflow-hidden'); document.getElementById('header')?.classList.remove('h-screen'); document.getElementById('header')?.classList.remove('expanded'); document.querySelector('#header nav')?.classList.add('hidden'); }; window.onload = onLoad; window.onpageshow = onPageShow; document.addEventListener('astro:after-swap', () => { initTheme(); onLoad(); onPageShow(); }); })();