React+TS前臺項目實戰(二十一)-- Search業務組件封裝實現全局搜索

文章目錄

  • 前言
  • 一、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)使用useCallback,useMemo,useEffect, memo,lodash.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: FC<Props> = 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.div`position: 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.div`flex-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: FC<Props> = ({ keyword = "", results, loading }) => {const isMobile = useIsMobile(); // 判斷是否是移動端const { t } = useTranslation(); // 使用國際化// 設置激活的模塊類型分類const [activatedCategory, setActivatedCategory] = useState<SearchResultType | 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 Record<SearchResultType, 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}`)}</div><div 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.div`display: 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.div`display: 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.Wrapper<Block, SearchResultType.Block>| Response.Wrapper<Transaction, SearchResultType.Transaction>
-------------------------------------------------------------------------------------------------------
// @/models/Block/index.ts
export interface Block {blockHash: stringnumber: numbertransactionsCount: numberproposalsCount: numberunclesCount: numberuncleBlockHashes: string[]reward: stringrewardStatus: 'pending' | 'issued'totalTransactionFee: stringreceivedTxFee: stringreceivedTxFeeStatus: 'pending' | 'calculated'totalCellCapacity: 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組件封裝】。關注本欄目,將實時更新。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/37771.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/37771.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/37771.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

spring如何給bean動態取不同的別名

開源項目SDK&#xff1a;https://github.com/mingyang66/spring-parent 個人文檔&#xff1a;https://mingyang66.github.io/raccoon-docs/#/ spring、springboot向容器中注入bean的時候一般情況下只有一個別名&#xff0c;在某些特殊場景需要指定多個別名。 方案一&#xff1a…

v-if 和 v-show 的含義、使用方式及使用時的區別

學習內容&#xff1a; v-if 和 v-show 的含義、使用方式及使用時的區別&#xff1a; 例如&#xff1a; v-if 的含義v-if 的用法v-show 的含義v-show 的用法v-if 與 v-show 區別 知識小結&#xff1a; 小結 1、v-if v-if 是一種條件性地渲染元素的指令。當條件為真時&#…

ic基礎|功耗篇04:門級低功耗技術

大家好&#xff0c;我是數字小熊餅干&#xff0c;一個練習時長兩年半的IC打工人。我在兩年前通過自學跨行社招加入了IC行業。現在我打算將這兩年的工作經驗和當初面試時最常問的一些問題進行總結&#xff0c;并通過匯總成文章的形式進行輸出&#xff0c;相信無論你是在職的還是…

【chatgpt】npy文件和npz文件區別

npy文件和npz文件都是用于存儲NumPy數組的文件格式。它們的主要區別如下&#xff1a; npy文件&#xff1a;這種文件格式用于存儲單個NumPy數組。它是一種簡單的二進制文件格式&#xff0c;可以快速地讀寫NumPy數組。 npz文件&#xff1a;這種文件格式是一個壓縮包&#xff0c;…

《Windows API每日一練》6.2 客戶區鼠標消息

第五章已經講到&#xff0c;Windows只會把鍵盤消息發送到當前具有輸入焦點的窗口。鼠標消息則不同&#xff1a;當鼠標經過窗口或在窗口內被單擊&#xff0c;則即使該窗口是非活動窗口或不帶輸入焦點&#xff0c; 窗口過程還是會收到鼠標消息。Windows定義了 21種鼠標消息。不過…

UE5藍圖快速實現打開網頁與加群

藍圖節點&#xff1a;啟動URL 直接將對應的網址輸入&#xff0c;并使用即可快速打開對應的網頁&#xff0c;qq、discord等群聊的加入也可以直接通過該節點來完成。 使用后會直接打開瀏覽器。

第11章 規劃過程組(收集需求)

第11章 規劃過程組&#xff08;一&#xff09;11.3收集需求&#xff0c;在第三版教材第377~378頁&#xff1b; 文字圖片音頻方式 第一個知識點&#xff1a;主要輸出 1、需求跟蹤矩陣 內容 業務需要、機會、目的和目標 項目目標 項目范圍和 WBS 可…

【強化學習】第01期:緒論

筆者近期上了國科大周曉飛老師《強化學習及其應用》課程&#xff0c;計劃整理一個強化學習系列筆記。筆記中所引用的內容部分出自周老師的課程PPT。筆記中如有不到之處&#xff0c;敬請批評指正。 文章目錄 1.1 概述1.2 Markov決策過程1.2.1 Markov Process (MP) 馬爾科夫過程1…

(五)SvelteKit教程:錯誤頁和重定向

&#xff08;五&#xff09;SvelteKit教程&#xff1a;錯誤頁和重定向 設置404頁面和重定向非常容易&#xff0c;我們還是在 /about 目錄下學習這個知識&#xff0c;文件結構如下&#xff1a; ├── layout.svelte ├── page.svelte ├── about │ └── [aboutID] │…

基于深度學習的人臉關鍵點檢測

1. 任務和目標 人臉關鍵點檢測的主要任務是識別并定位人臉圖像中的特定關鍵點&#xff0c;例如眼睛的角點、眉毛的頂點、鼻子的底端、嘴角等。這些關鍵點不僅能提供面部結構的幾何信息&#xff0c;還可以用于分析表情、識別個體&#xff0c;甚至檢測面部姿勢。 2. 技術和方法…

什么是數據類型,Python 有哪些基本數據類型?

一、什么是數據類型 數據類型是計算機語言中一個基本概念&#xff0c;它定義了變量可以存儲什么樣的數據以及可以對這些數據執行什么樣的操作。在Python中&#xff0c;數據類型決定了變量的存儲方式、內存占用、數據的合法操作和表示方式等。 數據類型的作用包括&#xff1a;…

計算機中的16g加32g不對稱雙通道性能分析

計算機中的16g加32g不對稱雙通道性能分析 16GB加32GB不對稱雙通道配置會對性能產生一定影響&#xff0c;但仍然在穩定兼容的范圍內。 在探討16GB加32GB不對稱雙通道配置的性能影響時&#xff0c;我們首先需要理解雙通道技術的基本原理。雙通道技術通過同時向兩根內存中讀寫數…

數據結構速成--排序算法

由于是速成專題&#xff0c;因此內容不會十分全面&#xff0c;只會涵蓋考試重點&#xff0c;各學校課程要求不同 &#xff0c;大家可以按照考綱復習&#xff0c;不全面的內容&#xff0c;可以看一下小編主頁數據結構初階的內容&#xff0c;找到對應專題詳細學習一下。 這一章…

C語言中常用的運算符、表達式和語句

C語言是一種通用的、高級的編程語言&#xff0c;其歷史可以追溯到20世紀60年代末至70年代初。C語言最初是由丹尼斯里奇&#xff08;Dennis Ritchie&#xff09;在貝爾實驗室為開發UNIX操作系統而設計的。它繼承了許多B語言的特性&#xff0c;而B語言則是由迷糊老師&#xff08;…

安全與加密常識(0)安全與加密概述

文章目錄 一、信息安全的基本概念二、加密技術概述三、常見的安全協議和實踐四、加密的挑戰與應對 在數字時代&#xff0c;信息安全和加密已成為保護個人和企業數據不受侵犯的關鍵技術。本文將探討信息安全的基礎、加密的基本原理&#xff0c;以及實用的保護措施&#xff0c;以…

RAG一文讀懂!概念、場景、優勢、對比微調與項目代碼示例

本文結合“基于 ERNIE SDKLangChain 搭建個人知識庫”的代碼示例&#xff0c;為您講解 RAG 的相關概念。 01 概念 在2020年 Facebook AI Research(FAIR)團隊發表一篇名為《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》的論文。這篇論文首次提出了 RA…

Java應用cpu過高如何分析

1. 查看進程cpu使用情況 top 2. 根據PID查看指定進程的各線程的cpu使用情況 top -H -p PID 線程分析&#xff1a; jstack&#xff1a;生成Java線程堆棧&#xff0c;用于分析是否有線程處于忙等待狀態或死循環。命令&#xff1a; shell jstack -l <pid> > threaddu…

機器人控制系列教程之關節空間運動控制器搭建(1)

機器人位置控制類型 機器人位置控制分為兩種類型&#xff1a; 關節空間運動控制—在這種情況下&#xff0c;機器人的位置輸入被指定為一組關節角度或位置的向量&#xff0c;這被稱為機器人的關節配置&#xff0c;記作q。控制器跟蹤一個參考配置&#xff0c;記作 q r e f q_{re…

免費翻譯API及使用指南——百度、騰訊

目錄 一、百度翻譯API 二、騰訊翻譯API 一、百度翻譯API 百度翻譯API接口免費翻譯額度&#xff1a;標準版&#xff08;5萬字符免費/每月&#xff09;、高級版&#xff08;100萬字符免費/每月-需個人認證&#xff0c;基本都能通過&#xff09;、尊享版&#xff08;200萬字符免…

學習陽明心學,需要下真功夫,持續用功

陽明心學是功夫之學&#xff0c;看到善的就發揚光大&#xff0c;看到惡的就立即改正&#xff0c;這才是真功夫