@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 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.0.6",
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.0.6"
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
- btns.push(
116
- <CRUDDetail
117
- key="read"
118
- id={record[idField]}
119
- record={record}
120
- onSuccess={handleReload}
121
- trigger={<a>{readProps?.operateText || '查看'}</a>}
122
- action="read"
123
- {...detailProps}
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
- btns.push(
130
- <Link
131
- key="read_detail"
132
- to={`./detail/${record[detailIdIndex || 'id']}`}
133
- target={readProps?.target}
134
- >
135
- {readProps?.operateText || '查看'}
136
- </Link>,
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 disabled = updateProps?.operateIsDisabled?.(record) || false;
166
+ const hidden = updateProps?.operateIsHidden?.(record) || false;
167
+ if (!hidden) {
168
+ const disabled = updateProps?.operateIsDisabled?.(record) || false;
142
169
 
143
- if (disabled) {
144
- btns.push(
145
- <span key="update" className="text-desc cursor-not-allowed">
146
- {updateProps?.operateText || '编辑'}
147
- </span>,
148
- );
149
- } else {
150
- btns.push(
151
- <CRUDDetail
152
- key="update"
153
- id={record[idField]}
154
- record={record}
155
- onSuccess={handleReload}
156
- trigger={<a>{updateProps?.operateText || '编辑'}</a>}
157
- action="update"
158
- {...detailProps}
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 disabled = deleteProps?.operateIsDisabled?.(record) || false;
166
- btns.push(
167
- <OperateDelete
168
- key="delete"
169
- name={record[deleteProps.nameIndex]}
170
- desc={deleteProps.desc}
171
- operateText={deleteProps.operateText}
172
- disabled={disabled}
173
- onDelete={getHandleDelete(record)}
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="fec-crud">
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: 'id',
21
- dataIndex: 'id',
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 WithSearch: Story = {
100
+ export const HoverShow: Story = {
62
101
  render: () => {
63
102
  const columns = [
64
103
  {
65
- title: 'id',
66
- dataIndex: 'id',
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={['create', 'delete']}
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: 'id',
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
- <div key="search">
65
- {simpleSearchProps && (
66
- <SearchRender
67
- placeholder={simpleSearchProps.placeholder}
68
- value={searchValue}
69
- onChange={(value) => setSearchValue(value)}
70
- />
71
- )}
72
- </div>,
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 className="fec-crud-of-simple">
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
  );
@@ -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
  }
@@ -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
+ };
@@ -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 };