@aozi6666/bee-design 0.1.2 → 0.2.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/dist/App.d.ts +4 -0
- package/{build → dist}/components/AutoComplete/autoCompleteDropdown.d.ts +2 -2
- package/dist/components/AutoComplete/index.d.ts +4 -0
- package/{build → dist}/components/Button/button.d.ts +2 -2
- package/{build → dist}/components/Button/button.types.d.ts +1 -1
- package/dist/components/Button/index.d.ts +2 -0
- package/{build → dist}/components/Icon/icon.d.ts +2 -2
- package/dist/components/Icon/icon.types.d.ts +6 -0
- package/dist/components/Icon/index.d.ts +2 -0
- package/dist/components/Input/index.d.ts +4 -0
- package/dist/components/Input/input.d.ts +5 -0
- package/{build → dist}/components/Input/input.types.d.ts +4 -4
- package/{build → dist}/components/Menu/index.d.ts +4 -4
- package/{build → dist}/components/Menu/menu.d.ts +2 -2
- package/{build → dist}/components/Menu/menuContext.d.ts +1 -1
- package/{build → dist}/components/Menu/menuItem.d.ts +2 -2
- package/{build → dist}/components/Menu/subMenu.d.ts +1 -1
- package/dist/components/Progress/index.d.ts +2 -0
- package/dist/components/Progress/progress.d.ts +4 -0
- package/{build → dist}/components/Progress/progress.types.d.ts +2 -2
- package/dist/components/Transition/index.d.ts +3 -0
- package/dist/components/Transition/transition.d.ts +4 -0
- package/dist/components/Transition/transition.types.d.ts +9 -0
- package/{build → dist}/components/Upload/dragger.d.ts +1 -1
- package/dist/components/Upload/index.d.ts +2 -0
- package/{build → dist}/components/Upload/upload.d.ts +2 -2
- package/{build → dist}/components/Upload/upload.types.d.ts +1 -1
- package/{build → dist}/components/Upload/uploadList.d.ts +2 -2
- package/dist/index.cjs +16272 -0
- package/dist/index.cjs.map +1 -0
- package/{build → dist}/index.css +24 -24
- package/dist/index.d.ts +9 -0
- package/dist/index.esm.js +709 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.umd.js +14381 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/main.d.ts +1 -0
- package/dist/setupIcons.d.ts +1 -0
- package/dist/setupTests.d.ts +1 -0
- package/package.json +52 -82
- package/README.md +0 -182
- package/build/App.d.ts +0 -4
- package/build/App.js +0 -137
- package/build/components/AutoComplete/autoComplete.js +0 -150
- package/build/components/AutoComplete/autoComplete.types.js +0 -1
- package/build/components/AutoComplete/autoCompleteDropdown.js +0 -17
- package/build/components/AutoComplete/index.d.ts +0 -4
- package/build/components/AutoComplete/index.js +0 -3
- package/build/components/Button/button.js +0 -43
- package/build/components/Button/button.types.js +0 -19
- package/build/components/Button/index.d.ts +0 -2
- package/build/components/Button/index.js +0 -2
- package/build/components/Icon/icon.js +0 -24
- package/build/components/Icon/icon.types.d.ts +0 -6
- package/build/components/Icon/icon.types.js +0 -2
- package/build/components/Icon/index.d.ts +0 -2
- package/build/components/Icon/index.js +0 -2
- package/build/components/Input/index.d.ts +0 -4
- package/build/components/Input/index.js +0 -3
- package/build/components/Input/input.d.ts +0 -5
- package/build/components/Input/input.js +0 -32
- package/build/components/Input/input.types.js +0 -1
- package/build/components/Menu/index.js +0 -9
- package/build/components/Menu/menu.js +0 -48
- package/build/components/Menu/menuContext.js +0 -2
- package/build/components/Menu/menuItem.js +0 -20
- package/build/components/Menu/subMenu.js +0 -57
- package/build/components/Progress/index.d.ts +0 -2
- package/build/components/Progress/index.js +0 -2
- package/build/components/Progress/progress.d.ts +0 -4
- package/build/components/Progress/progress.js +0 -6
- package/build/components/Progress/progress.types.js +0 -2
- package/build/components/Transition/index.d.ts +0 -3
- package/build/components/Transition/index.js +0 -2
- package/build/components/Transition/transition.d.ts +0 -4
- package/build/components/Transition/transition.js +0 -10
- package/build/components/Transition/transition.types.d.ts +0 -9
- package/build/components/Transition/transition.types.js +0 -1
- package/build/components/Upload/dragger.js +0 -42
- package/build/components/Upload/index.d.ts +0 -2
- package/build/components/Upload/index.js +0 -2
- package/build/components/Upload/native/axios-react.js +0 -99
- package/build/components/Upload/native/from-html.js +0 -5
- package/build/components/Upload/upload.js +0 -192
- package/build/components/Upload/upload.types.js +0 -3
- package/build/components/Upload/uploadList.js +0 -13
- package/build/hooks/useClickOutside.js +0 -38
- package/build/hooks/useDebounce.js +0 -28
- package/build/index.css.map +0 -1
- package/build/index.d.ts +0 -9
- package/build/index.js +0 -12
- package/build/main.d.ts +0 -1
- package/build/main.js +0 -7
- package/build/setupTests.d.ts +0 -1
- package/build/setupTests.js +0 -1
- /package/{build → dist}/components/AutoComplete/autoComplete.d.ts +0 -0
- /package/{build → dist}/components/AutoComplete/autoComplete.types.d.ts +0 -0
- /package/{build → dist}/components/Upload/native/axios-react.d.ts +0 -0
- /package/{build → dist}/components/Upload/native/from-html.d.ts +0 -0
- /package/{build → dist}/hooks/useClickOutside.d.ts +0 -0
- /package/{build → dist}/hooks/useDebounce.d.ts +0 -0
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import React, { createContext, useState, useRef, useContext, useMemo, useEffect } from 'react';
|
|
4
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
5
|
+
import { library } from '@fortawesome/fontawesome-svg-core';
|
|
6
|
+
import { fas } from '@fortawesome/free-solid-svg-icons';
|
|
7
|
+
import { CSSTransition } from 'react-transition-group';
|
|
8
|
+
import { clamp } from '@aozi6666/bee-utils';
|
|
9
|
+
import axios from 'axios';
|
|
10
|
+
|
|
11
|
+
/* 定义 Button 组件可以接收哪些 props(组件 API) */
|
|
12
|
+
// TS 导出 两个固定值 'lg' / 'sm'
|
|
13
|
+
/*
|
|
14
|
+
as const: 把 ButtonSize对象的值 变成不可修改的 字面量类型
|
|
15
|
+
- 如果没有 as const: Large: string
|
|
16
|
+
- 如果有 as const: Large: "lg"
|
|
17
|
+
|
|
18
|
+
不写 as const: Large: 'lg' 会被 TS 自动推导为 string 类型
|
|
19
|
+
*/
|
|
20
|
+
const ButtonType = {
|
|
21
|
+
Default: "default",
|
|
22
|
+
Link: "link",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const Button = ({ className, disabled = false, size, btnType = ButtonType.Default, children, href, ...restProps }) => {
|
|
26
|
+
// btn, btn-lg, btn-primary
|
|
27
|
+
// ① classNames 生成 class
|
|
28
|
+
/*
|
|
29
|
+
用户写:<Button btnType="primary" size="lg">
|
|
30
|
+
组件算出 class:btn btn-primary btn-lg
|
|
31
|
+
*/
|
|
32
|
+
// 返回 字符串
|
|
33
|
+
const classes = classNames("btn", className, {
|
|
34
|
+
/*
|
|
35
|
+
外部传来 <Button btnType={ButtonType.Primary}>
|
|
36
|
+
-> 变成 { "btn-primary": true }
|
|
37
|
+
-> classNames 会加上: btn-primary
|
|
38
|
+
-> 最终变为 : btn btn-primary
|
|
39
|
+
*/
|
|
40
|
+
[`btn-${btnType}`]: btnType,
|
|
41
|
+
[`btn-${size}`]: size,
|
|
42
|
+
// 是 link 按钮 && disabled === true
|
|
43
|
+
// classNames 会加上: disabled
|
|
44
|
+
disabled: btnType === ButtonType.Link && disabled,
|
|
45
|
+
});
|
|
46
|
+
// ② 根据类型决定渲染:
|
|
47
|
+
/*
|
|
48
|
+
用户写:<Button btnType="link" href="https://xxx">
|
|
49
|
+
组件算出: <a class="btn btn-link">...</a>
|
|
50
|
+
*/
|
|
51
|
+
if (btnType === ButtonType.Link && href) {
|
|
52
|
+
return (jsx("a", { className: classes, href: href, ...restProps, children: children }));
|
|
53
|
+
}
|
|
54
|
+
/*
|
|
55
|
+
用户写:<Button btnType="primary">
|
|
56
|
+
组件算出: <button class="btn btn-primary">
|
|
57
|
+
*/
|
|
58
|
+
return (jsx("button", { className: classes, disabled: disabled, ...restProps, children: children }));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const MenuContext = createContext({ index: "0" });
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 为网站提供导航功能的菜单。支持横向纵向两种模式,支持下拉菜单。
|
|
65
|
+
*
|
|
66
|
+
* ```javascript
|
|
67
|
+
* import { Menu } from 'vikingship'
|
|
68
|
+
*
|
|
69
|
+
* //然后可以使用 Menu.Item 和 Menu.Submenu 访问选项和子下拉菜单组件
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
const Menu = ({ className, mode = "horizontal", style, children, defaultIndex = "0", onSelect, defaultOpenSubMenus = [], }) => {
|
|
73
|
+
const [currentActive, setActive] = useState(defaultIndex);
|
|
74
|
+
const classes = classNames("viking-menu", className, {
|
|
75
|
+
"menu-vertical": mode === "vertical",
|
|
76
|
+
"menu-horizontal": mode !== "vertical",
|
|
77
|
+
});
|
|
78
|
+
const handleClick = (index) => {
|
|
79
|
+
setActive(index);
|
|
80
|
+
if (onSelect) {
|
|
81
|
+
onSelect(index);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const passedContext = {
|
|
85
|
+
index: currentActive ? currentActive : "0",
|
|
86
|
+
onSelect: handleClick,
|
|
87
|
+
mode,
|
|
88
|
+
defaultOpenSubMenus,
|
|
89
|
+
};
|
|
90
|
+
const renderChildren = () => {
|
|
91
|
+
return React.Children.map(children, (child, index) => {
|
|
92
|
+
const childElement = child;
|
|
93
|
+
const { displayName } = childElement.type;
|
|
94
|
+
if (displayName === "MenuItem" || displayName === "SubMenu") {
|
|
95
|
+
return React.cloneElement(childElement, {
|
|
96
|
+
index: index.toString(),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.error("Warning: Menu has a child which is not a MenuItem component");
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
return (jsx("ul", { className: classes, style: style, "data-testid": "test-menu", children: jsx(MenuContext.Provider, { value: passedContext, children: renderChildren() }) }));
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
let didSetup = false;
|
|
108
|
+
function setupIcons() {
|
|
109
|
+
if (didSetup)
|
|
110
|
+
return;
|
|
111
|
+
library.add(fas);
|
|
112
|
+
didSetup = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 提供了一套常用的图标集合 基于 react-fontawesome。
|
|
117
|
+
*
|
|
118
|
+
* 支持 react-fontawesome的所有属性 可以在这里查询 https://github.com/FortAwesome/react-fontawesome#basic
|
|
119
|
+
*
|
|
120
|
+
* 支持 fontawesome 所有 free-solid-icons,可以在这里查看所有图标 https://fontawesome.com/icons?d=gallery&s=solid&m=free
|
|
121
|
+
* ### 引用方法
|
|
122
|
+
*
|
|
123
|
+
* ~~~js
|
|
124
|
+
* import { Icon } from 'vikingship'
|
|
125
|
+
* ~~~
|
|
126
|
+
*/
|
|
127
|
+
const Icon = (props) => {
|
|
128
|
+
setupIcons();
|
|
129
|
+
// icon-primary
|
|
130
|
+
const { className, theme, ...restProps } = props;
|
|
131
|
+
const classes = classNames("viking-icon", className, {
|
|
132
|
+
[`icon-${theme}`]: theme,
|
|
133
|
+
});
|
|
134
|
+
return jsx(FontAwesomeIcon, { className: classes, ...restProps });
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const Transition = (props) => {
|
|
138
|
+
const { children, classNames: classNamesProp, animation, wrapper = false, unmountOnExit = true, appear = true, timeout, ...restProps } = props;
|
|
139
|
+
const cls = animation ? animation : classNamesProp;
|
|
140
|
+
const nodeRef = useRef(null);
|
|
141
|
+
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 }) }));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const SubMenu = ({ index, title, children, className }) => {
|
|
145
|
+
const context = useContext(MenuContext);
|
|
146
|
+
const openedSubMenus = (context.defaultOpenSubMenus ?? []);
|
|
147
|
+
const isOpend = index && context.mode === "vertical" ? openedSubMenus.includes(index) : false;
|
|
148
|
+
const [menuOpen, setOpen] = useState(isOpend);
|
|
149
|
+
const classes = classNames("menu-item submenu-item", className, {
|
|
150
|
+
"is-active": context.index === index,
|
|
151
|
+
"is-opened": menuOpen,
|
|
152
|
+
"is-vertical": context.mode === "vertical",
|
|
153
|
+
});
|
|
154
|
+
const handleClick = (e) => {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
setOpen(!menuOpen);
|
|
157
|
+
};
|
|
158
|
+
let timer;
|
|
159
|
+
const handleMouse = (e, toggle) => {
|
|
160
|
+
if (timer)
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
timer = setTimeout(() => {
|
|
164
|
+
setOpen(toggle);
|
|
165
|
+
}, 300);
|
|
166
|
+
};
|
|
167
|
+
const clickEvents = context.mode === "vertical"
|
|
168
|
+
? {
|
|
169
|
+
onClick: handleClick,
|
|
170
|
+
}
|
|
171
|
+
: {};
|
|
172
|
+
const hoverEvents = context.mode !== "vertical"
|
|
173
|
+
? {
|
|
174
|
+
onMouseEnter: (e) => {
|
|
175
|
+
handleMouse(e, true);
|
|
176
|
+
},
|
|
177
|
+
onMouseLeave: (e) => {
|
|
178
|
+
handleMouse(e, false);
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
: {};
|
|
182
|
+
const renderChildren = () => {
|
|
183
|
+
const subMenuClasses = classNames("viking-submenu", {
|
|
184
|
+
"menu-opened": menuOpen,
|
|
185
|
+
});
|
|
186
|
+
const childrenComponent = React.Children.map(children, (child, i) => {
|
|
187
|
+
const childElement = child;
|
|
188
|
+
if (childElement.type.displayName === "MenuItem") {
|
|
189
|
+
return React.cloneElement(childElement, {
|
|
190
|
+
index: `${index}-${i}`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.error("Warning: SubMenu has a child which is not a MenuItem component");
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return (jsx(Transition, { in: menuOpen, timeout: 300, animation: "zoom-in-top", children: jsx("ul", { className: subMenuClasses, children: childrenComponent }) }));
|
|
198
|
+
};
|
|
199
|
+
return (jsxs("li", { className: classes, ...hoverEvents, children: [jsxs("div", { className: "submenu-title", ...clickEvents, children: [title, jsx(Icon, { icon: "angle-down", className: "arrow-icon" })] }), renderChildren()] }, index));
|
|
200
|
+
};
|
|
201
|
+
SubMenu.displayName = "SubMenu";
|
|
202
|
+
|
|
203
|
+
const MenuItem = (props) => {
|
|
204
|
+
const { index, disabled, className, style, children } = props;
|
|
205
|
+
const context = useContext(MenuContext);
|
|
206
|
+
const classes = classNames("menu-item", className, {
|
|
207
|
+
"is-disabled": disabled,
|
|
208
|
+
"is-active": context.index === index,
|
|
209
|
+
});
|
|
210
|
+
const handleClick = () => {
|
|
211
|
+
if (context.onSelect && !disabled && typeof index === "string") {
|
|
212
|
+
context.onSelect(index);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
return (jsx("li", { className: classes, style: style, onClick: handleClick, children: children }));
|
|
216
|
+
};
|
|
217
|
+
MenuItem.displayName = "MenuItem";
|
|
218
|
+
|
|
219
|
+
// Menu 做类型转换
|
|
220
|
+
const TransMenu = Menu;
|
|
221
|
+
// 把子组件挂到 Menu 上
|
|
222
|
+
TransMenu.Item = MenuItem;
|
|
223
|
+
TransMenu.SubMenu = SubMenu;
|
|
224
|
+
|
|
225
|
+
const Input = (props) => {
|
|
226
|
+
const { disabled, size, icon, prepend, append, style, onChange, ...restProps } = props;
|
|
227
|
+
const classes = useMemo(() => classNames("viking-input-wrapper", {
|
|
228
|
+
[`input-size-${size}`]: size,
|
|
229
|
+
"input-group": prepend || append,
|
|
230
|
+
"input-group-prepend": !!prepend,
|
|
231
|
+
"input-group-append": !!append,
|
|
232
|
+
}), [append, prepend, size]);
|
|
233
|
+
const inputClasses = classNames("viking-input-inner", {
|
|
234
|
+
"is-disabled": disabled,
|
|
235
|
+
});
|
|
236
|
+
const handleChange = (e) => {
|
|
237
|
+
if (onChange)
|
|
238
|
+
onChange(e);
|
|
239
|
+
};
|
|
240
|
+
const renderPrepend = () => {
|
|
241
|
+
if (!prepend)
|
|
242
|
+
return null;
|
|
243
|
+
return jsx("div", { className: "viking-input-group-prepend", children: prepend });
|
|
244
|
+
};
|
|
245
|
+
const renderAppend = () => {
|
|
246
|
+
if (!append && !icon)
|
|
247
|
+
return null;
|
|
248
|
+
return (jsxs("div", { className: "viking-input-group-append", children: [append, icon ? (jsx("div", { className: "icon-wrapper", children: jsx(Icon, { icon: icon }) })) : null] }));
|
|
249
|
+
};
|
|
250
|
+
return (jsxs("div", { className: classes, style: style, children: [renderPrepend(), jsx("input", { className: inputClasses, disabled: disabled, onChange: handleChange, ...restProps }), renderAppend()] }));
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/*
|
|
254
|
+
防抖(debounce)Hook:
|
|
255
|
+
-频繁触发的操作 → 只在最后一次停顿后执行
|
|
256
|
+
- 只要输入还在继续,就一直取消定时器;
|
|
257
|
+
- 只有用户停下来 delay 毫秒,才把值更新出去。
|
|
258
|
+
|
|
259
|
+
若后续常用于对象 / 数组:
|
|
260
|
+
可以配合 useMemo 或 useCallback 使用,减少下游无谓重渲
|
|
261
|
+
*/
|
|
262
|
+
function useDebounce(value, delay = 300) {
|
|
263
|
+
// 初始值为 value
|
|
264
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
265
|
+
// 当 value值与 delay改变时,执行
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
// 启动一个定时器,300ms后执行
|
|
268
|
+
const handler = window.setTimeout(() => {
|
|
269
|
+
setDebouncedValue(value);
|
|
270
|
+
}, delay);
|
|
271
|
+
// 清理函数:value 又变了,就取消上一次定时器
|
|
272
|
+
// 只要一直触发,就一直取消;直到停下来,才执行最后一次。
|
|
273
|
+
return () => {
|
|
274
|
+
clearTimeout(handler);
|
|
275
|
+
};
|
|
276
|
+
}, [value, delay]);
|
|
277
|
+
return debouncedValue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// “点击某个区域外面时,执行一些事情。”
|
|
281
|
+
/*监听整个页面点击,
|
|
282
|
+
-如果点击目标不在 ref 指向的 DOM 里,就执行回调。
|
|
283
|
+
*/
|
|
284
|
+
function useClickOutside(ref, handler, eventType = "click") {
|
|
285
|
+
const handlerRef = useRef(handler);
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
handlerRef.current = handler;
|
|
288
|
+
}, [handler]);
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
// 监听 document 的 click 事件
|
|
291
|
+
// 点击回调:页面每次被点击时,要执行的函数
|
|
292
|
+
const listener = (event) => {
|
|
293
|
+
// 拿到真实 边界DOM
|
|
294
|
+
const el = ref?.current;
|
|
295
|
+
// 空值保护: 当前拿不到 DOM,直接return
|
|
296
|
+
if (!el)
|
|
297
|
+
return;
|
|
298
|
+
// 点击发生在组件内部, 直接return
|
|
299
|
+
/*
|
|
300
|
+
event.target: 实际点到的元素
|
|
301
|
+
el.contains:DOM 原生 API,判断 el元素是否包含某个节点
|
|
302
|
+
*/
|
|
303
|
+
// 复杂场景下如果下拉框通过 portal 渲染到 body 等其他 DOM 树中,单纯依赖 contains 判断可能不够,需要更通用的命中判断策略
|
|
304
|
+
if (el.contains(event.target))
|
|
305
|
+
return;
|
|
306
|
+
handlerRef.current(event); // 执行回调函数
|
|
307
|
+
};
|
|
308
|
+
// 监听挂在 document整个页面上
|
|
309
|
+
document.addEventListener(eventType, listener);
|
|
310
|
+
// 清理函数: 卸载组件时,移除监听(防止内存泄漏/重复监听)
|
|
311
|
+
return () => {
|
|
312
|
+
document.removeEventListener(eventType, listener);
|
|
313
|
+
};
|
|
314
|
+
}, [ref, eventType]);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const AutoCompleteDropdown = (props) => {
|
|
318
|
+
const { loading, showDropdown, suggestions, highlightIndex, onSelect, renderOption, onExited } = props;
|
|
319
|
+
const renderTemplate = (item) => {
|
|
320
|
+
return renderOption ? renderOption(item) : item.value;
|
|
321
|
+
};
|
|
322
|
+
return (jsx(Transition, { in: showDropdown || loading, animation: "zoom-in-top", timeout: 300, onExited: onExited, children: jsxs("ul", { className: "viking-suggestion-list", children: [loading && (jsx("div", { className: "suggestions-loading-icon", children: jsx(Icon, { icon: "spinner", spin: true }) })), suggestions.map((item, index) => {
|
|
323
|
+
const cnames = classNames("suggestion-item", {
|
|
324
|
+
"is-active": index === highlightIndex,
|
|
325
|
+
});
|
|
326
|
+
return (jsx("li", { className: cnames, onClick: () => onSelect(item), children: renderTemplate(item) }, index));
|
|
327
|
+
})] }) }));
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 输入框自动完成功能。当输入值需要自动完成时使用,支持同步和异步两种方式
|
|
332
|
+
* 支持 Input 组件的所有属性 支持键盘事件选择
|
|
333
|
+
* ### 引用方法
|
|
334
|
+
*
|
|
335
|
+
* ~~~js
|
|
336
|
+
* import { AutoComplete } from 'vikingship'
|
|
337
|
+
* ~~~
|
|
338
|
+
*/
|
|
339
|
+
const AutoComplete = (props) => {
|
|
340
|
+
const { fetchSuggestions, onSelect, onChange, value, renderOption, ...restProps } = props;
|
|
341
|
+
// 输入框当前显示的文本
|
|
342
|
+
const [inputValue, setInputValue] = useState(value || "");
|
|
343
|
+
// 候选项列表:下拉建议列表的数据源(渲染 `<li>` 就靠它)
|
|
344
|
+
const [suggestions, setSugestions] = useState([]);
|
|
345
|
+
// 加载状态:异步请求进行中就显示 loading
|
|
346
|
+
const [loading, setLoading] = useState(false);
|
|
347
|
+
// 是否展示下拉(配合 `Transition` 动画)
|
|
348
|
+
const [showDropdown, setShowDropdown] = useState(false);
|
|
349
|
+
// 高亮索引:键盘上下选择时,哪一项高亮(对应 class `is-active`)
|
|
350
|
+
const [highlightIndex, setHighlightIndex] = useState(-1);
|
|
351
|
+
// 两个关键 ref:
|
|
352
|
+
// 用来区分“用户打字触发搜索” vs “用户选中后把值塞回去(不应该再搜一次)”
|
|
353
|
+
// 用户打字时:true,用户选中:false
|
|
354
|
+
const triggerSearch = useRef(false);
|
|
355
|
+
// 挂到最外层 div 上,给 `useClickOutside` 判断“点击是否发生在组件外”
|
|
356
|
+
const componentRef = useRef(null);
|
|
357
|
+
// 防抖Hook:把“频繁输入”变成“停顿后再触发一次”
|
|
358
|
+
const debouncedValue = useDebounce(inputValue, 300);
|
|
359
|
+
// 自定义Hook:点击组件外部时,触发自定义的回调
|
|
360
|
+
/* 在 `document` 上挂一个 `click` 监听
|
|
361
|
+
* @param componentRef 组件的 ref
|
|
362
|
+
* @param callback 点击外部时执行的回调
|
|
363
|
+
*/
|
|
364
|
+
// componentRef: 指向 AutoComplete 最外层 DOM
|
|
365
|
+
useClickOutside(componentRef, () => {
|
|
366
|
+
setSugestions([]);
|
|
367
|
+
setShowDropdown(false);
|
|
368
|
+
});
|
|
369
|
+
// 监听 `debouncedValue防抖后的值` 变化
|
|
370
|
+
// fetchSuggestions这个函数本身的引用地址变没变,改变时,监听变化
|
|
371
|
+
// 因为:React在 effect 里用到了这个函数,避免:闭包拿到旧函数
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
// 调度函数: 把 函数 放入微任务队列(执行)
|
|
374
|
+
// 页面更新等逻辑处理完再更新 UI,减少闪烁、时序混乱的问题
|
|
375
|
+
const schedule = (fn) => {
|
|
376
|
+
queueMicrotask(fn);
|
|
377
|
+
};
|
|
378
|
+
/* 停止搜索,关闭下拉框
|
|
379
|
+
* 两种情况:
|
|
380
|
+
* 1. 输入框为空(防抖后的值为空)
|
|
381
|
+
* 2. 用户选中下拉建议某一项后,输入框值会变为选中项的值(不应该再搜一次)
|
|
382
|
+
*/
|
|
383
|
+
if (!debouncedValue || !triggerSearch.current) {
|
|
384
|
+
// 调用 调度函数: 把函数放入微任务队列(执行)
|
|
385
|
+
schedule(() => {
|
|
386
|
+
setShowDropdown(false); // 关闭下拉框 展示
|
|
387
|
+
setHighlightIndex(-1); // 重置高亮索引
|
|
388
|
+
});
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
// 搜索执行
|
|
392
|
+
// 1. 拿当前输入内容,获取 建议项
|
|
393
|
+
const results = fetchSuggestions(debouncedValue);
|
|
394
|
+
// 异步返回:
|
|
395
|
+
if (results instanceof Promise) {
|
|
396
|
+
schedule(() => {
|
|
397
|
+
setLoading(true); // 显示“加载中”
|
|
398
|
+
setHighlightIndex(-1); // 不高亮任何项
|
|
399
|
+
});
|
|
400
|
+
// 异步返回: 拿到数据后,更新状态
|
|
401
|
+
results.then((data) => {
|
|
402
|
+
setLoading(false); // 关闭 “加载中”
|
|
403
|
+
setSugestions(data); // 更新建议项列表
|
|
404
|
+
setShowDropdown(data.length > 0); // 有数据,就显示下拉框
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
// 同步返回: 本地有个数组,直接筛选
|
|
409
|
+
schedule(() => {
|
|
410
|
+
setLoading(false); // 关闭 “加载中”
|
|
411
|
+
setSugestions(results);
|
|
412
|
+
setShowDropdown(results.length > 0);
|
|
413
|
+
setHighlightIndex(-1);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}, [debouncedValue, fetchSuggestions]);
|
|
417
|
+
const highlight = (index) => {
|
|
418
|
+
// 调用utils: 限制索引在 0 和 (建议项长度 - 1) 之间
|
|
419
|
+
const nextIndex = clamp(index, 0, suggestions.length - 1);
|
|
420
|
+
setHighlightIndex(nextIndex);
|
|
421
|
+
};
|
|
422
|
+
const handleKeyDown = (e) => {
|
|
423
|
+
switch (e.keyCode) {
|
|
424
|
+
case 13:
|
|
425
|
+
if (suggestions[highlightIndex]) {
|
|
426
|
+
handleSelect(suggestions[highlightIndex]);
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
case 38:
|
|
430
|
+
highlight(highlightIndex - 1);
|
|
431
|
+
break;
|
|
432
|
+
case 40:
|
|
433
|
+
highlight(highlightIndex + 1);
|
|
434
|
+
break;
|
|
435
|
+
case 27:
|
|
436
|
+
setShowDropdown(false);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
// 回调:消息框内容发生变化
|
|
441
|
+
const handleChange = (e) => {
|
|
442
|
+
// 获取 输入框内容 (去掉首尾空格)
|
|
443
|
+
const value = e.target.value.trim();
|
|
444
|
+
// 更新 输入框内容
|
|
445
|
+
// `inputValue` 变化 → 经过 `useDebounce` 得到 `debouncedValue`
|
|
446
|
+
setInputValue(value);
|
|
447
|
+
// 组件使用者传来的回调:消息框内容发生变化
|
|
448
|
+
if (onChange) {
|
|
449
|
+
onChange(value);
|
|
450
|
+
}
|
|
451
|
+
// 告诉后面“这次是用户输入,应当触发搜索”
|
|
452
|
+
triggerSearch.current = true;
|
|
453
|
+
};
|
|
454
|
+
// 回调:用户选中某一项
|
|
455
|
+
const handleSelect = (item) => {
|
|
456
|
+
setInputValue(item.value);
|
|
457
|
+
setShowDropdown(false);
|
|
458
|
+
if (onSelect) {
|
|
459
|
+
onSelect(item);
|
|
460
|
+
}
|
|
461
|
+
triggerSearch.current = false;
|
|
462
|
+
};
|
|
463
|
+
return (jsxs("div", { className: "viking-auto-complete", ref: componentRef, children: [jsx(Input, { ...restProps, value: inputValue, onChange: handleChange, onKeyDown: handleKeyDown }), jsx(AutoCompleteDropdown, { loading: loading, showDropdown: showDropdown, suggestions: suggestions, highlightIndex: highlightIndex, onSelect: handleSelect, renderOption: renderOption, onExited: () => {
|
|
464
|
+
setSugestions([]);
|
|
465
|
+
} })] }));
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const Progress = (props) => {
|
|
469
|
+
const { percent, strokeHeight = 15, showText = true, styles, theme = "primary" } = props;
|
|
470
|
+
return (jsx("div", { className: "viking-progress-bar", style: styles, children: jsx("div", { className: "viking-progress-bar-outer", style: { height: `${strokeHeight}px` }, children: jsx("div", { className: `viking-progress-bar-inner color-${theme}`, style: { width: `${percent}%` }, children: showText && jsx("span", { className: "inner-text", children: `${percent}%` }) }) }) }));
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const UploadList = (props) => {
|
|
474
|
+
// 从 Prop 中取出 fileList 文件列表 和 回调
|
|
475
|
+
const { fileList, onRemove } = props;
|
|
476
|
+
console.log("firelist", fileList);
|
|
477
|
+
return (jsx("ul", { className: "viking-upload-list", children: fileList.map((item) => {
|
|
478
|
+
return (jsxs("li", { className: "viking-upload-list-item", children: [jsxs("span", { className: `file-name file-name-${item.status}`, children: [jsx(Icon, { icon: "file-alt", theme: "secondary" }), item.name] }), jsxs("span", { className: "file-status", children: [(item.status === "uploading" || item.status === "ready") && (jsx(Icon, { icon: "spinner", spin: true, theme: "primary" })), item.status === "success" && jsx(Icon, { icon: "check-circle", theme: "success" }), item.status === "error" && jsx(Icon, { icon: "times-circle", theme: "danger" })] }), jsx("span", { className: "file-actions", children: jsx(Icon, { icon: "times", onClick: () => {
|
|
479
|
+
onRemove(item);
|
|
480
|
+
} }) }), item.status === "uploading" && jsx(Progress, { percent: item.percent || 0 })] }, item.uid));
|
|
481
|
+
}) }));
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const Dragger = (props) => {
|
|
485
|
+
const { onFile, children } = props;
|
|
486
|
+
// 负责拖拽样式
|
|
487
|
+
const [dragOver, setDragOver] = useState(false);
|
|
488
|
+
// 使用 `classNames` 拼 `className`:
|
|
489
|
+
const klass = classNames("viking-uploader-dragger", {
|
|
490
|
+
// 如果 `dragOver === true` 时,再加一个:`is-dragover`
|
|
491
|
+
"is-dragover": dragOver,
|
|
492
|
+
});
|
|
493
|
+
// 上传流程
|
|
494
|
+
const handleDrop = (e) => {
|
|
495
|
+
e.preventDefault(); // 阻止默认行为
|
|
496
|
+
setDragOver(false); // 拖拽结束:改变拖拽样式
|
|
497
|
+
// dataTransfer: 浏览器 drop-API 的对象
|
|
498
|
+
// 包含: files(文件列表FileList)、items、types
|
|
499
|
+
// 通过 onFile回调 传回 Upload 处理
|
|
500
|
+
onFile(e.dataTransfer.files);
|
|
501
|
+
};
|
|
502
|
+
// 回调: 控制拖拽状态(改变样式)
|
|
503
|
+
const handleDrag = (e, over) => {
|
|
504
|
+
e.preventDefault();
|
|
505
|
+
setDragOver(over);
|
|
506
|
+
};
|
|
507
|
+
return (jsx("div", { className: klass,
|
|
508
|
+
// e: 监听DragEvent拖拽事件对象
|
|
509
|
+
onDragOver: (e) => {
|
|
510
|
+
handleDrag(e, true);
|
|
511
|
+
}, onDragLeave: (e) => {
|
|
512
|
+
handleDrag(e, false);
|
|
513
|
+
}, onDrop: handleDrop, children: children }));
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* 通过点击或者拖拽上传文件
|
|
518
|
+
* ### 引用方法
|
|
519
|
+
*
|
|
520
|
+
* ~~~js
|
|
521
|
+
* import { Upload } from 'vikingship'
|
|
522
|
+
* ~~~
|
|
523
|
+
*/
|
|
524
|
+
const Upload = (props) => {
|
|
525
|
+
const { action, defaultFileList, beforeUpload, onProgress, onSuccess, onError, onChange, onRemove, name = "file", headers, data, withCredentials, accept, multiple, children, drag, } = props;
|
|
526
|
+
// 文件输入框 ref 引用
|
|
527
|
+
const fileInput = useRef(null);
|
|
528
|
+
// (子组件)页面上 正在显示 的 上传文件列表
|
|
529
|
+
const [fileList, setFileList] = useState(defaultFileList || []);
|
|
530
|
+
// **`UploadList`** 子组件: 从 上传文件列表fileList 渲染一堆 列表项 class
|
|
531
|
+
// 上传列表状态更新器
|
|
532
|
+
const updateFileList = (updateFile, updateObj) => {
|
|
533
|
+
setFileList((prevList) => {
|
|
534
|
+
return prevList.map((file) => {
|
|
535
|
+
// 在列表里找到同一个 uid 文件
|
|
536
|
+
if (file.uid === updateFile.uid) {
|
|
537
|
+
// 更新部分字段
|
|
538
|
+
return { ...file, ...updateObj };
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
return file;
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
// 点击上传回调:用户点击 上传文件 输入框
|
|
547
|
+
const handleClick = () => {
|
|
548
|
+
// 用 ref 拿到“隐藏的” input元素
|
|
549
|
+
if (fileInput.current) {
|
|
550
|
+
// 调用浏览器的原生能力: 弹出 系统级文件选择框
|
|
551
|
+
fileInput.current.click();
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
// 文件上传回调:当有文件传来的时候触发
|
|
555
|
+
const handleFileChange = (e) => {
|
|
556
|
+
// 获取文件列表:类型为 FileList
|
|
557
|
+
const files = e.target.files;
|
|
558
|
+
if (!files) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
// 调用 上传文件函数(含发送请求):决定 每个/不同文件 怎么处理
|
|
562
|
+
uploadFiles(files);
|
|
563
|
+
// 清空 文件输入框
|
|
564
|
+
if (fileInput.current) {
|
|
565
|
+
fileInput.current.value = "";
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
// 传递给 UploadList 子组件的 回调函数
|
|
569
|
+
const handleRemove = (file) => {
|
|
570
|
+
setFileList((prevList) => {
|
|
571
|
+
return prevList.filter((item) => item.uid !== file.uid);
|
|
572
|
+
});
|
|
573
|
+
if (onRemove) {
|
|
574
|
+
onRemove(file);
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
// 函数: 上传文件: 决定 每个文件 上传
|
|
578
|
+
// (beforeUpload: “上传前钩子”)
|
|
579
|
+
const uploadFiles = (files, test) => {
|
|
580
|
+
// 传来的文件列表 FileList类型,不是数组 =》 转为数组
|
|
581
|
+
const postFiles = Array.from(files);
|
|
582
|
+
//
|
|
583
|
+
if (test) {
|
|
584
|
+
console.log("drag", postFiles[0]);
|
|
585
|
+
}
|
|
586
|
+
// 遍历 数组 每一项
|
|
587
|
+
postFiles.forEach((file) => {
|
|
588
|
+
// 没有配置 beforeUpload,直接上传
|
|
589
|
+
if (!beforeUpload) {
|
|
590
|
+
post(file);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
// 用户使用了 beforeUpload => 需要等待 用户的异步处理结果(例如压缩图片)
|
|
594
|
+
// beforeUpload(file)用户在钩子里写的回调: 会返回 Promise<newFile>
|
|
595
|
+
// 获取 Promise<newFile> 用这个处理完的 File 对象发送
|
|
596
|
+
// 执行用户传进来的回调函数,并把它的返回值接住,放进 result
|
|
597
|
+
const result = beforeUpload(file);
|
|
598
|
+
//
|
|
599
|
+
if (result && result instanceof Promise) {
|
|
600
|
+
// 获取 异步回调 reslove(newFile)后的 newFile
|
|
601
|
+
result.then((processedFile) => {
|
|
602
|
+
post(processedFile); // 发送用户异步处理完的 新File文件
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
else if (result !== false) {
|
|
606
|
+
// 用户使用了拦截:return false ==> 永远不会触发 post, 不上传
|
|
607
|
+
// 返回 true → 上传原文件
|
|
608
|
+
post(file);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
};
|
|
613
|
+
// 函数: 发axios请求
|
|
614
|
+
const post = (file) => {
|
|
615
|
+
// 改造浏览器原生File,创建 内部文件对象 `_file` (文件本体 + 上传状态)
|
|
616
|
+
const _file = {
|
|
617
|
+
uid: Date.now() + "upload-file",
|
|
618
|
+
status: "ready",
|
|
619
|
+
name: file.name,
|
|
620
|
+
size: file.size,
|
|
621
|
+
percent: 0,
|
|
622
|
+
raw: file,
|
|
623
|
+
};
|
|
624
|
+
// 请求发送之前放进 fileList,列表先显示出来
|
|
625
|
+
setFileList((prevList) => {
|
|
626
|
+
return [_file, ...prevList];
|
|
627
|
+
});
|
|
628
|
+
// 2) 构建 `FormData`
|
|
629
|
+
const formData = new FormData();
|
|
630
|
+
formData.append(name || "file", file);
|
|
631
|
+
// 如果传了 `data`, 额外字段也 append 进 FormData
|
|
632
|
+
// (例如 `userId`、`token`)
|
|
633
|
+
if (data) {
|
|
634
|
+
Object.keys(data).forEach((key) => {
|
|
635
|
+
formData.append(key, data[key]);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
// 使用 axios 发送 POST 请求
|
|
639
|
+
axios
|
|
640
|
+
.post(action, formData, {
|
|
641
|
+
headers: {
|
|
642
|
+
...headers,
|
|
643
|
+
"Content-Type": "multipart/form-data",
|
|
644
|
+
},
|
|
645
|
+
// 跨域请求 凭证信息(Cookie)
|
|
646
|
+
// 需要 后端 允许跨域携带凭证: Access-Control-Allow-Credentials: true
|
|
647
|
+
withCredentials,
|
|
648
|
+
// axios 提供的 请求配置回调(“系统回调”)
|
|
649
|
+
// (不需要手动调用)上传过程中,axios 内部自动不断触发 onUploadProgress
|
|
650
|
+
onUploadProgress: (e) => {
|
|
651
|
+
// 把字节进度算成百分比
|
|
652
|
+
const total = e.total ?? 0;
|
|
653
|
+
const percentage = total ? Math.round((e.loaded * 100) / total) : 0;
|
|
654
|
+
// 更新 fileList 中这条文件的 percent/status
|
|
655
|
+
if (percentage < 100) {
|
|
656
|
+
// 更新 React state(驱动UI):让 UploadList 子组件重新渲染
|
|
657
|
+
updateFileList(_file, { percent: percentage, status: "uploading" });
|
|
658
|
+
// 更新当前 _file 对象,保证传给回调的值是新的
|
|
659
|
+
_file.status = "uploading";
|
|
660
|
+
_file.percent = percentage;
|
|
661
|
+
// 将 axios 的 onUploadProgress 得到的上传进度结果,包装一层
|
|
662
|
+
// 提供给 Upload 组件的外部使用者 外部钩子onProgress:给组件外部使用
|
|
663
|
+
if (onProgress) {
|
|
664
|
+
onProgress(percentage, _file);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
})
|
|
669
|
+
.then((resp) => {
|
|
670
|
+
// 成功时:更新React UI内部状态
|
|
671
|
+
updateFileList(_file, { status: "success", response: resp.data });
|
|
672
|
+
// 更新当前 _file 对象,保证传给回调的值是新的
|
|
673
|
+
_file.status = "success";
|
|
674
|
+
_file.response = resp.data;
|
|
675
|
+
// 通知外部 onSuccess / onChange 钩子
|
|
676
|
+
if (onSuccess) {
|
|
677
|
+
onSuccess(resp.data, _file);
|
|
678
|
+
}
|
|
679
|
+
if (onChange) {
|
|
680
|
+
onChange(_file);
|
|
681
|
+
}
|
|
682
|
+
})
|
|
683
|
+
.catch((err) => {
|
|
684
|
+
// 失败时:更新React UI内部状态
|
|
685
|
+
updateFileList(_file, { status: "error", error: err });
|
|
686
|
+
// 更新当前 _file 对象,保证传给回调的值是新的
|
|
687
|
+
_file.status = "error";
|
|
688
|
+
_file.error = err;
|
|
689
|
+
//
|
|
690
|
+
if (onError) {
|
|
691
|
+
onError(err, _file);
|
|
692
|
+
}
|
|
693
|
+
if (onChange) {
|
|
694
|
+
onChange(_file);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
};
|
|
698
|
+
return (jsxs("div", { className: "viking-upload-component", children: [jsxs("div", { className: "viking-upload-input", style: { display: "inline-block" }, onClick: handleClick, children: [children, drag ? (
|
|
699
|
+
// 传给 Dragger子组件回调函数 onFile
|
|
700
|
+
// 当用户拖拽文件,Dragger 的onDrop状态触发 onFile(files)
|
|
701
|
+
jsx(Dragger, { onFile: (files) => {
|
|
702
|
+
uploadFiles(files, true);
|
|
703
|
+
}, children: children })) : (
|
|
704
|
+
// 如果 drag = false, 直接渲染 children(普通模式)
|
|
705
|
+
children), jsx("input", { className: "viking-file-input", style: { display: "none" }, ref: fileInput, onChange: handleFileChange, type: "file", accept: accept, multiple: multiple })] }), jsx(UploadList, { fileList: fileList, onRemove: handleRemove })] }));
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
export { AutoComplete, Button, Icon, Input, TransMenu as Menu, Progress, Transition, Upload, setupIcons };
|
|
709
|
+
//# sourceMappingURL=index.esm.js.map
|