@wwog/react 1.2.4 → 1.2.6
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 +79 -3
- package/dist/index.d.mts +127 -81
- package/dist/index.js +1 -1
- package/package.json +5 -4
- package/src/{ProcessControl → components/ProcessControl}/Switch.tsx +2 -2
- package/src/components/ProcessControl/index.ts +4 -0
- package/src/components/Struct/index.ts +2 -0
- package/src/components/Sundry/ClassName.tsx +86 -0
- package/src/components/Sundry/index.ts +4 -0
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useControlled.ts +22 -27
- package/src/index.ts +9 -4
- package/src/utils/cx.test.ts +33 -0
- package/src/utils/cx.ts +25 -0
- package/src/utils/reactUtils.ts +22 -0
- package/src/utils/sundry.test.ts +130 -0
- package/src/utils/sundry.ts +130 -0
- package/src/Common/index.ts +0 -4
- package/src/ProcessControl/index.ts +0 -5
- package/src/utils/index.ts +0 -153
- /package/src/{ProcessControl → components/ProcessControl}/If.tsx +0 -0
- /package/src/{ProcessControl → components/ProcessControl}/Pipe.tsx +0 -0
- /package/src/{ProcessControl → components/ProcessControl}/When.tsx +0 -0
- /package/src/{Common → components/Struct}/ArrayRender.tsx +0 -0
- /package/src/{Common → components/Struct}/DateRender.tsx +0 -0
- /package/src/{Common → components/Sundry}/Scope.tsx +0 -0
- /package/src/{Common → components/Sundry}/SizeBox.tsx +0 -0
- /package/src/{ProcessControl → components/Sundry}/Toggle.tsx +0 -0
package/README.md
CHANGED
|
@@ -312,6 +312,48 @@ function Layout() {
|
|
|
312
312
|
}
|
|
313
313
|
```
|
|
314
314
|
|
|
315
|
+
#### `<ClassName>` (v1.2.5+)
|
|
316
|
+
|
|
317
|
+
用于将 CSS 类名分类编写的组件,内置类似`clsx`的功能,并且可以去除重复的 className。
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
import { ClassName } from "@wwog/react";
|
|
321
|
+
|
|
322
|
+
function Example() {
|
|
323
|
+
return (
|
|
324
|
+
<ClassName
|
|
325
|
+
className={{
|
|
326
|
+
base: "p-2 bg-white",
|
|
327
|
+
hover: "hover:bg-gray-100",
|
|
328
|
+
active: "active:bg-gray-200",
|
|
329
|
+
focus: "focus:ring-2",
|
|
330
|
+
other: "button",
|
|
331
|
+
}}
|
|
332
|
+
>
|
|
333
|
+
<button>点击我</button>
|
|
334
|
+
</ClassName>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
还可以使用容器包装元素:
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
<ClassName
|
|
343
|
+
className={{
|
|
344
|
+
base: ["p-2", { "bg-red-500": isError }],
|
|
345
|
+
hover: { "hover:bg-blue-500": true },
|
|
346
|
+
}}
|
|
347
|
+
asWrapper="span"
|
|
348
|
+
>
|
|
349
|
+
内容
|
|
350
|
+
</ClassName>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
- `className`:分类的类名对象,支持各种状态的类名(base, hover, active, focus, disabled 等)
|
|
354
|
+
- `asWrapper`:是否生成包含所有 className 的 wrapper,默认 false,传递标签名如'div'或'span'
|
|
355
|
+
- `children`:子元素,通常是一个 React 元素
|
|
356
|
+
|
|
315
357
|
### hooks
|
|
316
358
|
|
|
317
359
|
- 一些常用的 hooks 的封装
|
|
@@ -324,11 +366,45 @@ function Layout() {
|
|
|
324
366
|
|
|
325
367
|
- 用于部分组件的内部函数,如需要也可使用
|
|
326
368
|
|
|
327
|
-
formatDate
|
|
369
|
+
#### `formatDate`
|
|
370
|
+
|
|
371
|
+
比较标准的格式化时间函数
|
|
372
|
+
|
|
373
|
+
#### `childrenLoop`
|
|
374
|
+
|
|
375
|
+
可以中断的子节点遍历,让一些分支流程拥有极致性能
|
|
376
|
+
|
|
377
|
+
#### `Counter`
|
|
378
|
+
|
|
379
|
+
计数器
|
|
380
|
+
|
|
381
|
+
#### `cn` (v1.2.5+)
|
|
382
|
+
|
|
383
|
+
一个高效的 CSS 类名合并工具函数,类似于`clsx`或`classnames`,但能自动去除重复的类名。
|
|
384
|
+
|
|
385
|
+
```tsx
|
|
386
|
+
import { cn } from "@wwog/react";
|
|
387
|
+
|
|
388
|
+
function Example({ isActive, isDisabled }) {
|
|
389
|
+
return (
|
|
390
|
+
<div
|
|
391
|
+
className={cn("base-class", ["array-class-1", "array-class-2"], {
|
|
392
|
+
"active-class": isActive,
|
|
393
|
+
"disabled-class": isDisabled,
|
|
394
|
+
})}
|
|
395
|
+
>
|
|
396
|
+
内容
|
|
397
|
+
</div>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
```
|
|
328
401
|
|
|
329
|
-
|
|
402
|
+
支持多种参数类型:
|
|
330
403
|
|
|
331
|
-
|
|
404
|
+
- 字符串: `"class1 class2"`
|
|
405
|
+
- 字符串数组: `["class1", "class2"]`
|
|
406
|
+
- 对象: `{ "class1": true, "class2": false }`
|
|
407
|
+
- 以上类型的任意组合
|
|
332
408
|
|
|
333
409
|
## License
|
|
334
410
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React$1, { ReactNode, FC, Dispatch } from 'react';
|
|
1
|
+
import React$1, { ReactNode, FC, HTMLElementType, Dispatch } from 'react';
|
|
2
2
|
|
|
3
3
|
interface SwitchProps<T> {
|
|
4
4
|
value: T;
|
|
@@ -190,46 +190,6 @@ interface PipeProps {
|
|
|
190
190
|
*/
|
|
191
191
|
declare const Pipe: FC<PipeProps>;
|
|
192
192
|
|
|
193
|
-
interface ToggleProps<T = boolean> {
|
|
194
|
-
/**
|
|
195
|
-
* @description_en The initial value to toggle.
|
|
196
|
-
* @description_zh 初始切换值。
|
|
197
|
-
* @default 0
|
|
198
|
-
*/
|
|
199
|
-
index?: number;
|
|
200
|
-
/**
|
|
201
|
-
* @description_en Array of values to toggle between.
|
|
202
|
-
* @description_zh 可切换的值数组。
|
|
203
|
-
*/
|
|
204
|
-
options: T[];
|
|
205
|
-
/**
|
|
206
|
-
* @description_en Function to determine the next value index in the toggle sequence.
|
|
207
|
-
* @description_zh 确定切换序列中下一个值索引的函数。
|
|
208
|
-
* @optional
|
|
209
|
-
*/
|
|
210
|
-
next?: (curIndex: number, options: T[]) => number;
|
|
211
|
-
/**
|
|
212
|
-
* @description_en Render function, receiving the toggled value and toggle function.
|
|
213
|
-
* @description_zh 渲染函数,接收切换后的值和切换函数。
|
|
214
|
-
*/
|
|
215
|
-
render: (value: T, toggle: () => void) => ReactNode;
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* @description_zh 一个声明式组件,用于在预定义选项中切换值并通过 render 函数传递给子组件,支持自定义切换逻辑。
|
|
219
|
-
* @description_en A declarative component for toggling between predefined values and passing them to children via a render function, supporting custom toggle logic.
|
|
220
|
-
* @component
|
|
221
|
-
* @example
|
|
222
|
-
* ```tsx
|
|
223
|
-
* <Toggle
|
|
224
|
-
* options={["light", "dark"]}
|
|
225
|
-
* render={(theme, toggleTheme) => (
|
|
226
|
-
* <div onClick={toggleTheme}>当前主题: {theme}</div>
|
|
227
|
-
* )}
|
|
228
|
-
* />
|
|
229
|
-
* ```
|
|
230
|
-
*/
|
|
231
|
-
declare const Toggle: <T>(props: ToggleProps<T>) => React$1.ReactNode;
|
|
232
|
-
|
|
233
193
|
interface SizeBoxProps {
|
|
234
194
|
size?: number | string;
|
|
235
195
|
height?: number | string;
|
|
@@ -244,13 +204,6 @@ interface SizeBoxProps {
|
|
|
244
204
|
*/
|
|
245
205
|
declare const SizeBox: FC<SizeBoxProps>;
|
|
246
206
|
|
|
247
|
-
interface ArrayRenderProps<T> {
|
|
248
|
-
items: T[];
|
|
249
|
-
renderItem: (item: T, index: number) => React$1.ReactNode;
|
|
250
|
-
filter?: (item: T) => boolean;
|
|
251
|
-
}
|
|
252
|
-
declare function ArrayRender<T>(props: ArrayRenderProps<T>): ReactNode;
|
|
253
|
-
|
|
254
207
|
/**
|
|
255
208
|
* Props for the `Scope` component.
|
|
256
209
|
*
|
|
@@ -306,6 +259,98 @@ interface ScopeProps {
|
|
|
306
259
|
*/
|
|
307
260
|
declare const Scope: FC<ScopeProps>;
|
|
308
261
|
|
|
262
|
+
type CxInput = string | string[] | Record<string, boolean> | undefined | null | false;
|
|
263
|
+
declare function cx(...args: CxInput[]): string;
|
|
264
|
+
|
|
265
|
+
interface ClassNameProps {
|
|
266
|
+
className?: {
|
|
267
|
+
base?: CxInput;
|
|
268
|
+
hover?: CxInput;
|
|
269
|
+
active?: CxInput;
|
|
270
|
+
focus?: CxInput;
|
|
271
|
+
disabled?: CxInput;
|
|
272
|
+
[key: string]: CxInput;
|
|
273
|
+
};
|
|
274
|
+
/**
|
|
275
|
+
* @description 传入容器标签名.是否生成包含所有 `className` 的 `wrapper`, 默认 false, 传递 `true` 为 `div。`
|
|
276
|
+
* @description_en Whether to generate a `wrapper` containing all `className`, default is false, and pass the container tag name, if `true` will be `div`.
|
|
277
|
+
*/
|
|
278
|
+
asWrapper?: boolean | HTMLElementType;
|
|
279
|
+
children?: React$1.ReactNode;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* @description 用于将 `className` 分类编写的组件,内置了类似`clsx`的功能,并且去除重复的 className。
|
|
283
|
+
* @description_en A component for `className` classification, built-in similar to `clsx` functionality, and removes duplicate className.
|
|
284
|
+
* @component
|
|
285
|
+
* @example
|
|
286
|
+
* ```tsx
|
|
287
|
+
* <ClassName className={{ base: "p-2 bg-red", hover: ["hover:bg-blue", { "hover:text-white": true }] }}>
|
|
288
|
+
* <button>Click me</button>
|
|
289
|
+
* </ClassName>
|
|
290
|
+
* ```
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```tsx
|
|
294
|
+
* <ClassName
|
|
295
|
+
* className={{
|
|
296
|
+
* base: ["p-2", { "bg-red": condition }],
|
|
297
|
+
* hover: { "hover:bg-blue": true },
|
|
298
|
+
* }}
|
|
299
|
+
* asWrapper="span"
|
|
300
|
+
* >
|
|
301
|
+
* <button>Click me</button>
|
|
302
|
+
* </ClassName>
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
declare const ClassName: FC<ClassNameProps>;
|
|
306
|
+
|
|
307
|
+
interface ToggleProps<T = boolean> {
|
|
308
|
+
/**
|
|
309
|
+
* @description_en The initial value to toggle.
|
|
310
|
+
* @description_zh 初始切换值。
|
|
311
|
+
* @default 0
|
|
312
|
+
*/
|
|
313
|
+
index?: number;
|
|
314
|
+
/**
|
|
315
|
+
* @description_en Array of values to toggle between.
|
|
316
|
+
* @description_zh 可切换的值数组。
|
|
317
|
+
*/
|
|
318
|
+
options: T[];
|
|
319
|
+
/**
|
|
320
|
+
* @description_en Function to determine the next value index in the toggle sequence.
|
|
321
|
+
* @description_zh 确定切换序列中下一个值索引的函数。
|
|
322
|
+
* @optional
|
|
323
|
+
*/
|
|
324
|
+
next?: (curIndex: number, options: T[]) => number;
|
|
325
|
+
/**
|
|
326
|
+
* @description_en Render function, receiving the toggled value and toggle function.
|
|
327
|
+
* @description_zh 渲染函数,接收切换后的值和切换函数。
|
|
328
|
+
*/
|
|
329
|
+
render: (value: T, toggle: () => void) => ReactNode;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* @description_zh 一个声明式组件,用于在预定义选项中切换值并通过 render 函数传递给子组件,支持自定义切换逻辑。
|
|
333
|
+
* @description_en A declarative component for toggling between predefined values and passing them to children via a render function, supporting custom toggle logic.
|
|
334
|
+
* @component
|
|
335
|
+
* @example
|
|
336
|
+
* ```tsx
|
|
337
|
+
* <Toggle
|
|
338
|
+
* options={["light", "dark"]}
|
|
339
|
+
* render={(theme, toggleTheme) => (
|
|
340
|
+
* <div onClick={toggleTheme}>当前主题: {theme}</div>
|
|
341
|
+
* )}
|
|
342
|
+
* />
|
|
343
|
+
* ```
|
|
344
|
+
*/
|
|
345
|
+
declare const Toggle: <T>(props: ToggleProps<T>) => React$1.ReactNode;
|
|
346
|
+
|
|
347
|
+
interface ArrayRenderProps<T> {
|
|
348
|
+
items: T[];
|
|
349
|
+
renderItem: (item: T, index: number) => React$1.ReactNode;
|
|
350
|
+
filter?: (item: T) => boolean;
|
|
351
|
+
}
|
|
352
|
+
declare function ArrayRender<T>(props: ArrayRenderProps<T>): ReactNode;
|
|
353
|
+
|
|
309
354
|
interface DateRenderProps<T = string> {
|
|
310
355
|
/**
|
|
311
356
|
* @description_en The input date to render (Date object, ISO string, or timestamp).
|
|
@@ -349,11 +394,33 @@ interface DateRenderProps<T = string> {
|
|
|
349
394
|
*/
|
|
350
395
|
declare function DateRender<T = string>({ source, format, children, }: DateRenderProps<T>): React$1.JSX.Element | null;
|
|
351
396
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
397
|
+
interface UseControlledOptions<T> {
|
|
398
|
+
/**
|
|
399
|
+
* @description - 非受控模式下的默认值,会被受控模式下的值覆盖
|
|
400
|
+
* @description_en - Default value in uncontrolled mode, will be overridden by the value in controlled mode
|
|
401
|
+
*/
|
|
402
|
+
defaultValue: T;
|
|
403
|
+
/**
|
|
404
|
+
* @description - 值变更前的回调函数,可用于拦截或修改新值
|
|
405
|
+
* @description_en - Callback function before the value changes, can be used to intercept or modify the new value
|
|
406
|
+
*/
|
|
407
|
+
onBeforeChange?: (newValue: T, currentValue: T) => boolean | void;
|
|
408
|
+
/**
|
|
409
|
+
* @description - 当值发生变化时触发的回调函数名
|
|
410
|
+
* @description_en - Callback function name triggered when the value changes
|
|
411
|
+
* @default - onChange
|
|
412
|
+
*/
|
|
413
|
+
trigger?: string;
|
|
414
|
+
/**
|
|
415
|
+
* @description - 值的属性名
|
|
416
|
+
* @description_en - Property name of the value
|
|
417
|
+
* @default - value
|
|
418
|
+
*/
|
|
419
|
+
valuePropName?: string;
|
|
420
|
+
props: Record<string, any>;
|
|
421
|
+
}
|
|
422
|
+
declare function useControlled<T>(options: UseControlledOptions<T>): [T, Dispatch<React.SetStateAction<T>>];
|
|
423
|
+
|
|
357
424
|
/**
|
|
358
425
|
* @param schema
|
|
359
426
|
* @example
|
|
@@ -393,32 +460,11 @@ declare class Counter {
|
|
|
393
460
|
next(): number;
|
|
394
461
|
}
|
|
395
462
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
defaultValue: T;
|
|
402
|
-
/**
|
|
403
|
-
* @description - 值变更前的回调函数,可用于拦截或修改新值
|
|
404
|
-
* @description_en - Callback function before the value changes, can be used to intercept or modify the new value
|
|
405
|
-
*/
|
|
406
|
-
onBeforeChange?: (newValue: T, currentValue: T) => boolean | void;
|
|
407
|
-
/**
|
|
408
|
-
* @description - 当值发生变化时触发的回调函数名
|
|
409
|
-
* @description_en - Callback function name triggered when the value changes
|
|
410
|
-
* @default - onChange
|
|
411
|
-
*/
|
|
412
|
-
trigger?: string;
|
|
413
|
-
/**
|
|
414
|
-
* @description - 值的属性名
|
|
415
|
-
* @description_en - Property name of the value
|
|
416
|
-
* @default - value
|
|
417
|
-
*/
|
|
418
|
-
valuePropName?: string;
|
|
419
|
-
props: Record<string, any>;
|
|
420
|
-
}
|
|
421
|
-
declare function useControlled<T>(options: UseControlledOptions<T>): [T, Dispatch<React.SetStateAction<T>>];
|
|
463
|
+
/**
|
|
464
|
+
* @description 性能优化,替代 React.Children.forEach, 回调可以返回 false 来中断循环
|
|
465
|
+
* @description_en Replace React.Children.forEach, the callback can return false to interrupt the loop
|
|
466
|
+
*/
|
|
467
|
+
declare function childrenLoop(children: React$1.ReactNode | undefined, callback: (child: React$1.ReactNode, index: number) => boolean | void): void;
|
|
422
468
|
|
|
423
|
-
export { ArrayRender, Counter, DateRender, False, If, Pipe, Scope, SizeBox, Switch, Toggle, True, When, childrenLoop, formatDate, useControlled };
|
|
424
|
-
export type { ArrayRenderProps, DateRenderProps, ElseIfProps, ElseProps, FalseProps, IfProps, PipeProps, ScopeProps, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, TrueProps, UseControlledOptions, WhenProps };
|
|
469
|
+
export { ArrayRender, ClassName, Counter, DateRender, False, If, Pipe, Scope, SizeBox, Switch, Toggle, True, When, childrenLoop, cx, formatDate, useControlled };
|
|
470
|
+
export type { ArrayRenderProps, ClassNameProps, CxInput, DateRenderProps, ElseIfProps, ElseProps, FalseProps, IfProps, PipeProps, ScopeProps, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, TrueProps, UseControlledOptions, WhenProps };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import r,{useMemo as
|
|
1
|
+
import r,{useMemo as h,useEffect as C,isValidElement as I,Fragment as M,cloneElement as R,useState as T,useCallback as H}from"react";function A(n,t){if(n===void 0)return;let e=0;if(Array.isArray(n)){for(const l of n)if(t(l,e++)===!1)break}else t(n,e)}const P=(n,t)=>n===t,S=n=>r.createElement(r.Fragment,null,n.children);S.displayName="Switch_Case";const E=n=>r.createElement(r.Fragment,null,n.children);E.displayName="Switch_Default";const g=n=>{const{value:t,compare:e=P,children:l,strict:a=!1}=n,o=new Set;let s=null,c=null,u=!1;return A(l,(d,i)=>{if(!r.isValidElement(d))throw new Error(`Switch Children only accepts valid React elements at index ${i}`);const f=d.type;if(f.displayName===S.displayName){const m=d.props;if(o.has(m.value))throw new Error(`Switch found duplicate Case value at index ${i}: ${JSON.stringify(m.value)}${a?" (detected in strict mode)":""}`);if(o.add(m.value),!s&&e(t,m.value)&&(s=m.children,a===!1))return!1}else if(f.displayName===E.displayName){if(u)throw new Error(`Switch can only have one Default child at index ${i}`);if(u=!0,c=d.props.children,!a&&s)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(f.displayName||f.name||f)} at index ${i}`)}),r.createElement(r.Fragment,null,s??c)};g.displayName="Switch",g.Case=S,g.Default=E,g.createTyped=function(){return{Switch:g,Case:S,Default:E}};const w=n=>r.createElement(r.Fragment,null,n.children),N=({children:n})=>r.createElement(r.Fragment,null,n),F=n=>r.createElement(r.Fragment,null,n.children);w.displayName="If_Then",N.displayName="If_Else",F.displayName="If_ElseIf";const p=({condition:n,children:t})=>{let e=null,l=null;const a=[];if(r.Children.forEach(t,o=>{if(!r.isValidElement(o))throw new Error("If component only accepts valid React elements");const s=o.type;if(s.displayName===w.displayName){if(e)throw new Error("If component can only have one Then child");e=o}else if(s.displayName===F.displayName)a.push(o);else if(s.displayName===N.displayName){if(l)throw new Error("If component can only have one Else child");l=o}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(s.displayName||s.name||s)}`)}),n)return e?r.createElement(r.Fragment,null,e.props.children):null;for(const o of a)if(o.props.condition)return r.createElement(r.Fragment,null,o.props.children);return l?r.createElement(r.Fragment,null,l.props.children):null};p.displayName="If",p.Then=w,p.ElseIf=F,p.Else=N,p.createTyped=function(){return{If:p,Then:w,ElseIf:F,Else:N}};const W=({condition:n,children:t})=>n?r.createElement(r.Fragment,null,t):null,_=({condition:n,children:t})=>n===!1?r.createElement(r.Fragment,null,t):null,B=({all:n,any:t,none:e,children:l,fallback:a})=>h(()=>(n&&(t||e)&&console.warn('When: Multiple condition types (all, any, none) provided; "all" takes precedence.'),!!(n&&n.length>0&&n.every(Boolean)||t&&t.length>0&&t.some(Boolean)||e&&e.length>0&&e.every(o=>!o))),[n,t,e])?r.createElement(r.Fragment,null,l):r.createElement(r.Fragment,null,a||null),V=({data:n,transform:t,render:e,fallback:l})=>{const a=h(()=>t.reduce((o,s)=>s(o),n),[n,t]);return a==null?r.createElement(r.Fragment,null,l||null):r.createElement(r.Fragment,null,e(a))},Z=n=>{const{children:t,h:e,w:l,size:a,height:o,width:s,className:c}=n;return r.createElement("div",{style:{width:a||l||s,height:a||e||o,flexShrink:0},className:c},t)},$=({let:n,props:t,children:e,fallback:l})=>{const a=h(()=>typeof n=="function"?n(t):n,[n,t]);return!e||!Object.keys(a).length?r.createElement(r.Fragment,null,l||null):r.createElement(r.Fragment,null,e(a))};function v(...n){const t=new Set;for(const e of n)if(e){if(typeof e=="string")t.add(e);else if(Array.isArray(e))e.forEach(l=>t.add(l));else if(typeof e=="object")for(const[l,a]of Object.entries(e))a&&t.add(l)}return Array.from(t).join(" ")}const z=n=>{const{className:t,children:e,asWrapper:l}=n;if(C(()=>{I(e)===!1&&console.warn("<ClassName>: children is not a valid React element. Please check your code.")},[e]),!e)return null;if(!t)return r.createElement(M,null,e);const a=v(...Object.values(t));return l?r.createElement(typeof l=="string"?l:"div",{className:a},e):I(e)?R(e,{className:v(e.props.className,a)}):r.createElement(M,null,e)},L=n=>{const{index:t=0,options:e,next:l,render:a}=n;C(()=>{if(e.length<t+1)throw new Error(`Index ${t} is out of bounds for options array of length ${e.length}. Defaulting to first option.`)},[t,e]);const[o,s]=T(t),c=()=>{s(u=>e.length?l?l(u,e):(u+1)%e.length:u)};return a(e[o],c)};function q(n){const{items:t,renderItem:e,filter:l}=n;return t?r.createElement(M,null,t.map((a,o)=>l&&!l(a)?null:e(a,o))):(console.error("ArrayRender: items is null"),null)}function G({source:n,format:t,children:e}){const l=h(()=>{if(n instanceof Date)return n;if(typeof n=="string"||typeof n=="number"){const o=new Date(n);return isNaN(o.getTime())?null:o}return null},[n]),a=h(()=>l?t?t(l):l.toLocaleString():null,[l,t]);return!a||!e?null:r.createElement(r.Fragment,null,e(a))}const K="onChange",Q="value";function U(n){const{defaultValue:t,onBeforeChange:e,trigger:l=K,valuePropName:a=Q,props:o}=n,s=Object.prototype.hasOwnProperty.call(o,a),[c,u]=T(t),d=s?o[a]:c,i=h(()=>o[l],[o,l]),f=H(m=>{const y=typeof m=="function"?m(d):m;e&&e(y,d)===!1||(s||u(y),i&&i(y))},[s,e,d,i]);return[d,f]}function X(n,t){const e=t||new Date,l=e.getFullYear(),a=e.getMonth()+1,o=e.getDate(),s=e.getHours(),c=e.getMinutes(),u=e.getSeconds(),d=e.getMilliseconds(),i=e.getDay(),f=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],m=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],y=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],Y=["January","February","March","April","May","June","July","August","September","October","November","December"],x=m[i],D=f[i],b=a-1,k=Y[b],O=y[b],J={YY:l.toString().slice(2),YYYY:l.toString(),M:a.toString(),MM:a.toString().padStart(2,"0"),MMM:O,MMMM:k,D:o.toString(),DD:o.toString().padStart(2,"0"),d:i.toString(),dd:D,ddd:D,dddd:x,H:s.toString(),HH:s.toString().padStart(2,"0"),h:(s%12).toString(),hh:(s%12).toString().padStart(2,"0"),m:c.toString(),mm:c.toString().padStart(2,"0"),s:u.toString(),ss:u.toString().padStart(2,"0"),SSS:d.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:s<12?"AM":"PM",a:s<12?"am":"pm"};return n.replace(/YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|Z{1,2}|A|a/g,j=>J[j])}class ee{count=0;next(){return this.count++}}export{q as ArrayRender,z as ClassName,ee as Counter,G as DateRender,_ as False,p as If,V as Pipe,$ as Scope,Z as SizeBox,g as Switch,L as Toggle,W as True,B as When,A as childrenLoop,v as cx,X as formatDate,U as useControlled};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwog/react",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"author": "wwog",
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "unbuild",
|
|
19
19
|
"format": "biome format --write src",
|
|
20
|
-
"check": "biome check --
|
|
21
|
-
"test": "
|
|
22
|
-
"test:types": "tsc --noEmit --skipLibCheck"
|
|
20
|
+
"check": "biome check --write src",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:types": "tsc --noEmit --skipLibCheck",
|
|
23
|
+
"all-suites": "pnpm run format && pnpm run check && pnpm run test:types && pnpm run test"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@biomejs/biome": "^1.9.4",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { childrenLoop } from "
|
|
2
|
+
import { childrenLoop } from "../../utils/reactUtils";
|
|
3
3
|
|
|
4
4
|
export interface SwitchProps<T> {
|
|
5
5
|
value: T;
|
|
@@ -120,5 +120,5 @@ Switch.createTyped = function <T>() {
|
|
|
120
120
|
Switch: (props: SwitchProps<T>) => React.ReactElement | null;
|
|
121
121
|
Case: (props: SwitchCaseProps<T>) => React.ReactElement;
|
|
122
122
|
Default: (props: SwitchDefaultProps) => React.ReactElement;
|
|
123
|
-
}
|
|
123
|
+
};
|
|
124
124
|
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
cloneElement,
|
|
3
|
+
FC,
|
|
4
|
+
Fragment,
|
|
5
|
+
isValidElement,
|
|
6
|
+
useEffect,
|
|
7
|
+
type HTMLElementType,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { cx, type CxInput } from "../../utils/cx";
|
|
10
|
+
|
|
11
|
+
export interface ClassNameProps {
|
|
12
|
+
className?: {
|
|
13
|
+
base?: CxInput;
|
|
14
|
+
hover?: CxInput;
|
|
15
|
+
active?: CxInput;
|
|
16
|
+
focus?: CxInput;
|
|
17
|
+
disabled?: CxInput;
|
|
18
|
+
[key: string]: CxInput;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* @description 传入容器标签名.是否生成包含所有 `className` 的 `wrapper`, 默认 false, 传递 `true` 为 `div。`
|
|
22
|
+
* @description_en Whether to generate a `wrapper` containing all `className`, default is false, and pass the container tag name, if `true` will be `div`.
|
|
23
|
+
*/
|
|
24
|
+
asWrapper?: boolean | HTMLElementType;
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @description 用于将 `className` 分类编写的组件,内置了类似`clsx`的功能,并且去除重复的 className。
|
|
30
|
+
* @description_en A component for `className` classification, built-in similar to `clsx` functionality, and removes duplicate className.
|
|
31
|
+
* @component
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <ClassName className={{ base: "p-2 bg-red", hover: ["hover:bg-blue", { "hover:text-white": true }] }}>
|
|
35
|
+
* <button>Click me</button>
|
|
36
|
+
* </ClassName>
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```tsx
|
|
41
|
+
* <ClassName
|
|
42
|
+
* className={{
|
|
43
|
+
* base: ["p-2", { "bg-red": condition }],
|
|
44
|
+
* hover: { "hover:bg-blue": true },
|
|
45
|
+
* }}
|
|
46
|
+
* asWrapper="span"
|
|
47
|
+
* >
|
|
48
|
+
* <button>Click me</button>
|
|
49
|
+
* </ClassName>
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export const ClassName: FC<ClassNameProps> = (props) => {
|
|
53
|
+
const { className, children, asWrapper } = props;
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (isValidElement(children) === false) {
|
|
57
|
+
console.warn(
|
|
58
|
+
"<ClassName>: children is not a valid React element. Please check your code."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}, [children]);
|
|
62
|
+
|
|
63
|
+
if (!children) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!className) {
|
|
68
|
+
return <Fragment>{children}</Fragment>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const generatedCls = cx(...Object.values(className));
|
|
72
|
+
|
|
73
|
+
if (asWrapper) {
|
|
74
|
+
const Wrapper = typeof asWrapper === "string" ? asWrapper : "div";
|
|
75
|
+
return <Wrapper className={generatedCls}>{children}</Wrapper>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isValidElement(children)) {
|
|
79
|
+
return cloneElement(children, {
|
|
80
|
+
//@ts-expect-error type error
|
|
81
|
+
className: cx(children.props.className, generatedCls),
|
|
82
|
+
} as any);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return <Fragment>{children}</Fragment>;
|
|
86
|
+
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from
|
|
1
|
+
export * from './useControlled'
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { useCallback, useMemo, useState
|
|
1
|
+
import {type Dispatch, useCallback, useMemo, useState} from 'react'
|
|
2
2
|
|
|
3
|
-
const DEFAULT_TRIGGER =
|
|
4
|
-
const DEFAULT_VALUE_PROP_NAME =
|
|
3
|
+
const DEFAULT_TRIGGER = 'onChange'
|
|
4
|
+
const DEFAULT_VALUE_PROP_NAME = 'value'
|
|
5
5
|
|
|
6
6
|
export interface UseControlledOptions<T> {
|
|
7
7
|
/**
|
|
8
8
|
* @description - 非受控模式下的默认值,会被受控模式下的值覆盖
|
|
9
9
|
* @description_en - Default value in uncontrolled mode, will be overridden by the value in controlled mode
|
|
10
10
|
*/
|
|
11
|
-
defaultValue: T
|
|
11
|
+
defaultValue: T
|
|
12
12
|
/**
|
|
13
13
|
* @description - 值变更前的回调函数,可用于拦截或修改新值
|
|
14
14
|
* @description_en - Callback function before the value changes, can be used to intercept or modify the new value
|
|
15
15
|
*/
|
|
16
|
-
onBeforeChange?: (newValue: T, currentValue: T) => boolean | void
|
|
16
|
+
onBeforeChange?: (newValue: T, currentValue: T) => boolean | void
|
|
17
17
|
/**
|
|
18
18
|
* @description - 当值发生变化时触发的回调函数名
|
|
19
19
|
* @description_en - Callback function name triggered when the value changes
|
|
20
20
|
* @default - onChange
|
|
21
21
|
*/
|
|
22
|
-
trigger?: string
|
|
22
|
+
trigger?: string
|
|
23
23
|
/**
|
|
24
24
|
* @description - 值的属性名
|
|
25
25
|
* @description_en - Property name of the value
|
|
26
26
|
* @default - value
|
|
27
27
|
*/
|
|
28
|
-
valuePropName?: string
|
|
29
|
-
props: Record<string, any
|
|
28
|
+
valuePropName?: string
|
|
29
|
+
props: Record<string, any>
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function useControlled<T>(
|
|
33
|
-
options: UseControlledOptions<T
|
|
33
|
+
options: UseControlledOptions<T>,
|
|
34
34
|
): [T, Dispatch<React.SetStateAction<T>>] {
|
|
35
35
|
const {
|
|
36
36
|
defaultValue,
|
|
@@ -38,36 +38,31 @@ export function useControlled<T>(
|
|
|
38
38
|
trigger = DEFAULT_TRIGGER,
|
|
39
39
|
valuePropName = DEFAULT_VALUE_PROP_NAME,
|
|
40
40
|
props,
|
|
41
|
-
} = options
|
|
42
|
-
const isControlled = Object.prototype.hasOwnProperty.call(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
46
|
-
const [internalValue, setInternalValue] = useState<T>(defaultValue);
|
|
47
|
-
const value = isControlled ? props[valuePropName] : internalValue;
|
|
48
|
-
const onChange = useMemo(() => props[trigger], [props, trigger]);
|
|
41
|
+
} = options
|
|
42
|
+
const isControlled = Object.prototype.hasOwnProperty.call(props, valuePropName)
|
|
43
|
+
const [internalValue, setInternalValue] = useState<T>(defaultValue)
|
|
44
|
+
const value = isControlled ? props[valuePropName] : internalValue
|
|
45
|
+
const onChange = useMemo(() => props[trigger], [props, trigger])
|
|
49
46
|
|
|
50
47
|
const setValue = useCallback<Dispatch<React.SetStateAction<T>>>(
|
|
51
48
|
(newValue) => {
|
|
52
49
|
const resolvedValue =
|
|
53
|
-
typeof newValue ===
|
|
54
|
-
? (newValue as (prev: T) => T)(value)
|
|
55
|
-
: newValue;
|
|
50
|
+
typeof newValue === 'function' ? (newValue as (prev: T) => T)(value) : newValue
|
|
56
51
|
if (onBeforeChange) {
|
|
57
|
-
const shouldProceed = onBeforeChange(resolvedValue, value)
|
|
52
|
+
const shouldProceed = onBeforeChange(resolvedValue, value)
|
|
58
53
|
if (shouldProceed === false) {
|
|
59
|
-
return
|
|
54
|
+
return
|
|
60
55
|
}
|
|
61
56
|
}
|
|
62
57
|
if (!isControlled) {
|
|
63
|
-
setInternalValue(resolvedValue)
|
|
58
|
+
setInternalValue(resolvedValue)
|
|
64
59
|
}
|
|
65
60
|
if (onChange) {
|
|
66
|
-
onChange(resolvedValue)
|
|
61
|
+
onChange(resolvedValue)
|
|
67
62
|
}
|
|
68
63
|
},
|
|
69
|
-
[isControlled, onBeforeChange,
|
|
70
|
-
)
|
|
64
|
+
[isControlled, onBeforeChange, value, onChange],
|
|
65
|
+
)
|
|
71
66
|
|
|
72
|
-
return [value, setValue]
|
|
67
|
+
return [value, setValue]
|
|
73
68
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
|
|
1
|
+
export * from './components/ProcessControl'
|
|
2
|
+
export * from './components/Sundry'
|
|
3
|
+
export * from './components/Struct'
|
|
4
|
+
|
|
5
|
+
export * from './hooks'
|
|
6
|
+
|
|
7
|
+
export * from './utils/sundry'
|
|
8
|
+
export * from './utils/cx'
|
|
9
|
+
export * from './utils/reactUtils'
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
import {cx} from './cx'
|
|
3
|
+
|
|
4
|
+
describe('cx', () => {
|
|
5
|
+
it('应该正确合并字符串类名', () => {
|
|
6
|
+
expect(cx('foo', 'bar')).toBe('foo bar')
|
|
7
|
+
expect(cx('foo', 'bar', 'baz')).toBe('foo bar baz')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('应该过滤掉 falsy 值', () => {
|
|
11
|
+
expect(cx('foo', null, 'bar', undefined, false)).toBe('foo bar')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('应该正确处理数组', () => {
|
|
15
|
+
expect(cx(['foo', 'bar'])).toBe('foo bar')
|
|
16
|
+
expect(cx('baz', ['foo', 'bar'])).toBe('baz foo bar')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('应该正确处理对象', () => {
|
|
20
|
+
expect(cx({foo: true, bar: false})).toBe('foo')
|
|
21
|
+
expect(cx({foo: true, bar: true})).toBe('foo bar')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('应该正确处理混合输入', () => {
|
|
25
|
+
expect(cx('foo', ['bar', 'baz'], {qux: true, quux: false})).toBe('foo bar baz qux')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('应该去除重复的类名', () => {
|
|
29
|
+
expect(cx('foo', 'foo', 'bar')).toBe('foo bar')
|
|
30
|
+
expect(cx('foo', ['foo', 'bar'])).toBe('foo bar')
|
|
31
|
+
expect(cx('foo', {foo: true})).toBe('foo')
|
|
32
|
+
})
|
|
33
|
+
})
|
package/src/utils/cx.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type CxInput = string | string[] | Record<string, boolean> | undefined | null | false
|
|
2
|
+
|
|
3
|
+
export function cx(...args: CxInput[]): string {
|
|
4
|
+
const classes = new Set<string>()
|
|
5
|
+
|
|
6
|
+
for (const arg of args) {
|
|
7
|
+
if (!arg) {
|
|
8
|
+
continue
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (typeof arg === 'string') {
|
|
12
|
+
classes.add(arg)
|
|
13
|
+
} else if (Array.isArray(arg)) {
|
|
14
|
+
arg.forEach((item) => classes.add(item))
|
|
15
|
+
} else if (typeof arg === 'object') {
|
|
16
|
+
for (const [key, value] of Object.entries(arg)) {
|
|
17
|
+
if (value) {
|
|
18
|
+
classes.add(key)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return Array.from(classes).join(' ')
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
/**
|
|
3
|
+
* @description 性能优化,替代 React.Children.forEach, 回调可以返回 false 来中断循环
|
|
4
|
+
* @description_en Replace React.Children.forEach, the callback can return false to interrupt the loop
|
|
5
|
+
*/
|
|
6
|
+
export function childrenLoop(
|
|
7
|
+
children: React.ReactNode | undefined,
|
|
8
|
+
callback: (child: React.ReactNode, index: number) => boolean | void,
|
|
9
|
+
): void {
|
|
10
|
+
if (children === undefined) return
|
|
11
|
+
let index = 0
|
|
12
|
+
if (Array.isArray(children)) {
|
|
13
|
+
for (const child of children) {
|
|
14
|
+
const shouldContinue = callback(child, index++)
|
|
15
|
+
if (shouldContinue === false) {
|
|
16
|
+
break
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
callback(children, index)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
import {Counter, formatDate} from './sundry'
|
|
3
|
+
|
|
4
|
+
describe('formatDate', () => {
|
|
5
|
+
// 使用一个固定的日期来测试,以避免时间差异导致的测试失败
|
|
6
|
+
// 2023-04-15 14:30:45.678 星期六
|
|
7
|
+
const testDate = new Date(2023, 3, 15, 14, 30, 45, 678)
|
|
8
|
+
|
|
9
|
+
it('应该正确格式化年份', () => {
|
|
10
|
+
expect(formatDate('YY', testDate)).toBe('23')
|
|
11
|
+
expect(formatDate('YYYY', testDate)).toBe('2023')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('应该正确格式化月份', () => {
|
|
15
|
+
expect(formatDate('M', testDate)).toBe('4')
|
|
16
|
+
expect(formatDate('MM', testDate)).toBe('04')
|
|
17
|
+
expect(formatDate('MMM', testDate)).toBe('Apr')
|
|
18
|
+
expect(formatDate('MMMM', testDate)).toBe('April')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('应该正确格式化日期', () => {
|
|
22
|
+
expect(formatDate('D', testDate)).toBe('15')
|
|
23
|
+
expect(formatDate('DD', testDate)).toBe('15')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('应该正确格式化星期', () => {
|
|
27
|
+
expect(formatDate('d', testDate)).toBe('6')
|
|
28
|
+
expect(formatDate('dd', testDate)).toBe('Sat')
|
|
29
|
+
expect(formatDate('ddd', testDate)).toBe('Sat')
|
|
30
|
+
expect(formatDate('dddd', testDate)).toBe('Saturday')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('应该正确格式化小时', () => {
|
|
34
|
+
expect(formatDate('H', testDate)).toBe('14')
|
|
35
|
+
expect(formatDate('HH', testDate)).toBe('14')
|
|
36
|
+
expect(formatDate('h', testDate)).toBe('2')
|
|
37
|
+
expect(formatDate('hh', testDate)).toBe('02')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('应该正确格式化分钟和秒', () => {
|
|
41
|
+
expect(formatDate('m', testDate)).toBe('30')
|
|
42
|
+
expect(formatDate('mm', testDate)).toBe('30')
|
|
43
|
+
expect(formatDate('s', testDate)).toBe('45')
|
|
44
|
+
expect(formatDate('ss', testDate)).toBe('45')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('应该正确格式化毫秒', () => {
|
|
48
|
+
expect(formatDate('SSS', testDate)).toBe('678')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('应该正确格式化上午/下午', () => {
|
|
52
|
+
expect(formatDate('A', testDate)).toBe('PM')
|
|
53
|
+
expect(formatDate('a', testDate)).toBe('pm')
|
|
54
|
+
|
|
55
|
+
const morningDate = new Date(2023, 3, 15, 9, 30, 45, 678)
|
|
56
|
+
expect(formatDate('A', morningDate)).toBe('AM')
|
|
57
|
+
expect(formatDate('a', morningDate)).toBe('am')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('应该正确组合多种格式', () => {
|
|
61
|
+
expect(formatDate('YYYY-MM-DD', testDate)).toBe('2023-04-15')
|
|
62
|
+
expect(formatDate('YYYY/MM/DD HH:mm:ss', testDate)).toBe('2023/04/15 14:30:45')
|
|
63
|
+
expect(formatDate('YYYY年MM月DD日 HH时mm分ss秒', testDate)).toBe('2023年04月15日 14时30分45秒')
|
|
64
|
+
expect(formatDate('YY-MM-DD hh:mm:ss A', testDate)).toBe('23-04-15 02:30:45 PM')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('当不传入日期时应该使用当前日期', () => {
|
|
68
|
+
// 由于测试时间不确定,这里只测试格式是否正确,不测试具体的值
|
|
69
|
+
const result = formatDate('YYYY-MM-DD')
|
|
70
|
+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('Counter', () => {
|
|
75
|
+
it('应该从0开始计数', () => {
|
|
76
|
+
const counter = new Counter()
|
|
77
|
+
expect(counter.count).toBe(0)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('next() 方法应该返回当前计数并递增', () => {
|
|
81
|
+
const counter = new Counter()
|
|
82
|
+
expect(counter.next()).toBe(0)
|
|
83
|
+
expect(counter.count).toBe(1)
|
|
84
|
+
expect(counter.next()).toBe(1)
|
|
85
|
+
expect(counter.count).toBe(2)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('连续调用 next() 应该正确递增', () => {
|
|
89
|
+
const counter = new Counter()
|
|
90
|
+
expect(counter.next()).toBe(0)
|
|
91
|
+
expect(counter.next()).toBe(1)
|
|
92
|
+
expect(counter.next()).toBe(2)
|
|
93
|
+
expect(counter.next()).toBe(3)
|
|
94
|
+
expect(counter.count).toBe(4)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
/* describe("cx", () => {
|
|
99
|
+
it("应该正确合并字符串类名", () => {
|
|
100
|
+
expect(cx("foo", "bar")).toBe("foo bar");
|
|
101
|
+
expect(cx("foo", "bar", "baz")).toBe("foo bar baz");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("应该过滤掉 falsy 值", () => {
|
|
105
|
+
expect(cx("foo", null, "bar", undefined, false)).toBe("foo bar");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("应该正确处理数组", () => {
|
|
109
|
+
expect(cx(["foo", "bar"])).toBe("foo bar");
|
|
110
|
+
expect(cx("baz", ["foo", "bar"])).toBe("baz foo bar");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("应该正确处理对象", () => {
|
|
114
|
+
expect(cx({ foo: true, bar: false })).toBe("foo");
|
|
115
|
+
expect(cx({ foo: true, bar: true })).toBe("foo bar");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("应该正确处理混合输入", () => {
|
|
119
|
+
expect(cx("foo", ["bar", "baz"], { qux: true, quux: false })).toBe(
|
|
120
|
+
"foo bar baz qux"
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("应该去除重复的类名", () => {
|
|
125
|
+
expect(cx("foo", "foo", "bar")).toBe("foo bar");
|
|
126
|
+
expect(cx("foo", ["foo", "bar"])).toBe("foo bar");
|
|
127
|
+
expect(cx("foo", { foo: true })).toBe("foo");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
*/
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param schema
|
|
3
|
+
* @example
|
|
4
|
+
* YY | 18 | Two-digit year
|
|
5
|
+
* YYYY | 2018 | Four-digit year
|
|
6
|
+
* M | 1-12 | The month, beginning at 1
|
|
7
|
+
* MM | 01-12 | The month, 2-digits
|
|
8
|
+
* MMM | Jan-Dec | The abbreviated month name
|
|
9
|
+
* MMMM | January-December | The full month name
|
|
10
|
+
* D | 1-31 | The day of the month
|
|
11
|
+
* DD | 01-31 | The day of the month, 2-digits
|
|
12
|
+
* d | 0-6 | The day of the week, with Sunday as 0
|
|
13
|
+
* dd | Su-Sa | The min name of the day of the week
|
|
14
|
+
* ddd | Sun-Sat | The short name of the day of the week
|
|
15
|
+
* dddd | Sunday-Saturday | The name of the day of the week
|
|
16
|
+
* H | 0-23 | The hour
|
|
17
|
+
* HH | 00-23 | The hour, 2-digits
|
|
18
|
+
* h | 1-12 | The hour, 12-hour clock
|
|
19
|
+
* hh | 01-12 | The hour, 12-hour clock, 2-digits
|
|
20
|
+
* m | 0-59 | The minute
|
|
21
|
+
* mm | 00-59 | The minute, 2-digits
|
|
22
|
+
* s | 0-59 | The second
|
|
23
|
+
* ss | 00-59 | The second, 2-digits
|
|
24
|
+
* SSS | 000-999 | The millisecond, 3-digits
|
|
25
|
+
* Z | +05:00 | The offset from UTC, ±HH:mm
|
|
26
|
+
* ZZ | +0500 | The offset from UTC, ±HHmm
|
|
27
|
+
* A | AM | PM
|
|
28
|
+
* a | am | pm
|
|
29
|
+
*/
|
|
30
|
+
export function formatDate(schema: string, date?: Date): string {
|
|
31
|
+
const d = date || new Date()
|
|
32
|
+
const year = d.getFullYear()
|
|
33
|
+
const month = d.getMonth() + 1
|
|
34
|
+
const day = d.getDate()
|
|
35
|
+
const hour = d.getHours()
|
|
36
|
+
const minute = d.getMinutes()
|
|
37
|
+
const second = d.getSeconds()
|
|
38
|
+
const millisecond = d.getMilliseconds()
|
|
39
|
+
const week = d.getDay()
|
|
40
|
+
const weekName = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
41
|
+
const weekFullName = [
|
|
42
|
+
'Sunday',
|
|
43
|
+
'Monday',
|
|
44
|
+
'Tuesday',
|
|
45
|
+
'Wednesday',
|
|
46
|
+
'Thursday',
|
|
47
|
+
'Friday',
|
|
48
|
+
'Saturday',
|
|
49
|
+
]
|
|
50
|
+
const monthName = [
|
|
51
|
+
'Jan',
|
|
52
|
+
'Feb',
|
|
53
|
+
'Mar',
|
|
54
|
+
'Apr',
|
|
55
|
+
'May',
|
|
56
|
+
'Jun',
|
|
57
|
+
'Jul',
|
|
58
|
+
'Aug',
|
|
59
|
+
'Sep',
|
|
60
|
+
'Oct',
|
|
61
|
+
'Nov',
|
|
62
|
+
'Dec',
|
|
63
|
+
]
|
|
64
|
+
const monthFullName = [
|
|
65
|
+
'January',
|
|
66
|
+
'February',
|
|
67
|
+
'March',
|
|
68
|
+
'April',
|
|
69
|
+
'May',
|
|
70
|
+
'June',
|
|
71
|
+
'July',
|
|
72
|
+
'August',
|
|
73
|
+
'September',
|
|
74
|
+
'October',
|
|
75
|
+
'November',
|
|
76
|
+
'December',
|
|
77
|
+
]
|
|
78
|
+
// 直接使用 week 索引,不进行转换,因为 getDay() 已经返回了正确的星期索引 (0-6)
|
|
79
|
+
const weekFull = weekFullName[week]!
|
|
80
|
+
const weekShort = weekName[week]!
|
|
81
|
+
const monthIndex = month - 1
|
|
82
|
+
const monthFull = monthFullName[monthIndex]!
|
|
83
|
+
const monthShort = monthName[monthIndex]!
|
|
84
|
+
const map: Record<string, string> = {
|
|
85
|
+
YY: year.toString().slice(2),
|
|
86
|
+
YYYY: year.toString(),
|
|
87
|
+
M: month.toString(),
|
|
88
|
+
MM: month.toString().padStart(2, '0'),
|
|
89
|
+
MMM: monthShort,
|
|
90
|
+
MMMM: monthFull,
|
|
91
|
+
D: day.toString(),
|
|
92
|
+
DD: day.toString().padStart(2, '0'),
|
|
93
|
+
d: week.toString(),
|
|
94
|
+
dd: weekShort,
|
|
95
|
+
ddd: weekShort,
|
|
96
|
+
dddd: weekFull,
|
|
97
|
+
H: hour.toString(),
|
|
98
|
+
HH: hour.toString().padStart(2, '0'),
|
|
99
|
+
h: (hour % 12).toString(),
|
|
100
|
+
hh: (hour % 12).toString().padStart(2, '0'),
|
|
101
|
+
m: minute.toString(),
|
|
102
|
+
mm: minute.toString().padStart(2, '0'),
|
|
103
|
+
s: second.toString(),
|
|
104
|
+
ss: second.toString().padStart(2, '0'),
|
|
105
|
+
SSS: millisecond.toString().padStart(3, '0'),
|
|
106
|
+
Z: '+08:00',
|
|
107
|
+
ZZ: '+0800',
|
|
108
|
+
A: hour < 12 ? 'AM' : 'PM',
|
|
109
|
+
a: hour < 12 ? 'am' : 'pm',
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return schema.replace(
|
|
113
|
+
/YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|Z{1,2}|A|a/g,
|
|
114
|
+
(match) => {
|
|
115
|
+
return map[match]!
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class Counter {
|
|
121
|
+
count = 0
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @description 获取下一个计数值,不考虑越界。
|
|
125
|
+
* @description_en Get the next count value, without considering overflow.
|
|
126
|
+
*/
|
|
127
|
+
next() {
|
|
128
|
+
return this.count++
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/Common/index.ts
DELETED
package/src/utils/index.ts
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
/**
|
|
3
|
-
* @description 性能优化,替代 React.Children.forEach, 回调可以返回 false 来中断循环
|
|
4
|
-
* @description_en Replace React.Children.forEach, the callback can return false to interrupt the loop
|
|
5
|
-
*/
|
|
6
|
-
export function childrenLoop(
|
|
7
|
-
children: React.ReactNode | undefined,
|
|
8
|
-
callback: (child: React.ReactNode, index: number) => boolean | void
|
|
9
|
-
): void {
|
|
10
|
-
if (children === undefined) return;
|
|
11
|
-
let index = 0;
|
|
12
|
-
if (Array.isArray(children)) {
|
|
13
|
-
for (const child of children) {
|
|
14
|
-
const shouldContinue = callback(child, index++);
|
|
15
|
-
if (shouldContinue === false) {
|
|
16
|
-
break;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
} else {
|
|
20
|
-
callback(children, index);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param schema
|
|
26
|
-
* @example
|
|
27
|
-
* YY | 18 | Two-digit year
|
|
28
|
-
* YYYY | 2018 | Four-digit year
|
|
29
|
-
* M | 1-12 | The month, beginning at 1
|
|
30
|
-
* MM | 01-12 | The month, 2-digits
|
|
31
|
-
* MMM | Jan-Dec | The abbreviated month name
|
|
32
|
-
* MMMM | January-December | The full month name
|
|
33
|
-
* D | 1-31 | The day of the month
|
|
34
|
-
* DD | 01-31 | The day of the month, 2-digits
|
|
35
|
-
* d | 0-6 | The day of the week, with Sunday as 0
|
|
36
|
-
* dd | Su-Sa | The min name of the day of the week
|
|
37
|
-
* ddd | Sun-Sat | The short name of the day of the week
|
|
38
|
-
* dddd | Sunday-Saturday | The name of the day of the week
|
|
39
|
-
* H | 0-23 | The hour
|
|
40
|
-
* HH | 00-23 | The hour, 2-digits
|
|
41
|
-
* h | 1-12 | The hour, 12-hour clock
|
|
42
|
-
* hh | 01-12 | The hour, 12-hour clock, 2-digits
|
|
43
|
-
* m | 0-59 | The minute
|
|
44
|
-
* mm | 00-59 | The minute, 2-digits
|
|
45
|
-
* s | 0-59 | The second
|
|
46
|
-
* ss | 00-59 | The second, 2-digits
|
|
47
|
-
* SSS | 000-999 | The millisecond, 3-digits
|
|
48
|
-
* Z | +05:00 | The offset from UTC, ±HH:mm
|
|
49
|
-
* ZZ | +0500 | The offset from UTC, ±HHmm
|
|
50
|
-
* A | AM | PM
|
|
51
|
-
* a | am | pm
|
|
52
|
-
*/
|
|
53
|
-
export function formatDate(schema: string, date?: Date): string {
|
|
54
|
-
const d = date || new Date();
|
|
55
|
-
const year = d.getFullYear();
|
|
56
|
-
const month = d.getMonth() + 1;
|
|
57
|
-
const day = d.getDate();
|
|
58
|
-
const hour = d.getHours();
|
|
59
|
-
const minute = d.getMinutes();
|
|
60
|
-
const second = d.getSeconds();
|
|
61
|
-
const millisecond = d.getMilliseconds();
|
|
62
|
-
const week = d.getDay();
|
|
63
|
-
const weekName = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
64
|
-
const weekFullName = [
|
|
65
|
-
"Sunday",
|
|
66
|
-
"Monday",
|
|
67
|
-
"Tuesday",
|
|
68
|
-
"Wednesday",
|
|
69
|
-
"Thursday",
|
|
70
|
-
"Friday",
|
|
71
|
-
"Saturday",
|
|
72
|
-
];
|
|
73
|
-
const monthName = [
|
|
74
|
-
"Jan",
|
|
75
|
-
"Feb",
|
|
76
|
-
"Mar",
|
|
77
|
-
"Apr",
|
|
78
|
-
"May",
|
|
79
|
-
"Jun",
|
|
80
|
-
"Jul",
|
|
81
|
-
"Aug",
|
|
82
|
-
"Sep",
|
|
83
|
-
"Oct",
|
|
84
|
-
"Nov",
|
|
85
|
-
"Dec",
|
|
86
|
-
];
|
|
87
|
-
const monthFullName = [
|
|
88
|
-
"January",
|
|
89
|
-
"February",
|
|
90
|
-
"March",
|
|
91
|
-
"April",
|
|
92
|
-
"May",
|
|
93
|
-
"June",
|
|
94
|
-
"July",
|
|
95
|
-
"August",
|
|
96
|
-
"September",
|
|
97
|
-
"October",
|
|
98
|
-
"November",
|
|
99
|
-
"December",
|
|
100
|
-
];
|
|
101
|
-
const weekIndex = week === 0 ? 6 : week - 1;
|
|
102
|
-
const weekFull = weekFullName[weekIndex]!;
|
|
103
|
-
const weekShort = weekName[weekIndex]!;
|
|
104
|
-
const monthIndex = month - 1;
|
|
105
|
-
const monthFull = monthFullName[monthIndex]!;
|
|
106
|
-
const monthShort = monthName[monthIndex]!;
|
|
107
|
-
const map: Record<string, string> = {
|
|
108
|
-
YY: year.toString().slice(2),
|
|
109
|
-
YYYY: year.toString(),
|
|
110
|
-
M: month.toString(),
|
|
111
|
-
MM: month.toString().padStart(2, "0"),
|
|
112
|
-
MMM: monthShort,
|
|
113
|
-
MMMM: monthFull,
|
|
114
|
-
D: day.toString(),
|
|
115
|
-
DD: day.toString().padStart(2, "0"),
|
|
116
|
-
d: week.toString(),
|
|
117
|
-
dd: weekShort,
|
|
118
|
-
ddd: weekShort,
|
|
119
|
-
dddd: weekFull,
|
|
120
|
-
H: hour.toString(),
|
|
121
|
-
HH: hour.toString().padStart(2, "0"),
|
|
122
|
-
h: (hour % 12).toString(),
|
|
123
|
-
hh: (hour % 12).toString().padStart(2, "0"),
|
|
124
|
-
m: minute.toString(),
|
|
125
|
-
mm: minute.toString().padStart(2, "0"),
|
|
126
|
-
s: second.toString(),
|
|
127
|
-
ss: second.toString().padStart(2, "0"),
|
|
128
|
-
SSS: millisecond.toString().padStart(3, "0"),
|
|
129
|
-
Z: "+08:00",
|
|
130
|
-
ZZ: "+0800",
|
|
131
|
-
A: hour < 12 ? "AM" : "PM",
|
|
132
|
-
a: hour < 12 ? "am" : "pm",
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
return schema.replace(
|
|
136
|
-
/YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|Z{1,2}|A|a/g,
|
|
137
|
-
(match) => {
|
|
138
|
-
return map[match]!;
|
|
139
|
-
}
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export class Counter {
|
|
144
|
-
count = 0;
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* @description 获取下一个计数值,不考虑越界。
|
|
148
|
-
* @description_en Get the next count value, without considering overflow.
|
|
149
|
-
*/
|
|
150
|
-
next() {
|
|
151
|
-
return this.count++;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|