deepsea-components 5.15.21 → 5.15.23
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 +23 -23
- package/dist/components/AutoFit.cjs +7 -7
- package/dist/components/AutoFit.js +7 -7
- package/dist/components/AutoScroll.cjs +7 -7
- package/dist/components/AutoScroll.js +7 -7
- package/dist/components/AutoSizeTextarea.cjs +4 -4
- package/dist/components/AutoSizeTextarea.js +4 -4
- package/dist/components/Flow.cjs +16 -16
- package/dist/components/Flow.js +16 -16
- package/dist/components/FormLabel.cjs +1 -1
- package/dist/components/FormLabel.js +1 -1
- package/dist/components/InfiniteScroll.cjs +13 -13
- package/dist/components/InfiniteScroll.js +13 -13
- package/dist/components/LoopSwiper.cjs +33 -33
- package/dist/components/LoopSwiper.js +33 -33
- package/dist/components/Scroll.cjs +13 -13
- package/dist/components/Scroll.js +13 -13
- package/dist/components/ScrollMask.module.cjs +2 -2
- package/dist/components/ScrollMask.module.js +2 -2
- package/dist/components/Skeleton.cjs +17 -17
- package/dist/components/Skeleton.js +17 -17
- package/dist/components/TransitionNum.cjs +1 -1
- package/dist/components/TransitionNum.js +1 -1
- package/dist/components/Trapezium.cjs +4 -4
- package/dist/components/Trapezium.js +4 -4
- package/package.json +4 -3
- package/src/components/AutoFit.tsx +104 -104
- package/src/components/AutoScroll.tsx +150 -150
- package/src/components/AutoSizeTextarea.tsx +50 -50
- package/src/components/CircleText.tsx +81 -81
- package/src/components/CopyButton.tsx +31 -31
- package/src/components/Echart.tsx +69 -69
- package/src/components/Flow.tsx +271 -271
- package/src/components/FormLabel.tsx +41 -41
- package/src/components/HlsPlayer.tsx +34 -34
- package/src/components/IconFileType.tsx +162 -162
- package/src/components/InfiniteScroll.tsx +163 -163
- package/src/components/InputFile.tsx +93 -93
- package/src/components/InputFileButton.tsx +122 -122
- package/src/components/LoopSwiper.tsx +125 -125
- package/src/components/ReadExcel.tsx +13 -13
- package/src/components/ReadSheet.tsx +17 -17
- package/src/components/Ring.tsx +30 -30
- package/src/components/Scroll.tsx +97 -97
- package/src/components/ScrollMask.module.css +87 -87
- package/src/components/ScrollMask.tsx +68 -68
- package/src/components/SectionRing.tsx +40 -40
- package/src/components/Skeleton.tsx +45 -45
- package/src/components/Title.tsx +27 -27
- package/src/components/TransitionBox.tsx +44 -44
- package/src/components/TransitionNum.tsx +54 -54
- package/src/components/Trapezium.tsx +60 -60
- package/src/components/Unify.tsx +38 -38
- package/src/components/WriteExcel.tsx +13 -13
- package/src/components/WriteSheet.tsx +25 -25
- package/src/utils/getReactVersion.ts +7 -7
- package/src/utils/index.ts +33 -33
- package/tsconfig.json +16 -16
|
@@ -1,163 +1,163 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { css } from "@emotion/css"
|
|
4
|
-
import { clsx } from "deepsea-tools"
|
|
5
|
-
import { CSSProperties, ComponentPropsWithoutRef, MouseEvent as ReactMouseEvent, forwardRef, useEffect, useImperativeHandle, useRef } from "react"
|
|
6
|
-
import { useSize } from "soda-hooks"
|
|
7
|
-
|
|
8
|
-
export type InfiniteScrollProps = ComponentPropsWithoutRef<"div"> & {
|
|
9
|
-
/**
|
|
10
|
-
* 滚动的方向
|
|
11
|
-
* @default "vertical"
|
|
12
|
-
*/
|
|
13
|
-
direction?: "vertical" | "horizontal"
|
|
14
|
-
/** 渲染 children 的容器的类名 */
|
|
15
|
-
containerClassName?: string
|
|
16
|
-
/** 渲染 children 的容器的样式 */
|
|
17
|
-
containerStyle?: CSSProperties
|
|
18
|
-
/**
|
|
19
|
-
* 两个 children 容器之间的间距
|
|
20
|
-
* @default 0
|
|
21
|
-
*/
|
|
22
|
-
gap?: number
|
|
23
|
-
/** 一个周期的时间,单位:毫秒 */
|
|
24
|
-
duration: number
|
|
25
|
-
/** 是否将 gap 的距离也加入 overflow 的计算 */
|
|
26
|
-
withGap?: boolean
|
|
27
|
-
/** 尺寸刚好相同时,是否视为溢出 */
|
|
28
|
-
withEqual?: boolean
|
|
29
|
-
/** 鼠标移入时,是否停止动画 */
|
|
30
|
-
pauseOnHover?: boolean
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 无限滚动的组件
|
|
35
|
-
* 子元素会内置的容器中被渲染两次,首尾相连,形成无限滚动的效果
|
|
36
|
-
* 但是,如果内部检测到并没有溢出,那么不会渲染两次,并且没有动画
|
|
37
|
-
*/
|
|
38
|
-
export const InfiniteScroll = forwardRef<HTMLDivElement, InfiniteScrollProps>((props, ref) => {
|
|
39
|
-
const {
|
|
40
|
-
className,
|
|
41
|
-
direction = "vertical",
|
|
42
|
-
children,
|
|
43
|
-
containerClassName,
|
|
44
|
-
containerStyle,
|
|
45
|
-
gap = 0,
|
|
46
|
-
duration,
|
|
47
|
-
withGap,
|
|
48
|
-
withEqual,
|
|
49
|
-
pauseOnHover,
|
|
50
|
-
onMouseEnter,
|
|
51
|
-
onMouseLeave,
|
|
52
|
-
...rest
|
|
53
|
-
} = props
|
|
54
|
-
|
|
55
|
-
const wrapper = useRef<HTMLDivElement>(null)
|
|
56
|
-
const wrapperSize = useSize(wrapper)
|
|
57
|
-
|
|
58
|
-
const ele = useRef<HTMLDivElement>(null)
|
|
59
|
-
|
|
60
|
-
useImperativeHandle(ref, () => wrapper.current as HTMLDivElement, [wrapper.current])
|
|
61
|
-
|
|
62
|
-
const container = useRef<HTMLDivElement>(null)
|
|
63
|
-
const containerSize = useSize(container)
|
|
64
|
-
|
|
65
|
-
function bigger(containerSize: number, wrapperSize: number) {
|
|
66
|
-
return withEqual ? containerSize + (withGap ? gap : 0) >= wrapperSize : containerSize + (withGap ? gap : 0) > wrapperSize
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const overflow =
|
|
70
|
-
wrapperSize && containerSize
|
|
71
|
-
? direction === "vertical"
|
|
72
|
-
? bigger(containerSize.height, wrapperSize.height)
|
|
73
|
-
: bigger(containerSize.width, wrapperSize.width)
|
|
74
|
-
: false
|
|
75
|
-
|
|
76
|
-
const animation = useRef<Animation | undefined>(undefined)
|
|
77
|
-
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
if (!wrapperSize || !containerSize || !overflow) return
|
|
80
|
-
animation.current = ele.current?.animate(
|
|
81
|
-
{
|
|
82
|
-
transform:
|
|
83
|
-
direction === "vertical"
|
|
84
|
-
? [`translateY(0)`, `translateY(-${containerSize.height + gap}px)`]
|
|
85
|
-
: [`translateX(0)`, `translateX(-${containerSize.width + gap}px)`],
|
|
86
|
-
},
|
|
87
|
-
{ duration, iterations: Infinity },
|
|
88
|
-
)
|
|
89
|
-
return () => animation.current?.cancel()
|
|
90
|
-
}, [wrapperSize, containerSize, overflow, direction, duration])
|
|
91
|
-
|
|
92
|
-
function enter(e: ReactMouseEvent<HTMLDivElement, MouseEvent>) {
|
|
93
|
-
if (pauseOnHover) animation.current?.pause()
|
|
94
|
-
onMouseEnter?.(e)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function leave(e: ReactMouseEvent<HTMLDivElement, MouseEvent>) {
|
|
98
|
-
if (pauseOnHover) animation.current?.play()
|
|
99
|
-
onMouseLeave?.(e)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
if (!pauseOnHover && animation.current?.playState === "paused") animation.current?.play()
|
|
104
|
-
}, [animation.current, pauseOnHover])
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<div
|
|
108
|
-
ref={wrapper}
|
|
109
|
-
className={clsx(
|
|
110
|
-
css`
|
|
111
|
-
position: relative;
|
|
112
|
-
${direction === "vertical" ? "overflow-y: hidden;" : "overflow-x: hidden;"}
|
|
113
|
-
`,
|
|
114
|
-
className,
|
|
115
|
-
)}
|
|
116
|
-
onMouseEnter={enter}
|
|
117
|
-
onMouseLeave={leave}
|
|
118
|
-
{...rest}
|
|
119
|
-
>
|
|
120
|
-
<div
|
|
121
|
-
ref={ele}
|
|
122
|
-
style={{
|
|
123
|
-
position: "absolute",
|
|
124
|
-
width: direction === "vertical" ? "100%" : containerSize && (overflow ? containerSize.width * 2 + gap : containerSize.width),
|
|
125
|
-
height: direction === "horizontal" ? "100%" : containerSize && (overflow ? containerSize.height * 2 + gap : containerSize.width),
|
|
126
|
-
}}
|
|
127
|
-
>
|
|
128
|
-
<div
|
|
129
|
-
ref={container}
|
|
130
|
-
className={clsx(
|
|
131
|
-
css`
|
|
132
|
-
position: absolute;
|
|
133
|
-
left: 0;
|
|
134
|
-
top: 0;
|
|
135
|
-
${direction === "vertical" ? "width: 100%;" : "height: 100%;"}
|
|
136
|
-
`,
|
|
137
|
-
containerClassName,
|
|
138
|
-
)}
|
|
139
|
-
style={containerStyle}
|
|
140
|
-
>
|
|
141
|
-
{children}
|
|
142
|
-
</div>
|
|
143
|
-
{overflow && (
|
|
144
|
-
<div
|
|
145
|
-
ref={container}
|
|
146
|
-
className={clsx(
|
|
147
|
-
css`
|
|
148
|
-
position: absolute;
|
|
149
|
-
right: 0;
|
|
150
|
-
bottom: 0;
|
|
151
|
-
${direction === "vertical" ? "width: 100%;" : "height: 100%;"}
|
|
152
|
-
`,
|
|
153
|
-
containerClassName,
|
|
154
|
-
)}
|
|
155
|
-
style={containerStyle}
|
|
156
|
-
>
|
|
157
|
-
{children}
|
|
158
|
-
</div>
|
|
159
|
-
)}
|
|
160
|
-
</div>
|
|
161
|
-
</div>
|
|
162
|
-
)
|
|
163
|
-
})
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { css } from "@emotion/css"
|
|
4
|
+
import { clsx } from "deepsea-tools"
|
|
5
|
+
import { CSSProperties, ComponentPropsWithoutRef, MouseEvent as ReactMouseEvent, forwardRef, useEffect, useImperativeHandle, useRef } from "react"
|
|
6
|
+
import { useSize } from "soda-hooks"
|
|
7
|
+
|
|
8
|
+
export type InfiniteScrollProps = ComponentPropsWithoutRef<"div"> & {
|
|
9
|
+
/**
|
|
10
|
+
* 滚动的方向
|
|
11
|
+
* @default "vertical"
|
|
12
|
+
*/
|
|
13
|
+
direction?: "vertical" | "horizontal"
|
|
14
|
+
/** 渲染 children 的容器的类名 */
|
|
15
|
+
containerClassName?: string
|
|
16
|
+
/** 渲染 children 的容器的样式 */
|
|
17
|
+
containerStyle?: CSSProperties
|
|
18
|
+
/**
|
|
19
|
+
* 两个 children 容器之间的间距
|
|
20
|
+
* @default 0
|
|
21
|
+
*/
|
|
22
|
+
gap?: number
|
|
23
|
+
/** 一个周期的时间,单位:毫秒 */
|
|
24
|
+
duration: number
|
|
25
|
+
/** 是否将 gap 的距离也加入 overflow 的计算 */
|
|
26
|
+
withGap?: boolean
|
|
27
|
+
/** 尺寸刚好相同时,是否视为溢出 */
|
|
28
|
+
withEqual?: boolean
|
|
29
|
+
/** 鼠标移入时,是否停止动画 */
|
|
30
|
+
pauseOnHover?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 无限滚动的组件
|
|
35
|
+
* 子元素会内置的容器中被渲染两次,首尾相连,形成无限滚动的效果
|
|
36
|
+
* 但是,如果内部检测到并没有溢出,那么不会渲染两次,并且没有动画
|
|
37
|
+
*/
|
|
38
|
+
export const InfiniteScroll = forwardRef<HTMLDivElement, InfiniteScrollProps>((props, ref) => {
|
|
39
|
+
const {
|
|
40
|
+
className,
|
|
41
|
+
direction = "vertical",
|
|
42
|
+
children,
|
|
43
|
+
containerClassName,
|
|
44
|
+
containerStyle,
|
|
45
|
+
gap = 0,
|
|
46
|
+
duration,
|
|
47
|
+
withGap,
|
|
48
|
+
withEqual,
|
|
49
|
+
pauseOnHover,
|
|
50
|
+
onMouseEnter,
|
|
51
|
+
onMouseLeave,
|
|
52
|
+
...rest
|
|
53
|
+
} = props
|
|
54
|
+
|
|
55
|
+
const wrapper = useRef<HTMLDivElement>(null)
|
|
56
|
+
const wrapperSize = useSize(wrapper)
|
|
57
|
+
|
|
58
|
+
const ele = useRef<HTMLDivElement>(null)
|
|
59
|
+
|
|
60
|
+
useImperativeHandle(ref, () => wrapper.current as HTMLDivElement, [wrapper.current])
|
|
61
|
+
|
|
62
|
+
const container = useRef<HTMLDivElement>(null)
|
|
63
|
+
const containerSize = useSize(container)
|
|
64
|
+
|
|
65
|
+
function bigger(containerSize: number, wrapperSize: number) {
|
|
66
|
+
return withEqual ? containerSize + (withGap ? gap : 0) >= wrapperSize : containerSize + (withGap ? gap : 0) > wrapperSize
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const overflow =
|
|
70
|
+
wrapperSize && containerSize
|
|
71
|
+
? direction === "vertical"
|
|
72
|
+
? bigger(containerSize.height, wrapperSize.height)
|
|
73
|
+
: bigger(containerSize.width, wrapperSize.width)
|
|
74
|
+
: false
|
|
75
|
+
|
|
76
|
+
const animation = useRef<Animation | undefined>(undefined)
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!wrapperSize || !containerSize || !overflow) return
|
|
80
|
+
animation.current = ele.current?.animate(
|
|
81
|
+
{
|
|
82
|
+
transform:
|
|
83
|
+
direction === "vertical"
|
|
84
|
+
? [`translateY(0)`, `translateY(-${containerSize.height + gap}px)`]
|
|
85
|
+
: [`translateX(0)`, `translateX(-${containerSize.width + gap}px)`],
|
|
86
|
+
},
|
|
87
|
+
{ duration, iterations: Infinity },
|
|
88
|
+
)
|
|
89
|
+
return () => animation.current?.cancel()
|
|
90
|
+
}, [wrapperSize, containerSize, overflow, direction, duration])
|
|
91
|
+
|
|
92
|
+
function enter(e: ReactMouseEvent<HTMLDivElement, MouseEvent>) {
|
|
93
|
+
if (pauseOnHover) animation.current?.pause()
|
|
94
|
+
onMouseEnter?.(e)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function leave(e: ReactMouseEvent<HTMLDivElement, MouseEvent>) {
|
|
98
|
+
if (pauseOnHover) animation.current?.play()
|
|
99
|
+
onMouseLeave?.(e)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!pauseOnHover && animation.current?.playState === "paused") animation.current?.play()
|
|
104
|
+
}, [animation.current, pauseOnHover])
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div
|
|
108
|
+
ref={wrapper}
|
|
109
|
+
className={clsx(
|
|
110
|
+
css`
|
|
111
|
+
position: relative;
|
|
112
|
+
${direction === "vertical" ? "overflow-y: hidden;" : "overflow-x: hidden;"}
|
|
113
|
+
`,
|
|
114
|
+
className,
|
|
115
|
+
)}
|
|
116
|
+
onMouseEnter={enter}
|
|
117
|
+
onMouseLeave={leave}
|
|
118
|
+
{...rest}
|
|
119
|
+
>
|
|
120
|
+
<div
|
|
121
|
+
ref={ele}
|
|
122
|
+
style={{
|
|
123
|
+
position: "absolute",
|
|
124
|
+
width: direction === "vertical" ? "100%" : containerSize && (overflow ? containerSize.width * 2 + gap : containerSize.width),
|
|
125
|
+
height: direction === "horizontal" ? "100%" : containerSize && (overflow ? containerSize.height * 2 + gap : containerSize.width),
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
<div
|
|
129
|
+
ref={container}
|
|
130
|
+
className={clsx(
|
|
131
|
+
css`
|
|
132
|
+
position: absolute;
|
|
133
|
+
left: 0;
|
|
134
|
+
top: 0;
|
|
135
|
+
${direction === "vertical" ? "width: 100%;" : "height: 100%;"}
|
|
136
|
+
`,
|
|
137
|
+
containerClassName,
|
|
138
|
+
)}
|
|
139
|
+
style={containerStyle}
|
|
140
|
+
>
|
|
141
|
+
{children}
|
|
142
|
+
</div>
|
|
143
|
+
{overflow && (
|
|
144
|
+
<div
|
|
145
|
+
ref={container}
|
|
146
|
+
className={clsx(
|
|
147
|
+
css`
|
|
148
|
+
position: absolute;
|
|
149
|
+
right: 0;
|
|
150
|
+
bottom: 0;
|
|
151
|
+
${direction === "vertical" ? "width: 100%;" : "height: 100%;"}
|
|
152
|
+
`,
|
|
153
|
+
containerClassName,
|
|
154
|
+
)}
|
|
155
|
+
style={containerStyle}
|
|
156
|
+
>
|
|
157
|
+
{children}
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
})
|
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { ChangeEvent, ComponentProps, ReactNode, useState } from "react"
|
|
4
|
-
|
|
5
|
-
export interface InputFileDataTypeMap {
|
|
6
|
-
base64: string
|
|
7
|
-
text: string
|
|
8
|
-
arrayBuffer: ArrayBuffer
|
|
9
|
-
binary: string
|
|
10
|
-
file: File
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type InputFileDataType = keyof InputFileDataTypeMap
|
|
14
|
-
|
|
15
|
-
export async function getFileData<T extends InputFileDataType>(file: File, type: T): Promise<InputFileDataTypeMap[T]> {
|
|
16
|
-
const fileReader = new FileReader()
|
|
17
|
-
switch (type) {
|
|
18
|
-
case "arrayBuffer":
|
|
19
|
-
fileReader.readAsArrayBuffer(file)
|
|
20
|
-
break
|
|
21
|
-
case "binary":
|
|
22
|
-
fileReader.readAsBinaryString(file)
|
|
23
|
-
break
|
|
24
|
-
case "base64":
|
|
25
|
-
fileReader.readAsDataURL(file)
|
|
26
|
-
break
|
|
27
|
-
case "text":
|
|
28
|
-
fileReader.readAsText(file)
|
|
29
|
-
break
|
|
30
|
-
default:
|
|
31
|
-
return file as any
|
|
32
|
-
}
|
|
33
|
-
return new Promise(resolve => {
|
|
34
|
-
fileReader.addEventListener("load", () => {
|
|
35
|
-
resolve(fileReader.result as any)
|
|
36
|
-
})
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface InputFileBaseProps extends Omit<ComponentProps<"input">, "multiple" | "type"> {}
|
|
41
|
-
|
|
42
|
-
export type FileType<Multiple extends boolean> = Multiple extends true ? File[] : File
|
|
43
|
-
export type ValueType<Multiple extends boolean, Type extends InputFileDataType> = Multiple extends true
|
|
44
|
-
? InputFileDataTypeMap[Type][]
|
|
45
|
-
: InputFileDataTypeMap[Type]
|
|
46
|
-
|
|
47
|
-
export interface InputFileExtraProps<Multiple extends boolean = false, Type extends InputFileDataType = "file"> {
|
|
48
|
-
multiple?: Multiple
|
|
49
|
-
type?: Type
|
|
50
|
-
onValueChange?: (data: ValueType<Multiple, Type>) => void
|
|
51
|
-
onFileChange?: (data: FileType<Multiple>) => void
|
|
52
|
-
/** 是否在捕获文件后清除 input 上的文件,默认为 false,主要区别在于如果不清除,连续两次选择同样的文件不会触发 onChange 事件,如果用于 form 表单,请设置为 flase */
|
|
53
|
-
clearAfterChange?: boolean
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface InputFileProps<Multiple extends boolean = false, Type extends InputFileDataType = "file">
|
|
57
|
-
extends InputFileBaseProps,
|
|
58
|
-
InputFileExtraProps<Multiple, Type> {}
|
|
59
|
-
|
|
60
|
-
/** 专用于读取文件的组件 */
|
|
61
|
-
export function InputFile<Multiple extends boolean = false, Type extends InputFileDataType = "file">(props: InputFileProps<Multiple, Type>): ReactNode {
|
|
62
|
-
const { multiple = false, type = "file", onChange: _onChange, disabled: _disabled, clearAfterChange, onValueChange, onFileChange, ...rest } = props
|
|
63
|
-
const [disabled, setDisabled] = useState(false)
|
|
64
|
-
|
|
65
|
-
async function onChange(e: ChangeEvent<HTMLInputElement>) {
|
|
66
|
-
_onChange?.(e)
|
|
67
|
-
const input = e.target
|
|
68
|
-
const { files } = input
|
|
69
|
-
if (!files || files.length === 0) return
|
|
70
|
-
setDisabled(true)
|
|
71
|
-
try {
|
|
72
|
-
if (multiple) {
|
|
73
|
-
const files2: File[] = Array.from(files)
|
|
74
|
-
const values: InputFileDataTypeMap[Type][] = []
|
|
75
|
-
for (const file of files2) {
|
|
76
|
-
const value = (await getFileData(file, type)) as InputFileDataTypeMap[Type]
|
|
77
|
-
values.push(value)
|
|
78
|
-
}
|
|
79
|
-
onFileChange?.(files2 as FileType<Multiple>)
|
|
80
|
-
onValueChange?.(values as ValueType<Multiple, Type>)
|
|
81
|
-
} else {
|
|
82
|
-
const file = files[0]
|
|
83
|
-
onFileChange?.(file as FileType<Multiple>)
|
|
84
|
-
onValueChange?.((await getFileData(file, type)) as ValueType<Multiple, Type>)
|
|
85
|
-
}
|
|
86
|
-
} finally {
|
|
87
|
-
if (clearAfterChange) input.value = ""
|
|
88
|
-
setDisabled(false)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return <input disabled={disabled || _disabled} type="file" multiple={multiple} onChange={onChange} {...rest} />
|
|
93
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { ChangeEvent, ComponentProps, ReactNode, useState } from "react"
|
|
4
|
+
|
|
5
|
+
export interface InputFileDataTypeMap {
|
|
6
|
+
base64: string
|
|
7
|
+
text: string
|
|
8
|
+
arrayBuffer: ArrayBuffer
|
|
9
|
+
binary: string
|
|
10
|
+
file: File
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type InputFileDataType = keyof InputFileDataTypeMap
|
|
14
|
+
|
|
15
|
+
export async function getFileData<T extends InputFileDataType>(file: File, type: T): Promise<InputFileDataTypeMap[T]> {
|
|
16
|
+
const fileReader = new FileReader()
|
|
17
|
+
switch (type) {
|
|
18
|
+
case "arrayBuffer":
|
|
19
|
+
fileReader.readAsArrayBuffer(file)
|
|
20
|
+
break
|
|
21
|
+
case "binary":
|
|
22
|
+
fileReader.readAsBinaryString(file)
|
|
23
|
+
break
|
|
24
|
+
case "base64":
|
|
25
|
+
fileReader.readAsDataURL(file)
|
|
26
|
+
break
|
|
27
|
+
case "text":
|
|
28
|
+
fileReader.readAsText(file)
|
|
29
|
+
break
|
|
30
|
+
default:
|
|
31
|
+
return file as any
|
|
32
|
+
}
|
|
33
|
+
return new Promise(resolve => {
|
|
34
|
+
fileReader.addEventListener("load", () => {
|
|
35
|
+
resolve(fileReader.result as any)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface InputFileBaseProps extends Omit<ComponentProps<"input">, "multiple" | "type"> {}
|
|
41
|
+
|
|
42
|
+
export type FileType<Multiple extends boolean> = Multiple extends true ? File[] : File
|
|
43
|
+
export type ValueType<Multiple extends boolean, Type extends InputFileDataType> = Multiple extends true
|
|
44
|
+
? InputFileDataTypeMap[Type][]
|
|
45
|
+
: InputFileDataTypeMap[Type]
|
|
46
|
+
|
|
47
|
+
export interface InputFileExtraProps<Multiple extends boolean = false, Type extends InputFileDataType = "file"> {
|
|
48
|
+
multiple?: Multiple
|
|
49
|
+
type?: Type
|
|
50
|
+
onValueChange?: (data: ValueType<Multiple, Type>) => void
|
|
51
|
+
onFileChange?: (data: FileType<Multiple>) => void
|
|
52
|
+
/** 是否在捕获文件后清除 input 上的文件,默认为 false,主要区别在于如果不清除,连续两次选择同样的文件不会触发 onChange 事件,如果用于 form 表单,请设置为 flase */
|
|
53
|
+
clearAfterChange?: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface InputFileProps<Multiple extends boolean = false, Type extends InputFileDataType = "file">
|
|
57
|
+
extends InputFileBaseProps,
|
|
58
|
+
InputFileExtraProps<Multiple, Type> {}
|
|
59
|
+
|
|
60
|
+
/** 专用于读取文件的组件 */
|
|
61
|
+
export function InputFile<Multiple extends boolean = false, Type extends InputFileDataType = "file">(props: InputFileProps<Multiple, Type>): ReactNode {
|
|
62
|
+
const { multiple = false, type = "file", onChange: _onChange, disabled: _disabled, clearAfterChange, onValueChange, onFileChange, ...rest } = props
|
|
63
|
+
const [disabled, setDisabled] = useState(false)
|
|
64
|
+
|
|
65
|
+
async function onChange(e: ChangeEvent<HTMLInputElement>) {
|
|
66
|
+
_onChange?.(e)
|
|
67
|
+
const input = e.target
|
|
68
|
+
const { files } = input
|
|
69
|
+
if (!files || files.length === 0) return
|
|
70
|
+
setDisabled(true)
|
|
71
|
+
try {
|
|
72
|
+
if (multiple) {
|
|
73
|
+
const files2: File[] = Array.from(files)
|
|
74
|
+
const values: InputFileDataTypeMap[Type][] = []
|
|
75
|
+
for (const file of files2) {
|
|
76
|
+
const value = (await getFileData(file, type)) as InputFileDataTypeMap[Type]
|
|
77
|
+
values.push(value)
|
|
78
|
+
}
|
|
79
|
+
onFileChange?.(files2 as FileType<Multiple>)
|
|
80
|
+
onValueChange?.(values as ValueType<Multiple, Type>)
|
|
81
|
+
} else {
|
|
82
|
+
const file = files[0]
|
|
83
|
+
onFileChange?.(file as FileType<Multiple>)
|
|
84
|
+
onValueChange?.((await getFileData(file, type)) as ValueType<Multiple, Type>)
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
if (clearAfterChange) input.value = ""
|
|
88
|
+
setDisabled(false)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return <input disabled={disabled || _disabled} type="file" multiple={multiple} onChange={onChange} {...rest} />
|
|
93
|
+
}
|