@aozi6666/bee-design 0.1.0 → 0.1.2

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.
package/README.md CHANGED
@@ -1,73 +1,182 @@
1
- # React + TypeScript + Vite
1
+ ## Bee Design
2
2
 
3
- This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
3
+ React UI Component Library inspired by Honeycomb 🐝
4
+ 一个基于 React + TypeScript 的轻量级组件库,用来练习和演示现代前端工程化(组件开发、单元测试、Storybook、CI 等)实践。
4
5
 
5
- Currently, two official plugins are available:
6
+ ### GitHub
6
7
 
7
- - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
- - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
8
+ `https://github.com/aozi6666/Bee-Design`
9
9
 
10
- ## React Compiler
10
+ ### Install
11
11
 
12
- The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
12
+ ```bash
13
+ npm install @aozi6666/bee-design
14
+ # or
15
+ yarn add @aozi6666/bee-design
16
+ ```
17
+
18
+ ### Quick Start
19
+
20
+ ```tsx
21
+ import React from "react";
22
+ import { createRoot } from "react-dom/client";
23
+ import BeeButton from "@aozi6666/bee-design/build/components/Button";
24
+ import "@aozi6666/bee-design/build/index.css";
25
+
26
+ const App = () => (
27
+ <div style={{ padding: 24 }}>
28
+ <BeeButton>Bee Design Button</BeeButton>
29
+ </div>
30
+ );
31
+
32
+ createRoot(document.getElementById("root")!).render(<App />);
33
+ ```
34
+
35
+ > **提示**:发布到 npm 后,推荐在业务项目中通过别名导入,如 `import { Button } from '@aozi6666/bee-design'`。当前版本的打包入口为 `build/index.js`,已经在 `package.json` 中配置好。
36
+
37
+ ---
38
+
39
+ ## Features
40
+
41
+ - **基于 React + TypeScript**:完整的类型定义,开发体验友好。
42
+ - **现代工程化**:使用 Vite 开发体验、本地 Storybook 文档、Jest + Testing Library 单元测试、ESLint 规范代码。
43
+ - **常用基础组件**:涵盖表单、导航、反馈等常见场景。
44
+ - **渐进式学习**:代码中包含一定注释,适合学习组件封装、hooks 抽离以及 TS 类型设计。
45
+
46
+ ---
47
+
48
+ ## Components
13
49
 
14
- ## Expanding the ESLint configuration
50
+ Bee Design 当前提供以下组件(持续扩展中):
15
51
 
16
- If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
52
+ - **Button 按钮**
53
+ - 类型:`primary` / `default` / `danger` / `link`
54
+ - 支持 `href` / `target`、禁用状态等
55
+ - **Input 输入框**
56
+ - 支持前后缀图标、禁用状态、清空等能力
57
+ - **AutoComplete 自动完成**
58
+ - 根据输入关键字异步 / 同步返回候选项
59
+ - 支持自定义下拉项渲染(`renderOption`)
60
+ - **Upload 上传**
61
+ - 支持点击上传、拖拽上传(`drag`)、上传进度展示、文件列表
62
+ - 提供原生表单上传示例和 `axios` 上传方式
63
+ - **Menu 菜单**
64
+ - 水平 / 垂直导航菜单
65
+ - 支持子菜单展开、选中高亮
66
+ - **Icon 图标**
67
+ - 基于 Font Awesome 封装
68
+ - 支持主题颜色、尺寸、旋转动画等
69
+ - **Progress 进度条**
70
+ - 支持不同主题颜色和高度
71
+ - **Transition 动画**
72
+ - 对 `react-transition-group` 的简单封装,用于出入场动画
73
+ - **Hooks**
74
+ - `useClickOutside`:点击组件外区域的处理
75
+ - `useDebounce`:防抖输入处理
17
76
 
18
- ```js
19
- export default defineConfig([
20
- globalIgnores(['dist']),
21
- {
22
- files: ['**/*.{ts,tsx}'],
23
- extends: [
24
- // Other configs...
77
+ 具体用法可以查看 `src/components` 下各个组件的 `*.stories.tsx` 示例和测试用例。
25
78
 
26
- // Remove tseslint.configs.recommended and replace with this
27
- tseslint.configs.recommendedTypeChecked,
28
- // Alternatively, use this for stricter rules
29
- tseslint.configs.strictTypeChecked,
30
- // Optionally, add this for stylistic rules
31
- tseslint.configs.stylisticTypeChecked,
79
+ ---
32
80
 
33
- // Other configs...
34
- ],
35
- languageOptions: {
36
- parserOptions: {
37
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
- tsconfigRootDir: import.meta.dirname,
39
- },
40
- // other options...
41
- },
42
- },
43
- ])
81
+ ## Usage Example
82
+
83
+ `AutoComplete` 为例:
84
+
85
+ ```tsx
86
+ import React, { useCallback } from "react";
87
+ import AutoComplete, {
88
+ type DataSourceType,
89
+ } from "@aozi6666/bee-design/build/components/AutoComplete";
90
+ import "@aozi6666/bee-design/build/index.css";
91
+
92
+ interface LakerPlayer {
93
+ value: string;
94
+ number: number;
95
+ }
96
+
97
+ const players: Array<DataSourceType<LakerPlayer>> = [
98
+ { value: "bradley", number: 11 },
99
+ { value: "james", number: 23 },
100
+ // ...
101
+ ];
102
+
103
+ const Demo = () => {
104
+ const fetchSuggestions = useCallback(
105
+ (query: string) => players.filter((p) => p.value.toLowerCase().includes(query.toLowerCase())),
106
+ [],
107
+ );
108
+
109
+ return (
110
+ <AutoComplete fetchSuggestions={fetchSuggestions} placeholder="输入湖人队球员英文名试试" />
111
+ );
112
+ };
44
113
  ```
45
114
 
46
- You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
-
48
- ```js
49
- // eslint.config.js
50
- import reactX from 'eslint-plugin-react-x'
51
- import reactDom from 'eslint-plugin-react-dom'
52
-
53
- export default defineConfig([
54
- globalIgnores(['dist']),
55
- {
56
- files: ['**/*.{ts,tsx}'],
57
- extends: [
58
- // Other configs...
59
- // Enable lint rules for React
60
- reactX.configs['recommended-typescript'],
61
- // Enable lint rules for React DOM
62
- reactDom.configs.recommended,
63
- ],
64
- languageOptions: {
65
- parserOptions: {
66
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
- tsconfigRootDir: import.meta.dirname,
68
- },
69
- // other options...
70
- },
71
- },
72
- ])
115
+ > **建议**:`fetchSuggestions` 推荐使用 `useCallback` 包裹,避免在父组件重复渲染时创建新函数,从而触发不必要的副作用或额外请求。
116
+
117
+ 更多示例请参考仓库中的 `src/App.tsx` 和 Storybook 故事文件。
118
+
119
+ ---
120
+
121
+ ## Local Development
122
+
123
+ 克隆仓库:
124
+
125
+ ```bash
126
+ git clone https://github.com/aozi6666/Bee-Design.git
127
+ cd Bee-Design
128
+ npm install
73
129
  ```
130
+
131
+ 本地开发预览:
132
+
133
+ ```bash
134
+ npm run dev
135
+ ```
136
+
137
+ 运行 Storybook:
138
+
139
+ ```bash
140
+ npm run storybook
141
+ ```
142
+
143
+ 运行单元测试:
144
+
145
+ ```bash
146
+ npm run test
147
+ # CI 模式(用于 prepublish)
148
+ npm run test:ci
149
+ ```
150
+
151
+ 构建组件库(打包到 `build/` 目录):
152
+
153
+ ```bash
154
+ npm run build
155
+ ```
156
+
157
+ 发布前检查脚本(测试 + lint + build):
158
+
159
+ ```bash
160
+ npm run prepublishOnly
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Scripts
166
+
167
+ - **`npm run dev`**: 使用 Vite 启动开发服务器
168
+ - **`npm run build`**: 构建 TypeScript 和样式到 `build/`
169
+ - **`npm run lint`**: 使用 ESLint 检查 `src` 下的代码
170
+ - **`npm run test` / `npm run test:ci`**: 使用 Jest + Testing Library 运行单元测试
171
+ - **`npm run storybook` / `npm run build-storybook`**: 启动或构建 Storybook 文档站点
172
+
173
+ ---
174
+
175
+ ## Roadmap
176
+
177
+ - [ ] 完善文档站点与在线 Demo
178
+ - [ ] 增加更多表单组件(`Select` / `Checkbox` / `Radio` 等)
179
+ - [ ] 增加 Layout、Modal 等业务常用组件
180
+ - [ ] 打包为更符合行业习惯的 API 形式(`import { Button } from '@aozi6666/bee-design'`)
181
+
182
+ 欢迎在 GitHub 上提 issue 或 PR,一起完善 Bee Design 🐝。
@@ -1,5 +1,5 @@
1
- import type { AutoCompleteProps } from './autoComplete.types';
2
- export type { AutoCompleteProps, DataSourceType } from './autoComplete.types';
1
+ import type { AutoCompleteProps } from "./autoComplete.types";
2
+ export type { AutoCompleteProps, DataSourceType } from "./autoComplete.types";
3
3
  /**
4
4
  * 输入框自动完成功能。当输入值需要自动完成时使用,支持同步和异步两种方式
5
5
  * 支持 Input 组件的所有属性 支持键盘事件选择
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // AutoComplete 组件: 带搜索建议的 Input
3
- import { useState, useEffect, useRef } from 'react';
4
- import Input from '../Input/input';
5
- import useDebounce from '../../hooks/useDebounce';
6
- import useClickOutside from '../../hooks/useClickOutside';
7
- import AutoCompleteDropdown from './autoCompleteDropdown';
3
+ import { useState, useEffect, useRef } from "react";
4
+ import Input from "../Input/input";
5
+ import useDebounce from "../../hooks/useDebounce";
6
+ import useClickOutside from "../../hooks/useClickOutside";
7
+ import AutoCompleteDropdown from "./autoCompleteDropdown";
8
8
  /**
9
9
  * 输入框自动完成功能。当输入值需要自动完成时使用,支持同步和异步两种方式
10
10
  * 支持 Input 组件的所有属性 支持键盘事件选择
@@ -17,53 +17,80 @@ import AutoCompleteDropdown from './autoCompleteDropdown';
17
17
  export const AutoComplete = (props) => {
18
18
  const { fetchSuggestions, onSelect, onChange, value, renderOption, ...restProps } = props;
19
19
  // 输入框当前显示的文本
20
- const [inputValue, setInputValue] = useState(value || '');
21
- // 下拉建议列表的数据源(渲染 `<li>` 就靠它)
20
+ const [inputValue, setInputValue] = useState(value || "");
21
+ // 候选项列表:下拉建议列表的数据源(渲染 `<li>` 就靠它)
22
22
  const [suggestions, setSugestions] = useState([]);
23
- // 异步请求进行中就显示 loading
23
+ // 加载状态:异步请求进行中就显示 loading
24
24
  const [loading, setLoading] = useState(false);
25
25
  // 是否展示下拉(配合 `Transition` 动画)
26
26
  const [showDropdown, setShowDropdown] = useState(false);
27
- // 键盘上下选择时,哪一项高亮(对应 class `is-active`)
27
+ // 高亮索引:键盘上下选择时,哪一项高亮(对应 class `is-active`)
28
28
  const [highlightIndex, setHighlightIndex] = useState(-1);
29
29
  // 两个关键 ref:
30
30
  // 用来区分“用户打字触发搜索” vs “用户选中后把值塞回去(不应该再搜一次)”
31
+ // 用户打字时:true,用户选中:false
31
32
  const triggerSearch = useRef(false);
32
33
  // 挂到最外层 div 上,给 `useClickOutside` 判断“点击是否发生在组件外”
33
34
  const componentRef = useRef(null);
34
35
  // 防抖Hook:把“频繁输入”变成“停顿后再触发一次”
35
36
  const debouncedValue = useDebounce(inputValue, 300);
36
- // 自定义Hook:点击组件外部时关闭下拉
37
+ // 自定义Hook:点击组件外部时,触发自定义的回调
37
38
  /* 在 `document` 上挂一个 `click` 监听
38
39
  * @param componentRef 组件的 ref
39
40
  * @param callback 点击外部时执行的回调
40
41
  */
42
+ // componentRef: 指向 AutoComplete 最外层 DOM
41
43
  useClickOutside(componentRef, () => {
42
44
  setSugestions([]);
43
45
  setShowDropdown(false);
44
46
  });
45
- // 监听 `debouncedValue` 变化
47
+ // 监听 `debouncedValue防抖后的值` 变化
48
+ // fetchSuggestions这个函数本身的引用地址变没变,改变时,监听变化
49
+ // 因为:React在 effect 里用到了这个函数,避免:闭包拿到旧函数
46
50
  useEffect(() => {
47
- if (debouncedValue && triggerSearch.current) {
48
- setSugestions([]);
49
- const results = fetchSuggestions(debouncedValue);
50
- if (results instanceof Promise) {
51
- setLoading(true);
52
- results.then(data => {
53
- setLoading(false);
54
- setSugestions(data);
55
- setShowDropdown(data.length > 0);
56
- });
57
- }
58
- else {
59
- setSugestions(results);
60
- setShowDropdown(results.length > 0);
61
- }
51
+ // 调度函数: 函数 放入微任务队列(执行)
52
+ // 页面更新等逻辑处理完再更新 UI,减少闪烁、时序混乱的问题
53
+ const schedule = (fn) => {
54
+ queueMicrotask(fn);
55
+ };
56
+ /* 停止搜索,关闭下拉框
57
+ * 两种情况:
58
+ * 1. 输入框为空(防抖后的值为空)
59
+ * 2. 用户选中下拉建议某一项后,输入框值会变为选中项的值(不应该再搜一次)
60
+ */
61
+ if (!debouncedValue || !triggerSearch.current) {
62
+ // 调用 调度函数: 把函数放入微任务队列(执行)
63
+ schedule(() => {
64
+ setShowDropdown(false); // 关闭下拉框 展示
65
+ setHighlightIndex(-1); // 重置高亮索引
66
+ });
67
+ return;
68
+ }
69
+ // 搜索执行
70
+ // 1. 拿当前输入内容,获取 建议项
71
+ const results = fetchSuggestions(debouncedValue);
72
+ // 异步返回:
73
+ if (results instanceof Promise) {
74
+ schedule(() => {
75
+ setLoading(true); // 显示“加载中”
76
+ setHighlightIndex(-1); // 不高亮任何项
77
+ });
78
+ // 异步返回: 拿到数据后,更新状态
79
+ results.then((data) => {
80
+ setLoading(false); // 关闭 “加载中”
81
+ setSugestions(data); // 更新建议项列表
82
+ setShowDropdown(data.length > 0); // 有数据,就显示下拉框
83
+ });
62
84
  }
63
85
  else {
64
- setShowDropdown(false);
86
+ // 同步返回: 本地有个数组,直接筛选
87
+ schedule(() => {
88
+ setLoading(false); // 关闭 “加载中”
89
+ setSugestions(results);
90
+ setShowDropdown(results.length > 0);
91
+ setHighlightIndex(-1);
92
+ });
65
93
  }
66
- setHighlightIndex(-1);
67
94
  }, [debouncedValue, fetchSuggestions]);
68
95
  const highlight = (index) => {
69
96
  if (index < 0)
@@ -1,10 +1,10 @@
1
- import type { ReactElement } from 'react';
2
- import type { InputProps } from '../Input/input.types';
1
+ import type { ReactElement } from "react";
2
+ import type { InputProps } from "../Input/input.types";
3
3
  interface DataSourceObject {
4
4
  value: string;
5
5
  }
6
- export type DataSourceType<T = {}> = T & DataSourceObject;
7
- export interface AutoCompleteProps extends Omit<InputProps, 'onSelect' | 'onChange'> {
6
+ export type DataSourceType<T = Record<string, unknown>> = T & DataSourceObject;
7
+ export interface AutoCompleteProps extends Omit<InputProps, "onSelect" | "onChange"> {
8
8
  /**
9
9
  * 返回输入建议的方法,可以拿到当前的输入,然后返回同步的数组或者是异步的 Promise
10
10
  */
@@ -1,4 +1,4 @@
1
1
  import AutoComplete from './autoComplete';
2
2
  export default AutoComplete;
3
- export * from './autoComplete';
4
- export type * from './autoComplete.types';
3
+ export { AutoComplete } from './autoComplete';
4
+ export type { AutoCompleteProps, DataSourceType } from './autoComplete.types';
@@ -1,3 +1,3 @@
1
1
  import AutoComplete from './autoComplete';
2
2
  export default AutoComplete;
3
- export * from './autoComplete';
3
+ export { AutoComplete } from './autoComplete';
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import type { FC, CSSProperties, ReactNode } from 'react';
3
2
  type MenuMode = 'horizontal' | 'vertical';
4
3
  export interface MenuProps {
@@ -14,13 +13,6 @@ export interface MenuProps {
14
13
  defaultOpenSubMenus?: string[];
15
14
  children?: ReactNode;
16
15
  }
17
- interface IMenuContext {
18
- index: string;
19
- onSelect?: (selectedIndex: string) => void;
20
- mode?: MenuMode;
21
- defaultOpenSubMenus?: string[];
22
- }
23
- export declare const MenuContext: React.Context<IMenuContext>;
24
16
  /**
25
17
  * 为网站提供导航功能的菜单。支持横向纵向两种模式,支持下拉菜单。
26
18
  *
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import React, { useState, createContext } from 'react';
2
+ import React, { useState } from 'react';
3
3
  import classNames from 'classnames';
4
- export const MenuContext = createContext({ index: '0' });
4
+ import { MenuContext } from './menuContext';
5
5
  /**
6
6
  * 为网站提供导航功能的菜单。支持横向纵向两种模式,支持下拉菜单。
7
7
  *
@@ -0,0 +1,9 @@
1
+ type MenuMode = 'horizontal' | 'vertical';
2
+ export interface IMenuContext {
3
+ index: string;
4
+ onSelect?: (selectedIndex: string) => void;
5
+ mode?: MenuMode;
6
+ defaultOpenSubMenus?: string[];
7
+ }
8
+ export declare const MenuContext: import("react").Context<IMenuContext>;
9
+ export {};
@@ -0,0 +1,2 @@
1
+ import { createContext } from 'react';
2
+ export const MenuContext = createContext({ index: '0' });
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { useContext } from 'react';
3
3
  import classNames from 'classnames';
4
- import { MenuContext } from './menu';
4
+ import { MenuContext } from './menuContext';
5
5
  export const MenuItem = (props) => {
6
6
  const { index, disabled, className, style, children } = props;
7
7
  const context = useContext(MenuContext);
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useContext, useState } from 'react';
3
3
  import classNames from 'classnames';
4
- import { MenuContext } from './menu';
4
+ import { MenuContext } from './menuContext';
5
5
  import Icon from '../Icon/icon';
6
6
  import Transition from '../Transition/transition';
7
7
  export const SubMenu = ({ index, title, children, className }) => {
8
8
  const context = useContext(MenuContext);
9
- const openedSubMenus = context.defaultOpenSubMenus;
9
+ const openedSubMenus = (context.defaultOpenSubMenus ?? []);
10
10
  const isOpend = (index && context.mode === 'vertical') ? openedSubMenus.includes(index) : false;
11
11
  const [menuOpen, setOpen] = useState(isOpend);
12
12
  const classes = classNames('menu-item submenu-item', className, {
@@ -20,7 +20,8 @@ export const SubMenu = ({ index, title, children, className }) => {
20
20
  };
21
21
  let timer;
22
22
  const handleMouse = (e, toggle) => {
23
- clearTimeout(timer);
23
+ if (timer)
24
+ clearTimeout(timer);
24
25
  e.preventDefault();
25
26
  timer = setTimeout(() => {
26
27
  setOpen(toggle);
@@ -2,17 +2,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useRef } from 'react';
3
3
  import { CSSTransition } from 'react-transition-group';
4
4
  const Transition = (props) => {
5
- const { children, classNames: classNamesProp, animation, wrapper = false, unmountOnExit = true, appear = true, addEndListener, ...restProps } = props;
5
+ const { children, classNames: classNamesProp, animation, wrapper = false, unmountOnExit = true, appear = true, timeout, ...restProps } = props;
6
6
  const cls = animation ? animation : classNamesProp;
7
7
  const nodeRef = useRef(null);
8
- const transitionProps = {
9
- classNames: cls,
10
- unmountOnExit,
11
- appear,
12
- ...restProps,
13
- };
14
- if (addEndListener)
15
- transitionProps.addEndListener = addEndListener;
16
- return (_jsx(CSSTransition, { nodeRef: nodeRef, ...transitionProps, children: _jsx("div", { ref: nodeRef, "data-transition-wrapper": wrapper ? 'true' : 'false', children: children }) }));
8
+ return (_jsx(CSSTransition, { nodeRef: nodeRef, classNames: cls, unmountOnExit: unmountOnExit, appear: appear, timeout: timeout, ...restProps, children: _jsx("div", { ref: nodeRef, "data-transition-wrapper": wrapper ? 'true' : 'false', children: children }) }));
17
9
  };
18
10
  export default Transition;
@@ -1,10 +1,9 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { CSSTransitionProps } from 'react-transition-group/CSSTransition';
3
3
  export type AnimationName = 'zoom-in-top' | 'zoom-in-left' | 'zoom-in-bottom' | 'zoom-in-right';
4
- export type TransitionProps = Omit<CSSTransitionProps<HTMLElement>, 'addEndListener' | 'timeout'> & {
4
+ export type TransitionProps = Omit<CSSTransitionProps<HTMLElement>, 'timeout'> & {
5
5
  animation?: AnimationName;
6
6
  wrapper?: boolean;
7
7
  children?: ReactNode;
8
- timeout: CSSTransitionProps<HTMLElement>['timeout'];
9
- addEndListener?: (node: HTMLElement, done: () => void) => void;
8
+ timeout: NonNullable<CSSTransitionProps<HTMLElement>['timeout']>;
10
9
  };
@@ -68,7 +68,7 @@ export const Upload = (props) => {
68
68
  // (beforeUpload: “上传前钩子”)
69
69
  const uploadFiles = (files, test) => {
70
70
  // 传来的文件列表 FileList类型,不是数组 =》 转为数组
71
- let postFiles = Array.from(files);
71
+ const postFiles = Array.from(files);
72
72
  //
73
73
  if (test) {
74
74
  console.log('drag', postFiles[0]);
@@ -103,7 +103,7 @@ export const Upload = (props) => {
103
103
  // 函数: 发axios请求
104
104
  const post = (file) => {
105
105
  // 改造浏览器原生File,创建 内部文件对象 `_file` (文件本体 + 上传状态)
106
- let _file = {
106
+ const _file = {
107
107
  uid: Date.now() + 'upload-file',
108
108
  status: 'ready',
109
109
  name: file.name,
@@ -6,8 +6,8 @@ export interface UploadFile {
6
6
  status?: UploadFileStatus;
7
7
  percent: number;
8
8
  raw?: File;
9
- response?: any;
10
- error?: any;
9
+ response?: unknown;
10
+ error?: unknown;
11
11
  }
12
12
  export interface UploadProps {
13
13
  /** 必选参数, 上传的地址 */
@@ -19,23 +19,19 @@ export interface UploadProps {
19
19
  /** 文件上传时的钩子 */
20
20
  onProgress?: (percentage: number, file: UploadFile) => void;
21
21
  /** 文件上传成功时的钩子 */
22
- onSuccess?: (data: any, file: UploadFile) => void;
22
+ onSuccess?: (data: unknown, file: UploadFile) => void;
23
23
  /** 文件上传失败时的钩子 */
24
- onError?: (err: any, file: UploadFile) => void;
24
+ onError?: (err: unknown, file: UploadFile) => void;
25
25
  /** 文件状态改变时的钩子,上传成功或者失败时都会被调用 */
26
26
  onChange?: (file: UploadFile) => void;
27
27
  /** 文件列表移除文件时的钩子 */
28
28
  onRemove?: (file: UploadFile) => void;
29
29
  /** 设置上传的请求头部 */
30
- headers?: {
31
- [key: string]: any;
32
- };
30
+ headers?: Record<string, string>;
33
31
  /** 上传的文件字段名 */
34
32
  name?: string;
35
33
  /** 上传时附带的额外参数 */
36
- data?: {
37
- [key: string]: any;
38
- };
34
+ data?: Record<string, string>;
39
35
  /** 是否发送 cookie 凭证信息 */
40
36
  withCredentials?: boolean;
41
37
  /** 可选参数, 接受上传的文件类型 */
@@ -1,3 +1,3 @@
1
- import type { RefObject } from 'react';
2
- declare function useClickOutside<T extends HTMLElement>(ref: RefObject<T | null>, handler: (event: MouseEvent) => void): void;
1
+ import type { RefObject } from "react";
2
+ declare function useClickOutside<T extends HTMLElement>(ref: RefObject<T | null>, handler: (event: MouseEvent) => void, eventType?: "click" | "mousedown"): void;
3
3
  export default useClickOutside;
@@ -1,18 +1,38 @@
1
- import { useEffect } from 'react';
2
- function useClickOutside(ref, handler) {
1
+ // “点击某个区域外面时,执行一些事情。”
2
+ /*监听整个页面点击,
3
+ -如果点击目标不在 ref 指向的 DOM 里,就执行回调。
4
+ */
5
+ import { useEffect, useRef } from "react";
6
+ function useClickOutside(ref, handler, eventType = "click") {
7
+ const handlerRef = useRef(handler);
3
8
  useEffect(() => {
9
+ handlerRef.current = handler;
10
+ }, [handler]);
11
+ useEffect(() => {
12
+ // 监听 document 的 click 事件
13
+ // 点击回调:页面每次被点击时,要执行的函数
4
14
  const listener = (event) => {
15
+ // 拿到真实 边界DOM
5
16
  const el = ref?.current;
17
+ // 空值保护: 当前拿不到 DOM,直接return
6
18
  if (!el)
7
19
  return;
20
+ // 点击发生在组件内部, 直接return
21
+ /*
22
+ event.target: 实际点到的元素
23
+ el.contains:DOM 原生 API,判断 el元素是否包含某个节点
24
+ */
25
+ // 复杂场景下如果下拉框通过 portal 渲染到 body 等其他 DOM 树中,单纯依赖 contains 判断可能不够,需要更通用的命中判断策略
8
26
  if (el.contains(event.target))
9
27
  return;
10
- handler(event);
28
+ handlerRef.current(event); // 执行回调函数
11
29
  };
12
- document.addEventListener('click', listener);
30
+ // 监听挂在 document整个页面上
31
+ document.addEventListener(eventType, listener);
32
+ // 清理函数: 卸载组件时,移除监听(防止内存泄漏/重复监听)
13
33
  return () => {
14
- document.removeEventListener('click', listener);
34
+ document.removeEventListener(eventType, listener);
15
35
  };
16
- }, [ref, handler]);
36
+ }, [ref, eventType]);
17
37
  }
18
38
  export default useClickOutside;
@@ -1,2 +1,2 @@
1
- declare function useDebounce(value: any, delay?: number): any;
1
+ declare function useDebounce<T>(value: T, delay?: number): T;
2
2
  export default useDebounce;
@@ -1,10 +1,24 @@
1
- import { useState, useEffect } from 'react';
1
+ /*
2
+ 防抖(debounce)Hook:
3
+ -频繁触发的操作 → 只在最后一次停顿后执行
4
+ - 只要输入还在继续,就一直取消定时器;
5
+ - 只有用户停下来 delay 毫秒,才把值更新出去。
6
+
7
+ 若后续常用于对象 / 数组:
8
+ 可以配合 useMemo 或 useCallback 使用,减少下游无谓重渲
9
+ */
10
+ import { useState, useEffect } from "react";
2
11
  function useDebounce(value, delay = 300) {
12
+ // 初始值为 value
3
13
  const [debouncedValue, setDebouncedValue] = useState(value);
14
+ // 当 value值与 delay改变时,执行
4
15
  useEffect(() => {
16
+ // 启动一个定时器,300ms后执行
5
17
  const handler = window.setTimeout(() => {
6
18
  setDebouncedValue(value);
7
19
  }, delay);
20
+ // 清理函数:value 又变了,就取消上一次定时器
21
+ // 只要一直触发,就一直取消;直到停下来,才执行最后一次。
8
22
  return () => {
9
23
  clearTimeout(handler);
10
24
  };
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "@aozi6666/bee-design",
3
3
  "private": false,
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/aozi6666/Bee-Design.git"
8
+ },
9
+ "homepage": "https://github.com/aozi6666/Bee-Design",
10
+ "bugs": {
11
+ "url": "https://github.com/aozi6666/Bee-Design/issues"
12
+ },
5
13
  "type": "module",
6
14
  "files": [
7
15
  "build",
@@ -12,37 +20,54 @@
12
20
  "types": "build/index.d.ts",
13
21
  "scripts": {
14
22
  "dev": "vite",
15
- "clean":"rimraf ./build",
23
+ "clean": "rimraf ./build",
24
+ "lint": "eslint --ext js,ts,tsx src --max-warnings 5",
25
+ "lint:fix": "npm run lint -- --fix",
26
+ "stylelint": "stylelint \"src/**/*.{css,scss}\"",
27
+ "stylelint:fix": "npm run stylelint -- --fix",
28
+ "format": "prettier --write .",
29
+ "format:check": "prettier --check .",
30
+ "lint:staged": "lint-staged",
16
31
  "build": "npm run clean && npm run build-ts && npm run build-css",
17
- "build:site": "vite build",
18
32
  "build-ts": "tsc -p tsconfig.build.json",
19
33
  "build-css": "sass ./src/styles/index.scss ./build/index.css",
20
34
  "webpack:dev": "webpack",
21
35
  "webpack:build": "NODE_ENV=production webpack",
22
- "lint": "eslint .",
36
+ "storybook": "storybook dev -p 6006",
37
+ "build-storybook": "storybook build",
38
+ "build:site": "vite build",
23
39
  "preview": "vite preview",
24
40
  "test": "jest",
25
- "storybook": "storybook dev -p 6006",
26
- "build-storybook": "storybook build"
41
+ "test:ci": "cross-env CI=true jest",
42
+ "prepublishOnly": "npm run test:ci && npm run lint && npm run build",
43
+ "prepare": "husky"
44
+ },
45
+ "lint-staged": {
46
+ "*.{js,jsx,ts,tsx}": [
47
+ "eslint --fix",
48
+ "prettier --write"
49
+ ],
50
+ "*.{css,scss}": [
51
+ "stylelint --fix",
52
+ "prettier --write"
53
+ ],
54
+ "*.{json,md,mdx,yml,yaml}": [
55
+ "prettier --write"
56
+ ]
57
+ },
58
+ "peerDependencies": {
59
+ "react": "^18 || ^19",
60
+ "react-dom": "^18 || ^19"
27
61
  },
28
62
  "dependencies": {
29
63
  "@fortawesome/fontawesome-svg-core": "^6.7.2",
30
64
  "@fortawesome/free-solid-svg-icons": "^6.7.2",
31
65
  "@fortawesome/react-fontawesome": "^3.2.0",
32
- "@types/classnames": "^2.3.0",
33
- "@types/jest": "^30.0.0",
34
- "@types/react-transition-group": "^4.4.12",
35
66
  "axios": "^1.13.6",
36
67
  "classnames": "^2.5.1",
37
- "css-loader": "^7.1.4",
38
- "html-webpack-plugin": "^5.6.6",
68
+ "cross-env": "^10.1.0",
39
69
  "lodash": "^4.17.23",
40
- "react": "^19.2.0",
41
- "react-dom": "^19.2.0",
42
- "react-transition-group": "^4.4.5",
43
- "rimraf": "^6.1.3",
44
- "style-loader": "^4.0.0",
45
- "webpack-dev-server": "^5.2.3"
70
+ "react-transition-group": "^4.4.5"
46
71
  },
47
72
  "devDependencies": {
48
73
  "@chromatic-com/storybook": "^5.0.1",
@@ -56,24 +81,41 @@
56
81
  "@testing-library/jest-dom": "^6.9.1",
57
82
  "@testing-library/react": "^16.3.2",
58
83
  "@testing-library/user-event": "^14.6.1",
84
+ "@types/classnames": "^2.3.0",
85
+ "@types/jest": "^30.0.0",
59
86
  "@types/node": "^24.10.1",
60
87
  "@types/react": "^19.2.7",
61
88
  "@types/react-dom": "^19.2.3",
89
+ "@types/react-transition-group": "^4.4.12",
62
90
  "@vitejs/plugin-react": "^5.1.1",
63
91
  "@vitest/browser-playwright": "^4.0.18",
64
92
  "@vitest/coverage-v8": "^4.0.18",
93
+ "css-loader": "^7.1.4",
65
94
  "eslint": "^9.39.1",
66
95
  "eslint-plugin-react-hooks": "^7.0.1",
67
96
  "eslint-plugin-react-refresh": "^0.4.24",
68
97
  "eslint-plugin-storybook": "^10.2.17",
69
98
  "globals": "^16.5.0",
99
+ "html-webpack-plugin": "^5.6.6",
100
+ "husky": "^9.1.7",
70
101
  "jest": "^30.3.0",
71
102
  "jest-environment-jsdom": "^30.3.0",
72
103
  "playwright": "^1.58.2",
104
+ "react": "^19.2.0",
105
+ "react-dom": "^19.2.0",
106
+ "rimraf": "^6.1.3",
73
107
  "sass": "^1.97.3",
74
108
  "sass-embedded": "^1.97.3",
75
109
  "storybook": "^10.2.17",
110
+ "style-loader": "^4.0.0",
111
+ "stylelint": "^17.4.0",
112
+ "stylelint-config-standard": "^40.0.0",
113
+ "stylelint-config-standard-scss": "^16.0.0",
114
+ "stylelint-scss": "^7.0.0",
76
115
  "terser-webpack-plugin": "^5.4.0",
116
+ "postcss-scss": "^4.0.9",
117
+ "prettier": "3.8.1",
118
+ "lint-staged": "^16.4.0",
77
119
  "ts-jest": "^29.4.6",
78
120
  "ts-loader": "^9.5.4",
79
121
  "typescript": "~5.9.3",
@@ -81,7 +123,8 @@
81
123
  "vite": "^7.3.1",
82
124
  "vitest": "^4.0.18",
83
125
  "webpack": "^5.105.4",
84
- "webpack-cli": "^7.0.0"
126
+ "webpack-cli": "^7.0.0",
127
+ "webpack-dev-server": "^5.2.3"
85
128
  },
86
129
  "jest": {
87
130
  "preset": "ts-jest/presets/default-esm",