忙到忘记记笔记😅

单纯的记一下笔记

index.tsx

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import React, { useState, useRef, useEffect } from 'react'
import { Button, Modal, ModalProps } from 'antd'
import type { DraggableData, DraggableEvent } from 'react-draggable'
import Draggable from 'react-draggable'
import Title from './title'

interface IFooterProps {
readOnly?: boolean
customFooter?: boolean
}

/**
* modal 封装
* 1. 支持拖动
* 2. 支持放大到全屏
* 4. modal确定按钮 提交form表单
*/
const ModalComps: React.FC<ModalProps & IFooterProps> = React.memo((props): React.ReactElement => {
const [form, setForm] = useState(null) // 子级的form
const [disabled, setDisabled] = useState(false) // 是否开始拖拽
const [full, setFull] = useState(false) // 是否是全屏
const [bounds, setBounds] = useState({ left: 0, top: 0, bottom: 0, right: 0 })
const draggleRef = useRef<HTMLDivElement>(null)

useEffect(() => {
// 弹窗关闭的时候,full 重置
if (props?.visible === false) setFull(false)
}, [props.visible])

// 给子组件注入事件 onModalForm
// 子组件通过onModalForm将form传给modal, modal接管form表单
const childrenRender = () => {
const prop = {
onModalForm: getChildrenForm,
}
return React.Children.map(props?.children, (child: any) => React.cloneElement(child, prop))
}

// 获取到子组件form 实例
const getChildrenForm = (form: any) => setForm(form)

// modal点击提交的时候 将子form的数据传给 modal 的确认事件
const onSubmit = (e: React.MouseEvent<HTMLElement>) => {
if (form) {
// @ts-ignore
form
// @ts-ignore
?.validateFields()
.then((values: IObjectProps) => {
return values
})
.catch(() => {
return false
})
.then((res: any) => {
// 校验不通过,不触发 modal 确认事件
if (res === false) return
props?.onOk?.(res || '')
})
return
}
props?.onOk?.(e)
}

// 拖动位置处理
const onStart = (_event: DraggableEvent, uiData: DraggableData) => {
const { clientWidth, clientHeight } = window.document.documentElement
const targetRect = draggleRef.current?.getBoundingClientRect()
if (!targetRect) return

setBounds({
left: -targetRect.left + uiData.x,
right: clientWidth - (targetRect.right - uiData.x),
top: -targetRect.top + uiData.y,
bottom: clientHeight - (targetRect.bottom - uiData.y),
})
}

// modal 可拖动处理
const modalRender = (modal: React.ReactNode) => {
// modal 全局弹窗 时不允许拖动
if (full) return modal

return (
<Draggable
disabled={disabled}
bounds={bounds}
onStart={(event, uiData) => onStart(event, uiData)}
>
<div ref={draggleRef}>{modal}</div>
</Draggable>
)
}

return (
<Modal
width={900}
{...props}
title={
<Title
title={props.title}
disabled={disabled}
setDisabled={setDisabled}
full={full}
setFull={setFull}
/>
}
className={`msh-modal ${full ? 'full' : ''}`}
centered={!full}
modalRender={modalRender}
footer={[
props.readOnly ? (
props.customFooter ? (
props.footer
) : (
<Button key="cancel" onClick={props.onCancel}>
关闭
</Button>
)
) : (
<div key="btn">
<Button key="submit" type="primary" onClick={onSubmit}>
确认
</Button>
<Button key="back" onClick={props.onCancel}>
取消
</Button>
</div>
),
]}
getContainer={'#root'}
>
{childrenRender()}
</Modal>
)
})

export default ModalComps

title.tsx

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
import React from 'react'
import Icon from '@/assets/svg'

interface ITitleProps {
title?: string | unknown
disabled: boolean // 是否可以拖动
full: boolean // 全屏状态
setFull: (data: boolean) => any
setDisabled: (data: boolean) => any // 设置是否允许拖动
}

/**
* 封装modal title
*/
const Title: React.FC<ITitleProps> = ({ title, disabled, full, setFull, setDisabled }) => {
const handleSetFull = () => {
setFull(!full)
// 全屏状态关闭拖动
setDisabled(true)
}
return (
<div
className="msh-modal-header-title flex flex-jsc-between flex-align-center"
style={{
width: '100%',
cursor: full ? 'unset' : 'move',
}}
onMouseOver={() => {
if (disabled) {
setDisabled(false)
}
}}
onMouseOut={() => {
setDisabled(true)
}}
onFocus={() => {}}
onBlur={() => {}}
>
{title || '这是title'}
{/* 全屏处理 icon */}
{!full && <Icon name="full" onClick={handleSetFull} className="cursor-pointer" />}
{full && <Icon name="full2" onClick={handleSetFull} className="cursor-pointer" />}
</div>
)
}

export default React.memo(Title)

富文本 支持antd form表单

使用的富文本编辑器 wangEditor

  • 使用示例
1
2
3
4
5
6
7
8
9
10
11
// antd form item
<Form.Item
name="test"
label="中文简介"
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
initialValue={}
rules={[{ required: true, max: 65535, message: '请输入xxxx' }]}
>
<MyEditor />
</Form.Item>
  • 组件
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
import React, { useState, useEffect } from 'react'
import { Editor, Toolbar } from '@wangeditor/editor-for-react'
import { IDomEditor, IEditorConfig } from '@wangeditor/editor'

import '@wangeditor/editor/dist/css/style.css' // 引入 css

type valueType = string | null | undefined
interface IEditorProps {
value?: valueType
onChange?: (value: valueType) => void
}

/**
* 可以配合 form 的editor 组件
* @param props
* @returns
*/
const MyEditor:React.FC<IEditorProps> = (props) => {
const [editor, setEditor] = useState<IDomEditor | null>(null) // 存储 editor 实例
const [html, setHtml] = useState('') // 编辑器内容

useEffect(() => {
props.value && setHtml(props.value)
}, [])

const editorConfig: Partial<IEditorConfig> = {
placeholder: '请输入内容...',
autoFocus: false, // 不自动聚焦
}

// 及时销毁 editor ,重要!
useEffect(() => {
return () => {
if (editor == null) return
editor.destroy()
setEditor(null)
}
}, [editor])

const contentChangeHandle = (editor: any) => {
const node = editor.getHtml()
setHtml(editor.getHtml())
// 内容为空时 gethtml得到的内容为 '<p><br></p>'
props?.onChange?.(node === '<p><br></p>' ? '' : node)
}

return (
<>
<div className="editor-wrap">
<Toolbar editor={editor} mode="default" style={{ borderBottom: '1px solid #d9d9d9' }} />
<Editor
defaultConfig={editorConfig}
value={html}
onCreated={setEditor}
onChange={contentChangeHandle}
mode="default"
style={{ height: '240px', overflowY: 'hidden' }}
/>
</div>
</>
)
}

export default MyEditor

Protable 自定义 input模糊搜索

  • 使用示例
    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
    // 1. searchInput 单独使用
    <Form.Item
    name="employeeId"
    label="会员号"
    rules={[{ required: true, message: '请输入会员号' }]}
    >
    <SearchInput onSearch={handleSearchEmployeeId} placeholder="请输入会员号" onChange={value => handleEmployeeIdChange(value, form)} />
    </Form.Item>

    // 2. 配合Procomponent ProTable 使用
    const columns: ProColumns = [
    {
    title: '序号',
    dataIndex: 'index',
    valueType: 'index',
    width: 48,
    },
    {
    title: '会员号',
    dataIndex: 'employeeId',
    ellipsis: true,
    copyable: true,
    width: 150,
    renderFormItem: EmployeeSearch, // 见下面 EmployeeSearch
    formItemProps: {
    rules: [
    {
    required: true,
    message: '此项为必填项',
    },
    ],
    },
    },
    ]


  • searchInput
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
import React, { useState, useEffect } from 'react'
import { Select } from 'antd'
const { Option } = Select

interface ISearchInputProps {
value?: string
onChange?: (value: string) => void
placeholder?: string
style?: React.CSSProperties
onSearch: (value: string, setData: any) => void // 用户输入,触发请求
}

/**
* 支持输入+搜索的 select
* 可配合form使用
* @param props
*/
const SearchInput: React.FC<ISearchInputProps> = props => {
const [data, setData] = useState<any[]>([])
const [value, setValue] = useState<string>();

useEffect(() => {
if (props?.value && typeof props.value === 'object') {
setData(props?.value || [])
}
}, [])


const handleSearch = (newValue: string) => {
if(newValue){
setValue(newValue)
props?.onSearch(newValue, setData)
// @ts-ignore
props?.onChange(newValue?.trim())
return
}
setData([])
}

const handleChange = (newValue: string) => {
setValue(newValue)

// 值回传给父级
// @ts-ignore
props?.onChange(newValue?.trim())
};

const handleClear =()=>{
setData([])
}

const options =
typeof data === 'object' && data?.map(d => <Option key={d.value}>{d.text}</Option>)


return (
<Select
allowClear
showSearch
value={value}
showArrow={false}
style={props.style}
filterOption={false}
onChange={handleChange}
onSearch={handleSearch}
onClear={handleClear}
notFoundContent={null}
defaultActiveFirstOption={false}
placeholder={props.placeholder}
>
{options}
</Select>
)
}

export default React.memo(SearchInput)

  • employeeSearch
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
import SearchInput from '@/pages/components/searchInput'
import { handleSearchEmployeeId } from '@/utils/index'

// 自定义会员号选项 select
const EnployeeSearch = (
_: any,
{ type, defaultRender, formItemProps, fieldProps, ...rest }: any,
form: any
) => {
if (type === 'form') {
return null
}
const status = form.getFieldValue('state')
if (status !== 'open') {
return (
// value 和 onchange 会通过 form 自动注入。
<SearchInput
{...formItemProps}
{...fieldProps}
onSearch={handleSearchEmployeeId}
placeholder="请输入会员号"
style={{
width: '100%',
}}
/>
)
}
return defaultRender(_)
}

export default EnployeeSearch

...

// handleSearchEmployeeId
/**
* 全局 employeeId 模糊查询函数
*/
export const handleSearchEmployeeId = debounce(
(value: string, callback: (data: { value: string; text: string }[]) => void) => {
getEmployeeIdListApi({ employeeId: value }).then((res: any) => {
if (res?.success !== 't') return

callback(
res.result?.map((item: string) => {
return { value: item, text: item }
})
)
})
},
300
)


upload

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import React, { useEffect, useState } from 'react'
import { Upload, Modal } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import type { RcFile, UploadFile, UploadProps, ShowUploadListInterface } from 'antd/es/upload/interface'
import './index.module.scss'

type ValueType = UploadFile[] | null | undefined

interface IUploadProps {
api: (data: any) => Promise<any>
value?: ValueType
onChange?: (value: ValueType) => void
preview?: boolean // 是否支持预览
length?: number // 当上传照片数到达限制后,上传按钮消失。
maxCount?: number // 通过 maxCount 限制上传数量。当为 1 时,始终用最新上传的代替当前。
multiple?: boolean // 是否支持多选
data?: Record<string, any> // 上传所需额外参数或返回上传额外参数的方法
beforeUpload?: UploadProps['beforeUpload']
showUploadList?: ShowUploadListInterface // 控制图片hover时,各种icon的显示
}

const getBase64 = (file: RcFile): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => resolve(reader.result as string)
reader.onerror = error => reject(error)
})

/**
* 上传图片组件,可以配合form表单使用
* 注: 目前仅支持单张上传,多张上传接口和前端目前都不支持
* @returns
*/
const UploadComp: React.FC<IUploadProps> = props => {
const [previewVisible, setPreviewVisible] = useState(false)
const [previewImage, setPreviewImage] = useState('')
const [previewTitle, setPreviewTitle] = useState('')
const [fileList, setFileList] = useState<UploadFile[]>([])

useEffect(() => {
if (!props.value || !props.value[0]?.url) return
// 1. 接受 form的默认值
setFileList(props.value)
}, [props.value])

const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
setFileList(newFileList)
}

// 预览
const handlePreview = async (file: UploadFile) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj as RcFile)
}

setPreviewImage(file.url || (file.preview as string))
setPreviewVisible(true)
setPreviewTitle(file.name || file.url!.substring(file.url!.lastIndexOf('/') + 1))
}

const handleCancel = () => setPreviewVisible(false)

// 上传之前处理,return false 不走upload组件上传
const handleBeforeUpload: UploadProps['beforeUpload'] = (file: RcFile, fileList: RcFile[]) => {
const formdata = new FormData()
formdata.append('files', file)

// 将需要多传的参数 注入formdata
if (props.data && Object.keys(props.data).length) {
for (let key in props.data) {
formdata.append(key, props.data[key])
}
}

// 图片上传 需求不需要图片压缩 未做压缩处理
props.api(formdata).then((res: any) => {
if (res.success === 't' && res.result) {
// 2.数据返回给上级form
props?.onChange && props?.onChange(res.result)
}
})

return false
}

// 删除
const handleRemove: UploadProps['onRemove'] = (param): void => {
props?.onChange && props?.onChange([])
}

return (
<>
<Upload
listType="picture-card"
multiple={props?.multiple}
maxCount={props.maxCount}
fileList={fileList}
onPreview={handlePreview}
onChange={handleChange}
onRemove={handleRemove}
beforeUpload={handleBeforeUpload}
showUploadList={{ showPreviewIcon: props.preview, ...props.showUploadList }}
>
{fileList.length < (props?.length || 20) && <PlusOutlined />}
</Upload>

{/* 图片预览 */}
{props.preview ? (
<Modal
visible={previewVisible}
title={previewTitle}
footer={null}
onCancel={handleCancel}
destroyOnClose
>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
) : (
''
)}
</>
)
}

UploadComp.defaultProps = {
multiple: false,
length: 10, // 默认图片列表可以展示图片的数量,当上传照片数到达限制后,上传按钮消失。
maxCount: 10,
preview: true, // 默认支持预览
showUploadList: {}
}

export default UploadComp