Components
coderljw 2024-10-13 大约 6 分钟
# 1. ProTable
- 基于 Ant-Pro 封装,新增删除最后一项自动翻页、参数字段名称转换,修复 ProTable 清空时为空字符串和空数组的 BUG。
import React, { useRef } from 'react'
import { ProTable as AntProTable } from '@ant-design/pro-components'
import type { ActionType } from '@ant-design/pro-components'
import type { ProTableProps } from '@ant-design/pro-components'
import type { IProps, ParamsType, RequiredPick } from './interface'
import { generateParams, safeGetValue } from './utils'
const defaultConvertParams = {
current: 'current',
pageSize: 'pageSize',
data: 'data',
total: 'total',
success: 'success',
}
const ProTable = <
DataType extends Record<string, any>,
Params extends ParamsType = ParamsType,
ValueType = 'text',
>({
request,
actionRef,
convertParams,
filterNullValues = true,
...rest
}: ProTableProps<DataType, Params, ValueType> &
RequiredPick<ProTableProps<DataType, Params, ValueType>, 'request'> &
IProps) => {
const innerActionRef = useRef<ActionType>()
const proActionRef = (actionRef as ReturnType<typeof useRef<ActionType>>) || innerActionRef
return (
<AntProTable<DataType, Params, ValueType>
rowKey='id'
cardBordered
defaultSize='large'
dateFormatter='string'
actionRef={proActionRef}
form={{ ignoreRules: false }}
editable={{ type: 'multiple' }}
pagination={{ defaultPageSize: 10 }}
request={async (params, ...restParams) => {
const mergeParams = { ...defaultConvertParams, ...convertParams }
const { success, data, total } = mergeParams
const gParams = generateParams(params, mergeParams, filterNullValues) as Params
const res = await request(gParams, ...restParams)
const safeData = safeGetValue<DataType[]>(res, data, [])
// 删除最后一项自动翻页
if (!safeData.length && (params.current || 1) > 1) {
proActionRef.current!.pageInfo!.current -= 1
}
return {
data: safeData,
total: safeGetValue<number>(res, total, safeData.length),
success: safeGetValue<boolean>(res, success, false),
}
}}
{...rest}
/>
)
}
export default React.memo(ProTable)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
- utils
import type { MergeParams, Params } from './interface'
export const generateParams = (
params: Params,
mergeParams: MergeParams,
filterNullValues: boolean,
): Params => {
const { current, pageSize, ...rest } = params
const filterParams = Object.entries(rest).reduce((acc, [key, value]) => {
if ((typeof value === 'string' || Array.isArray(value)) && !value?.length) {
return acc
}
return { ...acc, [key]: value }
}, {})
return {
[mergeParams.current]: current,
[mergeParams.pageSize]: pageSize,
...(filterNullValues ? filterParams : rest),
}
}
export const safeGetValue = <T,>(
source: Record<string, any>,
prop: string,
underwriteValue?: any,
): T => {
const keys = prop.split('.')
let res: any = source
keys.forEach((k) => {
const curData = res?.[k]
res = curData || underwriteValue
})
return res
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- interface
export type ParamsType = Record<string, any>
export type RequiredPick<T, K extends keyof T> = Required<Pick<T, K>>
export type FilterUndefined<T> = T extends undefined ? never : T
export type IProps = Partial<{
/**
* @name 转换参数
* @desc 可链式取值
* @example { total: count }
* @example { data: data.list }
*/
convertParams: Partial<{
current: string
pageSize: string
data: string
total: string
success: string
}>
/**
* @name 过滤空值
* @desc 过滤ProTable清空时,出现的空字符串与空数组
*/
filterNullValues: boolean
}>
export type Params = ParamsType & {
pageSize?: number | undefined
current?: number | undefined
keyword?: string | undefined
}
export type MergeParams = FilterUndefined<Required<IProps['convertParams']>>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 2. Upload
- UploadAvatar
import React, { useState } from 'react'
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons'
import { Upload } from 'antd'
import type { UploadProps } from 'antd'
import { customRequest, generateBeforeUpload, getBase64 } from './utils'
export type UploadAvatarProps = UploadProps & {
/**
* @name 图片链接
*/
value?: string
onChange?: (url: string) => void
/**
* @name 上传图片类型
* @default ['jpg', 'png', 'webp']
*/
fileType?: string[] | null
/**
* @name 限制上传图片大小
* @desc 单位:M
* @default 5
*/
fileSize?: number | null
}
const UploadAvatar: React.FC<UploadAvatarProps> = ({
value,
onChange,
fileType = ['jpg', 'png', 'webp'],
fileSize = 5,
children,
...rest
}) => {
const [loading, setLoading] = useState(false)
const [imageUrl, setImageUrl] = useState<string | undefined>(value)
const handleChange: UploadProps['onChange'] = async ({ file }) => {
const { status, originFileObj, response } = file
if (status === 'uploading') {
setLoading(true)
}
if (status === 'done') {
const url = await getBase64(originFileObj!)
setLoading(false)
setImageUrl(url)
onChange?.(response)
}
}
return (
<Upload
listType='picture-card'
showUploadList={false}
customRequest={customRequest}
beforeUpload={generateBeforeUpload({ fileType, fileSize })}
onChange={handleChange}
{...rest}
>
{React.Children.toArray(children).length ? (
React.cloneElement(children as React.ReactElement, { loading, imageUrl })
) : imageUrl ? (
<img src={imageUrl} alt={imageUrl} style={{ width: '100%' }} />
) : (
<div>
{loading ? <LoadingOutlined /> : <PlusOutlined />}
<div style={{ marginTop: 8 }}>Upload</div>
</div>
)}
</Upload>
)
}
export default React.memo(UploadAvatar)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
- ProFormUploadAvatar
import React from 'react'
import type { ProFormItemProps } from '@ant-design/pro-components'
import { ProForm } from '@ant-design/pro-components'
import UploadAvatar from './UploadAvatar'
import type { UploadAvatarProps } from './UploadAvatar'
type ProFormUploadAvatarProps = ProFormItemProps & {
fieldProps?: UploadAvatarProps
}
const ProFormUploadAvatar: React.FC<ProFormUploadAvatarProps> = ({
fieldProps,
children,
...rest
}) => {
return (
<ProForm.Item {...rest}>
<UploadAvatar children={children} {...fieldProps} />
</ProForm.Item>
)
}
export default React.memo(ProFormUploadAvatar)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- ProFormUploadButton
import React, { useState } from 'react'
import { ProFormUploadButton as AntProFormUploadButton } from '@ant-design/pro-components'
import type { ProFormUploadButtonProps as AntProFormUploadButtonProps } from '@ant-design/pro-components'
import { Modal } from 'antd'
import type { UploadProps } from 'antd'
import type { ModalProps } from 'antd'
import { saveAs } from 'file-saver'
import { customRequest, generateBeforeUpload, getBase64 } from './utils'
type ProFormUploadButtonProps = AntProFormUploadButtonProps & {
/**
* @name 上传图片类型
* @default ['jpg', 'png', 'webp']
*/
fileType?: string[] | null
/**
* @name 限制上传图片大小
* @desc 单位:M
* @default 5
*/
fileSize?: number | null
fieldProps?: UploadProps
/**
* @name 图片预览对话框
*/
modalProps?: ModalProps
}
const ProFormUploadButton: React.FC<ProFormUploadButtonProps> = ({
fileType = ['jpg', 'png', 'webp'],
fileSize = 5,
fieldProps,
modalProps,
...rest
}) => {
const [visible, setVisible] = useState(false)
const [previewImg, setPreviewImg] = useState<string>()
const onPreview: UploadProps['onPreview'] = async (file) => {
if (file.type?.startsWith('image')) {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj!)
}
setPreviewImg(file.url || file.preview)
setVisible(true)
} else {
saveAs(file.originFileObj as File, file.originFileObj?.name)
}
}
const onChange: UploadProps['onChange'] = ({ fileList }) => {
return fileList.map((file) => {
if (!file.status) file.status = 'error'
return file
})
}
const beforeUpload = generateBeforeUpload({ fileType, fileSize })
return (
<>
<AntProFormUploadButton
max={1}
fieldProps={{
name: 'file',
listType: 'picture-card',
customRequest,
beforeUpload,
onPreview,
onChange,
...fieldProps,
}}
{...rest}
/>
<Modal
visible={visible}
footer={null}
onCancel={() => setVisible(false)}
width='50%'
{...modalProps}
>
<img src={previewImg} alt={previewImg} style={{ width: '100%' }} />
</Modal>
</>
)
}
export default React.memo(ProFormUploadButton)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
- utils
import type { UploadProps } from 'antd'
import { message } from 'antd'
import { lookup } from 'mime-types'
import { UploadImage } from '@/services/pusheng/OtherService'
export const getBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => resolve(reader.result as string)
reader.onerror = reject
})
}
export const customRequest: UploadProps['customRequest'] = ({
file,
filename,
onSuccess,
onProgress,
onError,
}) => {
const formData = new FormData()
formData.append(filename!, file)
UploadImage(formData, {
onUploadProgress: onProgress,
})
.then(({ data }) => onSuccess?.(data?.[0]))
.catch(onError)
}
type GenerateBeforeUpload = (params?: {
fileType?: string[] | null
fileSize?: number | null
}) => UploadProps['beforeUpload']
export const generateBeforeUpload: GenerateBeforeUpload = ({ fileType, fileSize } = {}) => {
return ({ type, size }) => {
if (fileType?.length && !fileType.some((i) => type === lookup(i))) {
message.warning(`仅支持${fileType.join('、')}图片类型`)
return false
}
if (fileSize && size > fileSize * 1024 * 1024) {
message.warning(`图片大小不能超过${fileSize}M`)
return false
}
return true
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 3. Tinymce
富文本编辑器(Tinymce (opens new window))
public 放入下载的 Tinymce SDK (opens new window) 文件,langs (opens new window) 目录下放入需要转换的语言,接着配置
tinymceScriptSrc
路径和language
语言。
import React from 'react'
import { Editor } from '@tinymce/tinymce-react'
import type { IAllProps } from '@tinymce/tinymce-react'
import { lookup } from 'mime-types'
import type { TinyMCE } from 'tinymce'
import { UploadImage } from '@/services/pusheng/OtherService'
type UploadHandler = Parameters<TinyMCE['init']>[0]['images_upload_handler']
export type TinymceProps = Omit<IAllProps, 'onChange'> & {
/**
* @name 上传图片类型
* @default ['jpg', 'png', 'webp']
*/
fileType?: string[]
/**
* @name 限制上传图片大小
* @desc 单位:M
* @default 5
*/
fileSize?: number
onChange?: (content: string) => void
}
const Tinymce: React.FC<TinymceProps> = ({
init,
fileType = ['jpg', 'png', 'webp'],
fileSize = 5,
onChange,
...rest
}) => {
const beforeUpload = async ({ type, size }: Blob) => {
if (fileType?.length && !fileType.some(i => type === lookup(i))) {
throw Error(`仅支持${fileType.join('、')}图片类型`)
}
if (fileSize && size > fileSize * 1024 * 1024) {
throw Error(`图片大小不能超过${fileSize}M`)
}
}
const uploadImage: UploadHandler = async ({ blob, name }, progress) => {
await beforeUpload(blob())
const formData = new FormData()
formData.append(name(), blob())
const { data } = await UploadImage(formData, {
onUploadProgress: ({ loaded, total }: ProgressEvent) =>
progress((loaded / total) * 100),
})
return data?.[0] as string
}
return (
<Editor
tinymceScriptSrc='/tinymce/tinymce.min.js'
init={{
height: 500,
menubar: false,
language: 'zh-Hans',
images_reuse_filename: true,
images_upload_handler: uploadImage,
plugins: [
'advlist',
'autolink',
'lists',
'link',
'image',
'charmap',
'anchor',
'searchreplace',
'visualblocks',
'code',
'fullscreen',
'insertdatetime',
'media',
'table',
'preview',
'help',
'wordcount',
],
toolbar: `undo redo | forecolor backcolor | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent lineheight | removeformat charmap table link image insertdatetime fullscreen`,
contextmenu: 'link image imagetools table spellchecker bold copy',
content_style:
'body { font-family: system-ui, —apple-system, Segoe UI, Rototo, Emoji, Helvetica, Arial,sans-serif; font-size:14px }',
placeholder: '请输入',
...init,
}}
onEditorChange={content => onChange?.(content)}
{...rest}
/>
)
}
export default React.memo(Tinymce)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
- ProFormTinymce
import React from 'react'
import { ProForm } from '@ant-design/pro-components'
import type { ProFormItemProps } from '@ant-design/pro-components'
import Tinymcefrom from './index'
import type { TinymceProps } from './index'
export type ProFormTinymceProps = ProFormItemProps & {
fieldProps?: TinymceProps
}
const ProFormTinymce: React.FC<ProFormTinymceProps> = ({ fieldProps, ...rest }) => {
return (
<ProForm.Item {...rest}>
<Tinymcefrom {...fieldProps} />
</ProForm.Item>
)
}
export default React.memo(ProFormTinymce)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 4. ErrorRetry
import React from 'react'
import { Alert, Button, Spin } from 'antd'
import type { AlertProps, ButtonProps, SpinProps } from 'antd'
export type ErrorRetryProps = {
/**
* @name 错误信息
*/
error?: Error | string
/**
* @name 加载状态
* @default false
*/
loading?: boolean
/**
* @name 重试按钮的加载状态
* @default false
*/
retryLoading?: boolean
/**
* @name 重试按钮的文本
* @default false
*/
retryText?: React.ReactNode
/**
* @name 点击重试按钮的回调
*/
onRetry?: () => void
/**
* @name Spin组件属性
*/
spinProps?: SpinProps
/**
* @name Alert组件属性
*/
alertProps?: AlertProps
/**
* @name Button组件属性
*/
buttonProps?: ButtonProps
/**
* @name 子元素
*/
children?: React.ReactNode
}
const ErrorRetry: React.FC<ErrorRetryProps> = ({
error,
loading = false,
retryLoading = false,
retryText,
onRetry,
spinProps,
alertProps,
buttonProps,
children,
}) => {
return (
<Spin spinning={loading} {...spinProps}>
{!!error && (
<Alert
showIcon
type='error'
message='Request Error'
description={(typeof error === 'string' ? error : error?.message) || '暂无错误信息'}
action={
<Button danger loading={retryLoading} onClick={onRetry} {...buttonProps}>
重试
</Button>
}
{...alertProps}
/>
)}
<div style={{ display: !!error ? 'node' : 'unset' }}> {children}</div>
</Spin>
)
}
export default React.memo(ErrorRetry)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 5. OperationGroup
import React from 'react'
import { EllipsisOutlined } from '@ant-design/icons'
import type { DropdownProps, SpaceProps } from 'antd'
import { Dropdown, Space } from 'antd'
import { omit } from 'lodash'
export type OperationGroupProps = {
/**
* @name 展示数量
*/
count?: number
/**
* @name 省略元素
* @desc 默认三个点 EllipsisOutlined
*/
restElement?: React.ReactNode
/**
* @name Space组件属性
*/
spaceProps?: SpaceProps
/**
* @name Dropdown组件属性
*/
dropdownProps?: DropdownProps
/**
* @name 子元素
*/
children?: React.ReactElement | React.ReactElement[]
} & Record<string, any>
const OperationGroup: React.FC<OperationGroupProps> = ({
count,
restElement,
spaceProps,
dropdownProps,
children,
...rest
}) => {
const components = React.Children.toArray(children) as React.ReactElement[]
const showComponents = components.slice(0, count)
const dropdownComponents = components.slice(count || components.length)
const generateComponent = (Component: React.ReactElement) => {
return React.cloneElement(Component, {
...Component.props,
...rest,
})
}
return (
<Space {...spaceProps}>
{showComponents.map(generateComponent)}
{!!dropdownComponents.length && (
<Dropdown
trigger={['click']}
menu={{
items: dropdownComponents.map((Component, index) => ({
key: index,
label: generateComponent(Component),
})),
...omit(dropdownProps?.menu, 'items'),
}}
{...omit(dropdownProps, 'menu')}
>
{restElement || (
<a>
<EllipsisOutlined />
</a>
)}
</Dropdown>
)}
</Space>
)
}
export default React.memo(OperationGroup)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76