@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 +169 -60
- package/build/components/AutoComplete/autoComplete.d.ts +2 -2
- package/build/components/AutoComplete/autoComplete.js +55 -28
- package/build/components/AutoComplete/autoComplete.types.d.ts +4 -4
- package/build/components/AutoComplete/index.d.ts +2 -2
- package/build/components/AutoComplete/index.js +1 -1
- package/build/components/Menu/menu.d.ts +0 -8
- package/build/components/Menu/menu.js +2 -2
- package/build/components/Menu/menuContext.d.ts +9 -0
- package/build/components/Menu/menuContext.js +2 -0
- package/build/components/Menu/menuItem.js +1 -1
- package/build/components/Menu/subMenu.js +4 -3
- package/build/components/Transition/transition.js +2 -10
- package/build/components/Transition/transition.types.d.ts +2 -3
- package/build/components/Upload/upload.js +2 -2
- package/build/components/Upload/upload.types.d.ts +6 -10
- package/build/hooks/useClickOutside.d.ts +2 -2
- package/build/hooks/useClickOutside.js +26 -6
- package/build/hooks/useDebounce.d.ts +1 -1
- package/build/hooks/useDebounce.js +15 -1
- package/package.json +61 -18
package/README.md
CHANGED
|
@@ -1,73 +1,182 @@
|
|
|
1
|
-
|
|
1
|
+
## Bee Design
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React UI Component Library inspired by Honeycomb 🐝
|
|
4
|
+
一个基于 React + TypeScript 的轻量级组件库,用来练习和演示现代前端工程化(组件开发、单元测试、Storybook、CI 等)实践。
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
### GitHub
|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
|
|
10
|
+
### Install
|
|
11
11
|
|
|
12
|
-
|
|
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
|
-
|
|
50
|
+
Bee Design 当前提供以下组件(持续扩展中):
|
|
15
51
|
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
2
|
-
export type { AutoCompleteProps, DataSourceType } from
|
|
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
|
|
4
|
-
import Input from
|
|
5
|
-
import useDebounce from
|
|
6
|
-
import useClickOutside from
|
|
7
|
-
import AutoCompleteDropdown from
|
|
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
|
-
//
|
|
20
|
+
const [inputValue, setInputValue] = useState(value || "");
|
|
21
|
+
// 候选项列表:下拉建议列表的数据源(渲染 `<li>` 就靠它)
|
|
22
22
|
const [suggestions, setSugestions] = useState([]);
|
|
23
|
-
//
|
|
23
|
+
// 加载状态:异步请求进行中就显示 loading
|
|
24
24
|
const [loading, setLoading] = useState(false);
|
|
25
25
|
// 是否展示下拉(配合 `Transition` 动画)
|
|
26
26
|
const [showDropdown, setShowDropdown] = useState(false);
|
|
27
|
-
//
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
setShowDropdown(
|
|
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
|
-
|
|
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
|
|
2
|
-
import type { InputProps } from
|
|
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 =
|
|
7
|
-
export interface AutoCompleteProps extends Omit<InputProps,
|
|
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
|
|
4
|
-
export type
|
|
3
|
+
export { AutoComplete } from './autoComplete';
|
|
4
|
+
export type { AutoCompleteProps, DataSourceType } from './autoComplete.types';
|
|
@@ -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
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
|
-
|
|
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 {};
|
|
@@ -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 './
|
|
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 './
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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>, '
|
|
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
|
-
|
|
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
|
-
|
|
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?:
|
|
10
|
-
error?:
|
|
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:
|
|
22
|
+
onSuccess?: (data: unknown, file: UploadFile) => void;
|
|
23
23
|
/** 文件上传失败时的钩子 */
|
|
24
|
-
onError?: (err:
|
|
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
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
28
|
+
handlerRef.current(event); // 执行回调函数
|
|
11
29
|
};
|
|
12
|
-
document
|
|
30
|
+
// 监听挂在 document整个页面上
|
|
31
|
+
document.addEventListener(eventType, listener);
|
|
32
|
+
// 清理函数: 卸载组件时,移除监听(防止内存泄漏/重复监听)
|
|
13
33
|
return () => {
|
|
14
|
-
document.removeEventListener(
|
|
34
|
+
document.removeEventListener(eventType, listener);
|
|
15
35
|
};
|
|
16
|
-
}, [ref,
|
|
36
|
+
}, [ref, eventType]);
|
|
17
37
|
}
|
|
18
38
|
export default useClickOutside;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare function useDebounce(value:
|
|
1
|
+
declare function useDebounce<T>(value: T, delay?: number): T;
|
|
2
2
|
export default useDebounce;
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
26
|
-
"
|
|
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
|
-
"
|
|
38
|
-
"html-webpack-plugin": "^5.6.6",
|
|
68
|
+
"cross-env": "^10.1.0",
|
|
39
69
|
"lodash": "^4.17.23",
|
|
40
|
-
"react": "^
|
|
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",
|