component-library-for-react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ import e from"./useDebounce.js";import t from"./useThrottle.js";import n from"./useClickOutside.js";export{n as useClickOutside,e as useDebounce,t as useThrottle};
@@ -0,0 +1 @@
1
+ import{useEffect as e,useRef as t}from"react";var n=(n,r)=>{let i=t(r);e(()=>{i.current=r},[r]),e(()=>{let e=e=>{let t=n.current;!t||t.contains(e.target)||i.current(e)};return document.addEventListener(`click`,e),document.addEventListener(`touchstart`,e),()=>{document.removeEventListener(`click`,e),document.removeEventListener(`touchstart`,e)}},[n])};export{n as default};
@@ -0,0 +1 @@
1
+ import{useCallback as e,useEffect as t,useRef as n}from"react";var r=(r,i,a=!1)=>{let o=n(null),s=e(()=>{o.current&&=(clearTimeout(o.current),null)},[]),c=e((...e)=>{if(o.current&&clearTimeout(o.current),a){let t=!o.current;o.current=setTimeout(()=>{o.current=null},i),t&&r(...e)}else o.current=setTimeout(()=>{r(...e),o.current=null},i)},[r,i,a]);return t(()=>s,[s]),{run:c,cancel:s}};export{r as default};
@@ -0,0 +1 @@
1
+ import{useCallback as e,useEffect as t,useRef as n}from"react";function r(r,i=300,a=!1){let o=n(r),s=n(null),c=n(0);t(()=>{o.current=r},[r]);let l=e(()=>{s.current&&=(clearTimeout(s.current),null),c.current=0},[]),u=e((...e)=>{let t=Date.now();if(c.current===0){if(a){o.current(...e),c.current=t;return}c.current=t}let n=i-(t-c.current);n<=0?(s.current&&=(clearTimeout(s.current),null),o.current(...e),c.current=t):s.current||=setTimeout(()=>{o.current(...e),c.current=Date.now(),s.current=null},n)},[i,a]);return t(()=>l,[l]),{run:u,cancel:l}}export{r as default};
@@ -0,0 +1 @@
1
+ import e from"./hooks/useDebounce.js";import t from"./hooks/useThrottle.js";import n from"./hooks/useClickOutside.js";import"./hooks/index.js";import r from"./packages/LazyLoadImage/index.js";import i from"./packages/VirtualList/index.js";import a from"./packages/Upload/index.js";import"./packages/index.js";export{r as LazyLoadImage,a as Upload,i as VirtualList,n as useClickOutside,e as useDebounce,t as useThrottle};
@@ -0,0 +1 @@
1
+ import{useEffect as e,useRef as t,useState as n}from"react";import{Fragment as r,jsx as i}from"@emotion/react/jsx-runtime";var a=({src:a,placeholder:o,alt:s,className:c=``,style:l={}})=>{let[u,d]=n(o),[f,p]=n(!1),m=t(null);return e(()=>{if(!m.current)return;let e=new IntersectionObserver(([t])=>{t.isIntersecting&&(d(a),m.current&&e.unobserve(m.current))});return e.observe(m.current),()=>{e.disconnect()}},[a]),i(r,{children:i(`img`,{src:u,alt:s,ref:m,className:c,style:{objectFit:`cover`,transition:`opacity 0.3s ease-in-out`,opacity:f?1:.5,width:`200px`,...l},onLoad:()=>{p(!0)}})})};export{a as default};
@@ -0,0 +1,5 @@
1
+ import{useRef as e,useState as t}from"react";import{jsx as n,jsxs as r}from"@emotion/react/jsx-runtime";import{css as i}from"@emotion/react";var a=({accept:a,action:o,multiple:s=!1,maxCount:c,onChange:l})=>{let u=e(null),[d,f]=t([]),[p,m]=t(!1),h=()=>{u.current&&u.current.click()},g=async e=>{let t=e.target.files?Array.from(e.target.files):[];if(t.length===0)return;let n=[...d,...t];c&&n.length>c&&(n=n.slice(0,c),alert(`最多只能上传${c}个文件`)),f(n),l?.(n);try{await _(n)}catch(e){console.log(e)}finally{u.current&&(u.current.value=``)}},_=async e=>{m(!0);let t=new FormData;s?e.forEach(e=>{t.append(`files`,e)}):t.append(`file`,e[0]);try{return(await fetch(o,{method:`POST`,body:t})).ok?!0:Error(`上传失败`)}catch{return Error(`上传失败`)}finally{m(!1)}},v=e=>{let t=d.filter((t,n)=>n!==e);f(t),l?.(t)},y=i`cursor:pointer;padding:8px 16px;`,b=i`display:none;`,x=i`margin-top:15px;list-style:none;padding-left:0px;
2
+ li {
3
+ margin-bottom:8px;
4
+ }
5
+ `;return r(`div`,{children:[n(`input`,{type:`file`,ref:u,accept:a,multiple:s,onChange:g,css:b}),n(`button`,{onClick:h,disabled:p,css:y,children:p?`上传中...`:`请选择上传的文件`}),n(`ul`,{css:x,children:d.map((e,t)=>r(`li`,{children:[n(`span`,{style:{marginRight:`10px`},children:e.name}),r(`span`,{style:{color:`#888`,fontSize:`12px`,marginRight:`15px`},children:[`(`,(e.size/1024).toFixed(2),` KB)`]}),n(`button`,{onClick:()=>v(t),style:{color:`red`,border:`none`,background:`none`,cursor:`pointer`},children:`删除`})]},e.name))})]})};export{a as default};
@@ -0,0 +1 @@
1
+ import{useMemo as e,useRef as t,useState as n}from"react";import{jsx as r,jsxs as i}from"@emotion/react/jsx-runtime";import{css as a}from"@emotion/react";var o=({listData:o,itemHeight:s,height:c,renderItem:l})=>{let[u,d]=n(0),f=t(null),p=o.length*s,{startIndex:m,endIndex:h,startOffset:g}=e(()=>{let e=Math.floor(u/s),t=e+Math.ceil(c/s),n=Math.max(0,e-2);return{startIndex:n,endIndex:Math.min(o.length,t+2),startOffset:n*s}},[u,o.length,s,c]),_=e(()=>o.slice(m,h),[o,m,h]),v=e=>{e.currentTarget&&d(e?.currentTarget?.scrollTop)},y=a`height:${c}px;overflow-y:auto;position:relative;border:1px solid #ccc;`,b=a`height: ${p}px; position: absolute;width: 100%; top: 0;left: 0;z-index: -1;`,x=a`transform: translateY(${g}px);position: absolute; left: 0; right: 0;top: 0 `;return i(`div`,{ref:f,onScroll:v,css:y,children:[r(`div`,{css:b}),r(`div`,{css:x,children:_.map((e,t)=>{let n=m+t;return r(`div`,{style:{height:`${s}px`},children:l(e,n)},n)})})]})};export{o as default};
@@ -0,0 +1 @@
1
+ import e from"./LazyLoadImage/index.js";import t from"./VirtualList/index.js";import n from"./Upload/index.js";export{e as LazyLoadImage,n as Upload,t as VirtualList};
@@ -0,0 +1 @@
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));exports.__toESM=s;
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./useDebounce.js"),t=require("./useThrottle.js"),n=require("./useClickOutside.js");exports.useClickOutside=n.default,exports.useDebounce=e.default,exports.useThrottle=t.default;
@@ -0,0 +1 @@
1
+ let e=require("react");var t=(t,n)=>{let r=(0,e.useRef)(n);(0,e.useEffect)(()=>{r.current=n},[n]),(0,e.useEffect)(()=>{let e=e=>{let n=t.current;!n||n.contains(e.target)||r.current(e)};return document.addEventListener(`click`,e),document.addEventListener(`touchstart`,e),()=>{document.removeEventListener(`click`,e),document.removeEventListener(`touchstart`,e)}},[t])};exports.default=t;
@@ -0,0 +1 @@
1
+ let e=require("react");var t=(t,n,r=!1)=>{let i=(0,e.useRef)(null),a=(0,e.useCallback)(()=>{i.current&&=(clearTimeout(i.current),null)},[]),o=(0,e.useCallback)((...e)=>{if(i.current&&clearTimeout(i.current),r){let r=!i.current;i.current=setTimeout(()=>{i.current=null},n),r&&t(...e)}else i.current=setTimeout(()=>{t(...e),i.current=null},n)},[t,n,r]);return(0,e.useEffect)(()=>a,[a]),{run:o,cancel:a}};exports.default=t;
@@ -0,0 +1 @@
1
+ let e=require("react");function t(t,n=300,r=!1){let i=(0,e.useRef)(t),a=(0,e.useRef)(null),o=(0,e.useRef)(0);(0,e.useEffect)(()=>{i.current=t},[t]);let s=(0,e.useCallback)(()=>{a.current&&=(clearTimeout(a.current),null),o.current=0},[]),c=(0,e.useCallback)((...e)=>{let t=Date.now();if(o.current===0){if(r){i.current(...e),o.current=t;return}o.current=t}let s=n-(t-o.current);s<=0?(a.current&&=(clearTimeout(a.current),null),i.current(...e),o.current=t):a.current||=setTimeout(()=>{i.current(...e),o.current=Date.now(),a.current=null},s)},[n,r]);return(0,e.useEffect)(()=>s,[s]),{run:c,cancel:s}}exports.default=t;
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./hooks/useDebounce.js"),t=require("./hooks/useThrottle.js"),n=require("./hooks/useClickOutside.js");require("./hooks/index.js");const r=require("./packages/LazyLoadImage/index.js"),i=require("./packages/VirtualList/index.js"),a=require("./packages/Upload/index.js");require("./packages/index.js"),exports.LazyLoadImage=r.default,exports.Upload=a.default,exports.VirtualList=i.default,exports.useClickOutside=n.default,exports.useDebounce=e.default,exports.useThrottle=t.default;
@@ -0,0 +1 @@
1
+ let e=require("react"),t=require("@emotion/react/jsx-runtime");var n=({src:n,placeholder:r,alt:i,className:a=``,style:o={}})=>{let[s,c]=(0,e.useState)(r),[l,u]=(0,e.useState)(!1),d=(0,e.useRef)(null);return(0,e.useEffect)(()=>{if(!d.current)return;let e=new IntersectionObserver(([t])=>{t.isIntersecting&&(c(n),d.current&&e.unobserve(d.current))});return e.observe(d.current),()=>{e.disconnect()}},[n]),(0,t.jsx)(t.Fragment,{children:(0,t.jsx)(`img`,{src:s,alt:i,ref:d,className:a,style:{objectFit:`cover`,transition:`opacity 0.3s ease-in-out`,opacity:l?1:.5,width:`200px`,...o},onLoad:()=>{u(!0)}})})};exports.default=n;
@@ -0,0 +1,5 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.js");let t=require("react");t=e.__toESM(t,1);let n=require("@emotion/react/jsx-runtime"),r=require("@emotion/react");var i=({accept:e,action:i,multiple:a=!1,maxCount:o,onChange:s})=>{let c=(0,t.useRef)(null),[l,u]=(0,t.useState)([]),[d,f]=(0,t.useState)(!1),p=()=>{c.current&&c.current.click()},m=async e=>{let t=e.target.files?Array.from(e.target.files):[];if(t.length===0)return;let n=[...l,...t];o&&n.length>o&&(n=n.slice(0,o),alert(`最多只能上传${o}个文件`)),u(n),s?.(n);try{await h(n)}catch(e){console.log(e)}finally{c.current&&(c.current.value=``)}},h=async e=>{f(!0);let t=new FormData;a?e.forEach(e=>{t.append(`files`,e)}):t.append(`file`,e[0]);try{return(await fetch(i,{method:`POST`,body:t})).ok?!0:Error(`上传失败`)}catch{return Error(`上传失败`)}finally{f(!1)}},g=e=>{let t=l.filter((t,n)=>n!==e);u(t),s?.(t)},_=r.css`cursor:pointer;padding:8px 16px;`,v=r.css`display:none;`,y=r.css`margin-top:15px;list-style:none;padding-left:0px;
2
+ li {
3
+ margin-bottom:8px;
4
+ }
5
+ `;return(0,n.jsxs)(`div`,{children:[(0,n.jsx)(`input`,{type:`file`,ref:c,accept:e,multiple:a,onChange:m,css:v}),(0,n.jsx)(`button`,{onClick:p,disabled:d,css:_,children:d?`上传中...`:`请选择上传的文件`}),(0,n.jsx)(`ul`,{css:y,children:l.map((e,t)=>(0,n.jsxs)(`li`,{children:[(0,n.jsx)(`span`,{style:{marginRight:`10px`},children:e.name}),(0,n.jsxs)(`span`,{style:{color:`#888`,fontSize:`12px`,marginRight:`15px`},children:[`(`,(e.size/1024).toFixed(2),` KB)`]}),(0,n.jsx)(`button`,{onClick:()=>g(t),style:{color:`red`,border:`none`,background:`none`,cursor:`pointer`},children:`删除`})]},e.name))})]})};exports.default=i;
@@ -0,0 +1 @@
1
+ const e=require("../../_virtual/_rolldown/runtime.js");let t=require("react");t=e.__toESM(t,1);let n=require("@emotion/react/jsx-runtime"),r=require("@emotion/react");var i=({listData:e,itemHeight:i,height:a,renderItem:o})=>{let[s,c]=(0,t.useState)(0),l=(0,t.useRef)(null),u=e.length*i,{startIndex:d,endIndex:f,startOffset:p}=(0,t.useMemo)(()=>{let t=Math.floor(s/i),n=t+Math.ceil(a/i),r=Math.max(0,t-2);return{startIndex:r,endIndex:Math.min(e.length,n+2),startOffset:r*i}},[s,e.length,i,a]),m=(0,t.useMemo)(()=>e.slice(d,f),[e,d,f]),h=e=>{e.currentTarget&&c(e?.currentTarget?.scrollTop)},g=r.css`height:${a}px;overflow-y:auto;position:relative;border:1px solid #ccc;`,_=r.css`height: ${u}px; position: absolute;width: 100%; top: 0;left: 0;z-index: -1;`,v=r.css`transform: translateY(${p}px);position: absolute; left: 0; right: 0;top: 0 `;return(0,n.jsxs)(`div`,{ref:l,onScroll:h,css:g,children:[(0,n.jsx)(`div`,{css:_}),(0,n.jsx)(`div`,{css:v,children:m.map((e,t)=>{let r=d+t;return(0,n.jsx)(`div`,{style:{height:`${i}px`},children:o(e,r)},r)})})]})};exports.default=i;
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./LazyLoadImage/index.js"),t=require("./VirtualList/index.js"),n=require("./Upload/index.js");exports.LazyLoadImage=e.default,exports.Upload=n.default,exports.VirtualList=t.default;
@@ -0,0 +1,4 @@
1
+ import { default as useDebounce } from './useDebounce';
2
+ import { default as useThrottle } from './useThrottle';
3
+ import { default as useClickOutside } from './useClickOutside';
4
+ export { useDebounce, useThrottle, useClickOutside };
@@ -0,0 +1,2 @@
1
+ declare const useClickOutsideFn: <T extends HTMLElement>(ref: React.RefObject<T | null>, handler: (event: MouseEvent | TouchEvent) => void) => void;
2
+ export default useClickOutsideFn;
@@ -0,0 +1,7 @@
1
+ type FunctionType = (...args: any[]) => any;
2
+ type FunctionReturn<T extends FunctionType> = {
3
+ run: (...args: Parameters<T>) => void;
4
+ cancel: () => void;
5
+ };
6
+ declare const useDebounceFn: <T extends FunctionType>(fn: T, delay: number, immediate?: boolean) => FunctionReturn<T>;
7
+ export default useDebounceFn;
@@ -0,0 +1,7 @@
1
+ type FunctionType = (...args: any[]) => any;
2
+ type FunctionRetur<T extends FunctionType> = {
3
+ run: (...args: Parameters<T>) => void;
4
+ cancel: () => void;
5
+ };
6
+ declare function useThrottleFn<T extends FunctionType>(fn: T, delay?: number, immediate?: boolean): FunctionRetur<T>;
7
+ export default useThrottleFn;
@@ -0,0 +1,2 @@
1
+ export * from './hooks';
2
+ export * from './packages';
@@ -0,0 +1,10 @@
1
+ import { CSSProperties } from 'react';
2
+ interface LazyLoadImagePropsType {
3
+ src: string;
4
+ placeholder: string;
5
+ alt?: string;
6
+ className?: string;
7
+ style?: CSSProperties;
8
+ }
9
+ declare const LazyLoadImage: ({ src, placeholder, alt, className, style }: LazyLoadImagePropsType) => import("@emotion/react/jsx-runtime").JSX.Element;
10
+ export default LazyLoadImage;
@@ -0,0 +1,9 @@
1
+ interface UploadPropsType {
2
+ accept?: string;
3
+ action: string;
4
+ multiple?: boolean;
5
+ maxCount?: number;
6
+ onChange?: (files: File[]) => void;
7
+ }
8
+ declare const Upload: ({ accept, action, multiple, maxCount, onChange }: UploadPropsType) => import("@emotion/react/jsx-runtime").JSX.Element;
9
+ export default Upload;
@@ -0,0 +1,9 @@
1
+ import { default as React } from 'react';
2
+ interface VirtualListPropsType<T> {
3
+ listData: T[];
4
+ itemHeight: number;
5
+ height: number;
6
+ renderItem: (item: T, index: number) => React.ReactNode;
7
+ }
8
+ declare const VirtualList: <T>({ listData, itemHeight, height, renderItem }: VirtualListPropsType<T>) => import("@emotion/react/jsx-runtime").JSX.Element;
9
+ export default VirtualList;
@@ -0,0 +1,4 @@
1
+ import { default as LazyLoadImage } from './LazyLoadImage/index';
2
+ import { default as VirtualList } from './VirtualList/index';
3
+ import { default as Upload } from './Upload/index';
4
+ export { LazyLoadImage, VirtualList, Upload };
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "component-library-for-react",
3
+ "version": "1.0.0",
4
+ "description": "这是一个关于react的组件库",
5
+ "type": "module",
6
+ "main": "./dist/lib/index.js",
7
+ "module": "./dist/es/index.js",
8
+ "types": "./dist/types/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types/index.d.ts",
12
+ "import": "./dist/es/index.js",
13
+ "require": "./dist/lib/index.js"
14
+ },
15
+ "./hooks": {
16
+ "types": "./dist/types/hooks/index.d.ts",
17
+ "import": "./dist/es/hooks/index.js",
18
+ "require": "./dist/lib/hooks/index.js"
19
+ },
20
+ "./packages": {
21
+ "types": "./dist/types/packages/index.d.ts",
22
+ "import": "./dist/es/packages/index.js",
23
+ "require": "./dist/lib/packages/index.js"
24
+ }
25
+ },
26
+ "typesVersions": {
27
+ "*": {
28
+ "hooks": [
29
+ "dist/types/hooks/index.d.ts"
30
+ ],
31
+ "packages": [
32
+ "dist/types/packages/index.d.ts"
33
+ ]
34
+ }
35
+ },
36
+ "sideEffects": [
37
+ "**/*.css",
38
+ "./src/hooks/index.ts",
39
+ "./src/packages/index.ts",
40
+ "./src/index.ts"
41
+ ],
42
+ "scripts": {
43
+ "dev": "vite",
44
+ "build": "tsc --noEmit && vite build",
45
+ "lint": "eslint src --ext .ts,.tsx",
46
+ "prepare": "husky",
47
+ "rebuild-readme": "node ./scripts/rebuild-readme.js",
48
+ "test": "echo 'test pass'"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://github.com/peichaojie/component-library-for-react"
53
+ },
54
+ "keywords": [
55
+ "react",
56
+ "react-component"
57
+ ],
58
+ "author": "裴超杰",
59
+ "license": "ISC",
60
+ "devDependencies": {
61
+ "@types/node": "^25.9.3",
62
+ "@types/react": "^19.2.17",
63
+ "@types/react-dom": "^19.2.3",
64
+ "@typescript-eslint/eslint-plugin": "^8.61.1",
65
+ "@typescript-eslint/parser": "^8.61.1",
66
+ "@vitejs/plugin-react": "^6.0.2",
67
+ "eslint": "^10.5.0",
68
+ "eslint-config-prettier": "^10.1.8",
69
+ "eslint-plugin-prettier": "^5.5.6",
70
+ "eslint-plugin-react-hooks": "^7.1.1",
71
+ "eslint-plugin-react-refresh": "^0.5.3",
72
+ "fast-glob": "^3.3.3",
73
+ "husky": "^9.1.7",
74
+ "lint-staged": "^16.4.0",
75
+ "prettier": "^3.8.4",
76
+ "typescript": "^6.0.3",
77
+ "vite": "^8.0.16",
78
+ "vite-plugin-dts": "^5.0.2"
79
+ },
80
+ "peerDependencies": {
81
+ "@emotion/react": "^11.14.0",
82
+ "react": "^18.0.0 || ^19.0.0",
83
+ "react-dom": "^18.0.0 || ^19.0.0"
84
+ },
85
+ "files": [
86
+ "dist"
87
+ ]
88
+ }
package/readme.md ADDED
@@ -0,0 +1,366 @@
1
+ # 项目全量使用文档
2
+
3
+ ## useClickOutside 使用文档
4
+
5
+ `useClickOutside` 是一个用于监听目标元素外部点击事件的自定义 Hook。当用户点击了指定 DOM 元素之外的区域时,会触发传入的回调函数。可用于下拉菜单等组件的收起逻辑。
6
+
7
+ ### 1. API 参数说明
8
+
9
+ #### 入参 (Inputs)
10
+
11
+ | 参数名 | 类型 | 必填 | 说明 |
12
+ | :--- | :--- | :--- | :--- |
13
+ | `ref` | `RefObject<HTMLElement \| null>` | 是 | 由 `useRef` 创建的引用对象,需绑定到目标 DOM 元素上 |
14
+ | `handler` | `(event: MouseEvent \| TouchEvent) => void` | 是 | 点击目标元素外部时触发的回调函数 |
15
+
16
+ #### 出参 (Outputs)
17
+
18
+ - **`void`**: 该 Hook 无返回值。
19
+
20
+ ### 2. 基础用法(下拉菜单)
21
+
22
+ 点击按钮打开菜单,点击菜单外部的任意地方自动关闭菜单。
23
+
24
+
25
+ ```tsx
26
+ import React, { FC, useState, useRef, useCallback } from "react";
27
+ import { css } from "@emotion/react";
28
+ import { useClickOutside } from '@/hooks/index'
29
+
30
+ const App: FC = () => {
31
+ const [isOpen, setIsOpen] = useState(false);
32
+ const [list] = useState([
33
+ { text: '菜单001', key: '001' },
34
+ { text: '菜单002', key: '002' }
35
+ ]);
36
+ const ref = useRef<HTMLUListElement | null>(null);
37
+ const changeIsOpenState = (e: React.MouseEvent<HTMLButtonElement>) => {
38
+ setIsOpen(!isOpen);
39
+ e.stopPropagation();
40
+ }
41
+ const handler = useCallback(() => {
42
+ setIsOpen(false);
43
+ }, []);
44
+ useClickOutside(ref, handler);
45
+ const listStyle = css`
46
+ list-style: none;
47
+ li {
48
+ color: #333;
49
+ &:hover {
50
+ color: red;
51
+ cursor: pointer;
52
+ }
53
+ }
54
+ `;
55
+ return (
56
+ <div>
57
+ <button onClick={e => changeIsOpenState(e)}>
58
+ {isOpen ? '关闭' : '打开'}菜单
59
+ </button>
60
+ {isOpen && (
61
+ <ul css={listStyle} ref={ref}>
62
+ {list.map(item => (
63
+ <li key={item.key}>{item.text}</li>
64
+ ))}
65
+ </ul>
66
+ )}
67
+ </div>
68
+ );
69
+ };
70
+
71
+ export default App;
72
+ ```
73
+
74
+
75
+
76
+
77
+ ---
78
+
79
+ ## useDebounce 使用文档
80
+
81
+ `useDebounce` 是一个用于处理函数防抖的自定义 Hook。它支持**延迟执行**和**立即执行**两种模式,并支持手动取消。
82
+
83
+ ### 1. API 参数说明
84
+
85
+ #### 入参 (Inputs)
86
+
87
+ | 参数名 | 类型 | 必填 | 默认值 | 说明 |
88
+ | :---------- | :--------- | :--- | :------ | :------------------------- |
89
+ | `fn` | `Function` | 是 | - | 需要进行防抖处理的目标函数 |
90
+ | `delay` | `number` | 否 | `300` | 防抖延迟时间(毫秒) |
91
+ | `immediate` | `boolean` | 否 | `false` | 是否在第一次触发时立即执行 |
92
+
93
+ #### 出参 (Outputs)
94
+
95
+ 返回一个对象,包含以下两个方法:
96
+
97
+ - **`run`**: `(...args) => void` —— 实际在业务中调用的防抖包装函数。参数与原函数 `fn` 保持一致。
98
+ - **`cancel`**: `() => void` —— 手动取消当前未执行的定时器。
99
+
100
+ ### 2. 基础用法(搜索框输入)
101
+
102
+ 连续输入时不会触发change事件,停止输入 2000ms 后才触发。
103
+
104
+ ```tsx
105
+ import { FC, useEffect } from "react";
106
+ import { useDebounce } from "@/hooks/index";
107
+
108
+ const App: FC = () => {
109
+ const logValue = (label:string,value:string)=>{
110
+ console.log(`当前输入的${label} 是 ${value}`)
111
+ }
112
+ const { run: runUsername, cancel: cancelUsername } = useDebounce(
113
+ (val: string) => logValue('username', val),
114
+ 2000
115
+ );
116
+ const { run: runPassword, cancel: cancelPassword } = useDebounce(
117
+ (val: string) => logValue('password', val),
118
+ 2000
119
+ );
120
+ useEffect(() => {
121
+ return () => {
122
+ cancelUsername();
123
+ cancelPassword();
124
+ };
125
+ }, [cancelUsername, cancelPassword]);
126
+
127
+ return <div>
128
+ <form encType="application/x-www-form-urlencoded">
129
+ <label htmlFor="username">
130
+ 用户名:
131
+ <input type="text" name="username" onChange={e=>runUsername(e.target.value)}/>
132
+ </label>
133
+ &nbsp;&nbsp;
134
+ <label htmlFor="password">
135
+ 密 码:
136
+ <input type="password" name="password" onChange={e=>runPassword(e.target.value)}/>
137
+ </label>
138
+ &nbsp;&nbsp;
139
+ <button type="button">提交</button>
140
+ </form>
141
+ </div>
142
+ };
143
+
144
+ export default App;
145
+ ```
146
+
147
+
148
+ ---
149
+
150
+ ## useThrottle 使用文档
151
+
152
+ `useThrottle` 是一个用于函数节流(Throttle)的自定义 Hook。它可以将连续的高频操作限制为每隔固定时间仅执行一次。
153
+
154
+ ### 1. API 参数说明
155
+
156
+ #### 入参 (Inputs)
157
+
158
+ | 参数名 | 类型 | 必填 | 默认值 | 说明 |
159
+ | :--- | :--- | :--- | :--- | :--- |
160
+ | `fn` | `Function` | 是 | - | 需要限制执行频率的目标业务函数 |
161
+ | `delay` | `number` | 否 | `300` | 节流的时间间隔(毫秒) |
162
+ | `immediate` | `boolean` | 否 | `false` | 是否在第一次触发时立即执行。设为 `false` 则首下会延迟 `delay` 毫秒后执行 |
163
+
164
+ #### 出参 (Outputs)
165
+ 返回一个包含控制方法的对象:
166
+ * **`run`**: `(...args) => void` —— 经过节流包装后的触发函数,参数类型与原函数严格一致。
167
+ * **`cancel`**: `() => void` —— 清除当前正在排队的定时器,并重置时间戳计数。
168
+
169
+ ### 2. 基础用法:(表单提交)
170
+ 由于 `immediate` 默认为 `false`,在连续点击提交的按钮时,每隔2s执行一次。
171
+
172
+ ```tsx
173
+ import React ,{ FC, useEffect, useRef } from "react";
174
+ import { useThrottle } from "@/hooks/index";
175
+
176
+ const App: FC = () => {
177
+ const formRef = useRef<HTMLFormElement|null>(null);
178
+ const submitFormData = (e:React.MouseEvent<HTMLButtonElement>)=>{
179
+ const formData = new FormData(formRef.current!);
180
+ console.log(formData.get('username'))
181
+ }
182
+ const {run,cancel} = useThrottle(submitFormData,2000);
183
+ useEffect(()=>{
184
+ return ()=>cancel()
185
+ },[cancel])
186
+
187
+ return <div>
188
+ <form ref={formRef} encType="application/x-www-form-urlencoded">
189
+ <label htmlFor="username">
190
+ 用户名:
191
+ <input type="text" name="username"/>
192
+ </label>
193
+ &nbsp;&nbsp;
194
+ <label htmlFor="password">
195
+ 密 码:
196
+ <input type="password" name="password"/>
197
+ </label>
198
+ &nbsp;&nbsp;
199
+ <button type="button" onClick={run}>提交</button>
200
+ </form>
201
+ </div>
202
+ };
203
+
204
+ export default App;
205
+ ```
206
+
207
+
208
+ ---
209
+
210
+ ## LazyLoadImage 使用文档
211
+
212
+ `LazyLoadImage` 是一个支持图片懒加载和渐显动画效果的 React 组件。
213
+
214
+ ### 导入组件
215
+
216
+ ```tsx
217
+ import LazyLoadImage from "./LazyLoadImage";
218
+ ```
219
+
220
+ ### 属性 API (Props)
221
+
222
+ | 属性名 | 类型 | 必填 | 默认值 | 说明 |
223
+ | :--- | :--- | :--- | :--- | :--- |
224
+ | `src` | `string` | 是 | - | 真实图片的 URL 地址 |
225
+ | `placeholder` | `string` | 是 | - | 占位图或 Base64 低清图的 URL 地址 |
226
+ | `alt` | `string` | 否 | - | 图片的替代文本 |
227
+ | `className` | `string` | 否 | `""` | 额外的 CSS 类名 |
228
+ | `style` | `CSSProperties` | 否 | `{}` | 覆盖或新增的内联样式 |
229
+
230
+ ### 使用示例
231
+
232
+ ### 1. 自定义样式与尺寸
233
+ 组件默认宽度为 `200px`。你可以通过 `style` 或 `className` 自定义宽高。
234
+
235
+ ```tsx
236
+ <LazyLoadImage
237
+ src="http://adolphjie.com/xxx.png"
238
+ placeholder="/assets/default-avatar.png"
239
+ style={{ width: "100px", height: "100px", borderRadius: "50%" }}
240
+ />
241
+ ```
242
+
243
+
244
+ ---
245
+
246
+ ## Upload 使用文档
247
+
248
+ `LazyLoadImage` 是一个基于 React的轻量级文件上传组件,支持类型限制、多选以及最大数量截断。
249
+
250
+ ### 导入组件
251
+
252
+ ```tsx
253
+ import Upload from "./Upload";
254
+ ```
255
+
256
+ ### 属性 API (Props)
257
+
258
+ | 参数 | 类型 | 必填 | 默认值 | 说明 |
259
+ | :--- | :--- | :--- | :--- | :--- |
260
+ | `action` | `string` | 是 | - | 上传的后端接口 URL 地址 |
261
+ | `accept` | `string` | 否 | - | 限制可选的文件类型,如 `image/*` 或 `.pdf,.docx` |
262
+ | `multiple` | `boolean` | 否 | `false` | 是否支持按住 Ctrl/Shift 选择多个文件 |
263
+ | `maxCount` | `number` | 否 | - | 允许上传的最大文件数量限制 |
264
+ | `onChange` | `(files: File[]) => void` | 否 | - | 文件列表发生变化(添加/删除/截断)时的回调函数 |
265
+
266
+ ### 基础使用示例
267
+
268
+ #### 1. 单张图片上传
269
+ ```tsx
270
+ import Upload from './Upload';
271
+ const APP = ()=>{
272
+ const params = {
273
+ action:"http://127.0.0.1:4000/file",
274
+ }
275
+ return <div>
276
+ <Upload {...params}/>
277
+ </div>
278
+ }
279
+ export default APP;
280
+ ```
281
+
282
+ #### 2. 多文档限制上传(最多3个)
283
+ ```tsx
284
+ import Upload from './Upload';
285
+ const params = {
286
+ action:"http://127.0.0.1:4000/files",
287
+ multiple:true,
288
+ accept:".pdf,.doc,.docx",
289
+ maxCount:3,
290
+ onChange:(files:File[]) => console.log('最新的文档列表:', files)
291
+ }
292
+ const APP = ()=>{
293
+ return <div>
294
+ <Upload {...params}/>
295
+ </div>
296
+ }
297
+ export default APP;
298
+ ```
299
+
300
+ ### 后端接收注意事项
301
+ * **单文件模式 (`multiple={false}`)**:后端需通过名为 `file` 的字段获取单个文件。
302
+ * **多文件模式 (`multiple={true}`)**:后端需通过名为 `files` 的字段获取文件数组(例如 Node.js 中使用 `upload.array('files')`)。
303
+
304
+
305
+ ---
306
+
307
+ ## VirtualList 使用文档
308
+
309
+ `VirtualList` 是一个专为 React 打造的高性能、轻量级**固定高度虚拟列表组件**。当你的业务场景需要承载几千甚至数万条结构相同的列表数据时,该组件能将 DOM 节点数控制在极低的个位数,确保页面维持 60 FPS 的流畅度。
310
+
311
+ ### 导入组件
312
+
313
+ ```tsx
314
+ import VirtualList from "./VirtualList";
315
+ ```
316
+
317
+ ### 属性 API (Props)
318
+
319
+ | 参数名 | 类型 | 必填 | 默认值 | 说明 |
320
+ | :--- | :--- | :---: | :---: | :--- |
321
+ | `listData` | `T[]` | 是 | - | 列表的数据源数组(任意类型对象的数组)。 |
322
+ | `itemHeight` | `number` | 是 | - | 单行数据的绝对高度(单位 px),行内内容必须严丝合缝匹配此高度。 |
323
+ | `height` | `number` | 是 | - | 可视区域的容器高度(单位 px)。超出该高度将自动出现滚动条。 |
324
+ | `renderItem` | `(item: T, index: number) => ReactNode` | 是 | - | 单行自定义渲染函数。接收单条数据 `item` 及全局真实索引 `index`。 |
325
+
326
+ ### 使用示例
327
+
328
+ ```tsx
329
+ import { FC,useState } from "react";
330
+ import {VirtualList} from "./packages/index"
331
+
332
+ const APP:FC = ()=>{
333
+ const [list] = useState<Array<string>>([
334
+ "内容...1",
335
+ "内容...2",
336
+ "内容...3",
337
+ "内容...4",
338
+ "内容...5",
339
+ "内容...6",
340
+ "内容...7",
341
+ "内容...1",
342
+ "内容...2",
343
+ "内容...3",
344
+ "内容...4",
345
+ "内容...5",
346
+ "内容...6",
347
+ "内容...7",
348
+ ]);
349
+ const props = {
350
+ listData:list,
351
+ itemHeight:30,
352
+ height:100,
353
+ renderItem:(item:string,index:number)=>{
354
+ return <div key={index}>{item}</div>
355
+ }
356
+ }
357
+ return <div>
358
+ <VirtualList {...props}></VirtualList>
359
+ </div>
360
+ }
361
+
362
+ export default APP;
363
+ ```
364
+
365
+ ---
366
+