@fe-free/core 2.0.6 → 2.1.1
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/CHANGELOG.md +17 -0
- package/package.json +3 -2
- package/src/crud/crud.stories.tsx +14 -0
- package/src/crud/crud.tsx +83 -53
- package/src/crud/crud_of_simple.stories.tsx +83 -7
- package/src/crud/crud_of_simple.tsx +27 -11
- package/src/crud/style.scss +29 -0
- package/src/crud/types.tsx +8 -0
- package/src/form/form.stories.tsx +98 -0
- package/src/form/index.tsx +6 -1
- package/src/form/pro_form_upload.tsx +50 -0
- package/src/index.ts +5 -0
- package/src/upload/index.tsx +173 -0
- package/src/use_localforage_state.tsx +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @fe-free/core
|
|
2
2
|
|
|
3
|
+
## 2.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- feat: crud
|
|
8
|
+
- @fe-free/tool@2.1.1
|
|
9
|
+
|
|
10
|
+
## 2.1.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- feat: upload
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- @fe-free/tool@2.1.0
|
|
19
|
+
|
|
3
20
|
## 2.0.6
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fe-free/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"author": "",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"axios": "^1.6.5",
|
|
30
30
|
"classnames": "^2.5.1",
|
|
31
31
|
"github-markdown-css": "^5.8.1",
|
|
32
|
+
"localforage": "^1.10.0",
|
|
32
33
|
"lodash-es": "^4.17.21",
|
|
33
34
|
"react-ace": "^11.0.1",
|
|
34
35
|
"react-markdown": "^9.1.0",
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"remark-gfm": "^4.0.1",
|
|
39
40
|
"vanilla-jsoneditor": "^0.23.1",
|
|
40
41
|
"zustand": "^4.5.4",
|
|
41
|
-
"@fe-free/tool": "2.
|
|
42
|
+
"@fe-free/tool": "2.1.1"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
|
44
45
|
"@ant-design/pro-components": "^2.8.7",
|
|
@@ -183,6 +183,14 @@ export const MoreCustom: Story = {
|
|
|
183
183
|
},
|
|
184
184
|
}}
|
|
185
185
|
createButton={<Button type="primary">自定义新建文本</Button>}
|
|
186
|
+
readProps={{
|
|
187
|
+
operateIsDisabled: (record) => {
|
|
188
|
+
if (record.id % 3) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
},
|
|
193
|
+
}}
|
|
186
194
|
requestDeleteByRecord={fakeDeleteByRecord}
|
|
187
195
|
deleteProps={{
|
|
188
196
|
nameIndex: 'name',
|
|
@@ -214,6 +222,12 @@ export const MoreCustom: Story = {
|
|
|
214
222
|
}
|
|
215
223
|
return true;
|
|
216
224
|
},
|
|
225
|
+
operateIsHidden: (record) => {
|
|
226
|
+
if (record.id % 4) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
},
|
|
217
231
|
}}
|
|
218
232
|
/>
|
|
219
233
|
);
|
package/src/crud/crud.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ActionType } from '@ant-design/pro-components';
|
|
2
2
|
import { Button, message } from 'antd';
|
|
3
|
+
import classNames from 'classnames';
|
|
3
4
|
import { isString } from 'lodash-es';
|
|
4
5
|
import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react';
|
|
5
6
|
import { Link } from 'react-router-dom';
|
|
@@ -112,67 +113,97 @@ function CRUDComponent<
|
|
|
112
113
|
const btns: React.ReactNode[] = [];
|
|
113
114
|
|
|
114
115
|
if (actions.includes('read')) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
116
|
+
const hidden = readProps?.operateIsHidden?.(record) || false;
|
|
117
|
+
if (!hidden) {
|
|
118
|
+
const disabled = readProps?.operateIsDisabled?.(record) || false;
|
|
119
|
+
if (disabled) {
|
|
120
|
+
btns.push(
|
|
121
|
+
<span key="read" className="text-desc cursor-not-allowed">
|
|
122
|
+
{readProps?.operateText || '查看'}
|
|
123
|
+
</span>,
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
btns.push(
|
|
127
|
+
<CRUDDetail
|
|
128
|
+
key="read"
|
|
129
|
+
id={record[idField]}
|
|
130
|
+
record={record}
|
|
131
|
+
onSuccess={handleReload}
|
|
132
|
+
trigger={<a>{readProps?.operateText || '查看'}</a>}
|
|
133
|
+
action="read"
|
|
134
|
+
{...detailProps}
|
|
135
|
+
/>,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
if (actions.includes('read_detail')) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
const hidden = readProps?.operateIsHidden?.(record) || false;
|
|
143
|
+
if (!hidden) {
|
|
144
|
+
const disabled = readProps?.operateIsDisabled?.(record) || false;
|
|
145
|
+
if (disabled) {
|
|
146
|
+
btns.push(
|
|
147
|
+
<span key="read" className="text-desc cursor-not-allowed">
|
|
148
|
+
{readProps?.operateText || '查看'}
|
|
149
|
+
</span>,
|
|
150
|
+
);
|
|
151
|
+
} else {
|
|
152
|
+
btns.push(
|
|
153
|
+
<Link
|
|
154
|
+
key="read_detail"
|
|
155
|
+
to={`./detail/${record[detailIdIndex || 'id']}`}
|
|
156
|
+
target={readProps?.target}
|
|
157
|
+
>
|
|
158
|
+
{readProps?.operateText || '查看'}
|
|
159
|
+
</Link>,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
138
163
|
}
|
|
139
164
|
|
|
140
165
|
if (actions.includes('update')) {
|
|
141
|
-
const
|
|
166
|
+
const hidden = updateProps?.operateIsHidden?.(record) || false;
|
|
167
|
+
if (!hidden) {
|
|
168
|
+
const disabled = updateProps?.operateIsDisabled?.(record) || false;
|
|
142
169
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
170
|
+
if (disabled) {
|
|
171
|
+
btns.push(
|
|
172
|
+
<span key="update" className="text-desc cursor-not-allowed">
|
|
173
|
+
{updateProps?.operateText || '编辑'}
|
|
174
|
+
</span>,
|
|
175
|
+
);
|
|
176
|
+
} else {
|
|
177
|
+
btns.push(
|
|
178
|
+
<CRUDDetail
|
|
179
|
+
key="update"
|
|
180
|
+
id={record[idField]}
|
|
181
|
+
record={record}
|
|
182
|
+
onSuccess={handleReload}
|
|
183
|
+
trigger={<a>{updateProps?.operateText || '编辑'}</a>}
|
|
184
|
+
action="update"
|
|
185
|
+
{...detailProps}
|
|
186
|
+
/>,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
161
189
|
}
|
|
162
190
|
}
|
|
163
191
|
|
|
164
192
|
if (actions.includes('delete') && deleteProps) {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
193
|
+
const hidden = deleteProps?.operateIsHidden?.(record) || false;
|
|
194
|
+
if (!hidden) {
|
|
195
|
+
const disabled = deleteProps?.operateIsDisabled?.(record) || false;
|
|
196
|
+
btns.push(
|
|
197
|
+
<OperateDelete
|
|
198
|
+
key="delete"
|
|
199
|
+
name={record[deleteProps.nameIndex]}
|
|
200
|
+
desc={deleteProps.desc}
|
|
201
|
+
operateText={deleteProps.operateText}
|
|
202
|
+
disabled={disabled}
|
|
203
|
+
onDelete={getHandleDelete(record)}
|
|
204
|
+
/>,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
176
207
|
}
|
|
177
208
|
|
|
178
209
|
return (
|
|
@@ -206,9 +237,8 @@ function CRUDComponent<
|
|
|
206
237
|
operateColumnProps,
|
|
207
238
|
actions,
|
|
208
239
|
deleteProps,
|
|
240
|
+
readProps,
|
|
209
241
|
handleReload,
|
|
210
|
-
readProps?.operateText,
|
|
211
|
-
readProps?.target,
|
|
212
242
|
detailProps,
|
|
213
243
|
detailIdIndex,
|
|
214
244
|
updateProps,
|
|
@@ -259,7 +289,7 @@ function CRUDComponent<
|
|
|
259
289
|
});
|
|
260
290
|
|
|
261
291
|
return (
|
|
262
|
-
<div className=
|
|
292
|
+
<div className={classNames('fec-crud')}>
|
|
263
293
|
<Table<DataSource>
|
|
264
294
|
rowKey="id"
|
|
265
295
|
{...tableProps}
|
|
@@ -17,10 +17,45 @@ export const Normal: Story = {
|
|
|
17
17
|
render: () => {
|
|
18
18
|
const columns = [
|
|
19
19
|
{
|
|
20
|
-
title: '
|
|
21
|
-
dataIndex: '
|
|
20
|
+
title: '名字(省略)',
|
|
21
|
+
dataIndex: 'name',
|
|
22
22
|
search: true,
|
|
23
|
+
ellipsis: true,
|
|
23
24
|
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<CRUDOfSimple
|
|
29
|
+
actions={['create', 'delete']}
|
|
30
|
+
tableProps={{
|
|
31
|
+
columns,
|
|
32
|
+
request: fakeRequest,
|
|
33
|
+
pagination: false,
|
|
34
|
+
}}
|
|
35
|
+
requestDeleteByRecord={fakeDeleteByRecord}
|
|
36
|
+
deleteProps={{
|
|
37
|
+
nameIndex: 'name',
|
|
38
|
+
}}
|
|
39
|
+
detailForm={() => (
|
|
40
|
+
<>
|
|
41
|
+
<ProFormText
|
|
42
|
+
name="name"
|
|
43
|
+
label="名字"
|
|
44
|
+
required
|
|
45
|
+
rules={[{ required: true }]}
|
|
46
|
+
extra="extra extra extra extra"
|
|
47
|
+
/>
|
|
48
|
+
</>
|
|
49
|
+
)}
|
|
50
|
+
requestCreateByValues={fakeCreate}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const WithSearch: Story = {
|
|
57
|
+
render: () => {
|
|
58
|
+
const columns = [
|
|
24
59
|
{
|
|
25
60
|
title: '名字(省略)',
|
|
26
61
|
dataIndex: 'name',
|
|
@@ -53,19 +88,59 @@ export const Normal: Story = {
|
|
|
53
88
|
</>
|
|
54
89
|
)}
|
|
55
90
|
requestCreateByValues={fakeCreate}
|
|
91
|
+
simpleSearchProps={{
|
|
92
|
+
name: 'name',
|
|
93
|
+
widthFull: true,
|
|
94
|
+
}}
|
|
56
95
|
/>
|
|
57
96
|
);
|
|
58
97
|
},
|
|
59
98
|
};
|
|
60
99
|
|
|
61
|
-
export const
|
|
100
|
+
export const HoverShow: Story = {
|
|
62
101
|
render: () => {
|
|
63
102
|
const columns = [
|
|
64
103
|
{
|
|
65
|
-
title: '
|
|
66
|
-
dataIndex: '
|
|
104
|
+
title: '名字(省略)',
|
|
105
|
+
dataIndex: 'name',
|
|
67
106
|
search: true,
|
|
107
|
+
ellipsis: true,
|
|
68
108
|
},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<CRUDOfSimple
|
|
113
|
+
actions={['create', 'delete']}
|
|
114
|
+
tableProps={{
|
|
115
|
+
columns,
|
|
116
|
+
request: fakeRequest,
|
|
117
|
+
pagination: false,
|
|
118
|
+
}}
|
|
119
|
+
requestDeleteByRecord={fakeDeleteByRecord}
|
|
120
|
+
deleteProps={{
|
|
121
|
+
nameIndex: 'name',
|
|
122
|
+
}}
|
|
123
|
+
detailForm={() => (
|
|
124
|
+
<>
|
|
125
|
+
<ProFormText
|
|
126
|
+
name="name"
|
|
127
|
+
label="名字"
|
|
128
|
+
required
|
|
129
|
+
rules={[{ required: true }]}
|
|
130
|
+
extra="extra extra extra extra"
|
|
131
|
+
/>
|
|
132
|
+
</>
|
|
133
|
+
)}
|
|
134
|
+
requestCreateByValues={fakeCreate}
|
|
135
|
+
simpleOperateHoverShow
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const JustSearch: Story = {
|
|
142
|
+
render: () => {
|
|
143
|
+
const columns = [
|
|
69
144
|
{
|
|
70
145
|
title: '名字(省略)',
|
|
71
146
|
dataIndex: 'name',
|
|
@@ -76,7 +151,7 @@ export const WithSearch: Story = {
|
|
|
76
151
|
|
|
77
152
|
return (
|
|
78
153
|
<CRUDOfSimple
|
|
79
|
-
actions={['
|
|
154
|
+
actions={['delete']}
|
|
80
155
|
tableProps={{
|
|
81
156
|
columns,
|
|
82
157
|
request: fakeRequest,
|
|
@@ -99,7 +174,8 @@ export const WithSearch: Story = {
|
|
|
99
174
|
)}
|
|
100
175
|
requestCreateByValues={fakeCreate}
|
|
101
176
|
simpleSearchProps={{
|
|
102
|
-
name: '
|
|
177
|
+
name: 'name',
|
|
178
|
+
widthFull: true,
|
|
103
179
|
}}
|
|
104
180
|
/>
|
|
105
181
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useDebounce } from 'ahooks';
|
|
2
2
|
import { Input } from 'antd';
|
|
3
|
+
import classNames from 'classnames';
|
|
3
4
|
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
5
|
import { CRUD } from './crud';
|
|
5
6
|
import type { CRUDMethods, CRUDProps } from './types';
|
|
@@ -8,12 +9,15 @@ interface CRUDOfSimpleProps<
|
|
|
8
9
|
DataSource extends Record<string, any> = any,
|
|
9
10
|
Key extends string | number = string,
|
|
10
11
|
> extends CRUDProps<DataSource, Key> {
|
|
12
|
+
simpleOperateHoverShow?: boolean;
|
|
11
13
|
// 传才开启搜索
|
|
12
14
|
simpleSearchProps?: {
|
|
13
15
|
/** 搜索项的名称,默认 keywords */
|
|
14
16
|
name: string;
|
|
15
17
|
/** 搜索项的 placeholder,默认 请输入 */
|
|
16
18
|
placeholder?: string;
|
|
19
|
+
/** 占满宽度 */
|
|
20
|
+
widthFull?: boolean;
|
|
17
21
|
};
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -39,12 +43,13 @@ function SearchRender(props: {
|
|
|
39
43
|
allowClear
|
|
40
44
|
value={props.value}
|
|
41
45
|
onChange={(e) => props.onChange(e.target.value)}
|
|
46
|
+
className="w-full"
|
|
42
47
|
/>
|
|
43
48
|
);
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
function CRUDOfSimpleComponent(props: CRUDOfSimpleProps, ref: React.ForwardedRef<CRUDMethods>) {
|
|
47
|
-
const { simpleSearchProps, tableProps, ...rest } = props;
|
|
52
|
+
const { simpleSearchProps, tableProps, simpleOperateHoverShow, ...rest } = props;
|
|
48
53
|
|
|
49
54
|
useTips(props);
|
|
50
55
|
const [searchValue, setSearchValue] = useState<string>('');
|
|
@@ -61,15 +66,16 @@ function CRUDOfSimpleComponent(props: CRUDOfSimpleProps, ref: React.ForwardedRef
|
|
|
61
66
|
const toolBarRender = useCallback(
|
|
62
67
|
(...args) => {
|
|
63
68
|
return [
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
simpleSearchProps && (
|
|
70
|
+
<SearchRender
|
|
71
|
+
key="search-input"
|
|
72
|
+
placeholder={simpleSearchProps.placeholder}
|
|
73
|
+
value={searchValue}
|
|
74
|
+
onChange={(value) => setSearchValue(value)}
|
|
75
|
+
/>
|
|
76
|
+
),
|
|
77
|
+
// 留更多间隔,避免直接贴右边。
|
|
78
|
+
simpleSearchProps && <div key="search-gap" />,
|
|
73
79
|
// @ts-ignore
|
|
74
80
|
...(tableProps.toolBarRender ? tableProps.toolBarRender(...args) : []),
|
|
75
81
|
];
|
|
@@ -89,7 +95,12 @@ function CRUDOfSimpleComponent(props: CRUDOfSimpleProps, ref: React.ForwardedRef
|
|
|
89
95
|
}, [debouncedSearchValue, simpleSearchProps, tableProps.params]);
|
|
90
96
|
|
|
91
97
|
return (
|
|
92
|
-
<div
|
|
98
|
+
<div
|
|
99
|
+
className={classNames('fec-crud-of-simple', {
|
|
100
|
+
'fec-crud-of-simple-hover-show': simpleOperateHoverShow,
|
|
101
|
+
'fec-crud-of-simple-search-width-full': simpleSearchProps?.widthFull,
|
|
102
|
+
})}
|
|
103
|
+
>
|
|
93
104
|
<CRUD
|
|
94
105
|
ref={ref}
|
|
95
106
|
{...rest}
|
|
@@ -104,6 +115,11 @@ function CRUDOfSimpleComponent(props: CRUDOfSimpleProps, ref: React.ForwardedRef
|
|
|
104
115
|
// 简单的隐藏搜索栏
|
|
105
116
|
search: false,
|
|
106
117
|
}}
|
|
118
|
+
operateColumnProps={{
|
|
119
|
+
// hoverShow 情况下,默认 width 1
|
|
120
|
+
width: simpleOperateHoverShow ? 1 : undefined,
|
|
121
|
+
...props.operateColumnProps,
|
|
122
|
+
}}
|
|
107
123
|
/>
|
|
108
124
|
</div>
|
|
109
125
|
);
|
package/src/crud/style.scss
CHANGED
|
@@ -13,4 +13,33 @@
|
|
|
13
13
|
.ant-pro-table-list-toolbar {
|
|
14
14
|
border-bottom: 1px solid #f0f0f0;
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
&.fec-crud-of-simple-hover-show {
|
|
18
|
+
.ant-table-cell-fix-right {
|
|
19
|
+
position: absolute !important;
|
|
20
|
+
display: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.ant-table-row {
|
|
24
|
+
&:hover {
|
|
25
|
+
.ant-table-cell-fix-right {
|
|
26
|
+
display: block;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&.fec-crud-of-simple-search-width-full {
|
|
33
|
+
.ant-pro-table-list-toolbar-container {
|
|
34
|
+
justify-content: unset;
|
|
35
|
+
|
|
36
|
+
.ant-pro-table-list-toolbar-right {
|
|
37
|
+
justify-content: unset;
|
|
38
|
+
|
|
39
|
+
& > div {
|
|
40
|
+
flex: 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
16
45
|
}
|
package/src/crud/types.tsx
CHANGED
|
@@ -62,6 +62,10 @@ interface CRUDProps<DataSource = any, Key = string> {
|
|
|
62
62
|
readProps?: {
|
|
63
63
|
/** 文本 */
|
|
64
64
|
operateText?: string;
|
|
65
|
+
/** ”查看”是否禁用 */
|
|
66
|
+
operateIsDisabled?: (record: DataSource) => boolean;
|
|
67
|
+
/** ”查看”是否隐藏 */
|
|
68
|
+
operateIsHidden?: (record: DataSource) => boolean;
|
|
65
69
|
/** 打开方式, action 为 read_detail 有效 */
|
|
66
70
|
target?: '_blank';
|
|
67
71
|
/** 保存按钮文本 */
|
|
@@ -81,6 +85,8 @@ interface CRUDProps<DataSource = any, Key = string> {
|
|
|
81
85
|
operateText?: string;
|
|
82
86
|
/** ”编辑”是否禁用 */
|
|
83
87
|
operateIsDisabled?: (record: DataSource) => boolean;
|
|
88
|
+
/** ”编辑”是否隐藏 */
|
|
89
|
+
operateIsHidden?: (record: DataSource) => boolean;
|
|
84
90
|
/** 保存按钮文本 */
|
|
85
91
|
submitText?: string;
|
|
86
92
|
/** 重置按钮文本 */
|
|
@@ -99,6 +105,8 @@ interface CRUDProps<DataSource = any, Key = string> {
|
|
|
99
105
|
operateText?: string;
|
|
100
106
|
/** “删除”是否禁用 */
|
|
101
107
|
operateIsDisabled?: (record: DataSource) => boolean;
|
|
108
|
+
/** ”删除”是否隐藏 */
|
|
109
|
+
operateIsHidden?: (record: DataSource) => boolean;
|
|
102
110
|
/** 显示名称索引 */
|
|
103
111
|
nameIndex: keyof DataSource;
|
|
104
112
|
/** 删除确认描述 */
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { ProForm } from '@ant-design/pro-components';
|
|
2
2
|
import {
|
|
3
3
|
ProFormEditor,
|
|
4
|
+
ProFormImageUpload,
|
|
5
|
+
ProFormImageUploadDragger,
|
|
4
6
|
ProFormJSON,
|
|
5
7
|
ProFormJavascript,
|
|
6
8
|
ProFormListNumber,
|
|
7
9
|
ProFormListText,
|
|
8
10
|
ProFormSwitchNumber,
|
|
11
|
+
ProFormUpload,
|
|
12
|
+
ProFormUploadDragger,
|
|
9
13
|
} from '@fe-free/core';
|
|
10
14
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
11
15
|
import { useState } from 'react';
|
|
@@ -136,3 +140,97 @@ export const ProFormListNumberComponent: Story = {
|
|
|
136
140
|
</ProFormBase>
|
|
137
141
|
),
|
|
138
142
|
};
|
|
143
|
+
|
|
144
|
+
function customRequest(option: any) {
|
|
145
|
+
const { file, onProgress, onSuccess } = option;
|
|
146
|
+
|
|
147
|
+
// 模拟上传进度
|
|
148
|
+
let percent = 0;
|
|
149
|
+
const interval = setInterval(() => {
|
|
150
|
+
percent += 10;
|
|
151
|
+
onProgress({ percent });
|
|
152
|
+
|
|
153
|
+
if (percent >= 100) {
|
|
154
|
+
clearInterval(interval);
|
|
155
|
+
// 模拟上传成功
|
|
156
|
+
onSuccess({
|
|
157
|
+
data: {
|
|
158
|
+
url: `https://picsum.photos/200/300?random=${Date.now()}`,
|
|
159
|
+
name: file.name,
|
|
160
|
+
uid: file.uid,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}, 100);
|
|
165
|
+
|
|
166
|
+
// 返回 abort 方法,用于取消上传
|
|
167
|
+
return {
|
|
168
|
+
abort: () => {
|
|
169
|
+
clearInterval(interval);
|
|
170
|
+
console.log('上传已取消');
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const ProFormUploadComponent: Story = {
|
|
176
|
+
render: () => (
|
|
177
|
+
<ProFormBase>
|
|
178
|
+
<ProFormUpload label="file" name="file" fieldProps={{ customRequest }} />
|
|
179
|
+
<ProFormUploadDragger
|
|
180
|
+
label="file_dragger"
|
|
181
|
+
name="file_dragger"
|
|
182
|
+
fieldProps={{ customRequest }}
|
|
183
|
+
/>
|
|
184
|
+
<ProFormUploadDragger
|
|
185
|
+
label="files_dragger"
|
|
186
|
+
name="files_dragger"
|
|
187
|
+
fieldProps={{ multiple: true, maxCount: 2, customRequest }}
|
|
188
|
+
/>
|
|
189
|
+
<ProFormUpload
|
|
190
|
+
label="files"
|
|
191
|
+
name="files"
|
|
192
|
+
fieldProps={{ multiple: true, maxCount: 2, showCount: true, customRequest }}
|
|
193
|
+
/>
|
|
194
|
+
<ProFormUpload
|
|
195
|
+
label="files_picture"
|
|
196
|
+
name="files_picture"
|
|
197
|
+
fieldProps={{ multiple: true, maxCount: 2, listType: 'picture', customRequest }}
|
|
198
|
+
/>
|
|
199
|
+
<ProFormUpload
|
|
200
|
+
label="files_picture_card"
|
|
201
|
+
name="files_picture_card"
|
|
202
|
+
fieldProps={{ multiple: true, maxCount: 2, listType: 'picture-card', customRequest }}
|
|
203
|
+
/>
|
|
204
|
+
</ProFormBase>
|
|
205
|
+
),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const ProFormImageUploadComponent: Story = {
|
|
209
|
+
render: () => (
|
|
210
|
+
<ProFormBase>
|
|
211
|
+
<ProFormImageUpload
|
|
212
|
+
label="image"
|
|
213
|
+
name="image"
|
|
214
|
+
fieldProps={{
|
|
215
|
+
customRequest,
|
|
216
|
+
}}
|
|
217
|
+
/>
|
|
218
|
+
<ProFormImageUploadDragger
|
|
219
|
+
label="image_dragger"
|
|
220
|
+
name="image_dragger"
|
|
221
|
+
fieldProps={{
|
|
222
|
+
customRequest,
|
|
223
|
+
}}
|
|
224
|
+
/>
|
|
225
|
+
<ProFormImageUploadDragger
|
|
226
|
+
label="images_dragger"
|
|
227
|
+
name="images_dragger"
|
|
228
|
+
fieldProps={{
|
|
229
|
+
multiple: true,
|
|
230
|
+
maxCount: 2,
|
|
231
|
+
customRequest,
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
</ProFormBase>
|
|
235
|
+
),
|
|
236
|
+
};
|
package/src/form/index.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export { ProFormListNumber, ProFormListText } from './form_list/form_list';
|
|
2
2
|
export { ProFormListHelper } from './form_list/form_list_helper';
|
|
3
3
|
export { ProFormListModalHelper } from './form_list/form_list_modal_helper';
|
|
4
|
-
|
|
5
4
|
export { ProFormEditor } from './pro_form_editor';
|
|
6
5
|
export { ProFormJavascript } from './pro_form_javascript';
|
|
7
6
|
export { ProFormJSON } from './pro_form_json';
|
|
@@ -10,6 +9,12 @@ export {
|
|
|
10
9
|
SwitchNumber,
|
|
11
10
|
type SwitchNumberProps,
|
|
12
11
|
} from './pro_form_switch_number';
|
|
12
|
+
export {
|
|
13
|
+
ProFormImageUpload,
|
|
14
|
+
ProFormImageUploadDragger,
|
|
15
|
+
ProFormUpload,
|
|
16
|
+
ProFormUploadDragger,
|
|
17
|
+
} from './pro_form_upload';
|
|
13
18
|
|
|
14
19
|
import { pinyinMatch } from '@fe-free/tool';
|
|
15
20
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// 避免循环引用
|
|
2
|
+
import { ProForm, type ProFormItemProps } from '@ant-design/pro-components';
|
|
3
|
+
import type {
|
|
4
|
+
ImageUploadDraggerProps,
|
|
5
|
+
ImageUploadProps,
|
|
6
|
+
UploadDraggerProps,
|
|
7
|
+
UploadProps,
|
|
8
|
+
} from '../upload';
|
|
9
|
+
import { ImageUpload, ImageUploadDragger, Upload, UploadDragger } from '../upload';
|
|
10
|
+
|
|
11
|
+
function ProFormUpload(props: ProFormItemProps<UploadProps>) {
|
|
12
|
+
const { fieldProps, ...rest } = props;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<ProForm.Item {...rest}>
|
|
16
|
+
<Upload {...fieldProps} />
|
|
17
|
+
</ProForm.Item>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ProFormUploadDragger(props: ProFormItemProps<UploadDraggerProps>) {
|
|
22
|
+
const { fieldProps, ...rest } = props;
|
|
23
|
+
return (
|
|
24
|
+
<ProForm.Item {...rest}>
|
|
25
|
+
<UploadDragger {...fieldProps} />
|
|
26
|
+
</ProForm.Item>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ProFormImageUpload(props: ProFormItemProps<ImageUploadProps>) {
|
|
31
|
+
const { fieldProps, ...rest } = props;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<ProForm.Item {...rest}>
|
|
35
|
+
<ImageUpload {...fieldProps} />
|
|
36
|
+
</ProForm.Item>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ProFormImageUploadDragger(props: ProFormItemProps<ImageUploadDraggerProps>) {
|
|
41
|
+
const { fieldProps, ...rest } = props;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<ProForm.Item {...rest}>
|
|
45
|
+
<ImageUploadDragger {...fieldProps} />
|
|
46
|
+
</ProForm.Item>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { ProFormImageUpload, ProFormImageUploadDragger, ProFormUpload, ProFormUploadDragger };
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ export { EditorMention } from './editor_mention';
|
|
|
13
13
|
export type { EditorMentionProps } from './editor_mention';
|
|
14
14
|
export {
|
|
15
15
|
ProFormEditor,
|
|
16
|
+
ProFormImageUpload,
|
|
17
|
+
ProFormImageUploadDragger,
|
|
16
18
|
ProFormJSON,
|
|
17
19
|
ProFormJavascript,
|
|
18
20
|
ProFormListHelper,
|
|
@@ -20,9 +22,12 @@ export {
|
|
|
20
22
|
ProFormListNumber,
|
|
21
23
|
ProFormListText,
|
|
22
24
|
ProFormSwitchNumber,
|
|
25
|
+
ProFormUpload,
|
|
26
|
+
ProFormUploadDragger,
|
|
23
27
|
proFormSelectSearchProps,
|
|
24
28
|
} from './form';
|
|
25
29
|
export { Markdown } from './markdown';
|
|
26
30
|
export { Table } from './table';
|
|
27
31
|
export type { TableProps } from './table';
|
|
32
|
+
export { useLocalforageState } from './use_localforage_state';
|
|
28
33
|
export { CustomValueTypeEnum, customValueTypeMap } from './value_type_map';
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// 避免循环引用
|
|
2
|
+
import { InboxOutlined, PlusOutlined, UploadOutlined } from '@ant-design/icons';
|
|
3
|
+
import type { UploadProps as AntdUploadProps, UploadFile } from 'antd';
|
|
4
|
+
import { Upload as AntdUpload, Button, message } from 'antd';
|
|
5
|
+
import classNames from 'classnames';
|
|
6
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
interface UploadBaseProps {
|
|
9
|
+
value?: string[] | string;
|
|
10
|
+
onChange?: (value?: string[] | string) => void;
|
|
11
|
+
multiple?: boolean;
|
|
12
|
+
maxCount?: number;
|
|
13
|
+
action?: string;
|
|
14
|
+
customRequest?: AntdUploadProps['customRequest'];
|
|
15
|
+
listType?: AntdUploadProps['listType'];
|
|
16
|
+
accept?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface UploadProps extends UploadBaseProps {
|
|
20
|
+
showCount?: boolean;
|
|
21
|
+
}
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
23
|
+
interface UploadDraggerProps extends UploadBaseProps {}
|
|
24
|
+
|
|
25
|
+
function useUpload(props: ImageUploadProps) {
|
|
26
|
+
const { value, onChange, multiple, maxCount } = props;
|
|
27
|
+
// 转换成 Upload 格式。
|
|
28
|
+
const defaultFileList = useMemo(() => {
|
|
29
|
+
if (!value) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const arr = (multiple ? value : [value]) as string[];
|
|
34
|
+
return arr.map((url) => ({ uid: url, url, name: url?.split('/').pop() || '' })) as UploadFile[];
|
|
35
|
+
}, [multiple, value]);
|
|
36
|
+
|
|
37
|
+
// 存起来,已选的文件。以便做一些判断。
|
|
38
|
+
const [fileList, setFileList] = useState<UploadFile[]>(defaultFileList);
|
|
39
|
+
|
|
40
|
+
const handleChange = useCallback(
|
|
41
|
+
(info) => {
|
|
42
|
+
setFileList(info.fileList);
|
|
43
|
+
|
|
44
|
+
// 找到真正上传成功的。
|
|
45
|
+
const newValue = info.fileList
|
|
46
|
+
.map((item) => item.url || item.response?.data.url)
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
|
|
49
|
+
onChange?.(multiple ? newValue : newValue[0]);
|
|
50
|
+
},
|
|
51
|
+
[multiple, onChange],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// 选文件还是可能多选,如果多选,则提示。
|
|
55
|
+
const handleBeforeUpload = useCallback(
|
|
56
|
+
(f, fl) => {
|
|
57
|
+
// 多选 >1 情况下,超出的则提示。
|
|
58
|
+
if (multiple && maxCount && maxCount > 1) {
|
|
59
|
+
const index = fl.findIndex((item) => item.uid === f.uid);
|
|
60
|
+
if (index >= maxCount - fileList.length) {
|
|
61
|
+
message.warning(`最多只能上传 ${maxCount} 个文件,超出部分会忽略。`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
},
|
|
67
|
+
[fileList, multiple, maxCount],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// 多选情况下,超出则上传按钮 disabled
|
|
71
|
+
const isDisabled = useMemo(() => {
|
|
72
|
+
if (multiple && maxCount && maxCount > 1) {
|
|
73
|
+
return fileList.length >= maxCount;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}, [fileList.length, maxCount, multiple]);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
onChange: handleChange,
|
|
80
|
+
beforeUpload: handleBeforeUpload,
|
|
81
|
+
isDisabled,
|
|
82
|
+
fileList,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function Upload(props: ImageUploadProps) {
|
|
87
|
+
const { multiple, maxCount, showCount, action, customRequest, listType, accept } = props;
|
|
88
|
+
const { onChange, beforeUpload, isDisabled, fileList } = useUpload(props);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<AntdUpload
|
|
92
|
+
action={action}
|
|
93
|
+
customRequest={customRequest}
|
|
94
|
+
onChange={onChange}
|
|
95
|
+
accept={accept}
|
|
96
|
+
listType={listType}
|
|
97
|
+
defaultFileList={fileList}
|
|
98
|
+
maxCount={multiple ? maxCount : 1}
|
|
99
|
+
multiple={multiple}
|
|
100
|
+
beforeUpload={beforeUpload}
|
|
101
|
+
// 不可,否则会没法删除
|
|
102
|
+
// disabled={isDisabled}
|
|
103
|
+
>
|
|
104
|
+
{listType === 'picture-card' ? (
|
|
105
|
+
<button style={{ border: 0, background: 'none' }} type="button" disabled={isDisabled}>
|
|
106
|
+
<PlusOutlined />
|
|
107
|
+
<div style={{ marginTop: 8 }}>本地上传</div>
|
|
108
|
+
</button>
|
|
109
|
+
) : (
|
|
110
|
+
<Button icon={<UploadOutlined />} disabled={isDisabled}>
|
|
111
|
+
本地上传{showCount && multiple ? `(${fileList.length}/${maxCount})` : ''}
|
|
112
|
+
</Button>
|
|
113
|
+
)}
|
|
114
|
+
</AntdUpload>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function UploadDragger(props: ImageUploadDraggerProps) {
|
|
119
|
+
const { multiple, maxCount, action, customRequest, listType, accept } = props;
|
|
120
|
+
const { onChange, beforeUpload, isDisabled, fileList } = useUpload(props);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<AntdUpload.Dragger
|
|
124
|
+
action={action}
|
|
125
|
+
customRequest={customRequest}
|
|
126
|
+
onChange={onChange}
|
|
127
|
+
accept={accept}
|
|
128
|
+
listType={listType}
|
|
129
|
+
defaultFileList={fileList}
|
|
130
|
+
maxCount={multiple ? maxCount : 1}
|
|
131
|
+
multiple={multiple}
|
|
132
|
+
beforeUpload={beforeUpload}
|
|
133
|
+
// 不可,否则会没法删除
|
|
134
|
+
// disabled={isDisabled}
|
|
135
|
+
>
|
|
136
|
+
<div
|
|
137
|
+
className={classNames({
|
|
138
|
+
'cursor-not-allowed': isDisabled,
|
|
139
|
+
})}
|
|
140
|
+
>
|
|
141
|
+
<p className={classNames('ant-upload-drag-icon')}>
|
|
142
|
+
<InboxOutlined
|
|
143
|
+
className={classNames({
|
|
144
|
+
'!text-desc': isDisabled,
|
|
145
|
+
})}
|
|
146
|
+
/>
|
|
147
|
+
</p>
|
|
148
|
+
<p
|
|
149
|
+
className={classNames('ant-upload-text', {
|
|
150
|
+
'!text-desc': isDisabled,
|
|
151
|
+
})}
|
|
152
|
+
>
|
|
153
|
+
点击或拖拽到此区域进行上传
|
|
154
|
+
</p>
|
|
155
|
+
</div>
|
|
156
|
+
</AntdUpload.Dragger>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
161
|
+
interface ImageUploadProps extends UploadProps {}
|
|
162
|
+
function ImageUpload(props: ImageUploadProps) {
|
|
163
|
+
return <Upload {...props} accept="image/*" listType="picture" />;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
167
|
+
interface ImageUploadDraggerProps extends UploadProps {}
|
|
168
|
+
function ImageUploadDragger(props: ImageUploadDraggerProps) {
|
|
169
|
+
return <UploadDragger {...props} accept="image/*" listType="picture" />;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { ImageUpload, ImageUploadDragger, Upload, UploadDragger };
|
|
173
|
+
export type { ImageUploadDraggerProps, ImageUploadProps, UploadDraggerProps, UploadProps };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import localforage from 'localforage';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
const useLocalforageState = <T = any,>(
|
|
5
|
+
key: string,
|
|
6
|
+
options: {
|
|
7
|
+
defaultValue?: T;
|
|
8
|
+
},
|
|
9
|
+
): [T | undefined, (value: T) => void, boolean] => {
|
|
10
|
+
const [ready, setReady] = useState(false);
|
|
11
|
+
const [value, setValue] = useState<T | undefined>(options.defaultValue);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
localforage.getItem(key).then((v) => {
|
|
15
|
+
if (v !== undefined) {
|
|
16
|
+
setValue(v as T);
|
|
17
|
+
}
|
|
18
|
+
setReady(true);
|
|
19
|
+
});
|
|
20
|
+
}, [key]);
|
|
21
|
+
|
|
22
|
+
const setValueAndSave = (v: T) => {
|
|
23
|
+
setValue(v);
|
|
24
|
+
localforage.setItem(key, v);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return [value, setValueAndSave, ready];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { useLocalforageState };
|