遵义网站页设计制作,母婴网站建设方案,广州越秀区酒店,制作动画的网站文章目录 前言一、Search组件封装1. 效果展示2. 功能分析3. 代码详细注释4. 使用方式 二、搜索结果展示组件封装1. 功能分析2. 代码详细注释 三、引用到文件#xff0c;自行取用总结 前言
今天#xff0c;我们来封装一个业务灵巧的组件#xff0c;它集成了全局搜索和展示搜… 文章目录 前言一、Search组件封装1. 效果展示2. 功能分析3. 代码详细注释4. 使用方式 二、搜索结果展示组件封装1. 功能分析2. 代码详细注释 三、引用到文件自行取用总结 前言
今天我们来封装一个业务灵巧的组件它集成了全局搜索和展示搜索结果的功能。通过配置文件我们可以为不同的模块定制展示和跳转逻辑集中管理不同模块当要加一个模块时只需要通过配置即可从而减少重复的代码并方便地进行维护和扩展。同时我们将使用React Query来实现搜索功能并模拟请求成功、请求失败和中断请求的处理方式。 一、Search组件封装
1. 效果展示
1输入内容当停止输入后请求接口数据 注如请求数据时添加加载状态请求结束后取消加载状态 2点击清除按钮清除输入框数据并中止当前请求重置react-query请求参数 3请求失败展示失败界面 4是否显示搜索按钮 5移动端效果 2. 功能分析 1搜索功能灵活性 使用防抖搜索useMemo以及react-query自带监听输入状态只在输入框停止输入后才会触发接口请求避免在用户仍在输入时进行不必要的API调用 2请求库选择 使用Tanstack React Query中的useQuery钩子来管理加载状态并获取搜索结果 3导航到搜索结果 点击搜索结果项或在搜索结果显示后按下回车键时会跳转到对应的页面 4清除搜索 点击清空按钮会清空输入框的内容并取消接口请求重置请求参数隐藏搜索结果列表 5搜索结果展示 一旦获取到搜索结果该组件使用SearchResults组件渲染搜索结果。它还显示搜索结果的加载状态 6搜索按钮 如果hasButton属性为true还将渲染一个搜索按钮当点击时触发搜索 7使用国际化语言可全局切换使用使用联合类型声明使用不同模块添加配置即可 8使用useCallbackuseMemouseEffect, memolodash.debounce等对组件进行性能优化 9提供一些回调事件供外部调用 3. 代码详细注释
引入之前文章封装的 输入框组件可自行查看以及下面封装的结果展示组件
// /components/Search/index.tsx
import { FC, useCallback, useMemo, memo, useEffect, useState } from react;
import { useNavigate } from react-router-dom;
import debounce from lodash.debounce;
import { useTranslation } from react-i18next;
import { useQuery, useQueryClient } from tanstack/react-query;
import { SearchContainer, SearchButton } from ./styled;
import Input from /components/Input;
import { querySearchInfo } from /api/search;
import { useIsMobile } from /hooks;
import { SearchResults } from ./searchResults;
import { getURLBySearchResult } from ./utils;// 组件的属性类型
type Props {defaultValue?: string;hasButton?: boolean;onClear?: () void;
};
// 搜索框组件
const Search: FCProps memo(({ defaultValue, hasButton, onClear: handleClear }) {const queryClient useQueryClient();const navigate useNavigate();const { t } useTranslation();const isMobile useIsMobile();const [keyword, _setKeyword] useState(defaultValue || );const searchValue keyword.trim();// 获取搜索结果数据const fetchData async (searchValue: string) {const { data } await querySearchInfo({p: searchValue,});return {data,total: data.length,};};// 使用useQuery实现搜索const {refetch: refetchSearch,data: _searchResults,isFetching,} useQuery([moduleSearch, searchValue], () fetchData(searchValue), {enabled: false,});// 从查询结果中获取搜索结果数据const searchResultData _searchResults?.data;// 使用useMemo函数创建一个防抖函数debouncedSearch用于实现搜索请求功能const debouncedSearch useMemo(() {return debounce(refetchSearch, 1500, { trailing: true }); // 在搜索值变化后1.5秒后触发refetchSearch函数}, [refetchSearch]); // 当refetchSearch函数发生变化时重新创建防抖函数debouncedSearch// 监听搜索值变化当有搜索值时调用debouncedSearch函数进行搜索useEffect(() {if (!searchValue) return;debouncedSearch();}, [searchValue]);// 重置搜索const resetSearch useCallback(() {debouncedSearch.cancel(); // 取消搜索轮询queryClient.resetQueries([moduleSearch, searchValue]); // 重置查询缓存}, [debouncedSearch, queryClient, searchValue]);// 清空搜索const onClear useCallback(() {resetSearch(); // 调用重置方法handleClear?.(); // 调用清空回调方法}, [resetSearch, handleClear]);// 设置搜索内容如果值为空则调用清空方法const setKeyword (value: string) {if (value ) onClear();_setKeyword(value);};// 搜索按钮点击事件const handleSearch () {// 如果没有搜索内容或者搜索无数据则直接返回if (!searchValue || !searchResultData) return;// 根据搜索结果数据的第一个元素获取搜索结果对应的URLconst url getURLBySearchResult(searchResultData[0]);// 跳转到对应的URL如果获取不到URL则跳转到失败的搜索页面navigate(url ?? /search/fail?q${searchValue});};return (SearchContainer{/* 搜索框 */}Input loading{isFetching} value{keyword} hasPrefix placeholder{t(navbar.search_placeholder)} autoFocus{!isMobile} onChange{(event) setKeyword(event.target.value)} onEnter{handleSearch} onClear{onClear} /{/* 搜索按钮hasButton为true时显示 */}{hasButton SearchButton onClick{handleSearch}{t(search.search)}/SearchButton}{/* 搜索结果列表组件展示 */}{(isFetching || searchResultData SearchResults keyword{keyword} results{searchResultData ?? []} loading{isFetching} /}/SearchContainer);
});export default Search;
------------------------------------------------------------------------------
// /components/Search/styled.tsx
import styled from styled-components;
import variables from /styles/variables.module.scss;
export const SearchContainer styled.divposition: relative;margin: 0 auto;width: 100%;padding-right: 0;display: flex;align-items: center;justify-content: center;background: white;border: 0 solid white;border-radius: 4px;
;
export const SearchButton styled.divflex-shrink: 0;width: 72px;height: calc(100% - 4px);margin: 2px 2px 2px 8px;border-radius: 0 4px 4px 0;background-color: #121212;text-align: center;line-height: 34px;color: #fff;letter-spacing: 0.2px;font-size: 14px;cursor: pointer;media (max-width: ${variables.mobileBreakPoint}) {display: none;}
;4. 使用方式
// 引入组件
import Search from /components/Search
// 使用
{/* 带搜索按钮 */}
Search hasButton /
{/* 不带搜索按钮 */}
Search /二、搜索结果展示组件封装
注这个组件在上面Search组件中引用单独列出来讲讲。运用关注点分离的策略将页面分割成多个片段易维护容易定位代码位置。
1. 功能分析 1组件接受搜索内容是否显示loading加载以及搜索列表这三个参数 2根据搜索结果列表按模块类型分类数据这里举例2种类型如Transaction 和 Block 3对搜索的模块类型列表添加点击事件当点击某个模块时展示该模块的数据 4不同模块类型的列表展示不同效果例如类型是 Transaction显示交易信息包括交易名称和所在区块的编号类型是 Block则显示区块信息包括区块编号 5通过useEffect监听数据变化发生变化时重置激活的模块类型分类默认不选中任何模块类型 6封装不同模块匹配对应的地址名字的方法统一管理 7采用联合等进行类型声明的定义 2. 代码详细注释
// /components/Search/SearchResults/index.tsx
import { useTranslation } from react-i18next;
import classNames from classnames;
import { FC, useEffect, useState } from react;
import { SearchResultsContainer, CategoryFilterList, SearchResultList, SearchResultListItem } from ./styled;
import { useIsMobile } from /hooks;
import Loading from /components/Loading;
import { SearchResultType, SearchResult } from /models/Search;
// 引入不同模块匹配对应的地址名字方法
import { getURLBySearchResult, getNameBySearchResult } from ../utils;// 组件的类型定义
type Props {keyword?: string; // 搜索内容loading?: boolean; // 是否显示 loading 状态results: SearchResult[]; // 搜索结果列表
};// 列表数据每一项Item的渲染
const SearchResultItem: FC{ keyword?: string; item: SearchResult } ({ item, keyword }) {const { t } useTranslation(); // 使用国际化const to getURLBySearchResult(item); // 根据搜索结果项获取对应的 URLconst displayName getNameBySearchResult(item); // 根据搜索结果项获取显示名称// 如果搜索结果项类型是 Transaction则显示交易信息if (item.type SearchResultType.Transaction) {return (SearchResultListItem to{to}div className{classNames(content)}{/* 显示交易名称 */}div className{classNames(secondary-text)} title{displayName}{displayName}/div{/* 显示交易所在区块的编号 */}div className{classNames(sub-title, monospace)}{t(search.block)} # {item.attributes.blockNumber}/div/div/SearchResultListItem);}// 否则类型是Block, 显示区块信息return (SearchResultListItem to{to}div className{classNames(content)} title{displayName}{displayName}/div/SearchResultListItem);
};// 搜索结果列表
export const SearchResults: FCProps ({ keyword , results, loading }) {const isMobile useIsMobile(); // 判断是否是移动端const { t } useTranslation(); // 使用国际化// 设置激活的模块类型分类const [activatedCategory, setActivatedCategory] useStateSearchResultType | undefined(undefined);// 当搜索结果列表发生变化时重置激活的分类useEffect(() {setActivatedCategory(undefined);}, [results]);// 根据搜索结果列表按模块类型分类数据const categories results.reduce((acc, result) {if (!acc[result.type]) {acc[result.type] [];}acc[result.type].push(result);return acc;}, {} as RecordSearchResultType, SearchResult[]);// 按模块类型分类的列表const SearchResultBlock (() {return (SearchResultList{Object.entries(categories).filter(([type]) (activatedCategory undefined ? true : activatedCategory type)).map(([type, items]) (div key{type} className{classNames(search-result-item)}div className{classNames(title)}{t(search.${type})}/divdiv className{classNames(list)}{items.map((item) (SearchResultItem keyword{keyword} key{item.id} item{item} /))}/div/div))}/SearchResultList);})();// 如果搜索结果列表为空则显示空数据提示否则显示搜索结果列表return (SearchResultsContainer{!loading Object.keys(categories).length 0 (CategoryFilterList{(Object.keys(categories) as SearchResultType[]).map((category) (div key{category} className{classNames(categoryTagItem, { active: activatedCategory category })} onClick{() setActivatedCategory((pre) (pre category ? undefined : category))}{t(search.${category})} {(${categories[category].length})}/div))}/CategoryFilterList)}{loading ? Loading size{isMobile ? small : undefined} / : results.length 0 ? div className{classNames(empty)}{t(search.no_search_result)}/div : SearchResultBlock}/SearchResultsContainer);
};------------------------------------------------------------------------------
// /components/Search/SearchResults/styled.tsx
import styled from styled-components;
import Link from /components/Link;
export const SearchResultsContainer styled.divdisplay: flex;flex-direction: column;gap: 12px;width: 100%;max-height: 292px;overflow-y: auto;background: #fff;color: #000;border-radius: 4px;box-shadow: 0 4px 4px 0 #1010100d;position: absolute;z-index: 2;top: calc(100% 8px);left: 0;.empty {padding: 28px 0;text-align: center;font-size: 16px;color: #333;}
;
export const CategoryFilterList styled.divdisplay: flex;flex-wrap: wrap;padding: 12px 12px 0;gap: 4px;.categoryTagItem {border: 1px solid #e5e5e5;border-radius: 24px;padding: 4px 12px;cursor: pointer;transition: all 0.3s;.active {border-color: var(--cd-primary-color);color: var(--cd-primary-color);}}
;
export const SearchResultList styled.div.search-result-item {.title {color: #666;font-size: 0.65rem;letter-spacing: 0.5px;font-weight: 700;padding: 12px 12px 6px;background-color: #f5f5f5;text-align: left;}.list {padding: 6px 8px;}}
;
export const SearchResultListItem styled(Link)display: block;width: 100%;padding: 4px 0;cursor: pointer;border-bottom: solid 1px #e5e5e5;.content {display: flex;align-items: center;justify-content: space-between;width: 100%;padding: 4px;border-radius: 4px;text-align: left;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;color: var(--cd-primary-color);}.secondary-text {flex: 1;width: 0;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;}.sub-title {font-size: 14px;color: #666;overflow: hidden;margin: 0 4px;}:last-child {border-bottom: none;}:hover,:focus-within {.content {background: #f5f5f5;}}
;三、引用到文件自行取用
1获取不同模块地址展示名称的方法
// /components/Search/utils
import { SearchResultType, SearchResult } from /models/Search;
// 根据搜索结果项类型返回对应的 URL 链接
export const getURLBySearchResult (item: SearchResult) {const { type, attributes } item;switch (type) {case SearchResultType.Block:// 如果搜索结果项类型是 Block则返回对应的区块详情页面链接return /block/${attributes.blockHash};case SearchResultType.Transaction:// 如果搜索结果项类型是 Transaction则返回对应的交易详情页面链接return /transaction/${attributes.transactionHash};default:// 如果搜索结果项类型不是 Block 或者 Transaction则返回空字符串return ;}
};
// 根据搜索结果项类型返回不同显示名称
export const getNameBySearchResult (item: SearchResult) {const { type, attributes } item;switch (type) {case SearchResultType.Block:return attributes?.number?.toString(); // 返回高度case SearchResultType.Transaction:return attributes?.transactionHash?.toString(); // 返回交易哈希default:return ; // 返回空字符串}
};2用到的类型声明
// /models/Search/index.ts
import { Response } from /request/types
import { Block } from /models/Block
import { Transaction } from /models/Transaction
export enum SearchResultType {Block block,Transaction ckb_transaction,
}
export type SearchResult | Response.WrapperBlock, SearchResultType.Block| Response.WrapperTransaction, SearchResultType.Transaction
-------------------------------------------------------------------------------------------------------
// /models/Block/index.ts
export interface Block {blockHash: stringnumber: numbertransactionsCount: numberproposalsCount: numberunclesCount: numberuncleBlockHashes: string[]reward: stringrewardStatus: pending | issuedtotalTransactionFee: stringreceivedTxFee: stringreceivedTxFeeStatus: pending | calculatedtotalCellCapacity: stringminerHash: stringminerMessage: stringtimestamp: numberdifficulty: stringepoch: numberlength: stringstartNumber: numberversion: numbernonce: stringtransactionsRoot: stringblockIndexInEpoch: stringminerReward: stringliveCellChanges: stringsize: numberlargestBlockInEpoch: numberlargestBlock: numbercycles: number | nullmaxCyclesInEpoch: number | nullmaxCycles: number | null
}
-------------------------------------------------------------------------------------------------------
// /models/Transaction/index.ts
export interface Transaction {isBtcTimeLock: booleanisRgbTransaction: booleanrgbTxid: string | nulltransactionHash: string// FIXME: this type declaration should be fixed by adding a transformation between internal state and response of APIblockNumber: number | stringblockTimestamp: number | stringtransactionFee: stringincome: stringisCellbase: booleantargetBlockNumber: numberversion: numberdisplayInputs: anydisplayOutputs: anyliveCellChanges: stringcapacityInvolved: stringrgbTransferStep: string | nulltxStatus: stringdetailedMessage: stringbytes: numberlargestTxInEpoch: numberlargestTx: numbercycles: number | nullmaxCyclesInEpoch: number | nullmaxCycles: number | nullcreateTimestamp?: number
}总结
下一篇讲【全局常用Echarts组件封装】。关注本栏目将实时更新。